这一章讲的是PE构造以及压缩加壳,因为之前看到过epf文件的构造,所以理解PE起来会容易很多,也可以去类比;
先总结一下PE文件的特点:有个头来控制各个节区的偏移,且在文件和内存中的形式相似却不一样;可以把内存中运行的程序成为文件的映像;
第一:PE头
PE头是个结构体,里面包含了更多的结构体,每个结构体有着调控和指向的作用;
DOS头,40个字节,名称 IMAGE_DOS_HEADER;里面的内容中,e_magic 代表签名,一般是4D5A;e_ifanew 代表NT头偏移量;
DOS头后有个DOS存根,PE文件可要可不要;
NT头,包含三个成员,签名(signature),一般是PE00;以及文件头和可选头;
文件头包含4个重要成员:Machine CPU唯一对应码;NumberOfSections 节区数量;SizeOfOptionalHeader 可选头大小;Characteristics 文件的信息情况;
可选头的成员:Magic 可选头签名,32位是10B,64位是20B;AddressOfEntryPoint 俗称EP,显示的是EP的RVA值,这是程序执行的入口地址;ImageBase 文件的优先装入地址,exe和dll 在 0 ~ 7fffffff,sys在后面的内存中,其中,dll的起始值为 10000000;SectionAlignment FileAlignment 前者针对内存而言,后者针对磁盘而言,他们都是对齐,所以是节区的最小单位;SizeOfImage PE文件在内存中的空间大小;SizeOfHeader 整个PE头的大小;SubSystem 是否为sys后缀又或是exe和dll后缀;NumberOfRvaAndSizes DataDirectory数组的下标数;DataDirectory 由IMAGE_DATA_DIRECTORY组成的数组;
节区头,分三类,code(可执行,可读);data(可写,可读);resource(可读);
第二:压缩和UPX
这个部分讲述了内存和磁盘以及压缩如何处理文件的内容;
Rva to Raw 可以这么理解,raw就是生肉,还没烤熟,所以在磁盘中为文件,没有驱动;那么它的意思就是,从 内存 到 磁盘;具体是在说它们的偏移映射;
具体算法:Raw = Rva - VirtualAddress + PointerToRawData;
INT 与 IAT,前者全称:import name table,后者全称:import address table,后者叫做 导入地址表;
DLL 全称:动态链接库,一共两种方式,显式连接,隐式连接;前者是使用时链接,使用后释放,后者是启动时链接,结束后释放(类似于静态链接);
CreateFileW() 函数,因为不知道PE的实际版本,所以用(01001104)地址处的值来进行跳转;
IMAGE_IMPORT_DESCRIPTOR 导入库;
IMAGE_OPTIONAL_Header32.DataDirectory[1].VirtualAddress DataDirectory数组,这个值是导入库的起始地址;
EAT 和 API 前者能求出相应库中导出函数的起始地址,后者通过GetProcAddress() 函数获取要取函数的地址;
压缩即运行时压缩——UPX,因为解压通常会有解码循环,所以在逆向加壳产物的思想就是,尽量避免循环;
UPX的特征:EP码在 pushad 和 popad 之间;且跳转到OEP处的JUMP指令紧跟 popad 之后;
硬件断点:由CPU支持,最多打4个,与普通断点的区别是:在断点处的指令完成之后暂停调试;
第三:重定位
讲述PE文件链接后成为程序时,从文件到磁盘地址的变化内容;PE文件加入内存时,文件会被加载到 ImageBase 所指地址,若再加入DLL,那么加入的DLL会被重新定位;
原理:
- 在应用程序中查找硬编码地址位置;
- 用读值减去ImageBase(VA -> RVA);
- 加上实际加载地址(RVA -> VA);
首先需要了解:基址重定位表(Relocation Table),这个表也在DataDirectory数组中,下标为5;
其中的重要成员:VirtualAddress 基准地址,RVA值,4字节,可以用来算 Rva To Raw;SizeOfBlock 重定位块大小,4字节;TypeOffset 偏移,由4位Type和12位offset组成,共2字节;
删除reloc节以理解整个PE的工作:整理节区头,删除节区,修改文件头和可选头;
节区头从文件偏移270处开始,大小28个字节,节区起始位置偏移为C000;
文件头中的 NumberOfSections 改少1,因为删除了一个节区;
可选头中 SizeOfImage 减少,减少量:知晓reloc节中的VirtualSize值,并根据SectionAlignment扩展变化后的值;
第四:Upack压缩和内嵌补丁
Upack也是PE文件运行时的压缩器;但因为神奇的压缩技巧,会导致一些查看器认为文件损坏无法查看,所以需要特别地用Stud_PE来查看;
压缩技巧:
- 重叠文件头,把PE头和MZ头重叠;
- 修改文件头中 SizeOfOptionalHeader 的值,明面上改了可选头大小,实际上增大了可选头和节区头之间的距离,在空隙之间插入解码代码;
- 修改可选头中 NumberOfRvaAndSizes 的值,增多DataDirectory数组,向文件头插入自身代码;
- 修改节区头,Upack把自身代码记录到程序运行不需要的目录,不用特别增加节区;
- 重叠节区;
- Rva to Raw 时,使用异常处理:PointerToRawData应该遵循对齐,但Upack会改值,运行时强制将这个值改为对齐的整数倍,一般是0;
- 导入表,看似结尾没有NULL结尾会出错,实际上映射到内存后,后面会有空区域自行补充0;
- 导入地址表;
解码循环:Upack把压缩后的数据放到第二个节区,在运行解码循环解压到第一个节区;解压后设置IAT,用导入的两个API;之后一边循环一边构建原本的IAT,完成后连接到OEP;
内嵌补丁:注入的代码,一般用于针对难以直接修改的代码的更新升级或者恶意篡改;需要对PE文件熟悉来更改PE的控制设定增添节区;
总结
了解完PE和压缩部分的知识过后,也懂得了UPX的实际作用,以及如何逆向简单加壳程序;PE展示的成果其实远不止于此,正因为了解了PE构造和压缩,才能更好地藏匿木马节区和后门函数;现在学习地正是以前看不懂的电脑思维,以前就会思考为什么编译器能读懂高级语言呢?怎么编译成可执行文件呢?懂得了它的思维,就可以利用它的漏洞;实验还多,任重而道远,继续冲冲冲!