分类归档: C++

如何将一个文件分割成多个小文件

   你也许会遇到到这样一个问题?当你有一个较大的软件,而无法用一张软盘将其全部拷下时,你也许会想到该将它分解开,分盘拷回去后,再将它们合并起来。现在的这种分割工具很多,你想自己动手做一个适合自己的分割工具么?下面就让我用以前用VC做的一个<袖珍文件分割器>的例程来告诉你吧!程序运行后界面如下图:

基本构成思想:文件分割的基本思想比我之前发表的另一篇文章<如何将多个文件合并为一个可执行程序>的构成思想简单多了,它主要也分为分割文件和合并分割后的文件二大部分。分割文件,将原文件按指定分割大小进行等分,然后顺序读取其指定分割大小数据后到写到各自的新建文件中。合并文件,将各分割后的文件顺序读取后,写入到一个文件中既可。

1、分割文件时:打开文件,读取指定的分割大小一段数据,写入到一新建文件中,接着再读同样大小的一段数据,再写入到一新建文件中……,直到读出文件最后一部分数据,写入到最后一个新建文件中。对每一个分割后的新建文件名,采用原文件名前加数字信息的方法,按分割的顺序,按个加上一数字标识信息,以便合并时使用。
分割文件的部分代码实现如下:
//文件分割涵数
int CFileSpltDlg::SplitMe()
{
…… (省略:此部分代码实现省略掉)
//分割文件
do {
//动态建立一个新建文件名的前的数字
name = _ltoa(l, buff, 10);
name += _T("_");
CString newpath;

//判断选择目录未尾是否已有""符
if(m_targetpath.Right(1)==’\’)
newpath = m_targetpath;
else
newpath = m_targetpath + _T("\");
if (!destFile.Open(newpath + name + m_SourceFile.GetFileName(),
CFile::modeWrite |
CFile::shareExclusive |
CFile::typeBinary |
CFile::modeCreate, &ex)) {
TCHAR szError[1024];
ex.GetErrorMessage(szError, 1024);
::AfxMessageBox(szError);
m_SourceFile.Close();
return 1;
}
do {
dwRead = m_SourceFile.Read(buffer, nCount);
destFile.Write(buffer, dwRead);
}//当文件小于指定要分割的大小时
while (dwRead > 0 && destFile.GetLength() < newlen);
destFile.Close();

l++;
UpdateWindow();
}while (dwRead > 0);
m_SourceFile.Close();
return 0;

2、合并文件时:和上面分割所采用的方法相反,将各个分割后的小文件读出后,按其分割后文件名前数字大小的顺序,按个写入到新建的文件中,这一新建文件的名字,为去掉分割后文件前面数字部分后的文件名(既原文件名)。
合并文件的部分代码实现如下:
// 文件合并涵数
int CFileSpltDlg::MergeMe()
{
…… (省略:此部分代码实现省略掉)
//开始合并文件
do {
//自动定位分割文件名前的数字信息
pref = _ltoa(l, buff, 10);
pref += _T("_");
//打开新的分割文件
if (!m_SourceFile.Open(newpath + pref + m_filename,
CFile::modeRead |
CFile::shareExclusive |
CFile::typeBinary, &ex)) {
TCHAR szError[1024];
ex.GetErrorMessage(szError, 1024);
destFile.Close();
m_path = _T("");
m_filename = _T("");
newpath = _T("");
UpdateData(FALSE);
return 0;
}
else
//形成一个新的文件名
name = _T(newpath + pref + m_filename);
do {//写入到目标文件中
dwRead = m_SourceFile.Read(buffer, nCount);
destFile.Write(buffer, dwRead);
}while (dwRead > 0);

m_SourceFile.Close();

l++;
UpdateWindow();
}while (l < 500);//little bit dirty solution, but you can always improve it!…

return 0;
}

以上各部分代码的具体实现,请在下载例程后,参看源代码既可。

联系方式:
地址:陕西省西安市劳动路2号院六单元
邮编:710082
作者EMAIL:jingzhou_xu@163.net
如何将一个文件分割成多个小文件

Read: 762

用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: 762

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: 672