在上一篇文章中,我们解释了操作系统的虚拟内存的概念和实现原理,以及基于此的native hook的实现原理,接下来我们要讲的就是native hook的一种实现:plt/got hook。
构建工程
arduino
// lib.h
#ifndef LIB_H
#define LIB_H
void start();
#endif
/********************************************************/
// lib.c
#define _GNU_SOURCE
#include<stdio.h>
#include "lib.h"
int global_value = 17;
int global_value2 = 0xffeebbaa;
void sayWords(){
printf("hello owrld from C \n");
printf("number: %d %d \n",global_value,global_value2);
}
void start(){
sayWords();
}
我们把lib.c的逻辑视作程序的主要逻辑,把它编译成动态链接库:
vbnet
gcc lib.c -shared -fPIC -o lib_test.so // 输出lib_test.so
然后创建main.c,作为可执行程序
arduino
#define _GNU_SOURCE
#include<stdio.h>
#include "lib.h"
int main(){
start();
return 0;
}
然后编译链接main文件:
css
gcc main.c -L. -l_test -o main.o
-l_test会自动补全为lib_test,此时查看main依赖的so库
bash
$ ldd main.o
linux-vdso.so.1 (0x00007ffe853a7000)
lib_test.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fce61a00000)
/lib64/ld-linux-x86-64.so.2 (0x00007fce61d7f000)
如果显示lib_test.so not found ,需要把该so放入特定的目录下(/usr/lib),或者把当前目录设置到LD_LIBRARY_PATH中,建议直接设置环境变量
bash
$ open ~/.bashrc // 打开bashrc文件
// 文件中添加存放lib_test.so的路径(绝对路径)
export LD_LIBRARY_PATH="/home/your_path:$LD_LIBRARY_PATH"
$ source ~/.bashrc
之后,在命令窗口中重新查看mian的so库依赖,然后应该会正常打印lib_test.so的路径了。然后可以执行一下./main.o程序,看是否符合预期。
至此,我们构建了一个正常的可执行程序,它依赖了一个我们自己的lib_test.so库还有一些其他的系统so库,
如何hook
此时我们想要hook一下lib_test.so库中使用的printf函数,根据前一篇文章梳理的原理和过程,我们需要找到该so库的重定位表,这个表中指向了需要重定位的外部导入符号的地址,里面就有printf的项。我们使用readelf查看一下
sql
$ readelf -r lib_test.so
Relocation section '.rela.plt' at offset 0x5e0 contains 3 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000004018 0000000200000007 R_X86_64_JUMP_SLOT 0000000000000000 puts@GLIBC_2.2.5 + 0
0000000000004020 0000000300000007 R_X86_64_JUMP_SLOT 0000000000000000 printf@GLIBC_2.2.5 + 0
0000000000004028 0000000800000007 R_X86_64_JUMP_SLOT 0000000000001159 sayWords + 0
可以看到printf的重定位之后的真实地址记录在偏移为0x4020的地址处,也就是GOT(全局偏移表)中,lib_test.so中所有调用了printf的函数最终都会走向0x4020处。
所以我们要把自己编写的myPrintf函数的首地址填入到该偏移地址处,但是这个记录的偏移只是在ELF文件中的偏移地址,并不是运行时的真实地址,需要等ELF文件被映射到虚拟内存空间中某个地址上之后,才知道它的真实地址。所以我们还要找到lib_test.so在运行时的基地址(载入的起始地址),然后加上我们找到的偏移地址才是真实地址。
查找运行时库的运行地址我们依然使用C基础库中的dl_iterate_phdr函数,同时需要考虑对运行时的某段地址进行写入操作时 会碰到权限问题,修改内存的读写权限需要使用mprotect这个系统调用,这个我我们都直接参照网络上的用法。
我们补全main.c的hook代码:
arduino
#define _GNU_SOURCE
#include<stdio.h>
#include "lib.h"
#include <stdarg.h>
#include <link.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <inttypes.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#define PAGE_MASK (~(PAGE_SIZE - 1))
#endif
#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE)
// 我们用来替换printf的hook函数
int myPrintf(const char *format, ...){
puts("hook printf start =============================");
va_list args;
va_start(args, format);
int result = printf(format,args);
va_end(args);
puts("hook printf end =============================");
return result;
}
static int
callback(struct dl_phdr_info *info, size_t size, void *data)
{
char *type;
int p_type;
printf("Name: \"%s\" (%d segments)\n", info->dlpi_name,
info->dlpi_phnum);
if(strstr(info->dlpi_name,"lib_test") != NULL){ // 找到lib_test.so这个运行库
// 先打印一下
printf("this so %s %14p \n",info->dlpi_name,info->dlpi_addr);
// info->dlpi_addr就是程序的基地址,加上偏移0x4020就是运行时的地址
uintptr_t addr = info->dlpi_addr+0x4020;
// 设置写入权限
mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
// 把addr地址转换为一个void二级指针,并把addr地址上的内容修改为myPrintf函数地址
*(void **)addr = myPrintf;
__builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}
//printf("base_addr: %14p \n",info->dlpi_addr);
return 0;
}
void hookPrintf(){
// 遍历共享对象列表,从中获取lib_test.so的地址
dl_iterate_phdr(callback, NULL);
}
int main(){
// 程序运行到main时,程序依赖的so都已经加载到内存中了,
//因此我们可以开始查找lib_test.so的运行时内存基地址
hookPrintf();
start();
return 0;
}
通过上面的逻辑代码,我们可以实现对位于基地址+偏移地址的函数进行修改.
然后重新编译链接main文件:
css
gcc main.c -L. -l_test -o main.o
接着运行main.o程序,会发现lib_test.so中使用的函数printf被替换成了myPrintf.
hook导出表函数
我们前面讲的hook lib_test.so中使用的printf函数,实际上定义在libc.so中,因此对于lib_test.so而言,printf函数是一个导入函数。那么定义在lib_test.so内部的函数是否也可以被hook呢?答案是显然的。
而且其基本原理与hook导入函数是一致的,我们在lib_test.so中定义了sayWords函数,因此我们尝试hook它。
首先我们需要知道该函数所在的重定位地址的偏移,通过readelf 读取重定位表可知:
sql
$ readelf -r lib_test.so
Relocation section '.rela.plt' at offset 0x5e0 contains 3 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000004018 0000000200000007 R_X86_64_JUMP_SLOT 0000000000000000 puts@GLIBC_2.2.5 + 0
0000000000004020 0000000300000007 R_X86_64_JUMP_SLOT 0000000000000000 printf@GLIBC_2.2.5 + 0
0000000000004028 0000000800000007 R_X86_64_JUMP_SLOT 0000000000001159 sayWords + 0
sayWords在偏移地址为0x4028处,那么同样的,我们找到lib_test.so在运行时的基地址,然后加上这个偏移地址就得到了GOT中记录sayWords函数地址的表项处,然后把我们预先写好的替代函数放到该地址处即可替换掉对sayWords函数的调用。
arduino
#define _GNU_SOURCE
#include<stdio.h>
#include "lib.h"
#include <stdarg.h>
#include <link.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <inttypes.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#define PAGE_MASK (~(PAGE_SIZE - 1))
#endif
#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE)
void mySayWords(){
printf("hook say Words ======================= \n");
}
static int
callback(struct dl_phdr_info *info, size_t size, void *data)
{
char *type;
int p_type;
printf("Name: \"%s\" (%d segments)\n", info->dlpi_name,
info->dlpi_phnum);
if(strstr(info->dlpi_name,"lib_test") != NULL){
printf("this is we want %s %14p \n",info->dlpi_name,info->dlpi_addr);
uintptr_t addr = info->dlpi_addr+0x4028;
mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
void **temp = (void **)addr; // 使用void二级指针来修改似乎更加方便
*temp = mySayWords;
__builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}
//printf("base_addr: %14p \n",info->dlpi_addr);
return 0;
}
void hookPrintf(){
dl_iterate_phdr(callback, NULL);
}
int main(){
hookPrintf();
start();
return 0;
}
在编译运行后过后,我们就可以看到hook后的结果了。