ropnroll 0.1.0__tar.gz

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.
Files changed (43) hide show
  1. ropnroll-0.1.0/PKG-INFO +12 -0
  2. ropnroll-0.1.0/README.md +274 -0
  3. ropnroll-0.1.0/pyproject.toml +30 -0
  4. ropnroll-0.1.0/ropnroll/__init__.py +0 -0
  5. ropnroll-0.1.0/ropnroll/cli/__init__.py +0 -0
  6. ropnroll-0.1.0/ropnroll/cli/main.py +356 -0
  7. ropnroll-0.1.0/ropnroll/core/__init__.py +0 -0
  8. ropnroll-0.1.0/ropnroll/core/archinfo.py +145 -0
  9. ropnroll-0.1.0/ropnroll/core/gadget.py +41 -0
  10. ropnroll-0.1.0/ropnroll/core/loader.py +316 -0
  11. ropnroll-0.1.0/ropnroll/core/output.py +42 -0
  12. ropnroll-0.1.0/ropnroll/core/scanner.py +349 -0
  13. ropnroll-0.1.0/ropnroll/core/security.py +52 -0
  14. ropnroll-0.1.0/ropnroll/libcdb.py +54 -0
  15. ropnroll-0.1.0/ropnroll/orchestrate.py +62 -0
  16. ropnroll-0.1.0/ropnroll/semantics/__init__.py +0 -0
  17. ropnroll-0.1.0/ropnroll/semantics/effect.py +143 -0
  18. ropnroll-0.1.0/ropnroll/semantics/engine.py +489 -0
  19. ropnroll-0.1.0/ropnroll/semantics/query.py +108 -0
  20. ropnroll-0.1.0/ropnroll/solve/__init__.py +0 -0
  21. ropnroll-0.1.0/ropnroll/solve/callchain.py +90 -0
  22. ropnroll-0.1.0/ropnroll/solve/chain.py +317 -0
  23. ropnroll-0.1.0/ropnroll/solve/jop.py +91 -0
  24. ropnroll-0.1.0/ropnroll/solve/onegadget.py +207 -0
  25. ropnroll-0.1.0/ropnroll/solve/pivot.py +52 -0
  26. ropnroll-0.1.0/ropnroll/solve/pool.py +82 -0
  27. ropnroll-0.1.0/ropnroll/solve/srop.py +221 -0
  28. ropnroll-0.1.0/ropnroll/solve/syscallchain.py +65 -0
  29. ropnroll-0.1.0/ropnroll/verify/__init__.py +0 -0
  30. ropnroll-0.1.0/ropnroll/verify/emulate.py +140 -0
  31. ropnroll-0.1.0/ropnroll.egg-info/PKG-INFO +12 -0
  32. ropnroll-0.1.0/ropnroll.egg-info/SOURCES.txt +41 -0
  33. ropnroll-0.1.0/ropnroll.egg-info/dependency_links.txt +1 -0
  34. ropnroll-0.1.0/ropnroll.egg-info/entry_points.txt +2 -0
  35. ropnroll-0.1.0/ropnroll.egg-info/requires.txt +8 -0
  36. ropnroll-0.1.0/ropnroll.egg-info/top_level.txt +1 -0
  37. ropnroll-0.1.0/setup.cfg +4 -0
  38. ropnroll-0.1.0/tests/test_libcdb.py +18 -0
  39. ropnroll-0.1.0/tests/test_onegadget.py +12 -0
  40. ropnroll-0.1.0/tests/test_scanner.py +62 -0
  41. ropnroll-0.1.0/tests/test_semantics.py +67 -0
  42. ropnroll-0.1.0/tests/test_solver.py +141 -0
  43. ropnroll-0.1.0/tests/test_srop.py +30 -0
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: ropnroll
3
+ Version: 0.1.0
4
+ Summary: Semantic ROP/JOP gadget finder and automatic exploit-chain synthesizer
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: capstone>=5.0
7
+ Requires-Dist: lief>=0.14
8
+ Requires-Dist: unicorn>=2.0
9
+ Requires-Dist: rich>=13.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == "dev"
12
+ Requires-Dist: keystone-engine>=0.9; extra == "dev"
@@ -0,0 +1,274 @@
1
+ # ropnroll
2
+
3
+ A ROP/JOP gadget finder and **automatic exploit-chain synthesizer**. You give it
4
+ a binary (and optionally a library it links against); it gives you a verified,
5
+ ready-to-fire chain for "call this function with these arguments," "make this
6
+ syscall," "pop a shell via a single sigreturn frame," "pivot the stack," or a
7
+ raw semantic gadget search — grounded in real CPU emulation at every step, not
8
+ a modeled instruction set.
9
+
10
+ Built for the case in the brief: you already have an arbitrary read/write
11
+ primitive (or a straightforward stack overflow) and want the rest of exploit
12
+ construction — register setup, calling convention, ASLR-aware addressing,
13
+ chain correctness — handled for you. It's been fired for real against a live
14
+ process (see [Live-fire demo](#live-fire-demo)), not just checked against a
15
+ static binary.
16
+
17
+ ## Support matrix
18
+
19
+ Honest, not aspirational — "tested" means an automated test in `tests/`
20
+ actually exercises it against a real binary, every run.
21
+
22
+ | | ELF (Linux) | PE (Windows) |
23
+ |---|---|---|
24
+ | **x86-64** | ✅ tested — scanner, solver, call/syscall/SROP chains, live-fire exploit, verifier | ✅ tested — loading, mitigations (incl. Control Flow Guard), scanning, MS x64 ABI (RCX/RDX/R8/R9 + shadow space) |
25
+ | **x86 (32-bit)** | 🟡 wired up, scanner-level tested; solver/chain builders share the same code path as x86-64 but aren't exercised by an automated test against a real 32-bit target | ✅ tested — loading, mitigations, scanning (cdecl/stdcall stack-arg calling convention) |
26
+ | **ARM32 / ARM64** | 🟡 scanner + semantic engine wired up (shares the Unicorn-measured-effects design with x86), not exercised by an automated test against a real ARM binary this session | — |
27
+ | **MIPS32 / MIPS64** | 🟡 scanner + semantic engine wired up, including branch-delay-slot handling; not exercised by an automated test this session | — |
28
+ | **RISC-V / PowerPC / Thumb** | ❌ not supported | — |
29
+
30
+ SROP is Linux x86-64 only (it's a Linux kernel ABI detail, not portable). The
31
+ one-gadget finder and libc.rip lookup are scoped to x86/x86-64. Everything in
32
+ the ✅ row has a corresponding test in `tests/` that runs against a real
33
+ binary — not a mock — every time the suite runs.
34
+
35
+ ## Why another one of these
36
+
37
+ Market research before writing any code, plus a live head-to-head:
38
+
39
+ | Tool | Approach | Notable limitation ropnroll targets |
40
+ |---|---|---|
41
+ | **Ropper** / **ROPgadget** | Syntactic/regex gadget search | No semantics at all — you read disassembly and build chains by hand |
42
+ | **angrop** (angr) | Symbolic execution via claripy | Powerful but slow (minutes on libc-sized targets per public benchmarks), no standalone CLI |
43
+ | **ROPium** | Custom IR + semantic queries (`rax=rbx+8`) | Gadget-level chaining only; no concrete verification, no JOP synthesis |
44
+ | **Ropinator** | Z3-backed solver + "affine executor" fast path, TUI + MCP server | Closest competitor — see below |
45
+ | **wedp** | WinDbg plugin for interactive Windows exploit dev | Different category: live-debugger-assisted workflow, not offline gadget/chain synthesis |
46
+ | **one_gadget** | Curated, per-glibc-version static analysis of source | Needs a maintained version database; ropnroll's `onegadget` instead concretely emulates forward from shell-string references, no database, works on whatever you hand it |
47
+ | **libc-database** / pwntools `libcdb` | Local database you build and query | ropnroll's `libcid` hits libc.rip directly — no local DB to maintain |
48
+
49
+ **Ropinator specifically** (`pip install ropinator`) is the tool ropnroll has the
50
+ most in common with, so I installed it and ran it head-to-head rather than
51
+ guessing. On `libc.so.6` at matched depth: Ropinator found 63,964 gadgets in
52
+ 20s wall-clock (using ~2m39s of CPU across threads); ropnroll found 34,235 in
53
+ 15.9s single-threaded, pure Python. The gap isn't random — **13,971 (~22%) of
54
+ Ropinator's results contain a mid-body conditional jump** (`je`, `jne`, ...),
55
+ which ropnroll's scanner deliberately excludes: a gadget whose body can branch
56
+ away before reaching its own terminator isn't a gadget you can rely on to do
57
+ what its disassembly suggests. That's a real trade — fewer total matches for a
58
+ hard reliability guarantee on every one that's returned.
59
+
60
+ Where ropnroll tries to actually add something new, not just re-implement:
61
+
62
+ 1. **Semantics measured by real emulation, not modeled.** Every gadget's
63
+ effect (which register gets what value, which memory gets written) is
64
+ determined by actually running it in Unicorn across several input vectors
65
+ and fitting the relation — correct for whatever the silicon does,
66
+ automatically, with no per-opcode IR to write or get wrong.
67
+ 2. **Concrete chain verification.** Once a chain is built, ropnroll re-emulates
68
+ the *whole thing* against the real target's mapped memory and reports
69
+ pass/fail per goal register, plus exactly which gadget faulted. This isn't
70
+ a nicety — building this tool, the verifier (and a live-fire test against
71
+ a real process) caught five real bugs in the solver, none of which would
72
+ have been caught by inspection. See "How this was validated."
73
+ 3. **A register/call/syscall solver that goes past one hop.** Backward-
74
+ chaining search with memoization and a work budget, not just "pop-style
75
+ gadget or bail" — finds multi-gadget indirection paths (`pop reg1` →
76
+ `mov reg2, reg1` → `mov target, reg2`) when no direct popper exists, and
77
+ explicitly rejects any candidate that turns out to be a disguised stack
78
+ pivot or that touches memory through an uncontrolled register.
79
+ 4. **SROP as a first-class goal**, not an afterthought: one sigreturn frame
80
+ can set *every* register in a single step, including chaining straight
81
+ into execve with zero conventional gadgets beyond a single `pop rax`.
82
+ 5. **JOP is actually synthesized, not just tagged.** `ropnroll jop` finds
83
+ *dispatcher* gadgets in the Bletsch et al. sense — an indirect jump/call
84
+ through a register that the same gadget also advances by a constant — by
85
+ asking the semantic engine directly, and can build the function-pointer
86
+ table that turns one into an arbitrary-length JOP chain.
87
+ 6. **CET/CFG-aware by construction.** Under Intel CET-IBT or Windows Control
88
+ Flow Guard, an indirect branch may only land on an approved entry point.
89
+ The JOP dispatcher builder checks a PE's real Guard CF function table
90
+ (read via LIEF) instead of silently producing a chain that dies on the
91
+ first hop on a hardened target.
92
+ 7. **Empirical one-gadget discovery.** No curated glibc-version database:
93
+ finds every place the binary's own code loads a shell-string pointer, and
94
+ concretely emulates forward (with syscalls it can't service — `clone`,
95
+ `mmap` — faked plausibly so real code paths like modern glibc's
96
+ `posix_spawn`-based `system()` can be walked) to confirm a real execve
97
+ fires. A hit is observed, not guessed.
98
+ 8. **libc identification** against libc.rip from as few as 2–3 leaked symbol
99
+ offsets, with the exact build's full symbol table and a download link, in
100
+ one call.
101
+ 9. **Live-target orchestration.** Attach to a running process via
102
+ `/proc/<pid>/maps` (no ptrace, no special privileges), rebase gadget pools
103
+ to the *actual* runtime addresses, and hand back an `Image` the rest of
104
+ the tool uses exactly like a static file. `examples/live_fire_demo.py`
105
+ uses this to build a chain and pop a real shell in a real subprocess.
106
+
107
+ What I'm *not* claiming to beat Ropinator on: it already ships a TUI and an
108
+ MCP server for agent-native use, and covers RISC-V/PowerPC/Thumb, which this
109
+ project doesn't attempt (see the support matrix above — a deliberate scope
110
+ decision, not an oversight). It's also a shipped, presumably battle-tested
111
+ tool; this was built and tested in one sitting against real binaries and a
112
+ real live process, not years of field use.
113
+
114
+ ## Install
115
+
116
+ ```bash
117
+ # pip
118
+ pip install -e .
119
+
120
+ # uv (fast, and what this repo's CI/dev workflow assumes)
121
+ uv pip install -e .
122
+ # or, without an explicit venv step:
123
+ uv sync # if you're working from a clone and want a project-local .venv
124
+
125
+ # uvx: run the CLI directly without a persistent install
126
+ uvx --from git+https://github.com/jordanallred/ropnroll ropnroll security ./target
127
+
128
+ # once a release is published to PyPI (tag a v* release to trigger it):
129
+ uvx ropnroll security ./target
130
+ ```
131
+
132
+ Needs `capstone`, `lief`, `unicorn`, `rich` — all pulled in automatically.
133
+ `libcid` uses the standard library's `urllib` (no extra dependency).
134
+
135
+ ## Usage
136
+
137
+ ```bash
138
+ # security/mitigation report (NX, PIE, RELRO, canary, CFG for PE...)
139
+ ropnroll security ./target
140
+
141
+ # raw gadget listing (like Ropper/ROPgadget, but conditional-jump-free by default)
142
+ ropnroll scan ./target --regex "pop r.i"
143
+
144
+ # semantic search, ROPium-style DSL
145
+ ropnroll search ./target ./libc.so.6 --query "rdi=rax+8"
146
+
147
+ # stack pivots / JOP dispatcher gadgets
148
+ ropnroll pivot ./target
149
+ ropnroll jop ./libc.so.6
150
+
151
+ # build + verify a ret2libc call, emit a pwntools-ready payload
152
+ ropnroll call ./target ./libc.so.6 --target system --args 0x1cc42f \
153
+ --verify --emit pwntools
154
+
155
+ # build + verify a direct execve("/bin/sh", NULL, NULL) syscall chain
156
+ ropnroll syscall ./libc.so.6 --nr 59 --args 0x1cc42f,0,0 --verify --emit pwntools
157
+
158
+ # a sigreturn frame that pops a shell with a single gadget, period
159
+ ropnroll srop ./libc.so.6 --args 0x1cc42f,0,0 --verify --emit pwntools
160
+
161
+ # empirically confirmed one-gadgets (no curated database)
162
+ ropnroll onegadget ./libc.so.6
163
+
164
+ # identify a libc build from a couple of leaked symbol offsets
165
+ ropnroll libcid --symbol system=0x58750 --symbol read=0x11bd20 --download /tmp/libc.so.6
166
+ ```
167
+
168
+ `call`, `syscall`, and `srop` accept multiple binaries (target + libc) and
169
+ pool their gadgets together, same as a real exploit would pull from both.
170
+
171
+ ## Live-fire demo
172
+
173
+ `examples/live_fire_demo.py` doesn't just build a chain — it spawns the real
174
+ vulnerable test binary, attaches to it, reads its *actual* runtime memory map
175
+ (standing in for whatever leak primitive gets you the same information
176
+ against a real target), rebases ropnroll's gadget pool to those live
177
+ addresses, builds a ret2libc chain with the solver, delivers it through the
178
+ real stack overflow, and confirms a real shell by running a command in it and
179
+ reading the output back:
180
+
181
+ ```
182
+ [*] live binary base : 0x555555554000
183
+ [*] live libc base : 0x7ffff7c00000
184
+ [*] building the call chain with ropnroll's own solver (not hand-picked)...
185
+ pop-style: pop rdi ; ret @ 0x7ffff7d0c08d sets ['rdi']
186
+ [*] payload is 104 bytes (72 junk + 32 chain)
187
+ [+] SHELL CONFIRMED -- got real code execution via a chain ropnroll built itself
188
+ ```
189
+
190
+ (This sandbox has ASLR disabled system-wide — no root available to change it
191
+ — so the "defeated" addresses happen to match the static file. The code path
192
+ reads live addresses from the live process either way, so the mechanism is
193
+ real; there just isn't a randomized target to prove it against here.)
194
+
195
+ ## How this was validated
196
+
197
+ Nothing here is asserted without having actually run it — against real
198
+ binaries, a real libc, and in one case a real running process:
199
+
200
+ - Scanner + semantic engine run against `/lib/x86_64-linux-gnu/libc.so.6` (a
201
+ real, unmodified system libc), freshly-`gcc`-compiled x86-64 test binaries,
202
+ and real 32-bit/64-bit Windows PE executables — loading, mitigation
203
+ detection (including Control Flow Guard), and gadget scanning all
204
+ confirmed.
205
+ - The register solver, call/syscall/SROP chain builders were used to build
206
+ an actual `system("/bin/sh")` ret2libc chain, a direct `execve` syscall
207
+ chain, and a single-gadget SROP execve chain — sourced *entirely* from
208
+ libc's own gadgets, no hand-picked addresses — then concretely
209
+ re-emulated against libc's real mapped memory via `--verify`. All pass.
210
+ - The one-gadget finder found and concretely confirmed a real one-gadget in
211
+ system libc by emulating through actual modern glibc `posix_spawn`
212
+ internals (not a toy code path).
213
+ - The libc.rip lookup was checked against a real leak of this exact libc's
214
+ `system`/`read`/`printf` offsets and correctly identified the exact build,
215
+ including the `/bin/sh` string offset matching what the scanner found
216
+ independently.
217
+ - `examples/live_fire_demo.py` spawns the real vulnerable binary, attaches
218
+ via `/proc/<pid>/maps`, and pops a real shell with a chain the solver
219
+ built itself — the strongest verification available short of a randomized
220
+ remote target.
221
+ - Along the way, **five real bugs were found and fixed by this same testing
222
+ loop, not by inspection**: a terminator-classification bug that let
223
+ byte-overlap coincidences masquerade as valid gadgets; a `leave`-based
224
+ pivot gadget getting silently used as a plain register setter; a
225
+ chain-assembly gap that left an unfilled word between gadgets; a solver
226
+ combinatorial-blowup bug once stricter safety checks narrowed the search
227
+ (fixed with memoization + a work budget); and a ret2libc stack-alignment
228
+ gotcha (entering a function via `ret` instead of `call` needs the same
229
+ rsp%16==8 residue, or its own callees crash on the first SSE instruction)
230
+ that the live-fire demo caught by actually crashing until it was fixed.
231
+
232
+ ## Design notes
233
+
234
+ - **Scanner** (`core/scanner.py`): classic backward-window technique for x86
235
+ (misaligned/"unintended" gadgets included, like Ropper/ROPgadget), fixed-
236
+ instruction-width backward stepping for ARM/ARM64/MIPS (including MIPS
237
+ branch-delay-slot handling). Deliberately excludes conditional branches,
238
+ privileged instructions, and segment-register writes from gadget bodies —
239
+ each found because a gadget slipping through actually broke something
240
+ downstream, not preemptively.
241
+ - **Semantic engine** (`semantics/engine.py`): per-gadget effects are inferred
242
+ by running the gadget in Unicorn with several trial register values (basis
243
+ includes 0, all-ones, and a few patterns to catch AND/OR/XOR/scale
244
+ relations, not just copy/add) and fitting a closed-form relation to each
245
+ output register and memory write. Results are exact, not guessed — a
246
+ relation is only accepted if it holds across every trial.
247
+ - **Solver** (`solve/chain.py`): greedy multi-cover search over "pop"-style
248
+ (load-from-stack) gadgets first; falls back to depth-bounded, memoized
249
+ backward-chaining search for register-to-register indirection when no
250
+ direct popper exists. Rejects any candidate that's a disguised stack pivot
251
+ or touches memory through a register nothing set.
252
+ - **Windows ABI awareness**: x86-64 PE targets automatically get the
253
+ Microsoft calling convention (RCX/RDX/R8/R9 + 32 bytes of shadow space),
254
+ not SysV's RDI/RSI/RDX/RCX/R8/R9 — picked from the binary's own format.
255
+ - **Stack-alignment correction**: `build_call` can take `bytes_before_chain`
256
+ (how much padding precedes the chain in the final payload) and will insert
257
+ a single bare `ret` gadget automatically if needed to keep a directly-
258
+ entered function's incoming stack correctly aligned.
259
+
260
+ ## Known limitations / roadmap (being upfront, not shipping silently)
261
+
262
+ - ARM/ARM64/MIPS share the same scanner/semantic-engine design as x86-64 and
263
+ should work the same way, but aren't exercised by an automated test
264
+ against a real binary this session — see the support matrix.
265
+ - RISC-V, PowerPC, and Thumb are explicitly out of scope (a deliberate call
266
+ to focus on the architectures people actually hit, not a gap to fill
267
+ later without reconsidering).
268
+ - Register-value solving is depth-bounded backward-chaining, not full
269
+ BFS/constraint propagation like angrop — handles multi-hop indirection but
270
+ isn't exhaustive.
271
+ - `onegadget`'s syscall-faking (for `clone`/`mmap`/etc.) is a pragmatic
272
+ stand-in for real kernel behavior, not a full OS emulation layer — it gets
273
+ far enough to find real gadgets through modern glibc, but an exotic code
274
+ path could still need a syscall this doesn't stub correctly.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ropnroll"
7
+ version = "0.1.0"
8
+ description = "Semantic ROP/JOP gadget finder and automatic exploit-chain synthesizer"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "capstone>=5.0",
12
+ "lief>=0.14",
13
+ "unicorn>=2.0",
14
+ "rich>=13.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0",
20
+ "keystone-engine>=0.9",
21
+ ]
22
+
23
+ [project.scripts]
24
+ ropnroll = "ropnroll.cli.main:main"
25
+
26
+ [tool.setuptools.packages.find]
27
+ include = ["ropnroll*"]
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["tests"]
File without changes
File without changes
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from rich.console import Console
7
+ from rich.markup import escape
8
+ from rich.table import Table
9
+
10
+ from .. import libcdb
11
+ from ..core import loader, output, scanner, security
12
+ from ..core.gadget import Terminator
13
+ from ..solve import callchain, chain as chainmod, jop, onegadget, pivot, srop, syscallchain
14
+ from ..solve.pool import GadgetPool
15
+ from ..semantics import query as querymod
16
+ from ..verify.emulate import verify_chain
17
+
18
+ console = Console()
19
+
20
+
21
+ def _load_pool(paths: list[str], scan_opts: scanner.ScanOptions) -> tuple[GadgetPool, list]:
22
+ pool = GadgetPool()
23
+ images = []
24
+ for p in paths:
25
+ img = loader.load(p)
26
+ gs = scanner.scan_image(img, scan_opts)
27
+ pool.add(img, gs)
28
+ images.append(img)
29
+ return pool, images
30
+
31
+
32
+ def _scan_opts(args) -> scanner.ScanOptions:
33
+ bad = bytes.fromhex(args.bad_bytes) if getattr(args, "bad_bytes", None) else b""
34
+ return scanner.ScanOptions(
35
+ max_insns=args.max_insns, rop=not args.no_rop, jop=not args.no_jop,
36
+ sys=not args.no_sys, bad_bytes=bad,
37
+ )
38
+
39
+
40
+ def cmd_scan(args):
41
+ pool, images = _load_pool([args.binary], _scan_opts(args))
42
+ gadgets = pool.all()
43
+ if args.regex:
44
+ import re
45
+ rx = re.compile(args.regex)
46
+ gadgets = [g for g in gadgets if rx.search(g.text)]
47
+ gadgets.sort(key=lambda g: g.address)
48
+ if args.out:
49
+ with open(args.out, "w") as f:
50
+ for g in gadgets:
51
+ f.write(f"0x{g.address:016x} : {g.text}\n")
52
+ console.print(f"[green]wrote {len(gadgets)} gadgets to {args.out}[/green]")
53
+ return
54
+ for g in gadgets[: args.limit]:
55
+ console.print(f"0x{g.address:016x} : {escape(g.text)}")
56
+ console.print(f"[bold]{len(gadgets)}[/bold] gadgets total"
57
+ + (f" (showing first {args.limit})" if len(gadgets) > args.limit else ""))
58
+
59
+
60
+ def cmd_security(args):
61
+ img = loader.load(args.binary)
62
+ opts = _scan_opts(args)
63
+ gs = scanner.scan_image(img, opts)
64
+ report = security.build_report(img, gs)
65
+ t = Table(title=f"security report: {args.binary}")
66
+ t.add_column("property")
67
+ t.add_column("value")
68
+ for k, v in report.lines:
69
+ t.add_row(k, v)
70
+ console.print(t)
71
+
72
+
73
+ def cmd_search(args):
74
+ pool, images = _load_pool(args.binary, _scan_opts(args))
75
+ try:
76
+ results = querymod.search(pool, args.query, limit=args.limit)
77
+ except ValueError as e:
78
+ console.print(f"[red]{e}[/red]")
79
+ sys.exit(1)
80
+ if not results:
81
+ console.print("[yellow]no gadgets matched[/yellow]")
82
+ return
83
+ for g, eff, field in results:
84
+ console.print(f"0x{g.address:016x} : {escape(g.text)} [dim]sp_delta={eff.sp_delta}[/dim]")
85
+
86
+
87
+ def cmd_pivot(args):
88
+ pool, images = _load_pool(args.binary, _scan_opts(args))
89
+ pivots = pivot.find_pivots(pool)
90
+ t = Table(title="stack pivots")
91
+ t.add_column("address")
92
+ t.add_column("gadget")
93
+ t.add_column("kind")
94
+ for p in pivots[: args.limit]:
95
+ t.add_row(f"0x{p.gadget.address:x}", escape(p.gadget.text), p.kind)
96
+ console.print(t)
97
+
98
+
99
+ def cmd_jop(args):
100
+ pool, images = _load_pool(args.binary, _scan_opts(args))
101
+ disp = jop.find_dispatchers(pool, img=images[0])
102
+ t = Table(title="JOP dispatcher gadgets (self-advancing jmp/call-through-register)")
103
+ t.add_column("address")
104
+ t.add_column("gadget")
105
+ t.add_column("reg")
106
+ t.add_column("advance")
107
+ for d in disp[: args.limit]:
108
+ t.add_row(f"0x{d.gadget.address:x}", escape(d.gadget.text), d.reg, f"0x{d.advance:x}")
109
+ console.print(t)
110
+ console.print(f"[bold]{len(disp)}[/bold] dispatcher(s) found")
111
+
112
+
113
+ def cmd_onegadget(args):
114
+ img = loader.load(args.binary)
115
+ if img.arch not in ("x86_64", "x86"):
116
+ console.print(f"[red]one-gadget search is scoped to x86/x86-64 (target is {img.arch})[/red]")
117
+ sys.exit(1)
118
+ console.print("[dim]searching for shell-string references and emulating forward "
119
+ "from each one -- this concretely runs the real code, so it can take "
120
+ "a few seconds...[/dim]")
121
+ rep = onegadget.find_one_gadgets(img, max_insns=args.max_insns)
122
+ t = Table(title="one-gadgets (empirically confirmed via emulation, not a curated database)")
123
+ t.add_column("address")
124
+ t.add_column("string")
125
+ t.add_column("starting state that worked")
126
+ t.add_column("instructions run")
127
+ for c in rep.candidates:
128
+ t.add_row(f"0x{c.address:x}", c.string.decode(), c.constraint, str(c.instructions_run))
129
+ console.print(t)
130
+ console.print(f"searched {rep.searched_sites} shell-string reference site(s), "
131
+ f"[bold]{len(rep.candidates)}[/bold] confirmed")
132
+
133
+
134
+ def cmd_libcid(args):
135
+ offsets = {}
136
+ for item in args.symbol:
137
+ name, _, off = item.partition("=")
138
+ if not off:
139
+ console.print(f"[red]expected name=0xoffset, got {item!r}[/red]")
140
+ sys.exit(1)
141
+ offsets[name] = int(off, 0)
142
+ try:
143
+ matches = libcdb.identify(offsets)
144
+ except ConnectionError as e:
145
+ console.print(f"[red]{e}[/red]")
146
+ sys.exit(1)
147
+ if not matches:
148
+ console.print("[yellow]no matching libc build found[/yellow]")
149
+ return
150
+ for m in matches:
151
+ console.print(f"[bold]{m.id}[/bold] buildid={m.buildid}")
152
+ console.print(f" download: {m.download_url}")
153
+ for k, v in sorted(m.symbols.items()):
154
+ console.print(f" {k} = 0x{v:x}")
155
+ if args.download:
156
+ if len(matches) > 1:
157
+ console.print(f"[yellow]{len(matches)} matches -- downloading the first ({matches[0].id})[/yellow]")
158
+ path = libcdb.download(matches[0], args.download)
159
+ console.print(f"[green]saved to {path}[/green]")
160
+
161
+
162
+ def cmd_srop(args):
163
+ pool, images = _load_pool(args.binary, _scan_opts(args))
164
+ if pool.ai.arch != "x86_64":
165
+ console.print("[red]SROP support here is scoped to Linux x86-64[/red]")
166
+ sys.exit(1)
167
+ args_list = [int(a, 0) for a in args.args.split(",")] if args.args else [0, 0, 0]
168
+ while len(args_list) < 3:
169
+ args_list.append(0)
170
+ res = srop.build_srop_execve(pool, path_ptr=args_list[0], argv_ptr=args_list[1], envp_ptr=args_list[2])
171
+ for l in res.solve.log:
172
+ console.print(f"[dim]{l}[/dim]")
173
+ if not res.ok:
174
+ console.print(f"[red]could not build SROP chain: {res.solve.unresolved}[/red]")
175
+ sys.exit(1)
176
+ console.print(output.stack_layout(res.chain), markup=False)
177
+ expect = {"rax": 59, "rdi": args_list[0], "rsi": args_list[1], "rdx": args_list[2]}
178
+ consistent = srop.verify_srop_frame(res.chain, res.frame_offset_words, expect)
179
+ console.print(f"\nframe self-consistency check: {'[green]OK[/green]' if consistent else '[red]FAIL[/red]'}")
180
+ if args.verify:
181
+ ok, result = srop.emulate_srop_chain(images[0], res.chain, expect)
182
+ console.print(f"emulated (simulated sigreturn semantics -- see srop.py docstring): "
183
+ f"{'[green]OK[/green]' if ok else '[red]FAIL[/red]'}")
184
+ if args.emit:
185
+ _emit(res.chain, args.emit, args.out)
186
+
187
+
188
+ def _emit(chain, fmt: str, out: str | None):
189
+ if fmt == "pwntools":
190
+ text = output.to_pwntools(chain)
191
+ elif fmt == "c":
192
+ text = output.to_c_array(chain)
193
+ elif fmt == "json":
194
+ text = output.to_json(chain)
195
+ elif fmt == "raw":
196
+ data = output.to_raw(chain)
197
+ if out:
198
+ with open(out, "wb") as f:
199
+ f.write(data)
200
+ console.print(f"[green]wrote {len(data)} raw bytes to {out}[/green]")
201
+ else:
202
+ sys.stdout.buffer.write(data)
203
+ return
204
+ else:
205
+ raise ValueError(fmt)
206
+ if out:
207
+ with open(out, "w") as f:
208
+ f.write(text)
209
+ console.print(f"[green]wrote to {out}[/green]")
210
+ else:
211
+ console.print(text, markup=False)
212
+
213
+
214
+ def _do_verify(images, chain, final_target, goal_regs):
215
+ rep = verify_chain(images[0], chain, final_target=final_target, goal_regs=goal_regs)
216
+ console.print(f"\n[bold]-- concrete verification (Unicorn) --[/bold]")
217
+ console.print(f"reached target: {rep.reached_target} fault: {rep.fault} "
218
+ f"insns executed: {rep.instructions_executed}")
219
+ for reg, (ok, want, got) in rep.goal_results.items():
220
+ mark = "[green]OK[/green]" if ok else "[red]MISMATCH[/red]"
221
+ console.print(f" {reg}: expected 0x{want:x}, got "
222
+ f"{'0x%x' % got if got is not None else '?'} {mark}")
223
+ if rep.fault:
224
+ console.print(f"[red]fault at 0x{rep.fault_address:x} near: {rep.last_gadget_context}[/red]")
225
+ console.print(f"[bold]{'PASS' if rep.ok else 'FAIL'}[/bold]")
226
+
227
+
228
+ def cmd_call(args):
229
+ pool, images = _load_pool(args.binary, _scan_opts(args))
230
+ target = int(args.target, 0) if args.target.startswith("0x") or args.target.isdigit() else None
231
+ if target is None:
232
+ for img in images:
233
+ if args.target in img.symbols:
234
+ target = img.symbols[args.target]
235
+ break
236
+ if target is None:
237
+ console.print(f"[red]symbol {args.target!r} not found[/red]")
238
+ sys.exit(1)
239
+ args_list = [int(a, 0) for a in args.args.split(",")] if args.args else []
240
+ res = callchain.build_call(pool, target, args_list)
241
+ for l in res.solve.log:
242
+ console.print(f"[dim]{l}[/dim]")
243
+ if not res.ok:
244
+ console.print(f"[red]could not build call chain; unresolved: {res.solve.unresolved}[/red]")
245
+ sys.exit(1)
246
+ console.print(output.stack_layout(res.chain), markup=False)
247
+ if args.emit:
248
+ _emit(res.chain, args.emit, args.out)
249
+ if args.verify:
250
+ goal = {r: v for r, v in zip(pool.ai.call_arg_regs, args_list)}
251
+ _do_verify(images, res.chain, target, goal)
252
+
253
+
254
+ def cmd_syscall(args):
255
+ pool, images = _load_pool(args.binary, _scan_opts(args))
256
+ args_list = [int(a, 0) for a in args.args.split(",")] if args.args else []
257
+ res = syscallchain.build_syscall(pool, args.nr, args_list)
258
+ for l in res.solve.log:
259
+ console.print(f"[dim]{l}[/dim]")
260
+ if not res.ok:
261
+ console.print(f"[red]could not build syscall chain; unresolved: {res.solve.unresolved}[/red]")
262
+ sys.exit(1)
263
+ console.print(output.stack_layout(res.chain), markup=False)
264
+ if args.emit:
265
+ _emit(res.chain, args.emit, args.out)
266
+ if args.verify:
267
+ _do_verify(images, res.chain, res.gadget_addr, {})
268
+
269
+
270
+ def build_parser() -> argparse.ArgumentParser:
271
+ p = argparse.ArgumentParser(prog="ropnroll", description="ROP/JOP gadget finder and chain synthesizer")
272
+ common = argparse.ArgumentParser(add_help=False)
273
+ common.add_argument("--max-insns", type=int, default=6)
274
+ common.add_argument("--no-rop", action="store_true")
275
+ common.add_argument("--no-jop", action="store_true")
276
+ common.add_argument("--no-sys", action="store_true")
277
+ common.add_argument("--bad-bytes", help="hex string, e.g. 000a0d")
278
+
279
+ sub = p.add_subparsers(dest="cmd", required=True)
280
+
281
+ s = sub.add_parser("scan", parents=[common])
282
+ s.add_argument("binary")
283
+ s.add_argument("--regex")
284
+ s.add_argument("--limit", type=int, default=50)
285
+ s.add_argument("--out")
286
+ s.set_defaults(func=cmd_scan)
287
+
288
+ s = sub.add_parser("security", parents=[common])
289
+ s.add_argument("binary")
290
+ s.set_defaults(func=cmd_security)
291
+
292
+ s = sub.add_parser("search", parents=[common])
293
+ s.add_argument("binary", nargs="+")
294
+ s.add_argument("--query", required=True, help='e.g. "rdi=rax+8", "[rbx]=rax", "rax=0"')
295
+ s.add_argument("--limit", type=int, default=20)
296
+ s.set_defaults(func=cmd_search)
297
+
298
+ s = sub.add_parser("pivot", parents=[common])
299
+ s.add_argument("binary", nargs="+")
300
+ s.add_argument("--limit", type=int, default=30)
301
+ s.set_defaults(func=cmd_pivot)
302
+
303
+ s = sub.add_parser("jop", parents=[common])
304
+ s.add_argument("binary", nargs="+")
305
+ s.add_argument("--limit", type=int, default=30)
306
+ s.set_defaults(func=cmd_jop)
307
+
308
+ s = sub.add_parser("onegadget")
309
+ s.add_argument("binary")
310
+ s.add_argument("--max-insns", type=int, default=2000,
311
+ help="instruction budget per candidate site during emulation")
312
+ s.set_defaults(func=cmd_onegadget)
313
+
314
+ s = sub.add_parser("srop", parents=[common], help="build a sigreturn-oriented execve chain")
315
+ s.add_argument("binary", nargs="+")
316
+ s.add_argument("--args", default="", help="path_ptr,argv_ptr,envp_ptr (comma-separated)")
317
+ s.add_argument("--emit", choices=["pwntools", "raw", "c", "json"])
318
+ s.add_argument("--out")
319
+ s.add_argument("--verify", action="store_true")
320
+ s.set_defaults(func=cmd_srop)
321
+
322
+ s = sub.add_parser("libcid", help="identify a libc build from leaked symbol offsets via libc.rip")
323
+ s.add_argument("--symbol", action="append", required=True,
324
+ help="name=0xoffset (offset from the leaked libc's own base), repeatable")
325
+ s.add_argument("--download", metavar="PATH", help="save the (first) matched libc here")
326
+ s.set_defaults(func=cmd_libcid)
327
+
328
+ s = sub.add_parser("call", parents=[common])
329
+ s.add_argument("binary", nargs="+")
330
+ s.add_argument("--target", required=True, help="address (0x...) or symbol name")
331
+ s.add_argument("--args", default="", help="comma-separated integers, e.g. 0x1000,0,0")
332
+ s.add_argument("--emit", choices=["pwntools", "raw", "c", "json"])
333
+ s.add_argument("--out")
334
+ s.add_argument("--verify", action="store_true")
335
+ s.set_defaults(func=cmd_call)
336
+
337
+ s = sub.add_parser("syscall", parents=[common])
338
+ s.add_argument("binary", nargs="+")
339
+ s.add_argument("--nr", type=int, required=True)
340
+ s.add_argument("--args", default="")
341
+ s.add_argument("--emit", choices=["pwntools", "raw", "c", "json"])
342
+ s.add_argument("--out")
343
+ s.add_argument("--verify", action="store_true")
344
+ s.set_defaults(func=cmd_syscall)
345
+
346
+ return p
347
+
348
+
349
+ def main(argv=None):
350
+ parser = build_parser()
351
+ args = parser.parse_args(argv)
352
+ args.func(args)
353
+
354
+
355
+ if __name__ == "__main__":
356
+ main()
File without changes