159 lines
4.5 KiB
Python
159 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
|
|
MAGIC_TTAB = 0x42415454 # 'TTAB'
|
|
HDR_FMT = "<6I"
|
|
|
|
|
|
def rol32(x: int, r: int) -> int:
|
|
r &= 31
|
|
return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF
|
|
|
|
|
|
def prng_step(state: int, tweak: int) -> int:
|
|
x = (state ^ tweak) & 0xFFFFFFFF
|
|
x ^= (x << 13) & 0xFFFFFFFF
|
|
x ^= (x >> 17) & 0xFFFFFFFF
|
|
x ^= (x << 5) & 0xFFFFFFFF
|
|
return x & 0xFFFFFFFF
|
|
|
|
|
|
def alu_op_int(a: int, b: int) -> int:
|
|
return (a + b) & 0xFFFFFFFF
|
|
|
|
|
|
def hash_op_int(a: int, b: int) -> int:
|
|
return (a + b + 0x9E3779B9) & 0xFFFFFFFF
|
|
|
|
|
|
def load_firmware(path: Path):
|
|
blob = path.read_bytes()
|
|
if len(blob) < struct.calcsize(HDR_FMT):
|
|
raise ValueError("firmware too small")
|
|
magic, version, code_len, data_len, flag_len, target = struct.unpack_from(HDR_FMT, blob, 0)
|
|
if magic != MAGIC_TTAB or version != 1:
|
|
raise ValueError("bad firmware header")
|
|
|
|
off = struct.calcsize(HDR_FMT)
|
|
code_sz = code_len * 4
|
|
data_sz = data_len * 4
|
|
if len(blob) < off + code_sz + data_sz:
|
|
raise ValueError("truncated firmware")
|
|
|
|
code = list(struct.unpack_from(f"<{code_len}I", blob, off))
|
|
off += code_sz
|
|
data = list(struct.unpack_from(f"<{data_len}I", blob, off))
|
|
return {
|
|
"code": code,
|
|
"data": data,
|
|
"flag_len": flag_len,
|
|
"target": target,
|
|
}
|
|
|
|
|
|
def solve(
|
|
fw,
|
|
prefix: str | None,
|
|
suffix: str | None,
|
|
alphabet: str | None,
|
|
no_format: bool,
|
|
) -> bytes:
|
|
data = fw["data"]
|
|
flag_len = fw["flag_len"]
|
|
target = fw["target"]
|
|
if flag_len < 2 or len(data) < 4:
|
|
raise ValueError("bad firmware constants")
|
|
|
|
seed_r14 = data[0]
|
|
seed_r15 = data[1]
|
|
seed_prng = data[2]
|
|
|
|
output_base = 3 + 3 * flag_len
|
|
if len(data) < output_base + flag_len + 1:
|
|
raise ValueError("bad firmware layout")
|
|
|
|
consts = data[3:output_base]
|
|
c_mul = consts[0::3]
|
|
c_rot = consts[1::3]
|
|
c_tw = consts[2::3]
|
|
|
|
outputs = data[output_base:output_base + flag_len]
|
|
target2 = data[output_base + flag_len]
|
|
|
|
recovered = []
|
|
r15 = seed_r15
|
|
for out in outputs:
|
|
b = (out - r15 - 0x9E3779B9) & 0xFFFFFFFF
|
|
if b > 0xFF:
|
|
raise RuntimeError("invalid output stream: byte out of range")
|
|
recovered.append(b)
|
|
r15 = out
|
|
|
|
flag = bytes(recovered)
|
|
|
|
# format constraints
|
|
if not no_format:
|
|
if prefix is None or suffix is None or alphabet is None:
|
|
raise ValueError("format constraints require prefix/suffix/alphabet")
|
|
if not flag.startswith(prefix.encode("ascii")):
|
|
raise RuntimeError("prefix mismatch")
|
|
if not flag.endswith(suffix.encode("ascii")):
|
|
raise RuntimeError("suffix mismatch")
|
|
mid = flag[len(prefix): len(flag) - len(suffix)]
|
|
alpha = set(alphabet.encode("ascii"))
|
|
if any(ch not in alpha for ch in mid):
|
|
raise RuntimeError("alphabet mismatch")
|
|
|
|
# verify accumulators match firmware targets
|
|
prng = seed_prng
|
|
r14 = seed_r14
|
|
r15 = seed_r15
|
|
mul_out = 0
|
|
for i, ch in enumerate(flag):
|
|
stale_mul = mul_out
|
|
stale_prng = prng
|
|
mul_out = (ch + c_mul[i]) & 0xFFFFFFFF
|
|
rot = rol32(r14, c_rot[i])
|
|
alu = alu_op_int(rot, stale_prng)
|
|
r14 = hash_op_int(stale_mul, alu)
|
|
prng = prng_step(prng, c_tw[i])
|
|
r15 = hash_op_int(r15, ch)
|
|
|
|
if r14 != target or r15 != target2:
|
|
raise RuntimeError("verification failed against firmware targets")
|
|
|
|
return flag
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Recover flag from firmware.bin.")
|
|
ap.add_argument("firmware", nargs="?", default="public/firmware.bin")
|
|
ap.add_argument("--prefix", default="caplag{", help="known flag prefix")
|
|
ap.add_argument("--suffix", default="}", help="known flag suffix (1 char)")
|
|
ap.add_argument("--alphabet", default="0123456789ABCDEF", help="alphabet for middle bytes")
|
|
ap.add_argument("--no-format", action="store_true", help="disable prefix/suffix/alphabet constraints")
|
|
args = ap.parse_args()
|
|
|
|
fw = load_firmware(Path(args.firmware))
|
|
flag = solve(
|
|
fw,
|
|
prefix=None if args.no_format else args.prefix,
|
|
suffix=None if args.no_format else args.suffix,
|
|
alphabet=None if args.no_format else args.alphabet,
|
|
no_format=args.no_format,
|
|
)
|
|
try:
|
|
print(flag.decode("ascii"))
|
|
except UnicodeDecodeError:
|
|
print(flag)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|