标签归档: C++

用Visual C++操作INI文件

在我们写的程序当中,总有一些配置信息需要保存下来,以便完成程序的功能,最简单的办法就是将这些信息写入INI文件中,程序初始化时再读入.具体应用如下:

  一.将信息写入.INI文件中.

  1.所用的WINAPI函数原型为:

BOOL WritePrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpString,
LPCTSTR lpFileName
);

  其中各参数的意义:

   LPCTSTR lpAppName 是INI文件中的一个字段名.

   LPCTSTR lpKeyName 是lpAppName下的一个键名,通俗讲就是变量名.

   LPCTSTR lpString 是键值,也就是变量的值,不过必须为LPCTSTR型或CString型的.

   LPCTSTR lpFileName 是完整的INI文件名.

  2.具体使用方法:设现有一名学生,需把他的姓名和年龄写入 c:studstudent.ini 文件中.

CString strName,strTemp;
int nAge;
strName="张三";
nAge=12;
::WritePrivateProfileString("StudentInfo","Name",strName,"c:\stud\student.ini");

  此时c:studstudent.ini文件中的内容如下:

   [StudentInfo]
Name=张三

  3.要将学生的年龄保存下来,只需将整型的值变为字符型即可:

strTemp.Format("%d",nAge);
::WritePrivateProfileString("StudentInfo","Age",strTemp,"c:\stud\student.ini");
二.将信息从INI文件中读入程序中的变量.

  1.所用的WINAPI函数原型为:

DWORD GetPrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpDefault,
LPTSTR lpReturnedString,
DWORD nSize,
LPCTSTR lpFileName
);

  其中各参数的意义:

   前二个参数与 WritePrivateProfileString中的意义一样.

   lpDefault : 如果INI文件中没有前两个参数指定的字段名或键名,则将此值赋给变量.

   lpReturnedString : 接收INI文件中的值的CString对象,即目的缓存器.

   nSize : 目的缓存器的大小.

   lpFileName : 是完整的INI文件名.

  2.具体使用方法:现要将上一步中写入的学生的信息读入程序中.

CString strStudName;
int nStudAge;
GetPrivateProfileString("StudentInfo","Name","默认姓名",strStudName.GetBuffer(MAX_PATH),MAX_PATH,"c:\stud\student.ini");

  执行后 strStudName 的值为:"张三",若前两个参数有误,其值为:"默认姓名".

  3.读入整型值要用另一个WINAPI函数:

UINT GetPrivateProfileInt(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
INT nDefault,
LPCTSTR lpFileName
);

  这里的参数意义与上相同.使用方法如下:

nStudAge=GetPrivateProfileInt("StudentInfo","Age",10,"c:\stud\student.ini");
三.循环写入多个值,设现有一程序,要将最近使用的几个文件名保存下来,具体程序如下:

  1.写入:

CString strTemp,strTempA;
int i;
int nCount=6;
file://共有6个文件名需要保存
for(i=0;i {strTemp.Format("%d",i);
strTempA=文件名;
file://文件名可以从数组,列表框等处取得.
::WritePrivateProfileString("UseFileName","FileName"+strTemp,strTempA,
"c:\usefile\usefile.ini");
}
strTemp.Format("%d",nCount);
::WritePrivateProfileString("FileCount","Count",strTemp,"c:\usefile\usefile.ini");
file://将文件总数写入,以便读出.

  2.读出:

nCount=::GetPrivateProfileInt("FileCount","Count",0,"c:\usefile\usefile.ini");
for(i=0;i {strTemp.Format("%d",i);
strTemp="FileName"+strTemp;
::GetPrivateProfileString("CurrentIni",strTemp,"default.fil", strTempA.GetBuffer(MAX_PATH),MAX_PATH,"c:\usefile\usefile.ini");

file://使用strTempA中的内容.

}

  补充四点:

   1.INI文件的路径必须完整,文件名前面的各级目录必须存在,否则写入不成功,该函数返回 FALSE 值.

为在VC++中, \ 才表示一个 .

   3.也可将INI文件放在程序所在目录,此时 lpFileName 参数为: ".\student.ini".

 

Read: 747

PE 格式学习总结(四)– PE文件中的资源

    程序所用到的各种资源,比如 bmp,cursor,menu,对话框等都存在PE文件中。
    我们将详细介绍关于资源的各种结构,通过一个例子来说明资源及其相关结构是怎么放在PE文件中的。以及如何在遍历PE文件中的所有资源。我们只最终找到这些资源在文件中的位置和长度。而不具体分析某种资源的格式,比如有个BMP的资源,我们不分析BMP格式。

一 找到资源在文件中位置。

    资源都放在PE文件的某个节中,该节的节表项中的PointerToRawData,就是资源节在文件中的位置。

1.1 得到PE Header在文件中的位置。
    通过DOS Header结构的成员e_lfanew,可以确定PE Header的在文件中的位置。

1.2 得到文件中节的数目。
    确定PE Header的在文件中的位置之后,就可以确定PE Header中的成员FileHeader和成员OptionalHeader在文件中的位置。根据 FileHeader 中的 成员NumberOfSections 的值,就可以确定文件中节的数目,也就是节表数组中元素的个数。

1.3 得到节表在文件中的位置。
    PE Header在文件中的位置加上PE Header结构的大小就可以得到节表在文件中的开始位置。PE Header结构的大小可以由Signature的大小加上FileHeader的大小再加上FileHeader中的 SizeOfOptionalHeade来确定。其实到目前为止SizeOfOptionalHeade也就是结构Optional Header的大小也是固定的,所以整个PE Header结构的大小也是固定。不过为了安全起见,还是用Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定比较保险。

1.4 得到资源节在文件中的位置。
    第1.2步中我们确定了文件中节的数目,第1.3步中我们确定了节表在文件中的位置。
    现在有两种方法来确定资源在文件中的位置。
    第一种方法,根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
比较每一个节表项的Name字段,看是否等于".rsrc"。如果等于。就找到了资源节的节表项。
这个节表项中的 PointerToRawData 中的值,就是资源节在文件中的位置。
    第二种方法,取得PE Header中的Optional Header中的DataDirectory数组中的第三项,
也就是资源项。DataDirectory[]数组的每项都是IMAGE_DATA_DIRECTORY结构,该结构定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
取得DataDirectory数组中的第三项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。
然后根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),
到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。
我们遍历整个节表,看我们取得的资源节的RVA,在哪个节表项的RVA范围之内。
如果在范围之内,就找到了资源节的节表项。
这个节表项中的 PointerToRawData 中的值,就是资源节在文件中的位置。
如果这个PE文件没有资源的话,DataDirectory数组中的第三项内容为0。

这样我们就得到了资源在文件中开始的位置。

二 PE文件中的资源。

    我们已经得到了资源节在文件中的位置。
资源节最开始是一个IMAGE_RESOURCE_DIRECTORY结构。
在WINNT.H中定义如下。

typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
这个结构长度为16字节,共有6个字段。
各字段含义如下:
Characteristics: Resource flags,保留用于以后使用,目前都为0。
TimeDateStamp:资源编译器产生资源的时间。
MajorVersion:
MinorVersion:
NumberOfNamedEntries:用字符串来标示IMAGE_RESOURCE_DIRECTORY_ENTRY项的,紧跟着本结构的IMAGE_RESOURCE_DIRECTORY_ENTRY数组的成员个数。
Number of ID Entries:用整形数字来表示IMAGE_RESOURCE_DIRECTORY_ENTRY项的,紧跟着本结构的IMAGE_RESOURCE_DIRECTORY_ENTRY数组的成员个数。

    IMAGE_RESOURCE_DIRECTORY后面一定会紧跟着一个IMAGE_RESOURCE_DIRECTORY_ENTRY数组。
IMAGE_RESOURCE_DIRECTORY_ENTRY结构定义如下。

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

这个结构长度为8个字节。共有两个字段,每个字段4个字节。
根据不同情况,这两个字段的含义不一样。这个结构的定义如果看不懂的话,后面的例子一看就会明白了。
第 一个字段,当第一个字段的最高位是1的时候,表示,这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容是一个 IMAGE_RESOURCE_DIR_STRING,用里面的字符串来标明这个IMAGE_RESOURCE_DIRECTORY_ENTRY。当第一 个字段的最高位是0的时候,表示,这个DWORD的低WORD中的值作为id标明这个IMAGE_RESOURCE_DIRECTORY_ENTRY。
第二个字段,当第二个字段的最高位是1的时候,表示,还有下一层的结构。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容会是一个下一层的IMAGE_RESOURCE_DIRECTORY结构,这个请看后面的例子中的说明。
当 第二个字段的最高位是0的时候,表示,已经没有下一层的结构了。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容会是一个 IMAGE_RESOURCE_DATA_ENTRY结构,IMAGE_RESOURCE_DATA_ENTRY结构会说明资源的位置。

    标示一个IMAGE_RESOURCE_DIRECTORY_ENTRY一般都是使用id,就是一个整数。
但是也有少数的使用IMAGE_RESOURCE_DIR_STRING来标示一个IMAGE_RESOURCE_DIRECTORY_ENTRY。
IMAGE_RESOURCE_DIRECTORY_ENTRY结构定义如下。
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length;
WCHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
这个结构中将有一个Unicode的字符串,是字对齐的。所有这些用来标识的IMAGE_RESOURCE_DIR_STRING都放在一起,这个结构的长度是可变的,由第一个字段Length指明后面的Unicode字符串的长度。

    经过3层IMAGE_RESOURCE_DIRECTORY_ENTRY(一般是3层,也有可能更少些。第一层资源类型bmp,menu等等,第二层资源 名,第三层是资源的Language。)最终会找到一个IMAGE_RESOURCE_DATA_ENTRY结构,这个结构中存有相应(某资源类型,某资 源名,某资源Language)资源的位置和大小,就真正找到资源了。IMAGE_RESOURCE_DATA_ENTRY定义如下。
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;
DWORD Size;
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
这个结构长16个字节,有4个字段。
OffsetToData:这是一个内存中的RVA,要转化成文件中的位置,需要用这个值减去资源节的开始RVA,
资源节的开始RVA可以由Optional Header中的DataDirectory数组中的第三项中的VirtualAddress的值得到。
或者节表中,资源节那项中的VirtualAddress的值得到。相减之后,就可以得到相对于资源节开始的偏移。
再加上资源节在文件中的开始位置,节表中资源节那项中的PointerToRawData的值,就是资源在文件中的位置。

Size:资源的大小,以字节为单位。
CodePage:一般来说是Unicode code page。
Reserved:保留,值为0。

上面是资源各种结构的说明,知道这些结构还远远不够,下面我们通过一个例子来看如何通过这些结构找到资源。

我们的例子是Win2k中的可执行文件telnet.exe。为了防止大家版本不同,本文附带了这个PE文件。

PE文件的资源的各种结构放在一个树型结构中,这个结构一般有3层,如图4.1,就是telnet.exe中的情况。

图4.1

图中长的长方形表示一个IMAGE_RESOURCE_DIRECTORY结构,长16个字节,简称directory。
图中短的长方形表示一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,长8个字节,简称directory_entry。
图中圆圈表示一个IMAGE_RESOURCE_DATA_ENTRY结构,长16个字节,简称data_entry。

为了以后的叙述方便还给树的每一个节点起了名字,第一层的叫11,第二层的叫21,22,23,24,第三层的叫31,32,33,34,35,36,37,38,39,310,311,312。

在资源节开始处,是一个directory结构,这个结构中指明了紧跟在它后面的一个directory_entry结 构数组中的元素的个数。这个directory结构之后,紧跟着的就是那个directory_entry结构数组。他们一起组成了11。就如图4.1中 所示。其他的每个节点,21,22..31,32..312,都是这样,每个 directory结构后面紧跟directory_entry 结构数组。11中的directory_entry结构数组中的每一个元素,都存有到下一层某个节点的偏移。也就是通过directory_entry结 构数组的每个元素可以找到21,22,23,24。其他的节点中情况也是一样。图中看不到的一点是,所有的节点之间都是紧紧的挨在一起存放的,11之后紧 跟着的是21,21之后紧跟着的是22,22之后紧跟着的是23。依此类推。directory_entry结构数组中的每一个元素除了有到下一层某节点 的偏移,(是下一层的节点,还是已经到了最终的data_entry,后面详细叙述)还有一个Name或者Id字段(是Name还是Id后面详细叙述), 根据不同的层,代表的含义也不一样。第一层的每个directory_entry的这个值,代表类型。比如11的第一个directory_entry的 Id值为3,3代表icon,从这个directory_entry往下的都是都是图标了(关于不同类型值的定义,后面详细叙述)。第二层每个 directory_entry的这个值代表Name,第三层代表Language。11,21,31的左边那个data_entry,的三个值分别为 3,1,409(都是16进制),就是说是一个图标类型,Name为1h,Language为409h的资源。

下面我们来通过telnet.exe中资源节的具体内容来看,用开始讲到的寻找资源节在文件中位置的方法,我们找到了资源节在文件中的位置为00013600h。

我们为了看起来清楚,每一行是一个结构,并且每个结构的不同成员用 / 分开,例如,
一个directory结构 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 04 00  
可 以看到结构成员,Characteristics为0,TimeDateStamp为0,MajorVersion为4,(如果你不明白为什么是0004 而不是0400的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍),MinorVersion为0,NumberOfNamedEntries为0, NumberOfIdEntries为4。

一个directory_entry结构 03 00 00 00 / 30 00 00 80
可以看到结构 成员,第一个字段的第一个字节00h的二进制为00000000,最高位为0,所以低两个字节中的值为Id,Id为3。第二个字段的第一个字节80h(如 果你不明白为什么第一个字节是80h而不是30h的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍)的二进制为10000000,最高位为1 所以说明还有下一层,还没有到叶子,所以第二字段代表到下一层某个节点的偏移 OffsetToData 值为30。

一个data_entry结构 E0 23 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
可 以看到结构成员,OffsetToData为323E0h(这是一个内存中的RVA,要转化成文件中的位置,需要用这个值减去资源节的开始RVA,资源节 的开始RVA可以由Optional Header中的DataDirectory数组中的第三项中的VirtualAddress的值得到。或者节表中,资源节那项中的 VirtualAddress的值得到。相减之后,就可以得到相对于资源节开始的偏移。再加上资源节在文件中的开始位置,节表中资源节那项中的 PointerToRawData的值,就是资源在文件中的位置。),Size为130h,CodePage为4E4h,Reserved为0。

下面就是telnet.exe中的内容,可以用16进制编辑器打开附带的telnet.exe对照着看。

00013600h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 04 00
(directory结构,16字节长。图4.1中11中的directory。0个NamedEntries,4个IdEntries。)
00013610h: 03 00 00 00 / 30 00 00 80
(directory_entry 结构,8字节长。图4.1中11中的directory_entry数组第一个元素。第一个字段高位为0,说明第一个字段表示id,由于是第一层,所以类 型id为3。第二个字段高位为1,说明还有下一层,第二字段中的低31位为到图4.1中21的偏移,30+00013600h=00013630h。)
00013618h: 06 00 00 00 / 50 00 00 80
00013620h: 0E 00 00 00 / A0 00 00 80
00013628h: 10 00 00 00 / B8 00 00 80
00013630h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory21)
00013640h: 01 00 00 00 / D0 00 00 80 (d0+00013600h=000136d0h。)
00013648h: 02 00 00 00 / F0 00 00 80
00013650h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 08 00 (directory22)
00013660h: 08 00 00 00 / 10 01 00 80
00013668h: 09 00 00 00 / 30 01 00 80
00013670h: 0C 00 00 00 / 50 01 00 80
00013678h: 0D 00 00 00 / 70 01 00 80
00013680h: 10 00 00 00 / 90 01 00 80
00013688h: 11 00 00 00 / B0 01 00 80
00013690h: 12 00 00 00 / D0 01 00 80
00013698h: 39 00 00 00 / F0 01 00 80
000136a0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 01 00 / 00 00
(directory结构,16字节长。图4.1中23。1个NamedEntries,0个IdEntries。)
000136b0h: D0 03 00 80 / 10 02 00 80
(directory结构中已经表明这是一个NamedEntries,第一个字段中的高位为1,说明第一个字段中的值为一个指向IMAGE_RESOURCE_DIR_STRING结构的偏移,3D0+00013600h=000139D0h。)
000136b8h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 01 00 (directory24)
000136c8h: 01 00 00 00 / 30 02 00 80
000136d0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory31)
000136e0h: 09 04 00 00 / 50 02 00 00
(directory_entry 结构,8字节长。第一个字段高位为0,说明第一个字段表示id,由于是第三层,所以Language id为409h。第二个字段高位为0,说明已经是叶子了,第二字段中的低31位为到一个data_entry结构的偏移,250+00013600h= 00013850h。)
000136e8h: 04 08 00 00 / 60 02 00 00
000136f0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory32)
00013700h: 09 04 00 00 / 70 02 00 00
00013708h: 04 08 00 00 / 80 02 00 00
00013710h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory33)
00013720h: 09 04 00 00 / 90 02 00 00
00013728h: 04 08 00 00 / A0 02 00 00
00013730h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory34)
00013740h: 09 04 00 00 / B0 02 00 00
00013748h: 04 08 00 00 / C0 02 00 00
00013750h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory35)
00013760h: 09 04 00 00 / D0 02 00 00
00013768h: 04 08 00 00 / E0 02 00 00
00013770h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory36)
00013780h: 09 04 00 00 / F0 02 00 00
00013788h: 04 08 00 00 / 00 03 00 00
00013790h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory37)
000137a0h: 09 04 00 00 / 10 03 00 00
000137a8h: 04 08 00 00 / 20 03 00 00
000137b0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory38)
000137c0h: 09 04 00 00 / 30 03 00 00
000137c8h: 04 08 00 00 / 40 03 00 00
000137d0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory39)
000137e0h: 09 04 00 00 / 50 03 00 00
000137e8h: 04 08 00 00 / 60 03 00 00
000137f0h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory310)
00013800h: 09 04 00 00 / 70 03 00 00
00013808h: 04 08 00 00 / 80 03 00 00
00013810h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory311)
00013820h: 09 04 00 00 / 90 03 00 00
00013828h: 04 08 00 00 / A0 03 00 00
00013830h: 00 00 00 00 / 00 00 00 00 / 04 00 / 00 00 / 00 00 / 02 00 (directory312)
00013840h: 09 04 00 00 / B0 03 00 00
00013848h: 04 08 00 00 / C0 03 00 00
00013850h: E0 23 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
(data_entry 结构,16字节长,存有一个资源的RVA和大小。资源节开始处的RVA为32000。先算出该资源相对于资源开始处的偏移323E0-32000= 3E0h。再用偏移加上资源节开始处的文件偏移13600得到该资源在文件中的位置,3E0+13600=139E0h。)
00013850h: 10 25 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
00013850h: 40 26 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
00013860h: 28 29 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
00013870h: 10 2C 03 00 / 70 00 00 00 / E4 04 00 00 / 00 00 00 00
00013880h: 80 2C 03 00 / 70 00 00 00 / E4 04 00 00 / 00 00 00 00
00013890h: F0 2C 03 00 / 56 03 00 00 / E4 04 00 00 / 00 00 00 00
000138a0h: 48 30 03 00 / C0 01 00 00 / E4 04 00 00 / 00 00 00 00
000138b0h: F0 2C 03 00 / 56 03 00 00 / E4 04 00 00 / 00 00 00 00
000138c0h: 48 30 03 00 / C0 01 00 00 / E4 04 00 00 / 00 00 00 00
000138d0h: 08 32 03 00 / A8 01 00 00 / E4 04 00 00 / 00 00 00 00
000138e0h: B0 33 03 00 / F4 00 00 00 / E4 04 00 00 / 00 00 00 00
000138f0h: A4 34 03 00 / B6 00 00 00 / E4 04 00 00 / 00 00 00 00
00013900h: 5C 35 03 00 / 94 00 00 00 / E4 04 00 00 / 00 00 00 00
00013910h: F0 35 03 00 / 40 04 00 00 / E4 04 00 00 / 00 00 00 00
00013920h: 30 3A 03 00 / DC 02 00 00 / E4 04 00 00 / 00 00 00 00
00013930h: 0C 3D 03 00 / 32 02 00 00 / E4 04 00 00 / 00 00 00 00
00013940h: 40 3F 03 00 / 90 01 00 00 / E4 04 00 00 / 00 00 00 00
00013950h: D0 40 03 00 / FC 04 00 00 / E4 04 00 00 / 00 00 00 00
00013960h: CC 45 03 00 / C0 03 00 00 / E4 04 00 00 / 00 00 00 00
00013970h: 8C 49 03 00 / B6 00 00 00 / E4 04 00 00 / 00 00 00 00
00013980h: 44 4A 03 00 / 84 00 00 00 / E4 04 00 00 / 00 00 00 00
00013990h: C8 4A 03 00 / 22 00 00 00 / E4 04 00 00 / 00 00 00 00
000139a0h: EC 4A 03 00 / 22 00 00 00 / E4 04 00 00 / 00 00 00 00
000139b0h: 10 4B 03 00 / 60 03 00 00 / E4 04 00 00 / 00 00 00 00
000139c0h: 70 4E 03 00 / 60 03 00 00 / E4 04 00 00 / 00 00 00 00
000139d0h: 06 00 / 54 00 45 00 4C 00 4E 00 45 00 54 00 00 00
(IMAGE_RESOURCE_DIR_STRING结构,长度可变。第一个字段2个字节长,值为6。表明其后的Unicode字符串长度为6。第二字段是一个Unicode字符串,不包括最后的结束符,长度为6,内容是"TELNET")
000139e0h: 28 00 00 00 20 00 00 00 40 00 00 00 01 00 01 00
000139f0h: …

需要补充说明的是,每个directory后面紧跟的是directory_entry数组, directory_entry数组的每个元素,有两个字段,每个字段的高位用来判断该字段代表的含义。尤其是第二字段 OffsetToData ,如果高位为1表明还有下一层,指向另一个directory。如果高位为0,表明指向一个data_entry。directory_entry第一个 字段通常都是作为id,里面低WORD中的值,用来标示这个directory_entry,很少的情况下,第一字段保存一个到unicode字符串的偏 移(本例中000136a0h),用字符串来标示这个directory_entry。如果一个directory后两个字段都不为0的话,即后面紧跟的 directory_entry数组既有NamedEntries,又有IDEntries,那么directory_entry数组首先是 NamedEntries之后紧跟着IDEntries。一般情况下都是一般来说都是有三层,第一层中的directory_entry数组的每个元素的 id,代表不同的类型,不同类型的值在 WINGDI.H 中定义如下
#define RT_CURSOR MAKEINTRESOURCE(1)
#define RT_BITMAP MAKEINTRESOURCE(2)
#define RT_ICON MAKEINTRESOURCE(3)
#define RT_MENU MAKEINTRESOURCE(4)
#define RT_DIALOG MAKEINTRESOURCE(5)
#define RT_STRING MAKEINTRESOURCE(6)
#define RT_FONTDIR MAKEINTRESOURCE(7)
#define RT_FONT MAKEINTRESOURCE(8)
#define RT_ACCELERATOR MAKEINTRESOURCE(9)
#define RT_RCDATA MAKEINTRESOURCE(10)
#define RT_MESSAGETABLE MAKEINTRESOURCE(11)

#define DIFFERENCE 11
#define RT_GROUP_CURSOR MAKEINTRESOURCE((DWORD)RT_CURSOR + DIFFERENCE)
#define RT_GROUP_ICON MAKEINTRESOURCE((DWORD)RT_ICON + DIFFERENCE)
#define RT_VERSION MAKEINTRESOURCE(16)
#define RT_DLGINCLUDE MAKEINTRESOURCE(17)
#if(WINVER >= 0x0400)
#define RT_PLUGPLAY MAKEINTRESOURCE(19)
#define RT_VXD MAKEINTRESOURCE(20)
#define RT_ANICURSOR MAKEINTRESOURCE(21)
#define RT_ANIICON MAKEINTRESOURCE(22)
#endif /* WINVER >= 0x0400 */
#define RT_HTML MAKEINTRESOURCE(23)
也有可能有不到三层的情况,比如只有类型和Name两层,没有Language层。

我们再来看几个data_entry
00013850h: 10 25 03 00 / 30 01 00 00 / E4 04 00 00 / 00 00 00 00
00013850h: 40 26 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
00013860h: 28 29 03 00 / E8 02 00 00 / E4 04 00 00 / 00 00 00 00
可 以算出 00013850h 处的 data_entry 中,资源的文件位置为 13B10h(32510-32000+13600) 长度为 130h,所以该资源结束处的位置在文件中的 13C40 h处。而一下个data_entry (00013850h处)中,资源在文件中的位置为 13C40h (32640-32000+13600) 长度为 2E8h。我们可以看到两个资源是首尾相接的,就是说一个资源和另一个资源是紧挨在一起的,中间没有空隙,其他的资源用相同的方法计算,也可以得到同样的 结论。

总结,找到资源节开始的位置,首先是一个directory,后面紧跟着directory_entry数 组,数组的每个元素代表的资源类型不同,通过每个元素,我们可以找到第二层另一个directory,后面紧跟着directory_entry数组。这 一层的数组的每个元素代表的资源Name不同。然后我们可以找到第三层的每个directory,后面紧跟着directory_entry数组。这一层 的数组的每个元素代表的资源Language不同。然后通过每个directory_entry我们可以找到每个data_entry。通过每个 data_entry,我们就可以找到每个真正的资源。
本部分内容较为复杂,需要多阅读几遍。

三 遍历PE文件中的资源

    遍历那个树型结构,找到每个资源的方法之一是,

    一个函数,用来处理directory和它后面紧跟着的directory_entry数组。比如叫 DumpResourceDirectory(),它的参数中的一个是一个directory的地址,函数根据这个地址,得到一个directory结 构,从中得到directory_entry数组元素的个数。然后for循环遍历每个元素,对于每个元素做判断看是否已经到了叶子,也就是 directory_entry的第二个字段的高位是否为0,是1表示没有到叶子,递归调用本函数,不过传入的参数,是根据这个 directory_entry中保存的另一个directory的地址。是0表示已经到了叶子,调用另一个处理叶子的函数,传入相关地址。

   处理叶子的函数,用来处理data_entry结构,负责根据data_entry结构找到真正的资源。比如叫DumpResourceEntry(),它的参数中的一个是一个data_entry的地址。然后跟据data_entry中的值作处理。

   这样,通过递归和判断,就能遍历PE文件中所有的资源。

   用这种方法遍历图4.1中的树,顺序会是11,21,31,32,22,33,34,35,36,37,38,39,310,23,311,24,312。

    这种遍历方法的源程序,可以参考 PEDUMP – Matt Pietrek 1995 。《Windows95系统程式设计大奥秘》附书源码中有。

本文所使用的PE文件 telnet

Read: 765

PE 格式学习总结(三)– PE文件中的输入函数

    关于输入部分,我们将详细介绍关于输入函数的各种结构,通过一个例子来说明输入函数及其相关结构是怎么放在PE文件中的。以及如何在PE文件中找到这些东西。

一 找到输入部分在文件中位置。

1.1 得到PE Header在文件中的位置。
    通过DOS Header结构的成员e_lfanew,可以确定PE Header的在文件中的位置。

1.2 得到文件中节的数目。
    确定PE Header的在文件中的位置之后,就可以确定PE Header中的成员FileHeader和成员OptionalHeader在文件中的位置。根据 FileHeader 中的 成员NumberOfSections 的值,就可以确定文件中节的数目,也就是节表数组中元素的个数。

1.3 得到节表在文件中的位置。
    PE Header在文件中的位置加上PE Header结构的大小就可以得到节表在文件中的开始位置。PE Header结构的大小可以由Signature的大小加上FileHeader的大小再加上FileHeader中的 SizeOfOptionalHeade来确定。其实到目前为止SizeOfOptionalHeade也就是结构Optional Header的大小也是固定的,所以整个PE Header结构的大小也是固定。不过为了安全起见,还是用Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定比较保险。

1.4 得到输入部分在文件中的位置。
    第1.2步中我们确定了文件中节的数目,第1.3步中我们确定了节表在文件中的位置。
    现在来确定输入部分在文件中的位置。
    取得PE Header中的Optional Header中的DataDirectory数组中的第二项,
也就是输入部分项。DataDirectory[]数组的每项都是IMAGE_DATA_DIRECTORY结构,该结构定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
取得DataDirectory数组中的第二项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。
如果这个RVA的值为0表示这个PE文件中没有输入部分。
然后根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),
到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。
我们遍历整个节表,看我们取得的输入部分的RVA,在哪个节表项的RVA范围之内。
如果在范围之内,就找到了输入部分所在节的节表项。
这 个节表项中的 PointerToRawData 中的值,就是输入部分所在节在文件中的位置。这个节表项中的VirtualAddress 中的值,就是输入部分所在节在内存中的RVA。用输入部分的RVA减去输入部分所在节的RVA,就可以得到输入部分在该节内偏移。用这个偏移加上该节的在 文件中的位置,就可以得到输入部分在文件中的位置。即DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]. VirtualAddress – SectionTable[i].VirtualAddress + SectionTable[i].PointerToRawData 。

这样我们就得到了输入部分在文件中开始的位置。

二 PE文件中的输入部分。

    输入部分,如果要调用别的PE文件中的输出函数,需要那些东西呢?首先需要知道所需函数在哪个文件中,比如函数 NtRaiseHardError 就在PE文件 ntdll.dll 中。所以我们需要一个文件名。而如何找到某个函数的入口地址呢,我们还需要知道该函数的函数名,或者改函数的序号,通过这两者的任一种,我们就可以找到该 函数的入口地址(如果不知道为什么,请看

IURL PE 格式学习总结(二)– PE文件中的输出函数)。所以我们还需要函数名或者序号,这两者之一。PE文件的输入部分,有这些内容。我们还可以想到,当一个PE文件被执行的时候,它 会把所用的输入函数所在的每一个文件载入内存,并且,根据函数名或者序号,获得每一个输入函数的入口地址,存放起来,在程序执行的时候使用。还有就是,一 个可执行文件一般都使用好几个PE文件(通常是dll)的输出函数。所以需要有多个dll(就说成dll吧,提供输出函数的PE文件差不多都是dll,下 面就按dll说)的相关信息。

    前面我们已经得到了输入部分在文件中开始的位置,在输入部分的最开始,是一个IMAGE_IMPORT_DESCRIPTOR 结构数组,这个数组的最后一个元素内容全为空,标示着这个数组的结束,这个数组的每个元素,保存着一个dll的相关信息。紧跟着这个 IMAGE_IMPORT_DESCRIPTOR数组的是几个紧挨着的DWORD数组, 数组的每个元素存有函数名字符串的RVA,或者直接保存序号,每个数组的最后一项为空,标示结束。这几个数组之后,紧跟着的是dll名字的字符串和各个输 入函数名结构。

IMAGE_IMPORT_DESCRIPTOR 结构在WINNT.H中定义如下。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real datetime stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

这个结构长度为20个字节,共有5个字段。
各字段含义如下:

OriginalFirstThunk:(在WINNT.H中Characteristics这个叫法已经不对了)这里实际上保存着一个RVA,这 个RVA指向一个DWORD数组,这个数组可以叫做输入查询表。每个数组元素,或者叫一个表项,保存着一个指向函数名的RVA或者保存着一个函数的序号。
TimeDateStamp:当这个值为0的时候,表明还没有bind。不为0的话,表示已经bind过了。有关bind的内容后面介绍。
ForwarderChain:
Name:一个RVA,这个RVA指向一个ascii以空字符结束的字符串,这个字符串就是本结构对应的dll文件的名字。
FirstThunk:一个RVA,这个RVA指向一个DWORD数组,这个数组可以叫输入地址表。如果bind了的话,这个数组的每个元素,就是一个输入函数的入口地址。

输入查询表,就是OriginalFirstThunk所指向的那个DWORD数组,它的每一个元素是一个DWORD值,当最高位为1时,低31位 中的值,就是一个序号。当最高位为0时,这个元素的值就是一个指向一个输入函数名结构的RVA。这个数组的最后一个元素值为空,表示数组的结束。

输入函数名结构,在WINNT.H中定义如下。

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

这个结构的长度不定,有两个成员。第一个成员是一个WORD类型,长2个字节,保存着输入函数的序号。第二个成员是一个ascii字符串,这个字符 串是输入函数的名字。为了保证字对齐,可能会在ascii结束符之后再填充一个。比如,1b 01 4e 74 54 65 72 6d 69 6e 61 74 65 50 72 6f 63 65 73 73 00 00 ,如果不填充最后一个00的话,长度为21个字节,不是字对齐。所以要填充一个00。

输入地址表,就是FirstThunk所指向的那个DWORD数组,它的每一个元素是一个DWORD值。如果程序已经bind了的话,(判断依据是 TimeDateStamp,TimeDateStamp为0则没有bind)那么这里的每个元素的值,就是一个输入函数的入口地址。如果没有bind的 话,那么在本pe文件执行时,载入器会载入dll文件,获得每一个输入函数的入口地址,并填入这个输入地址表的每一项中。(这些是我猜的,大家但愿我猜对 吧)这个数组的最后一个元素值为空,表示数组的结束。

bind,从上面的介绍中可以看到,如果没有bind的话,每次pe文件被执行时,载入器都要查询一遍每个函数的入口地址,所以为了优化这一点,就有了bind,把入口点直接存在输入地址表中。

载入器会载入所需要的dll。注意一下没有bind的情况下,载入器对输入部分所要做的事情。总之,在载入之后,所需的dll(根据文件名)已经都被载入到内存。并且输入地址表中的每一个元素都是一个输入函数的入口地址了。

下面我们来看一个例子,通过例子就可以明白是怎么回事了。

我们的例子是Win2k中的exe文件csrss.exe。为了防止大家版本不同,本文附带了这个PE文件。

每个结构的不同成员用 / 分开。每行是一个结构。可以用16进制编辑器打开附带的 routetab.dll 对照着看。
括号中内容为注释。

用开始讲到的寻找输入部分在文件中位置的方法,我们找到了输入部分在文件中的位置为000008DCh。
我们来计算一下第一个IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk,Name,FirstThunk。
输入部分所在节的开始rva(由DataDirectory[2]得到)为1000h。输入部分在节在文件中的位置为600h。
Name 为rva(值从结构中可以看到是0000135e,如果你不明白为什么是0000135e而不是5e130000的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍),则Name相对于所在节开始处的偏移为135e-1000。而Name在文件中的位置为 Name在相对于所在节开始的偏移加上所在节开始处在文件中的位置。所以Name在文件中的位置为135eh-1000h+600h=95eh。同样方法 我们可以算出, OriginalFirstThunk:
1318-1000+600=918。FirstThunk:1000-1000+600=600。

000008DC: 18 13 00 00 / ff ff ff ff / ff ff ff ff / 5e 13 00 00 / 00 10 00 00
(结构IMAGE_IMPORT_DESCRIPTOR,每个代表一个dll。可以看到两个IMAGE_IMPORT_DESCRIPTOR,所以本PE文件的输入函数,是由两个dll提供的。第三个全为空,表示结束。)
000008F0: 20 13 00 00 / ff ff ff ff / ff ff ff ff / c2 13 00 00 / 08 10 00 00
(结构IMAGE_IMPORT_DESCRIPTOR)
00000904: 00 00 00 00 / 00 00 00 00 / 00 00 00 00 / 00 00 00 00 / 00 00 00 00  
(全为空,表示结束IMAGE_IMPORT_DESCRIPTOR数组结束)
00000918: 44 13 00 00 (文件中的地址为1344-1000+600=944,指向一个输入函数名结构)
0000091C: 00 00 00 00 (为空,一个输入查询表结束)
00000920: 84 13 00 00 (文件中的地址为1384-1000+600=984,指向一个输入函数名结构)
00000924: 98 13 00 00 (1398-1000+600=998)
00000928: 6a 13 00 00 (136a-1000+600=96a)
0000092C: ae 13 00 00 (13ae-1000+600=9ae)
00000930: cc 13 00 00 (13cc-1000+600=9cc)
00000934: dc 13 00 00 (13dc-1000+600=9dc)
00000938: ee 13 00 00 (13ee-1000+600=9ee)
0000093C: 0e 14 00 00 (140e-1000+600=a0e)
00000940: 00 00 00 00 (为空,一个输入查询表结束)
00000944: 18 00 / 43 73 72 53 65 72 76 65 72 49 6e 69 74 69 61 6c 69 7a 61 74 69 6f 6e 00
(输入函数名结构 IMAGE_IMPORT_BY_NAME hint为18 Name为 "CsrServerInitialization.")
0000095E: 43 53 52 53 52 56 2e 64 6c 6c 00 00
(第一个IMAGE_IMPORT_DESCRIPTOR的Name指向这里"CSRSRV.dll")
0000096A: 00 01 / 4e 74 53 65 74 49 6e 66 6f 72 6d 61 74 69 6f 6e 50 72 6f 63 65 73 73 00
("NtSetInformationProcess.")
00000984: 1c 01 / 4e 74 54 65 72 6d 69 6e 61 74 65 54 68 72 65 61 64 00
00000998: 1b 01 / 4e 74 54 65 72 6d 69 6e 61 74 65 50 72 6f 63 65 73 73 00 00
000009AE: d8 00 / 4e 74 52 61 69 73 65 48 61 72 64 45 72 72 6f 72 00 00
000009C2: 6e 74 64 6c 6c 2e 64 6c 6c 00

000009CC: 0d 00 / 44 62 67 42 72 65 61 6b 50 6f 69 6e 74 00
000009DC: 4a 01 / 52 74 6c 41 6c 6c 6f 63 61 74 65 48 65 61 70 00
000009EE: 85 02 / 52 74 6c 55 6e 69 63 6f 64 65 53 74 72 69
6e 67 54 6f 41 6e 73 69 53 74 72 69 6e 67 00 00
00000A0E: 30 02 / 52 74 6c 4e 6f 72 6d 61 6c 69 7a 65 50 72 6f 63 65 73 73 50 61 72 61 6d 73 00
00000A2A: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000A3A: …

00000600: 38 1f f8 5f
00000604: 00 00 00 00 (为空,一个输入地址表结束)
00000608: 6d f0 f8 77
0000060C: d8 c3 f8 77
00000610: a5 b7 f8 77
00000614: 38 a4 f9 77
00000618: df f9 f9 77
0000061C: 6b 97 fc 77
00000620: ec e5 f8 77
00000624: 18 2c f9 77
00000628: 00 00 00 00 (为空,一个输入地址表结束)

本例比较可惜的是,在两个输入查询表中,都是函数名结构的RVA,没有直接的序号(是序号还是RVA的判别方法为,看最高位是否为1,为1,其余部分表示序号。为0,整个字段表示RVA)。

三 遍历PE文件中的输入

    用while循环,遍历IMAGE_IMPORT_DESCRIPTOR数组的每个元素(每个可以找到一个dll的信息和该 dll提供的输入函数)。当某元素的值都为空时,表示遍历到了数组的最后。而对于IMAGE_IMPORT_DESCRIPTOR数组的一个元素,再用 while循环,遍历IMAGE_IMPORT_DESCRIPTOR中,两个RVA所指的两个DWORD数组,输入查询表和输入地址表。判断结束的条件 也是看,是否数组元素的值已经为空了。也就是while(..){..while(..){}..}这样就可获得每一个有关输入的内容。

    实现遍历输入的源程序,可以参考 PEDUMP – Matt Pietrek 1995 。《Windows95系统程式设计大奥秘》附书源码中有。

本文所使用的PE文件csrss.exe

Read: 732

PE 格式学习总结(二)– PE文件中的输出函数

    一般来说输出函数都是在dll中。我们将详细介绍关于输出函数的各种结构,通过一个例子来说明输出函数及其相关结构是怎么放在PE文件中的。以及如何在PE文件中找到这些东西。

一 找到输出函数在文件中位置。

1.1 得到PE Header在文件中的位置。
    通过DOS Header结构的成员e_lfanew,可以确定PE Header的在文件中的位置。

1.2 得到文件中节的数目。
    确定PE Header的在文件中的位置之后,就可以确定PE Header中的成员FileHeader和成员OptionalHeader在文件中的位置。根据 FileHeader 中的 成员NumberOfSections 的值,就可以确定文件中节的数目,也就是节表数组中元素的个数。

1.3 得到节表在文件中的位置。
    PE Header在文件中的位置加上PE Header结构的大小就可以得到节表在文件中的开始位置。PE Header结构的大小可以由Signature的大小加上FileHeader的大小再加上FileHeader中的 SizeOfOptionalHeade来确定。其实到目前为止SizeOfOptionalHeade也就是结构Optional Header的大小也是固定的,所以整个PE Header结构的大小也是固定。不过为了安全起见,还是用Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定比较保险。

1.4 得到输出函数在文件中的位置。
    第1.2步中我们确定了文件中节的数目,第1.3步中我们确定了节表在文件中的位置。
    现在来确定输出函数在文件中的位置。
    取得PE Header中的Optional Header中的DataDirectory数组中的第一项,
也就是输出函数项。DataDirectory[]数组的每项都是IMAGE_DATA_DIRECTORY结构,该结构定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
取得DataDirectory数组中的第一项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。
如果这个RVA的值为0表示这个PE文件中没有输出函数。
然后根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),
到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。
我们遍历整个节表,看我们取得的输出函数的RVA,在哪个节表项的RVA范围之内。
如果在范围之内,就找到了输出函数所在节的节表项。
这 个节表项中的 PointerToRawData 中的值,就是输出函数所在节在文件中的位置。这个节表项中的VirtualAddress 中的值,就是输出函数所在节在内存中的RVA。用输出函数的RVA减去输出函数所在节的RVA,就可以得到输出函数在该节内偏移。用这个偏移加上该节的在 文件中的位置,就可以得到输出函数在文件中的位置。即DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]. VirtualAddress – SectionTable[i].VirtualAddress + SectionTable[i].PointerToRawData 。

这样我们就得到了输出函数在文件中开始的位置。

二 PE文件中的输出函数。

    输出函数是用来给其他程序使用的。其他程序如果知道了某个输出函数的入口地址(就是实现这个函数功能的代码开始的地方),就可以转到那里去执行。一个PE 文件中,如果有有输出函数,一般都不是一个。所以有一个数组来保存每个输出函数的入口地址。在PE文件中,提供两种方法,来找到某个输出函数的入口地址。 第一种方法是通过入口地址数组序号,就是说知道是入口地址数组中的第几个元素,这样就可以得到里面的入口地址。第二种方法是通过函数名,通过比较函数名, 然后得到对应该函数名的入口地址数组的序号,从而得到该函数名的对应函数的入口地址。为了能够通过函数名得到序号,就需要一些相关的结构。具体内容后面 讲。总得来说PE文件的输出函数部分中就是这些东西。

    前面我们已经得到了输出函数部分在文件中开始的位置,在输出函数部分的最开始,是一个IMAGE_EXPORT_DIRECTORY 结构,这个结构提供很多重要的信息。这个结构的后面紧跟着的是 输出函数入口地址数组 。输出函数入口地址数组之后紧跟着的是输出函数名的指针数组。输出函数名的指针数组之后紧跟着的是输出函数名对应的序号的数组。输出函数名对应的序号的数 组之后紧跟着dll的名字和输出函数的名字。注意,他们之间是紧挨着的。并且顺序为IMAGE_EXPORT_DIRECTORY,输出函数入口地址的数 组,输出函数名的指针的数组,输出函数名对应的序号的数组。最后是dll的名字的字符串和那些输出函数名的字符串。

    先看IMAGE_EXPORT_DIRECTORY 结构,在WINNT.H中定义如下。

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

这个结构长度为40个字节,共有11个字段。
各字段含义如下:

Characteristics:一个保留字段,目前为止值为0。
TimeDateStamp:产生的时间。
MajorVersion:
MinorVersion:
Name:一个RVA,指向一个dll的名称的ascii字符串。
Base:输出函数的起始序号。一般为1。
NumberOfFunctions:输出函数入口地址的数组 中的元素个数。
NumberOfNames:输出函数名的指针的数组 中的元素个数,也是输出函数名对应的序号的数组 中的元素个数。
AddressOfFunctions:一个RVA,指向输出函数入口地址的数组。
AddressOfNames:一个RVA,指向输出函数名的指针的数组。
AddressOfNameOrdinals:一个RVA,指向输出函数名对应的序号的数组。

输出函数入口地址的数组,这个数组是一个DWORD数组,每个元素都是一个RVA,指向一个输出函数的入口地址,每个元素长4个字节。

输出函数名的指针的数组,这个数组是一个DWORD数组,每个元素都是一个RVA,指向一个输出函数名的ascii字符串,每个元素长4个字节。

输出函数名对应的序号的数组,这个数组是一个WORD数组,每个元素都是某个输出函数名函数对应的索引,这个索引是输出函数入口地址的数组的索引(已经用序号减去起始序号了),这个每个元素长2个字节。

dll名字符串和输出函数名字符串,都是ascii字符串,以空结束。一个紧挨着一个。dll名字符串的地址存在IMAGE_EXPORT_DIRECTORY 的 Name 中。输出函数名字符串 的地址存在 输出函数名的指针的数组 中。

还有要注意的是:

   输出函数入口地址的数组包含着输出函数的入口点地址,一个序号减去起始序号(起始序号就是 IMAGE_EXPORT_DIRECTORY 中的 Base),用来索引这个数组。比如,起始序号为1,要找序号为1的函数的入口地址,那么该函数的入口地址为 输出函数入口地址数组[0](0是由1-1算出来的)序号为3的函数的入口地址为   输出函数入口地址数组[2](2是由3-1算出来的)。

当载入器要修正一个函数的调用,而这个函数是用序号输入的,载入器只要用序号减去起始序号,得到输出函数入口地址的数组的索引,就可以了。
当 载入器要修正一个函数的调用,而这个函数是用函数名输入的,载入器比较输出函数名的指针的数组每个元素所指的函数名,比如在第3个元素中比较,发现相同。 载入器就会从 输出函数名对应的序号的数组 的第三个元素的值 得到该函数的序号。用这个序号就可以在象前面那样用序号得到入口地址。输出函数名的指针的数组 和 输出函数名对应的序号的数组 有相同的元素个数(IMAGE_EXPORT_DIRECTORY 中的 NumberOfNames)。并且是有所关联的,函数名指针数组的第i个元素的序号,在序号数组的第i个元素中。输出函数名的指针的数组和输出函数名对 应的序号的数组,分开成两个数组,而不是合并成一个结构体的数组(这个结构体第一个成员是指针,第二个成员是序号),是因为,那样的话数组的一个元素长6 个字节,不利于对齐。

下面我们来通过一个例子,来看上面所介绍的内容。

我们的例子是Win2k中的dll文件routetab.dll。为了防止大家版本不同,本文附带了这个PE文件。

用开始讲到的寻找输出部分在文件中位置的方法,我们找到了输出部分在文件中的位置为00001460h。

由于第一个结构IMAGE_EXPORT_DIRECTORY比较长,一行方不下,所以放了三行,结构的不同成员用 / 分开。
其他每行是一个结构。可以用16进制编辑器打开附带的 routetab.dll 对照着看。

我们来算一下 Name,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals 在文件中的位置。
输出部分的开始rva(由DataDirectory[1]得到)为1e60h。输出部分在文件中的位置为1460h。
Name 为rva(值从结构中可以看到是00001eec,如果你不明白为什么是00001eec而不是ec1e0000的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍),则Name相对于输出部分开始处的偏移为1eec-1e60。而Name在文件中的位置为 Name在相对于输出部分开始的偏移加上输出部分开始处在文件中的位置。所以Name在文件中的位置为1EEC-1E60+1460=14ECh。同样方 法我们可以算出, AddressOfFunctions:
1e88-1e60+1460=1488。AddressOfNames:1eb0- 1e60+1460=14b0 。AddressOfNameOrdinals:1ed8-1e60+1460=14d8。 从结构中还可以看到有0000000a(十进制10)个输出函数。

00001460: {00 00 00 00 / dc 5b ec 37 / 00 00 / 00 00 / ec 1e 00 00 /
00001470: 01 00 00 00 / 0a 00 00 00 / 0a 00 00 00 / 88 1e 00 00 /
00001480: b0 1e 00 00 / d8 1e 00 00 }
(我们用大括号括起来了,IMAGE_EXPORT_DIRECTORY结构,长度为40个字节)

00001488: 41 1a 00 00 (函数入口点的RVA,长4个字节)
0000148C: 64 1a 00 00
00001490: 02 18 00 00
00001494: 02 18 00 00
00001498: 71 16 00 00
0000149C: 07 16 00 00
000014A0: 26 18 00 00
000014A4: 84 1a 00 00
000014A8: 06 17 00 00
000014AC: 5b 19 00 00

000014B0: f9 1e 00 00 (函数名的指针,长4个字节,指向 1ef9-1e60+1460=14f9)
000014B4: 02 1f 00 00
000014B8: 0e 1f 00 00
000014BC: 21 1f 00 00
000014C0: 30 1f 00 00
000014C4: 42 1f 00 00
000014C8: 4d 1f 00 00
000014CC: 5b 1f 00 00
000014D0: 6c 1f 00 00
000014D4: 81 1f 00 00

000014D8: 00 00 (索引,说明的1个函数名的函数,入口地址在 地址数组[0])
(并不是每个PE文件序号数组的第0个元素值就是0,第1个元素值就是1,ntdll.dll中就不是)
000014DA: 01 00
000014DC: 02 00
000014DE: 03 00
000014E0: 04 00
000014E2: 05 00
000014E4: 06 00
000014E6: 07 00
000014E8: 08 00
000014EA: 09 00

000014EC: 52 4f 55 54 45 54 41 42 2e 64 6c 6c 00                       ROUTETAB.dll.
000014F9: 41 64 64 52 6f 75 74 65 00                                   AddRoute.
00001502: 44 65 6c 65 74 65 52 6f 75 74 65 00                          DeleteRoute.
0000150E: 46 72 65 65 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00     FreeIPAddressTable.

00001521: 46 72 65 65 52 6f 75 74 65 54 61 62 6c 65 00                 FreeRouteTable.
00001530: 47 65 74 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00        GetIPAddressTable.
00001542: 47 65 74 49 66 45 6e 74 72 79 00                             GetIfEntry.
0000154D: 47 65 74 52 6f 75 74 65 54 61 62 6c 65 00                    GetRouteTable.
0000155B: 52 65 66 72 65 73 68 41 64 64 72 65 73 73 65 73 00           RefreshAddresses.
0000156C: 52 65 6c 6f 61 64 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00                                                                      ReloadIPAddressTable.
00001581: 53 65 74 41 64 64 72 43 68 61 6e 67 65 4e 6f 74 69 66 79 45 76 65 6e 74 00
                                                                    SetAddrChangeNotifyEvent.
0000159A: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000015AA: …
000015F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

还是比较清楚的,就不再讲了。

三 遍历PE文件中的输出

    根据前面的方法得到输出部分的开始地址,最开始是一个IMAGE_EXPORT_DIRECTORY,根据这个结构中的内容,可以得到,和输出相关的三个数组的开始地址,和元素个数。用for循环可以很简单的遍历。

    实现遍历输出的源程序,可以参考 PEDUMP – Matt Pietrek 1995 。《Windows95系统程式设计大奥秘》附书源码中有。

本文所使用的PE文件routetab.dll

Read: 674

PE 格式学习总结(一)– PE文件概述

零 前言

    PE格式,是Windows的可执行文件的格式。Windows中的 exe文件,dll文件,都是PE格式。PE 就是Portable Executable 的缩写。Portable 是指对于不同的Windows版本和不同的CPU类型上PE文件的格式是一样的,当然CPU不一样了,CPU指令的二进制编码是不一样的。只是文件中各种 东西的布局是一样的。

图 1.1

    图1.1是 JIURL PEDUMP 打开 Win2K 中的 explorer.exe 的截图。JIURL PEDUMP 是我写的一个小工具,从文件开始的 Dos Header 一直到 Section Table,打开PE文件之后,点击相应结构,就会高亮显示文件中相应的部分。不过没有Sections。对了解 PE 格式有所帮助,可以很好的配合后面的介绍。可以到我的主页 http://jiurl.yeah.net 上下载。

一   PE文件格式概述

PE文件结构的总体层次分布如下所示

————–
|DOS MZ Header |
|————–|
|DOS Stub      |
|————–|
|PE Header     |
|————–|
|Section Table |
|————–|
|Section 1     |
|————–|
|Section 2     |
|————–|
|Section …   |
|————–|
|Section n     |
————–

 

1.1 DOS Header

    PE文件最开始是一个简单的 DOS MZ header,它是一个 IMAGE_DOS_HEADER 结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ Header 之后的 DOS Stub。

1.2   DOS Stub   

    DOS Stub 是一个有效的 DOS 程序。当程序在DOS下运时,输出象 "This program cannot be run in DOS mode" 这样的提示。在 图1.1中就可以看到字符串 "This program cannot be run in DOS mode"。这是编译器生成的默认stub程序。你也可以通过链接选项 /STUB:filename 指定任何有效的MS-DOS可执行文件来替换它。

1.3 PE Header

    紧接着 DOS Stub 的是 PE Header。它是一个 IMAGE_NT_HEADERS 结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量。因而 ?DOS stub 直接定位到真正的文件头 PE header。

1.4 Section Table

    PE Header 接下来的数组结构 Section Table (节表)。如果PE文件里有5个节,那么此 Section Table 结构数组内就有5个成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。图1中的节表有4个成员。

1.5 Sections

    PE文件的真正内容划分成块,称之为sections(节)。Sections 是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,我们可以找到这些节。图1.1所示的 explorer.exe 中有4个节。程序的代码,资源等等就放在这些节中。

二 PE文件格式中的结构及其作用

这部分内容请参考下面的几篇文章,使用工具 JIURL PEDUMP 有助于快速了解。
大家不要因此,而失望不看,本文重点在后三篇,本篇只是为了有个交代,和介绍些相关内容。
注意,在WINNT.H中,有所有PE相关结构的定义。我们用到的结构定义都来自那里。

Microsoft Portable Executable and Common Object File Format Specification
MSDN

《Windows95系统程式设计大奥秘》
第8章 PE 与COFF OBJ 档案格式
Matt Pietrek 着 侯杰译

Iczelion的PE教程

PE学习笔记(一) rivershan
PE学习笔记(二) rivershan

Inside Windows
An In-Depth Look into the Win32 Portable Executable File Format

Matt Pietrek
已经被人翻译了。

Inside Windows
An In-Depth Look into the Win32 Portable Executable File Format
Matt Pietrek

三 几个要注意的问题

3.1 文件中大量的空白

    在 PE Header结构 中的 OptionalHeader 结构中的成员 FileAlignment 的值是文件中节的对齐粒度,单位是字节,这个值应该是2的n次方,范围从512到64k。如果这里的值是512,那么PE文件中的节的长度都是512字节 的整数倍,内容不够的部分用0填充。比如一个PE文件的 FileAlignment 为200h(十进制512),它的第一个节在400h处,长度为100h,那么从文件400h到500h中为这一节的内容,而文件对齐粒度是200h,所 以为了使这一节长度为FileAlignment的整数倍,500h到600h会被用零填充。而下一个节的开始地址为600h。用16进制编辑器打开PE 文件,就可以看到这种情况,PE文件头的内容结束到第一个节开始之间的地方,每一个节中内容结束到下一节开始的地方都会有大量的空白。VC6编译链接时默 认的FileAlignment为1000h(4k),可以使用链接选项 /ALIGN:number 来改变这个值。比如把4k改成512时,可以明显减小生成文件的大小。

3.2 big-endian和little-endian

    PE Header中的 FileHeader 的成员 Machine 中的值,根据WINNT.H中的定义,对于 Intel CPU 应该为 0x014c。但是你用16进制编辑器打开PE文件,看到这个WORD显示的却是 4c 01 。你看到的并没有错,你看到的 4c 01 就是 0x014c,只不过由于 intel cpu 是ittle-endian,所以显示出来是这样的。对于 big-endian 和 little-endian,请看下面的例子。

比如一个整形int变量。长为四个字节。
这个变量的地址比如为n。
则这个变量的4个字节地址分别为n,n+1,n+2,n+3。

当 这个整形变量 的值为 0x12345678 时,

对于 big-endian 来说
地址n+0的那个字节中的值为 0x12
地址n+1的那个字节中的值为 0x34
地址n+2的那个字节中的值为 0x56
地址n+3的那个字节中的值为 0x78

按如下方式就会显示为
n n+1 n+2 n+3
12 34 56 78

对于 ittle-endian 来说
地址n+0的那个字节中的值为 0x78
地址n+1的那个字节中的值为 0x56
地址n+2的那个字节中的值为 0x34
地址n+3的那个字节中的值为 0x12

按如下方式就会显示为
n n+1 n+2 n+3
78 56 34 12

Intel使用的是 ittle-endian 。

一个整形 int 变量 i,的地址是&i,那么这个i的四个字节是&i,&i+1,&i+2,&i+3。
可以用这样一个程序看到。

#include <stdio.h>
#include <conio.h>

void main()
{
int i;
char* p;
p=(char*)&i;

printf("i: ");
scanf("%x",&i);
printf("n");

printf("&i+0: %xn",*p);
printf("&i+1: %xn",*(p+1));
printf("&i+2: %xn",*(p+2));
printf("&i+3: %xn",*(p+3));

printf("n");
printf("&i-4: %xn",*(p-4));
printf("&i-3: %xn",*(p-3));
printf("&i-2: %xn",*(p-2));
printf("&i-1: %xn",*(p-1));

printf("n");
printf("&i+4: %xn",*(p+4));
printf("&i+5: %xn",*(p+5));
printf("&i+6: %xn",*(p+6));
printf("&i+7: %xn",*(p+7));

getch();

}

当我们输入 12345678 的时候可以看到,输出

i: 12345678

&i+0: 78
&i+1: 56
&i+2: 34
&i+3: 12

&i-4: 7c
&i-3: ffffffff
&i-2: 12
&i-1: 0

&i+4: ffffffc0
&i+5: ffffffff
&i+6: 12
&i+7: 0
正是&i,&i+1,&i+2,&i+3这四个字节中储存了i的值。

对于int,WORD,DWORD等等都要注意 big-endian 和 little-endian 。

3.3 RVA (Relative Virtual Address) 相对虚拟地址

    RVA是一个简单的相对于PE载入点的内存偏移。比如,PE载入点为0X400000,那么代码节中的地址0X401000的RVA为(target address) 0x401000 – (load address)0x400000 = (RVA)0x1000.换句话说 RVA是0x1000,载入点为0X400000,那么该RVA的在内存中的实际地址就是0X401000。注意一下RVA是指内存中,不是指文件中。是 指相对于载入点的偏移而不是一个内存地址,只有RVA加上载入点的地址,才是一个实际的内存地址。

3.4 三种不同的地址

    PE的各种结构中,涉及到很多地址,偏移。有些是指在文件中的偏移,有的是指在内存中的偏移。一定要搞清楚,这个地址或者是偏移,是指在文件中,还是指在 内存中。第一种,文件中的地址。比如用16进制编辑器打开PE文件,看到的地址(偏移)就是文件中的地址,我们使用某个结构的文件地址,就可以在文件中找 到该结构。第二种,文件被整个映射到内存时,比如某些PE分析软件, 把整个PE文件映射到内存中,这时是内存中的地址,如果知道某一个结构在文件中的地址的话,那么这个PE文件被映射到内存之后该结构的在内存中的地址,可 以用文件中的地址加上映射内存的地址,就可以得到在该结构内存中的地址。第三种,执行PE时,PE文件会被载入器载入内存,这时经常需要的是RVA。比如 知道一个结构的RVA,那么载入点加上RVA就可以得到内存中该结构的实际地址。比如,某个程序,我们用16进制编辑器打开它,看到PE Header开始在16进制编辑器显示为000000C8的地方。于是我们在16进制编辑器显示为000000FC的地方找到了 OptionalHeader的ImageBase,值为400000h,那么当这个程序被执行时,如果内存中400000h处没有使用,该程序就会被载 入到那里。而我用CreateFileMapping将这个PE文件映射到内存中时,可以得到块内存的地址为5505024。对于映射入内存的这个PE文 件,我们就可以在内存中000000FCh+05505024h=5505120处找到这个PE的OptionalHeader的ImageBase。

3.5 几个重要结构的说明

PE Header 的 FileHeader 的 NumberOfSections:这是一个很重要的字段,用来确定文件中节的数目。

PE Header 的 OptionalHeader 的 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:一个IMAGE_DATA_DIRECTORY 结构数组。到目前为止这个数组的长度是固定的,有16个元素,这16个元素分别代表
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
每个元素是一个IMAGE_DATA_DIRECTORY结构,IMAGE_DATA_DIRECTORY定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
第一个字段是一个RVA,第二个字段是一个大小。

Section Table 节表紧跟在OptionalHeader之后,是一个IMAGE_SECTION_HEADER结构的数组。该数组中成员的个数由 File Header (IMAGE_FILE_HEADER) 结构中 NumberOfSections 域的域值来定。节表中的成员是IMAGE_SECTION_HEADER 结构,IMAGE_SECTION_HEADER 结构的长度固定,长40个字节。整个Section Table 的长度不固定,等于 NumberOfSections*sizeof(IMAGE_SECTION_HEADER)。IMAGE_SECTION_HEADER 结构中,

VirtualAddress:本节的RVA(相对虚拟地址)。
PointerToRawData:这是本节基于文件的偏移量。

3.6 DOS MZ Header 中的 MZ

    MZ是MZ格式的主要作者 Mark Zbikowski 的名字的缩写。

Read: 920