标准:《Microsoft可移植可执行文件和通用目标文件格式文件规范》,简称《PE COFF文件规范》
注意:这里的LIB指的是静态库,要和编写DLL所生成的lib区别开来
《PE COFF文件规范》 中称静态库格式为:
档案(库)文件格式,在WinNT.h中称Archive format.
《PE COFF文件规范》 中称另一种Lib为:
导入库格式-是描述由一个映像导出供其它映像使用的符号的库
具体思路:
1.解析静态库(Lib)文件,提取出其中的所有目标文件成员(obj)
2.解析各个目标文件成员(obj),提取出其中的函数信息(函数名,函数数据,函数数据大小)
3.把提取出的函数信息,组织成自定义文件格式(flb)
函数库(.flb)文件格式: 签名-函数头表-函数名称段-函数数据段
一.解析静态库(Lib)文件,提取出其中的所有目标文件成员(obj)
1.静态库(Lib)文件结构
档案(库)文件(.lib)结构非常简单,就是签名+很多个成员,详细参见《PE COFF文件规范》第7节,或者看这里:http://dev.csdn.net/htmls/21/21543.html
签名,WinNT.h是这么定义的:
成员,是由头部和成员内容组成;WinNT.h是这么定义头部的:
三个特别成员:第一链接器成员,第二链接器成员,长名称成员;(签名后面紧跟着的,就是这三个特别成员)
对于两个链接器成员,WinNT.h是这么定义它们头部的Name域的:
对于长名称成员,WinNT.h是这么定义它们头部的Name域的:
目标文件成员(重点),三个特别成员之后直到文件结束,都是由这种目标文件成员组成。解析lib文件的本质就是解析它们,对于它们的头部Name域,有两种形式:
1.“名称/”
2.“/n” 当名称大小大于Name域的16个字节的话,就会存在长名称成员中;而n(十进制)就给出其名称位于长名称成员中的偏移
目标文件成员的内容,就是标准的目标文件(COFF格式)。
2.定位目标文件成员
解析lib文件的本质就是解析它们,它们是标准的目标文件格式,所有的函数信息都这里面;定位它们,是CLibParser这个类的工作。具体工作分为两步:
1.定位第一个目标文件成员。
2.遍历所有目标文件成员。
具体代码:
1.定位第一个目标文件成员
2.遍历所有目标文件成员:
二.解析各个目标文件成员(obj),提取出其中的函数信息(函数名,函数数据,函数数据大小)
1.目标文件(.obj)结构:
文件头(IMAGE_FILE_HEADER)
节表(IMAGE_SECTION_HEADER)
。。。
节数据
。。。
重定位表(IMAGE_RELOCATION),几乎每个节都有一张重定位表
符号表(IMAGE_SYMBOL),目标文件的中心,所有工作都围绕这张表展开
字符串表,一些符号的名称太长(超过8个字节),其名称就会存在这里
2.解析目标文件(obj)
目标文件的核心是符号表,整个解析工作就是围绕它展开。CObjParser负责解析目标文件(.obj)
具体步骤:
1.定位符号表,过滤出其中的函数符号
2.根据函数符号获得函数名称,输出到.nam中间文件,并记录下相应的偏移和大小
3.根据函数符号获得函数数据,输出到.dat中间文件,并记录下相应的偏移和大小
4.根据函数符号对重定位信息进行标志,在以后检测的时候,会跳过这些信息
1.定位符号表,过滤出其中的函数符号
《PE COFF文件规范》中对函数符号式这么描述的:存储类别为EXTERNAL(2)、Type 域的值为(0x20)以及SectionNumber 域的值大于0,表明它是一个函数
具体方法:
2根据函数符号获得函数名称,输出到.nam中间文件,并记录下相应的偏移和大小
3.根据函数符号获得函数数据,输出到.dat中间文件,并记录下相应的偏移和大小
这就是我上面提到的那个问题,函数数据的size确定问题,我现在的方法是:直接断定pSymbol所在节从pSymbol->Value偏移处开始到节结束都是pSymbol所对应的函数数据,但如果一个节中包含多个函数数据,这种方法就有问题了,现在的一些测试还没遇到问题。但这种方法显然不严谨,《PE COFF文件规范》里提到一种:
“5.5.1 辅助符号表记录格式之一:函数定义”;里面可以拿到size;
但悲剧的是每个函数符号后面一张辅助符号表都没有,所以这种方法流产了
我到现在也找不到好方法,望有牛人提点!
下面是具体方法:
4.根据函数符号对重定位信息进行标志,在以后检测的时候,会跳过这些信息
关于重定位信息,我是这么理解的,比如在一个函数数据里有这样一条命令
Call 0x0040124a,而obj里此数据可能是这样的Call 0x00000000,要到链接的时候,根据重定位信息再进行修订
所以我直接用0x00000000标志这里是重定位信息,以后检测的时候,直接跳过这些重定位信息,不比较。
三.自定义文件格式函数库文件(.flb)
自动义函数库文件(.flb)结构非常简单:
签名:
函数头表:
函数名称段,里面存的是所有函数名,是一张以null结尾的字符串表
函数数据段,里面存的是所有函数的二进制数据
组织函数库文件
1.定义一个全局的函数头表,在解析每个函数符号的时候,记录相应的偏移和大小
2.将所有的函数名写入一个.nam中间文件
3.将所有的函数数据写入一个.dat中间文件
4.最后修订好偏移,按照顺序将 函数头表,.nam文件内容,.dat文件内容,写入最终的flb文件中去
至此,所有解析工作完成,剩下的就是测试flb文件是否正确!
注意:这里的LIB指的是静态库,要和编写DLL所生成的lib区别开来
《PE COFF文件规范》 中称静态库格式为:
档案(库)文件格式,在WinNT.h中称Archive format.
《PE COFF文件规范》 中称另一种Lib为:
导入库格式-是描述由一个映像导出供其它映像使用的符号的库
具体思路:
1.解析静态库(Lib)文件,提取出其中的所有目标文件成员(obj)
2.解析各个目标文件成员(obj),提取出其中的函数信息(函数名,函数数据,函数数据大小)
3.把提取出的函数信息,组织成自定义文件格式(flb)
函数库(.flb)文件格式: 签名-函数头表-函数名称段-函数数据段
一.解析静态库(Lib)文件,提取出其中的所有目标文件成员(obj)
1.静态库(Lib)文件结构
档案(库)文件(.lib)结构非常简单,就是签名+很多个成员,详细参见《PE COFF文件规范》第7节,或者看这里:http://dev.csdn.net/htmls/21/21543.html
签名,WinNT.h是这么定义的:
1
2
|
#define IMAGE_ARCHIVE_START_SIZE 8
#define IMAGE_ARCHIVE_START "!<arch>\n"
|
成员,是由头部和成员内容组成;WinNT.h是这么定义头部的:
1
2
3
4
5
6
7
8
9
10
|
typedef struct _IMAGE_ARCHIVE_MEMBER_HEADER {
BYTE Name[16];
//
File member name - `/' terminated.
BYTE Date[12];
//
File member
date
- decimal.
BYTE UserID[6];
//
File member user
id
- decimal.
BYTE GroupID[6];
//
File member group
id
- decimal.
BYTE Mode[8];
//
File member mode - octal.
BYTE Size[10];
//
File member size – 头部大小不计算在内.
BYTE EndHeader[2];
//
String to end header.
} IMAGE_ARCHIVE_MEMBER_HEADER, *PIMAGE_ARCHIVE_MEMBER_HEADER;
#define IMAGE_SIZEOF_ARCHIVE_MEMBER_HDR 60
|
三个特别成员:第一链接器成员,第二链接器成员,长名称成员;(签名后面紧跟着的,就是这三个特别成员)
对于两个链接器成员,WinNT.h是这么定义它们头部的Name域的:
1
|
#define IMAGE_ARCHIVE_LINKER_MEMBER "/ "
|
对于长名称成员,WinNT.h是这么定义它们头部的Name域的:
1
|
#define IMAGE_ARCHIVE_LONGNAMES_MEMBER "// "
|
目标文件成员(重点),三个特别成员之后直到文件结束,都是由这种目标文件成员组成。解析lib文件的本质就是解析它们,对于它们的头部Name域,有两种形式:
1.“名称/”
2.“/n” 当名称大小大于Name域的16个字节的话,就会存在长名称成员中;而n(十进制)就给出其名称位于长名称成员中的偏移
目标文件成员的内容,就是标准的目标文件(COFF格式)。
2.定位目标文件成员
解析lib文件的本质就是解析它们,它们是标准的目标文件格式,所有的函数信息都这里面;定位它们,是CLibParser这个类的工作。具体工作分为两步:
1.定位第一个目标文件成员。
2.遍历所有目标文件成员。
具体代码:
1.定位第一个目标文件成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
PBYTE CLibParser::GetFirstObjSection()
{
int iCtrl=0;
//
第一个链接器成员
PBYTE pSect = m_pLibImage+IMAGE_ARCHIVE_START_SIZE;
if
(!pSect)
return
NULL;
while
(pSect)
{
//
第二个链接器成员
if
(memcmp(((PIMAGE_ARCHIVE_MEMBER_HEADER)pSect)->Name,IMAGE_ARCHIVE_LINKER_MEMBER,16)==0)
{
//Nothing
}
//
第三个长名称成员
else
if
(memcmp(((PIMAGE_ARCHIVE_MEMBER_HEADER)pSect)->Name,IMAGE_ARCHIVE_LONGNAMES_MEMBER,16)==0)
//LONG
Name
{
//Nothing
//
尽管长名称成员的头部必须存在,但它本身却可以为空。
}
else
//
第一个目标文件成员
{
return
pSect;
}
//
注意BYTE Size[10];要用atol((LPSTR)..)这种方法才能得到正确size
PIMAGE_ARCHIVE_MEMBER_HEADER pAME=(PIMAGE_ARCHIVE_MEMBER_HEADER)pSect;
pSect += atol((LPSTR)pAME->Size) + sizeof(IMAGE_ARCHIVE_MEMBER_HEADER);
//
两个成员之间有可能是由\n隔开
if
(*pSect==
'\n'
) pSect++;
iCtrl++;
//
防止遇到错误的Lib文件,而死在这里面
if
(iCtrl>3)
{
break
;
}
}
return
NULL;
}
|
2.遍历所有目标文件成员:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
BOOL CLibParser::ParseObjs(PBYTE pObjSect)
{
do
{
PIMAGE_ARCHIVE_MEMBER_HEADER pAME=(PIMAGE_ARCHIVE_MEMBER_HEADER)pObjSect;
pObjSect+=sizeof(IMAGE_ARCHIVE_MEMBER_HEADER);
//
去掉头部,剩下的就是Obj(COFF格式)
//
判断是否是导入库格式,以防止错误的把导入库lib当做静态库lib,而程序直接挂掉
if
(bImportlibraryFormat(pObjSect))
{
MessageBox(NULL,
"This is not a Archive Format File,it's a Import Format File!"
,
"WARNING"
,MB_ICONWARNING);
return
FALSE;
}
//
解析目标成员(OBJ)
CObjParser objParser;
objParser.Parse(pObjSect,m_pNameFile,m_pDataFile,&m_FuncTable);
//
注意:BYTE Size[10];要用atol((LPSTR)..)这种方法才能得到正确size
pObjSect += atol((LPSTR)pAME->Size) ;
//
注意:两个成员之间有可能是由\n隔开,《PE COFF 文件格式》中并没有提到
if
(*pObjSect==
'\n'
)
pObjSect++;
}
while
(pObjSect<m_pLibImage+m_fsize);
return
TRUE;
}
|
二.解析各个目标文件成员(obj),提取出其中的函数信息(函数名,函数数据,函数数据大小)
1.目标文件(.obj)结构:
文件头(IMAGE_FILE_HEADER)
1
2
3
4
5
6
7
8
9
|
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
//
指向符号表
DWORD NumberOfSymbols;
//
符号表的大小
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
|
节表(IMAGE_SECTION_HEADER)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
//
指定节大小
DWORD PointerToRawData;
//
指向节数据
DWORD PointerToRelocations;
//
指向此节重定位信息表
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
//
此节重定位信息表的大小
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
|
。。。
节数据
。。。
重定位表(IMAGE_RELOCATION),几乎每个节都有一张重定位表
1
2
3
4
5
6
7
8
|
typedef struct _IMAGE_RELOCATION {
union {
DWORD VirtualAddress;
//
在相应节中的偏移
DWORD RelocCount; };
DWORD SymbolTableIndex;
//
此重定位信息的符号表索引
WORD Type;
} IMAGE_RELOCATION;
typedef IMAGE_RELOCATION UNALIGNED *PIMAGE_RELOCATION;
|
符号表(IMAGE_SYMBOL),目标文件的中心,所有工作都围绕这张表展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
typedef struct _IMAGE_SYMBOL {
union {
BYTE ShortName[8];
struct {
DWORD Short;
//
如果是长名称,这四个字节为0
DWORD Long;
//
指定长名称位于字符串表中的偏移
} Name;
DWORD LongName[2];
//
PBYTE [2]
} N;
DWORD Value;
//
如果此符号是函数符号,给出函数数据在相应节中的偏移
SHORT SectionNumber;
//
此符号相应节位于节表的索引,从1开始
WORD Type;
//
如果是函数符号,此值为0x20
BYTE StorageClass;
//
如果是函数符号,此值为
//IMAGE_SYM_CLASS_EXTERNAL
(2)
BYTE NumberOfAuxSymbols;
//
辅助符号表的个数,如果有辅助符号表,
//
紧跟着每个符号表后面,一般是0-1张辅助符号表
} IMAGE_SYMBOL;
typedef IMAGE_SYMBOL UNALIGNED *PIMAGE_SYMBOL;
|
字符串表,一些符号的名称太长(超过8个字节),其名称就会存在这里
2.解析目标文件(obj)
目标文件的核心是符号表,整个解析工作就是围绕它展开。CObjParser负责解析目标文件(.obj)
具体步骤:
1.定位符号表,过滤出其中的函数符号
2.根据函数符号获得函数名称,输出到.nam中间文件,并记录下相应的偏移和大小
3.根据函数符号获得函数数据,输出到.dat中间文件,并记录下相应的偏移和大小
4.根据函数符号对重定位信息进行标志,在以后检测的时候,会跳过这些信息
1.定位符号表,过滤出其中的函数符号
《PE COFF文件规范》中对函数符号式这么描述的:存储类别为EXTERNAL(2)、Type 域的值为(0x20)以及SectionNumber 域的值大于0,表明它是一个函数
具体方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//
遍历符号表,过滤出其中的函数符号
for
(DWORD i=0;i<m_pFileHeader->NumberOfSymbols;i++)
{
pSymbol=m_pSymbol+i;
//
存储类别为EXTERNAL()、Type 域的值表明它是一个函数(x20)
//
以及SectionNumber 域的值大于,它就标志着函数的开头
if
(ISFCN(pSymbol->Type)&&pSymbol->SectionNumber>0
&&pSymbol->StorageClass==IMAGE_SYM_CLASS_EXTERNAL)
{
memset(&funcHeader,0,sizeof(funcHeader));
GetNameofSymb(pSymbol,funcHeader);
GetDataofSymb(pSymbol,funcHeader);
m_pFuncTable->push_back(funcHeader);
}
//
直接跳过辅助符号表
i+=pSymbol->NumberOfAuxSymbols;
}
|
2根据函数符号获得函数名称,输出到.nam中间文件,并记录下相应的偏移和大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
void CObjParser::GetNameofSymb(PIMAGE_SYMBOL pSymbol,FuncHeader& funcHeader)
{
PCHAR pName=NULL;
CHAR shortNam[9]={0};
//
如果符号名称长度不超过个字节,那么符号表的ShortName 域
//
就是包含符号名本身的一个字节长的数组;
if
(pSymbol->N.Name.Short)
{
//pName
= (PCHAR)pSymbol->N.ShortName;
//
注意:符号名可能正好占满个字节,那就没有NULL结束符了,
//
所以不能简单的用上面的方法
memcpy_s(shortNam,9,pSymbol->N.ShortName,8);
pName=shortNam;
}
//
否则的话,它给出了字符串表中的一个偏移地址
else
{
pName= m_pStrings+pSymbol->N.Name.Long;
}
//
记录偏移
if
(m_pFuncTable->size()==0)
{
funcHeader.NameOff=0;
}
else
{
FuncHeader& funcHeadPrev=m_pFuncTable->at(m_pFuncTable->size()-1);
funcHeader.NameOff=funcHeadPrev.NameOff+funcHeadPrev.NameSize;
}
//
记录大小
funcHeader.NameSize=strlen(pName)+1;
//
写入nam文件
fwrite(pName,funcHeader.NameSize,1,m_pNamFile);
fflush(m_pNamFile);
}
|
3.根据函数符号获得函数数据,输出到.dat中间文件,并记录下相应的偏移和大小
这就是我上面提到的那个问题,函数数据的size确定问题,我现在的方法是:直接断定pSymbol所在节从pSymbol->Value偏移处开始到节结束都是pSymbol所对应的函数数据,但如果一个节中包含多个函数数据,这种方法就有问题了,现在的一些测试还没遇到问题。但这种方法显然不严谨,《PE COFF文件规范》里提到一种:
“5.5.1 辅助符号表记录格式之一:函数定义”;里面可以拿到size;
但悲剧的是每个函数符号后面一张辅助符号表都没有,所以这种方法流产了
我到现在也找不到好方法,望有牛人提点!
下面是具体方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
//
获得函数数据
void CObjParser::GetDataofSymb(PIMAGE_SYMBOL pSymbol,FuncHeader& funcHeader)
{
PIMAGE_SECTION_HEADER pISH = m_pSectionHeader+(pSymbol->SectionNumber-1);
//SectionNumber
从开始的索引
if
(!pISH)
{
MessageBox(NULL,
"Get SectionHeader Error!"
,
"Error"
,MB_ICONWARNING);
return
;
}
//
记录偏移
if
(m_pFuncTable->size()==0)
{
funcHeader.DataOff=0;
}
else
{
FuncHeader& funcHeadPrev=m_pFuncTable->at(m_pFuncTable->size()-1);
funcHeader.DataOff=funcHeadPrev.DataOff+funcHeadPrev.DataSize;
}
//
记录大小
//
这种记算函数大小的方法并准确,这样做是默认这个pSymbol所在节从pSymbol->Value偏移处开始到
//
节结束都是pSymbol所对应的函数数据,但有可能此节还包括其他函数数据
//
拿节大小SizeOfRawData-函数在此节的偏移Value
funcHeader.DataSize=pISH->SizeOfRawData-pSymbol->Value;
//
标志重定位位置
MarkRelocatePos(pISH);
//
获取函数数据
PBYTE funData=m_pObjImage+pISH->PointerToRawData+pSymbol->Value;
//
写入dat文件
fwrite(funData,funcHeader.DataSize,1,m_pDatFile);
fflush(m_pDatFile);
}
|
4.根据函数符号对重定位信息进行标志,在以后检测的时候,会跳过这些信息
关于重定位信息,我是这么理解的,比如在一个函数数据里有这样一条命令
Call 0x0040124a,而obj里此数据可能是这样的Call 0x00000000,要到链接的时候,根据重定位信息再进行修订
所以我直接用0x00000000标志这里是重定位信息,以后检测的时候,直接跳过这些重定位信息,不比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//
标志重定位信息
void CObjParser::MarkRelocatePos(PIMAGE_SECTION_HEADER pISH)
{
//
用四个字节0标志重定位信息位置
DWORD pReloMark=0;
DWORD modifyOff=0;
//
获得重定位表
PIMAGE_RELOCATION pIR = (PIMAGE_RELOCATION)(m_pObjImage + pISH->PointerToRelocations);
//
重定位表大小
DWORD RefCount = pISH->NumberOfRelocations;
//
遍历重定位表
for
(DWORD i =0;i<RefCount;i++)
{
//
待重定位偏移
modifyOff=pISH->PointerToRawData+pIR[i].VirtualAddress;
//
修订
memcpy_s(m_pObjImage+modifyOff,4,&pReloMark,4);
}
}
|
三.自定义文件格式函数库文件(.flb)
自动义函数库文件(.flb)结构非常简单:
签名:
1
2
|
#define IMAGE_FLIB_START_SIZE 8
#define IMAGE_FLIB_START "!<flib>\n"
|
函数头表:
1
2
3
4
5
6
|
typedef struct _FlibFuncHeader
//flib
文件中的函数头结构
{
DWORD NameOff;
//
函数名称的文件偏移
DWORD DataOff;
//
函数数据的文件偏移
DWORD DataSize;
//
函数数据的大小
}FlibFuncHeader,*PFlibFuncHeader;
|
函数名称段,里面存的是所有函数名,是一张以null结尾的字符串表
函数数据段,里面存的是所有函数的二进制数据
组织函数库文件
1.定义一个全局的函数头表,在解析每个函数符号的时候,记录相应的偏移和大小
2.将所有的函数名写入一个.nam中间文件
3.将所有的函数数据写入一个.dat中间文件
4.最后修订好偏移,按照顺序将 函数头表,.nam文件内容,.dat文件内容,写入最终的flb文件中去
至此,所有解析工作完成,剩下的就是测试flb文件是否正确!