一开始的 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
[原创]从0到1构建一个Hook工具之Frida-like风格的Hook
1 浏览
0 回复
暂无回复,快来抢沙发吧!