volatility3 icon indicating copy to clipboard operation
volatility3 copied to clipboard

Fixes transition-PDEs being treated as large pages

Open f-block opened this issue 4 years ago • 4 comments

For windows, the overloaded _page_is_valid is used (from WindowsMixin), which also returns true if a PTE/PDE is in transition state. While this is fine for most cases (normal/4kb pages resp. PTEs), this does not work when checking for large pages. Large pages are not paged out (at least not so far; see Windows Internals 7th Edition Part 1, page 304), so they don't reach the transition state. Hence, _MMPTE_TRANSITION does not contain the LargePage bit, but instead contains the page protection where also the LargePage bit would be. _translate_entry does, however, check bit 7 (which is only for _MMPTE_HARDWARE the LargePage bit) in both cases, and hence treats a PDE in transition state as a large page, that actually references a Page Table.

Here an example for the translation of an affected virtual address. In the first case without the fix, in the second with the fix. The final PTE in the second case has a value of 0x0, so the exception is expected:

(primary_Process5032_1) >>> proc_layer.translate(0x7fff30400000) (5970591744, 'memory_layer')

With the fix:

(primary_Process5032_1) >>> proc_layer.translate(0x7fff30400000) Traceback (most recent call last): File "", line 1, in File "/opt/forensic_volatility3/volatility3/framework/layers/linear.py", line 14, in translate mapping = list(self.mapping(offset, 0, ignore_errors)) File "/opt/forensic_volatility3/volatility3/framework/layers/intel.py", line 203, in mapping mapped_offset, _, layer_name = self._translate(offset) File "/opt/forensic_volatility3/volatility3/framework/layers/intel.py", line 343, in _translate return self._translate_swap(self, offset, self._bits_per_register // 2) File "/opt/forensic_volatility3/volatility3/framework/layers/intel.py", line 295, in _translate_swap return super()._translate(offset) File "/opt/forensic_volatility3/volatility3/framework/layers/intel.py", line 107, in _translate entry, position = self._translate_entry(offset) File "/opt/forensic_volatility3/volatility3/framework/layers/intel.py", line 137, in _translate_entry "Page Fault at entry " + hex(entry) + " in table " + name) volatility3.framework.exceptions.PagedInvalidAddressException: Page Fault at entry 0x0 in table page directory

f-block avatar Jun 16 '21 12:06 f-block

Hiya, thanks for this. I'm a little concerned that to fix a windows problem it's changing the logic for all operating systems, and may look redundant given that the normal _page_is_valid already does this test?

So my question is, would:

def _page_is_valid(entry):
  """Check for bit 11 (windows specific) and bit 10 (also windows specific) but ensure bit 7 is not set"""
  return bool((entry & 1) or ((entry & 1 << 11) and not (entry & 1 << 10) and not (entry & 1 << 7)))

have the same effect, or is that only true when it's a large page (in which case, we could pass that into the _page_is_valid method as a parameter)?

I disagree with the decision Microsoft made to make use of pages that are marked as not present, and I'm keen to avoid their decision impacting the speed/flow for other operating systems, even if it's just one extra quick check. It's carried out every time a page translation happens, so the more streamlined we can make it, the better...

ikelos avatar Jun 20 '21 20:06 ikelos

Hi,

yes you are right, with this fix you would do the test for the valid bit twice, and no, your suggested change won't work, because you are checking the bit 7 for the transition state, and this bit is in this case part of the protection field, so depending on the protection of the corresponding page, bit 7 can be set or not. It does, however, not have anything to do with large pages.

To illustrate, this is the PTE in hardware state, with the LargePage field (bit 7):

[_MMPTE_HARDWARE] @ 0x75acafa0
  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   0x1 bitfield (bit 7)
  0x0 NoExecute   0x1 bitfield (bit 63)
  0x0 Owner   0x1 bitfield (bit 2)
  0x0 PageFrameNumber   0x3165 bitfield (bits 12-48)
  0x0 SoftwareWsIndex   0x232 bitfield (bits 52-63)
  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 reserved1   0x0 bitfield (bits 48-52)

And here in transition state:

[_MMPTE_TRANSITION] @ 0x66fa3968
  0x0 CacheDisable   0x0 bitfield (bit 4)
  0x0 PageFrameNumber   0x1e749 bitfield (bits 12-48)
  0x0 Protection   0x4 bitfield (bits 5-10)
  0x0 Prototype   0x0 bitfield (bit 10)
  0x0 Spare   0x1 bitfield (bit 2)
  0x0 Transition   0x1 bitfield (bit 11)
  0x0 Unused   0x8830 bitfield (bits 48-64)
  0x0 Valid   0x0 bitfield (bit 0)
  0x0 Write   0x1 bitfield (bit 1)
  0x0 WriteThrough   0x0 bitfield (bit 3)

As you can see, bit 7 is here part of the Protection field.

One way to reduce the double checks would be to change my fix from:

if large_page and (entry & 1) and (entry & (1 << 7)):

to this:

if large_page and (entry & (1 << 7)) and (entry & 1):

This would mean that we only check the valid bit twice for PTEs in transition state and actual large pages. I did the effort of doing a quick evaluation with a VM running several processes (browsers, office, ...) and deactivated the pagefile, so pages are not paged out but end up at max in the transition state (to create a potential worst case scenario for the test). I invoked the translation for each virtual address for each VAD for each process, which resulted in a total of 4.244.510 virtual addresses.

The amount of (entry & 1) checks in the first version of the if statement were 12.260.351, in the second version only 74.004. Obviously this is not representative, but it at least shows that this can significantly reduce the double checks.

The only other alternative that I can currently think of is to not "overload" the original page_is_valid and to add ​this Windows function: page_is_in_transition: Checks for the transition state.

Then it would be possible to fix this issue without any double checks.

But then, page_is_valid will not return true for pages in transition state. This could be fixed by adding another function: page_is_mapped: Which would do the same as the current page_is_valid, so checking for valid and transition. In consequence, the usage of page_is_valid should then be replaced by page_is_mapped where appropriate.

But I'm not sure if that's the option you want to go for.

Cheers, Frank

f-block avatar Jun 22 '21 12:06 f-block

Hiya, just as an update I'm sorry I've taken so long, but I'm still considering more/better page fault handling code than we have at the moment.

ikelos avatar Jul 16 '21 13:07 ikelos

Sorry it took me a while, but I've worked on the page fault handler mechanism, and I'm now happier that it can be kept more separate from the Intel layers (which should behave like the hardware) The pull request allows for faults to be handled in a single method (which in windows is currently used to handle transition pages as well as swap pages). It doesn't directly follow the hardware page handler (which sets CR2 and then provides an error code indicating why the fault occurred), but I think we pass essentially the same information and make it easier for the worklfow of those trying to duplicate architectures.

Let me know what you think, very happy for revisions, modifications, and I'd be interested to verify that it solves the same issue this PR is trying solve...

ikelos avatar Oct 17 '21 14:10 ikelos