> Unikernels have no shells. Most attacks I’ve seen invoke /bin/sh to modify the system they are attacking. Without a shell the attacker doesn’t have this opportunity. This forces the attacker to use machine code to subvert the system, decreasing the likelihood of succeeding with the attack.
And, while this is a more minor rebuttal, it's still worth saying: it's totally incorrect. Modern exploits (for definitions of "modern" meaning "written after 1999") do not care if you have a shell on your system.
The author has presumably confused the concept of a POC, which is an exploit reduced and simplified for the consumption and understanding of laypeople, with that of a real exploit.
As one the early users[1] guilty of using the "there is no shell" argument for unikernel security, I agree with what you say. I would rephrase the argument as "there is no fork()/exec()", meaning that a unikernel cannot spawn a new process.
A unikernel application which only executes native code[2],
combined with a hypervisor / hardware that enforces immutable page tables and NX, AFAICT leaves ROP/JOP as the sole remaining attack vector for RCE. Further, each deployment of the application should be done via a new build (or at least re-link), which can employ the build-time ASLR Per mentions to mitigate ROP/JOP attacks.
I'd be interested in your thoughts on what other relevant attack vectors I've not thought of that would allow for a persistent compromise of a unikernel. Feel free to discuss here or on the devel.unikernel.org forum thread[3].
What difference does it make if there's no fork/exec? Back in 1999 we had MOSDEF and CORE IMPACT, both of which used staged loaders to boot up a language runtime in a remote process after compromising it with a stack overflow. In a broader sense this is stuff that viruses had been doing for about 8 years prior to that.
Only executing native code is no defense at all. You're not even stopping attackers from the '90s like that.
ROP is only a hardship until exploit code can find a way to allocate executable memory or hook the program's own dynamic execution (note: this is not necessarily a "JIT" or "eval") capabilities. ASLR only matters when exploits can't find infoleaks.
Really, what you're describing here is the browser security model. When staffed by the largest, best security teams in the world, the browser security model almost works.
> Back in 1999 we had MOSDEF and CORE IMPACT (...)
I'm not familiar with these. Googling "MOSDEF attack" suggests Massive Attack songs and "CORE IMPACT" various products by Core Security, I presume you're referring to neither? :-)
> Only executing native code is no defense at all. You're not even stopping attackers from the '90s like that.
> ROP is only a hardship until exploit code can find a way to allocate executable memory or hook the program's own dynamic execution (...)
Only executing native code loaded at deployment time, i.e. presumably from a trusted source. What I meant by "immutable page tables" is that after loading the unikernel image the set of executable pages is fixed, and all of those are read only. This is not something current unikernels do, but that's more due to lack of development time rather than any inherent implementation complexity.
If there is no way for the exploit code to ask the hypervisor for new executable pages or overwrite existing ones then the amount of damage it can do and its ability to persist in the running system is greatly reduced.
> Really, what you're describing here is the browser security model. When staffed by the largest, best security teams in the world, the browser security model almost works.
It's similar but not the same. The browser security model breaks down in a large part due to exposing way too many APIs, most of which are too complex to sensibly audit. Linux system calls have a similar problem, which is why you have to hand-craft things like seccomp profiles to match your particular application.
Unikernels (as used to implement network services) lend themselves extremely well to running on top of a well defined API / sandbox boundary, designed with security in mind. Again, we're not there yet, but the steps we need to explore in this direction are fairly clear.
Again, the hard part here is architecting your application not to need fork/exec for its own purposes anyway, which (as far as I'm aware) is a strict prerequisite of porting it to the unikernel model. If you can do that, forswearing fork/exec for security purposes is straightforward and doesn't require a unikernel. (Also, if you take the prctl approach, you can do that after you've forked a few helper processes, which is probably a much easier port!)
Or in other words, the argument isn't "unikernels are secure," it is "programming in an environment without fork/exec is secure," and unikernels are a needlessly complicated way to provide that environment.
> If you want to prevent fork/exec, that's super easy to do in a conventional Linux application:
And mmap(), mprotect(), ptrace(), ..., $syscall_du_jour(). It's not about fork/exec, it's about limiting the APIs available to the unikernel by default and by design.
Yes, you can do this for conventional Linux applications. But I wouldn't call it super easy, at least not for the vast majority of developers out there.
I assume it's referring to shell injection attacks. No shell means no shell injections. Of course, it also means that whatever functionality you were calling in a child process now must be in the same address space as the rest of your system. (E.g. rather than call Imagemagick to convert some incoming images from the client, you now must have a library with equivalent functionality in your unikernel.) Whether that's a net security improvement is questionable.
If the argument is supposed to mean "Unikernel applications cannot call system("echo " + user_provided_input)", well, it's pretty easy to do that in conventional applications: just don't call system. If you want to be sure instead of relying on static analysis/code review, rm /bin/sh in your production containers, or something.
Changing all your code to respect this standard is strictly less hard than porting it to a unikernel, because it's one step of porting it to a unikernel.
This argument is completely incoherent.