一、这到底是个什么东西?
简单来说,这是一个专门为64位Linux系统设计的"寄生型"可执行文件感染程序。它的核心玩法很巧妙——把自己当成"寄生虫",悄悄塞进其他正常的程序文件里。被感染的程序看起来和原来一模一样,能正常使用,但每次运行时都会先执行病毒代码,然后再回到原来的功能。
这个名字取自经典电影《黑水晶》中的反派种族,暗示它像那些生物一样,具有寄生和复制的本能。它的"威力"不在于破坏数据,而在于隐蔽性和传播性——被感染的程序越多,它扩散得越广,而且很难被发现。
它能做什么?
- 自动感染程序:在系统中寻找符合条件的ELF可执行文件,把自己复制进去
- 劫持输出函数:随机把程序输出的英文字母替换成"火星文"(比如把S替换成5,把E替换成3)
- 反调试保护:检测到调试器就立即退出,防止被安全人员分析
- 权限感知:普通用户只感染当前目录,root权限则感染系统核心目录
二、涉及的核心知识领域
这个程序的技术含量相当高,融合了多个计算机安全领域的知识:
1. ELF文件格式(Executable and Linkable Format)
这是Linux系统可执行文件的标准格式。理解它就像理解一个ZIP包的内部结构——你需要知道哪里是"文件头"、哪里是"程序段表"、哪里是"节区表"。病毒正是通过精确修改这些结构来实现"寄生"的。
2. 进程内存布局与虚拟地址空间
程序运行时,操作系统会把文件内容映射到内存中。病毒利用了"代码段"和"数据段"之间的对齐空隙,以及虚拟地址和文件偏移的映射关系,找到可以插入代码的位置。
3. 位置无关代码(Position Independent Code, PIC)
这是最关键的技术之一。因为病毒不知道自己会被插入到哪个地址运行,所以它的所有地址引用都必须"相对化"——就像你在火车上扔球,球的运动是相对于火车的,而不是相对于地面的。
4. 系统调用直接调用(Syscall Direct Invocation)
正常程序通过C标准库(libc)调用系统功能,但病毒为了减小体积、避免依赖,直接用汇编指令触发Linux系统调用。这相当于绕过"前台接待",直接敲开"总经理办公室"的门。
5. 动态链接与PLT/GOT机制
现代Linux程序大多使用动态链接,运行时才会把共享库中的函数地址填入"全局偏移表"(GOT)。病毒通过修改GOT中的函数指针,实现"偷梁换柱"——程序以为在调用puts(),实际上调的是病毒自己的函数。
6. 反调试技术
通过ptrace系统调用的特性来检测是否被调试。原理很简单:一个进程只能被一个调试器附加,如果病毒自己先调用ptrace(PTRACE_TRACEME)失败,就说明已经有调试器在盯着它了。
三、设计思路与感染原理详解
整体架构流程图
┌─────────────────────────────────────────────────────────────────┐
│ 病毒执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ _start() │───▶│ 保存寄存器 │───▶│ do_main() │ │
│ │ (入口跳板) │ │ (保护现场) │ │ (主逻辑) │ │
│ └──────────────┘ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 反调试检查 │ │
│ │ if (ptrace(PTRACE_TRACEME) < 0) → 检测到调试器,退出 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 选择目标目录 │ │
│ │ root用户: 随机选择 /bin, /usr/bin, /sbin, /usr/sbin │ │
│ │ 普通用户: 仅扫描当前目录 (.) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 遍历目录文件 │ │
│ │ 使用 getdents64 系统调用读取目录内容 │ │
│ │ 跳过隐藏文件和病毒自身 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 感染目标文件 │ │
│ │ 1. 检查ELF格式、架构、是否已感染 │ │
│ │ 2. 修改ELF头部和程序头,扩展text段 │ │
│ │ 3. 将病毒代码写入文件 │ │
│ │ 4. 可选:劫持PLT/GOT中的puts函数 │ │
│ │ 5. 用临时文件替换原文件 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 恢复执行环境 │ │
│ │ 恢复寄存器 → 跳转到原程序入口点 → 正常执行 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
感染技术的核心:反向文本段填充(Reverse Text Padding)
这是整个病毒最精妙的地方。传统的感染方式是在代码段末尾追加病毒,但这样会导致入口点不在.text节区内,容易被杀毒软件发现。这个病毒反其道而行之——把代码段往"前面"扩展。
感染前的内存布局:
内存地址增长方向 ──────────────────────────────────────────────▶
0x400000 │┌──────────────┐│
││ ELF头部 ││ 64字节
│├──────────────┤│
││ 程序头表 ││ 描述各段如何加载
│├──────────────┤│
││ ││
││ .text段 ││ 代码段 (可读可执行)
││ (代码) ││
││ ││
│└──────────────┘│
│ ...对齐空隙... │ 页面对齐留下的空白
│┌──────────────┐│
││ .data段 ││ 数据段 (可读可写)
││ (数据) ││
│└──────────────┘│
感染后的内存布局:
内存地址增长方向 ──────────────────────────────────────────────▶
0x3ff000 │┌──────────────┐│◄── 新的text段起始地址 (降低了!)
││ ││
││ 病毒代码 ││ ← 寄生虫住在这里
││ (寄生体) ││
││ ││
0x400000 │├──────────────┤│◄── 原来的起始地址
││ ELF头部 ││ 64字节
│├──────────────┤│
││ 程序头表 ││ 整体向前移动了paddingSize
│├──────────────┤│
││ .text段 ││ 原来的代码段
││ (原代码) ││
││ ││
│└──────────────┘│
│ ...对齐空隙... │
│┌──────────────┐│
││ .data段 ││ 数据段也整体前移了
││ (原数据) ││
│└──────────────┘│
If you need the complete source code, please add the WeChat number (c17865354792)
关键修改点:
- 降低text段的虚拟地址:
phdr[i].p_vaddr -= paddingSize,给病毒腾出前面的空间 - 扩大text段的大小:
phdr[i].p_filesz += paddingSize,告诉系统"我的代码段变大了" - 移动后续段的文件偏移:所有在text段之后的段,它们的
p_offset都要加上paddingSize - 修改入口点:
ehdr->e_entry = 新的text段起始地址 + ELF头大小,让程序从病毒代码开始执行 - 调整节区表:
.text节的sh_addr和sh_offset也要相应调整,这样strip命令不会破坏病毒
这样做的好处是:
- 入口点仍在
.text节区内:不触发"入口点不在代码段"的启发式检测 - 不改变段权限:不需要把数据段改成可执行,不引起怀疑
- 兼容PaX安全机制:通过设置
p_align = 0x1000确保内存页对齐
病毒代码的"自举"过程
病毒怎么知道自己有多大?怎么找到自己在内存中的位置?这靠两个巧妙的技巧:
// 技巧1:通过call/pop获取当前指令指针(RIP)
unsignedlongget_rip(void)
{
long ret;
__asm__ __volatile__ (
"call get_rip_label \n"// call会把下一条指令地址压栈
".globl get_rip_label \n"
"get_rip_label: \n"
"pop %%rax \n"// 弹出栈顶,就是当前RIP
"mov %%rax, %0" : "=r"(ret)
);
return ret;
}
// 技巧2:PIC_RESOLVE_ADDR宏,将编译时地址转换为运行时地址
#define PIC_RESOLVE_ADDR(target) \
(get_rip() - ((char *)&get_rip_label - (char *)target))
打个比方:你在一列行驶的火车上,想知道窗外某棵树相对于你的位置。你知道火车头(编译时的基准点)到你当前位置的距离,也知道火车头到那棵树的距离,一减就得到了树相对于你的距离。这就是位置无关代码的核心思想。
PLT/GOT劫持:让程序"说胡话"
病毒有一个恶趣味的功能——随机把程序输出的英文替换成"火星文"。这需要劫持puts()函数。
正常情况下的函数调用流程:
程序调用 puts("Hello World")
│
▼
┌───────────────┐
│ PLT[puts] │ 过程链接表(跳板)
│ jmp *GOT[n] │ 第一次调用时,GOT[n]指向PLT的下一行
└───────┬───────┘
│
▼
┌───────────────┐
│ GOT[n] │ 全局偏移表(存储真实地址)
│ 0x... │ 初始时指向PLT的解析代码
└───────┬───────┘
│
▼
┌───────────────┐
│ libc.so中的 │
│ 真实puts() │
└───────────────┘
被劫持后的流程:
程序调用 puts("Hello World")
│
▼
┌───────────────┐
│ PLT[puts] │
│ jmp *GOT[n] │
└───────┬───────┘
│
▼
┌───────────────┐
│ GOT[n] │ ← 被病毒修改为指向 evil_puts()
│ 0x病毒地址 │
└───────┬───────┘
│
▼
┌───────────────┐
│ evil_puts() │ 病毒的替换函数
│ (火星文转换) │
└───────┬───────┘
│
▼
┌───────────────┐
│ 调用原始puts │ 最终还是会输出,只是内容被改了
│ (输出火星文) │
└───────────────┘
病毒通过解析目标文件的动态段(Dynamic Segment),找到DT_SYMTAB(符号表)、DT_STRTAB(字符串表)、DT_JMPREL(重定位表)和DT_PLTGOT(GOT表地址),然后遍历重定位项,找到puts对应的GOT条目,把里面的地址换成病毒中evil_puts()的地址。
四、代码实现的关键细节解读
1. 自定义的"微型C库"
病毒没有链接libc,而是自己实现了一套极简的C标准库函数:
// 内存操作
voidMemset(void *mem, unsignedchar byte, unsignedint len);
void _memcpy(void *dst, void *src, unsignedint len);
// 字符串操作
size_t _strlen(char *s);
int _strcmp(constchar *s1, constchar *s2);
int _strncmp(constchar *s1, constchar *s2, size_t n);
// 格式化输出(极简版printf)
int _printf(char *fmt, ...);
// 数字转换
char *itoa(long x, char *t); // 整数转字符串
char *itox(long x, char *t); // 整数转十六进制
这些实现都很朴素——比如_memcpy就是一个简单的字节循环拷贝,_strlen就是一个字符一个字符地数。但正因为简单,所以不需要依赖外部库。
2. 系统调用的"裸奔"方式
以_open()为例,看看病毒是怎么直接调用Linux内核的:
long _open(constchar *path, unsignedlong flags, long mode)
{
long ret;
__asm__ volatile(
"mov %0, %%rdi \n"// 参数1:文件路径 → RDI寄存器
"mov %1, %%rsi \n"// 参数2:打开标志 → RSI寄存器
"mov %2, %%rdx \n"// 参数3:权限模式 → RDX寄存器
"mov $2, %%rax \n"// 系统调用号:2 = open
"syscall"// 触发系统调用
: : "g"(path), "g"(flags), "g"(mode)
);
asm ("mov %%rax, %0" : "=r"(ret)); // 返回值在RAX中
return ret;
}
这就是x86_64 Linux的系统调用约定:
- 参数依次放在
RDI、RSI、RDX、R10、R8、R9
3. 感染文件的具体写入过程
intinject_parasite(size_t psize, size_t paddingSize,
elfbin_t *target, elfbin_t *self,
ElfW(Addr) orig_entry_point)
{
// 1. 创建临时文件
ofd = _open(TMP, O_CREAT|O_WRONLY|O_TRUNC, st.st_mode);
// 2. 写入原始ELF头部(64字节)
_write(ofd, mem, ehdr_size);
// 3. 写入病毒代码(去掉末尾的跳转指令部分)
_write(ofd, parasite, initial_parasite_len);
// 4. 写入"跳转到原入口点"的跳板指令
// 0x68是push指令,0xc3是ret指令
// 合起来就是:push 原入口地址; ret
// 效果等同于:jmp 原入口地址
uint8_t jmp_patch[6] = {0x68, 0x0, 0x0, 0x0, 0x0, 0xc3};
*(uint32_t *)&jmp_patch[1] = orig_entry_point;
_write(ofd, jmp_patch, sizeof(jmp_patch));
// 5. 写入病毒代码的剩余部分(rodata等)
_write(ofd, ¶site[...], RODATA_PADDING + ...);
// 6. 跳转到padding后的位置
_lseek(ofd, offset, SEEK_SET);
// 7. 写入原文件的剩余内容(程序头、代码、数据、节区等)
_write(ofd, mem, final_length);
_close(ofd);
return0;
}
写入完成后,用_rename(TMP, fpath)把临时文件替换原文件,感染完成。
4. 随机性与感染控制
病毒不是见文件就感染,而是有一定的"节制":
// 只有1/10的概率感染一个文件(root模式下)
rnum = get_random_number(10);
if (rnum != LUCKY_NUMBER) // LUCKY_NUMBER = 7
continue;
// 随机数生成器(基于ASLR和微秒级时间)
staticinlineuint32_tget_random_number(int max)
{
structtimevaltv;
_gettimeofday(&tv, NULL);
return _rand(&tv.tv_usec) % max;
}
这种"节制"有两个目的:
五、防御与检测思路
1. 静态检测
- 检查ELF头部的EI_PAD区域:病毒在这里写入了魔数
0x15D25作为感染标记 - 检查入口点位置:虽然病毒尽量让入口点在
.text节内,但仔细分析仍可能发现异常 - 检查text段的大小:被感染的文件text段会异常增大
2. 动态检测
- 监控系统调用序列:病毒会大量调用
open、getdents64、mmap、rename等 - 检测ptrace异常:如果程序自己调用
ptrace(PTRACE_TRACEME),值得怀疑 - 观察文件修改行为:短时间内大量可执行文件被修改是明显信号
3. 防护建议
- 使用文件完整性监控工具(如AIDE、Tripwire)
- 使用只读挂载(
mount -o remount,ro /usr)保护关键系统目录 - 启用SELinux或AppArmor等强制访问控制机制
总结
这个程序展示了一个事实:理解系统底层机制的人,可以做出非常精巧的东西。它的每一行代码都体现了对ELF格式、x86_64架构、Linux内核的深刻理解。从安全研究的角度,它是一个绝佳的学习样本——它告诉我们,防御者必须比攻击者更懂系统,才能保护好它。
Welcome to follow WeChat official account【程序猿编码】