Hacker News new | past | comments | ask | show | jobs | submit login
mimmutable() for OpenBSD (lwn.net)
112 points by signa11 on Dec 10, 2022 | hide | past | favorite | 39 comments



Im not a c programmer or otherwise work this low level, so im probably missing something obvious.

What is the point of making something immutable if you can just create a new write-exec region? Surely if you have enough control over the process to change permissions on an existing memory segment you have enough to mmap a new region with the permissions you want?


No, you're not missing anything. The primary reason this thing was designed was so try to patch out some holes in another unusual "mitigation", where system calls are checked to make sure they come from OpenBSD's libc. In this case adding new pages with syscalls in them would not work, but it seems like Theo read a paper at some point where they mprotected libc and patched its code and this spooked him into realizing that if you can mprotect libc itself this address range could be rewritten to contain attacker-controlled code, and make syscalls, which would break msyscall. So mimmutable prevents an attacker from ever remapping those pages, so they can never modify them even with mprotect.

Of course, msyscall itself is a weak mitigation, because an attacker can (as you mentioned) allocate another page with arbitrary code in it save for the syscall instruction, construct a call state with it, and then jump to any syscall instruction in libc. So this doesn't protect against much, anyways.

mimmutable itself is not really a dud, it's just not that great if you try to layer it on top of something already useless. It's great if you want to make sure a page (of data, usually) really stays immutable. But using it to prevent the introduction of new code is not all that effective, unless you are far more stringent about how you allow processes to allocate executable regions. For example, if you prevent a program from mapping in any new PROT_EXEC pages after it's initialized itself and then mimmutable all its code, then no new code can be introduced, which is a useful property. But you need that extra bit which mimmutable by itself doesn't give you.

(FWIW, macOS already has something similar to this called VM_FLAGS_PERMANENT, which prevents a mapping from being removed. This, together with the maximum protection on a mapping, can be used to prevent a region from ever gaining a certain permission. I believe you can reduce it to PROT_NONE though.)


> But using it to prevent the introduction of new code is not all that effective, unless you are far more stringent about how you allow processes to allocate executable regions. For example, if you prevent a program from mapping in any new PROT_EXEC pages after it's initialized itself and then mimmutable all its code, then no new code can be introduced, which is a useful property. But you need that extra bit which mimmutable by itself doesn't give you.

You mean like pledge(2)? The vast majority of the base system is pledged. So programs without the prot_exec promise cannot make new PROT_EXEC mappings, or add it to any existing pages.

https://man.openbsd.org/pledge#prot_exec

Perhaps you should spend more than 5 minutes reading about OpenBSD's layers of mitigations.


I think your comment would stand on its own better if it didn’t include the last line, both before and after you edited it. I do actually read up on what other OSes are doing, you know. And I look at what attackers target. What you’re missing here is the actual threat model this is supposed to protect against, and how this doesn’t actually work to protect against it, despite using building blocks that are “valid” in the sense that they can be used to build other, legitimate mitigations.

pledge is a good mitigation by itself. It’s a great way to sandbox things, and solve the problem of “this process no longer has any need to do these operations, so let’s just remove its capabilities to do that”. If you use it correctly it’s one of the strongest ways to make your program secure.

mimmutable by itself can be used to force a mapping to remain non-writable. This can be valuable because if you make the code that reads from that data also immutable, you know it will always read the same data, regardless of what an attacker can do in your address space. That is also a useful thing to have in certain cases.

If you read the thread where mimmutable was introduced you can see that the primary usecase proposed by Theo is to shore up msyscall. The threat model he has in mind is an attacker who can mprotect libc to be writable, which if I remember correctly was some minor detail of a Linux writeup once. However, there are several problems here.

One is that mimmutable by itself doesn’t actually fix the problem here because of the concern brought up at the top of this thread. You will note that the thing you quoted practically screams “but what if there was a way to prevent new executable mappings?” I actually did this intentionally, with full knowledge of how you can use pledge to do this. I think it is important for people who are not security experts to be able to follow the thinking that goes into analyzing these things, and be able to synthesize the usecase I mentioned at the start of this comment for themselves.

You will still note that I called msyscall weak in my original comment, and it still remains weak even after mimmutable+pledge is implemented. As others have mentioned as well using ROP to jump to the syscall instructions in libc with your own arguments (it’s not special…) bypasses restrictions in the current design.

In fact I can extend it with something OpenBSD does not have a good implementation of yet, strong CFI, which would prevent jumping into the middle of a function to execute that syscall instruction. But there are more fundamental reasons why this doesn’t work.

Protecting certain “sensitive” operations is actually something you can do. PPL as I mentioned below does this. It’s built on top of strong CFI (PAC) and code integrity guarantees (KTRR), which we are assuming you can construct in userspace on OpenBSD. The difference is that PPL has a tiny surface, and OpenBSD’s libc has a large one. In practice this means that libc cannot actually tell whether a request coming from an application is legitimate or not. If you look at the threat model for mimmutable again, consider that it assumes an attacker has enough control to call mprotect with controlled arguments: this means they know the layout of the address space and they can subvert control flow. With this kind of control, it’s not that hard to just decide to do other system calls instead. And who needs a syscall for that? You can literally just call the libc function which is a wrapper around the syscall. How is it to know that my call to write is malicious and the seven thousand the program already made was not?


> As others have mentioned as well using ROP to jump to the syscall instructions in libc with your own arguments (it’s not special…) bypasses restrictions in the current design.

...ignoring other mitigations.

> In fact I can extend it with something OpenBSD does not have a good implementation of yet, strong CFI, which would prevent jumping into the middle of a function to execute that syscall instruction. But there are more fundamental reasons why this doesn’t work.

Sounds like you need to spend 5 more minutes reading about retguard.


I need you, just for a moment, to understand that I am familiar with most OpenBSD mitigations and how they work, and that I have chosen my words carefully in my response to account for them. The fact that I even have to do this because you seem intent on leveling attacks at me, personally, in your responses is unfortunate because it would take me far less time to respond if I could assume to have a good faith conversation with you, which is what this site is really for, and not have to qualify every single thing I said with every possible "rebuttal" you are going to respond to me with. There is a world of difference between responding with genuine curiosity, e.g. "wouldn't retguard help protect against this?", and what you've done here.

Anyways, the critical word in the thing you've quoted is "strong"–retguard falls apart if the attacker has the ability to leak the value being used to protect the stack. In almost all cases an attacker in the position to make a controlled call into libc has the ability to also leak or forge any value of their choosing in the address space. A strong implementation would typically move this value completely out of the reach of such an attacker, for example by implementing signing in hardware (e.g. PAC) or or at a higher privilege level. I don't think these things are really feasible yet for OpenBSD so I don't blame them for not doing it, but without strong CFI it's hard to actually prevent this kind of thing from happening. (And, as I mentioned, this was only a tangent anyways; even with good CFI the more fundamental issue remains.)


I will agree that you have chosen your words carefully, and with obvious intent.


Isn’t pledge(2) trivially easy to escape anymore? This used to be one of the differences compared to eg capsicum(2).


is pledge easy to escape? can you give some examples?


For a while you could just execute another binary, it would run without restrictions imposed on the (pledged) parent. This is a stark contrast to Capsicum, where the monotonicity (ie the fact that once you loose the permission to something you'll never ever get it back, unless being explicitly passed it again) is one of the fundamental assumption behind the design.


No, not really.


pledge(2) aborts.


And? How’s that relevant?


man 3p pledge, try escaping it with a simple Perl script.


In other words you don't know what you're talking about, otherwise you would be able to answer a simple question.


>What is the point of making something immutable if you can just create a new write-exec region?

You aren't the only thing using the memory. If you create a new region only you will be the one using it.


>One of those marks executable memory that is empowered to call into the kernel; on OpenBSD systems, only the C library is given that capability. That will prevent hostile code loaded elsewhere from making direct system calls; protecting the rest of a process with mimmutable() will prevent the changing of protections to allow system calls from elsewhere (such changes would be done with msyscall() on OpenBSD).

This is, unfortunately, a continuation of OpenBSD's long tradition of security "mitigations" that are entirely detached from any actual understanding of exploitation [1]. The above suggestion that attackers can no longer make syscalls from their shellcode, while ostensibly true, is moot because you can simply JOP to one of the authorized syscall instructions and entirely defeat the mitigation. Even if mimutable outright prevented any new code from being mapped, this still wouldn't really stop anyone; just look at iOS where attackers have thrived despite there only being one process on the entire OS which is able to map arbitrary code.

> Whenever a process enters the kernel, its stack pointer is checked to see whether it is, indeed, pointing into a stack region; if not, the process is killed.

This too is trivially defeated. You simply need to add another stage to your payload in which you copy your ROP payload onto the legitimate stack, pivot, and continue executing. If you managed to pivot off the original stack, you already have a pivot gadget and so you only then need to trigger a call to memcpy. This mitigation isn't stopping any real attacker.

While I never hope to discourage kernel developers from thinking about security mitigations, I think it is really important that people working on these mitigations actually converse with people who write exploits. Other vendors have seen considerable success with their mitigations [2] in that they specifically target things that attackers need and want to do, such as heap spray.

[1] See: ROP gadget elimination, a feature which does not actually stop ROP but merely hopes that "a substantial reduction of gadgets is powerful." <https://marc.info/?l=openbsd-tech&m=150317547021396&w=2>. Speaking from experience and having written plenty of actual exploits on attacker hostile platforms, trying to block code execution against an attacker who already has kernel read/write is an utterly ridiculous idea because even if you manage to stamp out every single ROP gadget, they can just go and stomp on your kernel page tables to map new kernel code. Even baring that, kernel R/W is still a complete compromise even if kernel code execution is somehow impossible because you can still just use R/W to bypass any control the kernel could ever hope to enforce.

[2] Life and death of an iOS attacker by Luca Todesco <https://www.youtube.com/watch?v=8mQAYeozl5I>, a discussion of exploit mitigations on iOS and the exponential cost of defeating mitigations on iOS. Luca's company writes exploits for iOS devices as a service and he is extremely knowledgable in this area.


> "This too is trivially defeated. You simply need to add another stage to your payload in which you copy your ROP payload onto the legitimate stack, pivot, and continue executing."

Have you, or someone else as knowledgeable as you, been able to provide a succesful POC doing this? I admit that I haven't gone about looking for results on this topic and method of attack but I would be very interested in reading about it and see a practical example achieving it, since it's (in your own words) trivially defeated and simply needs just this or that thing.


Fine, you've nerd sniped me. I'll write a blogpost defeating both mitigations next week :)


i really hope you do this because it's super annoying to see all these folks talk about how these mitigations don't work but nobody really _shows_ it.

it's always the same thing whenever openbsd is mentioned.

"these mitigations don't work."

- "okay, please show us that they don't work"

"well, i don't use openbsd"

rinse. repeat.

it would also be nice to see some patches/fixes/suggestions/etc submitted after you've bypassed/defeated/whatever these things sent to the mailing lists. i don't suppose you'd agree to that?


I don't believe these mitigations are fixable, they are based on a fundamental misunderstanding of how people actually write exploits and what attackers are capable of. The issue is that it is extremely hard to try and limit what an attacker can do once they already have code execution in a process. There is a reason why most low level exploit mitigations apply before this point--once they have code execution, it's largely a lost cause to try and protect the integrity of the compromised process. The way we mitigate after code execution is by taking a step up and using things like sandboxing to try and prevent the compromise from impacting the rest of the system.

Also, I don't think it is quite right of you to be miffed that people aren't writing exploit PoCs to prove these mitigations are moot. These mitigations are trivially wrong to anyone who has any experience with exploit development, and being told to "show proof then" is baffling. It's like trying to explain to your uncle that no, vaccines will not make your child autistic, and him demanding proof. Obviously, that's neither how autism or vaccines work and trying to demonstrate that takes significantly more effort than most anyone cares to put in to a random (internet) argument. But, I am a fool who has too much time, so I'll bite.


can't wait!


Thank you! I'll keep an eye out. I'm genuinely interested.


> trying to block code execution against an attacker who already has kernel read/write is an utterly ridiculous idea

iOS does this. Kernel code is immutable (obviously), page tables are locked at reset and cannot be modified except by PPL code. ROP and JOP are mitigated against using hardware CFI (PAC). As you mentioned, when vendors actually understand what exploits look like and what attackers need to do, it enables them to do a good job. OpenBSD remains a system with a few good ideas and a model of what exploits look like that appears to be informed by dreams and idle musings, rather than actual research.


I believe I should have couched that statement with a "for most everyone else" :)

The PPL is a strong mitigation, I agree, but it is definitely one that is really only supported through their ability to ship custom hardware. Pulling off a PPL-like thing using MPK or a hypervisor is technically possible but in practice no real platform other than Apple is actually able to enforce memory mappings on all devices on an SoC and so anything you dream up to protect page tables is as durable as a wet paper bag since there's usually a half dozen other chips that will hapilly DMA any and all parts of physical memory for you. I dream of a day where we have an SMMU in front of every single device on a regular smartphone SoC (or, hell, even a PC platform), but that day has not yet come and I'm not really holding my breath either. As such, nobody else can actually meaningfully guarantees kernel code integrity under kernel R/W and so even if they had a perfect PAC implementation (which, as I'm sure you know, is incredibly difficult), you could just corrupt the backing code pages and sidestep it.


Right, iOS definitely has a different threat model which requires (and takes advantage of) Apple's ability to make custom hardware. In this case, though, OpenBSD is looking to protect userspace, and they could provide guarantees for that (iOS does this via codesigning, but you could also just force security-sensitive programs to stop using dlopen).


> you can simply JOP to one of the authorized syscall instructions and entirely defeat the mitigation

But now you need to know where the libc is mapped in order to find those syscalls instructions yeah? Which increases the complexity of the exploit, because instead of making a syscall you have to find where the libc is mapped, and the entire point of ASLR is making that difficult, no?


If you've managed to map shellcode at all, you necessarily already know where a valid syscall instruction is since otherwise you would not have been able to trigger the mmap syscall to map the shellcode. In any event, mapping shellcode is entirely optional anyways since to even get to that point you must already have full control through ROP, at which point you could do everything the program could (albeit in a significantly more annoying way).


The main binary has to be able to call into libc somehow, and thus it's typically easy to get a libc pointer if you have an existing method of code execution.

I don't know how it works on OpenBSD, but on Linux there's a section of memory called the PLT that contains pointers for all the functions from dynamic libraries, including those from libc. Also, there's typically libc pointers all over the stack.


If you look at the mitigations OpenBSD is doing as attack surface reduction, it means ultimately fewer tools in the attackers toolbox.

It seems many of you are missing the forest for the trees.


The question we have to ask ourselves is whether OpenBSDs mitigations are taking away enough attack surface (or in this case, exploitation tools) to make it worth the engineering and user cost.

I'd argue that things like msyscall and mstack don't at all because they cost attackers only a couple of minutes of time once to develop a bypass technique (ie move the stack pointer before a syscall, reuse the authorized syscall instruction) that they can apply everywhere. This greatly contrasts with mitigations like ASLR where each time an attacker wants to bypass the mitigation they are forced to develop a novel, program dependent strategy to leak some information. This is a huge pain and has definitely killed some otherwise exploited bugs because no such leak could be devised.


> I'd argue that things like msyscall and mstack don't at all because they cost attackers only a couple of minutes of time once to develop a bypass technique (ie move the stack pointer before a syscall, reuse the authorized syscall instruction) that they can apply everywhere.

If you read up on library order randomization/re-link and retguard, you may find your technique won't be so reusable, even once you do manage to locate a syscall stub in libc.


It also means fewer tools in developers' toolboxes. Not being able to make your own system calls directly, or exercise control over your own address space, means that anything that doesn't conform to a C runtime won't run in OpenBSD. Perhaps that's a tradeoff that the OpenBSD developers are willing to make, but even so, these particular "mitigations" do not address the root cause of many security vulnerabilities: failure to verify untrusted input.


I think that only Linux had some syscall stability. Every other operating system provides libc or other kind of library with stable interface but syscalls are not stable. Yes, technically you can execute those instructions and your software will work on a fixed kernel version, but that's not what people do.


Lack of syscall stability isn’t a showstopper, it just means that the functions invoking the syscall need to be versioned according to uname(). The uname() call itself can be done via direct syscall if you’re confident that its ABI won’t change; or, if you want to be extra sure, you can use the libc wrapper for uname() and then munmap() the C runtime to replace it with whatever. Either way requires the maintainer to be proactive with implementing code paths for upstream upcoming kernel versions before they are mainlined.

OpenBSD will make the above approach impossible, because with mimmutable(), libc will not be unmappable.


> The uname() call itself can be done via direct syscall if you’re confident that its ABI won’t change; or, if you want to be extra sure, you can use the libc wrapper for uname() and then munmap() the C runtime to replace it with whatever.

I would ask why you would want to do this.


So just ROP to the syscall you want in libc. Instead of targeting the syscall instruction, directly call mmap. System call randomization is a feature designed to discourage legitimate software from hardcoding syscall numbers. It is not meant as an exploit mitigation.


Can't stop the instruction set being able to write. Can stop higher logic invoking them, sometimes. Good to do. I really like the level of integration with compiler, low level boot, trusted code regions, "you will almost never call this, but your OS might on your behalf"

Live patching un-stoppable systems may be a challenge unless you code to passing live IO and state across process boundaries


someone show us bypasses for all these mitigations that don't mitigate anything please




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: