Linux内核如何替换内核函数并调用原始函数

替换一个已经在内存中的函数,使得执行流流入咱们本身的逻辑,而后再调用原始的函数,这是一个很古老的话题了。好比有个函数叫作funcion,而你但愿统计一下调用function的次数,最直接的方法就是 若是有谁调用function的时候,调到下面这个就行了 :linux

void new_function()
{
    count++;
    return function();
}

网上不少文章给出了实现这个思路的Trick,并且一直以来计算机病毒也都采用了这种偷梁换柱的伎俩来实现本身的目的。然而,当你亲自去测试时,发现事情并不那么简单。golang

网上给出的许多方法均再也不适用了,缘由是在早期,这样作的人比较少,处理器和操做系统大可没必要理会一些不符合常规的作法,可是随着这类Trick开始作坏事影响到正常的业务逻辑时,处理器厂商以及操做系统厂商或者社区便不得不在底层增长一些限制性机制,以防止这类Trick继续起做用。服务器

常见的措施有两点:架构

  • 可执行代码段不可写

这个措施便封堵住了你想经过简单memcpy的方式替换函数指令的方案。tcp

  • 内存buffer不可执行

这个措施便封堵住了你想把执行流jmp到你的一个保存指令的buffer的方案。函数

  • stack不可执行

别看这些措施都比较low,一看谁都懂,它们却避免了大量的缓冲区溢出带来的危害。学习

那么若是咱们想用替换函数的Trick作正常的事情,怎么办?测试

我来简单谈一下个人方法。首先我不会去HOOK用户态的进程的函数,由于这样意义不大,改一下重启服务会好不少。因此说,本文特指HOOK内核函数的作法。毕竟内核从新编译,重启设备代价很是大。spa

咱们知道,咱们目前所使用的几乎全部计算机都是冯诺伊曼式的统一存储式计算机,即指令和数据是存在一块儿的,这就意味着咱们必然能够在操做系统层面随意解释内存空间的含义。操作系统

咱们在作正当的事情,因此我假设咱们已经拿到了系统的root权限而且能够编译和插入内核模块。那么接下来的事情彷佛就是一个流程了。

是的,修改页表项便可,即使没法简单地经过memcpy来替换函数指令,咱们仍是能够用如下的步骤来进行指令替换:

  1. 从新将函数地址对应的物理内存映射成可写;
  2. 用本身的jmp指令替换函数指令;
  3. 解除可写映射。

很是幸运,内核已经有了现成的 text_poke/text_poke_smp 函数来完成上面的事情。

一样的,针对一个堆上或者栈上分配的buffer不可执行,咱们依然有办法。办法以下:

  1. 编写一个stub函数,实现随意,其代码指令和buffer至关;
  2. 用上面重映射函数地址为可写的方法用buffer重写stub函数;
  3. 将stub函数保存为要调用的函数指针。

是否是有点意思呢?下面是一个步骤示意图:

Linux内核如何替换内核函数并调用原始函数

下面是一个代码,我稍后会针对这个代码,说几个细节方面的东西:

#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/cpu.h>
#include <linux/module.h>
#include <net/tcp.h>
#define OPTSIZE    5
// saved_op保存跳转到原始函数的指令
char saved_op[OPTSIZE] = {0};
// jump_op保存跳转到hook函数的指令
char jump_op[OPTSIZE] = {0};
static unsigned int (*ptr_orig_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);
static unsigned int (*ptr_ipv4_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);
// stub函数,最终将会被保存指令的buffer覆盖掉
static unsigned int stub_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state)
{
    printk("hook stub conntrackn");
    return 0;
}
// 这是咱们的hook函数,当内核在调用ipv4_conntrack_in的时候,将会到达这个函数。
static unsigned int hook_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state)
{
    printk("hook conntrackn");
    // 仅仅打印一行信息后,调用原始函数。
    return ptr_orig_conntrack_in(ops, skb, in, out, state);
}
static void *(*ptr_poke_smp)(void *addr, const void *opcode, size_t len);
static __init int hook_conn_init(void)
{
    s32 hook_offset, orig_offset;
    // 这个poke函数完成的就是重映射,写text段的事
    ptr_poke_smp = kallsyms_lookup_name("text_poke_smp");
    if (!ptr_poke_smp) {
        printk("err");
        return -1;
    }
    // 嗯,咱们就是要hook住ipv4_conntrack_in,因此要先找到它!
    ptr_ipv4_conntrack_in = kallsyms_lookup_name("ipv4_conntrack_in");
    if (!ptr_ipv4_conntrack_in) {
        printk("err");
        return -1;
    }
    // 第一个字节固然是jump
    jump_op[0] = 0xe9;
    // 计算目标hook函数到当前位置的相对偏移
    hook_offset = (s32)((long)hook_ipv4_conntrack_in - (long)ptr_ipv4_conntrack_in - OPTSIZE);
    // 后面4个字节为一个相对偏移
    (*(s32*)(&jump_op[1])) = hook_offset;
    // 事实上,咱们并无保存原始ipv4_conntrack_in函数的头几条指令,
    // 而是直接jmp到了5条指令后的指令,对应上图,应该是指令buffer里没
    // 有old inst,直接就是jmp y了,为何呢?后面细说。
    saved_op[0] = 0xe9;
    // 计算目标原始函数将要执行的位置到当前位置的偏移
    orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE));
    (*(s32*)(&saved_op[1])) = orig_offset;
    get_online_cpus();
    // 替换操做!
    ptr_poke_smp(stub_ipv4_conntrack_in, saved_op, OPTSIZE);
    ptr_orig_conntrack_in = stub_ipv4_conntrack_in;
    barrier();
    ptr_poke_smp(ptr_ipv4_conntrack_in, jump_op, OPTSIZE);
    put_online_cpus();
    return 0;
}
module_init(hook_conn_init);
static __exit void hook_conn_exit(void)
{
    get_online_cpus();
    ptr_poke_smp(ptr_ipv4_conntrack_in, saved_op, OPTSIZE);
    ptr_poke_smp(stub_ipv4_conntrack_in, stub_op, OPTSIZE);
    barrier();
    put_online_cpus();
}
module_exit(hook_conn_exit);
MODULE_DESCRIPTION("hook test");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.1");

测试是OK的。

须要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

Linux内核如何替换内核函数并调用原始函数

在上面的代码中,saved_op中为何没有old inst呢?直接就是一个jmp y,这岂不是将原始函数中的头几个字节的指令给遗漏了吗?

其实说到这里,还真有个很差玩的Trick,起初我真的就是老老实实保存了前5个本身的指令,而后当须要调用原始ipv4_conntrack_in时,就先执行那5个保存的指令,也是OK的。随后我objdump这个函数发现了下面的代码:

0000000000000380 <ipv4_conntrack_in>:
      380:   e8 00 00 00 00          callq  385 <ipv4_conntrack_in+0x5>
      385:   55                      push   %rbp
      386:   49 8b 40 18             mov    0x18(%r8),%rax
      38a:   48 89 f1                mov    %rsi,%rcx
      38d:   8b 57 2c                mov    0x2c(%rdi),%edx
      390:   be 02 00 00 00          mov    $0x2,%esi
      395:   48 89 e5                mov    %rsp,%rbp
      398:   48 8b b8 e8 03 00 00    mov    0x3e8(%rax),%rdi
      39f:   e8 00 00 00 00          callq  3a4 <ipv4_conntrack_in+0x24>
      3a4:   5d                      pop    %rbp
      3a5:   c3                      retq
      3a6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
      3ad:   00 00 00

注意前5个指令: e8 00 00 00 00 callq 385 <ipv4_conntrack_in+0x5>

能够看到,这个是能够忽略的。由于无论怎么说都是紧接着执行下面的指令。因此说,我就省去了inst的保存。

若是按照个人图示中常规的方法的话,代码稍微改一下便可:

char saved_op[OPTSIZE+OPTSIZE] = {0};
...
    // 增长一个指令拷贝的操做
    memcpy(saved_op, (unsigned char *)ptr_ipv4_conntrack_in, OPTSIZE);
    saved_op[OPTSIZE] = 0xe9;
    orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE + OPTSIZE));
    (*(s32*)(&saved_op[OPTSIZE+1])) = orig_offset;
...

可是以上的只是玩具。

有个很是现实的问题。在我保存原始函数的头n条指令的时候,n究竟是多少呢?在本例中,显然n是5,符合现在Linux内核函数第一条指令几乎都是callq xxx的惯例。

然而,若是一个函数的第一条指令是下面的样子:

op d1 d2 d3 d4 d5

即一个操做码须要5个操做数,我要是只保存5个字节,最后在stub中的指令将会是下面的样子:

op d1 d2 d3 d4 0xe9 off1 off2 off3 off4

这显然是错误的,op操做码会将jmp指令0xe9解释成操做数。

解药呢?固然有咯。

咱们不能鲁莽地备份固定长度的指令,而是应该这样作:

curr = 0
if orig[0] 为单字节操做码
    saved_op[curr] = orig[curr];
    curr++;
else if orig[0] 携带1个1字节操做数
    memcpy(saved_op, orig, 2);
    curr += 2;
else if orig[0] 携带2字节操做数
    memcpy(saved_op, orig, 3);
    curr += 3;
...
saved_op[curr] = 0xe9; // jmp
offset = ...
(*(s32*)(&saved_op[curr+1])) = offset;

这是正确的作法。