天天看点

PLT hook笔记

1. hook技术概述   

     hook技术是一种拦截用户函数调用的技术。通过hook技术可以实现统计用户对某些函数的调用次数,对函数注入新的功能的目标。在Linux平台,Hook技术可以分成用户和内核两个层面,每个类比中都存在不同的hook技术。本文主要介绍针对动态链接技术的PLT hook。

2. 代码实例

    首先我们先用一个实例来向大家展示一下PLT hook的效果。代码的功能是验证用户在命令行输入的密码,hook的目标是strcmp函数,通过将strcmp函数的返回值置为0,达到无论用户输入任何密码,即使是错误的,都返回验证通过的提示。 

    先编写passwd.c。该代码的作用是调用strcmp函数并判断用户输入的密码是否正确,并打印相应的提示。文件中只有一个函数就是check_is_authenticated。

PLT hook笔记

  其次编写我们的main.c 该函数实现了将要替换strcmp函数的my_strcmp,和使hook生效的hook函数。代码的具体细节

1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <string.h>
 5 #include <inttypes.h>
 6 #include <execinfo.h>
 7 #include <sys/user.h>
 8 #include <sys/mman.h>
 9 #include "passwd.h"
10 
11 #define PAGE_SHIFT  12
12 #define PAGE_SIZE  (_AC(1,UL) << PAGE_SHIFT)
13 #define PAGE_MASK  (~(PAGE_SIZE-1))
14 
15 #define __AC(X,Y) (X##Y)
16 #define _AC(X,Y) __AC(X,Y)
17 
18 #define PAGE_START(addr) ((addr) & PAGE_MASK)
19 #define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
20 
21 int my_strcmp(const char *s1, const char *s2) 
22 {
23     return 0;
24 }
25 
26 uintptr_t get_base_addr(char *libname) 
27 {
28     FILE *fp;
29     char line[1024];
30     //char base_addr[1024];
31     uintptr_t base_addr = 0;
32 
33     if (NULL == (fp = fopen("/proc/self/maps", "r"))) {
34         perror("open err");
35         return -1;
36     }
37 
38     while (NULL != fgets(line, sizeof(line), fp)) {
39         if (NULL != strstr(line, libname)) {
40             sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr);
41             printf("line2:%s, base_addr:%"PRIxPTR"\n", line, base_addr);
42             break;
43         } 
44     }
45     fclose(fp);
46 
47     return base_addr;
48 }
49 
50 void hook() {
51     uintptr_t base_addr;
52     uintptr_t addr;
53     //1. get the base addr of libpasswd.so
54     base_addr = get_base_addr("libpasswd");
55     if (0 == base_addr) return;
56     addr = base_addr + 0x201020; 
57     //2. add the write permisson
58     mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ|PROT_WRITE);
59     //3. replace our hook func my_strcmp
60     *(void **)addr = my_strcmp; 
61     //4. clear the cache
62     __builtin___clear_cache((void *)PAGE_START(addr),
63             (void *)PAGE_END(addr));
64 }
65 
66 int main() 
67 {
68     hook();
69     check_is_authenticated("abcd");
70     return 0;
71 }      

  接下来我们将passwd.c编译成动态库libpasswd.so,main.c调用libpasswd.so中的check_is_authenticated函数。我们是用下列gcc命令将passwd.c编译为动态库,其中-fpic是生成位置无关代码,-shared表示我们生成的是一个动态库,不需要main函数的参与。

PLT hook笔记

  在将libpasswd.so放置到/usr/lib/目录中之后,使用以下命令编译main.c

PLT hook笔记

  运行之后我们 可以看到验证结果始终是正确的,说明我们对于libpasswd.so中的strcmp函数的Hook已经生效。

PLT hook笔记

怎么样,是不是很神奇呀!下面我们就来看看plt hook到底是怎么实现的。

3. PLT hook原理

  说了这么久,PLT hook到底是怎么实现的呢?到底什么是PLT呢?下面我就带大家了解以下到底什么是PLT。

    在了解PLT之前我们需要先了解下什么是共享库和动态链接技术。静态链接技术,是以一种将多个可链接目标文件链接为一个独立的可执行文件的过程。在链接的过程中,连接器会将静态库中的函数完整的复制到可执行文件的文本段中。在一个运行较多进程的系统中,这种链接方式对于内存消耗是不可小觑的。如下图所示是两种不同的连接方式生成的可执行文件的大小,可以看到二者相差极大。为了解决这个问题,共享库诞生了,共享库是一个目标模块或者说目标文件,在运行或者加载时,可以加载到内存的任意位置,并和内存中的程序链接起来,这个过程是一个叫做动态链接器的组件完成的。在完成链接的过程中,链接器仅仅复制一些重定位和符号表信息到可执行文件中,大大减小了可执行文件的大小。

PLT hook笔记

   PLT(Procedure Linkage Table)全程过程链接表,主要用于协助程序完成延迟加载的功能,假设程序调用一个动态库中的函数,因为动态库可以被加载到内存中的任意位置,因此我们无法去预测这个函数的运行时地址。正常的做法是为该函数调用生成一个重定位记录,然后动态链接器在程序加载的时候去解析它。但是这并不符合位置无关代码的做法,因为需要链接器修改调用模块的文本段。GNU使用延迟加载的技术去解决这个问题,把对函数地址的解析延迟到了对于函数的实际调用的时刻。使用这种技术,在第一次函数调用时的开销较大,但是在之后的调用中只会花费一条指令和一个间接的内存引用。

   延迟调用的完成需要PLT和GOT(Global Offset Table)全局偏移量表协作完成。如果一个目标模块调用任何在共享库中的函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,PLT是代码段的一部分。下面我们来看看PLT和GOT表中的内容:

  PLT。PLT表中的每一项是一个16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。PLT[1]是系统启动函数的条目。从第三项开始是用户调用的动态库函数的条目。GOT。GOT中每个表项是8字节的地址类型数据。和PLT类似,GOT[0]和GOT[1]也是特殊条目,包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每一个条目对应一个被调用的函数,其地址在需要时进行解析。每个条目都有一个对应的PLT条目。下面我们还是以一个实例来解释一下延迟调用的过程。

  下列代码是一个动态库libpasswd.so中调用strcmp函数的过程,从下列代码,我们可以看到对于strcmp函数的调用,实际上调用的是PLT中的strcmp条目。strcmp条目实际上是三条汇编代码,让我们这些汇编语句都做了什么。在560行,使用jmpq指令跳转到GOT中strcmp对应的条目,GOT初始时都指向它对应的PLT条目的第二条指令,在这里也就是"pushq $0x0"。在把strcmp的ID(0x0)压入栈中后,使用jmpq指令跳转到PLT[0], 前面说过PLT[0]存储的是动态链接器相关的条目,先使用pushq命令将GOT[1]中存储的动态连接器的一个参数压栈("pushq 0x200ab2(%rip)")。值得注意的是这里的0x200ab2是一个常量,这里用到了位置无关代码(PIC)的相关知识,不再赘述。然后使用jmpq指令,通过GOT[2]中存储的动态链接器的地址间接跳转到动态链接器中,这里要注意代码中"*"的意义,"*"表示获取地址对应的内存,类似指针变量的解引用运算符。在跳转到动态链接器之后,连接器通过我们刚刚压入栈中的两个值(被调用函数的ID, 和GOT[1])来确定strcmp函数的运行时地址,并用这个地址重写strcmp对应的GOT条目,在将控制传递给strcmp函数。

PLT hook笔记
PLT hook笔记

  以上就是第一次调用strcmp函数的流程,可以看出开销还是不小的。在后续的调用中,首先还是跳转到strcmp的plt条目中,然后执行jmpq *0x200aaa(%rip)指令,不同的是这是strcmp对应的GOT条目已经被写入了strcmp的运行时地址,所以这条jmp指令直接将程序的执行跳转到strcmp函数中。那么是不是只要在strcmp对应的GOT条目中写入我们自己的函数的地址,就可以将控制跳转到自己实现的函数中了嘛。本着这样的思路我们来看看开始时的代码。

4. 代码分析

代码分成两部分,一个是被hook的动态库libpasswd.so和调用被hook动态库的文件main.c。这里要强调的是我们拦截的是动态库中的函数。下面我来讲解一下核心的hook函数。

  1.  第一步我们要获取libpasswd在进程中的首地址,时刻记住我们拦截的是动态库中的函数,将来要替换的GOT[strcmp]也属于libpasswd。
  2.  第二步就是获取libpasswd中调用strcmp对应的GOT条目的地址,为什么是addr = base_addr + 0x201020呢?我们回到上文的libpasswd的plt段,可以看到strcmp的plt条目的第一条指令已经写出了相应的GOT条目的地址就是0x201020。又因为我们拦截的动态库的strcmp函数,所以必须加上libpasswd在main中的首地址。
PLT hook笔记

    3.  因为我们要写入目标进程的数据段,所以必须给相应的页增加写权限,这里使用mprotect函数来调整相应页的权限。

         4.  第四步就是将相应的GOT条目的内容替换为我们的函数的地址。这一句值得说道说道,*(void **)addr = my_strcmp;大家觉得这句和 (void *)addr = my_strcmp有什么区别呢?毕竟对*(void **) = (void *)看上去也是正确的啊!起初我也有这样的疑惑,但是仔细想想,其实并不是这样,我们要记住GOT表中的每一项都存储的是一个地址,同时addr存储的是相应的GOT条目的地址,因此我们必须先将addr强转为一个指向指针类型的指针。再使用星号解引用这样获取的就是GOT条目中存储的内容。这里需要理解的是指针类型其实和int char double是一样的也是一种数据类型。将上面的代码转换为以下形式也许更好理解:*( (void *) *addr) = my_strcmp。

1 void hook() {
 2     uintptr_t base_addr;
 3     uintptr_t addr;
 4     //1. get the base addr of libpasswd.so
 5     base_addr = get_base_addr("libpasswd");
 6     if (0 == base_addr) return;
 7     addr = base_addr + 0x201020; 
 8     //2. add the write permisson
 9     mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ|PROT_WRITE);
10     //3. replace our hook func my_strcmp
11     *(void **)addr = my_strcmp; 
12     //4. clear the cache
13     __builtin___clear_cache((void *)PAGE_START(addr),
14             (void *)PAGE_END(addr));
15 }      

        5. 最后一步我们需要清除硬件缓存,因为GOT表项的内容很可能已经绕过内存被缓存到了硬件缓存中,可能会导致hook失败。

  以上就是我了解到的PLT hook的全部内容,这里我们还只是hook自己的进程中调用的动态库的函数。对于其他进程的函数因为涉及到读取/proc/map文件和修改页权限,因此必须要root权限才能够运行。具体如何实现在下一篇文章中,我们再进行探讨。

参考

1. https://www.polarxiong.com/archives/x64%E4%B8%8BPIC%E7%9A%84%E6%96%B0%E5%AF%BB%E5%9D%80%E6%96%B9%E5%BC%8F-RIP%E7%9B%B8%E5%AF%B9%E5%AF%BB%E5%9D%80.html 关于RIP相对调用的精彩讲解

2. https://juejin.im/post/5aea75a2f265da0ba266d0a6#heading-13 代码的出处

3. 深入理解计算机系统 本文关于链接知识的来源