添加shellcode有以下几种的方式:
1.直接在任意节的空白区添加代码
2.新增节添加代码
3.扩大最后一个节添加代码
4.合并节并添加代码
今天我们学习如何扩大节,合并节
扩大节
在上一节的学习中,我们可以通过上移NT头和节表覆盖DOS Stub以获取足够的空白区来增加新的节表以及后续新增节,但是当整体上移覆盖DOS Stub之后,多出来的空白区域还不够新增节表时,我们就可以使用扩大最后一个节的方法来添加我们的shellcode
扩大节主要用于当我们在任意空白区添加比较多的代码,但空白区不够时使用,我们通常扩大最后一个节。这是因为当我们扩大其他某个节时,其他节的偏移量之类的属性都需要修改,这样很麻烦。而选择扩大最后一个节,这样就不用修改其他节的属性了
扩大节步骤:
1.拉伸到内存
2.分配一块新的空间:SizeOfImage + EX
3.将最后一个节的SizeofRawDate和VirtualSize改成N
N = (取SizeofRawDate和VirtualSize内存对齐后大者的值) + EX
令SizeofRawDate = VirtualSize
4.修改SizeOfImage大小为SizeOfImage + EX
就直接把要添加的代码加到最后一个节中即可,这样可以保证不影响上面的所有地址。具体操作参考代码区空白区添加代码文章
注意事项:
1.文件加载到内存后,最后一个节的VirtualAddress + VirtualSize对齐后的位置就是一个文件在内存中的结束位置,即SizeOfImage。文件在硬盘上时,最后一个节的PointerToRawData + SizeOfRawData的位置就是整个文件再硬盘上的结尾。
2.文件在内存中,节与节之间的排列是根据VirtualAddress和VirtualSize以及其内存对齐后的结果进行判断。如果在硬盘上,节与节之间的排列,是根据PointerToRawData和SizeOfRawData来判断,这是因为SizeOfRawData已经是文件数据经过对齐后的结果了
合并节
当我们整体上移后,留出来的空白区域不足以添加我们新的节表时,我们便可以利用合并节来添加数据了,将添加的代码加到最后一个节的空间中。由于内存对齐的原因,我们会有很大一部分的空间供我们去添加代码。这样可以保证不影响上面的所有相对地址的计算
注意:合并节一定要在ImageBuffer中进行操作,不然在FileBuffer容易出现错误,比如节中未初始化数据在拉伸后导致节变大等等情况。
合并节就是将所有的节表只保留第一个(其他节表可删可不会删,因为这些数据没有意义了),把第一个节开始一直到最后一个节的结尾都当成第一个节
但是可能会造成合并后文件变大,因为文件对齐粒度一般都要小于内存对齐粒度,所以虚拟内存中每个节的空白区一般比文件空白区要大一些,现在合并节是把虚拟内存中从第一个节开始到文件结束都当成一个节,最后再变成FileBuffer就会变大。
步骤:
1.NumberOfSections改成1
2.修改第一个节表的字段
VirtualSize:
方法一:SizeOfImage - 第一个节VirtualAddress
方法二:还可以通过最后一个节的VirtualAddress + 最后一个节的VirtualSize内存对齐后的大小 - SizeOfHeader内存对齐后的大小获取整个VirtualSize
方便起见我们选择方法一
SizeOfRawData:等于VirtualSize即可
将第一个节的属性改为包含所有节的属性,即用第一个节的属性与其他的节的属性做或运算
数据目录
数据目录的引入
可选PE头时,有一个字段为DWORD NumberOfRvaAndSizes,NumberOfRvaAndSizes的值表示_IMAGE_DATA_DIRECTORY DataDirectory[16];这种类型的结构体的数量
Win32操作系统下,PE文件不仅有文件本身的数据,编译器也会替我们往每一个节中加很多重要的内容:比如一个文件使用系统提供的函数时,编译器就会在PE文件中添加这些函数的相关信息,告诉该文件在运行时需要去哪里找到这个函数并进行系统调用。由于一个程序不仅可以使用系统的函数,也可以提供函数给别的文件使用,所以此时编译器会添加此程序中供别人使用的函数相关信息
编译器添加的内容包含了诸如:PE程序的图标位置,用到了哪些系统提供的函数,为其他的程序提供哪些函数等各种信息,如果编译器不添加这些数据的话,程序就找不到这些东西的地址,程序也无法正常运行。
数据目录的作用
每个PE文件都有16个数据目录用来记录编译器在PE文件中添加的16种不同的信息,并说明这些信息存在在哪里
数据目录的内容
可选PE头最后一个成员,就是数据目录,一共有16个
分别是:导出表的数据目录、导入表的数据目录、资源表的数据目录、异常信息表的数据目录、安全证书表的数据目录、重定位表的数据目录、调试信息表的数据目录、版权所有表的数据目录、全局指针表的数据目录、TLS表的数据目录、加载配置表的数据目录、绑定导入表的数据目录、IAT表的数据目录、延迟导入表的数据目录、COM信息表的数据目录、最后一个保留未使用
其中比较重要的就是导出表、导入表、重定位表、IAT表这四张表的数据目录,这四张表和程序的运行有直接关系,不管是加壳、脱壳、破解、病毒或反病毒,这些都是基础中的基础
数据目录的结构
结构如下:
struct _IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress; //内存偏移,必须有
DWORD Size; //大小,破坏了也不会影响程序运行
};
16个数据目录的大小一样,都是8字节
手动查找数据目录
从节表开始往上倒着推8行,就是数据目录开始
也可以从可选PE头开始,往后数第25个DWORD就是数据目录开始
上图中前8字节是导出表的数据目录:当中前4个字节为导出表的VirtualAddress–0x0;后4字节为导出表的Size–0x0,说明此可执行文件没有导出表
再接着8字节为导入表的数据目录:前4字节为为导入表的VirtualAddres–0x6D20;后4字节为导入表的Size–0xC8,此可执行文件的导入表在ImageBase + 0x6D20处开始,大小为0xC8
下面以此类推
数据目录的应用
数据目录只是记录了对应的表的内存偏移地址和大小。我们通过找到相应表的数据目录,得到相应表的内存偏移地址,然后加上ImageBase才是相应表本身真正的所在位置!
举例:我们找到导出表的数据目录,通过数据目录中的VirtualAddress + ImageBase得到导出表所在的地址
IMAGE_DIRECTORY_ENTRY_EXPORT
struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
};
如图: