Qiling Workshop
I followed the Qiling Workshop of Ph0wn Labs #3, by Blackb0x_
For environment setup, I used the Docker container provided on the GitHub link. Qiling is installed in /opt
, and there are several root filesystems in /rootfs
.
There were 2 challenges: one Linux, one Windows. Both challenges can be solved in multiple ways, for example with static analysis, but the goal of this workshop was to use Qiling to solve. This is the writeup for the Windows challenge.
Windows challenge
We have a windows.exe
crackme. I’m on Linux, it’s possible to run the binary with wine, but the goal of the workshop is to run it with Qiling.
rootfs
I start a Python script to run Qiling:
1
2
3
4
5
6
7
8
9
10
| from qiling import *
# sandbox to emulate the EXE
def my_sandbox(path, rootfs):
ql = Qiling(path, rootfs)
ql.run()
if __name__ == "__main__":
my_sandbox(["windows.exe"], "/rootfs/x86_windows")
|
This doesn’t work: it doesn’t find the executable. Even with a full path, it complains the binary needs to be under the rootfs. So I copy the binary in /rootfs/x86_windows/bin/windows.exe
so that it accepts to launch it.
1
2
3
| if __name__ == "__main__":
# execute Windows EXE under our rootfs
my_sandbox(["/rootfs/x86_windows/bin/windows.exe"], "/rootfs/x86_windows")
|
invalid memory read
Then, it compalins about an invalid memory read.
1
2
3
4
5
6
7
| [x] Hexdump:
[x] ff 25 44 0f 88 6b cc cc
[x] Disassembly:
[=] 000000006b823c20 [kernel32.dll + 0x023c20] ff 25 44 0f 88 6b jmp qword ptr [rip
+ 0x6b880f44]
...
unicorn.unicorn_py3.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
|
The error occurs at jmp qword ptr [rip + 0x6b880f44]
at 0x000000006b823c20 in kernel32.dll
. I open the dll in Radare2 and head to this address to understand what is happening:
1
2
3
4
5
| root@1cb654fb8b1f:/rootfs/x86_windows/Windows/System32# r2 ./kernel32.dll
0x6b81f8e0]> s 0x6b823c20
[0x6b823c20]> pd 10
;-- WriteConsoleA:
0x6b823c20 ff25440f886b jmp dword [sym.imp.api_ms_win_core_console_l1_1_0.dll_WriteConsoleA] ; 0x6b880f44 ; "\\x90\n"
|
THe issue is that WriteConsoleA is not mapped by Qiling.
WriteConsoleA
We need to write a hook for WriteConsoleA
.The Microsoft documentation tells us the function “writes a character string to a console screen buffer”. The prototype is the following:
1
2
3
4
5
6
7
| BOOL WINAPI WriteConsole(
_In_ HANDLE hConsoleOutput,
_In_ const VOID *lpBuffer,
_In_ DWORD nNumberOfCharsToWrite,
_Out_opt_ LPDWORD lpNumberOfCharsWritten,
_Reserved_ LPVOID lpReserved
);
|
To hook a Windows API, we need a special Python decorator which is documented in Qiling / Hijack / Windows API. We customize the decorator to match the parameters of WriteConsoleA.
1
2
3
4
5
6
7
| @winsdkapi(cc=CDECL, params={
'hConsoleOutput' : POINTER,
'lpBuffer' : POINTER,
'nNumberOfCharsToWrite': UINT,
'lpNumberOfCharsWritten' : UINT,
'lpReserved' : POINTER
})
|
We implement a basic write with a Python print
. We read the content of the lpBuffer pointer with ql.mem.read
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @winsdkapi(cc=CDECL, params={
'hConsoleOutput' : POINTER,
'lpBuffer' : POINTER,
'nNumberOfCharsToWrite': UINT,
'lpNumberOfCharsWritten' : UINT,
'lpReserved' : POINTER
})
def hook_WriteConsoleA(ql : Qiling, address: int, params):
lpBuffer = params['lpBuffer']
towrite = params['nNumberOfCharsToWrite']
written = params['lpNumberOfCharsWritten']
data = ql.mem.read(lpBuffer, towrite)
text = data.decode(errors='ignore') # decode en ASCII/UTF-8 avec gestion d'erreur
print(f"[WriteConsoleA] {text}")
return towrite
|
ReadConsole
It’s better but it’s not finished: Qiling complains this time about not having ReadConsoleA.
Its documentation is here.
We do the same.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @winsdkapi(cc=CDECL, params={
'hConsoleOutput' : HANDLE,
'lpBuffer' : LPVOID,
'nNumberOfCharsToRead': DWORD,
'lpNumberOfCharsRead' : LPDWORD,
'pInputControl' : LPVOID
})
def hook_ReadConsole(ql : Qiling, address: int, params):
lpBuffer = params['lpBuffer']
toread = params['nNumberOfCharsToRead']
lpNumberOfCharsRead = params['lpNumberOfCharsRead']
user_input = input("ReadConsole input > ")[:toread]
print(f"Read {user_input} ({len(user_input)} bytes)")
ql.mem.write(lpBuffer, user_input.encode())
if lpNumberOfCharsRead != 0:
ql.mem.write(lpNumberOfCharsRead, len(user_input).to_bytes(4, 'little'))
return 1
|
Now, the program runs successfully :) But we need to find the correct password.
Reversing the CrackMe
With r2, we see the main functionality of the crackme lies in main
. The program reads user input, applies some modification to it and compares it with ph0wn
. If it is the same, we get a congratulations message, or it’s a fail.
This is the part that modifies user input (first argument). The modification is applied 3 times with a different key (g
, !
and E
) over the same length, 0x22.
1
2
3
4
5
6
7
8
9
10
11
12
| 0x1400011d7 41b067 mov r8b, 0x67 ; 'g' ; 103
| 0x1400011da ba22000000 mov edx, 0x22 ; '\"' ; 34 ; int64_t arg2
| 0x1400011df 488d4c2468 lea rcx, [var_68h] ; int64_t arg1
| 0x1400011e4 e817feffff call decode
| 0x1400011e9 41b021 mov r8b, 0x21 ; '!' ; 33
| 0x1400011ec ba22000000 mov edx, 0x22 ; '\"' ; 34 ; int64_t arg2
| 0x1400011f1 488d4c2468 lea rcx, [var_68h] ; int64_t arg1
| 0x1400011f6 e805feffff call decode
| 0x1400011fb 41b045 mov r8b, 0x45 ; 'E' ; 69
| 0x1400011fe ba22000000 mov edx, 0x22 ; '\"' ; 34 ; int64_t arg2
| 0x140001203 488d4c2468 lea rcx, [var_68h] ; int64_t arg1
| 0x140001208 e8f3fdffff call decode
|
I use r2ai to understand the decode function with Claude Sonnet 3.5
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
| [0x140001000]> r2ai -a decompile this func
...
This function appears to be some sort of encryption/decryption routine that processes a buffer byte by byte. Here's what it does:
1. Takes three parameters:
- A pointer to a buffer (LPVOID)
- The length of the buffer (int64_t)
- A key byte (passed in r8b register)
2. For each byte in the buffer:
- Performs multiple XOR operations:
* First with 0x34 (52)
* Then with 0x24 (36)
* Finally with the provided key byte
- Does some additional arithmetic operations that might be meant to obscure the actual encryption process
3. The function includes what appears to be some anti-analysis code or additional transformations:
- Bit manipulation with counter + 4
- Complex arithmetic with constants 0x2C (44) and 0x21 (33)
Interesting observations:
1. Some of the operations appear to be redundant or might be meant to confuse analysis
2. The XOR operations with 0x34 and 0x24 are actually overwritten before being used
3. The only effective transformation on the buffer is the XOR with the key parameter
4. The final arithmetic operations with 0x2C and 0x21 don't affect the buffer contents
This might be a simple XOR encryption routine that has been obfuscated with additional meaningless operations to make analysis more difficult. The actual effective operation is just XORing each byte with the key parameter.
|
The AI tells us the program is somewhat obfuscated with junk code, but in reality it performs nothing more than an XOR.
1
2
3
4
5
| void simplified_process_buffer(uint8_t* buffer, int64_t length, uint8_t key) {
for(int i = 0; i < length; i++) {
buffer[i] ^= key;
}
}
|
Reversing the password
We know the result must be ph0wn
. We can simply crack the password by reversing the XOR operations:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #include <stdio.h>
#include <stdint.h>
void xor(uint8_t* buffer, int64_t length, uint8_t key) {
for(int i = 0; i < length; i++) {
buffer[i] ^= key;
}
}
void main(void) {
char buffer[0x23] = "ph0wn";
xor(buffer, 0x22, 0x67);
xor(buffer, 0x22, 0x21);
xor(buffer, 0x22, 0x45);
printf("buffer=%s\n", buffer);
}
|
The result is sk3tm
Checking we get congrats
We run the binary with Qiling, and enter that password. We get the expected congratulations message.
1
2
3
4
5
6
| [WriteConsoleA] Enter the password:
[=] WriteConsoleA(hConsoleOutput = 0xfffffff5, lpBuffer = 0x14001a000, nNumberOfCharsToWrite = 0x14, lpNumberOfCharsWritten = 0x1cf4c, lpReserved = 0) = 0x14
ReadConsole input > sk3tm
Read sk3tm (5 bytes)
[=] ReadConsole(hConsoleOutput = 0xfffffff6, lpBuffer = 0x80000001cf80, nNumberOfCharsToRead = 0x21, lpNumberOfCharsRead = 0x80000001cf48, pInputControl = 0) = 0x1
[WriteConsoleA] Congratulation on stage 2 !
|