GDB คือ debugger โดยปกติ programmer จะใช้สำหรับช่วยในการแก้ bug ด้วยการดูค่าของตัวแปรต่างๆ ที่บรรทัดต่างๆ ของ code แต่ในมุมมองของ hacker ตัว debugger นั้น ช่วยให้เข้าใจโปรแกรม และช่องโหว่ของโปรแกรม และเราจะได้ใช้ gdb ไปตลอด tutorial นี้ โดยหัวข้อนี้ ผมตั้งใจให้คนที่ไม่เคยใช้ gdb ได้เห็นคำสั่งต่างๆ และได้ลองนิดหน่อย (ไม่ต้องให้คล่องนะครับ ต้องได้ใช้อีกเยอะ เดี๋ยวก็จำได้เอง)
ก่อนจะเริ่ม debug ก็ต้องสั่ง gdb แล้วเราจะเข้าไปในอยู่ใน gdb แล้วสั่งคำสั่งเพื่อตรวจสอบ process ได้ โดย gdb มีรูปแบบของ parameter ที่สำคัญตามนี้
# debug โปรแกรม prog $ gdb ./prog # ตรวจสอบ core dump file (มีหลายรูปแบบ) $ gdb ./prog core $ gdb -c core ./prog $ gdb -c core # ส่งโปรแกรม prog arguments เข้าไปด้วย (มีหลายรูปแบบ) $ gdb --args ./prog arg1 arg2 # attach เข้าไปใน process ที่ run อยู่ (สมมติว่า pid คือ 1234) (มีหลายรูปแบบ) $ gdb ./prog 1234 $ gdb -p 1234
ส่วน parameter อื่นๆ ให้หาอ่านเองนะครับ ง่ายสุดก็ man gdb
หลังจากสั่ง gdb ก็จะเจอ gdb prompt โดยมีคำสั่งต่างๆ ที่ใช้บ่อยๆ สำหรับการเขียน exploit ตามนี้ (ADDR ในตารางข้างล่าง สามารถใช้ register แทนได้เช่น $eax, $esp)
คำสั่งเต็ม | คำสั่งย่อ | คำอธิบาย |
---|---|---|
run | r | เริ่มโปรแกรม |
kill | k | หยุดโปรแกรม |
quit | q | ออกจาก GDB |
continue | c | ทำงานต่อโดยหยุดที่ breakpoint ถัดไป |
disassemble | disas | แสดง assembly code ของ function ที่ EIP อยู่ |
disassemble ADDR | disas ADDR | แสดง assembly code ที่ address ADDR (ใช้ชื่อ function ได้) |
disassemble ADDR1 ADDR2 | disas ADDR1 ADDR2 | แสดง assembly code ที่ address ADDR1 ถึง ADDR2 |
info breakpoints | i b | แสดง breakpoint ทั้งหมด |
info registers | i r | แสดงค่าของ CPU registers ทั้งหมด |
info frame | i f | แสดงข้อมูลเกี่ยวกับ stack frame ปัจจุบัน |
backtrace | bt | แสดง call stack |
break *ADDR | b *ADDR | set breakpoint ที่ address ADDR (ถ้าใช้ชื่อ function ไม่ต้องมี *) |
enable [NUM] | en [NUM] | enable breakpoint หมายเลขที่ NUM |
disable [NUM] | dis [NUM] | disable breakpoint หมายเลขที่ NUM |
delete [NUM] | d [NUM] | delete breakpoint หมายเลขที่ NUM |
delete | d | delete breakpoint ทั้งหมด |
nexti [num] | ni [num] | ทำงานคำสั่งถัดไป ไม่เข้าไปใน call |
stepi [num] | si [num] | ทำงานคำสั่งถัดไป เข้าไปใน call |
x/NFU ADDR | แสดงค่าของ address ADDR โดย N คือจำนวนที่จะแสดงผล F คือรูปแบบที่จะแสดงผล (ดูตารางถัดไป) U คือจำนวน byte มี b (byte), h (2 bytes), w (4 bytes), g (8 bytes) | |
display/F ADDR | disp/F ADDR | แสดงค่าของ address ADDR ทุกครั้งที่ถึงหยุดทำงานชั่วคราว |
display | disp | แสดงค่าที่อยู่ใน display list ทั้งหมด |
undisplay [NUM] | und [NUM] | ลบ display ที่เก็บไว้ที่ NUM |
set ADDR=VAL | set ค่า VAL ไปที่ address ADDR |
ต่อไปก็รูปแบบการแสดงผล (ค่า F จากตารางข้างบน) จะเหมือน C เกือบหมด
รูปแบบ | คำอธิบาย |
---|---|
a | pointer |
c | character |
d | signed decimal |
f | floating point number |
o | octal |
s | string |
t | binary |
u | unsigned decimal |
x | hexadecimal |
คำสั่งตั้งเยอะ ใครจะจำได้หมด ต้องลองใช้บ่อยๆ ให้มันซึมเข้าไปเองครับ โดยผมจะลองใช้คำสั่งต่างๆ กับโปรแกรมในหัวข้อ "Buffer Overflow คืออะไร" แต่ให้ compile ตามนี้ (ex_05_1.c)
$ gcc -fno-pie -fno-stack-protector -z norelro -z execstack -mpreferred-stack-boundary=2 -o ex_05_1 ex_05_1.c
หลังจากนั้น มาลองใช้ gdb กัน (ให้ลองทำตามด้วยนะครับ อย่าเอาแต่อ่าน) โดยผมจะใส่คำอธิบายไว้หลังเครื่องหมาย # (ไม่ต้องพิมพ์นะครับ คำอธิบายนะครับ) และตามสัญญาจากหัวข้อที่แล้ว ว่าจะให้เห็นการส่งผ่าน argument อีกรูปหนึ่ง (สำหรับคนที่ไม่ชอบ AT&T syntax สามารถใช้คำสั่ง set disassembly-flavor intel เพื่อให้เป็น MASM syntax แต่ผมแนะนำให้ใช้ default เพื่อที่จะได้รู้หลากหลาย)
$ gdb -q ./ex_05_1 Reading symbols from /home/worawit/tutz/ch05/ex_05_1...(no debugging symbols found)...done. (gdb) disas main # disassemble main 0x08048434 <+0>: push %ebp 0x08048435 <+1>: mov %esp,%ebp 0x08048437 <+3>: sub $0x14,%esp # หัวข้อที่แล้ว -0xc แต่คราวนี้ -0x14 เพิ่มมา 8 bytes ใช้สำหรับส่ง argument ให้ strcpy 0x0804843a <+6>: movl $0x0,-0x4(%ebp) ... # ขอละไว้ มันยาว 0x08048455 <+33>: mov 0xc(%ebp),%eax # เอา argument ตัวที่ 2 (argv) ไปที่ 0x08048458 <+36>: add $0x4,%eax # eax+4 เพื่อชี้ไปที่ address ของ argv[1] 0x0804845b <+39>: mov (%eax),%eax # เอาค่าของ argv[1] เก็บใน eax 0x0804845d <+41>: mov %eax,0x4(%esp) # เก็บไปไว้ที่ esp+4 (เป็น argument ตัวที่ 2 ของ strcpy) 0x08048461 <+45>: lea -0xc(%ebp),%eax # โหลด address ของ buf ไว้ที่ eax 0x08048464 <+48>: mov %eax,(%esp) # เก็บไปไว้ที่ esp (เป็น argument ตัวที่ 1 ของ strcpy) 0x08048467 <+51>: call 0x8048344 <strcpy@plt> ... # ขอละไว้ มันยาว (gdb) b main # set breakpoint ไว้ที่ main Breakpoint 1 at 0x804843a # สังเกตว่า set ที่หลัง function prologue (gdb) r Starting program: /home/worawit/tutz/ch05/ex_05_1 Breakpoint 1, 0x0804843a in main () (gdb) b *0x08048467 # set breakpoint ที่คำสั่ง call strcpy Breakpoint 2 at 0x8048467 (gdb) r UUUUUUUUUUUUUUUUUUU # run โปรแกรมอีกรอบ โดยมี argument The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/worawit/tutz/ch05/ex_05_1 UUUUUUUUUUUUUUUUUUU Breakpoint 1, 0x0804843a in main () (gdb) i r # แสดง registers ทั้งหมด eax 0xbffff7b4 -1073743948 ecx 0xa988bb4b -1450656949 edx 0x2 2 ebx 0x293ff4 2703348 esp 0xbffff6f4 0xbffff6f4 ebp 0xbffff708 0xbffff708 esi 0x0 0 edi 0x0 0 eip 0x804843a 0x804843a... # ขอละไว้ มันยาว (gdb) c # ทำงานต่อ หยุดที่ breakpoint ถัดไป Continuing. Before strcpy: magic is 0x00000000 Breakpoint 2, 0x08048467 in main () (gdb) display/i $pc # add display ให้แสดงคำสั่งที่ eip ชี้อยู่ (pc คือ program counter ใช้แทน eip ได้) 1: x/i $pc => 0x8048467 : call 0x8048344 (gdb) x/8x $ebp-0xc # แสดงค่าตั้งแต่ 0xbffff6fc (buf) ไป 8*4=32 bytes 0xbffff6fc: 0x00293ff4 0x080484b0 0x00000000 0xbffff788 0xbffff70c: 0x00154bd6 0x00000002 0xbffff7b4 0xbffff7c0 (gdb) ni # ทำงานคำสั่งถัดไป โดยไม่เข้าไปใน call 0x0804846c in main () 1: x/i $pc # คำสั่งที่อยู่ใน display list แสดงทุกครั้งที่โปรแกรมหยุด => 0x804846c : mov $0x8048580,%eax (gdb) x/8x $ebp-0xc # แสดงค่าที่ memory ของ buf อีกครั้ง (ค่า dword ที่ 3 คือ magic) 0xbffff6fc: 0x55555555 0x55555555 0x55555555 0x55555555 0xbffff70c: 0x00555555 0x00000002 0xbffff7b4 0xbffff7c0 (gdb) i f # แสดงข้อมูล stack frame Stack level 0, frame at 0xbffff710: eip = 0x804846c in main; saved eip 0x555555 Arglist at 0xbffff708, args: Locals at 0xbffff708, Previous frame s sp is 0xbffff710 Saved registers: ebp at 0xbffff708, eip at 0xbffff70c (gdb) x/2s $esp # แสดงข้อมูลที่ esp ในรูปแบบ string จำนวน 2 string 0xbffff6f4: "\374\366\377\277\360\370\377\277", 'U' 0xbffff710: "\002" (gdb) # Enter เฉยๆ คือทำคำสั่งข้างบนซ้ำ แต่แสดงที่ address ถัดไป 0xbffff712: "" 0xbffff713: "" (gdb) c # ให้โปรแกรมทำงานต่อ Continuing. After strcpy: magic is 0x55555555 Hahaha, you WIN Program received signal SIGSEGV, Segmentation fault. 0x00555555 in ?? () (gdb) i r ebp eip ebp 0x55555555 0x55555555 eip 0x555555 0x555555 (gdb) q A debugging session is active. Inferior 1 [process 1857] will be killed. Quit anyway? (y or n) y $
ให้สังเกต ที่่คำสั่ง i f จะเห็นว่า saved ebp อยู่ที่ 0xbffff708 และ saved eip อยู่ที่ 0xbffff70c นั้นค่าถูกทำให้เปลี่ยน หลังจากเรียก strcpy (ตัว saved eip ที่มี 00 นำหน้านั้น 00 (NULL) มาจากตัวจบของ string ใน C แต่ที่อยู่ข้างหน้า เพราะแสดงเป็น integer ถ้างงก็คิดเรื่อง endian) แสดงให้เห็นว่า ข้อมูลที่เราใส่เข้าไปนั้น นอกจากจะเขียนทับ magic แล้วยังเขียนทับข้อมูลสำคัญ ที่กำหนดว่าให้โปรแกรมทำงานต่อที่ไหนหลังจากจบ main ทำให้โปรแกรมมีการอ้างถึง memory ที่ invalid คือ eip ชี้ไปที่ 0x00555555 ทำให้เกิด segmentation fault ขึ้น
ส่วนวิธีการ call function ในครั้งนี้จะไม่ใช้การ push argument แล้ว call อย่างที่เห็นใน assembly ข้างบน แต่จะเป็นการจองเนื้อที่บน stack ไว้สำหรับการส่ง argument แล้วใช้วิธี mov เพื่อย้ายค่าไปเป็น argument ต่างๆ แทน
ก่อนจะเริ่มในหัวข้อถ้ดไป ผมอยากให้ลองเอาโปรแกรมในหัวข้อ "Function กับ Stack" โดย compile ตามนี้ (ex_05_2.c)
$ gcc -fno-pie -fno-stack-protector -z norelro -z execstack -mpreferred-stack-boundary=2 -o ex_05_2 ex_05_2.c
แล้วให้ลอง
1. disassemble แล้วลองอ่าน assembly ดู
2. ลองใช้ stepi กับ nexti กับคำสั่ง call
3. ลองใช้ x/10s $esp แล้ว Enter ไปเรื่อยๆ จนหมด stack (bottom of stack) แล้วสังเกตค่าที่เป็นตัวอักษร อ่านรู้เรื่อง
Reference:
- GNU GDB Debugger Command Cheat Sheet
- GDB Cheat Sheet
No comments:
Post a Comment