Open-source project
david942j/one_gadget avatar
david942j/one_gadget

What david942j/one_gadget computes, and how to read its constraints

The best tool for finding one gadget RCE in libc.so.6

2,356 stars149 forksRubyMIT

At a glance

What is it?
david942j/one_gadget is a Ruby gem that finds addresses in a glibc where a single jump reaches execve("/bin/sh"), along with the register and memory conditions each address needs. It is built for CTF pwn work and for anyone reverse engineering a libc, and its output is only as good as its constraint lists, which the project now verifies by running them.
Who is it for?
Use david942j/one_gadget when you have a specific libc in hand and want candidate offsets in seconds instead of a session in a disassembler, and treat the constraint lists as part of the answer rather than decoration. Do not use it as your only source for a target: the offsets are libc-version specific, BuildID lookups reach out to the project repository, and the search starts from exec and posix_spawn calls, so a libc built with different code may yield nothing.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 14 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The specific question david942j/one_gadget answers

Exploiting a heap or stack bug on a glibc binary usually runs into the same wall: you control some memory, you may know where your library is mapped, and you would rather not hand-assemble a ROP chain just to get a shell. The project defines the target narrowly. It looks for a single address in libc that, when jumped to, calls `execve("/bin/sh", NULL, NULL)`, and the README states the motivation in plain terms: while solving CTF pwn challenges, this is what you usually want, and this way you do not have to dig through objdump or IDA Pro every time.

So the audience is two groups who happen to want the same artifact. The first is people working through binary exploitation exercises and capture-the-flag pwn tasks, who know what a GOT entry is and just want the offsets. The second is reverse engineers who want a cross-check: someone else has already done the tedious walk, and a list of addresses is a cheap way to decide whether it is worth opening the binary by hand.

What it is not is a general exploitation framework. The output is offsets, one instruction sequence per candidate, and a set of conditions. Turning an offset into control of a remote process is still your problem, and the README does not pretend otherwise.

Walking the control-flow graph backward from exec and posix_spawn

The mechanism is symbolic execution over the code surrounding each candidate address, combined with a backward walk that gives the search its shape. OneGadget starts from every `exec` and `posix_spawn` call in the library and walks the control-flow graph backwards, following conditional branches in both directions. Everything it passes through becomes part of the analysis.

When a candidate is only reachable if a branch is taken, or only reachable if it is not, that decision is recorded as a constraint. The README gives the shape of one as `x2 == 0x1`. A stack-slot version looks different but means the same thing: `[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv`. Each candidate therefore ships with a precondition list describing what the registers and stack must look like at the instant of the jump.

Two design consequences follow from starting at `exec` and `posix_spawn`. A gadget that reaches a shell by some other route is out of scope by construction, so an unusual libc build can legitimately produce nothing. And because the search follows branches backwards, the cost grows with the size of the reachable region behind each call, which is why the tool prints a level rather than every candidate it can find. Level 0 is the default and, per the CLI help, the tool selects gadgets with higher successful probability at that level; raising it asks for more of what it found, not for a different analysis.

Installing the gem and reading your first gadget list

Installation is a single gem command, and the README says the gem is available on RubyGems.org:

bash
gem install one_gadget

Point it at a libc file and it prints candidates with their constraints. Using the file bundled in the repository as the example:

bash
one_gadget spec/data/libc-2.31-9fdb74e7b217d06c93172a8243f8547f947ee6d1.so
0xe3b31 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp
0xe3b34 execve("/bin/sh", rsi, rdx)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
0xe3c20 execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp
0x107cea posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x38], 0, rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
  [rsp+0x38] == NULL || (s32)[[rsp+0x38]+0x4] <= 0x0

Four candidates at the default level for this glibc, three of them `execve` and one `posix_spawn`, each with a precondition list. Read the constraints as a checklist, not as decoration. The first gadget needs `r15` to point at NULL or at a valid argv, and `rdx` to point at NULL or at a valid envp. If you can arrange neither, that offset is useless to you even though it is on the list.

Two flags earn their place early. `-b` takes a BuildID instead of a path, and `--info` prints version information for a BuildID, which is the quickest way to confirm you are analysing the library you think you are. `-o json` changes the output format to `<pretty|raw|json>`, and `-r` is a shorthand alias for `raw` that prints offsets only, one per space-separated entry, which is what you want when feeding a list into your own tooling.

Aletheia: why an unverified constraint list is worth little

The most interesting engineering decision in the repository is not the search. It is the checking. The README puts it plainly: constraints are only worth as much as they are complete, so the gadgets the repository ships are verified by being run.

The harness is called Aletheia. It loads the libc in a live process, arranges exactly what a gadget's constraints ask for, poisons every register and page the constraints do not mention, jumps to the offset, and then requires a real `/bin/sh` to come back and list the root directory. The poisoning step is the important one. A gadget that works only because a leftover register happened to hold a usable pointer would pass a naive test; here it fails, and the README says that is how several missing constraints were found.

You can run the same check yourself from a clone of the repository:

bash
bundle exec rake aletheia:verify                # every libc under spec/data, level 0
bundle exec rake "aletheia:verify[1, aarch64]"  # one output level, one architecture

Two practical notes. Aletheia is development tooling: it lives outside `lib/` and `bin/`, so it is not part of the published gem, which means the verification path only exists in a checkout. And it works by spawning a real shell, so a verification run on a build machine is spawning `/bin/sh` processes with an attacker-shaped libc loaded. That is a development environment decision, not a reason to avoid the tool, but it belongs on the checklist before you wire the task into CI.

Where the gadget data comes from, and where it runs out

The gem is not a disassembler that only works offline. It carries a payload: the gadgets of the libcs most likely to be asked for, described as the glibc of every Ubuntu LTS still in standard support plus every libc the tests cover. When you hand it a BuildID that is not in that set, the README says the gadgets are fetched from the project repository, which keeps them all.

That design has two consequences worth planning around. First, an offline or air-gapped workflow is limited to the shipped builds plus any local file you analyse directly, and a BuildID miss becomes a failed lookup rather than a slow one. Second, the results for a given BuildID arrive as data maintained by the project rather than as a computation performed on your machine. If you are the kind of user who verifies offsets before trusting them, Aletheia's model is the right instinct applied to the wrong layer: nothing in the README describes verifying a fetched BuildID result.

There is also a coverage ceiling that is not a bug. Because the search starts from `exec` and `posix_spawn` calls, a libc compiled with different hardening, a different symbol layout, or a different C library implementation may contain no reachable candidate at all. The tool supports i386, amd64, aarch64, arm in both A32 and Thumb-2 modes, riscv64, and mips in o32 with both endiannesses. Architectures outside that list are simply out of scope.

One-gadget offsets against hand disassembly and against a ROP chain

The honest alternative is what the README tells you it replaces: opening the libc in objdump or IDA Pro and finding the `execve` call sites yourself. The difference in approach is not accuracy but cost. Reading disassembly gives you the ground truth for your specific file and costs an afternoon per library. OneGadget gives you every candidate for the same file in about a second and hands you the preconditions, but it can only report what its backward walk from `exec` and `posix_spawn` reaches.

The other alternative is building a ROP chain. A chain gives you full control: you can set every argument to `execve` exactly, so there are no constraints to satisfy and no probability to reason about. What it costs is gadgets, layout, and the assumption that you can point the chain's registers somewhere useful. A one-gadget offset removes all of that and replaces it with a coin flip on the state of registers you did not set. The `-n` option exists for the case where that trade goes the other way: if you can write to the GOT but the libc base address is unknown, overwriting the low two bytes of a GOT entry with the low two bytes of a nearby gadget leaves one nibble to guess, which the README puts at least a 1 in 16. That is strictly worse than a chain you control, and strictly better than guessing a base address.

The practical conclusion is that these are different tools for different states of knowledge. An offset list is a fast filter; a chain is the fallback when the constraints cannot be met.

MIT licensing, a 2.0 boundary, and what a recent release run means

The project is MIT licensed, with the text in the repository's `LICENSE` file. That permits use inside closed-source and commercial work, and requires the notice to travel with copies. This is a reading of the licence text rather than legal advice, and it is the least complicated thing about adopting the gem.

The release history deserves more attention. Three tags landed within about a week: v2.0.0 on 2026-08-29, then v2.1.0 and v2.1.1 both on 2026-09-05. The repository is not archived and the last push was on 2026-09-15, so the project is being worked on right now. A 2.0 tag on a gem is a compatibility break by the usual reading of the version number, and the two rapid minor tags afterwards look like settling work rather than a stable plateau. The repository has a `CHANGELOG.md` at its top level, so the specifics of what changed are recorded rather than inferred, and that file is the first place to look before upgrading an existing dependency across the boundary.

One flag deserves a mention for anyone scripting the tool. `-s, --script` runs a supplied exploit script once per possible gadget, invoked as `exploit-script $offset`. That is convenient for batch testing, and it is also arbitrary code execution driven by a list you did not write, so it belongs behind the same review as any other script hook.

Editorial conclusion

Use david942j/one_gadget when you have a specific libc in hand and want candidate offsets in seconds instead of a session in a disassembler, and treat the constraint lists as part of the answer rather than decoration. Do not use it as your only source for a target: the offsets are libc-version specific, BuildID lookups reach out to the project repository, and the search starts from exec and posix_spawn calls, so a libc built with different code may yield nothing. Verify first by running one_gadget against a file you already have, comparing the offsets with your own disassembly of the same file, and reading CHANGELOG.md before upgrading across the 2.0 boundary.

Frequently asked questions

Which architectures does david942j/one_gadget support?

The README lists i386, amd64 (x86-64), aarch64 (ARMv8), arm (ARMv7, in both A32 and Thumb-2 modes), riscv64 (RV64GC), and mips (MIPS32 o32, big- and little-endian). Architectures outside that list are not covered.

How do I install david942j/one_gadget and run it?

The gem is available on RubyGems.org and installs with gem install one_gadget. You then point it at a libc file, as in one_gadget /path/to/libc, or at a BuildID with the -b option. Its licence is the MIT License.

What do the constraints next to a one_gadget offset mean?

They are the conditions that must hold when you jump to the offset, for example [r15] == NULL || r15 == NULL || r15 is a valid argv. They come from following conditional branches both ways during the backward walk, and the README says the shipped gadgets are verified by running them under the Aletheia harness, which fails a gadget whose list is missing something.

How does one_gadget find gadgets, and can it return nothing?

It walks the control-flow graph backwards from each exec and posix_spawn call, following conditional branches in both directions, and symbolically executes the code around each candidate. Because the search starts from those two calls, a libc with different code may contain no reachable candidate.

Where do the gadget offsets come from when I pass a BuildID?

The gem carries gadgets for the glibc of every Ubuntu LTS still in standard support and for every libc the tests cover. Any other BuildID is fetched from the project repository, which keeps them all, while a local file path is analysed directly.

How do I sort one_gadget output by distance to a function?

Use the -n option, or --near, followed by the functions you care about or the path to a file whose GOT functions should be used. The README describes it as sorting the gadgets by how far they are from the functions you name, and it is aimed at partial GOT overwrites when the libc base address is unknown.

Official sources

  1. david942j/one_gadget on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/david942j-one-gadget.svg)](https://hysenlabs.com/projects/david942j-one-gadget)