本文主要是介绍Linux内核地址空间随机化ASLR的几种实现方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
ASLR(Address Space Layout Randomization)在2005年被引入到Linux内核kernel 2.6.12中。地址空间随机化在内核中有多种实现和表现方式,下面分别介绍。
堆栈随机化
堆栈随机化是一项安全增强,它允许对系统调用发生时,内核使用的堆栈添加一个随机偏移。这给基于stack的攻击增加了难度,因为stack攻击通常要求stack有个固定的layout。现在每次系统调用,stack的layout都变化的话,攻击就无规律可寻了。
首先以系统调用为主线,看一下Linux内核ARM64架构堆栈随机化的实现方式:
在ARM64上,主要在系统调用的链条中增加了invoke_syscall()这个函数来实现堆栈随机化:
linux-5.4.175未增加堆栈随机化的实现:
linux-5.16.4增加堆栈随机化的实现:
我们模仿上面的逻辑,写一个demo来分析其原理:
#include <stdio.h>
#include <stdlib.h>#define DBG(fmt, ...) do { printf("%s line %d, "fmt"\n", __func__, __LINE__, ##__VA_ARGS__); } while (0)int __attribute__((noinline)) bar(void)
{unsigned int abc = 0;DBG("stack var addr %p.", &abc);return 0;
}int __attribute__((noinline)) foo(void)
{int offset = rand()%256; void *ptr = __builtin_alloca(offset);bar();return 0;
}int main(void)
{int i = 0;for(i = 0; i < 10; i ++){DBG("loop %d.", i);foo();}return 0;
}
去掉随机化之后,和上面对比,你会发现,堆栈地址在每次调用是都保持了不变。
具体的说,在内核实现中,这个机制允许在每个系统调用时,随机选择内核堆栈的偏移量,比如在启动时,通过输入randomize_kstack_offset=on/off控制是否启用这个机制。
得益于在每次系统调用时都分配了随机的内核堆栈偏移量,该功能可在启用后让基于堆栈的攻击变得更加困难。
堆栈随机化的另一种实现
nvidia GPU驱动中实现了另一种堆栈随机化价值。NVIDIA GPU驱动分为开源部分和闭源部分,为了安全,从开源代码调用闭源代码时,会调用NV_ENTER_RM_RUNTIME重新设置新的堆栈空间,退出时调用NV_EXIT_RM_RUNTIME恢复开源上下文的堆栈。
调用:
这个功能是需要一个支持__use_altstack__的customlize工具链才可以使用。
经过实际测试,altstack功能貌似没有起作用。
#include <stdio.h>unsigned long stack[0x1000];
#define NV_ENTER_RM_RUNTIME(sp,fp) \
{ \__asm__ __volatile__ ("movq %%rbp,%0" : "=r" (fp)); /* save %rbp */ \__asm__ __volatile__ ("movq %0,%%rbp" :: "r" (sp)); \
}#define NV_EXIT_RM_RUNTIME(sp,fp) \
{ \register void *__rbp __asm__ ("rbp"); \if (__rbp != (void*)(sp)) \{ \printf("%s line %d, fata error.\n", __func__, __LINE__); \} \__asm__ __volatile__ ("movq %0,%%rbp" :: "r" (fp)); /* restore %rbp */ \
}unsigned long spp;
void __attribute__((altstack(0))) call_printf(unsigned long newsp)
{void* fp;NV_ENTER_RM_RUNTIME(newsp,fp);printf("%s line %d.\n", __func__, __LINE__);NV_EXIT_RM_RUNTIME(newsp,fp);return;
}int main(void)
{spp = (unsigned long)stack + 0x1000*8;call_printf(spp); return 0;
}
根据GITHUB上NVIDIA 的说法:
Does the altstack attribute require compiler support to be used? · NVIDIA/open-gpu-kernel-modules · Discussion #299 · GitHub
具体可以查看src/nvidia/arch/nvalloc/unix/include/osapi.h中NV_EXIT_RM_RUNTIME的定义,在工具链没有定义__use_altstack__时定义方式是空的。
加入altstack的作用
NVIDIA考虑到了安全,调用到闭源库后,会切栈,这样在闭源库内通过工具dump_stack时,会由于开源和闭源的堆栈不连续,导致无法导出完整的调用堆栈,一般来讲,开源的部分能看出业务逻辑来的,而闭源部门是无法根据函数名称得到业务逻辑信息的,所以,这样便会使攻击方无法将闭源部分的调用堆栈和业务逻辑,场景联系在一起,这样就失去了一个很重要的信息源。
用户态应用ASLR随机化
1.查看当前操作系统的ASLR配置情况,两种命令任君选择:
root@zlcao-RedmiBook-14:~# cat /proc/sys/kernel/random
random/ randomize_va_space
root@zlcao-RedmiBook-14:~# cat /proc/sys/kernel/randomize_va_space
0
root@zlcao-RedmiBook-14:~# sysctl -a --pattern randomize
kernel.randomize_va_space = 0
root@zlcao-RedmiBook-14:~#
2.配置选项:
0-:关闭
1-:半随机化,共享库,栈,mmap以及vdso将被随机化。
2-: 全随机化,除了1中所有,还有HEAP。
地址随机化的表现:
ALSR关闭时,每次动态库的加载地址相同:
关闭ALSR,有多种方法可以关闭ALSR:
方法1: echo 0 > /proc/sys/kernel/randomize_va_space
方法2:sysctl -w kernel.randomize_va_space=0
root@zlcao-RedmiBook-14:~# sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
root@zlcao-RedmiBook-14:~# cat /proc/sys/kernel/randomize_va_space
0
root@zlcao-RedmiBook-14:~#
方法3:启动命令行加入norandmaps,内核启动阶段自动将randomize_va_space设置为0。
randomize_va_space变量是如何发挥作用的?
关键代码在下面这幅图中,当启动ALSR后,加载器在加载可执行程序的时候,会调用arch_randomize_brk函数得到一个随机的地址空间范围。
trace的调用堆栈如下
root@zlcao-Vostro-3268:~# cd /sys/kernel/debug/tracing/
root@zlcao-Vostro-3268:/sys/kernel/debug/tracing# echo arch_randomize_brk > set_ftrace_filter
root@zlcao-Vostro-3268:/sys/kernel/debug/tracing# echo function > current_tracer
root@zlcao-Vostro-3268:/sys/kernel/debug/tracing# echo 1 > options/func_stack_trace
root@zlcao-Vostro-3268:/sys/kernel/debug/tracing# echo 1 > tracing_on
root@zlcao-Vostro-3268:/sys/kernel/debug/tracing# cat trac
cat: trac: 没有那个文件或目录
root@zlcao-Vostro-3268:/sys/kernel/debug/tracing# cat trace
# tracer: function
#
# entries-in-buffer/entries-written: 4/4 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |cat-5261 [001] .... 57035.744709: arch_randomize_brk <-load_elf_binarycat-5261 [001] .... 57035.744719: <stack trace>=> arch_randomize_brk=> load_elf_binary=> search_binary_handler=> __do_execve_file.isra.39=> __x64_sys_execve=> do_syscall_64=> entry_SYSCALL_64_after_hwframecat-5264 [002] .... 57036.998736: arch_randomize_brk <-load_elf_binarycat-5264 [002] .... 57036.998746: <stack trace>=> arch_randomize_brk=> load_elf_binary=> search_binary_handler=> __do_execve_file.isra.39=> __x64_sys_execve=> do_syscall_64=> entry_SYSCALL_64_after_hwframe
Kernel 内核空间映射地址的随机化
内核线性地址空间(direct mapping)。这块区域把所有物理内存线性映射到PAGE_OFFSET
虚拟地址的值可能是固定的0xffff888000000000,
或者KASLR使能后的随机地址page_offset_base
。
KASLR是Kernel ASLR的缩写,开启内核地址随机化需要打开两个配置项,如果配置了这两个选项,page_offset_base、vmalloc_base、vmemmap_base的基址会在函数
kernel_randomize_memory()中被随机化赋予一个和是范围的值。
CONFIG_DYNAMIC_MEMORY_LAYOUT=y
CONFIG_RANDOMIZE_MEMORY=y
开启以后会随机化线性映射区域基址(page_offset_base)、vmalloc区域基址(vmalloc_base)、vmemmap区域基址(vmemmap_base)。参考文件arch/x86/mm/kaslr.c的实现:
start_kernel() → setup_arch() → kernel_randomize_memory():
对三个地址赋值的代码在下图中的139行,vaddr的产生算法中有随机变量发挥作用。
Kernel物理加载地址的随机化
物理加载地址的随机化是上面介绍的所有随机化机制里面最根本的随即化实现,前面介绍的种种,基本上都是围绕虚拟地址的随机化,而内核物理加载地址的随机化则是将整个Linux内核的物理加载地址进行了随机化处理,每次启动,内核加载到物理内存的具体位置都是不同的,让攻击者无法定位到内核的具体位置。
这个功能需要打开CONFIG_RANDOMIZE_BASE获取支持:
可以看到,X86架构有两个kaslr.c文件,分别处理内核和内核加载程序的随机化操作,前者上面已经了做了介绍,这里介绍的是后者。
测试关闭CONFIG_RANDOMIZE_BASE后的内核加载地址是否不再随机化:
可以看到,内核运行时实际的符号地址等于链接脚本中指定的地址,也等于反编译文件的开始地址,所以,内核加载地址没有被随机化。
另外,由于CONFIG_RANDOMIZE_BASE是CONFIG_DYNAMIC_MEMORY_LAYOUT和CONFIG_RANDOMIZE_MEMORY的先决条件,所以,当关闭内核的物理加载地址随机化后,内核虚拟地址空间的随机化也被关闭了,此时,PAGE_OFFSET是宏定义的值:
内核加载地址随机化是如何实现的呢?这涉及到内核bzImage文件的格式问题,内核顶层目录编译得到的ELF文件vmlinux是真正的内核,这个内核是保护模式下的,无法直接运行,所以LINUX构建系统在将这个真正的内核压缩后,套上一个解压程序作为外壳后,再次装配出一个新的ELF文件,也叫vmlinux,bzImage文件就是由最终的这个vmlinux提取二进制指令段后压缩生成的:
如果将内核映像比作航天器,则壳程序就相当于火箭的一级推进子系统,这部分负责将真正的内核解压后加载到内存,为内核创建保护模式的环境,之后跳转到真正的内核继续运行,而后者就相当于二级推进了。
既然如此,内核拷贝随机化的操作一定发生在一级推进程序里面,我们看内核启动的流程:
可以看到,一级助推程序中会调用choose_random_location函数获取一个随机的加载地址,之后将内核解压到这个地址,之后再跳入到这个地址继续执行真正的内核,所以,加载地址随机化的逻辑搞清楚了。
如前所述,这一切流程都需要打开CONFIG_RANDOMIZE_BASE选项,支持kaslr才能实现:
关闭KASLR有一个优雅的办法,choose_random_location入口处会检测命令行中是否有nokaslr字符,如果有,则直接退出生成随机地址的逻辑,见下图:
所以,关闭地址随机化最好的办法,是在启动命令行中加入nokaslr项:
关于加载地址随机化后,如何调整内核启动阶段的页表映射具体细节可以结合这篇文章:
linux内核——内存管理之早期页表建立过程 - 知乎
结束!
这篇关于Linux内核地址空间随机化ASLR的几种实现方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!