漫谈兼容内核之十七:
再谈Windows的进程创建
毛德操
在漫谈之十中。我根据“Microsoft Windows Internals 4e”一书第六章的叙述介绍了Windows的进程创建和映像装入的过程。但是,由于缺乏源代码的支撑,这样的叙述对于只是想对此有个大致了解的读者固然不无帮助,可是对于需要实际从事研发、特别是兼容内核开发的读者就显得过于抽象笼统了。不幸,Windows内核的代码是不公开的,我们无法通过Windows内核的代码来确切地了解和理解它的方方面面。虽说是“科学无禁区”,但是现实往往不那么理想。幸运的是我们有了ReactOS。当然,ReactOS不等于Windows,但是读者将会看到,至少就Windows进程的创建而言,它的代码和“Internals”书中的叙述还是相当吻合的。本文引用的代码均取自ReactOS的0.2.6版,大致上是一年前的版本。
正如“Internals”所述,Windows进程的创建是个复杂的过程,分成好几个步骤,涉及到好几个系统调用。Win32 API函数CreateProcessW()就是这些步骤的整合。这是由动态连接库kernel32.dll导出的库函数,其主体就在这个DLL中。还有个CreateProcessA(),是与CreateProcessW()连在一起的,前者接受ASCII码的字符串,而后者要求使用“宽字符”、即Unicode的字符串。实际上CreateProcessA()只是把ASCII字符串转换成Unicode字符串,然后就调用CreateProcessW()。
[CreateProcessW()]
BOOL STDCALL
CreateProcessW (LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation)
{
. . . . . .
TidyCmdLine = GetFileName(lpCurrentDirectory, lpApplicationName, lpCommandLine,
Name, sizeof(Name) / sizeof(WCHAR));
. . . . . .
if (lpApplicationName != NULL && lpApplicationName[0] != 0)
{
wcscpy (TempApplicationNameW, lpApplicationName);
i = wcslen(TempApplicationNameW);
if (TempApplicationNameW[i - 1] == L'.')
{
TempApplicationNameW[i - 1] = 0;
}
else
{
s = max(wcsrchr(TempApplicationNameW, L'//'),
wcsrchr(TempApplicationNameW, L'/'));
if (s == NULL)
{
s = TempApplicationNameW;
}
else
{
s++;
}
e = wcsrchr(s, L'.');
if (e == NULL)
{
wcscat(s, L".exe");
e = wcsrchr(s, L'.');
}
}
}
else if (L'"' == TidyCmdLine[0])
{
wcscpy(TempApplicationNameW, TidyCmdLine + 1);
s = wcschr(TempApplicationNameW, L'"');
if (NULL == s)
{
return FALSE;
}
*s = L'/0';
}
else
{
wcscpy(TempApplicationNameW, TidyCmdLine);
s = wcschr(TempApplicationNameW, L' ');
if (NULL != s)
{
*s = L'/0';
}
}
s = max(wcsrchr(TempApplicationNameW, L'//'), wcsrchr(TempApplicationNameW, L'/'));
if (NULL == s)
{
s = TempApplicationNameW;
}
s = wcsrchr(s, L'.');
if (NULL == s)
{
wcscat(TempApplicationNameW, L".exe");
}
if (!SearchPathW(NULL, TempApplicationNameW, NULL,
sizeof(ImagePathName)/sizeof(WCHAR), ImagePathName, &s))
{
return FALSE;
}
e = wcsrchr(s, L'.');
if (e != NULL && (!_wcsicmp(e, L".bat") || !_wcsicmp(e, L".cmd")))
{
// the command is a batch file
IsBatchFile = TRUE;
if (lpApplicationName != NULL && lpApplicationName[0])
{
// FIXME: use COMSPEC for the command interpreter
wcscpy(TempCommandLineNameW, L"cmd /c ");
wcscat(TempCommandLineNameW, lpApplicationName);
lpCommandLine = TempCommandLineNameW;
wcscpy(TempApplicationNameW, L"cmd.exe");
if (!SearchPathW(NULL, TempApplicationNameW, NULL,
sizeof(ImagePathName)/sizeof(WCHAR), ImagePathName, &s))
{
return FALSE;
}
}
else
{
return FALSE;
}
}
/* Process the application name and command line */
RtlInitUnicodeString(&ImagePathName_U, ImagePathName);
RtlInitUnicodeString(&CommandLine_U, IsBatchFile ? lpCommandLine : TidyCmdLine);
. . . . . .
/* Initialize the current directory string */
if (lpCurrentDirectory != NULL)
{
RtlInitUnicodeString(&CurrentDirectory_U, lpCurrentDirectory);
}
else
{
GetCurrentDirectoryW(256, TempCurrentDirectoryW);
RtlInitUnicodeString(&CurrentDirectory_U, TempCurrentDirectoryW);
}
/* Create a section for the executable */
hSection = KlMapFile (ImagePathName);
因为这是个W32 API函数,对于调用参数这里就不作解释了。
代码中首先是对应用名和命令行的处理。这里要考虑许多不同的情况。例如:
l 调用参数lpApplicationName 或lpCommandLine可能是空指针。
l 命令行中的应用名、即可执行文件名可能是加引号的,也可能是不加引号的。
l 应用名可能是个完整的路径,也可能只是不带路径的应用名。
l 应用名可能带扩展名(例如.exe),也可能不带扩展名。
l 应用名后面可能带一个点,但是不带扩展名字符。
l 如果不带扩展名,那么应用名可能是指一个.exe文件,也可能是指.com或.bat文件。
要是在文件系统的指定目录中发现应用名所代表的是.com或.bat文件,那就要把应用名改成cmd.exe,而把原来的应用名和命令行作为传递给cmd.exe的参数。
具体的代码就留给读者自己阅读了。由于这里因篇幅的考虑而作了删节,读者最好还是阅读原始的ReactOS代码。代码中wcscpy()一类的函数相当于strcpy()一类,只不过所处理的是宽字符而不是普通的ASCII字符。
这里的最后一步操作是KlMapFile(),目的是为目标映像建立一个共享内存区、即Section对象。不过,对于CreateProcessW()而言,这其实还只能说是第一步。
[CreateProcessW() > KlMapFile()]
HANDLE KlMapFile(LPCWSTR lpApplicationName)
{
. . . . . .
InitializeObjectAttributes(&ObjectAttributes, &ApplicationNameString,
OBJ_CASE_INSENSITIVE, NULL, SecurityDescriptor);
/* Try to open the executable */
Status = NtOpenFile(&hFile, SYNCHRONIZE|FILE_EXECUTE|FILE_READ_DATA,
&ObjectAttributes, &IoStatusBlock,
FILE_SHARE_DELETE|FILE_SHARE_READ,
FILE_SYNCHRONOUS_IO_NONALERT|FILE_NON_DIRECTORY_FILE);
. . . . . .
Status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL,
PAGE_EXECUTE, SEC_IMAGE,
hFile);
NtClose(hFile);
. . . . . .
return(hSection);
}
先打开目标映像文件,再通过系统调用NtCreateSection()为已经打开的映像文件创建一个共享内存区对象。注意NtCreateSection()只是创建了一个共享内存区对象,并将它与一个已打开文件挂上钩,而并未将其映射到任何进程的用户空间,所以函数名KlMapFile()不免误导。由于调用时使用了参数SEC_IMAGE,表明目标文件是个映像文件,NtCreateSection()会对目标文件的头部进行检验,以确认其为PE格式的可执行映像。如果发现并非PE格式映像,就会通过hSection返回0。
回到CreateProcessW()的代码中,如果KlMapFile()的返回值非0就说明为目标映像文件创建的共享内存区对象已经成功,下面就可以用这个已打开对象(hSection为其Handle)去创建进程了。可是,如果返回值是0,那就说明目标映像文件并不是一个PE格式的文件。既然扩充名是.exe,却又不是PE格式的文件,那是怎么回事呢?原来,DOS格式的可执行文件也用.exe作为扩充名,这种文件的头部并非PE格式,但是也有DOS格式的“签名” IMAGE_DOS_SIGNATURE可供验证。
[CreateProcessW()]
if (hSection == NULL)
{
. . . . . .
DPRINT("Inspecting Image Header for image type id/n");
. . . . . .
InitializeObjectAttributes(&ObjectAttributes, &ApplicationNameString,
OBJ_CASE_INSENSITIVE, NULL, SecurityDescriptor);
// Try to open the executable
Status = NtOpenFile(&hFile, SYNCHRONIZE|FILE_EXECUTE|FILE_READ_DATA,
&ObjectAttributes, &IoStatusBlock,
FILE_SHARE_DELETE|FILE_SHARE_READ,
FILE_SYNCHRONOUS_IO_NONALERT|FILE_NON_DIRECTORY_FILE);
. . . . . .
// Read the dos header
Offset.QuadPart = 0;
Status = ZwReadFile(hFile, NULL, NULL, NULL, &Iosb,
&DosHeader, sizeof(DosHeader), &Offset, 0);
. . . . . .
if (Iosb.Information != sizeof(DosHeader)) {
DPRINT("Failed to read dos header from file/n");
SetLastErrorByStatus(STATUS_INVALID_IMAGE_FORMAT);
return FALSE;
}
// Check the DOS signature
if (DosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
DPRINT("Failed dos magic check/n");
SetLastErrorByStatus(STATUS_INVALID_IMAGE_FORMAT);
return FALSE;
}
NtClose(hFile);
DPRINT("Launching VDM.../n");
return CreateProcessW(L"ntvdm.exe", (LPWSTR)lpApplicationName,
lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags,
lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
}
DOS格式的可执行映像都是16位的,不能直接在32位的WinNT内核上运行,而得要借助系统工具软件vdm.exe的支持才能运行。为了在32位x86结构的CPU芯片上兼容为16位芯片开发的软件,Intel在x86芯片上提供了一种“虚拟机模式”。而VDM.、即“虚拟DOS机”,则是微软开发的一种系统软件,利用x86芯片的虚拟机模式为16位的DOS应用软件供一个虚拟的DOS环境,使DOS应用软件感觉到就好像是在DOS操作系统上运行一样。可想而知,ntvdm.exe就是实现于WinNT内核上的VDM。注意ntvdm.exe本身是32位软件,它所支持的目标软件才是16位的,ntvdm.exe连同其所支持的目标软件一起作为一个进程在WinNT内核上运行。所以,对于16位软件这里递归地调用CreateProcessW(),而以ntvdm.exe作为新的应用名,但是命令行则不变。
当然,16位软件只是少数,WinNT上运行的绝大多数软件都是32位PE格式的,所以NtCreateSection()一般都会返回一个非0的Handle,从而跳过上面这段代码。我们继续往下看:
[CreateProcessW()]
/* Get some information about the executable */
Status = ZwQuerySection(hSection, SectionImageInformation, &Sii, sizeof(Sii), &i);
. . . . . .
if (0 != (Sii.Characteristics & IMAGE_FILE_DLL))
{
NtClose(hSection);
DPRINT("Can't execute a DLL/n");
SetLastError(ERROR_BAD_EXE_FORMAT);
return FALSE;
}
if (IMAGE_SUBSYSTEM_WINDOWS_GUI != Sii.Subsystem
&& IMAGE_SUBSYSTEM_WINDOWS_CUI != Sii.Subsystem)
{
NtClose(hSection);
DPRINT("Invalid subsystem %d/n", Sii.Subsystem);
SetLastError(ERROR_CHILD_NOT_COMPLETE);
return FALSE;
}
/* Initialize the process object attributes */
if(lpProcessAttributes != NULL)
{
if(lpProcessAttributes->bInheritHandle)
{
ProcAttributes |= OBJ_INHERIT;
}
ProcSecurity = lpProcessAttributes->lpSecurityDescriptor;
}
InitializeObjectAttributes(&ProcObjectAttributes, NULL,
ProcAttributes, NULL, ProcSecurity);
/* initialize the process priority class structure */
PriorityClass.Foreground = FALSE;
if(dwCreationFlags & IDLE_PRIORITY_CLASS)
{
PriorityClass.PriorityClass = PROCESS_PRIORITY_CLASS_IDLE;
}
else if(dwCreationFlags & BELOW_NORMAL_PRIORITY_CLASS)
{
PriorityClass.PriorityClass = PROCESS_PRIORITY_CLASS_BELOW_NORMAL;
}
else if . . . . . .
. . . . . .
/* Create a new process */
Status = NtCreateProcess(&hProcess, PROCESS_ALL_ACCESS,
&ProcObjectAttributes, NtCurrentProcess(),
bInheritHandles, hSection, NULL, NULL);
. . . . . .
为目标映像创建的Section对象中含有许多来自目标映像PE头部的信息,可以通过ZwQuerySection()、即NtQuerySection()询问、获取这些信息。所获取的信息在数据结构Sii中,这是个SECTION_IMAGE_INFORMATION数据结构,其Subsystem字段表明了映像的模式。一个PE映像可以是GUI模式的、面向“视窗”和图像的应用,也可以是“控制台”、即CUI模式的面向命令行和字符的应用。但是二者必居其一,否则就错了。
然后,这里对一个局部量的数据结构PriorityClass进行了一些设置,设置的依据来自调用参数dwCreationFlags中的一些标志位
接着就是对NtCreateProcess()的调用了。不过刚才的PriorityClass