Android Natvie Hook 讲解
- 一.什么是Hook,以及Android Native层 hook
- 二. got 表 Hook
- 1.Got Hook 需要掌握知识点
- 1.1编译链接
- 1.2ELF文件
- 1.2.1 ELF文件格式
- ELF整体结构
- ELF Header
- Section Head Table
- Program Head Table
- 1.2.2 ELF加载流程
- 1.3 Linux 内存相关
- 1.3.1 查看进程 内存空间文件映射情况
- 1.3.2 内存属性修改
- 2.plt/got 表作用
- 3.got 表 hook 原理
- got 表参与函数调用流程
- hook 方案
- 4.got 表 hook 流程(hook fopen函数为例)
- 4.1 获取动态库的基地址
- 4.2 计算SO中 program head table的地址
- 4.3 遍历程序头部表,获取动态段地址
- 4.4 遍历动态段,找到got表地址
- 4.5 修改内存属性为可写
- 4.6 遍历got表,修改要替换的fopen函数
- 4.7 恢复存属性为可读可执行
- 三. inline Hook
- 1.inline hook 名词解释、知识点
- 1.1处理器架构
- 1.2ARM处理器 指令集
- 1.3ARM 架构流水线
- 1.4ARM 常用寄存器
- 1.5相关汇编指令
- 1.6常见寻址方式
- 1.7 汇编机器码格式
- 2.inline hook 原理
- 3.inline hook 流程(ARM为例)
- 3.1 寻址指令替换地址
- 3.2构建8字节跳转代码
- 3.3 构建stub函数
- 3.4 执行备份代码,跳转到原函数继续执行
- 4.指令修复
一.什么是Hook,以及Android Native层 hook
hook指 通过一些技术手段,修改一个目标函数,在一个函数或一条指令执行前或执行后 注入我们自己的代码或指令。
Android java层 Hook 一般采用动态代理、字节码注入以及Xposed 和其衍生品。
Andoid navite HooK方式和框架相对较少,尤其可以稳定的inlineHook框架。
常用Navtive 层Hook方法有GOT/PLT Hook、 inlineHook 以及Trap Hook等,先简单介绍下原理以及各自特点
Hook 级别 | 效率 | 应用范围 | 原理 | 难易程度 | |
GOT/PLT Hook | 方法级 | 高 | 范围窄,只限于绑定表函数 | 在ELF动态连接过程上修改,修改函数绑定表的 函数对应地址,入侵性低,但只能用于绑定表的函数(导入导出函数) | 中等 |
Trap Hook | 指令级 | 低 | 范围广,任何指令都行 | 调试器原理,基于SIGTRAP断点信号,所以效率不高 | 中等 |
Inline Hook | 指令级 | 高 | 范围广,绝大部分函数都行,函数太短不稳定 | 运行时,修改CPU要执行的机器码,进行指令级替换 | 极难 |
一般来说,简单的绑定表函数,用GOT Hook,其他用情况 inline Hook,Trap Hook 等其他方式很少用到,所以主要介绍 GOT 表Hook和 InlineHook。
二. got 表 Hook
1.Got Hook 需要掌握知识点
1.1编译链接
程序要运行起来,必须要经过 编译和链接两个过程,共包含 四个步骤:预处理、编译、汇编和链接。
- 预编译:对宏定义、条件指令、注释等处理。
- 编译:
编译器把一种高级语言(C/CPP)翻译成另外一种低级语言(汇编/机器码),主要流程是:
1) 首先编译器进行语法分析,也就是要把那些字符串分离出来,构建语法树。
2)然后进行语义分析,就是把各个由语法分析分析出的语法单元的意义搞清楚。
3)中间代码生成,产生机器码和无关机器码的中间代码。
4)目标代码生成与优化,编辑器对指令进行优化
- 汇编: 调用汇编器,把汇编翻译成机器码,生成目标文件。
- 链接:
将各种代码和数据部分收集起来并组合成为一个单一文件的过程叫做链接,最终生成ELF可执行文件。
连接器主要功能作用:
1)解析符号。解析文件中的变量、函数名称、语句等内容生成符号表。符号表是一个结构数组,每个表项包含符号名、长度和位置信息等。
2)重新分配段(Section)空间。每个目标文件(.o 文件)都有自己独立的.text段(Section)和.data段(Section)等空间。链接器将多个目标文件链接之后,生成一个新的可执行文件,需要对.text段以及.data段进行空间划分
1.2ELF文件
ELF文件:
ELF文件是 Linux的可执行文件格式,类似window PE文件
1.2.1 ELF文件格式
ELF整体结构
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)
ELF文件提供了两种视图:链接视图和执行视图
- 链接视图-可重定位目标文件:包含代码、数据、定位信息(指出哪些符号引用处需要重定位)等。可被链接合并,生成可执行文件或共享目标文件(.so文件)或静态链接库文件(.a文件)。
- 执行视图-可执行目标文件
ELF文件各个部分解释:
- ELF头:描述整个文件的组织,说明文件类型、大小、各个节区表的偏移地址
- 程序头表:描述与程序执行直接相关的目标文件结构信息。用来在文件中定位各个段的映像。同时包含其他一些用来为程序创建映像所必须的信息
- 节/段:sections主要给Linker用,从链接的角度来描述elf文件。segments主要给Loader用,从加载的角度来描述elf文件。
Linker需要关心.text, .rel.text, .data, .rodata等等,关键是Linker需要做relocation。而Loader只需要知道Read/Write/Execute的属性,加载ELF文件时候,把多个sections组成segments来加载,所以一个segment包含若干个section。
-节区头表:描述 文件节区的信息,大小、偏移等
ELF Header
ELF Header包含的信息,在Linux C中 <ELF.h>头文件中有定义:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // ELF的一些标识信息,前四位为.ELF,其他的信息比如大小端等
ELF32_Half e_type; // 文件类型
ELF32_Half e_machine; // 文件的目标体系架构,比如ARM
ELF32_Word e_version; // 目标文件版本
ELF32__Addr e_entry; // 程序入口的虚拟地址
ELF32_Off e_phoff; // 程序头部表偏移地址
ELF32_Off e_shoff; // 节区头部表偏移地址
ELF32_Word e_flags; // 保存与文件相关的,特定于处理器的标志
ELF32_Half e_ehsize; // ELF头的大小
ELF32_Half e_phentsize; // 每个程序头部表的大小
ELF32_Half e_phnum; // 程序头部表的数量
ELF32_Half e_shentsize; // 每个节区头部表的大小
ELF32_Half e_shnum; // 节区头部表的数量
ELF32_Half e_shstrndx; // 节区字符串表位置
}Elf32_Ehdr;
readelf命令可以读取 ELF 文件,readelf - h xxx 可以读取ELF文件的 Head信息,例如
其中 e_ident对应的具体结构如下
Section Head Table
节区头表的 结构体定义为:
typedef struct{
Elf32_Word sh_name; //节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串。
Elf32_Word sh_type; //为节区类型
Elf32_Word sh_flags; //节区标志
Elf32_Addr sh_addr; //如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0。
Elf32_Off sh_offset; //此成员的取值给出节区的第一个字节与文件头之间的偏移。
Elf32_Word sh_size; //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。
Elf32_Word sh_link; //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
Elf32_Word sh_info; //此成员给出附加信息,其解释依赖于节区类型。
Elf32_Word sh_addralign; //某些节区带有地址对齐约束.
Elf32_Word sh_entsize; //某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。
}Elf32_Shdr;
其中重点关注 sh_type,它包含12个值,有些节区是系统预订的,一般以点开头号,说几个重点的
类型 | 取值 | 说明 |
SHT_PROGBITS | 1 | 此节区包含程序定义的信息,其格式和含义都由程序来解释。 |
SHT_STRTAB | 3 | 此节区包含字符串表。目标文件可能包含多个字符串表节区。 |
SHT_DYNAMIC | 6 | 此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制 |
SHT_DYNSYM | 11 | 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。 |
- 符号表(.dynsym) sh_type 为SHT_DYNSYM(11)
符号表包含用来定位、重定位程序中符号定义和引用的信息,记录了该文件中的符号。 - 字符串表(.dynstr)sh_type 为SHT_STRTAB(3)
字符串表中存放着所有符号的名称字符串。 - 代码段(.text)sh_type 为SHT_PROGBITS(1)
包含可执行指令 - 重定位表
可重定位文件必须包含如何修改其节区内容信息,使得 可执行文件和共享目标文件 所在进程映射正确的信息。
常见的重定位表类型:
- .rel.text:重定位的地方在.text段内,以offset指定具体要定位位置。在链接时候由链接器完成。.rel.text属于普通重定位辅助段 ,他由编译器编译产生,存在于obj文件内。连接器连接时,他用于最终可执行文件或者动态库的重定位。通过它修改原obj文件的.text段后,合并到最终可执行文件或者动态文件的.text段。其类型一般为R_386_32和R_386_PC32。
- .rel.dyn:重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,一般是在.init段内。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。
- .rel.plt:重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。首次调用时会重定位函数地址,把最终函数地址放到.got内,以后读取该.got就直接得到最终函数地址。这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。
- .plt段(过程链接表):所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。
- .got(全局偏移量表):存放符号的相对地址
Program Head Table
程序头部表结构体定义如下
typedef struct {
Elf32_Word p_type; //此数组元素描述的段的类型,或者如何解释此数组元素的信息。
Elf32_Off p_offset; //此成员给出从文件头到该段第一个字节的偏移
Elf32_Addr p_vaddr; //此成员给出段的第一个字节将被放到内存中的虚拟地址
Elf32_Addr p_paddr; //此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
Elf32_Word p_filesz; //此成员给出段在文件映像中所占的字节数。可以为0。
Elf32_Word p_memsz; //此成员给出段在内存映像中占用的字节数。可以为0。
Elf32_Word p_flags; //此成员给出与段相关的标志。
Elf32_Word p_align; //此成员给出段在文件中和内存中如何对齐。
} Elf32_phdr;
p_type 类型中,重点关注 为2的,表示PT_DYNAMIC ,给出动态链接信息,结构是一个 包含了动态节结构体数组。
typedef struct dynamic {
Elf32_Sword d_tag;
union {
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
其中 d_tag 是动态节的类型,重点关注以下几个,got 表 hook 时候,会用到
名称 | 数值 | 介绍 |
DT_SYMTAB | 6 | 该元素保存着符号表的地址 |
DT_STRTAB | 3 | 该元素保存着字符串表地址 |
其中当d_tag为6的时候,为got表,got数据结构为
typedef struct elf32_sym {
Elf32_Word st_name; // 符号名称
Elf32_Addr st_value; // 符号的偏移地址
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
看个例子 readelf --segments xx 可以读取程序头表、列出所有段 以及段包含的节
1.2.2 ELF加载流程
首先了库可以分为动态链接库和静态链接库:
- 静态链接库:在程序的编译链接阶段就完成函数和变量的地址解析,并打包进代码
- 动态链接库:不需要在编译时就被打包到进代码,等到运行的阶段,进行动态加载和重定位
其次说下,动态链接库的加载流程。(详细过程可参考 )
1.动态库在链接时候,依赖的动态库会被记录到.dynamic区段中;加载动态库所需的Linker由.interp来指示。
2.程序运行时,系统会通过.interp区段找到链接器的绝对路径,然后将控制权交给Linker(Linker是Android系统动态库so的加载器/链接器)
3.Linker 负责解析.dynamic中的记录,找到依赖的动态库并加载
4.动态链接库加载完成后,Linker 通过 GOT/PLT来进行动态重定位,使符号可以正常访问到(GOT 表 Hook 的就是这一步)
1.3 Linux 内存相关
1.3.1 查看进程 内存空间文件映射情况
/proc/{pid}/maps 是进程运行时的虚拟内存映射文件,可以通过这个文件来查看加载的动态库在内存中的绝对地址
格式如下
76093000-76096000 | r-xp | 00000000 | b3:19 | 941 | /system/lib/libmemalloc.so
第一列 76093000-76096000 是虚拟地址(VMA)
第二列 r-xp 是权限 r=read,w=write,x=executed,s=shared,p=private
第三列 00000000 VMA对应的 segment 在映像文件中的偏移。
第四列 b3:19 主次设备号
第五列:941 映像文件的节点号
第六列:/system/lib/libmemalloc.so 映像文件的路径
通过此文件,可以找到目标 so 的基地址,再通过 got 表,找到每个符号的偏移地址,两者相加,即可得到 目标函数在内存中的绝对地址
1.3.2 内存属性修改
代码段属性是 rx, 需要使用mprotect改变内存页为rwx
2.plt/got 表作用
在介绍 plt/got 表之前,先了解下 pic
- pic 技术。 ELF 格式的共享库使用 PIC 技术使代码和数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC 在代码中的跳转和分支指令不使用绝对地址。
- got 全局偏移表。PIC 在 ELF 可执行映像的数据段中建立一个存放所有全局变量指针的全局偏移量表 GOT。对于模块外部引用的全局变量和全局函数,用 GOT 表的表项内容作为地址来间接寻址;对于本模块内的静态变量和静态函数,用 GOT 表的首地址作为一个基准,用相对于该基准的偏移量来引用。因为不论程序被加载到何种地址空间,模块内的静态变量和静态函数与 GOT 的距离是固定的,并且在链接阶段就可知晓其距离的大小。这样,PIC 使用 GOT 来引用变量和函数的绝对地址,把位置独立的引用重定向到绝对位置。对于 PIC 代码,代码段内不存在重定位项,实际的重定位项只是在数据段的 GOT 表内
- plt 过程链接表。用于把位置独立的函数调用重定向到绝对位置。通过 PLT 动态链接的程序支持惰性绑定模式。每个动态链接的程序和共享库都有一个 PLT,PLT 表的每一项都是一小段代码(桩函数),对应于本运行模块要引用的一个全局函数。程序对某个函数的访问都被调整为对 PLT 入口的访问。
- GOT和PLT关系。每个 PLT 入口项对应一个 GOT 项,执行函数实际上就是跳转到相应 GOT 项存储的地址。该 GOT 项初始值为 PLTn项中的 push 指令地址(即 jmp 的下一条指令,所以第 1 次跳转没有任何作用),待符号解析完成后存放符号的真正地址。
动态链接器在装载映射共享库时在 GOT 里设置 2 个特殊值:
在 GOT+4( 即 GOT[1]) 设置动态库映射信息数据结构link_map 地址;
在 GOT+8(即 GOT[2])设置动态链接器符号解析函数的地址_dl_runtime_resolve。
PLT 的第 1 个入口 PLT0 是一段访问动态链接器的特殊代码。程序对 PLT 入口的第 1 次访问都转到了 PLT0,最后跳入 GOT[2]存储的地址执行符号解析函数。
待完成符号解析后,将符号的实际地址存入相应的 GOT 项,这样以后调用函数时可直接跳到实际的函数地址,不必再执行符号解析函数
3.got 表 hook 原理
got 表参与函数调用流程
- 外部函数第一次调用流程
- 外部函数第二次调用流程
外部函数调用总结:调用对应桩函数—>桩函数取出.got表表内地址—>然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got表对应处,同时跳转执行该地址指令.以后桩函数从.got取得地址都是真实函数地址了。
hook 方案
我们知道,函数调用,最终是从 got 表取出函数地址,所以可以直接 修改 got 表对应函数的地址为我们自己的函数地址,完成 Hook。
4.got 表 hook 流程(hook fopen函数为例)
4.1 获取动态库的基地址
void *base_addr = get_model_base(getpid(), "libnative-lib.so"); // 读取map文件,按行读取分割匹配,获取so对应的内存基地址
4.2 计算SO中 program head table的地址
Elf32_Ehdr *header = (Elf32_Ehdr *) (base_addr);
if (memcmp(header->e_ident, "\177ELF", 4) != 0) { // 判断为elf文件
LOGD("[Error!] not elf file ");
return 0;
}
Elf32_Phdr *phdr_table = (Elf32_Phdr *) (base_addr + header->e_phoff); // 程序头部表的地址
if (phdr_table == 0) {
LOGD("[error !] phdr_table address : 0 ");
return 0;
}
size_t phr_count = header->e_phnum; // 程序头表项个数
4.3 遍历程序头部表,获取动态段地址
遍历 program head table,找到p_type 为dynameic的,获取到p_offset
unsigned long dynamicAddr; // 动态节的 地址
unsigned int dynamicSize; // 动态段 大小
for (int j = 0; j < phr_count; j++) {
if (phdr_table[j].p_type == PT_DYNAMIC) {
dynamicAddr = phdr_table[j].p_vaddr + base_addr;
dynamicSize = phdr_table[j].p_memsz;
break;
}
}
4.4 遍历动态段,找到got表地址
开始遍历dynamic段结,d_tag为6即为GOT表地址
int symbolTableAddr = 0; // 符号表地址
Elf32_Dyn *dynamic_table = (Elf32_Dyn *) dynamicAddr;
for (i = 0; i < dynamicSize / 8; i++) {
int val = dynamic_table[i].d_un.d_val;
if (dynamic_table[i].d_tag == 6) {
symbolTableAddr = val + base_addr;
break;
}
}
4.5 修改内存属性为可写
uint32_t page_size = getpagesize(); // 获取内存分页的起始地址(需要内存对齐)
mprotect((uint32_t) mem_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);
4.6 遍历got表,修改要替换的fopen函数
int oldFunc = fopen - (int) base_addr; // 原目标函数 地址
int newFunc = fakeFunc; // 替换的hook函数的偏移地址
while (1) {
if (symTab[i].st_value == oldFunc) {
symTab[i].st_value = newFunc; // st_value 保存的是偏移地址
break;
}
i++;
}
4.7 恢复存属性为可读可执行
mprotect((uint32_t) mem_page_start, page_size, PROT_READ | PROT_EXEC);
经过以上几个步骤,就完成了基于got表的函数hook
三. inline Hook
1.inline hook 名词解释、知识点
1.1处理器架构
Android 处理器架构有7种:
- armeabi 第5代 ARM v5TE,使用软件浮点运算,兼容所有ARM设备,通用性强,速度慢(只支持armeabi)
- armeabi-v7a 第7代 ARM v7,使用硬件浮点运算,具有高级扩展功能(支持 armeabi 和armeabi-v7a,大部分手机都是这个架构)
- arm64-v8a 第8代,64位,包含AArch32、AArch64两个执行状态对应32、64bit(支持 armeabi-v7a、armeabi 和
arm64-v8a) - x86 intel 32位,一般用于平板(支持 armeabi(性能有所损耗) 和 x86)
- x86_64 intel 64位,一般用于平板(支持 x86 和 x86_64)
- mips 基本没见过
- mips64 基本没见过(支持 mips和 mips_64)
绝大多数手机都是ARM架构,分32位和64位,下面都以32位ARM处理器为基础。
1.2ARM处理器 指令集
ARM处理器支持 ARM、Thumb、Thumb-2 三种指令集
- ARM 指令集:传统ARM处理器支持的指令集,长度为32位,运行速度也是最快
- Thumb 指令集:以看作是 ARM 指令压缩形式的子集,是针对代码密度的问题而提出的,它具有 16 位的代码密度,不如ARM指令的效率高
- Thumb2 指令集:在前面两者之间取了一个平衡, 兼有二者的优势,指令长度有16位、32位两种,保证效率又节约空间
1.3ARM 架构流水线
在ARM7中有3级流水线,“取指–>译码–>执行”
在ARM9中有5级流水线,“取指–>译码–>执行–>访存(LS1)–>回写(LS2)”
不管三级还是五级流水线,第一级流水线执行的时候,第三级流水线在取指。
而 PC 指向的是要取的指令的地址。在ARM状态下,一条指令为32位,所以PC = 当前正在运行地址+8
1.4ARM 常用寄存器
1. 共有37个寄存器,31个通用+6个状态寄存器
2. 可访问的有: R0-R15 16个 通用寄存器 + 一个状态 寄存器 共17 个
常用关键寄存器列表
寄 存 器 | 作用 | 说明 |
R0-R3 | 普通寄存器;用户函数参数以及返回值传递 | – |
R4-R6 | 普通寄存器 | R4~R11 主要用于保存局部变量,但在 Thumb 程序中,通常只能使用 R4~R7 来保存局部变量 |
R13 | SP | 栈顶指针 |
R14 | LR | 存放函数返回地址 |
R15 | PC | 程序计数器 |
CPSR | 状态寄存器 | 获取当前寄存器的状态 |
1.5相关汇编指令
指令 | 描述 |
STR | 寄存器->内存;STR R0,[R1]; :R0数据放入R1为地址内存中 |
LDR | 内存->寄存器; LDR R0,[R1]; :R1地址数据装入R0 |
B | 跳转 |
BL | 带链接跳转.函数执行调用,使lr指向调用者的下一条指令,即函数返回地址 |
BLX | 带链接和状态切换的跳转。切换ARM和thumb指令 |
1.6常见寻址方式
ARM有7种常见寻址方式,了解基本的立即数寻址,寄存器寻址,寄存器间接寻址。
立即数寻址 MOV R0, #64 ; 64 ->R0
寄存器寻址 ADD R0,R1,R2; r0 =r1+r2
寄存器间接寻址 LDR R0,[R1]; R0 <- r1
1.7 汇编机器码格式
- 31–28 条件位
- 27–26为保留位,恒为00
- 25位:标志0-11位 存放的是立即数还是寄存器。若为寄存器则置0,若为立即数则置1。
- 24–21为opcode,标明指令的类型,下面是opcode的取值表
- 19–16位 第一个个源操作数寄存器
- 15–12位目的寄存器
- 11-0 操作数
2.inline hook 原理
inline Hook 的基本思路就是在已有的代码段中插入跳转指令,把代码的执行流程转向我们实现的 Hook 函数中,然后再进行指令修复,并跳转回原函数继续执行。
3.inline hook 流程(ARM为例)
借鉴一张网图
3.1 寻址指令替换地址
首先跟got表hook一样,读取/proc/self/map文件,找到目标so的基地址,
然后把目标函数的偏移地址 +8 作为指令替换的地址。
void *base_addr = get_model_base(getpid(), "libnative-lib.so"); // 读取map文件,按行读取分割匹配,获取so对应的内存基地址
int *addr = base_addr + targer_addr + 8 ; // 指令替换开始地址
3.2构建8字节跳转代码
首先 备份要被替换的2条ARM汇编代码
memcpy(backup_addr, addr, 8)
然后 用寄存器间接寻址方式,通过LDR PC指令和一个地址(共计8 Bytes)替换备份的2条汇编代码,修改pc的值,来修改程序执行流程。
跳转指令为啥不用B系列指令来跳转?因为ARM的B系列指令跳转范围只有4M,Thumb的B系列指令跳转范围只有256字节,然而大多数情况下跳转范围都会大于4M,故采用LDR PC, [PC, ?]构造跳转指令。
构建的跳转的汇编指令如下
LDR PC, [PC, #-4]
xxxxaddr
翻译成c语言为
BYTE szLdrPCOpcodes[8] = {0x04, 0xF0, 0x1F, 0xE5}; // LDR PC, [PC, #-4]对应的机器码为:0xE51FF004
memcpy(szLdrPCOpcodes + 4, &pJumpAddress, 4); // 将目的地址拷贝到跳转指令下方的4 Bytes中
3.3 构建stub函数
构造思路是先压栈保存寄存器现场,然后跳转到用户的自定义的hook函数并执行,
执行完成后,从栈恢复所有的寄存器状态,然后跳转到之前备份的8字节代码处。
压栈出栈,以及切换状态 需要用到汇编代码
_shellcode_start_s:
push {r0, r1, r2, r3} ;压栈保存cpu现场
mrs r0, cpsr
str r0, [sp, #0xC]
str r14, [sp, #8]
add r14, sp, #0x10
str r14, [sp, #4]
pop {r0}
push {r0-r12}
mov r0, sp
ldr r3, _hookstub_function_addr_s
blx r3 ;跳转到用户自定义hook函数
ldr r0, [sp, #0x3C] ;恢复cpu现场
msr cpsr, r0
ldmfd sp!, {r0-r12}
ldr r14, [sp, #4]
ldr sp, [r13]
ldr pc, _old_function_addr_s
3.4 执行备份代码,跳转到原函数继续执行
先执行之前备份的2条ARM代码(共计8 Btyes),然后用LDR指令跳转回Hook地址+8 bytes的地址处继续执行
备份代码1
备份代码2
LDR PC, [PC, #-4] ; 跳回hook地址+8处,继续执行原函数
HOOK_ADDR+8
4.指令修复
以上流程看似一切正常,cpu的现场都被保存和恢复了,但是,如果说 被替换的两条指令,在恢复执行的时候,有用到pc寄存器的值,这两条指令的执行位置是在stub跳板函数中,并不是原函数,所以pc的值并不是预期的值,这种情况就要进行执行指令修复。
指令修复需要考虑以下指令(ARM为例,thumb和thumb2类似)
- B系列跳转
- ADD_ARM
- ADR_ARM, LDR_ARM, MOV_ARM
- 其它指令 OTHER_ARM
篇幅原因以最简单的ADD指令为例讲解下指令修复:
ADD指令 可能用到PC寄存器,用到PC的时候,PC位于ADD指令机器码的为16到19位。修复时候,给PC正确的值即可。
- 1.首先找一个指令没有用到的寄存器Rr
int rd;
int rm;
int r;
// 解析指令得到rd、rm寄存器
rd = (instruction & 0xF000) >> 12;
rm = instruction & 0xF;
// 为避免冲突,排除rd、rm寄存器,选择一个临时寄存器Rr
for (r = 12; ; --r) {
if (r != rd && r != rm) {
break;
}
}
- 2.然后把Rr值压栈
// trampoline_instructions 存放修正后指令的首地址,用于调用原函数
trampoline_instructions[index++] = 0xE52D0004 | (r << 12); // PUSH {Rr},保护Rr寄存器值
- 3.把PC的值给Rr
trampoline_instructions[index++] = 0xE59F0008 | (r << 12); // LDR Rr, [PC, #8],
- 4 变换原指令
ADR Rd, <label>
为ADR Rd, Rr, ?
trampoline_instructions[index++] = (instruction & 0xFFF0FFFF) | (r << 16);
- 5.Rr出栈,恢复Rm原来的值
trampoline_instructions[index++] = 0xE49D0004 | (r << 12); // POP {Rr}
- 6.继续执行下面的代码
trampoline_instructions[index++] = 0xE28FF000;// ADD PC, PC,0 跳过下一条指令
trampoline_instructions[index++] = pc; // 存放的地址
指令修复,推荐看下 https://gtoad.github.io/2018/07/13/Android-Inline-Hook-Fix/ 小渣渣的我,真心膜拜。