论坛首页 安全编程开发区 阅读主题

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

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

项目仓库在:

Nook仓库地址

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

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

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

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

这里案例使用的是d56K9s2c8@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关键方法如下:


在 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 会得到

...(内容过长,已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291347.htm

暂无回复,快来抢沙发吧!

请登录后参与讨论

立即登录 注册账号