Pipeline de análisis

Antes conviene leer «Anatomía de un APK» · «Glosario»

referencia técnica · Actualizado el 25 de septiembre de 2026

1. Qué es y por qué existe

Los demás documentos 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
  ~15 ms     ~25 ms     ~0,2 s    ~0,2 s     ~0,2 s   10 s-10 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 · Qué sobrevive siempre a la ofuscación»). 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 que no se han reproducido llevan aviso explícito en su sección.

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 «muestrario», 14 de los 16 ficheros con extensión .apks no son un APK Set, son .xapk de APKCombo con la extensión cambiada. La tabla que diferencia todos los formatos —qué es cada uno, cómo se reconoce y qué se puede hacer con él— está en «Formatos y extensiones»; la estructura interna del AAB y del APK Set, en «AAB, splits y contenedores → §6 · Detección por estructura, nunca por extensión».

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 muestrario.

             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 muestrario, 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 · Encontrar el base dentro de un contenedor») 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 · Las categorías», 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 muestrario:

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 («§10.1 de Contenedor ZIP · resources.arsc sin comprimir y alineado a 4 bytes»)
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 · Comprobación 3 — la forma del DEX»)
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 · Categoría por categoría»).

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 $MUESTRAS/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 del <application> badging solo resume la etiqueta, el icono y las marcas testOnly, application-isGame y application-debuggable.

$BT/aapt2 dump xmltree $MUESTRAS/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 muestrario, 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 · La trampa: verify no informa de los esquemas presentes»)
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 · Componentes exportados, con la regla del valor implícito»)
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 muestrario 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 muestrario (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 muestrario 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 → §9 · El volcador»). 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 9 de APK Signing Block · El volcador» lo hace, y su «sección 11 · El bloque desconocido de Google Keep: 0x2146444e» documenta qué contiene.

Aparece en 25 de los 717 APK del muestrario, todos base.apk de aplicaciones de Play — incluidos los que van dentro de los .apkm de APKMirror, lo que demuestra que ese sitio redistribuye binarios de Play sin recompilarlos. Ninguno de F-Droid lo lleva, pero tampoco los split de esas mismas aplicaciones ni el APK suelto que llegó por APKPure: su presencia indica Play; su ausencia no indica nada.

El esquema .proto del frosting no es público, pero su estructura exterior se ha reproducido sobre los tres base.apk de Play de las muestras locales: la firma ECDSA del bloque verifica con la clave pública de Play, dos campos del protobuf coinciden con el versionCode y el minSdkVersion del manifiesto, y el campo 4, que se ha leído como marca de tiempo de la firma, no puede serlo. El detalle está en la «sección 11 de APK Signing Block · El bloque desconocido de Google Keep: 0x2146444e». 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_FAILED_INVALID_APK: … signatures are inconsistent con install-multiple o cualquier otra sesión de instalación, catalogado en «Diagnóstico de fallos → §2.3 · Los cinco que hay que conocer al detalle»).

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 documentació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 muestrario 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 muestrario: 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.

⚠️ sin verificar: esta métrica no reproduce el gradiente de ofuscación que se tomó como referencia (Twitch 99,9 %, Shazam 100 %, WhatsApp 50 %, «las de Google entre 0,1 % y 2,5 %»), y esa referencia no tiene fuente: está en el documento, sin cita, desde su primera versión en el repositorio. La parte de Google depende del instrumento: con el umbral de dos caracteres, Keep da un 2,3 %, dentro de ese rango, y es la mediana —o el umbral de tres— lo que la coloca entre las más renombradas. Twitch y WhatsApp no casan con ningún umbral de la tabla, y ni ellas ni Shazam siguen en la máquina de referencia para volver a medirlas. La «sección 10 de R8 y ProGuard · Medición propia: la fracción de identificadores cortos» llega a la misma conclusión midiendo sobre identificadores definidos en lugar de sobre 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 $MUESTRAS/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 muestrario 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 muestrario 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. Es coherente con un hueco conocido del material: el muestrario no contiene ninguna muestra de PairIP, 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, que lo atribuye, «probablemente en parte», a que una aplicación de Play tiene 63.748 métodos de media. La aritmética sola no lo explica: con una tasa de fallo por método del 0,019 % y fallos independientes, la probabilidad de que ninguno falle sería del 0,0006 %, no del 21 %. Los fallos se concentran en unas aplicaciones y no en otras. 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 → §6 · La comparación honesta: qué dicen las mediciones».

7.2 Qué ruta según qué pregunta

Pregunta Nivel («§4 de Anatomía · Los cuatro niveles de “decompilar”») 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.

Los tiempos de la escala de la «sección 1 · Qué es y por qué existe» están medidos sobre las 61 muestras del «muestrario» que siguen en la máquina de referencia, con las órdenes de la checklist de la «sección 10 · Checklist ejecutable» y una pasada por muestra (25 de septiembre de 2026). Medianas: 14 ms para identificar, 25 ms para inventariar, 0,24 s para el manifiesto, 0,23 s para la firma y 0,18 s para las protecciones, contando la métrica de nombres y la forma del DEX; ningún paso de 0 a 4 llegó a 3 s en ninguna muestra. Decompilar con jadx --show-bad-code llevó 15,6 s de mediana, más de un minuto en 8 de las 61 y 9 min 43 s en la peor, SimpleX Chat. Las aplicaciones grandes de Play no están en local y no entran en estas cifras.

7.3 Las recetas de jadx

⚠️ Salida no reproducida en esta sección. La sintaxis procede del README oficial del proyecto, consultado el 12 de agosto de 2026, y no se ha pegado aquí ninguna salida real. Sí hay recetas de jadx ejecutadas con la versión 1.5.6, con tiempos y códigos de salida, en «Decompiladores a Java → §7 · Recetas de jadx CLI».

# 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.
# OJO al -P: sin él no renombra nada y no avisa. Ver más abajo.
jadx -d salida/ -Prename-mappings.invert=yes --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 → §3.6 · --deobf y mapping.txt: dos cosas distintas»).

⚠️ Y hay un segundo modo de producir el mismo informe falso, más traicionero: pasar el mapping.txt sin invertirlo. R8 lo escribe en el sentido original -> ofuscado, y jadx aplica el mapeo de izquierda a derecha: busca en el DEX clases llamadas com.ejemplo.Real, que no existen, y no renombra nada. Sale con exit=0 y sin un solo aviso — igual que si el fichero no existiera, caso en el que también responde done y 0. El interruptor es una opción de plugin que no aparece en la lista principal de --help:

-Prename-mappings.invert=yes    # default: no

Reproducido con jadx 1.5.6 sobre un DEX con la clase a.b y un mapping.txt con com.ejemplo.Real -> a.b:. Sin el -P sale sources/a/b.java; con él, sources/com/ejemplo/Real.java, encabezado por /* JADX INFO: renamed from: a.b */. La ayuda de la herramienta refuerza la trampa: --mappings-path declara aceptar «Tiny y Tiny v2, Enigma o directorio Enigma», sin mencionar el formato de ProGuard y R8, que sí parsea.

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 → §6 · jadx».

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 de esta documentación: la «sección 3.5 de Decompiladores a Java · Los modos de fallo, y qué significa cada uno» 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 6.1 de Diagnóstico de fallos · Corrección previa: dos cadenas que circulan y no existen» 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 · Componentes exportados, con la regla del valor implícito») 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 "$APK" | grep -oE 'https?://[a-zA-Z0-9./_-]+' | sort -u | head

Sobre el APK y no sobre classes.dex: así dexdump recorre todos los classes*.dex. Con solo el primero, en una aplicación multidex faltan cadenas y se concluye en falso lo del párrafo siguiente.

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 · Comprobación 2 — la ofuscación de recursos») 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"

# 0 · qué es: sin AndroidManifest.xml en la raíz no es un APK suelto; clasificar el
#     contenedor con el algoritmo de §2.2, sacar el base (§2.4) y repetir con él
unzip -Z1 "$APK" | grep -qx AndroidManifest.xml || { echo "no es un APK suelto: §2.2"; exit 2; }

# 1 · inventario (§3): categorías, DEX, ABIs y cómo va resources.arsc
unzip -Z1 "$APK" | cut -d/ -f1 | sort | uniq -c | sort -rn
unzip -Z1 "$APK" | grep -cE '^classes[0-9]*\.dex$'
unzip -Z1 "$APK" | sed -n 's|^lib/\([^/]*\)/.*|\1|p' | sort -u
unzip -Zv "$APK" resources.arsc | grep -E 'compression method|offset of local header'

# 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
#   componentes exportados: recorrer /tmp/tree.txt con la regla del valor implícito de §4.3

# 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" 2>&1 \
  | grep -E "^(Verifies|DOES NOT|Verified using|ERROR)|SHA-256 digest"
#   el 2>&1 hace falta: DOES NOT VERIFY y los ERROR salen por stderr
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: la métrica de nombres (§6.2) y la forma del DEX (§6.4) no tienen orden
#     propia; lo que sí la tiene es la ofuscación de recursos (§6.3) y lo nativo (§6.5).
#     El veredicto, con la tabla de §6.6
$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"   (ejecutado con jadx 1.5.6: 30-herramientas/01, §7)
#     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 · Paso 3 — Verificar la firma»).
  3. zipalign — https://developer.android.com/tools/zipalign · -c, -P 16 y la regla de orden respecto a apksigner («§10 · Checklist ejecutable», paso 3b).
  4. <application> — https://developer.android.com/guide/topics/manifest/application-element · valores por defecto de android:debuggable, allowBackup y extractNativeLibs («§4.2 · Los diez campos que se miran»).
  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 · Componentes exportados, con la regla del valor implícito»).
  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 · Los diez campos que se miran»).
  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 · La cifra que hay que tener presente antes de empezar»).
  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 · Las recetas de jadx»).
  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 · Medir la cobertura, que es el paso que nadie da» 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 muestrario — 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. aapt2 — DumpManifest.cpp, etiqueta android-16.0.0_r1 — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-16.0.0_r1/tools/aapt2/dump/DumpManifest.cpp · Consultado el 25 de septiembre de 2026. De aquí sale lo que badging imprime del <application>: application-label, application-icon, application:, testOnly, application-isGame y application-debuggable («sección 4 · Paso 2 — Leer el manifiesto»).