当前位置: 首页 > 编程 > 正文

一.题记 近日,科锐一季度一次的最小PE比赛来临,规则就是手写能弹出对话框的最小PE文件,当然为了照顾我们初学 […]

一.题记

近日,科锐一季度一次的最小PE比赛来临,规则就是手写能弹出对话框的最小PE文件,当然为了照顾我们初学PE对字段还不是很熟悉,允许我们参考Mspaint.exe的PE结构.
课堂上Boss钱给我们讲解了PE最小PE思路,并带领着我们写了一个留有优化余地的185字节的版本PE文件.此文是将课堂笔记整理而成,虽力求完美,但难免因本人的学识浅薄而存在不足之处,望大神们批评指正.

二.环境

  1. OS:Windows XP
  2. IDE:010 Editor
  3. 参考:Mspaint.exe

* 优化PE大小的思路是基于结构重叠,结构重叠是指将两个结构合并存一个位置上.举个例子,假如一个正常PE文件的PE标识从(FOR:40h)开始,这时候将NT头挪到(FOR:04h)的位置,再将DOS头中e_lfanew成员改为0x0000004,此时DOS头与PE头重叠.
* 之所以选择XP,是因为Win7对可执行文件的检查很严格,基本没什么文章可做.不支持重叠,而XP支持重叠.

三.185字节版本PE

3.1 1024字节PE模版

3.1.1 DOS头

  • 将Mspaint.exe中的DOS头拷贝到新建的1024.exe中.

3.1.2 NT头

  • 将Mspaint.exe中的NT头(PE标识+PE头+可选PE头(可选PE头中目录项只拷贝4项))拷贝到新建的1024.exe中.

3.1.3 节表

  • 将Mspaint.exe中的一项节表拷贝到新建的1024.exe中.

3.1.4 修改PE文件

  • 根据下列备注修改1024.exe.不重要的字段填充AA,并适当修改对齐.目录项先全部填充0.
1. DOS头
struct _IMAGE_DOS_HEADER {
WORD e_magic;           //5A 4D                    //(!重要) |"MZ标记"
WORD e_cblp;           //AA CC                    //CC是为了让程序断下
WORD e_cp;               //AA AA                    
WORD e_crlc;           //AA AA                    
WORD e_cparhdr;        //AA AA                    
WORD e_minalloc;       //AA AA                    
WORD e_maxalloc;       //AA AA                    
WORD e_ss;             //AA AA                    
WORD e_sp;             //AA AA                    
WORD e_csum;           //AA AA                    
WORD e_ip;             //AA AA                    
WORD e_cs;             //AA AA                    
WORD e_lfarlc;         //AA AA                    
WORD e_ovno;           //AA AA                    
WORD e_res[4];         //AA AA AA AA AA AA AA AA  
WORD e_oemid;          //AA AA                    
WORD e_oeminfo;        //AA AA                    
WORD e_res2[10];       //20个A                    
DWORD e_lfanew;        //00 00 00 40              //(!重要) |用于定位PE标识
};                                                              

2. NT头                                                         
struct _IMAGE_NT_HEADERS{                                       
DWORD Signature;       //00 00 45 50                            //(!重要) |PE标识
_IMAGE_FILE_HEADER FileHeader;                                  
_IMAGE_OPTIONAL_HEADER OptionalHeader;
}

2. 标准PE头(大小固定)
struct _IMAGE_FILE_HEADER {
WORD  Machine;                               //01 4C           //(!重要) 
WORD  NumberOfSections;                      //00 01           //(!重要) |节总数(1)
DWORD TimeDateStamp;                         //AA AA AA AA         
DWORD PointerToSymbolTable;                  //AA AA AA AA 
DWORD NumberOfSymbols;                       //AA AA AA AA
WORD SizeOfOptionalHeader;                   //00 80           //(!重要) |可选PE头的大小
WORD Characteristics;                        //01 0F           //(!重要) |可执行文件值为10F 
};

3. 可选PE头((大小不固定,32和64不同))
struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;                                  //01 0B           //(!重要) |10B-32位下的PE文件
BYTE MajorLinkerVersion;                     //AA              
BYTE MinorLinkerVersion;                     //AA              
DWORD SizeOfCode;                            //AA AA AA AA     
DWORD SizeOfInitializedData;                 //AA AA AA AA     
DWORD SizeOfUninitializedData;               //AA AA AA AA    
DWORD AddressOfEntryPoint;                   //00 00 00 02     //(!重要) |程序入口点(02,断在CC处)
DWORD BaseOfCode;                            //AA AA AA AA     
DWORD BaseOfData;                            //AA AA AA AA     
DWORD ImageBase;                             //00 40 00 00     //(!重要) |内存镜像基址
DWORD SectionAlignment;                      //00 00 10 00     //(!重要) |内存对齐
DWORD FileAlignment;                         //00 00 02 00     //(!重要) |文件对齐
WORD MajorOperatingSystemVersion;            //AA AA           
WORD MinorOperatingSystemVersion;            //AA AA           
WORD MajorImageVersion;                      //AA AA           
WORD MinorImageVersion;                      //AA AA           
WORD MajorSubsystemVersion;                  //00 04           //(!重要) |子系统版本号            
WORD MinorSubsystemVersion;                  //AA AA           
DWORD Win32VersionValue;                     //AA AA AA AA   
DWORD SizeOfImage;                           //00 00 20 00     //(!重要) |PE文件映射到内存后的尺寸,SectionAlignment的倍数
DWORD SizeOfHeaders;                         //00 00 02 00     //(!重要) |所有头+节表按照文件对齐后的大小
DWORD CheckSum;                              //AA AA AA AA     
WORD Subsystem;                              //00 02           //(!重要) |子系统
WORD DllCharacteristics;                     //00 00           //(!重要) |
DWORD SizeOfStackReserve;                    //00 40 00 00     //(!重要) |初始化时保留的栈大小(桟最大值)
DWORD SizeOfStackCommit;                     //00 00 10 00     //(!重要) |初始化时实际提交的栈大小(实际使用桟大小)
DWORD SizeOfHeapReserve;                     //00 10 00 00     //(!重要) |初始化时保留的堆大小(堆最大值)
DWORD SizeOfHeapCommit;                      //00 01 00 00     //(!重要) |初始化时实际提交的堆大小(实际使用堆大小)
DWORD LoaderFlags;                           //AA AA AA AA     
DWORD NumberOfRvaAndSizes;                   //00 00 00 04     //(!重要) |目录项数目(4),其实最优是2项,有导入表即可
_IMAGE_DATA_DIRECTORY DataDirectory[16];                       //目录项(4个目录项,先全初始化为0)
};

4. 节表
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER
 {
    BYTE Name[IMAGE_SIZEOF_SHORT_NAME];    //EE EE EE EE EE EE EE EE               
    union                                              
        {
            DWORD PhysicalAddress;                      
            DWORD VirtualSize;
        } Misc;                            //00 00 10 00  //(!重要) |提交到内存中大小
    DWORD VirtualAddress;                  //00 00 10 00  //(!重要) |提交到内存中的偏移
    DWORD SizeOfRawData;                   //00 00 20 00  //(!重要) |节在文件中对齐后的尺寸
    DWORD PointerToRawData;                //00 00 02 00  //(!重要) |节在文件中的偏移                          
    DWORD PointerToRelocations;            //AA AA AA AA         
    DWORD PointerToLinenumbers;            //AA AA AA AA          
    WORD NumberOfRelocations;              //AA AA       
    WORD NumberOfLinenumbers;              //AA AA
    DWORD Characteristics;                 //60 AA AA AA  //(!重要) |节的属性 高位给运行属性即可
};
  • 附上修改好的图.

3.1.5 节数据填充00

  • 因为节表中PointerToRawData的值是200h,SizeOfRawData的值也是200h.所以要在FOR:100h-FOR:3F0h的位置填充上00,保持对齐.

  • 填充后文件大小为1024KB.

3.1.6 0x80000003错误

  • 将修改好的文件保存为1024.exe,双击运行,如果修改无误,会报0x80000003错误,此时不要灰心,出现这个错误说明PE格式正确.然后以此为模版新建一个文件开始重叠结构.(忘了怎么设置,重现XP经典又亲切的大红叉...)

3.2 185字节PE文件

3.2.1 DOS头

  • 以上文中的1024.exe文件为模版,在010Edid中新建一名为180.EXE的文件.并将1024.exe中的DOS拷贝到180.EXE中.

3.2.2 初次重叠-NT头

  • 将1024.EXE中PE标识(FOR:40h)-数据目录(FOR:D7h)处的NT头数据往185.EXE中DOS头(FOR:04h)的位置往后覆盖.

3.2.3 修改DOS头e_lfanew

  • 因为上文中将NT头与DOS头重叠,DOS头中e_lfanew(用于定位PE标识的偏移)成员(FOR:3Ch),被PE头中的SectionAlignment(内存对齐)对齐覆盖成(00 00 10 00h).而我们现在的PE标识如今在(FOR:04h),巧的是WinXp最小支持4对齐.所以我们将内存对齐(FOR:3Ch)与文件对齐(FOR:40h)都更改为(00 00 00 04h).

3.2.4 修改节表

3.2.4.1 拷贝节表

  • 将1024.EXE中节表拷贝到185.EXE尾部.

3.2.4.2 修改节表Name成员

  • 因为节名称只是说明性意义,操作系统不参考,所以此处8字节可以随意更改.将(FOR:9Ch)-(FOR:A3h)的位置填充EE(EE并没有特定意义,只是为了区分).

3.2.4.3 修改节表Misc成员

  • 因为我们整个头部所占的大小为C4h,将Misc成员(FOR:A4h)修改为(00 00 00 C4h).

3.2.4.4 修改节表VirtualAddress成员

  • 两节合一,所以在内存中从0偏移开始.将VirtualAddress成员(FOR:A8h)修改为(00 00 00 00h)

3.2.4.5 修改节表SizeOfRawData成员

  • 我们整个头部大小为C4h,满足4对齐,可将SizeOfRawData成员(FOR:ACh)修改为(00 00 00 C4h).

3.2.4.6 修改节表PointerToRawData成员

  • 文件偏移从0h开始.将PointerToRawData成员(FOR:B0h)修改为(00 00 00 00h).

3.2.5 修改可选PE头SizeOfImage成员

  • 映射到内存中要以1000对齐,所以将SizeOfImage成员(FOR:54h)修改为(00 00 10 00h).

3.2.6 修改可选PE头SizeOfHeaders成员

  • 所有头与节表的大小为C4h,正好是4对齐,所以将SizeOfHeaders成员(FOR:58h)修改为(C4 00 00 00h).

3.2.7 修改可选PE头SizeOfStackReserve|SizeOfStackCommit|SizeOfHeapReserve|SizeOfHeapCommit成员

  • 这四个成员分别为桟保留(FOR:64h)|桟提交(FOR:68h)|堆保留(FOR:6Ch)|堆提交(FOR:70h). 高字节保留为0即可,低3字节可填充为AA.

3.2.8 再见0x80000003错误

  • 将刚修改的文件保存,运行.再次弹出不久前见到的0x80000003错误报告.说明刚才修改的格式正确.

3.2.9 导入表

3.2.9.1 API-MessageBoxa

  • MessageBoxa我们从(FOR:Ch)填充.

3.2.9.2 库名称-user32.dll

  • 库名称 user32 我们从(FOR:30h)填充.

3.2.9.3 导入表Name成员与FirstThunk成员

  • Name成员从(FOR:B4h)填充(00 00 00 30h).
  • (FOR:44h)刚好有8个字节可用,此处可以放IAT表.所以FirstThunk成员(FOR:B8h)我们填充(00 00 00 44h).

3.2.9.4 IAT表

  • IAT表以0结尾,所以在(FOR:48h)填充4字节0.
  • MessageBoxA在(FOR:04h)处,前面还有两字节字段,所以在(FOR:44h)填充(00 00 00 44h).

3.2.9.5 导入表目录项

  • 导入表起始位置为(FOR:A8h),因此目录项第二项(FOR:84h)填入(00 00 00 A8).

3.2.10 验证导入表

  • 保存修改,用OD打开185.EXE文件.转到内存0x00400000,查看导入表是否填充正确.

  • MessageBoxA的地址已经填入.说明我们导入表填充正确.

3.2.11 对话框内容与标题

  • 对话框标题MyPE,就在(FOR:02h)填充"My",与后面的"PE"刚好组成字符串"MyPE".

  • 对话框内容就选(FOR:0Ch)其实的字符串"MessageBoxA"吧.

3.2.12 调用代码

  • 一般情况下,调用185.EXE中MessageBoxA的汇编代码如下:
6A 00              push 0
68 02 00 40 00      push 0x00400002
68 0C 00 40 00      push 0x0040000c
6A 00              push 0
FF 15 44 00 40 00 call [0x00400044]
C3                  ret
  • 但此文件中明显没有足够的空间让我们正常填写此代码,只能通过跳转来塞入指令.
从0x1E开始填充
6A 00              push 0
68 02 00 40 00      push 0x00400002
EB 3E             jmp  0x00400065



从0x65开始填充
68 0C 00 40 00      push 0x0040000c
6A 00               push 0
FF 15 44 00 40 00 call [0x00400044]
C3                  ret

3.2.13 修改OEP

  • 将OEP(FOR:2Ch)修改为(00 00 00 1Eh)

3.2.14 PE特性

PE有一个特性,PE文件加载到内存后,不足一个分页的位置会用0填充,也就是说,PE在文件末尾,有初值为0的默认数据,也就是说如果文件末尾的字节为0,就可以把这个字节删掉,进而减小文件体积.
而且操作系统不对执行属性负责(前提为DEP数据执行保护.如果开了,那么必须要有执行属性才能运行,默认DEP关闭),即使节中没有执行属性,也可运行.如果把读属性去掉,会报C00000005.如果手写PE出现C05错误,那么说明PE格式没有问题,请检查代码.操作系统装载,尝试运行没有错误,因为没有读的权限,所以会报错.如果格式错误,则提示不是有效XX位程序.
而文件头默认就有读属性,所以本文件将节表中文件属性去掉并不影响运行.

  • 从本文件中从(FOR:BCh)-(FOR:C3h)删掉.保存文件后检验成果吧.

  • 185字节,完美运行.

3.2.15 再减小体积

  1. 节表中节表名字段还剩8字节,可以将导出表(FOR:B4h)挪到节表名(FOR:9Ch)的位置覆盖.

  2. 将目录项中导出表(FOR:84h)目录更新为新地址(00 00 00 90h).

  3. 删除尾部,保存运行.

四.尾声

  • 上文是将课堂笔记整理而成,因本人学识浅薄难免有遗漏之处,若有不足之处望各位大哥指出让小弟改正.
  • 最小PE只是为了初学PE的我们熟悉PE结构而已,实战中也并没有谁抠字节写个最小PE文件就为了弹个对话框吧...并且在Win7以上的系统检查越来越严格,不支持重叠.
  • 最小PE比赛中,为了避免在网上参考现成的代码,不能在第四字节放NT头,其他地方随意,因为要做到最优,只能在第四字节放NT头.比赛过后,参考了其他同学的思路,完成了141字节的最小PE.过两天放假了再整理141字节最小PE的笔记发上来吧.
  • 时间不早了,洗洗睡吧...
本文固定链接: https://blog.050k.com/?p=67 | LseKit's Blog

浅谈XP下最小PE:等您坐沙发呢!

发表评论

快捷键:Ctrl+Enter