PS:要转载请注明出处,本人版权所有。PS: 这个只是基于《我自己》的理解,如果和你的原则及想法相冲突,请谅解,勿喷。
环境说明
无
前言
当我们为android移植linux的驱动程序的时候,总会遇到一些错误,这些错误有一部分就是android 内核开启的安全的机制导致的。本文就会介绍一种内核的安全机制:Kernel Control Flow Integrity(kCFI)。
此外,这里还要说明一下,Control Flow Integrity(CFI)与 Kernel Control Flow Integrity(kCFI)是不一样的,kCFI只检查函数指针,CFI还具备其他很多的检查,详情请参考:https://clang.llvm.org/docs/ControlFlowIntegrity.html 。
Kernel Control Flow Integrity(kCFI)原理简单介绍
Control Flow Integrity的翻译是控制流完整性,从直译来看,其实就是用一些方法保证来保证我们的指令执行到正确的位置。我们从clang官方文档知道,kCFI只检查函数指针,那么其实kCFI就是保证函数指针跳转到正确的位置,并且返回到正确的位置。
从这里来看,其实我们可以看到对于函数指针来说,我们需要保护两个地方:跳转到正确的位置、返回到正确的位置。这两个地方有两个专有名词:
此外,我们还应该知道,在编写代码的时候,分为直接函数调用(direct function call),间接函数调用(indirect function call)。他们的示例如下:
voidtarget(void){//... ...}typedefvoid(*fn)(void);intmain(int argc, char * argv[]){// direct function call target();//indirect function call fn _id_fn = target; _id_fn();}
从示例可以知道,indirect function call其实就是函数指针这种调用形式。
此外,我们还要知道,如果我们想破坏代码的执行流,那么我们必须在可写、可读、可执行的内存里面写入shellcode,并跳转到这个shellcode,否则我们的代码是无法工作的。那么显而易见的事情是,通过函数指针来调用函数,我们的目标是明确的,因此我们可以校验这些目标的原型、地址等等信息。
因为我们需要验证目标的原型、地址等等信息,所以,当我们在生成可执行文件的时候,需要知道所有的函数目标的信息,这个时候,就需要一个叫做Link Time Optimization(LTO)的功能,因为只有最终可执行文件链接时,才知道所有的函数目标信息。
kCFI演示示例
首先在qemu中运行一个arm64的linux模拟器,然后为linux内核配置如下内核选项:
# General architecture-dependent options -> LTOCONFIG_CFI_CLANG=yCONFIG_CFI_PERMISSIVE=y
我们的测试驱动例子:
#include<linux/module.h>// 必须的头文件,定义了MODULE_*宏#include<linux/kernel.h>// 包含内核信息头文件#include<linux/init.h>// 包含 __init 和 __exit 宏staticint param_int = 0;module_param(param_int, int, 0644);staticvoidhello_cfi_i(int i){ printk(KERN_INFO "hello_cfi_i\n");}staticvoidhello_cfi_f(float i){ printk(KERN_INFO "hello_cfi_f\n");}typedefvoid(*hello_cfi_func_i)(int);typedefvoid(*hello_cfi_func_f)(float);structnode { hello_cfi_func_i i0[1]; hello_cfi_func_f f0[1]; hello_cfi_func_i i1[1]; hello_cfi_func_f f1[1]; hello_cfi_func_i i2[1]; hello_cfi_func_f f2[1];};structnodefn_arr = { .i0 = {hello_cfi_i}, .f0 = {hello_cfi_f}, .i1 = {hello_cfi_i}, .f1 = {hello_cfi_f}, .i2 = {hello_cfi_i}, .f2 = {hello_cfi_f},};// 模块初始化函数staticint __init hello_init(void){ fn_arr.i0[param_int](param_int); printk(KERN_INFO "Hello, World!\n");return0; // 返回0表示加载成功}// 模块清理函数staticvoid __exit hello_exit(void){ printk(KERN_INFO "Goodbye, World!\n");}// 注册模块初始化和清理函数module_init(hello_init);module_exit(hello_exit);MODULE_LICENSE("GPL"); // 模块许可证MODULE_AUTHOR("Your Name"); // 模块作者MODULE_DESCRIPTION("A simple Hello World Module"); // 模块描述
我们传入参数0,执行fn_arr.i0[0],测试正常跳转

我们传入参数1,执行fn_arr.i0[1],测试传入参数原型不匹配(本来应该调用hello_cfi_i,实际调用hello_cfi_f)

测试数组越界访问

后记
从上面来看,kCFI一般会对调用类型、调用的目标地址进行判断,更多细节,去看CFI的具体原理。
参考文献
- • https://clang.llvm.org/docs/ControlFlowIntegrity.html
- • https://source.android.com/docs/security/test/kcfi
- • https://outflux.net/slides/2020/lca/cfi.pdf
打赏、订阅、收藏、丢香蕉、硬币,请关注公众号
PS: 请尊重原创,不喜勿喷。PS: 要转载请注明出处,本人版权所有。PS: 有问题请留言,看到后我会第一时间回复。