本帖最后由 YY菌 于 2024-12-27 17:52 编辑
原帖来源:【Win32API】关于CreateProcess的第二个参数的研究 https://www.0xaa55.com/thread-27526-1-1.html (出处: 技术宅的结界)
在学Win32API编程的过程中肯定少不了创建子进程的需求,以前在学VB6的时候就会用到Shell函数来创建子进程,到了学VC的时候就需要手动调用WinAPI来实现了,这个时候我们最开始发现和VB6的Shell类似的自然是WinExec函数(连参数都跟VB6的Shell“一样”)。随着需求的进一步增加又会发现WinExec的功能不够用了(比如VB6的Shell至少会返回进程id,而WinExec返回的却是错误代码,还有就是WinExec只有ANSI版没有Unicode版),这时就会接触到一个更加高级的API函数CreateProcess,可以发现WinExec的第一个参数对应CreateProcess的第二个参数,WinExec的第二参数则是对应STARTUPINFO结构体中的wShowWindow字段。最开始大家都是先从ANSI编程开始的按照WinExec的习惯去使用CreateProcessA没有任何问题,后来开始学习Unicode编程了用到CreateProcessW就发现不对劲了(程序产生了0xC0000005异常),新手时期大多数人都会一脸懵逼。
在后面有了一定技术水平之后,就会知道0xC0000005异常是内存权限错误,而CreateProcessW的第二个参数的类型是LPWSTR(并且标记了__inout_opt),也就是说这个参数是会被CreateProcessW修改的,于是试了一下先把字符串常量赋值给字符数组变量再传给CreateProcessW果然就OJBK了(提示:在旧版C艹标准中,const wchar_t *和const wchar_t []的都是不能赋值给wchar_t *变量的,但是 L"字符串常量" 却可以赋值给wchar_t *这就是导致CreateProcessW坑到一大堆新手的罪魁祸首)。当第一次了解到这里的时候直接就被震惊了,甚至感觉很不科学,明明在CreateProcessW返回后的这个参数的内容和传入之前一模一样,为啥就被修改了呢?既然修改了返回后的内容还是一样,那修改这个参数的意义是啥呢?同样是 __inout_opt LPSTR 参数的CreateProcessA为啥却没有这个问题?随着一系列的疑问下来,便决定对CreateProcess的这第二个参数好好研究。
根据MSDN的描述来看,当第一个参数lpApplicationName为nullptr时,会根据第二个参数lpCommandLine中的第一个命令行来生成lpApplicationName,看到这里还暂时看不出来为啥跟lpCommandLine参数被修改有啥关系,接下来再看如果lpCommandLine参数的第一个命令行不是绝对路径的话,它会按搜素路径顺序去查找对应可执行文件(1.当前进程的主exe安装路径,2.当前进程的工作目录,3.GetSystemDirectory返回的目录,4. GetWindowsDirectory返回的目录,5.Path环境变量对应的目录),到这里绝对大部对C/C艹语言或WinAPI不熟悉的开发者多半会觉得不就是查找文件嘛,跟lpCommandLine被修改有啥关系?这时我就需要提示一下,在C语言标准中字符串都是以\0为结束标记的,而lpCommandLine是包含多个命令行参数组合的字符串,每个命令行参数之间都是使用空格来分割的(如果参数中本身包含空格则需要带上双引号,如果参数本身也包含双引号那就需要连续两个双引号转义一个双引号),这时是不是就会想到lpCommandLine中的第一个命令行参数大概率不会是\0结尾(除非只有一个参数且没带双引号的情况)。所以,CreateProcessW是不是需要先把可执行文件名结尾处的空格或双引号先临时改成\0才能实现对文件的搜索?至于最后调用完后返回的内容没有变,这是因为在搜索完可执行文件的全路径后,再最终创建进程的时候要把原始的命令行参数传给子进程,所以必须得改回去。经过测试发现,当我们正确传入非空lpApplicationName参数时,再给lpCommandLine直接传字符串常量,这时调用CreateProcessW就没有任何问题,这也更加进一步验证了lpCommandLine被修改问题的关键就在这个路径搜索上。
关于CreateProcessW的原理是研究明白了,但大家是不是还有很多疑惑?为啥CreateProcessA的lpApplicationName为nullptr给lpCommandLine传常量却没有这个问题呢?难道CreateProcessA不走路径搜索吗?答案显然不是,因为在Win32中kernel32.dll这层API函数的ANSI版绝大多数都是内部做临时编码转换,然后再调用的Unicode版实现。也就是说CreateProcessA会把ANSI字符集的lpCommandLine在内存中临时转换成Unicode字符集的副本,然后再调用CreateProcessW,这样CreateProcessW在处理路径搜索时修改的就是在CreateProcessA分配的临时内存上(这个内存是一定具有写入权限的,不然转换出来的Unicode字符串也没法写进去),而不是CreateProcessA本来接收的lpCommandLine参数。因此CreateProcessA永远不会修改用户传入的参数,自然也就不会有这个问题。在最新版的MSDN文档中巨硬已经补充了一句“此函数的 Unicode 版本(CreateProcessW)可以修改此字符串的内容。 因此,此参数不能是指向只读内存的指针(例如 常量 变量或文本字符串)。 如果此参数是常量字符串,则函数可能会导致访问冲突。”
自此,我们现在已经完全搞明白了CreateProcess的第二个参数lpCommandLine神奇现象的原理,下面就来模仿一下其内部实现(这里会用到一个冷门API函数SearchPathW):
- #define WIN32_LEAN_AND_MEAN
- #include <Windows.h>
- #include <malloc.h>
- #ifdef UNICODE
- #define MyCreateProcess MyCreateProcessW
- #else
- #define MyCreateProcess MyCreateProcessA
- #endif // !UNICODE
- // Unicode 版实现
- EXTERN_C DECLSPEC_NOINLINE BOOL WINAPI MyCreateProcessW(__in_opt LPCWSTR lpApplicationName, __inout_opt LPWSTR lpCommandLine, __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles, __in DWORD dwCreationFlags, __in_opt LPVOID lpEnvironment, __in_opt LPCWSTR lpCurrentDirectory, __in LPSTARTUPINFOW lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation)
- {
- // 先判断第一个参数为空且第二个参数不为空的情况(如果成立则从lpCommandLine中取第一个命令行参数来搜索可执行文件)
- if (!lpApplicationName && lpCommandLine) {
- // 定义两个变量分别标记lpCommandLine中可执行文件的起始位置和结束位置
- LPWSTR app4cl = lpCommandLine, appec;
- // 如果lpCommandLine的第一个字符不是双引号,那么就以空格结尾,否则就以双引号结尾。
- if (L'"' != *app4cl) appec = wcschr(app4cl, L' ');
- else appec = wcschr(++app4cl, L'"');
- // 如果找到正确位置需要临时修改成\0结束标记,否则SearchPathW会出错,没有找到则说明lpCommandLine中没有多个参数(刚好本身就有\0结尾)。
- if (appec) *appec = L'\0';
- // 开始用从lpCommandLine中取到的文件名搜索对应的.exe文件(SearchPathW的第一个参数传nullptr表示使用系统默认的搜索路径规则)
- DWORD len = SearchPathW(nullptr, app4cl, L".exe", 0, nullptr, nullptr); // 在不接收搜索结果的情况下函数会返回包括\0的长度
- if (len) { // 搜索成功则分配内存并获取结果
- // 一般来说可执行文件路径都会被限制在MAX_PATH长度,不会太长这里就偷懒从堆栈上来分配临时内存了。
- (void*&)lpApplicationName = _alloca(len << 1); // 注意:内存的分配都是按字节大小来算长度的,所以Unicode文本长度需要 * 2。
- // 分配好缓冲区内存之后就可以开始接收搜索结果了(提示:现在返回的长度是不包括\0的了)
- len = SearchPathW(nullptr, app4cl, L".exe", len, LPWSTR(lpApplicationName), nullptr);
- }
- // 搜索完成后就把被临时修改的lpCommandLine给改回去(由于确定只会有双引号和空格两种情况,所以我这里偷懒了没有保存原始的字符)
- if (appec) *appec = app4cl != lpCommandLine ? L'"' : L' ';
- // 如果搜索失败则返回失败(SearchPathW内部会设置LastError,不用自己再设置)
- if (!len) return FALSE;
- }
- // 假设存在的一个内部CreateProcess函数(由于这里仅研究第二个参数lpCommandLine,不考虑其它细节,因此放弃了使用RtlCreateUserProcess来实现的想法)
- return CreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
- }
- // ANSI 版实现
- EXTERN_C DECLSPEC_NOINLINE BOOL WINAPI MyCreateProcessA(__in_opt LPCSTR lpApplicationName, __inout_opt LPSTR lpCommandLine, __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles, __in DWORD dwCreationFlags, __in_opt LPVOID lpEnvironment, __in_opt LPCSTR lpCurrentDirectory, __in LPSTARTUPINFOA lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation)
- {
- // 字符串参数的索引常量
- enum { AppLen, CmdLen, CurLen, DskLen, TitLen, LensCount, };
- // Unicode 缓冲区
- LPSTARTUPINFOEXW psiex = nullptr;
- __try {
- int srcls[LensCount], dstls[LensCount + 1];
- // 计算所有ANSI参数的完整长度(含\0,因此nullptr长度为0 ""则为1)
- srcls[AppLen] = lpApplicationName ? lstrlenA(lpApplicationName) + 1 : 0;
- srcls[CmdLen] = lpCommandLine ? lstrlenA(lpCommandLine) + 1 : 0;
- srcls[CurLen] = lpCurrentDirectory ? lstrlenA(lpCurrentDirectory) + 1 : 0;
- srcls[DskLen] = lpStartupInfo->lpDesktop ? lstrlenA(lpStartupInfo->lpDesktop) + 1 : 0;
- srcls[TitLen] = lpStartupInfo->lpTitle ? lstrlenA(lpStartupInfo->lpTitle) + 1 : 0;
- *dstls = ((max(sizeof(*lpStartupInfo), lpStartupInfo->cb) + 3) >> 2) << 1;
- // 计算所有ANSI参数转换为Unicode之后的完全长度(含\0,算的字符个数不是字节数)
- dstls[AppLen + 1] = lpApplicationName ? MultiByteToWideChar(CP_ACP, 0, lpApplicationName, srcls[AppLen], nullptr, 0) : 0;
- dstls[CmdLen + 1] = lpCommandLine ? MultiByteToWideChar(CP_ACP, 0, lpCommandLine, srcls[CmdLen], nullptr, 0) : 0;
- dstls[CurLen + 1] = lpCurrentDirectory ? MultiByteToWideChar(CP_ACP, 0, lpCurrentDirectory, srcls[CurLen], nullptr, 0) : 0;
- dstls[DskLen + 1] = lpStartupInfo->lpDesktop ? MultiByteToWideChar(CP_ACP, 0, lpStartupInfo->lpDesktop, srcls[DskLen], nullptr, 0) : 0;
- dstls[TitLen + 1] = lpStartupInfo->lpTitle ? MultiByteToWideChar(CP_ACP, 0, lpStartupInfo->lpTitle, srcls[TitLen], nullptr, 0) : 0;
- // 累加所有长度,以便后期使用的复杂度从On降至O1
- for (int i = 0; i < LensCount;) { register auto cur = dstls[i]; dstls[++i] += cur; }
- // 使用总长度分配缓冲区内存(这次的参数可能会特别长,特别是lpCommandLine最大长度为32767,因此最好从堆上分配)
- (LPVOID&)psiex = LocalAlloc(LMEM_FIXED, dstls[LensCount] << 1);
- if (!psiex) __leave;
- // 拷贝StartupInfo结构体(STARTUPINFOA和STARTUPINFOW的结构体完全一致,只是需要将其中的LPSTR字段替换成LPWSTR字段)
- CopyMemory(psiex, lpStartupInfo, max(sizeof(*lpStartupInfo), lpStartupInfo->cb));
- // 转换所有的ANSI参数为Unicode,并按条件替换
- if (lpApplicationName) MultiByteToWideChar(CP_ACP, 0, lpApplicationName, srcls[AppLen], LPWSTR(psiex) + dstls[AppLen], dstls[AppLen + 1] - dstls[AppLen]);
- if (lpCommandLine) MultiByteToWideChar(CP_ACP, 0, lpCommandLine, srcls[CmdLen], LPWSTR(psiex) + dstls[CmdLen], dstls[CmdLen + 1] - dstls[CmdLen]);
- if (lpCurrentDirectory) MultiByteToWideChar(CP_ACP, 0, lpCurrentDirectory, srcls[CurLen], LPWSTR(psiex) + dstls[CurLen], dstls[CurLen + 1] - dstls[CurLen]);
- if (lpStartupInfo->lpDesktop) MultiByteToWideChar(CP_ACP, 0, lpStartupInfo->lpDesktop, srcls[DskLen], psiex->StartupInfo.lpDesktop = LPWSTR(psiex) + dstls[DskLen], dstls[DskLen + 1] - dstls[DskLen]);
- if (lpStartupInfo->lpTitle) MultiByteToWideChar(CP_ACP, 0, lpStartupInfo->lpTitle, srcls[TitLen], psiex->StartupInfo.lpTitle = LPWSTR(psiex) + dstls[TitLen], dstls[TitLen + 1] - dstls[TitLen]);
- //转换完成后调用对应的 Unicode 版实现就行了
- return MyCreateProcessW((lpApplicationName ? LPCWSTR(psiex) + dstls[AppLen] : 0), (lpCommandLine ? LPWSTR(psiex) + dstls[CmdLen] : 0), lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, (lpCurrentDirectory ? LPCWSTR(psiex) + dstls[CurLen] : 0), &psiex->StartupInfo, lpProcessInformation);
- }
- __finally {
- // 再离开作用域后需要释放从堆中分配的内存
- if (psiex) LocalFree(psiex);
- }
- return FALSE;
- }
- // WinExec 实现
- EXTERN_C DECLSPEC_NOINLINE UINT WINAPI MyWinExec(__in LPCSTR lpCmdLine, __in UINT uCmdShow)
- {
- // 等待超时时长(WinExec最长等待30秒,ShellExecuteEx则是60秒,CreateProcess则不会做任何等待)
- static const DWORD TIMEOUT = 30000;
- PROCESS_INFORMATION processInfo;
- // 启动参数设置(WinExec只有用到wShowWindow字段)
- STARTUPINFOA startupInfo = { sizeof(startupInfo), nullptr, nullptr, nullptr, 0, 0, 0, 0, 0, 0, 0, STARTF_USESHOWWINDOW, WORD(uCmdShow), };
- // 调用CreateProcessA(由于CreateProcessA内部永远不会修改到lpCommandLine,所以直接强转LPSTR传递是安全的)
- if (MyCreateProcessA(nullptr, LPSTR(lpCmdLine), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startupInfo, &processInfo)) {
- // 关闭线程句柄(用不上)
- CloseHandle(processInfo.hThread);
- // 等待子进程GUI初始化完成(以第一次把消息队列清空为条件)
- WaitForInputIdle(processInfo.hProcess, TIMEOUT);
- // 关闭进程句柄(用完了)
- CloseHandle(processInfo.hProcess);
- // 返回成功
- return HINSTANCE_ERROR + TRUE;
- }
- // 失败则获取错误码
- DWORD dwErr = GetLastError();
- // 转换错误码
- switch (dwErr)
- {
- case ERROR_FILE_NOT_FOUND: // 找不到可执行文件
- case ERROR_PATH_NOT_FOUND: // 无效的文件路径
- break;
- case ERROR_BAD_EXE_FORMAT: // 文件不是EXE格式
- dwErr = ERROR_BAD_FORMAT;
- break;
- default: // 其它错误状态都是返回0
- dwErr = FALSE;
- }
- return UINT(dwErr);
- }
复制代码压缩包密码没有密码 ,如果有那就在隐藏内容中(回帖可见) )
|
|