Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secured protected virtual processes #7121

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 154 additions & 54 deletions osquery/tables/system/windows/processes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,13 @@ const std::map<unsigned long, std::string> kMemoryConstants = {

const unsigned long kMaxPathSize = 0x1000;

/**
* All of these structs are needed to interface with
* the NtQueryInformationProcess function, which is leveraged
* to get the CommandLine data for a process invocation.
*/
typedef NTSTATUS(NTAPI* NtQueryInformationProcessPtr)(
IN HANDLE ProcessHandle,
IN unsigned long ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL);

typedef ULONG(NTAPI* RtlNtStatusToDosErrorPtr)(NTSTATUS Status);

NtQueryInformationProcessPtr kNtQueryInformationProcess = nullptr;

RtlNtStatusToDosErrorPtr kRtlNtStatusToDosError = nullptr;
// See the Process Hacker implementation for more details on the hard coded 60
// https://github.com/processhacker/processhacker/blob/master/phnt/include/ntpsapi.h#L160
const PROCESSINFOCLASS ProcessCommandLine = static_cast<PROCESSINFOCLASS>(60);
// See the ZwQueryInformatioNprocess docs for more details on the hard coded 61
// https://docs.microsoft.com/en-us/windows/win32/procthread/zwqueryinformationprocess
const PROCESSINFOCLASS ProcessProtectionInformation =
static_cast<PROCESSINFOCLASS>(61);

typedef struct {
USHORT Length;
Expand Down Expand Up @@ -133,6 +123,64 @@ typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

typedef struct _PROCESS_EXTENDED_BASIC_INFORMATION {
SIZE_T Size; // Ignored as input, written with structure size on output
PROCESS_BASIC_INFORMATION BasicInfo;
union {
ULONG Flags;
struct {
ULONG IsProtectedProcess : 1;
ULONG IsWow64Process : 1;
ULONG IsProcessDeleting : 1;
ULONG IsCrossSessionCreate : 1;
ULONG IsFrozen : 1;
ULONG IsBackground : 1;
ULONG IsStronglyNamed : 1;
ULONG IsSecureProcess : 1;
ULONG IsSubsystemProcess : 1;
ULONG SpareBits : 23;
} s;
};
} PROCESS_EXTENDED_BASIC_INFORMATION, *PPROCESS_EXTENDED_BASIC_INFORMATION;

typedef enum _PS_PROTECTED_TYPE : UCHAR {
PsProtectedTypeNone,
PsProtectedTypeProtectedLight,
PsProtectedTypeProtected,
PsProtectedTypeMax
} PS_PROTECTED_TYPE;

const std::map<PS_PROTECTED_TYPE, std::string> kProtectedTypes = {
{PsProtectedTypeNone, "PsProtectedTypeNone"},
{PsProtectedTypeProtectedLight, "PsProtectedTypeProtectedLight"},
{PsProtectedTypeProtected, "PsProtectedTypeProtected"},
{PsProtectedTypeMax, "PsProtectedTypeMax"},
};

typedef enum _PS_PROTECTED_SIGNER : UCHAR {
PsProtectedSignerNone,
PsProtectedSignerAuthenticode,
PsProtectedSignerCodeGen,
PsProtectedSignerAntimalware,
PsProtectedSignerLsa,
PsProtectedSignerWindows,
PsProtectedSignerWinTcb,
PsProtectedSignerWinSystem,
PsProtectedSignerApp,
PsProtectedSignerMax
} PS_PROTECTED_SIGNER;

typedef struct _PS_PROTECTION {
union {
struct {
PS_PROTECTED_TYPE Type : 3;
BOOLEAN Audit : 1;
PS_PROTECTED_SIGNER Signer : 4;
} s;
UCHAR Level;
};
} PS_PROTECTION, *PPS_PROTECTION;

/// Given a pid, enumerates all loaded modules and memory pages for that process
Status genMemoryMap(unsigned long pid, QueryData& results) {
auto proc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
Expand Down Expand Up @@ -250,13 +298,14 @@ Status getUserProcessParameters(HANDLE proc,
if (!ReadProcessMemory(
proc, pbi.PebBaseAddress, &peb, sizeof(peb), &bytes_read)) {
return Status::failure("Reading PEB failed for " + std::to_string(pid) +
" with " + std::to_string(status));
" with " + std::to_string(GetLastError()));
}

if (!ReadProcessMemory(
proc, peb.ProcessParameters, &out, sizeof(out), &bytes_read)) {
return Status::failure("Reading USER_PROCESS_PARAMETERS failed for " +
std::to_string(pid));
std::to_string(pid) + " with " +
std::to_string(GetLastError()));
}

return Status::success();
Expand Down Expand Up @@ -293,18 +342,9 @@ Status getProcessCommandLineLegacy(HANDLE proc,
Status getProcessCommandLine(HANDLE& proc,
std::string& out,
const unsigned long pid) {
if (kNtQueryInformationProcess == nullptr) {
return Status::failure("Failed to resolve NtQueryInformationProcess with " +
std::to_string(GetLastError()));
}

unsigned long size_out = 0;
PROCESS_BASIC_INFORMATION proc_info;
SecureZeroMemory(&proc_info, sizeof(PROCESS_BASIC_INFORMATION));

// See the Process Hacker implementation for more details on the hard coded 60
// https://github.com/processhacker/processhacker/blob/master/phnt/include/ntpsapi.h#L160
auto ret = kNtQueryInformationProcess(proc, 60, NULL, 0, &size_out);
auto ret =
NtQueryInformationProcess(proc, ProcessCommandLine, NULL, 0, &size_out);

if (ret != STATUS_BUFFER_OVERFLOW && ret != STATUS_BUFFER_TOO_SMALL &&
ret != STATUS_INFO_LENGTH_MISMATCH) {
Expand All @@ -314,8 +354,8 @@ Status getProcessCommandLine(HANDLE& proc,
}

std::vector<char> cmdline(size_out, 0x0);
ret =
kNtQueryInformationProcess(proc, 60, cmdline.data(), size_out, &size_out);
ret = NtQueryInformationProcess(
proc, ProcessCommandLine, cmdline.data(), size_out, &size_out);

if (!NT_SUCCESS(ret)) {
return Status::failure("NtQueryInformationProcess failed for " +
Expand Down Expand Up @@ -363,7 +403,12 @@ void getProcessPathInfo(HANDLE& proc,
SecureZeroMemory(path.data(), kMaxPathSize);
auto ret = QueryFullProcessImageNameW(proc, 0, path.data(), &out);
if (ret != TRUE) {
VLOG(1) << "Failed to lookup path information for process " << pid;
ret = QueryFullProcessImageNameW(
proc, PROCESS_NAME_NATIVE, path.data(), &out);
}
if (ret != TRUE) {
VLOG(1) << "Failed to lookup path information for process " << pid
<< " with " << GetLastError();
} else {
r["path"] = SQL_TEXT(wstringToString(path.data()));
}
Expand All @@ -373,7 +418,11 @@ void getProcessPathInfo(HANDLE& proc,
r["on_disk"] = INTEGER(
boost_path.empty() ? -1 : osquery::pathExists(path.data()).ok());
}
}

void getProcessCurrentDirectoryInfo(HANDLE& proc,
const unsigned long pid,
DynamicTableRowHolder& r) {
std::string currDir{""};
auto s = getProcessCurrentDirectory(proc, currDir, pid);
if (!s.ok()) {
Expand Down Expand Up @@ -411,7 +460,7 @@ void genProcessUserTokenInfo(HANDLE& proc, DynamicTableRowHolder& r) {
elevated = elevation.TokenIsElevated;
}
}
r["is_elevated_token"] = INTEGER(elevated);
r["elevated_token"] = INTEGER(elevated);
if (ret != 0 && !tok_user.empty()) {
auto sid = PTOKEN_OWNER(tok_user.data())->Owner;

Expand Down Expand Up @@ -475,6 +524,23 @@ void genProcRssInfo(HANDLE& proc, DynamicTableRowHolder& r) {
r["total_size"] = ret == TRUE ? BIGINT(mem_ctr.PrivateUsage) : BIGINT(-1);
}

PS_PROTECTED_TYPE getProcessProtectedType(HANDLE& proc,
const unsigned long pid) {
PS_PROTECTION psp{0};
unsigned long len{0};
PROCESS_EXTENDED_BASIC_INFORMATION pebi{0};
NTSTATUS status = NtQueryInformationProcess(
proc, ProcessProtectionInformation, &psp, sizeof(psp), &len);
if (NT_SUCCESS(status)) {
return psp.s.Type;
}
if (status != STATUS_INVALID_INFO_CLASS) {
VLOG(1) << "Failed to get process protection type " << pid << " with "
<< status;
}
return PS_PROTECTED_TYPE::PsProtectedTypeNone;
}

TableRows genProcesses(QueryContext& context) {
TableRows results;

Expand Down Expand Up @@ -548,21 +614,29 @@ TableRows genProcesses(QueryContext& context) {
r["handle_count"] = BIGINT(-1);

r["on_disk"] = BIGINT(-1);
r["elevated_token"] = BIGINT(-1);
r["secure_process"] = BIGINT(-1);
r["protection_type"] = SQL_TEXT("");
r["virtual_process"] = BIGINT(-1);

auto proc_handle =
OpenProcess(PROCESS_ALL_ACCESS, FALSE, proc.th32ProcessID);
if (pid == 0) {
results.push_back(r);
ret = Process32NextW(proc_snap, &proc);
continue;
}

auto proc_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
auto const proc_handle_manager =
scope_guard::create([&proc_handle]() { CloseHandle(proc_handle); });

// If we fail to get all privs, open with less permissions
if (proc_handle == NULL) {
proc_handle = OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION, FALSE, proc.th32ProcessID);
proc_handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
}

if (proc_handle == NULL) {
VLOG(1) << "Failed to open handle to process " << proc.th32ProcessID
<< " with " << GetLastError();
VLOG(1) << "Failed to open handle to process " << pid << " with "
<< GetLastError();
results.push_back(r);
ret = Process32NextW(proc_snap, &proc);
continue;
Expand All @@ -571,23 +645,49 @@ TableRows genProcesses(QueryContext& context) {
auto nice = GetPriorityClass(proc_handle);
r["nice"] = nice != FALSE ? INTEGER(nice) : "-1";

if (context.isAnyColumnUsed({"cwd", "root", "path", "on_disk"})) {
auto protection = getProcessProtectedType(proc_handle, pid);
r["protection_type"] = SQL_TEXT(kProtectedTypes.at(protection));

bool isProtectedProcess =
protection != PS_PROTECTED_TYPE::PsProtectedTypeNone;
bool isSecureProcess = false;
bool isVirtualProcess = false;
{
PROCESS_EXTENDED_BASIC_INFORMATION pebi{0};
unsigned long len{0};
NTSTATUS status = NtQueryInformationProcess(
proc_handle, ProcessBasicInformation, &pebi, sizeof(pebi), &len);
// Handle return on pre Windows 8.1 and just populate the non extended
// ProcessBasicInformation variant
if (status == STATUS_INFO_LENGTH_MISMATCH) {
status = NtQueryInformationProcess(proc_handle,
ProcessBasicInformation,
&pebi.BasicInfo,
sizeof(pebi.BasicInfo),
&len);
}
if (NT_SUCCESS(status)) {
isSecureProcess = pebi.s.IsSecureProcess;
r["secure_process"] = BIGINT(isSecureProcess);
isVirtualProcess = pebi.BasicInfo.PebBaseAddress == NULL;
r["virtual_process"] = BIGINT(isVirtualProcess);
} else {
VLOG(1) << "Failed to query ProcessBasicInformation for pid "
<< proc.th32ProcessID << " with " << status;
}
}

if (context.isAnyColumnUsed({"path", "on_disk"})) {
getProcessPathInfo(proc_handle, pid, r);
}

if (context.isColumnUsed("cmdline")) {
if (kNtQueryInformationProcess == nullptr) {
// GetModuleHandle doesn't increment a reference, no need to track
// handle
kNtQueryInformationProcess =
reinterpret_cast<NtQueryInformationProcessPtr>(GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"));

kRtlNtStatusToDosError =
reinterpret_cast<RtlNtStatusToDosErrorPtr>(GetProcAddress(
GetModuleHandleA("ntdll.dll"), "RtlNtStatusToDosError"));
}
if (context.isAnyColumnUsed({"cwd", "root"}) && !isProtectedProcess &&
!isSecureProcess && !isVirtualProcess) {
getProcessCurrentDirectoryInfo(proc_handle, pid, r);
}

if (context.isColumnUsed("cmdline") && !isSecureProcess &&
!isVirtualProcess) {
std::string cmd{""};
auto s = getProcessCommandLine(
proc_handle, cmd, static_cast<unsigned long>(proc.th32ProcessID));
Expand All @@ -598,7 +698,7 @@ TableRows genProcesses(QueryContext& context) {
r["cmdline"] = cmd;
}

if (context.isAnyColumnUsed({"uid", "gid", "is_elevated_token"})) {
if (context.isAnyColumnUsed({"uid", "gid", "elevated_token"})) {
genProcessUserTokenInfo(proc_handle, r);
}

Expand Down
5 changes: 4 additions & 1 deletion specs/processes.table
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ schema([
Column("nice", INTEGER, "Process nice level (-20 to 20, default 0)"),
])
extended_schema(WINDOWS, [
Column("is_elevated_token", INTEGER, "Process uses elevated token yes=1, no=0"),
Column("elevated_token", INTEGER, "Process uses elevated token yes=1, no=0"),
Column("secure_process", INTEGER, "Process is secure (IUM) yes=1, no=0"),
Column("protection_type", TEXT, "The protection type of the process"),
Column("virtual_process", INTEGER, "Process is virtual (e.g. System, Registry, vmmem) yes=1, no=0"),
Column("elapsed_time", BIGINT, "Elapsed time in seconds this process has been running."),
Column("handle_count", BIGINT, "Total number of handles that the process has open. This number is the sum of the handles currently opened by each thread in the process."),
Column("percent_processor_time", BIGINT, "Returns elapsed time that all of the threads of this process used the processor to execute instructions in 100 nanoseconds ticks."),
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/tables/processes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ TEST_F(ProcessesTest, test_sanity) {

// Add the platform-specific columns.
if (isPlatform(PlatformType::TYPE_WINDOWS)) {
row_map.emplace("is_elevated_token", NormalType);
row_map.emplace("elevated_token", IntType);
row_map.emplace("secure_process", IntType);
row_map.emplace("protection_type", NormalType);
row_map.emplace("virtual_process", IntType);
row_map.emplace("elapsed_time", IntType);
row_map.emplace("handle_count", IntType);
row_map.emplace("percent_processor_time", IntType);
Expand Down