source/ClassProperties.cpp (351 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include <exception> #include <mutex> #include <InstructionAnalyzer.h> #include <RedexResources.h> #include <Walkers.h> #include <re2/re2.h> #include <mariana-trench/Assert.h> #include <mariana-trench/ClassProperties.h> #include <mariana-trench/Constants.h> #include <mariana-trench/Features.h> #include <mariana-trench/Heuristics.h> #include <mariana-trench/Log.h> #include <mariana-trench/Options.h> #include <mariana-trench/Redex.h> #include <mariana-trench/model-generator/ModelGenerator.h> namespace { // Various component sources are not matching the class names in the // manifest leading to features (like exported) not being added. std::string strip_inner_class(std::string class_name) { auto position = class_name.find_first_of("$"); if (position != std::string::npos) { return class_name.substr(0, position) + ";"; } return class_name; } bool is_class_exported_via_uri(const DexClass* clazz) { if (!clazz->get_anno_set()) { return false; } auto dfa_annotation = marianatrench::constants::get_dfa_annotation(); auto private_schemes = marianatrench::constants::get_private_uri_schemes(); for (const auto& annotation : clazz->get_anno_set()->get_annotations()) { if (!annotation->type() || annotation->type()->str() != dfa_annotation.type) { continue; } for (const DexAnnotationElement& element : annotation->anno_elems()) { if (element.string->str() != "value") { continue; } auto* patterns = dynamic_cast<DexEncodedValueArray*>(element.encoded_value.get()); mt_assert(patterns != nullptr); for (auto& encoded_pattern : *patterns->evalues()) { auto* pattern = dynamic_cast<DexEncodedValueAnnotation*>(encoded_pattern.get()); mt_assert(pattern != nullptr); if (!pattern->type() || pattern->type()->str() != dfa_annotation.pattern_type) { continue; } std::string pattern_value = pattern->show(); // We only care about patterns that specify a scheme or a pattern if (pattern_value.find("scheme") == std::string::npos && pattern_value.find("pattern") == std::string::npos) { continue; } if (!std::any_of( private_schemes.begin(), private_schemes.end(), [&](std::string scheme) { return pattern_value.find(scheme) != std::string::npos; })) { LOG(2, "Class {} has DFA annotations with a public URI scheme.", clazz->get_name()->str()); return true; } } } } return false; } std::optional<std::string> get_privacy_decision_number_from_annotations( const std::vector<std::unique_ptr<DexAnnotation>>& annotations) { auto privacy_decision_type = marianatrench::constants::get_privacy_decision_type(); for (const auto& annotation : annotations) { if (!annotation->type() || annotation->type()->str() != privacy_decision_type) { continue; } for (const DexAnnotationElement& element : annotation->anno_elems()) { if (element.string->str() != "value") { continue; } else { return element.encoded_value->show(); } } } return std::nullopt; } std::optional<std::string> get_privacy_decision_number_from_class( const DexClass* clazz) { if (!clazz->get_anno_set()) { return std::nullopt; } return get_privacy_decision_number_from_annotations( clazz->get_anno_set()->get_annotations()); } std::unordered_set<std::string> get_class_fragments(const DexClass* clazz) { std::unordered_set<std::string> exported_fragments; const static re2::RE2 fragment_regex = re2::RE2("(L[^a][^; ]*Fragment;)"); auto methods = clazz->get_all_methods(); for (DexMethod* method : methods) { const auto& code = method->get_code(); if (!code) { continue; } const cfg::ControlFlowGraph& cfg = code->cfg(); for (const auto* block : cfg.blocks()) { std::string match; // [0x7f5380273670] OPCODE: INVOKE_STATIC // Lcom/example/myapplication/ui/main/MainFragment;.newIn[...] // Here we match the class of any Fragment if (re2::RE2::PartialMatch(show(block), fragment_regex, &match)) { exported_fragments.emplace(match); } } } return exported_fragments; } } // namespace namespace marianatrench { ClassProperties::ClassProperties( const Options& options, const DexStoresVector& stores, const Features& features, const Dependencies& dependencies, std::unique_ptr<AndroidResources> android_resources) : features_(features), dependencies_(dependencies) { try { if (android_resources == nullptr) { android_resources = create_resource_reader(options.apk_directory()); } const auto manifest_class_info = android_resources->get_manifest_class_info(); for (const auto& tag_info : manifest_class_info.component_tags) { std::unordered_set<std::string> parent_classes; auto dex_class = redex::get_class(tag_info.classname); bool protection_level = false; bool permission = false; if (!tag_info.protection_level.empty() && tag_info.protection_level != "normal") { protection_level_classes_.emplace(tag_info.classname); protection_level = true; } if (!tag_info.permission.empty()) { permission_classes_.emplace(tag_info.classname); permission = true; } if (tag_info.is_exported == BooleanXMLAttribute::True || (tag_info.is_exported == BooleanXMLAttribute::Undefined && tag_info.has_intent_filters)) { exported_classes_.emplace(tag_info.classname); if (!protection_level && !permission && dex_class) { if (tag_info.tag == ComponentTag::Activity) { const auto& exported_fragments = get_class_fragments(dex_class); exported_classes_.insert( exported_fragments.begin(), exported_fragments.end()); } parent_classes = generator::get_custom_parents_from_class(dex_class); parent_exposed_classes_.insert( parent_classes.begin(), parent_classes.end()); } } else { unexported_classes_.emplace(tag_info.classname); } } } catch (const std::exception& e) { // Redex may assert, or throw `std::runtime_error` if the file is missing. ERROR(2, "Manifest could not be parsed: {}", e.what()); } std::mutex mutex; for (auto& scope : DexStoreClassesIterator(stores)) { walk::parallel::classes(scope, [&](DexClass* clazz) { if (is_class_exported_via_uri(clazz)) { std::lock_guard<std::mutex> lock(mutex); dfa_public_scheme_classes_.emplace(clazz->str()); } std::optional<std::string> privacy_decision_number = get_privacy_decision_number_from_class(clazz); if (privacy_decision_number) { std::lock_guard<std::mutex> lock(mutex); privacy_decision_classes_.emplace( clazz->str(), *privacy_decision_number); } }); } } bool ClassProperties::is_class_exported(const std::string& class_name) const { auto outer_class = strip_inner_class(class_name); return exported_classes_.count(class_name) > 0 || exported_classes_.count(outer_class) > 0; } bool ClassProperties::is_child_exposed(const std::string& class_name) const { auto outer_class = strip_inner_class(class_name); return parent_exposed_classes_.count(class_name) > 0 || parent_exposed_classes_.count(outer_class) > 0; } bool ClassProperties::is_class_unexported(const std::string& class_name) const { auto outer_class = strip_inner_class(class_name); return unexported_classes_.count(class_name) > 0 || unexported_classes_.count(outer_class) > 0; } bool ClassProperties::is_dfa_public(const std::string& class_name) const { auto outer_class = strip_inner_class(class_name); return dfa_public_scheme_classes_.count(class_name) > 0 || dfa_public_scheme_classes_.count(outer_class) > 0; } bool ClassProperties::has_protection_level( const std::string& class_name) const { auto outer_class = strip_inner_class(class_name); return protection_level_classes_.count(class_name) > 0 || protection_level_classes_.count(outer_class) > 0; } bool ClassProperties::has_permission(const std::string& class_name) const { auto outer_class = strip_inner_class(class_name); return permission_classes_.count(class_name) > 0 || permission_classes_.count(outer_class) > 0; ; } std::optional<std::string> ClassProperties::get_privacy_decision_number_from_class_name( const std::string& class_name) const { auto it = privacy_decision_classes_.find(class_name); if (it != privacy_decision_classes_.end()) { return it->second; } else { return std::nullopt; } } std::optional<std::string> ClassProperties::get_privacy_decision_number_from_method( const Method* method) const { std::optional<std::string> privacy_decision_number = std::nullopt; // add the annotation from the enclosing method by default if (method->dex_method()->get_anno_set()) { privacy_decision_number = get_privacy_decision_number_from_annotations( method->dex_method()->get_anno_set()->get_annotations()); } if (!privacy_decision_number) { privacy_decision_number = get_privacy_decision_number_from_class_name(method->get_class()->str()); } return privacy_decision_number; } FeatureMayAlwaysSet ClassProperties::propagate_features( const Method* caller, const Method* /*callee*/, const Features& feature_factory) const { FeatureSet features; std::optional<std::string> privacy_decision_number = get_privacy_decision_number_from_method(caller); if (privacy_decision_number) { features.add(feature_factory.get("pd-" + *privacy_decision_number)); } return FeatureMayAlwaysSet::make_always(features); } FeatureMayAlwaysSet ClassProperties::issue_features( const Method* method) const { auto clazz = method->get_class()->str(); auto features = get_class_features(clazz, /* via_dependency */ false); if (!has_user_exposed_properties(clazz) && !has_user_unexposed_properties(clazz)) { features.join_with(compute_transitive_class_features(method)); } return FeatureMayAlwaysSet::make_always(features); } FeatureSet ClassProperties::get_class_features( const std::string& clazz, bool via_dependency) const { FeatureSet features; if (is_class_exported(clazz)) { features.add(features_.get("via-caller-exported")); } if (is_child_exposed(clazz)) { features.add(features_.get("via-child-exposed")); } if (is_class_unexported(clazz)) { features.add(features_.get("via-caller-unexported")); } if (has_permission(clazz)) { features.add(features_.get("via-caller-permission")); } if (has_protection_level(clazz)) { features.add(features_.get("via-caller-protection-level")); } // `via-public-dfa-scheme` feature only applies within the same class. if (!via_dependency && is_dfa_public(clazz)) { features.add(features_.get("via-public-dfa-scheme")); } if (via_dependency) { features.add(features_.get("via-dependency-graph")); features.add(features_.get(fmt::format("via-class:{}", clazz))); } return features; } bool ClassProperties::has_user_exposed_properties( const std::string& class_name) const { return is_class_exported(class_name) || is_child_exposed(class_name); }; bool ClassProperties::has_user_unexposed_properties( const std::string& class_name) const { return is_class_unexported(class_name) || has_permission(class_name) || has_protection_level(class_name); }; namespace { struct QueueItem { const Method* method; size_t depth; }; } // namespace FeatureSet ClassProperties::compute_transitive_class_features( const Method* callee) const { // Check cache if (const auto* target_method = via_dependencies_.get(callee, nullptr)) { return get_class_features( target_method->get_class()->str(), /* via_dependency */ true); } size_t depth = 0; std::queue<QueueItem> queue; queue.push(QueueItem{.method = callee, .depth = depth}); std::unordered_set<const Method*> processed; QueueItem target{.method = nullptr, .depth = 0}; // Traverse the dependency graph till we find closest "exported" class. // If no "exported" class is reachable, find the closest "unexported" class. // "exported" class that is only reachable via an "unexported" class is // ignored. while (!queue.empty()) { auto item = queue.front(); queue.pop(); processed.emplace(item.method); depth = item.depth; const auto& class_name = item.method->get_class()->str(); if (has_user_exposed_properties(class_name)) { target = item; break; } if (target.method == nullptr && has_user_unexposed_properties(class_name)) { // Continue search for user exposed properties along other paths. target = item; continue; } if (depth == Heuristics::kMaxDepthClassProperties) { continue; } for (const auto* dependency : dependencies_.dependencies(item.method)) { if (processed.count(dependency) == 0) { queue.push(QueueItem{.method = dependency, .depth = depth + 1}); } } } if (target.method != nullptr) { LOG(4, "Class properties found for: `{}` via-dependency with `{}` at depth {}", show(callee), show(target.method), target.depth); via_dependencies_.insert({callee, target.method}); return get_class_features( target.method->get_class()->str(), /* via_dependency */ true); } return FeatureSet(); } } // namespace marianatrench