之前的hook框架已经可以满足绝大多数需求,但是设计之初未详细的看完arm64下浮点相关的部分,以为和arm一样也是通过通用寄存器、栈传递,现在修复这一部分,这个bug只对dump读写寄存器相关的部分有影响,对replace部分(采用定义一个和被hook函数原型一致的函数)无影响。
arm应该不用保存浮点相关的寄存器
目前来看arm浮点参数,float参数占用一个寄存器或者栈,double占用相邻的两个寄存器或栈;返回值float还是占用寄存器r0,double占用寄存器r0和r1。所以可以不考虑保存浮点寄存器s0-s31/d0-d31/q0-q15。如果有例外情况请告诉我,这些寄存器应该是NEON相关的,所以armv7-a/r以下的应该是不存在这些寄存器和指令的(好像有的资料说armv6也有类似的浮点指令)?

如上图,传参通过r0、r1,返回通过r0。
arm64需要保存
但是arm64传参使用的是float从s0开始为第一个浮点参数,double从d0开始为第一个浮点参数,返回值float为s0,double为d0,所以应该保存浮点寄存器和提供接口。

如上图,传参通过D0、D1,返回通过D0。

如上图,传参通过S0、S1,返回通过S0。

如上图,传参通过W0、S0、S1,返回通过S0。

arm64代码实现
按说把浮点寄存器存储在栈的最底部比较好,但是代码改动稍微有点大(其实是偏移都要改一遍),所以放在最顶层吧。
.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]
save_v0_v31:
sub sp, sp, #0x200; //分配栈空间
#stp D0, D1, [SP]; //理论上是不是只保存存储double的部分就可以?
stp Q0, Q1, [SP]; //不支持V0-V31,但是支持Q0-Q31,一致,每个寄存器占128位,低64位保存double,低32位float
stp Q2, Q3, [SP, #0x20];
stp Q4, Q5, [SP, #0x40];
stp Q6, Q7, [SP, #0x60];
stp Q8, Q9, [SP, #0x80];
stp Q10, Q11, [SP, #0xa0];
stp Q12, Q13, [SP, #0xc0];
stp Q14, Q15, [SP, #0xe0];
stp Q16, Q17, [SP, #0x100];
stp Q18, Q19, [SP, #0x120];
stp Q20, Q21, [SP, #0x140];
stp Q22, Q23, [SP, #0x160];
stp Q24, Q25, [SP, #0x180];
stp Q26, Q27, [SP, #0x1a0];
stp Q28, Q29, [SP, #0x1c0];
stp Q30, Q31, [SP, #0x1e0];
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, #0x308]; //lr当作pc,覆盖栈上的x0
blr x3
restore_regs://恢复寄存器
#ldp D0, D1, [SP];
ldp Q0, Q1, [SP];
ldp Q2, Q3, [SP, #0x20];
ldp Q4, Q5, [SP, #0x40];
ldp Q6, Q7, [SP, #0x60];
ldp Q8, Q9, [SP, #0x80];
ldp Q10, Q11, [SP, #0xa0];
ldp Q12, Q13, [SP, #0xc0];
ldp Q14, Q15, [SP, #0xe0];
ldp Q16, Q17, [SP, #0x100];
ldp Q18, Q19, [SP, #0x120];
ldp Q20, Q21, [SP, #0x140];
ldp Q22, Q23, [SP, #0x160];
ldp Q24, Q25, [SP, #0x180];
ldp Q26, Q27, [SP, #0x1a0];
ldp Q28, Q29, [SP, #0x1c0];
ldp Q30, Q31, [SP, #0x1e0];
add sp, sp, #0x200;
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
编译器貌似并不支持存储V0-V31,还是像armv7的neon一样使用Q0-Q31(armv7只有Q0-Q15,16个128位的寄存器)。Q0-Q31可以拆成D0-D31,D0占Q0的低64位,D1占Q1的低64位;同时也可以拆成S0-S31,S0占Q0的低32位,S1占Q1的低32位。不清楚为什么这么设计,感觉有些浪费。
为了方便这里定义一个联合体,qregs就是128位的Q0-Q31,dregs为了方便解析double,同理fregs为了方便解析float。
#if defined(__aarch64__)
union my_neon_regs {
long double qregs[32];
double dregs[32][2];
// float fregs[64*2];
float fregs[32][4];
};
#define dregs(i) dregs[i][0]
#define fregs(i) fregs[i][0]
struct my_pt_regs {
union my_neon_regs neon;
__u64 uregs[31];
__u64 sp;
__u64 pstate; //有时间应该修复,pc在前,但是涉及到栈和生成shellcode都要改,先这么用吧,和系统结构体有这点不同
__u64 pc;
};
通过这样的方式操作浮点相关的寄存器,完成对double、float参数、返回值的读写。
else if (pInfo->pBeHookAddr == retDou) {
#if defined(__aarch64__)
// LE("d0=%.15f, d1=%.15f, d2=%.15f, d3=%.15f", regs->neon.dregs[0], regs->neon.dregs[1], regs->neon.dregs[2], regs->neon.dregs[3]);
LE("d0=%.15f, d1=%.15f, d2=%.15f, d3=%.15f", regs->neon.dregs(0), regs->neon.dregs(1), regs->neon.dregs(2), regs->neon.dregs(3));
LE("s0=%.15f, s1=%.15f, s2=%.15f, s3=%.15f", (float)regs->neon.fregs(0), regs->neon.fregs(1), regs->neon.fregs(2), regs->neon.fregs(3));
LE("q0=%.15llf, q1=%.15llf, q2=%.15llf, q3=%.15llf", (long double)regs->neon.qregs[0], regs->neon.qregs[1], regs->neon.qregs[2], regs->neon.qregs[3]);
#endif
} else if (pInfo->pBeHookAddr == retFlo) {
#if defined(__aarch64__)
LE("d0=%.15f, d1=%.15f, d2=%.15f, d3=%.15f", regs->neon.dregs(0), regs->neon.dregs(1), regs->neon.dregs(2), regs->neon.dregs(3));
LE("s0=%.15f, s1=%.15f, s2=%.15f, s3=%.15f", (float)regs->neon.fregs(0), regs->neon.fregs(1), regs->neon.fregs(2), regs->neon.fregs(3));
LE("q0=%.15llf, q1=%.15llf, q2=%.15llf, q3=%.15llf", (long double)regs->neon.qregs[0], regs->neon.qregs[1], regs->neon.qregs[2], regs->neon.qregs[3]);
#endif
}
例如这样操作。


部分inline hook的又一个检测方式
在arm64下很多框架都是采用X16、X17,多是X17寄存器完成跳转,那么简单分析下这个流程:
1、跳板/shellcode类似如下:
LDR X17, 8;
BR x17;
ADDR(64) //hook函数地址
2、hook函数内调用原函数,那么执行完备份/修复的几条指令后,还要使用X17跳回被hook的函数。
x16、x17一般只在plt中使用,那么x17一般都是保存的一个函数的地址。
但是上面的步骤2肯定不是跳到函数的起始地址,而是函数起始地址+16(或者更大,看修复指令的情况)。那么根据自身的业务场景在容易被hook的函数内检测X17寄存器的值是不是就是当前函数的地址+(16到32),如果在这个范围内,那么就可以认为当前函数被hook了。实现方式多种多样,例如内嵌汇编获取X17寄存器的值。
同样的arm也可以检测R12寄存器,但是比较少的inlineHook框架会在arm下使用R12寄存器,因为可以操作pc,但是不是完全没有,我见过一个。
当然其实要想检测hook还有更多方法。
各位大佬多提bug和多参与完善吧,一个人实在没太多时间,感谢
Unidbg 模拟执行精讲