Skip to content

Commit ca626e9

Browse files
authored
New blog post: Section Objects as Kernel/User communication mode
1 parent ae73796 commit ca626e9

File tree

1 file changed

+370
-0
lines changed

1 file changed

+370
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
title: Section Objects as Kernel/User communication mode
2+
author: hugsy
3+
category: research
4+
tags: windows, hack, memory-manager
5+
date: 2023-04-04 00:00 +0000
6+
modified: 2023-04-04 00:00 +0000
7+
8+
I've recently decided to read cover to cover some Windows Internals books, and currently reading the amazing book ["What Makes It Page"](), it gave me some ideas to play with [Section Objects](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/section-objects-and-views) as they covered in great details. One thought that occured to me was that even though a section is created from user or kernel land, its mapping can be in user-mode as much as in kernel (when called from the kernel).
9+
10+
11+
## Windows Section Objects
12+
13+
For quick reminder, a Section Object on Windows is a specific type of kernel object (of structure [`nt!SECTION`](https://www.vergiliusproject.com/kernels/x64/Windows%2011/22H2%20(2022%20Update)/_SECTION)) that represents a block of memory that processes can share between themselves or between a process and the kernel. It can be mapped to the paging file (i.e. backed by memory) or to a file on disk, but either can be handled using the same set of API, and even though they are allocated by the Object Manager, it is one of the many jobs of the Memory Manager to handle their access (handle access, permission, mapping etc.). In usermode the high level API is [`kernel32!CreateFileMapping`](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappingw), which after some hoops into `kernelbase`, boils down to [`ntdll!NtCreateSection`](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntcreatesection)
14+
15+
![createfilemappingw](https://pad.pwnbox.blah.cat:8443/uploads/fc2d3446-f23b-43c9-8590-da132404c8ef.png)
16+
17+
18+
The signature is as follow:
19+
```c++
20+
NTSTATUS
21+
NTAPI
22+
NtCreateSection (
23+
_Out_ PHANDLE SectionHandle,
24+
_In_ ACCESS_MASK DesiredAccess,
25+
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
26+
_In_opt_ PLARGE_INTEGER MaximumSize,
27+
_In_ ULONG SectionPageProtection,
28+
_In_ ULONG AllocationAttributes,
29+
_In_opt_ HANDLE FileHandle
30+
);
31+
```
32+
33+
If successful, the syscall will return a section handle in `SectionHandle`, which will refer to an instance of a `nt!_SECTION`. Therefore the handle will be added to the handle table of the calling process, accessible from kernel and user modes unless `OBJ_KERNEL_HANDLE` is specified in the `ObjectAttributes`. This will be important for us in the following, because it implies that if the process terminates, so will the section object.
34+
35+
In itself the Section Object doesn't have a lot going on, unless it is mapped to memory. This is achieved through `kernel32!MapViewOfView(Ex)` which again, boils down to the syscall `ntdll!NtMapViewOfSection`, whose signature is as follow:
36+
37+
```c=
38+
//
39+
// Syscall entry point
40+
//
41+
NTSTATUS
42+
NTAPI
43+
NtMapViewOfSection(
44+
HANDLE SectionHandle,
45+
HANDLE ProcessHandle,
46+
PVOID *BaseAddress,
47+
ULONG_PTR ZeroBits,
48+
SIZE_T CommitSize,
49+
PLARGE_INTEGER SectionOffset,
50+
PSIZE_T ViewSize,
51+
SECTION_INHERIT InheritDisposition,
52+
ULONG AllocationType,
53+
ULONG Win32Protect)
54+
```
55+
56+
Reversing this function is relatively straight forward:
57+
```c=
58+
{
59+
[...]
60+
if ( NT_SUCCESS(MiValidateZeroBits(&ZeroBits)) )
61+
{
62+
AccessMode = KeGetCurrentThread()->PreviousMode;
63+
64+
//
65+
// Internal function to the Memory Manager to map a view of a section
66+
//
67+
Status = MiMapViewOfSectionCommon(
68+
ProcessHandle,
69+
SectionHandle,
70+
0,
71+
BaseAddress,
72+
ViewSize,
73+
SectionOffset,
74+
Win32Protect,
75+
ZeroBits,
76+
AccessMode,
77+
&SectionParameter);
78+
[...]
79+
```
80+
81+
which makes us jump to:
82+
```c=
83+
NTSTATUS MiMapViewOfSectionCommon(
84+
HANDLE ProcessHandle,
85+
HANDLE SectionHandle,
86+
ACCESS_MASK DesiredAccess,
87+
PVOID BaseAddress,
88+
uint64_t ViewSize,
89+
uint64_t SectionOffset,
90+
uint32_t Win32Protect,
91+
uint8_t ZeroBits,
92+
KPROCESSOR_MODE AccessMode,
93+
SECTION_PARAMETER *SectionParameter)
94+
{
95+
[...]
96+
//
97+
// Get a reference to the asking process
98+
//
99+
Status = ObpReferenceObjectByHandleWithTag(ProcessHandle, (DesiredAccess + 8), ProcessType, AccessMode, 'MmVw', &SectionParameter->ProcessObject, nullptr, nullptr);
100+
if (Status >= 0)
101+
{
102+
PSECTION* SectionObject = nullptr;
103+
pSectionObject = &SectionObject;
104+
//
105+
// Get a reference to the section
106+
//
107+
Status = ObReferenceObjectByHandle(SectionHandle, &MmMakeSectionAccess[((uint64_t)SectionParameter->ProtectMaskForAccess)], MmSectionObjectType, AccessMode, pSectionObject, nullptr);
108+
SectionParameter->SectionObject = SectionObject;
109+
if (Status < 0)
110+
{
111+
ObfDereferenceObjectWithTag(SectionParameter->ProcessObject, 'MmVw');
112+
}
113+
}
114+
[...]
115+
116+
if (AccessMode == KernelMode)
117+
{
118+
//
119+
// In KM, do whatever
120+
//
121+
ViewSize_1 = ViewSize;
122+
}
123+
else
124+
{
125+
PVOID* pBaseAddress_1 = BaseAddress;
126+
//
127+
// With a request coming from UM, validate the BaseAddress is within UM bounds
128+
//
129+
if (BaseAddress >= 0x7fffffff0000)
130+
{
131+
pBaseAddress_1 = 0x7fffffff0000;
132+
}
133+
*(int64_t*)pBaseAddress_1 = *(int64_t*)pBaseAddress_1;
134+
ViewSize_1 = ViewSize;
135+
uint64_t r8_2 = ViewSize_1;
136+
if (ViewSize_1 >= 0x7fffffff0000)
137+
{
138+
r8_2 = 0x7fffffff0000;
139+
}
140+
*(int64_t*)r8_2 = *(int64_t*)r8_2;
141+
}
142+
SectionParameter->BaseAddress = *(int64_t*)BaseAddress;
143+
SectionParameter->ViewSize = *(int64_t*)ViewSize_1;
144+
145+
[...]
146+
}
147+
```
148+
149+
150+
What matters the most here would be the `BaseAddress` argument which will hold the UM address of the mapping. Meaning that Section Objects can be used to create communication channels between kernel <-> user mode (on top of obviously user <-> user). This is particularly nice especially because it allows to control finely the permission to the area: for instance a driver could create a section as read-writable, map its own view as RW, but expose to any process as RO. As a matter of fact, this is exactly how Windows 11 decided to protect the `(K)USER_SHARED_DATA` memory region, frequently used by kernel exploit since it's read/writable in ring-3 at a well-known address, making it a perfect way to bypass ALSR. The protection was added in 22H1 global variable which is initialized at boot-time and mapped as RW from the kernel through the `nt!MmWriteableUserSharedData`; however from user-mode only a read-only view is exposed to processes. For complete details about that protection, I invite the reader to refer to Connor McGarr's in-depth [excellent blog post](https://connormcgarr.github.io/kuser-shared-data-changes-win-11/){:target=blank} on the subject.
151+
152+
153+
## Section Object as a Kernel/User Communication Vector
154+
155+
Purely coincidentally, a colleague of mine stumbled upon a problem where they wanted to be able to capture the user-mode context of a thread from a driver, through `PsGetThreadContext`. The tricky part here was that `PsGetThreadContext()` follows the following signature:
156+
157+
```cpp
158+
NTSTATUS
159+
PSAPI
160+
PsGetThreadContext(
161+
IN PETHREAD Thread,
162+
IN OUT PCONTEXT ThreadContext,
163+
IN KPROCESSOR_MODE PreviousMode
164+
);
165+
```
166+
[Link](https://github.com/fengjixuchui/ApiSetSchema/blob/7dd5f58c527df37212aa1a596057e79afa44af3d/driver/process.h#L138-L144)
167+
168+
Where `ThreadContext` is the linear address to write the thread `CONTEXT` passed as first argument. However, the 3rd argument, `PreviousMode` matters the most: if specified as `UserMode` (1), the function performs a check to make sure the `ThreadContext` linear address resides within the usermode address range. Since I really love turning theory into practice, I figured this would be a perfect practice case for the technique mentioned above, so I ended up writing a PoC driver to serve that purpose in a (IMHO) fairly nice way. This actually didn't take long thanks to my [driver template](https://github.com/hugsy/modern-cpp-windows-driver-template) and all I had to do was implement the steps which were:
169+
170+
1. Create a section in the `System` process. Why in `System`? Simply because section handles must be tight to a process: therefore if the section is created in a "normal" process, the handle to it will be close when/if said process terminates, effectively closing the section. So we can use the `DriverEntry` to make sure the section handle is stored in the `System` kernel handle table. Save the handle in a global variable.
171+
172+
```c++=124
173+
// create section
174+
{
175+
OBJECT_ATTRIBUTES oa {};
176+
InitializeObjectAttributes(
177+
&oa,
178+
nullptr,
179+
OBJ_EXCLUSIVE | OBJ_KERNEL_HANDLE | OBJ_FORCE_ACCESS_CHECK,
180+
nullptr,
181+
nullptr);
182+
LARGE_INTEGER li {.QuadPart = 0x1000};
183+
Status =
184+
::ZwCreateSection(&Globals.SectionHandle, SECTION_MAP_WRITE, &oa, &li, PAGE_READWRITE, SEC_COMMIT, NULL);
185+
EXIT_IF_FAILED(L"ZwCreateSection");
186+
}
187+
```
188+
189+
[Link](https://github.com/hugsy/shared-kernel-user-section-driver/blob/main/MiniFilter/MinifilterDriver.cpp#L124-L137)
190+
191+
By breakpointing at the end of DriverEntry we confirm that the handle resides in the System process.
192+
193+
```
194+
[*] Loading CHANGEME
195+
[+] PsGetContextThread = FFFFF8061670B5B0
196+
[+] Section at FFFFFFFF80002FB4
197+
[+] Loaded fs filter CHANGEME
198+
Break instruction exception - code 80000003 (first chance)
199+
MinifilterDriver+0x7275:
200+
fffff806`1aa57275 cc int 3
201+
```
202+
203+
![](https://pad.pwnbox.blah.cat:8443/uploads/d4b64773-6412-46dc-a9f4-f21e703e2659.png)
204+
205+
206+
2. Then I can use any callback (process/image notification, minifilter callbacks etc.) to invoke `ZwMapViewOfSection`, reusing the section handle from the step earlier, and `NtCurrentProcess()` as process handle.
207+
208+
```c++=204
209+
NTSTATUS Status = ::ZwMapViewOfSection(
210+
Globals.SectionHandle,
211+
NtCurrentProcess(),
212+
&BaseAddress,
213+
0L,
214+
0L,
215+
NULL,
216+
&ViewSize,
217+
ViewUnmap,
218+
0L,
219+
PAGE_READWRITE);
220+
EXIT_IF_FAILED(L"ZwMapViewOfSection");
221+
```
222+
[Link](https://github.com/hugsy/shared-kernel-user-section-driver/blob/main/MiniFilter/MinifilterDriver.cpp#L204-L215)
223+
224+
`BaseAddress` will return an 64KB-aligned address located randomly (ASLR). The best thing here, is that we also control `ZeroBits`, allowing to (partly) control where that address will land.
225+
226+
3. We're free to call `PsGetThreadContext()` with the returned `BaseAddress` value.
227+
228+
```c++=224
229+
PCONTEXT ctx = reinterpret_cast<PCONTEXT>(BaseAddress);
230+
ctx->ContextFlags = CONTEXT_FULL;
231+
Status = Globals.PsGetContextThread(PsGetCurrentThread(), ctx, UserMode);
232+
EXIT_IF_FAILED(L"PsGetContextThread");
233+
234+
DbgBreakPoint();
235+
```
236+
[Link](https://github.com/hugsy/shared-kernel-user-section-driver/blob/main/MiniFilter/MinifilterDriver.cpp#L224-L228)
237+
238+
To prevent any inadverted permission drop of the view (and therefore BSoD-ing us during the call to `PsGetThreadContext`), we can secure the location using `MmSecureVirtualMemory`.
239+
240+
From WinDbg we can confirm the VAD is mapped when the breakpoint is hit:
241+
242+
![](https://pad.pwnbox.blah.cat:8443/uploads/03ba2044-6cd9-4efe-8570-524044a87d7f.png)
243+
244+
And as soon as the syscall returns, we're unmapped:
245+
246+
![](https://pad.pwnbox.blah.cat:8443/uploads/748def89-0331-44bb-a112-9ded9992da45.png)
247+
248+
4. Close the section in the driver unload callback.
249+
250+
251+
That's pretty much it: what we've got at the end is kernel driver controlled communication vector to any process in usermode: as the section handle is part of System kernel handle table, it's untouchable from ring-3 unless the driver dictactes otherwise by creating a view (with proper permissions) to it. This approach is great as it allows the driver to control everything, but if we want to give a user-mode process some say into it, it's also possible simply by turning the anonymous section we created for this PoC into a named one, then call sequentially `OpenFileMapping(SectionName)` then `MapViewOfFile()`. In addition, it could very well be ported to a process <-> process communication but here I wanted to play with the minifilter callbacks as an on-demand mechanism.
252+
253+
## Side-track
254+
255+
The careful reader will have notice that the step introduce a tiny race condition window, where another thread can also access the memory region. That bothered me, so I also examined more advanced options relying on the shared section objects. By nature they involve 2 PTEs:
256+
- the "real" PTE (hardware PTE), effectively used for VA -> PA translation;
257+
- along with a prototype PTE.
258+
259+
When the view is created, the memory manager will create empty PTEs but expect a page fault. This is verified quickly by breaking right after the call to `ZwMapViewOfSection`
260+
261+
```
262+
[*] Loading CHANGEME
263+
[+] PsGetContextThread = FFFFF8061670B5B0
264+
[+] Section at FFFFFFFF800035E4
265+
[+] Loaded fs filter CHANGEME
266+
[+] in PID=3292/TID=4676 , MappedSection=0000018D40BF0000
267+
Break instruction exception - code 80000003 (first chance)
268+
MinifilterDriver+0x17a7:
269+
fffff806`1aa517a7 cc int 3
270+
kd> !pte2 0x000018D40BF0000
271+
@$pte2(0x000018D40BF0000)
272+
va : 0x18d40bf0000
273+
cr3 : 0x3e64d000
274+
pml4e_offset : 0x3
275+
pdpe_offset : 0x35
276+
pde_offset : 0x5
277+
cr3_flags : [- -]
278+
pml4e : PDE(PA=3e66d000, PFN=3e66d, Flags=[P RW U - - A D - -])
279+
pdpe : PDE(PA=3df0e000, PFN=3df0e, Flags=[P RW U - - A D - -])
280+
pde : PDE(PA=d97b6000, PFN=d97b6, Flags=[P RW U - - A D - -])
281+
pte_offset : 0x1f0
282+
pte : PTE(PA=0, PFN=0, Flags=[- RO K - - - - - -])
283+
kernel_pxe : 0xffffeb00c6a05f80
284+
kd> dx -r1 @$pte2(0x000018D40BF0000).pte
285+
@$pte2(0x000018D40BF0000).pte : PTE(PA=0, PFN=0, Flags=[- RO K - - - - - -])
286+
address : 0xd97b6f80
287+
value : 0x0
288+
[...]
289+
PhysicalPageAddress : 0x0
290+
Pte : 0x0 [Type: _MMPTE *] <<<<
291+
```
292+
293+
However, after the call to `PsGetThreadContext` the entry is correctly populated:
294+
295+
```
296+
kd> g
297+
[+] Rip=00007ffa42e8d724
298+
[+] Rbp=00000020eccff550
299+
[+] Rsp=00000020eccff448
300+
[+] Rax=0000000000000033
301+
[+] Rbx=0000000000214040
302+
[+] Rcx=00000020eccff490
303+
[+] Rdx=0000000000100080
304+
[+] Rdx=0000000000100080
305+
[+] PsGetContextThread() succeeded
306+
Break instruction exception - code 80000003 (first chance)
307+
MinifilterDriver+0x1936:
308+
fffff806`1aa51936 cc int 3
309+
kd> dx -r1 @$pte2(0x000018D40BF0000)
310+
@$pte2(0x000018D40BF0000) : VA=0x18d40bf0000, PA=0xe23a0000, Offset=0x0
311+
va : 0x18d40bf0000
312+
cr3 : 0x3e64d000
313+
pml4e_offset : 0x3
314+
pdpe_offset : 0x35
315+
pde_offset : 0x5
316+
cr3_flags : [- -]
317+
pml4e : PDE(PA=3e66d000, PFN=3e66d, Flags=[P RW U - - A D - -])
318+
pdpe : PDE(PA=3df0e000, PFN=3df0e, Flags=[P RW U - - A D - -])
319+
pde : PDE(PA=d97b6000, PFN=d97b6, Flags=[P RW U - - A D - -])
320+
pte_offset : 0x1f0
321+
pte : PTE(PA=e23a0000, PFN=e23a0, Flags=[P RW U - - A D - -])
322+
offset : 0x0
323+
pa : 0xe23a0000
324+
kernel_pxe : 0xffffeb00c6a05f80
325+
```
326+
327+
The PTE is valid:
328+
329+
```
330+
kd> dx -r1 @$pte2(0x000018D40BF0000).pte
331+
@$pte2(0x000018D40BF0000).pte : PTE(PA=e23a0000, PFN=e23a0, Flags=[P RW U - - A D - -])
332+
address : 0xd97b6f80
333+
value : 0xc0000000e23a0867
334+
Flags : Flags=[P RW U - - A D - -]
335+
PageFrameNumber : 0xe23a0
336+
Pfn [Type: _MMPFN]
337+
PhysicalPageAddress : 0xe23a0000
338+
Pte : 0xffff9480f55f81d0 [Type: _MMPTE *]
339+
```
340+
341+
So this means we have a great way to determine whether a physical page was accessed, using `MmGetPhysicalAddress()`. To test this we invoke it after the mapping (where we expect a null value) and a second time after the call to `PsGetThreadContext`:
342+
![](https://pad.pwnbox.blah.cat:8443/uploads/ac738af0-04fe-4b85-a9d2-ea3911be93cb.png)
343+
344+
The 2nd value for `PhyBaseAddress` points to the physical address where the function output is stored.
345+
At that point, I thought it would be sufficient to stop because we have an effective way to honeypot potential corruptions attempts:
346+
- Create a section with many pages (the more the better)
347+
- During the preparation to the invokation of `PsGetThreadContext`, choose randomly one page that will receive the `CONTEXT`
348+
- Map all the pages separately
349+
- Call `PsGetThreadContext`
350+
351+
Once the call is over, we can use the method above to validate whether any other page than the one we know valid were accessed. If so, discard the result.
352+
353+
Isn't Windows awesome?
354+
355+
356+
# End
357+
358+
There are a lot of possible fun uses of sections, and since I want to try to document more of my "stuff". Some offensive cool use case would be for instance, would be to expose code "on-demand" to a specific thread/process, removing the mapped execution page(s) from the process VAD as soon as we're done.
359+
I'll try to post follow-up updates.
360+
361+
For those interested in the code, you would find a minifilter driver ready to build & compile on the Github project: [<i class="fa fa-github"></i> hugsy/shared-kernel-user-section-driver](https://github.com/hugsy/shared-kernel-user-section-driver){:target=blank}
362+
363+
So, see you next time?
364+
365+
366+
Credits:
367+
- [What Makes It Page](https://www.amazon.com/What-Makes-Page-Windows-Virtual/dp/1479114294)
368+
- [Windows Internals 7th edition, Part 1](https://www.amazon.com/Windows-Internals-Part-architecture-management/dp/0735684189)
369+
- [Vergilius Project](https://www.vergiliusproject.com/)
370+
- [MSDN - Managing Memory Sections](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-memory-sections)

0 commit comments

Comments
 (0)