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后的结果了。

相关推荐
喝拿铁写前端10 分钟前
智能系统的冰山结构
前端
Zhichao_9737 分钟前
【UE5 C++课程系列笔记】33——商业化Json读写
c++·ue5
无奈何杨1 小时前
扣子coze的AI工作流搭建技术,开源项目FlowGram流程搭建引擎
前端
云边有个稻草人1 小时前
【C++】第八节—string类(上)——详解+代码示例
开发语言·c++·迭代器·string类·语法糖auto和范围for·string类的常用接口·operator[]
ElasticPDF-新国产PDF编辑器1 小时前
Angular 项目 PDF 批注插件库在线版 API 示例教程
前端·pdf·angular.js
6武71 小时前
Vue 数据传递流程图指南
前端·javascript·vue.js
惊鸿一博1 小时前
c++ &&(通用引用)和&(左值引用)区别
开发语言·c++
jakeswang2 小时前
查询条件与查询数据的ajax拼装
前端·ajax
samuel9182 小时前
axios取消重复请求
前端·javascript·vue.js