Level 09 - Imphash

Analysis
When extracting the imphash.zip archive, we find the following files:
| Text Only | |
|---|---|
In service.py, it appears to contain the logic for the nc server:
It appears that the service.py script takes in a Base64-encoded PE file and runs it through the command r2 -q -c imp -e bin.relocs.apply=true file.exe. This command likely generates an out file, which is then read, displayed to the nc client, and subsequently removed from the filesystem.
The radare2 command specifically loaded the libcoreimp plugin. It seems likely that the goal is to exploit a vulnerability in this plugin to achieve remote code execution and exfiltrate the flag.
Before analyzing the plugin, we can first set up an environment by referencing the Dockerfile to install radare2 and the necessary plugin.
Here is the content of the Dockerfile:
We can set up similar environment with the following set of commands:
| Bash | |
|---|---|
Now we can begin reversing the plugin using IDA. Below is the pseudocode for r_cmd_imp_client, where the main logic of the plugin resides:
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
From the pseudocode, we can see that the function analyzes a PE file, extracts the imported libraries, concatenates them, and calculates the MD5 hash of the list. The hash is then appended to an echo string, which is further concatenated with > out, and finally executed by r_core_cmd_str, which evaluates the command.
If we can somehow control and overwrite v12 or v13 (which are adjacent in memory), we could inject and execute arbitrary r2 commands, such as !sh or cat flag.txt.
Let's focus on this for-loop:
The logic first retrieves the library and function names from the current object. It checks if the library name contains any character from .dll, .ocx, or .sys. If it does, then it calculates the length of the library name (excluding its extension due to -4) and the function name, ensuring they fit within the buffer. If the length check passes, it concatenates the lowercase library name (without extension), a dot (.), the lowercase function name, and a comma (,), then appends this string to the buffer.
Since strpbrk only checks if any character from its second argument exists in the first argument, it will meet the condition as long as we provide any character from these three strings (.dll, .ocx, or .sys). This means we can fulfill the "file extension check" very easily. If our libname is short enough (e.g., a.), it could result in a negative value. Since a. has a length of 2, subtracting 4 will give us -2. This is possible because the result is stored in v17, which is a signed integer. When v17 is -2, v14 (which was initialized to 0) is also affected. Therefore, v15[v14++] will result in negative indexing, modifying the value of v14, since it is adjacent to v15 in memory.
As a result, this part of the code will write to arbitrary memory addresses for X amount of bytes, where X depends on the length of the function name:
| C | |
|---|---|
We can test this theory by writing a simple script using lief to generate a PE file from scratch and then test the imphash plugin using pwndbg. In this example, the PE file will have a. as the libname and a repeated 206 times as the name. The number 206 has been carefully chosen (will explain soon...).
We can run the debugger with the following commands to start our debugging environment:
| Bash | |
|---|---|
In pwndbg:
From here, we can set a breakpoint at 0x00007ffff7dd760d, which corresponds to just before the execution of v15[v14++] = 46;. If we examine the current value of v14 (rbp - 0x10A0), it is:

After executing, the value of v14 is:

The v14 value increments due to the ++ operator, which increases v14 by 1. This moves v14 back by 208 bytes (0xFE - 0x2E). If we set a breakpoint at the second tolower() and continue execution, we observe that the library's function name starts writing as from 0x7fffffffbde1 onwards.

Referring to the pseudocode, after writing the entire library's function name, v14 will be incremented by v16, which is essentially the length of the function name.
The reason why I picked 206 as the length of the function name is because we will strategically stop writing as 2 bytes away from v14, as such when v15[v14++] = 44; gets executed, it will be just 1 byte away from v14. So v14 is still intact. Therefore if we add another library we can with a. as name but ' (will explain later).
I chose 206 as the length of the function name because it strategically stops writing 2 bytes away from v14. As a result, when v15[v14++] = 44; is executed, it will be just 1 byte away from v14, leaving v14 intact. Therefore, we can add another library with a. as the name and 'AAAAAA (the reason for which will be explained later).

The next library's v15[v14++] = 46; will not initially interfere with v14's value, allowing the for-loop to smoothly overwrite v14.

Still within the loop, once the LSB of v14 is overwritten with ', v14 becomes 0xff27. As a result, the subsequent A characters in the function name will begin writing into v13.

The reason we add a new library or function instead of just increasing the name length is that increasing the name would not bring us far enough back to overwrite v13 properly due to the increasing value of k. This would introduce null bytes between > out and our arbitrary command, preventing execution. By strategically stopping the buffer 2 bytes before v14 and then processing a new library's function name, k will reset and remain low enough to overwrite addresses further back.
Solution
The following script will create a PE file using lief that exploits the vulnerability and prints the base64-encoded version of the PE file:
Running the script and piping to the nc server python3 solve.py | nc chals.tisc24.ctf.sg 53719 gives us the flag.

The flag is TISC{pwn1n6_w17h_w1nd0w5_p3}.