|Informative Information for the Uninformed|
The first step that must be taken to implement this system involves identifying a method that can be used to trap references to arbitrary pages in memory. Fortunately, previous work has done much to identify some of the different approaches that can be taken to accomplish this[9,5]. For the purposes of this paper, one of the most useful approaches centers around the ability to define whether or not a page is restricted from user-mode access. This is controlled by the Owner bit in a linear address' page table entry (PTE). When the Owner bit is set to 0, the page can only be accessed at privilege level 0. This effectively restricts access to kernel-mode in all modern operating systems. Likewise, when the Owner bit is set to 1, the page can be accessed from all privilege levels. By toggling the Owner bit to 0 in the PTEs associated with a given set of linear addresses, it is possible to trap all user-mode references to those addresses at runtime. This effectively solves the first hurdle in implementing a solution to intercept memory access behavior.
Using the approach outlined above, any reference that is made from user-mode to a linear address whose PTE has had the Owner bit set to 0 will result in an access violation exception being passed to the user-mode exception dispatcher. This exception must be handled by a custom exception handler that is able to distinguish transient access violations from ones that occurred as a result of the Owner bit having been modified. This custom exception handler must also be able to recover from the exception in a manner that allows execution to resume seamlessly. Distinguishing exceptions is easy if one assumes that the custom exception handler has knowledge in advance of the address regions that have had their Owner bit modified. Given this assumption, the act of distinguishing exceptions is as simple as seeing if the fault address is within an address region that is currently being monitored. While distinguishing exceptions may be easy, being able to gracefully recovery is an entirely different matter.
To recover and resume execution with no noticeable impact to an application means that the exception handler must have a mechanism that allows the application to access the data stored in the pages whose virtual mappings have had their access restricted to kernel-mode. This, of course, would imply that the application must have some way, either direct or indirect, to access the contents of the physical pages associated with the virtual mappings that have had their PTEs modified. The most obvious approach would be to simply toggle the Owner bit to permit user-mode access. This has many different problems, not the least of which being that doing so would be expensive and would not behave properly in multi-threaded environments (memory accesses could be missed or worse). An alternative to updating the Owner bit would be to have a device driver designed to provide support to processes that would allow them to read the contents of a virtual address at privilege level 0. However, having the ability to read and write memory through a driver means nothing if the results of the operation cannot be factored back into the instruction that triggered the exception.
Rather than attempting to emulate the read and write access, a better approach can be used. This approach involves creating a second virtual mapping to the same set of physical pages described by the linear addresses whose PTEs were modified. This second virtual mapping would behave like a typical user-mode memory mapping. In this way, the process' virtual address space would contain two virtual mappings to the same set of physical pages. One mapping, which will be referred to as the original mapping, would represent the user-mode inaccessible set of virtual addresses. The second mapping, which will be referred to as the mirrored mapping, would be the user-mode accessible set of virtual addresses. By mapping the same set of physical pages at two locations, it is possible to transparently redirect address references at the time that exceptions occur. An important thing to note is that in order to provide support for mirroring, a disassembler must be used to figure out which registers need to be modified.
To better understand how this could work, consider a scenario where an application contains a mov [eax], 0x1 instruction. For the purposes of this example, assume that the eax register contains an address that is within the original mapping as described above. When this instruction executes, it will lead to an access violation exception being generated as a result of the PTE modifications that were made to the original mapping. When the exception handler inspects this exception, it can determine that the fault address was one that is contained within the original mapping. To allow execution to resume, the exception handler must update the eax register to point to the equivalent address within the mirrored region. Once it has altered the value of eax, the exception handler can tell the exception dispatcher to continue execution with the now-modified register information. From the perspective of an executing application, this entire operation will occur transparently. Unfortunately, there's still more work that needs to be done in order to ensure that the application continues to execute properly after the exception dispatcher continues execution.
The biggest problem with modifying the value of a register to point to the mirrored address is that it can unintentionally alter the behavior of subsequent instructions. For example, the application may not function properly if it assumes that it can access other non-mirrored memory addresses relative to the address stored within eax. Not only that, but allowing eax to continue to be accessed through the mirrored address will mean that subsequent reads and writes to memory made using the eax register will be missed for the time that eax contains the mirrored address.
In order to solve this problem, it is necessary to come up with a method of restoring registers to their original value after the instruction executes. Fortunately, the underlying architecture has built-in support that allows a program to be notified after it has executed an instruction. This support is known as single-stepping. To make use of single-stepping, the exception handler can set the trap flag (0x100) in the saved value of the eflags register. When execution resumes, the processor will generate a single step exception after the original instruction executes. This will result in the custom exception handler being called. When this occurs, the custom exception handler can determine if the single step exception occurred as a result of a previous mirroring operation. If it was the result of a mirroring operation, the exception handler can take steps to restore the appropriate register to its original value.
Using these four primary steps, a complete solution to the problem of intercepting memory accesses can be formed. First, the Owner bit of the PTEs associated with a region of virtual memory can be set to 0. This will cause user-mode references to this region to generate an access violation exception. Second, an additional mapping to the set of physical pages described the original mapping can be created which is accessible from user-mode. Third, any access violation exceptions that reach the custom exception handler can be inspected. If they are the result of a reference to a region that is being tracked, the register contents of the thread context can be adjusted to reference the user-accessible mirrored mapping. The thread can then be single-stepped so that the fourth and final step can be taken. When a single-step exception is generated, the custom exception handler can restore the original value of the register that was modified. When this is complete, the thread can be allowed to continue as if nothing had happened.