With non-executable memory enabled, why can a memory-corruption bug still hand an attacker execution?
answer
- the pages changed, the flow did not
- stop supplying code, supply addresses
- resident code is already executable
- fragments that already end in a return
- first move: make a page executable again
basics
~20 sNon-executable memory only stops injected bytes from running. It does not stop hijacked control flow, so the attacker reuses code already mapped in the process - library functions and instruction fragments that are already executable and already legitimate jump targets.
solid answer
~50 sNon-executable memory enforces write-xor-execute: data pages cannot be executed and code pages cannot be written. That kills exactly one step of the classic exploit - writing machine code into a buffer and returning into it. Everything before that step still works: the overflow still happens, the saved return address is still overwritten, and the processor still honours it. So the attacker stops supplying code and starts supplying *addresses*. Return-into-library redirects execution to a function that is already mapped; return-oriented programming chains short instruction sequences that already end in a return, stitched together by a stack of addresses under attacker control. Every one of those bytes sits in a mapping the loader marked executable on purpose. A common first move for such a chain is to call the platform routine that changes page protections, re-creating the very capability the mitigation removed.
go deeper
Be ready to say what write-xor-execute stops (running bytes from a data page) and what it leaves intact (the overflow and the hijacked return address), then name code reuse as the answer attackers moved to.
Explain the mechanics: a stack of addresses instead of a payload, fragments ending in a return, and the common first goal of calling the routine that makes a controlled page executable again.
Show that you reason in preconditions - which link the control removes, which links are untouched, and why host-level binary removal misses a technique that runs entirely inside the victim process.
Own the framing for people who pay: mitigations set a price on one link, and a price selects which adversary can still afford the exploit. Never let it be recorded as immunity.
## What non-executable memory actually removes Non-executable memory - exposed by the hardware as the no-execute page bit, and applied as the write-xor-execute rule - marks data pages non-executable and code pages non-writable. Before it was standard, the textbook exploit for a stack overflow in a network daemon was four steps: overflow the buffer, place machine code in it, overwrite the saved return address so it points back into that buffer, and let the function return so the processor runs the attacker's bytes. The mitigation removes step four and nothing else. The buffer is still overflowable. The saved return address is still overwritable. Only *executing bytes that live on a data page* now faults. Read that list again, because it is the shape of every mitigation on this subject: one link of a chain became impossible, and the rest of the chain is untouched. That is what a mitigation is - a price on one link - and it is a different statement from a fix, which removes the defect. ## What survives: the control-flow hijack itself The defect gives the attacker a corrupted value that the processor will treat as a control-flow target: a saved return address, a function pointer, a virtual-table entry, a callback stored in a heap object. Nothing about page permissions makes that value trustworthy again. So the attacker's problem shifts from *what code do I supply* to *what code is already here that I can point at*. In a long-lived memory-unsafe daemon, the answer is: a great deal. The process has the C library mapped, plus every library the daemon links, plus the daemon's own code. All of it is executable by design, because that is what code is. ## Code reuse Two classic forms: - **Return into an existing function.** Overwrite the return address with the address of a resident library function and lay out the stack so that function sees the arguments you want. No new code anywhere. - **Return-oriented programming.** Instead of whole functions, find short instruction sequences that already end in a return instruction - a couple of useful instructions, then `ret`. Overwrite the stack with a list of such addresses. Each fragment does a little work and returns, which pops the next address and continues. The attacker has built a program out of somebody else's instruction bytes. The variant that chains indirect jumps instead of returns works the same way. A chain does not have to be long. Very often the whole goal is to call the routine that changes page protections on a region the attacker already filled with bytes, or to start a child process. In the first case the chain simply re-creates the capability the mitigation took away, then jumps into the newly executable region. ## Why hardening the host around it does not answer this Removing interpreters and shells from the file system, or blocking a binary from running, addresses a *substitutable* precondition. Reuse happens inside the victim process, using code that process already loaded, and the library's own system-call wrappers are sufficient. The precondition the technique cannot substitute is that a corrupted control-flow value is honoured. That is the thing a control has to take away. ## The control that does aim at it Control-flow integrity is the answer aimed at reuse: constrain every indirect transfer to a set of targets computed as valid, mark legal indirect entry points so a jump into the middle of an instruction stream faults, and - for the backward edge that return-oriented chains abuse - keep a shadow copy of return addresses and fault when the two disagree. It is a real price increase: coarse-grained variants shrink the usable target set, and a shadow stack removes the backward edge outright. It is still a price. Coarse policies leave enough valid entry points that call-oriented chains remain constructible, and a chain that only calls whole valid functions is inside the policy. ## What to say when someone claims immunity The honest sentence is: with these mitigations, an attacker holding this defect needs at least one more capability - typically a way to learn where things are, and a way to reach code already resident. Those are extra work, and extra work selects who can still afford the exploit. Someone firing a public proof of concept gets a crashed worker. Somebody who can spend a month finding a second defect gets execution. Both facts are true at once, and only the second one is a security position.
- If the attacker is reusing resident code, what does control-flow integrity add?It constrains where an indirect transfer may land: forward edges to a computed set of valid targets, and the backward edge properly only with a shadow stack that keeps its own copy of return addresses. It shrinks the usable target set rather than removing reuse - coarse policies still leave enough valid entry points to chain whole functions - so it raises the price again instead of closing the class.
- Does stripping shells and interpreters off the host stop this?No. The chain runs inside the victim process using code that process already loaded; the C library's own system-call wrappers are enough to change page protections or start a child. Removing binaries takes away a convenience, not the precondition. The precondition is that a corrupted control-flow value is still honoured.
- State precisely what write-xor-execute promises.That no page is writable and executable at the same moment. It does not promise a page can never become executable - if the process retains the ability to change page protections, the chain's first job is to call that facility on memory it already controls, and then the promise has been kept while the attacker runs code anyway.
Locking the workshop so nobody can bring in their own tools does not help if every tool they need is already bolted to the bench inside.
saying these in an interview costs you the question
- Says non-executable memory makes memory corruption unexploitable
- Assumes every exploit must inject shellcode somewhere
- Thinks removing shells from the host removes the capability
- Confuses non-executable pages with read-only pages
- Treats a mitigation and a patch as the same kind of thing