逆核系列No.10--修改PE文件注入DLL

本节内容

通过修改PE相应内容达到加载DLL的功能,参考书《逆向工程核心原理》


IMPORT TABLE

通过先前PE格式的相关学习,了解到PE文件中的IMPORT TABLE记录着载入的模块以及相应的导入函数信息

IMPORT TABLE导入表的起始地址位于PE格式中的OptionalHeader.DataDirectory[16]中第二个元素,使用PE-view查看:

(PS:建议还不熟悉PE格式的手动进行字段值的查找练习)

读取出来的IMPORT TABLE的RVA = 0x000084CC,结合各个节区头信息:

因此0x000084CC位于节区.rdata,转换成RAW:0x000084CC-0x00006000+0x00005200 = 0x000076CCHxD下跟随到该地址:

选中蓝色背景是第一个IMAGE_IMPORT_DESCRIPTOR结构体,往后每一个颜色的框都是一个IMAGE_IMPORT_DESCRIPTOR结构体,最后以黄色全NULL标志结束,因此示例程序导入的dll有4个,使用软件验证:

本节通过修改PE文件里的内容达到载入DLL的目的,因此思路就有了:往文件偏移地址0x000076CC的末尾,黄色框处填补要导入的dll构造对应的_IMAGE_IMPORT_DESCRIPTOR结构体,这里人就需要一个全null的_IMAGE_IMPORT_DESCRIPTOR结构体结尾保持PE格式的完成,显然强行操作会覆盖到后续的内容(这可能导致PE文件无法正常的运行)

因此需要考虑将块内容进行搬迁。将搬迁后的IDT地址以及size写回Optionalheader.datadirector[1]。搬迁方案有三:

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

方案一

查找文件空白区域

利用PE文件映射到内存中由于对齐产生的NULL区域,先查看对齐的相关:IMAGE_OPTIONAL_HEADER.SectionAlignment & FileAlignment:

SectionAlignment = 0x00001000 FIleAlignment = 0x00000200,结合节区头信息:

画出.text 和 .rdata映射示意图:

由于节区载入内存的对其关系,使得.text .rdata节区都有200字节的null空白区域

节区.text空白区域地址转化的结果: 0x5C7C -> 0x507C ~0x5200

节区.rdata空白区域地址转化的结果:0x8C56 -> 0x7E56 ~ 0x8000,显然上面所列的节区中包含的空白区域都是足够写入扩容后的IDT信息的。这里使用节区.rdata的空白区域(0x7E56 ~ 0x8000)写入搬迁后的IDT

构建新的_IMAGE_IMPORT_DESCRIPTOR

确定写入的区域后,需要构造导入新dll的结构体_IMAGE_IMPORT_DESCRIPTOR

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //INT(Import Name Table) address (RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; //library name string address (RVA)
DWORD FirstThunk; //IAT(Import Address Table) address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;

这部分是需要结合要导入的DLL源文件

待插入DLL – myhack3.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
#include "stdio.h"
#include "windows.h"
#include "shlobj.h"
#include "Wininet.h"
#include "tchar.h"

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

//略

#ifdef __cplusplus
extern "C" {
#endif
// 出现在IDT 中的dummy export function...
__declspec(dllexport) void dummy() //保证形式完整的需要
{
return;
}
#ifdef __cplusplus
}
#endif


BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
//略
}

这里需要了解一点关于dll文件的知识:

__declspec是Microsoft VC中专用的关键字,它配合着一些属性可以对标准C/C++进行扩充。__declspec关键字应该出现在声明的前面。

__declspec(dllexport)用于Windows中的动态库中,声明导出函数、类、对象等供外面调用,省略给出.def文件。即将函数、类等声明为导出函数,供其它程序调用,作为动态库的对外接口函数、类等。

也就是说需要在PE文件的_IMAGE_IMPORT_DESCRIPTOR设置导入的函数为__declspec修饰的dummy函数,则会完成相应dll的载入?

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //pointer to string dummy
};
DWORD TimeDateStamp; //00
DWORD ForwarderChain; //00
DWORD Name; //pointer to dll name myhack3.dll
DWORD FirstThunk; //pointer to string dummy
} IMAGE_IMPORT_DESCRIPTOR;
1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

关于Hint成员的作用,描述来源

Hint contains the index into the export table of the DLL the function resides in. This field is for use by the PE loader so it can look up the function in the DLL’s export table quickly.This value is not essential and some linkers may set the value in this field to 0.

并不是必须设置的成员,但空间需要保留,设置了可以让PE装载器快速从DLL导出表中获取相关的函数信息

关于这几个结构体的关系,在网上找到了描述得比较好的,感谢互联网:

经过上面的分析,在空白区域写入构建结构体_IMAGE_IMPORT_DESCRIPTOR需要的内容

  • dummy字符串(第一个字节内容需要补充hint的值,这里设置00就好)(myhack3.dll中使用关键字__declspec对其进行修饰,表明myhack3.dummy函数作为动态库myhack3.dll的对外接口函数) 写入位置
  • 链接库名称myhack3.dll字符串

_IMAGE_IMPORT_DESCRIPTOR的成员都是以RVA存放的,不难理解,时PE装载器将PE文件载入内存中运行将相应地址写入完成dll函数调用的,

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //0x0000FF50 -> 0x00008D40(RVA)
};
DWORD TimeDateStamp; //0x00000000
DWORD ForwarderChain; //0x00000000
DWORD Name; //0x00007F30 -> 0x00008D30(RVA)
DWORD FirstThunk; //0x0000FF50 -> 0x00008D40(RVA)
} IMAGE_IMPORT_DESCRIPTOR;

因此,新增的_IMAGE_IMPORT_DESCRIPTOR字节码应该是408D0000 00000000 00000000 308D0000 408D0000

完成_IMAGE_IMPORT_DESCRIPTOR的构造后需要将原来IDT(0x000076CC[RAW])的内容整体搬迁,搬入节区.rdata的空白区域,这里选择的起始地址时0x00007E80(RAW)。

写入节区的权限问题

由于意图将IDT写入节区.rdata,当PE装载器装载PE文件需要往IDT中写入实际函数的地址(由于写入的都是偏移地址,需要根据载入的ImageBase等作相应的变动)因此节区.rdata作为相关区域需要具备可写属性,也就是需要修改.rdata节区头中对节区权限的描述成员characteristic:

当前对节区是没有写入的权限的,关于权限描述,由于比较多这里只列出需要用到的,详情查看官方文档

Flag Meaning
IMAGE_SCN_CNT_INITIALIZED_DATA0x00000040 The section contains initialized data.
IMAGE_SCN_MEM_READ0x40000000 The section can be read.
IMAGE_SCN_MEM_WRITE0x80000000 The section can be written to.

前两个是节区.rdata自身具备的,需要在此基础上往上附加权限,由于该值是使用多个flag对应的值进行OR运算的。

旧权限0x40000040 = 0x00000040 OR 0x40000000,附加上IMAGE_SCN_MEM_WRITE0x80000000 = 0xC0000040,因此修改.rdata节区头中的characteristic = 0xC0000040

写入新的IDT

接着上文分析,新增的_IMAGE_IMPORT_DESCRIPTOR字节码应该是408D0000 00000000 00000000 308D0000 408D0000

完成_IMAGE_IMPORT_DESCRIPTOR的构造后需要将原来IDT(0x000076CC[RAW])的内容整体搬迁,搬入节区.rdata的空白区域,这里选择的起始地址时0x00007E80(RAW)。

修改结束后需要修改Optionalheader.DataDirectory[1]中关于导入表的字段,新的导入表所在地址 = RAW 0x00007E80 = RVA 0x00008C80,导入表的SIze = 0x00000078

特别注意

这里需要指出的是关于BOUND IMPORT TABLE 绑定导入表是一种提高DLL加载速度的技术.对于PE文件而言是可选项, 不是必须. 当修改PE文件时, 如果添加导入的DLL文件, 需要注意有没有绑定导入表, 如果有需要删除或修改绑定导入表, 否则会运行时出错.

由于不是必选项,因此这里直接做null处理,绑定导入表位于Optionalheader.DataDirectory[11]

目标文件中的绑定导入表是null因此不用管

修改验证

先在PE查看器中查看是否识别IDT成功

可以看到达到预期的效果载入myhack3.dll

接下来运行程序,使用进程查看器,看模块myhack3.dll是否加载成功

可以看到dll注入成功,并且dll中的DllMain成功得到执行,由于不是内容重点,这里贴下源码即可:

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
#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
// 出现在IDT 中的dummy export function...
__declspec(dllexport) void dummy() //保证形式完整的需要
{
return;
}
#ifdef __cplusplus
}
#endif

BOOL DownloadURL(LPCTSTR szURL, LPCTSTR szFile)
{
//下载网页文件到本地
}

BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
DWORD dwPID = 0;

GetWindowThreadProcessId(hWnd, &dwPID); //获取开启某个窗口或者新窗口的线程ID

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

return TRUE;
}

HWND GetWindowHandleFromPID(DWORD 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;

WideCharToMultiByte(CP_ACP, 0, wcsFile, -1,
szFile, MAX_PATH, NULL, NULL); //转换字符,转换结果保存在szFile的指针对应的buf中

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

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)); //回到No.149处
break;
}

return TRUE;
}

大致完成在宿主进程中创建新线程,在线程中调用回调函数ThreadProc,在同目录下调用DownloadURL下载文件。

回顾

Author: Victory+
Link: https://cvjark.github.io/2022/05/05/逆核系列No-10-修改PE文件注入DLL/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.