Files
loader/KillSafety.cpp
2026-11-03 20:45:39 -08:00

452 lines
13 KiB
C++

#include "Common.h"
#include "KillSafety.h"
#include "DriverClient.h"
#include "Logger.h"
#include <algorithm>
#include <cwctype>
#include <set>
#include <sstream>
static std::wstring Lower(const std::wstring& s)
{
std::wstring out = s;
for (auto& c : out)
c = (wchar_t)towlower(c);
return out;
}
static std::wstring BaseNameFromPath(const std::wstring& path)
{
size_t slash = path.find_last_of(L"\\/");
if (slash == std::wstring::npos)
return path;
return path.substr(slash + 1);
}
bool IsProtectedImage(const std::wstring& imageName)
{
static const wchar_t* protectedImages[] =
{
// Kernel / session manager / critical subsystems. Terminate any of
// these and the machine bugchecks immediately.
L"system",
L"registry",
L"memcompression",
L"smss.exe",
L"csrss.exe",
L"wininit.exe",
L"winlogon.exe",
L"services.exe",
L"lsass.exe",
L"svchost.exe",
L"fontdrvhost.exe",
L"dwm.exe",
L"wudfhost.exe",
L"sihost.exe",
L"ctfmon.exe",
L"explorer.exe",
L"taskhostw.exe",
L"logonui.exe",
L"userinit.exe",
L"wininit.exe",
L"spoolsv.exe",
L"audiodg.exe",
L"conhost.exe",
L"sshd.exe",
};
const std::wstring lower = Lower(imageName);
for (const wchar_t* p : protectedImages)
{
if (lower == Lower(p))
return true;
}
return false;
}
// Collect our own pid and every ancestor up to the root.
static std::set<DWORD> SelfAndAncestors()
{
std::set<DWORD> result;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE)
{
result.insert(GetCurrentProcessId());
return result;
}
std::vector<std::pair<DWORD, DWORD>> links; // pid -> parent pid
PROCESSENTRY32W pe{};
pe.dwSize = sizeof(pe);
if (Process32First(snap, &pe))
{
do
{
links.push_back({ pe.th32ProcessID, pe.th32ParentProcessID });
} while (Process32Next(snap, &pe));
}
CloseHandle(snap);
DWORD cur = GetCurrentProcessId();
for (int depth = 0; depth < 16 && cur != 0; ++depth)
{
if (!result.insert(cur).second)
break;
DWORD parent = 0;
for (const auto& link : links)
{
if (link.first == cur)
{
parent = link.second;
break;
}
}
cur = parent;
}
return result;
}
static std::vector<DWORD> PidsForImage(const std::wstring& imageName)
{
std::vector<DWORD> pids;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE)
return pids;
PROCESSENTRY32W pe{};
pe.dwSize = sizeof(pe);
if (Process32First(snap, &pe))
{
do
{
if (_wcsicmp(pe.szExeFile, imageName.c_str()) == 0)
pids.push_back(pe.th32ProcessID);
} while (Process32Next(snap, &pe));
}
CloseHandle(snap);
return pids;
}
bool KillReport::Refused(KillOutcome o) const
{
return o == KillOutcome::Protected ||
o == KillOutcome::SelfOrAncestor ||
o == KillOutcome::IdentityMismatch;
}
std::wstring KillReport::Format() const
{
std::wostringstream os;
os << (dryRun ? L"DRY-RUN" : L"EXECUTED")
<< L" killed=" << killed
<< L" candidates=" << attempts.size();
const size_t maxLines = 30;
size_t shown = 0;
for (const auto& a : attempts)
{
if (shown >= maxLines)
{
os << L"\r\n ... " << (attempts.size() - shown) << L" more";
break;
}
++shown;
const wchar_t* state = L"?";
switch (a.outcome)
{
case KillOutcome::WouldKill: state = L"would-kill"; break;
case KillOutcome::Killed: state = L"killed"; break;
case KillOutcome::Protected: state = L"SKIP protected"; break;
case KillOutcome::SelfOrAncestor: state = L"SKIP own-tree"; break;
case KillOutcome::IdentityMismatch:state = L"SKIP identity"; break;
case KillOutcome::OpenFailed: state = L"skip open-failed"; break;
case KillOutcome::AccessDenied: state = L"SKIP access-denied(needs SYSTEM or PPL)"; break;
case KillOutcome::DriverFailed: state = L"driver-failed"; break;
}
os << L"\r\n " << a.pid << L" " << state;
if (!a.imagePath.empty())
os << L" " << a.imagePath;
}
return os.str();
}
void KillProcessesSafe(
const std::vector<std::wstring>& imageNames,
bool dryRun,
DWORD maxKills,
KillReport& report,
bool allowBlindDriver)
{
report = KillReport();
report.dryRun = dryRun;
if (maxKills == 0)
maxKills = 16;
const std::set<DWORD> own = SelfAndAncestors();
std::set<DWORD> seen;
DWORD kills = 0;
for (const auto& requested : imageNames)
{
if (kills >= maxKills)
break;
if (requested.empty())
continue;
const std::wstring requestedBase = BaseNameFromPath(requested);
// Refuse before enumerating: a protected image is never a candidate.
if (IsProtectedImage(requestedBase))
{
KillAttempt blocked;
blocked.outcome = KillOutcome::Protected;
blocked.imagePath = requestedBase;
report.attempts.push_back(blocked);
continue;
}
for (DWORD pid : PidsForImage(requestedBase))
{
if (kills >= maxKills)
break;
if (pid == 0 || pid == 4 || pid <= 4)
continue;
if (!seen.insert(pid).second)
continue;
KillAttempt attempt;
attempt.pid = pid;
if (own.count(pid))
{
attempt.outcome = KillOutcome::SelfOrAncestor;
report.attempts.push_back(attempt);
continue;
}
// Hold the handle across the IOCTL: the process object stays alive,
// so the PID cannot be recycled into something critical mid-kill.
HANDLE proc = OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE,
FALSE,
pid);
if (!proc)
{
// User-mode terminate is refused for PPL / SYSTEM images. The
// driver path does not need PROCESS_TERMINATE, so verify identity
// through a query handle and hand the PID to the kernel.
HANDLE queryOnly = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!queryOnly)
{
// Nothing openable (SYSTEM image while we are not elevated).
// Only the kernel can still act, and only when the caller
// explicitly allowed it: re-check the PID against a fresh
// snapshot so a recycled PID cannot slip through.
bool stillMatches = false;
for (DWORD again : PidsForImage(requestedBase))
{
if (again == pid)
{
stillMatches = true;
break;
}
}
if (allowBlindDriver && stillMatches && !IsProtectedImage(requestedBase))
{
if (dryRun)
{
attempt.outcome = KillOutcome::WouldKill;
attempt.imagePath += L" (driver)";
report.attempts.push_back(attempt);
continue;
}
const bool viaDriver = TerminateProcessViaDriver(pid);
if (viaDriver)
{
attempt.outcome = KillOutcome::Killed;
++kills;
report.killed = (int)kills;
}
else
{
attempt.outcome = KillOutcome::DriverFailed;
}
report.attempts.push_back(attempt);
Sleep(150 + (GetTickCount() % 200));
continue;
}
attempt.outcome =
(GetLastError() == ERROR_ACCESS_DENIED)
? KillOutcome::AccessDenied
: KillOutcome::OpenFailed;
report.attempts.push_back(attempt);
continue;
}
wchar_t fullPath[4096]{};
DWORD len = _countof(fullPath);
if (!QueryFullProcessImageNameW(queryOnly, 0, fullPath, &len))
{
CloseHandle(queryOnly);
attempt.outcome = KillOutcome::OpenFailed;
report.attempts.push_back(attempt);
continue;
}
attempt.imagePath.assign(fullPath, len);
const std::wstring actualBase = BaseNameFromPath(attempt.imagePath);
if (_wcsicmp(actualBase.c_str(), requestedBase.c_str()) != 0)
{
CloseHandle(queryOnly);
attempt.outcome = KillOutcome::IdentityMismatch;
report.attempts.push_back(attempt);
continue;
}
if (IsProtectedImage(actualBase))
{
CloseHandle(queryOnly);
attempt.outcome = KillOutcome::Protected;
report.attempts.push_back(attempt);
continue;
}
if (dryRun)
{
CloseHandle(queryOnly);
attempt.outcome = KillOutcome::WouldKill;
report.attempts.push_back(attempt);
continue;
}
const bool viaDriver = TerminateProcessViaDriver(pid);
CloseHandle(queryOnly);
if (viaDriver)
{
attempt.outcome = KillOutcome::Killed;
++kills;
report.killed = (int)kills;
}
else
{
attempt.outcome = KillOutcome::DriverFailed;
}
report.attempts.push_back(attempt);
Sleep(150 + (GetTickCount() % 200));
continue;
}
wchar_t fullPath[4096]{};
DWORD len = _countof(fullPath);
if (!QueryFullProcessImageNameW(proc, 0, fullPath, &len))
{
CloseHandle(proc);
attempt.outcome = KillOutcome::OpenFailed;
report.attempts.push_back(attempt);
continue;
}
attempt.imagePath.assign(fullPath, len);
const std::wstring actualBase = BaseNameFromPath(attempt.imagePath);
// Identity must match what we asked to kill, otherwise it is a
// different image that happened to share a PID or a name variant.
if (_wcsicmp(actualBase.c_str(), requestedBase.c_str()) != 0)
{
CloseHandle(proc);
attempt.outcome = KillOutcome::IdentityMismatch;
report.attempts.push_back(attempt);
continue;
}
if (IsProtectedImage(actualBase))
{
CloseHandle(proc);
attempt.outcome = KillOutcome::Protected;
report.attempts.push_back(attempt);
continue;
}
if (dryRun)
{
CloseHandle(proc);
attempt.outcome = KillOutcome::WouldKill;
report.attempts.push_back(attempt);
continue;
}
const bool ok = TerminateProcessViaDriver(pid);
CloseHandle(proc);
if (ok)
{
attempt.outcome = KillOutcome::Killed;
++kills;
report.killed = (int)kills;
}
else
{
attempt.outcome = KillOutcome::DriverFailed;
}
report.attempts.push_back(attempt);
// Space kills out; a tight loop of terminate IOCTLs is both noisy
// and a good way to catch a scheduler hiccup.
Sleep(150 + (GetTickCount() % 200));
}
}
}