📄
字号:
漫谈兼容内核之十三:关于“进程挂靠”
[align=center]毛德操[/align]
上一篇漫谈在介绍APC机制时提到:线程在Windows内核中运行时有时候需要暂时“挂靠(Attach)”到别的进程的用户空间,即暂时切换到另一个进程的用户空间。这称为“进程挂靠”,因为用户空间是一个进程最主要的特征。
显然,要是当前线程的操作与用户空间无关、不需要访问用户空间,那么当时的用户空间到底是谁的用户空间根本就无关紧要,所以这必定发生在与用户空间有关的操作中。
一般而言,如果线程T属于进程P,那么当这个线程在内核中运行时的用户空间应该就是进程P的用户空间,它也没有必要访问到别的进程的用户空间去。可是,Windows内核允许一些跨进程的操作,特别是跨用户空间的操作,所以有时候就需要把当时的用户空间切换到别的进程的用户空间,或者说挂靠到别的进程。在Windows中,一个进程实际上只是意味着一个用户(地址)空间,说一个线程属于某个进程的意思是它使用的是某个特定的用户空间,系统空间则是由所有线程共用的。那么“某个特定的用户空间”是什么意思呢?实质上就是一个具体的页面映射方案,或者一套具体的映射目录和页面表,以及相关的其它数据结构。而所谓“切换到某个进程的用户空间”,就是把这套具体的映射目录和页面表装入CPU中的页面映射机构,使其真正发生作用。当然,在完成了有关的操作以后还要回到原来的用户空间,否则就无法从内核“返回”自己的用户空间了。
然而究竟什么时候需要用到进程挂靠呢?最好还是通过一个实例来加以说明。
前几篇漫谈中说到,在启动一个PE格式的EXE映像运行时先要创建一个进程,然后把目标EXE映像和ntdll.dll的映像映射到新建进程的用户空间,并且在映射后的ntdll.dll映像中找到LdrInitializeThunk()等函数的入口。在这个过程中,当前线程属于作为创建者的那个进程,或“父进程”,而其部分操作的对象则在新建进程、即“子进程”的用户空间。所以此时就用到了进程挂靠,使当前线程挂靠到新建进程的用户空间。下面我们通过LdrpMapSystemDll()的代码来说明为什么有进程挂靠、以及怎样实现进程挂靠。
在创建进程的过程中要调用到一个函数LdrpMapSystemDll(),其作用是把“系统DLL”、即ntdll.dll映射到新建进程的用户空间,并从中获取几个重要函数的入口。当然,这是个内核函数,是在系统空间运行的。
[code]NTSTATUS LdrpMapSystemDll(HANDLE ProcessHandle, PVOID* LdrStartupAddr)
{
CHAR BlockBuffer [1024];
. . . . . .
UNICODE_STRING DllPathname =
ROS_STRING_INITIALIZER(L"\\SystemRoot\\system32\\ntdll.dll");
. . . . . .
/*
* Locate and open NTDLL to determine ImageBase
* and LdrStartup
*/
InitializeObjectAttributes(&FileObjectAttributes, &DllPathname, 0, NULL, NULL);
DPRINT("Opening NTDLL\n");
Status = ZwOpenFile(&FileHandle, FILE_READ_ACCESS, &FileObjectAttributes,
&Iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
. . . . . .
Status = ZwReadFile(FileHandle, 0, 0, 0, &Iosb, BlockBuffer, sizeof(BlockBuffer), 0, 0);
. . . . . .
. . . . . .
DosHeader = (PIMAGE_DOS_HEADER) BlockBuffer;
NTHeaders = (PIMAGE_NT_HEADERS) (BlockBuffer + DosHeader->e_lfanew);
. . . . . .
ImageBase = NTHeaders->OptionalHeader.ImageBase;
ImageSize = NTHeaders->OptionalHeader.SizeOfImage;
/*
* Create a section for NTDLL
*/
DPRINT("Creating section\n");
Status = ZwCreateSection(&NTDllSectionHandle, SECTION_ALL_ACCESS, NULL,
NULL, PAGE_READWRITE, SEC_IMAGE | SEC_COMMIT, FileHandle);
. . . . . .
ZwClose(FileHandle);
/*
* Map the NTDLL into the process
*/
ViewSize = 0;
ImageBase = 0;
Status = ZwMapViewOfSection(NTDllSectionHandle, ProcessHandle,
(PVOID*)&ImageBase, 0, ViewSize, NULL,
&ViewSize, 0, MEM_COMMIT, PAGE_READWRITE);
. . . . . .
. . . . . .
CurrentProcess = PsGetCurrentProcess();
if (Process != CurrentProcess)
{
DPRINT("Attaching to Process\n");
KeAttachProcess(&Process->Pcb);
}
/*
* retrieve ntdll's startup address
*/
if (SystemDllEntryPoint == NULL)
{
RtlInitAnsiString (&ProcedureName,
"LdrInitializeThunk");
Status = LdrGetProcedureAddress ((PVOID)ImageBase,
&ProcedureName,
0,
&SystemDllEntryPoint);
. . . . . .
*LdrStartupAddr = SystemDllEntryPoint;
}
. . . . . .
. . . . . .
if (Process != CurrentProcess)
{
KeDetachProcess();
}
ObDereferenceObject(Process);
ZwClose(NTDllSectionHandle);
return(STATUS_SUCCESS);
}[/code]
先看一下大致的流程:
通过InitializeObjectAttributes()设置好一个OBJECT_ATTRIBUTES数据结构FileObjectAttributes;然后用这个数据结构作为参数之一,通过系统调用ZwOpenFile()打开目标文件ntdll.dll。之所以如此,是因为ZwOpenFile()并不接受文件名作为参数,而必须把文件名放在OBJECT_ATTRIBUTES数据结构中。当然,这个数据结构中还有别的信息。
通过ZwReadFile()读入目标文件的开头1K字节,目的在于获取其DosHeader和NTHeaders,进而获取其NTHeaders->OptionalHeader中的ImageBase和SizeOfImage两项信息,前者是映像在文件中的起点,后者是映像的大小。
通过ZwCreateSection()为目标文件建立(并打开)一个Section对象。从逻辑的意义上,这个Section对象就与目标文件的内容划上了等号。
至此,目标文件已经可以关闭,因为不再需要通过文件读写等常规的文件操作访问这个文件了。
通过ZwMapViewOfSection()将已建立的Section、即目标文件的内容映射到目标进程的用户空间。
通过KeAttachProcess()将当前线程挂靠到目标进程。
通过LdrGetProcedureAddress()从已经映射到目标进程用户空间的映像中获取函数LdrInitializeThunk()的入口地址。
再通过LdrGetProcedureAddress()获取若干其它函数的入口地址。
通过KeDetachProcess()撤销挂靠,回到当前线程所属的进程。
关闭所创建的Section对象。
首先要说明,函数名以Zw开头的函数实际上就是以Nt开头的对应系统调用。以打开文件为例,在用户空间调用时要用NtOpenFile(),在内核中调用则用ZwOpenFile()。
显然,这个流程中的进程挂靠、即KeAttachProcess()和KeDetachProcess()、是因为要执行LdrGetProcedureAddress()而产生的需求。对此我们很自然地就会有两个问题:首先,为什么LdrGetProcedureAddress()需要进程挂靠?其次,既然LdrGetProcedureAddress()需要,那为什么ZwMapViewOfSection()倒又不需要?二者不是都涉及目标进程的用户空间吗?
要回答这两个问题,就得近一步深入到这两个函数的代码中。
如前所述,系统调用NtCreateSection()在内核中创建一个Section对象,并使这个对象与一个(已经打开的)目标文件挂上勾,此后就可以通过另一个系统调用NtMapViewOfSection()将目标文件的部分或全部内容映射到某个用户空间(Section可以为多个进程共享,分别映射到不同空间的相同或不同地址上)。
下面先看NtMapViewOfSection()。
[code][LdrpMapSystemDll() > NtMapViewOfSection()]
NTSTATUS STDCALL
NtMapViewOfSection(IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress OPTIONAL,
IN ULONG ZeroBits OPTIONAL,
IN ULONG CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PULONG ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType OPTIONAL,
IN ULONG Protect)
{
PVOID SafeBaseAddress;
LARGE_INTEGER SafeSectionOffset;
ULONG SafeViewSize;
PSECTION_OBJECT Section;
PEPROCESS Process;
KPROCESSOR_MODE PreviousMode;
PMADDRESS_SPACE AddressSpace;
NTSTATUS Status = STATUS_SUCCESS;
PreviousMode = ExGetPreviousMode();
if(PreviousMode != KernelMode)
{
. . . . . .
}
else
{
SafeBaseAddress = (BaseAddress != NULL ? *BaseAddress : NULL);
SafeSectionOffset.QuadPart = (SectionOffset != NULL ? SectionOffset->QuadPart : 0);
SafeViewSize = (ViewSize != NULL ? *ViewSize : 0);
}
. . . . . .
AddressSpace = &Process->AddressSpace;
. . . . . .
Status = MmMapViewOfSection(Section,
Process,
(BaseAddress != NULL ? &SafeBaseAddress : NULL),
ZeroBits,
CommitSize,
(SectionOffset != NULL ? &SafeSectionOffset : NULL),
(ViewSize != NULL ? &SafeViewSize : NULL),
InheritDisposition,
AllocationType,
Protect);
. . . . . .
return(Status);
}[/code]
参数SectionHandle代表着一个Section对象,ProcessHandle则代表着一个用户空间,BaseAddress是要求装入的地址,而SectionOffset是目标文件中的起点。还有个参数Protect是对映射后的内存区间(而不是目标文件)的访问保护,在这里是PAGE_READWRITE。
显然,实际的操作是由MmMapViewOfSection()完成的,函数名中的前缀Mm表示这个函数属于内存管理。
[code][LdrpMapSystemDll() > NtMapViewOfSection() > MmMapViewOfSection()]
NTSTATUS STDCALL
MmMapViewOfSection(IN PVOID SectionObject, ……)
{
. . . . . .
PMADDRESS_SPACE AddressSpace;
. . . . . .
Section = (PSECTION_OBJECT)SectionObject;
AddressSpace = &Process->AddressSpace;
MmLockAddressSpace(AddressSpace);
if (Section->AllocationAttributes & SEC_IMAGE)
{
ULONG i;
ULONG NrSegments;
ULONG_PTR ImageBase;
ULONG ImageSize;
PMM_IMAGE_SECTION_OBJECT ImageSectionObject;
PMM_SECTION_SEGMENT SectionSegments;
ImageSectionObject = Section->ImageSection;
SectionSegments = ImageSectionObject->Segments;
NrSegments = ImageSectionObject->NrSegments;
ImageBase = (ULONG_PTR)*BaseAddress;
if (ImageBase == 0)
{
ImageBase = ImageSectionObject->ImageBase;
}
ImageSize = 0;
for (i = 0; i < NrSegments; i++)
{
if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD))
{
ULONG_PTR MaxExtent;
MaxExtent = (ULONG_PTR)SectionSegments[i].VirtualAddress +
SectionSegments[i].Length;
ImageSize = max(ImageSize, MaxExtent);
}
}
/* Check there is enough space to map the section at that point. */
if (MmLocateMemoryAreaByRegion(AddressSpace, (PVOID)ImageBase,
PAGE_ROUND_UP(ImageSize)) != NULL)
{
. . . . . .
/* Otherwise find a gap to map the image. */
ImageBase = (ULONG_PTR)MmFindGap(AddressSpace,
PAGE_ROUND_UP(ImageSize), PAGE_SIZE, FALSE);
. . . . . .
}
for (i = 0; i < NrSegments; i++)
{
if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD))
{
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -