commit dd458333228a0f67a011e8423334b78f9ced80ea Author: asepharyana Date: Sun Aug 16 21:55:22 2026 +0700 CTF toolkit: lib (net, crypto_utils), templates per category, scaffold, cheatsheet, ps_and_qs example diff --git a/__pycache__/scaffold.cpython-312.pyc b/__pycache__/scaffold.cpython-312.pyc new file mode 100644 index 0000000..f5e16f2 Binary files /dev/null and b/__pycache__/scaffold.cpython-312.pyc differ diff --git a/cheatsheets/CHEATSHEET.md b/cheatsheets/CHEATSHEET.md new file mode 100644 index 0000000..3b767b3 --- /dev/null +++ b/cheatsheets/CHEATSHEET.md @@ -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) diff --git a/examples/cipher b/examples/cipher new file mode 100644 index 0000000..12fe8c4 Binary files /dev/null and b/examples/cipher differ diff --git a/examples/ps_and_qs.py b/examples/ps_and_qs.py new file mode 100644 index 0000000..996f3e9 --- /dev/null +++ b/examples/ps_and_qs.py @@ -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() diff --git a/examples/pub1.pub b/examples/pub1.pub new file mode 100644 index 0000000..08921c6 --- /dev/null +++ b/examples/pub1.pub @@ -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----- diff --git a/examples/pub2.pub b/examples/pub2.pub new file mode 100644 index 0000000..66f466b --- /dev/null +++ b/examples/pub2.pub @@ -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----- diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/__pycache__/__init__.cpython-312.pyc b/lib/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..2dca0e9 Binary files /dev/null and b/lib/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/__pycache__/crypto_utils.cpython-312.pyc b/lib/__pycache__/crypto_utils.cpython-312.pyc new file mode 100644 index 0000000..2f9e368 Binary files /dev/null and b/lib/__pycache__/crypto_utils.cpython-312.pyc differ diff --git a/lib/__pycache__/net.cpython-312.pyc b/lib/__pycache__/net.cpython-312.pyc new file mode 100644 index 0000000..b840593 Binary files /dev/null and b/lib/__pycache__/net.cpython-312.pyc differ diff --git a/lib/crypto_utils.py b/lib/crypto_utils.py new file mode 100644 index 0000000..b7abe58 --- /dev/null +++ b/lib/crypto_utils.py @@ -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 diff --git a/lib/net.py b/lib/net.py new file mode 100644 index 0000000..2e66e23 --- /dev/null +++ b/lib/net.py @@ -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) diff --git a/scaffold.py b/scaffold.py new file mode 100644 index 0000000..97a853f --- /dev/null +++ b/scaffold.py @@ -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() diff --git a/templates/__pycache__/crypto_skeleton.cpython-312.pyc b/templates/__pycache__/crypto_skeleton.cpython-312.pyc new file mode 100644 index 0000000..d948b76 Binary files /dev/null and b/templates/__pycache__/crypto_skeleton.cpython-312.pyc differ diff --git a/templates/__pycache__/forensics_skeleton.cpython-312.pyc b/templates/__pycache__/forensics_skeleton.cpython-312.pyc new file mode 100644 index 0000000..ff7fb13 Binary files /dev/null and b/templates/__pycache__/forensics_skeleton.cpython-312.pyc differ diff --git a/templates/__pycache__/misc_skeleton.cpython-312.pyc b/templates/__pycache__/misc_skeleton.cpython-312.pyc new file mode 100644 index 0000000..ebf28b8 Binary files /dev/null and b/templates/__pycache__/misc_skeleton.cpython-312.pyc differ diff --git a/templates/__pycache__/pwn_skeleton.cpython-312.pyc b/templates/__pycache__/pwn_skeleton.cpython-312.pyc new file mode 100644 index 0000000..5a7d642 Binary files /dev/null and b/templates/__pycache__/pwn_skeleton.cpython-312.pyc differ diff --git a/templates/__pycache__/re_skeleton.cpython-312.pyc b/templates/__pycache__/re_skeleton.cpython-312.pyc new file mode 100644 index 0000000..9989253 Binary files /dev/null and b/templates/__pycache__/re_skeleton.cpython-312.pyc differ diff --git a/templates/__pycache__/web_skeleton.cpython-312.pyc b/templates/__pycache__/web_skeleton.cpython-312.pyc new file mode 100644 index 0000000..fe66208 Binary files /dev/null and b/templates/__pycache__/web_skeleton.cpython-312.pyc differ diff --git a/templates/crypto_skeleton.py b/templates/crypto_skeleton.py new file mode 100644 index 0000000..57e5bee --- /dev/null +++ b/templates/crypto_skeleton.py @@ -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() diff --git a/templates/forensics_skeleton.py b/templates/forensics_skeleton.py new file mode 100644 index 0000000..7bdd314 --- /dev/null +++ b/templates/forensics_skeleton.py @@ -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 diff --git a/templates/misc_skeleton.py b/templates/misc_skeleton.py new file mode 100644 index 0000000..90e5481 --- /dev/null +++ b/templates/misc_skeleton.py @@ -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") diff --git a/templates/pwn_skeleton.py b/templates/pwn_skeleton.py new file mode 100644 index 0000000..c1c2b21 --- /dev/null +++ b/templates/pwn_skeleton.py @@ -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() diff --git a/templates/re_skeleton.py b/templates/re_skeleton.py new file mode 100644 index 0000000..66c763a --- /dev/null +++ b/templates/re_skeleton.py @@ -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") diff --git a/templates/web_skeleton.py b/templates/web_skeleton.py new file mode 100644 index 0000000..ae4deb6 --- /dev/null +++ b/templates/web_skeleton.py @@ -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()