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