El marco Kernel-Bridge: Puente en anillo0

¿Alguna vez has querido mirar bajo el capó del sistema operativo, mirar la estructura interna de sus mecanismos, girar los tornillos y ver las oportunidades que se han abierto? Quizás incluso quisieron trabajar directamente con el hardware, pero ¿pensaron que los controladores eran ciencia espacial?

Propongo caminar a lo largo del puente hasta el núcleo y ver qué tan profundo es la madriguera del conejo.

Por lo tanto, presento el marco del controlador para la piratería del kernel, escrito en C ++ 17, y diseñado, si es posible, para eliminar las barreras entre el kernel y el modo de usuario o para suavizar su presencia tanto como sea posible. Y también, un conjunto de API y contenedores de modo de usuario y kernel para un desarrollo rápido y conveniente en Ring0 tanto para programadores principiantes como avanzados.

Características clave:

  • Acceso a puertos de E / S, así como reenvío de instrucciones de entrada , salida , cli y sti al modo de usuario a través de IOPL
  • Envolturas sobre el tweeter del sistema
  • Acceda a MSR (registros específicos del modelo)
  • Un conjunto de funciones para acceder a la memoria en modo de usuario de otros procesos y memoria del núcleo
  • Trabajar con memoria física, DMI / SMBIOS
  • Creación de flujos nucleares y en modo usuario, entrega APC
  • Devolución de llamada Ob *** y Ps *** en modo de usuario y filtros del sistema de archivos
  • Descargue controladores y bibliotecas de kernel sin firmar

... y mucho más

Y comenzaremos cargando y conectando el marco a nuestro proyecto C ++.

Github

Para el ensamblaje, es muy recomendable utilizar la última versión de Visual Studio y el último WDK (Kit de controladores de Windows) disponible, que se puede descargar desde el sitio web oficial de Microsoft .

Para las pruebas, el VMware Player gratuito con Windows instalado, no inferior a Windows 7, de cualquier capacidad es perfecto.

La asamblea es trivial y no causará preguntas:

  1. Abra Kernel-Bridge.sln
  2. Elija la profundidad de bits requerida
  3. Ctrl + Shift + B

Como resultado, obtenemos un controlador, una biblioteca en modo de usuario, así como archivos de utilidades relacionados ( * .inf para instalación manual, * .cab para firmar el controlador en Microsoft Hardware Certification Publisher, etc.).

Para instalar el controlador (si no es necesaria una firma digital para x64, el certificado EV correspondiente), debe poner el sistema en modo de prueba, ignorando la firma digital de los controladores. Para hacer esto, ejecute la línea de comando como administrador:

bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe /set TESTSIGNING ON

... y reinicie la máquina. Si todo se hace correctamente, aparecerá una inscripción en la esquina inferior derecha de que Windows está en modo de prueba.

La configuración del entorno de prueba se ha completado, comencemos a usar la API en nuestro proyecto.

El marco tiene la siguiente jerarquía:

/ Kernel-Bridge / API : un conjunto de funciones para usar en controladores y módulos de kernel , no tiene dependencias externas y se puede usar libremente en proyectos de terceros
/ User-Bridge / API : un conjunto de envoltorios de modo de usuario sobre el controlador y las funciones de utilidad para trabajar con archivos PE, caracteres PDB, etc.
/ SharedTypes / - encabezados nucleares y en modo usuario que contienen los tipos comunes necesarios

El controlador se puede cargar de dos maneras: como controlador normal y como minifiltro. Se prefiere el segundo método, porque da acceso a la funcionalidad avanzada de filtros y devoluciones de llamada en modo de usuario para eventos del sistema.

Entonces, cree un proyecto de consola en C ++, conecte los archivos de encabezado necesarios y cargue el controlador:

 #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; } 

Genial Ahora podemos usar la API e interactuar con el núcleo.
Comencemos con la funcionalidad más popular en el entorno de los desarrolladores de trucos: leer y escribir la memoria de otro proceso:

 using namespace Processes::MemoryManagement; constexpr int Size = 64; BYTE Buffer[Size] = {}; BOOL Status = KbReadProcessMemory( //  KbWriteProcessMemory,   ProcessId, 0x7FFF0000, //     ProcessId &Buffer, Size ); 

Nada complicado! Bajemos un nivel: leer y escribir memoria nuclear:

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

¿Qué pasa con las funciones para interactuar con el hierro? Por ejemplo, puertos de E / S.

Los reenviaremos al modo de usuario seleccionando 2 bits IOPL en el registro EFlags, que son responsables del nivel de privilegio en el que están disponibles las instrucciones de entrada / salida / cli / sti .

Por lo tanto, podremos ejecutarlos en modo de usuario sin el error de instrucción privilegiada:

 #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(); 

¿Pero qué hay de la verdadera libertad? Después de todo, a menudo se desea ejecutar código arbitrario con privilegios de kernel. Escribimos todo el código del núcleo en el modo de usuario y le transferimos el control desde el núcleo (SMEP se apaga automáticamente, antes de llamar al controlador, guarda el contexto de la FPU y la llamada en sí se realiza dentro de un bloque 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 

Pero además de mimar con shells, también existe una funcionalidad seria que le permite crear DLP simples basados ​​en el subsistema de filtros de archivos, objetos y procesos.

El marco permite filtrar CreateFile / ReadFile / WriteFile / DeviceIoControl , así como eventos de apertura / duplicación de controladores ( ObRegisterCallbacks ) y eventos de procesos / subprocesos de inicio y carga de módulos ( PsSet *** NotifyRoutine ). Esto permitirá, por ejemplo, bloquear el acceso a archivos arbitrarios o reemplazar información sobre los números de serie del disco duro.

Principio de funcionamiento:

  1. El controlador registra filtros de archivos e instala las devoluciones de llamada Ob *** / Ps ***
  2. El controlador abre un puerto de comunicación al que se conectan los clientes que desean suscribirse a un evento
  3. Las aplicaciones en modo de usuario se conectan al puerto y reciben datos del controlador sobre el evento que ocurrió, realizan el filtrado (trunca los identificadores en los derechos, bloquean el acceso al archivo, etc.) y devuelven el evento al núcleo
  4. El conductor aplica los cambios recibidos.

Un ejemplo de suscribirse a ObRegisterCallbacks y cortar el acceso al proceso actual:

 #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); //   }); 

Entonces, revisamos brevemente los puntos principales de la parte del marco del modo de usuario, pero la API central permaneció detrás de escena.

Todas las API y contenedores están ubicados en la carpeta correspondiente: / Kernel-Bridge / API /
Incluyen trabajar con memoria, con procesos, con cadenas y bloqueos, y mucho más, simplificando en gran medida el desarrollo de sus propios controladores. Las API y los contenedores dependen solo de ellos mismos y no dependen del entorno externo: puede usarlos libremente en su propio controlador.

Un ejemplo de trabajo con cadenas en el núcleo es un obstáculo para todos los principiantes:

 #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()); 

Si desea implementar su propio controlador para su código IOCTL, puede hacerlo fácilmente de acuerdo con el siguiente esquema:

  1. Escriba un controlador en /Kernel-Bridge/Kernel-Bridge/IOCTLHandlers.cpp
  2. En el mismo archivo, agregue un controlador al final de la matriz de controladores en la función DispatchIOCTL
  3. Agregue el índice de consulta a la enumeración Ctls :: KbCtlIndices en CtlTypes.h en la MISMA POSICIÓN que en la matriz de Controladores en el elemento 2
  4. Llame a su controlador desde el modo de usuario escribiendo un contenedor en User-Bridge.cpp , haciendo una llamada usando la función KbSendRequest

Los tres tipos de E / S son compatibles (METHOD_BUFFERED, METHOD_NEITHER y METHOD_IN_DIRECT / METHOD_OUT_DIRECT), de forma predeterminada, se utiliza METHOD_NEITHER.

Eso es todo! El artículo cubre solo una pequeña fracción de todas las posibilidades. Espero que el marco sea útil para desarrolladores novatos de componentes del kernel, ingenieros inversos, desarrolladores de trampas, anti-trampas y protecciones.

Y también, todos están invitados a participar en el desarrollo. En los planes futuros:

  • Contenedores para la manipulación directa de registros PTE y reenvío de memoria nuclear al modo de usuario
  • Inyectores basados ​​en las características existentes de creación y entrega de flujo APC
  • Plataforma GUI para ingeniería inversa en vivo e investigación de kernel de Windows
  • Motor de secuencias de comandos para ejecutar fragmentos de código del núcleo
  • Soporte para SEH en módulos cargados dinámicamente
  • Pasando las pruebas HLK

Gracias por su atencion!

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


All Articles