逆向工程核心原理-25

逆向工程核心原理-第25章-通过修改PE加载DLL

前面的三种DLL都是动态注入(从外部注入), 除此之外, 还可以采用”手工修改可执行程序”(从内部注入). 这种直接修改文件的方法, 只用修改一次, 后面每当程序运行时都会自动加载指定的DLL文件.

25.1 练习文件

我们将修改TextView.exe文件, 使其再运行时加载myhack3.dll文件(效果是下载一个html文件)

25.1.1 TextView.exe

TextView.exe是一个非常简单的文本查看程序, 只用将文件拖入即可查看文件内容

将a.txt中的内容呈现出来了

Untitled

然后我们用PEView查看其导入表

Untitled

可以看到导入了四个DLL: KERNEL32.dll, USER32.dll, GDI32.dll, SHELL32.dll

25.1.2 TextView_patched.exe

TextView_patched.exe是修改了TextView.exe文件的IDT(导入表)后得到的文件, 即再IDT中添加了导入myhack3.dll的部分, 使其运行时会自动导入myhack3.dll文件

使用PEView查看TextView_patched.exe的IDT(导入表)

Untitled

可以看到新增了一个myhack3.dll文件, 这样, 运行TextView_patched.exe时就会自动加载myhack3.dll文件

下面运行一下TextView_patched.exe试试看是否会产生myhack3.dll的效果(下载html文件)(再XP上跑不了, 反而再Win10上可以)

Untitled

Untitled

25.2 源代码 - myhack3.cpp

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
#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"

DWORD WINAPI ThreadProc(LPVOID lParam)
{
TCHAR szPath[MAX_PATH] = {0,};//路径字符串
TCHAR *p = NULL;

GetModuleFileName(NULL, szPath, sizeof(szPath));//获取完整路径

if (p = _tcsrchr(szPath, L'\\') )
{
_tcscpy_s(p+1, wcslen(DEF_INDEX_FILE) + 1, DEF_INDEX_FILE);//替换进程名来作为下载html文件的名称

if ( DownloadURL(DEF_URL, szPath))//下载URL到指定文件
{
DropFile(szPath);
}
}

return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch( fdwReason )
{
case DLL_PROCESS_ATTACH://首次加载DLL时执行
CloseHandle(CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL));//创建线程来运行ThreadProc
break;
}

return TRUE;
}

跟之前章节的分析有很多共通之处, 所以省去了很多分析, 就是创建线程来下载URL

我们接着分析ThreadProc中的DownloadURL()和DropFile()函数

25.2.2 DownloadURL()

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
#define _CRT_SECURE_NO_WARNINGS
BOOL DownloadURL(LPCTSTR szURL, LPCTSTR szFile)
/*第一个参数szURL为指向"http://www.google.com/index.html"字符串的指针*/
//第二个参数是文件的下载路径(包含文件名)
{
BOOL bRet = FALSE; //类型为BOOL
HINTERNET hInternet = NULL, hURL = NULL;
BYTE pBuf[DEF_BUF_SIZE] = { 0, };
DWORD dwBytesRead = 0;
FILE* pFile = NULL; //文件指针
errno_t err = 0;

hInternet = InternetOpen(L"ReverseCore",//指向的字符串作为用户代理的HTTP协议
INTERNET_OPEN_TYPE_PRECONFIG,//指定访问类型:直连,代理等
NULL,//指向一个字符串表示代理服务器名称,INTERNET_OPEN_TYPE_PRECONFIG直连直接设NULL
NULL,//指向字符串表示主机名或IP地址, INTERNET_OPEN_TYPE_PRECONFIG直接设为NULL
0);//看的不是很懂, 但是不重要
//返回值:返回一个有效HINTERNET(网络句柄), 用来传递给WinINet
/*该函数的作用是:初始化一个应用程序,以使用 WinINet 函数
为了使网络连接生效,必须用函数InternetOpen函数创建一个HINTERNET根句柄*/

if (NULL == hInternet)//因为hInternet初始化为NULL, 如果不变说明上面的网络设置错误
{
OutputDebugString(L"InternetOpen() failed!");//打印错误信息
return FALSE;//返回FALSE
}

hURL = InternetOpenUrl( hInternet,
szURL,
NULL,
0,
INTERNET_FLAG_RELOAD,
0);

/*
* 作用是通过一个完整的FTP,Gopher或HTTP网址打开一个资源
* HINTERNET InternetOpenUrl(
HINTERNET hInternet, Internet会话句柄, 必须由前面调用的InternetOpen返回
LPCTSTR lpszUrl, 一个字符串指针, 指定读取的网址, 示例中就是传入的网址字符串
LPCTSTR lpszHeaders, 一个字符串指针, 指定发送到HTTP服务器的头信息
DWORD dwHeadersLength, 头长度
DWORD dwFlags, 操作类型
DWORD_PTR dwContext) 一个指向一个应用程序定义的值, 不重要

返回值: 如果成功连接, 则返回一个有效句柄, 失败则返回NULL
*/
if (NULL == hURL)
{
OutputDebugString(L"InternetOpenUrl() failed!");
goto _DownloadURL_EXIT;
}

//检查是否成功建立连接

if (err = _tfopen_s(&pFile, szFile, L"wt"))//打开一个文件, 访问类型是w(可写)t(以文本模式打开) {
OutputDebugString(L"fopen() failed!");//打印错误信息
goto _DownloadURL_EXIT;//跳到EXIT, 关闭已打开的句柄
}
/*
* fopen_s基本功能是以指定访问权限打开一个文件
fopen_s函数原型:
errno_t fopen_s(
FILE** pFile, 指向文件指针的指针,它将接收指向打开文件的指针
const char *filename, 要打开的文件名
const char *mode 允许的访问类型
);
成功则返回0, 失败则返回错误代码
*/

//输出句柄 接收指针
while (InternetReadFile(hURL, pBuf, DEF_BUF_SIZE, &dwBytesRead))
{
if (!dwBytesRead)
break;

fwrite(pBuf, dwBytesRead, 1, pFile);
}
/*
InternetReadFile基本功能是:从一个指定类型的句柄中读取数据
函数原型:
BOOL InternetReadFile(
[in] HINTERNET hFile, 先前InternetOpenUrl返回的句柄
[out] LPVOID lpBuffer, 指向接收数据的缓冲区
[in] DWORD dwNumberOfBytesToRead, 要读取的字节数
[out] LPDWORD lpdwNumberOfBytesRead 指向实际读取字节数的变量的指针
);

*/

bRet = TRUE;
//bRet初始化为FALSE, 当上述函数调用均成功时, 改为TRUE表示整个过程没有出现错误

_DownloadURL_EXIT:
if (pFile)
fclose(pFile);

if (hURL)
InternetCloseHandle(hURL);

if (hInternet)
InternetCloseHandle(hInternet);

return bRet;
}

/*
* 整个流程: InternetOpen获取网络句柄 -> InternetOpenUrl建立连接并返回URL句柄->
InternetReadFile将URL句柄的数据存入指定缓冲区(下载) -> fwrite把缓冲区的数据写入文件
*/

25.2.3 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
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
#define _CRT_SECURE_NO_WARNINGS
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
DWORD dwPID = 0;

GetWindowThreadProcessId(hWnd, &dwPID);

if (dwPID == (DWORD)lParam)
{
g_hWnd = hWnd;
return FALSE;
}

return TRUE;
}

HWND GetWindowHandleFromPID(DWORD dwPID)
{
EnumWindows(EnumWindowsProc, dwPID);

return g_hWnd;
}

BOLL DropFile(LPCTSTR wcsFile)//是从这里开始执行的
//传入参数为已经写入数据的html文件
{
HWND hWnd = NULL;
//一个窗口句柄
DWORD dwBufSize = 0;
//描述块大小变量
BYTE* pBuf = NULL;
//块指针
DROPFILES* pDrop = NULL;
/*一个结构体类型
typedef struct _DROPFILES {
DWORD pFiles; 文件列表从此结构开头的偏移, 以字节为单位
POINT pt; 落点, 坐标取决于fNC(下一个成员)
BOOL fNC; 如果此成员为TRUE 则pt指定窗口非客户去中某个点的屏幕坐标, 如果为FALSE, pt指定客户区域中一个点的客户坐标
客户区跟非客户区是指窗口中不同的位置
BOOL fWide; 指示文件是否包含ANSI或Unicode字符的值.如果该值为零, 则文件包含ANSI字符, 否则它博阿寒Unicode字符
} DROPFILES, *LPDROPFILES;
*/

char szFile[MAX_PATH] = { 0, };
//定义一个字符数组
HANDLE hMem = 0;
//定义一个内存句柄
WideCharToMultiByte(CP_ACP, 0, wcsFile, -1,
szFile, MAX_PATH, NULL, NULL);
/*
* 函数作用: 映射一个unicode字符串到一个多字节字符串, 执行转换的代码页, 接收转换字符串, 允许额外的控制等操作
* 函数原型:
* int WideCharToMultiByte(

UINT CodePage, //指定执行转换的[代码页], 可以理解为要转化成的字符串的编码方式

DWORD dwFlags, //允许你进行额外的控制,它会影响使用了读音符号(比如重音)的字符

LPCWSTR lpWideCharStr, //指定要转换为宽字节字符串的[缓冲区]

int cchWideChar, //指定由参数lpWideCharStr指向的缓冲区的[字符]个数

LPSTR lpMultiByteStr, //指向接收被转换字符串的缓冲区

int cchMultiByte, //指定由参数lpMultiByteStr指向的[缓冲区]最大值

LPCSTR lpDefaultChar, //遇到一个不能转换的宽字符,函数便会使用pDefaultChar参数指向的字符

LPBOOL pfUsedDefaultChar //至少有一个字符不能转换为其多字节形式,函数就会把这个变量设为TRUE

);
*
* 在本示例中第三个参数为html文件, 即把html文件的unicode字符串转换为CP_ACP编码方式, 并
* 存储到第五个参数szFile(字符数组)中
*/

dwBufSize = sizeof(DROPFILES) + strlen(szFile) + 1;
// 结构体所占内存空间 + html文件字符串所占内存空间(因为用的是strlen,所以要加一)

if (!(hMem = GlobalAlloc(GMEM_ZEROINIT, dwBufSize)))
/*
*函数作用: 从堆中分配一定数目的字节数
参数一表示分配属性, GMEM_ZEROINIT表示分配的内存初始化为0
参数二表示分配内存大小, 用到上一句所需内存的总和: 存放一个DROPFILE结构体, 和html转换后的字符串
*/
{
OutputDebugString(L"GlobalAlloc() failed!!!");
return FALSE;
}

pBuf = (LPBYTE)GlobalLock(hMem);
//GlobalLock作用是锁定指定的内存,hMem为内存句柄
//返回值为内存的首地址
//内存锁定: 一般给应用程序分配的内存都是可移动的或可丢弃的, 锁定内存后地址才固定,方便存取这块内存
pDrop = (DROPFILES*)pBuf;
//定义DROPFILES类型指针
pDrop->pFiles = sizeof(DROPFILES);
/*
* typedef struct _DROPFILES {
DWORD pFiles; 文件列表从此结构开头的偏移, 以字节为单位
POINT pt; 落点, 坐标取决于fNC(下一个成员)
BOOL fNC; 如果此成员为TRUE 则pt指定窗口非客户去中某个点的屏幕坐标, 如果为FALSE, pt指定客户区域中一个点的客户坐标
客户区跟非客户区是指窗口中不同的位置
BOOL fWide; 指示文件是否包含ANSI或Unicode字符的值.如果该值为零, 则文件包含ANSI字符, 否则它博阿寒Unicode字符
} DROPFILES, *LPDROPFILES;
*/
strcpy_s((char*)(pBuf + sizeof(DROPFILES)), strlen(szFile) + 1, szFile);
/*
* 在结构体后面把转换后的字符串存入内存
*/
GlobalUnlock(hMem);
//解除内存锁定

if (!(hWnd) = GetWindowHandleFromPID(GetCurrentProcessId()))
//获取当前进程PID(TextView.exe), 随后获取当前进程的窗口句柄
{
OutputDebugString(L"GetWndHandleFromPID() failed!!!");
return FALSE;
}

PostMessage(hWnd, WM_DROPFILES, (WPARAM)pBuf, NULL);
/*
* 函数作用: 将一条消息放入消息队列中, 消息队列里的消息锡通过调用GetMessage和PeekMessage获取
函数原型:
BOOL WINAPI PostMessage(HWND hWnd, 当前进程(TextView.exe)窗口指针
UINT Msg, 指定被寄送的消息类型, 示例中的WM_DROPFILES是拖动消息类型
WPARAM wParam, 指定附加的消息的特定的信息
LPARAM lParam); 指定附加的消息的特定的消息
*/

return TRUE;
}

//流程: 先将html的unicode字符串转化后放入内存中 -> 定义DROPFILES结构体用于拖动消息的描述也存放在内存中
// -> 获取窗口句柄 -> 使用PostMessage实现拖动消息(内存(文件内容))到该进程的窗口(利用窗口句柄)中

25.2.4 dummy()

1
2
3
4
5
6
7
8
9
10
11
#ifdef __cplusplus
extern "C" {
#endif
// 出现再IDT中的dump export function...
__declspec(dllexport) void dummy()
{
return;
}
#ifdef __cplusplus
}
#endif

dummy()函数是myhack3.dll文件向外部提供服务的导出函数, 但是我们可以看到它并没有实现任何功能. 导出他只是为了保证形式上的完整性, 通俗点讲就是”走走样子”.

在PE文件中导入DLL, 实质就是在文件代码内调用该DLL提供的导出函数. PE文件头中记录这DLL名称, 函数名称等信息. 因此, myhack3.dll至少要向外提供1个以上的导出函数才能保持形式上的完整性.

25.3 修改 TextView.exe 文件的准备工作

25.3.1 修改思路

PE文件中导入的DLL信息以结构体列表形式存储在IDT(导入表), 只要讲myhack3.all添加到列表尾部就可以了, 此前还要确认IDT中有无足够空间.

25.3.2 查看IDT是否由足够空间

首先使用PEView查看TextView.exe的IDT地址(PE文件头的可选头中存有导入表RVA值(注意是虚拟地址值))

我们使用PEView查看

Untitled

可以看到IDT的RVA地址为0x84CC, 我们找到IDT的位置, 位于.rdata节中

Untitled

根据第十三章的知识: IDT是由IMAGE_IMPORT_DESCRIPTOR(以下称IID)结构体组成的数组, 且数组末尾以NULL结构体结束. 由于每个导入的DLL文件都对应一个IID结构体(每个IID结构体的大小为14个字节) 所以整个IID区域为RVA: 84CC ~ 852F, 整体大小 = 0x14 * 5 = 0x64(十进制为20个字节)

IID结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
IMAGE_IMPORT_DESCRIPTOR
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristis;
DWORD OriginalFirstThunk; //INT(导入名称表)的RVA地址
};
DWORD TimeDateStamp; //一个时间
DWORD ForwarderChain; //第一个转发链引用的索引, 这是负责DLL转发的成员
//DLL转发: 是指DLL将其导出的一些函数转发给另一个DLL
DWORD Name; //指向DLL名称字符串的指针
DWORD FirstThunk; //IAT(导入地址表)的RVA地址
} IMAGE_IMPORT_DESCRIPTOR;

我们讲PEView切换为FileOffse, 来查看IDT的文件偏移

Untitled

使用HxD(HexEditor)打开TextView.exe

Untitled

IDT的文件偏移为76CC ~ 772F, 大小为64字节, 共有5个IID结构体(最后一个为NULL结构体)

可以看到IDT后面紧跟着其他的数据, 所以我们没有足够的空间来添加IID

25.3.3 移动 IDT

在这种情况下, 我们要先把整个IDT移动其他的更广阔的位置, 然后再添加新的IID. 移动的选择有以下的三种:

  • 查找文件中的空白区域;
  • 增加文件最后一个节区的大小;
  • 再文件末尾添加新的节区

第一个方式是最简单的, 所以先尝试这个

我们应该遵循就近的原则, 如果原节区有足够的空白空间, 那就转移到原节区.

我们可以再PEV中看到, 在.rdata节区的尾部有大片空白空间

Untitled

0x8C60 ~ 0x8DFF这片空间的大小为0x190, 看上去是足够容纳IDT的, 但是这些内存有可能不是可用区域, 并不是文件中的所有区域都会被无条件加载到进程的虚拟内存, 只有节区头中明确记录的区域才会被加载, 使用PEV工具查看TextView.exe文件的.rdata节区头, 见下图

Untitled

我们整理这些描述信息

  • Pointer to Raw Data = 5200, [文件]节区的起始位置
  • Size of Raw Data = 2E00, [文件]节区的大小
  • RVA = 6000, [内存]节区的起始位置
  • Virtual Size = 2C56, [内存]节区大小

可以看到.rdata节区在磁盘文件于在内存中的大小是不相同的

那么

1
2
3
4
实际空白大小 = (节区磁盘大小 - 节区内存大小) = 2E00 - 2C56 = 1AA
注意这里有一个理解误区就是: 节区内存大小所指的是**已经**被使用的大小, 也就是说这段内存不管是不是
空白的, 都不能被使用.
其次, 映射是磁盘节区整个区域都被映射, 而虚拟节区大小则是被使用的区域大小

1AA的空白大小可以容纳IDT, 所以我们只用这片区域

我们在RVA:8C80处创建IDT(前面的那一小块区域已经被使用了)

25.4 修改 TextView.exe

先把TextView.exe复制到工作文件夹, 然后重命名为TextView_Patch.exe, 然后使用HxD打开该文件进行修改.

25.4.1 修改导入表的RVA值

IMAGE_OPTIONAL_HEADER(可选头)中的导入表成员用来之处IDT的位置(RVA)与大小

我们使用PEV工具查看

Untitled

接下来记住其文件偏移为160(上图为RVA切换一下就好了), 使用HxD, 对其进行修改

Untitled

Untitled

从现在开始, 导入表位于RVA :8C80处, 大小为0x78

25.4.2 删除绑定导入表

位于可选头中的BOUND IMPORT TABLE (绑定导入表)是一种提高DLL加载速度的技术

Untitled

若想正常导入myhack3.dll, 需要向导入表添加信息. 然而, 该绑定导入表是可选项, 删除也没有关系, 删除即RVA跟Size都设置为0

25.4.3 创建新IDT

先使用HxD复制原IDT(RAW: 76CC ~ 772F), 然后覆写到IDT的新位置(RAW: 7E80)

Untitled

然后再新IDT尾部(RAW: 7ED0)添加与myhack3.dll对应的IID

Untitled

我们根据上面对IID结构体的分析, 发现其成员每个都占四个字节, 所以这里分别设置的是第1, 4, 5个成员OriginalFirstThunk(INT), Name 和 FirstThunk(IAT).

25.4.4 设置Name, INT, IAT

前面添加的IID结构体成员拥有指向其他数据结构(INT, Name, IAT)的RVA值. 因此, 必须设置这些数据结构才能保证TextView_Patch.exe文件正常运行. 为了方便HxD修改, 我们先把RVA转换成RAW

成员 RVA RAW
INT 8D00 7F00
Name 8D10 7F10
IAT 8D20 7F20

在指定的地址上创建好相应的数据

Untitled

使用PEV工具查看虚拟地址

Untitled

逐个分析

首先是第一个成员OriginalFirstThunk是指向的INT的指针, 上图指向的是8d00也就是

Untitled

指向的是8D30

Untitled

可以看到前两个字节为00 00, 这两个字节为导入函数的Ordinal, 后面紧跟这函数的名称字符串.

其次是第四个成员Name, 指向的是8D10

Untitled

就是我们的DLL名称字符串

最后是第五个成员FirstThunk是指向IAT的指针, 其值为8D20

Untitled

指向的也是8D30

Untitled

所以INT跟IAT在这里相同

25.4.5 修改IAT节区的属性值

加载PE文件到内存时, PE装载器会修改IAT, 写入函数的实际地址, 所以相关节区一定要拥有WRITE(可写)的属性.

使用PEView查看.rdata节区头

原本书中为40000040, 没有可写属性的, 但是这里HxD好像直接帮我修改了

Untitled

我们添加一个WRITE属性这里用的时FileOffset, 所以直接再HxD中找224

Untitled

Untitled

因为原本IAT所在的节区时有可写属性, 所以不用修改, 但是我们将myhack3.dll的IAT放在了.rdata节区中, 与其他的IAT在不同的节区中, 所以要修改节区属性, 使其可写.

或者我们也可以直接修改FirstThunk到原IAT的末尾(如果还有空白区域的话), 并在末尾写上myhack3.dll的导入地址, 这样就可以不用修改节区头可写属性了. 我们在后面再试试, 这里先继续

25.5 检测验证

首先使用PEV查看修改后的TextView_Patch.exe文件, 查看其IDT

Untitled

可以看到以成功添加myhack3.dll

运行测试

Untitled

可以发现是成功运行的

更改IAT的另一个方法

首先前面转移IDT是一样的, 我们需要修改的是IID的第五个成员FirstThunk指向的是原IAT的尾巴, 并在尾巴处添加一个dummy的IAT

首先我们看看IAT

Untitled

IAT也在.rdata中, 我们只用看尾巴的那一块, 现在是文件偏移, 等会要修改的地方

然后是RVA, 因为IID成员用的是RVA

Untitled

然后我们新增的IID中的第一个跟第四个成员都不用改, 跟原来的一样, 第五个成员是指向IAT的指针, 我们修改成尾巴的RVA6150, 然后再6150的RAW处修改其值(原本为0), 改成8D00指向IMAGE_IMPORT_BY_NAME结构体指针的指针

Untitled

Untitled

检验

Untitled

可以看到IAT中有了myhack3.dll项目

同时查看IDT

Untitled

里面也有myhack3.dll

运行检验一下

Untitled

运行成功.

文章作者: LamのCrow
文章链接: http://example.com/2022/04/08/逆向工程核心原理-第 59812/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow