利用PE数据目录的导入表获取函数名及其地址

PE文件是以64字节的DOS文件头开始的(IMAGE_DOS_HEADER),接着是一段小DOS程序,然后是248字节的

NT文件头(IMAGE_NT_HEADERS),NT的文件头位置由IMAGE_DOS_HEADER的e_lfanew给出!

NT文件头的前4个字节是文件签名(“PE00"字符串),紧接着是20字节的IMAGE_FILE_HEADER结构,它的

后面是224字节的IMAGE_OPTIONAL_HEADER结构,而就在这个结构里,里面有模块基地址,代码和数据大

小和基地址、线程堆栈和进程堆的配置,程序入口点的地址,还有数据目录表指针,PE文件还保留着16

个数据目录,常见的有导入表,导出表,资源和重定位表,而我们这里就是用的到了导入表

IMAGE_IMPORT_DESCRIPTOR,代码如下

[cpp] view plaincopy

  1. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  2. {
  3. int nRetCode = 0;
  4. // initialize MFC and print and error on failure
  5. if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
  6. {
  7. // TODO: change error code to suit your needs
  8. cerr << _T("Fatal Error: MFC initialization failed") << endl;
  9. nRetCode = 1;
  10. }
  11. else
  12. {
  13. // TODO: code your application‘s behavior here.
  14. CString strHello;
  15. strHello.LoadString(IDS_HELLO);
  16. cout << (LPCTSTR)strHello << endl;
  17. }
  18. //这里开始
  19. HMODULE hMod = ::GetModuleHandle(NULL);
  20. IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER *)hMod;
  21. IMAGE_OPTIONAL_HEADER *pOptHeader = (IMAGE_OPTIONAL_HEADER *)((BYTE *)hMod +
  22. pDosHeader->e_lfanew + 24);
  23. IMAGE_IMPORT_DESCRIPTOR *pImportDesc = (IMAGE_IMPORT_DESCRIPTOR *) ((BYTE *)hMod +
  24. pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
  25. while(pImportDesc->FirstThunk)
  26. {
  27. char *pszDllName = (char *)((BYTE *)hMod + pImportDesc->Name);
  28. printf("/n模块名称:%s/n", pszDllName);
  29. IMAGE_THUNK_DATA *pThunk = (IMAGE_THUNK_DATA *)((BYTE *)hMod + pImportDesc
  30. ->OriginalFirstThunk);
  31. int n = 0;
  32. //MessageBox(NULL, "Test", "MESS", MB_OK);
  33. char *pszFunName = NULL;
  34. while(pThunk->u1.Function)
  35. {
  36. pszFunName = (char *)((BYTE *)hMod + (DWORD)pThunk-
  37. >u1.AddressOfData + 2);
  38. PDWORD lpAddr = (DWORD *)((BYTE *)hMod + pImportDesc->FirstThunk) +
  39. n;
  40. try
  41. {
  42. printf("function name : %-25s", (char *)pszFunName);
  43. }
  44. catch(...)
  45. {
  46. printf("function name :unknown!");
  47. }
  48. printf("addr :%0X/n", lpAddr);
  49. n++;
  50. pThunk++;
  51. }
  52. pImportDesc++;
  53. }
  54. return nRetCode;
  55. }

顺便带上一些检查是否为PE的代码

[cpp] view plaincopy

  1. CFileDialog dlg(true);
  2. if(dlg.DoModal() != IDOK)
  3. {
  4. return ;
  5. }
  6. HANDLE hFile = ::CreateFile(dlg.GetFileName(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  7. if(hFile == INVALID_HANDLE_VALUE)
  8. {
  9. ::MessageBox(NULL, "INVALID FILE", "VALID PE", MB_OK);
  10. }
  11. IMAGE_DOS_HEADER dosHeader;
  12. IMAGE_NT_HEADERS ntHeader;
  13. bool bValid = false;
  14. DWORD dwRead;
  15. ::ReadFile(hFile, &dosHeader, sizeof(dosHeader), &dwRead, NULL);
  16. if(dwRead == sizeof(dosHeader))
  17. {
  18. if(dosHeader.e_magic == IMAGE_DOS_SIGNATURE)
  19. {
  20. if(::SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN) != -1)
  21. {
  22. ::ReadFile(hFile, &ntHeader, sizeof(ntHeader), &dwRead, NULL);
  23. if( dwRead == sizeof(ntHeader) )
  24. {
  25. if( ntHeader.Signature == IMAGE_NT_SIGNATURE )
  26. {
  27. bValid = true;
  28. }
  29. }
  30. }
  31. }
  32. }
  33. if( bValid)
  34. ::MessageBox(NULL, "It‘s a PE", "PE FILE", MB_OK);
  35. else
  36. ::MessageBox(NULL, "It‘s not a PE", "PE FILE", MB_OK);

1、PE    件格式的背景和由来:

在开始介绍PE 结构之前,有必要向读者提一提常用的PE件结构分析工具:Win32 SDK

提供的 DUMPBIN 可以转储PE文件和COFF OBJ/LIB 件;Borland 的使用者可用TDUMP 观

察PE 文件,但TDUMP 不支持COFF OBJ。

2、PE 件的顺序结构:

我们可以把PE 的内存映象结构用下面的图示简要的表示出来:

DOS   MZ   HEADER

DOS   STUB

PE

Header Signature( “PE/0/0”)

FileHeader

OptionalHeader

Section Table(array of IMAGE_SECTION_HEADER)

.text

.data

.edata

.idata

.reloc

COFF   Line Number

COFF   Symbols

Code   View Debug nformation

2.1、DOS header 和 DOS Stub:

所有的 PE 文件 (或32位的DLLS)都必须以一个简单的DOS MZ header 为起始

(IMAGE_DOS_HEADER 结构体)。在实际中,除了e_lfanew (PE header 的 件偏移量)我

们可以不必太关心其余成 数据。DOS Stub只是提供了PE 文件在DOS 下执行时 ,DOS

会把它当作有效的执行文件而顺利执行。通常会在屏幕上输出 " This program cannot

run in DOS mode " 之类的提示语。程序员也可以改变DOS Stub,根据自己的意图实现

完整的 DOS 代码。

2.2、PE Header:

PE 表头内含程序代码和各种资料的大小位置、适用的操作系统、堆栈(stack)最初大小

等等重要信息。犹如执行 件的纲目。整个PEHeader是一个IMAGE_NT_HEADERS 结构体,

在Win32 SDK 中定义如下:

[cpp] view plaincopy

  1. typedef struct _IMAGE_NT_HEADERS
  2. {
  3. DWORD  Signature;//PE 标记,值为50h,45h,00h,00h (ASCII:”PE/0/0”)
  4. IMAGE_FILE_HEADER   FileHeader;
  5. IMAGE_OPTIONAL_HEADER32   OptionalHeader;
  6. }IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;

PE ignature 为校验标记,由连接器产生。通常装载器通过指向此位的指针e_lfanew (DOS

header 中)来检验此文件是否为PE 格式。FileHeader域包含了关于PE 文件物理分布的

一般信息, opionalHeader域包含了关于PE 文件逻辑分布的信息。显然可以pNTHeader=

dosHeader + dosHeader->e_lfanew 获得pe 头的地址。

2.2.1、File Header 结构域:

在Win32 SDK 中File Header 定义如下:

[cpp] view plaincopy

  1. typedef struct _IMAGE_FILE_HEADER
  2. {
  3. WORD        Machine;
  4. WORD        NumberOfSections;
  5. DWORD       TimeDateStamp;
  6. DWORD       PointerToSymbolTable;
  7. DWORD       NumberOfSymbols;
  8. WORD        SizeOfOptionalHeader;
  9. WORD        Characteristics;
  10. }IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;

各个成 的说明列表如下:

成员说

Machine 该文件运行所要求的CPU。对于Intel平台,该值是

IMAGE_FILE_MACHINE_I386(14Ch)。

NumberOfSections 文件的节数目。如果我们要在文件中增加或删除一个节,就需要修改

这个值。

TimeDateStamp 连接器创建文件时刻。从1969/12/31 4:00 P.M. 之后的总秒数。

PointerToSymbolTable COFF 符号表格的偏移位置,用于调试。

NumberOfSymbols COFF 符号表格中符号的个数,用于调试。

SizeOfOptionalHeader指示紧随本结构之后的OptionalHeader结构大小,必须为有效值。

Characteristics 关于文件信息的标记,比如文件是exe(0x0002) 还是dll(0x2000)在

实际应用中,有三个域对我们有用:NumberOfSections,SizeOfOptionalHeader和

Characteristics。我们通常不会改变SizeOfOptionalHeader和Characteristics的值,

如果要遍历节表就得使用 NumberOfSections。

2. 2。2、Option Header 结构域:

这是PE 表头的第三个成分。对于PE 文件而言,这一部分其实并不是可有可无。因为COFF

格式允许不同的设计者在标准的File header之后定义一个结构。Optional header 正是

PE 设计者认为在基本的File header 信息之外还需要的一些重要的信息。完整的结构定

义可以参考Win32 SDK 中的WINNT.H 头文件,这里列举了其中的重要成 :

成员说

AddressOfEntryPoint PE 装载器准备运行的PE          件的第一个指令的RVA。若要改变整个

执行的流程,可以将该值指定到新的RVA,这样新RVA 处的指令首先被执行。

ImageBase PE   件的优先装载地址。如果该值是400000h,PE装载器将尝试把文件装到

虚拟地址空间的400000h 处。若该地址区域已被其他模块占用,那PE 装载器会选用其他

空闲地址,这个过程我们把它叫做重定位。

SectionAlignment 内存中节对齐的粒度(granularity)。一旦装载到内存中,每个节(s

ection)保证从一个此值的倍数的虚拟地址开始。如果该值是4096                          (1000h),那么每节

的起始地址必须是4096 的倍数。

FileAlignment   件中节对齐的粒度。文件中 成每个section 的原始数据(raw   data)

保证是从一个此值的倍数的虚拟地址开始。如果该值是(200h),,那么每节的起始地址必

须是512 的倍数。

MajorSubsystemVersion

MinorSubsystemVersion win32子系统版本。若PE 文件是专门为Win32 设计的,该子系

统版本必定是4.0 否则不会有3维立体感对话框等。

SizeOfImage 内存中整个PE 映像体(map)的尺寸。它是所有头和节经过节对齐处理后的

大小。

SizeOfHeaders 所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以

以此值作为PE 文件第一节的文件偏移量。

Subsystem NT 用来识别PE 文件属于哪个子系统。对于大多数Win32程序,只有两类值:

Windows GUI 和 Windows    CUI  (控制台)。

DataDirectory IMAGE_DATA_DIRECTORY 结构数 。每个结构给出一个重要数据结构的

RVA,比如引入地址表等。在PE header 中,有很多地址指针是用RVA 来表示的。 RVA 代

表相对虚拟地址 (Relative Virtual Address)。简言之,RVA 是虚拟空间中到参考点的

一段距离,类似文件偏移量。当然它是相对虚拟空间里的一个地址,而不是文件头部。举

例来说,如果PE 文件装入虚拟地址(VA)空间的400000h 处,且进程从虚址401000h 开始

执行,我们可以说进程执行起始地址在RVA 1000h。每个RVA 都是相对于模块的起始VA

(Virtual Address)而言的。

2.2.3、Option  Header 的中的重要数据成 Data Directory:

Data Directory   定义为IMAGE_DATA_DIRECTORY 结构体数 ,每个数 元素给出一个重

要数据结构的RVA (Relative Visual Address相对虚地址),比如引入表地址、重定位

表地址。通常共16个成 。(详细的宏定义请参阅WIN32 SDK 的WINNT.H 头 件)

这个数 起到让装载器迅速的在内存中找到特定的节(section)的作用,减去了遍历节表

的麻烦。

2. 3、Section  Header:

紧接在PE header 的是section table (IMAGE_SECTION_HEADER)。每个表项包含有该节

的属性、偏移量等。如果PE 文件里有3节,那么此数据结构就有3个元素。为了更好的

理解PE header和Section header在PE 文件中的组织关系,我们可以把PE 文件看作一

逻辑磁盘,PE header 是boot 扇区而sections是各种文件,节表视为逻辑磁盘中的根目

录。节表定义如下:

[cpp] view plaincopy

  1. typedef truct _IMAGE_SECTION_HEADER
  2. {
  3. BYTE       Name[IMAGE_SIZEOF_SHORT_NAME];
  4. union
  5. {
  6. DWORD       PhysicalAddress;
  7. DWORD       VirtualSize;
  8. }Misc;
  9. DWORD       VirtualAddress;
  10. DWORD       SizeOfRawData;
  11. DWORD       PointerToRawData;
  12. DWORD       PointerToRelocations;
  13. DWORD       PointerToLinenumbers;
  14. WORD        NumberOfRelocations;
  15. WORD        NumberOfLinenumbers;
  16. DWORD       Characteristics;
  17. }IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

重要成 的说明列表如下:

成员说

Name 节名。长度不超过8字节。仅仅是个标记而已,注意这里不用null结束。

VirtualAddres 本节的RVA (相对虚拟地址)。PE装载器将节映射至内存时会读取本值。

如果域值是1000h,而PE 文件装在地址400000h 处,那么本节就被载到401000h。

SizeOfRawData 经过文件对齐处理后节尺寸,PE 装载器提取本域值了解需映射入内存的

节字节数。

PointerToRawData 这是节基于文件的偏移量,PE装载器通过本域值找到节数据在 件中

的位置。

----------------------- Page 4-----------------------

Characteristics 包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未

初始数据,是否可写、可读等。

2.4、Section域

节(section)是PE 文件真正内容的划分。每一节是是拥有共同属性的数据的集合。每节

都有相应的命名,不过这个命名不是太重要,只要是相同属性的内容都可以放进一节,命

名只是便于识别。下面重点介绍一下一些重要的段。

2.4.1、text section

此节一般包含有连接器连接的所有obj 目标 件的执行代码。这个执行代码块是一个大

的.text。不同于在DOS 下面的执行 件可以分成几部分。如果是使用的Borland C++,

其 编译器将产生的代码存于名为CODE 的区域,连接器连接到名为CODE 而不是.text 的

节中。

2.4.2、data section

.data 是初始化的数据块。这些数据块包括编译时被初始化的字符串常量、全局(globle)

和静 (static)变量。

2.4.3、bss section

任何没有初始化的全局和局部变量都会存放到.bss节中。这个节并不占用文件的储藏空

间,所以 RawDataOffset         总是为0。

2.4.4、rsrc section

该节包含模块的全部资源。如图标、菜单、位图等等。

2.4.5、idata section

.idata 包含其他外来的如DLL 中的函数及数据信息。PE 文件的每一个输入函数都 确的

列于该节中。

2.4.6、edata section

与.idata对应,.edata 是该PE 文件输出函数和数据的列表,以供其他模块引用。有的

PE 文件没有引出函数或数据,也就没有该节。

3、其余部分:

在节 (Sections)的后面是COFF 符号表格、COFF调试信息、COFF 行号信息。这些域对

我们的作用不大,有兴趣的读者可参阅

时间: 2024-10-08 18:04:24

利用PE数据目录的导入表获取函数名及其地址的相关文章

在C语言中以编程的方式获取函数名

调试常用的 __FILE__, __FUNCTION__, __LINE__ 调试常用的 __FILE__, __FUNCTION__, __LINE__ 没想到 VC6 不支持 __FUNCTION__ 所以我写了如下的奇怪代码 //用来记录当前行和当前函数//也可说是记录 堆栈void log_stack(const char *file, int line, const char * function); //当然还要对 __FUNCTION__ 宏作点修饰,因为这个宏只是在函数里面才起作

vc 获取函数名称真实地址

首先写一个很简单的main函数: int main(){ printf("main的地址(?):%08x",main); } 单步调试,可得知 main函数的真实入口地址是:00be91a0 然而我们控制台输出的值是 为什么会出现这样的差别呢?院子里有一篇大牛写的有关注入的文章:http://www.cnblogs.com/fanzhidongyzby/archive/2012/08/30/2664287.html,里面就提到了这个问题. 其中提到一个解析真实地址的算法: //将函数地

PE知识复习之PE的绑定导入表

一丶简介 根据前几讲,我们已经熟悉了导入表结构.但是如果大家尝试过打印导入表的结构. INT IAT的时候. 会出现问题. PE在加载前 INT IAT表都指向一个名称表. 这样说是没错的. 但是如果你打印过导入表.会发现一个问题. 有的EXE程序.在打印IAT表的时候.发现里面是地址. 原因: 我们的PE程序在加载的时候.我们知道. PE中导入表的子表. IAT表.会填写函数地址. 但是这就造成了一个问题.PE程序启动慢.每次启动都要给IAT表填写函数地址. 我们可不可以在文件中就给填写好.

导入表结构复习 导入模块,函数名称,地址遍历

关于PE结构导入表,以前只是手动分析,没有通过编程来实现.而且PE文件结构,不巩固的话,一段时间之后就会忘记,所以记录下这次试验,为IAT挂钩做好准备,也算是复习一下. 测试 环境:windows xp sp3 IDE: vs 2008 sp1 build:release #include <windows.h> #include <stdio.h> #include <DbgHelp.h> #pragma comment(lib,"dbghelp.lib&q

[转载]通过PsGetCurrentProcess函数获取函数名

本文转载自: http://www.cnblogs.com/xiaojinma/archive/2012/12/07/2806543.html 通过PsGetCurrentProcess函数来获取当前调用驱动的进程的EPROCESS结构的地址.EPROCESS结构的0x174偏移处存放着进程名. 思路如下:驱动程序的加载函数DriverEntry是运行在System进程中的.(1) 通过PsGetCurrentProcess可以获取System进程的内核EPROCESS结构的地址,(2) 从该地

Python3基础 from...import...as 解决局部导入时的函数名重复问题

? ???????Python : 3.7.3 ?????????OS : Ubuntu 18.04.2 LTS ????????IDE : pycharm-community-2019.1.3 ??????Conda : 4.7.5 ???typesetting : Markdown ? code_module_same_fun_name.py """ @Author : 行初心 @Date : 2019/7/7 @Blog : www.cnblogs.com/xingch

函数名&amp;函数名取地址

有时看到如下的代码: /*****************************/ #include <stdio.h> #include <string.h> #include <stdlib.h> void test() { printf("123456\n"); } int main(int argc, char *argv[]) { printf("0x%x\n",test); printf("0x%x\n&q

JavaScript通过new Error() hack方法从函数体内部获取函数名

'use strict' function getFuncName (){ var callerName; { let reg = /(\w+)@|at ([^(]+) \(/g; reg.exec(new Error().stack); //跑一次exec, 跑到第二个匹配 let regResult = reg.exec(new Error().stack); callerName = regResult[1] || regResult[2]; } console.log(callerNam

IDA分析脱壳后丢失导入表的PE

1. 问题 一些程序经过脱壳后(如用OD的dump插件),一些导入表信息丢失了,导致拖入IDA后看不到API的信息(如右图所示,第一个红圈处实际是GetCurrentProcessId),给分析造成极大影响.   2. 分析 从OD来看,41F480处的值就是API GetCurrentProcessId的地址,也就是壳修改后的导入表的位置,该地址在.rsrc Section内,即程序的资源段内,那么我们需要修复其导入表,以指向资源段内的地址 3. 修复导入表 <加密与解密>第3版的13.4.