← Back to Writeups
CUSTOMMediumReversing

Check Summer

XESXOR9/10/20263 min read
#reversing#custom#medium#branch_pattern_inversion#crc_state_reconstruction#dynamic_trace_analysis

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
crc ^= 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 0x00424527 taken -> low bit was 0
  • fallthrough to XOR EDI,EDB88320 -> low bit was 1

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:

  • SPbCTF
  • spbctf{REDACTED}

The recovered flag validates cleanly: crc32(flag) = 0xaf44cf40.

Solution

  1. Reverse the function at 0x004244E1 and identify it as a standard bitwise CRC32 routine.
  2. Locate the branch in the trace corresponding to the crc & 1 test.
  3. Convert every JE / fallthrough decision into one bit of leakage.
  4. Group the decisions into 8-bit chunks, one chunk per processed input byte.
  5. For each chunk, test all 256 possible byte values against the current CRC state and keep the one whose simulated branch pattern matches.
  6. Continue until both CRC32 runs are fully reconstructed. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍