Malwares

Adicionando código em um executável do Windows com um editor hexadecimal

Tempo de leitura: 10 minutos

Um executável do Windows é quase um arquivo compactado. Dentro dele tem código executável, tem imagens, tem certificado digital (as vezes) e podem ter várias outras coisas mais! Mas mesmo com tanta coisa, tudo fica bastante organizado porque segue um formato específico: o formato PE.

O formato PE (Portable Executable) é um formato usado em arquivos executáveis, DLLs, códigos objeto, device drivers, etc., em sistemas operacionais Windows. Neste formato estão diversas informações muito importantes para que o loader consiga carregar a imagem e executá-la como deve. Por exemplo as funções que serão importadas de DLLs do sistema, funções que podem ser usadas por outros executáveis (funções exportadas), endereços que serão manipulados pelo ASLR e muito mais.

Conhecendo bem esse formato, é possível adicionar um código qualquer e fazê-lo ser executado sempre que o usuário executar o arquivo! Era exatamente isso que os vírus antigos faziam.

Existem vários programas que realizam um “parser” deste formato em qualquer executável. O que eu mais gosto é o CFF Explorer. Caso você queira ver como o CFF Explorer pode ajudar nesse processo, veja meu vídeo sobre “Como funciona a infecção de um executável?”. Porém neste artigo seguirei no modo raiz total usando apenas um editor hexadecimal para adicionar um código ao executável do “Notepad++”. O editor hexadecimal que usarei é o HxD, como sempre.

Verificando o “DOS Header”

O DOS Header é um “cabeçalho” mantido por questões de compatibilidade. O campo que deve ser buscado está no endereço offset 3C. O campo se chama “e_lfanew”, ocupa o tamanho de um DWORD (4 bytes) e diz onde está o início do próximo cabeçalho (NT Header).

Abrindo o executável do Notepad++ no HxD, vou até o endereço offset 3C e vejo os 4 bytes a partir deste endereço. Lá está escrito: 08 01 00 00

Campo “e_lfanew”. Fica no endereço offset 0x3C e ocupa 4 bytes

Este número deve ser lido de “trás para frente” porque é armazenado em “little endian”. Portanto no endereço offset 3C está escrito 00 00 01 08. Ou seja, no endereço offset 0x108 (hexadecimal) começa o próximo header: o NT Header

Pegando o endereço onde contém as instruções que devem ser executadas primeiro

No NT Header existem 2 outros headers: o “File Header” e o “Optional Header” (que apesar do nome, não tem nada de opcional). No Optional Header existe um campo extremamente importante: o “AddressOfEntryPoint”. Este campo também ocupa o espaço de um DWORD e contém o endereço onde está a primeira instrução de máquina que deve ser executada quando o programa for iniciado. Essa primeira instrução é o “Ponto de Entrada”.

O “AddressOfEntryPoint” fica 40 bytes após o início do NT Header. No exemplo do Notepad++, o NT Header inicia no endereço offset 0x108 (hexadecimal). 0x108 + 0x28 (40 em decimal) = 0x130. Portanto no endereço offset 0x130 está o endereço de entrada do executável.

AddressOfEntryPoint no endereço offset 0x130

Já sabemos que esse valor está em little endian. Por isso o valor é: 00 12 DB 1B

Mas este número não deve ser entendido como offset no arquivo. Esse número se refere a um endereço “virtual”. Para entender isso, devemos ver as informações das “seções” deste programa.

Section Headers

O formato PE, como eu disse, tem várias coisas. Contém imagens, códigos executáveis, tabelas de endereços que devem ser realocados pelo ASLR, etc. Tudo isso fica organizado em “seções” e cada sessão tem características. Essas características definem se uma seção tem código executável, se a seção pode ser escrita (quando estiver na memória), etc. A seção que contém o código executável é a seção “.text”.

O cabeçalho de seções fica 248 bytes (0xF8 em hexadecimal) após o início do NT Header. No caso do Notepad++ seria: 0x108 + 0xF8 = 0x200. Portanto o cabeçalho de seções começa no endereço offset 0x200.

Os campos de cada seção no cabeçalho de seções contêm:

  • Nome da seção (8 bytes);
  • Tamanho Virtual (4 bytes);
  • Endereço Virtual (4 bytes);
  • Tamanho “cru” ou “em disco” (4 bytes);
  • Endereço “cru” ou offset no arquivo (4 bytes);
  • Os campos Endereço de Realocação (4 bytes), Número de Linhas (4 bytes), Número de Realocações (2 bytes), Número de número de linhas (2 bytes) não são usados;
  • Características (4 bytes);
Cabeçalho da seção “.text”

No primeiro retângulo preto temos o nome que ocupa 8 bytes. Na coluna de “texto” do editor hexadecimal vemos que o nome dessa primeira seção é “.text”.

No segundo retângulo temos o “tamanho virtual”. Diz o tamanho total da seção quando estiver na memória. Também está em “little endian”, por isso na imagem deve ser lido: 00185B44

No terceiro retângulo temos endereço virtual. Esse endereço será adicionado ao “endereço base” que o executável terá quando for carregado. Está em “little endian”, portanto deve ser lido: 00001000

No quarto retângulo está o tamanho “em disco”. Em “little endian”, deve ser lido: 00185C00

No quinto retângulo está o endereço offset dessa sessão. Em “little endian”, deve ser lido: 00000400

No sexto, sétimo, oitavo e nono retângulos estão, respectivamente, Endereço de Realocação, Número de Linhas, Número de Realocações e Número de número de linhas. Como disse, não são usados então são preenchidos com zeros.

No décimo retângulo estão as “características” dessa seção quando carregada em memória. Está em “little endian”, portanto deve ser lida: 60000020. Isso significa que o conteúdo desta seção é executável, contém código e pode ser lido.

Verificando o espaço para escrever mais código

No segundo e quarto retângulos temos o tamanho virtual (em memória) e o tamanho da seção no arquivo físico (antes de ir para a memória). De forma geral o tamanho físico da seção é maior que o tamanho virtual! Uma das possibilidades de inserir código em um executável é escrever nesta diferença de tamanho.

Vamos ver: o tamanho físico da seção de código do Notepad++ é 0x00185C00. O tamanho de seção na memória é 0x00185B44.

0x00185C00 – 0x00185B44 = 0xBC (188 em decimal)

Portanto temos 188 bytes livres no Notepad++ para escrever algum código.

Sabemos também que o código da seção começa no endereço offset 0x400 e tem 0x00185C00 bytes (1.596.416 em decimal) de tamanho. Portanto a seção vai de 0x400 até 0x185FFF (início do offset + tamanho em disco – 1).

Indo até o endereço offset 0x185FFF vemos os 188 bytes que não tem código desta seção.

188 bytes não utilizados da seção .text

Criando um código bem pequeno em Assembly

Neste ponto, para inserir um código, será necessário usar funções de DLLs do Windows. Isso é chato porque não é possível saber o endereço previamente! Então o que meu código deveria fazer seria pegar o endereço direto da IAT, eu teria que acrescentar alguns endereços na tabela de reloc por conta do ASLR, enfim. Eu queria fazer um código mais universal. Um código que funcionasse em qualquer arquivo PE sem precisar de tanta coisa.

Que tipo de código funciona em qualquer executável em tempo de execução sem nenhuma modificação (em teoria) e sem precisar modificar estruturas do formato PE? Shellcodes! Eu criei alguns shellcodes mas não conseguia descer o tamanho dele. Então eu encontrei o shellcode universal do Peter Ferrie, fiz algumas pequenas modificações nele, gerei o executável e peguei o código hexadecimal referente as instruções.

Este é o código em Assembly que usei:

xor edx,edx ; Limpa EDX
push edx ; Empurra zero para a stack
push 636C6163h ; Empurra clac (calc ao contrário)
push esp ; Empurra ESP. Assim terá, no topo da pilha, um ponteiro
;para "calc". Será usado para chamar WinExec
pop ecx ; Retira esse ponteiro da pilha e coloca em ECX
push edx ; Empurra zero
push ecx ; Empurra o ponteiro. Argumentos prontos para chamar
;WinExec
;Começando a andar pela memória do processo
assume fs:nothing ; comando para o MASM não ver erro em usar o
;registrador fs
mov esi,dword ptr fs:[edx+30h] ;Pega a PEB
assume fs:error ;Agora pode voltar ao normal do MASM
mov esi,dword ptr ds:[esi+0Ch]  ; Pega PPEB_LDR_DATA (LoaderData)
mov esi,dword ptr ds:[esi+0Ch]  ; Pega InLoadOrderModuleList
lodsd ; Pega o Flink do InLoadOrderModuleList
mov esi,dword ptr ds:[eax] ; Chegou o kernel32.dll
mov edi,dword ptr ds:[esi+18h] ; Pega o BaseAddress
mov ebx,dword ptr ds:[edi+3Ch] ; Percorrendo a memória da DLL
;Pega o e_lfanew do DOS Header
mov ebx,dword ptr ds:[edi+ebx+78h] ; Pegando o Export Directory 
;RVA
mov esi,dword ptr ds:[edi+ebx+20h] ; Pegando o AddressOfNames dos 
;exports
add esi,edi ; Entrando no endereço BaseAddress + RVA de 
;AddressOfNames
mov edx,dword ptr ds:[edi+ebx+24h] ; Pegando RVA de 
;AddressOfNameOrdinals (BaseAddress + Export Directory RVA + 24h)
aqui:
movzx ebp,word ptr ds:[edi+edx] ; movzx move algo para um 
;registrador e completa o resto do registrador com 0 se precisar. 
;Aqui ele pega o primeiro ordinal do AddressOfNameOrdinals e joga 
;em EBP (BaseAddress + AddressOfNameOrdinals)        
inc edx 
inc edx
; os dois acima vão para o próximo ordinal 
lodsd ; Carrega um DWORD apontado por de DS:ESI em EAX e soma 4 
;(porque é DWORD) em ESI
cmp dword ptr ds:[edi+eax],456E6957h ; Compara o nome da função 
;(BaseAddress + RVA do nome da Função) com EniW (os 4 primeiros
; bytes de WinExec ao contrário) 
jne aqui ; Se não for igual, apenas pule de volta para "aqui" 
mov esi,dword ptr ds:[edi+ebx+1Ch] ; Agora pegará o endereço de 
;AddressOfFunctions (BaseAddress + Export Directory RVA + 0x1C)
add esi,edi ; BaseAddress + RVA de AddressOfFunctions
add edi,dword ptr ds:[esi+ebp*4h] ; Pegará o endereço de 
;AddressOfFunctions + "OrdinalNumber"*4, e somará o conteúdo do 
;endereço de memória deste
;resultado com  EDI (que tem o incío de AddressOfFunctions)
call edi ; Agora EDI tem o endereço de memória do WinExec. É só 
;chamar a função =D

Este código gerou o seguinte código hexadecimal com apenas 72 bytes: 33D2526863616C6354595251648B72308B760C8B760CAD8B308B7E188B5F3C8B5C3B788B743B2003F78B543B240FB72C3A4242AD813C3857696E4575F08B743B1C03F7033CAEFFD7

Pronto! Já tenho o código hexadecimal que quero inserir no executável. Agora preciso inseri-lo no fim da seção .text, converter o endereço offset para endereço relativo virtual (RVA) e modificar o AddressOfEntryPoint com o novo RVA.

Convertendo RVA para offset e de offset para RVA (modificando o AddressOfEntryPoint)

Analisando os cabeçalhos das seções novamente, vejo que tem 7 seções: a .text, a .rdata, a .data, a .gfids, a .tls, a .rsrc e a .reloc

As 7 seções do Notepad++ no Editor Hexadecimal

Buscando todos os “tamanhos crus” das seções e seus “endereços virtuais” nos cabeçalhos de seções exatamente como fizemos na seção .text, vemos que que na memória:

.text começa em 0x00001000 e vai até 0x00186BFF

.rdata começa em 0x00187000 e vai até 0x001F8BFF

.data começa em 0x001F9000 e vai até 0x002011FF

.gfids começa em 0x00214000 e vai até 0x00214FFF

.tls começa em 0x00215000 e vai até 0x002151FF

.rsrc começa em 0x00216000 e vai até 0x002C15FF

.reloc começa em 0x002C2000 e vai até 0x002D63FF

Eu cheguei nesses valores fazendo esta conta: endereço virtual + tamanho cru – 1.

O AddressOfEntryPoint está marcado como 0x0012DB1B. Por isso sabemos que está em algum lugar da seção “.text”. Como converter este endereço virtual para endereço offset do arquivo? Simples: pegue este endereço, diminua o “endereço virtual” da seção e some o “endereço cru” do cabeçalho de seção.

Dos cabeçalhos de seção, .text tem endereço virtual “0x00001000” e “Raw Address” 0x00000400. O “Ponto de Entrada” (AddressOfEntryPoint) está em 0x0012DB1B. Portanto para achar o endereço offset do arquivo:

0x0012DB1B – 0x00001000 + 0x00000400 = 0x0012CF1B

O endereço offset que começa o código do Notepad++ está em 0x0012CF1B

Endereço offset do AddressOfEntryPoint

Nós conseguimos confirmar isso facilmente abrindo o programa em um debugger e vendo o código hexadecimal das instruções! Veja que todas elas conferem com a imagem do editor hexadecimal com exceção das que o debugger sublinhou porque estas são alteradas em tempo de execução pelo ASLR.

Ponto de Entrada visto no Debugger (x32dbg)

Já descobrimos também onde começa e termina a seção .text no arquivo (começa em 0x00000400 e termina em 0x00185FFF inclusive). Então vou até o endereço offset do fim dessa seção e vou colocar o código um pouco acima, nos espaços com zero (no endereço offset 0x00185F60).

Meu código adicionado no fim da seção

Agora preciso converter este endereço offset do arquivo para o endereço virtual! Para fazer isso, uso o mesmo processo: endereço offset – endereço “cru” do início da seção + endereço virtual

0x00185F60 – 0x00000400 + 0x00001000 = 0x00186B60

Pronto! Agora colocarei este endereço lá no AddressOfEntryPoint (lembrando que deve ser em Little Endian)

Alterando AddressOfEntryPoint com Editor Hexadecimal

Salvando esse arquivo com o nome “2.exe”, ao executar, vejo que meu código está rodando como deveria e está abrindo a calculadora! Mas só isso! Não inicia o Notepad++… Por que? Bem, porque precisamos retornar para o endereço antigo!

Finalizando a injeção de código (indo para o AddressOfEntryPoint antigo)

Agora eu preciso retornar para o endereço antigo. Para isso usarei a instrução “jmp”. Nesta instrução, só preciso colocar quantos bytes o código deve avançar ou voltar.

Se eu tenho um código assim:

push 0x15
pop ECX
aqui:
add eax, 0x05
jmp aqui

A instrução “jmp aqui” se tornará “EB FA” em código de máquina. Este “FA” em um espaço de 1 byte significa: -6. Ou seja, retorne 6 bytes. Um outro código semelhante seria:

jmp aqui
push 0x15
pop ECX
aqui:
add eax, 0x05

Agora o jmp iria para código de máquina como: “EF 06”. Seria: avance 6 bytes!

E espaços muito longos? Por exemplo avançar 5 mil bytes? Então ficaria: E9 88130000. Este “88130000” seria visto como DWORD (4 bytes) e está em “little endian”. Portanto deveria ser lido como “0x00001388” que é 5 mil em decimal! Ou seja: avance 5 mil bytes!

E volte 5000 bytes? Mesmo princípio. Retornar 5 mil bytes é o mesmo que “pule -5000 bytes”. -5000 em hexadecimal é 0xFFFFEC78. Colocando em Little Endian fica assim: E9 78ECFFFF!

Legal! Então vamos fazer cálculos.

O EntryPoint antigo era 0x0012DB1B. O “Endereço Virtual” do meu código (e novo EntryPoint) é “0x00186B60”. Meu código tem 72 bytes. Vou acrescentar mais um “E9” (que é o código de máquina para salto longo) e 4 bytes após o “E9”.

Então quantos bytes terão de diferença entre o antigo EntryPoint e o último byte do meu código?

0x0012DB1B (Antigo EntryPoint) – 0x00186B60 (Endereço Virtual de onde começa meu código) – 0x48 (72 bytes em hexadecimal) – 0x05 (E9, que é o código de máquina para salto longo, e 4 bytes de endereço) = 0xFFFA 6F6E (-364.690 em decimal)

Calculando quantos bytes de distância está o AddressOfEntryPoint original do meu código

Lembrando que colocarei esse valor em Little Endian, eu acrescento ao final do meu código: E9 6E6FFAFF

“jmp” adicionado ao final do meu código

Agora apenas salvo o arquivo e toda vez que eu executar o “2.exe”, será executado meu código para abrir a calculadora antes de executar o código do Notepad++.

É claro que coloquei um código bem tranquilo. Mas e se fosse um código malicioso? Que não abrisse nada visível na tela do usuário? É assim que vários vírus funcionam!

Gostou da matéria? Se sim, deixa um comentário!

Abraços e até a próxima

Leave a Comment

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *