代码虚拟与自动化分析(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-21 08:39:06

点击下载

作者:章立春

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

代码虚拟与自动化分析

代码虚拟与自动化分析试读:

前言

在完成了《软件保护及分析技术——原理与实践》一书以后,回顾该书的内容,发现基于该书的定位和目标,无法涉及更多软件安全领域的高阶技术,因此有了本书。

在现代软件安全技术中,代码加密技术,尤其是代码虚拟加密技术,已经成为最为主要的,同时也是最为有效的软件保护手段之一。尽管代码虚拟化技术不断成熟,但目前没有足够的相关文档和信息被公开,这使代码虚拟化技术成为一种封闭的“黑箱技术”,只有少部分人才能了解并运用它。

写作目的

虚拟化技术在现代计算机技术中占据着越来越重要的位置。在现代计算机系统中,从硬件到软件,从上层应用到系统底层,都需要虚拟化技术。

什么是虚拟化?虚拟化是一个泛指,在计算机中是指将某种逻辑行为发生的环境进行更换,使这种逻辑行为在不同环境中的发生和运行都能得到相同的逻辑结果的技术。传统的计算机虚拟化技术都是为了使程序在更加安全、稳定的环境中更加快捷、方便地完成特定的逻辑处理或者程序功能而设计的,但在计算机技术飞速发展的今天,很多事物都发生了质的变化,在虚拟化技术中同样如此。于是,在现代的虚拟化技术中出现了一个重要的分支——代码虚拟化。

从广义上说,代码虚拟的目的也是使软件的逻辑行为运行在一个更加安全的环境中,但与其他虚拟化技术不同的是,代码虚拟化的安全目标是针对人而非计算机环境的,也就是说,代码虚拟机的实质并不是从技术上使程序的实际代码运行更加稳定或者更加安全,而是使代码的运行过程更为复杂,从而使他人难以理解代码自身的逻辑行为,防止代码运行流程被篡改或逆向。代码虚拟化技术的这种特征,使其超出了一般的技术范畴,成为一种在人与人之间借助软件安全进行博弈游戏的载体。

彻底理解代码虚拟化技术要从虚拟化和反虚拟化两个方面进行。在现代的代码虚拟化技术中,传统的反虚拟化技术的发展远远落后于虚拟化技术,而且遇到了难以跨越的障碍。这是因为随着计算机运算速度的加快,虚拟化技术可以使用大量的计算机运算来实现代码的虚拟化,大大膨胀和扩张虚拟化代码量,使代码的运行量达到一种难以人为进行分析的地步,从而使人工分析代码的逻辑和行为变得不切实际。

针对这种情况,自动化分析技术应运而生。既然代码虚拟可以利用计算机强大的运算能力,那么自动化分析技术也可以运用计算机的运算能力来帮助我们更加方便地理解这些代码。更富戏剧性的是,在自动化分析方面,我们使用较多也较为有效的技术恰恰是代码虚拟化技术,这就形成了利用虚拟化技术理解分析虚拟化技术的奇妙关系。在本书中,笔者将与读者一起感受这个代码中的虚拟世界。

本书内容

本书主要分成3部分来讨论代码虚拟和自动化分析技术。第1部分主要讨论代码虚拟的各种实现方法,并通过一些代码虚拟化技术的应用案例引领读者了解代码虚拟化的技术基础。第2部分主要介绍和讨论代码自动化分析技术,也就是针对我们了解的各种代码虚拟化技术,对如何对其进行理解和利用进行自动化分析,并通过实际的代码将自动化分析技术从理论变为实际程序,提升我们对自动化分析技术的理解和运用水平。第3部分使用自动化分析技术分析Winlicense与VMProtect保护系统,全面展示自动化分析技术,并通过这种实际的分析过程深入体会代码虚拟化技术。章立春2017年2月第1部分实现原理第1章 代码虚拟化原理第2章 模拟虚拟化第3章 自动化分析原理第4章 花型替换分析第1章 代码虚拟化原理

代码虚拟化技术是一项应用相当普遍的技术。也许我们会觉得,在日常计算机应用中很少见到虚拟化技术的身影,但如果提到Visual Basic、Java、C#,却没有人会否认这些语言在计算机中的重要性。然而,从技术方面来讲,这些高级语言之所以会如此成功,与其运用虚拟化技术实现了健壮的安全性、稳定性和可扩展性等是密不可分的。另外,代码虚拟化技术还支撑着像Virtual Box、VPS这样的虚拟系统环境。这些环境使软件的开发测试及服务器的资源利用更加有效,因此,它们对代码虚拟化技术的重要性是毋庸置疑的。

代码虚拟化技术还有子分类,就是全虚拟化与局部虚拟化。所谓全虚拟化就是给至少一个程序的所有代码指令提供完整的运行环境,使被虚拟化的程序或代码在运行时不需要与虚拟环境以外的环境进行交涉(如直接使用运行平台的 CPU 指令集)。这种虚拟化技术往往虚拟了一个完整的CPU及相关环境,被虚拟执行的代码与模拟该CPU的环境无关。以这种方式实现的虚拟化技术就称为全虚拟化,这类技术使指令运行环境更加安全和稳定。局部虚拟化是指在虚拟化技术的实现和应用过程中,并没有给被虚拟化的程序代码提供完整的运行环境,只给程序中的一部分指令提供了另外一个执行环境,程序的大部分指令仍然运行在非虚拟化的执行环境中。局部虚拟化的主要目的是将程序中关键代码的运行变得更加复杂,以防止被逆向分析或者篡改。从技术发展上来讲,这类虚拟化技术将技术层面的世界引向了更加复杂的一面,而不是使其简单化。

对于全虚拟化技术,由于应用广泛,在公开环境中已经多有讨论,因此在本书中我们将主要关注代码局部虚拟化。

代码虚拟化是一般虚拟化技术的一个分支。根据本书的定位,我们关注的是代码的局部虚拟化技术。由于这种技术在公开环境中讨论得很少,而且没有固定的技术规范和模式,因此更加值得我们去详细了解。在本书中,我们主要讨论对程序按照某一CPU平台指令集编译后的字节码进行虚拟化从而实现运行效果等价的技术,并针对x86指令集进行举例和讨论(虚拟化技术本身不受限于指令集)。根据局部代码虚拟化技术的形式,我们一般用“代码虚拟机”一词来代指,因此在本书中,我们所说的代码虚拟机都是指这种形式的虚拟化技术。

由于代码虚拟机只是将某个程序的部分代码转入虚拟化的环境中执行,按照一般程序指令代码的运行特点,这些代码虚拟化技术从框架上具有高度的相似性,因此,我们就从代码虚拟化的基本框架开始讨论。1.1 代码虚拟机运行时框架

要了解代码虚拟机的基本框架,就要从代码指令的一般运行原理说起。我们先来看一段一般的程序代码,具体如下。00401C10  55        push ebp; frame.func1(title,text)00401C11  8BEC       mov ebp,esp00401C13  6A 40       push 4000401C15  8B45 08     mov eax,dword ptr [ebp+8]00401C18  50        push eax00401C19  8B4D 0C     mov ecx,dword ptr [ebp+0C]00401C1C  51        push ecx00401C1D  6A 00       push 000401C1F  FF15 B8624000  call dword ptr [<&USER32.MessageBoxA>]00401C25  83F8 06     cmp eax,600401C28  75 14       jne short 00401C3E00401C2A  6A 40       push 4000401C2C  8B55 08     mov edx,dword ptr [ebp+8]00401C2F  52        push edx00401C30  8B45 0C     mov eax,dword ptr [ebp+0C]00401C33  50        push eax00401C34  6A 00       push 000401C36  FF15 B8624000  call dword ptr [<&USER32.MessageBoxA>]00401C3C  EB 12       jmp short 00401C5000401C3E  6A 40       push 4000401C40  8B4D 08     mov ecx,dword ptr [ebp+8]00401C43  51        push ecx00401C44  8B55 0C     mov edx,dword ptr [ebp+0C]00401C47  52        push edx00401C48  6A 00       push 000401C4A  FF15 B8624000  call dword ptr [<&USER32.MessageBoxA>]00401C50  5D        pop ebp00401C51  C3        retn

以上是一个简单的函数编译后的汇编指令序列,其对应的源代码如下。#include void func1(char* title,char* text){if (MessageBoxA(0,text,title,MB_OK | MB_ICONINFORMATION) == IDYES)MessageBoxA(0,text,title,MB_OK | MB_ICONINFORMATION);elseMessageBoxA(0,text,title,MB_OK | MB_ICONINFORMATION);}int main(){func1("test","test");return 0;}

这段代码就是一般程序的基本形式。我们看到,指令的执行是有先后顺序的,当函数func1被调用时,00401020处的指令被执行,接着就是执行00401021处的指令。在没有流程控制指令干预的前提下,指令的执行总是按照这种模式进行。代码局部虚拟化的目的就是在保证执行效果相同的情况下,将这段代码中的一部分转换到虚拟化环境中去执行。经过虚拟化处理的代码如下。00401C10  55         push ebp     ;frame_vmp.func1(title,text)00401C11  8BEC       mov ebp,esp00401C13  E9 15820000   jmp 00409E2D  ;通过流程控制语句将代码的执行转移到其他地方00401C18  83ED 02     sub ebp,2     ;下面都是实际上不会执行的垃圾指令00401C1B  9C        pushfd00401C1C  D2E0       shl al,cl00401C1E  9C        pushfd00401C1F  66:8945 04   mov word ptr [ebp+4],ax00401C23  52        push edx00401C24  E8 0A660000   call 0040823300401C29  E8 6A6F0000   call 00408B9800401C2E  9C        pushfd00401C2F  895C24 20    mov dword ptr [esp+20],ebx00401C33  E9 59570000   jmp 0040739100401C38  FF7424 04    push dword ptr [esp+4]00401C3C  E9 B1570000   jmp 004073F200401C41  F8        clc00401C42  E9 F4730000   jmp 0040903B00401C47  60        pushad00401C48  33B7 5D000010  xor esi,dword ptr [edi+1000005D]00401C4E  9D        popfd00401C4F  50        push eax00401C50  5D        pop ebp       ;最终,代码返回这里,继续执行00401C51  C3        retn

以上代码是我们利用一款名为 VMProtect 的软件保护系统对示例代码进行代码虚拟化处理的结果。从结果中我们看到,在进行虚拟化处理以后,代码的执行流程被更改了,从00401C13处开始进入一个原始指令代码之外的地方运行,在运行完毕后回到00401C50处继续执行,也就是说,从00401C13处到00401C50处的代码就是被虚拟化执行的代码。

上面的示例几乎是所有代码虚拟化技术的基本形式。对代码进行虚拟化的目的就是将从00401C13处到00401C50处的代码变形加密,然后放到一个虚拟化的环境中去执行,但是其运行效果与没有加密时的运行效果是一样的,从而实现了对这段代码的隐藏与保护。从程序使用的角度来看,是否进行虚拟化处理没有差别。因此,我们可以给出代码虚拟机的一般工作形式,如图1.1所示。图1.1

代码虚拟机的工作流程大体可以分为3个阶段。第一阶段是从流程控制指令被执行开始的。通过流程控制指令,原始指令序列的控制权交给了代码虚拟机的入口代码,在入口代码中需要对原始指令执行时的代码现场环境进行保存,然后建立虚拟化运行环境,将现场数据都转移到虚拟化环境中,进入具体的虚拟化执行流程。第二阶段是具体的模拟执行阶段。在这个阶段,代码虚拟机需要对被虚拟化的指令进行模拟执行,将模拟执行的结果反映到真实的环境中去。第三阶段是出口阶段。被虚拟化指令模拟执行完毕,需要将仍储存在虚拟化环境中的数据都恢复到实际的环境中,然后转移到原本被虚拟化指令后面的真实指令上去。通过这3个阶段就完成了一个局部代码虚拟化过程。

通过上面介绍的3个阶段所需要的技术,我们可以总结出一般代码虚拟机的基本部件,如图1.2所示。图1.2

这就是一般代码虚拟机的构成部件。通过代码虚拟机的3个运行阶段及运行流程,我们可以很容易地理解流程控制指令和这3个阶段的功能,但同时我们看到了一种名为“虚拟化媒介”的部件。这个部件有什么作用呢?回顾本节开始的例子代码,在对代码进行虚拟化处理以后,从00401C13处到00401C50处的原始代码数据被替换成了其他数据。那么,原始的代码数据去哪儿了呢?局部代码虚拟化技术的主要目的是对原始指令进行加密,并将代码放到虚拟执行环境中执行,但是要求执行结果与原始代码相同,也就是说,虚拟化技术可以改变从00401C13处到00401C50处的代码数据,但是必须保留其功能性质,而代码虚拟机可以改变实现从00401C13处到00401C50处代码代表的功能性质,但是不能使这个过程中有信息丢弃。因此,代码虚拟机必须使用一种媒介来代表这段原始代码的功能性质,这就是虚拟化媒介的作用。在一般的代码虚拟机中,虚拟化媒介就是我们常说的OPCODE。这样,一般的代码虚拟机在运行时就由我们上面所说的5个部分组成。这些部分都是代码虚拟机模拟执行代码运行时所必需的,我们称之为代码虚拟机的 Runtime 或者crt部件。那么,这些部件的具体实现细节又是怎样的呢?下面我们分别进行讲解。1.1.1 流程控制指令

在代码虚拟机的运行时部件中,流程控制指令主要负责中转原始代码指令序列的执行流程,将程序的执行流程中转到代码虚拟机的运行时代码中去。在这里要注意,代码虚拟机的运行时部件在设计过程中,尽管不是必须,但是为了使代码虚拟机具有良好的兼容性,一般都使用与被虚拟化代码相同的平台环境,以达到方便地在实际环境与虚拟化环境之间进行切换、交互运算结果并降低代码虚拟机设计复杂程度的目的。所以,流程控制指令的设计自然也是如此。

流程控制指令在不同的运行环境下是不一样的。例如,我们虚拟化的目标是 x86指令集代码,那么一般需要采用 x86指令来设计流程控制指令。因此,流程控制指令的设计非常简单,只需要选用相应目标平台上的流程控制语句就可以了。例如,在 x86中我们可以选择jmp、ret等指令,在ARM中我们可以选择BL等指令。1.1.2 入口代码

当原始指令的执行流程被流程控制指令转移到虚拟机入口代码处以后,整个程序的控制权就转移给虚拟机的运行时代码了。由于代码虚拟机运行时代码与被虚拟化代码在同一个环境中运行,代码虚拟机中的各种指令运行与被虚拟化目标的运行环境会发生实时交互,在代码虚拟机运行时代码中的操作会直接影响被虚拟化程序,因此,入口代码的设计就要特别小心。既然代码虚拟机的目标是完整地实现被虚拟化代码与未虚拟时执行结果的等价,那么在代码虚拟机的设计中,就需要保证在整个过程中不会发生信息丢失(例如,中转流程时线程的上下文数据)。在代码虚拟机的设计中,这些信息在进入入口代码之后都会被尽快保存下来,以防止代码虚拟机的运行时代码破坏这些信息。同时,这些信息也需要转移到虚拟化环境中,成为开始模拟原始指令的数据依据。根据这一点,入口代码的设计就有了固定的模式。尽管不同运行平台的细节存在差别,但都包含如下4个部分的功能。

1.保存原始代码现场

我们以x86指令集为示例给出保存原始代码现场的设计模板。例如,在x86指令集中,指令执行时最关键的现场数据就是CPU的各种寄存器数据和栈空间数据。那么,在x86平台上,要想设计代码虚拟机入口,就要先将这些数据进行保存。要做到这一点,我们可以采用类似下面的指令序列。pushadpushfd

通过这两个指令,我们就将当前CPU线程的寄存器数值压入当前线程的栈空间内了。在随后的指令中,我们即便改变了CPU寄存器的值,也能在需要时恢复原始数据。这就是保存现场的目的。然而,在x86的64位平台上,不存在像pushad这样的指令,因此我们应当采用push指令逐一保存有可能被修改的寄存器。所以,使用什么指令来实现现场保存是根据平台而定的。

2.初始化虚拟化环境

初始化虚拟化环境是虚拟机入口代码的另外一个不可避免的过程,在这个过程中需要对虚拟化环境进行初始化。既然目的是将原始的代码指令放到一个虚拟的环境中运行,那么前提就是需要有虚拟化环境。至于虚拟化环境采用什么形式,则根据不同的代码虚拟机设计而定。但是在一般情况下,代码虚拟机不可避免地需要完全或者部分虚拟一定数量的寄存器。因为代码虚拟机的运行会使用真实的CPU寄存器,所以原始指令的寄存器数据都会被转移到其他地方,原始指令对寄存器的各种操作都会被转换成对这些虚拟的寄存器的操作。在不同的虚拟机设计下,虚拟环境的区别是很大的,有的虚拟机将虚拟化寄存器保存在一个固定的内存空间中,有的代码虚拟机将虚拟寄存器放在临时申请的栈空间中。但是,无论怎样设计,其目的和原理是不变的。

3.迁移现场数据到虚拟化环境

虚拟化环境建立以后,初始化代码还需要将之前保存的实际现场数据都转移到虚拟化环境中去。因为在模拟原始指令的执行时需要提供正确的原始环境数据,而在保存环境数据时虚拟化环境还没有被初始化,所以必须要对当时保存的数据进行调整,使其迁移到相应的虚拟化环境中去,这样虚拟化出来的指令执行结果才是等价的。

4.准备虚拟化媒介

准备虚拟化媒介需要在入口代码中完成,但可以不是顺序的。在代码虚拟机的入口处,代码除了要完成虚拟化环境的初始化,往往还需要准备本次代码虚拟化所对应的目标代码,也就是告诉虚拟机应当模拟执行哪段代码。因为一个代码虚拟机往往可以用来模拟执行许多代码片段,所以具体应当执行哪些代码片段就需要在入口代码中甄别出来。1.1.3 解码执行器

代码虚拟机的入口代码处理完毕,代码的运行状态就具备了模拟执行代码的一些必要条件。这个时候,代码的控制权交给了代码虚拟机的解码执行器。解码执行器的工作过程相对简单,就是根据入口代码提供的虚拟化媒介对虚拟化媒介进行解码,并根据解码结果实施不同的动作,直到虚拟化媒介给出结束模拟执行的指令为止。一旦发生了结束动作,就按照动作规定将控制权转移给代码虚拟机的出口代码。实际上,该阶段代码的设计实现往往是一段循环代码,其流程是:解码虚拟化媒介→执行动作→解码虚拟化媒介。尽管过程相当简单,但是从具体的代码设计上,这也是代码虚拟机最复杂的部分。为了更好地隐藏原始指令的功能性意图,代码虚拟机的动作指令设计通常都是相当复杂的,不仅要配合其他代码加密技术,而且实现解码和执行动作的方式各不相同,对每个动作本身也是自由发挥的。因此,代码虚拟机的主要性质都反映在这个环节中。1.1.4 出口代码

代码虚拟机的出口代码往往被设计成代码虚拟机模拟执行循环中的一个子动作。在出口代码执行过程中,通常将运算过后的寄存器数据从虚拟化环境中取出,并将这些数据恢复到实际的CPU寄存器中去,而且可以选择销毁虚拟化环境,再将其他现场数据还原成与真实指令执行相同的样子,最后再次通过流程控制指令将其转移到真实指令中去执行。这样,一个代码虚拟化执行过程就结束了。1.2 代码虚拟机非运行时部件

在1.1节中我们了解到,代码虚拟机运行时框架只是一款代码虚拟机在运行时所必需的组件,但要完整地支撑一款代码虚拟机正常模拟执行指令,仅有运行时部件是不够的。例如,将原始代码转换为虚拟化媒介这个过程,在大多数情况下并非发生在程序运行过程中,而是在代码加密过程中利用运行时部件以外的程序提前将原始代码指令转换为虚拟化媒介,直接存放到目标程序中或者其他位置,代码虚拟机在运行时直接进行解码模拟执行即可。又如,对原始指令的处理往往不是在运行时才修改的,而是在转换为虚拟化媒介后,原始指令数据就被丢弃了。这说明,一款完整的代码虚拟机除了上面提及的运行时部件外,还存在许多非运行时部件,这些部件根据不同的虚拟机有不同的设计,但总体来说还是有共通的性质。在正常情况下,代码虚拟机除了运行时部件外,还包括一些非运行时部件,如图1.3所示。

我们看到,一般的代码虚拟机除了运行时部件外,还包括译码器、虚拟化媒介编码器和原程序处理器。这3个部件分别用于从原始程序中读取需要进行虚拟化的代码数据进行解码识别(例如,对x86指令进行反编译),将编译出来的功能性结果传递给虚拟化媒介编码程序,将这些原始代码转换为对应的虚拟化媒介。然后,虚拟化媒介编码器将编码后的数据传递给原程序处理器,由原程序处理器一并打包到加密后的程序中。原程序处理器还用于将代码虚拟机的运行时部件同时打包到加密后的程序中,并负责添加条件控制指令及移除已经被虚拟化的代码指令等。然而,对复杂一点的代码虚拟机,我们会意识到,代码虚拟机的运行时部件不能使用静态编译出来的代码程序,因为通过这种方式得到的运行时部件,其自身的工作流程和代码逻辑很容易被逆向分析,而代码虚拟机的加密效果和复杂程度完全取决于代码虚拟机的运行时部件。因此,在现代的代码虚拟机系统中,运行时部件都是根据被加密代码动态生成的。在这种情况下,还需要增加一个运行时部件生成器。图1.3

从整体结构上看,一般的代码虚拟机包含上面提及的3个部分。在这种框架下我们可以看到,一款虚拟机的复杂程度完全取决于该虚拟机的虚拟化媒介编码器,而虚拟化媒介编码器的编码方式取决于运行时部件(虚拟化媒介的解码在运行时部件中完成),因此,运行时部件的复杂程度直接决定了整个虚拟机的保护效果和复杂程度。一款优良的代码虚拟机在运行时部件的设计上会特别复杂,也出现了多种技术方案。在现代主流的代码虚拟机设计中,有4种运行时部件的技术方案比较成熟,分别是模拟虚拟化、转译虚拟化、变形虚拟化和脚本虚拟化。我们将在之后的章节中分别讨论这4个技术流派。在本节中,我们接着对代码虚拟机的各个组成部件进行简单的了解。1.2.1 译码器

代码虚拟机的译码器实际上就是一个解析被虚拟化代码指令的功能性信息的程序,和计算机编程语言中的词法分析器是一个道理。代码虚拟机之所以需要译码器,是因为我们如果想要虚拟化模拟执行一个指令,必须事先知道这条指令是什么指令、这条指令的意图是什么及这条指令包含哪些数据等信息。因为我们模拟执行这条指令的结果应该与执行这条指令本身的结果等价,所以,译码器的工作过程就是分析整理原始指令的功能性信息的自动化过程。

在一般情况下,译码器以一个目标平台指令集所对应的反汇编引擎为核心,配合相关的整理代码,使我们可以将原始指令信息转换为统一且固定的中间信息,并提供给虚拟化媒介编码器使用。当然,译码器除了收集原始指令的自身信息之外,还需要收集能够影响指令执行结果的其他信息,例如指令的重定位信息、在内存中的地址信息等。宗旨就是:我们收集的信息必须能够实现我们以其他方式得到的与该条指令执行等价的运算结果。1.2.2 虚拟化媒介编码器

虚拟化媒介编码器是代码虚拟机中一个非常重要的部件,其主要目的是将译码器翻译出来的原始指令的功能性信息转换为另外一种形式的数据,并提供给代码虚拟机的运行时部件,以作为模拟指令的依据。根据这些信息,代码虚拟机可以模拟出与原始指令执行等价的结果。

在代码虚拟机中如何编码虚拟化媒介的数据,直接关系到代码虚拟机在执行过程中的虚拟化结果,同时决定了代码虚拟机对原始指令的保护程度。如果虚拟化媒介编码器的编码结果一定会在代码虚拟机运行部件中被解码,那么解码过程就是公开的。因此,如何在这种情况下使编码与解码的过程更加隐蔽和难以理解,在很大程度上决定了代码虚拟机的保护效果。但是,虚拟化媒介编码器的编码过程并不是随意的,而是通过代码虚拟机在虚拟化媒介解码器中的实现及动作指令的设计,根据译码器翻译出来的原始指令的相关数据综合处理完成的。因此,虚拟化媒介编码器是整个代码虚拟机设计中最复杂的部件。1.2.3 运行时部件

代码虚拟机的运行时部件主要包括代码虚拟机在程序运行过程中进入代码虚拟化以后的具体运行代码。这些代码的主要目的是将虚拟化媒介进行解码,并根据解码结果执行相应的动作,以通过各种设定好的动作在执行环境中产生与被虚拟化指令相同的数据结果。根据代码虚拟机的特点,运行时部件一般包括入口代码、出口代码、用于解码虚拟化媒介的解码器及与解码结果相对应的各种动作指令。也就是说,运行时部件实际上是代码虚拟机模拟执行代码中除了与虚拟化媒介编码相关的代码之外的所有代码。

在运行时部件的代码中,最为关键的部分是根据虚拟化媒介的解码结果执行的各种动作代码的设计。在本书的第2部分中我们将看到,对于解码过程,都会有对应的自动化分析技术很好地记录解码结果,但动作代码在多数情况下还是需要配合人工来分析。在现代的代码虚拟机中,这些动作指令大都比较多,少则几十个,多则上千个,看起来就像一般代码中的一个大的switch分支语句,因此,我们经常用“OP代码分支”来称呼这些具体的动作代码。1.2.4 运行时部件生成器

这个部件不是所有的代码虚拟机都必须拥有的组件,但是在代码虚拟机设计中,固定的运行时代码已经不适合现代技术的发展了。因此,如果是出于保护代码的目的而设计的代码虚拟机,其运行时部件应至少有一部分是动态生成的,这就涉及运行时部件生成器。

好的代码虚拟机应该是整个运行时部件都是动态生成的,但在实际应用中,由于技术的复杂性和可操作性,往往没有实现这一点,更多的情况是编写一些模板代码,然后将这些模板代码进行变形等代码加密,最后生成运行时部件。1.2.5 原始程序处理器

原始程序处理器是一个综合型的处理工具,它具体负责对需要保护的代码的宿主程序进行修改,使程序在代码被虚拟化以后也能正确地被识别并启动。而且,出于保护的目的,还需要移除已经被转换为虚拟化媒介的代码,添加流程控制指令,完成为了确保运行时代码能够正常运行的其他工作——实际上,再复杂一点就成了一款软件保护程序,也就是我们俗称的“壳”。不过,如果只关乎代码虚拟机,那么原始程序的处理程序并不需要太复杂。因此,很多工作都是运行时代码自身就能完成的,原始程序处理器只需要将运行时代码和虚拟化媒介按要求打包到目标程序即可。1.3 本章小结

在本章中,我们对代码虚拟化原理进行了简单的介绍。之所以没有细致、深入地讲解代码虚拟化的运行细节和流程,是因为在本书的第2部分中我们会详细讲解这个过程(将这些内容放到第2部分,配合实际操作,更容易理解)。但我们还是对代码虚拟机的大体框架和各组件有了初步的了解,并且知道一款代码虚拟机的复杂程度取决于其虚拟化媒介的编码过程及运行时代码的OP代码分支的设计。

现代的代码虚拟机设计有4种主流技术方案,在后面我们会分别讨论,并尝试针对每种技术方案实现一个简单的代码虚拟机,以便对代码虚拟机的实现有一个彻底的了解。第2章 模拟虚拟化

模拟虚拟化是指在设计OP代码分支的时候,直接或者准直接模拟目标代码指令的运行流程,只根据虚拟化环境对指令的操作目标进行简单的调整,从而实现虚拟化执行的代码虚拟机实现方法。

模拟虚拟化是代码虚拟机实现技术中最为简单的一种,从结构上来说,模拟虚拟化的运行时代码大多数是静态的,因此不需要运行时部件生成器。在公开环境中,Bochs内部使用的就是这种实现方式。尽管 Bochs 的设计目标是打造一款全环境虚拟机,但由于效率问题,在很多情况下我们仍然将其当成一款代码虚拟机来分析和使用。要想了解模拟虚拟化的具体实现,不妨从简单分析Bochs开始,然后着手去实现一个样机。2.1 Bochs简单分析

Bochs 是一款开源的虚拟机系统,可以模拟并运行一个完整的虚拟系统。但是,Bochs没有采用任何硬件虚拟化技术,整个模拟过程都是直接使用 C/C++代码实现的。因此, Bochs具备模拟整个CPU执行指令的部件(尽管其设计目的并非用于代码虚拟机,也不具备一般代码虚拟机框架中的入口代码与出口代码,没有具体的虚拟化媒介编码过程,但其他部件都是具备的),特别适合我们理解和分析代码虚拟机的运行过程。而且,通过对齐进行简单的改装,就能达到实现一款代码虚拟机的目的。下面我们先对其进行简单的分析,从而对代码虚拟机有一个具体的印象。

在Bochs的源代码目录结构中直接使用目录名表示Bochs的各个部件,如图2.1所示。

最为关键的是“cpu”目录,该目录下的文件是整个CPU的模拟代码,其中按照指令的类别将模拟代码写在了对应的文件中,如图2.2所示。

我们可以很容易地猜到,3dnow指令是在3dnow.cc中模拟实现的,逻辑运算指令是在arith*文件中完成的,结构非常清晰。在工程文件上,cpu与fpu也分别被独立成组件,如图2.3所示。图2.1图2.2图2.32.1.1 模拟CPU对象

一个虚拟化系统的关键在于对CPU的虚拟化,因此我们的分析也从其对CPU的虚拟开始(在Bochs中就是cpu组件)。

我们首先找到 cpu 项目最关键的工程文件 cpu.h,在这个头文件中定义了具体的虚拟CPU对象。CPU对象的部分定义代码如下。class BOCHSAPI BX_CPU_C : public logfunctions {  //CPU 对象从一个基础类继承而来,但是该类本身不具备与模拟相关的功能public: // for now...unsigned bx_cpuid; //cpuid编号bx_cpuid_t *cpuid; //指向一个bx_cpud_t对象,用于管理模拟CPU的类型Bit64u isa_extensions_bitmask; //CPU的各种属性标记Bit32u cpu_extensions_bitmask;Bit32u vmx_extensions_bitmask;Bit32u svm_extensions_bitmask;// General register set// rax: accumulator// rbx: base// rcx: count// rdx: data// rbp: base pointer// rsi: source index// rdi: destination index// esp: stack pointer// r8..r15 x86-64 extended registers// rip: instruction pointer// nil: null register// tmp: temp registerbx_gen_reg_t gen_reg[BX_GENERAL_REGISTERS+3];  //这里就是最重要的用于存储模拟寄存器的数据位置/* 31|30|29|28| 27|26|25|24| 23|22|21|20| 19|18|17|16* ==|==|=====| ==|==|==|==| ==|==|==|==| ==|==|==|==* 0| 0| 0| 0| 0| 0| 0| 0| 0| 0|ID|VP| VF|AC|VM|RF** 15|14|13|12| 11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0* ==|==|=====| ==|==|==|==| ==|==|==|==| ==|==|==|==* 0|NT| IOPL| OF|DF|IF|TF| SF|ZF| 0|AF| 0|PF| 1|CF*/Bit32u eflags; // Raw 32-bit value in x86 bit position.  //这里用于存储原始CPU的标志寄存器// lazy arithmetic flags statebx_lf_flags_entry oszapc; //这是为了加速而设计的延时逻辑运算标记寄存器// so that we can back up when handling faults, exceptions, etc.// we need to store the value of the instruction pointer, before// each fetch/execute cycle.bx_address prev_rip;  //用于记录上一条指令执行的内存地址bx_address prev_rsp;bx_bool  speculative_rsp;Bit64u icount;Bit64u icount_last_sync;/* user segment register set */bx_segment_reg_t sregs[6];   //用于模拟段寄存器/* system segment registers */bx_global_segment_reg_t gdtr; /* global descriptor table register */bx_global_segment_reg_t idtr; /* interrupt descriptor table register */bx_segment_reg_t     ldtr; /* local descriptor table register */bx_segment_reg_t     tr;  /* task register *//* debug registers DR0-DR7 */bx_address dr[4]; /* DR0-DR3 */   //调试寄存器系列模拟bx_dr6_t  dr6;bx_dr7_t  dr7;

由于篇幅问题,我们只给出了部分对象的定义。但是从定义中我们已经了解到,要建立一个虚拟化环境,就需要根据被模拟的代码目标使关系到代码运行的数据对象都能够用代替的方法表示出来。在上面的CPU对象中,Bochs将被模拟环境的寄存器数据存放都转移到了 CPU 对象的成员结构中,这样在执行被模拟指令代码时将不再需要实际环境中的CPU寄存器,而是访问该CPU对象实例中的成员数据。这在代码虚拟机中几乎是固定的模式,无论结构成员多么复杂,最终都会将寄存器数值体现到内存空间中。2.1.2 译码器

由于 Bochs 并非真正意义上的代码虚拟机,因此不存在我们之前介绍的传统的入口代码与出口代码。下面我们直接来看Bochs模拟执行代码的第1个过程译码。

在 Bochs 中,译码的过程是通过一个译码函数来完成的,该函数根据不同的平台版本有不同的名称,示例如下。BX_SMF int fetchDecode32(const Bit8u *fetchPtr, bxInstruction_c *i, unsigned remainingInPage)

其中,参数fetchPtr表示被译码指令的内存地址,参数i用于存放译码后指令所对应的功能性信息,第3个参数用于处理内存页访问,我们无须关心。

该函数为 CPU 对象的成员函数,具体实体在代码文件 fetchdecode.cc 中,其部分代码如下。BX_CPU_C::fetchDecode32(const Bit8u *iptr, bxInstruction_c *i, unsignedremainingInPage){b1 = *iptr++;remain--;//省略部分代码switch (b1) { //处理指令的第1个字节,实际上就是处理OPCODE的第1个字节case 0x0f:  //2-byte escapeif (remain != 0) {remain--;b1 = 0x100 | *iptr++;break;}return(-1);case 0x66: // OpSize //处理size前缀os_32 = !is_32;if(!sse_prefix) sse_prefix = SSE_PREFIX_66;i->setOs32B(os_32);if (remain != 0) {goto fetch_b1;}return(-1);case 0x67: // AddrSizei->setAs32B(!is_32);if (remain != 0) {goto fetch_b1;}return(-1);case 0xf2: // REPNE/REPNZ//处理rep前缀case 0xf3: // REP/REPE/REPZsse_prefix = (b1 & 3) ^ 1;rep = b1 & 3;if (remain != 0) {goto fetch_b1;}return(-1);case 0x26: // ES://处理段寄存器修饰case 0x2e: // CS:case 0x36: // SS:case 0x3e: // DS:seg_override = (b1 >> 3) & 3;if (remain != 0) {goto fetch_b1;}

从上面的代码中不难看出,该函数就是 Bochs 的译码器。其功能正如我们所说的,对被虚拟化指令的功能性信息进行解析,并将其存储到bxInstruction_c结构对象中去。根据这些功能性信息,CPU类可以模拟出指令的执行结果。2.1.3 解码执行器

在Bochs中没有虚拟化媒介编码器。Bochs的设计目的并非加密指令代码,其译码器的译码结果直接传递给解码执行器进行模拟执行。所以,我们可以将bxInstruction_c结构对象当成虚拟化媒介。

下面来看解码执行的过程。Bochs的解码执行环节在cpu.cc源文件的cpu_loop函数中实现,代码如下。void BX_CPU_C::cpu_loop(void){BX_CPU_THIS_PTR prev_rip = RIP; // commit new EIPBX_CPU_THIS_PTR speculative_rsp = 0;while (1) {if (BX_CPU_THIS_PTR async_event) {if (handleAsyncEvent()) {return;}}bxICacheEntry_c *entry = getICacheEntry();bxInstruction_c *i = entry->i;   //取得 bxInstruction_c 结构对象,在该结构中存放了译码结果for(;;) {   //执行循环// want to allow changing of the instruction inside instrumentation callbackBX_INSTR_BEFORE_EXECUTION(BX_CPU_ID, i);RIP += i->ilen();// when handlers chaining is enabled this single call will execute entire traceBX_CPU_CALL_METHOD(i->execute1, (i)); //根据译码结果调用对应的动作代码分支,即OP代码分支,OP代码分支是在译码过程中确定的BX_SYNC_TIME_IF_SINGLE_PROCESSOR(0);if (BX_CPU_THIS_PTR async_event) break;i = getICacheEntry()->i;}if (BX_CPU_THIS_PTR async_event) break;if (++i == last) {entry = getICacheEntry();i = entry->i;last = i + (entry->tlen);}}// clear stop trace magic indication that probably was set by repeat or branch32/64BX_CPU_THIS_PTR async_event &= ~BX_ASYNC_EVENT_STOP_TRACE;} // while (1)}

从上就是 Bochs 的解码执行主循环,过程非常简单。实际上,在上面的函数中还做了相当多的额外工作,在实际的代码虚拟机中一般只有一个循环,很少有其他处理代码。需要注意的是,这个解码执行过程属于运行时对象的一部分,因为翻译bxInstruction_c对象可以在任何时刻进行,所以我们可以事先翻译bxInstruction_c对象,然后保存结果,交由解码执行器去执行。2.1.4 OP代码分支

OP代码分支就是具体模拟执行代码的地方了。我们看到,在解码执行器中,解码执行函数实际上就是 CPU 对象的成员函数,它可以随时访问虚拟化环境。bxInstruction_c 对象的结构定义如下。class bxInstruction_c {public:// Function pointers; a function to resolve the modRM address// given the current state of the CPU and the instruction data,// and a function to execute the instruction after resolving// the memory address (if any).BxExecutePtr_tR execute1; //这个地址就代表译码后的指令所对应的OP代码分支union {BxExecutePtr_tR execute2;//扩展信息,有的指令可能有2个OP代码分支bxInstruction_c *next;} handlers;BxResolvePtr_tR ResolveModrm;struct {// 15...0 opcodeBit16u ia_opcode;//译码的OPCODE// 7...4 (unused)// 3...0 ilen (0..15)Bit8u ilen;// 7...6 VEX Vector Length (0=no VL, 1=128 bit, 2=256 bit)//     repUsed (0=none, 2=0xF2, 3=0xF3)// 5...5 extend8bit// 4...4 mod==c0 (modrm)// 3...3 os64// 2...2 os32// 1...1 as64// 0...0 as32Bit8u metaInfo1;} metaInfo;// using 5-bit field for registers (16 regs in 64-bit, RIP, NIL)Bit8u metaData[8];union {// Form (longest case): [opcode+modrm+sib/displacement32/immediate32]struct {union {Bit32u Id;Bit16u Iw;Bit8u Ib;};union {Bit16u displ16u; // for 16-bit modrm formsBit32u displ32u; // for 32-bit modrm formsBit32u Id2;Bit16u Iw2;Bit8u Ib2;};} modRMForm;};

这个结构可以将被模拟指令的功能性信息都翻译到结构中对应的成员数据上,解码执行器根据这个结构中的数据信息进行模拟运算,其中 execute1指针指向了具体模拟该指令执行的代码分支。由此可知,在Bochs虚拟机中,指令的执行是一对一的关系,也就是说,一条指令有一个对应的OP代码分支用于模拟这条指令的执行。我们找一个代码分支进行分析,示例如下。BX_INSF_TYPE BX_CPP_AttrRegparmN(1) BX_CPU_C::ADD_GdEdR(bxInstruction_c *i){ //reg形式的寄存器加OP代码分支Bit32u op1_32, op2_32, sum_32;op1_32 = BX_READ_32BIT_REG(i->dst());  //根据译码结果读取虚拟环境中的目标寄存器,即操作数1op2_32 = BX_READ_32BIT_REG(i->src()); //根据译码结果读取源寄存器,即操作数2sum_32 = op1_32 + op2_32; //对2个操作数进行加运算BX_WRITE_32BIT_REGZ(i->dst(), sum_32); //将结果写入虚拟化环境寄存器,即模拟的寄存器SET_FLAGS_OSZAPC_ADD_32(op1_32, op2_32, sum_32); //根据计算结果计算标志寄存器BX_NEXT_INSTR(i);}

BX_READ_32BIT_REG是宏定义,用于读取虚拟化环境中的寄存器,其定义如下。#define BX_READ_32BIT_REG(index) (BX_CPU_THIS_PTR gen_reg[index].dword.erx)

可以看到,实际上这个宏定义的功能就是访问gen_reg成员结构中对应的寄存器数据。我们在CPU类声明中已经看到,gen_reg结构是用于模拟实际寄存器的成员结构。2.2 模拟虚拟化特征

在2.1节我们给出了Bochs的简单分析。从分析过程中我们了解了虚拟化环境对象的基本形式,也看到了译码器、解码执行器及OP代码分支的基本实现。尽管这些实现在不同的代码虚拟机上都不一样,但大体形式是相同的,所以,我们可以给出模拟虚拟化的技术特征。所谓模拟虚拟化是指像 Bochs 这样,使用确定、简单、一对一的方式来翻译和执行被模拟指令的虚拟化方式。对被模拟指令的模拟动作,一般在一个OP代码分支中完成。

在现代成熟保护系统的虚拟机中,Winlicense代码虚拟机符合这种代码特征。2.3 实现模拟虚拟化样机

在了解了模拟虚拟化的具体特征并对 Bochs 进行了简单分析以后,我们对模拟虚拟化代码虚拟机有了一定的了解。现在,我们可以通过自己动手实现一个简单的模拟代码虚拟机来进一步体会代码虚拟机的工作原理和实现过程。2.3.1 模拟目标设定

既然是自己动手,那么在本节中我们就要实现一款代码虚拟机除运行时部件生成器之外的所有部件。因为模拟虚拟化是代码虚拟机的一种比较简单的形式,因此目前我们还没有必要去实现复杂度较高的运行时代码生成器(将在后面的章节中实现)。由于实现一个完整的代码虚拟机需要非常大的工作量,因此这里只是有针对性地实现一个样机。我们首先构建如下目标程序。#include int main(){DWORD dwval;__asm{mov eax,0x10000000  //自己构建mov指令add eax,0x1234     //构建add指令,这两条指令就是代码虚拟机样机模拟的目标mov dwval,eax}char buf[16];itoa(dwval,buf,16);MessageBoxA(0,buf,"Result:",MB_OK|MB_ICONINFORMATION);return 0;}

编译后对应的代码如下。00401000  55        push ebp00401001  8BEC       mov ebp,esp00401003  83EC 14     sub esp,1400401006  B8 00000010   mov eax,10000000  ->这里就是我们模拟的目标代码0040100B  05 34120000   add eax,1234     ->模拟目标代码00401010  8945 FC     mov dword ptr [ebp-4],eax00401013  6A 10       push 1000401015  8D45 EC     lea eax,[ebp-14]00401018  50        push eax00401019  FF75 FC     push dword ptr [ebp-4]0040101C  E8 D4430000   call 004053F500401021  83C4 0C     add esp,0C00401024  6A 40       push 40

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载