建立一個程式
總述
如圖,建立一個程式主要分為兩部分,使用者態部分和核心部分。
既然我們想看看一個程式是怎麼被建立的,那我們就用 WinDbg 來看看從使用者態到核心態都呼叫了什麼:
第一步:我們先看看 nt 下有哪些方法跟建立程式相關的
0: kd> x nt!*CreateProcess*
fffff802`55d8a218 nt!PspSetCreateProcessNotifyRoutine (void)
fffff802`55cd9714 nt!ExpWnfCreateProcessContext (void)
fffff802`55dd9a2f nt!PspCreateProcess$filt$0 (void)
fffff802`55be24f4 nt!PspDeleteCreateProcessContext (void)
fffff802`55c40ed0 nt!MmCreateProcessAddressSpace (void)
fffff802`55dbd430 nt!PspCreateProcess (void)
fffff802`5594fb10 nt!ViCreateProcessCallback (void)
fffff802`55fdaaa4 nt!ViCreateProcessCallbackInternal (ViCreateProcessCallbackInternal)
fffff802`55f04550 nt!NtCreateProcessEx (NtCreateProcessEx)
fffff802`55fd1ce0 nt!VerifierPsSetCreateProcessNotifyRoutineEx (VerifierPsSetCreateProcessNotifyRoutineEx)
fffff802`559f4bf0 nt!ZwCreateProcessEx (ZwCreateProcessEx)
fffff802`56349360 nt!pXdvPsSetCreateProcessNotifyRoutineEx = <no type information>
fffff802`55cfd12c nt!PspValidateCreateProcessProtection (PspValidateCreateProcessProtection)
fffff802`55d89ea0 nt!PsSetCreateProcessNotifyRoutineEx (PsSetCreateProcessNotifyRoutineEx)
fffff802`5632e9d4 nt!PspCreateProcessNotifyRoutineCount = <no type information>
fffff802`55d89f00 nt!PsSetCreateProcessNotifyRoutineEx2 (PsSetCreateProcessNotifyRoutineEx2)
fffff802`5632e9d8 nt!PspCreateProcessNotifyRoutineExCount = <no type information>
fffff802`55d8a050 nt!PsSetCreateProcessNotifyRoutine (PsSetCreateProcessNotifyRoutine)
fffff802`55ed2e70 nt!MiCreateProcessDefaultAweInfo (MiCreateProcessDefaultAweInfo)
fffff802`55be0d1c nt!PspBuildCreateProcessContext (PspBuildCreateProcessContext)
fffff802`559f5970 nt!ZwCreateProcess (ZwCreateProcess)
fffff802`562ec260 nt!PspCreateProcessNotifyRoutine = <no type information>
fffff802`55fd1cc0 nt!VerifierPsSetCreateProcessNotifyRoutine (VerifierPsSetCreateProcessNotifyRoutine)
fffff802`56349378 nt!pXdvPsSetCreateProcessNotifyRoutine = <no type information>
fffff802`55f044c0 nt!NtCreateProcess (NtCreateProcess)
第二步:我們選擇 nt!MmCreateProcessAddressSpace 打上斷點(不要問我為啥選這個,實在不會選,就直接 bm nt!CreateProcess)
0: kd> bu nt!MmCreateProcessAddressSpace
Breakpoint 2 hit
nt!MmCreateProcessAddressSpace:
fffff802`55c40ed0 488bc4 mov rax,rsp
0: kd> k
# Child-SP RetAddr Call Site
00 ffff928c`4e5a7b48 fffff802`55d08608 nt!MmCreateProcessAddressSpace
01 ffff928c`4e5a7b50 fffff802`55cff75a nt!PspAllocateProcess+0x13ec
02 ffff928c`4e5a82c0 fffff802`55a096b5 nt!NtCreateUserProcess+0xa1a
03 ffff928c`4e5a8a90 00007ff8`cfc2e634 nt!KiSystemServiceCopyEnd+0x25
04 00000000`02aac598 00007ff8`cd818e73 ntdll!NtCreateUserProcess+0x14
05 00000000`02aac5a0 00007ff8`cd8171a6 KERNELBASE!CreateProcessInternalW+0xfe3
06 00000000`02aadb70 00007ff8`ced4cbb4 KERNELBASE!CreateProcessW+0x66
07 00000000`02aadbe0 00007ff8`cb56152d KERNEL32!CreateProcessWStub+0x54
08 00000000`02aadc40 00007ff8`cb4f6722 windows_storage!CInvokeCreateProcessVerb::CallCreateProcess+0x2cd
09 00000000`02aadef0 00007ff8`cb55a75c windows_storage!CInvokeCreateProcessVerb::_PrepareAndCallCreateProcess+0x2d6
0a 00000000`02aadf70 00007ff8`cb55a583 windows_storage!CInvokeCreateProcessVerb::_TryCreateProcess+0x3c
0b 00000000`02aadfa0 00007ff8`cb55a46d windows_storage!CInvokeCreateProcessVerb::Launch+0xef
0c 00000000`02aae040 00007ff8`cb599dc4 windows_storage!CInvokeCreateProcessVerb::Execute+0x5d
0d 00000000`02aae080 00007ff8`cb481d87 windows_storage!CBindAndInvokeStaticVerb::InitAndCallExecute+0x214
0e 00000000`02aae100 00007ff8`cb4f5787 windows_storage!CBindAndInvokeStaticVerb::TryCreateProcessDdeHandler+0x63
0f 00000000`02aae180 00007ff8`cb54586d windows_storage!CBindAndInvokeStaticVerb::Execute+0x1e7
10 00000000`02aae4a0 00007ff8`cb545785 windows_storage!RegDataDrivenCommand::_TryInvokeAssociation+0xad
11 00000000`02aae500 00007ff8`ce152b22 windows_storage!RegDataDrivenCommand::_Invoke+0x141
12 00000000`02aae570 00007ff8`ce1529da SHELL32!CRegistryVerbsContextMenu::_Execute+0xce
13 00000000`02aae5e0 00007ff8`ce15630c SHELL32!CRegistryVerbsContextMenu::InvokeCommand+0xaa
14 00000000`02aae8e0 00007ff8`ce15618d SHELL32!HDXA_LetHandlerProcessCommandEx+0x10c
15 00000000`02aae9f0 00007ff8`cb93be08 SHELL32!CDefFolderMenu::InvokeCommand+0x13d
16 00000000`02aaed50 00007ff8`cb93c7b6 windows_storage!CShellLink::_InvokeDirect+0x1d0
17 00000000`02aaf070 00007ff8`cb93945a windows_storage!CShellLink::_ResolveAndInvoke+0x202
18 00000000`02aaf230 00007ff8`ce15630c windows_storage!CShellLink::InvokeCommand+0x1aa
19 00000000`02aaf310 00007ff8`ce15618d SHELL32!HDXA_LetHandlerProcessCommandEx+0x10c
1a 00000000`02aaf420 00007ff8`ce3709d5 SHELL32!CDefFolderMenu::InvokeCommand+0x13d
1b 00000000`02aaf780 00007ff8`ce6244f9 SHELL32!SHInvokeCommandOnContextMenu2+0x1f5
1c 00000000`02aaf9c0 00007ff8`ceeec3f9 SHELL32!s_DoInvokeVerb+0xc9
1d 00000000`02aafa30 00007ff8`ced47034 shcore!_WrapperThreadProc+0xe9
1e 00000000`02aafb10 00007ff8`cfbe2651 KERNEL32!BaseThreadInitThunk+0x14
1f 00000000`02aafb40 00000000`00000000 ntdll!RtlUserThreadStart+0x21
如上 Windbg 輸出的結果所示,正是描述了從使用者態的 CreateProcess → 核心態的 NtCreateUserProcess. 其他鏈路,比如 CreateProcessAsTokenW 我們也可以驗證下,這裡就不做贅述。
使用者態部分,包含一些我們常用到的方法:CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, CreateProcessAsTokenW.
而核心部分,則都是通過 NT 下的 NtCreateUserProcess 來進行建立。
建立程式流程
建立一個程式,主要以下7個步驟。
步驟1:轉換、驗證引數和標誌
這一步驟主要是將從使用者態引數到核心態引數的一個轉換,同時新增必要的標識。
其中主要包含以下部分:
- 優先順序的確定
- Native 屬性和 Win32 屬性的對映
- 對現代應用(modern application)的特定標識(PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME),方便後續特殊處理
- Debug 和 Error 的預設
- 確定特定的桌面環境(指程式需要建立到哪個特定的虛擬桌面)
Windows 的虛擬桌面其實只有一個 Desktop 物件,使用 desktop.exe 可以真正的建立多個虛擬桌面。
- 將引數做轉換。(比如 c:\temp\a.exe 可能轉換成 \device\harddiskvolume1\temp\a.exe)
做完這些工作,建立程式的使用者態的初始化基本就結束了。接下來就會嘗試呼叫核心態的 NtCreateUserProcess 來建立程式。
步驟2:開啟要執行的映象
這個部分已經切換到核心模式執行了,主要目的就是確定要怎麼開啟映象。
主要包含以下部分:
- 如上圖,根據要開啟的映象檔案確認需要真正執行的程式。(比如,當檔案是一個 .cmd 檔案時,真正需要執行的是 cmd.exe 這個程式,那就需要重新回到步驟一 CreateProcessInternalW )
- 如果程式是現代應用,則需要確定他的證書,確保它是可以被執行的。(比如非商店應用在Windows 設定不執行旁載入的情況下是不能被執行的)
- 如果程式是 Trustlet,還需要新增特定的標識
- 會嘗試去開啟 Windows exe 檔案。首先建立 section object,然後判斷其是否可以被開啟。
- 然後會在 Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 下尋找特定的 option。例如要開啟的檔案是SppExtComObj.exe,就會找到確認 Image File Execution Options 下是否存在 SppExtComObj.exe 子項,如果存在 PspAllocateProcess 就會再去找是否存在 debugger 的key,如果這個key存在,就會將 debugger 的值替換 SppExtComObj.exe,並且重新執行步驟一。
這就是映象劫持(IFEO)的原理了,通過自行在登錄檔中建立子項,就可以實現想開啟A程式,實際開啟B程式
- 對於非 Windows exe 檔案來說,有以下這些行為:
至此,Windows 就已經可以開啟一個可執行檔案並且建立了部分物件,並對映到新的程式的地址空間了。
步驟3:建立 Windows 執行體程式物件
這個部分主要是通過 PspAllocateProcess 來建立 Windows 執行體物件(也就是核心中描述程式的物件)。主要分為以下幾個部分:
- 設定 EPROCESS 物件。初始化或者從父程式繼承屬性,同時會根據 IFEO 的各種 key 來確定對應的值(比如:UseLargePages、PerfOptions、IoPriority、PagePriority、CpuPriorityClass、WorkingSetLimitInKB)
- 建立初始程式地址空間。
- 建立核心程式結構,也就是是初始化 KPROCESS。
- 完成程式地址空間的設定。(這塊需要有一些記憶體管理上的知識,有點迷糊,後面再來補上 @frend guo )
- 配置 PEB。
- 完成執行體程式物件的配置。
到此,Windows 執行程式物件已經建立完成了。接下來就該建立第一個執行緒了。
步驟4:建立初始執行緒的執行緒棧和上下文
這個部分主要是建立程式中的第0個執行緒,並且將其棧和上下文初始化完成。
由於其是在核心中直接建立的執行緒,所以跟使用者模式下建立執行緒會有些不一樣。這裡主要分為 PspAllocateThread 和 PspInsertThread 兩個部分來分析。
對於 PspAllocateThread 主要包含以下工作:
- 阻止 WOW64 程式的 UMS,還阻止了使用者模式下 System 程式中建立執行緒的呼叫
- 建立執行體執行緒物件並初始化
- LPC、IO管理和執行體用到的各種列表都將初始化
- 執行緒建立時間、TID都將被建立
- 建立執行緒的棧和上下文
- 為新執行緒分配 TEB
- 配置 ETHREAD、KTHREAD(通過KeInitThread)
對於 PspInsertThread,主要包含以下工作:
- 跟據屬性做一些執行緒的初始化工作,然後再插入到程式的執行緒列表裡。比如初始化執行緒的首選處理器(thread ideal processor)、執行緒組親和性(thread group affinity)、初始化安全執行緒物件(如果是IUM下)、排程設定、動態優先順序、執行緒量子(thread quantum)。
- 將執行緒物件插入到程式控制程式碼表(process handle table)
- 如果是程式的第一個執行緒被建立,所有程式註冊的回撥都會被呼叫。
- 會呼叫 KeReadyThread 回應執行體,已經處理準備好的狀態。
到這裡,已經建立了必要的程式和執行緒物件了。
步驟5:執行 Windows 子系統特定的初始化
這個步驟主要是做一些使用者模式下的檢查和初始化。也是 Windows 子系統登記此程式的過程。
- Windows 會做一些檢查來確保 Windows 是允許該程式執行的。比如校驗映象版本、確保 Windows 認證是否阻止此程式(策略)以及在一些特定 Windows 版本中,是否匯入了系統不允許匯入的 DLL 或者 API。
- 如果軟體策略有約束,則會為此程式建立一個約束的 token,並將其儲存到 PEB 中
- CreateProcessInternalW 會呼叫一些內部方法來獲取系統的 SxS 資訊
- 根據收集到的要傳送到 Csrss 的資訊構造到 Windows 子系統的訊息。
- 在收到訊息後,Windows 子系統將執行以下步驟:
- CsrCreateProcess 會為程式和執行緒複製控制程式碼。程式和執行緒的使用計數(usage count)將會從1增加到2
- 分配 Csrss process structure (CSR_PROCESS)
- Csrss 執行緒結構(CSR_THREAD)將會被分配並初始化
- 通過 CsrCreateThread 將執行緒插入到程式的執行緒列表中
- 會話中的程式計數會遞增
- 設定程式的關閉優先順序(The process shutdown level)為 0x280。也就是程式預設的等級。
- 新建立的 csrss 程式結構將會被插入到 Windows 子系統範疇的程式列表中。
到此,程式、執行緒的環境建好,需要使用的資源也已經分配好了,Windows 子系統也知道並登記此程式和執行緒。於是就可以開始執行初始執行緒了。
步驟6:開始執行初始執行緒
這個階段,除非呼叫者指定 CREATE_SUSPENDED,否則,初始化執行緒都將恢復執行,並開始執行後續的程式初始化工作。此時的初始化工作已經切換到新程式中了。
步驟7:在新程式的上下文中執行程式的初始化
新的執行緒開始執行,將在核心模式執行 KiStartUserThread,它將執行緒的 IRQL 從 DPC 降低到 APC,然後再呼叫系統初始化執行緒例程 PspUserThreadStartup,它將執行以下步驟:
- 安裝異常鏈。
- 將 IRQL 降低至 PASSIVE_LEVEL(也就是0)
- 禁用執行時交換主程式 token 的能力
- 根據核心模式下的資料結構(KTHREAD) 設定 TEB 中的 local ID 和執行緒的首選處理器
- 呼叫 DbgkCreateThread 來檢查新的程式是否向映象傳送訊息。(用於 load dll)
- 然後繼續做一些列的檢查。
- 接下來就會切換到使用者模式下,回到 RtlUserThreadStart 中。
到這裡,程式的建立就結束了,將執行 Image 中的入口方法,進到程式的上下文中去。
結束一個程式
結束程式主要分為兩種方式:主動結束(ExitProcess)和被動停止(TerminateProcess)。
ExitProcess 和 TerminateProcess 呼叫的執行體中的 NtTerminalProcess。它主要執行以下邏輯:
- 輪詢程式中所有執行緒,如果不是當前執行緒,呼叫 PspTerminateThreadByPointer 結束(需要等待返回)。
- 如果需要終止的是當前程式,則判斷如果是當前執行緒,呼叫 PspTerminateThreadByPointer 結束,不等待返回。
- 最後再清理控制程式碼表和解引用物件。
總結
建立一個繼承需要考慮非常多的點,從使用者模式到核心模式的轉換,核心物件的構建再回到使用者模式與子系統的通訊。這個部分牽涉到的內容非常多,也非常值得詳細研究。先了解大框架,再來細化其中的每個點。