Como lidar com criptografia AES em análise de malware

Na investigação de malwares, existem basicamente dois tipos de análise: estática e dinâmica. De maneira simplificada, a análise estática compreende todos os modos de estudar um software sem executá-lo, enquanto sua contraparte dinâmica é baseada em observar o programa durante seu funcionamento. É de interesse dos autores de malware dificultar ambas as formas de análise, dificultando assim a detecção.

Neste boletim focaremos na criptografia AES em análise de malware. Ou seja, de que forma ela é empregada para dificultar o estudo estático de softwares maliciosos e como lidar com esse tipo de técnica.

Amostra escolhida

Esse boletim é baseado em uma amostra do HawkEye Keylogger de janeiro de 2022 (MD5:019A689DCC5128D85718BD043197B311). Trata-se de um executável .NET que emprega diversas técnicas de ofuscação, entre elas o uso de criptografia AES para ocultar strings importantes relacionadas a seu
funcionamento.

Sendo um .NET, sua análise estática envolve código em C#. Entendemos que essa característica facilita a compreensão dos conceitos que apresentaremos, uma vez que sua leitura é mais simples que assembly, linguagem envolvida na análise de malwares em outras linguagens.

Por fim, essa amostra está disponível de maneira pública na internet. Isso permite que os interessados possam reproduzir todos os passos de análise aqui delineados em suas próprias máquinas.

Identificação

O emprego de criptografia AES nem sempre será óbvio ao analisar o conteúdo criptografado em si. Consideremos o exemplo abaixo, uma das funções principais do Hawkeye Keylogger:

Figura 1: Função com ofuscação de strings

À primeira-vista, as strings declaradas na função acima estão codificadas com base64 (uma pista crucial para chegar a essa conclusão é a presença de “==” ao final do texto). Nosso primeiro impulso é decodificar esse conteúdo e, com sorte, descobrir o que o autor do malware decidiu esconder de nós. Peguemos como exemplo a string gnfyANw8:

gnfyANw8 = "EcCYDcH+PxTAszyHU/StRg==";

Após decodificação, obtemos o seguinte resultado:

.À.
Áþ?.À³<.Sô.F

Ok, estamos sem sorte. O resultado é ilegível, o que não faz sentido para uma string. Caso sigamos analisando o código após essas declarações de variáveis, encontraremos uma pista de que sua desofuscação não envolve apenas base64. Cada uma delas é passada individualmente como parâmetro para uma mesma função. Esse comportamento é demonstrado no trecho de código a seguir:

Path.Combine(this.njZ5r2Fw(this.bfuPrPbT), this.njZ5r2Fw(this.gnfyANw8)) 

A imagem abaixo traz o conteúdo dessa função na íntegra.

Figura 2: Função responsável por descriptografar variáveis

AES e Rjindael

A sigla AES se refere a um padrão de criptografia (Advanced Encryption Standard) estabelecido pelo NIST. Tal padrão engloba três membros da família de cifras Rjindael, caracterizado pelo tamanho em bits da chave utilizada. Especificamente, são parte da AES as cifras Rjindael que utilizam chaves de 128, 192 e 256 bits.

AES se enquadra no grupo de criptografia simétrica, o que significa que uma mesma chave é usada para criptografar e descriptografar um conteúdo. A criptografia assimétrica (como por exemplo RSA) requer uma chave privada para ser revertida; por esse motivo ela é muito usada em ransomware, mas raramente é empregada para ofuscação.

Criptografia é um campo muito complexo e não é o intuito deste boletim explicar o funcionamento passo a passo do padrão AES. Explicaremos apenas os conceitos necessários para identificar sua presença em funções (como naquela demonstrada pela Figura 2) e realizar a descriptografia de elementos sem que seja preciso executar o malware (isso é, manteremos toda a abordagem focada em análise estática). O primeiro passo é associar menções a Rjindael com AES.

Em seguida, o código em questão começa a realizar operações envolvendo arrays. Destacamos a seguir os trechos relevantes:

byte[] array = new byte[32];
byte[] sourceArray =
md5CryptoServiceProvider.ComputeHash(Encoding.ASCII.GetBytes(s));
Array.Copy(sourceArray, 0, array, 0, 16);
Array.Copy(sourceArray, 0, array, 15, 16);
rijndaelManaged.Key = array;
rijndaelManaged.Mode = CipherMode.ECB;

O primeiro ponto de atenção é o tamanho do array criado: 32 bytes, ou 256 bits. Ao final do trecho destacado, vemos que esse array será utilizado como chave para a cifra Rjindael – ou seja, trata-se de criptografia AES-256. De posse dessa chave, poderemos realizar o trabalho dessa função sem de fato executá-la, frustrando o esforço do autor do malware para que certas strings só sejam descriptografadas durante a execução. Precisamos então obter manualmente o array de nome array.

Criando uma chave AES

Os passos de criação de uma chave AES variam pouco entre aplicações diversas, o que significa que a internet possui amplos recursos que nos ajudam a identificar cada parte dessa rotina na função do item anterior. Em suma, sempre se espera como resultado um array de 8, 24 ou 32 bytes (AES-128, 192 e 256, respectivamente).

No código que analisamos esse array é criado com as seguintes instruções:

byte[] array = new byte[32];
byte[] sourceArray =
md5CryptoServiceProvider.ComputeHash(Encoding.ASCII.GetBytes(s));
Array.Copy(sourceArray, 0, array, 0, 16);
Array.Copy(sourceArray, 0, array, 15, 16);

Já falamos da criação de um array vazio de 32 bytes de tamanho para receber a chave. Em seguida vemos um método para computar o hash da variável s utilizando o algoritmo MD5. Essa variável foi declarada no começo da função que analisamos e possui a string hlNCwqLIdBYdaloFZajsnThCVnhYFN. E por que gerar um hash MD5? Porque independente da entrada, esse hash sempre possui como saída 16 bytes. Esse hash é armazenado em sourceArray.

Em seguida vemos duas linhas empregando o método Array.Copy. Esse método é utilizado no código apresentado com as seguintes entradas:

Copy(Array, Int32, Array, Int32, Int32)

Traduzindo: a função copiará elementos de um array (sourceArray) a partir da posição Int32 (0) para outro array (array) a partir da posição Int32 (0). Essa cópia será de Int32 (16) elementos. Lembrando as características dos dois arrays, entendemos que 16 bytes serão copiados do array que contém o hash MD5 (ou seja, todos os bytes serão copiados) para o array vazio de 32 bytes. Temos então 16 bytes preenchidos e outros
16 bytes em branco em array.

A linha seguinte utiliza o mesmo método para copiar novamente todos os bytes de sourceArray, dessa vez com destino à posição 15 de array. Isso significa sobrescrever o último byte da operação anterior com o primeiro byte de sourceArray e preencher o restante. Temos agora 31 bytes preenchidos e um byte zerado. Para facilitar o entendimento, demonstraremos a seguir as operações descritas nos dois últimos parágrafos.

sourceArray = [6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC E5] (hash MD5 da variável s)
array = [00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00]
(array vazio de 32 bytes)

Primeira operação de cópia:

array = [6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC E5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00]

Segunda operação de cópia:

array = [6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC 6E 13 31 95 5E AE 65 A4 13 8D 96 26 FC E2 CC E5 00]

Eis nossa chave de 32 bytes.

Decifrando variáveis

De posse da chave, só falta entendermos como os inputs para a função que desfaz a criptografia são tratados. Isso é demonstrado nas últimas três linhas da função:

ICryptoTransform cryptoTransform = rijndaelManaged.CreateDecryptor();
byte[] array2 = Convert.FromBase64String(input);
result = Encoding.ASCII.GetString(cryptoTransform.TransformFinalBlock(array2,
0, array2.Length));

As strings são decodificadas de base64 e armazenadas em hexadecimal em um novo array, array2. Esse array será descriptografado com o método cryptoTransform.TransformFinalBlock, convertido para ASCII e armazenado na variável result. Usando essas informações junto à chave que calculamos no item anterior, criamos uma receita no CyberChef para desfazer a criptografia de variáveis empregada pelo Hawkeye Keylogger. Ela está disponível neste link. Vejamos o que cada variável do item 3 contém após a execução da função de descriptografia:

gnfyANw8 = " JavaUpdater.exe
bfuPrPbT = " C:\Users\Ebecco\AppData\Local\Temp\"
hXkfmZ = "Software\Microsoft\Windows NT\CurrentVersion\Windows"
QaP8HK = " Load"

Isso nos permite identificar o arquivo com caminho completo para onde o malware será copiado (“C:\Users\Ebecco\AppData\Local\Temp\JavaUpdater.exe”) e uma chave nova (Load) que será criada no registro (“Software\Microsoft\Windows NT\CurrentVersion\Windows”).

Também podemos procurar por outras funções que chamem a função njZ5r2Fw para encontrar mais strings criptografadas no código. A imagem abaixo traz um exemplo:

Figura 3: Exemplo de string criptografada em outra função

Após utilizar nossa receita do CyberChef, descobrimos que “iqfiKzfW5rUJpUrUzDCUhA==” significa “img”. Trata-se de uma imagem presente nos resources do malware, que será usada em esteganografia para obter um novo executável, que existe apenas em memória. Mas isso já é assunto para um boletim futuro.

Figura 4: Imagem que será transformada em executável durante a execução do malware

Conclusão

A criptografia é um método eficaz para proteger conteúdo, seja ele maligno ou benigno. Ser capaz de identificar sua presença e revertê-la é uma ferramenta importante no arsenal de um analista de malware e estende muito o alcance de uma análise estática.

Como já mencionado ao longo deste relatório, o código que implementa um algoritmo de criptografia costuma ser reutilizado, não escrito do zero para cada implementação. Assim, os conceitos aqui apresentados não se aplicam apenas à amostra escolhida para análise; podem ser empregados na análise de outros malwares que também utilizem criptografia AES. Isso também é verdade para a receita que fornecemos do CyberChef – desde que você possua a chave, outros aspectos dela (como a chave) podem ser adaptados para desfazer a criptografia utilizada em outras amostras.

Se você se sente confortável programando em C#, também pode aproveitar a função presente no código do malware para trabalhar a seu favor. Para este fim, fornecemos uma função genérica para reverter a criptografia, cortesia de Saskia Hiltemann no GitHub.

using System;
using System.Security.Cryptography;
using System.Text;

public class Program
{
     public static void Main()
     {


               string pass = "[STRING CRIPTOGRAFADA]";
               string target= "[STRING DA CHAVE]";
               byte[] keyarray = new byte[32];

               // Base64 decrypt target string
               byte[] target2 = Convert.FromBase64String(target);

               // Set our encryption/decryption key
               MD5CryptoServiceProvider mD5CryptoServiceProvider = new 
MD5CryptoServiceProvider();
               byte[] sourceArray = 
mD5CryptoServiceProvider.ComputeHash(Encoding.ASCII.GetBytes(pass));
               Array.Copy(sourceArray, 0, keyarray, 0, 16);
               Array.Copy(sourceArray, 0, keyarray, 15, 16);

               // Set up Rijndael Decrypt
               RijndaelManaged rijndaelManaged = new RijndaelManaged();
               rijndaelManaged.Key = keyarray;
               rijndaelManaged.Mode = CipherMode.ECB;
               ICryptoTransform cryptoTransformDecrypt = rijndaelManaged.CreateDecryptor();

               // Do it
               byte[] result = cryptoTransformDecrypt.TransformFinalBlock(target2,0, 
target2.Length);
               System.Text.Encoding encoding = new System.Text.ASCIIEncoding();

               // print result
               Console.WriteLine(encoding.GetString(result));

     }
}

Referências

  1. https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
  2. https://github.com/shiltemann/
  3. VX-UNDERGROUND
  4. https://docs.microsoft.com/en-us/dotnet/api/?view=net-6.0

Por Alexandre Siviero

Deixe um comentário

O seu endereço de e-mail não será publicado.