文章目录
HOOK 概述
原理:修改符号指向
我们可以通过 HOOK
系统的socket
函数族来实现无需修改代码的异步化改造。
链接
- 编译器可以将我们编写的代码编译成为目标代码,而链接器则负责将多个目标代码收集起来并组合成为一个单一的文件。
- 链接过程可以执行于
编译
时(compile time),也可以执行于加载
时(load time),甚至可以执行于运行
时(run time)。 - 执行于编译时的链接被称为静态链接,而执行于加载时和运行时被称为动态链接。
运行时动态链接
可以通过共享库的方式来让引用程序在下次运行时执行不同的代码,Unix-like 系统提供了 dlopen
,dlsym
系列函数来供程序在运行时操作外部的动态链接库,从而获取动态链接库中的函数或者功能调用。
例如:微信后台的 c/c++ 协程库 libco。 libco 使用动态链接 Hook 系统函数,最大的特点是将系统中的关于网络操作的阻塞函数全部进行相应的非侵入式改造,对于 read
,write
等阻塞函数,libco 均定义了自己的版本,然后通过 LD_PRELOAD
进行运行时地解析,从而来达到阻塞时自动让出协程,并在 IO 事件发生时唤醒协程的目的。
linux上的常见HOOK方式
修改函数指针
通过函数指针来指向不同的函数地址控制执行流,通常运用于编程中。
比如说glic提供
__malloc_hook
,__realloc_hook
,__free_hook
可以实现hook
自定义malloc/free
函数
用户态动态库拦截
LD_PRELOAD
可以影响程序的运行时链接,允许用户在运行前优先加载指定库。
可以通过这个指定我们预定义的库,指定库中符号为程序动态链接所需库的同名符号,这样就能实现覆盖,使得程序只访问我们指定符号。
一般情况下,动态库加载加载顺序为:LD_PRELOAD
>LD_LIBRARY_PATH
>/etc/ld.so.cache
>/lib
>/usr/lib
getpid
测试程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("my uid is %d\n", getpid());
return 0;
}
HOOK程序:
#include <unistd.h>
#include <sys/types.h>
pid_t getpid()
{
return 0;
}
命令:
gcc demo1.c -o demo1
gcc -fPIC -shared hook1.c -o hook1.so
./demo1
LD_PRELOAD=./hook1.so ./demo1
运行:
malloc 第一版
测试程序:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *a = (char*)malloc(sizeof(char) * 8);
char *b = (char*)malloc(sizeof(char) * 16);
char *c = (char*)malloc(sizeof(char) * 32);
char *d = (char*)malloc(sizeof(char) * 64);
free(d);
free(c);
free(b);
free(a);
return 0;
}
HOOK程序:
#include <stdio.h>
#include <stdlib.h>
static size_t allocSize = 0;
void *malloc(size_t size)
{
void *res = __libc_malloc(size);
allocSize += *(int*)((char*)res - sizeof(size_t)) & ~((0x1) | (0x2) | (0x4));
printf("malloc allocSize: %ld\n", allocSize);
return res;
}
void free(void *ptr)
{
__libc_free(ptr);
allocSize -= *(int*)((char*)ptr - sizeof(size_t)) & ~((0x1) | (0x2) | (0x4));
printf("free allocSize: %ld\n", allocSize);
}
命令:
gcc demo2.c -o demo2
gcc -fPIC -shared hook2.c -o hook2.so -w
./demo2
LD_PRELOAD=./hook2.so ./demo2
执行:
malloc 第二版
这边问题在于printf
会调用malloc
,产生了递归调用,最终core
掉。
HOOK程序:
#include <stdio.h>
#include <stdlib.h>
static size_t allocSize = 0;
static int enableMallocHook = 1;
static int enableFreeHook = 1;
void *malloc(size_t size)
{
if (enableMallocHook)
{
enableMallocHook = 0;
void *res = __libc_malloc(size);
allocSize += *(int*)((char*)res - sizeof(size_t)) & ~((0x1) | (0x2) | (0x4));
printf("malloc allocSize: %ld\n", allocSize);
enableMallocHook = 1;
return res;
}
else
{
return __libc_malloc(size);
}
}
void free(void *ptr)
{
if (enableMallocHook)
{
enableMallocHook = 0;
__libc_free(ptr);
allocSize -= *(int*)((char*)ptr - sizeof(size_t)) & ~((0x1) | (0x2) | (0x4));
printf("free allocSize: %ld\n", allocSize);
enableMallocHook = 1;
}
else
{
__libc_free(ptr);
}
}
命令:
gcc demo2.c -o demo2
gcc -fPIC -shared hook2.c -o hook2.so -w
./demo2
LD_PRELOAD=./hook2.so ./demo2
执行:
试一下ls
命令:
直接挂掉了,这次问题出现在了我们没处理free空指针
。
malloc/free通过指针获取到空间大小
我们可以看到allocSize += *(int*)((char*)res - sizeof(size_t)) & ~((0x1) | (0x2) | (0x4));
,并且
- 申请8 计算得到32(8+16 后16字节对齐)
- 申请16 计算得到32(8+16 后16字节对齐)
- 申请32 计算得到48(8+32 后16字节对齐)
- 申请64 计算得到80(8+64 后16字节对齐)
malloc_chunk
的基础结构:
mchunk_prev_size
:该字段记录物理相邻的前一个chunk的大小(低地址chunk)。如果前一个chunk处于空闲,则该字段记录前一个chunk大小;如果前一个chunk已经被使用,则该字段空间可以被前一个chunk的用户数据空间复用。mchunk_size
:该字段是chunk的大小。该字段的低三个比特位对 chunk 的大小没有影响,所以被复用为标志位。- A:
NON_MAIN_ARENA
的缩写,指所用arena是不是main arena的flag - M:
IS_MMAPPED
的缩写,指所用chunk是不是经由mmap分配所得 - P:
PREV_INUSE
的缩写,指当前chunk的前一个chunk是不是allocated chunk,是的话这个bit为1,否则为0
- A:
fd
和bk
:当chunk空闲的时候,会放置到bins上双向链表管理。fd 指向下一个(非物理相邻)空闲的 chunk。bk 指向上一个(非物理相邻)空闲的 chunk。由于只有chunk空闲的时候,才会放置到bins上进行空闲管理,所以fd和bk占用的是用户数据区域user datafd_nextsize
和bk_nextsize
:用于管理large块的时候的空闲chunk双向链表的管理。一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历,也是复用用户数据区域。large chunk的空间肯定装的下。
最小的空间:mchunk_prev_size
字段 + mchunk_size
字段 + fd
字段 + bk
字段 所需要的空间。64位需要16字节,32位需要8字节。chunk
对齐规则:按照2*SIZE_SZ
进行对齐,64位系统是16字节,32位系统是8字节。chunk
的size:(chunk的mchunk_size
字段空间 + 用户数据区域(最小16))& 对齐字节(2*SIZE_SZ
)。
搞个程序gdb看一下:
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
char *p = malloc(100);
*p = 'A'; // 0x41
char *h = p - 8;
printf("%d\n", malloc_usable_size(p)); // 104
printf("%d\n", *((int *)h) & ~((0x1) | (0x2) | (0x4))); // 112
printf("%d", *((int *)h)); // 113
}
运行:
gcc testmalloc.c -o testmalloc -g
我们可以看到malloc
大小100的空间,指针h
指向了header
,为113,去掉低3位为112。
100+8后16字节对齐是112。
malloc 第三版
demo3.c和demo2.c一模一样
HOOK程序:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
static void* (*sysMalloc)(size_t size) = NULL;
static void (*sysFree)(void *ptr) = NULL;
static int allocSize = 0;
static int enableMallocHook = 1;
static int enableFreeHook = 1;
void init()
{
sysMalloc = dlsym(RTLD_NEXT, "malloc");
sysFree = dlsym(RTLD_NEXT, "free");
}
void *malloc(size_t size)
{
if (sysMalloc)
{
init();
}
if (enableMallocHook)
{
enableMallocHook = 0;
void *res = sysMalloc(size);
allocSize += malloc_usable_size(res);
printf("malloc allocSize: %ld\n", allocSize);
enableMallocHook = 1;
return res;
}
else
{
return sysMalloc(size);
}
}
void free(void *ptr)
{
if (enableMallocHook)
{
enableMallocHook = 0;
sysFree(ptr);
allocSize -= malloc_usable_size(ptr);
printf("free allocSize: %ld\n", allocSize);
enableMallocHook = 1;
}
else
{
sysFree(ptr);
}
}
命令:
gcc demo3.c -o demo3
gcc -fPIC -shared hook3.c -o hook3.so -ldl -w
./demo3
LD_PRELOAD=./hook3.so ./demo3
执行:
再试一下ls
命令:
strncmp
测试程序:
#include <stdio.h>
#include <string.h>
int main()
{
int res = strncmp("test", "aaa", 4);
printf("%d\n", res);
return 0;
}
HOOK程序:
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <stdlib.h>
static int (*sysStrncmp)(const char *__s1, const char *__s2, size_t __n) = NULL;
void init()
{
sysStrncmp = dlsym(RTLD_NEXT, "strncmp");
}
extern int strncmp(const char *__s1, const char *__s2, size_t __n)
{
if (!sysStrncmp)
{
init();
}
printf("参数:%s, %s, %ld\n", __s1, __s2, __n);
return sysStrncmp(__s1, __s2, __n);
}
命令:
gcc demo4.c -o demo4
gcc -fPIC -shared hook4.c -o hook4.so -ldl -w
./demo4
LD_PRELOAD=./hook4.so ./demo4
LD_PRELOAD=./hook4.so ls
运行:
自己的程序没成功,但是ls
却成功了。
自己写的程序里面没有strncmp
的动态符号
汇编看一下,发现已经被编译器优化变成纯汇编了,并没有函数调用:
objdump -d demo4
0000000000401122 <main>:
401122: 55 push %rbp
401123: 48 89 e5 mov %rsp,%rbp
401126: 48 83 ec 10 sub $0x10,%rsp
40112a: c7 45 fc 13 00 00 00 movl $0x13,-0x4(%rbp)
401131: 8b 45 fc mov -0x4(%rbp),%eax
401134: 89 c6 mov %eax,%esi
401136: bf 04 20 40 00 mov $0x402004,%edi
40113b: b8 00 00 00 00 mov $0x0,%eax
401140: e8 eb fe ff ff callq 401030 <printf@plt>
401145: b8 00 00 00 00 mov $0x0,%eax
40114a: c9 leaveq
40114b: c3 retq
40114c: 0f 1f 40 00 nopl 0x0(%rax)
内核态系统调用拦截
Linux内核中所有的系统调用都是放在一个叫做sys_call_table
的内核数组中,数组的值就表示这个系统调用服务程序的入口地址。
sys_call_table
在实模式下叫中断向量表
在保护模式中IDT,又称中断描述符表
当用户态发起一个系统调用时,会通过80软中断进入到syscall_hander
,进而进入全局的系统调用表sys_call_table
去查找具体的系统调用,那么如果我们将这个数组中的地址改成我们自己的程序地址,就可以实现系统调用劫持。
问题:
- sys_call_table的符号没有导出,不能直接获取。(grep sys_call_table /boot/ -r)
- sys_call_table所在的内存页是只读属性的,无法直接进行修改。(清除CR0寄存器的WP控制位)
- Linux大概从4.8开始加入了保护机制,每次开机sys_call_table的地址都会变化
堆栈式文件系统
Linux通过vfs虚拟文件系统来统一抽象具体的磁盘文件系统,从上到下的IO栈形成了一个堆栈式。
内核中采用了很多c语言形式的面向对象,也就是函数指针的形式,例如read是vfs提供用户的接口,具体底下调用的是ext2的read操作。我们只要实现VFS提供的各种接口,就可以实现一个堆栈式文件系统。
协程的HOOK
使用用户态动态库拦截,dlsym
,在不改造业务代码的情况下,使用协程,如libgo中代码:
unsigned int sleep(unsigned int seconds)
{
// 如果没初始化则初始化hook
if (!sleep_f) initHook();
// 获取当前调度任务
Task* tk = Processer::GetCurrentTask();
DebugPrint(dbg_hook, "task(%s) hook sleep(seconds=%u). %s coroutine.",
tk->DebugInfo(), seconds,
Processer::IsCoroutine() ? "In" : "Not in");
// 如果没有任务,则调用真正的sleep
if (!tk)
return sleep_f(seconds);
// 挂起当前协程, 并在指定时间后自动唤醒
Processer::Suspend(std::chrono::seconds(seconds));
Processer::StaticCoYield();
return 0;
}
其中initHook
中:
sleep_f = (sleep_t)dlsym(RTLD_NEXT, "sleep");
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » 协程6 --- HOOK
发表评论 取消回复