July 19, 2022

关于逆向工程核心原理-PE文件

PE结构体

一:DOS头

1
2
3
4
5
6
7
IMAGE_DOS_HEADER 	//DOS头
{

WORD e_magic; //DOS签名:4D5A("MZ")
...
LONG E_lfanew; //NT头的偏移
}

二:DOS存根

  1. 就在DOS头下方

  2. 大小不固定

  3. 作用是当运行在DOS下 会显示无法在DOS下运行并退出

  4. DOS存根是可选项

三:NT头

image-20211228214923117

1
2
3
4
5
6
IMAGE_NT_HEADERS	//NT头
{
DOWRD Signature; // PE签名
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头
}

四:NT头:文件头

image-20211228215505782

1
2
3
4
5
6
7
8
9
IMAGE_FILE_HEADER	//NT头:文件头
{
WORD Machine; //机器码代表了什么CPU架构
WORD NumberOfSections; //节区数
DWORD TimeDateStamp; //创建文件的时间
...
WORD SizeOfOptionalHeader; //指出NT头:可选头的的大小
WORD Characteristics; //标识文件属性,是否可运行,是否是DLL文件等
}

五:NT头:可选头

image-20211228220124558

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IMAGE_OPTIONAL_HEADER32
{
WORD Magic; //魔术字 32位是10B 64位的20B
...
DWORD AdressOfEnterPoint; //程序最先执行的代码起始地址 是EP的RVA的值
...
DWORD ImageBase; /*不同类型文件有不同的优先装载地址
EXE DLL -> 0x7FFF FFFF
SYS -> 内核区域0x8000 0000 ~ 0xFFFF FFFF
DLL文件一般是 1000 0000
执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后将EIP的值
设置为ImageBase + AddressOfEntryPoint
*/
DWORD SectionAlignment; //内存最小单位
DWORD FileAlignment; //磁盘最小单位
...
DWORD SizeOfImage; //PE文件映像在虚拟内存所占大小
DOWRD SizeOfHeaders; //PE头大小
...
WORD Subsystem; //文件类型 分.sys或.exe .dll等
...
DWORD NumberOfRvaAndSizes; //指出DataDirectory(最后一个成员)数组的个数
IMAGE_DATA_DIRECTORY DataDirectory; //结构里的重要成员有 EXPORT/IMPORT/RESOURCE/TLS
}

六:节区头

image-20211228223047814

1
2
3
4
5
6
7
8
9
10
11
12
13
IMAGE_SERCTION_HEADER	//节区头
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //没有必须ASCLL码的限制 不用NULL结尾 里面都可以放
union
{
DWORD PhysicalAddress;
DWORD VirtualSize; //内存中节区所占大小
} Misc;
DWORD VirtualAddress; //内存中的节区起始地址(RVA)
DWORD SizeOfRawData; //该节区的在文件基地址
DWORD PointToRawData; //该节区的在内存基地址
DWORD Characteristics; //节区属性(bit OR)
}

后记:今天又重新看了遍第十三章,现在是2021.12.8 23:04,第十三章的重点一部分是我上面写的这样,这样写一遍有助我好好理解整个格式,还要两个重点是EAT和IAT的过程,该自己走一遍,但脑子里依然回响着作者的一句话,”先学这么多就好”,虽然挺不喜欢东西放着不去学,但经常控制不住自己,就像平常我不会特意去看AES SM4加密原理等,碰到对应的题目了,我才会老实去看,也许明天就去试试了(现在在书上仿佛看的已经比较明白了)。

  1. 第十三章 IAT

  2. 第十三章 EAT

  3. 奇怪的PE文件探

IAT

IAT的提供机制与隐式链接有关,即程序开始时既一同加载DLL,程序终止时再释放占用内存

IMAGE_IMPORT_DESCRIPTOR

执行一个普通程序往往需要导入多个库,导入多少库就有多少个IMAGE_IMPORT_DESCRIPTOR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
union
{
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT(Import Name Table) address 指向IMAGE_IMPORT_BY_NAME的地址(RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // 库名称字符串的地址(RVA)
DWORD FirstThunk; // IAT(Import Address Table) IAT的地址(RVA)
} IMAGE_IMPORT_DESCRIPTOR;

typedef struct _IMAGE_IMPORT_BY_NAME
{
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
  1. 读取IID的Name的成员,获取库名的字符串 (“kernel32.dll”)

  2. 装载相应库 -> LoadLibrary(“kernel32.dll”)

  3. 读取IID的OriginalFirstThunk成员,获取INT地址

  4. 逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)

  5. 使用IMAGE_IMPORT_BY_NAME的Hint (ordinal) 或Name项,获取相应函数的起始地址

    用语句表达就是 GetProcAddress(“GetCurrentThreadId”)

  6. 读取IID的FirstThunk (IAT) 成员,获取IAT地址

  7. 将上面获得的函数地址输入相应的IAT数组值

  8. 重复4~7步骤,直到INT结束(遇到NULL)

现在来跟书上看看IAT的步骤看看装载IAT过程

0x00 查找IID数组位置

查找IID,就在可选头的第二个就是IAT,第一个是EAT

第一个4字节是虚拟地址,第二个4字节是Size成员

因为是在文件里,所以RVA公式算出是 0x6A04

(都是在.text节区 内存基地址是0x1000 文件基地址是0x400)

image-20220223162115882

0x01 分析IID数组各成员

跳转到6A04可查看IID数组

image-20220224122134573

直接拿书上的表了

image-20220223164425919

1. 库名称(Name)

从这可以查看库名称

image-20220223170008790

2. OriginalFirstThunk – INT

从库中导入的API函数名称字符串地址

image-20220223170440012

3. IMAGE_IMPORT_BY_NAME

从7A7A转成6E7A跳过去查看一下

一开始的000F是库函数的固有编号 (为ordinal)

后面是字符串以00结尾

image-20220223170748422

4. FirstThunk - IAT

RVA: 12C4

RAW: 6C4

文件偏移6C4~6EB为IAT数组,对应comdlg32.dll库

第一个元素被硬编码成76324906,实际无意义,notpad.exe文件加载到内存时,准确的地址会取代该值(不同的系统值会不一样)

image-20220223170953845

我自己电脑运行notpad.exe的IAT

image-20220223172830551

EAT

通过EAT才能准确求得从相应库中导出函数的起始地址,且PE文件仅有一个结构体来说明库函数的导出信息

IMAGE_EXPORT_DIRECTORY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics;
DWORD TimeDateStamp; // creation time date stamp (创建时间日期戳)
DWORD MajorVersion;
DWORD MinorVersion;
DWORD Name; // address of library file name (库文件名地址)
DWORD Base; // ordinal base (序数基数)
DWORD NumberOfFunctions; // number of functions
DWORD NumberOfNames; // number of names
DWORD AddressOfFunctions; // address of function start address array
DWORD AddressOfNames; // address of function name string array
DWORD AddressOfOrdinals; // address of ordinal array
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

来个书上的解析图

image-20220223211435836

从库中获取函数地址的API为 GetProcAddress() 函数,该API引用EAT来获取指定API的地址,下面写下获取函数地址的流程

  1. 利用 AddressOfNames 成员转到 “函数名称数组”

  2. “函数名称数组” 中存储着字符串地址。通过比较(strcmp)字符串,查找指定的函数名称(此时数组的索引值称为name_index)

  3. 利用 AddressOfNameOrdinal 成员,转到 ordinal 数组

  4. 在 ordinal 数组中通过 name_index 查找相应 ordinal 值

  5. 利用 AddressOfFunctions 成员转到 “函数地址数组”(EAT)

  6. 在 “函数地址数组” 中将刚刚求得的 ordinal 用作数组索引,获得指定函数的起始地址

(也就是上面1到5步都是找获得确定索引值,然后从EAT起始地址通过索引值找到正确地址)

PS: 对于没有函数名称的导出函数,可以通过 Ordinal 查找它们的地址。从Ordinal值中减去IMAGE_EXPORT_DIRECTORY.Base成员后得到一个值,使用该值作为“函数地址数组”的索引。

0x00 查找IED数组位置

查找IED,与IID同理

image-20220224084814903

0x01 分析IED数组各成员

转到到IA2C即可查看IED数组成员

image-20220224085228942

书上的表

image-20220224121006846

image-20220224121029466

1. 函数名称数组

书上的和我有点不一样,我的是RVA = 3538h,所以RAW = 2938h

现在查找 ”AddAtomW“ 函数,只要查找到第三个

image-20220224085809768

2. 查找指定函数名称

RVA = 4BB3h -> RAW = 3FB3h

成功查找到,且以00结尾(第三个元素,所以数组索引为2)

image-20220224090029222

3. Ordinal数组

RVA = 441Ch -> RAW = 381Ch

都是两字节组成的数组(ordinal数组中各元素大小为2个字节)

image-20220224090425672

4. ordinal

从步骤2获得的Index值(2)应用到步骤3中的Ordinal数组即可求得Ordinal(2)

​ AddressOfNameOrdinals[index] = ordinal (index = 2, ordinal = 2)

PS: 还没碰到无函数名称索引,如果碰到到时候ordinal的作用会具象一点

5. 函数地址数组 - EAT

RVA = 2654h -> RAW = 1A54h

于是拿索引值为2,可得到值326D9h

image-20220224104030981

6. AddAtomW函数地址

我的kernel32.dll的imagebase是7C90 0000h

image-20220224104216294

因此AddAtomW的实际地址为(7C80 0000 + 326D9h = 7C83 26D9h)

7. 验证成果!!

olldbg验证一下

image-20220224104407516

没问题!

2022 7.19

没想到过了这么久又回来了, 发现文中几处不对的地方, 这回是要将给别人听了, 自己做PPT理了一遍, 更清楚了!

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.