Unknown APT uses NetamLoader to attack a Win10 OS customized for governments in Asia
Introduction
A couple of months ago, our research revealed a highly sophisticated infection chain and a complex .NET assembly-loading framework built on XAML markup, which strongly indicates a supply-chain attack targeting a custom Windows build used exclusively by government entities in Asia.
The malicious framework has been labeled NetamLoader; at present, no attribution to any known APT group has been established.
The sample came to light in the first half of 2026, when Kaspersky Security Network (KSN) issued a malware detection alert for a .config file using AppDomainManager injection to load arbitrary .NET assemblies. The file was packaged inside a CAB archive placed in the directory that stores Windows operating system update packages.
Initial analysis of the CAB contents found no malicious code: most executables were legitimate components of the third-party patch management tool Patch My PC, and even the .NET assemblies used for the injection were Microsoft-signed. A later re-examination of the original .config file and the full loading process revealed the infection chain and framework described in this report.
Kasperskyâs products detect this threat as HEUR:Trojan.MSIL.NetamLoader.gen, Trojan.MSIL.Zapchast.*, and Trojan.Script.Agent.*.
Background
During routine threat hunting, KSN sent us a malware detection alert. The notification flagged a .config file that employed AppDomainManager injection to load arbitrary .NET assemblies. The file was compressed into a CAB archive named e4537a1762fcafd2fd96bfc825bad34f9db9a24d and saved in the folder that stores Windows operating system update packages.
Below is the full path to this archive:
C:\Windows\SoftwareDistribution\Download\e4537a1762fcafd2fd96bfc825bad34f9db9a24d
Starting with this CAB archive, we uncovered a highly complex infection chain that uses a previously unknown advanced loading framework.
Initial infection
The CAB file contains the following seven files, which are extracted and placed in the path C:\Windows\SoftwareDistribution\Download\Install\:
Except for Resources.resx, none of the files contain malicious code. We did not observe how the CAB file was delivered to the victim; we only saw the process PatchMyPC-ScriptRunner.exe running with the following arguments:
C:\Windows\SoftwareDistribution\Download\Install\PatchMyPC-ScriptRunner.exe -updatepackage -applicationid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
After the file is launched in this manner, the infection chain progresses to stage 1.
Stage 1
Starting with the execution of PatchMyPC-ScriptRunner.exe, which then served as a host process for loading several payloads, we identified a complex twoâstage loading mechanism with multiple steps. The overall flow of stage 1 is summarized in the diagram below.
Step 1
The AppDomainManager injection technique is used to load an arbitrary .NET assembly. With this mechanism, when PatchMyPC-ScriptRunner.exe runs, the accompanying configuration file PatchMyPC-ScriptRunner.exe.config in the same directory is loaded before the executableâs main function runs.
Below is the content of PatchMyPCâScriptRunner.exe.config.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
According to the configuration file, the class Microsoft.ApplicationServer.Caching.DataCacheFactoryConfiguration from the Microsoft.HostIntegration.Connectors.dll module (a Microsoftâsigned assembly) is loaded. DataCacheFactoryConfiguration is the configuration class for the Windows Server AppFabric Cache Client, specifying the settings for a new cache client.
When the configuration class is loaded, its instance first attempts to find and read the default configuration file, fabric.config, located in the same directory. The contents of fabric.config are shown below:
1
2
3
4
5
6
Step 2
The DataCacheFactoryConfiguration class has a design flaw that can be abused to load arbitrary assemblies. The fabric.config file is meticulously crafted. When the file is loaded, the DataCacheFactoryConfiguration instance examines its className attribute, loads the specified type (System.Resources.ResXResourceSet from the System.Windows.Forms assembly), and creates a ResXResourceSet instance, passing Resources.resx to its constructor for parsing.
In the .resx file, multiple layers of payloads - including several XAML files, .NET assemblies, and JSON data - are encoded, compressed, or encrypted (see the diagram below). Each layer is unpacked and loaded by the layer above it. Once a layer finishes its operation, the next layer is extracted and loaded. Below, we analyze each layer in the order in which it is loaded:
The Microsoft RESX schema defines the XML layout used by .resx files, which has a human-readable structure and can hold application resources: strings, images, icons, and serialized objects. The Resources.resx file contains a Base64âencoded binary object that was serialized using the wellâknown, highly vulnerable .NET BinaryFormatter class.
Parsing this .resx file with ResXResourceSet causes the embedded Base64âencoded binary payload to invoke BinaryFormatter.Deserialize(), triggering a deserialization vulnerability that enables a TypeConfuseDelegate attack. The TypeConfuseDelegate then invokes XamlReader.Parse(), which loads a XAML payload (named XAML-1) embedded within the binary object.
XAML is a markup language for building user interfaces in .NET applications, but its capabilities extend far beyond that. Using two builtâin objects, ObjectDataProvider and ResourceDictionary, logic equivalent to C# code can be expressed in an XMLâlike syntax.
Step 3
The XAML-1 code acts as a loader for an embedded payload. The process is as follows:
- Retrieve the full command line of the current host process
PatchMyPCâScriptRunner.exe, converting all arguments to lowercase. - Generate the AES key by hashing the commandâline string with SHAâ256.
- Decode the Base64 string embedded in XAML-1 into a binary array. Use the first 16 bytes of that array as the IV, then decrypt the remaining encrypted data with AES. The decrypted output is another piece of XAML code, which we refer to as XAML-2.
- Load XAML-2 into memory using the .NET API
System.Windows.Markup.XamlReader.Load(). - If any exception occurs during decryption, trigger
Environment.Exit(0), silently terminating the host process to prevent error messages that might reveal the attack.
Stage 2
XAMLâ2 initiates stage 2 of the infection. The diagram below shows the overall flow of this stage.
XAMLâ2 contains two embedded Base64âencoded sections. These sections are actually two additional XAML files (referred to as XAML-2-1 and XAML-2-2) that were first compressed with ZIP and then encoded in Base64. After decoding and decompressing, XAML-2 loads each file into memory separately with XamlReader.Load().
XAML-2-1
The first file consists of 796 lines. Its sole purpose is to hijack a key AMSI API to bypass antiâmalware scanning. To avoid detection by antiâmalware signature scans, sensitive strings are split into very short fragments. When XAML-2-1 is loaded and parsed, the fragments are reassembled into the original string.
The key steps it takes to achieve its goal are as follows:
- Check the OS architecture by verifying whether
IntPtr.Sizeequals 8. If it does not, the host process isnât 64âbit, so abort the hijack operation. - Combine the split strings into a single CLSID GUID (
fdb00e52-a214-4aa1-8fba-4357bb0072ec), then try to create an instance of the AMSI COM object using this GUID. - Patch the AMSI module.
- Create a delegate for the runtime method
Console.Clear()and then obtain its native function pointer. Next, patch the method by overwriting its first bytes in memory with the binary payload (41 C7 00 01 00 00 00 33 C0 C3). - Obtain the native pointer of the AMSI COM object with
Marshal.GetIUnknownForObject(). From this pointer, read the address of its virtual method table and add0x18to reach another pointer that holds the address of theCAmsiAntimalware::Scanmethod. - Use
VirtualProtect()to make theScan()pointer writable, change its value from the address of theCAmsiAntimalware::Scan()method to the address of the patchedConsole.Clear(), and then restore the pointerâs original protection attributes. Once this step is complete, every scan returnsAMSI_RESULT_CLEAN(value = 0). - Create a delegate for the runtime method One patch targets the
Console.Clear()API, which resides in the memory space of an existing legitimate module, while the other modifies only the virtual tables of the AMSI module, allowing the malware to pass code integrity checks.
XAML-2-2
XAML-2-2 consists of two parts: a .NET module (the first loader, .NET 1 Loader) and a JSON data payload (the C2 configuration, JSON-1). Both components are compressed with ZIP and then encoded in Base64. XAML-2-2 first decodes and decompresses the .NET assembly .NET 1 Loader, then uses the Assembly.CreateInstance() API to create an instance of the .NET 1 Loader and passes the Base64âencoded JSON-1 to the m.C() method of .NET 1 Loader as a parameter.
.NET 1 Loader
.NET 1 Loader decodes, decompresses, and parses JSONâ1. The decoded and decompressed JSONâ1 is shown below:
1
2
3
4
{ "p": "[Base64âencoded JSON, which we call JSONâ1-p]" "c": "[Base64âencoded JSON, which we call JSONâ1-c]" }
- JSON-1-c This JSON contains the configuration data that .NET 1 Loader uses to report the execution results of the loaded payload to the C2 server.
- sc false Flag. Determines whether to report the execution result of
m.C(). When set to false, reporting to C2 is skipped. - c null Reporting C2 server. The result returned by the
m.C()method of .NET 1 Loader is sent to this server. - tp null Unknown item, suspected to be a trusted SSL certificate. Thumbprint was used to verify this alleged C2 certificate.
- e null The AES key that encrypts the JSON result returned by the
m.C()method of .NET 1 Loader.
- sc false Flag. Determines whether to report the execution result of
- JSON-1-p JSON-1-p is a more complex configuration file. .NET 1 Loader parses its data to prepare and load the subsequent stages. The following configuration keys store the payloads:
- oib : Base64âencoded .NET 2 Plugin (PE file)
- oub : Base64âencoded XAML that invokes
Exit(0) - ibk : AES key used to decrypt
ibi - ibi : A Base64âencoded collection of compressed, encrypted XAML templates used to build XAML-3. Detailed instructions for decrypting and assembling XAMLâ3 are provided in the XAMLâ3 section below.
- Other items are passed as C2 configuration to the .NET 2 Plugin at a later stage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[{"c": [{"cu": "https://openaigpt4v.com/model_v4/","tp": "*","rh": ""}],"mib": 79200,"mab": 100800,"mis": 1200,"mas": 2400,"e": "9fT04e03tAiZAPogcDWt0w==","ii": null,"aop": null,"oib": "TVqQAAMAAAAEAAAA//8AALgAAAAAA...","oub": "PFJlc291cmNlRGljdGlvbmFyeS...","wi": 100801,"ibk": "W7BHmIRBRY3FOIfWprbW/ZJh5GbPND3sY+qCBDY7O2A=","ibi": "4pUDtQVbD6leQrDvtT3gaaCLec...","de": false}]
After obtaining the configuration, .NET 1 Loader proceeds with the following key steps:
- Delete the update records from the system log related to the malicious update packages, along with the malicious files and cache files used in stage 1.
- Delete malicious files extracted from the CAB
1
2
3
PatchMyPC-ScriptRunner.exe.config
fabric.config
Resources.resx
- Delete CLR usage logs and Windows system Prefetch cache files to eliminate evidence that could be used in forensic analysis.
1
2
3
%LOCALAPPDATA%\Microsoft\CLR_v4.0\UsageLogs\PatchMyPC-ScriptRunner.exe.log
%WINDIR%\Prefetch\PATCHMYPC-SCRIPTRUNNER.EXE-*.pf
- Retrieve the files in the folders matching the pattern
20????at the path below, where each file name starts with â20â followed by six more characters (e.g., 20260101). Read the filesâ contents, filter out any lines containing the exact substringGetExtendedUpdateInfoHandler, and write the filtered content back to the log files. The path contains the keywordCmitUpdateAgent, which may be linked to a custom Windows 10 build used by a government in Asia.
"%WINDIR%\Logs\CmitUpdateAgent\20????\20??????"
- Delete malicious files extracted from the CAB
- Reset a registryâbased exceptionâstate logging system by deleting the nine GUID keys under the path
LocalMachine\SOFTWARE\Classes\appid\[specific_GUID]one at a time, along with all of their child subkeys. The algorithm that generates the GUID path is unique- it derives the GUID from the C2 URL- so each C2 server has its own distinct set of bound GUIDs. Of the nine GUIDs derived using this method, one indicates that an exception occurred, and the others are numbered 0 through 7. For example, when the malware catches exception code 129, it creates the registry subkey{bde7ddc2-1f6f-4397-a0df-ae7c7d67d65a}to indicate that an exception occurred. It then checks the bit mask, where 129 equals binary 10000001. Consequently, it creates the eighth subkey{74084115-433e-4fe8-8d01-41c48fbfeef6}and the first subkey{bf6f4bde-97db-4dcd-9546-7ebf10061519}. - Verify that each necessary configuration item exists in JSON-1-p, then validate some of its items against hardcoded values. If any item fails the check, halt the infection chain and submit the execution result to the reporting C2 server.
- Verify that the current user is an administrator; if not, throw an exception with code 128.
- Check whether the configuration item
aopexists. If it does, Base64-decode its value, use the result as XAML-3, and skip to step 9. - Generate a unique malware instance ID for the configuration item
iiby callingGuid.NewGuid(). - Query the
SusClientIdvalue from the registry keySOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate. Then generate the value for the newly defined configuration itemsiby computing a GUID using the same algorithm as above. Use the concatenation of the retrievedSusClientIdvalue and the byte sequence\x14\x43\x9B\x64as the input data for the algorithm.
Comments
No comments yet. Start the discussion.