Skip to content

Latest commit

 

History

History
835 lines (733 loc) · 26.6 KB

archventuretime.md

File metadata and controls

835 lines (733 loc) · 26.6 KB

Archventuretime

Archventuretime is a reversing challenge, where you have to obtain a license-key by reversing a binary with checks the key. The binary creates 4 new binaries, which are all compiled for different architectures.

First look

The challenge consists of binary and a Dockerfile. The Dockerfile installs various QEMU packages on an ubuntu system and then starts the binary. After starting the docker you are prompted with Enter license key> , so I entered a few random chars and, surprise, [WARNING] Invalid format! (Note: i changed the flag in docker run -t to docker run -ti to get inputs working).

The main binary

So let's look at the included binary chal:

> file chal
chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, [...] , for GNU/Linux 3.2.0, stripped

Unfortunately chal is stripped so reversing it will be a bit harder. So let's get to work and decompile chal with ghidra. If we search for the Enter license key> , we find the following functions. I added some comments to it, with ideas I got from the initial look.

undefined8 FUN_00101c48(void) {
  /* [...] <- variable definitions removed for readability */
  
  // read liscence key up to length 24d, and remove new lines
  printf("Enter license key> ");
  fgets((char *)&local_48,0x18,stdin);
  sVar1 = strcspn((char *)&local_48,"\n");
  *(undefined *)((long)&local_48 + sVar1) = 0;

  // call some function with the liscence key, not sure what it does yet
  FUN_001014a9(&local_48);
  
  // remove every 6th char from the key and save the result in local_28
  local_50 = 0;
  for (local_4c = 0; local_4c < 0x17; local_4c = local_4c  + 1) {
    if ((local_4c + 1) % 6 == 0) {
      local_50 = local_50 + 1;
    }
    else {
      *(undefined *)((long)&local_28 + (long)(local_4c - l ocal_50)) =
           *(undefined *)((long)&local_48 + (long)local_4c) ;
    }
  }

  // call a function, with the stripped liscence key
  FUN_001015dc(&local_28);

  // call a function with the liscence key
  FUN_00101920(&local_48);
  
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return * /
    __stack_chk_fail();
  }
  return 0;
}

We saw that the functions reads the key and then calls three different functions with it. So let's call the function readKey and take a look what the first called function does. We already know, that it takes the license-key as an argument, so I already renamed the parameter and added comments:

void FUN_001014a9(char *key) {
  size_t sVar1;
  ushort **ppuVar2;
  int local_c;
  
  // ensure that the key length is >= 23d, else print a warning and exit
  // since we read keys with length up to 24d we know that the liscence key must exactly be 24d 
  // chars long
  sVar1 = strlen(key);
  if (sVar1 < 0x17) {
    puts(PTR_s_[WARNING]_Invalid_format!_0012a010) ;
                    /* WARNING: Subroutine does not return * /
    exit(1);
  }
  // loop through the key
  local_c = 0;
  do {
    if (0x16 < local_c) {
      return;
    }
    // check that every 6th char is a '-' 
    if ((local_c + 1) % 6 == 0) {
      if (key[local_c] != '-') {
        puts(PTR_s_[WARNING]_Invalid_format!_0012a01 0);
                    /* WARNING: Subroutine does not return * /
        exit(1);
      }
    }
    // check that every other char is uppercase and alphanumeric, if this is not the case
    // print a warning and exit the programm
    else {
      // __ctype_b_loc() returns a struct with informations about the char
      ppuVar2 = __ctype_b_loc();
      // char is not upper case?
      if (((*ppuVar2)[key[local_c]] & 0x800) == 0) {
        ppuVar2 = __ctype_b_loc();
        // char is not a number?
        if (((*ppuVar2)[key[local_c]] & 0x100) == 0) {
          puts(PTR_s_[WARNING]_Invalid_format!_0012a0 10);
                    /* WARNING: Subroutine does not return * /
          exit(1);
        }
      }
    }
    local_c = local_c + 1;
  } while(true);
}

So the method seems to check that the license-key is in the format "XXXXX-XXXXX-XXXXX-XXXXX", with X being an alphanumeric char. So let's call the method checkFormat and construct a key and input it into the binary.

> ./chal
Enter license key> 12345-ABCDE-12345-ABCDE
[ERROR] Invalid license key!

Yaaay 🎉, a different error message. Since the error message isn't printed in checkFormat our key has the correct format now and we can continue to the next method call in the readKey function. Again, I already renamed the parameter of the function and added comments:

void FUN_001015dc(char *strippedKey) {
   /* [...] <- variable definitions removed for readability */

  // change working directory to '/tmp'
  chdir("/tmp");
  
  // loop with 4 iterations
  for (local_d0 = 0; local_d0 < 4; local_d0 = local_d0 + 1 ) {
    lVar3 = (long)(int)local_d0;
    puVar1 = (&PTR_s_qemu-riscv64_-L_/usr/riscv64-li n_00129c80)[lVar3 * 3];
    __buf = (&PTR_DAT_00129c88)[lVar3 * 3];
    __n = *(size_t *)(&DAT_00129c90 + lVar3 * 0x18);
    local_a4 = 0x5858586b63656863;
    local_9c = 0x585858;
    iVar2 = mkstemp((char *)&local_a4);
    write(iVar2,__buf,__n);
    close(iVar2);
    chmod((char *)&local_a4,0x1c0);
    snprintf((char *)&local_98,0x80,"%s %s %s",puVar1, &local_a4,strippedKey);
    iVar2 = system((char *)&local_98);
    if (iVar2 != 0) {
      remove((char *)&local_a4);
      puts(PTR_s_[ERROR]_Invalid_license_key!_0012a0 18);
                    /* WARNING: Subroutine does not return * /
      exit(1);
    }
    remove((char *)&local_a4);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return * /
    __stack_chk_fail();
  }
  return;
}

Ok a lot is happening here, let's give names to the variables:

void FUN_001015dc(char *strippedKey) {

  /* [...] <- variable definitions removed for readability */

  // change working directory to '/tmp'
  chdir("/tmp");

  // loop with 4 iterations
  for (idx = 0; idx < 4; idx = idx + 1) {
    idx_ = (long)(int)idx;
  // command in the form of qemu-riscv64 -L /usr/riscv64-linux-gnu, the architecture changes in every iteration */
    commandPrefix = (&PTR_s_qemu-riscv64_-L_/usr/r iscv64-lin_00129c80)[idx_ * 3];

	// read raw
    __buf = (&PTR_DAT_00129c88)[idx_ * 3];
    __n = *(size_t *)(&DAT_00129c90 + idx_ * 0x18);

	// create a file with name checkXXXXXX, with X being random chars 
	// and write __buf into it
    filename = 0x5858586b63656863;
    local_9c = 0x585858;
    fileDescriptor = mkstemp((char *)&filename);
    write(fileDescriptor,__buf,__n);
    close(fileDescriptor);

	// make the file executable
    chmod((char *)&filename,0x1c0);

	// execute the command qemu-riscv64 -L <architecture> filename strippedKey
    snprintf((char *)&local_98,0x80,"%s %s %s",commandPrefix,&filename,strippedKey);
    status = system((char *)&local_98);

	// if the previously executed command returns an error, print an error and exit
	
    if (status != 0) {
      // deltete the file
      remove((char *)&filename);
      puts(PTR_s_[ERROR]_Invalid_license_key!_0012a0 18);
                    /* WARNING: Subroutine does not return * /
      exit(1);
    }
    // delete the file
    remove((char *)&filenameTemplate);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return * /
    __stack_chk_fail();
  }
  return;
}

The function creates 4 new binaries, executes them with the license-key as an argument. If one binary fails, an error is printed and the chal exists, so we call the function checkWithBinaries. Before we dive deeper in the newly created binaries, let's take a quick look at the third function call in the readKey function:

void FUN_00101920(void *param_1) {
    /* [...] <- variable definitions removed for readability */

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts(PTR_s_[CORRECT]_License_key_validated_001 2a020);
  printf("Decrypting product");
  fflush(stdout);
  for (local_130 = 0; local_130 < 3; local_130 = local_13 0 + 1) {
    sleep(1);
    putc(0x2e,stdout);
    fflush(stdout);
  }
  putc(10,stdout);
  putc(10,stdout);
  fflush(stdout);

/* [...] <- variable definitions removed for readability */


  // use AES to decrypt the Flag with the key
  iVar1 = FUN_00101828(&local_e8,0x50,&local_108,&l ocal_118,&local_98);
  *(undefined *)((long)&local_98 + (long)iVar1) = 0;
  puts(
      "Welcome to GPN CTF 2024!\n\n            ========= ==            \n        ===================        \n     -= ======================-     \n    ============ ===============    \n  -===============-==== =========-  \n  ===========::::::============= =  \n =============:=========::::====== \n==== ===========:::::::::::::=======\n===========::=-::: ::::::::::=======\n==========::::=-::::::::::========= \n=========::::::=-:-================\n====== ==::::::::=-:================\n ======::::::::::=-:== ============ \n  =====:::::::::::=-=============  \n  -=====:::::::::::=============-  \n    ========= ==================    \n     -================ =======-     \n        ===================        \n            ===========            \n\n"
      );
  puts((char *)&local_98);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return * /
    __stack_chk_fail();
  }
  return;
}

This function seems to decrypt the Flag with the license-key using the AES and print a nice message. As far as I can tell, the key decryption looks save. So we need to obtain the license-key by reversing the binaries created by the checkWithBinaries function.

Four new binaries

At first, we actually need to obtain the binaries, which isn't trivial because the are instantly delted after their execution:

void  checkWithBinaries(char  *strippedKey) {
    // [...]
	if  (status !=  0)  {  
		remove((char  *)&filename);
		puts(PTR_s_[ERROR]_Invalid_license_key!_0012a0 18); 
		exit(1); 
	}
	// [...]
}

I can think of two good ways to achieve this:

  • Debug chal with gdb and break right before remove gets called
  • Patch the binary and change remove to strlen for example

I went with the second option, and created chal_patched, executing it yields:

> ./chal_patched
Enter license key> 12345-ABCDE-12345-ABCDE
[ERROR] Invalid license key!
> ls /tmp | grep "check"
checkr5Rt4S

Voila, the first binary! Decompiling it with ghirda and searching a bit yields the following main function (I already renamed a few symbols):

ulong main(int param_1,long param_2)

{
  undefined4 strLen;
  long check;
  ulong succ_;
  char *arg1;
  
  if (param_1 < 2) {
    succ_ = 1;
  }
  else {
    arg1 = *(char **)(param_2 + 8);
    strLen = getStrLen(arg1);
    sort(arg1,strLen);
    check = strcmp(arg1,"067889BBCKKMOPPUVWYY");
    if (check == 0) {
      succ_ = 0;
    }
    else {
      succ_ = 1;
    }
  }
  return succ_;
}

Sort is just a basic quicksort implementation. So the binary just checks if the inputed liscence-key consists of the same chars as the correct liscence key in arbitrary order. With this we know all chars of our Key 067889BBCKKMOPPUVWYY and can construct a new key bypassing the first binary:

> rm /tmp/check*
> ./chal_patched 
Enter license key> 06788-9BBCK-KMOPP-UVWYY
[ERROR] Invalid license key!
> ls /tmp/ | grep check
checkHhOuNh
checkJRzpc8

Nice! Our second binary, lets also analyze it with ghidra (I already renamed a few symbols):

undefined8 main(int param_1,long param_2) {

   /* [...] <- variable definitions removed for readability */
  
  if (param_1 < 2) {
    uVar1 = 1;
  }
  else {
    __s = *(char **)(param_2 + 8);
    strlen(__s);
    // loop through the key, split it in to 4 blocks of size 5
    for (local_24 = 0; local_24 < 4; local_24 = local_24 + 1) {
      local_20 = 0;
      local_1c = 0;
      for (local_18 = local_24 * 5; local_18 < (local_24 + 1) * 5; local_18 = local_18 + 1) {
       // check if the char is uppercase
       // if thats the case we add its value (ascii representation) and -0x41 to local_1c 
        ppuVar2 = __ctype_b_loc();
        if (((*ppuVar2)[__s[local_18]] & 0x800) == 0) {
          ppuVar2 = __ctype_b_loc();
          if (((*ppuVar2)[__s[local_18]] & 0x100) != 0) {
            local_1c = local_1c + __s[local_18] + -0x41;
          }
        }
        // check if the char is numeric
        // if thats the case we add its value and -0x30 to local_20
        else {
          local_20 = local_20 + __s[local_18] + -0x30;
        }
      }
      // check if the sum for every block matches some constant
      // numeric chars and uppercase chars are seperated into two diffrent sums
      if (local_20 != *(int *)(&DAT_00102010 + (long)loca l_24 * 4)) {
        return 1;
      }
      if (local_1c != *(int *)(&DAT_00102020 + (long)loca l_24 * 4)) {
        return 1;
      }
    }
    uVar1 = 0;
  }
  return uVar1;
}

So this method splits the Key into 4 Blocks of 5 and then sums the block seperatly for uppercase and numeric chars. For example:

Key: 06788-9BBCK-KMOPP-UVWYY (Note: the key is passed without the '-' to the binary)
Blocks:
06788 -> sumNumeric = 0 + 6 + 7 + 8, sumUppercase = 0
9BBCK -> sumNumeric = 9, sumUppercase = B + B + C + K = 1 + 1 + 2 + 10
KMOPP -> sumNumeric = 0, sumUppercase = K + M + O + P + P = 10 + 12 + 14 + 15
UVWYY -> sumNumeric = 0, sumUppercase = U + V + W + Y + Y = 20 + 21 + 22 + 24 + 34

Then the binary takes those sums and checks each sum against a constant in the data section. With that we can write a script, which given all possible letters (from the first binary) outputs every possible key (Note that this script generates some duplicates, but this won't be a problem):

import itertools  
  
  
# the constants we got from the second binary  
num0 = 0  
num1 = 0x00000007  
num2 = 0x0000000E  
num3 = 0x00000011  
  
let0 = 0x0000003D  
let1 = 0x00000024  
let2 = 0x0000002C  
let3 = 0x00000032  
  
# all possbile letters and numbers (from the first binary)  
letters = list("BBCKKMOPPUVWYY")  
numbers = list("067889")  
  
  
def list_diff(l1, l2):  
    """  
 :return l1 without all elements in l2 """  lx1 = l1[:]  
    for i in l2:  
        if i in lx1:  
            lx1.remove(i)  
    return lx1  
  
  
def get_combinations_up_to_5(l):  
    result = []  
    for i in range(0, 6):  
        result += list(itertools.combinations(l, i))  
    return result  
  
  
def find_sets(l, k):  
    """  
 find sets up to length 5 which sum up to k """  result = []  
    combinations = get_combinations_up_to_5(l)  
    for i in combinations:  
        if sum(i) == k:  
            result.append(i)  
    return result  
  
  
def flatten(l):  
    result = []  
    for i in l:  
        if type(i) is list:  
            for j in i:  
                result.append(j)  
        else:  
            result.append(i)  
    return result  
  
  
def flatten_result(l):  
    result = []  
    for i in l:  
        result.append(flatten(i))  
    return result  
  
  
def combine(elem, l):  
    result = []  
    for i in l:  
        if i is list:  
            for j in i:  
                result.append([elem, j])  
        else:  
            result.append([elem, i])  
    return result  
  
  
def partition_subset_sum(l, sizes):  
    """  
 partitions l in to combinations up to length 5 which sum matches sizes[i] """  valid = find_sets(l, sizes[0])  
    if len(sizes) == 1:  
        return valid  
 result = []  
    for valid_set in valid:  
        new_l = list_diff(l, valid_set)  
        new_valid = partition_subset_sum(new_l, sizes[1:])  
        result += combine(valid_set, new_valid)  
  
    return flatten_result(result)  
  
  
def encode(l, n):  
    encoded = []  
    for i in l:  
        encoded.append(ord(i) - n)  
    return encoded  
  
  
def decode(l, n):  
    decoded = []  
    for i in l:  
        tuple_ = []  
        for j in i:  
            tuple_.append(chr(j + n))  
        decoded.append(tuple(tuple_))  
  
    return decoded  
  
  
number_subsets = [decode(i, 48) for i in partition_subset_sum(encode(numbers, 48), [num0, num1, num2, num3])]  
letter_subsets = [decode(i, 65) for i in partition_subset_sum(encode(letters, 65), [let0, let1, let2, let3])]

Lets add print statements and run the code

for i in letter_subsets:  
    for j in number_subsets:  
        x0 = i[0] + j[0]  
        x1 = i[1] + j[1]  
        x2 = i[2] + j[2]  
        x3 = i[3] + j[3]  
        if len(x0) == 5 and len(x1) == 5 and len(x2) == 5 and len(x3) == 5:  
            print(x0, x1, x2, x3)

With that we can build a new liscence key, which sould get past the first and second binary:

[...]
('K', 'K', 'M', 'O', 'P') ('B', 'P', 'U', '0', '7') ('B', 'V', 'W', '6', '8') ('C', 'Y', 'Y', '8', '9')

Lets bring it into the right format:

KKMOP-BPU07-BVW68-CYY89

And check if it works:

> rm /tmp/check*
> ./chal_patched 
Enter license key> KKMOP-BPU07-BVW68-CYY89
[ERROR] Invalid license key!
> ls /tmp/ | grep check
checkiUZg6q
checklhvtqU
checktyOyll

Here we go, our third binary :). As always we put it into ghidra (I already renamed a few symbols)

undefined4 .opd.FUN_1000074c(int argc,longlong arg s) {
  int len;
  char *alphaNumIndex;
  int notSucc;
  undefined4 uVar1;
  char *key;
  int idx;
  
  if (argc < 2) {
    uVar1 = 1;
  }
  else {
    key = *(char **)(args + 8);
    // strLen wraps strlen
    len = strLen(key);
    // loop through the key
    for (idx = 0; idx < len + -1; idx = idx + 1) {
      // get the index in the alphabet + numerbs in the key
      // AlphaNum points to ABCDEFGHIJKLMNOPQRSTUVWXYZ012345689
      // strChr wraps strchr
      alphaNumIndex._4_4_ = strChr(AlphaNum,key[idx]) ;
      
      // UNK_10000940 points to a array of intergers with only the last 8Bit set
      // So we have 36 sections of size 24.
      // 36 is the size of the alpahbet plus the numbers
      // we pass the start of the section (index by the current char) to charInRange and we 
      // also pass the next char into char in range
      notSucc = charInRange(&UNK_10000940 + (longlong)(alphaNumIndex._4_4_ - (int)AlphaNum) * 96,
                            key[(longlong)idx + 1],24);
      if (notSucc == 0) {
        return 1;
      }
    }
    uVar1 = 0;
  }
  return uVar1;
}

So we loop through the key, in charInRange we perform some sort of check and if returns 0 the program exits with an error. Next we examine charInRange (Symbols renamed):

undefined4 charInRange(longlong prevCharAddr,int char,int x24) {
  int idx;
  
  idx = 0;
  while(true) {
    // x24 is always 24
    if (x24 <= idx) {
      // keep in mind 0 means error
      return 0;
    }
    if (char == *(int *)(prevCharAddr + (longlong)idx * 4) ) break;
    idx = idx + 1;
  }
  return 1;
}

charInRange checks if the next char is somwhere in the section. If that is not the case it returns an error. This is the next constraint for out liscence-key. Nice! The next python script. Lets start off by extracting the array out of the binary:

integers = []
try:  
    with open("<pathToBinary>/third", "rb") as fin:  
        # 0x0000940 is the start address of the array
        # 0x00016C0 the end address
        fin.seek(0x0000940)  
        for i in range(int((0x00016C0 - 0x0000940) / 4)):  
            fin.read(3)  
            x = fin.read(1)  
            integers.append(x)  
  
        for i in range(0, len(integers), 24):  
            blocksOf24.append(integers[i:i + 24])  
except FileNotFoundError:  
    print("File 'third' not found.")  
    exit(1)

Let's also add some code to generate permutations of the previously generated blocks and check if they are within the constrains of the third binary:

from itertools import combinations, permutations  
from second import letter_subsets, number_subsets
  
alphaNum = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")  
  
def getPermutations(l):  
    c = []  
    n = 5  
  for i in range(n - 1, n):  
        c.extend(permutations(l, i + 1))  
    return c  
  
def printAsKey(l):  
    for x in range(len(l)):  
        if (x + 1) % 5 == 0 and x != 19:  
            print(l[x] + "-")  
        else:  
            print(l[x])  
    print("\n")  
  
def getAsInput(l):  
    result = ""  
  for x in range(len(l)):  
        result += l[x]  
    return result  
  
def combineLists(l1, l2):  
    result = []  
    c00 = getPermutations(l1)  
    c1 = getPermutations(l2)  
    for xx in c00:  
        for y in c1:  
            result.append(list(xx) + list(y))  
    return result  
  
integers = []  
blocksOf24 = []  
  
try:  
    with open("third", "rb") as fin:  
        fin.seek(0x0000940)  
        for i in range(int((0x00016C0 - 0x0000940) / 4)):  
            fin.read(3)  
            x = fin.read(1)  
            integers.append(x)  
  
        for i in range(0, len(integers), 24):  
            blocksOf24.append(integers[i:i + 24])  
except FileNotFoundError:  
    print("File 'third' not found.")  
    exit(1)  
  
def checkRule(chr, nextChr):  
    index = alphaNum.index(str(chr))  
    block = blocksOf24[index]  
    if nextChr.encode() in block:  
        return True  
 else:  
        return False  
  
possibleCombinations = []  
  
for pN in number_subsets:  
    for pL in letter_subsets:  
        pC = [pN[0] + pL[0], pN[1] + pL[1], pN[2] + pL[2], pN[3] + pL[3]]  
        continueFlag = False  
 for x in pC:  
            if len(x) != 5:  
                continueFlag = True  
 break if continueFlag:  
            continue  
  possibleCombinations.append(pC)  
  
def check(permutations):  
    result = []  
    for possibility in permutations:  
        appendFlag = True  
 for i in range(4):  
            if not checkRule(possibility[i], possibility[i + 1]):  
                appendFlag = False  
 break if appendFlag:  
            result.append(possibility)  
    return result  
  
def checkBordersAndCombine(l1, l2):  
    result = []  
    for xx in l1:  
        for y in l2:  
            if len(y) == 10:  
                # this condition will make sense after we saw the fourth binary
                if not (y[8] == "8" and y[7] == "M" and y[5] == "Y"):  
                    continue  
 if checkRule(xx[-1], y[0]):  
                result.append(xx + y)  
    return result  
  
allCombinations = []  
for pC in possibleCombinations:  
    firstPermutations = check(getPermutations(pC[0]))  
    secondPermutations = check(getPermutations(pC[1]))  
    thirdPermutations = check(getPermutations(pC[2]))  
    fourthPermutations = check(getPermutations(pC[3]))  
    firstSecond = checkBordersAndCombine(firstPermutations, secondPermutations)  
    thirdFourth = checkBordersAndCombine(thirdPermutations, fourthPermutations)  
    allCombinations__ = checkBordersAndCombine(firstSecond, thirdFourth)  
    allCombinations.extend(allCombinations__)  
  
print(allCombinations[0])

Running the program yields:

('P', 'B', 'V', 'C', 'W', '7', 'B', 'K', 'K', 'P', '0', 'Y', '8', '6', 'U', 'Y', '9', 'M', '8', 'O')

Formating it correctly and inputing it into the chal again gives us:

> rm /tmp/check*
> ./chal_patched 
Enter license key> 06788-9BBCK-KMOPP-UVWYY
[ERROR] Invalid license key!
> ls /tmp/ | grep check
check0HCUhx
checkALAL1v
checkN8Dg5E
checkWGTL4q

Thats the fourth binary! You already know whats the next step, we put it into ghidra :) (Symbols renamed and comments added)

undefined8 FUN_00100754(int param_1,long param_ 2) {

  /* [...] <- variable definitions removed for readability */
  
  if (param_1 < 2) {
    uVar1 = 1;
  }
  else {
    key = *(char **)(param_2 + 8);
    strLen = strlen(key);
    for (idx = 0; idx < (int)strLen; idx = idx + 1) {
      // DAT_001008e8 points to an integer value with len 20
      // All values are 0xFFFFFFFF except those at index 8, 7 and 5
      // 8 -> 8
      // 7 -> M
      // 5 -> Y
      // An Integer with value 0xFFFFFFFF is smaller than 0 so we fail the first check
      // for index 8,7 and 5 we don't
      // => our key is 8 at index 8, M at index 7 and Y at index 5
      if ((0 < *(int *)(&DAT_001008e8 + (long)idx * 4)) &&
         ((uint)(byte)key[idx] != *(uint *)(&DAT_001008e8 + (long)idx * 4))) {
        return 9;
      }
    }
    for (kdx = 0; kdx < (int)strLen; kdx = kdx + 1) {
      for (udx = 0; udx < 10; udx = udx + 1) {
        index = ((long)kdx * 10 + (long)udx) * 16;
        uStack_c = (uint)((ulong)*(undefined8 *)(&DAT_0 0100938 + index) >> 32);
        if (uStack_c == (byte)key[kdx]) {
          index1 = (int)*(undefined8 *)(&DAT_00100940 +  index);
          local_4 = (uint)((ulong)*(undefined8 *)(&DAT_00 100940 + index) >> 0x20);
          if ((byte)key[index1] != local_4) {
            return 1;
          }
        }
      }
    }
    uVar1 = 0;
  }
  return uVar1;
}

This leaves us with 2 possibilties, reverse the second for-loop or bruteforce the key. With the previous constrains we have about 500k (probably lots of duplicates included) possible keys left. At this point I was pretty hungry and wanted to take a break so I went for the second option and enjoyed some nice Gulasch at GPN:

import subprocess  
import asyncio  
  
  
def background(f):  
    def wrapped(*args, **kwargs):  
        return asyncio.get_event_loop().run_in_executor(None, f, *args, **kwargs)  
  
    return wrapped  
  
  
@background  
def bruteforce(ix):  
    x = allCombinations[ix]  
    result = subprocess.Popen(  
        "qemu-aarch64 -L /usr/aarch64-linux-gnu <pathToBinary>/fourth " + getAsInput(x),  
        shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)  
    result.communicate()[0]  
    return_code = result.returncode  
    if return_code == 0:  
        print("Success")  
        print(getAsInput(x))  
  
        exit(0)  
    if ix % 100 == 0:  
        print(ix)  
  
  
for ix in range(len(allCombinations)):  
    bruteforce(ix)

With that I'am able to test about 7k keys per minute

500k / 7k per min = 70 min worstcase

Actually the key was found around the 10 minute mark.

[...]
71800
71900
Success
UPPBKK0Y7C6B8VWY9M8O

Nice, lets bring it into the correct format:

./chal 
Enter license key> UPPBK-K0Y7C-6B8VW-Y9M8O
[CORRECT] License key validated

Decrypting product...

Welcome to GPN CTF 2024!

            ===========            
        ===================        
     -=======================-     
    ===========================    
  -===============-=============-  
  ===========::::::==============  
 =============:=========::::====== 
===============:::::::::::::=======
===========::=-:::::::::::::=======
==========::::=-::::::::::=========
=========::::::=-:-================
========::::::::=-:================
 ======::::::::::=-:============== 
  =====:::::::::::=-=============  
  -=====:::::::::::=============-  
    ===========================    
     -=======================-     
        ===================        
            ===========            


GPNCTF{W0nd3rful!_Y0u're_2_cl3ver_f0r_th4t_l1cens3_ch3ck!_W3ll_d0ne_<3}