Hubert Hackin''
  • All posts
  • About
  • Our CTF

NSEC25 General Bilge Alarm - Sun, Jun 1, 2025 - Quentin Stiévenart Lucas Bajolet

Archaeology | Rev | Nsec25

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:

  1. _DAT_04000000 & 7 (used twice)
  2. _DAT_04000080 & 0x77
  3. _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:

  1. (_DAT_04000000 & 7) == 5
  2. (_DAT_04000080 & 0x77) == 0x33
  3. (_DAT_04000060 & 7) == 6
  4. 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 address 0x400010a, this toggles the timer.
  • 0x20 (Left arrow) – this decrements the value kept in the local stack at r7 + 0x9c, which started at 0x3 before the main loop.
  • 0x10 (Right arrow) – this increments the value kept in the local stack at r7 + 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 at r7 + 0x94 (maximum 7)
  • 0x01 (A) – Decrements the value at r7 + 0x94 (minimum 1)
  • 0x40 (Up arrow) – Increments the value at r7 + 0x98 (maximum 7)
  • 0x80 (Down arrow) – Decrements the value at r7 + 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.

  1. (0x04000000 & 7) == 5
  2. (0x04000080 & 0x77) == 0x33
  3. (0x04000060 & 7) == 6
  4. 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.

  1. 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).
  2. 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.
  3. 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 be 6, let’s press B 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:

  1. timer % 60 -> result stored in *(r7 + 0x8): that’s the number of seconds
  2. timer / 3600 -> result stored in *(r7 + 0x4): that’s the number of hours
  3. (timer % 3600) / 60 -> result stored in *(r7): that’s the number of minutes

We then compare those values each to the following:

  1. *(r7) == 0 - 0 hours
  2. *(r7 + 0x4) == 0 - 0 minutes
  3. *(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:

  1. Press Down 4 times
  2. Press B 5 times
  3. Press Right twice
  4. Wait for 37 seconds
  5. Press Start to trigger configurator (Yeah we didn’t explain where that check was, because we don’t know :p)

flag

Back to Home


Hackez la Rue! | © Hubert Hackin'' | 2025-06-06 | theme hugo.386