Artificial truth

The more you see, the less you believe.

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

Paper notes - Clean the Scratch Registers: A Way to Mitigate Return-Oriented Programming Attacks
Tue 05 October 2021 — download

The latest Linux Plumbers Conference had a talk about security improvements in GCC, and one of the new mitigation is "call-used registers wiping on return", based on the paper "Clean the Scratch Registers: A Way to Mitigate Return-Oriented Programming Attacks", published in 2018 by Zelin Rong, Peidai Xie, Jingyuan Wang, Shenglin Xu and Yongjun Wang.

The idea is to use Pintools to monitor the execution, and to:

  • flag written-to scratch registers;
  • reset the "used scratch registers" flags on every call;
  • clear flagged scratch registers on every ret.

We ran the programs under the instrumentation of the system and then ran the exploit scripts to attack them to see whether we can get the shell.

[…] we picked up 7 typical shellcodes and rewrote them in ROP with the gadgets which generated by ROPGadget

This is utterly ridiculous: Nobody is using ROPGadget to generate rop-chains for real-world exploits. I've never seen been used for this purpose seriously by anyone, not even in CTF. Its algorithm is ultra-naïve, and can be considered as a toy or a proof of concept at best when it comes to automatic exploit construction. And even when used as a pure ROP-gadget finder, it tends to miss a ton of them compared to ropper or ropr.

Moreover, checking that trashing the registers on every ret will of course break existing exploits written without this "mitigation" in mind. One could simply recompile stuff with debug options, and it'll break a ton of exploits by simply changing the hardcoded ROP offsets as a side-effect. This is a dumb metric.

Anyway, I see at least four low-hanging generic bypasses, beside using JOP/COOP/SROP/…

Of course, more specific bypasses exist, but they're more binary-specific.

It is noteworthy that our system can handle the situations like setjmp, signal, exception and lazy binding that some dynamic detecting solutions can’t handle.

Since their system doesn't do anything against SROP, it's not really noteworthy at all.

The average performance overhead is nearly 16.2 times

With the lowest impact (gzip) being 4900%, and the highest (firefox) 25000%, this is completely ludicrous.

But things get even worse: the GCC implementation does the following:

In this new pass "pass_zero_call_used_regs": scan the exit block from backward to look for "return":

This means that unaligned gadgets aren't taken care of at all.

The commits message says:

This is used to increase program security by either mitigating Return-Oriented Programming (ROP) attacks or preventing information leakage through registers.

There is absolutely zero "information leakage" being prevented by this.

The patch adding this to the Linux kernel also uses ROPGadget to try to generate a complete (userland) /bin/sh popping rop-chain, sigh.

Additionally this helps reduce the number of useful ROP gadgets in the kernel image by about 20%:

As demonstrated in Is Less Really More? Why Reducing Code Reuse Gadget Counts via Software Debloating Doesn't Necessarily Indicate Improved Security from 2020, removing ROP gadgets doesn't equal to improved security, sometimes it's even worsening it.

Benchmarks are showing a ~1.5% performance impact when zeroing used scratch registers, and 17% for all* scratch registers. For comparison, PaX' RAP has an impact of ~7%, and is killing code-reuse attacks ~completely.

-fzero-call-used-regs (which has 9 (!) different options) looks a lot like the deprecated -mmitigate-rop gcc option: useless at best, providing a false sense of security with a maybe possibly small performance impact at worse.

tl;dr yet another example of MitiGator that should have listened to Halvar's advice.

Thanks to Matteo Rizzo and others for proofreading this article.