hooks 说明
分成三个部分:
- hooks 是什么,有什么作用?
- hooks 有什么危险,有什么被利用的方式?
- hooks 删除之后,原来正常(非恶意)使用 hooks 的应该改成说明?
hooks 介绍
如下所示,以前的时候 free 是这样的逻辑,其中 free_hook 是全局变量。
free()
├─ if (__free_hook != NULL)
│ └─ call __free_hook(ptr)
└─ else
└─ _int_free()
学术化讲,它的作用是在 不改 glibc、不重编译程序的前提下,你能在 malloc/free 发生时插入一点自己的逻辑。”
举例一:最朴素、最合理的例子:内存分配统计
你在 2003 年维护一个大型服务器程序:
- 不能重启
- 不能重新编译
- 怀疑内存泄漏
你想知道:到底是谁在 malloc?你可以写一个这样的函数:
void *my_malloc_hook(size_t size, const void *caller)
{
printf("[malloc] size=%zu, caller=%p\n", size, caller);
__malloc_hook = old_hook;
void *p = malloc(size);
__malloc_hook = my_malloc_hook;
return p;
}
然后把正在运行的程序 attach gdb,并且设置 __malloc_hook(没错,hooks 最大的优势就是在 gdb 中可以替换这个全局变量),此时就能进行检测了。
举例二:复现诊断
如果程序出现问题,或者想要统计一些指标。同样地可以用 hooks,我们需要修改程序,把对应的 hooks 修改成我们写的函数即可:
#include <malloc.h>
static void *(*old_malloc_hook)(size_t, const void *);
static void *my_malloc_hook(size_t size, const void *caller)
{
__malloc_hook = old_malloc_hook;
void *p = malloc(size);
__malloc_hook = my_malloc_hook;
return p;
}
int main()
{
old_malloc_hook = __malloc_hook;
__malloc_hook = my_malloc_hook;
void *p = malloc(100);
}
这个也很方便,这样我们就可以跑这个程序,作为诊断手段了。
hooks 危险
要注意,如 free_hook 这些是全局变量。很显然,攻击者如果把某个 hooks 变量修改成危险函数指针,执行对应的 hook 函数时(比如 free_hook 对应 free),那么就执行危险函数了。
这在以前很流行的攻击方式。只要能任意写,那么很多就是利用 hook 来攻击的。
所以在 glibc 2.34 中,hooks 这些全局变量被全部删除,如新版本的 free 就直接 _int_free,而不再进行判断是否用 hooks 了。
free()
└─ __libc_free()
└─ _int_free()
hooks 删除之后
但是这就有一个问题了,hooks 对于正常的行为而言很方便呀,删除了 hooks,那之前比如可以统计 malloc 次数啥的需求,我现在应该怎么做呢?
有很多方法,这里只说一个最主流的:LD_PRELOAD,它的本质时动态链接器在做符号解析时,把自定义的共享库放在搜索顺序最前面。下面具体详谈。
命令行执行
当我们把自己的 malloc 函数编译成 hook.so,然后按照下面的格式执行:
LD_PRELOAD=./hook.so ./a.out
动态链接器的行为变成:
加载顺序:
1. hook.so ← 你指定的
2. a.out 依赖的 so
3. libc.so
4. 其他库
寻找链接库
ELF 的规则是:“第一个定义该符号的共享对象胜出”
举例:
// hook.so
void *malloc(size_t);
// libc.so
void *malloc(size_t);
当程序里调用 malloc(0x100);,此时命中 hook.so 的 malloc
如何确保真正的函数也执行
很多时候,我们还是希望真正的函数也执行,比如下面的语句,我们只是想统计 malloc 的次数:
void *malloc(size_t size)
{
printf("malloc %zu\n", size);
return real_malloc(size);
}
我们可以用上面的操作让动态链接时链接的是我们的 malloc,那 real_malloc 又是怎么找到的?没有全局变量了呀。
关键函数:dlsym,其中 RTLD_NEXT: “从当前共享对象之后,继续找符号”
real_malloc = dlsym(RTLD_NEXT, "malloc");
因此搜索顺序:
跳过 hook.so
→ libc.so
→ …
所以我们可以从 libc.so 中得到 real_malloc。
缺点
但其实有一个问题:如果程序不能重启呢?以前是可以用 attach gdb,然后设置 malloc_hook 全局变量的。
回答:没有等价方案。 hooks 被删除,意味着:
👉 glibc 明确放弃了“不中断进程、运行时热接管 allocator”这一能力。