解析静态库(Lib)文件,提取出所有函数信息,组织成自定义格式文件

标准:《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是这么定义的:
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文件是否正确!
 
赞赏
 
二维码
最新回复 ( 52)
 1
某某  2010-6-28 18:26
3 楼
一.解析.flb文件
1.加载.flb文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 加载flb文件
     FILE* pFile;
     fopen_s(&pFile,szLib, "rb" );
     if  (!pFile)
     {
         CHAR ErrorStr[MAX_PATH];
         sprintf_s(ErrorStr,MAX_PATH, "Can't Open %s" ,szLib);
         MessageBox(NULL,ErrorStr, "Error" ,MB_ICONWARNING);
         return  FALSE;
     }
     int len=_filelength(_fileno(pFile));
     m_pImage=(PBYTE)malloc(len);
     fread_s(m_pImage,len,len,1,pFile);
     fclose(pFile);

2.检测签名
1
2
3
4
5
// 检测签名
     if  (memcmp(m_pImage,IMAGE_FLB_START,IMAGE_FLB_START_SIZE)!=0)
     {
         return  FALSE;
     }

3.定位函数头表
1
2
// 定位函数头表
     m_pFuncHeader=(PFuncHeader)(m_pImage+IMAGE_FLB_START_SIZE);

4.检测函数,只要遍历函数头表就可以了
1
2
3
4
5
6
7
8
9
10
// 遍历函数库
     do
     {
         if (CheckFunc(pFuncHeader,pFuncDat))
         {  
             pNam=(PCSTR)(m_pImage+pFuncHeader->NameOff);
             return  pNam;
         }
         pFuncHeader++;
     while  (pFuncHeader->DataSize); // 函数头表以一个完全为空的函数头成员结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BOOL CLibScanner::CheckFunc(PFuncHeader pFuncHeader,PCBYTE pFuncDat)
{
     PBYTE pImpFuncDat=m_pImage+pFuncHeader->DataOff;
     // 进行逐字节比对
     for  (DWORD i=0;i<pFuncHeader->DataSize;i++)
     {
         // 跳过重定位信息
         // 只要是连续四个字节为,就当作重定位信息处理
         // 注意 while :有可能两个重定位信息相邻,比如两个操作数的情况
         while (*(PDWORD)(pImpFuncDat+i)==0)
         {  
             i+=4;
         }
         if  (pImpFuncDat[i]!=pFuncDat[i])
         {
             return  FALSE;
         }
     }
     return  TRUE;
}

这样就可以使用flb文件了

二.拿到函数数据
要想测试flb文件,你还得拿到函数数据,我使用的是PVDasm,下面说一下我的方法:
1.        解析PE文件,获得代码段
2.        使用反汇编引擎,PVDasm,监控Call指令(只做0xe8),获得函数数据
3.        加载相应的flb文件,检测每个获得函数数据,是否属于此flb文件中的函数

我已经把各个版本的C语言静态库都搞成相应的flb文件了,我写了几个test,(附件里都有)并和ida做过比较,占时还没发现问题,但那个函数数据size的问题,就像锅里的一坨S,请大家帮帮忙!

最后再提一下我的问题,希望有前辈指点一下!
函数数据的size确定问题,我现在的方法是:直接断定一个节中只有一个函数数据!
《PE COFF文件规范》里提到一种:
“5.5.1 辅助符号表记录格式之一:函数定义”;里面可以拿到size;
但悲剧的是每个函数符号后面一张辅助符号表都没有,所以这种方法流产了
我到现在也找不到好方法,望有牛人提点!
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值