声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
No.1
结构图
pe结构可以总结为以下这个略缩图,DOS头和NT头就是PE文件中两个重要的文件头,不过里面nt头会比较复杂,下面详细介绍
No.2
Dos头数据格式定义如下,大小为64字节:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number (固定标志,不会变的标志)4D5A MZ,用来判断是否是DOS程序的
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // DOS代码的执行位置,16位DOS下告知程序无法运行
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header (PE文件头偏移,文件偏移值FA)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在32位程序下,有用的就是2个字段,第一个是e_magic标志和最后一个字段e_lfanew 是指向PE文件头格式的偏移值
打印dos头的部代码:
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
pFileBuffer = ReadPEFile(FILEPATH);
if(!pFileBuffer)
{
printf("文件读取失败\n");
return ;
}
//判断是否是有效的MZ标志
if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE)
{
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
//打印DOC头
printf("********************DOC头********************\n");
printf("MZ标志:%x\n",pDosHeader->e_magic);
printf("PE偏移:%x\n",pDosHeader->e_lfanew);
pe文件的e_magic固定是4D5A 对应的字母也就是MZ,这里的e_lfanew为0130(由于是小端模式,这里的e_lfanew的地址是从右往左读,所以是0130),所以nt头的开始地址是0000+0130=0130
学习pe结构建议手工先不依赖工具写下每个字段的值,然后用工具解析pe文件,再看看自己手工写下的值跟工具显示的一不一样,这里使用petools
而DOS stub 则为dos头和nt头中间那部分 是由代码与数据混合而成,这中间的代码不会被运行,可视为垃圾代码,但作为攻击者,这块是个可以藏数据的地方,dos头也是ctf常考的一部分。
No.3
由结构图可见,nt头可分为三部分,分别为开头4个字节的pe文件标志,然后到pe文件头,接下来是可选pe头,由于pe文件头跟可选pe头都是结构体,他们又会有自己的各项成员
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //固定值 PE文件标志4个字节
IMAGE_FILE_HEADER FileHeader; //pe文件头结构体
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //可选pe头结构体
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
pe文件头结构:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //CPU类型 不可更改
WORD NumberOfSections; //节数量 可以更改
DWORD TimeDateStamp; //文件创建时间 可以更改
DWORD PointerToSymbolTable; //符号表偏移 可以更改
DWORD NumberOfSymbols; //符号数量 可以更改
WORD SizeOfOptionalHeader; //可选pe头的大小 可以更改
WORD Characteristics; //文件属性 不可更改
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可以看到,一个word是2个字节,dword就是4个字节,所以pe文件头的大小是固定的,也就是20个字节
OptionalHeader只是翻译过来叫可选pe头,但他是pe结构必不可少的一部分,并不是可选不可选的,嘻嘻-0-,虽然可选pe头的大小是不确定的,但在pe文件头可选pe头的结构:
//YES:表示可以改 NO:表示不能改 RES:表示能改但是有限制
typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic; //NO 机器型号,判断是 PE是 32位还是 64位
BYTE MajorLinkerVersion; //YES 链接器版本号高版本
BYTE MinorLinkerVersion; //YES 链接器版本号低版本,组合起来就是 5.12 其中 5是高版本,C是低版本
DWORD SizeOfCode; //YES 代码节的总大小(512为一个磁盘扇区)
DWORD SizeOfInitializedData; //YES 初始化数据的节的总大小,也就是.data
DWORD SizeOfUninitializedData; //YES 未初始化数据的节的大小,也就是.data?
DWORD AddressOfEntryPoint; //NO 程序执行入口地址(OEP) RVA(相对虚拟偏移地址)
DWORD BaseOfCode; //YES 代码的节的起始 RVA(相对偏移)也就是代码区的偏移,偏移+模块首地址定位代码区
DWORD BaseOfData; //YES 数据结的起始偏移(RVA),同上
DWORD ImageBase; //YES 程序的建议模块基址(意思就是说作参考用的,模块建议基址如果被使用了就会使用别的地址)
DWORD SectionAlignment; //RES 内存中的节对齐 一般是0x1000
DWORD FileAlignment; //RES 文件中的节对齐 一般是0x200
WORD MajorOperatingSystemVersion; //YES 操作系统版本号高位
WORD MinorOperatingSystemVersion; //YES 操作系统版本号低位
WORD MajorImageVersion; //YES PE版本号高位
WORD MinorImageVersion; //YES PE版本号低位
WORD MajorSubsystemVersion; //NO 子系统版本号高位
WORD MinorSubsystemVersion; //YES 子系统版本号低位
DWORD Win32VersionValue; //YES 32位系统版本号值,注意只能修改为4 5 6表示操作系统支持nt4.0 以上,5的话依次类推
DWORD SizeOfImage; //RES 整个程序也就是整PE文件在内存中占用的空间(包含PE映射尺寸)
DWORD SizeOfHeaders; //RES 所有头大小(头的结构体大小)+节表结构体大小,记得值一定是文件对齐值的倍数,也就是到第一节区实际位置的偏移
DWORD CheckSum; //YES 校验和,对于驱动程序,可能会使用
WORD Subsystem; //NO 文件的子系统 :0x02表示窗口程序
WORD DllCharacteristics; //NO DLL文件属性,也可以成为特性,可能DLL文件可以当做驱动程序使用
DWORD SizeOfStackReserve; //RES 预留的栈的大小
DWORD SizeOfStackCommit; //RES 立即申请的栈的大小(分页为单位)
DWORD SizeOfHeapReserve; //RES 预留的堆空间大小
DWORD SizeOfHeapCommit; //RES 立即申请的堆的空间的大小
DWORD LoaderFlags; //YES 与调试有关
DWORD NumberOfRvaAndSizes; //RES 下面数据目录的数量:0x10表示有16个
IMAGE_DATA_DIRECTORY DataDirectory[16]; //RES 数据目录,默认16个,可以查看宏*/
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
可以看看这里16进制编辑器的显示
打印nt头的代码:
VOID PrintNTHeaders()
{
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
pFileBuffer = ReadPEFile(FILEPATH);
if(!pFileBuffer)
{
printf("文件读取失败\n");
return ;
}
//判断是否是有效的MZ标志
if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE)
{
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
//打印DOC头
printf("********************DOC头********************\n");
printf("MZ标志:%x\n",pDosHeader->e_magic);
printf("PE偏移:%x\n",pDosHeader->e_lfanew);
//判断是否是有效的PE标志
if(*((PDWORD)((DWORD)pFileBuffer+pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)
{
printf("不是有效的PE标志\n");
free(pFileBuffer);
return ;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer+pDosHeader->e_lfanew);
//打印NT头
printf("********************NT头********************\n");
printf("NT:%x\n",pNTHeader->Signature);
pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);
printf("********************PE头********************\n");
printf("PE:%x\n",pPEHeader->Machine);
printf("节的数量:%x\n",pPEHeader->NumberOfSections);
printf("SizeOfOptionalHeader:%x\n",pPEHeader->SizeOfOptionalHeader);
//可选PE头
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader+IMAGE_SIZEOF_FILE_HEADER);
printf("********************OPTIOIN_PE头********************\n");
printf("OPTION_PE:%x\n",pOptionHeader->Magic);
//释放内存
free(pFileBuffer);
}
No.4
节表结构体如下,一般一个pe文件不止一个节表,每个节表的大小为40字节,pe文件节表的数量可在pe文件头里面的NumberOfSections 字段可见
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //YES 节区的名字 8个字节
union
{
DWORD PhysicalAddress;
DWORD VirtualSize; //YES 节区在内存的大小,实际值是按内存对齐算
} Misc;
DWORD VirtualAddress; //虚拟地址 节区的 RVA地址(拷到内存中哪个位置)
DWORD SizeOfRawData; //在文件中对齐的尺寸(拷多大)
DWORD PointerToRawData; //在文件中的偏移FA(从文件哪里开始拷)
DWORD PointerToRelocations; //在 OBJ文件中使用
DWORD PointerToLinenumbers; //行号表位置,调试使用
WORD NumberOfRelocations; //在 OBJ文件中使用
WORD NumberOfLinenumbers; //行号表的数量
DWORD Characteristics; //节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
可选pe头结束紧跟着就是节表,如图有6个节表
打印全部节表的部分代码:
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader+pPEHeader->SizeOfOptionalHeader);
//nt头开始位置+pe标志+pe文件头+可选pe头大小= pSectionHeader开始位置
printf("NumberOfSections: %d\n",pPEHeader->NumberOfSections);
for(int i=0;i<pPEHeader->NumberOfSections;i++) //遍历节表各字段
{ //printf("%d\n",pPEHeader->NumberOfSections);
//pSectionHeader=pSectionHeader+i*IMAGE_SIZEOF_SECTION_HEADER;
printf("pSectionHeader->Name:%s\n",pSectionHeader->Name);
printf("pSectionHeader->VirtualSize: %x\n",pSectionHeader->Misc.VirtualSize);
printf("pSectionHeader->VirtualAddress:%x\n",pSectionHeader->VirtualAddress);
printf("pSectionHeader->SizeOfRawData:%x\n",pSectionHeader->SizeOfRawData);
printf("pSectionHeader->PointerToRawData:%x\n",pSectionHeader->PointerToRawData);
printf("pSectionHeader->PointerToRelocations:%x\n",pSectionHeader->PointerToRelocations);
printf("pSectionHeader->PointerToLinenumbers:%x\n",pSectionHeader->PointerToLinenumbers);
printf("pSectionHeader->NumberOfRelocations:%x\n",pSectionHeader->NumberOfRelocations);
printf("pSectionHeader->NumberOfLinenumbers: %x\n",pSectionHeader->NumberOfLinenumbers);
printf("pSectionHeader->NumberOfLinenumbers: %x\n",pSectionHeader->NumberOfLinenumbers);
printf("pSectionHeader->Characteristics: %x\n",pSectionHeader->Characteristics);
printf("\n");
printf("%d\n",sizeof(pSectionHeader));
pSectionHeader++;
}
petools 节表显示:
这里用*号标记出比较重要的字段,如果要编写程序解析pe文件的pe头,这里星号字段是肯定会用到,而且要理解其作用的各个头中比较重要的字段:
1、DOC头:
WORD e_magic * "MZ标记" 用于判断是否为可执行文件.
DWORD e_lfanew; * PE头相对于文件的偏移,用于定位PE文件
2、标准PE头:
WORD Machine; * 程序运行的CPU型号:0x0 任何处理器/0x14C 386及后续处理器
WORD NumberOfSections; * 文件中存在的节的总数,如果要新增节或者合并节 就要修改这个值.
DWORD TimeDateStamp; * 时间戳:文件的创建时间(和操作系统的创建时间无关),编译器填写的.
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; * 可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h 大小可以自定义.
WORD Characteristics; * 每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1
3、可选PE头:
WORD Magic; * 说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;* 所有代码节的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD SizeOfInitializedData;* 已初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD SizeOfUninitializedData;* 未初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的 没用
DWORD AddressOfEntryPoint;* 程序入口
DWORD BaseOfCode;* 代码开始的基址,编译器填的 没用
DWORD BaseOfData;* 数据开始的基址,编译器填的 没用
DWORD ImageBase;* 内存镜像基址
DWORD SectionAlignment;* 内存对齐
DWORD FileAlignment;* 文件对齐
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;* 内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍
DWORD SizeOfHeaders;* 所有头+节表按照文件对齐后的大小,否则加载会出错
DWORD CheckSum;* 校验和,一些系统文件有要求.用来判断文件是否被修改.
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;* 初始化时保留的堆栈大小
DWORD SizeOfStackCommit;* 初始化时实际提交的大小 DOS
DWORD SizeOfHeapReserve;* 初始化时保留的堆大小 ...
DWORD SizeOfHeapCommit;* 初始化时实践提交的大小 PE标记
DWORD LoaderFlags; 标准PE
DWORD NumberOfRvaAndSizes;* 目录项数目 可选PE头
No.5
在介绍数据目录前说这个之前补充以下几点知识:相对虚拟地址(Relative Virtual Address,RVA) 又叫内存偏移 Memory Offset
文件偏移地址(FO_A_) 又叫 文件偏移 File OffsetVA: 全名virtualAddress 虚拟地址. 就是内存中虚拟地址. 例如 0x00401000
由于文件读取到内存中会进行拉伸,如下图:
因为读到内存中文件被拉伸过,所以文件中的地址偏移跟内存中的偏移不一样设x 为节数据的任意一位置内存偏移转文件偏移总结:
1.计算RVA 公式: x - ImageBase == RVA
2.计算差值偏移: RVA - 节.VirtualAddress == 差值偏移.
3.计算FOA: 差值偏移 + 节.PointerToRawData == FOA
rva转foa代码:
DWORD RvaToFileOffset(IN LPVOID pFileBuffer,IN DWORD dwRva)
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
if (!pFileBuffer)
{
printf("(RvaToFileOffset)Can't open file!\n");
return 0;
}
//判断是否是有效的MZ标志
if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) //typedef DWORD near *PDWORD;
{
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return 0;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
//判断是否是有效的PE标志
if(*((PDWORD)((DWORD)pFileBuffer+pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)
{
printf("不是有效的PE标志\n");
free(pFileBuffer);
return 0;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
PIMAGE_SECTION_HEADER pSectionTemp = pSectionHeader;
if(dwRva<pOptionHeader->SizeOfHeaders)
{
return dwRva;
}
else
{
for(int i=0;i<pPEHeader->NumberOfSections;i++,pSectionTemp++)
{
if(dwRva>=pSectionTemp->VirtualAddress && dwRva<pSectionTemp->VirtualAddress+pSectionTemp->Misc.VirtualSize)
{
return dwRva-pSectionTemp->VirtualAddress+pSectionTemp->PointerToRawData;
}
}
}
printf("RvaToFileOffset faild\n");
return 0;
}
foa转rva代码:
DWORD FoatoRva(IN LPVOID pFileBuffer,IN DWORD dwFoa)
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
if (!pFileBuffer)
{
printf("(RvaToFileOffset)Can't open file!\n");
return 0;
}
//判断是否是有效的MZ标志
if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) //typedef DWORD near *PDWORD;
{
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return 0;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
//判断是否是有效的PE标志
if(*((PDWORD)((DWORD)pFileBuffer+pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)
{
printf("不是有效的PE标志\n");
free(pFileBuffer);
return 0;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
PIMAGE_SECTION_HEADER pSectionTemp = pSectionHeader;
if(dwFoa<pOptionHeader->SizeOfHeaders)
{
return dwFoa;
}
else
{
for(int i=0;i<pPEHeader->NumberOfSections;i++,pSectionTemp++)
{
if(dwFoa>=pSectionTemp->PointerToRawData && dwFoa<pSectionTemp->PointerToRawData+pSectionTemp->SizeOfRawData)
{
return dwFoa-pSectionTemp->PointerToRawData+pSectionTemp->VirtualAddress;
}
}
}
printf("FoatoRva faild\n");
return 0;
}
总结就是在文件中计算偏移要用foa,内存中计算偏移要用rva,如果需要互相转换就调用相关转换函数
No.6
接下来重点介绍下位于可选pe头的最后的IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录,默认16个
# define IMAGE_DIRECTORY_ENTRY_EXPORT 0 导出表
# define IMAGE_DIRECTORY_ENTRY_IMPORT 1 导入表
# define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 资源目录
# define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 异常目录
# define IMAGE_DIRECTORY_ENTRY_SECURITY 4 安全目录
# define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 重定位基本表
# define IMAGE_DIRECTORY_ENTRY_DEBUG 6 调试目录
# define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 描术字串
# define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 机器值
# define IMAGE_DIRECTORY_ENTRY_TLS 9 TLS目录
# define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 载入配值目录
# define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 绑定导入表
# define IMAGE_DIRECTORY_ENTRY_IAT 12 导入地址表
# define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 延迟载入描述
# define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 COM信息
这里有几个数据目录比较重要,这里逐一介绍,分别是导出表、导入表、重定位基本表、绑定导入表和导入地址表,至少得懂得输出、修改和移动这几个表,才算掌握pe结构的大概
IMAGE_DIRECTORY_ENTRY_EXPORT 0 // 导出表
IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 导入表
IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 资源表
IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
IMAGE_DIRECTORY_ENTRY_IAT 12 // 导入表地址
No.7
一般的简单pe文件并不存在导出表,一般的dll都有导出表,因为写有导出函数可以给到别的pe文件调用功能函数
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 未使用
DWORD TimeDateStamp; // 时间戳
WORD MajorVersion; // 未使用 11
WORD MinorVersion; // 未使用 12
DWORD Name; // 指向该导出表文件名字符串 13
DWORD Base; // 导出函数起始序号 15
DWORD NumberOfFunctions; // 所有导出函数的个数 17
DWORD NumberOfNames; // 以函数名字导出的函数个数
DWORD AddressOfFunctions; // 导出函数地址表RVA
DWORD AddressOfNames; // 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
从下图可以看到,name是dll的名称字符串地址的RVA,而AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals则分别对应导出函数地址表RVA、导出函数名称表RVA和导出函数序号表RVA,他们的数量也是对应NumberOfFunctions和NumberOfNames,分别提供按照名字导出和序号导出两种。
petools显示的导出表:
输入导出表代码:
VOID PrintExportDrectory()
{
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
ReadPEFile(DLLPATH, &pFileBuffer);
if(!pFileBuffer){
printf("PrintExportDrectory读取文件失败\n");
return;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
IMAGE_DATA_DIRECTORY* pFileBufferr_Data = NULL;
pFileBufferr_Data=(IMAGE_DATA_DIRECTORY*)((DWORD)(&(pOptionHeader->NumberOfRvaAndSizes)) + 4);//数据目录起始位置
IMAGE_EXPORT_DIRECTORY* pFileBuffer_Drectory=(IMAGE_EXPORT_DIRECTORY*)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBufferr_Data->VirtualAddress));
printf("---------------------------------------打印导出表信息--------------------------------------\n");
printf("ExportDrectory的VirtualAddress:%x\n",pOptionHeader->DataDirectory[0].VirtualAddress);
printf("ExportDrectory的Size:%x\n",pOptionHeader->DataDirectory[0].Size);
if(pOptionHeader->DataDirectory[0].Size==0 && pOptionHeader->DataDirectory[0].VirtualAddress==0)
{
printf("ExportDrectory is empty\n");
return ;
}
printf("Characteristics:%x\n",pFileBuffer_Drectory->Characteristics);
printf("TimeDateStamp:%x\n",pFileBuffer_Drectory->TimeDateStamp);
printf("MajorVersion:%x\n",pFileBuffer_Drectory->MajorVersion);
printf("Name:%x\n",pFileBuffer_Drectory->Name);
printf("Base:%x\n",pFileBuffer_Drectory->Base);
printf("NumberOfFunctions:%x\n",pFileBuffer_Drectory->NumberOfFunctions);
printf("NumberOfNames:%x\n",pFileBuffer_Drectory->NumberOfNames);
printf("AddressOfFunctions:%x\n",pFileBuffer_Drectory->AddressOfFunctions);
printf("AddressOfNames:%x\n",pFileBuffer_Drectory->AddressOfNames);
printf("AddressOfNameOrdinals:%x\n",pFileBuffer_Drectory->AddressOfNameOrdinals);
/*打印地址表*/
printf("*************** AddressOfFunctions ***************\n");
LPDWORD AddressOfFunctions = (LPDWORD)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBuffer_Drectory->AddressOfFunctions));
for(int i=0;i < pFileBuffer_Drectory->NumberOfFunctions;i++)
{
printf("第%d个pFileBuffer_Drectory->NumberOfFunctions :%x\n",i,*AddressOfFunctions);
AddressOfFunctions++;
}
/*打印序号表*/
printf("*************** AddressOfNameOrdinals ***************\n");
LPWORD AddressOfNameOrdinals=(LPWORD)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBuffer_Drectory->AddressOfNameOrdinals));
for(int k=0;k < pFileBuffer_Drectory->NumberOfNames;k++)
{
printf("第%d个pFileBuffer_Drectory->AddressOfNameOrdinals :%x\n",k,*AddressOfNameOrdinals);
AddressOfNameOrdinals++;
}
/*打印名称表*/
printf("*************** AddressOfNames ***************\n");
LPDWORD AddressOfNames=(LPDWORD)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBuffer_Drectory->AddressOfNames));
for(int n=0;n < pFileBuffer_Drectory->NumberOfNames;n++)
{
printf("第%d个pFileBuffer_Drectory->AddressOfNames :%s\n",n,LPSTR((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *AddressOfNames)));
printf("第%d个pFileBuffer_Drectory->AddressOfNames 长度为 :%d\n",n,strlen(LPSTR((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *AddressOfNames))));
AddressOfNames++;
}
free(pFileBuffer);
}
No.8
位于数据目录的第二个即是导入表,大小为20字节,结构体如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,该名字以 两个字节的0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
其中OriginalFirstThunk和FirstThunk又指向了一个IMAGE_THUNK_DATA结构体,分别是INT和IAT,IMAGE_THUNK_DATA结构体如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为空,编译器决定 如果不为空 是函数在导出表中的索引
BYTE Name[1]; //函数名称,这里只有一个字节,采用越界的方式读取
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
pe文件加载前:
pe文件加载后:
输出导入表的步骤
一、找到第二个数据目录,把rva转成foa,找到导入表位置,然后遍历每个导入表,直到有sizeof(IMAGE_IMPORT_DESCRIPTOR)个0出现,即20个0结束
二、每个导入表遍历OriginalFirstThunk和FirstThunk,直到出现4字节的0,遍历OriginalFirstThunk和FirstThunk的时候,如果最高位为1,即为按序号导出,应该去掉最高位的1,然后按序号导出,如果最高位不是1,就是直接对应IMAGE_IMPORT_BY_NAME的rva步骤如下图:
sizeOf(IMAGE_IMPORT_DESCRIPTOR)个0代表导入表结束
petools显示的导入表:
打印导入表代码:
VOID PrintImport()
{
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader_last = NULL;
BOOL isOK = FALSE;
size_t FileBufferSize=ReadPEFile(NOTEPAD_FILEPATH,&pFileBuffer);
DWORD addr;
if (!pFileBuffer)
{
printf("(MoveExport)Can't open file!\n");
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
PIMAGE_IMPORT_DESCRIPTOR pFileBuffer_Import = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer, pOptionHeader->DataDirectory[1].VirtualAddress));
DWORD OriginalFirstThunk_temp=NULL;
PDWORD OriginalFirstThunk=PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->OriginalFirstThunk));
PDWORD FirstThunk=PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->FirstThunk));
printf("pFileBuffer_Import->FirstThunk:%x\n",pFileBuffer_Import->FirstThunk);
printf("pFileBuffer_Import->OriginalFirstThunk:%x\n",pFileBuffer_Import->OriginalFirstThunk);
int n=1;
int m=1;
while (pFileBuffer_Import->OriginalFirstThunk) {
LPSTR name = (LPSTR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->Name));
//1 输出导出表dll的名字
printf("第%d个dll的名称为:%s\n", n, name);
LPSTR pName = (LPSTR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->Name));
printf("================%s================\n", name);
//解析INT表
PDWORD OriginalFirstThunk = (PDWORD)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, (DWORD)pFileBuffer_Import->OriginalFirstThunk));
while (*OriginalFirstThunk) {
if ((*OriginalFirstThunk & 0x80000000) >> 31 == 1) { //如果第一位为1,表示为序号导入
DWORD ord = *OriginalFirstThunk & 0x7fffffff;
printf("第%d个函数按序号导入:%d\n",m, ord);
}
else {
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *OriginalFirstThunk));
printf("第%d个按函数名导入:%s\n",m, pName->Name);
}
//下一个INT表项
OriginalFirstThunk++;
m++;
}
//下一张导入表
printf("按enter键显示下一个dll导入表信息");
getchar();
pFileBuffer_Import++;
n++;
m = 1;
printf("\n");
}
printf("************************FirstThunk阶段************************\n");
pFileBuffer_Import = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pOptionHeader->DataDirectory[1].VirtualAddress));
n = 1;
m = 1;
//PDWORD OriginalFirstThunk=PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->OriginalFirstThunk));
while (pFileBuffer_Import->FirstThunk) {
LPSTR name = (LPSTR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->Name));
//1 输出导出表dll的名字
printf("第%d个dll的名称为:%s\n", n, name);
PDWORD FirstThunk = PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->FirstThunk));
while (*FirstThunk) {
if (pFileBuffer_Import->TimeDateStamp == 0)
{
if ((*FirstThunk & 0x80000000) >> 31 == 1)
{
OriginalFirstThunk_temp = *FirstThunk & 0x7FFFFFFF;
printf("第%d个函数的序号为:%d\n", m, OriginalFirstThunk_temp);
}
else
{
//PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *OriginalFirstThunk));
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *FirstThunk));
printf("第%d个的函数名导入:%s\n", m, pName->Name);
}
}
else
{
printf("TimeDateStamp 不为0 已添加绑定导入表 直接输出函数地址\n");
printf("第%d个函数的地址为:%x\n", m, *FirstThunk);
}
FirstThunk++;
m++;
}
//2 遍历OriginalFirstThunk 输出每个函数的序号或者名字
//3 遍历FirstThunk 输出每个函数的序号或者名字
printf("按enter键显示下一个dll导入表信息:\n");
getchar();
pFileBuffer_Import++;
n++;
}
printf("pe加载前的导入表输出完成!\n");
}
No.9
绑定导入表位于数据目录的第12项结构体如下:
typedef struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD Reserved;
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
PE加载EXE相关的DLL时,首先会根据IMAGE_IMPORT_DESCRIPTOR结构中的TimeDateStamp来判断是否要重新
计算IAT表中的地址。
TimeDateStamp == 0 未绑定
TimeDateStamp == -1 已绑定 真正的绑定时间为IMAGE_BOUND_IMPORT_DESCRIPTOR的TimeDateStamp
No.10
数据目录项的第6个结构,就是重定位表
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;
重定位表是这样连续一块一块的,直到遇到8个字节的0为结束
一般exe用不到重定位表,用的比较多的是dll,一般分配内存时候占不到原有的位置,则依靠重定位表修复地址,以免调用时候发生错误。
修复步骤:
1、通过IMAGE_DATA_DIRECTORY结构的VirtualAddress
属性 找到第一个IMAGE_BASE_RELOCATION
2、判断一共有几块数据:
最后一个结构的VirtualAddress与SizeOfBlock都为0
3、具体项 宽度:2字节
也就是这个数据
内存中的页大小是1000H 也就是说2的12次方 就可以表示
一个页内所有的偏移地址 具体项的宽度是16字节 高四位
代表类型:值为3 代表的是需要修改的数据 值为0代表的是
用于数据对齐的数据,可以不用修改.也就是说 我们只关注
高4位的值为3的就可以了.
4、VirtualAddress 宽度:4字节
当前这一个块的数据,每一个低12位的值+VirtualAddress 才是
真正需要修复的数据的RVA
真正的RVA = VirtualAddress + 具体项的低12位
5、SizeOfBlock 宽度:4字节
当前块的总大小
具体项的数量 = (SizeOfBlock - 8)/2
重定位表:
招聘启事
安恒雷神众测SRC运营(实习生)
————————
【职责描述】
1. 负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2. 负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3. 参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4. 积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5. 积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。
【任职要求】
1. 责任心强,性格活泼,具备良好的人际交往能力;
2. 对网络安全感兴趣,对行业有基本了解;
3. 良好的文案写作能力和活动组织协调能力。
简历投递至
设计师(实习生)
————————
【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。
【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;精通photoshop/illustrator/coreldrew/等设计制作软件;
3、有品牌传播、产品设计或新媒体视觉工作经历;
【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽
简历投递至
安全招聘
————————
公司:安恒信息
岗位:Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京
工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…
【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案
【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)
【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;
岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)
【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。
【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。
【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。
简历投递至
岗位:红队武器化Golang开发工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)
【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。
【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。
【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。
简历投递至
专注渗透测试技术
全球最新网络攻击技术
END