PICOCTF : asm4 (Reverse Engineering) writeup
ลิงค์โจทย์ครับ!!! : https://play.picoctf.org/practice/challenge/5?category=3&originalEvent=1&page=2
สวัสดีครับ! สำหรับโจทย์ข้อนี้ เค้าจะให้ตัว Assembly source x86 มานะครับ โดยเค้าบอกว่า function asm4 เรียกใช้ด้วย agrument “picoCTF_724a2” แล้วถามหาค่าที่ถูก returnกลับมา ในรูปแบบของ 0x…
ก่อนจะไปอ่านกัน ผมขอบอกก่อนว่า writeupนี้ มีวิธี 2 วิธี หากใครต้องการความรู้เน้นๆ ผมแนะนำให้อ่านวิธีที่ 2 ที่อยู่เกือบล่างสุดนะครับ! (เขียนนานมากกก เพราะผมอยากให้ได้ความรู้กันจริงๆ 😭)
เอาละครับ เรามาเริ่มกันเลย!!!
อันดับแรก เค้าให้ไฟล์นี้เรามา
asm4:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: push ebx
<+4>: sub esp,0x10
<+7>: mov DWORD PTR [ebp-0x10],0x252
<+14>: mov DWORD PTR [ebp-0xc],0x0
<+21>: jmp 0x518 <asm4+27>
<+23>: add DWORD PTR [ebp-0xc],0x1
<+27>: mov edx,DWORD PTR [ebp-0xc]
<+30>: mov eax,DWORD PTR [ebp+0x8]
<+33>: add eax,edx
<+35>: movzx eax,BYTE PTR [eax]
<+38>: test al,al
<+40>: jne 0x514 <asm4+23>
<+42>: mov DWORD PTR [ebp-0x8],0x1
<+49>: jmp 0x587 <asm4+138>
<+51>: mov edx,DWORD PTR [ebp-0x8]
<+54>: mov eax,DWORD PTR [ebp+0x8]
<+57>: add eax,edx
<+59>: movzx eax,BYTE PTR [eax]
<+62>: movsx edx,al
<+65>: mov eax,DWORD PTR [ebp-0x8]
<+68>: lea ecx,[eax-0x1]
<+71>: mov eax,DWORD PTR [ebp+0x8]
<+74>: add eax,ecx
<+76>: movzx eax,BYTE PTR [eax]
<+79>: movsx eax,al
<+82>: sub edx,eax
<+84>: mov eax,edx
<+86>: mov edx,eax
<+88>: mov eax,DWORD PTR [ebp-0x10]
<+91>: lea ebx,[edx+eax*1]
<+94>: mov eax,DWORD PTR [ebp-0x8]
<+97>: lea edx,[eax+0x1]
<+100>: mov eax,DWORD PTR [ebp+0x8]
<+103>: add eax,edx
<+105>: movzx eax,BYTE PTR [eax]
<+108>: movsx edx,al
<+111>: mov ecx,DWORD PTR [ebp-0x8]
<+114>: mov eax,DWORD PTR [ebp+0x8]
<+117>: add eax,ecx
<+119>: movzx eax,BYTE PTR [eax]
<+122>: movsx eax,al
<+125>: sub edx,eax
<+127>: mov eax,edx
<+129>: add eax,ebx
<+131>: mov DWORD PTR [ebp-0x10],eax
<+134>: add DWORD PTR [ebp-0x8],0x1
<+138>: mov eax,DWORD PTR [ebp-0xc]
<+141>: sub eax,0x1
<+144>: cmp DWORD PTR [ebp-0x8],eax
<+147>: jl 0x530 <asm4+51>
<+149>: mov eax,DWORD PTR [ebp-0x10]
<+152>: add esp,0x10
<+155>: pop ebx
<+156>: pop ebp
<+157>: ret
โดยบอกว่าไฟล์นี้เป็นฟังก์ชั่นของ asm4
เพื่อความง่ายในการ analyse ผมจึงเขียน Graph คร่าวๆของฟังก์ชั่นตัวนี้ครับได้เป็นรูปด้วยล่างนี้เลย
เราจะสังเกตเห็นว่ามันมีloopอยู่ 2จุด ถ้าสมมุติเราลอง manual คำนวนไปเรื่อยๆไล่ตาม instruction ไปเรื่อยๆ เราก็สามารถหา flag ได้เลย แต่ใครจะลองทำล่ะ? ถูกไหม
แน่นอนครับ ผมนี่แหละคนนึงที่ลองทำ 5555
วิธีที่ 1 : C asm function solution
โกงไหมไม่รู้นะ เอาไว้ manual reverse เป็นวิธีที่ 2 นะครับ ไม่งั้นจะไม่จบกันสักที5555
เขียนให้ asm4
รับ parameter เป็น char pointer และไปเรียกใช้ asm อีกที โดยวิธีการเขียน function asm ก็สามารถอ่านได้เต็มๆในนี้เลยครับ แต่ผมจะขออธิบายคร่าวๆพอนะ
#include <stdio.h>
#include <stdlib.h>
int asm4(char * input){
int val;
asm(
"asmcode1"
"asmcode2"
: "=r" (val)
: [input_asm]"m" (input)
);
return val;
}
int main()
{
printf("0x%x",asm4("picoCTF_724a2"));
return 0;
}
ตามโค้ดด้านบน ผมร่างตัวโค้ดคร่าวๆเสร็จแล้ว เดี๋ยวผมจะอธิบายทีละขั้นตอนว่า มันเป็นยังไงบ้าง
- สร้างฟังก์ชั่น
asm4
โดยให้parameter เป็นchar
- โดยกำหนดตัวแปร
val
ให้ type เป็น integer - เรียกใช้ assembly โดยตรงโดยให้โค้ด Assembly เป็น asmcode1,asmcode2 เรียงลงไปเรื่อยๆ
- output ของการเรียกใช้จะถูกเก็บไว้ใน
val
และ input ที่เป็น parameter ของฟังก์ชั่นจะนำเก็บเอาไว้ในinput_asm
โอเค คร่าวๆประมาณนี้ครับ
เพื่อใครสงสัยนะครับว่า =r
และ m
ที่เป็น string ที่ได้ใส่เข้าไปใน input,output parameter ของฟังชั่น asm
=r
มีหน้าที่แยกกันนะครับ=
สำหรับบอกว่าเป็น output และr
เพื่อให้ compiler เอาตัว General purpose registers(พวก eax,ebx,…) ที่ควรจะส่งค่า return ให้ส่งค่า return กลับมาที่ variable ที่เลือกไว้m
เพื่อบอกว่า ค่าที่เราจะใส่เข้าไปเป็น address ของ memory (เพราะว่า char เป็น pointer ที่ชี้ไปยัง string ที่เก็บไว้ใน memory ครับ)
หลังจากนั้น เราก็มาแก้code assembly ให้สามารถนำไปใส่ในฟังก์ชั่น asm
ได้ครับ
โดยวิธีการคร่าวๆมีประมาณนี้
- กำหนด
label
สำหรับการ jmp ของ loop ที่ asm4+23,27,51,138 ก่อน - เปลี่ยนพวก jmp address ให้เป็น jmp label ให้หมด
- เปลี่ยน
ebp+0x8
ให้เป็น%[input_asm]
จากโค้ดที่เราเขียนไว้ด้านบนเลยครับ
(ถามว่าผมรู้ได้ยังไงหรอว่ามันคือ input ของ function? แน่นอนครับ ประสบการณ์ 55555)
ล้อเล่นๆ เดี๋ยวผมอธิบายต่อด้านล่างนะครับ - ลบในส่วนของ
push ebp
,mov ebp,esp
ออก เพราะว่าตรงส่วนนี้ คือส่วนที่จะ setup stack สำหรับการเรียกใช้ฟังก์ชั่น แต่ว่าในเคสนี้ เราสร้างฟังก์ใหม่แล้ว และแค่ exec assembly instruction เฉยๆ - ลบในส่วนของ
pop ebp
,ret
ออก เพราะว่าตรงส่วนนี้ คือส่วนที่จะ de-setup stack เอาง่ายๆก็คือ พอเรา jmp เข้าฟังชั่นก็จะต้องมี stack เอาไว้ลองรับตัว local variable ใช่มะ และพอเราใช้เสร็จ แล้วก็แค่ ลบออก แต่ในที่นี้เหมือนข้างบนเลย เราอยู่ใน function ที่ถูกเรียกใช้อยู่แล้ว ทำให้ไม่ต้องไปจัดการอะไรเพิ่มเติมครับ
อธิบายสำหรับคนที่ต้องการรู้ว่า
ebp+0x8
ทำไมถึงเป็น input
ภาพด้านบนนี้คือ stack ของ โค้ดก่อนเข้า asm4
นะครับ เพราะว่าเราไม่ได้กำหนดตัวแปรให้กับ picoCTF_724a2
แต่นำไปใช้กับฟังก์ชั่นตรงๆเลย ทำให้ string นี้ไปอยู่ที่ top stack หรือตรงที่ esp
point ไป
แล้วหลังจากที่เรียกใช้ฟังก์ชั่นก็จะมีการ set upstack เล็กๆน้อยๆทำให้ esp,ebp ขยับขึ้นไปเล็กน้อยตามนี้ครับ
เพราะฉนั้น ในตำแหน่ง ebp
ที่จะไม่เคลื่อนไปมากกว่านี้ เพราะว่ามันคือ base pointer
จึงสามารถ reference ไปหา string ที่อยู่ใน stack ก่อนหน้านี้ได้ เป็น 0x61FF10-0x61FF08
= 0x8
นั้นแหละครับทำไมถึงเป็น ebp+0x8
จบครับ เรากลับมาต่อเนื้อหาหลักกันต่อ!
เราก็จะได้โค้ดนี้ครับ!
#include <stdio.h>
#include <stdlib.h>
int asm4(char * input){
int val;
asm(
// "push ebp"
// "mov ebp,esp"
"push ebx;"
"sub esp,0x10;"
"mov DWORD PTR [ebp-0x10],0x252;"
"mov DWORD PTR [ebp-0xc],0x0;"
"jmp asm_27;"
"asm_23:"
add DWORD PTR [ebp-0xc],0x1;"
"asm_27:"
"mov edx,DWORD PTR [ebp-0xc];"
"mov eax,DWORD PTR [%[input_asm]];"
"add eax,edx;"
"movzx eax,BYTE PTR [eax];"
"test al,al;"
"jne asm_23;"
"mov DWORD PTR [ebp-0x8],0x1;"
"jmp asm_138;"
"asm_51:"
"mov edx,DWORD PTR [ebp-0x8];"
"mov eax,DWORD PTR [%[input_asm]];"
"add eax,edx;"
"movzx eax,BYTE PTR [eax];"
"movsx edx,al;"
"mov eax,DWORD PTR [ebp-0x8];"
"lea ecx,[eax-0x1];"
"mov eax,DWORD PTR [%[input_asm]];"
"add eax,ecx;"
"movzx eax,BYTE PTR [eax];"
"movsx eax,al;"
"sub edx,eax;"
"mov eax,edx;"
"mov edx,eax;"
"mov eax,DWORD PTR [ebp-0x10];"
"lea ebx,[edx+eax*1];"
"mov eax,DWORD PTR [ebp-0x8];"
"lea edx,[eax+0x1];"
"mov eax,DWORD PTR [%[input_asm]];"
"add eax,edx;"
"movzx eax,BYTE PTR [eax];"
"movsx edx,al;"
"mov ecx,DWORD PTR [ebp-0x8];"
"mov eax,DWORD PTR [%[input_asm]];"
"add eax,ecx;"
"movzx eax,BYTE PTR [eax];"
"movsx eax,al;"
"sub edx,eax;"
"mov eax,edx;"
"add eax,ebx;"
"mov DWORD PTR [ebp-0x10],eax;"
"add DWORD PTR [ebp-0x8],0x1;"
"asm_138:"
"mov eax,DWORD PTR [ebp-0xc];"
"sub eax,0x1;"
"cmp DWORD PTR [ebp-0x8],eax;"
"jl asm_51;"
"mov eax,DWORD PTR [ebp-0x10];"
"add esp,0x10;"
: "=r" (val)
: [input_asm]"m" (input)
);
return val;
}
int main()
{
printf("0x%x",asm4("picoCTF_724a2"));
return 0;
}
ครับผม ทีนี้เราก็ compile เลย!
เอ๋?? ทำไม compile ไม่ติดล่ะ
เพราะว่าเราไม่ได้ใส่ flag อีกตัวครับ เราจำเป็นต้องใส่ -masm=intel
ลงไปด้วย เพราะว่าตอนนี้เราใช้ att ที่เป็น default อยู่แต่ assembly ที่เราเขียน เราใช้ syntax index ครับ
ทีนี้เราก็แค่รัน เราก็ได้ flag แล้วครับ คนที่537 เย่!
วิธีที่ 2 : Manual Reverse engineering
ครับ ตามชื่อครับ เรามาเริ่มกันเลยดีกว่า
เริ่มจากการ analyse loop แรกก่อนนะครับ
เราจะเห็นได้อย่างชัดเจนเลยนะครับว่า มันจะนำ input
ของเราไปใส่ไว้ใน eax และ ebp-0xc
ไปไว้ใน edx
หลังจากนั้นก็ บวก input
pointer ไป edx
เอาง่ายๆก็คือ index character
ที่ละตัวครับ
หลังจากนั้นให้เช็คว่า test al al
หรือก็คือ 1 byte แรกสุด (Little Endian) ที่เก็บไว้ใน eax
(ตัวอักษรตัวสุดท้าย) เป็น NULL BYTE (\x00)
หรือไม่ ถ้าไม่ใช่ก็ให้ Loop โดยเพิ่มค่า ebp-0xc
ไปอีก 1
เอาง่ายๆเลย ebp-0xc
ก็คือตัว Len character (นับตัวอักษร) ดีๆนี่เอง
สุดท้ายแล้ว เราจะรู้ว่า edx
ที่เก็บ ebp-0xc
ไว้จะมีค่าเป็น 0xD
หรือก็คือ 13
นั่นเองครับ
ต่อมาเรามาดูในส่วนที่ 2 ครับ
ดูแค่คร่าวๆพอครับ เราจะรู้ว่ามันเป็นเหมือนกับ for loop อีกแล้ว โดยมันจะลูปตั้งแต่ 1 ถึง [ebp-0xc]-1
ทีนี้เรามาดูในโค้ดการทำงานหลักกันครับ
โอ้ววว เยอะแยะมากเลยใช่ไหมครับ ผมก็นั่งดูจะตาลายเลยแหละ 555
ถ้างั้น ผมจะสรุปให้อ่านนะครับ
- เริ่มจาก 51, 54, 57 เป็นการเตรียม pointer char ที่ชี้ไปที่ตัวอักษรตัวที่ i
- 59, 62 เตรียม text เป็น byte
- 65, 68, 71, 74 เตรียม pointer char ที่ชี้ไปที่ตัวอักษรตัวที่ i — 1
- 76, 79 เตรียม text เป็น byte
- 82 ลบกันแล้วเก็บไว้ที่
edx
(แบบ ascii) - 84, 86
eax = edx
,edx = eax
- 88, 91 เตรียมค่า
ebp-0x10 + edx
ก็คือตัวเลขปริศนา(val
) กับedx
เป็นval + text[i] — text[i — 1]
แล้วเก็บไว้ที่ebx
- 94,97 เตรียมค่า i + 1 สำหรับลูปในครั้งต่อไป โดยเก็บเอาไว้ที่
edx
- 100, 103 เป็นการเตรียม pointer char ที่ชี้ไปที่ตัวอักษรตัวที่ i + 1
- 105, 108 เตรียม text เป็น byte
- 111, 114, 117 เป็นการเตรียม pointer char ที่ชี้ไปที่ตัวอักษรตัวที่ i
- 119, 122 เตรียม text เป็น byte
- 125 ลบกันแล้วเก็บไว้ที่
edx
(แบบ ascii) - 127
eax = edx
- 129 นำ
eax
ที่เป็นค่าที่ลบกันล่าสุด ไปรวมกับebx
ที่เตรียมเอาไว้ สรุปได้เป็นval + text[i] — text[i — 1] + text[i+1] -text[i]
และนำไปเก็บไว้ที่eax
- 131 นำ
eax
ไปเก็บไว้เป็นval
- 134 ทำให้
ebp — 0x8
= 0x1
ประมาณนี้ครับ ตอนนี้เราเข้าใจการทำงานแล้ว ก็เขียน python script เลยย
def asm4(a1):
v2 = 0x252
i = 0xD
for j in range(1, i - 1):
v2 += ord(a1[j]) - ord(a1[j - 1]) + ord(a1[j + 1]) - ord(a1[j])
return v2
print(asm4("picoCTF_724a2"))
จบครับ แยกย้าย
สรุปสำหรับวันนี้นะครับ อย่าหาอ่าน Assembly นานๆครับ เดี๋ยวสายตาเสีย 5555