Reading EPROCESS and CR3 from MmPfnDatabase
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Reading EPROCESS and CR3 from MmPfnDatabase
After reading two writeups — one a “thought experiment” on bypassing CR3 protection, the other on identifying whether an arbitrary physical frame is a CR3 and decrypting its owning EPROCESS on Windows 10 1909 — I went looking for an actual implementation and couldn’t find one. So I bolted together a working version myself, borrowing a little from here, a little from there.
1. Locating MmPfnDatabase via KeCapturePersistentThreadState
In a remote windbg session, disassemble the function:
u KeCapturePersistentThreadState l 50

The code below parses out the mov target. I’ll be honest — the instruction-walking logic is cribbed, and I don’t fully follow every byte, but it reliably returns the MmPfnDatabase address:
ULONG64 EnumMmPfnDatabaseAddr() {
// Get the KeCapturePersistentThreadState address
UNICODE_STRING uc_funcName;
RtlInitUnicodeString(&uc_funcName, L"KeCapturePersistentThreadState");
ULONG64 ul_funcAddr = MmGetSystemRoutineAddress(&uc_funcName);
if (ul_funcAddr == NULL)
{
return 0;
}
//DbgPrint("KeCapturePersistentThreadState addr = %p \n", ul_funcAddr);
for (INT i = 0; i < 0x120; i++) {
if (*(PUCHAR)(ul_funcAddr + i) == 0x48 && *(PUCHAR)(ul_funcAddr + i + 1) == 0x8b && *(PUCHAR)(ul_funcAddr + i + 2) == 0x05) {
// Parse the mov displacement
INT i_movCode = *(INT*)(ul_funcAddr + i + 3);
ULONG64 ul_movJmp = ul_funcAddr + i + i_movCode + 7;
// This is MmPfnDatabase
return ul_movJmp;
}
}
}
2. Walking MmPfnDatabase to find the entry that matches the target EPROCESS
ULONG64 DecryptEProcess(LONG64 ListEntryFlink) {
//DbgPrint("before: %p\n", ListEntryFlink);
ListEntryFlink |= 0xF000000000000000;
ListEntryFlink = ListEntryFlink >> 0xd;
//DbgPrint("after: %p\n", ListEntryFlink);
return ListEntryFlink;
}
// Get CR3 via MmPfnDatabase
ULONG64 EnumCR3() {
ULONG64 MmPfnDatabase = *(PULONG64)EnumMmPfnDatabaseAddr();
//DbgPrint("MmPfnDatabase: %p\n", MmPfnDatabase);
INT MmNumberOfPhysicalPages = 0x3F9100;//0x7ff00;//0x3F9100;
PMMPFN tempMMPFN = NULL;
for (ULONG64 i = 0; i < MmNumberOfPhysicalPages; i++) {
tempMMPFN = (PMMPFN)(MmPfnDatabase + i * sizeof(MMPFN));
if (tempMMPFN->ListEntry.Flink != 0) {
if (DecryptEProcess(tempMMPFN->ListEntry.Flink) == g_targetProcess) {
return i << 12ull;
}
}
}
return 0;
}
DecryptEProcess comes straight from the second writeup. On other Windows versions you can rewrite the function via MiSetPageTablePfnBuddy.
MmNumberOfPhysicalPages throws an unresolved-symbol error in windbg:
kd> dd MmNumberOfPhysicalPages
Couldn't resolve error at 'MmNumberOfPhysicalPages'
So I just derived it from my host: 16273 * 1024 / 4 = 0x3f9100.
The MMPFN struct below is a slapdash definition — I only care about the first 8 bytes, and as long as sizeof(MMPFN) is 0x30 it works. The layout differs across Windows versions; check it with dt _MMPFN:
typedef struct _MMPFN {
union {
LIST_ENTRY ListEntry; // +0x000
RTL_BALANCED_NODE TreeNode; // +0x000
ULONG64 u1; // +0x000
ULONG64 PteAddress; // +0x008
ULONGLONG PteLong; // +0x008
ULONG64 OriginalPte; // +0x010
};
ULONG64 u2;
ULONG32 u3;
UCHAR NodeBlinkLow[2];
UCHAR Unused;
union {
UCHAR ViewCount;
UCHAR NodeFlinkLow;
UCHAR ModifiedListBucketIndex;
};
ULONG64 u4;
} MMPFN, * PMMPFN;
g_targetProcess comes from walking PspCidTable and matching the process name — that part is well documented elsewhere.
3. The result
My test account got banned, so I couldn’t verify it live in the game; notepad had to stand in. The driver:
NTSTATUS EnumProcessByPspCidTable(char* targetName) {
PHANDLE_TABLE pHandleTable = NULL;
pHandleTable = (PHANDLE_TABLE) * (PSIZE_T)GetPspCidTable();
//DbgPrint("pHandleTable:%p\n", pHandleTable);
g_targetProcess = EnumHandleTable(pHandleTable, targetName);
if (g_targetProcess == 0)
return g_targetProcess;
DbgPrint("PEPROCESS %p\n", g_targetProcess);
PULONG64 pcr3 = (LONG64)g_targetProcess + 0x28;
g_targetCR3 = *pcr3;
DbgPrint("g_targetCR3: %p\n", g_targetCR3);
g_targetCR3 = EnumCR3();
DbgPrint("g_targetCR3: %p\n", g_targetCR3);
return STATUS_SUCCESS;
}
The two CR3 values it produced matched.

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch