Skip to content

hooks 说明

复制本地路径 | 在线编辑

分成三个部分:

  1. hooks 是什么,有什么作用?
  2. hooks 有什么危险,有什么被利用的方式?
  3. 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.somalloc

如何确保真正的函数也执行

很多时候,我们还是希望真正的函数也执行,比如下面的语句,我们只是想统计 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”这一能力。

Comments