VMP源码学习(1)变异分析与代码bug

admin 2025-12-29 00:55:21 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章深入分析了VMP源码中的变异混淆机制,详细阐述了垃圾代码初始化、空闲寄存器与标志位获取、指令等效替换及垃圾代码填充等核心流程。作者指出了源码中存在的两个具体bug,包括垃圾指令列表重复及获取空闲标志位时的逻辑错误,这可能导致垃圾指令生成受限。 综合评分: 81 文章分类: 代码审计,逆向分析,二进制安全,漏洞分析


cover_image

VMP源码学习(1) 变异分析与代码bug

原创

CrazyHarb

冲鸭安全

2025年7月31日 10:01 北京

VMP源码学习——变异分析

背景:

VMP在23年泄露了一份代码,尽管目前在github上代码已经大部分被删除,但也让我们有机会得以窥探一眼这款商业混淆软件的内部原理。目前,网上关于这份源码的分析“少之又少”(这句话怎么这么耳熟),所以笔者打算分享一下对该源码的分析,旨在抛砖引玉。

功能分析:

针对代码的混淆与加密,目前提供了三种功能即(变异、虚拟、变异+虚拟),代码中支持了变异、虚拟两种混淆功能,本文将从变异功能出发,抛砖引玉,深入VMP而出不来。

入口点:

| | | — | | C                   void IntelFunction::Mutate(const CompileContext &ctx, bool for_virtualization) |

垃圾代码初始化:

首先,会初始化一个垃圾指令的列表,这里的垃圾指令代表的是不会对后续的运行产生影响的指令,垃圾指令的存在只是为了扩大文件,加大逆向的时间成本,初始化逻辑中IntelCommand代表一条指令,例如

| | | — | | Assembly language                   mov rax, 1 |

IntelOperand代表操作数,例如 rax rbx等,在这段初始化中,垃圾指令的列表共初始化了78条垃圾指令,列表如下:

| | | — | | Assembly language                   mov rxx,rxx                   mov rxx, value                   movsx xx, xl/xh                   movsx exx,xx                   movsx rxx, xx                   movsxd rxx, exx                   movzxxx, xl/xh                   movzxexx, xx                   movzxrxx, xx                   notrxx                   negrxx                   incrxx                   decrxx                   cmprxx, rxx                   cmprxx, value                   test rxx, rxx                   test rxx, value                   andrxx, rxx                   andrxx, value                   or   rxx, rxx                   or   rxx, value                   xorrxx, rxx                   xorrxx, value                   addrxx, rxx                   addrxx, value                   adcrxx, rxx                   adcrxx, value                   subrxx, rxx                   subrxx, value                   shlrxx, cl                   shlrxx, byte of value                   shrrxx, cl                   shrrxx, byte of value                   salrxx, cl                   salrxx, byte of value                   sarrxx, cl                   sarrxx, byte of value                   rolrxx, cl                   rolrxx, byte of value                   rorrxx, cl                   rorrxx, byte of value                   shrdrxx, cl                   shrdrxx, byte of value                   shldrxx, cl                   shldrxx, byte of value                   btrxx, rxx                   btrxx, byte of value                   btcrxx, rxx                   btcrxx, byte of value                   btrrxx, rxx                   btrrxx, byte of value                   btsrxx, rxx                   btsrxx, rxx                   setz/seto/setc/…                   cmovc/cmova/cmovo/…                   clc                   stc                   cmc                   cbw                   cwde                   cwd                   cdq                   cdqe                   cqo                   lahf                   bswap rxx                   xchgrxx, rxx                   xaddrxx, rxx                   jmp   value                   ; 后面的指令是当for_virtualization == false(非虚拟化选项)的时候才会存在                   sbb rxx, rxx                   sbb rxx, value                   rcl rxx, cl                   rcl rxx, byte                   rcr rxx, cl                   rcr rxx, byte                   bsr rxx, rxx                   bsf rxx, rxx                   rdtsc |

上面的垃圾指令列表中包含了所有的垃圾指令,但没有体现出具体的随机长度,若有具体分析的必要,可以参考一下源码

指令的处理:

初始化完成垃圾代码列表后,变异逻辑将对汇编指令进行了逐条处理,我们先忽略掉“指令块”这些概念,只看指令的变异处理。实际上,所谓的变异功能,包含了以下几步逻辑处理,即:指令信息获取→获取空闲寄存器→指令替换→垃圾代码筛选→垃圾代码填充。我们分步来看一下:

1. 指令信息获取

此步骤将会调用IntelCommand::GetCommandInfo指令获取当前指令的信息,当然,这个函数中没有任何的奇淫巧计,全部都是暴力硬编码,例如 rdtsc及lahf中,代码中的实现为:

| | | — | | C                   case cmRdtsc:                        command_info_list.Add(atWrite, regEAX, otRegistr, osDWord);                        command_info_list.Add(atWrite, regEDX, otRegistr, osDWord);                        break;                   case cmLahf:                        command_info_list.Add(atRead, regEFX, otRegistr, size_);                        command_info_list.set_need_flags(fl_S | fl_Z | fl_A | fl_P | fl_C);                        command_info_list.Add(atWrite, regEAX, otHiPartRegistr, osByte);                        break; |

可以看到:

①针对rdtsc指令,指令信息中指明了eax和edx将会被写入。

②针对lahf指令,标志位将会被读取,同时指明了sf、zf、af、pf、cf将会被读取,最后ah将会被写入。

以此类推,这个函数中包含了大部分的指令,其余的指令则返回获取失败。

2. 获取空闲寄存器

此步骤将会调用IntelFunction::GetFreeRegisters进行获取空闲寄存器,尽管代码量很少,但这块逻辑可能比较抽象,所以笔者结合流程图与简单的举个例子进行简单的分析,假设当前指令为如下:

| | | — | | C                   sub rbx, rax                   mov rax, rbx                   cpuid                   mov rdx, rcx                   rdtsc                   nop                   jnz xxx |

假设目前要对第一行指令做处理,我们需要获取空闲寄存器,代码将从第二行指令开始扫描,扫描动作为:获取指令信息→判断是写入还是读取。这个扫描动作直到遇到无法获取的指令信息或有修改RIP的指令为止(例如样例中的第7行)

先说结论:

1先写入的话,就代表是空闲寄存器,因为开始指令到当前扫描指令之间,这个寄存器无论如何更改,都会被后面的指令改写覆盖

1先读取的话,就代表是使用中的寄存器

我们再来看一下扫描流程:

1当前扫描第2行,由于rbx被修改了,rax被读取了,所以当前扫描结果为,空闲寄存器: rbx, 已使用寄存器: rax

1当前扫描第3行,rax尽管被修改了,但rax在已使用的寄存器中,所以不作为空闲寄存器,所以当前扫描结果为: 空闲寄存器 rbx、rcx、rdx,  已使用寄存器 rbx、rax

1当前扫描第4行,当前扫描结果为 空闲寄存器 rbx、rcx、rdx,  已使用寄存器 rbx、rax、rcx

1当前扫描第5行,当前扫描结果为 空闲寄存器 rbx、rcx、rdx,  已使用寄存器 rbx、rax、rcx

1当前扫描第6行,由于没扫到任何的寄存器变动,所以当前扫描结果不变 空闲寄存器 rbx、rcx、rdx,  已使用寄存器 rbx、rax、rcx

1当前扫描第7行,由于本行指令将改变RIP,所以直接退出扫描

针对以上,我们可以看到运行返回结果的空闲寄存器为:rbx rcx rdx,当然,空闲标志位也是通过上面的逻辑进行获取的,流程图如下,这里就不再展开了

3. 指令替换

该步骤,将会对特定的指令规则进行平行替换结果不变,规则如下表展示:

| | | — | | C                   xor rxx, rxx(rxx相同)  ->  sub rxx,rxx                   add rxx, rxx1  ->   lea rxx, [rxx + reg1]                   add rxx, value ->   lea rxx, [rxx + value]                   sub rxx, value ->   lea rxx, [rxx – value]                   jmp [value]   ->    push [value]                                       ret                   jmp rxx       ->    push rxx                                       ret                   jmp [rxx]     ->    push [rxx]                                       ret |

当然替换也并不是每次都会发生,仅会在本条指令处理时,(rand() & 1)不为0的情况下,才会匹配规则,并进行替换。

且针对add与sub,会进行标志位逻辑判断,即:当前指令修改的标志位寄存器在空闲标志位寄存器中时(也就是说,当前更改的标志位不会影响后续代码执行结果),才会进行指令替换。

4. 垃圾代码筛选

前面我们已经获取到了空闲寄存器(包含空闲标志位寄存器),也初始化垃圾指令的列表,本节中将会对已有的垃圾指令列表进行筛选。

①获取当前指令信息,可以参考第1步骤,这里就直接跳过了。

②循环判断操作数:

1) 如果是读取则放过

2) 如果是写入,则判断是否是立即数,如果是也放过

3) 如果是寄存器或高位寄存器,则进一步判断寄存器类型

4) 如果是需要空闲寄存器,则从空闲寄存器列表中找一个,如果空闲寄存器列表为空,则筛选掉当前垃圾指令,如果所有空闲寄存器的长度都小于需要的大小,也筛选掉垃圾指令。

5) 如果是需要空闲标志位寄存器,则查看空闲标志寄存器,如果空闲标志位寄存器列表为空,则筛选掉当前垃圾指令。

6) 如果是高位寄存器,则按照长度2倍对齐,例如 ah/al -> ax

针对4)、5)我们举个例子,例如 sub rax, rbx ,那么我们获取到的四条操作信息:标志位:OSZAPC -> 更改、rbx -> 读取、 rax -> 读取、 rax -> 更改。然后会判断所有的操作信息,由于rbx仅有读取,所以直接跳过。会判断一下空闲标志位寄存器中是否存在OSZAPC标志位,判断一下空闲寄存器是否存在rax,如果都符合要求,证明当前的垃圾指令符合要求,放入筛选后的列表中。

5. 垃圾代码填充

经过上面的筛选及指令替换,我们到了最后一步,开始垃圾代码填充,可以放一首比较好听的bgm,完成最后的一部分,要想放的歌曲好听,好的手机必不可少,我新换的手机,旧的手机我放在…

① 首先,代码中会随机添加的垃圾指令的数量 n ( 0~4),然后开始指令预处理阶段

| | | — | | C                   rand() % 4 |

② 在筛选后的列表中随机一条指令,并将其从筛选后的列表中删掉,也就是针对一条指令的垃圾填充,不会重复使用相同的垃圾指令。

③ 遍历选取的垃圾指令的操作数,如果是空闲寄存器,就随机一个寄存器(空闲寄存器可以重复使用)并获取当前随机出来的寄存器长度,如果当前记录的最大长度大于随机出来的长度,就把当前记录的最大长度改为随机出来的长度

④ 记录所有操作数中最大的寄存器序号,这里如果当前操作数是指明了寄存器,且是rax,代码中会根据位数随机一个寄存器(rax ~ rbp 或 rax ~ r15)

⑤ 如果当前是32位程序,且寄存器不是 rax rdx rcx rbx,并且最小长度小于2字节,则将最小长度置为2字节

⑥ 如果最小长度大于最大长度,则执行失败,进行下一条垃圾指令填充,如果长度非随机,且当前垃圾指令长度小于最小长度或者当前指令长度大于最大长度,也算作执行失败

      ⑦ 上面预处理完成后,开始随机填入操作数的各种信息:

1)如果长度需要随机,则根据最大长度、最小长度随机一个长度。

2)如果操作数是寄存器,则填入上面随机出来的寄存器,且如果长度是1字节,垃圾指令的需求寄存器是空闲寄存器或者rax,最大长度大于1字节,且最大寄存器序号小于4(rax rdx rbx rcx),且随机一个数为奇数,则使用高位寄存器代替   即 mov al, bl  -> mov ah, bl

3)  如果是立即数,则随机一个数即可

4)如果是setx/cmovx 等指令,则随机一个标志位使用,随机一个数,如果是奇数,则取setnx/cmovnx指令。

⑧ 循环下一条垃圾指令填充,垃圾指令填充完成后,循环下一条指令处理

代码中的bug:

1. 垃圾指令的重复

根据垃圾指令列表可以得知,52行和53行的垃圾指令是相同的,根据之前的代码推测,有一个应该是

| | | — | | C                   bts rxx, value |

笔者想了一下应该不会有什么坑,猜测应该是写错了

2. 获取空闲标志位错误

根据流程图可以得知空闲标志位寄存器会在第一次获取成功后停止获取,也就是说,如果指令列表中,第一行指令改变了Z,第二行改变了C,理论上返回的空闲标志位为ZC,但由于获取成功一次后就停止获取了,所以实际返回的空闲指令寄存器是Z,也就是说有可能会影响部分垃圾指令的生成。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:冲鸭安全 CrazyHarb《VMP源码学习(1) 变异分析与代码bug》

评论:0   参与:  0