chromium/sandbox/win/src/sandbox_policy_diagnostic.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "sandbox/win/src/sandbox_policy_diagnostic.h"

#include <windows.h>

#include <stddef.h>

#include <cinttypes>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/json/json_string_value_serializer.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "sandbox/win/src/ipc_tags.h"
#include "sandbox/win/src/policy_engine_opcodes.h"
#include "sandbox/win/src/sandbox_policy_base.h"
#include "sandbox/win/src/target_process.h"
#include "sandbox/win/src/top_level_dispatcher.h"
#include "sandbox/win/src/win_utils.h"

namespace sandbox {

namespace {

// Keys in base::Value snapshots of Policies for chrome://sandbox.
const char kAppContainerCapabilities[] = "appContainerCapabilities";
const char kAppContainerInitialCapabilities[] =
    "appContainerInitialCapabilities";
const char kAppContainerSid[] = "appContainerSid";
const char kComponentFilters[] = "componentFilters";
const char kDesiredIntegrityLevel[] = "desiredIntegrityLevel";
const char kDesiredMitigations[] = "desiredMitigations";
const char kDisconnectCsrss[] = "disconnectCsrss";
const char kHandlesToClose[] = "handlesToClose";
const char kJobLevel[] = "jobLevel";
const char kLockdownLevel[] = "lockdownLevel";
const char kLowboxSid[] = "lowboxSid";
const char kPlatformMitigations[] = "platformMitigations";
const char kPolicyRules[] = "policyRules";
const char kProcessId[] = "processId";
const char kTag[] = "tag";
const char kZeroAppShim[] = "zeroAppShim";

// Closable handles.
const char kALPCPort[] = "ALPC Port";
const char kFileDeviceApi[] = "\\Device\\DeviceApi";
const char kFileKsecDD[] = "\\Device\\KsecDD";
const char kWindowsShellGlobalCounters[] = "*\\windows_shell_global_counters";

// Values in snapshots of Policies.
const char kDisabled[] = "disabled";
const char kEnabled[] = "enabled";

std::string GetTokenLevelInEnglish(TokenLevel token) {
  switch (token) {
    case USER_LOCKDOWN:
      return "Lockdown";
    case USER_LIMITED:
      return "Limited";
    case USER_INTERACTIVE:
      return "Interactive";
    case USER_RESTRICTED_SAME_ACCESS:
      return "Restricted Same Access";
    case USER_UNPROTECTED:
      return "None";
    case USER_RESTRICTED_NON_ADMIN:
      return "Restricted Non Admin";
    case USER_LAST:
      DCHECK(false) << "Unknown TokenType";
      return "Unknown";
  }
}

std::string GetJobLevelInEnglish(JobLevel job) {
  switch (job) {
    case JobLevel::kLockdown:
      return "Lockdown";
    case JobLevel::kLimitedUser:
      return "Limited User";
    case JobLevel::kInteractive:
      return "Interactive";
    case JobLevel::kUnprotected:
      return "Unprotected";
  }
}

std::string GetIntegrityLevelInEnglish(IntegrityLevel integrity) {
  switch (integrity) {
    case INTEGRITY_LEVEL_SYSTEM:
      return "S-1-16-16384 System";
    case INTEGRITY_LEVEL_HIGH:
      return "S-1-16-12288 High";
    case INTEGRITY_LEVEL_MEDIUM:
      return "S-1-16-8192 Medium";
    case INTEGRITY_LEVEL_MEDIUM_LOW:
      return "S-1-16-6144 Medium Low";
    case INTEGRITY_LEVEL_LOW:
      return "S-1-16-4096 Low";
    case INTEGRITY_LEVEL_BELOW_LOW:
      return "S-1-16-2048 Below Low";
    case INTEGRITY_LEVEL_UNTRUSTED:
      return "S-1-16-0 Untrusted";
    case INTEGRITY_LEVEL_LAST:
      return "Default";
  }
}

std::wstring GetSidAsString(const base::win::Sid& sid) {
  std::optional<std::wstring> result = sid.ToSddlString();
  if (!result) {
    DCHECK(false) << "Failed to make sddl string";
    return L"";
  }
  return *result;
}

std::string GetMitigationsAsHex(MitigationFlags mitigations) {
  return base::StringPrintf("%016" PRIx64,
                            base::checked_cast<uint64_t>(mitigations));
}

std::string GetPlatformMitigationsAsHex(MitigationFlags mitigations) {
  DWORD64 platform_flags[2] = {0};
  size_t flags_size = 0;
  sandbox::ConvertProcessMitigationsToPolicy(mitigations, &(platform_flags[0]),
                                             &flags_size);
  DCHECK(flags_size / sizeof(DWORD64) <= 2)
      << "Unexpected mitigation flags size";
  if (flags_size == 2 * sizeof(DWORD64))
    return base::StringPrintf("%016" PRIx64 "%016" PRIx64, platform_flags[0],
                              platform_flags[1]);
  return base::StringPrintf("%016" PRIx64, platform_flags[0]);
}

std::string GetComponentFilterAsHex(MitigationFlags mitigations) {
  COMPONENT_FILTER filter;
  sandbox::ConvertProcessMitigationsToComponentFilter(mitigations, &filter);
  return base::StringPrintf("%08lx", filter.ComponentFlags);
}

std::string GetIpcTagAsString(IpcTag service) {
  switch (service) {
    case IpcTag::UNUSED:
      DCHECK(false) << "Unused IpcTag";
      return "Unused";
    case IpcTag::PING1:
      return "Ping1";
    case IpcTag::PING2:
      return "Ping2";
    case IpcTag::NTCREATEFILE:
      return "NtCreateFile";
    case IpcTag::NTOPENFILE:
      return "NtOpenFile";
    case IpcTag::NTQUERYATTRIBUTESFILE:
      return "NtQueryAttributesFile";
    case IpcTag::NTQUERYFULLATTRIBUTESFILE:
      return "NtQueryFullAttributesFile";
    case IpcTag::NTSETINFO_RENAME:
      return "NtSetInfoRename";
    case IpcTag::NTOPENTHREAD:
      return "NtOpenThread";
    case IpcTag::NTOPENPROCESSTOKENEX:
      return "NtOpenProcessTokenEx";
    case IpcTag::GDI_GDIDLLINITIALIZE:
      return "GdiDllInitialize";
    case IpcTag::GDI_GETSTOCKOBJECT:
      return "GetStockObject";
    case IpcTag::USER_REGISTERCLASSW:
      return "RegisterClassW";
    case IpcTag::CREATETHREAD:
      return "CreateThread";
    case IpcTag::NTCREATESECTION:
      return "NtCreateSection";
  }
}

std::string GetOpcodeAction(EvalResult action) {
  switch (action) {
    case EVAL_TRUE:
      return "true";
    case EVAL_FALSE:
      return "false";
    case EVAL_ERROR:
      return "error";
    case ASK_BROKER:
      return "askBroker";
    case DENY_ACCESS:
      return "deny";
    case SIGNAL_ALARM:
      return "alarm";
    case FAKE_SUCCESS:
      return "fakeSuccess";
    case FAKE_ACCESS_DENIED:
      return "fakeDenied";
  }
}

std::string GetStringMatchOperation(int pos, uint32_t options) {
  if (pos == 0) {
    if (options) {
      return "exact";
    } else {
      return "prefix";
    }
  } else if (pos < 0) {
    return "scan";
  } else if (pos == kSeekToEnd) {
    return "ends";
  } else {
    DCHECK(false) << "Invalid pos (" << pos << ")";
    return "unknown";
  }
}

std::string GetPolicyOpcode(const PolicyOpcode* opcode, bool continuation) {
  // See |policy_engine_opcodes.cc|.
  uint32_t args[4];
  auto options = opcode->GetOptions();
  auto param = opcode->GetParameter();
  std::string condition;

  if (options & kPolNegateEval)
    condition += "!(";

  switch (opcode->GetID()) {
    case OP_ALWAYS_FALSE:
      condition += "false";
      break;
    case OP_ALWAYS_TRUE:
      condition += "true";
      break;
    case OP_NUMBER_MATCH:
      opcode->GetArgument(1, &args[1]);
      if (args[1] == UINT32_TYPE) {
        opcode->GetArgument(0, &args[0]);
        condition += base::StringPrintf("p[%d] == %x", param, args[0]);
      } else {
        const void* match_ptr = nullptr;
        opcode->GetArgument(0, &match_ptr);
        condition += base::StringPrintf("p[%d] == %p", param, match_ptr);
      }
      break;
    case OP_NUMBER_AND_MATCH:
      opcode->GetArgument(0, &args[0]);
      condition += base::StringPrintf("p[%d] & %x", param, args[0]);
      break;
    case OP_WSTRING_MATCH: {
      int pos;
      opcode->GetArgument(1, &args[1]);  // Length.
      opcode->GetArgument(2, &pos);      // Position.
      opcode->GetArgument(3, &args[3]);  // Options.
      // These are not nul-terminated so we have to wrap them here.
      auto match_string = std::wstring(opcode->GetRelativeString(0), 0,
                                       static_cast<size_t>(args[1]));
      condition += GetStringMatchOperation(pos, args[3]);
      condition +=
          base::StringPrintf("(p[%d], '%ls')", param, match_string.c_str());
    } break;
    case OP_ACTION:
      opcode->GetArgument(0, &args[0]);
      condition += GetOpcodeAction(static_cast<EvalResult>(args[0]));
      break;
    default:
      DCHECK(false) << "Unknown Opcode";
      return "Unknown";
  }

  if (options & kPolNegateEval)
    condition += ")";
  // If there is another rule add a joining token.
  if (continuation) {
    if (options & kPolUseOREval)
      condition += " || ";
    else
      condition += " && ";
  }
  return condition;
}

// Uses |service| to index into |policy_rules| returning a list of opcodes.
base::Value::List GetPolicyOpcodes(const PolicyGlobal* policy_rules,
                                   IpcTag service) {
  base::Value::List entry;
  PolicyBuffer* policy_buffer =
      policy_rules->entry[static_cast<size_t>(service)];
  // Build up rules and emit when we hit an action.
  std::string cur_rule;
  for (size_t i = 0; i < policy_buffer->opcode_count; i++) {
    const PolicyOpcode* opcode = &policy_buffer->opcodes[i];
    if (opcode->GetID() != OP_ACTION) {
      DCHECK(i + 1 < policy_buffer->opcode_count)
          << "Non-actions should not terminate rules";
      bool peak = policy_buffer->opcodes[i + 1].GetID() != OP_ACTION;
      cur_rule += GetPolicyOpcode(opcode, peak);
    } else {
      cur_rule += " -> ";
      cur_rule += GetPolicyOpcode(opcode, false);
      entry.Append(cur_rule);
      cur_rule.clear();
    }
  }
  return entry;
}

// policy_rules might be nullptr if no rules are defined.
base::Value::Dict GetPolicyRules(const std::vector<IpcTag>& ipcs,
                                 const PolicyGlobal* policy_rules) {
  base::Value::Dict results;

  for (auto ipc : ipcs) {
    if (policy_rules && policy_rules->entry[static_cast<size_t>(ipc)]) {
      results.Set(GetIpcTagAsString(ipc), GetPolicyOpcodes(policy_rules, ipc));
    } else {
      results.Set(GetIpcTagAsString(ipc), base::Value::List());
    }
  }

  return results;
}

// `handle_config` is a set of configuration bools - only output things
// if they are enabled.
base::Value::List GetHandlesToClose(HandleCloserConfig& handle_config) {
  base::Value::List results;
  if (!handle_config.handle_closer_enabled) {
    return results;
  }
  if (handle_config.section_windows_global_shell_counters) {
    results.Append(kWindowsShellGlobalCounters);
  }
  if (handle_config.file_device_api) {
    results.Append(kFileDeviceApi);
  }
  if (handle_config.file_ksecdd) {
    results.Append(kFileKsecDD);
  }
  if (handle_config.disconnect_csrss) {
    results.Append(kALPCPort);
  }
  return results;
}

}  // namespace

// We are a friend of PolicyBase so that we can steal its private members
// quickly in the BrokerServices tracker thread.
PolicyDiagnostic::PolicyDiagnostic(PolicyBase* policy) {
  DCHECK(policy);
  ConfigBase* config = policy->config();

  process_id_ = base::strict_cast<uint32_t>(policy->target_->ProcessId());
  lockdown_level_ = config->lockdown_level_;
  job_level_ = config->job_level_;
  tag_ = policy->tag_;

  // Select the final integrity level.
  if (config->delayed_integrity_level_ == INTEGRITY_LEVEL_LAST) {
    desired_integrity_level_ = config->integrity_level_;
  } else {
    desired_integrity_level_ = config->delayed_integrity_level_;
  }

  if (policy->dispatcher_) {
    // PolicyBase only ever holds a TopLevelDispatcher so this cast is safe.
    ipcs_ = (static_cast<TopLevelDispatcher*>(policy->dispatcher_.get()))
                ->ipc_targets();
  }

  desired_mitigations_ = config->mitigations_ | config->delayed_mitigations_;

  if (config->app_container_) {
    app_container_sid_.emplace(config->app_container_->GetPackageSid().Clone());
    for (const auto& sid : config->app_container_->GetCapabilities()) {
      capabilities_.push_back(sid.Clone());
    }
    for (const auto& sid :
         config->app_container_->GetImpersonationCapabilities()) {
      initial_capabilities_.push_back(sid.Clone());
    }

    app_container_type_ = config->app_container_->GetAppContainerType();
  }

  if (config->policy_) {
    PolicyGlobal* original_rules = config->policy_;
    size_t policy_mem_size = original_rules->data_size + sizeof(PolicyGlobal);
    policy_rules_.reset(
        static_cast<sandbox::PolicyGlobal*>(::operator new(policy_mem_size)));
    memcpy(policy_rules_.get(), original_rules, policy_mem_size);
    // Fixup pointers (see |PolicyGlobal| in policy_low_level.h).
    PolicyBuffer** original_entries = original_rules->entry;
    PolicyBuffer** copy_base = policy_rules_->entry;
    for (size_t i = 0; i < kSandboxIpcCount; i++) {
      if (policy_rules_->entry[i]) {
        policy_rules_->entry[i] = reinterpret_cast<PolicyBuffer*>(
            reinterpret_cast<char*>(copy_base) +
            (reinterpret_cast<char*>(original_entries[i]) -
             reinterpret_cast<char*>(original_entries)));
      }
    }
  }
  is_csrss_connected_ = config->is_csrss_connected();
  zero_appshim_ = config->zero_appshim();
  handles_to_close_ = config->handle_closer();
}

PolicyDiagnostic::~PolicyDiagnostic() = default;

const char* PolicyDiagnostic::JsonString() {
  // Lazily constructs json_string_.
  if (json_string_)
    return json_string_->c_str();

  base::Value::Dict dict;
  dict.Set(kProcessId, base::strict_cast<double>(process_id_));
  dict.Set(kTag, base::Value(tag_));
  dict.Set(kLockdownLevel, GetTokenLevelInEnglish(lockdown_level_));
  dict.Set(kJobLevel, GetJobLevelInEnglish(job_level_));
  dict.Set(kDesiredIntegrityLevel,
           GetIntegrityLevelInEnglish(desired_integrity_level_));
  dict.Set(kDesiredMitigations, GetMitigationsAsHex(desired_mitigations_));
  dict.Set(kPlatformMitigations,
           GetPlatformMitigationsAsHex(desired_mitigations_));
  dict.Set(kComponentFilters, GetComponentFilterAsHex(desired_mitigations_));

  if (app_container_sid_) {
    dict.Set(kAppContainerSid,
             base::AsStringPiece16(GetSidAsString(*app_container_sid_)));
    base::Value::List caps;
    for (const auto& sid : capabilities_) {
      auto sid_value = base::Value(base::AsStringPiece16(GetSidAsString(sid)));
      caps.Append(std::move(sid_value));
    }
    if (!caps.empty()) {
      dict.Set(kAppContainerCapabilities, std::move(caps));
    }
    base::Value::List imp_caps;
    for (const auto& sid : initial_capabilities_) {
      auto sid_value = base::Value(base::AsStringPiece16(GetSidAsString(sid)));
      imp_caps.Append(std::move(sid_value));
    }
    if (!imp_caps.empty()) {
      dict.Set(kAppContainerInitialCapabilities, std::move(imp_caps));
    }

    if (app_container_type_ == AppContainerType::kLowbox)
      dict.Set(kLowboxSid,
               base::AsStringPiece16(GetSidAsString(*app_container_sid_)));
  }

  if (ipcs_.size()) {
    dict.Set(
        kPolicyRules,
        GetPolicyRules(ipcs_, policy_rules_ ? policy_rules_.get() : nullptr));
  }

  dict.Set(kDisconnectCsrss, is_csrss_connected_ ? kDisabled : kEnabled);
  dict.Set(kZeroAppShim, zero_appshim_);
  dict.Set(kHandlesToClose, GetHandlesToClose(handles_to_close_));

  auto json_string = std::make_unique<std::string>();
  JSONStringValueSerializer to_json(json_string.get());
  CHECK(to_json.Serialize(dict));
  json_string_ = std::move(json_string);
  return json_string_->c_str();
}

}  // namespace sandbox