| // Copyright 2021 The Chromium OS Authors. All rights reserved. |
| // 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 <vector> |
| |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/logging.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/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. |
| return ::VbGetSystemPropertyInt("cros_debug") == 0 || report_in_dev_mode; |
| } |
| |
| std::string GenerateSignature(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; |
| } |
| |
| MaybeReport GenerateAnomalousSystemReport(const MountEntryMap& wx_mounts, |
| const MaybeMountEntries& all_mounts, |
| const MaybeProcEntries& all_procs) { |
| // First line: signature |
| // Second line: metadata |
| // signals: wx-mount|root-proc|non-vb-proc |
| // dest: /usr/local, e.g. |
| // Third+ line: content |
| |
| // We need at least one anomalous condition to generate the report signature. |
| if (wx_mounts.empty()) { |
| return std::nullopt; |
| } |
| |
| std::vector<std::string> lines; |
| |
| // Generate signature. |
| lines.emplace_back(GenerateSignature(wx_mounts)); |
| |
| // Generate metadata. |
| base::FilePath dest = wx_mounts.begin()->first; |
| std::string 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' |
| // Right now we only support listing which anomalies triggered the upload of |
| // this report, and signalling which specific anomaly was used for the |
| // signature. |
| base::StrAppend(&metadata, |
| {"signals\x01wx-mount", "\x02", "dest\x01", dest.value()}); |
| lines.emplace_back(metadata); |
| |
| // List anomalous conditions. |
| lines.emplace_back("=== Anomalous conditions ==="); |
| for (const auto& tuple : wx_mounts) { |
| lines.push_back(tuple.second.FullDescription()); |
| } |
| |
| lines.emplace_back("=== 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"); |
| } |
| |
| lines.emplace_back("=== 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(base::StringPiece 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, |
| int weight, |
| bool report_in_dev_mode) { |
| MaybeMountEntries maybe_mounts = ReadMounts(MountFilter::kUploadableOnly); |
| MaybeProcEntries maybe_procs = |
| ReadProcesses(ProcessFilter::kInitPidNamespaceOnly); |
| |
| MaybeReport anomaly_report = |
| GenerateAnomalousSystemReport(wx_mounts, maybe_mounts, maybe_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 |