Building a Process Tree

Author

Pavel Yosifovich
25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.

In Windows, every process is associated with a parent process, usually the one created it. This means that a process tree can be visualized, but in fact it’s a set of trees, because if the parent process no longer exists, a child process becomes a root of its own potential tree. This is not an issue, because processes in Windows have no particular dependency on their parent. For example, if process A creates process B, they live independently. If process A terminates, process B is unaffected.

Gain Insider Knowledge

Subscribe to updates from the TrainSec trainers

 Tools such as Process Explorer can show a process tree, indicating parent/child relationships between processes:

Building a Process Tree in windows 1

The process ID of the parent process is stored in the child’s process management object (the EPROCESS structure in kernel space).

The first thing we’ll do is define a structure to hold on to some basic process information:

struct ProcessInfo {

wstring Name;

DWORD Id;

DWORD ParentId;

ProcessInfo* Parent{ nullptr };

vector<unique_ptr<ProcessInfo>> Children;

};

Each process holds its executable (or other) name, id, parent id, a pointer to the process object of its parent (convenient if navigation is required), and a vector of child processes.

Processes are added into the internal kernel process list in order of creation. This is the order of processes we get when we enumerate processes with any enumeration API. This is convenient, because it means that parent processes appear earlier than their children in an enumeration.

Building the tree should be easy enough – while enumerating processes, we need to check if the parent exists, and if it does, attach the process to its parent. We can keep track of processes with a map for a quick search. Let’s enumerate processes with the ToolHelp API:

auto hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

PROCESSENTRY32 pe;

pe.dwSize = sizeof(pe);

unordered_map<DWORD, ProcessInfo*> processMap;

vector<unique_ptr<ProcessInfo>> roots;

Process32First(hSnapshot, &pe);

Now we can loop around, looking for a parent. If it exists, add to its children. Otherwise, it’s a root process (its parent does not exist):

do {

auto pi = make_unique<ProcessInfo>();

pi->Id = pe.th32ProcessID;

pi->ParentId = pe.th32ParentProcessID;

pi->Name = pe.szExeFile;

auto p = pi.get();

if (auto it = processMap.find(pi->ParentId); 

it != processMap.end()) {

pi->Parent = it->second;

it->second->Children.push_back(move(pi));

}

else {

roots.push_back(move(pi));

}

processMap.insert({ pe.th32ProcessID, p });

} while (Process32Next(hSnapshot, &pe));

This looks simple enough. Now we can output the process tree with a simple recursion:

void DisplayTree(vector<unique_ptr<ProcessInfo>> const& p, 

  int indent = 0) {

for (auto& pi : p) {

printf("%s", string(indent, ' ').c_str());

printf("%ws (%u)\n", pi->Name.c_str(), pi->Id);

DisplayTree(pi->Children, indent + 1);

}

}

If you run this, it would seem to work fine. However, there are three points that may not be immediately obvious. The simplest is the fact that processes that have no parent (and never did) show a parent ID of zero. This is the case for the Idle process (PID 0) and the System process (PID 4). So, for a parent of zero, the process should always be a root. This is easily fixed in the conditional:

if (auto it = pi->ParentId == 0 ? 

processMap.end() : processMap.find(pi->ParentId); 

it != processMap.end()) {

pi->Parent = it->second;

it->second->Children.push_back(move(pi));

}

The second point is more subtle. Process IDs in Windows may be reused. When a process is destroyed, its PID may be reused. Moreover, process IDs and thread IDs are generated from the same pool of numbers, which means that a dead thread ID can be reused for a new process, and vice versa.

This means that the parent ID that is stored in the child may refer to a process that is already dead, but that parent ID may have been reused by a new process. How do we resolve that? We need to check if the potential parent has been created before the child. If not, it’s not the real parent. We can do that by opening a handle to the parent, query its creation time, and compare it to the child (current) process creation time:

bool root = true;

auto it = pi->ParentId == 0 ? 

processMap.end() : processMap.find(pi->ParentId);

if(it != processMap.end()) {

//

// potential parent

//

root = GetCreateTime(pi->ParentId) > GetCreateTime(pi->Id);

}

if(root) {

roots.push_back(move(pi));

}

else {

pi->Parent = it->second;

it->second->Children.push_back(move(pi));

}

GetCreateTime returns the creation time as a 64-bit integer:

int64_t GetCreateTime(DWORD pid) {

auto hProcess = OpenProcess(

PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);

if (!hProcess)

return 0;

FILETIME create{}, dummy;

GetProcessTimes(hProcess, &create, &dummy, &dummy, &dummy);

CloseHandle(hProcess);

return *(int64_t*)&create;

}

If a process handle cannot be opened successfully, then we can’t make a good decision about the parent. The solution? Use the native API NtQuerySystemInformation to enumerate processes, where the creation time is provided for free for all processes. I’ll leave that as an exercise for the interested reader.

Lastly, a point worth mentioning about the meaning of a parent process.

What is a parent process, anyway? Intuition suggests is that a parent process is the creator of the child process. This is mostly true, but it doesn’t have to be. It’s possible to call CreateProcess and provide (through an extended process attribute) a different parent than the default being the caller process. For example, you can create a Notepad process that appears to be a child of Mspaint, even though the creator is your process (whatever that may be). Changing the parent makes the new process inherit certain attributes from that other process rather than the creator (such as current directory and environment variables). This does not change anything in the way we build the process tree, but bear in mind the meaning of “parent process”.

blue depth

About the author

Pavel Yosifovich
25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.

Black Friday & Cyber Monday Sale Started!

For a limited time, enjoy 25% off ALL available courses for the next month. Whether you’re looking to sharpen your skills or explore new areas of cybersecurity, now’s the perfect time to invest in your growth.

Use code BFRIDAY24 at checkout to claim your discount.

*Excluding bundles and monthly plans.