./RE

View Original

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:

See this content in the original post

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:

See this content in the original post

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 -

See this content in the original post