I can create a read-only page, but why not a write-only page?
There is an interesting hole in the diagram of page protections supported by the
|Deny write||Allow write|
The missing value is
PAGE_. Why is there no
The short answer is “Because no processor supports it.”
The page protections in most processors are not three bits, one for read, one for write, and one for execute. Different processors do things differently.¹ Typically, they start with a valid bit, which says whether any access is allowed at all. If the valid bit is not set, then you immediately get
PAGE_, and the rest of the page table entry is ignored.² If the valid bit is set, then other bits are used to configure the details of the protection. One way is to have separate bits for controlling write and execute access independently.³ Another is to take a few bits and treat them as an enumeration which selects from a list of possible page protection combinations, and “write-only” is not on the list of possibilities. Either way, there is no separate read bit; the read bit overloaded onto the valid bit: Every valid page is implicitly readable.
Naturally, if no processors support an operation, there’s no point adding a flag for it to the operating system. “Hi, here’s a flag that is not supported by anyone. If you set it, the operation always fails. Good luck with that!”
It’s also unclear how write-only pages would work anyway. CPUs nowadays typically do not issue precise writes. Rather, when you issue a write, the CPU loads the entire enclosing cache line, modifies the written bytes, and then writes the updated cache line back to memory (usually lazily). If the page were write-only, then the attempt to load the enclosing cache line would fail with an access violation,⁴ and you’d never get around to writing the updated cache line.
¹ For the purpose of this discussion, we’re looking only at user mode access.
² Operating systems often use the ignored bits to record information about the invalid page, so that if a page fault occurs, the operating system can quickly look up what it should do next. For example, the unused bits could be a pointer to a kernel data structure that says, “If somebody tries to access this page, then instead of raising an access violation, try to page the data in from the page file. The data is stored in frame N. After you read the data, change the protection to
PAGE_ and restart the instruction.”
³ On some processors, the control is done by setting the bit to allow access. On other processors, setting the bit denies access.
⁴ Alternatively, the CPU might load the cache line, bypassing the write-only protection, but try to prevent the code from observing any of the values that were loaded into that cache line. This could end up vulnerable to a side-channel attack.
In theory the OS could mark the range as uncachable as is done with IO ranges. But that would mean a specific range that all of the theoretical “Write only” memory would have to come out of to make this not completely insane. Realistically this is what SGX was designed to solve and seems to have failed miserably at IIRC.
Technically, write-only support could be simulated by the OS by making the page as unavailable and then catching the exception and processing the write in software. But what would be the use of all that work? Is there any practical application that would benefit of it? I don’t think so. If you need a mechanism to store data that you can not read later (whose usefulness is dubious, because nobody can prevent you from keeping a private copy of your data before sending it), you can implement an inbox via a custom API, message passing or something like that. Shoehorning an slow, complex and useless solution into the OS’ memory manager would make no sense.
How about execute-only pages? OpenBSD has recently started using them, with some architectures requiring weird contortions to make it work.
How would one execute a page if one cannot read it? And what’s the point, anyway? Hiding executable code?
Execute-only pages can be read by the CPU for the purposes of running the code, but not loaded into registers or copied into data pages. The point is to hide the currently executable code from an attacker.
There’s a family of exploit types (control flow attacks such as return-oriented programming) which function by using a data-only exploit that tricks the running program into jumping to a piece of executable code that it didn’t intend to, which does the thing the attacker wants. To do this, you need to know where in memory you’d find a useful piece of code to trick the attacked process into jumping to.
ASLR means that you can’t just look at the on-disk binary, or at a copy you’ve obtained by other means (e.g. downloaded from the Internet). The rise and rise of good monitoring means that you can’t spray an attack at a target, and rely on picking out the 1 in a million that don’t crash to find useful code “gadgets”. This leaves reading the code that’s currently running via a different attack, and then searching through the code you’ve read to find a useful gadget. Execute-only pages prevent you doing this, and make an attack harder.
Using execute-only pages as a defense for code execution is a bad idea. Basically, it’s security by obfuscation, which, in the best case, can only be treated as a mitigation (and in the worst can be completely useless). That said, in many processor architectures, execute-only pages can create bigger problems. If you have followed the series on processor architectures, you know that most processors can only load immediate 16 bit values, so, in order to load bigger ones (32 or 64 bits), you use some kind of IP-relative address mode. Which implies that the constants must be within the code reach, maybe in the same page as executable code.
Anyway, if you can read the process code pages, you are already running your own code in the process. You have already crossed the airtight hatch. If you were able to read it from other process, it would be even worse: it would mean you already have root/system privileges (no user-mode process can go and map another process’ address space).
On its own, it’s useless – without ASLR, the attacker can simply examine the binaries and learn where everything is going to be.
The intention of execute-only is “defence-in-depth”, basically to add an extra layer on top of ASLR.
If an attacker finds a hole that allows them to execute a group of gadgets they know to be on the same page as the hole they found, “execute-only” means they can’t use that first hole to read the code and search for the ALSR-d pages for other gadgets they desire to further the attack.
After all, an attack that often crashes the target will (should) get discovered pretty quickly.
I can see that being an issue on architectures where the compiler stores constants in between functions and loads them with a PC-relative load.
Once again we see the superiority of man over machine. The computer may not understand write-only memory, but I know plenty of programmers who can produce write-only source code.
Write-only-memory reminds me of this scene from Terry Pratchett’s the Hogfather:
How about where there is support from the hardware but not the software? Case in point, data breakpoints. VS debugger allows setting a HW data breakpoint on memory writes, but not memory reads. The HW supports breaking on memory reads.
Yes, it is useful when you have used it before to solve problems. I’m thinking of the Periscope add-on to Codeview, CV/ICE — 30 years ago — not the slow, single-step memory check that older VS or stock Codeview had, but full HW support, way back when dinosaurs still walked. Not. Available. Any. More.
Extra credit: CV/ICE also had another feature that let you HW break on any size access up to a page, not just up to a CPU word. Not. Available. Now. Not from the VS debugger. HW break on the page access, software work after that, but very fast still, and set up just like a regular HW break so no guru stuff needed.
I’m accustomed to seeing write-only memory implemented by memory mapping to a physical page that doesn’t exist, but I don’t think that will satisfy the guy asking for write only memory from VirtualAlloc.