A useful DLL-injection alert is not a match on one suspicious event. It is a correlation problem.
A process opening another process can be legitimate. Remote memory allocation can be legitimate. Even an image load can be ordinary. Confidence comes from joining several observations that describe the same actor, target, module, and time-bounded flow.
I wanted a repeatable way to test that logic without running offensive proof-of-concept code or touching a live process. The result is a small Python lab that generates synthetic telemetry and evaluates it with a deterministic detector.
The five-signal sequence
The suspicious scenario contains five ordered observations:
- A process opens a handle to a different process.
- The same actor allocates memory in that target.
- The actor writes a synthetic module path to the target region.
- The actor starts a remote thread.
- The target records the corresponding module image load.
The individual events are intentionally insufficient. A finding requires the complete ordered sequence in one correlation flow.
That distinction matters. A rule that alerts on the first event will produce noise from debuggers, accessibility tools, endpoint products, installers, and administrative utilities. A rule that waits for the full chain gives an analyst much more context, but it still produces a triage lead rather than proof of malicious behavior.
Make the correlation contract explicit
The detector enforces these invariants:
- all five actions must be present and ordered;
- every event must share the same flow identifier;
- actor PID, target PID, and module must remain consistent;
- actor and target PIDs must differ;
- every imported event must declare itself synthetic.
Extra observations may appear between required actions. Missing, reordered, or inconsistent observations break the match.
This gives the detector a contract that can be tested rather than a vague claim that it detects injection.
Test the negative cases first
The most useful experiments are the ones that should produce no finding.
Start with the full synthetic chain:
dll-injection-lab demo
Expected result: one high-severity finding across five synthetic events.
Then run the cooperative control:
dll-injection-lab demo --scenario cooperative
This scenario models an approved plug-in load inside the same fictional process. It emits user approval, a self-load request, and an image load. Because there is no cross-process chain, the detector returns zero findings.
Other useful mutations are:
- delete the remote-memory-write event;
- change one event's flow ID;
- swap the remote-thread and memory-write order;
- set actor and target to the same PID;
- change the module value halfway through the sequence.
Each mutation should make the finding disappear. Those tests are at least as important as the positive example because they expose rules that merely count keywords instead of correlating behavior.
Keep generation separate from detection
The lab writes newline-delimited JSON, so the telemetry can be inspected or edited without a special service:
dll-injection-lab simulate --scenario classic --output events.jsonl
dll-injection-lab detect --events events.jsonl --format json
That separation is useful for teaching and regression testing. The generator creates a known fixture; the detector consumes an ordinary event stream. You can remove one line, rerun the detector, and see exactly which dependency broke the match.
The same result can be emitted as JSON, Markdown, or SARIF. With --fail-on-findings, the CLI exits with status 1 when a finding exists, which makes the fixture useful as a CI test.
Guard the safety boundary in CI
A synthetic security lab can drift if later changes add host inspection or process-control code. The package therefore has a regression test that rejects prohibited imports and Windows process-memory API tokens.
The current runtime imports none of ctypes, psutil, socket, subprocess, or winreg. It does not enumerate processes, open a live process, allocate remote memory, write to another process, start a thread, load a DLL, or request administrator privileges.
That boundary is part of the test suite, not just documentation.
A repeated-event regression exposed a real bug
After publication, a reviewer suggested testing unrelated repeated events inside an otherwise valid flow. That case exposed a real defect: an earlier memory-write event for a different module could be selected first and suppress the valid chain that followed.
The root cause was correlation that grouped on the flow before selecting the ordered actions. The fix groups by flow, actor, target, and module before sequence matching. I also added regressions for reordered write/thread events and the exact stable SARIF rule ID. The suite now contains 35 passing tests.
The change is inspectable in commit 87b17ec. This is the practical reason to keep noisy and incomplete controls beside the happy path: they find correlation bugs that a single clean fixture cannot.
Try the experiment
The browser demo lets you remove events and watch the finding disappear without installing anything:
The Python implementation, fixtures, tests, and detection guide are available here:
A production detector would still need signer, path, user, integrity level, parentage, prevalence, allowlists, timing, and acquisition-quality context. This lab deliberately stops at portable correlation logic so the behavior of the rule remains easy to inspect.
Disclosure: I used AI assistance to edit this article. I reviewed the technical claims and verified the commands against the project.

Top comments (0)