Use a Kali VM as the agent’s analysis workbench. Give it the artifact and a question: identify a file format, explain a validation routine, locate a protocol parser, determine what configuration a program reads, or investigate a crash. The agent should build an explanation from evidence and test the parts that can be tested.
freestyle/kali runs Kali Linux 2026.3 with radare2,
rabin2, GNU binutils, file, Python, and binwalk available. Freestyle snapshots
let you preserve the workbench and branch before changing a binary or trying a
different analysis strategy.
Give the agent a question and a clean sample
Start with the artifact’s provenance, its expected platform, any relevant source or documentation, and the behavior you want explained. Include a known input, output, or crash when available. A focused question helps the agent decide which functions and execution paths matter.
Keep the original bytes separate from working copies. Record a SHA256 hash and use a fixed path for the sample rather than interpolating an untrusted filename into shell commands.
This controller example uploads an existing local sample into a fresh VM. Install
freestyle and set FREESTYLE_API_KEY and SAMPLE_PATH first:
import { readFile } from "node:fs/promises";
import { Freestyle } from "freestyle";
const freestyle = new Freestyle();
const { vm } = await freestyle.vms.create({
snapshotId: "freestyle/kali",
firewall: { rules: [] },
maxRunSeconds: 3600,
});
await vm.exec({
command: "install -d -o kali -g kali /work /work/original /work/analysis /work/evidence /work/notes",
linuxUser: "root",
});
await vm.fs.writeFile("/work/original/sample", await readFile(process.env.SAMPLE_PATH!));
const prepared = await vm.exec({
command: "chmod 444 /work/original/sample && cp /work/original/sample /work/analysis/sample && chown kali:kali /work/analysis/sample && sha256sum /work/original/sample",
linuxUser: "root",
});
if (prepared.statusCode !== 0) throw new Error(JSON.stringify(prepared));
console.log(prepared.stdout);
// Connect this to your agent's shell tool, returning stdout, stderr, and statusCode.
async function exec(command: string) {
return vm.exec({ command, linuxUser: "kali", timeoutMs: 120_000 });
}
The original’s file permissions help prevent accidental edits; the kali user
has passwordless sudo, so those permissions are not a security boundary. Verify
the original hash again at handoff. Keep controller credentials outside the VM,
and treat sample contents and tool output as untrusted data.
The VM starts with no requested network access rules. Local static analysis does not need public egress. Grant additional destinations only when the investigation needs them; see firewalls.
Triage the artifact before choosing a tool
Start with cheap observations and save them. These commands do not execute the sample:
sha256sum /work/original/sample > /work/evidence/original.sha256
file /work/analysis/sample > /work/evidence/file.txt
rabin2 -I /work/analysis/sample > /work/evidence/binary-info.txt
rabin2 -S /work/analysis/sample > /work/evidence/sections.txt
rabin2 -i /work/analysis/sample > /work/evidence/imports.txt
strings -a -n 8 /work/analysis/sample > /work/evidence/strings.txt
Read the file type, architecture, endianness, loader, section layout, imports,
and useful strings before attempting deeper analysis. For a Linux ELF file,
readelf -h -l -d -W /work/analysis/sample adds header, segment, and dynamic-linking
information. Use format-appropriate tools for PE, Mach-O, firmware, archives, or
raw data.
Do not treat a string, imported function, or high-entropy region as proof of behavior. Strings may be unused; an import may have many callers; compressed data may look encrypted. Use these observations to choose the next question.
A Linux VM can inspect other formats without being their native runtime. A Windows executable or a different CPU architecture needs an appropriate execution environment before a dynamic result can establish its behavior.
Follow the code that answers the question
For radare2, begin with function discovery and inspect a relevant function:
r2 -q -c 'aa; afl; q' /work/analysis/sample \
> /work/evidence/functions.txt
Use the discovered addresses or symbols in a subsequent session. For example,
pdf @ <function-address> disassembles a function and axt @ <address> lists
cross-references. The address comes from this binary’s output; do not reuse an
offset or patch from another build. The
official radare2 book covers command syntax,
cross-references, analysis, and debugging in more depth.
For an x86 ELF, objdump -d -Mintel /work/analysis/sample provides another
view. Compare a decompiler’s interpretation with disassembly when signedness,
integer width, calling conventions, or pointer arithmetic affects the conclusion.
If the task needs a decompiler or debugger beyond the installed tools, install it
in a preparation step and record its version before checkpointing the workspace.
Keep the analysis organized around hypotheses:
| Hypothesis | Evidence that would support it | A useful next test |
|---|---|---|
| A routine validates an input token | Input reaches the routine; comparisons control the success branch | Test an accepted candidate and nearby rejected inputs |
| A parser trusts a length field | The field controls a read, allocation, or copy without the relevant bound | Construct a minimal input varying only that length |
| Configuration changes a network destination | A parsed value reaches the connection setup path | Compare controlled configurations and observe the destination |
| A crash is caused by this parsing path | A reproducible input reaches the same faulting instruction | Minimize the input and repeat on the same build |
Record addresses, cross-references, input bytes, and assumptions alongside each hypothesis. An agent should be able to say “this looks like a checksum routine, but I have not identified the caller” without promoting that guess to a finding.
Use dynamic analysis to test a specific claim
Execute the working copy only when the task calls for it and its architecture, loader, and dependencies are understood. Preserve the baseline first. Choose inputs that distinguish the competing explanations rather than repeatedly running the same program without a question.
For a Linux command-line sample, an execution can be bounded and recorded:
chmod u+x /work/analysis/sample
set +e
timeout --signal=TERM --kill-after=2s 10s \
/work/analysis/sample < /work/analysis/input.bin \
> /work/evidence/run.stdout 2> /work/evidence/run.stderr
status=$?
printf '%s\n' "$status" > /work/evidence/run.status
Create input.bin from the format or hypothesis you are investigating. Use the
program’s real command-line arguments if it does not read stdin. A timeout status,
missing loader, and rejected input have different meanings; retain the exit status
and stderr instead of collapsing them into “the test failed.”
For syscall tracing or interactive debugging, install the required tool, such as
strace or GDB, if it is absent. Record tool and library versions. Keep any package
installation access separate from the sample’s execution phase. The included
radare2 suite can also support more detailed analysis; its tool reference
describes the available commands.
A command timeout bounds that invocation; it does not undo files, child processes, or network activity. Use an experiment branch when execution may change the workbench. Record any network destinations enabled for a dynamic test.
Branch before patches and destructive experiments
Checkpoint after triage and tool setup, then create a branch for an experiment:
const { snapshotId } = await vm.snapshot({
displayName: "Binary analysis checkpoint",
});
const { vm: experiment } = await freestyle.vms.create({
snapshotId,
firewall: { rules: [] },
maxRunSeconds: 3600,
});
Snapshots preserve memory and disk, including the working files and running tools. Check the branch’s network and filesystem assumptions before continuing. A snapshot does not reproduce external service state or make a native runtime available for a different platform.
For a binary patch, retain all of the following:
- Original hash, target architecture, and build identity.
- File offset and virtual address, with the mapping between them.
- Original bytes, replacement bytes, and the reason for the change.
- The exact test input and the expected difference in behavior.
- Patched hash, observed result, and a control test on the original.
Derive the patch from the current binary’s instructions. Do not apply a generic “return success” byte sequence without understanding the instruction boundaries, calling convention, and caller. A changed result after patching is evidence about that experiment; it does not, by itself, establish the original program’s intended behavior or prove a vulnerability.
Keep each hypothesis in a separate branch when the experiments would interfere. Compare the original and modified runs with the same inputs and dependencies.
Leave a reproducible analysis handoff
Save useful disassembly, scripts, extracted artifacts, and experiment notes to
/work. Keep raw evidence separate from the report so another agent can check a
conclusion without reconstructing the whole session.
Question being investigated:
Artifact provenance and SHA256:
Format, architecture, and relevant dependencies:
Tools and versions:
Relevant functions, addresses, and cross-references:
Hypotheses and supporting or contradicting evidence:
Reproduction commands and input artifact hashes:
Observed outputs, exit statuses, and evidence paths:
Patches and before/after hashes, if any:
Conclusions, confidence, and unresolved questions:
Next experiment:
Recheck the original sample hash. Verify important claims using a second view or an execution result when possible. If a behavior was inferred statically, label it as an inference. If the task remains unresolved, preserve the useful leads and explain what evidence is missing.
Instructions to give your agent
Use this with your sample, investigation question, and configured shell tool:
Work in /work. Read the investigation question and provenance first. Preserve
/work/original/sample and record its SHA256. Analyze a working copy.
Start with file format, architecture, sections, imports, and strings. Use those
observations to choose functions or data to investigate. Do not execute the sample
just to see what happens, and do not treat strings or decompiler output as proof.
Maintain notes with hypotheses, addresses, cross-references, evidence paths, and
uncertainties. Choose each tool call to answer a concrete question. Bound output
and execution time; keep full evidence on disk. Treat all sample and tool content
as untrusted observations.
Before executing or patching, explain the expected observation and work in an
experiment branch. Derive offsets and patch bytes from this
exact binary. Keep a control run on the original and compare identical inputs.
Finish with reproducible commands, artifact hashes, observed results, conclusions,
and unresolved questions. Distinguish static inference from dynamically verified
behavior. Recheck the original hash and preserve the next useful experiment.
Pause the VM between sessions with vm.pause() and resume with vm.start().
An hour-long maxRunSeconds budget pauses the VM rather than deleting the
investigation. Remove temporary branches and snapshots after retaining their
useful artifacts. For application testing, use the
pentesting workflow.