NSEC25 General Bilge Alarm - Sun, Jun 1, 2025 - Quentin Stiévenart Lucas Bajolet
This is a joint write-up for the GBA challenge that each of our team (Hubert and Hubière) solved independently and in different ways (different approaches and tools). This illustrates two different routes on how to solve such challenges.
Context
We are given two files:
general_bilge_alarm.elf
general_bilge_alarm.gba
Looks like a GBA game. The .gba
is there so we can actually run it, and the .elf
is there so we can reverse it. First, we can play the game with an emulator (better to mute the sound beforehand):
$ apt install visualboyadvance
$ visualboyadvance general_bilge_alarm.gba
No obvious flag at first sight. It might be good to look at the binary before continuing. Hopefully we won’t have to go back to the emulator.
$ strings general_bilge_alarm.elf | grep -ia FLAG
FLAG-%s%s%s%s
Approach 1: Looking at the decompiled code
The tool of choice for Hubert was Ghidra, and in particular its decompilation view.
If we open up the .elf
in Ghidra, Ghidra does most of the reversing for us, we just have to read C code. We find that the flag will be printed on the screen given the right configuration:
void submit_configuration(void)
{
int iVar1;
undefined b4 [4];
undefined local_168;
undefined b3 [5];
undefined local_15f;
undefined b2 [5];
undefined local_157;
undefined b1 [4];
undefined data [288];
char flag_to_show [28];
int i;
memcpy(data,
"GQMXGTNDCWCABEGWGBKDFJMJTBWFACYMMSHFFTNZMHGGXQEUZHVFTCPJCKUEAMGBSXABZRMTKQCFUFXWFJMZFBMFTM DXZMVZXSRPQTCMFMBKNVMVXZRQYQBGCQYYNVSNFGEWHKBDZJMXZSFEAUVSEPGQQYTKJNQMEEFHZQCFSXFDKVPSQMUPT JSMCRGNJZKCDJDQNSHTGPBMJUKUDQWMJUKMEYQFPZGXHCCTNPHYBUEQXSYCEMWQAHMVPNKYTAY"
,0x100);
iVar1 = configurator();
if (iVar1 != 1) {
do {
bm_puts(0x78,100,"BAD CONFIG",0x1f);
bm_puts(0x46,0x50,"BAD CONFIG",0x1f);
bm_puts(0x1e,0x5a,"BAD CONFIG",0x1f);
bm_puts(200,0x28,"BAD CONFIG",0x1f);
} while( true );
}
for (i = 0; i < 3; i = i + 1) {
b1[i] = data[(_DAT_04000000 & 7) + i];
}
data[1] = 0;
for (i = 0; i < 3; i = i + 1) {
b2[i] = data[(_DAT_04000080 & 0x77) + i];
}
local_157 = 0;
for (i = 0; i < 3; i = i + 1) {
b3[i] = data[(_DAT_04000060 & 7) + i];
}
local_15f = 0;
for (i = 0; i < 3; i = i + 1) {
b4[i] = data[(_DAT_04000000 & 7) + i];
}
local_168 = 0;
do {
sprintf(flag_to_show,"FLAG-%s%s%s%s",b1,b2,b3,b4);
bm_puts(10,0x3c,flag_to_show,0x3ff);
} while( true );
}
If configurator()
is happy and returns 1, the flag will be computed in four parts, all of which are read from the long string stored in data
. The only expressions of interest that influence this computation are:
_DAT_04000000 & 7
(used twice)_DAT_04000080 & 0x77
_DAT_04000060 & 7
Looking for usages of these globals, we find an initialization in test_tte_obj
, which strangely refers to the string “Stem Cell Generator” (the name of the GBA challenge of NSEC 2024). It is probably because of legacy code that was not removed, and it will be of no use for this challenge. We find another initialization in main
, which is probably the one used.
The main loop (function main_loop
) might be interesting as it writes to our globals, but it is a bit messy.
It seems that we need to enter some key combination to modify the globals and then trigger the flag calculation and print the flag, as long as configurator
is happy with the result of our keys. configurator
itself is reading from the globals.
Now we can take two routes: investigate configurator
, or investigate main_loop
. In Hubert, Quentin delved into configurator
, as it seemed to be the shorter route.
Looking at configurator
Not wanting to understand that the main loop, we can inspect configurator
first, as it is reading from our globals:
undefined8 configurator(void)
{
undefined2 uVar1;
int iVar2;
int iVar3;
int extraout_r1;
undefined4 extraout_r1_00;
undefined4 uVar4;
undefined4 in_lr;
uVar1 = _DAT_0400010c;
if ((_DAT_04000000 & 7) == 5) {
if ((_DAT_04000080 & 0x77) == 0x33) {
if ((_DAT_04000060 & 7) == 6) {
____aeabi_idivmod_from_thumb(_DAT_0400010c,0x3c);
iVar2 = ____aeabi_idiv_from_thumb(uVar1,0xe10);
____aeabi_idivmod_from_thumb(uVar1,0xe10);
iVar3 = ____aeabi_idiv_from_thumb(extraout_r1_00,0x3c);
if (((iVar2 == 0) && (iVar3 == 0)) && (extraout_r1 == 0x25)) {
uVar4 = 1; // our goal
}
else {
uVar4 = 0;
}
}
else {
uVar4 = 0;
}
}
else {
uVar4 = 0;
}
}
else {
uVar4 = 0;
}
return CONCAT44(in_lr,uVar4);
}
According to this, the flag will be computed only if:
(_DAT_04000000 & 7) == 5
(_DAT_04000080 & 0x77) == 0x33
(_DAT_04000060 & 7) == 6
- Some more complex operations on
_DAT_0400010c
. We will ignore them for now.
Parts 1-3 give us the values of all the expressions that we identified as relevant to compute the flag. We can just reimplement the same logic and get the flag.
(That’s if you’re not too sleep-deprived. Otherwise, you might reimplement it incorrectly. Fortunately, klammydia pointed out that i < 3
is not i < 4
).
$ python
>>> data = 'GQMXGTNDCWCABEGWGBKDFJMJTBWFACYMMSHFFTNZMHGGXQEUZHVFTCPJCKUEAMGBSXABZRMTKQCFUFXWFJMZFBMFTMDXZMVZXSRPQTCMFMBKNVMVXZRQYQBGCQYYNVSNFGEWHKBDZJMXZSFEAUVSEPGQQYTKJNQMEEFHZQCFSXFDKVPSQMUPTJSMCRGNJZKCDJDQNSHTGPBMJUKUDQWMJUKMEYQFPZGXHCCTNPHYBUEQXSYCEMWQAHMVPNKYTAY'
>>> 'FLAG-%s%s%s%s' % (data[5:5+3], data[0x33:0x33+3], data[6:6+3], data[5:5+3])
'FLAG-TNDFTCNDCTND'
Odd flag, but it works:
$ askgod submit FLAG-TNDFTCNDCTND
Congratulations, you score your team 3 points!
Message: We should now be in total control of the bilge alarm. We can override it at will.
Good thing we didn’t have to look at main_loop
!
Approach 2: Looking at main_loop
The approach taken by Hubière is at a lower level: looking at the disassembly view in Binary Ninja to understand the intended combination of buttons to reach the end of the validation function for the flag.
First, we’ll need a primer on the buttons of the GBA, and how they’re read.
There are some grassroot articles available online, the one used for this challenge is Kyle Halladay’s.
Essentially, inputs are mapped to address 0x4000130
, and each button is mapped to a bit at this address.
Since we have 10 buttons, we need two bytes to store all the values.
From this, we need a map of the different keys, which is fortunately given in the article linked above.
With this knowledge, we can take a look at the main loop function.
Lucas was using Binary Ninja to reverse-engineer this binary, for some reason, the loop could not be figured out by the tool, and the graph doesn’t show it.
By using a dichotomic approach, we can figure out that the part of the loop that is interesting to us starts after the last call to the draw_cool_lines
function, so 0x8000c5a
.
Below this, there are a few instructions to setup our registers, then two alternative paths, including one path doing the following:
ldr r3, [pc, #0x110] {0x400010c} {data_8000d80}
ldrh r3, [r3] {0x400010c}
lsls r3, r3, #0x10
lsrs r3, r3, #0x10
adds r2, r7, r6
str r3, [r2] {var_20_1}
adds r3, r7, r6
ldr r3, [r3] {var_20_1}
movs r2, #0xe1
lsls r1, r2, #4 {0xe10}
movs r0, r3
bl #____aeabi_uidiv_from_thumb
movs r3, r0
movs r4, r3
adds r3, r7, r6
ldr r3, [r3] {var_20_1}
movs r2, #0xe1
lsls r1, r2, #4 {0xe10}
movs r0, r3
bl #____aeabi_uidivmod_from_thumb
movs r3, r1
movs r1, #0x3c
movs r0, r3
bl #____aeabi_uidiv_from_thumb
movs r3, r0
movs r5, r3
adds r3, r7, r6
ldr r3, [r3] {var_20_1}
movs r1, #0x3c
movs r0, r3
bl #____aeabi_uidivmod_from_thumb
movs r3, r1
ldr r1, [pc, #0xcc] {data_8000d84} {data_800ff94, "%02d:%02d:%02d"}
adds r0, r7, #4 {var_bc}
str r3, [sp] {var_d0}
movs r3, r5
movs r2, r4
bl #sprintf
This is basically the routine that blits the clock on the bottom right of the screen, and reads a timer from an address: 0x400010c
. That’s kept as a 16-bit counter, and basically counts the number of seconds.
It’s not exactly clear what the exact logic that triggers this execution is, but it’s not super useful to us, besides to know that this address holds the global timer, which we’ll need to pass checks later on.
The other alternative is the main loop that handles button presses. There are a few things here, which trigger some changes to internal values, and consequently makes the “game” behave differently.
In this loop, there are a few calls to key_hit
, that’s a function included in this binary that checks if a key has changed state between two loops, and if the new state is set; so basically it only returns true
when we press a button, not when we keep it pressed.
With that in mind, there are a few button presses that are acknowledged by our loop:
0x80 || 0x04
(Down arrow or Select) – those two keys trigger a change in address0x400010a
, this toggles the timer.0x20
(Left arrow) – this decrements the value kept in the local stack atr7 + 0x9c
, which started at 0x3 before the main loop.0x10
(Right arrow) – this increments the value kept in the local stack atr7 + 0x9c
, making it effectively the corollary of pressing the left arrow.
Note that the left/right arrow values are clamped between 1 and 5.
After those, we have a small amount of code that shuffles values from the local frame to global values. Some we already know, some we don’t yet. In a nutshell:
r7 + 0x9c
->0x4000000
r7 + 0x98
->0x4000080
, this for some reason duplicates the last 4 bits to the next 4 (i.e. 0x03 -> 0x33).r7 + 0x94
->0x4000060
Once we’re done with this, we have a few more key presses to handle.
0x02
(B) – Increments the value atr7 + 0x94
(maximum 7)0x01
(A) – Decrements the value atr7 + 0x94
(minimum 1)0x40
(Up arrow) – Increments the value atr7 + 0x98
(maximum 7)0x80
(Down arrow) – Decrements the value atr7 + 0x98
(minimum 0)
And that’s about it for the main loop, with this knowledge in mind, let’s revisit the configurator
function.
Configurator (again)
Remember that there were a few checks that we need to pass in order for the flag to print out on our screen.
(0x04000000 & 7) == 5
(0x04000080 & 0x77) == 0x33
(0x04000060 & 7) == 6
- Some more complex operations on
0x0400010c
Having looked at the main loop, we know what those values mean, and how to manipulate them through the buttons, so now we can start playing with those to get the right combination so we can successfully pass those checks.
0x4000000 & 0x7
-> This is controlled by the left/right arrows, the starting value can be figured out through the disassembly, or more easily with an emulator with a memory inspector (I used mGBA, but VBA likely works too). The value starts with 3, we need 5, so we can press the Right arrow twice to get this (this makes the screen unreadable).0x4000080 & 0x77
-> This is controlled by the up/down arrow, and starts at 0x77; since we want to reach 0x33, we should press down 4 times.0x4000060 & 0x7
-> This is controlled by the B/A buttons, and starts 0x31 (so 1 since we& 0x07
the value), since we want it to be6
, let’s pressB
5 times.
Finally, the mystery value was the timer, and the obscure operations were just divisions/modulos on it. The GBA processor is soft-float, so the operations are implemented as functions, and while I didn’t dig into the exact implementation for them, we can fairly easily infer the convention for them.
The dividend is stored in r0
, and the divisor in r1
, the result of the operation is then returned in r0
.
Since the ELF binary we’ve been given is not stripped, we can easily know which is a division, and which is a modulo.
This makes it much easier to figure out what are the values we want out of the timer.
There are a series of 4 operations here:
timer % 60
-> result stored in*(r7 + 0x8)
: that’s the number of secondstimer / 3600
-> result stored in*(r7 + 0x4)
: that’s the number of hours(timer % 3600) / 60
-> result stored in*(r7)
: that’s the number of minutes
We then compare those values each to the following:
*(r7) == 0
- 0 hours*(r7 + 0x4) == 0
- 0 minutes*(r7 + 0x8) == 0x25
- 37 seconds
With that, we should have the full procedure for the flag to appear!
Getting the flag
Opening the GBA rom with our trusty emulator, we can start playing with the values so we reach the expected state.
So to summarize:
- Press
Down
4 times - Press
B
5 times - Press
Right
twice - Wait for 37 seconds
- Press
Start
to triggerconfigurator
(Yeah we didn’t explain where that check was, because we don’t know :p)