blob: a841158ba40acf36978781cbec26a28ad4cf8ae3 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "secagentd/process_cache.h"
#include <unistd.h>
#include <algorithm>
#include <cinttypes>
#include <memory>
#include <string>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "base/containers/lru_cache.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/hash/md5.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece_forward.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "openssl/sha.h"
#include "re2/re2.h"
#include "secagentd/bpf/process.h"
#include "secagentd/proto/security_xdr_events.pb.h"
namespace {
namespace bpf = secagentd::bpf;
namespace pb = cros_xdr::reporting;
using secagentd::ProcessCache;
static const char kErrorFailedToStat[] = "Failed to stat ";
static const char kErrorFailedToResolve[] = "Failed to resolve ";
static const char kErrorFailedToRead[] = "Failed to read ";
static const char kErrorFailedToParse[] = "Failed to parse ";
static const char kErrorSslSha[] = "SSL SHA error";
std::string StableUuid(ProcessCache::InternalProcessKeyType seed) {
base::MD5Digest md5;
base::MD5Sum(&seed, sizeof(seed), &md5);
// Convert the hash to a UUID string. Pretend to be version 4, variant 1.
md5.a[4] = (md5.a[4] & 0x0f) | 0x40;
md5.a[6] = (md5.a[6] & 0x3f) | 0x80;
return base::StringPrintf(
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
md5.a[0], md5.a[1], md5.a[2], md5.a[3], md5.a[4], md5.a[5], md5.a[6],
md5.a[7], md5.a[8], md5.a[9], md5.a[10], md5.a[11], md5.a[12], md5.a[13],
md5.a[14], md5.a[15]);
}
// Kernel arg and env lists use '\0' to delimit elements. Tokenize the string
// and use single quotes (') to designate atomic elements.
// bufsize is the total capacity of buf (used for bounds checking).
// payload_len is the length of actual payload including the final '\0'.
std::string SafeTransformArgvEnvp(const char* buf,
size_t bufsize,
size_t payload_len) {
std::string str;
if (payload_len <= 0 || payload_len > bufsize) {
return str;
}
base::CStringTokenizer t(buf, buf + payload_len, std::string("\0", 1));
while (t.GetNext()) {
str.append(base::StringPrintf("'%s' ", t.token().c_str()));
}
if (str.length() > 0) {
str.pop_back();
}
return str;
}
// Fills a FileImage proto with contents from bpf image_info.
void FillImageFromBpf(const bpf::cros_image_info& image_info,
pb::FileImage* file_image_proto) {
file_image_proto->set_pathname(std::string(image_info.pathname));
file_image_proto->set_mnt_ns(image_info.mnt_ns);
file_image_proto->set_inode_device_id(image_info.inode_device_id);
file_image_proto->set_inode(image_info.inode);
file_image_proto->set_canonical_uid(image_info.uid);
file_image_proto->set_canonical_gid(image_info.gid);
file_image_proto->set_mode(image_info.mode);
}
void FillProcessFromBpf(const bpf::cros_process_start& process_start,
pb::Process* process_proto) {
ProcessCache::PartiallyFillProcessFromBpfTaskInfo(process_start.task_info,
process_proto);
FillImageFromBpf(process_start.image_info, process_proto->mutable_image());
}
absl::Status GetNsFromPath(const base::FilePath& ns_symlink_path,
uint64_t* ns) {
// mnt_ns_symlink is not actually pathlike. E.g: "mnt:[4026531840]".
constexpr char kMntNsPattern[] = R"(mnt:\[(\d+)\])";
static const LazyRE2 kMntNsRe = {kMntNsPattern};
base::FilePath ns_symlink;
if (!base::ReadSymbolicLink(ns_symlink_path, &ns_symlink)) {
return absl::NotFoundError(
base::StrCat({kErrorFailedToResolve, ns_symlink_path.value()}));
}
if (!RE2::FullMatch(ns_symlink.value(), *kMntNsRe, ns)) {
return absl::NotFoundError(
base::StrCat({kErrorFailedToParse, ns_symlink.value()}));
}
return absl::OkStatus();
}
absl::Status GetStatFromProcfs(const base::FilePath& stat_path,
uint64_t* ppid,
uint64_t* starttime_t,
std::string* set_comm_if_kthread) {
std::string proc_stat_contents;
if (!base::ReadFileToString(stat_path, &proc_stat_contents)) {
return absl::NotFoundError(
base::StrCat({kErrorFailedToRead, stat_path.value()}));
}
// See https://man7.org/linux/man-pages/man5/proc.5.html for
// /proc/[pid]/stat format. All tokens are delimited with a whitespace. One
// major caveat is that comm (field 2) token may have an embedded whitespace
// and is so delimited by parentheses. The token may also have embedded
// parentheses though so we just ignore everything until the final ')'.
// StringTokenizer::set_quote_chars does not help with this. It accepts
// multiple quote chars but does not work for asymmetric quoting.
size_t end_of_comm = proc_stat_contents.rfind(')');
if (end_of_comm == std::string::npos) {
return absl::OutOfRangeError(
base::StrCat({kErrorFailedToParse, stat_path.value()}));
}
base::StringTokenizer t(proc_stat_contents.begin() + end_of_comm,
proc_stat_contents.end(), " ");
// We could avoid a separate loop here but the tokenizer API is awkward for
// random access.
std::vector<base::StringPiece> stat_tokens;
while (t.GetNext()) {
stat_tokens.push_back(t.token_piece());
}
// We need the following fields (1-indexed in man page):
// (4) ppid %d
// (9) flags %u
// (22) starttime %llu
// And remember that we started tokenizing at (2) comm.
static const size_t kPpidField = 2;
static const size_t kFlagsField = 7;
static const size_t kStarttimeField = 20;
uint32_t flags;
if ((stat_tokens.size() <= kStarttimeField) ||
(!base::StringToUint64(stat_tokens[kPpidField], ppid)) ||
(!base::StringToUint(stat_tokens[kFlagsField], &flags)) ||
(!base::StringToUint64(stat_tokens[kStarttimeField], starttime_t))) {
return absl::OutOfRangeError(
base::StrCat({kErrorFailedToParse, stat_path.value()}));
}
constexpr uint32_t kPfKthread = 0x00200000; // Defined in linux/sched.h.
if (flags & kPfKthread) {
size_t start_of_comm = proc_stat_contents.find('(');
if (start_of_comm != std::string::npos &&
(start_of_comm + 1 <= end_of_comm)) {
*set_comm_if_kthread = base::StrCat(
{"[",
base::MakeStringPiece(proc_stat_contents.begin() + start_of_comm + 1,
proc_stat_contents.begin() + end_of_comm),
"]"});
}
}
return absl::OkStatus();
}
absl::StatusOr<std::string> GenerateImageHash(
const base::FilePath& image_path_in_ns) {
base::File image(image_path_in_ns,
base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!image.IsValid()) {
return absl::NotFoundError(
base::StrCat({kErrorFailedToRead, image_path_in_ns.value()}));
}
SHA256_CTX ctx;
if (!SHA256_Init(&ctx)) {
return absl::InternalError(kErrorSslSha);
}
std::array<char, 4096> buf;
int bytes_read = 0;
while ((bytes_read = image.ReadAtCurrentPos(buf.data(), buf.size())) > 0) {
if (!SHA256_Update(&ctx, buf.data(), bytes_read)) {
return absl::InternalError(kErrorSslSha);
}
}
if (bytes_read < 0) {
return absl::AbortedError(
base::StrCat({kErrorFailedToRead, image_path_in_ns.value()}));
}
static_assert(sizeof(buf) >= SHA256_DIGEST_LENGTH);
if (!SHA256_Final(reinterpret_cast<unsigned char*>(buf.data()), &ctx)) {
return absl::InternalError(kErrorSslSha);
}
return base::HexEncode(buf.data(), SHA256_DIGEST_LENGTH);
}
absl::StatusOr<ProcessCache::InternalImageValueType>
VerifyStatAndGenerateImageHash(
const ProcessCache::InternalImageKeyType& image_key,
const base::FilePath& image_path_in_ns) {
auto hash = GenerateImageHash(image_path_in_ns);
if (!hash.ok()) {
return hash.status();
}
base::stat_wrapper_t image_stat;
if (base::File::Stat(image_path_in_ns.value().c_str(), &image_stat) ||
(image_stat.st_dev != image_key.inode_device_id) ||
(image_stat.st_ino != image_key.inode) ||
(image_stat.st_mtim.tv_sec != image_key.mtime.tv_sec) ||
(image_stat.st_mtim.tv_nsec != image_key.mtime.tv_nsec) ||
(image_stat.st_ctim.tv_sec != image_key.ctime.tv_sec) ||
(image_stat.st_ctim.tv_nsec != image_key.ctime.tv_nsec)) {
return absl::NotFoundError(
base::StrCat({"Failed to match stat of image hashed at ",
image_path_in_ns.value()}));
}
return ProcessCache::InternalImageValueType{.sha256 = hash.value()};
}
// Safely initialized and trivially destructible static value of SC_CLK_TCK.
const uint64_t GetScClockTck() {
static const uint64_t kScClockTck = sysconf(_SC_CLK_TCK);
return kScClockTck;
}
} // namespace
namespace secagentd {
constexpr ProcessCache::InternalProcessCacheType::size_type
kProcessCacheMaxSize = 256;
constexpr ProcessCache::InternalImageCacheType::size_type kImageCacheMaxSize =
256;
uint64_t ProcessCache::LossyNsecToClockT(bpf::time_ns_t ns) {
static constexpr uint64_t kNsecPerSec = 1000000000;
const uint64_t sc_clock_tck = GetScClockTck();
// Copied from the kernel procfs code though we unfortunately cannot use
// ifdefs and need to do comparisons live.
if ((kNsecPerSec % sc_clock_tck) == 0) {
return ns / (kNsecPerSec / sc_clock_tck);
} else if ((sc_clock_tck % 512) == 0) {
return (ns * sc_clock_tck / 512) / (kNsecPerSec / 512);
} else {
return (ns * 9) /
((9ull * kNsecPerSec + (sc_clock_tck / 2)) / sc_clock_tck);
}
}
// Converts clock_t to seconds.
int64_t ProcessCache::ClockTToSeconds(uint64_t clock_t) {
return clock_t / GetScClockTck();
}
void ProcessCache::PartiallyFillProcessFromBpfTaskInfo(
const bpf::cros_process_task_info& task_info, pb::Process* process_proto) {
ProcessCache::InternalProcessKeyType key{
LossyNsecToClockT(task_info.start_time), task_info.pid};
process_proto->set_process_uuid(StableUuid(key));
process_proto->set_canonical_pid(task_info.pid);
process_proto->set_canonical_uid(task_info.uid);
process_proto->set_commandline(SafeTransformArgvEnvp(
task_info.commandline, sizeof(task_info.commandline),
task_info.commandline_len));
process_proto->set_rel_start_time_s(ClockTToSeconds(key.start_time_t));
}
ProcessCache::ProcessCache(const base::FilePath& root_path)
: process_cache_(
std::make_unique<InternalProcessCacheType>(kProcessCacheMaxSize)),
image_cache_(
std::make_unique<InternalImageCacheType>(kImageCacheMaxSize)),
root_path_(root_path) {}
ProcessCache::ProcessCache() : ProcessCache(base::FilePath("/")) {}
void ProcessCache::PutFromBpfExec(
const bpf::cros_process_start& process_start) {
InternalProcessKeyType key{
LossyNsecToClockT(process_start.task_info.start_time),
process_start.task_info.pid};
auto process_proto = std::make_unique<pb::Process>();
FillProcessFromBpf(process_start, process_proto.get());
InternalProcessKeyType parent_key{
LossyNsecToClockT(process_start.task_info.parent_start_time),
process_start.task_info.ppid};
InternalImageKeyType image_key{
process_start.image_info.inode_device_id, process_start.image_info.inode,
process_start.image_info.mtime, process_start.image_info.ctime};
{
base::AutoLock cache_lock(image_cache_lock_);
auto it = InclusiveGetImage(
image_key, base::FilePath(process_start.image_info.pathname));
if (it != image_cache_->end()) {
process_proto->mutable_image()->set_sha256(it->second.sha256);
}
}
base::AutoLock lock(process_cache_lock_);
process_cache_->Put(
key, InternalProcessValueType({std::move(process_proto), parent_key}));
}
void ProcessCache::EraseProcess(uint64_t pid, bpf::time_ns_t start_time_ns) {
InternalProcessKeyType key{LossyNsecToClockT(start_time_ns), pid};
base::AutoLock lock(process_cache_lock_);
auto it = process_cache_->Peek(key);
if (it != process_cache_->end()) {
process_cache_->Erase(it);
}
}
ProcessCache::InternalProcessCacheType::const_iterator
ProcessCache::InclusiveGetProcess(const InternalProcessKeyType& key) {
process_cache_lock_.AssertAcquired();
// PID 0 doesn't exist and is also used to signify the end of the process
// "linked list".
if (key.pid == 0) {
return process_cache_->end();
}
auto it = process_cache_->Get(key);
if (it != process_cache_->end()) {
return it;
}
absl::StatusOr<InternalProcessValueType> statusor;
{
base::AutoUnlock unlock(process_cache_lock_);
statusor = MakeFromProcfs(key);
if (!statusor.ok()) {
LOG(ERROR) << statusor.status();
return process_cache_->end();
}
}
it = process_cache_->Put(key, std::move(*statusor));
return it;
}
ProcessCache::InternalImageCacheType::const_iterator
ProcessCache::InclusiveGetImage(const InternalImageKeyType& image_key,
const base::FilePath& image_path_in_ns) {
image_cache_lock_.AssertAcquired();
auto it = image_cache_->Get(image_key);
if (it != image_cache_->end()) {
if (it->first.mtime.tv_sec == 0 || it->first.ctime.tv_sec == 0) {
// Invalidate entry and force checksum if its cached ctime or mtime seems
// missing.
image_cache_->Erase(it);
it = image_cache_->end();
} else {
return it;
}
}
absl::StatusOr<InternalImageValueType> statusor;
{
base::AutoUnlock unlock(image_cache_lock_);
// TODO(b/253661187): nsenter the process' mount namespace for correctness.
statusor = VerifyStatAndGenerateImageHash(image_key, image_path_in_ns);
if (!statusor.ok()) {
LOG(ERROR) << statusor.status();
return image_cache_->end();
}
}
it = image_cache_->Put(image_key, std::move(*statusor));
return it;
}
std::vector<std::unique_ptr<pb::Process>> ProcessCache::GetProcessHierarchy(
uint64_t pid, bpf::time_ns_t start_time_ns, int num_generations) {
std::vector<std::unique_ptr<pb::Process>> processes;
InternalProcessKeyType lookup_key{LossyNsecToClockT(start_time_ns), pid};
base::AutoLock lock(process_cache_lock_);
for (int i = 0; i < num_generations; ++i) {
auto it = InclusiveGetProcess(lookup_key);
if (it != process_cache_->end()) {
auto process_proto = std::make_unique<pb::Process>();
process_proto->CopyFrom(*it->second.process_proto);
processes.push_back(std::move(process_proto));
lookup_key = it->second.parent_key;
} else {
// Process no longer exists or we've reached init. Break and best-effort
// return what we were able to retrieve.
break;
}
}
return processes;
}
absl::StatusOr<ProcessCache::InternalProcessValueType>
ProcessCache::MakeFromProcfs(const ProcessCache::InternalProcessKeyType& key) {
InternalProcessKeyType parent_key;
auto process_proto = std::make_unique<pb::Process>();
process_proto->set_canonical_pid(key.pid);
process_proto->set_process_uuid(StableUuid(key));
process_proto->set_rel_start_time_s(ClockTToSeconds(key.start_time_t));
const base::FilePath proc_pid_dir =
root_path_.Append(base::StringPrintf("proc/%" PRIu64, key.pid));
base::stat_wrapper_t pid_dir_stat;
if (base::File::Stat(proc_pid_dir.value().c_str(), &pid_dir_stat)) {
return absl::NotFoundError(
base::StrCat({kErrorFailedToStat, proc_pid_dir.value()}));
}
process_proto->set_canonical_uid(pid_dir_stat.st_uid);
const base::FilePath exe_symlink_path = proc_pid_dir.Append("exe");
base::FilePath exe_path;
if (base::ReadSymbolicLink(exe_symlink_path, &exe_path)) {
// TODO(b/253661187): nsenter the process' mount namespace for correctness.
base::stat_wrapper_t exe_stat;
if (base::File::Stat(exe_path.value().c_str(), &exe_stat)) {
return absl::NotFoundError(
base::StrCat({kErrorFailedToStat, exe_path.value()}));
}
auto image_proto = process_proto->mutable_image();
const base::FilePath mnt_ns_symlink_path =
proc_pid_dir.Append("ns").Append("mnt");
uint64_t mnt_ns;
auto status = GetNsFromPath(mnt_ns_symlink_path, &mnt_ns);
if (!status.ok()) {
return status;
}
image_proto->set_pathname(exe_path.value());
image_proto->set_mnt_ns(mnt_ns);
image_proto->set_inode_device_id(exe_stat.st_dev);
image_proto->set_inode(exe_stat.st_ino);
image_proto->set_canonical_uid(exe_stat.st_uid);
image_proto->set_canonical_gid(exe_stat.st_gid);
image_proto->set_mode(exe_stat.st_mode);
InternalImageKeyType image_key{
exe_stat.st_dev,
exe_stat.st_ino,
{exe_stat.st_mtim.tv_sec, exe_stat.st_mtim.tv_nsec},
{exe_stat.st_ctim.tv_sec, exe_stat.st_ctim.tv_nsec}};
{
base::AutoLock lock(image_cache_lock_);
auto it = InclusiveGetImage(image_key, exe_path);
if (it != image_cache_->end()) {
process_proto->mutable_image()->set_sha256(it->second.sha256);
}
}
} // Else we're likely processing a kthread and there's no image to report.
const base::FilePath cmdline_path = proc_pid_dir.Append("cmdline");
std::string cmdline_contents;
if (!base::ReadFileToString(cmdline_path, &cmdline_contents)) {
return absl::NotFoundError(
base::StrCat({kErrorFailedToRead, cmdline_path.value()}));
}
process_proto->set_commandline(
SafeTransformArgvEnvp(cmdline_contents.c_str(), cmdline_contents.size(),
cmdline_contents.size()));
// This must be the last file that we read for this process because process
// starttime is used as a key against pid reuse.
const base::FilePath stat_path = proc_pid_dir.Append("stat");
uint64_t procfs_start_time_t;
// mutable_commandline is already empty if this process is a kthread. So put
// in the comm instead.
auto status =
GetStatFromProcfs(stat_path, &parent_key.pid, &procfs_start_time_t,
process_proto->mutable_commandline());
if (!status.ok()) {
return status;
}
// TODO(b/254291026): Incoming ns is currently not derived using
// timens_add_boottime_ns.
if (key.start_time_t != procfs_start_time_t) {
return absl::AbortedError(
base::StringPrintf("Detected PID reuse on %" PRIu64
" (want time %" PRIu64 ", got time %" PRIu64 ")",
key.pid, key.start_time_t, procfs_start_time_t));
}
// parent_key.pid is filled in by this point but we also need start_time.
// parent_key.pid == 0 implies current process is init or a kthread. No need
// to traverse further.
if (parent_key.pid != 0) {
const base::FilePath parent_stat_path = root_path_.Append(
base::StringPrintf("proc/%" PRIu64 "/stat", parent_key.pid));
uint64_t unused_ppid;
std::string unused_comm;
status = GetStatFromProcfs(parent_stat_path, &unused_ppid,
&parent_key.start_time_t, &unused_comm);
if (!status.ok() || key.start_time_t < parent_key.start_time_t) {
LOG(WARNING) << "Failed to establish parent linkage for PID " << key.pid;
// Signifies end of our "linked list".
parent_key.pid = 0;
}
}
return InternalProcessValueType{std::move(process_proto), parent_key};
}
bool ProcessCache::IsEventFiltered(
const cros_xdr::reporting::XdrProcessEvent& event) {
const cros_xdr::reporting::Process* process = nullptr;
const cros_xdr::reporting::Process* parent_process = nullptr;
if (event.has_process_exec()) {
parent_process = event.process_exec().has_process()
? &event.process_exec().process()
: nullptr;
process = event.process_exec().has_spawn_process()
? &event.process_exec().spawn_process()
: nullptr;
} else if (event.has_process_terminate()) {
parent_process = event.process_terminate().has_parent_process()
? &event.process_terminate().parent_process()
: nullptr;
process = event.process_terminate().has_process()
? &event.process_terminate().process()
: nullptr;
}
const auto& parent_filter = filter_rules_parent_;
const auto& image_filter = filter_rules_process_;
const auto& should_filter = [](const cros_xdr::reporting::Process& p,
const InternalFilterRuleSetType& filters,
const std::string& type) -> bool {
const auto& matching_filter = filters.find(p.image().sha256());
if (matching_filter == filters.end()) {
return false;
}
if (matching_filter->second.commandline.empty()) {
// Commands match and there is no shell script to match.
return true;
}
for (const auto& commandline : matching_filter->second.commandline) {
if (p.commandline() == commandline) {
// Exact commandline match.
return true;
}
}
// Commands match but no matching shell script.
return false;
};
if (parent_process &&
should_filter(*parent_process, parent_filter, "parent_process")) {
return true;
}
if (process && should_filter(*process, image_filter, "process")) {
return true;
}
return false;
}
void ProcessCache::InitializeFilter(bool underscorify) {
// Image pathnames are adjusted by root_path_ for testing. Also they need
// to be underscorified for the unit test framework to function correctly.
// Since shell scripts just look at commandline they don't need to
// be underscorified or adjusted by root_path_ for testing.
std::vector<InternalFilterRule> parent_filter_seeds = {
// Shell rules
// TODO(b:267391331): make temp logger into a real application.
{
.image_pathname = "bin/sh",
.commandline =
{"'/bin/sh' '/usr/share/cros/init/temp_logger.sh'",
"'/bin/sh' '/usr/local/libexec/recover-duts/recover_duts'",
"'/bin/sh' "
"'/usr/local/libexec/recover-duts/hooks/check_ethernet.hook'"},
}};
std::vector<InternalFilterRule> process_filter_seeds = {
// Command rules
// TODO(b:267391049): We think this is being execve by some base library
// to determine how much space is left on the system. This spams the event
// logs so we add a filter. The base library should really be fixed.
{.image_pathname = "usr/sbin/spaced_cli"}};
std::vector<
std::pair<InternalFilterRuleSetType&, std::vector<InternalFilterRule>&>>
filter_seeds = {{filter_rules_parent_, parent_filter_seeds},
{filter_rules_process_, process_filter_seeds}};
for (auto& v : filter_seeds) {
for (auto& k : v.second) {
if (underscorify) {
std::replace(k.image_pathname.begin(), k.image_pathname.end(), '/',
'_');
}
k.image_pathname = root_path_.Append(k.image_pathname).value();
auto result = GenerateImageHash(base::FilePath(k.image_pathname));
if (!result.ok()) {
LOG(ERROR) << "XdrProcessEvent filter failed to create rule for "
<< "image_path_name:" << k.image_pathname
<< " error:" << result.status();
continue;
}
v.first.emplace(std::make_pair(result.value(), std::move(k)));
}
}
LOG(INFO) << "Process filter rules created:";
for (const auto& key : filter_rules_parent_) {
LOG(INFO) << "PARENT: SHA256:" << key.first
<< " pathname:" << key.second.image_pathname;
if (!key.second.commandline.empty())
LOG(INFO) << "Commands:";
for (auto commandline : key.second.commandline) {
LOG(INFO) << commandline;
}
}
for (const auto& key : filter_rules_process_) {
LOG(INFO) << "PROCESS: SHA256:" << key.first
<< " pathname:" << key.second.image_pathname;
if (!key.second.commandline.empty())
LOG(INFO) << "Commands:";
for (auto commandline : key.second.commandline) {
LOG(INFO) << commandline;
}
}
}
} // namespace secagentd