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:
- Access to I / O ports, as well as forwarding in , out , cli, and sti instructions to a user interface via IOPL
- Wrappers over the system beeper
- MSR Access (Model-Specific Registers)
- A set of functions for access to the user memory of other processes and to the kernel memory
- Work with physical memory, DMI / SMBIOS
- Creation of usermode and nuclear flows, APC delivery
- Homer Ob *** and Ps *** -kallback and file system filters
- Loading unsigned drivers and kernel libraries
… 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:
- Open Kernel-Bridge.sln
- Choose the desired bit depth
- 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(
Nothing complicated! Let us descend to the level below - reading and writing nuclear memory:
using namespace VirtualMemory; constexpr int Size = 64; BYTE Buffer[Size];
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;
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:
- The driver registers file filters and installs Ob *** / Ps *** callbacks
- The driver opens a Communication- port to which clients connect, wishing to subscribe to this or that event.
- 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
- 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:
- Write handler in /Kernel-Bridge/Kernel-Bridge/IOCTLHandlers.cpp
- In the same file, add the handler to the end of the Handlers array in the DispatchIOCTL function
- Add the query index to the Ctls :: KbCtlIndices enumeration in CtlTypes.h in the SAME POSITION, as in the Handlers array in item 2
- 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:
- Wrappers for direct manipulations with PTE-records and forwarding of nuclear memory to user field
- Injectors based on existing streaming and APC delivery functions
- GUI platform for live reverse engineering and Windows kernel research
- Script engine to execute pieces of nuclear code
- SEH support in dynamically loaded modules
- HLK test passing
Thank you for attention!