0x01 概述
inline hook是一项改变函数调用控制流的技术。与传统的PLT(或IAT)表劫持,库打桩等技术差异,inline hook会修改函数本身(通常是头部的数个字节)来进行函数的hook。inline hook较PLT劫持更复杂,但是不会受到PLT劫持技术的诸多限制。例如PLT劫持/库打桩只能控制ELF导入的外部动态库函数,对于库内函数调用和本身步伐内部函数调用无法hook。而inline hook几乎能适应所有情况。
本文通过运行时库打桩引入,重要介绍Linux X64平台下inline hook的原理和实现,windows平台的相关实现实在异曲同工。
0x02 前排说明
这应该是高考前发的末了一篇文章了,重要为了给高考攒点品德
因为精力有限,时间仓促,文章着重报告实现原理和边界细节,给出的代码片断重要是为了概念演示而不能直接运行。假如文章内容有疏漏,还请您多多指教。
0x03 情境导入
X同砚硕招简历填了一个自己的github项目,项目重要是通过hook来给某闭源的软件扩展功能和修bug。硕招面试时,导师对他的项目非常感兴趣,问他hook的具体实现。X同砚懵了,他用的就是一个现成的库啊,应该关注的重点岂非不是怎样靠逆向来修复软件bug吗。
一阵沉默...X同砚只想着:hook是什么?能吃吗?好吃吗?回家后,X同砚打开了电脑,搜索了自己用的库的介绍,发现它用了一个叫做inline hook的神奇玩意。用某搜索软件搜一下,发现第一页全部都是复读机(一个"转载"一个),而且都是描述x86下实现的太古文章了。于是X同砚决定自(qiu)己(zhu)动(wang)手(you)研究。
0x04 大道至简——从库打桩开始
(限于篇幅缘故原由,这里不介绍库打桩的背景知识了,因为库打桩和inline hook也没有关系。)X同砚十分庆幸自己曾经读过CSAPP,里面有一章就是介绍库打桩技术的,看起来也和hook相关。他马上着手实验,搞出来一个测试环境。
目的就是劫持rand函数,让他输出自己的荣幸数字

末了通过LD_PRELOAD=./a.so ./a.out 成功劫持了a.out里面对rand()的调用。

!上图为32位体系的示意图,和64位体系差异,仅供类比理解

很轻松,成功了。然而最关键的问题是,hook的精髓不是直接暴力修改函数,而是修改后还能调用原函数。他都把原函数前14字节覆盖了,还怎么调用?
聪明的他想到了:直接把前14个字节内包含的指令复制出来,然后在末了面跳回原函数,不就做成了一个original (下文称为shadow function)
使命完成,全文完。
0x06 知识回敲——为什么我的inline hook挂掉了
假如文章只在0x05就完了,那么和网上的绝大多数介绍就基本没啥区别了。"假如hook这么简单,为什么还有那么多人用现成的库呢?"X同砚嘀咕。
实际上,很多文章/很多实现很粗暴的库都先做了一个假定,x86函数的开头都是由
mov %edi,%edi
push %ebp
mov %esp,%ebp
构成的,恰好足够放一个32位相对地点jmp。
而在64位平台下一般是
push %rbp
mov %rbp,%rsp
一共四个字节,还需要再改一个函数指令才能放得下一个32位相对地点jmp。
在这种情况下,直接把开头的指令复制到shadow function依然能执行。因为开头的指令是rip地点无关,且不会引用函数体。
感性地理解,开头的指令无论放到那里,放到哪个步伐,他都执行了同一件事情。
但是实际情况真的这么好吗?众所周知,push rbp,rbp=rsp实在是带有frame pointer的函数的经典入口操作,gcc只需要开到O2及以上就会打开-fomit-frame-pointer,函数开头的这几个指令就被删除了。
没有了这4字节的固定指令,意味着我们必须得处置惩罚函数的前5字节指令来放置自己的jmp。假如你觉得问题依然和上面一样简单的话你就大错特错了。
以此函数为例说明

0x07 以子之矛,陷子之盾——inline hook的防范
上面已经讨论了inline hook能遇到的绝大多数情况,然而inline hook不是万能的,仍然可以进行反hook。一种直接想到的办法就是检测函数的开头数个字节,假如被修改那么就被hook了,但是这种方法容易被patch(因为有明显的特性),再者每次调用都要检测,非常贫苦。
下面介绍一种比较巧妙的方法,来导致hook后无法调用original从而反hook这牵扯到inline hook的第二种边界情况——回跳(第一种即上文描述的rip相关指令)。
我们已经明白了,inlinehook的原理就是覆盖目的函数的前几个字节。但是我们无法包管前几个字节没有被函数内部引用。
比如下面的例子
LOC_ENTRY:
xor %rax,%rax; #48 31 c0,用来初始化循环计数,并且使第二条指令处在jmp HOOKED指令的中心
LOC_ANTIHOOK:
add $1,%rax; #回跳,若被hook此处会被覆盖从而引发段错误
LOC_CONFUSE_CF: jmp LOC_ANTIHOOK_LP; #控制流混淆,骗过hook库对于回跳的分析,认为此处函数竣事
ret;
LOC_ANTIHOOK_LP: cmp $2,%rax;
jne LOC_ANTIHOOK; #回跳
LOC_REAL: #真实函数体
mov $1,%rax;
ret;
由于函数体引用了LOC_ANTIHOOK处的指令,而我们的inline hook又恰好覆盖了这里,以是调用original会导致指令错误,步伐终止。


</strong>
后:(可以看到ANTIHOOK被覆盖了)
<strong><font size="5"><font size="3"> |