April 13, 2022

关于逆向工程核心原理-通过修改PE加载DLL

(简称关于逆向工程核心原理的第二十五章学习)

通过PE文件加载DLL

其实就是通过修改导入表来加载DLL,简单说下实验要用的

TextView

呜呜呜最简单的win32编程,寒假为什么没有好好学,看着那些代码似曾相识,没时间后悔了

就是一个简单的文本查看程序简易版Notepad

正常版的导入表,基地址转换也不用多说了

image-20220413103031354

TextView_Pacted

注进去咯,注意基地址也换了,因为把整个DLL导入表都换了个地方(后面细锁

image-20220413103316335

实验时间!

可以发现启动时自动加载,当程序启动时自动导入MyHack3.dlldll起了个线程自动下载了指定网站的页面

image-20220413103526009

MyHack.dll

开审!

DllMain()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DllMain函数的功能很简单
// 1. 创建线程(CreateThread)
// 2. 运行指定的线程过程(ThreadProc)
// 3. 在线程过程(ThreadProc)中调用DownloadURL()和DropFile()函数,下载指定网页并将其拖放到文本查看器
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
CloseHandle(CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL));
break;
}

return TRUE;
}

ThreadProc()

可以注意一下GetModuleFileName() API

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
DWORD WINAPI ThreadProc(LPVOID lParam)
{
TCHAR szPath[MAX_PATH] = {0,};
TCHAR *p = NULL;

OutputDebugString(L"ThreadProc() start...");

// 第一个参数的NULL,该API就检索当前进程的可执行文件的路径,也就是TextView的路径
// 之前用的好像都是dll的句柄 也就是找dll的路径
GetModuleFileName(NULL, szPath, sizeof(szPath));

if( p = _tcsrchr(szPath, L'\\') )
{
// 常规操作 把\\后的TextView.exe换成index.html
_tcscpy_s(p+1, wcslen(DEF_INDEX_FILE)+1, DEF_INDEX_FILE);

OutputDebugString(L"DownloadURL()");
// 下载文件,作者自写的
if( DownloadURL(DEF_URL, szPath) )
{
// 拖放到TextView
OutputDebugString(L"DropFlie()");
DropFile(szPath);
}
}

OutputDebugString(L"ThreadProc() end...");

return 0;
}

DownloadURL()

DownloadURL()函数会下载参数szURL中指定的网页文件,保存到szPath目录

PS: 作者写的DownloadURL是通过InternetOpen() InternetOpenUrl() InternetReadFile() API对URLDownLoad()的简单实现

这些API均在wininet.dll实现

URLDownloadToFile() API也就是之前用的,在urlmon.dll中提供

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
BOOL DownloadURL(LPCTSTR szURL, LPCTSTR szFile)
{
BOOL bRet = FALSE;
HINTERNET hInternet = NULL, hURL = NULL;
BYTE pBuf[DEF_BUF_SIZE] = {0,};
DWORD dwBytesRead = 0;
FILE *pFile = NULL;
errno_t err = 0;

// 初始化应用程序对WinlNet函数的使用
// 第一个参数 此名称用作HTTP协议中的用户代码(雾,那我用自己的应该也可以
// 第二个参数 从注册表检索代理或直接配置
hInternet = InternetOpen(L"ReverseCore",
INTERNET_OPEN_TYPE_PRECONFIG,
NULL,
NULL,
0);
if( NULL == hInternet )
{
OutputDebugString(L"InternetOpen() failed!");
return FALSE;
}

// 打开由完整的FTP或HTTP URL指定的资源
// hInternet, 该句柄必须有InternelOpeN()API的调用返回
// szURL, 该变量为指定的URL。仅支持ftp: http: https:
// INTERNET_FLAG_RELOAD, 强制从源服务器下载请求的文件
hURL = InternetOpenUrl(hInternet,
szURL,
NULL,
0,
INTERNET_FLAG_RELOAD,
0);
if( NULL == hURL )
{
OutputDebugString(L"InternetOpenUrl() failed!");
goto _DownloadURL_EXIT;
}

// 打开指定文件,也就是和TextDll同路径的index.html
if( err = _tfopen_s(&pFile, szFile, L"wt") )
{
OutputDebugString(L"fopen() failed!");
goto _DownloadURL_EXIT;
}

// hURL, 从InternelOpenUrl返回的句柄
// pBuf, 指向接收数据的缓冲区的指针
// DEF_BUF_SIZE, 要接受的字节数
// dwBytesRead, 指向接受读取字节数的变量的指针,说人话就是存放的读取的长度
while( InternetReadFile(hURL, pBuf, DEF_BUF_SIZE, &dwBytesRead) )
{
if( !dwBytesRead )
break;

// 把pBuf所指向的数组中的数据写入到给定流pFile中( 长度是dwBytesRead 1次放入
fwrite(pBuf, dwBytesRead, 1, pFile);
}

bRet = TRUE;

// 关闭各个句柄
_DownloadURL_EXIT:
if( pFile )
fclose(pFile);

if( hURL )
InternetCloseHandle(hURL);

if( hInternet )
InternetCloseHandle(hInternet);

return bRet;
}

DropFile()

DropFile() 函数将下载的index.html拖放到TextView_Patch.exe进程并显示其内容

  1. 使用PID获取TextView的主窗口句柄

  2. 调用postMessage(WM_DROPFILES)API将消息放入消息队列

获取窗口句柄

单独领出来讲一下获取进程PID方法,和上章的DLL卸载不一样,但应该实现的是一样的都是获取句柄

  1. GetCurrentProcessId获取了调用当前进程的进程标识符,然后传入了自写函数GetWindowHandleFromPID
  2. 里面调用了EnumWindows这个函数会枚举每个窗口句柄传入回调函数,也就是第一个参数EnumWindowsProc,第二个参数是成为回调函数的参数
  3. 在回调函数EnumWindowsProc里,会调用GetWindowThreadProcessId通过窗口句柄来创建当前窗口的进程标识符
  4. 再与我们传入的TextView的进程标识符进行比较,是的话就成功获取TextView的进程句柄
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
//  EnuwWindows的回调函数
// hWnd为各个顶级窗口句柄
// lParam为传来的参数,也就是当前的进程标识符
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
DWORD dwPID = 0;

// 检索创建指定窗口的线程标识符,以及创建窗口的进程标识符
GetWindowThreadProcessId(hWnd, &dwPID);

// 于是获取的进程标识符与我们的TextView比较
if( dwPID == (DWORD)lParam )
{
// 是的话就赋值窗口句柄
g_hWnd = hWnd;
return FALSE;
}

return TRUE;
}

HWND GetWindowHandleFromPID(DWORD dwPID)
{
// 函数作用:
// 通过将窗口的句柄一次传递给应用程序的回调函数(也就是EnumWindowsProc),来枚举屏幕上的所有顶级窗口
// EnumWindows一直持续枚举最后一个顶级窗口或回调函数返回FALSE
// 参数:
// EnumWindowsProc, 为回调函数的指针
// dwPID, 传递给回调函数的应用程序定义的值
EnumWindows(EnumWindowsProc, dwPID);

return g_hWnd;
}

// GetCurrentProcessId() 检索调用进程的进程标识符
// GetWindowHandleFromPID是通过进程标识符来获取进程句柄
if( !(hWnd = GetWindowHandleFromPID(GetCurrentProcessId())) )
{
OutputDebugString(L"GetWndHandleFromPID() failed!!!");
return FALSE;
}

现在再来看下整个DropFile()

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
//  EnuwWindows的回调函数
// hWnd为各个顶级窗口句柄
// lParam为传来的参数,也就是当前的进程标识符
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
DWORD dwPID = 0;

// 检索创建指定窗口的线程标识符,以及创建窗口的进程标识符
GetWindowThreadProcessId(hWnd, &dwPID);

// 于是获取的进程标识符与我们的TextView比较
if( dwPID == (DWORD)lParam )
{
// 是的话就赋值窗口句柄
g_hWnd = hWnd;
return FALSE;
}

return TRUE;
}

HWND GetWindowHandleFromPID(DWORD dwPID)
{
// 函数作用:
// 通过将窗口的句柄一次传递给应用程序的回调函数(也就是EnumWindowsProc),来枚举屏幕上的所有顶级窗口
// EnumWindows一直持续枚举最后一个顶级窗口或回调函数返回FALSE
// 参数:
// EnumWindowsProc, 为回调函数的指针
// dwPID, 传递给回调函数的应用程序定义的值
EnumWindows(EnumWindowsProc, dwPID);

return g_hWnd;
}

BOOL DropFile(LPCTSTR wcsFile)
{
HWND hWnd = NULL;
DWORD dwBufSize = 0;
BYTE *pBuf = NULL;
DROPFILES *pDrop = NULL;
char szFile[MAX_PATH] = {0,};
HANDLE hMem = 0;

// 将UTF-16(宽字符)字符串映射到新字符串,也就是wcsFile映射到szFile
// 这个函数好像有漏洞
// 好像是这个函数把文件的内容都放入了szFile(也只能是这个了
WideCharToMultiByte(CP_ACP, 0, wcsFile, -1,
szFile, MAX_PATH, NULL, NULL);

// 这边应该是重新计算长度
dwBufSize = sizeof(DROPFILES) + strlen(szFile) + 1;

// 从堆中分配指定数量的字节,注意是全局
if( !(hMem = GlobalAlloc(GMEM_ZEROINIT, dwBufSize)) )
{
OutputDebugString(L"GlobalAlloc() failed!!!");
return FALSE;
}

// 锁定一个全局内存对象 并返回一个指向对象内存块第一个字节的指针
pBuf = (LPBYTE)GlobalLock(hMem);

pDrop = (DROPFILES*)pBuf;
pDrop->pFiles = sizeof(DROPFILES);
// szFile又放入pBuf这个全局对象内存块
strcpy_s((char*)(pBuf + sizeof(DROPFILES)), strlen(szFile) + 1, szFile);

// 减少与使用GMEM_MOVEABLE分配的内存对象关联的锁计数(雾,这锁定与减少是什么意思
GlobalUnlock(hMem);

// GetCurrentProcessId() 检索调用进程的进程标识符
// GetWindowHandleFromPID是通过进程标识符来获取进程句柄
if( !(hWnd = GetWindowHandleFromPID(GetCurrentProcessId())) )
{
OutputDebugString(L"GetWndHandleFromPID() failed!!!");
return FALSE;
}

// 在与创建指定窗口的线程(hWnd) 关联的消息队列放置一条消息(pBuf)
PostMessage(hWnd, WM_DROPFILES, (WPARAM)pBuf, NULL);

return TRUE;
}

dummy()

dummy函数是myhack3.dll的文件的向外提供服务的导出函数,无用,为了保持形式上的完整性

在PE文件中导入某个DLL,实质就是在文件代码内调用该DLL提供的导出函数

PE文件头中记录着DLL名称、函数名称等信息,因此,MyHack3.dll至少要向外提供1个以上导出函数才能保持形式上的完整性(DLL特性

1
2
3
4
5
6
7
8
9
10
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void dummy()
{
return;
}
#ifdef __cplusplus
}
#endif

现在再来审计整个MyHack源码就没什么问题了,待会动调看看数据细节

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#include "stdio.h"
#include "windows.h"
#include "shlobj.h"
#include "Wininet.h"
#include "tchar.h"

#pragma comment(lib, "Wininet.lib")

#define DEF_BUF_SIZE (4096)
#define DEF_URL L"http://www.google.com/index.html"
#define DEF_INDEX_FILE L"index.html"

HWND g_hWnd = NULL;

#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void dummy()
{
return;
}
#ifdef __cplusplus
}
#endif

BOOL DownloadURL(LPCTSTR szURL, LPCTSTR szFile)
{
BOOL bRet = FALSE;
HINTERNET hInternet = NULL, hURL = NULL;
BYTE pBuf[DEF_BUF_SIZE] = {0,};
DWORD dwBytesRead = 0;
FILE *pFile = NULL;
errno_t err = 0;

// 初始化应用程序对WinlNet函数的使用
// 第一个参数 此名称用作HTTP协议中的用户代码(雾,那我用自己的应该也可以
// 第二个参数 从注册表检索代理或直接配置
hInternet = InternetOpen(L"ReverseCore",
INTERNET_OPEN_TYPE_PRECONFIG,
NULL,
NULL,
0);
if( NULL == hInternet )
{
OutputDebugString(L"InternetOpen() failed!");
return FALSE;
}

// 打开由完整的FTP或HTTP URL指定的资源
// hInternet, 该句柄必须有InternelOpeN()API的调用返回
// szURL, 该变量为指定的URL。仅支持ftp: http: https:
// INTERNET_FLAG_RELOAD, 强制从源服务器下载请求的文件
hURL = InternetOpenUrl(hInternet,
szURL,
NULL,
0,
INTERNET_FLAG_RELOAD,
0);
if( NULL == hURL )
{
OutputDebugString(L"InternetOpenUrl() failed!");
goto _DownloadURL_EXIT;
}

// 打开指定文件,也就是和TextDll同路径的index.html
if( err = _tfopen_s(&pFile, szFile, L"wt") )
{
OutputDebugString(L"fopen() failed!");
goto _DownloadURL_EXIT;
}

// hURL, 从InternelOpenUrl返回的句柄
// pBuf, 指向接收数据的缓冲区的指针
// DEF_BUF_SIZE, 要接受的字节数
// dwBytesRead, 指向接受读取字节数的变量的指针,说人话就是存放的读取的长度
while( InternetReadFile(hURL, pBuf, DEF_BUF_SIZE, &dwBytesRead) )
{
if( !dwBytesRead )
break;

// 把pBuf所指向的数组中的数据写入到给定流pFile中(长度是dwBytesRead 1次放入
fwrite(pBuf, dwBytesRead, 1, pFile);
}

bRet = TRUE;

// 关闭各个句柄
_DownloadURL_EXIT:
if( pFile )
fclose(pFile);

if( hURL )
InternetCloseHandle(hURL);

if( hInternet )
InternetCloseHandle(hInternet);

return bRet;
}

// EnuwWindows的回调函数
// hWnd为各个顶级窗口句柄
// lParam为传来的参数,也就是当前的进程标识符
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
DWORD dwPID = 0;

// 检索创建指定窗口的线程标识符,以及创建窗口的进程标识符
GetWindowThreadProcessId(hWnd, &dwPID);

// 于是获取的进程标识符与我们的TextView比较
if( dwPID == (DWORD)lParam )
{
// 是的话就赋值窗口句柄
g_hWnd = hWnd;
return FALSE;
}

return TRUE;
}

HWND GetWindowHandleFromPID(DWORD dwPID)
{
// 函数作用:
// 通过将窗口的句柄一次传递给应用程序的回调函数(也就是EnumWindowsProc),来枚举屏幕上的所有顶级窗口
// EnumWindows一直持续枚举最后一个顶级窗口或回调函数返回FALSE
// 参数:
// EnumWindowsProc, 为回调函数的指针
// dwPID, 传递给回调函数的应用程序定义的值
EnumWindows(EnumWindowsProc, dwPID);

return g_hWnd;
}

BOOL DropFile(LPCTSTR wcsFile)
{
HWND hWnd = NULL;
DWORD dwBufSize = 0;
BYTE *pBuf = NULL;
DROPFILES *pDrop = NULL;
char szFile[MAX_PATH] = {0,};
HANDLE hMem = 0;

// 将UTF-16(宽字符)字符串映射到新字符串,也就是wcsFile映射到szFile
// 这个函数好像有漏洞
// 好像是这个函数把文件的内容都放入了szFile(也只能是这个了
WideCharToMultiByte(CP_ACP, 0, wcsFile, -1,
szFile, MAX_PATH, NULL, NULL);

// 这边应该是重新计算长度
dwBufSize = sizeof(DROPFILES) + strlen(szFile) + 1;

// 从堆中分配指定数量的字节,注意是全局
if( !(hMem = GlobalAlloc(GMEM_ZEROINIT, dwBufSize)) )
{
OutputDebugString(L"GlobalAlloc() failed!!!");
return FALSE;
}

// 锁定一个全局内存对象 并返回一个指向对象内存块第一个字节的指针
pBuf = (LPBYTE)GlobalLock(hMem);

pDrop = (DROPFILES*)pBuf;
pDrop->pFiles = sizeof(DROPFILES);
// szFile又放入pBuf这个全局对象内存块
strcpy_s((char*)(pBuf + sizeof(DROPFILES)), strlen(szFile) + 1, szFile);

// 减少与使用GMEM_MOVEABLE分配的内存对象关联的锁计数(雾,这锁定与减少是什么意思
GlobalUnlock(hMem);

// GetCurrentProcessId() 检索调用进程的进程标识符
// GetWindowHandleFromPID是通过进程标识符来获取进程句柄
if( !(hWnd = GetWindowHandleFromPID(GetCurrentProcessId())) )
{
OutputDebugString(L"GetWndHandleFromPID() failed!!!");
return FALSE;
}

// 在与创建指定窗口的线程(hWnd) 关联的消息队列放置一条消息(pBuf)
PostMessage(hWnd, WM_DROPFILES, (WPARAM)pBuf, NULL);

return TRUE;
}

DWORD WINAPI ThreadProc(LPVOID lParam)
{
TCHAR szPath[MAX_PATH] = {0,};
TCHAR *p = NULL;

OutputDebugString(L"ThreadProc() start...");

// 第一个参数的NULL,该API就检索当前进程的可执行文件的路径,也就是TextView的路径
// 之前用的好像都是dll的句柄 也就是找dll的路径
GetModuleFileName(NULL, szPath, sizeof(szPath));

if( p = _tcsrchr(szPath, L'\\') )
{
// 常规操作 把\\后的TextView.exe换成index.html
_tcscpy_s(p+1, wcslen(DEF_INDEX_FILE)+1, DEF_INDEX_FILE);

OutputDebugString(L"DownloadURL()");
// 下载文件,作者自写的
if( DownloadURL(DEF_URL, szPath) )
{
// 拖放到TextView
OutputDebugString(L"DropFlie()");
DropFile(szPath);
}
}

OutputDebugString(L"ThreadProc() end...");

return 0;
}

// DllMain函数的功能很简单
// 1. 创建线程(CreateThread)
// 2. 运行指定的线程过程(ThreadProc)
// 3. 在线程过程(ThreadProc)中调用DownloadURL()和DropFile()函数,下载指定网页并将其拖放到文本查看器
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
CloseHandle(CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL));
break;
}

return TRUE;
}

TextView.exe

现在聊下如何修改PE文件自动加载指定dll

1. 查看IDT是否又足够空间

可以发现五个IID结构体之后,最后一个是NULL结构体,后面也没有我们放入MyHack的空间

三个方法添加IID

  1. 查找文件中的空白区域
  2. 增加文件最后一个节区的大小
  3. 在文件末尾添加新节区

image-20220413192910954

这边直接在rdata末尾添加了,可以发现映射到内存的是2C56,而文件大小有2E00,于是就把整个IID移到rdata结尾

image-20220413193254649

2. 移动且增加导入表

准备把IID表移到8C80处,也就是文件的7E80处,所以得先把指向IID的地址修改成新地址

image-20220413193703031

并且删除绑定导入表,BOUND IMPORT TABLE,这是一种提高DLL加载速度的技术,这是个可选项,如果不删需要向该表添加信息,删除只需要全部清零即可

image-20220413194044524

上述搞完,就可以添加MyHack的导入表了

image-20220413194240266

再贴下IID的结构体

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; // Hint成员代表函数的Ordinal
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

用下书上的解析

特别说下IAT,IAT也是RVA数组,各元素既可以拥有与INT相同的值也可以拥有不同的值若INT数据准确

反正运行时,PE装载器会将虚拟内存中的IAT替换为该实际函数的地址

image-20220413194412004

3. 大功告成?

还差最后一步修改IAT节区属性,PE装载器在修改IAT,写入函数的实际地址,所以相关节区一定要有WRITE

所以要向Characteristics or 8000 0000,就可以拥有写权限

image-20220413195156552

现在就可以完美做出了和作者一样的TextView_Patch的版本了

image-20220413195340483

一个疑问

但仔细想想是不是哪里不对,如果rdata原先没有写权限,可原来的IAT也在rdata节区,但程序能正常运行

而我们修改后也在rdata节区,却不行了

这是因为!PE头的IMAGE_OPTIONAL_HEADER结构体Data Directoy数组中存在IAT\

image-20220413195952772

所以不想改写权限的就可以直接添加在这后面,并把IAT(SIZE)增加8个字节

把IAT地址改到这后面,再把原来的IAT数据删了

image-20220413202845807

再到这边改成IAT函数的地址,注意后面跟个4字节的导出表截断

image-20220413202945907

再修改一下IAT的大小,增加到15C,不过作者那份没改,可是并没有什么事情

image-20220413203155949

大功告成!

image-20220413203215181

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

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