Irony - CatTheQuest CTF 2024
Title: Irony⌗
Category: Reverse⌗
Medium - 12 points⌗
Description:
One of the hackers is a GameBoy fan. You might find a crucial element to trace them in this file that was lingering on the network.
Hint:
Use ghidra with gameboy plugin in order that can help your reverse engineer the game - Utilisez Ghidra avec le plugin Gameboy pour vous aider à faire de l ingénierie inverse du jeu.
Flag example: CAT{Koi9_Koi9_E0_09876}
Attachments: irony.gb
Challenge author: Ir0nByte
This was a reverse engineering challenge where they provided a ROM for the GameBoy. 9 teams could solve it. It took me about two days to solve it. I usually don’t do reverse engineering challenges because they are time intensive, but this CTF lasted 5 days.
Initial analysis⌗
You can use any GameBoy emulator to run it. I used the BGB emulator/debugger. If you play it, you will find this gardener.
If you explore more, you will find another NPC that gives you a hint.
If you execute strings irony.gb
you can find this:
{Mew0_Mew0_G0_%d}
looks like part of the flag, but it’s using string formatting. That means %d
will be replaced with a number. The real challenge is finding that number.
Failed attempts⌗
In the challenge description they added a hint that using this GameBoy extension for ghidra could be useful for doing static analysis, so I started from there. However, for me it wasn’t very helpful. It can’t find most of the functions in the ROM so you must find them manually, and the pseudocode generated misses most of the functionality.
I also looked for references to the address of the flag string, but ghidra found nothing.
BGB has a functionality called “cheat searcher” which can be used to find interesting addresses in memory. It is similar to Cheat Engine. With that I found where the counter variable is stored in memory.
There are three memory addresses where it is stored. You can see that at CA50
is it’s absolute value. CBB5
and D915
look like signed integers, but which is the real counter? BGB allows you to set up watchpoints to stop the code when something reads/writes/executes/jumps to an address. If you setup a write watchpoint at those addresses and begin to single-step through the code, sometimes D915
gets overwritten with other values that are not the counter. That indicates that it’s probably a function parameter. CBB5
is the real counter address.
My first idea was to set up a read watchpoint at CBB5
, and use trace i.e. single step until I stumble upon a conditional jump that used the current counter value. Here’s the gameboy CPU manual I used for reference.
However, I single-stepped for ages and couldn’t find it. When you talk with the gardener, the counter is reset to 0. For some reason, the gardener only reads it once, after setting the counter to 0. Like, what?
I tried to do the same with D915
and I found nothing. Maybe I missed it, but I ended up trying other ideas.
I looked for the address in memory of the I won't give the flag
string, and set up a read watchpoint. With trace reverse I could single-step backwards. I wanted to eventually find something like this:
if counter==secret:
print("I won't give the flag...")
else:
print("CAT\n{Mew0_Mew0_G0_%d}..." % counter)
With time I realized that there is a loop that reads the previous bytes in sequence. While reading it, sometimes it jumps somewhere and does lots of things before coming back and continuing to read it, or used some of these values to calculate memory addresses to read/write at runtime.
I wouldn’t say that I’m good at reverse engineering, so this had me confused for a while. As you will see, I ended up realizing this is bytecode.
Solution⌗
At some point I began wondering how the ROM was made. By searching different IDEs, I found this:
That is one of the default sample projects in GB Studio. It is similar to the game provided, so I guessed that’s how it was made. Then, I used GB Studio to create different ROMs based on this default sample project, with very small changes to the code of the gardener.
The first ROM I made had this:
Then I compiled another ROM with only one small change:
If $TurnipCounter == 23456
toIf $TurnipCounter == 12345
And compared the different ROMs with 010 editor
39 30
->0x3039
->12345
A0 5B
->0x5BA0
->23456
This is interesting. The number is stored near the string. I compiled more ROMs with similar changes like:
If $TurnipCounter == 12345
toIf $TurnipCounter != 23456
If $TurnipCounter == 12345
toIf $Seen Cat == 23456
Please help me, there is $Turnip Counter left
toPlease help me, there is $Seen Cat left
By diffing them, and single-stepping more with BGB, I found all of this:
- Before the string, at
CA48
,0B
is the ID of the variable that will replace%d
(in the originalirony.gb
ROM, the variable ID used for bothCounter = %d
andCAT\n{Mew0_Mew0_G0_%d}...
is11
, which confirms that the unknown number is replaced with the counter) - At
CA33
,0B
is a reference to $Turnip Counter - At
CA38
is the operation used (02 is<
, 01 is==
, 06 is!=
, 04 is>
) - At
CA36
-CA37
is the number that it is comparing with - The NULL byte at the end of the string (at
CA13
) indicates the end of the string - The
40
atCA46
seems to indicate the beginning of the string Please help me, there is %d left
will be read if the condition is falseThank you so much!!
andI'm not planting those next year
will be read if the condition is true
That is how I slowly realized that I was looking at bytecode. I was really sure that the number is inside this section.
I thought that the 11
at 00A6
might be a reference to the counter. I tried to set the counter value to some of the surrounding bytes, following the same little endian format as before and… nothing. Weird.
I began questioning everything. Is this really a simple ==
comparison? Why is it so different from the ROMs I made before? I want to understand what this bytecode is doing, but how?
I already found the address of the function that “interprets” these bytes, but it is quite big and it would take me forever to reverse. Diffing more ROMs could be a better option, but that would also take time. How else could we reverse engineer an undocumented bytecode interpreter?
Wait… undocumented?
I searched for “gb studio bytecode” and found GBVM. I wasn’t sure if this is it, so I dug into the source code:
- https://github.com/chrismaltby/gbvm/blob/master/src/core/vm_instructions.c#L47
- https://github.com/chrismaltby/gbvm/blob/master/src/core/vm.c#L200
- https://github.com/chrismaltby/gbvm/blob/master/include/vm.h#L82
// logical operators
#define VM_OP_EQ 1
#define VM_OP_LT 2
#define VM_OP_LE 3
#define VM_OP_GT 4
#define VM_OP_GE 5
#define VM_OP_NE 6
#define VM_OP_AND 7
#define VM_OP_OR 8
#define VM_OP_NOT 9
The values of VM_OP_EQ
VM_OP_LT
VM_OP_GT
VM_OP_NE
match with the ones I found before.
At
CA38
is the operation used (02 is<
, 01 is==
, 06 is!=
, 04 is>
)
// here we define all VM instructions: their handlers and parameter lengths in bytes
// this array must be nonbanked as well as STEP_VM()
// ...
{vm_load_text, BANK(VM_UI), 1}, // 0x40
This also matches with my previous finding.
The
40
atCA46
seems to indicate the beginning of the string
This must be it! Now it is easy to manually decode the instructions.
47 03 01 05 14 00 00 41 ff 00 44 07 01 45 fe 12 00 44 03 01 55 57 5e 0c 05 53 85 08 55 57 97 0c 06 53 86 04 1a 00 40 f3 78 2f 00 11 01
To make sure I got the alignment correctly, I began decoding it from the start.
47 vm_overlay_clear
03 01 05 14 00 00
41 vm_display_text
ff 00
44 vm_overlay_wait
07 01
45 vm_overlay_move_to
fe 12 00
44 vm_overlay_wait
03 01
55 vm_context_prepare
57 5e 0c 05
53 vm_input_attach
85 08
55 vm_context_prepare
57 97 0c 06
53 vm_input_attach
86 04
1a vm_if_const
00 40 f3 78 2f 00 11 01
vm_if_const
is an if condition! Let’s decode it:
1a 00 40 f3 78 2f 00 11 01
1a
is vm_if_const00
40 f3
is the address of thevm_load_text
instruction for the flag string78 2f
is the other number00 11
is the ID of the counter variable01
is an == comparison
78 2f
is the missing number! Yay!
It turns out that it wasn’t little endian… bruh. At least we got the flag :)
CAT{Mew0_Mew0_G0_30767}
Note: I didn’t mention other rabbit holes I fell into, like using ghidriff or checking the .sav files generated when you save the game, but they didn’t lead to anything worth mentioning.