blob: 94fce3315465c3105cdc806d0fa087c9f314bbcc [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/reporter.h"
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/logging.h>
#include <base/numerics/safe_conversions.h>
#include <base/rand_util.h>
#include <base/strings/strcat.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <brillo/process/process.h>
#include <crypto/sha2.h>
#include <vboot/crossystem.h>
#include "secanomalyd/mount_entry.h"
#include "secanomalyd/mounts.h"
#include "secanomalyd/processes.h"
namespace secanomalyd {
namespace {
constexpr size_t kHashPrefixLengthInBytes = 5u;
constexpr char kRootPathReplacement[] = "slashroot";
constexpr char kCrashReporterPath[] = "/sbin/crash_reporter";
constexpr char kSecurityAnomalyFlag[] = "--security_anomaly";
constexpr char kWeightFlag[] = "--weight";
} // namespace
bool ShouldReport(bool report_in_dev_mode) {
// Reporting should only happen when booted in Verified mode and not running
// a developer image, unless explicitly instructed otherwise.
static bool is_verified_mode = ::VbGetSystemPropertyInt("cros_debug") == 0;
return is_verified_mode || report_in_dev_mode;
}
std::string GenerateMountSignature(const MountEntryMap& wx_mounts) {
std::vector<std::string> dests;
for (const auto& p : wx_mounts) {
dests.emplace_back(p.first.value());
}
std::string signature;
// Use the first path as a visible sentinel for the signature.
// If the anomalous mount is on '/', replace the destination path with a
// default value so that the signature doesn't have consecutive dashes.
if (dests[0] != "/") {
base::ReplaceChars(dests[0], "/", "-", &signature);
} else {
signature = kRootPathReplacement;
}
// Hash the string resulting from joining all mount destinations separated
// by newlines. Take the first five bytes and use that to complete the
// signature.
std::vector<uint8_t> prefix(kHashPrefixLengthInBytes);
crypto::SHA256HashString(base::JoinString(dests, "\n"), prefix.data(),
prefix.size());
base::StrAppend(&signature, {"-", base::HexEncode(prefix)});
return signature;
}
std::optional<std::string> GenerateProcSignature(const ProcEntries& procs) {
if (procs.empty()) {
return std::nullopt;
}
std::string signature;
int signature_proc = base::RandInt(0, procs.size() - 1);
signature = procs[base::checked_cast<size_t>(signature_proc)].comm();
return std::optional<std::string>(signature);
}
std::optional<std::string> GeneratePathSignature(const FilePaths& paths) {
if (paths.empty()) {
return std::nullopt;
}
base::FilePath signature_path = *paths.begin();
return std::optional<std::string>(signature_path.value());
}
MaybeReport GenerateAnomalousSystemReport(
const MountEntryMap& wx_mounts,
const ProcEntries& forbidden_intersection_procs,
const FilePaths& executables_attempting_memfd_exec,
const MaybeMountEntries& all_mounts,
const MaybeProcEntries& all_procs) {
// First line: signature
// Second line: metadata
// signals: wx-mount | forbidden-intersection-violation |
// memfd-exec-attempt | multiple-anomalies
// dest: /usr/local, e.g.
// Third+ line: content
std::vector<std::string> lines;
// Generate signature based on the anomaly type. If multiple anomaly types are
// present, the order of preference for signature generation is memfd exec
// attempt, then W+X mount, then forbidden intersection process. At least one
// anomaly has to be preset to proceed.
std::optional<std::string> signature;
if (!executables_attempting_memfd_exec.empty()) {
signature = GeneratePathSignature(executables_attempting_memfd_exec);
} else if (!wx_mounts.empty()) {
signature = GenerateMountSignature(wx_mounts);
} else if (!forbidden_intersection_procs.empty()) {
signature = GenerateProcSignature(forbidden_intersection_procs);
} else {
return std::nullopt;
}
if (!signature) {
return std::nullopt;
}
lines.emplace_back(signature.value());
// Generate metadata.
// Metadata are a set of key-value pairs where keys and values are separated
// by \x01 and pairs are separated by \x02:
// 'signals\x01wx-mount\x02dest\x01/usr/local'
//
// Signal which anomaly type triggered the report generation, or whether
// the report was generated due to multiple anomalies.
std::string metadata = "signals\x01";
if (wx_mounts.empty() && executables_attempting_memfd_exec.empty()) {
base::StrAppend(&metadata, {"forbidden-intersection-violation"});
} else if (executables_attempting_memfd_exec.empty() &&
forbidden_intersection_procs.empty()) {
base::StrAppend(&metadata, {"wx-mount"});
} else if (forbidden_intersection_procs.empty() && wx_mounts.empty()) {
base::StrAppend(&metadata, {"memfd-exec-attempt"});
} else {
base::StrAppend(&metadata, {"multiple-anomalies"});
}
// Indicate the specific anomaly used for the signature generation.
base::StrAppend(&metadata, {"\x02"});
if (!executables_attempting_memfd_exec.empty()) {
base::StrAppend(&metadata, {"executable", "\x01", signature.value()});
} else if (!wx_mounts.empty()) {
base::FilePath dest = wx_mounts.begin()->first;
base::StrAppend(&metadata, {"dest"
"\x01",
dest.value()});
} else if (!forbidden_intersection_procs.empty()) {
base::StrAppend(&metadata, {"comm", "\x01", signature.value()});
}
lines.emplace_back(metadata);
// List anomalous conditions.
lines.emplace_back("=== Anomalous conditions ===");
if (!wx_mounts.empty()) {
lines.emplace_back("=== W+X mounts ===");
for (const auto& tuple : wx_mounts) {
lines.push_back(tuple.second.FullDescription());
}
}
if (!forbidden_intersection_procs.empty()) {
lines.emplace_back("=== Forbidden intersection processes ===");
for (const auto& e : forbidden_intersection_procs) {
lines.push_back(e.comm() + " " + e.args());
}
}
if (!executables_attempting_memfd_exec.empty()) {
lines.emplace_back("=== Executables attempting memfd exec ===");
for (const auto& e : executables_attempting_memfd_exec) {
lines.push_back(e.value());
}
}
// Include the list of all mounts.
lines.emplace_back("=== All mounts ===");
if (all_mounts) {
// List mounts.
for (const auto& mount_entry : all_mounts.value()) {
lines.push_back(mount_entry.FullDescription());
}
} else {
lines.emplace_back("Could not obtain mounts");
}
// Include the list of all processes.
lines.emplace_back("=== All processes ===");
if (all_procs) {
// List processes.
for (const auto& proc_entry : all_procs.value()) {
lines.emplace_back(proc_entry.args());
}
} else {
lines.emplace_back("Could not obtain processes");
}
// Ensure reports have a trailing newline. Trailing newlines make reports
// easier to read in a terminal.
lines.emplace_back("");
return MaybeReport(base::JoinString(lines, "\n"));
}
bool SendReport(std::string_view report,
brillo::Process* crash_reporter,
int weight,
bool report_in_dev_mode) {
if (!ShouldReport(report_in_dev_mode)) {
VLOG(1) << "Not in Verified mode, not reporting";
return true;
}
VLOG(1) << "secanomalyd invoking crash_reporter";
crash_reporter->AddArg(kCrashReporterPath);
crash_reporter->AddArg(kSecurityAnomalyFlag);
crash_reporter->AddArg(base::StringPrintf("%s=%d", kWeightFlag, weight));
crash_reporter->RedirectUsingPipe(STDIN_FILENO, true /*is_input*/);
if (!crash_reporter->Start()) {
LOG(ERROR) << "Failed to start crash reporting process";
return false;
}
int stdin_fd = crash_reporter->GetPipe(STDIN_FILENO);
if (stdin_fd < 0) {
LOG(ERROR) << "Failed to get stdin pipe for crash reporting process";
return false;
}
{
base::ScopedFD stdin(stdin_fd);
if (!base::WriteFileDescriptor(stdin_fd, report)) {
LOG(ERROR) << "Failed to write report to crash reporting process' stdin";
return false;
}
}
// |crash_reporter| returns 0 on success.
return crash_reporter->Wait() == 0;
}
bool ReportAnomalousSystem(const MountEntryMap& wx_mounts,
const ProcEntries& forbidden_intersection_procs,
const FilePaths& executables_attempting_memfd_exec,
const MaybeMountEntries& all_mounts,
const MaybeProcEntries& all_procs,
int weight,
bool report_in_dev_mode) {
// Filter out private mounts before upload.
MaybeMountEntries uploadable_mounts = FilterPrivateMounts(all_mounts);
// Filter out processes not in the init PID namespace.
ProcEntries init_pidns_procs;
if (all_procs) {
FilterNonInitPidNsProcesses(all_procs.value(), init_pidns_procs);
}
MaybeReport anomaly_report = GenerateAnomalousSystemReport(
wx_mounts, forbidden_intersection_procs,
executables_attempting_memfd_exec, uploadable_mounts,
MaybeProcEntries(init_pidns_procs));
if (!anomaly_report) {
LOG(ERROR) << "Failed to generate anomalous system report";
return false;
}
brillo::ProcessImpl crash_reporter;
if (!SendReport(anomaly_report.value(), &crash_reporter, weight,
report_in_dev_mode)) {
LOG(ERROR) << "Failed to send anomalous system report";
return false;
}
return true;
}
} // namespace secanomalyd