-
-
Frida 源码分析
-
发表于:
2024-8-15 16:13
4538
-
简单介绍一下 frida,frida 是一款用来进行 hook 的框架注入工具,其暴露给用户的接口,可以用 js 脚本语言进行方便快捷的 hook 和注入,而且其支持三种主流平台:pc/ios/andriod,尤其在安卓平台上,用 frida 可以说是必备技能。
但是,在使用的大多时候,我们都只是使用 js 代码对指定的地址/java 对象进行了 hook,其实现的原理并没有认真的去分析,近来觉得技术上有了瓶颈,逐花时间尝试对其原理进行分析。
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
.
├── frida-clr
├── frida-core frida 的主要功能实现模块
├── frida-gum frida的基础框架,提供 inline hook、代码追踪、内存监控、符号查找等
├── frida-qml qt的qml界面
├── frida-tools frida-tools
└── releng 编译相关
frida-gum
├── bindings 绑定js平台所用入口
│ ├── gumjs
│ └── gumpp
├── docs
├── ext 主要是win平台的调试相关
│ ├── dbghelp
│ └── symsrv
├── gum gum项目的主要实现目录,包括注入、库相关、内存管理、api解决等,子目录包含对不同平台封装的一些汇编指令
│ ├── arch-arm
│ ├── arch-arm64
│ ├── arch-mips
│ ├── arch-x86
│ ├── backend-arm
│ ├── backend-arm64
│ ├── backend-darwin
│ ├── backend-dbghelp
│ ├── backend-elf
│ ├── backend-freebsd
│ ├── backend-libdwarf
│ ├── backend-libunwind
│ ├── backend-linux
│ ├── backend-mips
│ ├── backend-posix
│ ├── backend-qnx
│ ├── backend-windows
│ └── backend-x86
├── libs 导出目录
│ └── gum
├── subprojects 导入目录
├── tests
│ ├── core
│ ├── data
│ ├── gumjs
│ ├── gumpp
│ ├── heap
│ ├── prof
│ └── stubs
├── tools
└── vapi vala的支持目录
frida-core 一些上层实现
├── inject 调api
├── lib 库
│ ├── agent 注入时调用的库
│ ├── base
│ ├── gadget
│ ├── payload
│ ├── pipe
│ └── selinux
├── portal
├── server
├── src
│ ├── api
│ ├── compiler
│ ├── darwin
│ ├── droidy
│ ├── freebsd
│ ├── fruity
│ ├── linux
│ ├── qnx
│ ├── socket
│ └── windows
├── tests
│ ├── labrats
│ └── pipe
├── tools
└── vapi
|
把其源码结构看清之后,单独把 gum 项目和 core 项目摘出来,gum 项目是整个 frida 的基础框架,包括注入,不同平台,不同架构等基础转换实现原理都在这个项目中。
- 目的 学习对于安卓程序 hook java 层代码
- 猜想 在内存中查找 dex 文件,匹配对应的类和方法修改其指向的函数地址
在 js 中调用的 Hook java 层的代码位于另一个项目分支 frida_java_bridge 中,其使用了 gum 项目中的 native 层 hook 函数。
流程如下:frida-js->frida-gum-> 调用 native_jni->jni 链接 java
首先需要获取系统加载的虚拟机,使用 JNI_GetCreatedJavaVMs
获取,该函数是 jvm 的导出函数,由 libart.so 或 libdvm.so 进行导出
1
2
3
4
5
6
7
|
const vms = Memory.alloc(pointerSize);
const vmCount = Memory.alloc(jsizeSize);
checkJniResult( 'JNI_GetCreatedJavaVMs' , temporaryApi.JNI_GetCreatedJavaVMs(vms, 1 , vmCount));
if (vmCount.readInt() === 0 ) {
return null ;
}
temporaryApi.vm = vms.readPointer();
|
有了系统运行的虚拟机,就可以调用虚拟机中的 jni 函数。
vm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const handle = api.vm;
let attachCurrentThread = null ;
let detachCurrentThread = null ;
let getEnv = null ;
function initialize () {
const vtable = handle.readPointer();
const options = {
exceptions: 'propagate'
};
attachCurrentThread = new NativeFunction(vtable.add(4 * pointerSize).readPointer(), 'int32' , [ 'pointer' , 'pointer' , 'pointer' ], options);
detachCurrentThread = new NativeFunction(vtable.add(5 * pointerSize).readPointer(), 'int32' , [ 'pointer' ], options);
getEnv = new NativeFunction(vtable.add(6 * pointerSize).readPointer(), 'int32' , [ 'pointer' , 'pointer' , 'int32' ], options);
}
|
调用 NativeFunction 获取 vm 虚函数表指针,实现最基础的 js 调用 jvm。
NativeFunction 实现原理,将 native 层的调用格式转换成 js 能够直接调的格式,相当于导出了 jvm 的函数。
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
|
class NativeFunction extends Function {
handle: BNativePointer;
#retType: Marshaler;
#argTypes: Marshaler[];
constructor(address: BNativePointer, retType: NativeFunctionReturnType, argTypes: NativeFunctionArgumentType[]) {
super ();
this .handle = address;
this . #retType = getMarshalerFor(retType);
this . #argTypes = argTypes.map(getMarshalerFor);
return new Proxy( this , {
apply(target, thiz, args) {
return target._invoke(args);
}
});
}
_invoke(args: any[]): any {
const nativeArgs = args.map((v, i) => this . #argTypes[i].toNative(v));
const nativeRetval = _invoke( this .handle.$v, ...nativeArgs);
return this . #retType.fromNative(nativeRetval);
}
}
|
实现了 jvm 的函数导出和调用,接下来还需要定位到 class,在 jvm 中定位一个加载的 class 通过 findclass
来找到。
在 java 环境下,所有可执行的程序被编译成 class 文件也叫类,类从被加载到虚拟机内存中开始到卸载出内存为止,整个生命周期为 加载-》验证-》准备-》解析-》初始化-》使用-》卸载
。
加载
类加载器
1
2
|
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
|
[注意]看雪招聘,专注安全领域的专业人才平台!