[Engenharia Reversa] Reconhecendo a Implementação Customizada do Algoritmo RC4 no REvil Ransomware

Ícaro César
12 min readNov 21, 2023

--

Olá companheiros, neste primeiro artigo iremos abordar como podemos reconhecer por meio de engenharia reversa, a implementação customizada (sem o uso de Windows APIs) do algoritmo de criptografia RC4. Para tornar esta análise mais verídica, iremos utilizar um sample real de um Ransomware do grupo APT Russo de RaaS (Ransomware as a Service) chamado REvil, também conhecido como Sodinokibi.

Porque Autores de Malwares Utilizam Algoritmos de Criptografia?

Ao compilar um binário de maneira simples, não é difícil identificar suas capacidades por meio da engenharia reversa. Os adversários, principalmente os mais sofisticados, sempre irão implementar uma maneira de ofuscar a funcionalidade dos malwares utilizados em suas campanhas.

Obviamente, existem diversos métodos de ofuscação, incluindo o uso de algoritmo de criptografia, implementação de algoritmos de hashing, compactação de seções de arquivos PE (no caso dos samples serem binários executáveis para sistemas Windows), entre outros. A implementação destes métodos tem o objetivo de dificultar a análise estática do espécime. Geralmente, quando um espécime emprega estes métodos de ofuscação, nós utilizamos o termo packed, que indica que o artefato contém conteúdo encriptado/compactado, e que será automaticamente decriptado ou descompactado em tempo de execução (possivelmente na memória).

Eu sei, eu sei, para quem nunca ouviu estes termos, pode parecer muito complicado. Mas leia os links de referência que estou colocando, exponha-se a assuntos que você tem dificuldade, e com o tempo a dor vai passar!

O que é o Algoritmo RC4 e Como é Implementado?

Agora que já compreendemos o porque autores de malware utilizam algoritmos de criptografia em seus espécimes, vamos compreender como é implementado de maneira customizada (sem a utilização de Windows APIs) o algoritmo de criptografia RC4, um dos mais utilizados por autores de malware, pela sua fácil implementação customizada em código.

Primeiramente, devemos lembrar que o algoritmo de criptografia RC4, representa o tipo de Criptografia Simétrica. Isso significa, que para criptografar e descriptografar os dados, é necessário apenas uma chave secreta. Para o RC4, esta chave secreta deve ter entre 1–256 bytes (podendo ser uma sequência de caracteres aleatórios).

De maneira simples, o algoritmo de criptografia RC4 contém três estágios:

  • Estágio 1 — Key Scheduling Algorithm (KSA): É a fase de inicialização do que chamamos de SBox (Substitution Box), ou seja, é a fase na qual o algoritmo cria um array onde cada valor é igual a sua posição no array, indo de 0 à 256 (prestem atenção neste número). Este array será gerado de forma pseudoaleatória a partir da chave secreta, e seu output será a tabela de permutação. É importante notar, que o algoritmo irá realizar esta ação em formato de um loop for, 256 vezes.Abaixo, segue a implementação desta fase em Python, para melhor compreensão.
def ksa(chave):
"""
Gera uma tabela de permutação (array) pseudoaleatória a partir da
chave secreta fornecida.

Quais argumentos esta função recebe?

A chave secreta

Qual o output desta função?

A tabela de permutação aleatória.

"""

# Inicializa a tabela de permutação.
permutacao = list(range(256))

# Percorre a chave fornecida.
for i in range(len(chave)):
# Gera um índice aleatório.
indice_aleatorio = i % len(permutacao)

# Troca os elementos da tabela de permutação.
permutacao[i], permutacao[indice_aleatorio] = permutacao[indice_aleatorio], permutacao[i]

return permutacao
  • Estágio 2 — Pseudo-Random Generation Algorithm (PRGA): Nesta fase, o PRGA irá receber como argumento a tabela de permutação criada na fase anterior (KSA), e irá utilizá-la para gerar uma sequência pseudoaleatória de dados. É nesta fase, que o algoritmo irá gerar o que chamamos de KeyStream, que irá ser o responsável por encriptar os dados que estão em seu formato original (plaintext). É importante notar que o algoritmo irá realizar esta ação, em formato de loop while, 256 vezes. Abaixo, segue uma implementação da fase PRGA em Python, para melhor compreensão.
def prga(tabela_de_permutacao):
"""
Gera uma sequência de dados pseudoaleatórios (KeyStream) a partir da tabela de
permutação fornecida.

Quais argumentos esta função recebe?

A tabela de permutação, gerada na fase KSA.

Qual o output desta função?

O Keystream (uma sequência de dados pseudoaleatórios)

"""

# Inicializa os índices i e j.
i = 0
j = 0

# Gera um número aleatório.
while True:
# Gera um novo índice j.
j = (j + 1) % 256

# Gera um novo índice i.
i = (i + tabela_de_permutacao[j]) % 256

# Troca os elementos da tabela de permutação.
tabela_de_permutacao[i], tabela_de_permutacao[j] = tabela_de_permutacao[j], tabela_de_permutacao[i]

# Retorna o elemento na posição i da tabela de permutação.
return tabela_de_permutacao[i]
  • Estágio 3 — A Operação XOR (Encriptação dos Dados): Esta é a função principal (main), onde de fato ocorre a encriptação dos dados utilizando as funções dos estágios anteriores. Para a encriptação ocorrer, a função principal irá receber como argumento, a chave secreta e os dados a serem encriptados (ou decriptados). E no corpo da função principal, irá ser executado os estágios do KSA e PRGA, para que enfim, seja feita uma operação XOR entre a KeyStream e os dados em formato original. O resultado da operação XOR, será a encriptação. Abaixo, segue a implementação desta fase em Python, para melhor compreensão.
def xor(dados, chave):
"""
Combina a KeyStream gerada pelo PRGA com os dados em formato original
em uma operação XOR, com o objetivo de serem criptografados/descriptografados.

Quais argumentos esta função recebe?

1. Os dados em formato original a serem criptografados/descriptografados.
2. A chave secreta.

Qual o output desta função?

Os dados criptografados/descriptografados.

"""

# Gera a tabela de permutação pseudoaleatória.
tabela_de_permutacao = ksa(chave)

# Gera a KeyStream.
stream = prga(tabela_de_permutacao)

# Combina os dados em formato original com a KeyStream,
# por meio de uma operação XOR.
for i in range(len(dados)):
dados[i] = dados[i] ^ stream()

return dados

Perfeito, agora que já sabemos como funciona o algoritmo de criptografia RC4, e porque os autores de malware utilizam tais métodos, vamos para a análise de um sample real do Ransomware REvil, e tentar identificar padrões da implementação do algoritmo RC4, por meio de um Disassembler/Decompiler.

Identificação da Implementação do RC4 no REvil Ransomware

Agora vamos de fato colocar a mão na massa, e realizar a identificação da implementação do RC4 por meio da engenharia reversa, em um sample do REvil. Abaixo podemos observar o artefato, e seu hash SHA256.

Apenas para validarmos que estamos lidando com um sample real, abaixo segue o print da análise do VirusTotal.

Você pode ter acesso a este sample, por meio do MalwareBazar, ou clicando aqui. Obviamente, eu não preciso dizer que é necessário ter cuidado ao manusear um sample real.

Neste artigo, e provavelmente nos próximos, eu irei utilizar o Ghidra para realizar a engenharia reversa, mas obviamente, o mesmo pode ser alcançado por meio de qualquer outro Disassembler/Decompiler.

Validando a Implementação da Técnica de Packing no REvil

Apenas com o objetivo de validar a implementação da técnica de ofuscação, que discutimos anteriormente, abaixo podemos observar que a ferramenta Detect It Easy identificou uma seção com uma alta entropia (por meio de linha de comando e por meio da interface gráfica), o que significa que ela pode estar packed (comprimida/encriptada).

Identificando Implementação da Técnica de Packing — RC4

Agora que já sabemos que uma técnica de ofuscação foi implementa, vamos importar o binário revil.malz para o Code Browser do Ghidra.

Para maiores detalhes de como criar projetos e importar arquivos no Ghidra, eu recomendo o vídeo abaixo.

Provavelmente irei fazer alguns artigos sobre este tópico no futuro, até lá, o Youtube pode ajudar aos curiosos (: .

Depois de abrirmos o nosso sample no Code Browser, vamos executar um processo de enumeração de chamadas de Windows APIs feitas pelo sample. Irei executar esta enumeração de forma automatizada, por meio de um script em Python que eu desenvolvi e publiquei em meu Github.

Este script, irá coletar as informações referentes Tabela de Símbolos Externos (que são as importações externas) do binário, e irá imprimir de maneira mais agradável no console de script do Ghidra. Abaixo, podemos observar o output gerado pela execução deste script.

Como podemos observar na imagem acima, apenas seis Windows APIs estão sendo importadas de forma explícita pelo sample. Isso é muito incomum para qualquer binário não-packed.

A presença de Windows APIs como LoadLibraryA, GetCurrentProcessID, e principalmente o GetModuleHandleA, nos permite supor que o artefato irá carregar determinadas Windows APIs em tempo de execução, com o propósito de evadir defesas como AV/EDR. Possivelmente, o artefato utilizará estas APIs, para localizar as demais APIs após o processo de dinâmico de decriptação (unpacking).

Agora que sabemos que não há nenhuma pista sobre qual é o método de ofuscação (sabemos que é o RC4, mas vamos manter a fantasia (: ), vamos para a análise estática das instruções em Assembly por meio do Disassembler, e utilizando a ajuda do Decompiler que irá imprimir um pseudocódigo em C das instruções em Assembly.

Acima, podemos observar a função ‘entry’, que são as instruções do início de execução do artefato, pela CPU (não é a função main). Nesta função, já podemos observar que o sample faz uma chamada para uma função local, identificada pelo Ghidra como FUN_0040110b, por meio do opcode CALL (o nome é autoexplicativo). Vamos investigar o conteúdo desta função, clicando duas vezes nela (tanto por meio do Disassembler, quanto por meio do Decompiler).

Acima, podemos começar a notar alguns padrões que abordamos na seção teórica da implementação do algoritmo RC4. Podemos observar a chamada da função interna FUN_00401000, que recebe um parâmetro, uma variável local local_124 que tem o tamanho de 256 bytes.

Ainda na imagem acima, podemos observar por meio da operação realizada no opcode LEA, que o param_1 será o endereço referente a variável local local_124. É interessante identificar este fato, pois no pseudocódigo em C, não temos essa informação de forma explícita.

Apesar de termos visto nas seções teóricas da implementação do RC4, que a principal constante é a identificação do valor em decimal 256, ainda precisamos validar o fluxo em loop nas fases do KGA e PRGA, além da fase de Operação XOR entre o KeyStream e os dados criptografados.

Para isso, vamos continuar a nossa análise das funções locais, com o objetivo de identificar o padrão da implementação do algoritmo de RC4.

Ao adentrar na primeira função local (FUN_00401000) anteriormente identificada, recebendo uma variável local tendo 256 bytes de tamanho, podemos observar alguns detalhes interessantes.

Vimos que na função anterior, o endereço de memória da variável local local_124 foi atribuía ao param_1, recebido na função anterior. Aqui, podemos observar que a CPU move o conteúdo (que tem 256 bytes de tamanho) do param_1 para o registrador EAX. E a partir deste momento, o binário inicia um processo de loop iterando 1 a cada rodada. Isso lembra alguma coisa? (:

Se analisarmos este fluxo através do modo de visualização gráfica do Ghidra, como podemos observar acima, ficará muito mais claro a execução do loop.

O Ghidra e os demais Disassemblers/Decompilers, nos permitem que possamos renomear funções, nomes de variáveis, e etc. Isto com o objetivo de ao longo da análise, a visualização do código em Assembly e do pseudocódigo em C fique mais legível.

Abaixo, podemos observar que ao renomearmos, as funções, variáveis e valores são modificadas globalmente, permitindo a melhor visualização da reutilização daqueles objetos.

Acima como podemos observar, os argumentos arg_chave e param_chave são reutilizados em mais duas funções. É importante notar, que arg_chave e param_chave contém o mesmo valor, conforme analisamos anteriormente. Assembly é uma linguagem MUITO arcaica, e o Decompiler faz o melhor que pode para poder analisá-lo. Por isso, é importante termos uma compreensão das operações realizadas em Assembly no Disassembly.

Agora vamos analisar a segunda função FUN_0040100e, para identificarmos mais padrões da implementação do algoritmo RC4.

Ao entrar na segunda função FUN_0040100e, nos deparamos com mais uma referência ao padrão da implementação do RC4, utilizando a atribuição de valores de 256 bytes, além da utilização da função KSA.

Este fluxo de análise, nos permite compreender que esta função é refente ao PRGA, aonde estará sendo criado o KeyStream a partir da tabela de permutação criada pelo KGA. Acima, podemos observar a visualização gráfica do loop desta função.

Renomearemos as variáveis, funções e valores para melhor visualização e seguiremos o fluxo de nossa análise para a próxima função

Já seguindo direto para a visualização gráfica, pois esta função fica óbvia a sua funcionalidade, podemos observar a operação XOR do parâmetro recebido. Onde podemos supor que esta função é referente a fase de Operação XOR para decriptar os dados (ou strings) criptografados do artefato, em formato de loop.

Plus — Identificação da Capacidade de Second Stager

Espero que você não tenha cochilado ao longo do artigo, como o escopo deste artigo é referente a identificação da implementação customizada do algoritmo de criptografia RC4, não iremos seguir com a análise pois já alcançamos o nosso objetivo, apesar de não estarmos nenhum pouco cansados com a quantidade de informações descrita neste artigo.

Apenas para dar um gostinho da capacidade de evasão de defesa descrita anteriormente, seguindo fluxo da função que renomeamos como Rotina_Pre_Decrypt, dentro da função FUN_0040139d. somos capazes de observar exatamente o que pressupomos no começo, sobre o carregamento de bibliotecas dinâmicas, e abaixo veremos um Plus.

Quando abrimos a nossa função alvo no modo de visualização gráfica do Ghidra, percebemos o quão grande esta função é. O que nos permite supor, que esta é a nossa função de Decriptação, onde possivelmente iremos observar o uso das funções identificadas anteriormente, para decriptar dados criptografados. Mas, o que seria estes dados criptografados?

Ao analisar a função FUN_0040139d, além de sermos capazes de identificarmos a utilização das funções do algoritmos RC4, também podemos identificar que o sample está realizando um parser dinâmico de outro binário que está dentro dele.

Na imagem acima, podemos observar que além da utilização da função KSA, está sendo realizada a utilização daquelas APIs que citamos no início do artigo, para o carregamento dinâmico de bibliotecas/APIs e etc, e também a uma condicional que checa se os valores dentro de um endereço de memória (DAT_0040300 e DAT_0040300 + DAT_0040303c), são referentes aos Magic Numbers do cabeçalho de um executável Windows, que são as strings MZ (0x5a4d) e PE (0x4550). A seguir, podemos observar os valores estáticos destas referências de endereços de memória, e como podemos observar nas imagens a seguir, de maneira estática, parecem ser dados aleatórios.

Isto nos leva a supor, que os dados criptografados são um arquivo que irá ser executado em memória (ou será escrito em disco, e executado). Ou seja, possivelmente, os dados criptografados é um Second Stager (um segundo estágio de infecção do malware).

Conclusão

Eu espero manter uma regularidade nas postagens, pois o campo de Engenharia Reversa é extenso, e não há tantos profissionais qualificados no mercado brasileiro, há excelentes profissionais, mas poderíamos ter mais. E creio que não temos,por falta de interesse, ou, por falta de material acessível. Espero que este artigo tenha despertado sua curiosidade.

--

--

Ícaro César
Ícaro César

Written by Ícaro César

Cyber Security Researcher |Threat Hunter | Reverse Engineering

Responses (1)