2024

lambda (easy)

Downloading the file given by the platform, we face with this:

import sys

sys.setrecursionlimit(10000000)

(lambda _0: _0(input))(lambda _1: (lambda _2: _2('Enter the flag: '))(lambda _3: (lambda _4: _4(_1(_3)))(lambda _5: (lambda _6: _6(''.join))(lambda _7: (lambda _8: _8(lambda _9: _7((chr(ord(c) + 12) for c in _9))))(lambda _10: (lambda _11: _11(''.join))(lambda _12: (lambda _13: _13((chr(ord(c) - 3) for c in _10(_5))))(lambda _14: (lambda _15: _15(_12(_14)))(lambda _16: (lambda _17: _17(''.join))(lambda _18: (lambda _19: _19(lambda _20: _18((chr(123 ^ ord(c)) for c in _20))))(lambda _21: (lambda _22: _22(''.join))(lambda _23: (lambda _24: _24((_21(c) for c in _16)))(lambda _25: (lambda _26: _26(_23(_25)))(lambda _27: (lambda _28: _28('16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r'))(lambda _29: (lambda _30: _30(''.join))(lambda _31: (lambda _32: _32((chr(int(c,36) + 10) for c in _29.split('_'))))(lambda _33: (lambda _34: _34(_31(_33)))(lambda _35: (lambda _36: _36(lambda _37: lambda _38: _37 == _38))(lambda _39: (lambda _40: _40(print))(lambda _41: (lambda _42: _42(_39))(lambda _43: (lambda _44: _44(_27))(lambda _45: (lambda _46: _46(_43(_45)))(lambda _47: (lambda _48: _48(_35))(lambda _49: (lambda _50: _50(_47(_49)))(lambda _51: (lambda _52: _52('Correct FLAG!'))(lambda _53: (lambda _54: _54('Incorrect'))(lambda _55: (lambda _56: _56(_41(_53 if _51 else _55)))(lambda _57: lambda _58: _58)))))))))))))))))))))))))))

Pretty unreadable, but we can get a better readability passing this code to an online source code formatter.

We can get a slight better readability of the code, which makes more easy to understand what it is doing and how we can do the reverse operation of the program.

Reading it, we can note that it is xoring our input, adding 12 and subtracting 3, we can do the reverse operation with this simple line of code and retrieve the original flag.

''.join(chr(((int(i, 36) + 10) ^ 123) - 12 + 3) for i in "16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r".split('_'))

home (normal)

The CTF gives a linux ELF binary to us.

Analysing it with Binary Ninja, we can discover some interesting behaviours.

First, the app checks if the current working directory is equal to "Service", if not, it doesn't execute. By second, it have a debugger checking with ptrace. If it detects that we are running the program with a debugger, it stops the application.

Finally, if all the preconditions succeeds, the program calls the constructFlag() function.

So, in order to run this program with a debugger attached to it we have two options:

  1. Patch the program to ignore the ptrace verification.

  2. Hijack the ptrace function in order to deliver fakes returned values.

I took the first option, but I didn't patched the file. We can see in the assembly code that the verification is made by verifying if the returned value of ptrace (rax register in x86 calling conventions) is different than 0xffffffffffffffff. If yes, it executes the constructFlag function, if not, it aborts the execution.

We can automize the process of setting rax register to 0 (or any other value) with GDB script files. I use GEF to work with PIE enabled but you can also do this with vanilla GDB.

pie b 0x19f3  # address of comparison
pie run
set $rax = 0

Executing the program with gdb ./chal_home -x chall.gdb, we can successfully bypass the ptrace verification.

Opening the constructFlag function on the disassembler, we can see that it has a lot of if elses that are hard to analyse.

By the very start of the program, we can note that it copying is 176 bytes from the &data_2010 address to the &var_118 (rbx - 0x118).

rdx = memcpy(&var_118, &data_2010, 176);

Checking for uses of the var_118 variable, we can get couple definitions on what seems an array item definition.

0000173e                                              *(uint8_t*)(&var_68 + ((int64_t)var_124)) = (((int8_t)*(uint32_t*)(&var_118 + (((int64_t)var_124) << 2))) + (0 - var_124));

[...]

00001600                                                  *(uint32_t*)(&var_118 + (((int64_t)var_120) << 2)) = (((rdx_3 ^ 0xffffffff) & 0x19f) | (rdx_3 & 0xfffffe60));

When I saw this, the first thing I imagined was to set a breakpoint on the end of the constructFlag and analyse the strings that come after the rbx-0x118, doing that, surprising, we can get the flag.

gef➤  x/360s $rbp-0x118  
0x7fffffffdbe8: ","  
0x7fffffffdbea: ""  
0x7fffffffdbeb: ""  
0x7fffffffdbec: ","  
0x7fffffffdbee: ""  
0x7fffffffdbef: ""  

[...]

0x7fffffffdc96: ""  
0x7fffffffdc97: ""  
0x7fffffffdc98: "\247"  
0x7fffffffdc9a: ""  
0x7fffffffdc9b: ""  
0x7fffffffdc9c: "+"  
0x7fffffffdc9e: ""  
0x7fffffffdc9f: ""  
0x7fffffffdca0: "FLAG{How_did_you_get_here_4VKzTLibQmPaBZY4}"

thread (hard)

I had some trouble when trying to reverse engineering those ELFs programs with Binary Ninja, so in the mid of the competition I switched to IDA free.

After disassemble the code and rename some variables, I could get this result:

We can see that the program doesn't do any kind of verification to check how you are running it (implementing anti-debugging or running conditions).

At the firsts lines of the pseudo code, we can note that it is getting 45 bytes from the user input, checking if the length of the data is exactly 45 characters and start to fill the array input_data with the user input.

On the second loop at the 21st line, we can see that it is setting the indexth indexes array item with index and starting a thread pool sending a pointer to that index to the start_routine function. Let's take a look on the last mentioned method.

This is the core of the challenge. It starts dereferencing that index pointer straight before starting a while loop (also can be interpreted as a for loop).

Within the loop, the program do a module operation with the index_dereferenced item of the zeros array (it is just a data array with lot of zeroes) plus the index dereferenced with 3.

After it, the program do different operations depending on the result of the module calculation and stores the value inside the input_data array. In the end off the loop, it adds 1 in the index_dereferenced item of the zeros array and restart the loop, and the loop will not execute if this value got greater than 2.

Each calculation can be simplified as:

  • module == 0 {((byte ^ 0x7f) + 5) * 3}

  • module == 1 {((byte * 3) ^ 0x7f) + 5}

  • module == 2 {((byte + 5 ) * 3) ^ 0x7f}

Going back to the main function, after the program encrypt every character of the user input, the program do a last loop to check if the result of the encrypted user content is equal to the encrypted flag stored in the program.

for ( m = 0; m <= 44; ++m ) {
	if ( input_data[m] != enc_flag[m] ) {
	  puts("Incorrect.");
	  return 1LL;
	}
  }
puts("Correct!");
  return 0LL;
}

I don't like using IDA to retrieve data from binaries, so I used Binary Ninja and extracted the contents of the enc_flag as 4 bit integers little-endian.

Doing the reverse operation of all the mathematics involved on the encryption of the program, we can create this simple python code:

enc_flag = [  
       0x000000a8, 0x0000008a, 0x000000bf, 0x000000a5,  
       0x000002fd, 0x00000059, 0x000000de, 0x00000024,  
       0x00000065, 0x0000010f, 0x000000de, 0x00000023,  
       0x0000015d, 0x00000042, 0x0000002c, 0x000000de,  
       0x00000009, 0x00000065, 0x000000de, 0x00000051,  
       0x000000ef, 0x0000013f, 0x00000024, 0x00000053,  
       0x0000015d, 0x00000048, 0x00000053, 0x000000de,  
       0x00000009, 0x00000053, 0x0000014b, 0x00000024,  
       0x00000065, 0x000000de, 0x00000036, 0x00000053,  
       0x0000015d, 0x00000012, 0x0000004a, 0x00000124,  
       0x0000003f, 0x0000005f, 0x0000014e, 0x000000d5,  
       0x0000000b  
]  
  
for i, byte in enumerate(enc_flag):  
   module = i % 3  
   if module == 0:  
       dec_byte = ((byte ^ 0x7f) - 5) // 3  
   elif module == 1:  
       dec_byte = ((byte // 3) ^ 0x7f) - 5  
   elif module == 2:  
       dec_byte = ((byte - 5 ) // 3) ^ 0x7f  
  
   print(chr(dec_byte), end='')  
  
print()

Executing the script, we can retrieve the flag.

gates (normal)

As usual, opening the file on IDA we can get a initial overview of the main function.

The screenshot has some renamed variables that didn't come as symbols with the binary.

On the start, we can note that it is performing a for loop until the v3 variable turns equal to the end of the user_input variable. These variables are pointers and if we divide where user_input ends (512) with the number that is begin used as incrementer (16), we can get the value 32, which is the size of the user input.

After this, it calls the function sub_1220 which both Binary Ninja and IDA Free couldn't decompile with perfection.

This function is extremely tedious to reverse because it contains a lot of conditions and mathematical operations to encrypt the user input. When I saw this I immediately opened to see how the data that is encrypted looks like.

Going back to the main function to search an interesting offset that the program access those files, I could find the file offsect 0x10e3 which loads the encrypted user input (unk_4E4D) and also loads the encrypted flag from the data segment to the memory.

Setting a breakpoint on this offset with GDB, printing both values and running with a user input of 32 "A", we can get this:

Running the application twice with the same configuration results in the same values.

Knowing this, I could try to replace somes "As" with "Bs" to see the application behaviour. The input I will be sending is BAAAABAABAAAABAAAAAABAAAABAAABAA.

We can see a side by side comparison of the results. On the left, it is the input begin all with "As" and on the right side it is the mixed input with "B" characters.

We can see that only where we put a letter be is modified. Therefor, the encryption algorithm doesn't seems to mix the values together to build a more stronger ciphertext.

So, as the program doesn't mixes the contents, we can tell that if we send 32 sequences of the same byte, we will get the encrypted version of that byte in each space of the array.

With that in mind, we can automate the GDB to loop through all the printable characters, send it to the program, dump the program and store a database with all those encrypted chars. Finally, we can loop through all the indexes of the encrypted flag and try to search on our database which character in that index would produce the encrypted byte.

Applying this methodology with Python, I made this program:

import gdb  
import string  
  
def dump_chars(register: str) -> str:  
   enc_data = gdb.execute(f"x/64gc ${register}", to_string=True).strip()  
   enc_data = enc_data.split('\n')  
   enc_data = [data.split(':')[-1].split('\t')[1] for data in enc_data]  
   enc_data = [int(data, 16) for data in enc_data]  
  
   return enc_data  
  
enc_flag = [  
       0x3b, 0x09, 0xe5, 0xae, 0x3e, 0xf1, 0x37, 0x81, 0xfc, 0xa1, 0x99, 0xae, 0xf7, 0x62, 0x7d, 0  
xf7,  
       0xd0, 0xcb, 0xa2, 0x18, 0xcd, 0x3e, 0x89, 0x0d, 0xd9, 0xdd, 0x62, 0x29, 0x8c, 0xf3, 0x01, 0  
xec  
]  
  
# setuping  
gdb.execute("gef config context.enable 0")  
gdb.execute("pie b 0x10e3")  
  
charset = {}  
  
for i, char in enumerate(string.printable):  
   with open("./input", 'wt') as f:  
       f.write(char * 32)  
          
   gdb.execute("pie run < ./input")  
  
   enc_chars = dump_chars("rax") # got encrypted chars  
   charset[char] = enc_chars  
  
   gdb.execute("kill")  
      
flag = ""  
for i, char in enumerate(enc_flag):  
   found = False  
  
   for option in charset:  
       if charset[option][i] == char:  
           flag += option  
           found = True  
           break  
  
   if found:  
       continue  
  
print(flag)

Executing it with the -x GDB flag, we get the flag with success.

Last updated