首页
社区
课程
招聘
[原创] 如何构建一款像 frida 一样的框架
发表于: 2017-9-1 12:21 34133

[原创] 如何构建一款像 frida 一样的框架

2017-9-1 12:21
34133

一般来说可以分为以下几个模块

需要分配部分内存用于写入指令, 这里需要关注两个函数都是关于内存属性相关的. 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-xrx-. 具体解释请参考下方坑 [坑-rwx 与 codesigning].

其实这里的指令写有种简单的方法, 就是在本地生成指令的16进制串, 之后直接写即可. 但这种应该是属于 hardcode.

这里使用 frida-gumCydiaSubstrace 都用的方法, 把需要用到的指令都写成一个小函数.

例如:

其实有另外一个小思路,  有一点小不足, 就是确定指令片段的长度, 但其实也有解决方法, 可以放几条特殊指令作为结尾标记.

先使用内联汇编写一个函数.

之后直接复制这块函数内存数据即可, 这一般适合那种指令片段堆.

这一部分实际上就是 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_callpost_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.


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 6
支持
分享
最新回复 (7)
雪    币: 3407
活跃值: (1237)
能力值: ( LV13,RANK:335 )
在线值:
发帖
回帖
粉丝
2
好文
2018-4-8 15:48
0
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
  感谢分享
2018-4-12 18:09
0
雪    币: 2
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
  牛b 
2018-6-23 01:24
0
雪    币: 182
活跃值: (214)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
厉害
2018-11-22 14:28
0
雪    币: 5482
活跃值: (3272)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
6
学习
2019-8-15 13:48
0
雪    币: 38
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
学习了,受益良多
2019-11-14 14:46
0
雪    币: 422
活跃值: (2653)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
牛批牛批
2022-11-17 11:56
0
游客
登录 | 注册 方可回帖
返回
//