Introducción
Diversas aplicaciones como las relacionadas con entornos bancarios o con las administraciones públicas contienen restricciones que impiden su ejecución en los dispositivos móviles Android que han sido alterados, por ejemplo, al instalar Magisk y obtener permisos de superadministrador, haber desbloqueado el cargador de arranque o si se están ejecutando en emuladores y no en dispositivos reales, todo ello para evitar su análisis.
En una aplicación de ejemplo habrá que eliminar esas restricciones del código fuente de la aplicación. Una opción será desempaquetar la aplicación y modificar el código .smali, que es un tipo de bytecode a bajo nivel no binario que puede ser leído. Para ello previamente extraeremos el archivo APK correspondiente a la aplicación y descompilaremos el código fuente de la aplicación utilizando la herramienta JADX.
Extracción del archivo APK
Las aplicaciones en Android se componen de un archivo comprimido con formato .apk (que realmente es un archivo ZIP). Pueden estar compuestas con únicamente un archivo o con varios en el caso de las “split APKs”, aplicaciones divididas que contienen los archivos respectivos al código de la arquitectura específica del procesador, de la resolución de la pantalla o de los idiomas soportados. Utilizaremos la herramienta adb para buscar y extraer los archivos. En este caso vamos a buscar el paquete con identificador com.aplicaciondeprueba.
adb shell pm path com.aplicaciondeprueba
Obtendremos un listado con las rutas en las que se encuentran el/los paquetes referentes a la aplicación. Los extraeremos también con adb.
adb pull /data/app/.../base.apk
...
adb pull /data/app/.../split_config.es.apk
Búsqueda del código fuente restrictivo
Al abrir la aplicación aparece el texto Por motivos de seguridad esta aplicación no se puede abrir en este dispositivo.. A continuación abrimos el archivo APK, en este caso, base.apk que contiene el código principal Java o Kotlin compilado, con la aplicación JADX, que nos mostrará el código fuente en Java. Usando el buscador con la cadena de texto previa encontramos una entrada de texto con el identificador insecure_modified_app_os_body en el archivo /res/values/strings.xml.
A continuación buscamos por este identificador en los archivos APK extraídos. Encontramos la actividad com.aplicaciondeprueba.InsecureAppActivity. El código fuente se encuentra en su completitud ofuscado con Proguard pero es posible observar en esta clase una serie de variables.
public /* synthetic */ class a {
/* renamed from: a */
public static final /* synthetic */ int[] f8742a;
static {
int[] iArr = new int[EnumC2230a.values().length];
iArr[EnumC2230a.ROOT_OS_APP.ordinal()] = 1;
iArr[EnumC2230a.NO_BIOMETRY.ordinal()] = 2;
iArr[EnumC2230a.OUT_OF_SERVICE.ordinal()] = 3;
iArr[EnumC2230a.NEW_UPDATE.ordinal()] = 4;
iArr[EnumC2230a.NO_INTERNET.ordinal()] = 5;
f8742a = iArr;
}
}
Parecen ser una serie de condiciones por las que no se abrirá la aplicación. En nuestro caso parece que es la ROOT_OS_APP. Buscamos las referencias a esa variable y encontramos una función llamada jd.a.
public final void a(PiracyCheckerError error) {
kotlin.jvm.internal.i.f(error, "error");
dg.a aVar = dg.a.ROOT_OS_APP;
int i10 = MainActivity.f8190j;
MainActivity mainActivity = this.f14200a;
mainActivity.k(aVar);
kd.i iVar = mainActivity.f8194g;
if (iVar != null) {
iVar.a(a.a.G(new ih.h("InsecureAppType", aVar), new ih.h("deeplink", String.valueOf(this.f14201b))), "doNotAllow on performSecurityCheck", null);
} else {
kotlin.jvm.internal.i.l("crashlyticsManager");
throw null;
}
}
Está función recibe como parámetro un objeto de la clase PiracyCheckerError, por lo que podemos confirmar que la aplicación está utilizando la biblioteca PiracyChecker para comprobar las condiciones de las restricciones. Buscando la utilización de está función, la encontramos en la actividad principal MainActivity, en concreto en la función MainActivity.a.C0115a.invokeSuspend. En esta función en concreto encontramos código que realiza la comprobación de si el dispositivo es un emulador.
...
try {
product = Build.PRODUCT;
} catch (Throwable unused3) {
product = "";
}
kotlin.jvm.internal.i.e(product, "product");
int i18 = (n.m0(product, "sdk", true) || n.m0(product, "Andy", true) || n.m0(product, "ttVM_Hdragon", true) || n.m0(product, "google_sdk", true) || n.m0(product, "Droid4X", true) || n.m0(product, "nox", true) || n.m0(product, "sdk_x86", true) || n.m0(product, "sdk_google", true) || n.m0(product, "vbox86p", true)) ? 1 : 0;
try {
manufacturer = Build.MANUFACTURER;
} catch (Throwable unused4) {
manufacturer = "";
}
...
O de si se encuentra rooteado:
...
i12 = 1;
process.destroy();
i11 = i12;
if (i11 == 0) {
String[] strArr2 = {
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin/su"
};
int i19 = i10;
while (true) {
if (i19 >= 10) {
i13 = i10;
break;
}
if (new File(strArr2[i19]).exists()) {
i13 = 1;
break;
}
i19++;
}
if (i13 == 0) {}
...
Al principio de la función invokeSuspend encontramos el siguiente código:
...
int i16 = MainActivity.f8190j;
mainActivity.getClass();
x xVar = new x(mainActivity, this.f8201c, mainActivity);
me.c cVar = new me.c(mainActivity);
xVar.invoke(cVar);
mainActivity.f8195h = cVar;
if (cVar.f16273d) {
...
Este código indica que si el atributo f16273d del objeto cVar de la clase me.c es verdadero se ejecutará el código entre llaves, que contiene todas las protecciones observadas anteriormente. Si pudieramos cambiar este valor a falso no se ejecutaría ese código y se continuaría con la normal ejecución de la aplicación. Buscando por esta variable solo observamos una asignación en la clase jd.x guardada anteriormente en la variable xVar, en su función invoke.
public final ih.m invoke(me.c cVar) {
me.c piracyChecker = cVar;
kotlin.jvm.internal.i.f(piracyChecker, "$this$piracyChecker");
piracyChecker.f16273d = true;
piracyChecker.f16274e = (String[]) Arrays.copyOf(new String[] {
"Gh45tyl10XPMht8jtrhamkRUInI="
}, 1);
piracyChecker.f16276g = true;
piracyChecker.f16275f.addAll(a.a.G(Arrays.copyOf(new ne.a[] {
ne.a.GOOGLE_PLAY
}, 1)));
piracyChecker.f16277h = true;
MainActivity mainActivity = this.f14202a;
String str = this.f14203b;
piracyChecker.f16271b = new v(mainActivity, str, this.f14204c);
piracyChecker.f16272c = new w(mainActivity, str);
return ih.m.f13121a;
}
Observando la biblioteca PiracyChecker también puede comprobar si la aplicación ha sido alterada comprobado los hashes de la firma de la aplicación, en este caso, Gh45tyl10XPMht8jtrhamkRUInI= o si cuenta con una licencia válida de Google Play en caso de que la aplicación sea de pago. Encontramos que la variable f16273d se ha establecido como verdadero.
Extracción de la aplicación y modificación del código Smali
Utilizaremos la aplicación apktool para desempaquetar y empaquetar la aplicación y uber-apk-signer para firmar y alinear la aplicación que se ha vuelto a empaquetar. En primer lugar, copiamos los archivos de la aplicación a una carpeta adicional.
mkdir app
cp *.apk app
A continuación desempaquetamos la aplicación.
java -jar apktool_2.9.3.jar d app/base.apk
Encontramos el archivo Smali de la clase x en el directorio base/smali/jd/x.smali.
.method public final invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 3
check-cast p1, Lme/c;
const-string v0, "$this$piracyChecker"
invoke-static {p1, v0}, Lkotlin/jvm/internal/i;->f(Ljava/lang/Object;Ljava/lang/String;)V
const-string v0, "Gh45tyl10XPMht8jtrhamkRUInI="
filled-new-array {v0}, [Ljava/lang/String;
move-result-object v0
const/4 v1, 0x1
...
La línea const/4 v1, 0x1 es la que guarda la variable temporal, en este caso 0x1 = verdadero. Modificando la variable a 0x0 hará que la variable f16273d sea falsa. Recompilamos la aplicación con apktool.
java -jar apktool_2.9.3.jar b base
La aplicación se empaquetará en el directorio base/dist/base.apk, la copiaremos al directorio anterior.
cp base/dist/base.apk app
A continuación firmaremos el paquete modificado y los demás, en el caso de que existan.
java -jar uber-apk-signer-1.3.0.jar --allowResign -a app
Al haber cambiado la firma original del paquete tendremos que desinstalar la aplicación original. Para instalar la aplicación podemos utilizar adb install en el caso de que la aplicación contenga un único paquete o adb install-multiple si tiene multiples paquetes. Los paquetes a instalar tienen el sufijo -aligned-debugSigned.
adb install-multiple app/base-aligned-debugSigned.apk app/split_config.es-aligned-debugSigned.apk app/split_config.x86_64-aligned-debugSigned.apk app/split_config.xxhdpi-aligned-debugSigned.apk
La aplicación iniciará sin la restricción mostrada anteriormente.
Conclusión
Las diversas protecciones personalizadas incluidas en las aplicaciones requerirán un análisis individualizado en cada uno de ellas. Otras medidas de protección en aplicaciones pueden ser el cifrado de las comunicaciones en red de forma simétrica.