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.
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)