diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb6fd9d..4a9de14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index 81c51a6..4123737 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/README.tpl.md b/README.tpl.md
index 1d4298a..d553371 100644
--- a/README.tpl.md
+++ b/README.tpl.md
@@ -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.
diff --git a/builds_list b/builds_list
index 6090eb4..2f6a027 100644
--- a/builds_list
+++ b/builds_list
@@ -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
diff --git a/docs/adding-an-architecture.md b/docs/adding-an-architecture.md
index 5ebbba1..3998a31 100644
--- a/docs/adding-an-architecture.md
+++ b/docs/adding-an-architecture.md
@@ -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) |
diff --git a/lib/one_gadget/builds/libc-2.36-019d4db0c5d400a83e3b5f3bebee6ac38dee3c48.rb b/lib/one_gadget/builds/libc-2.36-019d4db0c5d400a83e3b5f3bebee6ac38dee3c48.rb
new file mode 100644
index 0000000..a262996
--- /dev/null
+++ b/lib/one_gadget/builds/libc-2.36-019d4db0c5d400a83e3b5f3bebee6ac38dee3c48.rb
@@ -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:
+# .
+
+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)")
+
diff --git a/lib/one_gadget/emulators/mips.rb b/lib/one_gadget/emulators/mips.rb
index 173a0f2..d5fddb9 100644
--- a/lib/one_gadget/emulators/mips.rb
+++ b/lib/one_gadget/emulators/mips.rb
@@ -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
diff --git a/lib/one_gadget/fetchers.rb b/lib/one_gadget/fetchers.rb
index 409d6fc..6370e36 100644
--- a/lib/one_gadget/fetchers.rb
+++ b/lib/one_gadget/fetchers.rb
@@ -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
diff --git a/lib/one_gadget/fetchers/argument_resolution.rb b/lib/one_gadget/fetchers/argument_resolution.rb
index 0aa4bfc..a0a1abd 100644
--- a/lib/one_gadget/fetchers/argument_resolution.rb
+++ b/lib/one_gadget/fetchers/argument_resolution.rb
@@ -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,
diff --git a/lib/one_gadget/fetchers/base.rb b/lib/one_gadget/fetchers/base.rb
index 8a5dbce..b272544 100644
--- a/lib/one_gadget/fetchers/base.rb
+++ b/lib/one_gadget/fetchers/base.rb
@@ -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
diff --git a/lib/one_gadget/fetchers/mips.rb b/lib/one_gadget/fetchers/mips.rb
index 857564b..a9bfc98 100644
--- a/lib/one_gadget/fetchers/mips.rb
+++ b/lib/one_gadget/fetchers/mips.rb
@@ -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++, the form every other architecture produces
+ # for a global.
+ # @param [Array] lines
+ # @return [Array]
+ # @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
diff --git a/spec/data/mipsel-libc-2.36.so b/spec/data/mipsel-libc-2.36.so
new file mode 100755
index 0000000..a494c28
Binary files /dev/null and b/spec/data/mipsel-libc-2.36.so differ
diff --git a/spec/data/mipsel-musl-1.2.4.so b/spec/data/mipsel-musl-1.2.4.so
new file mode 100755
index 0000000..b2dfd1a
Binary files /dev/null and b/spec/data/mipsel-musl-1.2.4.so differ
diff --git a/spec/one_gadget_mips_spec.rb b/spec/one_gadget_mips_spec.rb
new file mode 100644
index 0000000..c735f42
--- /dev/null
+++ b/spec/one_gadget_mips_spec.rb
@@ -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
diff --git a/tasks/builds/refresh.rake b/tasks/builds/refresh.rake
index 52cfeb7..8ae0733 100644
--- a/tasks/builds/refresh.rake
+++ b/tasks/builds/refresh.rake
@@ -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