Hardening Wifidog
Wifidog run as root and that's bad. The omnipotent superuser
allows Wifidog to do absolutely everything on (and to) the system. An
attacker who is able to remotely execute arbitrary code
can easily take over the whole system.
Capable of Less
Enter Linux capabilities (man 7 capabilities).
Capabilities divide the full set of privileges into individual units. By dropping unneeded
capabilities such as the privilege to bypass file permission checks (CAP_DAC__OVERRIDE),
it becomes much harder for the hypothetical attacker to cause mischief on the system.
In addition to arbitrary remote execution, the attacker would have to escalate their
privileges. This is a lot harder.
The Art of Minimalism
Wifidog needs two privileges:
- CAP_NET_RAW to determine the IP address of its network interfaces
- CAP_NET_ADMIN to modify firewall rules
So, the whole thing should be easy. We start as root and use libcap to drop
all privileges but these two. Afterwards, we switch to a (traditionally) unprivileged
user because root can still read (and write!) its own files. Note that
unprivileged here means an UID other than 0. Running as UID 0 with dropped capabilities would be
problem because an attacker could still place a shell script in /etc/cron.d/
which would then be executed by cron running as root with the full set of
capabilities.
It doesn't work out that way. Of course.
The Devil, the Details and execve()
Linux knows thread-based capabilites and file-based capabilities. Wifidog internally
executes iptables to set firewall rules. Unfortunately, subprocesses do not inherit
capabilities by default. Even if we add CAP_NET_RAW and CAP_NET_ADMIN
to the INHERITABLE set for the wifidog process, Linux will still perform
transformations of the capability sets based on the file capabilities of the executables
invoked as a subprocess. The transformation rules are as follows (straight from the man page):
During an execve(2), the kernel calculates the new capabilities of the process
using the following algorithm:
P'(permitted) = (P(inheritable) & F(inheritable)) |
(F(permitted) & cap_bset)
P'(effective) = F(effective) ? P'(permitted) : 0
P'(inheritable) = P(inheritable) [i.e., unchanged]
where:
P denotes the value of a thread capability set before the execve(2)
P' denotes the value of a capability set after the execve(2)
F denotes a file capability set
cap_bset is the value of the capability bounding set (described below).
We care about P'(effective). Working backwards, we see that the effective capabilities of the subprocess
come from the PERMITTED set of the subprocess IFF the EFFECTIVE file capability F'(effective) is set.
To set up the prerequisite P'(permitted) set, the intersection of the INHERITABLE sets for Wifidog
and the subprocess, iptables, is taken.
To make things finally work, we use /usr/bin/setcap to set the file-based capabilities:
setcap cap_net_admin,cap_net_raw+ei /usr/bin/xtables-multi
On a side note, if you add a capability to both PERMITTED and EFFECTIVE set of an executable,
the process gets the capability in its PERMITTED set. In practice, anyone could then run iptables:
setcap cap_net_admin,cap_net_raw+ep /usr/bin/xtables-multi
Don't do that. It's mainly useful for tools like ping which would otherwise be SETUID0.
$ getcap /usr/bin/ping
/usr/bin/ping = cap_net_raw+ep
Minimalistic Capability Sets and Minimalistic Environments
That's quite the effort to reduce the set of privileges. It's confusing that
capability inheritance does not work out of the box and needs special treatment
for executables running as subprocesses. I'm not the only who considers this… problematic:
My main problem with the current design is that
OpenWrt does not support file-based capabilities out of the box.
Sure, it's compiled into the kernel, but the default file systems are compiled
without extended attributes which are in turn required to actually store the file-base capabilities.
Since I intended to use the hardened version of Wifidog on OpenWrt, this is a problem for me.
The obvious solution is to turn on extended attributes in my personal OpenWrt
builds, but that does not help anyone who runs stock images. In short, it must work out of the box.
Workarounds and Drawbacks
Linux capabilities supports a compatibility mode. If a program is executed as UID0, all capabilities
are granted by default. My approach is now changed to temporarily switch to a non-privileged user
which still has CAP_NET_ADMIN and CAP_NET_RAW. Before executing iptables, Wifidog temporarily
switches back to UID 0 to reap the benefits of the compability mode. The subprocess is then automatically
granted all INHERITABLE and PERMITTED capabilities in file-based sets.
From the man page, I am still not quite sure why iptables works in this setup. The EFFECTIVE bit should
still be required, but it's only granted on SUID root executables.
This method has an important drawback. In my original design, I switched to a non-privileged user
using setuid which drops root completely. Now I need to use seteuid to set the effective
user ID only. This permits me to switch back to UID 0 to invoke iptables. The problem is that
an attacker, given an exploit for arbitrary code execution, could perform the same seteuid call.
This opens up the way for privilege escalation attacks as described earlier with /etc/crond.d.
Linux Security Modules like SELinux
could provide a way out to restrict the files that wifidog (or an attacker) could access. The better
option here would be to enable extended attributes in OpenWrt, as that is far less intrusive and complex.
The way forward
My patches will hopefully land in Wifidog soon.
As described above, the current security improvement are not as big as I had originally hoped. In particular,
privilege escalation is still too easy if code can be ran as UID 0 even a reduced set of capabilities.
For this reason, I would recommend running the capability-enabled Wifidog in a chroot environment. The
upcoming OpenWrt release will feature jail support in its own init system, procd.
I plan on updating the OpenWrt package to make use of this new feature. Even without seccomp support,
a traditional chroot would (hopefully) suffice to protect against the type of attacks
described above. Wifidog does not have CAP_CHROOT which would be necessary to
break out of chroot.
Thoughts
This was a fun learning experience. Wifidog is a bit more robust and secure, and that's what counts.
Further Reading (summary from previous links)
Commented example code
First, here is the function that drops all but the required capabilities. Feel
free to bring your own implementation of set_user_group.
Call this early during initialization:
void
drop_privileges(const char *user, const char *group)
{
const int num_caps = 2;
/* The capabilities we want. */
cap_value_t cap_values[] = { CAP_NET_RAW, CAP_NET_ADMIN };
cap_t caps;
int ret = 0;
/*
* We are about to drop our effective UID to a non-privileged user.
* This clears the EFFECTIVE capabilities set, so we later re-enable
* these. We can do that because these are not cleared from
* the PERMITTED set on seteuid().
*/
set_user_group(user, group);
caps = cap_get_proc();
if (NULL == caps) {
exit(1);
}
/* Clear all caps and then set the caps we desire */
cap_clear(caps);
cap_set_flag(caps, CAP_PERMITTED, num_caps, cap_values, CAP_SET);
ret = cap_set_proc(caps);
if (ret == -1) {
exit(1);
}
cap_free(caps);
caps = cap_get_proc();
/* Now, we are running as non-privileged user and no capabilities are EFFECTIVE.
* We need to regain capabilities by promoting them from PERMITTED to
* EFFECTIVE and INHERITABLE.
*/
cap_set_flag(caps, CAP_EFFECTIVE, num_caps, cap_values, CAP_SET);
cap_set_flag(caps, CAP_INHERITABLE, num_caps, cap_values, CAP_SET);
ret = cap_set_proc(caps);
if (ret == -1) {
printf("Could not set capabilities!\n");
exit(1);
}
caps = cap_get_proc();
if (NULL == caps) {
printf("cap_get_proc failed, exiting!\n");
exit(1);
}
printf("Final capabilities: %s", cap_to_text(caps, NULL));
cap_free(caps);
}
Now, if you need to temporarily go back to UID 0 to execute
a subprocess, you might use something like this:
/**
* Calls popen with root privileges.
*
* This method is a wrapper around popen(). The effective
* user and group IDs of the current process are temporarily set
* to 0 (root) and then reset to the original, typically non-privileged,
* values before returning.
*
* @param command First popen parameter
* @param type Second popen parameter
* @returns File handle pointer returned by popen
*/
FILE *
popen_as_root(const char *command, const char *type)
{
FILE *p = NULL;
uid_t uid = getuid();
gid_t gid = getgid();
switch_to_root();
p = popen(command, type);
set_uid_gid(uid, gid);
return p;
}
For more details, see the full implementation in Wifidog
in src/capabilities.c.