mirror of
https://github.com/david942j/one_gadget.git
synced 2026-10-11 22:09:54 +00:00
Conditional-branch-aware gadget search (#321)
* fetcher: promote disasm index to Base Move disasm_lines/disasm_index (full objdump disassembly + address→index map) from the ARM fetcher into Base so any arch can walk the disassembly, and extract the terminal-call regexp into a helper. Groundwork for control-flow-aware candidate discovery; no behavior change. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Add conditional-branch-aware gadget search (aarch64) Gadgets in newer glibc are often split across a conditional branch (e.g. 2.43's execvp materialises "/bin/sh" before `cmp x2, #1; b.ne`), which the straight-line-only search could not find. - fetcher: enumerate candidates by walking the CFG *backward* from each terminal call along its predecessors (fall-through + branch targets), forking at conditional edges up to MAX_FORKS(4). Gated per-arch by follow_branches? (aarch64 on; others keep slice_prefix for now). - emulators: shared Conditional module models cmp/cmn/tst + b.<cond>/cbz/ cbnz/tbz/tbnz, turning each crossed edge into a constraint via one-line- ahead resolution (full relational: ==,!=,signed/unsigned </>/<=/>=, bit tests). Sound: abort a path we can't express (e.g. no preceding compare). - aarch64: inst_stp now handles non-sp stores like inst_str (needed to reach the argv-building block); resolve argv/envp fall back when a stack register has no tracked stack (frame pointer). - gadget: score branch conditions. 2.43 now yields execve("/bin/sh", x4, x6) guarded by `x2 == 0x1`. Existing aarch64 gadgets are unchanged or added to (fixtures updated); other arches untouched. Co-Authored-By: Claude Opus 4.8 <[email protected]> * emulators: conditional-branch-aware search for arm Enable follow_branches? for arm: model cmp/cmn/tst (previously nop-aliased) plus bne/beq/... condition-suffix branches and Thumb cbz/cbnz, reusing the shared Conditional module. Add arm branch classification (path_ends? covers bx / pop{..pc} / mov pc returns). Fixes surfaced by the new paths: guard inst_ldr/inst_str write-back against a non-register base, and skip posix_spawn candidates whose pid/file_actions/attrp is a concrete integer (a fixed address we can't constrain). arm gadgets are unchanged or added to (2.27, 2.39 each gain one; fixtures updated); e.g. 2.27 0x73f2c is now found guarded by `[r1] == 0`. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Extend conditional-branch-aware search to x86 (amd64/i386) Model cmp/test + the full jcc set (and jcxz) in the x86 emulator, reusing the shared Conditional module; retire amd64's jmp_case_candidates (the CFG walker subsumes it). With every arch now branch-aware, drop the dead straight-line candidates path, slice_prefix and branch?. Soundness/robustness fixes exposed by the richer x86 paths: - drop gadgets whose branch condition is unsatisfiable (X != X) and strip always-true ones (X == X); - dedup gadgets by offset (a branch can reach the call either way), keeping the easiest variant; - treat a resolve-time eval error as "not a gadget"; - Lambda.parse: model absolute [addr]; keep xmm arrays / fall back only for a call-target string base; - guard the SSE handlers, inst_sub, check_envp against non-Lambda operands. amd64/i386 now find a superset of the previous gadgets (fixtures updated for the force_file paths; prebuilt-DB assertions unchanged). Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs: note branch-aware gadget search in README Co-Authored-By: Claude Opus 4.8 <[email protected]> * perf: speed up branch-aware gadget search The backward CFG walk produces many overlapping candidate paths, and find() re-emulates every start line in each; the extra work made the specs notably slower. All behaviour-preserving (gadget output is unchanged): - find(): memoize by candidate suffix so shared tails are emulated once (~57% of starts were duplicates). - Processor#parse: build the instruction set (and a mnemonic index) once instead of re-allocating it per line. - CFG walk: precompute each line's branch classification and address once (line_kinds/disasm_addrs/predecessors_map) rather than re-running the per-arch regexes on every visit; walk with a backtracking Set instead of copying visited/path each step; precompile the terminal-call regex. - ARM emulator: cache the libc bytes instead of re-reading the file for every emulator instance. ~37% faster on the integration specs. Co-Authored-By: Claude Opus 4.8 <[email protected]> * perf: cut branch-aware setup cost on large libcs The previous pass eagerly classified every disassembly line (line_kinds / predecessors_map) — costly on big libcs (aarch64 2.43 has 310k lines). Rework so the whole-binary scan is cheap and the per-line classification only runs where the walk actually goes. Behaviour-preserving. - predecessors: computed lazily and memoized (the walk only visits lines near terminal calls), instead of precomputing for all lines. - branch_pred_map: reject the ~99% non-branch lines with one precompiled per-arch regex (branch_lead_regex) before the full test, and drop the mnemonic memoization that was allocating an entry per line during this scan. - disasm_cache: cache objdump output by command so re-analysing the same file (the specs load some libcs several times; the CLI reads a file once) skips the disassembly. ~45% faster on the full suite (60.7s -> 33.6s here). Co-Authored-By: Claude Opus 4.8 <[email protected]> * perf: windowed disassembly for arm (byte-scan the calls) A full objdump of a Thumb-2 arm libc is ~4x slower than amd64/aarch64 (~1.1s vs ~0.3s), yet the branch-aware search only ever touches ~0.35% of the disassembly — everything it needs sits within ~0x1246 bytes of an exec*/posix_spawn* call (measured max: 0x98b backward span + 0x8bb branch-in across real amd64/aarch64/arm libcs). So for arm we locate the calls *without* disassembling: take the exec*/posix_spawn* addresses from the symbol table and scan .text for A32/Thumb `BL` instructions branching to them, then objdump only a 0x2000 window around each (merging overlaps). The scan is exact for the direct `bl <exec*>` calls the search cares about — validated to miss none of the objdump-grep sites on the 2.23/2.27/2.39 fixtures (a rare false positive just adds an empty window); if it finds nothing we fall back to a full disassembly. Behaviour-preserving: gadget output is identical (the arm integration specs assert exact lists). Single-libc time drops ~52-57% (2.39: 2.84s -> 1.32s; 2.23: 1.70s -> 0.73s). Only arm opts in — the fast-to-objdump arches keep the exhaustive full scan. Co-Authored-By: Claude Opus 4.8 <[email protected]> * refactor: one branch_kind per arch instead of four predicates A new architecture had to implement four overlapping methods to classify a line for the CFG walk (conditional_branch?, unconditional_branch?, path_ends?, branch_lead_regex). Collapse them into one intuitive classifier: branch_kind(line) -> :conditional | :unconditional | :terminator | nil Base derives the predicates it needs from it and builds branch_lead_regex from a tiny branch_lead_chars hint. No behavior change. Co-Authored-By: Claude Opus 4.8 <[email protected]> * refactor: share compare/operand helpers across emulators handle_compare, operand_str and mnemonic were byte-for-byte identical in the aarch64/arm/x86 emulators (x86's `test ? :test : :cmp` is just `mnem.to_sym`). Move them into the shared Conditional module so a new arch only supplies the genuinely arch-specific parts: which mnemonics are compares/branches (branch_mnem?), how to split operands (operands), and how a branch maps to a queued decision (handle_branch). No behavior change. Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs: guide for adding a new architecture The arch surface is spread over a Fetcher and an Emulator subclass; collect the full contract in one place (docs/adding-an-architecture.md) and point the two base classes at it, so it's clear what a new arch must implement. Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs: add ruby lang hint to code block * style: keep lib comments ASCII, enforce via rubocop * docs: add @example usage to Conditional methods * docs: prefer example/param tags over inline e.g. * refactor: name branch conditions by predicate, adapters per arch * refactor: share ARM-family emulator logic via ArmFamily module * emulators: model each compare's ALU op, not just cmp Keys compares by ALU op (:sub/:add/:and) via each arch's COMPARES, dropping the :cmp special case so a new arch just declares its compare mnemonics. Fixes the test/tst mis-rendering this exposed: test r,r; jne rendered r != r (a false contradiction that dropped valid gadgets), test r,r; jz an unsound r == r tautology, and cmn now allows ordered conditions. Recovers gadgets on amd64 2.19/2.31. * emulators: tighten Conditional per review Rename COMPARE -> COMPARE_OPS so it no longer collides with the arch COMPARES mnemonic map; co-locate each op's renderer in the table and dispatch via fetch, so an unknown op raises loudly instead of yielding a nil constraint. Document Conditional's required host interface and branch_lead_chars' regex-safety contract. * docs: add class + pipeline diagrams to the arch guide * refactor: rename queue_* branch helpers to branch_on_* queue_cond_branch -> branch_on_compare, queue_cbz -> branch_on_zero, queue_tbz -> branch_on_bit. The names now describe what the branch tests (a recorded compare / a register being zero / a single bit) instead of the ARM mnemonic, and drop the misleading 'queue' (there is only one pending branch, not a queue). * docs: keep Conditional statements arch-neutral Move arch mnemonics (cmp/cbz/b.ne/jcc/tst/...) out of the descriptive prose, param and return docs into the @example blocks; the statements now say what each method does and why in arch-independent terms, and the render_* / COMPARE_OPS comments are labelled by ALU op rather than mnemonic. --------- Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
45b78efef8
commit
4732208300
@@ -60,6 +60,9 @@ Style/AccessorGrouping:
|
||||
Style/ArrayIntersect:
|
||||
Enabled: false
|
||||
|
||||
Style/AsciiComments:
|
||||
Enabled: true
|
||||
|
||||
Style/FormatStringToken:
|
||||
Enabled: false
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ Note: requires ruby version >= 2.1.0, you can use `ruby --version` to check.
|
||||
|
||||
OneGadget uses symbolic execution to find the constraints of gadgets to be successful.
|
||||
|
||||
Gadgets are found by walking the control-flow graph backward from each `exec`/`posix_spawn`
|
||||
call, following conditional branches both ways. When a gadget is only reachable if a branch is
|
||||
(not) taken, that decision shows up as an extra constraint, e.g. `x2 == 0x1`.
|
||||
|
||||
The article introducing how I develop this tool can be found [on my blog](https://david942j.blogspot.com/2017/02/project-one-gadget-in-glibc.html).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -38,6 +38,10 @@ Note: requires ruby version >= 2.1.0, you can use `ruby --version` to check.
|
||||
|
||||
OneGadget uses symbolic execution to find the constraints of gadgets to be successful.
|
||||
|
||||
Gadgets are found by walking the control-flow graph backward from each `exec`/`posix_spawn`
|
||||
call, following conditional branches both ways. When a gadget is only reachable if a branch is
|
||||
(not) taken, that decision shows up as an extra constraint, e.g. `x2 == 0x1`.
|
||||
|
||||
The article introducing how I develop this tool can be found [on my blog](https://david942j.blogspot.com/2017/02/project-one-gadget-in-glibc.html).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Adding a new architecture
|
||||
|
||||
Supporting an architecture means two classes plus a little wiring. The cleanest
|
||||
existing example to copy is **aarch64** (`lib/one_gadget/fetchers/aarch64.rb` and
|
||||
`lib/one_gadget/emulators/aarch64.rb`).
|
||||
|
||||
```ruby
|
||||
OneGadget::Fetcher::<Arch> < OneGadget::Fetcher::Base # find candidates, describe constraints
|
||||
OneGadget::Emulators::<Arch> < OneGadget::Emulators::Processor # symbolically execute a candidate
|
||||
```
|
||||
|
||||
The engine (candidate discovery via a control-flow walk, constraint solving,
|
||||
gadget trimming) lives in the base classes. Each architecture only fills in the
|
||||
parts that differ.
|
||||
|
||||
## At a glance
|
||||
|
||||
**Who owns what.** Two parallel hierarchies (fetcher, emulator); the engine base
|
||||
classes carry the shared logic, and each arch subclass fills in the boxes marked
|
||||
_you implement_. `arm`/`aarch64` also mix in `ArmFamily` to share their common
|
||||
pieces; `x86` keeps its own. The fetcher reaches its emulator through `emulator()`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef engine fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,font-family:monospace
|
||||
classDef arch fill:#dcfce7,stroke:#16a34a,color:#14532d,font-family:monospace
|
||||
classDef mixin fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,font-family:monospace
|
||||
|
||||
subgraph FE["Fetcher side: find candidate sequences"]
|
||||
direction TB
|
||||
FB["Fetcher::Base<br/>(engine)"]:::engine
|
||||
FA["Fetcher::<Arch><br/>(you implement)"]:::arch
|
||||
FB -->|inherits| FA
|
||||
end
|
||||
subgraph EM["Emulator side: execute one candidate"]
|
||||
direction TB
|
||||
EP["Emulators::Processor<br/>(engine)"]:::engine
|
||||
CO["module Conditional"]:::mixin
|
||||
AF["module ArmFamily"]:::mixin
|
||||
EA["Emulators::<Arch><br/>(you implement)"]:::arch
|
||||
EP -->|inherits| EA
|
||||
CO -.->|mixed into| EP
|
||||
AF -.->|"mixed into (arm, aarch64)"| EA
|
||||
end
|
||||
FA ==>|"emulator()"| EA
|
||||
```
|
||||
|
||||
Legend: 🟦 engine (shared base) · 🟩 what you implement · 🟪 shared mixin.
|
||||
|
||||
What each box is for:
|
||||
|
||||
- **`Fetcher::Base`** — the engine: walks the CFG for candidate sequences, then solves and trims gadgets.
|
||||
- **`Fetcher::<Arch>`** — the arch's disassembly, its string/branch recognition, and how to build its emulator.
|
||||
- **`Emulators::Processor`** — the engine: parse an instruction, track register/stack state, collect constraints.
|
||||
- **`module Conditional`** — shared compare/branch machinery; a crossed branch becomes a gadget constraint.
|
||||
- **`module ArmFamily`** — logic shared by arm and aarch64 (calling convention, memory model, condition codes).
|
||||
- **`Emulators::<Arch>`** — emulate each instruction, plus the calling convention and register model.
|
||||
|
||||
**Why each piece exists.** The runtime flow shows where your methods are called
|
||||
and what they feed:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
classDef entry fill:#fef9c3,stroke:#ca8a04,color:#713f12,font-family:monospace
|
||||
classDef engine fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,font-family:monospace
|
||||
classDef arch fill:#dcfce7,stroke:#16a34a,color:#14532d,font-family:monospace
|
||||
classDef mixin fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,font-family:monospace
|
||||
classDef out fill:#ffe4e6,stroke:#e11d48,color:#881337,font-family:monospace
|
||||
|
||||
A["OneGadget.gadgets(file)"]:::entry --> B["Fetcher::<Arch>#find"]:::arch
|
||||
B --> C["Base#candidates<br/>(backward CFG walk)"]:::engine
|
||||
C --> D{"for each start instruction"}
|
||||
D --> E["Base#emulate<br/>(process! per instruction)"]:::engine
|
||||
E --> F["module Conditional<br/>(per instruction)"]:::mixin
|
||||
E --> G["arch inst_* handlers"]:::arch
|
||||
F --> H["branch → constraint"]:::mixin
|
||||
D --> I["Base#resolve<br/>(argv / envp checks)"]:::engine
|
||||
H --> J["Gadget<br/>(offset + constraints)"]:::out
|
||||
I --> J
|
||||
```
|
||||
|
||||
Reading the flow:
|
||||
|
||||
- **`candidates`** walks the CFG backward from each `exec*`/`posix_spawn*` call to build candidate sequences.
|
||||
- **`find`** emulates every start instruction of each candidate, one instruction at a time.
|
||||
- **`Conditional`** runs first on each instruction (`resolve_pending_branch` → `handle_compare` → `handle_branch` → `branch_on_*`), turning a crossed branch into a constraint.
|
||||
- the arch's **`inst_*`** handlers then update the register/stack state.
|
||||
- **`resolve`** reads the call's arguments (`argument`, `str_bin_sh?`, `global_var?`) and builds the argv/envp constraints.
|
||||
- finally, a contradiction drops the gadget and a tautology is stripped.
|
||||
|
||||
## The fetcher — `Fetcher::<Arch> < Fetcher::Base`
|
||||
|
||||
| Method | What it returns |
|
||||
| --- | --- |
|
||||
| `emulator` | a fresh `Emulators::<Arch>` instance |
|
||||
| `call_str` | the call mnemonic that reaches a function — `'bl'` (arm/aarch64), `'call'` (x86) |
|
||||
| `str_bin_sh?(operand)` | does this operand reference the `"/bin/sh"` string? |
|
||||
| `str_sh?(operand)` | does it reference the standalone `"sh"` string? |
|
||||
| `global_var?(operand)` | does it reference a libc global (i.e. is it `$base`-relative)? |
|
||||
| `branch_kind(line)` | classify an instruction: `:conditional`, `:unconditional`, `:terminator`, or `nil` (not a branch) |
|
||||
| `branch_lead_chars` | the leading character(s) of this arch's branch mnemonics, e.g. `'bct'` or `'j'` (a cheap filter) |
|
||||
|
||||
`branch_kind` is the one method that drives the control-flow walk:
|
||||
|
||||
* `:conditional` — may or may not be taken; both edges are explored and the
|
||||
decision becomes a gadget constraint (e.g. `x2 == 0x1`).
|
||||
* `:unconditional` — always taken, to a determined target.
|
||||
* `:terminator` — ends the path (a return or an indirect/computed jump).
|
||||
* `nil` — not a branch; execution falls through to the next instruction.
|
||||
|
||||
Optional hooks (sensible defaults in `Base`):
|
||||
|
||||
| Method | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `objdump_options` | `[]` | extra objdump flags, e.g. x86 returns `%w[-M intel]` |
|
||||
| `terminal_call_sites` | `nil` | if the calls can be located cheaply *without* disassembling everything, return their addresses for windowed disassembly (a big win when a full objdump is slow, as on Thumb-2 arm). `nil` = disassemble the whole file. See `Fetcher::Arm`. |
|
||||
|
||||
## The emulator — `Emulators::<Arch> < Emulators::Processor`
|
||||
|
||||
| Method | What it does |
|
||||
| --- | --- |
|
||||
| `initialize` | `super(OneGadget::ABI.<arch>, sp_name)`, set `@pc`, define constant registers |
|
||||
| `instructions` | the `Instruction`s this arch models (anything else aborts the path) |
|
||||
| `process!(cmd)` | emulate one instruction (see the pattern below) |
|
||||
| `argument(idx)` | the value of the idx-th call argument (the calling convention) |
|
||||
| `get_corresponding_stack(obj)` | the sp/bp-based stack hash `obj` addresses, or `nil` |
|
||||
| `self.bits` | `32` or `64` |
|
||||
| `operands(cmd)` | split an instruction into its operand strings |
|
||||
| `branch_mnem?(mnem)` | is `mnem` a branch this emulator handles? |
|
||||
| `handle_branch(mnem, cmd)` | turn a branch into a pending decision (via the `branch_on_*` helpers below) |
|
||||
| `inst_<name>(...)` | one handler per supported instruction |
|
||||
|
||||
`process!` follows a fixed shape — resolve any pending branch, handle a
|
||||
compare/branch, otherwise parse and dispatch:
|
||||
|
||||
```ruby
|
||||
def process!(cmd)
|
||||
resolve_pending_branch(cmd) # from Conditional
|
||||
mnem = mnemonic(cmd) # from Conditional
|
||||
return handle_compare(COMPARES[mnem], cmd) if COMPARES.key?(mnem) # from Conditional
|
||||
return handle_branch(mnem, cmd) != :fail if branch_mnem?(mnem)
|
||||
|
||||
inst, args = parse(cmd)
|
||||
__send__(:"inst_#{inst.inst}", *args) != :fail
|
||||
end
|
||||
```
|
||||
|
||||
The **`Emulators::Conditional`** module (already included in `Processor`) provides
|
||||
the branch machinery so you don't reimplement it: `record_compare`,
|
||||
`handle_compare`, `branch_on_compare`, `branch_on_zero`, `branch_on_bit`,
|
||||
`resolve_pending_branch`, `operand_str`, `mnemonic`. You supply two small adapter
|
||||
tables that map your arch's mnemonics onto the shared vocabulary (directly, or via
|
||||
a shared base — arm/aarch64 share theirs through `ArmFamily`):
|
||||
|
||||
* **`COMPARES`** — compare mnemonic → ALU op (a `Conditional::COMPARE_OPS` key:
|
||||
`:sub` for `cmp`, `:add` for `cmn`, `:and` for `tst`/`test`); `handle_compare`
|
||||
reads it.
|
||||
* **`COND`** (x86 calls it `JCC`) — branch mnemonic → predicate (a
|
||||
`Conditional::RELATION` key, e.g. `:ne`, `:uge`). Your `handle_branch` parses the
|
||||
instruction and calls the right `branch_on_*`, passing this predicate to `branch_on_compare`.
|
||||
|
||||
## Wiring
|
||||
|
||||
* `OneGadget::Helper.architecture` — map the ELF machine string to your arch symbol.
|
||||
* `OneGadget::Fetcher.from_file` — add the arch symbol → `Fetcher::<Arch>` entry.
|
||||
* `OneGadget::Helper.arch_specific_objdump` — the cross objdump binary name.
|
||||
|
||||
## Tests
|
||||
|
||||
Add a `spec/one_gadget_<arch>_spec.rb` that runs `OneGadget.gadgets` against a
|
||||
real libc fixture and asserts the exact gadget offsets, plus emulator unit tests
|
||||
in `spec/emulators/<arch>_spec.rb`.
|
||||
@@ -21,7 +21,13 @@ module OneGadget
|
||||
|
||||
# @see OneGadget::Emulators::X86#process!
|
||||
def process!(cmd)
|
||||
inst, args = parse(cmd.gsub(/#-?(0x)?[0-9a-f]+/) { |v| v[1..] })
|
||||
resolve_pending_branch(cmd)
|
||||
cmd = cmd.gsub(/#-?(0x)?[0-9a-f]+/) { |v| v[1..] }
|
||||
mnem = mnemonic(cmd)
|
||||
return handle_compare(COMPARES[mnem], cmd) if COMPARES.key?(mnem)
|
||||
return handle_branch(mnem, cmd) != :fail if branch_mnem?(mnem)
|
||||
|
||||
inst, args = parse(cmd)
|
||||
sym = :"inst_#{inst.inst}"
|
||||
__send__(sym, *args) != :fail
|
||||
end
|
||||
@@ -49,6 +55,27 @@ module OneGadget
|
||||
|
||||
private
|
||||
|
||||
def branch_mnem?(mnem)
|
||||
mnem == 'b' || mnem.start_with?('b.') || %w[cbz cbnz tbz tbnz].include?(mnem)
|
||||
end
|
||||
|
||||
# Operands of +cmd+ (mnemonic dropped), each stripped of a trailing +<symbol>+.
|
||||
def operands(cmd)
|
||||
cmd.sub(/\A[0-9a-f]+:\s*\S+\s*/, '').split(',').map { |o| o.strip.sub(/\s*<.*>\z/, '') }
|
||||
end
|
||||
|
||||
def handle_branch(mnem, cmd)
|
||||
ops = operands(cmd)
|
||||
case mnem
|
||||
when 'b', 'b.al' then true # unconditional: control handled by the stitched path
|
||||
when 'cbz' then branch_on_zero(ops[1].to_i(16), operand_str(ops[0]), negate: false)
|
||||
when 'cbnz' then branch_on_zero(ops[1].to_i(16), operand_str(ops[0]), negate: true)
|
||||
when 'tbz' then branch_on_bit(ops[2].to_i(16), operand_str(ops[0]), Integer(ops[1]), negate: false)
|
||||
when 'tbnz' then branch_on_bit(ops[2].to_i(16), operand_str(ops[0]), Integer(ops[1]), negate: true)
|
||||
else branch_on_compare(COND[mnem[2..]], ops[0].to_i(16))
|
||||
end
|
||||
end
|
||||
|
||||
def inst_add(dst, src, op2, mode = 'sxtw')
|
||||
check_register!(dst)
|
||||
|
||||
@@ -92,11 +119,14 @@ module OneGadget
|
||||
raise_unsupported('stp', reg1, reg2, dst) unless reg64?(reg1) && reg64?(reg2)
|
||||
|
||||
dst_l = arg_to_lambda(dst).ref!
|
||||
raise_unsupported('stp', reg1, reg2, dst) unless dst_l.obj == sp && dst_l.deref_count.zero?
|
||||
|
||||
cur_top = dst_l.evaluate(eval_dict)
|
||||
sp_based_stack[cur_top] = registers[reg1]
|
||||
sp_based_stack[cur_top + size_t] = registers[reg2]
|
||||
if dst_l.obj == sp && dst_l.deref_count.zero?
|
||||
cur_top = dst_l.evaluate(eval_dict)
|
||||
sp_based_stack[cur_top] = registers[reg1]
|
||||
sp_based_stack[cur_top + size_t] = registers[reg2]
|
||||
else
|
||||
# Don't know where it points; just require it be writable (as inst_str does).
|
||||
add_writable(dst_l)
|
||||
end
|
||||
|
||||
registers[sp] += arg_to_lambda(dst).immi if dst.end_with?('!')
|
||||
end
|
||||
|
||||
@@ -21,20 +21,30 @@ module OneGadget
|
||||
def initialize(file = nil)
|
||||
super(OneGadget::ABI.arm, 'sp')
|
||||
@pc = 'pc'
|
||||
@data = file && File.binread(file)
|
||||
# find() builds a fresh emulator per candidate; cache the file's bytes so
|
||||
# the literal pool isn't re-read from disk thousands of times.
|
||||
@data = file && self.class.file_data(file)
|
||||
@prev_addr = nil
|
||||
# A32 until proven Thumb by a +.w+/+.n+ suffix or a 2-byte instruction stride.
|
||||
@thumb = false
|
||||
end
|
||||
|
||||
# Memoized bytes of +file+ (the target libc), shared across emulator instances.
|
||||
def self.file_data(file)
|
||||
(@file_data ||= {})[file] ||= File.binread(file)
|
||||
end
|
||||
|
||||
# @see OneGadget::Emulators::AArch64#process!
|
||||
def process!(cmd)
|
||||
resolve_pending_branch(cmd)
|
||||
line = cmd.strip
|
||||
track_mode(line)
|
||||
body, @literal = split_line(line)
|
||||
body = normalize(body)
|
||||
# push/pop take a {reg-list} whose commas would confuse the generic parser.
|
||||
mnem, rest = body.split(/\s+/, 2)
|
||||
return handle_compare(COMPARES[mnem], rest) if COMPARES.key?(mnem)
|
||||
return handle_branch(mnem, rest) != :fail if branch_mnem?(mnem)
|
||||
# push/pop take a {reg-list} whose commas would confuse the generic parser.
|
||||
return __send__(:"inst_#{mnem}", rest) != :fail if %w[push pop].include?(mnem)
|
||||
|
||||
inst, args = parse(body)
|
||||
@@ -105,7 +115,7 @@ module OneGadget
|
||||
# Rewrite one instruction into the plain form the generic parser expects:
|
||||
# drop the +.w+/+.n+ width suffix, map the flag-setting aliases we support
|
||||
# (+movs+/+adds+/+subs+) back to their base mnemonic, and strip the +#+ that
|
||||
# prefixes ARM immediates. Other conditional/flag variants (e.g. +moveq+) are
|
||||
# prefixes ARM immediates. Other conditional/flag variants (+moveq+, +addne+, ...) are
|
||||
# left intact so they fall through to "unsupported".
|
||||
# @example
|
||||
# normalize('movs r0, #0')
|
||||
@@ -159,8 +169,7 @@ module OneGadget
|
||||
return unless src.end_with?('!') || index.nonzero?
|
||||
|
||||
# pre-index ([reg, imm]!) or post-index ([reg], imm) write-back.
|
||||
lmda = OneGadget::Emulators::Lambda.parse(resolve_int_regs(src.delete('!')))
|
||||
registers[lmda.obj] += lmda.immi + index
|
||||
writeback(src, index)
|
||||
end
|
||||
|
||||
def inst_str(src, dst, index = 0)
|
||||
@@ -177,8 +186,13 @@ module OneGadget
|
||||
index = Integer(index)
|
||||
return unless dst.end_with?('!') || index.nonzero?
|
||||
|
||||
lmda = OneGadget::Emulators::Lambda.parse(resolve_int_regs(dst.delete('!')))
|
||||
registers[lmda.obj] += lmda.immi + index
|
||||
writeback(dst, index)
|
||||
end
|
||||
|
||||
# Pre-/post-index write-back of a +[reg, imm]+ memory operand to +reg+.
|
||||
def writeback(mem, index)
|
||||
lmda = OneGadget::Emulators::Lambda.parse(resolve_int_regs(mem.delete('!')))
|
||||
registers[lmda.obj] += lmda.immi + index if register?(lmda.obj)
|
||||
end
|
||||
|
||||
# push {r4, r5, lr}: registers are stored with the lowest-numbered at the
|
||||
@@ -204,11 +218,29 @@ module OneGadget
|
||||
|
||||
# Flag-only / no-effect instructions: keep emulating without changing state.
|
||||
def inst_nop(*); end
|
||||
alias inst_cmp inst_nop
|
||||
alias inst_cmn inst_nop
|
||||
alias inst_tst inst_nop
|
||||
alias inst_svc inst_nop
|
||||
|
||||
def branch_mnem?(mnem)
|
||||
return true if mnem == 'b' || %w[cbz cbnz].include?(mnem)
|
||||
|
||||
# +vs+/+vc+ aren't in COND but are still branches (they just abort the path).
|
||||
mnem.start_with?('b') && (COND.key?(mnem[1..]) || %w[vs vc].include?(mnem[1..]))
|
||||
end
|
||||
|
||||
def operands(rest)
|
||||
rest.to_s.split(',').map { |o| o.strip.sub(/\s*<.*>\z/, '') }
|
||||
end
|
||||
|
||||
def handle_branch(mnem, rest)
|
||||
ops = operands(rest)
|
||||
case mnem
|
||||
when 'b' then true # unconditional: control handled by the stitched path
|
||||
when 'cbz' then branch_on_zero(ops[1].to_i(16), operand_str(ops[0]), negate: false)
|
||||
when 'cbnz' then branch_on_zero(ops[1].to_i(16), operand_str(ops[0]), negate: true)
|
||||
else branch_on_compare(COND[mnem[1..]], ops[0].to_i(16))
|
||||
end
|
||||
end
|
||||
|
||||
# Read the little-endian word the current PC-relative +ldr+ points at.
|
||||
def literal_value
|
||||
raise_unsupported('ldr', 'pc') if @literal.nil? || @data.nil?
|
||||
|
||||
@@ -5,11 +5,29 @@ require 'one_gadget/emulators/lambda'
|
||||
module OneGadget
|
||||
module Emulators
|
||||
# Behaviour shared by the two ARM-architecture emulators, {Arm} (AArch32) and
|
||||
# {AArch64}: the parts of the calling convention and memory model that are
|
||||
# identical in 32- and 64-bit mode. Included into each; everything that
|
||||
# genuinely differs (instruction set, register width, +pc+ handling) stays in
|
||||
# the individual classes.
|
||||
# {AArch64}: the condition-code table plus the parts of the calling convention
|
||||
# and memory model that are identical in 32- and 64-bit mode. Included into
|
||||
# each; everything that genuinely differs (instruction set, register width,
|
||||
# +pc+ handling) stays in the individual classes.
|
||||
module ArmFamily
|
||||
# ARM condition-code suffix (the +<cc>+ in +b<cc>+ / +b.<cc>+) mapped to a
|
||||
# shared {Conditional::RELATION} predicate. Decodes the opaque mnemonics once:
|
||||
# +hs+/+cs+ and +lo+/+cc+ are aliases; +mi+/+pl+ (sign bit) act as signed
|
||||
# +</+>=+. +vs+/+vc+ (overflow) have no constraint form and are absent, so a
|
||||
# branch on them maps to +nil+ and aborts the path. (x86 uses {X86::JCC}.)
|
||||
COND = {
|
||||
'eq' => :eq, 'ne' => :ne,
|
||||
'hs' => :uge, 'cs' => :uge, 'lo' => :ult, 'cc' => :ult,
|
||||
'hi' => :ugt, 'ls' => :ule,
|
||||
'ge' => :sge, 'lt' => :slt, 'gt' => :sgt, 'le' => :sle,
|
||||
'mi' => :slt, 'pl' => :sge
|
||||
}.freeze
|
||||
|
||||
# This arch family's flag-setting compare mnemonics, mapped to the ALU op
|
||||
# whose result their flags reflect (see {Conditional::COMPARE_OPS}). x86 has
|
||||
# its own {X86::COMPARES}.
|
||||
COMPARES = { 'cmp' => :sub, 'cmn' => :add, 'tst' => :and }.freeze
|
||||
|
||||
# The sp-based stack that +obj+ addresses, or +nil+ when +obj+ isn't sp-relative.
|
||||
# @param [String, Lambda] obj A lambda object or its string.
|
||||
# @return [Hash{Integer => Lambda}, nil]
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'one_gadget/helper'
|
||||
|
||||
module OneGadget
|
||||
module Emulators
|
||||
# Shared modelling of compare instructions and conditional branches.
|
||||
#
|
||||
# A gadget candidate may cross a conditional branch: the fetcher stitches the
|
||||
# actual taken/not-taken path (see {OneGadget::Fetcher::Base#candidates}), and
|
||||
# the emulator turns the branch decision into a gadget constraint.
|
||||
#
|
||||
# Branches are resolved with one line of look-ahead: at the branch we record a
|
||||
# pending decision, and on the next line we compare that line's address to the
|
||||
# branch target to learn whether the stitched path took the branch.
|
||||
#
|
||||
# The including class (an {OneGadget::Emulators::Processor} subclass) must
|
||||
# provide +registers+ and +register?+ (operand lookup), +operands(cmd)+ (the
|
||||
# arch's operand splitter), +self.class.bits+ (32/64, for the signedness cast),
|
||||
# and the +@flags+/+@pending+/+@constraints+ state {Processor#initialize} sets up.
|
||||
#
|
||||
# @example A +b.ne+ whose branch is *not* taken becomes the constraint +x2 == 0x1+
|
||||
# # c7a4c: cmp x2, 1
|
||||
# # c7a50: b.ne c7a9c # stitched path falls through (does not jump)
|
||||
# # => emitted gadget constraint: x2 == 0x1
|
||||
module Conditional
|
||||
# Taken-semantics of each supported branch condition, keyed by a predicate
|
||||
# named after the comparison it encodes (the LLVM +icmp+ names): a leading
|
||||
# +u+ = unsigned, +s+ = signed. Value is +[relation, signedness]+, where
|
||||
# signedness (+nil+/+:u+/+:s+) selects the operand cast.
|
||||
# :eq :ne equality
|
||||
# :ult :ule :ugt :uge unsigned < <= > >=
|
||||
# :slt :sle :sgt :sge signed < <= > >=
|
||||
RELATION = {
|
||||
eq: ['==', nil], ne: ['!=', nil],
|
||||
ult: ['<', :u], ule: ['<=', :u], ugt: ['>', :u], uge: ['>=', :u],
|
||||
slt: ['<', :s], sle: ['<=', :s], sgt: ['>', :s], sge: ['>=', :s]
|
||||
}.freeze
|
||||
|
||||
# Relation under the not-taken branch.
|
||||
NEGATE = { '==' => '!=', '!=' => '==', '>=' => '<', '<' => '>=', '>' => '<=', '<=' => '>' }.freeze
|
||||
|
||||
# The flag-setting compares we model, keyed by the ALU operation the compare
|
||||
# performs -- its flags reflect that result. Each entry says whether magnitude
|
||||
# conditions (anything beyond +eq+/+ne+) are sound afterwards (+ordered+) and
|
||||
# names the method that renders its constraint text. An arch maps its own
|
||||
# mnemonics onto these ops (its +COMPARES+), so adding an arch needs no change
|
||||
# here; adding a genuinely new ALU op means one entry plus its +render_*+ method.
|
||||
COMPARE_OPS = {
|
||||
sub: { ordered: true, render: :render_sub }, # subtraction: flags from lhs - rhs
|
||||
add: { ordered: true, render: :render_add }, # addition: flags from lhs + rhs
|
||||
and: { ordered: false, render: :render_and } # bitwise AND: flags from lhs & rhs (zero flag)
|
||||
}.freeze
|
||||
|
||||
# Record a compare so a following conditional branch can be rendered.
|
||||
# Normally reached through {#handle_compare}; call it directly only when an
|
||||
# arch models a flag-setting instruction that {#handle_compare} doesn't cover.
|
||||
# @param [Symbol] op The ALU operation the compare performs -- a {COMPARE_OPS}
|
||||
# key (+:sub+/+:add+/+:and+). Its flags reflect this result, which decides
|
||||
# both which branch conditions are expressible and how the constraint is
|
||||
# rendered. Each arch maps its own mnemonics to these ops (their +COMPARES+);
|
||||
# a new arch adds a {COMPARE_OPS} entry only for a genuinely new operation.
|
||||
# @param [String] lhs Rendered left operand: a register's current value, or an immediate in hex.
|
||||
# @param [String] rhs Rendered right operand.
|
||||
# @return [true]
|
||||
# @example Record +cmp x2, 0x1+ (subtraction), readying the next branch
|
||||
# record_compare(:sub, '0x1', '0x1') #=> true
|
||||
# # a following +b.ne+ not taken renders 0x1 == 0x1 (a stripped tautology)
|
||||
def record_compare(op, lhs, rhs)
|
||||
@flags = { op:, lhs:, rhs: }
|
||||
true
|
||||
end
|
||||
|
||||
# Model a compare line: record its two operands' current values under the
|
||||
# compare's ALU op, so a following conditional branch can be rendered.
|
||||
#
|
||||
# Call this from +process!+ when the mnemonic is one of the arch's compares
|
||||
# (its +COMPARES+ maps the mnemonic to the op), *before* dispatching to the
|
||||
# +inst_*+ handlers.
|
||||
# @param [Symbol] op The compare's ALU operation (a {COMPARE_OPS} key), which the
|
||||
# arch resolves from the mnemonic.
|
||||
# @param [String] cmd Passed to the arch's +operands+ splitter, so it is whatever
|
||||
# that splitter expects (a full objdump line, or just its operand part).
|
||||
# @return [true]
|
||||
# @example Wiring in an arch's +process!+ (its +COMPARES+ holds the mnemonic map)
|
||||
# return handle_compare(COMPARES[mnem], cmd) if COMPARES.key?(mnem)
|
||||
# @example What it records for +cmp x2, 1+ (x2 currently 0x30)
|
||||
# handle_compare(:sub, '4a1c0: cmp x2, 1') #=> true
|
||||
# # records op :sub, lhs = 0x30, rhs = 0x1, ready for the next branch
|
||||
def handle_compare(op, cmd)
|
||||
lhs, rhs = operands(cmd)
|
||||
record_compare(op, operand_str(lhs), operand_str(rhs))
|
||||
end
|
||||
|
||||
# The mnemonic of an objdump line. Use it at the top of +process!+ to decide
|
||||
# whether a line is a compare or a branch.
|
||||
# @param [String] cmd One objdump line.
|
||||
# @return [String] The mnemonic, or +''+ when the line has none.
|
||||
# @example
|
||||
# mnemonic('4a1c0: cmp x2, 1') #=> 'cmp'
|
||||
# mnemonic('4a1d0: b.ne 4a200 <foo>') #=> 'b.ne'
|
||||
# mnemonic('4a1d4: je 4a200') #=> 'je'
|
||||
# mnemonic('') #=> ''
|
||||
def mnemonic(cmd)
|
||||
cmd[/\A[0-9a-f]+:\s*(\S+)/, 1] || ''
|
||||
end
|
||||
|
||||
# Render an operand for a constraint: a register becomes its current value,
|
||||
# an immediate becomes hex, anything else (a memory operand) stays as-is.
|
||||
# {#handle_compare} uses it on each compare operand; call it yourself only
|
||||
# when writing a bespoke +branch_on_*+ helper.
|
||||
# @param [String] operand A single operand string from a compare/branch line.
|
||||
# @return [String] The rendered operand.
|
||||
# @example (assuming register +x2+ currently holds the immediate +0x1+)
|
||||
# operand_str('x2') #=> '0x1' # a register -> its current value
|
||||
# operand_str('0x40') #=> '0x40' # a hex immediate -> unchanged
|
||||
# operand_str('16') #=> '0x10' # a decimal immediate -> hex
|
||||
# operand_str('[sp+0x8]') #=> '[sp+0x8]' # a memory operand -> unchanged
|
||||
def operand_str(operand)
|
||||
return registers[operand].to_s if register?(operand)
|
||||
|
||||
OneGadget::Helper.hex(Integer(operand))
|
||||
rescue ArgumentError
|
||||
operand
|
||||
end
|
||||
|
||||
# Register a branch on the last recorded compare's flags, resolved on the next
|
||||
# line. Call it from +handle_branch+; +cond+ is the comparison predicate and
|
||||
# +target+ is the branch's direct destination address.
|
||||
# @param [Symbol] cond The comparison predicate, a {RELATION} key. Each arch
|
||||
# first maps its own branch mnemonic to it via its adapter table.
|
||||
# @param [Integer] target Destination address of the branch.
|
||||
# @return [true, :fail] +:fail+ (abort the path) when it cannot be expressed
|
||||
# soundly: no compare was seen, or a magnitude condition follows an
|
||||
# equality-only compare (a {COMPARE_OPS} op with +ordered: false+).
|
||||
# @example After +cmp x2, #1+, a following +b.ne 4a200+ (arch maps +b.ne+ to +:ne+)
|
||||
# branch_on_compare(:ne, 0x4a200) #=> true
|
||||
# # if the stitched path FALLS THROUGH (doesn't reach 0x4a200) the not-taken
|
||||
# # relation of +:ne+ is emitted: x2 == 0x1 ; if it jumps there: x2 != 0x1
|
||||
# @example No preceding compare -> the path is unsound and is aborted
|
||||
# branch_on_compare(:ne, 0x4a200) #=> :fail # when @flags is nil
|
||||
def branch_on_compare(cond, target)
|
||||
return :fail if @flags.nil?
|
||||
|
||||
rel = RELATION[cond]
|
||||
return :fail if rel.nil?
|
||||
# A magnitude condition needs a compare whose flags reflect a full ordering
|
||||
# (+:sub+/+:add+); an equality-only compare (+:and+) supports just eq/ne.
|
||||
return :fail unless COMPARE_OPS.fetch(@flags[:op])[:ordered] || %i[eq ne].include?(cond)
|
||||
|
||||
op = @flags[:op]
|
||||
lhs = @flags[:lhs]
|
||||
rhs = @flags[:rhs]
|
||||
@pending = { target:, render: ->(taken) { compare_constraint(op, lhs, rhs, rel, taken) } }
|
||||
true
|
||||
end
|
||||
|
||||
# Register a self-contained branch that tests a register against zero. It carries
|
||||
# its own compare, so no preceding compare is needed. +negate:+ selects the sense:
|
||||
# +false+ branches when the register is zero, +true+ when it isn't.
|
||||
# @param [Integer] target Destination address of the branch.
|
||||
# @param [String] reg The tested register, already rendered (as {#operand_str} returns).
|
||||
# @param [Boolean] negate +false+ = branch when +reg+ is zero, +true+ = when it isn't.
|
||||
# @example aarch64 +cbz x0, 4a200+ - branch taken when +x0 == 0+
|
||||
# branch_on_zero(0x4a200, 'x0', negate: false) #=> true
|
||||
# # fall-through path emits x0 != 0 ; taken path emits x0 == 0
|
||||
# @example x86 reuses it for +jrcxz+/+jecxz+/+jcxz+ (always branch-if-zero)
|
||||
# branch_on_zero(0x4a200, 'rcx', negate: false)
|
||||
def branch_on_zero(target, reg, negate:)
|
||||
hit = negate ? '!=' : '==' # taken (not negated) => reg == 0
|
||||
miss = negate ? '==' : '!='
|
||||
@pending = { target:, render: ->(taken) { "#{reg} #{taken ? hit : miss} 0" } }
|
||||
true
|
||||
end
|
||||
|
||||
# Register a self-contained branch that tests a single bit of a register: also
|
||||
# carries its own test, so no preceding compare is needed. Renders a bitmask test.
|
||||
# @param [Integer] target Destination address of the branch.
|
||||
# @param [String] reg The tested register, already rendered (as {#operand_str} returns).
|
||||
# @param [Integer] bit The bit index being tested.
|
||||
# @param [Boolean] negate +false+ = branch when the bit is zero, +true+ = when it's set.
|
||||
# @example aarch64 +tbz w0, #4, 4a200+ - branch taken when bit 4 of +w0+ is 0
|
||||
# branch_on_bit(0x4a200, 'w0', 4, negate: false) #=> true
|
||||
# # taken path emits (w0 & 0x10) == 0 ; fall-through emits (w0 & 0x10) != 0
|
||||
def branch_on_bit(target, reg, bit, negate:)
|
||||
mask = OneGadget::Helper.hex(1 << bit)
|
||||
hit = negate ? '!=' : '=='
|
||||
miss = negate ? '==' : '!='
|
||||
@pending = { target:, render: ->(taken) { "(#{reg} & #{mask}) #{taken ? hit : miss} 0" } }
|
||||
true
|
||||
end
|
||||
|
||||
# Resolve the pending branch using +cmd+'s address: if it equals the branch
|
||||
# target the stitched path took the branch, else it fell through. On
|
||||
# resolution the rendered relation is appended to the gadget's constraints.
|
||||
# Must be called at the top of +process!+ for every line (a no-op when nothing
|
||||
# is pending), so the branch registered on the previous line sees this line's address.
|
||||
# @param [String] cmd The current objdump line.
|
||||
# @example The fixed first line of every arch's +process!+
|
||||
# def process!(cmd)
|
||||
# resolve_pending_branch(cmd)
|
||||
# ...
|
||||
# end
|
||||
# # if the previous line did +branch_on_compare(:ne, 0x4a200)+ and this
|
||||
# # +cmd+ sits at 0x4a200, the branch was taken; otherwise it fell through
|
||||
def resolve_pending_branch(cmd)
|
||||
return if @pending.nil?
|
||||
|
||||
taken = branch_addr(cmd) == @pending[:target]
|
||||
@constraints << [:raw, @pending[:render].call(taken)]
|
||||
@pending = nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Render the constraint a taken/not-taken branch imposes on the recorded
|
||||
# compare: flip the operator via {NEGATE} on the not-taken edge, prefix the
|
||||
# signedness cast, then defer to the op's +render_*+ method (see {COMPARE_OPS}).
|
||||
# Raises +KeyError+ if +op+ isn't a known {COMPARE_OPS} entry.
|
||||
# @param [Symbol] op The compare's ALU operation (a {COMPARE_OPS} key).
|
||||
# @param [Array(String, Symbol)] rel The +[operator, signedness]+ from {RELATION}.
|
||||
# @example +b.ne+ (not taken) after +cmp x2, 1+ -- a +:sub+ compare, 64-bit
|
||||
# compare_constraint(:sub, 'x2', '0x1', ['!=', nil], false) #=> 'x2 == 0x1'
|
||||
# @example +jne+ (taken) after +test eax, eax+ -- an +:and+ compare
|
||||
# compare_constraint(:and, 'eax', 'eax', ['!=', nil], true) #=> 'eax != 0'
|
||||
def compare_constraint(op, lhs, rhs, rel, taken)
|
||||
operator, sign = rel
|
||||
operator = NEGATE[operator] unless taken
|
||||
cast = sign ? "(#{sign}#{self.class.bits})" : ''
|
||||
__send__(COMPARE_OPS.fetch(op)[:render], cast, lhs, rhs, operator)
|
||||
end
|
||||
|
||||
# Subtraction (+:sub+): a direct comparison of the two operands.
|
||||
def render_sub(cast, lhs, rhs, op)
|
||||
"#{cast}#{lhs} #{op} #{rhs}"
|
||||
end
|
||||
|
||||
# Addition (+:add+): the flags reflect +lhs + rhs+, i.e. that sum compared against 0.
|
||||
def render_add(cast, lhs, rhs, op)
|
||||
"#{cast}(#{lhs} + #{rhs}) #{op} 0"
|
||||
end
|
||||
|
||||
# Bitwise AND (+:and+): a bitmask test; identical operands collapse to a plain zero test.
|
||||
def render_and(_cast, lhs, rhs, op)
|
||||
lhs == rhs ? "#{lhs} #{op} 0" : "(#{lhs} & #{rhs}) #{op} 0"
|
||||
end
|
||||
|
||||
# The address of an objdump line, used to tell whether the pending branch
|
||||
# was taken.
|
||||
# @example
|
||||
# branch_addr('4a200: nop') #=> 0x4a200
|
||||
def branch_addr(cmd)
|
||||
cmd[/\A\s*([0-9a-f]+):/, 1].to_i(16)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -125,13 +125,20 @@ module OneGadget
|
||||
if arg[0] == '['
|
||||
ridx = arg.rindex(']')
|
||||
immi = parse(arg[(ridx + 1)..])
|
||||
lm = parse(arg[1...ridx], predefined:).deref
|
||||
inner = parse(arg[1...ridx], predefined:)
|
||||
# An absolute address (the +[0x1234]+ in +lea rax, [0x1234]+) parses to an Integer;
|
||||
# model it as a based-nowhere Lambda so it can still be dereferenced.
|
||||
inner = Lambda.new(nil).tap { |l| l.immi = inner } if inner.is_a?(Integer)
|
||||
lm = inner.deref
|
||||
lm += immi unless immi.zero?
|
||||
return lm
|
||||
end
|
||||
|
||||
base, disp = mem_obj(arg)
|
||||
obj = predefined[base] || Lambda.new(base)
|
||||
obj = predefined[base]
|
||||
# A register may currently hold a call-target string (after a call);
|
||||
# treat that as a fresh symbolic base. Lambda/Integer/xmm-array pass through.
|
||||
obj = Lambda.new(base) if obj.nil? || obj.is_a?(String)
|
||||
obj += disp unless disp.zero?
|
||||
obj
|
||||
end
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'one_gadget/emulators/conditional'
|
||||
require 'one_gadget/emulators/lambda'
|
||||
require 'one_gadget/error'
|
||||
|
||||
module OneGadget
|
||||
# Instruction emulator to solve the constraint of gadgets.
|
||||
module Emulators
|
||||
# Base class of a processor.
|
||||
# Base of the per-architecture instruction emulators, used to symbolically
|
||||
# execute a candidate and solve its constraints. A subclass implements the
|
||||
# arch's supported instructions, calling convention and stack model; the
|
||||
# shared branch/compare machinery comes from {Conditional}.
|
||||
#
|
||||
# To add an architecture, see +docs/adding-an-architecture.md+.
|
||||
class Processor
|
||||
include Conditional
|
||||
|
||||
attr_reader :registers # @return [Hash{String => OneGadget::Emulators::Lambda}] The current registers' state.
|
||||
attr_reader :sp_based_stack # @return [Hash{Integer => OneGadget::Emulators::Lambda}] Stack content based on sp.
|
||||
attr_reader :sp # @return [String] Stack pointer.
|
||||
@@ -22,6 +30,8 @@ module OneGadget
|
||||
@registers = registers.to_h { |reg| [reg, to_lambda(reg)] }
|
||||
@sp = sp
|
||||
@constraints = []
|
||||
@flags = nil # last compare, for a following conditional branch
|
||||
@pending = nil # a conditional branch awaiting one-line-ahead resolution
|
||||
@sp_based_stack = Hash.new do |h, k|
|
||||
h[k] = OneGadget::Emulators::Lambda.new(sp).tap do |lmda|
|
||||
lmda.immi = k
|
||||
@@ -35,7 +45,13 @@ module OneGadget
|
||||
# @return [(Instruction, Array<String>)]
|
||||
# The parsing result.
|
||||
def parse(cmd)
|
||||
inst = instructions.find { |i| i.match?(cmd) }
|
||||
# instructions is a constant set; build the array (and a mnemonic index)
|
||||
# once instead of re-allocating it for every line.
|
||||
@inst_index ||= instructions.each_with_object({}) { |i, h| h[i.inst] ||= i }
|
||||
mnem = cmd[/\A[0-9a-f]+:\s*(\S+)/, 1] || cmd[/\A\s*(\S+)/, 1]
|
||||
inst = @inst_index[mnem]
|
||||
# Fall back to the original scan for any mnemonic that isn't a bare word.
|
||||
inst ||= (@inst_list ||= instructions).find { |i| i.match?(cmd) }
|
||||
raise Error::UnsupportedInstructionError, "Not implemented instruction in #{cmd}" if inst.nil?
|
||||
|
||||
[inst, inst.fetch_args(cmd)]
|
||||
|
||||
@@ -32,14 +32,32 @@ module OneGadget
|
||||
# @return [Boolean]
|
||||
# If successfully processed.
|
||||
def process!(cmd)
|
||||
inst, args = parse(cmd)
|
||||
# return registers[pc] = args[0] if inst.inst == 'call'
|
||||
return true if inst.inst == 'jmp' # believe the fetcher has handled jmp.
|
||||
resolve_pending_branch(cmd)
|
||||
mnem = mnemonic(cmd)
|
||||
return handle_compare(COMPARES[mnem], cmd) if COMPARES.key?(mnem)
|
||||
return handle_branch(mnem, cmd) != :fail if branch_mnem?(mnem)
|
||||
|
||||
inst, args = parse(cmd)
|
||||
sym = :"inst_#{inst.inst}"
|
||||
__send__(sym, *args) != :fail
|
||||
end
|
||||
|
||||
# x86 conditional-jump mnemonics mapped to shared {Conditional::RELATION}
|
||||
# predicates (x86's adapter table, as {AArch64::COND} is for arm/aarch64).
|
||||
JCC = {
|
||||
'je' => :eq, 'jz' => :eq, 'jne' => :ne, 'jnz' => :ne,
|
||||
'jb' => :ult, 'jc' => :ult, 'jnae' => :ult, 'jae' => :uge, 'jnb' => :uge, 'jnc' => :uge,
|
||||
'ja' => :ugt, 'jnbe' => :ugt, 'jbe' => :ule, 'jna' => :ule,
|
||||
'jl' => :slt, 'jnge' => :slt, 'jge' => :sge, 'jnl' => :sge,
|
||||
'jg' => :sgt, 'jnle' => :sgt, 'jle' => :sle, 'jng' => :sle,
|
||||
'js' => :slt, 'jns' => :sge
|
||||
}.freeze
|
||||
|
||||
# x86 flag-setting compare mnemonics mapped to the ALU op whose result their
|
||||
# flags reflect (see {Conditional::COMPARE_OPS}). +test+ is a bitwise AND, +cmp+
|
||||
# a subtraction.
|
||||
COMPARES = { 'cmp' => :sub, 'test' => :and }.freeze
|
||||
|
||||
# Supported instruction set.
|
||||
# @return [Array<Instruction>] The supported instructions.
|
||||
def instructions
|
||||
@@ -84,6 +102,34 @@ module OneGadget
|
||||
|
||||
private
|
||||
|
||||
def branch_mnem?(mnem)
|
||||
mnem == 'jmp' || JCC.key?(mnem) || %w[jcxz jecxz jrcxz].include?(mnem)
|
||||
end
|
||||
|
||||
# Operands of +cmd+ (mnemonic dropped), size hints removed, each stripped of
|
||||
# a trailing +<symbol>+.
|
||||
def operands(cmd)
|
||||
cmd.sub(/\A[0-9a-f]+:\s*\S+\s*/, '').split(',').map do |o|
|
||||
o.gsub(/\b(XMMWORD|QWORD|DWORD|WORD|BYTE|PTR)\b/, '').strip.sub(/\s*<.*>\z/, '')
|
||||
end
|
||||
end
|
||||
|
||||
# The (direct) target address of a jump line.
|
||||
def jump_target(cmd)
|
||||
cmd[/\A[0-9a-f]+:\s*\S+\s+([0-9a-f]+)/, 1].to_i(16)
|
||||
end
|
||||
|
||||
def handle_branch(mnem, cmd)
|
||||
return true if mnem == 'jmp' # unconditional: control handled by the stitched path
|
||||
return branch_on_zero(jump_target(cmd), cx_reg(mnem), negate: false) if mnem.end_with?('cxz')
|
||||
|
||||
branch_on_compare(JCC[mnem], jump_target(cmd))
|
||||
end
|
||||
|
||||
def cx_reg(mnem)
|
||||
{ 'jcxz' => 'cx', 'jecxz' => 'ecx', 'jrcxz' => 'rcx' }[mnem]
|
||||
end
|
||||
|
||||
def inst_mov(dst, src)
|
||||
src = arg_to_lambda(src)
|
||||
if register?(dst)
|
||||
@@ -105,6 +151,8 @@ module OneGadget
|
||||
def inst_movaps(dst, src)
|
||||
# XXX: here we only support `movaps [sp+*], xmm*`
|
||||
src, dst = check_xmm_sp(src, dst) { raise_unsupported('movaps', dst, src) }
|
||||
raise_unsupported('movaps', dst, src) unless src.is_a?(Array)
|
||||
|
||||
off = dst.evaluate(eval_dict)
|
||||
@constraints << [:raw, "#{sp} & 0xf == #{0x10 - off & 0xf}"]
|
||||
(128 / self.class.bits).times do |i|
|
||||
@@ -120,10 +168,14 @@ module OneGadget
|
||||
if self.class.bits == 64 && xmm_reg?(dst) && src.start_with?('r') && register?(src)
|
||||
dst = arg_to_lambda(dst)
|
||||
src = arg_to_lambda(src)
|
||||
raise_unsupported('movq', dst, src) unless dst.is_a?(Array)
|
||||
|
||||
dst[0] = src
|
||||
return
|
||||
end
|
||||
dst, src = check_xmm_sp(dst, src) { raise_unsupported('movq', dst, src) }
|
||||
raise_unsupported('movq', dst, src) unless dst.is_a?(Array)
|
||||
|
||||
off = src.evaluate(eval_dict)
|
||||
(64 / self.class.bits).times do |i|
|
||||
dst[i] = sp_based_stack[off + i * size_t]
|
||||
@@ -134,6 +186,8 @@ module OneGadget
|
||||
def inst_movhps(dst, src)
|
||||
# XXX: here we only support `movhps xmm*, [sp+*]`
|
||||
dst, src = check_xmm_sp(dst, src) { raise_unsupported('movhps', dst, src) }
|
||||
raise_unsupported('movhps', dst, src) unless dst.is_a?(Array)
|
||||
|
||||
off = src.evaluate(eval_dict)
|
||||
(64 / self.class.bits).times do |i|
|
||||
dst[i + 64 / self.class.bits] = sp_based_stack[off + i * size_t]
|
||||
@@ -162,6 +216,8 @@ module OneGadget
|
||||
|
||||
dst = arg_to_lambda(dst)
|
||||
src = arg_to_lambda(src)
|
||||
raise_unsupported('punpcklqdq', dst, src) unless dst.is_a?(Array) && src.is_a?(Array)
|
||||
|
||||
(64 / self.class.bits).times do |i|
|
||||
dst[i + 64 / self.class.bits] = src[i]
|
||||
end
|
||||
@@ -200,6 +256,7 @@ module OneGadget
|
||||
end
|
||||
|
||||
def inst_sub(dst, src)
|
||||
check_register!(dst)
|
||||
src = arg_to_lambda(src)
|
||||
raise Error::UnsupportedInstructionArgumentError, "Unhandled -= of type #{src.class}" unless src.is_a?(Integer)
|
||||
|
||||
|
||||
@@ -52,7 +52,9 @@ module OneGadget
|
||||
(gadgets[j].constraints - g.constraints).empty?
|
||||
end
|
||||
end
|
||||
res.sort_by(&:offset)
|
||||
# The same offset can reach the call via more than one branch direction;
|
||||
# list it once, keeping the easiest (fewest-constraint) variant.
|
||||
res.group_by(&:offset).map { |_, gs| gs.min_by { |g| g.constraints.size } }.sort_by(&:offset)
|
||||
end
|
||||
end
|
||||
extend ClassMethods
|
||||
|
||||
@@ -7,15 +7,25 @@ module OneGadget
|
||||
module Fetcher
|
||||
# Define common methods for gadget fetchers.
|
||||
class AArch64 < Base
|
||||
# aarch64 condition codes (used to recognise +b.<cond>+ branches).
|
||||
CONDS = %w[eq ne cs hs cc lo mi pl vs vc hi ls ge lt gt le].freeze
|
||||
|
||||
private
|
||||
|
||||
def emulator
|
||||
OneGadget::Emulators::AArch64.new
|
||||
end
|
||||
|
||||
# If str contains a branch instruction.
|
||||
def branch?(str)
|
||||
%w[b b.hi b.gt b.eq b.le b.ls b.lt b.ne b.cs].any? { |f| str.include?(" #{f} ") }
|
||||
def branch_lead_chars
|
||||
'bct'
|
||||
end
|
||||
|
||||
def branch_kind(line)
|
||||
m = mnemonic(line)
|
||||
return :conditional if %w[cbz cbnz tbz tbnz].include?(m) || (m.start_with?('b.') && CONDS.include?(m[2..]))
|
||||
return :unconditional if %w[b b.al].include?(m)
|
||||
|
||||
:terminator if %w[ret br braa brab braaz brabz].include?(m)
|
||||
end
|
||||
|
||||
def call_str
|
||||
|
||||
@@ -13,43 +13,16 @@ module OneGadget
|
||||
OneGadget::Emulators::Amd64.new
|
||||
end
|
||||
|
||||
# The branch-aware walker (see {Base#branch_aware_candidates}) already
|
||||
# stitches +jmp+ targets, so only the filter remains.
|
||||
def candidates
|
||||
# one basic block case
|
||||
cands = super do |candidate|
|
||||
super do |candidate|
|
||||
next true if candidate.include?('posix_spawn@')
|
||||
next false unless candidate.include?(bin_sh_hex) # works in x86-64
|
||||
next false unless candidate.lines.last.include?('execve') # only care execve
|
||||
|
||||
true
|
||||
end
|
||||
cands + jmp_case_candidates
|
||||
end
|
||||
|
||||
# find gadgets in form:
|
||||
# lea rdi, '/bin/sh'
|
||||
# ...
|
||||
# jmp xxx
|
||||
# xxx:
|
||||
# ...
|
||||
# call execve
|
||||
def jmp_case_candidates
|
||||
`#{@objdump.command}|grep -E '# #{bin_sh_hex}' -A 8`.split('--').map do |cand|
|
||||
cand = cand.lines.map(&:strip).reject(&:empty?)
|
||||
jmp_at = cand.index { |c| c.include?('jmp') }
|
||||
next nil if jmp_at.nil?
|
||||
|
||||
cand = cand[0..jmp_at]
|
||||
next if cand.any? { |c| c.include?(call_str) }
|
||||
|
||||
jmp_addr = cand.last.scan(/jmp\s+([\da-f]+)\s/)[0][0].to_i(16)
|
||||
dump = `#{@objdump.command(start: jmp_addr, stop: jmp_addr + 100)}|grep -E '[0-9a-f]+:'`
|
||||
remain = dump.lines.map(&:strip).reject(&:empty?)
|
||||
call_execve = remain.index { |r| r.match(/call.*<execve[^+]*>/) }
|
||||
next if call_execve.nil?
|
||||
|
||||
remain = remain[0..call_execve]
|
||||
[cand + remain].join("\n")
|
||||
end.compact
|
||||
end
|
||||
|
||||
def bin_sh_hex
|
||||
|
||||
+107
-15
@@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'elftools'
|
||||
|
||||
require 'one_gadget/emulators/arm'
|
||||
require 'one_gadget/fetchers/base'
|
||||
|
||||
@@ -7,8 +9,92 @@ module OneGadget
|
||||
module Fetcher
|
||||
# Fetcher for 32-bit ARM (A32 / Thumb-2).
|
||||
class Arm < Base
|
||||
# Condition codes as branch suffixes (+bne+, +bcs+, ...).
|
||||
CONDS = %w[eq ne cs hs cc lo mi pl vs vc hi ls ge lt gt le].freeze
|
||||
|
||||
private
|
||||
|
||||
# Locate the `bl` sites that call a terminal function *without* disassembling
|
||||
# the whole libc - a full objdump of a Thumb-2 arm libc is ~4x slower than
|
||||
# amd64/aarch64 (~1.1s vs ~0.3s), and only ~0.35% of it is ever needed.
|
||||
#
|
||||
# We take the exec*/posix_spawn* addresses from the symbol table and scan
|
||||
# +.text+ for A32/Thumb `BL` instructions branching to them. This is exact
|
||||
# for the direct `bl <exec*>` calls the search cares about (validated to
|
||||
# match the objdump-grep sites on the arm-libc-2.23/2.27/2.39 fixtures);
|
||||
# any false positive merely adds a harmless empty window, and if the scan
|
||||
# finds nothing we fall back to a full disassembly.
|
||||
def terminal_call_sites
|
||||
File.open(file) do |f|
|
||||
elf = ELFTools::ELFFile.new(f)
|
||||
targets = terminal_symbol_addresses(elf)
|
||||
return [] if targets.empty?
|
||||
|
||||
text = elf.section_by_name('.text')
|
||||
return nil if text.nil?
|
||||
|
||||
scan_bl(text.header.sh_addr.to_i, text.data, targets)
|
||||
end
|
||||
rescue ELFTools::ELFError
|
||||
nil # not something we can scan; disassemble everything
|
||||
end
|
||||
|
||||
# Addresses (Thumb bit cleared) of exported/defined exec*/posix_spawn* funcs.
|
||||
def terminal_symbol_addresses(elf)
|
||||
addrs = {}
|
||||
%w[.dynsym .symtab].each do |name|
|
||||
sec = elf.section_by_name(name)
|
||||
next if sec.nil?
|
||||
|
||||
sec.each_symbols do |sym|
|
||||
value = sym.header.st_value.to_i
|
||||
addrs[value & ~1] = true if value.nonzero? && sym.name =~ /\A(exec|posix_spawn)/
|
||||
end
|
||||
end
|
||||
addrs
|
||||
end
|
||||
|
||||
# Scan +data+ (loaded at +base+) for A32/Thumb BL branches into +targets+.
|
||||
# Over-approximates (a stray word may decode to a BL into a target); that is
|
||||
# harmless - it just adds an empty window - as long as no real BL is missed.
|
||||
def scan_bl(base, data, targets)
|
||||
halves = data.unpack('v*') # 16-bit little-endian halfwords
|
||||
sites = halves.each_index.filter_map do |i|
|
||||
addr = base + i * 2
|
||||
addr if call_targets_at(halves, i, addr).any? { |t| targets.key?(t) }
|
||||
end
|
||||
sites.uniq
|
||||
end
|
||||
|
||||
# Masked target(s) of any A32/Thumb BL that could begin at halfword +i+.
|
||||
def call_targets_at(halves, i, addr)
|
||||
targets = []
|
||||
targets << (thumb_bl_target(addr, halves[i], halves[i + 1]) & ~1) if thumb_bl?(halves, i)
|
||||
targets << a32_bl_target(addr, halves[i] | (halves[i + 1] << 16)) if i.even? && halves[i + 1]
|
||||
targets.compact
|
||||
end
|
||||
|
||||
def thumb_bl?(halves, i)
|
||||
halves[i].between?(0xf000, 0xf7ff) && halves[i + 1]&.allbits?(0xd000)
|
||||
end
|
||||
|
||||
def thumb_bl_target(addr, high, low)
|
||||
s = (high >> 10) & 1
|
||||
i1 = (~(((low >> 13) & 1) ^ s)) & 1
|
||||
i2 = (~(((low >> 11) & 1) ^ s)) & 1
|
||||
imm = (s << 24) | (i1 << 23) | (i2 << 22) | ((high & 0x3ff) << 12) | ((low & 0x7ff) << 1)
|
||||
imm -= (1 << 25) if s == 1
|
||||
(addr + 4 + imm) & 0xffffffff
|
||||
end
|
||||
|
||||
def a32_bl_target(addr, word)
|
||||
return nil unless ((word >> 24) & 0x0f) == 0x0b # cond|101|1 => BL
|
||||
|
||||
imm = (word & 0xffffff) << 2
|
||||
imm -= (1 << 26) if imm.anybits?(1 << 25)
|
||||
(addr + 8 + imm) & 0xffffffff
|
||||
end
|
||||
|
||||
def emulator
|
||||
OneGadget::Emulators::Arm.new(file)
|
||||
end
|
||||
@@ -63,26 +149,32 @@ module OneGadget
|
||||
[disasm_lines[ldr_at], disasm_lines[add_at]]
|
||||
end
|
||||
|
||||
# The target's full objdump disassembly as stripped +"ADDR: insn"+ lines,
|
||||
# cached for the lifetime of the fetcher.
|
||||
def disasm_lines
|
||||
@disasm_lines ||= `#{@objdump.command}`.lines.map(&:strip).grep(/\A[0-9a-f]+:/)
|
||||
def branch_lead_chars
|
||||
'bct'
|
||||
end
|
||||
|
||||
# Map from an instruction's address to its index in {#disasm_lines}, so a
|
||||
# given address can be located in the disassembly in O(1).
|
||||
def disasm_index
|
||||
@disasm_index ||= disasm_lines.each_with_index.to_h { |line, i| [line[/\A([0-9a-f]+):/, 1].to_i(16), i] }
|
||||
# +b+ is unconditional; +bne+/+beq+/... and Thumb +cbz+/+cbnz+ are
|
||||
# conditional (+bl+/+blx+ are calls, not branches). +bx+/table branches and
|
||||
# returns via +pop {..,pc}+ / +ldm .. {..,pc}+ / +mov pc,..+ / +ldr pc,..+
|
||||
# terminate the path.
|
||||
def branch_kind(line)
|
||||
m = branch_mnemonic(line)
|
||||
return :conditional if conditional_mnemonic?(m)
|
||||
return :unconditional if m == 'b'
|
||||
return :terminator if m.start_with?('bx') || %w[tbb tbh].include?(m)
|
||||
return :terminator if %w[pop ldm ldmia ldmfd ldmdb].include?(m) && line.match?(/\bpc\b/)
|
||||
|
||||
:terminator if line.match?(/:\s*(mov|ldr)(\.[wn])?\s+pc\b/)
|
||||
end
|
||||
|
||||
# If str contains a branch instruction. +bl+/+blx+ are calls, not branches.
|
||||
def branch?(str)
|
||||
mnem = (str[/\A\s*[0-9a-f]+:\s*(\S+)/, 1] || str.split.first || '').sub(/\.(w|n)\z/, '')
|
||||
return false if mnem.start_with?('bl')
|
||||
def conditional_mnemonic?(mnem)
|
||||
return true if %w[cbz cbnz].include?(mnem)
|
||||
|
||||
mnem.match?(/\A(b|bx|cbn?z|tbb|tbh|
|
||||
b(eq|ne|cs|hs|cc|lo|mi|pl|vs|vc|hi|ls|ge|lt|gt|le)|
|
||||
bx(eq|ne|cs|hs|cc|lo|mi|pl|vs|vc|hi|ls|ge|lt|gt|le))\z/x)
|
||||
!mnem.start_with?('bl') && mnem.start_with?('b') && CONDS.include?(mnem[1..])
|
||||
end
|
||||
|
||||
def branch_mnemonic(line)
|
||||
mnemonic(line).sub(/\.[wn]\z/, '')
|
||||
end
|
||||
|
||||
def call_str
|
||||
|
||||
+281
-42
@@ -5,12 +5,49 @@ require 'one_gadget/fetchers/objdump'
|
||||
|
||||
module OneGadget
|
||||
module Fetcher
|
||||
# Define common methods for gadget fetchers.
|
||||
# Base of the per-architecture gadget fetchers. It discovers candidate
|
||||
# instruction sequences - a backward control-flow walk from each
|
||||
# +exec+/+posix_spawn+ call - and turns a solved candidate into a
|
||||
# {OneGadget::Gadget::Gadget}. A subclass supplies only the arch-specific
|
||||
# pieces (call mnemonic, string/global recognition, branch classification).
|
||||
#
|
||||
# To add an architecture, see +docs/adding-an-architecture.md+; +AArch64+ is
|
||||
# the simplest example.
|
||||
class Base
|
||||
# The absolute path to glibc.
|
||||
# @return [String] The filename.
|
||||
attr_reader :file
|
||||
|
||||
# Give up on a control-flow path once it has crossed this many conditional branches.
|
||||
MAX_FORKS = 4
|
||||
# Hard cap on a single path's length (loop/runaway guard).
|
||||
PATH_BUDGET = 80
|
||||
|
||||
# How much to disassemble around each terminal call when an architecture can
|
||||
# locate the calls cheaply (see {#terminal_call_sites} / {#windowed_disasm}).
|
||||
#
|
||||
# The backward walk stays within {PATH_BUDGET} instructions and {MAX_FORKS}
|
||||
# branch hops of the call, and a branch *into* that region comes from close
|
||||
# by. Measured across real amd64/aarch64/arm libcs, the earliest line the
|
||||
# walk reaches is <0x98b before the call and branches into it come from
|
||||
# <0x8bb further - 0x1246 all told. WINDOW_BACK = 0x2000 leaves ~60% margin;
|
||||
# a gadget whose code and branch-predecessors exceed it would be missed,
|
||||
# which is why windowing is opt-in per arch (fast disassembly arches keep
|
||||
# the full, exhaustive scan) and falls back to full disassembly if the call
|
||||
# scan comes up empty.
|
||||
WINDOW_BACK = 0x2000
|
||||
# A little past the call, enough to cover the call instruction itself.
|
||||
WINDOW_FWD = 0x80
|
||||
|
||||
# Cache values that are a deterministic function of an objdump command
|
||||
# (its output and everything derived from it), so re-analysing the same
|
||||
# file - common in the specs, harmless for the CLI which reads a file once -
|
||||
# doesn't redo the disassembly or the whole-binary scans.
|
||||
def self.cached(kind, command)
|
||||
@cached ||= Hash.new { |h, k| h[k] = {} }
|
||||
@cached[kind][command] ||= yield
|
||||
end
|
||||
|
||||
# Instantiate a fetcher object.
|
||||
# @param [String] file Absolute path to the target libc.
|
||||
def initialize(file)
|
||||
@@ -24,20 +61,41 @@ module OneGadget
|
||||
# @return [Array<OneGadget::Gadget::Gadget>] Gadgets found.
|
||||
def find
|
||||
str_offset('/bin/sh') # ensure it's glibc-like; raises "not glibc?" if not found
|
||||
candidates.map do |cand|
|
||||
gadgets = []
|
||||
# Overlapping candidate paths share tails, so the same suffix (a start line
|
||||
# and everything after it) recurs across candidates; emulate each once.
|
||||
seen = {}
|
||||
candidates.each do |cand|
|
||||
lines = cand.lines
|
||||
# use processor to find which can lead to a valid one-gadget call.
|
||||
gadgets = []
|
||||
(lines.size - 2).downto(0) do |i|
|
||||
processor = emulate(lines[i..])
|
||||
options = resolve(processor)
|
||||
next if options.nil? # impossible to be a gadget
|
||||
suffix = lines[i..]
|
||||
next if seen.key?(key = suffix.join)
|
||||
|
||||
offset = offset_of(lines[i])
|
||||
gadgets << OneGadget::Gadget::Gadget.new(offset, **options)
|
||||
seen[key] = true
|
||||
gadget = resolve_suffix(suffix)
|
||||
gadgets << gadget unless gadget.nil?
|
||||
end
|
||||
gadgets
|
||||
end.flatten
|
||||
end
|
||||
gadgets
|
||||
end
|
||||
|
||||
# Emulate a candidate suffix and turn it into a gadget, or +nil+ if it isn't one.
|
||||
def resolve_suffix(lines)
|
||||
processor = emulate(lines)
|
||||
# resolve reads argument registers, which may not be evaluable on an
|
||||
# exotic path; such a candidate simply isn't a gadget.
|
||||
options = begin
|
||||
resolve(processor)
|
||||
rescue OneGadget::Error::Error
|
||||
nil
|
||||
end
|
||||
return if options.nil?
|
||||
# A branch that compares a value with itself yields a trivial condition:
|
||||
# drop the gadget if it's unsatisfiable, else strip the always-true one.
|
||||
return if options[:constraints].any? { |c| contradiction?(c) }
|
||||
|
||||
options[:constraints] = options[:constraints].reject { |c| tautology?(c) }
|
||||
OneGadget::Gadget::Gadget.new(offset_of(lines.first), **options)
|
||||
end
|
||||
|
||||
# Fetch candidates that end with call exec*.
|
||||
@@ -50,22 +108,7 @@ module OneGadget
|
||||
# @return [Array<String>]
|
||||
# Each +String+ returned is multi-lines of assembly code.
|
||||
def candidates(&)
|
||||
call_regexp = "#{call_str}.*<(exec[^+]*|posix_spawn[^+]*)>$"
|
||||
cands = []
|
||||
`#{@objdump.command}|grep -E '#{call_regexp}' -B 30`.split('--').each do |cand|
|
||||
lines = cand.lines.map(&:strip).reject(&:empty?)
|
||||
# split with call_regexp
|
||||
loop do
|
||||
idx = lines.index { |l| l =~ /#{call_regexp}/ }
|
||||
break if idx.nil?
|
||||
|
||||
cands << lines.shift(idx + 1).join("\n")
|
||||
end
|
||||
end
|
||||
# remove all jmps
|
||||
cands = slice_prefix(cands, &method(:branch?))
|
||||
cands.select!(&) if block_given?
|
||||
cands
|
||||
branch_aware_candidates(&)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -144,6 +187,10 @@ module OneGadget
|
||||
# @param [OneGadget::Emulators::Lambda] lmda The parsed +argv_ptr+ (a stack register, zero dereference).
|
||||
def check_stack_argv(processor, argv_ptr, lmda, allow_null)
|
||||
stack = processor.get_corresponding_stack(lmda.obj)
|
||||
# A stack register we don't track a stack for (the frame pointer):
|
||||
# fall back to treating it as an opaque pointer.
|
||||
return check_nonstack_argv(lmda.to_s, allow_null) if stack.nil?
|
||||
|
||||
argv = (0..3).map { |i| stack[lmda.immi + processor.class.bits / 8 * i].to_s }
|
||||
|
||||
# if argv is already valid, no constraints are needed! (but probably won't happen :p)
|
||||
@@ -229,9 +276,13 @@ module OneGadget
|
||||
return global_var?(envp_ptr) if envp_ptr.start_with?('[[')
|
||||
|
||||
lmda = OneGadget::Emulators::Lambda.parse(envp_ptr)
|
||||
if lmda.deref_count.zero? && OneGadget::ABI.stack_register?(lmda.obj)
|
||||
# A concrete integer or a stack register we don't track a stack for
|
||||
# (the frame pointer) both fall through to the opaque-pointer case.
|
||||
stack = lmda.is_a?(OneGadget::Emulators::Lambda) && lmda.deref_count.zero? &&
|
||||
OneGadget::ABI.stack_register?(lmda.obj) &&
|
||||
processor.get_corresponding_stack(lmda.obj)
|
||||
if stack
|
||||
# I haven't see this case after some tests, but just in case :)
|
||||
stack = processor.get_corresponding_stack(lmda.obj)
|
||||
envp = (0..3).map { |i| stack[lmda.immi + processor.class.bits / 8 * i].to_s }
|
||||
# TODO: Handle the case when libc will write something into envp
|
||||
cons = global_var?(envp[0]) ? nil : "#{envp_ptr} == NULL || {#{envp.join(', ')}, ...} is a valid envp"
|
||||
@@ -268,6 +319,10 @@ module OneGadget
|
||||
# Meet all constraints then posix_spawn eventually calls execve(path, argv, envp)
|
||||
def resolve_posix_spawn(processor)
|
||||
args = Array.new(6) { |i| processor.argument(i) }
|
||||
# pid/file_actions/attrp are reasoned about as pointers (Lambdas); a
|
||||
# concrete non-zero integer there is a fixed address we can't constrain.
|
||||
return nil if [args[0], args[2], args[3]].any? { |a| a.is_a?(Integer) && !a.zero? }
|
||||
|
||||
res = resolve_execve_args(processor, args[1].to_s, args[4].to_s, args[5].to_s, allow_null_argv: false)
|
||||
return nil if res.nil?
|
||||
|
||||
@@ -311,19 +366,6 @@ module OneGadget
|
||||
[]
|
||||
end
|
||||
|
||||
def slice_prefix(cands, &block)
|
||||
cands.map do |cand|
|
||||
lines = cand.lines
|
||||
to_rm = lines[0...-1].rindex(&block)
|
||||
lines = lines[to_rm + 1..] unless to_rm.nil?
|
||||
lines.join
|
||||
end
|
||||
end
|
||||
|
||||
# If str contains a branch instruction.
|
||||
def branch?(_str); raise NotImplementedError
|
||||
end
|
||||
|
||||
def str_offset(str)
|
||||
File.binread(file).index("#{str}\x00") ||
|
||||
raise(Error::ArgumentError, "File #{file.inspect} doesn't contain string #{str.inspect}, not glibc?")
|
||||
@@ -332,6 +374,203 @@ module OneGadget
|
||||
def offset_of(assembly)
|
||||
assembly.scan(/^([\da-f]+):/)[0][0].to_i(16)
|
||||
end
|
||||
|
||||
# A single (non-disjunctive) branch condition comparing a value with
|
||||
# itself: +X == X+ / +X >= X+ is always true; +X != X+ / +X < X+ never is.
|
||||
def trivial_relation(con)
|
||||
return nil if con.include?(' || ')
|
||||
|
||||
m = con.match(/\A(?:\([su]\d+\))?(.+?) (==|!=|<=|>=|<|>) (.+)\z/)
|
||||
return nil unless m && m[1] == m[3]
|
||||
|
||||
%w[== >= <=].include?(m[2])
|
||||
end
|
||||
|
||||
def tautology?(con)
|
||||
trivial_relation(con) == true
|
||||
end
|
||||
|
||||
def contradiction?(con)
|
||||
trivial_relation(con) == false
|
||||
end
|
||||
|
||||
# Regexp (as a String) matching an objdump line that calls a terminal
|
||||
# function (+exec*+ / +posix_spawn*+) we can turn into a gadget.
|
||||
def terminal_call_regexp
|
||||
"#{call_str}.*<(exec[^+]*|posix_spawn[^+]*)>$"
|
||||
end
|
||||
|
||||
# Enumerate gadget candidates by walking the control-flow graph *backward*
|
||||
# from each terminal call along its predecessors (the instruction that falls
|
||||
# through to it, plus any branch that targets it), forking at conditional
|
||||
# edges up to {MAX_FORKS} times. Each emitted candidate is a flat,
|
||||
# address-preserving line-list ending at the terminal call, consumed
|
||||
# unchanged by {#find}; the emulator turns each crossed conditional edge into
|
||||
# a constraint (see {OneGadget::Emulators::Conditional}).
|
||||
def branch_aware_candidates(&)
|
||||
cands = []
|
||||
re = /#{terminal_call_regexp}/
|
||||
disasm_lines.each_index do |idx|
|
||||
next unless disasm_lines[idx].match?(re)
|
||||
|
||||
back_walk(idx, 0, Set.new, []) { |lines| cands << lines.join("\n") }
|
||||
end
|
||||
cands.uniq!
|
||||
cands.select!(&) if block_given?
|
||||
cands
|
||||
end
|
||||
|
||||
# Depth-first backward walk. +visited+ (a Set) and +path+ are mutated with
|
||||
# backtracking so a fork explores independently without per-step copies;
|
||||
# +path+ is built in forward order (the terminal call stays last). Emits a
|
||||
# candidate at each leaf - {#find} then tries every start line within it.
|
||||
def back_walk(idx, forks, visited, path, &blk)
|
||||
addr = disasm_addrs[idx]
|
||||
return if path.size >= PATH_BUDGET
|
||||
|
||||
visited.add(addr)
|
||||
path.unshift(disasm_lines[idx])
|
||||
edges = predecessors(idx).reject do |pidx, cond|
|
||||
visited.include?(disasm_addrs[pidx]) || (cond && forks >= MAX_FORKS)
|
||||
end
|
||||
if edges.empty?
|
||||
blk.call(path.dup)
|
||||
else
|
||||
edges.each { |pidx, cond| back_walk(pidx, forks + (cond ? 1 : 0), visited, path, &blk) }
|
||||
end
|
||||
path.shift
|
||||
visited.delete(addr)
|
||||
end
|
||||
|
||||
# Address of each disassembly line, by index. Cached (per objdump command).
|
||||
def disasm_addrs
|
||||
@disasm_addrs ||= Base.cached(:addrs, @objdump.command) { disasm_lines.map { |l| offset_of(l) } }
|
||||
end
|
||||
|
||||
# Predecessors of +disasm_lines[idx]+ as +[pred_index, conditional_edge?]+:
|
||||
# the instruction that falls through to it, plus any branch that targets it.
|
||||
# Computed lazily (the walk only touches lines near terminal calls) and cached.
|
||||
def predecessors(idx)
|
||||
(@predecessors ||= {})[idx] ||= begin
|
||||
preds = []
|
||||
if idx.positive?
|
||||
kind = branch_kind(disasm_lines[idx - 1])
|
||||
preds << [idx - 1, kind == :conditional] unless %i[unconditional terminator].include?(kind)
|
||||
end
|
||||
(branch_pred_map[disasm_addrs[idx]] || []).each do |b|
|
||||
preds << [b, branch_kind(disasm_lines[b]) == :conditional]
|
||||
end
|
||||
preds
|
||||
end
|
||||
end
|
||||
|
||||
# Map of target-address => indexes of (conditional or unconditional) direct
|
||||
# branches that jump there. Scans the whole disassembly once (a branch can
|
||||
# target a call region from anywhere), so keep the per-line test cheap.
|
||||
def branch_pred_map
|
||||
@branch_pred_map ||= Base.cached(:branch_pred, @objdump.command) do
|
||||
lead = branch_lead_regex
|
||||
disasm_lines.each_index.with_object(Hash.new { |h, k| h[k] = [] }) do |i, map|
|
||||
line = disasm_lines[i]
|
||||
# Cheap precompiled reject (no per-line allocation) before the full test.
|
||||
next unless line.match?(lead)
|
||||
next unless %i[conditional unconditional].include?(branch_kind(line))
|
||||
|
||||
tgt = branch_target(line)
|
||||
map[tgt] << i if tgt
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Precompiled over-approximation of a branch line, built from the arch's
|
||||
# {#branch_lead_chars}, to skip the full test for the (majority) non-branch
|
||||
# lines during the whole-binary scan.
|
||||
def branch_lead_regex
|
||||
@branch_lead_regex ||= /\A[0-9a-f]+:\s+[#{branch_lead_chars}]/
|
||||
end
|
||||
|
||||
# Parse the (direct) target address of a branch line, or +nil+ if indirect.
|
||||
def branch_target(line)
|
||||
line.sub(/\A[0-9a-f]+:\s*\S+\s*/, '')[/\b([0-9a-f]+)\b\s*(?:<|\z)/, 1]&.to_i(16)
|
||||
end
|
||||
|
||||
# The mnemonic of an objdump line, memoized because each line is tested by
|
||||
# several branch predicates during the CFG scan.
|
||||
# @example
|
||||
# mnemonic('4a1d0: b.ne 4a200 <foo>') #=> 'b.ne'
|
||||
# mnemonic('4a1c0: cbz x0, 4a200') #=> 'cbz'
|
||||
def mnemonic(line)
|
||||
(@mnemonic ||= {})[line] ||= line[/\A[0-9a-f]+:\s*(\S+)/, 1] || ''
|
||||
end
|
||||
|
||||
# Classify a disassembly +line+ for the control-flow walk (arch-specific):
|
||||
# :conditional - may or may not be taken; the walk explores both edges
|
||||
# and turns the decision into a gadget constraint
|
||||
# :unconditional - always taken, to a determined (direct) target
|
||||
# :terminator - ends the path with no determined successor (a return or
|
||||
# an indirect/computed jump)
|
||||
# nil - not a branch; execution falls through to the next line
|
||||
def branch_kind(_line); raise NotImplementedError
|
||||
end
|
||||
|
||||
# The leading character(s) of this arch's branch mnemonics, used to build
|
||||
# {#branch_lead_regex}. Must be plain regex-safe literals: they are spliced
|
||||
# into a character class, so no +]+, +\+, +-+ or +^+.
|
||||
# @example
|
||||
# 'bct' # aarch64/arm: b, cbz/cbnz, tbz/tbnz
|
||||
# 'j' # x86: jmp, jcc
|
||||
def branch_lead_chars; raise NotImplementedError
|
||||
end
|
||||
|
||||
# The target's full objdump disassembly as stripped +"ADDR: insn"+ lines,
|
||||
# cached for the lifetime of the fetcher.
|
||||
def disasm_lines
|
||||
@disasm_lines ||= Base.cached(:disasm, @objdump.command) do
|
||||
sites = terminal_call_sites
|
||||
sites.nil? || sites.empty? ? full_disasm : windowed_disasm(sites)
|
||||
end
|
||||
end
|
||||
|
||||
# Disassemble the whole file (the exhaustive default).
|
||||
def full_disasm
|
||||
objdump_lines
|
||||
end
|
||||
|
||||
# Disassemble only [call-WINDOW_BACK, call+WINDOW_FWD] around each call site,
|
||||
# merging overlaps. Used when {#terminal_call_sites} located the calls without
|
||||
# a full disassembly (the win on the slow-to-objdump Thumb-2 arm libcs).
|
||||
def windowed_disasm(sites)
|
||||
windows = sites.sort.map { |a| [[a - WINDOW_BACK, 0].max, a + WINDOW_FWD] }
|
||||
merge_ranges(windows).flat_map { |lo, hi| objdump_lines(start: lo, stop: hi) }
|
||||
end
|
||||
|
||||
# Merge a list of sorted [lo, hi] ranges, coalescing any that overlap.
|
||||
def merge_ranges(ranges)
|
||||
ranges.each_with_object([]) do |(lo, hi), merged|
|
||||
if merged.last && lo <= merged.last[1]
|
||||
merged.last[1] = hi if hi > merged.last[1]
|
||||
else
|
||||
merged << [lo, hi]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def objdump_lines(start: nil, stop: nil)
|
||||
`#{@objdump.command(start:, stop:)}`.lines.map(&:strip).grep(/\A[0-9a-f]+:/)
|
||||
end
|
||||
|
||||
# Addresses of `bl`/`call` sites reaching a terminal function, found without a
|
||||
# full disassembly. +nil+ (the default) means the arch has no cheap finder,
|
||||
# so the whole file is disassembled.
|
||||
def terminal_call_sites
|
||||
nil
|
||||
end
|
||||
|
||||
# Map from an instruction's address to its index in {#disasm_lines}, so a
|
||||
# given address can be located in the disassembly in O(1).
|
||||
def disasm_index
|
||||
@disasm_index ||= disasm_lines.each_with_index.to_h { |line, i| [line[/\A([0-9a-f]+):/, 1].to_i(16), i] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'one_gadget/emulators/x86'
|
||||
require 'one_gadget/fetchers/base'
|
||||
|
||||
module OneGadget
|
||||
module Fetcher
|
||||
# Define common methods for gadget fetchers.
|
||||
class X86 < Base
|
||||
# Conditional-jump mnemonics (shared with the emulator).
|
||||
JCC = OneGadget::Emulators::X86::JCC
|
||||
|
||||
private
|
||||
|
||||
# If str contains a branch instruction.
|
||||
def branch?(str)
|
||||
%w[jmp je jne jl jb ja jg].any? { |f| str.include?(f) }
|
||||
def branch_lead_chars
|
||||
'j'
|
||||
end
|
||||
|
||||
# +jcc+/+jcxz+ are conditional; a direct +jmp+ is unconditional while an
|
||||
# indirect one (+jmp rax+, +jmp QWORD PTR [..]+) and +ret+/+leave+ terminate.
|
||||
def branch_kind(line)
|
||||
m = mnemonic(line)
|
||||
return :conditional if JCC.key?(m) || %w[jcxz jecxz jrcxz].include?(m)
|
||||
return branch_target(line).nil? ? :terminator : :unconditional if m == 'jmp'
|
||||
|
||||
:terminator if m.start_with?('ret') || m == 'leave'
|
||||
end
|
||||
|
||||
def objdump_options
|
||||
|
||||
@@ -105,9 +105,30 @@ module OneGadget
|
||||
when / == NULL$/ then calculate_null_score(expr.sub(' == NULL', ''))
|
||||
when / <= 0$/ then calculate_null_score(expr.sub(' <= 0', ''))
|
||||
when / is a valid (argv|envp)$/ then 0.2 # This usually means the register has to be a readable pointer.
|
||||
when / (==|!=|<=|>=|<|>) / then calculate_relation_score(expr) # a branch condition
|
||||
end
|
||||
end
|
||||
|
||||
# Score a branch-derived relational constraint such as +x2 == 0x1+ or
|
||||
# +(u64)x0 >= 0x400+: an equality on one specific value is harder than an
|
||||
# inequality/range, and a dereferenced left-hand side is harder still.
|
||||
def calculate_relation_score(expr)
|
||||
op = expr[/ (==|!=|<=|>=|<|>) /, 1]
|
||||
lhs = expr.split(/ #{Regexp.escape(op)} /, 2).first.sub(/\A\([su]\d+\)/, '')
|
||||
base = op == '==' ? 0.4 : 0.6
|
||||
base * 0.9**lhs_deref_count(lhs)
|
||||
end
|
||||
|
||||
# Dereference depth of a relation's left side, used to weight the score.
|
||||
# A compound expression (e.g. +(a & b)+ from +tst+, +(a + b)+ from +cmn+) is
|
||||
# not a plain lambda and has no dereference, so it scores as depth 0.
|
||||
def lhs_deref_count(lhs)
|
||||
lmda = OneGadget::Emulators::Lambda.parse(lhs)
|
||||
lmda.is_a?(OneGadget::Emulators::Lambda) ? lmda.deref_count : 0
|
||||
rescue OneGadget::Error::Error
|
||||
0
|
||||
end
|
||||
|
||||
def calculate_writable_score(identity)
|
||||
lmda = OneGadget::Emulators::Lambda.parse(identity)
|
||||
return 0.81 if lmda.deref_count != 0
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ constraints:
|
||||
skip_unless_objdump
|
||||
|
||||
expect { described_class.work(%w[--force --raw --level 1] + [libc_file]) }.to output(<<-EOS).to_stdout
|
||||
324279 324286 324293 324386 939679 940120 940127 940131 1090444 1090456
|
||||
323640 324279 324286 324293 324386 939679 939705 939712 940120 940127 940131 1090444 1090456
|
||||
EOS
|
||||
end
|
||||
|
||||
|
||||
@@ -8,6 +8,62 @@ describe OneGadget::Emulators::AArch64 do
|
||||
@processor = described_class.new
|
||||
end
|
||||
|
||||
describe 'conditional branches' do
|
||||
# Emit the branch decision by feeding cmp, the branch, then a follow-up line
|
||||
# whose address decides taken (== target) vs not-taken (!= target).
|
||||
def branch_constraint(compare, branch, target, follow_addr)
|
||||
@processor.process("1000: #{compare}") if compare
|
||||
@processor.process("1004: #{branch} #{format('%x', target)} <x>")
|
||||
@processor.process("#{format('%x', follow_addr)}: nop")
|
||||
@processor.constraints
|
||||
end
|
||||
|
||||
it 'renders cmp + b.ne / b.eq' do
|
||||
expect(branch_constraint('cmp x2, #1', 'b.ne', 0x2000, 0x1008)).to eq ['x2 == 0x1'] # not taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint('cmp x2, #1', 'b.ne', 0x2000, 0x2000)).to eq ['x2 != 0x1'] # taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint('cmp x0, x1', 'b.eq', 0x2000, 0x2000)).to eq ['x0 == x1'] # taken
|
||||
end
|
||||
|
||||
it 'falls back for an unparseable compare operand' do
|
||||
expect(@processor.send(:operand_str, 'somelabel')).to eq 'somelabel'
|
||||
end
|
||||
|
||||
it 'renders signed and unsigned comparisons' do
|
||||
expect(branch_constraint('cmp x0, #0x400', 'b.cs', 0x2000, 0x2000)).to eq ['(u64)x0 >= 0x400'] # taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint('cmp x0, #0x400', 'b.lt', 0x2000, 0x1008)).to eq ['(s64)x0 >= 0x400'] # not taken
|
||||
end
|
||||
|
||||
it 'renders cbz / cbnz' do
|
||||
expect(branch_constraint(nil, 'cbz x0,', 0x2000, 0x1008)).to eq ['x0 != 0'] # not taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint(nil, 'cbnz x1,', 0x2000, 0x2000)).to eq ['x1 != 0'] # taken
|
||||
end
|
||||
|
||||
it 'renders tbz / tbnz bit tests' do
|
||||
expect(branch_constraint(nil, 'tbz w3, #4,', 0x2000, 0x2000)).to eq ['(w3 & 0x10) == 0'] # taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint(nil, 'tbnz w3, #0,', 0x2000, 0x1008)).to eq ['(w3 & 0x1) == 0'] # not taken
|
||||
end
|
||||
|
||||
it 'aborts a cmp-based branch with no preceding compare' do
|
||||
expect(@processor.process('1004: b.ne 2000 <x>')).to be false
|
||||
end
|
||||
|
||||
it 'uses eq/ne but rejects magnitude conditions after tst' do
|
||||
# tst x0,x0 sets ZF = (x0 == 0); b.eq taken means x0 == 0.
|
||||
expect(branch_constraint('tst x0, x0', 'b.eq', 0x2000, 0x2000)).to eq ['x0 == 0'] # taken
|
||||
# tst with distinct operands is a real bitmask test.
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint('tst x0, x1', 'b.ne', 0x2000, 0x2000)).to eq ['(x0 & x1) != 0'] # taken
|
||||
@processor = described_class.new
|
||||
expect(@processor.process('1000: tst x0, x0')).to be true
|
||||
expect(@processor.process('1004: b.lt 2000 <x>')).to be false # magnitude after tst: unsound
|
||||
end
|
||||
end
|
||||
|
||||
describe 'process' do
|
||||
it 'libc-2.23 gadget' do
|
||||
gadget = <<-EOS
|
||||
|
||||
@@ -7,6 +7,35 @@ describe OneGadget::Emulators::Amd64 do
|
||||
@processor = described_class.new
|
||||
end
|
||||
|
||||
describe 'conditional branches' do
|
||||
def branch_constraint(compare, branch, target, follow_addr)
|
||||
@processor.process("1000: #{compare}") if compare
|
||||
@processor.process("1004: #{branch} #{format('%x', target)} <x>")
|
||||
@processor.process("#{format('%x', follow_addr)}: nop")
|
||||
@processor.constraints
|
||||
end
|
||||
|
||||
it 'renders cmp + jcc (Intel mnemonics)' do
|
||||
expect(branch_constraint('cmp rax,0x1', 'jne', 0x2000, 0x1008)).to eq ['rax == 0x1'] # not taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint('cmp rax,0x400', 'jb', 0x2000, 0x2000)).to eq ['(u64)rax < 0x400'] # taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint('cmp rsi,rdi', 'jg', 0x2000, 0x2000)).to eq ['(s64)rsi > rdi'] # taken
|
||||
end
|
||||
|
||||
it 'renders test (zero flag only) and jrcxz' do
|
||||
# test eax,eax sets ZF = (eax == 0); je taken means eax == 0.
|
||||
expect(branch_constraint('test eax,eax', 'je', 0x2000, 0x2000)).to eq ['eax == 0']
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint(nil, 'jrcxz', 0x2000, 0x1008)).to eq ['rcx != 0'] # not taken
|
||||
end
|
||||
|
||||
it 'aborts a magnitude branch after test' do
|
||||
@processor.process('1000: test eax,eax')
|
||||
expect(@processor.process('1004: jg 2000 <x>')).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'process' do
|
||||
it 'libc-2.24 gadget' do
|
||||
gadget = <<-EOS
|
||||
|
||||
@@ -8,6 +8,31 @@ describe OneGadget::Emulators::Arm do
|
||||
@processor = described_class.new
|
||||
end
|
||||
|
||||
describe 'conditional branches' do
|
||||
def branch_constraint(compare, branch, target, follow_addr)
|
||||
@processor.process("1000: #{compare}") if compare
|
||||
@processor.process("1004: #{branch} #{format('%x', target)} <x>")
|
||||
@processor.process("#{format('%x', follow_addr)}: nop")
|
||||
@processor.constraints
|
||||
end
|
||||
|
||||
it 'renders cmp + bne / beq (condition suffixes)' do
|
||||
expect(branch_constraint('cmp r2, #1', 'bne', 0x2000, 0x1008)).to eq ['r2 == 0x1'] # not taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint('cmp r0, r1', 'beq.n', 0x2000, 0x2000)).to eq ['r0 == r1'] # taken
|
||||
end
|
||||
|
||||
it 'renders signed/unsigned and cbz' do
|
||||
expect(branch_constraint('cmp r0, #0x400', 'bcs', 0x2000, 0x2000)).to eq ['(u32)r0 >= 0x400'] # taken
|
||||
@processor = described_class.new
|
||||
expect(branch_constraint(nil, 'cbz r0,', 0x2000, 0x1008)).to eq ['r0 != 0'] # not taken
|
||||
end
|
||||
|
||||
it 'aborts a cmp-based branch with no preceding compare' do
|
||||
expect(@processor.process('1004: bne 2000 <x>')).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'process' do
|
||||
it 'libc-2.23 gadget (Thumb PIC /bin/sh idiom)' do
|
||||
# ldr rX, [pc, #imm] loads a literal-pool word; add rX, pc turns it into a
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'one_gadget/fetchers/arm'
|
||||
|
||||
describe OneGadget::Fetcher::Arm do
|
||||
def fetcher(version)
|
||||
OneGadget::Fetcher::Arm.new(data_path("arm-libc-#{version}.so"))
|
||||
end
|
||||
|
||||
# Terminal-call sites found by disassembling the whole libc and grepping.
|
||||
def objdump_sites(fetcher)
|
||||
cmd = fetcher.instance_variable_get(:@objdump).command
|
||||
lines = `#{cmd}`.lines.grep(/#{fetcher.send(:terminal_call_regexp)}/)
|
||||
lines.map { |l| l[/\A\s*([0-9a-f]+):/, 1].to_i(16) }.sort
|
||||
end
|
||||
|
||||
describe 'terminal_call_sites (BL byte scan)' do
|
||||
# The scan must miss no real bl-to-exec site (a false positive only adds a
|
||||
# harmless empty window, so we require superset, not equality). Gadget-level
|
||||
# correctness for every arm fixture is covered by one_gadget_arm_spec; here we
|
||||
# check the scanner directly on 2.27, whose scan includes one such false
|
||||
# positive. (One version keeps this from re-running a full objdump per libc.)
|
||||
it 'finds every objdump bl-to-exec site (superset)' do
|
||||
skip_unless_objdump
|
||||
f = fetcher('2.27')
|
||||
sites = f.send(:terminal_call_sites)
|
||||
expect(objdump_sites(f) - sites).to be_empty
|
||||
expect(sites).not_to be_empty
|
||||
end
|
||||
|
||||
it 'falls back to a full disassembly (nil) when the file is not scannable' do
|
||||
f = fetcher('2.23')
|
||||
allow(f).to receive(:file).and_return(__FILE__) # a Ruby source, not an ELF
|
||||
expect(f.send(:terminal_call_sites)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -35,4 +35,21 @@ describe OneGadget::Fetcher::Base do
|
||||
expect(yielded).to match(/\Arsp == NULL \|\| \{.*\} is a valid envp\z/)
|
||||
end
|
||||
end
|
||||
|
||||
# A branch comparing a value with itself (e.g. +cmp x0, x0+): "X == X" is always
|
||||
# true so the constraint is dropped; "X != X" never is, so the gadget is infeasible.
|
||||
describe '#tautology? / #contradiction?' do
|
||||
it 'classifies a self-comparison by its operator' do
|
||||
expect(fetcher.send(:tautology?, 'rax == rax')).to be true
|
||||
expect(fetcher.send(:tautology?, '(s64)x0 >= x0')).to be true
|
||||
expect(fetcher.send(:contradiction?, 'rax != rax')).to be true
|
||||
expect(fetcher.send(:contradiction?, 'x0 < x0')).to be true
|
||||
end
|
||||
|
||||
it 'leaves genuine relations and disjunctions alone' do
|
||||
expect(fetcher.send(:tautology?, 'x2 == 0x1')).to be false
|
||||
expect(fetcher.send(:contradiction?, 'x2 == 0x1')).to be false
|
||||
expect(fetcher.send(:trivial_relation, '[rax] == NULL || rax == rax')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,6 +86,15 @@ constraints:
|
||||
expect(new(['writable: x20+0x338']).score).to be_within(eps).of 0.81
|
||||
end
|
||||
|
||||
it 'branch conditions' do
|
||||
expect(new(['x2 == 0x1']).score).to be_within(eps).of 0.4 # equality
|
||||
expect(new(['(u64)x0 >= 0x400']).score).to be_within(eps).of 0.6 # inequality
|
||||
expect(new(['[x1+0x8] != 0']).score).to be_within(eps).of 0.54 # deref penalised
|
||||
expect(new(['(x3 & 0x10) == 0']).score).to be_within(eps).of 0.4 # bit-test, parses to x3+0x10
|
||||
expect(new(['(x0 & x1) != 0']).score).to be_within(eps).of 0.6 # distinct regs: lhs unparseable
|
||||
expect(new(['(s64)(x0 + 0x10) < 0']).score).to be_within(eps).of 0.6 # cmn compare: lhs unparseable
|
||||
end
|
||||
|
||||
it 'level 3' do
|
||||
expect(new(['[[x4+0xad0]] == NULL']).score).to be_within(eps).of 0.9**3
|
||||
expect(new(['x4+0xad0 == NULL']).score).to be_within(eps).of 0.1
|
||||
|
||||
@@ -11,7 +11,8 @@ describe 'one_gadget_aarch64' do
|
||||
it 'libc-2.23' do
|
||||
path = data_path('aarch64-libc-2.23.so')
|
||||
expect(OneGadget.gadgets(file: path, force_file: true, level: 1))
|
||||
.to eq [0x3d6c4, 0x3d6cc, 0x3d6d0, 0x3d6d4, 0x3d6dc, 0x3d718, 0x60c1c, 0x60c20]
|
||||
.to eq [0x3d6c4, 0x3d6cc, 0x3d6d0, 0x3d6d4, 0x3d6dc, 0x3d718,
|
||||
0x60c1c, 0x60c20, 0x9b9e0, 0x9b9e4, 0x9bc5c]
|
||||
end
|
||||
|
||||
it 'libc-2.24' do
|
||||
@@ -21,9 +22,7 @@ describe 'one_gadget_aarch64' do
|
||||
|
||||
it 'libc-2.27' do
|
||||
path = data_path('aarch64-libc-2.27.so')
|
||||
expect(OneGadget.gadgets(file: path,
|
||||
force_file: true)).to eq [0x3f160, 0x3f168, 0x3f16c,
|
||||
0x3f184, 0x3f1a8, 0x63e90]
|
||||
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x3f160, 0x63e90, 0xa32e8]
|
||||
end
|
||||
|
||||
# glibc 2.43 no longer exposes a straight-line execve("/bin/sh") gadget; the
|
||||
@@ -40,6 +39,17 @@ describe 'one_gadget_aarch64' do
|
||||
expect(gadgets.map(&:effect).uniq)
|
||||
.to eq ['posix_spawn(sp+0xc, "/bin/sh", 0, sp+0x218, sp+0x50, environ)']
|
||||
end
|
||||
|
||||
# The execve("/bin/sh") gadget in 2.43's execvp is split across a
|
||||
# `cmp x2, #1; b.ne`, so it only appears once the not-taken branch is
|
||||
# expressed as the constraint `x2 == 0x1`.
|
||||
it 'resolves a gadget guarded by a conditional branch' do
|
||||
path = data_path('aarch64-libc-2.43.so')
|
||||
gadget = OneGadget.gadgets(file: path, force_file: true, level: 1, details: true)
|
||||
.find { |g| g.offset == 0xc7a40 }
|
||||
expect(gadget.effect).to eq 'execve("/bin/sh", x4, x6)'
|
||||
expect(gadget.constraints).to include('x2 == 0x1')
|
||||
end
|
||||
end
|
||||
|
||||
it 'objdump not installed' do
|
||||
|
||||
@@ -12,7 +12,8 @@ describe 'one_gadget_amd64' do
|
||||
it 'libc-2.19' do
|
||||
path = data_path('libc-2.19-cf699a15caae64f50311fc4655b86dc39a479789.so')
|
||||
expect(OneGadget.gadgets(file: path, force_file: true, level: 1))
|
||||
.to eq [0x46421, 0x46428, 0x4647c, 0xc1ba3, 0xc1bf2, 0xe4968, 0xe5765, 0xe5771, 0xe66bd]
|
||||
.to eq [0x461fb, 0x46421, 0x46428, 0x4647c, 0xc1ba3, 0xc1bf2, 0xc1ca7, 0xe4968, 0xe5765, 0xe5771,
|
||||
0xe654d, 0xe6552, 0xe6557, 0xe6670, 0xe6677, 0xe6685, 0xe668a, 0xe668f, 0xe6697, 0xe669e, 0xe66bd]
|
||||
end
|
||||
|
||||
it 'libc-2.24' do
|
||||
@@ -36,11 +37,12 @@ describe 'one_gadget_amd64' do
|
||||
it 'libc-2.31' do
|
||||
path = data_path('libc-2.31-9fdb74e7b217d06c93172a8243f8547f947ee6d1.so')
|
||||
expect(OneGadget.gadgets(file: path, force_file: true,
|
||||
level: 1)).to eq [0x51e2b, 0x51e32, 0x51e39, 0x51e40, 0x51e45, 0x51e55, 0x51e5a,
|
||||
0x51e5d, 0x51e62, 0x84165, 0x8416c, 0x84173, 0x84176, 0x8417b,
|
||||
0x84180, 0x8418c, 0x84192, 0x84199, 0x841a0, 0xe3b2e, 0xe3b31,
|
||||
0xe3b34, 0xe3d23, 0xe3d26, 0xe3d92, 0xe3d99, 0xe3da0, 0xe3dd7,
|
||||
0xe3de1, 0xe3de5, 0xe3ded, 0x1075da, 0x1075e2, 0x1075e7, 0x1075f1]
|
||||
level: 1)).to eq [0x51df8, 0x51e2b, 0x51e32, 0x51e39, 0x51e40, 0x51e45, 0x51e55,
|
||||
0x51e5a, 0x51e5d, 0x51e62, 0x51e65, 0x840c3, 0x84165, 0x8416c,
|
||||
0x84173, 0x84176, 0x8417b, 0x84180, 0x8418c, 0x84192, 0x84199,
|
||||
0x841a0, 0x841a3, 0xe3b2e, 0xe3b31, 0xe3b34, 0xe3d23, 0xe3d26,
|
||||
0xe3d88, 0xe3d92, 0xe3d99, 0xe3da0, 0xe3dd7, 0xe3de1, 0xe3de5,
|
||||
0xe3ded, 0x1075e7, 0x1075f1, 0x107cea]
|
||||
expect(OneGadget.gadgets(file: path)).to eq [0xe3b2e, 0xe3b31, 0xe3b34]
|
||||
end
|
||||
|
||||
|
||||
@@ -15,12 +15,20 @@ describe 'one_gadget_arm' do
|
||||
|
||||
it 'libc-2.27' do
|
||||
path = data_path('arm-libc-2.27.so')
|
||||
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x2d39c, 0x73f7a]
|
||||
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x2d39c, 0x73f7a, 0x73f96]
|
||||
end
|
||||
|
||||
it 'libc-2.39' do
|
||||
path = data_path('arm-libc-2.39.so')
|
||||
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x38f6c, 0x88a48, 0x9ef1a]
|
||||
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x38f6c, 0x88a48, 0x9ef1a, 0x9f2bc]
|
||||
end
|
||||
|
||||
it 'resolves a gadget guarded by a conditional branch' do
|
||||
path = data_path('arm-libc-2.27.so')
|
||||
gadget = OneGadget.gadgets(file: path, force_file: true, level: 1, details: true)
|
||||
.find { |g| g.offset == 0x73f2c }
|
||||
expect(gadget.effect).to eq 'execve("/bin/sh", sp-0x10, r2)'
|
||||
expect(gadget.constraints).to include('[r1] == 0')
|
||||
end
|
||||
|
||||
it 'resolves environ through the GOT base register' do
|
||||
|
||||
Reference in New Issue
Block a user