Linux应⽤层hook技术总结与实例
⽬录
1.前⾔
Linux hook技术⼀直是linux技术爱好者们研究的⼀个热点问题,的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来实现的,⼀般流程为:
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;
}
-----------------------------------
linux下的sleep函数
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的操作了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。