Pipeline de modificación

referencia técnica · Revisado el 12 de agosto de 2026

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 layout en 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 APK de release está firmado y publicado, y el repositorio se perdió con el proveedor. El APK es 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:

  1. El paso 0 no es opcional. Modificar una aplicación con packer produce un APK que 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.
  2. zipalign va antes de apksigner, y nada toca el fichero después. El porqué es estructural y está en la sección 7.
  3. Ninguna de las herramientas de desempaquetado cierra el ciclo. apktool no firma por diseño; APKEditor tampoco; aapt2 menos. 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 smali que aparece en esta sección procede del documento Desensambladores smali, cuya sección 5 lo transcribe a mano desde un volcado real de dexdump ejecutado 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, 0x0const/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-eqzif-nez, if-gezif-ltz Mismo tamaño, mismo formato, efecto opuesto
Eliminar una llamada sin retorno útil invoke-* a un método voidnop, 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_itemproto_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:

  1. 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.
  2. 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.
  3. Se rompe sharedUserId con otras aplicaciones del mismo autor, por lo mismo.
  4. 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.
  5. 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 adb existe en ~/Library/Android/sdk/platform-tools/adb pero no está en el PATH. La sintaxis de esta sección procede de la documentación oficial de adb, 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 -r solo funciona si la firma coincide con la de la versión instalada. Un APK refirmado exige desinstalar primero, con la pérdida de datos que eso implica (§7.3).
  • install-multiple es la vía para splits sin fusionarlos: todos deben compartir packageName, versionCode y 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 de adb install. El instalador devuelve un código INSTALL_* seco; la causa concreta —qué entrada, qué certificado, qué split falta— la escribe PackageManager en 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.

  1. zipalign — https://developer.android.com/tools/zipalign · la regla de alinear antes de firmar (§7.1), las opciones -c, -f y -P 16 (§7.4) y el hecho de que las entradas comprimidas se marcan (OK - compressed) (§6.3).
  2. apksigner — https://developer.android.com/tools/apksigner · la sintaxis de sign y verify de §7.4, las opciones --ks-pass, --min-sdk-version y --vN-signing-enabled, y la advertencia de que cualquier cambio posterior a la firma la invalida.
  3. Behavior changes: Apps targeting Android 11 — https://developer.android.com/about/versions/11/behavior-changes-11 · la obligación de que resources.arsc vaya sin comprimir y alineado a 4 bytes desde targetSdk 30 (§6.3).
  4. <application> — https://developer.android.com/guide/topics/manifest/application-element · el significado de android:extractNativeLibs y su efecto sobre la compresión de las .so (§6.3).
  5. adb — https://developer.android.com/tools/adb · la sintaxis de install, install -r, install-multiple y uninstall de §8, no ejecutada.
  6. Apktool — CLI Parameters — https://apktool.org/docs/cli-parameters/ · las opciones -r/--no-res, -s/--no-src, --copy-original y --no-crunch de §4.2, no ejecutadas.
  7. Apktool — FAQ — https://apktool.org/docs/faq/ · la declaración de que apktool construye APK sin firmar y de que --copy-original solo sirve para el esquema v1 (§4.2, §6.2).
  8. APKEditor — README — https://raw.githubusercontent.com/REAndroid/APKEditor/master/README.md · los modos de decodificación xml, json, raw y sig y la independencia de aapt2 de §4.2, no ejecutados.
  9. Ejecuciones locales sobre el corpus — 12 de agosto de 2026 con build-tools 37.0.0 (zipalign, apksigner 0.9, aapt2 2.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 de org.fossify.calculator_1.4.0.apk en el scratchpad. En ningún momento se ha escrito en el corpus.
  10. Documentos de este corpusDesensambladores smali (§5.2 y §5.3: aritmética de .registers/.locals, el techo de 4 bits, la verificación de ART y el catálogo de cambios seguros), Desempaquetado y reempaquetado (§4.2: la dependencia de aapt2 de apktool y la independencia de APKEditor), Alineación y zipalign (§7.1) y Esquemas v1, v2 y v3 (§7.3).