android-hook

前言

目标写出一个通用的android hook库,开发在AS下进行,方便运行和调试
先为了看懂:https://github.com/xiaobaiyey/dexload

钩子(hook)与注入(inject):
hook目的是修改执行逻辑,我直接理解为类似AOP的思想,不过这个更注重实现。
注入目的是非己内容加入自己代码,分为静态与动态,前者运行前修改可执行文件,后者通过ptrace等。
实现部分交集互用,有时不区分。一般hook目标需要注入为前提。
之前写过一篇注入的,但没有实现完整:https://xn--74q78i15hxv3arigm4e.cn/2018/05/26/android-hook/
猜测失败原因是Android 7.0之后对于非公开API的调用限制:

1
2
3
从 Android 7.0 开始,系统将阻止应用动态链接非公开 NDK 库,这种库可能会导致您的应用崩溃。此行为变更旨在为跨平台更新和不同设备提供统一的应用体验。
7.0更新详情:https://developer.android.com/about/versions/nougat/android-7.0-changes?hl=zh-cn#ndk
详细的ndK变更:https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md#Writable-and-Executable-Segments-Enforced-for-API-level-26

开发只要保证:连接不在apk中的库时只能连接公开的NDK,自己使用的so一定要放到apk中打包。
AOSP 7.0以后关于NDK限制机制的核心代码是在 /bionic/linker/linker.cpp,自己环境下可以修改源码来方便注入,其它环境下可以先注入处理掉检测部分,再注入自己的so。

Java Hook
Static Field Hook:静态成员hook
Method Hook:函数hook

Native So Hook
GOT Hook:全局偏移表hook
SYM Hook:符号表hook
Inline Hook:函数内联hook

本篇分析native的hook,下一篇java下针对art hook。

一个cpu是可以支持一系列指令的,比如我的n6p:
ro.product.cpu.abi=arm64-v8a
ro.product.cpu.abilist=arm64-v8a,armeabi-v7a,armeabi
ro.product.cpu.abilist32=armeabi-v7a,armeabi
ro.product.cpu.abilist64=arm64-v8a

ARM64有俩种执行状态:
AArch64 ——64 位执行状态,包括该状态的异常模型、内存模型、程序员模型和指令集支持
AArch32 ——32 位执行状态,包括该状态的异常模型、内存模型、程序员模型和指令集支持
这些执行状态支持三个主要指令集:
A32(或 ARM):32 位固定长度指令集,通过不同架构变体增强部分 32 位架构执行环境现在称为 AArch32。
T32 (Thumb) 是以 16 位固定长度指令集的形式引入的,随后在引入 Thumb-2 技术时增强为 16 位和 32 位混合长度指令集。部分 32 位架构执行环境现在称为 AArch32。
A64:提供与 ARM 和 Thumb 指令集类似功能的 32 位固定长度指令集。随 ARMv8-A 一起引入,它是一种 全新的AArch64 指令集。

hook实现分析

几个推荐的hook框架:
https://github.com/jmpews/HookZz/tree/master
https://github.com/iqiyi/xHook

GToad的实现

https://github.com/GToad/Android_Inline_Hook
https://github.com/GToad/Android_Inline_Hook_ARM64
他分别写了32与64的hook,都很新。
同时在他的博客https://gtoad.github.io/ 写了很多相关的文章。
接下来对其精要

PLT-Hook&Inline-Hook

PLT Hook通过修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。
只能修改指定模块单向使用其它模块的地方。got是代码模块导入其它模块的重定位点。
用于:统一hook目标模块针对同一外部的所有调用,如运行库、系统调用等。
无法hook某次精确调用,无法hook模块内自定义的函数。

Inline Hook在代码段中插入跳转指令,从而把程序执行流程引向自定义代码。
基本流程

图1
几乎应对全部情况,可以针对库函数修改达到批量hook。

GOT-hook

原理实现:https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md

曾经的一篇实现:https://xn--74q78i15hxv3arigm4e.cn/2018/06/29/android%E5%BA%94%E7%94%A8%E5%8A%A0%E5%9B%BA2/

linker对so的连接处理就是将rel指定的内容用其他符号指定的值填写,符号表标识的名字有自己定义的和需要的外部的名字,rel指向了这些外部的名字。自己的代码执行时链接已经完成,此时其他模块使用此模块与此模块使用其他模块的got已经填写完毕,因此got hook,只能动每个模块中got表部分-使用其他模块的调用。

也可以直接用xhook,还挺好用的。

32位Inline-hook

64兼容能用,实际这个版本更通用。
32位示例在n6p测试成功,不过实际使用有一点小心,当lib中存在v8版本64位机会优先选择64位版本使用,此时加入32位的so是错误的。不能同时加载32bit和64bit的so库。实际上64位进程中加载32位so就是错误的。
apk安装时,解析其中lib的类型,确定使用的虚拟机。64位设备提供32与64位俩种虚拟机,根据apk的不同fork使用,优先使用64位的。因此注意使用时abi的完全一致,apk安装时决定版本也要与使用so一致。

使用:hook框架产出so文件,加载so后自动执行hook逻辑。用constructor属性init_array中调用(destructor属性卸载时用)调用顺序与声明顺序一致。

需要的工具:https://github.com/keystone-engine/keystone 多架构汇编程序框架。在这里用于看机器码。

arm32基本步骤

1.根据/proc/self/maps中获取so的基地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
虚拟地址起始 虚拟地址结束 此段属性 文件中页单位偏移 文件所属设备号 文件所属节点号 文件名[stack]表示在进程中作为栈使用,[heap]表示堆
722e3c7000-722e3de000 r-xp 00000000 fd:00 1953 /system/lib64/liblog.so //大小17000 第一段
722e3de000-722e3df000 r--p 00016000 fd:00 1953 /system/lib64/liblog.so //大小1000 第二段
722e3df000-722e3e0000 rw-p 00017000 fd:00 1953 /system/lib64/liblog.so //大小1000 第三段
最小单位为0x1000,即4096字节,页大小。
readelf:
Elf file type is DYN (Shared object file)
Entry point 0x0
There are 8 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
第一段:
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001c0 0x00000000000001c0 R 0x8
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000160ac 0x00000000000160ac R E 0x1000
第二、三段: 这个LOAD到187CC 18000为第三段开始
这里将文件16000内容映射到17000处与16000处,后者供第一段使用,前者供第二段使用,为了另起一页开新段权限不同。
因此17000的前部分不用(是第一部分重复的)从178d0开始用。elf中指明明确的要加载的开始与大小,其中可以推出页的具体映射规划。
LOAD 0x00000000000168d0 0x00000000000178d0 0x00000000000178d0
0x0000000000000d68 0x0000000000000efc RW 0x1000
DYNAMIC 0x0000000000016910 0x0000000000017910 0x0000000000017910
0x0000000000000210 0x0000000000000210 RW 0x8
NOTE 0x0000000000000200 0x0000000000000200 0x0000000000000200
0x0000000000000038 0x0000000000000038 R 0x4
GNU_EH_FRAME 0x0000000000015b20 0x0000000000015b20 0x0000000000015b20
0x000000000000058c 0x000000000000058c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
GNU_RELRO 0x00000000000168d0 0x00000000000178d0 0x00000000000178d0
0x0000000000000730 0x0000000000000730 RW 0x8
Section to Segment mapping:
Segment Sections...
00
01 .note.android.ident .note.gnu.build-id .dynsym .dynstr .gnu.hash .gnu.version .gnu.version_d .gnu.version_r .rela.dyn .rela.plt .plt .text .rodata .eh_frame .eh_frame_hdr
02 .fini_array .data.rel.ro .dynamic .got .got.plt .data .bss
03 .dynamic
04 .note.android.ident .note.gnu.build-id
05 .eh_frame_hdr
06
07 .fini_array .data.rel.ro .dynamic .got .got.plt
ida解析:
第一段17000 0开始
LOAD 0000000000000000 0000000000003E48 R . X . L mempage 01 public CODE 64 00 0D
.plt 0000000000003E48 0000000000004758 R . X . L qword 04 public CODE 64 00 0D
.text 0000000000004758 0000000000013680 R . X . L dword 05 public CODE 64 00 0D
.rodata 0000000000013680 00000000000142B4 R . . . L para 06 public CONST 64 00 0D
LOAD 00000000000142B4 00000000000142B8 R . X . L mempage 01 public CODE 64 00 0D
.eh_frame 00000000000142B8 0000000000015B20 R . . . L qword 07 public CONST 64 00 0D
.eh_frame_hdr 0000000000015B20 00000000000160AC R . . . L dword 08 public CONST 64 00 0D
第二段1000 17000开始
.fini_array 00000000000178D0 00000000000178D8 R W . . L qword 09 public DATA 64 00 0D
.data.rel.ro 00000000000178D8 0000000000017910 R W . . L qword 0A public DATA 64 00 0D
LOAD 0000000000017910 0000000000017B20 R W . . L mempage 02 public DATA 64 00 0D
.got 0000000000017B20 0000000000017B70 R W . . L qword 0B public DATA 64 00 0D
.got.plt 0000000000017B70 0000000000018000 R W . . L qword 0C public DATA 64 00 0D
第三段1000 18000开始
.data 0000000000018000 0000000000018638 R W . . L para 0D public DATA 64 00 0D
.bss 0000000000018638 00000000000187CC R W . . L qword 0E public BSS 64 00 0D
.prgend 00000000000187CC 00000000000187CD ? ? ? . L byte 0F public 64 00 0F
extern 00000000000187D0 0000000000018B40 ? ? ? . L qword 10 public 64 00 10
abs 0000000000018B40 0000000000018B58 ? ? ? . L qword 11 public 64 00 11

从上面一个so的三种状态看结构,分别是:进程中maps下的信息,elf中段的信息,ida中解析出的segmentation
可见,maps下描述的是进程或线程中连续虚拟内存的区域。据说由mmap创建—这需要看完linker才能了解具体的加载过程了。
elf中段是文件如何映射,即文件偏移到VA的转化。
ida中则直接解析出了VA的信息。
对应关系:
maps是实际基址开始的三种权限段VA地址,ida中的是假设从基址0开始的VA,elf则是给出了文件部分的大小与位置所放到VA的大小与位置。
实际的权限与文件要求的权限并不一致,依赖段的内容决定。因此通过权限判别并不可靠。

显而易见,文件so内偏移不是VA偏移,通过文件偏移在内存中找代码位置要依靠段的位置换算。这一过程ida完成了,通过ida分析窗口的地址可以直接看VA偏移,此偏移加上内存中文件基址就是实际的文件中内容在内存的地址。

2.基地址加VA偏移定位hook点,保存hook点内容。

3.构造stub代码,以提前编好的汇编代码为模板填充构建。跳转到自定义的函数,通过寄存器远跳

4.将保存的原地址内容构造成可执行备份模块,并修复指令,最后跳回hook点。继续填充stub的尾部,跳到原内容备份处,通过地址数据改PC远跳。
备份的指令执行不一定能还原原语句功能,此时需要修补指令:
取PC的值进行计算的指令,因为PC改变了。包括相对寻址
跳转到hook点部分的指令,因为跳到中间会造成不完整的hook,跳到hook开始处没事。
主要有:
BLX, BL, B, BX-绝对跳转没问题,相对时需要修正,计算实际要跳转的地址使用修改PC跳转,有L时将原地址下一条保存到LR
ADD-可能使用PC,寻找其他寄存器替代
ADR, LDR, MOV-可能使用PC间接寻址,计算绝对地址获取值,将值赋给指定寄存器。

这里需要复习下arm32指令的格式:
cond 00 X opcode S Rn Rd So
条件码 00 X 指令编码 是否影响CPSR 第一个操作数寄存器编码 目标寄存器编码 第二个操作数

5.修改hook点代码,指向stub
用LDR PC, [PC, #-4] ,想远跳必须绝对地址。B立即数只能相对跳。
由于三级流水,执行时PC是正在执行指令+8的位置,只有直接使用PC的指令要当心这个问题。

Thumb-2方案


图2
https://gtoad.github.io/2018/07/06/Android-Native-Hook-Practice/

暂时先放弃对Thumb的支持….因为我还用不到….

64位Inline-hook

测试并分析64位的

ARM64切换到ARM32处理器模式来处理ARM32和Thumb-2需要产生Ring1的异常才行,因此普通进程并不会混用指令。
ARM64指令集中PC,SP不再是通用寄存器X0-X31中的一员。在ARM32中,R13就是SP,R14就是LR,R15就是PC。在ARM64中,X29是栈帧寄存器,X30是LR,而PC,SP是独立的寄存器。这也导致了对它们的操控和读写有了更多限制。没有了PUSH/POP和LDM/STM指令。


图3

基本流程与32位一致。

注意一下寄存器的恢复时机。占用的寄存器应该在跳至的代码中恢复,这里他是做的对的。

1
2
3
4
5
6
STP X1, X0, [SP, #-0x10]
LDR X0, 8
BR X0 //去时的x0修复在stub代码中
[TARGET_
ADDRESS] (64bit)
LDR X0, [SP, -0x8] //这是回来的时候修复X0的

我的hook方案与实现

64用hook,接口完善。

一次性hook,不用修复指令,完美复原。
不限次数hook,修复指令。

项目地址:https://github.com/imbaya2466/android-hook
含分析项目的注释代码及自己的实践hook。
待完成:

  1. 优化接口,改善数据结构 ok
  2. 修复bug-重复hook ok
  3. 删除hook功能 ok
  4. 一次性hook -失败
  5. 使hook函数提前结束(不使用BLR而使用BR,用户函数的ret将直接将hook函数返回)

无法一次性hook-arm64下。
因为不可修改pc,只能使用寄存器跳转,此时跳转回去必须使用一个寄存器但无代码可修复寄存器,因此失败。

一些坑:

  1. Cmake尝试失败,最后又用回了ndk-build
  2. 一定要在build.gradle中加abiFilters,否则会将arm代码视为别的abi编译产生错误。在application.mk中指定集合到studio没用。即含有汇编代码时需要指定平台编译!!
  3. AS build出apk里的so与AS调试用的so不一致,即使是debug编译出的。因此找偏移地址时要确定目标so不变。
  4. native方法不是System.loadLibrary时绑定,是调用native方法时才寻找符号表中符合的,也可以手动绑定。需要看art的native方法oat后的样子才能确定。

关于多线程安全

hook时暂停所有线程,并检查是否有线程位于hook点,有时就将其运行随机小时间,之后再次暂停,直到hook点没有执行线程,进行hook。hook后恢复所有线程。