Frida 源码分析
Frida 简介
简单介绍一下 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 |
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 进行导出
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); } }); }// 真实调用位置,在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 文件也叫类,类从被加载到虚拟机内存中开始到卸载出内存为止,整个生命周期为 加载-》验证-》准备-》解析-》初始化-》使用-》卸载。
加载
类加载器
12类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
其作用是将一个 class 文件的字节码以 classloader 定义的方式加载到虚拟机的内存中,在虚拟机内存中有一块内存位置叫代码区,类加载以自己的规则去声明类的内存。
在 jvm 启动的时候会加载需要的类,在加载时会先判断当前类是否已经加载,未加载才会调用加载器进行加载。加载器加载时先会调用父类加载器,如果加载不成功再使用该类的加载器加载,这种机制被称为双亲委派。
可以自己实现一个类加载器,需要继承 classLoader 抽象类
1 2 3 | 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 实例的父类加载器。
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 | 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。
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 | 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 来获得类对象的引用:
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 | //lib/class-factory.jsthis.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
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 | 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 没有这么简单
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 | 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); }···} |
其中两个函数实现
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 | 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 中关于方法的描述了解透彻才能有这么好的思路