MASTG-TECH-0043: Enganchamento de Métodos
Xposed¶
Vamos supor que você esteja testando um aplicativo que insiste em fechar no seu dispositivo com root. Você descompila o app e encontra o seguinte método altamente suspeito:
package com.example.a.b
public static boolean c() {
int v3 = 0;
boolean v0 = false;
String[] v1 = new String[]{"/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/",
"/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/"};
int v2 = v1.length;
for(int v3 = 0; v3 < v2; v3++) {
if(new File(String.valueOf(v1[v3]) + "su").exists()) {
v0 = true;
return v0;
}
}
return v0;
}
Este método percorre uma lista de diretórios e retorna true (dispositivo com root) se encontrar o binário su em qualquer um deles. Verificações como essa são fáceis de desativar - tudo que você precisa fazer é substituir o código por algo que retorne "false". O hook de métodos com um módulo Xposed é uma maneira de fazer isso (consulte "Teste de Segurança Básico para Android" para mais detalhes sobre instalação e conceitos básicos do Xposed).
O método XposedHelpers.findAndHookMethod permite que você substitua métodos existentes de classes. Ao inspecionar o código-fonte descompilado, você pode descobrir que o método que executa a verificação é c. Este método está localizado na classe com.example.a.b. A seguir está um módulo Xposed que substitui a função para que ela sempre retorne false:
package com.awesome.pentestcompany;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
public class DisableRootCheck implements IXposedHookLoadPackage {
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.example.targetapp"))
return;
findAndHookMethod("com.example.a.b", lpparam.classLoader, "c", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("Capturada verificação de root!");
param.setResult(false);
}
});
}
}
Assim como aplicativos Android regulares, os módulos para Xposed são desenvolvidos e implantados com o Android Studio. Para mais detalhes sobre como escrever, compilar e instalar módulos Xposed, consulte o tutorial fornecido por seu autor, rovo89.
Frida¶
Vamos usar o Frida para resolver Android UnCrackable L1 e demonstrar como podemos facilmente contornar a detecção de root e extrair dados secretos do aplicativo.
Quando você inicia o aplicativo crackme em um emulador ou dispositivo com root, perceberá que ele apresenta uma caixa de diálogo e fecha assim que você pressiona "OK" porque detectou root:

Vamos ver como podemos evitar isso.
O método principal (descompilado com CFR) tem esta aparência:
package sg.vantagepoint.uncrackable1;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.widget.EditText;
import sg.vantagepoint.a.b;
import sg.vantagepoint.a.c;
import sg.vantagepoint.uncrackable1.a;
public class MainActivity
extends Activity {
private void a(String string) {
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
alertDialog.setTitle((CharSequence)string);
alertDialog.setMessage((CharSequence)"This is unacceptable. The app is now going to exit.");
alertDialog.setButton(-3, (CharSequence)"OK", new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialogInterface, int n) {
System.exit((int)0);
}
});
alertDialog.setCancelable(false);
alertDialog.show();
}
protected void onCreate(Bundle bundle) {
if (c.a() || c.b() || c.c()) {
this.a("Root detected!");
}
if (b.a(this.getApplicationContext())) {
this.a("App is debuggable!");
}
super.onCreate(bundle);
this.setContentView(2130903040);
}
/*
* Enabled aggressive block sorting
*/
public void verify(View object) {
object = ((EditText)this.findViewById(2130837505)).getText().toString();
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
if (a.a((String)object)) {
alertDialog.setTitle((CharSequence)"Success!");
object = "This is the correct secret.";
} else {
alertDialog.setTitle((CharSequence)"Nope...");
object = "That's not it. Try again.";
}
alertDialog.setMessage((CharSequence)object);
alertDialog.setButton(-3, (CharSequence)"OK", new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialogInterface, int n) {
dialogInterface.dismiss();
}
});
alertDialog.show();
}
}
Observe a mensagem "Root detected" no método onCreate e os vários métodos chamados na instrução if anterior (que realizam as verificações reais de root). Note também a mensagem "This is unacceptable..." do primeiro método da classe, private void a. Obviamente, este método exibe a caixa de diálogo. Há um callback alertDialog.onClickListener definido na chamada do método setButton, que fecha o aplicativo via System.exit após a detecção bem-sucedida de root. Com o Frida, você pode impedir que o aplicativo feche fazendo hook do método MainActivity.a ou do callback dentro dele. O exemplo abaixo mostra como você pode fazer hook do MainActivity.a e impedir que ele encerre o aplicativo.
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
Java.perform(function() {
var mainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainActivity.a.implementation = function(v) {
console.log("[*] MainActivity.a called");
};
console.log("[*] MainActivity.a modified");
});
});
Encapsule seu código na função setImmediate para evitar timeouts (você pode ou não precisar fazer isso), depois chame Java.perform para usar os métodos do Frida para lidar com Java. Em seguida, recupere um wrapper para a classe MainActivity e substitua seu método a. Diferente do original, a nova versão de a apenas grava a saída no console e não fecha o aplicativo. Uma solução alternativa é fazer hook do método onClick da interface OnClickListener. Você pode substituir o método onClick e impedir que ele encerre o aplicativo com a chamada System.exit. Se você quiser injetar seu próprio script Frida, ele deve desativar completamente o AlertDialog ou alterar o comportamento do método onClick para que o aplicativo não feche quando você clicar em "OK".
Salve o script acima como uncrackable1.js e carregue-o:
frida -U -f owasp.mstg.uncrackable1 -l uncrackable1.js --no-pause
Depois que você vir a mensagem "MainActivity.a modified", o aplicativo não fechará mais.
Agora você pode tentar inserir uma "string secreta". Mas onde você a obtém?
Se você observar a classe sg.vantagepoint.uncrackable1.a, poderá ver a string criptografada com a qual sua entrada é comparada:
package sg.vantagepoint.uncrackable1;
import android.util.Base64;
import android.util.Log;
public class a {
public static boolean a(String string) {
byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
try {
arrby = sg.vantagepoint.a.a.a(a.b("8d127684cbc37c17616d806cf50473cc"), arrby);
}
catch (Exception exception) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("AES error:");
stringBuilder.append(exception.getMessage());
Log.d((String)"CodeCheck", (String)stringBuilder.toString());
arrby = new byte[]{};
}
return string.equals((Object)new String(arrby));
}
public static byte[] b(String string) {
int n = string.length();
byte[] arrby = new byte[n / 2];
for (int i = 0; i < n; i += 2) {
arrby[i / 2] = (byte)((Character.digit((char)string.charAt(i), (int)16) << 4) + Character.digit((char)string.charAt(i + 1), (int)16));
}
return arrby;
}
}
Observe a comparação string.equals no final do método a e a criação da string arrby no bloco try acima. arrby é o valor de retorno da função sg.vantagepoint.a.a.a. A comparação string.equals compara sua entrada com arrby. Então queremos o valor de retorno de sg.vantagepoint.a.a.a.
Em vez de reverter as rotinas de descriptografia para reconstruir a chave secreta, você pode simplesmente ignorar toda a lógica de descriptografia no aplicativo e fazer hook da função sg.vantagepoint.a.a.a para capturar seu valor de retorno.
Aqui está o script completo que impede a saída no root e intercepta a descriptografia da string secreta:
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
Java.perform(function() {
var mainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainActivity.a.implementation = function(v) {
console.log("[*] MainActivity.a called");
};
console.log("[*] MainActivity.a modified");
var aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
var retval = this.a(arg1, arg2);
var password = '';
for(var i = 0; i < retval.length; i++) {
password += String.fromCharCode(retval[i]);
}
console.log("[*] Decrypted: " + password);
return retval;
};
console.log("[*] sg.vantagepoint.a.a.a modified");
});
});
Após executar o script no Frida e ver a mensagem "[*] sg.vantagepoint.a.a.a modified" no console, insira um valor aleatório para "string secreta" e pressione verify. Você deve obter uma saída semelhante à seguinte:
$ frida -U -f owasp.mstg.uncrackable1 -l uncrackable1.js --no-pause
[*] Starting script
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]-> [*] MainActivity.a modified
[*] sg.vantagepoint.a.a.a modified
[*] MainActivity.a called.
[*] Decrypted: I want to believe
A função com hook retornou a string descriptografada. Você extraiu a string secreta sem precisar mergulhar profundamente no código do aplicativo e suas rotinas de descriptografia.
Agora você cobriu o básico da análise estática/dinâmica no Android. Claro, a única maneira de realmente aprender é com experiência prática: crie seus próprios projetos no Android Studio, observe como seu código é traduzido em bytecode e código nativo, e tente quebrar nossos desafios.
Nas seções restantes, apresentaremos alguns tópicos avançados, incluindo exploração de processos, módulos do kernel e execução dinâmica.