这里只讨论 ARM64 下的 inlinehook,做一个简单的demo,只是抛砖引玉,有兴趣了解更多细节的可以去查找资料,看开源项目。
ARM64 相比 ARM 的 inlinehook 要麻烦不少,因为有很多指令都没了,且无法直接访问 PC 寄存器。
ARM64处理器下是兼容ARM32指令集的,因此,ARM64处理器上可以运行ARM64,ARM32,Thumb-2(Thumb16+Thumb32)三套指令,但是这里我们只讨论 ARM64 模式。

流程图

inlinehook是实现对一个指定函数的hook,其大致流程如下图:
notion image
首先,需要替换目标函数的头部的指令,将pc的值修改为hook函数的地址,跳转过去执行。
对于被覆盖的指令,我们将其放入hook函数中执行,对应图中的红色部分,这里会遇到非常多的问题,有兴趣的自行查找资料。
hook函数执行完之后,需要将 pc 的值修改为覆盖指令的下一条指令地址,然后跳转过去执行。
在这些过程中,我们需要保证寄存器在执行hook函数前后的值是一样的,执行的覆盖指令也不能修改寄存器,除非先将寄存器的值储存起来。

方案设计

第一步——原程序插桩

设计思路是:首先插桩代码最基本的是要一个跳转功能。ARM64中PC不让直接读写,那我们怎么改变PC呢?通常程序跳转都有两类方法:相对寻址和直接寻址。
相对寻址是有距离限制的,由于我们的hook程序也是一个so,它的位置是不固定的,且目标so的位置也是不固定的,所以这两个so的距离也是不固定的,使用相对寻址很不明智。
直接寻址,ARM64中有BR X??可以直接把程序跳转到X??寄存器中存储的64位地址上。那么,这时候的方案就应该是:
但是,这样会破坏X0中原本的值,所以,我们需要储存一些 X0 的值:
上面的第一个指令就是把X1,X0保存到栈上,这里的X1当然是多余的,纯属是为了满足ARM64上栈要16字节对齐且没有PUSH指令可用的约束。
既然,我们改了栈,那么就需要平衡栈,所以,那最后跳转回原程序时,我们需要将栈的值修改回来:
这里,就与我们设想的流程稍微有点区别,我们需要等hook函数执行完之后,跳转到上面的最后一行指令执行,恢复 X0 寄存器的值。
这里有个需要注意的地方,就是我们的插桩指令至少有4条,占据了16字节,如果函数体很小的话,那么hook就会导致错误。所以这里是一个优化点。

第二步——hook程序

我们在这里需要先保存所有寄存器的值,保存了寄存器的值之后,就可以使用这些寄存器了,保存结构如下:
notion image
对应的指令如下:
由于没有了LDM/STM指令,我们向栈上存大量寄存器只能一对一对的来。
接下来,我们可以执行之前被覆盖的代码了,这里不演示,后面写一个demo具体来看。
然后就是恢复寄存器:
恢复寄存后,我们还需要再跳转到原函数继续执行,执行覆盖指令的最后一行:
这样,一个hook方案就设计好了。

例子

我们尝试对libc.so 中的 fopen 函数进行 inlinehook。
使用,dlopendlsym 找到 fopen 函数的地址:
运行编译后的程序,确认可以正确的获取到地址。如果是32位的so,这个地址可能是一个奇数,说明它是thumb模式。
接下来,我们编写hook函数,需要注意的是编译器对于一般的函数都会生成 prolog 和 epilog 代码,由于我们需要完全控制 hook 函数生成的汇编指令,所以需要使用裸函数,或者直接写汇编。
我们需要覆盖 fopen 函数的头部指令,需要就需要对 .text 段修改,由于 .text 是没有写权限的,所以需要使用 mprotect 函数来获取写权限。
可以查看 /proc/pid/maps 文件,看看是否生效:
可以看到,确实有一个段的权限变成了 rwx。
现在,可以开始覆盖指令了,但是蛋疼的地方出现了,在 ARM32 中,就非常的简单,只需要使用 LDR 就行:
上面的这两条指令,第一条是将下一条指令内容赋值给PC,所以,第二条指令其实不是一个真实的指令,而是hook函数的地址,这样非常简单的实现了hook函数的跳转。
在ARM64中,我们无法操作pc寄存器,只能使用 B 指令来进行跳转:
这里出现一个问题,TARGET_ADDRESS 是一个动态值,我们没办法在编译的时候确定。那该怎么办呢?需要采用一种特殊的写法:
ADDR 是hook函数地址,将这个地址给X0,然后使用 BR X0 进行跳转。注意这个地址占据了8个字节,所以实际上相当于6条指令。
LDR X0, 8 这个需要特别解释一下,这个 ida 的 patch program 生成的偏移是相对于 so 的基址的,不是相对于 pc,想要生成相对于 pc 的指令,需要按下面的写法:
我看了一个开源项目,它是直接使用的 LDR X0, 8,不知道是不是汇编语言写法处理方式不一样。
在IDA中,生成这些指令的汇编代码,然后覆盖掉 fopen 的函数起始地址的6条指令:
这里有一些细节需要注意:
  1. 我们在 sp - 0x60 的位置写入了 X8 与 X0,这是因为我们覆盖的 fopen 指令它会操作sp,它的函数栈大小是 0x50,所以我就将X8 与 X0 放到了 fopen 操作不到的位置,避免覆盖的指令修改栈中的数据导致我们的储存的数据丢失。
  1. 存放 X8 是因为后面,我们生成跳转回来的指令时,gcc 使用到了 X8。
  1. 存放 X0 是因为我们跳转使用的是 X0 寄存器,所以需要储存,后面也要还原。
覆盖指令写好之后,我们就可以写 hook 函数了:
hook 函数我写的比较简单,主要做了3件事:
  • 获取 X0 的值,因为X0是第一个参数,我们可以将 X0 赋值给一个全局变量,然后打印出来看是否hook成功
  • 执行被覆盖的指令,这个很简单,将 fopen 的前6条指令 copy 过来就行了
  • 执行完之后,要跳转回 fopen 继续执行,我们是先计算出了指令的地址,然后使用行内汇编来生成对应的指令
除了使用行内汇编,还可以使用汇编来实现,这个我也不太熟,简单介绍一下。
首先在汇编文件里面定义一个变量:
然后,将这个变量当成标号使用:
在别的C文件里面,使用 extern 就可以直接引用这变量了:
这样,我们就拿到了hook函数的起始地址。有兴趣的可以看下 GitHub 的相关开源项目。

源码

运行程序,输出:
可以看到我们成功的hook了 fopen 函数:
  • data 是 so 文件的前4个字节,就是 .elf
  • x0 与 x1 是参数
 
Loading...
二手的程序员
二手的程序员
路漫漫其修远兮
公告
 
可以占个坑,虽然现在没人说话。
QQ群:
notion image
Telegram群:
notion image
 
我的公众号:
notion image
我的微信:
notion image