查看原文
其他

逆向 | C++ 加壳程序的编写思路

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:460500587



微信公众号:计算机与网络安全

ID:Computer-network

的编写一直以来都被认为是软件安全领域中较为高端的技术,但是实际上编写一个自己的并不是多么困难的事情,只要你了解PE结构并会写代码,那么写一个的过程其实就会变成一系列重复的数据处理工作。


但是为什么非要自己编写这个看似毫无创意的工具呢?那是因为这会给你带来非常巨大的帮助。具体总结如下:


壳的原理与蠕虫病毒感染的原理基本一致,你可以在这个过程中学到很多东西。


最重要的一点,你可以将自己的技术附加到壳上,使其某方面具有与你同等的能力。


很多职业黑客之所以写自己的壳就是看中第二点,比如黑客中流传的免杀


其实除此之外,一个完全由自己实现的、未公开的还有很多其他优势,比如你可以用这个去保护自己发布的软件产品,由于这个是未公开的,因此即便是你写的保护方案并没有多么的精妙,它的相对保护能力依然会大大超过市面上的其他主流加密。这些等各位朋友拥有自己的后自然会体会到。


一、壳的运行流程


为了使得我们的讲解过程简洁易懂,对将要编写的做了很多精简,只保留构成一个的最基本元素,因此以下讨论的都是一个最简单的的流程。


如果要用一句话来高度概括的运行过程,那么就是一个能将宿主程序加密,并能将加密后的程序注入一段代码,从而使其能自动解密的程序行为。


在准备加壳之前,加壳程序会读取宿主程序的基本信息,并根据这些信息与使用者的设定来对代码段、数据段及资源段进行有选择的加密压缩操作。


然后,加壳程序会在相关程序中添加一个足够大的空区段,并将负责解密解压的代码(一般将其称为Stub部分或Shell部分)与相关配置信息写入这个新添加的空区段中。


最后,修改宿主程序的入口点信息,使宿主程序在运行时先执行Stub部分的代码,并修改新区段中的配置信息,使Stub部分的代码在完成工作后会跳转到真正的宿主程序入口点处,执行宿主程序。


至此,整个的执行流程就完成了,被加密后的宿主程序就可以顺利运行了,整个过程如图1所示。

图1  加壳前与加壳后程序的状态

二、设计一个纯C++编写的壳


壳究竟是应该用汇编编写还是应该用C/C++编写呢?这个看似老生常谈的话题,目前却仅有个别技术文献对其进行过深入的探讨。


目前世界上已知的用纯C++编写的壳应该还是比较罕见的,唯一的C++开源壳Bambam还存在着若干技术上、设计上的问题。


那么我们要如何设计一款由C++编写的壳呢?


1、用C++编写的壳应该是什么样的


很多没编写过壳甚至编写过壳的人都没仔细想过这个问题。我们拿最典型的Bambam来讲,它在Stub部分使用了大量的数组用以保存字符串,并使用了若干技巧回避了可能产生字符串与全局变量的代码。


要用C++编写壳,应该着重考虑的是可扩展性、可维护性,我们希望在Stub中编写代码时能像编写普通程序一样简单方便,可以随时使用任何运行时库、任何系统API、任何编程语言机制。如果我们做不到以上各点,如果我们在写Stub部分的代码时仍然感觉碍手碍脚,那么就不如用汇编去完成它了。


就目前来说,用C++做一个普通的壳完全可以做到零汇编代码使用量的境界(包括Stub部分)。


一个被设计到极致的C++壳其实就是一个PE融合程序。如果要将任意两个PE文件完全合二为一,会涉及程序的引导、资源段的Hook、数据段的融合以及各个PE结构的融合(包括输入表、输出表、重定位表)等。


总而言之,PE融合操作就是一种将两个PE文件完全拆碎,然后再将其组装为一个新PE文件的操作,整个过程涉及的若干操作非常复杂,且技术价值前景不明朗,因此不建议您在此上花费过多的时间。


2、编写过程中会遇到的问题


说到用C++写壳,其实前期真正有挑战的还是Stub部分,因为这部分代码要能寄生在任意可执行文件中,而且还要能正常运行才可以。这就带来了以下两个最初级的问题:


怎样才能将一个程序寄生在其他程序中,并且能正常执行?

这个寄生程序应该是什么样子的?


如果要想将一个程序寄生在其他程序中,并能同时兼容EXE与DLL两种格式的文件,那么首先要针对这两种不同的情况分别准备一小段非常简单的引导代码,这段代码可能只有5行左右,它唯一的任务就是引导寄生在宿主程序中的Stub代码(程序)正确执行。


然而什么样的Stub程序才能在这种情况下正确执行呢?我们知道,要想将程序中的代码注入其他程序中执行,首先应该考虑的就是代码重定位的问题,因此我们需要这个Stub程序一定要拥有重定位表,否则无法执行重定位操作。由此可推论Stub程序以DLL的形式存在最为合理。


其次,为了让我们在写Stub代码时能够不受约束,因此必须要对宿主程序以及加壳后的程序做一些处理,以使得我们在Stub中的代码能访问到自身的全区变量、字符串与相应的API等,由此便产生了两个稍复杂一些的问题。


要如何做才能将Stub程序的代码与全局变量很好地寄生到其他程序中?


Stub程序中不可避免地会用到其他程序未导入的API,如果其他程序中没有我们需要的某个API应该怎么办?


以上两个问题显得稍有些复杂,其实解决这两个问题并不难。


对于第一个问题,可以通过修改编译选项编译出较小体积的Stub程序,并将代码段与数据段合并在一起,这样就可以将Stub程序中的代码段与数据段直接复制到宿主程序中,从而避免了很多麻烦。


而对于第二个问题,我们可以使用动态获取API地址的方法来避免直接与PE文件中的导入表产生纠葛。当然,最完美的方法是能将宿主程序的导入表与Stub程序的导入表相融合。不过由于这个操作过于复杂,因此并不适合我们在现阶段研究。


最后,就剩下交互配置问题了,也就是要怎样做才能很好地让已经寄生在其他程序中的Stub代码能很方便地读取我们所提供的配置信息。


这个配置信息要满足两个要求,一个是Stub程序中的代码必须要能非常方便地访问到它,另一个就是我们的加壳程序的配置端也要能比较方便地对其进行读写操作。


在这里我提供的方案是通过Stub程序中一个导出的全局变量来完成这些工作。这个全局变量指向包含着所有配置信息的结构体,这样做有以下两个好处:


作为Stub程序来说,其本身访问自己的全局变量是非常简单且容易的事情,这符合我们尽量简化Stub程序复杂度的中心思想。


作为加壳程序的配置端来说,通过读取解析Stub程序的导出符号并不会增加多少工作量,而且这种与Stub向通信的方法还显得非常明朗。


当然,加壳程序的配置端与Stub部分交互数据的方案有很多种,大家如果有更好的解决方案也可以使用自己的方法。

三、用C++写一个简单的壳


为了展示一个用纯C++构成的壳的框架,我们将精简掉壳的大部分功能,只实现一个加密宿主程序代码段的壳。这个壳仅支持EXE格式的文件,它使用简单的异或算法将宿主程序的代码段加密。为了增强演示效果,加壳后的文件在执行时会有选择地弹出一个对话框显示解密信息,并在解密完成后让用户选择是否执行宿主程序。


这个看似简单的壳能为我们打一个扎实的框架根基,有了这个壳作为基础,以后添加资源处理、压缩、加密输入表与TLS处理等功能时就会觉得得心应手。


概括地讲,我们的壳要做以下这些操作,如图2所示。

图2  壳的执行流程

通过图2的描述,我们已经对将要编写的壳有了一个比较清晰的认识。需要注意的是,其实在“植入Stub”执行完毕后就已经完成所有加壳操作了,最后的“Stub执行”指的是被加壳程序在以后每次执行前需要做的事,也就是stub部分的功能。


1、配置工程


在配置工程时首先要做的就是确定整个程序的结构,及考虑一些必要的细节问题。例如我们的新项目要取一个好名字,而且还要思考怎样设计界面与逻辑部分,并且要考虑一些会影响Stub体积的细节问题。


首先,关于项目名称,将其定为A1Pack Base,这个名称的含义很明确,大致可以理解为一个由A1Pass编写的基础版加壳程序。


其次,关于整个项目的组织方式,打算将所有执行加壳操作的逻辑部分放到一个DLL中,这个DLL导出几个我们自定义的API,然后由界面部分调用这些自定义的API完成加壳操作。这样做的好处很明显,一是加壳的逻辑部分与界面部分互不干扰,界面部分可以随意替换;二是更有利于初学者理解整个程序的思路,因为与加壳逻辑无关的所有代码全部都被剥离开来,DLL中的所有代码都是与加壳操作直接相关的。


最后需要考虑的就是一些细节问题了,也就是Stub的编译选项问题。前面我们已经讨论过Stub程序的最优存在方式是DLL,Visual Studio 编译器编译出的DLL一般有5个区段,依次是.text、.data、.rdata、.rsrc与.reloc。经过测试,这5个区段中的.text、.data与.rdata可以合并,归并为新的.text段,而剩下的.rsrc与.reloc通过名称我们可以推测它们分别为资源段与重定位段,在实际操作时这两个段只起到辅助作用,其体积并不会对加壳后的程序产生影响,因此无须额外处理。


现在我们可以着手配置工程了。先建立一个名为A1Pack Base的MFC对话框项目,用以完成程序的界面部分;然后再添加一个名称为A1Pack_Base的DLL项目,用以完成程序的加壳逻辑部分;最后再添加一个名称为Stub的DLL项目,用以完成壳的Stub部分。整个项目建立完成后的项目结构如图3所示。

图3  配置完成后的项目结构

最后,在Stub项目中的dllmain.cpp文件前加入以下代码,以控制此项目在编译时的连接选项,从而使其生成我们所需要的程序。

以上代码的前两行的意思是将.data、.rdata这两个区段合并到.text区段中,第三行的意思是修改.text区段的属性为可读、可写、可执行。


目前为止,对于整个项目的必要配置已经完成了。其实还可以让它变得更好一些,例如把Stub工程生成的DLL文件以资源的方式保存在A1Pack_Base的DLL项目中,这样当我们的工程最终完工并生成后,就只会有A1Pack Base.exe与A1Pack_Base.dll这两个文件,而Stub.dll则会以资源的形式保存在A1Pack_Base.dll中。这要怎样配置呢?


首先将Stub这个工程以Release的方式编译一下,这样它就会在工程的Release目录下生成一个名为Stub.dll的文件。


然后在Visual Studio的资源选项卡中选中A1Pack_Base项目,并右击,在弹出的菜单中选择“添加”→“资源”,如图4所示。

图4  A1Pack_Base项目添加资源

在接下来弹出的对话框中单击“导入”按钮,这时会弹出一个文件选择对话框,在其中的文件过滤下拉框中选择“所有文件(*.*)”,然后找到我们刚刚生成的Stub.dll,选择并添加。


最后,在新弹出的资源类型对话框中输入STUB,单击“确定”按钮后,就成功添加了一个资源类型为"STUB"的二进制资源信息,如图5所示。

图5  成功将Stub.dll添加为二进制资源

这样一来,在稍后的项目中我们就可以通过读取资源来访问Stub.dll文件的所有内容了。为了保证以后我们每次编译A1Pack_Base项目时都会读取最新的Release版Stub.dll文件到资源中,我们需要设置一下。


如果你的项目文件目录发生了变化,那么就需要重新指定Stub.dll的路径,否则会导致项目生成失败。


2、编写Stub部分


为什么要先编写Stub部分?这样做主要有以下两个原因:第一,壳的最重要的功能核心部分就是Stub部分;第二,加壳程序A1Pack_Base就是为Stub部分服务而存在的,就像界面项目A1Pack Base的存在就是为A1Pack_Base而服务的道理一样。


在设计Stub部分的时候,要考虑好以下几个问题。


1)问题一:Stub部分的正常执行需要一些必要的信息及参数,要根据Stub部分将要完成的功能,为其准备好运行时用到的具体数据。


2)问题二:Stub部分毕竟是以一个DLL格式文件的形式存在的,但我们知道DLL格式文件像EXE格式文件一样,在执行我们的代码前会先执行很多引导代码,这些几乎完全不受我们控制的代码势必会对处于寄生状态的Stub部分产生很多不利影响。如果想要Stub部分能健壮稳定地运行,就必须解决这个问题。


3)问题三:由于我们的程序到时会丢弃掉自己的IAT与导入表信息,因此直接调用API肯定是不可行的,这就要求我们有一套能自动获取API函数地址的替代方案。


下面我们将会针对这些问题进行一一讲解。


(1)问题一的解决方案


由于目前我们的壳并不复杂,因此需要的信息也不多,只需要宿主程序OEP、控制是否显示解密完成消息框的标志位与必备的加解密区域信息。


因此,我们在Stub中定义了如下结构体:

通过以上代码可知,我们定义了一个名为GLOBAL_PARAM与PGLOBAL_PARAM的结构体类型,并用GLOBAL_PARAM类型定义了一个被导出的全局变量g_stcParam。


(2)问题二的解决方案


有关引导代码的问题其实很容易解决,可以通过修改编译选项来自定义一个入口函数,这样程序在编译时就会忽略那些引导代码。


我们在Stub项目的dllmain.cpp中添加如下代码,以指定自定义的入口函数。

(3)问题三的解决方案


对于第三个问题,可以使用动态加载API的方法解决。但是如果使用动态加载API技术,就会碰到一个由此衍生出来的新问题—如何获取最基础的GetProcAddress函数的地址。


我们应该能想到,当Stub程序被寄生到其他程序中后,由于其丢弃了自己包含所有导入API信息的导入表,因此调用任何API时都会导致程序崩溃,这其中当然包含我们所急需的GetProcAddress函数。那么该怎么办呢?


我们知道,GetProcAddress函数是从系统文件kernel32.dll中导出的,而kernel32.dll又是系统的基础链接库,所有运行的程序都会加载此动态链接库。试想一下,如果能找到kernel32.dll的加载基址,那么通过分析它的导出表就一定能找到GetProcAddress函数的地址。


获取kernel32.dll加载基址的公开方法目前有3种:一种是通过特征匹配的暴力搜索;另一种是利用系统的SEH机制找到kernel32.dll并搜索出加载基址;最后一种则是通过线程环境块TEB的信息逐步找到kernel32.dll的加载基址。在以上3种方法中,代码量最少的就是最后一种方法,这也是我们将要介绍的方法。应用这种方法的具体思路如下:


1)通过段选择字FS在内存中得到当前的线程环境块TEB的地址;


2)TEB偏移为0x30处是指向进程环境块PEB的指针;


3)PEB偏移为0x0C处是指向PEB_LDR_DATA结构[注释]的指针;


4)PEB_LDR_DATA偏移为0x1C处是模块初始化链表的头指针InInitializationOrderMo duleList;


5)InInitializationOrderModuleList中按顺序存放着此进程初始化模块的信息,在NT 5.x内核中,第一个节点存放的是ntdll.dll的基址;第二个节点存放的是kernel32.dll的基址;NT 6.1内核中,其第二个节点存放的是KernelBase.dll的基址(KernelBase.dll中包含着kernel32.dll中绝大多数常用API的另一份实现,其中就包含GetProcAddress函数)。


整个获取逻辑的流程如图6所示。

图6  获取kernel32.dll基址的流程

以下是获取kernel32.dll基址的代码。

到此为止,我们已经得到了kernel32.dll的基址(或与其等价的KernelBase.dll的基址),接下来要做的就是寻找其中导出的GetProcAddress函数地址了。


找出GetProcAddress函数地址的整个过程其实就是简单地对PE文件的导出表进行遍历查找操作,如果你对PE文件有一定了解的话,那么整个过程其实还是比较简单的。


首先需要根据kernel32.dll的基址找到其导出表的起始位置,并读出导出表的若干关键信息;然后对整个导出表进行循环遍历,直到找到函数名与GetProcAddress相匹配的导出项,根据此导出项的标号找到对应的函数相对偏移地址,用此地址加上kernel32.dll的基址就可以得到目前GetProcAddress函数所在内存的地址了。具体如代码清单1所示。

代码清单1  获取GetProcAddress函数地址的代码

以上3个问题解决后,Stub部分基本就可以很轻松地完成了。当然,还需要处理一些细节问题,例如获取必要的API地址与解密宿主程序代码段等,这些都是非常简单的,感兴趣的朋友可以查看随书附带的源代码,这里对源码进行了非常详细的注释,尽可能使其适合初学者阅读。


另外,在获取必要的API地址时,加载DLL尽量使用LoadLibraryEx函数,因为KernelBase.dll中没有导出LoadLibrary函数,所以使用LoadLibraryEx函数会增加壳的兼容性。


3、编写加壳部分


凡事都有两面性,既然我们的设计方案使得Stub部分变得非常简便可控,那么加壳部分就难免要稍微复杂一些了,毕竟加壳操作的流程与要求基本不会有太大变化,因此即便是再好的设计方案,一些必要的步骤与流程总是要有代码去执行的。


与普通的加壳程序相比,我们的加壳程序需要多做两件事,一个是要实现独特的参数写入方式,另一个就是需要对Stub的代码段进行重定位操作。这两件事情都不是太复杂。


在开始之前建议大家先看看前面的图2,对其大体流程建立一个整体的印象。我们上面所讲的Stub部分的编写就是在完成整个流程的第三部分,而此处所讨论的加壳部分的编写则是完成前两部分。


记得我们在前面讲过,加壳部分其实就是为Stub部分提供支撑与服务的,那么,如果想要将Stub顺利地植入其他程序并顺利运行,都需要做些什么呢?


下面我就按照加壳程序的逻辑流程为大家一一讲解我们将要做的事情。


(1)设计加壳部分


我们在建立项目时将作为加壳部分的A1Pack_Base建立成了一个DLL工程,为了方便外界调用,我们需要导出一个或多个函数。鉴于目前壳的功能还很简单,因此导出一个函数足矣,大致代码如下所示:

通过以上代码可知,我们导出了一个名为A1Pack_Base()的函数,此函数的两个参数分别为需要加壳的文件的路径及加壳后的程序是否弹出提示解密成功的对话框。


除此之外,由于加壳程序涉及大量的PE操作,因此我们有必要为此建立一个PE操作类。本例中的类名为CProcessingPE。


(2)读取目标文件相关信息


在A1Pack_Base函数内,首先要做的事情就是读取要加壳文件的各种信息,并将其映射到内存中。在这里我们要考虑一个细节问题,也就是加壳后被加壳的文件不能产生损毁,因此需要在加壳前备份源文件,或只读取源文件,将加壳后的程序保存在另一个文件里。


但是除此之外,我们还需要获取这个文件的关键PE信息,以便于在以后的操作处理中使用。获取目标文件关键PE信息大致如代码清单2所示。

代码清单2  获取目标文件关键PE信息

(3)读取代码段信息并处理


读取代码段信息的第一步是确定哪个区段是代码段。一般来讲,可读但不可写的区段为代码段,除此之外,在PE的选项头中也有对程序代码段的相关描述。


获取代码段起始与结束地址的代码如下:

获取了代码段的起始与结束地址,我们就可以对其进行加密了,具体代码如下:

通过以上代码可知,我们除了再将代码段的数据加密之外,还给代码段附加上了可写属性,这样做的目的是方便Stub部分的解密操作。因为Stub在植入宿主程序后需要对其代码段进行写操作(解密),因此如果代码段是不可写的,那么将导致程序崩溃。


(4)根据Stub体积添加新区段


到目前为止,我们已经做完了对原文件的预处理工作,接下来就要考虑Stub的植入了。植入Stub的第一个前提就是要在目标文件中开辟出一块空间来保存Stub,本例中使用添加区段的方式来开辟新的空间。


想要开辟一块空间容纳Stub,首先要做的是计算Stub的体积是多大,由于Stub部分已经被我们以资源的方式嵌入A1Pack_Base项目中了,因此需要通过读取资源的方式得到其详细信息,大致代码如下:

接下来,我们就可以根据以上代码获取的信息添加一个指定大小的区段了。添加区段的功能大致如代码清单3所示。

代码清单3  添加区段函数的代码

以上代码非常精简地展现了一个添加区段的基本流程,并且可以为绝大多数的PE文件添加区段,但是由于并没有考虑目标文件带有附加数据等细节问题,因此还不算完美。


(5)对Stub进行重定位处理并写入配置信息


如果想要Stub程序在宿主程序上执行,那么在植入前就必须要对其做重定位操作,不过普通的重定位逻辑并不能满足这个需求。


我们都知道,系统的重定位操作修复的只是基址部分,但是在我们将Stub移动到新的区段中后,其代码与数据所在的文件偏移与虚拟偏移全部都会发生巨大的变化,因此可以按照以下公式来计算新的加载地址,从而修复这个问题。

重定位的相关操作如代码清单4所示。

代码清单4  执行重定位操作的代码

在使用以上代码时需要注意,3.2.3步在修复重定位的时候并未考虑修复类型问题,如果要提高兼容性,应该对3种重定位类型进行区别对待。


对Stub的代码进行重定位修复后,就剩下将关键信息、配置信息等写入Stub的全局变量g_stcParam中了,我们获取到已导出的g_stcParam的地址,并向其地址中写入我们已经组织好的结构体即可。以下就是获取g_stcParam地址的关键代码:

以上代码的作用就是根据传入的导出名称找到其现在在内存中的地址,以方便访问修改。代码中多次用到的RVAToOffset函数是自定义函数。


(6)将Stub复制到新区段中


执行到这一步,目标文件已经处理完毕,Stub也整装待发了,只需要最后将Stub的.text段内容复制到新区段中,并将目标文件的OEP指向Stub的入口处就可以了。


但是在最后计算OEP的过程中,要注意Stub的PE文件中所保存的OEP不能直接拿来用,因为Stub的代码段被移动了位置,其RVA已经发生了变化。因此将这些因素考虑进去后,我们可以得出以下公式:

4、编写界面部分


由于界面部分涉及对A1Pack_Base.dll的调用,下面给出一个实例代码,大致如下:

到此为止,我们应该已经拥有了自己的第一个壳了,而且整个壳无论是从加壳部分还是从Stub部分来看都是用C++编写的。A1Pack Base的运行效果如图7所示。

图7  A1Pack Base的运行效果

经过测试,我们这个壳的兼容性还是很不错的,从简单的控制台程序到复杂的OllyDbg主程序都可以加壳成功,且运行稳定,除此之外我们的这个壳还支持多层加壳操作。


由此可见,使用这种方法编写的壳其稳定性还是非常不错的。虽然我们的壳功能单一,但是为了便于您理解代码,很多能提高兼容性与返回值判断等代码都是能省则省,因此综合而论还是应该对用C++写壳的方式给予正面评价。


四、设计一个由C++编写的专业壳


我们虽然已经用C++实现了一个壳的雏形,但是其与成熟的加壳程序仍有一段距离,如果我们要设计一个具有商业标准的压缩壳,还需要考虑以下几点问题:


从功能上来讲,一个成熟的壳应该能加密压缩宿主程序的所有区段,且能同时支持EXE与DLL程序的加壳操作。除此之外,必要的STL处理也是不可或缺的。


从加密压缩的算法上来讲,我们不能再用简单的异或加密敷衍了事了,而是应该选择一个优秀的压缩算法,以使得被加壳保护的程序有一个较为明显的变化。


从健壮性来讲,我们需要重新考虑一下整个程序的容错及报错机制。


1、为问题找到答案


如果我们要想加密压缩所有区段,并使编好的壳能同时支持EXE与DLL,并能正确处理TLS,那么就要求我们必须要考虑好目标PE文件资源与其他数据的备份问题。


我们都知道,PE文件中的某些资源并非仅在运行时才会用到,例如图标资源与版本信息等资源,这些资源在我们通过资源管理器查看目标文件时就会被系统读取。因此如果想让加壳后的程序不至于太难看,那么就有必要将这些资源移动到新的地方保存,以供系统使用。


除此之外,TLS也是需要谨慎处理的数据,这部分内容会在宿主程序运行之前首先运行,而且从理论机制上来讲,它甚至应该在壳代码运行之前就执行的。很显然我们不能让它这么做,故我们必须提供一种机制来中转它,以使其能达到同样的效果。


下面我们就来着重讨论一下这两个问题。


(1)如何保存关键资源


就资源的移动与备份来说,目前共有3种方案。


将关键的资源数据提取出来,并根据这些已提取的资源信息自己生成一个资源头,然后将这个完全由自己生成的资源信息复制到Stub部分。目前笔者还没见过使用这种方案处理资源的壳。这样做的好处是自己可以对宿主程序的资源实施完全的控制,几乎等于自己实现了一个资源的Rebuild功能,异常强大,但缺点是此方法非常复杂且不好操作,从目前市面上仅有的两款资源Rebuild工具就可以看出一二。


保留原资源段,但是同样需要将关键的资源数据提取出来,并将其保存在Stub部分,然后修复原资源头使其指向这些关键数据所在的新位置。看雪论坛hying的PE-Armor就是使用的这种方法,这样做的好处是简单有效,不足之处是这个资源段将宿主程序硬性“分割”了,因为我们在压缩宿主程序时不能压缩这个资源头,因此这就要求我们将宿主程序分为两部分来压缩,即资源头前面是一部分,资源头后面是另一部分。


同时移动资源源头与关键资源数据到Stub部分,使得处于新位置上的资源头同时指向所有资源数据,然后删除原资源头。ASPack等著名加壳软件都使用了这种方案,这样做的好处是既保证了简单有效,又保证了生成程序的体积(因为第一方案要同时存在两个资源头才能正常工作)。


因此,毫无疑问我们最好选择第三种方案。这种方案的关键操作有3点:第一个是如何取得资源头部的大小;第二个是如何取得关键的资源数据;第三个是如何修复移动位置之后的资源头。


以上3个操作中,前两个可以合二为一。因为我们已经知道资源头的起始地址,因此只要计算出它的结束地址就可以计算出它的体积。又因为资源头的结束地址是资源数据的开始地址,因此只需要遍历所有资源数据,取得其中地址最小的一个就可以了。而在遍历所有的资源数据过程中,我们就可以顺便把我们需要的数据保存下来。


(2)如何处理TLS机制


不管用何种语言写壳,在设计专业壳的过程中,碰到的第一个棘手的问题都是对TLS的处理。由于使用Delphi编写的程序对TLS有天生的依赖,所以如果我们的壳无法处理TLS,就代表我们放弃了对所有Delphi程序的支持。


当然,如果仅仅停留在对Delphi程序的支持上还是非常简单的,我们只需要在加壳时简单地备份并移动一下宿主程序的TLS结构即可解决这个问题。


但是这种处理还远远达不到“有效”的标准,如果宿主程序使用了TLS空间或对TLS回调函数有依赖,那么简单的备份处理就不能应付了。此时就需要我们从TLS的原理入手,来设计一个TLS的替换方案,使得我们能实现对TLS机制的有效支持。


简单地说,TLS的运行主要依赖于保存线程局部存储数据的内存地址和保存有TLS回调函数的指针数组。因此如果想要实现替换TLS机制,只要处理好这两个即可。但是之前需要先让壳的Stub部分本身拥有TLS机制,因为只有这样才能使用Stub的TLS将系统对TLS的处理接管过来。


我们可以在Stub部分加入以下代码以实现对于TLS的最基本支持。

以上代码从逻辑上讲毫无意义,它的唯一作用就是使Stub部分具有TLS特性。


现在我们需要思考的问题就是怎样将Stub部分的这个毫无意义的TLS替换成待加壳宿主程序的有意义的TLS。在这个过程中,首先要处理的就是数据问题。


目前市面上唯一的C++开源壳bambam对此的处理方式是模拟系统对TLS数据的装载过程,它首先会获取目标映像中TLS的索引值,并使用此索引值在段寄存器FS:[0x2C]中当前所有进程的TLS内存列表地址里找到当前进程的TLS数据区指针,然后再模拟系统的TLS数据复制与清零操作,最后读取宿主程序的TLS回调函数地址,并逐个调用(每个TLS回调函数仅在此被调用一次)。


虽然这种方法看起来“很有技术含量”,但是这却是一个伪解决方案。当然,相对于简单的TLS结构复制移动来说,bambam的这种处理方式或许兼容性更好一些,因为大多数使用TLS机制的程序都是用其做一些非常规操作(例如反调试等),且这些操作大多数只需执行一次即可。但是毕竟不是所有程序都是如此,例如在需要多线程并行操作的技术领域,TLS机制还是会被经常用到的,因为它对简化多线程程序的数据结构有很大帮助。这就意味着这些程序的TLS回调函数会被调用不止一次,bambam的那种方案显然就不能满足这种需求了。


另外,bambam只对当前壳正在运行的主线程TLS数据区进行必要的数据复制与清零操作,但是却无法对其他线程执行此初始化操作,这会直接导致其他用到TLS机制的线程运行异常。解决这个问题的方法是非常简单的,只需在加壳时使用宿主程序的TLS数据覆盖Stub部分的TLS数据(因为系统最终用到的是Stub部分的TLS结构),并在Stub跳到宿主程序OEP之前将宿主程序的TLS回调函数数组复制到Stub的TLS回调函数数组中,并将Stub部分的回调函数替换即可。


这样只需简单的两步,我们就已经完美地处理完TLS所有相关的内容了。上述过程大致的实现代码如代码清单5所示。

代码清单5  Stub中的替换TLS回调函数

以上代码的逻辑非常简单,它首先将目标文件的TLS回调函数指针与Stub部分的回调函数指针取出来,然后再用目标文件的TLS回调函数一一替换Stub的,并且在替换之前先模仿系统向目标文件的TLS回调函数发送一个进程创建的调用请求。


2、设计专业壳的框架


专业壳的框架基本上可用简单壳的框架为蓝本进行设计,但是很显然,简单壳的框架并不足以支撑专业壳的若干特性,因此需要对其做一些必要的改动与增强。


就专业壳来说,我们主要需要考虑以下几点:


由于需要同时支持EXE与DLL,因此便涉及DLL的重入问题。我们都知道,系统在创建进程与线程的时候都会调用DLL的入口,但加壳后DLL的OEP却是我们壳的入口点,如果未对其做良好的处理,那么就会导致Stub部分的代码被重复执行,这肯定会引发问题。


如果要设计专业壳,Stub部分的要求就会有明显的提高,因为一些高级语言的特性(如清空结构体与new操作等)会默认调用一些库的API,这就要求我们做出选择,是选择避免使用这些特性,还是选择对Stub部分提供一个完美的API支持解决方案。


专业壳的大概加壳操作流程如下:


1)对于DLL重入问题的处理有两个选择,一个是在壳运行时将宿主程序的OEP改回原来的值,这样当系统再次调用此DLL时就不会经过Stub部分,而是直接调用宿主程序的入口点。除此之外,也可以在壳的入口点设置一个判断标志位,如果Stub已经执行过就直接跳转到宿主程序的入口点。


2)完美解决对API的支持问题也不难,只需要让宿主程序数据目录表的导入目录指向Stub的导入表即可,这样一来整个加壳后程序中唯一有效的就是Stub部分的导入表,因此系统在加载整个程序时就会将Stub部分的IAT自动修复,而我们要做的就是在Stub部分中写一个函数修复宿主程序的IAT。如此一来Stub部分将可以像正常程序一样使用系统API(我们可以由此发散思维,使用同样的方式处理Stub的重定位问题)。


3)关于一个专业壳的流程问题,每个人都应该都有自己的想法,但就使用C++写的壳来说,有一些地方需要注意,如图8所示就是设计的一个C++壳的基本流程。

图8  用C++写的专业壳的基本流程

遵循以上流程,加壳后的目标程序有3个区段,其组成结构如图9所示。

图9  加壳后程序的数据组成结构

3、如何设计Stub部分


从整体来讲,专业壳的Stub部分变得更加简单了,因为它既不需要获取kernel32.dll的基址,也不用初始化所需要的API。现在要在Stub中做的唯一一件事就是逻辑部分的处理了。


经过前面的介绍,大家应该对Stub部分所作的逻辑处理部分也有了比较清晰的认识,那我们还需要设计些什么呢?答案是什么都不需要了,因为Stub部分就是这么简单!但是为了便于大家更清晰地了解整个Stub的设计思路,我在这里就再次总结一下。


总体来说,被加壳后的程序所使用的导入表、重定位表以及TLS表都是Stub部分的,这样当程序被系统加载时,Stub部分不需要任何额外处理就已经处于可以正常运行的状态了。


接下来只需要让Stub处理一下宿主程序的导入表、重定位表与一些其他的细节问题,即可保证宿主程序也能正常运行,至此Stub的使命就已经完成了。


4、如何设计加壳部分


对于加壳部分的设计,其主要的难点在于对宿主程序数据的逻辑处理问题,我们要时刻清楚需要提取什么、需要删除什么以及目的是什么,这就要求我们要对加壳的执行流程与逻辑有一个基本的概念。


我们经常会遇到的就是一种互相制约的状态,例如,如果你想压缩目标文件,就必须先将文件中的关键内容取出来,并放到合适的位置,但是这个位置只有在压缩完目标文件后才能计算出来,这看似陷入了一个死循环。


当遇到类似的问题时要冷静思考,且不要怕麻烦。就上面的情况来说,我们只需要将关键内容取出来后先暂时存放在一个地方即可,等到目标文件压缩完毕后再将其放到合适的位置上。


不过需要注意的是,这种情况在实际操作中并不常见,如果你在写壳的过程中屡次遇到类似的情况,那么你就要反思自己的整体逻辑是否出了问题。


我们以图8的流程为例,先讲讲预处理部分的第2步。有些人可能有疑问,为什么要在压缩前先将宿主程序按照区段信息映射到一个缓存中呢?难道就不能在Stub中做这件事吗?


首先,我们都清楚大多数的壳都会破坏宿主程序的区段信息,当然我们设计的壳应该也不会例外,这就要求我们自己实现对目标程序的内存映射操作,而完成这些操作的基本数据需求就是源程序的区段信息,这样我们就必须要保存源程序的区段信息,并在加壳时将其传递给Stub部分。如果我们将内存映射操作提前到加壳前进行,那么就免去了传递区段信息的麻烦。


其次,内存映射免不了需要宿主程序的一些其他PE关键信息,如果我们在Stub部分执行这个操作,那么这些信息就要逐一重新获取,而如果在加壳前做这个操作,由于前期的一些准备工作难免会用到这些关键信息,因此在执行内存映射时就可以直接就地取材了。


最后,直接压缩的宿主程序与压缩完成内存映射后的宿主程序并不会对压缩结果的体积产生影响,因为映射后的映像文件虽然总体积变大了数倍,但是实际数据并没有因此而增多,只是数据之间的0x00区域变大了而已。但是对一般的压缩算法来说,压缩10个0x00与压缩10万个0x00就压缩结果的体积来说并没有太大区别。


现在我们再说说预处理部分的第3步,有关于关键资源提取的逻辑我在前面已经讲过了,因此这里不再重复,我们现在要探讨的是一些逻辑问题与细节问题。


首先,之所以要在这一步中提取资源,目的就是在植入Stub的第2步中将这些资源数据随同资源头一起备份到Stub中,并修复资源头中的资源数据指针。


其次,在提取资源的过程中要时刻回想资源的3层目录结构。这部分在编码时不需要参考别人的已有代码,只需要对照PE的内容一点一点地自己编码即可,这样更利于我们快速解决资源数据遍历与提取问题。另外,在做这一步的时候还要注意,并不是所有资源都是用ID来索引的,我们需要兼顾用字符串(资源名)来索引资源的情况。


最后,在索引到关键资源后,要对其进行及时的备份,并将原数据删除,因为这部分数据我们以后永远都不会再用到了。删除后压缩有利于减小整个加壳后文件的体积。


除了这些以外,添加区段也是我们需要着重关注的。添加区段的操作还是比较简单的,只要我们不要搞混各种数值的计算,或在修改时不漏掉例如PE头中的SizeOfImage(映像大小)等关键信息就可以了。添加区段的操作如代码清单6所示。

代码清单6  添加区段操作

对于以上代码需要注意的是,某些变量如代表PE结构中NT头指针的pNtHeader与代表Dos头指针的pDosHeader等变量出于篇幅考虑,其来源并未给出。但是我们可以根据变量名简单地猜测其用途,这并不影响我们理解整段代码的意思。


理解了区段映射的缘由与资源处理的逻辑,并看懂了添加区段的代码,那么加壳部分的几个难点就已经被我们攻破了。


当然,除了以上内容,想要自己写一个专业的壳,开动脑筋设计并亲自动手调试是不可或缺的。


5、需要注意的细节问题


首先我们需要格外注意的就是各个区段中数据的RVA、VA及内存地址转换等问题。由于我们在操作PE文件时往往会将其先读取到内存中,因此在寻址的时候直接使用RVA或Offset都是不正确的,正确的方法应该是用Offset加上文件所在内存的起始地址。如果需查找的值是RVA,则要先将其转换为Offset再做操作。另外,由于Stub部分的代码在工作时目标程序肯定已经加载到内存中了,因此Stub中对PE文件的操作无须这些操作。


其次需要注意的就是结构转移后的寻址问题。例如在处理DLL文件时,我们需要将宿主文件的导出表转移到Stub部分,但是这并没完,还需要修改宿主程序的导出目录,以使其指向转移后的导出表。但是转移后的导出表的RVA究竟应该怎么计算呢?大家可以参考以下公式:

最后需要注意的就是一些细节问题,例如并非所有PE文件区段的SizeOfRawData都是有值的;并非所有PE文件的导入表都是有INT的;并非所有PE文件的TLS数据指向的都是有效的文件偏移等。类似的细节问题非常多,随着写壳过程的不断深入,就会发现越来越多的这些细节问题。但是这些细节问题都是一个壳兼容性优劣的决定性因素。我们发现,类似的细节越多,最终所能写出来的壳就越稳定,兼容性也就越优秀。


五、怎样调试由C++编写的Stub部分


由于Stub部分在运行时已经寄生到了其他的程序中,因此其调试目录相关的信息自然也就都失效了。当然,如果能在加壳时顺便处理调试目录的相关信息,那么直接调试处于寄生状态的也并非是不可能的事,但是我相信大多数人都不会这么做。


通过试验,发现使用调试API函数OutputDebugString可以给我们带来不少便捷。


由于Stub在调试时都是以Release方式编译的,因此其反汇编代码相比源码来说变化很大,如果没有扎实的软件逆向的基础,很难快速地将眼前的一大片反汇编代码与你写的源代码相对应。但是使用了OutputDebugString函数后情况就不同了。


首先可以使用OutputDebugString的打印调试信息功能,使用DbgView工具查看其打印出来的信息;其次在使用OllyDbg调试Stub的时候,适当地使用OutputDebugString也会起到导航标的作用。例如可以在每个函数的入口处加入以下代码:

其效果如图10所示。

图10  使用OutputDebugString后调试时的情况

从图10中可以看到,除了函数入口处的"Decrypt()Function Entry!"提醒我们已经进入Decrypt函数外,还有Decrypt()_01等标记函数中的关键点,这样我们就能迅速地在反汇编代码中定位到关键点,从而使我们的调试工作更加轻松可靠。


六、结语


本文详细地讲解了使用C++编写加壳程序的全部思路,并给出了一个基本的例子。您可以由这个例子为起点设计自己的专业,进而写出自己的加密免杀壳。


由于在的整个开发过程中,最不可控的就是Stub部分,因此在设计本文的内容时,时时以降低Stub部分的复杂度为第一要素。因此大家应该可以发现,无论是我们编写的简单还是后来设计的专业壳,其Stub部分都是非常简单的。这就最大化地控制了整个加壳软件在技术方面的不稳定因素。


由于篇幅限制与定位等因素,并不能在这里进行更为深入的讲解,但是如果大家能完全理解本文所讲的内容,那么编写出一个具有纯正C++血统(无任何汇编)的是完全有可能的。

微信公众号:计算机与网络安全

ID:Computer-network

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存