首页
社区
课程
招聘
[原创]从0到1构建一个Hook工具之Frida-like风格的Hook
发表于: 4小时前 114

[原创]从0到1构建一个Hook工具之Frida-like风格的Hook

4小时前
114

一开始的 Nook,本质上是一个 Android Hook 框架。它已经具备了几类底层能力:注入、Java Hook、Native Hook。但这距离Frida-like的Hook风格还有很大的差距,一个 Hook 框架,不等于一个可用的动态分析工具。框架解决的是“你注入进去之后能做什么”,工具解决的是“你如何把能力稳定、可重复、低摩擦地用起来”。

项目仓库在:

Nook仓库地址

大佬们可以尝试着用一下看,release里下一个server,然后

脚本语法基本和Frida一致,希望大家点点star哈哈,有问题可以提提issue或者直接在评论里提,后面有时间会持续维护和更新。

接下来要做的是把它推进成一个更接近 Frida 使用体验的东西:

因为这里的代码实现比较冗长,所以这篇文章和前面几篇的风格会有所不同:通过案例来介绍Nook以及简单介绍Frida背后是怎么做的。

这里案例使用的是19eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6p5c8g2u0q4i4K6u0V1j5h3b7J5x3o6l9I4i4K6u0r3c8Y4u0A6k6r3q4Q4x3X3c8x3j5h3u0K6

在此之前,作为一个Hook框架,Nook实现一次hook的基本语义是这样的:

并且我们需要手动的编译成so,通过Ninjector这样的注入工具注入到目标app中。

而我们的目标是像Frida那样,在设备上启动一个server,我们在host端只需要写好js脚本,通过一行命令就能完成hook,在已经有一个Hook框架的基础上,我们还需要什么呢?

首先我们把Nook定为三层结构:

另外我们还需要通信和协议层来进行远程的操控:

最后还需要一个脚本运行时,将工作模型从写c+编译+注入变为写js+动态加载,通过一个JS Bridge,把底层的Hook能力变成我们熟悉的脚本API

先从一个脚本开始:

一次Hook的本质是:

用户做的事情通常是:

这里 Host 侧承担的职责是:

nook-server 接到 Host 请求后,不是自己去执行 Hook。它真正负责的是:

agent 进入目标进程后,会初始化自己的运行时环境:

接着 server 把 SCRIPT_LOAD 这类请求转发给 agent,agent 再把脚本内容交给 JsRuntime::Evaluate(...) 一类入口去执行。

脚本一加载,首先跑到的是:

这一步表面语义很简单:在 Java 可用的时候执行回调”,对 Nook 来说,它实际做了两件事:

接下来脚本会执行:

这个 wrapper 里会延迟解析:

此时把脚本层意图翻译成底层 Hook 引擎能够理解的安装请求。当 implementation = fn 落到 native bridge 后, Nook 会继续把请求传给 Java hook 子系统。安装成功后,当 app 运行到MainActivity.get_random()时,底层 Java hook 会先截获这次调用,然后再把控制流送回 Nook runtime 持有的 JS callback,也就是:

于是完整链路变成:

第一个例子是最典型的 Frida Java Hook 入门题。

目标app关键方法如下:

test1

frida-0x1 里,目标是 Hook MainActivity.get_random(),把返回值强制改成 5,然后再观察后续 check(int, int) 的参数。

这个例子很适合作为起点,因为它对应的是 Frida 最基础、也最高频的能力:通过脚本替换 Java 方法实现。我们很容易就可以写出对应的hook代码:

Hook效果如下,输入14后就可以在app页面看到flag:“FRIDA{BABY_HOOK_0x1}”:

我们第一步就从这个脚本出发:

首先是第一句代码Java.perform(function()),这句代码意味着什么?

可以简单将其理解为:“等 Java 运行环境准备好之后,再执行这段回调。”它的目的不是单纯“执行一个函数”,而是保证下面这些 Java 相关操作发生在一个安全时机:已经拿到JNIEnv*,Java VM已经可用,目标app的ClassLoader/生命周期已经可以做到Java.use(...) 的阶段。不然你太早去 Java.use("com.ad2001.frida0x1.MainActivity"),很容易因为类还没准备好、ClassLoader 还没就绪而失败。

在Nook中,我是这么去处理的:

意思很容易理解:

在agent_rumtime里面,Nook 维护了一个readyCallbacks 队列。它会检查两类条件:Java._isClassLoaderReady()和Java._isLifecycleReady()。

如果还没 ready,就把当前脚本的回调缓存起来;等到 Java.__nookDispatchReady() 被触发时,再把这些回调取出来执行。

简单总结Java.perform(fn) 本质上做的是:

真正执行回调的是 Java.vm.perform(...):

也就是:带着一个有效的 Java 环境去执行你的回调。

Frida是怎么做的呢?其实做的事情是类似的,判断当前是不是app process,classFactory.loader是否已经准备好,ready了就this.vm.perform(fn),还没ready就把回调塞进 _pendingVmOps,然后启动 _performPendingVmOpsWhenReady()。

Frida 的 Java.perform 负责等待 Java / loader 可用,并把回调排队到 VM-ready 路径,也就是说,Frida 的 Java.perform 自己就带了一套 “等 app class loader ready 并把它接起来” 的逻辑。

Nook 这边则是

以及Frida 的vm.perform() 自己 attach/detach 线程,而Nook的Java.vm.perform() 先解析/确保可用 env,再在当前 JS 执行上下文里带着它跑。

简单总结就是Frida的Java.perform自己处理 pending + bootstrap,Nook 的 Java.perform的处理更简单,一个“基于 ready 条件和脚本桥接机制的等待执行器”,更复杂的时机控制被放到了后面的 spawn gate /script runtime bridge 里。

然后是

Java.use(...) 在 Nook 里做了什么?其实只是:

Java.use("com.ad2001.frida0x1.MainActivity") 本质不是“立刻找类并返回 JNI 对象”,也不会“立刻把这个类执行起来”,而是“构造一个能代表这个 Java 类的 JS wrapper”。

后面的比如

都是在这个wrapper之上继续展开的。

这个 wrapper 的生成逻辑在 CreateJavaUseWrapper()CreateJavaUseWrapper() 里面内嵌了一大段 JS factory 代码,用来构造一个 Frida 风格的 Java 类包装对象。这个 wrapper 至少包含几层东西:

另外,它会动态构造出一个 makeMethod(...),这个 makeMethod(...) 生成的方法对象会带这些元数据:

所以再下一句Hook代码中的 var getRandom = MainActivity.get_random.overload()不是普通 JS function,它其实是一个带 Java 方法元数据的 JS 方法 wrapper。

并且Java.use(...) 不一定立刻 resolve 全部方法, 当执行Java.use的时候Nook 并不会在这一刻就把 MainActivity 的所有成员都反射出来只是先返回一个代理对象,真正去处理成员访问,是在后面你写,比如MainActivity.get_random,即

简单总结就是Java.use(...) 的核心产物不是 JNI class handle,而是“后续可继续演化的 JS wrapper”。

Nook在这点的处理和Frida是类似的,都不是直接返回裸jclass,而是一个面向脚本层的类代理对象,不过Frida的ClassFactory比Nook成熟的多,Java.use(...) 只是默认 class factory 的入口,真正负责类包装、loader 关联、wrapper 缓存、对象 cast/use 的,是 ClassFactory。而且 Frida 的 runtime 里,classFactory.loader 是一个很关键的状态。

在Nook中,overload(...) 可以理解成:Java.use 拿到的是“方法组”,overload(...) 才是把这个方法组收窄成“某一个确定签名的方法”。

比如var getRandom = MainActivity.get_random.overload(),这里不是在调用 get_random,而是在做“选重载”。后面你给 getRandom.implementation、check.implementation 赋值时,挂钩目标就已经不是一个模糊的方法名了,而是一个唯一的方法签名。

Nook 在js_runtime里调用makeMethod(...) 给每个方法包装器都挂了一个 method.overload = function () { ... },它会:

所以 overload(...) 返回的其实是一个新的、更具体的方法包装器。 原生侧入口是 JsJavaResolveOverloadSignature,它再去调 ResolveJavaMethodSignature(...)。这层不是简单字符串匹配,而是会:

Frida的语义和Nook基本是一样的,Java.use(...).method 先是一个 dispatcher,.overload(...) 之后才变成具体 overload wrapper。

implementation翻译成中文的意思就是:将计划、决策或系统付诸实施的过程。当我们在脚本写下:

不是一个单纯的赋值过程,而是就在实施Hook:

在之前提到的 method wrapper 里,makeMethod(...) 给每个方法包装器都定义了:

所以,执行这行代码的时候发生的是:

这意味着 method wrapper 从这一刻开始,不只是“描述某个 Java 方法”,而是已经和一个真实安装好的 Hook 绑定起来了。

__nookJavaInstallImplementation(...) 背后做了什么呢?它会先从 method wrapper 上把安装 Hook 所需的元数据取出来,包括:

然后拼出一个JavaJsHookRequest

其中deferred表示这次安装走的是 deferred hook 流程,也就是允许目标方法还没完全 ready 时先注册,后面再由底层时机成熟后完成接管。

随后 JsJavaInstallImplementation 会调用

如果安装成功,它还会把这个 JS 回调函数保存到当前脚本的运行时回调表里:

所以这一层做了两件事:

那Nook 底层 Java Hook 是怎么真正装上的呢?它的大体流程是:

也就是说,implementation = fn 赋值最终会走到 Nook 底层已有的 Java Hook 能力,把某个类、某个方法、某个签名真正挂上。安装成功后,Nook 会得到:

这些信息会一起保存在 JavaJsHookRecord 里,后面调用原方法、卸载 Hook、转发回调时都要用到。

那么当目标方法命中后怎么回到这段 JS呢?目标 Java 方法被调用时,底层 Java Hook 不会直接执行你写的 JS 函数。中间还会经过一层“hook id -> script callback”的分发。大体链路是:

所以 implementation = fn 的本质,不只是“替换方法逻辑”,而是把:

这三者给串成了一条完整的调用链。

Frida这边的语义也是类似的,给某个具体overload wrapper赋implementation,本质就是在告诉frida-java-bridge

当我们在脚本里写:

这句的含义不是“再通过普通 Java 调用走一遍 check”,而是:

所以 callOriginal(...) 的本质不是普通方法调用,而是“从当前 Hook 上下文回到原始实现”的专用入口。Nook 是在每次 Java Hook 命中、准备进入 JS 回调时,动态构造一个当前回调专用的 receiver:

callOriginal(...) 对应的原生入口是JsJavaCallOriginal,它会先从 func_data 里取出当前绑定的 hook_id,然后把 JS 参数逐个转成 JavaJsValue,最后调用CallOriginalJavaJsHook(hook_id, args, arg_count, ...)

并且callOriginal(...) 只能在当前Hook回调里使用,因为Nook 默认实现里,callOriginal 依赖的是“当前线程正在处理哪一次 Java Hook 调用”这个活动上下文。

CallOriginalJavaJsHook(...) 会先通过 hook_id 找到对应的 JavaJsHookRecord,然后走默认实现DefaultCallOriginalJavaJsHook(...),这层会做几件关键的事:

也就是说,真正执行原方法的是CallOriginalNow(...),而不是再去走一次普通 Java.use(...).method(...) 调用流程,这是为了避免递归重入。

第二个例子是 frida-0x2,他的关键点是调用一个静态 Java 方法。

test2

Hook效果:

首先我们简单介绍一下静态方法,以及与之相对的另一个概念实例方法在Hook中有什么不同。对 Hook 框架来说,静态方法和普通实例方法必须额外区分,根本原因在于

这会直接影响三件事:

Nook做了两种判断的方案:一种情况是显式解析时,比如 .overload(...) 或列举 declared methods,这时候它会直接走 Java 反射,取 Method 的modifiers,再用 Modifier.isStatic(...) 判断,这个是最直接的静态判定。

另一种情况是像 MainActivity.get_flag(4919) 这种直接调用,没有先显式 .overload(...)。这时候 Nook 会在调用阶段综合判断:

它的策略基本是:

这个题表面上不复杂,核心是找到目标类,然后直接调用静态方法 MainActivity.get_flag(4919)

Hook代码也很短:

但它验证的是另一个很关键的能力:脚本运行时是不是一个可操作的 Java 对象环境

如果 Nook 只是 Hook 框架,那它擅长的是“拦截某个现有执行路径”;但 Frida-like 工具还得支持另一种分析方式:不等程序自己走到那里,而是脚本主动去调用。

所以这个例子验证的重点,不只是“Java.use 能不能找到类”,而是:

在上一个例子中我们已经知道了Java.use会返回一个Js Wrapper,而MainActivity.get_flag(4919)实际调的是 method wrapper,这个 wrapper 是makeMethod(...) 生成的,函数体最终会走到: __nookJavaInvoke(...)

JsJavaInvoke 主要做这几件事:

而类包装器wrapper上的方法其实并不是一开始就是静态的,Nook是在真正调用的那一刻才动态的把它收敛为静态调用,最后JNI层再明确分到 CallStatic*MethodA 这条路径。

在这个例子中,Java.use("com.ad2001.frida0x2.MainActivity") 返回的是类 wrapper。这个 wrapper 的receiverHandle 本身就是 0x0,因为它不是某个实例对象。随后你第一次访问:MainActivity.get_flag,js_runtime默认会生成makeMethod(canonicalMethodName, undefined, undefined, false),false意思就是这个 method wrapper 一开始默认并没有被认定为静态方法,真正关键发生在你执行MainActivity.get_flag(4919) 的时候。此时会进入 JsJavaInvoke。它先从 method wrapper 里解析出:

它表面还像个“未定的实例方法包装器”,但又没有 receiver,因为是在类 wrapper 上调的,于是 JsJavaInvoke 会先按当前记录去做一次重载解析。如果失败,并且发现:

它就会走一个静态回退分支,大意就是:

如果这次成功,就把当前 record 正式翻成静态方法。也就是说,Nook 的策略不是“类 wrapper 上的所有方法先天就标成静态”,而是:

然后尝试签名解析,直到收敛到目标方法,比如这里的 get_flag(int),等这些都定下来之后,才会进DefaultInvokeJavaMethod(...)。这里才是真正的JNI 分叉点:

第三个例子是 frida-0x3,这里要做的事情很简单:修改静态字段 Checker.code,让后续逻辑进入正确分支。

test3

这个案例对应的是 Frida Java API 里非常常见的一类操作:字段读写

方法 Hook 和字段修改其实是两种不同的能力边界:

Hook效果如下:

Hook代码也很简单:

在Nook中,Java字段也是通过暴露成一个字段包装器,然后通过.value去读写真实字段值,Checker.code.value背后的含义是:Checker.code先得到也给JavaField Wrapper,再通过这个wrapper的getvalue()/set value(...)去触发真实的字段读写,那字段又是怎么被包装出来的呢?在CreateJavaUseWrapper生成的Proxy里,当脚本访问Checker.code时,Nook会先走__nookJavaResolveField(className, fieldName, isStaticGuess, loaderHandle),如果字段存在,就调用makeField(fieldName, signature, isStatic)生成一个字段包装器:

判断code是否为静态字段的方法是:Nook 在字段解析时会先看当前 wrapper 有没有 receiver。类 wrapper 的 receiverHandle 是0x0,所以访问 Checker.code 时,会先按“静态字段”去解析,底层再通过反射和 Modifier.isStatic(...) 确认它确实是静态字段。最后这个字段会被收敛成:

读字段时,Checker.code.value 会进 __nookJavaReadField(...),原生入口是 JsJavaReadField,最终在DefaultReadJavaField(...) 里,如果是静态 int,就走 JNI 的GetStaticIntField(...)

写字段时,Checker.code.value = 512 会进 __nookJavaWriteField(...),原生入口是 JsJavaWriteField,最后在DefaultWriteJavaField(...) 里按字段类型和静态属性分叉。这里因为它是 static int,最终走的是SetStaticIntField(...)

Frida的做法则不是等到真正调用或访问时才去猜“这个成员是不是静态”,,而是在建模阶段就已经把“字段/方法”和“静态/实例”分开编码了:

当在Frida中写Check.code时:

第四个例子是 frida-0x4,他的核心是创建一个java对象实例,然后调用这个对象的实例方法。

这个例子验证的是 Frida 风格 Java 对象编程里更进一步的一层:脚本能不能像操作普通对象一样去操作目标 App 的 Java 实例。

Hook效果:

Hook代码:

核心分两步走,首先是Check.$new创建Java对象的实例,Nook在是怎么做的呢?

$new在Nook中还是被统一在了Java 调用桥中,在js_runtime里类wrapper自带一个$new,他会先构造一个假的method target:

然后直接丢给 __nookJavaInvoke(...)。也就是说,Nook 把“构造对象”也当成一种特殊的 Java 方法调用来处理,目标就是构造函数<init>。后面到了 DefaultInvokeJavaMethod(...),它会先判断当前方法是不是构造函数,如果是,就走构造分支:

所以 $new() 最后真的是走 JNI NewObjectA(...) 把 Java 对象构造出来,并且返回给脚本的不是裸 jobject,而是一个新的对象 wrapper。JsJavaInvoke 看到返回值是 Java object 后,会再调用CreateJavaUseWrapper(...),但这次传进去的 receiverHandle 不再是 0x0,而是刚创建好的那个对象句柄。于是:

当脚本继续执行:

此时 instance 已经不是类 wrapper,而是对象 wrapper。所以后面 Proxy 再创建 get_flag 这个 method wrapper 时,会把真实对象句柄带进去,这时 JsJavaInvoke 再去调用这个方法,就天然会按实例方法那条路径走。

第五个例子是 frida-0x5

这个题的重点不是自己 new 一个对象,而是找到当前进程里已经存在的 MainActivity 实例,然后在这个 live instance 上调用 方法

Hook效果:

Hook代码:

这里有两点和之前不同,一个是Java.performNow(function(...)),一个是Java.choose(...)\

为什么这里需要用Java.performNow(...)?其实是在表达一种不同的时机语义。Nook的Js runtime里面他们是这样被定义的:

也就是说Java.performNow(fn)只保证当前前程进入Java.vm.perform(fn),不会额外等待ClassLoader ready,不会走ready callback队列。

和上一个例子中自己构造一个Java对象然后调用实例方法不同,这里是直接去VM中找系统已经创建好的一个MainActivity,因为Activity这种Android组件不是普通类,不能简单的$new就指望它处于一个有效状态里,它依赖生命周期、上下文、主线程、系统管理等,所以这里需要用Java.choose(...)去找现成的实例,而不是自己new。

Nook 里 Java.choose(...)会做三件事:

默认实现最终落到DefaultEnumerateJavaObjects(...)。这里不是自己维护对象表,而是直接用dalvik.system.VMDebug.getInstancesOfClasses(...),然后把这些对象转成全局引用,再包装成对象 wrapper 交给脚本。大致流程是:

所以 onMatch(instance) 里的 instance 不是一个简单句柄,而是一个真正的对象 wrapper。它带着真实 receiverHandle,因此后面可以直接instance.flag(1337)

第六个例子是 frida-0x6,这个案例的核心是给Java方法传入一个对象参数

Hook效果:

Hook代码:

它比前一个例子再进一层。这里不仅要找到现有实例,还要:

核心代码是:

首先是$new,这个在上面的例子中做过了介绍,会返回一个Java Checker对象Wrapper,然后当我们将其作为参数传递给instance.get_flag(checker)时,Nook需要回答两个问题:1.这个JS值是不是一个Java 对象;2.如果是,它内部对应的jobject/handle是什么。

这一步首先发生在ParseJavaJsValue(...),当它看到传进来的 JS 值是一个对象时,会检查这个对象上有没有__nookJavaReceiverHandle,如果没有,再看__jptr,只要能从这两个属性里取到有效句柄,就会把它解析成JavaJsValueKind::kObject,并记录object_handleobject_class_name,所以它内部是真实包含着Java对象句柄的。

instance.get_flag(checker) 进入 JsJavaInvoke 之后,参数会先都被解析成 JavaJsValue。此时 checker 这个参数已经是kind = kObjectobject_handle = <Checker 实例句柄>

随后在真正发起 Java 调用前,Nook 会根据目标方法签名,对每个参数执行ConvertJavaJsValueToNookJavaHookValue(...)

对于对象参数,这层会把value.object_handle直接转换成jobject也就是out_value->l = reinterpret_cast<jobject>(value.object_handle);

这一步意味着脚本侧传进去的不是“某种序列化后的对象描述”,而是直接把对应 Java 对象实例本身传回给了目标方法。

把整题串起来,其实就是下面这条链:

第七个例子是 frida-0x7,这里的重点是构造函数路径,也就是在对象创建时就把逻辑改掉,让后续条件天然成立。

构造函数 Hook 一直都是 Frida Java 能力里很有代表性的一类场景。因为它意味着你不只是“在对象已经存在后动手”,而是可以在对象诞生那一刻就介入。

Hook效果:

Hook代码:

代码的核心就是Checker.$init.implementation和this.$init(600, 600);,它的语义就是:在当前hook回调上下文中调用原始构造函数实现并修改参数。

Nook中发挥了作用的是callOriginal,构造函数命中时,Nook 会给当前回调 receiver 上挂一个专用的 callOriginal 入口,对构造函数来说,$init(...) 在脚本层就是这个原始构造逻辑的入口映射。所以 Nook 里 Checker.$init.implementation 这条链本质上还是走前面同一套 Java Hook 安装流程,只不过目标方法变成了 <init>

前面几个例子基本都在 Java 层,到了 frida-0x8,重点开始转向 Native。

这个题的核心是 Hook Native 比较逻辑,把参与比较的 secret 内容观察出来。

Hook效果:

Hook代码:

这里的Hook对象选择的是libc中的strcmp函数,代码核心首先是Module.getExportByName("libc.so", "strcmp"),这句代码的含义是去libc.so的导出表里找strcmp,直接拿到它的运行地址,Frida中常见的几种拿地址的方式有:

Nook对Module.getExportByName(...)背后的实现其实很简单:

Frida的做法也是类似的,不过他是由gumjs暴露Module API,底层由gum去做模块/符号解析。

然后是Interceptor.attach(strcmpAdr,...),这句代码的含义是在目标函数地址入口打上Hook,函数每次被调用的时候先进入onEnter从而执行我们的逻辑。

大致流程是:

所以Nook的Interceptor.attach(...)本质就是在安装一个inline hook,安装成功后把

这些信息记进 native hook 注册表。 后面函数命中时,再根据 hook_id 回调到当前脚本注册的 onEnter/onLeave

这里还有一个问题就是对于strcmp这类高频的基础函数,一旦Hook引擎热路径中有多余的开销,它会被strcmp这种函数成百上千的放大,一开始Nook的做法就是简单的:

对于 strcmp 这种高频函数:

于是很容易出现两类放大:

这时用户看到的外部现象就是:

Nook的做法主要有三点:

首先是当前线程递归保护,直接 bypass 到 original,在 DispatchInlineHookSlot(...) 开头先判断:

这样就挡住 JS callback 内部再次触发 strcmp 的重入,也能挡住 runtime/日志/字符串辅助逻辑造成的二次命中。

第二就是进入 JS callback 前,临时把当前线程标记为 ignore。现在代码里有:

并且在真正同步调用 JS callback 之前,会这样包一层:

enter 和 leave 两边都这样做了。

这意味着:

第三点是把热路径上的 slot 读取改成 runtime snapshot,早期每次触发都去锁表、读 slot、拼装状态,现在 Nook在 ActivateInlineHookSlot(...) 里,安装 hook 成功后会把热路径需要的最小信息整理到:

DispatchInlineHookSlot(...) 走的是:GetInlineHookRuntimeSnapshot(...),这条路径只用原子位判断 slot 是否可用,再读预先整理好的 runtime snapshot。

Frida 在这件事上更成熟,在 frida-gum/gum/guminterceptor.c 里,_gum_function_context_begin_invocation(...) 一进来就先做:

然后 Frida 还有明确的每线程忽略计数:

后面Nook会慢慢补充优化上来。

第九个例子frida-0x9,和上一个例子相比,这里不只是观察,而是直接修改 Native 函数返回值,让 check_flag() 强制返回正确结果。

Hook效果:

Hook代码:

这个脚本的完整含义是:

和上一个例子不同,这里需要hook的不是通用的libc函数,而是JNI导出的真正目标函数,这里的关键代码是retval.replace(1337);retval并不只是一个普通的JS number,在Nook中,leave回调收到的是一个返回值包装对象wrapper,在js_runtime里面Nook会专门构造这个返回值对象:

也就是说retval既能被打印、读取值,也能通过replace()把底层返回值标记为"需要覆盖"。

Nook这部分的实现可以分为两层:

leave回调结束后,修改后的返回值又是怎么生效的呢?Nook 的 inline hook dispatch 流程大致是:

第十个例子frida-0xA这次的重点不是拦截,而是主动调用一个 Native 函数,也就是脚本层的 NativeFunction 能力。

Hook效果:

Hook代码:

hook代码的含义是:

核心代码就是首先是*var* get_flag = new NativeFunction(getFlagAddr, "void", ["int", "int"]),把一个Native地址包装成了JS里可以调用的函数对象,Nook 会做这几件事:

所以 NativeFunction 的本质不是“立刻调用一次”,而是:

那它又是怎么变成一个可调用函数对象的呢?核心在CreateNativeFunctionValue(...)

它做了两层事:

也就是说,脚本里拿到的 get_flag 虽然看起来像普通 JS 函数:

但实际上它背后是一个“带闭包数据的宿主函数”:

这点和 Java 侧的 method wrapper 思路很像。

然后当真正调用get_flag(1,2)时,会进入JsNativeFunctionInvoke(...),它的执行流程可以概括成:

DispatchTypedNativeFunction(...)负责的是Nook底层如何真正去call这个native函数,DispatchTypedNativeFunction(...) 会先判断:

如果没有浮点类型,它会走比较直接的 raw 调用路径:

本质上就是把目标地址强转成函数指针,然后调用。

但如果涉及浮点参数或浮点返回值,由于

所以 Nook 又提供了另一层 typed dispatch:

它会根据参数类型组合,挑出合适的函数签名去调。

然后Process.attachModuleObserver(...)监听模块加载时机,确保地址可用后再主动call。

Nook 里 Process.findModuleByName(...) 是怎么做的呢?

Process.attachModuleObserver(...) 呢?

注册完成后,如果提供了 onAdded,它会立刻:

Native侧模块加载后,Nook这边最终会走NotifyModuleObserverModuleLoaded(const char* module_path, ...)


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 5
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回