根据官方文档 ,Frida 17之后的版本中, GumJs 运行时不再捆绑 bridges(例如 frida-java-bridge、frida-objc-bridge, frida-swift-bridge)。因此这篇Java Hook原理分析参考的项目源码在frida-java-bridge 中。
以如下例子进行原理分析
1 2 3 4 5 6 7 var Adapter = Java .use (targetClass);Adapter ["doAdapter" ].implementation = function (i ) { console .log (">>> doAdapter is called: i=" + i); var result = this .doAdapter (i); console .log ("<<< doAdapter result=" + result); return result; }
Adapter[“doAdapter”]获取到的是methodPrototype对象,然后将自定义的hook函数赋值给implementation字段,使用到的是implementation的set()方法来安装Hook(对应android.js 1697行处)。
implementation.set 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 implementation : { enumerable : true , get () { const replacement = this ._r ; return (replacement !== undefined ) ? replacement : null ; }, set (fn) { const params = this ._p ; const holder = params[1 ]; const type = params[2 ]; if (type === CONSTRUCTOR_METHOD ) { throw new Error ('Reimplementing $new is not possible; replace implementation of $init instead' ); } const existingReplacement = this ._r ; if (existingReplacement !== undefined ) { holder.$f ._patchedMethods .delete (this ); const mangler = existingReplacement._m ; mangler.revert (vm); this ._r = undefined ; } if (fn !== null ) { const [methodName, classWrapper, type, methodId, retType, argTypes] = params; const replacement = implement (methodName, classWrapper, type, retType, argTypes, fn, this ); const mangler = makeMethodMangler (methodId); replacement._m = mangler; this ._r = replacement; mangler.replace (replacement, type === INSTANCE_METHOD , argTypes, vm, api); holder.$f ._patchedMethods .add (this ); } } }
set() 首先校验目标方法是否是构造方法,此方法不通过当前路径实现。然后检查目标方法是否已经被hook,如果被hook过了,则撤销之前的hook逻辑。最后,如果存在新的hook逻辑,则调用implement()方法将用户定义的js hook逻辑封装成native函数,并调用mangler.replace()方法对目标方法进行hook。
implement 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function implement (methodName, classWrapper, type, retType, argTypes, handler, fallback = null ) { const pendingCalls = new Set (); const f = makeMethodImplementation ([methodName, classWrapper, type, retType, argTypes, handler, fallback, pendingCalls]); const impl = new NativeCallback (f, retType.type , ['pointer' , 'pointer' ].concat (argTypes.map (t => t.type ))); impl._c = pendingCalls; return impl; }function makeMethodImplementation (params) { return function ( ) { return handleMethodInvocation (arguments , params); }; }
该函数主要是对用户定义的hook逻辑封装成native函数。其中makeMethodImplementation() 的作用是把一堆上下文参数封进闭包,生成一个符合 NativeCallback 签名的入口函数,其内部调用了handleMethodInvocation()方法。
handleMethodInvocation 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 function handleMethodInvocation (jniArgs, params) { const env = new Env (jniArgs[0 ], vm); const [methodName, classWrapper, type, retType, argTypes, handler, fallback, pendingCalls] = params; const ownedObjects = []; let self; if (type === INSTANCE_METHOD ) { const C = classWrapper.$C ; self = new C (jniArgs[1 ], STRATEGY_VIRTUAL , env, false ); } else { self = classWrapper; } const tid = getCurrentThreadId (); env.pushLocalFrame (3 ); let haveFrame = true ; vm.link (tid, env); try { pendingCalls.add (tid); let fn; if (fallback === null || !ignoredThreads.has (tid)) { fn = handler; } else { fn = fallback; } const args = []; const numArgs = jniArgs.length - 2 ; for (let i = 0 ; i !== numArgs; i++) { const t = argTypes[i]; const value = t.fromJni (jniArgs[2 + i], env, false ); args.push (value); ownedObjects.push (value); } const retval = fn.apply (self, args); if (!retType.isCompatible (retval)) { throw new Error (`Implementation for ${methodName} expected return value compatible with ${retType.className} ` ); } let jniRetval = retType.toJni (retval, env); if (retType.type === 'pointer' ) { jniRetval = env.popLocalFrame (jniRetval); haveFrame = false ; ownedObjects.push (retval); } return jniRetval; } ... }
当 Java 调用某个被 Hook 的方法时,负责将 JNI 的数据结构转换成 js 对象,执行用户定义的 js hook 代码,然后再将结果转换成回 JNI 的变量类型。
makeMethodMangler 这里分析andriod平台下的java hook,因此我们分析lib\android.js下的ArtMethodMangler,该类的初始化函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 class ArtMethodMangler { constructor (opaqueMethodId) { const methodId = unwrapMethodId (opaqueMethodId); this .methodId = methodId; this .originalMethod = null ; this .hookedMethodId = methodId; this .replacementMethodId = null ; this .interceptor = null ; } ... }
ArtMethodMangler.replace 对应ArtMethodMangler.replace()方法,位置在lib\android.js3707行处。接下来进行拆解分析。
1 2 3 4 replace (impl, isInstanceMethod, argTypes, vm, api) { const { kAccCompileDontBother, artNterpEntryPoint } = api; this .originalMethod = fetchArtMethod (this .methodId , vm);
这里主要是获取原函数的ArtMethod结构体中部分字段的值,具体为jniCode()、accessFlags()、quickCode()、interpreterCode(方法解释执行入口),对于fetchArtMethod方法的详细分析见fetchArtMethod 。
1 2 3 4 5 6 7 8 9 10 11 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); }
这部分主要检测是否进行了xposed hook,如果进行了,则借助xposed hook的信息获取原方法的ArtMethod指针,并重新调用fetchArtMethod获取原函数的ArtMethod结构体中部分字段的值。
1 2 3 4 5 6 7 8 9 10 11 12 const { hookedMethodId } = this ; const replacementMethodId = cloneArtMethod (hookedMethodId, vm);this .replacementMethodId = replacementMethodId;patchArtMethod (replacementMethodId, { jniCode : impl, accessFlags : ((originalFlags & ~(kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag)) | kAccNative | kAccCompileDontBother) >>> 0 , quickCode : api.artClassLinker .quickGenericJniTrampoline , interpreterCode : api.artInterpreterToCompiledCodeBridge }, vm);
调用cloneArtMethod()复制原函数的ArtMethod,后续的修改都在这个副本上进行的。之后调用patchArtMethod()方法对复制出来的ArtMethod中的jniCode、accessFlags、quickCode、interpreterCode进行修复,具体见patchArtMethod 。这里分析一下入参:
accessFlags:
清掉 kAccCriticalNative,避免走 critical native 调用路径。
清掉 kAccFastNative,避免走 fast native 调用路径。
清掉 kAccNterpEntryPointFastPathFlag,避免 nterp(ART使用的新一代解释器,逐步取代了早期的Mterp)快路径绕过 Frida 预期的调用路径。
加上 kAccNative,告诉 ART 这个method 是 native 方法。
加上 kAccCompileDontBother,告诉 ART 编译器不要尝试编译这个方法,因为 native 方法已经是机器码,不需要 ART JIT/AOT 编译。
总结一下就是 把方法标记为普通的 native 方法,同时禁用 ART 的特殊快速路径优化。
quickCode
原本是quick模式入口,现修改成api.artClassLinker.quickGenericJniTrampoline,代表的是ClassLinker的quick_generic_jni_trampoline_字段。也就是说,从 quick 路径进入这个方法时,不直接执行原来的 quick compiled code,而是进入 ART 的通用 JNI 调用桥,再由 JNI 桥读取 jniCode 并调用native化的hook代码。
interpreterCode
原本是解释器模式入口,现修改成api.artInterpreterToCompiledCodeBridge,是从解释器模式转成机器码模式的入口。这样解释器执行该方法时,会桥接到 compiled/JNI 路径,最终进入 generic JNI trampoline 和 native化的hook代码。
1 2 3 4 5 6 7 8 9 10 11 let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag;if ((originalFlags & kAccNative) === 0 ) { hookedMethodRemovedFlags |= kAccSkipAccessChecks; }patchArtMethod (hookedMethodId, { accessFlags : ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0 }, vm);
这里对原方法的accessFlags进行了处理,禁用了快速调用路径标志,如果原方法不是 native,还需要移除访问检查跳过标志,最后加上kAccCompileDontBother,告诉 ART 不要尝试 JIT/AOT 编译这个方法。
1 2 3 4 5 6 7 8 9 const quickCode = this .originalMethod .quickCode ;if (artNterpEntryPoint !== null && quickCode.equals (artNterpEntryPoint)) { patchArtMethod (hookedMethodId, { quickCode : api.artQuickToInterpreterBridge }, vm); }
如果原方法是解释执行,且使用了Nterp,那么quickCode就会替换成art_quick_to_interpreter_bridge,强制跳出解释器模式,并改用机器码模式。
1 2 3 4 5 6 7 if (!isArtQuickEntrypoint (quickCode)) { const interceptor = new ArtQuickCodeInterceptor (quickCode); interceptor.activate (vm); this .interceptor = interceptor; }
如果原方法的quickCode是 ART Quick 编译路径的入口,也就是说原方法已经是机器码模式了,那么就需要通过ArtQuickCodeInterceptor对机器码进行patch,这部分见ArtQuickCodeInterceptor.activate 。
最后是收尾工作
1 2 3 artController.replacedMethods .set (hookedMethodId, replacementMethodId);notifyArtMethodHooked (hookedMethodId, vm);
fetchArtMethod 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function fetchArtMethod (methodId, vm) { const artMethodSpec = getArtMethodSpec (vm); const artMethodOffset = artMethodSpec.offset ; return (['jniCode' , 'accessFlags' , 'quickCode' , '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; }, {})); }
getArtMethodSpec返回的是ArtMethod结构体布局描述,返回的结构是
1 2 3 4 5 6 7 8 9 { size: <ArtMethod结构体大小>, offset: { jniCode: <偏移,对应字段为JNI函数指针(native方法入口)>, quickCode: <偏移,对应字段为指向JIT/AOT编译后的机器码的入口>, accessFlags: <偏移,对应字段为方法的修饰信息>, interpreterCode: <偏移,对应字段为方法解释执行的入口> } }
由于Android版本的差异,有些字段就不存在(例如interpreterCode),于是借助reduce过滤出存在的字段并获取对应字段的值。
patchArtMethod 1 2 3 4 5 6 7 8 9 10 11 12 13 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]); }); }
获取jniCode、accessFlags、quickCode、interpreterCode字段,并进行修正,设置为相应的入参值。
ArtQuickCodeInterceptor.activate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 activate (vm) { const constraints = this ._allocateTrampoline (); const { trampoline, quickCode, redirectSize } = this ; const writeTrampoline = artQuickCodeReplacementTrampolineWriters[Process .arch ]; const prologueLength = writeTrampoline (trampoline, quickCode, redirectSize, constraints, vm); this .overwrittenPrologueLength = prologueLength; this .overwrittenPrologue = Memory .dup (this .quickCodeAddress , prologueLength); const writePrologue = artQuickCodePrologueWriters[Process .arch ]; writePrologue (quickCode, trampoline, redirectSize); }
该函数首先调用_allocateTrampoline()方法分片trampoline的内存空间、确定用于重定向的字节大小,以及获取空闲寄存器。以Arm64为例,接下来调用writeArtQuickCodeReplacementTrampolineArm64()函数编写trampoline,返回用于重定向的字节大小,然后保存原函数quickCode入口处相应字节大小的指令,用于后续恢复。最后调用writeArtQuickCodePrologueArm64()函数修改quickCode入口使其跳转到编写好的trampoline处。
writeArtQuickCodeReplacementTrampolineArm64 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 function writeArtQuickCodeReplacementTrampolineArm64 (trampoline, target, redirectSize, { availableScratchRegs }, vm) { const artMethodOffsets = getArtMethodSpec (vm).offset ; let offset; Memory .patchCode (trampoline, 256 , code => { const writer = new Arm64Writer (code, { pc : trampoline }); const relocator = new Arm64Relocator (target, writer); writer.putPushRegReg ('d0' , 'd1' ); writer.putPushRegReg ('d2' , 'd3' ); writer.putPushRegReg ('d4' , 'd5' ); writer.putPushRegReg ('d6' , 'd7' ); writer.putPushRegReg ('x1' , 'x2' ); writer.putPushRegReg ('x3' , 'x4' ); writer.putPushRegReg ('x5' , 'x6' ); writer.putPushRegReg ('x7' , 'x20' ); writer.putPushRegReg ('x21' , 'x22' ); writer.putPushRegReg ('x23' , 'x24' ); writer.putPushRegReg ('x25' , 'x26' ); writer.putPushRegReg ('x27' , 'x28' ); writer.putPushRegReg ('x29' , 'lr' ); writer.putSubRegRegImm ('sp' , 'sp' , 16 ); writer.putStrRegRegOffset ('x0' , 'sp' , 0 ); writer.putCallAddressWithArguments (artController.replacedMethods .findReplacementFromQuickCode , ['x0' , 'x19' ]); writer.putCmpRegReg ('x0' , 'xzr' ); writer.putBCondLabel ('eq' , 'restore_registers' ); writer.putStrRegRegOffset ('x0' , 'sp' , 0 ); writer.putLabel ('restore_registers' ); writer.putLdrRegRegOffset ('x0' , 'sp' , 0 ); writer.putAddRegRegImm ('sp' , 'sp' , 16 ); writer.putPopRegReg ('x29' , 'lr' ); writer.putPopRegReg ('x27' , 'x28' ); writer.putPopRegReg ('x25' , 'x26' ); writer.putPopRegReg ('x23' , 'x24' ); writer.putPopRegReg ('x21' , 'x22' ); writer.putPopRegReg ('x7' , 'x20' ); writer.putPopRegReg ('x5' , 'x6' ); writer.putPopRegReg ('x3' , 'x4' ); writer.putPopRegReg ('x1' , 'x2' ); writer.putPopRegReg ('d6' , 'd7' ); writer.putPopRegReg ('d4' , 'd5' ); writer.putPopRegReg ('d2' , 'd3' ); writer.putPopRegReg ('d0' , 'd1' ); writer.putBCondLabel ('ne' , 'invoke_replacement' ); do { offset = relocator.readOne (); } while (offset < redirectSize && !relocator.eoi ); relocator.writeAll (); if (!relocator.eoi ) { const scratchReg = Array .from (availableScratchRegs)[0 ]; writer.putLdrRegAddress (scratchReg, target.add (offset)); writer.putBrReg (scratchReg); } writer.putLabel ('invoke_replacement' ); writer.putLdrRegRegOffset ('x16' , 'x0' , artMethodOffsets.quickCode ); writer.putBrReg ('x16' ); writer.flush (); }); return offset; }
首先保存部分寄存器,然后调用findReplacementFromQuickCode方法查找是否存在replacement实现,如果有,则返回相应地址,否则为NULL(0),这里分情况进行讨论分析:
没有replacement实现,即 x0 == 0,跳到 restore_registers标签,从栈中恢复部分寄存器,此时x0还是原函数的methodId,接着将原始指令重写到 trampoline 中,执行完这些指令后,最后跳转到原 quick code 剩下的部分继续执行。
有replacement实现,即 x0 != 0,把返回的 replacement methodId 写回栈顶,然后恢复部分寄存器,此时x0就是 replacement methodId。接着跳转到invoke_replacement标签处,跳转到replacement method的quickCode入口进行执行。
findReplacementFromQuickCode 位置在lib\android.js 1925行处。
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 gpointerfind_replacement_method_from_quick_code (gpointer method, gpointer thread) { gpointer replacement_method; gpointer managed_stack; gpointer top_quick_frame; gpointer link_managed_stack; gpointer * link_top_quick_frame; replacement_method = get_replacement_method (method); if (replacement_method == NULL ) return NULL ; managed_stack = thread + ${threadOffsets.managedStack}; top_quick_frame = *((gpointer *) (managed_stack + ${managedStackOffsets.topQuickFrame})); if (top_quick_frame != NULL ) return replacement_method; link_managed_stack = *((gpointer *) (managed_stack + ${managedStackOffsets.link})); if (link_managed_stack == NULL ) return replacement_method; link_top_quick_frame = GSIZE_TO_POINTER (*((gsize *) (link_managed_stack + ${managedStackOffsets.topQuickFrame})) & ~((gsize) 1 )); if (link_top_quick_frame == NULL || *link_top_quick_frame != replacement_method) return replacement_method; return NULL ; } gpointerget_replacement_method (gpointer original_method) { gpointer replacement_method; g_mutex_lock (&lock); replacement_method = g_hash_table_lookup (methods, original_method); g_mutex_unlock (&lock); return replacement_method; }
主要是根据传入的原函数methodId 查找对应的replacement methodId,返回 NULL 表示应调用原始方法,否则返回指向replacement ArtMethod 的指针。
这里做了栈检查,这样来自hook逻辑中的原函数调用就不会是无限递归,而是返回NULL,执行原函数逻辑。这就是我们在hook A函数,并能够在hook逻辑内部调用原始A函数的原因。
writeArtQuickCodePrologueArm64 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function writeArtQuickCodePrologueArm64 (target, trampoline, redirectSize) { Memory .patchCode (target, 16 , code => { const writer = new Arm64Writer (code, { pc : target }); if (redirectSize === 16 ) { writer.putLdrRegAddress ('x16' , trampoline); } else { writer.putAdrpRegAddress ('x16' , trampoline); } writer.putBrReg ('x16' ); writer.flush (); }); }
这里则是根据可用重定位空间为8字节或16字节,设置不同的跳转指令。
总结 Frida 在进行 Java Hook时,首先会将用户编写的 JS 函数包装成 NativeCallback,使其具备 JNI native 函数的调用形式。之后的hook工作都围绕 ArtMethod 做修改。它会读取目标方法的 jniCode、accessFlags、quickCode、interpreterCode 字段,复制出一个 ArtMethod副本,用于replacement method,并把这个副本改造成 native 方法 :jniCode 指向前面生成的 NativeCallback,quickCode 指向 ART 的通用 JNI trampoline,interpreterCode 指向解释器到编译代码的桥接入口。这样无论方法从解释器路径还是 quick compiled code 路径进入,最终都能转到 Frida 的 replacement 实现。需要额外注意的是,对于已经编译成机器码的方法,Frida 还会 patch 原始机器码入口 ,即修改原始机器码前 8/16 字节为跳转到 trampoline 的指令,trampoline 中再通过 findReplacementFromQuickCode() 查询当前方法是否存在 replacement。如果存在,就跳转到 replacement method 入口;如果不存在,或者当前调用来自 Hook 逻辑内部对原函数的调用,则恢复执行原方法。
参考:
https://deepwiki.com/frida/frida-java-bridge/4.1-method-hooking-basics
[原创]源码简析之ArtMethod结构与涉及技术介绍-Android安全-看雪安全社区|专业技术交流与安全研究论坛
Frida Internal - Part 3: Java Bridge 与 ART hook - 有价值炮灰