首页
社区
课程
招聘
[讨论]记初窥Frida的心路历程(小白向)
发表于: 2025-6-30 03:18 1095

[讨论]记初窥Frida的心路历程(小白向)

2025-6-30 03:18
1095

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

从图中我大概明白了 :

  1. JS是应用层的脚本执行环境
  2. 绑定层py用于语言转换,把上层代码和core中的C代码连接
  3. 调用引擎层实现跨平台的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层的疑问,希望这一段摘录总结对大家有帮助。

 

 


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 2025-7-3 19:30 被X66iaM编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (4)
雪    币: 961
活跃值: (1790)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
2025-6-30 03:40
0
雪    币: 961
活跃值: (1790)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3

qwq目前都没入行,以后想干安卓逆向,只看了些教学视频,各位佬有什么推荐的学习资料吗?目前就懂一些arm、apk格式、敲敲fridajs、简单的密码学,感觉都写不上简历啊

最后于 2025-6-30 03:48 被X66iaM编辑 ,原因:
2025-6-30 03:46
0
雪    币: 2802
活跃值: (12057)
能力值: (RANK:385 )
在线值:
发帖
回帖
粉丝
4
感谢分享,学习下.
2025-7-8 10:33
0
雪    币: 579
活跃值: (555)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
建议深入内卷 算法 风控 检测 抓包
2025-7-8 12:34
0
游客
登录 | 注册 方可回帖
返回