Pipeline de análisis

referencia técnica · Revisado el 12 de agosto de 2026

1. Qué es y por qué existe

Los demás documentos del corpus explican piezas: el formato DEX, el APK Signing Block, qué hace jadx. Este explica una secuencia, y la secuencia importa porque el orden de los pasos determina cuánto tiempo se pierde.

El error caro del análisis de un APK no es equivocarse leyendo bytecode. Es decompilar antes de mirar el manifiesto: invertir cuarenta minutos y ocho gigabytes de RAM en producir un árbol de fuentes de una aplicación protegida con un packer, o que ni siquiera era un APK sino un ZIP con veintisiete splits dentro. Los seis pasos están ordenados por coste creciente y valor decreciente: los cuatro primeros son casi instantáneos y muchas veces suficientes; el quinto —decompilar— es el que puede fallar, tardar y mentir.

 coste ─────────────────────────────────────────────────────────────────▶
   0          1          2         3          4          5         6
identificar inventariar manifiesto firma  protecciones decompilar navegar
  ~10 ms     ~50 ms      ~1 s      ~1 s      ~2 s      1-40 min    horas
 ◀───────────────────────────────────────────────────── valor por segundo

La regla que resume el documento: el manifiesto, la firma y el inventario del ZIP dan un perfil completo del comportamiento observable de la aplicación sin decompilar una sola línea (sección 5 de Anatomía de un APK). El paso 5 solo se ejecuta cuando queda una pregunta que los cuatro anteriores no han respondido.

Aquí no se repite el uso de las herramientas —eso está en 30-herramientas—, sino el encadenado: el orden, su porqué y dónde se rompe cada eslabón.

Sobre las recetas. Las de aapt2, apksigner, zipalign y dexdump están ejecutadas en la máquina de referencia el 12 de agosto de 2026 y la salida pegada es la real. Las de jadx llevan aviso explícito: no está instalado y no se instala nada en esa máquina.

2. Paso 0 — Identificar qué tienes

2.1 Por qué nunca por extensión

La extensión de un contenedor Android es un rótulo que pone quien lo distribuye, y miente con frecuencia medible: en el corpus, 14 de los 16 ficheros con extensión .apks no son un APK Set, son .xapk de APKCombo con la extensión cambiada. El detalle de cada formato está en AAB, splits y contenedores, sección 6.

La detección correcta lee el Central Directory del ZIP y busca evidencias estructurales. No descomprime nada salvo, como mucho, un manifest.json de dos kilobytes.

2.2 El algoritmo, en orden de precedencia

El orden no es una optimización: las evidencias se solapan. Un .apkm tiene entradas *.apk en la raíz, así que también cumple la regla del zip plano, y comprobarla antes clasificaría mal ocho contenedores del corpus.

             abrir como ZIP ── falla ──▶ no es un contenedor Android
                    │
   ¿toc.pb en la raíz? ────────── sí ──▶ APK Set genuino de bundletool
                    │ no
   ¿info.json + META-INF/*.RSA? ─ sí ──▶ .apkm de APKMirror
                    │ no
   ¿manifest.json con xapk_version? sí ▶ .xapk de APKCombo o APKPure
                    │ no
   ¿varias entradas *.apk? ────── sí ──▶ zip plano de splits (SAI y similares)
                    │ no
   ¿AndroidManifest.xml en raíz? ─ sí ──▶ APK suelto   ← el único analizable directamente
                    │ no
   ¿BundleConfig.pb + base/manifest/? ─▶ .aab          ← hay que pasar por bundletool
                    │ no
              DESCONOCIDO — se informa, no se adivina

Traducido a código son doce comparaciones sobre zipfile.ZipFile(ruta).namelist(), con una única sutileza: raiz = [x for x in n if "/" not in x.rstrip("/")], porque las evidencias de los pasos 1 a 3 y 5 tienen que estar en la raíz del ZIP y no en cualquier subdirectorio. El paso 3 además abre el manifest.json y comprueba que la clave xapk_version existe; que el fichero se llame así no basta.

Ejecutado sobre los contenedores del corpus, comparando el veredicto con lo que promete la extensión (salida real, recortada):

APK-SET     43 entradas  com.ejemplo.fixture.apks
APKM        18 entradas  tv.twitch.android.app.apkm
XAPK        30 entradas  GoogleKeep_com.google.android.keep.xapk
XAPK        17 entradas  com.whatsapp.apks             ← la extensión .apks dice APK-SET
XAPK        30 entradas  com.google.android.keep.apks  ← la extensión .apks dice APK-SET
XAPK        39 entradas  com.google.android.gm.apks    ← la extensión .apks dice APK-SET
ZIP-SPLITS  39 entradas  com.netflix.mediaclient.sai.zip
AAB         12 entradas  com.ejemplo.fixture.aab

Catorce desajustes, todos del mismo tipo. El desajuste no es un error: es un dato del informe. Se continúa con el formato detectado y se advierte.

2.3 Qué hacer con cada tipo

Tipo detectado Siguiente paso
APK suelto Directo al paso 1
XAPK, APKM, APK-SET, ZIP-SPLITS Extraer, hallar el base (§2.4) y analizarlo. Si hace falta la aplicación como unidad, fusionar
AAB No se analiza como APK: no lleva resources.arsc sino resources.pb. Pasar por bundletool build-apks
NO-ES-ZIP / DESCONOCIDO Parar y decirlo. Un APK corrupto no se analiza a medias

2.4 Encontrar el base dentro de un contenedor

El base es el único split sin atributo split en su <manifest>. El nombre del fichero no sirve: en los .xapk se llama <paquete>.apk, en los .apkm base.apk, y en un APK Set splits/base-master.apk.

for f in *.apk; do
  s=$($BT/aapt2 dump badging "$f" 2>/dev/null | head -1 | grep -o "split='[^']*'")
  echo "${s:-BASE}  $f"
done

Los .xapk y los .apkm traen además un JSON descriptivo —manifest.json con split_apks, info.json con pname— que ahorra el barrido, pero lo escribe el sitio de descargas y no es autoridad sobre el contenido. Sirve para orientarse; el veredicto lo da el manifiesto.

3. Paso 1 — Inventariar

3.1 Qué se aprende sin abrir nada

El Central Directory da gratis: cuántas entradas hay, cuánto ocupa cada una sin comprimir, con qué método está comprimida, cuántos DEX hay, qué ABIs se empaquetan y si existe resources.arsc. Eso responde ya a preguntas caras: si hay código nativo, si es multidex, si los recursos van mapeables, si trae baseline profile.

El script reutilizable recorre zipfile.ZipFile(apk).infolist() y acumula tres cosas por categoría de ruta: número de entradas, file_size —el tamaño sin comprimir— y el reparto de compress_type. Las nueve categorías son las de la sección 3.2 de Anatomía de un APK, con una distinción que hay que hacer a mano: dentro de META-INF/, separar las entradas de firma v1 —MANIFEST.MF y los *.SF / *.RSA / *.DSA / *.EC— del resto, porque son cosas sin relación.

Ejecutado sobre com.aurora.store_75.apk del corpus:

categoria                  n  bytes sin compr.  metodo
1 manifiesto               1            21.336  1 deflate
2 dex                      2        12.555.168  2 deflate
3 resources.arsc           1         2.551.720  1 stored
4 res/                   984         1.253.449  224 stored · 760 deflate
5 lib/                     4            37.392  4 stored
6 assets/                  4           760.550  2 stored · 2 deflate
7 META-INF firma v1        3           217.583  3 deflate
8 META-INF resto         129            83.781  129 deflate
9 raiz y otros            21           171.049  21 deflate
ABIs        : ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64']
DEX         : 2 → ['classes.dex', 'classes2.dex']
resources.arsc: 2551720 bytes, stored

3.2 Cómo se lee ese inventario

Observación Qué significa
resources.arsc stored Correcto. Desde targetSdk 30 es obligatorio; si va deflate la instalación falla (§8.4)
resources.arsc ausente No es un APK completo: es un split de solo código o de solo .so, o el contenedor se identificó mal
Varios DEX Multidex. La numeración no tiene huecos y el orden es semántico: gana la primera definición
4 ABIs APK universal: no viene de Play. Cuatro directorios no implican cuatro arquitecturas realmente soportadas
lib/ stored extractNativeLibs="false": las .so se cargan del APK y tienen que ir alineadas
assets/ grande y opaco Modelos, bases de datos precargadas… y el payload de un packer (§6.4)
META-INF resto con *.version La huella del stack: cada biblioteca AndroidX deja su fichero

Ese último punto es el más infravalorado: el listado de META-INF/ y de la raíz identifica el stack tecnológico completo sin abrir un solo DEX (sección 3.3 de Anatomía de un APK).

4. Paso 2 — Leer el manifiesto

4.1 Las dos vistas, y hacen falta las dos

aapt2 dump badging es la vista interpretada: resume lo que el instalador y Play leen, con los permisos implícitos ya resueltos. Es la primera orden de cualquier análisis.

$BT/aapt2 dump badging $CORPUS/com.aurora.store_75.apk | head -6
package: name='com.aurora.store' versionCode='75' versionName='4.8.3' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
minSdkVersion:'23'
targetSdkVersion:'37'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.FOREGROUND_SERVICE'

aapt2 dump xmltree es la vista cruda: el árbol AXML con los identificadores de atributo tal como están en el binario. Hace falta para todo lo que badging no resume —y badging no resume nada del <application>.

$BT/aapt2 dump xmltree $CORPUS/com.aurora.store_75.apk --file AndroidManifest.xml \
  | grep -E "E: application|debuggable|usesCleartextTraffic|networkSecurityConfig|allowBackup|extractNativeLibs"
      E: application (line=72)
        A: http://schemas.android.com/apk/res/android:allowBackup(0x01010280)=true
        A: http://schemas.android.com/apk/res/android:extractNativeLibs(0x010104ea)=false

Lo que no aparece, no está declarado, y eso también es información: debuggable, usesCleartextTraffic y networkSecurityConfig tienen valor por defecto y hay que conocerlo antes de concluir nada.

4.2 Los diez campos que se miran

Campo Dónde Qué dice
package <manifest> La identidad real. No es el nombre del fichero: en el corpus, org.fossify.calculator_1.4.0.apk declara org.fossify.math
versionCode / versionName <manifest> El primero es el que compara el instalador; el segundo es texto para humanos
minSdkVersion <uses-sdk> Suelo de compatibilidad. Cambia el veredicto de apksigner verify (§5.2)
targetSdkVersion <uses-sdk> Qué comportamientos del sistema se aplican. ≥ 30 obliga a resources.arsc sin comprimir; fija el esquema de firma mínimo
uses-permission <manifest> La superficie de capacidades. Un permiso no declarado no se obtiene: es un límite duro, no una pista
exported componentes Qué puede invocar otra aplicación: la superficie de ataque local (§4.3)
android:debuggable <application> true en un APK de distribución permite acoplar un depurador. Por defecto false
android:usesCleartextTraffic <application> Tráfico HTTP en claro. Por defecto false desde targetSdk 28
android:networkSecurityConfig <application> Apunta a un recurso XML que puede permitir HTTP, fijar CA propias o confiar en el almacén del usuario, que es lo que habilita interceptar TLS
android:extractNativeLibs <application> false ⇒ las .so se cargan del APK y tienen que ir stored y alineadas

networkSecurityConfig merece el rodeo extra: el atributo solo da un resource ID, y el contenido está en un AXML bajo res/. Se resuelve en dos pasos —de identificador a ruta con dump resources, y de ruta a árbol con dump xmltree --file— y es lo que separa «declara una configuración de red» de «confía en las CA que instale el usuario».

4.3 Componentes exportados, con la regla del valor implícito

exported no declarado no significa false. La regla: si el componente tiene al menos un <intent-filter>, el valor implícito es true; si no, false. Desde targetSdk 31 declararlo es obligatorio cuando hay intent-filter, pero el corpus está lleno de aplicaciones anteriores.

El extractor recorre la salida de xmltree con tres variables de estado —elemento actual, nombre del componente y valor explícito de exported— más un contador de intent-filter vistos dentro del componente. Cada línea E: <elemento> (line=…) con elemento en {activity, activity-alias, service, receiver, provider} cierra el componente anterior y abre uno nuevo; el primer android:name(0x01010003)= que aparezca dentro es su nombre, y android:exported(0x01010010)= su valor explícito si lo hay. Al cerrar se aplica la regla del párrafo anterior.

Sobre Aurora Store, salida real (recortada; 21 componentes, 6 exportados):

activity        exported=true                             com.aurora.store.MainActivity
activity        exported=false                            com.aurora.store.ComposeActivity
provider        exported=true                             rikka.shizuku.ShizukuProvider
receiver        exported=true                             com.aurora.store.data.receiver.DeviceOwnerReceiver
service         exported=true                             androidx.work.impl.background.systemjob.SystemJobService
receiver        exported=true                             androidx.profileinstaller.ProfileInstallReceiver

Esa lista es el mapa de entrada a la aplicación, y es lo que alimenta el paso 6.

5. Paso 3 — Verificar la firma

5.1 La orden

Sobre el base.apk de Google Keep del corpus (salida real, sin los digests SHA-1 y MD5):

$BT/apksigner verify --verbose --print-certs keep/com.google.android.keep.apk
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): false
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
Verified for SourceStamp: true
Number of signers: 1
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate DN: CN=Android, OU=Android, O=Google Inc., …
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate SHA-256 digest: 7ce83c1b…2905053
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32)         certificate SHA-256 digest: f0fd6c5b…2d60db83
Source Stamp Signer:                                      certificate SHA-256 digest: 3257d599…733bbd6d

Dos firmantes v3 con rangos de SDK disjuntos, claves distintas y un source stamp: es rotación de clave desplegada en producción, y se lee de un vistazo.

5.2 La trampa: verify no informa de los esquemas presentes

apksigner verify no responde «¿está presente este esquema?» sino «¿se usó este esquema para verificar en el rango de API que me has dado?». Es la fuente de confusión número uno del ecosistema y se demuestra en dos órdenes. Sobre una copia del corpus firmada con v1+v2+v3:

$BT/apksigner verify --verbose --min-sdk-version 24 todos.apk | head -3
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
$BT/apksigner verify --verbose --min-sdk-version 21 todos.apk | head -3
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true

El mismo fichero, dos veredictos. Y los ficheros de v1 están físicamente dentro en los dos casos:

   156212  01-01-1981 01:01   META-INF/PROC.SF
     1265  01-01-1981 01:01   META-INF/PROC.RSA
   156085  01-01-1981 01:01   META-INF/MANIFEST.MF

La regla de lectura. Para saber qué esquemas hay, se mira el ZIP (META-INF/*.SF) y el APK Signing Block (APK Signing Block, sección 9). Para saber si se instalará, se ejecuta verify con el minSdkVersion real de la aplicación, que se leyó en el paso 2. Un informe que colapse las dos preguntas en un booleano miente la mitad de las veces.

Consecuencia recíproca: v1 sola ya no es firma. El mismo APK firmado únicamente con v1:

DOES NOT VERIFY
ERROR: Target SDK version 36 requires a minimum of signature scheme v2; the APK is not signed with this or a later signature scheme

Criptográficamente correcta, y aun así no se instala.

5.3 Qué dice la firma sobre la procedencia

Observación Lectura
Un firmante con DN autofirmado del desarrollador Distribución directa: F-Droid, sitio propio, release manual
CN=Android, O=Google Inc. Play App Signing: el APK lo re-firmó Play. La clave del desarrollador no aparece
Dos firmantes v3 con rangos de SDK disjuntos Rotación de clave desplegada (v3.1): los dispositivos antiguos validan con la clave vieja
Verified for SourceStamp: true Bloque 0x6dff800d: acredita el origen aparte de la firma de distribución
CN genérico y validez de 30 años Nada concluyente: es la convención de keytool

La evidencia más fuerte de procedencia es el frosting de Play, el bloque 0x2146444e que Play inserta en el APK Signing Block al distribuir. apksigner no lo reporta —no está en apksig— así que hay que buscarlo en el binario; el volcador de la sección 10 de APK Signing Block lo hace, y su sección 11 documenta qué contiene.

Aparece en 25 de los 717 APK del corpus, exactamente los que vienen de Play — incluidos los base.apk dentro de los .apkm de APKMirror, lo que demuestra que ese sitio redistribuye binarios de Play sin recompilarlos. Ninguno de F-Droid lo lleva.

⚠️ sin verificar: la interpretación campo a campo del protobuf del frosting. El esquema .proto no es público. Lo que sí está verificado es su presencia, su identificador y su correlación con la procedencia. El source stamp (0x6dff800d) sí lo reporta apksigner por su nombre.

5.4 Firma y splits

Un conjunto de splits tiene que estar firmado entero con la misma clave: PackageManager los trata como una sola aplicación. Comprobarlo antes de intentar nada es un bucle sobre apksigner verify --print-certs quedándose con el certificate SHA-256 digest de cada uno; más de un digest distinto significa que el conjunto no instalará (INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, catalogado en Diagnóstico de fallos, sección 3).

Un caso que despista: el META-INF/APKMIRRO.RSA de un .apkm es la firma de APKMirror sobre el contenedor, no la de la aplicación. Darla como veredicto sobre la aplicación es un error de análisis frecuente.

6. Paso 4 — Detectar protecciones antes de perder la tarde

6.1 Qué se busca

Este paso responde a una sola pregunta: ¿merece la pena decompilar? Cuatro comprobaciones, todas sobre el Central Directory y el primer DEX, todas en menos de dos segundos.

Lo que sigue es detección. Cómo funciona cada familia está en 40-ofuscacion, de R8 y ProGuard a Anti-análisis y hardening. Neutralizar cualquiera de ellas está fuera del alcance de esta publicación; aquí se detecta y se informa.

6.2 Comprobación 1 — la métrica de nombres

La señal más barata de ofuscación de identificadores: la fracción de clases cuyo nombre simple tiene uno o dos caracteres. Se lee del type_ids del primer DEX, sin desensamblar nada — bastan dos campos del header_item:

ss, so = struct.unpack_from("<II", dex, 56)     # string_ids_size / string_ids_off
ts, to = struct.unpack_from("<II", dex, 64)     # type_ids_size   / type_ids_off
# cada type_id es un índice al string pool; cada string es uleb128(longitud) + MUTF-8 + 0x00
# descartar los descriptores del framework (Ljava/, Landroid/, Lkotlin/…) y medir
# len(nombre_simple) <= 2 sobre las clases restantes

La estructura exacta de esas tablas está en Formato DEX.

6.2.1 El umbral de dos caracteres es el instrumento equivocado

Es la trampa de esta comprobación y conviene verla antes de usarla. Medido sobre el primer DEX de cada base.apk, con las clases del framework descartadas y el nombre simple sin sufijo de clase interna:

Aplicación Clases ≤2 car. ≤3 car. Mediana
Twitch 6.990 78,4 % 78,9 % 1
Fossify Calculator 5.280 85,1 % 85,1 % 1
Google Keep 7.482 2,3 % 97,0 % 3
WhatsApp 6.228 0,0 % 93,7 % 3
Netflix 8.920 9,9 % 20,0 % 15
Termux 2.696 5,0 % 5,2 % 16
F-Droid 9.793 0,7 % 1,0 % 17

Keep y WhatsApp dan 2,3 % y 0,0 % con el umbral de dos caracteres, y 97,0 % y 93,7 % con el de tres. Un lector que se quede en la primera columna los clasifica como «sin ofuscar» cuando están renombrados de arriba abajo: sus clases se llaman LX/000;, exactamente tres caracteres, porque R8 alarga los nombres a medida que se llena el ámbito.

El indicador robusto es la mediana de la longitud del nombre simple. La distribución es netamente bimodal —1 a 3 en el grupo renombrado, 15 a 17 en el que no— y no hay ninguna aplicación del corpus en la zona intermedia. Un umbral fijo cae del lado equivocado en cuanto la aplicación es grande; la mediana no.

6.2.2 Las dos lecturas que sí valen

  1. Mediana ≤ 3: renombrado por R8 o equivalente. El código sale como a.b.c y sin mapping.txt no vuelve. Enlaza a R8 y ProGuard.
  2. La métrica mide renombrado, no protección. Netflix tiene mediana 15 —no está renombrado— y es la muestra más protegida del corpus: usa DexGuard, que cifra cadenas y añade comprobaciones de integridad sin necesidad de acortar nombres de clase. Y la calculadora de Fossify tiene mediana 1 siendo software libre sin ninguna intención de ocultarse. Una métrica alta predice incomodidad; una métrica baja no predice nada.

⚠️ Qué no prueba esta métrica: los porcentajes dependen por completo del universo que se mida —aquí, los nombres simples de clase del type_ids del primer DEX— y del umbral que se elija, así que no son comparables con los de ninguna métrica que cuente otra cosa. Cambiar el universo mueve los valores y puede alterar hasta el orden entre aplicaciones. Lo que sobrevive a ese cambio es la separación bimodal y el uso de la mediana: R8 y ProGuard, sección 10, llega a la misma conclusión midiendo sobre los identificadores definidos en lugar de sobre las clases.

6.3 Comprobación 2 — la ofuscación de recursos

Ortogonal a la anterior y visible en un dump resources: el nombre lógico se conserva y el fichero se llama de otra forma.

$BT/aapt2 dump resources $CORPUS/org.fossify.calculator_1.4.0.apk | head -6
Binary APK
Package name=org.fossify.math id=7f
  type anim id=01 entryCount=40
    resource 0x7f010002 anim/abc_grow_fade_in_from_bottom
      () (file) res/aM.xml type=XML

res/aM.xml en lugar de res/anim/abc_grow_fade_in_from_bottom.xml. En el corpus también lo hace Google Keep, cuyos splits de densidad traen res/-B.png y res/0x.9.png. Es lo que deshace APKEditor refactor; ver Deofuscación.

6.4 Comprobación 3 — la forma del DEX

Un packer sustituye el DEX original por un stub que descifra y carga el código real en ejecución. La huella de forma es característica: un solo DEX pequeño junto a bibliotecas nativas grandes y un assets/ opaco.

Señal Umbral orientativo
Un solo classes.dex de menos de ~400 KB en una aplicación con .so Muy sospechoso
Entradas assets/*.dat, *.jar, *.dex, *.bin grandes Candidato a payload
.so con nombre de familia conocida (libjiagu, libSecShell, libDexHelper, libtprt…) Huella directa
lib/*/libpairipcore.so PairIP de Google Play

Barriendo el corpus entero —717 APK, contando los internos de los contenedores— con los patrones de nombre de la última fila, el resultado es 0 APK con huella de packer sobre 717 APK inspeccionados. Coincide con la búsqueda por cadenas de Anti-análisis y hardening, sección 8: el corpus no contiene ninguna muestra con PairIP —un hueco conocido y anotado como tal— y su único protector comercial es DexGuard en Netflix, que no es un packer con stub nativo.

Y la comprobación de assets/ sí da falsos positivos, que conviene ver antes de fiarse:

com.whatsapp.apk    DEX=11 (82.278.164 B)  .so=0  entradas=9206
   ⚑ payload en assets/: ['assets/ReferenceFaceShapeConstants/v01_high_end_face_compressed.bin', …]

Once DEX y ochenta megas de código: no hay packer, son datos de modelos. Estas comprobaciones producen señales, no veredictos, y un informe que las presente como conclusión miente.

6.5 Comprobación 4 — qué hay en nativo

unzip -l app.apk | awk '$4 ~ /^lib\//{print $4}' | sed 's|lib/[^/]*/||' | sort -u

Si la lista incluye bibliotecas con nombre propio de la aplicación —y no solo dependencias reconocibles como libcronet, libsqlite, libc++_shared—, hay lógica en nativo y el análisis a nivel DEX va a tener un agujero. Ver Código nativo y ELF y Análisis binario y estático.

6.6 El veredicto del paso 4

Resultado Qué hacer
Sin ofuscación, sin nativo propio Decompilar: va a salir bien
Nombres muy ofuscados, sin packer Decompilar contando con nombres inventados. Buscar mapping.txt antes
Recursos ofuscados Decompilar, y pasar refactor antes de leer
Lógica en nativo Decompilar el DEX y abrir los .so: el DEX solo no responde
Huella de packer Parar. classes.dex es el stub. Se informa y se acaba

7. Paso 5 — Decompilar

7.1 La cifra que hay que tener presente antes de empezar

Solo el 21,02 % de las aplicaciones de Google Play decompilan enteras, sin un solo método fallido, con jadx. Es la tabla VII del estudio SANER 2021 de Mauthe, Kargén y Shahmehri, y la explicación es aritmética: una aplicación de Play tiene 63.748 métodos de media, y con una tasa de fallo por método del 0,019 % la probabilidad de que ninguno falle es baja aunque el decompilador sea excelente. Combinar los cuatro decompiladores del estudio sube ese 21,02 % al 21,03 %.

Corolario operativo: hay que contar los marcadores de fallo en la salida. Nadie los cuenta por ti, y jadx no devuelve error por métodos fallidos — la decompilación parcial es su comportamiento normal. El desglose, con las tablas del estudio, está en Decompiladores a Java, sección 6.

7.2 Qué ruta según qué pregunta

Pregunta Nivel (§4 de Anatomía) Herramienta
«Quiero leer la lógica» 4 · decompilar jadx
«jadx ha fallado justo en el método que me importa» 4 · decompilar dex2jar + Vineflower o CFR
«Necesito citar lo que hace de verdad» 3 · desensamblar baksmali, o la vista lateral de jadx-gui
«Voy a modificar y reempaquetar» 3 · desensamblar apktool o baksmali → modificación
«Solo quiero los recursos» 2 · decodificar aapt2 dump, apktool -s, APKEditor
«Solo quiero los ficheros» 1 · desempaquetar unzip

Los cuatro niveles no son etapas de una escalera: son fuentes de información distintas, y el análisis serio las cruza. El nivel 4 solo puede escribir getString(R.string.app_name) porque alguien le ha dado la tabla del nivel 2.

⚠️ sin verificar: los tiempos de la escala de la sección 1 son órdenes de magnitud observados en esta máquina sobre el corpus, no una medición sistemática.

7.3 Las recetas de jadx

⚠️ No ejecutado localmente: jadx no está instalado en la máquina de referencia y estas reglas prohíben instalarlo. La sintaxis procede del README oficial del proyecto, consultado el 12 de agosto de 2026. No se ha comprobado ninguna salida.

# exploración: solo código, sin recursos — mucho más rápido
jadx -d salida/ --no-res app.apk

# análisis serio: con los métodos incoherentes visibles para poder auditarlos
jadx -d salida/ --show-bad-code app.apk

# con mapping.txt real, que es lo único que recupera nombres de verdad
jadx -d salida/ --mappings-path mapping.txt app.apk

# aplicación grande: subir el heap y bajar los hilos
JAVA_OPTS="-Xmx8g" jadx -j 4 -d salida/ app.apk

--deobf y --mappings-path no son lo mismo, y confundirlos produce informes falsos. --deobf es heurístico —cambia a.b.c por algo pronunciable y no recupera nada— y está desactivado por defecto en la CLI; --mappings-path es determinista y devuelve los nombres reales (Decompiladores a Java, sección 3.6).

7.4 Medir la cobertura, que es el paso que nadie da

Después de decompilar y antes de leer hay que contar cuántos métodos quedaron fuera. Los marcadores están verificados contra el código fuente de jadx (commit e738a26, 5 de agosto de 2026); el catálogo completo con su significado está en Diagnóstico de fallos, sección 7.

grep -rc "Code decompiled incorrectly" salida/sources/ | awk -F: '{s+=$2} END{print s" incoherentes"}'
grep -rc "JADX ERROR:"                  salida/sources/ | awk -F: '{s+=$2} END{print s" errores duros"}'
grep -rc "Method not decompiled:"       salida/sources/ | awk -F: '{s+=$2} END{print s" no decompilados"}'

Y el código de salida sirve para esto y solo para esto: 3 significa «decompiló, pero con errores», con el mensaje finished with errors, count: N. Un 0 no garantiza cobertura completa —los avisos no cuentan como error—, pero un 3 garantiza que hay huecos.

La distinción que el informe tiene que preservar: JADX ERROR: es un hueco visible —el método sale como volcado de instrucciones y se ve que falta—, mientras que Code decompiled incorrectly marca Java plausible que puede no corresponder al comportamiento real. Tratarlos como la misma categoría desperdicia justo la información útil.

⚠️ Corrección respecto a otra parte del corpus: la sección 3.5 de Decompiladores a Java cita los marcadores como // decompilation failed y // JADX WARNING: inconsistent code. Ninguna de las dos cadenas existe en el código de jadx; ver la sección 7.1 del documento de diagnóstico para el detalle de qué se emite realmente y desde qué fichero.

8. Paso 6 — Navegar el resultado

Con un árbol de veinte mil ficheros delante, la pregunta no es qué leer sino por dónde empezar. Cuatro anclas, en orden.

1 · Los componentes del manifiesto son los puntos de entrada reales. La lista del paso 2 (§4.3) es literalmente el conjunto de clases por las que el sistema entra en la aplicación: onCreate de la Activity lanzable, onReceive de cada receiver exportado, onStartCommand de cada service, query/insert de cada provider. Todo lo demás cuelga de ahí. Y sobrevive a la ofuscación: el manifiesto tiene que nombrar esas clases para que el sistema las resuelva, así que aunque el resto del código sea a.b.c esos nombres concretos están en claro.

2 · Los strings dan la superficie de red y de almacenamiento.

$BT/dexdump -d classes.dex | grep -oE 'https?://[a-zA-Z0-9./_-]+' | sort -u | head

Si no sale casi nada y la aplicación evidentemente habla por red, los strings están cifrados: señal de ofuscador comercial, y remite a Ofuscadores comerciales.

3 · Las llamadas al framework no se pueden renombrar. Landroid/... lo define el sistema, no la aplicación. Buscar Landroid/telephony/TelephonyManager, Ljavax/crypto/Cipher, Ldalvik/system/DexClassLoader o Ljava/lang/Runtime;->exec localiza el comportamiento sensible en código completamente ofuscado. Es el suelo del análisis.

4 · Los recursos nombran lo que el código ya no nombra. Un layout se llama activity_login.xml aunque la clase que lo infla se llame f.a. La tabla de recursos y los AXML de res/ son un diccionario semántico gratis — y por eso la ofuscación de recursos (§6.3) existe: justamente para quitarlo.

9. El árbol de decisión completo

              fichero de entrada
                       ▼
 PASO 0 ┌── detección estructural (§2.2) ──────────────────────┐
        │ APK ──┐  XAPK/APKM/SET/SPLITS ─┐  AAB ──┐  ¿? ──┐    │
        └───────┼────────────┬───────────┼────────┼───────┼────┘
                │            ▼           │        ▼       ▼
                │   extraer + hallar el  │   bundletool  PARAR
                │   base (§2.4) ─────────┘   build-apks  e informar
                ▼◀───────────┘
 PASO 1 ── inventario del Central Directory (§3) ──────────────
        │  ¿resources.arsc? ¿stored? ¿nº DEX? ¿ABIs?
        ▼
 PASO 2 ── aapt2 dump badging + dump xmltree (§4) ─────────────
        │  paquete · versiones · SDK · permisos
        │  componentes exportados · flags de seguridad
        ▼
 PASO 3 ── apksigner verify --print-certs (§5) ────────────────
        │  CON el minSdkVersion leído en el paso 2
        ▼
 PASO 4 ── nombres · recursos · forma del DEX · nativo (§6) ───
        │        │                  │
        │packer  │ofuscado          │limpio
        ▼        ▼                  │
   PARAR:     buscar                │
   informar   mapping.txt ──────────┤
   y no                             ▼
   decompilar          ┌────────────────────────────────────┐
                PASO 5 │ ¿qué pregunta hay que responder?   │
                       │  leer lógica ──────▶ jadx          │
                       │  citar comportamiento ▶ smali      │
                       │  modificar ────────▶ doc. 02       │
                       │  solo recursos ────▶ aapt2 dump    │
                       └──────────────┬─────────────────────┘
                                      ▼
                  medir la cobertura (§7.4) ─¿baja?─▶ 2.º decompilador
                                      ▼               sobre esos métodos
                PASO 6 · entrar por los componentes del manifiesto;
                         strings · Landroid/* · recursos (§8)

10. Checklist ejecutable

De arriba abajo. Cada línea o produce un dato del informe o corta el análisis.

#!/usr/bin/env bash
set -u
BT=~/Library/Android/sdk/build-tools/37.0.0
APK="$1"

python3 sniff.py "$APK" || exit 2        # 0 · si no es un APK suelto: extraer y repetir
python3 inventario.py "$APK"             # 1 · categorías, ABIs, DEX, resources.arsc

# 2 · manifiesto — y guardar el minSdk, que hace falta en el paso 3
MIN=$($BT/aapt2 dump badging "$APK" | sed -n "s/^minSdkVersion:'\(.*\)'/\1/p")
$BT/aapt2 dump badging "$APK" | grep -E "^(package|targetSdkVersion|uses-permission)"
$BT/aapt2 dump xmltree "$APK" --file AndroidManifest.xml > /tmp/tree.txt
grep -E "debuggable|usesCleartextTraffic|networkSecurityConfig" /tmp/tree.txt
python3 exportados.py < /tmp/tree.txt

# 3 · firma, CON el minSdk real, y el inventario físico de v1 aparte
$BT/apksigner verify --verbose --print-certs --min-sdk-version "${MIN:-21}" "$APK" \
  | grep -E "^(Verifies|DOES NOT|Verified using|ERROR)|SHA-256 digest"
unzip -l "$APK" | grep -E "META-INF/(MANIFEST\.MF|[A-Z0-9_-]+\.(SF|RSA))" \
  || echo "  (sin firma v1 física)"

# 3b · alineación: también es procedencia
$BT/zipalign -c 4       "$APK" >/dev/null 2>&1 && echo "align 4   OK" || echo "align 4   FALLA"
$BT/zipalign -c -P 16 4 "$APK" >/dev/null 2>&1 && echo "align 16K OK" || echo "align 16K FALLA"

# 4 · protecciones
python3 ofusc.py "$APK"; python3 huellas.py "$APK"
$BT/aapt2 dump resources "$APK" 2>/dev/null | grep -m3 "(file) res/"
unzip -l "$APK" | awk '$4 ~ /^lib\//{print $4}' | sed 's|lib/[^/]*/||' | sort -u

# 5 · solo si los pasos 0-4 no han respondido:
#     jadx -d salida/ --show-bad-code "$APK"   ⚠ no ejecutado: jadx no instalado
#     y contar los marcadores de §7.4 ANTES de leer nada
# 6 · entrar por los componentes exportados del paso 2

Fuentes

Todas las URL se consultaron el 12 de agosto de 2026.

  1. aapt2 — https://developer.android.com/tools/aapt2 · lista de subcomandos de dump y opciones --file y --no-values (§4.1, §6.3, §10).
  2. apksigner — https://developer.android.com/tools/apksigner · sintaxis de verify, --print-certs y --min-sdk-version (§5).
  3. zipalign — https://developer.android.com/tools/zipalign · -c, -P 16 y la regla de orden respecto a apksigner (§10, paso 3b).
  4. <application> — https://developer.android.com/guide/topics/manifest/application-element · valores por defecto de android:debuggable, allowBackup y extractNativeLibs (§4.2).
  5. <activity> — https://developer.android.com/guide/topics/manifest/activity-element · la regla del valor implícito de android:exported y su obligatoriedad desde targetSdk 31 (§4.3).
  6. Network security configuration — https://developer.android.com/privacy-and-security/security-config · comportamiento por defecto de usesCleartextTraffic desde targetSdk 28 y efecto de confiar en el almacén del usuario (§4.2).
  7. A Large-Scale Empirical Study of Android App Decompilation — Mauthe, Kargén y Shahmehri, SANER 2021 · https://www.ida.liu.se/~ulfka17/papers/SANER2021.pdf · el 21,02 % de aplicaciones de Play que decompilan enteras, los 63.748 métodos de media y el 0,019 % de fallo por método (§7.1).
  8. jadx — README oficial — https://github.com/skylot/jadx#readme · la sintaxis de --no-res, --show-bad-code, --mappings-path, --deobf, -j y JAVA_OPTS (§7.3).
  9. jadx — JadxCLI.java y MethodGen.java, commit e738a26 del 5 de agosto de 2026 — https://github.com/skylot/jadx/blob/master/jadx-cli/src/main/java/jadx/cli/JadxCLI.java y https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java · los literales de §7.4 y el código de salida 3 con finished with errors, count: {}. Repositorio clonado y leído directamente, no vía buscador.
  10. Ejecuciones locales sobre el corpus — 12 de agosto de 2026, build-tools 37.0.0 (aapt2 2.20-15087165, apksigner 0.9, zipalign, dexdump), OpenJDK 21.0.10 y Python 3.12.10 sobre macOS Darwin 25.5.0. De aquí salen todas las salidas pegadas de §2.2, §3.1, §4.1, §4.3, §5.1, §5.2, §6.2, §6.3 y §6.4, contra com.aurora.store_75.apk, org.fossify.calculator_1.4.0.apk, org.fdroid.fdroid_1023052.apk, com.x8bit.bitwarden_2026.5.0.apk, GoogleKeep_com.google.android.keep.xapk, com.netflix.mediaclient.sai.zip, tv.twitch.android.app.apkm y los contenedores de splits/.
  11. Corpus de verificación — 5,9 GB, 86 contenedores y 717 APK —58 sueltos de F-Droid y 659 dentro de contenedores de splits—, reunido el 12 de agosto de 2026 · el barrido de detección estructural de §2.2 (14 .apks que son .xapk), la métrica de §6.2 y el barrido de huellas de packer de §6.4 (0 sobre 717 APK).