⬆️ ⬇️

The Kernel-Bridge Framework: Ring0 Bridge

Have you ever wanted to look under the hood of the operating system, look at the internal structure of its mechanisms, twist the screws and look at the opportunities that have opened? Perhaps they even wanted to work directly with the hardware, but they believed that the drivers were rocketscience?



I propose to walk along the bridge to the core together and see how deep the rabbit hole is.



So, I imagine a driver framework for kernel hacking, written in C ++ 17, and designed, if possible, to remove the barriers between the core and the user mode or to maximize smooth their presence. And also, a set of user-mode and nuclear APIs and wrappers for quick and convenient development in Ring0 for both beginners and advanced programmers.

')

Key features:





… and much more.



And we will start with loading and connecting the framework to our C ++ project.



Github



For building it is very desirable to use the latest version of Visual Studio and the latest available WDK package (Windows Driver Kit), which can be downloaded from the official Microsoft website .



A free VMware Player with Windows installed, not less than Windows 7, of any capacity is perfect for testing.



Build trivial and questions will not cause:



  1. Open Kernel-Bridge.sln
  2. Choose the desired bit depth
  3. Ctrl + Shift + B


As a result, we will get a driver, a user-friendly library, as well as related service files ( * .inf for manual installation, * .cab for signing the driver on the Microsoft Hardware Certification Publisher, etc.).



To install the driver (if there is no digital code signature required for x64 - the corresponding EV certificate), you need to put the system into test mode, ignoring the digital signature of the drivers. To do this, run the command line as administrator:



bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS

bcdedit.exe /set TESTSIGNING ON



... and restart the machine. If everything is done correctly, an inscription appears in the lower right corner that Windows is in test mode.



Setup of the test environment is complete, let's start using the API in our project.



The framework has the following hierarchy:



/ Kernel-Bridge / API - a set of functions for use in drivers and kernel modules, do not have external dependencies and can be freely used in third-party projects

/ User-Bridge / API - a set of user-friendly wrappers for the driver and service functions for working with PE files, PDB symbols, etc.

/ SharedTypes / - both user mode and nuclear heders containing the necessary common types



The driver can be loaded in two ways: as a normal driver and as a mini-filter. The second method is preferable, since opens access to the advanced functionality of filters and user-mode callbacks on system events.



So, create a console project in C ++, connect the necessary header files and load the 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; } 


Fine! Now we can use the API and interact with the kernel.

Let's start with the most popular functionality among the developers of cheats - reading and writing the memory of someone else's process:



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


Nothing complicated! Let us descend to the level below - reading and writing nuclear memory:



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


What about functions for interacting with iron? For example, I / O ports.



Forward them to the user mode by setting 2 bits of IOPL in the EFlags register, which are responsible for the privilege level, in which the instructions in / out / cli / cli / sti are available .



Thus, we will be able to perform them in the user interface without the Privileged Instruction error:



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


But what about real freedom? After all, you often want to execute arbitrary code with kernel privileges. We write all the kernel code in the user mode and transfer control from the kernel to it (SMEP is automatically disabled, before the call the driver saves the FPU context and the call itself occurs inside the try..except block):



 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 


But in addition to pampering with the shells, there is also a serious functionality that allows you to create the simplest DLP based on the subsystem file, object and process filters.



The framework allows you to filter the CreateFile / ReadFile / WriteFile / DeviceIoControl , as well as the events of opening / duplicating handles ( ObRegisterCallbacks ) and the events of starting processes \ flows and loading modules ( PsSet *** NotifyRoutine ). This will allow, for example, to block access to arbitrary files or to replace information about the serial numbers of the hard disk.



Principle of operation:



  1. The driver registers file filters and installs Ob *** / Ps *** callbacks
  2. The driver opens a Communication- port to which clients connect, wishing to subscribe to this or that event.
  3. User-mode applications join the port and receive data about the event from the driver, filter it (cut handles in rights, block access to the file, etc.) and return the event to the kernel
  4. The driver applies the changes received.


An example of a ObRegisterCallbacks subscription and cuts in access to the current process:



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


So, we briefly went over the main points of the user-mode part of the framework, but the nuclear API remained behind the scenes.



The entire API and wrappers are located in the appropriate folder: / Kernel-Bridge / API /

These include working with memory, with processes, with strings and locks, and much more than that, greatly simplifying the development of your own drivers. The API and wrappers depend only on themselves and do not depend on the external environment: you can freely use them in your own driver.



An example of working with strings in the core is a stumbling block for all newbies:



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


If you want to implement your own handler for your IOCTL code, you can do it very easily according to the following scheme:



  1. Write handler in /Kernel-Bridge/Kernel-Bridge/IOCTLHandlers.cpp
  2. In the same file, add the handler to the end of the Handlers array in the DispatchIOCTL function
  3. Add the query index to the Ctls :: KbCtlIndices enumeration in CtlTypes.h in the SAME POSITION, as in the Handlers array in item 2
  4. Call your handler from the user interface by writing a wrapper in User-Bridge.cpp , making a call using the KbSendRequest function


All three types of I / O (METHOD_BUFFERED, METHOD_NEITHER and METHOD_IN_DIRECT / METHOD_OUT_DIRECT) are supported, METHOD_NEITHER is used by default.



That's all! The article covers only a small fraction of all the possibilities. I hope the framework will be useful for novice kernel component developers, reverse engineers, cheat developers, anti-cheats and defenses.



And also, everyone is welcome to take part in the development. Further plans:





Thank you for attention!

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



All Articles