Analisando Malware Empacotado com UPX Unpacker
Quando Usar
- Análise estática revela seções de alta entropia e poucas importações, indicando que o binário está empacotado
- PEiD, Detect It Easy ou PEStudio identificam UPX ou outro packer conhecido
- A tabela de importação contém apenas LoadLibrary e GetProcAddress (resolução de importação em tempo de execução, típica de binários empacotados)
- Você precisa recuperar o binário original para desmontagem e descompilação adequadas no Ghidra ou IDA
- A descompressão automatizada do UPX falha porque o autor do malware modificou os bytes mágicos ou cabeçalhos do UPX
Não use ao lidar com packers personalizados, protetores baseados em VM (Themida, VMProtect) ou amostras onde o desempacotamento dinâmico via depuração é mais apropriado.
Pré-requisitos
- UPX (Ultimate Packer for eXecutables) instalado (
apt install upx-uclou baixe de https://upx.github.io/) - Detect It Easy (DIE) para identificação de packers
- Python 3.8+ com a biblioteca
pefilepara reparo manual de cabeçalhos - x64dbg ou x32dbg para desempacotamento manual quando ferramentas automatizadas falham
- PE-bear ou CFF Explorer para inspeção e reparo de cabeçalhos PE
- VM de análise isolada sem conectividade de rede
Fluxo de Trabalho
Etapa 1: Identificar o Packer
Determine se a amostra está empacotada e identifique o packer:
# Verificar com Detect It Easy
diec suspect.exe
# Verificar com UPX (teste sem descompactar)
upx -t suspect.exe
# Detecção de entropia e packer baseada em Python
python3 << 'PYEOF'
import pefile
import math
pe = pefile.PE("suspect.exe")
print("Análise de Seções:")
for section in pe.sections:
name = section.Name.decode().rstrip('\x00')
entropy = section.get_entropy()
raw = section.SizeOfRawData
virtual = section.Misc_VirtualSize
print(f" {name:8s} Entropia: {entropy:.2f} Raw: {raw:>8} Virtual: {virtual:>8}")
# Verificar nomes de seção UPX
section_names = [s.Name.decode().rstrip('\x00') for s in pe.sections]
if 'UPX0' in section_names or 'UPX1' in section_names:
print("\n[!] Nomes de seção UPX detectados")
elif '.upx' in [s.lower() for s in section_names]:
print("\n[!] Nomes de seção variante UPX detectados")
# Verificar contagem de importações (binários empacotados têm muito poucas)
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
total_imports = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
print(f"\nTotal de importações: {total_imports}")
if total_imports < 10:
print("[!] Muito poucas importações - provavelmente empacotado")
else:
print("\n[!] Nenhum diretório de importação - fortemente empacotado")
PYEOF
Etapa 2: Tentar Descompressão UPX Padrão
Tente a descompressão UPX interna:
# Descompressão UPX padrão
upx -d suspect.exe -o unpacked.exe
# Se o UPX falhar com erro "not packed by UPX", os cabeçalhos podem estar modificados
# Saída detalhada para depuração
upx -d suspect.exe -o unpacked.exe -v 2>&1
# Verificar o arquivo descompactado
file unpacked.exe
diec unpacked.exe
Etapa 3: Reparar Cabeçalhos UPX Modificados
Se a descompressão padrão falhar, repare os bytes mágicos adulterados:
# Reparar cabeçalhos UPX modificados
import struct
with open("suspect.exe", "rb") as f:
data = bytearray(f.read())
# Bytes mágicos UPX: "UPX!" (0x55505821)
# Autores de malware comumente modificam estes para evitar descompressão automática
# Procurar por assinaturas UPX modificadas
upx_magic = b"UPX!"
modified_patterns = [b"UPX0", b"UPX\x00", b"\x00PX!", b"UPx!"]
# Encontrar e restaurar nomes de seção
pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
num_sections = struct.unpack_from("<H", data, pe_offset + 6)[0]
section_table_offset = pe_offset + 0x18 + struct.unpack_from("<H", data, pe_offset + 0x14)[0]
print(f"Offset PE: 0x{pe_offset:X}")
print(f"Número de seções: {num_sections}")
print(f"Offset da tabela de seções: 0x{section_table_offset:X}")
for i in range(num_sections):
offset = section_table_offset + (i * 40)
name = data[offset:offset+8]
print(f"Seção {i}: {name}")
# Restaurar bytes mágicos UPX no binário
# Procurar pela localização da assinatura do cabeçalho UPX (tipicamente perto do final dos dados empacotados)
for i in range(len(data) - 4):
if data[i:i+3] == b"UPX" and data[i+3] != ord("!"):
print(f"Bytes mágicos UPX modificados encontrados no offset 0x{i:X}: {data[i:i+4]}")
data[i:i+4] = b"UPX!"
print(f"Restaurado para: UPX!")
# Restaurar também nomes de seção se modificados
for i in range(num_sections):
offset = section_table_offset + (i * 40)
name = data[offset:offset+8].rstrip(b'\x00')
if name in [b"UPX0", b"UPX1", b"UPX2"]:
continue # Já está correto
# Verificar modificações comuns
if name.startswith(b"UP") or name.startswith(b"ux"):
original = f"UPX{i}".encode().ljust(8, b'\x00')
data[offset:offset+8] = original
print(f"Nome de seção restaurado no offset 0x{offset:X} para {original}")
with open("suspect_fixed.exe", "wb") as f:
f.write(data)
print("\nArquivo corrigido escrito. Tentar novamente: upx -d suspect_fixed.exe -o unpacked.exe")
Etapa 4: Desempacotamento Manual com Depurador
Quando o desempacotamento automatizado falhar completamente, use o desempacotamento dinâmico:
Desempacotamento UPX Manual com x64dbg:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Carregar a amostra empacotada no x64dbg
2. Executar até o ponto de entrada (breakpoint do sistema e depois F9)
3. Padrão do stub de descompressão UPX:
a. PUSHAD (salva todos os registradores)
b. Loop de descompressão (processa seções empacotadas)
c. Resolve importações (chamadas LoadLibrary/GetProcAddress)
d. POPAD (restaura registradores)
e. JMP para OEP (ponto de entrada original)
4. Definir breakpoint de hardware no ESP após PUSHAD:
- Após PUSHAD, clique com botão direito em ESP nos registradores -> Follow in Dump
- Definir breakpoint de hardware em acesso no endereço [ESP]
- Executar (F9) - para no POPAD antes do JMP para OEP
5. Avançar (F7/F8) até chegar ao JMP para OEP
6. No OEP: Use o plugin Scylla para despejar e corrigir importações:
- Plugins -> Scylla -> OEP = EIP atual
- Clique em "IAT Autosearch" -> "Get Imports"
- Clique em "Dump" para salvar o binário despejado
- Clique em "Fix Dump" para reparar a tabela de importação
Etapa 5: Validar Binário Descompactado
Verifique se a amostra descompactada é válida e completa:
# Verificar se o PE descompactado é válido
python3 << 'PYEOF'
import pefile
pe = pefile.PE("unpacked.exe")
# Verificar se as seções estão normais
print("Análise de Seções Descompactadas:")
for section in pe.sections:
name = section.Name.decode().rstrip('\x00')
entropy = section.get_entropy()
print(f" {name:8s} Entropia: {entropy:.2f}")
# Verificar se as importações foram resolvidas
print(f"\nContagem de importações:")
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll = entry.dll.decode()
count = len(entry.imports)
print(f" {dll}: {count} funções")
total = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
print(f" Total: {total} importações")
# Comparar tamanhos de arquivo
import os
packed_size = os.path.getsize("suspect.exe")
unpacked_size = os.path.getsize("unpacked.exe")
print(f"\nEmpacotado: {packed_size:>10} bytes")
print(f"Descompactado: {unpacked_size:>10} bytes")
print(f"Razão: {unpacked_size/packed_size:.1f}x")
PYEOF
Conceitos Chave
| Termo | Definição |
|---|---|
| Empacotamento (Packing) | Comprimir ou criptografar código executável para reduzir o tamanho do arquivo e dificultar a análise estática; o binário contém um stub de descompressão que restaura o código em tempo de execução |
| UPX | Ultimate Packer for eXecutables; empacotador de executáveis open-source comumente abusado por autores de malware por ser gratuito e eficaz |
| Ponto de Entrada Original (OEP) | Endereço real de início do código do malware antes do empacotamento; o stub de descompressão descomprime o código e então salta para o OEP |
| Reconstrução de Importações | Processo de reconstruir a tabela de endereços de importação após despejar um processo descompactado da memória usando ferramentas como Scylla ou ImpRec |
| PUSHAD/POPAD | Instruções x86 que salvam/restauram todos os registradores de propósito geral; o UPX usa esse padrão para preservar o estado dos registradores durante o desempacotamento |
| Entropia de Seção | Medida de aleatoriedade dos dados da seção PE; seções empacotadas mostram entropia > 7.0 enquanto seções de código normais têm média de 5.0-6.5 |
| Bytes Mágicos | Bytes de assinatura dentro de um arquivo que identificam seu formato; o UPX usa "UPX!" que autores de malware modificam para prevenir descompressão automatizada |
Ferramentas e Sistemas
- UPX: Empacotador de executáveis open-source com capacidade de descompressão integrada para arquivos devidamente empacotados
- Detect It Easy (DIE): Ferramenta de detecção de packers, compiladores e vinculadores que identifica proteção em arquivos PE, ELF e Mach-O
- x64dbg/x32dbg: Depurador Windows open-source usado para desempacotamento manual através de execução dinâmica e localização de OEP baseada em breakpoints
- Scylla: Ferramenta de reconstrução de importações integrada ao x64dbg para reconstruir a IAT após o despejo de memória
- PE-bear: Visualizador e editor de arquivos PE para inspecionar e reparar cabeçalhos PE após o desempacotamento
Cenários Comuns
Cenário: Desempacotando Malware com Cabeçalhos UPX Modificados
Contexto: Uma amostra de malware é identificada como empacotada com UPX pelos nomes das seções (UPX0, UPX1), mas upx -d falha com "CantUnpackException: header corrupted". O autor do malware modificou os bytes mágicos do UPX para evitar descompressão automatizada.
Abordagem:
- Abra o binário em um editor hexadecimal e procure pela área do cabeçalho UPX (tipicamente no final dos dados empacotados)
- Identifique os bytes mágicos modificados (ex.: "UPX!" alterado para "UPX\x00" ou completamente zerado)
- Use o script Python de reparo para restaurar a mágica "UPX!" e corrigir os nomes das seções
- Tente novamente
upx -dno binário reparado - Se o reparo falhar, recorra ao desempacotamento manual com x64dbg (PUSHAD -> BP de hardware no ESP -> POPAD -> JMP OEP)
- Valide se o binário descompactado tem importações adequadas e valores de entropia razoáveis
- Importe para Ghidra ou IDA para análise estática completa
Armadilhas:
- Assumir que UPX é o único packer; o binário pode estar duplamente empacotado (UPX + camada personalizada)
- Modificar a amostra empacotada original em vez de trabalhar em uma cópia
- Não reconstruir as importações após o despejo manual de memória (o binário despejado irá travar sem a correção da IAT)
- Esquecer de verificar dados de overlay anexados após as seções PE empacotadas com UPX
Formato de Saída
RELATÓRIO DE ANÁLISE DE DESEMPACOTAMENTO
===========================================
Amostra: suspect.exe
SHA-256: e3b0c44298fc1c149afbf4c8996fb924...
Packer: UPX 3.96 (cabeçalhos modificados)
BINÁRIO EMPACOTADO
Seções: UPX0 (entropia: 0.00) UPX1 (entropia: 7.89) .rsrc (entropia: 3.45)
Importações: 2 (kernel32.dll: LoadLibraryA, GetProcAddress)
Tamanho: 98.304 bytes
MÉTODO DE DESEMPACOTAMENTO
Método: Reparo de cabeçalho + UPX -d
Correção: Bytes mágicos UPX! restaurados no offset 0x1F000
Comando: upx -d suspect_fixed.exe -o unpacked.exe
Resultado: SUCESSO
BINÁRIO DESCOMPACTADO
Seções: .text (entropia: 6.21) .rdata (entropia: 4.56) .data (entropia: 3.12) .rsrc (entropia: 3.45)
Importações: 147 (kernel32, user32, advapi32, wininet, ws2_32)
Tamanho: 245.760 bytes (expansão de 2.5x)
OEP: 0x00401000
VALIDAÇÃO
PE Válido: Sim
Importações Resolvidas: Sim (147 funções em 8 DLLs)
Executável: Sim (roda sem travamento em sandbox)
PRÓXIMOS PASSOS
- Importar unpacked.exe para Ghidra para desmontagem completa
- Executar regras YARA contra o binário descompactado
- Enviar binário descompactado para VirusTotal para detecção melhorada