Introduction
Several months ago I documented an interesting behavior of the Windows memory manager on a public forum. In this post, I'll explain how the ParityError bit within the internal MMPFN structure can be used to prevent kernel memory from being copied through common APIs such as MmCopyMemory.
What is a Parity Error?
A parity or ECC error indicates that hardware detected corruption while accessing memory. Windows records this state in the PFN database so that the memory manager can avoid using or copying pages that are believed to be faulty.
How does Windows tell the Memory Manager (MM) that the given physical memory is faulty?
Windows keeps track of every physical page on the system through the Page Frame Number (PFN) Database or MmPfnDatabase. The entries of this database store metadata about its corresponding physical page, including the parity error status. In memory, each entry is represented by an _MMPFN structure. For reference here is the named structure for Windows 10 22H2 taken from the Vergilius Project:
//0x30 bytes (sizeof)
struct _MMPFN
{
union
{
struct _LIST_ENTRY ListEntry; //0x0
struct _RTL_BALANCED_NODE TreeNode; //0x0
struct
{
union
{
struct _SINGLE_LIST_ENTRY NextSlistPfn; //0x0
VOID* Next; //0x0
ULONGLONG Flink:36; //0x0
ULONGLONG NodeFlinkHigh:28; //0x0
struct _MI_ACTIVE_PFN Active; //0x0
} u1; //0x0
union
{
struct _MMPTE* PteAddress; //0x8
ULONGLONG PteLong; //0x8
};
struct _MMPTE OriginalPte; //0x10
};
};
struct _MIPFNBLINK u2; //0x18
union
{
struct
{
USHORT ReferenceCount; //0x20
struct _MMPFNENTRY1 e1; //0x22
};
struct
{
struct _MMPFNENTRY3 e3; //0x23
struct
{
USHORT ReferenceCount; //0x20
} e2; //0x20
};
struct
{
ULONG EntireField; //0x20
} e4; //0x20
} u3; //0x20
USHORT NodeBlinkLow; //0x24
UCHAR Unused:4; //0x26
UCHAR Unused2:4; //0x26
union
{
UCHAR ViewCount; //0x27
UCHAR NodeFlinkLow; //0x27
struct
{
UCHAR ModifiedListBucketIndex:4; //0x27
UCHAR AnchorLargePageSize:2; //0x27
};
};
union
{
ULONGLONG PteFrame:36; //0x28
ULONGLONG ResidentPage:1; //0x28
ULONGLONG Unused1:1; //0x28
ULONGLONG Unused2:1; //0x28
ULONGLONG Partition:10; //0x28
ULONGLONG FileOnly:1; //0x28
ULONGLONG PfnExists:1; //0x28
ULONGLONG Spare:9; //0x28
ULONGLONG PageIdentity:3; //0x28
ULONGLONG PrototypePte:1; //0x28
ULONGLONG EntireField; //0x28
} u4; //0x28
};The bit we are looking for is stored within e3 or the _MMPFNENTRY3 structure:
//0x1 bytes (sizeof)
struct _MMPFNENTRY3
{
UCHAR Priority:3; //0x0
UCHAR OnProtectedStandby:1; //0x0
UCHAR InPageError:1; //0x0
UCHAR SystemChargedPage:1; //0x0
UCHAR RemovalRequested:1; //0x0
UCHAR ParityError:1; //0x0
};If this bit is set, it signals that this physical page is faulty and vice versa.
Verifying the Behaviour
Up to this point I've given a slight overview of the Windows MM, shown you what a PFN entry looks like and where the parity bit is held. I've also told you that setting this bit tells your system that the memory is faulty, and in return this prevents most kernel APIs from copying your memory, but what good are words without proof?
MiCopySinglePage Disassembly
Below are snippets of Windows 10 22H2's build of ntoskrnl, inside the MiCopySinglePage function. MiCopySinglePage is used internally by MmCopyMemory to copy the memory.
If you managed to make it past the if ((0x4000000000000 & rax_1) != 0) cases then you're met instantly with a conditional that determines whether or not you continue to success or end up failing with error code 0xC0000709 aka STATUS_HARDWARE_MEMORY_ERROR.
14030c3a4 if (*((rbx_2 << 3) + -0x57fffffffdd) s>= 0 && MiIsPageSecured(-0x58000000000 + (rbx_2 << 3)) == 0)The important part here is the first condition: *((rbx_2 << 3) + -0x57fffffffdd) s>= 0. Although the decompiler emits this as a signed comparison, the value being tested is a signed byte. A signed 8 bit value is negative when bit 7 is set, which makes this condition equivalent to PFN->e3 & 0x80. What is 0x80 checking for? If we look back at the _MMPFNENTRY3 structure we can see that UCHAR ParityError:1 is the eighth bit (bit 7). Setting this bit causes this case to fail, skipping the page copy entirely and eventually reaching:
14046dd19 return STATUS_HARDWARE_MEMORY_ERROR; // 0xc0000709I won't be covering the implementation details here, but the reverse engineering above should provide enough information for interested readers to reproduce the behavior themselves.