
Fuzzer de kernel do Windows baseado em snapshots e guiado por cobertura
Rewind é um fuzzer orientado a cobertura baseado em snapshots que tem como alvo componentes do kernel do Windows.
A ideia é começar a partir de um snapshot de um sistema vivo em execução. Esse snapshot é composto pelas páginas de memória física juntamente com o estado da CPU.
Esse estado é usado para configurar o estado inicial de uma CPU virtual. Ao aproveitar a paginação sob demanda, apenas as páginas necessárias para a execução da função alvo são lidas do snapshot.
Como usamos uma máquina virtual dedicada com apenas as páginas de memória física úteis para a execução da função alvo, restaurar um snapshot é rápido.
Atualmente, 2 backends estão disponíveis:
WHVP usa a API WHVP (Windows Hypervisor Platform) para
fornecer acesso a uma partição do Hyper-V. Consulte
https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform
para mais detalhes.Bochs usa o emulador Bochs
(https://bochs.sourceforge.io/)Um backend KVM está sendo desenvolvido e deve estar disponível em breve.
Rewind fornece 2 recursos principais:
Ele também fornece um TUI básico (Interface de Usuário de Terminal) para reportar informações úteis sobre o fuzzing.
Foi testado no Windows e Linux (somente backend bochs para Linux por enquanto).
Sempre gostei de fazer pesquisa de vulnerabilidades em kernel, especialmente no kernel do Windows. O processo sempre envolve uma mistura de análise estática e dinâmica. Fazer análise dinâmica pode rapidamente se tornar tedioso. O ciclo debug / crash / reboot / reset de todos os breakpoints é lento e doloroso. Quando você quer fazer algum fuzzing, muitas vezes é necessário configurar uma ou várias máquinas virtuais, além de um depurador de kernel, e criar scripts gambiarras para lidar com a detecção de crashes...
Tirar snapshots com máquinas virtuais ajuda, mas é lento.
Durante 2018, a Microsoft introduziu um novo conjunto de APIs chamado Windows Hypervisor Platform (WHVP). Essas APIs permitem configurar uma partição (VM na linguagem do Hyper-V) com alguns processadores virtuais e ter controle sobre as saídas de VM (VM exits) que ocorrem na máquina virtual. É quase como ter seu próprio tratador de VM-exit no userland. Bastante útil para fazer coisas interessantes, por exemplo Simpleator ou applepie.
Então comecei a brincar com o WHVP e fiz um primeiro PoC que me permitia executar shellcode em uma partição do Hyper-V. Foi escrito em Python e era bastante lento. Esse primeiro PoC evoluiu rapidamente para algum tipo de tracer baseado em snapshot. Eu queria ter algo para inicializar a CPU virtual e que fosse bem fácil de configurar. Então, como eu já usava um depurador de kernel para brincar com meu alvo, decidi usar dumps de kernel feitos com WinDbg como meu snapshot. Com isso, eu só precisava configurar uma partição com uma CPU virtual. O contexto da CPU virtual é definido com o contexto retirado do dump. Sempre que a CPU virtual precisa de uma página física, eu uso as páginas do dump.
Com isso, eu conseguia fazer fork do estado do dump em uma partição e então retomar a execução. Isso me permitiu rastrear facilmente a execução da minha função alvo. Ao modificar os argumentos e reverter o estado da memória da partição, também era muito fácil fazer fuzzing no alvo.
Este trabalho foi apresentado na conferência SSTIC em 2020 e publicado no github.
A ferramenta implementa 2 possibilidades para obter a cobertura. A primeira usa a clássica TF (Trap Flag) para ter interrupções INT1 em cada instrução. Requer modificar o alvo e é lenta. Eu teria preferido usar a MONITOR trap flag. Mas o WHVP não oferece essa possibilidade.
Para ter um desempenho adequado (necessário para fuzzing), decidi reduzir a precisão da cobertura e adicionar um modo em que você só sabe quando uma instrução é executada pela primeira vez.
Para fazer isso, eu aplico patch nas páginas buscadas do snapshot com bytes 0xcc (apenas para páginas executáveis). Quando a CPU executar essas instruções modificadas, o hipervisor capturará a exceção e reescreverá as instruções com o código original.
É como ter um breakpoint de software exclusivo definido em cada instrução. Funciona 95% das vezes, mas em trechos específicos de código (como aqueles com jump tables, por exemplo) falhará, porque os dados serão substituídos.
Para superar isso, uma opção seria desassemblar o código antes de mapeá-lo e aplicar patch apenas no que for necessário (talvez na próxima vez).
Durante meus experimentos, encontrei várias limitações ao usar o WHVP. É lento, muito lento. O código-fonte do VirtualBox tem alguns comentários interessantes :)
Portanto, para ter um desempenho adequado, você realmente precisa limitar as saídas de VM, e isso é incompatível se você quiser usar o Hyper-V como um hipervisor de rastreamento (já que ele exige muitas saídas de VM).
Durante o mesmo período, comecei a usar o bochs (especialmente a parte de instrumentação) para verificar se os rastreamentos obtidos pela ferramenta estavam corretos. Bochs era uma espécie de oráculo para ver se eu tinha rastreamentos divergentes.
O Bochs é mais rápido que o WHVP ao fazer rastreamento completo, e você também tem os benefícios de ter acessos à memória e outras coisas úteis.
Decidi adicionar o bochs como outro backend. whvp não era mais um nome adequado e optei por rewind.

O rewind foi projetado em torno do meu próprio fluxo de trabalho quando estou conduzindo
avaliações de segurança para drivers de kernel na plataforma Windows.
O primeiro passo é instalar o software alvo dentro de uma máquina virtual. Como uso uma mistura de análise estática e dinâmica, também configurarei um depurador de kernel.
Depois de abrir alguns drivers aleatórios no IDA, começarei rapidamente a
mirar algumas funções. Para fazer isso, normalmente coloco alguns breakpoints com
windbg e, combinado com ret-sync,
posso começar a brincar.
É aí que o rewind entra em ação. Em vez de editar buffers aleatórios
na memória, executar passo a passo e anotar o IDB para ter uma ideia
aproximada do que está acontecendo, tirarei um snapshot com windbg e usarei
o rewind em vez disso.
Isso facilitará muito o processo. Ter um snapshot oferece muitas vantagens. Tudo é determinístico. Você pode repetir ad nauseam uma chamada de função. Você pode lançar um fuzzer se a função alvo parecer interessante. Você pode até fechar a VM, já que ela não é mais necessária.
Obviamente você precisa do Rust (instalação testada no Windows e Linux com Rust 1.50). O CMake também é necessário para algumas dependências.
Primeiro clone o repositório:
$ git clone [email protected]:quarkslab/rewind.git
Continue com a instalação do backend bochs
Clone o repositório bochscpu (https://github.com/yrp604/bochscpu) no diretório vendor:
$ cd vendor
$ git clone https://github.com/yrp604/bochscpu
Baixe os artefatos bochs pré-compilados do bochscpu-build (https://github.com/yrp604/bochscpu-build)
$ curl.exe -L --output bochs-x64-win.zip [artifact_url]
Extraia as pastas lib e bochs para o checkout do bochscpu.