Android_Inline_Hook
https://github.com/GToad/Android_Inline_Hook_ARM64
有32和64的实现,但是是分离的,要用的话还要自己把两份代码合并在一起。
缺点:
1、不支持函数替换(即hook后不执行原函数),现在只能修改参数寄存器,无法修改返回值。
2、不支持定义同类型的hook函数来接受处理参数,只能通过修改寄存器的方式修改参数。
多余4个/或者占两个字节的参数,那么参数还要自己从栈上捞取。所以issues中说的把mov r0,sp去掉用来接收参数也是有问题的,就是参数在栈上的情况,传过来的时候sp不是原来的sp了。
因为以上的两个缺点,想来没太多人用也是情理之中了,因为自己解析参数、不能修改返回值、不能不执行原函数,局限太大了。看来要想用起来,还得自己修改代码。。。
whale
https://github.com/asLody/whale
移植好像比较简单,记得好像移植过,但是hook了某个系统函数回调原函数就崩溃了。所以用之前可能要先帮他找一遍bug、修复。lody的代码bug一直很多。。。
后记:art hook的之前看过,应该说是xposed/frida的另一份代码,frida也是用的xposed的方式,只不过不修改系统文件、通过动态调用系统函数实现。frida是js的代码,这个是c/c++的实现。
内联hook大概看了下其实也是一样的套路,32位采用ldr pc的方式跳到hook函数,64位使用x17寄存器跳到hook函数。剩下的修复原函数也是一样的。只是没时间完整看一遍定位bug了。
HookZz
https://github.com/jmpews/HookZz
对于安卓程序员来说不太友好,编译需要cmake。
windows:
mkdir build_for_android_arm64 && cd build_for_android_arm64
set ANDROID_NDK=D:\android\NDK\android-ndk-r16b
C:\Users\EDZ\AppData\Local\Android\Sdk\cmake\3.10.2.4988404\bin\cmake … -DCMAKE_TOOLCHAIN_FILE=%ANDROID_NDK%/build/cmake/android.toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DANDROID_ABI=“arm64-v8a” -DANDROID_NATIVE_API_LEVEL=android-21 -DSHARED=OFF -DHOOKZZ_DEBUG=OFF -G “Unix Makefiles” -D"CMAKE_MAKE_PROGRAM:PATH=D:\app\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin\mingw32-make.exe"
需要指定-G “Unix Makefiles”,默认的NMake Makefiles编译不过,指定make,因为未设置环境变量,-D"CMAKE_MAKE_PROGRAM:PATH="
编译动态库,-DSHARED=ON;编译静态库,-DSHARED=OFF。
接下来为了方便还是移植到Android工程中吧。
呃。。。基本不可用!Android:arm/arm64均crach,arm64可以修复,https://www.gitmemory.com/foundkey,在OneLib\stdcxx\LiteIterator.cc中函数initWithCollection添加inCollection->initIterator(innerIterator);
但是arm还是crash,
#00 pc 00025ef0 [anon:libc_malloc]
12-12 15:12:53.685 181-181/? I/DEBUG: #01 pc 000081f1 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (LiteCollectionIterator::getNextObject()+28)
12-12 15:12:53.685 181-181/? I/DEBUG: #02 pc 00007531 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (gen_thumb_relocate_code(void*, int*, unsigned int, unsigned int)+312)
12-12 15:12:53.685 181-181/? I/DEBUG: #03 pc 00007ac1 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (InterceptRouting::Prepare()+56)
12-12 15:12:53.685 181-181/? I/DEBUG: #04 pc 00007cc1 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (FunctionInlineReplaceRouting::Dispatch()+12)
12-12 15:12:53.685 181-181/? I/DEBUG: #05 pc 00007d4d /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (ZzReplace+120)
且通过arm64测试来看,open、fopen可以hook成功。__system_property_get函数,第二个参数既是入参也做返回参数的情况无法正确hook,可以hook到,但是回调原函数,无论是使用第二个参数还是new参数均无法得到值,所以肯定哪里存在bug。
一个函数只能被hook一次,再次hook调用原函数(备份的第一个hook函数)崩溃,所以两次hook不能再调用原函数。
基于以上种种情况,可能还是自己实现Android_Inline_Hook比较好,毕竟Android_Inline_Hook代码易懂,hookZz太多无关代码,没时间看架构了。
后记:后来大概看了一下,32位也是ldr pc实现,好像也做了保存寄存器等操作,和Android_Inline_Hook基本是一样的,64位好像也是使用x17寄存器。其他也都是大同小异。所以也是没时间完整看一遍定位bug了。
自实现inline hook
因为以上的问题,目前/或者之前用过的一些hook框架或多或少都有些较大的bug(hookzz之前的某个版本应该是可以的),而对其进行修复成本较高,还不如自己写一个。
首先统一一些概念。把覆盖被hook函数的指令称为跳板0,因为这部分指令越短越好,所以其大多数情况下只是一个跳板,跳到真正的用于hook指令处,这部分指令称为shellcode(shellcode不是必须的,比如跳板0直接跳到hook函数去执行,那么就不需要shellcode)。
这里假设都有一些arm指令的基础了,或者对hook已经有些理解了。后来我想了下我这篇更偏向于怎么写一个稳定可用hook框架,更偏向设计、编程,所以适合已经有些基础的,不是从0讲述hook到实现,虽然接下来的部分也会有很细节的部分,但是是针对一些特定的点。建议可以先看下其他人,比如ele7enxxh、GToad写的一些文章。
inline hook这种东西,我是感觉当你掌握汇编、自己有需求的情况下,不经过学习也是可以从0写出一个hook框架的,确实是原理很简单的。
最简单的实现
最容易想到的一种实现方式,使用跳板0覆盖一个函数的指令,当执行到这个函数的时候其实就是执行跳板0,跳板0在不修改寄存器的情况跳到执行hook函数。如果在hook函数内不需要执行原函数是最简单的,到这hook就是一个完整的hook。
如果需要执行原函数,那么在跳板0覆盖指令之前先备份指令,执行原函数之前把备份的指令再覆盖回去,执行之后再覆盖回跳板0。确实是最简单的方法、也确实可以,但是也有一个很大的问题,就是多线程的问题,在把备份的指令覆盖回去之后,其他线程执行到这里不就hook不到了,甚至crash。
因为无法加锁(真正有效的锁),而暂停所有线程的太重了,所以基本上只有自己确定某个函数不存在多线程问题或者无需调用原函数才可用。写个demo,熟悉下hook还行,真的实际使用是不行的。
也是因为这个问题,基本上目前的inline hook都会避免再次覆盖指令。不能覆盖回去,那么就直接执行备份的指令,执行后再跳到跳板0之后再继续执行。
也确实是可行的,大部分指令是可以这么做的,但是也有例外。比如备份的指令中有b/bl指令跳到一个偏移地址,跳转到的地址等于当前地址+偏移。而备份后的指令取得当前地址肯定是不等于原来的当前地址的,所以就跳到错误的地址去执行了。
好在我们可以进行修复,计算出原来要跳到的绝对地址,把这条b指令替换成ldr pc指令。其他指令的一些修复也是类似的道理。
四种hook形式应用不同的场景
dump读写寄存器、堆栈–一种hook形式
Android_Inline_Hook就是这样的实现,只能接收读写寄存器和堆栈,原函数还运行。那么其实这种方式主要作用就是读写参数寄存器、栈,不能读写函数返回值,如果是不受参数控制流程的函数就无能为力了(当然是可以逆向分析,在已经得到返回值的指令处hook,但是应用场景太小了)。
那么实现的核心就是,跳板0->dumpshellcode。dumpshellcode把寄存器以数组的形式存放(栈就是天然的数组),把这个数组传递给dump函数,dump函数接收处理寄存器(未生效)。dump函数返回shellcode,恢复数组数据到寄存器,完成恢复或者修改。执行backupshellcode(备份的原函数的开头部分,修复pc,跳回原函数之后的部分)完成原函数的执行流程。
替换/拦截函数,接收处理参数,调用原函数、读写返回值–一种hook形式
最常用、最符合使用习惯的的方式。
实现的核心,跳板0->funshellcode。funshellcode可以在上面的dumpshellcode基础上实现也可以全新实现。
在dumpshellcode基础上可以取巧一些,前面的保存dump寄存器保留,把后面的执行backupshellcode换成执行hook函数。
全新实现就是不保存寄存器,那么就可以把hook函数放在跳板shellcode中,直接跳到hook函数;也可以跳板0->funshellcode,funshellcode中再跳转到hook函数。
之后就进入hook函数,如果不调用原函数,直接返回一个返回值或者void函数什么都不做即可(如果是参数也做返回值的情况就修改参数)。如果调用原函数,就通过一个结构体/map等查询被hook函数地址对应的backupshellcode,把backupshellcode转成函数指针,传参调用,即可完成原函数的调用、或者返回值。
dump读写寄存器、堆栈,调用原函数、读写返回值/寄存器–一种hook形式
在dump的基础上,执行backupshellcode(备份的原函数的开头部分,修复pc,跳回原函数之后的部分)完成原函数的执行流程之后返回到dumpshellcode,调用另一个dump函数,读写返回值(r0/x0,r1/x1寄存器)。不同于dump的地方在于如果要返回到dumpshellcode,那么在调用backupshellcode之前应该备份原来的lr寄存器。考虑到多线程的问题,肯定是不能用一个固定的变量/地址去存储lr寄存器的,可能被覆盖,同一个线程哪怕是递归调用函数也是有顺序的,所以每个线程的被hook函数使用一个顺序的容器保存lr,后进先出。
dump读写寄存器、堆栈,读写返回值/寄存器–一种hook形式
在dump的基础上,不执行backupshellcode(备份的原函数的开头部分,修复pc,跳回原函数之后的部分),直接修改r0/x0、r1/x1寄存器,返回。和dump函数很多地方是一致的,应用于只需要返回值,并不需要原函数执行的情况。
以上四种场景应该是足够满足hook的需要了。
arm64 实现难点
因为无法直接操作pc,那么实现跳转(通用情况)需要占用一个寄存器。要么使用一个不会被使用的寄存器(哪有绝对不会被使用的寄存器),要么先保存这个寄存器,通过栈保存(之前就是忽略了这个问题在固定地址保存寄存器,那么多线程情况下就可能被覆盖),跳过去之后先恢复这个寄存器。例如:
stp X1, X0, [SP, #-0x10];//保存寄存器
...
ldr x0, [sp, #-0x8];//恢复x0寄存器
对应shellcode我们很容易实现,但是如果是c/c++函数(我们的hook函数)就麻烦了,直接在函数开头插代码肯定是不行的。在源码中函数第一行内嵌汇编恢复寄存器?很可惜,这种只在无参、无返回值(空实现)、只有内嵌汇编的情况会成功,其他情况下源码中的第一行并不是汇编中的第一行。。。
所以似乎陷入了无解的状态,在llvm中函数开头插指令?似乎太重了。所以回到原点了,就要考虑真的没有不使用寄存器跳转的可能吗?其实还是有的,b或者bl到偏移
ARM64:
B:0x17向前跳转,0x14向后跳转
BL:0x97向前跳转 0x94向后跳转
偏移地址计算过程:
(目标地址 - 指令地址)/ 4 = 偏移
// 减8,指令流水造成。
// 除4,因为指令定长,存储指令个数差,而不是地址差。
完整指令:
.text:000000000008CC84 8D B3 FF 97 BL je_arena_malloc_hard
.text:0000000000079AB8 je_arena_malloc_hard
计算偏移:
(79AB8-8CC84) / 4 = FFFFFFFFFFFFB38D
FFB38D | 0x97000000 = 97FFB38D
所以理论上,如果被hook的函数和hook函数/跳板/shellcode的距离在正负67108860/64m的范围内,64m=67108864,还有0,比如BL 0=00000094。那么这样就可以省一个寄存器了,针对arm64不能直接操作pc的问题,这应该是一个解决方案。
那么单指令hook除了异常的方式是不是就是指的这种方式呢?只需要覆盖一条指令,关键是怎么确保被hook函数和hook函数地址在正负64m内呢。怎么能申请到这块地址的内存呢。
也许可以查看proc/pid/maps,在要hook的so附近寻找未使用的空间,然后使用mmap申请,不确定是否可行。
暂时可用的一些方式如下(最终未采用):
自定义section,增加蹦床,可读写执行
实际上到内存中还是被和代码段放在一起都是可读可执行,没有写的权限。
.section ".my_sec", "awx"
.global _myopen_start
//.text
_myopen_start:
ldr x0, [sp, #-0x8];
b my_open;
.end
//用于手动生成蹦床代码,因为hook代码和这个蹦床一起编译的,所以基本上不会超出加减64m,那么就可以使用b跳转
//到偏移,就不用占用一个寄存器了。需要每增加一个hook函数,就加一个蹦床,相应的生成shellcode中跳转到hook
//函数的地方都要改成这个蹦床的地址。难就难在这个不好通过内嵌汇编实现,因为b跟的是个偏移值,在汇编中可以使用
//函数名称,编译器替换,但是内嵌汇编中不行。而自己计算
shellcode如下,
//用于演示,因为不是最终方案,不写完整代码了。
//修改trampoline指向蹦床即可
.data
replace_start:
ldr x0, trampoline; //如果每一个函数都在源码中新建一个shellcode的话,而不是动态复制生成,那么这个shellcode和蹦床可以合为一个。
br x0; //不能改变lr寄存器
trampoline: //蹦床的地址
.double 0xffffffffffffffff
replace_end:
awx指定读写执行,在elf中(ida查看)确实是读写执行。如果这个section中仅包含变量,那么在内存中放在可读写的段;如果存在代码或者代码和变量均存在,都是和代码段一样仅可读执行。那么在单独的section中存放蹦床代码意义也不大了,还是需要调用mprotect修改内存权限。不过考虑到如果放在仅可读写的段中,那么万一映射后的rw-p和r-xp超过了64m,不就白瞎了,所以还是以代码或者至少这个section中存在一个函数,保证和被hook的函数都在r-xp。
77c5545000-77c557c000 r-xp 00000000 103:37 533353 /data/app/com.zhuotong.myinhk-TRvqt0ReHRjQeeAnpXDnFQ==/lib/arm64/libInlineHook.so
77c558b000-77c558e000 r--p 00036000 103:37 533353 /data/app/com.zhuotong.myinhk-TRvqt0ReHRjQeeAnpXDnFQ==/lib/arm64/libInlineHook.so
77c558e000-77c558f000 rw-p 00039000 103:37 533353 /data/app/com.zhuotong.myinhk-TRvqt0ReHRjQeeAnpXDnFQ==/lib/arm64/libInlineHook.so
而如果直接和.text一起,主要是怕对这块内存修改权限引发什么异常,不确定如果代码正在执行,修改权限是否会出问题,所以最好能仅修改蹦床的区间。而且不确定是否有只能修改为读写和读执行的系统,所以可能要先修改成读写,写了之后再修改成读执行?这样会不会也有几率触发问题,但是如果每个蹦床都分配一个页的内存也不现实。。。
内嵌汇编,在自定义section中增加蹦床
这样定义一个无参无返回的函数,倒是可以使这个函数就是内嵌汇编,但是不确定如果ollvm混淆等是否会被拆分、加入垃圾代码等。
而且问题在于怎么自动生成一个蹦床函数。宏定义?那这个宏要在函数之外,不太容易自动化实现,包括蹦床的函数名称、b后面的函数名称。和上面的汇编中添加一个蹦床一样的问题,除非自己实现预处理之类的自动插入汇编代码、宏等,似乎不太现实。。。
似乎很难实现自动化,手动写代码太麻烦且和arm的部分接口、行为不一致。但是如果自己简单测试、不在乎这些也是可以的。
在自定义section中预留蹦床空间
unsigned int hkArry[256*2] __attribute__ ((section(".my_sec")));
//下面为伪代码,通过这样也可以实现不修改/保存寄存器、不修改hook函数,动态生成蹦床完成hook。
//优点是不用保存寄存器,缺点是因为正负64m的限制,hook函数应该和hook库是一起编译成动态库/可执行文件的。不能单独使用hook库。
//256个蹦床,实际使用可能要考虑这个hkArry内存对齐的问题(如果编译器未做内存对齐)。
//unsigned int hkArry[256 * 2] __attribute__ ((section(".my_sec")));
//void test_trampoline(){
//仅用于演示,应该先设置内存为可读写/执行,
// hkArry[0] = 0xf85f83e0;//或者memcpy, ldr x0, [sp, #-0x8];
//例如这样计算偏移,组合b指令。
// unsigned long offset = (unsigned long) my_open - ((unsigned long) hkArry + 1 * 4);
// hkArry[1] = 0x14000000;//计算偏移,生成指令,b 0;
//}
256个蹦床。动态生成蹦床代码,第一条指令固定,第二条指令计算生成。需要hkArry修改为读写执行。
未实现
so的r-xp中应该是有未使用的多余的内存的,为了对齐、页面等,所以怎么确定多余的空间大小和位置,然后蹦床代码存放其中。
为什么尽量不使用x16、x17等不用保存的寄存器?
因为种种限制,所以最后采用的还是保存一个寄存器,而使用哪个寄存器呢,我是选的lr寄存器。为什么不用x16、x17等不用保存的寄存器?这里就涉及到一个标准和规范的问题。
理论上编译器编译的c/c++函数是遵守这个规范的,要么不使用x16、x17寄存器,要么只是临时中转,不会在调用其他函数之后再从x16、x17寄存器取值(因为其他函数可能改变x16、x17),但是内嵌汇编(虽然指定不使用x16、x17寄存器,但还是被编译器使用了)或者人工写的汇编,虽然不常见,但确实存在。而最常见的是plt函数内都是使用x16、x17做中转。所以使用一个不一定会被保存的寄存器不如使用一个会被保存的寄存器。而因为lr寄存器的特殊性,一般使用的话都会先压栈保存,结束恢复(32位不一定恢复,64位是会恢复lr寄存器的,因为不能直接操作pc,多数都是恢复lr,再ret)
所以其实主要还是兼容性考虑,尽量采用一些绕弯的方式不改变任何寄存器,实在没办法的情况下或者标准c/c++函数、非函数任意位置hook的情况下才使用x16、x17寄存器。
解析寄存器、栈取出参数,调用hook函数
难点在于:
1、不知道参数个数、参数类型。
2、需要确定各种类型参数占几个寄存器、可变参数等
其实在源码中定义hook函数的时候是有函数原型的,但是运行时拿不到。忽然想到"c++"函数名的规则,里面包含参数、返回值类型,似乎可行,但是很多情况并不希望导出函数,而且也并不一定都是c++实现的。
那么如果定义一个接口,传入函数原型也不是不行,基本类型就用相应的字符或者枚举标识,其他所有的都是void*指针。可还是怕碰见可变参数函数,不确定参数个数,参数类型,只有接收/实现函数才知道。所以似乎不太可行。
而libffi似乎不太适合这个情况,也是需要明确指令参数和返回类型,还要传输参数。
目前的实现没有经过自己解析参数,只是中转,通过定义一致的函数原型,让编译器帮助我们解析参数。
代码实现_arm64_统一的跳板0
因为arm64限制条件最多,那么应该先实现arm64,这样才能更好的保证api的通用/一致性,因为arm64不能操作pc不得不这么做,arm32同样也可以这么做,但是如果先实现arm32,可能就先入为主的设计一些arm32可用的api。这算是一种架构思维吧,或者多思考一下就明白了。
STP X1, X0, [SP, #-0x10];//因为要使用一个寄存器,所以先保存,当然不一定是x0
LDR X0, 8; //把shellcode/hook函数地址存到x0
BR X0; //执行shellcode/hook函数
ADDR(64)
LDR X0, [SP, -0x8];//因为不能操作pc,所以跳回来的时候免不了也要使用一个寄存器
其实很简单,注释基本都说明了,使用了24字节,所以如果能确定要hook的函数是标准的c/c++函数,不会使用x16、x17去保存值的话也可以这样,使用16字节,降低被hook函数太短失败的概率。
LDR X17, 8;
BR x17;
ADDR(64)
代码实现_arm64_dump函数
.include "../../asm/base.s"
//.extern _dump_start
//.extern _dump_end
//.extern _hk_info
//.set _dump_start, r_dump_start
//.set _dump_end, r__dump_end
//.set _hk_info, r__hk_info
//.global _dump_start
//.global _dump_end
//.global _hk_info
//.hidden _dump_start
//.hidden _dump_end
//.hidden _hk_info
//可用于标准的c/c++函数、非标准函数、函数的一部分(用于读写寄存器),前提都是字节长度足够
//非标准函数即非c/c++编译的函数,那么手写汇编可能存在并不遵守约定的情况,比如我们使用了sp寄存器,并在未使用的栈上保存寄存器
//但是可能不是满递减而是反过来满递增,或者不遵守栈平衡,往栈上写数据,但是并不改变sp寄存器。当然应该是很少见的。
.data
_dump_start: //用于读写寄存器/栈,需要自己解析参数,不能读写返回值,不能阻止原函数(被hook函数)的执行