Segurança

Malware Móvel Parte 3: Análise do Pegasus | 8kSec

O texto analisa uma variante do malware Pegasus/Chryasor em um aplicativo chamado 'Media Sync'. O estudo revela técnicas de obstrução e a presença de permissões perigosas, como android.permission.BRICK. A análise também destaca componentes ocultos que são carregados dinamicamente durante a execução do aplicativo, bem como funções refletivas criptografadas para esconder o comportamento malicioso.

Compartilhar
Mobile Malware Part 3: Pegasus Analysis | 8kSec

Detalhes do Aplicativo

Nome: Media Sync

Pacote: seC.dujmehn.qdtheyt

SHA-256 Hash: bd8cda80aaee3e4a17e9967a1c062ac5c8e4aefd7eaa3362f54044c2c94db52a

Introdução

Bem-vindos de volta, entusiastas de malware, ao terceiro capítulo da nossa saga sobre Análise de Malware Móvel! Hoje, mergulharemos de cabeça no mundo de uma variante Pegasus/Chryasor que é tão imprevisível quanto um passeio em montanha-russa. Ao longo desta análise, desvendaremos técnicas de camuflagem enganosas e embarcaremos em uma jornada emocionante através de uma horda de binários maliciosos.

Então, sem mais delongas, vamos começar!

Análise

Vamos iniciar a análise da amostra usando o JADX para obter uma ideia do que o malware Android está fazendo.

Android Manifest.xml

Permissões

[..REDACTED..]

[..REDACTED..]

Podemos ver que o aplicativo está solicitando uma grande quantidade de permissões, incluindo permissões perigosas como android.permission.BRICK, android.permission.MOUNT_FORMAT_FILESYSTEMS, android.permission.DIAGNOSTIC e muito mais.

Componentes

//AndroidManifest.xml

[..REDACTED..]

Há muitos componentes cujos nomes são camuflados (provavelmente), mas o ponto mais importante a notar é que 90 % desses componentes não estão presentes no disco/apk.

Agora, o que significa essa declaração de que os componentes não estão presentes no apk?

Básicamente, neste arquivo APK, o código Smali / Java desses componentes não está incluído e possivelmente é carregado em tempo real usando DexClassLoader, InMemoryDexClassLoader.

Os componentes nomeados EdQBqhCHusuyluh, FydwHusuyluh e SehuHusuyluh são os que estão presentes no apk.

Caso também olhemos para a seção de Recursos, poderíamos ver o diretório org/eclipse/paho/client/mqttv3, popularmente conhecido como Paho Android Service, que é famoso pela biblioteca de cliente MQTT.

O MQTT é o protocolo padrão usado para se comunicar com um servidor IoT/C2C via conexão TCP/IP.

Também, se descompilarmos o apk e olharmos dentro res/raw, poderíamos ver binários nomeados addk, take_screen_shot, libk, que abordaremos nas seções seguintes.

Análise do Código Fonte

Vamos começar nossa análise a partir do Broadcast Receiver EdQBqhCHusuyluh

Dica: A primeira função que é executada ao chamar um Broadcast Receiver é a função OnReceive(Context, Intent).

//Path -> seC/dujmehn/Besqjyed/EdQBqhCHusuyluh.java
@Override 
public void onReceive(Context context, Intent intent) {
    BuBlJJJJJXDFKYirSooj(ZQjvHTeBlVUCiutw(QVozqddybqHRWaDm(QmdafFtUsXvVcahq(mprchPoltGTBsyTr(new StringBuilder(onReceive9584()), mRvgkfrWJgjwKERW(new Date())), onReceive9585()), igMiBMAFlnpIGcji(intent))));
    try {
        jCtyZivgTauTIKZr(utdMSQrIClmTpHFk(context), new ComponentName(context, EdQBqhCHusuyluh.class), 1, 1);
    } catch (Throwable unused) {
        IVOkEjCYymmGsred(yeaXZTigXaYYobJf(wBYoNCNZXpMFYvJJJJJF(new StringBuilder(onReceive9586()), DXEOUgKPIcuXDPPe(r0))), r0);
    }
    try {
        pJgvOKzMdYWbWRjG(context, new Intent(context, QffIuhlysu.class));
    } catch (Throwable unused2) {
        PbNrMrmkNhtwhwfM(SldrPqhEjvpDJzsX(gwnDiWdGqIIaSogW(new StringBuilder(onReceive9587()), HhkJJJJJBuHgJZBJSeUv(r0))), r0);
    }
}

Vamos começar a analisar a linha,

BuBlJJJJJXDFKYirSooj(ZQjvHTeBlVUCiutw(QVozqddybqHRWaDm(QmdafFtUsXvVcahq(mprchPoltGTBsyTr(new StringBuilder(onReceive9584()), mRvgkfrWJgjwKERW(new Date())), onReceive9585()), igMiBMAFlnpIGcji(intent))));

Se verificarmos a função BuBlJJJJJXDFKYirSooj(), podemos notar uma chamada de função reflexiva.

//Path -> seC/dujmehn/Besqjyed/EdQBqhCHusuyluh.java
public static void BuBlJJJJJXDFKYirSooj(String str) {
  seC.dujmehn.qdtheyt.s.q.q.class.getMethod(BuBlJJJJJXDFKYirSooj5955(), String.class).invoke(null, str);
}
}
}
}

Agora uma pergunta pode surgir. O que é uma chamada de função reflexiva?

Considere isso como uma maneira alternativa de chamar uma função específica de uma determinada classe/pacote. Para chamar uma função Java através da reflexão, precisamos usar duas funções: getMethod() e invoke().

getMethod() → obtém o método especificado desta classe com os tipos de parâmetros especificados e retorna um objeto Method para o método especificado. Ele tem dois parâmetros:

  • methodName, que é o método a ser obtido.
  • parameterType, que é o array dos tipos de parâmetros do método especificado.

invoke() → invoca o método usando o objeto Method.

Se verificarmos BuBlJJJJJXDFKYirSooj5955() (que é simplesmente uma função de criptografia Base64 + XOR), ela retorna o nome da função que está sendo invocada.

import java.util.Base64;
    public class MyClass {
public static void main(String[] args) {
int x = 10;
int y = 25;
int z = x + y;
// Decodifica a string codificada em Base64
byte[] wjxzqy = Base64.getDecoder().decode("BQ==");
// Converte o array de bytes em uma string
String decodedString = new String(wjxzqy);
// Cria um objeto StringBuilder
StringBuilder decryptedString = new StringBuilder();
// Descriptografa a string usando uma cifra XOR simples
for (int i = 0; i < decodedString.length(); i++) {
decryptedString.append((char) (decodedString.charAt(i) ^ "d8c7862163c34762ac7a51ee3af50058".charAt(i % "d8c7862163c34762ac7a51ee3af50058".length())));
// Obtém a string descriptografada
String w = decryptedString.toString();
// Imprime a string descriptografada
System.out.println("Function Name = " + w);
  • Decodifica uma string codificada em Base64 BQ== para um array de bytes e, em seguida, converte-o em uma string.
  • Descriptografa a string usando uma cifra XOR simples. Itera por cada caractere da string decodificada e realiza uma operação XOR com o caractere correspondente da chave d8c7862163c34762ac7a51ee3af50058 de maneira cíclica.
  • A string descriptografada é armazenada na variável w.

No caso de executar o código acima, a saída impressa é a. Portanto, estamos invocando a função a do pacote seC.dujmehn.qdtheyt.s.q.q. O problema é que a classe q também não está presente no apk.

Aqui está o objetivo das outras funções refletivas obfuscadas:

  • ZQjvHTeBlVUCiutw() → chama o método toString da classe String.
  • QVozqddybqHRWaDm(), QmdafFtUsXvVcahq() e mprchPoltGTBsyTr() → chamam o método append da classe StringBuilder.
  • onReceive9584() [não usa Reflection] → retorna OnAlarmReceiver onReceive:
  • mRvgkfrWJgjwKERW() → retorna o horário GMT.
  • onReceive9585() → retorna a string action:.
  • igMiBMAFlnpIGcji() → retorna a ação da intent usada para chamar este Broadcast.

Caso combinemos tudo isso, a string poderia parecer:

"OnAlarmReceiver onReceive: " + GMT time + "action:" + Intent action

que é passada para a função a().

Agora, vamos nos concentrar no código dentro da primeira cláusula try.

jCtyZivgTauTIKZr(utdMSQrIClmTpHFk(context), new ComponentName(context, EdQBqhCHusuyluh.class), 1, 1);

A função jCtyZivgTauTIKZr() usa reflexão para chamar o método setComponentEnabledSetting() da classePackageManager. Se procurarmos por essa função na documentação oficial do Android para Package Manager, podemos ver que esta função é usada para definir a configuração de habilitação para um componente de pacote. Neste caso, eles estão ativando o componente EdQBqhCHusuyluh.

Caso analisemos o código da última cláusula try,

pJgvOKzMdYWbWRjG(context, new Intent(context, QffIuhlysu.class));

pJgvOKzMdYWbWRjG() é uma função que usa reflexão para chamar o método startService(). Neste código eles estão chamando o serviço QffIuhlysu (que também não está presente no disco).

Vamos seguir para o próximo componente disponível.

FydwHusuyluh.java

Assim como no componente anterior, vamos iniciar nossa análise a partir da função OnReceive()

//Path -> seC/dujmehn/Besqjyed/FydwHusuyluh.java
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
  try {
      String zoSDMlVDuwCIxpyV = zoSDMlVDuwCIxpyV(intent);
      mhVReMZccjAUGetY(jzxTDWGVbDeOjxsz(bGFBgWEDBqkoKoqb(new StringBuilder(onReceive7320()), zoSDMlVDuwCIxpyV)));
      JiQdkRDOwqunNJck(onReceive7321());
      ctWtShGoSwHSSxxr(CeWpeHaKgNFpdtUu(context), new ComponentName(context, FydwHusuyluh.class), 1, 1);
      String CPsvElsJJJJJdEWxvPak = CPsvElsJJJJJdEWxvPak(intent, onReceive7322());
      int UgtRmbrTtaChMXod = UgtRmbrTtaChMXod(intent, onReceive7323(), 1);
      YrXmfFPhiJJJJJVbIkvE(hBgTzNTeetEdKWyu(ivzmJYjJJJJJtYdaNrmk(new StringBuilder(onReceive7324()), CPsvElsJJJJJdEWxvPak)));
      nwXpEeIBSquoScUy(xwnXHzXYlBXFhbbE(CmTufwtRBPtdCAAK(new StringBuilder(onReceive7325()), UgtRmbrTtaChMXod)));
      Intent intent2 = new Intent(context, QffIuhlysuFydwuh.class);
      woYYyPIDYwvgDBiD(intent2, zoSDMlVDuwCIxpyV);
      CDdhpIDdKrIqKIRG(intent2, onReceive7326(), CPsvElsJJJJJdEWxvPak);
      PmhcIDdlIVkAssEa(intent2, onReceive7327(), UgtRmbrTtaChMXod);
      doFNUlGakzJJJJJTxsmD(context, intent2);
  } catch (Throwable unused) {
      wtyjhQukAxKGueQp(JsOyKHvPGlltfrdO(kqMxGArxaRFdleCJJJJJ(new StringBuilder(onReceive7329()), QbdEweZZIWkmOBuP(r0))), r0);
  }
}

Vamos analisar o bloco try linha por linha.

String zoSDMlVDuwCIxpyV = zoSDMlVDuwCIxpyV(intent);zoSDMlVDuwCIxpyV usa reflexão para retornar a ação do objeto intent

mhVReMZccjAUGetY(jzxTDWGVbDeOjxsz(bGFBgWEDBqkoKoqb(new StringBuilder(onReceive7320()), zoSDMlVDuwCIxpyV))); → Passa uma string que parece ser PingReceiver onReceive action: <intent_action> para a função a do pacote seC.dujmehn.qdtheyt.s.q.q

Ao longo das próximas 4-5 linhas, diferentes strings são passadas para essa função misteriosa a().

Eles também estão habilitando este receptor usando a função setComponentEnabledSetting()

A seguir, eles criam uma intent e passam vários extras para ela e então iniciam um novo serviço, ou seja, QffIuhlysuFydwuh

Vamos nos mover para o próximo receptor, ou seja, SehuHusuyluh

SehuHusuyluh.java

Se iniciarmos a análise a partir da função OnReceive(),

//Path -> seC/dujmehn/Cutyq/SehuHusuyluh.java
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
    try {
        String kfIJkjFmWhBsjKBb = kfIJkjFmWhBsjKBb(intent);
        JwmywxXEFXZRWoTm(jJJJJJfgySwEmZtctJJJJJZv(wgcFPzkzJvQwpQNR(NQVJqQmdtXCzMZwr(lBlRxthHkMBxrmAh(gqogknrztmhAqnbU(fESOpFhfggYrqIbU(MxoKecvKTxNPvSih(new StringBuilder(onReceive3935()), kfIJkjFmWhBsjKBb), ", Agent Version: "), seC.dujmehn.r.r.d), ", Date: "), RCvwiRQWhNVPiVwM(new Date())), onReceive3936())));
        RkuTHMzDzKwBxnYG(f39v, new RunnableC0259u(this, context, intent, kfIJkjFmWhBsjKBb));
    } catch (Throwable unused) {
        ARXNnuprJJJJJYzvtkOB(yKzdwDVMxrvTAScq(mYynmeuKZgangzlk(new StringBuilder(onReceive3937()), YPSHXSTQPliIBVkJJJJJ(r0))), r0);
    }
}

O primeiro par de linhas basicamente obtém a parte action da intent e passa uma string para a função a(), sobre a qual não precisamos entrar em detalhes.

A última linha basicamente inicia um novo Handler, o que inicialmente parece perigoso, mas nada muito acontece

Agora que concluímos a análise do código-fonte Java, uma questão para refletir:

Por que eles usaram Reflection para invocar funções em vez de chamá-las diretamente?

No fato, é uma estratégia muito boa usar Reflection por 2 razões importantes

  • Evitar verificações de analisador estático e obter pontuações baixas para serem marcadas como um aplicativo normal (Consulte este artigo para mais informações)
  • Tornar o processo de reversão de uma amostra muito mais difícil.

Binários ELF

Agora voltamos nossa atenção para os binários que encontramos no diretório res/raw.

Os binários incluem addk, cmdshell, libk, sucopier e take_screen_shot.

take_screen_shot

Se carregarmos o binário em um decompilador, poderíamos ver,

função principal do take_screen_shot binary

No método principal, podemos ver que estamos passando 2 argumentos, um deles sendo uma string de destino. Além disso, podemos ver que estamos passando uma string que parece ser o nome do binário.

Agora qual é a relevância do binário /dev/graphics/fb0?

Básicamente /dev/graphics/fb0 representa um Framebuffer. Framebuffers são essenciais para renderizar a IU na tela. Se olharmos no repositório de código-fonte do Android para Framebuffer [link1, link2], a execução do código depende da existência deste binário, o que explica sua importância e características (Certifique-se de verificar os links para o código-fonte).

Nós poderíamos até mesmo usar fb0 para obter capturas de tela usando os comandos,

adb pull /dev/graphics/fb0 fb0
ffmpeg -vframes 1 -vcodec rawvideo -f rawvideo -pix_fmt rgb32 -s 320x480 -i fb0 -f image2 -vcodec png image.png

libk

Se começarmos a analisar este binário ELF a partir de init(), podemos ver

binário ELF libk init function com libbinder.so

Podemos ver strings como /system/lib/libbinder.so e esta string estranha que parece ser _ZN7android14IPCThreadState8transactEijRKNS_6ParcelEPS1_j, que na verdade é uma função definida em libbinder.so. Depois de procurar por várias funções, encontramos um pedaço de código interessante.

referência do framework IInputContext no libk

Se realmente focarmos na string codificada com.android.internal.view.IInputContext e pesquisamos um pouco sobre ela, podemos ver que é um Framework Interno do Android que lida com contextos de entrada e operações relacionadas à entrada.

Aqui está o link ao Repositório de Código-fonte do Android para aprender mais sobre as funções implementadas. Isso fica ainda mais interessante com o próximo conjunto de código.

criação de arquivo temporário para dados de teclado

Aqui podemos ver que eles estão criando alguns arquivos temporários nomeados ulmntdd.tmp e os dados são escritos neles. Se verificarmos as referências para esta chamada de função, encontramos,

__android_log_print(3,"Jigglypuff_KS",
                          "call_func_from_parcel. function code: %d firstInt: %d secondInt %d dataPo sition: %d"
,param_1,uVar2,uVar3,uVar4);
android::Parcel::readString16();
__android_log_print(3,"Jigglypuff_KS","call_func_from_parcel. string16: (x): %x ,  char16: %s"
,*local_20,local_20);
iVar1 = strlen16(local_20);
mw_FUN_0001551c(iVar1,(byte *)local_20);
android::String16::~String16((String16 *)&local_20);

Juntando tudo isso, este binário ELF pode estar escrevendo todos os logs do teclado para o arquivo temporário que é posteriormente exfiltrado.

addk

Este binário ELF é usado para realizar a injeção de processo e possivelmente injetar shellcode para fornecer uma porta de entrada aos atacantes.

Se olharmos na função principal,

função main do binário ELF addk para injeção de processo

Podemos ver que o primeiro argumento passado é o PID do processo, que está sendo passado à função inject_process. Se verificarmos essa função,

função inject_process com referências de linker e mmap

Podemos ver algumas strings importantes como /system/bin/linker, que são geralmente usadas para carregar o executável ELF na memória (Para entender mais, consulte este blog), uso de mmap (usado para criar uma nova mapeamento no espaço de endereços virtuais do processo chamador).

Conclusão

No final das contas, nossa exploração desta variante Pegasus/Chryasor expandiu nosso conhecimento significativamente. Entendemos o conceito crucial de Reflection e testemunhamos a versatilidade que oferece aos criadores de malware. Ao examinar binários ELF como “addk,” “libk” e “take_screen_shot,” adquirimos valiosas informações sobre o mundo do código malicioso.

A descoberta do MQTT como método de comunicação destacou as formas intricadas pelas quais o malware mantém conexões e se comunica com seus operadores. Embora este exemplo não seguisse os padrões comuns de malware, reforçou a importância da adaptabilidade no campo da cibersegurança.

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa