本文为看雪论坛精华文章
看雪论坛作者ID:卓桐
[原创]如何写一个Android inline hook框架
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、修复。
后记: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:androidNDKandroid-ndk-r16b
C:UsersEDZAppDataLocalAndroidSdkcmake3.10.2.4988404bincmake .. -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:appmingw-w64x86_64-8.1.0-posix-seh-rt_v6-rev0mingw64binmingw32-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,在OneLibstdcxxhttp://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中增加蹦床
![f9284ea6fa02e4f98cf1bdf216baf7ff.png](https://i-blog.csdnimg.cn/blog_migrate/c9d0daf53972b99682ac3e93279c857a.png)
这样定义一个无参无返回的函数,倒是可以使这个函数就是内嵌汇编,但是不确定如果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修改为读写执行。
![3a14c8ce24e3c37d09a06e11abe61baa.png](https://i-blog.csdnimg.cn/blog_migrate/04bae020db9d0980e700410a99d411c9.jpeg)
未实现
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函数)的执行
//从行为上来我觉得更偏向dump,所以起名为dump。
sub sp, sp,
#0x20; //跳板在栈上存储了x0、x1,但是未改变sp的值
mrs x0, NZCV
str
x0, [sp,
#0x10]; //覆盖跳板存储的x1,存储状态寄存器
str
x30, [sp];
//存储x30
add x30, sp,
#0x20
str
x30, [sp,
#0x8]; //存储真实的sp
ldr x0, [sp,
#0x18]; //取出跳板存储的x0
save_x0_x29://保存寄存器x0-x29
sub sp, sp,
#0xf0; //分配栈空间
stp X0, X1, [SP];
//存储x0-x29
stp X2, X3, [SP,#0x10]
stp X4, X5, [SP,#0x20]
stp X6, X7, [SP,#0x30]
stp X8, X9, [SP,#0x40]
stp X10, X11, [SP,#0x50]
stp X12, X13, [SP,#0x60]
stp X14, X15, [SP,#0x70]
stp X16, X17, [SP,#0x80]
stp X18, X19, [SP,#0x90]
stp X20, X21, [SP,#0xa0]
stp X22, X23, [SP,#0xb0]
stp X24, X25, [SP,#0xc0]
stp X26, X27, [SP,#0xd0]
stp X28, X29, [SP,#0xe0]
call_onPreCallBack://调用onPreCallBack函数,第一个参数是sp,第二个参数是STR_HK_INFO结构体指针
mov x0, sp;
//x0作为第一个参数,那么操作x0=sp,即操作栈读写保存的寄存器
ldr x1, _hk_info;
ldr x3, [x1];
//onPreCallBack
bl get_lr_pc;
//lr为下条指令
add lr, lr,
#8; //lr为blr x3的地址
str
lr, [sp,
#0x108]; //lr当作pc,覆盖栈上的x0
blr x3
restore_regs://恢复寄存器
ldr x0, [sp,
#0x100]; //取出状态寄存器
msr NZCV, x0
ldp X0, X1, [SP];
//恢复x0-x29寄存器
ldp X2, X3, [SP,#0x10]
ldp X4, X5, [SP,#0x20]
ldp X6, X7, [SP,#0x30]
ldp X8, X9, [SP,#0x40]
ldp X10, X11, [SP,#0x50]
ldp X12, X13, [SP,#0x60]
ldp X14, X15, [SP,#0x70]
ldp X16, X17, [SP,#0x80]
ldp X18, X19, [SP,#0x90]
ldp X20, X21, [SP,#0xa0]
ldp X22, X23, [SP,#0xb0]
ldp X24, X25, [SP,#0xc0]
ldp X26, X27, [SP,#0xd0]
ldp X28, X29, [SP,#0xe0]
add sp, sp,
#0xf0
ldr x30, [sp];
//恢复x30
add sp, sp,
#0x20; //恢复为真实sp
call_oriFun:
stp X1, X0, [SP,
#-0x10]; //因为跳转还要占用一个寄存器,所以保存
ldr x0, _hk_info;
ldr x0, [x0,
#8]; //pOriFuncAddr
br x0
get_lr_pc:
ret;
//仅用于获取LR/PC
_hk_info:
//结构体STR_HK_INFO
.double
0xffffffffffffffff
_dump_end:
.end
基本上注释已经说明了。_hk_inf为如下结构体的指针,onPreCallBack函数原型如下。
//hook信息
typedef struct STR_HK_INFO{
void (*onPreCallBack)(struct my_pt_regs
*, struct STR_HK_INFO
*pInfo);
//回调函数,在执行原函数之前获取参数/寄存器的函数
void
*
pOriFuncAddr;
//存放备份/修复后原函数的地址
void (*pre_callback)(struct my_pt_regs
*, struct STR_HK_INFO
*pInfo);
//pre_callback,内部做保存lr的操作,之后回调onPreCallBack,不能被用户操作
void (*onCallBack)(struct my_pt_regs
*, struct STR_HK_INFO
*pInfo);
//回调函数,执行原函数之后获取返回值/寄存器的函数
void (*aft_callback)(struct my_pt_regs
*, struct STR_HK_INFO
*pInfo);
//aft_callback,内部做恢复lr的操作,之后回调onCallBack,不能被用户操作
void
*pHkFunAddr;
//hook函数,即自定义按照被hook的函数原型构造,处理参数/返回值的函数
//以上为在shellcode中通过偏移直接或间接用到的,所以如果有变动,相应的shellcode也要跟着变动
void
**ppHkFunAddr;
//上面pHkFunAddr函数在shellcode中的地址,已废弃;
void
**hk_infoAddr;
//shellcode中HK_INFO的地址
void
*pBeHookAddr;
//被hook的地址/函数
void
*pStubShellCodeAddr;
//跳过去的shellcode stub的地址
size_t shellCodeLength;
//上面pStubShellCodeAddr的字节数
void
**
ppOriFuncAddr;
//shellcode 中存放备份/修复后原函数的地址,已废弃,待去除;改成上面pHkFunAddr函数的指针,应用于解除hook
void
*pNewEntryForOriFuncAddr;
//和pOriFuncAddr一致
BYTE szbyBackupOpcodes[OPCODEMAXLEN];
//原来的opcodes
int
backUpLength;
//备份代码的长度,arm64模式下为24
int
backUpFixLengthList[BACKUP_CODE_NUM_MAX];
//保存
const char*
methodName;
} HK_INFO;
#if defined(__aarch64__)
struct my_pt_regs {
__u64 uregs[31];
__u64 sp;
__u64 pstate;
//有时间应该修复,pc在前,但是涉及到栈和生成shellcode都要改,先这么用吧,和系统结构体有这点不同
__u64 pc;
};
#define ARM_lr uregs[30]
#define ARM_sp sp
//#define SP (__u64*)sp
//#define SP_32(i) *(__u32*)((__u64)(regs->sp) + i*4)
//#define SP_32(i) *((__u32*)regs->sp+i)
//#define SP_32(i) *((__u64*)regs->sp+i)
#define SP(i) *((__u64*)regs->sp+i)
#elif defined(__arm__)
struct my_pt_regs {
long
uregs[18];
};
#ifndef __ASSEMBLY__
#define ARM_cpsr uregs[16]
#define ARM_pc uregs[15]
#define ARM_lr uregs[14]
#define ARM_sp uregs[13]
#define ARM_ip uregs[12]
#define ARM_fp uregs[11]
#define ARM_r10 uregs[10]
#define ARM_r9 uregs[9]
#define ARM_r8 uregs[8]
#define ARM_r7 uregs[7]
#define ARM_r6 uregs[6]
#define ARM_r5 uregs[5]
#define ARM_r4 uregs[4]
#define ARM_r3 uregs[3]
#define ARM_r2 uregs[2]
#define ARM_r1 uregs[1]
#define ARM_r0 uregs[0]
#define ARM_ORIG_r0 uregs[17]
#define ARM_VFPREGS_SIZE (32 * 8 + 4)
#endif
#define SP(i) *((__u32*)regs->ARM_sp+i)
#endif
my_pt_regs对应在栈上存放的寄存器。这里面其他寄存器都容易保存,pc寄存器因为不能直接操作,所以要取巧一些。我是利用bl跳转之前会把下一条指令的地址存放到lr寄存器,那么再跳回读取lr寄存器即可。
bl get_lr_pc;
//lr为下条指令
add lr, lr,
#8; //lr为blr x3的地址
str
lr, [sp,
#0x108]; //lr当作pc,覆盖栈上的x0
blr x3
...
get_lr_pc:
ret;
//仅用于获取LR/PC
当然这里保存pc寄存器其实也不是必须的。
dump_demo
/**
*
用户自定义的stub函数,嵌入在hook点中,可直接操作寄存器等
*
@param regs 寄存器结构,保存寄存器当前hook点的寄存器信息
*
@param pInfo 保存了被hook函数、hook函数等的结构体
*/
void onPreCallBack(my_pt_regs
*regs, HK_INFO
*pInfo)
//参数regs就是指向栈上的一个数据结构,由第二部分的mov r0, sp所传递。
{
const char*
name
=
"null";
if
(pInfo) {
if
(pInfo->methodName) {
name
=
pInfo->methodName;
}
else
{
char buf[20];
sprintf(buf,
"%p", pInfo->pBeHookAddr);
name
=
buf;
}
}
//
LE("tid=%d onPreCallBack:%s", gettid(), name);
#if defined(__aarch64__)
LE("tid=%d, onPreCallBack:%s, "
"x0=0x%llx, x1=0x%llx, x2=0x%llx, x3=0x%llx, x4=0x%llx, x5=0x%llx, x6=0x%llx, x7=0x%llx, x8=0x%llx, x9=0x%llx, x10=0x%llx,"
" x11=0x%llx, x12=0x%llx, x13=0x%llx, x14=0x%llx, x15=0x%llx, x16=0x%llx, x17=0x%llx, x18=0x%llx, x19=0x%llx, x20=0x%llx, "
"x21=0x%llx, x22=0x%llx, x23=0x%llx, x24=0x%llx, x25=0x%llx, x26=0x%llx, x27=0x%llx, x28=0x%llx, x29/FP=0x%llx, x30/LR=0x%llx, "
"cur_sp=%p, ori_sp=%p, ori_sp/31=0x%llx, NZCV/32=0x%llx, x0/pc/33=0x%llx, cur_pc=%llx, arg8=%x, arg9=%x, arg10=%x, arg11=%x, "
"arg12=%x, arg13=%x;"
, gettid(), name,
regs->uregs[0], regs->uregs[1], regs->uregs[2], regs->uregs[3], regs->uregs[4], regs->uregs[5],
regs->uregs[6], regs->uregs[7], regs->uregs[8], regs->uregs[9], regs->uregs[10], regs->uregs[11],
regs->uregs[12], regs->uregs[13], regs->uregs[14], regs->uregs[15], regs->uregs[16], regs->uregs[17],
regs->uregs[18], regs->uregs[19], regs->uregs[20], regs->uregs[21], regs->uregs[22], regs->uregs[23],
regs->uregs[24], regs->uregs[25], regs->uregs[26], regs->uregs[27], regs->uregs[28], regs->uregs[29], regs->uregs[30],
regs, ((char*)regs
+
0x110), regs->uregs[31], regs->uregs[32], regs->uregs[33], regs->pc,
SP(0), SP(1), SP(2), SP(3), SP(4), SP(5)
);
#elif defined(__arm__)
LE("tid=%d, onPreCallBack:%s, "
"r0=0x%lx, r1=0x%lx, r2=0x%lx, r3=0x%lx, r4=0x%lx, r5=0x%lx, r6=0x%lx, r7=0x%lx, r8=0x%lx, r9=0x%lx, r10=0x%lx, r11=0x%lx, r12=0x%lx, "
"cur_sp=%p, ori_sp=%p, ori_sp/13=0x%lx, lr=0x%lx, cur_pc=0x%lx, cpsr=0x%lx, "
"arg4=0x%lx, arg5=0x%lx, arg4=0x%lx, arg5=0x%lx;"
, gettid(), name,
regs->uregs[0], regs->uregs[1], regs->uregs[2], regs->uregs[3], regs->uregs[4], regs->uregs[5],
regs->uregs[6], regs->uregs[7], regs->uregs[8], regs->uregs[9], regs->uregs[10], regs->uregs[11],
regs->uregs[12],
regs, ((char*)regs
+
0x44), regs->uregs[13], regs->uregs[14], regs->uregs[15], regs->uregs[16],
regs->uregs[17], regs->uregs[18], SP(0), SP(1)
);
#endif
if
(pInfo) {
LE("onPreCallBack: HK_INFO=%p", pInfo);
if
(pInfo->pBeHookAddr
==
open
&& regs->uregs[0]) {
const char*
name
=
(const char
*)(regs->uregs[0]);
LE("onPreCallBack: open: %s , %o, %o", name, regs->uregs[1], (mode_t)regs->uregs[2]);
}
}
}
void test_dump(){
LE("open=%p, callback=%p",
open, onPreCallBack);
if
(dump((void
*)(open), onPreCallBack, NULL,
"open") !=
success) {
LE("hook open error");
}
int
fd
=
open("/system/lib/libc.so", O_RDONLY);
LE("open /system/lib/libc.so, fd=%d", fd);
}
自定义onPreCallBack修改寄存器即可。如果是一个函数,那么能控制的是参数,不能阻止原函数的调用。
代码实现_arm64_dump_with_ret函数
//.include
"../../asm/base.s"
.global
r_dump_start
.global
r_dump_end
.global
r_hk_info
.hidden r_dump_start
.hidden r_dump_end
.hidden r_hk_info
.data
r_dump_start:
//用于读写寄存器/栈,需要自己解析参数,不能读写返回值,不能阻止原函数(被hook函数)的执行
//从行为上来我觉得更偏向dump,所以起名为dump。
sub sp, sp,
#0x20; //跳板在栈上存储了x0、x1,但是未改变sp的值
mrs x0, NZCV
str
x0, [sp,
#0x10]; //覆盖跳板存储的x1,存储状态寄存器
str
x30, [sp];
//存储x30
add x30, sp,
#0x20
str
x30, [sp,
#0x8]; //存储真实的sp
ldr x0, [sp,
#0x18]; //取出跳板存储的x0
save_x0_x29://保存寄存器x0-x29
sub sp, sp,
#0xf0; //分配栈空间
stp X0, X1, [SP];
//存储x0-x29
stp X2, X3, [SP,#0x10]
stp X4, X5, [SP,#0x20]
stp X6, X7, [SP,#0x30]
stp X8, X9, [SP,#0x40]
stp X10, X11, [SP,#0x50]
stp X12, X13, [SP,#0x60]
stp X14, X15, [SP,#0x70]
stp X16, X17, [SP,#0x80]
stp X18, X19, [SP,#0x90]
stp X20, X21, [SP,#0xa0]
stp X22, X23, [SP,#0xb0]
stp X24, X25, [SP,#0xc0]
stp X26, X27, [SP,#0xd0]
stp X28, X29, [SP,#0xe0]
call_onPreCallBack://调用onPreCallBack函数,第一个参数是sp,第二个参数是STR_HK_INFO结构体指针
mov x0, sp;
//x0作为第一个参数,那么操作x0=sp,即操作栈读写保存的寄存器
ldr x1, r_hk_info;
ldr x3, [x1,
#0x10]; //pre_callback
bl get_lr_pc;
//lr为下条指令
add lr, lr,
8;
//lr为blr x3的地址
str
lr, [sp,
#0x108]; //lr当作pc,覆盖栈上的x0
blr x3
to_call_oriFun:
ldr x0, [sp,
#0x100]; //取出状态寄存器
msr NZCV, x0
ldp X0, X1, [SP];
//恢复x0-x29寄存器
ldp X2, X3, [SP,#0x10]
ldp X4, X5, [SP,#0x20]
ldp X6, X7, [SP,#0x30]
ldp X8, X9, [SP,#0x40]
ldp X10, X11, [SP,#0x50]
ldp X12, X13, [SP,#0x60]
ldp X14, X15, [SP,#0x70]
ldp X16, X17, [SP,#0x80]
ldp X18, X19, [SP,#0x90]
ldp X20, X21, [SP,#0xa0]
ldp X22, X23, [SP,#0xb0]
ldp X24, X25, [SP,#0xc0]
ldp X26, X27, [SP,#0xd0]
ldp X28, X29, [SP,#0xe0]
add sp, sp,
#0xf0
ldr x30, [sp];
//恢复x30
add sp, sp,
#0x20; //恢复为真实sp
stp X1, X0, [SP,
#-0x10]; //因为跳转还要占用一个寄存器,所以保存
ldr x0, r_hk_info;
ldr x0, [x0,
#8]; //pOriFuncAddr
blr x0;
to_aft_callback:
//有时间再把这部分代码优化掉,是可以再跳转到开头的,因为这部分代码都是一样的,判断lr可以区分出来的
STP X1, X0, [SP,
#-0x10]
sub sp, sp,
#0x20; //跳板在栈上存储了x0、x1,但是未改变sp的值
mrs x0, NZCV
str
x0, [sp,
#0x10]; //覆盖跳板存储的x1,存储状态寄存器
str
x30, [sp];
//存储x30
add x30, sp,
#0x20
str
x30, [sp,
#0x8]; //存储真实的sp
ldr x0, [sp,
#0x18]; //取出跳板存储的x0
sub sp, sp,
#0xf0; //分配栈空间
stp X0, X1, [SP];
//存储x0-x29
stp X2, X3, [SP,#0x10]
stp X4, X5, [SP,#0x20]
stp X6, X7, [SP,#0x30]
stp X8, X9, [SP,#0x40]
stp X10, X11, [SP,#0x50]
stp X12, X13, [SP,#0x60]
stp X14, X15, [SP,#0x70]
stp X16, X17, [SP,#0x80]
stp X18, X19, [SP,#0x90]
stp X20, X21, [SP,#0xa0]
stp X22, X23, [SP,#0xb0]
stp X24, X25, [SP,#0xc0]
stp X26, X27, [SP,#0xd0]
stp X28, X29, [SP,#0xe0]
mov x0, sp;
//x0作为第一个参数,那么操作x0=sp,即操作栈读写保存的寄存器
ldr x1, r_hk_info;
ldr x3, [x1,
#0x20]; //aft_callback
bl get_lr_pc;
//lr为下条指令
add lr, lr,
8;
//lr为blr x3的地址
str
lr, [sp,
#0x108]; //lr当作pc,覆盖栈上的x0
blr x3;
//执行aft_callback
to_popreg:
ldr x0, [sp,
#0x100]; //取出状态寄存器
msr NZCV, x0
ldp X0, X1, [SP];
//恢复x0-x29寄存器
ldp X2, X3, [SP,#0x10]
ldp X4, X5, [SP,#0x20]
ldp X6, X7, [SP,#0x30]
ldp X8, X9, [SP,#0x40]
ldp X10, X11, [SP,#0x50]
ldp X12, X13, [SP,#0x60]
ldp X14, X15, [SP,#0x70]
ldp X16, X17, [SP,#0x80]
ldp X18, X19, [SP,#0x90]
ldp X20, X21, [SP,#0xa0]
ldp X22, X23, [SP,#0xb0]
ldp X24, X25, [SP,#0xc0]
ldp X26, X27, [SP,#0xd0]
ldp X28, X29, [SP,#0xe0]
add sp, sp,
#0xf0
ldr x30, [sp];
//恢复x30
add sp, sp,
#0x20; //恢复为真实sp
//巧的是下条指令也是ret或者br lr,共用一条指令。
get_lr_pc:
ret;
//仅用于获取LR/PC
r_hk_info:
//结构体STR_HK_INFO
.double
0xffffffffffffffff
r_dump_end:
.end
和dump不同,在原函数执行后可以对返回值进行读写,且可以只处理参数或者只处理返回值。
因为要读写返回值,所以执行原函数之前需要修改lr寄存器,而读写返回值之后还要恢复正常的流程,那么lr寄存器是需要保存的。在这个shellcode或者结构体的一个字段存储lr都存在多线程覆盖的问题,所以使用和线程绑定的容器存储。那么何时保存呢?考虑到代码复用和最小的更改,那么可以在调用onPreCallBack函数内保存,但是这个函数是用户创建的,不应该让用户参与保存,而且这个onPreCallBack不一定存在。所以做一次中转,shellcode中先跳到一个一定存在的函数preCallBack,preCallBack内保存lr,并调用onPreCallBack(如果存在)。恢复lr也是同样的思路。
//头文件
#include <map>
#include <pthread.h>
#include <vector>
typedef std::vector<unsigned
long> LRS;
//static LRS lrs;
struct STR_LR {
};
typedef std::map<const void*, LRS*> LR_MAP;
//typedef std::map<pid_t, LR_MAP*> TID_MAP;
typedef std::map<pid_t, LR_MAP*> TID_MAP;
static TID_MAP
*
getTid_map();
static void saveLR(void*
key_fun, unsigned
long
lr);
static unsigned
long
getLR(void*
key_fun);
#endif
#ifdef __cplusplus
extern
"C"
{
#endif
#include "mhk.h"
//因为会被导出,所以两个静态库中存在两个同名函数,另一个被覆盖了。
//extern void pre_callback(struct my_pt_regs
*regs, HK_INFO*
info);
static void pre_callback(struct my_pt_regs
*regs, HK_INFO*
info);
//extern void aft_callback(struct my_pt_regs
*regs, HK_INFO*
info);
static void aft_callback(struct my_pt_regs
*regs, HK_INFO*
info);
typedef void (*callback)(struct my_pt_regs
*regs, HK_INFO*
info);
extern callback d_pre_callback;//
=
pre_callback;
extern callback d_aft_callback;//
=
aft_callback;
#ifdef __cplusplus
}
#endif
//头文件结束
static TID_MAP*
tid_map;
static pthread_mutex_t mutex
=
PTHREAD_MUTEX_INITIALIZER;
TID_MAP
*
getTid_map(){
//
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
if
(tid_map
==
NULL) {
tid_map
=
new TID_MAP;
}
pthread_mutex_unlock(&mutex);
return
tid_map;
}
//因为不清楚tid分配机制,是否存在已死亡线程的tid重新分配给新建线程,所以可以ls
-la
/proc/8205/task/根据线程时间
//判断是不是一个新线程,若是使用旧tid的新线程可以清空map,不过其实使用的栈结构存数据,理论上不区分也应该没问题的。
void saveLR(void*
key_fun, unsigned
long
lr){
pid_t tid
=
gettid();
TID_MAP
*map
=
getTid_map();
pthread_mutex_lock(&mutex);
auto it
=
map->find(tid);
if
(it
==
map->end()) {
auto lr_map
=
new LR_MAP;
auto ls
=
new LRS;
ls->push_back(lr);
lr_map->insert(std::make_pair(key_fun, ls));
map->insert(std::make_pair(tid, lr_map));
}
else
{
auto lr_map
=
it->second;
auto it_vt
=
lr_map->find(key_fun);
if
(it_vt
==
lr_map->end()) {
auto ls
=
new LRS;
ls->push_back(lr);
lr_map->insert(std::make_pair(key_fun, ls));
}
else
{
std::vector<unsigned
long>
*vt
=
it_vt->second;
vt->push_back(lr);
}
}
pthread_mutex_unlock(&mutex);
}
unsigned
long
getLR(void*
key_fun){
unsigned
long
lr
=
0;
pid_t tid
=
gettid();
TID_MAP
*map
=
getTid_map();
pthread_mutex_lock(&mutex);
auto it
=
map->find(tid);
if
(it
==
map->end()) {
LE("what's happened ? not found tid=%d", tid);//理论上不应该出现
}
else
{
auto lr_map
=
it->second;
auto it_vt
=
lr_map->find(key_fun);
if
(it_vt
==
lr_map->end()) {
LE("what's happened ? not found LR for=%p in tid=%d", key_fun, tid);//理论上不应该出现
}
else
{
std::vector<unsigned
long>
*vt
=
it_vt->second;
if
(!vt ||
/*vt->size() <=
0*/
vt->empty()) {
LE("what's happened ? null LR for=%p in tid=%d", key_fun, tid);
}
else
{
unsigned
long
size
=
vt->size();
lr
=
(*vt)[size
-
1];
vt->pop_back();
}
}
}
pthread_mutex_unlock(&mutex);
return
lr;
}
void pre_callback(struct my_pt_regs
*regs, HK_INFO*
info){
LE("dump_with_ret pre_callback");
saveLR(info->pBeHookAddr, regs->ARM_lr);
if
(info->onPreCallBack)
info->onPreCallBack(regs, info);
}
void aft_callback(struct my_pt_regs
*regs, HK_INFO*
info){
LE("dump_with_ret aft_callback");
unsigned
long
lr
=
getLR(info->pBeHookAddr);
regs->ARM_lr
=
lr;
if
(info->onCallBack)
info->onCallBack(regs, info);
}
callback d_pre_callback
=
pre_callback;
callback d_aft_callback
=
aft_callback;
目前是使用"c++"容器实现的,考虑到有些项目可能不能用c++,有时间再用c实现map、vector。
dump_with_ret_demo
void test_dump_with_ret(){
LE("open=%p, callback=%p",
open, onPreCallBack);
if
(dump((void
*)(open), onPreCallBack, onCallBack,
"open") !=
success) {
LE("hook open error");
}
int
fd
=
open("/system/lib/libc.so", O_RDONLY);
LE("open /system/lib/libc.so, fd=%d", fd);
}
void test_dump_ret(){
LE("open=%p, callback=%p",
open, onPreCallBack);
if
(dump((void
*)(open), NULL, onCallBack,
"open") !=
success) {
LE("hook open error");
}
int
fd
=
open("/system/lib/libc.so", O_RDONLY);
LE("open /system/lib/libc.so, fd=%d", fd);
}
代码实现_arm64_dump_just_ret函数
.global
j_dump_start
.global
j_dump_end
.global
j_hk_info
.hidden j_dump_start
.hidden j_dump_end
.hidden j_hk_info
.data
j_dump_start:
sub sp, sp,
#0x20; //跳板在栈上存储了x0、x1,但是未改变sp的值
mrs x0, NZCV
str
x0, [sp,
#0x10]; //覆盖跳板存储的x1,存储状态寄存器
str
x30, [sp];
//存储x30
add x30, sp,
#0x20
str
x30, [sp,
#0x8]; //存储真实的sp
ldr x0, [sp,
#0x18]; //取出跳板存储的x0
sub sp, sp,
#0xf0; //分配栈空间
stp X0, X1, [SP];
//存储x0-x29
stp X2, X3, [SP,#0x10]
stp X4, X5, [SP,#0x20]
stp X6, X7, [SP,#0x30]
stp X8, X9, [SP,#0x40]
stp X10, X11, [SP,#0x50]
stp X12, X13, [SP,#0x60]
stp X14, X15, [SP,#0x70]
stp X16, X17, [SP,#0x80]
stp X18, X19, [SP,#0x90]
stp X20, X21, [SP,#0xa0]
stp X22, X23, [SP,#0xb0]
stp X24, X25, [SP,#0xc0]
stp X26, X27, [SP,#0xd0]
stp X28, X29, [SP,#0xe0]
mov x0, sp;
//x0作为第一个参数,那么操作x0=sp,即操作栈读写保存的寄存器
ldr x1, j_hk_info;
ldr x3, [x1];
//onPreCallBack
bl get_lr_pc;
//lr为下条指令
add lr, lr,
8;
//lr为blr x3的地址
str
lr, [sp,
#0x108]; //lr当作pc,覆盖栈上的x0
blr x3
to_popreg:
ldr x0, [sp,
#0x100]; //取出状态寄存器
msr NZCV, x0
ldp X0, X1, [SP];
//恢复x0-x29寄存器
ldp X2, X3, [SP,#0x10]
ldp X4, X5, [SP,#0x20]
ldp X6, X7, [SP,#0x30]
ldp X8, X9, [SP,#0x40]
ldp X10, X11, [SP,#0x50]
ldp X12, X13, [SP,#0x60]
ldp X14, X15, [SP,#0x70]
ldp X16, X17, [SP,#0x80]
ldp X18, X19, [SP,#0x90]
ldp X20, X21, [SP,#0xa0]
ldp X22, X23, [SP,#0xb0]
ldp X24, X25, [SP,#0xc0]
ldp X26, X27, [SP,#0xd0]
ldp X28, X29, [SP,#0xe0]
add sp, sp,
#0xf0
ldr x30, [sp];
//恢复x30
add sp, sp,
#0x20; //恢复为真实sp
get_lr_pc:
ret;
//仅用于获取LR/PC
j_hk_info:
//结构体STR_HK_INFO
.double
0xffffffffffffffff
j_dump_end:
.end
和dump不同,不再执行原函数,可以直接修改x0、x1寄存器返回值或者什么都不做。这是四种方式中最简单的一种。
dump_just_ret_demo
void test_dump_just_ret(){
LE("open=%p, callback=%p",
open, onPreCallBack);
if
(dumpRet((void
*)(open), onCallBack,
"open") !=
success) {
LE("hook open error");
}
int
fd
=
open("/system/lib/libc.so", O_RDONLY);
LE("open /system/lib/libc.so, fd=%d", fd);
}
代码实现_arm64_replace函数
.global
replace_start
.global
replace_end
.global
p_hk_info
.hidden replace_start
.hidden replace_end
.hidden p_hk_info
.data
//这种方式尽量用于标准的c/c++函数,因为通过hook函数再调用原函数,只能保证参数寄存器和lr寄存器是一致的,其他寄存器可能被修改。
replace_start:
//如果只是替换/跳到hook函数,其实是不用保存寄存器的,只是重新写比较麻烦,所以在之前的基础上
sub sp, sp,
#0x20; //跳板在栈上存储了x0、x1,但是未改变sp的值
mrs x0, NZCV
str
x0, [sp,
#0x10]; //覆盖跳板存储的x1,存储状态寄存器
str
x30, [sp];
//存储x30
add x30, sp,
#0x20
str
x30, [sp,
#0x8]; //存储真实的sp
ldr x0, [sp,
#0x18]; //取出跳板存储的x0
sub sp, sp,
#0xf0; //分配栈空间
stp X0, X1, [SP];
//存储x0-x29
stp X2, X3, [SP,#0x10]
stp X4, X5, [SP,#0x20]
stp X6, X7, [SP,#0x30]
stp X8, X9, [SP,#0x40]
stp X10, X11, [SP,#0x50]
stp X12, X13, [SP,#0x60]
stp X14, X15, [SP,#0x70]
stp X16, X17, [SP,#0x80]
stp X18, X19, [SP,#0x90]
stp X20, X21, [SP,#0xa0]
stp X22, X23, [SP,#0xb0]
stp X24, X25, [SP,#0xc0]
stp X26, X27, [SP,#0xd0]
stp X28, X29, [SP,#0xe0]
mov x0, sp;
//x0作为第一个参数,那么操作x0=sp,即操作栈读写保存的寄存器
ldr x1, p_hk_info;
ldr x3, [x1,
#0x10]; //pre_callback,保存lr
bl get_lr_pc;
//lr为下条指令
add lr, lr,
8;
//lr为blr x3的地址
str
lr, [sp,
#0x108]; //lr当作pc,覆盖栈上的x0
blr x3
to_call_hkFun:
ldr x0, [sp,
#0x100]; //取出状态寄存器
msr NZCV, x0
ldp X0, X1, [SP];
//恢复x0-x29寄存器
ldp X2, X3, [SP,#0x10]
ldp X4, X5, [SP,#0x20]
ldp X6, X7, [SP,#0x30]
ldp X8, X9, [SP,#0x40]
ldp X10, X11, [SP,#0x50]
ldp X12, X13, [SP,#0x60]
ldp X14, X15, [SP,#0x70]
ldp X16, X17, [SP,#0x80]
ldp X18, X19, [SP,#0x90]
ldp X20, X21, [SP,#0xa0]
ldp X22, X23, [SP,#0xb0]
ldp X24, X25, [SP,#0xc0]
ldp X26, X27, [SP,#0xd0]
ldp X28, X29, [SP,#0xe0]
add sp, sp,
#0xf0
ldr x30, [sp];
//恢复x30
add sp, sp,
#0x20; //恢复为真实sp
ldr lr, p_hk_info;
ldr lr, [lr,
#0x28]; //pHkFunAddr
blr lr;
//既跳到pHkFunAddr执行,也设置了lr
to_aft_callback:
//其实这里可能存在问题,即如果hook函数或者其调用了原函数,非标准函数(非c/c++)未实现栈平衡
//比如手写的精心构造的汇编函数,可能存在覆盖栈上数据
STP X1, X0, [SP,
#-0x10]
sub sp, sp,
#0x20; //跳板在栈上存储了x0、x1,但是未改变sp的值
mrs x0, NZCV
str
x0, [sp,
#0x10]; //覆盖跳板存储的x1,存储状态寄存器
str
x30, [sp];
//存储x30
add x30, sp,
#0x20
str
x30, [sp,
#0x8]; //存储真实的sp
ldr x0, [sp,
#0x18]; //取出跳板存储的x0
sub sp, sp,
#0xf0; //分配栈空间
stp X0, X1, [SP];
//存储x0-x29
stp X2, X3, [SP,#0x10]
stp X4, X5, [SP,#0x20]
stp X6, X7, [SP,#0x30]
stp X8, X9, [SP,#0x40]
stp X10, X11, [SP,#0x50]
stp X12, X13, [SP,#0x60]
stp X14, X15, [SP,#0x70]
stp X16, X17, [SP,#0x80]
stp X18, X19, [SP,#0x90]
stp X20, X21, [SP,#0xa0]
stp X22, X23, [SP,#0xb0]
stp X24, X25, [SP,#0xc0]
stp X26, X27, [SP,#0xd0]
stp X28, X29, [SP,#0xe0]
mov x0, sp;
//x0作为第一个参数,那么操作x0=sp,即操作栈读写保存的寄存器
ldr x1, p_hk_info;
ldr x3, [x1,
#0x20]; //aft_callback, 恢复lr寄存器
bl get_lr_pc;
//lr为下条指令
add lr, lr,
8;
//lr为blr x3的地址
str
lr, [sp,
#0x108]; //lr当作pc,覆盖栈上的x0
blr x3;
//执行aft_callback
to_popreg:
ldr x0, [sp,
#0x100]; //取出状态寄存器
msr NZCV, x0
ldp X0, X1, [SP];
//恢复x0-x29寄存器
ldp X2, X3, [SP,#0x10]
ldp X4, X5, [SP,#0x20]
ldp X6, X7, [SP,#0x30]
ldp X8, X9, [SP,#0x40]
ldp X10, X11, [SP,#0x50]
ldp X12, X13, [SP,#0x60]
ldp X14, X15, [SP,#0x70]
ldp X16, X17, [SP,#0x80]
ldp X18, X19, [SP,#0x90]
ldp X20, X21, [SP,#0xa0]
ldp X22, X23, [SP,#0xb0]
ldp X24, X25, [SP,#0xc0]
ldp X26, X27, [SP,#0xd0]
ldp X28, X29, [SP,#0xe0]
add sp, sp,
#0xf0
ldr x30, [sp];
//恢复x30
add sp, sp,
#0x20; //恢复为真实sp
get_lr_pc:
br lr;
//仅用于获取LR/PC,最后相当于br lr
nop;
p_hk_info:
//结构体STR_HK_INFO
.double
0xffffffffffffffff
replace_end:
.end
和dump_with_ret大部分是一致的,执行原函数换成执行hook函数。因为这个hook函数不是shellcode,所以没有好办法在开头加入恢复寄存器的指令,那么就要使用一个无关的寄存器,因为要返回所以lr寄存器被保存了也改变了,所以这里就使用lr寄存器即可,blr lr,既跳到lr保存的地址去执行,也会把下条指令的地址存到lr。
replace_demo
typedef
int
(*old_open)(const char*
pathname,int
flags,/*mode_t mode*/...);
typedef
int
(*old__open)(const char*
pathname,int
flags,int
mode);
typedef
int
(*old__openat)(int
fd, const char
*pathname,
int
flags,
int
mode);
typedef
FILE*
(*old_fopen)(const char*
__path, const char*
__mode);
old_open
*ori_open;
old__open
*ori__open;
//可变参数的函数,需要自己按照被hook函数的逻辑,解析出参数再传递给原函数。
//因为并不清楚参数个数/类型,如果不改变参数的情况下还有方法不解析参数调用原函数。
//但是如果改变了参数,比如printf中的fmt,那么理论上后面的参数类型个数也应该改变,这种情况下应该是使用者
//已经清楚共用多少参数和类型,应该自己调用,而如果只改fmt,应该会出bug的。
//所以如果只是想打印明确类型的参数,不改变参数直接调用原函数的情况,应该实现下参数的解析重组/传递,待实现
int
my_open(const char*
pathname,int
flags, .../*int
mode*/){
mode_t mode
=
0;
if
(needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode
=
static_cast<mode_t>(va_arg(args,
int));
va_end(args);
}
LE("hk: open %s , %d, %d", pathname ? pathname :
"is null", flags, mode);
//测试解除hook
//
HK_INFO
*pInfo
=
isHookedByHkFun((void
*) (my_open));
//
unHook(pInfo);
int
fd
=
-1;
if
(!ori_open) {//理论上只有粗心没有给ori_open赋值。
LE("ori_open null !");
exit(1);
}
if
(!(*ori_open)) {//理论上应该是hook取消了,但是如果是自己忘了给ori_open赋值或者赋值为NULL,那就是自己的锅了,太粗心了,会陷入死循环。
LE("unhook open");
old_open tmp
=
open;
ori_open
=
&tmp;
}
fd
=
(*ori_open)(pathname, flags, mode);
LE("ori: open %s , fd=%d", pathname ? pathname :
"is null", fd);
return
fd;
}
void test_replace(){
LE("open=%p, callback=%p",
open, onPreCallBack);
const RetInfo info
=
dump_replace((void
*) (open), (void
*) (my_open), onPreCallBack,
onCallBack,
"open");
if
(info.status !=
success) {
LE("hook open error");
}
else
{
//考虑到解除hook的问题,不能用getOriFun直接获取备份的函数,应该获取函数的指针
//不然直接返回函数,free函数后无法知道。建议不用自己保存原函数,使用api获取,当然如果没有unhook的情况,就不需要考虑这些问题。
ori_open
=
(old_open*)(getPoriFun(info.info));
}
int
fd
=
open("/system/lib/libc.so", O_RDONLY);
LE("open /system/lib/libc.so, fd=%d", fd);
unHook(info.info);
}
FILE*
my_fopen(const char*
pathname,const char*
mode){
LE("hk: fopen %s , %s", pathname ? pathname :
"is null", mode);
FILE*
fd
=
NULL;
//理论上有可能存在取消hook了,但是hook函数还要执行,所以应该可以再回调原函数了。
//所以如果要绝对安全,那么hook和取消hook时暂停线程,检查函数调用栈是必须的。
auto ori_open
=
(old_fopen)(getOriFunByHkFun((void
*)(my_fopen)));
if
(!ori_open) {
ori_open
=
(old_fopen)fopen;
}
fd
=
ori_open(pathname, mode);
LE("ori: fopen %s , fd=%p", pathname ? pathname :
"is null", fd);
return
fd;
}
//9.0上arm64未导出__open函数,而arm下实现为:
//.text:0006FEDC
//.text:0006FEDC
EXPORT __open
//.text:0006FEDC
__open ; DATA XREF: LOAD:0000254C↑o
//.text:0006FEDC
; __unwind {
//.text:0006FEDC
PUSH {R7,LR}
//.text:0006FEDE
BLX j_abort
//.text:0006FEDE
; }
//
starts at
6FEDC
//.text:0006FEDE
; End of function __open
//open函数不再走__open,而是__openat,9.0上arm64未导出__openat函数(只是隐藏了符号,可以自己根据字符串解析出地址),arm导出。
void test_justReplace(){
LE("open=%p, callback=%p",
open, onPreCallBack);
//
const RetInfo info
=
dump_replace(dlsym(RTLD_DEFAULT,
"__openat"), (void
*) (my__open), NULL,
//
NULL,
"__open");
//android高版本没有__open了,__openat也隐藏符号了(只有64位隐藏了)
//
const RetInfo info
=
dump_replace(dlsym(RTLD_DEFAULT,
"__openat"), (void
*) (my__openat), NULL,
//
NULL,
"__openat");
const RetInfo info
=
dump_replace((void*)fopen, (void
*) (my_fopen), NULL,
NULL,
"fopen");
if
(info.status !=
success) {
LE("hook fopen error=%d", info.status);
}
FILE
*pFile
=
fopen("/system/lib/libc.so",
"re");
LE("fopen /system/lib/libc.so, fd=%p", pFile);
unHook(info.info);
}
api
以上就是4种hook方式和对应的实现。提供给用户的api接口如下:
#ifdef __cplusplus
#include <map>
#include <pthread.h>
#include <vector>
typedef std::vector<HK_INFO*> INFOS;
//typedef std::map<const void*, HK_INFO*> LR_MAP;
#endif
enum hk_status{
success, hooked, error
};
struct RetInfo {
enum hk_status status;
HK_INFO
*info;
};
//打印寄存器
/**
*
用户自定义的stub函数,嵌入在hook点中,可直接操作寄存器等
*
@param regs 寄存器结构,保存寄存器当前hook点的寄存器信息
*
@param pInfo 保存了被hook函数、hook函数等的结构体
*/
void default_onPreCallBack(my_pt_regs
*regs, HK_INFO
*pInfo);
/**
*
用户自定义的stub函数,嵌入在hook点中,可直接操作寄存器等
*
@param regs 寄存器结构,保存寄存器当前hook点的寄存器信息
*
@param pInfo 保存了被hook函数、hook函数等的结构体
*/
void default_onCallBack(my_pt_regs
*regs, HK_INFO
*pInfo);
/**
*
真实函数执行前调用onPreCallBack,执行后调用onCallBack,通过onPreCallBack控制参数,通过onCallBack控制返回值
*
@param pBeHookAddr 要hook的地址,必须
*
@param onPreCallBack 要插入的回调函数(读写参数寄存器), 可以为NULL(onCallBack不为空),当和onCallBack都为NULL的情况使用默认的打印寄存器的函数default_onPreCallBack,因为什么都不做为什么hook?
*
@param onCallBack 要插入的回调函数(读写返回值寄存器),可以为NULL,如果只关心函数执行后的结果
*
@param methodName 被hook的函数名称,可以为NULL。
*
@return
success:成功;error:错误;hooked:已被hook;
*/
hk_status dump(void
*pBeHookAddr, void (*onPreCallBack)(struct my_pt_regs
*, HK_INFO
*pInfo), void (*onCallBack)(struct my_pt_regs
*, struct STR_HK_INFO
*pInfo)
=
NULL, const char*
methodName
=
NULL);
/**
*
不执行真实函数,直接操作寄存器,之后恢复寄存器返回,理论上也是可以在onCallBack其中执行真实函数的,但是需要自己封装参数,调用后自己解析寄存器
*
@param pBeHookAddr 要hook的地址,必须
*
@param onCallBack 要插入的回调函数(读写参数寄存器),必须
*
@param methodName 被hook的函数名称,可以为NULL。
*
@return
success:成功;error:错误;hooked:已被hook;
*/
hk_status dumpRet(void
*pBeHookAddr, void (*onCallBack)(struct my_pt_regs
*, HK_INFO
*pInfo), const char*
methodName
=
NULL);
/**
*
针对标准函数,最常用的hook接口。定义一个和被hook函数原型一致的函数接收处理参数,可直接返回或者调用被备份/修复的原函数
*
@param pBeHookAddr 要hook的地址,必须
*
@param pHkFunAddr 和被hook函数原型一致的函数,接收处理参数,可直接返回或者调用被备份/修复的原函数,必须
*
@param onPreCallBack 要插入的回调函数(读写参数寄存器), 可以为NULL
*
@param onCallBack 要插入的回调函数(读写返回值寄存器),可以为NULL,如果只关心函数执行后的结果
*
@param methodName 被hook的函数名称,可以为NULL
*
@return
因为既要区分三种状态,还存在返回备份/修复的原函数的情况,使用结构体存储两个字段,参考demo。
*/
RetInfo dump_replace(void
*pBeHookAddr, void*pHkFunAddr, void (*onPreCallBack)(struct my_pt_regs
*, HK_INFO
*pInfo)
=
NULL, void (*onCallBack)(struct my_pt_regs
*, struct STR_HK_INFO
*pInfo)
=
NULL, const char*
methodName
=
NULL);
/**
*
通过被hook函数获取数据结构体
*
@param pBeHookAddr 被hook函数
*
@return
返回HK_INFO结构体
*/
HK_INFO
*isHooked(void*
pBeHookAddr);
/**
*
通过hook函数获取数据结构体
*
@param hkFun hook函数
*
@return
返回HK_INFO结构体
*/
HK_INFO
*isHookedByHkFun(void*
hkFun);
/**
*
获取备份/修复的被hook函数,主要是不清楚结构体字段的用户或者透明指针的情况
*
@param info
*
@return
返回备份/修复的被hook函数
*/
void*
getOriFun(HK_INFO*
info);
/**
*
获取备份/修复的被hook函数的指针,二级指针;可用于自己保存,推荐存在取消hook的情况下调用getOriFunByHkFun函数
*
@param info
*
@return
返回指向存储备份/修复的被hook函数的指针
*/
void**
getPoriFun(HK_INFO*
info);
/**
*
通过hook函数获取被hook的函数
*
@param hkFun hook函数
*
@return
返回被hook函数,如果被取消hook或者未被hook返回NULL
*/
void*
getOriFunByHkFun(void*
hkFun);
/**
*
取消hook,释放shellcode/备份的原方法占用的空间并还原原方法
*
@param info 如果成功会释放这个结构体,所以之后这个结构体/指针不能再用
*
@return
取消成功true,否则false
*/
bool
unHook(HK_INFO*
info);
/**
*
取消所有的hook
*
@return
*/
bool
unHookAll();
arm/thumb实现和难点
以上就是arm64的实现和定义的api接口。arm的只需要依葫芦画瓢即可,比arm64简单多了。
shellcode统一使用arm
shellcode理论上是arm还是thumb都没有关系,而且Android的编译链中的汇编器不支持大部分的32位Thumb-2指令(默认情况下),
文章过长,请去原文观看。[原创]如何写一个Android inline hook框架
文章过长,请去原文观看。
![f53a7c02b5a5e92d1d61d38d2be56879.png](https://i-blog.csdnimg.cn/blog_migrate/c36d079939778ca07b1b6dd87d263d8d.png)