OSDN Git Service

Enable obfuscation of resource names, with whitelisting support.
authorLuke Nicholson <lukenicholson@google.com>
Fri, 1 Dec 2017 23:29:03 +0000 (15:29 -0800)
committerLuke Nicholson <lukenicholson@google.com>
Tue, 5 Dec 2017 01:54:05 +0000 (17:54 -0800)
Test: Built aapt2, ran optimize on gmail apk with sample whitelist
config file, and flags enabled. Added two unit tests to TableFlattener
covering obfuscation logic.

Change-Id: Iad6329d75ff440121bf1a2cdf09c5f4bf4199d9d

tools/aapt2/cmd/Optimize.cpp
tools/aapt2/format/binary/TableFlattener.cpp
tools/aapt2/format/binary/TableFlattener.h
tools/aapt2/format/binary/TableFlattener_test.cpp
tools/aapt2/text/Unicode_test.cpp

index 2bf91a5..3b90637 100644 (file)
@@ -17,6 +17,7 @@
 #include <memory>
 #include <vector>
 
+#include "android-base/file.h"
 #include "android-base/stringprintf.h"
 
 #include "androidfw/ResourceTypes.h"
@@ -47,6 +48,7 @@ using ::aapt::configuration::Artifact;
 using ::aapt::configuration::PostProcessingConfiguration;
 using ::android::ResTable_config;
 using ::android::StringPiece;
+using ::android::base::ReadFileToString;
 using ::android::base::StringAppendF;
 using ::android::base::StringPrintf;
 
@@ -279,6 +281,20 @@ class OptimizeCommand {
   OptimizeContext* context_;
 };
 
+bool ExtractWhitelistFromConfig(const std::string& path, OptimizeContext* context,
+                                OptimizeOptions* options) {
+  std::string contents;
+  if (!ReadFileToString(path, &contents, true)) {
+    context->GetDiagnostics()->Error(DiagMessage()
+                                     << "failed to parse whitelist from config file: " << path);
+    return false;
+  }
+  for (const StringPiece& resource_name : util::Tokenize(contents, ',')) {
+    options->table_flattener_options.whitelisted_resources.insert(resource_name.to_string());
+  }
+  return true;
+}
+
 bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk,
                                 OptimizeOptions* out_options) {
   const xml::XmlResource* manifest = apk->GetManifest();
@@ -302,6 +318,7 @@ int Optimize(const std::vector<StringPiece>& args) {
   OptimizeContext context;
   OptimizeOptions options;
   Maybe<std::string> config_path;
+  Maybe<std::string> whitelist_path;
   Maybe<std::string> target_densities;
   Maybe<std::string> target_abis;
   std::vector<std::string> configs;
@@ -320,6 +337,10 @@ int Optimize(const std::vector<StringPiece>& args) {
               "All the resources that would be unused on devices of the given densities will be \n"
               "removed from the APK.",
               &target_densities)
+          .OptionalFlag("--whitelist-config-path",
+                        "Path to the whitelist.cfg file containing whitelisted resources \n"
+                        "whose names should not be altered in final resource tables.",
+                        &whitelist_path)
           .OptionalFlag(
               "--target-abis",
               "Comma separated list of the CPU ABIs that the APK will be optimized for.\n"
@@ -339,6 +360,9 @@ int Optimize(const std::vector<StringPiece>& args) {
                           "Enables encoding sparse entries using a binary search tree.\n"
                           "This decreases APK size at the cost of resource retrieval performance.",
                           &options.table_flattener_options.use_sparse_entries)
+          .OptionalSwitch("--enable-resource-obfuscation",
+                          "Enables obfuscation of key string pool to single value",
+                          &options.table_flattener_options.collapse_key_stringpool)
           .OptionalSwitch("-v", "Enables verbose logging", &verbose);
 
   if (!flags.Parse("aapt2 optimize", args, &std::cerr)) {
@@ -425,6 +449,15 @@ int Optimize(const std::vector<StringPiece>& args) {
     return 1;
   }
 
+  if (options.table_flattener_options.collapse_key_stringpool) {
+    if (whitelist_path) {
+      std::string& path = whitelist_path.value();
+      if (!ExtractWhitelistFromConfig(path, &context, &options)) {
+        return 1;
+      }
+    }
+  }
+
   if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) {
     return 1;
   }
index 2a51df3..a3034df 100644 (file)
@@ -220,12 +220,15 @@ class MapFlattenVisitor : public ValueVisitor {
 class PackageFlattener {
  public:
   PackageFlattener(IAaptContext* context, ResourceTablePackage* package,
-                   const std::map<size_t, std::string>* shared_libs, bool use_sparse_entries)
+                   const std::map<size_t, std::string>* shared_libs, bool use_sparse_entries,
+                   bool collapse_key_stringpool, const std::set<std::string>& whitelisted_resources)
       : context_(context),
         diag_(context->GetDiagnostics()),
         package_(package),
         shared_libs_(shared_libs),
-        use_sparse_entries_(use_sparse_entries) {
+        use_sparse_entries_(use_sparse_entries),
+        collapse_key_stringpool_(collapse_key_stringpool),
+        whitelisted_resources_(whitelisted_resources) {
   }
 
   bool FlattenPackage(BigBuffer* buffer) {
@@ -494,13 +497,23 @@ class PackageFlattener {
       // configuration available. Here we reverse this to match the binary
       // table.
       std::map<ConfigDescription, std::vector<FlatEntry>> config_to_entry_list_map;
-      for (ResourceEntry* entry : sorted_entries) {
-        const uint32_t key_index = (uint32_t)key_pool_.MakeRef(entry->name).index();
 
+      // hardcoded string uses characters which make it an invalid resource name
+      const std::string obfuscated_resource_name = "0_resource_name_obfuscated";
+
+      for (ResourceEntry* entry : sorted_entries) {
+        uint32_t local_key_index;
+        if (!collapse_key_stringpool_ ||
+            whitelisted_resources_.find(entry->name) != whitelisted_resources_.end()) {
+          local_key_index = (uint32_t)key_pool_.MakeRef(entry->name).index();
+        } else {
+          // resource isn't whitelisted, add it as obfuscated value
+          local_key_index = (uint32_t)key_pool_.MakeRef(obfuscated_resource_name).index();
+        }
         // Group values by configuration.
         for (auto& config_value : entry->values) {
           config_to_entry_list_map[config_value->config].push_back(
-              FlatEntry{entry, config_value->value.get(), key_index});
+              FlatEntry{entry, config_value->value.get(), local_key_index});
         }
       }
 
@@ -549,6 +562,8 @@ class PackageFlattener {
   bool use_sparse_entries_;
   StringPool type_pool_;
   StringPool key_pool_;
+  bool collapse_key_stringpool_;
+  const std::set<std::string>& whitelisted_resources_;
 };
 
 }  // namespace
@@ -593,7 +608,8 @@ bool TableFlattener::Consume(IAaptContext* context, ResourceTable* table) {
     }
 
     PackageFlattener flattener(context, package.get(), &table->included_packages_,
-                               options_.use_sparse_entries);
+                               options_.use_sparse_entries, options_.collapse_key_stringpool,
+                               options_.whitelisted_resources);
     if (!flattener.FlattenPackage(&package_buffer)) {
       return false;
     }
index 88cbddf..c2e1d4b 100644 (file)
@@ -35,6 +35,14 @@ struct TableFlattenerOptions {
   // This is only available on platforms O+ and will only be respected when
   // minSdk is O+.
   bool use_sparse_entries = false;
+
+  // When true, the key string pool in the final ResTable
+  // is collapsed to a single entry. All resource entries
+  // have name indices that point to this single value
+  bool collapse_key_stringpool = false;
+
+  // Set of whitelisted resource names to avoid altering in key stringpool
+  std::set<std::string> whitelisted_resources;
 };
 
 class TableFlattener : public IResourceTableConsumer {
index e11890b..f0b80d2 100644 (file)
@@ -127,6 +127,15 @@ class TableFlattenerTest : public ::testing::Test {
              << StringPiece16(actual_name.name, actual_name.nameLen) << "'";
     }
 
+    ResourceName actual_res_name(resName.value());
+
+    if (expected_res_name.entry != actual_res_name.entry ||
+        expected_res_name.package != actual_res_name.package ||
+        expected_res_name.type != actual_res_name.type) {
+      return ::testing::AssertionFailure() << "expected resource '" << expected_res_name.to_string()
+                                           << "' but got '" << actual_res_name.to_string() << "'";
+    }
+
     if (expected_config != config) {
       return ::testing::AssertionFailure() << "expected config '" << expected_config
                                            << "' but got '" << ConfigDescription(config) << "'";
@@ -450,4 +459,113 @@ TEST_F(TableFlattenerTest, LongSharedLibraryPackageNameIsIllegal) {
   ASSERT_FALSE(Flatten(context.get(), {}, table.get(), &result));
 }
 
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .SetPackageId("com.app.test", 0x7f)
+          .AddSimple("com.app.test:id/one", ResourceId(0x7f020000))
+          .AddSimple("com.app.test:id/two", ResourceId(0x7f020001))
+          .AddValue("com.app.test:id/three", ResourceId(0x7f020002),
+                    test::BuildReference("com.app.test:id/one", ResourceId(0x7f020000)))
+          .AddValue("com.app.test:integer/one", ResourceId(0x7f030000),
+                    util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u))
+          .AddValue("com.app.test:integer/one", test::ParseConfigOrDie("v1"),
+                    ResourceId(0x7f030000),
+                    util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u))
+          .AddString("com.app.test:string/test", ResourceId(0x7f040000), "foo")
+          .AddString("com.app.test:layout/bar", ResourceId(0x7f050000), "res/layout/bar.xml")
+          .Build();
+
+  TableFlattenerOptions options;
+  options.collapse_key_stringpool = true;
+
+  ResTable res_table;
+
+  ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020002), {}, Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+                     ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u,
+                     ResTable_config::CONFIG_VERSION));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+                     ResourceId(0x7f030000), test::ParseConfigOrDie("v1"), Res_value::TYPE_INT_DEC,
+                     2u, ResTable_config::CONFIG_VERSION));
+
+  std::u16string foo_str = u"foo";
+  ssize_t idx = res_table.getTableStringBlock(0)->indexOfString(foo_str.data(), foo_str.size());
+  ASSERT_GE(idx, 0);
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+                     ResourceId(0x7f040000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+
+  std::u16string bar_path = u"res/layout/bar.xml";
+  idx = res_table.getTableStringBlock(0)->indexOfString(bar_path.data(), bar_path.size());
+  ASSERT_GE(idx, 0);
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+                     ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+}
+
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .SetPackageId("com.app.test", 0x7f)
+          .AddSimple("com.app.test:id/one", ResourceId(0x7f020000))
+          .AddSimple("com.app.test:id/two", ResourceId(0x7f020001))
+          .AddValue("com.app.test:id/three", ResourceId(0x7f020002),
+                    test::BuildReference("com.app.test:id/one", ResourceId(0x7f020000)))
+          .AddValue("com.app.test:integer/one", ResourceId(0x7f030000),
+                    util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u))
+          .AddValue("com.app.test:integer/one", test::ParseConfigOrDie("v1"),
+                    ResourceId(0x7f030000),
+                    util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u))
+          .AddString("com.app.test:string/test", ResourceId(0x7f040000), "foo")
+          .AddString("com.app.test:layout/bar", ResourceId(0x7f050000), "res/layout/bar.xml")
+          .Build();
+
+  TableFlattenerOptions options;
+  options.collapse_key_stringpool = true;
+  options.whitelisted_resources.insert("test");
+  options.whitelisted_resources.insert("three");
+  ResTable res_table;
+
+  ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+                     ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:id/three", ResourceId(0x7f020002), {},
+                     Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+                     ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u,
+                     ResTable_config::CONFIG_VERSION));
+
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+                     ResourceId(0x7f030000), test::ParseConfigOrDie("v1"), Res_value::TYPE_INT_DEC,
+                     2u, ResTable_config::CONFIG_VERSION));
+
+  std::u16string foo_str = u"foo";
+  ssize_t idx = res_table.getTableStringBlock(0)->indexOfString(foo_str.data(), foo_str.size());
+  ASSERT_GE(idx, 0);
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:string/test", ResourceId(0x7f040000), {},
+                     Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+
+  std::u16string bar_path = u"res/layout/bar.xml";
+  idx = res_table.getTableStringBlock(0)->indexOfString(bar_path.data(), bar_path.size());
+  ASSERT_GE(idx, 0);
+  EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+                     ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+}
+
 }  // namespace aapt
index a8e797c..16bc2e8 100644 (file)
@@ -63,6 +63,7 @@ TEST(UnicodeTest, IsValidResourceEntryName) {
   EXPECT_FALSE(IsValidResourceEntryName("Føø/Bar"));
   EXPECT_FALSE(IsValidResourceEntryName("Føø:Bar"));
   EXPECT_FALSE(IsValidResourceEntryName("Føø;Bar"));
+  EXPECT_FALSE(IsValidResourceEntryName("0_resource_name_obfuscated"));
 }
 
 }  // namespace text