Frida inlineHook原理分析及简单设计一款AArch64 inlineHook工具

资讯 作者:看雪学院 2022-07-07 18:59:22 阅读:344


本文为看雪论坛精华文章
看雪论坛作者ID:abucs


近期突然发现64位APP分析需求激增,然而手边好用的 inlineHook 只有 Frida 一款,所以打算稍微研究下 Frida 的思路,以作借鉴,然后写一款满足简单自用需求的 AArch64 inlineHook 工具。




Frida inlineHook 思路分析

根据之前开发 AArch32 inlineHook 框架的经验,总结 inlineHook 框架开发的几个关键点大抵如下:
  1. 动态替换需要 Hook 的指令片段为一段经过设计的跳板指令,即 trampoline ,目标为我们设计好的一段 shellCode

  2. 在内存中设计并生成一段 shellCode ,这是我们的可控 shellCode ,在该 shellCode 中需要实现 Hook 的功能函数(即打印/替换-参数/结果)

  3. shellCode 的设计原则是保持 Hook 前后的栈平衡,并保护寄存器状态(即Hook结束后,保持与Hook开始前一致的栈布局与寄存器状态)

  4. 在 shellCode 中完成原函数的执行工作,被替换的掉的指令中若包含计算 PC-relative address ( 如 Branch 指令 ),需要对其正确解析执行


对我来说一个简单的工具只要满足前3点就足够了,第4点待后续优化的时候再行完善,所以我们接下来看看 Frida 是如何完成以上这几点的。


Step1:

首先我们简单编写一个 com.example.x64 应用作为目标 APP,且在 libx64.so 中放置一个 native 函数: Java_com_example_x64_JNI_aal ,马上使用 Frida Hook 
存在以下两种情况:
1、Frida Hook 函数开头指令(即直接 Hook 导出函数)
2、Hook 函数中间指定位置的指令


Frida 代码如下:
//## hookTest1: Hook 导出函数->Java_com_example_x64_JNI_aalfunction hookTest1() {    var helloAddr = Module.findExportByName("libx64.so", "Java_com_example_x64_JNI_aal");    console.log(helloAddr);    if(helloAddr != null){        Interceptor.attach(helloAddr,{            onEnter: function(args){                console.log("hook1 on enter");            },            onLeave: function(retval){                console.log("hook1 on leave");            }        });    }}//## hookTest2: Hook 指定位置->0x000000000000BBA0function hookTest2() {    var libutilityAddr = Module.findBaseAddress("libx64.so");    var getOriginalStringAddr = libutilityAddr.add(0x000000000000BBA0);    console.log(getOriginalStringAddr);    if(getOriginalStringAddr != null){        Interceptor.attach(getOriginalStringAddr,{            onEnter: function(args){                console.log("hook2 on enter");            },            onLeave: function(retval){                console.log("hook2 on leave");            }        });    }}

Hook 完毕,执行结果如下:


不知道为什么打出来了两次 hook1 on leave ,之后我使劲检查代码,确定并没有写错。猜测原因或许是Hook点2在最终返回值的设置上出现了什么问题吧。我们暂时忽略上面的问题,接下来分析这两个地址上的指令发生了甚么变化。

Step2:

挂上我们的调试器,首先对 Hook1 进行分析:Hook1 对应 Java_com_example_x64_JNI_aal 函数的入口位置( 0x7fac430b70 ),可以看到前16字节已经被替换掉,新指令为利用 x16 寄存器制作的一个跳板(trampoline),其目标为 0x7face7c600:
Hook2 情况与 Hook1 类似,也是生成了16字节的跳板指令(依然使用 x16 寄存器)来替换掉 0xBBA0 位置的16字节原始指令,在此不做展示。
 
P.S. > 在后续多次测试中发现,偶尔也会出现使用单条 Branch 指令(4字节)来替换掉被 Hook 地址的单条指令(4字节)的情况发生,如下图所示。


因为 Branch 指令存在跳转范围(+-128MB),所以 Frida 使用这种形式的 trampoline 需要对被 Hook 地址前后 128MB 范围进行检测,寻找空闲地址,不过这对本文实现一个简单的 inlineHook 模型并无太大影响,故不做深入讨论。
 
其实 Frida 还有一种跳转范围扩大至 +-4GB 大小的 trampoline 生成规则,在此也不做讨论了,因为在原理上大同小异,单纯属于细节优化问题。
 
另外还有不得不提的一点,当 trampoline 使用 x16 寄存器作为跳板寄存器时,Hook 结束后 x16 寄存器无疑会被污染,然而事实上 Frida 同时使用了 x16 与 x17 寄存器,那么关于这两个寄存器有什么说法呢?官方对这两个寄存器作用的描述如下:
描述中提到 x16、x17 寄存器作为内部过程调用中的临时寄存器,结合下图便能更好的理解官方的定义。


关于 trampoline 的研究就到此为止了,接下来我们看他生成的 shellCode。


Step3:

接下来我们开始分析 shellcode 部分,以 Hook1 为例。

Java_com_example_x64_JNI_aal 函数入口: 0x7fac430b70

入口处 trampoline 汇编代码如下:
进入 0x7face7c600 位置,分析如下图:

首先 mmap 了一段匿名内存( 7face7c000-7face83000 rwxp ),在 0x7face7c600 位置放置了以下几条汇编指令构成第二段跳板。
> ldr x17, =0x7facec12e0
> ldr x16, =0x7face7c000
> br x16

其中 x17 寄存器装载了一个地址( 0x7facec12e0 ),这个地址内部保存着 0x7fac430b70 ,正是 Java_com_example_x64_JNI_aal 函数入口地址。

而 x16 寄存器装载了此番生成的 shellCode 的地址( 0x7face7c000 ),将该段内存 dump 下来,拖入 ida 进行分析:

 
绿色、蓝色部分合并完成了栈平衡、寄存器保护与恢复工作。


我们在外部用 JS 编写的 Hook 功能代码( onEnter 部分 ),由 BLR X4 ( 0x7F7D8D8360 ) 跳转至 frida-agent-64.so (见下图)来完成。


在 JS 中可以打印,甚至修改函数入参的原因是因为入参(前8个在 X0-X7 寄存器上,后面的在栈上)已全部由绿色块指令压入栈中保存,所以在 BLR X4 进行函数调用时,合理设置 X0-X3 寄存器,使其正确的指向栈上某位置尤为关键。


我们接下来在 shellcode 最后一条 BR X16 指令上插入断点,分析函数的运行情况。


当断点触发时 BR X16 欲跳转至内存 0x7face7c630,其对应的汇编代码如上图所见,其中包含 Java_com_example_x64_JNI_aal 函数开头被替换的4条原始指令。

之后再次使用 x16 寄存器跳转至 0x7fac430b80,即函数 Java_com_example_x64_JNI_aal 开头偏移 0x10 的位置,以完成原函数的执行动作。
 
此时 hook1 on enter 打印完毕,但 hook1 on leave 还未打印,所以注意到 x30 寄存器中保存的返回地址是 0x7face7c60c,即前文中暂未分析的第三段跳板指令,汇编代码如下:
> ldr x17, =0x7facec12e0
> ldr x16, =0x7face7c100
> br x16

x17 寄存器行为与之前一致,x16 寄存器装载了第二段 shellCode 的地址( 0x7face7c100 ),刚才已经一起 dump 下来了,直接在 ida 分析。


绿色、蓝色部分代码作用不变,由 BLR X3 ( 0x7F7D8D86C8 ) 跳转至 frida-agent-64.so 来完成外部 JS 写的 Hook 功能代码中 onLeave 的部分。

最后由 BR X16 返回 Java_com_example_x64_JNI_aal 函数被调用时真正的 LR。
 
至此 shellcode 部分也大体分析完毕了,此时我们应该能够写出一款简单的 AArch64 inlineHook 工具模型了。



AArch64 inlineHook 开发


结合前文的分析,我们的 inlineHook 应该具备以下这几点功能:
  1. Hook 导出函数:即在函数开头进行 Hook ,能够执行原函数,并提供 onEnter 以及 onLeave 两层代码注入点,达到类似 Frida 那种 "代码托管" 一样的效果

  2. Hook 函数内指定地址:Hook 指定位置的汇编指令,仅提供 onEnter 一层代码注入点,因为考虑到在指定位置上 X30( LR ) 寄存器可能已经发生变化,此时用该寄存器做返回判断并不准确,故放弃 onLeave

  3. 在 onEnter 中提供入参的打印/修改操作 ( 本质是寄存器/堆栈内存打印/修改操作 )

  4. 在 onLeave 中提供返回值的打印/修改操作 ( 本质是寄存器/堆栈内存打印/修改操作 )


有了以上几点需求,我们现在可以开始开发了 ( 源码下载见文章末尾 )


Step1:

我们首先来设计 shellcode 部分,在本简易版工具中,我们的跳板指令选择使用 x16 寄存器的 16 字节 trampoline ,代码如下:
_trampoline_:    LDR                 X16, x64code0    BR                  X16x64code0:_jmp_addr_:    .dword 0x1111111111111111

接下需要做参数和返回地址入栈工作,以及全寄存器状态保护,代码如下:


接下来调用 Hook 功能函数的 onEnter 部分,并恢复寄存器及栈状态,最后取出返回地址并返回原函数执行。


对于 onLeave 部分的 shellcode 与之大体类似,就不贴图展示了。


Step2:

接下来开始编写函数完成 inlineHook 的插入
//## Hook目标函数extern "C" JNIEXPORT jstring JNICALLJava_com_cs_inline_MainActivity_stringFromJNI(        JNIEnv* env,        jobject /* thisobj */,        jstring jstr) {    std::string hello = "Hello from C++: ";    hello.append(env->GetStringUTFChars(jstr, nullptr));    return env->NewStringUTF(hello.c_str());} //## 该函数内部完成了对Java_com_cs_inline_MainActivity_stringFromJNI函数的inlineHookextern "C" JNIEXPORT void JNICALLJava_com_cs_inline_MainActivity_inlineHook1(JNIEnv* env,                                            jobject /* thisobj */){    //## Hook target函数为:Java_com_cs_inline_MainActivity_stringFromJNI    u_long func_addr = (u_long)Java_com_cs_inline_MainActivity_stringFromJNI;    extern u_long _shellcode_start_, _the_func_addr_, _end_func_addr_, _ori_ins_set1_, _retback_addr_, _shellcode_end_, _trampoline_, _jmp_addr_, _shellcode_part2_;    //## 计算shellcode整体长度    u_long total_len = (u_long)&_shellcode_end_ - (u_long)&_shellcode_start_;    LOGD(ANDROID_LOG_DEBUG, "[+] ShellCode len: %d, target func: %p", total_len, func_addr);     //## 使用mmap分配匿名内存存放shellcode    u_long page_size = getpagesize();    u_long shellcode_mem_start = (u_long)mmap(0, page_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);    memset((void *)shellcode_mem_start, 0, page_size);    memcpy((void *)shellcode_mem_start, (void *)&_shellcode_start_, total_len);    LOGD(ANDROID_LOG_DEBUG, "[+] shellcode_mem_start: %p", shellcode_mem_start);     //## 设置trampoline跳转的目标地址    *(u_long*)&_jmp_addr_ = shellcode_mem_start;     u_long mem_the_func_addr_ = (u_long)&_the_func_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start;    u_long mem_end_func_addr_ = (u_long)&_end_func_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start;    u_long mem_ori_ins_set1_ = (u_long)&_ori_ins_set1_ - (u_long)&_shellcode_start_ + shellcode_mem_start;    u_long mem_retback_addr_ = (u_long)&_retback_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start;    if(!off_shellcode_part2_)        off_shellcode_part2_ = (u_long)&_shellcode_part2_ - (u_long)&_shellcode_start_;     //## 设置onEnter及onLeave函数    *(u_long*)mem_the_func_addr_ = (u_long)on_enter_1;    *(u_long*)mem_end_func_addr_ = (u_long)on_leave_1;    //## 设置返回地址为距离Hook点0x10长度的指令地址,即偏移为trampoline的长度    *(u_long*)mem_retback_addr_ = (u_long)func_addr + 0x10;     //## 原指令保存,并未做任何解析,PC-relative address相关指令暂不支持    *(u_long*)mem_ori_ins_set1_ = *(u_long*)func_addr;    *(u_long*)(mem_ori_ins_set1_ + 8) = *(u_long*)(func_addr + 8);     //## 页权限修改并完成inlineHook    u_long entry_page_start = (u_long)(func_addr) & (~(page_size-1));    mprotect((u_long*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);    *(u_long*)func_addr = *(u_long*)&_trampoline_;    *(u_long*)(func_addr + 8) = *(u_long*)(((u_long)&_trampoline_) + 8);

inlineHook1 函数主要作用是分配 shellcode 的内存及设置其中的关键数据,并使用 trampoline 替换原指令完成 Hook,函数内注释较为详细,就不做过多解释了。
 
最后我们来编写 onEnter 及 onLeave 函数。
//## 使用线程局部存储保存原始返回地址LR(X30)u_long thread_local ori_lr = 0;u_long off_shellcode_part2_ = 0; void on_enter_1(u_long sp){    //## sp回到初始位置,取出返回地址LR    sp = sp + 0x60;    u_long lr = *(u_long*)(sp - 8);    u_long lr_ptr = sp - 8;    u_long pc = *(u_long*)(sp - 0x20);    pc -= 0x20;    //## 使用TLS保存LR    ori_lr = lr;    //## 一般来说8个参数顶天了    u_long arg1 = *(u_long*)(sp - 0x28);    u_long arg2 = *(u_long*)(sp - 0x30);    u_long arg3 = *(u_long*)(sp - 0x38);    u_long* arg3_ptr = (u_long*)(sp - 0x38);    u_long arg4 = *(u_long*)(sp - 0x40);    u_long arg5 = *(u_long*)(sp - 0x48);    u_long arg6 = *(u_long*)(sp - 0x50);    u_long arg7 = *(u_long*)(sp - 0x58);    u_long arg8 = *(u_long*)(sp - 0x60);    //## sp上还有参数的话照下面这么写    u_long arg9 = *(u_long*)(sp);    u_long arg10 = *(u_long*)(sp + 0x8);     //## 打印String参数    JNIEnv* env = reinterpret_cast<JNIEnv *>(arg1);    jstring jstr = reinterpret_cast<jstring>(arg3);    LOGD(ANDROID_LOG_INFO, "[+] arg3: %s", env->GetStringUTFChars(jstr, nullptr));    //## 替换String参数    jstring jstr_new = env->NewStringUTF("--This is on_enter_1 !");    *arg3_ptr = reinterpret_cast<u_long>(jstr_new);     //## 修改LR寄存器,保证原始函数执行完毕会回到on_leave_1函数    *(u_long*)lr_ptr = pc + off_shellcode_part2_;    LOGD(ANDROID_LOG_WARN, "[+] on_enter_1: %p", on_enter_1);} void on_leave_1(u_long sp){    //## sp回到初始位置    sp = sp + 0x10;    u_long x0 = *(u_long*)(sp - 8);    u_long* x0_ptr = (u_long*)(sp - 8);    u_long lr = *(u_long*)(sp - 0x10);    u_long* lr_ptr = (u_long*)(sp - 0x10);     //## do_something ...    LOGD(ANDROID_LOG_DEBUG, "[+] on_leave_1: %p", on_leave_1);     //## 取回LR并返回    *(u_long*)lr_ptr = ori_lr;}

在 onEnter 函数中需要保存原始函数的返回地址 LR 寄存器值至 TLS 中,并在最后设置临时返回地址为 onLeave 函数对应的 shellcode,最后再 onLeave 中再取回真实的 LR 并返回实际的函数调用链中,完成整个 inlineHook 流程。
 
另外 Hook 指定位置汇编指令的代码并未贴出,因为原理是一致的,仅仅在 onEnter 函数中不设置临时返回地址即可。



效果展示及总结


仅开启 Hook1 时的效果如下图所示:
总结:借鉴 Frida 的 inlineHook 原理设计了一款简单的 inlineHook 框架,满足了部分常用需求;关于框架的 trampoline 优化,PC-relative address 相关指令解析执行等工作,待后续继续开发优化。

代码已上传:
Gitee链接: https://gitee.com/zzy_cs/inline-hook
Git链接: https://github.com/zzyccs/inlineHook



看雪ID:abucs

https://bbs.pediy.com/user-home-772122.htm

*本文由看雪论坛 abucs 原创,转载请注明来自看雪社区



# 往期推荐

1.PWN学习笔记【格式化字符串漏洞练习】

2.LLVM PASS类pwn题入门

3.Akamai保护的相关网站(IHG,TI)学习记录

4.Il2Cpp恢复符号过程分析

5.记一次安全产品的漏洞挖掘

6.CVE-2016-3309提权漏洞学习笔记






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

在线申请SSL证书行业最低 =>立即申请

[广告]赞助链接:

关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

#
公众号 关注KnowSafe微信公众号
随时掌握互联网精彩
赞助链接