
Uma implementação de PoC para uma técnica de evasão que encerra a thread atual e a restaura antes de retomar a execução, enquanto implementa alterações de proteção de página durante a não execução.
██████╗ ███████╗ █████╗ ████████╗██╗ ██╗███████╗██╗ ███████╗███████╗██████╗
██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██║ ██╔════╝██╔════╝██╔══██╗
██║ ██║█████╗ ███████║ ██║ ███████║███████╗██║ █████╗ █████╗ ██████╔╝
██║ ██║██╔══╝ ██╔══██║ ██║ ██╔══██║╚════██║██║ ██╔══╝ ██╔══╝ ██╔═══╝
██████╔╝███████╗██║ ██║ ██║ ██║ ██║███████║███████╗███████╗███████╗██║
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝╚═╝
Uma implementação PoC para uma técnica de evasão que termina a thread atual e a restaura antes de retomar a execução, enquanto implementa alterações de proteção de página durante a não execução.

Métodos de suspensão e ofuscação são bem conhecidos na comunidade de maldev, com diferentes implementações. Eles têm o objetivo de se esconder de scanners de memória durante a suspensão, geralmente alterando proteções de página e até adicionando recursos interessantes como criptografar o shellcode, mas há outro ponto importante para esconder nosso shellcode: esconder a thread de execução atual. Spoofing da pilha é legal, mas depois de pensar um pouco sobre isso, pensei que não há necessidade de fazer spoofing da pilha... se não houver pilha :)
A usabilidade desta técnica fica a critério do leitor, mas de qualquer forma, acho que é uma maneira legal de revisar alguns tópicos e aprender um pouco de maldev para quem, como eu, está começando neste mundo.
A implementação principal mostrada aqui contém tudo que precisamos retirar da pilha na seção de dados, como variáveis globais, mas uma implementação movendo tudo para o heap será publicada em breve. Ela visa mostrar algumas modificações-chave que precisam ser feitas para tornar este código pic e injetável.
Este repositório espelhado entre GitHub e GitLab.
Tudo o que foi declarado aqui vem do meu entendimento dos diferentes tópicos abordados, seja por leitura ou por experiência durante o desenvolvimento. Estou ciente de que não sou um especialista e a última coisa que quero é espalhar desinformação, então se você acha que algo não está correto, adoraria que você me avisasse, pode me contatar no twitter ou abrir issues neste repositório. Muito obrigado pela sua compreensão. :)
O objetivo principal desta técnica é claro: encerrar a thread atual e restaurá-la antes de retomar a execução, mas o que exatamente isso significa e quais novas restrições impõe?
Para ser capaz de restaurar a execução, precisamos salvar duas coisas antes de encerrar a thread: primeiro, o estado da CPU, e segundo, a pilha, e efetivamente configurá-los novamente depois que a nova thread for lançada.
Eu falei sobre novas restrições que surgirão nesta técnica, e há duas grandes: Primeiro, precisamos armazenar fora da pilha qualquer coisa necessária desde o momento em que a thread termina até que a pilha seja restaurada, e como você verá, isso cria alguns novos desafios.
Segundo, sempre precisamos de pelo menos outra thread em execução em nosso processo, já que estamos encerrando nossa thread; se não houver outras threads, o processo terminará. Não acho que isso seja um grande problema, já que a maioria dos agentes é injetada em outros processos, podemos assumir que esse processo manterá pelo menos uma thread em execução.
Podemos ver neste PoC 4 funções principais:
Quando estamos prestes a salvar a pilha, surge uma pergunta: quanto da pilha precisa ser salvo?
Vamos revisar primeiro o que está na pilha depois que chamamos a função DeathSleep (esta é a função que salva o contexto, a pilha e prepara tudo para a ofuscação e restauração)

Como podemos ver, cada função tem três partes:
A porção mínima da pilha que obviamente precisamos salvar é tudo dentro do nosso programa principal, ou seja, seu espaço de sombra, seu endereço de retorno e tudo até a função DeathSleep. Qualquer coisa antes disso não é realmente necessária (salvar a pilha usada pela função de entrada tem suas vantagens, mas discutiremos isso depois), pois essa é a pilha usada pelas rotinas do Windows para lançar nossa nova thread. Além disso, decidi também armazenar o espaço de sombra da função DeathSleep (não realmente necessário, mas facilita o cálculo do Rsp no momento do despertar).
Então, no final, estamos salvando isto:

Cada função em uma compilação padrão deve ser composta de 3 partes: prólogo, código da função e epílogo.
O Rsp (ponteiro de pilha) deve ser modificado apenas no prólogo e epílogo da função. O prólogo aumenta o ponteiro de pilha (lembre-se, aumentar a pilha significa reduzir endereços, já que eles vão em direções opostas), para salvar registradores, armazenar todas as suas variáveis locais e depois armazenar o espaço de sombra, e o epílogo faz exatamente o oposto.
Isso significa que o ponteiro de pilha dentro do código da função deve sempre apontar para o final do espaço de sombra (roxo na imagem acima), e a soma do tamanho da pilha da função e do espaço de sombra pode ser encontrada calculando quanto o prólogo aumenta o ponteiro de pilha. Esse valor pode ser facilmente calculado usando as informações contidas nas tabelas de desenrolamento; uma explicação sobre seu uso não será abordada aqui, mas, em resumo, essas tabelas são usadas para permitir que qualquer outra thread ou processo mova-se corretamente pela pilha para ver seu conteúdo, tratar exceções ou analisá-la.
Capturar o contexto é provavelmente uma das coisas mais fáceis de fazer, pois podemos simplesmente usar RtlCaptureContext() na primeira linha do DeathSleep, antes que qualquer modificação seja feita nos registradores não voláteis. Ainda precisamos fazer duas modificações no contexto onde iremos restaurar a execução.