Return to abort() – Using code introspection to prevent stack-smashing
github.com
github.com
Given any routine which ends with the very common pattern:
return foo();
Which is assembled to the very common sequence: call foo
leave
ret
This project will instrument it to now look like: call foo
jmp $+2
.byte DE, AD
leave
ret
Whatever return address I hijack, I can now just point it at this valid return site, and begin my ROP stack as per normal.What's especially great is that the project guarantees this pattern for us. Now, every function has a path that looks like:
call __stack_chk_fail
jmp $+2
.byte DE, AD
< function frame cleanup >
ret
This is effectively a no-op for security.I cleaned up the author's code, added a sane makefile, and an example exploit here: https://github.com/zachriggle/return-to-abort
(Pull Request: https://github.com/cjdelisle/return-to-abort/pull/1)
Also, check out FSan:
https://pax.grsecurity.net/docs/PaXTeam-H2HC15-RAP-RIP-ROP.p...
(Not that you’ll be able to actually use RAP, as they’re only releasing it to commercial customers, but the description of it is worth checking out.)
Another approach is to take a hash of the types of the args and the return value (pointers obviously being opaque). This way we know the cookie value for any given function at compile time and we can stay out of the linker. However, in this case function a(int, char) can return to the call sight of function b(int, char) because to the code they're identical.
though at least being forced to return to the start of a function instead of somewhere randomly in the middle seems pretty powerful to me.
Some very early CPU architectures didn't actually support either putting the return address in a register or on the stack. For instance, on the PDP-8 (https://en.wikipedia.org/wiki/PDP-8#Subroutines) the JMS instruction writes the return address to the first word of the subroutine it's about to call (and the actual subroutine entry point is just after that), which meant it didn't conveniently support recursion. It wasn't alone in that either -- I think that it just wasn't quite appreciated how important recursion/reentrancy was back in the early 60s when these ISAs were designed.
This has at least one benefit over x86-style calls: a function like this:
void foo(void)
{
for (int i = 0; i < 10; i++)
some_leaf_function();
}
has to save its own return address to the stack, but it only needs to save it once, so all ten leaf calls can happen without stack access for the return address.Of course, architectures like x86 have specialized hardware to optimize calls, so it's probably a wash in the end.
Modern high-end CPUs have hardware return stacks too, but only as a hint to the branch predictor of where a ret instruction will jump to (return stack buffer).
Separately... there are exploit mitigations that create a separate stack just for return addresses, making them impossible to reach through stack buffer overflows. For a recent implementation, see Clang's SafeStack:
https://clang.llvm.org/docs/SafeStack.html
Or for a hardware-assisted version, there's Intel CET (not yet implemented on shipping CPUs, AFAIK):
https://software.intel.com/en-us/blogs/2016/06/09/intel-rele...
There are serious limitations to this approach, though: there's a lot of important data on the stack other than return addresses, and overwriting it is often enough for an attacker to redirect control flow eventually, just more indirectly.