In the process of kernel development in the security field, obtaining process information is a common requirement.
Due to the potential adversarial actions, the author chose the technical solution of traversing the PspCidTable in the project - the benefits of doing so are twofold:
- It can ensure that the list obtained is not filtered out by various Hooks or Callbacks as much as possible.
- If you want to do adversarial actions, malicious programs also need to traverse this table, and everyone’s adversarial cost is at least on one level.
(By the way, there are a lot of materials on the Internet about brute force searching through PspCidTable, it is recommended that you take a look)
However, a series of problems have also followed:
- In order to ensure compatibility, the author directly traversed a large block of memory address space. This resulted in some pointers obtained that do not actually point to any EPROCESS.
- Even if the address pointed to is legal, it is not a valid EPROCESS structure itself.
Solution
EPROCESS address is illegal causing blue screen
By observing different operating systems, it can be seen that the high address of the process’s EPROCESS structure is basically the same as the high address of Idle or System’s EPROCESS, so we can quickly exclude illegal EPROCESS addresses by verifying whether the high address of potential EPROCESS is consistent with Idle or System.
And by observing the starting address of the system’s non-paged memory, it is known that 0xFFFFFA80`03602000 starts as system non-paged memory. According to the characteristics of the operating system, this address can be verified for validity with MmIsAddressValid, and because it is non-paged memory, we do not need to consider the situation where these memories are swapped out.

So, to sum up, the way to verify whether EPROCESS is a legal address is:
- Under 64-bit, its high 6 bits, under 32-bit, its high 1 bit address is the same as the high address of Idle or System’s EPROCESS
- MmIsAddressValid verifies as true
The address is legal, but it is not an EPROCESS structure itself
At first, the author actually used whether Pid or PPid can be divided by 4 to verify whether it is a legal EPROCESS, but in fact, this constraint ability is still too weak, and it is still found in the traversal process that there is a very high probability not a legal process. Since EPROCESS itself is a structure that is not officially public, its EPROCESS structure will have subtle differences in various versions, which will cause some key field offsets to be unstable.
Therefore, we need to find another method with strong compatibility and stability.
By observing the EPROCESS structure of multiple versions of Windows systems on x86 and x64:
- kd> dt _EPROCESS
- nt!_EPROCESS
- +0x000 Pcb : _KPROCESS
- …………
- +0x128 SectionObject : Ptr32 Void
- +0x12c SectionBaseAddress : Ptr32 Void
- …………
It can be seen that it defines a SectionObject and SectionBaseAddress, and these two members are next to each other on all systems, in other words, Offset(SectionBaseAddress) - sizeof(void*) must be the position of SectionObject. And SectionObject, as the name suggests - it is an object of type Section:

If we can check whether the position of SectionObject is an object of type Section, can’t we check the legality of EPROCESS to a certain extent?
However, as mentioned earlier, the member structure of EPROCESS varies on different systems, how can we stably obtain the position of SectionObject? This requires the introduction of a function provided by Windows:

Emmm, very good, although the function is not documented, but the system exports it, so we can let the system tell us what the value of SectionBaseAddress is. Then we take this value and search in the potential EPROCESS structure, and check whether the “object” pointed to by reducing a pointer length is of Section type, we can achieve the purpose of verifying the legality of EPROCESS.
So, to sum up again:
- Call PsGetProcessSectionBaseAddress to get the value of SectionBaseAddress
- Search for the position containing this value in the target memory
- Check whether the “object” at the position of minus 1 pointer length is of Section type
Final solution
So combine:
- Locate the table containing the potential EPROCESS address through PspCidTable.
- Under 64-bit, its high 6 bits, under 32-bit, its high 1 bit address is the same as the high address of Idle or System’s EPROCESS.
- MmIsAddressValid verifies as true.
- Check whether the potential EPROCESS is an object of type Process.
- Call PsGetProcessSectionBaseAddress to get the value of SectionBaseAddress.
- Search for the position containing this value in the target memory.
- Check whether the “object” at the position of minus 1 pointer length is of Section type.
The above is the solution for brute force searching for valid items in the EPROCESS table + not blue screen.
Coding Time
- // TableEntry is the potential EPROCESS storage table obtained through PspCidTable
- for (i = 0; i < 4096; TableEntry++, i++)
- {
- if (TableEntry && MmIsAddressValid(TableEntry))
- {
- ULONG_PTR Align = 0;
- PEPROCESS EProcess = NULL;
- NTSTATUS status = STATUS_UNSUCCESSFUL;
- UNICODE_STRING ProcessType = {0}, Type = {0};
- // find potential EPROCESS from TableEntry
- EProcess = _ps_PsGetEProcessObjectFromTableEntry(TableEntry);
- // if it is a valid address in non-paged memory
- if(EProcess == 0 || !MmIsAddressValid(EProcess))
- {
- continue;
- }
- #ifdef _WIN64
- Align = 0xFFFFF00000000000;
- #else
- Align = 0xF0000000;
- #endif
- if ( \
- (ULONG_PTR)((ULONG_PTR)EProcessIdle & Align) == (ULONG_PTR)((ULONG_PTR)EProcess & Align) || \
- (ULONG_PTR)((ULONG_PTR)EProcessSystem & Align) == (ULONG_PTR)((ULONG_PTR)EProcess & Align)
- )
- {
- RtlInitUnicodeString(&ProcessType, L"Process");
- status = _ob_ObjGetTypeUnicodeStringByObject(EProcess, &Type);
- // Check whether this "potential" EPROCESS is a type of process ?
- if (NT_SUCCESS(status) && 0 == RtlCompareUnicodeString(&ProcessType, &Type, TRUE))
- {
- UNICODE_STRING SectionType = {0};
- PUCHAR Address = EProcess;
- ULONG_PTR SectionBaseAddress = NULL, SectionObject = NULL;
- RtlFreeUnicodeString(&Type);
- RtlInitUnicodeString(&SectionType, L"Section");
- ARK_DECLARE_ROUTINE(PsGetProcessSectionBaseAddress);
- SectionBaseAddress = PsGetProcessSectionBaseAddress(EProcess);
- // first search `SectionBaseAddress` offset in `EPROCESS`
- // Search address containing SectionBaseAddress
- while(Address <= Address + 0x500)
- {
- // find a possiable `SectionBaseAddress` position
- if (*(PULONG_PTR)(Address) == SectionBaseAddress)
- {
- // then get `SectionObject` in `EPROCESS`
- // this member usually just before `SectionBaseAddress`
- // sub by sizeof(void *)
- SectionObject = *(PULONG_PTR)(Address - 1 * sizeof(PULONG_PTR));
- status = _ob_ObjGetTypeUnicodeStringByObject(SectionObject, &Type);
- // if it is a valid EPROCESS
- // if it is a section type object
- if (NT_SUCCESS(status) && 0 == RtlCompareUnicodeString(&SectionType, &Type, TRUE))
- {
- RtlFreeUnicodeString(&Type);
- // check passed, insert save it into list
- _ps_PspInsertContextNode(ProcessContext, EProcess);
- }
- break;
- }
- ++Address;
- }
- }
- }
- }
- }
The functions of _ps_PsGetEProcessObjectFromTableEntry and _ob_ObjGetTypeUnicodeStringByObject in the above code are to safely obtain the desired content from the input memory, and you can implement it according to your own needs.
Effect
By the way, I almost forgot to say:
- Idle进程是不在PspCidTable中的,需要用PCR结构获取。
- The Idle process is not in the PspCidTable, it needs to be obtained with the PCR structure.
- The System process is in the PspCidTable, but its SectionObject structure is empty. This process can be confirmed by verifying whether the pid is 4 and whether the result of PsGetProcessImageFileName is System.