The Challenge
No libc, no system, no win function. The binary is almost bare-metal: it has a handful of hand-crafted gadgets and a writable data section. The only way out is to call execve directly via the Linux syscall interface.
Approach
The binary has exactly the gadgets needed for a manual execve call:
0x40102f—syscall0x401032—pop rdi; ret0x401034—pop rsi; ret0x401036—pop rdx; ret0x401038—xor rax, rdi; ret
execve needs: rax = 59, rdi = ptr to "/bin/sh", rsi = 0, rdx = 0.
The trick for rax: first pop rdi loads 59, then xor rax, rdi sets rax = rax XOR 59. If rax is zero at that point (freshly returned from a prior syscall or zeroed), the result is 59. Then pop rdi is called again to point at the string.
Before the ROP chain fires I send /bin/sh\x00 to land at 0x404000, which is a static writable address.
Solution
|
|
The first recv + send writes "/bin/sh\x00" to the first data read the binary does. 40 null bytes reach the saved return address. The ROP chain then builds the syscall register state step by step. The final syscall executes execve("/bin/sh", NULL, NULL) — a shell.
What I Learned
When there is no libc and no win function, raw syscalls are the path. xor rax, rdi as a way to load rax avoids needing a dedicated pop rax; ret gadget — a common trick when the gadget set is minimal. Knowing the Linux amd64 syscall table (execve = 59) and calling convention (rdi/rsi/rdx order) is required knowledge for bare-metal ROP.