#!/usr/bin/env python3 """CodeShell.kr — Opcode Picnic (Reversing, 50p) solver. Recovers the accepted 28-byte input from 01_opcode_picnic_JsAiqDC.bin. Method: the checker loops i = 0..27, taking input[27-i] (a reversed index table in .rodata), runs a 4-round mixing loop, and compares the low byte of the accumulator against a 28-byte target table. The mixing loop is fully determined by (i, input_byte), so each byte can be brute-forced independently in 0..255. The register-level model below was validated against a live GDB trace of the real binary (every intermediate value matched for a test input), not inferred from the disassembly alone. Usage: uv run python solvers/opcode_picnic.py \ assets/challenge-files/01_opcode_picnic_JsAiqDC.bin """
import sys
M32 = 0xFFFFFFFF
defrol32(x, c): c &= 31 return ((x << c) | (x >> (32 - c))) & M32 if c else x & M32
defrol8(x, c): c &= 7 return ((x << c) | (x >> (8 - c))) & 255if c else x & 255
defcheck_byte(i, b): """Low byte of the accumulator for outer round i with input byte b.""" r10 = (i * 0x45D9F3B) & M32 r8 = 0 r13 = 0 edi = 0 eax = (r10 ^ 0x9F80C83A) & M32 esi = b & 0xFF whileTrue: edx = eax ecx = (eax >> 7) & M32 edx = (rol32(edx, 13) ^ ecx) & M32 eax = (eax ^ edx) & M32 eax = (eax ^ r8) & M32 r8 = (r8 + 0x7F4A7C15) & M32 edx = (eax >> ((edi * 8) & 31)) & M32 edx = (edx ^ esi) & M32 x = (edi + i) & M32 q = x // 7 rem = x - 7 * q esi = ((eax >> 19) + r13 + rol8(edx & 0xFF, (rem + 1) & 7)) & M32 r13 = (r13 + 0xB) & M32 edi += 1 if edi == 4: break return esi & 0xFF
defsolve(path): data = open(path, "rb").read() key = list(data[0x2020:0x203C]) # index table, 27..0 target = list(data[0x2040:0x205C]) # 28 expected bytes
out = [None] * len(target) for i, idx inenumerate(key): hits = [b for b inrange(256) if check_byte(i, b) == target[i]] iflen(hits) != 1: raise SystemExit(f"round {i}: {len(hits)} candidates {hits}") out[idx] = hits[0] returnbytes(out)
if __name__ == "__main__": p = sys.argv[1] iflen(sys.argv) > 1else \ "assets/challenge-files/01_opcode_picnic_JsAiqDC.bin" flag = solve(p) print("recovered:", flag.decode())