hook情境导入X同学硕招简历填了自己的项目

2024-01-18 01:05:04

0x01 概述

Hook是一种改变函数调用控制流程的技术。 与传统的PLT(或IAT)表劫持、库打桩等技术不同,hook会修改函数本身(通常是头部的几个字节)来hook函数。 Hook比PLT劫持更复杂,但不会受到PLT劫持技术的很多限制。 例如PLT劫持/库打桩只能控制ELF导入的外部动态库函数,而无法hook库内的函数调用或自身程序内的函数调用。 而且 Hooks 几乎可以适应所有情况。

本文通过运行时库打桩的方式来介绍,主要介绍Linux X64平台下hooks的原理和实现。 平台的相关实现其实都是类似的。

0x02 前排指令

这应该是我高考前发的最后一篇文章了。 主要是为了高考攒点品格。

由于精力有限,时间仓促,文章重点讨论实现原理和边界细节。 给出的代码片段主要用于概念演示,不能直接运行。 文章如有疏漏之处,还请各位不吝赐教。

0x03 情况导入

同学硕士面试的时候,导师对他的项目很感兴趣,向他询问了hook的具体实现。 同学

一阵沉默……X同学只是想:什么是钩子? 可以吃吗? 好吃吗? 回到家后,X同学打开电脑,搜索自己使用的库的介绍,发现它使用了一个叫做hook的神奇东西。 我用某搜索软件搜索了一下,发现第一页都是重复的(一个“转载”),而且都是描述x86下实现的古文。 于是X同学决定自己去研究一下。

0x04 从简约之路——从图书馆打桩开始

(限于篇幅,这里就不介绍库打桩的背景知识了,因为库打桩与hook无关。)X同学很幸运,他读过CSAPP。 里面有一章介绍库打桩技术。 挂钩相关。 他立即开始尝试并创建了一个测试环境。

目标是劫持rand函数,让它输出自己的幸运数字

hook_hook_hook

按照书上的说明,写了一个假的rand函数,并编译成.so

hook_hook_hook

最后通过=./a.so ./a.out成功劫持了a.out中rand()的调用。

hook_hook_hook

“这是一个钩子吗?我喜欢它。” X同学认为自己已经掌握了核心技术。 然而,当他尝试用同样的方法重写a.out中的其他函数时,却失败了。

经过查(zi)和读(xun)资料(lao),他得到了一个非常通俗的原理描述。 最初的a.out是动态链接的产物,一些需要使用的函数并没有真正编译。 只有当程序被加载时(/RELRO)或者这个函数被实际使用时(lazy),动态链接器才会找到这个函数并提供给a.out

这就是为什么rand可以窃取和改变支柱,但main不能。 因为 rand 是由外部动态链接库提供的,所以您可以诱使动态链接器加载您自己的实现。 main函数编译成a.out,不涉及动态链接,所以不能用这种方式hook

0x05 果子狸换王子——实现挂钩

为了避免第二天面试的尴尬,X同学只能正面继续回答这个问题。 他突然想到,为什么不能直接修改被调用的函数呢? 翻阅使用说明书后,他发现其实只要将函数头改为 jmp -> 为自己的函数即可。他直接手写汇编,将函数头覆盖为

jmp *0(%rip) 跳转到64位地址....(指令ff 25 00 00 00 00)

.dq跳转地址

(&,"\xff\x25\x00\x00\x00\x00...",14) 总共花费了 14 个字节来完成任务。 当他兴奋地直接覆盖它时,他犯了段错误。

hook_hook_hook

!上图是32位系统的示意图,与64位系统不同,仅供类比。

hook_hook_hook

原来他修改了一个只能读取和执行的文本段,所以出现了段错误。

可以修改指定内存页的保护属性,先允许写入,写入后禁止。

hook_hook_hook

这很容易而且很成功。 但最关键的问题是,hook的本质并不是直接暴力修改函数,而是修改后仍然调用原来的函数。 他已经覆盖了原函数的前14个字节,那他怎么还能调用呢?

他很聪明,想到了:直接复制前14个字节包含的指令,然后在最后跳回原来的函数,然后创建一个(以下简称)

任务完成,全文完成。

0x06 知识击退——为什么我的hook挂了?

如果文章只以0x05结尾的话,那就和网上大多数介绍基本一样了。 “如果 如此简单,为什么还有那么多人使用现成的库?” X同学嘀咕道。

事实上,许多文章/许多实现粗糙的库首先假设 x86 函数的开头是

移动%edi,%edi

推%ebp

移动%esp,%ebp

它由刚好足以容纳 32 位相对地址 jmp 的空间组成。

在64位平台上,通常是

推送%rbp

移动 %rbp,%rsp

总共有四个字节,需要更改另一条功能指令以适应32位相对地址jmp。

在这种情况下,直接复制第一条指令仍然可以执行。 因为开头的指令与rip地址无关,也不引用函数体。

感性地理解,无论初始指令放在哪里,或者放在哪个程序中,它都会执行相同的事情。

但实际情况真有那么好吗? 众所周知,push rbp,rbp=rsp实际上是带frame的函数的经典入口操作。 gcc只需要打开到O2及以上就可以打开-fomit-frame-,而函数开头的这些指令都被删除了。

缺少这些 4 字节固定指令意味着我们必须处理函数的前 5 字节指令来放置我们自己的 jmp。 如果你认为问题仍然像上面那么简单,那你就完全错了。

以这个函数为例

hook_hook_hook

即使push rbp仍然有固定的结构,rbp=rsp,但第三条指令是rip地址相关指令(rip相对寻址),不能被复制执行。

下面的描述可能有点难以理解,所以建议结合文末的图片来吃。

首先你需要考虑 rip 相关指令,

例如lea 0x4(%rip),%rax就是rip相关指令。 因为他的操作数的计算使用的是rip寄存器的值。 也就是说,在 和 处执行这条指令的结果是不同的。 如果还是按照上面的方法暴力复制的话,显然会挂掉。

lea 0x4(%rip),%rax === rax=0x4+当前指令后的rip =+4+指令长度

lea 0x4(%rip),%rax === rax=0x4+当前指令后的rip =+4+指令长度

如果你愿意阅读手册,你会发现有很多指令可以使用rip作为基地址寄存器的寻址...

不过没关系,因为大多数指令的寻址偏移都是32位,给我们留下了足够的空间来修复它

例如,将上述命令移至 时,可改为

lea(0x4+-)(%rip),%rax ===当前指令后rax=0x4+rip-(-)=+4+指令长度

要修改这种偏移量,需要使用一些反汇编库。 这里推荐ZyDis。 这个库可以直接给出指令中操作数对应的偏移量的具体位置。 修改起来非常方便。

再次考虑 rip 相关指令的特殊情况

如果它以跳转指令开始,情况就会变得复杂,就像许多函数内跳转指令一样。 也就是说,jmp的相对偏移量是一个8位的数字。例如

跳跃

……

雷特

这里的jmp指令无法使用上面的方法进行修补。 因为偏移空间只有8位,所以没有办法一次性从它跳回来。 有些钩子库遇到这种情况,选择直接拒绝钩子。

这是处理此类指令的方法。 虽然这么短的jmp指令不能修改偏移量,但是我们可以打破这个限制。

现在直接从头修改

jmp +9 //这里假设对应,忽略指令长度

..... //其他说明

jmp test+n //跳回原来的函数执行

第一行的jmp+9显然需要修改,他本来是要跳转到的。我们考虑在后面追加一个far jmp来跳转到

jmp test+9 //跳回到原来的地址(这里)

然后修改jmp+9的偏移量,使其跳转到+e,短jmp的补丁就完成了。

hook_hook_hook

0x07 使用孩子的矛和陷阱的盾——防止钩子

上面已经讨论了钩子可能遇到的大多数情况。 不过,钩子也不是万能的,反钩子还是可以进行的。 立即想到的方法是检测函数的前几个字节。 如果修改了,就会被hook。 不过,这种方法很容易被打补丁(因为它有明显的特点)。 而且每次调用都要检测,很麻烦。

下面介绍一种比较巧妙的方法,让hook无法被调用,从而反转hook。 这就涉及到hook-的第二种边界情况(第一种就是上面介绍的rip相关指令)。

我们已经了解了,其原理就是覆盖目标函数的前几个字节。 但我们不能保证前几个字节不会被函数内部引用。

例如下面的例子

:

异或%rax,%rax; #48 31 c0,用于初始化循环计数并使第二条指令位于jmp指令中间

:

添加 $1,%rax; #反弹回来,如果被hook了,就会被覆盖,导致段错误。

:jmp; #控制流混乱,欺骗hook库对的分析,以为函数到这里就结束了

ret;

: cmp $2,%rax;

jne ;#

:#实函数体

移动 $1,%rax;

ret;

由于函数体引用了 at 指令,而我们的钩子恰好覆盖了它,因此调用将导致指令错误,程序将终止。

hook_hook_hook

hook_hook_hook

hook_hook_hook

这是挂接前和挂接后的对比

hook_hook_hook

之后:(可以看到它被覆盖了)

hook_hook_hook

虽然有些钩子库会进行反弹检测,但由于是纯粹的裸露反汇编而不是控制流分析,因此上述情况会导致钩子库误认为函数已经结束,而忽略后面的反弹。

测试一下,你会发现调用后错误。 成功反钩。

hook实验源码在附件中,需要Linux平台。 在 1.1.0 下测试。

0x08 简单实现&常见问题解答

为了让读者更容易理解这篇文章,我写了一个简单的实现(当然,也涵盖了文章中描述的所有情况)。 实现稍长,已放在附件中。

hook_hook_hook

hook_hook_hook

问:为什么需要辅助跳远?

A:Linux平台下主文件和动态库文件的加载地址相差2GB以上,因此无法直接使用32位相对跳转实现hook。 可以使用32位jmp跳转到任意64位地址(需要6字节),也可以先使用32位相对jmp(5字节)跳转到64位jmp。 一般来说,覆盖的字节越少,hook遇到复杂情况的概率就越小。本实现采用的是后面的5字节实现

(如果极度追求性能,也可以使用jmp+64位地址,共14字节)

问:好像不止这种钩子

答:是的。 例如,还有一种方法是分析整个文件,对目标函数进行外部引用,然后修改指令跳转到目标函数。 然而,这种方法过于依赖静态分析,无法处理调用。 本文介绍的方法是最流行的,成功率也最高。

问:大多数情况下,我们不会遇到文中所写的RIP相关说明的特殊情况。

A:为了方便hook,有些DLL会在函数头里面放一条空指令,方便替换(不知道机制,所以猜测)。 比如上面的 mov %edi,%edi 可以用一个短的 jmp 代替,而不影响原来的函数逻辑。

不过,现在钩子更多地用于动态调试辅助和二进制热修改。 实际遇到的很多函数并不具备如此优越的钩子使用条件。 例如,libc 中的某些函数以 lea 开头,并且必须相对于 32 位进行修补。

0x09 结论

正如王安石在《宝蟾山行》中所说:“天下之雄奇奇异之景,往往处于险远之地,很少有人到访,所以只有有志之士才能到达。”

要做知识,就需要知道如何进行深入研究,知道正在发生什么、为什么发生。 我想在这里分享一些技术探索过程,希望对大家有所鼓励。

下载附件请见左下角原文。

--官方论坛

--推荐给朋友

标签: hook
首页
欧意官网
欧意安卓下载
okx国际官网