實戰 | 進程啟動技術的思路和研究
常規api創建進程
通過常用的api來創建進程是常規啟動進程的方式,最常用的幾個api有WinExec、ShellExecute、CreateProcess,我們一個一個來看一下
WinExec
首先是WinExec,這個api結構如下,這個api只能夠運行exe文件,算是比較局限
UINT WinExec( [in] LPCSTR lpCmdLine, // 命令行 [in] UINT uCmdShow // 顯示選項 );
實現代碼如下
BOOL winexec(char* szPath, UINT Cmd)
{
UINT ret = 0;
ret = ::WinExec(szPath,Cmd);
if (ret > 31)
{
return TRUE;
}
return FALSE;
}
ShellExecute
ShellExecute的功能是運行一個外部程序(或者是打開一個已注冊的文件、打開一個目錄、打印一個文件等等),并對外部程序有一定的控制
HINSTANCE ShellExecuteA( [in, optional] HWND hwnd, [in, optional] LPCSTR lpOperation, [in] LPCSTR lpFile, [in, optional] LPCSTR lpParameters, [in, optional] LPCSTR lpDirectory, [in] INT nShowCmd );
實現代碼如下
BOOL shellexecute(char* szPath, UINT Cmd)
{
HINSTANCE hIns = 0;
hIns = ::ShellExecute(NULL,NULL,szPath,NULL,NULL,Cmd);
if ((DWORD)hIns > 32)
{
return TRUE;
}
return FALSE;
}
CreateProcess
最常用的創建進程api,本質是調用了內核函數NtCreateProcess創建進程并用NtCreateThread創建一個線程
BOOL CreateProcessA( [in, optional] LPCSTR lpApplicationName, [in, out, optional] LPSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCSTR lpCurrentDirectory, [in] LPSTARTUPINFOA lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation );
實現代碼如下
char szCommandLine[]="notepad";
STARTUPINFO si={sizeof(si)};
PROCESS_INFORMATION pi;
si.dwFlags=STARTF_USESHOWWINDOW;//指定wShowWindow成員效
si.wShowWindow=TRUE;//此成員設為TRUE的話則顯示新建進程的主窗口
BOOL bRet=CreateProcess(
NULL,//不在此指定可執行文件的文件名
szCommandLine,//命令行參數
NULL,//默認進程安全性
NULL,//默認進程安全性
FALSE,//指定當前進程內句柄不可以被子進程繼承
CREATE_NEW_CONSOLE,//為新進程創建一個新的控制臺窗口
NULL,//使用本進程的環境變量
NULL,//使用本進程的驅動器和目錄
&si,
&pi);
if(bRet)
{
//不使用的句柄最好關掉
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
printf("新進程的ID號:%d",pi.dwProcessId);
printf("新進程的主線程ID號:%d",pi.dwThreadId);
}
getchar();
return 0;
這里著重說一下CreateProcess的實現過程
在Windows中,進程是不活動的,只是作為線程的容器,現代操作系統將線程作為最小調度單位,進程作為資源分配的最小單位。
所以,CreateProcess作為一個相對高層的函數,要先通過系統調用``NtCreateProcess()創建進程(容器),成功以后就立即通過系統調用NtCreateThread()創建其第一個線程。
第一階段:打開目標映像文件
對于32位exe映像,CreateProcess先打開其映像文件,在為其創建一個Section即文件映射區,將文件內容映射進來,前提是目標文件是一個合格的EXE文件(PE文件頭部檢測);
第二階段:創建內核中的進程對象
實際上就是創建以EPROCESS為核心的相關數據結構,這就是系統調用NtCreateProcess()要做的事情,主要包括:
①分配并設置EPROCESS數據結構;
②其他相關的數據結構的設置,如句柄表等等;
③為目標進程創建初始的地址空間;
④對EPROCESS進行初始化;
⑤將系統Dll映射到目標用戶空間,如ntdll.dll等
⑥設置目標進程的PEB;
⑦將其他需要映射到用戶空間,如與”當地語言支持“即NLS有關的數據結構;
⑧完成EPROCESS創建,將其掛入進程隊列并插入創建者的句柄表
第三階段:創建初始線程
前面說過,進程只是一個容器,干活兒是里面的線程,所以下一步就是創建目標進程的初始線程
與EPROCESS對應,線程的數據結構是ETHREAD,與進程環境塊PEB對應,線程也有線程環境塊TEB;
PEB在用戶空間的位置大致是固定的,在7ffd0000左右,PEB的下方就是TEB,進程有幾個線程就有幾個TEB,每個TEB占一個4KB的頁面;
這個階段是通過調用NtCreateThread()完成的,主要包括:
①創建和設置目標線程的ETHREAD數據結構,并處理好與EPROCESS的關系(例如進程塊中的線程計數等等)。
②在目標進程的用戶空間創建并設置目標線程的TEB。
③將目標線程在用戶空間的起始地址設置成指向Kernel32.dll中的BaseProcessStart()或BaseThreadStart(),前者用于進程中的第一個線程,后者用于隨后的線程。用戶程序在調用NtCreateThread()時也要提供一個用戶級的起始函數(地址), BaseProcessStart()和BaseThreadStart()在完成初始化時會調用這個起始函數。ETHREAD數據結構中有兩個成份,分別用來存放這兩個地址。
④調用KeInitThread設置目標線程的KTHREAD數據結構并為其分配堆棧和建立執行環境。特別地,將其上下文中的斷點(返回點)設置成指向內核中的一段程序KiThreadStartup,使得該線程一旦被調度運行時就從這里開始執行。
⑤系統中可能登記了一些每當創建線程時就應加以調用的“通知”函數,調用這些函數。
第四階段:通知windows子系統
每個進程在創建/退出的時候都要向windows子系統進程csrss.exe進程發出通知,因為它擔負著對windows所有進程的管理的責任,
注意,這里發出通知的是CreateProcess的調用者,不是新建出來的進程,因為它還沒有開始運行。
至此,CreateProcess的操作已經完成,但子進程中的線程卻尚未開始運行,它的運行還要經歷下面的第五和第六階段。
第五階段:啟動初始線程
新創建的線程未必是可以被立即調度運行的,因為用戶可能在創建時把標志位CREATE_ SUSPENDED設成了1;
如果那樣的話,就需要等待別的進程通過系統調用恢復其運行資格以后才可以被調度運行。否則現在已經可以被調度運行了。至于什么時候才會被調度運行,則就要看優先級等等條件了。
第六階段:用戶空間的初始化和Dll連接
DLL連接由ntdll.dll中的LdrInitializeThunk()在用戶空間完成。在此之前ntdll.dll與應用軟件尚未連接,但是已經被映射到了用戶空間(第二階段第⑤步)
函數LdrInitializeThunk()在映像中的位置是系統初始化時就預先確定并記錄在案的,所以在進入這個函數之前也不需要連接。
session0創建進程
Intel的CPU將特權級別分為4個級別:RING0,RING1,RING2,RING3。Windows只使用其中的兩個級別RING0和RING3,RING0只給操作系統用,RING3誰都能用。如果普通應用程序企圖執行RING0指令,則Windows會顯示“非法指令”錯誤信息。
ring0是指CPU的運行級別,ring0是最高級別,ring1次之,ring2更次之…… 拿Linux+x86來說, 操作系統(內核)的代碼運行在最高運行級別ring0上,可以使用特權指令,控制中斷、修改頁表、訪問設備等等。應用程序的代碼運行在最低運行級別上ring3上,不能做受控操作。如果要做,比如要訪問磁盤,寫文件,那就要通過執行系統調用(函數),執行系統調用的時候,CPU的運行級別會發生從ring3到ring0的切換,并跳轉到系統調用對應的內核代碼位置執行,這樣內核就為你完成了設備訪問,完成之后再從ring0返回ring3。這個過程也稱作用戶態和內核態的切換。
RING設計的初衷是將系統權限與程序分離出來,使之能夠讓OS更好的管理當前系統資源,也使得系統更加穩定。舉個RING權限的最簡單的例子:一個停止響應的應用程式,它運行在比RING0更低的指令環上,你不必大費周章的想著如何使系統回復運作,這期間,只需要啟動任務管理器便能輕松終止它,因為它運行在比程式更低的RING0指令環中,擁有更高的權限,可以直接影響到RING0以上運行的程序,當然有利就有弊,RING保證了系統穩定運行的同時,也產生了一些十分麻煩的問題。比如一些OS虛擬化技術,在處理RING指令環時便遇到了麻煩,系統是運行在RING0指令環上的,但是虛擬的OS畢竟也是一個系統,也需要與系統相匹配的權限。而RING0不允許出現多個OS同時運行在上面,最早的解決辦法便是使用虛擬機,把OS當成一個程序來運行。

我們知道一般用戶進程都在3環,而系統進程一般都在0環創建,那么我們可以嘗試突破session0的隔離來創建進程
思路
由于SESSION 0會話隔離,使得在系統服務進程內不能通過直接調用CreateProcess等函數創建進程,而是通過CreateProcessAsUser函數來創建。這樣,創建的進程才會顯示UI界面,與用戶進行交互。
首先,調用WTSGetActiveConsoleSessionId函數來獲取當前程序的活動會話ID,即Session Id。該函數的調用不需要任何參數,直接返回Session Id。根據Session Id繼續調用WTSQueryUserToken函數來檢索用戶令牌,并獲取對應的用戶令牌句柄。
然后,使用DuplicateTokenEx函數創建一個一個新令牌,并復制上述獲取的用戶令牌。設置新令牌的訪問權限問MAXIMUM_ALLOWED,表示獲取所有令牌權限。新訪問令牌的模擬級別為SecurityIdentification,而且令牌類型為TokenPrimary,表示新令牌是可以在CreateProcessAsUser函數中使用的主令牌。
最后,根據新令牌調用CreateEnvironmentBlock函數創建一個環境塊,用來傳遞給CreateProcessAsUser使用。在不需要使用進程環境塊后,可以通過調用DestroyEnvironmentBlock函數進行釋放。獲取環境塊之后,就可以調用CreateProcessAsUser來創建用戶桌面進程了。新令牌句柄作為用戶主令牌的句柄,指定創建進程的路徑,設置優先級和創建標志,設置STARTUPINFO結構信息,獲取PROCESS_INFORMATION結構信息。
函數
WTSGetActiveConsoleSessionId
檢索Session Id
DWORD WTSGetActiveConsoleSessionId(void);
WTSQueryUserToken
獲取由Session Id指定的登錄用戶的主訪問令牌
BOOL WTSQueryUserToken(
_In_ ULONG SessionId,
_Out_ PHANDLE phToken
);
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 );
CreateEnvironmentBlock
檢索指定用戶的環境變量
BOOL WINAPI CreateEnvironmentBlock(
_Out_ LPVOID *lpEnvironment,
_In_opt_ HANDLE hToken,
_In_ BOOL bInherit
);
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
);
實現
首先使用WTSGetActiveConsoleSessionId來獲取session ID
::WTSGetActiveConsoleSessionId();
使用WTSQueryUserToken獲取當前會話的用戶令牌
::WTSQueryUserToken(dwSessionID, &hToken)
復制令牌
::DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hDuplicatedToken)
然后使用CreateEnvironmentBlock創建用戶的session環境
::CreateEnvironmentBlock(&lpEnvironment,hDuplicatedToken, FALSE)
再在復制的會話下面執行創建進程的操作
::CreateProcessAsUser(hDuplicatedToken, lpszFileName, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &si, &pi)
完整代碼如下
BOOL CreateUserProcess(char* lpszFileName)
{
BOOL bRet = TRUE;
DWORD dwSessionID = 0;
HANDLE hToken = NULL;
HANDLE hDuplicatedToken = NULL;
LPVOID lpEnvironment = NULL;
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
do
{
// 獲得當前Session ID
dwSessionID = ::WTSGetActiveConsoleSessionId();
// 獲得當前Session的用戶令牌
if (FALSE == ::WTSQueryUserToken(dwSessionID, &hToken))
{
printf("[!] WTSQueryUserToken failed");
bRet = FALSE;
break;
}
// 復制令牌
if (FALSE == ::DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,
SecurityIdentification, TokenPrimary, &hDuplicatedToken))
{
printf("[!] WTSQueryUserToken failed");
bRet = FALSE;
break;
}
// 創建用戶Session環境
if (FALSE == ::CreateEnvironmentBlock(&lpEnvironment,
hDuplicatedToken, FALSE))
{
printf("[!] WTSQueryUserToken failed");
bRet = FALSE;
break;
}
// 在復制的用戶Session下執行應用程序,創建進程
if (FALSE == ::CreateProcessAsUser(hDuplicatedToken,
(LPCWSTR)lpszFileName, NULL, NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
lpEnvironment, NULL, &si, &pi))
{
printf("[!] WTSQueryUserToken failed");
bRet = FALSE;
break;
}
} while (FALSE);
// 關閉句柄, 釋放資源
if (lpEnvironment)
{
::DestroyEnvironmentBlock(lpEnvironment);
}
if (hDuplicatedToken)
{
::CloseHandle(hDuplicatedToken);
}
if (hToken)
{
::CloseHandle(hToken);
}
return bRet;
}
因為要實現session0隔離,這里需要添加一個系統服務進程,需要調用StartServiceCtrlDispatcher來設置入口點函數,這里就不展開說了,看一下實現的效果,這里將session0exe.exe注冊到系統服務打開nc.exe到session1

內存加載運行
將資源加載到內存,然后把DLL文件按照映像對齊大小映射到內存中,切不可直接將DLL文件數據存儲到內存中。
因為根據PE結構的基礎知識可知,PE文件有兩個對齊字段,一個是映像對齊,另一個是文件對齊大。其中,映像對齊大小是PE文件加載到內存中所用的對齊大小,而文件對齊大小是PE文件存儲在本地磁盤所用的對齊大小。一般文件對齊大小會比映像對齊大小要小,這樣文件會變小,以此節省磁盤空間。
然而,成功映射內存數據之后,在DLL程序中會存在硬編碼數據,硬編碼都是以默認的加載基址作為基址來計算的。由于DLL可以任意加載到其他進程空間中,所以DLL的加載基址并非固定不變。當改變加載基址的時候,硬編碼也要隨之改變,這樣DLL程序才會計算正確。
如何知道硬編碼的位置?答案就藏在PE結構的重定位表中,重定位表記錄的就是程序中所有需要修改的硬編碼的相對偏移位置。
根據重定位表修改硬編碼數據后,這只是完成了一半的工作。DLL作為一個程序,自然也會調用其他庫函數,例如MessageBox。
那么DLL如何知道MessageBox函數的地址呢?它只有獲取正確的調用函數地址后,方可正確調用函數。PE結構使用導入表來記錄PE程序中所有引用的函數及其函數地址。在DLL映射到內存之后,需要根據導入表中的導入模塊和函數名稱來獲取調用函數的地址。若想從導入模塊中獲取導出函數的地址,最簡單的方式是通過GetProcAddress函數來獲取。
但是為了避免調用敏感的WIN32 API函數而被殺軟攔截檢測,采用直接遍歷PE結構導出表的方式來獲取導出函數地址。
實現
這里有一些其他的函數,例如修復IAT表、修復重定位表的代碼就不細說了,這里需要有一定的基礎知識才能夠實現,主要是說一下在進程中的操作
首先獲取大小
DWORD dwSizeOfImage = GetSizeOfImage(lpData);
使用VirutalAlloc分配內存
::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
然后使用RtlZeroMemory清空空間數據
RtlZeroMemory(lpBaseAddress, dwSizeOfImage);
對dll數據進行FileBuffer -> ImageBuffer的轉換
MmMapFile(lpData, lpBaseAddress);
修復文件的重定位表和IAT表
DoRelocationTable(lpBaseAddress); DoImportTable(lpBaseAddress);
修改函數的ImageBase
SetImageBase(lpBaseAddress);
調用dll的入口函數
CallDllMain(lpBaseAddress,IsExe)
完整代碼如下
LPVOID LoadLibrary(LPVOID lpData,BOOL IsExe)
{
LPVOID lpBaseAddress = NULL;
// 獲取映像大小
DWORD dwSizeOfImage = GetSizeOfImage(lpData);
// 在進程中申請一個可讀、可寫、可執行的內存塊
lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == lpBaseAddress)
{
printf("[!] VirtualAlloc failed");
return NULL;
}
::RtlZeroMemory(lpBaseAddress, dwSizeOfImage);
if (FALSE == MmMapFile(lpData, lpBaseAddress))
{
printf("[!] MmMapFile failed");
return NULL;
}
// 修改重定位表
if (FALSE == DoRelocationTable(lpBaseAddress))
{
printf("[!] DoRelocationTable failed");
return NULL;
}
// 修改導入表
if (FALSE == DoImportTable(lpBaseAddress))
{
printf("[!] DoImportTable failed");
return NULL;
}
// 修改PE文件的加載基址
if (FALSE == SetImageBase(lpBaseAddress))
{
printf("[!] SetImageBase failed");
return NULL;
}
// 調用DLL的入口函數DllMain
if (FALSE == CallDllMain(lpBaseAddress,IsExe))
{
printf("[!] CallDllMain failed");
return NULL;
}
return lpBaseAddress;
}
然后新建一個dll,寫入成功即彈窗

內存加載運行成功
