ropnroll 0.1.0__py3-none-any.whl
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.
- ropnroll/__init__.py +0 -0
- ropnroll/cli/__init__.py +0 -0
- ropnroll/cli/main.py +356 -0
- ropnroll/core/__init__.py +0 -0
- ropnroll/core/archinfo.py +145 -0
- ropnroll/core/gadget.py +41 -0
- ropnroll/core/loader.py +316 -0
- ropnroll/core/output.py +42 -0
- ropnroll/core/scanner.py +349 -0
- ropnroll/core/security.py +52 -0
- ropnroll/libcdb.py +54 -0
- ropnroll/orchestrate.py +62 -0
- ropnroll/semantics/__init__.py +0 -0
- ropnroll/semantics/effect.py +143 -0
- ropnroll/semantics/engine.py +489 -0
- ropnroll/semantics/query.py +108 -0
- ropnroll/solve/__init__.py +0 -0
- ropnroll/solve/callchain.py +90 -0
- ropnroll/solve/chain.py +317 -0
- ropnroll/solve/jop.py +91 -0
- ropnroll/solve/onegadget.py +207 -0
- ropnroll/solve/pivot.py +52 -0
- ropnroll/solve/pool.py +82 -0
- ropnroll/solve/srop.py +221 -0
- ropnroll/solve/syscallchain.py +65 -0
- ropnroll/verify/__init__.py +0 -0
- ropnroll/verify/emulate.py +140 -0
- ropnroll-0.1.0.dist-info/METADATA +12 -0
- ropnroll-0.1.0.dist-info/RECORD +32 -0
- ropnroll-0.1.0.dist-info/WHEEL +5 -0
- ropnroll-0.1.0.dist-info/entry_points.txt +2 -0
- ropnroll-0.1.0.dist-info/top_level.txt +1 -0
ropnroll/__init__.py
ADDED
|
File without changes
|
ropnroll/cli/__init__.py
ADDED
|
File without changes
|
ropnroll/cli/main.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Per-architecture Capstone/Unicorn wiring and register tables."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import capstone as cs
|
|
7
|
+
import unicorn as uc
|
|
8
|
+
import unicorn.x86_const as ux86
|
|
9
|
+
import unicorn.arm_const as uarm
|
|
10
|
+
import unicorn.arm64_const as uarm64
|
|
11
|
+
import unicorn.mips_const as umips
|
|
12
|
+
|
|
13
|
+
from .loader import ARCH_X86, ARCH_X86_64, ARCH_ARM, ARCH_ARM64, ARCH_MIPS, ARCH_MIPS64
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ArchInfo:
|
|
18
|
+
arch: str
|
|
19
|
+
bits: int
|
|
20
|
+
cs_arch: int
|
|
21
|
+
cs_mode: int
|
|
22
|
+
uc_arch: int
|
|
23
|
+
uc_mode: int
|
|
24
|
+
sp_reg: str
|
|
25
|
+
ip_reg: str
|
|
26
|
+
gpr: list[str] # general purpose regs usable for semantic fitting
|
|
27
|
+
call_arg_regs: list[str] # argument-passing registers, in order (empty => stack-only)
|
|
28
|
+
ret_reg: str
|
|
29
|
+
reg_width: int # bytes
|
|
30
|
+
uc_reg_const: dict[str, int] # name -> unicorn constant
|
|
31
|
+
max_gadget_bytes: int
|
|
32
|
+
max_gadget_insns: int
|
|
33
|
+
insn_alignment: int # 1 for x86 (byte-granular), 4 for arm64/mips, 2 for thumb/arm-mixed
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _uc_x86_regmap():
|
|
37
|
+
return {
|
|
38
|
+
"rax": ux86.UC_X86_REG_RAX, "rbx": ux86.UC_X86_REG_RBX, "rcx": ux86.UC_X86_REG_RCX,
|
|
39
|
+
"rdx": ux86.UC_X86_REG_RDX, "rsi": ux86.UC_X86_REG_RSI, "rdi": ux86.UC_X86_REG_RDI,
|
|
40
|
+
"rbp": ux86.UC_X86_REG_RBP, "rsp": ux86.UC_X86_REG_RSP,
|
|
41
|
+
"r8": ux86.UC_X86_REG_R8, "r9": ux86.UC_X86_REG_R9, "r10": ux86.UC_X86_REG_R10,
|
|
42
|
+
"r11": ux86.UC_X86_REG_R11, "r12": ux86.UC_X86_REG_R12, "r13": ux86.UC_X86_REG_R13,
|
|
43
|
+
"r14": ux86.UC_X86_REG_R14, "r15": ux86.UC_X86_REG_R15, "rip": ux86.UC_X86_REG_RIP,
|
|
44
|
+
"eflags": ux86.UC_X86_REG_EFLAGS,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _uc_x86_32_regmap():
|
|
49
|
+
return {
|
|
50
|
+
"eax": ux86.UC_X86_REG_EAX, "ebx": ux86.UC_X86_REG_EBX, "ecx": ux86.UC_X86_REG_ECX,
|
|
51
|
+
"edx": ux86.UC_X86_REG_EDX, "esi": ux86.UC_X86_REG_ESI, "edi": ux86.UC_X86_REG_EDI,
|
|
52
|
+
"ebp": ux86.UC_X86_REG_EBP, "esp": ux86.UC_X86_REG_ESP, "eip": ux86.UC_X86_REG_EIP,
|
|
53
|
+
"eflags": ux86.UC_X86_REG_EFLAGS,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _uc_arm_regmap():
|
|
58
|
+
m = {f"r{i}": getattr(uarm, f"UC_ARM_REG_R{i}") for i in range(13)}
|
|
59
|
+
m.update({"sp": uarm.UC_ARM_REG_SP, "lr": uarm.UC_ARM_REG_LR, "pc": uarm.UC_ARM_REG_PC,
|
|
60
|
+
"cpsr": uarm.UC_ARM_REG_CPSR})
|
|
61
|
+
return m
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _uc_arm64_regmap():
|
|
65
|
+
m = {f"x{i}": getattr(uarm64, f"UC_ARM64_REG_X{i}") for i in range(29)}
|
|
66
|
+
m.update({"sp": uarm64.UC_ARM64_REG_SP, "x29": uarm64.UC_ARM64_REG_X29,
|
|
67
|
+
"x30": uarm64.UC_ARM64_REG_X30, "lr": uarm64.UC_ARM64_REG_X30,
|
|
68
|
+
"pc": uarm64.UC_ARM64_REG_PC})
|
|
69
|
+
return m
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _uc_mips_regmap():
|
|
73
|
+
names = ["zero", "at", "v0", "v1", "a0", "a1", "a2", "a3",
|
|
74
|
+
"t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7",
|
|
75
|
+
"s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7",
|
|
76
|
+
"t8", "t9", "k0", "k1", "gp", "sp", "fp", "ra"]
|
|
77
|
+
m = {}
|
|
78
|
+
for n in names:
|
|
79
|
+
const = getattr(umips, f"UC_MIPS_REG_{n.upper()}", None)
|
|
80
|
+
if const is not None:
|
|
81
|
+
m[n] = const
|
|
82
|
+
m["pc"] = umips.UC_MIPS_REG_PC
|
|
83
|
+
return m
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_archinfo(arch: str, little_endian: bool = True) -> ArchInfo:
|
|
87
|
+
if arch == ARCH_X86_64:
|
|
88
|
+
return ArchInfo(
|
|
89
|
+
arch=arch, bits=64, cs_arch=cs.CS_ARCH_X86, cs_mode=cs.CS_MODE_64,
|
|
90
|
+
uc_arch=uc.UC_ARCH_X86, uc_mode=uc.UC_MODE_64,
|
|
91
|
+
sp_reg="rsp", ip_reg="rip",
|
|
92
|
+
gpr=["rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp",
|
|
93
|
+
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"],
|
|
94
|
+
call_arg_regs=["rdi", "rsi", "rdx", "rcx", "r8", "r9"],
|
|
95
|
+
ret_reg="rax", reg_width=8, uc_reg_const=_uc_x86_regmap(),
|
|
96
|
+
max_gadget_bytes=24, max_gadget_insns=6, insn_alignment=1,
|
|
97
|
+
)
|
|
98
|
+
if arch == ARCH_X86:
|
|
99
|
+
return ArchInfo(
|
|
100
|
+
arch=arch, bits=32, cs_arch=cs.CS_ARCH_X86, cs_mode=cs.CS_MODE_32,
|
|
101
|
+
uc_arch=uc.UC_ARCH_X86, uc_mode=uc.UC_MODE_32,
|
|
102
|
+
sp_reg="esp", ip_reg="eip",
|
|
103
|
+
gpr=["eax", "ebx", "ecx", "edx", "esi", "edi", "ebp"],
|
|
104
|
+
call_arg_regs=[], # cdecl/stdcall: stack-based
|
|
105
|
+
ret_reg="eax", reg_width=4, uc_reg_const=_uc_x86_32_regmap(),
|
|
106
|
+
max_gadget_bytes=20, max_gadget_insns=6, insn_alignment=1,
|
|
107
|
+
)
|
|
108
|
+
if arch == ARCH_ARM:
|
|
109
|
+
mode = cs.CS_MODE_ARM if little_endian else (cs.CS_MODE_ARM | cs.CS_MODE_BIG_ENDIAN)
|
|
110
|
+
return ArchInfo(
|
|
111
|
+
arch=arch, bits=32, cs_arch=cs.CS_ARCH_ARM, cs_mode=mode,
|
|
112
|
+
uc_arch=uc.UC_ARCH_ARM, uc_mode=uc.UC_MODE_ARM,
|
|
113
|
+
sp_reg="sp", ip_reg="pc",
|
|
114
|
+
gpr=[f"r{i}" for i in range(13)],
|
|
115
|
+
call_arg_regs=["r0", "r1", "r2", "r3"],
|
|
116
|
+
ret_reg="r0", reg_width=4, uc_reg_const=_uc_arm_regmap(),
|
|
117
|
+
max_gadget_bytes=24, max_gadget_insns=6, insn_alignment=4,
|
|
118
|
+
)
|
|
119
|
+
if arch == ARCH_ARM64:
|
|
120
|
+
return ArchInfo(
|
|
121
|
+
arch=arch, bits=64, cs_arch=cs.CS_ARCH_ARM64, cs_mode=cs.CS_MODE_ARM,
|
|
122
|
+
uc_arch=uc.UC_ARCH_ARM64, uc_mode=uc.UC_MODE_ARM,
|
|
123
|
+
sp_reg="sp", ip_reg="pc",
|
|
124
|
+
gpr=[f"x{i}" for i in range(29)],
|
|
125
|
+
call_arg_regs=["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"],
|
|
126
|
+
ret_reg="x0", reg_width=8, uc_reg_const=_uc_arm64_regmap(),
|
|
127
|
+
max_gadget_bytes=32, max_gadget_insns=6, insn_alignment=4,
|
|
128
|
+
)
|
|
129
|
+
if arch in (ARCH_MIPS, ARCH_MIPS64):
|
|
130
|
+
bits = 64 if arch == ARCH_MIPS64 else 32
|
|
131
|
+
cs_mode = (cs.CS_MODE_MIPS64 if bits == 64 else cs.CS_MODE_MIPS32)
|
|
132
|
+
cs_mode |= (cs.CS_MODE_LITTLE_ENDIAN if little_endian else cs.CS_MODE_BIG_ENDIAN)
|
|
133
|
+
uc_mode = (uc.UC_MODE_MIPS64 if bits == 64 else uc.UC_MODE_MIPS32)
|
|
134
|
+
uc_mode |= (uc.UC_MODE_LITTLE_ENDIAN if little_endian else uc.UC_MODE_BIG_ENDIAN)
|
|
135
|
+
return ArchInfo(
|
|
136
|
+
arch=arch, bits=bits, cs_arch=cs.CS_ARCH_MIPS, cs_mode=cs_mode,
|
|
137
|
+
uc_arch=uc.UC_ARCH_MIPS, uc_mode=uc_mode,
|
|
138
|
+
sp_reg="sp", ip_reg="pc",
|
|
139
|
+
gpr=["v0", "v1", "a0", "a1", "a2", "a3", "t0", "t1", "t2", "t3",
|
|
140
|
+
"t4", "t5", "t6", "t7", "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "t8", "t9"],
|
|
141
|
+
call_arg_regs=["a0", "a1", "a2", "a3"],
|
|
142
|
+
ret_reg="v0", reg_width=8 if bits == 64 else 4, uc_reg_const=_uc_mips_regmap(),
|
|
143
|
+
max_gadget_bytes=32, max_gadget_insns=8, insn_alignment=4,
|
|
144
|
+
)
|
|
145
|
+
raise ValueError(f"no ArchInfo for arch={arch!r}")
|
ropnroll/core/gadget.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Core gadget representation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Terminator(Enum):
|
|
9
|
+
RET = auto()
|
|
10
|
+
RET_IMM = auto()
|
|
11
|
+
JMP_REG = auto()
|
|
12
|
+
CALL_REG = auto()
|
|
13
|
+
JMP_MEM = auto()
|
|
14
|
+
CALL_MEM = auto()
|
|
15
|
+
SYSCALL = auto()
|
|
16
|
+
INT80 = auto()
|
|
17
|
+
OTHER = auto()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Gadget:
|
|
22
|
+
address: int
|
|
23
|
+
raw: bytes
|
|
24
|
+
text: str # "pop rdi ; ret"
|
|
25
|
+
insns: list # list of capstone CsInsn (kept for semantic lifting)
|
|
26
|
+
terminator: Terminator
|
|
27
|
+
module: str = "" # which Image/module this came from (for multi-binary pools)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def size(self) -> int:
|
|
31
|
+
return len(self.raw)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def n_insns(self) -> int:
|
|
35
|
+
return len(self.insns)
|
|
36
|
+
|
|
37
|
+
def __hash__(self):
|
|
38
|
+
return hash((self.module, self.address))
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return f"<Gadget {self.module + '!' if self.module else ''}0x{self.address:x}: {self.text}>"
|