【 标题 】 PE节load时的对齐问题
【 作者 】 linxer
【 环境 】 xp sp2
【 Q Q 】 3568599
【 声明 】 俺系初级选手,高手略过。失误之处敬请诸位大侠赐教!
本问题的研究源至upack 0.32 load后虚拟脱壳,发现自己写的load程序有很大问题,upack 0.32加壳后程序虚拟机中跑不起来,连入口处反汇编出来的代码都不对......后来,发现是section load不对。经过本人反复改写节表头信息(IMAGE_SECTION_HEADER),然后用OD加载,看其怎么load,发现以下规律,以前曾在论坛上发过一段PE load的代码,当时,对节表对齐属性该怎么效验也不清楚,那个时候,在load FSG1.33时,发现有些节属性是不对齐的,但windows却能让这个程序跑起来,心中甚是不解,后来没有办法,干脆把节对齐属性效验部分给屏蔽了(其实那个时候是自己的程序写的不对,效验太严格了)
前几天,在expressor v1.2虚拟脱壳后dump时,发现dump后程序windows无法load,我用OllyDump插件把OD中脱壳后程序dump出来也有这样的问题,后来发现是:expressor v1.2加壳后程序,其SizeOfImage不为SectionAlignment整数倍,我写的那个dump是在相对PE头SizeOfImage处建新节的(从以前看的OllyDump插件源码学的,不过,目前最新的源码好像修正这个bug了,可能是我用的插件比较老吧,才load不起来),这样就出现了最后一个节没对齐,windows load不起来。
下面结合我用upack v0.32加壳后程序说明之,高手请飘过,内容很简单。
以下示例中对齐属性如下:
00000078 00100000 DD 00001000 ; SectionAlignment = 1000
0000007C 00020000 DD 00000200 ; FileAlignment = 200
说下FileAlignment字段吧,这个字段在有些程序可以看到是0x1000等,我通过试验,好像发现这个字段不管是什么值,系统都用0x200,不过它必须大于等于0x200,小于等于0x1000,并为0x200整数倍
为了说明问题,这里定义两个变量:
SectionAlignmentMask = SectionAlignment - 1;
FileAlignmentMask = 0x200 - 1;
如不加说明,以下几个示例均非最后一个节
示例1:
节表信息如下:
00000138 2E 55 70 61>ASCII ".Upack" ; SECTION
00000140 00800000 DD 00008000 ; VirtualSize = 8000 (32768.)
00000144 00100000 DD 00001000 ; VirtualAddress = 1000 //按一般书上说,这个要对齐
00000148 B7000000 DD 000000B7 ; SizeOfRawData = B7 (183.) //按一般书上说,这个也要对齐
0000014C 11000000 DD 00000011 ; PointerToRawData = 11
00000150 00000000 DD 00000000 ; PointerToRelocations = 0
00000154 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000158 0000 DW 0000 ; NumberOfRelocations = 0
0000015A 0000 DW 0000 ; NumberOfLineNumbers = 0
0000015C 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
上面这个节,貌似:将磁盘中偏移0x11,大小0xb7的内容拷到内存中VirtualAddress=0x1000处,当然这个节在内存中大小是0x8000,未填充部分全填0
但,其实不是这样的,windows在load的时候是将磁盘中偏移0x00,大小0x200的内容拷到内存中去了,出乎我这个菜鸟的意料了
示例2:
节表信息如下:
00000138 2E 55 70 61>ASCII ".Upack" ; SECTION
00000140 00800000 DD 00008000 ; VirtualSize = 8000 (32768.)
00000144 00100000 DD 00001000 ; VirtualAddress = 1000
00000148 B7020000 DD 000002B7 ; SizeOfRawData = 2B7 (695.)
0000014C f1000000 DD 000000f1 ; PointerToRawData = f1
00000150 00000000 DD 00000000 ; PointerToRelocations = 0
00000154 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000158 0000 DW 0000 ; NumberOfRelocations = 0
0000015A 0000 DW 0000 ; NumberOfLineNumbers = 0
0000015C 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
看下这个win怎么load到内存的,它将磁盘中偏移0x00,大小0x400的内容拷到内存中去了
好了,看了上面两个例子,你应该知道它是怎么从磁盘中拷数据出去了吧,其思路用代码表示如下:
SizeOfRawData = (SizeOfRawData + FileAlignmentMask) & (0xffffffff ^ FileAlignmentMask);
PointerToRawData = PointerToRawData & (0xffffffff ^ FileAlignmentMask);
示例3:
节表信息如下:
00000138 2E 55 70 61>ASCII ".Upack" ; SECTION
00000140 00810000 DD 00008100 ; VirtualSize = 8100 (33024.)
00000144 00100000 DD 00001000 ; VirtualAddress = 1000
00000148 B7020000 DD 000002B7 ; SizeOfRawData = 2B7 (695.)
0000014C f1000000 DD 000000f1 ; PointerToRawData = f1
00000150 00000000 DD 00000000 ; PointerToRelocations = 0
00000154 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000158 0000 DW 0000 ; NumberOfRelocations = 0
0000015A 0000 DW 0000 ; NumberOfLineNumbers = 0
0000015C 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
上面这个例子把VirtualSize改成没有对齐了,发现程序图标也没了,windows还load不起来了,这个地方为什么不能运行是因为我们把VirtualSize调大了,占了第二个节的地方了,如果调小的话,则可以运行,可以看出这个字段的效验是不跨越其它节地界,这个字段只要不跨越其它节地界,为任何值都可以,0也不例外,这个值除了用来效验,没有其它用
示例4:
节表信息如下:
00000138 2E 55 70 61>ASCII ".Upack" ; SECTION
00000140 00800000 DD 00008000 ; VirtualSize = 8000 (32768.)
00000144 00110000 DD 00001100 ; VirtualAddress = 1100
00000148 B7000000 DD 000000B7 ; SizeOfRawData = B7 (183.)
0000014C 11000000 DD 00000011 ; PointerToRawData = 11
00000150 00000000 DD 00000000 ; PointerToRelocations = 0
00000154 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000158 0000 DW 0000 ; NumberOfRelocations = 0
0000015A 0000 DW 0000 ; NumberOfLineNumbers = 0
0000015C 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
这个示例跟示例3一样,图标没了,windows也load不起来了,VirtualAddress这个字段也是要严格效验的
好了,下面来看最后一个节的情况(以下均为最后一个节了)
示例5:
节表信息如下:
00000160 2E 72 73 72>ASCII ".rsrc" ; SECTION
00000168 00500000 DD 00005000 ; VirtualSize = 5000 (20480.)
0000016C 00900000 DD 00009000 ; VirtualAddress = 9000
00000170 48320000 DD 00003248 ; SizeOfRawData = 3248 (12872.)
00000174 00020000 DD 00000200 ; PointerToRawData = 200
00000178 00000000 DD 00000000 ; PointerToRelocations = 0
0000017C 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000180 0000 DW 0000 ; NumberOfRelocations = 0
00000182 0000 DW 0000 ; NumberOfLineNumbers = 0
00000184 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
这个节是upack v0.32加壳后程序的最后一个节,是好的,可见SizeOfRawData也没有对齐,这说明最后一个节SizeOfRawData可以不对齐
示例6:这个示例我们让PointerToRawData不对齐看看
节表信息如下:
00000160 2E 72 73 72>ASCII ".rsrc" ; SECTION
00000168 00500000 DD 00005000 ; VirtualSize = 5000 (20480.)
0000016C 00900000 DD 00009000 ; VirtualAddress = 9000
00000170 48320000 DD 00003248 ; SizeOfRawData = 3248 (12872.)
00000174 10020000 DD 00000210 ; PointerToRawData = 210
00000178 00000000 DD 00000000 ; PointerToRelocations = 0
0000017C 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000180 0000 DW 0000 ; NumberOfRelocations = 0
00000182 0000 DW 0000 ; NumberOfLineNumbers = 0
00000184 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
这么一改,程序图标又没了,windows也load不起来了,这是因为我们加大了PointerToRawData,导致SizeOfRawData + PointerToRawData > 文件长度了,所以windows不让load了,如果将其改小的话,可以load,但是会出现什么错误视具体程序定
下面还是冗余的改下VirtualSize和VirtualAddress字段吧,看看如何
示例7:
节表信息如下:
00000160 2E 72 73 72>ASCII ".rsrc" ; SECTION
00000168 00500000 DD 00005000 ; VirtualSize = 5000 (20480.)
0000016C 00910000 DD 00009100 ; VirtualAddress = 9100
00000170 48320000 DD 00003248 ; SizeOfRawData = 3248 (12872.)
00000174 00020000 DD 00000200 ; PointerToRawData = 200
00000178 00000000 DD 00000000 ; PointerToRelocations = 0
0000017C 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000180 0000 DW 0000 ; NumberOfRelocations = 0
00000182 0000 DW 0000 ; NumberOfLineNumbers = 0
00000184 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
让VirtualAddress不对齐后,程序图标又没了,windows也load不起来了,可见最后一个节的VirtualAddress也有严格的效验
示例8:
节表信息如下:
00000160 2E 72 73 72>ASCII ".rsrc" ; SECTION
00000168 10500000 DD 00005010 ; VirtualSize = 5010 (20496.)
0000016C 00900000 DD 00009000 ; VirtualAddress = 9000
00000170 48320000 DD 00003248 ; SizeOfRawData = 3248 (12872.)
00000174 00020000 DD 00000200 ; PointerToRawData = 200
00000178 00000000 DD 00000000 ; PointerToRelocations = 0
0000017C 00000000 DD 00000000 ; PointerToLineNumbers = 0
00000180 0000 DW 0000 ; NumberOfRelocations = 0
00000182 0000 DW 0000 ; NumberOfLineNumbers = 0
00000184 600000E0 DD E0000060 ; Characteristics = CODE|INITIALIZED_DATA|EXECUTE|READ|WRITE
让VirtualSize不对齐后,程序图标还有(我们是加大了VirtualSize,如果减小,估计就会没了,555,失算了,把它改成0,还有图标),但是windows load不起来了,如果把它改成0x4500也可以运行,这里调大了,导致最后一个节的VirtualAddress + VirtualSize > SizeOfImage了,因此load不起来,这种情况windows也是要效验的
下面给出这些属性的效验代码:
//pe check函数
//合法返回0,否则-1
//lpExePEBuff是pe文件地址
//nExePELen是pe文件长度
long pe_check(char *lpExePEBuff, unsigned long nExePELen)
{
unsigned long lSectionNum;
unsigned long lFileAlign;
unsigned long lFileAlignMask;
unsigned long lSectionAlignMask;
unsigned long nPointerToRawData;
unsigned long nSizeOfRawData;
unsigned long nSizeOfImage;
unsigned long nIndex;
IMAGE_DOS_HEADER *imDos_Headers;
IMAGE_NT_HEADERS * pINH;
IMAGE_SECTION_HEADER *pISH;
IMAGE_SECTION_HEADER * pOldEndISH;
//MZ PE标志效验省去
imDos_Headers = (IMAGE_DOS_HEADER *)lpExePEBuff;
pINH = (IMAGE_NT_HEADERS *)((char *)lpExePEBuff + imDos_Headers->e_lfanew);//NT头指针地址
//取PE文件的节数量
lSectionNum = pINH->FileHeader.NumberOfSections;
pISH = (IMAGE_SECTION_HEADER *)((char *)pINH + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + pINH->FileHeader.SizeOfOptionalHeader);
pOldEndISH = pISH + lSectionNum - 1;
//SizeOfImage按对齐属性向上取整
nSizeOfImage = pINH->OptionalHeader.SizeOfImage;
//各节在磁盘中的对齐掩码
lSectionAlignMask = pINH->OptionalHeader.SectionAlignment - 1; //各节在load后内存中的对齐掩码
nSizeOfImage = (nSizeOfImage + lSectionAlignMask) & (0xffffffff ^ lSectionAlignMask);
//效验最后节VirtualSize是否越界SizeOfImage
if(pOldEndISH->VirtualAddress + pOldEndISH->Misc.VirtualSize > nSizeOfImage)
{
return -1;
}
//貌似FileAlignment无用,系统好像是用0x200,只要是0x200整数倍,不大于0x1000,程序都可以运行
lFileAlign = g_pINH->OptionalHeader.FileAlignment;
if(lFileAlign < 0x200 && lFileAlign > 0x1000)
{
return -1;
}
if(lFileAlign % 0x200)
{
return -1;
}
lFileAlign = 0x200;
lFileAlignMask = lFileAlign - 1;
for(nIndex = 0; nIndex < lSectionNum; nIndex++, pISH++)
{
//效验VirtualAddress对齐属性
if(pISH->VirtualAddress & lSectionAlignMask)
{
//出现非法节
return -1;
}
//效验要装载磁盘数据是否超出文件大小
if(pISH->PointerToRawData + pISH->SizeOfRawData > nExePELen)
{
return -1;
}
//效验VirtualSize是否跨入下一节地界
if(pISH != pOldEndISH)
{
if(pISH->VirtualAddress + pISH->Misc.VirtualSize > (pISH + 1)->VirtualAddress)
{
//出现非法节
return -1;
}
}
//不管对不对齐均重算PE节磁盘地址,磁盘中大小信息
nPointerToRawData = pISH->PointerToRawData & (0xffffffff ^ lFileAlignMask);
nSizeOfRawData = (pISH->SizeOfRawData + lFileAlignMask) & (0xffffffff ^ lFileAlignMask);
//nSizeOfRawData要跟磁盘中剩余长度比较,取小者
if(nSizeOfRawData > nExePELen - nPointerToRawData)
{
nSizeOfRawData = nExePELen - nPointerToRawData;
}
if(pISH != pOldEndISH)
{
//发现这个VirtualSize没有用,用0程序也可以运行,这里效验如下
nVirtualSize = (pISH + 1)->VirtualAddress - pISH->VirtualAddress;
//nSizeOfRawData大于nVirtualSize也不行
if(nSizeOfRawData > nVirtualSize)
{
return -1;
}
}
//如果要load的话,是用nSizeOfRawData/nPointerToRawData
}
return 0;
}
【 总结 】
1.VirtualSize可以不对齐,但是它表示的值不能跨到其它节地界
2.VirtualAddress在任何节中也都要对齐
3.SizeOfRawData没有对齐要求,如不对齐,按这个公式重算:SizeOfRawData = (SizeOfRawData + FileAlignmentMask) & (0xffffffff ^ FileAlignmentMask);
4.PointerToRawData没有对齐要求,如不对齐,按这个公式重算:PointerToRawData = PointerToRawData & (0xffffffff ^ FileAlignmentMask);
[课程]Android-CTF解题方法汇总!
上传的附件: