Skip to content

Commit 6bcef0d

Browse files
committed
Add Windows Notification Facilities parse plugin
1 parent 5b15e31 commit 6bcef0d

File tree

2 files changed

+307
-0
lines changed

2 files changed

+307
-0
lines changed

Citronneur/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# volatility-wnf
2+
3+
See https://github.com/citronneur/volatility-wnf/ for update.
4+
5+
Browse and dump Windows Notification Facilities
6+
7+
This plugin is based on work of Alex Ionescu and Gabrielle Viala.
8+
9+
[https://blog.quarkslab.com/playing-with-the-windows-notification-facility-wnf.html]
10+
[https://www.blackhat.com/us-18/briefings/schedule/#the-windows-notification-facility-peeling-the-onion-of-the-most-undocumented-kernel-attack-surface-yet-11626]
11+
[https://www.youtube.com/watch?v=MybmgE95weo]
12+
13+
This plugin just walk through all process, or by filter one, and dump all subscribers.
14+
Additionnaly, it can dump associated data from a subscriber.
15+
16+
## Install
17+
18+
Please put *wnf.py* in your volatility plugin folder.
19+
20+
## Use
21+
22+
To dump all subscribers of all process
23+
```
24+
python vol.py -f your_dump --profile=your_profile wnf
25+
```
26+
27+
To dump all subscriber of a particular process
28+
```
29+
python vol.py -f your_dump --profile=your_profile wnf --pid PID
30+
```
31+
32+
To dump data associated to a particular subscriber
33+
```
34+
python vol.py -f your_dump --profile=your_profile wnfdata -s ADRESS_OF_SUBSCRIBER
35+
```
36+
37+
ADRESS_OF_SUBSCRIBER is the first field dump from wnf command.

Citronneur/wnf.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# Copyright (c) 2019, Sylvain Peyrefitte
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# 1. Redistributions of source code must retain the above copyright notice, this
8+
# list of conditions and the following disclaimer.
9+
#
10+
# 2. Redistributions in binary form must reproduce the above copyright notice,
11+
# this list of conditions and the following disclaimer in the documentation
12+
# and/or other materials provided with the distribution.
13+
#
14+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
25+
# Authors: Sylvain Peyrefitte <citronneur@gmail.com>
26+
27+
# Date: 2019-01-15
28+
# Version: 1.0
29+
#
30+
# Volatility Framework plugin to dump Structure associate with Windows Notification Facilities
31+
#
32+
# Usage:
33+
# 1) Move wnf.py to volatility/plugins in the
34+
# Volatilty Framework path.
35+
# 2) Run: python vol.py -f dump_from_windows_system.vmem
36+
# --profile=Selected_Profile wnf
37+
#------------------------------------------------------------------------------------
38+
39+
import volatility.plugins.common as common
40+
import volatility.utils as utils
41+
import volatility.win32 as win32
42+
import volatility.obj as obj
43+
from volatility.renderers import TreeGrid
44+
from volatility.renderers.basic import Address, Hex, Bytes
45+
46+
WNF_types = {
47+
# Header of every WNF struct
48+
'_WNF_CONTEXT_HEADER':[0x4, {
49+
'NodeTypeCode':[0x0, ['unsigned short']],
50+
'NodeByteSize':[0x2, ['unsigned short']]
51+
}],
52+
53+
'_WNF_PROCESS_CONTEXT':[0x88, {
54+
'Header': [0x0, ['_WNF_CONTEXT_HEADER']],
55+
'EProcess': [0x8, ['pointer64', ['_EPROCESS']]],
56+
'WnfContexts': [0x10, ['_LIST_ENTRY']],
57+
'Unk1ListHead': [0x40, ['_LIST_ENTRY']],
58+
'SubscriptionListHead': [0x58, ['_LIST_ENTRY']],
59+
'Unk2ListHead': [0x70, ['_LIST_ENTRY']],
60+
'Event': [0x80, ['pointer64', ['_KEVENT']]]
61+
}],
62+
63+
'_WNF_SUBSCRIPTION_CONTEXT':[0x88, {
64+
'Header': [0x0, ['_WNF_CONTEXT_HEADER']],
65+
'SubscriptionContexts': [0x18, ['_LIST_ENTRY']],
66+
'EProcess': [0x28, ['pointer', ['_EPROCESS']]],
67+
'NameInstance': [0x30, ['pointer', ['_WNF_NAME_INSTANCE_CONTEXT']]],
68+
'WnfId': [0x38, ['unsigned long long']],
69+
'NameSubscriptionContexts': [0x40, ['_LIST_ENTRY']]
70+
}],
71+
72+
'_WNF_NAME_INSTANCE_CONTEXT': [0xa8, {
73+
'Header': [0x0, ['_WNF_CONTEXT_HEADER']],
74+
'NameInstanceContexts': [0x10, ['_LIST_ENTRY']],
75+
'WnfId': [0x28, ['unsigned long long']],
76+
'ScopeInstance': [0x30, ['pointer', ['_WNF_SCOPE_INSTANCE_CONTEXT']]],
77+
'DataSize': [0x38, ['unsigned long long']], # Potential available data
78+
'WnfData': [0x58, ['pointer', ['_WNF_STATE_DATA']]],
79+
'NameSubscriptionContexts': [0x78, ['_LIST_ENTRY']], # List of subscription for this name
80+
}],
81+
82+
'_WNF_SCOPE_INSTANCE_CONTEXT': [0x50, {
83+
'Header': [0x0, ['_WNF_CONTEXT_HEADER']],
84+
'Scope': [0x10, ['unsigned int']],
85+
'ScopeInstanceContexts': [0x20, ['_LIST_ENTRY']],
86+
'ScopeMapInstanceHead': [0x28, ['pointer', ['void']]],
87+
'RootNameInstance': [0x38, ['pointer', ['void']]] # Pointer to root name instance tree
88+
}],
89+
90+
'_WNF_STATE_DATA': [None, {
91+
'Header': [0x0, ['_WNF_CONTEXT_HEADER']],
92+
'DataSize': [0x4, ['unsigned int']]
93+
}],
94+
95+
'_WNF_SCOPE_MAP_CONTEXT':[0x90, {
96+
'Header': [0x0, ['_WNF_CONTEXT_HEADER']],
97+
'ScopeInstanceContextsHead': [0x8, ['pointer', ['_WNF_SCOPE_INSTANCE_CONTEXT']]],
98+
'ScopeInstanceContextsTail': [0x10, ['pointer', ['_WNF_SCOPE_INSTANCE_CONTEXT']]]
99+
}]
100+
}
101+
class _WNF_SCOPE_INSTANCE_CONTEXT(obj.CType):
102+
"""
103+
Add some usefull function to access some importants elements
104+
"""
105+
def get_root_name_context_instance(self):
106+
"""
107+
This is because name instance is track over a tree
108+
:return: _WNF_NAME_INSTANCE_CONTEXT
109+
"""
110+
return obj.Object(
111+
'_WNF_NAME_INSTANCE_CONTEXT',
112+
offset = self.RootNameInstance.dereference().obj_offset - 0x10,
113+
vm = self.obj_vm,
114+
parent = self.obj_parent,
115+
native_vm = self.obj_native_vm,
116+
name = '_WNF_NAME_INSTANCE_CONTEXT'
117+
)
118+
119+
def get_scope_map_instance(self):
120+
"""
121+
Track scope instance
122+
:return: _WNF_SCOPE_MAP_CONTEXT
123+
"""
124+
return obj.Object(
125+
'_WNF_SCOPE_MAP_CONTEXT',
126+
offset=self.ScopeMapInstanceHead.dereference().obj_offset - 0x20,
127+
vm=self.obj_vm,
128+
parent=self.obj_parent,
129+
native_vm=self.obj_native_vm,
130+
name='_WNF_SCOPE_MAP_CONTEXT'
131+
)
132+
133+
class WnfObjectTypes(obj.ProfileModification):
134+
"""
135+
Update profile with WNF types
136+
"""
137+
conditions = {'os': lambda x: x == 'windows'}
138+
def modification(self, profile):
139+
profile.vtypes.update(WNF_types)
140+
profile.object_classes.update({
141+
'_WNF_SCOPE_INSTANCE_CONTEXT': _WNF_SCOPE_INSTANCE_CONTEXT
142+
})
143+
144+
class Wnf(common.AbstractWindowsCommand):
145+
"""
146+
Dump WNF name ids for a particular process
147+
"""
148+
149+
def __init__(self, config, *args, **kwargs):
150+
common.AbstractWindowsCommand.__init__(self, config, *args)
151+
config.add_option('pid', short_option='p', default=None, type = "int",
152+
help='PID of target process', action="store")
153+
154+
config.add_option('wnfid', short_option='i', default=None, type="str",
155+
help='ID of wnf name', action="store")
156+
157+
config.add_option('scope', short_option='s', default=None, type="str",
158+
help='WNF Scope address', action="store")
159+
160+
config.add_option('map', short_option='m', default=None, type="str",
161+
help='WNF Scope Map address', action="store")
162+
163+
164+
def calculate(self):
165+
"""
166+
Walk through all or specific processes
167+
"""
168+
addr_space = utils.load_as(self._config)
169+
for task in win32.tasks.pslist(addr_space):
170+
if self._config.pid is not None:
171+
if task.UniqueProcessId.v() == self._config.pid:
172+
yield task
173+
break
174+
else:
175+
yield task
176+
177+
def unified_output(self, data):
178+
return TreeGrid([
179+
("Subscriber", Address),
180+
("pid", int),
181+
("Process Name", str),
182+
("WnfName", Address),
183+
("WnfId", Hex),
184+
("Version", int),
185+
("LifeTime", int),
186+
("DataScope", int),
187+
("IsPermanent", int),
188+
("ScopeMap", Address),
189+
("Scope", Address),
190+
("ScopeType", int),
191+
("HasData", str),
192+
("DataSize", int)
193+
], self.generator(data))
194+
195+
def generator(self, tasks):
196+
for task in tasks:
197+
wnf_process_context = task.WnfContext.dereference_as('_WNF_PROCESS_CONTEXT')
198+
for subscriber in wnf_process_context.SubscriptionListHead.list_of_type("_WNF_SUBSCRIPTION_CONTEXT", "SubscriptionContexts"):
199+
# case of subscriber head
200+
if subscriber.Header.NodeTypeCode != 0x905:
201+
continue
202+
203+
# sometimes reference a not in memory object (explorer.exe)
204+
# very strange...
205+
if subscriber.NameInstance.Header.NodeTypeCode != 0x903:
206+
clear = subscriber.WnfId ^ 0x41C64E6DA3BC0074
207+
else:
208+
clear = subscriber.NameInstance.WnfId ^ 0x41C64E6DA3BC0074
209+
210+
if self._config.WNFID and int(self._config.wnfid, 16) != clear:
211+
continue
212+
213+
if self._config.Scope and int(self._config.scope, 16) != subscriber.NameInstance.ScopeInstance.v():
214+
continue
215+
216+
yield (0, [
217+
Address(subscriber.v()),
218+
task.UniqueProcessId.v(),
219+
str(task.ImageFileName),
220+
Address(subscriber.NameInstance),
221+
Hex(clear),
222+
int(clear & 0xf),
223+
int(clear >> 4 & 0x3),
224+
int(clear >> 6 & 0xf),
225+
int(clear >> 0xa & 0x1),
226+
Address(subscriber.NameInstance.ScopeInstance.get_scope_map_instance().v()),
227+
Address(subscriber.NameInstance.ScopeInstance),
228+
int(subscriber.NameInstance.ScopeInstance.Scope),
229+
str(bool(subscriber.NameInstance.WnfData)),
230+
int(subscriber.NameInstance.WnfData.DataSize)
231+
])
232+
233+
234+
def hexdump(src, length=16):
235+
"""
236+
Dump string into hex format
237+
:param src: memory
238+
:param length: length of line
239+
:return: human readable stringand hex
240+
"""
241+
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
242+
lines = []
243+
for c in xrange(0, len(src), length):
244+
chars = src[c:c+length]
245+
hex = ' '.join(["%02x" % ord(x) for x in chars])
246+
printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or '.') for x in chars])
247+
lines.append("%04x %-*s %s\n" % (c, length*3, hex, printable))
248+
return ''.join(lines)
249+
250+
class WnfData(common.AbstractWindowsCommand):
251+
"""
252+
Dump WNF data Associate to a subscriber
253+
"""
254+
255+
def __init__(self, config, *args, **kwargs):
256+
common.AbstractWindowsCommand.__init__(self, config, *args)
257+
258+
config.add_option('subscriber', short_option='s', default=None, type="str",
259+
help='Address of subsciber instance', action="store")
260+
261+
def calculate(self):
262+
addr_space = utils.load_as(self._config)
263+
for task in win32.tasks.pslist(addr_space):
264+
wnf_process_context = task.WnfContext.dereference_as('_WNF_PROCESS_CONTEXT')
265+
for subscriber in wnf_process_context.SubscriptionListHead.list_of_type("_WNF_SUBSCRIPTION_CONTEXT", "SubscriptionContexts"):
266+
if subscriber == int(self._config.subscriber, 16) and bool(subscriber.NameInstance.WnfData):
267+
return addr_space.read(subscriber.NameInstance.WnfData + 8, 4096)
268+
269+
def render_text(self, outfd, data):
270+
print hexdump(data)

0 commit comments

Comments
 (0)