lictor 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6e9c7b5e421a9542337e32d066b0cbe509b497f7d79184e6db586056758cef7f
4
+ data.tar.gz: 6ee268badd063fbb9339a399ab63051155a3fde7956edb0b49840376b0b03eb2
5
+ SHA512:
6
+ metadata.gz: dc09b6af16b52923a4ec5daee3d0b348ff0223b7a49262d037c439bc0ab04ea01de6006a3cd5fb6530217571638ffecfa78e4ef0f21cee2c5a5a54b85a324012
7
+ data.tar.gz: 7f881208f341632cf934bb79bda0531e30d35a83caf5e4cb83b687d29282a4ef6dff3fdbf6c1cf499cbc26f51cd79f23825b69825dfdb066b3720a5b030a3962
data/CHANGELOG.md ADDED
@@ -0,0 +1,240 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] - 2026-10-29
11
+
12
+ - The lab grows to eight targets: the ROP Emporium pair (callme,
13
+ ret2csu) joins as binary-only services - the challenge binaries and
14
+ their companion libraries vendored under `lab/targets/` with the
15
+ provenance noted, served through wrapper scripts (the loader needs
16
+ `LD_LIBRARY_PATH`, the lib opens the encrypted flag relative to the
17
+ cwd). The demo walks them like the rest: acquire, suggest (the
18
+ unbounded copy is a read), the pack-taught layout builds the payload
19
+ (measured offset), the discovered exploit lands the flag. The build
20
+ fetches the companion library beside the measured binary so the
21
+ debugger's cyclic run resolves it.
22
+ - The ret2plt loop is deterministic: the service's copy is bounded by
23
+ the read length (`memcpy`, not `strcpy`), so the read channel carries
24
+ NUL bytes and the textbook chain rides whole - the banner's leaks aim
25
+ pop rdi (entry+4), the service's own command string and system, the
26
+ chain enters system at the ABI alignment (measured under gdb,
27
+ 30/30 connections land the flag). The demo's oracle is now the
28
+ target's own flag (success means THIS target's flag, a wrong flag
29
+ fails), and the ret2plt source knowledge (`vuln("sof")`) enters
30
+ through suggest like fmtstr's.
31
+
32
+ - The ret2plt loop is deterministic: the service's copy is bounded by
33
+ the read length (`memcpy`, not `strcpy`), so the read channel carries
34
+ NUL bytes and the textbook chain rides whole - the banner's leaks aim
35
+ pop rdi (entry+4), the service's own command string and system, the
36
+ chain enters system at the ABI alignment (measured under gdb,
37
+ 30/30 connections land the flag). The demo's oracle is now the
38
+ target's own flag (success means THIS target's flag, a wrong flag
39
+ fails), and the ret2plt source knowledge (`vuln("sof")`) enters
40
+ through suggest like fmtstr's.
41
+ - README banner recompressed: `lictor-v1.png` 1.9M -> 969K (256-color palette
42
+ with dithering; repo weight only, no content change). `lictor-v0.png` and
43
+ `demo.gif` are untouched.
44
+ - The demo tape is self-sufficient and leak-free: the setup line (workdir
45
+ creation, `lictor.yml`, PATH, starship stripping) is typed under `Hide`/`Show`
46
+ so nothing of the host ever shows in `docs/art/demo.gif`, the workdir
47
+ `/tmp/lictor-demo` is created by the tape itself (a skipped manual step used
48
+ to leave the whole render in the caller's CWD, which broke the final trail
49
+ tail), and the repo root is referenced as `$PWD` captured before the `cd` -
50
+ no laptop path on screen.
51
+ - The demo's closed loop lands the flag on screen: the tape's `--flag-regex`
52
+ carried doubled backslashes (vhs sends the typed string verbatim, bash
53
+ single quotes preserved them, the oracle regex matched a literal backslash
54
+ and never fired - three executed/failed attempts in every render). The
55
+ regex is now backslash-free (`FLAG[{][^}]*}`). And `lictor run` surfaces
56
+ the flag the executor reports: the successful attempt prints
57
+ `flag: FLAG{...}` after the steps - the proof the loop closed, since the
58
+ exit code lies both ways.
59
+
60
+ - The engagement flow without flags: `lictor acquire <name>` resolves the source from the target's registry entry (the `--from`/`--bin` recorded at `target add`) - one `target add` and the acquire needs no flags; the explicit flags still override.
61
+
62
+ - The ROP chaineur: the build layout gains the `chain` slot - one arch-width word per
63
+ reference, resolved in order against the facts (`gadget:TYPE`, `plt_addr:NAME`,
64
+ `got_addr:NAME`, `win`, `leak:NAME`, `const:N`), loud BuildError on any missing piece.
65
+ `ret2plt` (the puts@got leak chain) and `ret2libc` (post-leak: ret-align + pop_rdi +
66
+ binsh + system) ship as built-in layouts; a pack's `build:` teaches chains for its own
67
+ techniques.
68
+ - `Lictor::Tube`: the ONE TCP implementation behind the banner readers (ADR 0001) -
69
+ connect, read_until (EOF/reset/deadline/limit), write, reset tracking. `harvest` and
70
+ `verify` consume it; `verify` honours `--facts` (it used to append to a hardcoded
71
+ `target.facts` in the CWD, ignoring the flag).
72
+ - The success oracle reads the FLAG, not the exit code: `run --flag-regex RE` makes the
73
+ flag pattern in the runner's output the success semantics (a polite exploit exits 0 on
74
+ failure, a caught-cleanup exploit exits non-zero after landing the flag - the exit code
75
+ lies both ways). Without the regex the exit code stays the oracle, the honest weaker
76
+ fallback.
77
+ - The two-steps state: the run's leak capture - the service's own output hex addresses
78
+ become `leaked_address` facts for the NEXT attempt (trailed as `leak` events), so a
79
+ leak technique followed by a rebuild technique resolves its `leak:NAME` refs from the
80
+ observed values. The boundary stays documented: a leak and its strike inside ONE
81
+ connection is the operator's exploit craft (the scaffold pattern), not the loop's.
82
+ - The run manifest + resume (ADR 0003): one stable run id per logical run - every trail
83
+ event carries it, the CLI prints it with the resume hint; the state (facts with the
84
+ merged leaks, the attempt history) lands on disk per attempt (`run.json` in the target
85
+ dir); `lictor run --resume <id>` continues THAT run - an interruption is a pause, not
86
+ an orphan.
87
+ - Named targets (ADR 0003): `lictor target add <name> <host:port> [--from URL | --bin
88
+ PATH]` scaffolds `targets/<name>/` (facts, payloads, its own trail) and registers it
89
+ in `targets.yml`; `--target NAME` scopes acquire/plan/build/run/fuzz/verify/harvest
90
+ state into the dir - the CWD-global `target.facts` clobbering is dead, two targets
91
+ coexist.
92
+ - Per-target priors: the isolation comes from the scoped trail (the `--target` trail
93
+ file IS the scope) - a failure on target A shifts A's decisions, B stays at the
94
+ defaults; proven by the isolation spec.
95
+ - `lictor report --defender`: the blue-side view of the SAME trail - the raw action
96
+ timeline (the scans, the banners read, the payloads sent, the techniques attempted
97
+ with their verdicts) plus, per attempted technique, the pack's `detection:` hints.
98
+ - The hardening matrix in the lab: the same sources compiled canary+PIE+full-RELRO+
99
+ FORTIFY, served on the 889x ports; `lab/purple.sh` replays the purple loop - `augure
100
+ coverage` flips against the corpus, `lictor report --defender` reads an attack trail.
101
+ - The lab gains the missing targets: `uaf_heap` (9995, tcache UAF with the fn pointer
102
+ on the heap - the 8 heap packs finally have a bench) and `orw` (9994, seccomp filter:
103
+ execve dies). The builder is seccomp-aware: a `sh` stub under `seccomp("true")` is a
104
+ booby trap, so the slot switches to the orw chain (open/read/write, clean exit) with
105
+ the flag path embedded. The `orw` technique ships as a built-in layout.
106
+ - ADRs: docs/adr/ records the architectural decisions - the Tube as the transport seam
107
+ (0001), verify as the arithmetic check with SMT a seam not a promise (0002),
108
+ engagement state as a directory per target, no database (0003).
109
+
110
+ ### Changed
111
+
112
+ - The speed pass (zero verdicts moved): the profiler's gadget scan precompiles its patterns and scans with String#index - profiling a 1MB binary drops 6.07 s to 58 ms (augure side, 45x); StaticOffset runs the cheap e8 scan first (the whole-text disassembly is the last resort - the >600 s hang on a 1 MB text dies); the MCTS depth-bounds the degenerate tree; the corpus guard memoizes; the CLI lazy-loads metasm (the fuzz/draft/scaffold paths load it on use); the build loads its packs once. The augure before/after table lives in augure/tools/bench.rb, the lictor one in tools/bench.sh.
113
+
114
+ - The report carries the WHOLE trail: `Trail#runs` maps acquire, build (offset, ret
115
+ source, size), scaffold, harvest, verify, suggest, leak and fuzz - the auditable
116
+ deliverable no longer omits most of the audit. `report --json` renders the same
117
+ completeness.
118
+ - The lab is six targets (+3 hardened mirrors); the demo walks suggest-then-decide for
119
+ the source knowledge (heap class, seccomp filter, controlled format) and
120
+ harvest-then-build for the orw chain.
121
+ - The capabilities table documents the new surfaces: the chain-slot build, the suggest
122
+ `-o` write-back, `harvest --facts`, the `run --flag-regex` oracle and the leak capture.
123
+
124
+ - The run's static enrichment died silently when the lazy metasm require moved (a NameError swallowed by the block's bare `rescue`): the enrichment now trails a `measure` event (the sink, the offset, the source - the report renders it) and the rescue catches only the KNOWN failure modes (the metasm decode family, the file errors) - a programming error crashes loudly instead of vanishing. The regression spec asserts the event's presence: its absence is the red flag.
125
+
126
+ - `suggest` and `harvest` never persisted their results: the merged fact base was
127
+ printed then dropped (the docs even claimed the tool "merges into the base"), and the
128
+ harvest's leaked addresses existed only on stdout - the agent-enrichment loop was a
129
+ dry-run simulator. `suggest -o PATH` writes the merged base, `harvest --facts PATH`
130
+ appends the leaked_address facts; regression-tested with a fake banner server.
131
+ - The safe default poisoned the learning: a handoff attempt (manual executor, nothing
132
+ executed) was recorded as `success: false`, and `Priors.from_trail` counted it - every
133
+ handoff-only run systematically downgraded the decided technique for all future
134
+ decisions. The outcome is trailed only when something actually ran; a handoff run
135
+ stops after one recipe (nothing changes between attempts) with status `handoff`; a
136
+ fresh auto-scaffold still continues the loop.
137
+ - `lictor build` never consulted pack `build:` sections: `layout_for` read packs but
138
+ `build()` passed none, and `augure::Pack` had no `build` accessor at all (the pack DSL
139
+ wrote a section nothing could read). Armor check 8 on the augure side validates the
140
+ optional `build:` (own technique, slot vocabulary); the builder accepts `packs:` and
141
+ the CLI passes the loaded packs. A pack teaching a NEW technique now materializes its
142
+ own layout.
143
+ - The `dword` slot packed with the arch width (8 bytes on x64): a partial-overwrite
144
+ layout emitted the wrong payload. `dword` is 4 bytes whatever the arch; `qword` is the
145
+ pointer width.
146
+ - One auto-scaffold per technique per run: the loop continued on a fresh scaffold (the
147
+ next attempt discovers it) but re-scaffolded the SAME technique on every further
148
+ attempt, hitting the scaffold's already-exists refusal.
149
+ - The executor timeout kills a black-holed runner instead of hanging forever.
150
+
151
+ ## [0.3.0] - 2026-10-04
152
+
153
+ ### Changed
154
+
155
+ - **v0.3.0: the operator is the authorization.** The gates and the consent tokens are
156
+ GONE - no allowlist file, no tokens, no typed confirmations (the review caught `run`
157
+ minting its own consent with the target as its own confirmation). The README carries
158
+ the authorized-use disclaimer; the trail is the record. `lictor init` scaffolds the
159
+ trail only; the doctor checks the environment only (gdb/ronin/docker as warns with
160
+ exact fixes).
161
+ - v0.3.0 in the gem: the operator-is-the-authorization model (consent and the allowlist
162
+ are gone, the trail is the record), the gemspecs say so, the gem ships the build
163
+ layouts and the scaffold template (`gem build` no longer produces a dead artifact),
164
+ `Unauthorized` is deleted.
165
+ - The demo and every doc reflect the current commands: the harvest act replaces the
166
+ consent act, the chain is scan → acquire → suggest → harvest → plan → build → run →
167
+ outcome → report.
168
+
169
+ ### Removed
170
+
171
+ - The consent system and the allowlist (the operator is the authorization; the trail is
172
+ the record). The gate-era `-a`/targets.yml spec residue went with them.
173
+ - The dead empty `begin` in the plan command and the MCP's six `Unauthorized` rescues
174
+ (the class is gone).
175
+
176
+ ### Fixed
177
+
178
+ - The trail of offensive operations was world-readable: `record` now creates the JSONL
179
+ 0600 (existing files keep the operator's perms). The profiler and acquire subprocesses
180
+ hang forever on a wedged binary: both carry a 300 s timeout raising a loud error.
181
+ - `lictor acquire` surfaces the profiler's stderr in the failure message (the profiling
182
+ subprocess used to die silently and the error said nothing).
183
+ - The gemspecs build identically from any working directory: `files` globs anchor to
184
+ `__dir__` and lictor-mcp reads its version from `lib/lictor-mcp/version.rb`.
185
+ - `lictor run` failure is truthful now: the lab exploit raises when the flag does not
186
+ come, so ronin's exit code can no longer lie about the success (the `--flag-regex`
187
+ oracle supersedes even this).
188
+
189
+ ## [0.2.0] - 2026-10-03
190
+
191
+ - `lictor plan <binary>`: ELF magic detection profiles the binary
192
+ through the augure-profile subprocess (trailed), then decides. A
193
+ `.facts` file passes through unchanged. One command from binary to
194
+ decision.
195
+ - `lictor scan`: nmap as a gated, trailed action against an allowlisted
196
+ host (subprocess; --nmap override for testing). Scans appear in
197
+ `runs` and in the engagement report.
198
+ - Network facts: service/software_version/remote emitted from nmap
199
+ scans; remote technique packs match them like binary facts.
200
+ - Fuzzing and triage (augure-profiler): RubyMutator built-in + radamsa
201
+ optional; GdbBackend (real run covered), RizinBackend, LldbBackend;
202
+ `lictor fuzz` gated and trailed.
203
+ - Executor interface: ManualExecutor (default, handoff) + RoninExecutor
204
+ (discovery from the operator's repos; fabricated names refused).
205
+ - `lictor run`: the closed loop - decide, execute, outcome, re-decide -
206
+ with budgets and interactive confirmation.
207
+ - Consent tokens: `lictor authorize <target>` - human-typed, TTL'd,
208
+ trail-recorded.
209
+ - lictor-mcp (4th gem): scan/plan/execute/validate_pack tools with
210
+ server-side gates and token validation.
211
+ - `lictor rule validate|install|list`: teach augure techniques as data
212
+ (packs), full armor + corpus guard.
213
+
214
+ ### Removed
215
+
216
+ - `--i-am-authorized` and `--target` flags: security theater. A flag
217
+ can be typed by reflex, by a script, or by anyone with shell access -
218
+ it proves nothing and taxes the legitimate user. Authorization is the
219
+ allowlist artifact (explicit host[:port] entries, colon-delimited
220
+ prefix match so .1 cannot pass for .10) plus the trail. A future
221
+ execute command will confirm interactively (show the recipe, type the
222
+ target), never through a flag.
223
+
224
+ ## [0.1.0] - 2026-10-02
225
+
226
+ - Project scaffold: authorization gates design, handoff-default README,
227
+ CI (gitleaks), MIT license, AGENTS.md.
228
+ - Gates: explicit allowlist (wildcards rejected), per-run consent flag,
229
+ allowlist-checked `--target`.
230
+ - Recon: nmap XML to target inventory via ruby-nmap; `lictor recon`
231
+ prints allowlist candidates (candidates, never authorization).
232
+ - Handoff default: augure decision printed as a ronin-exploits recipe
233
+ with mixin hints (Binary / FormatString); exploit names deferred to
234
+ the operator's ronin repos.
235
+ - The closed loop: `lictor outcome` records results, outcomes feed the
236
+ augure bandit priors (`Lictor::Priors.from_trail`), the next decision
237
+ adapts - end to end trailed.
238
+ - JSONL trail with run aggregation (`lictor runs`).
239
+ - Engagement report: auditable markdown (decision, rule provenance,
240
+ outcome) with the claims-discipline line; `lictor report -o`.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gregory Epp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # lictor
2
+
3
+ ![lictor - the fasces of fiber, the trail behind, the open door ahead](docs/art/lictor-v1.png)
4
+
5
+ **The orchestrator that carries out augure's judgments: nmap, fuzzing,
6
+ profiling, execution - every action recorded, handoff by default.**
7
+
8
+ > *Lictor (n.): the magistrate's attendant who carried the fasces and
9
+ > executed judgments. He did not judge. He executed what was judged.*
10
+
11
+ [![CI](https://github.com/log0u7/lictor/actions/workflows/ci.yml/badge.svg)](https://github.com/log0u7/lictor/actions/workflows/ci.yml)
12
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.4-ruby.svg)](https://www.ruby-lang.org/)
13
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
14
+
15
+ **v0.3.0** - pairs with [augure](https://github.com/log0u7/augure), the
16
+ auditable exploitation decision layer. augure decides; lictor executes;
17
+ the trail records everything.
18
+
19
+ > **Authorized use only.** This tool is for legally authorized security
20
+ > assessments and CTF labs. You are the operator: what it does, you
21
+ > did. Know the law where you stand.
22
+
23
+ ## Why lictor
24
+
25
+ Most automation in offensive tooling is a black box: it fires and you
26
+ find out why afterwards - or never. lictor inverts that:
27
+
28
+ - every technique choice comes from a **verified rule with its
29
+ evidence** (augure's explain payload: rule ID, facts, seeded draw);
30
+ - every action is **trailed** - one JSONL line, attributable, replayable;
31
+ - the dangerous edge - execution - is opt-in: the default executor is
32
+ the handoff, which prints the recipe and runs nothing.
33
+ - the mirror is the analysis target: the scan identifies, the local
34
+ reproduction gets profiled and built against, `verify` proves the
35
+ remote faults like the local model.
36
+
37
+ Equally strict when an LLM agent drives it through MCP. The brakes are
38
+ not a mode - they are the design.
39
+
40
+ ## The 60-second demo
41
+
42
+ ![lictor demo: scan, decide with provenance, run, and the auditable trail](docs/art/demo.gif)
43
+
44
+ Replay it yourself against the lab:
45
+
46
+ ```sh
47
+ sh lab/demo.sh
48
+ ```
49
+
50
+ The full chain is: **scan → acquire/mirror → (fuzz → triage) →
51
+ verify → suggest → harvest → plan → build → run → outcome → report**,
52
+ every step recorded in the trail (one stable run id, the manifest
53
+ written per attempt), the decision explained rule-by-rule.
54
+ `harvest` reads the service banner's hex leaks into `leaked_address`
55
+ facts; `suggest` is the agent's own step
56
+ `suggest` is the agent's own step: propose observations as facts, get
57
+ the decision delta back
58
+ (see [docs/how-to-llm-suggestions.md](docs/how-to-llm-suggestions.md));
59
+ `build` is the payload step: the offset is measured, the bytes are
60
+ assembled (below). When discovery finds NO exploit for the decided
61
+ technique, the run scaffolds it and the next attempt uses it; when NO
62
+ technique applies to the crash, `lictor draft` writes a staged pack
63
+ from the observations - the loop feeds augure back.
64
+
65
+ ## Named targets: the engagement model
66
+
67
+ State is scoped per target (ADR 0003), not clobbered in the CWD:
68
+
69
+ ```sh
70
+ lictor target add web01 10.0.0.5:9999 --from http://10.0.0.5:8000/binary
71
+ lictor --target web01 acquire web01 # or: acquire web01 --target web01
72
+ lictor --target web01 plan # reads targets/web01/target.facts
73
+ lictor --target web01 run web01 --facts targets/web01/target.facts \
74
+ --executor ronin --flag-regex "FLAG\{[^}]+\}"
75
+ lictor run ... --resume <run-id> # the manifest is written per attempt:
76
+ # an interruption is a pause, not an orphan
77
+ ```
78
+
79
+ `targets/<name>/` holds the facts, the payloads and the trail; the
80
+ registry is `targets.yml`. Priors are per-target by construction (the
81
+ scoped trail IS the scope): a failure on A does not shift B's
82
+ decisions.
83
+
84
+ The same loop exports as a BENCH: `sh lab/bench.sh` renders the
85
+ engagement's trail as `lictor/bench@1` - the tasks/actions/feedback
86
+ telemetry the LLM benchmarks read (NYU CTF Bench, InterCode). The
87
+ agent proposes, augure disposes, lictor executes, and the whole run
88
+ is the measurement.
89
+
90
+ ## From decision to payload: the offset is measured, not guessed
91
+
92
+ `lictor build` carries the original tool's inheritance - with every
93
+ number auditable instead of assumed:
94
+
95
+ - the **offset is measured**: the frame read statically (ret2win x64 =
96
+ 40, the canonical number) or the cyclic pattern crashed under the
97
+ debugger - never a magic constant;
98
+ - the **return address comes from the facts**: the winning symbol's
99
+ address, the gadget facts, or the addresses the service's own banner
100
+ leaked (`lictor harvest`);
101
+ - the **shellcode is assembly source** (execve, bash -p, the orw chain
102
+ for seccomp targets) assembled on demand by metasm - no hex blobs;
103
+ - the result is a payload file the trail records and `lictor run
104
+ --payload` sends as-is. When the flag does not come, the exploit says
105
+ so and the run says failed.
106
+
107
+ ```sh
108
+ lictor build ret2func --facts target.facts -o payload.bin
109
+ # technique: ret2func
110
+ # offset: 16 (measured)
111
+ # ret: win_symbol:win@4198870
112
+ # payload: payload.bin (24 bytes, arch x64)
113
+ lictor run 127.0.0.1:9999 --facts target.facts \
114
+ --executor ronin --payload payload.bin
115
+ ```
116
+
117
+ The slot vocabulary, the metasm usage step by step, and the published
118
+ lineage (metasm SSTIC 2007 -> the SSTIC 2008 deprotection paper ->
119
+ esec-lab 2010 -> miasm 2012 -> the 2015 ghost-writing technique) live
120
+ in [augure/docs/build-format.md](../augure/docs/build-format.md).
121
+
122
+ ## Capabilities
123
+
124
+ | Command | What it does |
125
+ |---|---|
126
+ | `target add / list` | Named targets: a dir per target (facts, payloads, its own trail) + the `targets.yml` registry; `--target` scopes every command. |
127
+ | `init` / `doctor` | Scaffold the engagement setup; verify the environment (exact fix per check; gdb/ronin/docker as warns). |
128
+ | `recon <nmap.xml>` | Print the service candidates from an nmap XML (passive, never scans). |
129
+ | `runs` | List the trailed runs. |
130
+ | `scan <host>` | nmap against a target; the scan is recorded. |
131
+ | `acquire <target>` | The lab convenience: copy or download the binary when the target serves it. The real flow is the MIRROR (below). |
132
+ | `fuzz <binary>` | Fuzz + triage a local binary: the fact **discovery** layer. Length sweep + mutations; gdb/rizin/lldb backends. |
133
+ | `verify <target>` | The differential: the crash input faults the remote like the local model → the facts become `remote-verified`. |
134
+ | `harvest <target>` | The banner's hex leaks become `leaked_address` facts - `--facts PATH` appends them. |
135
+ | `plan <binary-or-facts>` | Profile if ELF, decide (augure), print the recipe. |
136
+ | `build <binary>` | Assemble the payload for the selected technique: measured offset (cyclic pattern under the debugger), fact-sourced return address, metasm-assembled shellcode. Layouts cover the ROP chains (`chain` slots resolve `gadget:`/`plt_addr:`/`got_addr:`/`leak:`/`const:` refs from the facts, loud `BuildError` on any missing piece); under seccomp the `sh` stub switches to the orw chain. |
137
+ | `suggest <facts>` | The agent proposes observations as facts; augure re-decides and the delta comes back - `-o PATH` writes the merged base, so the enrichment is not a dry run. |
138
+ | `draft <facts>` | No technique applies? The observed facts + the measured constants draft a PACK in the staging dir, armor-validated, for the operator to review and install. |
139
+ | `exploit scaffold <technique>` | Draft the ronin exploit for a decided technique: the measured constants embedded, the payload loader wired - review, commit, run. |
140
+ | `run <target>` | The closed loop: decide, execute, outcome, re-decide - with an attempt budget, `--flag-regex` as the success oracle (the flag in the output, not the exit code), the leak capture (the output's hex addresses become facts for the next attempt) and the run manifest (`--resume <id>` continues an interrupted run). |
141
+ | `outcome <run-id>` | Record the result; the next decision adapts the priors. |
142
+ | `report` | The auditable engagement report (markdown, provenance included) - `--defender` renders the blue-side view: the action timeline + the packs' `detection:` hints. |
143
+ | `rule validate/install/list` | Teach augure a new technique - as data, through the armor. |
144
+
145
+ ## The operator is the authorization
146
+
147
+ No gates, no tokens. You decide what to target; the trail records what
148
+ was done:
149
+
150
+ 1. **Handoff is the default.** `plan` prints the recipe and executes
151
+ nothing; `run` with the ronin executor is opt-in and budgeted.
152
+ 2. **The trail is append-only.** Decision → rule provenance → action →
153
+ outcome. An engagement you cannot replay is an engagement you cannot
154
+ defend.
155
+ 3. **The mirror is the analysis target.** The scan identifies
156
+ (service + version); when the remote cannot be profiled, you stand
157
+ up the SAME service locally - your package, your install - and
158
+ lictor scans+profiles the mirror, builds on its facts, and `verify`
159
+ proves the remote faults like the local model before anything runs
160
+ for real.
161
+
162
+ ## Architecture
163
+
164
+ ```mermaid
165
+ flowchart LR
166
+ subgraph scope ["SCOPE - the engagement (ADR 0003)"]
167
+ T["lictor target add<br/>targets.yml registry"] --> TD["targets/NAME/<br/>facts, payloads, trail"]
168
+ end
169
+
170
+ TD --> F
171
+
172
+ subgraph decide ["DECIDE - the augure SDK"]
173
+ ELF["acquired binary<br/>(the MIRROR)"] --> PRF["augure-profiler<br/>metasm: checksec, gadgets, addresses"]
174
+ PRF --> F["facts + value domains"]
175
+ HST["harvest: the banner's leaks"] --> F
176
+ SUG["suggest: the operator's knowledge"] --> F
177
+ F --> A["augure Engine<br/>Datalog rules-as-data"]
178
+ P["technique packs<br/>(build:/detection:/mitre:)"] --> A
179
+ KB["KnowledgeBase<br/>22 patterns"] -. priors .-> A
180
+ A --> V["Verifier + Bandit + MCTS"]
181
+ V --> D["decision + provenance<br/>augure/decision@1"]
182
+ end
183
+
184
+ subgraph act ["ACT"]
185
+ D -- "handoff (default)" --> H["recipe printed"]
186
+ D -- "opt-in, budgeted" --> RN["ronin-exploits (subprocess)"]
187
+ RN --> O["outcome + leak capture"]
188
+ H --> O
189
+ O -- "the flag in the output<br/>(--flag-regex oracle)" --> ST["status"]
190
+ O --> MF["run.json manifest<br/>--resume continues"]
191
+ end
192
+
193
+ MF --> TR["trail (0600)<br/>one id per run, 14 event types"]
194
+ TR --> RP["report<br/>(--defender: the blue view)"]
195
+ O -. "outcomes become priors<br/>(per target)" .-> A
196
+ ```
197
+
198
+ ## The Ruby-native arsenal
199
+
200
+ | Need | Gem | Why |
201
+ |---|---|---|
202
+ | nmap XML parsing | [ruby-nmap 1.0.3](https://rubygems.org/gems/ruby-nmap) (postmodern, 703k dl) | the maintained standard; parses XML without running nmap |
203
+ | exploitation runtime | [ronin-exploits 1.1.1](https://rubygems.org/gems/ronin-exploits) + [ronin 2.1.1](https://rubygems.org/gems/ronin) | exploits as Ruby classes, mixins = the technique vocabulary; no daemon, `gem install` |
204
+ | binary analysis | `augure-profiler` (metasm) | pure Ruby, no subprocess, augure's sibling gem |
205
+ | fuzzing | built-in `RubyMutator` (zero-dep) + radamsa optional | the discovery layer for binaries with no write-up |
206
+ | persistence | stdlib JSONL | sequel when queries are actually needed |
207
+
208
+ **Why not Metasploit?** It IS Ruby - but a monolithic Rapid7 app you
209
+ must install and drive through a daemon. Lictor's path is gems: ronin
210
+ installs as a gem and runs as a subprocess, nothing to daemonize. A
211
+ ready-made msf module is fine for the operator's hands - it is just
212
+ not on lictor's critical path.
213
+
214
+ ## The MCP surface: when an LLM agent drives
215
+
216
+ [lictor-mcp](mcp_server/README.md) exposes seven tools over stdio - `scan_target`,
217
+ `plan_technique`, `execute_technique`, `suggest_facts`, `build_payload`,
218
+ `scaffold_exploit`, `validate_pack` -
219
+ and validates
220
+ **server-side**: the armor, the corpus guard, the trail.
221
+ The agent proposes; augure disposes; lictor executes - and the checks
222
+ run server-side, so the agent cannot weaken them. Agents teach augure new techniques through the same armor:
223
+ see [augure's pack format](https://github.com/log0u7/augure/blob/main/docs/pack-format.md)
224
+ and the [augure-operator skill](https://github.com/log0u7/augure/tree/main/skills/augure-operator).
225
+
226
+ ## Honest numbers
227
+
228
+ Read every claim below as a **prediction with provenance**, not a field
229
+ result - the lab is the measurement bench, not the world:
230
+
231
+ | Claim | Evidence |
232
+ |---|---|
233
+ | ret2func: the built payload lands the real flag, no brute-force | the lab, e2e: build (offset 16 measured) → run → `FLAG{augure_decides_lictor_executes}` |
234
+ | the offset measurement generalizes | static frame read (ret2win x64 = 40, the canonical number) + the cyclic pattern under the debugger, cross-validated on the same binaries |
235
+ | a missing exploit drafts itself | the run's auto-scaffold: discovery empty + binary in hand -> the exploit file lands in the repo, the next attempt uses it |
236
+ | an unknown crash becomes a pack | `lictor draft`: the observed facts + the measured constants -> an armor-validated pack in the staging dir |
237
+ | corpus: 33/33 technique classifications | `augure-benchmark`, CI-enforced at 100% |
238
+ | the eight-target lab, seven deterministic end-to-end | the lab: ret2func/ret2plt/shellcode/fmtstr/orw land the flag via `run` (orw reads its own flag, not the shared one); ret2plt's chain rides the read channel - the copy is bounded by the read length so NUL-bearing addresses cross, rdi aims the service's own command string, 30/30 connections - and the oracle is the target's own flag; the ROP Emporium pair (callme, ret2csu) joins through packs: callme's pop_rsi_r15 pattern and the csu popper aimed at the epilogue (not the symbol entry), an import emits no win_symbol; uaf_heap decides and hands off loyally |
239
+ | the hardening matrix predicts its own death | `lab/purple.sh`: the hardened mirrors (canary+PIE+RELRO+FORTIFY) + `augure coverage` flips - the decision's applicable set shrinks as documented |
240
+ | MEASURED against a typed-decision classifier (Laya, open weights, the fact text as state, the 16 heads as choices): augure **5/5** at 8.5 ms (warm, MCTS included); the classifier 1/5, confident-and-wrong (0.93-0.98 confidence on its errors) - the global-prior bias, reproduced by a 2026 model | `pappers/laya-bench.py`, measured on CPU (i7 8-core, torch) |
241
+
242
+ A run that fails says failed: `--flag-regex` makes the flag in the
243
+ output the success oracle (the exit code lies both ways), and the
244
+ manual executor hands off instead of pretending.
245
+
246
+ ## Acceptable use
247
+
248
+ Lictor exists for **authorized security work** (you are responsible for the legality where you stand): engagements with a
249
+ signed scope, labs you own, CTF environments. The authorization model
250
+ is load-bearing; bypassing it is the one contribution that will be
251
+ rejected on sight. Lictor writes no exploit code - it orchestrates the
252
+ operator's chosen tooling; the knowledge lives in the repos, not here.
253
+
254
+ ## Install
255
+
256
+ Lictor is distributed as a source checkout for a reason: an orchestrator
257
+ around pentest tooling has no business being one `gem install` away
258
+ from anyone. Bundle it from this repo:
259
+
260
+ ```ruby
261
+ # Gemfile
262
+ gem "augure", path: "../augure" # resolved by path - NOT on rubygems yet
263
+ # lictor + lictor-mcp: bundle from your own checkout, private
264
+ # (augure's tag release can publish it; until then the sibling
265
+ # checkout is the source of truth - the Gemfile in this repo says so)
266
+ ```
267
+
268
+ ## Documentation
269
+
270
+ The architectural decisions are recorded as ADRs in
271
+ [docs/adr/](docs/adr/): the Tube as the transport seam (0001), verify as
272
+ the arithmetic check with SMT a seam not a promise (0002), engagement
273
+ state as a directory per target, no database (0003).
274
+
275
+ | I want to... | Read |
276
+ |---|---|
277
+ | run the demo end to end | [the lab](lab/README.md) |
278
+ | understand the operator model | [the operator section](#the-operator-is-the-authorization) |
279
+ | see what augure decides and why | [augure's README](https://github.com/log0u7/augure) |
280
+ | write the auditability story | [augure's auditability page](https://github.com/log0u7/augure/blob/main/docs/auditability.md) |
281
+ | teach augure a technique | [the pack format](https://github.com/log0u7/augure/blob/main/docs/pack-format.md) |
282
+
283
+ ## License
284
+
285
+ MIT, same as augure.