From 3de799ef07d71331c9fd79e7c26be8b59047411d Mon Sep 17 00:00:00 2001 From: Rachel Cipkins <32401235+rcipkins@users.noreply.github.com> Date: Fri, 21 Feb 2020 18:13:41 -0500 Subject: [PATCH] Support for "matches" and "js" keys from "content_scripts" in the chrome_extensions table (#6140) Co-authored-by: William Woodruff --- .../tables/applications/browser_chrome.cpp | 64 ++--- osquery/tables/applications/browser_utils.cpp | 218 +++++++++++++++--- osquery/tables/applications/browser_utils.h | 7 +- specs/BUCK | 1 + specs/CMakeLists.txt | 1 + specs/chrome_extension_content_scripts.table | 20 ++ .../integration/tables/chrome_extensions.cpp | 4 +- 7 files changed, 249 insertions(+), 66 deletions(-) create mode 100644 specs/chrome_extension_content_scripts.table diff --git a/osquery/tables/applications/browser_chrome.cpp b/osquery/tables/applications/browser_chrome.cpp index 2394517e..32481094 100644 --- a/osquery/tables/applications/browser_chrome.cpp +++ b/osquery/tables/applications/browser_chrome.cpp @@ -6,6 +6,8 @@ * the LICENSE file found in the root directory of this source tree. */ +#include + #include #include @@ -18,39 +20,49 @@ namespace tables { #pragma warning(disable : 4503) #endif -QueryData genChromeExtensions(QueryContext& context) { +static std::vector getChromePaths() { + std::vector chromePaths; /// Each home directory will include custom extensions. - fs::path chrome_path; if (isPlatform(PlatformType::TYPE_WINDOWS)) { - chrome_path = - "\\AppData\\Local\\Google\\Chrome\\User Data\\%\\Extensions\\"; + chromePaths.push_back( + "\\AppData\\Local\\Google\\Chrome\\User Data\\%\\Extensions\\"); } else if (isPlatform(PlatformType::TYPE_OSX)) { - chrome_path = "/Library/Application Support/Google/Chrome/%/Extensions/"; + chromePaths.push_back( + "/Library/Application Support/Google/Chrome/%/Extensions/"); } else { - chrome_path = "/.config/google-chrome/%/Extensions/"; - } - fs::path brave_path; - if (isPlatform(PlatformType::TYPE_WINDOWS)) { - brave_path = "\\AppData\\Roaming\\brave\\Extensions\\"; - } else if (isPlatform(PlatformType::TYPE_OSX)) { - brave_path = - "/Library/Application " - "Support/BraveSoftware/Brave-Browser/%/Extensions/"; - } else { - brave_path = "/.config/BraveSoftware/Brave-Browser/%/Extensions/"; - } - fs::path chromium_path; - if (isPlatform(PlatformType::TYPE_WINDOWS)) { - chromium_path = "\\AppData\\Local\\Chromium\\Extensions\\"; - } else if (isPlatform(PlatformType::TYPE_OSX)) { - chromium_path = "/Library/Application Support/Chromium/%/Extensions/"; - } else { - chromium_path = "/.config/chromium/%/Extensions/"; + chromePaths.push_back("/.config/google-chrome/%/Extensions/"); } - return genChromeBasedExtensions(context, - {chrome_path, brave_path, chromium_path}); + if (isPlatform(PlatformType::TYPE_WINDOWS)) { + chromePaths.push_back("\\AppData\\Roaming\\brave\\Extensions\\"); + } else if (isPlatform(PlatformType::TYPE_OSX)) { + chromePaths.push_back( + "/Library/Application " + "Support/BraveSoftware/Brave-Browser/%/Extensions/"); + } else { + chromePaths.push_back("/.config/BraveSoftware/Brave-Browser/%/Extensions/"); + } + + if (isPlatform(PlatformType::TYPE_WINDOWS)) { + chromePaths.push_back("\\AppData\\Local\\Chromium\\Extensions\\"); + } else if (isPlatform(PlatformType::TYPE_OSX)) { + chromePaths.push_back( + "/Library/Application Support/Chromium/%/Extensions/"); + } else { + chromePaths.push_back("/.config/chromium/%/Extensions/"); + } + + return chromePaths; } + +QueryData genChromeExtensions(QueryContext& context) { + return genChromeBasedExtensions(context, getChromePaths()); } + +QueryData genChromeExtensionContentScripts(QueryContext& context) { + return genChromeBasedExtensionContentScripts(context, getChromePaths()); } + +} // namespace tables +} // namespace osquery diff --git a/osquery/tables/applications/browser_utils.cpp b/osquery/tables/applications/browser_utils.cpp index 111a56fa..0c051753 100644 --- a/osquery/tables/applications/browser_utils.cpp +++ b/osquery/tables/applications/browser_utils.cpp @@ -20,6 +20,17 @@ namespace osquery { namespace tables { namespace { +using ChromeExtensionContentScriptMap = + std::map, + std::set>>; + +using ChromeContentScriptDetails = + std::vector>>; + +using ChromeUserExtensions = + std::tuple /* extension_paths*/>; + #define kManifestFile "/manifest.json" const std::map kExtensionKeys = { @@ -35,6 +46,46 @@ const std::string kExtensionPermissionKey = "permissions"; const std::string kExtensionOptionalPermissionKey = "optional_permissions"; const std::string kProfilePreferencesFile = "Preferences"; const std::string kProfilePreferenceKey = "profile"; +const std::string kScriptKey = "js"; +const std::string kMatchesKey = "matches"; + +std::vector chromeExtensionPathsByUser( + const QueryData& users, const std::vector& chromePaths) { + std::vector extensionPathsByUser; + + for (const auto& row : users) { + if (row.count("uid") > 0 && row.count("directory") > 0) { + // For each user, enumerate all of their chrome profiles. + std::vector profiles; + for (const auto& chromePath : chromePaths) { + fs::path extension_path = row.at("directory") / chromePath; + if (!resolveFilePattern(extension_path, profiles, GLOB_FOLDERS).ok()) { + continue; + } + + // For each profile list each extension in the Extensions directory. + for (const auto& profile : profiles) { + std::vector unversionedExtensions = {}; + listDirectoriesInDirectory(profile, unversionedExtensions); + + if (unversionedExtensions.empty()) { + continue; + } + + std::vector extensionPaths; + for (const auto& unversionedExtension : unversionedExtensions) { + listDirectoriesInDirectory(unversionedExtension, extensionPaths); + } + + extensionPathsByUser.push_back( + std::make_tuple(row.at("uid"), extensionPaths)); + } + } + } + } + + return extensionPathsByUser; +} Status getChromeProfileName(std::string& name, const fs::path& path) { name.clear(); @@ -97,6 +148,41 @@ const std::string genPermissions(const std::string& permissionTypeKey, return permission_list; } +ChromeContentScriptDetails genContentScriptDetail(const pt::ptree& tree) { + ChromeContentScriptDetails details; + + if (const auto& content_script_array = + tree.get_child_optional("content_scripts")) { + for (const auto& content_script : content_script_array.get()) { + std::map> detail; + + if (const auto& js_script_array = + content_script.second.get_child_optional(kScriptKey)) { + for (const auto& js_script : js_script_array.get()) { + if (const auto& js_script_value = + js_script.second.get_value_optional()) { + detail[kScriptKey].push_back(js_script_value.get()); + } + } + } + + if (const auto& match_array = + content_script.second.get_child_optional(kMatchesKey)) { + for (const auto& match : match_array.get()) { + if (const auto& match_value = + match.second.get_value_optional()) { + detail[kMatchesKey].push_back(match_value.get()); + } + } + } + + details.push_back(detail); + } + } + + return details; +} + void genExtension(const std::string& uid, const std::string& path, const std::string& profile_name, @@ -174,58 +260,116 @@ void genExtension(const std::string& uid, r["identifier"] = fs::path(path).parent_path().parent_path().leaf().string(); r["path"] = path; + results.push_back(r); } +void genExtensionContentScripts( + const std::string& path, + ChromeExtensionContentScriptMap& contentScriptMap) { + std::string json_data; + if (!forensicReadFile(path + kManifestFile, json_data).ok()) { + VLOG(1) << "Could not read file: " << path + kManifestFile; + return; + } + + // Read the extension metadata into a JSON blob, then property tree. + pt::ptree tree; + try { + std::stringstream json_stream; + json_stream << json_data; + pt::read_json(json_stream, tree); + } catch (const pt::json_parser::json_parser_error& /* e */) { + VLOG(1) << "Could not parse JSON from: " << path + kManifestFile; + return; + } + + const std::string& version = tree.get("version", ""); + auto& scriptMatchPairs = contentScriptMap[std::make_tuple( + fs::path(path).parent_path().parent_path().leaf().string(), version)]; + + auto contentScriptDetail = genContentScriptDetail(tree); + for (auto& contentScript : contentScriptDetail) { + for (auto& script : contentScript[kScriptKey]) { + if (contentScript[kMatchesKey].empty()) { + scriptMatchPairs.insert(std::make_tuple(script, "")); + } else { + for (auto& match : contentScript[kMatchesKey]) { + scriptMatchPairs.insert(std::make_tuple(script, match)); + } + } + } + } +} + QueryData genChromeBasedExtensions(QueryContext& context, const std::vector& chromePaths) { QueryData results; - auto users = usersFromContext(context); - for (const auto& row : users) { - if (row.count("uid") > 0 && row.count("directory") > 0) { - // For each user, enumerate all of their chrome profiles. - std::vector profiles; - for (const auto& chromePath : chromePaths) { - fs::path extension_path = row.at("directory") / chromePath; - if (!resolveFilePattern(extension_path, profiles, GLOB_FOLDERS).ok()) { - continue; - } + const auto& extensionPathsByUser = + chromeExtensionPathsByUser(usersFromContext(context), chromePaths); - // For each profile list each extension in the Extensions directory. - for (const auto& profile : profiles) { - std::vector extensions = {}; - listDirectoriesInDirectory(profile, extensions); + for (const auto& userExtensionPaths : extensionPathsByUser) { + const auto& uid = std::get<0>(userExtensionPaths); + std::map profileNameMap; - if (extensions.empty()) { - continue; - } + for (const auto& version : std::get<1>(userExtensionPaths)) { + const auto& profile_path = fs::path(version) + .parent_path() + .parent_path() + .parent_path() + .parent_path(); - auto profile_path = fs::path(profile).parent_path().parent_path(); - - std::string profile_name; - auto status = getChromeProfileName(profile_name, profile_path); - if (!status.ok()) { - LOG(WARNING) << "Getting Chrome profile name failed: " - << status.getMessage(); - } - - // Generate an addons list from their extensions JSON. - std::vector versions; - for (const auto& extension : extensions) { - listDirectoriesInDirectory(extension, versions); - } - - // Extensions use ///manifest.json. - for (const auto& version : versions) { - genExtension(row.at("uid"), version, profile_name, results); - } + auto it = profileNameMap.find(profile_path); + if (it == profileNameMap.end()) { + auto status = + getChromeProfileName(profileNameMap[profile_path], profile_path); + if (!status.ok()) { + LOG(WARNING) << "Getting Chrome profile name failed: " + << status.getMessage(); } } + + genExtension(uid, version, profileNameMap[profile_path], results); } } return results; } + +QueryData genChromeBasedExtensionContentScripts( + QueryContext& context, const std::vector& chromePaths) { + QueryData results; + + // Extensions are frequently duplicated across profiles and + // Chrome installations, so we construct a map of + // (extension_id, version) -> {(script, match)} + // for deduplication purposes. + ChromeExtensionContentScriptMap contentScriptMap; + + const auto& extensionPathsByUser = + chromeExtensionPathsByUser(usersFromContext(context), chromePaths); + for (const auto& userExtensionPaths : extensionPathsByUser) { + for (const auto& version : std::get<1>(userExtensionPaths)) { + genExtensionContentScripts(version, contentScriptMap); + } + } + + for (const auto& it : contentScriptMap) { + Row r; + + r["identifier"] = std::get<0>(it.first); + r["version"] = std::get<1>(it.first); + + for (const auto& scriptMatchPair : it.second) { + r["script"] = std::get<0>(scriptMatchPair); + r["match"] = std::get<1>(scriptMatchPair); + results.push_back(r); + } + } + + return results; } -} + +} // namespace tables +} // namespace osquery diff --git a/osquery/tables/applications/browser_utils.h b/osquery/tables/applications/browser_utils.h index 3afc2a2e..46dd09d6 100644 --- a/osquery/tables/applications/browser_utils.h +++ b/osquery/tables/applications/browser_utils.h @@ -28,6 +28,9 @@ namespace tables { QueryData genChromeBasedExtensions(QueryContext& context, const std::vector& chrome_paths); +QueryData genChromeBasedExtensionContentScripts( + QueryContext& context, const std::vector& chrome_paths); + /// A helper check to rename bool-type values as 1 or 0. inline void jsonBoolAsInt(std::string& s) { auto expected = tryTo(s); @@ -35,5 +38,5 @@ inline void jsonBoolAsInt(std::string& s) { s = expected.get() ? "1" : "0"; } } -} -} +} // namespace tables +} // namespace osquery diff --git a/specs/BUCK b/specs/BUCK index 784bc2c6..dc0f7d29 100644 --- a/specs/BUCK +++ b/specs/BUCK @@ -819,6 +819,7 @@ osquery_gentable_cxx_library( "carbon_black_info.table", "carves.table", "chrome_extensions.table", + "chrome_extension_content_scripts.table", "cpuid.table", "curl.table", "curl_certificate.table", diff --git a/specs/CMakeLists.txt b/specs/CMakeLists.txt index a4e203bc..547d0f31 100644 --- a/specs/CMakeLists.txt +++ b/specs/CMakeLists.txt @@ -33,6 +33,7 @@ function(generateNativeTables) carbon_black_info.table carves.table chrome_extensions.table + chrome_extension_content_scripts.table cpuid.table curl.table curl_certificate.table diff --git a/specs/chrome_extension_content_scripts.table b/specs/chrome_extension_content_scripts.table new file mode 100644 index 00000000..8c1757c8 --- /dev/null +++ b/specs/chrome_extension_content_scripts.table @@ -0,0 +1,20 @@ +table_name("chrome_extension_content_scripts") +description("Chrome browser extension content scripts.") +schema([ + Column("uid", BIGINT, "The local user that owns the extension", + index=True), + Column("identifier", TEXT, "Extension identifier"), + Column("version", TEXT, "Extension-supplied version"), + Column("script", TEXT, "The content script used by the extension"), + Column("match", TEXT, "The pattern that the script is matched against"), + ForeignKey(column="uid", table="users"), +]) +attributes(user_data=True) +implementation("applications/browser_chrome@genChromeExtensionContentScripts") +examples([ + "select * from chrome_extensions join chrome_extension_content_scripts using (identifier)", +]) +fuzz_paths([ + "/Library/Application Support/Google/Chrome/", + "/Users", +]) diff --git a/tests/integration/tables/chrome_extensions.cpp b/tests/integration/tables/chrome_extensions.cpp index 45326241..9011753d 100644 --- a/tests/integration/tables/chrome_extensions.cpp +++ b/tests/integration/tables/chrome_extensions.cpp @@ -35,7 +35,9 @@ TEST_F(chromeExtensions, test_sanity) { {"persistent", IntType}, {"path", NonEmptyString}, {"permissions", NormalType}, - {"profile", NonEmptyString}}; + {"profile", NormalType}, + {"script", NormalType}, + {"match", NormalType}}; validate_rows(data, row_map); }