第25章 通过修改PE加载DLL

本文最后更新于:2022年5月19日 晚上

第25章 通过修改PE加载DLL
除了前面讲过的DLL动态注入技术外,还可以采用“手工修改可执行文件”的方式加载用户指定的DLL文件,本章将向各位介绍这种方法。学习这种技术前,首先要掌握有关PE文件格式的知识。

前面我们学过向“运行中的进程”强制注入指定DLL文件的方法。下面我们将换用另外一种方法,通过“直接修改目标程序的可执行文件”,使其运行时强制加载指定的DLL文件。这种方法只要应用过一次后(不需要另外的注入操作),每当进程开始运行时就会自动加载指定的DLL文件。其实,这是一种破解的方法。

25.1练习文件

本节将做个简单练习以帮助大家更好地理解要学习的内容。我们的目标是,直接修改
TextView.exe文件,使其在运行时自动加载myhack3.dll文件(这需要各位事先掌握修改PE文件头
的相关知识与技术)。

25.1.1 TextView.exe

TextView.exe是一个非常简单的文本查看程序,只要用鼠标将要查看的文本文件(myhack3.cpp )
拖动(Drop)到其中,即可通过它查看文本文件的内容,如图25-1所示。

图25-1 TextView.exe运行界面

各位可将任意一个文本文件拖入其中测试。接下来,使用010editor工具查看TextView.exe可执行文件的IDT (Import Directory Table,导入目录表)。

从图25-2中可以看到,TextView.exe中直接导入的DLL文件为KERNEL32.dll、USER32.dll、GDI32.dll、SHELL32.dll。

图25-2 010editor:TextView.exe的IDT

25.1.2 TextView_patched.exe

TextView_patched.exe是修改TextView.exe文件的IDT后得到的文件,即在IDT中添加了导入myhack3.dll的部分,运行时会自动导入myhack3.dll文件。使用PEView工具查看TextView_patched.exe的IDT,如图25-3所示。

图25-2 010editor:TextView_patched.exe的IDT

从图25-3可以看到,IDT中除了原来的4个DLL文件外,还新增了一个myhack3.dll文件。这样,运行TextView_Patched.exe文件时程序就会自动加载myhack3.dll文件。下面运行TextView_Patched.exe看看是否如此。

运行程序并稍等片刻,指定的index.html文件会被下载到工作目录,同时,文本查看程序会自动将其打开,如图25-4所示(运行程序后会自动加载myhack3.dll,尝试连接Google网站,下载网站的index.html文件,并将其拖放到TextView_Patched.exe程序)。进入工作目录,使用网络浏览器打开下载的index.html文件,如图25-5所示。

从图25-5中可以看到,下载的的确是谷歌的index.html文件。

系统环境不同(网络、防火墙策略、安全/管理程序等),可能导致index.html文件无法下载。若正常运行仍无法成功下载index.html文件,建议更换不同的系统环境再次测试。

25.2源代码-myhack3.cpp

本节将分析myhack3.dll的源代码(myhack3.cpp )。

所有源代码均使用 MS Visual C++ 2010 Express Edition 编写而成,在 Windows XP/7 32位环境中通过测试。

25.2.1 DIIMain()

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

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

GetModuleFileName(NULL, szPath, sizeof(szPath));

if( p = _tcsrchr(szPath, L'\\') )
{
_tcscpy_s(p+1, wcslen(DEF_INDEX_FILE)+1, DEF_INDEX_FILE);

OutputDebugString(L"DownloadURL()");
if( DownloadURL(DEF_URL, szPath) )
{
OutputDebugString(L"DropFlie()");
DropFile(szPath);
}
}

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

return 0;
}

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;
}

DllMain()函数的功能非常简单,创建线程运行指定的线程过程,在线程过程(ThreadProc )中调用DownloadURL()与DropFile()函数,下载指定的网页并将其拖放到文本查看程序。下面分别详细查看这2个函数。

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
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;

hInternet = InternetOpen(L"ReverseCore",
INTERNET_OPEN_TYPE_PRECONFIG,
NULL,
NULL,
0);
if( NULL == hInternet )
{
OutputDebugString(L"InternetOpen() failed!");
return FALSE;
}

hURL = InternetOpenUrl(hInternet,
szURL,
NULL,
0,
INTERNET_FLAG_RELOAD,
0);
if( NULL == hURL )
{
OutputDebugString(L"InternetOpenUrl() failed!");
goto _DownloadURL_EXIT;
}

if( err = _tfopen_s(&pFile, szFile, L"wt") )
{
OutputDebugString(L"fopen() failed!");
goto _DownloadURL_EXIT;
}

while( InternetReadFile(hURL, pBuf, DEF_BUF_SIZE, &dwBytesRead) )
{
if( !dwBytesRead )
break;

fwrite(pBuf, dwBytesRead, 1, pFile);
}

bRet = TRUE;

_DownloadURL_EXIT:
if( pFile )
fclose(pFile);

if( hURL )
InternetCloseHandle(hURL);

if( hInternet )
InternetCloseHandle(hInternet);

return bRet;
}

DownloadURL()函数会下载参数szURL中指定的网页文件,并将其保存到szFile目录。示例中,该函数用来连接谷歌网站(www.google.com),并下载网站的index.html文件。

实际上,上述示例中的 DownloadURL()函数是使用 IntemetOpen()、IntemetOpenUrl()、
IntemetReadFile() API 对 URLDownloadToFile() API 的简单实现。IntemetOpen()、
IntemetOpenUrl()、IntemetReadFile() API 均在 wininet.dll 中提供,而 URLDownloadToFile()
API 在 urlmon.dll 中提供。

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
BOOL DropFile(LPCTSTR wcsFile)
{
HWND hWnd = NULL;
DWORD dwBufSize = 0;
BYTE *pBuf = NULL;
DROPFILES *pDrop = NULL;
char szFile[MAX_PATH] = {0,};
HANDLE hMem = 0;

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);
strcpy_s((char*)(pBuf + sizeof(DROPFILES)), strlen(szFile)+1, szFile);

GlobalUnlock(hMem);

if( !(hWnd = GetWindowHandleFromPID(GetCurrentProcessId())) )
{
OutputDebugString(L"GetWndHandleFromPID() failed!!!");
return FALSE;
}

PostMessage(hWnd, WM_DROPFILES, (WPARAM)pBuf, NULL);

return TRUE;
}

DropFile()函数将下载的index.html文件拖放到TextView_Patch.exe进程并显示其内容。为此,需要先获取TextView_Patch.exe进程的主窗口句柄,再传送WM_DROPFILES消息。总之,DropFile()函数的主要功能是,使用PID获取窗口句柄,再调用postMessage(WM_DROPFILES)API将消息放入消息队列(此处省略有关API的详细说明)。

25.2.4 dummy()

在myhack3.cpp源代码中还要注意dummy()这个函数。

dummy()函数是myhack3.dll文件向外部提供服务的导岀函数,但正如所见,它没有任何功能。既然如此,为何还要将其导出呢?这是为了保持形式上的完整性,使myhack3.dll能够顺利添加到TextView.exe文件的导入表。

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

一般而言,向导入表中添加DLL是由程序的构建工具(VC++、VB、Delphi等)完成的,但下面我们将直接使用PE Viewer与Hex Editor两个工具修改TextView.exe的导入表,以便更好地学习代码逆向分析知识。

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

25.3.1 修改思路

如前所见,PE文件中导入的DLL信息以结构体列表形式存储在IDT中。只要将myhack3.dll添加到列表尾部就可以了。当然,此前要确认一下IDT中有无足够空间。

25.3.2查看IDT是否有足够空间

首先,使用PEView查看TextView.exe的IDT地址(PE文件头的IMAGE_OPTIONAL_HEADER
结构体中导入表RVA值即为IDT的RVA )。

图25-6 IDT的RVA值

从图25-6可知,IDT的地址(RVA )为84CC,文件偏移为76CC。

IDT的文件偏移为76CC~772F,整个大小为64字节,共有5个IID结构体,其中最后一个为NULL结构体。从图中可以看出IDT尾部存在其他数据,没有足够空间来添加myhack3.dll的IID结构体。

25.3.3移动 IDT

在这种情形下,我们要先把整个IDT转移到其他更广阔的位置,然后再添加新的IID。确定移
动的目标位置时,可以使用下面三种方式:

□查找文件中的空白区域;

□增加文件最后一个节区的大小;

□在文件末尾添加新节区。

首先尝试第一种方法,即查找文件中的空白区域(程序运行时未使用的区域)。正如在图25-10中看到的一样,.rdata节区尾部恰好存在大片空白区域(一般说来,节区或文件末尾都存在空白区域,PE文件中这种空白区域称为Null-Padding区域)。

接下来,把原IDT移动到该Null-Padding区域(RVA: 8C608DFF )中合适位置就行了。在此之前,先要确认一下该区域(RVA: 8C608DFF)是否全是空白可用区域(Null-Padding区域)。请注意,并不是文件中的所有区域都会被无条件加载到进程的虚拟内存,只有节区头中明确记录的区域才会被加载。使用010Editor工具查看TextView.exe文件的.rdata节区头,如图25-11所示。

图25-11 TextView.exe文件的.rdata节区头

从节区头中信息可以看岀,.rdata节区在磁盘文件与内存中的大小是不同的。 .rdata节区在磁盘文件中的大小为2E00,而文件执行后被加载到内存时,程序实际使用的数据大小(映射大小)仅为2C56,剩余未被使用的区域大小为1AA (2E00-2C56)。在这段空白区域创建IDT是不会有什么问题的。

PE文件尾部有些部分填充着NULL,但这并不意味着这些部分一定就是Null-Padding区域(空白可用区域)。这些区域也有可能是程序使用的区域,且并非所有Null-Padding区域都会加载到内存。只有分析节区头信息后才能判断。如果示例中TextView.exe的Null-Padding区域很小,无法容纳IDT,那么就要增加最后节区的尺寸或添加新节区,以保证有足够空间存放IDT。

由于图25-10中的Null-Padding区域可以使用,接下来,我们要在RVA: 8C80 ( RAW: 7E80 )位置创建IDT (请记住这个位置)

25.4 修改 TextView.exe

先把TextView.exe复制到工作文件夹,重命名为TextView_Patch.exe。下面使用TextView_Patch.exe文件练习打补丁。基本的操作步骤是:先使用PEView打开TextView.exe原文件,查看各种PE信息,然后使用HxD打开TextView_Patch.exe文件进行修改。

25.4.1 修改导入表的RVA值

IMAGE_OPTIONAL_HEADER的导入表结构体成员用来指出IDT的位置(RVA )与大小,如 图25-12所示。

图25-12 IMAGE_0PTI0NAL_HEADER的IMPORT Table RVA/Size

TextView.exe文件中,导入表的RVA值为84CC。接下来,将导入表的RVA值更改为新IDT的RVA值8C80,在Size原值64字节的基础上加14个字节(IID结构体的大小),修改为78字节(参考图25-13 )。

25.4.2 删除绑定导入表

BOUND IMPORT TABLE (绑定导入表)是一种提高DLL加载速度的技术,如图25-14所示。

图25-14 IMAGE_OPTIONAL_HEADER的BOUND IMPORT TABLE RVA/Size

若想正常导入myhack3.dll,需要向绑定导入表添加信息。但幸运的是,该绑定导入表是个可选项,不是必须存在的,所以可删除(修改其值为0即可)以获取更大便利。当然,绑定导入表完全不存在也没关系,但若存在,且其内信息记录错误,则会在程序运行时引发错误。本示例TextView.exe文件中,绑定导入表各项的值均为0,不需要再修改。修改其他文件时,一定要注意检查绑定导入表中的数据。

25.4.3 创建新IDT

先使用Hex Editor完全复制原IDT ( RAW: 76CC~772F ),然后覆写(Paste write)到IDT的新
位置(RAW: 7E80 ),如图25-15所示。

图25-15 HxD: TextView_Patch.exe的新IDT

然后在新IDT尾部(RAW: 7ED0)添加与myhack3.dll对应的IID (后面会单独讲解各成员的数据):

在准确位置(RAW:7ED0)写入相关数据,如图25-16所示。

图25-16 HxD:为myhack3.dll添加IMAGE_IMPORT_DESCRIPTOR结构体数据

25.4.4 设置 Name、INT、IAT

前面添加的IID结构体成员拥有指向其他数据结构(INT、Name、IAT)的RVA值。因此,必须准确设置这些数据结构才能保证TextView_Patch.exe文件正常运行。由前面设置可知INT、Name、IAT的RVA/RAW的值,整理如表25-2所示。

表25-2 INT、Name、IAT

RVA与RAW (文件偏移)间的转换可以借助PEView。但是建议各位掌握它们之间的转换方法,亲自计算(请参考第13章)。

这些地址(RVA: 8D00, 8D10,8D20)就位于新创建的IDT ( RVA: 8C80 )下方。我为了操作方便才选定该区域,各位选择其他位置也没关系。在HxD编辑器中转到7F00地址处,输入相应值,如图25-17所示。

图25-17 HxD:myhack3.dll的INT、Name、IAT

为了更好地理解以上内容,使用PEView打开TextView_Patch.exe文件查看同一区域,查看时使用RVA视图方式,如图25-18所示。

图25-18 PEView: myhack3.dll的INT、Name、IAT

下面讲解图25-18中显示的各值意义。

8CD0地址处存在着myhack3.dll的IID结构体,其中3个主要成员(RVA of INT、RVA of Name,RVA of IAT)的值分别是实际INT、Name、IAT的指针。

简单地说,INT ( Import Name Table,导入名称表)是RVA数组,数组的各个元素都是一个RVA地址,该地址由导入函数的Ordinal ( 2个字节)+Func Name String结构体构成,数组的末尾为NULL。上图中INT有1个元素,其值为8D30,该地址处是要导入的函数的Ordinal (2个字节)与函数的名称字符串(“dummy”)。

Name是包含导入函数的DLL文件名称字符串,在8D10地址处可以看到“myhack3.dll”字符串。

IAT也是RVA数组,各元素既可以拥有与INT相同的值,也可以拥有其他不同值(若INT中数据准确,IAT也可拥有其他不同值)。反正实际运行时,PE装载器会将虚拟内存中的IAT替换为实际函数的地址。

.INT的各元素其实是1个IMAGE_IMPORT_BY_NAME结构体的指针(RVA ),IMAGE_IMPORT_BY_NAME结构体定义如下:

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME { 
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME;

25.4.5 修改IAT节区的属性值

加载PE文件到内存时,PE装载器会修改IAT,写入函数的实际地址,所以相关节区一定要拥有WRITE (可写)属性。只有这样,PE装载器才能正常进行写入操作。使用010Editor查看.rdata节区头,如图25-19所示。

图25-19 .rdata节区头

向原属性值(Characteristics) 40000040添加IMAGE_SCN_MEM_WRITE (80000000)属性值。执行bit OR运算,最终属性值变为C0000040,如图25-20所示。

图25-20 HxD:向.text节区添加可写属性

TextView.exe文件的IAT原位于.rdata节区,且.rdata节区原本就没有可写属性,但程序仍能正常运行。可是若在TextView_Patched.exe中不进行上述操作,程序将无法正常运行。原因在哪?这是因为PE头的IMAGE_OPTIONAL_HEADER结构体DataDirectory数组中存在IAT,如图25-21所示。

若IAT存在于该地址区域( 6000~6154 ),即使相应节区不具有可写属性也没关系。 图25-22是 TextView.exe 文件的 IAT 区域(RVA 6000)的一部分。

从图25-22中可以看出,所有IAT均集中在相同区域(6000~6154)。同样,若不想在TextView_Patched.exe中给.rdata节区添加可写属性,可以在已存在的IAT区域后面为dummyO添加IAT,然后将IAT ( SIZE)增加8个字节。建议各位将经过这样修改的文件保存为 TextView_Patched2.exe.然后再与 TextView_Patched.exe 比较。

我们至此完成了所有修改。运行TextView_Patch.exe文件将会正常加载myhack3.dll文件。

25.5 检测验证

首先使用PEView工具打开修改后的TextView_Patch.exe文件,查看其IDT,如图25-23所示。

向IDT导入myhack3.dll的IID结构体已设置正常。在图25-24中可以看到,myhack3.dll的dummy()函数被添加到INT。

从文件的结构分析来看,修改成功。接下来,直接运行文件看看程序能否正常运行。先将TextView_Patch.exe与myhack3.dll放入相同文件夹,然后运行TextView_Patch.exe文件,如图25-25所示。

使用Process Explorer工具查看TextView_Patch.exe进程中加载的DLL文件,可以看到已经成功加载myhack3.dll ,且被加载的myhack3.dll文件下载了指定网站的index.html文件,并在TextViewPatch. exe中显示。

25.6小结

本章我们一起学习了直接修改PE文件来加载指定DLL文件的方法,其基本原理是将要加载的dll添加到IDT,这样程序运行时就会自动加载。只要理解了这一基本原理,再结合前面学过的有关PE文件头的知识,相信大家能够非常容易理解(关于PE文件头的知识请参考第13章)。

参考

《逆向工程核心原理》 第25章


第25章 通过修改PE加载DLL
https://m0ck1ng-b1rd.github.io/1999/02/23/逆向工程核心原理/第25章 通过修改PE加载DLL/
作者
何语灵
发布于
1999年2月23日
许可协议