windows进程是怎么启动的呢?对于用户来说就是双击,那么这个双击实际上是由explorer进程调用CreateProcess api来创建了这个进程。又或者是通过命令行,cmd拉起。那么对于开发者来说,应该怎么绕开双击,或者命令行启动这些方式呢?下面介绍几种方法。
代码怎么拉起一个进程呢,这个是有一些API的,如WinExec、ShellExecute、CreateProcess等函数。
/*WinExec 参数/返回值说明:
参数:
lpCmdLine:命令行参数。
ShowCmd:外部程序的运行方式。其取值如下:
----SW_HIDE 隐藏
----SW_MAXIMIZE 最大化
----SW_MINIMIZE 最小化,并把Z order顺序在此窗口之后(即窗口下一层)的窗口激活
----SW_RESTORE 激活窗口并还原为初始化大小 SW_SHOW 以当前大小和状态激活窗口
----SW_SHOW 用当前的大小和位置显示一个窗口,同时令其进入活动状态
----SW_SHOWDEFAULT 以默认方式运行
----SW_SHOWMAXIMIZED 激活窗口并最大化
----SW_SHOWMINIMIZED 激活窗口并最小化
----SW_SHOWMINNOACTIVE 最小化但不改变当前激活的窗口
----SW_SHOWNA 以当前状态显示窗口但不改变当前激活的窗口
----SW_SHOWNOACTIVATE 以初始化大小显示窗口但不改变当前激活的窗口
----SW_SHOWNORMAL 激活并显示窗口,如果是最大(小)化,窗口将会还原。第一次运行程序 时应该使用这个值
返回值:
函数成功,返回值>31。
函数失败,返回:
--0 系统内存或资源不足
--ERROR_BAD_FORMAT .EXE文件格式无效(比如不是32位应用程序)
--ERROR_FILE_NOT_FOUND 指定的文件设有找到
--ERROR_PATH_NOT_FOUND 指定的路径没有找到
*/
UINT WinExec(LPCSTR lpCmdLine,UINT uCmdShow);
/*ShellExecute 参数/返回值说明:
参数:
hWnd:用于指定父窗口句柄。当函数调用过程出现错误时,它将作为Windows消息窗口的父窗口。例如,可以将其设置为应用程序主窗口句柄,即Application.Handle,也可以将其设置为桌面窗口句柄(用GetDesktopWindow函数获得)。
lpOperation:用于指定要进行的操作
edit:启动编辑器并打开文档进行编辑,如果lpFile不是文件,则该函数失败;
open:执行由FileName参数指定的程序,或打开由FileName参数指定的文件或文件夹;
print:打印由FileName参数指定的文件;
explore:浏览由FileName参数指定的文件夹;
find:在由lpDirectory指定的目录中启动搜索;
lpFile:用于指定要打开的文件名、要执行的程序文件名或要浏览的文件夹名。
lpParameters:若FileName参数是一个可执行程序,则此参数指定命令行参数,否则此参数应为nil或PChar(0)。
lpDirectory:用于指定默认目录。
nShowCmd:同上
返回值:
函数成功,返回值>32。
函数失败,返回其他值。
*/
HINSTANCE ShellExecute( HWND hwnd,LPCSTR lpOperation,LPCSTR lpFile,LPCSTR lpParameters,LPCSTR lpDirectory,INT nShowCmd);
BOOL CreateProcess(LPCWSTR pszImageName,LPCWSTR pszCmdLine,LPSECURITY_ATTRIBUTES psaProcess,LPSECURITY_ATTRIBUTES psaThread,BOOL fInheritHandles,DWORD fdwCreate,LPVOID pvEnvironment,LPWSTR pszCurDir,LPSTARTUPINFOW psiStartInfo,LPPROCESS_INFORMATION pProcInfo);
WinExec示例代码
bool WinExecTest(char * p_exe_path, UINT i_ui_show)
{
UINT i_ret = 0;
i_ret = WinExec(p_exe_path, i_ui_show);
if (i_ret > 31)//大于31表示执行成功,否则失败
{
return true;
}
return false;
}
ShellExecute示例代码
bool ShellExcuteTest(wchar_t * p_exe_path, UINT i_ui_show)
{
HINSTANCE hInstance = 0;
hInstance = ::ShellExecute(NULL, NULL, p_exe_path, NULL, NULL, i_ui_show);
if ((DWORD)hInstance > 32)//大于32表示执行成功
{
return true;
}
return false;
}
CreateProcess示例代码
bool CreateProcessTest(wchar_t * p_exe_path, UINT i_ui_show)
{
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi;
bool b_ret = false;
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = i_ui_show;
b_ret = ::CreateProcess(NULL, p_exe_path, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (b_ret)
{
//关掉句柄
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
return true;
}
return false;
}
SESSION这个概念呢,实际上就是Windows将应用程序根据Windows账户分为了多个Session。比如Administor打开的进程可能属于Session1,而另一个账户Civilian打开的进程就属于Session2,另一个zhourui账户是SESSION3等等。那么SESSION 0呢是windows服务进程,如下所示,服务进程无法再与用户进程有交互式操作(UI 通信等),这就是Session隔离。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyjQmoMi-1654776711765)(C:\Users\K\AppData\Roaming\Typora\typora-user-images\image-20220425103346347.png)]
那么怎么服务层与应用层的进程怎么通信和交互呢?windows提供了一套WTS(Windows Terminal Service)的接口,可以进行通信和交互。
DWORD WTSGetActiveConsoleSessionId();
WTSGetActiveConsoleSessionId 检索控制台会话的标识符Session Id。微软描述
BOOL WTSQueryUserToken(
[in] ULONG SessionId,
[out] PHANDLE phToken
);
WTSQueryUserToken 获取Session Id指定的登录用户的主访问令牌。(https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsqueryusertoken)
BOOL DuplicateTokenEx(
[in] HANDLE hExistingToken,
[in] DWORD dwDesiredAccess,
[in, optional] LPSECURITY_ATTRIBUTES lpTokenAttributes,
[in] SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
[in] TOKEN_TYPE TokenType,
[out] PHANDLE phNewToken
);
DuplicateTokenEx 创建一个复制现有令牌的新访问令牌。此函数可以创建主令牌或模拟令牌。
BOOL CreateEnvironmentBlock(
[out] LPVOID *lpEnvironment,
[in, optional] HANDLE hToken,
[in] BOOL bInherit
);
CreateEnvironmentBlock 检索指定用户的环境变量。然后可以将此块传递给CreateProcessAsUser函数。
BOOL CreateProcessAsUserW(
[in, optional] HANDLE hToken,
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
CreateProcessAsUserW 创建一个新进程及其主线程,新进程在由指定令牌表示的用户的安全上下文中运行。
由于Session0的隔离,使得在系统服务进程内不能直接调用CreateProess等函数创建进程,而是要通过CreateProcessAsUser函数来创建,这样创建的进程才会显示UI界面,与用户进行交互。
//还有一些头文件和依赖
BOOL CreateUserProcess(wchar_t *p_file_nmae)
{
BOOL bRet = FALSE;
DWORD dwSessionID = 0;
HANDLE hToekn;
HANDLE hTokenDup = NULL;
LPVOID pEnv = NULL;
STARTUPINFOW si;
PROCESS_INFORMATION pi;
//获取当前session id
dwSessionID = ::WTSGetActiveConsoleSessionId();
do
{
if (FALSE == ::WTSQueryUserToken(dwSessionID, &hToekn))
{
//WTSQueryUserToken error
bRet = FALSE;
break;
}
//复制令牌
if (FALSE == ::DuplicateTokenEx(hToekn, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hTokenDup))
{
//DuplicateTokenEx error
bRet = FALSE;
break;
}
//创建用户会话环境
if (FALSE == ::CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE))
{
//CreateEnvironmentBlock error
bRet = FALSE;
break;
}
if (FALSE == ::CreateProcessAsUser(hTokenDup, p_file_nmae, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, pEnv, NULL, &si, &pi))
{
//CreateProcessAsUser error
bRet = FALSE;
break;
}
} while (FALSE);
//关闭句柄
::CloseHandle(hToekn);
::CloseHandle(hTokenDup);
return bRet;
}
突破SESSION 0隔离创建用户进程-源码链接
执行ServiceLoader.exe可以发现,他将CreateProcessAsUser_Test.exe注册成了服务,
运行在Session 0下。
继续执行,我们在这个CreateProcessAsUser_Test.exe服务中,拉起了一个test.exe,可以看到这个进程是在Session 1下。
至此,我们做到了突破Session 0隔离创建用户进程。
btw,我们可以通过hook CreatProcessAsUser来监控进程创建。
不通过LoadLibrary来加载DLL,不通过CreateProcess等API函数来加载exe,而是直接把DLL或者EXE等PE文件直接加载到内存中去执行。
内存直接加载运行技术的核心就是模拟PE加载器加载PE文件的过程,
这边以加载DLL为例:
首先就是把DLL文件按照映像对齐大小映射到内存中,切不可直接将DLL文件数据存储到内存中。根据PE知识可知是因为文件对齐粒度FileAlignment小于映像对齐粒度SectionAlignment。
然而,成功映射内存数据之后,在DLL中会存在硬编码数据,硬编码都是以默认的加载机制作为基址来计算的。但是由于DLL可以任意加载到其他进程空间,所以DLL的加载基址并非固定的(exe就是固定的,大家都知道进程是在虚拟空间,也就是_IMAGE_OPTIONAL_HEADER的ImageBase)。那么当如果DLL加载基址变了,那么就需要重新算偏移,重定位表记录的就是程序中所有需要修改的硬编码的相对偏移位置。
根据重定位表修改完硬编码数据之后,还要计算DLL调用别的库函数的地址。比如DLL调用了MessageBox,那么DLL如何知道MessageBox函数的地址呢?PE结构使用导入表记录所有引用的函数及函数地址。在DLL映射到内存后,需要更具导入表中的导入模块和函数名称来获取导入函数的地址。最简单的就是GetProcAddress函数来获取,但是调用Win32 AP可能会被杀毒软件检测到。
(顺便说下,为什么我们平时调用库函数只要依赖一下头文件就行了呢?因为我们在工程里面已经链接好了这个DLL,编译软件已经帮我们做好了,叫做隐式链接。那么这种LoadLibrary加载的DLL叫显式链接)
那么我们还可以采用直接遍历PE结构导出表的方式来获取函数地址。
完成上述操作之后,DLL加载工作才算完成,然后就是获取入口地址并跳转执行完成启动。
// 模拟LoadLibrary加载内存DLL文件到进程中
// lpData: 内存DLL文件数据的基址
// dwSize: 内存DLL文件的内存大小
// 返回值: 内存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文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint
if (FALSE == CallDllMain(lpBaseAddress))
{
ShowError("CallDllMain");
return NULL;
}
return lpBaseAddress;
}
我们可以通过暴力枚举PE结构特征头的方法,来枚举进程中加载的所有模块,将它与通过正常方法获取到的模块信息进行对比,判断是否有可疑的PE文件。
本文代码:github