I’m happy to announce the release of several plugins for Volatility 3 that allow you to dig deeper into the memory analysis. One of those plugins is PteMalfind
, which is essentially an improved version of malfind
. Another one is PteResolve
which, similarly to the WinDBG command !pte
, allows you to inspect Page Table Entry (PTE) information for e.g., a given virtual address. In this blog post we will have a closer look at these and more plugins, and the PteEnumerator
base class and what you can do with it. The memory dump used for this blog post is available here. Some of the injection tools used in this blog post can be gathered from here.
Table of Contents
PteMalfind
PteEnumerator
Some issues from the PteEnumerator journey
ApiSearch
PteMalfind
The PteMalfind
plugin is based on research done back in 2019 (Paper, Talk, Github Repo) and basically the next evolution from the initial ptenum
plugin (which has been renamed to PteMalfind
).
TL;DR: PteEnumerator
enumerates all PTEs for every given process and returns a pre-analyzed representation of them (more details below). PteMalfind
uses PteEnumerator
to identify pages that are executable and solves the problem of identifying injected code residing in VADs not marked as executable (for a detailed explanation see the Paper).
The “normal” scenario is injected code (e.g. shellcode or the result of reflective DLL injection), allocated for example via VirtualAllocEx
and a protection of PAGE_EXECUTE_READWRITE
. When searching for the result of reflective DLL injection with malfind
, the output is as expected with the beginning of a PE file (output stripped to make it more readable):
volatility3 -f mem.dump windows.malfind --pid 5376
PID Start VPN End VPN Protection PrivateMemory
5376 0x26d0f360000 0x26d0f374fff PAGE_EXECUTE_READWRITE 1
4d 5a 90 00 03 00 00 00 MZ......
04 00 00 00 ff ff 00 00 ........
b8 00 00 00 00 00 00 00 ........
40 00 00 00 00 00 00 00 @.......
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 e8 00 00 00 ........ |
0x26d0f360000: pop r10
0x26d0f360002: nop
0x26d0f360003: add byte ptr [rbx], al
0x26d0f360005: add byte ptr [rax], al
0x26d0f360007: add byte ptr [rax + rax], al
0x26d0f36000a: add byte ptr [rax], al
PID Start VPN End VPN Protection PrivateMemory
5376 0x26d0f380000 0x26d0f39afff PAGE_EXECUTE_READWRITE 1
4d 5a 90 00 03 00 00 00 MZ......
04 00 00 00 ff ff 00 00 ........
b8 00 00 00 00 00 00 00 ........
40 00 00 00 00 00 00 00 @.......
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 e8 00 00 00 ........ |
0x26d0f380000: pop r10
0x26d0f380002: nop
0x26d0f380003: add byte ptr [rbx], al
0x26d0f380005: add byte ptr [rax], al
0x26d0f380007: add byte ptr [rax + rax], al
0x26d0f38000a: add byte ptr [rax], al
The situation, however, changes, when VADs without such a protection still contain executable code. For these cases, malfind
will not include these VADs in its result. Such VADs might occur in the context of an exploit, where injected code (e.g., on the heap or stack; take a look at pid 4048 in the memory dump) is set executable afterwards (the VAD’s protection still states non-executable). Another way to achieve this is to simply call VirtualAllocEx
with an initial protection of e.g., PAGE_READONLY
and changing the protection of the VAD/page(s) right afterwards to PAGE_EXECUTE_READWRITE
. In all these cases, malfind
will not show these memory regions, even though they are executable. The github repository for the DFRWS USA 2019 contains several injection examples with such a modification. Within those, there is also a modification of the original reflective DLL injection project by Stephen Fewer with pre-built binaries. For this scenario we started a notepad instance (PID 1260) and injected code with the following command:
reflectiveDLL_m-injector.x64.exe 1260 reflectiveDLL_m.x64.dll
The result can now be analyzed with the PteMalfind
plugin (output stripped to make it more readable):
volatility3 -f mem.dump windows.ptemalfind --pid 1260
PID Start Addr End Addr Protection Type
1260 0x15318410000 0x15318426fff PAGE_READONLY PrivateMemory
Meta Info:
22 non empty page(s) (starting at 0x15318411000) with a total size of 0x16000 bytes in this VAD were executable (and for mapped image files also modified).
Skipping the first 0x1000 bytes, as they are either not modified (only applies for mapped image files), empty, not executable or skipped via cmdline argument.
4d 5a 90 00 03 00 00 00 MZ......
04 00 00 00 ff ff 00 00 ........
b8 00 00 00 00 00 00 00 ........
40 00 00 00 00 00 00 00 @.......
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 10 01 00 00 ........ |
0x15318411000: pop r10
0x15318411002: nop
0x15318411003: add byte ptr [rbx], al
0x15318411005: add byte ptr [rax], al
0x15318411007: add byte ptr [rax + rax], al
0x1531841100a: add byte ptr [rax], al |
1260 0x15318530000 0x1531854afff PAGE_READONLY PrivateMemory
Meta Info:
27 non empty page(s) (starting at 0x15318530000) with a total size of 0x1b000 bytes in this VAD were executable (and for mapped image files also modified).
4d 5a 90 00 03 00 00 00 MZ......
04 00 00 00 ff ff 00 00 ........
b8 00 00 00 00 00 00 00 ........
40 00 00 00 00 00 00 00 @.......
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 10 01 00 00 ........ |
0x15318530000: pop r10
0x15318530002: nop
0x15318530003: add byte ptr [rbx], al
0x15318530005: add byte ptr [rax], al
0x15318530007: add byte ptr [rax + rax], al
0x1531853000a: add byte ptr [rax], al |
As not all pages in the respective VAD might be executable, the “Meta Info” message tells you how many pages/bytes in this VAD are executable.
Another example is Phantom DLL Hollowing. This injector loads a DLL and replaces parts of its .text
section with shellcode and executes it. As this modification of the DLL leads to an unset PrototypePte
flag, we can detect this and reveal the executable and modified page. For that we have to use a plugin-option called --include-image-files
. As the name suggests, this option will also include executable and modified pages from mapped image files. Per default, this is deactivated as it can lead in some cases to many false positives.
volatility3 -f mem.dump windows.ptemalfind --pid 3456 --include-image-files
PID Start Addr End Addr Protection Type
3456 0x7fff7b5e0000 0x7fff7b655fff PAGE_EXECUTE_WRITECOPY Mapped Image File: \Windows\System32\aadauthhelper.dll
fc 48 83 e4 f0 eb 17 75 .H.....u
73 65 72 33 32 00 48 65 ser32.He
6c 6c 6f 20 66 72 6f 6d llo.from
20 45 52 4e 57 00 e8 c8 .ERNW...
00 00 00 41 51 41 50 52 ...AQAPR
51 56 48 31 d2 65 48 8b QVH1.eH.
52 60 48 8b 52 18 48 8b R`H.R.H.
52 20 48 8b 72 50 48 0f R.H.rPH.
0x7fff7b5e1000: cld
0x7fff7b5e1001: and rsp, 0xfffffffffffffff0
0x7fff7b5e1005: jmp 0x7fff7b5e101e
0x7fff7b5e1007: jne 0x7fff7b5e107c
0x7fff7b5e1009: jb 0x7fff7b5e103f
The unset PrototypePte
flag can be translated to: The content of the DLL for this page is no longer the same as it used to be (see the Paper, Section 2.3). Or a bit more technical: The page’s content is not anymore the same as its counterpart in the corresponding ImageSectionObject
. More details on this in a follow up blog post about Process Hollowing.
PteEnumerator, PteResolve and SimplePteEnumerator
Now let’s have a closer look at PteEnumerator
, how it can be used in plugins, the internal PTE representation and some generic analysis examples. As mentioned earlier, PteEnumerator
implements the logic to enumerate PTEs for a given process and pre-analyzes them into an instance of the PteRun
class. This instance holds various details about the PTE, amongst others its offset, value, and parsed information such as its state and the physical offset resp. the swap/pagefile offset, and offers several functions to further analyze it. For instance:
- Returning the executable state.
- Whether the corresponding page contains any data or only null bytes.
- Reading the content of the page
- Returning the associated
MMPFN
entry. - Returning the containing VAD object.
The internal representation can simply be printed:
In [1]: print(pte_run)
PteRun:
PID: 3456
vaddr: 0x7ffe0000
phys_offset: 0x122f000
length: 0x1000
pte_value: 0x920000000122f005
pte_paddr: 0x4c0f00
pte_vaddr: 0xa980003fff00
is_proto: Undetermined
is_proto_ptr: False
has_proto_set: True
state: HARD
is_exec: False
From top to bottom, the fields are:
PID
: The process ID this PTE belongs to.vaddr
: The virtual address that resolves to this PTE.phys_offset
: When the PTE is either inHARD
(_MMPTE_HARDWARE
) orTRANS
(_MMPTE_TRANSITION
) state, this field points to the actual physical page.length
: The size/length of the page (there can also be large/huge pages).pte_value
: The PTE’s value.pte_paddr
: The physical address of the PTE itself, so where the actual PTE value is stored (this will be adjusted soon, as the PTE can also be swapped).pte_vaddr
: The virtual address of the PTE.is_proto
: Whether or not this PTE is a Prototype PTE, in the sense of that it is not a MMU PTE (part of the paging structures used by the MMU) but is part of the PTEs, referenced bySUBSECTION
structures. This flag is typically set when we resolved a proto-pointer first. More on this in the next example.is_proto_ptr
: Is set if the PTE is just a pointer to a Prototype PTE. More on this in the next example.has_proto_set
: This is set to True, if the MMPFN entry for the page described by this PTE has thePrototypePte
flag set. This flag has already been described in Section PteMalfind. Noteworthy here is that this does not necessarily mean the PTE is a Prototype PTE (in the sense of not a MMU PTE), but rather that there is still a similar Prototype PTE (pointed to by aSUBSECTION
), describing the same page. For more details see the Windows Internals books or the great “What Makes It Page?: The Windows 7 (x64) Virtual Memory Manager” book by Enrico Martignetti.state
: The PTE’s state in an internal representation (related to the original MMPTE states), mainly because we differentiate two sub-states for the_MMPTE_SOFTWARE
and_MMPTE_PROTOTYPE
state.is_exec
: Whether or not the described page is executable.
To further investigate the is_proto
and is_proto_ptr
flags, let’s have a look at the next example:
PteRun:
PID: 3456
vaddr: 0x2d54c146000
phys_offset: 0x476c000
length: 0x1000
pte_value: 0x8a0000000476c821
pte_paddr: 0x9be5130
pte_vaddr: 0xaf8580e02130
is_proto: True
is_proto_ptr: False
has_proto_set: True
state: HARD
is_exec: False
Page is for this process executable: False
Protopointer (the actual MMU PTE):
PteRun:
PID: 3456
vaddr: 0x2d54c146000
phys_offset: None
length: 0x1000
pte_value: 0xffffffff00000430
pte_paddr: 0x1c933a30
pte_vaddr: 0xa9816aa60a30
is_proto: False
is_proto_ptr: True
has_proto_set: Undetermined
state: PROTOVAD
is_exec: False
Here, we now have two PteRun
instances. The second/lower one, is the actual MMU PTE (so the PTE, part of the processes’ paging structures), which in this case is a Protopointer: It points to a Prototype PTE, residing somewhere outside the paging structures. The first/upper PteRun
describes a PTE in _MMPTE_HARDWARE
state, pointing to a valid page.
This means: The page is still valid and residing in RAM, but not from the perspective of the process. As soon as the process accesses the virtual address, the second PTE will be overwritten with the first one, making the page valid again for this process, but up until this point, the page’s content is only accessible by dereferencing the Protopointer.
The PteRun
class also offers a string function that returns a more verbose output for a given PteRun
, especially containing the MMPTE struct:
In [2]: print(pte_run.get_full_string_repr())
Internal PteRun representation:
==============================
PteRun:
PID: 3456
vaddr: 0x2d54c146000
phys_offset: 0x476c000
length: 0x1000
pte_value: 0x8a0000000476c821
pte_paddr: 0x9be5130
pte_vaddr: 0xaf8580e02130
is_proto: True
is_proto_ptr: False
has_proto_set: True
state: HARD
is_exec: False
Page is for this process executable: False
Protopointer (the actual MMU PTE):
PteRun:
PID: 3456
vaddr: 0x2d54c146000
phys_offset: None
length: 0x1000
pte_value: 0xffffffff00000430
pte_paddr: 0x1c933a30
pte_vaddr: 0xa9816aa60a30
is_proto: False
is_proto_ptr: True
has_proto_set: Undetermined
state: PROTOVAD
is_exec: False
MMPTE struct:
=============
[_MMPTE_HARDWARE] @ 0x9be5130
0x0 Accessed 0x1 bitfield (bit 5)
0x0 CacheDisable 0x0 bitfield (bit 4)
0x0 CopyOnWrite 0x0 bitfield (bit 9)
0x0 Dirty 0x0 bitfield (bit 6)
0x0 Dirty1 0x0 bitfield (bit 1)
0x0 Global 0x0 bitfield (bit 8)
0x0 LargePage 0x0 bitfield (bit 7)
0x0 NoExecute 0x1 bitfield (bit 63)
0x0 Owner 0x0 bitfield (bit 2)
0x0 PageFrameNumber 0x476c bitfield (bits 12-48)
0x0 ReservedForHardware 0x0 bitfield (bits 48-52)
0x0 ReservedForSoftware 0x0 bitfield (bits 52-56)
0x0 Unused 0x0 bitfield (bit 10)
0x0 Valid 0x1 bitfield (bit 0)
0x0 Write 0x1 bitfield (bit 11)
0x0 WriteThrough 0x0 bitfield (bit 3)
0x0 WsleAge 0xa bitfield (bits 56-60)
0x0 WsleProtection 0x0 bitfield (bits 60-63)
Corresponding ProtoPointer:
---------------------------
[_MMPTE_PROTOTYPE] @ 0x1c933a30
0x0 Combined 0x0 bitfield (bit 11)
0x0 DemandFillProto 0x0 bitfield (bit 1)
0x0 HiberVerifyConverted 0x0 bitfield (bit 2)
0x0 Protection 0x1 bitfield (bits 5-10)
0x0 ProtoAddress 0xffffffff0000 bitfield (bits 16-64)
0x0 Prototype 0x1 bitfield (bit 10)
0x0 ReadOnly 0x0 bitfield (bit 3)
0x0 SwizzleBit 0x1 bitfield (bit 4)
0x0 Unused1 0x0 bitfield (bits 12-16)
0x0 Valid 0x0 bitfield (bit 0)
There are also two plugins, which ease the PTE printing/analysis: PteResolve
and SimplePteEnumerator
. PteResolve
is similar to WinDbg’s !pte extension and allows to resolve a PTE from multiple given values: A virtual address, the physical/virtual PTE address or only a PTE’s value:
volatility3 -f mem.dump windows.pte_resolve --pte-paddr 0x9be5130
PID Process Name Output
-1 N/A
Internal PteRun representation:
==============================
PteRun:
PID: Undetermined
vaddr: 0xb01c0426000
phys_offset: 0x476c000
length: 0x1000
pte_value: 0x8a0000000476c821
pte_paddr: 0x9be5130
pte_vaddr: 0xaf8580e02130
is_proto: Undetermined
is_proto_ptr: False
has_proto_set: True
state: HARD
is_exec: False
MMPTE struct:
=============
[_MMPTE_HARDWARE] @ 0x9be5130
0x0 Accessed 0x1 bitfield (bit 5)
0x0 CacheDisable 0x0 bitfield (bit 4)
0x0 CopyOnWrite 0x0 bitfield (bit 9)
0x0 Dirty 0x0 bitfield (bit 6)
0x0 Dirty1 0x0 bitfield (bit 1)
0x0 Global 0x0 bitfield (bit 8)
0x0 LargePage 0x0 bitfield (bit 7)
0x0 NoExecute 0x1 bitfield (bit 63)
0x0 Owner 0x0 bitfield (bit 2)
0x0 PageFrameNumber 0x476c bitfield (bits 12-48)
0x0 ReservedForHardware 0x0 bitfield (bits 48-52)
0x0 ReservedForSoftware 0x0 bitfield (bits 52-56)
0x0 Unused 0x0 bitfield (bit 10)
0x0 Valid 0x1 bitfield (bit 0)
0x0 Write 0x1 bitfield (bit 11)
0x0 WriteThrough 0x0 bitfield (bit 3)
0x0 WsleAge 0xa bitfield (bits 56-60)
0x0 WsleProtection 0x0 bitfield (bits 60-63)
NOTE: With newer versions, PteEnumerator
can’t resolve the PID solely from the physical PTE address.
And SimplePteEnumerator
enumerates all PTEs in a given range and allows to print them, while being able to filter for some basic characteristics such as executable and valid pages:
volatility3 -f mem.dump windows.simple_pteenum --pid 3456 --start 0x7fff7b5e1000 --end 0x7fff7b5e1000 --include-only-exec --print-pages
PID Process Matching pages Printed PteRun
3456 PhantomDllHoll 1
Internal PteRun representation:
==============================
PteRun:
PID: 3456
vaddr: 0x7fff7b5e1000
phys_offset: 0x11078000
length: 0x1000
pte_value: 0x600000011078005
pte_paddr: 0x24bf6f08
pte_vaddr: 0xa9bfffbdaf08
is_proto: Undetermined
is_proto_ptr: False
has_proto_set: False
state: HARD
is_exec: True
MMPTE struct:
=============
[_MMPTE_HARDWARE] @ 0x24bf6f08
0x0 Accessed 0x0 bitfield (bit 5)
0x0 CacheDisable 0x0 bitfield (bit 4)
0x0 CopyOnWrite 0x0 bitfield (bit 9)
0x0 Dirty 0x0 bitfield (bit 6)
0x0 Dirty1 0x0 bitfield (bit 1)
0x0 Global 0x0 bitfield (bit 8)
0x0 LargePage 0x0 bitfield (bit 7)
0x0 NoExecute 0x0 bitfield (bit 63)
0x0 Owner 0x1 bitfield (bit 2)
0x0 PageFrameNumber 0x11078 bitfield (bits 12-48)
0x0 ReservedForHardware 0x0 bitfield (bits 48-52)
0x0 ReservedForSoftware 0x0 bitfield (bits 52-56)
0x0 Unused 0x0 bitfield (bit 10)
0x0 Valid 0x1 bitfield (bit 0)
0x0 Write 0x0 bitfield (bit 11)
0x0 WriteThrough 0x0 bitfield (bit 3)
0x0 WsleAge 0x6 bitfield (bits 56-60)
0x0 WsleProtection 0x0 bitfield (bits 60-63)
Some issues from the PteEnumerator journey
Especially the implementation of several sanity checks and Unit tests for PteEnumerator
, but also the attempt to support the most recent Windows 10 versions (currently up until Windows 10 21h1 x64) revealed, besides various bugs in my code, also problems from changes in the PTE world with newer Windows versions. To help maybe also others encountering the same issues, I wanted to at least summarize those quickly here:
- PTEs in the
_MMPTE_TRANSITION
state started (beginning with at least Windows 10 1909) to include (probably) bit flag(s) within the upper bits of thePageFrameNumber
field, leading to way too high PFNs not pointing to the actual content anymore. PTEs in this state actually still point to memory in RAM and hence could still be read, but without correct handling of those bits the content is not retrievable anymore. This issue also existed in Volatility 3, but is already fixed since May (see also #475). - The MMPTE structures seem to constantly change between Windows versions, so any attempt to use hard coded offsets for any field member will eventually lead to wrong results (see e.g., here).
- Similar to the
_MMPTE_TRANSITION
case, thePageFileHigh
field of the_MMPTE_SOFTWARE
structure also introduced at least one bit flag, leading to wrong swap/pagefile offsets. In this case, the issue is more subtle, as the bit is not part of the most significant bits but right in the middle (see here or the following explanations for more details).
For the analysis of _MMPTE_SOFTWARE
PTEs, I’ve created a plugin called SwapEnumerator
(creative, I know ; ) ), that helps to see the current issue, but may also help on future changes/issues:
volatility3 -f mem.dump windows.swap_enum --pagefile-idx 0
Enumerated Pages Swap PTE count Pagefile IDX count Swap offset top 20 Bit count for PageFileHigh
622415 37037
idx count for 0: 37037
offset count for 0x2000000: 14724
offset count for 0x2001000: 258
offset count for 0x700b000: 84
offset count for 0x6c96000: 83
offset count for 0x6c95000: 83
offset count for 0x6c94000: 83
offset count for 0x6c93000: 83
offset count for 0x6c92000: 83
offset count for 0x6d1d000: 80
offset count for 0x6d1c000: 80
offset count for 0x6d1b000: 80
offset count for 0x6d1a000: 80
offset count for 0x6cea000: 78
offset count for 0x6d2d000: 76
offset count for 0x6d2c000: 76
offset count for 0x6d2b000: 76
offset count for 0x6d2a000: 76
offset count for 0x6ca4000: 75
offset count for 0x6cc9000: 43
offset count for 0x6efb000: 35
bit 0: 11249 set, 25788 unset.
bit 1: 11191 set, 25846 unset.
bit 2: 11019 set, 26018 unset.
bit 3: 11019 set, 26018 unset.
bit 4: 11346 set, 25691 unset.
bit 5: 10180 set, 26857 unset.
bit 6: 10476 set, 26561 unset.
bit 7: 10579 set, 26458 unset.
bit 8: 10702 set, 26335 unset.
bit 9: 11283 set, 25754 unset.
bit 10: 12689 set, 24348 unset.
bit 11: 12593 set, 24444 unset.
bit 12: 10217 set, 26820 unset.
bit 13: 37037 set, 0 unset.
bit 14: 14128 set, 22909 unset.
bit 15: 6556 set, 30481 unset.
bit 16: 5169 set, 31868 unset.
bit 17: 0 set, 37037 unset.
bit 18: 745 set, 36292 unset.
bit 19: 0 set, 37037 unset.
bit 20: 0 set, 37037 unset.
bit 21: 0 set, 37037 unset.
bit 22: 0 set, 37037 unset.
bit 23: 0 set, 37037 unset.
bit 24: 0 set, 37037 unset.
bit 25: 0 set, 37037 unset.
bit 26: 0 set, 37037 unset.
bit 27: 0 set, 37037 unset.
bit 28: 0 set, 37037 unset.
bit 29: 0 set, 37037 unset.
bit 30: 0 set, 37037 unset.
bit 31: 0 set, 37037 unset.
As can be seen, bit 13 (counting from 0) is always set for the PageFileHigh
field, never unset. Checking this with WinDbg’s !pte
extension shows that it unsets/ignores this bit in order to calculate the correct pagefile offset. This issue must be investigated further, but for now it seems that unsetting this bit results in the correct offsets.
The last two mentioned problems are still existent within Volatility 3, for which issues/pull requests have been created (see above). As not everything is already fixed (they are working on a re-implementation of the translation logic while handling all the special cases, which is not a trivial task), you should be aware that resolving/retrieving paged content with supplied pagefile(s) will not work (in the sense that Vol3 will return no/wrong content) for all Windows 10 versions >= 1803 17134 (Windows 10 1709 16299 still behaves “normally”).
It is, however, possible to correctly analyze these pages with PteEnumerator
respectively PteMalfind
(but not with the extensions, as they are using Volatility’s translation layer for scanning content).
So if necessary, it would also be easily possible to build a dumping tool based on PteEnumerator
(e.g., by using PteRun
‘s read
function). PteMalfind
on the other hand already comes with dumping functionality.
ApiSearch
ApiSearch
helps identifying pointers to APIs (functions defined in loaded DLLs). It does that by iterating over all loaded DLLs, enumerating their exports and searching for any pointers to the exported functions. Here a simple example output for the reflectively loaded DLL:
volatility3 -f mem.dump windows.apisearch --pid 1260 --address 0x15318530000
PID Process VAD Start Hit Addr API Distance
1260 notepad.exe 0x15318530000 0x1531853c000 KERNEL32.DLL!RaiseException 0
1260 notepad.exe 0x15318530000 0x1531853c008 KERNEL32.DLL!QueryPerformanceCounter 0
1260 notepad.exe 0x15318530000 0x1531853c010 KERNEL32.DLL!GetCurrentProcessId 0
1260 notepad.exe 0x15318530000 0x1531853c018 KERNEL32.DLL!GetCurrentThreadId 0
1260 notepad.exe 0x15318530000 0x1531853c020 KERNEL32.DLL!GetSystemTimeAsFileTime 0
1260 notepad.exe 0x15318530000 0x1531853c028 ntdll.dll!RtlInitializeSListHead 0
1260 notepad.exe 0x15318530000 0x1531853c030 KERNEL32.DLL!RtlCaptureContext 0
1260 notepad.exe 0x15318530000 0x1531853c038 KERNEL32.DLL!RtlLookupFunctionEntry 0
...
As can be seen, we have a list of API pointers right next to each other (the distance value tells us that there are X bytes after the last pointer until the next one, so a value of 0 means they are right next to each other), which is what we would expect for an already resolved IAT.
While this idea is not new at all, and this plugin is not based on PteEnumerator
or has anything to do with PTE analysis, it might still be helpful for some investigations and moreover, is a proof of concept for a PteMalfind
extension. In order to not have to call several plugins on potentially malicious content while writing down several memory addresses, PteMalfind
allows the invocation of external scanners/plugins on identified executable pages/VADs. To illustrate this, lets’ have a look at the following modified reflective DLL injection example. The initial situation is similar to the one described above, but with the one difference that the PE header has been stripped (so no easy PE recognition based on the leading MZ
):
volatility3 -f mem.dump windows.ptemalfind --pid 1792
PID Start Addr End Addr Protection Type
1792 0x201da540000 0x201da556fff PAGE_READONLY PrivateMemory
Meta Info:
22 non empty page(s) (starting at 0x201da541000) with a total size of 0x16000 bytes in this VAD were executable (and for mapped image files also modified).
Skipping the first 0x1000 bytes, as they are either not modified (only applies for mapped image files), empty, not executable or skipped via cmdline argument.
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
0x201da541000: add byte ptr [rax], al
0x201da541002: add byte ptr [rax], al
0x201da541004: add byte ptr [rax], al
0x201da541006: add byte ptr [rax], al
0x201da541008: add byte ptr [rax], al
...
PID Start Addr End Addr Protection Type
1792 0x201da560000 0x201da57afff PAGE_READONLY PrivateMemory
Meta Info:
26 non empty page(s) (starting at 0x201da561000) with a total size of 0x1a000 bytes in this VAD were executable (and for mapped image files also modified).
Skipping the first 0x1000 bytes, as they are either not modified (only applies for mapped image files), empty, not executable or skipped via cmdline argument.
48 83 ec 28 85 d2 74 25 H..(..t%
83 ea 01 74 20 83 ea 01 ...t....
74 1b 83 ea 01 74 16 83 t....t..
fa 03 75 31 4d 85 c0 74 ..u1M..t
2c 48 8b 05 b8 48 01 00 ,H...H..
49 89 00 eb 20 48 89 0d I....H..
ac 48 01 00 4c 8d 05 75 .H..L..u
b2 00 00 33 c9 48 8d 15 ...3.H..
0x201da561000: sub rsp, 0x28
0x201da561004: test edx, edx
0x201da561006: je 0x201da56102d
0x201da561008: sub edx, 1
...
So the question now is: Is this memory malicious and also maybe a PE file? To assist in the attempt to answer both questions, ApiSearch
can be called from within PteMalfind
. For this, the two plugin arguments --scanners
and --plugins
are available. --scanners
expects classes that implement the volatility3.framework.interfaces.layers.ScannerInterface
, --plugins
expects Volatility 3 plugins. Both also have to implement the PteMalfindInterface
, which essentially means they need to implement the function get_ptemalfind_results
that PteMalfind
calls and incorporates the results from (see ApiScanner
resp. ApiSearch
for an example implementation). In order for PteMalfind
to dynamically load the plugin/scanner, its name must be given in the following format: filename.classname
. So in the case of the ApiSearch
plugin, the class name is ApiSearch
and the containing python file name is apisearch
, so we can call it like this:
volatility3 -f mem.dump windows.ptemalfind --pid 1792 --plugins apisearch.ApiSearch
PID Start Addr End Addr Protection Type
1792 0x201da540000 0x201da556fff PAGE_READONLY PrivateMemory
Meta Info:
22 non empty page(s) (starting at 0x201da541000) with a total size of 0x16000 bytes in this VAD were executable (and for mapped image files also modified).
Skipping the first 0x1000 bytes, as they are either not modified (only applies for mapped image files), empty, not executable or skipped via cmdline argument.
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
0x201da541000: add byte ptr [rax], al
0x201da541002: add byte ptr [rax], al
0x201da541004: add byte ptr [rax], al
0x201da541006: add byte ptr [rax], al
0x201da541008: add byte ptr [rax], al
...
Output from extensions:
-
PID Start Addr End Addr Protection Type
1792 0x201da560000 0x201da57afff PAGE_READONLY PrivateMemory
Meta Info:
26 non empty page(s) (starting at 0x201da561000) with a total size of 0x1a000 bytes in this VAD were executable (and for mapped image files also modified).
Skipping the first 0x1000 bytes, as they are either not modified (only applies for mapped image files), empty, not executable or skipped via cmdline argument.
48 83 ec 28 85 d2 74 25 H..(..t%
83 ea 01 74 20 83 ea 01 ...t....
74 1b 83 ea 01 74 16 83 t....t..
fa 03 75 31 4d 85 c0 74 ..u1M..t
2c 48 8b 05 b8 48 01 00 ,H...H..
49 89 00 eb 20 48 89 0d I....H..
ac 48 01 00 4c 8d 05 75 .H..L..u
b2 00 00 33 c9 48 8d 15 ...3.H..
0x201da561000: sub rsp, 0x28
0x201da561004: test edx, edx
0x201da561006: je 0x201da56102d
0x201da561008: sub edx, 1
...
Output from extensions:
ApiSearch output:
0x201da56c000: KERNEL32.DLL!RaiseException distance: 0
0x201da56c008: KERNEL32.DLL!QueryPerformanceCounter distance: 0
...
0x201da56c1d8: KERNEL32.DLL!FlushFileBuffers distance: 0
0x201da56c1e0: KERNEL32.DLL!GetConsoleCP distance: 0
0x201da56c1e8: KERNEL32.DLL!GetConsoleMode distance: 0
0x201da56c1f0: KERNEL32.DLL!SetFilePointerEx distance: 0
0x201da56c1f8: KERNEL32.DLL!CloseHandle distance: 0
0x201da56c200: KERNEL32.DLL!WriteConsoleW distance: 0
0x201da56c208: KERNEL32.DLL!CreateFileW distance: 0
0x201da56c218: USER32.dll!MessageBoxA distance: 8
To invoke the also implemented ApiScanner
, the following command can be used (the result is the same, it’s just another way of extending PteMalfind
):
volatility3 -f mem.dump windows.ptemalfind --pid 1792 --scanners apisearch.ApiScanner
Again, we have a list of API pointers right next to each other: probably an already resolved IAT. The last API is in our case the “malicious” one, which invokes a MessageBoxA
. In contrast to this, the first detected memory area (also belonging to the reflective DLL injection) has no API pointers, as it is the 1:1 copied DLL from disk where only the loader stub is executed. So the ApiSearch
plugin doesn’t help in identifying this PE file. To further analyze this particular memory area, another scanner/plugin could be implemented and invoked (e.g., a Yara scanner).
That’s it for this year, so “guten Rutsch” and a happy new year!
Cheers,
Frank