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.

CiValidateImageHeader Call Stack

We navigate to CiValidateImageHeader() and follow the call into CiInitializePhase2().

CiInitializePhase2 Call

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.

ExInitializePagedLookasideList Usage

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.

Output

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.