Existem diferentes abordagens para analisar a segurança dos aplicativos, mas mais cedo ou mais tarde tudo se resume ao estudo da interação do programa com a API. É esta etapa que fornece mais informações sobre o funcionamento do aplicativo, sobre as funções utilizadas e os dados coletados. Mas e se o aplicativo for protegido por https://owasp.org/www-community/control … ey_Pinning e a segurança for implementada na camada? Vamos ver o que pode ser feito neste caso.
Antes de iniciar
SSLPinning em aplicativos móveis é implementado incorporando um certificado SSL no próprio programa. Quando uma conexão segura é aberta, o aplicativo não acessa o armazenamento do dispositivo, mas usa seu próprio certificado. Isso elimina imediatamente a capacidade de direcionar o tráfego para o Burp e analisá-lo. De fato, para ver o tráfego SSL, você precisa implementar um Burp CA no dispositivo, o que confirmaria que o servidor criado pelo Burp é válido e confiável.
SSLPinning não é uma panaceia, muitas vezes há notas sobre como ignorar a proteção em aplicativos bancários ou, em geral, sobre https://xakep.ru/2018/03/08/ssl-tls-fuckup/ . Mas se a proteção for construída corretamente, isso cria grandes problemas para o pesquisador.
https://forum.xda-developers.com/f/xposed-general.3094/ um framework que está sendo implementado no https://xakep.ru/2014/05/21/excurse-in- … hitecture/ . Isso acontece na inicialização do sistema, além do Zygote. Fork() é feito, que copia o Xposed para todos os processos em execução. A própria estrutura fornece a capacidade de injetar qualquer código antes e depois da função. Você pode alterar parâmetros de entrada, substituir uma função, ler dados, chamar funções internas e muito mais. Na verdade, será necessário mais de um artigo para descrever e demonstrar todos os recursos do Xposed. Em geral, se você nunca trabalhou com ele antes, recomendo que leia.
Para que o Xposed funcione, precisamos de um dispositivo com root. Para demonstrar o ataque, vamos usar um aplicativo móvel simples que usa uma das bibliotecas de rede mais comuns. Não há proteção contra SSLUnpinning neste aplicativo, pois o ataque que descrevi não tenta atacar o certificado e a comunicação de rede, mas visa interceptar dados antes que sejam cobertos por SSL. Para demonstrar o lado do servidor do ataque e o back-end do aplicativo móvel, usamos uma solução rápida na forma de Python e Flask. Você pode encontrar todas as fontes no meu GitHub .
Vamos voltar ao problema de interceptar o tráfego SSL. Usando o Xposed, você pode tentar desabilitar a verificação do certificado, por exemplo, "substituí-lo" pelo programa. Mas suponha que o aplicativo atacado esteja bem protegido, o back-end verifica a validade da proteção, detecta tentativas de interceptar ou proxy de tráfego. O que fazer neste caso? Desistir e passar para outro aplicativo? Mas e se você interceptar o tráfego antes que ele se torne rede?
Esta pergunta iniciou minha pesquisa.
Neste artigo, trabalharei com Android e Xposed, mas um resultado semelhante pode ser alcançado usando o framework Frida, que também está disponível em outros sistemas operacionais.
Levantamento de informações
Primeiro, vamos tentar executar o aplicativo. Vemos o botão ENVIAR na tela e um convite de texto para pressioná-lo. Pressionamos - a inscrição muda primeiro para "Espere ...", e então "Desculpe, hoje não" é exibido. Muito provavelmente, está sendo enviada uma solicitação que não passa na validação do lado do servidor. Vamos ver o que acontece dentro do aplicativo.
APK reverso
Vamos inverter o aplicativo para entender quais bibliotecas são usadas internamente.
CÓDIGO:
$ apktool d app-debug.apk
Abra o projeto no Android Studio e veja o que está em smali. Vemos imediatamente okhttp3.
Eu usei especificamente o OkHttp, pois esta biblioteca sustenta outras bibliotecas de API, como Retrofit 2
CÓDIGO
.method protected onCreate(Landroid/os/Bundle;)V
.line 34
iget-object v0, p0, Lcom/loony/mitmdemo/Demo;->sendButton:Landroid/widget/Button;
new-instance v1, Lcom/loony/mitmdemo/Demo$1;
invoke-direct {v1, p0}, Lcom/loony/mitmdemo/Demo$1;-><init>(Lcom/loony/mitmdemo/Demo;)V
invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
CÓDIGO
.method public onClick(Landroid/view/View;)V
.line 41
.local v0, "sr":Lcom/loony/mitmdemo/Demo$SendRequest;
const-string v1, "test"
filled-new-array {v1}, [Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Lcom/loony/mitmdemo/Demo$SendRequest;->execute([Ljava/lang/Object;)Landroid/os/AsyncTask;
No final da função onClick, uma tarefa assíncrona é chamada, obviamente denominada SendRequest. Vamos para com/loony/mitmdemo/Demo$SendRequest. Aqui vemos muitas chamadas para okhttp3. Portanto, estávamos errados em nossa suposição.
Aqui, um passo importante é determinar o alvo para interceptação. Escolhendo favoravelmente uma função ou classe, podemos obter mais opções do que interceptando qualquer outro objeto. Por que, por exemplo, interceptar uma instância de uma chave pública, se você pode interceptar todo o armazenamento?
Vejamos o uso padrão de okhttp3 em projetos Android
CÓDIGO:
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(JSON, requestJson.toString());
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Response response = client.newCall(request).execute();
A interceptação de execute será a mais benéfica aqui. Porque? Esta função retorna uma resposta e, obviamente, envia uma solicitação. Isso significa que, ao interceptar essa função, poderemos alterar o Request antes de enviar e obter o Response antes de retornar à função principal.
Ataque
Então, sabemos como a comunicação é implementada, temos uma ideia do que queremos interceptar e do que obter. Além disso, queremos de alguma forma visualizar esses dados e poder alterá-los antes de enviar ou receber. Para implementar isso, escrevi minha própria API, mas você pode ir além e conectar-se à API do Burp.
O processo básico de trabalhar com Xposed e criar módulos já foi descrito em muitos recursos.
MultiDex
Antes de começar, quero mostrar a você um truque interessante - interceptar funções em um aplicativo com MultiDex. Sem saber dele, perdi vários dias. O fato é que o aplicativo não carrega imediatamente todo o código na memória, mas é dividido em arquivos dex. Se você está tentando interceptar uma função que está no segundo ou terceiro dex, primeiro ela deve ser carregada na memória. Você pode fazer desta forma.
findAndHookMethod("android.app.Application", lpparam.classLoader, "attach", Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
if (lpparam.packageName.contains("com.loony.mitmdemo")) {
okhttpMitm(lpparam.classLoader);
}
}
});
Interceptamos o lançamento do aplicativo e obtemos uma instância do ClassLoader. Além disso, antes de colocar um gancho em uma função, você precisa carregar a classe na qual ela está localizada.
CÓDIGO:
classLoader.loadClass("class_name");
Verificando o vetor de ataque
Primeiro, vamos ter certeza de que estamos certos e adicionar um gancho que simplesmente imprima algo no console. Aqui não estamos interceptando Call, mas RealCall, já que Call é uma interface e não temos a capacidade de interceptá-la, mas podemos interceptar seus sucessores. Encontrei o sucessor de Call pelo código-fonte recebido, após apktool.
CÓDIGO:
findAndHookMethod("okhttp3.RealCall", classLoader, "execute", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
System.out.println("Yeah");
}
});
Quando o botão é pressionado, Sim é impresso no console - estamos no caminho certo. A função execute retorna uma resposta, então vamos adicionar um gancho afterHookedMethod. Antes da chamada da função, é possível interceptar o Request, e após a chamada da função, o Response.
Recebemos dados
Encontramos uma função que envia dados, o que vem a seguir? Primeiro, vamos abrir as fontes okhttp3 no GitHub e ver como é o RealCall.java. Vemos que é possível obter um Request se o código this.request() for chamado a partir da função execute. Vamos adicionar esta linha a beforeHookedMethod.
CÓDIGO
Object request = callMethod(param.thisObject, "request");
Aqui param.thisObject é uma chamada ao this da classe na qual a função que interceptamos está localizada. O segundo parâmetro é o nome da função que estamos chamando, no nosso caso é Request. Seguem os argumentos da função, mas não os temos, pois a função não aceita nada. Um pouco mais tarde mostrarei a chamada usando argumentos.
Portanto, obtivemos uma instância do objeto Request e agora gostaríamos de converter de Object para a instância Request no código do gancho.
CÓDIGO:
Request request = (okhttp3.Request) callMethod(param.thisObject, "request");
Infelizmente, não podemos simplesmente convertê-lo para uma instância de objeto, porque ele é criado em um ambiente diferente e nosso okhttp3.Request é diferente daquele no aplicativo. Você terá que receber dados chamando funções e lendo os valores das variáveis por meio do Xposed. Abrimos Request.java no GitHub e procuramos funções e variáveis que possam conter informações de nosso interesse: tipo de solicitação, cabeçalho, caminho, dados.
No meu caso, o código não é ofuscado, então posso usar os nomes das funções, como no GitHub. Caso contrário, é necessário encontrar a função correspondente no código reverso, pois alguns nomes podem diferir devido a ofuscamento. Decidi adicionar dados imediatamente ao JSONObject, isso será necessário mais tarde. Também codifico os dados do corpo para Base64 porque a função retorna uma matriz de bytes. Neste formulário, não podemos exibir informações corretamente no terminal.
CÓDIGO
Object request_object = callMethod(param.thisObject, "request");
finalJSON.put("url", getObjectField(request_object, "url"));
finalJSON.put("method", callMethod(request_object, "method"));
Object content = "null";
Object body = callMethod(request_object, "body");
content = getObjectField(body, "val$content");
finalJSON.put("body", Base64.encodeToString((byte[]) content, Base64.DEFAULT));
String[] namesAndValues = (String[]) getObjectField(callMethod(object, "headers"), "namesAndValues");
finalJSON.put("headers", new JSONArray(namesAndValues));
Observe que há uma conversão de Object para String[] no código. Isso pode ser feito para todos os tipos simples: String, int, long e outros.
Vamos fazer um procedimento semelhante com Response.
CÓDIGO:
Object response_object = param.getResult();
JSONObject finalJSON = new JSONObject();
finalJSON.put("code", getObjectField(response_object, "code"));
finalJSON.put("message", getObjectField(response_object, "message"));
String body = "null";
if ((boolean) callMethod(response_object, "isSuccessful")) {
Object source = callMethod(getObjectField(response_object, "body"), "source");
callMethod(source, "request", Long.MAX_VALUE);
Object buffer = callMethod(source, "buffer");
Object cloned_buffer = callMethod(buffer, "clone");
body = Base64.encodeToString((byte[]) callMethod(cloned_buffer, "readByteArray"), Base64.DEFAULT);
}
finalJSON.put("body", body);
String[] namesAndValues = (String[]) getObjectField(callMethod(object, "headers"), "namesAndValues");
finalJSON.put("headers", new JSONArray(namesAndValues));
Agora temos dois JSONObjects que contêm os dados a serem analisados. Vamos enviar para o console o que conseguimos coletar
CÓDIGO:
{
"url": "http://192.168.1.187:2451/api/v1.0/magic",
"method": "POST",
"body": "eyJzZWNyZXQiOiJIYWNrZXIgaXMgZXZlcnl3aGVyZSJ9",
"headers": [
"TestData",
"header_data"
]
}
Vamos ver o que há no corpo descompactando-o com Base64
CÓDIGO
{
"secret":"Hacker is everywhere"
}
Espere um minuto, isso é obviamente um erro! Leitores particularmente atentos notarão que o nome do recurso está escrito incorretamente. Mas e se precisarmos mudar alguma coisa antes do envio?
Fazemos substituição de dados
Para substituição, enviarei os dados coletados para minha API. Você poderia ficar sem ele, mas toda vez que fizer uma alteração em um módulo Xposed, será necessário reiniciar o dispositivo. Se você escrever a execução de comandos do centro C&C, poderá evitar esses problemas.
CÓDIGO:
def demo_attack(request):
body_text = base64.b64decode(request.json['body'])
body_text = body_text.replace("Hacker","hack")
return base64.b64encode(body_text)
@app.route('/api/v1.0/request', methods=['POST'])
def request_mitm():
if not request.json:
abort(400)
return json.dumps({
'success':True,
'task':True,
'data':demo_attack(request)
}), 200, {'ContentType':'application/json'}
Vamos adicionar o envio dos dados coletados para nosso endpoint. O código de envio usa uma solicitação POST padrão. No servidor, os dados são alterados e enviados de volta. Agora você precisa inseri-los na solicitação original.
CÓDIGO:Object mediaType = callStaticMethod(findClass("okhttp3.MediaType",classLoader),"parse","application/json; charset=utf-8");
byte[] newData = Base64.decode(task.getString("data"),Base64.DEFAULT);
Object newBody = callStaticMethod(findClass("okhttp3.RequestBody",classLoader),"create",mediaType,newData);
setObjectField(callMethod(param.thisObject, "request"),"body",newBody);
Agora vamos tentar executar o aplicativo e enviar uma solicitação. No console, vemos que nosso pedido mudou e a tela exibe “Parabéns!”.
Conclusões
Essa ideia já me ajudou mais de uma vez a encontrar problemas e contornar algumas camadas de proteção. Você pode se proteger disso? Certamente sim. Você pode adicionar bloqueio de aplicativo ou cliente se o Xposed for encontrado, adicionar assinaturas aos dados que são enviados. Você pode desconfiar de qualquer solicitação do cliente, mesmo que venha de uma conexão segura.
No entanto, tendo uma ferramenta tão poderosa como Xposed ou Frida em seu kit, você pode ignorar facilmente todos os métodos possíveis de proteção do lado do cliente.