crackmes Easy

RecordPlayer

Overview

Field Value
Category Easy
Author Fatmike
Binary PE32+ x64 Windows GUI (C/C++)
Flag CMO{y0u_g0t_r1ckr0ll3d}

Recon

The binary is a custom-skinned Windows audio player. Running it shows a record player UI with a PLAY button and two switches. The README explains:

  • One switch controls the direction the record spins
  • The other keeps the pitch steady
  • Both switches are broken

Key imports:

  • WINMM: waveOutOpen, waveOutWrite, waveOutPrepareHeader - audio playback
  • KERNEL32: FindResourceW, LoadResource, LockResource - embedded WAV resource
  • USER32: DialogBoxParamW, PostMessageW - custom dialog UI

The binary embeds a WAV resource (type SOUND, ID 141): 8-bit mono PCM, 44100 Hz, 720,623 bytes.

Program Flow

MainDialog Constructor (0x1400032B0)

Creates an audio player object (0x1B8 bytes) via sub_1400016F0, sets up a lambda callback, and creates the UI buttons.

Play Button Handler (0x140003860, case 1001)

  1. Loads WAV resource via sub_140001F50
  2. Sets direction: sub_140003A00(player, 1) -> direction = 2 * (1 ^ 1) - 1 = -1 (backward)
  3. Sets pitch toggle: sub_140003A20(player, 1) -> pitch_toggle = 1 (wobble enabled)
  4. Starts playback: sub_1400020F0(player)

Both switches are “broken”: the record plays backward with a wobbling pitch.

The Fix

To “fix” the switches:

  • Pass 0 instead of 1 to sub_140003A00 -> direction = 2 * (0 ^ 1) - 1 = 1 (forward)
  • Pass 0 instead of 1 to sub_140003A20 -> pitch_toggle = 0 (steady pitch)

This gives normal forward playback without pitch wobble.

Start Playback (0x1400020F0)

  • If direction == -1: start position = pcm_size - 1 (end of audio)
  • If direction == 1: start position = 0 (beginning)
  • Allocates two 0x2000-byte waveOut buffers (double-buffering)
  • Fills them via sub_140002310 and starts a playback thread

Buffer Fill / Verification (0x140002310)

For each output sample:

  1. Generate sample: If pitch_toggle == 0, reads pcm[position] and advances by direction. If pitch_toggle == 1, calls the pitch wobble function sub_140001E20.
  2. Ring buffer: Stores the sample in a block-based ring buffer (16 bytes per block).
  3. Sliding window: Maintains a counter that saturates at key_length (23). Once full, a 23-byte sliding window advances by 1 each sample.
  4. XOR + hash check: XORs the 23-byte window with a hardcoded key, computes FNV-1a hash, and compares against a target.
  5. Callback: On match, invokes the lambda which stores the XOR result string and posts WM_USER+1 to display it in a popup dialog.

Pitch Wobble (0x140001E20)

When enabled, applies linear interpolation between adjacent samples with a wobbling playback rate:

  • pitch_rate starts at 1.0 and oscillates between 0.3 and 1.2
  • pitch_delta starts at -1.0 and flips sign at the boundaries
  • Rate of change: pitch_delta * 0.00005 per sample

This creates a warped, unsteady playback.

Key Constants

XOR Key (23 bytes, from disassembly at 0x1400017B6)

30 2B 3D FC F6 B6 06 3B 0E B1 ED C0 E1 48 07 0C 0B BB F4 F9 48 01 19

Constructed from DWORDs on the stack:

Src[0] = 0xFC3D2B30  ->  30 2B 3D FC
Src[1] = 0x3B06B6F6  ->  F6 B6 06 3B
Src[2] = 0xC0EDB10E  ->  0E B1 ED C0
Src[3] = 0x0C0748E1  ->  E1 48 07 0C
Src[4] = 0xF9F4BB0B  ->  0B BB F4 F9
   + WORD 0x0148     ->  48 01
   + BYTE 0x19       ->  19

Important: IDA’s decompiler showed incorrect values for these constants (e.g., 0xFC3DB4A0 instead of 0xFC3D2B30). The disassembly had to be checked directly.

Target FNV-1a Hash

0x18940A3D  (at offset 0x150 in player object)

Also misrepresented by the decompiler as 0x18946A3D.

FNV-1a (0x140001030)

Standard FNV-1a with seed 0x811C9DC5 and prime 0x01000193:

hash = 0x811C9DC5
for each byte b:
    hash = (hash ^ b) * 0x01000193

Solve

The flag is hidden at PCM offset 132,300 in the embedded WAV. With forward playback and no pitch wobble (the “fixed” state), the 23-byte window at that offset XORs with the key to produce the flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const fs = require('fs');
const exe = fs.readFileSync('RecordPlayer.exe');
const pcm = exe.slice(0x78756, 0x78756 + 720623);

const key = Buffer.from([
    0x30, 0x2B, 0x3D, 0xFC, 0xF6, 0xB6, 0x06, 0x3B,
    0x0E, 0xB1, 0xED, 0xC0, 0xE1, 0x48, 0x07, 0x0C,
    0x0B, 0xBB, 0xF4, 0xF9, 0x48, 0x01, 0x19
]);

// XOR 23-byte window at offset 132300
const flag = Buffer.alloc(23);
for (let i = 0; i < 23; i++)
    flag[i] = pcm[132300 + i] ^ key[i];

console.log(flag.toString()); // CMO{y0u_g0t_r1ckr0ll3d}

Lessons Learned

IDA’s Hex-Rays decompiler can produce incorrect constant values, particularly for stack-initialized arrays. Always verify critical constants against the disassembly when results don’t match expectations.

Flag

CMO{y0u_g0t_r1ckr0ll3d}