blob: 038c44c3856ef0e90d73b7b4d5ea286fcdaf3222 [file] [log] [blame] [edit]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "secanomalyd/daemon.h"
#include <sys/types.h>
#include <sysexits.h>
#include <memory>
#include <string>
#include <string_view>
#include <absl/strings/match.h>
#include <base/command_line.h>
#include <base/files/file_util.h>
#include <base/files/file_path.h>
#include <base/logging.h>
#include <base/rand_util.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/time/time.h>
#include <brillo/process/process.h>
#include <brillo/message_loops/message_loop.h>
#include "secanomalyd/audit_log_reader.h"
#include "secanomalyd/metrics.h"
#include "secanomalyd/mount_entry.h"
#include "secanomalyd/mounts.h"
#include "secanomalyd/processes.h"
#include "secanomalyd/reporter.h"
#include "secanomalyd/system_context.h"
namespace secanomalyd {
namespace {
// Sets the sampling frequency for W+X mount count uploads, such that the
// systems with more W+X mounts are more likely to send a crash report, in
// addition to limiting the total number of uploaded reports.
constexpr int CalculateSampleFrequency(size_t wx_mount_count) {
if (wx_mount_count <= 5)
return 15;
else if (wx_mount_count <= 10)
return 10;
else if (wx_mount_count <= 15)
return 5;
else
return 2;
}
constexpr int kProcAnomalySampleFrequency = 10000000; // 10 million
constexpr base::TimeDelta kScanInterval = base::Seconds(30);
// Used to limit the total number of UMA reports.
// Per Platform.DailyUseTime histogram this interval should ensure that enough
// users run the reporting.
constexpr base::TimeDelta kUmaReportInterval = base::Hours(2);
// Generates a unique name for the next element being added to `set`, where the
// element is a unique instance of a certain path type denoted by a `prefix`.
// For example, unknown executable paths are recorded as:
// {"unknown_executable_1", "unknown_executable_2", etc...}
std::string GetNextUniquePath(const FilePaths& set, const std::string& prefix) {
int num_common_elements = 0;
for (base::FilePath element : set) {
if (absl::StartsWith(element.value(), prefix))
num_common_elements++;
}
return prefix + "_" + std::to_string(num_common_elements);
}
bool EmitSeccompCoverageUma(const ProcEntries& proc_entries) {
size_t total_proc_count = proc_entries.size();
size_t seccomp_proc_count = 0;
seccomp_proc_count = std::count_if(
proc_entries.begin(), proc_entries.end(), [](const ProcEntry& entry) {
return entry.sandbox_status()[ProcEntry::kSecCompBit] == 1;
});
unsigned int seccomp_proc_percentage =
static_cast<unsigned int>(round((static_cast<float>(seccomp_proc_count) /
static_cast<float>(total_proc_count)) *
100));
VLOG(1) << "Reporting SecComp coverage UMA metric";
if (!SendSecCompCoverageToUMA(seccomp_proc_percentage)) {
LOG(WARNING) << "Could not upload SecComp coverage UMA metric";
return false;
}
return true;
}
bool EmitNnpProcPercentageUma(const ProcEntries& proc_entries) {
size_t total_proc_count = proc_entries.size();
size_t nnp_proc_count = 0;
nnp_proc_count = std::count_if(
proc_entries.begin(), proc_entries.end(), [](const ProcEntry& entry) {
return entry.sandbox_status()[ProcEntry::kNoNewPrivsBit] == 1;
});
unsigned int nnp_proc_percentage =
static_cast<unsigned int>(round((static_cast<float>(nnp_proc_count) /
static_cast<float>(total_proc_count)) *
100));
VLOG(1) << "Reporting no_new_privs process percentage UMA metric";
if (!SendNnpProcPercentageToUMA(nnp_proc_percentage)) {
LOG(WARNING)
<< "Could not upload no_new_privs process percentage UMA metric";
return false;
}
return true;
}
bool EmitNonRootProcPercentageUma(const ProcEntries& proc_entries) {
size_t total_proc_count = proc_entries.size();
size_t nonroot_proc_count = 0;
nonroot_proc_count = std::count_if(
proc_entries.begin(), proc_entries.end(), [](const ProcEntry& entry) {
return entry.sandbox_status()[ProcEntry::kNonRootBit] == 1;
});
unsigned int nonroot_proc_percentage =
static_cast<unsigned int>(round((static_cast<float>(nonroot_proc_count) /
static_cast<float>(total_proc_count)) *
100));
VLOG(1) << "Reporting non-root process percentage UMA metric";
if (!SendNonRootProcPercentageToUMA(nonroot_proc_percentage)) {
LOG(WARNING) << "Could not upload non-root process percentage UMA metric";
return false;
}
return true;
}
bool EmitUnprivProcPercentageUma(const ProcEntries& proc_entries,
ino_t init_user_ns) {
size_t total_proc_count = proc_entries.size();
size_t unpriv_proc_count = 0;
unpriv_proc_count = std::count_if(
proc_entries.begin(), proc_entries.end(), [&](const ProcEntry& entry) {
return entry.sandbox_status()[ProcEntry::kNonRootBit] == 1 &&
(entry.sandbox_status()[ProcEntry::kNoCapSysAdminBit] == 1 ||
entry.userns() != init_user_ns);
});
unsigned int unpriv_proc_percentage =
static_cast<unsigned int>(round((static_cast<float>(unpriv_proc_count) /
static_cast<float>(total_proc_count)) *
100));
VLOG(1) << "Reporting unpriv process percentage UMA metric";
if (!SendUnprivProcPercentageToUMA(unpriv_proc_percentage)) {
LOG(WARNING) << "Could not upload unpriv process percentage UMA metric";
return false;
}
return true;
}
bool EmitNonInitNsProcPercentageUma(const ProcEntries& proc_entries,
ino_t init_pid_ns,
ino_t init_mnt_ns) {
size_t total_proc_count = proc_entries.size();
size_t non_initns_proc_count = 0;
non_initns_proc_count = std::count_if(
proc_entries.begin(), proc_entries.end(), [&](const ProcEntry& entry) {
return entry.pidns() != init_pid_ns && entry.mntns() != init_mnt_ns;
});
unsigned int non_initns_proc_percentage = static_cast<unsigned int>(
round((static_cast<float>(non_initns_proc_count) /
static_cast<float>(total_proc_count)) *
100));
VLOG(1) << "Reporting non-init namespace process percentage UMA metric";
if (!SendNonInitNsProcPercentageToUMA(non_initns_proc_percentage)) {
LOG(WARNING)
<< "Could not upload non-init namespace process percentage UMA metric";
return false;
}
return true;
}
} // namespace
int Daemon::OnInit() {
// DBusDaemon::OnInit() initializes the D-Bus connection, making sure |bus_|
// is populated.
int ret = brillo::DBusDaemon::OnInit();
if (ret != EX_OK) {
return ret;
}
// Initializes the audit log reader for accessing the audit log file.
InitAuditLogReader();
session_manager_proxy_ = std::make_unique<SessionManagerProxy>(bus_);
// The raw SessionManagerProxy pointer is un-owned by the SystemContext
// object.
system_context_ =
std::make_unique<SystemContext>(session_manager_proxy_.get());
return EX_OK;
}
int Daemon::OnEventLoopStarted() {
ScanForAnomalies();
ReportUmaMetrics();
return EX_OK;
}
void Daemon::ScanForAnomalies() {
VLOG(1) << "Scanning for W+X mounts";
DoWXMountScan();
VLOG(1) << "Scanning system processes";
DoProcScan();
VLOG(1) << "Scanning for audit log anomalies";
DoAuditLogScan();
if (generate_reports_) {
DoAnomalousSystemReporting();
}
brillo::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&Daemon::ScanForAnomalies, base::Unretained(this)),
kScanInterval);
}
void Daemon::ReportUmaMetrics() {
if (!ShouldReport(dev_)) {
return;
}
EmitWXMountCountUma();
EmitForbiddenIntersectionProcCountUma();
EmitMemfdExecProcCountUma();
EmitSandboxingUma();
brillo::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&Daemon::ReportUmaMetrics, base::Unretained(this)),
kUmaReportInterval);
}
void Daemon::DoWXMountScan() {
all_mounts_ = ReadMounts();
if (!all_mounts_) {
LOG(ERROR) << "Failed to read mounts";
return;
}
// Refreshed on every check to have the most up-to-date state.
system_context_->Refresh();
for (const auto& e : all_mounts_.value()) {
if (e.IsWX()) {
// Have we seen the mount yet?
if (wx_mounts_.count(e.dest()) == 0) {
if (e.IsUsbDriveOrArchive()) {
// Figure out what to log in this case.
// We could log the fact that the mount exists without logging
// |src| or |dest|.
continue;
}
if (e.IsNamespaceBindMount() || e.IsKnownMount(*system_context_)) {
// Namespace mounts happen when a namespace file in /proc/<pid>/ns/
// gets bind-mounted somewhere else. These mounts can be W+X but are
// not concerning since they consist of a single file and these files
// cannot be executed.
// There are other W+X mounts that are low-risk (e.g. non-persistent
// mounts) and that we're in the process of fixing. These are
// considered "known" W+X mounts and are also skipped.
VLOG(1) << "Not recording W+X mount at '" << e.dest() << "', type "
<< e.type();
// In case of a known mount, we need to update the context to remember
// that this mount was observed, as we might use this information to
// determine whether it should be ignored again in the future scans.
system_context_->RecordKnownMountObservation(e.dest());
continue;
}
// We haven't seen the mount, and it's not a type we want to skip, so
// save it.
wx_mounts_[e.dest()] = e;
VLOG(1) << "Found W+X mount at '" << e.dest() << "', type " << e.type();
VLOG(1) << "|wx_mounts_.size()| = " << wx_mounts_.size();
// Report metrics on the mount, if not running in dev mode.
if (ShouldReport(dev_)) {
// Report /usr/local mounts separately because those can indicate
// systems where |cros_debug == 0| but the system is still a dev
// system.
SecurityAnomaly mount_anomaly =
e.IsDestInUsrLocal()
? SecurityAnomaly::kMount_InitNs_WxInUsrLocal
: SecurityAnomaly::kMount_InitNs_WxNotInUsrLocal;
if (!SendSecurityAnomalyToUMA(mount_anomaly)) {
LOG(WARNING) << "Could not upload metrics";
}
}
}
}
}
}
void Daemon::DoProcScan() {
all_procs_ = ReadProcesses(ProcessFilter::kAll);
if (!all_procs_) {
return;
}
if (!init_proc_) {
init_proc_ = GetInitProcEntry(all_procs_.value());
}
if (!init_proc_) {
return;
}
ProcEntries procs;
FilterKernelProcesses(all_procs_.value(), procs);
ProcEntries flagged_procs;
std::copy_if(procs.begin(), procs.end(), std::back_inserter(flagged_procs),
[&](const ProcEntry& e) {
return IsProcInForbiddenIntersection(e, init_proc_.value());
});
forbidden_intersection_procs_ = MaybeProcEntries(flagged_procs);
if (forbidden_intersection_procs_) {
VLOG(1) << "|forbidden_intersection_procs_.size()| = "
<< forbidden_intersection_procs_->size();
}
}
void Daemon::DoAnomalousSystemReporting() {
// Skip reporting if for all anomaly types either the daemon has previously
// attempted to send a report or the anomaly does not exist.
if ((has_attempted_wx_mount_report_ || wx_mounts_.empty()) &&
(has_attempted_forbidden_intersection_report_ ||
forbidden_intersection_procs_.value().empty()) &&
(has_attempted_memfd_exec_report_ ||
executables_attempting_memfd_exec_.empty())) {
return;
}
// Makes checking for this anomaly type easier.
ProcEntries anomalous_procs = forbidden_intersection_procs_
? forbidden_intersection_procs_.value()
: ProcEntries();
// Stop subsequent reporting attempts for each discovered anomaly type.
if (!wx_mounts_.empty()) {
has_attempted_wx_mount_report_ = true;
}
if (!anomalous_procs.empty()) {
has_attempted_forbidden_intersection_report_ = true;
}
if (!executables_attempting_memfd_exec_.empty()) {
has_attempted_memfd_exec_report_ = true;
}
if (!ShouldReport(dev_)) {
VLOG(1) << "Not reporting anomalous system due to dev mode";
return;
}
VLOG(1) << "Attempting to report anomalous system";
int range = 0;
int weight = 1;
// If |dev_| is set or there are memfd execution attempts, always send the
// report (memfd execution attempts are exceedingly rare so we can afford to
// upload them all). Otherwise, if W+X anomalies exist, send one in every
// |CalculateSampleFrequency(wx_mounts_.size())| reports. Finally, if only
// forbidden intersection violations exist, send one in every
// |kProcAnomalySampleFrequency| reports.
if (dev_ || !executables_attempting_memfd_exec_.empty()) {
range = 1;
} else if (!wx_mounts_.empty()) {
range = CalculateSampleFrequency(wx_mounts_.size());
weight = range;
} else if (!anomalous_procs.empty() && forbidden_intersection_only_reports_) {
range = kProcAnomalySampleFrequency;
}
// |base::RandInt(min, max)| returns a random int between [min, max], which in
// this case gives the report one in |range| chance of being sent.
if (range < 1 || base::RandInt(1, range) > 1) {
return;
}
bool success = ReportAnomalousSystem(wx_mounts_, anomalous_procs,
executables_attempting_memfd_exec_,
all_mounts_, all_procs_, weight, dev_);
if (!success) {
// Reporting is best-effort so on failure we just print a warning.
LOG(WARNING) << "Failed to report anomalous system";
}
// Report whether uploading the anomalous system report succeeded.
if (!SendAnomalyUploadResultToUMA(success)) {
LOG(WARNING) << "Could not upload metrics";
}
}
void Daemon::InitAuditLogReader() {
audit_log_reader_ = std::make_unique<AuditLogReader>(kAuditLogPath);
}
void Daemon::DoAuditLogScan() {
if (!audit_log_reader_)
return;
std::string log_message;
LogRecord log_record;
while (audit_log_reader_->GetNextEntry(&log_record)) {
// This detects a successful memfd_create syscall and reports it to UMA to
// be used as the baseline metric for memfd execution attempts. The check
// will not be performed again, once the metric is successfully emitted.
if (!has_emitted_memfd_baseline_uma_ &&
log_record.tag == kSyscallRecordTag &&
secanomalyd::IsMemfdCreate(log_record.message)) {
// Report baseline condition to UMA if not in dev mode.
if (ShouldReport(dev_)) {
if (!SendSecurityAnomalyToUMA(
SecurityAnomaly::kSuccessfulMemfdCreateSyscall)) {
LOG(WARNING) << "Could not upload metrics";
} else {
has_emitted_memfd_baseline_uma_ = true;
}
}
}
std::string exe_path;
if (log_record.tag == kAVCRecordTag &&
secanomalyd::IsMemfdExecutionAttempt(log_record.message, exe_path)) {
if (exe_path == secanomalyd::kUnknownExePath) {
exe_path = GetNextUniquePath(executables_attempting_memfd_exec_,
secanomalyd::kUnknownExePath);
}
// Record the anomaly by adding the offending executable path to
// |executables_attempting_memfd_exec_| set.
executables_attempting_memfd_exec_.insert(base::FilePath(exe_path));
VLOG(1) << log_record.message;
VLOG(1) << "|executables_attempting_memfd_exec_.size()| = "
<< executables_attempting_memfd_exec_.size();
// Report anomalous condition to UMA if not in dev mode.
if (ShouldReport(dev_)) {
if (!SendSecurityAnomalyToUMA(
SecurityAnomaly::kBlockedMemoryFileExecAttempt))
LOG(WARNING) << "Could not upload metrics";
}
}
}
}
void Daemon::EmitWXMountCountUma() {
VLOG(1) << "Reporting W+X mount count UMA metric";
if (SendWXMountCountToUMA(wx_mounts_.size())) {
// After successfully reporting W+X mount count, clear the map.
// If mounts still exist they'll be re-added on the next scan.
wx_mounts_.clear();
} else {
LOG(WARNING) << "Could not upload W+X mount count UMA metric";
}
}
void Daemon::EmitForbiddenIntersectionProcCountUma() {
// Skip if already emitted or |forbidden_intersection_procs_| has not yet been
// populated.
if (has_emitted_forbidden_intersection_uma_ ||
!forbidden_intersection_procs_) {
return;
}
// Only report forbidden intersection process count in the logged-in state.
system_context_->Refresh(/*skip_known_mount_refresh=*/true);
if (!system_context_->IsUserLoggedIn()) {
return;
}
VLOG(1) << "Reporting forbidden intersection process count UMA metric";
if (!SendForbiddenIntersectionProcCountToUMA(
forbidden_intersection_procs_->size())) {
LOG(WARNING)
<< "Could not upload forbidden intersection process count UMA metric";
}
}
void Daemon::EmitMemfdExecProcCountUma() {
VLOG(1) << "Reporting memfd exec process count UMA metric";
if (SendAttemptedMemfdExecProcCountToUMA(
executables_attempting_memfd_exec_.size())) {
// After successfully reporting process count, clear the set. If the same
// processes attempt memfd executions again, they will be re-added to the
// set.
executables_attempting_memfd_exec_.clear();
} else {
LOG(WARNING) << "Could not upload memfd exec process count UMA metric";
}
}
void Daemon::EmitSandboxingUma() {
if (!has_emitted_landlock_status_uma_) {
VLOG(1) << "Reporting Landlock status UMA metric";
// If landlock is in any other state than enabled, such as not supported or
// an unknown state, we consider it disabled.
if (!SendLandlockStatusToUMA(system_context_->GetLandlockState() ==
LandlockState::kEnabled)) {
LOG(WARNING) << "Could not upload Landlock status UMA metric";
} else {
has_emitted_landlock_status_uma_ = true;
}
}
// Refresh the login state.
system_context_->Refresh(/*skip_known_mount_refresh=*/true);
if ((!has_emitted_seccomp_coverage_uma_ ||
!has_emitted_nonroot_proc_percentage_uma_ ||
!has_emitted_unpriv_proc_percentage_uma_) &&
system_context_->IsUserLoggedIn()) {
MaybeProcEntries maybe_proc_entries =
ReadProcesses(ProcessFilter::kNoKernelTasks);
if (!maybe_proc_entries.has_value() ||
maybe_proc_entries.value().size() == 0) {
return;
}
if (!has_emitted_seccomp_coverage_uma_) {
has_emitted_seccomp_coverage_uma_ =
EmitSeccompCoverageUma(maybe_proc_entries.value());
}
if (!has_emitted_nnp_proc_percentage_uma_) {
has_emitted_nnp_proc_percentage_uma_ =
EmitNnpProcPercentageUma(maybe_proc_entries.value());
}
if (!has_emitted_nonroot_proc_percentage_uma_) {
has_emitted_nonroot_proc_percentage_uma_ =
EmitNonRootProcPercentageUma(maybe_proc_entries.value());
}
// For the rest of the metrics, we need to have the init process entry.
if (!init_proc_) {
return;
}
if (!has_emitted_unpriv_proc_percentage_uma_) {
has_emitted_unpriv_proc_percentage_uma_ = EmitUnprivProcPercentageUma(
maybe_proc_entries.value(), init_proc_.value().userns());
}
if (!has_emitted_non_initns_proc_percentage_uma_) {
has_emitted_non_initns_proc_percentage_uma_ =
EmitNonInitNsProcPercentageUma(maybe_proc_entries.value(),
init_proc_.value().pidns(),
init_proc_.value().mntns());
}
}
}
} // namespace secanomalyd