Blaze Blog /

Análise do Ragnar Locker: O Caso da EDP

Security
Jul 30, 2020
20min read
Ilustração de um ciborgue a analisar dados num equipamento complexo

Introdução

A 13 de abril de 2020, a comunicação social portuguesa [1] noticiou que a Energias de Portugal (EDP), a gigante multinacional portuguesa do setor energético e uma das maiores operadoras europeias nos setores da energia e eólico, tinha sido alvo de um ataque de ransomware altamente direcionado (mais tarde identificado como Ragnar Locker [2]), em plena pandemia de COVID-19, enquanto o país se encontrava em estado de emergência. Os atacantes responsáveis pelo ransomware estariam, supostamente (embora não confirmado), a exigir 1580 BTC (9,9 milhões de euros), ameaçando divulgar todos os dados roubados (10 TB, segundo os próprios autores). Desde então, tem sido considerado um dos seus piores ataques informáticos.

Como tal, e enquanto empresa de consultoria em segurança da informação sediada no Porto, Portugal, decidimos tomar a iniciativa de investigar nós próprios a amostra de ransomware, pondo mãos à obra e indo diretamente à sua essência. Estávamos especificamente interessados em compreender como este ransomware foi construído, isto é, os seus detalhes técnicos, as suas capacidades e a sua sofisticação. A análise e os seus resultados finais são, por isso, apresentados neste artigo de blog, de forma detalhada, para todos os leitores curiosos que desejam saber mais sobre a parte final (destrutiva) do ataque.

Análise

Um dos primeiros passos que um analista deve dar ao interagir pela primeira vez com um executável potencialmente malicioso é realizar uma análise estática básica ao PE, por exemplo, observar os seus cabeçalhos PE, secções, importações, strings ou qualquer outra informação que possa ajudar a obter uma ideia geral do que o binário pode fazer ou conter. Neste caso específico, ao analisar as suas importações, podemos ver várias APIs do Windows que são frequentemente (ab)usadas por malware para ocultar as suas ações. Isto inclui (mas não se limita a) VirtualAlloc*(), LoadLibrary*() e GetProcAddress(). Algo estranho que se destacou ao observar as importações é a existência de várias APIs que só são necessárias para a sincronização de threads (InitializeCriticalSectionAndSpinCount(), EnterCriticalSection(), LeaveCriticalSection() e DeleteCriticalSection()), no entanto, não existem APIs importadas responsáveis pela criação de threads, como, por exemplo, CreateThread().

1

O dump hexadecimal do nosso alvo mostra várias strings, algumas das quais, após uma pesquisa no Google, revelam páginas relacionadas com malware e serviços de análise de malware.

Uma vez realizada uma análise mais aprofundada, torna-se bastante claro que o ransomware está, de alguma forma, ofuscado. Por exemplo, e para fins de demonstração, a imagem seguinte apresenta uma função que é chamada com a string "EV_MMAC_OID_TERMINATE_CONNECTION" como argumento, onde a string nunca é realmente utilizada para nada e um ciclo existente nunca é iniciado devido ao resultado da comparação deixar sempre o EFLAGS.ZF desativado (predicado opaco).

2

As APIs de sincronização de threads mencionadas anteriormente também participam na ofuscação (essencialmente código lixo), onde um ciclo é concluído após ser executado 2 000 000 de vezes, realizando operações aritméticas inúteis e chamando uma função que devolve sempre 0 ao longo do processo.

3

As partes interessantes, do ponto de vista da análise de malware, só ocorrem quando o ransomware chama a função VirtualAllocEx() com permissões de memória PAGE_EXECUTE_READWRITE (flProtect). A alocação de páginas com tais permissões de memória é um forte indicador de que algo interessante será escrito nelas, o qual será posteriormente tratado como código a ser executado, possivelmente participando no processo de descompactação.

4

A imagem seguinte demonstra o algoritmo utilizado pelo ransomware, onde este começa a descomprimir/desencriptar e a escrever shellcode na nova área de memória.

5

Quando termina de escrever o shellcode, chama a função GetModuleHandleW(L"kernel32″) para obter o endereço base da kernel32.dll que está mapeado no espaço de endereçamento do processo atual. Em seguida, transfere o fluxo de controlo para a nova área de memória RWX que contém o shellcode recém-desencriptado, passando o ponteiro para o endereço base da kernel32.dll obtido como argumento.

6

Neste ponto, ocorre a execução do shellcode. A imagem seguinte demonstra as suas instruções iniciais.

7

Como se pode ver na imagem acima, o shellcode começa por realizar uma série de instruções MOV r/m8, imm8 que são utilizadas para construir na stack, um byte de cada vez, strings que representam nomes de APIs do Windows. Depois de terminar de as colocar na stack, chama uma sub-rotina passando, novamente, o endereço base da kernel32.dll e a string "GetProcAddress" como argumento.

8

A imagem seguinte demonstra as instruções iniciais desta sub-rotina.

9

Para qualquer pessoa que tenha feito engenharia reversa de malware suficiente, ou simplesmente para qualquer pessoa familiarizada com o formato de ficheiro PE, torna-se claro que esta função está a ser utilizada para iterar manualmente através das exportações da kernel32.dll, a fim de resolver dinamicamente, em tempo de execução, o endereço da GetProcAddress() para que possa ser utilizada posteriormente. A pista é o facto de, primeiro, obter o endereço do cabeçalho PE da kernel32.dll somando o endereço base da kernel32.dll (Cabeçalho DOS) com o valor armazenado no campo e_lfanew (no offset 0x3C). Depois, obtém o endereço da Tabela de Exportação somando o Endereço Virtual Relativo (RVA) localizado em pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress (no offset 0x78). Após a resolução da GetProcAddress(), esta será então utilizada para resolver dinamicamente o resto das APIs cujos nomes foram construídos anteriormente, um byte de cada vez, na stack.

10

Em seguida, a função VirtualAlloc() é chamada, mas desta vez as permissões de acesso à memória não incluem a execução, permitindo apenas leituras e escritas (PAGE_READWRITE).

11

Eventualmente, é realizada uma série relativamente longa de operações, resultando em escritas na região de memória recém-alocada, onde um PE completo é desencriptado em tempo de execução. É facilmente reconhecível através do cabeçalho MS-DOS MZ. Este ponto específico é o melhor momento para despejar (dump) a região de memória para o disco, uma vez que o PE está no seu formato não mapeado (raw), ou seja, como é armazenado no disco, em oposição ao seu formato mapeado (virtual), ou seja, como precisa de ser carregado na memória pelo carregador para a execução real do programa.

12

Ao despejar o PE para o disco, este pode ser analisado mais a fundo através dos mesmos passos de análise estática básica para obter uma ideia geral do que este novo binário pode ter (ou fazer). Como podemos ver, contém uma secção .keys interessante e o seu Time Date Stamp (data de compilação) está definido para segunda-feira, 06.04.2020 19:57:20 UTC. Esta data é particularmente interessante, uma vez que é apenas alguns dias antes dos primeiros relatórios reais do ataque. Tenha em atenção, no entanto, que essa data pode ser facilmente modificada.

unpacked

Dando continuidade à execução, o SizeOfImage (o tamanho da imagem, em bytes, incluindo todos os cabeçalhos) do PE recém-descriptografado é obtido via pNtHdr->OptionalHeader.SizeOfImage, conforme observado pelo uso do deslocamento 0x3C para obter o endereço do cabeçalho PE e, em seguida, adicionando 0x50 a esse resultado. O SizeOfImage será usado como o argumento dwSize da chamada VirtualAlloc() que se segue, com lpAddress sendo o endereço base da imagem binária do processo em execução e as permissões de acesso à memória definidas como PAGE_EXECUTE_READWRITE.

13

Após a chamada VirtualAlloc(), a imagem do binário principal (do processo em execução) é sobrescrita com zeros, em um loop que termina após a execução SizeOfImage (do novo PE descriptografado) vezes.

14

Em seguida, os cabeçalhos do novo PE são copiados para o seu lugar, assim como ele carrega suas seções nos locais corretos, sem se preocupar com suas permissões de memória. Neste ponto, já podemos afirmar que o ransomware realiza injeção no próprio processo.

15

Ao comparar o endereço base onde o novo PE foi colocado com seu ImageBase (endereço base preferencial), via pNtHdr->OptionalHeader.ImageBase (conforme observado pelo deslocamento 0x34), ele pode decidir se as realocações de base precisam ocorrer ou não. Neste caso, as realocações de base não precisam ser realizadas, mas há código dentro do shellcode que poderia fazê-lo caso fosse necessário.

16

A Tabela de Endereços de Importação (IAT) é então corrigida, primeiro carregando as DLLs necessárias e, em seguida, resolvendo as importações necessárias pelo PE. A imagem a seguir demonstra esse processo inicial, conforme observado acessando a Tabela de Importação, via pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress (deslocamento 0x80).

17

Ao acessar AddressOfEntryPoint, via pNtHdr->OptionalHeader.AddressOfEntryPoint (deslocamento 0x28), o Ponto de Entrada Original (OEP) é obtido e subsequentemente chamado, transferindo assim a execução para o executável recém-descompactado, uma vez que ele está pronto para ser executado.

18

Uma das primeiras coisas feitas após o início da execução no ponto de entrada do novo PE é chamar uma sub-rotina que eventualmente chama GetLocaleInfoW() com LCID LOCALE_SYSTEM_DEFAULT (localidade padrão para o sistema operacional), a fim de compará-la com um possível conjunto de strings unicode construídas anteriormente na pilha por instruções mov. As strings unicode construídas são:

  • Bielorrusso
  • Azerbaijano
  • Ucraniano
  • Moldavo
  • Georgiano
  • Armênio
  • Turcomeno
  • Russo
  • Quirguiz
  • Cazaque
  • Uzbeque
  • Tadjique

Se as informações de localidade solicitadas corresponderem a qualquer uma dessas strings, conforme observado pelo uso de lstrcmpiW(), o processo atual é encerrado via TerminateProcess() com código de saída 666.

19

Em seguida, chama GetComputerNameW(), GetUserNameW() e outra função duas vezes com argumentos diferentes; a primeira com "SOFTWARE\Microsoft\Cryptography" e "MachineGuid", e a segunda com "SOFTWARE\Microsoft\Windows NT\CurrentVersion" e "ProductName".

20

A função simplesmente aloca uma página via VirtualAlloc(), abre a subchave fornecida via RegOpenKeyExW() a partir da hive do registro HKEY_LOCAL_MACHINE (HKLM) com direitos de acesso KEY_READ e, em seguida, recupera os dados do nome de valor fornecido associado à chave de registro aberta via RegQueryValueExW(). O ponteiro para os dados recuperados (a página retornada pela chamada VirtualAlloc()) é então o valor de retorno desta função.

21

Para cada um dos dados obtidos através das chamadas às APIs (GetComputerNameW() e GetUserNameW()) e da função responsável por recuperar os dados associados às chaves de registro abertas, ela executa uma série de operações. Especificamente, para cada caractere, ela aplica um XOR com o valor 0xAB01FF3C, soma o valor anterior ao próximo, rotaciona 13 bits para a esquerda e subtrai o resultado da operação de rotação do valor anterior à rotação. Isso é feito para que IDs únicos resultem das operações, sendo posteriormente concatenados.

22

O resultado das operações anteriores (IDs únicos) e sua concatenação é usado como lpName (o nome do objeto de evento) passado para CreateEventW(). Mas, primeiro, verifica se argc (contagem de argumentos) é 1; se não for, CreateEventW() é totalmente ignorado. No entanto, se for 1, entra-se em um loop onde CreateEventW() é chamado repetidamente, saindo apenas se o valor de retorno da chamada da API CreateEventW() for diferente de 183 (ERROR_ALREADY_EXISTS). Caso contrário, o loop é repetido 32.768 vezes, momento em que o processo atual é encerrado via TerminateProcess() com código de saída 666.

23

Em seguida, entra em outro loop, executado 17 vezes, onde tenta abrir \\.\PHYSICALDRIVE%d (um disco rígido físico) via CreateFileW(), onde %d é incrementado a cada iteração do loop, começando em 0. Se o valor de retorno de CreateFileW() for diferente de 0xFFFFFFFF, chama DeviceIoControl() no handle com o código de controle IOCTL_DISK_SET_DISK_ATTRIBUTES, tentando colocar o disco online e permitir operações de escrita (o campo Attributes na struct SET_DISK_ATTRIBUTES é definido como 0, e o campo AttributesMask como 0x3). Também chama DeviceIoControl() novamente, desta vez com o código de controle IOCTL_DISK_UPDATE_PROPERTIES, invalidando a tabela de partição em cache e sincronizando a visão do sistema do dispositivo de disco especificado, já que, neste ponto, ele teria sido modificado.

24

Para cada volume existente sem uma letra de unidade associada, tenta associar uma letra de unidade não utilizada. Isso é feito escaneando e iterando através dos volumes existentes no sistema usando a combinação FindFirstVolumeA()/FindNextVolumeA() e determinando se uma letra de unidade já está associada ao volume via GetVolumePathNamesForVolumeNameA(). Se nenhuma letra de unidade estiver associada ao volume, obtém as letras de unidade disponíveis via uma chamada para GetLogicalDrives(), onde o primeiro bit não definido da máscara de bits retornada (começando do 4º) pode ser usado.

25

Neste ponto, a mesma rotina será chamada duas vezes, uma após a outra, com argumentos diferentes.

26

Esta rotina é responsável por gerar, via CryptGenRandom(), bytes aleatórios criptograficamente com o comprimento especificado como segundo argumento da rotina, armazenando-os no endereço especificado no primeiro argumento. Esses bytes aleatórios são subsequentemente modificados por uma série relativamente longa de operações.

27

Outra rotina será chamada, desta vez três vezes. Um dos argumentos da função que é sempre passado é um ponteiro para uma string de aparência suspeita.

28

Verifica-se que esta função é responsável por descriptografar vários dados armazenados na seção .keys do binário. Na primeira vez que a rotina é chamada, ela descriptografa o ID de chat do cliente Tor usado para se comunicar com os perpetradores.

29

Na segunda vez que é chamada, é usada para descriptografar uma série de strings, que serão posteriormente usadas como referência para substrings a serem buscadas, a fim de determinar quais serviços interromper, como veremos. As strings são:

  • vss
  • sql
  • memtas
  • mepocs
  • sophos
  • veeam
  • backup
  • pulseway
  • logme
  • logmein
  • connectwise
  • splashtop
  • mysql
  • Dfs
30

Na terceira vez que é chamada, ela é usada para descriptografar outra série de strings, que posteriormente servirão como referência para substrings a serem buscadas, a fim de determinar quais processos encerrar, como veremos a seguir. As strings são:

  • sql
  • mysql
  • veeam
  • oracle
  • ocssd
  • dbsnmp
  • synctime
  • agntsvc
  • isqlplussvc
  • xfssvccon
  • mydesktopservice
  • ocautoupds
  • encsvc
  • firefox
  • tbirdconfig
  • mydesktopqos
  • ocomm
  • dbeng50
  • sqbcoreservice
  • excel
  • infopath
  • msaccess
  • mspub
  • onenote
  • outlook
  • powerpnt
  • steam
  • thebat
  • thunderbird
  • visio
  • winword
  • wordpad
  • EduLink2SIMS
  • bengine
  • benetns
  • beserver
  • pvlsvr
  • beremote
  • VxLockdownServer
  • postgres
  • fdhost
  • WSSADMIN
  • wsstracing
  • OWSTIMER
  • dfssvc.exe
  • dfsrs.exe
  • swc_service.exe
  • sophos
  • SAVAdminService
  • SavService.exe
31

Após a descriptografia de alguns dados, conforme visto nas etapas anteriores, ele tentará estabelecer uma conexão com o gerenciador de controle de serviços (via OpenSCManagerA()) do computador local (lpMachineName definido como NULL) e abrir o banco de dados SERVICES_ACTIVE_DATABASE (lpDatabaseName definido como NULL) com o argumento dwDesiredAccess definido como SC_MANAGER_ALL_ACCESS. A primeira chamada para EnumServicesStatusA() deve falhar, com o código de erro definido como ERROR_MORE_DATA, já que cbBufSize (o tamanho do buffer apontado pelo parâmetro lpServices, em bytes) está definido com um valor pequeno (36 bytes). Se falhar, pcbBytesNeeded receberá o número de bytes necessários para retornar as entradas de serviço restantes (via segunda chamada para EnumServicesStatusA()), momento em que a execução pode prosseguir para o caminho de código que tentará interromper alguns serviços.

32

Para cada serviço enumerado que contenha uma subcadeia (StrStrIA()) do conjunto possível de strings existentes nos dados descriptografados anteriormente, ele chamará uma sub-rotina.

33

Esta sub-rotina é responsável por chamar GetTickCount(), abrir o serviço existente via OpenServiceA() com dwDesiredAccess definido como SERVICE_STOP | SERVICE_QUERY_STATUS | SERVICE_ENUMERATE_DEPENDENTS, verificar o status do serviço via QueryServiceStatusEx() e:

  • Se o seu dwCurrentState for SERVICE_STOPPED, ele fecha o identificador do serviço via CloseServiceHandle() e retorna.
  • Se o seu dwCurrentState for SERVICE_STOP_PENDING, ele entrará em um loop onde aguarda via Sleep() por 10000 ou 1000 milissegundos, chama QueryServiceStatusEx() novamente e sai do loop se dwCurrentState for SERVICE_STOPPED ou se mais de 30000 milissegundos tiverem passado (através de outra chamada para GetTickCount() e subtraindo seu valor anterior).
  • Se o seu dwCurrentState for SERVICE_RUNNING, ele tentará enumerar seus serviços dependentes via EnumDependentServicesA(), abrir os serviços dependentes enumerados via OpenServiceA() com dwDesiredAccess de SERVICE_STOP | SERVICE_QUERY_STATUS e chamar ControlService() neles com dwControl de SERVICE_CONTROL_STOP. Após os serviços dependentes enumerados serem interrompidos, ele finalmente chamará ControlService() no serviço aberto inicialmente e o interromperá usando, novamente, o dwControl de SERVICE_CONTROL_STOP.
34

Após os serviços serem tratados (interrompidos), chega o momento da finalização de processos. Assim como no caso dos serviços, ele percorre todos os processos no sistema e verifica via StrStrIA() se uma subcadeia referida nos dados descriptografados anteriormente está presente em seu nome. Ele faz isso chamando primeiro CreateToolHelp32Snapshot() com dwFlags TH32CS_SNAPALL (e th32ProcessID 0), então os processos são iterados via combinação Process32FirstW()/Process32NextW().

35

Se o nome do processo contiver qualquer subcadeia conforme indicado pelos dados descriptografados, o processo é aberto via OpenProcess() com dwDesiredAccess de PROCESS_TERMINATE e encerrado via TerminateProcess() com código de saída 666.

36

Após a finalização do processo, outra rotina é chamada. Esta rotina chama primeiro GetNativeSystemInfo() para verificar o valor de DUMMYUNIONNAME.DUMMYSTRUCTNAME.wProcessorArchitecture armazenado na estrutura SYSTEM_INFO. Se wProcessorArchitecture for PROCESSOR_ARCHITECTURE_AMD64 (0x9), então LoadLibraryW(L"kernel32.dll") é chamado e o endereço de Wow64EnableWow64FsRedirection() é obtido via uma chamada para GetProcAddress(). Esta WinAPI é então chamada com Wow64FsEnableRedirection definido como FALSE, desativando assim o redirecionamento de pasta do sistema WOW64.

37

Quando o redirecionamento é desativado, duas strings unicode são criadas na pilha por uma série de instruções mov. Essas strings unicode serão usadas como lpCommandLine para chamadas subsequentes para CreateProcessW(). As linhas de comando executadas são:

  • wmic.exe shadowcopy delete
  • vssadmin delete shadows /all /quiet
38

Logo após a exclusão da cópia de sombra, LoadLibraryW(L"kernel32.dll") é chamado mais uma vez e Wow64EnableWow64FsRedirection() é obtido via GetProcAddress(), desta vez para ser chamado com Wow64FsEnableRedirection definido como TRUE, ativando assim o redirecionamento de pasta do sistema WOW64. A rotina então retorna.

39

Chegou a hora de descriptografar mais dados da seção .keys. Desta vez, os dados descriptografados são uma chave pública RSA de 2048 bits. Veremos como ela será usada mais adiante.

40

Outros dados que são descriptografados, por outra chamada à rotina, são a nota de resgate final. Consulte a imagem a seguir.

41

A chave pública RSA de 2048 bits é então convertida e suas informações de chave pública são importadas via CryptImportPublicKeyInfo() para o provedor.

42

Ao chamar duas vezes uma sub-rotina que invoca CryptEncrypt(), os dois dados criptograficamente aleatórios anteriores, gerados por ambas as chamadas para CryptGenRandom() e posteriormente modificados por uma série de operações, serão criptografados com a chave pública de 2048 bits.

43

Por meio de uma chamada para GetComputerNameW() e da mesma série de operações usadas para gerar IDs únicos para o nome do objeto de evento CreateEventW() (lpName), um ID codificado em hexadecimal é gerado.

44

Por meio de concatenação, usando lstrcatW() e uma chamada para SHGetSpecialFolderPathW() com csidl CSIDL_COMMON_DOCUMENTS, o caminho C:\Users\Public\Documents\RGNR_E354BDB6.txt é criado.

45

Ao longo do processo, um bloco de memória heap alocado via RtlAllocateHeap() é chamado com HEAP_ZERO_MEMORY como Flags, o que o inicializa com zeros. Por algum motivo, esta área de memória será, novamente, zerada após a chamada para RtlAllocateHeap().

heapzero

O ID de chat do cliente Tor descriptografado anteriormente é então convertido para Base64, fazendo uma chamada para CryptBinaryToStringA(), como visto pelo uso de dwFlags definido como CRYPT_STRING_BASE64.

46

A nota de resgate especificamente direcionada, descriptografada anteriormente, que será deixada nos sistemas atacados, é então gravada via WriteFile() no caminho C:\Users\Public\Documents\RGNR_E354BDB6.txt que havia sido criado momentos antes, abrindo-o primeiro via CreateFileW() com dwDesiredAccess de GENERIC_READ | GENERIC_WRITE.

47

Então, por meio de concatenação, o "RAGNAR SECRET" será anexado ao arquivo, que é simplesmente a versão codificada em Base64 do ID de chat do cliente Tor.

48

Após o arquivo com a nota de resgate ter sido gravado, o ransomware verificará se argc (contagem de argumentos) é maior que 1. O ransomware pode ser executado com as opções de linha de comando "-list" ou "-force". Elas são usadas simplesmente para determinar como os caminhos que servirão de base para iniciar a criptografia de arquivos são obtidos. A opção de linha de comando "-list" obtém os caminhos de um arquivo fornecido como argumento, enquanto "-force" inicia a criptografia de arquivos a partir do caminho fornecido como argumento. Como o objetivo final é a criptografia de arquivos, e essas opções de linha de comando provavelmente foram usadas apenas durante o desenvolvimento para fins de teste pelos atacantes, continuaremos examinando como se nenhum argumento fosse fornecido, ou seja, argc == 1.

49

Por meio da chamada de API GetLogicalDrives(), obtém-se uma máscara de bits que representa as unidades de disco disponíveis no momento. Para cada unidade de disco disponível, conforme indicado pelos bits definidos, a letra da unidade correspondente é recuperada adicionando 0x41 ('A'). Se GetVolumeInformationW() retornar com sucesso (diferente de zero) no volume e seu tipo de unidade (obtido via chamada de GetDriveTypeW()) for diferente de DRIVE_CDROM, o processo pode prosseguir usando-a como base para iniciar a criptografia de arquivos. A letra da unidade obtida também é comparada com a letra da unidade usada no WindowsDirectory (por exemplo, C:\Windows), obtida pela chamada de GetWindowsDirectoryW(), e, se coincidirem, um número inteiro tratado como flag será definido como 1; caso contrário, permanecerá 0.

50

O arquivo contendo a nota de resgate é então copiado para este novo caminho obtido.

51

Após a cópia do arquivo, uma sub-rotina é chamada com este novo caminho como argumento. Um dos outros argumentos desta sub-rotina é o número inteiro tratado como flag para indicar se a letra da unidade do caminho atual coincide com a letra da unidade onde o WindowsDirectory está localizado. Esta sub-rotina começa iterando por todos os arquivos e diretórios existentes no caminho fornecido como argumento, por meio da combinação FindFirstFileW()/FindNextFileW(). Inicialmente, ela se preocupa apenas com diretórios e verifica se não são "." ou "..". Se não for nenhum desses diretórios, ela verifica se a flag inteira passada como argumento está definida ou não. Se estiver definida, ou seja, se for a letra da unidade usada pelo WindowsDirectory, verificações adicionais são realizadas.

52

As verificações realizadas quando a flag passada como argumento está definida ocorrem para que certos diretórios sejam ignorados e nada seja feito neles. Os diretórios comparados com o diretório obtido atualmente são:

  • Windows
  • Windows.old
  • Tor Browser
  • Internet Explorer
  • Google
  • Opera
  • Opera Software
  • Mozilla
  • Mozilla Firefox
  • $Recycle.bin
  • ProgramData
  • All Users
53

Se o diretório obtido não for nenhum dos mencionados acima, o arquivo da nota de resgate será copiado para este novo diretório e a sub-rotina será chamada recursivamente com este novo caminho. O número inteiro tratado como flag continua sendo passado como definido.

54

Quando todos os arquivos/diretórios tiverem sido iterados e passado pelas verificações, ou seja, quando FindNextFileW() retornar NULL, o processo começará a iterar novamente por todos os arquivos/diretórios. O objetivo, desta vez, é procurar especificamente por arquivos. Para cada arquivo encontrado, ele compara seu nome com um conjunto de nomes de arquivo possíveis. Esses nomes de arquivo são:

  • O nome do arquivo da nota de resgate
  • autorun.inf
  • boot.ini
  • bootfont.bin
  • bootsect.bak
  • bootmgr
  • bootmgr.efi
  • bootmgfw.efi
  • desktop.ini
  • iconcache.db
  • ntldr
  • ntuser.dat
  • ntuser.dat.log
  • ntuser.ini
  • thumbs.db

Se o nome do arquivo encontrado corresponder a qualquer um dos nomes de arquivo acima, nenhuma ação será realizada e ele será ignorado.

55

Se o nome do arquivo encontrado não corresponder à lista de nomes de arquivo acima, verificações de extensão também serão realizadas. Especificamente, a extensão do arquivo atual é verificada em relação a:

  • .db
  • .sys
  • .dll
  • .lnk
  • .msi
  • .drv
  • .exe

Se a extensão do nome do arquivo atual corresponder a qualquer uma da lista acima, nada será feito e ele será ignorado. Caso contrário, o ponteiro para o nome do arquivo será adicionado a uma matriz de pilha.

56

Se 64 arquivos no diretório atual sob análise tiverem sido adicionados à matriz de pilha (passando assim por todas as verificações acima), 64 threads serão criadas via CreateThread(). Cada um dos 64 ponteiros na matriz de pilha é passado como lpParameter e a rotina que lida com a criptografia de arquivos é passada como lpStartAddress.

57

Se, ao final da análise do diretório, o número de arquivos na matriz de pilha for inferior a 64, uma thread será criada via CreateThread para cada um dos arquivos. Cada um dos ponteiros na matriz de pilha é passado como lpParameter e a rotina que lida com a criptografia de arquivos também é passada como lpStartAddress.

58

Na thread que lida com a criptografia de arquivos, o sistema primeiro lê, via ReadFile(), os 9 últimos bytes do arquivo. Se a string de marcação _RAGNAR_ for encontrada, o arquivo não será criptografado, pois ele já é o resultado de uma criptografia anterior, como veremos.

59

Se a marcação não for encontrada, uma rotina é chamada para realizar uma série de operações em bytes aleatórios criptograficamente, resultantes de chamadas para CryptGenRandom() (embora modificados logo em seguida por outra série de operações). A chamada para esta função é exibida abaixo. O resultado será usado posteriormente no processo real de criptografia do arquivo.

60

Uma prévia dessas operações é demonstrada na imagem a seguir.

61

Em seguida, a rotina que efetivamente criptografa o arquivo é chamada.

62

A cifra de criptografia utilizada baseia-se em operações de add-rotate-xor (ARX), parecendo ser uma versão modificada da cifra de fluxo Salsa20. A imagem a seguir demonstra o que se assemelha muito a ela.

63

Quando a criptografia do arquivo é concluída, ambas as sequências de bytes aleatórios criptograficamente, que foram criptografadas pela chave pública RSA de 2048 bits, serão anexadas ao arquivo criptografado. A marcação de arquivo _RAGNAR_ também é anexada.

64

Após tudo ser gravado no novo arquivo como parte do processo de criptografia, o arquivo será movido via MoveFile(), essencialmente adicionando a ele uma nova extensão: .ragnar_E354BDB6

65

Finalmente, após a conclusão de tudo, o ransomware encerrará a execução:

  • Recuperando o SessionID da sessão do console (a sessão atualmente conectada ao console físico) via WTSGetActiveConsoleSessionId()
  • Abrindo o token do processo atual via OpenProcessToken() com DesiredAccess definido como TOKEN_ALL_ACCESS
  • Criando um novo token de acesso que o duplica via DuplicateTokenEx com dwDesiredAccess definido como TOKEN_ALL_ACCESS e TokenType como TokenPrimary
  • Definindo o SessionID do console no novo token de acesso duplicado via SetTokenInformation()
  • Criando um processo usando o novo token de acesso via CreateProcessAsUserW(), iniciando o notepad.exe com o arquivo da nota de resgate como argumento
  • Chamando ExitProcess(0)
66

E agora, a exibição obrigatória da nota de resgate.

67

Conclusão

A partir da análise detalhada do ransomware Ragnar Locker, que deixou uma nota de resgate especificamente direcionada à Energias de Portugal, é possível concluir alguns pontos. Primeiro, é evidente que o próprio ransomware não tenta estabelecer conexões internas ou externas, o que prova que quaisquer ficheiros roubados tiveram de ser extraídos da rede antes da execução do ransomware pelos atacantes. O executável descompactado apresenta um carimbo de data/hora (data de compilação) de "segunda-feira, 06.04.2020 19:57:20 UTC", que é 7 dias anterior à implementação real, sugerindo possivelmente que os autores tiveram acesso às redes da EDP desde, pelo menos, essa data.

O ransomware não utiliza técnicas anti-depuração ou anti-VM, nem faz muito para impedir ou atrasar a análise por olhares indiscretos. Muitas das ações realizadas pelo ransomware exigiriam privilégios de SISTEMA, embora não contenha capacidades de "contorno" do UAC (note-se as aspas; o UAC não é um limite de segurança). No entanto, como foi executado manualmente pelos atacantes, que devem ter tido acesso prévio, tais permissões poderiam ser facilmente identificadas (e possivelmente obtidas) antes da implementação. É a definição exata de ransomware, não fazendo mais nem menos. Se o idioma predefinido dos sistemas onde o ransomware é executado tiver um conjunto específico de definições, o processo termina imediatamente.

Em teoria, os autores podem possuir capacidades de desencriptação de ficheiros, uma vez que os dados criptograficamente seguros usados para derivar a chave simétrica e o nonce são anexados aos ficheiros recém-encriptados, de forma encriptada, utilizando a chave pública RSA de 2048 bits incorporada no binário (desencriptada apenas em tempo de execução). O ransomware poderia, muito provavelmente, ter sido detetado através de análise estática ou em tempo de execução, devido ao seu uso intensivo de WinAPIs aparentemente maliciosas.

Esperamos que tenha apreciado a nossa análise aprofundada da vertente técnica das fases finais do ataque.

Até à próxima,
Blaze Labs

Hashes de IOC (SHA256)

Amostra Compactada:
68eb2d2d7866775d6bf106a914281491d23769a9eda88fc078328150b8432bb3

Amostra Descompactada:
1de475e958d7a49ebf4dc342f772781a97ae49c834d9d7235546737150c56a9c

Referências

[1] – https://observador.pt/2020/04/13/edp-alvo-de-ataque-informatico-que-bloqueou-sistemas-de-atendimento-aos-clientes/

[2] – https://www.bleepingcomputer.com/news/security/ragnarlocker-ransomware-hits-edp-energy-giant-asks-for-10m/

Do you have questions? Let's talk.

Get in touch with our cybersecurity experts

Read More