内核热补丁,真的安全么?

文章深入分析了Linux内核函数热替换中使用jump指令直接替换函数是否安全,探讨了函数调用约定、GCC编译优化、-pg选项、-fipa-ra选项以及notrace属性的影响。实验表明,在特定条件下,如启用-pg选项,直接替换函数是安全的。同时,文章也讨论了ARM64架构下的相似情况,得出结论在多种架构下,这种方法同样可行。
摘要由CSDN通过智能技术生成

简介: Linux 内核函数的热替换“撞上”函数调用约定还靠谱吗?

IMG_1931.png

Linux 内核热补丁可以修复正在运行的 linux 内核,是一种维持线上稳定性不可缺少的措施,现在比较常见的比如 kpatch 和 livepatch。内核热补丁可以修复内核中正在运行的函数,用已修复的函数替换掉内核中存在问题的函数从而达到修复目的。

函数替换的思想比较简单,就是在执行旧函数时绕开它的执行逻辑而跳转到新的函数中,有一种比较简单粗暴的方式,就是将原函数的第一条指令修改为“ jump 目标函数”指令,即直接跳转到新的函数以达到替换目的。

那么,问题来了,这么做靠谱吗?直接将原函数的第一条指令修改为 jump 指令,会破坏掉原函数和它的调用者之间的寄存器上下文关系,存在安全隐患!本文会针对该问题进行探索和验证。

安全性冲击:问题呈现

对于函数调用,假设存在这样两个函数 funA 和 funB,其中 funA 调用 funB 函数,这里称 funA 为 caller(调用者),funB 为 callee(被调用者),funA 和 funB 都使用了相同的寄存器 R,如下所示:

IMG_1932.png

图1 funA 和 funB 都使用了寄存器 R,funA 再次使用 R 时已经被 funB 修改

因此,当 funA 再次使用到 R 的数据已经是错误的数据了。如果 funA 在调用 funB 前保存寄存器 R 中的数据,funB 返回后再将数据恢复到 R 中,或者 funB 先保存 R 中原有的数据,然后在返回前恢复,就可以解决这类问题。

唯一的调用约定

那寄存器该由 caller 还是 callee 来保存?这就需要遵循函数的调用约定(call convention),不同的 ABI 和不同的平台,函数的调用约定是不一样的,对于 Linux 来说,它遵循的是 System V ABI 的 call convention,x86_64 平台下函数调用约定有且只有一种,调用者 caller 和被调用者 callee 需要对相应的寄存器进行保存和恢复操作:

  • Caller-save registers : RDI, RSI, RDX, RCX, R8, R9, RAX, R10, R11
  • Callee-save registers : RBX, RBP, R12, R13, R14, R15

调用约定,gcc 它遵守了吗?

设问:当函数实现很简单,只用到了少量寄存器,那没使用到的还需要保存吗?

答案:it depends。根据编译选项决定。

众所周知,GCC 编译器有 -O0、-O1、-O2 和 -Ox 等编译优化选项,优化范围和深度随 x 增大而增大(-O0是不优化,其中隐含的意思是,它会严格遵循 ABI 中的调用约定,对所有使用的寄存器进行保存和恢复)。

Linux 内核选用的都是 -O2 优化。GCC 会选择性的不遵守调用约定,也就是设问里提到的,不需要保存没使用到的寄存器。

当【运行时替换】撞见【调用约定】

GCC 之所以可以做这个优化,是因为 GCC 高屋建瓴,了解程序的执行流。当它知道 callee,caller 的寄存器分配情况,就会大胆且安全地做各种优化。

但是,运行时替换破坏了这个假设,GCC 所掌握的 callee 信息,极有可能是错误的。那么这些优化可能会引发严重问题。这里以一个具体的实例进行详细说明,这是一个用户态的例子( x86_64 平台)

//test.c 文件
//编译命令:gcc test.c -o test -O2  (kernel 采用的是 O2 优化选项)
//执行过程:./test
//输入参数:4

#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <math.h>

#define noinline __attribute__ ((noinline))  //禁止内联

static noinline int c(int x)
{
        return x * x * x;
}

static noinline int b(int x)
{
        return x;
}

static noinline int newb(int x)
{
        return c(x * 2) * x;
}

static noinline int a(int x)
{
        int volatile tmp = b(x);  // tmp = 8 ** 3 * 4
        return x + tmp;           // return 4(not 8) + tmp
}

int main(void)
{
        int x;
        scanf("%d", &x);

        if (mprotect((void*)(((unsigned long)&b) & (~0xFFFF)), 15, 
                             PROT_WRITE | PROT_EXEC | PROT_READ)) {
                perror("mprotect");
                return 1;
        }

        /* 利用 jump 指令将函数 b 替换为 newb 函数 */
        ((char*)b)[0] = 0xe9;
        *(long*)((unsigned long)b + 1) = (unsigned long)&newb
                                         - (unsigned long)&b - 5;
        
        printf("%d", a(x));
        return 0;
}
  • 程序解释:该程序是对输入的数字进行计算,运行时利用 jump 指令将程序中的函数 b 替换为 newb 函数,即,将 y = x + x 计
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值