逆向工程核心原理-第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 |
|
首先我们根据该卸载程序的执行流程依次分析
_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初始化为FALSEHANDLE 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 复制文件到同一文件夹
运行notepad.exe, 并查看PID
24.3.2 注入myhack.dll
查看ProcessExplorer
可以看到myhack.dll已经成功注入
24.3.3 卸载myhack.dll
可以看到EjectDll.exe成功卸载
再去ProcessExplorer看看
可以看到myhack.dll已经成功被卸载