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,792 @@
|
|
|
1
|
+
# CTF Crypto - RSA Attacks (Part 2: Specialized Techniques)
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [RSA p=q Validation Bypass (BearCatCTF 2026)](#rsa-pq-validation-bypass-bearcatctf-2026)
|
|
5
|
+
- [RSA Cube Root CRT when gcd(e, phi) > 1 (BearCatCTF 2026)](#rsa-cube-root-crt-when-gcde-phi--1-bearcatctf-2026)
|
|
6
|
+
- [Factoring n from Multiple of phi(n) (BearCatCTF 2026)](#factoring-n-from-multiple-of-phin-bearcatctf-2026)
|
|
7
|
+
- [RSA Signature Forgery via Multiplicative Homomorphism (MMA CTF 2015)](#rsa-signature-forgery-via-multiplicative-homomorphism-mma-ctf-2015)
|
|
8
|
+
- [Weak RSA Key Generation via Base Representation (Sharif CTF 2016)](#weak-rsa-key-generation-via-base-representation-sharif-ctf-2016)
|
|
9
|
+
- [RSA with gcd(e, phi(n)) > 1 (CSAW 2015)](#rsa-with-gcde-phin--1-csaw-2015)
|
|
10
|
+
- [Batch GCD for Shared Prime Factoring (BSidesSF 2025)](#batch-gcd-for-shared-prime-factoring-bsidessf-2025)
|
|
11
|
+
- [RSA Partial Key Recovery from dp dq qinv (0CTF 2016)](#rsa-partial-key-recovery-from-dp-dq-qinv-0ctf-2016)
|
|
12
|
+
- [RSA-CRT Fault Attack / Bit-Flip Recovery (CSAW CTF 2016)](#rsa-crt-fault-attack--bit-flip-recovery-csaw-ctf-2016)
|
|
13
|
+
- [RSA Homomorphic Decryption Oracle Bypass (ECTF 2016)](#rsa-homomorphic-decryption-oracle-bypass-ectf-2016)
|
|
14
|
+
- [RSA with Small Prime Factors and CRT Decomposition (Hack The Vote 2016)](#rsa-with-small-prime-factors-and-crt-decomposition-hack-the-vote-2016)
|
|
15
|
+
- [RSA Timing Attack on Montgomery Reduction (DEF CON 2017)](#rsa-timing-attack-on-montgomery-reduction-def-con-2017)
|
|
16
|
+
- [Bleichenbacher Low-Exponent RSA Signature Forgery (Google CTF 2017)](#bleichenbacher-low-exponent-rsa-signature-forgery-google-ctf-2017)
|
|
17
|
+
- [Coppersmith Small Roots for Linearly Related Primes (Tokyo Westerns 2017)](#coppersmith-small-roots-for-linearly-related-primes-tokyo-westerns-2017)
|
|
18
|
+
- [ROCA Attack on RSA CVE-2017-15361 (EasyCTF IV)](#roca-attack-on-rsa-cve-2017-15361-easyctf-iv)
|
|
19
|
+
- [RSA Signature Bypass with e=1 and Crafted Modulus (BackdoorCTF 2018)](#rsa-signature-bypass-with-e1-and-crafted-modulus-backdoorctf-2018)
|
|
20
|
+
- [Dependent-Prime RSA: q = e^-1 mod p (TokyoWesterns CTF 4th 2018)](#dependent-prime-rsa-q--e-1-mod-p-tokyowesterns-ctf-4th-2018)
|
|
21
|
+
- [RSA Three-Key Pairwise GCD Triangle (Trend Micro 2018)](#rsa-three-key-pairwise-gcd-triangle-trend-micro-2018)
|
|
22
|
+
- [RSA n = p^2*q Schmidt-Samoa Variant (ASIS Finals 2018)](#rsa-n--p2q-schmidt-samoa-variant-asis-finals-2018)
|
|
23
|
+
- [Modulus Recovery via GCD of Encryption Residuals (X-MAS CTF 2018)](#modulus-recovery-via-gcd-of-encryption-residuals-x-mas-ctf-2018)
|
|
24
|
+
- [Textbook RSA Negation via encrypt(-1) (X-MAS CTF 2018)](#textbook-rsa-negation-via-encrypt-1-x-mas-ctf-2018)
|
|
25
|
+
- [Poly-Exponent RSA: GCD of p^p Combinations (ASIS Finals 2018)](#poly-exponent-rsa-gcd-of-pp-combinations-asis-finals-2018)
|
|
26
|
+
- [Biased LSB Oracle with Mode-of-Runs Recovery (CSAW CTF 2018)](#biased-lsb-oracle-with-mode-of-runs-recovery-csaw-ctf-2018)
|
|
27
|
+
- [Cube-Root Wraparound via AES-CTR Length Hint (hxp 2018)](#cube-root-wraparound-via-aes-ctr-length-hint-hxp-2018)
|
|
28
|
+
- [RSA p = next_prime(2^k + small) Shared-Prime Batch GCD (ASIS Finals 2018)](#rsa-p--next_prime2k--small-shared-prime-batch-gcd-asis-finals-2018)
|
|
29
|
+
- [PNG Encryption Bounded by 512-bit Key → Trailer Replacement (ASIS Finals 2018)](#png-encryption-bounded-by-512-bit-key--trailer-replacement-asis-finals-2018)
|
|
30
|
+
- [Modulus Recovery via Plaintext Malleability (X-MAS 2018)](#modulus-recovery-via-plaintext-malleability-x-mas-2018)
|
|
31
|
+
- [RSA CRT d_p NULL-Byte Overflow Primes Leak (P.W.N. CTF 2018)](#rsa-crt-d_p-null-byte-overflow-primes-leak-pwn-ctf-2018)
|
|
32
|
+
- [Textbook RSA Signature Blinding via Message Factoring (P.W.N. CTF 2018)](#textbook-rsa-signature-blinding-via-message-factoring-pwn-ctf-2018)
|
|
33
|
+
- [Last-Byte Modulus Overwrite via strlen-1 Null Truncation (OTW Advent 2018)](#last-byte-modulus-overwrite-via-strlen-1-null-truncation-otw-advent-2018)
|
|
34
|
+
- [CRC32 Collision Oracle + RSA Homomorphic Signature Forgery (BSidesSF 2019)](#crc32-collision-oracle--rsa-homomorphic-signature-forgery-bsidessf-2019)
|
|
35
|
+
|
|
36
|
+
See also: [rsa-attacks.md](rsa-attacks.md) for foundational RSA attacks (small e, Wiener, Fermat, Pollard, Hastad, common modulus, Manger oracle, Coppersmith).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## RSA p=q Validation Bypass (BearCatCTF 2026)
|
|
41
|
+
|
|
42
|
+
**Pattern (Pickme):** Server validates user-submitted RSA key (checks `n`, `e`, `d`, `p*q=n`, `e*d ≡ 1 mod phi`), encrypts the flag, then tries test decryption. If decryption fails, leaks ciphertext in error message.
|
|
43
|
+
|
|
44
|
+
**Exploit:** Set `p = q`. The server computes `phi = (p-1)*(q-1) = (p-1)^2` (incorrect — real totient of `p^2` is `p*(p-1)`). All validation checks pass, but decryption with the wrong `d` fails, leaking the ciphertext.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from Crypto.Util.number import getPrime, inverse
|
|
48
|
+
|
|
49
|
+
p = getPrime(512)
|
|
50
|
+
q = p # p = q!
|
|
51
|
+
n = p * q # = p^2
|
|
52
|
+
e = 65537
|
|
53
|
+
wrong_phi = (p - 1) * (q - 1) # = (p-1)^2
|
|
54
|
+
d = inverse(e, wrong_phi) # passes server validation
|
|
55
|
+
|
|
56
|
+
# Server encrypts flag with our key, test decryption fails → leaks ciphertext c
|
|
57
|
+
# Decrypt with correct totient:
|
|
58
|
+
real_phi = p * (p - 1)
|
|
59
|
+
real_d = inverse(e, real_phi)
|
|
60
|
+
flag = pow(c, real_d, n)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Key insight:** `phi(p^2) = p*(p-1)`, NOT `(p-1)^2`. When a server validates RSA parameters but uses `(p-1)*(q-1)` without checking `p != q`, setting `p=q` creates a working key that the server will miscompute the private exponent for, causing decryption failure and error-path data leakage.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## RSA Cube Root CRT when gcd(e, phi) > 1 (BearCatCTF 2026)
|
|
68
|
+
|
|
69
|
+
**Pattern (Kidd's Crypto):** RSA with `e=3`, modulus composed of many small primes all ≡ 1 (mod 3). Since each `p-1` is divisible by 3, `gcd(e, phi(n)) = 3^k` and the standard modular inverse `d = e^-1 mod phi` doesn't exist.
|
|
70
|
+
|
|
71
|
+
**Solution:** Compute cube roots per-prime via CRT:
|
|
72
|
+
```python
|
|
73
|
+
from sympy.ntheory.residues import nthroot_mod
|
|
74
|
+
from sympy.ntheory.modular import crt
|
|
75
|
+
|
|
76
|
+
primes = [p1, p2, ..., p13] # All ≡ 1 mod 3
|
|
77
|
+
|
|
78
|
+
# For each prime, find all 3 cube roots of c mod p
|
|
79
|
+
roots_per_prime = []
|
|
80
|
+
for p in primes:
|
|
81
|
+
roots = nthroot_mod(c % p, 3, p, all_roots=True)
|
|
82
|
+
roots_per_prime.append(roots)
|
|
83
|
+
|
|
84
|
+
# Try all 3^13 = 1,594,323 combinations
|
|
85
|
+
from itertools import product
|
|
86
|
+
for combo in product(*roots_per_prime):
|
|
87
|
+
result, mod = crt(primes, list(combo))
|
|
88
|
+
try:
|
|
89
|
+
text = long_to_bytes(result).decode('ascii')
|
|
90
|
+
if text.isprintable():
|
|
91
|
+
print(f"Flag: {text}")
|
|
92
|
+
break
|
|
93
|
+
except:
|
|
94
|
+
continue
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Key insight:** When `gcd(e, phi(n)) > 1`, standard RSA decryption fails. Factor `n`, compute eth roots modulo each prime separately (each prime ≡ 1 mod e gives `e` roots), then enumerate all CRT combinations. Feasible when the number of primes is small (3^13 ≈ 1.6M combinations).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Factoring n from Multiple of phi(n) (BearCatCTF 2026)
|
|
102
|
+
|
|
103
|
+
**Pattern (Twisted Pair):** Given RSA `n` and a leaked pair `(re, rd)` where `re * rd ≡ 1 (mod k*phi(n))`. The value `re*rd - 1` is a multiple of `phi(n)`, enabling probabilistic factoring.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import random
|
|
107
|
+
from math import gcd
|
|
108
|
+
|
|
109
|
+
def factor_from_phi_multiple(n, phi_multiple):
|
|
110
|
+
"""Factor n given any multiple of phi(n) using Miller-Rabin variant."""
|
|
111
|
+
# Write phi_multiple = 2^s * d where d is odd
|
|
112
|
+
s, d = 0, phi_multiple
|
|
113
|
+
while d % 2 == 0:
|
|
114
|
+
s += 1
|
|
115
|
+
d //= 2
|
|
116
|
+
|
|
117
|
+
for _ in range(100): # 100 attempts
|
|
118
|
+
a = random.randrange(2, n - 1)
|
|
119
|
+
x = pow(a, d, n)
|
|
120
|
+
if x == 1 or x == n - 1:
|
|
121
|
+
continue
|
|
122
|
+
for _ in range(s - 1):
|
|
123
|
+
prev = x
|
|
124
|
+
x = pow(x, 2, n)
|
|
125
|
+
if x == n - 1:
|
|
126
|
+
break
|
|
127
|
+
if x == 1:
|
|
128
|
+
# prev is non-trivial square root of 1
|
|
129
|
+
p = gcd(prev - 1, n)
|
|
130
|
+
if 1 < p < n:
|
|
131
|
+
return p, n // p
|
|
132
|
+
# Check final
|
|
133
|
+
if x != n - 1:
|
|
134
|
+
p = gcd(x - 1, n)
|
|
135
|
+
if 1 < p < n:
|
|
136
|
+
return p, n // p
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
phi_mult = re * rd - 1
|
|
140
|
+
p, q = factor_from_phi_multiple(n, phi_mult)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Key insight:** Any multiple of `phi(n)` — not just `phi(n)` itself — enables factoring via the Miller-Rabin square root technique. If a server leaks `e*d` for any key pair, or if `re*rd - 1` is given, compute `gcd(a^(m/2) - 1, n)` for random `a` values. Succeeds with probability ≥ 1/2 per attempt.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## RSA Signature Forgery via Multiplicative Homomorphism (MMA CTF 2015)
|
|
148
|
+
|
|
149
|
+
**Pattern:** Signing oracle refuses to sign the target message `m` but will sign other values. Unpadded RSA is multiplicatively homomorphic: `S(a) * S(b) mod n == S(a * b) mod n`.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# Factor target message and sign each factor separately
|
|
153
|
+
divisor = 2
|
|
154
|
+
assert target_msg % divisor == 0
|
|
155
|
+
sig_a = sign_oracle(target_msg // divisor)
|
|
156
|
+
sig_b = sign_oracle(divisor)
|
|
157
|
+
forged_sig = (sig_a * sig_b) % n
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Key insight:** Textbook RSA signatures are homomorphic: `m^d mod n` preserves multiplication. If the oracle blacklists `m` but signs its factors, multiply the partial signatures. To find a suitable factorization, try small divisors (2, 3, ...) until `m / divisor` also passes the blacklist check. This is why PKCS#1 padding is essential — padded messages cannot be factored into other valid padded messages.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Weak RSA Key Generation via Base Representation (Sharif CTF 2016)
|
|
165
|
+
|
|
166
|
+
When RSA primes are generated as `p = kp * B + tp` where B = product_of_small_primes * 2^400 and kp is small (< 2^12):
|
|
167
|
+
|
|
168
|
+
1. **Compute n mod B^2:** Since n = p*q and both p,q have form k*B + t, expansion gives: `n = kp*kq*B^2 + (kp*tq + kq*tp)*B + tp*tq`
|
|
169
|
+
2. **Recover kp*kq:** Brute-force 2^24 possibilities for (kp, kq) where each < 2^12
|
|
170
|
+
3. **Solve quadratic:** From known kp*kq and the middle coefficient, recover tp and tq
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
B = product_of_first_443_primes * (2**400)
|
|
174
|
+
B2 = B * B
|
|
175
|
+
|
|
176
|
+
# n = A*B^2 + C*B + D where A=kp*kq, D=tp*tq
|
|
177
|
+
A = n // B2
|
|
178
|
+
D = n % B
|
|
179
|
+
|
|
180
|
+
# Brute-force kp, kq such that kp*kq == A
|
|
181
|
+
for kp in range(1, 2**12):
|
|
182
|
+
if A % kp == 0:
|
|
183
|
+
kq = A // kp
|
|
184
|
+
# Solve for tp, tq from remaining equations
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Key insight:** Structured prime generation creates a mixed-radix representation of n, allowing efficient factorization by reducing the search space from exponential to polynomial.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## RSA with gcd(e, phi(n)) > 1 (CSAW 2015)
|
|
192
|
+
|
|
193
|
+
When `gcd(e, phi(n)) = g > 1`, standard RSA decryption fails because `d = e^(-1) mod phi(n)` doesn't exist. Instead:
|
|
194
|
+
|
|
195
|
+
1. Compute `e' = e / g` (reduced exponent)
|
|
196
|
+
2. Compute `d' = e'^(-1) mod phi(n)` (now coprime)
|
|
197
|
+
3. Compute `m^g = pow(c, d', n)` (partial decryption)
|
|
198
|
+
4. Take g-th root: iterate candidate m values where `pow(m, g, n) == m^g`
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from sympy import factorint, mod_inverse
|
|
202
|
+
from gmpy2 import iroot
|
|
203
|
+
|
|
204
|
+
g = gcd(e, phi_n)
|
|
205
|
+
e_prime = e // g
|
|
206
|
+
d_prime = mod_inverse(e_prime, phi_n)
|
|
207
|
+
m_g = pow(c, d_prime, n)
|
|
208
|
+
|
|
209
|
+
# For small g, try integer root first
|
|
210
|
+
m, is_exact = iroot(m_g, g)
|
|
211
|
+
if is_exact:
|
|
212
|
+
plaintext = int(m)
|
|
213
|
+
else:
|
|
214
|
+
# Brute-force: m_g + k*n for small k
|
|
215
|
+
for k in range(10000):
|
|
216
|
+
m, exact = iroot(m_g + k * n, g)
|
|
217
|
+
if exact:
|
|
218
|
+
plaintext = int(m)
|
|
219
|
+
break
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Key insight:** Reduce e by the GCD to make decryption possible, then recover the g-th root. Filter candidates by checking plaintext length or ASCII validity.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Batch GCD for Shared Prime Factoring (BSidesSF 2025)
|
|
227
|
+
|
|
228
|
+
When multiple RSA moduli share a common prime factor (due to faulty hardware RNG, smartcard bugs, or weak seeding):
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from math import gcd
|
|
232
|
+
from functools import reduce
|
|
233
|
+
|
|
234
|
+
def batch_gcd(moduli):
|
|
235
|
+
"""Find shared factors among a list of RSA moduli"""
|
|
236
|
+
# Product tree
|
|
237
|
+
product = reduce(lambda a, b: a * b, moduli)
|
|
238
|
+
|
|
239
|
+
factors = {}
|
|
240
|
+
for n in moduli:
|
|
241
|
+
g = gcd(n, product // n)
|
|
242
|
+
if g != 1 and g != n:
|
|
243
|
+
p = g
|
|
244
|
+
q = n // p
|
|
245
|
+
factors[n] = (p, q)
|
|
246
|
+
return factors
|
|
247
|
+
|
|
248
|
+
# Usage: given list of public keys from smartcards
|
|
249
|
+
moduli = [key.n for key in public_keys]
|
|
250
|
+
shared = batch_gcd(moduli)
|
|
251
|
+
for n, (p, q) in shared.items():
|
|
252
|
+
phi = (p - 1) * (q - 1)
|
|
253
|
+
d = pow(e, -1, phi) # Private exponent
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
For keys with patterned primes (hardware RNG faults producing primes with fixed bit patterns), combine with Coppersmith's method to recover remaining random bits. See [advanced-math.md](advanced-math.md) for Coppersmith.
|
|
257
|
+
|
|
258
|
+
**Key insight:** A single shared prime compromises both keys. Batch GCD runs in O(n log n) time via product/remainder trees, making it feasible for thousands of keys. Real-world incidents: Taiwanese citizen smartcards (2013), many IoT device certificates.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## RSA Partial Key Recovery from dp dq qinv (0CTF 2016)
|
|
263
|
+
|
|
264
|
+
**Pattern:** Given only the CRT (Chinese Remainder Theorem) exponents (dp, dq, qinv) from a partial PEM (Privacy Enhanced Mail) key leak (e.g., bottom portion of private key file), recover the full key. Since `dp = d mod (p-1)`, iterate k and check if `p = (dp * e - 1) / k + 1` is prime.
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
import gmpy2
|
|
268
|
+
# dp, dq, qinv extracted from partial PEM; e is known (usually 65537)
|
|
269
|
+
for k in range(3, e):
|
|
270
|
+
p_candidate = (dp * e - 1) // k + 1
|
|
271
|
+
if gmpy2.is_prime(p_candidate):
|
|
272
|
+
p = p_candidate
|
|
273
|
+
break
|
|
274
|
+
# Similarly recover q from dq; verify qinv * q % p == 1
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Key insight:** Leaking just the CRT exponents from an RSA private key is sufficient to fully recover p and q. Recovery is O(e) -- essentially instant for e=65537.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## RSA-CRT Fault Attack / Bit-Flip Recovery (CSAW CTF 2016)
|
|
282
|
+
|
|
283
|
+
RSA signing service with intermittent single-bit errors in private exponent d during CRT computation. Collect valid and faulty signatures, then detect which bit flipped.
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from Crypto.Util.number import inverse
|
|
287
|
+
|
|
288
|
+
def recover_d_bits(n, e, valid_sig, faulty_sigs, msg):
|
|
289
|
+
"""Recover private key d bit-by-bit from CRT fault signatures"""
|
|
290
|
+
d_bits = [0] * 1024
|
|
291
|
+
m = pow(msg, 1, n)
|
|
292
|
+
s_good = valid_sig
|
|
293
|
+
for s_bad in faulty_sigs:
|
|
294
|
+
# ratio reveals which bit was flipped
|
|
295
|
+
ratio = (s_bad * inverse(s_good, n)) % n
|
|
296
|
+
for k in range(1024):
|
|
297
|
+
if ratio == pow(2, pow(2, k, n), n) or ratio == pow(inverse(2, n), pow(2, k, n), n):
|
|
298
|
+
d_bits[k] = 1
|
|
299
|
+
break
|
|
300
|
+
return d_bits
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Key insight:** When RSA-CRT has a single-bit fault in d, the ratio `faulty_sig * valid_sig^(-1) mod n` equals `2^(2^k) mod n` for the flipped bit position k, enabling bit-by-bit private key recovery.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## RSA Homomorphic Decryption Oracle Bypass (ECTF 2016)
|
|
308
|
+
|
|
309
|
+
Service decrypts any ciphertext except the target flag ciphertext. Exploit RSA's multiplicative homomorphism: `Dec(a * b mod n) = Dec(a) * Dec(b) mod n`.
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from Crypto.Util.number import long_to_bytes, inverse
|
|
313
|
+
|
|
314
|
+
# Server refuses to decrypt enc_flag directly
|
|
315
|
+
# But RSA is homomorphic: Dec(A*B) = Dec(A) * Dec(B) mod n
|
|
316
|
+
enc_2 = pow(2, e, n) # encrypt the number 2
|
|
317
|
+
enc_flag_times_2 = (enc_flag * enc_2) % n # = Enc(flag * 2)
|
|
318
|
+
|
|
319
|
+
dec_flag_times_2 = oracle_decrypt(enc_flag_times_2) # server allows this
|
|
320
|
+
dec_2 = oracle_decrypt(enc_2) # server allows this
|
|
321
|
+
|
|
322
|
+
# Recover flag: (flag * 2) * inverse(2) mod n = flag
|
|
323
|
+
flag = (dec_flag_times_2 * inverse(dec_2, n)) % n
|
|
324
|
+
print(long_to_bytes(flag))
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Key insight:** RSA without padding is multiplicatively homomorphic. Multiply the forbidden ciphertext by `Enc(r)` for any `r`, decrypt the product, then divide by `r` to recover the original plaintext.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## RSA with Small Prime Factors and CRT Decomposition (Hack The Vote 2016)
|
|
332
|
+
|
|
333
|
+
RSA modulus composed of many small prime factors (primes < 251, each appearing ~1500 times). Factor n, then use CRT to decompose decryption.
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from sympy import factorint
|
|
337
|
+
from sympy.ntheory.residues import primitive_root
|
|
338
|
+
from functools import reduce
|
|
339
|
+
|
|
340
|
+
n = ... # modulus with small factors
|
|
341
|
+
e = 65537
|
|
342
|
+
c = ... # ciphertext
|
|
343
|
+
|
|
344
|
+
factors = factorint(n) # {p1: k1, p2: k2, ...} where pi are small primes
|
|
345
|
+
|
|
346
|
+
# Decrypt modulo each prime power, then combine with CRT
|
|
347
|
+
from sympy.ntheory.modular import crt as chinese_remainder_theorem
|
|
348
|
+
|
|
349
|
+
remainders = []
|
|
350
|
+
moduli = []
|
|
351
|
+
for p, k in factors.items():
|
|
352
|
+
pk = p ** k
|
|
353
|
+
phi_pk = (p - 1) * p ** (k - 1) # Euler's totient for prime power
|
|
354
|
+
d_pk = pow(e, -1, phi_pk)
|
|
355
|
+
m_pk = pow(c, d_pk, pk)
|
|
356
|
+
remainders.append(m_pk)
|
|
357
|
+
moduli.append(pk)
|
|
358
|
+
|
|
359
|
+
# Combine using CRT
|
|
360
|
+
m = chinese_remainder_theorem(moduli, remainders)[0]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Key insight:** When n has many small prime factors, compute `d mod phi(p^k)` for each prime power independently, decrypt mod each, then combine via CRT. Much faster than computing `d mod phi(n)` directly.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## RSA Timing Attack on Montgomery Reduction (DEF CON 2017)
|
|
368
|
+
|
|
369
|
+
**Pattern:** Recover RSA private key bits via Kocher's timing attack when the number of modular subtractions during Montgomery reduction is leaked.
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
# Montgomery multiplication: extra subtraction when result >= modulus
|
|
373
|
+
# Leaked: count of extra subtractions per signature operation
|
|
374
|
+
# Attack: predict subtraction count for each private key bit guess
|
|
375
|
+
|
|
376
|
+
# For each bit position i (MSB to LSB):
|
|
377
|
+
# Guess bit = 0: predict timing for square only
|
|
378
|
+
# Guess bit = 1: predict timing for square + multiply
|
|
379
|
+
# Compare predictions against observed timing data
|
|
380
|
+
# Correct guess produces statistically significant correlation
|
|
381
|
+
|
|
382
|
+
# ~200K signatures needed for 768-bit key recovery
|
|
383
|
+
# Attacking squaring reduction is more effective than multiply
|
|
384
|
+
import numpy as np
|
|
385
|
+
for bit_pos in range(key_bits):
|
|
386
|
+
for guess in [0, 1]:
|
|
387
|
+
predicted = predict_reductions(known_bits + [guess], messages)
|
|
388
|
+
correlation = np.corrcoef(predicted, observed)[0, 1]
|
|
389
|
+
known_bits.append(0 if corr_0 > corr_1 else 1)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Key insight:** Montgomery multiplication performs an extra conditional subtraction when the intermediate result exceeds the modulus. If this count leaks (via timing, power, or as in this CTF -- directly), each bit of the private exponent can be determined by comparing predicted vs. observed subtraction patterns across many operations.
|
|
393
|
+
|
|
394
|
+
**References:** DEF CON 2017
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Bleichenbacher Low-Exponent RSA Signature Forgery (Google CTF 2017)
|
|
399
|
+
|
|
400
|
+
**Pattern:** Forge PKCS#1 v1.5 RSA signatures when e=3 by constructing a value whose cube root produces valid padding without knowing the private key.
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
# PKCS#1 v1.5 signature format:
|
|
404
|
+
# 00 01 FF FF ... FF 00 [DigestInfo] [Hash]
|
|
405
|
+
# With e=3, forge signature s where s^3 has correct prefix
|
|
406
|
+
|
|
407
|
+
# Construct target block (2048-bit key, SHA-256):
|
|
408
|
+
# 00 01 FF ... FF 00 [SHA-256 DigestInfo] [hash] [garbage]
|
|
409
|
+
import gmpy2
|
|
410
|
+
prefix = b'\x00\x01' + b'\xff' * padding_len + b'\x00' + digest_info + hash_value
|
|
411
|
+
# Convert to integer, append zeros for garbage bytes
|
|
412
|
+
target = int.from_bytes(prefix + b'\x00' * garbage_len, 'big')
|
|
413
|
+
# Cube root (rounds down, garbage absorbs the remainder)
|
|
414
|
+
forged_sig = gmpy2.iroot(target, 3)[0] + 1 # +1 to round up
|
|
415
|
+
# Verify: forged_sig^3 starts with correct PKCS#1 prefix
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Key insight:** PKCS#1 v1.5 signature verification checks that `sig^e mod n` starts with `00 01 FF...FF 00 DigestInfo Hash`. With e=3, an attacker computes the cube root of a carefully constructed value with the correct prefix and ignores trailing bytes. Implementations that don't verify the padding extends to the full block length (CVE-2006-4339) accept the forgery.
|
|
419
|
+
|
|
420
|
+
**References:** Google CTF 2017
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Coppersmith Small Roots for Linearly Related Primes (Tokyo Westerns 2017)
|
|
425
|
+
|
|
426
|
+
**Pattern:** RSA where `q = k*p + delta` for a known small constant `k` and unknown small `delta`. Since `p ≈ sqrt(N/k)`, approximate `q_approx = k * isqrt(N // k) + 2^512` as an upper bound. The univariate polynomial `F(x) = q_approx - x` has `delta` as a small root modulo `q` (which divides N). Coppersmith's method finds this root when `delta < N^(1/4)`.
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
from sage.all import *
|
|
430
|
+
|
|
431
|
+
N, e, c = ... # RSA parameters
|
|
432
|
+
k = 19 # known relationship: q = k*p + delta
|
|
433
|
+
|
|
434
|
+
# Approximate q from sqrt(N/k)
|
|
435
|
+
q_approx = k * isqrt(N // k) + 2**512
|
|
436
|
+
|
|
437
|
+
# Build univariate polynomial with q_approx as approximate root of N mod q
|
|
438
|
+
R.<x> = PolynomialRing(Zmod(N))
|
|
439
|
+
f = q_approx - x # root is delta = q_approx - q
|
|
440
|
+
|
|
441
|
+
# Coppersmith: find small root x = delta < N^0.25
|
|
442
|
+
roots = f.small_roots(X=2**512, beta=0.5)
|
|
443
|
+
if roots:
|
|
444
|
+
q = int(q_approx - roots[0])
|
|
445
|
+
p = N // q
|
|
446
|
+
assert p * q == N
|
|
447
|
+
phi = (p - 1) * (q - 1)
|
|
448
|
+
d = pow(e, -1, phi)
|
|
449
|
+
flag = long_to_bytes(pow(c, d, N))
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Key insight:** When `q ≈ k*p`, approximately half the bits of `p` (and `q`) are recoverable from `sqrt(N/k)`. The remaining unknown `delta` is small enough for Coppersmith when `delta < N^(1/4)`. The upper bound `q_approx` must exceed `q`; add a safety margin of `2^(bitlen/2)` to ensure the root is captured.
|
|
453
|
+
|
|
454
|
+
**References:** Tokyo Westerns CTF 2017
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## ROCA Attack on RSA CVE-2017-15361 (EasyCTF IV)
|
|
459
|
+
|
|
460
|
+
**Pattern:** Infineon RSA library generates keys with structured primes (detectable via fingerprint). Factor 512-bit keys in minutes:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# Detect ROCA vulnerability
|
|
464
|
+
pip install roca-detect
|
|
465
|
+
roca-detect rsa_key.pub
|
|
466
|
+
# Factor with neca tool
|
|
467
|
+
git clone https://gitlab.com/jix/neca.git
|
|
468
|
+
cd neca && cargo build --release
|
|
469
|
+
./target/release/neca <N_decimal>
|
|
470
|
+
# Or use: https://github.com/crocs-muni/roca (original research tool)
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Key insight:** CVE-2017-15361 affects Infineon TPMs, smart cards, and YubiKey 4. Primes have the form `p = k * M + (65537^a mod M)` where M is the product of first n primes. Detection is instant (check `65537^x mod M` divides `n mod M`). Factoring uses Coppersmith's method on the known structure. Keys up to 2048 bits are practically attackable. For CTF use: if `roca-detect` reports vulnerable, use `neca` for 512-bit keys or the `crocs-muni/roca` tool for larger keys.
|
|
474
|
+
|
|
475
|
+
**References:** EasyCTF IV (2018)
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
### RSA Signature Bypass with e=1 and Crafted Modulus (BackdoorCTF 2018)
|
|
480
|
+
|
|
481
|
+
**Pattern:** Server generates RSA signature and asks for `(n, e)` to verify it. With `e=1`, `pow(s, 1, n) = s mod n`. Set `n = signature - PKCS1_pad(message)` so verification passes. (BackdoorCTF 2018)
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
e = 1
|
|
485
|
+
n = signature ** e - PKCS1_pad(h.hexdigest())
|
|
486
|
+
# Now pow(signature, 1, n) == PKCS1_pad(message)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Key insight:** When the verifier accepts user-supplied public key parameters without constraints, setting `e=1` makes modular exponentiation trivial. Choose `n` such that `signature mod n` equals the expected padded hash.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Dependent-Prime RSA: q = e^-1 mod p (TokyoWesterns CTF 4th 2018)
|
|
494
|
+
|
|
495
|
+
**Pattern:** Key generation picks prime `p`, then sets `q = e^(-1) mod p` and rerolls until `q` is prime. The dependency `e*q ≡ 1 (mod p)` means `e*q = k*p + 1` for some small positive integer `k`, so `n = p*q = p*(k*p + 1)/e` — a quadratic in `p` that factors after enumerating a few candidate `k` values.
|
|
496
|
+
|
|
497
|
+
**Exploit:**
|
|
498
|
+
```python
|
|
499
|
+
from sage.all import PolynomialRing, ZZ
|
|
500
|
+
|
|
501
|
+
def factor_dependent_n(n, e, max_k=100000):
|
|
502
|
+
P = PolynomialRing(ZZ, 'p'); p = P.gen()
|
|
503
|
+
for k in range(2, max_k, 2):
|
|
504
|
+
# e*q = k*p + 1 and n = p*q => e*n = p*(k*p + 1)
|
|
505
|
+
poly = k * p * p + p - e * n
|
|
506
|
+
roots = poly.roots()
|
|
507
|
+
for root, _ in roots:
|
|
508
|
+
if root > 1 and n % root == 0:
|
|
509
|
+
return int(root), n // int(root)
|
|
510
|
+
return None
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Key insight:** Any key generator that derives `q` from `p` via a public arithmetic relation collapses RSA security to a small search. Write the relation as a polynomial `f_k(p) = 0` parameterized by a small integer, and use `.roots()` in Sage to recover `p`. The search space for `k` is typically under 2^16 because `q < p` forces `k ≈ e`.
|
|
514
|
+
|
|
515
|
+
**References:** TokyoWesterns CTF 4th 2018 — writeup 10862
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## RSA Three-Key Pairwise GCD Triangle (Trend Micro 2018)
|
|
520
|
+
|
|
521
|
+
**Pattern:** Three RSA moduli `N1 = p1*p2`, `N2 = p1*p3`, `N3 = p2*p3` share primes pairwise (every pair has exactly one common factor). A single pairwise `gcd` reveals the shared prime, giving a complete factorisation of all three moduli without running batch-GCD.
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
from math import gcd
|
|
525
|
+
|
|
526
|
+
def factor_triangle(n1, n2, n3):
|
|
527
|
+
p1 = gcd(n1, n2) # shared between N1 and N2
|
|
528
|
+
p2 = gcd(n1, n3) # shared between N1 and N3
|
|
529
|
+
p3 = gcd(n2, n3) # shared between N2 and N3
|
|
530
|
+
assert n1 == p1 * p2 and n2 == p1 * p3 and n3 == p2 * p3
|
|
531
|
+
return p1, p2, p3
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Key insight:** Batch-GCD handles arbitrary sets of moduli that might share a prime, but the three-key triangle is a closed-form case: every prime appears in exactly two moduli, so three `gcd()` calls factor all three keys. Spot the pattern when a challenge hands you exactly three public keys with matching bit-lengths and no other hint — contrast with [batch GCD for shared prime factoring](#batch-gcd-for-shared-prime-factoring-bsidessf-2025), which is the general case over larger key sets.
|
|
535
|
+
|
|
536
|
+
**References:** Trend Micro CTF 2018 — Offensive-Analysis 300, writeup 11129
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## RSA n = p^2*q Schmidt-Samoa Variant (ASIS Finals 2018)
|
|
541
|
+
|
|
542
|
+
**Pattern:** Modulus is generated as `n = p*p*q` (not the usual `p*q`). Naive `phi = (p-1)*(q-1)` is wrong — real totient is `phi = p*(p-1)*(q-1)`. If `gcd(e, phi) != 1`, reduce ciphertext to smaller field `q` and invert there.
|
|
543
|
+
|
|
544
|
+
```python
|
|
545
|
+
phi = p*(p-1)*(q-1)
|
|
546
|
+
# Reduce enc to mod-q by inverse_mod(q, phi)
|
|
547
|
+
qinv = inverse_mod(q, phi)
|
|
548
|
+
enc = pow(enc, qinv, n) % q
|
|
549
|
+
# Now m < q; invert e*p^2 over phi(q) = q-1
|
|
550
|
+
pinv = inverse_mod(p*p, q-1)
|
|
551
|
+
m = pow(enc, pinv, q)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Key insight:** Spot `n = p^2*q` by trying `iroot(n, 3)` near its cube root and running Pollard/Lenstra on small fraction of `n`. Once factored, switch to the correct totient and field before inversion.
|
|
555
|
+
|
|
556
|
+
**References:** ASIS CTF Finals 2018 — John-Bull, writeup 12401
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Modulus Recovery via GCD of Encryption Residuals (X-MAS CTF 2018)
|
|
561
|
+
|
|
562
|
+
**Pattern:** Oracle encrypts arbitrary plaintexts with a hidden RSA key but does not disclose `n`. Compute `m^e - enc(m)` for two plaintexts; both differences are multiples of `n`, so their GCD equals `n` (or a small multiple).
|
|
563
|
+
|
|
564
|
+
```python
|
|
565
|
+
e = 65537
|
|
566
|
+
r1 = bytes_to_long(b'a')**e - encrypt(b'a')
|
|
567
|
+
r2 = bytes_to_long(b'b')**e - encrypt(b'b')
|
|
568
|
+
n = gcd(r1, r2)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Key insight:** Works whenever the oracle is a black box but accepts chosen plaintexts. Strip small prime factors from the GCD to recover the true `n`.
|
|
572
|
+
|
|
573
|
+
**References:** X-MAS CTF 2018 — Santa's list, writeups 12701-12800
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Textbook RSA Negation via encrypt(-1) (X-MAS CTF 2018)
|
|
578
|
+
|
|
579
|
+
**Pattern:** Decrypt oracle refuses to decrypt the target ciphertext directly but accepts multiplications. Because `(-1)^e ≡ -1 (mod n)`, multiplying by `pow(-1, e, n)` flips the plaintext sign modulo `n`.
|
|
580
|
+
|
|
581
|
+
```python
|
|
582
|
+
ct_mutated = (ct_flag * pow(-1, e, n)) % n
|
|
583
|
+
plaintext = (-decrypt(ct_mutated)) % n
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Key insight:** Any odd `e` makes `encrypt(-1) = -1 mod n`, so the oracle returns `-m mod n` for the original ciphertext even if the original `m` is blacklisted.
|
|
587
|
+
|
|
588
|
+
**References:** X-MAS CTF 2018 — Santa's list 2.0, writeups 12701-12800
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Poly-Exponent RSA: GCD of p^p Combinations (ASIS Finals 2018)
|
|
593
|
+
|
|
594
|
+
**Pattern:** Challenge publishes several linear/polynomial combinations of `p` and `q`, e.g. `c1 = p^p mod q`, `c2 = (p+q)^(p+q) mod n`. Form two expressions both divisible by the target prime, then take their GCD to factor `n`.
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
p = gcd(c2*c4 - c3, pow(c4, c4, c2*c4 - c3) - c3)
|
|
598
|
+
q = gcd(c1*c4 - c3, pow(c4, c4, c1*c4 - c3) - c3)
|
|
599
|
+
for x in small_primes:
|
|
600
|
+
while p % x == 0: p //= x
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Key insight:** If two quantities share a secret factor but differ only by known scalars, `gcd(a, b)` recovers that factor. Strip cofactors with trial division on small primes.
|
|
604
|
+
|
|
605
|
+
**References:** ASIS CTF Finals 2018 — NMC, writeup 12427
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Biased LSB Oracle with Mode-of-Runs Recovery (CSAW CTF 2018)
|
|
610
|
+
|
|
611
|
+
**Pattern:** LSB oracle leaks the least significant bit of `m * 2^i mod n`, but the binary search converges inconsistently (oracle gives slightly wrong answers at random). Run the full recovery loop many times and take the per-byte mode across runs — correct bytes appear most often even when the full plaintext never converges.
|
|
612
|
+
|
|
613
|
+
```python
|
|
614
|
+
def recover():
|
|
615
|
+
beg, end = 0, n - 1
|
|
616
|
+
for bit in bits:
|
|
617
|
+
mid = (beg + end) // 2
|
|
618
|
+
if bit: beg = mid
|
|
619
|
+
else: end = mid
|
|
620
|
+
return long_to_bytes(end)
|
|
621
|
+
|
|
622
|
+
byte_counts = [Counter() for _ in range(flag_len)]
|
|
623
|
+
for _ in range(N):
|
|
624
|
+
flag = recover()
|
|
625
|
+
for i, b in enumerate(flag):
|
|
626
|
+
byte_counts[i][b] += 1
|
|
627
|
+
flag = bytes(c.most_common(1)[0][0] for c in byte_counts)
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Key insight:** Noisy side channels still leak the correct byte per position if each run independently biases toward the true value. Vote across runs rather than trying to perfect a single decryption.
|
|
631
|
+
|
|
632
|
+
**References:** CSAW CTF 2018 — Lost Mind, writeup 12490
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## Cube-Root Wraparound via AES-CTR Length Hint (hxp 2018)
|
|
637
|
+
|
|
638
|
+
**Pattern:** Low-exponent RSA (`e = 3`) with padded plaintext where `m^3 > n`, so naive cube root fails. AES-CTR ciphertext reveals the plaintext length (CTR preserves length), so you know exactly how many `n` to add. Rescale with `inverse(2, n)^k` to cancel padding shifts, then try `iroot(c + k*n, 3)` for small `k`.
|
|
639
|
+
|
|
640
|
+
```python
|
|
641
|
+
inv = pow(inverse(2, n), 2040, n)
|
|
642
|
+
c = c * inv % n
|
|
643
|
+
for k in range(1000):
|
|
644
|
+
m, ok = gmpy2.iroot(c + k*n, 3)
|
|
645
|
+
if ok: flag = long_to_bytes(int(m)); break
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
**Key insight:** When padding shifts the plaintext past `n`, the correct root lies in `c + k*n` for some small `k`; AES-CTR leaks the exact plaintext length so you can size the search.
|
|
649
|
+
|
|
650
|
+
**References:** hxp CTF 2018 — daring, writeup 12588
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## RSA p = next_prime(2^k + small) Shared-Prime Batch GCD (ASIS Finals 2018)
|
|
655
|
+
|
|
656
|
+
**Pattern:** Key generator constructs `p = next_prime(2^k + random_small_delta)` where `delta` is bounded by a few bits. Two independent keys generated with the same `k` converge on the same next prime whenever their `delta` values fall between the same two consecutive primes — common enough that any pair of collected moduli has `gcd(n1, n2) > 1`.
|
|
657
|
+
|
|
658
|
+
```python
|
|
659
|
+
from math import gcd
|
|
660
|
+
for n_a in collected:
|
|
661
|
+
for n_b in collected:
|
|
662
|
+
if n_a == n_b: continue
|
|
663
|
+
p = gcd(n_a, n_b)
|
|
664
|
+
if 1 < p < n_a:
|
|
665
|
+
q = n_a // p
|
|
666
|
+
d = pow(e + 2, -1, (p-1)*(q-1)) # NMC-style
|
|
667
|
+
break
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
**Key insight:** Any RSA keygen that sources primes from a low-entropy neighborhood (`next_prime(constant + small)`, `2^k + random_byte`, `Mersenne ± delta`) collapses to pairwise GCD because the `next_prime` step collapses many deltas onto the same prime.
|
|
671
|
+
|
|
672
|
+
**References:** ASIS CTF Finals 2018 — Ariogen, writeup 12414
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## PNG Encryption Bounded by 512-bit Key → Trailer Replacement (ASIS Finals 2018)
|
|
677
|
+
|
|
678
|
+
**Pattern:** Custom "polynomial bit sum" cipher encrypts PNG bytes as `C = sum(bit_i * (exp^i + (-1)^i))`. The key length (≤512 bits) means at most 64 bytes of plaintext are affected. Since PNG's trailing IDAT + IEND chunks are format-recoverable from any reference image, simply splice a valid trailer onto the decrypted prefix.
|
|
679
|
+
|
|
680
|
+
```python
|
|
681
|
+
# Only the first 64 bytes of output depend on the key; the rest matches plaintext bit-for-bit
|
|
682
|
+
first64 = decrypt_affected_prefix(ct)
|
|
683
|
+
rest = original_png[64:] # copy from any reference PNG with same image data
|
|
684
|
+
open('recovered.png', 'wb').write(first64 + rest)
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Key insight:** Whenever the key size caps the *number of affected bytes*, tail-of-file reconstruction beats full cryptanalysis. Works for any format (PNG, ZIP, MP3, PDF) where the tail has standard chunks or a parser tolerates garbage.
|
|
688
|
+
|
|
689
|
+
**References:** ASIS CTF Finals 2018 — Made by baby, writeup 12423
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Modulus Recovery via Plaintext Malleability (X-MAS 2018)
|
|
694
|
+
|
|
695
|
+
**Pattern:** Oracle decrypts ciphertext and returns the plaintext but hides the modulus `N`. Send two related ciphertexts `c` and `c * 2^e`. The decrypted plaintexts are `m` and `(2m) mod N`. If `2*m != (2m mod N)`, the difference `2m - (2m mod N) = N` recovers the modulus.
|
|
696
|
+
|
|
697
|
+
```python
|
|
698
|
+
m1 = decrypt(ct) # m
|
|
699
|
+
m2 = decrypt((ct * pow(2, e, m1)) % m1) # 2m mod N
|
|
700
|
+
N = 2*m1 - m2 if 2*m1 != m2 else None
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
**Key insight:** RSA's multiplicative homomorphism leaks the modulus whenever you can send `c * k^e` and observe the wraparound. Use any small multiplier that nudges the plaintext past `N`.
|
|
704
|
+
|
|
705
|
+
**References:** X-MAS CTF 2018 — Santa's list, writeup 12658
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## RSA CRT d_p NULL-Byte Overflow Primes Leak (P.W.N. CTF 2018)
|
|
710
|
+
|
|
711
|
+
**Pattern:** Server reads `d_p` into a buffer via `fgets()` that lacks a bounds check. Sending 33+ null bytes NULL-terminates `d_p_str` so the parsed integer is 0. The CRT path computes `m_2 = 0^0 = 1`, then signs `m - m_2 = m - 1`, which when squared in the gcd step yields `p = gcd(sig, N)`.
|
|
712
|
+
|
|
713
|
+
```python
|
|
714
|
+
io.send(b'\x00' * 40) # overflow d_p buffer
|
|
715
|
+
sig = io.recvline()
|
|
716
|
+
p = gcd(sig - 1, N) # factored!
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
**Key insight:** Any CRT-based RSA implementation that parses private key fields from network input is vulnerable to "make `d_p` equal to 0" tricks. The gcd of a forged signature and the modulus reveals a prime.
|
|
720
|
+
|
|
721
|
+
**References:** P.W.N. CTF 2018 — City RSA, writeup 12052
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## Textbook RSA Signature Blinding via Message Factoring (P.W.N. CTF 2018)
|
|
726
|
+
|
|
727
|
+
**Pattern:** Unpadded RSA refuses to sign certain messages (blacklist), but multiplicative homomorphism lets you decompose the forbidden message `m` into coprime factors `m = x * y mod N`, request signatures for `x` and `y` separately, and combine: `sig(m) = sig(x) * sig(y) mod N`.
|
|
728
|
+
|
|
729
|
+
```python
|
|
730
|
+
for x in range(2, 10000):
|
|
731
|
+
if (m * pow(x, -1, N)) % N == allowed_y:
|
|
732
|
+
y = allowed_y
|
|
733
|
+
sig = (sign(x) * sign(y)) % N
|
|
734
|
+
break
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Key insight:** Any unpadded RSA signing oracle is vulnerable to factoring-based blinding. The attacker only needs two valid-looking messages whose product equals the target.
|
|
738
|
+
|
|
739
|
+
**References:** P.W.N. CTF 2018 — City RSA, writeup 12052
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## Last-Byte Modulus Overwrite via strlen-1 Null Truncation (OTW Advent 2018)
|
|
744
|
+
|
|
745
|
+
**Pattern:** Server truncates `username[strlen(username)-1] = 0` to strip the trailing newline. Passing an empty username writes the null byte to the byte *before* the buffer — which turns out to be the last byte of `N`. The modified `N` is composite in a small way (differs from the real `N` by at most 255) and is factorable by Carmichael or `sage.factor`.
|
|
746
|
+
|
|
747
|
+
```python
|
|
748
|
+
io.sendline(b'') # empty username overwrites N[-1]
|
|
749
|
+
N_corrupt = recv_N()
|
|
750
|
+
for delta in range(256):
|
|
751
|
+
if is_factorable(N_corrupt + delta - 255):
|
|
752
|
+
N_real = N_corrupt + delta - 255; break
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
**Key insight:** "Strip the last character" idioms (`buf[strlen-1] = 0`) are classic off-by-one when `strlen == 0`. They write one byte backwards into whatever sits before the buffer — often a crypto constant.
|
|
756
|
+
|
|
757
|
+
**References:** OverTheWire Advent Bonanza 2018 — Day 14, writeup 12752
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## CRC32 Collision Oracle + RSA Homomorphic Signature Forgery (BSidesSF 2019)
|
|
762
|
+
|
|
763
|
+
**Pattern (rsaos):** Shell exposes `RSA(foldhash(cmd))` as a "signature" where `foldhash` is a 10-byte digest built from a CRC-like, factorable fold. Privileged commands are blocked, but signatures for arbitrary strings are handed out. Pick a target privileged command whose fold value factors entirely into primes `< 2^32`. For each small factor `f_i`, use [crchack](https://github.com/resilar/crchack) to craft an innocuous command whose CRC32 equals `f_i` exactly, get its signature, and multiply them modulo `N`. RSA's multiplicativity means the product is the signature of the fold *product*, which is the target fold.
|
|
764
|
+
|
|
765
|
+
```python
|
|
766
|
+
from primefac import primefac
|
|
767
|
+
import subprocess, random
|
|
768
|
+
|
|
769
|
+
def find_cmd_crc(target_crc):
|
|
770
|
+
while True:
|
|
771
|
+
open('/tmp/cmd', 'w').write(f'echo {random.randint(0, 10000)}')
|
|
772
|
+
cmd = subprocess.check_output(['./crchack/crchack', '/tmp/cmd', hex(target_crc)])
|
|
773
|
+
if b'\n' not in cmd:
|
|
774
|
+
return cmd
|
|
775
|
+
|
|
776
|
+
def find_cmd_fac(priv):
|
|
777
|
+
while True:
|
|
778
|
+
c = f'{priv} {random.randint(0, 10000)}'.encode()
|
|
779
|
+
fs = list(primefac(foldhash(c)))
|
|
780
|
+
if all(f < 2**32 for f in fs):
|
|
781
|
+
return c, fs
|
|
782
|
+
|
|
783
|
+
priv_cmd, factors = find_cmd_fac('get-flag')
|
|
784
|
+
t = 1
|
|
785
|
+
for f in factors: # each factor fits in CRC32 range
|
|
786
|
+
cmd = find_cmd_crc(f)
|
|
787
|
+
_, sig = get_sig(cmd) # oracle signs arbitrary cmd
|
|
788
|
+
t = (t * sig) % N # RSA: s1*s2 = sign(h1*h2)
|
|
789
|
+
send_priv(priv_cmd, sig=hex(t % N)) # forged signature for get-flag
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**Key insight:** Textbook RSA signatures are multiplicative: `sign(a) * sign(b) = sign(a*b) mod N`. If the "hash" is actually a linear/factorable function (CRC32, fold-XOR), factor the target digest into pieces small enough to fit in CRC output space, then use a CRC collision finder (`crchack`) to realise each factor as an innocuous message the oracle will sign. Multiply the signatures mod `N` to forge the privileged signature. Works for any signature scheme over a hash that is both homomorphic-friendly *and* collidable to specific targets.
|