I wrote a program recently, SyslogParse, to display apparmor and iptables rules based on violations found in my system log. I did this because my apparmor-utils packages always break / were quite slow when going through my profiles, and going through iptables rules in syslog was a big of a pain too.
I decided this would be a fun project to sort of “lock down” against theoretical attacks, and I’d like to blog that experience to demonstrate how to use these different sandboxing mechanisms, as well as how they make the program more secure.
What takes place below is after the process of designing the application from a functional point of view – “what do I need this thing to do?”.
Step One: Threat Modeling
This step was a little less important for SyslogParse, as I was going to secure it regardless of real-world threats, but I’ll explain how I went about threat modeling.
The first thing I did is figure out what permissions this SyslogParse needs. I know the application, by design, must read from /var/log/syslog – a file that you need root permissions to read from. So I’ll be running this as root in order to do work.
To make things easier for users who don’t log to syslog, I’ll take in a path parameter, which means someone running this program can specify an arbitrary input file. That is the attack surface – one file being taken in.
An attacker who can control content in that file can potentially escalate to root privileges.
Step Two: Seccomp Mode 2 Filters
I’ve discussed seccomp filters on my blog beforehand, but to give a short recap, seccomp filters are developer-defined rules that will dictate which system calls can be made, and do light validation on the parameters.
Seccomp filters are very simple to use, and they’re the first thing I implemented.
Here I declare the seccomp filter.
Here I initialize it to kill the process when rules are violated.
ctx = seccomp_init(SCMP_ACT_KILL);
And here is an example of a seccomp rule being created.
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(futex), 0);
In the above rule I’ve said to allow the futex system call, a call used when a program uses threads and has to set mutexes. The “0” means I have no additional verification of the system calls arguments. In an ideal world I’d validate arguments to all of these calls, but it’s not always possible.
In the end I had about 22 calls, 3 of which I validate parameters on.
The thing about seccomp is that there’s no point doing sandboxing before I set this up, because without it the kernel will always be an easy target, and as many sandboxes as I layer on, I can’t change that from within my SyslogParse code – until I use Seccomp.
23 calls is quite a lot (though considerably less than it could be), and I chose futex as an example to show that despite limiting the calls, the recent futex requeue exploit would bypass this seccomp sandboxing and all other sandboxing this program uses. There’s only so much we can do from within the context of this program.
What is nice, however, is that I now know my kernel’s attack surface. Barring flaws in the seccomp enforcement, I know how my attacker can interact with the kernel, and that in itself is quite valuable.
Step Two: Chroot
By design SyslogParse must be root, in order to read root files, so that means I’ve got the chroot capability. May as well make use of it.
There’s a misconception that chroots are really poor security boundaries. This isn’t entirely false, but it’s not the whole story.
With one call I can set up a chroot environment that’s not so easy to break out of, at least it won’t be by the end of this article.
That creates a folder /tmp/syslogparse/ with the permissions that only root can read from it. Right now we’re root, so we can read from it, but that won’t last too much longer (about two more steps).
The file system as SyslogParse now knows it is an empty wasteland that only root can read and no one else can write to. A regular user would have no ability to read or write to it, which is nice because Inter-Process Communication (IPC) would require at least write access, and ideally read and write access.
Step Three: RLimit
For SyslogParse this is a bit unnecessary, but I went with it anyways.
rlrmit() is a system call you can make that will irreversibly limit that process in some way. In this case, because I want to limit IPC, and because SyslogParse only ever writes to stdout, which is already open, I’m going to tell it that it can not write to any new files.
struct rlimit rlp;
rlp.rlim_cur = 0;
In a more literal way, I’ve told the system that my process can not write to a file that is larger than 0 bytes.
Step Four: Dropping Privileges
The last significant step in this sandbox is to lose root. In this case, dropping to user 65534, which, at least on my system, is the ‘nobody’ user. A more ideal situation would have SyslogParse drop to a completely nonexistent user (to avoid sharing a user with another process) but I’m going with this for now.
That’s all it takes – SyslogParse is now running as the nobody user/group. No more root, and the process is within a chroot environment that it has no permissions to read or write to.
Step Five: Apparmor
I’m on elementary OS, which has apparmor. So, in my makefile I’ve put an ‘mv’ command that puts my profile into the users apparmor directory.
For a small program like this the apparmor profile is very simple.
# Last Modified: Wed Aug 13 18:57:15 2014
A few library files, read access to /var/log/ (for arbitrary log files), and, because I threaded the process, it needs to read
The real benefit of this apparmor profile is that it takes effect before any code runs – the rest of the sandboxing all happens right after I open /var/log/syslog – there is very little code before it, but some, and a compromise at that point will lead to full root control of the process. With the apparmor profile the worst case scenario is that they have access to only what is listed there.
Overall, I think that’s a fairly robust sandbox. It was mostly for fun, but it was all fairly simple to implement.
If an attacker did break into this system, the above would make things a bit annoying, though the obvious path is to simply attack one of the allowed system calls, as I only validate parameters on 3 and there is clearly attack surface still left.
This isn’t bullet proof, and it’s not an excuse to not test your code. I fed SyslogParse garbage files/ unexpected input to make sure it failed gracefully/ err’d out immediately when it came across something it didn’t know how to deal with.
Lots of fun to write, and hopefully others can make use of this to make their programs a little bit stronger.