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.

Next, 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.

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

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.

After 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.

Figure 18: Receiving and executing the payload

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.

After 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

SHA256Details
943c1d64cda373beab24e3b1fdb715e14ce79b0f04674368e26db781cc68cea6Zip File
5a1b8fe009bfc405bd863f645f5f1112c1cf386b663da1722893ffe45c00ce24Loader (version.dll)
2886be48b8af62edd856a2605039a3341f0bb385474992308b775d1abc240f7eTorNet Loader
da6b59c1f7ed3e1986f9285a7ed4aff91c00cacd428938b67650a03af68ce7a4PureHVNC Loader
be682003b89b79d761ffebebb307a74a8ed6ca7324ffd4da185943bf2ced4dbaTorNet
5ef6d6fb0cd5ea08764e50c6b61cf2cfa441b0c1b12f52d74c0a92c28de13aa4PureHVNC

Network

AddressDetails
139.99.87[.]31:7702TorNet 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)
  1. 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. ↩︎
  2. 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. ↩︎
  3. This is determined based on the modules loaded in the process, the results of multiple WMI queries, screen size, user name, etc. ↩︎
  4. 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. ↩︎
  5. All C2 communication is encrypted using SSL. ↩︎

Share