Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

Traverse the unloaded modules in the kernel

2023-03-26 @ UTC+0

In the process of malware analysis, we should not miss any clues. The devil is in the details. This article will introduce how to traverse the list of unloaded modules in the Windows kernel, allowing us to hit the last hiding place of the malware, leaving them nowhere to hide.

Unloaded modules in WinDBG

The lm command, as a commonly used command in WinDBG, not only outputs the current loaded module list, but also outputs an unloaded module list at the end:


In the scenario of malicious sample analysis, this unloaded list could potentially be a breakthrough point, because malware authors might only focus on hiding in the regular infrastructure, but ignore the modules they have used and unloaded. Sometimes we can use this as a breakthrough point to find more clues.

Windows support for the unloaded list

In the leaked Windows source code, there is a global variable named MmUnloadedDrivers that points to the unloaded module list.

The corresponding data structure of this global variable can be found in base\ntos\inc\mm.h:

  1. typedef struct _UNLOADED_DRIVERS {  
  2.     UNICODE_STRING Name;  
  3.     PVOID StartAddress;  
  4.     PVOID EndAddress;  
  5.     LARGE_INTEGER CurrentTime;  
  6. } UNLOADED_DRIVERS, *PUNLOADED_DRIVERS;  

Its corresponding traversal code can be found in base\ntos\mm\shutdown.c:

  1. VOID MiReleaseAllMemory (    VOID    )  
  2. {  
  3.     ......  
  4.     if (MmUnloadedDrivers != NULL) {  
  5.         Entry = &MmUnloadedDrivers[0];  
  6.         for (i = 0; i < MI_UNLOADED_DRIVERS; i += 1) {  
  7.             if (Entry->Name.Buffer != NULL) {  
  8.                 RtlFreeUnicodeString (&Entry->Name);  
  9.             }  
  10.             Entry += 1;  
  11.         }  
  12.         ExFreePool (MmUnloadedDrivers);  
  13.     }  
  14.     ......  
  15. }  

And MI_UNLOADED_DRIVERS is defined in base\ntos\mm\mi.h:

  1. #define MI_UNLOADED_DRIVERS 50  

So, the system saves information of up to 50 unloaded modules at most. If it exceeds this number, it will delete the oldest one in the current saved list and then save the name (note, not including the path) of the latest unloaded module at the end of the list.

Traversing the unloaded module list

After understanding the basic data structure, assuming we can now get the address of the global variable MmUnloadedDrivers through the _internal_get_MmUnloadDriver_address function, we can easily write the traversal code, as follows:

  1. NTSTATUS enumerate_unloaded_modules()  
  2. {  
  3.     ULONG_PTR Index = 0;  
  4.     PULONG_PTR MmUnloadedDrivers = _internal_get_MmUnloadDriver_address();  
  5.   
  6.     if (NULL == MmUnloadedDrivers) return STATUS_UNSUCCESSFUL;  
  7.   
  8.     for (Index = 0; Index < MI_UNLOADED_DRIVERS; ++Index)  
  9.     {  
  10.         PUNLOADED_DRIVERS each_unloaded_module = (PUNLOADED_DRIVERS)*MmUnloadedDrivers + Index;  
  11.         if (MmIsAddressValid(each_unloaded_module) && MmIsAddressValid(each_unloaded_module->ModuleName.Buffer) && \  
  12.             (each_unloaded_module->ModuleName.Length == each_unloaded_module->ModuleName.MaximumLength || \  
  13.             each_unloaded_module->ModuleName.Length + sizeof(WCHAR) == each_unloaded_module->ModuleName.MaximumLength))  
  14.         {  
  15.             PKE_UNLOADED_MODULE buffer = ExAllocatePoolWithTag(PagedPool, \  
  16.                 sizeof(KE_UNLOADED_MODULE), UTL_ULDMDL_ALLOCATE_TAG);  
  17.             RtlZeroMemory(buffer, sizeof(KE_UNLOADED_MODULE));  
  18.   
  19.             buffer->StartAddress = each_unloaded_module->StartAddress;  
  20.             buffer->EndAddress = each_unloaded_module->EndAddress;  
  21.             buffer->ModuleName.Length = each_unloaded_module->ModuleName.Length;  
  22.             buffer->ModuleName.MaximumLength = each_unloaded_module->ModuleName.MaximumLength;  
  23.             buffer->ModuleName.Buffer = ExAllocatePoolWithTag(PagedPool, \  
  24.                 each_unloaded_module->ModuleName.MaximumLength, UTL_ULDMDL_ALLOCATE_TAG);  
  25.             if (each_unloaded_module->ModuleName.Buffer && MmIsAddressValid(each_unloaded_module->ModuleName.Buffer))  
  26.             {  
  27.                 RtlCopyMemory(buffer->ModuleName.Buffer, each_unloaded_module->ModuleName.Buffer, \  
  28.                     each_unloaded_module->ModuleName.Length > MAX_PATH ? MAX_PATH : each_unloaded_module->ModuleName.Length);  
  29.             }  
  30.             _internal_copy_buffer_to_r3(buffer, sizeof(KE_UNLOADED_MODULE));  
  31.         }  
  32.     }  
  33. }  

Verify the result:


It can be seen that apart from the driver utils.sys used for this traversal, the list is consistent with the list output by the lm command at the beginning of this article.
Catalog
Unloaded modules in WinDBG
Windows support for the unloaded list
Traversing the unloaded module list

CopyRight (c) 2020 - 2025 Debugwar.com

Designed by Hacksign