往linux内核函数挂钩子

概述

本文讲解替换一个已经在内存中的函数,使得执行流流入我们自己的逻辑,然后再调用原始的函数。比如有个函数叫做funcion,而你希望统计一下调用function的次数,最直接的方法就是如果有谁调用function的时候,调到下面这个函数就好了。

void new_function()

{

         count++;

         return function();

}

钩子存在的意义

当内核程序已经在运行过程中,如果需要对某个内核函数做出小的改动,原始方法是修改内核源码或驱动程序,重新编译在加载二进制文件,这样的工作量相对比较大。只有当动态加载驱动程序时修改才比较方便。为什么不对应用程序hook呢?因为这样意义不大,改一下源码重启服务会好很多,内核重新编译、重启设备代价非常大。

钩子原理

在x86架构与linux系统平台,每个函数编译后地址的前5个字节都是callq  function+0x5(及是默认指向下一条指令,注意图中的地址是一个相对地址概念,实际地址跟你运行的进程有关),图 1是文章后面用到的HerokHook.ko经过反编译得到的,从图 1中可以清晰看到跳转到函数的第一条指令是callq,指向下一条指令,紧接着是堆栈。

                                                                                                                               图 1

图 2是本次实验的流程图,orig_ptr指向linux内核需要hook的函数,当内核调用orig_ptr指向函数时候,首先会执行第一条指令,在我们的函数中修改callq  orig_ptr +0x5 为jmp Hook_ptr-5,在我们的函数中执行一系列操作后,在通过return ptr_tmp调用中间辅助函数,将ptr_tmp函数的前5字节xxx修改成jmp orig_ptr+0x5,这里必须跳过orig_ptr的前5字节,因为这5个字节函数已经被我们修改,不然就进入死循环。

                                                                                 图 2

中间辅助函数存在的意义,如果在hook_ptr中直接返回调用orig_ptr函数,那么没有绕过前5个字节就会进入死循环。在hook_ptr函数末尾不能添加jmp跳转指令,因为你不知道那些字节是保留,以及堆栈平衡情况。所以需要添加中间辅助函数。

内核钩子接口

读者可能认为现在已经具备注册钩子的条件,其实不是这样的,在早期Linux内核版本中,如果具备上述流程就可以通过memcpy和jmp buffer(buffer存放指令)挂钩子,由于一些不符合常规的做法已经影响正常的业务逻辑,所以Linux内核做了如下限制:

  1. 可执行代码段不可写:这个措施便封堵住了你想通过简单memcpy的方式替换函数指令的方案。
  2. 内存buffer不可执行:这个措施便封堵住了你想把执行流jmp到你的一个保存指令的buffer的方案。
  3. stack不可执行:避免缓冲区溢出、栈溢出。

查阅Linux内核资料,发现Linux内核已经提供了text_poke_smp和kallsyms_lookup_name函数接口。

钩子必然可挂载原理

大家都知道,x86平台采用的是冯诺依曼体系结构,冯诺依曼结构采用统一存储,即指令与数据采用相同总线传输,那么在操作系统层我们必然可以随意解释内存空间的含义。不管是通过内核接口还是自定义接口(申请权限,重新映射当前连续page页)都可以更改内存空间含义,所以很多不正常操作计算机的原理都是基于如此。早期的单机游戏可以搜索内存数据变化来确定状态值,进而重新映射当前page权限进行重新赋值操作。

代码编写

hook驱动程序

hello.c是原驱动程序,代码中编写最简单的hello驱动程序,Makefile,驱动程序。

test.c

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/init.h>

#include <linux/device.h>

#include <linux/miscdevice.h>

#include <linux/delay.h>

#include <asm/irq.h>

#include <asm/io.h>

#include <asm/uaccess.h>

#include <mach/regs-gpio.h>

#include <mach/hardware.h>

#include <linux/device.h>

#include <linux/gpio.h>

 

#define DEVICE_NAME  "hello"

static struct class *hello_class;

 

static int hello_open(struct tty_struct * tty, struct file * filp)

{

    printk("open is successd!\n");        

    return 0;

}

static int hello_read(struct file * file,char __user * userbuf,size_t bytes,loff_t * off)

{

  unsigned char buf[4];

  buf[0]=0x11;

  buf[1]=0x33;

  buf[2]=0x44;

  buf[3]=0x55;

  copy_to_user(userbuf,buf,sizeof(buf));

  return(sizeof(buf));

}

 

static struct file_operations hello_fops = {

     .owner = THIS_MODULE,

     .read   = hello_read,

     .open  =hello_open,

};

static int major;

static int hello_init(void)

{

         major= register_chrdev(0, DEVICE_NAME, &hello_fops);

         hello_class = class_create(THIS_MODULE, DEVICE_NAME);

         device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");

         printk(KERN_ALERT "init is scussed!\n");

         return 0;

}

static void hello_exit(void)

{

         unregister_chrdev(major, DEVICE_NAME);

         device_destroy(hello_class,MKDEV(major, 0));

         class_destroy(hello_class);

         printk(KERN_ALERT "Goodbye, cruel world\n");

}

 

module_init(hello_init);

module_exit(hello_exit);

MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("Herok");

MODULE_DESCRIPTION("A simple hello world module");

MODULE_ALIAS("A simplest module");

 

hook驱动程序的测试程序

test.c

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdio.h>

#include <poll.h>

 

int main(int argc,char **argv)

{

         int fd;

         unsigned char buf[4];

         fd = open("/dev/hello", O_RDWR); 

         if(fd<0){

                   printf("open is error!\n");

                   return -1;

         }

 

         read(fd,&buf,4);

         printf("%x\n",buf[0]);

         printf("%x\n",buf[1]);

         printf("%x\n",buf[2]);

         printf("%x\n",buf[3]);

         close(fd);

}

 

​​​​​​​hook驱动程序

HerokHook.c   

#include <linux/kallsyms.h>

#include <linux/cpu.h>

#include <linux/kprobes.h>

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/init.h>

#include <linux/device.h>

#include <linux/miscdevice.h>

#include <linux/delay.h>

#include <asm/io.h>

#include <asm/uaccess.h>

#include <linux/device.h>

 

#define OPTSIZE  5

 

char saved_op[OPTSIZE]={0};

char jump_op[OPTSIZE]={0};

 

int (*ptr_tmp_hello_read)(struct file * file,char __user * userbuf,size_t bytes,loff_t * off);

int (*ptr_orig_hello_read)(struct file * file,char __user * userbuf,size_t bytes,loff_t * off);

 

int stub_hello_conntrack_in(struct file * file,char __user * userbuf,size_t bytes,loff_t * off)

{

         printk("hook stub conntrack\n");   

         return 0;

}

int hook_hello_read(struct file * file,char __user * userbuf,size_t bytes,loff_t * off)

{

         printk(KERN_EMERG "hook conntrack herok\n");

         return ptr_tmp_hello_read(file,userbuf, bytes,off);

}

 

static void *(*ptr_poke_smp)(void *addr, const void *opcode, size_t len);

static __init int replace_function__init(void)

{

         s32 hook_offset, orig_offset;

 

         // 这个poke函数完成的就是重映射,写text段

         ptr_poke_smp = kallsyms_lookup_name("text_poke_smp");

         if (!ptr_poke_smp) {

                   printk(KERN_INFO "err");

                   return -1;

         }

         //找到需要hook的函数

         ptr_orig_hello_read = kallsyms_lookup_name("hello_read");

         printk(KERN_EMERG "ptr_orig_hello_read=%#x\n",ptr_orig_hello_read);

         if (!ptr_orig_hello_read) {

                   printk("err");

                   return -1;

         }

         jump_op[0] = 0xe9;  //jmp指令

         // 计算目标hook函数到当前位置的相对偏移

         hook_offset = (s32)((long)hook_hello_read - (long)ptr_orig_hello_read - OPTSIZE);

         // 后面4个字节为一个相对偏移

         (*(s32*)(&jump_op[1])) = hook_offset;

         saved_op[0] = 0xe9;

         // 计算目标原始函数将要执行的位置到当前位置的偏移

         orig_offset = (s32)((long)ptr_orig_hello_read + OPTSIZE - ((long)stub_hello_conntrack_in + OPTSIZE));

         (*(s32*)(&saved_op[1])) = orig_offset;

         get_online_cpus();

         // 替换操作!

         ptr_poke_smp(stub_hello_conntrack_in, saved_op, OPTSIZE);

         ptr_tmp_hello_read = stub_hello_conntrack_in;

         printk(KERN_EMERG "ptr_tmp_hello_read=%#x\n",ptr_tmp_hello_read);

         barrier();

         ptr_poke_smp(ptr_orig_hello_read, jump_op, OPTSIZE);

         put_online_cpus();

 

         return 0;

}

 

static __exit void replace_function_exit(void)

{

         get_online_cpus();

         ptr_poke_smp(ptr_orig_hello_read, saved_op, OPTSIZE);

         ptr_poke_smp(stub_hello_conntrack_in, jump_op, OPTSIZE);

         barrier();

         put_online_cpus();

}

module_init(replace_function__init);

module_exit(replace_function_exit);

 

MODULE_DESCRIPTION("hook test");

MODULE_LICENSE("GPL");

MODULE_VERSION("1.1");

​​​​​​​Makefile程序

代码如图 3。

                                                               图 3

 

测试

编译生成hello.ko和HerokHook.ko,依次加载这两个驱动程序,并且编译并执行测试程序,从程序运行结果发现,程序将先调用我们的hook函数,然后在调用原函数。图 4可以看到函数的地址空间,也可以通过cat /proc/modules得到所以内核的地址空间范围。

                                                                                                 图 4

结语

至于在Linux应用程序中如何编译与加载驱动程序读者可以自行百度,这个相对简单。在centos平台需要安装linux-headrs库,kernel-headers.x86_64和kernel.x86_64两个库,安装完成后再/usr/src/kernels目录下会出现内核文件,在Makefile中指定该路径就可以正常编译。

hook怎么在内核中玩完全由读者决定,最好的是与tcp这个代码分支比较多的糟糕代码一起玩,这样玩花样比较多,后期带领大家领略linux中TCP世界。

 

Never lock up your dreaming box, and the greatest peril to the soul is that one is likely to get precisely what he is seeking.