目录
函数地址替换是最简单的hook方式,在开发的时候发现C开发的程序和C++开发的程序hook还是有差别的。C开发的程序函数几乎是可以直接使用readelf查看,因此在查找函数偏移量的时候相对简单一些,但是如果是C++开发无法直接使用readelf命令查看函数地址,因此需要动态跟踪函数地址,找到函数偏移量然后针对该函数地址进行hook。
我们以getpid函数为例,该函数是linux系统中的系统函数。
该程序是我们希望被hook的目标程序。代码如下【main.cc】:
- #include
- #include
- #include
- // 替换getpid函数
- int mygetpid(){
- return 520;
- }
-
- int main(){
-
- while(true){
- printf("the pid is %d\n",getpid());
- sleep(1);
- }
- return 0;
- }
【makefile】文件如下:
- main:clean
- g++ main.cc -o main
- clean:
- rm -rf main
执行命令如下:
readelf -S main

我们首先需要关注的是节头中的这几项



其中0x404000是got.plt表在程序中的偏移位置,另外两个表以此类推,接下来,我们具体看看got.plt表中有哪些内容:

0x404028是getpid系统函数地址。
方法一:查看自定义函数
readelf -a main | grep -i mygetpid
![]()
0x401142该函数地址是我们需要将该函数(mygetpid)注入到目标函数(getpid)。
方法二:查看自定义函数
nm main | grep getpid
方法三:查看自定义函数
通常应用发布是release版本,也许没有调试信息,因此需要安装调试信息包,才能查到对应函数地址。
以浏览器为例:
org.chromium-dbgsym_**_amd64.deb
通过dpkg -L org.chromium-dbgsym找到调试信息文件。
最后通过nm 调试信息文件获取函数地址。
我们现在知道了两个函数地址mygetpid函数以及getpid函数。getpid函数是正常调用系统函数地址。
我们可以通过修改程序的内存地址来控制函数的流程。在linux中内存修改可以通过修改/proc/{$pid}/mem函数来完成。所以我们替换掉对应偏移量的地址。
编写注入程序如下:
- vim inject.c
- --------------------------------------------
- #include
- #include
- #include
- int main(int argc,char* argv[]) {
- int pid = atoi(argv[1]);
- unsigned long offset = 0x404028;
- unsigned long myfunctionaddr = 0x401142;
- 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;
- }
- -----------------------------------
- gcc -o inect inject.c
我们来看看演示效果
操作步骤如下:
1. 运行目标程序。
2. 运行注入程序(需要带参数,参数为目标程序的pid)
3. 目标程序的函数地址被修改。

在演示中我们可以看到他的getpid函数被强制修改为mygetpid进行调用。
我们可以看到在上面的例子中,我们修改了内存内容指向了程序的另外一个函数,但是该函数还是目标程序中已经存在的函数,那么我们需要如何修改为自己的函数呢?
Linux系统中,ELF格式的导入表只存储了符号(包括导出的全局对象、全局变量以及全局函数)名,因此在进程加载器初始化外部符号时,从模块链表头开始按模块搜索直到遇到该符号名。
这样就可以利用该特性,复写目标程序的函数(函数名一致),也就是我们构造一个和被注入函数一模一样的函数,让他优先加载我们的函数,这样也就可以达到hook的效果。
linux提供了一个环境变量LD_PRELOAD,这个环境变量可以让目标程序启动时,优先加载我们指定的so。
- vim preloadso.c
- -------------------------------------------
- #include
- int getpid(void){
- printf("i hook the getpid function!\n");
- return 420;
- }
- --------------------------------------------
- gcc -o preloadso.so -shared -fPIC preloadso.c
接下来使用LD_PRELOAD环境变量运行之前编写的目标程序main,结果如下:
LD_PRELOAD=./preloadso.so ./main

我们可以看到目标程序main的getpid函数已经被so文件中的getpid覆盖。
如果我们准备注入的函数名称与带注入的名称不一致怎么办?我们可以通过修改跳转地址的方式来进行hook,PRELOAD+内存修改。修改逻辑如下:
1) 获取程序基地址。
通过查找内存maps可以获取程序的动态收地址。
cat /proc/{进程id}/maps
2) 通过偏移量获取函数地址。(偏移量是不会改变的,除非修改代码)
函数运行地址=程序基地址+偏移量
3) 修改函数地址的指令,转为跳转指令。
- // offset为函数地址在软件中的偏移量,nm命令可以获取。
- unsigned long ShowFunAddress = baseAddress + offset;
- // 打开指定函数偏移的位置。
- lseek(fd,ShowFunAddress,SEEK_SET);
- // 开始注入代码
- // 伪造jmp指令
- unsigned char jmpcmd[14] = {0}; // JMP远跳只支持32位程序 64位程序地址占8个字节 寻址有问题
- jmpcmd[0] = 0xFF; // 当JMP指令为 FF 25 00 00 00 00时,会取下面的8个字节作为跳转地址
- jmpcmd[1] = 0x25; // 因此可以使用14个字节作为指令 (FF 25 00 00 00 00) + dstaddr
- jmpcmd[2] = 0x00;
- jmpcmd[3] = 0x00;
- jmpcmd[4] = 0x00;
- jmpcmd[5] = 0x00;
- unsigned long dstaddr = (unsigned long)myfunctionaddr;
- memcpy(&jmpcmd[6], &dstaddr, sizeof(dstaddr));
- // 注入代码
- write(fd,&jmpcmd, sizeof(jmpcmd));
note:
1) 伪造的函数需要和原函数一模一样,否则会出现奔溃。
2) 伪造函数的逻辑需要和原函数无冲突,否则会导致后续程序运行奔溃。
4) 修改内存地址指令跳转到指定函数地址。
如果为了节约指令地址可以使用该命令
48 B8 $offset * 8 FF E0
翻译成汇编
- mov 地址 %rax
- jmp %rax
1 ) 编写目标程序
代码如下:main.cc
- #include
- #include
- #include
- #include
-
- void showText(){
-
- printf("this is main function!\r\n");
- }
-
- int main(){
- while(true){
- showText();
- sleep(1);
- }
- return 0;
- }
makefile文件如下
- main: clean
- g++ main.cc -o main
- clean:
- rm -rf main
2)编写so动态库
common.cc
- #include "common.h"
- int char_to_hex(char x){
- switch (x)
- {
- case '0':
- return 0;
- case '1':
- return 1;
- break;
- case '2':
- return 2;
- case '3':
- return 3;
- case '4':
- return 4;
- case '5':
- return 5;
- case '6':
- return 6;
- case '7':
- return 7;
- case '8':
- return 8;
- case '9':
- return 9;
- case 'a':
- case 'A':
- return 10;
- case 'b':
- case 'B':
- return 11;
- case 'c':
- case 'C':
- return 12;
- case 'd':
- case 'D':
- return 13;
- case 'e':
- case 'E':
- return 14;
- case 'f':
- case 'F':
- return 15;
- default:
- return 0;
- break;
- }
- return 0;
- }
-
- unsigned long str_to_hex(string str){
- unsigned long result = 0;
- for(int i = str.length()-1 ; i>= 0 ;i--){
- unsigned long base = 1;
- if(str.at(i) == '0') continue;
- for(int j = 0 ; j < str.length()- 1 - i ; j++){
- base *= 16;
- }
- result += char_to_hex(str.at(i))*base;
- }
- return result;
- }
- // 获取程序基地址
- unsigned long getBaseAddress(string name){
-
- string cmd = "cat /proc/" + to_string(getpid()) + "/maps | grep " + name +" | head -n 1 | awk -F '-' '{print $1}'";
- // cout <
- int res = -1;
- int ret = -1;
- FILE * fp;
- if ((fp = popen(cmd.c_str(), "r") ) == NULL)
- {
- printf("Popen Error!\n");
- return 0;
- }
- char pRetMsg[512] ={0};
- while(fgets(pRetMsg, 512, fp) != NULL){
- // printf("safdad:%p ,%s,%d \r\n",&pRetMsg,pRetMsg,getpid()); //print all info
- if(NULL != strstr(pRetMsg, HDD_MOUNT_DIR)){
- printf("got df info:\n");
- ret = 0;
- break;
- }
- }
- if ((res = pclose(fp)) == -1){
- printf("close popenerror!\n");
- return 0;
- }
- pRetMsg[strlen(pRetMsg)-1] = '\0';
- // cout <
- return str_to_hex(string(pRetMsg));
- }
common.h
- #ifndef _COMMON_H
- #define _COMMON_H
- #include
- #include
-
- #include
- #include
-
- #include
- #include
- using namespace std;
- #define HDD_MOUNT_DIR "yubo.wang"
-
- // char转数字
- int char_to_hex(char x);
- // 16进制字符串转码
- unsigned long str_to_hex(string str);
- // 获取程序基地址
- unsigned long getBaseAddress(string name);
-
- #endif
preloadso.c
- #include
- #include
- #include
-
- #include
- #include
- #include
- #include
- #include
- #include "common.h"
- void showHook(){
- printf("this is hook function!\n");
- }
-
-
- int hookFunction(){
-
- char strProcessPath[1024] = {0};
- if(readlink("/proc/self/exe", strProcessPath,1023) <=0){
- return -1;
- }
- char *strProcessName = strrchr(strProcessPath, '/');
- if(strcmp(strProcessName,"/main")){return 0;}
- unsigned long baseAddress = getBaseAddress("main");
-
- unsigned long offset =0x401132-0x400000;
- // 打开浏览器证书弹窗的地址为 基地址+偏移量。
- unsigned long ShowFunAddress = baseAddress + offset;
- printf(" baseAddress:%llu, ShowFunAddress:%llu \r\n",baseAddress,ShowFunAddress);
- // 获取将要注入的函数地址。
- unsigned long myfunctionaddr = (unsigned long)&showHook;
- printf(" baseAddress:%llu, ShowFunAddress:%llu ,myfunctionaddr:%llu \r\n",baseAddress,ShowFunAddress,myfunctionaddr);
- // 打开程序内存。
- char filename[32];
- snprintf(filename, sizeof(filename),"/proc/%d/mem",getpid());
- int fd = 0;
- if((fd = open(filename, O_RDWR|O_SYNC)) ==-1)
- {
- return -1;
- }
- // 打开指定函数偏移的位置。
- lseek(fd,ShowFunAddress,SEEK_SET);
- // 开始注入代码
- // 伪造jmp指令
- unsigned char jmpcmd[14] = {0}; // JMP远跳只支持32位程序 64位程序地址占8个字节 寻址有问题
- jmpcmd[0] = 0xFF; // 当JMP指令为 FF 25 00 00 00 00时,会取下面的8个字节作为跳转地址
- jmpcmd[1] = 0x25; // 因此可以使用14个字节作为指令 (FF 25 00 00 00 00) + dstaddr
- jmpcmd[2] = 0x00;
- jmpcmd[3] = 0x00;
- jmpcmd[4] = 0x00;
- jmpcmd[5] = 0x00;
- unsigned long dstaddr = (unsigned long)myfunctionaddr;
- memcpy(&jmpcmd[6], &dstaddr, sizeof(dstaddr));
- // 注入代码
- write(fd,&jmpcmd, sizeof(jmpcmd));
- int res = -1;
- if ((res = close(fd)) == -1){
- printf("close popenerror!\n");
- return 0;
- }
- return 0;
- }
-
-
- int a=hookFunction();
makefile文件
- preloadso.so:clean
- g++ -o preloadso.so -shared -fPIC preloadso.c common.cc common.h
- clean:
- rm -rf preloadso.so
-
-
- install:
- rm -rf ../main/preloadso.so
- cp preloadso.so ../main/
3)运行注入
将编译好的so文件拷贝到main函数下运行。
LD_PRELOAD=./preloadso.so ./main

可以看到原始的代码已经不执行了,他执行到了我们hook的代码中。
上面已经说了如何获取函数地址,都是静态获取,c语言可以通过函数名称来获取函数地址。
方法如下:
- int (*f)(int ,int);
- f = (int(*)(int,int))dlsym(RTLD_NEXT,"add");
可以在程序运行的时候通过名称直接获取。
但是对于c++其导出的函数名不能直接通过dlsym函数获取。因此还是需要使用静态的方式获取函数偏移量来定位函数地址。如果哪位大神看到该文章有办法获取的话,能不能告知一下小弟。