CyberDiscovery's Valhalla

A while ago, I took part in a competition called 'CyberStart', which is a gamified introductory challenge site for young people to spark an interest in a career in computer security.

I was then emailed (on account of my "exceptional performance" ??) with an invite-only challenge which is accessible at the URL valhalla.joincyberdiscovery.com.

So, let's take a crack at it. It might be hard, but hey, it's for kids.

$ curl 'https://valhalla.joincyberdiscovery.com/' -o valhalla
$ head valhalla
ᚿᛔᚵᚬᚱᛀᚤᚡᚠᚰᚠᚠᚠᚠᚠᚠᚠᚠᚠᚠᚠᚠᚬᚠᚠᛐᚠᚡᚠᚠᚠᚠᛀᚡᚤᚠᚠ...
$ curl 'https://valhalla.joincyberdiscovery.com/' -o valhalla
$ head valhalla
ᚿᛔᚵᚬᚱᛀᚤᚡᚠᚰᚠᚠᚠᚠᚠᚠᚠᚠᚠᚠᚠᚠᚬᚠᚠᛐᚠᚡᚠᚠᚠᚠᛀᚡᚤᚠᚠ...

Oh, these are characters from the Runic block. I get why the challenge is called Valhalla now.

Research & Analysis

The Runic Unicode block can contain 96 code points, but only 89 are assigned.1

Let's mess around with the file that we've been given.

>>> # With the downloaded 'valhalla' file in the working directory:
>>> with open("valhalla") as f:
...   valhalla = f.read()
...
>>> runic_block = range(0x16A0, 0x16FF + 1)
>>> valhalla = "".join(c for c in valhalla if ord(c) in runic_block)
>>> min(valhalla)
'\u16a0'
>>> max(valhalla)
'\u16e0'

Hm, it looks like our lowest and highest used codepoints in the Runic block are 0x40 apart. How about base 64?

First, we translate from the Runic characters to Python's standard base64 alphabet:

>>> # In the same context as we defined 'valhalla'
>>> import string
>>>
>>> standard_b64 = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/="
>>> runic_b64 = "".join(map(chr, range(0x16a0, 0x16e0 + 1)))
>>> # Both standard_b64 and runic_b64 are 65 characters long.
>>>
>>> def translate_to_b64(s):
...   translation_table = str.maketrans(runic_b64, standard_b64)
...   return s.translate(translation_table)
...
>>> valhalla_b64 = translate_to_b64(valhalla)
>>> valhalla_b64[:16]
'f0VMRgEBAQAAAAAA'

That's a lot of consecutive As, that'd be a bunch of 0x0 bytes in base64. It looks like we're on the right track.

So, we try decoding valhalla_b64 using Python's built-in base64 library:

>>> # Same context, again.
>>> import base64
>>> valhalla_bytes = base64.standard_b64decode(valhalla_b64)
>>>
>>> # Maybe it's text?
>>> valhalla_bytes.decode("utf-8")
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 24: invalid start byte
>>> # Ah. Let's just write it out then.
>>> with open("valhalla.out", "wb") as f:
...   f.write(valhalla_bytes)
14164
>>> # Neat, that's how many bytes we wrote out.
>>> exit()

Alright, we now have a file, valhalla.out. Let's take a look at it:

$ file ./valhalla.out
valhalla.out: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=1e5f459df47978fe95f7857cad7afeccde0143d6, not stripped

Alright, we have a 32-bit Linux executable. Since I don't have multilib on my Arch install I'm going to start a Docker container to have a look at this.

$ base64 valhalla.out | xclip -selection clipboard
$ docker run -it 32bit/ubuntu:16.04 bash
...
root@some-container:/# cat | base64 -d > valhalla.out
[paste and hit ctrl-d]
# chmod +x valhalla.out
# ./valhalla.out
Are you Elite?
Not yet

Alright, we got it running, but it won't give us any useful information yet. Let's load it up in radare2.

$ apt install radare2
$ r2 valhalla.out
[0x00001180]> aaa

Patching the binary is really easy, sym._start has a jump to sym.exit which is always fulfilled, we reverse the condition from jnz to jz and get our solution:

[0x00001180]> V
(in visual mode, swap the 'jnz sym.exit' for a 'jz sym.exit')
$ ./valhalla.out
Are you Elite?
Yes you are!
Flag: Badge_Of_Honour_Please
  1. Runic (Unicode block) on Wikipedia