一般来说可以分为以下几个模块
需要分配部分内存用于写入指令, 这里需要关注两个函数都是关于内存属性相关的. 1. 如何使内存 可写
2. 如何使内存 可执行
3. 如何分配相近的内存来达到 near jump
这一部分与具体的操作系统有关. 比如 darwin
下分配内存使用 mmap
实际使用的是 mach_vm_allocate
. move to detail.
在 lldb 中可以通过 memory region address
查看地址的内存属性.
当然这里也存在一个巨大的坑, IOS 下无法分配 rwx
属性的内存页. 这导致 inlinehook 无法在非越狱系统上使用, 并且只有 MobileSafari
才有 VM_FLAGS_MAP_JIT
权限. 具体解释请参下方 [坑 - rwx 与 codesigning].
另一个坑就是如何在 hook 目标周围分配内存, 如果可以分配到周围的内存, 可以直接使用 b
指令进行相对地址跳(near jump
), 从而可以可以实现单指令的 hook.
举个例子比如 b label
, 在 armv8 中的可以想在 +-128MB
范围内进行 near jump
, 具体可以参考 ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile Page: C6-550
.
这里可以有三个尝试.
当然还可以有强制相对跳(double jump
), 直接对 +-128MB
内选一个地址强制 code patch 并修复.
先说坑, 非越狱状态下不允许设置 rw-
为 r-x
, 或者 设置 r-x
为 rx-
. 具体解释请参考下方坑 [坑-rwx 与 codesigning].
其实这里的指令写有种简单的方法, 就是在本地生成指令的16进制串, 之后直接写即可. 但这种应该是属于 hardcode.
这里使用 frida-gum
和 CydiaSubstrace
都用的方法, 把需要用到的指令都写成一个小函数.
例如:
其实有另外一个小思路, 有一点小不足, 就是确定指令片段的长度, 但其实也有解决方法, 可以放几条特殊指令作为结尾标记.
先使用内联汇编写一个函数.
之后直接复制这块函数内存数据即可, 这一般适合那种指令片段堆.
这一部分实际上就是 disassembler
, 这一部分可以直接使用 capstone
, 这里需要把 capstone
编译成多种架构.
这里的指令修复主要是发生在 hook 函数头几条指令, 由于备份指令到另一个地址, 这就需要对所有 PC(IP)
相关指令进行修复. 对于确定的哪些指令需要修复可以参考 Move to <解析ARM和x86_x64指令格式>.
大致的思路就是: 判断 capstone
读取到的指令 ID, 针对特定指令写一个小函数进行修复.
例如在 frida-gum
中:
跳板模块的设计是希望各个模块的实现更浅的耦合, 跳板函数主要作用就是进行跳转, 并准备 跳转目标
需要的参数. 举个例子, 被 hook 的函数经过入口跳板(enter_trampoline
), 跳转到调度函数(enter_chunk
), 需要被 hook 的函数相关信息等, 这个就需要在构造跳板时完成.
可以理解为所有被 hook 的函数都必须经过的函数, 类似于 objc_msgSend
, 在这里通过栈返回值来控制函数(replace_call
, pre_call
, half_call
, post_call
)调用顺序.
本质有些类似于 objc_msgSend
所有的被 hook 的函数都在经过 enter_trampoline
跳板后, 跳转到 enter_thunk
, 在此进行下一步的跳转判断决定, 并不是直接跳转到 replace_call
.
如果希望在 pre_call
和 post_call
使用同一个局部变量, 就想在同一个函数内一样. 在 frida-js
中也就是 this
这个关键字. 这就需要自建函数栈, 模拟栈的行为. 同时还要避免线程冲突, 所以需要使用 thread local variable
, 为每一个线程中的每一个 hook-entry
添加线程栈, 同时为每一次调用添加函数栈. 所以这里存在两种栈. 1. 线程栈(保存了该 hook-entry 的所有当前函数调用栈) 2. 函数调用栈(本次函数调用时的栈)
在进行指令修复时, 需要需要将 PC 相关的地址转换为绝对地址, 其中涉及到保存地址到寄存器. 一般来说是使用指令 ldr
. 也就是说如何完成该函数 writer_put_ldr_reg_address(relocate_writer, ARM64_REG_X17, target_addr);
frida-gum
的实现原理是, 有一个相对地址表, 在整体一段写完后进行修复.
在 HookZz 中的实现, 直接将地址写在指令后, 之后使用 b
到正常的下一条指令, 从而实现将地址保存到寄存器.
也就是下面的样子.
在进行 inlinehook 需要进行各种跳转, 通常会以以下模板进行跳转.
问题在于这会造成 x16 寄存器被污染(arm64 中 svc #0x80
使用 x16 传递系统调用号) 所以这里有两种思路解决这个问题.
思路一:
在使用寄存器之前进行 push
, 跳转后 pop
, 这里存在一个问题就是在原地址的几条指令进行 patch code
时一定会污染一个寄存器(也不能说一定, 如果这时进行压栈, 在之后的 invoke_trampline
会导致函数栈发生改变, 此时有个解决方法可以 pop
出来, 由 hook-entry 或者其他变量暂时保存, 但这时需要处理锁的问题. )
思路二:
挑选合适的寄存器, 不考虑污染问题. 这时可以参考, 下面的资料, 选择 x16 or x17, 或者自己做一个实验 otool -tv ~/Downloads/DiSpecialDriver64 > ~/Downloads/DiSpecialDriver64.txt
通过 dump 一个 arm64 程序的指令, 来判断哪个寄存器用的最少, 但是不要使用 x18
寄存器, 你对该寄存器的修改是无效的.
Tips: 之前还想过为对每一个寄存器都做适配, 用户可以选择当前的 hook-entry
选择哪一个寄存器作为临时寄存器.
参考资料:
这里也有一个问题, 这也是 frida-gum
中遇到一个问题, 就是对于 svc #0x80
类系统调用, 系统调用号(syscall number)的传递是利用 x16
寄存器进行传递的, 所以本框架使用 x17
寄存器, 并且在传递参数时使用 push
& pop
, 在跳转后恢复 x17
, 避免了一个寄存器的使用.
对于非越狱, 不能分配可执行内存, 不能进行 code patch
.
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)