背景
3月29日,开发人员Andres Freund在oss-security上发布上游 xz/liblzma 中的后门导致 ssh 服务器受到攻击的发现,这是一套为开发人员提供无损压缩的软件。该软件包通常用于压缩发行版 tarball、软件包、内核映像和 initramfs 映像。该项目遭到供应链攻击,项目维护者jiaT75(jia Tan)通过上传二进制测试文件和篡改编译脚本,使得编译过程中恶意二进制文件会替换原有文件,导致编译输出与公开的源码不匹配。
jia Tan的攻击者,在整个项目中的攻击时序如下图所示:
影响版本
检测脚本
-
目标系统:只针对 x86-64 架构的 Linux 系统 -
构建环境:需要使用 gcc 和 GNU 链接器进行构建
#!/bin/bash
if ! (echo "$build" | grep -Eq "^x86_64" > /dev/null 2>&1) && (echo "$build" | grep -Eq "linux-gnu$" > /dev/null 2>&1);then
echo "System not x86-64 linux. Exiting."
exit 1
fi
# Building with gcc and the gnu linker
if test "x$GCC" != 'xyes' > /dev/null 2>&1;then
echo "GCC not found. Exiting."
exit 1
fi
if test "x$CC" != 'xgcc' > /dev/null 2>&1;then
echo "CC not set to gcc. Exiting."
exit 1
fi
LDv=$LD" -v"
if ! $LDv 2>&1 | grep -qs 'GNU ld' > /dev/null 2>&1;then
echo "GNU ld not found. Exiting."
exit 1
fi
# Running as part of a debian or RPM package build:
if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then
echo "Running as part of a debian or RPM package build."
else
echo "Not running as part of a debian or RPM package build. Exiting."
exit 1
fi
# Injected code likely to work only on glibc based systems
echo "Injected code likely to work only on glibc based systems."
# Mention about xz versions
echo "Mention about xz 5.6.0 and 5.6.1 not widely integrated yet."
# If everything passes, the system is likely affected
echo "System appears to be affected by the vulnerability."
# Add further actions here if system is affected
exit 0
组件分析
Stage 0 编译阶段
以下是 m4/build-to-host.m4中的编译代码:
其作用是从文件中读取来自tests/files/bad-3-corrupt_lzma2.xz的字节,并将其输出到下一步的标准输出/输入,读取所有内容后,还会添加换行符 (n)。然后运行 tr (translate,如“将字符映射到其他字符”或“将字符替换为目标字符”),这基本上将选定的字符(或字节值)更改为其他字符(其他字节值)。 tr "t -" " t-",它对从tests/files/bad-3-corrupt_lzma2.xz 文件流式传输的字节进行以下替换:
-
0x09 (t) 替换为 0x20 -
0x20(空格)被替换为 0x09 -
0x2d (-) 替换为 0x5f -
0x5f (_) 替换为 0x2d
实际上是“修复”了bad-3-corrupt_lzma2.xz,使其再次形成一个正确的xz流。
Stage 1 使用bash提取恶意代码
通过bash命令解压缩和执行代码的脚本:
脚本执行步骤如下:
-
检查系统是否是 Linux,如果不是则退出脚本。
-
尝试从 config.status 文件中获取 srcdir 变量的值,并在此基础上设置 srcdir 变量。
-
定义了一个非常长的命令 i,这个命令包括了很多次的 head 和 tail 操作,可能是在对数据进行裁剪和处理。
-
通过管道将一个压缩文件 good-large_compressed.lzma(位于 $srcdir/tests/files/ 目录下)解压缩,并将输出通过 tr 命令进行转换,然后交给 /bin/sh 命令执行。
Stage 2 篡改编译代码
该bash文件提取自公开邮件infected.txt,部分脚本内容截图:(https://www.openwall.com/lists/oss-security/2024/03/29/4/1)
该部分脚本在所调查的 TAR 存档(5.6.0 和 5.6.1)中都不存在任何带有任何签名的文件。整个功能基本上看起来像一个“扩展/修补”系统,允许添加未来的脚本以在第 2 阶段的上下文中运行,而无需修改原始的负载测试文件。
Fragment 1: "~!:_ W" and "|_!{ -"
Fragment 3: "jV!.^%" and "%.R.1Z"
2. 如果找到这样的文件,则提取每个文件的偏移量(cut -d: -f2,假设 : 是字段分隔符,则取第二个字段),第一个偏移量 + 7 保存为 $start,第二个偏移量保存为 $start第二个文件中的内容保存为 $end。
3. 一旦脚本有了 $start 和 $end 偏移量,它就会切出文件中具有第一个签名的部分。
4. 接下来首先是替换密码
tr "5-51204-37752-115132-203 -4116-131" " -377"
5. 解压数据:
eval ... | xz -F raw --lzma2 -dc
解压脚本参考附件1。
后门分析
后门结构体保存:
后门参数初始化:
后门加密函数位置:
函数加入了反调试检查,如_Llzma_index_iter_rewind_cold在安全模式中运行函数,检查返回地址:
_Llzma_delta_decoder_init_part_0中建立虚表,指向后门恶意功能:
一些ELF以及环境检查,比如位于Llzma_simple_props_size_part_0检查GUN信息:
后门包含很多可疑hook函数,同时包含一个伪造的分配器对象,该对象查找符号而不是分配,并且在释放时不执行任何操作。位于Linit_pric_table_part_1中:
反编译代码示例:
__int64 backdoor_init(rootkit_ctx *ctx, DWORD *prev_got_ptr)
{
_DWORD *v2;
__int64 runtime_offset;
bool is_cpuid_got_zero;
void *cpuid_got_ptr;
__int64 got_value;
_QWORD *cpuid_got_ptr_1;
ctx->self = ctx;
// store data before overwrite
backdoor_ctx_save(ctx);
ctx->prev_got_ptr = ctx->got_ptr;
runtime_offset = ctx->head - ctx->self;
ctx->runtime_offset = runtime_offset;
is_cpuid_got_zero = (char *)*(&Llzma_block_buffer_decode_0 + 1) + runtime_offset == 0LL;
cpuid_got_ptr = (char *)*(&Llzma_block_buffer_decode_0 + 1) + runtime_offset;
ctx->got_ptr = cpuid_got_ptr;
if ( !is_cpuid_got_zero )
{
cpuid_got_ptr_1 = cpuid_got_ptr;
got_value = *(QWORD *)cpuid_got_ptr;
// replace with Llzma_delta_props_encoder (backdoor_init_stage2)
*(QWORD *)cpuid_got_ptr = (char *)*(&Llzma_block_buffer_decode_0 + 2) + runtime_offset;
// this calls Llzma_delta_props_encoder due to the GOT overwrite
runtime_offset = cpuid((unsigned int)ctx, prev_got_ptr, cpuid_got_ptr, &Llzma_block_buffer_decode_0, v2);
// restore original
*cpuid_got_ptr_1 = got_value;
}
return runtime_offset;
}
防护建议
使用检测脚本检查服务器是否满足漏洞环境标准,终端更新山石最新情报库。
关于山石情报中心
山石网科情报中心,涵盖威胁情报狩猎运维和入侵检测与防御团队。 山石网科情报中心专注于保护数字世界的安全。以情报狩猎、攻击溯源和威胁分析为核心,团队致力于预防潜在攻击、应对安全事件。山石网科情报中心汇集网络安全、计算机科学、数据分析等专家,多学科融合确保全面的威胁分析。我们积极创新,采用新工具和技术提升分析效率。团队协同合作,分享信息与见解,追求卓越,为客户保驾护航。无论是防范未来威胁还是应对当下攻击,我们努力确保数字世界安全稳定。其中山石网科网络入侵检测防御系统,是山石网科公司结合多年应用安全的攻防理论和应急响应实践经验积累的基础上自主研发完成,满足各类法律法规如 PCI、等级保护、企业内部控制规范等要求。
附件
附件一:自动解压缩代码
import struct
from dataclasses import dataclass
from typing import List
@dataclass
class next_state:
delta_actions: int
delta_mask: int
@dataclass
class final_state:
result_code: int
def parse_action(statedesc: bytes):
(dm_flags, da) = struct.unpack("<HH", statedesc)
dm = dm_flags & ~7
flags = dm_flags & 7
if flags & 4:
return final_state(da)
if flags & 2:
pass
else:
da = -da
if flags & 1:
pass
else:
dm = -dm
return next_state(delta_actions = da-4, delta_mask = dm-16)
def parse_actiontable(serialized: bytes):
while True:
state = serialized[:4]
if not state:
return
serialized = serialized[4:]
yield parse_action(state)
def parse_mask(serialized: bytes) -> str:
binmask = int.from_bytes(serialized, "little")
result = ""
for i in range(128):
if binmask & (1 << i):
result += chr(i)
return result
class automaton:
def __init__(self, maskdata : bytes, actiondata : bytes):
self.maskdata = maskdata
self.actiondata = actiondata
def all_strings(self):
init_mask_cursor = len(self.maskdata)-16
init_mask = parse_mask(self.maskdata[-16:])
init_action_cursor = len(self.actiondata)-4*len(init_mask)
yield from self._all_recursive("", init_mask_cursor, init_action_cursor)
def _all_recursive(self, prefix, mask_cursor, action_cursor):
mask = parse_mask(self.maskdata[mask_cursor:mask_cursor+16])
actions = parse_actiontable(self.actiondata[action_cursor:action_cursor + 4*len(mask)])
for (c, action) in zip(mask, actions):
string = prefix + c
if isinstance(action, final_state):
yield (action.result_code, string)
else:
yield from self._all_recursive(string, mask_cursor + action.delta_mask, action_cursor + action.delta_actions)
def load_malware_sample():
from elftools.elf.elffile import ELFFile
with open("liblzma_la-crc64-fast.o.this-is-malware", "rb") as malware_object:
malware_elf = ELFFile(malware_object)
mask_data = malware_elf.get_section_by_name(".rodata.crc64_clmul1").data()
action_data = malware_elf.get_section_by_name(".rodata.lzip_decode0").data()
return automaton(mask_data, action_data)
def main():
a = load_malware_sample()
for (id, string) in a.all_strings():
print(f"{id:4x}: {repr(string)}")
if __name__ == "__main__":
main()
原文始发于微信公众号(山石网科安全技术研究院):xz liblzma 供应链CVE-2024-3094分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论