Stage 1
In our case is very simple, so let's quickly run through it. From the import table, we can see that there's some work with file resources. I immediately thought there must be something there, so I checked it out, and sure enough, we see a base64 string. This is easy to spot because it almost always ends with a double equals sign "==".
We also see in the import table an API for working with encryption; we should immediately check what is being passed to it, and very quickly we find a string that we can assume is used for decryption. And that's exactly what happened. All we had to do was take this string and convert it into an SHA1 hash. This will be our decryption key for RC4 decryption. We take the first 5 bytes and pass them to the decryption function. Voilà, we see the standard PE file headers (MZ on the screenshot). This is our loader. The rest is a little more interesting.
Stage 2
Hancitor is a DLL embedded in malicious documents distributed via phishing emails. Infection typically occurs through a VBA macro that runs when the document is opened. Once dropped from the document, the initial packed DLL acts as an intermediate stage - it unpacks and exposes Hancitor’s main functionality. Based on collected information about the infected host, the malware decides which payload to deliver next. Hancitor then carries out the loading routine to install the final malicious content on the system.
## Technical summary
Configuration extraction: Hancitor contains an embedded configuration encrypted with RC4 using a hard-coded key. It uses the Microsoft Windows CryptoAPI to decrypt this data. The configuration includes the C2 addresses the malware will contact for further instructions.
Host profiling: The malware gathers host information to choose which payload to fetch and to generate a unique victim ID. Collected data may include OS version, IP address, domain trusts, computer name and username. For example, if the machine is joined to an Active Directory domain, conditions for deploying Cobalt Strike are considered met.
C2 communication: The victim profile is sent to the command-and-control server, which responds with a command encoded in base64 and additionally obfuscated with a single-byte XOR. The command specifies one of five available loading techniques and supplies a URL to download the next-stage malicious payload.
Payload download: Hancitor can retrieve a wide variety of payloads - from fully formed EXE or DLL files to carefully crafted shellcode. This flexibility makes it useful to different threat actors.
Malicious code execution: Whether by process injection or by dropping and executing files on disk, Hancitor can perform complex actions to ensure the downloaded malicious code runs on the victim machine.
## Host profiling
After unpacking the DLL and loading it into IDA Pro, we observed that the unpacked Hancitor sample exposes two exports that both forward to the same function - this is where static analysis begins. The malware’s workflow starts with collecting host information. The data it gathers includes the OS version, the victim’s IP address, domain names and DNS entries, the computer name and username, and whether the system is x86 or x64.
Hancitor queries network adapter details via GetAdaptersAddresses to collect MAC addresses and other network information. It then generates a unique victim identifier by XOR-ing the MACs of all connected adapters with the volume serial number of the Windows directory.
A routine called check_if_x64 determines the system architecture (x64 vs x86). After gathering all pieces, the malware builds a single concatenated string that contains the collected host data - this string is what gets sent to the C2. The mw_config_decryption call (explained elsewhere) extracts the embedded configuration, and parts of that config are included in the final host profile.
A useful detection indicator for YARA is the format string used when building the profile:
"GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d".
Finally, the characteristics collected from the host drive the payload decision logic - for example, if the machine is joined to an Active Directory domain, the loader may fetch and deploy Cobalt Strike.
## Configuration extraction
Before finishing the host profile, the loader decrypts its embedded configuration and includes it in the data sent to the C2. The decryption routine references two global variables near the start of the .data section; from how the routine’s parameters are arranged, the 8 bytes at 0x5A5010 are identified as the decryption key, immediately followed by the encrypted config blob.
Hancitor stores its configuration encrypted with RC4 and uses a hard-coded key. The sample uses the Microsoft Windows CryptoAPI to perform decryption, but the key is first run through SHA-1 - only the first five bytes of that SHA-1 digest are actually used as the RC4 key.
A control parameter signals the RC4 key size: in this sample the value indicates a 40-bit key (5 bytes), so the implementation uses the first five bytes of the hashed key for RC4 decryption.
You can reproduce the decryption statically (for example, with CyberChef). In this sample the 8-byte key {5A 3F 67 79 C1 C7 33 CE} hashes to SHA-1 {b8 06 d9 27 af cc bb f6 46 65 ba cf ff 2d 45 74 17 cc 00 69}; taking the first five bytes of that digest yields the RC4 key used to decrypt the configuration. The decrypted configuration contains the C2 addresses the loader will attempt to contact (the sample shows three C2 servers).
Later in this report we demonstrate an automated method (Python) to extract the embedded configuration programmatically.
C2 communication
Hancitor pulls the C2 URLs from its decrypted configuration and communicates with them using Wininet.dll high-level APIs. The sample uses a hard-coded User-Agent string - "Mozilla/5.0 (Windows NT 6.1; Win64; x64; Trident/7.0; rv:11.0) like Gecko" - which mimics common browser traffic.
The malware sends the assembled host profile to the server via an HTTP POST request and then awaits a matching command from the C2 based on the reported victim data. The server response contains a command that is base64-encoded and additionally obfuscated with a single-byte XOR (key 0x7A). Hancitor decodes and XORs the response before parsing it.
A decoded command has four components separated by delimiters:
A single character from the set { 'b', 'e', 'l', 'n', 'r' } indicating the action to take.
A colon : as a separator.
The URL pointing to the malicious payload to download.
A pipe | as the trailing delimiter.
Like these: http://fruciand.com/8/forum.php|http://forticheire.ru/8/forum.php|http://nentrivend.ru/8/forum.php
Executing C2 commands
After the loader retrieves and decodes the C2 response, it validates the command and routes it to the handler that will download and run the indicated payload. The URL to fetch the payload is taken from the C2 string starting at offset 3. The malware uses the first character of the command to select one of several execution branches.
There are five possible command types; one (n) is a no-op, leaving four actionable paths.
b - inject into a new svchost.exe (CREATE_SUSPENDED)
This branch creates a new svchost.exe process in a suspended state and verifies that the downloaded buffer is a valid PE (DLL or EXE) before injecting it. Injection follows the classic pattern - allocate memory in the remote process (VirtualAllocEx), write the payload (WriteProcessMemory) - but the loader then modifies the suspended thread’s context so that the EAX register points at the injected module’s OEP. By replacing the thread context, execution in the new process begins at the injected binary’s entry point.
e - execute PE inside the current process
Unlike b, this path runs the downloaded PE within the existing process rather than spawning svchost.exe. Hancitor parses the PE header to obtain ImageBase and AddressOfEntryPoint, rebuilds the import table by resolving dependencies via LoadLibraryA and GetProcAddress, and then either creates a new thread to run the payload or calls the entry directly depending on configuration flags. Reconstructing imports prevents crashes caused by unresolved dependencies.
l - shellcode execution
This branch treats the download as shellcode (no PE validation). Based on function flags, the loader either injects the shellcode into a newly created suspended svchost.exe (then creates a thread to run it) or invokes the shellcode as a function inside the current process. Because only the main thread was suspended, the malware creates a new thread in the remote process for execution.
r - drop to disk and execute
This is the only path that writes the payload to disk. Hancitor saves the downloaded binary to %TEMP% under a randomized name with the BN prefix. If the file is an EXE it is launched as a new process; if it is a DLL, the loader executes it via rundll32.exe.
Yara rules:
Stage1
Code based
rule PEHeader_Map_Alloc_Copy
{
strings:
$pat = {
55 8B EC 83 EC 30 A1 ?? ?? ?? ?? 33 C5 89 45 FC 53 56 57 8B D9
6A 00 89 5D F8 FF 15 ?? ?? ?? ?? 8B 7B 3C 03 FB 6A 40 68 00 30 00 00
89 7D F0 FF 77 50 FF 77 34 FF 15 ?? ?? ?? ?? FF 77 54 8B F0 2B 47 34
53 56 89 75 F4 89 45 E0 E8 ?? ?? ?? ?? 8B 4D F0 33 C0 0F B7 7F 14
83 C4 0C 83 C7 2C 33 DB 66 3B 41 06 73 2F
}
condition:
uint16(0) == 0x5A4D and $pat
}
Stage2
Code and string based
import "pe"
rule UA_Ipify_GUID_Format_in_rdata
{
strings:
$ua = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; Trident/7.0; rv:11.0) like Gecko" ascii wide
$ip = "http://api.ipify.org" ascii wide
$fmt64 = "GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x64)" ascii wide
$fmt32 = "GUID=%I64u&BUILD=%s&INFO=%s&EXT=%s&IP=%s&TYPE=1&WIN=%d.%d(x32)" ascii wide
condition:
uint16(0) == 0x5A4D and
for any i in (0 .. pe.number_of_sections - 1) :
( pe.sections[i].name == ".rdata" and
$ua in (pe.sections[i].raw_data_offset .. pe.sections[i].raw_data_offset + pe.sections[i].raw_data_size) and
$ip in (pe.sections[i].raw_data_offset .. pe.sections[i].raw_data_offset + pe.sections[i].raw_data_size) and
( $fmt64 in (pe.sections[i].raw_data_offset .. pe.sections[i].raw_data_offset + pe.sections[i].raw_data_size) or
$fmt32 in (pe.sections[i].raw_data_offset .. pe.sections[i].raw_data_offset + pe.sections[i].raw_data_size) )
)
}
import "pe"
rule C2_CommandDispatcher_Colon_Switch_Jumptable
{
strings:
$colon_check = { 0F BE 14 01 83 FA 3A 74 07 33 C0 E9 ?? ?? ?? ?? }
$switch_core = {
89 45 FC // mov [ebp-4], eax ; (var_4)
8B 4D FC // mov ecx, [ebp-4]
83 E9 62 // sub ecx, 62h ; 'b'
89 4D FC // mov [ebp-4], ecx
83 7D FC 10 // cmp [ebp-4], 10h
0F 87 ?? ?? ?? ?? // ja default
8B 55 FC // mov edx, [ebp-4]
0F B6 82 ?? ?? ?? ?? // movzx eax, byte ptr [edx+imm] ; selector
FF 24 85 ?? ?? ?? ?? // jmp dword ptr [eax*4 + imm] ; jumptable
}
$case_r = {
8B 4D 08 83 C1 02 51 // mov ecx,[ebp+8]; add ecx,2; push ecx
E8 ?? ?? ?? ?? // call sub_6F631EF0
83 C4 04 // add esp,4
8B 55 0C 89 02 // mov edx,[ebp+0C]; mov [edx],eax
B8 01 00 00 00 // mov eax,1
}
$case_l = {
6A 01 6A 01 // push 1; push 1
8B 45 08 83 C0 02 50 // mov eax,[ebp+8]; add eax,2; push eax
E8 ?? ?? ?? ?? // call sub_6F631F60
83 C4 0C // add esp,0Ch
8B 4D 0C 89 01 // mov ecx,[ebp+0C]; mov [ecx],eax
B8 01 00 00 00 // mov eax,1
}
$case_e = {
6A 00 // push 0
8B 55 08 83 C2 02 52 // mov edx,[ebp+8]; add edx,2; push edx
E8 ?? ?? ?? ?? // call sub_6F631E00
83 C4 08 // add esp,8
8B 4D 0C 89 01 // mov ecx,[ebp+0C]; mov [ecx],eax
B8 01 00 00 00 // mov eax,1
}
$case_b = {
8B 55 08 83 C2 02 52 // mov edx,[ebp+8]; add edx,2; push edx
E8 ?? ?? ?? ?? // call sub_6F631E80
83 C4 04 // add esp,4
8B 4D 0C 89 01 // mov ecx,[ebp+0C]; mov [ecx],eax
B8 01 00 00 00 // mov eax,1
}
condition:
uint16(0) == 0x5A4D and pe.machine == pe.MACHINE_I386 and
$colon_check and $switch_core and
@switch_core > @colon_check and (@switch_core - @colon_check) < 0x200 and
2 of ($case_b, $case_e, $case_l, $case_r)
}
Scripts:
Auto unpacking for Stage 2:
#!/usr/bin/env python3
import sys, base64, hashlib
from pathlib import Path
import pefile
from arc4 import ARC4
RESOURCE_ID = 0x67
PASSWORD = b"Just because you shot Jesse James, don\x92t make you Jesse James."
OUT_FILE = "stage2.bin"
def extract_text_resource(pe_path, res_id=RESOURCE_ID):
pe = pefile.PE(pe_path, fast_load=True)
try:
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]])
except Exception:
pass
if not hasattr(pe, "DIRECTORY_ENTRY_RESOURCE"):
raise RuntimeError("No resource directory found.")
for type_entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
tname = getattr(type_entry, "name", None) or getattr(type_entry, "id", None)
if str(tname) == "TEXT":
for id_entry in getattr(type_entry, "directory", []).entries:
iid = getattr(id_entry, "name", None) or getattr(id_entry, "id", None)
if iid == res_id:
for lang_entry in getattr(id_entry, "directory", []).entries:
off = lang_entry.data.struct.OffsetToData
size = lang_entry.data.struct.Size
fo = pe.get_offset_from_rva(off)
return pe.get_data(fo, size)
raise RuntimeError(f"TEXT resource with ID 0x{res_id:X} not found")
def derive_rc4_key_from_password(password: bytes, key_bytes=5):
h = hashlib.sha1(password).digest() # 20 bytes
return h[:key_bytes]
def main(pe_path):
pe_path = Path(pe_path)
if not pe_path.exists():
print("File not found:", pe_path); return
raw = extract_text_resource(str(pe_path), RESOURCE_ID)
print(f"Extracted TEXT resource: {len(raw)} bytes")
# strip trailing NULLs/newlines and remove whitespace (Base64 may contain newlines)
s = raw.rstrip(b"\x00")
b64txt = b"".join(s.split())
try:
bin_data = base64.b64decode(b64txt)
except Exception as e:
print("Base64 decode failed:", e)
return
print("Base64 decoded size:", len(bin_data))
# sub_401000 in binary = memcpy -> just use bin_data as ciphertext
ciphertext = bin_data
# derive RC4 key: SHA1(password) and take first 5 bytes (40-bit) as in sample
key = derive_rc4_key_from_password(PASSWORD, key_bytes=5)
print("Derived key (hex):", key.hex())
# decrypt with ARC4
rc4 = ARC4(key)
plaintext = rc4.decrypt(ciphertext)
Path(OUT_FILE).write_bytes(plaintext)
print("Decrypted saved to:", OUT_FILE)
print("Preview (first 64 bytes):", plaintext[:64])
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python decrypt_with_arc4.py <sample.exe>")
sys.exit(1)
main(sys.argv[1])
Auto extract malware config stage 2
import pefile
import hashlib
import binascii
import arc4
def extract_data_section(pe_path):
pe = pefile.PE(pe_path)
for sec in pe.sections:
if b".data" in sec.Name:
return sec.get_data()
return None
def rc4_decrypt_and_show(rc4_key_bytes, encrypted_blob):
cipher = arc4.ARC4(rc4_key_bytes)
plain = cipher.decrypt(encrypted_blob)
config = plain[:200]
try:
print(config.decode('utf-8', errors='replace'))
except Exception as e:
print(f"Failed to decode config: {e}")
print(config)
def run():
path = input("Enter file path: ")
data_sec = extract_data_section(path)
if not data_sec:
print("No .data section found.")
return
config_blob = data_sec[16:]
key_bytes = config_blob[0:8]
encrypted_data = config_blob[8:]
sha1_hash = hashlib.sha1(key_bytes).hexdigest()
rc4_key_hex = sha1_hash[0:10]
rc4_decrypt_and_show(binascii.unhexlify(rc4_key_hex), encrypted_data)
if __name__ == '__main__':
run()
HASHES:
F4C8221DA9FF96697C2B75EA17E75F7E8775E344849A32F4D6AB8F26CE417F5F - stage 1
9A6B2A199AF672934BC1DE34DD9C668BBE5106C3D6E4889CF2C8170AD4F9D2F6 - stage 2
















Top comments (0)