Linux Kernel CFI机制简介及测试禁用
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就是保证函数指针跳转到正确的位置,并且返回到正确的位置。
从这里来看,其实我们可以看到对于函数指针来说,我们需要保护两个地方:跳转到正确的位置、返回到正确的位置。这两个地方有两个专有名词:
- forward-edge
- backward-edge
此外,我们还应该知道,在编写代码的时候,分为直接函数调用(direct function call),间接函数调用(indirect function call)。他们的示例如下:
void target(void)
{
//... ...
}
typedef void(*fn)(void);
int main(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 -> LTO
CONFIG_CFI_CLANG=y
CONFIG_CFI_PERMISSIVE=y
我们的测试驱动例子:
#include <linux/module.h> // 必须的头文件,定义了MODULE_*宏
#include <linux/kernel.h> // 包含内核信息头文件
#include <linux/init.h> // 包含 __init 和 __exit 宏
static int param_int = 0;
module_param(param_int, int, 0644);
static void hello_cfi_i(int i){
printk(KERN_INFO "hello_cfi_i\n");
}
static void hello_cfi_f(float i){
printk(KERN_INFO "hello_cfi_f\n");
}
typedef void (*hello_cfi_func_i)(int);
typedef void (*hello_cfi_func_f)(float);
struct node {
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];
};
struct node fn_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},
};
// 模块初始化函数
static int __init hello_init(void)
{
fn_arr.i0[param_int](param_int);
printk(KERN_INFO "Hello, World!\n");
return 0; // 返回0表示加载成功
}
// 模块清理函数
static void __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: 有问题请留言,看到后我会第一时间回复。