Anatomía de un APK
1. Qué es un APK
Un APK (Android Package) es un fichero ZIP con una convención encima. No es un
formato nuevo, no es un ZIP «modificado» y no lleva cifrado: cualquier unzip lo abre, y lo
que sale son ficheros normales colocados en rutas concretas. Toda la especificidad de Android
está en qué entradas tiene que haber y en qué formato está el contenido de cada una.
Este documento introduce el vocabulario del corpus. Cada término técnico se explica aquí en una frase; la definición completa de todos ellos está en el glosario, que se puede leer en paralelo.
Cuatro condiciones lo convierten en instalable. Ninguna es opcional:
| Condición | Dónde vive | Qué pasa si falta |
|---|---|---|
Un AndroidManifest.xml en la raíz, en formato binario AXML |
raíz del ZIP | El instalador no sabe ni cómo se llama el paquete. INSTALL_PARSE_FAILED_NO_CERTIFICATES y familia |
Código ejecutable en classes.dex (y classes2.dex, classes3.dex…), en formato DEX |
raíz del ZIP | La aplicación instala pero no arranca, salvo que no tenga código |
Una tabla de recursos resources.arsc, si se usa algún recurso |
raíz del ZIP | Los identificadores del DEX no resuelven a nada |
| Una firma válida con un esquema que la versión de Android acepte | META-INF/ y/o el APK Signing Block |
El sistema rechaza el paquete antes de mirar nada más |
Las dos palabras que hay que retener de esa tabla son binario y convención.
Binario, porque el manifiesto y los recursos no son texto. Descomprimir un APK y hacer
cat AndroidManifest.xml devuelve basura: los primeros bytes son 03 00 08 00, la cabecera
de un chunk RES_XML_TYPE, no <?xml. Lo mismo con cualquier .xml bajo res/. La
conversión a texto legible es una reconstrucción, y es el nivel 2 de la sección 4.
Convención, porque nada en el formato ZIP obliga a que exista classes.dex. Lo obliga el
cargador de Android. Esa distinción importa mucho al analizar: un ZIP puede tener dos entradas
con el mismo nombre, o entradas cuya cabecera local contradiga al índice del final, y el
formato lo permite aunque Android no lo tolere. La historia de las dos vulnerabilidades más
conocidas de Android —Master Key y Janus— es exactamente la historia de dos implementaciones
que leyeron el mismo ZIP y no coincidieron; está contada en
Contenedor ZIP, sección 12.
Lo que un APK no es, y conviene fijarlo pronto:
- No es un
AAB. El Android App Bundle es un formato de publicación, no de instalación, y por dentro usa Protocol Buffers en vez de los formatos binarios de Android. Ver la sección 2.8 y AAB, splits y contenedores. - No es necesariamente un fichero. Desde el App Bundle, una aplicación instalada suele ser
un
base.apkmás variossplit. Lo que se descarga de un sitio de terceros con extensión.xapk,.apkso.apkmes un ZIP que contiene varios APK. - No contiene el
.odex, el.vdexni el.oat. Esos los generadex2oaten el dispositivo al instalar. Ver Artefactos auxiliares, sección 5.
2. El ciclo de build
2.1 El diagrama
Tres cadenas independientes que convergen en un ZIP, y dos pasos finales sobre el ZIP ya formado. El orden de esos dos últimos pasos —alinear y después firmar— es la fuente número uno de APK rotos.
RECURSOS CÓDIGO JVM CÓDIGO NATIVO
──────── ────────── ─────────────
res/ .kt .java .c .cpp
AndroidManifest.xml │ │
│ ▼ ▼
│ kotlinc / javac CMake / ndk-build
▼ │ │
aapt2 compile ▼ ▼
│ .class lib/<abi>/*.so
▼ │ │
*.flat ▼ │
│ D8 o R8 │
▼ │ └── mapping.txt │
aapt2 link ──── R.java ────────────┤ │
│ -I android.jar ▼ │
▼ classes*.dex │
AndroidManifest.xml (AXML) │ │
resources.arsc │ │
res/** ya compilados │ │
│ │ │
└─────────────────┬───────────────┴────────────────────────────┘
▼
empaquetado ZIP assets/ META-INF/ baseline.prof
│
▼
zipalign -P 16 4
│
▼
apksigner sign ← v1, v2, v3, v3.1 (+ .idsig para v4)
│
▼
APK final
Quien lo lea de derecha a izquierda tiene el mapa de la ingeniería inversa: cada flecha es una transformación, y la sección 4 clasifica cuáles se pueden deshacer y cuáles no.
2.2 Los recursos: aapt2 compile y aapt2 link
aapt2 (Android Asset Packaging Tool) es la herramienta oficial que convierte res/ y
AndroidManifest.xml en los formatos binarios del dispositivo. Trabaja en dos fases
separadas, y esa separación es toda la diferencia con el aapt original:
«AAPT2 supports faster compilation of resources by enabling incremental compilation. To accomplish incremental compilation, resource processing is separated into two steps: Compile: compiles resource files into binary formats. Link: merges all compiled files and packages them to a single package.»
aapt2 compile toma un fichero de recurso por invocación y produce un intermedio con
extensión .flat. Lo de res/values/ sale como *.arsc.flat; todo lo demás, como XML
binario *.flat; y los .png se pasan además por el crunch salvo que se le diga
--no-crunch.
aapt2 link funde todos los .flat, resuelve las referencias contra android.jar (la
opción -I) y emite tres cosas: la tabla resources.arsc, el AndroidManifest.xml ya en
AXML, y —fuera del APK— el fichero R.java con las constantes que el compilador de Java va
a necesitar. La documentación es explícita sobre lo que no produce: «the generated APK does
not contain DEX bytecode and is unsigned. You can't deploy this APK to a device». Al final de
esta rama hay un APK a medio hacer.
2.3 El código: javac/kotlinc, y luego D8 o R8
El compilador de Kotlin o de Java produce bytecode de la JVM en ficheros .class, uno por
clase, big-endian y con una tabla de constantes propia por fichero. Android no ejecuta eso.
D8 convierte ese bytecode a DEX: un solo fichero con todas las clases y las tablas de
cadenas, tipos, prototipos, campos y métodos compartidas. El porqué de ese cambio de reparto
está en Formato DEX, sección 1.
R8 es D8 más el trabajo que antes hacía ProGuard, en una sola pasada. La documentación
oficial lo divide en tres operaciones: code shrinking (tree shaking) —partiendo de los
puntos de entrada declarados en el manifiesto, construye el grafo de código alcanzable y borra
el resto—, logical optimizations —inlining de métodos y fusión de clases— y
obfuscation (minification), que acorta los nombres de clases, campos y métodos:
«com.example.MyActivity could become a.b.a».
De ahí sale mapping.txt, el fichero con la correspondencia entre los
nombres originales y los ofuscados. Sin él, la ofuscación es irreversible en la práctica; con
él, es reversible de forma determinista — es lo que hace retrace. Ver
R8 y ProGuard.
Dos cambios recientes que conviene tener presentes porque la documentación antigua circula
mucho: desde AGP 8.0 el modo full mode de R8 está activo por defecto, y desde AGP
9.3 la configuración se declara en un bloque optimization { enable = true } en vez de con
isMinifyEnabled / isShrinkResources, con las reglas keep en ficheros .keep del
source set src/<variant>/keepRules. El DSL antiguo sigue soportado.
2.4 El código nativo
Lo que se compila con el NDK no pasa por ninguna de las dos ramas anteriores: sale como
bibliotecas compartidas ELF y se copia a lib/<abi>/ dentro del ZIP, un directorio por
arquitectura. Es la única parte del APK que no es un formato propio de Android. Ver
Código nativo y ELF.
2.5 Empaquetado, alineación y firma
Los tres productos se meten en un ZIP junto con assets/, los perfiles de arranque y los
metadatos de build, y sobre ese ZIP ya cerrado actúan dos herramientas más.
zipalign no reordena nada ni recomprime: ajusta el relleno para que los datos de las
entradas sin comprimir empiecen en un offset alineado, y así el sistema pueda mapearlas
con mmap(2) en vez de copiarlas a RAM. Dos alineaciones distintas y ambas necesarias: 4
bytes para toda entrada stored, y 16 KiB (-P 16) para los .so.
apksigner firma. Y aquí está la regla que hay que memorizar, en palabras de la
documentación oficial:
«If you use
apksigner,zipalignmust be used before the APK file has been signed. If you sign your APK usingapksignerand make further changes to the APK, its signature is invalidated.»
El motivo es estructural: desde el esquema v2 la firma cubre todos los bytes del fichero, no las entradas del ZIP. Mover un byte después de firmar la invalida. Ver Alineación y zipalign y Esquemas v1, v2 y v3.
2.6 El ciclo entero, ejecutado
Todo lo anterior se puede hacer sin Gradle y sin Android Studio, con las build-tools que ya
están en el SDK. Lo siguiente está ejecutado el 12 de agosto de 2026 con build-tools 37.0.0
(aapt2 2.20-15087165, D8 9.2.4-dev, apksigner 0.9), javac 21.0.10 y
platforms/android-35, sobre un proyecto mínimo de una Activity y un string.
BT=~/Library/Android/sdk/build-tools/37.0.0
AJ=~/Library/Android/sdk/platforms/android-35/android.jar
# 1-2 · recursos: compilar y enlazar (produce AXML + resources.arsc + R.java)
$BT/aapt2 compile --dir src/res -o out/res.zip
$BT/aapt2 link -o out/base.apk -I $AJ --manifest src/AndroidManifest.xml \
--java out/gen --min-sdk-version 24 --target-sdk-version 35 out/res.zip
# 3-4 · código: javac produce bytecode de la JVM, D8 lo convierte a DEX
javac --release 17 -cp $AJ -d out/classes src/java/MainActivity.java \
out/gen/com/ejemplo/demo/R.java
$BT/d8 --release --min-api 24 --lib $AJ --output out/ out/classes/com/ejemplo/demo/*.class
# 5 · empaquetar: el DEX entra en el APK que dejó aapt2
cp out/base.apk out/app-unaligned.apk
(cd out && zip -q -X app-unaligned.apk classes.dex)
# 6 · alinear · 7 · firmar
$BT/zipalign -P 16 -f -v 4 out/app-unaligned.apk out/app-aligned.apk
keytool -genkeypair -keystore out/demo.jks -storepass pruebas123 -keypass pruebas123 \
-alias demo -keyalg RSA -keysize 2048 -validity 30 -dname "CN=Ejemplo Demo, C=ES"
cp out/app-aligned.apk out/app-release.apk
$BT/apksigner sign --ks out/demo.jks --ks-pass pass:pruebas123 \
--key-pass pass:pruebas123 --ks-key-alias demo out/app-release.apk
# 8 · verificar
$BT/apksigner verify -v --v4-signature-file out/app-release.apk.idsig out/app-release.apk
De aapt2 compile sale un solo intermedio, values_strings.arsc.flat; de aapt2 link, un
APK con dos entradas (AndroidManifest.xml de 1.224 bytes y resources.arsc de 564) más el
R.java, donde ya se ve la estructura 0xPPTTEEEE del resource ID — paquete 7f, tipo
01, entrada 0000:
public final class R {
public static final class string { public static final int app_name=0x7f010000; }
}
dexdump -f sobre el classes.dex recién producido responde DEX version '037'. No es
arbitrario: la versión la elige D8 a partir de --min-api 24, y la correspondencia completa
está medida en Formato DEX, sección 4. Y la verificación
final:
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v3.2 scheme (APK Signature Scheme v3.2): false
Verified using v4 scheme (APK Signature Scheme v4): true
Tres observaciones sobre esa salida, las tres del tipo que se aprende tarde:
apksignergeneró un.idsigsin pedírselo. La firma v4 vive en un fichero acompañante, fuera del APK, yapksigner verifyno la mira salvo que se le pase--v4-signature-file.v1: falseno significa que no haya firma v1. El APK resultante contieneMETA-INF/MANIFEST.MF,META-INF/DEMO.SFyMETA-INF/DEMO.RSA. Lo que diceapksigneres que, conminSdkVersion24, la verificación no necesita v1: repitiendo la orden con--min-sdk-version 21respondev1: true.- El APK alineado pesa 2.293 bytes y el firmado 8.587. La firma no es un adorno.
El resultado tiene seis entradas —AndroidManifest.xml, resources.arsc, classes.dex y los
tres de META-INF/— y es exactamente la anatomía mínima de la sección 1.
2.7 Los nombres de las tareas de Gradle
En un proyecto real nada de lo anterior se invoca a mano: lo orquesta el Android Gradle Plugin, cuya versión estable el 12 de agosto de 2026 es 9.3.1. Los nombres de tarea documentados son tres, y los tres llevan el nombre de la variante en camel case:
| Tarea | Produce |
|---|---|
assemble<Variante> — assembleDebug, assembleRelease |
Un APK en <módulo>/build/outputs/apk/ |
install<Variante> — installDebug |
Lo mismo, y lo instala en el dispositivo conectado |
bundle<Variante> — bundleRelease, :base:bundleDebug |
Un AAB |
Cada una de ellas dispara por debajo una cadena de tareas internas —compilación de Kotlin,
fusión de recursos, invocación de R8, empaquetado— cuyos nombres no forman parte del contrato
público y han cambiado varias veces entre versiones de AGP. Este documento no los cita a
propósito: ./gradlew tasks los lista para la versión que se tenga delante, que es la única
respuesta que no envejece. La build de depuración se firma sola con una clave de depuración y
sale ya alineada; la de release exige clave propia.
2.8 Dónde encaja el AAB
Desde agosto de 2018 existe el Android App Bundle, y desde agosto de 2021 es obligatorio para las aplicaciones nuevas de Google Play. Cambia quién genera el fichero instalable:
«An Android App Bundle is a publishing format that includes all your app's compiled code and resources, and defers APK generation and signing to Google Play.»
El desarrollador sube un .aab. Play lo procesa y genera del lado del servidor el conjunto
de APK que cada dispositivo concreto necesita: un base.apk más los split de su ABI, su
densidad y su idioma. El resto ni se descarga.
desarrollador Google Play dispositivo
┌────────────────┐ ┌──────────────────┐ ┌────────────────┐
│ bundleRelease │ │ generación de │ │ base.apk │
│ │ │ │ splits + firma │ │ + config.arm64 │
│ ▼ │ .aab │ con la clave │ splits │ + config.xxhdpi│
│ app.aab │ ───────► │ de Play App │ ───────► │ + config.es │
│ (protobuf) │ │ Signing │ │ │
└────────────────┘ └──────────────────┘ └────────────────┘
Dos consecuencias que atraviesan todo el corpus:
- La firma que ve el usuario no es la del desarrollador. Con Play App Signing, Play re-firma cada APK generado. La clave que el desarrollador guarda es la de upload.
- Una aplicación instalada ya no es un fichero. Analizarla exige o bien recoger todos sus
splits del dispositivo, o bien fusionarlos en un APK único, que es una operación no
trivial: hay que unir tablas de recursos, sanear el manifiesto y renumerar los
DEX. Ver Fusión de splits.
La misma generación se puede reproducir en local con bundletool, que es la herramienta que
usan Android Studio, AGP y el propio Play. Sobre el .aab de prueba del corpus:
java -jar ~/herramientas/bundletool.jar \
build-apks --bundle=$CORPUS/splits/com.ejemplo.fixture.aab --output=fixture.apks
unzip -l fixture.apks | head -8
3166 toc.pb
25551 splits/base-arm64_v8a.apk
25551 splits/base-arm64_v8a_2.apk
23037 splits/base-armeabi_v7a.apk
1283 splits/base-de.apk
1283 splits/base-es.apk
1287 splits/base-fr.apk
1484 splits/base-hdpi.apk
toc.pb es la tabla de contenidos en Protocol Buffers que describe a qué dispositivo va cada
split. Su presencia distingue un APK Set genuino de un ZIP de splits con la extensión puesta
a mano — y la extensión miente con frecuencia: 14 de los 16 ficheros .apks del corpus de
verificación no llevan toc.pb. El detalle completo, en
AAB, splits y contenedores, sección 6.
3. Inventario comentado de un APK real
Todo lo de esta sección está ejecutado el 12 de agosto de 2026 sobre
org.fdroid.fdroid_1023052.apk del corpus (F-Droid 1.23.2, minSdk 23, targetSdk 30).
3.1 Lo primero: es un ZIP
CORPUS=~/corpus-apk
unzip -l $CORPUS/org.fdroid.fdroid_1023052.apk | tail -3
4103288 01-01-1981 01:01 resources.arsc
--------- -------
23660746 1119 files
Mil ciento diecinueve entradas. Y esa fecha, 01-01-1981 01:01, no es la de compilación:
es la constante que el Android Gradle Plugin escribe en todas las entradas para que la salida
sea reproducible. Es idéntica en los 58 APK standalone del corpus.
3.2 Las categorías
Agrupando las 1.119 entradas por su prefijo de ruta, con el tamaño sin comprimir y el método de compresión de cada grupo:
categoria nº bytes sin compr. metodo
1 AndroidManifest.xml 1 48.248 1 deflate
2 classes*.dex 2 17.839.628 2 deflate
3 resources.arsc 1 4.103.288 1 stored
4 res/ 1067 1.218.209 852 deflate · 215 stored
5 lib/<abi>/ 4 37.392 4 stored
6 assets/dexopt/ (baseline profile) 2 9.915 2 stored
7 assets/ 6 9.710 3 deflate · 3 stored
8 META-INF/ firma v1 3 204.631 3 deflate
9 META-INF/ resto 19 23.308 19 deflate
10 raiz y otros 14 166.417 14 deflate
3.3 Categoría por categoría
1 · AndroidManifest.xml — Una sola entrada, en la raíz, en formato AXML. Sus primeros
bytes lo confirman:
unzip -p $CORPUS/org.fdroid.fdroid_1023052.apk AndroidManifest.xml | xxd -l 16
00000000: 0300 0800 78bc 0000 0100 1c00 4049 0000 ....x.......@I..
03 00 es RES_XML_TYPE, 08 00 el tamaño de la cabecera del chunk y 78 bc 00 00 el
tamaño total del fichero. Es la misma estructura de chunks que usa resources.arsc. Ver
XML binario (AXML), sección 2.
2 · classes*.dex — Aquí, dos ficheros. El límite de 65.536 referencias a método por
fichero DEX obliga a repartir las clases en classes.dex, classes2.dex, classes3.dex…
sin huecos en la numeración. El orden es semántico: si una clase se define en dos ficheros,
gana la del primero en orden de carga. Ver
Formato DEX, secciones 1 y 15, y el juego de instrucciones
en Bytecode Dalvik.
3 · resources.arsc — La tabla que convierte cada resource ID de 32 bits en el valor
que corresponde a la configuración del dispositivo. Está stored, sin comprimir: desde
targetSdkVersion 30 es obligatorio, porque el sistema la mapea en memoria. Ver
Tabla de recursos.
4 · res/ — Los 1.067 ficheros de recurso ya compilados. Los .xml de aquí no son
texto: son AXML, igual que el manifiesto. El reparto de compresión no es caprichoso: los
852 .xml van deflate y los 215 stored son 214 .png y un .ogg, formatos que ya vienen
comprimidos y a los que deflate no les quitaría nada. El nombre del fichero se conserva, y
ese es justo el detalle que la ofuscación de recursos ataca: en una aplicación ofuscada se ven
rutas como res/AB/x1.xml.
5 · lib/<abi>/ — Aquí solo cuatro .so, uno por arquitectura, todos del mismo
libandroidx.graphics.path.so. Cuatro directorios significa cuatro ABIs empaquetadas, no
necesariamente cuatro soportadas de verdad. Ver
Código nativo y ELF, sección 2.
6 · assets/dexopt/ — baseline.prof y baseline.profm: la lista de métodos que
conviene compilar por adelantado para que la aplicación arranque rápido la primera vez. Van
siempre en pareja, y están en 52 de los 58 APK standalone del corpus. Ver
Artefactos auxiliares, sección 3.
7 · assets/ — Ficheros que se entregan tal cual, sin identificador de recurso. Se
abren por ruta con AssetManager. Es un sistema de ficheros opaco dentro del APK, sin índice y
sin restricciones de nombre, y por eso es donde acaban los modelos de aprendizaje automático,
las bases de datos precargadas y —el caso que importa— el payload cifrado de los packer.
8 · META-INF/ firma v1 — MANIFEST.MF con un digest por entrada, CIARANG.SF con los
digests de las secciones del anterior, y CIARANG.RSA con el bloque PKCS#7. El nombre base
lo elige quien firma y no significa nada. Ver
Esquemas v1, v2 y v3.
9 · META-INF/ resto — Metadatos que vienen de las dependencias y de AGP:
services/ del mecanismo ServiceLoader, *.version de cada biblioteca AndroidX,
app-metadata.properties con la versión del plugin y version-control-info.textproto con el
hash de commit exacto del que salió el APK.
10 · Raíz y otros — Y aquí está lo interesante, que es lo que no encaja en ninguna convención:
DebugProbesKt.bin kotlin/kotlin.kotlin_builtins
kotlin-tooling-metadata.json kotlin/collections/collections.kotlin_builtins
okhttp3/internal/publicsuffix/publicsuffixes.gz
org/bouncycastle/x509/CertPathReviewerMessages.properties
org/bouncycastle/x509/CertPathReviewerMessages_de.properties
version.properties
Leído sin abrir un solo DEX: la aplicación es Kotlin (los kotlin_builtins), usa
corrutinas (DebugProbesKt.bin), usa OkHttp (y con él, casi seguro, Retrofit) y usa
BouncyCastle para criptografía. Ese inventario es la huella del stack, y es gratis.
3.4 Lo que cambia en un APK más moderno
El mismo recuento sobre com.x8bit.bitwarden_2026.5.0.apk (targetSdk 36) da tres
diferencias que no son cosméticas:
F-Droid 1.23.2 (targetSdk 30) |
Bitwarden (targetSdk 36) |
|
|---|---|---|
classes*.dex |
deflate |
stored |
lib/<abi>/ |
4 entradas, stored |
23 entradas, stored y alineadas a 16 KiB |
| Firma v1 | Presente (3 entradas en META-INF/) |
Ausente: solo v2 |
La firma v1 desaparece cuando minSdkVersion es lo bastante alto: apksigner verify -v sobre
Bitwarden responde v1: false / v2: true, sin más. Y la alineación a 16 KiB responde a un
requisito con fecha: desde el 1 de febrero de 2027, Google Play no admite actualizaciones
de aplicaciones con targetSdk 35 o superior que no soporten páginas de memoria de 16 KB en
dispositivos de 64 bits.
4. Los cuatro niveles de «decompilar»
«Decompilar un APK» se usa para cuatro operaciones distintas que se diferencian en una cosa: cuánta información se está inventando el que las hace. Confundirlas es el error conceptual más caro del dominio, porque lleva a confiar en una salida que puede no corresponderse con lo que la aplicación hace.
4.1 Nivel 1 — Desempaquetar: unzip -d salida/ app.apk
Abrir el ZIP y sacar los ficheros. Sin pérdida y trivial: los bytes que salen son
exactamente los que estaban. Lo único que se pierde es la información del propio contenedor
—el orden de las entradas, el método de compresión, el relleno de alineación y el
APK Signing Block, que no es una entrada del ZIP y por tanto unzip ni lo ve—. Quien
pretenda volver a montar el APK necesita todo eso; quien solo quiera mirar dentro, no.
4.2 Nivel 2 — Decodificar: aapt2 dump xmltree / aapt2 dump resources
Convertir los formatos binarios de Android a algo legible: AXML a XML de texto,
resources.arsc a un árbol res/values/. Reversible en el sentido que importa: la
información está toda ahí y se puede volver a compilar. Lo que no se recupera es la forma del
fuente —indentación, comentarios, si un booleano se escribió true o TRUE—, porque el
compilador nunca la guardó. Dos manifiestos distintos pueden producir el mismo AXML, y un
AXML admite varias reconstrucciones igualmente válidas.
4.3 Nivel 3 — Desensamblar: baksmali disassemble (o dexdump -d)
Convertir el DEX a smali, la sintaxis de texto del
bytecode Dalvik. Fiel y uno a uno:
cada instrucción del fichero es una línea del texto y viceversa, smali tiene inverso exacto
—el ensamblador del mismo nombre— y por eso es el nivel al que se edita una aplicación de la
que no se tiene el fuente. No se inventa nada. Lo que se pierde ya se había perdido al
compilar: los nombres de las variables locales y los números de línea solo están si el
debug_info_item sobrevivió, y ese es el primer campo que un ofuscador pone a cero.
⚠️ no ejecutado localmente: baksmali no está instalado en esta máquina; su sintaxis procede
de la documentación del proyecto y se detalla en
Desensambladores smali. Las salidas de
dexdump de este documento sí están ejecutadas.
4.4 Nivel 4 — Decompilar: jadx -d salida/ app.apk
Convertir el DEX en algo que se parezca a Java. Esto no es una traducción: es una
reconstrucción. El bytecode Dalvik está basado en registros y el compilador ya destruyó la
estructura del fuente: los bucles son saltos, las constantes pequeñas se fundieron dentro de
las instrucciones, las lambdas se convirtieron en clases sintéticas, los genéricos solo
sobreviven si quedó la anotación dalvik.annotation.Signature. El decompilador tiene que
inferir un programa Java cuyo bytecode sea equivalente al que ve, con tres consecuencias
que hay que asumir:
- Puede fallar, y falla por método, no por fichero. Lo normal es obtener un
.javacon la mayoría de los métodos reconstruidos y unos cuantos sustituidos por un comentario de error y elsmalioriginal. - Lo que sale no compila necesariamente. Y no siempre por un fallo de la herramienta: los
nombres admisibles en un
DEXson mucho más permisivos que los de Java, así que un ofuscador puede emitir identificadores que no existen como código Java válido. - Los fallos son silenciosos si no se miran. Nadie avisa de qué porcentaje del código que se está leyendo no es lo que la aplicación hace.
4.5 Tabla resumen
| Nivel | Qué hace | Herramienta típica | Reversible | Qué se pierde |
|---|---|---|---|---|
| 1 · Desempaquetar | ZIP → ficheros | unzip, apktool d -s, APKEditor |
Sí, con cuidado | Orden, compresión, alineación, APK Signing Block |
| 2 · Decodificar | AXML → XML, .arsc → res/values/ |
aapt2 dump, apktool, ARSCLib |
Sí | Formato del fuente: indentación, comentarios, mayúsculas |
| 3 · Desensamblar | DEX → smali |
baksmali, dexdump -d |
Sí, exactamente | Nada nuevo: los nombres de locales y las líneas ya se habían perdido al compilar |
| 4 · Decompilar | DEX → Java |
jadx, dex2jar + Vineflower/CFR, JEB | No | La certeza. El resultado es una hipótesis, y puede fallar por método |
4.6 El mismo método, en los cuatro niveles
Sobre el APK de ocho pasos de la sección 2.6, del que se conserva el fuente original:
// nivel 0 · el fuente, que en un APK ajeno no se tiene
android.util.Log.i("EJEMPLO", getString(R.string.app_name));
// nivel 3 · dexdump -d sobre classes.dex
0000: invoke-super {v0, v1}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V
0003: const/high16 v1, #int 2130771968 // #7f01
0005: invoke-virtual {v0, v1}, L…/MainActivity;.getString:(I)Ljava/lang/String;
0008: move-result-object v0
0009: const-string v1, "EJEMPLO" // string@0010
000b: invoke-static {v1, v0}, Landroid/util/Log;.i:(…)I
000e: return-void
R.string.app_name ya no existe. Lo que queda es la constante 2130771968 = 0x7f010000
incrustada en la instrucción. El nombre del recurso no está en el DEX: está en la tabla de
recursos, y es el nivel 2 el que lo devuelve.
// nivel 2 · aapt2 dump resources sobre el mismo APK
Package name=com.ejemplo.demo id=7f
type string id=01 entryCount=1
resource 0x7f010000 string/app_name
() "Ejemplo Demo"
Esa es la lección de la sección entera: los niveles no son etapas de una escalera, son
fuentes de información distintas, y el análisis serio los cruza. El nivel 4 solo puede
escribir getString(R.string.app_name) porque alguien le ha dado la tabla del nivel 2.
El resto de la salida de dexdump sobre ese mismo método dice qué más se recuperó y qué no:
positions : 0x0000 line=4 · 0x0005 line=5
locals : reg=0 this L…/MainActivity; · reg=1 (null) Landroid/os/Bundle;
Los números de línea sobrevivieron —d8 --release los conserva para que las trazas de pila
sean legibles—, pero el nombre del parámetro es (null): se perdió. Un decompilador lo
llamará bundle, arg0 o p1 según su heurística, y ninguna de las tres es el nombre que
escribió el desarrollador. Y eso, en un APK sin ofuscar. Con R8 en modo completo se
pierden además los nombres de clases, campos y métodos, y la única vía de vuelta es el
mapping.txt, que no viaja dentro del APK.
5. Qué sobrevive siempre a la ofuscación
Aunque el código sea completamente ilegible —y en el corpus hay aplicaciones con el 100 % de los identificadores ofuscados—, hay cosas que no se pueden ocultar sin romper la aplicación. Es el suelo del análisis, y conviene tenerlo claro antes de invertir horas en decompilar:
| Qué sobrevive | Por qué no se puede ocultar |
|---|---|
El nombre del paquete, la versión, minSdk y targetSdk |
El instalador los lee antes de ejecutar nada |
| Los permisos | Los concede el sistema a partir del manifiesto. Un permiso no declarado no se obtiene |
Los componentes exportados (activity, service, receiver, provider) |
Otras aplicaciones y el propio sistema tienen que poder resolverlos por nombre |
| La firma y la cadena de certificados | Es lo que el sistema verifica. Es además el identificador estable del autor entre versiones |
| Los recursos y su estructura | Se resuelven por identificador numérico en tiempo de ejecución. Los nombres se pueden ofuscar; la tabla, el conjunto de idiomas, de densidades y los valores, no |
| Las bibliotecas nativas y sus ABIs | El cargador las busca en lib/<abi>/ por convención de ruta |
| Las llamadas a la API de Android | Landroid/... no se puede renombrar: lo define el framework, no la aplicación |
| Las cadenas que llegan al sistema | URLs, nombres de fichero y nombres de tabla se pueden cifrar en reposo, pero tienen que estar en claro en el momento de usarse |
De ahí sale la regla práctica: el manifiesto, la firma, el inventario del ZIP y las llamadas
al framework dan un perfil completo del comportamiento observable de la aplicación sin
decompilar una sola línea. Un packer —un protector que sustituye el DEX
original por un stub que descifra y carga el código real en ejecución— sigue teniendo que
declarar sus permisos y sus componentes, y sigue teniendo que llamar a DexClassLoader desde
algún sitio.
Lo que sí desaparece: los nombres de clases, campos y métodos propios; los nombres de las variables locales y de los parámetros; los números de línea y el nombre del fichero fuente; los genéricos; los nombres de las entradas de recurso; y la estructura del código, si hay aplanamiento de flujo de control. Ver R8 y ProGuard y Anti-análisis y hardening.
6. Mapa de lectura
Esta tabla organiza la lectura por pregunta concreta, con la sección de este documento que la enmarca.
| Lo que quieres hacer | Empieza aquí | Y sigue por |
|---|---|---|
| Saber qué hace la aplicación sin tocar el código (§3, §5) | Pipeline de análisis | XML binario (AXML) |
Averiguar qué es ese .xapk / .apks / .apkm (§2.8) |
AAB, splits y contenedores, §6 | Fusión de splits |
| Leer el código — decide antes el nivel (§4) | Decompiladores a Java (nivel 4) | Desensambladores smali (nivel 3) |
| Modificar la aplicación y que se instale (§2.5) | Pipeline de modificación | Firma y empaquetado |
| Escribir un parser | Contenedor ZIP | Código fuente de referencia |
| Enfrentarte a código ofuscado (§5) | R8 y ProGuard | Deofuscación |
| Entender por qué ha fallado algo | Diagnóstico de fallos | — |
| Elegir herramienta, y saber si sigue viva | Mapa del ecosistema | Comparativa y selección |
| Buscar una palabra | Glosario | — |
Fuentes
- Build your app from the command line — Android Developers — https://developer.android.com/build/building-cmdline
Consultado el 12 de agosto de 2026. De aquí salen los nombres de tarea
assembleDebug,assembleReleaseybundle<Variante>(sección 2.7), el flujo deaapt2 --proto-formathaciabundletool build-bundle(2.8) y la generación de claves conkeytool(2.6). - AAPT2 — Android Developers — https://developer.android.com/tools/aapt2
Consultado el 12 de agosto de 2026. De aquí salen la cita sobre las dos fases compile y
link, la tabla de tipos de fichero
.flat, la nota de que el APK que producelinkno contiene DEX ni firma, y las opciones--no-crunch,-Iy--java(sección 2.2). - d8 — Android Developers — https://developer.android.com/tools/d8
Consultado el 12 de agosto de 2026. De aquí salen la definición de
D8y las opciones--release,--min-apiy--lib(secciones 2.3 y 2.6). - Enable app optimization with R8 — Android Developers — https://developer.android.com/topic/performance/app-optimization/enable-app-optimization
Consultado el 12 de agosto de 2026; la URL
developer.android.com/build/shrink-coderedirige aquí. De aquí salen las tres operaciones deR8, el ejemplocom.example.MyActivity→a.b.a, el full mode por defecto desde AGP 8.0 y el bloqueoptimization {}de AGP 9.3 (sección 2.3). - Android Gradle plugin 9.3.0 release notes (julio de 2026) — https://developer.android.com/build/releases/gradle-plugin Consultado el 12 de agosto de 2026. De aquí sale que la rama estable de AGP el día de la consulta es la 9.3, con Gradle 9.5.0, SDK Build Tools 36.0.0 y JDK 17 (sección 2.7).
- zipalign — Android Developers — https://developer.android.com/tools/zipalign
Consultado el 12 de agosto de 2026. De aquí salen la cita sobre
mmap(2), la regla de-P 16para los.soy el orden respecto aapksigner(sección 2.5). - apksigner — Android Developers — https://developer.android.com/tools/apksigner
Consultado el 12 de agosto de 2026. De aquí salen la sintaxis de
signyverifyy la advertencia de que modificar el APK tras firmarlo invalida la firma (secciones 2.5 y 2.6). - About Android App Bundles — Android Developers — https://developer.android.com/guide/app-bundle
Consultado el 12 de agosto de 2026. De aquí sale la definición citada del
AABcomo formato de publicación que difiere la generación y la firma de los APK a Google Play (sección 2.8). - Corpus de verificación — 5,9 GB, 86 contenedores y 717 APK: 58 sueltos de F-Droid y 659
dentro de contenedores de splits.
Medido el 12 de agosto de 2026. De aquí salen íntegras las secciones 3.1 a 3.4, la salida de
bundletool build-apksde la sección 2.8 y el dato de los 14 de 16.apkssintoc.pb. - Ejecución local del ciclo de build y de los cuatro niveles —
build-tools37.0.0 (aapt22.20-15087165,D89.2.4-dev,apksigner0.9,zipalign,dexdump),javac21.0.10,platforms/android-35ybundletool1.18.3, el 12 de agosto de 2026. De aquí salen todas las salidas de las secciones 2.6, 2.8, 4.2 y 4.6. - Documentos de formatos de este corpus —
10-formatos/Consultados el 12 de agosto de 2026. La sección 3 es un resumen de los ocho, y cada categoría del inventario enlaza al que la desarrolla; los datos de compresión, alineación,baseline profiles, ABIs, límite de 65.536 referencias,packersy firma de las secciones 1, 3.3, 3.4 y 5 proceden de ellos.