frida 代码结构:frida-core: Frida core library intended for static linking into bindingsfrida-gum: Low-leve……
frida 代码结构:frida-core : Frida core library intended for static linking into bindingsfrida-gum : Low-level code instrumentation library used by frida-core bindings:frida-python : Frida Python bindingsfrida-node : Frida Node.js bindingsfrida-qml : Frida Qml pluginfrida-swift : Frida Swift bindingsfrida-tools : Frida CLI toolscapstone : instruction disammbler
frida-gum 解析:frida-gum 本身就是一种跨平台的设计. 有两个点需要处理统一: 1. 针对 CPU 架构的代码 2. 针对操作系统 (Backend) 的代码. 同时要在这两个点上构建 CPU/OS 无关代码, 以及规定一些统一的接口.
frida-gum/gum/arch-* 定义的是与 CPU 架构有关的代码, 也就是汇编级操作, 比如汇编指令的读 / 写 / 修复.
frida-gum/gum/backend-* 分两种情况: 1. 定义的是与操作系统有关的代码, 更多是一些内存 / 进程等操作 2. 对 arch 层级代码的封装成统一逻辑
frida-gum/* 对 arch 和 backend 的抽象封装成上层的平台 / 架构无关代码.
frida-gum/bindings/gumjs/: 分 V8 和 Duktape 两个引擎,实现了 Module、Memory、NativeFunction 等功能(https://www.frida.re/docs/javascript-api/)
两种模式
attach 模式 attach 到已经存在的进程,核心原理是 ptrace 修改进程内存,如果进程处于调试状态(traceid 不等于 0),则 attach 失败
spawn 模式 启动一个新的进程并挂起,在启动的同时注入 frida 代码,适用于在进程启动前的一些 hook,如 hook RegisterNative 等,注入完成后调用 resume 恢复进程。
frida-java 解析 源码结构
index.js: vm VM 虚拟机的 wrapper classFactory class 的 wrapper available 逻辑变量, 指明当前的进程是否载入了虚拟机 androidVersion 当前版本号 enumerateLoadedClasses 枚举所有加载的类 enumerateLoadedClassesSync 上面那个 API 的同步版本, 载入完毕才将所有的类作为一个数组返回 enumerateClassLoaders Android N 以上的支持 enumerateClassLoadersSync 同上
classFactory.js: use: 找到类 implementation: 实现一个函数 overloads: $new $alloc $init
vm.js: getEnv perform attachCurrentThread DetachCurrentThread
android.js /global Memory, Module, NativeCallback, NativeFunction, NULL, Process / getApi ensureClassInitialized getAndroidVersion getAndroidApiLevel getArtMethodSpec getArtThreadSpec getArtThreadFromEnv withRunnableArtThread withAllArtThreadsSuspended makeArtClassVisitor makeArtClassLoaderVisitor cloneArtMethod
env.js JNIEnv 的 wrapper
Hook 分析
implementation 区分了 ART 实现和 Dalvik 实现
Dalvik hook 实现frida 兼容了低版本的 Android, 低于 Android 5.0 时,采用 Dalvik 虚拟机,其核心实现在 replaceDalvikImplementation 函数中。
frida 的 Dalvik hook 和 xposed 的 hook 原理相同,都是把要 hook 的 java 函数变成 native 函数,并修改函数的入口为自定义的内容,这样在调用时就会执行自定义的代码。
首先我们看一下 Dalvik 虚拟机执行 java 函数过程:
第 4 步 dvmCallMethodV 会根据 accessFlags 决定调用 native 还是 java 函数,因此修改 accessFlags 后,Dalvik 会认为这个函数是一个 native 函数,便走向了 native 分支。
Java 层的每一个函数在 Dalvik 中都对应一个 Method 数据结构,在源代码中定义如下:
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 //https://android.googlesource.com/platform/dalvik/+/6d874d2bda563ada1034d2b3219b35d800fc6860/vm/oo/Object.h#418 struct Method { ClassObject* clazz; /* method所属的类 public、native等*/ u4 accessFlags; /* 访问标记 */ u2 methodIndex; //method索引 //三个size为边界值,对于native函数,这3个size均等于参数列表的size u2 registersSize; /* ins + locals */ u2 outsSize; u2 insSize; const char* name;//函数名称 /* * Method prototype descriptor string (return and argument types) */ DexProto prototype; /* short-form method descriptor string */ const char* shorty; /* * The remaining items are not used for abstract or native methods. * (JNI is currently hijacking "insns" as a function pointer, set * after the first call. For internal-native this stays null.) */ /* the actual code */ const u2* insns; /* instructions, in memory-mapped .dex */ /* cached JNI argument and return-type hints */ int jniArgInfo; /* * Native method ptr; could be actual function or a JNI bridge. We * don't currently discriminate between DalvikBridgeFunc and * DalvikNativeFunc; the former takes an argument superset (i.e. two * extra args) which will be ignored. If necessary we can use * insns==NULL to detect JNI bridge vs. internal native. */ DalvikBridgeFunc nativeFunc; /* * Register map data, if available. This will point into the DEX file * if the data was computed during pre-verification, or into the * linear alloc area if not. */ const RegisterMap* registerMap; };
replaceDalvikImplementation 修改了 method 中的 accessFlags、registersSize、outsSize、insSize 和 jniArgInfo,将原 java 函数对应的结构体修改为一个 native 函数,并调用 dvmUseJNIBridge(dvmUseJNIBridge 实现代码 )为这个 Method 设置一个 Bridge,改变结构体中的 nativeFunc,指向自定义的函数。
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 function replaceDalvikImplementation (fn) { if (fn === null && dalvikOriginalMethod === null) { return; } //备份原来的method, if (dalvikOriginalMethod === null) { dalvikOriginalMethod = Memory.dup(methodId, DVM_METHOD_SIZE); dalvikTargetMethodId = Memory.dup(methodId, DVM_METHOD_SIZE); } if (fn !== null) { //自定的代码 implementation = implement(f, fn); let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0); if (type === INSTANCE_METHOD) { argsSize++; } // 把method变成native函数 /* * make method native (with kAccNative) * insSize and registersSize are set to arguments size */ const accessFlags = (Memory.readU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS)) | kAccNative) >>> 0; const registersSize = argsSize; const outsSize = 0; const insSize = argsSize; Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS), accessFlags); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE), registersSize); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE), outsSize); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_INS_SIZE), insSize); Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO), computeDalvikJniArgInfo(methodId)); //调用dvmUseJNIBridge为这个Method设置一个Bridge,本质上是修改结构体中的nativeFunc为自定义的implementation函数 api.dvmUseJNIBridge(methodId, implementation); patchedMethods.add(f); } else { patchedMethods.delete(f); Memory.copy(methodId, dalvikOriginalMethod, DVM_METHOD_SIZE); implementation = null; } }
自定义的 js 代码如何生成? implement 的实现
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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 function implement (method, fn) { if (method.hasOwnProperty('overloads')) { throw new Error('Only re-implementing a concrete (specific) method is possible, not a method "dispatcher"'); } const C = method.holder; // eslint-disable-line const type = method.type; const retType = method.returnType; const argTypes = method.argumentTypes; const methodName = method.methodName; const rawRetType = retType.type; const rawArgTypes = argTypes.map((t) => (t.type)); const pendingCalls = method[PENDING_CALLS]; // eslint-disable-line let frameCapacity = 2; const argVariableNames = argTypes.map((t, i) => ('a' + (i + 1))); const callArgs = argTypes.map((t, i) => { if (t.fromJni) { frameCapacity++; return ['argTypes[', i, '].fromJni.call(self, ', argVariableNames[i], ', env)'].join(''); } else { return argVariableNames[i]; } }); let returnCapture, returnStatements, returnNothing; if (rawRetType === 'void') { returnCapture = ''; returnStatements = 'env.popLocalFrame(NULL);'; returnNothing = 'return;'; } else { if (retType.toJni) { frameCapacity++; returnCapture = 'result = '; returnStatements = 'var rawResult;' + 'try {' + 'if (retType.isCompatible.call(this, result)) {' + 'rawResult = retType.toJni.call(this, result, env);' + '} else {' + 'throw new Error("Implementation for " + methodName + " expected return value compatible with \'" + retType.className + "\'.");' + '}'; if (retType.type === 'pointer') { returnStatements += '} catch (e) {' + 'env.popLocalFrame(NULL);' + 'throw e;' + '}' + 'return env.popLocalFrame(rawResult);'; returnNothing = 'return NULL;'; } else { returnStatements += '} finally {' + 'env.popLocalFrame(NULL);' + '}' + 'return rawResult;'; returnNothing = 'return 0;'; } } else { returnCapture = 'result = '; returnStatements = 'env.popLocalFrame(NULL);' + 'return result;'; returnNothing = 'return 0;'; } } let f; eval('f = function (' + ['envHandle', 'thisHandle'].concat(argVariableNames).join(', ') + ') {' + // eslint-disable-line 'var env = new Env(envHandle, vm);' + 'if (env.pushLocalFrame(' + frameCapacity + ') !== JNI_OK) {' + 'return;' + '}' + 'var self = ' + ((type === INSTANCE_METHOD) ? 'new C(thisHandle);' : 'new C(null);') + 'var result;' + 'var tid = Process.getCurrentThreadId();' + 'try {' + 'pendingCalls.add(tid);' + 'if (ignoredThreads[tid] === undefined) {' + returnCapture + 'fn.call(' + ['self'].concat(callArgs).join(', ') + ');' + '} else {' + returnCapture + 'method.call(' + ['self'].concat(callArgs).join(', ') + ');' + '}' + '} catch (e) {' + 'env.popLocalFrame(NULL);' + "if (typeof e === 'object' && e.hasOwnProperty('$handle')) {" + 'env.throw(e.$handle);' + returnNothing + '} else {' + 'throw e;' + '}' + '} finally {' + 'pendingCalls.delete(tid);' + '}' + returnStatements + '};'); Object.defineProperty(f, 'methodName', { enumerable: true, value: methodName }); Object.defineProperty(f, 'type', { enumerable: true, value: type }); Object.defineProperty(f, 'returnType', { enumerable: true, value: retType }); Object.defineProperty(f, 'argumentTypes', { enumerable: true, value: argTypes }); Object.defineProperty(f, 'canInvokeWith', { enumerable: true, value: function (args) { if (args.length !== argTypes.length) { return false; } return argTypes.every((t, i) => (t.isCompatible(args[i]))); } }); return new NativeCallback(f, rawRetType, ['pointer', 'pointer'].concat(rawArgTypes)); }
在自定义的代码里调用原函数?
ART hook 实现frida 的 ART hook 实现也是把 java method 转为 native method, 但 ART 的运行机制不同于 Dalvik, 其实现也较为复杂,这里从 ART 运行机制开始解释。
ART 是一种代替 Dalivk 的新的运行时, 它具有更高的执行效率。ART 虚拟机执行 Java 方法主要有两种模式:quick code 模式和 Interpreter 模式。
quick code 模式:执行 arm 汇编指令
Interpreter 模式:由解释器解释执行 Dalvik 字节码
即使是在 quick code 模式中,也有类方法可能需要以 Interpreter 模式执行。反之亦然。解释执行的类方法通过函数 artInterpreterToCompiledCodeBridge 的返回值调用本地机器指令执行的类方法;本地机器指令执行的类方法通过函数 GetQuickToInterpreterBridge 的返回值调用解释执行的类方法;
ART 中的每一个函数都对应一个 ARTMethod 结构体,其中 entry_point_from_interpreter_ 和 entry_point_from_quick_compiled_code_ 分别表示两种模式的调用入口 ARTMethod 结构如下:
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 //http://androidxref.com/8.1.0_r33/xref/art/runtime/art_method.h#708 class ArtMethod { GcRoot<mirror::Class> declaring_class_; //method所属的class // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access. GcRoot<mirror::PointerArray> dex_cache_resolved_methods_; // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access. GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_; // Access flags; low 16 bits are defined by spec. uint32_t access_flags_; /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */ // Offset to the CodeItem. uint32_t dex_code_item_offset_; // Index into method_ids of the dex file associated with this method. uint32_t dex_method_index_; /* End of dex file fields. */ // Entry within a dispatch table for this method. For static/direct methods the index is into // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the // ifTable. uint32_t method_index_; // Fake padding field gets inserted here. // Must be the last fields in the method. // PACKED(4) is necessary for the correctness of // RoundUp(OFFSETOF_MEMBER(ArtMethod, ptr_sized_fields_), pointer_size). struct PACKED(4) PtrSizedFields { // Method dispatch from the interpreter invokes this pointer which may cause a bridge into // 以interpreter模式调用入口 void* entry_point_from_interpreter_; void* entry_point_from_jni_; //jni函数入口 // 以quick code调用时的函数入口 void* entry_point_from_quick_compiled_code_; } ptr_sized_fields_; }
ART 的执行流程如下图:
如图所示,对于一个 native method, ART 虚拟机首先会尝试 quickcode 模式执行,检查 ARTMethod 结构中的 entry_point_from_quick_compiled_code_ 成员,这里分 3 种情况:
如果函数已经存在 quick code, 则指向这个函数对应的 quick code 的起始地址,而当 quick code 不存在时,它的值则会代表其他的意义;
当一个 java 函数不存在 quick code 时,它的值是函数 artQuickToInterpreterBridge 的地址,用以从 quick 模式切换到 Interpreter 模式来解释执行 java 函数代码;
当一个 java native(JNI)函数不存在 quick code 时,它的值是函数 art_quick_generic_jni_trampoline 的地址,用以执行没有 quick code 的 jni 函数;
因此,如果 frida 把一个 java method 改为 jni method, 显然是不存在 quick code,这时需要将 entry_point_from_quick_compiled_code_ 值修改为 art_quick_generic_jni_trampoline 的地址。
art_quick_generic_jni_trampoline 函数实现比较复杂(代码分析 ),主要负责 jni 调用的准备,包括堆栈的设置,参数的设置等, 该函数最终会调到 entry_point_from_jni_,即 jni 函数的入口。
因此,frida 把 java method 改为 jni method,需要修改 ARTMethod 结构体中的这几个值: access_flags_ = native entry_point_from_jni_ = 自定义代码的入口 entry_point_from_quick_compiled_code_ = art_quick_generic_jni_trampoline 函数的地址 entry_point_from_interpreter_ = artInterpreterToCompiledCodeBridge 函数地址
frida 对 ARTMethod 的修改在 replaceArtImplementation 函数中:
1 2 3 4 5 6 7 8 9 10 patchMethod(methodId, { //jnicode入口entry_point_from_jni_改为自定义的代码 'jniCode': implementation, //修改为access_flags_为native 'accessFlags': (Memory.readU32(methodId.add(artMethodOffset.accessFlags)) | kAccNative | kAccFastNative) >>> 0, //entry_point_from_quick_compiled_code_ 'quickCode': api.artQuickGenericJniTrampoline, //entry_point_from_interpreter_ 'interpreterCode': api.artInterpreterToCompiledCodeBridge });
patchMethod 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 function patchMethod (methodId, patches) { 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 suffix = (name === 'accessFlags' ? 'U32' : 'Pointer'); Memory['write' + suffix](address, patches[name]); }); }
getArtMethodSpec 实现:
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 function _getArtMethodSpec (vm) { const api = getApi(); let spec; vm.perform(() => { const env = vm.getEnv(); const process = env.findClass('android/os/Process'); const setArgV0 = env.getStaticMethodId(process, 'setArgV0', '(Ljava/lang/String;)V'); const runtimeModule = Process.getModuleByName('libandroid_runtime.so'); const runtimeStart = runtimeModule.base; const runtimeEnd = runtimeStart.add(runtimeModule.size); const apiLevel = getAndroidApiLevel(); const entrypointFieldSize = (apiLevel <= 21) ? 8 : pointerSize; const expectedAccessFlags = kAccPublic | kAccStatic | kAccFinal | kAccNative; let jniCodeOffset = null; let accessFlagsOffset = null; let remaining = 2; for (let offset = 0; offset !== 64 && remaining !== 0; offset += 4) { const field = setArgV0.add(offset); if (jniCodeOffset === null) { const address = Memory.readPointer(field); if (address.compare(runtimeStart) >= 0 && address.compare(runtimeEnd) < 0) { jniCodeOffset = offset; remaining--; } } if (accessFlagsOffset === null) { const flags = Memory.readU32(field); if (flags === expectedAccessFlags) { accessFlagsOffset = offset; remaining--; } } } if (remaining !== 0) { throw new Error('Unable to determine ArtMethod field offsets'); } const quickCodeOffset = jniCodeOffset + entrypointFieldSize; const size = (apiLevel <= 21) ? (quickCodeOffset + 32) : (quickCodeOffset + pointerSize); spec = { size: size, offset: { jniCode: jniCodeOffset, quickCode: quickCodeOffset, accessFlags: accessFlagsOffset } }; if ('artInterpreterToCompiledCodeBridge' in api) { spec.offset.interpreterCode = jniCodeOffset - entrypointFieldSize; } }); return spec; }
参考:
https://bbs.pediy.com/thread-229215.htm
基于 Frida 的全平台逆向分析
Xposed 框架原理深入研究
art_quick_generic_jni_trampoline 分析
[ART Method Execution](https://blog.csdn.net/hl09083253cy/article/details/78418702)
[ART 执行类方法解析流程](https://blog.csdn.net/zhu929033262/article/details/75093012)
https://github.com/TinyNiko/TinyNiko.github.io/blob/master/Frida.pdf
Creating a Java VM from Android Native Code https://calebfenton.github.io/2017/04/05/creating_java_vm_from_android_native_code/
mac 下编译 frida
git clone https://github.com/frida/frida
创建代码签名证书 frida-cert 参考 https://sourceware.org/gdb/wiki/BuildingOnDarwin 中的 2.1.1 . Create a certificate 部分,将 gdb-cert 替换为 frida-cert 即可
make
采坑记录:
ANDROID_NDK_ROOT must be set to the location of your r15c NDK. 解决办法: 设置环境变量 ANDROID_NDK_ROOT 为 ndk_r15c,必须为 r15 版本,我只是在当前 shell 里 export ANDROID_NDK_ROOT=/home/xxx/ndk-path 时无法编译通过,设为系统环境变量时,编译才通过。
Dependency ‘glib-2.0’ not found
1 2 meson.build:123:0: ERROR: Dependency 'glib-2.0' not found, tried Extra Frameworks and Pkg-Config: 'utf-8' codec can't decode byte 0xe5 in position 16: invalid continuation byte
实际运行 pkg-config –modversion glib-2.0 时,发现 glib-2.0 是存在的,出现以上错误是因为路径中包含中文!!!
AttributeError: module ‘enum’ has no attribute ‘IntFlag’ 解决办法: 设置 PYTHONPATH 为 python3.6 的路径, export PYTHONPATH=/usr/bin/python3.6