Tonight I read this weeks LWN quotes-page a little later than usual because I was busy at work for most of the day. Anyway as always LWNs content was awesome, and this particular list lead to an interesting discussion about a new Linux-Security-Module (LSM).
That read weirdly, what I was trying to say was that every Thursday morning I like to read LWN at work. Tonight is the first chance I had to get round to it.
One of the later replies in the thread was particularly interesting as it said:
Suggestion:
Create an security module that looks for the attribute
security.WHITELISTED
on things being executed/mmapped and denys it if the attribute
isn't present. Create a program (whitelistd) that reads
/etc/whitelist.conf and scans the system to ensure that only
things on the list have the attribute.
So I figured that was a simple idea, and it didn't seem too hard even for myself as a non-kernel non-developer. There are several linux security modules included in the kernel-releases, beneath the top-level security/
directory, so I assumed I could copy & paste code around them to get something working.
During the course of all this work, which took about 90 minutes from start to Finnish (that pun never gets old), this online documentation was enormously useful:
Brief attr
primer
If you're not familiar with the attr
tool it's pretty simple. You can assign values to arbitrary labels on files. The only annoying thing is you have to use extra-flags to commands like rsync
, tar
, cp
, etc, to preserve the damn things.
Set three attributes on the file named moi
:
$ touch moi
$ attr -s forename -V "Steve" moi
$ attr -s surname -V "Kemp" moi
$ attr -s name -V "Steve Kemp" moi
Now list the attributes present:
$ attr -l moi
Attribute "name" has a 10 byte value for moi
Attribute "forename" has a 5 byte value for moi
Attribute "surname" has a 4 byte value for moi
And retrieve one?
$ attr -q -g name moi
Steve Kemp
LSM Skeleton
My initial starting point was to create "steve_lsm.c
", with the following contents:
#include <linux/lsm_hooks.h>
/*
* Log things for the moment.
*/
static int steve_bprm_check_security(struct linux_binprm *bprm)
{
printk(KERN_INFO "STEVE LSM check of %s\n", bprm->filename);
return 0;
}
/*
* Only check exec().
*/
static struct security_hook_list steve_hooks[] = {
LSM_HOOK_INIT(bprm_check_security, steve_bprm_check_security),
};
/*
* Somebody set us up the bomb.
*/
static void __init steve_init(void)
{
security_add_hooks(steve_hooks, ARRAY_SIZE(steve_hooks), "steve");
printk(KERN_INFO "STEVE LSM initialized\n");
}
With that in place I had to modify the various KBuild
files beneath
security/
to make sure this could be selected as an LSM, and add in
a Makefile
to the new directory security/steve/
.
With the boiler-plate done though, and the host machine rebooted into my
new kernel it was simple to test things out.
Obviously the first step, post-boot, is to make sure that the module
is active, which can be done in two ways, looking at the output of dmesg
,
and explicitly listing the modules available:
~# dmesg | grep STEVE | head -n2
STEVE LSM initialized
STEVE LSM check of /init
$ echo $(cat /sys/kernel/security/lsm)
capability,steve
Making the LSM functional
The next step was to make the module do more than mere logging. In short
this is what we want:
- If a binary is invoked by root - allow it.
- Although note that this might leave a hole, if the user can enter a new namespace where their UID is 0..
- If a binary is invoked by a non-root user look for an extended attribute on the target-file named
security.WHITELISTED
.
- If this is present we allow the execution.
- If this is missing we deny the execution.
NOTE we don't care what the content of the extended attribute is, we just care whether it exists or not.
Reading the extended attribute is pretty simple, using the __vfs_getxattr
function. All in all our module becomes this:
#include <linux/xattr.h>
#include <linux/binfmts.h>
#include <linux/lsm_hooks.h>
#include <linux/sysctl.h>
#include <linux/ptrace.h>
#include <linux/prctl.h>
#include <linux/ratelimit.h>
#include <linux/workqueue.h>
#include <linux/string_helpers.h>
#include <linux/task_work.h>
#include <linux/sched.h>
#include <linux/spinlock.h>
#include <linux/lsm_hooks.h>
/*
* Perform a check of a program execution/map.
*
* Return 0 if it should be allowed, -EPERM on block.
*/
static int steve_bprm_check_security(struct linux_binprm *bprm)
{
// The current task & the UID it is running as.
const struct task_struct *task = current;
kuid_t uid = task->cred->uid;
// The target we're checking
struct dentry *dentry = bprm->file->f_path.dentry;
struct inode *inode = d_backing_inode(dentry);
// The size of the label-value (if any).
int size = 0;
// Root can access everything.
if ( uid.val == 0 )
return 0;
size = __vfs_getxattr(dentry, inode, "user.whitelisted", NULL, 0);
if ( size >= 0 )
{
printk(KERN_INFO "STEVE LSM check of %s resulted in %d bytes from 'user.whitelisted' - permitting access for UID %d\n", bprm->filename, size, uid.val );
return 0;
}
printk(KERN_INFO "STEVE LSM check of %s denying access for UID %d [ERRO:%d] \n", bprm->filename, uid.val, size );
return -EPERM;
}
/*
* The hooks we wish to be installed.
*/
static struct security_hook_list steve_hooks[] = {
LSM_HOOK_INIT(bprm_check_security, steve_bprm_check_security),
};
/*
* Initialize our module.
*/
void __init steve_add_hooks(void)
{
/* register ourselves with the security framework */
security_add_hooks(steve_hooks, ARRAY_SIZE(steve_hooks), "steve");
printk(KERN_INFO "STEVE LSM initialized\n");
}
Once again we reboot with this new kernel, and we test that the LSM
is active. After the basic testing, as before, we can now test real
functionality. By default no binaries will have the attribute
we look for present - so we'd expect ALL commands to fail, unless
executed by root. Let us test that:
~# su - nobody -s /bin/sh
No directory, logging in with HOME=/
Cannot execute /bin/sh: Operation not permitted
That looks like it worked. Let us allow users to run /bin/sh
:
~# attr -s whitelisted -V 1 /bin/sh
Unfortunately that fails, because symlinks are weird, but repeating the test with /bin/dash
works as expected:
~# su - nobody -s /bin/dash
No directory, logging in with HOME=/
Cannot execute /bin/dash: Operation not permitted
~# attr -s whitelisted -V 1 /bin/dash
~# attr -s whitelisted -V 1 /usr/bin/id
~# su - nobody -s /bin/dash
No directory, logging in with HOME=/
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
$ uptime
-su: 2: uptime: Operation not permitted
And our logging shows the useful results as we'd expect:
STEVE LSM check of /usr/bin/id resulted in 1 bytes from 'user.WHITELISTED' - permitting access for UID 65534
STEVE LSM check of /usr/bin/uptime denying access for UID 65534 [ERRO:-95]
Surprises
If you were paying careful attention you'll see that we changed what we
did part-way through this guide.
- The initial suggestion said to look for
security.WHITELISTED
.
- But in the kernel module I look for
user.whitelisted
.
- And when setting the attribute I only set
whitelisted
.
Not sure what is going on there, but it was very confusing. It appears
to be the case that when you set an attribute a secret user.
prefix is added to the name.
Could be worth some research by somebody with more time on their hands than I have.
Anyway I don't expect this is a terribly useful module, but it was my first, and I think it should be pretty stable. Feedback on my code certainly welcome!
Tags: kernel-programming, linux-security-module, lsm, lwn, security
|