G.O.S.S.I.P阅读推荐2026-02-26Frida+js->ReactNative

admin 2026-03-03 04:48:55 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了利用Frida向ReactNative应用注入JavaScript代码的技术方案。针对ReactNative0.74版本引入的Bridgeless架构及旧版Legacy架构,详细阐述了在Android和iOS平台下拦截JSBundle加载及主动注入JS的具体实现方法。文章改进了以往通过定时器获取实例的低效方式,提出利用Frida的chooseAPI直接获取实例,并通过Hook原生Alert模块解决了执行结果回传的难题,实现了类似浏览器F12控制台的调试效果,具有较高的实战参考价值。 综合评分: 93 文章分类: 移动安全,逆向分析,安全工具,实战经验


cover_image

G.O.S.S.I.P 阅读推荐 2026-02-26 Frida + js -> React Native

安全研究GoSSIP

2026年2月26日 20:20 上海

以下文章来源于非尝咸鱼贩 ,作者0xcc

非尝咸鱼贩 .

临渊羡鱼,不如在家咸鱼

动机和背景

我好像有个偏小众的恶趣味,就是给别人生产环境的应用开 js 控制台。虽然几年前发的那个某小程序的思路早就不能用了。

最近看到国外开发者 Pilfer 一直在社交网络上宣传他的新产品 Bytecode Studio,这是一款专门用于反编译和分析 React Native 字节码的工具。

https://bytecodestudio.com

他在两年前写过一篇博客 Reverse Engineering and Instrumenting React Native Apps:

https://pilfer.github.io/mobile-reverse-engineering/react-native/reverse-engineering-and-instrumenting-react-native-apps/

这篇文章介绍了在 Android 平台的 legacy 架构下动态向当前运行的 React Native 应用注入 JavaScript 代码的过程。通过 js 层的 hook,他可以实现拦截网络请求、JSON 序列化,以及无意中 dump 一些 UI 层级结构等功能。文章里的相关代码开源了:

https://github.com/Pilfer/heresy

他的 GitHub 主页还有一个基于 Rust 的 hermes 字节码反编译工具。有了这些技术积累,并不奇怪他会做 Bytecode Studio。

而数天前 React Native 发布了 0.74 版本,默认启用 Bridgeless 架构。请注意之前引用的文章只讲了 legacy 架构。

https://reactnative.dev/blog/2024/04/22/release-0.74

而还是这几天,radare2 发布了一款插件 r2hermes,专门用于分析 hermes 字节码。

https://github.com/radareorg/r2hermes

虽然笔者不做客户端,这一系列内容勾起了我的兴趣,也就有了今天这篇文章。

Legacy 和 Bridgeless 是什么鬼

React Native 有两套架构。

Legacy 架构下,JavaScript 运行在独立线程,通过 Bridge 与 Native 通信。Native 侧核心类在 iOS 是 RCTCxxBridge,Android 是 CatalystInstanceImpl。所有跨语言调用都要序列化成 JSON 经过 Bridge 传递。

0.74 版本默认启用的 Bridgeless 架构移除了这座”桥”,JavaScript 直接调用 Native 方法,性能更好。iOS 侧核心类变为 RCTInstance,Android 侧变为 ReactInstance

向其中注入 JS 代码可以拦截网络请求、修改界面、调试业务逻辑等,静态反编译和动态修改运行时是软件逆向常见的手法。下面结合具体代码来说明实现思路。

脚本实现

我们一共要支持 4 种情况:Android 和 iOS 的 Legacy 和 Bridgeless 架构。

frida 里可以简单实用 ObjC.classes 和 Java.classes 来检查类是否存在。

| 平台 | Legacy 架构 | Bridgeless 架构 | | — | — | — | | iOS | RCTCxxBridge | RCTInstance | | Android | CatalystInstanceImpl | com.facebook.react.runtime.ReactInstance |

拦截 JS Bundle 加载

React Native 的 JS 代码以 Bundle 的形式加载。如果应用版本很旧,可能用的是压缩混淆后的 js,分析很简单。不过目前多数情况都是 hemes 字节码,分析门槛比前者显著提高。

我们可以拦截以下方法拿到 js 或者字节码。

iOS legacy 架构:

-[RCTCxxBridge executeSourceCode:withSourceURL:sync:] -[RCTCxxBridge executeApplicationScript:url:async:]

iOS bridgeless 架构:

-[RCTInstance _loadJSBundle:]

Android legacy 架构:

CatalystInstanceImpl 的 loadScriptFromAssets 以及 loadScriptFromFile

Android bridgeless 架构:

com.facebook.react.runtime.ReactInstance 的 loadJSBundleFromFile 和 loadJSBundleFromAssets

主动注入 JS 代码

React Native 核心的逻辑使用 C++ 实现,用 frida 直接交互虽然不是不可能,但是构造参数非常麻烦,还得处理内存管理和偏移量适配等问题。

从 Java 层或者 Objective-C 层并没有提供可以传入字符串的接口,只能把 js 写入临时的 bundle 然后载入。

在文章开头提到的 Pilfer 的博客里,作者为了拿到当前运行的 CatalystInstanceImpl 实例,用了比较 hack 的方法,创建定时器等待 loadScriptFromAssets 被调用,然后在 hook 里把实例保存下来。

// This is the app identifier you're trying to hookconst package_name = 'com.foo.bar';// Write the hermes-hook.js payload to fileconst f = new File(`/data/data/${package_name}/files/hermes-hook.js`, 'w');f.write(`console.log(Object.keys(this)); console.log('hello from React Native!');`);f.close();Java.perform(function () {  // Lazily wait for the class to be available to us    var looper = setInterval(function () {    try {      const CatalystInstanceImpl = Java.use("com.facebook.react.bridge.CatalystInstanceImpl");      CatalystInstanceImpl.loadScriptFromAssets.implementation = function (assetManager, assetURL, z) {        // Load the original index.android.bundle        this.loadScriptFromAssets(assetManager, assetURL, z);        // Load custom JS into the global hermes context        this.loadScriptFromFile(`/data/data/${package_name}/files/hermes-hook.js`, `/data/data/${package_name}/files/hermes-hook.js`, z);      };      clearInterval(looper);    } catch (error) {      console.log('failed');    }  }, 10);});

其实 frida 本身的 Java.choose 和 ObjC.choose 就可以直接在内存里检索到实例。

以 iOS 的 legacy 架构为例:

const nsData = ObjC.classes.NSData.dataWithContentsOfFile_(path);const nsURL = ObjC.classes.NSURL.fileURLWithPath_(path);instance["- enqueueApplicationScript:url:onComplete:"](nsData, nsURL, NULL);

新架构注入 js bundle 用的是 RCTInstance 的 _loadJSBundle: 方法。但是没想到吧,还有惊喜。这个方法在三年前的提交改过名字,之前是没有下划线的

https://github.com/facebook/react-native/commit/0dcf81b4f19484a4e43

不过这个适配好做,直接 respondsToSelector: 判断一下就行。

获取返回值

在这里遇到了另一个问题,上层封装的加载 js 接口并不等待脚本执行完成,也没有提供获取执行结果的接口。虽然我们在 js 脚本里使用 console.log 可以在 iOS 的系统日志或者 Android 的 logcat 里看到输出,对手工测试的场景绰绰有余,但如果想开发自动化工具,到处 grep 就不太优雅。

很容易想到一个很糟糕的思路:js 里内置了 XMLHttpRequest,直接把执行的结果回传到一个本地监听的 http 服务器上。也不是不可以。

那么既然我们想到了 React Native 内置函数这一点,又有二进制级别的函数插桩,不妨直接用 alert 当作 callback 回传。这并不是笔者原创,多年以前就有人用这个思路实现 WebView 的 js 和 native 互传数据了。

先把待执行代码包装一下:

const wrapped = `try {  var r = (function() { return ${script} })();  alert('frida-callback:${id}:' + JSON.stringify(r));} catch (e) {  alert('frida-callback:${id}:' + JSON.stringify({ error: e.message }));}`;

接着这个字符串 frida-callback:… 会被封装成字典格式传到 native 层。以 iOS 为例,就是一个 NSMutableDictionary,其中的 key 是 “message”。但这里有一个小坑。从 6 年前的一个提交到截止本文发布的版本,这个 native 方法接受的参数是一个 C++ 的对象,解引用第一个指针才是 NSMutableDictionary:

RCT_EXPORT_METHOD(alertWithArgs : (JS::NativeAlertManager::Args &)args callback : (RCTResponseSenderBlock)callback)

而 2019 年的这个 a5ad0bf12468fc831c2a 提交当中,函数原型曾经是直接传的:

RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args                  callback:(RCTResponseSenderBlock)callback)

这就导致同样的代码会崩,还得特殊处理一下。不过都 7 年了,如果不是特别执着兼容性,直接按照新的函数原型来构造参数就好了。

&nbsp;&nbsp;const&nbsp;{&nbsp;RCTAlertManager&nbsp;} =&nbsp;ObjC.classes;&nbsp;&nbsp;const&nbsp;method =&nbsp;RCTAlertManager["- alertWithArgs:callback:"];&nbsp;&nbsp;const&nbsp;original = method.implementation;&nbsp; method.implementation&nbsp;=&nbsp;ObjC.implement(&nbsp; &nbsp; method,&nbsp; &nbsp;&nbsp;function&nbsp;(&nbsp; &nbsp; &nbsp; handle: NativePointer,&nbsp; &nbsp; &nbsp; selector: NativePointer,&nbsp; &nbsp; &nbsp; args: NativePointer,&nbsp; &nbsp; &nbsp; callback: NativePointer,&nbsp; &nbsp; ) {&nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;message =&nbsp;new&nbsp;ObjC.Object(args.readPointer())&nbsp;// <- 注意 readPointer&nbsp; &nbsp; &nbsp; &nbsp; .objectForKey_("message")&nbsp; &nbsp; &nbsp; &nbsp; .toString();&nbsp; &nbsp; &nbsp;&nbsp;console.debug(`React Native alert(${message})`);&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;original(handle, selector, args, callback);&nbsp; &nbsp; },&nbsp; );&nbsp;&nbsp;console.log('replaced RCTAlertManager["- alertWithArgs:callback:"]');

这个 Module 看上去不受 bridgeless 架构的影响,都可以用同样的方式 hook。

最后看看效果,搞了个类似 F12 的东西:


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:安全研究GoSSIP 《G.O.S.S.I.P 阅读推荐 2026-02-26 Frida + js -> React Native》

评论:0   参与:  0