PICOCTF : asm4 (Reverse Engineering) writeup

Natsuiro XCN
6 min readOct 3, 2023

--

ลิงค์โจทย์ครับ!!! : 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 คร่าวๆของฟังก์ชั่นตัวนี้ครับได้เป็นรูปด้วยล่างนี้เลย

graph of function asm4
graph of function asm4

เราจะสังเกตเห็นว่ามันมีloopอยู่ 2จุด ถ้าสมมุติเราลอง manual คำนวนไปเรื่อยๆไล่ตาม instruction ไปเรื่อยๆ เราก็สามารถหา flag ได้เลย แต่ใครจะลองทำล่ะ? ถูกไหม

แน่นอนครับ ผมนี่แหละคนนึงที่ลองทำ 5555

วิธีที่ 1 : C asm function solution

โกงไหมไม่รู้นะ เอาไว้ manual reverse เป็นวิธีที่ 2 นะครับ ไม่งั้นจะไม่จบกันสักที5555

m.c

เขียนให้ 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;
}

ตามโค้ดด้านบน ผมร่างตัวโค้ดคร่าวๆเสร็จแล้ว เดี๋ยวผมจะอธิบายทีละขั้นตอนว่า มันเป็นยังไงบ้าง

  1. สร้างฟังก์ชั่น asm4 โดยให้parameter เป็น char
  2. โดยกำหนดตัวแปร val ให้ type เป็น integer
  3. เรียกใช้ assembly โดยตรงโดยให้โค้ด Assembly เป็น asmcode1,asmcode2 เรียงลงไปเรื่อยๆ
  4. 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 เลย!

gcc -m32 m.c -o a.exe

เอ๋?? ทำไม compile ไม่ติดล่ะ
เพราะว่าเราไม่ได้ใส่ flag อีกตัวครับ เราจำเป็นต้องใส่ -masm=intel ลงไปด้วย เพราะว่าตอนนี้เราใช้ att ที่เป็น default อยู่แต่ assembly ที่เราเขียน เราใช้ syntax index ครับ

gcc -masm=intel -m32 m.c -o a.exe

ทีนี้เราก็แค่รัน เราก็ได้ flag แล้วครับ คนที่537 เย่!

วิธีที่ 2 : Manual Reverse engineering

ครับ ตามชื่อครับ เรามาเริ่มกันเลยดีกว่า

เริ่มจากการ analyse loop แรกก่อนนะครับ

first loop in asm4

เราจะเห็นได้อย่างชัดเจนเลยนะครับว่า มันจะนำ 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 ครับ

loop 2 asm4

ดูแค่คร่าวๆพอครับ เราจะรู้ว่ามันเป็นเหมือนกับ for loop อีกแล้ว โดยมันจะลูปตั้งแต่ 1 ถึง [ebp-0xc]-1 ทีนี้เรามาดูในโค้ดการทำงานหลักกันครับ

loop 2 asm4 p.2

โอ้ววว เยอะแยะมากเลยใช่ไหมครับ ผมก็นั่งดูจะตาลายเลยแหละ 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

--

--