乐于分享
好东西不私藏

Frida源码分析之Java Hook原理篇

Frida源码分析之Java Hook原理篇

根据官方文档(https://frida.re/docs/bridges/?utm_source=chatgpt.com),Frida 17之后的版本中, GumJs 运行时不再捆绑 bridges(例如 frida-java-bridge、frida-objc-bridge, frida-swift-bridge)。因此这篇Java Hook原理分析参考的项目源码在frida-java-bridge(https://github.com/frida/frida-java-bridge)中。

以如下例子进行原理分析:

varAdapter = 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

implementation: {    enumerable: true,get () {const replacement = this._r;return (replacement !== undefined) ? replacement : null;    },set (fn) {const params = this._p;const holder = params[1]; // classWrapperconst type = params[2]; // 方法类型:实例/静态/构造// 构造方法通过$init进行hookif (type === CONSTRUCTOR_METHOD) {throw new Error('Reimplementing $new is not possible; replace implementation of $init instead');      }// 卸载已存在的hook逻辑const existingReplacement = this._r;if (existingReplacement !== undefined) {// holder.$f._patchedMethods 是 classFactory 维护的“当前已打补丁的方法集合”,用于后续统一清理        holder.$f._patchedMethods.delete(this);const mangler = existingReplacement._m;        mangler.revert(vm);// 删除hook逻辑,恢复成原函数this._r = undefined;      }if (fn !== null) {const [methodName, classWrapper, type, methodId, retType, argTypes] = params;// 将用户定义的JS hook函数封装成native函数const replacement = implement(methodName, classWrapper, type, retType, argTypes, fn, this);const mangler = makeMethodMangler(methodId);        replacement._m = mangler;this._r = replacement;// 对原方法进行hook        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

functionimplement (methodName, classWrapper, type, retType, argTypes, handler, fallback = null) {const pendingCalls = newSet();// 返回一个函数,内部调用了用户的hook代码,并且对入参类型进行了转换(jni to js),对返回值类型进行了转换(js to jni)const f = makeMethodImplementation([methodName, classWrapper, type, retType, argTypes, handler, fallback, pendingCalls]);// 将做了参数类型适配的hook代码封装成native函数const impl = newNativeCallback(f, retType.type, ['pointer', 'pointer'].concat(argTypes.map(t => t.type)));  impl._c = pendingCalls;return impl;}functionmakeMethodImplementation (params) {returnfunction () {returnhandleMethodInvocation(arguments, params);  };}

该函数主要是对用户定义的hook逻辑封装成native函数。其中makeMethodImplementation() 的作用是把一堆上下文参数封进闭包,生成一个符合 NativeCallback 签名的入口函数,其内部调用了handleMethodInvocation()方法。

handleMethodInvocation

functionhandleMethodInvocation (jniArgs, params) {// 将JNIEnv封装成js层的JNIEnvconstenv = newEnv(jniArgs[0], vm);const [methodName, classWrapper, type, retType, argTypes, handler, fallback, pendingCalls] = params;constownedObjects = [];// 实例方法创建类实例对象,静态方法直接使用classWrapper  let self;if (type === INSTANCE_METHOD) {constC = classWrapper.$C;self = newC(jniArgs[1], STRATEGY_VIRTUAL, env, false);  } else {self = classWrapper;  }consttid = getCurrentThreadId();//创建局部帧,管理本地引用	  env.pushLocalFrame(3);  let haveFrame = true;// 当前线程与env进行关联  vm.link(tid, env);try {    pendingCalls.add(tid);//线程id加入到集合中// fallback为null, fn赋值为用户定义的hook代码    let fn;if (fallback === null || !ignoredThreads.has(tid)) {fn = handler;    } else {fn = fallback;    }// 将jni参数转成js参数constargs = [];constnumArgs = jniArgs.length - 2; for (let i = 0; i !== numArgs; i++) {constt = argTypes[i];constvalue = t.fromJni(jniArgs[2 + i], env, false);// 跳过前两个JNI参数(JNIEnv*, jobject/jclass)      args.push(value);// js参数保存到ownedObjects中      ownedObjects.push(value);    }// 用户定义的hook方法执行constretval = fn.apply(self, args);if (!retType.isCompatible(retval)) {thrownewError(`Implementation for ${methodName} expected return value compatible with ${retType.className}`);    }// 返回值从js转成jni    let jniRetval = retType.toJni(retval, env);// 如果返回的是对象引用(pointer),需要把该引用“带出”当前local frame,否则frame弹出后引用失效// 同时保存到ownedObjects中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,该类的初始化函数如下:

classArtMethodMangler {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行处。接下来进行拆解分析。

replace (impl, isInstanceMethod, argTypes, vm, api) {const { kAccCompileDontBother, artNterpEntryPoint } = api;// 获取原函数ArtMethod字段快照this.originalMethod = fetchArtMethod(this.methodId, vm);

这里主要是获取原函数的ArtMethod结构体中部分字段的值,具体为jniCode()、accessFlags()、quickCode()、interpreterCode(方法解释执行入口),对于fetchArtMethod方法的详细分析见fetchArtMethod。

const originalFlags = this.originalMethod.accessFlags;// 判断当前函数是否被xposed hook过了if ((originalFlags & kAccXposedHookedMethod) !== 0 && xposedIsSupported()) {// 该函数已被xposed hook过了// 此时jniCode是xposed的hookInfo指针const hookInfo = this.originalMethod.jniCode;// 获取真正的ArtMethod指针this.hookedMethodId = hookInfo.add(2 * pointerSize).readPointer();// 获取原函数ArtMethod字段快照this.originalMethod = fetchArtMethod(this.hookedMethodId, vm);}

这部分主要检测是否进行了xposed hook,如果进行了,则借助xposed hook的信息获取原方法的ArtMethod指针,并重新调用fetchArtMethod获取原函数的ArtMethod结构体中部分字段的值。

const { hookedMethodId } = this; // hookedMethodId为ArtMethod指针const replacementMethodId = cloneArtMethod(hookedMethodId, vm);// 复制原函数的ArtMethod,用于后续修改this.replacementMethodId = replacementMethodId;// 把克隆出来的 ArtMethod 改造成一个 native 方法patchArtMethod(replacementMethodId, {    jniCode: impl, // 用户定义的hook代码逻辑    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:

    总结一下就是 把方法标记为普通的 native 方法,同时禁用 ART 的特殊快速路径优化。

    • 清掉 kAccCriticalNative,避免走 critical native 调用路径。
    • 清掉 kAccFastNative,避免走 fast native 调用路径。
    • 清掉 kAccNterpEntryPointFastPathFlag,避免 nterp(ART使用的新一代解释器,逐步取代了早期的Mterp)快路径绕过 Frida 预期的调用路径。
    • 加上 kAccNative,告诉 ART 这个method 是 native 方法。
    • 加上 kAccCompileDontBother,告诉 ART 编译器不要尝试编译这个方法,因为 native 方法已经是机器码,不需要 ART JIT/AOT 编译。
  • 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代码。

// Remove kAccFastInterpreterToInterpreterInvoke and kAccSkipAccessChecks to disable use_fast_path// in interpreter_common.h// 修改被 hook 的原始 ArtMethod 的 access_flags,目的不是把原方法直接改成 replacement,而是让 ART 后续调用原方法时不要走某些 fast path,从而确保 Frida 的 replacement 映射逻辑有机会介入。let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag;if ((originalFlags & kAccNative) === 0) {    hookedMethodRemovedFlags |= kAccSkipAccessChecks;}// 修复原方法的ArtMethodpatchArtMethod(hookedMethodId, {    accessFlags: ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0}, vm);

这里对原方法的accessFlags进行了处理,禁用了快速调用路径标志,如果原方法不是 native,还需要移除访问检查跳过标志,最后加上kAccCompileDontBother,告诉 ART 不要尝试 JIT/AOT 编译这个方法。

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 !== null && quickCode.equals(artNterpEntryPoint)) {    patchArtMethod(hookedMethodId, {        quickCode: api.artQuickToInterpreterBridge    }, vm);}

如果原方法是解释执行,且使用了Nterp,那么quickCode就会替换成art_quick_to_interpreter_bridge,强制跳出解释器模式,并改用机器码模式。

if (!isArtQuickEntrypoint(quickCode)) {const interceptor = newArtQuickCodeInterceptor(quickCode);// 开始patch    interceptor.activate(vm);this.interceptor = interceptor;}

如果原方法的quickCode是 ART Quick 编译路径的入口,也就是说原方法已经是机器码模式了,那么就需要通过ArtQuickCodeInterceptor对机器码进行patch,这部分见ArtQuickCodeInterceptor.activate。

最后是收尾工作:

// 使用hash表记录已经替换的方法,方便后续恢复artController.replacedMethods.set(hookedMethodId, replacementMethodId);notifyArtMethodHooked(hookedMethodId, vm);

fetchArtMethod

functionfetchArtMethod (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结构体布局描述,返回的结构是

{size:<ArtMethod结构体大小>,offset: {jniCode:<偏移,对应字段为JNI函数指针(native方法入口)>,quickCode:<偏移,对应字段为指向JIT/AOT编译后的机器码的入口>,accessFlags:<偏移,对应字段为方法的修饰信息>,interpreterCode:<偏移,对应字段为方法解释执行的入口>  }}

由于Android版本的差异,有些字段就不存在(例如interpreterCode),于是借助reduce过滤出存在的字段并获取对应字段的值。

patchArtMethod

functionpatchArtMethod (methodId, patches, vm) {const artMethodSpec = getArtMethodSpec(vm);// 获取ArtMethod中字段偏移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

activate (vm) {const constraints = this._allocateTrampoline();const { trampoline, quickCode, redirectSize } = this;// Arm64下对应writeArtQuickCodeReplacementTrampolineArm64函数const writeTrampoline = artQuickCodeReplacementTrampolineWriters[Process.arch];const prologueLength = writeTrampoline(trampoline, quickCode, redirectSize, constraints, vm);this.overwrittenPrologueLength = prologueLength;this.overwrittenPrologue = Memory.dup(this.quickCodeAddress, prologueLength);// Arm64下对应writeArtQuickCodePrologueArm64函数const writePrologue = artQuickCodePrologueWriters[Process.arch];    writePrologue(quickCode, trampoline, redirectSize);}

该函数首先调用_allocateTrampoline()方法分片trampoline的内存空间、确定用于重定向的字节大小,以及获取空闲寄存器。以Arm64为例,接下来调用writeArtQuickCodeReplacementTrampolineArm64()函数编写trampoline,返回用于重定向的字节大小,然后保存原函数quickCode入口处相应字节大小的指令,用于后续恢复。最后调用writeArtQuickCodePrologueArm64()函数修改quickCode入口使其跳转到编写好的trampoline处。

writeArtQuickCodeReplacementTrampolineArm64

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);// 保存CPU上下文// Save FPRs.    writer.putPushRegReg('d0', 'd1');    writer.putPushRegReg('d2', 'd3');    writer.putPushRegReg('d4', 'd5');    writer.putPushRegReg('d6', 'd7');// Save core args, callee-saves & LR.    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');// Save ArtMethod* + alignment padding.    writer.putSubRegRegImm('sp', 'sp', 16); // sub sp, sp, 16    writer.putStrRegRegOffset('x0', 'sp', 0);// str x0, [sp, #0]// 插入调用findReplacementFromQuickCode的指令,查找是否存在replacement method,如果没有返回NULL    writer.putCallAddressWithArguments(artController.replacedMethods.findReplacementFromQuickCode, ['x0', 'x19']);    writer.putCmpRegReg('x0', 'xzr');// cmp x0, xzr    writer.putBCondLabel('eq', 'restore_registers');// b.eq restore_registers// Set value of x0 in the current frame.    writer.putStrRegRegOffset('x0', 'sp', 0);// str x0, [sp, #0]// 恢复CPU上下文    writer.putLabel('restore_registers');// Restore ArtMethod*    writer.putLdrRegRegOffset('x0', 'sp', 0);    writer.putAddRegRegImm('sp', 'sp', 16);// Restore core args, callee-saves & LR.    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');// Restore FPRs.    writer.putPopRegReg('d6', 'd7');    writer.putPopRegReg('d4', 'd5');    writer.putPopRegReg('d2', 'd3');    writer.putPopRegReg('d0', 'd1');    writer.putBCondLabel('ne', 'invoke_replacement');//b.ne invoke_replacement// 没有replacementdo {      offset = relocator.readOne();    } while (offset < redirectSize && !relocator.eoi);// 将原函数开头前redirectSize字节写入trampoline    relocator.writeAll();// 原函数字节数 > redirectSize, 跳转到剩余部分继续执行if (!relocator.eoi) {const scratchReg = Array.from(availableScratchRegs)[0];      writer.putLdrRegAddress(scratchReg, target.add(offset));// ldr x16/x17, qucikcode + redirectSize      writer.putBrReg(scratchReg);// br x16/x7    }// 有replacement    writer.putLabel('invoke_replacement');    writer.putLdrRegRegOffset('x16', 'x0', artMethodOffsets.quickCode);// ldr x16, [x0, #quickCode]    writer.putBrReg('x16');// br 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行处。

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)returnNULL;/*   * Stack check.   *   * Return NULL to indicate that the original method should be invoked, otherwise   * return a pointer to the replacement ArtMethod.   *   * If the caller is our own JNI replacement stub, then a stack transition must   * have been pushed onto the current thread's linked list.   *   * Therefore, we invoke the original method if the following conditions are met:   *   1- The current managed stack is empty.   *   2- The ArtMethod * inside the linked managed stack's top quick frame is the   *      same as our replacement.   */  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;returnNULL;}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

functionwriteArtQuickCodePrologueArm64 (target, trampoline, redirectSize) {  Memory.patchCode(target, 16, code => {const writer = newArm64Writer(code, { pc: target });if (redirectSize === 16) {      writer.putLdrRegAddress('x16', trampoline);// ldr x16, trampoline    } else {      writer.putAdrpRegAddress('x16', trampoline);// adrp x16, trampoline    }    writer.putBrReg('x16');// br 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结构与涉及技术介绍

https://bbs.kanxue.com/thread-248898-1.htm

Frida Internal - Part 3: Java Bridge 与 ART hook - 有价值炮灰

https://evilpan.com/2022/04/17/frida-java/

看雪ID:gal2xy

https://bbs.kanxue.com/user-home-985561.htm

*本文为看雪论坛优秀文章,由 gal2xy原创,转载请注明来自看雪社区
第十届安全开发者峰会【议题征集】-欢迎投稿

# 往期推荐

我们绕过了 GarudaDefender 整套 Frida 检测,但这已经不是重点了

一次 Flutter App 实战:还原 encData 参数解密流程

单机DMA劫持HyperV!调试+取证两种思路解决2026腾讯游戏安全技术竞赛决赛

Android风险环境检测——签名校验

和爱豆更近一步——爱豆聊天App反调试绕过

球分享

球点赞

球在看

点击阅读原文查看更多

相关学习资料