软件保护及分析技术:原理与实践(txt+pdf+epub+mobi电子书下载)


发布时间:2021-03-28 15:18:56

点击下载

作者:章立春

出版社:电子工业出版社

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

软件保护及分析技术:原理与实践

软件保护及分析技术:原理与实践试读:

前言

软件保护与分析技术领域一直以来都充满着神秘色彩。一提到这个领域,总会激发外行人的强烈好奇和异样的眼光。作为一个业内人士,我认为,尽管这个行业内充满了利益瓜葛,但是,若我们抛开利益,回归这个技术领域的本质,就会发现更加重要的其实是这个领域给我们带来的乐趣、对事物的理解,以及那些成功与挫败的体验,这些才是这个领域最为本质也是最吸引人的地方。

很难想象,如果我们涉及计算机行业,但又一直只是充当一个代码工具,在现在这个软件设计越来越趋向对象化、自动化的年代,我们真不知道除了代码之外还能有些什么,更不用说长期做重复单调的工作需要多大的耐心和毅力。

黑格尔曾经说过,事物总是在对立中才能进步。软件加密与解密这个领域正好有两种永远对立的事物,这就使得这个领域不存在终点,永远都有更加深入的技术等待着我们去探索,让这个领域充满了乐趣。但是,随着计算机硬件的快速发展,这个领域的这种对立正在慢慢地发生变化并且失去平衡。因为计算机越来越快的运算速度,使得加密技术可以使用计算机的这种计算优势将安全性转移到计算机运算速度上,也就是说,现代的加密技术可以使用巨大的运算量来换取安全性的提高。一方面,这对破解来说是极不乐观的,因为在破解技术上,大多数的分析还是依靠人脑。另一方面,由于计算机领域具有累积性,慢慢地,加密与解密这个领域开始具备了排斥性,使这个领域的门槛越来越高,这让无数热爱计算机技术的人失去了体验这个领域内众多乐趣的机会。

写作背景

我相信很多准备进入软件行业的技术人员都对软件的保护和破解领域具有浓厚的兴趣,但是我也能想象,当他们兴致勃勃地从网上搜索各种加密与解密入门教程,并着手模仿和练习时,却发现现在的软件随便用PEiD查看都会显示“VMProtect”字样,然后他们转而查找有关VMProtect的资料——好不容易找到一点点关于VMProtect的资料后却发现,文档中的内容完全不知所云,或者文档中描述的内容和他们实际操作的代码完全不一样,这让他们感到极度挫败。更加严重的问题在于:当我们习惯使用这些成熟、强大的保护系统以后,就慢慢丧失了追寻事物本质的能力;我们看重成熟的保护系统给软件带来的安全性,就慢慢忽视了这些保护系统保护软件的技术细节,这使得我们产生一种错觉,甚至可能以为一旦拥有这样的保护系统,我们的软件就安全了,这是相当严重的问题。

作为一个成熟的技术人员,我们应当足够了解我们所开发软件的整个运行流程,这样才能在各种情况下做出相对准确和客观的判断。因此,这也包括我们对软件保护系统及软件破解技术的了解。

本书全面介绍了软件保护与分析领域的相关技术,因此涵盖很多高手看来较低级的技术,还会提及很多基础性的内容,但由于篇幅限制,我会尽量简化这些部分。在我看来,在加密与解密领域,对技术的理解必须用理论与实际结合并侧重于实际操作的方式效果才会更好。因为这个领域是一个需要培养自己动手能力和花大量时间去具体操作才能完全理解的领域,只有在实际的操作过程中才能体会到这个领域中的千变万化,所以,书中较多地采用教学的方式,并且尽量提供相应的代码和示例,最后会一步步地引导读者打造自己的工具以提升动手能力。

本书充分考虑到读者可能面临的实际情况,所以尽量不涉及和不使用内核技术来解决问题,而是将各种方法都放在Ring3下模拟和实现,以避免频繁的蓝屏和复杂的内核调试过程吓退读者。

本书特点

保护与分析全覆盖:本书涵盖软件保护与软件破解的大部分相关技术,不仅介绍了大量的软件保护技术,也介绍了大量的软件破解技术,通过保护与破解技术的对比,读者可以更加深刻地理解和体会各种技术的优缺点。

理论与实践结合:本书不仅通过理论来介绍各种技术,还通过实际代码和操作流程将这些理论转化为实际的程序或者工具。通过本书的“自己动手”部分,读者可以充分体会到将一种技术理论转变为实际操作过程或者程序的整个步骤,并从中体会到理论与实际应用的差别,最终对各种技术有充分、全面的理解。

高阶技术:本书不仅介绍一些常见而普通的技术,在许多方面,本书所介绍的技术即使对很多业内人士来说,也是相当有难度的。通过这些技术的示例和应用,希望能让读者明白:在计算机领域,缺乏的并不是工具,而是idea。

读者对象

本书的内容并非是为初学者设计—尽管我希望能将我的想法表达得足够简单。在开始阅读本书之前,我假定你(读者)至少已经具备以下初步的计算机能力:

● 基本的C/C++编程能力

● 基本的x86汇编能力

● 一定的软件运行原理知识

如果你已经具备以上条件,那么恭喜你,你将可以很好地进入本书的其他部分。由于本书涉及的技术属于两大对立的板块,所以,为了清晰地阐述本书结构,在章节安排上我采取了归纳的形式,各个章节在技术难度上没有绝对的先后关系。读者在阅读的时候,可以根据自己的实际情况调整阅读顺序。章立春2016年1月第0章准备知识

如果你跳过了前言直接阅读本章,强烈建议你还是返回仔细阅读前言。在这里我们不会详尽地讨论计算机的运行原理,只是关心与本书内容相关的知识,以及Windows平台下的程序运行基本流程和必要条件。

为了照顾一些基础比较差而又对软件加密与解密特别好奇的读者,我们特别安排了本章。如果已经具备相关知识,请跳过本章。0.1 Win32程序

首先我们引出一个问题:什么是程序?

顾名思义,程序就是一系列的流程和动作。在计算机中,我们所说的程序,一般是指能够操控计算机帮助我们完成一件或多件事情的一种事物,这种事物以数字的方式存在。

回顾一下计算机原理。计算机最重要的组件就是CPU(中央处理器), CPU最重要的功能就是进行逻辑运算,比如加、减、乘、除这些常用的运算。为了让CPU能够真正为我们所用,我们必定要有控制CPU进行运算的能力。在计算机中,因为CPU只能进行纯数字的运算,所以CPU是数字指令模式的,也就是说,我们使用CPU的过程是:向CPU输入一段数字和处理这些数字的方式,CPU经过运算后返回一堆数字结果。另外,计算机处理我们所输入的数字的方式,我们也是以数字的形式告诉CPU的,如今大部分CPU只接受二进制(只需要用0和1表示)数字,任何其他形式的数字最终都会转换为二进制输入到CPU中。我们将输入CPU的这些数据(包括需要被处理的数字和处理方式)称为指令或代码,将CPU的运算结果称为指令结果。为了实现不同的目的,需要让CPU处理不同数目和类别的指令,这类指令称为指令集。你可能听说过x86、x87、mmx、sse、MIPS等,它们都是不同的指令集。一块CPU可能具备处理多种指令集的能力,比如我们目前常用的Intel通用处理器一般都具备处理x86、x87、mmx指令集的能力。

如图0.1所示是一段x86指令示例。图0.1

图0.1中共包含8条指令。第2列的十六进制数据就是输入CPU的指令数据,可以发现,指令始终是数字形式的(在十六进制中,A、B、C、D、E、F代表数字而非字母)。第1列的地址表示这些指令在内存中的存放位置。第3列的汇编代码就是这些数字指令所对应的指令翻译,我们称之为汇编代码。CPU的计算与这里看到的第3列内容无关,这些代码形式只是为了方便我们阅读和理解这些数字指令而翻译出来的。

从图0.1中我们还可以看出,每条指令可以具有不同的长度。在x86指令集的CPU中,指令是顺序执行的,也就是说,除非遇到流程控制指令,否则CPU会自上而下地顺序执行指令。

所以,程序就是一系列CPU指令的集合,我们甚至可以说,一条指令就是一个程序。但是,随着计算机技术的发展,对程序的这种范围上的定义越来越不适合表述现在的情况。现在,一台普通计算机每秒所运行的指令都是上百万级别的,因此我们对程序的这种定义就显得很笼统。

现在的各种多任务操作系统,如Windows、Linux、Mac OS,都有一种按照功能将一族代码归类的设计,称之为进程。在这种设计理念下,一个进程将为了实现一个目的而设计的大量指令归类到同一个集合中进行管理,所以我们常把进程称为程序,如图0.2所示。图0.2

在图0.2中,计算机会同时运行多个程序,每一个程序都负责特定的工作,也因此拥有特定的指令集合。根据冯·诺依曼体系(一种计算机设计体系,核心思想是将代码复制到内存后再交由CPU执行),每个程序运行时,这些指令都会被复制到内存中提供给CPU执行,在程序没有执行时,这些代码就存储在文件中。第1列的映像名称就表示这个程序的入口代码数据所在的文件名称。根据系统设计的不同,各系统所要求的文件存储程序代码的方式也不同。在Linux下程序文件格式为ELF,在Windows下程序文件格式为PE。0.2 PE程序文件

PE文件格式是Windows下程序代码在文件中的存储规范,任何Win32子系统的程序都要以这种规范存储才能被系统正确识别并加载运行。PE程序的详细格式,请读者自行阅读相关文档。当然,这里我们也只提供一种直观地观察PE文件程序格式的方法,希望帮助读者更加快速地了解PE文件格式。

要理解PE文件格式,除了埋头阅读介绍文档,更好的方法是通过PE查看或编辑工具来观察实际的PE文件,目前使用比较多的PE文件工具有PEiD、LordPE、stud_pe、Explorer Suite、PE-Explorer等。在这里我们选用stud_pe,因为它有一个直观的十六进制和PE文件头同步查看器。

启动stud_pe,软件运行界面如图0.3所示。图0.3

依次单击“File”→“Open PE File”菜单项,打开一个PE程序文件(例如c:\windows\system32\notepad.exe),然后单击“Basic HADERS tree view in hexeditor”按钮,打开如图0.4所示的界面。通过该界面显示的内容和相关文档,我们可以详细地观察PE文件头的各个成员在文件中的对应数据和位置。相信通过这样的直观观察,读者一定能够很快对PE文件格式有所了解。图0.4

在充分了解PE文件格式后,我们将简单讨论PE文件格式中各成员的重要程度,这对于我们理解软件破解是很有帮助的。

一个PE程序要被Windows正确识别并启动运行,需要PE文件的各个成员数据设定正确,但这并非对所有成员都是必要的。

实际上,在PE文件头中,DOS Header(详见Windows S DK w innt.h头文件中的IMAGE_DOS_HEADER)部分对于32位与64位PE程序来说,只有其中的Magic Number和e_lfanew(File address of new exe header)成员是重要的,其他成员都只有16位程序才会使用。在这个头中,Magic Number固定为4D和5A(MZ),所以,判断一个PE文件最简单的方法就是观察文件是否由字符“MZ”开始,如果不是,则该文件一定不是可以直接运行的PE文件。

紧接着DOS头的是FileHeader(详见winnt.h中的IMAGE_FILE_HEADER)。在这个头中,Machine(如图0.4所示的Signature)、NumberOfSections、SizeOfOptionalHeader、Characteristics成员都是必需的,Machine成员固定为“50 45 00 00”字节序列。因此,在判断DOS头后,进一步验证PE文件是否正确的方法就是验证这里的字节序列。NumberOfSections指明了该PE文件头所拥有的区段数目,对于一个能够正确运行的PE程序文件来说,区段映射必须正确。

SizeOfOptionalHeader指明了紧接着FileHeader的Optional Header的大小,操作系统会验证这里的值来判断PE文件的特征,所以也是必需且要正确设定的。Characteristics指定了PE文件所具备的信息,如是否是DLL模块等,这对于操作系统是否能够正确识别PE程序文件是必要的。

在Optional Header中(尽管这个头的名字的意思是“可选头”,但是这个文件头在32位及以上的PE文件中是必需的),这个文件头根据PE文件(32位/64位)的不同有不同的结构,分别在winnt.h中定义为IMAGE_OPTIONAL_HEADER32和IMAGE_OPTIONAL_HEADER64。两者的大体结构是一样的,只是在各成员中的大小不一样。在Optional Header中,很多成员(如Magic、AddressOfEntryPoint、ImageBase、SectionAlignment、FileAlignment、DllCharacteristics、NumberOfRvaAndSizes)都是必需的,其中MajorSubsystemVersion、MinorSubsystemVersion成员指定了该PE程序文件运行所需要的最小子系统版本。从Visual Studio 2 012开始,编译器默认将这个版本设定为6.0,因此Windows 2003及以下系统默认无法运行Visual Studio 2 012编译器编译的PE程序文件。

在Optional Header中,还有一个DataDirectory数组成员,这个成员指定了PE程序运行时的另外一些信息,如图0.5所示。这个目录表指定了该PE程序运行时的必要信息,如导入表等。图0.50.3 Win32进程的启动

进入Windows系统操作界面以后,绝大部分进程都是由其他进程启动的。所以,为了理解Windows进程的启动过程,我们最好从“在一个程序中启动另外一个进程”开始。下面我们就在实际操作中一步步了解这个过程。

在一个程序中启动另外一个进程比较常用的Win32函数是CreateProcess(这是函数别名,对应于CreateProcessA或CreateProcessW,本书其他部分也只引用函数别名)。我们可以构建如图0.6所示的程序代码(读者可以在本书相应的代码目录下找到示例代码)。图0.6

编译完成后,用调试器OllyDbg启动程序并直接运行,等待程序中断,然后步过,如图0.7所示。图0.7

一路跟踪,发现CreateProcessW函数最终调用NtCreateuserProcess函数,如图0.8所示。图0.8

当我们再次步过该函数,可以发现新的进程已经被创建,如图0.9所示。这说明,真正的进程初始化和创建工作都是由系统内核完成的。由于本书不涉及Windows内核方面的内容,所以这里只简单说明原理。图0.9

Windows内核在调用NtCreateUserProcess函数后,根据传递过来的参数,先检查参数中指定的程序文件状态和文件格式(本例中为c:\windows\system32\notepad.exe),如果是合格的PE程序文件且允许载入,就开启一个新进程,并将PE程序数据按照文件格式要求映射到内存空间中。然后,进行进程的首要初始化,包括进程的PEB等。

接下来,内核会通过新建的进程空间中ntdll模块的LdrInitializeThunk函数,将代码执行转移到新进程中进行进一步初始化。使用进程工具观察,我们可以发现新进程被创建后部分模块已经自动载入了,如图0.10所示。图0.10

实际上,只有ntdll.dll模块是完全在内核中载入的,其他模块的载入(包括kernel32.dll的载入)都是在新进程本身的空间中初始化的。我们可以通过下面的方法了解这个过程。

LdrInitializeThunk函数是新进程创建后进程空间中运行的第一条指令入口,我们可以使调试器在这个入口点中断,从而分析之后的整个初始化过程。普通调试器并不具备中断到LdrInitializeThunk函数的功能,使用OllyDbg的插件StrongOD并打开“Break On Ldr”功能就可以中断到LdrInitializeThunk函数入口。

如果读者希望使用其他调试器(例如IDA)中断到LdrInitializeThunk函数,这里提供另外一种方法。构建如图0.11所示的代码(该段代码可以在本书的代码目录下找到)。图0.11

编译并运行该程序,就能够启动一个处于暂停状态的进程,此时用任何调试器附加到该进程上,就能中断于LdrInitializeThunk函数处(别忘了清除此处的“int3”),如图0.12所示。图0.12

根据774A9E5E(平台不同,该地址会不同)处的NtContinue可以发现,LdrInitializeThunk是由一个断点异常回调的,真正的线程入口在ntdll.RtlUserThreadStart处。在函数774A9F56中,系统将对进程进行初始化。我们可以在LdrInitializeThunk函数处查看此时进程的模块列表:除了ntdll外,连kernel32都没有载入,而以后可以知道,这对我们来说实在是一个好消息。此时,通过查看PEB中的LoaderData数据可以发现,进程的模块链表也没有初始化,所以,在unk_774A9F56中会首先初始化进程正常运行所必需的环境信息(如模块链表),然后通过LdrLoadDll载入kernel32.dll,最后初始化主模块的PE文件头中所要求的初始化信息,正式进入PE程序文件的加载过程。0.4 PE程序的加载

通过0.3节我们可以了解,代码执行进入LdrInitializeThunk后,系统将开始对主模块进行初始化装载。在这个过程中,由于进程的主模块内存地址在进程创建时就已经由内核映射到内存空间了,所以,要想了解整个PE程序的加载,我们可以通过更加方便的手段—加载DLL来分析。系统加载DLL和加载EXE主模块之间没有太大的区别,关键在于加载DLL时,整个程序从文件读取数据开始就是在进程本身的空间中完成的,所以能很好地被我们调试和观察。下面我们就观察一下DLL的加载流程。

构建如图0.13所示的代码,编译在调试器中启动后中断于LoadLibrary的调用。图0.13

此时,我们忽略其他无伤大雅的代码,直接跟踪ntdll模块中LdrLoadDll的位置,如图0.14所示。图0.14

根据函数名称可以确定,该函数的作用就是加载DLL模块。进入该函数,发现该函数首先对模块地址做进一步处理和判断,并查找当前模块列表是否已经载入该模块。如果模块已经载入,那么LdrLoadDll就扮演GetModuleHandle的角色,只是将模块的引用计数调整好就直接返回了。在NtOpenSection处下断点,如图0.15所示。图0.15

通过查看NtOpenSection的参数可知,LdrLoadDll首先尝试用模块名称直接测试是否有该名称模块的内存映射。如果有,就直接通过NtMapViewOfSection函数尝试映射模块的内存空间。如果没有找到区段,那么我们系统首先通过NtOpenFile打开模块文件,再使用NtMapViewOfSection函数。NtMapViewOfSection函数的定义如下。typedef NTSTATUS NTAPI LPNtMapViewOfSection(IN HANDLE hSection,IN HANDLE hProcess, IN OUT PVOID *BaseAddress, IN ULONG ZeroBits, IN ULONG CommitSize, IN OUT PLARGE_INTEGER SectionOffset OPTIONAL, IN OUT PULONG ViewSize, IN SECTION_INHERIT InheritDisposition, IN ULONG AllocationType,IN ULONG Protect)

其中,参数InheritDisposition指明了映射类型。当该参数指定为SEC_IMAGE时,该函数将校验映射区块的文件格式。当内存映射完成,系统就开始向模块列表添加模块信息,然后转入PE的相关加载。为了观察Windows是如何加载PE程序的,我们可以观察RtlImageDirectoryEntryToData函数。我们要庆幸,PE程序中大多数需要初始化的数据都由PE头中的DataDirectory目录指定,而Windows正好利用RtlImageDirectoryEntryToData函数定位数据目录并导出了该函数。该函数定义如下。void WINAPI RtlImageDi rectoryEntryToData(HMODULE hmod, voi d* a2, DWOR D type,LPDWORD lpSize)

其中,参数type定义了需要获取数据目录的索引,在系统完成映像内存映射后,我们就可以在该函数上下断点并进行观察。通过观察可以发现,在系统所有的数据目录加载过程中,对主模块来说导入表是最为重要的,对DLL模块来说重定位目录也是至关重要的,其他数据目录可以根据程序的不同进行选择。

当系统加载各目录以后,就进入模块入口代码的调用过程,控制权就移交给被加载的程序代码了。第1部分软件保护

为什么需要进行软件保护?

作为软件或程序的设计者,我们自然希望自己设计的程序能够完全按照自己希望的那样运行,更关键的是,我们希望自己的劳动成果能够有正确的体现。例如,我们为某种目的而设计了一套相当强大的软件,并且为此付出了几年的心血,因此,我们希望从使用该软件的人或机构那里得到相应的回报,比如现金,我们希望别人在使用这套我们辛苦开发出来的程序时能够付费。

但是,如果软件的开发者与用户之间没有明确的信任关系,我们该如何确保用户在使用我们的软件时付费呢?其中比较重要的手段就是,通过更改软件的设计,让软件具备一种验证能力(授权),通过对软件的这部分功能进行操作,如输入验证码等方式,控制用户对软件的使用。

这听起来相当“高科技”,但是根据我们对程序的理解和现实情况,一个程序是由计算机指令进行特定的组合,并按照特定的顺序执行,以实现程序的逻辑,而这些指令的执行顺序在真正的运行环境中是非常容易被篡改的。构建一个例子,如图P1.1所示(该例子代码可以在本书代码目录下找到)。图P1.1

编译该例子后,用软件调试器启动该程序并直接运行,定位到DebugBreak()函数的中断位置,如图P1.2所示。图P1.2

如图P1.2所示是程序编译后的对应汇编版本。按照程序设计的流程,当程序界面使用getchar获取任意一个由我们输入的字符后,我们再输入除零外的任何其他字符,程序都显示字符串“功能1! ”,也就是说,程序在00DE10A3处跳转正常的情况下,不会跳转到00DE10C9处的代码。但是,在实际情况中,一旦这个程序被分发到终端(例如用户)运行,用户同样可以像我们这样使用调试器并打开。尽管用户没有程序源代码,但可以看到相同的汇编代码。而且,用户不仅可以看到这些代码,还可以修改这些代码。用户可以将00DE10A3处的指令修改成jmp指令,如图P1.3所示。图P1.3

此时的情况就是,原定的程序经过用户的修改,变得和我们的预期不一样了。修改00DE10A3处的指令后,无论用户输入什么字符串,程序都会知道00DE10C9处的代码,也就是显示“功能2! ”这个字符串,这和我们设计这个程序的预期完全不一样。

程序的设计者自然希望程序完全按照自己的设计运行,但是通过上面的例子可以看出,程序的修改在这种情况下是非常容易的。所以,我们有很强的意愿,希望能通过一定的手段防止程序的流程被修改,让程序完全按照我们的设计运行,这就是所有软件保护技术的核心和最终目的。

实现这个目标是很困难的。因为根据CPU的一般设计,输入CPU的指令可以是CPU所支持的任意指令,也就是说,CPU自身不会也不太可能判断它所接受的指令是否是被篡改的指令。所以,要想防止我们为程序设计的指令被篡改,必须通过其他技术手段来实现。第1章 软件保护技术

为了防止我们的程序在终端上的执行被篡改,一般可以通过如下3个方面的技术来达到目的。

1.防止调试

防止调试就是阻碍用户对我们所设计的程序进行调试,尽量使用户很难甚至不能调试程序。如果用户无法调试被保护的程序,那么篡改程序的执行流程就会变得非常困难。这种思路曾经在一段时间内指导了很多保护系统的设计者,因此在这方面也发展出了非常多的技术手段,之后我们将讨论其中的一部分。

2.防止分析

防止分析是指将程序中的代码或者数据通过各种技术手段(如变形、移位等)变换为更加复杂和不直观的等价代码。虽然在程序执行结果上是相同的,但是这样做将大大提高分析者对代码的阅读和理解难度。这种技术手段的最终思想都是:用计算机在速度和存储量上的优势使代码变得复杂,拖延分析者对程序代码的理解时间,迫使分析者无法正确理解程序代码的真正用意,也就无从改变程序的运行流程甚至破解程序。

3.防止修改

防止修改是指通过技术手段,防止被保护程序的代码和数据被修改,主要通过在程序中增加对程序自身或相关代码的校验或者对程序代码进行签名验证等工作实现对程序自身的保护。

以上3个方面没有一个能够100%达到目的,而且从理论上说,这些手段都具有先天缺陷,因此,在现今的软件保护系统设计中,通常是多个或全部使用。下面我们将详细讨论各种软件保护技术。1.1 反调试

反调试作为一种重要的软件保护手段,已经成为各种保护系统的必备技术,现今的各种游戏保护系统尤其注重反调试技术。反调试技术的应用能够将众多分析者拒之门外——很多分析者对软件的分析和研究严重依赖现存的调试软件和文献,如果通过反调试技术使他们对程序的调试结果不能达到预期,他们大都会放弃。在这里我们就介绍其中几种基本的反调试技术。1.1.1 函数检测

函数检测就是通过Windows自带的公开或未公开的函数直接检测程序是否处于调试状态。最简单的调试器检测函数是IsDebuggerPresent,该函数的原型为“BOOL WINAPI IsDebuggerPresent(VOID)”,当检测到程序处于调试状态时返回“TRUE”。IsDebuggerPresent函数的汇编代码如图1.1所示。图1.1

不难发现,该函数实际上是从程序的PEB信息中取出PEB的第3个字节。PEB的数据结构如图1.2所示。图1.2

可以看到,在PEB结构中,第3个字节正是成员BeingDebugged,也就是说,当进程处于调试状态时,系统会将该字节设定为1,示例代码如图1.3所示。图1.31.1.2 数据检测

数据检测是指程序通过测试一些与调试相关的关键位置的数据来判断是否处于调试状态。例如,在1.1.1节的函数检测中,我们了解到PEB的第3个字节表示进程是否处于调试状态,数据检测就是在程序中由程序自身直接定位到这些数据地址并测试其中的数值,这样就避免了调用函数(调用函数是非常引人注目的,也容易被Hook)。但是,使用这种数据检测方式需要处理很多平台之间的兼容性问题,如果选取的测试数据的位置会根据平台的变化而变化,那就很麻烦。这种方式也会带来好处,例如将检测代码放到虚拟机中就会很隐蔽。我们可以构建检测代码,如图1.4所示。图1.41.1.3 符号检测

符号检测是一种具有针对性的检测,主要针对一些使用了驱动的调试器或者监视器,如SOFTICE、TRW、SYSDEBUGGER、FILEMON、PROCESSEXPLORER等。这些调试器在启动后会创建相应的驱动链接符号,以用于应用层与其驱动的通信。但是,因为其创建的符号一般情况下比较固定,所以符号检测就通过测试这些符号的名称来确定是否存在相应的调试软件。例如,我们经常在调试CreateFile时看到的类似“\\.\SoftICE”的符号名称,就表示正在检测调试器,示例代码如图1.5所示。图1.51.1.4 窗口检测

窗口检测通过检测当前桌面中是否存在特定的调试器窗口来判断是否存在调试器,一般利用FindWindow等函数来查找相关窗口。这种方式现在已经很少使用,因为它有很多缺点,如窗口名称和类名很容易改变、只能通过这种方式检测、只能检测到是否存在调试器窗口、不能检测到调试器是否正在调试该程序,示例代码如图1.6所示。图1.61.1.5 特征码检测

特征码检测枚举当前所有正在运行的进程,并在进程的内存空间中搜索特定调试器的代码片段。定位OllyDbg调试器的特征代码,如图1.7所示。图1.7

选取一段具有明显的OllyDbg特征的数据,示例如下,构建如图1.8所示的检测代码。图1.80x41, 0x00, 0x62, 0x00, 0x6F, 0x00, 0x75, 0x00, 0x74, 0x00,0x20, 0x00, 0x4F, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x79, 0x00,0x44, 0x00, 0x62, 0x00, 0x67, 0x00, 0x00, 0x00, 0x4F, 0x00,0x4B, 0x00, 0x00, 0x001.1.6 行为检测

行为检测是指在程序中通过代码感知程序处于调试时与未处于调试时的各种差异来判断程序是否处于调试状态。举个例子,我们调试程序时步过2条指令所花费的时间会远远超过CPU连续执行这2条指令所花费的时间。可以通过rdtsc指令构建如下代码。当我们单步到00401006处且有所停留时,0040100A处会有感知。00401004 0F31 rdtsc00401006 89D1 movecx, edx00401008 0F31 rdtsc0040100A 29CA subedx, ecx0040100C 83FA02 cmpedx,20040100F 7701 jashort 00401012

有些程序会检测程序运行时到达入口的是否只有1个线程,有些程序会从驱动层判断是否有来自保护程序的中断等。行为检测方式多种多样,无法在这里尽述了。1.1.7 断点检测

断点检测功能根据调试器设置断点的技术原理来检测软件代码中是否设置了断点,其实也是一种行为检测。由于调试器设置断点时有明显的特征,因此这里单独讨论。

在调试器中,一般使用两种方式设置代码断点:一种是通过修改代码指令为int3触发软件异常;另外一种是通过硬件调试寄存器设定硬件断点。针对不同的断点设置类型有不同的侦测办法,一般针对软件断点,保护系统会试图分析比较重要的代码区域,然后检测指令是否存在设计之外的int3指令。一般的程序中除了异常捕获外很少使用int3,如果这些int3在函数入口处,则不太可能是编译器生成的,如图1.9所示。图1.9

图1.9中的代码不可能由编译器编译生成。在函数入口处一般都是跨模块处理的,根据异常处理机制,在设置异常处理程序之前,模块外的异常处理程序很难处理其他模块的异常,因此这段代码一旦执行必定引起程序异常。而调试程序时,在函数入口设定断点是非常正常的,所以,如果保护系统侦测到类似这样的代码,就可以判定存在调试器。

硬件断点的侦测要复杂一些。由于程序工作在保护模式下,无法访问硬件调试断点,因此在保护系统中,如果要侦测硬件断点,一般需要构建异常程序来获取dr系列寄存器的值。

断点侦测是一个非常有用的侦测手段,但是其应用非常困难,这主要是由其效率低、确定关键代码比较困难造成的。1.1.8 功能破坏

所谓功能破坏是指通过某种技术手段,在保证被保护程序能够正常执行的情况下,将系统原本提供的与调试相关的功能破坏,从而使调试器无法正常工作。因为在大多数的程序中都不会使用系统所提供的调试功能,所以保护系统有时会充分利用这一点。

NtSetInformationThread是系统提供的一个设置线程属性的函数,这个函数的原型如下。NTSTATUS NTAPI NtSetInformationThread( IN HANDLE hThread, IN THREADINFOCLASS ThreadInfoClass, IN PVOID ThreadInfoBuffer, IN ULONG ThreadInfoBufferLength );

在这个函数的参数ThreadInfoClass中指定了许多与线程相关的属性,其中有一项是ThreadHideFromDebugger。这一属性可以对调试器隐藏被设置线程的异常,也就是在系统内核过滤异常信息的时候,不会将设置了此标记的线程引发的异常传递给调试器。一旦保护系统设定此标记,那么调试器在调试被保护的程序时就无法正常接收线程的异常,调试器将无法进行正常的调试工作。我们可以构建如图1.10所示的代码来破坏调试器下软件断点的能力。图1.10

在测试上面的程序时我们发现,尽管用调试器启动了程序,但最后一个DebugBreak()函数还是会引发程序的异常,而且调试器无法捕获该异常。

还有一些功能模块会造成系统级别的破坏。例如,很多有内核驱动的保护程序总是将被保护程序的进程“武装”起来,使调用WriteProcessMemory等函数的操作失败,这也属于功能破坏,在这里我们只需要了解这种概念就可以了。1.1.9 行为占用

行为占用是指在需要保护的程序中,程序自身将一些只能同时有1个实例的功能占为己用。例如,一般情况下,一个进程只能同时被1个调试器调试,那么保护系统就可以设计这样一种模式:以调试方式启动被保护的程序,然后利用系统的调试机制使被保护的程序运行或者进行其他加密与解密操作,这样,由于保护系统占用了程序的调试接口,就无法同时使用其他调试器调试被保护的程序了。这种方式对于反调试来说,如果利用得好,会有非常好的效果。它不仅阻碍了其他调试器启动和调试被保护的程序,还阻碍了调试器以附加调试的方式附加到进程上进行调试。这种技术在以前有不少保护系统使用,一般称为双进程保护。1.2 反虚拟机

虚拟机是一种特殊的调试利器,是目前分析和调试程序不可或缺的利器。在桌面虚拟机中,最有名的当属VMware和VirtualBox。虚拟机拥有一种特殊的能力,就是对真实系统环境的模拟,这样可以使软件无法正确判断当前的环境。例如,一款程序是针对计算机硬件序列号实行一机一码授权的,如果这款软件对其在虚拟机中运行时获取的信息进行了授权,那么由于虚拟机环境是可以复制的,所以程序的一机一码授权能力从某种程度上就失效了。另外,虚拟机对于分析者对程序的分析也是相当有帮助的。正确使用虚拟机的快照等功能,会使复杂的分析大大简化。所以,为了避免上述情况出现,好的软件保护系统将检测并禁止被保护的软件在虚拟机中运行,这就需要有能够识别虚拟机环境的技术,即反虚拟机技术。

由于虚拟机模拟的是整个计算机环境,所以检测虚拟机的技术会比反调试技术更加困难。在这里,我们只介绍其中通用性比较高的一种——虚拟机BIOS检测。

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

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载