Pipeline de análisis
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,zipalignydexdumpestá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 elAPK Signing Block(APK Signing Block, sección 9). Para saber si se instalará, se ejecutaverifycon elminSdkVersionreal 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 |
| 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
- Mediana ≤ 3: renombrado por
R8o equivalente. El código sale comoa.b.cy sinmapping.txtno vuelve. Enlaza a R8 y ProGuard. - 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 failedy// 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.
- aapt2 — https://developer.android.com/tools/aapt2 · lista de subcomandos de
dumpy opciones--filey--no-values(§4.1, §6.3, §10). - apksigner — https://developer.android.com/tools/apksigner · sintaxis de
verify,--print-certsy--min-sdk-version(§5). - zipalign — https://developer.android.com/tools/zipalign ·
-c,-P 16y la regla de orden respecto aapksigner(§10, paso 3b). <application>— https://developer.android.com/guide/topics/manifest/application-element · valores por defecto deandroid:debuggable,allowBackupyextractNativeLibs(§4.2).<activity>— https://developer.android.com/guide/topics/manifest/activity-element · la regla del valor implícito deandroid:exportedy su obligatoriedad desdetargetSdk31 (§4.3).- Network security configuration —
https://developer.android.com/privacy-and-security/security-config · comportamiento por defecto
de
usesCleartextTrafficdesdetargetSdk28 y efecto de confiar en el almacén del usuario (§4.2). - 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).
- jadx — README oficial — https://github.com/skylot/jadx#readme · la sintaxis de
--no-res,--show-bad-code,--mappings-path,--deobf,-jyJAVA_OPTS(§7.3). - jadx —
JadxCLI.javayMethodGen.java, commite738a26del 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 confinished with errors, count: {}. Repositorio clonado y leído directamente, no vía buscador. - Ejecuciones locales sobre el corpus — 12 de agosto de 2026,
build-tools37.0.0 (aapt22.20-15087165,apksigner0.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, contracom.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.apkmy los contenedores desplits/. - 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.apksque son.xapk), la métrica de §6.2 y el barrido de huellas de packer de §6.4 (0 sobre 717APK).