简单介绍一下 frida,frida 是一款用来进行 hook 的框架注入工具,其暴露给用户的接口,可以用 js 脚本语言进行方便快捷的 hook 和注入,而且其支持三种主流平台:pc/ios/andriod,尤其在安卓平台上,用 frida 可以说是必备技能。
但是,在使用的大多时候,我们都只是使用 js 代码对指定的地址/java 对象进行了 hook,其实现的原理并没有认真的去分析,近来觉得技术上有了瓶颈,逐花时间尝试对其原理进行分析。
把其源码结构看清之后,单独把 gum 项目和 core 项目摘出来,gum 项目是整个 frida 的基础框架,包括注入,不同平台,不同架构等基础转换实现原理都在这个项目中。
在 js 中调用的 Hook java 层的代码位于另一个项目分支 frida_java_bridge 中,其使用了 gum 项目中的 native 层 hook 函数。
流程如下:frida-js->frida-gum-> 调用 native_jni->jni 链接 java
首先需要获取系统加载的虚拟机,使用 JNI_GetCreatedJavaVMs
获取,该函数是 jvm 的导出函数,由 libart.so 或 libdvm.so 进行导出
有了系统运行的虚拟机,就可以调用虚拟机中的 jni 函数。
vm.js
调用 NativeFunction 获取 vm 虚函数表指针,实现最基础的 js 调用 jvm。
NativeFunction 实现原理,将 native 层的调用格式转换成 js 能够直接调的格式,相当于导出了 jvm 的函数。
实现了 jvm 的函数导出和调用,接下来还需要定位到 class,在 jvm 中定位一个加载的 class 通过 findclass
来找到。
在 java 环境下,所有可执行的程序被编译成 class 文件也叫类,类从被加载到虚拟机内存中开始到卸载出内存为止,整个生命周期为 加载-》验证-》准备-》解析-》初始化-》使用-》卸载
。
其作用是将一个 class 文件的字节码以 classloader 定义的方式加载到虚拟机的内存中,在虚拟机内存中有一块内存位置叫代码区,类加载以自己的规则去声明类的内存。
在 jvm 启动的时候会加载需要的类,在加载时会先判断当前类是否已经加载,未加载才会调用加载器进行加载。加载器加载时先会调用父类加载器,如果加载不成功再使用该类的加载器加载,这种机制被称为双亲委派。
可以自己实现一个类加载器,需要继承 classLoader 抽象类
ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。
总结一下双亲委派模型的执行流程:
JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
回到 frida 中,了解了类的加载,那么 frida 使用 findclass 就必须要先获取到 java 类的 classloader,但是 frida 所在的线程是通过 pthread_create 创建,然后调用 AttachCurrentThread 获取的 JNIEnv,此时 FindClass 只会从系统的 classloader 开始查找,所以 app 自身的类是无法通过 env->findClass 来获取。因此需要手工的获取到加载该 app 的 classloader。
frida 使用 Java.use 来获得类对象的引用:
对于安卓,谷歌将虚拟机需要加载的 class 文件替换成了 dex 文件,修改后的 dex 文件格式如下

具体结构和 pe 文件大同小异,也是由文件头规范哪种规格,指针指向文件偏移查找对应 code or data。
那 hook 的实现其实也是类似 pe 里的 hook,不过谷歌给我们提供了一个 hook 函数 dvmUseJNIBridge
在 art 中的 hook 没有这么简单
其中两个函数实现
在 art 中描述方法的方式被修饰到了 ArtMethod 中,对于 frida 来说,重要的只有'jniCode', 'accessFlags', 'quickCode', 'interpreterCode'四个字段,而这四个字段代表了一个函数的执行方式和行为,替换了之后便达到了 hook 的目的。
总的来说,不是查找的 dex 文件,是直接在虚拟机中找到对应的类再找到对应的 methodID 再进行修改,但是具体实现确实没有想的这么简单,需要对 jvm 和 dvm/art 中关于方法的描述了解透彻才能有这么好的思路
.
├── 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
.
├── 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
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();
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();
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);
}
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);
}
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);
}
}
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);
}
}
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
ClassLoader 类有两个关键的方法:
protected
Class loadClass(String name,
boolean
resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为
true
,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
protected
Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
ClassLoader 类有两个关键的方法:
protected
Class loadClass(String name,
boolean
resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为
true
,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
protected
Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
protected
Class<?> loadClass(String name,
boolean
resolve)
throws
ClassNotFoundException
{
synchronized
(getClassLoadingLock(name)) {
Class c = findLoadedClass(name);
if
(c ==
null
) {
long
t0 = System.nanoTime();
try
{
if
(parent !=
null
) {
c = parent.loadClass(name,
false
);
}
else
{
c = findBootstrapClassOrNull(name);
}
}
catch
(ClassNotFoundException e) {
}
if
(c ==
null
) {
long
t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if
(resolve) {
resolveClass(c);
}
return
c;
}
}
protected
Class<?> loadClass(String name,
boolean
resolve)
throws
ClassNotFoundException
{
synchronized
(getClassLoadingLock(name)) {
Class c = findLoadedClass(name);
if
(c ==
null
) {
long
t0 = System.nanoTime();
try
{
if
(parent !=
null
) {
c = parent.loadClass(name,
false
);
}
else
{
c = findBootstrapClassOrNull(name);
}
}
catch
(ClassNotFoundException e) {
}
if
(c ==
null
) {
long
t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if
(resolve) {
resolveClass(c);
}
return
c;
}
}
this
.perform = function (fn) {
assertJavaApiIsAvailable();
if
(!isAppProcess() || classFactory.loader !==
null
) {
threadsInPerform++;
try
{
vm.perform(fn);
}
catch
(e) {
setTimeout(() => {
throw
e; },
0
);
}
finally
{
threadsInPerform--;
}
}
else
{
pending.push(fn);
if
(pending.length ===
1
) {
threadsInPerform++;
try
{
vm.perform(() => {
const
ActivityThread = classFactory.use(
'android.app.ActivityThread'
);
const
app = ActivityThread.currentApplication();
if
(app !==
null
) {
classFactory.loader = app.getClassLoader();
performPending();
}
else
{
const
m = ActivityThread.getPackageInfoNoCheck;
let initialized =
false
;
m.implementation = function () {
const
apk = m.apply(
this
, arguments);
if
(!initialized) {
initialized =
true
;
classFactory.loader = apk.getClassLoader();
performPending();
}
return
apk;
};
}
});
}
finally
{
threadsInPerform--;
}
}
}
};
this
.perform = function (fn) {
assertJavaApiIsAvailable();
if
(!isAppProcess() || classFactory.loader !==
null
) {
threadsInPerform++;
try
{
vm.perform(fn);
}
catch
(e) {
setTimeout(() => {
throw
e; },
0
);
}
finally
{
threadsInPerform--;
}
}
else
{
pending.push(fn);
if
(pending.length ===
1
) {
threadsInPerform++;
try
{
[培训]科锐逆向工程师培训第53期2025年7月8日开班!