Guia sobre cliente-servidor reverso apk no exemplo do trabalho NeoQUEST-2020


Hoje temos um programa rico (haveria muitas áreas de segurança cibernética ao mesmo tempo!): Considere decompilar um aplicativo Android, interceptar tráfego para obter URLs, reconstruir apk sem código fonte, trabalhar com criptoanalistas e muito mais :)

Segundo a lenda NeoQUEST-2020 , o herói encontrou peças antigas de robôs que devem ser usadas para obter a chave. Vamos começar!

1. Reversim apk


Portanto, diante de nós, é um pouco que conseguimos extrair de um robô semi-desmontado - um aplicativo apk que de alguma forma deve nos ajudar a obter a chave. Vamos fazer o mais óbvio: execute o apk e veja sua funcionalidade. A interface de aplicativo mais que minimalista não deixa dúvidas - este é um cliente de arquivo FileDroid personalizado que permite baixar um arquivo de um servidor remoto. Ok, parece fácil. Conectamos o telefone à Internet, tentamos fazer o download (imediatamente key.txt - bem, e se?) - sem sucesso, o arquivo está ausente no servidor.



Prosseguimos para o próximo evento em termos de complexidade - descompilamos apk usando JADXe analise o código-fonte do aplicativo, que felizmente não é ofuscado. Nossa tarefa atual é entender quais arquivos o servidor remoto oferece para download e selecionar o arquivo com a chave deles.

Começamos com a classe com.ctf.filedroid.MainActivity, que contém o método onClick () mais interessante para nós, no qual o clique no botão "Download" é ​​processado. Dentro desse método, a classe ConnectionHandler é chamada duas vezes: primeiro, o método ConnectionHandler.getToken () é chamado e somente então, ConnectionHandler.getEncryptedFile (), que passa o nome do arquivo solicitado pelo usuário, é chamado.



Sim, primeiro precisamos de um token! Vamos examinar um pouco mais com o processo de obtê-lo.
O método ConnectionHandler.getToken () recebe duas linhas de entrada e envia uma solicitação GET, passando essas linhas como os parâmetros "crc" e "sign". Em resposta, o servidor envia os dados no formato JSON, a partir dos quais nosso aplicativo extrai o token de acesso e os usa para baixar o arquivo. Isso é tudo, é claro, bom, mas o que são "crc" e "sign"?



Para entender isso, avançamos na classe Cheques, fornecendo os métodos badHash () e badSign (). O primeiro calcula a soma de verificação de classes.dex e resources.arsc, concatena esses dois valores e envolve-os na Base64 (preste atenção ao sinalizador 10 = NO_WRAP | URL_SAFE, será útil). E o segundo método? E ele faz o mesmo com a impressão digital SHA-256 da assinatura do aplicativo. Eh, parece que o FileDroid não está realmente ansioso para ser reconstruído :(



Ok, digamos que recebemos o token. Qual é o próximo? Passamos para a entrada do método ConnectionHandler.getEncryptedFile (), que anexa o nome do arquivo solicitado ao token e gera outra solicitação GET, desta vez com os parâmetros "token" e "file". O servidor em resposta (a julgar pelo nome do método) envia um arquivo criptografado, armazenado em / sdcard /.

Então, para resumir um pouco do subtotal: temos duas notícias e ... ambas são ruins. Em primeiro lugar, o FileDroid não suporta realmente o nosso zelo pela modificação do apk (a soma de verificação e a assinatura são verificadas) e, em segundo lugar, o arquivo recebido do servidor promete ser criptografado.

Ok, resolveremos os problemas assim que eles estiverem disponíveis, e agora nosso principal problema é que ainda não sabemos qual arquivo precisamos baixar. No entanto, no processo de estudar a classe ConnectionHandler, não pudemos deixar de notar que, entre os métodos getToken () e getEncryptedFile (), os desenvolvedores do FileDroid esqueceram outro método muito sedutor, chamado de getListing (). Portanto, o servidor suporta essa funcionalidade ... Parece que é isso que você precisa!



Para obter a listagem, precisaremos do conhecido "crc" e "sign" - não é um problema, já sabemos de onde eles vêm. Lemos os valores, enviamos uma solicitação GET e ... Então, pare. Para onde vamos enviar a solicitação GET? Seria bom obter o URL do servidor remoto primeiro. Eh, retornamos a MainActivity.onClick () e vemos como os argumentos netPath são gerados para chamar os métodos getToken () e getEncryptedFile ():

Method getSecureMethod = 
wat.class.getDeclaredMethod("getSecure", new Class[]{String.class});

// . . .

// netPath --> ConnectionHandler.getToken()
(String) getSecureMethod.invoke((Object) null, new Object[]{"fnks"})

// netPath --> ConnectionHandler. getEncryptedFile()
(String) getSecureMethod.invoke((Object) null, new Object[]{"qdkm"})

Combinações estranhas de letras “fnks” e “qdmk” nos forçam a voltar ao resultado da descompilação do método wat.getSecure (). Spoiler: JADX tem esse resultado mais ou menos.



Após uma inspeção mais detalhada, fica claro que todo esse conteúdo não muito agradável do método pode ser substituído pelo caso de comutação usual desse tipo:

// . . .
switch(CODE)
{
    case «qdkm»: 
        r.2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(2);
        break;
    case «tkog»: 
        r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(1);
        break;
    case «fnks»: 
        String r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(0);
	break;
}
java.lang.StringBuilder r1 = new java.lang.StringBuilder 
r1.<init>(r2) 
java.lang.String r0 = r1.toString() 
java.lang.String r1 = radon(r0)
return r1 

Como “fnks” e “qdmk” já são usados ​​para obter um token e baixar um arquivo, “tkog” deve fornecer o URL necessário para solicitar uma lista dos arquivos disponíveis no servidor. Parece que há uma esperança de obter o caminho necessário mais barato ... Primeiro, vamos ver como as URLs são armazenadas no aplicativo. Abrimos a função com.ctf.filedroid.x37AtsW8g.rlieh786d () e vemos que cada URL é salvo como uma matriz de bytes codificados, e a própria função forma uma sequência desses bytes e a retorna.



Boa. Mas a linha é passada para a função com.ctf.filedroid.wat.radon (), cuja implementação é enviada à biblioteca nativa libae3d8oe1.so. Arm64 reverso? Boa tentativa, FileDroid, mas vem outra hora?

2. Obtenha os URLs do servidor


Vamos tentar abordar do outro lado: para interceptar o tráfego, obter URLs em texto não criptografado (e como bônus, também soma de verificação e valores de assinatura!), Combine-os com matrizes de bytes de com.ctf.filedroid.x37AtsW8g.rlieh786d () - ele pode A criptografia acaba sendo a cifra normal de Caesar ou XOR? .. Então não será difícil restaurar o terceiro URL e executar a listagem.

Para redirecionar o tráfego, você pode usar qualquer proxy conveniente ( Charles , violinista , BURPetc.) Configuramos o encaminhamento em um dispositivo móvel, instalamos o certificado apropriado, verificamos se a interceptação foi bem-sucedida e iniciamos o FileFroid. Estamos tentando baixar um arquivo arbitrário e ... consulte "NetworkError". Este erro foi causado pela presença de fixação de certificado (consulte o método com.ctf.filedroid.ConnectionHandler.sendRequest): o cliente de arquivo verifica se o certificado "conectado" no aplicativo corresponde ao servidor com o qual ele interage. Agora está claro por que a integridade dos recursos do aplicativo é controlada!



No entanto, no tráfego interceptado, podemos ver pelo menos o nome de domínio do servidor acessado pelo cliente de arquivo, o que significa que a esperança de descriptografar os URLs permanece!



Vamos retornar à função com.ctf.filedroid.x37AtsW8g.rlieh786d () e observe que as primeiras dezenas de bytes coincidem em todas as matrizes:

cArr[0] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'i', . . .};

cArr[1] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . .};

cArr[2] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . ., '='};

Além disso, o último byte da terceira matriz sugere que ele não estava sem base64. Vamos tentar decodificar e cutucar os bytes resultantes com uma parte conhecida da URL:



parece que ninguém nunca ficou tão feliz com o ARMag3dd0n! O problema é pequeno: decodifique sequencialmente os URLs base64 e o xorim com a chave encontrada. Mas ... e se não fosse XOR, mas uma cifra de permutação feita por si mesma, que você não consegue entender nem com cem tentativas?

3. Reconstrua apk com Frida


Como parte desse artigo, consideraremos um método de solução mais indolor (e, em nossa opinião, mais bonito) - usando a estrutura Frida , que nos permitirá executar métodos arbitrários de aplicativos apk com os argumentos necessários em tempo de execução. Para fazer isso, você precisa de um telefone com direitos de root ou um emulador. Assumimos o seguinte plano de ação:

  1. Instalando os componentes Frida em um PC e um telefone de teste.
  2. Recupere URLs correspondentes a solicitações de token ou listagem e faça o download do arquivo (usando o Frida).
  3. Recuperando os valores de soma de verificação e assinatura do aplicativo original.
  4. Obter uma lista de arquivos armazenados no servidor e identificar o arquivo desejado.
  5. Baixe e descriptografe o arquivo.

Primeiro, esclareceremos a relação entre o telefone raiz e o apk. Instalamos o aplicativo, executamos, mas o cliente de arquivo não deseja inicializar completamente, apenas pisca e fecha. Verificamos as mensagens através do logcat - sim, o FileDroid já sente que algo está errado e resiste ao máximo.



Voltamos novamente à classe MainActivity e descobrimos que o método doChecks () é chamado em onCreate () e exibe os seguintes erros no log:



Além disso, onResume () também verifica se a porta típica de Frida está aberta:



Nosso cliente de arquivo é um pouco intolerante para depuração, root e o próprio Frida. Como essa oposição não está absolutamente incluída em nossos planos, obtemos o código smali do aplicativo usando o utilitário apktool, abra o arquivo MainActivity.smali em qualquer editor de texto, encontre o método onCreate () e transforme a chamada doChecks () em um comentário inofensivo:



Em seguida, privamos o método suicide () da oportunidade de realmente desligar o aplicativo:



Em seguida, vamos construir nosso aplicativo um pouco melhorado novamente usando apktool e assinar executando os seguintes comandos (você pode precisar de direitos de administrador):

cd "C:\Program Files\Java\jdk-14\bin"
.\keytool -genkey -v -keystore filedroid.keystore -alias filedroid_alias -keyalg RSA -keysize 2048 -validity 10000
.\jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore  filedroid.keystore filedroid_patched.apk filedroid_alias
.\jarsigner -verify -verbose -certs filedroid_patched.apk

Nós reinstalamos o aplicativo no telefone, execute-o - viva, o download ocorre sem incidentes, o log está limpo!



Prosseguimos com a instalação do framework Frida em um PC e dispositivo móvel:

$ sudo pip3 install frida-tools
$ wget https://github.com/frida/frida/releases/download/$(frida --version)/frida-server-$(frida --version)-android-arm.xz
$ unxz frida-server-$(frida --version)-android-arm.xz
$ adb push frida-server-$(frida --version)-android-arm /data/local/tmp/frida-server

Inicie o servidor de estrutura Frida em um dispositivo móvel:

$ adb shell su - "chmod 755 /data/local/tmp/frida-server"
$ adb shell su - "/data/local/tmp/frida-server &"  

Estamos preparando um script get-urls.js simples que chamará wat.getSecure () para todos os servidores de solicitação suportados:

Java.perform(function () 
{
     const wat = Java.use('com.ctf.filedroid.wat');
	console.log(wat.getSecure("fnks"));
	console.log(wat.getSecure("qdmk"));
	console.log(wat.getSecure("tkog"));
});

Iniciamos o FileDroid no dispositivo móvel e “nos apegamos” ao nosso script para o processo correspondente:



4. Obtenha a lista de arquivos no servidor


Finalmente, o servidor remoto tornou-se um pouco mais próximo de nós! Agora sabemos que o servidor suporta solicitações das seguintes maneiras:

  1. filedroid.neoquest.ru/api/verifyme?crc= {crc} & sign = {sign}
  2. filedroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  3. filedroid.neoquest.ru/api/file?file= {file} & token = {token}

Para obter uma lista dos arquivos disponíveis, resta calcular os valores de soma de verificação e assinatura do aplicativo original e, em seguida, codificá-los em base64.

Esse script em python3 permitirá que você faça isso:

Spoiler
import hashlib
import binascii
import base64
from asn1crypto import cms, x509
from zipfile import ZipFile


def get_info(apk):
    with ZipFile(apk, 'r') as zipObj:
        classes = zipObj.read("classes.dex")
        resources = zipObj.read("resources.arsc")
        cert = zipObj.read("META-INF/CERT.RSA")
        crc = "%s%s" % (get_crc(classes), get_crc(resources))
        return get_full_crc(classes, resources).decode("utf-8"), get_sign(cert).decode("utf-8")


def get_crc(file):
    crc = binascii.crc32(file) & 0xffffffff
    return crc


def get_full_crc(classes, resources):
    crc = "%s%s" % (get_crc(classes), get_crc(resources))
    return base64.urlsafe_b64encode(bytes(crc, "utf-8"))


def get_sign(file):
    pkcs7 = cms.ContentInfo.load(file)
    data = pkcs7['content']['certificates'][0].chosen.dump()
    sha256 = hashlib.sha256()
    sha256.update(data)
    return base64.urlsafe_b64encode(sha256.digest())  

get_info('filedroid.apk')


Você também pode manualmente. Consideramos o CRC32 de classes.dex e resources.arsc uma ferramenta conveniente (por exemplo, para Linux - o utilitário crc32 padrão), obtemos os valores 1276945813 e 2814166583 respectivamente, concatenamos-os (sairão 12769458132814166583) e codificamos em base64, por exemplo, aqui :



Para executar um procedimento semelhante para assinar o aplicativo, na janela JADX, vá para a seção "Assinatura APK", copie o valor "Impressão digital SHA-256" e codifique-o em base64 como uma matriz de bytes:



Importante:no apk original, a codificação base64 é realizada com o sinalizador URL_SAFE, ou seja, em vez dos caracteres “+” e “/”, “-” e “_” são usados, respectivamente. É necessário ter certeza de que isso também será observado com a auto-codificação. Para fazer isso, ao codificar on-line, você pode substituir o alfabeto usado por "ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 + /" por "ABCDEFGHIJKLMNOPQRSTUVWXYjcqppjj64.pqjp64.jqqpp64.jqqpp64.jqqpp64.jqqpp64.jqqppjj64.pqj64.jpg

Por fim, temos todos os ingredientes para listar com êxito os arquivos:

  1. filedroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  2. crc: MTI3Njk0NTgxMzI4MTQxNjY1ODM =
  3. sign: HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY =

Executamos a solicitação GET - e felicidades, nossa lista! Além disso, o nome de um dos arquivos fala por si - "abra-se-você-quer-escapar" - parece que precisamos dele.



Em seguida, solicitamos um token de acesso único e baixamos o arquivo:

import requests

response = requests.get('https://filedroid.neoquest.ru/api/verifyme', 
    		params={    'crc': 'MTI3Njk0NTgxMzI4MTQxNjY1ODM=', 
'sign': HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY=},
verify=False)
    
token = response.json()['token']
print(token)
response = requests.get('https://filedroid.neoquest.ru/api/file', 
params={'token': token, 'file': '0p3n1fuw4nt2esk4p3.jpg'}, verify=False)

with open("0p3n1fuw4nt2esk4p3.jpg", 'wb') as fd:
        fd.write(response.content)

Abrimos o arquivo baixado e lembramos de uma pequena circunstância que deixamos para mais tarde:



5. Adicione uma pitada de criptografia ...


Muito cedo, adiamos o FileDroid. Vamos voltar ao JADX e ver se os desenvolvedores do cliente de arquivos deixaram algo útil para nós. Sim, este é o caso quando a limpeza de código claramente não é popular: o método decryptFile () não utilizado está aguardando silenciosamente nossa atenção na classe ConnectionHandler. O que nós temos? Modo de

criptografia AES do CBC , sinhroposylka ocupa os primeiros 16 bytes ... Preguiça - o mecanismo do progresso, é melhor usar novamente o Frida e decifrar nosso 0p3n1fuw4nt2esk4p3.jpg sem esforço. Mas basta passar como a chave de criptografia? Não há muitas opções, mas, dada a presença de outro método savePlainFile (arquivo String, token String) “esquecido”, a escolha é óbvia.

Prepare o seguinte script decrypt.js (como token indique o valor real, por exemplo, 'HoHknc572mVpZESSQN1Xa7S9zOidxX1PMbykdoM1EXI ='):

Java.perform(function () {
    const JavaString = Java.use('java.lang.String');
    const file_name = JavaString.$new('0p3n1fuw4nt2esk4p3.jpg');
    const ConnectionHandler = Java.use('com.ctf.filedroid.ConnectionHandler');
    const result = ConnectionHandler.savePlainFile(file_name, <token>);
    console.log(result);
});

Colocamos o arquivo criptografado 0p3n1fuw4nt2esk4p3.jpg em / sdcard /, executamos o FileDroid e injetamos o script decrypt.js usando o Frida. Após a execução do script, o arquivo plainfile.jpg aparecerá em / sdcard /. Abrimos e ... resolvido!



Essa tarefa difícil exigiu que os participantes tivessem conhecimento e habilidades em várias áreas de segurança da informação de uma só vez, e estamos felizes por a maioria dos concorrentes ter conseguido lidar com isso!

Esperamos que aqueles que não tiveram tempo ou conhecimento suficientes antes de receber a chave passem com êxito tarefas semelhantes em qualquer CTF :)

All Articles