Direct-NtCreateUserProcess
0x00 Prologue
最近以来, 使用Native API绕过AV/EDR的讨论似乎总保持着一定的热度, HellsGate后的各种衍生Gate, Syswhispers也从2进化到了3。这类技术的本质上比较相似, 动态获取目标功能号后, 再调用syscall
指令从而native call。但发现一个现象, 研究者们似乎更倾向于使用NtCreateThread
而非NtCreateProcess
来验证他们的代码有效性。于是, 今年年初开始, 我开始针对进程创建的native call进行一系列研究。
进程创建的native call也并非崭新的话题, 需要4个native API即可满足需求:
NtCreateFile
NtCreateSection
NtCreateProcess(Ex)
NtCreateThread(Ex)
BlackHat 2017中的Process Doppelganging技术就是基于此诞生的。
而时至今日, NtCreateProcess
在Win10时代只在系统进程创建时被调用, 用户进程的创建使用NtCreateUserProcess
。这就是本文的主题, 围绕NtCreateUserProcess
创建一个正常用户进程。
0x01 最简化实现
以下研究基于Windows10 21H2 (19044.1415)。
让我们从原理开始, 先看看Windows下创建用户进程的各个阶段 (Windows Internals 7th):
结合kernelbase和ntdll, 可以得出:
Stage1,5均在kernelbase!CreateProcessInternalW
中实现。而我们关注的NtCreateUserProcess
也是从CreateProcessInternalW
中调用的:
默认情况下, ThreadFlags
被赋值为1, 代表创建后线程暂停。
借助processhacker提供的头文件, NtCreateUserProcess
的原型为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateUserProcess(
_Out_ PHANDLE ProcessHandle,
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK ProcessDesiredAccess,
_In_ ACCESS_MASK ThreadDesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ProcessObjectAttributes,
_In_opt_ POBJECT_ATTRIBUTES ThreadObjectAttributes,
_In_ ULONG ProcessFlags, / / PROCESS_CREATE_FLAGS_ *
_In_ ULONG ThreadFlags, / / THREAD_CREATE_FLAGS_ *
_In_opt_ PVOID ProcessParameters, / / PRTL_USER_PROCESS_PARAMETERS
_Inout_ PPS_CREATE_INFO CreateInfo,
_In_opt_ PPS_ATTRIBUTE_LIST AttributeList
);
|
这里我们关注最后3个参数:
- ProcessParameters
- CreateInfo
- AttributeList
ProcessParameters
该参数的构建, 需要依靠另一个Native API: RtlCreateProcessParametersEx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | NTSYSAPI
NTSTATUS
NTAPI
RtlCreateProcessParametersEx(
_Out_ PRTL_USER_PROCESS_PARAMETERS * pProcessParameters,
_In_ PUNICODE_STRING ImagePathName,
_In_opt_ PUNICODE_STRING DllPath,
_In_opt_ PUNICODE_STRING CurrentDirectory,
_In_opt_ PUNICODE_STRING CommandLine,
_In_opt_ PVOID Environment,
_In_opt_ PUNICODE_STRING WindowTitle,
_In_opt_ PUNICODE_STRING DesktopInfo,
_In_opt_ PUNICODE_STRING ShellInfo,
_In_opt_ PUNICODE_STRING RuntimeData,
_In_ ULONG Flags / / pass RTL_USER_PROC_PARAMS_NORMALIZED to keep parameters normalized
);
|
其中参数2ImagePathName
为目标进程的Imag路径, 传入时该字符串必须以\\??\\
为前缀。
函数返回值保存在参数1pProcessParameters
。子进程的PEB的字段ProcessParameters
保存此信息。
函数用法与在NtCreateProcess
时期相同, 直接使用即可。
CreateInfo
结构体信息来自processhacker:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | typedef struct _PS_CREATE_INFO
{
SIZE_T Size;
PS_CREATE_STATE State;
union
{
/ / PsCreateInitialState
struct
{
union
{
ULONG InitFlags;
struct
{
UCHAR WriteOutputOnExit : 1 ;
UCHAR DetectManifest : 1 ;
UCHAR IFEOSkipDebugger : 1 ;
UCHAR IFEODoNotPropagateKeyState : 1 ;
UCHAR SpareBits1 : 4 ;
UCHAR SpareBits2 : 8 ;
USHORT ProhibitedImageCharacteristics : 16 ;
};
};
ACCESS_MASK AdditionalFileAccess;
} InitState;
/ / PsCreateFailOnSectionCreate
struct
{
HANDLE FileHandle;
} FailSection;
/ / PsCreateFailExeFormat
struct
{
USHORT DllCharacteristics;
} ExeFormat;
/ / PsCreateFailExeName
struct
{
HANDLE IFEOKey;
} ExeName;
/ / PsCreateSuccess
struct
{
union
{
ULONG OutputFlags;
struct
{
UCHAR ProtectedProcess : 1 ;
UCHAR AddressSpaceOverride : 1 ;
UCHAR DevOverrideEnabled : 1 ; / / from Image File Execution Options
UCHAR ManifestDetected : 1 ;
UCHAR ProtectedProcessLight : 1 ;
UCHAR SpareBits1 : 3 ;
UCHAR SpareBits2 : 8 ;
USHORT SpareBits3 : 16 ;
};
};
HANDLE FileHandle;
HANDLE SectionHandle;
ULONGLONG UserProcessParametersNative;
ULONG UserProcessParametersWow64;
ULONG CurrentParameterFlags;
ULONGLONG PebAddressNative;
ULONG PebAddressWow64;
ULONGLONG ManifestAddress;
ULONG ManifestSize;
} SuccessState;
};
} PS_CREATE_INFO, * PPS_CREATE_INFO;
|
这是个大有用处的结构体, 由状态, 结构体长度, 以及与状态对应union组成, 有2个状态是我们需要的:
状态 |
值 |
说明 |
InitState |
0 |
调用`NtCreateUserProcess 时作为参数, 应设置为该状态 |
SuccessState |
6 |
调用`NtCreateUserProcess 后, 成功时为该状态 |
初始化, 最简化的情况下, 只需要填入状态和大小即可:
1 2 | createInfo.State = PsCreateInitialState;
createInfo.Size = sizeof(PS_CREATE_INFO);
|
成功状态的相关信息见下文。
AttributeList
属性列表, 结构体信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | typedef struct _PS_ATTRIBUTE
{
ULONG_PTR Attribute;
SIZE_T Size;
union
{
ULONG_PTR Value;
PVOID ValuePtr;
};
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _PS_ATTRIBUTE_LIST
{
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[ 1 ];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
|
使用时, 根据TotalLength
手动调整Attributes
数组的大小。每个PS_ATTRIBUTE
在x64下大小为0x20, 其中字段Attribute
由宏PsAttributeValue
提供:
1 2 3 4 5 | (((Number) & PS_ATTRIBUTE_NUMBER_MASK) | \
((Thread) ? PS_ATTRIBUTE_THREAD : 0 ) | \
(( Input ) ? PS_ATTRIBUTE_INPUT : 0 ) | \
((Additive) ? PS_ATTRIBUTE_ADDITIVE : 0 ))
|
并且, processhacker提供了现成的定义。对比正常进程创建时调用的NtCreateUserProcess
, 最简化情况下我们仅需要:
1 2 | PsAttributeValue(PsAttributeImageInfo, FALSE, FALSE, FALSE)
|
其对应的PS_ATTRIBUTE
的字段ValuePtr
, 为目标进程的image路径, 和RtlCreateProcessParametersEx
中使用的一样。
构建好参数后, 我们可以调用NtCreateUserProcess
了! 注意, 如果flag设置为线程暂停, 还需要调用NtResumeThread
进程才可以正常跑起来。
成功调用后, 如上文所述, 可以检查CreateInfo
的state
字段是否为6, 并按照其字段, 输出我们感兴趣的信息。
如果你旨在寻找一种手动调用NtCreateUserProcess
以创建进程的方法, 成功了, 我们的故事就到此为止了。祝你享受一个美好的周末。
不过, 仍有一些奇怪的地方。正如本节一开始所述, NtCreateUserProcess
调用后可以视为阶段4的结束, 但NtResumeThread
视作阶段6的开始, 中间阶段5的缺失, 会不会导致其他的问题呢。
我们创建子进程notepad.exe
:
噢, "不幸"的事情发生了。
0x02 寻找错误原因
版本差异
我们以notepad.exe为切入点, 尝试找到一种解决方案以正常创建进程。
调试方法, 在NtCreateUserProcess
调用后, 在NtResumeThread
之前, 附加到目标进程。断在LdrIntializeThunk
, 由于Parallel Loading技术会断下多次。
直接f9跑起来, 发现触发异常:
根据日志, 异常发生在comctl32.dll
的加载中。查看notepad.exe的导出表,
而加载路径中的comctl32.dll
, 导出表确实没有序号345 (0x159):
与可以正常创建的notepad.exe对比, 正常加载的comctl32.dll
存在该导出函数。
二者对比, 发现路径上存在差异:
- 不正常:
WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_5.82.19041.1110_none_792d1c772443f647
- 正常:
WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.19041.1110_none_60b5254171f9507e
明显是版本不匹配, 一个5.82
一个6.0
。推测存在一种机制, 被创建的进程有能力指定加载DLL的版本。而路径中的WinSxS
已经提示与SxS重定向机制相关。
Fusion/Sxs redirection
简单来讲, CreateProcessInternalsW
在阶段5的工作之一, 先要收集SxS信息, 例如清单文件(Manifest), 之后构建一个包含SxS数据的信息, 发送给Csrss以通知Windows子进程实施SxS重定向机制。有关SxS重定向的原理性描述, 参考Windows Internals 7th, P162。
以直观的角度, 查看notepad.exe的清单文件, 显示依赖的comctl32.dll
版本为6.0.0.0
而默认加载的Windows\System32
下的comctl32.dll
文件, 版本为5.82
:
问题的答案呼之欲出, 正是由于阶段5, 缺少通知Windows子系统csrss的信息, 导致SxS重定向机制没有生效。
0x03 通知Windows子系统
CsrClientCallServer 和 BASE_API_MSG
参考Windows Internals 7th, 进程创建流程中的阶段5, 主要实现Windows子系统相关的初始化操作, 分为3个步骤:
- client对参数检查并构造信息, 将该信息发送给Windows子系统
- Windows子系统(csrss)根据信息判断是否合法, 并实施对应的初始化操作
- client后创建阶段, 包含进程参数更新等操作
这3个步骤, 主要围绕Native API: CsrClientCallServer
, 以及它的第一个参数, 结构体BASE_API_MSG
。
思路是这样的, 假设CsrClientCallServer
是唯一与Windows子系统通信的函数, 那么我们可以认为其参数涵盖了所有进程创建必备的信息。这样, 只要分析出正常进程时其参数各个字段的信息, 我们就有机会自行构造该参数, 自行告知Windows子系统, 使其相信我们是合法的。
我参考了以下资料:
- Geoff Chappel
- ReactOS
- leecher1337/ntvdmx64
CsrClientCallServer
的函数原型:
1 2 3 4 5 6 7 | NTSTATUS
CsrClientCallServer(
PBASE_API_MSG ApiMessage,
PVOID CaptureBuffer,
CSR_API_NUMBER ApiNumber,
ULONG DataLength
);
|
而有关BASE_API_MSG
结构体的相关信息较为过时, 经过亿点点逆向, 最终可以得出以下结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | typedef struct {
BYTE dummy[ 0x28 ];
} PORT_MESSAGE;
typedef struct _CSR_CAPTURE_HEADER {
ULONG Length;
PVOID RelatedCaptureBuffer; / / real: PCSR_CAPTURE_HEADER
ULONG CountMessagePointers;
PCHAR FreeSpace;
ULONG_PTR MessagePointerOffsets[ 1 ]; / / Offsets within CSR_API_MSG of pointers
} CSR_CAPTURE_HEADER, * PCSR_CAPTURE_HEADER;
typedef ULONG CSR_API_NUMBER;
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
typedef struct
{
ULONG_PTR UniqueProcess;
ULONG_PTR UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
__declspec(align( 8 ))
typedef struct
{
BYTE byte0; / / + 00
BYTE byte1; / / + 01
BYTE byte2; / / + 02
BYTE byte3; / / + 02
ULONG64 DUMMY; / / + 08
ULONG_PTR ManifestAddress; / / + 10
ULONG64 ManifestSize; / / + 18
HANDLE SectionHandle; / / + 20
ULONG64 Offset; / / + 28
ULONG_PTR Size; / / + 30
} BASE_SXS_STREAM; / / 0x38
typedef struct
{
ULONG Flags; / / + 00
ULONG ProcessParameterFlags; / / + 04
HANDLE FileHandle; / / + 08
UNICODE_STRING FileName1; / / + 10
UNICODE_STRING FileName2; / / + 20
BYTE Field30[ 0x10 ]; / / + 30
BASE_SXS_STREAM PolicyStream; / / + 40
UNICODE_STRING AssemblyName; / / + 78
UNICODE_STRING FileName3; / / + 88
BYTE Field98[ 0x10 ]; / / + 98
UNICODE_STRING FileName4; / / + a8
BYTE OtherFileds[ 0x110 ]; / / + b8
} BASE_SXS_CREATEPROCESS_MSG; / / 0x1C8
__declspec(align( 8 ))
typedef struct {
HANDLE ProcessHandle; / / + 00
HANDLE ThreadHandle; / / + 08
CLIENT_ID ClientId; / / + 10
ULONG CreationFlags; / / + 20
ULONG VdmBinaryType; / / + 24
ULONG VdmTask; / / + 28
HANDLE hVDM; / / + 30
BASE_SXS_CREATEPROCESS_MSG Sxs; / / + 38
ULONG64 PebAddressNative; / / + 200
ULONG_PTR PebAddressWow64; / / + 208
USHORT ProcessorArchitecture; / / + 210
} BASE_CREATEPROCESS_MSG;
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
__declspec(align( 8 ))
typedef struct {
PORT_MESSAGE h;
PCSR_CAPTURE_HEADER CaptureBuffer; / / 0x28
CSR_API_NUMBER ApiNumber; / / 0x30
ULONG ReturnValue; / / 0x34
ULONG64 Reserved; / / 0x38
BASE_CREATEPROCESS_MSG CreateProcessMSG; / / 0x40
} BASE_API_MSG, * PBASE_API_MSG;
|
伪造这样的结构体难以第一次就成功, 如果调用错误只有CsrClientCallServer
的NTSTATUS返回值能提供信息。为此还需要了解与Windows子系统的通信的基础概念与机制。
InternalSxsCreateProcess
CsrClientCallServer
本质上使用ALPC进行通信,
在alpc通信模型中, 具有客户端(client)和服务端(server)。
利用windbg的!alpc
命令, 结合PortAddress
或结合EPROCESS
得到详细信息。调试下, 我们可以确认父进程确实与csrss.exe进行通信。
但依然不知道扔进去的数据, csrss是如何处理的。参考一些公开资料, 可以了解到csrss由csrsrv.dll
实现本体功能, 并由另外3个dll负责具体功能:
- basesrv.dll
- sxssrv.dll
- winsrv.dll
这三个都简单看下, 发现令人感兴趣的函数名
csrsrv!CsrApiRequestThread
basesrv!BaseSrvCreateProcess2
对csrss.exe进行内核态下的调试, 控制alpc的调用, 我们可以得到以下执行逻辑:
sxssrv!InternalSxsCreateProcess
实现了与Windows子系统相关的进程创建的具体逻辑。
这样, 当调用CsrClientCallServer
失败时, 我们有了可以优先考虑并分析的函数。
最后的N步
回到notepad.exe的345错误。先调试正常创建的notepad.exe, 在LdrInitializeThunk
断下, 此时PEB.ActivationContextData
字段已经有值了, 说明该字段由父进程创建有关。将该字段置0, F9, 会出现STATUS_ORDINAL_NOT_FOUND
一样的报错。
我们可以推测该字段的缺失正是SxS机制没有起作用的表现。所以, 只要我们找到正常创建进程下, 何时子进程的PEB中诞生了该字段的值, 就能找到根本原因。
仍然需要通过对正常进程创建时的BASE_API_MSG
分析。
注意到在BASE_SXS_STREAM
的偏移+0x10和偏移+0x18处有值, 而在伪造的BASE_API_MSG
中为空。
有趣的是, 偏移+0x10上的值看起来是个地址, 但却不能follow。那么如果不是父进程, 只有一种可能性, 就是子进程的。此时子进程已经被NtCreateUserProcess
调用后被创建, 所以可以查看其内存布局:
而这正是notepad.exe的Manifest文件的数据, 偏移+0x18即为Manifest文件的长度。
于是, 将PS_CREATE_INFO.SuccessState
后返回的结果, 复制给CreateProcessMSG.Sxs
:
1 2 | m.CreateProcessMSG.Sxs.PolicyStream.ManifestAddress = createInfo.SuccessState.ManifestAddress;
m.CreateProcessMSG.Sxs.PolicyStream.ManifestSize = createInfo.SuccessState.ManifestSize;
|
之后, 如果调用CsrClientCallServer
返回STATUS_ACCESS_DENIED
。欢迎来到最后一步!
正如上一小节所述, 优先分析InternalSxsCreateProcess
何处抛出的STATUS_ACCESS_DENIED
:
与正常情况对比:
异常情况:
发现没有读权限, 自然无法读子进程, 产生访问拒绝也不奇怪了。
往上溯源, 最终发现句柄在nt!NtCreateUserProcess
中被创建:
createProcCtx
作为PspCaptureCreateInfo
的返回值:
而默认情况下, 会or 0x100020
, 代表Sync | Execute/Traverse
。
所以, 我们需要在NtCreateUserProcess
的CreateInfo
参数中, 添加AdditionalFileAccess
:
1 | createInfo.InitState.AdditionalFileAccess = 0x1000a1 ; / / Synch | Read / List | Execute / Traverse | ReadAttr (used by SxS)
|
至此, 所需的皆以满足:
正常创建notepad.exe成功!
完整的项目地址: Direct-NtCreateUserProcess
0x04 Epilogue
本文至此就要结束了。NtCreateUserProcess
之前存在公开代码(Microwave89), 可惜无法正常工作, 但是作者提供了一种可行的蓝图。本项目中进程创建过程中的API皆为Native API, 理论上下一步可以全部使用Syswhisper3等Direct Syscall技术以实现Bypass AV/EDR。
另外, BASE_API_MSG
对于红队来讲, 似乎还有可挖掘的空间, 期待后续对此的研究。
参考文章以及代码:
- Windows Internals 7th
- Geoff Chappel
- ReactOS
- processhacker
- leecher1337/ntvdmx64
[培训]《安卓高级研修班(网课)》月薪三万计划
最后于 2022-5-16 08:44
被D0pam1ne编辑
,原因: