The picture presents the whole process of translating a virtual address to its corresponding physical address and the security features along the way.

Let’s describe the picture above in detail: in the code the code segment and virtual address are used to reference certain memory location. At first, the WP flag in CR0 register is checked whether it contains the value 0 or 1. Basically the WP is used to protect the read-only memory from being written to in kernel-mode, which allows additional protection when we’ve gained access to protected mode. Note that the WP bit only takes effect in kernel-mode, while the user-mode code can never write to read-only pages, regardless of the value stored in WP bit. The WP bit can hold two values:
- 0: the kernel is allowed to write to read-only pages regardless of the R/W and U/S flags in PDEs and PTEs.
- 1: the kernel is not allowed to write to read-only pages due to the WP not being set; rather than that, the R/W and U/S flags in PDEs and PTEs are used to determine whether kernel has access to certain pages – it only has access to pages marked as writeable, but never to pages marked as read-only.
When checking whether certain:
- Segment Table: if the DPL of the segment descriptor is higher than the RPL of the code segment register, then the access to that segment is allowed, otherwise it’s ignored. The DPL member in the segment descriptor is used to differentiate between privileged and unprivileged instructions. In segment table there are two code and two data segments, one having the DPL=0 and the other the DPL=3. They are both mapped to the same base address 0x00000000, but are considered as different segments. This is because the CS register can hold the value 0x08 (when privileged code executes) or 0x1B (when unprivileged code executes). If we’re executing a code when CS is set to 0x08 then privileged code is being executed, but if we’re executing the same code when CS is set to 0x1B, it’s considered unprivileged code.
- Page Directory Table / Page Table: if the WP flag in CR0 register is set to 1, then the R/W and U/S flags in page directory table and page table are used to defined access the kernel has to specific memory page. If the R/W (Read/Write) flag is set to 1, then the kernel can read and write to the page, otherwise it can merely read from it. The U/S (User/Supervisor) flag is set 1 for all pages that contain the kernel addresses, which are above 0x80000000. If an unprivileged code (from user-mode) tries to access such pages, an access violation occurs: a code that has CS register set to 0x1B doesn’t have access to such pages.
We’ve seen that kernel address protection is realized with the combination of segmentation and page-level protection. Basically the CS register is used to determine whether the code is given read/write access to the specific page in memory. Remember that we can execute privileged instructions from user-mode by using one of the following approaches [4]:
- “int 0x2e” instruction
- sysenter instruction
- far call
Now that we’ve got that all cleared out, let’s see what kind of display we have regarding the SSDT table. First we have to check the value stored in the 16-bit (WP – Write Protect) of the CR0 register. We can easily do so by using the .formats command to print the value of CR0 register in binary form as seen below. The highlighted bit is WP bit and is set to 1, which means that page directory table and page table are used to determine whether the CPU can read/write from/to pages. Because of this, the CPU won’t be able to write to read-only pages.

Let’s now display a single entry from the SSDT KiServiceTable by using the “dps nt!KiServiceTable l1” command, which displays the first entry from the SSDT table that’s located at address 0x826af6f0. After that, we used the “!pte 0x826af6f0” command to display various flags about the PDE/PTE in which the address is located.

On the picture above we can see the first column, which represents the PDE that has the following flags: DAKWEV and the second column, which represents the PTE that has the following flags: GAKREV. The flags in PDE/PTE entries printed by the !pte command are presented below [5]:
- Valid (V): in data is located in physical memory and has not been swapped out.
- Read/Write (R/W): the data is read-only or writeable.
- User/Kernel (U/K): the page is either owned by user-mode or kernel-mode.
- Writethrough (T): a writethrough caching policy.
- CacheDisable (N): whether or not the page can be cached.
- Accessed (A): set when the page has been accessed by either reading from it or writing in it.
- Dirty (D): the data in the page has been modified.
- LargePool (L): only used in PDE and specifies whether large page sizes are used, which is true when PAE is enabled. If set, the page size is 4MB, otherwise it’s 4KB.
- Global (G): affects the translation caching flushes and translation lookaside buffer cache.
- Prototype (P): a software field used by Windows.
- Executable (E): the instructions in the page can be executed.
Once we’ve studied the flags used by the !pte command, we can see that the PDE is marked as writeable, but PTE is read-only, but both are executable. This means that we cannot simply write some values into the PTE where the SSDT table is located. Now that we know that we’re dealing with a system service dispatch table located in read-only memory, we can start looking at a ways to bypass that limitation and mark the memory as writeable. There are three ways to get write access to the SSDT table [1]:
- Change CR0 WP Flag: if we set the WP flag in CR0 register to 0, the PDE/PTE restrictions are not considered when granting write access to the read-only pages when we’re in kernel-mode.
- Modify Registry: we can alter the “HKLMSYSTEMCurrentControlSetControlSessionManagerMemoryManagementEnforceWriteProtection” registry key, which allows us write access.
- MDL (Memory Descriptor List): the Windows operating system uses MDL to describe the physical page layout for a virtual memory buffer. To make the SSDT table writeable, we need to allocate our own MDL, which is associated with the physical memory of where the SSDT table is stored. Because we have allocated our own MDL, we can control it in any way we want and therefore can also change its flags accordingly.