New Loader Executing TorNet and PureHVNC
Around May 2025, a ZIP file containing multiple files, including a newly discovered malware loader, was uploaded to VirusTotal. This loader had several characteristics not often seen in other malware loaders, such as its ability to execute two malware families (TorNet and PureHVNC) and its implementation of API hashing with MurmurHash2. In this article, we will share the information gained from analyzing this loader.
ZIP Compressed Files
The ZIP file contained the following items.

All files and folders in the ZIP file, except for the legitimate executable, had hidden and system file attributes. Since hidden and system files are not displayed in Windows Explorer by default, only the legitimate executable file is visible in the extracted folder from the perspective of a victim user.

When the legitimate executable is run, it will load a malicious DLL file called version.dll prepared by the attacker through DLL sideloading1. The original (legitimate) version.dll has been renamed to npHPReader64.dll. Malicious version.dll will forward the requested procedure from legitimate executable to npHPReader64.dll. However, the malicious version.dll also has a loader functionality that injects and executes multiple malware (TorNet loader and PureHVNC loader) into other processes.
Persistence
Once the loader is executed, it registers the automatic execution setting in the registry to achieve malware persistence2. First, it creates a npHPReader64 folder under the %LOCALAPPDATA% folder.

npHPReader64 folderNext, the loader will copy all the files required to execute itself to the created folder.

Finally, the loader writes a value to the registry key to achieve persistence.
- Key:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run - Value:
npHPReader64 - Data: Path of the copied legitimate executable file

Between API calls, the loader calls function with meaningless code that does not store return value to EAX register. This appears to be an obfuscation method that is intended to break the decompilation result. However, it can be resolved by patching the corresponding function call to NOP instruction.
API Hashing using MurmurHash2
In the following procedure, loader calls the Windows API functions that have been dynamically resolved using API hashing. API hashing dynamically resolves the address of an API function from a hashed function name and DLL name, generated using a chosen algorithm.

This loader uses an algorithm called MurmurHash2, with seed value 0xB801FCDA, to hash API function and DLL file name. API hashing using MurmurHash2 is also implemented in well-known infostealer malware “LummaStealer”, but its implementation in this type of loader had not been observed before.

The constant value 0x5BD1E995 is magic number of MurmurHash2. Hash value will be calculated from magic number, seed value, and string to be hashed. For example, hash value of LoadLibraryA will be 0x439C7E33.
We developed an IDAPython script that identifies API function from hashed value used in this loader. Check Appendix B for it.

Decrypt and Decompress Payloads
The payloads to be executed by the loader are stored within it, but it is LZMA compressed and AES-128-ECB encrypted. After creating persistence setting, loader will decrypt and decompress payloads to execute them.

As mentioned at the beginning of this article, this loader executes two different payloads, but each of them was encrypted with different AES keys. The AES key for each payload is as follows:
- Payload 1 (TorNet Loader):
37C1FF3236DD4989153CCAC2CA712192 - Payload 2 (PureHVNC Loader):
6CB15D6A5C9AB4C2B2885FF35836892A

The SHA-256 hash of each payload is shown in Appendix A.
Code Injection
Once payloads are decrypted and decompressed, loader calls CreateProcessA function to create C:\Windows\Microsoft.NET\Framework\v4.0.30319\jsc.exe process in a suspended state.

CreateProcessA functionAfter that, the loader writes the payloads to virtual memory of created process using VirtualAllocEx and WriteProcessMemory function.

Finally, the loader calls functions such as SetThreadContext and ResumeThread to execute payloads in context of created process.

Simple loader developed using .NET Framework
The payloads injected into jsc.exe were simple loaders developed using .NET Framework. In that loader, next-stage payload (TorNet or PureHVNC) is stored in AES-256-CBC encrypted and GZip compressed format within an array object.

Decrypted and decompressed payload will be loaded and executed using Assembly.Load and Delegate.CreateDelegate function.

TorNet
TorNet is a downloader malware that communicates with malicious server via the TOR (The Onion Router) network.
Once TorNet is executed, it first Base64 decodes and deserialize the hardcoded configuration data. It uses Protocol Buffers for deserialization. In this configuration data, information such as the address and port number of the remote server is included.

Next, TorNet will check whether the running system is a sandbox environment3. After that, TorNet will download TOR to temporary folder and execute it.

TOR will host SOCKS proxy on port 9050 to allow applications to communicate via the TOR network. TorNet will use this SOCKS proxy to communicate with remote server and receive .NET assembly that is DES3-ECB encrypted, and GZip compressed. After decrypting and decompressing the payload, TorNet will load the received .NET assembly.

PureHVNC
PureHVNC is a commercial RAT that allows attackers to access the infected machine remotely4.
Once PureHVNC is executed, it first Base64 decodes, GZip decompresses, and deserialize the hardcoded configuration data. Similar to TorNet, PureHVNC uses Protocol Buffers for deserialization. In this configuration data, information such as the address, port number, and certificate of the C2 server is included.

Next, depending on hardcoded parameter, PureHVNC will create persistence setting. This is done by registering scheduled task via PowerShell, but in this case, the feature was disabled, probably because the persistence setting had been implemented at the first stage loader.

PureHVNC will also call SetThreadExecutionState function with argument 0x80000003 (ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED) depending on the hardcoded parameter. In this case, the feature was also disabled, but it likely exists to prevent the machine from entering sleep mode.

SetThreadExecutionState functionAfter all the initialization procedure, PureHVNC will perform initial communication with the C2 server5. This communication includes various information about infected system, which it collects using API functions and WMI queries.

Finally, PureHVNC will receive GZip compressed payload (PureHVNC module) from C2 server and execute it. At this point, PureHVNC will first receive 4 bytes of data (payload size), allocate a buffer for the payload size, and then receive the entire payload. This is the same procedure that TorNet uses when receiving payloads. Since there are also other similarities, TorNet and PureHVNC might have been developed by the same author.

According to the report from FortiGuard Labs, PureHVNC was storing the received payload to registry until version 4.1.0. This feature had been removed at least since version 4.1.9 , since we could not find any relevant code of it. This change was likely made to prevent payload preservation through actions like digital forensic investigation.
Reference: PureHVNC Deployed via Python Multi-stage Loader – FortiGuard Labs
Conclusion
While the loader’s core functionality is not novel, we did find some interesting implementations, such as API hashing using MurmurHash2 and the deployment of two different malware families (TorNet and PureHVNC) by one loader. Given the possibility that these techniques may be used in future attacks, continued vigilance is necessary.
Appendix A: IoCs ↩︎
Files
| SHA256 | Details |
|---|---|
| 943c1d64cda373beab24e3b1fdb715e14ce79b0f04674368e26db781cc68cea6 | Zip File |
| 5a1b8fe009bfc405bd863f645f5f1112c1cf386b663da1722893ffe45c00ce24 | Loader (version.dll) |
| 2886be48b8af62edd856a2605039a3341f0bb385474992308b775d1abc240f7e | TorNet Loader |
| da6b59c1f7ed3e1986f9285a7ed4aff91c00cacd428938b67650a03af68ce7a4 | PureHVNC Loader |
| be682003b89b79d761ffebebb307a74a8ed6ca7324ffd4da185943bf2ced4dba | TorNet |
| 5ef6d6fb0cd5ea08764e50c6b61cf2cfa441b0c1b12f52d74c0a92c28de13aa4 | PureHVNC |
Network
| Address | Details |
|---|---|
| 139.99.87[.]31:7702 | TorNet Remote Server |
| 139.99.85[.]213:{56001, 56002, 56003} | PureHVNC C2 Server |
Appendix B: API Hashing Resolver Script ↩︎
The following script uses Unicorn and IDAPython API to identify API functions called from hash values in the loader. Please use it for analyzing similar samples.
from unicorn import *
from unicorn.x86_const import *
import idaapi
import idautils
import ida_funcs
import pefile
import struct
API_RESOLVER_FN = 0x10067C3A
HASHCODE_START = 0x1007030D
HASHCODE_END = 0x1007051E
ENUM_NAME = 'APIHASH'
def calculate_hash(string, seed=0xB801FCDA):
string = string.lower()
code = ida_bytes.get_bytes(HASHCODE_START, HASHCODE_END - HASHCODE_START)
uc = Uc(UC_ARCH_X86, UC_MODE_32)
stack_base = 0x00100000
stack_size = 0x00100000
EBP = stack_base + (stack_size // 2)
uc.mem_map(stack_base, stack_size)
uc.mem_write(stack_base, b"\x00" * stack_size)
uc.reg_write(UC_X86_REG_EDI, len(string))
uc.reg_write(UC_X86_REG_ESI, len(string))
len_bytes = struct.pack('<I', len(string))
uc.mem_write(EBP + 0xC, len_bytes)
seed_bytes = struct.pack('<I', seed)
uc.mem_write(EBP + 0x10, seed_bytes)
uc.mem_write(EBP - 0x48, string)
uc.reg_write(UC_X86_REG_EBP, EBP)
code_base = 0x00200000
code_size = 0x00100000
uc.mem_map(code_base, code_size, UC_PROT_ALL)
uc.mem_write(code_base, b"\x00" * code_size)
uc.mem_write(code_base, code)
code_end = code_base + len(code)
uc.emu_start(code_base, code_end, timeout=0, count=0)
return uc.reg_read(UC_X86_REG_EAX)
def main():
# Calculate hash value of API functions
api_dict = {}
for dll in ['kernel32.dll', 'ntdll.dll']:
try:
pe = pefile.PE('C:\\Windows\\System32\\' + dll)
api_list = [e.name for e in pe.DIRECTORY_ENTRY_EXPORT.symbols]
api_list = [api for api in api_list if api != None]
except (AttributeError, pefile.PEFormatError):
continue
for api in api_list:
api_dict[calculate_hash(api)] = api
# Create enum type for API hash
enum = idc.get_enum(ENUM_NAME)
if enum == idc.BADADDR:
enum = idc.add_enum(idaapi.BADNODE, ENUM_NAME, idaapi.hex_flag())
# Collect used API hash values
for xref in idautils.XrefsTo(API_RESOLVER_FN):
ea = xref.frm
push_cnt = 0
while ea != idc.BADADDR:
if idc.print_insn_mnem(ea) == 'push':
push_cnt += 1
if push_cnt == 3:
hash_value = idc.get_operand_value(ea, 0) & 0xFFFFFFFF
break
ea = idc.prev_head(ea)
# Print API hashing resolution result
if hash_value not in api_dict:
print(f'[-] Failed: {hex(hash_value)} used at {hex(xref.frm)}')
else:
print(f'[+] Resolved: {hex(hash_value)} ---> {api_dict[hash_value]} used at {hex(xref.frm)}')
# Add enum member and apply it
enum_value = idc.get_enum_member(enum, hash_value, 0, 0)
if enum_value == -1:
idc.add_enum_member(enum, ENUM_NAME + "_" + api_dict[hash_value].decode(), hash_value)
idc.op_enum(ea, 0, enum, 0)
if __name__ == '__main__':
main()Code language: Python (python)
- An attack method that exploits the search order of DLL files in Windows to trick applications into loading malicious DLL files by misidentifying them as legitimate DLL files. ↩︎
- Settings that automatically execute malware when the device starts up, so that the attacker’s access is maintained even if the infected device is restarted. ↩︎
- This is determined based on the modules loaded in the process, the results of multiple WMI queries, screen size, user name, etc. ↩︎
- Analyzed sample only had downloader functionality, similar to TorNet, but based on information from previously observed attack campaigns, it is highly likely that it is a modular RAT. ↩︎
- All C2 communication is encrypted using SSL. ↩︎
