-
-
[原创]Windows内核之载入exe文件
-
发表于: 1天前 307
-
前言:
本文基于ReactOS 0.4.15源码。
在创建进程时会载入exe文件,分两步进行,首先是将可执行映像映射到用户空间,这一步可以理解为对虚拟内存地址的规划。在实际执行exe文件时会触发缺页中断,此时才会读取exe文件的内容,本文介绍的就是缺页中断的处理。
缺页中断的处理函数是MmNotPresentFaultSectionView,来看看这个函数。
Entry = MmGetPageEntrySectionSegment(Segment, &Offset);
if (Entry == 0)
{
/*
* If the entry is zero, then we need to load the page.
*/
if ((Offset.QuadPart >= (LONGLONG)PAGE_ROUND_UP(Segment->RawLength.QuadPart)) && (MemoryArea->VadNode.u.VadFlags.VadType == VadImageMap))
{
/* We are beyond the data which is on file. Just get a new page. */
MI_SET_USAGE(MI_USAGE_SECTION);
if (Process) MI_SET_PROCESS2(Process->ImageFileName);
if (!Process) MI_SET_PROCESS2("Kernel Section");
Status = MmRequestPageMemoryConsumer(MC_USER, FALSE, &Page);
if (!NT_SUCCESS(Status))
{
MmUnlockSectionSegment(Segment);
return STATUS_NO_MEMORY;
}
MmSetPageEntrySectionSegment(Segment, &Offset, MAKE_SSE(Page << PAGE_SHIFT, 1));
MmUnlockSectionSegment(Segment);
Status = MmCreateVirtualMapping(Process, PAddress, Attributes, Page);
if (!NT_SUCCESS(Status))
{
DPRINT1("Unable to create virtual mapping\n");
KeBugCheck(MEMORY_MANAGEMENT);
}
ASSERT(MmIsPagePresent(Process, PAddress));
if (Process)
MmInsertRmap(Page, Process, Address);
DPRINT("Address 0x%p\n", Address);
return STATUS_SUCCESS;
}
MmUnlockSectionSegment(Segment);
MmUnlockAddressSpace(AddressSpace);
/* The data must be paged in. Lock the file, so that the VDL doesn't get updated behind us. */
FsRtlAcquireFileExclusive(Segment->FileObject);
PFSRTL_COMMON_FCB_HEADER FcbHeader = Segment->FileObject->FsContext;
Status = MmMakeSegmentResident(Segment, Offset.QuadPart, PAGE_SIZE, &FcbHeader->ValidDataLength, FALSE);
FsRtlReleaseFile(Segment->FileObject);
/* Lock address space again */
MmLockAddressSpace(AddressSpace);
if (!NT_SUCCESS(Status))
{
if (Status == STATUS_NO_MEMORY)
{
return Status;
}
/* Damn */
DPRINT1("Failed to page data in!\n");
return STATUS_IN_PAGE_ERROR;
}
/* Everything went fine. Restart the operation */
return STATUS_MM_RESTART_OPERATION;
}首先调用MmGetPageEntrySectionSegment,此时返回的Entry为空,所以会进入if语句,此时会执行Mm函数MakeSegmentResident,这个函数内会读取exe文件的内容。之后返回STATUS_MM_RESTART_OPERATION,这会导致MmNotPresentFaultSectionView被再次调用,不过此时执行的路径就和第一次不同了。
/* We already have a page on this section offset. Map it into the process address space. */
Page = PFN_FROM_SSE(Entry);
Status = MmCreateVirtualMapping(Process,
PAddress,
Attributes,
Page);
if (!NT_SUCCESS(Status))
{
DPRINT1("Unable to create virtual mapping\n");
KeBugCheck(MEMORY_MANAGEMENT);
}
if (Process)
MmInsertRmap(Page, Process, Address);
/* Take a reference on it */
MmSharePageEntrySectionSegment(Segment, &Offset);
MmUnlockSectionSegment(Segment);
DPRINT("Address 0x%p\n", Address);
return STATUS_SUCCESS;这次执行会将物理内存Page和地址PAddress绑定起来,从而可以通过虚拟内存地址PAddress访问物理内存,这样就访问到exe文件内容了。其中的Page就是在MmMakeSegmentResident函数中读取到exe文件内容并写入到Page中的,来看看这个函数。
PMDL Mdl = IoAllocateMdl(NULL, ReadLength, FALSE, FALSE, NULL);
if (!Mdl)
{
/* Damn. Roll-back. */
MmLockSectionSegment(Segment);
while (ChunkOffset < ChunkEnd)
{
if (ToReadPageBits & 1)
{
LARGE_INTEGER CurrentOffset;
CurrentOffset.QuadPart = ChunkOffset;
ASSERT(MM_IS_WAIT_PTE(MmGetPageEntrySectionSegment(Segment, &CurrentOffset)));
MmSetPageEntrySectionSegment(Segment, &CurrentOffset, 0);
}
ToReadPageBits >>= 1;
ChunkOffset += PAGE_SIZE;
}
MmUnlockSectionSegment(Segment);
return STATUS_INSUFFICIENT_RESOURCES;
}
/* Get our pages */
PPFN_NUMBER Pages = MmGetMdlPfnArray(Mdl);
RtlZeroMemory(Pages, BYTES_TO_PAGES(ReadLength) * sizeof(PFN_NUMBER));
for (UINT i = 0; i < BYTES_TO_PAGES(ReadLength); i++)
{
Status = MmRequestPageMemoryConsumer(MC_USER, FALSE, &Pages[i]);
if (!NT_SUCCESS(Status))
{
/* Damn. Roll-back. */
for (UINT j = 0; j < i; j++)
MmReleasePageMemoryConsumer(MC_USER, Pages[j]);
goto Failed;
}
}在这个函数中,首先调用了IoAllocateMdl函数分配了Mdl,然后调用MmRequestPageMemoryConsumer函数分配了物理内存,注意此时分配了多个物理内存页面,而在缺页中断中只用了其中一个内存页面。分配的物理内存标号保存在Pages数组中,这个数组的位置就在Mdl数据结构的上方。
Status = IoPageRead(FileObject, Mdl, &FileOffset, &Event, &Iosb);
if (Status == STATUS_PENDING)
{
KeWaitForSingleObject(&Event, WrPageIn, KernelMode, FALSE, NULL);
Status = Iosb.Status;
}
if (Mdl->MdlFlags & MDL_MAPPED_TO_SYSTEM_VA)
{
MmUnmapLockedPages(Mdl->MappedSystemVa, Mdl);//解除绑定
}
for (UINT i = 0; i < BYTES_TO_PAGES(ReadLength); i++)
{
ULONG_PTR Entry = MAKE_SSE(Pages[i] << PAGE_SHIFT, 0);
LARGE_INTEGER CurrentOffset;
CurrentOffset.QuadPart = ChunkOffset + (i * PAGE_SIZE);
ASSERT(MM_IS_WAIT_PTE(MmGetPageEntrySectionSegment(Segment, &CurrentOffset)));
if (SetDirty)
Entry = DIRTY_SSE(Entry);
MmSetPageEntrySectionSegment(Segment, &CurrentOffset, Entry);
}然后调用IoPageRead函数去读取exe文件的内容,并等待读取的完成。最后将物理页面Pages封装为Entry,这一步完成就会使得MmNotPresentFaultSectionView执行第二条路径,在这个路径中从Entry中提取出Page,并完成映射。来看看IoPageRead函数。
NTSTATUS
NTAPI
IoPageRead(IN PFILE_OBJECT FileObject,
IN PMDL Mdl,
IN PLARGE_INTEGER Offset,
IN PKEVENT Event,
IN PIO_STATUS_BLOCK StatusBlock)
{
PIRP Irp;
PIO_STACK_LOCATION StackPtr;
PDEVICE_OBJECT DeviceObject;
IOTRACE(IO_API_DEBUG, "FileObject: %p. Mdl: %p. Offset: %p\n",
FileObject, Mdl, Offset);
/* Get the Device Object */
DeviceObject = IoGetRelatedDeviceObject(FileObject);
/* Allocate IRP */
Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
/* If allocation failed, try to see whether we can use
* the reserve IRP
*/
if (Irp == NULL)
{
/* We will use it only for paging file */
if (MmIsFileObjectAPagingFile(FileObject))
{
InterlockedExchangeAdd(&IoPageReadIrpAllocationFailure, 1);
Irp = IopAllocateReserveIrp(DeviceObject->StackSize);
}
else
{
InterlockedExchangeAdd(&IoPageReadNonPagefileIrpAllocationFailure, 1);
}
/* If allocation failed (not a paging file or too big stack size)
* Fail for real
*/
if (Irp == NULL)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
}
/* Get the Stack */
StackPtr = IoGetNextIrpStackLocation(Irp);
/* Create the IRP Settings */
Irp->MdlAddress = Mdl;
Irp->UserBuffer = MmGetMdlVirtualAddress(Mdl);
Irp->UserIosb = StatusBlock;
Irp->UserEvent = Event;
Irp->RequestorMode = KernelMode;
Irp->Flags = IRP_PAGING_IO |
IRP_NOCACHE |
IRP_SYNCHRONOUS_PAGING_IO |
IRP_INPUT_OPERATION;
Irp->Tail.Overlay.OriginalFileObject = FileObject;
Irp->Tail.Overlay.Thread = PsGetCurrentThread();
/* Set the Stack Settings */
StackPtr->Parameters.Read.Length = MmGetMdlByteCount(Mdl);
StackPtr->Parameters.Read.ByteOffset = *Offset;
StackPtr->MajorFunction = IRP_MJ_READ;
StackPtr->FileObject = FileObject;
/* Call the Driver */
return IoCallDriver(DeviceObject, Irp);
}这里就是简单的填写Irp的各个字段然后调用驱动去完成读取工作,注意这里的
Irp->MdlAddress = Mdl;
在驱动中会使用这个字段,那么exe文件的数据会读取到哪个地方呢?读取会调用MsfsRead函数。
if (Irp->MdlAddress) Buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); else Buffer = Irp->UserBuffer;
这里会调用if语句代码。
#define MmGetSystemAddressForMdlSafe(_Mdl, _Priority) \ (((_Mdl)->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA \ | MDL_SOURCE_IS_NONPAGED_POOL)) ? \ (_Mdl)->MappedSystemVa : \ (PVOID) MmMapLockedPagesSpecifyCache((_Mdl), \ KernelMode, MmCached, NULL, FALSE, (_Priority)))
这里是一个宏,会调用到MmMapLockedPagesSpecifyCache函数。
PointerPte = MiReserveSystemPtes(PageCount, SystemPteSpace);
Base = (PVOID)((ULONG_PTR)MiPteToAddress(PointerPte) + Mdl->ByteOffset);
do
{
//
// We're done here
//
if (*MdlPages == LIST_HEAD) break;
//
// Write the PTE
//
TempPte.u.Hard.PageFrameNumber = *MdlPages;
MI_WRITE_VALID_PTE(PointerPte++, TempPte);
} while (++MdlPages < LastPage);
return Base;首先获取指向Pte的指针,然后根据pte获取到虚拟内存地址Base,之后就使用这个Base访问物理内存。虚拟内存和物理内存的绑定实在do循环中进行的。
这样我们就知道了MsfsRead函数中的Buffer指向的就是Mdl上方的Pages数组中的物理页面,并且Buffer地址就是这里返回的Base。剩下的就简单了,只是向Buffer中写数据,继续看MsfsRead函数
memcpy(Buffer, &Message->Buffer, min(Message->Size,Length));
这样物理地址中就有exe文件的数据了。这里完成了虚拟内存和物理内存的绑定,必然就有地方释放,释放是在MmUnmapLockedPages函数中。
RtlZeroMemory(StartingPte, NumberOfPtes * sizeof(MMPTE));
将Pte位置值写0就清除了绑定。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。