Uma série de programas cada vez mais complexos que demonstram a conexão de funções no Windows de 64 bits.
Eu escrevi esses programas enquanto aprendia como funciona a conexão de funções. Eles podem ser úteis para outras pessoas que estão tentando fazer a mesma coisa (ou para mim no futuro, depois de esquecer como tudo isso funciona novamente). Eles devem ser examinados em ordem. Na primeira vez que uma função for usada em um programa, ela será incluída no arquivo .cpp desse programa de exemplo específico, com seu nome prefixado por um sublinhado. Quaisquer exemplos subsequentes que usem a mesma função usarão a cópia dessa função incluída em hooking_common.h, para minimizar a duplicação de código e manter os programas de exemplo posteriores pequenos o suficiente para serem facilmente legíveis.
Trabalhei um pouco para limpá-los, mas os exemplos posteriores ainda estão um pouco confusos. É importante observar que o resultado final deste repositório não é suficiente para escrever uma biblioteca de hooking com todos os recursos, mas é o suficiente para iniciar esse caminho.
Em tempo de execução, alguns projetos podem depender de outros projetos sendo construídos (injetar uma dll requer que a dll tenha sido construída) ou em execução ao mesmo tempo (conectar um programa de destino requer que ele já esteja em execução).
Todos os exemplos foram criados usando Visual Studio 2019 (v142) com Windows SDK 10.0.17763.0. Não acho que haja nada aqui que dependa da versão do VS ou do SDK, mas estou listando aqui apenas para garantir. É quase certo que existem algumas coisas específicas do MSVC.
Finalmente, o último exemplo do trampolim instala um gancho no mspaint. Presumo que em algum momento no futuro, uma atualização do mspaint fará com que este exemplo seja quebrado. No momento em que este artigo foi escrito, a versão atual do mspaint era 1909 (OS Build 18363.1016).
Os exemplos estão divididos em duas categorias: os que usam trampolins e os que não usam. Os exemplos não-trampolim existem apenas para demonstrar o redirecionamento do fluxo do programa de uma função para outra em diferentes situações. Construir trampolins é complicado, e quando eu estava tentando descobrir como funcionava o engate de funções, foi imensamente útil começar construindo primeiro os exemplos que não são trampolins. Além disso, existem 4 "programas alvo" que são usados por exemplos que desejam demonstrar como instalar ganchos em diferentes processos (já em execução).
A maioria desses exemplos vaza memória relacionada aos ganchos. Eu realmente não me importo, porque esses exemplos são apenas para demonstrar um conceito de conexão e porque essas alocações "vazadas" precisam existir até o encerramento do programa de qualquer maneira.
Embora não pareça haver muita terminologia padrão para técnicas de conexão de funções, o código (e leia-mes) neste repositório usa os seguintes termos:
Como esses exemplos não criam trampolins ao instalar seus ganchos, penso nessas funções como uma demonstração de engate "destrutivo", no sentido de que a função original fica completamente inutilizável após ser enganchada.
Um pequeno exemplo de substituição dos bytes iniciais de uma função com uma instrução de salto que redireciona o fluxo do programa para uma função diferente dentro do mesmo programa. Como não há nenhum trampolim sendo construído, esta operação é destrutiva e a função original não pode mais ser chamada. Este é o único exemplo de 32 bits no repositório.
A versão de 64 bits do exemplo anterior. Em aplicações de 64 bits, as funções podem estar localizadas longe o suficiente na memória para não serem acessíveis por meio de uma instrução de salto relativo de 32 bits. Como não há instrução de salto relativo de 64 bits, este programa primeiro cria uma função de "relé", que contém bytes para uma instrução jmp absoluta que pode alcançar qualquer lugar na memória (e salta para a função de carga útil). O salto de 32 bits instalado na função de destino salta para esta função de relé, em vez de imediatamente para a carga útil.
Fornece um exemplo de uso das técnicas do projeto anterior para conectar uma função membro, em vez de uma função livre.
Um pouco diferente dos exemplos anteriores, este programa mostra como instalar um gancho em uma função de membro virtual obtendo o endereço dessa função por meio da vtable de um objeto. Nenhum outro exemplo trata de funções virtuais, mas achei interessante o suficiente para incluir aqui.
O exemplo mais simples de instalação de um gancho em outro processo em execução. Este exemplo usa a biblioteca DbgHelp para localizar uma função em um processo de destino (A - Target With Free Function) por nome de string. Isso só é possível porque o programa alvo é construído com símbolos de depuração habilitados. Embora simples, este exemplo é um pouco mais longo que os programas anteriores devido ao grande número de novas funções que introduz (para localizar e manipular um processo remoto).
Este exemplo mostra como conectar uma função que outro processo importou de uma dll. Há algumas nuances sobre como obter o endereço de uma função dll em um processo remoto devido ao modo como o ASLR funciona, o que é demonstrado aqui. Caso contrário, este exemplo é quase idêntico ao anterior.
Este exemplo mostra como instalar um gancho em uma função que não é importada por uma dll e que não está na tabela de símbolos (provavelmente porque o processo remoto não possui símbolos de depuração). Isso significa que não há uma maneira (fácil) de encontrar a função de destino pelo nome da string. Em vez disso, este exemplo pressupõe que você usou um desmontador como x64dbg para obter o endereço virtual relativo (RVA) da função que deseja conectar. Este programa usa esse RVA para instalar um gancho.
Semelhante ao acima, exceto que este exemplo usa injeção de dll para instalar a função de carga útil em vez de escrever bytes de código de máquina brutos. Isso é muito mais fácil de trabalhar, já que suas cargas podem ser escritas em C++ novamente. A carga útil deste exemplo está contida no projeto 08B-DLL-Payload.
Os exemplos a seguir instalam trampolins ao conectar, o que significa que o programa ainda pode executar a lógica na função de destino após a instalação de um gancho. Como a instalação de um gancho substitui pelo menos os primeiros 5 bytes da função de destino, as instruções contidas nesses 5 bytes são movidas para a função trampolim. Assim, chamar a função trampolim executa efetivamente a lógica original da função alvo.
O equivalente de instalação de trampolim do exemplo nº 2. Este exemplo é um pouco estranho porque eu queria demonstrar a criação de um trampolim sem a necessidade de usar um motor de desmontagem. Neste caso, a função alvo foi criada para ter uma instrução conhecida de 5 bytes no início, então podemos apenas copiar os primeiros cinco bytes dessa função para a função trampolim. Isso significa que criar o trampolim é muito fácil, pois sabemos que seu tamanho exato e que não utiliza nenhum endereçamento relativo que precise ser corrigido. Se você estivesse escrevendo um trampolim para um caso de uso realmente específico, provavelmente conseguiria apenas fazer uma variação disso.
Este exemplo mostra um cenário semelhante ao anterior, exceto que desta vez estou usando um desmontador (capstone) para obter os bytes que precisamos roubar da função de destino. Isso permite que o código de gancho seja usado em qualquer função, não apenas naquelas que sabemos que serão casos fáceis. Na verdade, há muita coisa acontecendo neste exemplo, porque ele salta de um gancho direcionado (como o anterior) para a construção de uma função de gancho genérica. O trampolim precisa converter chamadas/saltos relativos em instruções que usam endereços absolutos, o que complica ainda mais as coisas. Este também não é um exemplo 100% polido de conexão genérica, ele falhará com instruções de loop e se você tentar conectar funções com menos de 5 bytes de instruções.
Basicamente igual ao anterior, exceto que este exemplo inclui código para pausar todos os threads em execução enquanto instala um gancho. Não é garantido que isso seja seguro em todos os casos, mas é definitivamente muito mais seguro do que não fazer nada.
Isso expande o código de gancho/trampolim usado nos dois exemplos anteriores para suportar o redirecionamento de múltiplas funções para a mesma carga útil e para permitir que funções de carga útil chamem outras funções com ganchos instalados nelas.
Este é o primeiro exemplo de trampolim que instala um gancho em um processo diferente (neste caso, o aplicativo de destino B - Target com funções livres de DLL). Toda a lógica de hooking está contida em uma carga útil de dll 13B - Trampoline Imported Func DLL Payload. Não há muita novidade aqui, este exemplo apenas combina o trabalho de conexão de trampolim já feito com as técnicas mostradas anteriormente para conectar uma função importada de uma dll.
A joia da coroa do repo. Este exemplo injeta uma carga útil de dll (14B - Trampoline Hook MSPaint Payload) em uma instância em execução do mspaint (você deve iniciar o mspaint antes de executá-lo). O gancho instalado faz com que os pincéis sejam desenhados em vermelho, independentemente da cor que você realmente selecionou no MSPaint. Honestamente, não há nada aqui que não tenha sido mostrado no exemplo anterior, é muito legal ver isso funcionando em um programa não planejado.
Aplicativo de destino simples que chama uma função livre em um loop. Compilado com informações de depuração incluídas.
Aplicativo de destino que chama uma função livre que foi importada de uma dll (B2 - GetNum-DLL) em loop.
Aplicativo de destino que chama uma função de membro não virtual em um loop.
Aplicativo de destino que chama uma função de membro virtual em um loop.