CTF toolkit: lib (net, crypto_utils), templates per category, scaffold, cheatsheet, ps_and_qs example
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,63 @@
|
|||||||
|
# CTF Cheatsheet — distilled from p4-team/ctf (784 writeups)
|
||||||
|
|
||||||
|
## GENERAL WORKFLOW (their consistent pattern)
|
||||||
|
1. **Read the source first.** 80% of solutions = one logic bug. Binary/PHP/Py/Ruby.
|
||||||
|
2. **Identify category**, then apply the matching recipe below.
|
||||||
|
3. **Modular solver**: separate file `solve.py` / `exploit.py` / `attack.py` / `*.sage`.
|
||||||
|
4. **Verify each step with asserts** (like `check_jump()` in their sage code).
|
||||||
|
5. Print the flag; never hardcode it.
|
||||||
|
6. Use `scaffold.py` to generate event + task skeletons.
|
||||||
|
|
||||||
|
## WEB
|
||||||
|
- **Sanitization order bug (piapiapia)**: `filter()` ran AFTER `serialize()` ->
|
||||||
|
string-length change lets you inject into serialized object. Bypass validation
|
||||||
|
with arrays (`nickname[]=`).
|
||||||
|
- **Read provided source/PHP** — vuln is almost always visible. SQLi/filter bypass/
|
||||||
|
SSRF/LFI derive from the source, not black-box.
|
||||||
|
- **Tools**: requests, beautifulsoup, burp, sometimes selenium.
|
||||||
|
|
||||||
|
## BINARY / PWN
|
||||||
|
- **Leak first**: format string `%p %p %p...` or GOT leak, then ROP.
|
||||||
|
- **ret2libc**: leak libc base -> one_gadget / system("/bin/sh").
|
||||||
|
- **Stack overflow + CET (smash)**: emulator CET config block at fixed offset from
|
||||||
|
libc; write 0 to disable, then free ROP.
|
||||||
|
- **Arbitrary write primitive**: overwrite saved RBP to control a later frame pointer.
|
||||||
|
- **Debugger harness**: script a remote debugger (breakpoints, read/mod registers)
|
||||||
|
to dump memory (registers_matter).
|
||||||
|
- **Tools**: pwntools (remote/ELF/ROP/context), gdb+gef/pwndbg, checksec, ROPgadget.
|
||||||
|
|
||||||
|
## RE (reverse engineering)
|
||||||
|
- **Static-first**: IDA/Ghidra; extract `.rodata` bytes -> often just RSA params.
|
||||||
|
- **RSA-from-dump (reversing_is_amazing)**: parse `db XXh` lines -> `RSA.importKey`
|
||||||
|
-> decrypt given ciphertext.
|
||||||
|
- **Symbolic execution**: angr to reach a "win" state, avoiding "fail" states.
|
||||||
|
- **Emulation / patching**: unicorn to emulate a function; lief to patch binaries.
|
||||||
|
- **Tools**: IDA, ghidra, radare2, lief, pyelftools, angr, unicorn, capstone.
|
||||||
|
|
||||||
|
## MISC
|
||||||
|
- **Oracle byte-by-byte (heXdump)**: `xxd -r -ps` does NOT truncate -> overwrite 1
|
||||||
|
byte, match output, recover flag char-by-char over CHARSET.
|
||||||
|
- **Encoding chains**: brute b64/b32/b16/hex until "flag"/"CTF" appears.
|
||||||
|
- **PRNG reversing (xor_and_shift)**: linear PRNG over GF(2) -> symbolic exec +
|
||||||
|
matrix exponentiation in sage to "jump" the state.
|
||||||
|
- **Constraint solving**: z3 when inputs must satisfy arithmetic conditions.
|
||||||
|
|
||||||
|
## FORENSICS
|
||||||
|
- **PCAP**: tshark/scapy to extract streams; look for exfil/TLS keys.
|
||||||
|
- **Memory**: volatility (imageinfo, pslist, dump).
|
||||||
|
- **Stego**: PIL for pixel work; binwalk for appended data; audio via spectrogram.
|
||||||
|
- **Tools**: scapy, tshark/wireshark, volatility, PIL, binwalk.
|
||||||
|
|
||||||
|
## CRYPTO (bonus — most common, 123 challenges in p4)
|
||||||
|
- **RSA recover n (lost_modulus)**: have e,d,ipmq=inv(p,q),iqmp=inv(q,p), not n ->
|
||||||
|
derive quadratic in phi -> `gmpy2.iroot` -> p,q. (lib.crypto_utils.recover_n_from_keys)
|
||||||
|
- **Common modulus**: same m, same n, coprime e -> CRT combine.
|
||||||
|
- **Wiener**: small d -> continued fractions on e/n. (lib.crypto_utils.wiener)
|
||||||
|
- **Håstad broadcast**: same small m^e across moduli with small e -> CRT + e-th root.
|
||||||
|
- **Lattice/LLL**: small roots, Coppersmith, hidden-number problem.
|
||||||
|
- **Reduced-round block cipher (a2s)**: differential cryptanalysis; 2^16-bit DeltaSet.
|
||||||
|
- **Tools**: pycryptodome, gmpy2, sage, numpy, z3, angr (rare).
|
||||||
|
|
||||||
|
## QUICK SETUP
|
||||||
|
pip install pwntools pycryptodome gmpy2 requests beautifulsoup4 pillow scapy
|
||||||
|
# + sage, z3-solver, angr, capstone, unicorn, lief (as needed)
|
||||||
Binary file not shown.
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
EXAMPLE: SECCON 2017 Quals — "Ps and Qs" (Crypto, 200p)
|
||||||
|
https://github.com/p4-team/ctf/tree/master/2017-12-09-seccon-quals/crypto_ps_and_qs
|
||||||
|
|
||||||
|
VULN: Two RSA public keys (pub1.pub, pub2.pub) share a prime (common factor).
|
||||||
|
FACT: gcd(n1, n2) = p -> recover q1 = n1/p, q2 = n2/p -> private keys -> decrypt.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
cd /home/code/ctfkit
|
||||||
|
python3 examples/ps_and_qs.py
|
||||||
|
Expected flag: SECCON{1234567890ABCDEF}
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Util.number import long_to_bytes
|
||||||
|
|
||||||
|
sys.path.insert(0, "/home/code/ctfkit")
|
||||||
|
from lib.crypto_utils import gcd, modinv
|
||||||
|
|
||||||
|
HERE = __file__.rsplit("/", 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
pub1 = RSA.importKey(open(f"{HERE}/pub1.pub").read())
|
||||||
|
pub2 = RSA.importKey(open(f"{HERE}/pub2.pub").read())
|
||||||
|
p = gcd(pub1.n, pub2.n)
|
||||||
|
q1 = pub1.n // p
|
||||||
|
q2 = pub2.n // p
|
||||||
|
assert p * q1 == pub1.n and p * q2 == pub2.n, "common-factor failed"
|
||||||
|
msg = int.from_bytes(open(f"{HERE}/cipher", "rb").read(), "big")
|
||||||
|
|
||||||
|
d1 = modinv(pub1.e, (p - 1) * (q1 - 1))
|
||||||
|
pt = long_to_bytes(pow(msg, d1, pub1.n)).decode(errors="replace")
|
||||||
|
import re
|
||||||
|
m = re.search(r"SECCON\{[^}]+\}", pt)
|
||||||
|
flag = m.group(0) if m else pt
|
||||||
|
print("shared prime p =", p)
|
||||||
|
print("FLAG =", flag)
|
||||||
|
return flag
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAz8+77qffFDqKwgixqh0v
|
||||||
|
hlRaxMtYjJSj+xwUrZGk8Lk2FXxaS4acGKi4ZPRya/j83AIMtBBCuslnhKt9A/k3
|
||||||
|
SUfvsLw9Zlgxl0NAFZ/8PbfI50tjkP2m7sMLgcb/Yk6NP1sXv7elx//Y7PTmUYs5
|
||||||
|
Or793Q+uukMIdGumP4EGtZ1+BYlDoAExp9TlOMRksnBXdkftvEeMwc6Vhe/odzBb
|
||||||
|
OnwufETbVHXt2tw0WiyQqUZ3HKwKRUzby0YfKEDnYTyD6c7MlAN/oJu52qPxgFYs
|
||||||
|
Ad8L5sUfDAbo8OLW4aXlDQoow4gRQHcKn0WTQUa381m5Oc4j8PpQem9ORUVxQwlS
|
||||||
|
ADwg8dl6ZxQLbl/L+zs3bk4klprrHUic/HKvTxWkeIoaqXyJdW0dTZSqR+fNOoGu
|
||||||
|
y5JEjMksd9LvV2qg28E1CGKszdrdvOgDV/DNW4VN0PjEYn/ktxiyTs/hHtJMO+Iv
|
||||||
|
AGQ7vtTuXjRa8Xblt20jovgODsbzTlcYxipw/lVwwouAe0TyLq3r2bX/kG9qhb6I
|
||||||
|
wMj25fiApR8X+E2xwu7+qK80BAREztGjffDk9fcsw/ULfkJ8jC2LYYbq12LwxESz
|
||||||
|
yjoBA+0SqTvOnK50eaIp67wKZI6qb5flBRpm6wnr1zSOkvdfEl69w2fip9Had1nU
|
||||||
|
H64uJjW/S3p/kb7Ks6x9Bb0CAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuzPMf8yOyvO/ntlcWDeS
|
||||||
|
4exrgO6HXsIGTbzwdZXINEkjv1NlJNTgp1V0x3mMc7GX3SsbQgVLHknLRfvwTm8R
|
||||||
|
TPijZcPfNkVST3eCaAOKP6JoAunR7b+7Xt+1oMN1Nw1/EPV9q71Pdx2tNjLwG5vO
|
||||||
|
EEiZZu6ILasXozt4aqX3MWWlQFEwCx35KAOSo+3p0/ycTYpqBjUfbvNZjo3is507
|
||||||
|
Ga9koXFs0Vgmw/JMsT3rciw6A+8dK+LQpabiEP9dAYNnvjv5nqJroAblFkpN1Vqr
|
||||||
|
zUSd5c4YZIJdwWDlDVCesOb+cj7xgmge3blAhLg+yeLpQ+h8uHUJqw/Zscoiwc6v
|
||||||
|
85/Kz2cp/A4FeGcNh9fw+cy+Ccs+Es64lVcqmXnRC/2/r6JgVo2NsYS+ErPjGT4H
|
||||||
|
cpzjwdnNgoPtaYOgY4gDagpwKU8jOSlEd4KA596fYBY6gVDjD/Sk6gJ5LL6DBbqi
|
||||||
|
6Zr+UeF9r8Vr4NOEFHvNOOnRKTTscSYiIXdzpLOFGpsMbHw+AfYRGh4aVX9OKuSi
|
||||||
|
R86bdczMsYGYJfMFSqHAVb0+I0AJOuLvHQ+loXaCXv33lQcCf1EECAAJFC8NQ+Lx
|
||||||
|
DPrSIIE7u5AU1PQyXtrFOPtegrdT4q07JGB9c4CqZPy5i1nqi1pza4CTgySM7OCx
|
||||||
|
clXqVZ6QEn93ivbX6KZtrZECAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
lib/crypto_utils.py — common CTF crypto helpers (RSA / lattice / misc).
|
||||||
|
|
||||||
|
Patterns distilled from p4-team/ctf writeups.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
from math import gcd, isqrt
|
||||||
|
|
||||||
|
|
||||||
|
def egcd(a, b):
|
||||||
|
if b == 0:
|
||||||
|
return (a, 1, 0)
|
||||||
|
g, x, y = egcd(b, a % b)
|
||||||
|
return (g, y, x - (a // b) * y)
|
||||||
|
|
||||||
|
|
||||||
|
def modinv(a, m):
|
||||||
|
g, x, _ = egcd(a % m, m)
|
||||||
|
if g != 1:
|
||||||
|
raise ValueError("modinv: no inverse")
|
||||||
|
return x % m
|
||||||
|
|
||||||
|
|
||||||
|
def isqrt(n):
|
||||||
|
return math.isqrt(n)
|
||||||
|
|
||||||
|
|
||||||
|
def factor_trivial(n):
|
||||||
|
"""Tiny factor finder for small/weak moduli."""
|
||||||
|
for p in range(2, 1 << 20):
|
||||||
|
if n % p == 0:
|
||||||
|
return p, n // p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---- RSA recovery recipes (from p4 writeups) ----
|
||||||
|
|
||||||
|
def recover_n_from_keys(e, d, ipmq, iqmp):
|
||||||
|
"""
|
||||||
|
From p4 'lost_modulus': we know e, d, ipmq=modinv(p,q), iqmp=modinv(q,p)
|
||||||
|
but NOT n. Recover n via quadratic equation on phi. Returns (p, q) or None.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import gmpy2
|
||||||
|
except Exception:
|
||||||
|
raise SystemExit("gmpy2 required for recover_n_from_keys")
|
||||||
|
|
||||||
|
def find_phi(e, d):
|
||||||
|
kfi = e * d - 1
|
||||||
|
k = kfi // (int(d) * 3)
|
||||||
|
while True:
|
||||||
|
fi = kfi // k
|
||||||
|
try:
|
||||||
|
d0 = gmpy2.invert(e, fi)
|
||||||
|
if d == d0:
|
||||||
|
yield fi
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
k += 1
|
||||||
|
|
||||||
|
def solve(ipmq, iqmp, possible_phi):
|
||||||
|
a = iqmp - 1
|
||||||
|
b = ipmq + iqmp - 2 - possible_phi
|
||||||
|
c = ipmq * possible_phi - possible_phi
|
||||||
|
delta = b * b - 4 * a * c
|
||||||
|
if delta > 0:
|
||||||
|
r, correct = gmpy2.iroot(delta, 2)
|
||||||
|
if correct:
|
||||||
|
for x in [(-b - r) // (2 * a), (-b + r) // (2 * a)]:
|
||||||
|
if gmpy2.is_prime(x + 1):
|
||||||
|
q = x + 1
|
||||||
|
p = possible_phi // x + 1
|
||||||
|
return int(p), int(q)
|
||||||
|
return None
|
||||||
|
|
||||||
|
for phi in find_phi(e, d):
|
||||||
|
res = solve(ipmq, iqmp, phi)
|
||||||
|
if res:
|
||||||
|
return res
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def common_modulus_attack(c1, c2, e1, e2, n):
|
||||||
|
"""Same message encrypted with same n, coprime exponents."""
|
||||||
|
g, a, b = egcd(e1, e2)
|
||||||
|
if g != 1:
|
||||||
|
raise ValueError("e1,e2 not coprime")
|
||||||
|
if a < 0:
|
||||||
|
c1, a = modinv(c1, n), -a
|
||||||
|
if b < 0:
|
||||||
|
c2, b = modinv(c2, n), -b
|
||||||
|
m = (pow(c1, a, n) * pow(c2, b, n)) % n
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def hastad_broadcast(cts, es, n, mlen=1):
|
||||||
|
"""CRT-combine same small message raised to small exponents e across moduli.
|
||||||
|
cts[k] = m^es[k] mod n[k]. Returns m if m^max(e) < n_prod."""
|
||||||
|
from functools import reduce
|
||||||
|
N = reduce(lambda a, b: a * b, n)
|
||||||
|
result = 0
|
||||||
|
for c, ni in zip(cts, n):
|
||||||
|
Ni = N // ni
|
||||||
|
result = (result + c * Ni * modinv(Ni, ni)) % N
|
||||||
|
k = max(es)
|
||||||
|
return int(round(result ** (1.0 / k)))
|
||||||
|
|
||||||
|
|
||||||
|
def wiener(e, n):
|
||||||
|
"""Wiener's attack: small d. Returns d or None."""
|
||||||
|
def cf(a, b):
|
||||||
|
while b:
|
||||||
|
yield a // b
|
||||||
|
a, b = b, a % b
|
||||||
|
def convergents(cf_gen):
|
||||||
|
h0, h1 = 0, 1
|
||||||
|
k0, k1 = 1, 0
|
||||||
|
for q in cf_gen:
|
||||||
|
h0, h1 = h1, q * h1 + h0
|
||||||
|
k0, k1 = k1, q * k1 + k0
|
||||||
|
yield h1, k1
|
||||||
|
for k, d in convergents(cf(e, n)):
|
||||||
|
if k == 0:
|
||||||
|
continue
|
||||||
|
if (e * d - 1) % k == 0:
|
||||||
|
phi = (e * d - 1) // k
|
||||||
|
s = n - phi + 1
|
||||||
|
disc = s * s - 4 * n
|
||||||
|
if disc >= 0:
|
||||||
|
r = isqrt(disc)
|
||||||
|
if r * r == disc and (s + r) % 2 == 0:
|
||||||
|
return d
|
||||||
|
return None
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
lib/net.py — Connection helpers (p4-team style: nc / receive_until_match / send).
|
||||||
|
|
||||||
|
Works with pwntools if installed; otherwise falls back to a raw-socket
|
||||||
|
implementation so your solvers run even on a bare Python.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pwn import remote, context, p64, u64, ELF, ROP # noqa
|
||||||
|
HAVE_PWN = True
|
||||||
|
except Exception:
|
||||||
|
HAVE_PWN = False
|
||||||
|
|
||||||
|
|
||||||
|
class Conn:
|
||||||
|
"""Thin wrapper around pwntools.remote, or a raw socket if pwntools is missing."""
|
||||||
|
|
||||||
|
def __init__(self, host, port, timeout=10, use_pwntools=True):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.timeout = timeout
|
||||||
|
self.use_pwntools = use_pwntools and HAVE_PWN
|
||||||
|
if self.use_pwntools:
|
||||||
|
context.log_level = os.environ.get("CTF_LOG", "info")
|
||||||
|
self.s = remote(host, port, timeout=timeout)
|
||||||
|
else:
|
||||||
|
self.s = socket.create_connection((host, port), timeout=timeout)
|
||||||
|
self._buf = b""
|
||||||
|
|
||||||
|
# ---- low level ----
|
||||||
|
def recv_raw(self, n=4096):
|
||||||
|
if self.use_pwntools:
|
||||||
|
return self.s.recv(n)
|
||||||
|
data = b""
|
||||||
|
try:
|
||||||
|
while len(data) < n:
|
||||||
|
chunk = self.s.recv(n - len(data))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
data += chunk
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
return data
|
||||||
|
|
||||||
|
def recv_until(self, marker, timeout=None):
|
||||||
|
"""Receive until `marker` (bytes) appears. Returns everything including marker."""
|
||||||
|
if self.use_pwntools:
|
||||||
|
return self.s.recvuntil(marker)
|
||||||
|
marker = marker.encode() if isinstance(marker, str) else marker
|
||||||
|
end = time.time() + (timeout or self.timeout)
|
||||||
|
buf = self._buf
|
||||||
|
while marker not in buf and time.time() < end:
|
||||||
|
try:
|
||||||
|
self.s.settimeout(max(0.1, end - time.time()))
|
||||||
|
chunk = self.s.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buf += chunk
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
self._buf = b""
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def recv_until_match(self, pat, timeout=None):
|
||||||
|
"""Receive until regex `pat` matches. Returns the matched prefix+match."""
|
||||||
|
if self.use_pwntools:
|
||||||
|
return self.s.recvline_regex(pat) if hasattr(self.s, "recvline_regex") else self.s.recvuntil(pat.encode())
|
||||||
|
rx = re.compile(pat.encode() if isinstance(pat, str) else pat)
|
||||||
|
end = time.time() + (timeout or self.timeout)
|
||||||
|
buf = self._buf
|
||||||
|
while time.time() < end:
|
||||||
|
m = rx.search(buf)
|
||||||
|
if m:
|
||||||
|
self._buf = buf[m.end():]
|
||||||
|
return buf[:m.end()]
|
||||||
|
try:
|
||||||
|
self.s.settimeout(max(0.1, end - time.time()))
|
||||||
|
chunk = self.s.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buf += chunk
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
data = data.encode() if isinstance(data, str) else data
|
||||||
|
if self.use_pwntools:
|
||||||
|
self.s.sendline(data) if data.endswith(b"\n") else self.s.send(data)
|
||||||
|
else:
|
||||||
|
self.s.sendall(data)
|
||||||
|
|
||||||
|
def sendline(self, data):
|
||||||
|
data = data.encode() if isinstance(data, str) else data
|
||||||
|
if self.use_pwntools:
|
||||||
|
self.s.sendline(data)
|
||||||
|
else:
|
||||||
|
self.s.sendall(data + b"\n")
|
||||||
|
|
||||||
|
def interactive(self):
|
||||||
|
if self.use_pwntools:
|
||||||
|
self.s.interactive()
|
||||||
|
else:
|
||||||
|
print("[!] interactive needs pwntools; dropping to manual mode")
|
||||||
|
import select
|
||||||
|
while True:
|
||||||
|
r, _, _ = select.select([self.s, sys.stdin], [], [])
|
||||||
|
if self.s in r:
|
||||||
|
print(self.recv_raw(4096).decode(errors="replace"), end="")
|
||||||
|
if sys.stdin in r:
|
||||||
|
self.s.sendall(sys.stdin.readline().encode())
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try:
|
||||||
|
self.s.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def nc(host, port, timeout=10):
|
||||||
|
"""Drop-in for the p4 pattern `nc(host, port)`."""
|
||||||
|
return Conn(host, port, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
# convenience re-exports for templates
|
||||||
|
def receive_until(s, marker, timeout=None):
|
||||||
|
return s.recv_until(marker, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def receive_until_match(s, pat, timeout=None):
|
||||||
|
return s.recv_until_match(pat, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def send(s, data):
|
||||||
|
s.send(data)
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
scaffold.py — generate a new CTF / task skeleton (p4-team style).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scaffold.py new hitcon "HITCON CTF 2024" 5 700
|
||||||
|
python3 scaffold.py add hitcon "Safe Bank" pwn
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
|
||||||
|
TEMPLATES = {
|
||||||
|
"pwn": "pwn_skeleton.py",
|
||||||
|
"crypto": "crypto_skeleton.py",
|
||||||
|
"web": "web_skeleton.py",
|
||||||
|
"re": "re_skeleton.py",
|
||||||
|
"reverse": "re_skeleton.py",
|
||||||
|
"forensics": "forensics_skeleton.py",
|
||||||
|
"misc": "misc_skeleton.py",
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def new_ctf(args):
|
||||||
|
stamp = datetime.now().isoformat()[:10]
|
||||||
|
dirname = f"{stamp}-{args.slug}"
|
||||||
|
newdir = ROOT / dirname
|
||||||
|
newdir.mkdir(exist_ok=True)
|
||||||
|
(newdir / "README.md").write_text(f"# {args.name}\n\n### Table of contents\n")
|
||||||
|
print(f"[+] created {dirname}/")
|
||||||
|
|
||||||
|
|
||||||
|
def add_task(args):
|
||||||
|
matches = sorted(ROOT.glob(f"*-{args.slug}"))
|
||||||
|
if not matches:
|
||||||
|
print("no such CTF slug"); return
|
||||||
|
d = matches[-1]
|
||||||
|
slug = re.sub(r"[^a-z0-9]+", "-", args.task.lower())
|
||||||
|
taskdir = d / slug
|
||||||
|
taskdir.mkdir(exist_ok=True)
|
||||||
|
tmpl = TEMPLATES.get(args.category, "pwn_skeleton.py")
|
||||||
|
src = (ROOT / "templates" / tmpl).read_text()
|
||||||
|
(taskdir / "solve.py").write_text(src)
|
||||||
|
rd = d / "README.md"
|
||||||
|
rd.write_text(rd.read_text() + f"* [{args.task} ({args.category})]({slug})\n")
|
||||||
|
print(f"[+] added {taskdir}/solve.py")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
n = sub.add_parser("new"); n.add_argument("slug"); n.add_argument("name"); n.add_argument("place"); n.add_argument("teams"); n.set_defaults(func=new_ctf)
|
||||||
|
a = sub.add_parser("add"); a.add_argument("slug"); a.add_argument("task"); a.add_argument("category"); a.set_defaults(func=add_task)
|
||||||
|
args = p.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
CRYPTO skeleton — copy & fill. (p4-team style: read source, do the math)
|
||||||
|
|
||||||
|
For RSA challenges, start by importing lib.crypto_utils and try the recipes:
|
||||||
|
recover_n_from_keys, common_modulus_attack, wiener, hastad_broadcast
|
||||||
|
"""
|
||||||
|
import lib.crypto_utils as cu
|
||||||
|
from Crypto.Util.number import long_to_bytes, bytes_to_long, getPrime, inverse
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1) Read the challenge output / source. Example: RSA with weird params.
|
||||||
|
e = 0x10001
|
||||||
|
# ... load n, c, etc from the provided data ...
|
||||||
|
|
||||||
|
# 2) Try the right recipe. Example (from 'lost_modulus'):
|
||||||
|
# p, q = cu.recover_n_from_keys(e, d, ipmq, iqmp)
|
||||||
|
# n = p * q
|
||||||
|
# m = pow(c, d, n)
|
||||||
|
# print(long_to_bytes(m))
|
||||||
|
|
||||||
|
raise NotImplementedError("fill in the crypto math")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
FORENSICS / MISC skeleton — copy & fill. (p4-team style: oracle / byte-by-byte,
|
||||||
|
pcap parse, stego.)
|
||||||
|
|
||||||
|
Example (from 'heXdump'): the service uses `xxd -r -ps` which does NOT truncate,
|
||||||
|
so you overwrite 1 byte at a time and brute the flag char-by-char against a known
|
||||||
|
oracle output.
|
||||||
|
"""
|
||||||
|
from lib.net import nc, receive_until, receive_until_match, send, sendline # noqa
|
||||||
|
import string
|
||||||
|
|
||||||
|
CHARSET = string.ascii_letters + string.digits + "{}_-!@#$%^&*()+=/."
|
||||||
|
|
||||||
|
|
||||||
|
def byte_by_byte_oracle(base_conn_setup, oracle_fn, known_prefix="flag{"):
|
||||||
|
"""Generic oracle recover: find next char where oracle output == baseline."""
|
||||||
|
known = known_prefix
|
||||||
|
while "}" not in known:
|
||||||
|
baseline = oracle_fn(known) # output for current known prefix
|
||||||
|
for c in CHARSET:
|
||||||
|
test = oracle_fn(known + c)
|
||||||
|
if test == baseline:
|
||||||
|
known += c
|
||||||
|
print(known)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
known += "?"
|
||||||
|
print("stuck at", known)
|
||||||
|
break
|
||||||
|
return known
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Wire oracle_fn to your specific protocol; see heXdump writeup.
|
||||||
|
pass
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
MISC skeleton — copy & fill. (p4-team style: oracle, encoding, stego, brute)
|
||||||
|
|
||||||
|
p4 'misc' covers a lot: byte-by-byte oracle (heXdump), encoding tricks,
|
||||||
|
PRNG reversing, image stego, constraint solving. See forensics_skeleton.py
|
||||||
|
for the generic oracle helper.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
|
# ---- encoding chain helper (common in misc) ----
|
||||||
|
def try_decodings(blob):
|
||||||
|
"""Brute a chain of common decodings to spot a flag."""
|
||||||
|
results = []
|
||||||
|
data = blob
|
||||||
|
for _ in range(3):
|
||||||
|
for name, fn in [
|
||||||
|
("b64", lambda d: base64.b64decode(d)),
|
||||||
|
("b32", lambda d: base64.b32decode(d)),
|
||||||
|
("b16", lambda d: base64.b16decode(d)),
|
||||||
|
("hex", lambda d: bytes.fromhex(d.decode())),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
out = fn(data)
|
||||||
|
if b"flag" in out.lower() or b"CTF" in out:
|
||||||
|
results.append((name, out))
|
||||||
|
data = out
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ---- generic byte-by-byte oracle ----
|
||||||
|
CHARSET = string.ascii_letters + string.digits + "{}_-!@#$%^&*()+=/."
|
||||||
|
|
||||||
|
|
||||||
|
def recover_oracle(known_start, oracle_fn, stop="}"):
|
||||||
|
"""oracle_fn(prefix) returns a stable baseline string for a given known prefix."""
|
||||||
|
known = known_start
|
||||||
|
while stop not in known:
|
||||||
|
baseline = oracle_fn(known)
|
||||||
|
found = None
|
||||||
|
for c in CHARSET:
|
||||||
|
if oracle_fn(known + c) == baseline:
|
||||||
|
found = c
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
known += "?"
|
||||||
|
break
|
||||||
|
known += found
|
||||||
|
print(known)
|
||||||
|
return known
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise NotImplementedError("pick a misc technique and fill it in")
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
PWN skeleton — copy & fill. (p4-team style)
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Leak (format string / GOT / libc) -> step 1
|
||||||
|
2. Build ROP / overwrite -> step 2
|
||||||
|
3. Get shell / read flag -> step 3
|
||||||
|
"""
|
||||||
|
from lib.net import nc, receive_until, receive_until_match, send, sendline # noqa
|
||||||
|
from pwn import * # noqa (ELF, ROP, p64, u64, context)
|
||||||
|
|
||||||
|
context.log_level = 'info'
|
||||||
|
context.arch = 'amd64' # or 'i386'
|
||||||
|
|
||||||
|
HOST, PORT = "challenge.host", 1337
|
||||||
|
# binary = ELF('./challenge')
|
||||||
|
# libc = ELF('./libc.so.6')
|
||||||
|
|
||||||
|
|
||||||
|
def step1_leak(s):
|
||||||
|
"""Leak libc/stack/PIE base. Adapt to the vuln (format string shown here)."""
|
||||||
|
s.recv_until(b"name > ")
|
||||||
|
# classic format-string leak
|
||||||
|
sendline(s, b"%9$p|%11$p")
|
||||||
|
line = s.recv_until(b"\n")
|
||||||
|
leak = int(line.split(b"|")[0], 16)
|
||||||
|
log.info("leak = %#x", leak)
|
||||||
|
return leak
|
||||||
|
|
||||||
|
|
||||||
|
def step2_exploit(s, leak):
|
||||||
|
"""Construct payload. Fill with your gadgets/ROP."""
|
||||||
|
payload = b"A" * 40 # padding to saved RIP
|
||||||
|
payload += p64(leak) # example: overwrite with leaked addr
|
||||||
|
# payload += rop.chain(...)
|
||||||
|
s.recv_until(b"message > ")
|
||||||
|
sendline(s, payload)
|
||||||
|
|
||||||
|
|
||||||
|
def step3(s):
|
||||||
|
s.recv_until(b"$ ") # shell prompt, or just:
|
||||||
|
sendline(s, b"cat flag*; cat /flag*")
|
||||||
|
print(s.recvall(timeout=3).decode(errors='replace'))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = nc(HOST, PORT)
|
||||||
|
leak = step1_leak(s)
|
||||||
|
step2_exploit(s, leak)
|
||||||
|
step3(s)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
RE (reverse engineering) skeleton — copy & fill. (p4-team style)
|
||||||
|
|
||||||
|
Common p4 patterns:
|
||||||
|
* Extract bytes from a .rodata / data dump (IDA "db 30h" lines) -> often just
|
||||||
|
RSA key material. See 'reversing_is_amazing': parse the bytes, RSA.importKey,
|
||||||
|
then decrypt the given ciphertext.
|
||||||
|
* Symbolic execution / path constraint solving with angr.
|
||||||
|
* Binary parsing / patching with lief; emulation with unicorn.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
# ---- pattern A: RSA key from an IDA/Ghidra byte dump ----
|
||||||
|
def bytes_from_rodata(dump_text):
|
||||||
|
"""Parse lines like: .rodata:0000 db 30h, 82h, 2, 5Ch ; comment"""
|
||||||
|
out = []
|
||||||
|
for line in dump_text.splitlines():
|
||||||
|
m = re.search(r"\bdb\b\s+(.*)", line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
body = m.group(1).split(";")[0]
|
||||||
|
for tok in re.findall(r"([0-9a-fA-F]+)h?|(\d+)", body):
|
||||||
|
val = tok[0] or tok[1]
|
||||||
|
try:
|
||||||
|
out.append(int(val, 16) if (tok[0] and val.endswith("h")) or
|
||||||
|
(tok[0] and not val.isdigit()) else int(val))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def solve_rsa_from_dump(dump_text, ciphertext_int):
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Util.number import long_to_bytes
|
||||||
|
data = bytes_from_rodata(dump_text)
|
||||||
|
key = RSA.importKey(bytearray(data))
|
||||||
|
return long_to_bytes(pow(ciphertext_int, key.e, key.n))
|
||||||
|
|
||||||
|
|
||||||
|
# ---- pattern B: angr symbolic execution (uncomment & adapt) ----
|
||||||
|
"""
|
||||||
|
import angr, claripy
|
||||||
|
def solve_with_angr(binary, find_addr, avoid_addr, input_len=20):
|
||||||
|
proj = angr.Project(binary, auto_load_libs=False)
|
||||||
|
sim = proj.factory.entry_state()
|
||||||
|
flag = sim.solver.BVS('flag', input_len*8)
|
||||||
|
for i in range(input_len):
|
||||||
|
sim.memory.store(sim.regs.rsp + i, flag.get_byte(i))
|
||||||
|
sim = proj.factory.simgr(sim)
|
||||||
|
sim.explore(find=find_addr, avoid=avoid_addr)
|
||||||
|
if sim.found:
|
||||||
|
return sim.found[0].solver.eval(flag, cast_to=bytes)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ---- pattern C: lief parse / patch ----
|
||||||
|
"""
|
||||||
|
import lief
|
||||||
|
def parse(binary):
|
||||||
|
f = lief.parse(binary)
|
||||||
|
for sym in f.symbols: print(sym.name, hex(sym.value))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise NotImplementedError("pick a pattern above and fill it in")
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""
|
||||||
|
WEB skeleton — copy & fill. (p4-team style: read the source, find the
|
||||||
|
sanitization-order bug, then script the request.)
|
||||||
|
|
||||||
|
Key lesson from 'piapiapia': filter() ran AFTER serialize() -> length
|
||||||
|
manipulation / object injection. Always diff the order of sanitize vs use.
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE = "http://challenge.host:port"
|
||||||
|
S = requests.Session()
|
||||||
|
|
||||||
|
|
||||||
|
def get_token():
|
||||||
|
r = S.get(BASE + "/login")
|
||||||
|
# parse CSRF / cookies as needed
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def exploit():
|
||||||
|
# 1) Find the input that bypasses validation (array, encoding, filter order).
|
||||||
|
# 2) Craft payload.
|
||||||
|
# 3) Send & parse response for flag.
|
||||||
|
r = S.post(BASE + "/endpoint", data={"nickname[]": "PAYLOAD"})
|
||||||
|
print(r.text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
exploit()
|
||||||
Reference in New Issue
Block a user