Introduction
A while ago we did some research. That specific project might be published at some other time in the future and we won’t go into too much detail about it here. But as part of this project we wanted to gain access into an internal data structure used by some driver. Sadly, the driver’s global pointer to this data structure is not exported, and we couldn’t find a way to access it from outside the driver itself. It is stored in the pool, so we couldn’t even scan the driver address space for signs of this structure.
Of course, there is always the option of doing binary parsing on the driver based on a function signature that references the global, and/or using an array of known offsets for the global variable and adding the driver base to find it. But these methods require finding and using the correct RVA for every version of the driver, as well as all potential function signatures. Because this driver does not have exported functions, such signatures would be brittle and subject to change between releases. Therefore, although often used by malware authors, we find these techniques ugly and inconvenient to implement — we knew we could do better.
So, we reverse engineered the data structure itself and came up with an interesting idea that can give us easy access to this data structure and to many others. The data structure we were interested in is very large and contains, among other things, a few lookaside lists embedded in it. Lookaside lists are single linked lists containing pool allocations of a fixed size. They are used by drivers for caching memory allocations instead of always requesting them from the memory manager. Let’s see what makes these interesting.
System Lookaside Lists
Here is the wdm.h
definition of a GENERAL_LOOKASIDE_LAYOUT
(GENERAL_LOOKASIDE
is just an aligned version of GENERAL_LOOKASIDE_LAYOUT
):
//
// The goal here is to end up with two structure types that are identical except
// for the fact that one (GENERAL_LOOKASIDE) is cache aligned, and the other
// (GENERAL_LOOKASIDE_POOL) is merely naturally aligned.
//
// An anonymous structure element would do the trick except that C++ can't handle
// such complex syntax, so we're stuck with this macro technique.
//
#define GENERAL_LOOKASIDE_LAYOUT \
union { \
SLIST_HEADER ListHead; \
SINGLE_LIST_ENTRY SingleListHead; \
} DUMMYUNIONNAME; \
USHORT Depth; \
USHORT MaximumDepth; \
ULONG TotalAllocates; \
union { \
ULONG AllocateMisses; \
ULONG AllocateHits; \
} DUMMYUNIONNAME2; \
\
ULONG TotalFrees; \
union { \
ULONG FreeMisses; \
ULONG FreeHits; \
} DUMMYUNIONNAME3; \
\
POOL_TYPE Type; \
ULONG Tag; \
ULONG Size; \
union { \
PALLOCATE_FUNCTION_EX AllocateEx; \
PALLOCATE_FUNCTION Allocate; \
} DUMMYUNIONNAME4; \
\
union { \
PFREE_FUNCTION_EX FreeEx; \
PFREE_FUNCTION Free; \
} DUMMYUNIONNAME5; \
\
LIST_ENTRY ListEntry; \
ULONG LastTotalAllocates; \
union { \
ULONG LastAllocateMisses; \
ULONG LastAllocateHits; \
} DUMMYUNIONNAME6; \
ULONG Future[2];
A useful fact to notice is that this structure contains a linked list (GENERAL_LOOKASIDE.ListEntry
), meaaning all lookaside lists do. Depending on whether the lookaside list was created with ExInitializeNPagedLookasideList
or ExInitializePagedLookasideList
(or, if ExInitializeLookasideListEx
was used, the PoolType
which was passed in), the data structure will be entered into one of two list heads. As such, if we follow the ListEntry
of any lookaside list, we’ll eventually end up at either ExPagedLookasideListHead
or ExNPagedLookasideListHead
. Since we create our own lookaside list through these APIs, if we pick the same pool type as our target structure, we can therefore through all other lookasides, and eventually reach the one contained in our target structure. In this particular use case, using our own definition of the structure, the useful CONTAINING_RECORD
macro, and the knowledge that the first member of the structure is a “magic” ULONG
that always contains the same value, we searched all lookaside lists using this mechanism until we reached our structure.
But the possibilities don’t stop there – this method gives us access to any kernel structure, exported or not, that contains a lookaside list. So what else is there?
Pool-Based Lookaside Lists
With some WinDbg magic, we can also find out valuable information about the data – whether it’s inside a driver (and which one!) or in the kernel pool, who it belongs to, the allocation size, etc. To explore the possibilities, we wrote a simple WinDbg script that iterates through all lookaside lists and uses the extremely helpful !pool
extension to dump information about them. Although we could build similar functionality in a custom C driver, there is no Windows Kernel API that can supply us with similar information about pool allocations and parsing pool pages to retrieve it is a lot of work, so we decided to avoid implementing the same functionality in C due to laziness. In fact, while we tried to implement our own C-based pool parser, we ended up realizing that nobody had described the myriad of changes in Windows 10 RS5
and above’s pool manager, so we’re busy writing a book on the topic.
Using our script, we found structures containing lookaside lists that belong to FltMgr.sys
, Win32k.sys
, Windows Defender drivers, various display drivers, and much more.
dx -r0 @$GeneralLookaside = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPagedLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry")
dx -r0 @$lookasideAddr = @$GeneralLookaside.Select(l => ((__int64)&l).ToDisplayString("x"))
dx -r0 @$extractBetween = ((x,y,z) => x.Substring(x.IndexOf(y) + y.Length, x.IndexOf(z) - x.IndexOf(y) - y.Length))
dx -r0 @$extractWithSize = ((x,y,z) => x.Substring(x.IndexOf(y) + y.Length, z))
dx -r2 @$poolData = @$lookasideAddr.Select(l => Debugger.Utility.Control.ExecuteCommand("!pool "+l+" 2")).Where(l => l[1].Length != 0x55 && l[1].Length != 0).Select(l => new {address = "0x" + @$extractBetween(l[1], "*", "size:"), tag = @$extractWithSize(l[1], "(Allocated) *", 4), tagDesc = l[2].Contains(",") ? @$extractBetween(l[2], ": ", ",") : l[2].Substring(l[2].IndexOf(":")+2), binary = l[2].Contains("Binary") ? l[2].Substring(l[2].IndexOf("Binary :")+9) : "unknown", size = "0x" + @$extractBetween(l[1], "size:", "previous size:").Replace(" ", "")})
[0x4a]
address : 0xffff988679939400
tag : Vi10
tagDesc : Video memory manager process heap
binary : dxgmms2.sys
size : 0x70
[0x4b]
address : 0xffff98867b647650
tag : DxgK
tagDesc : Vista display driver support
binary : dxgkrnl.sys
size : 0x640
[0x4c]
address : 0xffff98867b647650
tag : DxgK
tagDesc : Vista display driver support
binary : dxgkrnl.sys
size : 0x640
[0x4d]
address : 0xffff9886790f5430
tag : Vi17
tagDesc : Video memory manager pool
binary : dxgmms2.sys
size : 0x150
[0x4e]
address : 0xffff98867966e230
tag : Usla
tagDesc : USERTAG_LOOKASIDE
binary : win32k!InitLockRecordLookaside
size : 0xa0
[0x4f]
address : 0xffff98867966ea50
tag : Usla
tagDesc : USERTAG_LOOKASIDE
binary : win32k!InitLockRecordLookaside
size : 0xa0
[0x50]
address : 0xffff98867966e690
tag : Gla1
tagDesc : GDITAG_HMGR_LOOKASIDE_DC_TYPE
binary : win32k.sys
size : 0xa0
[0x51]
address : 0xffff98867966e550
tag : Gla4
tagDesc : GDITAG_HMGR_LOOKASIDE_RGN_TYPE
binary : win32k.sys
size : 0xa0
[0x52]
address : 0xffff98867966ecd0
tag : Gla5
tagDesc : GDITAG_HMGR_LOOKASIDE_SURF_TYPE
binary : win32k.sys
size : 0xa0
There are some results in which the pool tag is unknown, making the tracking of the driver they belong to difficult. A fun way to solve that is using driver verifier’s pool tracking feature. We can modify our script and replace the !pool <address> 2
command with !verifier <address> 2
and receive information about the allocating driver and the completes stack trace of the allocation. But running this command on so many addresses is extremely slow and it dumps a lot of information that is hard to sort through. So another option is going for a more manual approach – enabling driver verifier but executing the previous script as it is, and only querying specific addresses that seem interesting with verifier.
Image-Based Lookaside Lists
Initially we only searched for data in the pool because that is where the structure we were interested in was allocated. But with this trick we also get access to lookaside lists that are inside drivers, and we can use the cool new RtlPcToFileName
function to find out what driver these structures are in. In this case we did choose to implement this in C code since it’s more straightforward and faster to execute:
_Use_decl_annotations_
NTSTATUS
DriverEntry (
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS status;
LOOKASIDE_LIST_EX lookaside;
PLIST_ENTRY lookasideList;
PLIST_ENTRY lookasideListHead;
PGENERAL_LOOKASIDE generalLookaside;
UNICODE_STRING pcName = RTL_CONSTANT_STRING(L"RtlPcToFileName");
DECLARE_UNICODE_STRING_SIZE(driverName, 32);
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
auto RtlPcToFileNamePtr = (decltype(RtlPcToFileName)*)(MmGetSystemRoutineAddress(&pcName));
NT_ASSERT(RtlPcToFileNamePtr != nullptr);
//
// Create our own lookaside list to use for finding other lookaside lists in the kernel.
//
status = ExInitializeLookasideListEx(&lookaside,
nullptr,
nullptr,
PagedPool,
0,
8,
'Fake',
0);
if (!NT_SUCCESS(status))
{
goto Exit;
}
//
// Iterate over our lookaside list to find all the other lookaside lists
// and print information about them
//
generalLookaside = nullptr;
lookasideListHead = &lookaside.L.ListEntry;
lookasideList = lookasideListHead->Flink;
do
{
generalLookaside = CONTAINING_RECORD(lookasideList,
GENERAL_LOOKASIDE,
ListEntry);
//
// Use RtlPcToFileName to find whether the lookaside list is
// inside a driver and if so, which one
//
status = RtlPcToFileNamePtr(generalLookaside, &driverName);
if (NT_SUCCESS(status))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID,
DPFLTR_ERROR_LEVEL,
"Lookaside list is in driver %wZ\n",
driverName);
}
else
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID,
DPFLTR_ERROR_LEVEL,
“Lookaside list is not inside a driver\n”);
}
lookasideList = lookasideList->Flink;
} while (lookasideList != lookasideListHead);
status = STATUS_SUCCESS;
Exit:
ExDeleteLookasideListEx(&lookaside);
return status;
}
With this code we found lookaside lists inside of Ntoskrnl.exe
, Ci.dll
, Ntfs.sys
and more. Of course, since these are embedded inside of the driver memory, our only way to know whether these are independent lookaside lists or they are part of a larger structure is to dump the addresses and reverse engineer the drivers. But we’re all nerds who like reverse engineering, or we wouldn’t be writing/reading this blog.
We can also implement the same query in WinDbg if we choose to, using the ln
command which searches for the nearest symbol to an address:
dx -r0 @$GeneralLookaside = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPagedLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry")
dx -r0 @$lookasideAddr = @$GeneralLookaside.Select(l => ((__int64)&l).ToDisplayString("x"))
dx -r2 @$symData = @$lookasideAddr.Select(l => new {addr = l, sym = Debugger.Utility.Control.ExecuteCommand("ln "+l)}).Where(l => l.sym.Count() > 3).Select(l => new {addr = l.addr, sym = @$extractBetween(l.sym[3], " ", "|")})
[0x9]
addr : 0xfffff8000e4eb300
sym : nt!AlpcpLookasides+0x100
[0xa]
addr : 0xfffff8000e4db180
sym : nt!IopSymlinkInfoLookasideList
[0xb]
addr : 0xfffff8000e4ef040
sym : nt!WmipDSChunkInfoLookaside
[0xc]
addr : 0xfffff8000e4eefc0
sym : nt!WmipGEChunkInfoLookaside
[0xd]
addr : 0xfffff8000e4ef140
sym : nt!WmipISChunkInfoLookaside
[0xe]
addr : 0xfffff8000e4ef0c0
sym : nt!WmipMRChunkInfoLookaside
[0xf]
addr : 0xfffff8001172a880
sym : FLTMGR!FltGlobals+0x340
[0x10]
addr : 0xfffff8001172ad00
sym : FLTMGR!FltGlobals+0x7c0
[0x11]
addr : 0xfffff8001172af00
sym : FLTMGR!FltGlobals+0x9c0
[0x12]
addr : 0xfffff8001172b080
sym : FLTMGR!FltGlobals+0xb40
This is a pretty cool trick, which led to all sorts of cool discoveries. And we only searched for paged lookaside lists. There is a whole world of non-paged lookaside lists that we didn’t even look at yet. We ran the same WinDbg scripts as before, and just changed our starting point from nt!ExPagedLookasideListHead
to nt!ExNPagedLookasideListHead
to get the non-paged lookaside lists, and got some interesting results. We looked for non-paged lookaside lists in the pool:
[0x55]
address : 0xffff97884ba5c990
tag : Vkin
tagDesc : Hyper-V VMBus KMCL driver (incoming packets)
binary : vmbkmcl.sys
size : 0x2d0
[0x56]
address : 0xffff97884bad1590
tag : NDnd
tagDesc : NDIS_TAG_POOL_NDIS
binary : ndis.sys
size : 0x800
[0x57]
address : 0xffff97884bad3000
tag : NDrt
tagDesc : NDIS_TAG_RST_NBL
binary : ndis.sys
size : 0x800
[0x58]
address : 0xffff97884ba19130
tag : Nnbf
tagDesc : NetIO NetBufferLists
binary : netio.sys
size : 0x800
And inside of drivers:
[0x14]
addr : 0xfffff8000e4db100
sym : nt!IopOplockFoExtLookasideList
[0x15]
addr : 0xfffff8000e4ee880
sym : nt!WmipRegLookaside
[0x16]
addr : 0xfffff80010e40bc0
sym : ACPI!BuildRequestLookAsideList
[0x17]
addr : 0xfffff80010e40dc0
sym : ACPI!RequestLookAsideList
[0x18]
addr : 0xfffff80010e40c40
sym : ACPI!DeviceExtensionLookAsideList
[0x19]
addr : 0xfffff80010e40d40
sym : ACPI!RequestDependencyLookAsideList
[0x1a]
addr : 0xfffff80010e40cc0
sym : ACPI!ObjectDataLookAsideList
[0x17]
addr : 0xfffff80010e40f40
sym : ACPI!XswContextLookAsideList
Per-Processor Lookaside Lists
There’s actually one more linked list of lookaside lists that we haven’t talked about yet: ExPoolLookasideListHead
. Since the first versions of Windows NT, and up until Windows 10 RS5
when the pool manager was rewritten to use the Backend Heap (again, the topic of a future book!), it leveraged a per-processor array of 32
lookaside lists, one for each indexed multiple of the pool block size. On x86
, this basically meant any 8
-byte aligned allocation from 8
to 256
bytes, and on x64
, any 16
-byte aligned allocation from 16
to 512
bytes.
Since there was both a paged and nonpaged pool, each KPRCB
had two such arrays — the PPNPagedLookasideList
and the PPPagedLookasideList
. With Windows 8
and the introduction of the non-executable nonpaged pool, a third array was created: PPNxPagedLookasideList
. All of these lookaside lists are therefore inserted into the same linked list head, and on our system, you can easily see how many processors (16
) are present:
lkd> dx -r0 @$poolasides = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPoolLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry")
@$poolasides = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPoolLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry")
lkd> dx @$poolasides.Count(), d
@$poolasides.Count(), d : 1536
lkd> dx 1536 / 32 / 3
1536 / 32 / 3 : 16
lkd> dx *(int*)&nt!KeNumberProcessors
*(int*)&nt!KeNumberProcessors : 16 [Type: int]
Originally, this seemed exciting, as it would imply the ability to easily locate not only structures that contain a lookaside list, but in fact, any pool structure that’s a multiple of the pool block size. Unfortunately, if we take a look at these lists on modern Windows 10
systems, we find that they’re completely unused:
lkd> dx @$poolasides.Sum(p => p.TotalAllocates + p.TotalFrees)
@$poolasides.Sum(p => p.TotalAllocates + p.TotalFrees) : 0x0
Indeed, looking at the code in ExAllocatePoolWithTag
and friends, this logic was completely removed as part of the heap-related changes we’ll cover in a future research paper.
Executive Resources
The even cooler thing is that lookaside lists are not the only kernel structures that are linked to all other structures of the same type! Another example is theERESOURCE
, a structure used to implement read/write locking for drivers. Executive resources are also contained inside of many kernel structures, and can give us access to even more internal kernel information, if we know how to find them. We changed our WinDbg scripts to iterate over the linked list found in ERESOURCE.SystemResourcesList
, starting from nt!ExpSystemResourcesList
.
We first searched for ERESOURCE
objects in the pool:
[0xb8]
address : 0xffff97884bf9fb90
tag : Ntfx
tagDesc : Unrecognized NTFS tag (update base\published\pooltag.w)
binary : ntfs.sys
size : 0x170
[0xb9]
address : 0xffff97884bf50e80
tag : SeTl
tagDesc : Security Token Lock
binary : nt!se
size : 0x80
[0x4c]
address : 0xffff97884bf9ed30
tag : Ntfx
tagDesc : Unrecognized NTFS tag (update base\published\pooltag.w)
binary : ntfs.sys
size : 0x170
And then for ERESOURCE
objects inside of drivers:
[0x3c]
addr : 0xfffff8001268e8e0
sym : Ntfs!NtfsDynamicRegistrySettingsResource
[0x3d]
addr : 0xfffff80011211ef0
sym : NDIS!SharedMemoryResource
[0x3e]
addr : 0xfffff80012967630
sym : ksecpkg!g_rgCachedPagedSslProvs+0x410
[0x3f]
addr : 0xfffff80011a032f8
sym : tcpip!FlIsolationState+0x18
[0x40]
addr : 0xfffff80011d482e0
sym : mup!MupProviderTable+0x20
[0x41]
addr : 0xfffff80011d48100
sym : mup!MupiSurrogateList+0x20
[0x42]
addr : 0xfffff8000f4ac370
sym : CI!g_IgnoreLifetimeSigningEKU+0x70
[0x43]
addr : 0xfffff8000f4acb80
sym : CI!g_GRLContextLock
[0x44]
addr : 0xfffff80012f081c0
sym : netbios!g_erGlobalLock
We found some very interesting results that are probably worth further investigation, such as pool structures related to NTFS volume objects, structures inside Ci.dll
, and much, much more. On our machine we found over 400 000
executive resources:
lkd> dx -r0 @$eresource = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExpSystemResourcesList, "nt!_ERESOURCE", "SystemResourcesList")
@$eresource = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExpSystemResourcesList, "nt!_ERESOURCE", "SystemResourcesList")
lkd> dx @$eresource.Count(),d
@$eresource.Count(),d : 400960
Because of the sheer number, making analysis with LINQ unwieldly, we wanted to get pool information for some of these ERESOURCE
structures using C code and start analyzing them. Unfortunately, unlike lookaside lists, ERESOURCE
structures don’t have their pool tag as part of the structure, so we have to write a pool parser to get the pool information for each ERESOURCE
. As we’ve mentioned before, as it turns out, in RS5
and later, that is not an easy task at all, as you’ll see in our upcoming research on the new backend heap-backed kernel pool.
Read our other blog posts:
- Secure Kernel Research with LiveCloudKd
- Troubleshooting a System Crash
- KASLR Leaks Restriction
- Investigating Filter Communication Ports
- An End to KASLR Bypasses?
- Understanding a New Mitigation: Module Tampering Protection
- One I/O Ring to Rule Them All: A Full Read/Write Exploit Primitive on Windows 11
- One Year to I/O Ring: What Changed?
- HyperGuard Part 3 – More SKPG Extents
- An Exercise in Dynamic Analysis