Windows Data Structures and Callbacks, Part 1
A process can contain thousands of pointers to executable code, some of which are stored in opaque, but writeable data structures only known to Microsoft, a handful of third party vendors and of course bad guys that want to hide malicious code from memory scanners. This post documents what some of the data structures contain rather than PoCs to demonstrate code redirection or evasion, which I probably won’t discuss much anymore. The names of some structure fields won’t be entirely accurate, but feel free to drop me an email if you think something needs correcting. No, I don’t have access to source code. These structures were reverse engineered or can be found on MSDN.
ntdll!RtlpDynamicFunctionTable contains DYNAMIC_FUNCTION_TABLE entries and callback functions for a range of memory that can be installed using ntdll!RtlInstallFunctionTableCallback. ntdll!RtlGetFunctionTableListHead returns a pointer to the list and since NTDLL.dll uses the same base address for each process, you can read entries from a remote process very easily.
typedef enum _FUNCTION_TABLE_TYPE { RF_SORTED, RF_UNSORTED, RF_CALLBACK } FUNCTION_TABLE_TYPE; typedef PRUNTIME_FUNCTION (CALLBACK *PGET_RUNTIME_FUNCTION_CALLBACK) (ULONG_PTR ControlPc, PVOID Context); typedef struct _DYNAMIC_FUNCTION_TABLE { LIST_ENTRY Links; DWORD64 TableIdentifier; LARGE_INTEGER TimeStamp; DWORD64 MinimumAddress; DWORD64 MaximumAddress; PVOID BaseAddress; PGET_RUNTIME_FUNCTION_CALLBACK Callback; PCWSTR OutOfProcessCallbackDll; FUNCTION_TABLE_TYPE Type; ULONG EntryCount; ULONG64 UnknownStruct[3]; // referenced by RtlAvlInsertNodeEx } DYNAMIC_FUNCTION_TABLE, *PDYNAMIC_FUNCTION_TABLE;
Microsoft recommends against using it, but sechost!SetTraceCallback can still receive ETW events. Entries of type EVENT_CALLBACK_ENTRY are located at sechost!EtwpEventCallbackList.
typedef VOID (CALLBACK PEVENT_CALLBACK)(PEVENT_TRACE pEvent); ULONG WMIAPI SetTraceCallback( LPCGUID pGuid, PEVENT_CALLBACK EventCallback); typedef struct _EVENT_CALLBACK_ENTRY { LIST_ENTRY ListHead; GUID ProviderId; PEVENT_CALLBACK Callback; } EVENT_CALLBACK_ENTRY, *PEVENT_CALLBACK_ENTRY;
It’s possible to receive notifications about a DLL being loaded or unloaded using ntdll!LdrRegisterDllNotification. It’s used to hook API for Common Language Runtime (CLR) in ClrGuard. Entries of type LDR_DLL_NOTIFICATION_ENTRY can be located at ntdll!LdrpDllNotificationList.
typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA { ULONG Flags; // Reserved. PUNICODE_STRING FullDllName; // The full path name of the DLL module. PUNICODE_STRING BaseDllName; // The base file name of the DLL module. PVOID DllBase; // A pointer to the base address for the DLL in memory. ULONG SizeOfImage; // The size of the DLL image, in bytes. } LDR_DLL_LOADED_NOTIFICATION_DATA, *PLDR_DLL_LOADED_NOTIFICATION_DATA; typedef struct _LDR_DLL_UNLOADED_NOTIFICATION_DATA { ULONG Flags; // Reserved. PUNICODE_STRING FullDllName; // The full path name of the DLL module. PUNICODE_STRING BaseDllName; // The base file name of the DLL module. PVOID DllBase; // A pointer to the base address for the DLL in memory. ULONG SizeOfImage; // The size of the DLL image, in bytes. } LDR_DLL_UNLOADED_NOTIFICATION_DATA, *PLDR_DLL_UNLOADED_NOTIFICATION_DATA; typedef VOID (CALLBACK *PLDR_DLL_NOTIFICATION_FUNCTION)( ULONG NotificationReason, PLDR_DLL_NOTIFICATION_DATA NotificationData, PVOID Context); typedef union _LDR_DLL_NOTIFICATION_DATA { LDR_DLL_LOADED_NOTIFICATION_DATA Loaded; LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded; } LDR_DLL_NOTIFICATION_DATA, *PLDR_DLL_NOTIFICATION_DATA; typedef struct _LDR_DLL_NOTIFICATION_ENTRY { LIST_ENTRY List; PLDR_DLL_NOTIFICATION_FUNCTION Callback; PVOID Context; } LDR_DLL_NOTIFICATION_ENTRY, *PLDR_DLL_NOTIFICATION_ENTRY; typedef NTSTATUS(NTAPI *_LdrRegisterDllNotification) ( ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID *Cookie); typedef NTSTATUS(NTAPI *_LdrUnregisterDllNotification)(PVOID Cookie);
Kernel drivers can secure user-space memory using ntoskrnl!MmSecureVirtualMemory. This prevents the memory being freed or having its page protection made more restrictive. i.e PAGE_NOACCESS. To monitor changes, developers can install a callback using AddSecureMemoryCacheCallback. Entries of type RTL_SEC_MEM_ENTRY are located at ntdll!RtlpSecMemListHead.
typedef BOOLEAN (CALLBACK *PSECURE_MEMORY_CACHE_CALLBACK)(PVOID, SIZE_T); typedef struct _RTL_SEC_MEM_ENTRY { LIST_ENTRY Links; ULONG Revision; ULONG Reserved; PSECURE_MEMORY_CACHE_CALLBACK Callback; } RTL_SEC_MEM_ENTRY, *PRTL_SEC_MEM_ENTRY;
A process can register for Plug and Play events using cfgmgr32!CM_Register_Notification. Microsoft recommends legacy systems up to Windows 7 use RegisterDeviceNotification, but I didn’t examine that function. Notification entries of type _HCMNOTIFICATION are located at cfgmgr32!EventSystemClientList. _CM_CALLBACK_INFO is the structure sent to \Device\DeviceApi\CMNotify when a process registers a callback. As you can see from the WnfSubscription field, it uses the Windows Notification Facility (WNF) to receive events.
typedef DWORD (CALLBACK *PCM_NOTIFY_CALLBACK)( _In_ HCMNOTIFICATION hNotify, _In_opt_ PVOID Context, _In_ CM_NOTIFY_ACTION Action, _In_ PCM_NOTIFY_EVENT_DATA EventData, _In_ DWORD EventDataSize); typedef struct _CM_CALLBACK_INFO { WCHAR ModulePath[MAX_PATH]; CM_NOTIFY_FILTER EventFilter; }; typedef struct _tagHCMNOTIFICATION { USHORT Signature; // 0xF097 SRWLOCK SharedLock; CONDITION_VARIABLE ConditionVar; LIST_ENTRY EventSystemClientList; LIST_ENTRY EventSystemPendingClients; BOOL Active; BOOL InProgress; CM_NOTIFY_FILTER EventFilter; PCM_NOTIFY_CALLBACK Callback; PVOID Context; HANDLE NotifyFile; // handle for \Device\DeviceApi\CMNotify PWNF_USER_SUBSCRIPTION WnfSubscription; LIST_ENTRY Links; } _HCMNOTIFICATION, *_PHCMNOTIFICATION;
When kernelbase!KernelBaseBaseDllInitialize is executed, it installs an exception handler kernelbase!UnhandledExceptionFilter via SetUnhandledExceptionFilter. Unless a Vectored Exception Handler (VEH) is installed afterwards, this is the top level handler executed for any faults that occur. VEH callbacks installed using AddVectoredExceptionHandler or AddVectoredContinueHandler are located at ntdll!LdrpVectorHandlerList
// vectored handler list typedef struct _RTL_VECTORED_HANDLER_LIST { SRWLOCK Lock; LIST_ENTRY List; } RTL_VECTORED_HANDLER_LIST, *PRTL_VECTORED_HANDLER_LIST; // exception handler entry typedef struct _RTL_VECTORED_EXCEPTION_ENTRY { LIST_ENTRY List; PULONG_PTR Flag; // some flag related to CFG ULONG RefCount; PVECTORED_EXCEPTION_HANDLER VectoredHandler; } RTL_VECTORED_EXCEPTION_ENTRY, *PRTL_VECTORED_EXCEPTION_ENTRY;
Windows provides API to enable application recovery, dumping process memory and generating reports via the WER service. WER settings for a process can be located within the Process Environment Block (PEB) at WerRegistrationData.
I’ll discuss structures separately, but for the few that aren’t. Signature is set internally by kernelbase!WerpInitPEBStore and simply contains the string “PEB_SIGNATURE”. AppDataRelativePath is set by WerRegisterAppLocalDump. kernelbase!RegisterApplicationRestart can be used to set RestartCommandLine, which is used as the command line when the process is to be eh..restarted. 🙂
typedef struct _WER_PEB_HEADER_BLOCK { LONG Length; WCHAR Signature[16]; WCHAR AppDataRelativePath[64]; WCHAR RestartCommandLine[RESTART_MAX_CMD_LINE]; WER_RECOVERY_INFO RecoveryInfo; PWER_GATHER Gather; PWER_METADATA MetaData; PWER_RUNTIME_DLL RuntimeDll; PWER_DUMP_COLLECTION DumpCollection; LONG GatherCount; LONG MetaDataCount; LONG DumpCount; LONG Flags; WER_HEAP_MAIN_HEADER MainHeader; PVOID Reserved; } WER_PEB_HEADER_BLOCK, *PWER_PEB_HEADER_BLOCK;
A recovery callback can be installed using kernel32!RegisterApplicationRecoveryCallback. kernelbase!GetApplicationRecoveryCallback will read the Callback, Parameter, PingInterval and Flags from a remote process. kernel32!ApplicationRecoveryFinished can read if the Finished event is signalled. ApplicationRecoveryInProgress will determine if the InProgress event is signalled. Started is a handle, but I’m unsure what it’s for exactly.
typedef struct _WER_RECOVERY_INFO { ULONG Length; PVOID Callback; PVOID Parameter; HANDLE Started; HANDLE Finished; HANDLE InProgress; LONG LastError; BOOL Successful; DWORD PingInterval; DWORD Flags; } WER_RECOVERY_INFO, *PWER_RECOVERY_INFO;
As part of a report created by WER, kernelbase!WerRegisterMemoryBlock inserts information about a range of memory that should be included. It’s also possible to exclude a range of memory using kernelbase!WerRegisterExcludedMemoryBlock, which internally sets bit 15 of the Flags in a WER_GATHER structure. Files that might otherwise be excluded from a report can also be saved via kernelbase!WerRegisterFile.
typedef struct _WER_FILE { USHORT Flags; WCHAR Path[MAX_PATH]; } WER_FILE, *PWER_FILE; typedef struct _WER_MEMORY { PVOID Address; ULONG Size; } WER_MEMORY, *PWER_MEMORY; typedef struct _WER_GATHER { PVOID Next; USHORT Flags; union { WER_FILE File; WER_MEMORY Memory; } v; } WER_GATHER, *PWER_GATHER;
Applications can register custom meta data using kernelbase!WerRegisterCustomMetadata.
typedef struct _WER_METADATA { PVOID Next; WCHAR Key[64]; WCHAR Value[128]; } WER_METADATA, *PWER_METADATA;
Developers might want to customize the reporting process and that’s what kernelbase!WerRegisterRuntimeExceptionModule is for. It inserts the path of DLL into the registration data that’s loaded by werfault.exe once an exception occurs. In the WER_RUNTIME_DLL structure, MAX_PATH is used for CallbackDllPath, but the correct length for the structure and DLL should be read from the Length field.
typedef struct _WER_RUNTIME_DLL { PVOID Next; ULONG Length; PVOID Context; WCHAR CallbackDllPath[MAX_PATH]; } WER_RUNTIME_DLL, *PWER_RUNTIME_DLL;
If more than one process is required for dumping, an application can use kernelbase!WerRegisterAdditionalProcess to specify the process and thread ids. I’m open to correction, but it appears that only one thread per process is allowed by the API.
typedef struct _WER_DUMP_COLLECTION { PVOID Next; DWORD ProcessId; DWORD ThreadId; } WER_DUMP_COLLECTION, *PWER_DUMP_COLLECTION;
Finally, the main heap header used for dynamic allocation of memory for WER structures. The signature here should contain a string “HEAP_SIGNATURE”. The mutex is simply for exclusive access during allocations. FreeHeap may be inaccurate, but it appears to be used to improve performance of memory allocations. Instead of requesting a new block of memory from the OS, WER functions can use from this block if possible.
typedef struct _WER_HEAP_MAIN_HEADER { WCHAR Signature[16]; LIST_ENTRY Links; HANDLE Mutex; PVOID FreeHeap; ULONG FreeCount; } WER_HEAP_MAIN_HEADER, *PWER_HEAP_MAIN_HEADER;
The WER service could be a point of privilege escalation and lateral movement. There’s potential to use it for exfiltration of sensitive data by modifying information in the registry settings. An attacker may be capable of dumping a process and having a report sent to a server they control using the CorporateWERServer setting. They might also use their own public key to encrypt this data and prevent recovery of what exactly is being gathered. This is all hypothetical of course and I don’t know if it can actually be used for this.