Smali - Descompilación/[Modificación]/Compilación

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks

A veces es interesante modificar el código de la aplicación para acceder a información oculta para ti (quizá contraseñas bien ofuscadas o flags). Entonces, puede ser útil descompilar el apk, modificar el código y recompilarlo.

Referencia de opcodes: http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

Método rápido

Usando Visual Studio Code y la extensión APKLab, puedes descompilar automáticamente, modificar, recompilar, firmar e instalar la aplicación sin ejecutar ningún comando.

Otro script que facilita mucho esta tarea es https://github.com/ax/apk.sh

Split APKs / App Bundles

Los targets modernos suelen distribuirse como split APKs (base.apk + split_config.*.apk) en lugar de un único APK monolítico. Si parcheas solo base.apk, los recursos o las librerías nativas pueden desincronizarse y la instalación puede fallar.

Evaluación rápida desde un dispositivo:

adb shell pm path com.example.app
adb pull /data/app/.../base.apk
adb pull /data/app/.../split_config.arm64_v8a.apk
adb pull /data/app/.../split_config.en.apk

Si el objetivo es un paquete split, reconstruye todo el conjunto o usa herramientas que unan primero los APKs. apk.sh es útil aquí porque puede combinar split APKs en un único APK parcheable y corregir los identificadores de recursos públicos.
Para flujos de trabajo de reempaquetado orientados a Frida/Objection, también consulta Android Anti-Instrumentation & SSL Pinning Bypass.

Descompilar el APK

Con APKTool puedes acceder al código smali y a los recursos:

apktool d APP.apk

Si apktool te da algún error, intenta instalar la latest version

Algunos archivos interesantes que deberías revisar son:

  • res/values/strings.xml (y todos los xml dentro de res/values/*)
  • AndroidManifest.xml
  • Cualquier archivo con extensión .sqlite o .db

Si apktool tiene problemas decodificando la aplicación echa un vistazo a https://ibotpeaches.github.io/Apktool/documentation/#framework-files o intenta usar el argumento -r (No decodificar recursos). Entonces, si el problema estaba en un recurso y no en el código fuente, no tendrás el problema (tampoco decompilarás los recursos).

Cambiar código smali

Puedes cambiar instrucciones, cambiar el valor de algunas variables o añadir nuevas instrucciones. Yo modifico el código Smali usando VS Code, luego instalas la smalise extension y el editor te dirá si alguna instrucción es incorrecta.
Algunos ejemplos se pueden encontrar aquí:

O puedes consultar abajo algunos Smali changes explained.

Recompilar el APK

Después de modificar el código puedes recompilar el código usando:

apktool b . #In the folder generated when you decompiled the application

Esto compilará el nuevo APK dentro de la carpeta dist.

Si apktool lanza un error, intenta instalar la última versión

Firmar el nuevo APK

Luego, necesitas generar una clave (se te pedirá una contraseña y algunos datos que puedes rellenar aleatoriamente):

keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias <your-alias>

Finalmente, firma el nuevo APK:

jarsigner -keystore key.jks path/to/dist/* <your-alias>

jarsigner todavía funciona para algunas pruebas rápidas, pero para compilaciones modernas de Android se prefiere apksigner porque maneja los esquemas de firma de APK más recientes.

Optimizar la nueva aplicación

zipalign es una herramienta de alineación de archivos que proporciona una optimización importante para los archivos de aplicación de Android (APK). Más información aquí.

zipalign [-f] [-v] <alignment> infile.apk outfile.apk
zipalign -v 4 infile.apk

Si el APK contiene bibliotecas nativas empaquetadas (lib/*.so), Android ahora recomienda usar -P 16 para que los archivos .so estén alineados tanto para dispositivos con tamaño de página de 16 KiB como de 4 KiB:

zipalign -P 16 -f -v 4 infile.apk outfile.apk

Firma el nuevo APK (¿otra vez?)

Si prefieres usar apksigner en lugar de jarsigner, deberías firmar el apk después de aplicar la optimización con zipaling. PERO TEN EN CUENTA QUE SOLO TIENES QUE FIRMAR LA APLICACIÓN UNA VEZ CON jarsigner (antes de zipalign) O CON aspsigner (después de zipaling).

apksigner sign --ks key.jks ./dist/mycompiled.apk

Un flujo más moderno y práctico es:

apktool b . -o dist/app-unsigned.apk
zipalign -P 16 -f -v 4 dist/app-unsigned.apk dist/app-aligned.apk
apksigner sign --ks key.jks --out dist/app-signed.apk dist/app-aligned.apk
apksigner verify --verbose --print-certs dist/app-signed.apk

Notas importantes:

  • Si modificas un APK después de firmarlo con apksigner, la firma se invalida y debes volver a firmarlo.
  • apksigner verify --print-certs es útil para confirmar que el APK reconstruido es instalable y para inspeccionar el certificado que el target expondrá en tiempo de ejecución.

Modificando Smali

Para el siguiente código Java Hello World:

public static void printHelloWorld() {
System.out.println("Hello World")
}

El código Smali sería:

.method public static printHelloWorld()V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello World"
invoke-virtual {v0,v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method

El conjunto de instrucciones de Smali está disponible here.

Cambios leves

Modificar los valores iniciales de una variable dentro de una función

Algunas variables se definen al comienzo de la función usando el opcode const, puedes modificar sus valores o definir nuevas:

#Number
const v9, 0xf4240
const/4 v8, 0x1
#Strings
const-string v5, "wins"

Operaciones básicas

#Math
add-int/lit8 v0, v2, 0x1 #v2 + 0x1 and save it in v0
mul-int v0,v2,0x2 #v2*0x2 and save in v0

#Move the value of one object into another
move v1,v2

#Condtions
if-ge #Greater or equals
if-le #Less or equals
if-eq #Equals

#Get/Save attributes of an object
iget v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I #Save this.o inside v0
iput v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I #Save v0 inside this.o

#goto
:goto_6 #Declare this where you want to start a loop
if-ne v0, v9, :goto_6 #If not equals, go to: :goto_6
goto :goto_6 #Always go to: :goto_6

Cambios mayores

Peculiaridades de Smali que suelen romper las reconstrucciones

  • Prefiere aumentar .locals cuando solo necesites registros temporales en el cuerpo de un método existente. Los registros de parámetros (p0, p1…) se mapean a los registros más altos del método, por lo que cambiar a ciegas a .registers a menudo rompe el orden de los argumentos.
  • move-result, move-result-wide, y move-result-object deben aparecer inmediatamente después del invoke-* correspondiente. Insertar logging u otro opcode entre ellos hace que el método sea inválido.
  • long y double son valores wide y consumen un par de registros. Si reutilizas esos registros más tarde, recuerda que v10 también ocupa v11.
  • Si necesitas pasar muchos registros, o registros con números muy altos, usa las variantes /range como invoke-virtual/range.

Registro

#Log win: <number>
iget v5, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I #Get this.o inside v5
invoke-static {v5}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; #Transform number to String
move-result-object v1 #Move to v1
const-string v5, "wins" #Save "win" inside v5
invoke-static {v5, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I #Logging "Wins: <num>"

Recommendations:

  • Si vas a usar variables declaradas dentro de la función (declaradas v0,v1,v2…) coloca estas líneas entre la .local y las declaraciones de las variables (const v0, 0x1)
  • Si quieres poner el código de logging en medio del código de una función:
  • Suma 2 al número de variables declaradas: Ex: de .locals 10 a .locals 12
  • Las nuevas variables deben ser los números siguientes de las variables ya declaradas (en este ejemplo deberían ser v10 y v11, recuerda que empieza en v0).
  • Cambia el código de la función de logging y usa v10 y v11 en lugar de v5 y v1.

Patching common anti-tamper checks

Cuando una app es repackeada, una de las primeras cosas que puede romperse es una comprobación in-app de signature / installer / integrity. Buenas cadenas para buscar en JADX o en el árbol smali son:

  • GET_SIGNATURES
  • GET_SIGNING_CERTIFICATES
  • apkContentsSigners
  • MessageDigest
  • SHA-256
  • Base64
  • getInstallerPackageName
  • com.android.vending

Las apps modernas a menudo llaman a PackageManager.getPackageInfo(..., GET_SIGNING_CERTIFICATES), hashéan los bytes del signer con MessageDigest, y comparan el resultado con una constante hardcodeada. En la práctica, suele ser más fácil parchear el final boolean / branch que reescribir todo el código de manejo de firmas.

Example patterns:

# Force a boolean result to "valid"
const/4 v0, 0x1

# Or invert the branch that sends execution to the tamper handler
if-eqz v0, :tamper_detected   # original
if-nez v0, :tamper_detected   # patched

Si el código de verificación es ruidoso, busca la última comparación antes del diálogo de error / finish() / System.exit() / telemetry call y parchea ahí en lugar de tocar toda la rutina.

Toasting

Recuerda añadir 3 al número de .locals al comienzo de la función.

Este código está preparado para ser insertado en la mitad de una función (cambia el número de las variables según sea necesario). Tomará el valor de this.o, lo transformará a String y luego hará un toast con su valor.

const/4 v10, 0x1
const/4 v11, 0x1
const/4 v12, 0x1
iget v10, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I
invoke-static {v10}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v11
invoke-static {p0, v11, v12}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v12
invoke-virtual {v12}, Landroid/widget/Toast;->show()V

Loading a Native Library at Startup (System.loadLibrary)

A veces necesitas precargar una biblioteca nativa para que se inicialice antes que otras JNI libs (p. ej., para habilitar telemetry/logging a nivel de proceso). Puedes inyectar una llamada a System.loadLibrary() en un inicializador estático o al principio de Application.onCreate(). Ejemplo smali para un inicializador estático de clase ():

.class public Lcom/example/App;
.super Landroid/app/Application;

.method static constructor <clinit>()V
.registers 1
const-string v0, "sotap"         # library name without lib...so prefix
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
return-void
.end method

Alternativamente, coloca las mismas dos instrucciones al inicio de tu Application.onCreate() para garantizar que la biblioteca se cargue lo antes posible:

.method public onCreate()V
.locals 1

const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

invoke-super {p0}, Landroid/app/Application;->onCreate()V
return-void
.end method

Notas:

  • Asegúrate de que la variante ABI correcta de la library exista bajo lib// (p. ej., arm64-v8a/armeabi-v7a) para evitar UnsatisfiedLinkError.
  • Cargar muy temprano (class static initializer) garantiza que el native logger pueda observar la actividad JNI posterior.

Smali Static Analysis / Rule-Based Hunting

Después de descompilar con apktool, puedes scan Smali line-by-line con reglas regex para detectar rápidamente lógica anti-análisis (chequeos de root/emulador) y probables secretos hardcodeados. Esta es una técnica de fast triage: trata los hits como pistas que debes verificar en el Smali circundante o en el Java/Kotlin reconstruido.

Ideas clave:

  • Library filtering: suprime o etiqueta hallazgos bajo espacios de nombres comunes de terceros para que te concentres en rutas de código propiedad de la app.
  • Context hints: exige que las cadenas sospechosas aparezcan cerca de las APIs que las consumen (dentro del mismo método, dentro de N líneas).
  • Confidence: usa niveles simples (high/medium) para clasificar las pistas y reducir falsos positivos.

Ejemplo de prefijos de librerías para suprimir por defecto:

Landroidx/
Lkotlin/
Lkotlinx/
Lcom/google/
Lcom/squareup/
Lokhttp3/
Lokio/
Lretrofit2/

Ejemplos de reglas de detección (regex + heurísticas de contexto):

{
"category": "root_check",
"regex_patterns": [
"(?i)invoke-static .*Runtime;->getRuntime\\(\\).*->exec\\(.*\\"(su|magisk|busybox)\\"",
"(?i)const-string [vp0-9, ]+\\"(/system/xbin/su|/system/bin/su|/sbin/su)\\""
],
"context_hint": "Only report when the same method also calls File;->exists/canExecute or Runtime;->exec."
}

Heurísticas adicionales que funcionan bien en la práctica:

  • Comprobaciones de paquete/ruta root: requerir llamadas cercanas a PackageManager;->getPackageInfo o File;->exists para cadenas como com.topjohnwu.magisk o /data/local/tmp.
  • Comprobaciones de emulador: emparejar literales sospechosos (p. ej., ro.kernel.qemu, generic, goldfish) con getters Build.* cercanos y comparaciones de cadenas (->equals, ->contains, ->startsWith).
  • Secretos hardcodeados: marcar const-string solo cuando un .field cercano o un identificador move-result incluya palabras clave como password, token, api_key. Ignorar explícitamente marcadores únicamente de UI como AutofillType, InputType, EditorInfo.

Escáneres basados en reglas como PulseAPK Core implementan este modelo para detectar rápidamente lógica anti-análisis y posibles secretos en Smali.

Referencias

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks