Check Summer
Check Summer
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-08 | Status: Solved Techniques: branch_pattern_inversion, crc_state_reconstruction, dynamic_trace_analysis
Summary
Task: a Windows crackme exposes only an instruction trace and a CRC32-based checker, so the challenge is to recover the original checked string rather than invert a final checksum. Solution: parse the per-bit branch decisions inside the bitwise CRC32 loop and reconstruct each input byte from the evolving CRC state until both checked strings are recovered.
Check Summer — spbctf
Description
No organizer description was preserved in the task files.
The task provided check_summer.exe and an instruction trace check_summer_rtrace.txt. The important hint was that the CPU execution had been recorded, which suggests the decisive leak is in control flow rather than in static data.
Analysis
The relevant function starts at 0x004244E1 and is a standard bitwise CRC32 implementation over a Delphi AnsiString.
Its inner loop has the classic shape:
c read-only • copy allowedcrc ^= byte; for (int i = 0; i < 8; i++) { if (crc & 1) crc = (crc >> 1) ^ 0xEDB88320; else crc >>= 1; }
The key observation is that the trace records which side of the branch after AND EDI,1 was taken:
JE 0x00424527taken -> low bit was0- fallthrough to
XOR EDI,EDB88320-> low bit was1
That gives one leaked bit of CRC state per loop iteration. Since there are 8 iterations for every processed byte, each byte yields an 8-bit branch pattern.
At the start of a byte, the previous CRC state is already known. Therefore we can try all 256 candidate byte values, simulate the 8 CRC steps, and keep the unique candidate whose taken/not-taken pattern matches the trace. This is not CRC32 brute force over the full string and it is not static extraction from the PE; the decisive oracle is the per-bit control flow leak in the dynamic trace.
Parsing the trace reveals two separate CRC32 computations. Reconstructing their inputs gives:
SPbCTFspbctf{REDACTED}
The recovered flag validates cleanly: crc32(flag) = 0xaf44cf40.
Solution
- Reverse the function at
0x004244E1and identify it as a standard bitwise CRC32 routine. - Locate the branch in the trace corresponding to the
crc & 1test. - Convert every
JE/ fallthrough decision into one bit of leakage. - Group the decisions into 8-bit chunks, one chunk per processed input byte.
- For each chunk, test all 256 possible byte values against the current CRC state and keep the one whose simulated branch pattern matches.
- Continue until both CRC32 runs are fully reconstructed.
python read-only • copy allowed#!/usr/bin/env python3 import zlib POLY = 0xEDB88320 def step_bits(crc, byte): """Return (branch_pattern, next_crc) for one processed byte.""" crc ^= byte bits = [] for _ in range(8): take_je = (crc & 1) == 0 bits.append(take_je) # True => JE taken, False => XOR path if take_je: crc >>= 1 else: crc = (crc >> 1) ^ POLY crc &= 0xffffffff return tuple(bits), crc def recover_bytes(observed_patterns): crc = 0xffffffff out = bytearray() for pat in observed_patterns: for b in range(256): bits, nxt = step_bits(crc, b) if bits == pat: out.append(b) crc = nxt break else: raise ValueError(f"no byte matches pattern {pat}") return bytes(out) # `observed_patterns` is extracted from the trace by reading each # `JE 0x00424527` decision and checking whether the next relevant line is # `MOV EDI,EBX` (JE taken) or `XOR EDI,EDB88320` (fallthrough). flag = b"spbctf{REDACTED}" print(hex(zlib.crc32(flag) & 0xffffffff)) # 0xaf44cf40
Once the branch patterns are decoded from the trace, this script recovers the original bytes deterministically.