UMDCTF 2022 Writeup(s)
Bongocat
The challenge is given as an Intel Hex file. Converting that to a raw binary file and tossing it in Veles, we see some sort of structured data.

Looks like some sort of machine code at a glance, but which is it? I had no idea, so I tossed it into Airbus’ cpu_rec, which suggested that it might be AVR. Fascinating.
What’s next in the triage? Well, let’s take a glance at the hexdump. A few strings immediately stood out.

Googling it led us to a repo that the challenge appears to be based on.

OK, so it’s some sort of firmware for a lily58 keyboard. Googling, it seems to be based on the Pro Micro, which hosts a Atmega32u4. This matches my expectation of AVR from using cpu_rec. IDA Pro has support for AVR, so let’s try opening it up. But wait, there’s no support for our specific CPU? Oh well, whatever, we can still try using ATmega32_L and see how far that gets us.

And what it got us was a complete mess. The compiler has decided to inline the entrypoint function that is called at the end of _RESET. Seeing this, I somewhat despaired but not completely – I realized that QMK was GPL2 licensed, so I slid into the authors DMs asking for the code, hoping that they’d do it given that I likely was the only one who would do something so ridiculous and also because it would be funny. It was 4 AM so I did not get an answer until much later (saying no until after the CTF is over, which is reasonable). In the meantime, I continued my triage.

At this point, I decided the best way forward was to call in the support: I joined the QMK discord and started poking around. Amazingly, someone instantly recognized what this code is!

OK, very nice. Let’s generate a comparison binary using the QMK configurator then and do a comparison. We were able to get some matches, which is a suggestion that the assumption that this is inlined is correct.


But that does not get us very far. There are only a few hours left, and I did not consider it realistic to fully finish triaging this binary that is inlined and optimized, mapping out the RAM layout accurately, and writing a new IDA CFG for AVR to support the Atmega32u4 to be realistic in that timeframe. Had this been a longer running CTF, brute force static reverse engineering like that would’ve been my route.
OK, what then is the alternative? My next attempt is at dynamic analysis – I wanted to emulate this since I do not have a physical device to run this on. That did not get very far – I found what seemed to be an emulator but I could not get it working. And then interestingly, someone from the QMK Discord did have an Atmega32u4 and attempted to run the firmware for me, and confirmed that it communicates via i2c to the oled, however nothing showed up. I concluded then (wrongly) that perhaps it is not the oled but rather USB via which the flag is being sent – as a series of keystrokes potentially, or hidden as the key mapping (so that the first row would contain the flag perhaps). In which case, I had no chance of getting things going – a binary like this is a daunting task for me in such a timeframe.
So I took a break, and then after a while the author released a new binary that supposedly would make things much easier after I mentioned to them the issue with inlining. And indeed it was a lot nicer! We could actually see things clearly now.

So I spent the next hour reversing, mapping functions from the “known good” to this binary manually – but then I started wondering if I am approaching this wrong. So I asked the author for a sanity check to confirm and indeed I was wrong – the keymapping did not contain the flag. What then is left? Well, maybe the oled screen does indeed contain the flag – after all, the challenge is called bongocat and the original code did show the bongo cat via the oled display.
So I reviewed the code in the repo again when an epiphany struck me and reminded me of the thought I had at the back of my mind the entire time – that cantor.dust would be the perfect tool for this! Unfortunately, Battelle and Christopher Domas never released the original cantor.dust, all we have is Veles which sadly does not have the feature of cantor.dust that is useful for exploring unknown bitmap data unfortunately. But that is no matter – the bitmap is there, and if it is unencrypted, we can still pull it out.
So then, we need to extract the non-code portion of the binary, and try to parse it and see if we can get an image out of it somehow. We can see clearly via Veles that the first portion of the file contains an array of jmps that is standard to the CPU platform, and then some data, and then code.

Doing some research, I found a tool that is able to convert between the image format for OLED and back. So I grabbed a random portion of the RAM data and shoved it into the tool (set to 128×32 as that is the resolution of the display for the Lily58), shuffled it around until something interesting popped up.

That looks to me like some sort of font data! Font data has the same format as the plain image bitmap, so we clearly are on the right track. Eventually, something wonderful popped up.

I was exhilarated to see this as I was expecting to not finish this challenge at that point. Moving things around some more, I finally had the buffer that yielded the fully legible bitmap:
00 00 00 00 00 F0 00 00 E0 30 30 60 F0 10 10 10 B0 E0 40 80 60 20 20 30 30 C0 00 00 C0 40 40 60 60 00 00 00 40 40 40 80 C0 40 40 40 40 40 00 80 F8 88 88 88 88 88 88 40 00 00 00 00 F8 88 08 18 00 00 C0 40 40 40 40 40 C0 80 80 00 00 00 00 00 00 80 00 80 60 30 60 80 80 C0 60 40 C0 00 08 F0 00 80 80 40 60 00 00 00 00 00 00 00 00 00 00 00 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 1C 10 10 10 1C 07 00 00 0F 00 00 00 01 00 00 1C 07 00 0E 13 10 10 1C 06 03 01 00 3F 21 20 20 30 10 10 00 00 00 00 30 0C 03 00 00 00 00 00 00 0F 18 00 00 00 00 00 00 00 18 14 36 23 C1 81 00 00 00 00 7F C0 80 80 DC 30 60 E0 BF 00 00 00 00 00 40 21 1F 03 00 00 3C 0F 03 00 78 4C 03 00 06 79 03 05 0C 38 40 00 00 00 40 40 40 40 40 40 00 00 78 0F 00 0F C9 CB 6A 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FC 04 04 04 0C 08 00 70 98 08 08 08 98 F0 00 00 E0 30 10 10 10 20 E0 80 00 F0 00 00 00 08 C4 C8 48 78 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 61 62 6F 75 74 20 68 65 72 65 00 29 00 1E 00 1F 00 20 00 21 00 22 00 2B 00 14 00 1A 00 08 00 15 00 17 00 E0 00 04 00 16 00 07 00 09 00 0A 00 E1 00 1D 00 1B 00 06 00 19 00 05 00 00 00 01 51 E3 00 E2 00 2C 00 2F 00 35 00 27 00 26 00 25 00 24 00 23 00 2D 00 13 00 12 00 0C 00 18 00 1C 00 34 00 33 00 0F 00 0E 00 0D 00 0B 00 E5 00 38 00 37 00 36 00 10 00 11 00 00 00 02 51 E7 00 2A 00 28 00 30 00 01 00 01

And that’s the flag: UMDCTF{QMK_is_cool}.
I am a strong believer in the use of data visualization in reverse engineering and Veles is a tool that I almost always use as an initial triage of an unknown binary. And I am very glad that in this case, a similar approach worked out well. Special thanks to Sir Kane, KarlK90, Xelux and fauxpark for helping me with this unfamiliar architecture.
Kernel Infernal 1 & 2
Most of the trouble was not with the challenge data. Rather, it was getting Linux debugging going. That took me a good 2 hours – but once everything was ready it was fairly smooth sailing. The working directory of the crasher was the base64 encoded version of the flag for 1, and for 2 we are tasked with finding the “first CR3”. Initially, I interpreted this as finding the CR3 that was used during early boot phase and spent an hour reading early boot code to see if that is saved somewhere or printed somewhere, but no luck there. But then I realized that I was overthinking it, and it really was just looking for the CR3 of the first process. So I used ps in crash to get the task_struct of the first process (swapper running on core 0), and then I used gdb x/128xg to dump the active_mm struct. Using IDA on the vmlinux binary to help visualize mm_struct better, I found the pgd value which was the flag.