Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss]

Paper notes - Evaluating the effectiveness of current anti-ROP defenses
Tue 08 January 2019 — download

This fine paper from peoples from the Ruhr-Universität Bochum explains how to bypass in a generic fashion three different ROP mitigations: kBouncer, ROPecker, and ROPGuard. All of them are using the LBR (last branch recording) CPU feature to detect suspicious control-flows.

tl;dr fill the LBR non-suspicious-looking data, and your ROP-chain won't get detected.

kBouncer

kBouncer hooks dangerous functions (link VirtualProtect), checks for suspicious LBR records, and if none are found, set a "checkpoint" consumed when the corresponding syscall is hit; preventing direct calls to syscalls. An attack is detected either if there is a non call-preceded return, or a chain of up to 20 gadgets ending in the latest LBR stack.

A possible bypass for kBouncer is to execute enough code before calling a dangerous function to fill the LBR stack with legitimate records, with things called LBR-flushing function.

  • On x86-32, those gadgets are are looking either like call <anything>; A; jmp {esi, edi, ebx, ebp} or call <anything>; A; call {esi, edi, ebx, ebp}; B; ret. Interestingly, calling EtwInitializeProcess (present in ntdll.dll), pre_c_init (present in all binaries) or even lstrcmpiW is enough to flush the LBR.
  • On x64-64, since the calling convention is different (first arguments passed in registers instead of the stack), a JOP-style dispatcher gadget is used to call dummy gadgets to fill the LBR.

ROPGuard

ROPGuard hooks winapi function, and check is something fishy is going on via several tricks; the two main ones being apparently:

  1. Past and future control flow analysis: checking if the return address is call-preceded, and simulating the control flow to check for future non call-preceded returns. Simulation stops after a certain number of instructions, or at the first call/jump.
  2. Stack checks: checking if the stack pointer is pointing inside the correct range (to detect stack pivots).

The aforementioned kBouncer bypasses are enough to fool the first mitigation (the control flow simulation stops early due to jumps/calls), and the second one isn't even triggered.

ROPecker

ROPecker doesn't hook functions, instead it works by catching access violations outside of a circular buffer of a couple of executable pages, if not attack is detected upon fault, the page is added to the ring. The attack detection works by analyzing the past (via LBR) and (simulated) future control flow. If a sequence of instruction matches all the following criteria, then it's considered as a ROP-gadget:

  1. The last instruction is an indirect branch
  2. Doesn't contain any direct branching instruction
  3. Is 6 instructions long at most

The past detection ends when a branch isn't a ROP-gadget, same for the future detection. If the length of accumulated gadget chain exceed a certain threshold (between 11 and 16, as suggested by the paper), then an attack is detected.

Bypassing ROPecker is a simple matter of interleaving non-gadgets (containing a branch, or more than 6 instructions) for time to time in their ROP chain to thwart the detection.