When code injection attacks become more and more difficult, attackers start to seek other opportunities to execute arbitrary code with completely re-using existing executable code in application image and/or shared libraries.
Typically, for example, those techniques without code injection could be return-to-libc, ROP (Return Oriented Programming), JOP (Jump Oriented Programming), or even SROP (Sigreturn Oriented Programming, see Framing Signals—A Return to Portable Shellcode).
Regarding techniques that defend against those control flow hijackings, here below are the lists (also incomplete).
- Change existing compilers to re-generate the code binaries. There are some typical solutions like, generate return-less binary code, generate control flow friendly binary (with extra IDs/labels for CFG hardening), modify all the control flow (ret, jmp, call) instructions to a well-known center redirection table, etc.
- Without binary changes, make static binary instrument and dynamic control flow tracing. For instance, build control flow graph, and enforce the control flow execution exactly aligning with the known CFG paths.
- Hardware assisted CFI. CFI(Control-Flow Integrity) is an efficient way to defend against ROP/JOP attacks. However, due to performance issue, complete CFI enforcement is impossible in practice. So, there are some lightweight CFI checks with the help of latest processor LBR (last branch record) to examine the control flow behaviors base upon the experience (rather than the full CFG analysis).
- THere are some specific solutions, like stack shadowing (to check if the target "ret" is call-preceding instruction), code section shadowing. But most of them are not a generic solution, but have many assumptions and limitations.
Regarding to the defenses with CFI, there are many papers that focus on the policy of checking whether or not the history control flow instructions behave as a malicious software. Typically, for example:
- Some solutions to check the length of each one of ROP gadgets. If it is very short (e.g. less than 5~6 instructions), it might be suspected. If there are consecutive chain of gadgets with "short" instruction sequences in a control flow, then it might be a ROP attack.
- Some of them to check whether if the target of "ret" is a Call-Preceded instruction (the instruction immediately preceding is a CALL instruction), this is because normally, every "ret" instruction returns back to an instruction that immediately follows a corresponding "call" instruction.
- Or check if the target of all the indirect call/jmp instructions are the "entry-point" functions. This is also normally true for a legitimate application, because generally an indirect "jmp" or "call" won't be calling into a certain middle location in a function.
However, just as those papers (see the links in References below) indicate, all the current CFI solutions based upon above assumptions can be bypassed with advanced ROP gadgets.
For example, in Nicholas's paper, he just used some call-preceded ROP gadgets and long termination gadgets for flushing attacking history to bypass checks of the famous kBouncer and ROPecker CFI solutions.
But his solution has an assumption for kBounce/ROPecker: the last branch records (LBR) can be stored in only 16 (at most) pairs of LBR MSRs.
As a matter of fact, if appropriately configured, the last branch records can also be stored into a variable-sized memory-resident branch trace store (BTS) buffer specified by DS(Debug Store) save area pointed by the IA32_DS_AREA MSR . And processor doesn't restrict how many pairs of last branch records could be stored in that BTS buffer, it also allows us to make processor generate an interrupt before the count of records reaches to the max records configured (or when the BTS buffer is nearly full). This means that we will never miss history LBR records. If we don't consider performance cost when enforcing CFI check at run time, this could be a good solution to trace all the control flow information.
However, even if we can get all the control flow traces (to defend against Nicholas's "history flushing" solution), does it mean that we can completely defend against control flow attacks? Unless that we can make full CFI checks with CFG, one of another problems that we might always encounter is how to design a better policy to reduce "false positive", and at the same time to catch all the ROP attacks at run time with acceptable performance cost in practice.