首页
论坛
课程
招聘
[原创]Frida inlineHook原理分析及简单设计一款AArch64 inlineHook工具
2022-6-12 23:36 21619

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

2022-6-12 23:36
21619

--> 0. 前言

 

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

--> 1. 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 函数中间指定位置的指令
Java_com_example_x64_JNI_aal_show1
> Frida 代码如下:

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
//## hookTest1: Hook 导出函数->Java_com_example_x64_JNI_aal
function 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 指定位置->0x000000000000BBA0
function 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 完毕,执行结果如下:
Frida_hook_show1
> 不知道为什么打出来了两次 hook1 on leave ,之后我使劲检查代码,确定并没有写错。。好吧,猜测原因或许是Hook点2在最终返回值的设置上出现了什么问题吧。。
我们暂时忽略上面的问题,接下来分析这两个地址上的指令发生了甚么变化

-- Step2 --

> 挂上我们的调试器,首先对 Hook1 进行分析:
> Hook1 对应 Java_com_example_x64_JNI_aal 函数的入口位置( 0x7fac430b70 ),可以看到前16字节已经被替换掉,新指令为利用 x16 寄存器制作的一个跳板(trampoline),其目标为 0x7face7c600:
hoo1_trampoline_analysis1
> Hook2 情况与 Hook1 类似,也是生成了16字节的跳板指令(依然使用 x16 寄存器)来替换掉 0xBBA0 位置的16字节原始指令,在此不做展示

 

P.S. > 在后续多次测试中发现,偶尔也会出现使用单条 Branch 指令(4字节)来替换掉被 Hook 地址的单条指令(4字节)的情况发生,如下图所示
singal_Branch_inst
> 因为 Branch 指令存在跳转范围(+-128MB),所以 Frida 使用这种形式的 trampoline 需要对被 Hook 地址前后 128MB 范围进行检测,寻找空闲地址,不过这对本文实现一个简单的 inlineHook 模型并无太大影响,故不做深入讨论

 

P.P.S. > 其实 Frida 还有一种跳转范围扩大至 +-4GB 大小的 trampoline 生成规则,在此也不做讨论了,因为在原理上大同小异,单纯属于细节优化问题

 

> 另外还有不得不提的一点,当 trampoline 使用 x16 寄存器作为跳板寄存器时,Hook 结束后 x16 寄存器无疑会被污染,然而事实上 Frida 同时使用了 x16 与 x17 寄存器,那么关于这两个寄存器有什么说法呢?官方对这两个寄存器作用的描述如下:
x16x17_official_desc
> 描述中提到 x16、x17 寄存器作为内部过程调用中的临时寄存器,结合下图便能更好的理解官方的定义
x16x17_plt_table
> 关于 trampoline 的研究就到此为止了,接下来我们看他生成的 shellCode

-- Step3 --

> 接下来我们开始分析 shellcode 部分,以 Hook1 为例
> Java_com_example_x64_JNI_aal 函数入口: 0x7fac430b70
> 入口处 trampoline 汇编代码如下:
shellcode_analysis1
> 进入 0x7face7c600 位置,分析如下图:
shellcode_analysis2_debugger
> 首先 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 进行分析:
shellcode1_ida
> 绿色、蓝色部分合并完成了栈平衡、寄存器保护与恢复工作;
> 我们在外部用 JS 编写的 Hook 功能代码( onEnter 部分 ),由 BLR X4 ( 0x7F7D8D8360 ) 跳转至 frida-agent-64.so (见下图)来完成;
> 在 JS 中可以打印,甚至修改函数入参的原因是因为入参(前8个在 X0-X7 寄存器上,后面的在栈上)已全部由绿色块指令压入栈中保存,所以在 BLR X4 进行函数调用时,合理设置 X0-X3 寄存器,使其正确的指向栈上某位置尤为关键
findmem_frida_agent
> 我们接下来在 shellcode 最后一条 BR X16 指令上插入断点,分析函数的运行情况
brk_br_x16_analysis
> 当断点触发时 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 分析
shellcode2_ida
> 绿色、蓝色部分代码作用不变,由 BLR X3 ( 0x7F7D8D86C8 ) 跳转至 frida-agent-64.so 来完成外部 JS 写的 Hook 功能代码中 onLeave 的部分
> 最后由 BR X16 返回 Java_com_example_x64_JNI_aal 函数被调用时真正的 LR

 

> 至此 shellcode 部分也大体分析完毕了,此时我们应该能够写出一款简单的 AArch64 inlineHook 工具模型了

--> 2. AArch64 inlineHook 开发

 

> 结合前文的分析,我们的 inlineHook 应该具备以下这几点功能:

  1. Hook 导出函数:即在函数开头进行 Hook ,能够执行原函数,并提供 onEnter 以及 onLeave 两层代码注入点,达到类似 Frida 那种 "代码托管" 一样的效果
  2. Hook 函数内指定地址:Hook 指定位置的汇编指令,仅提供 onEnter 一层代码注入点,因为考虑到在指定位置上 X30( LR ) 寄存器可能已经发生变化,此时用该寄存器做返回判断并不准确,故放弃 onLeave
  3. onEnter 中提供入参的打印/修改操作 ( 本质是寄存器/堆栈内存打印/修改操作 )
  4. onLeave 中提供返回值的打印/修改操作 ( 本质是寄存器/堆栈内存打印/修改操作 )

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

-- Step1 --

> 我们首先来设计 shellcode 部分

 

> 在本简易版工具中,我们的跳板指令选择使用 x16 寄存器的 16 字节 trampoline ,代码如下:

1
2
3
4
5
6
_trampoline_:
    LDR                 X16, x64code0
    BR                  X16
x64code0:
_jmp_addr_:
    .dword 0x1111111111111111

> 接下需要做参数和返回地址入栈工作,以及全寄存器状态保护,代码如下:
codding_shellcode_start
> 接下来调用 Hook 功能函数的 onEnter 部分,并恢复寄存器及栈状态,最后取出返回地址并返回原函数执行
codding_shellcode_middle
> 对于 onLeave 部分的 shellcode 与之大体类似,就不贴图展示了;

-- Step2 --

> 接下来开始编写函数完成 inlineHook 的插入

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
//## Hook目标函数
extern "C" JNIEXPORT jstring JNICALL
Java_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函数的inlineHook
extern "C" JNIEXPORT void JNICALL
Java_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,函数内注释较为详细,就不做过多解释了

 

> 最后我们来编写 onEnteronLeave 函数

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
//## 使用线程局部存储保存原始返回地址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 函数中不设置临时返回地址即可

--> 3. 效果展示及总结

 

> 仅开启 Hook1 时的效果如下图所示
result_show

 

> #总结:借鉴 Frida 的 inlineHook 原理设计了一款简单的 inlineHook 框架,满足了部分常用需求;关于框架的 trampoline 优化,PC-relative address 相关指令解析执行等工作,待后续继续开发优化...

 

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


[招生]科锐逆向工程师培训46期预科班将于 2023年02月09日 正式开班

收藏
点赞18
打赏
分享
打赏 + 80.00雪花
打赏次数 1 雪花 + 80.00
 
赞赏  Editor   +80.00 2022/07/08 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (13)
雪    币: 779
活跃值: 活跃值 (2413)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
王麻子本人 活跃值 2022-6-14 20:59
2
0
雪    币: 219
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
大佬带我飞 活跃值 2022-6-15 14:06
3
0
路过大佬,手动点赞
雪    币: 6152
活跃值: 活跃值 (18526)
能力值: ( LV12,RANK:540 )
在线值:
发帖
回帖
粉丝
随风而行aa 活跃值 10 2022-6-15 16:08
4
0
点个赞 支持
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
yinqingwang 活跃值 2022-6-19 12:29
5
0
点个赞 支持
雪    币: 66
活跃值: 活跃值 (944)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
saloyun 活跃值 2022-6-28 20:20
6
0
点个赞 支持
雪    币: 210
活跃值: 活跃值 (27)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
snowstorm 活跃值 2022-7-8 08:32
7
0
文章里提到的调试工具看起来比较简单有效,是私有的,还是开源的,在哪能够下载呢
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
uiceb 活跃值 2022-7-8 09:43
8
0
点赞
雪    币: 365
活跃值: 活跃值 (1650)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kakasasa 活跃值 2022-7-8 13:57
9
0
感谢分享
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
imBobbyyyyy 活跃值 2022-7-8 16:51
10
0
强啊大佬,大佬牛逼
雪    币: 429
活跃值: 活跃值 (695)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
abucs 活跃值 2022-7-12 18:06
11
0
snowstorm 文章里提到的调试工具看起来比较简单有效,是私有的,还是开源的,在哪能够下载呢
自己写的调试器,目前较为拙劣,暂未开源
雪    币: 0
活跃值: 活跃值 (236)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
一梦不醒 活跃值 2022-7-21 00:20
12
0
大佬强啊
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_hcyhfzvg 活跃值 2022-7-24 19:09
13
0
大佬强啊
雪    币: 595
活跃值: 活跃值 (865)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
skyun 活跃值 3 2022-7-26 13:52
14
0
大佬强啊
游客
登录 | 注册 方可回帖
返回