Military Grade was another challenge at 2022’s Car Hack Village that no team was able to solve during the competition. Which means it was perfect to spend some more time diving in and understanding just why it went unsolved. Here’s the description:

This device contains military-grade encryption. Which country’s military is up for debate. Here’s the flag: 3EJQ6KSW4DJZBKGWD8RM6YFW92MU6YFX5AKM29FRV8DUXYA

# Files provided

This was part of a series of challenges targeted at an embedded device that was some micro controller that interfaced with vehicles. I’m not entirely sure what the original purpose of the device is, however we are given one single firmware image, fw_image.

# Board info

We were able to look at the chip and see it was a MCF54415CMJ250, and binwalk -A fw_image tells us that there are many Motorola Coldfire instructions, so time to load it up into Ghidra and start chipping away at it.

# Ghidra

Upon first load, as we have no base address, things will not have proper references. This is evident by the fact that if you search for strings in Ghidra, and look for X-REF’s, there will be nothing. So the first challenge is identifying the structure of the firmware image. I accomplished this by staring at the first 0x150 or so bytes until things made sense.

00000000: 4e58 5100 ffff ffff 31e4 37aa 001d 3454  NXQ.....1.7...4T
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 3030 2e30 3136 3231 3200 4d00 0000 0000  00.016212.M.....
00000060: 400d 8de6 0000 0000 0000 0000 0000 0000  @...............
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0f80 4000 0000 0000 0400 0000 1380  ....@...........
00000110: 4000 0600 001c c400 001c d780 4100 0000  @...........A...
00000120: 0000 5cd4 0000 0000 0000 0000 0000 0000  ..\.............
00000130: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000140: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000150: 0000 0000 0000 0000 0000 0000 0000 0000  ................

As we can see, there seems to be some magic bytes at the start, NXQ but google reveals nothing and file also reports nothing. This is looking like a custom file format.

# Reversing the file format

At 0x50 into the image, there is what appears to be a type of numbering or version label. Interesting, but not what I want. Looking further at 0x100 we can see, in sections of 4 bytes, something interesting. The first is that of 0000 0f80, 4000 0000, 0000 0400. The bytes in the middle stood out to me as a potential load address, as 0x40000000 is somewhat common. At this point, I took a few guesses as to what the other bytes meant, and eventually arrived at the fact that 0000 0f80 was the offset into the base firmware image, and 0000 0400 was the length of data. This was confirmed by looking at the next set of bytes.

If we extend this pattern to the rest we can clearly that our guess fits, because our next guess at an offset 0000 1380 is exactly equal to the previous offset plus the previous length 0000 0f80 + 0000 0400 = 0x1380. This fits for the next section as well. So we can write a quick script to extract these sections of the file and correctly map them into Ghidra.

# Quick into to the Ghidra API

Ghidra has a very rich API with an extensive library of documentation. Our goal is to programmatically create all the necessary memory blocks within a program. To do this we will be using the python bindings for Ghidra. We need to start by getting a reference to the current programs memory, this can all be input into the Ghidra python console.

mem = currentProgram.getMemory()

This allows us to work within the confines of the memory. Next, we need to get a reference block to set the default permissions of our soon to be created memory regions.

block = mem.getBlock(currentAddress)

For now, the memory protections of the current block are more than alright. Next, we will need to iterate through the address ranges of the firmware image and define these as memory regions instantiated with the data from our image, then fill them with the data bytes from the file.

import struct # Need to work with bytes
with open('./fw_image','rb') as fd:
    dat = fd.read()
    fd.seek(0x100)  # Offset to our file table
    for _ in range(3): # Only three entries
        offset = struct.unpack(">L",fd.read(4))
        locmap = struct.unpack(">L",fd.read(4))
        length = struct.unpack(">L",fd.read(4))
        data = dat[offset:offset+length]
        mem.createBlock(block, f"{hex(locmap)}", toAddr(locmap), length)
        for i in range(len(data)):
            mem.setByte(toAddr(locmap + i), ord(data[i]))

Now we have a sane memory map with proper references.

# Finding the decryption method.

Luckily for us, the challenge authors gave us a nice string to easily find, checking Search -> For Strings... and searching for decrypt shows us one reference. Awesome. Unfortunately for us, this is an undefined function with improperly displayed calling conventions. Here is the entire function

undefined4 UndefinedFunction_400a75fc(void)

{
  int in_D0;
  int iVar1;

  if (1 < in_D0) {
    iVar1 = FUN_400fc538();
    if (iVar1 == 0) {
      FUN_400abb78(0x90,s_ServerMain_401ba1ed,6,s_configuring_wifi_parameters_401ba1fd);
      *_DAT_416af9e8 = 4;
      _DAT_416af9e8[1] = _DAT_416af9e8[1] + 1;
      FUN_400fc508();
      _DAT_416af9e8[6] = 1;
      _DAT_416af9e8[7] = 0;
      FUN_4002b024();
    }
    iVar1 = FUN_400fc538();
    if ((iVar1 == 0) && (2 < in_D0)) {
      FUN_400abb78(0x90,s_ServerMain_401ba1ed,6,s_"server_debug_<level>"_has_been_r_401ba21f);
    }
    iVar1 = FUN_400fc538();
    if (iVar1 == 0) {
      if (in_D0 < 3) {
        _DAT_416af9e8[0x46] = 0;
      }
      else {
        iVar1 = FUN_400fc538();
        if (iVar1 == 0) {
          _DAT_416af9e8[0x46] = 1;
        }
        else {
          iVar1 = FUN_400fc538();
          if (iVar1 == 0) {
            _DAT_416af9e8[0x46] = 2;
          }
          else {
            _DAT_416af9e8[0x46] = 3;
          }
        }
      }
      FUN_4002b024();
    }
    iVar1 = FUN_400fc538();
    if (iVar1 == 0) {
      FUN_400fc49c();
      FUN_400fc49c();
      iVar1 = FUN_400a9f04();
      if (0 < iVar1) {
        FUN_400aa210();
      }
    }
  }
  return 0;
}

This is a lot. Lets start by defining it by pressing F at the very top of the disassembly view.

We can move down to where the string decrypt is referenced

                             LAB_400a76ac                                    XREF[1]:     400a768a(j)
        400a76ac 20 2b 00 04     move.l     (0x4,A3),D0
        400a76b0 20 40           movea.l    D0,A0
        400a76b2 43 f9 40        lea        (s_decrypt_401ba257).l,A1                        = "decrypt"
                 1b a2 57
        400a76b8 4e b9 40        jsr        FUN_400fc538.l                                   undefined FUN_400fc538()
                 0f c5 38

Digging in to the that function FUN_400fc538, here is our decompilation.

int FUN_400fc538(void)

{
  byte bVar1;
  byte bVar2;
  byte *in_A0;
  byte *pbVar3;
  byte *in_A1;
  byte *pbVar4;

  bVar1 = *in_A0;
  bVar2 = *in_A1;
  pbVar3 = in_A0 + 1;
  pbVar4 = in_A1 + 1;
  if (bVar1 == bVar2) {
    do {
      if (bVar1 == 0) {
        return 0;
      }
      bVar1 = *pbVar3;
      bVar2 = *pbVar4;
      pbVar3 = pbVar3 + 1;
      pbVar4 = pbVar4 + 1;
    } while (bVar1 == bVar2);
  }
  return (uint)bVar1 - (uint)bVar2;
}

Nothing too serious, looks like pretty simple strcmp. So we compare some value with decrypt, we can extrapolate from this and guess that the value passed into A0 is some user input, while A1 is the address of our compare value.

We can fix this function to make it slightly prettier to look at. Right click the function and click Edit function, on the side you will need to check the Use Custom Storage box, and then click the green plus on the side. Lets edit parameter 1 by double clicking on it and setting its type to a register variable with a location of A0, do the same with param2, setting it to A1.

iVar2 = strcmp(*(undefined4 *)(in_A0 + 4),s_decrypt_401ba257);

While not entirely necessary for this part of the challenge, this sort of decompilation fix-up is very useful analyzing large images. Moving on to FUN_400fc49c

void FUN_400fd25c(void)

{
  uint in_D0;
  uint uVar1;
  uint in_D1;
  uint uVar2;
  uint *in_A0;
  uint *puVar3;

  uVar2 = in_D0 & 0xff;
  if (0x1f < in_D1) {
    uVar1 = -(int)in_A0 & 3;
    if (uVar1 != 0) {
      in_D1 = in_D1 - uVar1;
      puVar3 = in_A0;
      do {
        in_A0 = (uint *)((int)puVar3 + 1);
        *(char *)puVar3 = (char)uVar2;
        uVar1 = uVar1 - 1;
        puVar3 = in_A0;
      } while (uVar1 != 0);
    }
    if (uVar2 != 0) {
      uVar2 = uVar2 | uVar2 << 0x18 | uVar2 << 0x10 | uVar2 << 8;
    }
    for (uVar1 = in_D1 >> 5; uVar1 != 0; uVar1 = uVar1 - 1) {
      *in_A0 = uVar2;
      in_A0[1] = uVar2;
      in_A0[2] = uVar2;
      in_A0[3] = uVar2;
      in_A0[4] = uVar2;
      in_A0[5] = uVar2;
      puVar3 = in_A0 + 7;
      in_A0[6] = uVar2;
      in_A0 = in_A0 + 8;
      *puVar3 = uVar2;
    }
    for (uVar1 = (in_D1 & 0x1f) >> 2; uVar1 != 0; uVar1 = uVar1 - 1) {
      *in_A0 = uVar2;
      in_A0 = in_A0 + 1;
    }
    in_D1 = in_D1 & 3;
  }
  for (; in_D1 != 0; in_D1 = in_D1 - 1) {
    *(char *)in_A0 = (char)uVar2;
    in_A0 = (uint *)((int)in_A0 + 1);
  }
  return;
}

This looks like a bit of a mess, so lets start by fixing up the calling convention.

        400a76c4 41 ee fe ec     lea        (-0x114,A6),A0
        400a76c8 73 7c 00 84     mvs.w:     #0x84,D1
        400a76cc 42 80           clr.l      D0
        400a76ce 4e b9 40        jsr        FUN_400fc49c.l                                   undefined FUN_400fc49c()
                 0f c4 9c

It looks like some address is placed into A0, some immediate is placed into D1, and D0 is cleared.

Doing the same steps as before to clean up the arguments, we now have this

FUN_400fc49c(auStack280,0x84,0);

Diving into the function, we see it’s a wrapper around another. Lets set the same calling convention here. Looking at the second function, near the end, after naming the params, we are left with this.

for (; const != 0; const = const - 1) { 
  *(char *)buffer = (char)uVar2;     
  buffer = (uint *)((int)buffer + 1);
} 

At the top, uVar2 is set equal to our nul constant. We can assume this is probably memset, and move on.

# Discovering the base32 function

Next, we have FUN_400a9f04 which is quite long. Lets apply the same fixing up to the calling convention. Here is what we are given

int FUN_400a9f04(void)

{
  bool bVar1;
  short in_D0w;
  int iVar2;
  int iVar3;
  int iVar4;
  uint uVar5;
  uint uVar6;
  byte bVar7;
  int iVar8;
  int iVar9;
  int in_A0;
  undefined *puVar10;
  int in_A1;
  undefined local_40;
  undefined local_3f;
  undefined uStack62;
  undefined uStack61;
  undefined uStack60;
  undefined uStack59;
  undefined uStack58;
  undefined uStack57;
  undefined uStack56;
  undefined uStack55;
  undefined uStack54;
  undefined uStack53;
  undefined uStack52;
  undefined uStack51;
  undefined uStack50;
  undefined uStack49;
  undefined uStack48;
  undefined uStack47;
  undefined uStack46;
  undefined uStack45;
  undefined uStack44;
  undefined uStack43;
  undefined uStack42;
  undefined uStack41;
  undefined uStack40;
  undefined uStack39;
  undefined uStack38;
  undefined uStack37;
  undefined uStack36;
  undefined uStack35;
  undefined uStack34;
  undefined uStack33;

  if (((in_A1 != 0) && (in_A0 != 0)) && (-1 < in_D0w)) {
    FUN_400fc49c(&local_40,0x20,0);
    local_40 = 0x51;
    local_3f = 0x41;
    uStack62 = 0x5a;
    uStack61 = 0x32;
    uStack60 = 0x57;
    uStack59 = 0x53;
    uStack58 = 0x58;
    uStack57 = 0x33;
    uStack56 = 0x45;
    uStack55 = 0x44;
    uStack54 = 0x43;
    uStack53 = 0x34;
    uStack52 = 0x52;
    uStack51 = 0x46;
    uStack50 = 0x56;
    uStack49 = 0x35;
    uStack48 = 0x54;
    uStack47 = 0x47;
    uStack46 = 0x42;
    uStack45 = 0x36;
    uStack44 = 0x59;
    uStack43 = 0x48;
    uStack42 = 0x4e;
    uStack41 = 0x37;
    uStack40 = 0x55;
    uStack39 = 0x4a;
    uStack38 = 0x4d;
    uStack37 = 0x38;
    uStack36 = 0x4b;
    uStack35 = 0x39;
    uStack34 = 0x4c;
    uStack33 = 0x50;
    iVar2 = FUN_400fc4f8();
    iVar9 = (int)(iVar2 * 5 + ((uint)(iVar2 * 5 >> 2) >> 0x1d)) >> 3;
    iVar4 = 0;
    if (0 < iVar2) {
      do {
        bVar7 = *(byte *)(in_A0 + iVar4);
        uVar6 = (uint)bVar7;
        bVar1 = 0x7f < uVar6;
        uVar5 = uVar6;
        if (bVar1) {
          uVar5 = 0xffffffff;
        }
        if (((&DAT_401cab09)[uVar5] & 3) != 0) {
          uVar5 = uVar6;
          if (bVar1) {
            uVar5 = 0xffffffff;
          }
          if (((&DAT_401cab09)[uVar5] & 1) == 0) {
            if (bVar1) {
              uVar6 = 0xffffffff;
            }
            if (((&DAT_401cab09)[uVar6] & 2) != 0) {
              bVar7 = bVar7 - 0x20;
            }
            *(byte *)(in_A0 + iVar4) = bVar7;
          }
        }
        iVar4 = iVar4 + 1;
      } while (iVar4 < iVar2);
    }
    if (2 < iVar2) {
      puVar10 = &local_40;
      FUN_400fc57c();
      if (puVar10 == (undefined *)0x0) {
        uVar5 = 0xffffffff;
      }
      else {
        uVar5 = (int)puVar10 - (int)&local_40;
      }
      puVar10 = &local_40;
      FUN_400fc57c();
      if (puVar10 == (undefined *)0x0) {
        iVar4 = -1;
      }
      else {
        iVar4 = (int)puVar10 - (int)&local_40;
      }
      if ((-1 < (int)uVar5) && (-1 < iVar4)) {
        uVar5 = uVar5 | iVar4 << 5;
        uVar6 = 10;
        iVar4 = 2;
        iVar8 = 0;
        if (iVar9 < 1) {
          return iVar9;
        }
        do {
          *(char *)(in_A1 + iVar8) = (char)uVar5;
          uVar5 = (int)uVar5 >> 8;
          uVar6 = uVar6 - 8;
          while (((int)uVar6 < 8 && (iVar4 < iVar2))) {
            puVar10 = &local_40;
            iVar4 = iVar4 + 1;
            FUN_400fc57c();
            if (puVar10 == (undefined *)0x0) {
              iVar3 = -1;
            }
            else {
              iVar3 = (int)puVar10 - (int)&local_40;
            }
            if (iVar3 < 0) {
              return 0;
            }
            uVar5 = uVar5 | iVar3 << (uVar6 & 0x3f);
            uVar6 = uVar6 + 5;
          }
          iVar8 = iVar8 + 1;
          if (iVar9 <= iVar8) {
            return iVar9;
          }
        } while( true );
      }
    }
  }
  return -1;
}

We can see a very suspicious loading of a string on the stack. This is a common method to hide secret data from simple analysis. Once we take all the hex values and convert it to ASCII, we are left with what looks suspiciously like a base32 alphabet. QAZ2WSX3EDC4RFV5TGB6YHN7UJM8K9LP.

The first sub function we call is FUN_400fc4f8, here is what we have.

int FUN_400fc4f8(void)                 
{
  char cVar1;                          
  int iVar2;                           
  char *in_A0;
                                       
  iVar2 = -1;
  do {         
    iVar2 = iVar2 + 1;
    cVar1 = *in_A0;                    
    in_A0 = in_A0 + 1;                 
  } while (cVar1 != '\0');
  return iVar2;                        
}

This is pretty obviously strlen. Lets label it as such and move on. The next sub function is FUN_400fc57c, lets fix up the calling convention and look at it.

int FUN_400fc57c(char *param_1,undefined4 param_2)                            
{
  char cVar1;
  char *pcVar2;
  cVar1 = *param_1;
  pcVar2 = param_1 + 1;
  while (cVar1 != '\0') { 
    if (cVar1 == (char)param_2) {
      return (int)(pcVar2 + -1);
    }
    cVar1 = *pcVar2;
    pcVar2 = pcVar2 + 1;
  }                          
  if ((char)param_2 == '\0') {
    return (int)(pcVar2 + -1);
  }       
  return 0;
} 

After some reasoning, I determined this was strchr, and labeled it as such. Finally, there is one more function to check, FUN_400aa210. We apply the same fix up, and can see its applying a 5 byte xor key NEXIQ to whatever is supplied to it.

uint FUN_400aa210(byte *param_1,byte *out)

{
  uint in_D0;
  int iVar1;
  uint i;
  byte *pbVar2;
  byte *pbVar3;
  byte buf [8];

  iVar1 = 5;
  pbVar2 = (byte *)s_NEXIQWIFICONFIG_ONUSB_401ba4ce;
  pbVar3 = buf;
  do {
    *pbVar3 = *pbVar2;
    iVar1 = iVar1 + -1;
    pbVar2 = pbVar2 + 1;
    pbVar3 = pbVar3 + 1;
  } while (iVar1 != 0);
  if ((param_1 == (byte *)0x0) || (out == (byte *)0x0)) {
    i = in_D0 & 0xffffff00;
  }
  else {
    i = 0;
    if (0 < (int)in_D0) {
      do {
        out[i] = param_1[i] ^ buf[i % 5];
        i = i + 1;
      } while ((int)i < (int)in_D0);
    }
    i = 1;
  }
  return i;
}

Without the need to dive into our base32 function, instead lets just rip the code out of Ghidra and compile it ourselves. This part was mostly straightforward, just time consuming. After cleaning up our variable names and some typecasting, this is the function I extracted.

#include <stdio.h>
#include <string.h>

void DECRYPT(char * param_1)
{
  char next;
  char * strchr_r;
  int inp_len;
  int inp, idx;
  int i, j, iVar4;
  int unsigned_input;
  unsigned char unsigned_byt;
  char alphabet[] = "QAZ2WSX3EDC4RFV5TGB6YHN7UJM8K9LP";
  char key[] = "NEXIQ";

  if ((param_1 != (char *)0x0) ) {
    inp_len = strlen(param_1);
    iVar4 = (int)(inp_len * 5 + ((int)(inp_len * 5 >> 2) >> 0x1d)) >> 3;
    i = 0;
    if (2 < inp_len) {
      strchr_r = strchr(alphabet, (int)param_1[0]);
      if (strchr_r == 0x0) {
        inp = 0xffffffff;
      }
      else {
        inp = (int)(strchr_r - (int)alphabet);
      }
      strchr_r = strchr(alphabet, (int)param_1[1]);
      if (strchr_r == 0x0) {
        i = -1;
      }
      else {
        i = (int)(strchr_r - (int)alphabet);
      }
      if ((-1 < (int)inp) && (-1 < i)) {
        inp = inp | i << 5;
        unsigned_byt = 10;
        i = 2;
        j = 0;
        if (iVar4 < 1) {
          return;
        }
        do {
          // Print the flag
          printf("%c", ((char)inp & 0xff) ^ key[j % 5]);
          inp = (int)inp >> 8;
          unsigned_byt = unsigned_byt - 8;
          while (((int)unsigned_byt < 8 && (i < inp_len))) {
            next = param_1[i];
            i = i + 1;
            strchr_r = strchr(alphabet, (int)next);
            if (strchr_r == 0x0) {
              idx = -1;
            }
            else {
              idx = (int)(strchr_r - (int)alphabet);
            }
            if (idx < 0) {
              return;
            }
            inp = inp | idx << (unsigned_byt & 0x3f);
            unsigned_byt = unsigned_byt + 5;
          }
          j = j + 1;
          if (iVar4 <= j) {
            return ;
          }
        } while( 1 );
      }
    }
  }
  puts("");
  return;
}

int main() {
        DECRYPT("3EJQ6KSW4DJZBKGWD8RM6YFW92MU6YFX5AKM29FRV8DUXYA");
}

# Getting our flag

Finally, lets save this as military-grade.c, compile this and cross our fingers that it works. gcc military-grade.c, ignore our warnings and run it.

$ ./a.out
I h0pe y0u've r3-d ba5e32 b4!

And there’s our flag.