Skip to content

Getting started

This is the whole loop: generate a payload, write the YAML, pack it, run it and measure the result.

  • rustc on PATH — the stub is compiled with bare rustc, not cargo. When it is missing the packer stops with instructions.
  • Workspace dependencies built once (cargo build --release at the repo root), so the stub can link the .rlib files under target/.
  • Windows. The stub is built for arch: x64 or arch: x86 (the payload must match). On Linux/WSL it cross-compiles to the matching -pc-windows-gnu target (see How it fits together); to build with no local toolchain use the Docker workflow in the repository README.md.

Any x64 PE image, DLL or raw shellcode blob works. With Metasploit’s msfvenom:

Terminal window
msfvenom -p windows/x64/exec CMD=calc.exe -f exe -o payload.exe # a PE image
msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o payload.bin # raw shellcode

Do not add an msfvenom encoder (-e): picaro encrypts the payload with XChaCha20-Poly1305 and obfuscates the stub’s strings, so an extra encoder only makes the input larger.

A build is one schema-6 YAML document. Strict by design: unknown keys are an error, so a typo never silently does nothing.

schema: 6
name: 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 }
schema: 6
name: demo
build:
crypto:
algo: xchacha20poly1305
key: random
payloads:
beacon:
source: ./payload.bin
format: shellcode
output:
path: ./dist/demo.exe
arch: x64
strip: true
trim_paths: all
runtime:
- technique: reflective_loading
params: { payload: beacon }

Save it as payload.yaml. Input and output paths are resolved relative to the directory you run picaro from (the repo’s examples assume the repo root).

The blocks:

  • build.crypto — the default encryption for every payload (a payload may override it with its own crypto block).
  • build.payloads — the named payloads an Execution step can load; source is a local path, file://, or an http(s):// URL fetched in memory.
  • build.stub — optional: a custom stub template, the syscall layer and string obfuscation. Omitted here, so the builtin stub and the defaults apply.
  • build.output — where the packed PE lands and how it is finalised.
  • runtime — the ordered steps: Preparation and Control techniques plus the Execution techniques that run the payloads (see Runtime steps).

Choosing the format and the execution technique

Section titled “Choosing the format and the execution technique”
build.payloads.<name>.formatInputRuns with
pe (default)an executable matching output.archreflective_loading, or process_hollowing
dlla library matching output.archreflective_loading (set export to call a function)
shellcodea raw code blobreflective_loading, or the in-process runners execution_callbacks, module_stomping and fibers
dotneta managed (.NET) assemblydotnet_hosting (the stub hosts the CLR)

reflective_loading maps the payload into the packed binary’s own process and handles the native formats, so it is the safe default. Three lighter in-process runners take shellcode only and return control to the loader (so they chain in a runtime: list): execution_callbacks (a legitimate enumeration API invokes the payload), module_stomping (a signed DLL’s code section is overwritten) and fibers (the payload runs on a fiber). process_hollowing only runs format: pe; it runs a full PE (imports, TLS state, entry hand-off) but not the payload’s TLS callbacks — see Process hollowing evidence. A managed assembly is not native code: it needs format: dotnet and the dotnet_hosting technique, which hosts the CLR in the stub and loads the assembly from memory (the two imply each other, and the assembly’s bitness must match output.arch).

Terminal window
# Validate the plan and the payload without compiling anything:
cargo run -- build payload.yaml --dry-run
# Pack it:
cargo run -- build payload.yaml

-v/--verbose prints the resolved plan and per-step detail; the manifest (dist/demo.manifest.json) records the artifact, payload and plan hashes. picaro prints a banner before every subcommand; -q/--quiet skips it.

Terminal window
.\dist\demo.exe # run it
Terminal window
cargo run -- audit dist/demo.exe # measure it
cargo run -- techniques list # look around the registry

audit reports headers, sections and their entropy, imports/exports, a strings scan (naming each flagged string and why it matched), YARA and (optionally) an offline Defender scan.

  • schema: 6 is the only accepted version; an older document is rejected with a migration hint.
  • Unknown keys are an error.
  • Only Execution techniques take a params.payload; with two or more payloads it is required.
  • A terminal step (reflective_loading of a PE executable, dotnet_hosting) must be last.
  • build.output.arch is x64 or x86, and the payload must match it: the stub and the payload share one address space. Both execution techniques and the preparations support x86 (see x86 support); sleep_obfuscation, syscalls.mode: indirect and api_resolution stay x64-only, and an x86 pipeline that needs one of them is rejected at validation with a message naming the technique.
  • Positional CLI args after the plan path are substituted as $arg.0$, $arg.1$, … (and name=value as $arg.name$), so one plan can target several hosts.
  • Plan — every block and field, plus the wizard.
  • Examples — the runnable plans.
  • Techniques — what each runtime technique does and what it costs.
  • Loader features — the stub-level configuration (syscall layer, API resolution, string obfuscation, payload arguments).