文章作者:sunwear [E.S.T] 

不同的进程真的不能够拥有相同的PID么?我相信大部分人都会说,这是不可能的,因为PID是在操作系统中表示进程的唯一性标示,因此不可能出现不同的进程拥有相同的PID,否则在系统调度的时候就会出现混乱。可是真的是这样么?有这样一个程序xxxx,当我们用xxx工具来观察系统中的pid。我们发现,在这个程序运行时,系统中竟然出现了不同的进程拥有了相同的pid。这是为什么?我们的理解与我们看到的现象竟然出现了矛盾。
首先让我们来了解一下EPROCESS结构。每个Windows 2000进程都由一个执行程序进程(EPROCESS)块表示,也就是说在内核中,进程是靠EPROCESS来识别的.下面是EPROCESS的结构定义

typedef struct _EPROCESS { 

KPROCESS Pcb; 

NTSTATUS ExitStatus; 

KEVENT LockEvent; 

ULONG LockCount; 

LARGE_INTEGER CreateTime; 

LARGE_INTEGER ExitTime; 

PKTHREAD LockOwner; 


HANDLE UniqueProcessId; 


LIST_ENTRY ActiveProcessLinks; 


SIZE_T QuotaPeakPoolUsage[2]; 

SIZE_T QuotaPoolUsage[2]; 


SIZE_T PagefileUsage; 

SIZE_T CommitCharge; 

SIZE_T PeakPagefileUsage; 


SIZE_T PeakVirtualSize; 

SIZE_T VirtualSize; 


MMSUPPORT Vm; 

LIST_ENTRY SessionProcessLinks; 


PVOID DebugPort; 

PVOID ExceptionPort; 

PHANDLE_TABLE ObjectTable; 


PACCESS_TOKEN Token; 


FAST_MUTEX WorkingSetLock; 

PFN_NUMBER WorkingSetPage; 

BOOLEAN ProcessOutswapEnabled; 

BOOLEAN ProcessOutswapped; 

UCHAR AddressSpaceInitialized; 

BOOLEAN AddressSpaceDeleted; 

FAST_MUTEX AddressCreationLock; 

KSPIN_LOCK HyperSpaceLock; 

struct _ETHREAD *ForkInProgress; 

USHORT VmOperation; 

UCHAR ForkWasSuccessful; 

UCHAR MmAgressiveWsTrimMask; 

PKEVENT VmOperationEvent; 

PVOID PaeTop; 

ULONG LastFaultCount; 

ULONG ModifiedPageCount; 

PVOID VadRoot; 

PVOID VadHint; 

PVOID CloneRoot; 

PFN_NUMBER NumberOfPrivatePages; 

PFN_NUMBER NumberOfLockedPages; 

USHORT NextPageColor; 

BOOLEAN ExitProcessCalled; 


BOOLEAN CreateProcessReported; 

HANDLE SectionHandle; 


PPEB Peb; 

PVOID SectionBaseAddress; 


PEPROCESS_QUOTA_BLOCK QuotaBlock; 

NTSTATUS LastThreadExitStatus; 

PPAGEFAULT_HISTORY WorkingSetWatch; 

HANDLE Win32WindowStation; 

HANDLE InheritedFromUniqueProcessId; 

ACCESS_MASK GrantedAccess; 

ULONG DefaultHardErrorProcessing; 

PVOID LdtInformation; 

PVOID VadFreeHint; 

PVOID VdmObjects; 

PVOID DeviceMap; 


ULONG SessionId; 


LIST_ENTRY PhysicalVadList; 

union { 

HARDWARE_PTE PageDirectoryPte; 

ULONGLONG Filler; 

}; 

ULONG PaePageDirectoryPage; 

UCHAR ImageFileName[ 16 ]; 

ULONG VmTrimFaultValue; 

BOOLEAN SetTimerResolution; 

UCHAR PriorityClass; 

union { 

struct { 

UCHAR SubSystemMinorVersion; 

UCHAR SubSystemMajorVersion; 

}; 

USHORT SubSystemVersion; 

}; 

PVOID Win32Process; 

struct _EJOB *Job; 

ULONG JobStatus; 

LIST_ENTRY JobLinks; 

PVOID LockedPagesList; 

PVOID SecurityPort ; 

PWOW64_PROCESS Wow64Process; 


LARGE_INTEGER ReadOperationCount; 

LARGE_INTEGER WriteOperationCount; 

LARGE_INTEGER OtherOperationCount; 

LARGE_INTEGER ReadTransferCount; 

LARGE_INTEGER WriteTransferCount; 

LARGE_INTEGER OtherTransferCount; 


SIZE_T CommitChargeLimit; 

SIZE_T CommitChargePeak; 


LIST_ENTRY ThreadListHead; 


PRTL_BITMAP VadPhysicalPagesBitMap; 

ULONG_PTR VadPhysicalPages; 

KSPIN_LOCK AweLock; 

} EPROCESS;


每个Windows进程都会由系统空间中的一个EPROCESS块来标示。其中有一个UniqueProcessId的属性存储了在系统空间中唯一的进程ID,也就是我们常说的PID。
下面我们在来看看进程的创建,正如文章开始所说的例子,一个父进程不断创建子进程,子进程结束但是HANDLE并未关闭,WIN32子系统进程(csrss)不断创建新进程。
进程创建过程可以通过跟踪分析CreateProcess来了解。我简单的说一下。
首先CreateProcess找到执行程序对应的WIN32映射执行程序后,创建执行程序对象。
首先就是设置EPROCESS块其中包括把进程和会话ID存储到对应的字段中,设置进程退出状态,并创建访问令牌。
然后创建初始地址空间和内核进程块与地址空间的设置以及PEB的设置。
再创建线程和堆栈环境。
下面的一部就是向WIN32子系统传递信息,包括新建的进程线程句柄。创建标志中的项以及ID和确认其属于WIN32应用程序的标志。后面就是初始化线程并完成整个进程的初始化。

下面我们来看看程序退出的方面。
ExitProcess函数在结束进程的时候 并没有释放EPROCESS在内存中的数据,导致进程结束,但EPROCESS还存在.

通过上面的分析,可以知道子进程的EPROCESS并不会同进程一起消失,因为EPROCESS必须等到所有的Handle关闭后才会关闭。也就是说进程的EPROCESS Handle count为0的时候,EPROCESS才会被关闭。说白了是,一旦有进程拥有EPROCESS的句柄,那么即使进程退出,那么EPROCESS也暂时不会被清除出内存,直到所有的Handle都关闭后,才会被清除。上面提到的进程关闭而HANDLE未关闭的进程就称做僵尸进程(tombie process)。这样的情况下win32子系统无法意识到有些进程已成为僵尸。
一般查看进程信息的程序均调用 NtQuerySystemInformation函数来显示出进程的信息。

NTSTATUS 

NtQuerySystemInformation ( 

IN SYSTEM_INFORMATION_CLASS SystemInformationClass, 

OUT PVOID SystemInformation, 

IN ULONG SystemInformationLength, 

OUT PULONG ReturnLength OPTIONAL 

)


SystemInformationClass 信息的类别,SystemInformation 一个指向函数输出缓冲区的指针,SystemInformationLength 是这个缓冲区的长度,ReturnLength是写入字节的数目。
NtQuerySystemInformation函数 是直接枚举EPROCESS,所以,如果直接通过NtQuerySystemInformation枚举进程的话,会出现有些相同的PID。原因就是CSRSS根本就不考虑已死的进程。 通过上面的信息也可以了解,只有在进程结束后,EPROCESS从内存中释放前,才会出现这种情况。