Edited at

Subscribe a notification from WNF

More than 1 year has passed since last update.


Introduction

In Black Hat USA 2018, Alex Ionescu (@aionescu) unveiled a new toy: Windows Notification Facility.

The Windows Notification Facility: Peeling the Onion of the Most Undocumented Kernel Attack Surface Yet - Black Hat USA 2018 | Briefings Schedule

https://www.blackhat.com/us-18/briefings/schedule/index.html#the-windows-notification-facility-peeling-the-onion-of-the-most-undocumented-kernel-attack-surface-yet-11626

It seems that the current version of Windows kindly delivers push notifications to any user-mode processes and kernel drivers at tons of important events such as process creation, start menu tile installation, WiFi connection status change, and etc.

In the BH talk, he also introduced a tool Mach2, that can control thousands of prerelease features in Windows through WNF.

riverar/mach2: Feature Control Multi-tool

https://github.com/riverar/mach2

Based on the code of Mach2 and some simple kernel-debugging technique, let me introduce a small program to subscribe WNF and explain how I created it.


Analysis of the publisher code

Mach2 consumes two WNF-related system calls: NtUpdateWnfStateData and NtQueryWnfStateData. I think these names explain well: NtUpdateWnfStateData publishes a new state, which can be queried by NtQueryWnfStateData.

From the BH demo, I already knew Edge navigation pushes a notification and it can be subscribed. Actually it would be a good entrypoint of WNF, so let's analyze how Edge pushes a notification.

First, launch Edge and set a breakpoint at NtUpdateWnfStateData in Edge's manager process and content process.

0: kd> vertarget

Windows 10 Kernel Version 17134 MP (2 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 17134.1.amd64fre.rs4_release.180410-1804
Machine Name:
Kernel base = 0xfffff803`ec2aa000 PsLoadedModuleList = 0xfffff803`ec658170
Debug session time: Sat Aug 25 13:55:33.753 2018 (UTC - 7:00)
System Uptime: 0 days 0:24:36.324

0: kd> !process 0n4784 0
Searching for Process with Cid == 12b0
PROCESS ffffa185845d8080
SessionId: 1 Cid: 12b0 Peb: 3692e32000 ParentCid: 031c
DirBase: 27740000 ObjectTable: ffffdb09aa262e00 HandleCount: 1247.
Image: MicrosoftEdge.exe
0: kd> !process 0n4484 0
Searching for Process with Cid == 1184
PROCESS ffffa18584cb9580
SessionId: 1 Cid: 1184 Peb: bd30256000 ParentCid: 031c
DirBase: 69880000 ObjectTable: ffffdb09aa8a0440 HandleCount: 1014.
Image: MicrosoftEdgeCP.exe

0: kd> bp /p ffffa185845d8080 nt!NtUpdateWnfStateData
0: kd> bp /p ffffa18584cb9580 nt!NtUpdateWnfStateData
0: kd> bl
0 e fffff803`ec73cf0c 0001 (0001) nt!NtUpdateWnfStateData
Match process data ffffa185`845d8080
1 e fffff803`ec73cf0c 0001 (0001) nt!NtUpdateWnfStateData
Match process data ffffa185`84cb9580

When I did some navigation on the browser, the breakpoint at the manager process was hit. Looking at the callstack below, you can see EMODEL!CTabWindow::_OnNavigationComplete is calling ntdll!RtlPublishWnfStateData. So obviously Edge pushes a notification when navigation is completed.

Breakpoint 0 hit

nt!NtUpdateWnfStateData:
fffff803`ec73cf0c 4c8bdc mov r11,rsp
0: kd> !process -1 0
PROCESS ffffa185845d8080
SessionId: 1 Cid: 12b0 Peb: 3692e32000 ParentCid: 031c
DirBase: 27740000 ObjectTable: ffffdb09aa262e00 HandleCount: 1253.
Image: MicrosoftEdge.exe
0: kd> kL
Child-SP RetAddr Call Site
ffff848d`8332fa88 fffff803`ec463343 nt!NtUpdateWnfStateData
ffff848d`8332fa90 00007ffb`aa89d764 nt!KiSystemServiceCopyEnd+0x13
00000036`933fe7b8 00007ffb`aa87ca0b ntdll!NtUpdateWnfStateData+0x14
00000036`933fe7c0 00007ffb`893f3385 ntdll!RtlPublishWnfStateData+0x4b
00000036`933fe830 00007ffb`8945345b EMODEL!CTabWindow::_OnNavigationComplete+0x395
00000036`933fea60 00007ffb`8944fe61 EMODEL!Microsoft::WRL::Details::DelegateArgTraits<long (__cdecl ABI::Windows::Foundation::ITypedEventHandler_impl<ABI::Windows::Foundation::Internal::AggregateType<ABI::WebRuntime::BrowsingContext * __ptr64,ABI::WebRuntime::IBrowsingContext * __ptr64>,ABI::Windows::Foundation::Internal::AggregateType<ABI::WebRuntime::NavigationCompletedEventArgs * __ptr64,ABI::WebRuntime::INavigationCompletedEventArgs * __ptr64> >::*)(ABI::WebRuntime::IBrowsingContext * __ptr64,ABI::WebRuntime::INavigationCompletedEventArgs * __ptr64) __ptr64>::DelegateInvokeHelper<ABI::Windows::Foundation::ITypedEventHandler<ABI::WebRuntime::BrowsingContext * __ptr64,ABI::WebRuntime::NavigationCompletedEventArgs * __ptr64>,<lambda_24c54b165d2c35dbde8442e015c21012>,-1,ABI::WebRuntime::IBrowsingContext * __ptr64,ABI::WebRuntime::INavigationCompletedEventArgs * __ptr64>::Invoke+0x1b
00000036`933fea90 00007ffb`8940981e EMODEL!Microsoft::WRL::InvokeTraits<-2>::InvokeDelegates<<lambda_8860911b6575297232878b65e442a3ec>,Windows::Foundation::ITypedEventHandler<WebRuntime::BrowsingContext * __ptr64,WebRuntime::NavigationCompletedEventArgs * __ptr64> >+0x65
00000036`933feaf0 00007ffb`89410159 EMODEL!WebRuntime::BrowsingContext::InvokeNavigationCompletedEvent+0xee
00000036`933feb70 00007ffb`8940fbfa EMODEL!BrowserCouplingIsoSink::HandleDocumentComplete+0xe5
00000036`933febe0 00007ffb`8940f449 EMODEL!BrowserCouplingIsoSink::LCIEWinProc+0x37a
00000036`933fec90 00007ffb`8c5fcccf EMODEL!LCIEBrowserCouplingIsoSink_WinProc+0x1a9
00000036`933fed40 00007ffb`894ae9ff edgeIso!IsoDispatchMessageToArtifacts+0x54f
00000036`933fee50 00007ffb`894abf92 EMODEL!LCIEDispatchToArtifactsOnThread+0xf
00000036`933fee80 00007ffb`a4802cc6 EMODEL!Microsoft::WRL::Details::DelegateArgTraits<long (__cdecl ABI::Windows::System::IDispatcherQueueHandler::*)(void) __ptr64>::DelegateInvokeHelper<Microsoft::WRL::Implements<Microsoft::WRL::RuntimeClassFlags<2>,ABI::Windows::System::IDispatcherQueueHandler,Microsoft::WRL::FtmBase>,<lambda_8904907745c6387a83a814456134490b>,-1>::Invoke+0x12
00000036`933feeb0 00007ffb`a47c8c7a CoreMessaging!Windows::System::DispatcherQueue::DeferInvokeCallback+0x16
00000036`933feee0 00007ffb`a47ed0ac CoreMessaging!System__Action$CallbackThunk+0xea
00000036`933fef50 00007ffb`a47eca69 CoreMessaging!Microsoft::CoreUI::Dispatch::DeferredCall::Callback_Dispatch+0x22c
00000036`933fefc0 00007ffb`a47d288f CoreMessaging!Microsoft::CoreUI::Dispatch::DeferredCallDispatcher::Callback_OnDispatch+0xe9
00000036`933ff020 00007ffb`a47d0d18 CoreMessaging!Microsoft::CoreUI::Dispatch::EventLoop::Callback_RunCoreLoop+0x60f
00000036`933ff110 00007ffb`a47cce09 CoreMessaging!Microsoft::CoreUI::Dispatch::UserAdapter::OnUserDispatch+0x1e8
00000036`933ff1e0 00007ffb`a47ccc08 CoreMessaging!Microsoft::CoreUI::Dispatch::UserAdapter_DoWork+0xf9
00000036`933ff2c0 00007ffb`aa5d6cc1 CoreMessaging!Microsoft::CoreUI::Dispatch::UserAdapter_WindowProc+0x58
00000036`933ff2f0 00007ffb`aa5d6693 USER32!UserCallWinProcCheckWow+0x2c1
00000036`933ff480 00007ffb`a47cfef4 USER32!DispatchMessageWorker+0x1c3
00000036`933ff510 00007ffb`a47d006a CoreMessaging!Microsoft::CoreUI::Dispatch::UserAdapter::DispatchNextUserQueueItem+0x154
00000036`933ff5c0 00007ffb`a47d1174 CoreMessaging!Microsoft::CoreUI::Dispatch::UserAdapter::RunIntegratedLoop+0x5a
00000036`933ff680 00007ffb`a47d2fc7 CoreMessaging!Microsoft::CoreUI::Dispatch::UserAdapter::Callback_HostModeRun+0x78
00000036`933ff6c0 00007ffb`a47fe815 CoreMessaging!Microsoft::CoreUI::Dispatch::EventLoop::Callback_Run+0x177
00000036`933ff740 00007ffb`a47c636b CoreMessaging!Microsoft::CoreUI::Messaging::MessageSession$R::Microsoft__CoreUI__IExportMessageSession_Impl::Run+0x35
00000036`933ff780 00007ffb`a4842c0c CoreMessaging!Microsoft::CoreUI::IExportMessageSession$X__ExportAdapter::Run+0x4b
00000036`933ff820 00007ffb`a483f826 CoreMessaging!Windows::System::DispatcherQueue::RunLoop+0x64
00000036`933ff880 00007ffb`a7ea3034 CoreMessaging!Windows::System::DispatcherQueueController::DispatcherQueueThreadProc+0x136
00000036`933ff8f0 00007ffb`aa871431 KERNEL32!BaseThreadInitThunk+0x14
00000036`933ff920 00000000`00000000 ntdll!RtlUserThreadStart+0x21

Let's take a closer look at what is being passed from emodel.dll to ntdll.dll, by setting a breakpoint at the call instruction to RtlPublishWnfStateData as follows.

0: kd> ub 00007ffb`893f3385

EMODEL!CTabWindow::_OnNavigationComplete+0x36d:
00007ffb`893f335d 488bcf mov rcx,rdi
00007ffb`893f3360 ff156ad45600 call qword ptr [EMODEL!_imp_wcslen (00007ffb`899607d0)]
00007ffb`893f3366 4c8d0c4502000000 lea r9,[rax*2+2]
00007ffb`893f336e 4c897c2420 mov qword ptr [rsp+20h],r15
00007ffb`893f3373 4c8bc7 mov r8,rdi
00007ffb`893f3376 33d2 xor edx,edx
00007ffb`893f3378 488b0d89965b00 mov rcx,qword ptr [EMODEL!WNF_EDGE_LAST_NAVIGATED_HOST (00007ffb`899aca08)]
00007ffb`893f337f ff15d3d55600 call qword ptr [EMODEL!_imp_RtlPublishWnfStateData (00007ffb`89960958)]
0: kd> bc*
0: kd> bp /p ffffa185`845d8080 00007ffb`893f337f
0: kd> bl
0 e 00007ffb`893f337f 0001 (0001) EMODEL!CTabWindow::_OnNavigationComplete+0x38f
Match process data ffffa185`845d8080
0: kd> g
Breakpoint 0 hit
EMODEL!CTabWindow::_OnNavigationComplete+0x38f:
0033:00007ffb`893f337f ff15d3d55600 call qword ptr [EMODEL!_imp_RtlPublishWnfStateData (00007ffb`89960958)]

Now we're in EMODEL. Let's see what we have right now.

1: kd> ub . l10

EMODEL!CTabWindow::_OnNavigationComplete+0x34c:
0033:00007ffb`893f333c 488b93a8130000 mov rdx,qword ptr [rbx+13A8h]
0033:00007ffb`893f3343 488d4da0 lea rcx,[rbp-60h]
0033:00007ffb`893f3347 e8b0560800 call EMODEL!CUString::CUString (00007ffb`894789fc)
0033:00007ffb`893f334c 90 nop
0033:00007ffb`893f334d 44397c2430 cmp dword ptr [rsp+30h],r15d
0033:00007ffb`893f3352 7c32 jl EMODEL!CTabWindow::_OnNavigationComplete+0x396 (00007ffb`893f3386)
0033:00007ffb`893f3354 488b7dc0 mov rdi,qword ptr [rbp-40h]
0033:00007ffb`893f3358 4885ff test rdi,rdi
0033:00007ffb`893f335b 7429 je EMODEL!CTabWindow::_OnNavigationComplete+0x396 (00007ffb`893f3386)
0033:00007ffb`893f335d 488bcf mov rcx,rdi
0033:00007ffb`893f3360 ff156ad45600 call qword ptr [EMODEL!_imp_wcslen (00007ffb`899607d0)]
0033:00007ffb`893f3366 4c8d0c4502000000 lea r9,[rax*2+2]
0033:00007ffb`893f336e 4c897c2420 mov qword ptr [rsp+20h],r15
0033:00007ffb`893f3373 4c8bc7 mov r8,rdi
0033:00007ffb`893f3376 33d2 xor edx,edx
0033:00007ffb`893f3378 488b0d89965b00 mov rcx,qword ptr [EMODEL!WNF_EDGE_LAST_NAVIGATED_HOST (00007ffb`899aca08)]
1: kd> r
rax=000000000000000e rbx=0000019af44cbf90 rcx=04810a28a3bc08f5
rdx=0000000000000000 rsi=0000019af4e43f1c rdi=0000019af4e4e280
rip=00007ffb893f337f rsp=00000036933fe830 rbp=00000036933fe930
r8=0000019af4e4e280 r9=000000000000001e r10=00000fff736ec53a
r11=5555500000000001 r12=0000000000000000 r13=00007ffb899802d8
r14=0000019af3fb1de0 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
EMODEL!CTabWindow::_OnNavigationComplete+0x38f:
0033:00007ffb`893f337f ff15d3d55600 call qword ptr [EMODEL!_imp_RtlPublishWnfStateData (00007ffb`89960958)] ds:002b:00007ffb`89960958={ntdll!RtlPublishWnfStateData (00007ffb`aa87c9c0)}

The first parameter is a strange 64bit integer 04810a28a3bc08f5, that was stored at EMODEL!WNF_EDGE_LAST_NAVIGATED_HOST. This should represent the structure WNF_STATE_NAME, which was used in Mach2. I don't remember well, but Alex explained this is not just a random number but generated from some human-friendly information and xor'ed with a magic number (probably to obfuscate?). Anyway, what is important here is 04810a28a3bc08f5 is a notification ID and you can get it from the symbol EMODEL!WNF_EDGE_LAST_NAVIGATED_HOST.

The second parameter is just 0 and I don't know what it means.

To understand the third and fourth parameter, the function call to wcslen at 00007ffb`893f3360 is a great hint. After wcslen, the fourth parameter r9 is set to rax (= return value from wcslen) * 2 + 2 at 00007ffb`893f3366. So r9 is the size of a UTF-16 string buffer. And, the third parameter r8 is moved from rdi that was passed to wcslen. So the third parameter is a string. Let's check it out.

1: kd> du /c 100 @r8

0000019a`f4e4e280 "www.kernel.org"

Great. It's confirmed that Edge is surely pushing a hostname to WNF.


Hand-decompile the subscriber code

Mach2 does not depend on subscribing part of WNF, but we already learned NtQueryWnfStateData, NtUpdateWnfStateData, and RtlPublishWnfStateData are all implemented in ntdll. It's natural that a subscribing function is also implemented in ntdll.dll. Let's start with querying ntdll's functions in the symbol.

1: kd> x ntdll!*WnfState*

00007ffb`aa8821e0 ntdll!RtlTestAndPublishWnfStateData (void)
00007ffb`aa842144 ntdll!RtlpSubscribeWnfStateChangeNotificationInternal (void)
00007ffb`aa87c9c0 ntdll!RtlPublishWnfStateData (void)
00007ffb`aa8a29f3 ntdll!RtlQueryWnfStateData$filt$0 (void)
00007ffb`aa8a2c47 ntdll!RtlQueryWnfStateDataWithExplicitScope$filt$0 (void)
00007ffb`aa89bbd0 ntdll!ZwGetCompleteWnfStateSubscription (<no parameter info>)
00007ffb`aa89d4d0 ntdll!ZwSubscribeWnfStateChange (<no parameter info>)
00007ffb`aa89b730 ntdll!ZwCreateWnfStateName (<no parameter info>)
00007ffb`aa89b8d0 ntdll!NtDeleteWnfStateName (<no parameter info>)
00007ffb`aa89b730 ntdll!NtCreateWnfStateName (<no parameter info>)
00007ffb`aa89b8d0 ntdll!ZwDeleteWnfStateName (<no parameter info>)
00007ffb`aa89bbd0 ntdll!NtGetCompleteWnfStateSubscription (<no parameter info>)
00007ffb`aa89d4d0 ntdll!NtSubscribeWnfStateChange (<no parameter info>)
00007ffb`aa89ca10 ntdll!ZwQueryWnfStateNameInformation (<no parameter info>)
00007ffb`aa89b8b0 ntdll!ZwDeleteWnfStateData (<no parameter info>)
00007ffb`aa89c9f0 ntdll!NtQueryWnfStateData (<no parameter info>)
00007ffb`aa89d750 ntdll!NtUpdateWnfStateData (<no parameter info>)
00007ffb`aa89ca10 ntdll!NtQueryWnfStateNameInformation (<no parameter info>)
00007ffb`aa89c9f0 ntdll!ZwQueryWnfStateData (<no parameter info>)
00007ffb`aa89b8b0 ntdll!NtDeleteWnfStateData (<no parameter info>)
00007ffb`aa89d750 ntdll!ZwUpdateWnfStateData (<no parameter info>)
00007ffb`aa842100 ntdll!RtlSubscribeWnfStateChangeNotification (<no parameter info>)
00007ffb`aa842970 ntdll!RtlUnsubscribeWnfStateChangeNotification (<no parameter info>)
00007ffb`aa89d730 ntdll!ZwUnsubscribeWnfStateChange (<no parameter info>)
00007ffb`aa881440 ntdll!RtlQueryWnfStateDataWithExplicitScope (<no parameter info>)
00007ffb`aa87b540 ntdll!RtlQueryWnfStateData (<no parameter info>)
00007ffb`aa89d730 ntdll!NtUnsubscribeWnfStateChange (<no parameter info>)

Easy. Obviously ntdll!NtSubscribeWnfStateChange is a system call entrypoint to subscribe WNF. The next step is to set a breakpoint at the kernel system call nt!NtSubscribeWnfStateChange.

The function nt!NtSubscribeWnfStateChange is aggressively called from many places even without doing any operations. I picked up one of those callers RPCRT4!LoadFirewallExtensionIfNecessary because it looks straightforward to hand-decompile. Here's its snippet calling ntdll!RtlSubscribeWnfStateChangeNotification that ends up calling nt!NtSubscribeWnfStateChange.

RPCRT4!LoadFirewallExtensionIfNecessary+0x21:

00007ffb`5b1db45d 488b15bc750e00 mov rdx,qword ptr [RPCRT4!WNF_RPCF_FWMAN_RUNNING (00007ffb`5b2c2a20)]
00007ffb`5b1db464 4c8d05b5dc0500 lea r8,[RPCRT4!FwManInstalledStateChangeNotification (00007ffb`5b239120)]
00007ffb`5b1db46b 488364242000 and qword ptr [rsp+20h],0
00007ffb`5b1db471 488d4c2450 lea rcx,[rsp+50h]
00007ffb`5b1db476 4533c9 xor r9d,r9d
00007ffb`5b1db479 c705ed2e100002000000 mov dword ptr [RPCRT4!gFwExtensionState (00007ffb`5b2de370)],2
00007ffb`5b1db483 ff15372c0e00 call qword ptr [RPCRT4!_imp_RtlQueryWnfStateData (00007ffb`5b2be0c0)]
00007ffb`5b1db489 8bd0 mov edx,eax
00007ffb`5b1db48b 85c0 test eax,eax
00007ffb`5b1db48d 0f88b5db0600 js RPCRT4!LoadFirewallExtensionIfNecessary+0x6dc0c (00007ffb`5b249048)

RPCRT4!LoadFirewallExtensionIfNecessary+0x57:
00007ffb`5b1db493 448b442450 mov r8d,dword ptr [rsp+50h]
00007ffb`5b1db498 4c8d0d81dc0500 lea r9,[RPCRT4!FwManInstalledStateChangeNotification (00007ffb`5b239120)]
00007ffb`5b1db49f 488b157a750e00 mov rdx,qword ptr [RPCRT4!WNF_RPCF_FWMAN_RUNNING (00007ffb`5b2c2a20)]
00007ffb`5b1db4a6 488d0ddb2b1000 lea rcx,[RPCRT4!gFwManStateNotification (00007ffb`5b2de088)]
00007ffb`5b1db4ad c744243801000000 mov dword ptr [rsp+38h],1
00007ffb`5b1db4b5 8364243000 and dword ptr [rsp+30h],0
00007ffb`5b1db4ba 488364242800 and qword ptr [rsp+28h],0
00007ffb`5b1db4c0 488364242000 and qword ptr [rsp+20h],0
00007ffb`5b1db4c6 ff15ec2b0e00 call qword ptr [RPCRT4!_imp_RtlSubscribeWnfStateChangeNotification (00007ffb`5b2be0b8)]
00007ffb`5b1db4cc 8bd0 mov edx,eax
00007ffb`5b1db4ce 85c0 test eax,eax
00007ffb`5b1db4d0 0f8872db0600 js RPCRT4!LoadFirewallExtensionIfNecessary+0x6dc0c (00007ffb`5b249048)

You can see it calls RtlQueryWnfStateData with WNF_RPCF_FWMAN_RUNNING first, and then passes a 32bit integer stored in rsp+50h to ntdll!RtlSubscribeWnfStateChangeNotification. The function RPCRT4!FwManInstalledStateChangeNotification should be a callback function from WNF.

Considering Windows x64 calling convention, you can assume RtlQueryWnfStateData takes five parameters and RtlSubscribeWnfStateChangeNotification takes eight. You can confirm it from the 32bit assembly. In addition, the import library ntdll.lib in Windows Kit contains the symbols name _RtlQueryWnfStateData@24 and _RtlSubscribeWnfStateChangeNotification@36, from which you can know the total size of parameters of each API. This is also a great hint.

Let's take a look at 32bit assembly of the callback function RPCRT4!FwManInstalledStateChangeNotification.

rpcrt4!FwManInstalledStateChangeNotification:

4efbede0 8bff mov edi,edi
4efbede2 55 push ebp
4efbede3 8bec mov ebp,esp
4efbede5 837d1000 cmp dword ptr [ebp+10h],0
4efbede9 0f85bd080200 jne rpcrt4!FwManInstalledStateChangeNotification+0x208cc (4efdf6ac)

rpcrt4!FwManInstalledStateChangeNotification+0xf:
4efbedef 33c0 xor eax,eax

rpcrt4!FwManInstalledStateChangeNotification+0x11:
4efbedf1 5d pop ebp
4efbedf2 c21c00 ret 1Ch

rpcrt4!FwManInstalledStateChangeNotification+0x208cc:
4efdf6ac e80a750000 call rpcrt4!LoadFirewallExtensionInternal (4efe6bbb)
...

It ends with ret 1C, which means it's stdcall and the total size of parameters is 0x1C bytes. If you set a breakpoint at any WNF callback function and check the first parameter value, you'll see it's the value of notification ID.

So, the prototypes should be something like this:

NTSTATUS NTAPI WnfCallback(uint64_t, void*, void*, void*, void*, void*);

extern "C" {
NTSTATUS NTAPI RtlQueryWnfStateData(uint32_t*,
uint64_t,
decltype(WnfCallback),
uint64_t);
NTSTATUS NTAPI
RtlSubscribeWnfStateChangeNotification(void*,
uint64_t,
uint32_t,
decltype(WnfCallback),
size_t,
size_t,
size_t,
size_t);
}


Monitor Edge navigation

We've got a notification ID 04810a28a3bc08f5 for Edge navigation, and the function prototypes. I created a simple console program using these information and uploaded its source code to the gist. To build, clone it and just run NMAKE on the "Native Tools Command Prompt" of your Visual Studio.

https://gist.github.com/msmania/472912cd6e9ab067be3211ba3f5f0f9e

Below is the screenshot. Both 32bit and 64bit process can receive the same notifications from 64bit Edge. It looks like InPrivate mode does not push notifications.

wnf.png


Wrapping Up

I believe it's worth spending some time on analyzing or fuzzing these WNF as Alex mentioned. Even without putting on a security hat, there may be an interesting notification you can use to build your tool though using this kind of undocumented APIs is not supported.