Building

Release of PTE Analysis plugins for Volatility 3

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 in HARD (_MMPTE_HARDWARE) or TRANS (_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 by SUBSECTION 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 the PrototypePte 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 a SUBSECTION), 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 the PageFrameNumber 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, the PageFileHigh 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