windows原理02

导出表

什么是导出,什么是导入,为什么又这两种行为?

​ 一个程序的运行是由多个部分组成的,通常是由一个exe和多个dll组成。dll会提供函数,变量给其他模块使用。并非dll中所有的函数都能够提供给其他的模块使用,只有在编写dll的时候,函数,变量被导出了,才能提供给其他模块使用,导出表就是专门用来记录本文件导出信息的一个数据结构

​ 本模块使用了其他哪些模块提供的哪些函数,需要记录这些信息,记录的信息就在导入表中

怎么找到导出表?

​ 通常来说,dll文件提供到处的函数给其他模块使用,那么我要分析导出表,应该分析一个dll文件。

img

导出表的RVA:0x016CC0 FOA:0x05EC0

大小:0x162

ctrl+g转到0x5EC0就可以查看到导出表的结构

img

如何解析导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

导出表结构有三个非常重要的表:

  • 导出函数地址表:存放的是导出函数的RVA地址
  • 导出函数名称表:存放的是导出函数名称的RVA
  • 导出函数序号表:存放的是导出函数的序号
  1. 名称表元素的个数和序号表元素的个数是相同的

  2. 地址表中的元素可能会比序号表和名称表元素个数要多

  3. 因为windows的PE文件支持两种导出方式

    3.1 名称导出 函数既有名称又有序号

    3.2 序号导出 函数只有序号,没有名称

    无论哪种导出方式,肯定都有函数地址。

  4. 地址表中多出来的,就是没有名称的函数。或者是无效的函数

img

img

函数地址表的RVA:0x016CE8 FOA:0x5EE8

函数名称表的RVA:0x016CF0 FOA:0x5EF0

函数序号表的RVA:0x016CF8 FOA:0x5EF8

img

使用代码解析导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#include <stdio.h>
#include <Windows.h>
#include <tchar.h>

char* ReadFileToMemory(const char* pFilePath)
{
//获取文件句柄
HANDLE hFile = CreateFileA(pFilePath,
GENERIC_READ | GENERIC_WRITE,
FALSE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("无效句柄\n");
return 0;
}
//获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//申请内存空间
char* pBuf = new char[dwFileSize] {};
if (!pBuf)
{
CloseHandle(hFile);
printf("申请内存失败\n");
return 0;
}
//读取文件内容到内存中
DWORD dwRead;
ReadFile(hFile, pBuf, dwFileSize, &dwRead, NULL);
return pBuf;
}

bool IsPeFile(char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("不是PE文件\n");
return false;
}
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
if (pNt->Signature != IMAGE_NT_SIGNATURE)
{
printf("不是PE文件\n");
return false;
}
return true;
}

DWORD RVAtoFOA(DWORD dwRVA, char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
//区段个数
DWORD dwCount = pNt->FileHeader.NumberOfSections;
//区段首地址
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);

for (DWORD i = 0; i < dwCount; i++)
{
//FOA = RVA - 内存中区段首地址 + 文件中区段首地址
if (dwRVA >= pSec->VirtualAddress &&
dwRVA < pSec->VirtualAddress + pSec->SizeOfRawData)
{
return dwRVA - pSec->VirtualAddress + pSec->PointerToRawData;
}
pSec++;
}
return 0;
}

void ShowExportTable(char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
//找到导出表(数据目录表的第一项)
PIMAGE_DATA_DIRECTORY pData = &pNt->OptionalHeader.DataDirectory[0];
//导出表RVA转FOA
DWORD dwExportFOA = RVAtoFOA(pData->VirtualAddress, pBuf);
//得到导出表在文件中的具体位置 = FOA + pBuf
PIMAGE_EXPORT_DIRECTORY pExport =
(PIMAGE_EXPORT_DIRECTORY)(dwExportFOA + pBuf);
//解析导出表
printf("模块名:%s\n", RVAtoFOA(pExport->Name, pBuf) + pBuf);
//导出地址表中的函数个数
//pExport->NumberOfFunctions;
//导出名称表中函数名称个数
//pExport->NumberOfNames;
DWORD* pFuncAddr =
(DWORD*)(RVAtoFOA(pExport->AddressOfFunctions, pBuf) + pBuf);
DWORD* pFuncNameAddr =
(DWORD*)(RVAtoFOA(pExport->AddressOfNames, pBuf) + pBuf);
WORD* pFuncOrdinalAddr =
(WORD*)(RVAtoFOA(pExport->AddressOfNameOrdinals, pBuf) + pBuf);

for (int i = 0; i < pExport->NumberOfFunctions; i++)
{
if (pFuncAddr[i] == 0)
{
continue;
}
//判断是否存在函数名称,如果存在则输出
//判断条件:序号表中存在的序号都是有名称的
bool bFlag = FALSE;
for (int j = 0; j < pExport->NumberOfNames; j++)
{
if (i == pFuncOrdinalAddr[j])
{
//有名称的函数
bFlag = TRUE;
DWORD dwNameRVA = pFuncNameAddr[j];
printf("函数序号:%d 函数名称[%s]\n",
i + pExport->Base,
RVAtoFOA(dwNameRVA, pBuf) + pBuf);
break;
}
}
if (!bFlag)
{
//i+pExport->Base 调用号
printf("函数序号:%d 函数名称[NULL]\n",
i + pExport->Base);
}
}
}

int main()
{
char* pBuf = ReadFileToMemory("123.dll");
if (IsPeFile(pBuf))
{
ShowExportTable(pBuf);
}
delete pBuf;
return 0;
}

了解导出表的相关知识后有什么用?

能够获得一个模块任何导出函数的地址。相当于能够自己实现GetProcAddress。

如果导入地址表被破坏了,可以修复导入地址表。

可以检测IAT-HOOK,主要的思路就是获取IAT此位置原始的函数地址。

适用于不方便使用GetProcAddress而需要通过函数名或者序号获取函数地址的情况。

导入表

什么是导入表

当自己的模块需要使用其他模块提供的函数的时候,就需要在自己的模块中记录这些信息,记录这些信息的位置就是导入表。

img

IMAGE_THUNK_DATA 的值最高位为1时,表示函数是以序号方式输入,这时低31为被当作函数序号。当最高位是0时,表示函数是以字符串类型的函数名方式输入的。

三个重要字段:

OriginalFirstThunk:INT的RVA

FirstThunk:IAT的RVA

Name:导入的dll的名称的RVA

INT和IAT在还是文件的时候,里面存储的东西是一样的,都是函数名称的RVA。

在程序没有运行的时候,无法得到此模块会加载到什么位置,也就无法得到函数地址,所以IAT在文件中存储的是名称。

当程序运行起来之后,系统会区将IAT填充上函数的地址

所有调用其他模块函数的代码全部都是 call ds:[IAT地址]

如何找到导入表?

通过数据目录表的第二项,得到RVA,就能够找到IMAGE_IMPORT_DESCRIPTOR结构体的数组,数组以全零元素为结尾。

img

导入表结构的起始RVA:0x018154 FOA:0x6554

img

如何解析导入表

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; //0 for terminating null import descriptor
DWORD OriginalFirstThunk; //包含指向IMAGE_THUNK_DATA(输入名称表)结构的数组
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //当可执行文件不与被输入的dll进行绑定时,此字段为0
DWORD ForwarderChain; //第一个被转向的API的索引
DWORD Name; //指向被输入的dll的ascii字符串的RVA
DWORD FirstThunk; //指向输入地址表(IAT)的RVA,IAT是一个IMAGE_THUNK_DATA结构的数组
} IMAGE_IMPORT_DESCRIPTOR;

img

INT的RVA:0x0182C8 FOA:0x66C8

img

IAT的RVA:0x018124 FOA:0x6524

img

Name的RVA:0x018306 FOA:0x6706

img

如何用代码解析?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <stdio.h>
#include <Windows.h>
#include <tchar.h>

char* ReadFileToMemory(const char* pFilePath)
{
//获取文件句柄
HANDLE hFile = CreateFileA(pFilePath,
GENERIC_READ | GENERIC_WRITE,
FALSE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("无效句柄\n");
return 0;
}
//获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//申请内存空间
char* pBuf = new char[dwFileSize] {};
if (!pBuf)
{
CloseHandle(hFile);
printf("申请内存失败\n");
return 0;
}
//读取文件内容到内存中
DWORD dwRead;
ReadFile(hFile, pBuf, dwFileSize, &dwRead, NULL);
return pBuf;
}

bool IsPeFile(char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("不是PE文件\n");
return false;
}
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
if (pNt->Signature != IMAGE_NT_SIGNATURE)
{
printf("不是PE文件\n");
return false;
}
return true;
}

DWORD RVAtoFOA(DWORD dwRVA, char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
//区段个数
DWORD dwCount = pNt->FileHeader.NumberOfSections;
//区段首地址
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);

for (DWORD i = 0; i < dwCount; i++)
{
//FOA = RVA - 内存中区段首地址 + 文件中区段首地址
if (dwRVA >= pSec->VirtualAddress &&
dwRVA < pSec->VirtualAddress + pSec->SizeOfRawData)
{
return dwRVA - pSec->VirtualAddress + pSec->PointerToRawData;
}
pSec++;
}
return 0;
}

void ShowImportTable(char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
//找到导入表(数据目录表中的第二项)
DWORD dwImportRVA =
pNt->OptionalHeader.DataDirectory[1].VirtualAddress;
PIMAGE_IMPORT_DESCRIPTOR pImport =
(PIMAGE_IMPORT_DESCRIPTOR)(RVAtoFOA(dwImportRVA, pBuf) + pBuf);
//解析导入表(以全0位结尾)
while (pImport->Name)
{
//导入模块名
printf("导入模块名称:%s\n", RVAtoFOA(pImport->Name, pBuf) + pBuf);
//导入名称表(以0结构为结尾)
PIMAGE_THUNK_DATA pThunkINT =
(PIMAGE_THUNK_DATA)(RVAtoFOA(pImport->OriginalFirstThunk, pBuf) + pBuf);
//导入地址表(以0结构为结尾)
PIMAGE_THUNK_DATA pThunkIAT =
(PIMAGE_THUNK_DATA)(RVAtoFOA(pImport->FirstThunk, pBuf) + pBuf);
while (pThunkINT->u1.AddressOfData)
{
//判断导入函数的导入方式
if (IMAGE_SNAP_BY_ORDINAL32(pThunkINT->u1.AddressOfData))
{
//序号导入
printf("\t函数序号:[%d]函数名称:[NULL]\n",
pThunkINT->u1.AddressOfData & 0xFFFF);
}
else
{
//名称导入
DWORD dwNameFOA = RVAtoFOA(pThunkINT->u1.AddressOfData, pBuf);
PIMAGE_IMPORT_BY_NAME pName =
(PIMAGE_IMPORT_BY_NAME)(dwNameFOA + pBuf);
printf("\t函数序号:[%d]函数名称:[%s]\n",
pName->Hint, pName->Name);
}
//下一个导入函数
pThunkINT++;
//pThunkIAT++;如果是加载到内存中再做遍历,在磁盘上时,于INT是一样的
}
//下一个导入表(下一个导入模块)
pImport++;
}
}

int main()
{
char* pBuf = ReadFileToMemory("123.dll");
if (IsPeFile(pBuf))
{
ShowImportTable(pBuf);
}
delete pBuf;
return 0;
}

知道了导入表相关内容有什么用?

知道了模块之间是如何进行配合的。

​ call [0x…]

可以IAT-HOOK,替换IAT表中的内容,就可以HOOK

知道一个exe用了哪些模版的哪些函数。可以根据函数名猜测供功能,利于分析。

TLS表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<windows.h>

#pragma comment(linker, "/INCLUDE:__tls_used")

// TLS变量
__declspec (thread) int g_nNum = 0x11111111;
__declspec (thread) char g_szStr[] = "TLS g_nNum = 0x%p ...\r\n";
// TLS回调函数A
void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Red) {
if (DLL_THREAD_DETACH == Reason) // 如果线程退出则打印信息
printf("t_TlsCallBack_A -> ThreadDetach!\r\n");
return;
}
// TLS回调函数B
void NTAPI t_TlsCallBack_B(PVOID DllHandle, DWORD Reason, PVOID Red) {
if (DLL_THREAD_DETACH == Reason) // 如果线程退出则打印信息
printf("t_TlsCallBack_B -> ThreadDetach!\r\n");
return;
}
/*
* 注册TLS回调函数,".CRT$XLB"的含义是:
* CRT表明使用C RunTime机制
* X表示标识名随机
* L表示TLS callback section
* B其实也可以为B-Y的任意一个字母
*/
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback[] = {
t_TlsCallBack_A,
t_TlsCallBack_B,
NULL };
#pragma data_seg()


DWORD WINAPI t_ThreadFun(PVOID pParam) {
printf("t_Thread -> first printf:");
printf(g_szStr, g_nNum);
g_nNum = 0x22222222; // 注意这里
printf("t_Thread -> second printf:");
printf(g_szStr, g_nNum);
return 0;
}
int _tmain(int argc, _TCHAR* argv[]) {
printf("_tmain -> TlsDemo.exe is runing...\r\n\r\n");
CreateThread(NULL, 0, t_ThreadFun, NULL, 0, 0);
Sleep(100); // 睡眠100毫秒用于确保第一个线程执行完毕
printf("\r\n");
CreateThread(NULL, 0, t_ThreadFun, NULL, 0, 0);
system("pause");
return 0;
}

延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <windows.h>
//包含头文件和库文件
#include <delayimp.h>
#pragma comment(lib, "Delayimp.lib")

//设置“连接器”>“输入”>“延迟加载的DLL”选项
//中的值为我们需要延迟加载的DLL名称(大小写必须完全一致)

int _tmain(int argc, _TCHAR* argv[])
{
MessageBox(0, 0, 0, 0);
return 0;
}