首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
35
8
Frida 源码分析
发表于: 2024-8-15 16:13
9749
Frida 源码分析
zhnnmsl
2024-8-15 16:13
9749
# Frida 源码分析 # Frida 简介 简单介绍一下 frida,frida 是一款用来进行 hook 的框架注入工具,其暴露给用户的接口,可以用 js 脚本语言进行方便快捷的 hook 和注入,而且其支持三种主流平台:pc/ios/andriod,尤其在安卓平台上,用 frida 可以说是必备技能。 但是,在使用的大多时候,我们都只是使用 js 代码对指定的地址/java 对象进行了 hook,其实现的原理并没有认真的去分析,近来觉得技术上有了瓶颈,逐花时间尝试对其原理进行分析。 # 源码结构 ```cpp . ├── 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-core 把其源码结构看清之后,单独把 gum 项目和 core 项目摘出来,gum 项目是整个 frida 的基础框架,包括注入,不同平台,不同架构等基础转换实现原理都在这个项目中。 ## Inject--Java 层注入 - 目的 学习对于安卓程序 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 进行导出 ```java 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 ```javascript 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 的函数。 ```javascript 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); } }); } // 真实调用位置,在ts里代表陷阱函数,执行到代理函数时,此处会调用 _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`` 来找到。 ### Jvm 下的类 在 java 环境下,所有可执行的程序被编译成 class 文件也叫类,类从被加载到虚拟机内存中开始到卸载出内存为止,整个生命周期为 `加载-》验证-》准备-》解析-》初始化-》使用-》卸载`。 #### 加载 ##### 类加载器 > ``` > 类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。 > 每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。 > ``` 其作用是将一个 class 文件的字节码以 classloader 定义的方式加载到虚拟机的内存中,在虚拟机内存中有一块内存位置叫代码区,类加载以自己的规则去声明类的内存。 在 jvm 启动的时候会加载需要的类,在加载时会先判断当前类是否已经加载,未加载才会调用加载器进行加载。加载器加载时先会调用父类加载器,如果加载不成功再使用该类的加载器加载,这种机制被称为双亲委派。 可以自己实现一个类加载器,需要继承 classLoader 抽象类 ```java ClassLoader 类有两个关键的方法: protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。 protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。 ``` ##### 双亲委派 > ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 > 虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。 ```java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //首先,检查该类是否已经加载过 Class c = findLoadedClass(name); if (c == null) { //如果 c 为 null,则说明该类没有被加载过 long t0 = System.nanoTime(); try { if (parent != null) { //当父类的加载器不为空,则通过父类的loadClass来加载该类 c = parent.loadClass(name, false); } else { //当父类的加载器为空,则调用启动类加载器来加载该类 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //非空父类的类加载器无法找到相应的类,则抛出异常 } if (c == null) { //当父类加载器无法加载时,则调用findClass方法来加载该类 //用户可通过覆写该方法,来自定义类加载器 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) { //对类进行link操作 resolveClass(c); } return c; } } ``` 总结一下双亲委派模型的执行流程: - 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。 - 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。 > JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。 回到 frida 中,了解了类的加载,那么 frida 使用 findclass 就必须要先获取到 java 类的 classloader,但是 frida 所在的线程是通过 pthread_create 创建,然后调用 AttachCurrentThread 获取的 JNIEnv,此时 FindClass 只会从系统的 classloader 开始查找,所以 app 自身的类是无法通过 env->findClass 来获取。因此需要手工的获取到加载该 app 的 classloader。 ```java this.perform = function (fn) { assertJavaApiIsAvailable(); //目标进程不是app,并且classloader已经初始化 if (!isAppProcess() || classFactory.loader !== null) { threadsInPerform++; try { vm.perform(fn); } catch (e) { setTimeout(() => { throw e; }, 0); } finally { threadsInPerform--; } } else { //第一次调用java.perform时,会先获取加载该app的classloader 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) { //获取到加载该app的classloader classFactory.loader = app.getClassLoader(); performPending(); // already initialized, continue } 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--; } } } }; ``` frida 使用 Java.use 来获得类对象的引用: ```java //lib/class-factory.js this.use = function (className) { let C = classes[className]; //先从缓存中查找 if (!C) { const env = vm.getEnv(); //获取jni_env, 调用native层的JavaVm.GetEnv if (loader !== null) { //loader已经在Java.perform中初始化了 const usedLoader = loader; if (cachedLoaderMethod === null) { cachedLoaderInvoke = env.vaMethod('pointer', ['pointer']); cachedLoaderMethod = loader.loadClass.overload('java.lang.String').handle; } const getClassHandle = function (env) { const classNameValue = env.newStringUtf(className); const tid = Process.getCurrentThreadId(); ignore(tid); try { //env.handle 指向jni层的JNIEnv, 利用jni调用classloader.loadClass(className) return cachedLoaderInvoke(env.handle, usedLoader.$handle, cachedLoaderMethod, classNameValue); } finally { unignore(tid); env.deleteLocalRef(classNameValue); } }; //构建对应java类的wrapper C = ensureClass(getClassHandle, className); } } ``` ### Hook java 层 对于安卓,谷歌将虚拟机需要加载的 class 文件替换成了 dex 文件,修改后的 dex 文件格式如下  具体结构和 pe 文件大同小异,也是由文件头规范哪种规格,指针指向文件偏移查找对应 code or data。 那 hook 的实现其实也是类似 pe 里的 hook,不过谷歌给我们提供了一个 hook 函数 `dvmUseJNIBridge` ```java class DalvikMethodMangler { constructor (methodId) { this.methodId = methodId; this.originalMethod = null; } replace (impl, isInstanceMethod, argTypes, vm, api) { const { methodId } = this; this.originalMethod = Memory.dup(methodId, DVM_METHOD_SIZE); let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0); if (isInstanceMethod) { argsSize++; } /* 设置成native函数 * make method native (with kAccNative) * insSize and registersSize are set to arguments size */ const accessFlags = (methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS).readU32() | kAccNative) >>> 0; const registersSize = argsSize; const outsSize = 0; const insSize = argsSize; // 按照frida的注释,这段就是在修改method的属性使其以native函数运行 methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS).writeU32(accessFlags); methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE).writeU16(registersSize); methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE).writeU16(outsSize); methodId.add(DVM_METHOD_OFFSET_INS_SIZE).writeU16(insSize); methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO).writeU32(computeDalvikJniArgInfo(methodId)); api.dvmUseJNIBridge(methodId, impl);//函数作用将method的nativeFunc成员变量替换为impl函数,impl函数是经过转换的native函数 } ··· } ``` 在 art 中的 hook 没有这么简单 ```java class ArtMethodMangler { constructor (opaqueMethodId) { const methodId = unwrapMethodId(opaqueMethodId); this.methodId = methodId; this.originalMethod = null; this.hookedMethodId = methodId; this.replacementMethodId = null; this.interceptor = null; } replace (impl, isInstanceMethod, argTypes, vm, api) { const { kAccCompileDontBother, artNterpEntryPoint } = api; this.originalMethod = fetchArtMethod(this.methodId, vm); // 对methodid进行解析,将原函数进行保存 const originalFlags = this.originalMethod.accessFlags; if ((originalFlags & kAccXposedHookedMethod) !== 0 && xposedIsSupported()) { const hookInfo = this.originalMethod.jniCode; this.hookedMethodId = hookInfo.add(2 * pointerSize).readPointer(); this.originalMethod = fetchArtMethod(this.hookedMethodId, vm); } const { hookedMethodId } = this; const replacementMethodId = cloneArtMethod(hookedMethodId, vm); // 在内存中拷贝一份methodID的结构 this.replacementMethodId = replacementMethodId; // 将拷贝的methodID结构中规定的入口点等全部改成hook的结构 patchArtMethod(replacementMethodId, { jniCode: impl, accessFlags: ((originalFlags & ~(kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag)) | kAccNative | kAccCompileDontBother) >>> 0, quickCode: api.artClassLinker.quickGenericJniTrampoline, interpreterCode: api.artInterpreterToCompiledCodeBridge }, vm); // Remove kAccFastInterpreterToInterpreterInvoke and kAccSkipAccessChecks to disable use_fast_path // in interpreter_common.h let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag; if ((originalFlags & kAccNative) === 0) { hookedMethodRemovedFlags |= kAccSkipAccessChecks; } patchArtMethod(hookedMethodId, { accessFlags: ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0 }, vm); const quickCode = this.originalMethod.quickCode; // Replace Nterp quick entrypoints with art_quick_to_interpreter_bridge to force stepping out // of ART's next-generation interpreter and use the quick stub instead. if (artNterpEntryPoint !== undefined && quickCode.equals(artNterpEntryPoint)) { patchArtMethod(hookedMethodId, { quickCode: api.artQuickToInterpreterBridge }, vm); } if (!isArtQuickEntrypoint(quickCode)) { const interceptor = new ArtQuickCodeInterceptor(quickCode); interceptor.activate(vm); this.interceptor = interceptor; } artController.replacedMethods.set(hookedMethodId, replacementMethodId); notifyArtMethodHooked(hookedMethodId, vm); } ··· } ``` 其中两个函数实现 ```javascript function fetchArtMethod (methodId, vm) { const artMethodSpec = getArtMethodSpec(vm); // 自己实现的一个函数,提取os中的虚函数表,遍历得到ArtMethod结构,并将所有偏移保存到offset中 const artMethodOffset = artMethodSpec.offset; return (['jniCode', // JNI桥接代码的入口点。当一个Java方法通过JNI调用本地(Native)代码时,这个字段指向的就是该JNI方法的执行入口。此字段在JNI方法(如用native关键字声明的方法)中非常重要,用于连接Java和C/C++代码。 'accessFlags', // 位掩码(bitmask) 'quickCode', // 方法的快速编译代码入口。ART中引入了AOT(Ahead-of-Time)编译,将Java方法编译为本地机器码以加快执行速度。这个字段存储了编译后的代码地址,调用该方法时将直接跳转到此处执行机器码,从而提升性能。 'interpreterCode'] // 指向解释器执行入口。当一个方法没有被编译为机器码,或者当前的执行模式是解释执行时,这个字段指向的方法代码会由解释器逐条解释并执行。解释器执行的速度比编译后的代码慢,但对于一些特定场景(如动态加载代码)依然非常有用。 .reduce((original, name) => { const offset = artMethodOffset[name]; if (offset === undefined) { return original; } const address = methodId.add(offset); const read = (name === 'accessFlags') ? readU32 : readPointer; original[name] = read.call(address); return original; }, {})); } function patchArtMethod (methodId, patches, vm) { const artMethodSpec = getArtMethodSpec(vm); const artMethodOffset = artMethodSpec.offset; Object.keys(patches).forEach(name => { const offset = artMethodOffset[name]; if (offset === undefined) { return; } const address = methodId.add(offset); const write = (name === 'accessFlags') ? writeU32 : writePointer; write.call(address, patches[name]); }); } ``` 在 art 中描述方法的方式被修饰到了 ArtMethod 中,对于 frida 来说,重要的只有'jniCode', 'accessFlags', 'quickCode', 'interpreterCode'四个字段,而这四个字段代表了一个函数的执行方式和行为,替换了之后便达到了 hook 的目的。 > 总的来说,不是查找的 dex 文件,是直接在虚拟机中找到对应的类再找到对应的 methodID 再进行修改,但是具体实现确实没有想的这么简单,需要对 jvm 和 dvm/art 中关于方法的描述了解透彻才能有这么好的思路
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#基础理论
#HOOK注入
#源码框架
收藏
・
35
点赞
・
8
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
zzzhangyu
谢谢你的细致分析,受益匪浅!
2026-3-3 15:08
mb_bcgnztsa
谢谢你的细致分析,受益匪浅!
2025-11-12 10:10
mb_syelvztw
感谢你分享这么好的资源!
2025-4-10 16:30
clz3216
你的分享对大家帮助很大,非常感谢!
2025-2-16 16:25
sinker_
期待更多优质内容的分享,论坛有你更精彩!
2024-8-18 03:22
huangjw
感谢你分享这么好的资源!
2024-8-17 14:15
你瞒我瞒
非常支持你的观点!
2024-8-16 15:43
xsSkyFall
感谢你的积极参与,期待更多精彩内容!
2024-8-16 09:25
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
11
)
快乐的小跳蛙
雪 币:
910
活跃值:
(5347)
能力值:
( LV2,RANK:10 )
在线值:
发帖
2
回帖
87
粉丝
3
关注
私信
快乐的小跳蛙
2
楼
tql
2024-8-15 17:01
0
mb_ldbucrik
雪 币:
6
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
691
粉丝
7
关注
私信
mb_ldbucrik
3
楼
学到了,感谢楼主
2024-8-16 00:07
0
NSky99
雪 币:
3282
活跃值:
(3904)
能力值:
( LV3,RANK:20 )
在线值:
发帖
1
回帖
42
粉丝
10
关注
私信
NSky99
4
楼
nb
2024-8-16 08:51
0
TrumpWY
雪 币:
887
活跃值:
(1105)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
79
粉丝
2
关注
私信
TrumpWY
5
楼
tql
2024-8-16 09:15
0
KingSelyF
雪 币:
1290
活跃值:
(6333)
能力值:
( LV6,RANK:81 )
在线值:
发帖
11
回帖
291
粉丝
48
关注
私信
KingSelyF
1
6
楼
收藏=学会
2024-8-16 10:25
0
院士
雪 币:
7145
活跃值:
(7644)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
261
粉丝
5
关注
私信
院士
7
楼
楼主,native层呢?
2024-8-16 12:52
0
西瓜帅
雪 币:
1825
活跃值:
(2963)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
76
粉丝
1
关注
私信
西瓜帅
8
楼
学到了
2024-8-17 09:45
0
西瓜帅
雪 币:
1825
活跃值:
(2963)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
76
粉丝
1
关注
私信
西瓜帅
9
楼
学到了,感谢楼主
2024-8-17 09:46
0
NSky99
雪 币:
3282
活跃值:
(3904)
能力值:
( LV3,RANK:20 )
在线值:
发帖
1
回帖
42
粉丝
10
关注
私信
NSky99
10
楼
tql
2024-11-18 19:27
0
dawnsky1111
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
25
粉丝
0
关注
私信
dawnsky1111
11
楼
66
2025-10-1 17:53
0
zylyy
雪 币:
144
活跃值:
(2723)
能力值:
( LV2,RANK:10 )
在线值:
发帖
11
回帖
490
粉丝
3
关注
私信
zylyy
12
楼
学到了,感谢楼主
2025-11-28 13:26
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
zhnnmsl
2
发帖
12
回帖
20
RANK
关注
私信
他的文章
Frida 源码分析
9749
[原创]某视频平台关键 so vm 解释器还原
46758
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部