Location>code7788 >text

Linux Kernel CFI Mechanism Introduction and Test Disabling

Popularity:817 ℃/2024-07-28 18:50:17
Environmental description

  not have

preamble


  When we port linux driver for android, we always encounter some errors, some of these errors are caused by android kernel security mechanism. In this article, we will introduce a kernel security mechanism: Kernel Control Flow Integrity (kCFI).

  Also, it should be noted here that Control Flow Integrity (CFI) is not the same as Kernel Control Flow Integrity (kCFI). kCFI only checks for function pointers, and CFI has a lot of other checks, so please refer to it for details:/docs/





A Brief Introduction to the Kernel Control Flow Integrity (kCFI) Principle


  The translation of Control Flow Integrity is Control Flow Integrity, from the direct translation, it is actually using some methods to ensure to ensure that our instructions are executed to the correct location. We know from the official clang documentation that kCFI only checks function pointers, so actually kCFI is to ensure that the function pointer jumps to the correct location and returns to the correct location.

  From here, we can actually see that for function pointers, there are two places we need to protect: jumping to the right place, and returning to the right place. There are two proper names for these two places:

  • forward-edge
  • backward-edge

  In addition, we should also know that when writing code, it is divided into direct function call (direct function call), indirect function call (indirect function call). Their examples are as follows:

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();
} 

  As you can see from the example, the indirect function call is actually a function pointer call.

  Also, we need to know that if we want to break the execution flow of our code, then we have to write shellcode inside writable, readable and executable memory and jump to that shellcode, otherwise our code won't work. The obvious thing then is that by calling functions via function pointers, our targets are explicit, so we can check the prototypes, addresses, and other information about those targets.

  Because we need to verify the target's prototype, address and other information, so when we are generating the executable file, we need to know all the information about the function target, this time, we need a function called Link Time Optimization (LTO), because only when the final executable file is linked, we know all the information about the function target.





Example of kCFI demo


  First run an arm64 linux emulator in qemu, then configure the following kernel options for the linux kernel:

# General architecture-dependent options -> LTO
CONFIG_CFI_CLANG=y
CONFIG_CFI_PERMISSIVE=y

  Our test-driven example:

#include <linux/> // Required header files,definedMODULE_*macro (computing)
#include <linux/> // Contains kernel information headers
#include <linux/> // embody __init cap (a poem) __exit macro (computing)

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},
};
// Module Initialization Functions
static int __init hello_init(void)
{

    fn_arr.i0[param_int](param_int);

    printk(KERN_INFO "Hello, World!\n");
    return 0; // come (or go) back0Indicates successful loading
}

// Module cleanup functions
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, World!\n");
}

// 注册模块初始化cap (a poem)清理函数
module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL"); // Module License
MODULE_AUTHOR("Your Name"); // Module authors
MODULE_DESCRIPTION("A simple Hello World Module"); // Module Description

  We pass in parameter 0 and execute fn_arr.i0[0] to test for a normal jump

rep_img

  We pass in parameter 1, execute fn_arr.i0[1], and test for a mismatch in the prototype of the passed in parameter (it should have called hello_cfi_i, but actually called hello_cfi_f)

rep_img

  Testing for array out-of-bounds access

rep_img




postscript


  From the above, kCFI generally makes a judgment on the type of call, the target address of the call, and for more details, go to the specific principles of CFI.

bibliography

  • /docs/
  • /docs/security/test/kcfi
  • /slides/2020/lca/



Rewards, subscriptions, favorites, throwing bananas, coins, please pay attention to the public number (Siege Lion's Road to Moving Bricks)
qrc_img