逆向工程核心原理-24

逆向工程核心原理-第24章-DLL卸载

关于函数解析的部分很多都参考了SYJ学长的博客:https://bbs.pediy.com/thread-267014.htm

24.1 DLL卸载的工作原理

我们前面学习过使用CreateRemoteThread()API进行DLL注入的工作原理, 概括如下:

  • 运行注入程序
  • 注入程序给目标程序远程创建线程, 并在线程中运行LoadLibrary(), 实现注入DLL

而我们的DLL卸载工作原理也非常相似:

  • 运行卸载程序
  • 卸载程序给目标程序远程创建线程, 并在线程中运行FreeLibrary(), 实现卸载DLL

提示: 每个Windows内核对象(KernelObject)都拥有一个引用计数(ReferenceCount)代表对象被使用的次数. 调用10次LoadLibrary(”a.dll”), a.dll的引用计数就变成10, 卸载a.dll时同时需要调用10次Freelibrary()(每调用一次LoadLibrary(), 引用计数加1, 每调用一次Freelibrary(), 引用计数就会减1). 因此卸载DLL时要充分考虑好”引用计数”这个因素

24.2 实现DLL卸载

我们首先分析一下EjectDll.exe程序, 它用来从目标进程(notepad.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
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
#include <windows.h>
#include <tlhelp32.h>
#include <tchar.h>

#define DEF_PROC_NAME (L"notepad.exe") //定义要卸载对应dll的进程名
#define DEF_DLL_NAME (L"myhack.dll") //定义要卸载的dll名

DWORD FindProcessID(LPCTSTR szProcessName)
{
DWORD dwPID = 0xFFFFFFFF; //初始化PID为0xFFFFFFFF
HANDLE hSnapShot = INVALID_HANDLE_VALUE; //初始化快照句柄为INVALID_HANDLE_VALUE
PROCESSENTRY32 pe; //定义一个存放 快照进程信息 的一个结构体
//1.获取当前系统进程快照
pe.dwSize = sizeof(PROCESSENTRY32);
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
//2.查找进程
Process32First(hSnapShot, &pe);
do
{
if (!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile)) //比较进程名
{
dwPID = pe.th32ProcessID;
break;
}
}while (Process32Next(hSnapShot, &pe)); //循环查找
//关闭句柄并返回
CloseHandle(hSnapShot);
return dwPID;
}

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp; //用来储存特权值信息的结构体
HANDLE hToken; //Token的句柄
LUID luid;

//1.打开一个与进程相关联的访问token
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
_tprintf(L"OpenProcessToken failed: %u\n", GetLastError());
return FALSE;
}

//2.查找系统权限值并储存到luid中
if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid))
{

_tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError());
return FALSE;
}

//3.将这些都存入到TOKEN_PRIVILEGES结构体中
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if (bEnablePrivilege) //根据判断本函数的第二个参数设置属性
{
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
}
else
{
tp.Privileges[0].Attributes = 0;
}

//4.提权
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL))
{
_tprintf(L"AdjustTokenPrivileges error:%u\n", GetLastError());
return FALSE;
}

if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
{
_tprintf(L"The token dose not have the specified privilege.\n");
return FALSE;
}
return TRUE;
}

BOOL EjectDll(DWORD dwPID, LPCTSTR szDllName)
{
BOOL bMore = FALSE, bFound = FALSE;
HANDLE hSnapshot, hProcess, hThread;
HMODULE hModule = NULL;
MODULEENTRY32 me = { sizeof(me) }; //定义一个用于储存模块快照的结构体
LPTHREAD_START_ROUTINE pThreadProc;

//1.
//dwPID=notepad进程ID
//使用TH32CS_SNAPMODULE参数
//获取加载到notepad进程的DLL名称
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);

//此函数检索与进程相关联的第一个模块的信息
bMore = Module32First(hSnapshot, &me);

for (; bMore; bMore = Module32Next(hSnapshot, &me)) //bMore用于判断该进程的模块快照是否还有,bFound用于判断是否找到了我们想要卸载的dll模块
{
if (!_tcsicmp((LPCTSTR)me.szModule, szDllName) || !_tcsicmp((LPCTSTR)me.szExePath, szDllName))
{
bFound = TRUE;
break;
}
}

if (!bFound)
{
CloseHandle(hSnapshot);
return FALSE;
}
//2. 通过进程PID获取进程句柄
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
_tprintf(L"OpenProcess(%d) failed!!![%d]\n", dwPID, GetLastError);
return FALSE;
}
//3. 获取FreeLibrary函数的地址
hModule = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary");
//4.创建线程来执行FreeLibrary(modBaseAddr要卸载的dll模块基址)
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL);

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);
CloseHandle(hSnapshot);

return TRUE;
}

int _tmain(int argc, TCHAR* argv[])
{
/*步骤一:查找对应进程的PID*/
DWORD dwPID = 0xFFFFFFFF; //进程的pid是DWORD格式的,先初始化为0xFFFFFFFF
dwPID = FindProcessID(DEF_PROC_NAME);
if (dwPID == 0xFFFFFFFF)
{
_tprintf(L"There is no %s process\n", DEF_PROC_NAME);
return 1;
}
_tprintf(L"PID of \%s\ is %d\n",DEF_PROC_NAME, dwPID);

/*步骤二:更改权限*/
if (!SetPrivilege(SE_DEBUG_NAME, TRUE))
return 1;

/*步骤三:调用函数卸载dll模块*/
if (EjectDll(dwPID, DEF_DLL_NAME))
_tprintf(L"EjectDll(%d,\"%s\") success!!!\n", dwPID, DEF_DLL_NAME);
else
_tprintf(L"EjectDll(%d,\"%s\") failed!!!\n", dwPID, DEF_DLL_NAME);
return 0;
}

首先我们根据该卸载程序的执行流程依次分析

_tmain函数

进入该函数后回对dwPID初始化为0xFFFFFFFF, 随机调用FindProcessID(DEF_PROC_NAME)注意传入的参数时指向”notepad.exe”的字符串, 在上面定义了.

我们跟进FindProcessID(DEF_PROC_NAME)

FindProcessID(DEF_PROC_NAME)函数

DWORD dwPID = 0xFFFFFFFF;

开头定义了dwPID为0xFFFFFFFF,

HANDLE hSnapShot = INVALID_HANDLE_VALUE;

定义并初始化了一个快照句柄, INVALID_HANDLE_VALUE是宏定义的无效句柄值, 在这里作为初始值来使用.
PROCESSENTRY32 pe; //定义一个存放 快照进程信息 的一个结构体

定义一个存放 快照进程信息 的一个结构体

这里看一下该结构体的组成

typedef struct tagPROCESSENTRY32

{

DWORD dwSize; 描述该结构的长度, 以字节为单位, 不初始化该成员就去调用process32Firse会失败

DWORD cntUsage; 无效成员

DWORD th32ProcessID; 被描述进程的PID

ULONG_PTR th32DefaultHeapID; 被描述进程的默认堆ID

DWORD th32ModuleID; 无效成员

DWORD cntThreads; 别描述进程开启的线程数

DWORD th32ParentProcessID; 被描述进程的父进程ID

LONG pcPriClassBase; 被描述线程创建线程的优先权

DWORD dwFlags; 无效成员

TCHAR szExeFile[MAX_PATH]; 进程的可执行文件名称

} PROCESSENTRY32, *****PPROCESSENTRY32;

pe.dwSize = sizeof(PROCESSENTRY32);

从上面可以知道这里是初始化pe的dwSize成员, 要不然后面的Process32Firse会调用失败
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);

通过获取线程信息为指定的进程, 进程使用的堆[HEAP], 模块[MODULE], 线程建立一个快照.

其函数原型及分析:

HANDLE WINAPI CreateToolhelp32Snapshot(

DWORD dwFlags, //用来指定“快照”中需要返回的对象,可以是TH32CS_SNAPPROCESS等

DWORD th32ProcessID //一个进程ID号,用来指定要获取哪一个进程的快照,当获取系统进程列表或获取 当前进程快照时可以设为0

);

在本示例源代码中的dwFlags的值为TH32CS_SNAPALL, 表示在快照中包含系统中所有的进程和线程.

返回值:

若调用成功, 则返回快照的句柄, 调用失败则返回INVALID_HANDLE_VALUE
Process32First(hSnapShot, &pe);

该函数用来从快照中获取进程, 这个函数用来获取第一个进程的句柄, 注意参数的关系, hSnapShot是上面刚刚赋值完成的快照句柄, 其中包含了当前系统中所有的进程和线程, 而&pe值是一个初始化了dwSize的快照进程信息结构体, 只能够存储一个进程的信息
do { if (!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile)) //比较进程名 { dwPID = pe.th32ProcessID; break; } }while (Process32Next(hSnapShot, &pe)); //循环查找

这一个循环先比较了进程名(因为前面先使用了Process32Firse获取了第一个进程信息), 然后比较进程名是否于传入参数szProcessName(指向”notepad.exe”)相同, 相同则退出循环, 不相同寻找下一个进程, 最后的Process32Next就是从hSnapShot中寻找下一个进程, 并将其信息存放在pe快照进程信息结构体中

`CloseHandle(hSnapShot);`

最后关闭快照句柄

`return dwPID;`

返回dwPID, 如果找到了就是一个整数, 如果没找到返回就是初始化的-1, 以此来作为是否找到的信号.

回到main函数

if (dwPID == 0xFFFFFFFF) { _tprintf(L"There is no %s process\n", DEF_PROC_NAME); return 1; }
正如前面所讲, 返回值表示是否找到了notepad.exe进程, 如果没找到则打印相应的错误信息并退出进程

`_tprintf(L"PID of \%s\ is %d\n",DEF_PROC_NAME, dwPID);`

如果找到了就打印其PID并继续执行

if (!SetPrivilege(SE_DEBUG_NAME, TRUE)) return 1;

下面就将调用SetPrivilege(设置权限)函数: 传入的参数SE_DEBUG_NAME是一个系统定义的权限量级, 第二个参数是TRUE

SetPrivilege()函数(设置权限)

TOKEN_PRIVILEGES tp;      //用来储存特权值信息的结构体

用来存储特权值信息的结构体

HANDLE hToken;                  //Token的句柄

令牌(标志)的句柄

LUID luid;

locally unique identifier翻译过来是本地唯一标识符

1.打开一个与进程相关联的访问token

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))

{

_tprintf(L"OpenProcessToken failed: %u\n", GetLastError());

**return** FALSE;

}

我们首先得知道什么是访问令牌: Windows用来描述进程或线程安全上下文的一种对象, 系统使用访问令牌来辨识拥有进程的用户, 以及线程试图执行系统任务时是否具有所需的特权(权限)

下面是OpenProcessToken的函数原型:

BOOL OpenProcessToken(
__in HANDLE ProcessHandle, //要修改访问权限的进程句柄
__in DWORD DesiredAccess, //指定你要进行的操作类型
__out PHANDLE TokenHandle //返回的访问令牌指针
);

示例中第一个参数使用了函数GetCurrentProcess()函数获取当前进程的伪句柄(不反应真实的句柄信息, 只用来作用于当前线程或进程本身)

示例中第二个参数指定了我们要进行的操作是修改访问令牌的特权

第三个参数用来存放返回的访问令牌指针

如果函数调用失败则返回值为0, 打印错误信息后退出

**//**2.查找系统权限值并储存到luid中

**if** (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid))

{

_tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError());

**return** FALSE;

}

调用了LookupPrivilegeValue下面是函数原型

BOOL LookupPrivilegeValue(
LPCTSTR lpSystemName,
LPCTSTR lpName,
PLUID lpLuid);
}

第一个参数表示要查看的系统, 本地系统直接用NULL, 示例中用的就是NULL

第二个参数指向一个以’\0’结尾的字符串, 指定特权的名称, 示例中指向的是SetPrivilege的参数SE_DEBUG_NAME

第三个参数用来接收所返回的指定特权名称的信息, 是上面声明的结构体变量luid

函数调用成功后, 信息存入luid结构体中

**//**3.将这些都存入到TOKEN_PRIVILEGES结构体中

tp.PrivilegeCount **=** 1;

tp.Privileges[0].Luid **=** luid;

下面是变量tp的类型TOKEN_PRIVILEGES的定义

typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount;
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
}

成员一表示PRIVILEGES中的条目数

成员二为一个LUID_AND_ATTRIBUTES结构的数组, 每个结构包含一个特权的LUID和属性

示例中给成员一赋值为一, 表示成员二中只有一个条目, 同时给数组中元素的成员赋值luid

**if** (bEnablePrivilege)  **//**根据判断本函数的第二个参数设置属性

{

tp.Privileges[0].Attributes **=** SE_PRIVILEGE_ENABLED;

}

bEnablePrivilege为SetPrivilege的第二个参数True, 也是给唯一条目的第二个成员赋值TRUE

**else**

{

tp.Privileges[0].Attributes **=** 0;

}

否则置为零

**//**4.提权

**if** (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL))

{

_tprintf(L"AdjustTokenPrivileges error:%u\n", GetLastError());

**return** FALSE;

}

使用了AdjustTokenPrivileges函数, 下面是该函数的原型

AdjustTokenPrivileges是一种函数,用于启用或禁止,指定访问令牌的特权。

启用或禁用特权一个有TOKEN_ADJUST_PRIVILEGES访问的访问令牌.

BOOL AdjustTokenPrivileges(

HANDLE TokenHandle, //包含特权的句柄,

BOOL DisableAllPrivileges,//禁用所有权限标志, 示例中是FALSE, 表示不禁用

PTOKEN_PRIVILEGES NewState,//新特权信息的指针(结构体), 示例中是pt, 就是我们上面赋值的tp

DWORD BufferLength, //缓冲数据大小,以字节为单位的PreviousState的缓存区(sizeof), 不重要

PTOKEN_PRIVILEGES PreviousState,//接收被改变特权当前状态的Buffer, 不重要

PDWORD ReturnLength //接收PreviousState缓存区要求的大小, 不重要

);

如果函数成功, 返回非0值, 为了确定这个函数是否修改了所有指定的特权, 可以调用GetLastErroe函数

**if** (GetLastError() **==** ERROR_NOT_ALL_ASSIGNED)

{

_tprintf(L"The token dose not have the specified privilege.\n");

**return** FALSE;

}

**return** TRUE;

检查是否发生了错误, 如果发生则打印错误信息, 并返回FALSE, 如果成功修改特权, 则返回TRUE

回到mian函数

此时我们已经修改了当前进程(EjectDLL.exe)的特权, 继续跟进

if (EjectDll(dwPID, DEF_DLL_NAME))
如果调用成功, 则打印成功, 失败相反

_tprintf(L"EjectDll(%d,\"%s\") success!!!\n", dwPID, DEF_DLL_NAME); else _tprintf(L"EjectDll(%d,\"%s\") failed!!!\n", dwPID, DEF_DLL_NAME);

return 0;

EjectDll()函数(卸载工作)

main传入了两个参数:

  • notepad.exe的进程PID
  • 我们要卸载的myhack.dll的字符串指针

BOOL bMore = FALSE, bFound = FALSE;
HANDLE hSnapshot, hProcess, hThread;
HMODULE hModule = NULL;
MODULEENTRY32 me = { sizeof(me) }; //定义一个用于储存模块快照的结构体
LPTHREAD_START_ROUTINE pThreadProc;

BOOL bMore = FALSE, bFound = FALSE;

bMore 初始化为FALSE, bFOUND初始化为FALSE
HANDLE hSnapshot, hProcess, hThread;

定义句柄: 快照句柄, 进程句柄, 线程句柄
HMODULE hModule = NULL;
定义模块(DLL)句柄

MODULEENTRY32 me = { sizeof(me) }; //定义一个用于储存模块快照的结构体
存储模块快照的结构体, 注意这里初始化了me的dwSize成员

LPTHREAD_START_ROUTINE pThreadProc;

线程函数的起始地址
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
给notepad进程创建进程快照存入, 参数TH32CS_SNAPMODULE表示在快照中包含指定进程(notepad)的所有模块(DLL)

//此函数检索与进程相关联的第一个模块的信息 bMore = Module32First(hSnapshot, &me);
从快照中得到模块信息, 并存储在me中, 该结构体跟之前的进程信息结构体相似
for (; bMore; bMore = Module32Next(hSnapshot, &me)) //bMore用于判断该进程的模块快照是否还有,

bFound用于判断是否找到了我们想要卸载的dll模块 { if (!_tcsicmp((LPCTSTR)me.szModule, szDllName) || !_tcsicmp((LPCTSTR)me.szExePath, szDllName)) { bFound = TRUE; break; } }
该循环用于寻找模块, 如果找到我们所需的myhack.dll模块则返回. 同时, bFound = TRUE, 用于接下来的返回
if (!bFound) { CloseHandle(hSnapshot); return FALSE; }

相反如果没找到myhack.dll模块, 则关闭快照, 并返回FALSE
//2. 通过进程PID获取进程句柄 if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) { _tprintf(L"OpenProcess(%d) failed!!![%d]\\n", dwPID, GetLastError); return FALSE; }

这里调用了OpenProcess获取notepad进程句柄, 如果成功则继续下一步, 失败则打印错误信息, 并返回FALSE
//3. 获取FreeLibrary函数的地址 hModule = GetModuleHandle(L"kernel32.dll");
获取kernel32.dll句柄, 为接下来找到FreeLibrary做准备

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary");
通过kernel32.dll的句柄获取FreeLibrary库函数句柄

//4.创建线程来执行FreeLibrary(modBaseAddr要卸载的dll模块基址) hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL);
创建线程运行FreeLibrary来卸载myhack.dll
`WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);`

关闭线程
CloseHandle(hProcess);

关闭进程
CloseHandle(hSnapshot);
关闭快照
return TRUE;

返回TRUE表示成功

总体的流程

其实相对于创建远程线程注入DLL, 卸载DLL多了两个步骤, 分别是: 查找PID和提权

注入时不用查找是因为我们命令行参数已经把需要注入的PID写进去了, 而卸载DLL时, 因为没有参数给信息, 所以只好自己去找了, 其实这个放进注入里面也可以的.

找到目标进程的pid和提权后, 卸载指定的myhack.dll, 如果像卸载其他的模块只需要修改函数中的宏定义即可, 该过程通过再notepad中一个一个对比DLL名是否是myhack.dll筛选出myhack.dll, 然后创建线程卸载该DLL

24.3 DLL卸载练习

24.3.1 复制文件到同一文件夹

Untitled

运行notepad.exe, 并查看PID

Untitled

24.3.2 注入myhack.dll

Untitled

查看ProcessExplorer

Untitled

可以看到myhack.dll已经成功注入

24.3.3 卸载myhack.dll

Untitled

可以看到EjectDll.exe成功卸载

再去ProcessExplorer看看

Untitled

可以看到myhack.dll已经成功被卸载

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