Linux应用层hook技术总结与实例

目录

1.前言

2.ring3 hook

2.1inject

2.2hook

2.2.1 got/plt介绍

2.2.2 got/plt hook 实现

2.2.3 PRELOAD HOOK


 


1.前言

    Linux hook技术一直是linux技术爱好者们研究的一个热点问题,Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3,   ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之,以此类推。

  拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。  应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。

  Linux的hook一般发生在ring0和ring3层,其中ring0层通常是hook Linux系统调用表中的系统调用,而ring3层则一般hook的动态链接库中的函数,接下来我们将会分类讨论。

2.ring3 hook

2.1inject

  当我们需要hook某个应用程序时,我们需要往目标程序中添加自己的代码,这种添加代码的方式就被称之为inject,inject方式又一般分为两种:

  • 动态注入
  • 静态注入

 所谓静态注入就是在程序还未运行时,去修改其so文件,以达到注入函数的目的,由于没具体研究过,所以此处仅提一下,有兴趣的同学可以看一些这个项目,而动态注入一般是使用ptrace来实现的,一般流程为:

  • 1、attach上目标进程。
  • 2、查找到目标进程的dlopen函数,调用dlopen加载到注入的so到目标进程空间。
  • 3、查找到目标进程的dlsym函数,调用dlsym查找到新so中的目标函数地址,并调用目标函数。
  • 4、在目标函数中实现自己的代码逻辑

2.2hook

2.2.1 got/plt介绍

在ring3 层的hook,一般是指 PLT/GOT hook,也就是对程序的got表进行替换,接下里将对程序的got表进行一些介绍,以便于读者理解。

PLT (Problogcedure Linkage Table) 和 GOT (Global Offset Table) 是 GCC 中生成shared library的重要元素。至于为何一定要这两个表?

众所周知Linux对外部函数的引用是采用动态链接的,也就是说,在用到某个函数时,才会具体的去定位其在内存中的位置,之所以这么做是为了程序能更快的启动,否则如果程序启动时,就去加载所有引用函数,会让程序启动的很慢。

为了更好的说明,我们首先编写一个简单的程序,这是一个简单的打印程序pid的函数

vim main.c
------------------------
#include <stdio.h>
#include <unistd>
int main(){
    printf("the pid is %d\n",getpid());
    return 0;
}
--------------------
gcc -o gotTest main.c
readelf -a gotTest

可得到如下结果

我们首先需要关注的是节头中的这几项

其中0x601000是got.plt表在程序中的偏移位置,另外两个表以此类推,我们再来看got.Plt表的具体内容如下

我们可以看到,getpid和printf函数都在这个表中,其中偏移量是他们在表中的地址,信息是他们实际的地址,由于程序未启动,地址还没加载,所以显示的并不是程序的实际地址。

那么这个got表和got.plt表到底是怎么运作的呢?

首先,当一个程序第一次调用一个外部函数时,就会跳转到.plt表(注意,不是.got.plt),而这个表中包含有一些代码,这些代码总共有两个作用:

(1)调用链接器来解析某个外部函数的地址, 并填充到.got.plt中, 然后跳转到该函数。
(2)在.got.plt中查找并跳转到对应外部函数(如果已经填充过)。

  相对的,.got.plt也同样具有两个功能:

 1)如果在之前查找过该符号,内容为外部函数的具体地址。

 2)如果没查找过, 则内容为跳转回.plt的代码。

所以当你首次调用某个外部函数时,其流程为code → .plt → .got.plt → .plt→.got.plt→target function

在这里插入图片描述

结合上图可更好的理解整个过程。

接下来要hook函数就很简单了,只需要将运行中的got.plt表中对应的地址覆盖为我们自己的函数地址,当调用时,自然就调用到我们自己的函数了。

2.2.2 got/plt hook 实现

接下来我们来实现一下hook的过程

首先,将测试代码改造一下,改造后测试代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdbool>
int mygetpid(){
    return 12306;
}

int main(){

    while(true){
        printf("the pid is %d\n",getpid());
        sleep(1);
    }
    return 0;
}

改造后的代码,每隔一段时间就会打印一下pid,然后我们还新增了一个函数,用于到时候替换用,我们再用readelf -a 来查看一下编译成的执行文件的elf情况如下:

首先是.got.plt表

接下来是.symtab,.symtab是c程序的符号表,其中包含有各种程序的符号,其内容如下

我们可以看到,getpid函数和我们自己编写的mygetpid函数在这个表中都可以看到,由于getpid是外部引用函数,其地址是使用时动态加载,所以此时为0,接下来的内容就很明确了,我们只需要把.got.plt表中,位置为0X601018的值,覆写成我们自己的mygetpid函数的地址,就可以hook住getpid函数了。

那么我们应该怎么才能修改程序运行时候的内存地址呢,我们都知道,linux秉承的是万物皆文件的原则,程序在运行时候,其内存会映射为一个/proc/$pid/mem文件,修改这个文件,等于修改程序内存(其实这样说不够严谨,差不多是这个意思)。

于是我们可以编写个程序用来修改程序运行时候的内存,代码如下

vim inject.c
--------------------------------------------
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char* argv[]) {
    int pid = atoi(argv[1]);
    unsigned long offset = 0x601018;
    unsigned long myfunctionaddr = 0x4005b6;
    char filename[32];
    snprintf(filename, sizeof(filename),"/proc/%d/mem",pid);
    int fd = open(filename, O_RDWR|O_SYNC);
    lseek(fd,offset,SEEK_SET);
    write(fd,&myfunctionaddr, sizeof(unsigned long));
    return 0;
}
-----------------------------------
gcc -o inect inject.c

现在先启动目标程序

然后启动我们的注入程序

再来看我们程序的输出

已经变成了我们预先设定的12306,那么如果我们还想调用原来的getpid函数该怎么做,其实也很简答,只需在目标程序中定义一个函数指针,然后将原pid函数的地址写入指针就可以了。

2.2.3 PRELOAD HOOK

Linux系统中,ELF格式的导入表只存储了符号(包括导出的全局对象、全局变量以及全局函数)名,因此在进程加载器初始化外部符号时,从模块链表头开始按模块搜索直到遇到该符号名。

这一特性对动态调用函数和静态调用函数的调用都会产生影响,也就是说,如果我们构造一个和目标函数一模一样的函数,并写上自己的代码,让它优先于目标函数加载,那不就可以对函数进行hook了,而Linux提供了一个环境变量LD_PRELOAD,这个环境变量可以让程序在启动时,优先加载我们指定的so,接下来,让我们测试一下,首先编写一个自己的so

vim preloadso.c
-------------------------------------------
#include <stdio.h>
int getpid(void){
        printf("i hook the getpid function!\n");
        return 12306;
}
--------------------------------------------
gcc -o preloadso.so -shared -fPIC preloadso.c 

接下来使用LD_PRELOAD环境变量运行之前编写的gotTest,结果如下

可见我们已经成功的替换了getpid函数,那么如果我们想要调用原始的getpid函数该怎么做呢,我们可以用dlsym函数配合RTLD_NEXT变量获取原始getpid函数的地址,然后剩下的就类似于上面got表hook的操作了。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值