david942jandClaude Opus 4.8 4732208300 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]>
2026-07-25 10:20:47 +08:00
2026-06-27 18:00:21 +08:00
2025-10-31 10:43:28 +08:00
2019-05-04 19:39:14 +08:00
2019-05-04 06:07:25 -07:00
2026-07-19 16:15:41 +08:00
2017-03-27 17:05:27 +08:00
2019-03-07 13:46:47 +08:00
2019-12-21 23:45:09 +08:00
2017-02-07 16:03:34 +08:00
2019-12-21 23:45:09 +08:00

Downloads

Gem Version Build Status Maintainability Code Coverage Inline docs Yard Docs MIT License

OneGadget

When playing ctf pwn challenges we usually need the one-gadget RCE (remote code execution), which leads to call execve('/bin/sh', NULL, NULL).

This gem provides such gadgets finder, no need to use objdump or IDA-pro every time like a fool 😉

To use this tool, type one_gadget /path/to/libc in command line and enjoy the magic 😆

Installation

Available on RubyGems.org!

$ gem install one_gadget

Note: requires ruby version >= 2.1.0, you can use ruby --version to check.

Supported Architectures

  • i386
  • amd64 (x86-64)
  • aarch64 (ARMv8)
  • arm (ARMv7, A32/Thumb-2)

Implementation

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.

Usage

Command Line Interface

$ one_gadget
# Usage: one_gadget <FILE|-b BuildID> [options]
#     -b, --build-id BuildID           BuildID[sha1] of libc.
#     -f, --[no-]force-file            Force search gadgets in file instead of build id first.
#     -l, --level OUTPUT_LEVEL         The output level.
#                                      OneGadget automatically selects gadgets with higher successful probability.
#                                      Increase this level to ask OneGadget show more gadgets it found.
#                                      Default: 0
#     -n, --near FUNCTIONS/FILE        Order gadgets by their distance to the given functions or to the GOT functions of the given file.
#     -o, --output-format FORMAT       Output format. FORMAT should be one of <pretty|raw|json>.
#                                      Default: pretty
#     -r, --raw                        Alias of -o raw. Output gadgets offset only, split with one space.
#     -s, --script exploit-script      Run exploit script with all possible gadgets.
#                                      The script will be run as 'exploit-script $offset'.
#         --info BuildID               Show version information given BuildID.
#         --base BASE_ADDRESS          The base address of libc.
#                                      Default: 0
#         --version                    Current gem version.

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
#   [rsi] == NULL || rsi == NULL || rsi is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp

x86_64

Given BuildID

$ one_gadget -b aad7dbe330f23ea00ca63daf793b766b51aceb5d
# 0x4557a execve("/bin/sh", rsp+0x30, environ)
# constraints:
#   [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
#
# 0xf1651 execve("/bin/sh", rsp+0x40, environ)
# constraints:
#   [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
#
# 0xf24cb execve("/bin/sh", rsp+0x60, environ)
# constraints:
#   [rsp+0x60] == NULL || {[rsp+0x60], [rsp+0x68], [rsp+0x70], [rsp+0x78], ...} is a valid argv

build id

Gadgets Near Functions

Why

Consider this scenario when exploiting:

  1. Able to write on GOT (Global Offset Table)
  2. Base address of libc is unknown

In this scenario you can choose to write two low-byte on a GOT entry with one-gadget's two low-byte. If the function offset on GOT is close enough with the one-gadget, you will have at least 1/16 chance of success.

Usage

Reorder gadgets according to the distance of given functions.

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6 --near exit,mkdir
# [OneGadget] Gadgets near exit(0x46a40):
# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
#   [rsi] == NULL || rsi == NULL || rsi is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# [OneGadget] Gadgets near mkdir(0x10de70):
# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
#   [rsi] == NULL || rsi == NULL || rsi is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#

near

Regular expression is acceptable.

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6 --near 'write.*' --raw
# [OneGadget] Gadgets near writev(0x114690):
# 932612 932609 932606
#
# [OneGadget] Gadgets near write(0x10e280):
# 932612 932609 932606
#

Pass an ELF file as the argument, OneGadget will take all GOT functions for processing.

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6 --near spec/data/test_near_file.elf --raw
# [OneGadget] Gadgets near exit(0x46a40):
# 932606 932609 932612
#
# [OneGadget] Gadgets near puts(0x84420):
# 932606 932609 932612
#
# [OneGadget] Gadgets near printf(0x61c90):
# 932606 932609 932612
#
# [OneGadget] Gadgets near strlen(0x9f630):
# 932606 932609 932612
#
# [OneGadget] Gadgets near __cxa_finalize(0x46f10):
# 932606 932609 932612
#
# [OneGadget] Gadgets near __libc_start_main(0x23f90):
# 932606 932609 932612
#

Show All Gadgets

Sometimes one_gadget finds too many gadgets to show them in one screen, by default gadgets would be filtered automatically according to the difficulty of constraints.

Use option --level 1 to show all gadgets found instead of only those with higher probabilities.

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6 --level 1
# 0x51dfb posix_spawn(rsp+0xc, "/bin/sh", 0, rbp, rsp+0x50, environ)
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   {"sh", "-c", rbx, NULL} is a valid argv
#   rbp == NULL || (u16)[rbp] == NULL
#
# 0x51e02 posix_spawn(rsp+0xc, "/bin/sh", 0, rbp, rsp+0x50, environ)
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   rax == NULL || {"sh", rax, rbx, NULL} is a valid argv
#   rbp == NULL || (u16)[rbp] == NULL
#
# 0x51e09 posix_spawn(rsp+0xc, "/bin/sh", 0, rbp, rsp+0x50, environ)
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   rcx == NULL || {rcx, rax, rbx, NULL} is a valid argv
#   rbp == NULL || (u16)[rbp] == NULL
#
# 0x51e10 posix_spawn(rsp+0xc, "/bin/sh", rdx, rbp, rsp+0x50, environ)
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   rcx == NULL || {rcx, (u64)xmm1, rbx, NULL} is a valid argv
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rbp == NULL || (u16)[rbp] == NULL
#
# 0x51e15 posix_spawn(rsp+0xc, "/bin/sh", rdx, rbp, rsp+0x50, environ)
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   (u64)xmm0 == NULL || {(u64)xmm0, (u64)xmm1, rbx, NULL} is a valid argv
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rbp == NULL || (u16)[rbp] == NULL
#
# 0x51e25 posix_spawn(rdi, "/bin/sh", rdx, rbp, rsp+0x50, [rax])
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   (u64)xmm0 == NULL || {(u64)xmm0, (u64)(xmm0 >> 64), rbx, NULL} is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rdi == NULL || writable: rdi
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rbp == NULL || (u16)[rbp] == NULL
#
# 0x51e2a posix_spawn(rdi, "/bin/sh", rdx, rbp, r8, [rax])
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   [r8] == NULL || r8 is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rdi == NULL || writable: rdi
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rbp == NULL || (u16)[rbp] == NULL
#
# 0x51e2d posix_spawn(rdi, "/bin/sh", rdx, rcx, r8, [rax])
# constraints:
#   address rsp+0x60 is writable
#   rsp & 0xf == 0
#   [r8] == NULL || r8 is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rdi == NULL || writable: rdi
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rcx == NULL || (u16)[rcx] == NULL
#
# 0x51e32 posix_spawn(rdi, "/bin/sh", rdx, rcx, r8, [rax])
# constraints:
#   address rsp+0x68 is writable
#   rsp & 0xf == 0
#   [r8] == NULL || r8 is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rdi == NULL || writable: rdi
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rcx == NULL || (u16)[rcx] == NULL
#
# 0x84135 posix_spawn(rbx+0xe0, "/bin/sh", r12, 0, rsp+0x60, environ)
# constraints:
#   address rsp+0x70 is writable
#   rsp & 0xf == 0
#   {"sh", "-c", rbp, NULL} is a valid argv
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   r12 == NULL || (s32)[r12+0x4] <= 0
#
# 0x8413c posix_spawn(rbx+0xe0, "/bin/sh", r12, 0, rsp+0x60, environ)
# constraints:
#   address rsp+0x70 is writable
#   rsp & 0xf == 0
#   rax == NULL || {"sh", rax, rbp, NULL} is a valid argv
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   r12 == NULL || (s32)[r12+0x4] <= 0
#
# 0x84143 posix_spawn(rbx+0xe0, "/bin/sh", r12, 0, rsp+0x60, environ)
# constraints:
#   address rsp+0x70 is writable
#   rsp & 0xf == 0
#   rcx == NULL || {rcx, rax, rbp, NULL} is a valid argv
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   r12 == NULL || (s32)[r12+0x4] <= 0
#
# 0x84146 posix_spawn(rbx+0xe0, "/bin/sh", rdx, 0, rsp+0x60, environ)
# constraints:
#   address rsp+0x70 is writable
#   rsp & 0xf == 0
#   rcx == NULL || {rcx, rax, rbp, NULL} is a valid argv
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#
# 0x8414b posix_spawn(rbx+0xe0, "/bin/sh", rdx, 0, rsp+0x60, environ)
# constraints:
#   address rsp+0x78 is writable
#   rsp & 0xf == 0
#   rcx == NULL || {rcx, rax, [rsp+0x70], NULL} is a valid argv
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#
# 0x84150 posix_spawn(rbx+0xe0, "/bin/sh", rdx, 0, rsp+0x60, environ)
# constraints:
#   address rsp+0x78 is writable
#   rsp & 0xf == 0
#   rcx == NULL || {rcx, (u64)xmm1, [rsp+0x70], NULL} is a valid argv
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#
# 0x8415c posix_spawn(rbx+0xe0, "/bin/sh", rdx, 0, rsp+0x60, [rax])
# constraints:
#   address rsp+0x78 is writable
#   rsp & 0xf == 0
#   (u64)xmm0 == NULL || {(u64)xmm0, (u64)xmm1, [rsp+0x70], NULL} is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#
# 0x84162 posix_spawn(rbx+0xe0, "/bin/sh", rdx, rcx, rsp+0x60, [rax])
# constraints:
#   address rsp+0x78 is writable
#   rsp & 0xf == 0
#   (u64)xmm0 == NULL || {(u64)xmm0, (u64)(xmm0 >> 64), [rsp+0x70], NULL} is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rbx+0xe0 == NULL || writable: rbx+0xe0
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rcx == NULL || (u16)[rcx] == NULL
#
# 0x84169 posix_spawn(rdi, "/bin/sh", rdx, rcx, rsp+0x60, [rax])
# constraints:
#   address rsp+0x78 is writable
#   rsp & 0xf == 0
#   (u64)xmm0 == NULL || {(u64)xmm0, (u64)(xmm0 >> 64), [rsp+0x70], NULL} is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rdi == NULL || writable: rdi
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rcx == NULL || (u16)[rcx] == NULL
#
# 0x84170 posix_spawn(rdi, "/bin/sh", rdx, rcx, r8, [rax])
# constraints:
#   address rsp+0x78 is writable
#   rsp & 0xf == 0
#   [r8] == NULL || r8 is a valid argv
#   [[rax]] == NULL || [rax] == NULL || [rax] is a valid envp
#   rdi == NULL || writable: rdi
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#   rcx == NULL || (u16)[rcx] == NULL
#
# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
#   [r15] == NULL || r15 == NULL || r15 is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
#   [rsi] == NULL || rsi == NULL || rsi is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# 0xe3cf3 execve("/bin/sh", r10, r12)
# constraints:
#   address rbp-0x78 is writable
#   [r10] == NULL || r10 == NULL || r10 is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3cf6 execve("/bin/sh", r10, rdx)
# constraints:
#   address rbp-0x78 is writable
#   [r10] == NULL || r10 == NULL || r10 is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
#
# 0xe3d62 execve("/bin/sh", rbp-0x50, r12)
# constraints:
#   address rbp-0x48 is writable
#   r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3d69 execve("/bin/sh", rbp-0x50, r12)
# constraints:
#   address rbp-0x48 is writable
#   rax == NULL || {rax, r13, NULL} is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3d70 execve("/bin/sh", rbp-0x50, r12)
# constraints:
#   address rbp-0x50 is writable
#   rax == NULL || {rax, [rbp-0x48], NULL} is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3da7 execve("/bin/sh", rbp-0x50, r12)
# constraints:
#   address rbp-0x50 is writable
#   [rbp-0x68] == NULL || {"/bin/sh", [rbp-0x68], NULL} is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3db1 execve("/bin/sh", rbp-0x50, r12)
# constraints:
#   address rbp-0x50 is writable
#   rax == NULL || {rax, [rbp-0x68], NULL} is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3db5 execve("/bin/sh", r10, r12)
# constraints:
#   addresses r10+0x10, rbp-0x50 are writable
#   [r10] == NULL || r10 == NULL || r10 is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0xe3dbd execve("/bin/sh", r10, r12)
# constraints:
#   addresses r10+0x10, rbp-0x48 are writable
#   [r10] == NULL || r10 == NULL || r10 is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp
#
# 0x1077ca posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x38], 0, rsp+0x70, [rsp+0xf0])
# constraints:
#   [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
#   [[rsp+0xf0]] == NULL || [rsp+0xf0] == NULL || [rsp+0xf0] is a valid envp
#   [rsp+0x38] == NULL || (s32)[[rsp+0x38]+0x4] <= 0
#
# 0x1077d2 posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x38], 0, rsp+0x70, r9)
# constraints:
#   [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
#   [r9] == NULL || r9 == NULL || r9 is a valid envp
#   [rsp+0x38] == NULL || (s32)[[rsp+0x38]+0x4] <= 0
#
# 0x1077d7 posix_spawn(rsp+0x64, "/bin/sh", rdx, 0, rsp+0x70, r9)
# constraints:
#   [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
#   [r9] == NULL || r9 == NULL || r9 is a valid envp
#   rdx == NULL || (s32)[rdx+0x4] <= 0
#
# 0x1077e1 posix_spawn(rdi, "/bin/sh", rdx, 0, r8, r9)
# constraints:
#   [r8] == NULL || r8 is a valid argv
#   [r9] == NULL || r9 == NULL || r9 is a valid envp
#   rdi == NULL || writable: rdi
#   rdx == NULL || (s32)[rdx+0x4] <= 0

Other Architectures

i386
$ one_gadget /lib32/libc.so.6
# 0xc890b execve("/bin/sh", [ebp-0x2c], esi)
# constraints:
#   address ebp-0x20 is writable
#   ebx is the GOT address of libc
#   [[ebp-0x2c]] == NULL || [ebp-0x2c] == NULL || [ebp-0x2c] is a valid argv
#   [esi] == NULL || esi == NULL || esi is a valid envp
#
# 0x1421b3 execl("/bin/sh", eax)
# constraints:
#   ebp is the GOT address of libc
#   eax == NULL
#
# 0x1421b4 execl("/bin/sh", [esp])
# constraints:
#   ebp is the GOT address of libc
#   [esp] == NULL

i386

AArch64
$ one_gadget spec/data/aarch64-libc-2.27.so
# 0x3f160 execve("/bin/sh", sp+0x70, environ)
# constraints:
#   address x20+0x338 is writable
#   x3 == NULL
#
# 0x3f184 execve("/bin/sh", sp+0x70, environ)
# constraints:
#   addresses x19+0x4, x20+0x338 are writable
#   [sp+0x70] == NULL
#
# 0x3f1a8 execve("/bin/sh", x21, environ)
# constraints:
#   addresses x19+0x4, x20+0x338 are writable
#   [x21] == NULL || x21 == NULL
#
# 0x63e90 execl("/bin/sh", x1)
# constraints:
#   x1 == NULL

aarch64

ARM32
$ one_gadget spec/data/arm-libc-2.39.so
# 0x38f6c posix_spawn(r0, "/bin/sh", r2, r8, [sp], r3)
# constraints:
#   [[sp]] == NULL || [sp] is a valid argv
#   [r3] == NULL || r3 == NULL || r3 is a valid envp
#   r0 == NULL || writable: r0
#   r2 == NULL || (s32)[r2+0x4] <= 0
#   r8 == NULL || (u16)[r8] == NULL
#
# 0x88a48 execve("/bin/sh", r4, r5)
# constraints:
#   [r4] == NULL || r4 == NULL || r4 is a valid argv
#   [r5] == NULL || r5 == NULL || r5 is a valid envp
#
# 0x9ef1a posix_spawn([sp+0x34], "/bin/sh", [sp+0x2c], 0, [sp+0x3c], r3)
# constraints:
#   [[sp+0x3c]] == NULL || [sp+0x3c] is a valid argv
#   [r3] == NULL || r3 == NULL || r3 is a valid envp
#   [sp+0x34] == NULL || writable: [sp+0x34]
#   [sp+0x2c] == NULL || (s32)[[sp+0x2c]+0x4] <= 0

Combine with Script

Pass your exploit script as one_gadget's arguments, it can try all gadgets one by one, so you don't need to try every possible gadgets manually.

$ one_gadget ./spec/data/libc-2.19.so -s 'echo "offset ->"'

--script

In Ruby Scripts

require 'one_gadget'
OneGadget.gadgets(file: '/lib/x86_64-linux-gnu/libc.so.6')
#=> [932606, 932609, 932612]

# or in shorter way
one_gadget('/lib/x86_64-linux-gnu/libc.so.6', level: 1)
#=> [335355, 335362, 335369, 335376, 335381, 335397, 335402, 335405, 335410, 540981, 540988, 540995, 540998, 541003, 541008, 541020, 541026, 541033, 541040, 932606, 932609, 932612, 933107, 933110, 933218, 933225, 933232, 933287, 933297, 933301, 933309, 1079242, 1079250, 1079255, 1079265]

# from build id
one_gadget('b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0')
#=> [324286, 324293, 324386, 1090444]

To Python Lovers

import subprocess
def one_gadget(filename):
  return [int(i) for i in subprocess.check_output(['one_gadget', '--raw', filename]).decode().split(' ')]

one_gadget('/lib/x86_64-linux-gnu/libc.so.6')
#=> [932606, 932609, 932612]

Make OneGadget Better

Any suggestion or feature request is welcome! Feel free to send a pull request.

Please let me know if you find any libc that make OneGadget fail to find gadgets. And, if you like this work, I'll be happy to be starred 😬

S
Description
The best tool for finding one gadget RCE in libc.so.6
Readme MIT
25 MiB
Languages
Ruby 99.7%
C 0.1%