xorl %eax, %eax

grsecurity’s GRKERNSEC_MODHARDEN Protection and the RDS Local Root Exploit

leave a comment »

I was recently reading that cool post by Christophe Devine regarding Dan Rosenberg’s RDS local privilege escalation vulnerability.
Of course, the main reason that ‘linux-rds-exploit.c‘ doesn’t work against grsecurity kernels is PAX_UDEREF as the post comments but ‘GRKERNSEC_MODHARDEN’ will prevent a widely used exploitation technique that was arrived along with auto-loading modules. As in this case, it is common to trigger an operation such as creating a socket in order to force the kernel into auto-loading the required to module. Since RDS is usually compiled as a module and it’s rarely used that’s what was happening in Dan Rosenberg’s exploit. The kernel was auto-loading the vulnerable module and then it was a common exploitation procedure.

GRKERNSEC_MODHARDEN is a simple code that is placed inside kernel/kmod.c like this:

#ifdef CONFIG_GRKERNSEC_MODHARDEN
	/* we could do a tighter check here, but some distros
	   are taking it upon themselves to remove CAP_SYS_MODULE
	   from even root-running apps which cause modules to be
	   auto-loaded
	*/
	if (current_uid()) {
		gr_log_nonroot_mod_load(module_name);
		return -EPERM;
	}
#endif

So, if the current_uid() isn’t that of root (aka 0) it will invoke gr_log_nonroot_mod_load() passing the requested module’s name and return with permission denied error code. The logging function is the following:

void
gr_log_nonroot_mod_load(const char *modname)
{
        gr_log_str(GR_DONT_AUDIT, GR_NONROOT_MODLOAD_MSG, modname);
        return;
}

That is defined like this:

#define gr_log_str(audit, msg, str) gr_log_varargs(audit, msg, GR_ONE_STR, str)

And here is gr_log_varargs():

void gr_log_varargs(int audit, const char *msg, int argtypes, ...)
{
	int logtype;
	char *result = (audit == GR_DO_AUDIT) ? "successful" : "denied";
	char *str1, *str2, *str3;
	void *voidptr;
	int num1, num2;
	unsigned long ulong1, ulong2;
	struct dentry *dentry;
	struct vfsmount *mnt;
	struct file *file;
	struct task_struct *task;
	const struct cred *cred, *pcred;
	va_list ap;

	BEGIN_LOCKS(audit);
	logtype = gr_log_start(audit);
	if (logtype == FLOODING) {
		END_LOCKS(audit);
		return;
	}
	va_start(ap, argtypes);
	switch (argtypes) {
	case GR_TTYSNIFF:
		task = va_arg(ap, struct task_struct *);
		gr_log_middle_varargs(audit, msg, &task->signal->curr_ip, gr_task_fullpath0(task), task->comm, task->pid, gr_parent_task_fullpath0(task), task->real_parent->comm, task->real_parent->pid);
		break;
	case GR_SYSCTL_HIDDEN:
		str1 = va_arg(ap, char *);
		gr_log_middle_varargs(audit, msg, result, str1);
		break;
	case GR_RBAC:
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		gr_log_middle_varargs(audit, msg, result, gr_to_filename(dentry, mnt));
		break;
	case GR_RBAC_STR:
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		str1 = va_arg(ap, char *);
		gr_log_middle_varargs(audit, msg, result, gr_to_filename(dentry, mnt), str1);
		break;
	case GR_STR_RBAC:
		str1 = va_arg(ap, char *);
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		gr_log_middle_varargs(audit, msg, result, str1, gr_to_filename(dentry, mnt));
		break;
	case GR_RBAC_MODE2:
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		str1 = va_arg(ap, char *);
		str2 = va_arg(ap, char *);
		gr_log_middle_varargs(audit, msg, result, gr_to_filename(dentry, mnt), str1, str2);
		break;
	case GR_RBAC_MODE3:
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		str1 = va_arg(ap, char *);
		str2 = va_arg(ap, char *);
		str3 = va_arg(ap, char *);
		gr_log_middle_varargs(audit, msg, result, gr_to_filename(dentry, mnt), str1, str2, str3);
		break;
	case GR_FILENAME:
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		gr_log_middle_varargs(audit, msg, gr_to_filename(dentry, mnt));
		break;
	case GR_STR_FILENAME:
		str1 = va_arg(ap, char *);
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		gr_log_middle_varargs(audit, msg, str1, gr_to_filename(dentry, mnt));
		break;
	case GR_FILENAME_STR:
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		str1 = va_arg(ap, char *);
		gr_log_middle_varargs(audit, msg, gr_to_filename(dentry, mnt), str1);
		break;
	case GR_FILENAME_TWO_INT:
		dentry = va_arg(ap, struct dentry *);
		mnt = va_arg(ap, struct vfsmount *);
		num1 = va_arg(ap, int);
		num2 = va_arg(ap, int);
		gr_log_middle_varargs(audit, msg, gr_to_filename(dentry, mnt), num1, num2);
		break;
	case GR_FILENAME_TWO_INT_STR:
		dentry = va_arg(ap, struct dentry *);
 		mnt = va_arg(ap, struct vfsmount *);
 		num1 = va_arg(ap, int);
 		num2 = va_arg(ap, int);
 		str1 = va_arg(ap, char *);
 		gr_log_middle_varargs(audit, msg, gr_to_filename(dentry, mnt), num1, num2, str1);
 		break;
 	case GR_TEXTREL:
 		file = va_arg(ap, struct file *);
 		ulong1 = va_arg(ap, unsigned long);
 		ulong2 = va_arg(ap, unsigned long);
 		gr_log_middle_varargs(audit, msg, file ? gr_to_filename(file->f_path.dentry, file->f_path.mnt) : "<anonymous mapping>", ulong1, ulong2);
 		break;
 	case GR_PTRACE:
 		task = va_arg(ap, struct task_struct *);
 		gr_log_middle_varargs(audit, msg, task->exec_file ? gr_to_filename(task->exec_file->f_path.dentry, task->exec_file->f_path.mnt) : "(none)", task->comm, task->pid);
 		break;
 	case GR_RESOURCE:
 		task = va_arg(ap, struct task_struct *);
 		cred = __task_cred(task);
 		pcred = __task_cred(task->real_parent);
 		ulong1 = va_arg(ap, unsigned long);
 		str1 = va_arg(ap, char *);
 		ulong2 = va_arg(ap, unsigned long);
 		gr_log_middle_varargs(audit, msg, ulong1, str1, ulong2, gr_task_fullpath(task), task->comm, task->pid, cred->uid, cred->euid, cred->gid, cred->egid, gr_parent_task_fullpath(task), task->real_parent->comm, task->real_parent->pid, pcred->uid, pcred->euid, pcred->gid, pcred->egid);
 		break;
 	case GR_CAP:
 		task = va_arg(ap, struct task_struct *);
 		cred = __task_cred(task);
 		pcred = __task_cred(task->real_parent);
 		str1 = va_arg(ap, char *);
 		gr_log_middle_varargs(audit, msg, str1, gr_task_fullpath(task), task->comm, task->pid, cred->uid, cred->euid, cred->gid, cred->egid, gr_parent_task_fullpath(task), task->real_parent->comm, task->real_parent->pid, pcred->uid, pcred->euid, pcred->gid, pcred->egid);
 		break;
 	case GR_SIG:
 		str1 = va_arg(ap, char *);
 		voidptr = va_arg(ap, void *);
 		gr_log_middle_varargs(audit, msg, str1, voidptr);
 		break;
 	case GR_SIG2:
 		task = va_arg(ap, struct task_struct *);
 		cred = __task_cred(task);
 		pcred = __task_cred(task->real_parent);
 		num1 = va_arg(ap, int);
 		gr_log_middle_varargs(audit, msg, num1, gr_task_fullpath0(task), task->comm, task->pid, cred->uid, cred->euid, cred->gid, cred->egid, gr_parent_task_fullpath0(task), task->real_parent->comm, task->real_parent->pid, pcred->uid, pcred->euid, pcred->gid, pcred->egid);
 		break;
 	case GR_CRASH1:
 		task = va_arg(ap, struct task_struct *);
 		cred = __task_cred(task);
 		pcred = __task_cred(task->real_parent);
 		ulong1 = va_arg(ap, unsigned long);
 		gr_log_middle_varargs(audit, msg, gr_task_fullpath(task), task->comm, task->pid, cred->uid, cred->euid, cred->gid, cred->egid, gr_parent_task_fullpath(task), task->real_parent->comm, task->real_parent->pid, pcred->uid, pcred->euid, pcred->gid, pcred->egid, cred->uid, ulong1);
 		break;
 	case GR_CRASH2:
 		task = va_arg(ap, struct task_struct *);
 		cred = __task_cred(task);
 		pcred = __task_cred(task->real_parent);
 		ulong1 = va_arg(ap, unsigned long);
 		gr_log_middle_varargs(audit, msg, gr_task_fullpath(task), task->comm, task->pid, cred->uid, cred->euid, cred->gid, cred->egid, gr_parent_task_fullpath(task), task->real_parent->comm, task->real_parent->pid, pcred->uid, pcred->euid, pcred->gid, pcred->egid, ulong1);
 		break;
 	case GR_RWXMAP:
 		file = va_arg(ap, struct file *);
 		gr_log_middle_varargs(audit, msg, file ? gr_to_filename(file->f_path.dentry, file->f_path.mnt) : "<anonymous mapping>");
 		break;
 	case GR_PSACCT:
 		{
 			unsigned int wday, cday;
 			__u8 whr, chr;
 			__u8 wmin, cmin;
 			__u8 wsec, csec;
 			char cur_tty[64] = { 0 };
 			char parent_tty[64] = { 0 };
 
 			task = va_arg(ap, struct task_struct *);
 			wday = va_arg(ap, unsigned int);
 			cday = va_arg(ap, unsigned int);
 			whr = va_arg(ap, int);
 			chr = va_arg(ap, int);
 			wmin = va_arg(ap, int);
 			cmin = va_arg(ap, int);
 			wsec = va_arg(ap, int);
 			csec = va_arg(ap, int);
 			ulong1 = va_arg(ap, unsigned long);
 			cred = __task_cred(task);
 			pcred = __task_cred(task->real_parent);
 
 			gr_log_middle_varargs(audit, msg, gr_task_fullpath(task), task->comm, task->pid, &task->signal->curr_ip, tty_name(task->signal->tty, cur_tty), cred->uid, cred->euid, cred->gid, cred->egid, wday, whr, wmin, wsec, cday, chr, cmin, csec, (task->flags & PF_SIGNALED) ? "killed by signal" : "exited", ulong1, gr_parent_task_fullpath(task), task->real_parent->comm, task->real_parent->pid, &task->real_parent->signal->curr_ip, tty_name(task->real_parent->signal->tty, parent_tty), pcred->uid, pcred->euid, pcred->gid, pcred->egid);
 		}
 		break;
 	default:
 		gr_log_middle(audit, msg, ap);
 	}
 	va_end(ap);
 	gr_log_end(audit);
 	END_LOCKS(audit);
 }

Finally, for completeness, here is the configuration information for that feature:

config GRKERNSEC_MODHARDEN
	bool "Harden module auto-loading"
	depends on MODULES
	help
	  If you say Y here, module auto-loading in response to use of some
	  feature implemented by an unloaded module will be restricted to
	  root users.  Enabling this option helps defend against attacks 
	  by unprivileged users who abuse the auto-loading behavior to 
	  cause a vulnerable module to load that is then exploited.

	  If this option prevents a legitimate use of auto-loading for a 
	  non-root user, the administrator can execute modprobe manually 
	  with the exact name of the module mentioned in the alert log.
	  Alternatively, the administrator can add the module to the list
	  of modules loaded at boot by modifying init scripts.

	  Modification of init scripts will most likely be needed on 
	  Ubuntu servers with encrypted home directory support enabled,
	  as the first non-root user logging in will cause the ecb(aes),
	  ecb(aes)-all, cbc(aes), and cbc(aes)-all  modules to be loaded.

On 7 November 2010, Dan Rosenberg committed an equivalent patch that for the Linux kernel as we can read here.
To do this, a new sysctl value was added named ‘modules_restrict’ that by default is disabled but by setting it to 1 non-root users cannot auto-load kernel modules and by setting it to 2, kernel modules cannot be either loaded or unloaded and that value cannot be changed.
The definitions were added at include/linux/module.h like this:

int module_proc_update_handler(struct ctl_table *table, int write,
	void __user *buffer, size_t *length, loff_t *ppos);

extern int modules_restrict; /* for sysctl */

And __request_module() that is used to request the loading of a module was patched to include the following code:

	/* Can non-root users cause auto-loading of modules? */
	if (current_uid() && modules_restrict)
		return -EPERM;

Quite similar to spender’s approach without the logging support. Also, kernel/module.c was updated to initialize the new integer and include a handler routine.

int modules_restrict = 0;
       ...
/* Proc update handler for modules_restrict sysctl */
int module_proc_update_handler(struct ctl_table *table, int write,
	void __user *buffer, size_t *length, loff_t *ppos)
{

	/* If module loading is entirely disabled, do not allow
	 * it to be re-enabled. */
	if (modules_restrict == 2)
		return -EPERM;

	return proc_dointvec_minmax(table, write, buffer, length, ppos);

}

Written by xorl

November 8, 2010 at 01:55

Leave a comment