Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
packer-tutorial — Um tutorial sobre como escrever um packer para Windows! | Kitploit
Ferramentas/GitHubGitHub/frank2/packer-tutorial
Engenharia ReversaAnálise de MalwareAnálise de BináriosAprendizado e Educação
GitHubfrank2/packer-tutorial

packer-tutorial

Um tutorial sobre como escrever um packer para Windows!

Ver Repositório
31932há 2 anosRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

EMPACOTADORES

Índice

  1. O que é um empacotador?: Uma introdução ao propósito dos empacotadores e um guia sobre quais são as necessidades de desenvolvimento de um empacotador.
  2. Pré-requisitos: Ferramentas necessárias para trabalhar com este tutorial.
  3. Experimentando: Uma demonstração do que você vai construir com este tutorial.
  4. Esboçando nosso projeto CMake: Um mini tutorial de CMake sobre como criar um sistema de build para as necessidades um tanto complexas de um empacotador.
  5. Empacotando binários no seu stub: Um tutorial sobre como escrever a parte do empacotador da combinação empacotador/stub e também uma introdução ao formato de arquivo executável do Windows.
    1. Gerenciando recursos
    2. Analisando um arquivo PE
    3. Manipulando um arquivo PE
  6. Simulando o loader: Um tutorial sobre como construir um stub executável mínimo para desempacotar e carregar um executável de destino, e também uma introdução à manipulação mais avançada de executáveis do Windows.
    1. Lendo nosso PE da memória
    2. Carregando nosso PE para execução
    3. Resolvendo importações de API
    4. Resolvendo endereços
    5. Transferindo a execução
  7. Exercícios complementares: Alguns exercícios para expandir ainda mais seu conhecimento em desenvolvimento de empacotadores.

Parece uma leitura assustadora? Experimente a versão em apresentação, que resume este readme. Vídeo no YouTube em breve!

O que é um empacotador?

Um empacotador é um programa que descomprime e lança outro programa dentro do seu espaço de endereçamento (ou, às vezes, no espaço de endereçamento de outro processo). Ele é por vezes conhecido por ser o vetor que ataca ambientes de análise, como depuradores e sandboxes virtuais. É usado principalmente para algumas coisas:

  • Compressão: Os empacotadores são comumente empregados para comprimir o código de um determinado binário. Este é um dos seus poucos usos legítimos. Veja UPX para um exemplo de empacotador com compressão.
  • Ofuscação: Os empacotadores também são empregados ao tentar ofuscar ou defender um programa de engenharia reversa. Veja o empacotador packman da Riot Games para um exemplo de empacotador anti-reversão.
  • Evasão: Malware frequentemente usa uma variedade de empacotadores para evadir antivírus e até mesmo EDR. Veja esta análise de um empacotador SmokeLoader para um exemplo de empacotador evasivo.

Fundamentalmente, um empacotador tem apenas alguns passos básicos:

  1. Estágio de compressão: É aqui que o executável original é comprimido, ofuscado ou ambos, em um novo binário.
  2. Estágio de descompressão: É aqui que o executável empacotado descomprime ou desofusca seu executável original para carregamento.
  3. Estágio de carregamento: É aqui que o empacotador imita uma variedade de etapas semelhantes ao carregador de executáveis da plataforma host. Devido à complexidade dos binários executáveis, esta é a etapa mais complicada, dependendo do nível de profundidade que você deseja emular.
  4. Estágio de execução: É aqui que o código transfere do executável stub host para o código recém-carregado (ou seja, desempacotado).

Igualmente simples, um empacotador consiste apenas em algumas peças:

  • O empacotador: Esta peça é responsável por preparar o que é chamado de stub executável e fornecer a ele os dados necessários para desempacotar um determinado binário. Este é, em última análise, o estágio de compressão.
  • O stub: Este binário é a peça de código responsável por descomprimir, carregar e executar o binário original. Como você pode ver, o stub executável faz a maior parte do trabalho pesado de um empacotador.

Construir um empacotador pode ser complicado, já que o stub precisa ser construído e importado de alguma forma para o executável do empacotador. No Windows, aprender o sistema de build do Visual Studio além da simples compilação pode ser uma tarefa trabalhosa. Felizmente, o CMake fornece um sistema de build simples, multiplataforma, que suporta o Visual Studio e é altamente customizável!

Este tutorial tem como objetivo ensinar o seguinte:

  • Como segmentar e integrar as duas principais peças de um empacotador em um ambiente de build coeso e testável.
  • Como manipular um executável do Windows para adicionar dados adicionais e carregáveis.
  • Como navegar em um executável do Windows para recuperar e carregar dados arbitrários.
  • Como emular partes do carregador do Windows para carregar e executar um executável do Windows.

Se você já está familiarizado com C++ e CMake, sinta-se à vontade para pular direto para a seção de empacotamento. Caso contrário, continue lendo!

Pré-requisitos

  • Conhecimento de C++: Este tutorial é meio inútil se você não sabe C++, pois vamos fazer muita aritmética de ponteiros.
  • Visual Studio: O Visual Studio contém um compilador C++ para Windows completo. Este projeto foi testado com o Visual Studio 2019, mas versões mais novas devem funcionar.
  • CMake: O CMake é o sistema de build que usamos para ajudar a instrumentar nossos builds para o compilador do Visual Studio.

Experimentando

Primeiro, vamos provar, de alguma forma, que este sistema de build funciona e cria um executável corretamente empacotado. Depois de instalar o CMake e o Visual Studio, navegue até o diretório raiz deste repositório com o terminal de sua preferência e execute o seguinte:``` $ mkdir build $ cd build $ cmake ../

root@kitploit:~
Isso criará os arquivos de projeto necessários para compilar o código do tutorial do Packer. Em seguida, execute:```
$ cmake --build ./ --config Release

Isso compilará o projeto packer no modo Release. Em seguida, você pode executar o seguinte teste:``` $ ctest -C Release ./

root@kitploit:~
Se tudo correr bem, test\_pack e test_unpack devem ser concluídos com êxito. Se você quiser ver os resultados do empacotamento por si mesmo, deverá ver isto:```
$ ./packed.exe
I'm just a little guy!

Vamos falar sobre todas as partes móveis que tornaram isso possível.

Esboçando nosso projeto CMake

Então sabemos quais são os principais componentes de um empacotador, mas como podemos testar nosso empacotador imediatamente como parte do ciclo de desenvolvimento? Precisaremos adicionar um terceiro binário ao nosso projeto geral para testar adequadamente o processo de empacotamento e desempacotamento do nosso binário. No geral, precisamos de três projetos:

  • o empacotador
  • o stub
  • o executável dummy para empacotar

Também precisamos de uma biblioteca de compressão para garantir que o binário seja comprimido dentro do executável stub. zlib funcionará bem para isso.

Nossa hierarquia de projetos, para começar, deve ficar assim:``` packer/ +---+ CMakeLists.txt + dummy/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + stub/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + src/ | | | +---+ main.cpp | + zlib-1.2.13/ | +---+ CMakeLists.txt + ...

root@kitploit:~
Nosso main.cpp em cada pasta pode simplesmente ficar assim por enquanto:```cpp
#include <iostream>

int main(int argc, char *argv[]) {
    std::cout << "I'm just a little guy!" << std::endl;
    
    return 0;
}

Já devemos estabelecer que temos uma cadeia de dependências simples para lidar, que o CMake resolverá muito bem para nós com alguma instrumentação:

  • packer e stub dependem de zlib
  • packer depende de stub
  • dummy depende de packer (porque precisa ser empacotado pelo packer)

Vamos começar com o projeto raiz, o packer, para a nossa instrumentação do CMake.

O CMake normalmente exige uma versão mínima para lidar, já que existe há muito tempo e suporta uso de longo prazo de versões anteriores. Depois disso, podemos declarar nosso projeto packer como um projeto C++.```cmake

target a cmake version, you can target a lower version if you like

cmake_minimum_required(VERSION 3.24)

declare our packer as a C++ project (since zlib is a C project and the compilation

detection might get confused)

project(packer CXX)

root@kitploit:~
Também queremos declarar nosso empacotador como "MultiThreaded" (ou seja, /MT) em vez de "MultiThreadedDLL" (ou seja, /MD) para não termos que nos preocupar com dependências de DLL em tempo de execução.```cmake
# this line will mark our packer as MultiThreaded, instead of MultiThreadedDLL
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

A declaração entre colchetes angulares na declaração de definição de variável é chamada de expressão de gerador e nos ajuda a resolver dados de tempo de configuração quando necessário; você a verá bastante ao longo deste arquivo. O que essa expressão de gerador faz é emitir a string Debug quando detecta que a configuração é Debug, e não emite nada caso contrário. Isso fornece uma biblioteca de tempo de execução de MultiThreadedDebug quando um perfil de compilação Debug é selecionado, e MultiThreaded quando um perfil de compilação sem depuração é selecionado, como Release. Consulte expressões de gerador condicionais para entender bem o que está acontecendo aí. Para mais informações sobre a variável CMAKE_MSVC_RUNTIME_LIBRARY, veja a documentação do CMake.

O CMake permitirá que você organize seu código-fonte em hierarquias como o Visual Studio faz automaticamente ao criar projetos com sua interface, e também fará uma busca recursiva em suas pastas por nomes de arquivos correspondentes. Em nosso arquivo de configuração, definimos uma recursão global em cabeçalhos (.hpp), código (.cpp) e scripts de recurso (.rc). Para nosso exemplo, só precisamos mesmo de main.cpp, mas isso é útil saber para projetos maiores.```cmake

this will collect header, source and resource files into convenient variables

file(GLOB_RECURSE SRC_FILES ${PROJECT_SOURCE_DIR}/src/.cpp) file(GLOB_RECURSE HDR_FILES ${PROJECT_SOURCE_DIR}/src/.hpp) file(GLOB_RECURSE RC_FILES ${PROJECT_SOURCE_DIR}/src/*.rc)

this will give you source groups in the resulting Visual Studio project

source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Header Files" FILES ${HDR_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Source Files" FILES ${SRC_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Resource Files" FILES ${RC_FILES})

root@kitploit:~
Aqui devo explicar que o CMake é mais precisamente descrito como um **sistema de make**. Ele cria o "sistema de make" adequado para o ambiente fornecido com base no compilador fornecido. No Linux, isso seria um makefile para os compiladores detectados (ou fornecidos). Para nós no Windows, isso produz um projeto do Visual Studio compatível com a nossa versão do Visual Studio, o que significa que, uma vez que você crie o sistema de make do MSVC, você pode simplesmente usar o Visual Studio para tudo, se assim desejar! Em última análise, você está usando o CMake para configurar o Visual Studio. Então, o que você está fazendo aqui é essencialmente criar as árvores de arquivos do seu projeto no Visual Studio que a GUI faz para você ao criar um projeto.

Em seguida, precisamos adicionar nossos projetos dependentes:```cmake
# this will add zlib as a build target
add_subdirectory(${PROJECT_SOURCE_DIR}/zlib-1.2.13)

# this will add our stub project
add_subdirectory(${PROJECT_SOURCE_DIR}/stub)

# this will add our test dummy project
add_subdirectory(${PROJECT_SOURCE_DIR}/dummy)

Uma coisa que mencionei antes é que o projeto do packer depende do projeto do stub. O packer, de alguma forma, precisa reter o stub e manipulá-lo para eventualmente obter nosso executável empacotado. Podemos usar arquivos de recursos do Windows para eventualmente colocar nosso executável stub dentro do binário do packer, independentemente da configuração de build! Além disso, podemos usar o CMake para gerar esses arquivos para nós, de modo que nossas referências aos executáveis gerados sejam consistentes dentro do projeto CMake! Vamos gerar nossos arquivos de recursos por enquanto para podermos incluí-los em nosso projeto:```cmake

this will make sure our stub data will be included in the resources of our packer

despite where it may reside in cmake's build system

file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.hpp" CONTENT "#pragma once\n#define IDB_STUB 1000\n") file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.rc" CONTENT "#include <winresrc.h>\n#include "stub.hpp"\nIDB_STUB STUB "$<TARGET_FILE:stub>"\n")

root@kitploit:~
`CMAKE_CURRENT_BINARY_DIR` é a string que contém o seu diretório de build atual. O Visual Studio despeja os binários em pastas com base na sua configuração, então usamos a instrução de gerador `$<CONFIG>` para obter a configuração atual do build. Também usamos a instrução de gerador `$<TARGET_FILE:stub>` para gerar o nome do arquivo executável do binário stub depois que ele for compilado. Quando incluímos esses arquivos em nosso projeto -- o arquivo RC gerado e o cabeçalho gerado -- podemos integrar com sucesso nosso binário stub ao projeto packer.

Em seguida, declaramos um executável para nosso projeto packer com os arquivos coletados anteriormente:```cmake
# this will create our packer executable
add_executable(packer ${HDR_FILES} ${SRC_FILES})
target_sources(packer PRIVATE ${RC_FILES} "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/stub.rc")

Em seguida, vinculamos a biblioteca zlib importada para o linker:```cmake

this will link zlib to our packer

target_link_libraries(packer zlibstatic)

root@kitploit:~
Em vez disso, você pode vincular apenas `zlib` se realmente quiser a DLL do zlib.

Como temos arquivos gerados (e o zlib também, como parte de sua etapa de build), precisamos incluir os diretórios dinâmicos nos cabeçalhos de inclusão do projeto. Devido a uma dependência de os projetos estarem na raiz de um projeto no CMake, também adicionamos os diretórios de inclusão do zlib para o nosso binário stub:```cmake
# zlib, as part of its build step, drops a config header in the build directory.
# we do this too, so make sure to include everything for the build!
target_include_directories(packer PUBLIC
  "${PROJECT_SOURCE_DIR}/src"
  "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>"
  "${PROJECT_SOURCE_DIR}/zlib-1.2.13"
  "${CMAKE_CURRENT_BINARY_DIR}/zlib-1.2.13"
)

# also set the includes for the stub from here.
# we can't set this in the stub CMake file because CMake requires includes to be in the same
# directory as the build target. for this file, our build target is packer, so this sets
# up includes relative to the packer executable.
target_include_directories(stub PUBLIC
  "${PROJECT_SOURCE_DIR}/zlib-1.2.13"
  "${CMAKE_CURRENT_BINARY_DIR}/zlib-1.2.13"
)

Por fim, resolvemos as coisas em nossa cadeia de dependências informando ao CMake sobre a nossa situação de dependências: marque o stub como uma dependência do packer e o packer como uma dependência do dummy.```cmake

this will add our stub as a dependency and our dummy as being dependent on the packer.

add_dependencies(packer stub) add_dependencies(dummy packer)

root@kitploit:~
Os arquivos CMake para [o stub](https://github.com/frank2/packer-tutorial/blob/main/stub/CMakeLists.txt) e [o dummy](https://github.com/frank2/packer-tutorial/blob/main/dummy/CMakeLists.txt) devem ser bem fáceis de entender agora!

O melhor é que o CMake pode gerenciar os testes por nós! E neste ponto, gerar testes para o nosso empacotador é um processo tranquilo: podemos simplesmente emitir um comando, e se o código de saída for 0, o teste passa. Para simplificar, digamos que empacotamos um binário passando-o como primeiro argumento do executável. Queremos algo assim:```
$ packer.exe dummy.exe

Para fazer isso com o CMake é muito simples:```cmake

enable testing to verify our packer works

enable_testing() add_test(NAME test_pack COMMAND "$<TARGET_FILE:packer>" "$<TARGET_FILE:dummy>")

root@kitploit:~
Finalmente, testar se o programa descompacta ou não é ainda mais simples: basta executar a saída! Não deveria ser surpreendente quantos erros você encontrará que travam o programa simplesmente ao executá-lo quando você está tentando emular o loader! Digamos novamente, por uma questão de simplicidade, que o binário deve gerar a saída para "packed.exe" para um binário compactado. Nesse caso, tudo o que você precisa fazer é isto:```cmake
add_test(NAME test_unpack
  COMMAND "packed.exe")

Isto falhará graciosamente se o seu packer não tiver gerado o binário. Infelizmente, o CMake para Visual Studio ignora a variável ADDITIONAL_CLEAN_FILES, então terá de limpar manualmente todos os ficheiros gerados no seu sistema de build, incluindo os do stub.rc e stub.hpp gerados acima.

Parabéns! Muito trabalho foi feito para compilar e testar com sucesso o seu packer. Agora que comemos os vegetais, o nosso packer pode fazer o seguinte:

  • compilar todas as dependências na ordem correta
  • compilar o binário do stub e injetá-lo no nosso binário do packer
  • testar o processo de empacotar/desempacotar automaticamente

Agora podemos avançar para o que interessa!

Empacotar binários no seu stub

Então, configurámos com sucesso o nosso compilador para compilar o executável do stub como um recurso no nosso binário do packer, mas como conseguimos colocar um binário num estado empacotado no stub? Não podemos adicioná-lo como recurso porque não podemos esperar que o utilizador final do packer use apenas um compilador; o executável do packer deve ser uma solução autónoma.

Uma técnica que usei muito (embora possa ser óbvia para análise) é adicionar uma nova secção ao binário do stub para eventualmente ser carregada em tempo de execução. Isto acabará por servir também como um curso intensivo sobre o formato PE. Mas primeiro, como extraímos os dados dos recursos?

Gerir recursos

Existem três passos básicos para obter dados de recursos de um binário em tempo de execução:

  • Encontrar o recurso
  • Carregar o recurso
  • Bloquear o recurso (o que obtém os bytes do recurso)

A seguinte função demonstra como procurar e adquirir um recurso de um determinado binário:```cpp std::vectorstd::uint8_t load_resource(LPCSTR name, LPCSTR type) { auto resource = FindResourceA(nullptr, name, type);

if (resource == nullptr) { std::cerr << "Error: couldn't find resource." << std::endl; ExitProcess(6); }

auto rsrc_size = SizeofResource(GetModuleHandleA(nullptr), resource); auto handle = LoadResource(nullptr, resource);

if (handle == nullptr) { std::cerr << "Error: couldn't load resource." << std::endl; ExitProcess(7); }

auto byte_buffer = reinterpret_cast<std::uint8_t *>(LockResource(handle));

return std::vectorstd::uint8_t(&byte_buffer[0], &byte_buffer[rsrc_size]); }

root@kitploit:~
Com nossos dados em um vetor facilmente maleável, agora podemos analisar a imagem stub e adicionar novos dados a ela.

### Parsing a PE file

Um executável Windows, em seus componentes mais básicos, é dividido em duas partes: seus *cabeçalhos* e seus *dados de seção*. Os cabeçalhos contêm muitos metadados importantes para o processo de carregamento, e os dados de seção são apenas isso-- dados, que podem ser código executável (ou seja, a seção `.text`) ou dados arbitrários (ou seja, uma seção `.data`). O início de todo executável Windows começa com uma estrutura `IMAGE_DOS_HEADER`:```c
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Embora essa estrutura pareça ter muita coisa acontecendo, você provavelmente pode deduzir pelo nome que esse cabeçalho é um resquício de versões passadas do Windows e do Microsoft DOS. Aqui, só nos preocupamos com dois valores nesse cabeçalho: e_magic e e_lfanew. e_magic é simplesmente o valor do cabeçalho mágico no topo da imagem, o "MZ" no início do arquivo. e_lfanew é o deslocamento para os cabeçalhos NT a partir do início do arquivo, que são os cabeçalhos PE contendo muito mais informações de metadados sobre o executável. Podemos, por exemplo, construir um validador PE simples para o nosso empacotador assim:```cpp void validate_target(const std::vectorstd::uint8_t &target) { auto dos_header = reinterpret_cast<const IMAGE_DOS_HEADER *>(target.data());

// IMAGE_DOS_SIGNATURE is 0x5A4D (for "MZ") if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { std::cerr << "Error: target image has no valid DOS header." << std::endl; ExitProcess(3); }

auto nt_header = reinterpret_cast<const IMAGE_NT_HEADERS *>(target.data() + dos_header->e_lfanew);

// IMAGE_NT_SIGNATURE is 0x4550 (for "PE") if (nt_header->Signature != IMAGE_NT_SIGNATURE) { std::cerr << "Error: target image has no valid NT header." << std::endl; ExitProcess(4); }

// IMAGE_NT_OPTIONAL_HDR64_MAGIC is 0x020B if (nt_header->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { std::cerr << "Error: only 64-bit executables are supported for this example!" << std::endl; ExitProcess(5); } }

root@kitploit:~
`IMAGE_NT_HEADERS` é uma estrutura bastante grande no geral, então não vou documentar tudo aqui, mas você pode encontrar tudo o que precisa saber sobre ela na [documentação da Microsoft sobre o cabeçalho](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64). De qualquer forma, só precisaremos de alguns membros da estrutura desses cabeçalhos. Por enquanto, devemos comprimir nosso binário alvo para adicioná-lo aos dados do nosso stub.

Usar as funções `compress` e `decompress` do zlib é bastante direto. Se você realmente quiser se aprofundar no trabalho com o zlib, sugiro usar as funções `deflate`/`inflate`, que permitem trabalhar em fluxos de compressão em blocos por vez. Veja a seção "Advanced Functions" do [manual do zlib](https://www.zlib.net/manual.html). Para este exemplo, porém, `compress` e `decompress` serão suficientes.

Para começar, obtemos um valor de tamanho com a função `compressBound` do zlib com base no tamanho do nosso binário alvo. Esse valor de tamanho corresponde ao valor máximo necessário para armazenar um fluxo de dados compactado, dado o tamanho dos dados. Podemos então usar esse valor para alocar um vetor que conterá os dados compactados. A função `compress` eventualmente retorna o tamanho real do buffer compactado, para o qual podemos redimensionar nosso vetor para o tamanho adequado.```cpp
// get the maximum size of a compressed buffer of the target binary's size.
uLong packed_max = compressBound(target.size());
uLong packed_real = packed_max;

// allocate a vector with that size
std::vector<std::uint8_t> packed(packed_max);
   
if (compress(packed.data(), &packed_real, target.data(), target.size()) != Z_OK)
{
   std::cerr << "Error: zlib failed to compress the buffer." << std::endl;
   ExitProcess(8);
}

// resize the buffer to the real compressed size
packed.resize(packed_real);

Manipulando um arquivo PE

Vamos dedicar um momento para falar sobre alinhamento de dados. Um determinado fluxo de dados é considerado alinhado se o seu endereço ou tamanho for divisível por um determinado limite de alinhamento. Por exemplo, em arquivos PE, as seções de dados em disco são tipicamente alinhadas ao limite de 0x400, enquanto em memória são alinhadas ao limite de 0x1000. Podemos determinar se um determinado valor está alinhado realizando um módulo do alinhamento sobre o valor (ou seja, valor % alinhamento == 0). Arquivos PE podem ser arbitrariamente alinhados a outros valores, e esse valor está presente e é importante para o carregador de PE como um todo. Alinhar um determinado valor a um determinado limite é uma operação relativamente simples:```cpp template T align(T value, T alignment) { auto result = value + ((value % alignment == 0) ? 0 : alignment - (value % alignment)); return result; }

root@kitploit:~
Esta função essencialmente preenche um valor potencialmente desalinhado com o restante necessário para ser devidamente alinhado a um determinado limite.

Para adicionar corretamente dados arbitrários ao nosso arquivo PE, precisamos estar atentos, em particular, ao *alinhamento de arquivo* — podemos calcular os valores alinhados adequados para o arquivo PE quando ele estiver em memória mais tarde, mas por enquanto, ao adicionar nossos dados, precisamos alinhar nosso arquivo ao limite de alinhamento de arquivo. No código a seguir, obtemos os cabeçalhos, depois os limites de alinhamento de arquivo e de alinhamento de seção, e então prosseguimos para alinhar nossos dados de stub ao limite do arquivo e anexar nossa seção recém-empacotada.```cpp
// next, load the stub and get some initial information
std::vector<std::uint8_t> stub_data = load_resource(MAKEINTRESOURCE(IDB_STUB), "STUB");
auto dos_header = reinterpret_cast<IMAGE_DOS_HEADER *>(stub_data.data());
auto e_lfanew = dos_header->e_lfanew;

// get the nt header and get the alignment information
auto nt_header = reinterpret_cast<IMAGE_NT_HEADERS64 *>(stub_data.data() + e_lfanew);
auto file_alignment = nt_header->OptionalHeader.FileAlignment;
auto section_alignment = nt_header->OptionalHeader.SectionAlignment;

// align the buffer to the file boundary if it isn't already
if (stub_data.size() % file_alignment != 0)
   stub_data.resize(align<std::size_t>(stub_data.size(), file_alignment));
      
// save the offset to our new section for later for our new PE section
auto raw_offset = static_cast<std::uint32_t>(stub_data.size());

// encode the size of our unpacked data into the stub data
auto unpacked_size = target.size();
stub_data.insert(stub_data.end(),
                 reinterpret_cast<std::uint8_t *>(&unpacked_size),
                 reinterpret_cast<std::uint8_t *>(&unpacked_size)+sizeof(std::size_t));

// add our compressed data.
stub_data.insert(stub_data.end(), packed.begin(), packed.end());

Agora, podemos ter adicionado os dados da nossa seção de acordo com os limites das seções do arquivo, mas o nosso executável stub ainda não tem conhecimento dessa seção no arquivo PE. Precisamos não apenas analisar a tabela de seções do arquivo PE, mas também adicionar uma nova entrada que aponte para a nossa seção. Esse é o motivo da variável raw_offset.

Primeiro, podemos incrementar o número de seções facilmente atualizando o NumberOfSections. Normalmente, os dados após a última tabela de seções são zerados, então podemos sobrescrever os dados zerados com a nossa nova seção facilmente.```cpp // increment the number of sections in the file header auto section_index = nt_header->FileHeader.NumberOfSections; ++nt_header->FileHeader.NumberOfSections;

root@kitploit:~
Em seguida, precisamos obter um ponteiro para a própria tabela de seções. Embora, tecnicamente, ela venha imediatamente após o cabeçalho opcional dos cabeçalhos NT, o tamanho do cabeçalho opcional é, na verdade, determinado pelo valor `SizeOfOptionalHeader` do cabeçalho de arquivo NT. Então, para chegar até ela, precisamos calcular um ponteiro do topo da struct `OptionalHeader` até o deslocamento fornecido pelo valor `SizeOfOptionalHeader`.```cpp
// acquire a pointer to the section table
auto size_of_header = nt_header->FileHeader.SizeOfOptionalHeader;
auto section_table = reinterpret_cast<IMAGE_SECTION_HEADER *>(
   reinterpret_cast<std::uint8_t *>(&nt_header->OptionalHeader)+size_of_header
);

Finalmente, estamos prontos para começar a adicionar os metadados da nossa seção. Este é o nosso PE cabeçalho de seção:```c typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // IMAGE_SIZEOF_SHORT_NAME is 8 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

root@kitploit:~
As variáveis específicas que nos interessam para o nosso novo cabeçalho de seção são `Name`, `VirtualSize`, `VirtualAddress`, `SizeOfRawData`, `PointerToRawData` e `Characteristics`. Neste ponto, você deve estar ciente de que a razão pela qual você precisa conhecer dois tipos de alinhamento-- alinhamento de arquivo e alinhamento de memória-- é porque existem dois tipos diferentes de estados de memória para um determinado executável PE: como ele se parece no *disco*, e como ele se parece na *memória*, como resultado do processo de carregamento. É possível configurar um determinado arquivo PE para ter o mesmo layout de memória independentemente de ser carregado ou não, mas não é uma configuração frequente.

A variável `Name` é o rótulo de 8 bytes que você pode dar à sua nova seção. Escolhi `.packed`, pois é uma string ASCII de 7 bytes e cabe perfeitamente no buffer.

`VirtualAddress` refere-se ao deslocamento de uma determinada seção na memória. Também é conhecido como "endereço virtual relativo", ou RVA. `VirtualSize` refere-se ao tamanho da seção na memória. (Vale notar que o MSVC compila esse valor como o valor de tamanho não alinhado da seção, então seguimos essa convenção também em nossa seção.) `PointerToRawData` refere-se ao deslocamento de uma determinada seção no disco, e `SizeOfRawData` refere-se ao tamanho da seção no disco.

`Characteristics` é complicado e pode se referir a uma seção ser legível, gravável ou executável, além de outros indicadores; veja a seção de características da [documentação de `IMAGE_SECTION_HEADER`](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header). Por enquanto, você deve saber que tudo o que precisamos é que a seção seja legível e marcada como contendo dados inicializados.

Com tudo isso em mente, agora podemos criar nossa nova seção!```cpp
// get a pointer to our new section and the previous section
auto section = &section_table[section_index];
auto prev_section = &section_table[section_index-1];

// calculate the memory offset, memory size and raw aligned size of our packed section
auto virtual_offset = align(prev_section->VirtualAddress + prev_section->Misc.VirtualSize, section_alignment);
auto virtual_size = section_size;
auto raw_size = align<DWORD>(section_size, file_alignment);

// assign the section metadata
std::memcpy(section->Name, ".packed", 8);
section->Misc.VirtualSize = virtual_size;
section->VirtualAddress = virtual_offset;
section->SizeOfRawData = raw_size;
section->PointerToRawData = raw_offset;

// mark our section as initialized, readable data.
section->Characteristics = IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA;

Isso agora apresenta um pequeno problema, porém: o tamanho da imagem mudou. Você pensaria que não seria um problema, mas no cabeçalho opcional do cabeçalho NT há uma variável chamada SizeOfImage que determina quanto espaço o loader precisa alocar para nosso executável. Isso é uma correção simples, no entanto: o tamanho que precisamos para nossa imagem é o tamanho da última seção alinhada ao limite de alinhamento da seção.```cpp // calculate the new size of the image. nt_header->OptionalHeader.SizeOfImage = align(virtual_offset + virtual_size, section_alignment);

root@kitploit:~
E é isso! Adicionamos com sucesso nosso binário compactado como uma nova seção para o nosso stub eventualmente descomprimir e carregar. Agora podemos simplesmente salvar a imagem do stub modificada em disco.```cpp
std::ofstream fp("packed.exe", std::ios::binary);
   
if (!fp.is_open()) {
   std::cerr << "Error: couldn't open packed binary for writing." << std::endl;
   ExitProcess(9);
}
   
fp.write(reinterpret_cast<const char *>(stub_data.data()), stub_data.size());
fp.close();

Parabéns! Até agora, conseguimos realizar o seguinte:

  • Compilamos, injetamos e recuperamos nosso binário stub do diretório de recursos do nosso empacotador em tempo de execução
  • Analisamos os cabeçalhos executáveis do nosso binário stub e do nosso binário alvo para recuperar informações essenciais
  • Modificamos um executável para estender seus dados de seção de modo a conter nosso executável empacotado

Estamos na metade do caminho na escrita do nosso empacotador! Agora podemos avançar para a parte indiscutivelmente mais difícil do processo: dar corpo ao binário stub.

Simulando o carregador

Embora os detalhes de escrever um stub de desempacotamento possam ficar complicados internamente, no fundo tudo se resume a apenas alguns passos:

  • Recuperar os dados da imagem: adquirir e descomprimir (e opcionalmente desofuscar) a representação do binário alvo para processamento posterior.
  • Carregar a imagem: de longe o mais complicado, é aqui que o carregador é simulado e a imagem alvo é preparada para execução.
  • Chamar o entrypoint da imagem: normalmente chamado de "original entry point", ou OEP, este é o ponto em que você transfere do estágio de carregamento para o estágio de execução do seu binário empacotado.

Esse processo é tão simples que nossa rotina principal é apenas um punhado de funções:```cpp int main(int argc, char *argv[]) { // first, decompress the image from our added section auto image = get_image();

// next, prepare the image to be a virtual image
auto loaded_image = load_image(image);

// resolve the imports from the executable load_imports(loaded_image);

// relocate the executable relocate(loaded_image);

// get the headers from our loaded image auto nt_headers = get_nt_headers(loaded_image);

// acquire and call the entrypoint auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint; reinterpret_cast<void(*)()>(entrypoint)();

return 0; }

root@kitploit:~
### Lendo nosso PE da memória

Primeiro, precisamos de alguma forma obter os dados da nossa seção criada pelo packer do binário em execução. É possível obter os cabeçalhos do binário em execução em tempo de execução? Sim, absolutamente! [`GetModuleHandleA`](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea), com um argumento nulo, em última análise, retorna um ponteiro para os cabeçalhos do nosso PE em execução! Então, em tempo de execução, você tem acesso fácil à imagem como ela existe na memória. Isso é o que torna adicionar uma nova seção ao nosso binário tão atraente: podemos analisar muito facilmente nossa seção alvo do binário.

Com o que aprendemos sobre a análise da tabela de seções, esta seção de código deve ser fácil de entender:```cpp
// find our packed section
auto base = reinterpret_cast<const std::uint8_t *>(GetModuleHandleA(NULL));
auto nt_header = get_nt_headers(base);
auto section_table = reinterpret_cast<const IMAGE_SECTION_HEADER *>(
   reinterpret_cast<const std::uint8_t *>(&nt_header->OptionalHeader)+nt_header->FileHeader.SizeOfOptionalHeader
);
const IMAGE_SECTION_HEADER *packed_section = nullptr;

for (std::uint16_t i=0; i<nt_header->FileHeader.NumberOfSections; ++i)
{
   if (std::memcmp(section_table[i].Name, ".packed", 8) == 0)
   {
      packed_section = &section_table[i];
      break;
   }
}

if (packed_section == nullptr) {
   std::cerr << "Error: couldn't find packed section in binary." << std::endl;
   ExitProcess(1);
}

Em seguida, precisamos descomprimir os dados do nosso stub a partir do binário. O zlib recomenda que codifiquemos o tamanho da carga útil original descomprimida de alguma forma acessível à rotina de descompressão, por isso codificamos o tamanho do binário descomprimido no cabeçalho dos nossos dados empacotados. Então, obtemos um ponteiro para os nossos dados descomprimidos, criamos um novo buffer que possa armazenar os dados descomprimidos e prosseguimos para chamar a função de descompressão do zlib.```cpp // decompress our packed image auto section_start = base + packed_section->VirtualAddress; auto section_end = section_start + packed_section->Misc.VirtualSize; auto unpacked_size = *reinterpret_cast<const std::size_t *>(section_start); auto packed_data = section_start + sizeof(std::size_t); auto packed_size = packed_section->Misc.VirtualSize - sizeof(std::size_t);

auto decompressed = std::vectorstd::uint8_t(unpacked_size); uLong decompressed_size = static_cast(unpacked_size);

if (uncompress(decompressed.data(), &decompressed_size, packed_data, packed_size) != Z_OK) { std::cerr << "Error: couldn't decompress image data." << std::endl; ExitProcess(2); }

return decompressed;

root@kitploit:~
Como você pode ver, `get_image` era uma função relativamente simples no final das contas. Extraímos nosso binário alvo da seção anexada, então agora precisamos carregá-lo.

### Carregando nosso PE para execução

O carregador de executáveis do Windows faz muitas coisas diferentes nos bastidores e suporta muitas configurações diferentes de executáveis. É provável que você encontre uma variedade de erros com o empacotador que este tutorial produz se explorar mais do que o binário de exemplo, pois a configuração que construiremos hoje é tecnicamente muito mínima. Mas, para estabelecer esse mínimo absoluto para execução, precisamos fazer as seguintes coisas para carregar um executável Windows moderno:

* alocar a imagem que contém a representação em memória da imagem do executável
* mapear as seções da nossa imagem executável, incluindo os cabeçalhos, para essa imagem alocada
* resolver as importações em tempo de execução para outras bibliotecas que nosso binário precisa
* remapear a imagem do binário para que vários endereços na nossa imagem apontem para onde deveriam

Vamos começar com a função `load_image`. Para começar, precisamos obter nossa tabela de seções do binário empacotado. Isso eventualmente será mapeado na nossa imagem recém-alocada. Para um buffer de executável adequado, a alocação é muito simples: pegue o valor `SizeOfImage` dos nossos cabeçalhos de seção e crie um novo buffer com [`VirtualAlloc`](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc) que crie uma imagem legível, gravável e executável para mapearmos.```cpp
// get the original image section table
auto nt_header = get_nt_headers(image.data());
auto section_table = reinterpret_cast<const IMAGE_SECTION_HEADER *>(
   reinterpret_cast<const std::uint8_t *>(&nt_header->OptionalHeader)+nt_header->FileHeader.SizeOfOptionalHeader
);

// create a new VirtualAlloc'd buffer with read, write and execute privileges
// that will fit our image
auto image_size = nt_header->OptionalHeader.SizeOfImage;
auto base = reinterpret_cast<std::uint8_t *>(VirtualAlloc(nullptr,
                                                          image_size,
                                                          MEM_COMMIT | MEM_RESERVE,
                                                          PAGE_EXECUTE_READWRITE));

if (base == nullptr) {
   std::cerr << "Error: VirtualAlloc failed: Windows error " << GetLastError() << std::endl;
   ExitProcess(3);
}

Com o nosso buffer alocado, o que precisamos fazer é copiar os cabeçalhos e as seções para ele. Isso precisa estar em conformidade com a variável SectionAlignment mencionada anteriormente. Felizmente, a maneira como preparamos a nossa seção-- e a maneira como as outras seções são preparadas-- elas já estão alinhadas ao limite do SectionAlignment. Basta dizer que o resultado final é uma simples aritmética de ponteiros: copiar a nossa imagem alvo no deslocamento PointerToRawData, para a nossa imagem carregada no deslocamento VirtualAddress.

Opcionalmente, você pode copiar os cabeçalhos do PE para o topo da imagem. Manter os cabeçalhos originais facilita o desenvolvimento, mas querer removê-los é um bom passo em direção à construção de um empacotador hostil à análise. Copiamos os cabeçalhos aqui para facilitar o uso.```cpp // copy the headers to our new virtually allocated image std::memcpy(base, image.data(), nt_header->OptionalHeader.SizeOfHeaders);

// copy our sections to their given addresses in the virtual image for (std::uint16_t i=0; i<nt_header->FileHeader.NumberOfSections; ++i) if (section_table[i].SizeOfRawData > 0) std::memcpy(base+section_table[i].VirtualAddress, image.data()+section_table[i].PointerToRawData, section_table[i].SizeOfRawData);

return base;

root@kitploit:~
Então a parte fácil do processo de carregamento acabou: recuperamos nosso binário da memória, desempacotamos e remapeamos suas seções para uma região de memória executável. Com nossa imagem preparada, podemos mergulhar nos detalhes minuciosos do processo de carregamento.

### Resolvendo importações de API

Dentro dos cabeçalhos opcionais há algo chamado *diretório de dados*. Esse diretório contém muitas informações diferentes sobre o executável, como símbolos exportados pela imagem e recursos como ícones e bitmaps. Neste tutorial, vamos analisar dois diretórios de dados: o **diretório de importação** e o **diretório de realocação**. Cada diretório é fixo (hardcoded), e seus índices podem ser encontrados [na documentação do cabeçalho opcional](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32) (role para baixo até a descrição de `DataDirectory`). Um diretório de dados está presente se o valor de `VirtualAddress` for não nulo.```c
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Recuperamos ponteiros para os diretórios de dados através do casting do RVA fornecido. Por exemplo, é assim que você eventualmente obtém a tabela de importação a partir do diretório de dados de importação:```cpp // get the import table directory entry auto nt_header = get_nt_headers(image); auto directory_entry = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

// if there are no imports, that's fine-- return because there's nothing to do. if (directory_entry.VirtualAddress == 0) { return; }

// get a pointer to the import descriptor array auto import_table = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR *>(image + directory_entry.VirtualAddress);

root@kitploit:~
Para resolver importações de API, o carregador analisa este diretório e, em seguida, carrega as bibliotecas necessárias e adquire as suas funções importadas. Felizmente, é um diretório relativamente fácil de analisar.

Começa com uma estrutura de descritor de importação:```c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

O que mais nos preocupa são duas variáveis com nomes um tanto confusos: OriginalFirstThunk e FirstThunk. OriginalFirstThunk contém informações sobre as importações desejadas por este executável em relação à DLL fornecida pelo RVA Name. Para aumentar a confusão, FirstThunk também contém. O que as diferencia? FirstThunk contém as importações depois de resolvidas. Essa resolução é realizada pela infame função GetProcAddress.

Nossos thunks são estruturas de dados adicionais com as quais precisamos lidar:```c typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; // PBYTE ULONGLONG Function; // PDWORD ULONGLONG Ordinal; ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA64;

root@kitploit:~
Esta estrutura de dados cobre tanto thunks de importação quanto de exportação, portanto `ForwarderString` pode ser ignorado. Um thunk de importação pode ser um `Ordinal` ou um RVA para outra estrutura, `IMAGE_IMPORT_BY_NAME`. Um *ordinal* é simplesmente um deslocamento na tabela de exportação da DLL fornecida. Uma estrutura `IMAGE_IMPORT_BY_NAME` tem esta aparência:```c
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Isto é um exemplo de uma estrutura variádica. Ela tira vantagem da falta de verificação de limites no acesso a arrays para criar estruturas que podem ter tamanho variável. Name, neste caso, deve ser uma C-string terminada em zero.

Como dados binários não contêm tipos, um ordinal e um import são diferenciados pelo bit mais significativo na entrada de thunk. O ordinal está contido na metade inferior do inteiro em implementações de 32 bits e de 64 bits.

Nossa tabela de importação funciona como uma C-string-- sua última entrada é um OriginalFirstThunk terminado em nulo para sinalizar o fim das importações potenciais. Nossos dados de thunk funcionam da mesma forma, terminando com uma entrada nula no array de thunks.

Para juntar tudo, é assim que abordamos a análise da tabela de importação em pseudocódigo:``` for every import descriptor: load the dll parse the original and first thunk

root@kitploit:~
for every thunk:
    if ordinal bit set:
        import by ordinal
    else:
        import by name
        
    store import in first thunk
root@kitploit:~
Curiosamente, apesar de `GetProcAddress` ser tipada com uma C-string como argumento para a função, o Windows espera que você importe por ordinal simplesmente convertendo o valor ordinal como uma C-string. Vai entender.

Com essas explicações em mente, este loop while agora deve fazer sentido:```cpp
// when we reach an OriginalFirstThunk value that is zero, that marks the end of our array.
// typically all values in the import descriptor are zero, but we do this
// to be shorter about it.
while (import_table->OriginalFirstThunk != 0)
{
   // get a string pointer to the DLL to load.
   auto dll_name = reinterpret_cast<char *>(image + import_table->Name);

   // load the DLL with our import.
   auto dll_import = LoadLibraryA(dll_name);

   if (dll_import == nullptr) {
      std::cerr << "Error: failed to load DLL from import table: " << dll_name << std::endl;
      ExitProcess(4);
   }

   // load the array which contains our import entries
   auto lookup_table = reinterpret_cast<IMAGE_THUNK_DATA64 *>(image + import_table->OriginalFirstThunk);

   // load the array which will contain our resolved imports
   auto address_table = reinterpret_cast<IMAGE_THUNK_DATA64 *>(image + import_table->FirstThunk);

   // an import can be one of two things: an "import by name," or an "import ordinal," which is
   // an index into the export table of a given DLL.
   while (lookup_table->u1.AddressOfData != 0)
   {
      FARPROC function = nullptr;
      auto lookup_address = lookup_table->u1.AddressOfData;

      // if the top-most bit is set, this is a function ordinal.
      // otherwise, it's an import by name.
      if (lookup_address & IMAGE_ORDINAL_FLAG64 != 0)
      {
         // get the function ordinal by masking the lower 32-bits of the lookup address.
         function = GetProcAddress(dll_import,
                                   reinterpret_cast<LPSTR>(lookup_address & 0xFFFFFFFF));

         if (function == nullptr) {
            std::cerr << "Error: failed ordinal lookup for " << dll_name << ": " << (lookup_address & 0xFFFFFFFF) << std::endl;
            ExitProcess(5);
         }
      }
      else {
         // in an import by name, the lookup address is an offset to
         // an IMAGE_IMPORT_BY_NAME structure, which contains our function name
         // to import
         auto import_name = reinterpret_cast<IMAGE_IMPORT_BY_NAME *>(image + lookup_address);
         function = GetProcAddress(dll_import, import_name->Name);

         if (function == nullptr) {
            std::cerr << "Error: failed named lookup: " << dll_name << "!" << import_name->Name << std::endl;
            ExitProcess(6);
         }
      }

      // store either the ordinal function or named function
      // in our address table.
      address_table->u1.Function = reinterpret_cast<std::uint64_t>(function);

      // advance to the next entries in the address table and lookup table
      ++lookup_table;
      ++address_table;
   }

   // advance to the next entry in our import table
   ++import_table;
}

Com as importações resolvidas, podemos agora seguir para a parte final: tratar do diretório de realocação!

Resolvendo endereços

O próximo diretório de dados a ser enfrentado é o chamado diretório de realocação. Esse diretório é responsável por traduzir endereços absolutos dentro do código para seus novos valores de base. Esse processo essencialmente implementa algo que você pode conhecer, chamado randomização do layout do espaço de endereços, mas também é responsável por garantir que os espaços de endereço de DLLs não colidam entre si.

Primeiro, precisamos garantir que nosso binário seja realmente capaz de mover bases de endereços. Às vezes, especialmente em binários mais antigos, isso não está habilitado. Dentro das características de um determinado executável Windows estão suas características, confusamente chamadas de DllCharacteristics no cabeçalho opcional. Estamos interessados na característica "base dinâmica". Existe uma maneira especial de desempacotar binários sem ASLR, que não abordaremos, então é um erro se nosso binário não oferecer suporte a isso. (Isso é melhor como um erro no seu empacotador, não no seu stub, mas para facilitar o fluxo do aprendizado sobre o cabeçalho PE, foi colocado aqui em vez disso.)```cpp // first, check if we can even relocate the image. if the dynamic base flag isn't set, // then this image probably isn't prepared for relocating. auto nt_header = get_nt_headers(image);

if (nt_header->OptionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE == 0) { std::cerr << "Error: image cannot be relocated." << std::endl; ExitProcess(7); }

// once we know we can relocate the image, make sure a relocation directory is present auto directory_entry = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

if (directory_entry.VirtualAddress == 0) { std::cerr << "Error: image can be relocated, but contains no relocation directory." << std::endl; ExitProcess(8); }

root@kitploit:~
Em seguida, precisamos calcular o *delta de endereço*. Isso é simplesmente a diferença entre a variável `ImageBase` da imagem e o endereço base da imagem virtual. É usado para ajustar rapidamente os valores de endereços fixos no binário.```cpp
// calculate the difference between the image base in the compiled image
// and the current virtually allocated image. this will be added to our
// relocations later.
std::uintptr_t delta = reinterpret_cast<std::uintptr_t>(image) - nt_header->OptionalHeader.ImageBase;

Agora estamos prontos para enfrentar a tabela de realocação.```c typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION;

root@kitploit:~
Observe o array `TypeOffset` comentado, na verdade ele é relevante aqui! Uma tabela de realocação consiste em blocos de offsets contendo endereços a ajustar, identificados pelo RVA `VirtualAddress`. O array `TypeOffset` contém valores de palavras codificados que contêm o tipo de realocação bem como o offset a partir de `VirtualAddress` a ajustar. No que diz respeito ao tipo de realocação, para binários de 64 bits, estamos preocupados apenas com um tipo de realocação. Infelizmente, essa estrutura não é realmente uma struct variádica, então temos que fazer alguma aritmética de ponteiros para obter o array `TypeOffset`.

Como mencionado, o array `TypeOffset` contém palavras codificadas. Os 4 bits superiores (máscara `0xF000`) contêm o tipo de realocação, que pode ser encontrado na [seção "base relocation types"](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) da documentação do formato PE. Os 12 bits inferiores (máscara `0x0FFF`) contêm o offset a partir do argumento `VirtualAddress` a ajustar.

Explicar isso é uma tarefa árdua e acaba parecendo muito confuso no final. Obter um ponteiro para um endereço a realocar parece com isto:```cpp
auto ptr = reinterpret_cast<std::uintptr_t *>(image + relocation_table->VirtualAddress + offset);

E ajustar esse endereço fica assim:```cpp *ptr += delta;

root@kitploit:~
Por mais complicado que seja explicar o diretório de realocação, as operações são, na verdade, muito simples de entender no código.

Avançar para o próximo bloco de dados de realocação é facilitado pela variável `SizeOfBlock`. Este bloco contém o tamanho do nosso cabeçalho *assim como* o tamanho do array `TypeOffset`. Se `TypeOffset` fosse, em vez disso, um array de tamanho estático, simplesmente avançaríamos para a próxima entrada de realocação adicionando a chamada a `sizeof` do cabeçalho de realocação.

Com tudo isso explicado, você deverá ser capaz de entender este código de realocação:```cpp
// get the relocation table.
auto relocation_table = reinterpret_cast<IMAGE_BASE_RELOCATION *>(image + directory_entry.VirtualAddress);

// when the virtual address for our relocation header is null,
// we've reached the end of the relocation table.
while (relocation_table->VirtualAddress != 0)
{
   // since the SizeOfBlock value also contains the size of the relocation table header,
   // we can calculate the size of the relocation array by subtracting the size of
   // the header from the SizeOfBlock value and dividing it by its base type: a 16-bit integer.
   std::size_t relocations = (relocation_table->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(std::uint16_t);

   // additionally, the relocation array for this table entry is directly after
   // the relocation header
   auto relocation_data = reinterpret_cast<std::uint16_t *>(&relocation_table[1]);

   for (std::size_t i=0; i<relocations; ++i)
   {
      // a relocation is an encoded 16-bit value:
      //   * the upper 4 bits are its relocation type
      //     (https://learn.microsoft.com/en-us/windows/win32/debug/pe-format see "base relocation types")
      //   * the lower 12 bits contain the offset into the relocation entry's address base into the image
      //
      auto relocation = relocation_data[i];
      std::uint16_t type = relocation >> 12;
      std::uint16_t offset = relocation & 0xFFF;
      auto ptr = reinterpret_cast<std::uintptr_t *>(image + relocation_table->VirtualAddress + offset);

      // there are typically only two types of relocations for a 64-bit binary:
      //   * IMAGE_REL_BASED_DIR64: a 64-bit delta calculation
      //   * IMAGE_REL_BASED_ABSOLUTE: a no-op
      //
      if (type == IMAGE_REL_BASED_DIR64)
         *ptr += delta;
   }

   // the next relocation entry is at SizeOfBlock bytes after the current entry
   relocation_table = reinterpret_cast<IMAGE_BASE_RELOCATION *>(
      reinterpret_cast<std::uint8_t *>(relocation_table) + relocation_table->SizeOfBlock
   );
}

Parabéns! Nossa imagem agora está pronta para execução! Já realizamos muita coisa até aqui:

  • desempacotamos nosso binário da nossa tabela de seções em tempo de execução
  • mapeamos nosso binário em uma região de memória executável
  • resolvemos as importações necessárias para nosso binário executar
  • realocamos os endereços na imagem para apontar para nossa nova base de imagem

Agora estamos prontos para executar nosso binário desempacotado!

Transferindo a execução

Vamos voltar ao nosso loop principal agora:```cpp int main(int argc, char *argv[]) { // first, decompress the image from our added section auto image = get_image();

// next, prepare the image to be a virtual image
auto loaded_image = load_image(image);

// resolve the imports from the executable load_imports(loaded_image);

// relocate the executable relocate(loaded_image);

// get the headers from our loaded image auto nt_headers = get_nt_headers(loaded_image);

// acquire and call the entrypoint auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint; reinterpret_cast<void(*)()>(entrypoint)();

return 0; }

root@kitploit:~
Como você pode ver, transferir a execução é muito simples, embora exija conhecimento de [ponteiros de função](https://en.wikipedia.org/wiki/Function_pointer). Estas são as nossas partes relevantes:```cpp
// acquire and call the entrypoint
auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint;
reinterpret_cast<void(*)()>(entrypoint)();

AddressOfEntryPoint é, como você pode adivinhar, um RVA no ponto de entrada de código da nossa imagem carregada. Apesar do que você possa saber sobre entrypoints main, o entrypoint bruto de um binário específico não é tipado— seu compilador C++ é o principal responsável por configurar o ambiente de código para alimentar argumentos às suas funções main esperadas, sejam elas main ou WinMain.

Fundamentalmente, é assim que um ponteiro de função é declarado:```c return_type (*variable_name)(int arg1, int arg2, ...)

root@kitploit:~
Portanto, nosso ponteiro de função para chamar nosso ponto de entrada-- sem ter tipo associado a ele-- fica assim:```c
void (*entrypoint)()

Como um cast, isso se reduz a:```c void(*)()

root@kitploit:~
Juntando tudo, podemos simplesmente converter nosso entrypoint em um ponteiro de função e chamá-lo na mesma linha, assim:```cpp
reinterpret_cast<void(*)()>(entrypoint)();

Com tudo carregado corretamente, você deverá ver o seu programa empacotado em execução. No nosso caso, como empacotamos nosso executável fictício, ele simplesmente exibe uma mensagem:``` $ ./packed.exe I'm just a little guy!

root@kitploit:~
Parabéns! Você terminou! Você acabou de escrever um packer para Windows!

## Exercícios adicionais

* **Ataca o analista**: Aprenda a implementar [algumas técnicas de anti-debug](https://anti-reversing.com/Downloads/Anti-Reversing/The_Ultimate_Anti-Reversing_Reference.pdf) e endureça o seu packer.
* **Expanda o seu suporte**: Aprenda a implementar um binário não realocável, ou aprenda a implementar outros diretórios, como o diretório de armazenamento local de thread (`IMAGE_TLS_DIRECTORY`) e o diretório de recursos (`IMAGE_RESOURCE_DIRECTORY`). Como a documentação é escassa neste nível, você pode ver as minhas implementações em [exe-rs](https://github.com/exe-rs).
* **Ofusque o seu stub**: Tente descobrir como o analista vai desempacotar o seu binário e dificulte o desempacotamento, como por exemplo [apagando os cabeçalhos](https://github.com/frank2/packer-tutorial/blob/main/stub/src/main.cpp#L84).
Baixar ferramenta