Skip to content

Add ARM execve chain support and a ret2libc spawn_shell generator (x86/x86_64/ARM)#199

Open
robbiegal wants to merge 1 commit into
sashs:masterfrom
robbiegal:master
Open

Add ARM execve chain support and a ret2libc spawn_shell generator (x86/x86_64/ARM)#199
robbiegal wants to merge 1 commit into
sashs:masterfrom
robbiegal:master

Conversation

@robbiegal

@robbiegal robbiegal commented May 31, 2026

Copy link
Copy Markdown

Summary

Two additions to the ROP-chain generator:

  1. ARM --chain execveArchitectureArm previously raised "ArchitectureArm does not have support for execve chain generation at the moment." It now generates a real execve("/bin/sh", NULL, NULL) chain.
  2. New --chain spawn_shell for x86, x86_64 and ARM — a ret2libc system("/bin/sh") generator, with verified system@plt resolution and, when the command string isn't already in the binary, a write-what-where .bss fallback.

Both degrade gracefully (clearly-labelled placeholders + log messages) when a binary lacks the required gadgets, consistent with the existing generators.

spawn_shell usage

ropper --file ./target --chain spawn_shell                            # system("/bin/sh")
ropper --file ./target --chain "spawn_shell cmd=/bin/dash"            # any path
ropper --file ./target --chain "spawn_shell address=0x7ffff7a52290"   # libc system()
ropper --file ./target --chain "spawn_shell string=0x601060"          # &"/bin/sh"

Options: cmd (default /bin/sh, any path), address (libc system), string (&cmd), align (x86_64 — toggle the stack-alignment ret, default on).

Per-architecture layout:

Arch Chain Convention
x86 [&system][ret][&"/bin/sh"] cdecl — argument on the stack, no call gadget needed
x86_64 [pop rdi; ret][&"/bin/sh"][ret][&system] System V — arg in rdi; extra ret keeps the stack 16-byte aligned for glibc movaps
ARM [pop {r0,..,pc}][&"/bin/sh"→r0][pad][&system→pc] AAPCS — arg in r0, system reached via the pc slot

Address resolution (graceful precedence)

  • system(): address= (absolute, not rebased) → defined system symbol (.symtab/.dynsym, rebased) → verified system@plt → import hint + placeholder.
  • "/bin/sh": string= (absolute) → existing copy already in the binary (rebased, no write) → write cmd into a writable scratch section, .bss preferred (rebased) → placeholder.

system@plt is resolved by disassembling .plt/.plt.sec/.plt.got and returning the stub whose indirect jump provably dereferences system's GOT slot (x86_64 jmp [rip+disp], x86 jmp [abs], ARM add ip, pc, #imm[,#rot] ; … ; ldr pc, [ip, #imm]!). The match is verified against the relocation target, so a wrong address is never emitted silently; x86 PIE jmp [ebx+off] (GOT base unknown at rest) and stripped PLTs fall back to the hint + placeholder.

The .bss write is chosen by reading the ELF section headers directly (SHF_WRITE | SHF_ALLOC, non-TLS, sh_size ≥ needed), preferring .bss then .data then any other writable section — so writability and free space are verified natively. .bss is SHT_NOBITS, so its size is taken from sh_size rather than from getSection('.bss') (whose .size/.bytes would crash on a NOBITS section). The written string is always NUL-terminated, and the pointer to it is emitted with the same rebase_N(offset) convention as the write so they always coincide.

ARM execve support

ArchitectureArm had no gadget categories (so every ARM gadget was uncategorised) and no svc 0 ending. This PR adds:

  • Gadget categories for ARM: LOAD_REG (pop {…pc} / ldm sp!, {…pc}), WRITE_MEM, LOAD_MEM, SYSCALL.
  • The svc 0 SYS ending (LE + BE).
  • A generator (RopChainARMExecve) that discovers terminal pop-pc/ldm dispatch gadgets, sets r0/r1/r2/r7 with a re-loading-aware register-cover planner, writes the command into .data, and emits TODO placeholders for any missing piece (e.g. svc 0, an r0 pop).

Files changed

 README.md                              |   3 +-
 ropper/arch.py                         |  25 +-
 ropper/console.py                      |   4 +-
 ropper/options.py                      |   3 +-
 ropper/ropchain/arch/__init__.py       |   1 +
 ropper/ropchain/arch/ropchainarm.py    | 569 ++++++++++++++++++++++  (new)
 ropper/ropchain/arch/ropchainx86.py    |  81 ++++-
 ropper/ropchain/arch/ropchainx86_64.py |  87 ++++-
 ropper/ropchain/ropchain.py            | 388 ++++++++++++++++++++++
 testcases/test_chain_arm.py            |  56 ++++  (new)
 testcases/test_chain_spawn_shell.py    | 342 +++++++++++++++++  (new)
 11 files changed, 1552 insertions(+), 7 deletions(-)
File Type Description
ropper/ropchain/ropchain.py mod Shared base helpers: _findSymbolAddress, _findImportGotSlot, _findPltStub (PLT disassembly), _findExistingString, _findWritableSection (native .bss/writable-section selection), _nulTerminateAndPad, _useBinaryForRebase, _rebaseLine, and the ret2libc building blocks _resolveSystemAddress / _resolveBinshPointer.
ropper/arch.py mod ArchitectureArm gadget categories + svc 0 SYS ending; ArchitectureArmBE endianness ending.
ropper/ropchain/arch/ropchainarm.py new ARM chain module: RopChainARM base, RopChainARMExecve, RopChainARMSpawnShell.
ropper/ropchain/arch/ropchainx86.py mod RopChainX86SpawnShell + arch hooks (_writeCmdToMemory); registered in availableGenerators.
ropper/ropchain/arch/ropchainx86_64.py mod RopChainSpawnShellX86_64 + arch hooks; _paddingNeededFor regex fix (below).
ropper/ropchain/arch/__init__.py mod Export the ARM generators for RopChain.__subclasses__() discovery.
ropper/console.py mod Interactive ropchain help text for spawn_shell (incl. cmd= and the .bss write) and ARM execve.
ropper/options.py mod spawn_shell added to the --help "available rop chain generators" list; execve marked [Linux x86, x86_64, ARM].
README.md mod Mirror of the same --help generator list.
testcases/test_chain_arm.py new ARM execve regression tests.
testcases/test_chain_spawn_shell.py new spawn_shell tests across all three arches: PLT resolution ground-truthed by an independent disassembly path, .bss selection, NUL-termination, and pointer/write-address consistency.

Fixes made during development

  • Scratch-buffer pointer was off by imageBase. getSection().offset is already image-base-relative, but it was routed through a helper that subtracts imageBase again, so the written string and the pointer to it disagreed. The pointer is now emitted with the same rebase_N(offset) convention as the write.
  • x86_64 _paddingNeededFor dropped pop r8/pop r9. The ^pop (...)$ regex matched exactly three characters, silently omitting padding for the 2-character extended registers and letting the following chain word be consumed. Broadened to \w{2,3} (also hardens the existing execve/mprotect generators).
  • Duplicate IMAGE_BASE. _useBinaryForRebase registered the binary with a synthetic section identity that didn't match the gadgets', emitting two identical IMAGE_BASE/rebase_N lines for one file. It now reuses the gadgets' own (fileName, section) identity, so a single entry is emitted.
  • Missing NUL terminator on ARM. The ARM string writer padded only to a 4-byte boundary, so a command whose length was a multiple of 4 (e.g. /bin/cat) was written without a terminator — masked only by a zero-filled .bss. All three writers now share _nulTerminateAndPad, which appends the NUL unconditionally before word-aligning.

Tests

$ python -m pytest testcases/ -q
58 passed

spawn_shell's PLT resolution is ground-truthed against the relocation table on ls-x86, ls-x86_64 and ls-arm: every resolved stub dereferences exactly its symbol's GOT slot.

ARM --chain execve: ArchitectureArm previously raised "does not have
support for execve chain generation". This adds ARM gadget categories
(pop/ldm pop-pc LOAD_REG, WRITE_MEM, LOAD_MEM, SYSCALL) and the svc 0
SYS ending, plus a generator that discovers terminal pop-pc / ldm
dispatch gadgets, fills r0/r1/r2/r7 with a re-loading-aware register
cover planner, writes the command into .data, and emits clearly-labelled
placeholders when a gadget (svc 0, an r0 pop, ...) is missing.

New --chain spawn_shell for x86, x86_64 and ARM: a ret2libc
system("/bin/sh") generator emitting the correct per-arch calling
convention -- x86 cdecl (stack arg), x86_64 System V (rdi + a movaps
stack-alignment ret), ARM AAPCS (r0 + pc dispatch). Options: cmd
(default /bin/sh, any path), address (libc system), string (&cmd),
align (x86_64).

system() resolution: address= > a system symbol defined in the binary
(rebased) > verified system@plt > import hint > placeholder. _findPltStub
disassembles .plt/.plt.sec/.plt.got and returns the stub whose indirect
jump provably dereferences system's GOT slot (x86_64 jmp [rip+disp],
x86 jmp [abs], ARM add ip,pc,#imm[,#rot] ; ... ; ldr pc,[ip,#imm]!),
verified against the relocation, so it never emits a silently-wrong
address; x86 PIE jmp [ebx+off] and stripped PLTs fall back to the hint.

"/bin/sh" resolution: string= > an existing copy already in the binary
(no write) > write the string into a writable scratch section, .bss
preferred, via write-what-where gadgets > placeholder. _findWritableSection
reads ELF section headers directly (SHF_WRITE|SHF_ALLOC, not TLS,
sh_size >= needed) so writability and free space are verified natively;
it avoids getSection('.bss') because .bss is SHT_NOBITS (raw is None,
size comes from sh_size).

Correctness fixes:
- The scratch-buffer pointer was double-imageBase-subtracted (pointer !=
  written bytes); it now uses the same rebase_N(offset) convention as the
  write.
- x86_64 _paddingNeededFor's ^pop (...)$ regex dropped pop r8/pop r9
  (2-char regs), under-padding chains; broadened to \w{2,3} (also hardens
  execve/mprotect).
- _useBinaryForRebase registers the binary with the gadgets' own
  (fileName, section) identity, so a later gadget registration reuses the
  entry instead of emitting a duplicate IMAGE_BASE.
- The written string is always NUL-terminated: the shared
  _nulTerminateAndPad appends the NUL unconditionally before word-aligning
  (ARM previously skipped it for cmd lengths that were a multiple of 4).

Help text (--help epilog in options.py, README.md, interactive ropchain
help in console.py) lists spawn_shell, the .bss write and cmd=, and marks
execve as ARM-capable. Tests: testcases/test_chain_arm.py and
testcases/test_chain_spawn_shell.py; full suite 58/58.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sashs

sashs commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Thank you very much for the PR. I will test it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants