Synthetic Memory Protections/syscall() "hardening"
Theo de Raadt announced the following in May 2019:
Recently I considered the potential case of code-upload into the JIT
W|X
code arenas which might contain native system call instructions. I wish to block direct system calls from those areas, thereby forcing the attacker to deal with the (hopefully) more complex effort of using JIT support environment code, or probably even harder discovering the syscall stubs directly inside the randomly-relinked libc. Even if the JIT support access method is not more complicated, I want to remove the direct syscall exploitation avenue.
This diff refactors the
MAP_STACK
pseudo-permission bit code, adding additional code which checks if a syscall is being performed from a writeable page. If the page is writeable, the process is killed.
This one is more a “good practices enforcement” than a mitigation:
an attacker able to modify the JIT’ed code would just have to wait
for it to be remapped as read/executable to have their payload executed.
For non-JIT’ed code, ROP is still a valid options, if only to mprotect
the write bit away. And RETGUARD
won’t help in this case, since an arbitrary R/W is usually involved when it
comes to JIT-related exploits.
In 2019, De Raadt gave a talk at the CUUG 2019 about how unpredictability and non-uniformity improves security. During this talk, he said “Trying to develop X-only (to prevent blind ROP)”, showing that he doesn’t understand what BROP is about: the fork+exec technique is more than enough to neuter it completely.
The 27th of November 2019, Theo de Raadt added a mitigation for what he referred to previously as the “direct syscall exploitation avenue”:
The following change only permits system calls from address-ranges in the process which system calls are expected from.
If you manage to upload exploit code containing a raw system call sequence and instruction, and mprotect -w+x that block, such a system call will not succeed but the process is killed. This obliges the attacker to use the libc system call stubs, which in some circumstances are difficult to find due to libc random-relinking at boot.
This is done by adding 1 extra condition to the fast-path of the “syscall not on a writeable page” check.
For static binaries, the valid regions are the base program’s text segment and the signal trampoline page.
For dynamic binaries, valid regions are ld.so’s text segment, the signal trampoline, and libc.so’s text segment… AND the main program’s text.
So, if an attacker manages to get arbitrary code execution to inject a shellcode in the current process, and mark it as both non-writeable and executable, likely via ROP, they won’t be able to use syscalls directly inside the shellcode. An attacker with this amount of control can likely trivially gain an arbitrary read primitive, and ROP their way away.
This approach was indeed used in December 2015, to gain arbitrary code execution on the PS4 by CTurt
Amusingly, since some programs are directly issuing syscalls
(like everything written in Go before 1.16),
the .text
segment is whitelisted as well, so this mitigation is unlikely to
break any existing exploit.
This was “improved”
in August 2022, by having daemons in /sbin
dynamically linked. As deraadt@
said:
syscalls are in a randomly located libc, and every syscall stub is randomly located inside that due to random relinking. As opposed to fixed offset inside a release binary.
This is mitigation a strict subset of PAX_MPROTECT
/Windows’ AEG: it only prevents the
introduction of new usages of syscalls, but not of arbitrary code.
Moreover, in November 2022 De Raadt managed to put in the same paragraph “preventing attackers from uploading direct system call instruction code” and “ROP”, meaning that he knows about ROP as an exploitation technique, yet advocates for mitigations that are completely bypassed by it.
In 2023, in his CanSecWest talk, De Raadt said, talking about
Apple, maybe about 10 years ago, decided that they wanted to add a safety mechanism to their libc, something a little bit wearker than this. And so they made it so that libc was the only place you could do system calls from.
But that’s completely made up! Apple recommends not using raw syscalls because they don’t want to commit to a specific ABI, it has nothing to do with security.
This approach looks a bit like a subset of Protecting Against Unexpected System Calls, from C. M. Linn, M. Rajagopalan, S. Baker, C. Collberg, S. K. Debray, J. H. Hartman, published in 2005 at the 14th USENIX Security Symposium. The papers assumes that the attacker has no arbitrary read of any kind before running arbitrary code, and add a bunch of more-or-less crazy counter-measures to thwart runtime arbitrary reads, like immediate bindings, adding fake entries to the GOT/PLT, static linking, inserting dead code, binary obfuscation, code layout randomisation at the block level, splitting the binary into several disjoin memory maps up to the basic blocks level, pointers encryption, local variables order randomisation, … and even with this, the paper says: “However, increasing the attack code’s time and space requirements make intrusion detection easier; ideally, the bar is raised high enough that attacks have obvious symptoms.”
Chris Rohlf, security veteran, said the following about this mitigation in 2019:
I haven’t used OpenBSD in years so perhaps I am assuming too much. But an exploit mitigation that requires syscall call site verification seems like a minimal security gain in exchange for breaking the ABI for many language runtimes. The irony here is that this mitigation is intended to make exploitation of memory safety vulns harder but it breaks many memory safe languages in the process. If you have a system that absolutely must be secure as its first priority then by all means disable all the JITs, strip all the features, and enable stuff like this. But in order to get mass adoption a mitigation must work seamlessly for most general purpose use cases.
As well as:
I have a hard time believing only Go is affected but I’ll take the data at face value. Either way this mitigation is still entirely too myopic for me. It provides so little value its not worth it.
and:
pledge is strong. Much prefer that approach to weak mitigations that effectively boil down to weakly held secrets
The go runtime doesn’t use the system’s libc, for various reasons, one of them being it’s Go isn’t C.
Alexei Bulazel, (speaker at BlackHat, Recon, Shmoocon, … and RPISEc alumni)) said in 2019:
Windows Defender’s malware emulator did a thing like this to mitigate abuse of the custom “apicall” syscall instruction after @taviso discovered any malware binary could use it. It could be easily bypassed by just jumping to an
apicall
instruction in memory with controlled args
In October 2023, De Raadt sent an email about “Removing syscall(2) from libc and kernel”:
I already made it difficult to call execve() directly in a few ways. The kernel must be entered via the exact syscall instruction, inside the libc syscall stub. Immediately before that syscall instruction, the SYS_execve instruction is loaded into a register. On some architectures, the PLT-reachable stub performs a retguard check, which can be triggered by a few methods. Stack pivots are also mostly prevented because of other checks. It is not possible to enter via the SYS_syscall (syscall register = 0) case either.
This is hilarious and useless in equal measures.
It’s difficult to discover code-locations online only, because most architectures also have xonly code now. Some methods can use PLT entries (which also vary based upon random relink), but I’ve not seem much methodology using PLT entry + offset.
Using the PLT and/or the GOT to recover functions addresses is known since decades in the CTF community.
So in this next step, I’m going to take away the ability to perform syscall #0 (SYS_syscall), with the first argument being the real system call.
Removing this syscall will make it harder to run software written with portability in mind on OpenBSD (like assembly stubs for arm and x86 for cross-platform programs), while doing absolutely nothing security wise.
This is a completely useless mitigation, mitigating absolutely anything.