PE结构-空白区添加代码
空白区域添加代码
经过一个多星期的PE学习,现在把几个能够较好体现PE结构和各种属性的练习整理出来
目的:在notepad.exe中添加一段代码,使其在运行的时候,先弹出一个窗口,然后再执行主程序。
所利用的知识点:
文件对齐,内存对齐,硬编码,PE头结构
①首先要知道,PE文件为了更好的执行,添加了文件对齐和内存对齐的属性,在老版本编译器中,文件对齐一般为200h,内存对齐一般为1000h,在现今版本编译器中,大多数都为1000h。
②DOS头+DOS存根+PE标识+PE文件头+PE可选头+节表 该部分组成了宏观意义上的“PE头”。“PE头”不管是在内存对齐还是文件对齐中,都不会拉伸,中间没有空隙。
③而节表和节数据之间为了对齐而产生的空隙区域,为我们利用提供了方法,添加一段保护代码,就叫做壳,添加shellcode,就叫做病毒。
步骤一 (寻找合适地址)
首先,因为要调用MessageBox函数,所以需要先找到MessageBox函数的地址。利用od寻找
用winhex打开notepad,找到节表后的空白区域,这里我选择在300h处添加自己的代码
*步骤二 (RVA FOA)
要理解怎样写入上述的数据,这里需要掌握硬编码的知识,这里给出两个公式,不花费过多时间解释,请自行领会
E8后跟随的硬编码 = 要跳转的地址- E8所在的地址 - E8所在指令的长度
E9后跟随的硬编码 = 要跳转的地址- E9所在的地址 - E9所在指令的长度
而且更要注意的是,在该notepad中,文件对齐和内存对齐大小并不一样,这里又需要将FOA 转化为 RVA的知识
FOA = n.PointerToRawData + 偏移(即在RVA中所属节+偏移,因为在内存中节空隙之间会被拉伸,但节中的数据并不会被拉伸)
因为300h在文件中仍属于节表区域,而从DOS头到节表区域中间是没有空隙的,所以在内存中,所对应的就是ImageBase+300h,即1000300(这里已经查看了ImageBase)
将我们所需要的数据带入公式中可得
E8后跟的硬编码 =要跳转的地址 - E8所在的地址 - E8所在指令的长度 77D507EA - 1000308 - 5 76D504DD E9后跟的硬编码 =要跳转的地址 - E9所在的地址 - E9所在指令的长度 1000000+739D -100030D - 5 708B
随后,将结果写入即可,切记(小端序存储)
步骤三 (修改EP)
上述步骤已经将想要添加的代码成功添加到程序中,但仍不可运行,因为,程序的入口点(EP)没有改变,当程序加载到内存中时,入口点的VA就是ImageBase+EP,于是,下一步就是要让OEP指向自己写的代码,将自己写的代码执行完毕后,再jmp到原来的OEP处,执行原来的功能。
而则就要求熟练掌握PE头中的各个部分了,PE文件中的ImageBase属性正是在IMAGE_OPTIONAL_HEADER中,而PE头的结构想必都非常熟悉,我前面的博客中已经介绍了,请自行阅读。
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
PE指纹和文件头分别占4个字节和20个字节,紧接着就是可选头,可选头的大小不固定,如果不更改,在32位系统中则为E0。
//可选PE头 struct _IMAGE_OPTIONAL_HEADER{ 0x00 WORD Magic; //※幻数(魔数),0x0107:ROM image,0x010B:32位PE,0X020B:64位PE //0x02 BYTE MajorLinkerVersion; //连接器主版本号 //0x03 BYTE MinorLinkerVersion; //连接器副版本号 0x04 DWORD SizeOfCode; //所有代码段的总和大小,注意:必须是FileAlignment的整数倍,存在但没用 0x08 DWORD SizeOfInitializedData; //已经初始化数据的大小,注意:必须是FileAlignment的整数倍,存在但没用 0x0c DWORD SizeOfUninitializedData; //未经初始化数据的大小,注意:必须是FileAlignment的整数倍,存在但没用 0x10 DWORD AddressOfEntryPoint; //※程序入口地址OEP,这是一个RVA(Relative Virtual Address),通常会落在.textsection,此字段对于DLLs/EXEs都适用。 0x14 DWORD BaseOfCode; //代码段起始地址(代码基址),(代码的开始和程序无必然联系) 0x18 DWORD BaseOfData; //数据段起始地址(数据基址) 0x1c DWORD ImageBase; //※内存镜像基址(默认装入起始地址),默认为4000H 0x20 DWORD SectionAlignment; //※内存对齐:一旦映像到内存中,每一个section保证从一个「此值之倍数」的虚拟地址开始 0x24 DWORD FileAlignment; //※文件对齐:最初是200H,现在是1000H //0x28 WORD MajorOperatingSystemVersion; //所需操作系统主版本号 //0x2a WORD MinorOperatingSystemVersion; //所需操作系统副版本号 //0x2c WORD MajorImageVersion; //自定义主版本号,使用连接器的参数设置,eg:LINK /VERSION:2.0 myobj.obj //0x2e WORD MinorImageVersion; //自定义副版本号,使用连接器的参数设置 //0x30 WORD MajorSubsystemVersion; //所需子系统主版本号,典型数值4.0(Windows 4.0/即Windows 95) //0x32 WORD MinorSubsystemVersion; //所需子系统副版本号 //0x34 DWORD Win32VersionValue; //总是0 0x38 DWORD SizeOfImage; //※PE文件在内存中映像总大小,sizeof(ImageBuffer),SectionAlignment的倍数 0x3c DWORD SizeOfHeaders; //※DOS头(64B)+PE标记(4B)+标准PE头(20B)+可选PE头+节表的总大小,按照文件对齐(FileAlignment的倍数) 0x40 DWORD CheckSum; //PE文件CRC校验和,判断文件是否被修改 //0x44 WORD Subsystem; //用户界面使用的子系统类型 //0x46 WORD DllCharacteristics; //总是0 0x48 DWORD SizeOfStackReserve; //默认线程初始化栈的保留大小 0x4c DWORD SizeOfStackCommit; //初始化时实际提交的线程栈大小 0x50 DWORD SizeOfHeapReserve; //默认保留给初始化的process heap的虚拟内存大小 0x54 DWORD SizeOfHeapCommit; //初始化时实际提交的process heap大小 //0x58 DWORD LoaderFlags; //总是0 0x5c DWORD NumberOfRvaAndSizes; //目录项数目:总为0X00000010H(16) 0x60 _IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 };
在可选头中,通过偏移,即可找到AddressOfEntryPoint,它的值就是需要修改成自己所写的代码所在的偏移地址。
这样,空白区添加代码所需的全部工作已经完成。运行发现结果正常。先弹出一个窗口,关闭窗口后,打开notepad本体。
总结
必须熟练掌握RVA FOA的转换,在反病毒和脱壳加壳领域中,是非常重要的基本功