本篇主要讲述进程的启动过程、线程的调度与切换、进程挂靠
进程的启动过程:
BOOL CreateProcess
(
LPCTSTR , //
LPTSTR , // command line string
LPSECURITY_ATTRIBUTES , // SD
LPSECURITY_ATTRIBUTES , // SD
BOOL , //
DWORD , // creation flags
LPVOID , // new environment block
LPCTSTR , // current directory name
LPSTARTUPINFO , // startup information
LPPROCESS_INFORMATION // process information
);
这个Win32API在内部最终调用如下:
CreateProcess(…)
{
…
NtCreateProcess(…);//间接调用这个系统服务,先创建进程
NtCreateThread(…);//间接调用这个系统服务,再创建该进程的第一个线程(也即主线程)
…
}
进程的4GB地址空间分两部分,内核空间+用户空间
看下面几个定义:
#define MmSystemRangeStart 0x80000000 //系统空间的起点
#define MM_USER_PROB_ADDRESS MmSystemRangeStart-64kb //除去高端的64kb隔离区
#define MM_HIGHEST_USER_ADDRESS MmUserProbAddress-1 //实际的用户空间中最高可访问地址
#define MM_LOWEST_USER_ADDRESS 64kb //实际的用户空间中最低可访问地址
#define KI_USER_SHARED_DATA 0xffdf0000 //内核空间与用户空间共享的一块区域
由此可见,用户地址空间的范围实际上是从 64kb---->0x80000000-64kb 这块区域。
(访问NULL指针报异常的原因就是NULL(0)落在了最前面的64kb保留区中)
内核中提供了一个全局结构变量,该结构的类型是KUSER_SHARED_DATA。内核中的那个结构体变量所在的虚拟页面起始地址为:0xffdf0000,大小为一个页面大小。这个内核页面对应的物理内存页面也映射到了每个进程的用户地址空间中,而且是固定映在同一处:0x7ffe0000。这样,用户空间的程序直接访问用户空间中的这个虚拟地址,就相当于直接访问了内核空间中的那个公共页面。所以,那个内核页面称之为内核空间提供给各个进程的一块共享之地。(事实上,这个公共页面非常有用,可以在这个页面中放置代码,应用程序直接在r3层运行这些代码,如在内核中进行IAT hook)
如上:【用户空间的范围就是低2GB的空间除去前后64kb后的那块区域】
圈定了用户空间的地皮后,现在就到了划分用户空间的时候了。
用户空间的布局:(以区段(Area)为单位进行划分)
NTSTATUS
MmInitializeProcessAddressSpace(IN PEPROCESS Process,IN PVOID Section,IN OUT PULONG Flags)
{
NTSTATUS Status = STATUS_SUCCESS;
SIZE_T ViewSize = 0;
PVOID ImageBase = 0;
PROS_SECTION_OBJECT SectionObject = Section;
USHORT Length = 0;
…
KeAttachProcess(&Process->Pcb);//必须将当前线程挂靠到子进程的地址空间
Process->AddressSpaceInitialized = 2;
if (SectionObject)
{
FileName = SectionObject->FileObject->FileName;
Source = (PWCHAR)((PCHAR)FileName.Buffer + FileName.Length);
if (FileName.Buffer)
{
while (Source > FileName.Buffer)
{
if (*--Source ==L”\\”)
{
Source++;
break;
}
else
Length++;
}
}
Destination = Process->ImageFileName;//任务管理器显示的进程名就是这个(大小写相同)
Length = min(Length, sizeof(Process->ImageFileName) - 1);
while (Length--) *Destination++ = (UCHAR)*Source++;
*Destination = ’\0’;
//将进程的exe文件映射到地址空间中
Status = MmMapViewOfSection(Section,Process,&ImageBase,0,0,NULL,&ViewSize,0,
MEM_COMMIT,…);
Process->SectionBaseAddress = ImageBase;//记录实际映射到的地址(一般为0x00400000)
}
KeDetachProcess();//撤销挂靠
return Status;
}
//上面的函数将进程的exe文件映射到用户地址空间中(注意exe文件内部是分开按节映射)
NTSTATUS PspMapSystemDll(PEPROCESS Process,PVOID *DllBase,BOOLEAN UseLargePages)
{
LARGE_INTEGER Offset = {{0, 0}};
SIZE_T ViewSize = 0; PVOID ImageBase = 0;
//将NTDLL.dll文件映射到地址空间(每个NTDLL.dll事实上都映射到所有进程地址空间的同一处)
Status = MmMapViewOfSection(PspSystemDllSection,Process,&ImageBase,0,0,&Offset,&ViewSize,
ViewShare,0,…);
if (DllBase) *DllBase = ImageBase;
return Status;
}
上面这个函数将ntdll.dll映射到地址空间
下面这个函数创建该进程的PEB
NTSTATUS MmCreatePeb(PEPROCESS Process,PINITIAL_PEB InitialPeb,OUT PPEB *BasePeb)
{
PPEB Peb = NULL;
SIZE_T ViewSize = 0;
PVOID TableBase = NULL;
KAFFINITY ProcessAffinityMask = 0;
SectionOffset.QuadPart = (ULONGLONG)0;
*BasePeb = NULL;
KeAttachProcess(&Process->Pcb);//因为PEB指针是子进程中的地址,所以要挂靠
//创建一个PEB
Status = MiCreatePebOrTeb(Process, sizeof(PEB), (PULONG_PTR)&Peb);
RtlZeroMemory(Peb, sizeof(PEB));
//根据传入的InitialPeb参数初始化新建的peb
Peb->InheritedAddressSpace = InitialPeb->InheritedAddressSpace;
Peb->Mutant = InitialPeb->Mutant;
Peb->ImageUsesLargePages = InitialPeb->ImageUsesLargePages;
Peb->ImageBaseAddress = Process->SectionBaseAddress;//
Peb->OSMajorVersion = NtMajorVersion; Peb->OSMinorVersion = NtMinorVersion;
Peb->OSBuildNumber = (USHORT)(NtBuildNumber & 0x3FFF);
Peb->OSPlatformId = 2; /* VER_PLATFORM_WIN32_NT */
Peb->OSCSDVersion = (USHORT)CmNtCSDVersion;
Peb->NumberOfProcessors = KeNumberProcessors;
//经典的两个调试检测标志
Peb->BeingDebugged = (BOOLEAN)(Process->DebugPort != NULL ? TRUE : FALSE);
Peb->NtGlobalFlag = NtGlobalFlag;
Peb->MaximumNumberOfHeaps = (PAGE_SIZE - sizeof(PEB)) / sizeof(PVOID);
Peb->ProcessHeaps = (PVOID*)(Peb + 1);//PEB结构体后面是一个堆数组
NtHeaders = RtlImageNtHeader(Peb->ImageBaseAddress);//获取文件头中的NT头
Characteristics = NtHeaders->FileHeader.Characteristics;
if (NtHeaders)
{
_SEH2_TRY
{
ImageConfigData = RtlImageDirectoryEntryToData(Peb->ImageBaseAddress,TRUE,
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG,&ViewSize);
Peb->ImageSubsystem = NtHeaders->OptionalHeader.Subsystem;
Peb->ImageSubsystemMajorVersion = NtHeaders->OptionalHeader.MajorSubsystemVersion;
Peb->ImageSubsystemMinorVersion = NtHeaders->OptionalHeader.MinorSubsystemVersion;
if (NtHeaders->OptionalHeader.Win32VersionValue)
{
Peb->OSMajorVersion = NtHeaders->OptionalHeader.Win32VersionValue & 0xFF;
Peb->OSMinorVersion = (NtHeaders->OptionalHeader.Win32VersionValue >> 8) & 0xFF;
Peb->OSBuildNumber = (NtHeaders->OptionalHeader.Win32VersionValue >> 16) & 0x3FFF;
Peb->OSPlatformId = (NtHeaders->OptionalHeader.Win32VersionValue >> 30) ^ 2;
}
if (ImageConfigData != NULL)
{
ProbeForRead(ImageConfigData,sizeof(IMAGE_LOAD_CONFIG_DIRECTORY),
sizeof(ULONG));//读取pe文件中的加载配置信息
if (ImageConfigData->CSDVersion)
Peb->OSCSDVersion = ImageConfigData->CSDVersion;
if (ImageConfigData->ProcessAffinityMask)
ProcessAffinityMask = ImageConfigData->ProcessAffinityMask;
}
if (Characteristics & IMAGE_FILE_UP_SYSTEM_ONLY)
Peb->ImageProcessAffinityMask = 0;
else
Peb->ImageProcessAffinityMask = ProcessAffinityMask;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
KeDetachProcess();
_SEH2_YIELD(return STATUS_INVALID_IMAGE_PROTECT);
}
_SEH2_END;
}
KeDetachProcess();
*BasePeb = Peb;
return STATUS_SUCCESS;
}
如上,上面这个函数为进程创建一个PEB并根据exe文件头中的某些信息初始化里面的某些字段
事实上,这个PEB结构体的地址固定安排在0x7FFDF000处,占据一个页面大小。该页中这个PEB结构体后面就是一个堆数组,存放该进程中创建的所有堆。
下面的函数为子进程分配一个参数块(即创建参数)和环境变量块(即环境变量字符串)
NTSTATUS
BasepInitializeEnvironment(HANDLE ProcessHandle,PPEB Peb,//子进程的Peb
LPWSTR ApplicationPathName,LPWSTR lpCurrentDirectory,
LPWSTR lpCommandLine,
LPVOID lpEnvironment,//传给子进程的环境变量块
SIZE_T EnvSize,//环境变量块的大小
LPSTARTUPINFOW StartupInfo,DWORD CreationFlags,
BOOL InheritHandles)
{
PRTL_USER_PROCESS_PARAMETERS RemoteParameters = NULL;
PPEB OurPeb = NtCurrentPeb();//当前进程(即父进程)的Peb
LPVOID Environment = lpEnvironment;
RetVal = GetFullPathNameW(ApplicationPathName, MAX_PATH,FullPath,&Remaining);
RtlInitUnicodeString(&ImageName, FullPath);
RtlInitUnicodeString(&CommandLine, lpCommandLine);
RtlInitUnicodeString(&CurrentDirectory, lpCurrentDirectory);
if (StartupInfo->lpTitle)
RtlInitUnicodeString(&Title, StartupInfo->lpTitle);
else
RtlInitUnicodeString(&Title, L"");
Status = RtlCreateProcessParameters(&ProcessParameters,&ImageName,
lpCurrentDirectory ?&CurrentDirectory : NULL,
&CommandLine,Environment,&Title,..);
if (Environment)
Environment = ScanChar = ProcessParameters->Environment;
else
Environment = ScanChar = OurPeb->ProcessParameters->Environment;
if (ScanChar)
{
EnviroSize =CalcEnvSize(ScanChar);//计算环境变量块的长度
Size = EnviroSize;
//为子进程分配一个环境变量块(跨进程远程分配内存)
Status = ZwAllocateVirtualMemory(ProcessHandle,
(PVOID*)&ProcessParameters->Environment,
0,&Size,MEM_COMMIT,PAGE_READWRITE);
//将环境变量块复制到子进程的空间中
ZwWriteVirtualMemory(ProcessHandle,ProcessParameters->Environment,
Environment,
EnviroSize,
NULL);
}
ProcessParameters->StartingX = StartupInfo->dwX;
ProcessParameters->StartingY = StartupInfo->dwY;
ProcessParameters->CountX = StartupInfo->dwXSize;
ProcessParameters->CountY = StartupInfo->dwYSize;
ProcessParameters->CountCharsX = StartupInfo->dwXCountChars;
ProcessParameters->CountCharsY = StartupInfo->dwYCountChars;
ProcessParameters->FillAttribute = StartupInfo->dwFillAttribute;
ProcessParameters->WindowFlags = StartupInfo->dwFlags;
ProcessParameters->ShowWindowFlags = StartupInfo->wShowWindow;
if (StartupInfo->dwFlags & STARTF_USESTDHANDLES)//让子进程使用自定义的三个标准IO句柄
{ //经常用于匿名管道重定向
ProcessParameters->StandardInput = StartupInfo->hStdInput;
ProcessParameters->StandardOutput = StartupInfo->hStdOutput;
ProcessParameters->StandardError = StartupInfo->hStdError;
}
if (CreationFlags & DETACHED_PROCESS)
ProcessParameters->ConsoleHandle = HANDLE_DETACHED_PROCESS;
else if (CreationFlags & CREATE_NO_WINDOW)
ProcessParameters->ConsoleHandle = HANDLE_CREATE_NO_WINDOW;
else if (CreationFlags & CREATE_NEW_CONSOLE)
ProcessParameters->ConsoleHandle = HANDLE_CREATE_NEW_CONSOLE;
else
{
//让子进程继承父进程的控制台句柄
ProcessParameters->ConsoleHandle = OurPeb->ProcessParameters->ConsoleHandle;
//让子进程继承父进程的三个标准句柄
if (!(StartupInfo->dwFlags &
(STARTF_USESTDHANDLES | STARTF_USEHOTKEY | STARTF_SHELLPRIVATE)))
{
BasepCopyHandles(ProcessParameters,OurPeb->ProcessParameters,InheritHandles);
}
}
Size = ProcessParameters->Length;//参数块本身的长度
//在子进程中分配一个参数块
Status = NtAllocateVirtualMemory(ProcessHandle,&RemoteParameters,0,&Size,
MEM_COMMIT,PAGE_READWRITE);
ProcessParameters->MaximumLength = Size;
//在子进程中分配一个参数块
Status = NtWriteVirtualMemory(ProcessHandle,RemoteParameters,ProcessParameters,
ProcessParameters->Length,NULL);
//将参数块复制到子进程的地址空间中
Status = NtWriteVirtualMemory(ProcessHandle,
&Peb->ProcessParameters,
&RemoteParameters,
sizeof(PVOID),
NULL);
RtlDestroyProcessParameters(ProcessParameters);
return STATUS_SUCCESS;
}
下面的函数创建第一个线程的(用户栈、内核栈、初始内核栈帧)
HANDLE
BasepCreateFirstThread(HANDLE ProcessHandle,LPSECURITY_ATTRIBUTES lpThreadAttributes,
PSECTION_IMAGE_INFORMATION SectionImageInfo,PCLIENT_ID ClientId)
{
BasepCreateStack(ProcessHandle,
SectionImageInfo->MaximumStackSize,//默认为1MB
SectionImageInfo->CommittedStackSize,//默认为4kb
&InitialTeb);//创建(即分配)该线程的用户栈
BasepInitializeContext(&Context,
NtCurrentPeb(),//赋给context.ebx
SectionImageInfo->TransferAddress,//赋给context.eax(也即oep)
InitialTeb.StackBase,// 赋给context.esp
0);//0表示是主线程的用户空间总入口
ObjectAttributes = BasepConvertObjectAttributes(&LocalObjectAttributes,
lpThreadAttributes,NULL);
Status = NtCreateThread(&hThread,THREAD_ALL_ACCESS,ObjectAttributes,ProcessHandle,
ClientId,&Context,&InitialTeb,TRUE);
Status = BasepNotifyCsrOfThread(hThread, ClientId);//通知csrss进程线程创建通知
return hThread;
}
下面的函数用来分配一个用户栈(每个线程都要分配一个)【栈底、栈顶、提交界】
NTSTATUS
BasepCreateStack(HANDLE hProcess,
SIZE_T StackReserve,//栈的保留大小。默认为1MB
SIZE_T StackCommit,//初始提交大小。默认为4KB,一个页面
OUT PINITIAL_TEB InitialTeb)//用来构造初始teb
{
ULONG_PTR Stack = NULL;
BOOLEAN UseGuard = FALSE;
Status = NtQuerySystemInformation(SystemBasicInformation,&SystemBasicInfo,
sizeof(SYSTEM_BASIC_INFORMATION),NULL);
if (hProcess == NtCurrentProcess())
{
Headers = RtlImageNtHeader(NtCurrentPeb()->ImageBaseAddress);
StackReserve = (StackReserve) ?
StackReserve : Headers->OptionalHeader.SizeOfStackReserve;
StackCommit = (StackCommit) ?
StackCommit : Headers->OptionalHeader.SizeOfStackCommit;
}
else
{
StackReserve = (StackReserve) ? StackReserve :SystemBasicInfo.AllocationGranularity;
StackCommit = (StackCommit) ? StackCommit : SystemBasicInfo.PageSize;
}
//栈的区段长度对齐64kb
StackReserve = ROUND_UP(StackReserve, SystemBasicInfo.AllocationGranularity);
StackCommit = ROUND_UP(StackCommit, SystemBasicInfo.PageSize);
//预定这么大小的栈(默认1MB)
Status = ZwAllocateVirtualMemory(hProcess, (PVOID*)&Stack,0,&StackReserve,MEM_RESERVE,
PAGE_READWRITE);
InitialTeb->AllocatedStackBase = (PVOID)Stack;//栈区段的分配基址
InitialTeb->StackBase = (PVOID)(Stack + StackReserve);//栈底
Stack += StackReserve - StackCommit;
if (StackReserve > StackCommit)
{
UseGuard = TRUE;
Stack -= SystemBasicInfo.PageSize;
StackCommit += SystemBasicInfo.PageSize; //多提交一个保护页
}
//初始提交这么大小的页面(也就是最常见的一个页外加一个保护页的大小)
Status = ZwAllocateVirtualMemory(hProcess, (PVOID*)&Stack,0,&StackCommit,MEM_COMMIT,
PAGE_READWRITE);
InitialTeb->StackLimit = (PVOID)Stack;// StackLimit表示第一个尚未提交页的边界
if (UseGuard)
{
SIZE_T GuardPageSize = SystemBasicInfo.PageSize;
Status = ZwProtectVirtualMemory(hProcess, (PVOID*)&Stack,&GuardPageSize,
PAGE_GUARD | PAGE_READWRITE);//改为PAGE_GUARD属性
InitialTeb->StackLimit = (PVOID)((ULONG_PTR)InitialTeb->StackLimit - GuardPageSize);
}
return STATUS_SUCCESS;
}
下面这个函数构造该线程的初始寄存器上下文
VOID
BasepInitializeContext(IN PCONTEXT Context,IN PVOID Parameter,IN PVOID StartAddress,
IN PVOID StackAddress,IN ULONG ContextType)
{
Context->Eax = (ULONG)StartAddress;//oep或用户指定的线程入口函数
Context->Ebx = (ULONG)Parameter;//peb
Context->Esp = (ULONG)StackAddress;//栈底就是初始栈顶
Context->SegFs = KGDT_R3_TEB | RPL_MASK;//fs指向TEB
Context->SegEs = KGDT_R3_DATA | RPL_MASK;
Context->SegDs = KGDT_R3_DATA | RPL_MASK;
Context->SegCs = KGDT_R3_CODE | RPL_MASK;
Context->SegSs = KGDT_R3_DATA | RPL_MASK;
Context->SegGs = 0;
Context->EFlags = 0x3000; // IOPL 3
if (ContextType == 1)
Context->Eip = (ULONG)BaseThreadStartupThunk; //普通线程的用户空间总入口
else if (ContextType == 2) //纤程
Context->Eip = (ULONG)BaseFiberStartup;
else
Context->Eip = (ULONG)BaseProcessStartThunk; //主线程的用户空间总入口
Context->ContextFlags = CONTEXT_FULL;//所有字段全部有效
Context->Esp -= sizeof(PVOID);//腾出参数空间
}
当线程创建起来后,会紧跟着创建它的teb。现在暂时不看NtCreateThread是怎样实现的,看一下teb的创建过程。
NTSTATUS
MmCreateTeb(IN PEPROCESS Process,
IN PCLIENT_ID ClientId,//线程的客户id即【进程id.线程id】
IN PINITIAL_TEB InitialTeb,
OUT PTEB *BaseTeb)//返回teb的地址
{
NTSTATUS Status = STATUS_SUCCESS;
*BaseTeb = NULL;
KeAttachProcess(&Process->Pcb);//挂靠到子进程地址空间
Status = MiCreatePebOrTeb(Process, sizeof(TEB), (PULONG_PTR)&Teb);//从peb处往低地址端搜索
_SEH2_TRY
{
RtlZeroMemory(Teb, sizeof(TEB));
Teb->NtTib.ExceptionList = -1;//初始是没有seh
Teb->NtTib.Self = (PNT_TIB)Teb;//将self指向指针结构的地址,方便寻址
Teb->NtTib.Version = 30 << 8;
Teb->ClientId = *ClientId;
Teb->RealClientId = *ClientId;
Teb->ProcessEnvironmentBlock = Process->Peb;//关键,teb中有个指针指向peb
Teb->CurrentLocale = PsDefaultThreadLocaleId;
if ((InitialTeb->PreviousStackBase == NULL) &&
(InitialTeb->PreviousStackLimit == NULL))
{
Teb->NtTib.StackBase = InitialTeb->StackBase;//栈底
Teb->NtTib.StackLimit = InitialTeb->StackLimit;//提交边界(最近未提交页的地址)
Teb->DeallocationStack = InitialTeb->AllocatedStackBase;
}
else
{
Teb->NtTib.StackBase = InitialTeb->PreviousStackBase;
Teb->NtTib.StackLimit = InitialTeb->PreviousStackLimit;
}
Teb->StaticUnicodeString.MaximumLength = sizeof(Teb->StaticUnicodeBuffer);
Teb->StaticUnicodeString.Buffer = Teb->StaticUnicodeBuffer;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
KeDetachProcess();
*BaseTeb = Teb;
return Status;
}
这样,经过以上的操作后,进程用户空间的典型布局就定出来了。
----------------------------------------------------------------------------------------->
64kb 64kb 64kb 一般1MB 一般在0x00400000处 n*4kb 4kb 4kb
禁区|环境变量块|参数块|主线程的栈|其它空间|exe文件各个节|其他空间|各teb|peb|内核用户共享区|
--------------------------->
60kb 64kb 0x80000000开始
无效区|隔离区|系统空间…
用户空间的布局:一句口诀【环、参、栈、文、堆、t、p】
进程的创建:
NTSTATUS
NtCreateProcess(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess,
IN BOOLEAN InheritObjectTable,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL)
{
ULONG Flags = 0;
if ((ULONG)SectionHandle & 1) Flags = PS_REQUEST_BREAKAWAY;
if ((ULONG)DebugPort & 1) Flags |= PS_NO_DEBUG_INHERIT;
if (InheritObjectTable) Flags |= PS_INHERIT_HANDLES;
return NtCreateProcessEx(ProcessHandle,
DesiredAccess,
ObjectAttributes,
ParentProcess,
Flags,
SectionHandle,
DebugPort,
ExceptionPort,
FALSE);
}
NTSTATUS
NtCreateProcessEx(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess,
IN ULONG Flags,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL,
IN BOOLEAN InJob)
{
if (!ParentProcess)
Status = STATUS_INVALID_PARAMETER;
else
{
Status = PspCreateProcess(ProcessHandle,
DesiredAccess,
ObjectAttributes,
ParentProcess,
Flags,
SectionHandle,
DebugPort,
ExceptionPort,
InJob);
}
return Status;
}
如上,CreateProcess API调用NtCreateProcess系统服务,最终会调用下面的函数完成进程的创建工作
NTSTATUS
PspCreateProcess(OUT PHANDLE ProcessHandle,//返回子进程的句柄
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess OPTIONAL,//父进程可以是任意第三方进程
IN ULONG Flags,
IN HANDLE SectionHandle OPTIONAL,//exe文件的section对象
IN HANDLE DebugPort OPTIONAL,//调试器进程中某个线程的调试端口
IN HANDLE ExceptionPort OPTIONAL)
{
ULONG DirectoryTableBase[2] = {0,0};//为子进程分配的页目录所在的物理页面地址
PETHREAD CurrentThread = PsGetCurrentThread();
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PEPROCESS CurrentProcess = PsGetCurrentProcess();
PACCESS_STATE AccessState = &LocalAccessState;//记录着当前线程的令牌和申请的访问权限
BOOLEAN NeedsPeb = FALSE;//表示是否需要为其创建一个peb,绝大多数都要
if (ParentProcess)//事实上只有”system”进程没有父进程
{
Status = ObReferenceObjectByHandle(ParentProcess,
PROCESS_CREATE_PROCESS,//表示要为其创建子进程
PsProcessType,PreviousMode, (PVOID*)&Parent);
Affinity = Parent->Pcb.Affinity;//继承父进程的cpu亲缘性
}
else
{
Parent = NULL;
Affinity = KeActiveProcessors;
}
MinWs = PsMinimumWorkingSet;
MaxWs = PsMaximumWorkingSet;
//关键。创建该进程的内核对象结构
Status = ObCreateObject(PreviousMode,
PsProcessType,//进程对象类型
ObjectAttributes,PreviousMode,NULL,
sizeof(EPROCESS),//内核进程对象
0,0, (PVOID*)&Process);
RtlZeroMemory(Process, sizeof(EPROCESS));
InitializeListHead(&Process->ThreadListHead);//初始时该进程尚无任何线程
PspInheritQuota(Process, Parent);//继承父进程的资源配额块
ObInheritDeviceMap(Parent, Process);//继承父进程的磁盘卷设备位图
if (Parent)
Process->InheritedFromUniqueProcessId = Parent->UniqueProcessId;//记录父进程的pid
if (SectionHandle)//exe文件的section,一般都有
{
//获得对应的section对象
Status = ObReferenceObjectByHandle(SectionHandle,SECTION_MAP_EXECUTE,
MmSectionObjectType,PreviousMode,
(PVOID*)&SectionObject);
}
Else …
Process->SectionObject = SectionObject;//记录该进程的exe文件section
if (DebugPort)//由调试器启动的子进程,都会传递一个调试端口给子进程
{
Status = ObReferenceObjectByHandle(DebugPort,
DEBUG_OBJECT_ADD_REMOVE_PROCESS,
DbgkDebugObjectType,PreviousMode,
(PVOID*)&DebugObject);
//每个被调进程与调试器中的一个调试器线程通过一个调试端口连接,形成一个调试会话
Process->DebugPort = DebugObject; //可用于检测调试
if (Flags & PS_NO_DEBUG_INHERIT)//指示不可将调试端口再继承给它的子进程
InterlockedOr((PLONG)&Process->Flags, PSF_NO_DEBUG_INHERIT_BIT);
}
else
{
if (Parent)
DbgkCopyProcessDebugPort(Process, Parent);//继承父进程的调试端口
}
if (ExceptionPort)
{
Status = ObReferenceObjectByHandle(ExceptionPort,PORT_ALL_ACCESS,LpcPortObjectType,
PreviousMode, (PVOID*)&ExceptionPortObject);
Process->ExceptionPort = ExceptionPortObject;
}
Process->ExitStatus = STATUS_PENDING;//默认的退出码
if (Parent)
{
/*创建页目录和内核部分的页表,然后从系统公共的内核页表中复制内核空间中的那些页表项(这样,每个进程的内核地//址空间的映射就相同了)*/
MmCreateProcessAddressSpace(MinWs,Process,DirectoryTableBase)
}
Else …
InterlockedOr((PLONG)&Process->Flags, PSF_HAS_ADDRESS_SPACE_BIT);
Process->Vm.MaximumWorkingSetSize = MaxWs;
//初始化进程对象的内部结构成员
KeInitializeProcess(&Process->Pcb,PROCESS_PRIORITY_NORMAL,Affinity,DirectoryTableBase);
Status = PspInitializeProcessSecurity(Process, Parent);//继承父进程的令牌
Process->PriorityClass = PROCESS_PRIORITY_CLASS_NORMAL;//初始创建时都是普通优先级类
Status = STATUS_SUCCESS;
if (SectionHandle) //一般都有
{
//初始化地址空间并将exe文件映射到用户空间中
Status = MmInitializeProcessAddressSpace(Process,SectionObject,&Flags,ImageFileName);
NeedsPeb = TRUE;
}
Else …
if (SectionObject)//映射(即加载)exe文件后,再映射ntdll.dll到用户空间(事实上固定映到某处)
PspMapSystemDll(Process, NULL, FALSE);
CidEntry.Object = Process;
CidEntry.GrantedAccess = 0;
//进程id、线程id实际上都是全局PspCidTable句柄表中的句柄,他们也指向对应的对象
Process->UniqueProcessId = ExCreateHandle(PspCidTable, &CidEntry);//分配pid进程号
Process->ObjectTable->UniqueProcessId = Process->UniqueProcessId;
if ((Parent) && (NeedsPeb))//用户空间中的进程都会分配一个peb,且固定在某处
{
RtlZeroMemory(&InitialPeb, sizeof(INITIAL_PEB));
InitialPeb.Mutant = (HANDLE)-1;
if (SectionHandle)
Status = MmCreatePeb(Process, &InitialPeb, &Process->Peb);//创建peb(固定在某处)
Else …
}
/*将进程加入全局的“活动进程链表”中,这个链表仅供系统统计用,因此可以恣意篡改,如隐藏进程。任务管理器等其他绝大多数进程枚举工具内部就是遍历的这个进程链表*/
InsertTailList(&PsActiveProcessHead, &Process->ActiveProcessLinks);
//这个函数用来将进程对象插入句柄表,返回一个进程句柄
Status = ObInsertObject(Process,AccessState,DesiredAccess,1,NULL,&hProcess);
//根据进程的优先级类计算该进程的基本优先级和时间片(初始创建时作为后台进程)
Process->Pcb.BasePriority =PspComputeQuantumAndPriority(Process,
PsProcessPriorityBackground,&Quantum);
Process->Pcb.QuantumReset = Quantum;
KeQuerySystemTime(&Process->CreateTime);//记录进程的创建时间
PspRunCreateProcessNotifyRoutines(Process, TRUE);//发出一个进程创建通知消息
*ProcessHandle = hProcess;//返回对应的句柄
return Status;
}
上面的NtCreateProcess、PspCreateProces只是创建了一个进程(它的内核对象、地址空间等),进程本身是不能运行的,所以CreateProcess API最终还会调用NtCreateThread创建并启动主线程。
线程从运行空间角度看,分为两种线程:
1、 用户线程(主线程和CreateThread创建的普通线程都是用户线程):线程部分代码运行在用户空间
2、 内核线程(由驱动程序调用PsCreateSystemThread创建的线程):线程的所有代码运行在内核空间
两种线程的运行路径分别为:
1、 KiThreadStartup->PspUserThreadStartup->用户空间中的公共入口->映像文件中的入口
2、 KiThreadStartup->PspSystemThreadStartup->内核空间中用户指定的入口
下面是每个用户线程的启动流程
NTSTATUS
NtCreateThread(OUT PHANDLE ThreadHandle,//返回线程句柄
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,//目标进程
OUT PCLIENT_ID ClientId,//返回pid.tid
IN PCONTEXT ThreadContext,//线程初始的用户空间寄存器上下文
IN PINITIAL_TEB InitialTeb,//线程的初始teb
IN BOOLEAN CreateSuspended)//是否初始创建为挂起态
{
INITIAL_TEB SafeInitialTeb = *InitialTeb;
if (KeGetPreviousMode() != KernelMode)
{
//用户空间线程必须指定初始的寄存器上下文(因为要模拟回到用户空间)
if (!ThreadContext) return STATUS_INVALID_PARAMETER;
}
return PspCreateThread(ThreadHandle,DesiredAccess,ObjectAttributes,ProcessHandle,
NULL,ClientId,ThreadContext,&SafeInitialTeb,CreateSuspended,
NULL,//用户线程无需StartRoutine
NULL);//用户线程无需StartContext
}
NTSTATUS
PspCreateThread(OUT PHANDLE ThreadHandle, //返回线程句柄
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle, //目标进程(用于创建用户线程)
IN PEPROCESS TargetProcess, //目标进程(用于创建内核线程)
OUT PCLIENT_ID ClientId, //返回pid.tid
IN PCONTEXT ThreadContext,//用户空间的初始寄存器上下文
IN PINITIAL_TEB InitialTeb, //线程的初始teb(内核线程不需要)
IN BOOLEAN CreateSuspended, //是否初始创建为挂起态
IN PKSTART_ROUTINE StartRoutine OPTIONAL,//内核线程的用户指定入口
IN PVOID StartContext OPTIONAL)//入口参数
{
PTEB TebBase = NULL;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PACCESS_STATE AccessState = &LocalAccessState;
if (StartRoutine) PreviousMode = KernelMode;//只有内核线程才会显式指定StartRoutine
if (ProcessHandle)//用户线程的目标进程
{
Status = ObReferenceObjectByHandle(ProcessHandle,PROCESS_CREATE_THREAD,PsProcessType,
PreviousMode, (PVOID*)&Process,NULL);
}
else
Process = TargetProcess;
//关键。创建该线程对象的内核结构
Status = ObCreateObject(PreviousMode,PsThreadType,ObjectAttributes,PreviousMode,NULL,
sizeof(ETHREAD),0,0, (PVOID*)&Thread);
RtlZeroMemory(Thread, sizeof(ETHREAD));
Thread->ExitStatus = STATUS_PENDING;
Thread->ThreadsProcess = Process;//指定该线程的所属进程
Thread->Cid.UniqueProcess = Process->UniqueProcessId; //指定该线程的所属进程的pid
CidEntry.Object = Thread;
CidEntry.GrantedAccess = 0;
Thread->Cid.UniqueThread = ExCreateHandle(PspCidTable, &CidEntry);//分配一个tid
InitializeListHead(&Thread->IrpList);//该线程发起的所有未完成的irp请求链表
InitializeListHead(&Thread->PostBlockList);
InitializeListHead(&Thread->ActiveTimerListHead);
if (ThreadContext)//if 用户线程
{
//用户线程都会分配一个teb
Status = MmCreateTeb(Process, &Thread->Cid, InitialTeb, &TebBase);
Thread->StartAddress = ThreadContext->Eip;//用户空间中的公共入口
Thread->Win32StartAddress = ThreadContext->Eax;//真正线程入口(oep或用户指定的入口)
//初始化线程对象结构、构造用户线程的初始运行环境
Status = KeInitThread(&Thread->Tcb,
PspUserThreadStartup,//内核中的用户线程派遣函数入口
NULL,//StartRoutine=NULL,使用用户空间中那个公共的入口
Thread->StartAddress,//StartContext
ThreadContext,//用户空间中的初始寄存器上下文
TebBase,&Process->Pcb);
}
Else //内核线程
{
Thread->StartAddress = StartRoutine;//用户指定的入口
//初始化线程对象结构、构造内核线程的初始运行环境
Status = KeInitThread(&Thread->Tcb,
PspSystemThreadStartup, //内核中的内核线程派遣函数入口
StartRoutine, //用户指定的入口
StartContext,//入口参数
NULL,//无需context
NULL,//无需teb
&Process->Pcb);
}
InsertTailList(&Process->ThreadListHead, &Thread->ThreadListEntry);//插入进程总线程链表
Process->ActiveThreads++;
KeStartThread(&Thread->Tcb);//设置该线程的初始优先级、时间片信息(从进程继承)
PspRunCreateThreadNotifyRoutines(Thread, TRUE);//通知系统线程创建消息
if (CreateSuspended) KeSuspendThread(&Thread->Tcb);//挂起线程
//将令牌与申请的权限传递到访问状态中
Status = SeCreateAccessStateEx(NULL,ThreadContext ?PsGetCurrentProcess() : Process,
&LocalAccessState,&AuxData,DesiredAccess,…);
Status = ObInsertObject(Thread,AccessState,DesiredAccess,0,NULL,&hThread);//插入句柄表
if (NT_SUCCESS(Status))
{
if (ClientId) *ClientId = Thread->Cid;
*ThreadHandle = hThread;
}
KeQuerySystemTime(&Thread->CreateTime);//记录线程的创建时间
KeReadyThread(&Thread->Tcb); //构造好初始运行环境后,加入就绪队列,现在线程就将跑起来了
return Status;
}
如上,上面函数创建内核线程对象,然后调用下面的函数初始化对象结构,创建它的内核栈,然后构造好它的初始运行环境(指内核栈中的初始状态),设置好初始的优先级和时间片后,就启动线程运行(指加入就绪队列)。这样,当该线程不久被调度运行时,就能跟着内核栈中初始的状态,一直运行下去(指调度时:恢复线程切换线程,从KiThreadStartup函数开始运行,然后恢复用户空间寄存器现场,回到用户空间的公共总入口处(kernel32模块中的BaseProcessStrartThunk或BaseThreadStrartThunk)继续执行)
NTSTATUS
KeInitThread(IN OUT PKTHREAD Thread,
IN PKSYSTEM_ROUTINE SystemRoutine,//用户线程是PspUserThreadStartup
IN PKSTART_ROUTINE StartRoutine,//用户线程是NULL,使用公共的总入口
IN PVOID StartContext,//入口参数
IN PCONTEXT Context,//用户空间的初始寄存器上下文
IN PVOID Teb,//用户线程的初始teb
IN PKPROCESS Process)
{
BOOLEAN AllocatedStack = FALSE;//表示是否分配了内核栈
KeInitializeDispatcherHeader(&Thread->DispatcherHeader,ThreadObject,
sizeof(KTHREAD) / sizeof(LONG),FALSE);//线程也是可等待对象
InitializeListHead(&Thread->MutantListHead);
for (i = 0; i< (THREAD_WAIT_OBJECTS + 1); i++)
Thread->WaitBlock[i].Thread = Thread;//线程内部内置的四个预定等待块
Thread->EnableStackSwap = TRUE; //指示内核栈可以被置换到外存
Thread->IdealProcessor = 1;
Thread->SwapBusy = FALSE;//一个标记当前线程是否正在进行切换的标记
Thread->KernelStackResident = TRUE; //线程初始创建时,内核栈当然位于物理内存中
Thread->AdjustReason = AdjustNone;//优先级的调整原因
Thread->ServiceTable = KeServiceDescriptorTable;//该线程使用的系统服务表描述符表(非SSDT)
//初始时,线程的两个APC队列都为空
InitializeListHead(&Thread->ApcState.ApcListHead[0]);
InitializeListHead(&Thread->ApcState.ApcListHead[1]);
Thread->ApcState.Process = Process;//当前进程
Thread->ApcStatePointer[OriginalApcEnvironment] = &Thread->ApcState;
Thread->ApcStatePointer[AttachedApcEnvironment] = &Thread->SavedApcState;
Thread->ApcStateIndex = OriginalApcEnvironment;
Thread->ApcQueueable = TRUE;//标记初始时,APC队列可插入
//一个专用于挂起线程的APC,后文会有介绍
KeInitializeApc(&Thread->SuspendApc,Thread,
OriginalApcEnvironment,
KiSuspendNop,
KiSuspendRundown,
KiSuspendThread,//该apc真正的函数
KernelMode,NULL);
KeInitializeSemaphore(&Thread->SuspendSemaphore, 0, 2);
Timer = &Thread->Timer;//可复用
KeInitializeTimer(Timer);
TimerWaitBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];//定时器固定占用一个等待快
TimerWaitBlock->Object = Timer;
TimerWaitBlock->WaitKey = STATUS_TIMEOUT;
TimerWaitBlock->WaitType = WaitAny;
TimerWaitBlock->NextWaitBlock = NULL;
TimerWaitBlock->WaitListEntry.Flink = &Timer->Header.WaitListHead;
TimerWaitBlock->WaitListEntry.Blink = &Timer->Header.WaitListHead;
Thread->Teb = Teb;//记录teb
KernelStack = MmCreateKernelStack(FALSE, 0);//关键。分配该线程的内核栈
AllocatedStack = TRUE;//标记为已分配
Thread->InitialStack = KernelStack;//初始的内核栈顶(即栈底)
Thread->StackBase = KernelStack;//内核栈底
Thread->StackLimit = KernelStack -12kb;//普通线程的内核栈的大小为12kb
Thread->KernelStackResident = TRUE;//初始时,内核栈当然位于物理内存中
Status = STATUS_SUCCESS;
//关键。下面这个函数构造初始的内核栈帧(模拟切换时的状态)
KiInitializeContextThread(Thread,
SystemRoutine,//用户线程为PspUserThreadStartup
StartRoutine,//用户线程为NULL(表示使用公共总入口)
StartContext,//入口参数
Context);//用户空间的初始寄存器上下文
Thread->State = Initialized;//标记为已初始化好,可以运行了
return Status;
}
下面这个函数就是用来实际执行构造线程的初始运行环境(即初始的内核栈状态)工作
初始的内核栈会模拟该线程仿佛以前曾经运行过,曾经被切换后的状态,这样,该线程一旦得到初始调度机会,就向得到重新调度机会一样,继续运行。
每个处于非运行状态的线程的内核栈的布局是:(从栈底到栈顶)【浮点、trap、函数、切】
浮点寄存器帧|trap现场帧|内核各层函数参数、局部变量帧|线程切换帧
每次发生系统调用、中断、异常时线程都会进入内核,在内核栈先保存浮点寄存器,然后保存寄存器现场,
进入内核函数嵌套调用,最后由于时间片等原因发生线程切换,保存切换时的现场,等待下次调度运行时,从上次切换出时的断点处继续执行。
注意每当重回到用户空间后,线程的内核栈就是空白的。一个线程的绝大多数时间都是运行在用户空间,因此,绝大多数时刻,线程的内核栈都呈现空白状态(里面没存放任何数据)。
下面这个函数就是用来初始构造模拟线程被切换出时的现场(实际线程还没运行过,即还没切换过)。
非常关键。
VOID
KiInitializeContextThread(IN PKTHREAD Thread,
IN PKSYSTEM_ROUTINE SystemRoutine,//用户线程为PspUserThreadStartup
IN PKSTART_ROUTINE StartRoutine, //用户线程为NULL(表示公共总入口)
IN PVOID StartContext, //入口参数
IN PCONTEXT ContextPointer) //用户线程的初始上下文(内核线程没有)
{
PFX_SAVE_AREA FxSaveArea;//内核栈中的浮点寄存器保存区
PFXSAVE_FORMAT FxSaveFormat;
PKSTART_FRAME StartFrame;//线程公共起始函数KiThreadStartup的栈帧
PKSWITCHFRAME CtxSwitchFrame;//切换帧
PKTRAP_FRAME TrapFrame;//trap现场帧
CONTEXT LocalContext;//临时变量
PCONTEXT Context = NULL;
ULONG ContextFlags;
PKUINIT_FRAME InitFrame;//线程的初始内核栈帧(由浮点帧、trap帧、起始函数帧、切换帧组成)
InitFrame = (PKUINIT_FRAME)( Thread->InitialStack - sizeof(KUINIT_FRAME));
FxSaveArea = &InitFrame->FxSaveArea;//初始帧中的浮点保存区
RtlZeroMemory(FxSaveArea,KTRAP_FRAME_LENGTH + sizeof(FX_SAVE_AREA));
TrapFrame = &InitFrame->TrapFrame;//初始帧中的trap现场帧(最重要)
StartFrame = &InitFrame->StartFrame;//起始函数(指KiThreadStartup)的参数帧(重要)
CtxSwitchFrame = &InitFrame->CtxSwitchFrame;//切换帧(非常重要)
if (ContextPointer)//如果是要构造用户线程的初始帧
{
RtlCopyMemory(&LocalContext, ContextPointer, sizeof(CONTEXT));
Context = &LocalContext;
ContextFlags = CONTEXT_CONTROL;
{初始化浮点寄存器部分略}
Context->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;//初始时不需要调试寄存器
//关键。模拟保存进入内核空间中时的现场
KeContextToTrapFrame(Context,NULL,TrapFrame,Context->ContextFlags | ContextFlags,
UserMode);//将Context中各个寄存器填写到Trap帧中(模拟自陷现场)
TrapFrame->HardwareSegSs |= RPL_MASK;
TrapFrame->SegDs |= RPL_MASK;TrapFrame->SegEs |= RPL_MASK;
TrapFrame->Dr7 = 0;//不需要调试寄存器
TrapFrame->DbgArgMark = 0xBADB0D00;
TrapFrame->PreviousPreviousMode = UserMode;
TrapFrame->ExceptionList = -1;
Thread->PreviousMode = UserMode;//模拟从用户空间自陷进来时构造的帧
StartFrame->UserThread = TRUE;//相当于push传参给KiThreadStartup
}
else
{
{内核线程则会初始化成不同的浮点寄存器,略}
Thread->PreviousMode = KernelMode; //模拟从内核空间发起系统调用时构造的帧
StartFrame->UserThread = FALSE; //相当于push传参给KiThreadStartup
}
StartFrame->StartContext = StartContext;//相当于push传参给KiThreadStartup
StartFrame->StartRoutine = StartRoutine; //相当于push传参给KiThreadStartup
StartFrame->SystemRoutine = SystemRoutine; //相当于push传参给KiThreadStartup
//关键。模拟线程仿佛上次在执行call KiThreadStartup时,被切换了出去
CtxSwitchFrame->RetAddr = KiThreadStartup;//以后线程一调度就从这儿开始执行下去
CtxSwitchFrame->ApcBypassDisable = TRUE;
CtxSwitchFrame->ExceptionList = -1;//线程的初始内核seh链表当然为空(-1表示空)
Thread->KernelStack = CtxSwitchFrame;//记录上次切换时的内核栈顶(模拟的)
}
为了弄懂线程初始时的内核栈布局,必须理解下面几个结构体定义和函数。
typedef struct _KUINIT_FRAME //每个线程的初始内核栈帧
{
KSWITCHFRAME CtxSwitchFrame;//切换帧
KSTART_FRAME StartFrame;//KiThreadStartup函数的参数帧
KTRAP_FRAME TrapFrame;//trap现场帧
FX_SAVE_AREA FxSaveArea;//浮点保存区
} KUINIT_FRAME, *PKUINIT_FRAME;
其中浮点保存区就位于栈底,向上依次是trap现场帧、KiThreadStartup的参数帧、切换帧
typedef struct _KSTART_FRAME //KiThreadStartup的参数帧
{
PKSYSTEM_ROUTINE SystemRoutine; //用户线程为PspUserThreadStartup
PKSTART_ROUTINE StartRoutine;//用户线程为NULL(表示使用公共总入口)
PVOID StartContext;//入口参数
BOOLEAN UserThread;//标志
} KSTART_FRAME, *PKSTART_FRAME;
typedef struct _KSWITCHFRAME //切换帧
{
PVOID ExceptionList;//保存线程切换时的内核she链表(不是用户空间中的seh)
Union
{
BOOLEAN ApcBypassDisable;//用于首次调度
UCHAR WaitIrql;//用于保存切换时的WaitIrql
};
PVOID RetAddr;//保存发生切换时的断点地址(以后切换回来时从这儿继续执行)
} KSWITCHFRAME, *PKSWITCHFRAME;
不管是用户线程还是内核线程,都是最开始从下面这个函数开始执行起来的。
Void KiThreadStartup(PKSYSTEM_ROUTINE SystemRoutine //用户线程为PspUserThreadStartup
PKSTART_ROUTINE StartRoutine //转用作PspUserThreadStartup的参数
Void* StartContext//转用作PspUserThreadStartup的参数
BOOL UserThread
)
{
Xor ebx,ebx
Xor esi,esi
Xor edi,edi
Xor ebp,ebp
Mov ecx,APC_LEVEL
Call KfLowerIrql //降到APC级别
Pop eax //弹出的第一个值刚好是SystemRoutine
Call eax //调用SystemRoutine(注意StartRoutine,StartContext又是它的参数)
----------------------------------华丽的分割线------------------------------------------
//注意若创建的是内核线程,那么上面的eax是PspSystemThreadStartup,这个函数是“不返回的”,
它执行完毕后不会ret回来,而是直接跳去用户指定的内核入口了。反之,若能回来,那么可以肯定是
用户线程(而且,StartRoutine和StartContext这两个参数已被PspSystemThreadStartup在内部弹出)。那么,现在就可以顺利弹出trap帧,恢复用户空间中的寄存器上下文,继续执行,于是Jmp KiServiceExit2, jmp到那儿去,退回用户空间。
Pop ecx //此时ecx=栈帧中UserThread字段的值
Or ecx,ecx
Jz BadThread //UserThread不为1就显示蓝屏界面
Mov ebp,esp //此时的内核栈顶就是trap帧的地址。
Jmp KiServiceExit2 //此时内核栈中只剩余浮点保存区和trap帧,将恢复用户空间现场退回用户空间
}
上面的线程公共入口函数内部会call SystemRoutine 进入对应的函数。如果创建的是用户线程,调用的就是PspUserThreadStartup。
VOID
PspUserThreadStartup(IN PKSTART_ROUTINE StartRoutine,//对于用户线程无意义
IN PVOID StartContext)//对于用户线程无意义
{
BOOLEAN DeadThread = FALSE;
KeLowerIrql(PASSIVE_LEVEL);
Thread = PsGetCurrentThread();
if (Thread->DeadThread)
DeadThread = TRUE;
Else …
if (!(Thread->DeadThread) && !(Thread->HideFromDebugger))
DbgkCreateThread(Thread, StartContext);//通知内核调试器一个新线程启动了
if (!DeadThread)
{
KeRaiseIrql(APC_LEVEL, &OldIrql);
//返回用户空间的总入口前先执行一下apc,完成其他初始工作(如加载其他依赖库)
KiInitializeUserApc(KeGetExceptionFrame(&Thread->Tcb),
KeGetTrapFrame(&Thread->Tcb),
PspSystemDllEntryPoint,//ntdll.LdrInitializeThunk
NULL,PspSystemDllBase,NULL);
KeLowerIrql(PASSIVE_LEVEL);
}
Else …
//这个函数是典型的‘返回型’函数,会返回到上面函数的call eax指令后面,进而退回用户空间的总入口函数去去执行。不过,在退回总入口前,这儿插入了一个APC,执行完这个附加的APC后才会正式退回用户空间的总入口(因为这个LdrInitializeThunk APC函数还有一些重要的附加工作要做)
Return;
}
相信大家一直有一个疑问,就是用户空间的总入口到底做了什么工作,我们后文再看。
-------------------------------------------------------------------------------------
现在我们回到CreateProcess API,总结一下这个函数到底在内部干了什么,看以下总结
1、 打开目标可执行文件
若是exe文件,先检查‘映像劫持’键,然后打开文件,创建一个section,等候映射
若是 bat、cmd脚本文件,则启动的是cmd.exe进程,脚本文件作为命令行参数
若是DOS的exe、com文件,启动ntvdm.exe v86进程,原文件作为命令行参数
若是posix、os2文件,启动对应的子系统服务进程
2、 创建、初始化进程对象;创建初始化地址空间;加载映射exe和ntdll文件;分配一个PEB
3、 创建、初始化主线程对象;创建TEB;构造初始的运行环境(内核初始栈帧)
4、 通知windows子系统(csrss.exe进程)新进程创建事件(csrss.exe进程含有绝大多数进程的句柄)
这样,进程、主线程都创建起来了,只需等待得到cpu调度便可投入运行。
-------------------------------------------------------------------------------------
现在具体看一下CreateProcess的创建过程,它会在内部直接转调CreateProcessInternalW函数
BOOL
CreateProcessInternalW(HANDLE hToken,//暂时无用
LPCWSTR lpApplicationName,//程序文件名
LPWSTR lpCommandLine,//命令行
LPSECURITY_ATTRIBUTES lpProcessAttributes,//SD
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
BOOL bInheritHandles,//是否继承父进程句柄表中的那些可继承句柄
DWORD dwCreationFlags,
LPVOID lpEnvironment,//环境变量块
LPCWSTR lpCurrentDirectory,//指定给子进程的当前目录
LPSTARTUPINFOW lpStartupInfo,//附加启动信息
LPPROCESS_INFORMATION lpProcessInformation,//返回创建结果
PHANDLE hNewToken)//暂时无用
{
BOOLEAN CmdLineIsAppName = FALSE;//表示文件名是否就是命令行
UNICODE_STRING ApplicationName = { 0, 0, NULL };
HANDLE hSection = NULL, hProcess = NULL, hThread = NULL, hDebug = NULL;
LPWSTR CurrentDirectory = NULL;
PPEB OurPeb = NtCurrentPeb();//当前进程即父进程的peb
SIZE_T EnvSize = 0;//环境变量块的大小
//检查下面的‘映像劫持’键,略
{HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options}
if ((dwCreationFlags & (DETACHED_PROCESS | CREATE_NEW_CONSOLE)) ==
(DETACHED_PROCESS | CREATE_NEW_CONSOLE))
{
SetLastError(ERROR_INVALID_PARAMETER);//这俩标志不可同时使用
return FALSE;
}
StartupInfo = *lpStartupInfo;
RtlZeroMemory(lpProcessInformation, sizeof(PROCESS_INFORMATION));
PriorityClass.Foreground = FALSE;//初始创建的进程作为后台进程看待
//根据创建标志计算对应的优先级类别(一种优先级类对应一种基本优先级)
PriorityClass.PriorityClass = (UCHAR)BasepConvertPriorityClass(dwCreationFlags);
GetAppName:
if (!lpApplicationName)//很常见
{
NameBuffer = RtlAllocateHeap(RtlGetProcessHeap(),0,MAX_PATH * sizeof(WCHAR));
lpApplicationName = lpCommandLine;
处理NameBuffer,略
lpApplicationName = NameBuffer;// 最终获得命令行中包含的应用程序文件名
}
else if (!lpCommandLine || *lpCommandLine == UNICODE_NULL)
{
CmdLineIsAppName = TRUE;
lpCommandLine = (LPWSTR)lpApplicationName;
}
//事实上只是为程序文件创建一个section,等待映射(函数名有误导)
Status = BasepMapFile(lpApplicationName, &hSection, &ApplicationName);
if (!NT_SUCCESS(Status))
{
If(是一个bat批脚本文件)
命令行改为“cmd /c bat文件名”,goto GetAppName,重新解析
Else …
}
if (!StartupInfo.lpDesktop)//继承父进程的桌面
StartupInfo.lpDesktop = OurPeb->ProcessParameters->DesktopInfo.Buffer;
//查询section对象的映像文件信息
Status = ZwQuerySection(hSection,SectionImageInformation,
&SectionImageInfo,sizeof(SectionImageInfo),NULL);
if (SectionImageInfo.ImageCharacteristics & IMAGE_FILE_DLL) 失败返回;
if (IMAGE_SUBSYSTEM_WINDOWS_GUI == SectionImageInfo.SubSystemType)
{
dwCreationFlags &= ~CREATE_NEW_CONSOLE;//GUI程序无需控制台
dwCreationFlags |= DETACHED_PROCESS;
}
ObjectAttributes = BasepConvertObjectAttributes(&LocalObjectAttributes,
lpProcessAttributes,NULL);
//if创建的是一个要被当前线程调试的子进程
if (dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS))
{
Status = DbgUiConnectToDbg();//连接到
hDebug = DbgUiGetThreadDebugObject();//为当前线程创建一个调试端口(用来父子进程通信)
}
//关键。调用系统服务,创建内核中的进程对象,并初始化其地址空间等N多内容
Status = NtCreateProcess(&hProcess,PROCESS_ALL_ACCESS,ObjectAttributes,
NtCurrentProcess(),bInheritHandles,hSection,
hDebug,); //当前线程的调试端口(将传给子进程)
//设置进程的优先级类别
if (PriorityClass.PriorityClass != PROCESS_PRIORITY_CLASS_INVALID)
{
Status = NtSetInformationProcess(hProcess,ProcessPriorityClass,
&PriorityClass,sizeof(PROCESS_PRIORITY_CLASS));
}
Status = NtQueryInformationProcess(hProcess,ProcessBasicInformation,&ProcessBasicInfo,
sizeof(ProcessBasicInfo),NULL);
if(lpEnvironment && !(dwCreationFlags & CREATE_UNICODE_ENVIRONMENT))
lpEnvironment = BasepConvertUnicodeEnvironment(&EnvSize, lpEnvironment);
RemotePeb = ProcessBasicInfo.PebBaseAddress;//子进程的peb地址(实际上是固定的)
//关键。创建子进程的参数块和环境变量块
Status = BasepInitializeEnvironment(hProcess,RemotePeb,lpApplicationName,
CurrentDirectory,lpCommandLine,
lpEnvironment,EnvSize,//环境变量块的地址、长度
&StartupInfo,dwCreationFlags,bInheritHandles);
//如果没有显式指定这三个标准句柄给子进程,就继承父进程中的那3个标准句柄(最常见)
if (!bInheritHandles && !(StartupInfo.dwFlags & STARTF_USESTDHANDLES) &&
SectionImageInfo.SubSystemType == IMAGE_SUBSYSTEM_WINDOWS_CUI)
{
PRTL_USER_PROCESS_PARAMETERS RemoteParameters;
Status = NtReadVirtualMemory(hProcess,&RemotePeb->ProcessParameters,
&RemoteParameters,sizeof(PVOID),NULL);
BasepDuplicateAndWriteHandle(hProcess,OurPeb->ProcessParameters->StandardInput,
&RemoteParameters->StandardInput);
BasepDuplicateAndWriteHandle(hProcess,OurPeb->ProcessParameters->StandardOutput,
&RemoteParameters->StandardOutput);
BasepDuplicateAndWriteHandle(hProcess,OurPeb->ProcessParameters->StandardError,
&RemoteParameters->StandardError);
}
//通知csrss.exe进程,一个新的进程已创建
Status = BasepNotifyCsrOfCreation(dwCreationFlags,ProcessBasicInfo.UniqueProcessId,
bInheritHandles);
--------------------------------------华丽的分割线---------------------------------
//至此,已创建好了进程,接下来创建该进程中的第一个线程(主线程)
//这个函数创建主线程的用户栈、内核栈,并建立起初始的运行环境(内核栈帧)
hThread = BasepCreateFirstThread(hProcess,lpThreadAttributes,&SectionImageInfo,
&ClientId);//返回线程的pid.tid
if (!(dwCreationFlags & CREATE_SUSPENDED))
NtResumeThread(hThread, &Dummy);//恢复线程,挂入就绪队列(即可以开始运行这个线程了)
lpProcessInformation->dwProcessId = (DWORD)ClientId.UniqueProcess;
lpProcessInformation->dwThreadId = (DWORD)ClientId.UniqueThread;
lpProcessInformation->hProcess = hProcess;
lpProcessInformation->hThread = hThread;
return TRUE;
}
NTSTATUS
BasepMapFile(IN LPCWSTR lpApplicationName,OUT PHANDLE hSection,
IN PUNICODE_STRING ApplicationName)
{
RelativeName.Handle = NULL;
//转为NT路径格式
RtlDosPathNameToNtPathName_U(lpApplicationName,ApplicationName,NULL,&RelativeName)
if (RelativeName.DosPath.Length)
ApplicationName = &RelativeName.DosPath;
InitializeObjectAttributes(&ObjectAttributes,ApplicationName,OBJ_CASE_INSENSITIVE,
RelativeName.Handle,NULL);
//打开程序文件
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);
//为文件创建一个公共section,等候映射(多个进程可以同用一个exe文件)
Status = NtCreateSection(hSection,SECTION_ALL_ACCESS,NULL,NULL,PAGE_EXECUTE,
SEC_IMAGE,hFile);
NtClose(hFile);
return Status;
}
如上,这个函数的名字有误导,其实只是创建一个section,并没有立即映射到地址空间(多个进程可以共享同一程序文件的)
当用户线程从内核的KiThreadStartup运行起来后,进入PspUserThreadStartup,最后回到用户空间的总入口处(主线程的用户空间根是BaseProcessStartThunk,其他线程的用户空间根是BaseThreadStartThunk)继续运行。不过前文讲了,在正式从内核回到用户空间的的总入口前,会扫描执行中途插入的APC函数,做完附加的APC工作后才从总入口处继续运行。插入的这个APC函数是LdrInitializeThunk,它的主要工作是负责加载exe文件依赖的所有动态库以及其他工作。
LdrInitializeThunk() //APC
{
Lea eax,[esp+16]
Mov [esp+4],eax //第一个参数=Context*
Xor ebp,ebp
Jmp LdrpInit //实际的工作
}
VOID LdrpInit(PCONTEXT Context,PVOID SystemArgument1,PVOID SystemArgument2) //APC
{
if (!LdrpInitialized)//if 主线程
{
LdrpInit2(Context, SystemArgument1, SystemArgument2);
LdrpInitialized = TRUE;
}
LdrpAttachThread();//各线程创建后都会通知进程中的所有模块一个ThreadAttach消息
}
看看主线程的初始化工作,也即进程的初始化工作,如下:
VOID LdrpInit2(PCONTEXT Context,PVOID SystemArgument1,PVOID SystemArgument2)
{
PPEB Peb = NtCurrentPeb();//现在就是子进程的peb啦(在子进程的地址空间中)
PVOID BaseAddress = SystemArgument1;//ntdll模块的地址
ImageBase = Peb->ImageBaseAddress;
PEDosHeader = (PIMAGE_DOS_HEADER) ImageBase;
if (PEDosHeader->e_magic != IMAGE_DOS_SIGNATURE || PEDosHeader->e_lfanew == 0L ||
*(PULONG)((PUCHAR)ImageBase + PEDosHeader->e_lfanew) != IMAGE_NT_SIGNATURE)
{
//验证PE文件签名
ZwTerminateProcess(NtCurrentProcess(), STATUS_INVALID_IMAGE_FORMAT);
}
RtlNormalizeProcessParams(Peb->ProcessParameters);
NTHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)ImageBase + PEDosHeader->e_lfanew);
Status = ZwQuerySystemInformation(SystemBasicInformation,&SystemInformation,
sizeof(SYSTEM_BASIC_INFORMATION),NULL);
Peb->NumberOfProcessors = SystemInformation.NumberOfProcessors;
RtlInitializeHeapManager();
//创建进程的默认堆
Peb->ProcessHeap = RtlCreateHeap(HEAP_GROWABLE,NULL,
NTHeaders->OptionalHeader.SizeOfHeapReserve,//一般为0
NTHeaders->OptionalHeader.SizeOfHeapCommit,//一般为4kb
NULL,NULL);
RtlpInitializeVectoredExceptionHandling();//初始化向量化异常
RtlInitializeCriticalSection(&PebLock);
Peb->FastPebLock = &PebLock;
//初始化peb中内置的动态tls位图
Peb->TlsBitmap = &TlsBitMap;
Peb->TlsExpansionBitmap = &TlsExpansionBitMap;
Peb->TlsExpansionCounter = 64;
RtlInitializeBitMap(&TlsBitMap, Peb->TlsBitmapBits,64);//固定指向内置的那个64位tls位图
RtlInitializeBitMap(&TlsExpansionBitMap, Peb->TlsExpansionBitmapBits,1024);
//初始化回调表
Peb->KernelCallbackTable = RtlAllocateHeap(RtlGetProcessHeap(),0,sizeof(PVOID) *
(USER32_CALLBACK_MAXIMUM + 1));
RtlInitializeCriticalSection(&LoaderLock);
Peb->LoaderLock = &LoaderLock;
//从默认堆中分配一个加载信息块
Peb->Ldr = (PPEB_LDR_DATA) RtlAllocateHeap(Peb->ProcessHeap,0,sizeof(PEB_LDR_DATA));
Peb->Ldr->Length = sizeof(PEB_LDR_DATA);
Peb->Ldr->Initialized = FALSE;//表示尚未完成初始化(也即尚未完成加载dll等工作)
Peb->Ldr->SsHandle = NULL;
InitializeListHead(&Peb->Ldr->InLoadOrderModuleList);//加载顺序的模块表
InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList);//内存地址顺序的模块表
//初始化顺序模块表,初始化顺序与加载顺序相反(最底层的dll最先得到初始化)
InitializeListHead(&Peb->Ldr->InInitializationOrderModuleList);
LoadImageFileExecutionOptions(Peb);
ExeModule = RtlAllocateHeap(Peb->ProcessHeap,0,sizeof(LDR_DATA_TABLE_ENTRY));
ExeModule->DllBase = Peb->ImageBaseAddress;
RtlCreateUnicodeString(&ExeModule->FullDllName,
Peb->ProcessParameters->ImagePathName.Buffer);
RtlCreateUnicodeString(&ExeModule->BaseDllName,
wcsrchr(ExeModule->FullDllName.Buffer, L'\\') + 1);
ExeModule->Flags = LDRP_ENTRY_PROCESSED;//exe模块没有dll标志
ExeModule->LoadCount = -1;//标记为无法动态卸载
ExeModule->TlsIndex = -1;ExeModule->SectionPointer = NULL;ExeModule->CheckSum = 0;
NTHeaders = RtlImageNtHeader(ExeModule->DllBase);
ExeModule->SizeOfImage = LdrpGetResidentSize(NTHeaders);
ExeModule->TimeDateStamp = NTHeaders->FileHeader.TimeDateStamp;
//先插入exe文件的模块描述符
InsertTailList(&Peb->Ldr->InLoadOrderModuleList,&ExeModule->InLoadOrderLinks);
wcscpy(FullNtDllPath, SharedUserData->NtSystemRoot);//一般为C:\Windows
wcscat(FullNtDllPath, L"\\system32\\ntdll.dll");
NtModule = (PLDR_DATA_TABLE_ENTRY)
RtlAllocateHeap(Peb->ProcessHeap,0,sizeof(LDR_DATA_TABLE_ENTRY));
memset(NtModule, 0, sizeof(LDR_DATA_TABLE_ENTRY));
NtModule->DllBase = BaseAddress;
NtModule->EntryPoint = 0;
RtlCreateUnicodeString(&NtModule->FullDllName, FullNtDllPath);
RtlCreateUnicodeString(&NtModule->BaseDllName, L"ntdll.dll");
NtModule->Flags = LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED;
NtModule->LoadCount = -1;//标记无法动态卸载ntdll.dll
NtModule->TlsIndex = -1;NtModule->SectionPointer = NULL;NtModule->CheckSum = 0;
NTHeaders = RtlImageNtHeader(NtModule->DllBase);
NtModule->SizeOfImage = LdrpGetResidentSize(NTHeaders);
NtModule->TimeDateStamp = NTHeaders->FileHeader.TimeDateStamp;
//再插入ntdll文件的模块描述符
InsertTailList(&Peb->Ldr->InLoadOrderModuleList,&NtModule->InLoadOrderLinks);
InsertTailList(&Peb->Ldr->InInitializationOrderModuleList,
&NtModule->InInitializationOrderModuleList);//NTDLL不依赖其他库
LdrpInitLoader();//获取“\KnownDlls\KnownDllPath”路径
//PE加载器的核心函数,用来执行模块的重定位、加载导入库、处理tls
ExeModule->EntryPoint = LdrPEStartup(ImageBase, NULL, NULL, NULL);
Peb->Ldr->Initialized = TRUE;//标志该进程的所有dll都已加载完成
if (Peb->BeingDebugged)
DbgBreakPoint();//int 3通知调试器, 首次触发调试中断
}
进程初始时的重点工作就是加载exe文件依赖的所有子孙dll,由下面的函数完成这项工作
(注意这个函数专用来启动初始化进程的主exe文件,是启动阶段的核心函数)
PEPFUNC LdrPEStartup (PVOID ImageBase,//exe文件的内存地址(进程的主exe文件)
HANDLE SectionHandle,
PLDR_DATA_TABLE_ENTRY* Module,
PWSTR FullDosName)
{
PEPFUNC EntryPoint = NULL;
DosHeader = (PIMAGE_DOS_HEADER) ImageBase;
NTHeaders = (PIMAGE_NT_HEADERS) ((ULONG_PTR)ImageBase + DosHeader->e_lfanew);
//if 实际加载地址与pe头中的预期加载地址不同,执行重定位工作(常见于dll文件,exe文件也可能)
if (ImageBase != (PVOID) NTHeaders->OptionalHeader.ImageBase)
Status = LdrPerformRelocations(NTHeaders, ImageBase);//遍历.reloc节中项目,执行重定位
if (Module != NULL)//也即if 是dll文件(事实上这个条件永不满足)
{
*Module = LdrAddModuleEntry(ImageBase, NTHeaders, FullDosName);//加入加模块载顺序链表
(*Module)->SectionPointer = SectionHandle;
}
Else //也即进程的主exe文件,这才是正题
{
Module = &tmpModule;
Status = LdrFindEntryForAddress(ImageBase, Module);//直接在模块表中查找
}
if (ImageBase != (PVOID) NTHeaders->OptionalHeader.ImageBase)
(*Module)->Flags |= LDRP_IMAGE_NOT_AT_BASE;
Status = RtlAllocateActivationContextStack(&ActivationContextStack);
if (NT_SUCCESS(Status))
{
NtCurrentTeb()->ActivationContextStackPointer = ActivationContextStack;
NtCurrentTeb()->ActivationContextStackPointer->ActiveFrame = NULL;
}
Status = LdrFixupImports(NULL, *Module);//加载子孙dll,修正IAT导入表
Status = LdrpInitializeTlsForProccess();//初始化进程的静态tls,详见后文
if (NT_SUCCESS(Status))
{
LdrpAttachProcess();//发送一个ProcessAttach消息,调用该模块的DllMain函数
LdrpTlsCallback(*Module, DLL_PROCESS_ATTACH);
}
if (NTHeaders->OptionalHeader.AddressOfEntryPoint != 0)
EntryPoint = (ULONG_PTR)ImageBase+ NTHeaders->OptionalHeader.AddressOfEntryPoint;
return EntryPoint;//返回oep
}
下面的函数加载指定模块依赖的所有子孙dll
NTSTATUS
LdrFixupImports(IN PWSTR SearchPath OPTIONAL,//自定义的dll搜索路径(不提供的话就使用标准路径)
IN PLDR_DATA_TABLE_ENTRY Module)//指定模块
{
ULONG TlsSize = 0;
NTSTATUS Status = STATUS_SUCCESS;
//获取tls目录
TlsDirectory = (PIMAGE_TLS_DIRECTORY)RtlImageDirectoryEntryToData(Module->DllBase,
TRUE,IMAGE_DIRECTORY_ENTRY_TLS,&Size);
if (TlsDirectory)
{
TlsSize = TlsDirectory->EndAddressOfRawData- TlsDirectory->StartAddressOfRawData
+ TlsDirectory->SizeOfZeroFill;
if (TlsSize > 0 && NtCurrentPeb()->Ldr->Initialized)//if 动态加载该模块
TlsDirectory = NULL;// 动态加载的模块不支持静态tls
}
ImportModuleDirectory = (PIMAGE_IMPORT_DESCRIPTOR)
RtlImageDirectoryEntryToData(Module->DllBase,
TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&Size);
BoundImportDescriptor = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)
RtlImageDirectoryEntryToData(Module->DllBase,TRUE,
MAGE_DIRECTORY_ENTRY_BOUND_IMPORT,&Size);
if (BoundImportDescriptor != NULL && ImportModuleDirectory == NULL)
return STATUS_UNSUCCESSFUL;
if (BoundImportDescriptor) 处理绑定导入表,略
else if (ImportModuleDirectory)
{
ImportModuleDirectoryCurrent = ImportModuleDirectory;//当前依赖的模块
while (ImportModuleDirectoryCurrent->Name)//遍历IMT导入模块表中的各个依赖模块
{
ImportedName = Module->DllBase + ImportModuleDirectoryCurrent->Name;//模块名
if (SearchPath == NULL) //如果没提供自定义搜索路径,就构造一个标准搜索路径
{
//标准搜索路径是:exe文件目录;当前目录;Sytem32目录;Windows目录;Path环境变量
ModulePath = LdrpQueryAppPaths(Module->BaseDllName.Buffer);
Status = LdrpGetOrLoadModule(ModulePath, ImportedName, &ImportedModule, TRUE);
if (NT_SUCCESS(Status)) goto Success;
}
//在模块加载表中查找该模块或者加载该模块(找不到就加载)
Status = LdrpGetOrLoadModule(SearchPath, ImportedName, &ImportedModule, TRUE);
Success:
//处理该依赖模块的IAT导入地址表(获取各个导入函数的实际地址,填到IAT对应的表项中)
Status = LdrpProcessImportDirectoryEntry(Module, ImportedModule, ImportModuleDirectoryCurrent);
ImportModuleDirectoryCurrent++;//下一个依赖的模块
}
}
if (TlsDirectory && TlsSize > 0)
LdrpAcquireTlsSlot(Module, TlsSize, FALSE);
return STATUS_SUCCESS;
}
NTSTATUS LdrpGetOrLoadModule(PWCHAR SearchPath,//搜索路径
PCHAR Name,//模块名,是ASC形式
PLDR_DATA_TABLE_ENTRY* Module,
BOOLEAN Load)//指找不到的话,是否加载
{
RtlInitAnsiString(&AnsiDllName, Name);
Status = RtlAnsiStringToUnicodeString(&DllName, &AnsiDllName, TRUE);
Status = LdrFindEntryForName (&DllName, Module, Load);
if (Load && !NT_SUCCESS(Status))
{
Status = LdrpLoadModule(SearchPath,0,&DllName,Module,NULL);
if (NT_SUCCESS(Status))
Status = LdrFindEntryForName (&DllName, Module, FALSE);
}
return Status;
}
之所以要在加载模块表中查找,找不到才加载,是因为避免同一个模块加载两次。下面的函数用来加载一个模块。(注意这个函数也供LoadLibrary API内部间接调用)
NTSTATUS
LdrpLoadModule(IN PWSTR SearchPath OPTIONAL,
IN ULONG LoadFlags,
IN PUNICODE_STRING Name,//模块名
PLDR_DATA_TABLE_ENTRY *Module,
PVOID *BaseAddress OPTIONAL)//返回实际加载的地址
{
if (Module == NULL)
Module = &tmpModule;
LdrAdjustDllName(&AdjustedName, Name, FALSE);
MappedAsDataFile = FALSE;
Status = LdrFindEntryForName(&AdjustedName, Module, TRUE);//仍要先查找
if (NT_SUCCESS(Status))
{
if (NULL != BaseAddress)
*BaseAddress = (*Module)->DllBase;
}
else
{
//先尝试在\KnownDlls对象目录中查找该dll文件的section对象
Status = LdrpMapKnownDll(&AdjustedName, &FullDosName, &SectionHandle);
if (!NT_SUCCESS(Status))//若找不到,则为该dll文件创建一个映像文件section
{
MappedAsDataFile = (0 != (LoadFlags & LOAD_LIBRARY_AS_DATAFILE));
//内部会调用NtCreateSection系统服务,创建一个section
Status = LdrpMapDllImageFile(SearchPath, &AdjustedName, &FullDosName,
MappedAsDataFile, &SectionHandle);
}
ViewSize = 0;//表示映射整个dll文件
ImageBase = 0;//表示不指定映射的地址
ArbitraryUserPointer = NtCurrentTeb()->NtTib.ArbitraryUserPointer;
NtCurrentTeb()->NtTib.ArbitraryUserPointer = FullDosName.Buffer;
Status = NtMapViewOfSection(SectionHandle,NtCurrentProcess(),
&ImageBase,0,0,NULL,&ViewSize,ViewShare,0,…);
NtCurrentTeb()->NtTib.ArbitraryUserPointer = ArbitraryUserPointer;
if (NULL != BaseAddress)
*BaseAddress = ImageBase;
if (MappedAsDataFile)//dll可以当做纯数据文件加载
{
if (NULL != BaseAddress)
*BaseAddress = (PVOID) ((char *) *BaseAddress + 1);//复用标志
*Module = NULL;
return STATUS_SUCCESS;
}
//if 实际加载映射的地址与该dll期望的加载地址不同,执行重定位
if (ImageBase != (PVOID) NtHeaders->OptionalHeader.ImageBase)
Status = LdrPerformRelocations(NtHeaders, ImageBase);
*Module = LdrAddModuleEntry(ImageBase, NtHeaders, FullDosName.Buffer);
(*Module)->SectionPointer = SectionHandle;
if (ImageBase != (PVOID) NtHeaders->OptionalHeader.ImageBase)
(*Module)->Flags |= LDRP_IMAGE_NOT_AT_BASE;
if (NtHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL)
(*Module)->Flags |= LDRP_IMAGE_DLL;
//又加载该dll本身依赖的所有其他dll,修正它的导入表
Status = LdrFixupImports(SearchPath, *Module);
//当所有子孙dll初始化完后,自己才初始化完毕,此时才加入初始化顺序链表中
InsertTailList(&NtCurrentPeb()->Ldr->InInitializationOrderModuleList,
&(*Module)->InInitializationOrderModuleList);
}
return STATUS_SUCCESS;
}
至此,进程启动时的初始化工作已经初始完毕。Exe文件及其依赖的所有dll以及tls工作最终都完成处理了,这时候该APC函数将返回。这个LdrInitializeThunk要返回哪里呢?答案是返回到内核,然后才恢复用户寄存器现场,正式退回用户空间,执行主线程的用户空间总入口函数BaseProcessStartThunk,换句话说,当程序流执行到BaseProcessStartThunk这个函数时,进程已初始化,各dll已完成加载。此时,万事俱备,只欠东风了,线程可以放马在用户空间执行了。
_BaseProcessStartThunk@0://主线程的用户空间总入口(内核总入口是KiThreadStartup)
{
xor ebp, ebp
push eax //oep
push 0 //表示不会返回
jmp _BaseProcessStartup@4
}
__declspec(noreturn) )//主线程的入口
VOID BaseProcessStartup(PPROCESS_START_ROUTINE lpStartAddress
{
UINT uExitCode = 0;
_SEH2_TRY //放在try块中保护
{
NtSetInformationThread(NtCurrentThread(),ThreadQuerySetWin32StartAddress,
&lpStartAddress,sizeof(PPROCESS_START_ROUTINE));
//lpStartAddress即oep,一般就是WinMainCRTStartup/MainCRTStartup
uExitCode = (lpStartAddress)();//转去oep
}
_SEH2_EXCEPT(BaseExceptionFilter(_SEH2_GetExceptionInformation()))
{
uExitCode = _SEH2_GetExceptionCode();
}
_SEH2_END;
ExitProcess(uExitCode);//当WinMain函数正常退出后,进程才退出
}
-------------------------------------------------------------------------------------
_BaseThreadStartupThunk@0: //一般普通线程的用户空间总入口(内核总入口是KiThreadStartup)
{
xor ebp, ebp
push ebx //用户自己的context*参数
push eax //用户自己的线程入口函数
push 0 //表示不会返回
jmp _BaseThreadStartup@8
}
__declspec(noreturn) //一般普通线程的入口
VOID BaseThreadStartup(LPTHREAD_START_ROUTINE lpStartAddress,//用户自己的线程入口函数
LPVOID lpParameter)//用户自己函数的context*
{
volatile UINT uExitCode = 0;
_SEH2_TRY //也置于try块中保护
{
uExitCode = (lpStartAddress)((PVOID)lpParameter);
}
_SEH2_EXCEPT(BaseThreadExceptionFilter(_SEH2_GetExceptionInformation()))
{
uExitCode = _SEH2_GetExceptionCode();
} _SEH2_END;
ExitThread(uExitCode);//用户自己的线程入口函数返回后,线程自然退出
}
注:
Dll可以在进程启动初期,被PE加载器静态加载外,程序员也可以调用LoadLibrary API显式的动态加载。看一下这个函数的原理,实际上这个函数不是API,是个宏,指向LoadLibraryW/LoadLibraryA。
HINSTANCE LoadLibraryW (LPCWSTR lpLibFileName)
{
return LoadLibraryExW (lpLibFileName, 0, 0);
}
HINSTANCE
LoadLibraryExW (
LPCWSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags
)
{
if (dwFlags & DONT_RESOLVE_DLL_REFERENCES)
DllCharacteristics = IMAGE_FILE_EXECUTABLE_IMAGE;
dwFlags &= LOAD_WITH_ALTERED_SEARCH_PATH;
SearchPath = GetDllLoadPath(lpLibFileName);//构造该dll的标准搜索路径
RtlInitUnicodeString(&DllName, (LPWSTR)lpLibFileName);
if (dwFlags & LOAD_LIBRARY_AS_DATAFILE)
{
//在加载模块表中查找该dll
Status = LdrGetDllHandle(SearchPath, NULL, &DllName, (PVOID*)&hInst);
if (!NT_SUCCESS(Status))//若找不到
{
Status = LoadLibraryAsDatafile(SearchPath, DllName.Buffer, &hInst);
Return Status;
}
}
if (InWindows) //Windows中的实现
Status = LdrLoadDll(SearchPath,&DllCharacteristics,&DllName, (PVOID*)&hInst);
Else //ROS中的实现
Status = LdrLoadDll(SearchPath, &dwFlags, &DllName, (PVOID*)&hInst);
if ( !NT_SUCCESS(Status))
{
SetLastErrorByStatus (Status);
return NULL;
}
return hInst;
}
//下面的函数用来从指定dll文件路径构造一个dll搜索路径(完整原代码)
LPWSTR GetDllLoadPath(LPCWSTR lpModule)
{
ULONG Pos = 0, Length = 0;
PWCHAR EnvironmentBufferW = NULL;
LPCWSTR lpModuleEnd = NULL;
UNICODE_STRING ModuleName;
DWORD LastError = GetLastError();
if ((lpModule != NULL) && (wcslen(lpModule) > 2) && (lpModule[1] == ':'))
lpModuleEnd = lpModule + wcslen(lpModule);
else
{
ModuleName = NtCurrentPeb()->ProcessParameters->ImagePathName;
lpModule = ModuleName.Buffer;
lpModuleEnd = lpModule + (ModuleName.Length / sizeof(WCHAR));
}
if (lpModule != NULL)
{
while (lpModuleEnd > lpModule && *lpModuleEnd != L'/' &&
*lpModuleEnd != L'\\' && *lpModuleEnd != L':')
{
--lpModuleEnd;
}
Length = (lpModuleEnd - lpModule) + 1;
}
//看到没,LoadLibrary的dll搜索路径顺序是这样(注意与静态加载时的搜索路径不同)
Length += GetCurrentDirectoryW(0, NULL);
Length += GetDllDirectoryW(0, NULL);
Length += GetSystemDirectoryW(NULL, 0);
Length += GetWindowsDirectoryW(NULL, 0);
Length += GetEnvironmentVariableW(L"PATH", NULL, 0);
EnvironmentBufferW = RtlAllocateHeap(RtlGetProcessHeap(), 0,Length * sizeof(WCHAR));
if (lpModule)
{
RtlCopyMemory(EnvironmentBufferW, lpModule, (lpModuleEnd - lpModule) *sizeof(WCHAR));
Pos += lpModuleEnd - lpModule;
EnvironmentBufferW[Pos++] = L';';
}
Pos += GetCurrentDirectoryW(Length, EnvironmentBufferW + Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetDllDirectoryW(Length - Pos, EnvironmentBufferW + Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetSystemDirectoryW(EnvironmentBufferW + Pos, Length - Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetWindowsDirectoryW(EnvironmentBufferW + Pos, Length - Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetEnvironmentVariableW(L"PATH", EnvironmentBufferW + Pos, Length - Pos);
SetLastError(LastError);
return EnvironmentBufferW;
}
NTSTATUS NTAPI
LdrLoadDll (IN PWSTR SearchPath OPTIONAL,
IN PULONG LoadFlags OPTIONAL,
IN PUNICODE_STRING Name,
OUT PVOID *BaseAddress)//也即返回的hModule
{
PPEB Peb = NtCurrentPeb();
Status = LdrpLoadModule(SearchPath, LoadFlags ? *LoadFlags : 0, Name, &Module, BaseAddress);
if (NT_SUCCESS(Status) && (!LoadFlags || 0 == (*LoadFlags & LOAD_LIBRARY_AS_DATAFILE)))
{
if (!(Module->Flags & LDRP_PROCESS_ATTACH_CALLED))
Status = LdrpAttachProcess();//通知一个ProcessAttach消息
}
*BaseAddress = NT_SUCCESS(Status) ? Module->DllBase : NULL;
return Status;
}
下面的函数在每次一个新线程创建时调用,用以调用各个模块的DllMain和tls回调函数
NTSTATUS LdrpAttachThread (VOID)
{
Status = LdrpInitializeTlsForThread();
if (NT_SUCCESS(Status))
{
ModuleListHead = &NtCurrentPeb()->Ldr->InInitializationOrderModuleList;
Entry = ModuleListHead->Flink;
while (Entry != ModuleListHead)//遍历初始化顺序模块表
{
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InInitializationOrderModuleList);
if (Module->Flags & LDRP_PROCESS_ATTACH_CALLED &&
!(Module->Flags & LDRP_DONT_CALL_FOR_THREADS) &&
!(Module->Flags & LDRP_UNLOAD_IN_PROGRESS))
{
//调用DllMain,注意是DLL_THREAD_ATTACH通知码
LdrpCallDllEntry(Module, DLL_THREAD_ATTACH, NULL);
}
Entry = Entry->Flink;
}
Entry = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Flink;//exe模块
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
LdrpTlsCallback(Module, DLL_THREAD_ATTACH);
}
return Status;
}
下面的函数在主线程创建时调用,用以调用各个模块的DllMain和tls回调函数
NTSTATUS LdrpAttachProcess(VOID)
{
NTSTATUS Status = STATUS_SUCCESS;
ModuleListHead = &NtCurrentPeb()->Ldr->InInitializationOrderModuleList;
Entry = ModuleListHead->Flink;
while (Entry != ModuleListHead)
{
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY,
InInitializationOrderModuleList);
if (!(Module->Flags & (LDRP_LOAD_IN_PROGRESS|LDRP_UNLOAD_IN_PROGRESS|
LDRP_ENTRY_PROCESSED)))
{
Module->Flags |= LDRP_LOAD_IN_PROGRESS;
//调用DllMain,注意是DLL_PROCESS_ATTACH通知码
Result = LdrpCallDllEntry(Module, DLL_PROCESS_ATTACH, (Module->LoadCount ==
LDRP_PROCESS_CREATION_TIME ? 1 : 0));
if (Module->Flags & LDRP_IMAGE_DLL && Module->EntryPoint != 0)
Module->Flags |= LDRP_PROCESS_ATTACH_CALLED|LDRP_ENTRY_PROCESSED;
else
Module->Flags |= LDRP_ENTRY_PROCESSED;
Module->Flags &= ~LDRP_LOAD_IN_PROGRESS;
}
Entry = Entry->Flink;
}
return Status;
}
线程调度与切换:
众所周知:Windows系统是一个分时抢占式系统,分时指每个线程分配时间片,抢占指时间片到期前,中途可以被其他更高优先级的线程强制抢占。
背景知识:每个cpu都有一个TSS,叫‘任务状态段’。这个TSS内部中的一些字段记录着该cpu上当前正在运行的那个线程的一些信息(如ESP0记录着该线程的内核栈位置,IO权限位图记录着当前线程的IO空间权限)
IO空间有64KB,IO权限位图中的每一位记录着对应IO地址的IN、OUT许可权限,所以IO权限位图本身有8KB大小,TSS中就就记录着当前线程IO权限位图的偏移位置。
每当切换线程时:自然要跟着修改TSS中的ESP0和IO权限位图。TSS0中为什么要保存当前线程的内核栈位置?原因是:每当一个线程内部,从用户模式进入内核模式时,需要将cpu中的esp换成该线程的内核栈(各线程的内核栈是不同的)每当进入内核模式时,cpu就自动从TSS中找到ESP0,然后MOV ESP, TSS.ESP0,换成内核栈后,cpu然后在内核栈中压入浮点寄存器和标准的5个寄存器:原cs、原eip、原ss、原esp、原eflags。这就是为什么需要在TSS中记录当前线程的内核栈地址。(注意ESP0并不是栈底地址,而是要压入保存寄存器处的存放地址)
与线程切换相关的数据结构定义:
Struct KPCR //处理器控制块(内核中的fs寄存器总是指向这个结构体的基址)
{
KPCR_TIB Tib;
KPCR* self;//方便寻址
KPRCB* Prcb;
KIRQL irql;//物理上表示cpu的当前中断级,逻辑上理解为当前线程的中断级更好
USHORT* IDT;//本cpu的中断描述符表的地址
USHORT* GDT;//本cpu的全局描述符表的地址
KTSS* TSS;//本cpu上当前线程的信息(ESP0)
…
}
Struct KPCR_TIB
{
Void* ExceptionList;//当前线程的内核seh链表头结点地址
Void* StackBase;//内核栈底地址
Void* StackLimit;//栈的提交边界
…
KPCR_TIB* self;//方便寻址
}
Struct KPRCB
{
…
KTHREAD* CurrentThread;//本cpu上当前正在运行的线程
KTHREAD* NextThread;//将剥夺(即抢占)当前线程的下一个线程
KTHREAD* IdleThread;//空转线程
BOOL QuantumEnd;//重要字段。指当前线程的时间片是否已经用完。
LIST_ENTRY WaitListHead;//本cpu的等待线程队列
ULONG ReadSummary;//各就绪队列中是否为空的标志
ULONG SelectNextLast;
LIST_ENTRY DispatcherReadyListHead[32];//对应32个优先级的32个就绪线程队列
FX_SAVE_AREA NpxSaveArea;
…
}
typedef struct _KSWITCHFRAME //切换帧(用来保存切换线程)
{
PVOID ExceptionList;//保存线程切换时的内核she链表(不是用户空间中的seh)
Union
{
BOOLEAN ApcBypassDisable;//用于首次调度
UCHAR WaitIrql;//用于保存切换时的WaitIrql
};
//实际上首次时为KiThreadStartup,以后都固定为call KiSwapContextInternal后面的那条指令
PVOID RetAddr;//保存发生切换时的断点地址(以后切换回来时从这儿继续执行)
} KSWITCHFRAME, *PKSWITCHFRAME;
typedef struct _KTRAP_FRAME //Trap现场帧
{
------------------这些是KiSystemService保存的---------------------------
ULONG DbgEbp;
ULONG DbgEip;
ULONG DbgArgMark;
ULONG DbgArgPointer;
ULONG TempSegCs;
ULONG TempEsp;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
ULONG SegGs;
ULONG SegEs;
ULONG SegDs;
ULONG Edx;//xy 这个位置不是用来保存edx的,而是用来保存上个Trap帧,因为Trap帧是可以嵌套的
ULONG Ecx; //中断和异常引起的自陷要保存eax,系统调用则不需保存ecx
ULONG Eax;//中断和异常引起的自陷要保存eax,系统调用则不需保存eax
ULONG PreviousPreviousMode;
struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh链表的开头地址
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Ebp;
----------------------------------------------------------------------------------------
ULONG ErrCode;//发生的不是中断,而是异常时,cpu还会自动在栈中压入对应的具体异常码在这儿
-----------下面5个寄存器是由int 2e内部本身保存的或KiFastCallEntry模拟保存的现场---------
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp;
ULONG HardwareSegSs;
---------------以下用于用于保存V86模式的4个寄存器也是cpu自动压入的-------------------
ULONG V86Es;
ULONG V86Ds;
ULONG V86Fs;
ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;
下面这个核心函数用来切换线程(从当前线程切换到新线程去)。这个函数的原型是:
BOOL FASTCALL KiSwapContex(KTHREAD* Currentthread*, KTHREAD* NewThread);
返回值表示下次切换回来时是否需要手动扫描执行内核APC。这个函数的汇编代码为:
@KiSwapContext@8: //开头的@表示fastcall调用约定
{
sub esp, 4 * 4 //腾出局部变量空间
//保存这4个寄存器,因为KiSwapContextInternal函数内部要使用这几个寄存器
mov [esp+12], ebx
mov [esp+8], esi
mov [esp+4], edi
mov [esp+0], ebp
mov ebx, fs:[KPCR_SELF] //ebx=当前cpu的KPCR*
mov edi, ecx //edi= KiSwapContext的第一个参数,即CurrentThread
mov esi, edx //edi= KiSwapContext的第而个参数,即NewThread
movzx ecx, byte ptr [edi+KTHREAD_WAIT_IRQL] //ecx=当前线程的WaitIrql
call @KiSwapContextInternal@0 //调用真正的切换工作函数
这中间已经被切换到新线程去了,当前线程已经让出cpu,挂入了就绪队列。需要等到下次重新被调度运行时,才又从这儿的断点处继续向下执行下去
mov ebp, [esp+0] //这条指令就是断点处,以后切换回来时就从这个断点处继续执行
mov edi, [esp+4]
mov esi, [esp+8]
mov ebx, [esp+12
add esp, 4 * 4
ret
}
下面的函数完成真正的切换工作(返回值表示切换回来后是否需要手动扫描执行内核apc)
@KiSwapContextInternal@0: //edi指向当前线程,esi指向要切换到的新线程,ebx指向当前KPCR*
{
inc dword ptr es:[ebx+KPCR_CONTEXT_SWITCHES] //递增当前cpu上发生的历史线程切换计数
push ecx //保存本线程切换时的WaitIrql
push [ebx+KPCR_EXCEPTION_LIST] //保存本线程切换时的内核seh链表
-------------------------至此,上面的两条push连同本函数的返回地址(即断点地址),就构成了一个切换帧。当前线程切换时的内核栈顶位置就在此处-----------------------------
AfterTrace:
mov ebp, cr0
mov edx, ebp //将cr0寄存器保存在edx中(cr0的Bit3位“TaskSwitched”标志位,与浮点运算相关)
SetStack:
mov [edi+KTHREAD_KERNEL_STACK], esp //保存本线程切换时的内核栈顶位置
mov eax, [esi+KTHREAD_INITIAL_STACK] //eax=新线程的内核栈底地址
--------------------------------------------------------------------------------
cli //下面检查Npx浮点寄存器,要关中断
movzx ecx, byte ptr [esi+KTHREAD_NPX_STATE] //ecx=新线程的Npx状态
and edx, ~(CR0_MP + CR0_EM + CR0_TS)
or ecx, edx
or ecx, [eax - (NPX_FRAME_LENGTH - FN_CR0_NPX_STATE)] //获得新线程需要的cr0
cmp ebp, ecx
jnz NewCr0 //如果新线程需要的cr0不同于当前的cr0,则修改当前cr0为新线程的cr0
StackOk:
Sti
--------------------------------------------------------------------------------
mov esp, [esi+KTHREAD_KERNEL_STACK] //关键。恢复成新线程当初被切换时的内核栈顶
mov ebp, [esi+KTHREAD_APCSTATE_PROCESS] //ebp=目标进程
mov eax, [edi+KTHREAD_APCSTATE_PROCESS] //eax=当前进程
cmp ebp, eax //检查是否是切换到同一个进程中的其他线程(若是。就不用切换LDT和cr3)
jz SameProcess
//若切换到其他进程中的线程,则要同时修改LDT和CR3
mov ecx, [ebp+KPROCESS_LDT_DESCRIPTOR0]
or ecx, [eax+KPROCESS_LDT_DESCRIPTOR0]
jnz LdtReload //如果两个进程的LDT不同,就要换用不同的LDT
UpdateCr3:
mov eax, [ebp+KPROCESS_DIRECTORY_TABLE_BASE]
mov cr3, eax //关键。将cr3换成目标进程的页目录
SameProcess:
xor eax, eax
mov gs, ax
mov eax, [esi+KTHREAD_TEB] //新线程的TEB地址
mov [ebx+KPCR_TEB], eax //当前KPCR中的TEB指向新线程的TEB
mov ecx, [ebx+KPCR_GDT]
//修改GDT中的TEB描述符,指向新线程的TEB
mov [ecx+0x3A], ax
shr eax, 16
mov [ecx+0x3C], al
mov [ecx+0x3F], ah
mov eax, [esi+KTHREAD_INITIAL_STACK] //eax=新线程的内核栈底位置
sub eax, NPX_FRAME_LENGTH //跳过浮点保存区空间
test dword ptr [eax - KTRAP_FRAME_SIZE + KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK
jnz NoAdjust //检查新线程是否运行在V86模式
sub eax, KTRAP_FRAME_V86_GS - KTRAP_FRAME_SS //跳过V86保存区
NoAdjust:
mov ecx, [ebx+KPCR_TSS]
mov [ecx+KTSS_ESP0], eax //关键,修改TSS中的ESP0,指向新线程的内核栈底
mov ax, [ebp+KPROCESS_IOPM_OFFSET]
mov [ecx+KTSS_IOMAPBASE], ax //修改TSS中的IO权限位图偏移指向新进程中的IO权限位图
inc dword ptr [esi+KTHREAD_CONTEXT_SWITCHES] //递增线程的切换次数(也即历史调度次数)
pop [ebx+KPCR_EXCEPTION_LIST] //将当前KPCR中记录的seh链表恢复成新线程的seh链表
pop ecx //ecx=新线程原来切换前的WaitIrql
cmp byte ptr [ebx+KPCR_PRCB_DPC_ROUTINE_ACTIVE], 0 //检查当前是否有DPC函数处于活动状态
jnz BugCheckDpc //蓝屏
//至此,cpu中的寄存器内容全部换成了新线程的那些寄存器,从这个意思上说,此时就已完成了全部切换工作,下面的代码都是在新线程的环境中运行了。
--------------------------------新线程环境---------------------------------------
cmp byte ptr [esi+KTHREAD_PENDING_KERNEL_APC], 0
jnz CheckApc //看到没,每次线程得到重新调度运行前,都会扫描执行内核apc队列中的函数
xor eax, eax
ret //此处返回值表示没有内核apc
CheckApc:
cmp word ptr [esi+KTHREAD_SPECIAL_APC_DISABLE], 0 //检查是否禁用了APC
jnz ApcReturn
test cl, cl //检查WaitIrql,如果是APC级,就在本函数内部返回前,发出apc中断
jz ApcReturn
//if(SPECIAL APC 没禁用 && WaitIrql!=PASSIVE_LEVEL),切换回来时就先执行内核APC
mov cl, APC_LEVEL
call @HalRequestSoftwareInterrupt@4 //发出一个apc中断
or eax, esp //既然发出apc中断了,那么就return FALSE表示无需手动扫描执行apc
ApcReturn:
setz al
ret //此处返回值表示切回来后是否需要手动扫描执行apc
//当这个函数返回时,之前已经换成新线程的内核栈了。当函数返回后,将回到KiSwapContext中,当KiSwapContext返回到调用方时,那个调用方就是新线程当初调用的KiSwapContext的函数,这样,就沿着新线程的内核栈,逐级向上回溯到新线程中了。因此,可以说,切换内核栈,即是切换线程。
LdtReload:
mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR0]
test eax, eax //检测目标进程有没有LDT
jz LoadLdt
mov ecx, [ebx+KPCR_GDT]
mov [ecx+KGDT_LDT], eax //改指目标进程的LDT
mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR1]
mov [ecx+KGDT_LDT+4], eax//改指目标进程的LDT
/* Write the INT21 handler */
mov ecx, [ebx+KPCR_IDT]
mov eax, [ebp+KPROCESS_INT21_DESCRIPTOR0]
mov [ecx+0x108], eax
mov eax, [ebp+KPROCESS_INT21_DESCRIPTOR1]
mov [ecx+0x10C], eax
mov eax, KGDT_LDT
LoadLdt:
lldt ax
jmp UpdateCr3
NewCr0:
mov cr0, ecx
jmp StackOk
BugCheckDpc:
mov eax, [edi+KTHREAD_INITIAL_STACK]
push 0
push eax
push esi
push edi
push ATTEMPTED_SWITCH_FROM_DPC
call _KeBugCheckEx@20 //蓝屏提示:“尝试从活动DPC例程中切换线程”
}
如上:线程从KiSwapContextInternal这个函数内部切换出去,某一时刻又切换回这个函数内。
或者也可以理解为:线程从KiSwapContext这个函数切换出去,某一时刻又切换回这个函数内。
(注:可以hook这两个函数,来达到检测隐藏进程的目的)
明白了线程切换的过程,所做的工作后,接下来看:线程的切换时机(也即一个线程什么时候会调用
KiSwapContext这个函数把自己切换出去),相信这是大伙最感兴趣的问题。
线程的调度策略与切换时机:
调度策略:Windows严格按优先级调度线程。
优先级分成32个,每个cpu对应有32个就绪线程队列。每当要发生线程切换时,就根据调度策略从32条就绪队列中,按优先级从高到低的顺序扫描(同一个就绪队列中,由于优先级相同,则按FIFO顺序扫描),这样,从32条就绪队列中,找到优先级最高的那个候选就绪线程,给予调度执行。
当一个线程得到调度执行时,如果一直没有任何其他就绪线程的优先级高于本线程,本线程就可以畅通无阻地一直执行下去,直到本次的时间片用完。但是如果本次执行的过程中,如果有个就绪线程的优先级突然高于了本线程,那么本线程将被抢占,cpu将转去执行那个线程。但是,这种抢占可能不是立即性的,只有在当前线程的irql在DISPATCH_LEVEL以下(不包括),才会被立即抢占,否则,推迟抢占(即把那个高优先级的就绪线程暂时记录到当前cpu的KPCR结构中的NextThread字段中,标记要将抢占)。
切换时机:一句话【时片、抢占、等、主动】
1、 时间片耗尽
2、 被抢占
3、 因等待事件、资源、信号时主动放弃cpu(如调用WaitForSingleObject)
4、 主动切换(如主动调用SwitchToThread这个Win32 API)
但是:即使到了切换时机了,也只有当线程的irql在DISPATCH_LEVEL以下(不包括)时,才可以被切换出去,否则,线程将继续占有cpu,一直等到irql降到DISPATCH_LEVEL以下。
线程的状态(不含挂起态,其实挂起态本质上也是一种等待态)
1、Ready就绪态(挂入相应的就绪队列)
2、某一时刻得到调度变成Running运行态
3、因等待某一事件、信号、资源等变成Waiting等待状态
4、Standby状态。指处于抢占者状态(NextThread就是自己)
5、DeferredReady状态。指‘将’进入就绪态。
先看一下主动放弃cpu,切换线程的函数
NTSTATUS NtYieldExecution()
{
NTSTATUS Status = STATUS_NO_YIELD_PERFORMED;
KIRQL OldIrql;
PKPRCB Prcb = KeGetCurrentPrcb();//当前cpu的控制块
PKTHREAD Thread = KeGetCurrentThread(), NextThread;
if (Prcb->ReadySummary==0)
return Status;//如果没有其他线程处于就绪态,就不用切换了
//重要。线程的调度过程与切换过程,本身就运行在SynchLevel,目的是防止在执行调度、切换工作的过程中又被切换了出去。因此,可以说,调度、切换这个过程是原子的。
OldIrql = KeRaiseIrqlToSynchLevel();//先提到SynchLevel,再做调度、切换工作
if (Prcb->ReadySummary!=0)//如果当前cpu上有就绪线程
{
KiAcquireThreadLock(Thread);
KiAcquirePrcbLock(Prcb);
if (Prcb->NextThread != NULL)
NextThread = Prcb->NextThread;//优先选择那个等待抢占的线程
Else //如果当前没有候选抢占线程,就从就绪队列调度出一个线程
NextThread = KiSelectReadyThread(1, Prcb);
if (NextThread)
{
Thread->Quantum = Thread->QuantumReset;//设置下次调度运行的时间片
Thread->Priority = KiComputeNewPriority(Thread, 1);//略微降低一个优先级
KiReleaseThreadLock(Thread);
KiSetThreadSwapBusy(Thread);//标记本线程正在被切换
Prcb->CurrentThread = NextThread;//标记已切换到下一个线程
Prcb->NextThread = NULL;//初始运行时尚未有任何抢占者线程
NextThread->State = Running;//标记线程状态正在运行
Thread->WaitReason = WrYieldExecution;//标记本线程上次被切换的原因是主动放弃
KxQueueReadyThread(Thread, Prcb);//将本线程转入就绪队列
Thread->WaitIrql = APC_LEVEL;//这将导致下次切换回来时会自动发出apc中断
MiSyncForContextSwitch(NextThread);
KiSwapContext(Thread, NextThread);//真正切换到目标线程
---------------------------华丽的分割线---------------------------------------
Status = STATUS_SUCCESS;//本线程下次切回来时继续从这里执行下去
}
else
{
KiReleasePrcbLock(Prcb);
KiReleaseThreadLock(Thread);
}
}
KeLowerIrql(OldIrql);//完成调度、切换过程后,降低到原irql(这个过程可能会执行apc)
return Status;
}
//下面就是调度策略:按优先级从高到低的顺序扫描32条就绪队列,取下最高优先级的线程
PKTHREAD
KiSelectReadyThread(IN KPRIORITY Priority,//指调度出的线程必须>=这个优先级
IN PKPRCB Prcb)//指定cpu
{
ULONG PrioritySet;
LONG HighPriority;//含有就绪线程的最高优先级队列
PLIST_ENTRY ListEntry;
PKTHREAD Thread = NULL;//调度出来的线程
PrioritySet = Prcb->ReadySummary >> Priority;
if (!PrioritySet) goto Quickie;
BitScanReverse((PULONG)&HighPriority, PrioritySet);//从高位到地位扫描那个标志位图
HighPriority += Priority;
ASSERT(IsListEmpty(&Prcb->DispatcherReadyListHead[HighPriority]) == FALSE);
ListEntry = Prcb->DispatcherReadyListHead[HighPriority].Flink;//队列中的第一个线程
Thread = CONTAINING_RECORD(ListEntry, KTHREAD, WaitListEntry);
ASSERT(HighPriority == Thread->Priority);//确保优先级符合
ASSERT(Thread->Affinity & AFFINITY_MASK(Prcb->Number));//确保cpu亲缘性
ASSERT(Thread->NextProcessor == Prcb->Number);//确保是在那个cpu中等待调度
if (RemoveEntryList(&Thread->WaitListEntry))//取下来
Prcb->ReadySummary ^= PRIORITY_MASK(HighPriority);//如果队列变空了,修改对应的标志位
Quickie:
return Thread;
}
每当一个非实时线程被切换出去,放弃cpu后,系统都会略微降低该线程的优先级,以免该线程总是占住cpu不放。下面的函数就是做这个目的。
SCHAR KiComputeNewPriority(IN PKTHREAD Thread,//非实时线程
IN SCHAR Adjustment)//‘调减量’
{
SCHAR Priority;
Priority = Thread->Priority;//原优先级
if (Priority < LOW_REALTIME_PRIORITY)//只对非实时性线程做调整
{
//先减去‘恢减量’(对应于唤醒线程时系统临时提高的优先级量,现在要把它恢复回去)
Priority -= Thread->PriorityDecrement;
//再减去‘调减量’,这才是真正的调整,上面只是恢复优先级
Priority -= Adjustment;
if (Priority < Thread->BasePriority)
Priority = Thread->BasePriority;//优先级不管怎么调,不能低于基本优先级
Thread->PriorityDecrement = 0;
}
return Priority;
}
下面的函数用来将现场加入指定cpu的相应优先级的就绪队列
VOID KxQueueReadyThread(IN PKTHREAD Thread,IN PKPRCB Prcb)
{
BOOLEAN Preempted;
KPRIORITY Priority;
ASSERT(Prcb == KeGetCurrentPrcb());
ASSERT(Thread->State == Running);
ASSERT(Thread->NextProcessor == Prcb->Number);
{
Thread->State = Ready;//有运行态改为就绪态
Priority = Thread->Priority;
Preempted = Thread->Preempted;//表示是否是因为被抢占原因而让出的cpu
Thread->Preempted = FALSE;
Thread->WaitTime = KeTickCount.LowPart;//记录上次被切换的时间
//若是被抢占原因让出的cpu,就把那个线程加入队列的开头,以平衡它的怒气,否则加入尾部
Preempted ? InsertHeadList(&Prcb->DispatcherReadyListHead[Priority],
&Thread->WaitListEntry) :
InsertTailList(&Prcb->DispatcherReadyListHead[Priority],
&Thread->WaitListEntry);
Prcb->ReadySummary |= PRIORITY_MASK(Priority);//标志相应的就绪队列不空
KiReleasePrcbLock(Prcb);
}
}
前面说的主动切换。但主动切换是非常少见的,一般都是不情愿的,被动切换。典型的被动切换情形是:
每触发一次时钟中断(通常每10毫秒触发一次),就会在时钟中断的isr中递减当前线程KTHREAD结构中的Quantum字段(表示剩余时间片),当减到0时(也即时间片耗尽时),会将KPCRB结构中的QuantumEnd字段标记为TRUE。同时,当cpu在每次扫描执行完DPC队列中的函数后,irql将降到DISPATCH_LEVEL以下,这时系统会检查QuantumEnd字段,若发现时间片已经用完(可能已经用完很久了),就会调用下面的函数切换线程,这时切换线程的一种典型时机。
VOID KiQuantumEnd() //每次时间片自然到期后执行这个函数
{
PKPRCB Prcb = KeGetCurrentPrcb();
PKTHREAD NextThread, Thread = Prcb->CurrentThread;//当前线程
if (InterlockedExchange(&Prcb->DpcSetEventRequest, 0))//检查是否有‘触发DPC事件’的请求
KeSetEvent(&Prcb->DpcEvent, 0, 0);
KeRaiseIrqlToSynchLevel();//提升到SynchLevel,准备调度、切换
KiAcquireThreadLock(Thread);
KiAcquirePrcbLock(Prcb);
if (Thread->Quantum <= 0)//确认该线程的时间片已到期
{
if ((Thread->Priority >= LOW_REALTIME_PRIORITY) &&
(Thread->ApcState.Process->DisableQuantum))
{
Thread->Quantum = MAX_QUANTUM;//实时线程可以禁用时间片机制
}
else
{
Thread->Quantum = Thread->QuantumReset;//设置下次调度时的时间片
Thread->Priority = KiComputeNewPriority(Thread,1);//降低一个优先级(以免占住cpu)
if (Prcb->NextThread != NULL)
{
NextThread = Prcb->NextThread//直接使用这个候选的线程
Thread->Preempted = FALSE;//因为是时间片到期发生的切换,所以不是被抢占
}
else
{
NextThread = KiSelectReadyThread(Thread->Priority, Prcb);//调度出一个线程
//表示这个线程已被选中处于候选抢占状态,将立马上架投入运行
NextThread->State = Standby;
}
}
}
KiReleaseThreadLock(Thread);
KiSetThreadSwapBusy(Thread);//标记当前线程正在被切换
Prcb->CurrentThread = NextThread;//标记为切换到下一个线程了
Prcb->NextThread = NULL;//初始运行时没有抢占者线程
NextThread->State = Running;//已在运行了
Thread->WaitReason = WrQuantumEnd;//标记上次被切换的原因是时间片到期
KxQueueReadyThread(Thread, Prcb);//当前线程转入就绪队列
Thread->WaitIrql = APC_LEVEL;// 这将导致下次切换回来时会自动发出apc中断
KiSwapContext(Thread, NextThread);//正式切换到新线程
---------------------------华丽的分割线---------------------------------------
KeLowerIrql(DISPATCH_LEVEL);
}
除了时间片自然到期,线程被切换外,线程还可以在运行的过程中被其他高优先级线程,强制抢占而切换。
如一个线程调用ResumeThread将别的线程恢复调度时,自己会检查那个刚被恢复成就绪态的线程是否因优先级高于自己而要抢占本线程,如果是,就会切换到那个线程。因此这个api内部有切换线程的可能
ULONG KeResumeThread(IN PKTHREAD Thread) //恢复指定目标线程
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);//当前irql一定<=DISPATCH_LEVEL
KiAcquireApcLock(Thread, &ApcLock);//锁定apc队列,同时提升irql到DISPATCH_LEVEL
PreviousCount = Thread->SuspendCount;
if (PreviousCount)
{
Thread->SuspendCount--;//递减挂起计数
//若挂起计数减到0,唤醒目标线程,进入就绪队列或者变成抢占者线程
if ((!Thread->SuspendCount) && (!Thread->FreezeCount))
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState++;
KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT);//尝试唤醒它
KiReleaseDispatcherLockFromDpcLevel();
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);//注意这个函数只释放apc队列锁,不降低irql
//关键函数。降低当前线程的irql,同时先检查是否有抢占者线程,若有,先执行抢占切换。
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;//返回之前的挂起计数
}
//下面这个函数的主功能是降回当前线程的irql到指定OldIrql。不过在正式的降低前,会先检查是否发生了抢占,若有,就先执行线程切换,等下次切换回来后再降低当前线程的irql。
//这个函数经常在系统中的其它线程的运行状态一改变后,就主动调用。其目的是检测是否为此而发生了可能的抢占现象,若已发生,就立即进行抢占式切换。比如,改变了某其它线程的优先级,唤醒了某其他的线程,挂起恢复了某其他线程,给某线程挂入了一个APC等等操作后,都会调用,以尝试立即切换。
VOID FASTCALL //注意,这个函数只能在DISPATCH_LEVEL及其以上irql级别调用
KiExitDispatcher(IN KIRQL OldIrql) //降低irql,检测是否有抢占
{
PKPRCB Prcb = KeGetCurrentPrcb();
PKTHREAD Thread, NextThread;
BOOLEAN PendingApc;
ASSERT(KeGetCurrentIrql() >= DISPATCH_LEVEL); //确保
KiCheckDeferredReadyList(Prcb);
if (OldIrql >= DISPATCH_LEVEL)//如果要降回的irql不在DISPATCH_LEVEL以下,那就不能切换
{
if ((Prcb->NextThread) && !(Prcb->DpcRoutineActive))
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
goto Quickie;
}
if (!Prcb->NextThread)//如果没有抢占者线程,那很好,直接降低irql就是
goto Quickie;
//若发现有抢占发生,下面将执行抢占切换
KiAcquirePrcbLock(Prcb);
NextThread = Prcb->NextThread;
Thread = Prcb->CurrentThread;
KiSetThreadSwapBusy(Thread);
Prcb->CurrentThread = NextThread;
Prcb->NextThread = NULL;
NextThread->State = Running;
KxQueueReadyThread(Thread, Prcb);
Thread->WaitIrql = OldIrql;//可以肯定:OldIrql=APC_LEVEL或PASSIVE_LEVEL,并且:如果原irql是在AP_LEVEL的话,KiSwapContext内部会在返回前发出apc中断
PendingApc = KiSwapContext(Thread, NextThread);
-------------------------------------华丽的分割线---------------------------------------
//如果切回来后发现阻塞有内核apc,需要手动扫描执行apc(可以肯定原irql不是APC_LEVEL)
if (PendingApc)
{
ASSERT(OldIrql == PASSIVE_LEVEL);//可以肯定原来是PASSIVE_LEVEL级
KeLowerIrql(APC_LEVEL);//当然要先降到APC级别去
KiDeliverApc(KernelMode, NULL, NULL);//切换回来后,自己手动扫描执行内核apc
}
Quickie:
KeLowerIrql(OldIrql);//本函数真正的工作:降低到指定irql
}
//如上,上面的函数在降低irql前,先尝试检测是否发生了抢占式切换。若有,立即切换。
否则,降低irql。注意降低irql到DISPATCH_LEVEL下以后,也可能会因为之前时间片早已到期,但是在DISPATCH_LEVEL以上迟迟没有得到切换,现在降到下面了就会引发线程切换(迟来的切换!)
当一个线程被唤醒时(如isr中将某线程唤醒),往往会提高其优先级,导致发生抢占。一旦发现某个线程的优先级高于当前线程的优先级(并且也高于上一个候选的抢占者线程的优先级),系统就会把这个线程作为新的候选抢占者线程记录到KPCRB结构的NextThread字段中。这样,只要时机一成熟吗,就会发生抢占式切换。
下面的函数用来唤醒一个线程
VOID FASTCALL
KiUnwaitThread(IN PKTHREAD Thread,
IN LONG_PTR WaitStatus,
IN KPRIORITY Increment)//略微提高的优先级量(以便目标线程尽快得到调度)
{
KiUnlinkThread(Thread, WaitStatus);//从所有等待对象的线程链表中脱链
Thread->AdjustIncrement = (SCHAR)Increment;//要调整的优先级量
Thread->AdjustReason = AdjustUnwait;//跳转原因为唤醒
KiReadyThread(Thread);//关键函数。将线程转为就绪态
}
下面的函数用来将一个线程转为就绪态
VOID KiReadyThread(IN PKTHREAD Thread)
{
IN PKPROCESS Process = Thread->ApcState.Process;
if (Process->State != ProcessInMemory)
ASSERT(FALSE);//蓝屏
else if (!Thread->KernelStackResident)//如果该线程的内核栈被置换到外存了
{
ASSERT(Process->StackCount != MAXULONG_PTR);
Process->StackCount++;
ASSERT(Thread->State != Transition);
Thread->State = Transition;
ASSERT(FALSE);//蓝屏
}
else
KiInsertDeferredReadyList(Thread);//实质函数
}
VOID KiInsertDeferredReadyList(IN PKTHREAD Thread)
{
Thread->State = DeferredReady;//将进入就绪态
Thread->DeferredProcessor = 0;//0号cpu
KiDeferredReadyThread(Thread);//实质函数,就绪化指定线程
}
//下面的函数将指定线程转换为‘就绪态’或者‘抢占态’
//也可理解为‘就绪化’某个线程,但特殊处理抢占情形(抢占态是一种特殊的就绪态)
VOID FASTCALL KiDeferredReadyThread(IN PKTHREAD Thread)
{
PKPRCB Prcb;
BOOLEAN Preempted;
ULONG Processor = 0;//一律挂入0号cpu的就绪队列
KPRIORITY OldPriority;//目标线程的当前优先级
PKTHREAD NextThread;
if (Thread->AdjustReason == AdjustBoost) //if是线程首次启动时的调整优先级 。。。
else if (Thread->AdjustReason == AdjustUnwait) //if是唤醒时调整的优先级 。。。
Preempted = Thread->Preempted;
OldPriority = Thread->Priority;
Thread->Preempted = FALSE;
Thread->NextProcessor = 0;
Prcb = KiProcessorBlock[0];
KiAcquirePrcbLock(Prcb);
if (KiIdleSummary)//如果0号cpu运行着空转线程,目标线程的优先级肯定高于那个空转线程
{
KiIdleSummary = 0;
Thread->State = Standby;//将目标程序改为‘抢占态’
Prcb->NextThread = Thread;//指向自己
KiReleasePrcbLock(Prcb);
return;
}
Thread->NextProcessor = (UCHAR)Processor;//0
NextThread = Prcb->NextThread;//获得0号cpu上的原抢占者线程
if (NextThread)//如果原来已有一个抢占者线程
{
ASSERT(NextThread->State == Standby);//可以确定那个线程处于抢占态
if (OldPriority > NextThread->Priority)//若高于原‘抢占者线程’的优先级
{
NextThread->Preempted = TRUE;//标志那个抢占者线程又被目标线程抢占了
Prcb->NextThread = Thread;//更改新的抢占者线程,时机一成熟就抢占
Thread->State = Standby;//更为抢占态
NextThread->State = DeferredReady;//原抢占者线程进入将就绪态
NextThread->DeferredProcessor = Prcb->Number;//0
KiReleasePrcbLock(Prcb);
KiDeferredReadyThread(NextThread);//原抢占者线程转入0号cpu就绪队列
return;
}
}
else//如果原来没有抢占者线程(最典型的情况)
{
NextThread = Prcb->CurrentThread;
if (OldPriority > NextThread->Priority)//如果优先级高于当前运行的那个线程
{
if (NextThread->State == Running)
NextThread->Preempted = TRUE;//标记已被抢占
Prcb->NextThread = Thread; //指定抢占者线程,时机一成熟就抢占
Thread->State = Standby;//标记目标线程处于抢占态了
KiReleasePrcbLock(Prcb);
if (KeGetCurrentProcessorNumber() != 0)
KiIpiSend(AFFINITY_MASK(Thread->NextProcessor), IPI_DPC);//给0号cpu发一个通知
return;
}
}
//如果目标线程的优先级低于当前的抢占者线程,也低于当前运行中的线程
Thread->State = Ready;//更为就绪态
Thread->WaitTime = KeTickCount.LowPart;//记录上次被切换的时间
//如果目标线程上次是因为被抢占而切出的cpu,现在就挂入队头(平衡怒气)
Preempted ? InsertHeadList(&Prcb->DispatcherReadyListHead[OldPriority],
&Thread->WaitListEntry) :
InsertTailList(&Prcb->DispatcherReadyListHead[OldPriority],
&Thread->WaitListEntry);
Prcb->ReadySummary |= PRIORITY_MASK(OldPriority);//更改相应就绪队列的标志
KiReleasePrcbLock(Prcb);
}
如上,上面这个函数用于将线程挂入0号cpu的就绪队列或者置为抢占者线程。
进程、线程的优先级:
线程的调度策略是严格按优先级的,因此,优先级,不妨叫做‘调度优先级’。那么优先级是啥,是怎么确定的呢?
先要弄清几个概念:
进程的优先级类:每种优先级类对应一种基本优先级
进程的基本优先级:为各个线程的默认基本优先级
线程的基本优先级:每个线程刚创建时的基本优先级继承它所属进程的基本优先级,但可以人为调整
线程的当前优先级:又叫时机优先级。当前优先级可以浮动,但永远不会降到该线程的基本优先级下面
系统调度线程时,是以线程的当前优先级为准的,它才不管你的基本优先级是什么,你所属的进程的基本优先级又是什么,它只看你的当前优先级。
进程基本优先级与线程基本优先级是一种水涨船高的关系。进程的基本优先级变高了,那么它里面的各个线程的基本优先级也会跟着升高对应的幅度。各个线程初始创建时的基本优先级等于其进程的基本优先级
线程的基本优先级与线程的当前优先级也是一种水涨船高的关系。线程的基本优先级升高了,那么线程的当前优先级也会跟着升高对应的幅度。另外:线程的当前优先级可以随时变化(比如每次一让出cpu时就略微降低那么一点点优先级),但是永远不会降到其基本优先级以下。基本优先级就是它的最低保障!
综上,可理解为:线程基本优先级相对于进程的基本优先级,线程的当前优先级相对于线程的基本优先级
线程1的当前优先级 线程2的当前优先级 线程3的当前优先级
线程1的基本优先级 线程2的基本优先级 线程3的基本优先级
进程的基本优先级
------------------------------------------------------------------------------------------
系统中总共分32个优先级:0到31,其中又分为两段。0到15的是非实时优先级,16-31的表示实时优先级。
#define LOW_PRIORITY 0
#define LOW_RELATIVE_PRIORITY 15 //最低的实时优先级
#define HIGH_PRIORITY 31//最高的实时优先级,也是整个系统最高的优先级
SetPriorityClass这个Win32 API改变的就是一个进程的优先级类,而一种优先级类对应一种基本优先级,所以这个函数实际上改变的是进程的基本优先级。实际上最终调用到下面的函数
KPRIORITY
KeSetPriorityAndQuantumProcess(IN PKPROCESS Process,
IN KPRIORITY Priority,//新的基本优先级
IN UCHAR Quantum OPTIONAL)//新的时间片
{
KLOCK_QUEUE_HANDLE ProcessLock;
KPRIORITY Delta;
PLIST_ENTRY NextEntry, ListHead;
KPRIORITY NewPriority, OldPriority;
PKTHREAD Thread;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
if (Process->BasePriority == Priority) return Process->BasePriority;
if (Priority==0) Priority = 1;//只有空转线程的优先级才能是0
KiAcquireProcessLock(Process, &ProcessLock);//获得自旋锁,同时提升irql到DISPATCH_LEVEL
if (Quantum)
Process->QuantumReset = Quantum;//修改进程的时间片(也即里面各个线程的时间片)
OldPriority = Process->BasePriority;
Process->BasePriority = (SCHAR)Priority;//修改为新的基本优先级
Delta = Priority - OldPriority;//计算提升幅度(注意Delta可以是负数)
ListHead = &Process->ThreadListHead;
NextEntry = ListHead->Flink;
if (Priority >= LOW_REALTIME_PRIORITY)//如果将基本优先级提到了实时级别
{
while (NextEntry != ListHead)//遍历该进程中的每个线程
{
Thread = CONTAINING_RECORD(NextEntry, KTHREAD, ThreadListEntry);
if (Quantum) Thread->QuantumReset = Quantum;//同时设置线程的时间片
KiAcquireThreadLock(Thread);
NewPriority = Thread->BasePriority + Delta;//水涨船高
if (NewPriority < LOW_REALTIME_PRIORITY)
NewPriority = LOW_REALTIME_PRIORITY;// 实时优先级的最小值
else if (NewPriority > HIGH_PRIORITY)
NewPriority = HIGH_PRIORITY;// 实时优先级的最大值
if (!(Thread->Saturation) || (OldPriority < LOW_REALTIME_PRIORITY))
{
Thread->BasePriority = (SCHAR)NewPriority; //水涨船高
Thread->Quantum = Thread->QuantumReset;//当前剩余时间片=初始时间片
Thread->PriorityDecrement = 0;
KiSetPriorityThread(Thread, NewPriority);//提高线程优先级要做的附加工作
}
KiReleaseThreadLock(Thread);
NextEntry = NextEntry->Flink;//下一个线程
}
}
else//如果将基本优先级提到了非实时级别
{
while (NextEntry != ListHead)
{
Thread = CONTAINING_RECORD(NextEntry, KTHREAD, ThreadListEntry);
if (Quantum) Thread->QuantumReset = Quantum;
KiAcquireThreadLock(Thread);
NewPriority = Thread->BasePriority + Delta;
if (NewPriority >= LOW_REALTIME_PRIORITY)
NewPriority = LOW_REALTIME_PRIORITY - 1;//非实时优先级的最大值
else if (NewPriority <= LOW_PRIORITY)
NewPriority = 1;//非实时优先级的最小值
if (!(Thread->Saturation) || (OldPriority >= LOW_REALTIME_PRIORITY))
{
Thread->BasePriority = (SCHAR)NewPriority;//水涨船高
Thread->Quantum = Thread->QuantumReset;//当前剩余时间片=初始的时间片
Thread->PriorityDecrement = 0;
KiSetPriorityThread(Thread, NewPriority); //提高线程优先级要做的附加工作
}
KiReleaseThreadLock(Thread);
NextEntry = NextEntry->Flink;//下一个线程
}
}
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseProcessLockFromDpcLevel(&ProcessLock);
//降低到原irql,同时先检查是否发生了抢占式切换(因为显式改变了线程的优先级,有可能让其他线程的优先级突然高于了当前线程而要发生抢占现象,所以要检测这种情况)
KiExitDispatcher(ProcessLock.OldIrql);
return OldPriority;
}
线程的基本优先级一变了,它的当前优先级就会跟着变,线程的当前优先级一变了,那么就会有很多的附加工作要做,下面的函数就用来做这个工作(如改变就绪队列、置为抢占者等)。
VOID FASTCALL //设置线程的当前优先级
KiSetPriorityThread(IN PKTHREAD Thread,
IN KPRIORITY Priority)//新的当前优先级
{
PKPRCB Prcb;
ULONG Processor;
BOOLEAN RequestInterrupt = FALSE;
KPRIORITY OldPriority;
PKTHREAD NewThread;
if (Thread->Priority != Priority)//if 优先级变了
{
for (;;)
{
if (Thread->State == Ready)//如果目标线程处于就绪态
{
if (!Thread->ProcessReadyQueue)//其实一般都会满足这个条件
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
//如果现在仍处于就绪态,并且仍在那个cpu上等待
if ((Thread->State == Ready) && (Thread->NextProcessor == Prcb->Number))
{
if (RemoveEntryList(&Thread->WaitListEntry))//从原就绪队列摘下
Prcb->ReadySummary ^= PRIORITY_MASK(Thread->Priority);
Thread->Priority = (SCHAR)Priority;//=更为新的优先级
KiInsertDeferredReadyList(Thread);//挂入新的就绪队列(或置为抢占态)
KiReleasePrcbLock(Prcb);
}
Else …
}
}
else if (Thread->State == Standby) //如果目标线程处于抢占态
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
if (Thread == Prcb->NextThread)//如果仍处于抢占态
{
OldPriority = Thread->Priority;
Thread->Priority = (SCHAR)Priority;//更改优先级
if (Priority < OldPriority)//如果优先级降了(可能不再成为抢占者线程了)
{
NewThread = KiSelectReadyThread(Priority + 1, Prcb);
if (NewThread)//如果选出了一个比现在的优先级更高的线程
{
NewThread->State = Standby;
Prcb->NextThread = NewThread;//更为新的抢占者线程
KiInsertDeferredReadyList(Thread);//原抢占线程则转入就绪队列
}
}
KiReleasePrcbLock(Prcb);
}
Else …
}
else if (Thread->State == Running) //如果目标线程正在运行
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
if (Thread == Prcb->CurrentThread)//如果仍在运行
{
OldPriority = Thread->Priority;
Thread->Priority = (SCHAR)Priority;//更改优先级
if ((Priority < OldPriority) && !(Prcb->NextThread))//可能会出现抢占
{
NewThread = KiSelectReadyThread(Priority + 1, Prcb);
if (NewThread)// 如果选出了一个比现在的优先级更高的线程
{
NewThread->State = Standby;
Prcb->NextThread = NewThread;//出现了新的抢占线程
RequestInterrupt = TRUE;//需要立即中断
}
}
KiReleasePrcbLock(Prcb);
if (RequestInterrupt)
{
//通知目标cpu进行抢占切换
if (KeGetCurrentProcessorNumber() != Processor)
KiIpiSend(AFFINITY_MASK(Processor), IPI_DPC);
}
}
Else …
}
Else …
break;
}
}
}
如上,这个函数改变目标线程的优先级为指定优先级,并根据目标线程的当前所处状态,最对应的就绪队列、抢占者线程调整。可见,强行改变某个线程的当前优先级并不是件简单的工作,需要全盘综合考虑各方面因素,做出相应的调整。
下面的函数是一个小型的封装函数:(他还会还原时间片)
KPRIORITY
KeSetPriorityThread(IN PKTHREAD Thread,
IN KPRIORITY Priority)
{
KIRQL OldIrql;
KPRIORITY OldPriority;
OldIrql = KiAcquireDispatcherLock();
KiAcquireThreadLock(Thread);
OldPriority = Thread->Priority;
Thread->PriorityDecrement = 0;
if (Priority != Thread->Priority)//if 优先级变了
{
Thread->Quantum = Thread->QuantumReset;//关键。还原时间片
KiSetPriorityThread(Thread, Priority);//再做真正的修改工作
}
KiReleaseThreadLock(Thread);
KiReleaseDispatcherLock(OldIrql);
return OldPriority;
}
除了修改进程的基本优先级会影响到里面每个线程的基本优先级和当前优先级外,也可以用下面的函数直接修改线程的基本优先级和当前优先级。
NTSTATUS
NtSetInformationThread(IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength)
{
…
switch (ThreadInformationClass)
{
case ThreadPriority://设置当前优先级
Priority = *(PLONG)ThreadInformation;//这个值是相对于进程基本优先级的差值
KeSetPriorityThread(&Thread->Tcb, Priority);
break;
case ThreadBasePriority://设置基本优先级
Priority = *(PLONG)ThreadInformation;
KeSetBasePriorityThread(&Thread->Tcb, Priority);
break;
case …
}//end switch
}//end func
线程的基本优先级(非当前优先级)可以用下面的函数设置:
LONG
KeSetBasePriorityThread(IN PKTHREAD Thread,
IN LONG Increment)//这个是相对于进程基本优先级的差值
{
KIRQL OldIrql;
KPRIORITY OldBasePriority, Priority, BasePriority;
LONG OldIncrement;
PKPROCESS Process;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
Process = Thread->ApcState.Process;
OldIrql = KiAcquireDispatcherLock();
KiAcquireThreadLock(Thread);
OldBasePriority = Thread->BasePriority;
OldIncrement = OldBasePriority - Process->BasePriority;
if (Thread->Saturation) //如果是个饱和增量
OldIncrement = 16 * Thread->Saturation;//16或-16
Thread->Saturation = 0;
if (abs(Increment) >= 16) //饱和增量
Thread->Saturation = (Increment > 0) ? 1 : -1;
BasePriority = Process->BasePriority + Increment;//算得现在的基本优先级
if (Process->BasePriority >= LOW_REALTIME_PRIORITY)
{
Priority = BasePriority;//实时线程例外,当前优先级=基本优先级
}
else
{
Priority = KiComputeNewPriority(Thread, 0);//其实就是当前优先级
//看到没,线程的基本优先级一升高,它的当前优先级跟着升高对应的幅度
Priority += (BasePriority - OldBasePriority);
}
Thread->BasePriority = (SCHAR)BasePriority;//更改线程的基本优先级
Thread->PriorityDecrement = 0;
if (Priority != Thread->Priority)//如果当前优先级变了,做相关的附加工作
{
Thread->Quantum = Thread->QuantumReset;
KiSetPriorityThread(Thread, Priority);
}
KiReleaseThreadLock(Thread);
KiReleaseDispatcherLock(OldIrql);
return OldIncrement;
}
线程局部存储:TLS
----对TLS这个概念陌生的朋友请先自己查阅相关资料。
TLS分为两种方法:静态tls、动态tls。两种方法都可以达到tls的目的。
静态tls:
在编写程序时:只需在要声明为tls的全局变量前加上__declspec(thread)关键字即可。如:
__declspec(thread) int g_a = 1;
__declspec(thread) int g_b;
__declspec(thread) int g_c = 0;
__declspec(thread) int g_d;
编译器在遇到这样的变量时,自然会将这种变量当做tls变量看待,编译链接存放到pe文件的.tls节中,
Exe文件中可使用静态tls,动态库文件中使用静态tls则会有很大的缺点,所以动态库文件中一般都使用动态tls来达到tls的目的。为此,Windows专门提供了一组api和相关基础设施来实现动态tls。
DWORD TlsAlloc():为当前线程分配一个tls槽。返回本线程分得的槽号
BOOL TlsSetValue(DWORD idx,void* val):写数据到指定槽中
VOID* TlsGetValue(DWORD ):从指定槽中读数据
BOOL TlsFree(DWORD idx);//释放这个槽给进程,使得其他线程可以分得这个槽
相关的结构:
Struct PEB
{
…
RTL_BITMAP* TlsBitmap;//标准的64位动态tls分配标志位图(固定使用下面的64位结构)
DWORD TlsBitmapBits[2];//内置的64bit大小的tls位图(每一位标志表示对应tls槽的分配情况)
…
}
Struct RTL_BITMAP
{
ULONG SizeOfBitmap;//动态tls位图的大小,默认就是8B(64bit)
BYTE* Buffer;//动态tls位图的地址,默认就指向PEB结构中的那个内置的tls位图。当要使用的tls槽个数超过64个时,将使用扩展的tls位图。
}
Struct TEB
{
…
Void* ThreadLocalStoragePointer;//本线程的那片静态tls区的地址
Void* TlsSlots[64];//内置的64个tls槽(每个槽中可以存放4B大小的任意数据)
Void* TlsExpansionSlots;//另外扩展的1024个tls槽
…
}
下面的函数分配一个空闲的tls槽,返回分到的槽号(即索引)
DWORD TlsAlloc()
{
ULONG Index;
RtlAcquirePebLock();
//先从标准的64位tls位图中找到一个空闲的tls槽(也即未被其他线程占用的tls槽)
Index = RtlFindClearBitsAndSet(NtCurrentPeb()->TlsBitmap, 1, 0);
if (Index == -1)//如果找不到
{
//再去扩展的tls槽位图中查找
Index = RtlFindClearBitsAndSet(NtCurrentPeb()->TlsExpansionBitmap, 1, 0);
if (Index != -1)//如果找到了
{
if (NtCurrentTeb()->TlsExpansionSlots == NULL)
{
NtCurrentTeb()->TlsExpansionSlots = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,1024 * sizeof(PVOID));
}
NtCurrentTeb()->TlsExpansionSlots[Index] = 0;//分到对应的槽后,自动将内容清0
Index += 64;
}
else
SetLastError(ERROR_NO_MORE_ITEMS);
}
else
NtCurrentTeb()->TlsSlots[Index] = 0; //分到对应的槽后,自动将内容清0
RtlReleasePebLock();
return Index;
}
下面的函数将数据写入指定tls槽中
BOOL TlsSetValue(DWORD Index, LPVOID Value)
{
if (Index >= 64) //扩展tls槽中
{
if (NtCurrentTeb()->TlsExpansionSlots == NULL)
{
NtCurrentTeb()->TlsExpansionSlots = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,1024 *sizeof(PVOID));
}
NtCurrentTeb()->TlsExpansionSlots[Index - 64] = Value;
}
else
NtCurrentTeb()->TlsSlots[Index] = Value;
return TRUE;
}
下面的函数读取指定tls槽中的值
LPVOID TlsGetValue(DWORD Index)
{
if (Index >= 64)
return NtCurrentTeb()->TlsExpansionSlots[Index - 64];
else
return NtCurrentTeb()->TlsSlots[Index];
}
下面的函数用来释放一个tls槽给进程
BOOL TlsFree(DWORD Index)
{
BOOL BitSet;
RtlAcquirePebLock();
if (Index >= 64)
{
//检测该tls槽是否已分配
BitSet = RtlAreBitsSet(NtCurrentPeb()->TlsExpansionBitmap,Index - 64,1);
if (BitSet)//若已分配,现在标记为空闲
RtlClearBits(NtCurrentPeb()->TlsExpansionBitmap,Index - 64,1);
}
else
{
BitSet = RtlAreBitsSet(NtCurrentPeb()->TlsBitmap, Index, 1);
if (BitSet)
RtlClearBits(NtCurrentPeb()->TlsBitmap, Index, 1);
}
if (BitSet)
{
//将所有线程的对应tls槽内容清0
NtSetInformationThread(NtCurrentThread(),ThreadZeroTlsCell,&Index,sizeof(DWORD));
}
else
SetLastError(ERROR_INVALID_PARAMETER);
RtlReleasePebLock();
return BitSet;
}
上面这些关于动态tls的函数都不难理解。动态tls功能强大,但使用起来不方便。静态tls不好用在动态库中,比较局限,但静态tls使用方便。话又说回来,静态的tls的使用方便背后,又包含着较为复杂的初始化流程。下面看静态tls的初始化流程。
回顾一下进程创建时的启动流程:
在进程启动时,初始化主exe文件的函数内部:
PEFUNC LdrPEStartup(…)
{
…
Status = LdrFixupImports(NULL, *Module);//加载子孙dll,修正IAT导入表
Status = LdrpInitializeTlsForProccess();//初始化进程的静态tls
if (NT_SUCCESS(Status))
{
LdrpAttachProcess();//发送一个ProcessAttach消息,调用该模块的DllMain函数
LdrpTlsCallback(*Module, DLL_PROCESS_ATTACH);//调用各模块的tls回调函数
}
…
}
钻进各个函数里面去看一下:
NTSTATUS LdrFixupImports(…)
{
…
if (TlsDirectory)
{
TlsSize = TlsDirectory->EndAddressOfRawData- TlsDirectory->StartAddressOfRawData
+ TlsDirectory->SizeOfZeroFill;
if (TlsSize > 0 && NtCurrentPeb()->Ldr->Initialized)//if 动态加载该模块
TlsDirectory = NULL;// 动态加载的模块不支持静态tls
}
…
if (TlsDirectory && TlsSize > 0)//处理静态加载的dll模块中的静态tls节
LdrpAcquireTlsSlot(Module, TlsSize, FALSE);
…
}
在修正每个exe、dll文件的导入表时,会检查该文件中.tls节的大小。由于这个函数本身也会被LoadLibrary函数在内部调用,所以,这个函数他会检测是不是在动态加载dll,若是,如果发现dll中含有静态tls节,就什么都不做。反之,若dll是在进程启动阶段静态加载的,就会调用LdrpAcquireTlsSlot处理那个模块中的tls节。具体是怎么处理的呢?我们看:
VOID LdrpAcquireTlsSlot(PLDR_DATA_TABLE_ENTRY Module, ULONG Size, BOOLEAN Locked)
{
if (!Locked)
RtlEnterCriticalSection (NtCurrentPeb()->LoaderLock);
Module->TlsIndex = LdrpTlsCount;//记录这个模块tls节的索引(即tls号)
LdrpTlsCount++;//递增进程中的tls节个数
LdrpTlsSize += Size;//递增进程中tls节总大小
if (!Locked)
RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
}
如上,每个模块在进程启动时的静态加载过程中,只是递增一下进程中总的tls节个数与大小,以及分配该模块的tls节编号,以便在进程完全初始化完成(即加载了所有模块)后,统一集中处理各模块中的静态tls节。
下面再看LdrPEStartup函数中调用的LdrpInitializeTlsForProccess函数,显然,这个函数是在LdrFixupImports函数加载了该exe依赖的所有子孙dll文件后才调用的。前面已经统计完了该进程中所有模块的所有tls节的总大小以及tls节总个数,现在就到调用这个函数集中统一处理该进程的静态tls时候了。我们看:
NTSTATUS LdrpInitializeTlsForProccess()
{
PLIST_ENTRY ModuleListHead;
PLIST_ENTRY Entry;
PLDR_DATA_TABLE_ENTRY Module;
PIMAGE_TLS_DIRECTORY TlsDirectory;
PTLS_DATA TlsData;
ULONG Size;
if (LdrpTlsCount > 0) //如果有模块中存在tls节
{
//分配一个tls描述符数组,用来记录各模块的tls节信息(注意分配的只是描述符,并不用来存放tls节体。另外,每个进程的tls描述符数组都记录在ntdll.dll模块中的LdrpTlsArray全局变量中)
LdrpTlsArray = RtlAllocateHeap(RtlGetProcessHeap(),0,
LdrpTlsCount * sizeof(TLS_DATA));
ModuleListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
Entry = ModuleListHead->Flink;
while (Entry != ModuleListHead)//遍历所有含有tls节的静态加载模块
{
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (Module->LoadCount ==-1 && Module->TlsIndex != -1)
{
//获得pe文件中tls目录的信息
TlsDirectory = RtlImageDirectoryEntryToData(Module->DllBase,
TRUE,IMAGE_DIRECTORY_ENTRY_TLS,&Size);
TlsData = &LdrpTlsArray[Module->TlsIndex];//指向该模块对应的描述符
//非0区在原模块中的地址
TlsData->StartAddressOfRawData = TlsDirectory->StartAddressOfRawData;
//非0区的大小
TlsData->TlsDataSize = TlsDirectory->EndAddressOfRawData - TlsDirectory->
StartAddressOfRawData;
//0区的大小(即尚未初始化的tls变量总大小)
TlsData->TlsZeroSize = TlsDirectory->SizeOfZeroFill;
//tls回调函数数组的地址
if (TlsDirectory->AddressOfCallBacks)
TlsData->TlsAddressOfCallBacks = TlsDirectory->AddressOfCallBacks;
else
TlsData->TlsAddressOfCallBacks = NULL;
TlsData->Module = Module;//该tls节所在的原模块
//重要。回填到原模块中,该tls节分得的索引。(写复制机制可确保各进程一份)
*(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex;
}
Entry = Entry->Flink;
}
}
return STATUS_SUCCESS;
}
如上,这个函数为进程建立起一个tls描述符数组。
typedef struct _TLS_DATA //tls节描述符
{
PVOID StartAddressOfRawData; //非0区在原模块中的地址
DWORD TlsDataSize;// 非0区的大小
DWORD TlsZeroSize;// 0区大小
PIMAGE_TLS_CALLBACK *TlsAddressOfCallBacks;//回调函数数组
PLDR_DATA_TABLE_ENTRY Module;//所在模块
} TLS_DATA, *PTLS_DATA;
非0区与0区是什么意思呢?tls节中各个变量可能有的没有初值,凡是没有初值的tls的变量都被安排到tls节的末尾,并且不予分配文件空间(这样,可以节省文件体积),只记录他们的总字节数即可。
__declspec(thread) int g_a = 1;//已初始化,被安排到tls节中的非0区
__declspec(thread) int g_b;//被安排到0区
__declspec(thread) int g_c = 0;//已初始化,被安排到tls节中的非0区
__declspec(thread) int g_d; //被安排到0区
所有未予初始化的tls变量都默认赋予初值0。
最后:每当一个线程创建时的初始化工作如下:
NTSTATUS
LdrpAttachThread (VOID)
{
。。。
Status = LdrpInitializeTlsForThread();//关键处。初始化每个线程的静态tls
调用各dll的DllMain,略
return Status;
}
如上,每当一个线程初始运行时,除了会调用进程中各个dll的DllMain函数外,还会初始化自己的静态tls,建立起本线程独立的一份静态tls副本。如下:
NTSTATUS LdrpInitializeTlsForThread(VOID)
{
PVOID* TlsPointers;
PTLS_DATA TlsInfo;
PVOID TlsData;
ULONG i;
PTEB Teb = NtCurrentTeb();
Teb->StaticUnicodeString.Length = 0;
Teb->StaticUnicodeString.MaximumLength = sizeof(Teb->StaticUnicodeBuffer);
Teb->StaticUnicodeString.Buffer = Teb->StaticUnicodeBuffer;
if (LdrpTlsCount > 0)//如果本进程中有包含tls节的静态模块
{
//将各模块内部的tls节提取出来,连成一片,形成一块‘tls片区’
TlsPointers = RtlAllocateHeap(RtlGetProcessHeap(),0,
LdrpTlsCount * sizeof(PVOID) + LdrpTlsSize);//头部指针数组+所有tls块的总大小
//指向头部后面的各tls节体部分
TlsData = (PVOID)((ULONG_PTR)TlsPointers + LdrpTlsCount * sizeof(PVOID));
Teb->ThreadLocalStoragePointer = TlsPointers;//指向本线程自己的那份tls的头部
TlsInfo = LdrpTlsArray;//指向本进程的tls描述符数组
for (i = 0; i < LdrpTlsCount; i++, TlsInfo++)
{
TlsPointers[i] = TlsData;//将数组指针指向对应的tls块
if (TlsInfo->TlsDataSize)
{
//提取对应模块内部的tls节体(非0区部分)到这儿来
memcpy(TlsData, TlsInfo->StartAddressOfRawData, TlsInfo->TlsDataSize);
TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsDataSize);
}
if (TlsInfo->TlsZeroSize)//0区部分
{
memset(TlsData, 0, TlsInfo->TlsZeroSize);//自动初始化为0
TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsZeroSize);//跨过0区部分
}
}
}
return STATUS_SUCCESS;
}
看到没,每个线程诞生之初,就将进程中各模块内部的tls节提取出来,复制到一个集中的地方存放,这样,
吗,每个线程都建立了一份自己连续的tls片区。以后,要访问tls变量时,访问的都是自己的那份tls片区,
当然,如何访问?这离不开编译器对静态tls机制提供的支持。
编译器在遇到__declspec(thread)关键字时,会认为那个变量是tls变量,将之编译链接到pe文件的.tls节中存放,另外每条访问tls变量的高级语句都被做了恰当的编译。每个tls变量都被编译为二级地址:
“Tls节号.节内偏移”,每个模块的tls节号(即索引)保存在那个模块的tls目录中的某个固定字段中(详见: *(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex 这条语句),这样,编译器从模块的这个位置取得该模块的tls节分得的节号,以此节号为索引,根据TEB中的保存的那块“tls片区”的头部数组,找到对应于本模块tls节副本的位置,然后加上该tls变量在节内的偏移,就正确找到对应的内存单元了。
进程挂靠与跨进程操作:
前面总在说:“将一个线程挂靠到其他进程的地址空间”,这是怎么回事?现在就来看一下。
当父进程要创建一个子进程时:会在父进程中调用CreateProcess。这个函数本身是运行在父进程的地址空间中的,但是由它创建了子进程,创建了子进程的地址空间,创建了子进程的PEB。当要初始化子进程的PEB结构时,由于PEB本身位于子进程的地址空间中,如果直接访问PEB那是不对的,那将会映射到不同的物理内存。所以必须挂靠到子进程的地址空间中,去读写PEB结构体中的值。下面的函数就是用来挂靠的
VOID KeAttachProcess(IN PKPROCESS Process) //将当前线程挂靠到指定进程的地址空间
{
KLOCK_QUEUE_HANDLE ApcLock;
PKTHREAD Thread = KeGetCurrentThread();
if (Thread->ApcState.Process == Process) return;//如果已经位于目标进程,返回
if ((Thread->ApcStateIndex != OriginalApcEnvironment) || (KeIsExecutingDpc()))
KeBugCheckEx(~);//蓝屏错误
else
{
KiAcquireApcLock(Thread, &ApcLock);
KiAcquireDispatcherLockAtDpcLevel();//挂靠过程操作过程中禁止线程切换
KiAttachProcess(Thread, Process, &ApcLock, &Thread->SavedApcState);//实质函数
}
}
VOID
KiAttachProcess(IN PKTHREAD Thread,//指定线程
IN PKPROCESS Process,//要挂靠到的目标进程
IN PKLOCK_QUEUE_HANDLE ApcLock,
IN PRKAPC_STATE SavedApcState)//保存原apc队列状态
{
Process->StackCount++;//目标线程的内核栈个数递增(也即增加线程个数)
KiMoveApcState(&Thread->ApcState, SavedApcState);//复制保存原apc队列状态
//每当一挂靠,必然要清空原apc队列
InitializeListHead(&Thread->ApcState.ApcListHead[KernelMode]);
InitializeListHead(&Thread->ApcState.ApcListHead[UserMode]);
Thread->ApcState.Process = Process;//关键。将表示当前进程的字段更为目标进程
Thread->ApcState.KernelApcInProgress = FALSE;
Thread->ApcState.KernelApcPending = FALSE;
Thread->ApcState.UserApcPending = FALSE;
if (SavedApcState == &Thread->SavedApcState)//一般满足
{
//修改指向,但不管怎么修改,ApcState字段总是表示当前apc状态
Thread->ApcStatePointer[OriginalApcEnvironment] = &Thread->SavedApcState;
Thread->ApcStatePointer[AttachedApcEnvironment] = &Thread->ApcState;
Thread->ApcStateIndex = AttachedApcEnvironment;
}
if (Process->State == ProcessInMemory)//if 没被置换出去
{
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseApcLockFromDpcLevel(ApcLock);
KiSwapProcess(Process, SavedApcState->Process);//实质函数
//调用这个函数的目的是检测可能的抢占切换条件是否已发生。(若已发生就赶紧切换)
KiExitDispatcher(ApcLock->OldIrql);//降到指定irql(同时检查是否发生了抢占式切换)
}
Else …
}
实质性的函数是KiSwapProcess,继续看
VOID KiSwapProcess(IN PKPROCESS NewProcess,IN PKPROCESS OldProcess)
{
PKIPCR Pcr = (PKIPCR)KeGetPcr();
//关键。修改cr3(存放进程页目录的物理地址)寄存器为目标进程的页表
__writecr3(NewProcess->DirectoryTableBase[0]);
Ke386SetGs(0);//将gs寄存器清0
Pcr->TSS->IoMapBase = NewProcess->IopmOffset;//修改当前线程的IO权限位图为目标进程的那份
}
看到没,进程挂靠的实质工作,就是将cr3寄存器改为目标寄存器的地址空间,这样,线程的所有有关内存的操作,操作的都是目标进程的地址空间。
明白了进程挂靠后,理解跨进程操作就很容易了。
一个进程可以调用OpenProcess打开另一个进程,取得目标进程的句柄后,就可调用VirtualAllocEx、WriteProcessMemory、ReadProcessMemory、CreateRemoteThread等函数操作那个进程的地址空间。这些跨进程操作的函数功能强大,而且带有破坏性,以至于往往被杀毒软件重点封杀,特别是CreateRemoteThread这个函数,冤啊。
所有的跨进程操作都必经一步:打开目标进程。(这是一道需要重点把手的关口)
HANDLE
OpenProcess(DWORD dwDesiredAccess,//申请的权限
BOOL bInheritHandle,//指本次打开得到的句柄是否可继承给子进程
DWORD dwProcessId)//目标进程的pid
{
NTSTATUS errCode;
HANDLE ProcessHandle;
OBJECT_ATTRIBUTES ObjectAttributes;
CLIENT_ID ClientId;
ClientId.UniqueProcess = UlongToHandle(dwProcessId);
ClientId.UniqueThread = 0;
InitializeObjectAttributes(&ObjectAttributes,NULL,
(bInheritHandle ? OBJ_INHERIT : 0),NULL,NULL);
//调用系统服务打开进程
errCode = NtOpenProcess(&ProcessHandle,dwDesiredAccess,&ObjectAttributes,&ClientId);
if (!NT_SUCCESS(errCode))
{
SetLastErrorByStatus(errCode);
return NULL;
}
return ProcessHandle;
}
NTSTATUS
NtOpenProcess(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId)//pid.tid
{
KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
ULONG Attributes = 0;
BOOLEAN HasObjectName = FALSE;
PETHREAD Thread = NULL;
PEPROCESS Process = NULL;
if (PreviousMode != KernelMode)
{
_SEH2_TRY
{
ProbeForWriteHandle(ProcessHandle);
if (ClientId)
{
ProbeForRead(ClientId, sizeof(CLIENT_ID), sizeof(ULONG));
SafeClientId = *ClientId;
ClientId = &SafeClientId;
}
ProbeForRead(ObjectAttributes,sizeof(OBJECT_ATTRIBUTES),sizeof(ULONG));
HasObjectName = (ObjectAttributes->ObjectName != NULL);
Attributes = ObjectAttributes->Attributes;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
else
{
HasObjectName = (ObjectAttributes->ObjectName != NULL);
Attributes = ObjectAttributes->Attributes;
}
if ((HasObjectName) && (ClientId))//不能同时给定进程名与id
return STATUS_INVALID_PARAMETER_MIX;
//传递当前令牌以及要求的权限到AccessState中
Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess,
&PsProcessType->TypeInfo.GenericMapping);
//检查当前令牌是否具有调试特权(这就是为什么经常在打开目标进程前要启用调试特权)
if (SeSinglePrivilegeCheck(SeDebugPrivilege, PreviousMode))
{
if (AccessState.RemainingDesiredAccess & MAXIMUM_ALLOWED)
AccessState.PreviouslyGrantedAccess |= PROCESS_ALL_ACCESS;
else
AccessState.PreviouslyGrantedAccess |=AccessState.RemainingDesiredAccess;
AccessState.RemainingDesiredAccess = 0;
}
if (HasObjectName) //以对象名的方式查找该进程对象
{
Status = ObOpenObjectByName(ObjectAttributes,PsProcessType,PreviousMode,
&AccessState,0,NULL,&hProcess);
SeDeleteAccessState(&AccessState);
}
else if (ClientId)
{
if (ClientId->UniqueThread)//根据tid查找线程、进程对象
Status = PsLookupProcessThreadByCid(ClientId, &Process, &Thread);
Else //根据pid从获活动进程链表中查找进程对象,最常见
Status = PsLookupProcessByProcessId(ClientId->UniqueProcess,&Process);
if (!NT_SUCCESS(Status))
{
SeDeleteAccessState(&AccessState);
return Status;
}
//在该进程对象上打开一个句柄
Status = ObOpenObjectByPointer(Process,Attributes,&AccessState,0,
PsProcessType,PreviousMode,&hProcess);
SeDeleteAccessState(&AccessState);
if (Thread)
ObDereferenceObject(Thread);
ObDereferenceObject(Process);
}
else
return STATUS_INVALID_PARAMETER_MIX;
if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
*ProcessHandle = hProcess;//返回打开得到的进程句柄
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
return Status;
}
如上,这个函数在检测权限满足后,就打开目标进程,返回一个句柄给调用者。
看下面的典型跨进程写数据函数:
NTSTATUS
NtWriteVirtualMemory(IN HANDLE ProcessHandle,//远程进程
IN PVOID BaseAddress,
IN PVOID Buffer,
IN SIZE_T NumberOfBytesToWrite,
OUT PSIZE_T NumberOfBytesWritten OPTIONAL)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PEPROCESS Process;
NTSTATUS Status = STATUS_SUCCESS;
SIZE_T BytesWritten = 0;
if (PreviousMode != KernelMode)
{
if ((((ULONG_PTR)BaseAddress + NumberOfBytesToWrite) < (ULONG_PTR)BaseAddress) ||
(((ULONG_PTR)Buffer + NumberOfBytesToWrite) < (ULONG_PTR)Buffer) ||
(((ULONG_PTR)BaseAddress + NumberOfBytesToWrite) > MmUserProbeAddress) ||
(((ULONG_PTR)Buffer + NumberOfBytesToWrite) > MmUserProbeAddress))
{
return STATUS_ACCESS_VIOLATION;
}
_SEH2_TRY
{
if (NumberOfBytesWritten) ProbeForWriteSize_t(NumberOfBytesWritten);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
if (NumberOfBytesToWrite)
{
Status = ObReferenceObjectByHandle(ProcessHandle,PROCESS_VM_WRITE,PsProcessType,
PreviousMode, (PVOID*)&Process,NULL);
if (NT_SUCCESS(Status))
{
Status = MmCopyVirtualMemory(PsGetCurrentProcess(),Buffer,Process,
BaseAddress,NumberOfBytesToWrite,
PreviousMode,&BytesWritten);
ObDereferenceObject(Process);
}
}
if (NumberOfBytesWritten)
{
_SEH2_TRY
{
*NumberOfBytesWritten = BytesWritten;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
}
_SEH2_END;
}
return Status;
}
NTSTATUS
MmCopyVirtualMemory(IN PEPROCESS SourceProcess,
IN PVOID SourceAddress,
IN PEPROCESS TargetProcess,
OUT PVOID TargetAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T ReturnSize)
{
NTSTATUS Status;
PEPROCESS Process = SourceProcess;
if (SourceProcess == PsGetCurrentProcess()) Process = TargetProcess;
if (BufferSize > 512)//需要使用MDL
{
Status = MiDoMappedCopy(SourceProcess,SourceAddress,TargetProcess,TargetAddress,
BufferSize,PreviousMode,ReturnSize);
}
else
{
Status = MiDoPoolCopy(SourceProcess,SourceAddress,TargetProcess,TargetAddress,
BufferSize,PreviousMode,ReturnSize);
}
return Status;
}
NTSTATUS
MiDoMappedCopy(IN PEPROCESS SourceProcess,
IN PVOID SourceAddress,
IN PEPROCESS TargetProcess,
OUT PVOID TargetAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T ReturnSize)
{
PFN_NUMBER MdlBuffer[(sizeof(MDL) / sizeof(PFN_NUMBER)) + MI_MAPPED_COPY_PAGES + 1];
PMDL Mdl = (PMDL)MdlBuffer;
SIZE_T TotalSize, CurrentSize, RemainingSize;
volatile BOOLEAN FailedInProbe = FALSE, FailedInMapping = FALSE, FailedInMoving;
volatile BOOLEAN PagesLocked;
PVOID CurrentAddress = SourceAddress, CurrentTargetAddress = TargetAddress;
volatile PVOID MdlAddress;
KAPC_STATE ApcState;
BOOLEAN HaveBadAddress;
ULONG_PTR BadAddress;
NTSTATUS Status = STATUS_SUCCESS;
TotalSize = 14 * PAGE_SIZE;//每次拷贝14个页面大小
if (BufferSize <= TotalSize) TotalSize = BufferSize;
CurrentSize = TotalSize;
RemainingSize = BufferSize;
while (RemainingSize > 0)
{
if (RemainingSize < CurrentSize) CurrentSize = RemainingSize;
KeStackAttachProcess(&SourceProcess->Pcb, &ApcState);//挂靠到源进程
MdlAddress = NULL;
PagesLocked = FALSE;
FailedInMoving = FALSE;
_SEH2_TRY
{
if ((CurrentAddress == SourceAddress) && (PreviousMode != KernelMode))
{
FailedInProbe = TRUE;
ProbeForRead(SourceAddress, BufferSize, sizeof(CHAR));
FailedInProbe = FALSE;
}
MmInitializeMdl(Mdl, CurrentAddress, CurrentSize);
MmProbeAndLockPages(Mdl, PreviousMode, IoReadAccess);
PagesLocked = TRUE;
MdlAddress = MmMapLockedPagesSpecifyCache(Mdl,KernelMode,MmCached, NULL,
FALSE,HighPagePriority);
KeUnstackDetachProcess(&ApcState);//撤销挂靠
KeStackAttachProcess(&TargetProcess->Pcb, &ApcState);//挂靠到目标进程
if ((CurrentAddress == SourceAddress) && (PreviousMode != KernelMode))
{
FailedInProbe = TRUE;
ProbeForWrite(TargetAddress, BufferSize, sizeof(CHAR));
FailedInProbe = FALSE;
}
FailedInMoving = TRUE;
RtlCopyMemory(CurrentTargetAddress, MdlAddress, CurrentSize);//拷贝
}
_SEH2_EXCEPT()。。。
if (Status != STATUS_SUCCESS) return Status;
KeUnstackDetachProcess(&ApcState);
MmUnmapLockedPages(MdlAddress, Mdl);
MmUnlockPages(Mdl);
RemainingSize -= CurrentSize;
CurrentAddress = (PVOID)((ULONG_PTR)CurrentAddress + CurrentSize);
CurrentTargetAddress = (PVOID)((ULONG_PTR)CurrentTargetAddress + CurrentSize);
}
*ReturnSize = BufferSize;
return STATUS_SUCCESS;
}
看到没,要挂靠到目标进程中去复制数据。如果源进程不是当前进程,还要先挂靠到源进程中。
线程的挂起与恢复:
SuspendThread->NtSuspendThread->PsSuspenThread-> KeSuspendThread,直接看KeSuspendThread函数
ULONG KeSuspendThread(PKTHREAD Thread)
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
PreviousCount = Thread->SuspendCount;
if (Thread->ApcQueueable)
{
Thread->SuspendCount++;//递增挂起计数
if (!(PreviousCount) && !(Thread->FreezeCount))
{
if (!Thread->SuspendApc.Inserted)//if尚未插入那个‘挂起APC’
{
Thread->SuspendApc.Inserted = TRUE;
KiInsertQueueApc(&Thread->SuspendApc, IO_NO_INCREMENT);//插入‘挂起APC’
}
else
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState--;
KiReleaseDispatcherLockFromDpcLevel();
}
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;
}
这个专有的‘挂起APC’是一个特殊的APC,我们看他的工作:
VOID
KiSuspendThread(IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2)
{
//等待挂起计数减到0
KeWaitForSingleObject(&KeGetCurrentThread()->SuspendSemaphore,Suspended,KernelMode,
FALSE,NULL);
}
如上,向指定线程插入一个‘挂起APC’后,那个线程下次一得到调度,就会先执行内核中的所有APC,当执行到这个APC的时候,就会一直等到挂起计数降到0。换言之,线程刚一得到调度运行的就会,就又重新进入等待了。因此,‘挂起态’也是一种特殊的‘等待态’。什么时候挂起计数会减到0呢?只有在别的线程恢复这个线程的挂起计数时。
ULONG KeResumeThread(IN PKTHREAD Thread)
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
PreviousCount = Thread->SuspendCount;
if (PreviousCount)
{
Thread->SuspendCount--;//递减挂起计数
if ((Thread->SuspendCount==0) && (!Thread->FreezeCount))
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState++;
//当挂起计数减到0时,唤醒目标线程
KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT);
KiReleaseDispatcherLockFromDpcLevel();
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;
}
就这样简单。
当一个线程处于等待状态时,可以指示本次睡眠是否可被强制唤醒,不必等到条件满足
如:
DWORD WaitForSingleObjectEx(
HANDLE ,
DWORD ,
BOOL //指示本次等待过程中是否可以被其他线程(或其他线程发来的APC)强制唤醒。
);
BOOLEAN
KeAlertThread(IN PKTHREAD Thread,
IN KPROCESSOR_MODE AlertMode)
{
BOOLEAN PreviousState;
KLOCK_QUEUE_HANDLE ApcLock;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
KiAcquireDispatcherLockAtDpcLevel();
PreviousState = Thread->Alerted[AlertMode];//检测是否收到了来自那个模式的强制唤醒要求
if (PreviousState==FALSE)
{
if ((Thread->State == Waiting) && //线程处于等待状态
(Thread->Alertable) && //线程可被强制唤醒
(AlertMode <= Thread->WaitMode)) //模式条件符合
{
//强制唤醒那个线程
KiUnwaitThread(Thread, STATUS_ALERTED, THREAD_ALERT_INCREMENT);
}
Else //仅仅标记已收到过来自那个模式的强制唤醒请求
Thread->Alerted[AlertMode] = TRUE;
}
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousState;
}
注意AlertMode <= Thread->WaitMode条件指:用户模式的强制唤醒请求不能唤醒内核模式的等待。
DLL注入:
前面讲过,每个进程在启动的时候会加载主exe文件依赖的所有子孙dll。实际上,一般的Win32 GUI进程
都会加载user32.dll模块。这个模块一加载,就会自动搜索注册表键 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows 下的值:AppInit_DLLs,该值是一个dll列表,user32.dll会读取这个值,调用LoadLibrary加载里面的每个dll,因此我们可以把我们的dll名称添加到这个列表中,达到dll注入的目的。我们看:
INT
DllMain( //User32.dll的DllMain
IN PVOID hInstanceDll,
IN ULONG dwReason,
IN PVOID reserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
Init();//会调用这个函数
…
…
}
}
BOOL Init(VOID)
{
…
LoadAppInitDlls();//会调用这个函数加载那些dll
…
}
VOID LoadAppInitDlls()
{
szAppInit[0] = UNICODE_NULL;
if (GetDllList())//读取这册表键的值,将要加载的dll列表保存在全局变量szAppInit中
{
WCHAR buffer[KEY_LENGTH];
LPWSTR ptr;
size_t i;
RtlCopyMemory(buffer, szAppInit, KEY_LENGTH);
for (i = 0; i < KEY_LENGTH; ++ i)
{
if(buffer[i] == L' ' || buffer[i] == L',')//dll名称之间必须用空格或逗号隔开
buffer[i] = 0;
}
for (i = 0; i < KEY_LENGTH; )
{
if(buffer[i] == 0)
++ i;
else
{
ptr = buffer + i;
i += wcslen(ptr);
LoadLibraryW(ptr);//加载每个dll
}
}
}
}
BOOL GetDllList()
{
NTSTATUS Status;
OBJECT_ATTRIBUTES Attributes;
BOOL bRet = FALSE;
BOOL bLoad;
HANDLE hKey = NULL;
DWORD dwSize;
PKEY_VALUE_PARTIAL_INFORMATION kvpInfo = NULL;
UNICODE_STRING szKeyName = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows");
UNICODE_STRING szLoadName = RTL_CONSTANT_STRING(L"LoadAppInit_DLLs");
UNICODE_STRING szDllsName = RTL_CONSTANT_STRING(L"AppInit_DLLs");
InitializeObjectAttributes(&Attributes, &szKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL);
Status = NtOpenKey(&hKey, KEY_READ, &Attributes);
if (NT_SUCCESS(Status))
{
dwSize = sizeof(KEY_VALUE_PARTIAL_INFORMATION) + sizeof(DWORD);
kvpInfo = HeapAlloc(GetProcessHeap(), 0, dwSize);
if (!kvpInfo)
goto end;
//先要在那个键中建立一个DWORD值:LoadAppInit_DLLs,并将数值设为1
Status = NtQueryValueKey(hKey,&szLoadName,KeyValuePartialInformation,
kvpInfo,dwSize,&dwSize);
RtlMoveMemory(&bLoad,kvpInfo->Data,kvpInfo->DataLength);
HeapFree(GetProcessHeap(), 0, kvpInfo);
kvpInfo = NULL;
if (bLoad)//if 需要加载初始列表的那些dll
{
Status = NtQueryValueKey(hKey,&szDllsName,KeyValuePartialInformation,
NULL,0,&dwSize);
kvpInfo = HeapAlloc(GetProcessHeap(), 0, dwSize);
Status = NtQueryValueKey(hKey, &szDllsName,KeyValuePartialInformation,
kvpInfo,dwSize,&dwSize);
if (NT_SUCCESS(Status))
{
LPWSTR lpBuffer = (LPWSTR)kvpInfo->Data;
if (*lpBuffer != UNICODE_NULL)
{
INT bytesToCopy, nullPos;
bytesToCopy = min(kvpInfo->DataLength, KEY_LENGTH * sizeof(WCHAR));
if (bytesToCopy != 0)
{
//dll列表拷到全局变量
RtlMoveMemory(szAppInit,kvpInfo->Data,bytesToCopy);
nullPos = (bytesToCopy / sizeof(WCHAR)) - 1;
szAppInit[nullPos] = UNICODE_NULL;
bRet = TRUE;
}
}
}
}
}
end:
if (hKey)
NtClose(hKey);
if (kvpInfo)
HeapFree(GetProcessHeap(), 0, kvpInfo);
return bRet;
}
因此,只需在那个键下面添加一个DWORD值:LoadAppInit_DLLs,设为1,然后在AppInit_DLLs值中添加我们的dll即可达到将我们的dll加载到任意GUI进程的地址空间中。