Ox00 前言

Ox01 PE文件结构

1. 基本概念

  • PE文件

    • 是一种文件格式,目前所学的 exe,dll,sys等都是PE文件

    • 通过检查DOS头的**"MZ"(0x5A4D)和NT头的"PE"(0x00004550)**来判断一个文件是不是有效的PE文件

  • 术语解释

    • RVA: 数据在虚拟内存中的偏移地址

    • VA: 数据在虚拟内存中的绝对地址

      • VA = 实际加载基址 + RVA
    • FOA: 数据在文件中偏移地址

      • FOA = 需要转换的RVA - 所在的区段RVA + 所在区段的FOA.

2. PE 头信息

  • IMAGE_DOS_HEADER

    • e_magic: "MZ" - 0x5A4D

    • e_lfanew:指向NT头的偏移

  • IMAGE_NT_HEADERS

    • signature: "PE" - 0x00004550

    • IMAGE_FILE_HEADER

      • NumberOfSections:区段的数量

      • SizeOfOptionalHeader: 扩展头的大小,会被用于查找区段表

      • Characteristics:可以判断当前是不是DLL是不是32位重定位已分离(表示不需要重定位)

    • IMAGE_OPTIONAL_HEADER32

      • Magic:CPU架构:x32(0x10B)x64(0x20B)

      • AddressOfEntryPoint:程序的入口点(OEP) RVA

      • ImageBase:镜像的默认加载基址

      • SectionAlignment :内存对齐粒度,通常是0x1000,要求大于文件粒度

      • FileAlignment:文件对齐粒度,通常是0x200, 拉伸

      • SizeOfImage:镜像大小,即内存中的大小最后一个区段的RVA + 最后一个区段的大小

      • SizeOfHeaders:整个PE头部大小,通常是 0x400

      • DllCharacteristics :特征标识0x40 有随机基址

      • NumberOfRvaAndSizes:数据目录表个数 0x10

      • DataDirectory:数据目录表

    • IMAGE_SECTION_HEADER(重要)

      • Name[8]:区段名

      • virtualSize:内存中大小

      • VirtualAddress:内存位置RVA

      • SizeOfRawData:文件中大小

      • PointerToRawData:文件位置FOA

      • Characteristics:区段属性(可读可写可执行),是否有代码


3. 导入表

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;	// INT
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain; 
    DWORD   Name;					// 模块名称
    DWORD   FirstThunk;				// IAT 
} IMAGE_IMPORT_DESCRIPTOR;

导入表中保存的是一组 以全0为结尾MAGE_IMPORT_DESCRIPTOR 结构。

  • INT:导入名称表,指向了一组以0结尾IMAGE_THUNK_DATA 结构,可以找到函数的ID或名称,但是有些时候其中会被填充为0,在文件和内存中它们保存的值是相同的。

    • 当 IMAGE_THUNK_DATA 字段的最高位为1,表示这是一个序号导入的函数

      • 序号的值为 0x0000???? & 0x0000FFFF = 0x0000????
    • 最高位为0,表示这是一个名称导入的函数,

      • 名称和序号保存在 0x???????? 的位置 IMAGE_IMPORT_BY_NAME
  • IAT:导入地址表,在文件中存放的是INT的数据,当程序被加载到内存时,会被填充为函数的真实地址

  • image.png


4. 导出表

typedef struct _IMAGE_EXPORT_DIRECTORY {
    // 省略了无意义的字段
    DWORD   Name;					// 模块名RVA
    DWORD   Base;					// 序号的起始数值
    DWORD   NumberOfFunctions;		// 函数的数量
    DWORD   NumberOfNames;			// 有名字的函数的数量
    DWORD   AddressOfFunctions;     // 导出地址表的的RVA 【DWORD】
    DWORD   AddressOfNames;         // 导出名称表的RVA【DWORD-RVA】
    DWORD   AddressOfNameOrdinals;  // 导出序号表的RVA【WORD】
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • 导出表只能有一个
  • 名称表的数量和序号表的数量相同
  • 导出函数的序号 = 基址(Base) + 索引(函数地址表下标)

image.png


5. 重定位表

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
  • 重定位表由多组重定位块组成,每个重定位块保存的是一个分页(RVA)中需要重定位的所有数据。
  • 重定位项的个数 = (SizeOfBlock– sizeof(IMAGE_BASE_RELOCATION)) / 2
  • 每一个重定位项都是一个WORD类型的值,标识了当前数据的**类型**以及偏移(相对VirtualAddress)
  • 需要重定位的数据所在的位置 = 实际加载基址 + VirtualAddress + 重定位数据的偏移,其中保存的是一个偏移
  • 重定位公式:需要重定位的地址 - 默认加载基址 + 实际加载基址

6. 资源表

  • 三层结构

    • 一层:资源种类;二层:此种资源的个数,名称; 三层:资源数据

    • 每一层都是 IMAGE_RESOURCE_DIRECTORY 开始,后面跟着一组 IMAGE_RESOURCE_DIRECTORY_ENTRY结构体,保存的是资源的类型和名称

      • 结构体个数 = NumberOfNamedEntries + NumberOfIdentries
  • 第一层:第二层:

    • 资源的类型名称,当 NameIsString 为1时,标识这是一个自定义的名称,使用 NameOffset字段可以找到 IMAGE_RESOURCE_DIR_STRING_U 字段,其中保存了字符串名称。
  • 第三层:

    • 指向 IMAGE_RESOURCE_DATA_ENTRY,保存了数据的偏移大小.

7. 其他表

  • TLS表 IMAGE_TLS_DIRECTORY
    • 使用 __declspec(thread) int nNum = 99; 定义TLS变量
    • TLS 函数的原型如下,和DllMain相比,它们只有返回值类型不同
    • Void NTAPI t_TlsCallBackA(PVOID DllHandle,DWORD Reason,PVOID Red);
    • TLS 函数的调用顺序位于main函数前,通常用于进行反调试

Ox10 程序OEP特征

程序特征的识别

  • 通过区段进行识别

    • 参考逆向 05 课上笔记
  • 通过连链接器版本识别
    | VS | 连接器版本 |
    | :---: | :---: |
    | VC 4.2 | XXX |
    | VC 5.0 | 5.0 |
    | VC 6.0(98) | 6.0 |
    | VS 2003 | 7.0 / 7.1 |
    | VS 2005 | 8.0 |
    | VS 2008 | 9.0 |
    | VS 2010 | 10.0 |
    | VS 2012 | 11.0 |
    | VS 2013 | 12.0 |
    | VS 2015 | 14.0 |
    | VS 2017 | 14.2 |
    | VS 2019 | 14.22 |

  • 通过入口特征进行识别

    • VC 6.0 & 易语言

      • sub esp, 0x58; call dword ptr [GetVersion];
    • Delphi 程序

      • 连续的5个call,紧跟着一串0,第一个call内调用了 GetModuleHandle
    • Borland C++ 程序

      • 第一条语句是JMP ,有明显的 "fb:C++HOOK" 字符串
    • C\C++ 程序:

      • debug版本为连续的两个call,release版本为 call jmp

Ox20 IAT修复的方法

  • 脱壳的基本步骤
    • 找OEP -> [还原IAT] -> dump文件 -> 修复IAT
    • 通过在 OEP 位置查找 FF15 和 FF25 可以找到 IAT 设置硬件写入断点,重新运行程序
  • 具体的步骤见课上笔记
    • aspck: 抹除重定位
    • FSG: IAT表中间有 FFFF
  • ImpRec

Ox30 常见脱壳技巧

  • ESP定律
  • 原程序代码段内存执行断点
  • 单步跟踪