协程的hook操作

1. hook 原理

hook机制本质上是一种函数的劫持技术,比如我们通常需要调用malloc函数来进行内存分配,那么能不能我们自己封装一个同名、同入参和同返回值的malloc函数来替代系统的malloc函数,在我们自己封装的malloc函数中实现一些特定的功能,而且也能回调系统的malloc,这就是hook机制。
系统提供给我们的dlopen、dlsym族函数可以用来操作动态链接库,比如我们要hook系统调用函数read,我们可以使用dlsym族函数获取hook前函数的地址,这样就可以在自己实现的read中回调原函数,并加上一些额外的逻辑,并且在运行是会调用我们的版本了。
通过hook机制,libco可以达到用户无感的情况下把同步的代码替换为异步,这也是腾讯工程师写出libco的目的。libco库里面提供了socket族函数的hook,使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造,号称单机可以达到千万连接。

1.1 函数解析

#include <dlfcn.h>  
void * dlopen( const char * pathname, int mode ); 
  函数描述: 
  在dlopen的()函数以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程。使用dlclose()来卸载打开的库。 
  mode:分为这两种 
  RTLD_LAZY 暂缓决定,等有需要时再解出符号 
  RTLD_NOW 立即决定,返回前解除所有未决定的符号。 
  RTLD_LOCAL 
  RTLD_GLOBAL 允许导出符号 
  RTLD_GROUP 
  RTLD_WORLD 


  返回值: 
  打开错误返回NULL 
  成功,返回库引用 
  编译时候要加入 -ldl (指定dl库) 
根据动态链接库操作句柄与符号,返回符号对应的地址。
包含头文件:
#include <dlfcn.h>
函数定义:
void*dlsym(void* handle,const char* symbol)
函数描述:
dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。
handle是由 dlopen打开 动态链接库后返回的 指针,symbol就是要求获取的函数或 全局变量的名称。

dlsym主要针对系统库的调用,dlopen主要针对第三方的库。且两者使用都需要包含头文件#include <dlfcn.h>。

2. hook实现

hook的实现有两种方法,第一种是使用环境变量LD_PRELOAD,第二种是直接代码入侵。

2.1 使用环境变量LD_PRELOAD

系统为我们提供了 dlopen,dlsym工具,用于运行时加载动态库。可执行文件在运行时可以加载不同的动态库,这就为hook系统函数提供了基础。下面用一个小小的例子来说明如何利用dlsym工具hook系统函数。假设现在我们需要统计程序中malloc的调用次数,但是不能修改原有程序。最简单的思路类似于Java中动态代理Proxy的做法,先找到系统的malloc函数,然后将其替换为自定义的函数,在自定义函数中增加调用次数,并回调系统的原有malloc函数。例如我们要统计以下main.c中调用malloc的次数:

// main.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int index;
    for (index=0; index < 10; index++) {
        char* p = (char*)malloc(4);
        printf("index:%d, p[0]=%d\n", index, *p);
        free(p);
    }
    printf("hello world\n");
    return 0;
}

为了能让自己的malloc函数回调系统的malloc函数,我们需要利用dlsym获取系统的malloc函数。

// myhook.c
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>

int count = 0;

void *malloc(size_t size) {
    void *(*myMalloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
    count++;
    // 这里使用第一个字节为count数来表示程序进入了这个malloc函数
    char* data = (char*)myMalloc(size);
    *data = count;
    return (void*)data;
}

RTLD_NEXT允许从调用方链接映射列表中的下一个关联目标文件获取符号,即找到glibc.so中的malloc函数。

下一步则是要让可执行文件main找到自定义的malloc函数。

在linux操作系统的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。loader在进行动态链接的时候,会将有相同符号名的符号覆盖成LD_PRELOAD指定的so文件中的符号。换句话说,可以用我们自己的so库中的函数替换原来库里有的函数,从而达到hook的目的。

编译

$ gcc -o main main.c
$ gcc -o libmymalloc.so -fPIC -shared -D_GNU_SOURCE myhook.c -ldl

运行

$ LD_PRELOAD=./libmymalloc.so ./main

index:0, p[0]=1
index:1, p[0]=3
index:2, p[0]=4
index:3, p[0]=5
index:4, p[0]=6
index:5, p[0]=7
index:6, p[0]=8
index:7, p[0]=9
index:8, p[0]=10
index:9, p[0]=11
hello world

例2:hook read

// hookread.cpp
#include <dlfcn.h>
#include <unistd.h>
 
#include <iostream>
 
typedef ssize_t (*read_pfn_t)(int fildes, void *buf, size_t nbyte);
 
static read_pfn_t g_sys_read_func = (read_pfn_t)dlsym(RTLD_NEXT,"read");
 
ssize_t read( int fd, void *buf, size_t nbyte ){
    std::cout << "进入 hook read\n";
    return g_sys_read_func(fd, buf, nbyte);
}
 
void co_enable_hook_sys(){
    std::cout << "可 hook\n";
}
 
// main.cpp
#include <bits/stdc++.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
 
using namespace std;
 
int main(){
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    char buffer[10000];
    
    int res = read(fd, buffer ,10000);
    return 0;
}

编译运行

g++ -o main main.cpp
g++ -o hookread.so -fPIC -shared -D_GNU_SOURCE hookread.cpp -ldl
LD_PRELOAD=./hookread.so ./main
 
输出:
进入 hook read

2.2 源码入侵

不使用LD_PRELOAD的Hook。我们看看libco库是如何实现hook的呢,它的makefile中并没有LD_PRELOAD相关的信息。其秘密在于co_hook_sys_call.cpp,其将 co_enable_hook_sys()的定义在该cpp文件内,这样就把该文件的所有函数都导出了(即导出符号表)。

//co_hook_sys_call.cpp
ssize_t read(int fd, void* buf, size_t bytes) 
{
...
}

...

void co_enable_hook_sys() //这函数必须在这里,否则本文件会被忽略!!!
{
    stCoRoutine_t *co = GetCurrThreadCo();
    if( co )
    {
        co->cEnableSysHook = 1;
    }
}

我们仍然以上面malloc的例子来说明:

// main.c
#include <stdio.h>
#include <stdlib.h>
#include "myhook.h"

int main() {
    int index = 0;
    for (index=0; index < 10; index++) {
         printf("index:%d, hook res:%d\n", index, enable_hook());
         void *p = malloc(4);
         free(p);
    }
    printf("hello world\n");
    return 0;
}
// myhook.h
int enable_hook(); 
// myhook.c
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>
#include "myhook.h"

static int count = 0;

void *malloc(size_t size) {
    void *(*myMalloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
    count++;
    return myMalloc(size);
}

int enable_hook() {
    return count;
}

编译运行

$ gcc -o libmymalloc.so -fPIC -shared -D_GNU_SOURCE myhook.c -ldl
$ gcc main.c  -L . -Wl,-rpath . -lmymalloc -o main
$ ./main
index:0, hook res:0
index:1, hook res:2
index:2, hook res:3
index:3, hook res:4
index:4, hook res:5
index:5, hook res:6
index:6, hook res:7
index:7, hook res:8
index:8, hook res:9
index:9, hook res:10
hello world

例2:

//hookread.cpp
#include <dlfcn.h>
#include <unistd.h>
 
#include <iostream>
 
#include "hookread.h"
 
typedef ssize_t (*read_pfn_t)(int fildes, void *buf, size_t nbyte);
 
static read_pfn_t g_sys_read_func = (read_pfn_t)dlsym(RTLD_NEXT,"read");
 
ssize_t read( int fd, void *buf, size_t nbyte ){
    std::cout << "进入 hook read\n";
    return g_sys_read_func(fd, buf, nbyte);
}
 
void co_enable_hook_sys(){
    std::cout << "可 hook\n";
}
 
// hookread.h
void co_enable_hook_sys();
 
 
// main.cpp
#include <bits/stdc++.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "hookread.h"
#include <unistd.h>
 
using namespace std;
 
int main(){
    co_enable_hook_sys();
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    char buffer[10000];
    
    int res = read(fd, buffer ,10000);
    return 0;
}
 

编译运行:

g++ hookread.cpp -o hookread.i -E
g++ hookread.i -o hookread.s -S
g++ hookread.s -o hookread.o -c
g++ main.cpp -ldl hookread.o
./a.out
 
输出:
可 hook
进入 hook read

这种方式算是对源代码进行了侵入,必须调用特定的函数(即本例中的enable_hook()),才能将hook的函数导出,并链接到现有的可执行文件的内存空间中。

参考:
https://blog.csdn.net/MOU_IT/article/details/115050472
https://blog.csdn.net/weixin_43705457/article/details/106895038

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值