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,664 @@
|
|
|
1
|
+
# CTF Crypto - PRNG & Key Recovery
|
|
2
|
+
|
|
3
|
+
Foundational PRNG state/seed recovery techniques. For CTF-era advanced attacks (MT19937 constraint propagation, Rule 86 cellular automaton, Java LCG MITM, LFSR bit-fold, Z3 timing oracle, randcrack DSA, NTP-poisoned UUID), see [prng-attacks.md](prng-attacks.md).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Mersenne Twister (MT19937) State Recovery](#mersenne-twister-mt19937-state-recovery)
|
|
7
|
+
- [MT State Recovery from random.random() Floats via GF(2) Matrix (PHD CTF Quals 2012)](#mt-state-recovery-from-randomrandom-floats-via-gf2-matrix-phd-ctf-quals-2012)
|
|
8
|
+
- [Time-Based Seed Attacks](#time-based-seed-attacks)
|
|
9
|
+
- [C srand/rand Synchronization via Python ctypes](#c-srandrand-synchronization-via-python-ctypes)
|
|
10
|
+
- [Layered Encryption Recovery](#layered-encryption-recovery)
|
|
11
|
+
- [LCG Parameter Recovery Attack](#lcg-parameter-recovery-attack)
|
|
12
|
+
- [ChaCha20 Key Recovery](#chacha20-key-recovery)
|
|
13
|
+
- [GF(2) Matrix PRNG Seed Recovery (0xFun 2026)](#gf2-matrix-prng-seed-recovery-0xfun-2026)
|
|
14
|
+
- [Middle-Square PRNG Brute Force (UTCTF 2024)](#middle-square-prng-brute-force-utctf-2024)
|
|
15
|
+
- [Deterministic RNG from Flag Bytes + Hill Climbing (VuwCTF 2025)](#deterministic-rng-from-flag-bytes--hill-climbing-vuwctf-2025)
|
|
16
|
+
- [Byte-by-Byte Oracle with Random Mode Matching (VuwCTF 2025)](#byte-by-byte-oracle-with-random-mode-matching-vuwctf-2025)
|
|
17
|
+
- [RSA Key Reuse / Replay (UTCTF 2024)](#rsa-key-reuse--replay-utctf-2024)
|
|
18
|
+
- [Password Cracking Strategy](#password-cracking-strategy)
|
|
19
|
+
- [Logistic Map / Chaotic PRNG Seed Recovery (BYPASS CTF 2025)](#logistic-map--chaotic-prng-seed-recovery-bypass-ctf-2025)
|
|
20
|
+
- [V8 XorShift128+ State Recovery (Math.random Prediction)](#v8-xorshift128-state-recovery-mathrandom-prediction)
|
|
21
|
+
|
|
22
|
+
See [prng-attacks.md](prng-attacks.md) for CTF-era advanced attacks (2017+).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Mersenne Twister (MT19937) State Recovery
|
|
27
|
+
|
|
28
|
+
Python's `random` module uses Mersenne Twister. If you can observe outputs, you can recover the state and predict future values.
|
|
29
|
+
|
|
30
|
+
**Key properties:**
|
|
31
|
+
- 624 × 32-bit internal state
|
|
32
|
+
- Each output is tempered from state
|
|
33
|
+
- After 624 outputs, state is twisted (regenerated)
|
|
34
|
+
|
|
35
|
+
**Basic untemper (reverse single output):**
|
|
36
|
+
```python
|
|
37
|
+
def untemper(y):
|
|
38
|
+
y ^= y >> 18
|
|
39
|
+
y ^= (y << 15) & 0xefc60000
|
|
40
|
+
for _ in range(7):
|
|
41
|
+
y ^= (y << 7) & 0x9d2c5680
|
|
42
|
+
y ^= y >> 11
|
|
43
|
+
y ^= y >> 22
|
|
44
|
+
return y
|
|
45
|
+
|
|
46
|
+
# Given 624 consecutive outputs, recover state
|
|
47
|
+
state = [untemper(output) for output in outputs]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Python's randrange(maxsize) on 64-bit:**
|
|
51
|
+
- `maxsize = 2^63 - 1`, so `getrandbits(63)` is used
|
|
52
|
+
- Each 63-bit output uses 2 MT outputs: `(mt1 << 31) | (mt2 >> 1)`
|
|
53
|
+
- One bit lost per output → need symbolic solving
|
|
54
|
+
|
|
55
|
+
**Symbolic approach with z3:**
|
|
56
|
+
```python
|
|
57
|
+
from z3 import *
|
|
58
|
+
|
|
59
|
+
def symbolic_temper(y):
|
|
60
|
+
y = y ^ (LShR(y, 11))
|
|
61
|
+
y = y ^ ((y << 7) & 0x9d2c5680)
|
|
62
|
+
y = y ^ ((y << 15) & 0xefc60000)
|
|
63
|
+
y = y ^ (LShR(y, 18))
|
|
64
|
+
return y
|
|
65
|
+
|
|
66
|
+
# Create symbolic MT state
|
|
67
|
+
mt = [BitVec(f'mt_{i}', 32) for i in range(624)]
|
|
68
|
+
solver = Solver()
|
|
69
|
+
|
|
70
|
+
# For each observed 63-bit output
|
|
71
|
+
for i, out63 in enumerate(outputs):
|
|
72
|
+
if 2*i + 1 >= 624: break
|
|
73
|
+
y1 = symbolic_temper(mt[2*i])
|
|
74
|
+
y2 = symbolic_temper(mt[2*i + 1])
|
|
75
|
+
combined = Concat(Extract(31, 0, y1), Extract(31, 1, y2))
|
|
76
|
+
solver.add(combined == out63)
|
|
77
|
+
|
|
78
|
+
if solver.check() == sat:
|
|
79
|
+
state = [solver.model()[mt[i]].as_long() for i in range(624)]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Applications:**
|
|
83
|
+
- MIME boundary prediction (email libraries)
|
|
84
|
+
- Session token prediction
|
|
85
|
+
- CAPTCHA bypass (predictable codes)
|
|
86
|
+
- Game RNG exploitation
|
|
87
|
+
|
|
88
|
+
## MT State Recovery from random.random() Floats via GF(2) Matrix (PHD CTF Quals 2012)
|
|
89
|
+
|
|
90
|
+
**Pattern:** Server exposes `random.random()` float outputs (e.g., via an API endpoint). Standard MT untemper requires 624 × 32-bit integer outputs, but `random.random()` produces 53-bit floats — truncating each to 8 usable bits per observation. A precomputed GF(2) magic matrix maps observed byte values back to the 624-word MT state.
|
|
91
|
+
|
|
92
|
+
**Key insight:** `random.random()` returns `(a*2^27+b)/2^53` where `a` = 27 bits from one MT output and `b` = 26 bits from the next. Truncating `int(float * 256)` yields only 8 bits per float, so 3360+ observations are needed (vs. 624 for integer outputs). The `not_random` library precomputes the GF(2) relationship between observed bits and state bits.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import random, gzip, hashlib
|
|
96
|
+
|
|
97
|
+
# Load precomputed GF(2) magic matrix (from github.com/fx5/not_random)
|
|
98
|
+
f = gzip.GzipFile("magic_data", "r")
|
|
99
|
+
magic = eval(f.read())
|
|
100
|
+
f.close()
|
|
101
|
+
|
|
102
|
+
def rebuild_from_floats(floats):
|
|
103
|
+
"""Convert float observations to byte values, then recover MT state."""
|
|
104
|
+
vals = [int(f * 256) for f in floats] # truncate to 8-bit
|
|
105
|
+
return rebuild_random(vals)
|
|
106
|
+
|
|
107
|
+
def rebuild_random(vals):
|
|
108
|
+
"""Recover MT19937 state from 3360+ byte observations using GF(2) matrix."""
|
|
109
|
+
def getbit(bit):
|
|
110
|
+
assert bit >= 0
|
|
111
|
+
return (vals[bit // 8] >> (7 - bit % 8)) & 1
|
|
112
|
+
state = []
|
|
113
|
+
for i in range(624):
|
|
114
|
+
val = 0
|
|
115
|
+
data = magic[i % 2]
|
|
116
|
+
for bit in data:
|
|
117
|
+
val <<= 1
|
|
118
|
+
for b in bit:
|
|
119
|
+
val ^= getbit(b + (i // 2) * 8 - 8)
|
|
120
|
+
state.append(val)
|
|
121
|
+
state.append(0)
|
|
122
|
+
ran = random.Random()
|
|
123
|
+
ran.setstate((3, tuple(state), None))
|
|
124
|
+
# Advance past consumed outputs
|
|
125
|
+
for i in range(len(vals) - 3201 + 394):
|
|
126
|
+
ran.randint(0, 255)
|
|
127
|
+
return ran
|
|
128
|
+
|
|
129
|
+
# Collect 3360+ random.random() floats from the target
|
|
130
|
+
floats = [...] # observed values from server API
|
|
131
|
+
|
|
132
|
+
# Recover state and predict future outputs
|
|
133
|
+
my_random = rebuild_from_floats(floats[:3360])
|
|
134
|
+
|
|
135
|
+
# Verify predictions match remaining observations
|
|
136
|
+
for observed, predicted in zip(floats[3360:], [my_random.random() for _ in range(40)]):
|
|
137
|
+
assert '%.16f' % observed == '%.16f' % predicted
|
|
138
|
+
|
|
139
|
+
# Forge password reset token (same hash the server computes)
|
|
140
|
+
token = hashlib.md5(('%.16f' % my_random.random()).encode()).hexdigest()
|
|
141
|
+
reset_url = f'http://target/reset/{user_id}-{token}/'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Attack flow (password reset token prediction):**
|
|
145
|
+
1. Request 3360+ random float values from an API endpoint that exposes them (e.g., `/?count=3360`)
|
|
146
|
+
2. Simultaneously trigger a password reset (the reset token is `md5(random.random())`)
|
|
147
|
+
3. Recover the MT state from the observed floats
|
|
148
|
+
4. Predict the `random.random()` call used for the reset token
|
|
149
|
+
5. Construct the reset URL with the predicted token
|
|
150
|
+
|
|
151
|
+
**When to use:** Server uses Python's `random.random()` for security-sensitive tokens (session IDs, password resets, CSRF tokens) and also exposes random values through another endpoint. The `not_random` library handles the bit-level math — focus on collecting enough float observations and synchronizing timing with the target operation.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Time-Based Seed Attacks
|
|
156
|
+
|
|
157
|
+
When encryption uses time-based PRNG seed:
|
|
158
|
+
```python
|
|
159
|
+
seed = f"{username}_{timestamp}_{random_bits}"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Attack approach:**
|
|
163
|
+
1. **Username:** Extract from metadata, email headers, challenge context
|
|
164
|
+
2. **Timestamp:** Get from file metadata (ZIP, exiftool)
|
|
165
|
+
3. **Random bits:** Check for hardcoded seed in binary, or bruteforce if small range
|
|
166
|
+
|
|
167
|
+
**Timestamp extraction:**
|
|
168
|
+
```bash
|
|
169
|
+
# Set timezone to match target
|
|
170
|
+
TZ=Pacific/Galapagos exiftool file.enc
|
|
171
|
+
# Look for File Modification Date/Time
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Bruteforce milliseconds:**
|
|
175
|
+
```python
|
|
176
|
+
from datetime import datetime
|
|
177
|
+
import random
|
|
178
|
+
|
|
179
|
+
for ms in range(1000):
|
|
180
|
+
ts = f"2021-02-09!07:23:54.{ms:03d}"
|
|
181
|
+
seed = f"{username}_{ts}_{rdata}"
|
|
182
|
+
rng = random.Random()
|
|
183
|
+
rng.seed(seed)
|
|
184
|
+
key = bytes(rng.getrandbits(8) for _ in range(32))
|
|
185
|
+
if try_decrypt(ciphertext, key):
|
|
186
|
+
print(f"Found seed: {seed}")
|
|
187
|
+
break
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## C srand/rand Synchronization via Python ctypes
|
|
191
|
+
|
|
192
|
+
**Pattern:** Binary seeds C's PRNG with `srand(time(NULL))` at startup and uses `rand()` for encryption keys, random challenges, or XOR masks. Python's `random` module uses Mersenne Twister (different algorithm), so calling `random.seed(t)` produces wrong outputs. Use `ctypes` to load the same libc and call C's `srand()`/`rand()` directly.
|
|
193
|
+
|
|
194
|
+
**Basic synchronization (L3akCTF 2024, MireaCTF):**
|
|
195
|
+
```python
|
|
196
|
+
from ctypes import CDLL
|
|
197
|
+
from time import time
|
|
198
|
+
|
|
199
|
+
# Load the SAME libc used by the target binary
|
|
200
|
+
libc = CDLL('./libc.so.6') # or CDLL('libc.so.6') for system libc
|
|
201
|
+
|
|
202
|
+
# Seed at the same second as the binary starts
|
|
203
|
+
libc.srand(int(time()))
|
|
204
|
+
|
|
205
|
+
# Generate the same sequence as the binary's rand() calls
|
|
206
|
+
for i in range(16):
|
|
207
|
+
value = libc.rand() & 0xff # match binary's truncation (e.g., & 0xff for byte)
|
|
208
|
+
print(value)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Decrypting XOR-encrypted data (L3akCTF 2024 chonccfile):**
|
|
212
|
+
```python
|
|
213
|
+
from ctypes import CDLL
|
|
214
|
+
from time import time
|
|
215
|
+
from pwn import u32, p32
|
|
216
|
+
|
|
217
|
+
libc_imp = CDLL('./libc.so.6')
|
|
218
|
+
libc_imp.srand(int(time()))
|
|
219
|
+
|
|
220
|
+
# Binary XORs each 4-byte block with rand() output
|
|
221
|
+
encrypted_data = b'...' # read from heap/memory
|
|
222
|
+
result = b''
|
|
223
|
+
for i in range(0, len(encrypted_data), 4):
|
|
224
|
+
block = u32(encrypted_data[i:i+4])
|
|
225
|
+
libc_imp.rand() # skip delay-related rand() call if binary does extra calls
|
|
226
|
+
key = libc_imp.rand()
|
|
227
|
+
block ^= key
|
|
228
|
+
result += p32(block)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Timing considerations:**
|
|
232
|
+
- `time(NULL)` has 1-second granularity — start the exploit within the same second as the binary
|
|
233
|
+
- Remote targets may have startup delay — try offsets of `+1` or `+2` seconds
|
|
234
|
+
- Account for any `rand()` calls between `srand()` and the target usage (e.g., random delays)
|
|
235
|
+
- Not 100% reliable on first try — retry with adjacent seeds if needed
|
|
236
|
+
|
|
237
|
+
**Key insight:** Python's `random` and C's `rand()` are completely different PRNGs. When a C binary uses `srand(time(NULL))`, the only way to reproduce the sequence from Python is `ctypes.CDLL` calling the same libc's `srand`/`rand`. Load the challenge's provided `libc.so.6` for exact compatibility. This works for any C PRNG output prediction — XOR keys, random challenges, token generation, or encrypted heap data.
|
|
238
|
+
|
|
239
|
+
**Alternative — custom shared library (MireaCTF):**
|
|
240
|
+
```c
|
|
241
|
+
// random_lib.c — compile with: gcc -shared -o random_lib.so random_lib.c
|
|
242
|
+
#include <stdlib.h>
|
|
243
|
+
void setseed(int seed) { srand(seed); }
|
|
244
|
+
int generate() { return rand() & 0xff; }
|
|
245
|
+
```
|
|
246
|
+
```python
|
|
247
|
+
from ctypes import CDLL
|
|
248
|
+
lib = CDLL('./random_lib.so')
|
|
249
|
+
lib.setseed(int(time()) + 1) # +1 for remote delay
|
|
250
|
+
numbers = [lib.generate() for _ in range(16)]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Layered Encryption Recovery
|
|
256
|
+
|
|
257
|
+
When binary uses multiple encryption layers:
|
|
258
|
+
1. Identify encryption order (e.g., Serpent → TEA)
|
|
259
|
+
2. Find seed derivation (e.g., sum of flag chars)
|
|
260
|
+
3. Keys often derived from `srand()` sequence
|
|
261
|
+
4. Bruteforce seed range (sum of printable ASCII is limited)
|
|
262
|
+
|
|
263
|
+
## LCG Parameter Recovery Attack
|
|
264
|
+
|
|
265
|
+
Linear Congruential Generators are weak PRNGs. Given consecutive outputs, recover parameters:
|
|
266
|
+
|
|
267
|
+
**LCG formula:** `x_{n+1} = (a * x_n + c) mod m`
|
|
268
|
+
|
|
269
|
+
**Recovery from output sequence (SageMath):**
|
|
270
|
+
```python
|
|
271
|
+
# Given sequence: [s0, s1, s2, s3, ...]
|
|
272
|
+
# crypto-attacks library: github.com/jvdsn/crypto-attacks
|
|
273
|
+
from attacks.lcg import parameter_recovery
|
|
274
|
+
|
|
275
|
+
sequence = [
|
|
276
|
+
72967016216206426977511399018380411256993151454761051136963936354667101207529,
|
|
277
|
+
49670218548812619526153633222605091541916798863041459174610474909967699929824,
|
|
278
|
+
# ... more outputs
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
m, a, c = parameter_recovery.attack(sequence)
|
|
282
|
+
print(f"Modulus m: {m}")
|
|
283
|
+
print(f"Multiplier a: {a}")
|
|
284
|
+
print(f"Increment c: {c}")
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Weak RSA from LCG primes:**
|
|
288
|
+
- If RSA primes are generated from LCG, recover LCG params first
|
|
289
|
+
- Use known plaintext XOR ciphertext to extract LCG outputs
|
|
290
|
+
- Regenerate same prime sequence to factor N
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
# Recover XOR key (which is LCG output)
|
|
294
|
+
def recover_lcg_output(plaintext, ciphertext, timestamp):
|
|
295
|
+
pt_bytes = plaintext.encode('utf-8').ljust(32, b'\0')
|
|
296
|
+
ct_int = int.from_bytes(bytes.fromhex(ciphertext), 'big')
|
|
297
|
+
return timestamp ^ int.from_bytes(pt_bytes, 'big') ^ ct_int
|
|
298
|
+
|
|
299
|
+
# After recovering LCG params, generate RSA primes
|
|
300
|
+
lcg = LCG(a, c, m, seed)
|
|
301
|
+
primes = []
|
|
302
|
+
while len(primes) < 8:
|
|
303
|
+
candidate = lcg.next()
|
|
304
|
+
if is_prime(candidate) and candidate.bit_length() == 256:
|
|
305
|
+
primes.append(candidate)
|
|
306
|
+
|
|
307
|
+
n = prod(primes)
|
|
308
|
+
phi = prod(p - 1 for p in primes)
|
|
309
|
+
d = pow(65537, -1, phi)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## ChaCha20 Key Recovery
|
|
313
|
+
|
|
314
|
+
When ChaCha20 key is derived from recoverable data:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
from Crypto.Cipher import ChaCha20
|
|
318
|
+
|
|
319
|
+
# If key derived from predictable source (timestamp, PID, etc.)
|
|
320
|
+
for candidate_key in generate_candidates():
|
|
321
|
+
cipher = ChaCha20.new(key=candidate_key, nonce=nonce)
|
|
322
|
+
plaintext = cipher.decrypt(ciphertext)
|
|
323
|
+
if is_valid(plaintext): # Check for expected format
|
|
324
|
+
print(f"Key found: {candidate_key.hex()}")
|
|
325
|
+
break
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Ghidra emulator for key extraction:**
|
|
329
|
+
When key is computed by complex function, emulate it rather than reimplementing.
|
|
330
|
+
|
|
331
|
+
## GF(2) Matrix PRNG Seed Recovery (0xFun 2026)
|
|
332
|
+
|
|
333
|
+
**Pattern (BitStorm):** PRNG using only XOR, shifts, and rotations is linear over GF(2).
|
|
334
|
+
|
|
335
|
+
**Key insight:** Express entire PRNG as matrix multiplication: `output_bits = M * seed_bits (mod 2)`. With enough outputs, Gaussian elimination recovers the seed.
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
import numpy as np
|
|
339
|
+
|
|
340
|
+
def build_prng_matrix(prng_func, seed_bits=2048, output_bits=2048):
|
|
341
|
+
"""Build GF(2) matrix by running PRNG on unit vectors."""
|
|
342
|
+
M = np.zeros((output_bits, seed_bits), dtype=np.uint8)
|
|
343
|
+
for i in range(seed_bits):
|
|
344
|
+
# Set bit i of seed
|
|
345
|
+
seed = 1 << (seed_bits - 1 - i)
|
|
346
|
+
output = prng_func(seed)
|
|
347
|
+
for j in range(output_bits):
|
|
348
|
+
M[j, i] = (output >> (output_bits - 1 - j)) & 1
|
|
349
|
+
return M
|
|
350
|
+
|
|
351
|
+
# Given output, solve: M * seed = output (mod 2)
|
|
352
|
+
# Use GF(2) Gaussian elimination (see modern-ciphers.md solve_gf2)
|
|
353
|
+
seed = solve_gf2(M, output_bits_array)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Identification:** Any PRNG using only `^`, `<<`, `>>`, bitwise rotate. DON'T try iterative state recovery — go straight to the matrix.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Middle-Square PRNG Brute Force (UTCTF 2024)
|
|
361
|
+
|
|
362
|
+
**Pattern (numbers go brrr):** Middle-square method with small seed space.
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
# PRNG: seed = int(str(seed * seed).zfill(12)[3:9]) — 6-digit seed
|
|
366
|
+
# Seed source: int(time.time() * 1000) % (10**6) — only 1M possibilities
|
|
367
|
+
# AES key: 8 rounds of PRNG, each produces seed % 2^16 as 2-byte fragment
|
|
368
|
+
|
|
369
|
+
def middle_square_keygen(seed):
|
|
370
|
+
key = b''
|
|
371
|
+
for _ in range(8):
|
|
372
|
+
seed = int(str(seed * seed).zfill(12)[3:9])
|
|
373
|
+
key += (seed % (2**16)).to_bytes(2, 'big')
|
|
374
|
+
return key
|
|
375
|
+
|
|
376
|
+
# Brute-force: encrypt known plaintext, compare
|
|
377
|
+
for seed in range(10**6):
|
|
378
|
+
key = middle_square_keygen(seed)
|
|
379
|
+
if try_decrypt(ciphertext, key):
|
|
380
|
+
print(f"Seed: {seed}")
|
|
381
|
+
break
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Even with time-limited interactions:** 1 known-plaintext pair suffices for offline brute force.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Deterministic RNG from Flag Bytes + Hill Climbing (VuwCTF 2025)
|
|
389
|
+
|
|
390
|
+
**Pattern (Totally Random Art):** Flag bytes seed Python `random.Random()`. First N bytes of flag are known format, remaining bytes produce deterministic output.
|
|
391
|
+
|
|
392
|
+
**Attack:** When PRNG seed is known/derivable from flag format, hill-climb unknown characters:
|
|
393
|
+
```python
|
|
394
|
+
import random
|
|
395
|
+
|
|
396
|
+
def render(flag_bytes):
|
|
397
|
+
rng = random.Random()
|
|
398
|
+
rng.seed(flag_bytes)
|
|
399
|
+
grid = [[0]*10 for _ in range(5)]
|
|
400
|
+
for b in flag_bytes:
|
|
401
|
+
steps, stroke = divmod(b, 16)
|
|
402
|
+
x, y = 0, 0
|
|
403
|
+
for _ in range(steps):
|
|
404
|
+
dx, dy = rng.choice([(0,1),(0,-1),(1,0),(-1,0)])
|
|
405
|
+
x = (x + dx) % 10
|
|
406
|
+
y = (y + dy) % 5
|
|
407
|
+
grid[y][x] = (grid[y][x] + stroke) % 16
|
|
408
|
+
return grid
|
|
409
|
+
|
|
410
|
+
# Hill climb: try each byte value, keep the one that maximizes grid match
|
|
411
|
+
target = parse_target_art()
|
|
412
|
+
flag = list(b'VuwCTF{')
|
|
413
|
+
for pos in range(7, 17):
|
|
414
|
+
best_score, best_char = -1, 0
|
|
415
|
+
for c in range(32, 127):
|
|
416
|
+
candidate = bytes(flag + [c])
|
|
417
|
+
score = sum(1 for y in range(5) for x in range(10)
|
|
418
|
+
if render(candidate)[y][x] == target[y][x])
|
|
419
|
+
if score > best_score:
|
|
420
|
+
best_score, best_char = score, c
|
|
421
|
+
flag.append(best_char)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Byte-by-Byte Oracle with Random Mode Matching (VuwCTF 2025)
|
|
427
|
+
|
|
428
|
+
**Pattern (Unorthodox IV):** Server encrypts with one of N random modes/IVs per encryption. Can submit own plaintexts.
|
|
429
|
+
|
|
430
|
+
**Attack strategy:**
|
|
431
|
+
1. Connect, get encrypted flag
|
|
432
|
+
2. Probe with known prefix to check if connection can "reach" the flag's mode (same mode = same ciphertext prefix). ~50 probes, if no match, reconnect.
|
|
433
|
+
3. Once reachable, test candidate characters. Mode match AND next byte match = correct char. Mode match but byte mismatch = eliminate candidate permanently.
|
|
434
|
+
4. Elimination persists across reconnections.
|
|
435
|
+
|
|
436
|
+
**Key insight:** Probe for mode reachability first to avoid wasting attempts. Elimination-based search is more efficient than confirmation-based when modes are randomized.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## RSA Key Reuse / Replay (UTCTF 2024)
|
|
441
|
+
|
|
442
|
+
**Pattern (simple signature):** RSA keys reused across rounds with alternating inputs.
|
|
443
|
+
|
|
444
|
+
**Attack:** Submit previously captured encrypted output back to the server. If keys are static across interactions, replay attacks are trivial. Always check if crypto keys change between rounds.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Logistic Map / Chaotic PRNG Seed Recovery (BYPASS CTF 2025)
|
|
449
|
+
|
|
450
|
+
**Pattern (Chaotic Trust):** Stream cipher using the logistic map `x_{n+1} = r * x * (1 - x)` as PRNG. Keystream generated by packing iterated float values.
|
|
451
|
+
|
|
452
|
+
**Key insight:** Logistic map with `r ≈ 4.0` is chaotic but deterministic — recovering the seed (initial x value) enables full keystream reconstruction. Seed is usually a decimal between 0 and 1, such as 0.123456789.
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
import struct
|
|
456
|
+
|
|
457
|
+
def logistic_map(x, r=3.99):
|
|
458
|
+
return r * x * (1 - x)
|
|
459
|
+
|
|
460
|
+
def decrypt_logistic(cipher_hex, seed):
|
|
461
|
+
cipher = bytes.fromhex(cipher_hex)
|
|
462
|
+
x = seed
|
|
463
|
+
stream = b""
|
|
464
|
+
|
|
465
|
+
while len(stream) < len(cipher):
|
|
466
|
+
x = logistic_map(x)
|
|
467
|
+
# Pack float as bytes for keystream (check endianness)
|
|
468
|
+
stream += struct.pack("<f", x)[-2:] # or full 4 bytes
|
|
469
|
+
|
|
470
|
+
stream = stream[:len(cipher)]
|
|
471
|
+
return bytes(a ^ b for a, b in zip(cipher, stream))
|
|
472
|
+
|
|
473
|
+
# Brute-force seed precision
|
|
474
|
+
for precision in range(6, 12):
|
|
475
|
+
for base in [123456, 234567, 314159, 271828]:
|
|
476
|
+
seed = base / (10 ** precision)
|
|
477
|
+
result = decrypt_logistic(cipher_hex, seed)
|
|
478
|
+
if b"FLAG" in result or b"CTF" in result:
|
|
479
|
+
print(f"Seed: {seed}, Flag: {result}")
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Variations:**
|
|
483
|
+
- **r parameter:** Usually `r = 3.99` or `r = 4.0` (full chaos regime)
|
|
484
|
+
- **Packing:** `struct.pack("<f", x)` (4 bytes), `struct.pack("<d", x)` (8 bytes), or `[-2:]` for 2-byte fragments
|
|
485
|
+
- **Seed range:** Often a recognizable decimal like `0.123456789` or derived from challenge hints
|
|
486
|
+
|
|
487
|
+
**Identification:** Challenge mentions "chaos", "logistic", "butterfly effect", or provides `r` parameter. Look for source code containing `x = r * x * (1 - x)` iteration.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## V8 XorShift128+ State Recovery (Math.random Prediction)
|
|
492
|
+
|
|
493
|
+
**Pattern:** Web challenge uses `Math.floor(CONST * Math.random())` to generate tokens, codes, or game values. V8's `Math.random()` uses XorShift128+ (xs128p) PRNG. Given consecutive floored outputs, recover the internal state (state0, state1) with Z3, then predict future values.
|
|
494
|
+
|
|
495
|
+
**V8 internals:**
|
|
496
|
+
1. xs128p produces 64-bit state; V8 uses `state0 >> 12 | 0x3FF0000000000000` to create a double in [1.0, 2.0), then subtracts 1.0
|
|
497
|
+
2. `Math.random()` reads from a **64-value LIFO cache**. When the cache is empty, `RefillCache()` generates 64 new values. Values are consumed in reverse order from the cache
|
|
498
|
+
3. Only `state0` is used for the output (not `state1`)
|
|
499
|
+
|
|
500
|
+
**xs128p algorithm:**
|
|
501
|
+
```python
|
|
502
|
+
def xs128p(state0, state1):
|
|
503
|
+
s1 = state0 & 0xFFFFFFFFFFFFFFFF
|
|
504
|
+
s0 = state1 & 0xFFFFFFFFFFFFFFFF
|
|
505
|
+
s1 ^= (s1 << 23) & 0xFFFFFFFFFFFFFFFF
|
|
506
|
+
s1 ^= (s1 >> 17) & 0xFFFFFFFFFFFFFFFF
|
|
507
|
+
s1 ^= s0 & 0xFFFFFFFFFFFFFFFF
|
|
508
|
+
s1 ^= (s0 >> 26) & 0xFFFFFFFFFFFFFFFF
|
|
509
|
+
state0 = state1 & 0xFFFFFFFFFFFFFFFF
|
|
510
|
+
state1 = s1 & 0xFFFFFFFFFFFFFFFF
|
|
511
|
+
return state0, state1, state0 # output is new state0
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Z3 solver for `Math.floor(CONST * Math.random())`:**
|
|
515
|
+
```python
|
|
516
|
+
from z3 import *
|
|
517
|
+
from decimal import Decimal
|
|
518
|
+
import struct
|
|
519
|
+
|
|
520
|
+
def to_double(value):
|
|
521
|
+
double_bits = (value >> 12) | 0x3FF0000000000000
|
|
522
|
+
return struct.unpack('d', struct.pack('<Q', double_bits))[0] - 1
|
|
523
|
+
|
|
524
|
+
def from_double(dbl):
|
|
525
|
+
return struct.unpack('<Q', struct.pack('d', dbl + 1))[0] & 0x7FFFFFFFFFFFFFFF
|
|
526
|
+
|
|
527
|
+
def sym_xs128p(s0, s1):
|
|
528
|
+
s1_ = s0
|
|
529
|
+
s0_ = s1
|
|
530
|
+
s1_ ^= (s1_ << 23)
|
|
531
|
+
s1_ ^= LShR(s1_, 17)
|
|
532
|
+
s1_ ^= s0_
|
|
533
|
+
s1_ ^= LShR(s0_, 26)
|
|
534
|
+
return s1, s1_ # new state0, state1
|
|
535
|
+
|
|
536
|
+
def solve_v8_random(observed_values, multiple):
|
|
537
|
+
"""Recover xs128p state from consecutive Math.floor(multiple * Math.random()) outputs.
|
|
538
|
+
observed_values must be in REVERSE order (oldest first after tac)."""
|
|
539
|
+
ostate0, ostate1 = BitVecs('ostate0 ostate1', 64)
|
|
540
|
+
sym_s0, sym_s1 = ostate0, ostate1
|
|
541
|
+
slvr = SolverFor("QF_BV")
|
|
542
|
+
|
|
543
|
+
for val in observed_values:
|
|
544
|
+
sym_s0, sym_s1 = sym_xs128p(sym_s0, sym_s1)
|
|
545
|
+
calc = LShR(sym_s0, 12) # V8's ToDouble mantissa bits
|
|
546
|
+
# Constrain: floor(multiple * to_double(state0)) == val
|
|
547
|
+
lower = from_double(Decimal(val) / Decimal(multiple))
|
|
548
|
+
upper = from_double(Decimal(val + 1) / Decimal(multiple))
|
|
549
|
+
lower_m = lower & 0x000FFFFFFFFFFFFF
|
|
550
|
+
upper_m = upper & 0x000FFFFFFFFFFFFF
|
|
551
|
+
upper_e = (upper >> 52) & 0x7FF
|
|
552
|
+
slvr.add(And(lower_m <= calc, Or(upper_m >= calc, upper_e == 1024)))
|
|
553
|
+
|
|
554
|
+
if slvr.check() == sat:
|
|
555
|
+
m = slvr.model()
|
|
556
|
+
return m[ostate0].as_long(), m[ostate1].as_long()
|
|
557
|
+
return None, None
|
|
558
|
+
|
|
559
|
+
# Predict next values after state recovery
|
|
560
|
+
def predict_next(state0, state1, multiple, count):
|
|
561
|
+
results = []
|
|
562
|
+
for _ in range(count):
|
|
563
|
+
state0, state1, output = xs128p(state0, state1)
|
|
564
|
+
import math
|
|
565
|
+
results.append(math.floor(multiple * to_double(output)))
|
|
566
|
+
return results
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Usage (tool: d0nutptr/v8_rand_buster):**
|
|
570
|
+
```bash
|
|
571
|
+
# Collect observed values, reverse them (LIFO cache order), pipe to solver
|
|
572
|
+
cat observed_codes.txt | tac | python3 xs128p.py --multiple 100000
|
|
573
|
+
|
|
574
|
+
# Generate predictions from recovered state
|
|
575
|
+
python3 xs128p.py --multiple 100000 --gen <state0>,<state1>,<count>
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**Key insight:** The LIFO cache means observed values are in reverse generation order — reverse them with `tac` before solving. The Z3 `QF_BV` (quantifier-free bitvector) theory efficiently handles the bitwise operations. Typically 5-10 consecutive outputs suffice for a unique solution.
|
|
579
|
+
|
|
580
|
+
**Common pitfalls:**
|
|
581
|
+
- Forgetting to reverse the observation order (cache is LIFO)
|
|
582
|
+
- Multiple browser tabs or web workers may have separate PRNG states
|
|
583
|
+
- Cache boundary (every 64 calls) can introduce discontinuities if observations span a refill
|
|
584
|
+
|
|
585
|
+
**Inverse xorshift128+ (backward prediction):** After recovering state, step the PRNG backward to predict values generated *before* the observed sequence. Essential when the target value was generated earlier than observations (e.g., predicting another user's 2FA code). (Midnight Flag 2026)
|
|
586
|
+
|
|
587
|
+
```python
|
|
588
|
+
def undo_rshift_xor(val, shift):
|
|
589
|
+
"""Invert val ^= (val >> shift)"""
|
|
590
|
+
result = val
|
|
591
|
+
for _ in range(3): # 3 iterations sufficient for 64-bit
|
|
592
|
+
result = val ^ (result >> shift)
|
|
593
|
+
return result & 0xFFFFFFFFFFFFFFFF
|
|
594
|
+
|
|
595
|
+
def undo_lshift_xor(val, shift):
|
|
596
|
+
"""Invert val ^= (val << shift)"""
|
|
597
|
+
result = val
|
|
598
|
+
for _ in range(3):
|
|
599
|
+
result = val ^ ((result << shift) & 0xFFFFFFFFFFFFFFFF)
|
|
600
|
+
return result & 0xFFFFFFFFFFFFFFFF
|
|
601
|
+
|
|
602
|
+
def reverse_step(s0, s1):
|
|
603
|
+
"""Run xs128p one step backward: (s0, s1) → (old_s0, old_s1)"""
|
|
604
|
+
old_s1 = s0
|
|
605
|
+
known = (s1 ^ s0 ^ ((s0 >> 26) & 0xFFFFFFFFFFFFFFFF)) & 0xFFFFFFFFFFFFFFFF
|
|
606
|
+
x = undo_rshift_xor(known, 17)
|
|
607
|
+
old_s0 = undo_lshift_xor(x, 23)
|
|
608
|
+
return old_s0, old_s1
|
|
609
|
+
|
|
610
|
+
# Usage: step backward N times from recovered state
|
|
611
|
+
for _ in range(N):
|
|
612
|
+
state0, state1 = reverse_step(state0, state1)
|
|
613
|
+
predicted = math.floor(CONST * to_double(state0))
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**When to use:** Web challenge where JavaScript generates predictable-looking random values (tokens, verification codes, game rolls) using `Math.random()`. Look for patterns like `Math.floor(N * Math.random())` or `Math.random().toString(36).substr(2)` in client-side or server-side Node.js code.
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Password Cracking Strategy
|
|
621
|
+
|
|
622
|
+
**Attack order for unknown passwords:**
|
|
623
|
+
1. Common wordlists: `rockyou.txt`, `10k-common.txt`
|
|
624
|
+
2. Theme-based wordlist (usernames, challenge keywords)
|
|
625
|
+
3. Rules attack: wordlist + `best66.rule`, `dive.rule`
|
|
626
|
+
4. Hybrid: `word + ?d?d?d?d` (word + 4 digits)
|
|
627
|
+
5. Brute force: start at 4 chars, increase
|
|
628
|
+
|
|
629
|
+
**SHA256 with hex salt (VuwCTF 2025, Delicious Cooking):** Format `hash$hex_salt`. Salt must be hex-decoded before `SHA256(password + salt_bytes)`. Password often derivable from security questions (e.g., "fav movie + PIN" = "ratatouille0000"-"ratatouille9999").
|
|
630
|
+
|
|
631
|
+
**CTF password patterns:**
|
|
632
|
+
```text
|
|
633
|
+
base_password + year → actnowonclimatechange2026
|
|
634
|
+
username + digits → nemo123, admin2026
|
|
635
|
+
theme + numbers → flag2026, ctf2025
|
|
636
|
+
leet speak → p@ssw0rd, s3cr3t
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**Hashcat modes reference:**
|
|
640
|
+
```bash
|
|
641
|
+
# Common modes
|
|
642
|
+
-m 0 # MD5
|
|
643
|
+
-m 1000 # NTLM
|
|
644
|
+
-m 5600 # NTLMv2
|
|
645
|
+
-m 13600 # WinZip AES
|
|
646
|
+
-m 13000 # RAR5
|
|
647
|
+
-m 11600 # 7-Zip
|
|
648
|
+
|
|
649
|
+
# Attack modes
|
|
650
|
+
-a 0 # Dictionary
|
|
651
|
+
-a 3 # Brute force mask
|
|
652
|
+
-a 6 # Hybrid (word + mask)
|
|
653
|
+
-a 7 # Hybrid (mask + word)
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**When password relates to another in challenge:**
|
|
657
|
+
- Try variations: `password + year`, `password + 123`
|
|
658
|
+
- Try reversed: `drowssap`
|
|
659
|
+
- Try with common suffixes: `!`, `@`, `#`, `1`, `123`
|
|
660
|
+
- If SMB/FTP password known, ZIP password often related
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
See [prng-attacks.md](prng-attacks.md) for CTF-era advanced attacks (2017+).
|