Pipeline de modificación
1. Qué es y por qué existe
Modificar un APK ajeno y redistribuirlo tiene implicaciones de propiedad intelectual, de
contrato de licencia y de responsabilidad sobre el binario resultante, y son problema de quien lo
hace. Este documento describe el procedimiento técnico; el marco legal aplicable depende de la
jurisdicción, de la licencia de la aplicación y del uso, y no se trata aquí.
Los casos de uso legítimos dominantes son cuatro y ninguno pasa por redistribuir:
- Análisis. Instrumentar una aplicación para observar qué hace: añadir trazas, desactivar el
pinning de certificados en una copia propia de laboratorio para poder ver el tráfico, marcar
debuggable. - Accesibilidad. Corregir contrastes, tamaños de fuente o etiquetas de un
layouten una aplicación que no se mantiene. - Investigación de seguridad. Reproducir una vulnerabilidad, construir la prueba de concepto, verificar un parche.
- Binarios propios sin fuente. El caso más común en la industria y el menos glamuroso: la
aplicación es tuya, el
APKde release está firmado y publicado, y el repositorio se perdió con el proveedor. ElAPKes el único artefacto que queda.
Y una frontera dura, que es la misma en todo el corpus: aquí no se documenta cómo saltarse PairIP, Play Integrity, un packer comercial ni una comprobación de integridad en ejecución. La sección 10 explica por qué esas aplicaciones no son modificables de forma realista, que es información útil justamente para no perder el tiempo.
Lo que este documento aporta y no está en 30-herramientas es el encadenado: qué paso va antes de cuál, qué se pierde en cada uno, y cuál es el punto de fallo característico de cada eslabón.
2. El pipeline canónico
┌───────────────────────────────────────────────────────────────────────┐
│ 0 · ANALIZAR primero pipeline de análisis, pasos 0 a 4 │
│ ¿packer? ¿integridad en ejecución? ¿lógica en nativo? → parar │
├───────────────────────────────────────────────────────────────────────┤
│ 1 · DESEMPAQUETAR apktool d · APKEditor decode · unzip │
│ recursos a XML de texto y DEX a smali, o en crudo │
├───────────────────────────────────────────────────────────────────────┤
│ 2 · EDITAR recursos ──▶ §4 · código smali ──▶ §5 │
├───────────────────────────────────────────────────────────────────────┤
│ 3 · REEMPAQUETAR apktool b (invoca aapt2) · APKEditor build │
│ sale SIN firma y SIN alinear. El ZIP ya no es el de entrada (§6) │
├───────────────────────────────────────────────────────────────────────┤
│ 4 · ALINEAR zipalign -P 16 -f 4 entrada.apk salida.apk │
├───────────────────────────────────────────────────────────────────────┤
│ 5 · FIRMAR apksigner sign --ks … ← SIEMPRE el último │
├───────────────────────────────────────────────────────────────────────┤
│ 6 · VERIFICAR apksigner verify --verbose · zipalign -c │
├───────────────────────────────────────────────────────────────────────┤
│ 7 · INSTALAR adb install · adb install-multiple │
└───────────────────────────────────────────────────────────────────────┘
Tres reglas del diagrama, y las tres se incumplen constantemente:
- El paso 0 no es opcional. Modificar una aplicación con packer produce un
APKque instala y no arranca, y el diagnóstico cuesta horas. Se descarta en dos segundos con la sección 6 de Pipeline de análisis. zipalignva antes deapksigner, y nada toca el fichero después. El porqué es estructural y está en la sección 7.- Ninguna de las herramientas de desempaquetado cierra el ciclo. apktool no firma por diseño;
APKEditor tampoco;
aapt2menos. Los pasos 4 y 5 son siempre manuales (Firma y empaquetado).
3. La bifurcación fundamental
Lo primero que hay que decidir es qué se va a tocar, porque editar recursos y editar código son dos problemas de dificultad muy distinta.
| Recursos | Código | |
|---|---|---|
| Qué se edita | AXML y resources.arsc |
classes*.dex |
| Representación intermedia | XML de texto, o JSON | smali |
| ¿Hay inverso exacto? | Sí con ARSCLib/APKEditor; con aapt2, no |
Sí: smali es el inverso de baksmali |
| Qué valida el cambio | El compilador de recursos, al reconstruir | El verificador de ART, al cargar la clase |
| Cuándo falla | Al reempaquetar: se ve enseguida | Al ejecutar el código parcheado: se ve tarde |
| Dificultad típica | Baja | Alta |
La asimetría tiene una causa concreta. Un recurso es un dato: cambiar el texto de un string o el
color de un drawable no puede producir un programa incoherente, y si el reempaquetado sale, sale.
Una instrucción de bytecode es parte de un programa con invariantes de tipo por registro que
ART comprueba al cargar la clase: un parche que deje un registro con un int en una rama y una
referencia en la otra produce un fallo de verificación en el momento de cargar la clase, no en la
línea editada, con una traza que apunta a otro sitio. La sección 8.3 de
Desensambladores smali lo desarrolla.
De ahí la regla de trabajo: si el objetivo se puede conseguir tocando solo recursos o solo el
manifiesto, se hace así. Muchos cambios que parecen de código no lo son —un texto, un color, un
endpoint que estaba en un string, un flag booleano en res/values/bools.xml, la configuración de
seguridad de red— y ahorran el paso 5 entero.
4. Editar recursos
4.1 Qué se puede cambiar
| Objetivo | Dónde vive | Dificultad |
|---|---|---|
| Textos, colores, dimensiones, booleanos | resources.arsc: tipos string, color, dimen, bool |
Trivial |
layout, drawable vectorial, menu |
AXML bajo res/ |
Baja |
| Iconos y bitmaps | Ficheros bajo res/ |
Trivial: se sustituye el fichero |
debuggable, usesCleartextTraffic, networkSecurityConfig |
AndroidManifest.xml (AXML) |
Baja |
Permisos, componentes, exported |
AndroidManifest.xml |
Baja, con consecuencias grandes |
versionCode / versionName |
AndroidManifest.xml |
Trivial |
| Nombre de paquete | Manifiesto, tabla y referencias del DEX |
Alta: no es un cambio de recurso |
La última fila es la trampa clásica. Renombrar el paquete no es editar un atributo: aapt2 y
apktool ofrecen --rename-manifest-package, pero el DEX sigue conteniendo referencias al paquete
original en clases, ContentProvider authorities, intent-filter y ficheros de configuración de
bibliotecas de terceros. El resultado instala en paralelo a la original y falla en el primer sitio
donde alguien construya un nombre por concatenación.
4.2 Las dos vías, y por qué no dan lo mismo
apktool decodifica los recursos a XML de texto y, al reconstruir, llama a aapt2 para que
los vuelva a compilar. aapt2 no está escrito para reconstruir sino para construir desde
fuentes: optimiza PNG, normaliza valores, reordena entradas y resuelve referencias a su manera. El
resultado es funcionalmente parecido y byte a byte distinto, y hereda los fallos de aapt2 —el
clásico «resource … is private» al enlazar contra un recurso del framework marcado como privado,
que es legal en un binario ya construido y que aapt2 rechaza por diseño.
APKEditor, sobre ARSCLib, lee y escribe resources.arsc y AXML directamente. No hay
compilación intermedia, así que no hay nada que perder por el camino ni fallos de aapt2 que
heredar: lo que no se toca, no cambia.
El detalle de opciones de las dos está en Desempaquetado y reempaquetado, secciones 2 y 3. Lo que aquí importa es cuándo usar cuál:
| Situación | Herramienta |
|---|---|
| Solo hay que tocar código, los recursos dan igual | apktool con -r/--no-res: no decodifica recursos, así que aapt2 no interviene |
Hay que tocar recursos y el APK es normal |
Cualquiera de las dos |
| apktool falla al reconstruir | APKEditor: es el motivo por el que existe |
| Hay que garantizar que el resto sale idéntico | APKEditor decode -t raw: no decodifica, luego no puede perder |
Solo hay que cambiar un fichero de res/ que no es AXML |
unzip + zip, o directamente reescribir la entrada del ZIP |
Y el diagnóstico que ahorra más tiempo, ejecutable en dos órdenes: si apktool d falla, se prueba
con -r; si con -r funciona, el problema estaba en los recursos. Si falla con -s, estaba en el
código.
⚠️ No ejecutado localmente: apktool y APKEditor no están instalados en la máquina de referencia y estas reglas prohíben instalarlos. Las opciones citadas proceden de la documentación oficial de cada proyecto, consultada el 12 de agosto de 2026, y del documento de herramientas correspondiente. No se ha comprobado ninguna salida suya.
4.3 Lo que sí está ejecutado: el manifiesto sin reconstruir
Para leer y comprobar un manifiesto no hace falta nada más que el SDK, y merece la pena hacerlo antes y después de cualquier edición:
BT=~/Library/Android/sdk/build-tools/37.0.0
$BT/aapt2 dump xmltree app.apk --file AndroidManifest.xml \
| grep -E "debuggable|usesCleartextTraffic|networkSecurityConfig|exported"
aapt2 es aquí un oráculo: si el manifiesto reescrito produce el mismo xmltree que se
esperaba, la edición es correcta a nivel de formato.
5. Editar código en smali
5.1 El flujo
classes*.dex ──baksmali / apktool d──▶ smali/**/*.smali ──[editar]──▶
──smali / apktool b──▶ classes*.dex ──▶ ZIP ──▶ alinear ──▶ firmar
No existe ninguna herramienta que recompile a DEX el Java que salió de un decompilador, y no
por falta de ganas: ese Java no compila. Le faltan tipos, tiene referencias sintéticas, y un
ofuscador puede haber emitido identificadores que no son código Java válido. El smali, en cambio,
vuelve a DEX por construcción, instrucción a instrucción. Toda modificación real de una
aplicación pasa por aquí.
⚠️ No ejecutado localmente: baksmali, smali y apktool no están instalados. El
smalique aparece en esta sección procede del documento Desensambladores smali, cuya sección 5 lo transcribe a mano desde un volcado real dedexdumpejecutado en esta máquina. Las instrucciones y los registros son reales; los nombres de etiqueta son la convención de baksmali.
5.2 El problema de los registros
Es el que hace difícil la edición y merece entenderse antes de tocar nada. Dentro de un método hay un solo banco de registros, y los parámetros ocupan los últimos. Las dos directivas que lo declaran son alternativas:
.registers N N = total de registros, parámetros incluidos
.locals M M = registros que NO son parámetros
N = M + ins_size (ins_size cuenta `this` y cuenta DOS por cada long o double)
De ahí el error clásico: alguien necesita un registro más para su parche, ve .locals 2 y lo sube
a .locals 3 creyendo que ha añadido un registro al final. Lo que ha hecho es insertar uno:
como los parámetros viven al final del banco, subir el número de locales desplaza todos los p
hacia arriba. Con .locals 2 y tres registros de parámetros, p0 era v2; con .locals 3 pasa
a ser v3, y todas las referencias explícitas a v2, v3 y v4 del cuerpo apuntan ahora a otra
cosa.
Como baksmali escribe los parámetros con notación p, el desplazamiento es transparente para
ellos, y por eso el error pasa desapercibido hasta que revienta. Lo que se rompe son las
referencias que usan notación v sobre registros que resultaban ser parámetros, y el código copiado
de otro método.
Y hay dos restricciones más que se acumulan:
El techo de 4 bits. La mayoría de las instrucciones codifican sus registros en 4 bits: solo
alcanzan v0 a v15. Si el método ya usa dieciséis, subir .locals empuja los parámetros por
encima de v15 y todas las instrucciones que los referencian dejan de poder codificarse: hay
que reescribirlas a sus variantes /from16 o /16, que cambian de tamaño. La salida habitual es no
tocar el banco: guardar el registro que se va a usar en uno alto con move/16, usarlo y
restaurarlo. Funciona, y es tedioso.
La verificación de tipos de ART. ART comprueba al cargar que cada registro tiene un tipo
coherente en cada punto del programa, y en particular que donde dos ramas confluyen el tipo
coincide por los dos caminos. Un parche que deje un registro con un int en una rama y una
referencia en la otra no pasa la verificación. La opción --register-info de baksmali anota en
comentarios el estado de tipos de cada registro en cada instrucción, y es justo la información que
hace falta para no romper esto a ciegas.
5.3 Qué cambios son seguros
Baratos y seguros — no cambian el tamaño de la instrucción, no tocan registros, no alteran el flujo:
| Cambio | Cómo | Cuidado |
|---|---|---|
| Valor de una constante | const/4 v0, 0x0 → const/4 v0, 0x1 |
const/4 codifica un literal de 4 bits con signo: de −8 a 7. Fuera de ese rango hay que pasar a const/16 o const, que ocupan más — sigue siendo correcto porque el ensamblador recalcula offsets, pero deja de ser una sustitución de un carácter |
| Invertir una condición | if-eqz → if-nez, if-gez → if-ltz |
Mismo tamaño, mismo formato, efecto opuesto |
| Eliminar una llamada sin retorno útil | invoke-* a un método void → nop, o borrar la línea |
El ensamblador recalcula offsets y etiquetas |
| Redirigir un salto | Reapuntar la etiqueta a otra ya existente | Que la etiqueta destino exista y sea alcanzable |
Cambiar el contenido de un const-string |
Escribir el nuevo literal | El ensamblador añade la entrada al pool de strings |
Caros, y con efectos que arrastran:
| Cambio | Por qué arrastra |
|---|---|
| Cambiar la firma de un método | La firma es parte de su identidad en el DEX (method_id_item → proto_id_item). Cambiarla hace que todas las invocaciones desde todas las clases dejen de resolver, y esas invocaciones están repartidas por todos los classes*.dex, incluidos los que no se han desensamblado. Si además el método sobrescribe uno de una superclase o implementa una interfaz, rompe el enlace y ART lo detecta al cargar |
| Añadir un parámetro | Todo lo anterior, más que sube ins_size y desplaza el banco entero de registros (§5.2) |
| Añadir un campo | Hay que crear el .field y luego inicializarlo, lo que significa tocar <init> en todos los constructores, o <clinit> si es estático. Un iput sobre un campo inexistente es error de verificación al cargar |
| Añadir un método | Más fácil: un .method nuevo con .registers correcto suele bastar. Pero si hay que llamarlo desde otra clase, esa clase también se toca |
| Insertar código en medio de un método | El problema de §5.2 en estado puro |
5.4 La regla de oro del parcheo
Cuando el Java y el smali discrepan, el smali tiene razón. El Java del decompilador es una
hipótesis, y buscar en él el método a parchear puede llevar al método equivocado: R8 hace
inlining, outlining a métodos sintéticos compartidos y fusión de clases, así que un método que
en el fuente existía puede no aparecer, y uno que en el fuente tenía diez líneas puede salir con
doscientas. La localización del parche se hace sobre el desensamblado, aunque la comprensión se
haga sobre el Java.
6. Reempaquetar: qué se pierde
6.1 La salida nunca es byte-idéntica a la entrada
Se puede medir sin apktool y sin APKEditor. Sobre org.fossify.calculator_1.4.0.apk del corpus, un
ciclo de reempaquetado que preserva el método de compresión de cada entrada y su nombre, y que
solo cambia el contenido de un fichero:
# 1 · reempaquetar preservando compress_type, descartando la firma v1, cambiando una entrada
python3 reempaqueta.py entrada.apk reempaquetado.apk
ls -la entrada.apk reempaquetado.apk
-rw-r--r--@ 1 jj wheel 7047681 entrada.apk
-rw-r--r--@ 1 jj wheel 7009573 reempaquetado.apk
Treinta y ocho mil bytes menos sin haber quitado ninguna entrada. Comparando entrada a entrada el
APK de partida contra el final ya firmado:
entradas: original 1584 final 1584
solo en el original : 0
solo en el final : 0
contenido distinto : 1 ['assets/dexopt/baseline.profm'] ← el cambio pedido
método distinto : 0
tamaño comprimido distinto (mismo contenido): 245 de 1584
245 entradas de 1.584 tienen exactamente el mismo contenido y ocupan un número de bytes
distinto. No se ha tocado ninguna: lo único que ha pasado es que se han vuelto a comprimir con
una configuración de deflate diferente de la que usó el APK original. Ese es el suelo de lo que
un reempaquetado cambia aunque no se edite nada.
6.2 El inventario de lo que se pierde
| Qué | Se pierde porque | ¿Se preserva? |
|---|---|---|
El APK Signing Block |
No es una entrada del ZIP: vive entre los datos y el Central Directory, y ninguna herramienta de reempaquetado lo reproduce |
No: hay que volver a firmar (§7) |
| Las entradas de firma v1 | Sus digests ya no cuadran | No: se descartan y se regeneran al firmar |
| El relleno de alineación | Vive en el extra field del Local File Header y depende del offset de destino, que ya no es el de origen |
Se recalcula con zipalign (§7) |
| El orden de las entradas | Lo decide el escritor del ZIP |
Solo si lo preserva a propósito |
| Los tamaños comprimidos | Distinta configuración de deflate (§6.1) |
No de forma práctica |
| Las marcas de tiempo | Depende del escritor; AGP escribe 01-01-1981 01:01 en todas para que la build sea reproducible |
Sí, si se fijan explícitamente |
| El método de compresión por entrada | Lo decide el escritor si no se le dice otra cosa | Sí, y es obligatorio (§6.3) |
6.3 Lo que hay que preservar a mano, y por qué
Dos cosas, y equivocarse en cualquiera de ellas produce un APK que no instala o que instala y no
carga sus bibliotecas nativas:
1 · resources.arsc tiene que ir stored. Desde targetSdkVersion 30 es un requisito de
instalación, no una optimización. La comprobación es incómoda porque las herramientas locales no
la detectan: construyendo a propósito un APK con resources.arsc comprimido,
$BT/aapt2 dump badging comprimido.apk | head -1
$BT/zipalign -c -v 4 comprimido.apk | grep arsc
package: name='org.fossify.math' versionCode='10' versionName='1.4.0' …
3322768 resources.arsc (OK - compressed)
aapt2 lo lee sin protestar y zipalign -c responde (OK - compressed) y sale con 0. El fallo
solo aparece al instalar, como INSTALL_PARSE_FAILED_RESOURCES_ARSC_COMPRESSED
(Diagnóstico de fallos, sección 8). La comprobación hay que hacerla
a mano:
python3 -c "
import zipfile,sys; i=zipfile.ZipFile(sys.argv[1]).getinfo('resources.arsc')
print('OK stored' if i.compress_type==0 else 'FALLA: resources.arsc comprimido')" app.apk
2 · Las .so tienen que ir stored si extractNativeLibs es false. Si el manifiesto declara
android:extractNativeLibs="false", el sistema carga las bibliotecas directamente del APK sin
extraerlas, y para eso tienen que estar sin comprimir y alineadas. Un reempaquetado que las
comprima produce un APK que instala y muere en el primer System.loadLibrary. apktool anota esto
en el campo doNotCompress de apktool.yml justamente porque es información que la decodificación
pierde.
Y una tercera, menor pero visible: META-INF/ no de firma se conserva. Los *.version, los
services/ del mecanismo ServiceLoader y los *.kotlin_module no tienen nada que ver con la
firma y borrarlos rompe la reflexión y el ServiceLoader en tiempo de ejecución. Lo único que hay
que quitar es MANIFEST.MF, *.SF y *.RSA/*.DSA/*.EC.
7. Alinear y firmar
7.1 El orden, demostrado
zipalign antes de apksigner. Siempre. La documentación oficial de zipalign lo dice con
todas las letras, y se puede comprobar. Firmando una copia del corpus con v1+v2+v3 y pasándole
zipalign después:
$BT/apksigner verify --verbose firmado.apk | head -4
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
$BT/zipalign -p -f 4 firmado.apk roto.apk; echo "zipalign exit=$?"
$BT/apksigner verify --verbose roto.apk; echo "verify exit=$?"
zipalign exit=0
DOES NOT VERIFY
ERROR: JAR signer PROC.RSA: JAR signature META-INF/PROC.SF indicates the APK is signed using APK Signature Scheme v2 but no such signature was found. Signature stripped?
ERROR: JAR signer PROC.RSA: JAR signature META-INF/PROC.SF indicates the APK is signed using APK Signature Scheme v3 but no such signature was found. Signature stripped?
verify exit=1
zipalign devolvió 0: para él la operación fue un éxito. Y el mensaje de error acusa de un
stripping que no ha habido.
Repitiendo el mismo experimento sobre un APK firmado solo con v2 y v3, sin v1, el mensaje
cambia y despista aún más:
DOES NOT VERIFY
ERROR: Missing META-INF/MANIFEST.MF
Un fichero que nunca tuvo MANIFEST.MF, acusado de que le falta. La explicación: zipalign
reescribe el ZIP entrada por entrada a un fichero nuevo, con Central Directory y EOCD nuevos,
y el hueco donde vivía el APK Signing Block no se reproduce. Sin bloque de firma el
verificador cae al camino de v1, y lo primero que busca ahí es MANIFEST.MF.
| Orden | Resultado |
|---|---|
| construir → alinear → firmar | Correcto |
| construir → firmar → alinear | Firma destruida. Hay que volver a firmar |
| construir → firmar → tocar un fichero | Firma inválida (§7.2) |
alinear → firmar → zipalign -c |
Correcto: -c solo lee |
El corolario práctico: zipalign -c es seguro siempre; zipalign sin -c sobre un APK firmado
obliga a refirmar.
7.2 Un solo byte basta
Cambiando un byte dentro de resources.arsc de un APK firmado, sin mover un solo offset —el
fichero mide exactamente lo mismo antes y después—:
DOES NOT VERIFY
ERROR: APK Signature Scheme v3 signer #1: APK integrity check failed. CHUNKED_SHA256 digest mismatch. Expected: <655a9adc…cafe8d6c>, actual: <bf95b77b…39f57b21>
Ese es el mensaje que hay que reconocer: digest mismatch significa que el contenido cambió
después de firmar, mientras que Signature stripped? y Missing META-INF/MANIFEST.MF significan
que el bloque entero desapareció, casi siempre por un zipalign a destiempo. Son diagnósticos
distintos y llevan a arreglos distintos.
7.3 La firma original no se puede conservar, y no es culpa de las herramientas
Es la pregunta que todo el mundo hace, y la respuesta no es «las herramientas no saben»: es una imposibilidad criptográfica.
Una firma es Firma(clave_privada, digest(contenido)). Verificarla consiste en recalcular el
digest del contenido que se tiene delante y comprobar que la firma corresponde a ese digest
usando la clave pública del certificado. Si el contenido cambia, el digest cambia, y producir
una firma válida para el digest nuevo exige la clave privada, que no está en el APK —está en
el keystore del autor, y todo el mecanismo existe precisamente para que nadie más la tenga—.
Dicho al revés: si se pudiera conservar la firma tras modificar el contenido, la firma no serviría
para nada. No es una limitación que alguien pueda arreglar: es la propiedad que hace útil a la
firma. Cualquier herramienta que reescriba el ZIP —apktool, APKEditor, zipalign— la
invalida por construcción.
Lo único que se puede hacer es volver a firmar con otra clave, y eso cambia la identidad de la aplicación. Sobre el ciclo completo ejecutado en §6.1:
echo -n "original: "; $BT/apksigner verify --print-certs entrada.apk | grep -m1 "SHA-256 digest"
echo -n "modificado:"; $BT/apksigner verify --print-certs final.apk | grep -m1 "SHA-256 digest"
original: V2 Signer: certificate SHA-256 digest: affdb124d3f4720c2f98dbca9eacba0514fba4306e20a2786c861c3c0d6ff292
modificado:V3.0 Signer: certificate SHA-256 digest: 62f16ba8df8383834df518e180d6e49111c6c2951faa146955b02354f8f6e472
Y las consecuencias prácticas son cinco, todas inevitables:
- No se puede instalar encima de la original. El instalador rechaza la actualización con
INSTALL_FAILED_UPDATE_INCOMPATIBLE. Hay que desinstalar primero, lo que borra los datos de la aplicación. - Se pierden los permisos de nivel
signature. Los que se conceden a aplicaciones firmadas con la misma clave que quien los declara dejan de concederse. - Se rompe
sharedUserIdcon otras aplicaciones del mismo autor, por lo mismo. - Play deja de reconocerla. No hay actualizaciones desde la tienda, y cualquier servicio de terceros registrado por huella de certificado —Maps, Firebase, login social, notificaciones push— deja de autenticar.
- La aplicación puede notarlo. Una comprobación de integridad en ejecución que compare la huella de su propio certificado contra una constante detecta el cambio (§10).
7.4 La receta completa, ejecutada
Pasos 3 a 6 del pipeline, sobre una copia del corpus, con salida real:
BT=~/Library/Android/sdk/build-tools/37.0.0
# 4 · alinear, con la regla de 16 KB para las .so
$BT/zipalign -P 16 -f 4 reempaquetado.apk alineado.apk
$BT/zipalign -c -P 16 4 alineado.apk; echo "check exit=$?"
# 5 · firmar — v2+v3; v1 solo si minSdk < 24
$BT/apksigner sign --ks proc.p12 --ks-type PKCS12 --ks-key-alias proc \
--ks-pass pass:procpass --key-pass pass:procpass --min-sdk-version 26 \
--v1-signing-enabled false --v2-signing-enabled true --v3-signing-enabled true \
--out final.apk alineado.apk
# 6 · verificar
$BT/apksigner verify --verbose final.apk
check exit=0
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 for SourceStamp: false
Dos advertencias sobre esa orden. --ks-pass pass:... no se usa fuera de una demostración: la
contraseña queda en el historial del shell y en la tabla de procesos; en cualquier uso real se pasa
env: o file:. Y firmar solo con v1 hoy es no firmar: sobre el mismo APK, con
targetSdk 36, apksigner verify responde
ERROR: Target SDK version 36 requires a minimum of signature scheme v2, y el APK no se instala
aunque la firma v1 sea criptográficamente correcta.
8. Instalar y probar
⚠️ No ejecutado localmente: no hay ningún dispositivo Android ni emulador conectado al entorno de referencia, y
adbexiste en~/Library/Android/sdk/platform-tools/adbpero no está en elPATH. La sintaxis de esta sección procede de la documentación oficial deadb, consultada el 12 de agosto de 2026. Ninguna de estas órdenes se ha ejecutado.
ADB=~/Library/Android/sdk/platform-tools/adb
$ADB install final.apk # instalación limpia
$ADB install -r final.apk # reinstalar conservando datos: exige la MISMA firma
$ADB install-multiple base.apk config.arm64_v8a.apk config.es.apk config.xxhdpi.apk
$ADB uninstall com.ejemplo.app # borra los datos
Tres notas que ahorran tiempo:
install -rsolo funciona si la firma coincide con la de la versión instalada. UnAPKrefirmado exige desinstalar primero, con la pérdida de datos que eso implica (§7.3).install-multiplees la vía para splits sin fusionarlos: todos deben compartirpackageName,versionCodey clave de firma, o la instalación falla entera (Fusión de splits, sección 8).- El error real está en
logcat, no en la salida deadb install. El instalador devuelve un códigoINSTALL_*seco; la causa concreta —qué entrada, qué certificado, qué split falta— la escribePackageManageren el log del sistema.
Y la prueba de humo mínima después de instalar: arrancar la aplicación, ejercitar la
funcionalidad parcheada, y mirar logcat aunque parezca que funciona. Un DEX editado que no
supere la verificación de ART falla al cargar la clase, no al ejecutar la línea editada: la
aplicación arranca bien y muere al tocar la pantalla parcheada, con una traza que apunta a otro
sitio (§5.2).
9. Los puntos de fallo, paso a paso
Resumen para consulta. Cada síntoma está desarrollado en Diagnóstico de fallos, con su causa raíz y su arreglo.
| Paso | Falla característico | Se ve como | Detalle |
|---|---|---|---|
| 0 · analizar | Packer, integridad en ejecución, lógica en nativo | Todo funciona hasta que la aplicación arranca y se cierra | §10 y doc. 04 §9 |
| 1 · desempaquetar | Framework del fabricante no instalado | Los recursos no decodifican | doc. 04 §6 |
| 1 · desempaquetar | resources.arsc que no parsea |
Could not decode arsc file |
doc. 04 §6, §8 |
| 2 · editar | Banco de registros desplazado | Verificación de ART al cargar la clase |
§5.2, doc. 04 §9 |
| 3 · reempaquetar | aapt2 rechaza un recurso privado |
Falla el build, no se produce APK |
§4.2, doc. 04 §6 |
| 3 · reempaquetar | resources.arsc sale comprimido |
Nada local lo detecta; falla al instalar | §6.3, doc. 04 §8 |
| 3 · reempaquetar | .so comprimidas con extractNativeLibs=false |
Instala y muere en System.loadLibrary |
§6.3 |
| 4 · alinear | Alineación de 16 KB no cumplida | zipalign -c -P 16 4 sale con 1 |
doc. 04 §5 |
| 5 · firmar | Orden invertido | Signature stripped? / Missing META-INF/MANIFEST.MF |
§7.1 |
| 5 · firmar | Solo v1 | requires a minimum of signature scheme v2 |
§7.4 |
| 6 · verificar | Se editó algo después de firmar | CHUNKED_SHA256 digest mismatch |
§7.2 |
| 7 · instalar | Firma distinta de la instalada | INSTALL_FAILED_UPDATE_INCOMPATIBLE |
§7.3, doc. 04 §2 |
| 7 · instalar | Splits con firmas distintas | INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES |
doc. 04 §2 |
10. Qué NO se puede modificar de forma realista
Tres familias. En las tres, el pipeline de este documento produce un APK que instala
perfectamente y no funciona, y esa es la parte cara: el fallo no aparece en ninguno de los seis
pasos, aparece en ejecución.
1 · Aplicaciones con packer. El classes.dex que se desensambla no es el código de la
aplicación: es un stub que descifra y carga el payload real en tiempo de ejecución. Se puede
parchear el stub con toda la corrección del mundo y no se está tocando la lógica. La detección es
barata —sección 6.4 de Pipeline de análisis— y el modelo de
funcionamiento está en Packers y protectores.
Extraer el payload está fuera del alcance de este corpus.
2 · Aplicaciones con comprobación de integridad en ejecución. La aplicación comprueba, desde
dentro, que sigue siendo la que se publicó: la huella SHA-256 de su propio certificado leída con
PackageManager.getPackageInfo(..., GET_SIGNING_CERTIFICATES), el CRC de una entrada concreta del
ZIP, el digest del classes.dex, o una atestación remota como Play Integrity. Refirmar cambia
la huella (§7.3), así que estas comprobaciones fallan por construcción, y el síntoma típico es
que la aplicación arranca y se cierra sin traza útil, o que funciona en modo degradado. Ver
Anti-análisis y hardening. Neutralizar estas
comprobaciones está fuera del alcance de este corpus.
3 · Aplicaciones con la lógica en nativo. Si lo que hay que cambiar está en un .so, el smali
no llega: hay que editar ELF, lo que significa parchear instrucciones de máquina, mantener el
tamaño de los segmentos y no romper las reubicaciones. Es un problema de otro orden de dificultad y
de otras herramientas (Análisis binario y estático).
Mover lógica a nativo es, de hecho, una técnica deliberada para escapar del alcance de este
documento.
Y un cuarto caso que no es una protección sino una consecuencia del modelo de distribución: una aplicación instalada desde Play no es un fichero, sino un conjunto de splits. Modificarla exige o bien parchear el split correcto y reinstalar el conjunto entero con la misma clave, o bien fusionarlos antes — y la fusión tiene sus propios modos de fallo, en Fusión de splits.
Fuentes
Todas las URL se consultaron el 12 de agosto de 2026.
- zipalign — https://developer.android.com/tools/zipalign · la regla de alinear antes de
firmar (§7.1), las opciones
-c,-fy-P 16(§7.4) y el hecho de que las entradas comprimidas se marcan(OK - compressed)(§6.3). - apksigner — https://developer.android.com/tools/apksigner · la sintaxis de
signyverifyde §7.4, las opciones--ks-pass,--min-sdk-versiony--vN-signing-enabled, y la advertencia de que cualquier cambio posterior a la firma la invalida. - Behavior changes: Apps targeting Android 11 —
https://developer.android.com/about/versions/11/behavior-changes-11 · la obligación de que
resources.arscvaya sin comprimir y alineado a 4 bytes desdetargetSdk30 (§6.3). <application>— https://developer.android.com/guide/topics/manifest/application-element · el significado deandroid:extractNativeLibsy su efecto sobre la compresión de las.so(§6.3).- adb — https://developer.android.com/tools/adb · la sintaxis de
install,install -r,install-multipleyuninstallde §8, no ejecutada. - Apktool — CLI Parameters — https://apktool.org/docs/cli-parameters/ · las opciones
-r/--no-res,-s/--no-src,--copy-originaly--no-crunchde §4.2, no ejecutadas. - Apktool — FAQ — https://apktool.org/docs/faq/ · la declaración de que apktool construye
APKsin firmar y de que--copy-originalsolo sirve para el esquema v1 (§4.2, §6.2). - APKEditor — README — https://raw.githubusercontent.com/REAndroid/APKEditor/master/README.md
· los modos de decodificación
xml,json,rawysigy la independencia deaapt2de §4.2, no ejecutados. - Ejecuciones locales sobre el corpus — 12 de agosto de 2026 con
build-tools37.0.0 (zipalign,apksigner0.9,aapt22.20-15087165), OpenJDK 21.0.10 (keytool) y Python 3.12.10 sobre macOS Darwin 25.5.0. De aquí salen todas las salidas pegadas de §4.3, §6.1, §6.3, §7.1, §7.2 y §7.4, ejecutadas sobre copias deorg.fossify.calculator_1.4.0.apken el scratchpad. En ningún momento se ha escrito en el corpus. - Documentos de este corpus — Desensambladores smali
(§5.2 y §5.3: aritmética de
.registers/.locals, el techo de 4 bits, la verificación deARTy el catálogo de cambios seguros), Desempaquetado y reempaquetado (§4.2: la dependencia deaapt2de apktool y la independencia de APKEditor), Alineación y zipalign (§7.1) y Esquemas v1, v2 y v3 (§7.3).