一、这到底是个什么东西?

简单来说,这是一个专门为64位Linux系统设计的"寄生型"可执行文件感染程序。它的核心玩法很巧妙——把自己当成"寄生虫",悄悄塞进其他正常的程序文件里。被感染的程序看起来和原来一模一样,能正常使用,但每次运行时都会先执行病毒代码,然后再回到原来的功能。

这个名字取自经典电影《黑水晶》中的反派种族,暗示它像那些生物一样,具有寄生和复制的本能。它的"威力"不在于破坏数据,而在于隐蔽性传播性——被感染的程序越多,它扩散得越广,而且很难被发现。

它能做什么?

  1. 自动感染程序:在系统中寻找符合条件的ELF可执行文件,把自己复制进去
  2. 劫持输出函数:随机把程序输出的英文字母替换成"火星文"(比如把S替换成5,把E替换成3)
  3. 反调试保护:检测到调试器就立即退出,防止被安全人员分析
  4. 权限感知:普通用户只感染当前目录,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)

关键修改点:
  1. 降低text段的虚拟地址phdr[i].p_vaddr -= paddingSize,给病毒腾出前面的空间
  2. 扩大text段的大小phdr[i].p_filesz += paddingSize,告诉系统"我的代码段变大了"
  3. 移动后续段的文件偏移:所有在text段之后的段,它们的p_offset都要加上paddingSize
  4. 修改入口点ehdr->e_entry = 新的text段起始地址 + ELF头大小,让程序从病毒代码开始执行
  5. 调整节区表.text节的sh_addrsh_offset也要相应调整,这样strip命令不会破坏病毒

这样做的好处是:

  • 入口点仍在.text节区内:不触发"入口点不在代码段"的启发式检测
  • 不改变段权限:不需要把数据段改成可执行,不引起怀疑
  • 兼容PaX安全机制:通过设置p_align = 0x1000确保内存页对齐

病毒代码的"自举"过程

病毒怎么知道自己有多大?怎么找到自己在内存中的位置?这靠两个巧妙的技巧:

// 技巧1:通过call/pop获取当前指令指针(RIP)
unsigned long get_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标准库函数:

// 内存操作
void Memset(void *mem, unsigned char byte, unsigned int len);
void _memcpy(void *dst, void *src, unsigned int len);

// 字符串操作
size_t _strlen(char *s);
int _strcmp(const char *s1, const char *s2);
int _strncmp(const char *s1, const char *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(const char *path, unsigned long 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的系统调用约定:

  • 系统调用号放在RAX
  • 参数依次放在RDIRSIRDXR10R8R9
  • 返回值在RAX
  • syscall指令触发

3. 感染文件的具体写入过程

int inject_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, &parasite[...], RODATA_PADDING + ...);
    
    // 6. 跳转到padding后的位置
    _lseek(ofd, offset, SEEK_SET);
    
    // 7. 写入原文件的剩余内容(程序头、代码、数据、节区等)
    _write(ofd, mem, final_length);
    
    _close(ofd);
    return 0;
}

写入完成后,用_rename(TMP, fpath)把临时文件替换原文件,感染完成。

4. 随机性与感染控制

病毒不是见文件就感染,而是有一定的"节制":

// 只有1/10的概率感染一个文件(root模式下)
rnum = get_random_number(10);
if (rnum != LUCKY_NUMBER)  // LUCKY_NUMBER = 7
    continue;

// 随机数生成器(基于ASLR和微秒级时间)
static inline uint32_t get_random_number(int max)
{
    struct timeval tv;
    _gettimeofday(&tv, NULL);
    return _rand(&tv.tv_usec) % max;
}

这种"节制"有两个目的:

  1. 降低被发现概率:不会一次性感染太多文件引起注意
  2. 模拟自然传播:更像生物病毒的传播模式

五、防御与检测思路

1. 静态检测

  • 检查ELF头部的EI_PAD区域:病毒在这里写入了魔数0x15D25作为感染标记
  • 检查入口点位置:虽然病毒尽量让入口点在.text节内,但仔细分析仍可能发现异常
  • 检查text段的大小:被感染的文件text段会异常增大

2. 动态检测

  • 监控系统调用序列:病毒会大量调用opengetdents64mmaprename
  • 检测ptrace异常:如果程序自己调用ptrace(PTRACE_TRACEME),值得怀疑
  • 观察文件修改行为:短时间内大量可执行文件被修改是明显信号

3. 防护建议

  • 使用文件完整性监控工具(如AIDE、Tripwire)
  • 限制普通用户对系统目录的写权限
  • 使用只读挂载(mount -o remount,ro /usr)保护关键系统目录
  • 启用SELinux或AppArmor等强制访问控制机制

总结

这个程序展示了一个事实:理解系统底层机制的人,可以做出非常精巧的东西。它的每一行代码都体现了对ELF格式、x86_64架构、Linux内核的深刻理解。从安全研究的角度,它是一个绝佳的学习样本——它告诉我们,防御者必须比攻击者更懂系统,才能保护好它。

Welcome to follow WeChat official account【程序猿编码

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐