Exploring CI.dll and Bigpool Cache
The CI.dll
integrity module is responsible for authenticating and verifying the reliability of kernel modules. The kernel initializes it by calling CiInitialize()
, which returns the g_CiCallbacks
array. This list of callbacks is then used elsewhere in the kernel. For example, CiValidateImageHeader()
is called when a driver is loaded to verify its signature.
We navigate to CiValidateImageHeader()
and follow the call into CiInitializePhase2()
.
Here, in CiInitializePhase2()
, CI.dll
creates the g_CiEaCacheLookasideList
with a call to ExInitializePagedLookasideList()
which has type PAGED_LOOKASIDE_LIST
. See documentation. This lookaside list tracks paged big pool (heap) allocations.
Insiders are already aware that there are two (well, three) pool allocators: regular and big allocator. The regular allocator is used for any allocation less than or equal to a page in size, including the 32 byte pool header and initial free block. Big pool allocator is used when the size is more than one page, or when the pool type is CacheAligned
. Crucially, big pool allocations dont have room for headers, and are instead tracked in the PoolBigTable
.
In the docs, we see field ListEntry
which we use to iterate over the list. Interestingly, we also see PoolType
, Tag
and Size
. We generate the signature 48 8D 0D ?? ?? ?? ?? C7 44 24
and walk.
#pragma pack(push, 1)
struct ci_lookaside_t
{
u8 pad0[0x24];
u32 type; // +0x24
u32 tag; // +0x28
u32 size; // +0x2c
u8 pad1[0x10];
list_entry_t link; // +0x40
};
#pragma pack(pop)
void enum_ci_cache_lookaside(u8* ci_base)
{
auto ci_cache = uti::ida_sig<ci_lookaside_t*>(ci_base, "48 8D 0D ?? ?? ?? ?? C7 44 24").rva(3,7);
if (!ci_cache) // oopsie ^^^^^^^^^^^
return;
for (auto it = ci_cache->link.f; it != &ci_cache->link; it = it->f)
{
auto it_entry = CONTAINING_RECORD(it, ci_lookaside_t, link);
if (!it_entry)
continue;
auto it_tag = (u8*)(&it_entry->tag);
DbgPrintEx(0, 0, "-- size: %6X type: %4X tag: %c%c%c%c\n",
it_entry->size, it_entry->type, it_tag[0], it_tag[1], it_tag[2], it_tag[3]);
}
}
We load the driver and run it. The type is POOL_TYPE
, which is documented. The output shows many PagedPool
entries with varying sizes. CI.dll
keeps track of a lot of interesting telemetry. There are many lists like g_BootDriverList
, g_CiValidationLookasideList
, g_KernelHashBucketList
and g_KernelHashBucketList
.
The traditional way of dealing dealing with these lists is locking, deleting and creating them with ExDeleteLookasideListEx()
and ExInitializeLookasideListEx()
. But clearing lists which would otherwise include reference to legitimate drivers is suspicious. Indeed, the best approach is to walk every relevant list and unlinking references to your vulnerable driver.
In addition, ntoskrnl
tracks additional telemetry like PiDDBCacheTable
, MmLastUnloadedDriver
, MmUnloadedDrivers
. The more you dig, the more you uncover. All these lists, caches and trees are potential detection vectors. Advanced malware and cheats should consider all of them and more.