Finding Kernel Physical Read/Write Vulnerabilities
Welcome to this new Medium post, in this one I’ll show you how to identify a driver vulnerability that allows reading and writing arbitrary physical memory, without naming the specific driver.You’ll…

Welcome to this new Medium post, in this one I’ll show you how to identify a driver vulnerability that allows reading and writing arbitrary physical memory, without naming the specific driver.
You’ll understand the vulnerability by analysing decompiled code in Ghidra, and then we’ll write a Ghidra Java script to semi automate hunting for the same pattern in other drivers

Want to go deeper into Windows offensive development?
Video-based courses from beginner to advanced, and text-based modules (mini courses) with new releases constantly. Plus a technique database with 100+ real techniques updated weekly, and custom C2 agents and consulting for teams.
Introduction
Windows exposes a kernel object called \Device\PhysicalMemory, a section object backed by physical RAM. A kernel driver can open this section, map a view of it into a virtual address, and then read or write any physical address directly. This is intentional for hardware access, but it becomes a critical vulnerability when a driver exposes this capability to user mode via an IOCTL without proper validation
The standard call chain looks like this:
ZwOpenSection(\Device\PhysicalMemory)
- ObReferenceObjectByHandle(handle)
- HalTranslateBusAddress(physAddr)
- ZwMapViewOfSection(sectionHandle, physAddr, &virtualAddr)
Once ZwMapViewOfSection succeeds, the driver holds a kernel virtual address pointing directly at the requested physical memory. Any userland process that can send the right IOCTL can ask the driver to do this on its behalf
Vulnerable Decompiled Code
/* WARNING: Type propagation algorithm not settling */
int FUN_00400ef4(uint param_1,ulonglong param_2,longlong *param_3,undefined8 *param_4)
{
char cVar1;
char cVar2;
int iVar3;
char *pcVar4;
ulonglong uVar5;
undefined4 local_a8 [2];
longlong local_a0;
ulonglong local_98;
longlong local_90;
ulonglong local_88;
ulonglong local_80 [2];
undefined4 local_70 [2];
undefined8 local_68;
undefined1 *local_60;
undefined4 local_58;
undefined8 local_50;
undefined8 local_48;
undefined1 local_40 [24];
uVar5 = (ulonglong)param_1;
local_80[1] = 0;
local_90 = 0;
DbgPrint("Entering MapPhysicalMemoryToLinearSpace");
RtlInitUnicodeString(local_40,L"\\Device\\PhysicalMemory");
local_60 = local_40;
local_70[0] = 0x30;
local_68 = 0;
local_58 = 0x40;
local_50 = 0;
local_48 = 0;
*param_4 = 0;
iVar3 = ZwOpenSection(param_4,0xf001f,local_70);
if (iVar3 < 0) {
pcVar4 = "ERROR: ZwOpenSection failed";
}
else {
iVar3 = ObReferenceObjectByHandle(*param_4,0xf001f,0,0,local_80 + 1,0);
if (iVar3 < 0) {
pcVar4 = "ERROR: ObReferenceObjectByHandle failed";
}
else {
local_a0 = uVar5 + (param_2 & 0xffffffff);
local_a8[0] = 0;
local_98 = uVar5;
cVar1 = HalTranslateBusAddress(1,0,uVar5,local_a8,&local_98);
local_a8[0] = 0;
cVar2 = HalTranslateBusAddress(1,0,local_a0,local_a8,&local_a0);
if ((cVar1 == '\0') || (cVar2 == '\0')) {
pcVar4 = "ERROR: MappingLength = 0";
}
else if ((int)(local_a0 - local_98) == 0) {
pcVar4 = "ERROR: RtlLargeIntegerSubtract failed";
}
else {
local_88 = local_98;
local_80[0] = local_a0 - local_98 & 0xffffffff;
iVar3 = ZwMapViewOfSection(*param_4,0xffffffffffffffff,&local_90,0,local_80[0],&local_88,
local_80,1,0,0x204);
if (-1 < iVar3) {
local_90 = local_90 + (ulonglong)(uint)((int)local_98 - (int)local_88);
*param_3 = local_90;
goto LAB_00401107;
}
pcVar4 = "ERROR: ZwMapViewOfSection failed";
}
}
}
DbgPrint(pcVar4);
LAB_00401107:
if (iVar3 < 0) {
ZwClose(*param_4);
}
DbgPrint("Leaving MapPhysicalMemoryToLinearSpace");
return iVar3;
}
Function signature
int FUN_00400ef4(uint param_1, ulonglong param_2,
longlong *param_3, undefined8 *param_4)
Parameters:
- param_1: base physical address (low 32 bits)
- param_2: length of the region to map
- param_3: output: the mapped virtual address
- param_4: output: the section handle
Opening \Device\PhysicalMemory
RtlInitUnicodeString(local_40, L"\\Device\\PhysicalMemory");
// ...
iVar3 = ZwOpenSection(param_4, 0xf001f, local_70);
The driver opens the physical memory section with SECTION_ALL_ACCESS. No restrictions on caller privilege are checked
Getting a reference to the section object
iVar3 = ObReferenceObjectByHandle(*param_4, 0xf001f, 0, 0, local_80 + 1, 0);
ObReferenceObjectByHandle resolves the handle to a kernel object pointer. The AccessMode parameter is 0, meaning KernelMode. This bypasses all access checks.
A properly hardened driver would pass UserMode here
Translating the physical address
cVar1 = HalTranslateBusAddress(1, 0, uVar5, local_a8, &local_98);
cVar2 = HalTranslateBusAddress(1, 0, local_a0, local_a8, &local_a0);
HalTranslateBusAddress converts the bus address to a system physical address
Mapping the view
iVar3 = ZwMapViewOfSection(*param_4, 0xffffffffffffffff, &local_90,
0, local_80[0], &local_88,
local_80, 1, 0, 0x204);
The driver maps the physical region into the calling process’s virtual address space with read/write permission
The mapped virtual address is then returned to the caller:
local_90 = local_90 + (ulonglong)(uint)((int)local_98 - (int)local_88);
*param_3 = local_90;
The vulnerability: Any usermode process that can reach this IOCTL handler gets an arbitrary physical read/write primitive with no privilege check, no address range validation. Classic BYOVD
Automating the Hunt with Ghidra Java Scripting
Now the interesting part. Instead of manually auditing every driver, we can write a Ghidra script in Java that searches for this exact call pattern. The goal: find all functions that call both ZwMapViewOfSection and ZwOpenSection (or ZwOpenSection + ObReferenceObjectByHandle), flag them for review
Create a new script in Ghidra via Script Manager -> New Script -> Java:
// Hunts for functions containing the physical memory mapping pattern:
// ZwOpenSection + ObReferenceObjectByHandle + ZwMapViewOfSection
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.program.model.address.*;
import java.util.*;
public class PhysicalMemoryHunter extends GhidraScript {
// Target function names we're looking for
private static final String[] TARGETS = {
"ZwOpenSection",
"ObReferenceObjectByHandle",
"ZwMapViewOfSection",
"HalTranslateBusAddress"
};
@Override
public void run() throws Exception {
println("PhysicalMemoryHunter started");
FunctionManager fm = currentProgram.getFunctionManager();
SymbolTable st = currentProgram.getSymbolTable();
// Build a map: target name -> set of functions that call it
Map<String, Set<Function>> callerMap = new HashMap<>();
for (String target : TARGETS) {
callerMap.put(target, new HashSet<>());
SymbolIterator syms = st.getSymbols(target);
while (syms.hasNext()) {
Symbol sym = syms.next();
if (sym.getSymbolType() != SymbolType.FUNCTION) continue;
Function targetFn = fm.getFunctionAt(sym.getAddress());
if (targetFn == null) continue;
Set<Function> callers = getCallers(targetFn, fm);
callerMap.get(target).addAll(callers);
}
}
// Find functions that call at least ZwOpenSection AND ZwMapViewOfSection
Set<Function> openSectionCallers = callerMap.get("ZwOpenSection");
Set<Function> mapViewCallers = callerMap.get("ZwMapViewOfSection");
Set<Function> candidates = new HashSet<>(openSectionCallers);
candidates.retainAll(mapViewCallers);
if (candidates.isEmpty()) {
println("No candidates found.");
return;
}
println("Candidate functions:");
for (Function fn : candidates) {
// Check how many of the four targets this function calls
int score = 0;
for (String target : TARGETS) {
if (callerMap.get(target).contains(fn)) score++;
}
println(String.format(" %s @ %s [score %d/%d]",
fn.getName(),
fn.getEntryPoint().toString(),
score,
TARGETS.length));
}
}
// Returns all functions that reference (call) the given function
private Set<Function> getCallers(Function fn, FunctionManager fm) {
Set<Function> callers = new HashSet<>();
ReferenceManager rm = currentProgram.getReferenceManager();
AddressIterator refs = rm.getReferenceDestinationIterator(
fn.getBody().getMinAddress(),
fn.getBody().getMaxAddress(),
true
);
// Iterate references TO this function
for (Reference ref : currentProgram.getReferenceManager()
.getReferencesTo(fn.getEntryPoint())) {
Address fromAddr = ref.getFromAddress();
Function caller = fm.getFunctionContaining(fromAddr);
if (caller != null) callers.add(caller);
}
return callers;
}
}
What the script does
- Iterates the symbol table looking for each of the four target imports
- For each target, collects every function that has a reference to it
- Intersects the sets,functions that call both ZwOpenSection and ZwMapViewOfSection are the primary candidates
- Assigns a score out of 4 based on how many of the targets the function calls. A score of 3 or 4 is a strong signal
Running it
Open the Script Manager (Window -< Script Manager), place PhysicalMemoryHunter.java in your Ghidra scripts directory, and run it against the loaded driver binary.
The output window will list candidate functions with their addresses and scores
From there, navigate to each candidate in the decompiler and look for the same pattern we analysed above: no privilege check before ZwOpenSection, KernelMode (0) passed to ObReferenceObjectByHandle, and no address range validation before ZwMapViewOfSection
Extending the Script
A few directions to take this further:
- Check the IOCTL dispatch table: once you have candidate functions, trace backwards through xrefs to find which IOCTL code reaches them. This tells you whether user-mode can actually trigger the path.
- Add MmMapIoSpace as an alternative: some drivers use MmMapIoSpace instead of ZwMapViewOfSection to achieve the same result. Add it to TARGETS
Conclusions
Physical memory mapping vulnerabilities in kernel drivers are my favourite for BYOVD attacks: once you have arbitrary physical read/write, you can do almost whatever you want, here a list of BYOVD techniques where almost all of them are exploiting kernel R/W
https://medium.com/@s12deff/list/byovd-2e2fe179130e
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.