引言:攻击者要 ROP,得先知道内核在哪
内核漏洞利用(如 ROP 链)第一步往往是拿到内核里某个函数的地址 ------如果内核每次启动都加载在同一个固定地址 ,攻击者就能硬编码这些地址,漏洞利用变成"开卷考试"。KASLR(Kernel Address Space Layout Randomization,内核地址空间布局随机化) 就是让内核的物理位置 和虚拟布局每次启动都随机,让硬编码的地址失效。
KASLR 其实是两段独立的随机化:
- 物理随机化 :内核镜像被解压到哪个物理地址 ,启动时随机选(
choose_random_location,compressed 阶段)。 - 虚拟随机化 :内核的直接映射区 / vmalloc 区 / vmemmap 区 这三个大虚拟区域的基址 随机(
kernel_randomize_memory)。
这条链路:boot stub 选随机物理地址 → 解压 → __startup_64 算 load_delta 重定位页表 → setup_arch 里随机化虚拟布局。
全景图
#mermaid-svg-fb7hH1xmWKptjbKt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fb7hH1xmWKptjbKt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fb7hH1xmWKptjbKt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fb7hH1xmWKptjbKt .error-icon{fill:#552222;}#mermaid-svg-fb7hH1xmWKptjbKt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fb7hH1xmWKptjbKt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fb7hH1xmWKptjbKt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fb7hH1xmWKptjbKt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fb7hH1xmWKptjbKt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fb7hH1xmWKptjbKt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fb7hH1xmWKptjbKt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fb7hH1xmWKptjbKt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fb7hH1xmWKptjbKt .marker.cross{stroke:#333333;}#mermaid-svg-fb7hH1xmWKptjbKt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fb7hH1xmWKptjbKt p{margin:0;}#mermaid-svg-fb7hH1xmWKptjbKt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fb7hH1xmWKptjbKt .cluster-label text{fill:#333;}#mermaid-svg-fb7hH1xmWKptjbKt .cluster-label span{color:#333;}#mermaid-svg-fb7hH1xmWKptjbKt .cluster-label span p{background-color:transparent;}#mermaid-svg-fb7hH1xmWKptjbKt .label text,#mermaid-svg-fb7hH1xmWKptjbKt span{fill:#333;color:#333;}#mermaid-svg-fb7hH1xmWKptjbKt .node rect,#mermaid-svg-fb7hH1xmWKptjbKt .node circle,#mermaid-svg-fb7hH1xmWKptjbKt .node ellipse,#mermaid-svg-fb7hH1xmWKptjbKt .node polygon,#mermaid-svg-fb7hH1xmWKptjbKt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fb7hH1xmWKptjbKt .rough-node .label text,#mermaid-svg-fb7hH1xmWKptjbKt .node .label text,#mermaid-svg-fb7hH1xmWKptjbKt .image-shape .label,#mermaid-svg-fb7hH1xmWKptjbKt .icon-shape .label{text-anchor:middle;}#mermaid-svg-fb7hH1xmWKptjbKt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fb7hH1xmWKptjbKt .rough-node .label,#mermaid-svg-fb7hH1xmWKptjbKt .node .label,#mermaid-svg-fb7hH1xmWKptjbKt .image-shape .label,#mermaid-svg-fb7hH1xmWKptjbKt .icon-shape .label{text-align:center;}#mermaid-svg-fb7hH1xmWKptjbKt .node.clickable{cursor:pointer;}#mermaid-svg-fb7hH1xmWKptjbKt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fb7hH1xmWKptjbKt .arrowheadPath{fill:#333333;}#mermaid-svg-fb7hH1xmWKptjbKt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fb7hH1xmWKptjbKt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fb7hH1xmWKptjbKt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fb7hH1xmWKptjbKt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fb7hH1xmWKptjbKt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fb7hH1xmWKptjbKt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fb7hH1xmWKptjbKt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fb7hH1xmWKptjbKt .cluster text{fill:#333;}#mermaid-svg-fb7hH1xmWKptjbKt .cluster span{color:#333;}#mermaid-svg-fb7hH1xmWKptjbKt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fb7hH1xmWKptjbKt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fb7hH1xmWKptjbKt rect.text{fill:none;stroke-width:0;}#mermaid-svg-fb7hH1xmWKptjbKt .icon-shape,#mermaid-svg-fb7hH1xmWKptjbKt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fb7hH1xmWKptjbKt .icon-shape p,#mermaid-svg-fb7hH1xmWKptjbKt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fb7hH1xmWKptjbKt .icon-shape .label rect,#mermaid-svg-fb7hH1xmWKptjbKt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fb7hH1xmWKptjbKt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fb7hH1xmWKptjbKt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fb7hH1xmWKptjbKt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} compressed 阶段
extract_kernel misc.c:377
choose_random_location
kaslr.c:842 选随机物理地址
find_random_phys_addr :792
扫 e820 找不冲突的随机地址
解压内核到随机物理地址
__decompress misc.c:350
__startup_64 head64.c:179
算 load_delta 修页表
load_delta = physaddr - (text - __START_KERNEL_map)
head64.c:203
把 load_delta 加进各页表项
让内核在随机物理地址正确运行
setup_arch setup.c:1082
kernel_randomize_memory kaslr.c:64
随机化 page_offset_base
vmalloc_base / vmemmap_base
每次启动地址布局都不同
硬编码地址失效
一句话:boot stub choose_random_location 选随机物理地址 → 解压 → __startup_64 算 load_delta 修页表让内核在随机物理地址运行 → setup_arch 里 kernel_randomize_memory 随机化直接映射/vmalloc/vmemmap 虚拟基址 → 每次启动布局都不同。
一、KASLR 是什么:两段独立随机化
先看清 x86-64 的内核虚拟地址布局(L4 分页,几个默认基址):
| 区域 | 默认基址(不随机时) | 存什么 | KASLR 随机? |
|---|---|---|---|
| 内核 text | __START_KERNEL_map = 0xffffffff80000000(page_64_types.h:50) |
内核代码/数据 | ❌ 虚拟固定(物理随机) |
| 直接映射 | __PAGE_OFFSET_BASE_L4 = 0xffff888000000000(:42) |
所有物理内存的线性映射 | ✅ page_offset_base |
| vmalloc | __VMALLOC_BASE_L4 = 0xffffc90000000000(pgtable_64_types.h:124) |
vmalloc/vmap/ioremap(#17) | ✅ vmalloc_base |
| vmemmap | __VMEMMAP_BASE_L4 = 0xffffea0000000000(:130) |
struct page 数组(#1) |
✅ vmemmap_base |
两段随机化各管一边:
- 物理 KASLR (
CONFIG_RANDOMIZE_BASE):随机化内核镜像的物理加载地址 。但内核 text 虚拟地址固定 在__START_KERNEL_map------所以"虚拟固定、物理随机"。 - 虚拟 KASLR (
CONFIG_RANDOMIZE_MEMORY):随机化直接映射 / vmalloc / vmemmap 的虚拟基址。它们的物理地址由硬件决定,所以"虚拟随机、物理固定"。
这两段是互补的 :内核 text 虚拟地址固定(__START_KERNEL_map),但它的物理页是随机的;直接映射区物理地址固定(硬件决定的),但虚拟基址随机。攻击者无论从哪头突破,另一头仍是随机的。
二、物理随机化:choose_random_location
物理 KASLR 发生在内核还没解压、还在 compressed boot stub 阶段 。extract_kernel(misc.c:377)先调 choose_random_location(kaslr.c:842)选地址,再 __decompress(:350)解压:
c
// choose_random_location(arch/x86/boot/compressed/kaslr.c:842)------ 关键片段
void choose_random_location(unsigned long input, ...,
unsigned long *output, unsigned long output_size,
unsigned long *virt_addr)
{
if (cmdline_find_option_bool("nokaslr")) { // ① nokaslr 禁用
warn("KASLR disabled: 'nokaslr' on cmdline.");
return;
}
...
mem_avoid_init(input, input_size, *output); // ② 记录"不能放"的区间(:863)
min_addr = min(*output, 512UL << 20); // ③ 随机下限(:870)
min_addr = ALIGN(min_addr, CONFIG_PHYSICAL_ALIGN); // 2MB 对齐
random_addr = find_random_phys_addr(min_addr, output_size); // ④ 扫 e820 找随机物理地址(:875)
if (!random_addr)
warn("Physical KASLR disabled: no suitable memory region!");
else if (*output != random_addr)
*output = random_addr; // 更新物理加载地址
if (IS_ENABLED(CONFIG_X86_64))
random_addr = find_random_virt_addr(LOAD_PHYSICAL_ADDR, output_size); // ⑤ 随机虚拟地址(:887)
*virt_addr = random_addr;
}
find_random_phys_addr(:792)→ process_e820_entries(:772)遍历 e820 内存表,用 mem_avoid_overlap(:432)排除掉不能覆盖的区间 ------内核自己、boot_params、cmdline、initrd、固件保留区等(mem_avoid_init :383 记录的)。剩下的可用区间里,slots_fetch_random(:556)随机挑一个 2MB 对齐的槽位(CONFIG_PHYSICAL_ALIGN)放内核镜像。
Why 物理 KASLR 在 compressed 阶段就定 :内核镜像必须在解压前就确定落到哪个物理地址(解压是往物理地址写数据)。而这时真正的内核还没跑起来,只有 boot stub 能用------所以选地址的逻辑放在 compressed 里,选完直接解压过去。这也是为什么
nokaslr要在这里检查(:850)。
三、虚拟随机化:kernel_randomize_memory
物理 KASLR 只随机了内核镜像本身的位置。真正启动后(setup_arch,setup.c:1082),kernel_randomize_memory(mm/kaslr.c:64)随机化三个大虚拟区域的基址:
c
// kaslr_regions[](arch/x86/mm/kaslr.c:51)------ 要随机化的三个虚拟区域
static __initdata struct kaslr_memory_region {
unsigned long *base;
unsigned long size_tb;
} kaslr_regions[] = {
{ &page_offset_base, 0 }, // 直接映射区
{ &vmalloc_base, 0 }, // vmalloc 区
{ &vmemmap_base, 0 }, // vmemmap 区
};
// kernel_randomize_memory(arch/x86/mm/kaslr.c:64)------ 关键片段
void __init kernel_randomize_memory(void)
{
vaddr_start = pgtable_l5_enabled() ? __PAGE_OFFSET_BASE_L5 : __PAGE_OFFSET_BASE_L4; // :73
...
kaslr_regions[0].size_tb = 1 << (MAX_PHYSMEM_BITS - TB_SHIFT); // 直接映射区大小(:88)
kaslr_regions[1].size_tb = VMALLOC_SIZE_TB; // vmalloc 区 32TB(:89)
... // vmemmap 大小 = struct page 数组大小(:107-109)
/* 三个区之间剩下的空间就是可用的"随机熵" */
remain_entropy = vaddr_end - vaddr_start; // :112
for (i = 0; i < ARRAY_SIZE(kaslr_regions); i++)
remain_entropy -= get_padding(&kaslr_regions[i]);
for (i = 0; i < ARRAY_SIZE(kaslr_regions); i++) { // 逐个随机化
entropy = remain_entropy / (ARRAY_SIZE(kaslr_regions) - i);
prandom_bytes_state(&rand_state, &rand, sizeof(rand));
entropy = (rand % (entropy + 1)) & PUD_MASK; // 随机 + PUD 对齐(:127)
vaddr += entropy;
*kaslr_regions[i].base = vaddr; // 写入随机基址(:129)
vaddr += get_padding(&kaslr_regions[i]);
vaddr = round_up(vaddr + 1, PUD_SIZE); // 跳过本区 + 对齐
remain_entropy -= entropy;
}
}
核心:三个虚拟区域(直接映射/vmalloc/vmemmap)保持原有顺序,但在它们之间和前后随机插入空隙 ,让每个区的基址都落到一个 1GB(PUD_SIZE)对齐的随机位置。结果就是 page_offset_base/vmalloc_base/vmemmap_base 每次启动不同------直接映射区的基址不再固定是 0xffff888000000000。
Why 直接映射区要随机 :直接映射(page_offset)是内核访问所有物理内存 的线性窗口,
virt = page_offset_base + phys。如果page_offset_base固定,攻击者只要能泄漏任意一个内核数据地址,就能反推物理页、进而算到任意对象地址。随机化page_offset_base就断掉了"物理地址 → 虚拟地址"这条固定换算。这也是为什么kaslr_regions的注释(kaslr.c:45-46)强调"列表按虚拟地址排序,随机化后顺序保持"------顺序不能乱,只是整体错位。
四、__startup_64:load_delta 重定位
物理 KASLR 选完随机地址后,问题来了:内核编译时假设自己加载在 LOAD_PHYSICAL_ADDR(默认 16MB)物理地址、映射在 __START_KERNEL_map 虚拟地址,但现在被放到随机物理地址了,怎么还能正确跑?
答案在 __startup_64(head64.c:179)------内核最早运行的头代码,它算出偏移量 load_delta,把早期页表里的物理地址全部修正:
c
// __startup_64(arch/x86/kernel/head64.c:179)------ 关键片段
unsigned long __head __startup_64(unsigned long physaddr, struct boot_params *bp)
{
...
/* 我编译时该在的物理地址 vs 我实际跑的物理地址,差多少 */
load_delta = physaddr - (unsigned long)(_text - __START_KERNEL_map); // :203
if (load_delta & ~PMD_MASK) // 必须 2MB 对齐(:206)
for (;;);
load_delta += sme_get_me_mask(); // 加上 SME 加密掩码
/* 把 load_delta 加进各级页表项,修正物理地址 */
pgd = fixup_pointer(&early_top_pgt, physaddr);
...
*p += _PAGE_TABLE_NOENC - __START_KERNEL_map + load_delta; // :220
...
pud[510] += load_delta; // :228
pud[511] += load_delta; // :229
...
pmd[i] += load_delta; // :233
...
}
关键在 load_delta(:203):链接脚本(vmlinux.lds.S:125)用 AT(ADDR(.text) - LOAD_OFFSET) 定义 .text 段的物理地址,其中 LOAD_OFFSET = __START_KERNEL_map(:21)------所以 _text - __START_KERNEL_map 就是内核 text 编译时 的物理地址(LOAD_PHYSICAL_ADDR 附近)。而实际运行物理地址是 physaddr(boot stub 随机选的)。两者之差就是 load_delta。然后 __startup_64 把这个 load_delta 加进早期页表的每个 PGD/PUD/PMD 项 ------这样内核虽然被搬到随机物理地址,页表翻译后虚拟地址仍是 __START_KERNEL_map,代码照常执行。
Why 用
load_delta而不是重编译 :内核是位置无关的 ------所有内核符号都是"相对__START_KERNEL_map的偏移",运行时真实虚拟地址 =__START_KERNEL_map固定不变。物理 KASLR 只改变"这个固定虚拟地址背后映射到哪个物理页",所以只需要修页表里的物理地址 (加load_delta),不用动任何代码/数据的虚拟地址。这就是 KASLR 几乎零成本的原因------它搬的是页表项,不是内核代码。
五、用户态 ASLR(对比)
内核 KASLR 的"用户态兄弟"是用户进程的 ASLR,随机化的是用户态 mmap 的基址(#16):
c
// arch_pick_mmap_layout(arch/x86/mm/mmap.c:129)------ 用户态 ASLR
void arch_pick_mmap_layout(struct mm_struct *mm, struct rlimit *rlim_stack)
{
if (mmap_is_legacy()) // 老式布局(栈上往下)
...
else
*base = mmap_base(random_factor, task_size, rlim_stack); // top-down + 随机
arch_pick_mmap_base(&mm->mmap_base, &mm->mmap_legacy_base, ...); // :136
}
arch_mmap_rnd(mm/mmap.c:77)生成随机偏移,mmap_base(:82)算出随机化的 mmap 起点。用户态 ASLR 随机的是用户进程的 mmap 基址 (加载共享库、mmap 匿名区的起点),内核 KASLR 随机的是内核自己的物理位置和虚拟布局------两层独立,共同防御"硬编码地址"。
六、与前面各篇的关系 + 记住三点
6.1 与前面各篇的关系
| 篇 | 关系 |
|---|---|
| #17 vmalloc | vmalloc_base 就是虚拟 KASLR 随机化的三个基址之一(kaslr_regions[1]) |
| #1 物理内存初始化 | vmemmap_base(struct page 数组的基址,kaslr_regions[2])也随机 |
| PTE 篇 / 页表 | __startup_64 修早期页表(load_delta)是页表初始化的第一步 |
| CPU #1 compressed 自解压 | 物理 KASLR 发生在 compressed 阶段(extract_kernel → choose_random_location) |
| #16 mmap | 用户态 ASLR(mmap_base)是内核 KASLR 的用户态对照 |
6.2 与 AI Infra 的关系
- 安全基线 :训练/推理集群的宿主机内核被攻破,往往先要绕过 KASLR(泄漏地址 + 算偏移)。理解 KASLR 是理解内核漏洞利用(
ret2usr、ret2dir、KASLR bypass)的前提。 - 调试反直觉 :
/proc/kallsyms在kptr_restrict=1时隐藏内核符号地址(因为 KASLR 让地址每次变),排障时看 oops 地址要对上__START_KERNEL_map偏移。 nokaslr:性能剖析(perf)和内核调试常用nokaslr关掉随机化,让地址稳定、符号可解析。
6.3 记住三点
-
KASLR = 物理随机 + 虚拟随机两段 :物理段
choose_random_location(kaslr.c:842)在 compressed 阶段选随机物理地址放内核镜像(find_random_phys_addr:792 扫 e820 +mem_avoid_overlap:432 避冲突);虚拟段kernel_randomize_memory(mm/kaslr.c:64)随机化page_offset_base/vmalloc_base/vmemmap_base(kaslr_regions[]:51)。内核 text 虚拟地址固定__START_KERNEL_map,物理随机;直接映射区物理固定、虚拟随机------两段互补。 -
load_delta 重定位零成本搬页表 :
__startup_64(head64.c:179)算load_delta = physaddr - (text - __START_KERNEL_map)(:203),把差值加进早期页表每个 PGD/PUD/PMD 项(:220-233)------内核代码/数据虚拟地址不变,只是背后的物理页变了。KASLR 搬的是页表项不是代码。 -
三个虚拟区顺序不变、整体错位 :
kernel_randomize_memory保持直接映射/vmalloc/vmemmap 的顺序,在区间之间随机插 1GB(PUD_SIZE)对齐的空隙,让每个区基址随机。用户态 ASLR(arch_pick_mmap_layoutmmap.c:129)随机 mmap 基址,是内核 KASLR 的用户态对照。
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
extract_kernel(compressed 入口) |
arch/x86/boot/compressed/misc.c:377 |
choose_random_location(物理 KASLR 选地址) |
arch/x86/boot/compressed/kaslr.c:842 |
find_random_phys_addr(扫 e820 找随机物理地址) |
arch/x86/boot/compressed/kaslr.c:792 |
find_random_virt_addr(随机虚拟地址) |
arch/x86/boot/compressed/kaslr.c:821 |
process_e820_entries / mem_avoid_overlap(避冲突) |
arch/x86/boot/compressed/kaslr.c:772 / :432 |
mem_avoid_init(记录不可覆盖区间) |
arch/x86/boot/compressed/kaslr.c:383 |
kernel_randomize_memory(虚拟 KASLR) |
arch/x86/mm/kaslr.c:64 |
kaslr_regions[](三个随机化区域) |
arch/x86/mm/kaslr.c:51 |
__startup_64(load_delta 重定位) |
arch/x86/kernel/head64.c:179 |
kernel_randomize_memory 调用点 |
arch/x86/kernel/setup.c:1082 |
__START_KERNEL_map(内核 text 固定虚拟基址) |
arch/x86/include/asm/page_64_types.h:50 |
LOAD_OFFSET(= __START_KERNEL_map,链接脚本虚拟基址) |
arch/x86/kernel/vmlinux.lds.S:21 |
.text 物理地址定义(AT(ADDR(.text) - LOAD_OFFSET)) |
arch/x86/kernel/vmlinux.lds.S:125 |
__PAGE_OFFSET_BASE_L4(直接映射默认基址) |
arch/x86/include/asm/page_64_types.h:42 |
__VMALLOC_BASE_L4 / __VMEMMAP_BASE_L4 |
arch/x86/include/asm/pgtable_64_types.h:124 / :130 |
arch_pick_mmap_layout / arch_mmap_rnd(用户态 ASLR) |
arch/x86/mm/mmap.c:129 / :77 |