之前的hook框架已经可以满足绝大多数需求,但是设计之初未详细的看完arm64下浮点相关的部分,以为和arm一样也是通过通用寄存器、栈传递,现在修复这一部分,这个bug只对dump读写寄存器相关的部分有影响,对replace部分(采用定义一个和被hook函数原型一致的函数)无影响。
目前来看arm浮点参数,float参数占用一个寄存器或者栈,double占用相邻的两个寄存器或栈;返回值float还是占用寄存器r0,double占用寄存器r0和r1。所以可以不考虑保存浮点寄存器s0-s31/d0-d31/q0-q15。如果有例外情况请告诉我,这些寄存器应该是NEON相关的,所以armv7-a/r以下的应该是不存在这些寄存器和指令的(好像有的资料说armv6也有类似的浮点指令)?
如上图,传参通过r0、r1,返回通过r0。
但是arm64传参使用的是float从s0开始为第一个浮点参数,double从d0开始为第一个浮点参数,返回值float为s0,double为d0,所以应该保存浮点寄存器和提供接口。
如上图,传参通过D0、D1,返回通过D0。
如上图,传参通过S0、S1,返回通过S0。
如上图,传参通过W0、S0、S1,返回通过S0。
按说把浮点寄存器存储在栈的最底部比较好,但是代码改动稍微有点大(其实是偏移都要改一遍),所以放在最顶层吧。
.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 }
例如这样操作。
在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还有更多方法。