Deofuscación
Antes conviene leer «R8 y ProGuard», «Formato DEX», «Tabla de recursos»
1. Qué es y por qué existe
Deofuscar es recuperar los nombres y la estructura que la ofuscación destruyó. Es el documento de cierre de este bloque porque solo tiene sentido después de haber entendido qué hace cada transformación: cada tipo de deofuscación deshace una transformación concreta, y las que no se pueden deshacer también hay que nombrarlas.
La afirmación que gobierna todo lo que sigue, y que conviene fijar antes de entrar en
detalle: la ofuscación de nombres no esconde información, la destruye. Un mapping.txt
no es una clave que descifra el binario; es una copia de seguridad que el compilador hizo
al borrar. Con esa copia, deofuscar es una sustitución en una tabla. Sin ella, no hay nada
que descifrar: la palabra Calculadora ya no existe en ninguna parte del APK, y lo único
que se puede hacer es adivinarla a partir de lo que la rodea. Esa asimetría es la que
divide el problema en dos mitades con costes y garantías radicalmente distintos, y es la
razón de que el techo de la mitad heurística sea bajo por construcción y no por falta de
esfuerzo («sección 8 · El límite teórico»).
Alcance. El descifrado de cadenas («sección 6 · Descifrado de cadenas») se trata aquí como análisis estático para
poder leer código, que es un objetivo distinto de neutralizar una protección en ejecución.
La diferencia operativa —y no es un matiz retórico— es que el resultado es una anotación
junto al desensamblado, no un APK con las cadenas sustituidas. La «sección 6.4 · Por qué es análisis para leer, y no neutralización» lo desarrolla.
Versiones de referencia: R8 9.2.4-dev y aapt2 de build-tools 37.0.0, consultadas en agosto
y septiembre de 2026.
2. La taxonomía en cuatro tipos
El problema se divide en cuatro. La división no es administrativa: cada tipo tiene una garantía distinta, y esa garantía es lo que decide si el resultado se puede presentar como un hecho o solo como una sugerencia.
| # | Tipo | Naturaleza | Entrada necesaria | Garantía del resultado |
|---|---|---|---|---|
| 1 | retrace con mapping.txt |
Determinista | El mapping.txt de esa build |
Exacto, o «no lo sé». Nunca equivocado |
| 2 | Renombrado de recursos | Determinista | Ninguna externa: la estructura del propio APK |
Correcto por construcción donde aplica; no aplica en todas partes |
| 3 | Inferencia de nombres sin mapping | Heurística | Nada, pero necesita un modelo | Probable. Cada nombre lleva confianza y puede estar mal |
| 4 | Descifrado de cadenas | Análisis estático | Nada | Exacto donde converge, «desconocido» donde no. Nunca aproximado |
¿existe una copia de lo borrado?
│
┌─────────────────────┴─────────────────────┐
SÍ NO
│ │
┌─────────┴──────────┐ ┌─────────┴──────────┐
│ 1. mapping.txt │ │ ¿la información │
│ (el compilador │ │ sigue en el │
│ la guardó) │ │ binario, cifrada │
├────────────────────┤ │ o implícita? │
│ 2. estructura del │ └─────────┬──────────┘
│ APK: tipos, │ ┌───────┴────────┐
│ manifiesto, │ SÍ NO
│ framework │ │ │
└────────────────────┘ ┌─────────┴──────┐ ┌──────┴──────────┐
DETERMINISTA │ 4. descifrado │ │ 3. inferencia │
resultado o silencio │ de cadenas │ │ heurística │
│ exacto o │ │ probable, con │
│ desconocido │ │ confianza │
└────────────────┘ └─────────────────┘
El tipo 4 no encaja en la dicotomía determinista/heurístico y por eso se lista aparte: la
cadena sí está en el binario, cifrada, y el descifrador también, porque el programa tiene
que poder leerla. Es un problema de análisis, no de adivinación. O el análisis converge y el
resultado es exacto, o no converge y la respuesta correcta es «desconocido». Nunca hay una
cadena «probablemente https://api.ejemplo.com».
3. Determinista 1 — retrace con mapping.txt
3.1 El algoritmo
La gramática completa del fichero está en «R8 y ProGuard → §6 · mapping.txt: la gramática exacta», y
no se repite aquí. Lo que sí toca es el algoritmo inverso, que es lo que hace retrace:
entrada: clase ofuscada C, método ofuscado m, línea L
(los tres, tal como aparecen en la traza de pila)
1. Buscar la línea de clase cuyo lado derecho sea C.
Si no está → devolver la entrada sin cambios. NO adivinar.
2. Dentro de esa clase, reunir todas las entradas de método cuyo
nombre ofuscado sea m.
3. Filtrar las que tengan minifiedRange que contenga L.
· ninguna, y hay entradas sin rango → esas aplican
· ninguna en absoluto → devolver sin cambios
· varias con rangos distintos → AMBIGUO: devolver todas
4. El resultado es la LISTA de entradas con ese mismo minifiedRange,
en el orden en que aparecen: es la pila inlineada, de marco más
interno a más externo (sección 6.5 del documento 01).
5. Para cada entrada, proyectar L dentro de originalRange.
Si originalRange se omitió, vale minifiedRange.
Si es un solo número, ese es el resultado.
6. El nombre de fichero sale del comentario {"id":"sourceFile"} de
la clase, no de la traza.
7. Eliminar los marcos marcados {"id":"com.android.tools.r8.synthesized"}.
Aplicar rewriteFrame si la línea anterior es la excepción que su
condición exige.
Los pasos 4, 6 y 7 son los que separan un retrace correcto de una sustitución de texto. Un
lector que se quede en los pasos 1 a 3 devuelve un marco donde había tres, sin nombre de
fichero, y con marcos sintéticos que el usuario no escribió.
3.2 Qué recupera exactamente, y qué no
| Recupera | No recupera |
|---|---|
| Nombre completo de la clase, incluido el paquete original | El cuerpo del método inlineado, que sigue duplicado en el DEX |
| Nombre del método y del campo | Los nombres de las variables locales («sección 8 · El límite teórico») |
| Firma original: tipos de retorno y de argumentos | Las clases eliminadas por el shrinker |
| Número de línea del fuente | Los genéricos, si se eliminó Signature |
| Nombre del fichero fuente | El nombre de los parámetros |
| La pila de llamadas completa de los métodos inlineados | Nada de lo que la ofuscación no tocara y el shrinker sí |
Ese punto en negrita es el que más se subestima. mapping.txt no solo deshace el renombrado:
también deshace, en el eje de las trazas de pila, parte de la optimización. Una línea de
traza se convierte en tres marcos porque el fichero guarda la cadena de inlining. Ninguna
técnica heurística puede reconstruir eso.
3.3 Por qué es sin riesgo
Porque no hay ningún punto en el que se decida entre alternativas plausibles. La tabla la produjo el mismo compilador que produjo el binario. Los tres resultados posibles son «este es el nombre», «esta clase no está en el mapping» y «hay varias entradas y no puedo distinguirlas», y los tres son verdad.
Y hay una comprobación de correspondencia que casi nadie hace y que cuesta tres líneas: el
mapping.txt empieza por # pg_map_id: <hash> y el DEX lleva un marcador
~~R8{…,"pg-map-id":"<hash>",…} con el mismo valor
(«R8 y ProGuard → §9.1 · El marcador ~~R8{…} dentro del DEX»). Comparar ambos convierte «creo que este
mapping es el de este APK» en un hecho verificable. Sin esa comprobación, aplicar el
mapping.txt de la versión 5.1 a una traza de la 5.2 produce nombres plausibles y
equivocados, que es el peor resultado posible.
3.4 El problema real: nadie tiene el mapping.txt
mapping.txt se genera en la máquina de quien compila y no se distribuye. La
documentación oficial advierte de que «se sobrescribe cada vez que se compila el proyecto».
Sí viaja dentro del AAB que se sube a Google Play, pero no dentro del APK que se
instala.
Consecuencia: el tipo 1 sirve al desarrollador de la aplicación —que es quien recibe trazas de pila de producción— y no sirve a quien analiza una aplicación ajena, que es el caso de uso de la mayor parte de esta documentación. De ahí que los tipos 3 y 4 existan.
4. Determinista 2 — renombrado de recursos
Este es el caso interesante: se recupera información sin ninguna entrada externa, solo
apoyándose en la estructura del propio APK. Y es determinista porque lo que se reconstruye
no es el nombre que puso el desarrollador —eso se perdió— sino un nombre correcto por
construcción, derivado de datos que siguen ahí.
4.1 Qué queda de un resources.arsc ofuscado
Medido directamente sobre el «muestrario». Al colapsar los nombres, el key string pool
—donde viven los nombres de entrada— se reduce a una sola cadena para toda la tabla; en
WhatsApp la cadena es (name removed) y aparece una única vez en los 12,6 MB del fichero, y
en Snapchat es 0_resource_name_obfuscated, también una sola vez en el fichero. La segunda
la pone aapt2 --collapse-resource-names; la primera no está en el binario de aapt2: es la
constante RESOURCE_NAME_REMOVED de Redex, el optimizador de Meta
(libredex/RedexResources.h). DexGuard hace
otra cosa: sustituye cada nombre por su propio resource ID en decimal
(«Ofuscadores comerciales → §2.9 · Ofuscación de recursos»).
Y sin embargo, todo lo demás sobrevive intacto:
| Sobrevive | Por qué |
|---|---|
El nombre del tipo (anim, drawable, layout, string, attr, style, id…) |
Vive en un string pool distinto, el de tipos, y AAPT2 no lo colapsa. En el WhatsApp del muestrario están los 22 tipos con su nombre |
El resource ID completo 0xPPTTEEEE |
Es la clave de acceso; sin él la aplicación no funciona |
La estructura de configuraciones (ResTable_config) |
Idioma, densidad, orientación, versión de API de cada variante |
| El valor de cada entrada | Es lo que la aplicación lee |
| Las referencias entre recursos | Un style que hereda de otro, un layout que referencia un id |
Los identificadores del framework (0x01…) |
No son de la aplicación. 0x01010003 es android:name en cualquier APK del mundo |
| Las referencias desde el manifiesto | El manifiesto es contrato con el sistema |
Comprobado sobre el base.apk de WhatsApp, el más agresivamente ofuscado del muestrario:
type anim id=01 entryCount=93 type id id=0b entryCount=14436
type attr id=04 entryCount=2460 type layout id=0e entryCount=5441
type color id=06 entryCount=2389 type string id=12 entryCount=19907
type drawable id=08 entryCount=2612 type style id=15 entryCount=1930
Es decir: se sabe que el recurso 0x7f0e1234 es un layout, que existen 5.441 layouts,
qué fichero corresponde a cada uno y con qué configuraciones varía. Lo único que falta es
cómo se llamaba.
4.2 Qué se puede reconstruir, y cómo
Cuatro vías, en orden de fuerza.
a) Nombre canónico derivado del tipo y del identificador. No recupera nada, pero
convierte una tabla inutilizable en una utilizable: cada entrada recibe un nombre único y
estable de la forma <tipo>_<índice>. Es lo que hace falta para que el APK se pueda
decompilar y volver a compilar, porque las herramientas necesitan un nombre por recurso.
Coste nulo, beneficio: el APK vuelve a ser editable.
b) Recuperación desde el manifiesto. El AndroidManifest.xml referencia recursos por
identificador —android:label="@0x7f1400fc", android:icon="@0x7f110000", android:theme—
y el atributo dice para qué sirve el recurso. Un recurso referenciado desde
android:label de la application es la etiqueta de la aplicación; se puede nombrar
app_name con certeza semántica total, aunque no se sepa si el desarrollador lo llamaba así.
El número de recursos recuperables por esta vía es pequeño —decenas— pero son los más
importantes.
c) Recuperación desde el framework. Los atributos con identificador 0x01xxxxxx son del
paquete android y sus nombres están en android.jar: 0x01010003 es siempre
android:name. Cualquier style de la aplicación que sobrescriba 0x01010098 está
sobrescribiendo android:textColor, y eso nombra la intención del estilo. Esta vía es
completamente determinista: la tabla de identificadores del framework es pública y estable
por nivel de API.
d) Recuperación desde el valor. Un string cuyo valor es "Ajustes" en español y
"Settings" en inglés se puede nombrar settings. Un color cuyo valor es #FF000000 se
puede nombrar black. Es la más productiva en volumen —hay 19.907 string en WhatsApp, todos
con su texto intacto— y la única de las cuatro que no es determinista: es una heurística
sobre el contenido, y como tal debe marcarse.
4.3 El mapa oficial, cuando existe
Hay un detalle que cambia el planteamiento: la ofuscación de recursos de Google emite su
propio fichero de correspondencia. aapt2 optimize tiene la opción
--save-obfuscation-map <fichero>
descrita por la propia herramienta como «ruta donde volcar el mapa de rutas/nombres originales
a ofuscados». Y el binario de aapt2 37.0.0 contiene los descriptores protobuf
aapt.pb.CollapsedNamesMap y aapt.pb.CollapsedNamesMap.ResourceNameMapping, que son su
formato.
Es decir: existe el mapping.txt de los recursos. Igual que el de R8, se queda en la
máquina de quien compila. Cuando está, el tipo 2 se convierte en un tipo 1: sustitución en una
tabla. Cuando no está, se aplican las cuatro vías de la «sección 4.2 · Qué se puede reconstruir, y cómo».
Hay además una vía de exención documentada que produce una asimetría útil: las directivas
no_collapse y no_path_shorten de un fichero resources.cfg permiten excluir recursos
concretos. Un APK con nombres colapsados en el que algunos recursos conservan su nombre
real está señalando cuáles le importaban al desarrollador, casi siempre los que se acceden
por reflexión o por getIdentifier().
5. Heurístico — inferencia de nombres sin mapping.txt
Este es el caso real de quien analiza una aplicación ajena, y el que casi todo el mundo aplaza por caro.
5.1 Las señales disponibles
Todas salen del suelo descrito en «Ofuscadores comerciales → §8 · Qué NO se puede ofuscar»: lo que no se puede ofuscar.
| Señal | Qué aporta | Fuerza |
|---|---|---|
| Bibliotecas de terceros identificadas por forma | El código de la biblioteca existe sin ofuscar en Maven Central. Casar el bytecode normalizado devuelve los nombres reales, no inferidos | La más alta: recupera nombres verdaderos |
| Nombres que sobreviven por keep rules | onCreate, values(), CREATOR, los set*/get* de View, los métodos @JavascriptInterface («R8 y ProGuard → §5.2 · Las reglas que AGP empaqueta de verdad») |
Alta: son anclas fijas |
| Llamadas a la API de Android | Una clase que extiende Activity y llama a setContentView(R.layout.X) es la pantalla X |
Alta, cuando el recurso conserva nombre |
| Cadenas literales | Claves de SharedPreferences, endpoints, mensajes de log, nombres usados en reflexión |
Media; nula si están cifradas |
| Jerarquía de tipos | Implementar Comparator, extender RecyclerView.Adapter, tener un campo List<T> |
Media |
| Anotaciones de serialización | @SerializedName("user_id") de Gson conserva el nombre real del campo JSON, y sobrevive porque el runtime lo necesita |
Alta y muy infravalorada |
META-INF/*.kotlin_module |
El organigrama de módulos Gradle del proyecto, intacto incluso en las apps más ofuscadas | Media: da paquetes, no clases |
META-INF/*.version y *.properties |
Nombre y versión exactos de cada dependencia. El Netflix del muestrario lleva 98 y 30 respectivamente | Alta para identificar bibliotecas |
| Estructura del control de flujo | La forma del grafo como huella | Baja aisladamente, útil para casar bibliotecas |
5.2 Los trabajos publicados y sus cifras reales
DeGuard es la referencia canónica para Android. Bichsel, Raychev, Tsankov y Vechev, «Statistical Deobfuscation of Android Applications», ACM CCS 2016. Su resultado, literal:
«recupera el 79,1 % de los nombres de elementos de programa ofuscados con ProGuard, predice bibliotecas de terceros con una precisión del 91,3 %, y revela decodificadores de cadenas y clases que manejan datos sensibles en malware de Android.»
Las dos cifras salen del mismo modelo probabilístico y no miden la identificación
estructural de la tabla de la «sección 5.1 · Las señales disponibles». El 91,3 % es la precisión al predecir qué
bibliotecas importa la aplicación: DeGuard infiere los nombres de paquete y el artículo cuenta
cuántos de los que caen en una lista de 133 bibliotecas son correctos (la exhaustividad es del
91,0 %). Que supere al 79,1 % dice que el paquete de una biblioteca conocida se adivina mejor
que un nombre cualquiera, no que se recuperen nombres reales. El servicio apk-deguard.com seguía en
línea el 12 de agosto de 2026, con un aviso de que «estamos trabajando en una nueva versión de
DeGuard», pero el 24 de septiembre ya no contestaba: no hay demo pública que probar.
Su antecedente es JSNice: Raychev, Vechev y Krause, «Predicting Program Properties from "Big Code"», POPL 2015, sobre JavaScript minificado:
«JSNICE predice nombres correctos para el 63 % de los identificadores de nombre y sus predicciones de anotación de tipo son correctas en el 81 % de los casos.»
Ambos se apoyan en Nice2Predict, el marco de predicción estructurada que los autores publicaron como software libre.
Y hay una línea análoga sobre binarios nativos descompilados, que conviene citar por el método pero no confundir con el caso Android:
| Trabajo | Venue | Resultado |
|---|---|---|
| DIRE — Lacomis et al. | ASE 2019 | «predice nombres de variable idénticos a los del código fuente original hasta el 74,3 % de las veces», sobre 164.632 binarios x86-64 |
| DIRTY — Chen et al. | USENIX Security '22 | Nombres de variable 66,4 %, tipos 75,8 % |
| DIRE and its Data — Dramko et al. | TOSEM 2022 | El modelo con información de dominio predice identificadores un 22,94 % más a menudo que DIRE base |
⚠️ DIRE y DIRTY operan sobre C descompilado de x86-64, no sobre DEX. Se listan como línea de
trabajo análoga, no como recuperación de nombres en Android. Para Android/Java, DeGuard
sigue siendo la referencia.
5.3 Por qué el techo es bajo
Las cifras anteriores parecen buenas. No lo son tanto, por cuatro motivos que no se arreglan con más cómputo.
La información se destruyó, no se escondió. Un descifrador de cadenas tiene una respuesta
correcta esperándole en el binario. Un inferidor de nombres no: Calculadora no está en
ninguna parte. Lo máximo alcanzable es «un nombre que un programador razonable habría puesto»,
y eso solo coincide con el original cuando el original era el nombre obvio.
Un 79 % de acierto significa un 21 % de error, y el error es tóxico. La regla es que un
nombre inferido incorrecto es peor que ninguno, porque induce a error a quien lee. Un método llamado validarFirma que en realidad valida un
formato de fecha desvía una investigación entera. La mitigación —procedencia y confianza en
cada nombre, distinción visual, renombrado reversible— no sube la precisión: hace que el
error sea visible, que es lo mejor que se puede hacer.
La cobertura y la precisión se miden por separado o no se miden. Un sistema que nombra el 5 % de los símbolos con un 99 % de acierto y otro que nombra el 100 % con un 40 % son inutilizables por motivos opuestos. Publicar un número único los confunde. El criterio de aceptación tiene que ser explícito: dos métricas separadas, cobertura y precisión, sin mezclarlas en un número único.
La estabilidad entre versiones es un requisito, no una propiedad emergente. Si el nombre inferido para la misma clase cambia entre dos builds de la aplicación, el diff semántico se llena de ruido y las anotaciones compartidas dejan de servir. El plan lo convierte en criterio medible: «sobre dos versiones consecutivas de cinco apps, más del 90 % de los símbolos que no cambiaron reciben el mismo nombre inferido».
Hay además una oportunidad concreta en el muestrario que la medición de este bloque destapó.
De los 57 APK de F-Droid, 20 están ofuscados con R8 con una fracción de identificadores
cortos comparable a la de Twitch («R8 y ProGuard → §10.3 · El grupo de control de F-Droid»). Y son
software libre: su código fuente está publicado. Son, por tanto, los únicos casos del
muestrario en los que se puede construir la verdad de terreno de un evaluador de inferencia sin
comprar nada ni firmar nada.
6. Descifrado de cadenas
6.1 Por qué es lo que más rinde
Porque devuelve exactamente aquello sobre lo que un lector se orienta. Sin cadenas, un método
llamado a que llama a b y a c no dice nada. Con una cadena "api/v2/user/profile"
dentro, el método se entiende de golpe, aunque siga llamándose a.
Y hay una razón estructural: la cadena es información que sobrevivió. Está en el binario, cifrada, junto con el descifrador, porque el programa tiene que poder leerla en tiempo de ejecución. No hay que adivinar nada. Comparado con la inferencia de nombres, es la diferencia entre resolver un problema y estimar una respuesta.
Con la salvedad de escala que da el estudio de Dong et al.: el cifrado de cadenas aparece en el 0,0 % de las aplicaciones de Google Play, el 0,1 % de las de mercados de terceros y el 5,3 % del malware («Ofuscadores comerciales → §9 · Cuánta ofuscación hay realmente»). Es una técnica de alto valor para análisis de malware y de aplicaciones de alto valor comercial, no para el catálogo general.
6.2 En qué consiste como problema de análisis estático
Tres subproblemas encadenados:
1. Localizar el descifrador. Su forma es reconocible sin conocer al ofuscador: un método
estático que toma constantes —un entero, un array de bytes, una cadena— devuelve un String,
y es invocado desde muchos sitios con argumentos literales distintos. Buscar por forma del
grafo y de la firma envejece mucho mejor que buscar por firma del producto concreto.
2. Resolver los argumentos. Necesita propagación de constantes desde cada punto de
llamada, y valores de contexto: campos estáticos inicializados en <clinit>, arrays
inicializados con fill-array-data, constantes de clase.
3. Ejecutar el descifrador sin ejecutarlo. Interpretación abstracta de un subconjunto de
bytecode Dalvik («Bytecode Dalvik»), con un modelo de
los métodos del framework que hagan falta —String, StringBuilder, Arrays, y javax.crypto
con claves constantes, que cubren la mayoría de los casos reales—.
6.3 Qué lo hace caro
- El intérprete procesa bytecode hostil. Es superficie de ataque: tiene que ser puro por construcción —sin E/S, sin reflexión, sin JNI, sin cargar ninguna clase real— y estar acotado en pasos, memoria y profundidad de llamada, o un bucle infinito en la entrada cuelga la herramienta.
- La lista blanca del framework no termina nunca. Cada método modelado cubre unos casos y descubre otros. Es la clase de trabajo cuyo coste marginal no baja.
- Hay casos irresolubles por diseño, y hay que reconocerlos y declararlos en vez de
intentarlos por otra vía: descifrado que depende de código nativo, de una clave que llega
del servidor, de la firma del
APKo del entorno de ejecución.
Del lado empírico, el trabajo de referencia es StringHound: Glanz, Müller, Baumgärtner, Reif, Amann, Anthonysamy y Mezini, «Hidden in Plain Sight: Obfuscated Strings Threatening Your Privacy», ASIA CCS 2020. Identifica cadenas ofuscadas y reconstruye las originales mediante slicing sobre bytecode Java, y su resultado publicado es comparativo: «deofusca casi 30 veces más cadenas ofuscadas que otras herramientas de deofuscación de cadenas», evaluado sobre 100.000 aplicaciones de Google Play, donde reveló «usos criptográficos vulnerables, accesos inseguros a internet, claves de API, contraseñas incrustadas y explotación de privilegios». ⚠️ Es un factor comparativo, no un porcentaje de acierto absoluto; convertir «×30» en un «%» sería inventar un dato.
6.4 Por qué es análisis para leer, y no neutralización
La distinción no es retórica y tiene una consecuencia comprobable: el resultado es una anotación, no un binario.
| Análisis para leer | Neutralización |
|---|---|
| Anotaciones junto al desensamblado y una tabla JSON | Un APK con las cadenas en claro dentro |
| No se ejecuta nada: interpretación abstracta, sin instrumentación ni hooks | Se ejecuta la aplicación y se interceptan las llamadas |
| El artefacto resultante no es distribuible como aplicación | El artefacto resultante es una aplicación con la protección retirada |
| El proyecto anotado se reensambla sin cambios de comportamiento: los comentarios son comentarios | El binario cambia |
De ahí salen tres restricciones no negociables y un test:
no se ejecuta nada, no se reescribe el DEX, y lo que dependa de nativo, del servidor, de la
firma o del entorno se declara irresoluble con su motivo. Con esas tres, la capacidad sirve al
análisis de malware y a la auditoría, y no sirve como herramienta de bypass. La frontera está
exactamente entre «puedo leer qué dice esta constante» y «he producido un binario sin
protección».
7. Recursos ofuscados por nombre de fichero
Es el caso res/AB/x1.xml, distinto del de la «sección 4 · Determinista 2 — renombrado de recursos»: aquí lo que se ha perdido no es el
nombre de la entrada en la tabla sino la ruta dentro del ZIP.
Tres formas observadas en el muestrario:
| Forma | Ejemplos reales | Herramienta |
|---|---|---|
| Rutas planas, sin directorio de tipo | res/j.xml, res/0.xml, res/3q.webp, res/-.xml (WhatsApp) |
aapt2 --shorten-resource-paths |
| Todo bajo un único directorio | res/raw/A.xml, res/raw/AW.xml, res/raw/BA.xml — 4.655 de 4.656 entradas (Netflix) |
DexGuard |
| Directorios de dos letras | res/AB/x1.xml |
Varios |
Lo que se reconstruye, y es más de lo que parece. La ruta del fichero no es la fuente de
la verdad: la fuente es resources.arsc, que para cada entrada guarda el tipo, la
configuración y el valor —que en el caso de un recurso de fichero es precisamente la ruta—.
Así que la correspondencia inversa es directa y determinista:
resources.arsc ZIP
0x7f010002 tipo=anim config=(por defecto) ──────► res/raw/f.xml
0x7f0e0135 tipo=layout config=(por defecto) ─────► res/j.xml
0x7f080a41 tipo=drawable config=xxhdpi ─────────► res/3q.webp
De ahí se reconstruye la ruta canónica completa —res/anim/anim_2.xml,
res/layout/layout_309.xml, res/drawable-xxhdpi/drawable_2625.webp— porque el tipo y la
configuración están los dos en la tabla. No se recupera el nombre que puso el desarrollador,
pero sí se recupera toda la organización: qué es cada fichero, para qué configuración
sirve y qué identificador lo referencia. Un proyecto decompilado con esa reconstrucción es
navegable; uno con 4.655 ficheros en res/raw/ con nombres de dos letras, no.
Un matiz que importa al reempaquetar: la ruta vive en dos sitios a la vez, el nombre de la
entrada del ZIP y el valor de la entrada en resources.arsc. Renombrar uno sin el otro
produce un APK que instala y falla al resolver el recurso. Es la misma clase de duplicación
que el Local File Header frente al Central Directory
(«Contenedor ZIP»).
Y sobre el tercer caso, el de las extensiones: res/j.xml conserva la extensión, y con ella
el hecho de que es AXML y no un binario cualquiera. WhatsApp conserva .webp en sus
imágenes. Es información gratis que no siempre se aprovecha.
8. El límite teórico
Lo que sigue no se recupera nunca, ni con más cómputo, ni con mejores modelos, ni con un
mapping.txt. Distinguir esto de «todavía no lo hacemos bien» es una obligación de honestidad
de cualquier herramienta que prometa deofuscar.
| Perdido | Por qué es irrecuperable | Confirmación |
|---|---|---|
| Nombres de variables locales | Viven en el debug_info_item, en las operaciones DBG_START_LOCAL. Si debug_info_off vale 0, la estructura no existe en el fichero. mapping.txt no los guarda: su gramática no tiene línea para locales |
«Formato DEX → §12 · debug_info_item»; «R8 y ProGuard → §6 · mapping.txt: la gramática exacta» |
| Nombres de parámetros | Ídem: parameter_names[] del debug_info_item |
Ídem |
| Genéricos | Viven en la anotación de sistema dalvik.annotation.Signature. R8 en modo completo solo conserva Signature para lo casado por keep rules, así que List<String> queda como List |
«Formato DEX → §9 · La sección de datos»; «R8 y ProGuard → §8 · R8 full mode», punto 5 |
| Código eliminado por el shrinker | Nunca llegó al DEX. Lo inalcanzable solo queda listado en usage.txt, con su nombre; mapping.txt solo registra como R8$$REMOVED$$CLASS$$n las clases cuyo código sobrevive inlineado en otra. Como mucho se sabe que existía y cómo se llamaba, no qué hacía |
«R8 y ProGuard → §6.2 · Mapeo de clase» |
| La separación de métodos inlineados | El cuerpo está duplicado en cada llamador. mapping.txt recupera la pila de llamadas para trazas, no la estructura del código |
«Sección 3.2 · Qué recupera exactamente, y qué no» |
| Las clases fusionadas | Class merging produce una clase con los miembros de varias. Deshacerlo exige decidir qué campo era de cuál | «R8 y ProGuard → §3 · Los tres trabajos, que no son el mismo» |
| Comentarios y formato del fuente | Nunca estuvieron en el binario | — |
| La semántica de los nombres borrados | Calculadora no está en ninguna parte. Solo se puede proponer un nombre, con confianza |
«Sección 5.3 · Por qué el techo es bajo» |
Un matiz sobre el -dontobfuscate: recuperar los nombres de locales no es tarea de
deofuscación, sino de compilación. Si el proyecto se compiló sin LocalVariableTable, ni
siquiera un build sin ofuscar los habría tenido. En el muestrario, el classes.dex de
com.looker.droidify_710.apk tiene 28.006 code_item y 1.113 debug_info_item, pero solo
21 métodos van sin información de depuración: los demás comparten esos items, que R8
codifica por PC («R8 y ProGuard → §9.3 · Otras huellas»). Lo que falta no son las
líneas, sino los nombres: ninguno de los 1.113 lleva nombres de parámetros ni
DBG_START_LOCAL. El recuento completo está en «Formato DEX → §12 · debug_info_item».
9. Coste frente a beneficio
Reuniendo todo lo anterior. El esfuerzo va en meses-persona cuando hay una estimación; el resto se marca con una escala relativa.
| Técnica | Esfuerzo | Aplica a | Qué devuelve | Riesgo de estar mal |
|---|---|---|---|---|
retrace con mapping.txt |
Bajo | Solo quien tiene el mapping | Nombres exactos, firmas, líneas y la pila de inlining | Nulo |
| Nombre canónico de recursos | Muy bajo | Cualquier APK con nombres colapsados |
Un APK editable donde antes no lo era |
Nulo |
| Recursos desde manifiesto y framework | Bajo | Cualquier APK |
Decenas de nombres, los más importantes | Nulo |
Reconstrucción de rutas de res/ |
Bajo | Cualquier APK con rutas acortadas |
Toda la organización por tipo y configuración | Nulo |
| Recursos desde el valor | Medio | Aplicaciones con muchos string |
Miles de nombres plausibles | Medio: hay que marcarlo |
| Identificación de bibliotecas por forma | 1,0–1,8 m-p | Cualquier APK |
Nombres reales, con versión. El 91,3 % de DeGuard no mide esto («sección 5.2 · Los trabajos publicados y sus cifras reales») | Bajo si se exige cero falsos positivos con confianza alta |
| Descifrado de cadenas | 2,7–5,0 m-p | El ~5 % del malware, casi nada del catálogo legítimo | Cadenas exactas, o «desconocido» | Nulo donde converge |
| Inferencia heurística de nombres | 3,4–6,1 m-p | Cualquier APK |
Nombres probables. DeGuard: 79,1 % | Alto: el 21 % restante induce a error |
| Desempaquetado de packers | Sin fondo | 0,59 % de Google Play | El DEX original |
Fuera de alcance («03 → §9 · Por qué desempaquetar está fuera del alcance») |
Las cuatro primeras filas comparten dos propiedades: coste bajo y riesgo nulo. Son exactamente las que merece la pena hacer primero, y no por casualidad. Ordenar la tabla por «beneficio dividido por riesgo» reproduce esa decisión sin necesidad de conocerla.
Y hay una fila que merece atención especial: la identificación de bibliotecas por huella estructural está en la mitad heurística de la tabla pero devuelve nombres reales, no inferidos. Buena parte del código de una aplicación es biblioteca de terceros ofuscada junto al resto, y esa biblioteca existe sin ofuscar en Maven Central. Es, por unidad de esfuerzo, lo más rentable de todo el bloque heurístico.
Fuentes
- Bichsel, Raychev, Tsankov, Vechev — «Statistical Deobfuscation of Android Applications», ACM CCS 2016 — https://www.sri.inf.ethz.ch/publications/bichsel2016statistical Consultado el 12 de agosto de 2026. De aquí salen el 79,1 % de nombres recuperados y el 91,3 % de predicción de bibliotecas de la «sección 5.2 · Los trabajos publicados y sus cifras reales». ⚠️ La página escribe «Peter Tsankov»; otras fuentes lo dan como «Petar Tsankov». El PDF, https://files.sri.inf.ethz.ch/website/papers/deguard.pdf, consultado el 25 de septiembre de 2026: de aquí sale que el 91,3 % es la precisión media al predecir 133 bibliotecas identificadas por nombre de paquete, con el mismo modelo («sección 5.2 · Los trabajos publicados y sus cifras reales»).
- Raychev, Vechev, Krause — «Predicting Program Properties from "Big Code"», POPL 2015 — https://www.sri.inf.ethz.ch/publications/raychev2015predicting Consultado el 12 de agosto de 2026. De aquí salen el 63 % de nombres y el 81 % de tipos de la «sección 5.2 · Los trabajos publicados y sus cifras reales».
- Nice2Predict — http://www.nice2predict.org/ y https://github.com/eth-sri/Nice2Predict
Consultados el 12 de agosto de 2026. De aquí sale el marco de predicción estructurada de
la «sección 5.2 · Los trabajos publicados y sus cifras reales». El 24 de septiembre,
nice2predict.orgrechaza la conexión; el repositorio de GitHub sigue en pie. - apk-deguard.com — http://apk-deguard.com/
Consultado el 12 de agosto de 2026: respondía, ofrecía subida de
APKde hasta 256 MB y anunciaba una nueva versión. El 24 de septiembre ya no contesta ni por HTTP ni por HTTPS. - Lacomis et al. — «DIRE: A Neural Approach to Decompiled Identifier Naming», ASE 2019 — https://arxiv.org/abs/1909.09029; Chen et al. — «Augmenting Decompiler Output with Learned Variable Names and Types», USENIX Security '22 — https://arxiv.org/abs/2108.06363; Dramko et al., TOSEM 2022 — https://cmustrudel.github.io/papers/dramko2022tosem.pdf Consultados el 12 de agosto de 2026. De aquí sale la tabla de trabajos análogos sobre binarios nativos de la «sección 5.2 · Los trabajos publicados y sus cifras reales».
- Glanz, Müller, Baumgärtner, Reif, Amann, Anthonysamy, Mezini — «Hidden in Plain Sight: Obfuscated Strings Threatening Your Privacy», ASIA CCS 2020 — https://arxiv.org/abs/2002.04540 Consultado el 12 de agosto de 2026. De aquí sale StringHound, el factor de 30× y la evaluación sobre 100.000 aplicaciones de la «sección 6.3 · Qué lo hace caro».
- Dong et al. — «Understanding Android Obfuscation Techniques», SecureComm 2018 — https://diaowenrui.github.io/paper/securecomm18-dong.pdf Consultado el 12 de agosto de 2026. De aquí salen las cifras de prevalencia del cifrado de cadenas de la «sección 6.1 · Por qué es lo que más rinde».
aapt2 optimize --help, build-tools 37.0.0 — ejecutado el 12 de agosto de 2026. De aquí salen--save-obfuscation-map,--collapse-resource-names,--shorten-resource-pathsy las directivasno_collapse/no_path_shortende la «sección 4.3 · El mapa oficial, cuando existe».- R8 —
doc/retrace.md— https://r8.googlesource.com/r8/+/refs/heads/main/doc/retrace.md Consultado el 12 de agosto de 2026. De aquí salen la semántica desynthesizedy derewriteFrameque usan los pasos 6 y 7 del algoritmo de la «sección 3.1 · El algoritmo». - Troubleshoot app optimization —
https://developer.android.com/topic/performance/app-optimization/troubleshoot-the-optimization
Consultado el 12 de agosto de 2026. De aquí sale que
mapping.txtse sobrescribe en cada build y que viaja dentro delAAB(«sección 3.4 · El problema real: nadie tiene el mapping.txt»). - jadx — https://github.com/skylot/jadx
Consultado el 12 de agosto de 2026. Su deofuscación es renombrado de identificadores
(
--deobf,--deobf-min,--deobf-max,--deobf-cfg-file,--deobf-res-name-source,--mappings-path), no descifrado de cadenas. ⚠️ SuREADMEno documenta ninguna función de descifrado de cadenas y no se le debe atribuir. Consultado de nuevo el 25 de septiembre de 2026: las mismas opciones figuran en elREADMEde la etiquetav1.5.6, del 10 de julio de 2026, la última versión publicada. - Redex —
libredex/RedexResources.h— https://github.com/facebook/redex/blob/main/libredex/RedexResources.h Consultado el 25 de septiembre de 2026. De aquí sale la constanteRESOURCE_NAME_REMOVEDcon el valor(name removed), que no aparece en el binario deaapt237.0.0 («sección 4.1 · Qué queda de un resources.arsc ofuscado»). - R8 9.2.4-dev de build-tools 37 ejecutado localmente —
java -cp build-tools/37.0.0/lib/d8.jar com.android.tools.r8.R8Consultado el 25 de septiembre de 2026. De aquí sale que una clase inalcanzable solo va ausage.txty queR8$$REMOVED$$CLASS$$0aparece para una clase inlineada («sección 8 · El límite teórico»).