CVE -2021-22555 利用过程详解

kernel exploit 有用的结构体 一、可用于 Leak/AAR/AAW/RIP劫持的结构体 说明目前缺少kmalloc-8、kmalloc-16、kmalloc-64、kmalloc-512结构体。 1. shm_file_data size: 0x20 kmalloc-32 内核基址:可泄露。其ns、vm_ops指针可以泄露 heap:可泄露。file指向堆区域 stack:不能泄露 劫持RIP:不能 产生:调用shmat()映射共享内存 释放:shmctl() ?? 备注:尝试过重写vm_ops,但是没发现调用fake_vtable函. 阅读详情

CVE-2021-22555 利用过程详解

原文:Andy Nguyen (theflow@, Google),CVE-2021-22555: Turning \x00\x00 into 10000$
https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
PoC:https://github.com/google/security-research/tree/master/pocs/linux/cve-2021-22555

阅读路线:〇 速览 → 一 漏洞本体 → 二 选型 → 三 地图 → 四~九 十二步主线 → 十~十三 复盘 / 防御 / 面试。

章内容什么时候看
〇 一页速览档案 + 一句话 + 十二步总表先花 1 分钟建立全局
一 漏洞本体触发路径、越界从哪来、原语能力想懂"漏洞本身"
二 选型:为什么是 msg_msg分配模型、两种链、三个"恰好"想懂"为什么这么打"
三 全链地图参与对象、尺寸接力、全链总览、读图约定读十二步之前先有坐标
四 Stage 0初始化—
五 Stage 1(①–⑥)建链 → 挖洞 → 越界写 → 定位 → 造 UAF #1主线
六 Stage 2(⑦–⑩)伪造 msg_msg → 两次越界读 → 升级 UAF #2主线,最难
七 Stage 3(⑪)pipe_buffer 占位 → 泄露 kbase主线
八 Stage 4(⑫)伪 ops → pivot → ROP主线
九 Stage 5setns + execve 逃逸主线
十 三次 UAF 对比与关键数字复盘、查数面试前
十一 整链时序与 PoC 输出时序图 + 实跑输出对照验证
十二 修复、检测与防御补丁 / 检测 / 加固—
十三 面试复述要点5 句背版面试前

〇 一页速览

0.1 档案

项内容
编号CVE-2021-22555
组件Linux Netfilter x_tables(net/netfilter/x_tables.c)
类型堆越界写(写 0),漏洞年龄 15 年
触发32 位兼容模式下 setsockopt(IPT_SO_SET_REPLACE / IP6T_SO_SET_REPLACE),需 CAP_NET_ADMIN(可在 user+net namespace 里拿到)
原语在 slab 对象尾部越界写最多 0x4C 字节的 0x00,偏移可控
影响本地提权 + 逃逸容器/K8s Pod(在 kCTF 拿 10000 , G o o g l e 配捐翻倍到 20000 ,Google 配捐翻倍到 20000 ,Google配捐翻倍到20000)
绕过SMEP + SMAP + KASLR + CONFIG_SLAB_FREELIST_HARDENED
修复v5.12-rc 起在分配时把对齐填充算进去
时间线2021-04-06 报告 → 04-13 上游合入补丁 → 07-07 公开

0.2 一句话

内核在把 32 位兼容结构转成 64 位时,memset() 补的对齐字节没有算进 kmalloc 的分配长度,于是在相邻 slab 对象上留下"几个 0x00";作者用 msg_msg 把这 2 个 0x00 变成指针重定向 → UAF → 泄露堆地址 → 泄露内核基址 → 劫持 pipe_buffer->ops 做栈迁移执行 ROP,从容器里拿到 root shell。

0.3 十二步总表

步Stage动作对象 / 缓存留下的悬空指针收获
①1msgsnd 4096B × 4096primary / kmalloc-4096—铺满 kmalloc-4096
②1msgsnd 1024B × 4096secondary / kmalloc-1024—每条队列成环 P_i ⇄ S_i
③1msgrcv 掉每第 1024 条 primaryprimary / 4096—4 个 kmalloc-4096 空洞
④1setsockopt 触发越界写xt_table_info / 4096—P_4.next:&S_4 → &S_3
⑤1msgrcv(MSG_COPY) 比对 tag——fake_idx / real_idx
⑥1msgrcv 从真队列取走 S_3secondary / 1024P_4.m_list.nextUAF #1
⑦2unix socket 喷 1024B 占位sk_buff 数据区 / 1024—一块内容全控的"假 msg_msg"
⑧2放大 m_ts 后越界读——邻居 S_x.next = &P_x
⑨2段链 next 改成 &P_x 再读——fake 自身地址(堆地址已知)
⑩2m_list 自环后 msgrcv 释放fake / 1024sk_buff 数据指针UAF #2(可释任意 kmalloc-1024 对象)
⑪3喷 pipe + read(socket)pipe_buffer[] / 1024pipe->bufsanon_pipe_buf_ops ⇒ kbase
⑫4喷伪造 pipe_buffer + close(pipe)伪造 pipe_buffer / 1024—ops->release → ROP → root

全链只做了一件事:把"改错一个指针"(④)换来的"一次 free 机会"(⑥),借 sk_buff 升级成"能 free 任意对象"(⑩),再把受害者换成带函数指针的 pipe_buffer(⑪⑫)。


一 漏洞本体

1.1 触发路径

用户态 32 位(compat)程序
  └─ setsockopt(fd, SOL_IP, IPT_SO_SET_REPLACE, &repl, size)
        └─ do_ipt_set_ctl()            net/ipv4/netfilter/ip_tables.c
              └─ translate_compat_table()
                    ├─ xt_compat_match_from_user()
                    ├─ xt_compat_target_from_user()   ← 漏洞在这里
                    └─ xt_alloc_table_info()          ← 分配(少算了 pad)

1.2 错在哪

// net/netfilter/x_tables.c
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
                                unsigned int *size)
{
    const struct xt_target *target = t->u.kernel.target;
    struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t;
    int pad, off = xt_compat_target_offset(target);
    u_int16_t tsize = ct->u.user.target_size;
    ...
    t = *dstptr;
    memcpy(t, ct, sizeof(*ct));
    if (target->compat_from_user)
        target->compat_from_user(t->data, ct->data);
    else
        memcpy(t->data, ct->data, tsize - sizeof(*ct));

    pad = XT_ALIGN(target->targetsize) - target->targetsize;   // ← 补到 8 字节对齐
    if (pad > 0)
        memset(t->data + target->targetsize, 0, pad);          // ← 越界写 0

    tsize += off;
    t->u.user.target_size = tsize;
    ...
    *size += off;
    *dstptr += tsize;
}

要点:

  1. xt_table_info 的分配长度是按 32 位结构长度 + off(xt_compat_target_offset) 算的,pad 没有计入。
  2. 但写入时按 64 位结构的真实 targetsize 定位,再往后补 pad 字节 0 —— 这 pad 字节就落到下一个 slab 对象上。
  3. targetsize 用户不可直接改,但可以按名字换 target(TCPMSS / TTL / NFQUEUE / NFLOG …),从而改变 pad 的宽度;再通过在 xt_entry_match 与 xt_entry_target 之间加 padding,可以把越界偏移往后再推。
  4. 作者找到的最大可用组合是 NFLOG:struct xt_nflog_info 里有 char prefix[64],可把越界偏移推到 0x4C。
struct xt_nflog_info {
    __u32 len; __u16 group; __u16 threshold;
    __u16 flags; __u16 pad;
    char  prefix[64];       // ← 长度可被用户拉长,用来调偏移
};

1.3 分配端:为什么是 kmalloc-512 ~ 8192

struct xt_table_info *xt_alloc_table_info(unsigned int size)
{
    size_t sz = sizeof(*info) + size;
    if (sz < sizeof(*info) || sz >= XT_MAX_TABLE_SIZE) return NULL;
    info = kvmalloc(sz, GFP_KERNEL_ACCOUNT);   // ← 注意:ACCOUNT
    memset(info, 0, sizeof(*info));
    info->size = size;
    return info;
}
  • 最小 sz > 0x100 ⇒ 最小落在 kmalloc-512,最大可用到 kmalloc-8192。
  • 用 GFP_KERNEL_ACCOUNT 分配 ⇒ 攻击对象也必须是 GFP_KERNEL_ACCOUNT(5.9 之前 memcg 用的是独立 slab 缓存,混用就永远撞不上)。

1.4 原语评估:为什么"写 0"很难打,作者怎么选的

常见打法是否可行原因
打引用计数✗前 0x4C 字节内找不到合适的、带 refcount 的对象
打 free list 指针✗内核已开 CONFIG_SLAB_FREELIST_HARDENED(CVE-2016-6187 那套失效)
打结构体里的指针✓ 但要巧一次写 4 字节 0 太狠:0xffff91a49cb7f000 → 0xffff91a400000000(废)。若只写块首 2 字节 0,则 0xffff91a49cb7f000 → 0xffff91a49cb70000(仍在同一 64 KB 区域内,大概率仍有效)

结论:把原语降级成"相邻对象起始处的 2 字节清零"。 于是问题变成:什么对象的第 0~1 字节是个指针、且清 2 字节后还能指向"合法且可控"的东西?


二 选型:为什么是 msg_msg

2.1 分配模型(ipc/msgutil.c: alloc_msg())

alen = min(len, DATALEN_MSG);                       // DATALEN_MSG = 4096 - 48 = 4048
msg  = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);   // 头段
while (len > 0) {                                    // 超出部分切成段链
    alen = min(len, DATALEN_SEG);                    // DATALEN_SEG = 4096 - 8 = 4088
    seg  = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
    *pseg = seg; seg->next = NULL; pseg = &seg->next; len -= alen;
}
struct msg_msg {                    // 头部 48 字节
    struct list_head m_list;        // 0x00: next, prev  —— 指向"队列里下一条消息"
    long  m_type;                   // 0x10
    size_t m_ts;                    // 0x18: 消息长度,决定 msgrcv 拷多少给用户
    struct msg_msgseg *next;        // 0x20: 指向下一段(不是队列里的下一条!)
    void *security;                 // 0x28
    /* 数据紧随其后 */
};
struct msg_msgseg { struct msg_msgseg *next; };

2.2 先分清两种"链"

头里有两个都叫 next 的东西,全篇的混淆几乎都来自这里:

链结构谁维护何时存在在本漏洞里的角色
① m_list 队列环形链struct list_head(双向环形)ipc/msg.c 的消息队列每条消息都在链上,单条消息是"自环"被破坏的目标:2 字节清零打在 m_list.next(offset 0)
② next 段链msg_msg.next → msg_msgseg.next → …alloc_msg() 超长分段仅当 payload > DATALEN_MSG(4048) 才存在本漏洞两条消息都 ≤ 4048 ⇒ 段链天然为 NULL,正好当成"可控 next 指针"来伪造

破坏 m_list.next 不需要"链是好的":msgrcv 只在真正删除消息时才 unlink,MSG_COPY 偷看只顺着 m_list.next 读 —— 所以指针指错时用"偷看"就能察觉,且不会立刻崩。

连同 sk_buff 内部那个指针,全篇一共出现 三个 next,对照表见 §6.3。

2.3 三个"恰好"

  1. m_list.next 在 offset 0 —— 正好是 2 字节清零能打到的地方,而且它是"队列内下一条消息"的指针,改它不会破坏对象自身的合法性。
  2. m_ts 可被改成更大的值 —— msgrcv 按 m_ts 决定拷贝长度 ⇒ 天然的信息泄露原语。
  3. 用 GFP_KERNEL_ACCOUNT —— 和 xt_table_info 同一套 kmalloc 缓存,能相邻。

三 全链地图

读十二步之前,先建立三样坐标:谁参与、凭什么能互相顶替、图怎么画。

3.1 参与对象与"尺寸接力"

对象大小缓存属性出现在哪几步
primary msg_msg48 + 4048 = 4096kmalloc-4096GFP_KERNEL_ACCOUNT①③④⑤
secondary msg_msg48 + 976 = 1024kmalloc-1024GFP_KERNEL_ACCOUNT②③⑥⑦⑧⑨⑩
xt_table_infosizeof + size,最小 > 0x100kmalloc-512 ~ 8192(本例 4096)GFP_KERNEL_ACCOUNT④
sk_buff 数据区1024kmalloc-1024GFP_KERNEL_ACCOUNT⑦⑧⑨⑩⑪
pipe_buffer[]16 × 40 = 640kmalloc-1024GFP_KERNEL_ACCOUNT⑪⑫

全链能接上的唯一原因:尺寸卡死在 1024。 secondary、sk_buff 数据区、pipe_buffer[] 三者必须落进同一个 kmalloc-1024 缓存,才能互相顶替同一个槽位 —— 这就是从 ② 一路传到 ⑫ 的"接力棒"。另一个隐含前提是所有对象都用 GFP_KERNEL_ACCOUNT(5.9 之前 memcg 用独立 slab 缓存,混用就永远撞不上)。

3.2 全链总览

Stage 0 初始化
user+net ns / socketpair / msgget×4096

Stage 1 ①–⑥
建链 → 挖洞 → 越界写 → 定位 → free
UAF #1:P_4 → 悬空 S_3

Stage 2 ⑦–⑩
伪造 msg_msg 占位 → 两次超读
⇒ 堆地址;自环后释放 ⇒ UAF #2

Stage 3 ⑪
pipe_buffer 占位 → 泄露 ops ⇒ kbase

Stage 4 ⑫
伪造 ops → pivot → ROP

Stage 5
setns + execve ⇒ 逃逸容器、root shell

3.3 读图约定

下面五张图都按同一套约定画:

  • 每个竖块 = 一块内核对象;块顶的蓝色横条 = msg_msg 头里的 m_list.next(offset 0x00);箭头 = 这个指针指向谁。
  • 左列 = kmalloc-4096(primary),右列 = kmalloc-1024(secondary);两列是不同 slab,同一列上下相邻 = 地址相邻。
  • 虚线块 = 已 kfree / 悬空;红框 = 已被篡改或已失效。
  • 图源 img/cve22555_chains.drawio(可编辑)。

四 Stage 0:准备

unshare(CLONE_NEWUSER|CLONE_NEWNET)  →  在新 netns 里拿到 CAP_NET_ADMIN
socketpair(AF_UNIX) × N              →  后续 spray sk_buff
msgget() × 4096                      →  4096 个消息队列(队列号 = 后续的 tag)

五 Stage 1:制造 UAF(步 ①–⑥)

目标:把一条 primary 的 m_list.next 指到"别人家的 secondary"上,再 free 掉那条 secondary —— primary 侧就留下一个自己不知道的悬空指针(UAF #1)。

5.1 ① 喷 A、② 喷 B:把两条链建起来

  • 4096 条 primary 落在 kmalloc-4096、4096 条 secondary 落在 kmalloc-1024,两块缓存互不干扰。
  • ① 每个队列只放 1 条 primary ⇒ next = prev = 自己(自环);4096 条把 kmalloc-4096 铺满,彼此相邻。
  • ② 再给每个队列发 1 条 secondary ⇒ 队列成环 P_i ⇄ S_i。P_i.next 就是第 ④ 步要清零的那个指针。

在这里插入图片描述

5.2 ③ 挖洞、④ 越界写:把 next 改到邻居

  • ③ 每第 1024 条 primary 被 msgrcv 掉 ⇒ kmalloc-4096 空出 4 个槽位。
  • ④ setsockopt 让 xt_table_info(同缓存)落进洞里,它紧邻的下一条 primary 就是受害者 P_4;xt_table_info 尾部溢出的 2 字节 0x00 正好打在 P_4.next 上。

在这里插入图片描述

为什么只写 2 字节,而且还能"指对":

  • P_4.next 原本指向 S_4(kmalloc-1024 的段)。清零低 2 字节 = 把指针"向下对齐到 64 KB 边界"。
  • 这一段 64 KB 里全是 message 对象 ⇒ 大概率仍指向另一条 secondary(比如 S_3)。
  • 若写满 4 字节,0xffff91a49cb7f000 会变成 0xffff91a400000000(区域性作废);只写 2 字节则落到 0xffff91a49cb70000,仍在同一 64 KB 区域内。
  • 成功率:只有"低 2 字节原本就是 0"才失败 ⇒ 1 − 1024 65536 ≈ 98.4 % 1 - \frac{1024}{65536} \approx 98.4\% 1−655361024​≈98.4%。

5.3 ⑤ 打 tag:怎么知道撞上了哪两条

每条消息(primary 与 secondary 的数据区)都写入自己队列的下标 [0, 4096):

用 msgrcv(MSG_COPY) 遍历所有队列,比较 primary 数据里的 tag 与 secondary 数据里的 tag:
  tag 一致   → 正常
  tag 不一致 → 找到了!
     primary 的 tag  = fake_idx(指向了"别人家的 secondary"的假队列)
     secondary 的 tag = real_idx(这条 secondary 真正的队列)

5.4 ⑥ 制造 UAF #1

msgrcv(real_idx 队列, 取下 secondary)   →  free(S_3),kfree 掉 kmalloc-1024 那块
但 fake_idx 队列里的 P_4.next 仍指向它  →  悬空引用(UAF #1)

在这里插入图片描述

  • 左半:P_4.next 已成悬空、S_3 已被 kfree;队列 3 恢复自环(整条链重新自洽,所以不会立刻崩)。
  • 右半:第 ⑦ 步用 unix socket 的 sk_buff 数据区(同样 1024B)把这个槽位抢回来,内容由我们逐字节决定 —— 见 §6.1。

六 Stage 2:绕过 SMAP(步 ⑦–⑩)

SMAP 禁止内核直接读写用户地址,所以不能把 next 指向用户态伪造的结构;必须"用内核堆里的东西伪造内核堆里的东西"。

6.1 ⑦ 用 unix socket 精确占位

sendmsg() 到 socketpair 时,sk_buff 的数据缓冲区同样走 GFP_KERNEL_ACCOUNT,kmalloc(1024) 的 payload 里可以逐字节构造内容。喷一堆 1024 字节,把刚才 free 掉的槽位抢回来,内容伪装成 struct msg_msg:

字段伪造值目的
m_list.next/prev0x41414141故意给"非法指针"⇒ 之后无法 unlink/再释放(这是想要的,避免二次 free 提前触发检测)
m_type任意—
m_ts4048(DATALEN_MSG)放大长度,让 msgrcv 越过原 1024 边界读
nextNULL单段

6.2 第 ⑧ 步到底在读什么(关键一步)

copy_msg() 的拷贝只由一个长度参数驱动,而且从头部之后开始拷:

// ipc/msgutil.c
len  = src->m_ts;                 // ← 伪造时改成 4048
alen = min(len, DATALEN_MSG);     // DATALEN_MSG = 4096 - 48 = 4048
memcpy(dst + 1, src + 1, alen);   // src+1 = offset 0x30,拷 4048 字节

src 是 fake 对象——它是 kmalloc-1024 里的一个槽,总共只有 1024 字节。所以内核实际读的范围是:

内核读的区间内容归属
[fake+0x00, fake+0x30)48B 头(m_list / m_type / m_ts / next / security)自己
[fake+0x30, fake+0x400)976B 数据区自己
[fake+0x400, fake+0x1000)3072B 越界区别人

越界区的开头 [fake+0x400, fake+0x430) 正好是同 slab 里紧挨着的下一条 secondary S_x 的 48 字节头。换算到用户缓冲区(偏移要减去跳过的 0x30):

用户缓冲区偏移拿到的是
0 – 975fake 自己的数据(没用)
976(0x3D0)– 1023S_x.m_list.next / m_list.prev(两个内核指针)
1024 起S_x.m_type / m_ts / next / security …

两个必须理解的点:

  1. 为什么不崩:kmalloc-1024 的 slab 是一整块连续页(几页、几十个对象),多读 3 KB 仍落在同一 slab 的页里,既不跨未映射页,也不会触发任何长度检查——memcpy 只是照单搬字节。
  2. 为什么这个指针有用:S_x 是队列 x 里的第二条消息,而队列是环形双向链 P_x ⇄ S_x,所以 S_x.m_list.next 必然等于 &P_x。⇒ 第一次泄露拿到 primary 的地址。

6.3 第 ⑨ 步:把"越界读"变成"任意地址读一跳"

先分清三个都叫 next 的东西——这是全篇最容易混的地方:

字段位置谁用它fake 里填什么
m_list.next / prevfake+0x00 / +0x08msgrcv 真正删除消息时的 unlink⑦ 填 0x41414141(故意不可 unlink ⇒ 不会被误释放);⑩ 才改成 &fake 自环(可以 unlink ⇒ 能释放)
msg_msg.next(类型是 struct msg_msgseg *)fake+0x20copy_msg() 拷完头段后顺着它继续拷⑦ 是 NULL;⑨ 改成 &P_x ← 就这一步改它
sk_buff 里指向数据区的指针(head/data)在 sk_buff 结构里,不在这块内存中socket 收发时读写从头到尾没动过,一直指着这块 1024B —— 这才是"悬空引用"的来源

所以 ⑨ 改的不是 sk_buff 的指针,也不是队列里的 m_list(那个到 ⑩ 才动),而是 fake msg_msg 头里 offset 0x20 的段链指针 next:把 NULL 换成 &P_x。

改内容的方法(这块内存不在我们手上,只能"释放 → 重喷"):

  1. read() 一下 unix socket ⇒ sk_buff 释放它的数据区,这块 1024B 重新空出来(但 sk_buff 和 P_4.next 都还指着它);
  2. 再 sendmsg() 喷 1024 字节新内容(next = &P_x、m_ts 加大)⇒ 同一个槽位被重新占住;
  3. 再从 fake 队列 msgrcv(MSG_COPY),内核这次就按新内容走。

copy_msg 拷满 4048 字节后会顺着 next 继续拷,于是 跳到 P_x+8 处接着读(跳过 msg_msgseg.next 那 8 字节):

fake 头 → fake 数据 4048B → next = &P_x → 从 P_x+8 继续拷贝

读出来的就是 P_x 的 m_list.prev / next。而 P_x.m_list.next = &S_x —— 而 S_x 正是第 ⑧ 步读到的、紧挨在 fake 后面(fake+0x400) 的那个对象。于是:

& f a k e = & S x − 0 x 400 \&fake = \&S_x - 0x400 &fake=&Sx​−0x400

原文那句 “Subtracting 1024 from that address, we finally have the address of the fake message” 说的就是这个。

[+] kheap_addr: ffff91a49cb7f000      ← 第 ⑧ 步:相邻 secondary / primary 地址
[+] kheap_addr: ffff91a49c7a0000      ← 第 ⑨ 步:得到 fake 自身地址

一句话记住:⑧ 用"读得多"泄漏邻居,⑨ 用"next 指哪读哪"确认自己。两步合起来,把"我只知道有个指针指错了"变成"我知道自己在堆上的确切地址"。

6.4 ⑩ 把 UAF #1 升级成 UAF #2

现在伪造对象的地址已知,就可以把 m_list.next = m_list.prev = 自己(自环)⇒ 这条伪造消息可以被安全地 unlink / 释放了。

但占位它的是 sk_buff 的数据缓冲区,sk_buff 里还留着指向它的指针。于是 msgrcv 把它 free 掉之后,sk_buff 侧就成了悬空引用:

在这里插入图片描述

  • 左图(⑦⑧⑨):m_ts = 4048 让内核从 fake 往外多搬 3072B,抄到邻居 S_x.next = &P_x;再把 fake 头 offset 0x20 的段链 next(不是 sk_buff 的指针,也不是 m_list)改成 &P_x 顺着段链折回,读到 P_x.next = &S_x,于是 fake = &S_x − 0x400(推导见 §6.2 / §6.3)。
  • 右图(⑩):m_list 改成自环 ⇒ 这条 fake 能被安全 unlink / 释放;但 sk_buff 仍指着它 ⇒ UAF #2。

同一块内存的两个身份(图 D 的本质):

⑧⑨⑩ 三步全都围绕同一块 1024B 内存,而它在两个子系统眼里是两个完全不同的东西:

视角这块内存是什么谁持有指向它的指针能对它做什么
socket / sk_buffsk_buff 的数据区(skb->head)sk_buff —— 全程没被改过只能 sendmsg 重写内容
消息队列 / msg_msg一条 msg_msg(48B 头 + 976B 数据)P_4.m_list.next —— 第 ④ 步被改出来的悬空指针msgrcv(MSG_COPY) 读(⑧⑨);msgrcv 删除 ⇒ kfree(⑩)

所以三者的分工是:

  • P_4.m_list.next 只是"把手":它让我们能用标准 msgrcv 去操作这块内存。它本身不是"受害者",也没被谁再篡改一次。
  • 内容是 fake msg_msg:m_ts 决定拷多少(⑧ 放大到 4048)、next(+0x20,段链)决定拷到哪(⑨ 指 &P_x)、m_list 决定能不能被释放(⑩ 改成自环)。
  • sk_buff 的指针始终不变 —— 正因为不变,⑩ 用 msg_msg 路径 kfree 掉这块内存后,sk_buff 才成了悬空引用。

两条"释放"路,别搞混:

目的手段谁真的释放这块内存sk_buff 的处境
想换内容(⑧→⑨、⑨→⑩ 之间重造 fake)先 read(socket),再 sendmsg 重喷sk_buff 自己(read 时连带销毁旧 skb)旧 sk_buff 消失,新喷的 sk_buff 指向同一个槽位
想留悬空引用(⑩)msgrcv(fake 队列)msg_msg 路径(要求 m_list 能 unlink)仍然有效、仍指向那块已释放内存 ⇒ 悬空

P_4.m_list.next 这把"把手"的全部价值就在这里:它给了我们第二条释放路径 —— 用 msg_msg 的 API 去 kfree 一块 sk_buff 正在用的内存,于是造出一个 sk_buff 侧完全不知道的悬空引用。

一句话:不是 msg_msg 篡改了 sk_buff 的指针,而是两者共用同一块内存 —— msg_msg 是"演员",sk_buff 是不知情的"观众",P_4.m_list.next 是让演员上台的那张"门票"。

为什么第 ⑩ 步是"更好的 UAF"(这一步是全链的枢纽):

之前(free 掉一个 msg_msg)现在(free 掉一个 sk_buff 数据区)
只能释放满足"前两个成员是可写指针"的对象可以释放这个 slab 上的任意对象
且必须能构造合法 unlink无需任何头部约束

七 Stage 3:绕过 KASLR(步 ⑪)

7.1 为什么是 pipe_buffer

要求:GFP_KERNEL_ACCOUNT、落在 kmalloc-1024(和 secondary 同尺寸)、结构里有我们能用的指针。

// fs/pipe.c
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer), GFP_KERNEL_ACCOUNT);
// 16 * 40 = 640  → kmalloc-1024  ✓

struct pipe_buffer {
    struct page *page;
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;   // ← 间接函数指针
    unsigned int flags;
    unsigned long private;
};
static const struct pipe_buf_operations anon_pipe_buf_ops = {   // 在 .data
    .release = anon_pipe_buf_release, ...
};

写 pipe 时 pipe_buffer->ops = &anon_pipe_buf_ops。.data 与 .text 的相对偏移在给定内核镜像里固定 ⇒ 拿到 anon_pipe_buf_ops 就等于拿到 kbase。

7.2 泄露

spray 大量 pipe 并 write()  ← 抢占悬空 sk_buff 数据区
read(unix socket)           ← 通过悬空的 sk_buff 读回整块内容
                             → 泄露 anon_pipe_buf_ops 地址 → kbase
[+] anon_pipe_buf_ops: ffffffffa1e78380
[+] kbase_addr:        ffffffffa0e00000        (差值 0x1078380,镜像内固定)

顺带:read() 会把 sk_buff 的数据区再 free 一次 ⇒ 又出现一个悬空 pipe_buffer 槽位,且地址已知。


八 Stage 4:代码执行(步 ⑫)

8.1 结构关系

ops

release

伪造 pipe_buffer
(重新喷回原地址)

伪造 pipe_buf_operations
(放在已知堆地址)

栈迁移 gadget(pivot)

内核 ROP 链
(也在已知堆地址)

因为漏洞本身让我们知道堆地址,可以把假 pipe_buf_operations 和 ROP 链一起放在同一块可控内存里。

8.2 ROP 链做什么

  1. 先把 RBP 存到一个内核 scratch 地址(为了最后能干净地返回);
  2. commit_creds(prepare_kernel_cred(NULL)) —— 装 root 凭据;
  3. switch_task_namespaces(find_task_by_vpid(1), init_nsproxy) —— 把自己切到 init 进程的 namespace;
  4. 恢复 RBP 并返回 ⇒ free_pipe_info() 正常返回,不崩。

8.3 触发

close(所有 pipe fd)  →  pipe_release() → pipe_buf_release() → ops->release(...)  ← 落地执行

为什么要"尽快栈迁移":JOP gadget 难找且脆弱。先用一个 pivot gadget 把栈搬到我们控制的内存,然后跑完整的 ROP 链最稳。

  • 左半(⑪):pipe_buffer[] 占位(16×40=640B,同落 kmalloc-1024)⇒ 通过悬空的 sk_buff 读回 ops ⇒ kbase。
  • 右半(⑫):喷回伪造 pipe_buffer → ops 指向伪造的 pipe_buf_operations → release 是个 pivot gadget → 落到堆上的 ROP 链 → commit_creds + switch_task_namespaces。

在这里插入图片描述

九 Stage 5:逃逸容器、拿 root shell

setns(open("/proc/1/ns/mnt", O_RDONLY), 0);
setns(open("/proc/1/ns/pid", O_RDONLY), 0);
setns(open("/proc/1/ns/net", O_RDONLY), 0);
char *args[] = {"/bin/bash", "-i", NULL};
execve(args[0], args, NULL);

(此时已是 root,才有权限 setns 到 PID 1 的 namespace —— 从而跳出 Pod。)


十 三次 UAF 对比与关键数字

前面按 Stage 讲"怎么做",这里按"哪条指针悬空了"横向对比 —— 面试最常追问的就是这个。

10.1 三次 UAF 的区别(最容易被追问)

UAF#1(步 ⑥)UAF#2(步 ⑩)UAF#3(步 ⑪)
悬空的是P_4.m_list.next(链表指针)sk_buff 的数据指针pipe->bufs 数组指针
谁能触发它msgrcv(fake 队列)read() unix socketclose(pipe)
能顶替什么只能顶替 msg_msg(头部要合法)任意 kmalloc-1024 对象只能顶替 pipe_buffer[]
用途起手(把指针引导到我们手里)换受害者(换成带函数指针的对象)落地(把假 ops 塞进去)

10.2 关键数字速查

数字含义
DATALEN_MSG = 40484096 - sizeof(struct msg_msg),m_ts 被改大后的泄露上限
DATALEN_SEG = 40884096 - sizeof(struct msg_msgseg)
sizeof(struct msg_msg) = 48list_head 16 + m_type 8 + m_ts 8 + next 8 + security 8
4096 / 1024primary / secondary 消息总长(→ kmalloc-4096 / kmalloc-1024)
4096消息队列个数(= tag 空间)
每 1024 条挖 1 个洞洞中放 xt_table_info,让它紧邻一条 primary
2 字节实际清零的字节数(不是 4 字节,为了保 64 KB 内偏移)
1 - 1024/65536 ≈ 98.4%重定向命中"合法 secondary"的概率
0x4CNFLOG target 下可达到的最大越界偏移
kmalloc-512 ~ kmalloc-8192xt_table_info 可选的目标尺寸区间
1024pipe_buffer 数组(16×40=640)落入的缓存

十一 整链时序与 PoC 输出

Kernel 用户态 exploit Kernel 用户态 exploit STAGE 0 初始化 STAGE 1 制造 UAF(①–⑥) STAGE 2 绕过 SMAP(⑦–⑩) STAGE 3 绕过 KASLR(⑪) STAGE 4 代码执行(⑫) STAGE 5 收尾 unshare(user+net ns) / socketpair / msgget ×4096 ① msgsnd primary ×4096 (4096B) ② msgsnd secondary ×4096 (1024B) ③ msgrcv 释放每 1024 条中的 primary(挖洞) ④ setsockopt(IPT_SO_SET_REPLACE) → 越界写 2 字节 0 ⑤ msgrcv(MSG_COPY) 全队列比对 tag fake_idx / real_idx ⑥ msgrcv(real_idx) → free secondary(留下悬空 next) ⑦ unix socket spray 伪造 msg_msg(m_ts 放大) ⑧ msgrcv(MSG_COPY) 越界读 → 泄露邻居地址 ⑨ 段链 next=&P_x 再读 → fake 自身地址 ⑩ 自环后 msgrcv → free(sk_buff 悬空) ⑪ spray pipe_buffer 抢占 ⑪ read(socket) → 泄露 anon_pipe_buf_ops → kbase ⑫ spray 伪造 pipe_buffer(ops→假 ops) ⑫ close(pipes) → ops->>release → pivot → ROP commit_creds / switch_task_namespaces setns(mnt/pid/net) + execve("/bin/bash") root shell

PoC 实跑输出:

[+] STAGE 1: Memory corruption
[*] Triggering out-of-bounds write...
[*] Searching for corrupted primary message...
[+] fake_idx: ffc
[+] real_idx: fc4
[+] STAGE 2: SMAP bypass
[+] kheap_addr: ffff91a49cb7f000
[+] kheap_addr: ffff91a49c7a0000
[+] STAGE 3: KASLR bypass
[+] anon_pipe_buf_ops: ffffffffa1e78380
[+] kbase_addr: ffffffffa0e00000
[+] STAGE 4: Kernel code execution
[+] Root privileges gained.
[+] STAGE 5: Post-exploitation
root@theflow:/# id
uid=0(root) gid=0(root)

十二 修复、检测与防御

  • 修复思路:把 pad 计入分配长度(xt_compat_target_from_user() 与 xt_alloc_table_info() 的 size 计算对齐);上游补丁见 2021-04-13 合入。
  • 检测:内核日志中 setsockopt 大量失败/xt_table_info 分配异常;进程在 user+net ns 中频繁做 msgsnd 大批量消息 + socketpair + pipe 组合(典型 spray 指纹)。
  • 防御:
    • 允许 unprivileged user namespace 是这类漏洞的"启动条件"(kernel.unprivileged_userns_clone=0 或 seccomp 禁 unshare(CLONE_NEWUSER));
    • 容器侧用 gVisor / Kata 等强隔离;
    • CONFIG_SLAB_FREELIST_HARDENED、CONFIG_SLAB_FREELIST_RANDOM、CONFIG_INIT_ON_ALLOC_DEFAULT_ON(后者能把"伪造对象"这条路堵掉一大半);
    • 及时打补丁(5.12-rc 之后)。

十三 面试复述要点(5 句)

  1. 漏洞本质:compat 转换时 memset 的 pad 没进化分配长度 → 相邻对象尾部被写 0。
  2. 原语:可控偏移的少量 0x00 越界写,GFP_KERNEL_ACCOUNT,slab 512~8192。
  3. 关键创新:msg_msg 的 m_list.next 在 offset 0 —— 2 字节清零就能把指针"向下对齐",重定向到同 64KB 区内另一条消息,从而两个队列共享一个对象 → UAF。
  4. 泄露链:伪造 msg_msg 放大 m_ts ⇒ 越界读 ⇒ 堆地址;再用 pipe_buffer->ops → anon_pipe_buf_ops(.data,距 kbase 固定)⇒ 内核基址。
  5. 执行链:借 sk_buff 悬空指针把 UAF 泛化成"可释放任意对象",劫持 pipe_buffer->ops->release,pivot 到 ROP,commit_creds + switch_task_namespaces,setns 逃逸容器。
记一次Docker容器失败的逃逸 在授权渗透中碰到某客户搭建某开源漏洞环境。试试水,没想到这么菜😂!你这菜鸟,要你有何用。 阅读详情

相关推荐

google protobuf的应用

服务器端与客户端通信,使用google protobuf作为交互数据的序列化工具,其中客户端使用select机制实现I/O复用,服务端使用epoll机制,提高并发连接时的处理效率。软件环境:linux。文件dealpack.cc是服务端和客户端共有的工具包。

一个内核漏洞详解:容器逃逸

CVE-2021-22555:一个影响2006年(Linux kernel v2.6.19-rc1 发布)至今(Linux kernel v5.12-rc8)的所有Linux内核版本的漏洞,可导致本地提权与容器逃逸;该漏洞是个内核级漏洞,跟Linux的发行版本没有关系,也就是说只要Linux 内核版本在v2.6.19-rc1 ~v5.12-rc8 之间的内核,都存在被黑客利用该漏洞攻击的可能。 该漏洞是由Andy Nguyen (theflow@)发现,于2021年07月16日发布。换句话说,该漏洞已

flynetcn的专栏 3379

别再纠结选哪个向量库了!我用Spring AI + Redis Stack 7.2,5分钟搞定企业知识库RAG原型

本文介绍了如何利用Spring AI和Redis Stack 7.2快速构建企业级RAG应用,解决向量数据库选型难题。Redis Stack 7.2集成了RediSearch模块,支持向量相似性搜索,具备高性能、低成本、零学习成本等优势,特别适合中小团队快速搭建知识库系统。通过实战代码示例,展示了从环境准备到核心功能实现的完整流程。

weixin_29061041的博客 272

关于内核堆溢出漏洞的分析

然后,以相同的方式重新构造伪次要消息,这次构造的m_ts字段要大于消息分片的阈值,next字段等于相邻次要消息所指向的主要消息的地址-结构msg_msgseg的长度,这样做相当于将该主要消息伪造成下一个消息片段,那么在读取伪次要消息时,就可以读取该主要消息的next指针,该指针指向相邻次要消息,将指针内容减去1024即可获得伪次要消息即关键缓冲区的地址。通过msgsnd给每个消息队列都发送一个4096字节的消息,暂且称这些消息为主要消息,每个消息的内容是其所在消息队列的序号,分别为0-4095。

m0_59598029的博客 537

BPMN-JS :修改节点“Change Type”事件类型

修改路径:node_modules\bpmn-js\lib\features\replace\ReplaceOptions.js。

TaylorY的博客 679

别再花钱买商业软件了!这4款免费DIC工具,从散斑生成到应变分析全搞定

本文介绍了4款免费开源的DIC(数字图像相关技术)工具,涵盖从散斑生成到应变分析的全流程,帮助用户零成本构建专业级DIC工作流。这些工具在精度、算法透明度和生态整合方面具有优势,适用于材料力学和实验力学研究,可替代昂贵的商业软件。

weixin_42525369的博客 168

所学漏洞总结

一、CVE-2021-28474 MS SharePoint远程代码执行漏洞 1、漏洞简介 漏洞允许通过身份验证的用户在SharePoint web应用程序的服务账户中执行任意.NET代码。想要利用该漏洞,攻击者需要具备SharePoint站点的SPBasePermissions.ManageLists权限。默认情况下,经过身份验证的SharePoint用户可以创建网页页面,并拥有所需的所有权限。 2、漏洞成因 用于安全验证的代码和实际处理用户输入的代码之间不一致。 EditingPagePars

weixin_45540609的博客 1115

实验五 循环结构第1关:斐波那契数列第2关:统计一段英文语句中的单词数和句子数第3关:密码判断第4关:通过数列求自然常数e第5关:双倍余额递减法计算固定资产折旧第6关:销售统计第7关:投资年限计算

第1关:斐波那契数列第2关:统计一段英文语句中的单词数和句子数第3关:密码判断 第4关:通过数列求自然常数e 第5关:双倍余额递减法计算固定资产折旧

pro_daily的博客 8168

CVE漏洞复现-CVE-2021-22555 Linux Netfilter 权限提升漏洞

近日,互联网公开了Linux Netfilter权限提升漏洞的POC及EXP,相关CVE编号:CVE-2021-22555。该漏洞在kCTF中被用于攻击kubernetes pod容器实现虚拟化逃逸,该漏洞已在Linux内核代码中存在15年,Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。

把自己活成一道光,因为你不知道,谁会借着你的光,走出了黑暗。请保持心中的善良,因为你不知道,谁会借着你的善良,走出了绝望。请保持你心中的信仰,因为你不知道,谁会借着你的信仰,走出了迷茫。请相信自己的力量,因为你不知道,谁会因为相信你,开始相信了自己.... 3975

大模型——Ollama简明教程

在这篇文章中,我们将了解如何使用 Ollama、如何在 Ollama 中创建自己的模型 以及如何使用 Ollama 构建聊天机器人。这是深入研究 Ollama 的第一部分,以及我所学到的有关本地 LLM 的知识以及如何将它们用于基于推理的应用程序。在这篇文章中,你将了解——

1691

手把手教你学simulink(16.2)--基于Simulink的高通滤波器(High-Pass Filter)项目

基于Simulink的高通滤波器(High-Pass Filter)项目的详细实例介绍。这个项目将重点放在高通滤波器的设计、实现和仿真上,帮助你理解和应用高通滤波器在信号处理中的作用。

MHD0815的博客 926

Linux内核堆溢出漏洞原理与利用技术详解

堆溢出是内存安全领域的核心漏洞类型,其本质是程序对动态分配的内存区域执行了越界写入操作。在Linux内核环境中,由于采用SLAB/SLUB这类高效但复杂的内存分配器,堆溢出可能引发权限提升(Privilege Escalation)等严重后果。内核堆与用户态堆存在关键差异:地址空间布局随机化(ASLR)缺失、分配粒度更大、错误后果更严重。典型利用技术如SLUB Feng Shui和Freelist Poisoning通过精确控制内存布局实现攻击。防御方面,现代内核采用CONFIG_SLAB_FREELIST

weixin_29324877的博客 325

YOLO26涨点改进 | 独家主干改进篇 | CVPR 2024 | YOLO26利用InceptionNeXt主干,将大核深度卷积分解为四个并行分支,在提升性能的同时显著降低计算效率

本文聚焦YOLO26的核心涨点改进——基于CVPR 2024收录的InceptionNeXt主干网络,深度拆解了其“大核深度卷积四分支拆解”的核心逻辑,通过实验验证了改进的有效性,同时提供了可快速复现的工程代码与进一步涨点的思路。

qq_36130719的博客 574

从DEFCON CTF到云原生安全:容器逃逸与内核漏洞利用实战解析

容器安全是云原生架构中不可忽视的基石,而容器逃逸往往成为攻破整个集群的关键跳板。由于容器与宿主机共享内核,一旦内核出现可利用的漏洞,隔离机制就可能被击穿,攻击者将获得宿主机权限。CVE-2021-22555作为Netfilter模块的堆越界写漏洞,正是这一风险的典型代表,其利用过程涉及内核态攻击链、堆布局操纵与modprobe_path劫持等热点技术。在真实云环境与容器服务中,内核漏洞的影响范围被进一步放大,合理配置seccomp、capabilities以及纵深防御策略显得尤为重要。本文以DEFCON C

weixin_29323551的博客 269

MATLAB 基于深度学习的步态识别算法仿真

步态识别是一种通过分析人体行走方式来识别身份的技术。基于深度学习的步态识别算法利用卷积神经网络 (CNN) 提取步态特征,并通过分类器进行身份识别。基于深度学习的步态识别算法是实现身份识别的重要手段,具有广泛的应用前景。MATLAB 提供了丰富的工具和函数,可以方便地进行算法仿真和性能评估。

走向CTO的路上... 518

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

Linux内核条件竞争漏洞原理与防御实践

条件竞争是多线程编程中的典型并发问题,当多个执行流对共享资源进行非原子访问时,会导致不可预期的程序行为。在操作系统内核中,这类漏洞可能引发权限提升等严重安全问题。其技术本质在于临界区保护缺失或同步机制不当,攻击者通过精确控制线程调度时序,可以在检查与使用(TOCTOU)的时间窗口内篡改系统状态。现代防御方案结合原子操作、内存隔离和运行时检测等技术,其中引用计数加固和KASLR是当前主流防护手段。通过分析CVE-2021-22555等典型案例可见,文件描述符处理和内存管理子系统是条件竞争的高发区域,开发中应特

weixin_29324401的博客 258

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Linux提权 Netfilter

Netfilter 是一个 Linux 内核模块,主要提供数据包过滤(packet filtering)、网络地址转换(network address translation,)和其他与防火墙相关的工具。

qq_33522010的博客 361

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

小迪安全v2023学习笔记(九十六讲)—— 云原生篇&Docker安全&系统内核&版本漏洞&CDK自动利用&容器逃逸

本文摘要:本文主要探讨Docker容器逃逸的第二部分内容,聚焦于Docker自身漏洞导致的逃逸风险。课程介绍了利用系统内核漏洞(如DirtyCow、DirtyPipe等)和Docker软件版本漏洞(如CVE-2019-5736、CVE-2020-15257等)的逃逸方法。重点演示了CVE-2019-5736(runC容器逃逸)的完整利用过程,包括安装漏洞版本Docker、启动测试环境、编译并执行EXP等步骤,最终成功获取宿主机Shell。文章还介绍了自动化工具CDK的部分用法,并列举了多个近年披露的容器逃逸

uli0n的博客 3003

DEFCON CTF 2021 Qualifiers: cloud_abuser 容器逃逸与内核堆风水利用分析

堆风水精要通过msg_msg精确控制SLUB分配利用m_ts整数溢出实现OOB写结合security指针劫持控制流容器逃逸新向量题目揭示云环境中"容器内提权→内核漏洞→宿主机逃逸"的三段式攻击链,与真实漏洞利用高度相似。

Alfadi联盟 萧瑶的博客 1038

Docker安全漏洞与容器逃逸防御指南

云原生技术的普及使得 Docker 成为容器化部署的核心工具,但其安全性问题不容忽视。Docker 的安全涉及系统内核、版本漏洞、容器逃逸等多个方面,攻击者可能通过内核漏洞或配置缺陷实现逃逸。

VIz_dfgv的博客 387

Google CTF 2021 “Fullchain“ 题目深度剖析

技术经验提炼信息收集:通过内核符号泄露(kallsyms)绕过KASLR是内核利用的黄金起点堆利用:SLUB分配器的确定性行为(如msg_msg)是稳定利用的关键工具应用:QEMU+GDB调试内核时,lx-symbols命令自动加载符号题目设计点评创意:完美结合现实漏洞与CTF环境(Docker逃逸场景)难度:Hard(需完整掌握Linux内核漏洞利用链)学习价值:理解现代内核防护绕过技术的绝佳案例红队实战建议。

Alfadi联盟 萧瑶的博客 904

CFI技术新探索,struct_san今日登场

C/C++开发的应用程序,长久以来存在内存破坏类的安全问题。当攻击者掌握了目标程序的漏洞后,就可以开发漏洞利用程序劫持目标程序的控制流。早期的漏洞利用是采用代码注入的方式,通过在缓冲区置入一段代码(shellcode),然后控制pc寄存器跳入到缓冲执行这段代码。为了阻止此类攻击,后来计算机系统部署了DEP(DataExecutionPrevention)机制,通过配置内存页属性,将缓冲区设置成不可执行,起到了很好的防御效果。

yy1715713348的博客 402

一个内核网络漏洞详解|容器逃逸

CVE-2021-22555:一个影响2006年(Linux kernel v2.6.19-rc1 发布)至今(Linux kernel v5.12-rc8)的所有Linux内核版本的漏洞...

极客重生 1335

终极指南:Linux内核漏洞深度解析与Google安全研究项目中的CVE实战案例

在当今数字化时代,Linux内核作为众多操作系统的核心,其安全性至关重要。Google安全研究项目(se/security-research)致力于发现并修复Linux内核中的安全漏洞,为全球用户提供更安全的计算环境。本文将深入剖析Linux内核漏洞的类型、危害以及Google安全研究团队披露的典型CVE案例,帮助新手和普通用户更好地理解内核安全。 ## 一、Linux内核漏洞的常见类型与危害

gitblog_00387的博客 722
上一篇: CFG 地址检测位图构建
下一篇: CVE-2022-0995分析(内核越界 watch_queue_set_filter)
inquisiter
博客等级 码龄10年 374粉丝 228原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值