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

VSole2022-07-07 16:23:32

近期突然发现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(arg1);    jstring jstr = reinterpret_cast(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(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

数据寄存器状态寄存器
本作品采用《CC 协议》,转载必须注明作者和本文链接
不可中断状态实际上是系统对进程和硬件设备的一种保护机制。当负载存在明显升高趋势时,及时进行分析和调查。系统调用过程中并不会涉及虚拟内存等进程用户态资源,也不会切换进程。因此系统调用通常称为特权模式切换。进程是由内核管理和调度的,进程上下文切换只能发生在内核态。因此相比系统调用来说,在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存,栈保存下来。
汇编语言是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。Smali汇编基础Smali语言最早是由JesusFreke发布在Google Code上的一个开源项目,并不是拥有官方标准的语言。因此也将Smali语言称作Android虚拟机的反汇编语言。基本类型Smali基本数据类型中包含两种类型,原始类型和引用类型。而在Smali中则是以LpackageName/objectName的形式表示对象类型。
IT之家注:这里提到的 FLAGS 寄存器一般被称为“包含 x86 CPU 当前状态状态寄存器”,而 JCC 是基于 EFLAGS 寄存器内容的“允许条件分支的 CPU 指令”。简单来说,要想利用这个漏洞实现攻击,首先应该通过 EFLAGS 寄存器触发编码的瞬态执行,然后测量 JCC 指令的执行时间来获取该编码数据的内容。不过他们目前还不清楚是什么原因导致了这个 Bug。
关于堆栈ShellCode操作:基础理论002-利用fs寄存器寻找当前程序dll的入口:从动态运行的程序中定位所需dll003-寻找大兵LoadLibraryA:从定位到的dll中寻找所需函数地址004-被截断的shellCode:加解密,解决shellCode的零字截断问题
近期突然发现64位APP分析需求激增,然而手边好用的 inlineHook 只有 Frida 一款,所以打算稍微研究下 Frida 的思路,以作借鉴,然后写一款满足简单自用需求的 AArch64 inlineHook 工具。Step1:首先我们简单编写一个 com.example.x64 应用作为目标 APP,且在 libx64.so 中放置一个 native 函数:?
源码分析1、LLVM编译器简介LLVM 命名最早源自于底层虚拟机的缩写,由于命名带来的混乱,LLVM就是该项目的全称。LLVM 核心库提供了与编译器相关的支持,可以作为多种语言编译器的后台来使用。自那时以来,已经成长为LLVM的主干项目,由不同的子项目组成,其中许多是正在生产中使用的各种 商业和开源的项目,以及被广泛用于学术研究。
这样一旦运行的服务器宕机,就把备份的服务器运行起来。冷备的方案比较容易实现,但冷备的缺点是主机出现故障时备机不会自动接管,需要主动切换服务。当一台服务器宕机后,自动切换到另一台备用机使用。
Python人工智能第10篇介绍TF实现CNN图像分类任务
因此,探寻新的应对新型安全威胁的方法成为当前各机构的研究热点。为应对新型病毒和木马的安全威胁,行业内通常采用安全模块扩展技术。的主要安全目标是防止敏感数据的完整性和机密性遭到破坏。当 REE 执行时,CPU 状态寄存器以及总线信号中的对应位会置 1,安全内存和安全设备不再接受 CPU 的访问请求。TEE 使用快速中断请求,REE 使用中断请求。
虚拟机检测技术整理
2023-05-11 09:15:35
第一次尝试恶意代码分析就遇到了虚拟机检测,于是就想着先学习一下检测的技术然后再尝试绕过。学习后最终发现,似乎最好的方法不应该是去patch所有检测方法,而是直接调试并定位检测函数再绕过。但既然已经研究了两天,索性将收集到的资料整理一下,方便后人查找。恶意软件可以搜索这些文件、目录或进程的存在。VMware 虚拟机中可能会有如下的文件列表:C:\Program Files\VMware\
VSole
网络安全专家