Some of us at UMBC competed in UMDCTF 2022 this weekend! We had a fun time. Here are writeups for some of the challenges I personally solved.

Challenges sectioned off by category:

forensics

Kernel Infernal 1

The challenge provides a kdump (kernel dump) file.

nikola@laminator:~/downloads$ file ubuntu20.04-5.4.0-99-generic-cloudimg-20220215.kdump 
ubuntu20.04-5.4.0-99-generic-cloudimg-20220215.kdump: Kdump compressed dump v6, system Linux, node ubuntu, release 5.4.0-99-generic, version #112-Ubuntu SMP Thu Feb 3 13:50:55 UTC 2022, machine x86_64, domain (none)

The challenge text hints that we need to find the working directory of… something.

the kernel hadp an accident, wwhere was the scene of thde crime?
              ~              ~                         ~

kdump files are analyzed using the crash tool. In a Ubuntu container, we can install it via apt install crash.

We also need to install the debug info package. Ultimately, the package we need is apt install linux-image-5.4.0-99-generic-dbgsym. On Ubuntu, for me, this provided /usr/lib/debug/boot/vmlinux-5.4.0-99-generic, not the file in /usr/lib/debug/lib/modules/ that the tutorials said to look for.

Now we can run crash ubuntu20.04-5.4.0-99-generic-cloudimg-20220215.kdump /usr/lib/debug/boot/vmlinux-5.4.0-99-generic. This takes a second to process the dump, then gives us a crash> shell. Run the files command to view the open files at crash time, and more importantly, the CWD of (I believe) the process that caused the crash (?).

crash> files
PID: 5206   TASK: ffff9b1878e45d00  CPU: 1   COMMAND: "bash"
ROOT: /    CWD: /home/ubuntu/VU1EQ1RGe1QwdGExMXlDUjQ1SCEhfQ==
 FD       FILE            DENTRY           INODE       TYPE PATH
  0 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1
  1 ffff9b1878f99a00 ffff9b187c093540 ffff9b187c1cfad8 REG  /proc/sysrq-trigger
  2 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1
 10 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1
255 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1

There it is in base64 - that’s our flag!

$ base64 -d
VU1EQ1RGe1QwdGExMXlDUjQ1SCEhfQ==
UMDCTF{T0ta11yCR45H!!}

misc

Blockchain 1 - Hashcash

HashCash is a proof-of-work mechanism for fighting email spam; each email you send needs an X-Hashcash header with a payload that took some work to generate.

I first tried using this webapp, but to no avail (even after editing the JS to remove the hour and minute from the timestamp, it still had the values in hex instead of base64.)

However, turns out it’s easier than that - hashcash is in Ubuntu’s default repos. apt install hashcash. Use the -m flag to “mint” a hashcash payload, and -b 20 to specify 20 bits.

nikola@laminator:~$ hashcash -m gary@hashcash.com -b 20
hashcash token: 1:20:220305:gary@hashcash.com::vEpC5AphTiZoTJmX:000000000000000000000000000000000000000000000QaO

RSI 2

Google reveals a .osr file is an Osu! game replay file. I tried a few graphical Osu! replay viewers, but none of them seemed to work. So I installed osrparse.

Commands run (some removed for brevity):

import osrparse
r = osrparse.parse_replay_file("Replays/big_b.osr")
from pprint import pprint
pprint(vars(r))
p = r.play_data
pprint(p[0])
pprint([(x.x, x.y) for x in p])

import numpy as np
import matplotlib.pyplot as plt
xc = [x.x for x in p]
yc = [x.y for x in p]
plt.scatter(xc, yc)
plt.show()

RSI 1

I actually solved this one after RSI 2, funnily enough. Normally, trying to parse the osr file yields this error:

>>> r = osrparse.parse_replay_file("Replays/tutorial.osr")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/tmp/nikola/osu/ultimate_osu_analyzer/venv/lib/python3.8/site-packages/osrparse/replay.py", line 146, in parse_replay_file
    return parse_replay(data)
  File "/tmp/nikola/osu/ultimate_osu_analyzer/venv/lib/python3.8/site-packages/osrparse/replay.py", line 141, in parse_replay
    return Replay(replay_data)
  File "/tmp/nikola/osu/ultimate_osu_analyzer/venv/lib/python3.8/site-packages/osrparse/replay.py", line 40, in __init__
    self.parse_replay_and_initialize_fields(replay_data)
  File "/tmp/nikola/osu/ultimate_osu_analyzer/venv/lib/python3.8/site-packages/osrparse/replay.py", line 50, in parse_replay_and_initialize_fields
    self.parse_play_data(replay_data)
  File "/tmp/nikola/osu/ultimate_osu_analyzer/venv/lib/python3.8/site-packages/osrparse/replay.py", line 137, in parse_play_data
    self.play_data = [ReplayEvent(int(event[0]), float(event[1]), float(event[2]), int(event[3])) for event in events]
  File "/tmp/nikola/osu/ultimate_osu_analyzer/venv/lib/python3.8/site-packages/osrparse/replay.py", line 137, in <listcomp>
    self.play_data = [ReplayEvent(int(event[0]), float(event[1]), float(event[2]), int(event[3])) for event in events]
ValueError: invalid literal for int() with base 10: '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0

Since I was in a venv, I popped open the replay.py file and edited the erroring line to try and get some data out of it. I had to restart the Python interpreter and re-import the module (importlib.reload didn’t work). After some fiddling around, I did the following edit: self.play_data = [event for event in events]

And this yielded:

>>> r = osrparse.parse_replay_file("Replays/tutorial.osr")
>>> r
<osrparse.replay.Replay object at 0x7f0c06913f70>
>>> pprint(vars(r))
{'_Replay__replay_length': 53,
 'beatmap_hash': '3c8b50ebd781978beb39160c6aaf148c',
 'game_mode': <GameMode.Standard: 0>,
 'game_version': 20220222,
 'gekis': -1,
 'is_perfect_combo': True,
 'katus': 0,
 'life_bar_graph': '0|1,',
 'max_combo': -1,
 'misses': 0,
 'mod_combination': frozenset({<Mod.Hidden: 8>,
                               <Mod.HardRock: 16>,
                               <Mod.SuddenDeath: 32>,
                               <Mod.DoubleTime: 64>,
                               <Mod.Flashlight: 1024>}),
 'number_100s': 0,
 'number_300s': -1,
 'number_50s': 0,
 'offset': 180,
 'play_data': [['\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00UMDCTF{wE1c0m3_t0_o5u!}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']],
 'player_name': 'striker4150',
 'replay_hash': '5a3e5fce9240a9913982a656256653f5',
 'score': -1,
 'timestamp': datetime.datetime(2022, 2, 28, 0, 0)}
>>> 

The flag is visible inside the zeroes: UMDCTF{wE1c0m3_t0_o5u!}.

rev

tiny

Scan the QR code. It outputs a bunch of base64 text. Decode this, and extract it as a file - it is a compressed archive.

Extract the contents of the archive. It yields an ELF executable, but for MIPS!

root@ca946f284fd3:~# file extracted 
extracted: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, BuildID[sha1]=e44d3fe1ac41d4181999b4336415c1e6271bbea8, for GNU/Linux 3.2.0, stripped

Use qemu to run it. (apt install qemu qemu-mips qemu-system-mips qemu-user, then apt install gcc-mips-linux-gnu g++-mips-linux-gnu for the libraries.)

root@ca946f284fd3:~# qemu-mips -L /usr/mips-linux-gnu/ extracted 
lol no
lol no

Aw.

Pop it open in Ghidra, and we see that in main(), two functions are being called with an argument of 0. Within each, if the argument is zero, it just prints “lol no”, so obviously it won’t work.

We can just patch the arguments to be 1 instead. In MIPS the a0 register is typically the first argument.

before after

Now export the patched binary… (File > Export Program > Select format ELF) and run it.

root@ca946f284fd3:~# qemu-mips -L /usr/mips-linux-gnu/ patched3 
umdctf{h3y_m0m_1m_0n_th3_r@d1o}

DragonPit

Executing the binary yields a GLIBC version error:

root@ca946f284fd3:~# ./dragonPit 
./dragonPit: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./dragonPit)

To fix this, I downloaded the source for glibc 2.34, built it, and installed it into /opt/glibc-2.34. Now we can use its ld to run the binary.

root@ca946f284fd3:~# /opt/glibc-2.14/lib/ld-linux-x86-64.so.2 ./dragonPit 
When should Fred jump into the dragon pit? 
idk
You waited too long! 
Red Team has slain the Dragon! 
An Ally has been slain! 
root@ca946f284fd3:~# 

Looking in Ghidra, it seems we need our user input - which is 16 characters long - to match a string that is encoded in local_98,90,88,84 (84 being the null terminator). I tried just patching the conditional out of the binary, but to no avail - when printing the flag, the last few characters rely on parts of the user’s input. So we do need to figure out this “password” string.

Hovering over the variable contents reveals the string, albeit in reverse. Reverse them and put them together.

wh3nTheD
r4gsHPis
1048

wh3nTheDr4gsHPis1048
root@ca946f284fd3:~# /opt/glibc-2.14/lib/ld-linux-x86-64.so.2 ./dragonPit 
When should Fred jump into the dragon pit? 
wh3nTheDr4gsHPis1048
UMDCTF{BluSt0l3dr4g}

hw/rf

Gee, queue are ex?

As the title implies, open Gqrx and load in the IQ file.

enter image description here (Tweak the rate setting to get it going at a reasonable speed.)

enter image description here

The faces on the spectrogram spell out the flag.

pwn

Legacy

When failing it, you’ll see that it runs under Python 2.

$ nc 0.cloud.chals.io 28964 
I bet you can't guess my *secret* number!
I'll give you hint, its between 0 and 0,1000000000000000514!
1
2
3
3 chances left! 
2 chances left! 
1 chances left! 
Deprecated shmeprecated!
Python 2 will never die!

If you supply blank input, you’ll get an error and traceback:

$ nc 0.cloud.chals.io 28964 
I bet you can't guess my *secret* number!
I'll give you hint, its between 0 and 0,1000000000000000514!
2

3 chances left! 
2 chances left! 
Traceback (most recent call last):
  File "/home/ctf/legacy.py", line 15, in <module>
    if (input(str(3-i) + " chances left! \n") == secret):
  File "<string>", line 0
    
    ^
SyntaxError: unexpected EOF while parsing

Google “python2 vulnerability input” and see: https://www.geeksforgeeks.org/vulnerability-input-function-python-2-x/

Just supply secret as your input, and it will assume you mean the “secret” variable.

$ nc 0.cloud.chals.io 28964 
I bet you can't guess my *secret* number!
I'll give you hint, its between 0 and 0,1000000000000000514!
secret
3 chances left! 
No way!
UMDCTF{W3_H8_p7th0n2}