diff --git a/binding.gyp b/binding.gyp index 855bd4b86f0a3c18c7594212c0e42b6e35bc4001..0bb2af7923b6e6f1f0da40cae8067304cd1fea14 100644 --- a/binding.gyp +++ b/binding.gyp @@ -3,7 +3,6 @@ { "target_name": "windows_process_tree", "dependencies": [ - " ({ + const buildNode = ({ info: { pid, name, memory, commandLine, creationTimeMs }, children }, depth) => ({ pid, name, memory, commandLine, + creationTimeMs, children: depth > 0 ? children.map(c => buildNode(c, depth - 1)) : [], }); return buildNode(root, maxDepth); diff --git a/lib/index.ts b/lib/index.ts index f9aa005d9ced9e42885b8a976de5eb5bd61899ee..1b509af0b9065918bcb5cb75f2d7f23821d4a56a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -6,12 +6,15 @@ import { promisify } from 'util'; const native = process.platform === 'win32' ? require('../build/Release/windows_process_tree.node') : undefined; +/** The flag bits this compiled addon reports; undefined off win32. */ +export const supportedProcessDataFlags: number | undefined = native?.supportedProcessDataFlags; import { IProcessInfo, IProcessTreeNode, IProcessCpuInfo } from '@vscode/windows-process-tree'; export enum ProcessDataFlag { None = 0, Memory = 1, - CommandLine = 2 + CommandLine = 2, + CreationTime = 4 } type RequestCallback = (processList: IProcessInfo[]) => void; @@ -81,11 +84,12 @@ export function buildProcessTree(rootPid: number, processList: Iterable ({ + const buildNode = ({ info: { pid, name, memory, commandLine, creationTimeMs }, children }: IProcessInfoNode, depth: number): IProcessTreeNode => ({ pid, name, memory, commandLine, + creationTimeMs, children: depth > 0 ? children.map(c => buildNode(c, depth - 1)) : [], }); diff --git a/src/addon.cc b/src/addon.cc index 9214aff281251e797a70ecb9f6e0b52932a0503f..722edd42ddb4740296bfc47582a181bd6d00c464 100644 --- a/src/addon.cc +++ b/src/addon.cc @@ -53,6 +53,10 @@ void GetProcessCpuUsage(const Napi::CallbackInfo& args) { Napi::Object Init(Napi::Env env, Napi::Object exports) { exports.Set("getProcessList", Napi::Function::New(env, GetProcessList)); exports.Set("getProcessCpuUsage", Napi::Function::New(env, GetProcessCpuUsage)); + // Lets a caller prove THIS BINARY understands CREATIONTIME. The JS enum is + // patched source and says nothing about what the .node was compiled from. + exports.Set("supportedProcessDataFlags", + Napi::Number::New(env, MEMORY | COMMANDLINE | CREATIONTIME)); return exports; } diff --git a/src/process.cc b/src/process.cc index 3eea92077c4d1d433119361d5c432881859131e9..22a47421da919c76e2194280974d39c2287b098d 100644 --- a/src/process.cc +++ b/src/process.cc @@ -21,7 +21,8 @@ uint32_t GetRawProcessList(std::vector& process_info, if (Process32First(snapshot_handle, &process_entry)) { do { if (process_entry.th32ProcessID != 0) { - ProcessInfo pinfo; + // Value-initialize: `memory` is otherwise stack garbage when the flag is unset. + ProcessInfo pinfo{}; pinfo.pid = process_entry.th32ProcessID; pinfo.ppid = process_entry.th32ParentProcessID; @@ -33,23 +34,51 @@ uint32_t GetRawProcessList(std::vector& process_info, GetProcessCommandLine(pinfo); } + if (CREATIONTIME & process_data_flags) { + GetProcessCreationTime(pinfo); + } + strcpy(pinfo.name, process_entry.szExeFile); process_info.push_back(std::move(pinfo)); process_count++; } - } while (process_count < 1024 && Process32Next(snapshot_handle, &process_entry)); + } while (Process32Next(snapshot_handle, &process_entry)); } CloseHandle(snapshot_handle); return process_count; } +void GetProcessCreationTime(ProcessInfo& process_info) { + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_info.pid); + if (hProcess == NULL) { + return; + } + + FILETIME creationTime, exitTime, kernelTime, userTime; + if (GetProcessTimes(hProcess, &creationTime, &exitTime, &kernelTime, &userTime)) { + ULARGE_INTEGER timestamp; + timestamp.LowPart = creationTime.dwLowDateTime; + timestamp.HighPart = creationTime.dwHighDateTime; + constexpr ULONGLONG WINDOWS_EPOCH_OFFSET_100NS = 116444736000000000ULL; + constexpr ULONGLONG HUNDRED_NS_PER_MILLISECOND = 10000ULL; + if (timestamp.QuadPart >= WINDOWS_EPOCH_OFFSET_100NS) { + process_info.creationTimeMs = + (timestamp.QuadPart - WINDOWS_EPOCH_OFFSET_100NS) / HUNDRED_NS_PER_MILLISECOND; + } + } + + CloseHandle(hProcess); +} + void GetProcessMemoryUsage(ProcessInfo& process_info) { DWORD pid = process_info.pid; HANDLE hProcess; PROCESS_MEMORY_COUNTERS pmc; - hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid); + // PROCESS_VM_READ is never used here -- GetProcessMemoryInfo reads counters the + // kernel keeps, not the address space -- and acquiring it is what EDR scores. + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid); if (hProcess == NULL) { return; @@ -81,7 +110,8 @@ void GetCpuUsage(Cpu& cpu_info, bool first_pass) { DWORD pid = cpu_info.pid; HANDLE hProcess; - hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid); + // GetProcessTimes needs no more than PROCESS_QUERY_LIMITED_INFORMATION. + hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid); if (hProcess == NULL) { return; diff --git a/src/process.h b/src/process.h index 82f8e4bcfa742551e5d874a7632736a7611d7aa7..78d1d2c3b2360ed06fd624b4cb2f5042510f7a77 100644 --- a/src/process.h +++ b/src/process.h @@ -22,18 +22,22 @@ struct ProcessInfo { DWORD ppid; DWORD memory; // Reported in bytes std::string commandLine; + ULONGLONG creationTimeMs; }; enum ProcessDataFlags { NONE = 0, MEMORY = 1, - COMMANDLINE = 2 + COMMANDLINE = 2, + CREATIONTIME = 4 }; uint32_t GetRawProcessList(std::vector& process_info, DWORD flags); void GetProcessMemoryUsage(ProcessInfo& process_info); +void GetProcessCreationTime(ProcessInfo& process_info); + void GetCpuUsage(Cpu& cpu_info, bool first_run); #endif // SRC_PROCESS_H_ diff --git a/src/process_commandline.cc b/src/process_commandline.cc index ea822b120e8038a4803e34647042f08f4aaf5ca1..25907c0bf542bed6c72b1b462b19bcf3210c3cfd 100644 --- a/src/process_commandline.cc +++ b/src/process_commandline.cc @@ -7,61 +7,119 @@ #include "process_commandline.h" #include #include -#include +#include -bool GetProcessCommandLine(ProcessInfo& process_info) { - HINSTANCE ntdll = GetModuleHandleW(L"ntdll.dll"); +namespace { + +// Windows 8.1 and later hand back a process's command line as a UNICODE_STRING +// the kernel builds, needing only PROCESS_QUERY_LIMITED_INFORMATION. +// +// There is deliberately no PEB fallback. Reading the command line out of the +// target's address space -- opening it for VM reads and then chaining +// memory reads across every pid on a timer -- is the credential-dumping +// primitive this reader exists to not perform, so it is absent from the binary +// rather than one anomalous NTSTATUS away. Electron's floor is Windows 10, so +// every OS Orca supports has this class; if a hooked ntdll refuses it anyway, +// the command line comes back empty, which callers already handle, instead of +// silently reinstating the primitive on exactly the instrumented machines this +// reader was written for. +const ULONG kProcessCommandLineInformation = 60; + +const NTSTATUS kStatusInfoLengthMismatch = static_cast(0xC0000004L); +const NTSTATUS kStatusBufferTooSmall = static_cast(0xC0000023L); + +// A command line is a UNICODE_STRING, whose Length is a USHORT, so the kernel +// can never need more than the header plus 64 KiB. Refusing anything larger +// keeps a bogus size from throwing bad_alloc out of a scan that has already +// walked most of the table. +const ULONG kMaxCommandLineBytes = sizeof(UNICODE_STRING) + 0xFFFF + sizeof(wchar_t); + +// winternl.h's PROCESSINFOCLASS does not name class 60 and its enumerator range +// stops far short of it, so the class travels as a ULONG rather than a cast enum. +typedef NTSTATUS(NTAPI* NtQueryInformationProcessFn)(HANDLE, ULONG, PVOID, ULONG, PULONG); + +// ntdll ships no import library for this entry point; it has to be resolved. +NtQueryInformationProcessFn ResolveNtQueryInformationProcess() { + HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); if (!ntdll) { + return nullptr; + } + return reinterpret_cast( + GetProcAddress(ntdll, "NtQueryInformationProcess")); +} + +NtQueryInformationProcessFn NtQueryInformationProcessEntry() { + static NtQueryInformationProcessFn entry = ResolveNtQueryInformationProcess(); + return entry; +} + +bool StoreCommandLineUtf8(ProcessInfo& process_info, const wchar_t* data, size_t wide_length) { + if (wide_length == 0) { + return false; + } + int length = static_cast(wide_length); + int charcount = WideCharToMultiByte(CP_UTF8, 0, data, length, NULL, 0, NULL, NULL); + if (!charcount) { return false; } + process_info.commandLine.resize(static_cast(charcount)); + WideCharToMultiByte(CP_UTF8, 0, data, length, &process_info.commandLine[0], charcount, NULL, + NULL); + return true; +} + +} // namespace - decltype(NtQueryInformationProcess)* nt_query_information_process = - reinterpret_cast( - GetProcAddress(ntdll, "NtQueryInformationProcess")); +bool GetProcessCommandLine(ProcessInfo& process_info) { + NtQueryInformationProcessFn query = NtQueryInformationProcessEntry(); + if (!query) { + return false; + } - if (!nt_query_information_process) { + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_info.pid); + if (process == NULL) { return false; } - PROCESS_BASIC_INFORMATION pbi{}; - PEB peb = {NULL}; - RTL_USER_PROCESS_PARAMETERS process_parameters = {NULL}; + ULONG size = 0; + NTSTATUS status = query(process, kProcessCommandLineInformation, nullptr, 0, &size); + if (NT_SUCCESS(status)) { + // Nothing was written, so there is no command line to read. + CloseHandle(process); + return false; + } + if (status != kStatusInfoLengthMismatch && status != kStatusBufferTooSmall) { + CloseHandle(process); + return false; + } + if (size < sizeof(UNICODE_STRING) || size > kMaxCommandLineBytes) { + CloseHandle(process); + return false; + } - // Get process handle - DWORD pid = process_info.pid; - HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); - if (hProcess == INVALID_HANDLE_VALUE) { + std::vector buffer(size); + status = query(process, kProcessCommandLineInformation, &buffer[0], size, &size); + CloseHandle(process); + if (!NT_SUCCESS(status)) { return false; } - // Get Process Environment Block (PEB) - NTSTATUS status = nt_query_information_process(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), nullptr); - if (NT_SUCCESS(status) && pbi.PebBaseAddress) { - // Read PEB - if (ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), nullptr)) { - // Read the processs parameters - if (ReadProcessMemory(hProcess, peb.ProcessParameters, &process_parameters, sizeof(RTL_USER_PROCESS_PARAMETERS), nullptr)) { - if (process_parameters.CommandLine.Length > 0) { - std::wstring buffer; - buffer.resize(process_parameters.CommandLine.Length / sizeof(wchar_t)); - if (ReadProcessMemory(hProcess, process_parameters.CommandLine.Buffer, &buffer[0], process_parameters.CommandLine.Length, nullptr)) { - int wide_length = static_cast(buffer.length()); - int charcount = WideCharToMultiByte(CP_UTF8, 0, buffer.data(), wide_length, - NULL, 0, NULL, NULL); - if (charcount) { - process_info.commandLine.resize(static_cast(charcount)); - WideCharToMultiByte(CP_UTF8, 0, buffer.data(), wide_length, - &process_info.commandLine[0], charcount, - NULL, NULL); - } - CloseHandle(hProcess); - return true; - } - } - } - } + // Header and characters arrive in one allocation, but treat the header as + // untrusted: a hooked ntdll is the case this reader is written for, and an + // unchecked Buffer/Length here would be an over-read encoded straight into JS. + // Bound against buffer.size(), never `size` -- the second query overwrote it. + const UNICODE_STRING* command_line = reinterpret_cast(&buffer[0]); + const unsigned char* begin = &buffer[0]; + const unsigned char* end = begin + buffer.size(); + const unsigned char* chars = reinterpret_cast(command_line->Buffer); + if (chars == nullptr || chars < begin + sizeof(UNICODE_STRING) || chars > end || + command_line->Length > static_cast(end - chars)) { + return false; } - CloseHandle(hProcess); - return false; + // True only when a command line was actually stored, so "empty" and "not + // recovered" stay the same answer they were before this reader replaced the + // PEB read. `src/process.cc` discards the result either way. + return StoreCommandLineUtf8(process_info, command_line->Buffer, + command_line->Length / sizeof(wchar_t)); } diff --git a/src/process_worker.cc b/src/process_worker.cc index c9e3457a759c1acaa2644231a4917d45aed951f8..3f26a354477f062b34bd31fbd17be529e6a2fd7a 100644 --- a/src/process_worker.cc +++ b/src/process_worker.cc @@ -43,6 +43,11 @@ void GetProcessesWorker::OnOK() { Napi::String::New(env, pinfo.commandLine)); } + if ((CREATIONTIME & process_data_flags_) && pinfo.creationTimeMs != 0) { + object.Set("creationTimeMs", + Napi::Number::New(env, static_cast(pinfo.creationTimeMs))); + } + result.Set(i, object); } diff --git a/typings/windows-process-tree.d.ts b/typings/windows-process-tree.d.ts index 08bdac2fdc5ead6f0fcfb5ee5a021e2298c7d523..458981566fc45c0084badff566b1e3791ec1b629 100644 --- a/typings/windows-process-tree.d.ts +++ b/typings/windows-process-tree.d.ts @@ -7,9 +7,17 @@ declare module '@vscode/windows-process-tree' { export enum ProcessDataFlag { None = 0, Memory = 1, - CommandLine = 2 + CommandLine = 2, + CreationTime = 4 } + /** + * The flag bits the compiled addon actually understands, or undefined off + * win32. `ProcessDataFlag` above is source; this is what the binary reports, + * so it is the only way to tell a patched build from a stale prebuilt. + */ + export const supportedProcessDataFlags: number | undefined; + export interface IProcessInfo { pid: number; ppid: number; @@ -24,6 +32,9 @@ declare module '@vscode/windows-process-tree' { * The string returned is at most 512 chars, strings exceeding this length are truncated. */ commandLine?: string; + + /** Process creation time in Unix milliseconds. */ + creationTimeMs?: number; } export interface IProcessCpuInfo extends IProcessInfo { @@ -35,6 +46,7 @@ declare module '@vscode/windows-process-tree' { name: string; memory?: number; commandLine?: string; + creationTimeMs?: number; children: IProcessTreeNode[]; }