Linux内核地址空间随机化ASLR的几种实现方法

2024-04-26 03:18

本文主要是介绍Linux内核地址空间随机化ASLR的几种实现方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

ASLR(Address Space Layout Randomization)在2005年被引入到Linux内核kernel 2.6.12中。地址空间随机化在内核中有多种实现和表现方式,下面分别介绍。

堆栈随机化

堆栈随机化是一项安全增强,它允许对系统调用发生时,内核使用的堆栈添加一个随机偏移。这给基于stack的攻击增加了难度,因为stack攻击通常要求stack有个固定的layout。现在每次系统调用,stack的layout都变化的话,攻击就无规律可寻了。

cbdfdc3067bdaafb6ec7bef1549da510.png

首先以系统调用为主线,看一下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的几种实现方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/936570

相关文章

linux本机进程间通信之UDS详解

《linux本机进程间通信之UDS详解》文章介绍了Unix域套接字(UDS)的使用方法,这是一种在同一台主机上不同进程间通信的方式,UDS支持三种套接字类型:SOCK_STREAM、SOCK_DGRA... 目录基础概念本机进程间通信socket实现AF_INET数据收发示意图AF_Unix数据收发流程图A

Python如何实现读取csv文件时忽略文件的编码格式

《Python如何实现读取csv文件时忽略文件的编码格式》我们再日常读取csv文件的时候经常会发现csv文件的格式有多种,所以这篇文章为大家介绍了Python如何实现读取csv文件时忽略文件的编码格式... 目录1、背景介绍2、库的安装3、核心代码4、完整代码1、背景介绍我们再日常读取csv文件的时候经常

Golang中map缩容的实现

《Golang中map缩容的实现》本文主要介绍了Go语言中map的扩缩容机制,包括grow和hashGrow方法的处理,具有一定的参考价值,感兴趣的可以了解一下... 目录基本分析带来的隐患为什么不支持缩容基本分析在 Go 底层源码 src/runtime/map.go 中,扩缩容的处理方法是 grow

Springboot控制反转与Bean对象的方法

《Springboot控制反转与Bean对象的方法》文章介绍了SpringBoot中的控制反转(IoC)概念,描述了IoC容器如何管理Bean的生命周期和依赖关系,它详细讲解了Bean的注册过程,包括... 目录1 控制反转1.1 什么是控制反转1.2 SpringBoot中的控制反转2 Ioc容器对Bea

Go 1.23中Timer无buffer的实现方式详解

《Go1.23中Timer无buffer的实现方式详解》在Go1.23中,Timer的实现通常是通过time包提供的time.Timer类型来实现的,本文主要介绍了Go1.23中Timer无buff... 目录Timer 的基本实现无缓冲区的实现自定义无缓冲 Timer 实现更复杂的 Timer 实现总结在

基于Python实现多语言朗读与单词选择测验

《基于Python实现多语言朗读与单词选择测验》在数字化教育日益普及的今天,开发一款能够支持多语言朗读和单词选择测验的程序,对于语言学习者来说无疑是一个巨大的福音,下面我们就来用Python实现一个这... 目录一、项目概述二、环境准备三、实现朗读功能四、实现单词选择测验五、创建图形用户界面六、运行程序七、

linux环境openssl、openssh升级流程

《linux环境openssl、openssh升级流程》该文章详细介绍了在Ubuntu22.04系统上升级OpenSSL和OpenSSH的方法,首先,升级OpenSSL的步骤包括下载最新版本、安装编译... 目录一.升级openssl1.官网下载最新版openssl2.安装编译环境3.下载后解压安装4.备份

Vue中动态权限到按钮的完整实现方案详解

《Vue中动态权限到按钮的完整实现方案详解》这篇文章主要为大家详细介绍了Vue如何在现有方案的基础上加入对路由的增、删、改、查权限控制,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、数据库设计扩展1.1 修改路由表(routes)1.2 修改角色与路由权限表(role_routes)二、后端接口设计

C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)

《C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)》本文主要介绍了C#集成DeepSeek模型实现AI私有化的方法,包括搭建基础环境,如安装Ollama和下载DeepS... 目录前言搭建基础环境1、安装 Ollama2、下载 DeepSeek R1 模型客户端 ChatBo

Qt实现发送HTTP请求的示例详解

《Qt实现发送HTTP请求的示例详解》这篇文章主要为大家详细介绍了如何通过Qt实现发送HTTP请求,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、添加network模块2、包含改头文件3、创建网络访问管理器4、创建接口5、创建网络请求对象6、创建一个回复对