PspCidTable Enumeration
Ring 0Enumerate all processes and threads via the kernel's CID (Client ID) handle table, bypassing usermode hiding techniques.
Hidden Process Detection
PspCidTable enumeration can detect processes hidden via DKOM (Direct Kernel Object Manipulation) because it uses a different data structure than the ActiveProcessLinks list.
Overview
The PspCidTable is a kernel handle table that maintains references to all processes and threads by their Client ID (PID/TID). Unlike the ActiveProcessLinks list, which is commonly manipulated by rootkits, the CID table is harder to tamper with and provides a more complete view.
CidEntry Structure
crates/callback/src/pspcidtable.rs
pub struct CidEntry {
pub id: u32, // PID (for processes) or TID (for threads)
pub object_address: u64, // EPROCESS or ETHREAD kernel address
pub object_type: CidObjectType, // Process or Thread
pub parent_pid: u32, // Parent PID (processes) or owning PID (threads)
pub process_name: [u8; 16], // ImageFileName from EPROCESS (15 chars + null)
}
pub enum CidObjectType {
Process,
Thread,
}Implementation
The driver uses signature scanning to dynamically locate PspCidTable, avoiding hardcoded offsets that break across Windows versions:
Algorithm
// 1. Find PspCidTable via signature scanning
// Search ntoskrnl.exe for pattern that references PspCidTable
PVOID PspCidTable = FindPattern(ntoskrnl, pattern, mask);
// 2. Walk the handle table
// PspCidTable is an HANDLE_TABLE structure
for (each handle in table) {
// 3. Decode handle entry to get EPROCESS/ETHREAD pointer
PVOID Object = DecodeHandleEntry(entry);
// 4. Determine object type
POBJECT_TYPE ObjectType = ObGetObjectType(Object);
if (ObjectType == *PsProcessType) {
// 5. Read EPROCESS fields
entry.id = PsGetProcessId(Object);
entry.parent_pid = PsGetProcessInheritedFromUniqueProcessId(Object);
memcpy(entry.process_name, Object + ImageFileNameOffset, 16);
entry.object_address = (ULONG64)Object;
entry.object_type = Process;
}
else if (ObjectType == *PsThreadType) {
// 6. Read ETHREAD fields
entry.id = PsGetThreadId(Object);
entry.parent_pid = PsGetProcessId(PsGetThreadProcess(Object));
entry.object_address = (ULONG64)Object;
entry.object_type = Thread;
}
}API
Usage
use callback::enumerate_pspcidtable;
// Enumerate all CID entries
let entries: Vec<CidEntry> = enumerate_pspcidtable()?;
// Filter processes only
let processes: Vec<_> = entries.iter()
.filter(|e| e.object_type == CidObjectType::Process)
.collect();
// Find hidden processes (compare to ToolHelp32 enumeration)
let toolhelp_pids: HashSet<u32> = get_toolhelp_processes();
let hidden: Vec<_> = processes.iter()
.filter(|p| !toolhelp_pids.contains(&p.id))
.collect();IOCTL
| IOCTL | Code | Input | Output |
|---|---|---|---|
| ENUM_PSPCIDTABLE | 0x0022203C | None | Array of CidEntry |
UI Features
Access via Kernel Utilities tab → PspCidTable sub-tab:
- • Type filter — All, Processes, or Threads buttons
- • Columns — Type, ID, Process Name, Object Address, Parent/Owner PID
- • Sorting — Click column headers to sort
- • Search filter — Filter by name, ID, address, or parent PID
- • CSV export — Export to pspcidtable.csv
- • Context menu — Copy ID, Copy Process Name, Copy Object Address
- • Color coding — Green "Process" label, blue "Thread" label
Detecting Hidden Processes
To detect DKOM-hidden processes, compare results with standard enumeration:
- Enumerate processes via ToolHelp32 (CreateToolhelp32Snapshot)
- Enumerate processes via PspCidTable
- Processes in PspCidTable but NOT in ToolHelp32 are likely hidden via DKOM
Note: The DioProcess UI shows both enumerations for easy comparison.
Use Cases
- • Detect rootkits hiding processes via ActiveProcessLinks unlinking
- • View raw EPROCESS/ETHREAD kernel addresses
- • Forensic analysis of running system
- • Security research on process hiding techniques
- • Enumerate system threads (PID 4)
Limitations
- • Cannot detect processes hidden via PspCidTable manipulation (very rare)
- • Signature scanning may need updates for new Windows versions
- • Read-only: cannot unhide or manipulate entries