Saturday, January 1, 2011

GDB เบื้องต้น

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)

คำสั่งเต็มคำสั่งย่อคำอธิบาย
runrเริ่มโปรแกรม
killkหยุดโปรแกรม
quitqออกจาก GDB
continuecทำงานต่อโดยหยุดที่ breakpoint ถัดไป
disassembledisasแสดง assembly code ของ function ที่ EIP อยู่
disassemble ADDRdisas ADDRแสดง assembly code ที่ address ADDR (ใช้ชื่อ function ได้)
disassemble ADDR1 ADDR2disas ADDR1 ADDR2แสดง assembly code ที่ address ADDR1 ถึง ADDR2
info breakpointsi bแสดง breakpoint ทั้งหมด
info registersi rแสดงค่าของ CPU registers ทั้งหมด
info framei fแสดงข้อมูลเกี่ยวกับ stack frame ปัจจุบัน
backtracebtแสดง call stack
break *ADDRb *ADDRset 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
deleteddelete 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 ADDRdisp/F ADDRแสดงค่าของ address ADDR ทุกครั้งที่ถึงหยุดทำงานชั่วคราว
displaydispแสดงค่าที่อยู่ใน display list ทั้งหมด
undisplay [NUM]und [NUM]ลบ display ที่เก็บไว้ที่ NUM
set ADDR=VALset ค่า VAL ไปที่ address ADDR

ต่อไปก็รูปแบบการแสดงผล (ค่า F จากตารางข้างบน) จะเหมือน C เกือบหมด

รูปแบบคำอธิบาย
apointer
ccharacter
dsigned decimal
ffloating point number
ooctal
sstring
tbinary
uunsigned decimal
xhexadecimal

คำสั่งตั้งเยอะ ใครจะจำได้หมด ต้องลองใช้บ่อยๆ ให้มันซึมเข้าไปเองครับ โดยผมจะลองใช้คำสั่งต่างๆ กับโปรแกรมในหัวข้อ "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