前置知识
基于一些github的代码项目能更好的理解
首先我们先来了解一下,CreateProcess 和 NtCreateUserprocess。
在以前的 XP 时代,必须执行四个系统调用(NtOpenFile、NtCreateSection、NtCreateProcess(Ex)、NtCreateThread(Ex))才能创建一个新的准备好运行的用户模式进程。启动一个新进程,只需要调用一个系统服务,即NtCreateUserProcess。那CreateProcess又是一个什么角色呢?
CreateProcess此函数主要就是创建新的进程,创建的进程在调用进程的上下文(同样的访问令牌)中运行。
CreateProcess通过内核创建进程的步骤,大致分为六个阶段
● 打开目标映像文件
● 创建内核中的进程对象
● 创建初始线程
● 通知windows子系统 (每个进程在创建/退出的时候都要向windows子系统进程csrss.exe进程发出通知,因为它担负着对windows所有进程的管理的责任,注意,这里发出通知的是CreateProcess的调用者,不是新建出来的进程,因为它还没有开始运行)
● 启动初始线程
● 用户空间的初始化和Dll连接
CreateProcess其实是3环的函数,也就是用户态的 而NtCreateUserprocess函数是位于0环内的
网上有画的很直观的图片
我们也可以通过断点调试来验证一下触发的流程
断点的调试的流程与上图区分的是对应的。
那么从 API 到 NtCreateUserProcess 的调用链 就是kernel32.dll!CreateProcessW-->CreateProcessInternalW-->ntdll.dll!NtCreateUserProcess
其中NtCreateProcess()做的事情,主要包括:
● 创建以EPROCESS为核心的相关数据结构,分配并设置EPROCESS数据结构;
● 其他相关的数据结构的设置,如句柄表等等;
● 为目标进程创建初始的地址空间;
● 对EPROCESS进行初始化;
● 将系统Dll映射到目标用户空间,如ntdll.dll等
● 设置目标进程的PEB;
● 将其他需要映射到用户空间,如与”当地语言支持“即NLS有关的数据结构;
● 完成EPROCESS创建,将其挂入进程队列并插入创建者的句柄表
NtCreateUserProcess()是在用户模式下可访问的最后一个函数。为什么去实现它自然是因为藏的深。如果直接调用CreateProcess EDR/AV的话肯定会很容易hook到检查到的这个常见马子的操作的API。
如果调查层比较深,或者函数不是常见的类型的话就不容易察觉了。所以现在更多的都喜欢使用NtCreateUserProcess也就是CreateProcess 调用的最后一层来去实现想要的功能。
那么再来看一下NtCreateUserProcess的原型
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, _In_ ULONG ThreadFlags, _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters, _Inout_ PPS_CREATE_INFO CreateInfo, _In_ PPS_ATTRIBUTE_LIST AttributeList );
我们还需要再来了解NtCreateUserProcess的参数
ProcessHandle ThreadHandle这两个从名字就能知道了是分别用来存储进程和线程的句柄
ProcessDesiredAccess ThreadDesiredAccess 这两个对上述进程和线程的权利和控制权也就是访问权限的配置
下图也有针对如此更详细的描述,可使用参数的介绍
THREAD
通过ACCESS_MASK值,来标识我们对正在创建的进程和线程拥有的权利和控制权。
由于我们只处理进程和线程对象,我们可以使用进程和线程特定的访问权限,所以本POC使用的则是PROCESS_ALL_ACCESS 和THREAD_ALL_ACCESS 来进程和线程的权限拉到最高。
ProcessObjectAttributes ThreadObjectAttributes 它两是指向的指针OBJECT_ATTRIBUTES
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES;
此结构包含可应用于将要创建的对象或对象句柄的属性。
ProcessFlags ThreadFlags 这两个是比如我们希望在创建时挂起进程/线程 可以进行设置,它们参数可以从Process Hacker 项目中了解,这个规范化的命名也很好能让我们理解。还贴心有了注释,大致可以设置参数如下
ProcessFlags #define PROCESS_CREATE_FLAGS_BREAKAWAY 0x00000001 // NtCreateProcessEx & NtCreateUserProcess #define PROCESS_CREATE_FLAGS_NO_DEBUG_INHERIT 0x00000002 // NtCreateProcessEx & NtCreateUserProcess #define PROCESS_CREATE_FLAGS_INHERIT_HANDLES 0x00000004 // NtCreateProcessEx & NtCreateUserProcess #define PROCESS_CREATE_FLAGS_OVERRIDE_ADDRESS_SPACE 0x00000008 // NtCreateProcessEx only #define PROCESS_CREATE_FLAGS_LARGE_PAGES 0x00000010 // NtCreateProcessEx only, requires SeLockMemory #define PROCESS_CREATE_FLAGS_LARGE_PAGE_SYSTEM_DLL 0x00000020 // NtCreateProcessEx only, requires SeLockMemory #define PROCESS_CREATE_FLAGS_PROTECTED_PROCESS 0x00000040 // NtCreateUserProcess only #define PROCESS_CREATE_FLAGS_CREATE_SESSION 0x00000080 // NtCreateProcessEx & NtCreateUserProcess, requires SeLoadDriver #define PROCESS_CREATE_FLAGS_INHERIT_FROM_PARENT 0x00000100 // NtCreateProcessEx & NtCreateUserProcess #define PROCESS_CREATE_FLAGS_SUSPENDED 0x00000200 // NtCreateProcessEx & NtCreateUserProcess #define PROCESS_CREATE_FLAGS_FORCE_BREAKAWAY 0x00000400 // NtCreateProcessEx & NtCreateUserProcess, requires SeTcb #define PROCESS_CREATE_FLAGS_MINIMAL_PROCESS 0x00000800 // NtCreateProcessEx only #define PROCESS_CREATE_FLAGS_RELEASE_SECTION 0x00001000 // NtCreateProcessEx & NtCreateUserProcess #define PROCESS_CREATE_FLAGS_CLONE_MINIMAL 0x00002000 // NtCreateProcessEx only #define PROCESS_CREATE_FLAGS_CLONE_MINIMAL_REDUCED_COMMIT 0x00004000 // #define PROCESS_CREATE_FLAGS_AUXILIARY_PROCESS 0x00008000 // NtCreateProcessEx & NtCreateUserProcess, requires SeTcb #define PROCESS_CREATE_FLAGS_CREATE_STORE 0x00020000 // NtCreateProcessEx & NtCreateUserProcess #define PROCESS_CREATE_FLAGS_USE_PROTECTED_ENVIRONMENT 0x00040000 // NtCreateProcessEx & NtCreateUserProcess // ThreadFlags #define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001 // NtCreateUserProcess & NtCreateThreadEx #define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002 // NtCreateThreadEx only #define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004 // NtCreateThreadEx only #define THREAD_CREATE_FLAGS_LOADER_WORKER 0x00000010 // NtCreateThreadEx only #define THREAD_CREATE_FLAGS_SKIP_LOADER_INIT 0x00000020 // NtCreateThreadEx only #define THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE 0x00000040 // NtCreateThreadEx only #define THREAD_CREATE_FLAGS_INITIAL_THREAD 0x00000080 //
ProcessParameters 该参数指向一个RTL_USER_PROCESS_PARAMETERS结构体。
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, * PRTL_USER_PROCESS_PARAMETERS;
该结构体描述了要创建的进程的启动参数。而该结构将保存作为执行参数,构建则需要依靠另一个API: RtlCreateProcessParametersEx
NTSTATUS NTAPI
RtlCreateProcessParametersEx(
Out PRTL_USER_PROCESS_PARAMETERS* pProcessParameters,
In PUNICODE_STRING ImagePathName,
_Inopt PUNICODE_STRING DllPath,
_Inopt PUNICODE_STRING CurrentDirectory,
_Inopt PUNICODE_STRING CommandLine,
_Inopt PVOID Environment,
_Inopt PUNICODE_STRING WindowTitle,
_Inopt PUNICODE_STRING DesktopInfo,
_Inopt PUNICODE_STRING ShellInfo,
_Inopt PUNICODE_STRING RuntimeData,
In ULONG Flags
);
pProcessParameters参数指向的就是RTL_USER_PROCESS_PARAMETERS结构
ImagePathName就是启动进程的路径
通过RtlInitUnicodeString实现,是初始化UNICODE_STRING结构必须的。
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
结构的初始化UNICODE_STRING通过以下方式完成:
将Length和MaximumLength成员设置为的长度SourceString
将Buffer成员设置为传入的字符串的地址SourceString
使用了RtlInitUnicodeString函数最主要就是为了初始化UNICODE_STRING结构
最后简单代码实现
RtlInitUnicodeString(&ImagePath, (PWSTR)L"\??\C:\Windows\System32\calc.exe");
其中我们还需要了解一下Flag,其中从其他的参数也能明白一般创建的过程中可操作性最高的就是关于标志位的设置。
Flag是用来规范RTL_USER_PROCESS_PARAMETERS_NORMALIZED。
因为创建进程时,如果还没有完全初始化的话,那么被访问的内存只是描述进程的结构的相对偏移量,而不是实际的内存地址。
所以我们需要设置Flag来避免这一问题。
那么最后实现就是
RtlCreateProcessParametersEx(&ProcessParameters, &ImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, RTL_USER_PROCESS_PARAMETERS_NORMALIZED);
CreateInfo 参数它是一个指向PS_CREATE_INFO结构的指针
因为关于此参数的包括其结构的信息较少,所以这里借鉴两个文章可以很好的理解,就不再重复叙述
这个就是部分关于CREATE_INFO的结构体
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;
PS_CREATE_INFO结构是在NtCreateUserProcess 和ZwCreateUserProcess函数的用户模式和内核模式之间交换
其中我们要关注的就是PS_CREATE_STATE的PsCreateInitialState
所以设置为如下,初始化的话我们就填入状态和大小就行
CreateInfo.Size = sizeof(CreateInfo);
CreateInfo.State = PsCreateInitialState;
AttributeList 参数是用于设置进程和线程创建的属性,结构体信息
Windows 内部结构,第 1 部分(第 7 版) 书中有关于进程属性的一张表,很好的做了一部分诠释
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_IMAGE_NAME指定要创建的进程,NtImagePath保存创建进程的路径
PPS_ATTRIBUTE_LIST AttributeList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PS_ATTRIBUTE)); AttributeList->TotalLength = sizeof(PS_ATTRIBUTE_LIST) - sizeof(PS_ATTRIBUTE); AttributeList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME; AttributeList->Attributes[0].Size = NtImagePath.Length; AttributeList->Attributes[0].Value = (ULONG_PTR)NtImagePath.Buffer;
以上就是NtCreateUserProcess 各个参数的代表的意义和与其使用相关的一些东西。
那么最后构造实现就可以简单为如下
//无关紧要的就设置NULL大法即可
NtCreateUserProcess(&hProcess, &hThread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, NULL, NULL, NULL, NULL, ProcessParameters, &CreateInfo, AttributeList);
最后还有个清理痕迹,因为我们是在堆里面分配的空所以可以使用RtlFreeHeap函数,它可以释放由RtlAllocateHeap分配的内存。还有RtlDestroyProcessParameters 函数阔以释放存储在RTL_USER_PROCESS_PARAMETERS结构中的进程参数。
实现效果
PPID (父进程欺骗)
NtCreateUserProcess也可以进行PPID的操作
PPID 是恶意软件作者用来混入目标系统的技术之一。这是通过使恶意进程看起来像是由另一个进程产生的来完成的。这有助于规避基于异常父子进程关系的检测。
父进程欺骗有几处需要注意的,首先我们也要确定我们假借的父进程是哪个,然后确定其完整性如图是System则要求我们自身的权限很高才行,但如果Medium的话那么就不需要了效果如下
GetTokenInformation 可以用这个函数确定进程访问令牌的信息,然后用SID进行比较就可以确定出进程的级别
还记得PS_ATTRIBUTE_IMAGE_NAME是指定要创建的进程的名称,NtImagePath变量保存创建进程的文件的路径。那么要将一个进程生成为另一个进程的子进程,我们就可以使用PsAttributeParentProcess PS_ATTRIBUTE_NUM ,然后将HANDLE给父进程。
那么就直接替换把父节点的attribute加进去 通过PS_ATTRIBUTE_PARENT_PROCESS
// obtain handle to parent
OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, 0, 0, 0, 0);
CLIENT_ID cid = { (HANDLE)5644, NULL };
这里的进程id是我设置死的一个如果需要更改的话其实可以参考这段代码
HANDLE hToken;
OpenProcessToken(hProcess, TOKEN_QUERY, &hToken);
DWORD cbTokenIL = 0;
PTOKEN_MANDATORY_LABEL pTokenIL = NULL;
GetTokenInformation(hToken, TokenIntegrityLevel, NULL, 0, &cbTokenIL);
pTokenIL = (TOKEN_MANDATORY_LABEL)LocalAlloc(LPTR, cbTokenIL);
GetTokenInformation(hToken, TokenIntegrityLevel, pTokenIL, cbTokenIL, &cbTokenIL);
DWORD dwIntegrityLevel = GetSidSubAuthority(pTokenIL->Label.Sid, 0);
HANDLE hParent = NULL;
NtOpenProcess(&hParent, PROCESS_ALL_ACCESS, &oa, &cid);
// add parent process attribute
AttributeList->Attributes[1].Attribute = PS_ATTRIBUTE_PARENT_PROCESS;
AttributeList->Attributes[1].Size = sizeof(HANDLE);
AttributeList->Attributes[1].ValuePtr = hParent;
//要注意的是我们每添加一个新属性,都需要将PS_ATTRIBUTE增大,因为调用RtlAllocateHeap要为PS_ATTRIBUTE分配足够的内存空间
最后NtCreateUserProcess
NtCreateUserProcess(&hProcess, &hThread, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, NULL, NULL, NULL, NULL, ProcessParameters, &CreateInfo, AttributeList);
其实很多CreateProcess的项目,就比如ppid也有用CreateProcess写的,都可以加以改变改成用NtCreateUserProcess操作的,就可以把一个很简单的被查杀CreateProcess进行一个免杀保护了。(当然也不是都能改的具体情况具体分析)