aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAnyUnderstanding2024-06-03 22:28:31 +0200
committerGitHub2024-06-03 22:28:31 +0200
commitbcaae7797b1f2d1990b0a8ff5c7117d7362163a0 (patch)
tree00938c611efb7bd1432091133f88bbeb0ed9846f
parent4dd35a32edc18394e54ad9f116c21ea268bef8ab (diff)
Create archventuretime.md (WIP)
-rw-r--r--archventuretime.md840
1 files changed, 840 insertions, 0 deletions
diff --git a/archventuretime.md b/archventuretime.md
new file mode 100644
index 0000000..1145063
--- /dev/null
+++ b/archventuretime.md
@@ -0,0 +1,840 @@
+# Archventuretime
+## TODO -> still WIP
+Archventuretime is a reversing challenge, where you have to find obtain a license-key.
+
+## 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.
+```c
+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 commands:
+```c
+
+void FUN_001014a9(char *key) {
+ size_t sVar1;
+ ushort **ppuVar2;
+ int local_c;
+
+ // ensure that the key is greater than 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:
+
+```c
+
+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:
+```c
+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;
+ /* commandPrefix = qemu-riscv64 -L /usr/riscv64-linux-gnu */
+ 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 /usr/riscv64-linux-gnu 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:
+```c
+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:
+```c
+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):
+```c
+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 baisc quicksort implementation. So the binary just checks if the inputed liscence-key consists of the same chars as the corret 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:
+```c
+
+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:
+// TODO = I made a mistake here somwhere with the indices
+06788 -> sumNumeric = 0 + 6 + 7 + 8, sumUppercase = 0
+9BBCK -> sumNumeric = 9, sumUppercase = B + B + C + K = 1 + 1 + 2 + 10
+KMOPP -> sumNumeric = 0, sumUppercase = 11 + 13 + 14 + 15
+UVWYY -> sumNumeric = 0, sumUppercase = 20 + 21 + 22 + 24 + 34
+```
+Then we take those sums and check each against a constant that we can obtain from the binary. With that we can write a script, which given all possible letters (from the first binary) puts out every possible key (Note that this script puts out some duplicates, but this won't be a problem):
+```py
+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
+```py
+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)
+```
+```
+[('P', 'W', 'Y'), ('M', 'Y'), ('B', 'C', 'U', 'V'), ('B', 'K', 'K', 'O', 'P')]
+[(), ('7',), ('6', '8'), ('8', '9')]
+```
+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)
+```c
+
+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):
+```c
+
+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:
+```py
+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:
+```py
+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)
+```c
+
+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 with have about 500k of of possible keys left (including duplicatess). 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:
+```py
+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}
+```