O Kernel-Bridge Framework: Ponte no Ring0

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 ++.

Github

Para 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:

  1. Abra o Kernel-Bridge.sln
  2. Escolha a profundidade de bits necessária
  3. 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( //  KbWriteProcessMemory,   ProcessId, 0x7FFF0000, //     ProcessId &Buffer, Size ); 

Nada complicado! Vamos descer um nível - lendo e escrevendo memória nuclear:

 using namespace VirtualMemory; constexpr int Size = 64; BYTE Buffer[Size]; //  "",  ""    , //       : BOOL Status = KbCopyMoveMemory( reinterpret_cast<WdkTypes::PVOID>(Buffer), //  0xFFFFF80000C00000, //  Size, FALSE //  ,     ); 

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; //    KeStallExecutionProcessor: ULONG Result = 1337; KbExecuteShellCode( []( _GetKernelProcAddress GetKernelProcAddress, PVOID Argument ) -> ULONG { //      Ring0 //     : using _KeStallExecutionProcessor = VOID(WINAPI*)(ULONG Microseconds); auto Stall = reinterpret_cast<_KeStallExecutionProcessor>( GetKernelProcAddress(L"KeStallExecutionProcessor") ); Stall(1000 * 1000); //      ULONG Value = *static_cast<PULONG>(Argument); return Value == 1337 ? 0x1EE7C0DE : 0; }, &Result, // Argument &Result // Result ); //   Result = 0x1EE7C0DE 

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:

  1. O driver registra filtros de arquivos e instala retornos de chamada Ob *** / Ps ***
  2. O driver abre uma porta de comunicação na qual os clientes que desejam se inscrever em um evento se conectam
  3. 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
  4. 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:

  1. Escreva um manipulador em /Kernel-Bridge/Kernel-Bridge/IOCTLHandlers.cpp
  2. No mesmo arquivo, adicione um manipulador ao final da matriz Handlers na função DispatchIOCTL
  3. Adicione o índice de consulta à enumeração Ctls :: KbCtlIndices em CtlTypes.h na mesma posição, como na matriz Handlers no item 2
  4. 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!

Source: https://habr.com/ru/post/pt429198/


All Articles