Support for "matches" and "js" keys from "content_scripts" in the chrome_extensions table (#6140)

Co-authored-by: William Woodruff <william@yossarian.net>
This commit is contained in:
Rachel Cipkins 2020-02-21 18:13:41 -05:00 committed by GitHub
parent 57ffaa89fc
commit 3de799ef07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 249 additions and 66 deletions

View File

@ -6,6 +6,8 @@
* the LICENSE file found in the root directory of this source tree.
*/
#include <osquery/logger.h>
#include <osquery/tables/applications/browser_utils.h>
#include <osquery/utils/info/platform_type.h>
@ -18,39 +20,49 @@ namespace tables {
#pragma warning(disable : 4503)
#endif
QueryData genChromeExtensions(QueryContext& context) {
static std::vector<fs::path> getChromePaths() {
std::vector<fs::path> 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

View File

@ -20,6 +20,17 @@ namespace osquery {
namespace tables {
namespace {
using ChromeExtensionContentScriptMap =
std::map<std::tuple<std::string, std::string>,
std::set<std::tuple<std::string, std::string>>>;
using ChromeContentScriptDetails =
std::vector<std::map<std::string, std::vector<std::string>>>;
using ChromeUserExtensions =
std::tuple<std::string /* uid */,
std::vector<std::string> /* extension_paths*/>;
#define kManifestFile "/manifest.json"
const std::map<std::string, std::string> 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<ChromeUserExtensions> chromeExtensionPathsByUser(
const QueryData& users, const std::vector<fs::path>& chromePaths) {
std::vector<ChromeUserExtensions> 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<std::string> 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<std::string> unversionedExtensions = {};
listDirectoriesInDirectory(profile, unversionedExtensions);
if (unversionedExtensions.empty()) {
continue;
}
std::vector<std::string> 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<std::string, std::vector<std::string>> 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<std::string>()) {
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<std::string>()) {
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<std::string>("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<fs::path>& 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<std::string> 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<std::string> extensions = {};
listDirectoriesInDirectory(profile, extensions);
for (const auto& userExtensionPaths : extensionPathsByUser) {
const auto& uid = std::get<0>(userExtensionPaths);
std::map<fs::path, std::string> 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<std::string> versions;
for (const auto& extension : extensions) {
listDirectoriesInDirectory(extension, versions);
}
// Extensions use /<EXTENSION>/<VERSION>/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<fs::path>& 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

View File

@ -28,6 +28,9 @@ namespace tables {
QueryData genChromeBasedExtensions(QueryContext& context,
const std::vector<fs::path>& chrome_paths);
QueryData genChromeBasedExtensionContentScripts(
QueryContext& context, const std::vector<fs::path>& chrome_paths);
/// A helper check to rename bool-type values as 1 or 0.
inline void jsonBoolAsInt(std::string& s) {
auto expected = tryTo<bool>(s);
@ -35,5 +38,5 @@ inline void jsonBoolAsInt(std::string& s) {
s = expected.get() ? "1" : "0";
}
}
}
}
} // namespace tables
} // namespace osquery

View File

@ -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",

View File

@ -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

View File

@ -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",
])

View File

@ -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);
}