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 +7 -0
- data/CHANGELOG.md +240 -0
- data/LICENSE +21 -0
- data/README.md +285 -0
- data/bin/lictor +609 -0
- data/lib/config/build_layouts.yml +67 -0
- data/lib/lictor/acquisition.rb +78 -0
- data/lib/lictor/config.rb +24 -0
- data/lib/lictor/executor.rb +119 -0
- data/lib/lictor/exploit_scaffold.rb +61 -0
- data/lib/lictor/handoff.rb +62 -0
- data/lib/lictor/harvest.rb +58 -0
- data/lib/lictor/pack_draft.rb +84 -0
- data/lib/lictor/payload_builder.rb +187 -0
- data/lib/lictor/priors.rb +30 -0
- data/lib/lictor/profiler.rb +38 -0
- data/lib/lictor/recon.rb +61 -0
- data/lib/lictor/report.rb +123 -0
- data/lib/lictor/run.rb +174 -0
- data/lib/lictor/scanner.rb +31 -0
- data/lib/lictor/suggestion.rb +40 -0
- data/lib/lictor/targets.rb +38 -0
- data/lib/lictor/technique_packs.rb +25 -0
- data/lib/lictor/templates/scaffold_exploit.rb.erb +47 -0
- data/lib/lictor/trail.rb +77 -0
- data/lib/lictor/tube.rb +76 -0
- data/lib/lictor/version.rb +5 -0
- data/lib/lictor.rb +26 -0
- metadata +101 -0
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
|
+

|
|
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
|
+
[](https://github.com/log0u7/lictor/actions/workflows/ci.yml)
|
|
12
|
+
[](https://www.ruby-lang.org/)
|
|
13
|
+
[](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
|
+

|
|
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.
|