Deofuscación
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).
Alcance. El descifrado de cadenas (sección 6) 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 lo desarrolla.
2. La taxonomía en cuatro tipos
El problema se deja clasificar en cuatro tipos, y la clasificación útil es la que cruza cuánto cuesta cada uno con qué garantía de exactitud ofrece. La división no es administrativa: esa garantía es lo que decide si el resultado se puede presentar como un hecho o solo como una sugerencia. Los dos primeros tipos son deterministas y baratos; los dos últimos, caros, y solo uno de ellos devuelve algo que se pueda afirmar sin matices.
| # | 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, sección 6, 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) |
| 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, sección 9.1). 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 este corpus. 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 corpus. Con --collapse-resource-names, 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. DexGuard hace
otra cosa: sustituye cada nombre por su propio resource ID en decimal
(Ofuscadores comerciales, sección 2.9).
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 corpus 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 corpus:
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.
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 también el más caro de los cuatro: el único que no tiene ninguna copia de lo borrado a la que acudir.
5.1 Las señales disponibles
Todas salen del suelo descrito en Ofuscadores comerciales, sección 8: 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, sección 5.2) |
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 corpus 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 juntas confirman lo que dice la tabla de la sección 5.1: la identificación de
bibliotecas (91,3 %) va mejor que la inferencia de nombres (79,1 %), porque la primera
recupera nombres reales y la segunda los adivina. 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»; ⚠️ no se ha probado si la deofuscación funciona de extremo a extremo.
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, enunciada
en corto: 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: cobertura y precisión se dan por separado, sin mezclarlas.
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. Se deja medir sin ambigüedad: sobre dos versiones consecutivas de la misma aplicación, qué fracción de los símbolos que no cambiaron recibe el mismo nombre inferido.
Hay además una oportunidad concreta en el corpus 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, sección 10.3). Y son
software libre: su código fuente está publicado. Son, por tanto, los únicos casos del
corpus 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, sección 9). 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: 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, el resultado 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: 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 corpus:
| 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, sección 12; R8 y ProGuard, sección 6 |
| 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, sección 9; R8 y ProGuard, sección 8, punto 5 |
| Código eliminado por el shrinker | Nunca llegó al DEX. mapping.txt lo registra como R8$$REMOVED$$CLASS$$n: se sabe que existía y cómo se llamaba, no qué hacía |
R8 y ProGuard, sección 6.2 |
| 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 |
| 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, sección 3 |
| 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 |
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 corpus se mide: el
com.looker.droidify_710.apk tiene 28.006 code_item y solo 1.113 debug_info_item; el
96 % de los métodos no lleva información de depuración de ningún tipo. Eso no lo hizo el
ofuscador.
9. Coste frente a beneficio
Reuniendo todo lo anterior. El esfuerzo va en una escala relativa; el riesgo es la probabilidad de que lo que devuelve la técnica esté mal.
| 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 | Alto | Cualquier APK |
Nombres reales, con versión. DeGuard: 91,3 % | Bajo si se exige cero falsos positivos con confianza alta |
| Descifrado de cadenas | Muy alto | El ~5 % del malware, casi nada del catálogo legítimo | Cadenas exactas, o «desconocido» | Nulo donde converge |
| Inferencia heurística de nombres | Muy alto | 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, sección 9) |
Las cuatro primeras filas comparten dos propiedades: coste bajo y riesgo nulo. Ordenar la tabla por «beneficio dividido por riesgo» las deja arriba, y ese orden es la recomendación práctica: agotarlas antes de tocar nada de la mitad de abajo.
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, y con la mejor cifra publicada de todas (91,3 %). 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. ⚠️ La página escribe «Peter Tsankov»; otras fuentes lo dan como «Petar Tsankov».
- 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.
- 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.
- apk-deguard.com — http://apk-deguard.com/
Consultado el 12 de agosto de 2026: responde, ofrece subida de
APKde hasta 256 MB y anuncia una nueva versión. ⚠️ No se ha probado una deofuscación real de extremo a extremo. - 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.
- 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.
- 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.
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.- 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. - 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). - 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. - Mediciones propias sobre el corpus — ejecutadas el 12 de agosto de 2026 sobre
~/corpus-apkconbuild-tools37.0.0 (aapt2) y Python 3.12. De aquí salen: la unicidad de(name removed)y de0_resource_name_obfuscateden losresources.arsc(secciones 4.1 y la implicación final), la lista de tipos supervivientes de WhatsApp (sección 4.1), las tres formas de rutas deres/de la sección 7, y el recuento dedebug_info_itemde la sección 8.