Você está na página 1de 40

Introdução à Engenharia

Reversa de Aplicações Win32

Galileu Batista de Sousa


gbat2k -at +gmail -com
Agenda

Introdução à Engenharia Reversa


Sobre o que vamos efetivamente conversar
Introdução à Compilação
Fases da Compilação
Engenharia Reversa
Principais Ferramentas
Formatos de Arquivos “Executáveis”
Packers e Antidebugging
Malwares e suas características
Engenharia Reversa – é a obtenção de um
modelo de alto nível a partir do produto final.
Reengenharia – é construção de um novo
produto “igual”, a partir de um produto pronto.
Engenharia Reversa: em TI

Engenharia Reversa – é a reconstrução ou


adaptação de um programa código binário
executável.
Programa
Fonte
Liberar proteções não implica em
reconstruir o programa fonte original.
Compilador
Os assuntos envolvidos (arquitetura/
assembly / debugger / patcher) levam à
confusão nos nomes. Programa
Binário
Executável
A motivação para ER

Hackering:
Liberação de senhas / proteções /seriais
Lei 9809/98
Violar direitos de autor de programa de computador
6 meses a 2 anos de detenção + multa

Perda do Código Fonte e novas demandas


Investigação Policial:
Trojans / Bankers
Acesso a sistemas protegidos
Transplante de Aplicativos
O desafio da ER

Um programa binário é assim:


O desafio da ER

Ou assim ...
Metodologias para ER

Análise Estática (ou dead listing)


Disassembler do código binário
Análise das instruções (uma a uma)
Tediosa, complexa, compreensão de tudo
Análise Dinâmica
Auxílio de um debugger
Concentração em Red Points – interfaces conhecidas:
Sistema Operacional, usuário, bibliotecas conhecidas,
características da geração de código.
Agenda em detalhes

Uma visão das ferramentas envolvidas na


Engenharia reversa e a integração entre elas:
Compiladores / Packers / Identificadores de Executáveis /
Unpackers / Dissassembers / Debuggers / Decompiladores.
Arquitetura de computadores – o básico
Arquitetura de 32 bits (Intel x86)
Técnicas de localização/liberação de proteções
Código puro / Aplicações Delphi / Aplicações Java
Aplicação de patches de binários
Software Básico

Toda e qualquer ferramenta que suporta a


construção e execução de outros programas

Tem alguma interação / conhecimento da


arquitetura / hardware subjacente.

Sistema Operacional, Compilador, Loader,


Debugger, Profiler, Interpretador, Assembler,
Diassembler.
Agenda

Introdução à Engenharia Reversa


Sobre o que vamos efetivamente conversar
Introdução à Compilação
Fases da Compilação
Engenharia Reversa
Principais Ferramentas
Formatos de Arquivos “Executáveis”
Packers e Antidebugging
Malwares e suas características
Obtenção de código executável
Prog
Pré-processador
Fonte

Programa
Fonte

Compilador

Programa
Assembly

Prog
Montador
Objeto
Continuando ...
Programa
Programa Programa
Objeto
Objeto Objeto

Linker

Programa
Binário Execução
Executável

Packer

Programa
Loader
Binário
Executável
Um compilador típico

O compilador pode ser dividido em duas


grandes etapas:
Análise – Front End
Léxica, Sintática e Semântica
Síntese – Back End
Geração de código intermediário
Otimização de código
Geração de código final.
Um compilador por dentro

Código Código
Fonte Front End Back End Alvo

caracteres IR IR
Scanner Gerador Seletor de Instruções
tokens de IR IR

Parser Alocação Registradores


AST IR
Otimizador
Otimizador de máquina
An.Semântico
AST IR
Emissor de Código

AST = Árvore Sintática Abstrata Código Assembly


IR = Representação Intermediária
O Back End - Síntese

Um compilador usa determinados padrões para


geração de código:
Comandos estruturados têm uma tradução bem
conhecida e simples
A otimização de código:
Uso de registradores
Otimização de caches e pipelines

... é o que complica a descompilação


… e, por vezes, a liberação de proteções
O compilador monolítico

x86

FORTRAN

x64

ARM-32

C++

ARM-64

C#

IA-64
Separação: Back End / Front End

x86
FORTRAN

x64
c

ARM-32
IR
C++

ARM-64
C#
IA-64
IR = Representação Intermediária
Geração da Rep. Intermediária

Uma linguagem primitiva o suficiente para:


Permitir otimizações num nível similar às máquinas reais
Abstrair limitações das máquina reais:
Número de registradores, usos especiais, …
Permite a desvinculação entre linguagem e
arquitetura (no projeto do compilador):
“L linguagens” e “N máquinas”
N + M implementações para obter compiladores para
todas as possibilidades
Geração de RI - Expressões

LOAD t1, _C
LOAD t2, _D
MUL t2, t1, t2
A=B+C*D LOAD t3, _B
ADD t3,t2, t3
STORE _A, t3

Na prática, as referências às variáveis locais já são


estabelecidas como deslocamento na pilha, as
globais como endereços a serem associados depois
Geração de RI – Comando if

<<Condição>>

If (condicao) {
Comando JNE L1
}
<<Comando>>

L1:
Geração de RI – Comando if-else

<<Condição>>

If (condicao) { JNE L1
Comando1;
}
<<Comando1>>
Else {
JMP L2
Comando2;
}
L1:
<< Comando2>>

L2:
Geração de RI – Comando while

L1:
<<Condição>>

while (condicao) { JNE L2


Comando;
}
<<Comando>>
JMP L1

L2:
Geração de RI – Comando call

LOAD t1, _B
LOAD t2, _A
C = f (A, B) PARAM t2
PARAM t1
CALL _F, t3
STORE _C, t3
Variáveis Locais

As linguagens modernas suportam funções


recursivas, significando que:
O endereço das variáveis locais não pode ser fixo – cada
instância da função deve acessar uma “variável diferente”
Solução:
Cada rotina cria uma estrutura na pilha (o frame)
Um registrador (bp) aponta o início do frame
As variáveis são acessadas indiretamente, por um
deslocamento em relação ao bp
Graficamente …

void func ( int a, sp Buffer2 [0-3]


int b,
int c) Buffer2 [4-7]
{ Buffer2 [8-9]
char buffer1[5];
char buffer2[10]; Buffer1 [0-3]
...
} Buffer1 [4]

bp bp´
void main() {
func (1,2,3); End Retorno
}
0x1
 A instrução “ENTER n, 0” no x86
0x2

PUSH EBP 0x3

MOV EBP, ESP ....

SUB ESP, n
Otimização de código

Pode ser independente ou NÃO da arquitetura:


Movimentação de invariantes em laços
Eliminação de código inalcançável
Otimização de desvios
Reutilização de sub-expressões comuns
Uso de identidades algébricas
Alocação de registradores
Combinação de instruções
Geração de Código Final

Converter as instruções de código


intermediário em instruções da arquitetura
Casamento de padrões em árvores
Travessia recursiva em árvores de RI
Otimizar para a máquina
Pipelines / caches
Combinação de instruções
Gerar o prólogo/epílogo
Emitir as instruções
Tradução Dirigida pela Sintaxe

Parser &
Parser &
Análise
Análise Semântica
Semântica

Scanner
Scanner Gerador de RI

Tabela de
Símbolos
Fluxo de Controle
Fluxo de Dados
Compilador vs Interpretador

Compilador
Traduz código para execução posterior
Interpretador
Lê um comando no programa fonte (pode ser de baixo nível)
Executa o código diretamente, usando insts no seu próprio
espaço de endereçamento
Repete o processo até que um comando de parada seja
encontrado no programa fonte.
Java Bytecode ou MSIL (MS Interm Lang)
Compilação + interpretação de baixo nível
Assembler

Geração de Código objeto (binário) a partir de


código assembly (mnemônicos)
Podem conter um pré-processador integrado, o que o
torna um macro-assembler
Tradução de um-para-um
Resolução de endereços internos:
L1: TEST EAX, EAX
JNZ L2
SUB EAX, 1
ADD EBX, 10
JMP L1
L2:
ASM - Resolução de Endereços

Não existe resolução de símbolos externos:


É criada uma lista de símbolos referenciados
Uma lista de endereços do módulo que referenciam cada
símbolo

Quais são os símbolos?


Variáveis globais referenciadas no módulo
Variáveis globais criadas no módulo
Linkers

Combina vários módulos binários


Alguns oferecem rotinas e variáveis (símbolos)
Outros importam rotinas e variáveis (símbolos)
Possível importar e exportar ao mesmo tempo
Assembler:
Gera os módulos supondo que todos iniciam no Zero
Linker:
Posiciona as variáveis e rotinas
Resolve as dependências
O linker
var1
var2
var1
var3
var2
var3
func1
func1
func1
func2 func4
func2
func5 func5
var1
func3 func3
func3 var3
var2
var3
func1'
Tabela de

relocação
Tabela de
relocação func4
Tabela de
relocação
func5
A tabela de relocação

Onde variáveis globais são usadas:


Assembler cria o código supondo que inicia no “ZERO”

O linker necessita:
Reagrupar os vários módulos
Mudanças de endereços acontecem
Há, portanto, relocação de endereços
É necessária a atualização dos uso dos endereços (fixups)

Linker prepara código para carga no “ZERO”


Loaders

Faz a carga do programa, tornando-o processo:


É parte do Sistema Operacional
Se carga ocorre fora da posição original:
Atualização dos usos de endereços necessitam atualização

O caso das DLLs


São virtualmente carregadas no espaço do processo
“Todos” os endereços de uso das funções em DLL
necessitam ser atualizados (fixup).
Tipos de ligação

Estática:
Linker resolve completamente as referências

Semi-dinâmica:
Linker resolve parte das referências
Loader resolve outras (DLLs)

Dinâmica:
Algumas referências são tratadas pelo programador
LoadLibrary + GetProcessAddress
Os packers

Comprime e protegem o
código executável Programa
Programa
Análise estática impossível Executável
Executável
(Dados)

Alterações no executável Unpacker

dificultadas
Usados em malware (90%)
Packer
Decompiladores

Tentam reverter o código binário em fonte


Fases da decompilação
Loading: Identificação do formato do arquivo e da
arquitetura
Disassembling: Conversão do código de máquina em
uma representação intermediária
Conversão de conjuntos de instruções em idiomas
Análise de Fluxo de Dados
Análise de Fluxo do Programa (Bbásicos, dominância)
Emissão do código
Exercícios

Por enquanto é só isso ...

gbat2k -at +gmail -com

Você também pode gostar