blob: 4ea9350000e8e6391397059dc35519960e92df58 [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 <sysexits.h>
#include <memory>
#include <string>
#include <vector>
#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_piece.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/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 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 std::set<base::FilePath>& 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);
}
} // 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 for audit log anomalies";
DoAuditLogScan();
brillo::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&Daemon::ScanForAnomalies, base::Unretained(this)),
kScanInterval);
}
void Daemon::ReportUmaMetrics() {
if (!ShouldReport(dev_)) {
return;
}
EmitWXMountCountUma();
EmitMemfdExecProcCountUma();
EmitLandlockStatusUma();
brillo::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&Daemon::ReportUmaMetrics, base::Unretained(this)),
kUmaReportInterval);
}
void Daemon::DoWXMountScan() {
MaybeMountEntries mount_entries = ReadMounts(MountFilter::kAll);
if (!mount_entries) {
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 : mount_entries.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";
}
}
}
}
}
// Report the system as anomalous if the daemon has never attempted to send a
// report and the W+X mount count is non-zero.
if (generate_reports_ && !has_attempted_wx_mount_report_ &&
wx_mounts_.size() > 0) {
// Stop subsequent reporting attempts for this execution.
has_attempted_wx_mount_report_ = true;
DoAnomalousSystemReporting();
}
}
void Daemon::DoAnomalousSystemReporting() {
VLOG(1) << "Reporting anomalous system: W+X mount count";
if (ShouldReport(dev_)) {
// Send one out of every |kSampleFrequency| reports, unless |dev_| is set.
// |base::RandInt()| returns a random int in [min, max].
int range = dev_ ? 1 : CalculateSampleFrequency(wx_mounts_.size());
if (base::RandInt(1, range) > 1) {
return;
}
bool success = ReportAnomalousSystem(wx_mounts_, range, 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";
}
}
}
// TODO(b/255818130): Add a function for reporting the details of any
// discovered memfd execution events through crash reporter and invoke it
// here.
}
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::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::EmitLandlockStatusUma() {
if (has_emitted_landlock_status_uma_) {
return;
}
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;
}
}
} // namespace secanomalyd