Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 56 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,29 @@ rop3 is a tool developed in [Python](https://www.python.org/downloads/) and it r

## Features

- **Multi-format, multi-arch**: analyzes ELF, PE and Mach-O binaries (x86 and x86-64). For fat/universal Mach-O binaries, `--arch` selects the slice to analyze.
- **Gadget search**: ROP, JOP and RETF gadgets, with controls for search depth (`--depth`), undeterministic gadgets (`--allow-undeterministic-gadgets`) and complex memory operands (`--allow-complex-memory-ops`).
- **Operations and ROP chains**: search for high-level operations (`--op`/`--dst`/`--src`) and build ROP chains from a ROPLang file (`--ropchain`, `--exhaustive`), including multi-step composite operations.
- **Multi-format, multi-arch**: analyzes ELF, PE and Mach-O binaries across **x86, x86-64, AArch64 (ARM64) and RISC-V (RV64, including the compressed RVC extension)**. Architecture is detected from the binary; for fat/universal Mach-O binaries, `--arch` selects the slice to analyze.
- **Gadget search**: ROP, JOP and RETF gadgets, with controls for search depth (`--depth`, architecture-specific by default), `ret <imm>`/`retf <imm>` terminators (`--ret-imm`, off by default), undeterministic gadgets (`--allow-undeterministic-gadgets`) and complex memory operands (`--allow-complex-memory-ops`).
- **Framed gadget search** (`--frame`/`--no-frame`): on AArch64 and RISC-V, where the return address lives in a register, framed search (on by default) keeps only gadgets that restore it from the stack — the ones actually reachable in a ROP chain. It has no effect on x86, where `ret` already consumes the stack.
- **Operations and ROP chains**: search for high-level operations with `--op` and **positional, n-ary operands** (`--operands op1 op2 op3`), and build ROP chains from a ROPLang file (`--ropchain`, `--exhaustive`), including multi-step composite operations. `--keep-contradictory` disables the filtering of gadgets whose destination is overwritten before the terminator; `--reg-aliases` lets sub-registers (`al`, `ax`, `eax`) stand in for their full register.
- **Relocation**: rebase any binary (ELF/PE/Mach-O) with `--base`, one address per binary.
- **Bad-char filtering**: avoid bytes in the gadget address (`--badchar`) and/or in the gadget opcode bytes (`--badchar-bytes`). By default, duplicate gadgets prefer canary-free addresses (`0x00`, `0x0a`, `0x0d`, `0xff`); disable with `--keep-canary-address`.
- **Symbol annotation**: with `--symbols`, each gadget is tagged with the nearest symbol (`name+offset`) when the binary is not stripped.
- **Output formats**: human-readable text (default), or machine-readable `--output json`/`--output csv` for scripting. Colors are emitted only on a TTY.
- **Output formats**: human-readable text (default), machine-readable `--output json`/`--output csv` for scripting, or `--tuple` for a compact `<op, operands, written regs, read regs>` line per gadget. Colors are emitted only on a TTY. With `--verbose`, rop3 also prints a per-binary summary (format, architecture, bit width, instruction alignment and executable sections).
- **Interactive mode**: `--interactive` scans the binary once and drops into a REPL to explore gadgets, operations and chains without re-scanning.
- **Performance**: parallel scanning across processes (`--jobs N`) and an optional on-disk gadget cache (`--cache`) for repeated runs over the same file.
- **Library API**: use rop3 programmatically through the `Rop3` class (see [Use as a library](#use-as-a-library)).

## Supported architectures and formats

| Architecture | ELF | PE | Mach-O | Notes |
| --- | :---: | :---: | :---: | --- |
| x86 (i386) | ✓ | ✓ | ✓ | |
| x86-64 | ✓ | ✓ | ✓ | |
| AArch64 | ✓ | ✓ | ✓ | 4-byte instruction alignment; framed search on by default |
| RISC-V (RV64)| ✓ | | | 4-byte alignment, or 2-byte with the compressed (RVC) extension; framed search on by default |

Format is detected by magic bytes and the architecture from the binary's own headers. RISC-V is 64-bit only (RV32 is rejected). The high-level operations are defined per architecture, so some operations are unavailable on some targets — for example, the carry-flag operations (`eqc`, `ltc`, `gcf-eqc`, `gcf-ltc`) are not available on RISC-V, which has no condition/carry flags.

## Installation

We recommend to install rop3's dependencies with [pip](https://pypi.org/project/pip/) in a virtual environment to not to mess up with your current configuration:
Expand All @@ -45,25 +57,48 @@ Now, you can install dependencies in [requirements.txt](requirements.txt):

## Usage

```Shell
$ python rop3.py --binary /bin/ls # dump gadgets
$ python rop3.py --binary /bin/ls --op mov --operands rdi rax
$ python rop3.py --binary libaarch64.so --op mov --operands x0 x1
$ python rop3.py --binary libc.so.6 --ropchain chain.txt
$ python rop3.py --binary /bin/ls --interactive # REPL, scans once
```
usage: rop3.py [-h] [-v] [--depth <bytes>] [--all] [--rop | --no-rop] [--retf | --no-retf] [--jop | --no-jop] [--allow-undeterministic-gadgets] [--allow-complex-memory-ops] [--verbose]
[--binary <file> [<file> ...]] [--badchar <hex> [<hex> ...]] [--badchar-bytes <hex> [<hex> ...]] [--keep-canary-address] [--base <hex> [<hex> ...]] [--arch <name>] [--symbols]
[--output {text,json,csv}] [--op <op>] [--dst <reg>] [--src <reg>] [--ropchain <file>] [--exhaustive | --no-exhaustive] [--interactive] [--jobs <n>] [--cache] [--cache-dir <dir>]

This tool allows you to search for gadgets, operations, and ROP chains using a backtracking algorithm in a tree-like structure
```
usage: rop3.py [-h] [-v] [--depth <bytes>] [--all] [--rop | --no-rop]
[--retf | --no-retf] [--ret-imm | --no-ret-imm]
[--jop | --no-jop] [--frame | --no-frame] [--reg-aliases]
[--allow-undeterministic-gadgets] [--allow-complex-memory-ops]
[--keep-contradictory] [--verbose]
[--binary <file> [<file> ...]] [--badchar <hex> [<hex> ...]]
[--badchar-bytes <hex> [<hex> ...]] [--keep-canary-address]
[--base <hex> [<hex> ...]] [--arch <name>] [--symbols]
[--output {text,json,csv}] [--tuple] [--op <op>]
[--operands <reg> [<reg> ...]] [--ropchain <file>]
[--exhaustive | --no-exhaustive] [--interactive] [--jobs <n>]
[--cache] [--cache-dir <dir>]

This tool allows you to search for gadgets, operations, and ROP chains using a
backtracking algorithm in a tree-like structure

options:
-h, --help show this help message and exit
-v, --version display rop3.py's version and exit
--depth <bytes> depth for search engine (default to 5 bytes)
--depth <bytes> maximum gadget length in bytes (default: architecture-specific)
--all show the same gadget in different addresses
--rop, --no-rop search for ROP gadgets
--retf, --no-retf search for RETF gadgets
--ret-imm, --no-ret-imm
include gadgets ending in a `ret <imm>` / `retf <imm>` (disabled by default)
--jop, --no-jop search for JOP gadgets
--frame, --no-frame framed gadget search (default on): on AArch64/RISC-V keep only gadgets that restore the return address from the stack; no effect on x86
--reg-aliases allow sub-register aliases (al, ax, eax, ...) to substitute their full register when matching operations; they are then treated as the same register for chain assignment and side effects
--allow-undeterministic-gadgets
allow gadgets with conditional branches (e.g. jne) as intermediate instructions
--allow-complex-memory-ops
allow gadgets whose first instruction uses complex memory addressing (e.g. [r1*r2], [r1+r2*s+disp])
--keep-contradictory keep 'contradictory' operation gadgets whose destination register is overwritten before the ret (e.g. `add rax, rbx ; mov rax, rcx ; ret`); by default these are filtered out of --op results
--verbose show progress information (gadget counts, combinations)
--binary <file> [<file> ...]
specify a list of binary path files to analyze
Expand All @@ -79,9 +114,10 @@ options:
--symbols annotate gadgets with the nearest symbol (when the binary is not stripped)
--output {text,json,csv}
output format (default: text)
--tuple print each gadget as the tuple <op_name, op1[, op2], written registers, read registers> (overrides --output text)
--op <op> search for operation
--dst <reg> specify a destination register for the operation
--src <reg> specify a source register for the operation
--operands <reg> [<reg> ...]
operation operands, positionally (op1 op2 op3 ...); e.g. --op mov --operands rdi rax
--ropchain <file> plain text file with a ROP chain
--exhaustive, --no-exhaustive
exhaustive search for ROP chains
Expand All @@ -93,15 +129,15 @@ options:

### Parallel scan

`--jobs N` distributes the gadget scan over `N` worker processes. Each executable section is split into chunks scanned independently, then the results are merged and deduplicated, so the output is identical to a serial run. The speedup is sublinear (the merge, deduplication and sort run in the parent, and there is per-process start-up cost), so it is worth it mainly for large binaries and/or a high `--depth`; on small inputs the process overhead dominates and `--jobs 1` (the default) is faster.
`--jobs N` distributes the gadget scan over `N` worker processes. Each executable section is split into chunks scanned independently, then the results are merged and deduplicated, so the output is identical to a serial run. The speedup is sublinear (the merge, deduplication and sort run in the parent, and there is per-process start-up cost), so it is worth it mainly for large binaries and/or a high `--depth`; on small inputs the process overhead dominates and `--jobs 1` (the default) is faster. Framed architectures (AArch64/RISC-V) scan serially regardless of `--jobs`.

### Gadget cache

With `--cache`, the gadgets discovered for a binary are stored on disk and reused on later runs over the same file and options, skipping the scan. The cache key binds the file content hash and every option that affects the result, so a changed binary or option misses cleanly. This is especially handy for large binaries and for the interactive mode.

```Shell
$ python rop3.py --binary libc.so.6 --cache # first run scans and caches
$ python rop3.py --binary libc.so.6 --cache --op mov --dst rdi --src rax # reuses the cache
$ python rop3.py --binary libc.so.6 --cache --op mov --operands rdi rax # reuses the cache
```

### Interactive mode
Expand All @@ -121,7 +157,7 @@ rop3> chain chain.txt
rop3> quit
```

Commands: `gadgets`/`search [substring]`, `count`, `op <name> [dst] [src]`, `chain <file>`, `help`, `quit`.
Commands: `gadgets`/`search [substring]`, `count`, `op <name> [operands...]`, `chain <file>`, `help`, `quit`.

### Use as a library

Expand All @@ -134,14 +170,17 @@ r = Rop3("libc.so.6", base="0x7f0000000000", symbols=True)
for gadget in r.gadgets():
print(gadget)

r.find_op("mov", dst="rdi", src="rax") # list of matching gadgets
r.ropchain("chain.txt") # iterator over ROP chains
r.find_op("mov", operands=["rdi", "rax"]) # list of matching gadgets
r.ropchain("chain.txt") # iterator over ROP chains

for info in r.describe(): # per-binary summary (arch, bits, sections, ...)
print(info)
```

In the work that we presented in [15th IEEE Workshop on Offensive Technologies (WOOT21)](https://www.ieee-security.org/TC/SP2021/SPW2021/WOOT21/), we used rop3 to evaluate the executional power of Return Oriented Programming in a [subset of most common Windows DLLs](https://drive.google.com/file/d/1gOxUolzrw-xlaW6K-fhzZ7Z-sqxiaZeZ/view?usp=sharing>). Check the [paper](https://drive.google.com/file/d/1Pe7s7bLhJ_20MC-duQ7YiLP-Rx5VCjFK/view?usp=sharing) for further details.

```Shell
$ python rop3.py --binary ../tfg_inf/experiments/dlls/win10x86/SHELL32.dll --op mov --dst eax --src ecx
$ python rop3.py --binary ../tfg_inf/experiments/dlls/win10x86/SHELL32.dll --op mov --operands eax ecx
[SHELL32.dll @ 0x698a474c]: mov eax, ecx ; ret (x97)
[SHELL32.dll @ 0x698dc8c8]: mov eax, ecx ; pop ebx ; leave ; ret (x5) (modifies rbx, rbp)
[SHELL32.dll @ 0x6991a2b1]: mov eax, ecx ; pop ebx ; ret (x4) (modifies rbx)
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
capstone >= 4.0.1
capstone >= 5.0
pefile >= 2019.4.18
pyelftools >= 0.27
pyyaml >= 5.4
Expand Down
41 changes: 25 additions & 16 deletions rop3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import rop3.parser as parser
import rop3.binary as binary
import rop3.ropchain
import rop3.gadfinder as gadfinder

from rop3.api import Rop3

Expand All @@ -40,26 +39,36 @@ def main():
rop = Rop3.from_args(args)

try:
if args.verbose:
kinds = [k for k, on in (('ROP', args.rop), ('JOP', args.jop),
('RETF', args.retf)) if on]
debug.info(f"search: {'+'.join(kinds) or 'none'}, "
f"depth {args.depth if args.depth is not None else 'auto'} bytes, "
f"{args.jobs} job(s)")
for info in rop.describe():
for line in utils.binary_info_lines(info):
debug.info(line)

if args.interactive:
from rop3.interactive import Rop3Shell
Rop3Shell(rop).cmdloop()
elif args.ropchain:
result = rop.ropchain(args.ropchain)
utils.output_ropchains(result, args.output, exhaustive=args.exhaustive)
elif args.op:
result = rop.find_op(args.op, args.dst, args.src)
if result and isinstance(result[0], list):
''' Composite operation: a list of chains '''
if args.output == 'text':
for chain in result:
for gadget in chain:
utils.print_gadget(gadget)
else:
# --tuple renders gadgets as the tuple form; it overrides the
# textual --output (json/csv keep their structured formats).
out_fmt = 'tuple' if args.tuple else args.output

if args.ropchain:
result = rop.ropchain(args.ropchain)
utils.output_ropchains(result, out_fmt, exhaustive=args.exhaustive)
elif args.op:
result = rop.find_op(args.op, operands=args.operands)
if result and isinstance(result[0], list):
''' Composite operation: a list of chains '''
utils.output_ropchains(result, out_fmt, exhaustive=True)
else:
utils.output_ropchains(result, args.output, exhaustive=True)
utils.output_gadgets(result, out_fmt)
else:
utils.output_gadgets(result, args.output)
else:
utils.output_gadgets(rop.gadgets(), args.output)
utils.output_gadgets(rop.gadgets(), out_fmt)
except parser.ParserException as exc:
debug.error(str(exc))
except rop3.ropchain.RopChainNotFound as exc:
Expand Down
32 changes: 26 additions & 6 deletions rop3/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import rop3.gadfinder as gadfinder
from rop3.gadfinder import GadFinder
from rop3.ropchain import RopChain
from rop3.binary import Binary


class Rop3:
Expand All @@ -28,16 +29,17 @@ class Rop3:
from rop3 import Rop3
r = Rop3("libc.so.6", base="0x7f0000000000")
r.gadgets() # list[Gadget]
r.find_op("mov", dst="rdi", src="rax")
r.find_op("mov", ["rdi", "rax"])
r.ropchain("chain.txt")

The discovered gadgets are scanned once and cached on the instance, so
repeated queries (and the interactive mode) do not re-scan the binary.
'''

def __init__(self, binaries, *, depth=gadfinder.DEPTH, rop=True, jop=False,
def __init__(self, binaries, *, depth=None, rop=True, jop=False,
retf=False, all=False, allow_undeterministic=False,
allow_complex_mem=False, avoid_canary=True, base=None,
allow_complex_mem=False, avoid_canary=True, ret_imm=False,
reg_aliases=False, keep_contradictory=False, framed=True, base=None,
badchars=None, badchar_bytes=None, arch=None, symbols=False,
cache=False, cache_dir=None, jobs=1):
self.binaries = [binaries] if isinstance(binaries, str) else list(binaries)
Expand All @@ -62,6 +64,14 @@ def __init__(self, binaries, *, depth=gadfinder.DEPTH, rop=True, jop=False,
flags |= gadfinder.ALLOW_COMPLEX_MEM
if avoid_canary:
flags |= gadfinder.AVOID_CANARY
if ret_imm:
flags |= gadfinder.ALLOW_RET_IMM
if reg_aliases:
flags |= gadfinder.ALLOW_REG_ALIASES
if keep_contradictory:
flags |= gadfinder.KEEP_CONTRADICTORY
if not framed:
flags |= gadfinder.UNFRAMED

self._finder = GadFinder(depth, flags, cache=cache, cache_dir=cache_dir,
jobs=jobs)
Expand Down Expand Up @@ -97,9 +107,19 @@ def gadgets(self, refresh=False):
symbols=self.symbols)
return self._gadgets

def find_op(self, op, dst=None, src=None):
''' Gadgets (or ROP chains, for composite ops) implementing `op`. '''
return self._finder.find_op_from_gadgets(self.gadgets(), op, dst, src)
def describe(self):
''' One metadata dict per input binary (format, architecture, pointer
width, instruction alignment, executable sections). Used for
verbose reporting; does not scan for gadgets. '''
bases = self.base if isinstance(self.base, list) \
else [self.base] * len(self.binaries)
return [Binary(fn, b, self.arch).describe()
for fn, b in zip(self.binaries, bases)]

def find_op(self, op, operands=None):
''' Gadgets (or ROP chains, for compound ops) implementing `op`.
`operands` are positional: op1, op2, op3, ... '''
return self._finder.find_op_from_gadgets(self.gadgets(), op, operands)

def ropchain(self, ropfile):
''' Iterator over ROP chains satisfying the operations in `ropfile`. '''
Expand Down
Loading
Loading