Blkhurst

March 01 2025

Retro

Stage 2 Area 1

We are given an audio file 2_1.wav, along with the hint "This ones a bit retro".

For several months, I could not identify this sound. The audio seemed like random noise, but given the hint, I assumed it must be a form of old communication. I listened to recordings of dial-up modems, fax machine transmissions, SSTV signals, yet nothing sounded similar. With no clear direction, I had to put the challenge on hold.

Months later, I discovered that 8-bit computer programs could be stored on cassette tapes. The moment I listened to these tapes, I knew it was the same type of signal! This confirmed that the sound was not just noise but a form of data storage, giving me a clear direction for further research.

Demodulating & Decoding

Researching common 8-bit systems like ZX Spectrum, Commodore 64, and MSX, I learned that cassette-based programs often used FSK (Frequency Shift Keying) for encoding data. To manually demodulate and decode the signal, we need to determine the sample rate and bit depth of the audio file.

blkhurst@ubuntu:~$ exiftool 2_1.wav
File Name                       : 2_1.wav
File Size                       : 2.4 MB
File Type                       : WAV
Num Channels                    : 1
Sample Rate                     : 44100
Bits Per Sample                 : 16
Duration                        : 27.56 s
# Load using Signed 16-bit Samples
inspectrum -f s16 2_1.wav

Using Inspectrum, I confirmed the signal was FSK encoded with a baud rate of 1.8kBd, and demodulated the signal to binary data.

It was clear that the binary data was Manchester encoded as there were never three consecutive identical bits. However, Manchester encoding can follow either the IEEE 802.3 or G.E. Thomas standard. I decided to decode both signals using both methods, generating four possible outputs.

I was confident that I had extracted the binary data correctly, yet all four decoded files failed to match any known format. After retrying every emulator in case it was a raw ROM dump, I found myself at another dead end. Just as I was about to call it a day, I decided to experiment reversing the bit order of each byte (unlike endianness, which deals with byte order). I wasn't expecting much, but to my complete shock, the output contained a message: "hello"!

# Decoded Message - Signal 1
hello
 
Welcome! You are reading the first of two files. There is a short gap in the
audio before the next file. Decode the second in the same way.
 
Well done for getting this far. In this challenge you must prove that you can
master an old, probably unfamiliar computer system.
 
The second part of the audio encodes is a .PRG file for the Commodore 64. This
means you can load the decoded file into an emulator if you want to. An open
source one such as VICE might be good.
 
The first two bytes contain the load address for the rest of the file, and
the emulator expects this as it's part of the PRG file format.
 
** SO YOU NEED TO STRIP THESE TWO BYTES OFF ** if you load the file into any
other tool, as they are not part of the program and other tools probably won't
recognise the PRG file format.
 
You can run the game on a Commodore 64 by loading it in and then typing SYS 12800
Once finished, you can run it again in the same way.
 
Now, here's the challenge: I am going to run this game on my Commodore 64.
Once it's running, I need you to tell me the shortest sequence of keys to press so
that I win on the first move, every time.
 
There are some restrictions:
 
1. No modifications to the program, and no instructions for how I should run
it. I'll get it running and then will press the keys you tell me.
2. You can only tell me to press a sequence of digit keys (0 to 9), followed by a
single press of the RETURN key at the end.
3. My favourite number is 13110, so I want these digits in this order to the
first ones to press
 
The answer code to that you use to prove you've won will be made by appending
the following decimal numbers:
 
[The total number of digits I need to press][The last 6 digits before the RETURN key]
 
For example, if I needed to type 13110678901234 + RETURN to win, then the answer
code is 14901234
 
Remember, I need the *shortest* sequence of digits, followed by RETURN.
 
No brute forcing it-- I won't accept huge number of guesses.
I'll be impressed if you get this.
 

The Commodore 64 PRG file would have never been recognised as it does not contain a file signature.
Instead, the first 2 bytes indicate the load address (Little-Endian), followed by binary content.

VICE Emulator

Now that we have extracted the Commodore 64 PRG file, we can install a C64 emulator to run it. The first two bytes of the file (00 32) specify the load address in little-endian format, meaning the program loads at 0x3200 (decimal 12800).

Installing & Running VICE

sudo apt install vice
 
# VICE does not distribute the ROMs. To install them,
# Download source from http://vice-emu.sourceforge.net/index.html#download
tar -xvf vice-*.tar.gz
cp -r vice/data/* ~/.local/share/vice/
# Run Emulator
x64sc decoded_program.prg
# Launch program at its load address 12800 (0x3200).
SYS 12800

Understanding the Game

We are met with a guessing game, where we have only five attempts to guess the correct number. Our goal is not only to win the game, but to craft an input that ensures victory on the first try, every time.

Before analysing the machine code, I explored the input behaviour. By observing how different inputs affect the game, we can identify potential buffer overflows or memory corruption.

Input LengthEffect
> 10 charactersOverwrites the "You have X guesses left" message
19 charactersunlimited guesses (e.g., 123456789abcdef12345)
20 charactersThe final digit overwrites the counter - additional guesses.
> 37 charactersImmediate failure upon pressing ENTER

Analysing Assembly & Memory

Since this program was designed for the Commodore 64, it runs on the 6502 architecture, which is very different from the x86-64 architecture I had been using for previous reverse engineering challenges. Luckily, I'd recently been researching building an 8-bit computer - ALU, register file, program counter, and EEPROM as a control-unit lookup table to implement the instruction set - which made the transition much easier.

Unlike x86-64, the 6502 uses a simpler instruction set. It has just 3 registers - A (Accumulator), X (Index), and Y (Index) - uses branching instead of jumps, and loading instead of moving. Other architectures, such as ARM, Z80, and MIPS, have their own variations as well.

To begin reversing the program, I used radare2, specifying the 6502 architecture and mapping the file to the correct memory address:

# -a Specify 6502 architecture
# -m Map file to address 0x31fe (12800-2)
r2 -A -a 6502 -b 8 -m 0x31fe decoded_program.prg
 
# Analyse & Disassemble 643 bytes
pD 643 > decoded_program.asm

Next, I examined the raw hex dump to get an overview of the program's structure:

# Hex dump
[0x000031fe]> s 0x3200
[0x00003200]> px 641
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00003200  a900 8d20 d08d 21d0 2003 33a9 c285 32a9  ... ..!. .3...2.
....
0x000033d0  0d57 454c 434f 4d45 2054 4f20 5448 4520  .WELCOME TO THE
0x000033e0  4153 5445 524f 4944 0d20 382d 4249 5420  ASTEROID. 8-BIT
0x000033f0  4341 5349 4e4f 2e0d 0d1e 4920 4841 5645  CASINO....I HAVE
....

When analysing the assembly, I noticed that the instructions towards the end of the program didn't make sense. This is because it wasn't machine code, it was embedded data. Programs often store constants such as strings towards the end of a file, as seen in the partial hex dump above. The actual program logic appears to end around 0x00003360.

I began annotating the assembly, noting key memory addresses being accessed. I repeatedly saw reads and writes around 0x4000, suggesting this region was allocated for storing program variables. To confirm its purpose, I used VICE's activity monitor to inspect the memory at runtime.

This confirms our previous findings about input behaviour. The main vulnerability is an unbounded input buffer at 0x4000, allowing the user input to overflow and overwrite critical values in memory. The table below highlights key memory addresses.

AddressDescription
40004000-400AUser input buffer (stored as ASCII)
400B400B-4021Guesses Left String
$4014Guesses Remaining (stored as ASCII)
40294029-402ASecret number (two bytes, little-endian)
402B402B-402CUser's guess (two bytes, little-endian)

Crafting the Winning Input

Now that we have identified the key memory addresses, we have a few straightforward ways to win:

  1. Unlimited guesses - Overwriting the "Guesses Remaining" value in memory. (E.g. > 4014 39 for 9 attempts.)
  2. Direct win - Extracting the secret number from memory, converting it from hex to decimal, and entering the correct guess.

However, the challenge requires a crafted input that guarantees a win on the first try without using external tools.

To achieve this, I attempted to overwrite the stored secret number at $4029-$402A by crafting an input of the correct length. Since the game stores user input as ASCII, I needed to:

At first, this did not work - the game still failed. Upon re-examining the assembly, I noticed that the program checks for specific values (6273) in memory before allowing a win. This appears to be an overflow detection mechanism, preventing unintended overwrites.

By adjusting the input to insert "6273" before overwriting the secret number, I successfully bypassed the overflow detection, and produced a payload that guarantees a win on the first move, every time!

# Desired Number | 32 Padding | Bypass Check 6273 | "6" "3" = 13110
13110aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa627363
# Flag: 43627363
Debugging notes

This challenge combined:

  • Digital Signal Processing - FSK demodulation & Manchester decoding
  • Reverse Engineering - 6502 assembly & memory analysis
  • Binary Exploitation - buffer overflow & memory manipulation

By overwriting memory using an unbounded input buffer, we changed the secret number and forced a win condition on the first move, every time.

Conversion, emulation, demodulation attempts

  • Tried cassette conversions: wav2tzx, tzxtools, castools, bin2tap
  • Tested multiple emulators: Fuse (ZX Spectrum), VICE (C64), OpenMSX (MSX), QEMU, DOSBox
  • Tried minimodem for FSK demodulation with common baud rates and frequencies.
  • Briefly thought Fuse’s “Load Binary Data (65535-byte limit)” related to the "guessing game", re-reading the hint later, it was actually referring to stack corruption.
  • Initially opened the WAV in Inspectrum using the wrong format (-f cs16 instead of -f s16), which treats the file as complex signed 16-bit data rather than a single real-valued FSK signal. That made it look like two signals and sent me down a rabbit hole before I realised the mistake.

The moment it clicked

With no progress, I was about to call it for the day. I attempted one last idea: reversing the bit order. I hit Enter, and suddenly the output began with "hello".