Desmantelando o KillDisk: reverso do componente destrutivo BlackEnergy
Vamos pular a longa introdução sobre a ameaça BlackEnergy e ir direto ao estudo do componente de malware chamado “ololo.exe”, também conhecido pelo público como KillDisk. KillDisk é um módulo da estrutura BlackEnergy destinado à destruição de dados e à criação de caos/distração durante as operações APT.
https://www.virustotal.com/en/file/11b7b8a7965b52ebb213b023b6772dd2c76c66893fc96a18a9a33c8cf125af80/analysis/As principais ferramentas usadas em nossa análise de hoje são Process Monitor que é parte das utilidades SysInternals por Mark Russinovich e IDA Pro Disassembler. Todas as manipulações serão realizadas em ambiente virtual baseado no sistema operacional Windows XP. Começamos fazendo uma rápida configuração inicial da máquina virtual de teste, ligamos a máquina e criamos um snapshot chamado “Antes da infecção”. Vamos começar!
Para acompanhar todos os eventos relacionados ao nosso objeto de estudo, lançamos Process Monitor e garantimos que ele permaneça visível:E que ele rastreie o processo necessário:
Em seguida, carregamos o vírus em IDA Pro Disassembler e vemos a seguinte imagem.Essa é a função WinMain, ou seja, a principal, então começamos sua análise e vemos que a primeira (e única) coisa que ela faz é uma chamada de procedimento chamada
sub_40E070
:
Então vamos direto para esse procedimento. Aqui podemos encontrar procedimentos responsáveis por extrair extensões de arquivos para a memória e o primeiro procedimento logo após eles que chama algo interessante chamado sub_40E080
:
Vamos dar uma olhada mais de perto no seu conteúdo:
Seus interiores contêm uma chamada de sistema interessante
CreateFile
e alguns procedimentos sub_40C390
and sub_40C400
. Então já é hora de tirar um snapshot do nosso laboratório virtual e continuar nossa jornada!
Vamos examinar a chamada da função CreateFile, configurar o BreakPoint nele e executar nossa amostra de estudo:
Uma pequena dica sobre esta função:
Como sabemos a maioria dos compiladores passa argumentos para uma função através de uma pilha e como estamos lidando com uma amostra inicialmente escrita em C, e de acordo com as diretrizes C, os argumentos são empilhados na pilha da direita para a esquerda de modo que o primeiro argumento da função é empilhado por último na pilha e acaba sendo o primeiro elemento no topo dela. [1].
There is a special register for working with the stack – ESP (Extended Stack Pointer). The ESP, by definition, always points to the top of the stack:
Um olhar sobre a pilha:
O último argumento que foi empilhado na pilha (aquele no seu topo) é o primeiro argumento aceito pela função (de acordo com a sintaxe da
CreateFile
função) e contém o nome do arquivo em nosso caso \.PHYSICALDRIVE0
, portanto, o alvo é o primeiro drive físico.Agora analisamos outros argumentos enviados para a função pegando seus valores da pilha:
Algumas conclusões:
- lpFileName – ponteiro para a string ASCIIZ contendo o nome (caminho) do arquivo sendo aberto ou criado (como já descobrimos);
- dwDesiredAccess – tipo de acesso ao arquivo: em nosso caso, o valor é
0C0000000h
que significaGENERIC_READ+GENERIC_WRITE
oracesso de leitura-escrita
; - DwShareMode – um modo que possibilita o compartilhamento dos arquivos com diferentes processos e este parâmetro pode ter diferentes valores, em nosso caso o valor de 00000003b se traduz em
FILE_SHARE_READ+FILE_SHARE_WRITE
ououtros processos podem abrir arquivo para leitura/escrita
; - IpSecurityAttributes – ponteiro para a estrutura SecurityAttributes (arquivo winbase.h) que define configurações de segurança relacionadas ao objeto de arquivo de kernel, se a segurança não for definida, um valor de
NULL
é usado; - dwCreationDistribution – é responsável por decidir ações quando o arquivo existe ou não, em nosso caso um valor de
3
significaOPEN_EXISTING
orabrir arquivo se ele existir e retornar um erro se ele não existir
; - DwFlagsAndAttributes – sinalizadores e atributos; este parâmetro é usado para definir características do arquivo criado e é ignorado no modo de leitura 0.
- hTemplateFile – o parâmetro é usado apenas quando um novo arquivo é criado
0
. - Se for bem-sucedida, a função retorna um novo manipulador de arquivo para
ЕАХ
E se a função falhar, umNULL
é escrito emЕАХ
registro.
Muito bem, então, após chamar CreateFile
obtemos um valor não-zero para EAX, o que significa que contém o manipulador do arquivo solicitado, por exemplo, \.PHYSICALDRIVE0
:
Conseguimos nosso manipulador, então vamos prosseguir para a próxima chamada,
sub_40C390
:
Assim como antes, olhamos dentro do procedimento e vemos outra chamada interessante:
Dica da função:
Um olhar na pilha logo antes de chamar esta função:Conclusões:
- hDevice – descritor do dispositivo (reconhecemos um cara familiar aqui
\.PHYSICALDRIVE0
); - dwIoControlCode – código de operação. Em nosso caso, o valor é
0x70000h
que significaIOCTL_DISK_GET_DRIVE_GEOMETRY
ou a obtenção da informação referente à geometria dos discos (quantidade de cilindros, tipo de mídia, trilhas por cilindro, setores por trilha, bytes por setor); - lpInBuffer – buffer com dados de entrada. Não precisamos de nenhum dado de entrada, então
NULL
é; - nInBufferSize – tamanho do buffer de entrada em bytes igual a
0
no nosso caso; - lpOutBuffer – ponteiro para o buffer de saída. Seu tipo é definido pelo parâmetro dwIoControlCode
0x0011FCA8h
; - nOutBufferSize – tamanho do buffer de saída em bytes
0x18h
(24); - lpBytesReturned – ponteiro para uma variável que contém a quantidade de bytes que são escritos no output
0x0011FCA4h
; - lpOverlapped – ponteiro para uma estrutura OVERLAPPED;
- À medida que a chamada é processada, olhamos o buffer (no endereço que foi enviado como argumento para lpOutBuffer precisamente
0x0011FCA8h
):
- No endereço de
0x0011FCA4h
obtemos um retorno da quantidade de bytes escritos no buffer de saída conforme pretendido pela função (marcado em verde), obtivemos tanto quanto pedimos, 24 símbolos. - No endereço de
0x0011FCA8h
obtivemos informações sobre a geometria do primeiro disco físico (\.PHYSICALDRIVE0
):- quantidade de cilindros –
0x519h
(1305) - tipo de mídia –
0x0Ch
(12) significando disco rígido fixo. - trilhas por cilindro –
0x0FFh
(255) - setores por trilha –
0x3Fh
(63) - bytes por setor –
0x200
(512)
- quantidade de cilindros –
Agora nos referimos ao procedimento três, sub_40C400
:
Dentro do procedimento duas chamadas imediatamente atraíram nossa atenção, especificamente
SetFilePointerEx
e WriteFile
:
Como de costume, olhamos dentro da pilha de chamadas da função:
- hfile – nosso familiar descritor de dispositivo
\.PHYSICALDRIVE0
; - liDustanceToMove – posição inicial para o início de uma gravação
0
; - lpNewFilePointer – ponteiro para uma variável que recupera um novo ponteiro de posição do arquivo
0x0011FCA8h
. Em um caso quando o parâmetro está VAZIO (NULL), um novo ponteiro não é retornado no arquivo; - dwMoveMethod – tamanho do buffer de entrada em bytes. No nosso caso
0x200h
(512) – bytes no setor;
Agora nossa “besta” usa a WriteFile
função para preencher o arquivo com zeros ao qual já estabeleceu acesso direto e confiável.
Uma dica sobre a função:
A WriteFile função escreve dados no arquivo ou dispositivo de entrada/saída (I/O) especificado começando pela posição especificada no arquivo. Esta função é projetada para operação síncrona e assíncrona.
Lista de argumentos da função:Tudo bem, para garantir que não perdemos nada interessante, damos uma olhada no painel e entre todas as coisas prestamos atenção na pilha:
Tudo pronto para a ação:
- 1 – aguardando na chamada da função
WriteFile
- 2 – Process Monitor está ansiosamente esperando qualquer pequeno movimento dos músculos da nossa “besta”
- 3 – argumentos empurrados dos registradores para a pilha
- 4 – e bem, a própria pilha
- Um espaço dedicado na janela Hex View-EBP marcamos uma área que está sendo endereçada pelo segundo argumento da função (buffer de dados) e você pode confiar em mim nesta – contém exatamente
0x200h
(512
) de zeros.
Executamos o comando pressionando a tecla F8 e observamos a mudança:Como esperávamos, um olho atento de Process Monitor está ligado e fixa a gravação de 512 bytes a partir da posição 0.
Em seguida, o procedimento sai e é chamado novamente, mas agora com valores diferentes:
Agora pegamos os próximos 512 bytes que é particularmente visível em 0x200 (ao contrário dos argumentos anteriores da pilha que marcamos com um quadro azul)E eles têm o mesmo destino que os anteriores:
Esse carrossel continua até a verificação do endereço
0x40C865h
retornar o valor do EBX
registrador igual a 0x100h
(255 vezes
):
Para garantir que isso aconteceu, colocamos um ponto de parada na instrução que segue nosso ciclo vicioso de “destruição”:
Como um todo recebemos um
256
de operações de regravação:Basicamente é isso, nossa “besta” fez seu trabalho sujo e fecha o manipulador de arquivos chamando a
CloseHandle
função.
Dica da função:
Argumentos da função:
Nossa visão tradicional da preparação da pilha:
E o que temos no registrador ESI?
Como esperado, nosso descritor familiar
0x44h
é passado para a pilha como argumento da função.
Vemos os valores retornados pela função no registrador EAX: Se a função terminar com sucesso o valor retornado
não é nulo
.
Se a função terminar com um erro o valor retornado é nulo.
Esta ação não passou despercebida pelo olhar atento do nosso Process Monitor:
Tudo funcionou como o normal (se se pode chamar de normal a destruição da parte mais importante do drive do sistema) e o arquivo está fechado.
Vamos prosseguir:Para uma percepção mais confortável eu renomeei a função para
Eraser
. Depois de destruir com sucesso os dados em PhysicalDrive0
, um contador localizado no ESI
registrador é incrementado por 1
, verificado com valor de 0x0Ah
(10
) e lança a operação de regravação com um novo valor:e assim por diante até chegar a 10.
Desta forma a besta continua andando por todos os drives físicos e apagando os primeiros 512 * 255 = 128kb
(isto depende da quantidade de bytes em um setor que a “besta” aprendeu ao conhecer a geometria do disco).
Desta vez, no entanto, o resultado da função CreateFileW
será o seguinte valor: O que significa que em nosso “laboratório improvisado” não existe um segundo disco físico (ou bem, nenhum tipo de disco com um número sequencial maior que 0) e o ciclo não tem nada para regravar.
Agora que estamos armados com o conhecimento recém-adquirido, vamos tentar desabilitar a função destrutiva que acabamos de aprender modificando o código de uma forma que nossa modificação possa permitir ignorar a regravação dos dados:
Vamos lembrar o procedimento
sub_40E080
que contém a chamada da função CreateFileW
. Como sabemos que o valor de 0xFFFFFFFFh
apareceria após a conclusão da função CreateFileW
apenas no caso de um drive físico não estar presente e em tal cenário a função “destruição de dados” realiza uma saída, vamos mudar uma instrução de ramo condicional localizada no endereço 0x0040C7E4h
para uma incondicional:
IDA Pro é um instrumento legal, então só precisamos escrever um comando.
A única coisa que precisamos considerar é o tamanho do comando antes e depois, então vamos verificar quantos bytes são usados pela sequência condicional:
Como podemos ver, uma instrução de ramificação condicional é igual a
6
bytes enquanto a sequência incondicional levará apenas 5
e, portanto, devemos adicionar outro comando – NOP, para manter as coisas iguais:NOP nos próximos comandos, independentemente do que contivessem antes (mesmo que vejamos um 0 ali):
Fazemos uma verificação dupla para garantir que os endereços dos comandos estejam agora igualados com base no endereço do comando seguinte:
e pressionamos cancelar. Feito.
Após essas manipulações, o procedimento parecerá simples: entrar e sair, formando um loop vazio (já que não sabemos o que outras partes do programa podem interferir, deixamos assim nesse estado).A prova final identificada pelo Process Monitor indica que nenhum disco físico foi danificado em resultado da execução do KillDisk dissecado:
Malware leu as informações sobre o disco, estudou e continuou em seus negócios que analisaremos minuciosamente em nossos próximos artigos.
Fontes:
[1] – Hacker Disassembling Uncovered. Kris Kaspersky, 2009.
Traduzido do original por Andrii Bezverkhyi | CEO SOC Prime