Você já quis olhar sob o capô do sistema operacional, ver a estrutura interna de seus mecanismos, torcer os parafusos e ver as oportunidades que se abriram? Talvez eles até quisessem trabalhar diretamente com o hardware, mas pensavam que os drivers eram ciência do foguete?
Proponho caminhar ao longo da ponte até o núcleo e ver quão profunda é a toca do coelho.
Portanto, apresento a estrutura de driver para hackers do kernel, escrita em C ++ 17, e projetada, se possível, para remover barreiras entre o kernel e o modo de usuário ou suavizar sua presença o máximo possível. E também, um conjunto de APIs e wrappers de kernel e modo de usuário para desenvolvimento rápido e conveniente no Ring0 para programadores iniciantes e avançados.
Principais recursos:
- Acesso a portas de E / S, bem como instruções de entrada , saída , cli e sti para o modo de usuário via IOPL
- Envolve o tweeter de sistema
- Acessar MSR (Registros Específicos do Modelo)
- Um conjunto de funções para acessar a memória no modo de usuário de outros processos e a memória do kernel
- Trabalhar com memória física, DMI / SMBIOS
- Criação de modo de usuário e fluxos nucleares, entrega da APC
- Retornos de chamada Ob *** e Ps *** no modo de usuário e filtros do sistema de arquivos
- Baixe drivers não assinados e bibliotecas de kernel
... e muito mais.
E começaremos carregando e conectando a estrutura ao nosso projeto C ++.
GithubPara montagem, é altamente recomendável usar a versão mais recente do Visual Studio e o WDK (Windows Driver Kit) disponível mais recente, que pode ser baixado no
site oficial da Microsoft .
Para teste, o VMware Player gratuito com Windows instalado, não inferior ao Windows 7, de qualquer capacidade, é perfeito.
A montagem é trivial e não fará perguntas:
- Abra o Kernel-Bridge.sln
- Escolha a profundidade de bits necessária
- Ctrl + Shift + B
Como resultado, obtemos um driver, uma biblioteca no modo de usuário e arquivos de utilitários relacionados (
* .inf para instalação manual,
* .cab para assinar o driver no Microsoft Hardware Certification Publisher, etc.).
Para instalar o driver (se não for necessária uma assinatura digital para x64 - o certificado EV correspondente), é necessário colocar o sistema no modo de teste, ignorando a assinatura digital dos drivers. Para fazer isso, execute a linha de comando como administrador:
bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe /set TESTSIGNING ON
... e reinicie a máquina. Se tudo for feito corretamente, uma inscrição aparecerá no canto inferior direito do Windows em modo de teste.
A configuração do ambiente de teste foi concluída, vamos começar a usar a API em nosso projeto.
A estrutura possui a seguinte hierarquia:
/ Kernel-Bridge / API - um conjunto de funções para uso em drivers e módulos do
kernel , não possui dependências externas e pode ser usado livremente em projetos de terceiros
/ User-Bridge / API - um conjunto de invólucros no modo de usuário sobre as funções de driver e utilitário para trabalhar com arquivos PE, caracteres PDB, etc.
/ SharedTypes / - cabeçalhos nucleares e de modo de usuário que contêm os tipos comuns necessários
O driver pode ser carregado de duas maneiras: como um driver comum e como um minifiltro. O segundo método é preferido, porque dá acesso à funcionalidade avançada de filtros e retornos de chamada no modo de usuário para eventos do sistema.
Portanto, crie um projeto de console em C ++, conecte os arquivos de cabeçalho necessários e carregue o driver:
#include <Windows.h> #include "WdkTypes.h" // x32/x64 WDK #include "CtlTypes.h" // IOCTL- #include "User-Bridge.h" // API, int main() { using namespace KbLoader; BOOL Status = KbLoadAsFilter( L"X:\\Folder\\Path\\To\\Kernel-Bridge.sys", L"260000" // ); if (!Status) return 0; // ! // API ... // : KbUnload(); return 0; }
Ótimo! Agora podemos usar a API e interagir com o kernel.
Vamos começar com a funcionalidade mais popular no ambiente de desenvolvedores de truques - lendo e gravando a memória de outro processo:
using namespace Processes::MemoryManagement; constexpr int Size = 64; BYTE Buffer[Size] = {}; BOOL Status = KbReadProcessMemory(
Nada complicado! Vamos descer um nível - lendo e escrevendo memória nuclear:
using namespace VirtualMemory; constexpr int Size = 64; BYTE Buffer[Size];
E as funções para interagir com o ferro? Por exemplo, portas de E / S.
Nós os encaminharemos para o modo de usuário, colocando 2 bits IOPL no registro EFlags, responsáveis pelo nível de privilégio no qual as instruções
in /
out /
cli /
sti estão disponíveis.
Assim, poderemos executá-los no modo de usuário sem o erro de Privileged Instruction:
#include <intrin.h> using namespace IO::Iopl; // , ! KbRaiseIopl(); // in/out/cli/sti ! ULONG Frequency = 1000; // 1 kHz ULONG Divider = 1193182 / Frequency; __outbyte(0x43, 0xB6); // // : __outbyte(0x42, static_cast<unsigned char>(Divider)); __outbyte(0x42, static_cast<unsigned char>(Divider >> 8)); __outbyte(0x61, __inbyte(0x61) | 3); // ( ) for (int i = 0; i < 5000; i++); // Sleep(), IOPL ! __outbyte(0x61, __inbyte(0x61) & 252); // KbResetIopl();
Mas e a verdadeira liberdade? Afinal, muitas vezes se deseja executar código arbitrário com privilégios de kernel. Escrevemos todo o código do kernel no modo de usuário e transferimos o controle para ele a partir do kernel (o SMEP é desativado automaticamente, antes de chamar o driver salva o contexto da FPU e a própria chamada ocorre dentro de um bloco
try..except ):
using namespace KernelShells;
Mas, além de cuidar de shells, também há uma funcionalidade séria que permite criar DLPs simples com base no subsistema de filtros de arquivo, objeto e processo.
A estrutura permite filtrar
CreateFile /
ReadFile /
WriteFile /
DeviceIoControl , além de eventos de abertura / duplicação de identificadores (
ObRegisterCallbacks ) e eventos de processos / threads de início e carregamento de módulos (
PsSet *** NotifyRoutine ). Isso permitirá, por exemplo, bloquear o acesso a arquivos arbitrários ou substituir informações sobre os números de série do disco rígido.
Princípio de funcionamento:
- O driver registra filtros de arquivos e instala retornos de chamada Ob *** / Ps ***
- O driver abre uma porta de comunicação na qual os clientes que desejam se inscrever em um evento se conectam
- Os aplicativos no modo usuário se conectam à porta e recebem dados do driver sobre o evento que ocorreu, executam a filtragem (truncar identificadores de direitos, bloquear o acesso ao arquivo etc.) e retornar o evento ao kernel
- O driver aplica as alterações recebidas.
Um exemplo de inscrição no
ObRegisterCallbacks e corte de acesso ao processo atual:
#include <Windows.h> #include <fltUser.h> #include "CommPort.h" #include "WdkTypes.h" #include "FltTypes.h" #include "Flt-Bridge.h" ... // ObRegisterCallbacks: CommPortListener<KB_FLT_OB_CALLBACK_INFO, KbObCallbacks> ObCallbacks; // PROCESS_VM_READ: Status = ObCallbacks.Subscribe([]( CommPort& Port, MessagePacket<KB_FLT_OB_CALLBACK_INFO>& Message ) -> VOID { auto Data = static_cast<PKB_FLT_OB_CALLBACK_INFO>(Message.GetData()); if (Data->Target.ProcessId == GetCurrentProcessId()) { Data->CreateResultAccess &= ~PROCESS_VM_READ; Data->DuplicateResultAccess &= ~PROCESS_VM_READ; } ReplyPacket<KB_FLT_OB_CALLBACK_INFO> Reply(Message, ERROR_SUCCESS, *Data); Port.Reply(Reply); // });
Por isso, examinamos brevemente os principais pontos da parte da estrutura no modo de usuário, mas a API principal permaneceu nos bastidores.
Todas as APIs e wrappers estão localizadas na pasta correspondente:
/ Kernel-Bridge / API /Eles incluem trabalhar com memória, com processos, com strings e bloqueios e muito mais, simplificando bastante o desenvolvimento de seus próprios drivers. APIs e wrappers dependem apenas de si mesmos e do ambiente externo: você pode usá-los livremente em seu próprio driver.
Um exemplo de como trabalhar com strings no kernel é uma pedra de tropeço para todos os iniciantes:
#include <wdm.h> #include <ntstrsafe.h> #include <stdarg.h> #include "StringsAPI.h" WideString wString = L"Some string"; AnsiString aString = wString.GetAnsi().GetLowerCase() + " and another string!"; if (aString.Matches("*another*")) DbgPrint("%s\r\n", aString.GetData());
Se você deseja implementar seu próprio manipulador para o seu código IOCTL, você pode fazer isso facilmente, de acordo com o seguinte esquema:
- Escreva um manipulador em /Kernel-Bridge/Kernel-Bridge/IOCTLHandlers.cpp
- No mesmo arquivo, adicione um manipulador ao final da matriz Handlers na função DispatchIOCTL
- Adicione o índice de consulta à enumeração Ctls :: KbCtlIndices em CtlTypes.h na mesma posição, como na matriz Handlers no item 2
- Chame seu manipulador do modo de usuário escrevendo um wrapper no User-Bridge.cpp , fazendo uma chamada usando a função KbSendRequest
Todos os três tipos de E / S são suportados (METHOD_BUFFERED, METHOD_NEITHER e METHOD_IN_DIRECT / METHOD_OUT_DIRECT), por padrão, METHOD_NEITHER é usado.
Isso é tudo! O artigo cobre apenas uma pequena fração de todas as possibilidades. Espero que a estrutura seja útil para desenvolvedores iniciantes de componentes do kernel, engenheiros reversos, desenvolvedores de truques, anti-truques e proteções.
E também, todos estão convidados a participar do desenvolvimento. Nos planos futuros:
- Invólucros para manipulação direta de registros PTE e encaminhamento de memória nuclear para o modo de usuário
- Injetores baseados nos recursos existentes de criação e entrega de fluxo da APC
- Plataforma GUI para engenharia reversa ao vivo e pesquisa de kernel do Windows
- Mecanismo de script para executar partes do código do kernel
- Suporte para SEH em módulos carregados dinamicamente
- Passando nos testes HLK
Obrigado pela atenção!