ZeroDays CTF 2024 RE - 5

The fifth challenge presents a zip file named 'the_kings_secret.zip'.

Figure 1: Illustrates the 'the_kings_secret.zip' file

After extraction using 7-Zip, the contents reveal an executable (exe) file and a program database (pdb) file.

Figure 2: Depicts the contents extracted from the ZIP file

A PDB (Program Database) file is a proprietary file format used by Microsoft Visual Studio to store debugging information about a compiled program. It contains information such as function names, variable names, line numbers, and other debugging symbols. PDB files are essential for debugging applications because they provide the necessary information to map the compiled code back to its original source code during the debugging process. They allow developers to inspect variables, set breakpoints, and step through code while debugging.

Upon inspection using Detect-It-Easy, no packers are detected in the provided executable (exe) file.

Figure 3: Depicts the analysis of the executable file using Detect-It-Easy

Given the absence of packing, it is advisable to proceed directly to analysis by utilizing Binary Ninja and navigating to the main function.

In C programming, the main() function serves as the entry point for execution of a program. It is a mandatory function that must be defined in every C program. When the program is executed, the operating system invokes the main() function to begin execution. The main() function can have a return type of int, which indicates the status of the program's execution to the operating system. Typically, a return value of 0 indicates successful execution, while a non-zero value indicates an error or abnormal termination. Within the main() function, program logic is written to perform various tasks, such as initializing variables, invoking other functions, processing input, and producing output.

Upon loading the file in Binary Ninja and importing the PDB file to obtain symbol data, we navigate to the main() function, where the following code is observed:

140011f30  int32_t main(...)

140011f30  {
140011f4e      void s;
140011f4e      __builtin_memset(&s, 0xcccccccc, 0x1c8);
140011f57      int64_t rax_1 = (__security_cookie ^ &s);
140011f68      j___CheckForDebuggerJustMyCode(&__33CA3B9F_main@c);
140011f6d      char var_290 = 2;
140011f71      char var_28f = 0xe;
140011f75      char var_28e = 0x3d;
140011f79      char var_28d = 0x1f;
140011f7d      char var_28c = 0xe;
140011f81      char var_28b = 0x1b;
140011f85      char var_28a = 1;
140011f89      char var_289 = 0;
140011f8d      char var_288;
140011f8d      __builtin_memcpy(&var_288, "\x07\x06\x1e\x0f\x36\x06\x06\x73\x31\x07\x30\x1f\x1c\x17\x42\x2c\x34\x0c\x0d\x1e\x1a", 0x15);
140011fe1      char var_273 = 0x4f;
140011fe5      char var_272 = 0x46;
140011fe9      char var_271 = 0;
140011fed      char var_250 = 0xf4;
140011ff1      char var_24f = 0xf5;
140011ff5      char var_24e = 0xc5;
140011ff9      char var_24d = 0xf7;
140011ffd      char var_24c = 0xf5;
140012001      char var_24b = 0xe8;
140012005      char var_24a = 0xff;
140012009      char var_249 = 0xc5;
14001200d      char var_248 = 0xf1;
140012011      char var_247 = 0xf3;
140012015      char var_246 = 0xf6;
140012019      char var_245 = 0xf6;
14001201d      char var_244 = 0xf3;
140012021      char var_243 = 0xf4;
140012025      char var_242 = 0xfd;
140012029      char var_241 = 0x9a;
14001202d      int64_t var_220 = 0;
140012035      int32_t sPlainTextSize = 0;
140012046      j_printf("What do you know?\n");
140012052      j_printf(">");
140012077      void var_1c0;
140012077      if (fgets(&var_1c0, 0x20, __acrt_iob_func(0)) == 0)
140012077      {
14001207e          exit(1);
140012077      }
140012084      while (true)
140012084      {
140012084          int32_t rax_4 = getchar();
140012097          if (rax_4 == 0xa)
140012097          {
140012097              break;
140012097          }
1400120a0          if (rax_4 == 0xffffffff)
1400120a0          {
1400120a0              break;
1400120a0          }
1400120a0          if (!((rax_4 != 0xa && rax_4 != 0xffffffff)))
1400120a0          {
140012090              /* nop */
1400120a0          }
140012084      }
1400120ab      j_printf("What is it you want?\n");
1400120b7      j_printf(">");
1400120dc      void var_180;
1400120dc      if (fgets(&var_180, 0x10, __acrt_iob_func(0)) == 0)
1400120dc      {
1400120e3          exit(1);
1400120dc      }
1400120e9      while (true)
1400120e9      {
1400120e9          int32_t rax_7 = getchar();
1400120fc          if (rax_7 == 0xa)
1400120fc          {
1400120fc              break;
1400120fc          }
140012105          if (rax_7 == 0xffffffff)
140012105          {
140012105              break;
140012105          }
140012105          if (!((rax_7 != 0xa && rax_7 != 0xffffffff)))
140012105          {
1400120f5              /* nop */
140012105          }
1400120e9      }
140012109      int32_t var_154 = 0;
140012134      for (int32_t i = 0; i < 0x10; i = (i + 1))
140012134      {
14001215e          if ((((((uint32_t)*(uint8_t*)(&var_180 + ((int64_t)i))) ^ 0x12) ^ 0x44) ^ 0xcc) != ((uint32_t)&var_250[((int64_t)i)]))
14001215e          {
140012160              var_154 = 0;
14001216a              break;
14001215e          }
14001216e          var_154 = 1;
140012134      }
14001217a      int32_t var_114 = 0;
1400121a5      for (int32_t i_1 = 0; i_1 < 0x20; i_1 = (i_1 + 1))
1400121a5      {
1400121c2          int32_t temp0_1;
1400121c2          int32_t temp1_1;
1400121c2          temp0_1 = HIGHD(((int64_t)i_1));
1400121c2          temp1_1 = LOWD(((int64_t)i_1));
1400121c3          int32_t rdx_2 = (temp0_1 & 0xf);
1400121ef          if ((((uint32_t)*(uint8_t*)(&var_1c0 + ((int64_t)i_1))) ^ ((uint32_t)*(uint8_t*)(&var_180 + ((int64_t)(((temp1_1 + rdx_2) & 0xf) - rdx_2))))) != ((uint32_t)&var_290[((int64_t)i_1)]))
1400121ef          {
1400121f1              int32_t var_114_1 = 0;
1400121fb              break;
1400121ef          }
1400121ff          var_154 = 1;
1400121a5      }
140012212      if (var_154 == 0)
140012212      {
140012224          j_printf("You will not get what you want!\n");
14001222e          exit(1);
140012212      }
140012212      else if (j_SimpleDecryption(&CipherText, 0x30, &var_1c0, &var_180, &var_220, &sPlainTextSize) != 0)
14001226c      {
140012280          j_printf("Data: %s \n", var_220);
140012294          HeapFree(GetProcessHeap(), HEAP_NONE, var_220);
14001226c      }
1400122aa      void frame;
1400122aa      j__RTC_CheckStackVars(&frame, &data_14001c0b0);
1400122ca      return j___security_check_cookie((rax_1 ^ &s));
140011f30  }

Due to the inherent limitations of decompiling executables to source code, we encounter a reference program rather than the actual program, making it sometimes challenging to decipher and understand the code fully. However, the presence of the PDB (Program Database) file significantly simplifies the analysis in this scenario due to the debugging symbols it contains.

Between the section calling the j___CheckForDebuggerJustMyCode() function and the declaration of the variable sPlainTextSize, there are multiple memory operations involved in populating variables.

Figure 4: Illustrates the initial variable declarations observed in the main() function

Following this, the program proceeds to the first call of the fgets() function immediately after the printf() functions, displaying "What do you know?\n" where the "\n" represents the newline symbol and ">".

Figure 5: Depicts the first fgets() function encountered in the code

The fgets() function in C is used to read a line from the specified input stream and stores it into the string pointed to by str. It stops reading when either n-1 characters are read, the newline character is encountered, or the end-of-file is reached, whichever comes first. The newline character, if encountered, is included in the string copied to str. The fgets() function prototype is as follows:

char *fgets(char *str, int n, FILE *stream);

Here, str is a pointer to an array where the line read is stored, n specifies the maximum number of characters to be read (including the null terminator), and stream is a pointer to the input stream to read from (typically 'stdin' for standard input).

Revisiting the fgets() function above, the code snippet calls the fgets() function, which reads input from the standard input stream (stdin), and stores it in the buffer referenced by the variable var_1c0. The second argument, 0x20, specifies the maximum number of characters to read, which is 32 characters including the null terminator. The third argument, __acrt_iob_func(0), obtains a pointer to a FILE structure associated with stdin. The comparison checks whether fgets() returns a null pointer, indicating an error or reaching the end of the file. If fgets() returns a null pointer, indicating an error or reaching the end of the file, the program exits.

The next portion of the code represents a loop that continuously reads input characters from the standard input using the getchar() function. Inside the loop, each character read is checked against specific conditions to determine whether to continue reading or terminate the input process. Specifically, the loop checks if the character read is a newline character (0xa) or if it encounters the end-of-file indicator (0xffffffff). If either of these conditions is met, the loop breaks, terminating the input process. Additionally, there is a redundant conditional check that ensures the character read is neither a newline character nor the end-of-file indicator, although it doesn't affect the loop's behavior.

From the above analysis, it is evident that the string entered by the user is stored in the variable var_1c0. Additionally, we can infer that the maximum size of the string is 0x20 bytes, as specified in the fgets() function call. The input process terminates either when the maximum size is reached or when the newline character (Enter key) is encountered, as fgets() reads characters until it reaches the newline character or the specified maximum size.

Following the previous analysis, another fgets() function call is observed after the printf() functions, displaying the prompt "What is it you want?\n" followed by ">". This prompts the user for further input, with the same input handling characteristics as described earlier. This input is stored in var_180.

Figure 6: Depicts the second fgets() function encountered in the code

Subsequently, two loops are encountered, executing certain operations and setting the variable var_154 to either 0 or 1 based on their output.

Figure 7: Depicts the two loops utilized in validating the first and second input strings

In the first loop, each character of the second input string stored in the variable var_180, up to 0x10 characters is subjected to a series of XOR operations: first with 0x12, then with 0x44, and finally with 0xCC. The result is compared with the corresponding value in the array at var_250. If any character fails to match, indicating inequality, the check evaluates to true, setting var_154 to 0, and subsequently breaking the loop.

Upon review, it appears that the variable var_250 is initialized, but an array declaration is absent. This discrepancy is attributed to the non 1:1 decompilation of executables to source. Nonetheless, we can address this by examining the values of the 0x10 bytes immediately following the var_250 variable to determine the comparison bytes.

Figure 8: Displays the var_250 array as depicted in the decompiled code

Given the properties of the XOR operation, we can apply the series of XOR operations outlined above to the provided data to derive the expected second string corresponding to the first 0xF values. We can manually perform this operation in CyberChef to generate the desired second input string.

Figure 9: Illustrates the decoding of the expected second input string using CyberChef

As depicted above, the second desired string is generated, followed by a null character (0x00), which serves as a string terminator commonly used in C programming.

In the subsequent loop, each character of the first input string stored in the variable var_1c0 undergoes a series of XOR operations with var_180. The resultant value is then compared with the corresponding value in the array at var_290. If any character fails to match, indicating inequality, the check evaluates to true, setting var_114_1 to 0, and subsequently breaking the loop.

The value of the var_290 array can be examined by inspecting corresponding variable declarations and memory copy operations up to a size of 0x20 bytes.

Figure 10: Displays the var_290 array as depicted in the decompiled code

As performed previously, we can utilize CyberChef to manually generate the expected first string using the method described above.

Figure 11: Illustrates the decoding of the expected first input string using CyberChef

We utilize the bytes from the previous string as the XOR key instead of the string itself because the presence of the null byte (0x00) prevents it from being input as a UTF-8 string. With this approach, we generate the first expected string.

In the subsequent section of the code, the variable var_154 is checked to determine if it equals 0, which occurs if the inequality condition in the first loop is met. If variable var_154 equals 0, the program terminates with the message "You will not get what you want!\n" displayed via printf(). Conversely, if variable var_154 is not equal to 0, the program proceeds to call a function named j_SimpleDecryption() with the arguments CipherText, 0x30, the first input string, the second input string, var_220, and the sPlainTextSize variables. It then checks if the result of this function call does not equal 0. If it doesn't, the data in the var_220 variable is printed. Thus, it can be inferred that the var_220 variable likely contains the output string or the flag. In the previously provided code dump, the variable sPlainTextSize is initialized to 0, indicating that its purpose is likely to store the size of the output string or flag. These assumption is based on typical programming practices where such variables are used to track the size of data structures or strings.

Analyzing the CipherText variable reveals the following byte array:

Figure 12: Depicts the data array of the CipherText variable

To decrypt the flag, let's analyze the j_SimpleDecryption() function. On checking the function, we see it's a wrapper for another function, InstallAesEncryption().

Figure 13: Displays the function prototype of the function wrapped by the j_SimpleDecryption() function

From the provided snippet, it's evident that 0x30 represents the sCipherTextSize argument, the first input string corresponds to the pKey argument, and the second input string relates to the pIV argument, referring to the AES key and the initialization vector (IV) respectively.

Before proceeding with the decryption of the flag, it's crucial to acknowledge that while the second loop mentioned above doesn't directly set the var_154 variable to 0 if the character check fails and there is no subsequent check for the var_114_1 variable being set to 0 before initiating decryption, entering an incorrect first string would result in a faulty output rather than revealing the flag.

Utilizing the data analyzed above, we can manually generate the flag using CyberChef. Once again, the bytes are used instead of the actual strings in UTF-8 format, as non-printable characters are present in the strings generated above, forming part of the AES key and the IV.

Figure 14: Depicts the flag obtained through manual decryption using CyberChef

The acquired flag is as follows -

Flag - zerodays{no_more_dead_barbers_on_my_watch}
Saptarshi Laha

I'm a passionate Threat Intelligence Analyst based in Ireland, delving deep into the fascinating realms of Reverse Engineering and Malware Analysis. With a keen eye for dissecting malicious code and navigating Capture The Flag challenges, I guide you through virtual mazes of cryptographic puzzles and real-world malware samples, sharing insights and strategies for navigating the cybersecurity landscape.

https://BinHex.Ninja
Next
Next

ZeroDays CTF 2024 RE - 4