首页
社区
课程
招聘
[原创]PEEdit之初级开发篇
发表于: 2013-12-2 21:05 7253

[原创]PEEdit之初级开发篇

2013-12-2 21:05
7253

文章分为三大部分
                                序言
                               1.PE基础知识概述
                                 1.1:DOS头
                                 1.2:PE文件头
                                 1.3:区段表
                              2.工具编写
                                 2.1功能简介
                                 2.2界面概览
                                 2.3重要表的遍历思想
                                    2.3.1导出表
                                    2.3.2导入表
                                    2.3.3资源表
                                    2.3.4重定位表
                               3总结

******************************************我是分割线*******************************************
序言:
 PE格式是Windows下最常用的可执行文件格式,所谓PE格式也就是(Portable Executable File Format/可移值的执行体)。最常见的就是.exe .dll .sys .ocx等,了解PE需要具备C语言相关知识,编写这个PeEdit的初衷是把PE知识更好的吸收和总结,并封装成类,为以后工作做准备,同时做成界面版本使之成为一个小工具,这样可以不断积累自己的工具库。文章如有错误,还请大牛指正,以免误导别人
******************************************我是分割线*******************************************
1.PE基础知识概述:
    PE文件使用的是一个平面空间,所有代码和数据都被合并在一起,组成一个很大的结构。里面穿插了大量的跳转和结构体,既然说到了跳转,就需要了解相关地址转换的概念,这点很重要
RVA(相对虚拟地址): PE文件的各种数据结构中涉及地址的字段大部分都是以RVA表示的。
FileOffset(文件偏移地址):指的是文件在磁盘中的偏移
VA(虚拟地址)          :也就是内存偏移地址
ImageBase(基地址)      :文件被加载到内存时的首地址, 这个值是由PE文件决定的,但系统不一定会加载到指定位置,所以产生了重定位,按照默认设置,一般EXE文件的基地址为0x00400000,DLL文件基地址是0x10000000
关于地址转换,大部分都是RVA转FileOffset,公式是:
Offset = RVA –段的RVA +该段的FileOffset
例如:以user32 .dll为例

此处仅列出两个区段,仅为示例
如果RVA=0X30000,此值大于.text段小于.data段,所以该值位于.text段
套用公式 Offset = 0x30000 –0x10000 + 0x400
所以Offset=0x20400,是不是很简单
好了,具备以上知识,开始进入PE的世界吧

1.1:DOS头
    DOS部分由MZ格式的文件头和可执行代码的DOS stub组成,这部分是为了向下兼容而保留下的
MZ格式的文件头由IMAGE_DOS_HEADER结构定义:(winnt.h这个文件中定义关于PE的相关结构体)
struct _IMAGE_DOS_HEADER {
0x00 WORD e_magic; --这个就是”MZ”头
0x02 WORD e_cblp;
0x04 WORD e_cp;
0x06 WORD e_crlc;
0x08 WORD e_cparhdr;
0x0a WORD e_minalloc;
0x0c WORD e_maxalloc;
0x0e WORD e_ss;
0x10 WORD e_sp;
0x12 WORD e_csum;
0x14 WORD e_ip;
0x16 WORD e_cs;
0x18 WORD e_lfarlc;
0x1a WORD e_ovno;
0x1c WORD e_res[4];
0x24 WORD e_oemid;
0x26 WORD e_oeminfo;
0x28 WORD e_res2[10];
0x3c DWORD e_lfanew;-指向PE头
};
看似很庞大的结构体,其实我们只需要关注第一个和最后一个字段即可,第一个字段已经在winnt.h中被定义为IMAGE_DOS_SIGNATURE,最后一个e_lfanew字段指向PE文件头, 也就是PE文件头的相对偏移(RVA),偏移的换算我们已经讲过
如图所示:

最后一个字段指向0x000000E8,从此字段开始将是PE文件头,DOS头和PE头之间的部分就是DOS stub部分,如果此程序在DOS下运行会显示”This program cannot be run in DOS mode”后退出,这部分我不需要过多关注
1.2:PE文件头
 
PE文件头是由IMAGE_NT_HEADERS结构定义的:
struct _IMAGE_NT_HEADERS {
0x00 DWORD Signature;     
0x04 _IMAGE_FILE_HEADER FileHeader;
0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
};
1.21 Signature  这个字段就是我们看到的0x00004550,ASCII码字符是"PE00",也就是”PE”文件标识, DOS头部的e_lfanew字段正是指向"PE\0\0"。
1.22 FileHeader 这个字段实际上是一个结构体
struct _IMAGE_FILE_HEADER {
0x00 WORD Machine;         运行平台一般是4C 01
0x02 WORD NumberOfSections;区段数目,我们后面讲解
0x04 DWORD TimeDateStamp; 文件创建日期和时间
0x08 DWORD PointerToSymbolTable; 指向符号表(用于调试)
0x0c DWORD NumberOfSymbols; 符号表中的符号数量(用于调试)
0x10 WORD SizeOfOptionalHeader; IMAGE_OPTIONAL_HEADER32结构的大小
0x12 WORD Characteristics; 文件属性
};
我们来看下比较重要的字段
NumberOfSections,指出此文件的区段的数量
SizeOfOptionalHeader:指出紧随其后的IMAGE_OPTIONAL_HEADER32结构的长度,这个值一般等于00e0h
Characteristics:属性标志字段, 不同的值将影响系统对文件的装入方式,例如,当13位为1时,表示这是一个DLL文件,否则这是一个exe文件
如图

1.23 OptionalHeader这个字段是一个扩展头,里面包含的大量的信息
struct _IMAGE_OPTIONAL_HEADER {
0x00 WORD Magic;
0x02 BYTE MajorLinkerVersion;
0x03 BYTE MinorLinkerVersion;
0x04 DWORD SizeOfCode;
0x08 DWORD SizeOfInitializedData;
0x0c DWORD SizeOfUninitializedData;
0x10 DWORD AddressOfEntryPoint;
0x14 DWORD BaseOfCode;
0x18 DWORD BaseOfData;
0x1c DWORD ImageBase;
0x20 DWORD SectionAlignment;
0x24 DWORD FileAlignment;
0x28 WORD MajorOperatingSystemVersion;
0x2a WORD MinorOperatingSystemVersion;
0x2c WORD MajorImageVersion;
0x2e WORD MinorImageVersion;
0x30 WORD MajorSubsystemVersion;
0x32 WORD MinorSubsystemVersion;
0x34 DWORD Win32VersionValue;
0x38 DWORD SizeOfImage;
0x3c DWORD SizeOfHeaders;
0x40 DWORD CheckSum;
0x44 WORD Subsystem;
0x46 WORD DllCharacteristics;
0x48 DWORD SizeOfStackReserve;
0x4c DWORD SizeOfStackCommit;
0x50 DWORD SizeOfHeapReserve;
0x54 DWORD SizeOfHeapCommit;
0x58 DWORD LoaderFlags;
0x5c DWORD NumberOfRvaAndSizes;
0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16];
};
这个结构体比较大,我们关注下比较重要的字段
Magic:一般为0x010b
SizeOfCode:可执行代码的大小
AddressOfEntryPoint:程序入口点,是一个RVA,这个比较重要,在加壳时要用到
BaseOfCode:代码基址
BaseOfData:数据基址
ImageBase:指出文件的优先装入基址,如果恰好载入此基址,那么将不需要进行重定位,由于每个.exe系统都会分配固定大小的虚拟空间,所以一般.exe不需要重定位,每个.exe文件需要加载多个DLL文件,系统不可能会为每个DLL文件都载入到他们的默认基址处,所以DLL对于重定位依赖性很强. 一般EXE文件的默认优先装入地址被定为0x00400000,而DLL文件的默认优先装入地址被定为0x10000000。
SectionAlignment:区段在内存中的对齐大小,一般为0x1000
FileAlignment:区段在磁盘文件中的对齐大小,一般为0x200或者0x1000
SizeOfImage:装入内存后的总大小
SizeOfHeaders:注意这个字段,这个字段不能看他的名字想当然, 这个字段的意思是第一个区块的偏移地址(磁盘文件中).,并不是指向第一个区段表
Subsystem:子系统,
DataDirectory:这个是个非常重要的字段, 它由16个相同的IMAGE_DATA_DIRECTORY结构组成, IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。我们需要重点关注的是导出表,输入表,资源表和重定位表,TLS表
IMAGE_DATA_DIRECTORY STRUCT
DWORD   VirtualAddress ;数据的起始RVA
 DWORD   Size ;       数据块的长度
IMAGE_DATA_DIRECTORY ENDS
每个字段在winnt.h中已经给与定义,关键几个定义如下
IMAGE_DIRECTORY_ENTRY_EXPORT        导出表
IMAGE_DIRECTORY_ENTRY_IMPORT        导入表
IMAGE_DIRECTORY_ENTRY_RESOURCE     资源
IMAGE_DIRECTORY_ENTRY_BASERELOC    重定位表
IMAGE_DIRECTORY_ENTRY_TLS         TLS表
如图

如图所示,数据目录表的每个成员占据8个字节,前四个字节表示VirtualAddress,(数据表的起始RVA),后四个字节代表SIZE(数据块的大小)
1.3区段表
typedef struct _IMAGE_SECTION_HEADER {
0x00 BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; 区段名字
union {
0x08  DWORD PhysicalAddress;不用关心,一般是下面的VirtualSize起作用
0x08 DWORD VirtualSize;    内存中占据的大小
} Misc;
0x0c DWORD VirtualAddress;RVA
0x10 DWORD SizeOfRawData;在文件中的大小
0x14 DWORD PointerToRawData;FileOffset
0x18 DWORD PointerToRelocations;
0x1c DWORD PointerToLinenumbers;
0x20 WORD NumberOfRelocations;
0x22 WORD NumberOfLinenumbers;
0x24 DWORD Characteristics;属性
};
Characteristics:重点说下这个, 其中的不同数据位代表了不同的属性,具体属性可以去winnt.h中查找,例如.text段的属性是包含可执行代码,代码可执行,可读取属性,想要添加属性可以用 | 来进行添加,去掉属性用 & 运算去除


2.工具编写 
以上我们对PE结构有了大概的了解,现在我们对其进行详细遍历以及进行界面化的编写
2.1功能简介
我们预期实现PE头和区段信息浏览和修改,位置计算,数据目录中包括导入表,导出表,重定位,资源信息的浏览
  
2.2界面概览
   这是工具的主界面,囊括了PE头一个大概信息,更多的信息需要在数据目录表和区段信息中查看修改

下图是区段表中详细信息,点击一个区段可以查看相应信息,区段标志做了多选框,这样可以很方便查看数据所代表的意思,这个给了我很好的启发,根据权值来判断相应的操作,而且很直观

下图是数据目录表中的详细资源信息

下图是位置计算器,可以对RVA VA OFFSET三者进行互相转换

2.3重要表的遍历思想
2.3.1导出表:
来看一下导出表的结构
struct _IMAGE_EXPORT_DIRECTORY {
0x00 DWORD Characteristics;
0x04 DWORD TimeDateStamp;
0x08 WORD MajorVersion;
0x0a WORD MinorVersion;
0x0c DWORD Name;
0x10 DWORD Base;
0x14 DWORD NumberOfFunctions;
0x18 DWORD NumberOfNames;
0x1c DWORD AddressOfFunctions;
0x20 DWORD AddressOfNames;
0x24 DWORD AddressOfNameOrdinals;
};
Base:基序号,也就是最小序号
NumberOfFunctions:导出函数总数目,这个数值并不能真正反映出导出这么多函数
NumberOfNames:  以名字导出的函数总数目,这就给与我们提示,还有以序号导出的函数
AddressOfFunctions:指向一个数组,这个数组保存了函数地址,也就是EAT
AddressOfNames:是一个指针数组,保存了各个导出函数名的地址,也就是ENT
AddressOfNameOrdinals:此数组保存了函数序号,姑且叫她EIT
EAT的导出顺序是按照序号大小进行依次导出的,(注意这个序号是减去基序号后的相对序号)如果存在这个序号则会有对应的EAT,否则EAT的值为null,而导出的ENT和EIT是一一对应的两个表格
遍历思想:根据以上知识我们来进行具体遍历,由于知道导出的最大函数数目,所以用for循环遍历比较合适,我们先获取EAT ENT EIT的指针
PDWORD pEAT=(PDWORD)((DWORD)lpImage+RVAToOffset(lpImage,pExprot->AddressOfFunctions));  
PDWORD pENT=(PDWORD)((DWORD)lpImage+RVAToOffset(lpImage,pExprot->AddressOfNames));   
PWORD pEIT=(PWORD)((DWORD)lpImage+RVAToOffset(lpImage,pExprot->AddressOfNameOrdinals));
// dwOrdinal的值可以认为是序号的索引,由于是EAT是按照序号大小先后导出的,如果EAT有值,说明存在此函数,然后从EIT中寻找是否有此序号,如果没有说明没有导出此函数的函数名,如果有说明有序号有地址依次遍历,代码如下
for (DWORD dwOrdinal = 0; dwOrdinal < pExprot->NumberOfFunctions; dwOrdinal++)
    {           
      if (!pEAT[dwOrdinal]) //如果EAT的值为空则跳过
      {
        continue;
      }
      bool bFlag=FALSE; //设置标记位
      for (DWORD dwIndex = 0; dwIndex < pExprot->NumberOfNames; dwIndex++)
      {
        if (dwOrdinal==pEIT[dwIndex])
        {
          PCHAR pszFunName=(PCHAR)((DWORD)lpImage+RVAToOffset(lpImage,pENT[dwIndex]));
          printf("%04X 0x%p %s\r\n",pExprot->Base+dwOrdinal,pEAT[dwOrdinal],pszFunName);
          bFlag=TRUE;
          break;
        } 
      }
      if (!bFlag)
      {
        printf("%04X 0x%p %s\r\n",pExprot->Base+dwOrdinal,pEAT[dwOrdinal],"null");
      }
    }
  }
下图是遍历结果

2.3.2导入表:
还是先看一下此表的结构体
struct _IMAGE_IMPORT_DESCRIPTOR {
0x00 union {
0x00 DWORD Characteristics;
0x00 PIMAGE_THUNK_DATA OriginalFirstThunk;
} u;
0x04 DWORD TimeDateStamp; 
0x08 DWORD ForwarderChain; 
0x0c DWORD Name;
0x10 PIMAGE_THUNK_DATA FirstThunk;
};
OriginalFirstThunk:指向输入名称表的(INT)的RVA
FirstThunk:指向输入地址表的RVA,如果没有载入到内存,他和OriginalFirstThunk:指向的内容一致,他们分别指向如下结构体的数组,且这个数组以一个空的IMAGE_THUNK_DATA结尾
typedef struct _IMAGE_THUNK_DATA {
union {
0x00 LPBYTE ForwarderString;
0x00 PDWORD Function;
0x00 DWORD Ordinal;
0x00 PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;
Ordinal:导入函数的序号,如果此值的最高位为1,则采用序号导入的方式,则低31位将呗看作函数序号,我们可以用宏IMAGE_SNAP_BY_ORDINAL32来判断此项是否为序号
AddressOfData:指向IMAGE_IMPORT_BY_NAME结构
typedef struct _IMAGE_IMPORT_BY_NAME 
{
0x00 WORD Hint;导入函数的序号
0x02 BYTE Name[1];导入函数的名称
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
遍历思想:我们并不知道到底导入了多少DLL,所以用while循环比较合适,如果IMAGE_IMPORT_DESCRIPTOR结构体的name字段无值,说明遍历结束,如果有值进入IMAGE_THUNK_DATA结构体进行判断,这是个联合体,同一时段只能有一个位段有值,前两个位段只是在载入到内存时才有值,所以排除判断,前门说我们有一个宏来进行判断序号,所以用if(不是序号)else(直接输出序号)来判断即可
下面给出伪代码,读者可以自己试着编写出具体代码
//假设pImport表示指向导入表的指针, pTHUNK表示指向IMAGE_THUNK_DATA32的指针
While(pImport->Name)
{
  …….
   while (pTHUNK->u1.Ordinal) //如果此值无效,第四个字段才有效,所以先判断此值
       {
         If(!IMAGE_SNAP_BY_ORDINAL32(pTHUNK->u1.Ordinal))
            {
               …
               pTHUNK++;
               continue;
}
Else
{
}
}
pImport++;
}
测试效果如下

2.3.3资源表:
struct _IMAGE_RESOURCE_DIRECTORY {
0x00 DWORD Characteristics;属性
0x04 DWORD TimeDateStamp;建立时间
0x08 WORD MajorVersion;主板本
0x0a WORD MinorVersion;自版本
0x0c WORD NumberOfNamedEntries;用字符串作为资源标识的条目个数
0x0e WORD NumberOfIdEntries;用ID作为资源标识的个数
};
这是资源表的结构体,资源在PE文件中以目录的结构存在,一般情况下这个目录分三层,从根目录开始依次为资源类型,目录资源ID与资源代码页,这三层都是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;资源名为字符串
        } DUMMYSTRUCTNAME;
        DWORD   Name;资源类型
        WORD    Id;资源ID
    } DUMMYUNIONNAME;
    union {
        DWORD   OffsetToData;数据偏移地址
        struct {
            DWORD   OffsetToDirectory:31;子目录偏移地址
            DWORD   DataIsDirectory:1;下一层是目录
        } DUMMYSTRUCTNAME2;
    } DUMMYUNIONNAME2;
}IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
如果NameIsString为1, NameOffset指向IMAGE_RESOURCE_DIR_STRING_U
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
    WORD    Length;字符串长度
    WCHAR   NameString[ 1 ];字符串数组
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
经过三层遍历后,最后会到达IMAGE_RESOURCE_DATA_ENTRY
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;资源数据的RVA
    DWORD   Size;        资源数据的长度
    DWORD   CodePage;    代码页
    DWORD   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
遍历思想:在第一层时, IMAGE_RESOURCE_DIRECTORY_ENTRY的name字段或者结构体有效,保存的是资源类型,第二层遍历时ID或者是联合体内的数据有效,第三层时Name字段有效,保存的是资源语言区域类型,第二个联合体同一时间只有一个字段有效,所以比较好判断
下面给出伪代码
//假设pRes1指向第一层IMAGE_RESOURCE_DIRECTORY
//第一层为目录遍历
DWORD dwCount1=pRes1->NumberOfIdEntries+pRes1->NumberOfNamedEntries;
For(DWORD i = 0; i < dwCount1; i++)
   {
      获取IMAGE_RESOURCE_DIRECTORY_ENTRY结构指针pEntry1
      If(类型是字符串)
        {
           ………
}
      Else类型是Name字段
        {
           …………..
}
//接下来判断第二个联合体指向的是数据还是目录
If(如果为目录)
{
   获取第二层IMAGE_RESOURCE_DIRECTORY 的指针pRes2, 
      If(ID是字符串)//注意这层是ID
       {
}
Else  Id字段指明
{
}
//接下来判断第二个联合体指向的是数据还是目录
If(是目录)
{
        获取第三层IMAGE_RESOURCE_DIRECTORY 的指针pRes3
            //这是最后一层
           直接获取资源代码页的数据

}
Else是数据
{
}
}
Else是数据
{
  …….
}
}
效果如下

2.3.4重定位表:
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;需要重定位的起始RVA
    DWORD   SizeOfBlock;   本结构与TypeOffset总大小
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
这个相对比较简单,需要说明的是VirtualAddress字段,这个字段更像前面所说的基址,由于SizeOfBlock字段指明了这个重定位表的大小,所以用它的值减去VirtualAddress和SizeOfBlock这两个字段所占大小,此时的大小将是TypeOffset数组的大小,这个结构体的定义如下
struct{
WORD Offset:12;重定位的偏移,
WORD Type:4;重定位类型
}
注意Offset这个字段,这个字段的数据加上刚才的VirtualAddress才是真正的RVA
遍历思想:我们并不知道有几个重定位区块,所以用while循环比较适合,这个比较简单,贴出伪代码
While(pBase->VirtualAddress)
{
  获得需要重定位项的个数dwBaseOfNum
  获得指向TypeOffset的结构体指针pOffset
   for (DWORD i = 0; i < dwBaseOfNum; i++)
      {
        依次获取每个TypeOffset的类型,进而进行判断并修复
}
指向下个重定位位段
}
效果如下

到现在我们已经遍历了几个比较重要而且常用的目录表,其余的就是界面编写与数据修改部分,不在我们的讨论范围内

3:总结
  开发过程中,以前学过的一些控件的用法有些模糊了,尤其是树控件的消息响应,都是从网上找到相关资料进行解决,开发本来就是个很细的工作,不要抱着侥幸心理,例如资源的遍历,要考虑全面才能解析各种变态PE。虽然名字叫PEEdit,但只能编辑保存少部分信息,更多的信息还不能进行保存,希望可以在后续版本中进行不断优化和增加功能,成为一个真正的PeEdit.
后记:
走到现在,非常感谢任老师的教学,许Sir和薛老师的课下指导
整个界面模仿了大名鼎鼎的LoadPE,书籍参考了黑客免杀攻防(任晓珲著).
PeEdit.rar


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 5
支持
分享
最新回复 (3)
雪    币: 81
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
沙发  支持
2013-12-3 00:39
0
雪    币: 10
活跃值: (231)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
还是不错,,写的也挺详细的, 就是没源码..
  顶下15pb
2013-12-3 01:49
0
雪    币: 34
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
源码已上传,欢迎交流指正
2013-12-3 10:08
0
游客
登录 | 注册 方可回帖
返回
//