病毒木馬植入模塊成功植入用戶計算機之後,便會啓動攻擊模塊來對用戶計算機數據實施竊取和回傳等操作。通常植入和攻擊是分開在不同模塊之中的,這裏的模塊指的是DLLexe或其他加密的PE文件等。只有當前植入模塊成功執行後,方可繼續執行攻擊模塊,同時會刪除植入模塊的數據和文件。模塊化開發的好處不單單是便於開發管理,同時也可以減小因某一模塊的失敗而導致整個程序暴露的可能性。

本章介紹了3種常用的病毒木馬啓動技術,它包括:

q創建進程API:介紹使用WinExecShellExecute以及CreateProcess創建進程。

qSESSION 0隔離創建進程:主要通過CreateProcessAsUser函數實現用戶進程創建

q內存直接加載運行:模擬PE加載器,直接將DLLexePE文件加載到內存並啓動運行。

4.1 創建進程API

在一個進程中創建並啓動一個新進程,無論是對於病毒木馬程序還是普通的應用程序而言,這都是一個常見的技術,最簡單的方法無非是直接通過調用WIN32 API函數創建新進程。用戶層上,微軟提供了WinExecShellExecuteCreateProcess等函數來實現進程創建。

WinExecShellExecute以及CreateProcess除了可以創建進程外,還能執行CMD命令等功能。接下來,本節將介紹使用WinExecShellExecute以及CreateProcess函數創建進程。

4.1.1 函數介紹 1.WinExec函數

運行指定的應用程序。

函數聲明

UINT WINAPI WinExec(

_In_ LPCTSTR lpCmdLine,

_In_ UINT uCmdShow)

參數

lpCmdLine [in]

要執行的應用程序的命令行。如果在lpCmdLine參數中可執行文件的名稱不包含目錄路徑,則系統將按以下順序搜索可執行文件:

應用程序的目錄、當前目錄、Windows系統目錄、Windows目錄以及PATH環境變量中列出的目錄。

uCmdShow [in]

顯示選項。SW_HIDE表示隱藏窗口並激活其他窗口;SW_SHOWNORMAL表示激活並顯示一個窗口。

返回值

如果函數成功,則返回值大於31

如果函數失敗,則返回值是以下錯誤值之一。

0

系統內存或資源不足

ERROR_BAD_FORMAT

exe文件無效

ERROR_FILE_NOT_FOUND

找不到指定文件

ERROR_PATH_NOT_FOUND

找不到指定的路徑

2.ShellExecute函數

運行一個外部程序(或者是打開一個已註冊的文件、目錄,或打印一個文件等),並對外部程序進行一定程度的控制。

函數聲明

HINSTANCE ShellExecute(

_In_opt_ HWND hwnd,

_In_opt_ LPCTSTR lpOperation,

_In_ LPCTSTR lpFile,

_In_opt_ LPCTSTR lpParameters,

_In_opt_ LPCTSTR lpDirectory,

_In_ INT nShowCmd)

參數

hwnd [in, optional]

用於顯示UI或錯誤消息的父窗口的句柄。如果操作不與窗口關聯,則此值可以爲NULL

lpOperation [in, optional]

指向以空字符結尾的字符串的指針,它在本例中稱爲動詞,用於指定要執行的操作。常使用的動詞有:

edit:啓動編輯器並打開文檔進行編輯。如果lpFile不是文檔文件,則該函數將失敗。

explore:探索由lpFile指定的文件夾。

find:在由lpDirectory指定的目錄中啓動搜索。

open:打開由lpFile參數指定的項目。該項目可以是文件也可是文件夾。

print:打印由lpFile指定的文件。如果lpFile不是文檔文件,則該函數失敗。

NULL:如果可用,則使用默認動詞。如果不可用,則使用“打開”動詞。如果兩個動詞都不可用,則系統使用註冊表中列出的第一個動詞。

lpFile [in]

指向以空字符結尾的字符串的指針,該字符串要在其上執行指定謂詞的文件或對象。要指定一個Shell名稱空間對象,傳遞完全限定的解析名稱。如果lpDirectory參數使用相對路徑,則lpFile不要使用相對路徑。

lpParameters [in, optional]

如果lpFile指定一個可執行文件,則此參數是一個指向以空字符結尾的字符串的指針,該字符串指定要傳遞給應用程序的參數。如果lpFile指定一個文檔文件,則lpParameters應該爲NULL

lpDirectory [in, optional]

指向以空終止的字符串的指針,該字符串指定操作的默認目錄。如果此值爲NULL,則使用當前的工作目錄。如果在lpFile中提供了相對路徑,請不要對lpDirectory使用相對路徑。

nShowCmd [in]

指定應用程序在打開時如何顯示標誌。SW_HIDE表示隱藏窗口並激活其他窗口;SW_SHOWNORMAL表示激活並顯示一個窗口。

返回值

如果函數成功,則返回大於32的值。如果該函數失敗,則它將返回一個錯誤值,指示失敗的原因。

3.CreateProcess函數

創建一個新進程及主線程。新進程在調用進程的安全的上下文中運行。

函數聲明

BOOL WINAPI CreateProcess(

_In_opt_ LPCTSTR lpApplicationName,

_Inout_opt_ LPTSTR lpCommandLine,

_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,

_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,

_In_ BOOL bInheritHandles,

_In_ DWORD dwCreationFlags,

_In_opt_ LPVOID lpEnvironment,

_In_opt_ LPCTSTR lpCurrentDirectory,

_In_ LPSTARTUPINFO lpStartupInfo,

_Out_ LPPROCESS_INFORMATION lpProcessInformation)

參數

lpApplicationName [in, optional]

要執行的模塊的名稱。lpApplicationName參數可以是NULL。要運行批處理文件,必須啓動命令解釋程序,並將lpApplicationName設置爲cmd.exe

lpCommandLine [in, out, optional]

要執行的命令行。lpCommandLine參數可以是NULL。在這種情況下,該函數使用由lpApplicationName指向的字符串作爲命令行。如果lpApplicationNamelpCommandLine都不爲NULL,則由lpApplicationName指向的以空字符結尾的字符串會指定要執行的模塊,並且由lpCommandLine指向的以空字符結尾的字符串會指定命令行。

lpProcessAttributes [in, optional]

指向SECURITY_ATTRIBUTES結構的指針,用於確定是否可以由子進程繼承返回的新進程對象的句柄。如果lpProcessAttributesNULL,則不能繼承句柄。

lpThreadAttributes [in, optional]

指向SECURITY_ATTRIBUTES結構的指針,用於確定是否可以由子進程繼承返回的新線程對象的句柄。如果lpThreadAttributesNULL,則不能繼承句柄。

bInheritHandles [in]

如果此參數爲TRUE,則調用進程中的每個可繼承句柄都將由新進程來繼承。如果該參數爲FALSE,則不會繼承句柄。

dwCreationFlags [in]

控制優先級和創建進程的標誌。例如,CREATE_NEW_CONSOLE表示新進程將使用一個新控制檯,而不是繼承父進程的控制檯。CREATE_SUSPENDED表示新進程的主線程會以暫停的狀態來創建,直到調用ResumeThread函數時才運行。

lpEnvironment [in, optional]

指向新進程的環境塊的指針。如果此參數爲NULL,則新進程將使用調用進程的環境。

lpCurrentDirectory [in, optional]

指向進程當前目錄的完整路徑。該字符串還可以指定UNC路徑。如果此參數爲NULL,則新進程將具有與調用進程相同的當前驅動器和目錄。

lpStartupInfo [in]

指向STARTUPINFOSTARTUPINFOEX結構的指針。STARTUPINFOSTARTUPINFOEX的句柄在不需要時必須由CloseHandle關閉。

lpProcessInformation [out]

指向PROCESS_INFORMATION結構的指針,用於接收有關新進程的標識信息。PROCESS_INFORMATION中的句柄必須在不需要時由CloseHandle關閉。

返回值

如果函數成功,則返回值非零。

如果函數失敗,則返回值爲零。

4.1.2 實現過程

直接調用WinExec函數創建進程,具體的實現代碼如下所示。

BOOL WinExec_Test(char *pszExePath, UINT uiCmdShow)

{

UINT uiRet = 0;

uiRet = ::WinExec(pszExePath, uiCmdShow);

if (31 < uiRet)

{

return TRUE;

}

return FALSE;

}

在上述代碼中,WinExec函數只有兩個參數,第一個參數指定程序路徑或者CMD命令行,第二個參數指定顯示方式。若返回值大於31,則表示WinExec執行成功,否則執行失敗。

直接調用ShellExecute函數創建進程,具體的實現代碼如下所示。

BOOL ShellExecute_Test(char *pszExePath, UINT uiCmdShow)

{

HINSTANCE hInstance = 0;

hInstance = ::ShellExecute(NULL, NULL, pszExePath, NULL, NULL, uiCmdShow);

if (32 < (DWORD)hInstance)

{

return TRUE;

}

return FALSE;

}

ShellExecute函數不僅可以運行exe文件,也可以運行已經關聯的文件。例如,可以打開網頁、發送郵件、以默認程序打開文件、打開目錄、打印文件等。若返回值大於32,則表示執行成功,否則執行失敗。

直接調用CreateProcess函數創建進程,具體的實現代碼如下所示。

BOOL CreateProcess_Test(char *pszExePath, UINT uiCmdShow)

{

STARTUPINFO si = { 0 };

PROCESS_INFORMATION pi;

BOOL bRet = FALSE;

si.cb = sizeof(si);

si.dwFlags = STARTF_USESHOWWINDOW; //指定wShowWindow成員有效

si.wShowWindow = uiCmdShow;

bRet = ::CreateProcess(NULL, pszExePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

if (bRet)

{

//不使用的句柄最好關掉

::CloseHandle(pi.hThread);

::CloseHandle(pi.hProcess);

return TRUE;

}

return FALSE;

}

WinExec以及ShellExecute函數相比較而言,CreateProcess函數的參數更多,使用起來更復雜。我們着重關注以下5個參數:執行模塊名稱的參數lpApplicationName、執行命令行的參數lpCommandLine、控制進程優先級和創建進程標誌的參數dwCreationFlags、指向STARTUPINFO信息結構的參數lpStartupInfo,以及指向PROCESS_INFORMATION信息結構的參數lpProcessInformation。若CreateProcess函數執行成功,則返回TRUE,否則返回FALSE

4.1.3 測試

程序分別調用WinExec函數、ShellExecute函數,以及CreateProcess函數來創建1.exe2.exe以及3.exe進程,並以SW_SHOWNORMAL方式顯示程序窗口。直接運行上述程序,程序提示1.exe2.exe以及3.exe進程成功創建並運行,如圖4-1所示。

4-1 創建進程

4.1.4 小結

本小節主要通過調用WinExec函數、ShellExecute函數,以及CreateProcess函數來創建進程,實現程序的關鍵是對函數參數的理解。其中,除了進程路徑參數較爲重要之外,窗口顯示方式也值得注意。

WinExecShellExecute函數設置爲SW_HIDE方式可隱藏運行程序窗口,並且成功隱藏執行CMD命令行的窗口,對於其他程序窗口不能成功隱藏。而CreateProcess函數在指定窗口顯示方式的時候,需要在STARTUPINFO結構體中將啓用標誌設置爲STARTF_ USESHOWWINDOW,表示wShowWindow成員顯示方式有效。然後將wShowWindow置爲SW_HIDE隱藏窗口,創建方式爲CREATE_NEW_CONSOLE創建一個新控制檯,這樣可以成功隱藏執行CMD命令行的窗口,而其他程序窗口則不能成功隱藏。

如果在一個進程中想要創建以隱藏方式運行的進程,即隱藏進程窗口,則可以通過SendMessage向窗口發送SW_HIDE隱藏消息,也可以通過ShowWindow函數設置SW_HIDE來使窗口隱藏。這兩種實現方式的前提是已獲取了窗口的句柄。

WinExec只用於可執行文件,雖然使用方便,但是一個老函數。ShellExcute可通過Windows外殼打開任意文件,非可執行文件自動通過關聯程序打開對應的可執行文件,區別不大,不過ShellExcute可以指定運行時的工作路徑。WinExec必須得到GetMessage或超時之後才返回,而ShellExecuteCreateProcess都是無需等待直接返回的。

用戶層上,通常是利用WMI或者通過HOOK API來監控進程的創建。EnumWindows函數可以枚舉所有屏幕上的頂層窗口,包括隱藏窗口。

4.2 突破SESSION 0隔離創建用戶進程

病毒木馬通常會把自己注入系統服務進程或是僞裝成系統服務進程,並運行在SESSION 0中。處於SESSION 0中的程序能正常執行普通程序的絕大部分操作,但是個別操作除外。例如,處於SESSION 0中的系統服務進程,無法與普通用戶進程通信,不能通過Windows消息機制進行通信,更不能創建普通的用戶進程。

Windows XPWindows Server 2003,以及更老版本的Windows操作系統中,服務和應用程序使用相同的會話(SESSION)來運行,而這個會話是由第一個登錄到控制檯的用戶來啓動的,該會話就稱爲SESSION 0。將服務和用戶應用程序一起在SESSION 0中運行會導致安全風險,因爲服務會使用提升後的權限來運行,而用戶應用程序使用用戶特權(大部分都是非管理員用戶)運行,這會使得惡意軟件把某個服務作爲攻擊目標,通過“劫持”該服務以達到提升自己權限級別的目的。

Windows VISTA開始,只有服務可以託管到SESSION 0中,用戶應用程序和服務之間會進行隔離,並需要運行在用戶登錄系統時創建的後續會話中。如第一個登錄用戶創建Session 1,第二個登錄用戶創建Session 2,以此類推。

使用不同會話運行的實體(應用程序或服務)如果不將自己明確標註爲全局命名空間,並提供相應的訪問控制設置,那麼將無法互相發送消息,共享UI元素或共享內核對象。

Windows 7及以上版本的SESSION 0給服務層和應用層間的通信造成了很大的難度,但這並不代表沒有辦法實現服務層與應用層間的通信與交互。微軟提供了一系列以WTSWindows Terminal ServiceWindows終端服務)開頭的函數,從而可以完成服務層與應用層的交互

接下來,本節將介紹突破SESSION 0隔離,在服務程序中創建用戶桌面進程。

4.2.1 函數介紹 1.WTSGetActiveConsoleSessionId函數

檢索控制檯會話的標識符Session Id。控制檯會話是當前連接到物理控制檯的會話。

函數聲明

DWORD WTSGetActiveConsoleSessionId(void)

參數

無參數

返回值

如果執行成功,則返回連接到物理控制檯的會話標識符。

如果沒有連接到物理控制檯的會話(例如,物理控制檯會話正在附加或分離),則此函數返回0xFFFFFFFF

2.WTSQueryUserToken函數

獲取由Session Id指定的登錄用戶的主訪問令牌。要想成功調用此功能,則調用應用程序必須在本地系統賬戶的上下文中運行,並具有SE_TCB_NAME特權。

函數聲明

BOOL WTSQueryUserToken(

_In_ ULONG SessionId,

_Out_ PHANDLE phToken)

參數

SessionId [in]

遠程桌面服務會話標識符。在服務上下文中運行的任何程序都將具有一個值爲0的會話標識符。

phToken [out]

如果該功能成功,則會收到一個指向登錄用戶令牌句柄的指針。請注意,必須調用CloseHandle數才能關閉該句柄。

返回值

如果函數成功,則返回值非零,phToken參數指向用戶的主令牌;如果函數失敗,則返回值爲零。

3.DuplicateTokenEx函數

創建一個新的訪問令牌,它與現有令牌重複。此功能可以創建主令牌或模擬令牌。

函數聲明

BOOL WINAPI DuplicateTokenEx(

_In_ HANDLE hExistingToken,

_In_ DWORD dwDesiredAccess,

_In_opt_ LPSECURITY_ATTRIBUTES lpTokenAttributes,

_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,

_In_ TOKEN_TYPE TokenType,

_Out_ PHANDLE phNewToken)

參數

hExistingToken [in]

使用TOKEN_DUPLICATE訪問權限打開訪問令牌的句柄。

dwDesiredAccess [in]

指定新令牌的請求訪問權限。要想請求對調用者有效的所有訪問權限,請指定MAXIMUM_ ALLOWED

lpTokenAttributes [inoptional]

指向SECURITY_ATTRIBUTES結構的指針,該結構指定新令牌的安全描述符,並確定子進程是否可以繼承令牌。如果lpTokenAttributesNULL,則令牌獲取默認的安全描述符,並且不能繼承該句柄。

ImpersonationLevel [in]

指定SECURITY_IMPERSONATION_LEVEL枚舉中指示新令牌模擬級別的值。

TokenType [in]

TOKEN_TYPE枚舉中指定以下值之一。

TokenPrimary

新令牌是可以在CreateProcessAsUser函數中使用的主令牌

TokenImpersonation

新令牌是一個模擬令牌

phNewToken [out]

指向接收新令牌的HANDLE變量的指針。新令牌使用完成後,調用CloseHandle函數來關閉令牌句柄。

返回值

如果函數成功,則函數將返回一個非零值;

如果函數失敗,則返回值爲零。

4.CreateEnvironmentBlock函數

檢索指定用戶的環境變量,然後可以將此塊傳遞給CreateProcessAsUser函數。

函數聲明

BOOL WINAPI CreateEnvironmentBlock(

_Out_ LPVOID *lpEnvironment,

_In_opt_ HANDLE hToken,

_In_ BOOL bInherit)

參數

lpEnvironment [out]

當該函數返回時,已接收到指向新環境塊的指針。

hToken [inoptional]

Logon爲用戶,從LogonUser函數返回。如果這是主令牌,則令牌必須具有TOKEN_QUERYTOKEN_DUPLICATE訪問權限。如果令牌是模擬令牌,則必須具有TOKEN_QUERY權限。如果此參數爲NULL,則返回的環境塊僅包含系統變量。

bInherit[in]

指定是否可以繼承當前進程的環境。如果該值爲TRUE,則該進程將繼承當前進程的環境;如果此值爲FALSE,則該進程不會繼承當前進程的環境。

返回值

如果函數成功,則函數將返回TRUE;如果函數失敗,則返回FALSE

5.CreateProcessAsUser函數

創建一個新進程及主線程,新進程在由指定令牌表示的用戶安全上下文中運行。

函數聲明

BOOL WINAPI CreateProcessAsUser(

_In_opt_ HANDLE hToken,

_In_opt_ LPCTSTR lpApplicationName,

_Inout_opt_ LPTSTR lpCommandLine,

_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,

_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,

_In_ BOOL bInheritHandles,

_In_ DWORD dwCreationFlags,

_In_opt_ LPVOID lpEnvironment,

_In_opt_ LPCTSTR lpCurrentDirectory,

_In_ LPSTARTUPINFO lpStartupInfo,

_Out_ LPPROCESS_INFORMATION lpProcessInformation)

參數

hToken [inoptional]

表示用戶主令牌的句柄。句柄必須具有TOKEN_QUERYTOKEN_DUPLICATETOKEN_ASSIGN_PRIMARY訪問權限。

lpApplicationName [inoptional]

要執行模塊的名稱。該模塊可以基於Windows應用程序。

lpCommandLine [inoutoptional]

要執行的命令行。該字符串的最大長度爲32K個字符。如果lpApplicationNameNULL,則lpCommandLine模塊名稱的長度限制爲MAX_PATH個字符。

lpProcessAttributes [inoptional]

指向SECURITY_ATTRIBUTES結構的指針,該結構指定新進程對象的安全描述符,並確定子進程是否可以繼承返回進程的句柄。如果lpProcessAttributesNULLlpSecurityDeorNULL,則該進程將獲得默認的安全描述符,並且不能繼承該句柄。

lpThreadAttributes [inoptional]

指向SECURITY_ATTRIBUTES結構的指針,該結構指定新線程對象的安全描述符,並確定子進程是否可以繼承返回線程的句柄。如果lpThreadAttributesNULLlpSecurityDeorNULL,則線程將獲取默認的安全描述符,並且不能繼承該句柄。

bInheritHandles [in]

如果此參數爲TRUE,則調用進程中的每個可繼承句柄都由新進程繼承;如果參數爲FALSE,則不能繼承句柄。請注意,繼承的句柄具有與原始句柄相同的值和訪問權限。

dwCreationFlags [in]

控制優先級和進程創建的標誌。

lpEnvironment [inoptional]

指向新進程環境塊的指針。如果此參數爲NULL,則新進程將使用調用進程的環境。

lpCurrentDirectory [inoptional]

指向進程當前目錄的完整路徑。如果此參數爲NULL,則新進程將具有與調用進程相同的當前驅動器和目錄。

lpStartupInfo [in]

指向STARTUPINFOSTARTUPINFOEX結構的指針。用戶必須具有對指定窗口站和桌面的完全訪問權限。

lpProcessInformation [out]

指向一個PROCESS_INFORMATION結構的指針,用於接收新進程的標識信息。PROCESS_INFORMATION中的句柄必須在不需要時使用CloseHandle關閉。

返回值

如果函數成功,則函數將返回一個非零值;如果函數失敗,則返回零。

4.2.2 實現原理

由於SESSION 0的隔離,使得在系統服務進程內不能直接調用CreateProcess等函數創建進程,而只能通過CreateProcessAsUser函數來創建。這樣,創建的進程纔會顯示UI界面,與用戶進行交互。

SESSION 0中創建用戶桌面進程具體的實現流程如下所示。

首先,調用WTSGetActiveConsoleSessionId函數來獲取當前程序的會話ID,即Session Id。調用該函數不需要任何參數,直接返回Session Id。根據Session Id繼續調用WTSQueryUserToken函數來檢索用戶令牌,並獲取對應的用戶令牌句柄。在不需要使用用戶令牌句柄時,可以調用CloseHandle函數來釋放句柄。

其次,使用DuplicateTokenEx函數創建一個新令牌,並複製上面獲取的用戶令牌。設置新令牌的訪問權限爲MAXIMUM_ALLOWED,這表示獲取所有令牌權限。新訪問令牌的模擬級別爲SecurityIdentification,而且令牌類型爲TokenPrimary,這表示新令牌是可以在CreateProcessAsUser函數中使用的主令牌。

最後,根據新令牌調用CreateEnvironmentBlock函數創建一個環境塊,用來傳遞給CreateProcessAsUser使用。在不需要使用進程環境塊時,可以通過調用DestroyEnvironmentBlock函數進行釋放。獲取環境塊後,就可以調用CreateProcessAsUser來創建用戶桌面進程。CreateProcessAsUser函數的用法以及參數含義與CreateProcess函數的用法和參數含義類似。新令牌句柄作爲用戶主令牌的句柄,指定創建進程的路徑,設置優先級和創建標誌,設置STARTUPINFO結構信息,獲取PROCESS_INFORMATION結構信息。

經過上述操作後,就完成了用戶桌面進程的創建。但是,上述方法創建的用戶桌面進程並沒有繼承服務程序的系統權限,只具有普通權限。要想創建一個有系統權限的子進程,這可以通過設置進程訪問令牌的安全描述符來實現,具體的實現步驟在此就不詳細介紹了。

4.2.3 編碼實現

// 突破SESSION 0隔離創建用戶進程

BOOL CreateUserProcess(char *lpszFileName)

{

// 變量()

do

{

// 獲得當前Session Id

dwSessionID = ::WTSGetActiveConsoleSessionId();

// 獲得當前會話的用戶令牌

if (FALSE == ::WTSQueryUserToken(dwSessionID, &hToken))

{

ShowMessage("WTSQueryUserToken", "ERROR");

bRet = FALSE;

break;

}

// 複製令牌

if (FALSE == ::DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,

SecurityIdentification, TokenPrimary, &hDuplicatedToken))

{

ShowMessage("DuplicateTokenEx", "ERROR");

bRet = FALSE;

break;

}

// 創建用戶會話環境

if (FALSE == ::CreateEnvironmentBlock(&lpEnvironment,

hDuplicatedToken, FALSE))

{

ShowMessage("CreateEnvironmentBlock", "ERROR");

bRet = FALSE;

break;

}

// 在複製的用戶會話下執行應用程序,創建進程

if (FALSE == ::CreateProcessAsUser(hDuplicatedToken,

lpszFileName, NULL, NULL, NULL, FALSE,

NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,

lpEnvironment, NULL, &si, &pi))

{

ShowMessage("CreateProcessAsUser", "ERROR");

bRet = FALSE;

break;

}

} while (FALSE);

// 關閉句柄釋放資源()

return bRet;

}

4.2.4 測試

因爲程序要實現的是突破SESSION 0隔離,所以,在系統服務程序中創建用戶桌面進程。程序必須註冊成爲一個系統服務進程,這樣才處於SESSION 0中。服務程序的入口點與普通程序的入口點不同,需要通過調用函數StartServiceCtrlDispatcher來設置服務入口點函數。對於創建服務程序的內容本書沒有進行具體講解,讀者可以閱讀配套的示例代碼來理解該部分內容。同時,本書還開發了一個服務加載器ServiceLoader.exe(該加載器的源碼可以在相應章節的配套示例代碼中找到),它可將測試程序加載爲服務進程。

main函數中,設置服務入口點函數,使之成爲服務程序,並在服務程序中調用上述封裝好的函數進行測試。首先,以管理員身份運行服務加載器ServiceLoader.exe,這樣服務加載器會將CreateProcessAsUser_Test.exe程序加載爲服務進程,從而執行創建用戶進程的代碼。服務加載器提示創建和啓動服務成功後,立即顯示對話框和啓動“520.exe”程序,而且窗口界面也成功顯示,如圖4-2所示。

4-2 成功創建520.exe進程

然後,使用進程查看器ProcessExplorer.exe查看CreateProcessAsUser_Test.exe進程以及520.exe進程中的SESSION值,如圖4-3所示,CreateProcessAsUser_Test.exe進程處於SESSION 0中,而520.exe處於SESSION 1中。

4-3 SESSION信息

4.2.5 小結

突破SESSION 0隔離創建用戶進程,要求程序處於SESSION 0中,這樣纔會有效。創建服務程序時,需要在main函數中設置服務程序入口點函數,這樣才能成功地爲程序創建系統服務。該程序實現的關鍵是調用CreateProcessAsUser函數。需要程序創建並複製一個新的訪問令牌,並獲取訪問令牌的進程環境塊信息。

由於本節介紹的方法並沒有對進程訪問令牌進行設置,所以創建出來的用戶桌面進程是用戶默認的權限,並沒有繼承系統權限。

可以通過掛鉤CreateProcessAsUser函數監控進程創建。

4.3 內存直接加載運行

有很多病毒木馬都具有模擬PE加載器的功能,它們把DLL或者exePE文件從內存中直接加載到病毒木馬的內存中去執行,不需要通過LoadLibrary等現成的API函數去操作,以此躲過殺毒軟件的攔截檢測。

這種技術當然有積極的一面。假如程序需要動態調用DLL文件,內存加載運行技術可以把這些DLL作爲資源插入到自己的程序中。此時直接在內存中加載運行即可,不需要再將DLL釋放到本地。

本節主要針對DLLexe這兩種PE文件進行介紹,分別剖析如何直接從內存中加載運行。這兩種文件具體的實現原理相同,只需掌握其中一種,另一種也就容易掌握了。

4.3.1 實現原理

要想完全理解透徹內存直接加載運行技術,需要對PE文件結構有比較詳細的瞭解,至少要了解PE格式的導入表、導出表以及重定位表的具體操作過程。因爲內存直接加載運行技術的核心就是模擬PE加載器加載PE文件的過程,也就是對導入表、導出表以及重定位表的操作過程

那麼程序需要進行哪些操作便可以直接從內存中加載運行DLL或是exe文件呢?以加載DLL爲例介紹。

首先就是要把DLL文件按照映像對齊大小映射到內存中,切不可直接將DLL文件數據存儲到內存中。因爲根據PE結構的基礎知識可知,PE文件有兩個對齊字段,一個是映像對齊大小SectionAlignment,另一個是文件對齊大小FileAlignment。其中,映像對齊大小是PE文件加載到內存中所用的對齊大小,而文件對齊大小是PE文件存儲在本地磁盤所用的對齊大小。一般文件對齊大小會比映像對齊大小要小,這樣文件會變小,以此節省磁盤空間。

然而,成功映射內存數據之後,在DLL程序中會存在硬編碼數據,硬編碼都是以默認的加載基址作爲基址來計算的。由於DLL可以任意加載到其他進程空間中,所以DLL的加載基址並非固定不變。當改變加載基址的時候,硬編碼也要隨之改變,這樣DLL程序纔會計算正確。但是,如何才能知道需要修改哪些硬編碼呢?換句話說,如何知道硬編碼的位置?答案就藏在PE結構的重定位表中,重定位表記錄的就是程序中所有需要修改的硬編碼的相對偏移位置。

根據重定位表修改硬編碼數據後,這只是完成了一半的工作。DLL作爲一個程序,自然也會調用其他庫函數,例如MessageBox。那麼DLL如何知道MessageBox函數的地址呢?它只有獲取正確的調用函數地址後,方可正確調用函數。PE結構使用導入表來記錄PE程序中所有引用的函數及其函數地址。在DLL映射到內存之後,需要根據導入表中的導入模塊和函數名稱來獲取調用函數的地址。若想從導入模塊中獲取導出函數的地址,最簡單的方式是通過GetProcAddress函數來獲取。但是爲了避免調用敏感的WIN32 API函數而被殺軟攔截檢測,本書採用直接遍歷PE結構導出表的方式來獲取導出函數地址,這要求讀者熟悉導出表的具體操作原理。

完成上述操作之後,DLL加載工作纔算完成,接下來便是獲取入口地址並跳轉執行以便完成啓動。

具體的實現流程總結如下。

先,在DLL文件中,根據PE結構獲取其加載映像的大小SizeOfImage,並根據SizeOfImage在自己的程序中申請可讀、可寫、可執行的內存,那麼這塊內存的首地址就是DLL的加載基址

其次,根據DLL中的PE結構獲取其映像對齊大小SectionAlignment,然後把DLL文件數據按照SectionAlignment複製到上述申請的可讀、可寫、可執行的內存中。

接下來,根據PE結構的重定位表,重新對重定位表進行修正。

然後,根據PE結構的導入表,加載所需的DLL,並獲取導入函數的地址並寫入導入表中。

接着,修改DLL的加載基址ImageBase

最後,根據PE結構獲取DLL的入口地址,然後構造並調用DllMain函數,實現DLL加載。

exe文件相對於DLL文件實現原理唯一的區別就在於構造入口函數的差別,exe不需要構造DllMain函數,而是根據PE結構獲取exe的入口地址偏移AddressOfEntryPoint並計算出入口地址,然後直接跳轉到入口地址處執行即可。

要特別注意的是,對於exe文件來說,重定位表不是必需的,即使沒有重定位表,exe也可正常運行。因爲對於exe進程來說,進程最早加載的模塊是exe模塊,所以它可以按照默認的加載基址加載到內存。對於那些沒有重定位表的程序,只能把它加載到默認的加載基址上。如果默認加載基址已被佔用,則直接內存加載運行會失敗。

4.3.2 編碼實現

// 模擬LoadLibrary加載內存DLL文件到進程中

LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize)

{

LPVOID lpBaseAddress = NULL;

// 獲取鏡像大小

DWORD dwSizeOfImage = GetSizeOfImage(lpData);

// 在進程中申請一個可讀、可寫、可執行的內存塊

lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

if (NULL == lpBaseAddress)

{

ShowError("VirtualAlloc");

return NULL;

}

::RtlZeroMemory(lpBaseAddress, dwSizeOfImage);

// 將內存DLL數據按SectionAlignment大小對齊映射到進程內存中

if (FALSE == MmMapFile(lpData, lpBaseAddress))

{

ShowError("MmMapFile");

return NULL;

}

// 修改PE文件的重定位表信息

if (FALSE == DoRelocationTable(lpBaseAddress))

{

ShowError("DoRelocationTable");

return NULL;

}

// 填寫PE文件的導入表信息

if (FALSE == DoImportTable(lpBaseAddress))

{

ShowError("DoImportTable");

return NULL;

}

//修改頁屬性, 統一設置成屬性PAGE_EXECUTE_READWRITE

DWORD dwOldProtect = 0;

if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))

{

ShowError("VirtualProtect");

return NULL;

}

// 修改PE文件的加載基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase

if (FALSE == SetImageBase(lpBaseAddress))

{

ShowError("SetImageBase");

return NULL;

}

// 調用DLL的入口函數DllMain,函數地址即爲PE文件的入口點AddressOfEntryPoint

if (FALSE == CallDllMain(lpBaseAddress))

{

ShowError("CallDllMain");

return NULL;

}

return lpBaseAddress;

}

由於篇幅有限,對於如何修改重定位表、導入表以及遍歷導出表等操作在此就不詳細說明,讀者直接閱讀配套代碼即可,在配套代碼中均有詳細的註釋說明。

4.3.3 測試

直接內存加載運行TestDll.dll文件,若成功執行TestDll.dll入口處的彈窗代碼,彈窗提示則說明加載運行成功,如圖4-4所示。

4-4 TestDll.dll彈窗提示

4.3.4 小結

這個程序對於初學者來說,理解起來比較複雜。但是,只要熟悉PE結構,這個程序理解起來就會容易得多。對於重定位表、導入表,以及導出表部分的具體操作並沒有詳細講解。如果沒有了解PE結構,那麼理解起來會有些困難;如果瞭解了PE結構,那麼就很容易理解該部分知識。

可以通過暴力枚舉PE結構特徵頭的方法,來枚舉進程中加載的所有模塊,它與通過正常方法獲取到的模塊信息進行比對,從而判斷是否存在可疑的PE文件。

-------------------------------------------

說明:

),第一章送出三本書。同時第二章(,2.3節),第三章(3.1,3.2,3.3)每篇文章送出一本書。本篇文章送出三本書,也就是參與本節的活動,會增加你的中獎概率。文章不停,送書不止。在前四章更新完畢之後,會統一公佈中獎名單,並組織線上答疑活動,和本書作者親密交流。

  1. ------------------------------------------------------ 點擊下面鏈接,參與每篇文章的活動,提高中獎概率。今天是最後一天哦!

(內含送書福利)

相關文章