Find MIPS gadgets (#448)

* Find gadgets in a MIPS libc

* Pass over the fixtures that state no BuildID
This commit is contained in:
david942j
2026-09-03 01:13:27 +08:00
committed by GitHub
parent cb70256331
commit e9df151656
15 changed files with 406 additions and 21 deletions
+6
View File
@@ -15,6 +15,12 @@ releases are cut by giving that section a version and a date.
#422, #423, #425). Every gadget the riscv64 fixture reports has been run under
a debugger and seen to spawn a real shell, the same bar the other
architectures are held to (#424).
- MIPS (32-bit o32) support, in both byte orders (#448) -- big-endian is as widely
deployed as little-endian on this architecture, so both are read. Two things
about it are unlike the other architectures and are handled without the rest of
the engine having to know: a call names no target, going through the GOT
instead, and a branch or call takes effect one instruction late, so what follows
it runs first.
- Reading a libc that ships without section headers, as an embedded one commonly
does -- OpenWrt strips them from musl to save flash (#432). Nothing is missing
from such a file, so the symbols are read through `PT_DYNAMIC` and the code is
+28
View File
@@ -33,6 +33,7 @@ $ gem install one_gadget
- [x] aarch64 (ARMv8)
- [x] arm (ARMv7, A32/Thumb-2)
- [x] riscv64 (RV64GC)
- [x] mips (MIPS32 o32, big- and little-endian)
## Implementation
@@ -600,6 +601,33 @@ $ one_gadget spec/data/riscv64-libc-2.39.so
```
##### MIPS
Big-endian is as common as little-endian here, and both are read. This one is
OpenWrt's musl for ath79, which ships with no section headers -- the shape a
router's libc usually arrives in.
```bash
$ one_gadget spec/data/mips-musl-1.2.4.so
# 0x56fd4 execl("/bin/sh", a1-0x7c70)
# constraints:
# gp is the GOT address of libc
# a1-0x7c70 == NULL
#
# 0x77b44 posix_spawn(sp+0x20, "/bin/sh", s2, 0, sp+0x2c, environ)
# constraints:
# gp is the GOT address of libc
# {"sh", "-c", s7, NULL} is a valid argv
# s2 == NULL || (s32)[s2+0x4] <= 0x0
#
# 0x77b48 posix_spawn(sp+0x20, "/bin/sh", s2, 0, sp+0x2c, environ)
# constraints:
# gp is the GOT address of libc
# v0-0x7c70 == NULL || {v0-0x7c70, "-c", s7, NULL} is a valid argv
# s2 == NULL || (s32)[s2+0x4] <= 0x0
```
#### Combine with Script
Pass your exploit script to `one_gadget` and it runs the script once per gadget, so you
don't have to work through them by hand.
+11
View File
@@ -33,6 +33,7 @@ $ gem install one_gadget
- [x] aarch64 (ARMv8)
- [x] arm (ARMv7, A32/Thumb-2)
- [x] riscv64 (RV64GC)
- [x] mips (MIPS32 o32, big- and little-endian)
## Implementation
@@ -166,6 +167,16 @@ SHELL_OUTPUT_OF(one_gadget spec/data/arm-libc-2.39.so)
SHELL_OUTPUT_OF(one_gadget spec/data/riscv64-libc-2.39.so)
```
##### MIPS
Big-endian is as common as little-endian here, and both are read. This one is
OpenWrt's musl for ath79, which ships with no section headers -- the shape a
router's libc usually arrives in.
```bash
SHELL_OUTPUT_OF(one_gadget spec/data/mips-musl-1.2.4.so)
```
#### Combine with Script
Pass your exploit script to `one_gadget` and it runs the script once per gadget, so you
don't have to work through them by hand.
+1
View File
@@ -869,6 +869,7 @@ libc-2.35-a91d1bc05ce8f54989de1172c24b2b736378901f
libc-2.35-ab265082cac9486923c709d48ee5dde080e243ff
libc-2.35-c376d41cff4473142a97ac1ff1eab433859dc3d4
libc-2.35-dfca8b65dd2d2ca67f70dc7a556a6cfa8ba96ed8
libc-2.36-019d4db0c5d400a83e3b5f3bebee6ac38dee3c48
libc-2.39-328820b908de8ea1ef79afa8995e302e819163d7
libc-2.39-54ae5337044aaabab70d65e0c3444f7e4735e059
libc-2.39-70d6616bcfd43eaa73962220df98dd9c82ca6687
+1 -1
View File
@@ -111,7 +111,7 @@ Reading the flow:
The last three have defaults in `Base` that read the `$base`-relative form an arch
produces once it concretizes a pc-relative operand, so an arch that renders
addresses that way (aarch64, arm, amd64, riscv64) implements none of them. Override
addresses that way (aarch64, arm, amd64, mips, riscv64) implements none of them. Override
them only where the address reads differently — i386 reaches a global through its
GOT register, so all three are its own.
| `branch_kind(line)` | classify an instruction: `:conditional`, `:unconditional`, `:terminator`, or `nil` (not a branch) |
@@ -0,0 +1,168 @@
require 'one_gadget/gadget'
# spec/data/mipsel-libc-2.36.so
#
# MIPS R3000
#
# GNU C Library (Debian GLIBC 2.36-9+deb12u14) stable release version 2.36.
# Copyright (C) 2022 Free Software Foundation, Inc.
# This is free software; see the source for copying conditions.
# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE.
# Compiled by GNU CC version 12.2.0.
# libc ABIs: MIPS_PLT UNIQUE MIPS_O32_FP64 ABSOLUTE MIPS_XHASH
# Minimum supported kernel: 3.2.0
# For bug reporting instructions, please see:
# <http://www.debian.org/Bugs/>.
build_id = File.basename(__FILE__, '.rb').split('-').last
OneGadget::Gadget.add(build_id, 308164,
constraints: ["gp is the GOT address of libc", "a0 != v1", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308164,
constraints: ["gp is the GOT address of libc", "a0 == v1", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308168,
constraints: ["gp is the GOT address of libc", "a0 != v1", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308168,
constraints: ["gp is the GOT address of libc", "a0 == v1", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308172,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308176,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308180,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308184,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "sp+0x1fc == NULL || (u16)[sp+0x1fc] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, sp+0x1fc, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308188,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308192,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308196,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308200,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308204,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "writable: a0", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308208,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "writable: a0", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308212,
constraints: ["gp is the GOT address of libc", "readable: [sp+0x28]", "writable: a0", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308216,
constraints: ["gp is the GOT address of libc", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308220,
constraints: ["gp is the GOT address of libc", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308224,
constraints: ["gp is the GOT address of libc", "readable: a1", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308228,
constraints: ["gp is the GOT address of libc", "readable: a1", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308232,
constraints: ["gp is the GOT address of libc", "readable: a1", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308236,
constraints: ["gp is the GOT address of libc", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308240,
constraints: ["gp is the GOT address of libc", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308244,
constraints: ["gp is the GOT address of libc", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308248,
constraints: ["gp is the GOT address of libc", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308252,
constraints: ["gp is the GOT address of libc", "writable: s1", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "(u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308256,
constraints: ["gp is the GOT address of libc", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "s1 == NULL || (u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308260,
constraints: ["gp is the GOT address of libc", "{\"sh\", \"-c\", s5, NULL} is a valid argv", "s1 == NULL || (u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, sp+0x5c, environ)")
OneGadget::Gadget.add(build_id, 308264,
constraints: ["gp is the GOT address of libc", "[v1] == NULL || v1 is a valid argv", "s1 == NULL || (u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, v1, environ)")
OneGadget::Gadget.add(build_id, 308268,
constraints: ["gp is the GOT address of libc", "[v1] == NULL || v1 is a valid argv", "s1 == NULL || (u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", 0, s1, v1, environ)")
OneGadget::Gadget.add(build_id, 308272,
constraints: ["gp is the GOT address of libc", "[v1] == NULL || v1 is a valid argv", "a2 == NULL || (s32)[a2+0x4] <= 0x0", "s1 == NULL || (u16)[s1] == 0x0"],
effect: "posix_spawn(sp+0x3c, \"/bin/sh\", a2, s1, v1, environ)")
OneGadget::Gadget.add(build_id, 308276,
constraints: ["gp is the GOT address of libc", "[v1] == NULL || v1 is a valid argv", "a0 == NULL || writable: a0", "a2 == NULL || (s32)[a2+0x4] <= 0x0", "s1 == NULL || (u16)[s1] == 0x0"],
effect: "posix_spawn(a0, \"/bin/sh\", a2, s1, v1, environ)")
OneGadget::Gadget.add(build_id, 308280,
constraints: ["gp is the GOT address of libc", "[v1] == NULL || v1 is a valid argv", "a0 == NULL || writable: a0", "a2 == NULL || (s32)[a2+0x4] <= 0x0", "s1 == NULL || (u16)[s1] == 0x0"],
effect: "posix_spawn(a0, \"/bin/sh\", a2, s1, v1, environ)")
OneGadget::Gadget.add(build_id, 308284,
constraints: ["gp is the GOT address of libc", "[v1] == NULL || v1 is a valid argv", "a0 == NULL || writable: a0", "a2 == NULL || (s32)[a2+0x4] <= 0x0", "a3 == NULL || (u16)[a3] == 0x0"],
effect: "posix_spawn(a0, \"/bin/sh\", a2, a3, v1, environ)")
OneGadget::Gadget.add(build_id, 308288,
constraints: ["gp is the GOT address of libc", "[v1] == NULL || v1 is a valid argv", "a0 == NULL || writable: a0", "a2 == NULL || (s32)[a2+0x4] <= 0x0", "a3 == NULL || (u16)[a3] == 0x0"],
effect: "posix_spawn(a0, \"/bin/sh\", a2, a3, v1, environ)")
OneGadget::Gadget.add(build_id, 308292,
constraints: ["gp is the GOT address of libc", "readable: v0", "[v1] == NULL || v1 is a valid argv", "[[v0]] == NULL || [v0] == NULL || [v0] is a valid envp", "a0 == NULL || writable: a0", "a2 == NULL || (s32)[a2+0x4] <= 0x0", "a3 == NULL || (u16)[a3] == 0x0"],
effect: "posix_spawn(a0, \"/bin/sh\", a2, a3, v1, [v0])")
OneGadget::Gadget.add(build_id, 467588,
constraints: ["gp is the GOT address of libc", "[$base+0x1d32fc] == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467592,
constraints: ["gp is the GOT address of libc", "[v0+0x32fc] == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467596,
constraints: ["gp is the GOT address of libc", "[v0+0x32fc] == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467600,
constraints: ["gp is the GOT address of libc", "[v1+0x32fc] == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467604,
constraints: ["gp is the GOT address of libc", "[v1+0x32fc] == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467608,
constraints: ["gp is the GOT address of libc", "[v1+0x32fc] == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467612,
constraints: ["gp is the GOT address of libc", "s5 == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467616,
constraints: ["gp is the GOT address of libc", "s5 == 0x0", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467620,
constraints: ["gp is the GOT address of libc", "writable: s2+0x4", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467664,
constraints: ["gp is the GOT address of libc", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, environ)")
OneGadget::Gadget.add(build_id, 467668,
constraints: ["gp is the GOT address of libc", "readable: v0", "{\"sh\", \"-c\", s0, NULL} is a valid argv", "[[v0]] == NULL || [v0] == NULL || [v0] is a valid envp", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, sp+0x58, [v0])")
OneGadget::Gadget.add(build_id, 467672,
constraints: ["gp is the GOT address of libc", "readable: v0", "[v1] == NULL || v1 is a valid argv", "[[v0]] == NULL || [v0] == NULL || [v0] is a valid envp", "s7+0xa0 == NULL || writable: s7+0xa0", "s1 == NULL || (s32)[s1+0x4] <= 0x0"],
effect: "posix_spawn(s7+0xa0, \"/bin/sh\", s1, 0, v1, [v0])")
OneGadget::Gadget.add(build_id, 1604260,
constraints: ["gp is the GOT address of libc", "a2-0x6a84 == NULL"],
effect: "execl(\"/bin/sh\", \"sh\", a2-0x6a84)")
OneGadget::Gadget.add(build_id, 1604264,
constraints: ["gp is the GOT address of libc", "a2-0x6a84 == NULL"],
effect: "execl(\"/bin/sh\", \"sh\", a2-0x6a84)")
OneGadget::Gadget.add(build_id, 1604268,
constraints: ["gp is the GOT address of libc", "a1-0x6a88 == NULL"],
effect: "execl(\"/bin/sh\", a1-0x6a88)")
+10
View File
@@ -93,6 +93,8 @@ module OneGadget
resolve_pending_branch(cmd)
@cur_addr = cmd[/\A\s*([0-9a-f]+):/, 1]&.to_i(16)
@got_value = cmd[GOT_VALUE, 1]&.to_i(16)
mnem = mnemonic(cmd)
if transfer?(mnem)
@delayed = [mnem, cmd]
@@ -108,6 +110,12 @@ module OneGadget
private
# What a GOT slot holds, stated beside the load by
# +OneGadget::Fetchers::Mips+ because this architecture reaches its globals
# through that table rather than through the instruction's own address.
GOT_VALUE = /#\s*([0-9a-f]+)\s*\z/
private_constant :GOT_VALUE
# The calls, which are delayed exactly as the branches are.
CALLS = %w[jal jalr bal].freeze
private_constant :CALLS
@@ -232,6 +240,8 @@ module OneGadget
# @return [void]
def load_value(dst, mem, size)
check_register!(dst)
# a GOT slot holds an address, and the file says which
return registers[dst] = libc_base + @got_value if @got_value && size == size_t
value = read_value(arg_to_lambda(mem_operand(mem)))
registers[dst] = size == size_t ? value : clobbered_value
+2
View File
@@ -5,6 +5,7 @@ require 'one_gadget/fetchers/aarch64'
require 'one_gadget/fetchers/amd64'
require 'one_gadget/fetchers/arm'
require 'one_gadget/fetchers/i386'
require 'one_gadget/fetchers/mips'
require 'one_gadget/fetchers/riscv64'
require 'one_gadget/gadget'
require 'one_gadget/helper'
@@ -23,6 +24,7 @@ module OneGadget
amd64: OneGadget::Fetchers::Amd64,
arm: OneGadget::Fetchers::Arm,
i386: OneGadget::Fetchers::I386,
mips: OneGadget::Fetchers::Mips,
riscv64: OneGadget::Fetchers::Riscv64
}.freeze
@@ -245,9 +245,14 @@ module OneGadget
# @return [Object, nil] Truthy when +envp+ is acceptable, +nil+ to reject the gadget.
def check_envp(processor, envp_ptr)
# A doubly-dereferenced pointer that names a global variable is believed to
# be environ; one that doesn't drops the gadget.
return global_var?(envp_ptr.to_s) if envp_ptr.is_a?(OneGadget::Emulators::Lambda) &&
envp_ptr.deref_count >= 2
# be environ; one that doesn't drops the gadget. The two dereferences are
# the slot the address is read from and the variable itself, so an
# architecture that resolves the first of them lands one earlier -- naming
# the variable, which it then has to be able to recognise.
if envp_ptr.is_a?(OneGadget::Emulators::Lambda)
return global_var?(envp_ptr.to_s) if envp_ptr.deref_count >= 2
return true if envp_ptr.deref_count == 1 && environ?(envp_ptr.to_s)
end
envp_ptr = resolve_stack_deref(processor, envp_ptr)
# A concrete integer, or a register with nothing useful tracked for it,
+13
View File
@@ -171,6 +171,19 @@ module OneGadget
!branch_kind(line).nil? || call_line?(line)
end
# What a libc calls the variable holding the environment a process started
# with, allowing for the aliases exported beside it (+_environ+, +__environ+).
ENVIRON = /\A_*environ\z/
private_constant :ENVIRON
# Whether +str+ names the address of that variable. An architecture that can
# say which symbol an address belongs to answers this; the rest cannot, and
# recognise the environment by the shape of the pointer instead (see
# {ArgumentResolution#check_envp}).
# @param [String] _str A rendered value.
# @return [Boolean]
def environ?(_str) = false
# Whether +str+ references a libc global, i.e. an address the caller does not
# choose. The default reads the +$base+-relative form an arch produces once it
# concretizes a pc-relative operand; one that reaches its globals through a
+85 -15
View File
@@ -24,8 +24,44 @@ module OneGadget
# branch's target leaves from the delay slot rather than the branch
# ({#branch_pred_map}).
class Mips < Base
# Everything this arch reaches -- its calls and its globals alike -- goes
# through the GOT base in +gp+, which is a precondition the caller arranges
# by setting that register. Say so, and drop the read/write requirements
# rooted there: the GOT is a fixed, mapped libc address, so reaching through
# it asks nothing further of the caller (as i386 does for its own GOT
# register).
# @param [OneGadget::Emulators::Processor] processor
# @return [Hash, nil]
def resolve(processor)
res = super
return if res.nil?
got = got_base_constraint(processor, GOT_BASE) or return nil
res[:constraints].unshift(got)
res[:constraints].reject! { |con| con.match?(/\A(?:writable|readable): \[*#{GOT_BASE}\b/) }
res
end
# This arch reads a global through the GOT and resolves the slot to the
# address it holds, so what reaches a call is one dereference of the variable
# rather than two of the slot naming it. Which variable that is comes from
# the symbols already read for the table.
# @param [String] str A rendered value.
# @return [Boolean]
def environ?(str)
got = mips_got or return false
offset = string_file_offset(str.delete('[]')) or return false
ENVIRON.match?(got[:names][offset].to_s)
end
private
# The register o32 states the GOT base in.
GOT_BASE = 'gp'
private_constant :GOT_BASE
# A call, however it is spelled: through a register (which is how PIC code
# reaches everything) or directly.
def call_str = '(?:jalr|jal|bal)'
@@ -48,7 +84,7 @@ module OneGadget
# Rewrite the disassembly into what the engine reads everywhere else: every
# call named.
def objdump_lines(start: nil, stop: nil, extra: [])
name_got_calls(super)
state_got_values(name_got_calls(super))
end
# A window ends at the call that ends the gadget, but that call's delay slot
@@ -114,6 +150,28 @@ module OneGadget
end
end
# A libc global is reached through the GOT as well: the slot holds the
# address, and an +addiu+ applies the offset within it. What the slot holds
# is in the file, so state it beside the load -- which is what lets the
# value read as +$base+<off>+, the form every other architecture produces
# for a global.
# @param [Array<String>] lines
# @return [Array<String>]
# @example
# 'lw a0,-32496(gp)' #=> 'lw a0,-32496(gp) # b0000'
def state_got_values(lines)
lines.map do |line|
m = line.match(GOT_GLOBAL_LOAD) or next line
address = got_address(m[1].to_i)
address.nil? || address.zero? ? line : "#{line} # #{format('%x', address)}"
end
end
# Loading anything but the call target out of the GOT.
GOT_GLOBAL_LOAD = /:\s*lw\s+(?!t9,)\w+,(-?\d+)\(gp\)/
private_constant :GOT_GLOBAL_LOAD
# Loading the call target out of the GOT, which is +gp+-relative.
GOT_LOAD = /:\s*lw\s+t9,(-?\d+)\(gp\)/
private_constant :GOT_LOAD
@@ -132,20 +190,32 @@ module OneGadget
TARGET_WRITE = /:\s*(?!s[whb]\b)\S+\s+t9,/
private_constant :TARGET_WRITE
# The symbol a +gp+-relative GOT slot names, or +nil+ for one that names
# nothing this can read. This arch states its GOT in the dynamic segment, so
# the answer is there even for a file with no sections at all.
# What a +gp+-relative GOT slot holds, as +[address, name]+, or +nil+ for one
# this cannot read. This arch states its GOT in the dynamic segment, so the
# answer is there even for a file with no sections at all. An entry below
# +DT_MIPS_LOCAL_GOTNO+ holds the address outright; the rest correspond one
# for one with the dynamic symbols.
# @param [Integer] gp_offset The offset as the instruction writes it.
# @return [String, nil]
def got_symbol(gp_offset)
# @return [(Integer, String), nil]
def got_entry(gp_offset)
got = mips_got or return nil
index = (GP_BIAS + gp_offset) / 4
return got[:names][local_got_entry(got, index)] if index < got[:local]
got[:symbols][got[:gotsym] + index - got[:local]]&.name
if index < got[:local]
address = local_got_entry(got, index)
return address && [address, got[:names][address]]
end
got[:symbols][got[:gotsym] + index - got[:local]]
end
# @param [Integer] gp_offset
# @return [String, nil] The symbol the slot names.
def got_symbol(gp_offset) = got_entry(gp_offset)&.last
# @param [Integer] gp_offset
# @return [Integer, nil] The address the slot holds.
def got_address(gp_offset) = got_entry(gp_offset)&.first
# +gp+ points this far into the GOT, so that one signed 16-bit offset reaches
# the most of it. Every +gp+-relative offset is read against it.
GP_BIAS = 0x7ff0
@@ -178,12 +248,12 @@ module OneGadget
gotsym = dynamic.tag_by_type(:mips_gotsym)&.value or return nil
segment = elf.segments_by_type(:load).find { |seg| seg.vma_in?(address) } or return nil
names = {}
dynamic.symbols.each do |symbol|
value = symbol.header.st_value.to_i
names[value] = symbol.name unless value.zero? || symbol.name.empty?
end
{ local:, gotsym:, symbols: dynamic.symbols, names:, big: elf.endian == :big,
# Read out of the symbols now, rather than holding them: they are lazy, and
# the file they would read from is closed as soon as this returns.
symbols = dynamic.symbols.map { |symbol| [symbol.header.st_value.to_i, symbol.name] }
names = symbols.to_h { |value, name| [value, name] }
.reject { |value, name| value.zero? || name.empty? }
{ local:, gotsym:, symbols:, names:, big: elf.endian == :big,
offset: segment.vma_to_offset(address) }
end
end
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'one_gadget/one_gadget'
describe 'one_gadget_mips' do
describe 'from file' do
before(:each) do
skip_unless_objdump
end
# OpenWrt's musl, which is what a real router runs. Both byte orders are in
# wide use on this architecture -- ath79 is big-endian, ramips little -- so
# both are read here.
it 'musl 1.2.4, big-endian' do
path = data_path('mips-musl-1.2.4.so')
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x56fd4, 0x77b44, 0x77b48]
expect(OneGadget.gadgets(file: path, force_file: true, level: 1)).to eq [0x56fd4, 0x77b44, 0x77b48]
end
it 'musl 1.2.4, little-endian' do
path = data_path('mipsel-musl-1.2.4.so')
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x56f98, 0x77b00, 0x77b04]
expect(OneGadget.gadgets(file: path, force_file: true, level: 1)).to eq [0x56f98, 0x77b00, 0x77b04]
end
it 'libc-2.36' do
path = data_path('mipsel-libc-2.36.so')
expect(OneGadget.gadgets(file: path, force_file: true)).to eq [0x4b42c, 0x4b440]
expect(OneGadget.gadgets(file: path, force_file: true, level: 1))
.to eq [0x4b3d8, 0x4b424, 0x4b42c, 0x4b440, 0x722d0, 0x722d8, 0x187aa8, 0x187aac]
end
# Everything this arch reaches goes through the GOT base register, including
# the "/bin/sh" it passes: the table holds the address and the instruction
# after the load applies the offset within it. So the caller has to have set
# that register, exactly as i386 must set its own.
it 'requires the GOT base register the whole architecture reads through' do
gadget = OneGadget.gadgets(file: data_path('mipsel-libc-2.36.so'), force_file: true, details: true, level: 1)
.find { |g| g.offset == 0x4b440 }
expect(gadget.effect).to eq 'posix_spawn(a0, "/bin/sh", a2, a3, v1, environ)'
expect(gadget.constraints).to include 'gp is the GOT address of libc'
end
# Resolving the GOT slot means the environment arrives as one dereference of
# the variable rather than two of the slot naming it, so it is recognised by
# which variable it is -- otherwise the gadget would carry a constraint asking
# the caller to arrange what is already environ.
it 'names the environment it passes' do
gadget = OneGadget.gadgets(file: data_path('mips-musl-1.2.4.so'), force_file: true, details: true, level: 1)
.find { |g| g.offset == 0x77b44 }
expect(gadget.effect).to end_with 'environ)'
expect(gadget.constraints.join).not_to include 'valid envp'
end
it 'reports the same gadgets for a libc with no section headers' do
expect_same_gadgets_when_stripped('mipsel-libc-2.36.so')
end
end
end
+14 -2
View File
@@ -21,6 +21,15 @@ UBUNTU_MIRRORS = {
# A glibc security fix is published to one pocket or the other, so ask both and
# keep whichever answer is newer.
UBUNTU_POCKETS = %w[-updates -security].freeze
# Fixtures that state no BuildID, and so can never become a shipped entry: they
# are built without the note segment that carries one, as OpenWrt builds are.
# They are here to be searched, not to be shipped, so a refresh passes over them
# rather than reporting that it could not read them. Named rather than detected,
# so a fixture that unexpectedly lost its BuildID still says so.
NO_BUILD_ID = %w[
mips-musl-1.2.4.so
mipsel-musl-1.2.4.so
].freeze
namespace :builds do
desc 'Regenerates the shipped build database from Ubuntu LTS libcs and the spec fixtures'
@@ -71,8 +80,11 @@ end
def refresh_fixtures
Dir.glob(File.join(repo_root, 'spec', 'data', '*.so')).filter_map do |libc|
print "[spec] #{File.basename(libc)} .. "
regenerate(libc, File.join('spec', 'data', File.basename(libc)))
name = File.basename(libc)
next puts("[spec] #{name} .. states no BuildID, not shipped") if NO_BUILD_ID.include?(name)
print "[spec] #{name} .. "
regenerate(libc, File.join('spec', 'data', name))
end
end