先来简单了解下什么是InlineHook
什么是InlineHook,简单来说就是给函数(指令)的几条汇编指令备份,然后跳到我们自己的函数逻辑上。
注意:不要联想整个项目,现在只是流程讲解,项目实现会在后面接着讲,结合着流程。
InlineHook的第一步,劫持原来的函数到自己的函数上
举个例子:
比如open函数,我们查看地址
前四条汇编指令是这样的,当我们hook后:
就变成了
注意:在修改之前一定要备份这些指令,因为这些指令在后面要被执行
第一条是把地址的内容存到x17,第二条是跳转到x17
第三四条不是汇编指令,是目标要跳转到的地址,我们这个跳板用了16字节,当然你也可以继续跳转字节更少的跳板,本文用的是比较简单的跳板,让我们看看frida用的跳板是什么样的
看来我们跟frida的跳板差不多~
跳转到我们自定义的函数之后呢?
InlineHook的第二步,保存当前函数所有的寄存器状态,调用pre_hookcallback
要保存哪些寄存器呢?最简单的肯定包含X0-X30(包含了lr lr就是x30) 可以选择性保存sp(当然如果栈平衡就无所谓了) NZCV状态寄存器 以及Q系列寄存器(本项目还没有保存)
让我们看看frida是怎么做的
ebug238:00000077AAE20004 STP Q30, Q31, [SP,#-32]! debug238:00000077AAE20008 STP Q28, Q29, [SP,#-32]! debug238:00000077AAE2000C STP Q26, Q27, [SP,#-32]! debug238:00000077AAE20010 STP Q24, Q25, [SP,#0x70+var_90]! debug238:00000077AAE20014 STP Q22, Q23, [SP,#0x90+var_B0]! debug238:00000077AAE20018 STP Q20, Q21, [SP,#0xB0+var_D0]! debug238:00000077AAE2001C STP Q18, Q19, [SP,#0xD0+var_F0]! debug238:00000077AAE20020 STP Q16, Q17, [SP,#0xF0+var_110]! debug238:00000077AAE20024 STP Q14, Q15, [SP,#0x110+var_130]! debug238:00000077AAE20028 STP Q12, Q13, [SP,#0x130+var_150]! debug238:00000077AAE2002C STP Q10, Q11, [SP,#0x150+var_170]! debug238:00000077AAE20030 STP Q8, Q9, [SP,#0x170+var_190]! debug238:00000077AAE20034 STP Q6, Q7, [SP,#0x190+var_1B0]! debug238:00000077AAE20038 STP Q4, Q5, [SP,#0x1B0+var_1D0]! debug238:00000077AAE2003C STP Q2, Q3, [SP,#0x1D0+var_1F0]! debug238:00000077AAE20040 STP Q0, Q1, [SP,#0x1F0+var_210]! debug238:00000077AAE20044 STP X29, X30, [SP,#0x210+var_220]! debug238:00000077AAE20048 STP X27, X28, [SP,#0x220+var_230]! debug238:00000077AAE2004C STP X25, X26, [SP,#0x230+var_240]! debug238:00000077AAE20050 STP X23, X24, [SP,#0x240+var_250]! debug238:00000077AAE20054 STP X21, X22, [SP,#0x250+var_260]! debug238:00000077AAE20058 STP X19, X20, [SP,#0x260+var_270]! debug238:00000077AAE2005C STP X17, X18, [SP,#0x270+var_280]! debug238:00000077AAE20060 STP X15, X16, [SP,#0x280+var_290]! debug238:00000077AAE20064 STP X13, X14, [SP,#0x290+var_2A0]! debug238:00000077AAE20068 STP X11, X12, [SP,#0x2A0+var_2B0]! debug238:00000077AAE2006C STP X9, X10, [SP,#0x2B0+var_2C0]! debug238:00000077AAE20070 STP X7, X8, [SP,#0x2C0+var_2D0]! debug238:00000077AAE20074 STP X5, X6, [SP,#0x2D0+var_2E0]! debug238:00000077AAE20078 STP X3, X4, [SP,#0x2E0+var_2F0]! debug238:00000077AAE2007C STP X1, X2, [SP,#0x2F0+var_300]! debug238:00000077AAE20080 MRS X1, NZCV debug238:00000077AAE20084 STP X1, X0, [SP,#0x300+var_310]! debug238:00000077AAE20088 ADD X0, SP, #0x310+arg_0 debug238:00000077AAE2008C STP XZR, X0, [SP,#0x310+var_320]! debug238:00000077AAE20090 STR X30, [SP,#0x320+var_8] debug238:00000077AAE20094 STR X29, [SP,#0x320+var_10] debug238:00000077AAE20098 ADD X29, SP, #0x320+var_10 debug238:00000077AAE2009C MOV X1, SP debug238:00000077AAE200A0 ADD X2, SP, #0x320+var_218 debug238:00000077AAE200A4 ADD X3, SP, #0x320+var_10
为什么要保存呢? 因为在函数调用前如果调用了pre_hookcallback 或者在函数调用后调用post_hookcallback,里面的逻辑会污染原来的寄存器,导致函数崩溃。
我们简单举个例子,现在有一个函数
uint64_t test(int a, int b, int c,int d) { return a+b+c+d; }
这个函数主要用到了x0-x3
如果我们在pre_hookcallback调用了任意函数,会刷新x0寄存器,如果返回的是一个地址,那么x0的值在刷新后,会传入我们原来的函数中,造成寄存器污染。
我们将原寄存器保存到哪里?
frida是怎么做的?frida是直接将所有寄存器压入了栈中,然后在按顺序弹出,保证栈平衡
弹出过程(压入过程看上面)
debug238:00000077AAE200B4 ADD SP, SP, #0x10 debug238:00000077AAE200B8 LDP X1, X0, [SP+0x310+var_310],#0x10 debug238:00000077AAE200BC MSR NZCV, X1 debug238:00000077AAE200C0 LDP X1, X2, [SP+0x300+var_300],#0x10 debug238:00000077AAE200C4 LDP X3, X4, [SP+0x2F0+var_2F0],#0x10 debug238:00000077AAE200C8 LDP X5, X6, [SP+0x2E0+var_2E0],#0x10 debug238:00000077AAE200CC LDP X7, X8, [SP+0x2D0+var_2D0],#0x10 debug238:00000077AAE200D0 LDP X9, X10, [SP+0x2C0+var_2C0],#0x10 debug238:00000077AAE200D4 LDP X11, X12, [SP+0x2B0+var_2B0],#0x10 debug238:00000077AAE200D8 LDP X13, X14, [SP+0x2A0+var_2A0],#0x10 debug238:00000077AAE200DC LDP X15, X16, [SP+0x290+var_290],#0x10 debug238:00000077AAE200E0 LDP X17, X18, [SP+0x280+var_280],#0x10 debug238:00000077AAE200E4 LDP X19, X20, [SP+0x270+var_270],#0x10 debug238:00000077AAE200E8 LDP X21, X22, [SP+0x260+var_260],#0x10 debug238:00000077AAE200EC LDP X23, X24, [SP+0x250+var_250],#0x10 debug238:00000077AAE200F0 LDP X25, X26, [SP+0x240+var_240],#0x10 debug238:00000077AAE200F4 LDP X27, X28, [SP+0x230+var_230],#0x10 debug238:00000077AAE200F8 LDP X29, X30, [SP+0x220+var_220],#0x10 debug238:00000077AAE200FC LDP Q0, Q1, [SP+0x210+var_210],#0x20 debug238:00000077AAE20100 LDP Q2, Q3, [SP+0x1F0+var_1F0],#0x20 debug238:00000077AAE20104 LDP Q4, Q5, [SP+0x1D0+var_1D0],#0x20 debug238:00000077AAE20108 LDP Q6, Q7, [SP+0x1B0+var_1B0],#0x20 debug238:00000077AAE2010C LDP Q8, Q9, [SP+0x190+var_190],#0x20 debug238:00000077AAE20110 LDP Q10, Q11, [SP+0x170+var_170],#0x20 debug238:00000077AAE20114 LDP Q12, Q13, [SP+0x150+var_150],#0x20 debug238:00000077AAE20118 LDP Q14, Q15, [SP+0x130+var_130],#0x20 debug238:00000077AAE2011C LDP Q16, Q17, [SP+0x110+var_110],#0x20 debug238:00000077AAE20120 LDP Q18, Q19, [SP+0xF0+var_F0],#0x20 debug238:00000077AAE20124 LDP Q20, Q21, [SP+0xD0+var_D0],#0x20 debug238:00000077AAE20128 LDP Q22, Q23, [SP+0xB0+var_B0],#0x20 debug238:00000077AAE2012C LDP Q24, Q25, [SP+0x90+var_90],#0x20 debug238:00000077AAE20130 LDP Q26, Q27, [SP+0x70+var_70],#0x20 debug238:00000077AAE20134 LDP Q28, Q29, [SP+0x50+var_50],#0x20 debug238:00000077AAE20138 LDP Q30, Q31, [SP+0x30+var_30],#0x20 debug238:00000077AAE2013C LDP X16, X17, [SP+0x10+var_10],#0x10 debug238:00000077AAE20140 RET X16
这种设计需要保证栈平衡,在进入我们的跳板函数和出跳板函数 sp的值要一致,当然在不污染寄存器的情况下,你可以保存sp,在结束之前恢复(不推荐,很麻烦)
当然也可以和我一样,存在一个结构体中,我们的hook框架是怎么做的?
然后获取结构体的地址,将数据全部压入结构体中(Q系列寄存器还没有加入):
// 保存寄存器到 HookInfo->ctx ldp x0, x1, [sp], #0x10 // 恢复x16,x17 stp x0, x1, [x16, #128] // 保存原始x16,x17 // 恢复x0,x1到临时寄存器 ldp x0, x1, [sp], #0x10 // 从栈上加载原始x0,x1 // 保存所有寄存器到ctx stp x0, x1, [x16, #0] stp x2, x3, [x16, #16] stp x4, x5, [x16, #32] stp x6, x7, [x16, #48] stp x8, x9, [x16, #64] stp x10, x11, [x16, #80] stp x12, x13, [x16, #96] stp x14, x15, [x16, #112] stp x18, x19, [x16, #144] stp x20, x21, [x16, #160] stp x22, x23, [x16, #176] stp x24, x25, [x16, #192] stp x26, x27, [x16, #208] stp x28, x29, [x16, #224] str x30, [x16, #240]
这样不用考虑栈平衡的问题,因为我们没用到栈(当然会有新的分支,使用栈来储存,因为他真的很方便)
pre_hookcallback顾名思义就是在调用hook之前的回调函数,在这时,原函数还没有开始执行,但是参数已经被压入到了寄存器中
如何调用?
// 调用pre_callback sub x0, x16, #0x30 // HookInfo作为第一个参数 ldr x17, [x0] // 加载pre_callback函数指针 blr x17 // 调用pre_callback
我们非常简单的就实现了调用,在这里我们可以读取寄存器,修改寄存器
|
InlineHook的第三步,恢复寄存器,修复指令,调用原函数
因为前面做了太多的操作,比如
sub x0, x16, #0x30 // 这里污染了x16 x0寄存器 ldr x17, [x0] // 这里污染了x17寄存器 blr x17 // 这里污染了x30寄存器
我们需要把结构体里的寄存器复原,然后在调用原函数
ldr x16, _twojump_start //拿到结构体地址 add x16, x16, #0x30 //计算结构体ctx成员 // 恢复所有寄存器 比如在hook里修改了,那这里就要还原了 ldp x0, x1, [x16, #0] ldp x2, x3, [x16, #16] ldp x4, x5, [x16, #32] ldp x6, x7, [x16, #48] ldp x8, x9, [x16, #64] ldp x10, x11, [x16, #80] ldp x12, x13, [x16, #96] ldp x14, x15, [x16, #112] ldp x18, x19, [x16, #144] ldp x20, x21, [x16, #160] ldp x22, x23, [x16, #176] ldp x24, x25, [x16, #192] ldp x26, x27, [x16, #208] ldp x28, x29, [x16, #224] ldr x30, [x16, #240] sub x16, x16, #0x30
做完了该做的事情以后,终于轮到被hook的函数执行了
第一种实现:
把之前备份的指令还原回被hook函数,然后跳转执行
因为函数是在我们这里被“主动调用的”
// 调用原函数 ldr x17, [x16, #16] // 加载原函数地址 blr x17 //这里x0已经有返回值了
所以在执行完成后还是会返回我们的汇编指令
这种方法不提倡,因为如果被hook函数是多线程执行的(大部分都是这样)
会发生崩溃,具体大家可以思考一下
第二种实现:
将备份后的指令执行,然后跳回原来的函数(+16字节的位置)
第二种实现涉及到指令修复的问题:
为什么会出现指令修复,因为有一些特殊指令是和当前pc相关的,我们改变了指令的位置
enum class ARM64_INS_TYPE { UNKNOW, //除了下面意外的所有指令 ADR, // 形如 ADR Xd, label ADRP, // 形如 ADRP Xd, label B, // 形如 B label BL, // 形如 BL label B_COND, // 形如 B.cond label CBZ_CBNZ, // 形如 CBZ/CBNZ Rt, label TBZ_TBNZ, // 形如 TBZ/TBNZ Rt, #imm, label LDR_LIT, // 形如 LDR Rt, label };
对应的pc值也会发生改变:
2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[0]: 0xd100c3ff at old_addr: 0x70441c9f64, new_addr: 0x71088f815c 2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[1]: 0xa9027bfd at old_addr: 0x70441c9f68, new_addr: 0x71088f8160 2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[2]: 0x910083fd at old_addr: 0x70441c9f6c, new_addr: 0x71088f8164 2024-12-04 17:00:24.004 17504-17504 jiqiu2021 com.example.inlinehookstudy I Processing instruction[3]: 0xb81fc3a0 at old_addr: 0x70441c9f70, new_addr: 0x71088f8168
我们采用shadowhook里面的代码进行修复(我给重写并加上了注释)
拿修复adrp的部分做例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
其实就是把adrp(8字节)等价替换成了 (16字节的指令)
1 2 3 4 |
|
InlineHook的第四步,保存寄存器,调用post_hookcallback
在调用完原函数以后,相应的寄存器的值发生了变化,我们再次保存,然后调用函数调用后的回调函数
// 调用原函数 ldr x17, [x16, #16] // 加载原函数地址 blr x17 //这里x0已经有返回值了 ldr x16, _twojump_start add x17, x16, #48 // x17指向ctx // 再次保存寄存器到ctx stp x0, x1, [x17, #0] stp x2, x3, [x17, #16] stp x4, x5, [x17, #32] stp x6, x7, [x17, #48] stp x8, x9, [x17, #64] stp x10, x11, [x17, #80] stp x12, x13, [x17, #96] stp x14, x15, [x17, #112] stp x18, x19, [x17, #144] stp x20, x21, [x17, #160] stp x22, x23, [x17, #176] stp x24, x25, [x17, #192] stp x26, x27, [x17, #208] stp x28, x29, [x17, #224] // 调用post_callback mov x0, x16 // HookInfo作为第一个参数 ldr x16, [x16, #8] // 加载post_callback blr x16
注释写的很清楚,和上面高速相似,相信大家已经看懂了
就结束了吗?还没有结束,如果你在post_callback修改了寄存器的值,还需要恢复
// 恢复所有寄存器 ldr x16, _twojump_start add x16, x16, #0x30 // 恢复所有寄存器 比如在hook里修改了,那这里就要还原了 ldp x0, x1, [x16, #0] ldp x2, x3, [x17, #16] ldp x4, x5, [x16, #32] ldp x6, x7, [x16, #48] ldp x8, x9, [x16, #64] ldp x10, x11, [x16, #80] ldp x12, x13, [x16, #96] ldp x14, x15, [x16, #112] ldp x18, x19, [x16, #144] ldp x20, x21, [x16, #160] ldp x22, x23, [x16, #176] ldp x24, x25, [x16, #192] ldp x26, x27, [x16, #208] ldp x28, x29, [x16, #224] ldr x30, [x16, #240] ret
当然这里可以只恢复x0(返回值)
到此我们已经完整完成了一个hook的流程
项目设计思路
项目地址放在上面了。本次讲解的是newhook分支
项目的总hook管理结构体是:
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 |
|
这里重点注意三个函数:
registerHook
注册函数
removeHook
移除注册函数
getHook
获取hook状态(比如传入open地址,看是不是已经被hook了) 链式hook准备做,但是感觉没啥用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
hook信息储存结构体,在汇编里获取的也是这个
每一个hook,对应一个hookinfo
1 2 3 4 5 |
|
重点讲一下createHook一些点:
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
backup_orig_instructions(hookInfo)
主要是备份原来的函数,将指令拷贝到结构体里
1 2 3 4 5 6 7 8 9 10 11 |
|
每次都创建一个页大小的内存,来存储跳板函数就是.s里面的
.s里面我更喜欢叫他模板函数,因为他不是最终执行的,每创建一个函数都会创建一块内存,然后memcpy到mmap出来的这一块内存里,在填入当前hook的hookinfo
1 2 3 4 5 6 7 8 9 10 |
|
这一部分对备份的指令进行修复,然后填入mmap的那块空间里
1 2 3 4 5 6 7 8 9 10 11 |
|
之后添加跳回原来函数的跳转指令:
1 2 3 4 5 6 7 8 |
|
mmap分配出的空间目前是
填充过的.s模板汇编地址|修复过的原函数指令|跳回原函数执行的地址
1 2 3 4 5 |
|
修改目标函数头部,跳转到模板函数+16字节处(16字节存储地址)
1 |
|
hookInfo->backup_func指针指向修复过的指令地址,并且执行完修复的指令回跳回原函数位置
由于
1 2 3 |
|
使用的是blr x30寄存器被我们修改在这里了,所以回跳回模板函数
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 |
|
取消hook就很简单了,把目标函数头部还原即可