Hack.lu 2013 CTF Write-Up: Roboparty

Robot LHCH is happy. He made it into the castings for the tenth roman musical. He even is so happy that he went on the Oktoberfest to drink some beer. Unfortunately it seems that he drank too much so now he is throwing up part of his source code. Can you decipher the secret he knows?

Warning: Viewing this page is not recommended for people that suffer from epilepsy. We are dead serious.

And here is your totally eye-friendly challenge: https://ctf.fluxfingers.net/static/downloads/roboparty/index.html

The linked page provided a tripping psychedelic experience to our team. After connecting a laptop to the projector with the page in fullscreen mode, we spent half of the CTF partying hard.

totally_eye_friendly_gif

While dancing, we noticed a commented reference to a MIDI file within the page sources.

<!-- <audio/Phil autoplay loop src="robotparty.mid"> -->

We analyzed the .mid file to find out that:

  • there are no overlapping notes, i.e. only one note is executed at a time
  • all the notes of the song are natural (i.e. A, B, C, D, E, F and G)
  • the range of notes used is very limited, in fact only 12 different notes are used in the entire song (considering the same notes from different octave as distinct ones)

We failed several times trying various steganography approaches. Then, after reading again the challenge description (“[…] throwing up part of his source code”) we started thinking that the song could be a source code written in an esoteric programming language.

To our surprise there are several languages that rely on music notes for writing programs. Possible candidates were Velato, Fugue and Musical-X. Once again, the challenge description helped identifying the right language (“[…] tenth roman musical”). Unfortunately, to the best of our knowledge, there were no public interpreters for Musical-X. Hence we wrote a simple one for this brilliant language using Python.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import sys
import string


NOTES = {'C': 0, 'D': 1, 'E': 2, 'F': 3, 'G': 4, 'A': 5, 'B': 6}


def interval(note1, ott1, note2, ott2):
    val1 = NOTES[note1] + eval(ott1) * 7
    val2 = NOTES[note2] + eval(ott2) * 7

    up = val2 - val1 > 0

    return abs(val2 - val1), up


def main():
    if len(sys.argv) != 2:
        sys.stderr.write("Usage: ./musicalx <source file>\n")
        sys.exit(1)

    commands = open(sys.argv[1]).readlines()
    command_ptr = 0

    tapes = {k: [0] * 100 for k in NOTES.keys()}
    tapes_ptr = {k: 0 for k in NOTES.keys()}
    curr_tape = 'C'

    aux = False

    while command_ptr < len(commands) - 1:
        note1, ott1 = commands[command_ptr][0], commands[command_ptr][1]
        note2, ott2 = commands[command_ptr + 1][0], commands[command_ptr + 1][1]

        diff, up = interval(note1, ott1, note2, ott2)

        # current tape pointer
        i = tapes_ptr[curr_tape]

        if not aux:
            # up cases.
            if up:
                if diff == 1:
                    # 2nd up: increment value at pointer (mod 256)
                    tapes[curr_tape][i] = (tapes[curr_tape][i] + 1) % 256
                elif diff == 2:
                    # 3rd up: move tape pointer one space forward
                    tapes_ptr[curr_tape] += 1
                elif diff == 3:
                    sys.stderr.write('NOT IMPLEMENTED: 4th up')
                elif diff == 4:
                    # 5th up: select tape indicated by second note
                    curr_tape = note2
                elif diff == 5:
                    sys.stderr.write('NOT IMPLEMENTED: 6th up')
                elif diff == 6:
                    # 7th up: interpret next command as in Auxiliary list #1
                    aux = True
            # down cases.
            else:
                if diff == 1:
                    # 2nd down: decrement value at pointer (mod 256)
                    tapes[curr_tape][i] = (tapes[curr_tape][i] - 1) % 256
                elif diff == 2:
                    # 3rd down: move tape pointer one space backward
                    tapes_ptr[curr_tape] -= 1
                elif diff == 3:
                    # 4th down: output value at pointer
                    sys.stdout.write(str(tapes[curr_tape][i]))
                elif diff == 4:
                    # 5th down: move to initial position of current tape
                    tapes_ptr[curr_tape] = 0
                elif diff == 5:
                    sys.stderr.write('NOT IMPLEMENTED: 6th down')
                elif diff == 6:
                    # 7th down: change key to the second note of this command
                    if note2 != 'C':
                        sys.stderr.write('NOT IMPLEMENTED: 7th down(if note != C)')

        else:
            # auxiliary up cases.
            if up:
                if diff == 1:
                    # 2nd up: increase value at pointer by 64 (mod 256)
                    tapes[curr_tape][i] = (tapes[curr_tape][i] + 64) % 256
                elif diff == 2:
                    # 3rd up: move tape pointer 64 spaces forward
                    tapes_ptr[curr_tape] += 64
                elif diff == 3:
                    # 4th up: change notes not of this key to round up instead
                    # of down
                    sys.stderr.write('NOT IMPLEMENTED: aux 4th up')
                elif diff == 4:
                    # 5th up: select tape indicated by first note
                    curr_tape = note1
                elif diff == 5:
                    # 6th up: if value at pointer is even, then search backward
                    # to command with a first note the same note as the second
                    # note of this command
                    sys.stderr.write('NOT IMPLEMENTED: aux 6th up')
                elif diff == 6:
                    sys.stderr.write('aux 7th up   (RESERVED).')
            # auxiliary down cases.
            else:
                if diff == 1:
                    # 2nd down: decrease value at pointer by 64 (mod 256)
                    tapes[curr_tape][i] = (tapes[curr_tape][i] - 64) % 256
                elif diff == 2:
                    # 3rd down: move tape pointer 64 spaces backward
                    tapes_ptr[curr_tape] -= 64
                elif diff == 3:
                    sys.stderr.write('NOT IMPLEMENTED: aux 4th down')
                elif diff == 4:
                    # 5th down: skip next command
                    command_ptr += 1
                elif diff == 5:
                    # 6th down: if value at pointer is odd, then search forward
                    # to command with a first note the same note as the first
                    # note of this command
                    sys.stderr.write('NOT IMPLEMENTED: aux 6th down')
                elif diff == 6:
                    sys.stderr.write('aux 7th down   (RESERVED)')
            aux = False
        
        command_ptr += 1

if __name__ == "__main__":
    main()

By executing the notes through our brand new interpreter, we got the the following list of integers.

0 $ ./musicalx.py keys.txt 
8952894432985197117116491021177632771179049107333333

Grouping together the digits it was possible to obtain a list of decimal representation of ASCII characters:

[89, 52, 89, 44, 32, 98, 51, 97, 117, 116, 49, 102, 117, 76, 32, 77, 117, 90, 49, 107, 33, 33, 33]

Flag: Y4Y, b3aut1fuL MuZ1k!!!

(Writeup by Marco Gasparini & Marco Squarcina)

Author: Marco Squarcina

Computer Science student and an Open Source enthusiast. My main interests are computer security (especially mandatory access control systems), Linux systems administrations and audio applications.