Other answers have described scenarios when these protections are deliberately disabled, but I think there's another important angle to answer here. The main premise of your question - that modern protections make exploitation infeasible - is also flawed. In fact, ASLR, DEP and CFG can all be bypassed.
Based on how you asked your question, I believe you're familiar with basic stack buffer overflow attacks, where you write a good amount of arbitrary data out of bounds, clobber a return pointer sitting on the stack, then pivot execution to a payload that you also write to the stack.
ASLR
ASLR makes a basic buffer overflow difficult, because you can't find your own stack address. Without knowing your own stack address, you can't overwrite the return pointer to point there.
In a real exploit, you might start looking for a memory disclosure bug - something that allows you to leak pointers from your target's memory space. While ASLR can randomise the base address of the stack, it doesn't randomise the offsets between stack objects. Functions typically have fixed-size stack frames, and you can usually plot a predictable path between functions of interest in a target. This means that if you can leak any stack pointer from anywhere in your target, you can figure out the precise addresses of every object that might exist in the stack.
This requires a separate primitive from the out of bounds write in your buffer overflow, which definitely makes the attack harder, but it definitely doesn't make it infeasible.
DEP
Marking the stack and heap as non-executable also stops your basic buffer-overflow from working. While the overflow means you can still clobber a return pointer, you can't execute your favourite payload because you can't put it anywhere executable.
The standard method for working around this is called Return-Oriented Programming, or ROPping. Your target binary has to be mapped into an executable region of memory, so you know you can execute any code still in there.
Return-oriented programming involves finding small sequences of instructions called "gadgets" which give you useful primitives. One gadget might pop a value from the stack into rcx
. Another gadget might read something from the memory address stored in rcx
into rax
. Another might jump execution to the address stored in rcx
. ROPping involves building a chain of these primitive gadgets to do something more complex and useful.
If you don't have anywhere that's both writeable and executable, a typical ROP chain might set up a call to VirtualAlloc
, to map in a region of memory that's both writeable and executable, then a call to memcpy
to copy a payload from the stack into the memory you got out of VirtualAlloc
, then jump execution into that payload.
ROP chains are much fiddlier than just uploading a payload and jumping to it, but any reasonably complex program should give you enough gadgets to do what you need to.
ASLR will randomise the addresses of these gadgets, making life a little harder. Again, your first step would probably be to find a memory disclosure vulnerability. If all your gadgets live within the same .exe
or .dll
file, then ASLR will only randomise that file's base address in memory; the relative offsets of everything in that file will remain static.
This, again, means that a single pointer disclosure will be sufficient to learn the address of every gadget in your ROP chain, giving you a feasible attack.
CFG
CFG is Microsoft's name for their Control-flow Integrity Checking technique. In general, control-flow integrity checking ensures that a code pointer always points to some "safe" place. Whenever your target makes a jump based on address in memory, it first checks that that address is a sensible place for it to jump to. Given that a ROP chain often involves jumping part way down a function, perfect CFI makes this impossible.
I've not needed to think much about control flow integrity checking myself, but other answers on this site note that perfect CFI basically doesn't exist.
Sometimes the checks are quite coarse - checking merely that jumps are heading into the right page of memory rather than to the correct instruction within that page. The Wikipedia article on CFG notes that it can be bypassed by jumping into a module which doesn't use CFG, which suggests that's the case there.
When they are fine-grained, they often rely on hidden information that can then be leaked. For instance, PaX RAP XORs each functions return pointer with an encryption key each time it's called. The key needs to be fixed through the lifetime of several function calls, and is stored in one CPU register. The encrypted return pointer lives in another register. On return, the function then decrypts the encrypted return pointer and compares it to the address it's about to jump to. If it's different, it crashes.
Armed with some good memory disclosure, though, it's possible to derive this key.
Because the encrypted return pointer lives in a register but changes on each function call, the encrypted pointer needs to be pushed to the stack between function calls. If you've already defeated ASLR, and can leak an encrypted return pointer, you know what its value should be, so you can figure out what the encryption key is. This means that, rather than targeting the current stack frame with your buffer overflow, you can target the previous frame and clobber both the return pointer and the encrypted return pointer.
Yes it's worth learning
Yes, of course, it's still worth learning about basic buffer overflows. You might have noticed that the techniques up there are much harder and make more assumptions, but they build directly from the basic techniques you now already understand. In my view, there's no way to understand how to bypass ASLR unless you've got a good understanding of the attack class that it mitigates
This also means, on the flip-side, that it is never sufficient to just turn on all the "make my code secure please" compiler flags and assume you're now invulnerable. All these techniques make exploitation harder, but with enough effort and sufficiently good bugs, it's nowhere near impossible