0x01 前言
最近略读了一下看雪3W和XJB的安卓hook相关的课程,得到了一堆JS代码,猛猛把各个代码块的作用记成笔记,但是心中仍怅然无比,没有那种掌握知识的满足感。我想还是要明白每个JS语句下的动作,于是将学习的心路历程和相关资料整理成这篇文章。
叠甲
1.本文适合刚接触使用frida,了解NDK的同学茶余饭后观看
2.本文大多知识来源于聪明的claude-4-sonnet,用伪码大致描述方法论而非针对frida源码的阅读,如有错误,请大家批评指正,带带我~
3.既然是心路历程,免不了一些抽象言语,望理解
0x02 接触frida
打开某个java层hook课程,安装frida后把demo拖进jadx,照葫芦画瓢写了一段JS代码,用于hook一个登录界面输入账户密码后进行验证的a函数,如下图。



(PS:写这个js代码的时候刚把com. 打出来,后面cursor tab提示的代码连包名都一模一样,真没绷住)
那么照葫芦画瓢能有什么问题,顺利hook到了hex编码形式的a函数入参。
上述提及的课程都在教我们如何应用frida编写js代码进行hook,笔者看完后心里痒的不行啊,他到底做了什么我是一点不知道,瞬间联想到以后找工作的时候被面试官拷打的场景,头皮发麻,于是另起了文件夹"FridaHook",并对ai开始了提问学习,我想主要的疑点是:作为第三方程序,frida究竟如何hook一个java层或so层的函数,具体到如何获取函数地址、调用函数、拦截函数的参数和结果。
0x03 问Ai frida底层实现是什么
Frida hook java 的底层实现是什么 如何获取的apk函数信息,我想既然是第三方程序,免不了注入的操作。
它丢给我两张架构图

用户Python代码
↓
frida-python (绑定层)
↓ [语言转换]
frida-core (核心层)
↓ [平台抽象]
frida-gum (引擎层)
↓ [Hook实现]
平台特定代码 (Windows/Linux/Android)
↓ [系统调用]
操作系统API
从图中我大概明白了 :
- JS是应用层的脚本执行环境
- 绑定层py用于语言转换,把上层代码和core中的C代码连接
- 调用引擎层实现跨平台的hook算法,最后调用api实现功能。
下面给一个数据流动示例:
用户调用: session.attach(1234)
↓
Python绑定: py_session_attach()
↓
核心层: frida_session_attach_sync()
↓
引擎层: gum_interceptor_attach()
↓
平台层:
- Windows: OpenProcess() + CreateRemoteThread()
- Linux: ptrace() + mmap()
- Android: SELinux检查 + Zygote注入
// JavaScript应用层 - 脚本执行环境
Java.perform(function() {
// 用户看到的是这些简洁的API
var targetClass = Java.use("com.example.Target");
targetClass.method.implementation = function() {
// 用户代码
};
});
# 绑定层 frida-python/frida/__init__.py
class Session:
def __init__(self, impl):
self._impl = impl # 这里连接到C层实现
def create_script(self, source):
# Python调用 -> C函数调用
return Script(self._impl.create_script(source))
def attach(self, target):
# 语言绑定:Python参数 -> C参数转换
return self._impl.attach(target)
// 绑定层 frida-python的C扩展部分
static PyObject *
py_session_create_script(PySession* self, PyObject* args) {
const char* source;
if (!PyArg_ParseTuple(args, "s", &source))
return NULL;
// 调用frida-core的C API
FridaScript* script = frida_session_create_script_sync(
self->handle, source, NULL, NULL);
return py_script_new(script);
}
//核心层 frida-core/src/api.vala - 平台无关的核心接口
public class Session : Object {
public async Script create_script(string source) throws Error {
// 统一的脚本创建接口,隐藏平台差异
return yield backend.create_script(source);
}
public async void attach(uint pid) throws Error {
// 统一的进程附加接口
yield backend.attach_to_process(pid);
}
}
// 平台抽象接口
public interface Backend : Object {
public abstract async void inject_library_file(uint pid, string path);
public abstract async void attach_to_process(uint pid);
}
//平台适配
// frida-core 会根据运行平台选择具体实现
#ifdef G_OS_WIN32
#include "windows/windows-host-session.c"
#elif defined(HAVE_LINUX)
#include "linux/linux-host-session.c"
#elif defined(HAVE_DARWIN)
#include "darwin/darwin-host-session.c"
#endif
// 引擎层 frida-gum/gum/guminterceptor.c - Hook引擎核心
GumInterceptor * gum_interceptor_obtain(void) {
static GumInterceptor * interceptor = NULL;
if (interceptor == NULL) {
interceptor = g_object_new(GUM_TYPE_INTERCEPTOR, NULL);
// 初始化Hook引擎
gum_interceptor_init(interceptor);
}
return interceptor;
}
gboolean gum_interceptor_attach(GumInterceptor * self,
gpointer function_address,
GumInvocationListener * listener) {
// 1. 分配代码页
GumCodeAllocator * allocator = self->code_allocator;
// 2. 生成跳转代码
gpointer trampoline = gum_code_allocator_alloc_trampoline(allocator);
// 3. 原子性地修改目标函数
return gum_memory_patch_code(function_address, trampoline, GUM_PATCH_SIZE);
}// 引擎层 frida-gum/gum/gummemory.c - 内存操作
gboolean gum_memory_patch_code(gpointer address,
gpointer new_code,
gsize size) {
// 1. 修改内存保护属性
gum_mprotect(address, size, GUM_PAGE_RWX);
// 2. 写入新代码
memcpy(address, new_code, size);
// 3. 清除指令缓存
gum_clear_cache(address, size);
// 4. 恢复保护属性
gum_mprotect(address, size, GUM_PAGE_RX);
return TRUE;
}
//平台层
// WIN实现 frida-gum/gum/backend-windows/gumprocess-windows.c
gboolean gum_windows_attach_to_process(guint pid) {
HANDLE process = OpenProcess(
PROCESS_ALL_ACCESS, FALSE, pid);
if (process == NULL)
return FALSE;
// Windows特有的DLL注入
return gum_windows_inject_dll(process, agent_path);
}
// Linux实现 frida-gum/gum/backend-linux/gumprocess-linux.c
gboolean gum_linux_attach_to_process(guint pid) {
// Linux特有的ptrace机制
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
return FALSE;
return gum_linux_inject_so(pid, agent_path);
}
// Android实现 frida-gum/gum/backend-linux/gumprocess-linux.c (Android分支)
gboolean gum_android_attach_to_process(guint pid) {
// Android特有的SELinux处理
if (!gum_android_check_selinux_policy())
return FALSE;
// Zygote进程的特殊处理
return gum_android_inject_into_zygote_child(pid, agent_path);
}
疑点:绑定层中python怎么调用C代码的,没见过
0x04 问Ai 获取java方法的路径
路径1:JNI标准接口 (Frida优先选择)
// frida-java-bridge使用的方式
jmethodID get_method_via_jni(JNIEnv* env, const char* class_name,
const char* method_name, const char* signature) {
// 1. 通过标准JNI API查找类
jclass klass = (*env)->FindClass(env, class_name);
// 2. 通过标准JNI API获取方法ID
jmethodID method = (*env)->GetMethodID(env, klass, method_name, signature);
// 3. jmethodID实际上就是ArtMethod指针
return method; // 这个就是我们要的ArtMethod*
}
路径2:直接解析DEX文件
// 绕过JNI,直接解析DEX文件
ArtMethod* get_method_via_dex_parsing(const char* dex_path,
uint32_t method_idx) {
// 1. 解析DEX文件头
DexFile* dex_file = parse_dex_file(dex_path);
// 2. 找到方法定义
DexMethodId* method_id = &dex_file->method_ids[method_idx];
// 3. 通过ClassLinker解析为ArtMethod
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
ArtMethod* method = class_linker->ResolveMethod(method_id, dex_file);
return method;
}
路径3:ART内部数据结构遍历
// 直接遍历ART Runtime的内部表
ArtMethod* get_method_via_art_traversal(const char* class_descriptor,
const char* method_name) {
// 1. 获取ART Runtime实例
Runtime* runtime = Runtime::Current();
// 2. 获取ClassLinker
ClassLinker* class_linker = runtime->GetClassLinker();
// 3. 遍历已加载的类
ClassTable* class_table = class_linker->GetClassTable();
// 4. 在类表中查找目标类
mirror::Class* klass = class_table->Lookup(class_descriptor);
// 5. 遍历类的方法表
for (size_t i = 0; i < klass->NumDirectMethods(); i++) {
ArtMethod* method = &klass->GetDirectMethod(i);
if (strcmp(method->GetName(), method_name) == 0) {
return method;
}
}
return nullptr;
}
路径4:通过Java反射
// 通过Java反射机制获取Method对象,再转换为ArtMethod
ArtMethod* get_method_via_reflection(JNIEnv* env, const char* class_name,
const char* method_name) {
// 1. 获取Class对象
jclass clazz = (*env)->FindClass(env, class_name);
jmethodID get_class = (*env)->GetMethodID(clazz, "getClass", "()Ljava/lang/Class;");
jobject class_obj = (*env)->CallObjectMethod(clazz, get_class);
// 2. 通过反射获取Method对象
jclass class_class = (*env)->FindClass(env, "java/lang/Class");
jmethodID get_method = (*env)->GetMethodID(class_class, "getDeclaredMethod",
"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
jstring method_name_str = (*env)->NewStringUTF(env, method_name);
jobject method_obj = (*env)->CallObjectMethod(class_obj, get_method,
method_name_str, NULL);
// 3. 从Method对象中提取ArtMethod指针
// java.lang.reflect.Method内部有ArtMethod字段
ArtMethod* art_method = extract_art_method_from_reflection(method_obj);
return art_method;
}
路径5:内存搜索和模式匹配
我勒个搜内存啊
// 在内存中搜索ArtMethod结构
ArtMethod* get_method_via_memory_scan(const char* method_name) {
// 1. 扫描ART heap区域
void* heap_start = get_art_heap_start();
void* heap_end = get_art_heap_end();
// 2. 搜索可能的ArtMethod结构
for (void* addr = heap_start; addr < heap_end; addr += sizeof(void*)) {
ArtMethod* candidate = (ArtMethod*)addr;
// 3. 验证是否为有效的ArtMethod
if (is_valid_art_method(candidate)) {
const char* name = get_method_name_from_dex(candidate);
if (strcmp(name, method_name) == 0) {
return candidate;
}
}
}
return nullptr;
}
为什么frida优先使用jni路径获取java方法类?
JNI是官方标准接口,有完整的错误处理,跨Android版本兼容。Android 4.4 到 Android 14 都支持同样的JNI API,而ART内部结构却经常变化。ART适合调试工具系统分析,反射适用于获取隐藏API,绕过访问限制。frida使用了混合策略。
ArtMethod* gum_android_robust_find_method(const char* class_name,
const char* method_name) {
// 1. 首先尝试JNI标准路径
ArtMethod* method = try_jni_path(class_name, method_name);
if (method != NULL) return method;
// 2. JNI失败,尝试反射路径
method = try_reflection_path(class_name, method_name);
if (method != NULL) return method;
// 3. 反射失败,尝试ART内部遍历
method = try_art_traversal_path(class_name, method_name);
return method;
}
下面我们可以聚焦于正题了
0x05 问Ai Frida如何实现java层hook
数据流动
框框给出一堆啊,可以看出涉及到了art虚拟机.arm汇编以及v8引擎。
1. JavaScript Hook注册
Java.use("com.example.App").login.implementation = function(user, pass) {...}
2. Frida解析Hook请求
gum_android_hook_method() 被调用
3. 获取目标方法的ArtMethod
通过JNI GetMethodID() 获取方法指针
4. 替换方法入口点
entry_point_from_quick_compiled_code_ → trampoline
5. 应用调用被Hook的方法
app.login("admin", "123456")
6. ART虚拟机分发调用
method->entry_point_from_quick_compiled_code_()
7. 跳转到Frida trampoline
执行生成的ARM汇编跳转代码
8. Frida处理函数执行
gum_android_method_invocation_handler()
9. 参数转换 Java → JavaScript
jobject[] → v8::Array
10. 执行JavaScript Hook代码
用户的implementation函数被调用
11. 返回值转换 JavaScript → Java
v8::Value → jobject
12. 可选调用原始方法
hook->original_entry_point()
13. 返回到ART虚拟机
正常的Java方法返回流程
基于上述疑点进行提问:
1.entry_point_from_quick_compiled_code_
是Android ART虚拟机中ArtMethod结构的一个关键字段,它指向该Java方法编译后的机器码的入口地址。
ART的三种执行路径
// art/runtime/art_method.h (Android源码)
class ArtMethod {
// 三个不同的方法入口点
void* entry_point_from_interpreter_; // 解释器模式入口
void* entry_point_from_jni_; // JNI调用入口
void* entry_point_from_quick_compiled_code_; // 编译代码入口 ⭐
};
"Quick Compiled Code" 的含义
Quick = ART虚拟机的编译后端名称
┌─────────────────────────────────────┐
│ Java源码: public int add(int a, int b) │
│ { return a + b; } │
└─────────────────────────────────────┘
↓ javac编译
┌─────────────────────────────────────┐
│ DEX字节码: add-int v0, v1, v2 │
│ return v0 │
└─────────────────────────────────────┘
↓ ART Quick编译器
┌─────────────────────────────────────┐
│ ARM64机器码: │
│ add w0, w1, w2 // w0 = w1 + w2 │
│ ret // 返回 │
└─────────────────────────────────────┘
↑
entry_point_from_quick_compiled_code_ 指向这里Frida Hook的核心:替换这个指针
// Frida Hook之后:
ArtMethod login_method = {
.entry_point_from_quick_compiled_code_ = Frida trampoline
};
为什么选择这个入口点Hook?
1. 性能最优:直接在机器码层面拦截
不需要经过解释器,Hook开销最小
2. 覆盖最全:无论方法如何被调用都会经过这个入口
- 直接Java调用
- 反射调用
- JNI调用
都会最终通过这个入口点
3. 时机最佳:在方法真正执行之前拦截
可以完整控制方法的执行流程
2.V8引擎数据转换机制
Java → JavaScript 转换原理,这又是个坑,此前没有接触过qwq
// 真实的V8转换流程
v8::Local<v8::Array> convert_java_args_to_v8(JNIEnv* env,
jobjectArray java_args) {
// 1. 获取V8上下文
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
// 2. 获取Java数组长度
jsize arg_count = (*env)->GetArrayLength(env, java_args);
// 3. 创建JavaScript数组
v8::Local<v8::Array> js_args = v8::Array::New(isolate, arg_count);
// 4. 逐个转换数组元素
for (jsize i = 0; i < arg_count; i++) {
jobject java_obj = (*env)->GetObjectArrayElement(env, java_args, i);
v8::Local<v8::Value> js_value;
if (java_obj == NULL) {
js_value = v8::Null(isolate);
} else {
// 根据Java对象类型进行转换
jclass obj_class = (*env)->GetObjectClass(env, java_obj);
// 获取类名
jmethodID get_class = (*env)->GetMethodID(obj_class,
"getClass", "()Ljava/lang/Class;");
jobject class_obj = (*env)->CallObjectMethod(java_obj, get_class);
jmethodID get_name = (*env)->GetMethodID((*env)->GetObjectClass(env, class_obj),
"getName", "()Ljava/lang/String;");
jstring class_name = (*env)->CallObjectMethod(class_obj, get_name);
const char* name = (*env)->GetStringUTFChars(env, class_name, NULL);
// 类型转换分发
if (strcmp(name, "java.lang.String") == 0) {
// String类型转换
jstring java_str = (jstring)java_obj;
const char* utf_chars = (*env)->GetStringUTFChars(env, java_str, NULL);
js_value = v8::String::NewFromUtf8(isolate, utf_chars).ToLocalChecked();
(*env)->ReleaseStringUTFChars(env, java_str, utf_chars);
} else if (strcmp(name, "java.lang.Integer") == 0) {
// Integer类型转换
jmethodID int_value = (*env)->GetMethodID(obj_class, "intValue", "()I");
jint value = (*env)->CallIntMethod(java_obj, int_value);
js_value = v8::Integer::New(isolate, value);
} else if (strcmp(name, "java.lang.Boolean") == 0) {
// Boolean类型转换
jmethodID bool_value = (*env)->GetMethodID(obj_class, "booleanValue", "()Z");
jboolean value = (*env)->CallBooleanMethod(java_obj, bool_value);
js_value = v8::Boolean::New(isolate, value == JNI_TRUE);
} else {
// 复杂对象:创建Java对象的JavaScript包装器
js_value = create_java_object_wrapper(isolate, java_obj);
}
(*env)->ReleaseStringUTFChars(env, class_name, name);
}
// 5. 设置到JavaScript数组中
js_args->Set(context, i, js_value);
}
return js_args;
}
3.ART方法调用的完整生命周期
// ART方法调用的完整生命周期
void art_method_call_lifecycle() {
// 阶段1: 方法调用开始
// app.login("admin", "123") 在Java代码中被调用
// 阶段2: ART方法分发
// ArtMethod::Invoke() 被调用
// 检查方法的entry_point_from_quick_compiled_code_
// 阶段3: 跳转到Hook(如果已被Hook)
// 执行Frida生成的trampoline代码
// 阶段4: Frida处理
// 参数转换、JavaScript执行、返回值处理
// 阶段5: 返回到ART
void return_to_art_vm(jobject return_value, ArtMethod* method) {
// 1. 将返回值写入ART的返回值寄存器/栈位置
set_art_return_value(return_value, method->GetReturnType());
// 2. 恢复CPU寄存器状态
restore_cpu_registers();
// 3. 清理JNI本地引用
cleanup_jni_local_refs();
// 4. 返回到ART虚拟机的方法返回处理流程
// ART会处理异常检查、GC安全点等
// 5. 最终返回到Java调用点
// 调用app.login()的地方收到返回值
}
}
0x06 结尾
面向AI的学习虽然短平快,但内容仍然不可控,代码落地前都保持怀疑态度,还是需要进行源码级别的学习,才能称得上掌握。
本文到此戛然而止,明天还得上课(狗头),后续再屡屡so层的疑问,希望这一段摘录总结对大家有帮助。
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-7-3 19:30
被X66iaM编辑
,原因: