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 a
s 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}
.