ELF文件的native hook:实现

在上一篇文章中,我们解释了操作系统的虚拟内存的概念和实现原理,以及基于此的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后的结果了。

相关推荐
Ni-Guvara4 分钟前
函数对象笔记
c++·算法
似霰8 分钟前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
栈老师不回家17 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
芊寻(嵌入式)18 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
獨枭20 分钟前
C++ 项目中使用 .dll 和 .def 文件的操作指南
c++
霁月风23 分钟前
设计模式——观察者模式
c++·观察者模式·设计模式
前端啊龙23 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
橘色的喵24 分钟前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier
一颗松鼠27 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js