O C++ é uma das linguagens de programação mais utilizadas por desenvolvedores de malware, grupos cibercriminosos e grupos APT. Essa linguagem, uma extensão do C, permite a criação e manipulação de classes e facilita a programação orientada a objetos (POO).
Analisar um código malicioso escrito em C++ pode ser uma tarefa complexa e demorada. Por isso, separamos algumas dicas que podem ajudar no processo de análise de uma amostra de malware. Este guia é especialmente voltado para aqueles que estão iniciando na análise de malware em C++.
Primeiros passos
Primeiro, vamos criar um programa simples em C++ com duas classes. Em seguida, vamos compilá-lo para obter um arquivo executável. Usando a ferramenta IDA Free, poderemos comparar o código-fonte do programa com o pseudocódigo que será gerado pela ferramenta.
Dessa forma, analisaremos semelhanças entre ambos, diferenças, e aprenderemos a detectar objetos e a criar uma estrutura que seja representada por esse objeto. Por fim, veremos outro exemplo, que será sobre uma amostra de malware.
Conceitos básicos de POO
Antes de continuar, vamos revisar o que é um objeto e o que é uma classe em programação orientada a objetos.
- Objeto: Instância de uma classe, entidade que possui um estado e um comportamento.
- Classe: Conjunto de variáveis e métodos apropriados para operar com esses dados.
Primeiro exemplo: algo simples
O seguinte código de exemplo será composto por duas classes chamadas Vaca e Pato, que possuem variáveis e métodos. Além disso, teremos o método main, onde criaremos objetos dessas classes e os utilizaremos.
Em seguida, compilaremos esse código de exemplo para gerar um arquivo executável, que veremos posteriormente com o IDA Free, e dentro do IDA veremos como reconstruir essas classes nas chamadas estruturas.
As seguintes capturas de tela mostram o código-fonte de nossas classes e a lógica do método main do nosso exemplo.


Compilação e análise com IDA Free
A seguir, procederemos a compilar este exemplo e gerar um arquivo binário para visualizá-lo com o IDA Free.
A próxima captura de tela mostra o código compilado do método main no IDA Free do nosso exemplo.

A captura de tela seguinte mostra à esquerda o código-fonte do nosso exemplo e à direita o código pseudocódigo gerado pelo decompilador do IDA Free.

Interpretação do código compilado
Na Figura 4, podemos ver que, quando nosso código C++ é compilado, o IDA Free interpreta os métodos como sub-rotinas em uma posição específica de memória do arquivo executável. Essas sub-rotinas começam com o prefixo "sub_", por exemplo, sub_140001760.
Além disso, o IDA possui uma característica para detectar funções que pertencem a bibliotecas conhecidas ou APIs do Windows e atribui um nome específico a elas, em vez de um nome genérico (sub_XXXXXX). Por exemplo, no quadro à direita da figura, nas linhas 36 e 37, o IDA detectou a função responsável por excluir um objeto da memória e a renomeou como "j_j_free".
Perda de expressividade na compilação
Ao compilar um código com classes e objetos, o conceito de classe, com seus atributos e métodos, se perde, onde os atributos passam a ser posições de memória que armazenam o respectivo valor do atributo. E a expressividade de ambos também se perde. Por exemplo, onde tínhamos pat = new Pato();, agora passamos a ter v5 = operator new (0xC).
A seguinte captura de tela mostra o que acontece com os atributos da nossa classe no momento de compilar um código-fonte.

Seguindo o código decompilado do método main na Figura 3, o primeiro que vemos é a criação de um objeto por meio do método operator new, que irá atribuir 8 bytes de memória. Se compararmos isso com o nosso código-fonte, esta é a interpretação feita pelo IDA Free da criação do nosso objeto "vaca" quando foi criado no código-fonte.
Análise do método inicializar
Agora, vamos ver o que acontece com o método inicializar da nossa classe Vaca, que é a sub-rotina sub_1400017A0, vista a partir do decompilador.

Por um lado, se observamos a sub-rotina decompilada, sub_1400017A0, vemos que ela recebe como parâmetro uma variável extra _DWORD *a1. Essa variável é o objeto da nossa classe Vaca, ao qual serão atribuídos os valores de energia e peso sobre seus respectivos atributos.
Ao prestarmos atenção no código decompilado, especificamente nas linhas 5 e 7, vemos que o IDA trata nosso objeto como um array, carregando valores em uma posição específica. Isso ocorre porque o IDA não sabe que, na realidade, trata-se de um objeto pertencente a uma classe. Como mencionamos antes, o conceito de classe se perde quando o código-fonte é compilado, e nossas variáveis ou atributos passam a ser posições na memória, das quais precisamos indicar ao decompilador que, na verdade, se tratam de objetos de uma classe.
Criação da estrutura Vaca
Para indicar ao IDA Free que trate esse array como o objeto de uma classe, precisamos criar uma estrutura que represente a classe Vaca. Para isso, no IDA Free, vamos pressionar as teclas SHIFT+F1 para habilitar a aba "Local Types", que usaremos para criar nossa estrutura.

Esclarecimento: Em versões anteriores, o IDA Free ou IDA Freeware possui uma visualização apenas de estruturas, a qual pode ser acessada pressionando as teclas SHIFT+F9.
Dentro da visualização "Local Types", vamos adicionar um novo tipo. Para isso, dentro da visualização, clique com o botão direito do mouse -> "Add type…"

A estrutura será criada com um tamanho de 8 bytes. Isso podemos inferir porque temos duas variáveis do tipo int dentro da classe Vaca, das quais 4 bytes são usados para o valor do peso e os outros 4 bytes restantes para a energia. Além disso, lembre-se de que, quando o binário cria o objeto Vaca por meio do método operator new, ele é criado com um tamanho de 8 bytes. Esses 8 bytes são utilizados para as variáveis, pois os métodos da classe acabam sendo sub-rotinas declaradas no código quando são chamados.
Uma vez criada nossa estrutura, precisamos atribuir os dados do tipo às nossas variáveis, que, neste caso, são do tipo int, representado por 4 bytes. Para isso, nos posicionamos dentro da estrutura e pressionamos a tecla "d" até que tenhamos um tipo de dado de 4 bytes, representando o peso, depois repetimos o processo até que tenhamos o outro dado que representa a energia de nossa vaca.
As seguintes capturas de tela mostram a estrutura Vaca criada sem as variáveis e, em seguida, com suas respectivas variáveis declaradas.


Esclarecimento: Quando o IDA Free cria variáveis dentro de uma estrutura, ele atribui o prefixo field_ seguido de um número. Para alterar esse nome, basta posicionar-se sobre a variável, pressionar a tecla "n" e renomeá-la para o nome desejado.
Substituição dos tipos de dados na sub-rotina
Agora que já temos nossa estrutura criada, falta substituir, na sub-rotina de inicialização da Vaca, o tipo de dado da variável a1, que, por padrão, o IDA a definiu como tipo _DWORD. Para isso, nos posicionamos sobre o tipo de dado de a1 dentro da declaração da sub-rotina e, com a tecla "y", conseguiremos editar a declaração da variável, que de _DWORD *a1 passará a ser Vaca *mi_vaca.


Atualização da visão do decompilador
Depois de aplicarmos a alteração, o IDA Free atualiza a visão do decompilador com o novo tipo de dado. Caso isso não aconteça, é necessário pressionar a tecla "F5". Como podemos ver na figura anterior, agora nosso código está mais similar ao código-fonte, além de ser muito mais legível e fácil de interpretar.
Renomeando a sub-rotina
Em seguida, precisamos renomear esta sub-rotina para o nome utilizado no código-fonte. Para fazer isso, nos posicionamos na aba do decompilador sobre o nome da sub-rotina e, pressionando a tecla "n", o IDA nos mostrará uma nova aba onde procederemos para alterar o nome da sub-rotina para "Vaca::Inicializar".


Inferência de tipos de variáveis
A partir da figura anterior, podemos inferir agora que as variáveis v4 e v6 são do tipo Vaca, pois são usadas para criar objetos dessa classe. Por outro lado, as variáveis v5 e v7 serão utilizadas para um objeto da classe Pato.
Criação da estrutura Pato
Agora que já temos criada a estrutura Vaca, precisamos criar a estrutura do Pato. Para isso, vamos repetir o procedimento anterior, mas neste caso, ao definir o tamanho da estrutura, devemos considerar que ela terá 3 variáveis de 4 bytes cada uma. Portanto, teremos uma estrutura de 12 bytes, que em hexadecimal é 0xC.

Substituição de tipo de dado
Com essa nova estrutura, procedemos a substituir o tipo de dado utilizado pela variável a1 na sub-rotina responsável por inicializar o pato, sub_140001760. Indicamos que agora ela é um ponteiro para o tipo de dado Pato. Além disso, renomeamos a sub-rotina para o nome original "Pato::inicializar".

Renomeando métodos e atributos
Agora que já temos nossas estruturas criadas, o que falta é substituir os tipos de dados dentro dos métodos e renomear os métodos para nomes mais descritivos, como mostramos anteriormente.
Neste caso, dado que temos o código-fonte, utilizaremos os mesmos nomes para os atributos e métodos.
A seguinte captura de tela mostra o pseudocódigo decompilado do método main após renomearmos seus métodos e tipos de dados.

Correção de renomeações automáticas
Por último, se observarmos a figura anterior, vemos que na linha 34 há uma sub-rotina renomeada automaticamente pelo IDA Free como "Anonymous namespace ::_Transcode_result:: Error". Nesse caso, esse renomeamento está incorreto, pois se revisarmos nosso código-fonte, vemos que, na verdade, essa sub-rotina corresponde ao método "obter_peso" da classe Vaca. Isso nos mostra que, às vezes, é necessário inspecionar o código de cada sub-rotina para determinar se é um código de biblioteca ou código de nossa aplicação.
Segundo exemplo: malware
Agora, vamos ver como detectar objetos e criar estruturas sobre uma amostra de malware, onde temos apenas o binário e não temos acesso ao código-fonte para nos ajudar e guiar em nossa análise.
Utilizaremos a amostra com o hash DD2F8BF8D9F0B787267ECCAAD64D30D101E6B838, detectada pelas nossas soluções de segurança como Win64/Agent.ACC. A amostra foi compilada para x64 e desenvolvida com C++.
Análise de rotina pontual
Como realizar uma análise completa de uma amostra de malware é uma tarefa que leva tempo e foge do objetivo deste post, vamos analisar uma rotina pontual deste código malicioso e tentar reconstruir uma classe por meio de estruturas.

Criação da estrutura mi_estructura
Na figura anterior, podemos ver que é criada uma variável v7, que será um objeto com tamanho de 24 bytes, ou seja, 0x18 em hexadecimal. Em seguida, vemos que a este objeto serão atribuídos valores em seus atributos, mas acontece algo semelhante ao que vimos anteriormente: a variável é tratada como um array em vez de um objeto, e o decompilador do IDA Free, em vez de gerar algo do tipo "v7->atributo", nos mostra "v7[1]".
Então, nosso próximo passo é criar uma estrutura que chamaremos de mi_estructura, que deve ter um tamanho de 0x18 bytes e, por sua vez, vemos que o tipo de dado de cada atributo é do tipo QWORD, ou seja, 8 bytes.
Isso, por um lado, pode ser observado porque os valores a3 e a4 têm esse tipo de dado e também porque, em determinado momento, é atribuído a um de seus atributos um ponteiro para uma função. Como o malware foi compilado em x64, os ponteiros para funções são de 8 bytes (QWORD).

Modificação do tipo de dado
Uma vez que temos a estrutura declarada, precisamos voltar à nossa sub-rotina, posicionar o cursor sobre a variável v7 e, pressionando a tecla "y", podemos modificar seu tipo de dado para o da estrutura que criamos.


Renomear atributos
Agora que temos nossa nova estrutura aplicada à variável v7, por um lado, podemos renomear o atributo field_10 por algo mais expressivo, como ptr_a_funcion. Além disso, ainda precisamos determinar para que servem os dois atributos restantes para renomeá-los por um nome mais descritivo.
Análise da sub-rotina
Após finalizar a atribuição de valores aos atributos da variável v7, vemos que a sub-rotina chama a função beginthreadex, passando a sub-rotina a ser executada sub_14001DDC0 e passando como argumento nossa variável v7. Vamos analisar o conteúdo dessa sub-rotina.

Análise de atributos
Como vemos na figura anterior, a sub-rotina termina executando a função armazenada em um atributo de nossa estrutura e, ao mesmo tempo, passa como argumento os dois atributos que ainda precisamos renomear. Vamos então verificar o conteúdo da função ou sub-rotina para a qual esse atributo aponta, sub_140007570.

Documentação da API WSAConnectByNameW
Como vemos na figura anterior, nossos atributos são usados para chamar a API do Windows WSAConnectByNameW. Agora, precisamos consultar a documentação da Microsoft para entender o que essa API faz e quais atributos ela recebe.

Renomear atributos
Com base na documentação da Microsoft, vemos que esta API do Windows é responsável por estabelecer uma conexão com um host e uma porta específicos. Além disso, podemos identificar os nomes dos atributos que ainda faltavam.
Com base nesta e em outras APIs do Windows, como WSASocketW e recv, que são chamadas dentro da sub-rotina sub_140007570, podemos concluir que essa sub-rotina provavelmente trata de estabelecer um tipo de comunicação com um servidor, receber informações dele e manipulá-las.
Para finalizar, basta renomearmos os atributos field_8 para nodename e field_0 para servicename, completando assim nossa estrutura.

Conclusão
A análise de malware desenvolvido em C++ pode ser um processo complexo e demorado, especialmente quando o código malicioso faz uso intensivo de classes e técnicas como a ofuscação. A reconstrução de estruturas permite a identificação e a compreensão de classes utilizadas no malware, resultando em um código decompilado mais legível e facilitando a investigação das atividades maliciosas.
Entretanto, essa tarefa é desafiadora, pois, sem o código-fonte original, muitos detalhes, como os nomes de classes e atributos, são perdidos durante o processo de compilação para um arquivo executável. Isso exige que o analista utilize métodos e ferramentas especializadas para reconstruir a lógica e as intenções do malware de forma eficaz.