Skip to content

Commit

Permalink
add windows ads table
Browse files Browse the repository at this point in the history
  • Loading branch information
nachorpaez committed Nov 13, 2023
1 parent ac174de commit 4f3eec5
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 0 deletions.
1 change: 1 addition & 0 deletions osquery/tables/system/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ function(generateOsqueryTablesSystemSystemtable)

elseif(DEFINED PLATFORM_WINDOWS)
list(APPEND source_files
windows/ads.cpp
windows/appcompat_shims.cpp
windows/authenticode.cpp
windows/autoexec.cpp
Expand Down
179 changes: 179 additions & 0 deletions osquery/tables/system/windows/ads.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Copyright (c) 2014-present, The osquery authors
*
* This source code is licensed as defined by the LICENSE file found in the
* root directory of this source tree.
*
* SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only)
*/

#include <boost/algorithm/string/trim.hpp>
#include <boost/filesystem.hpp>
#include <string>
#include <windows.h>

#include <osquery/core/tables.h>
#include <osquery/filesystem/filesystem.h>
#include <osquery/logger/logger.h>
#include <osquery/utils/base64.h>
#include <osquery/utils/chars.h>
#include <osquery/utils/conversions/split.h>
#include <osquery/utils/conversions/windows/strings.h>
#include <osquery/utils/scope_guard.h>

namespace fs = boost::filesystem;

namespace osquery {
namespace tables {

const std::string kZoneIdentifierKey = "Zone.Identifier";

void setRow(QueryData& results,
const std::string& path,
const std::string& key,
const std::string& value) {
Row r;
r["path"] = path;
r["directory"] = boost::filesystem::path(path).parent_path().string();
r["key"] = key;
auto value_printable = isPrintable(value);
r["value"] = value_printable ? value : base64::encode(value);
r["base64"] = value_printable ? INTEGER(0) : INTEGER(1);
results.push_back(r);
}

void parseZoneIdentifier(QueryData& results,
const std::string& path,
const std::string& streamData) {
auto lines = split(streamData, "\n");
for (const auto& line : lines) {
auto key_len = line.find_first_of("=");
if (key_len == std::string::npos) {
continue;
}

setRow(results,
path,
line.substr(0, key_len),
line.substr(key_len + 1, line.size()));
}
}

// Process a file and extract all stream names and data.
void enumerateStreams(QueryData& results, const std::string& path) {
WIN32_FIND_STREAM_DATA findStreamData;
HANDLE hFind = FindFirstStreamW(stringToWstring(path).c_str(),
FindStreamInfoStandard,
&findStreamData,
0);

auto fd_guard = scope_guard::create([&] { FindClose(hFind); });

bool isFirstStream = true;

if (hFind != INVALID_HANDLE_VALUE) {
do {
// Skip first stream
if (isFirstStream) {
isFirstStream = false;
continue;
}
std::string stream(wstringToString(findStreamData.cStreamName));

// Split the stream string into a name and a type, format is
// ":streamname:$streamtype"
size_t splitPos = stream.find(':', 1);
std::string streamName = stream.substr(1, splitPos - 1);

std::stringstream streamPath;
streamPath << path << ":" << streamName;
std::string streamData;

if (!readFile(streamPath.str(), streamData).ok()) {
LOG(INFO) << "Couldn't read stream data: " << streamPath.str();
return;
}

if (streamName == kZoneIdentifierKey) {
parseZoneIdentifier(results, path, streamData);
} else {
// Remove trailing newlines
boost::trim_right(streamData);
setRow(results, path, streamName, streamData);
}
} while (FindNextStreamW(hFind, &findStreamData));
} else {
auto error_code = GetLastError();
if (error_code != ERROR_HANDLE_EOF) {
LOG(INFO) << "Error occurred while searching for streams in " << path
<< ". Error code: " << error_code;
}
}
}

QueryData genAds(QueryContext& context) {
QueryData results;
// Resolve file paths for EQUALS and LIKE operations.
auto paths = context.constraints["path"].getAll(EQUALS);
context.expandConstraints(
"path",
LIKE,
paths,
([&](const std::string& pattern, std::set<std::string>& out) {
std::vector<std::string> patterns;
auto status =
resolveFilePattern(pattern, patterns, GLOB_ALL | GLOB_NO_CANON);
if (status.ok()) {
for (const auto& resolved : patterns) {
out.insert(resolved);
}
}
return status;
}));

for (const auto& path_string : paths) {
boost::filesystem::path path = path_string;
boost::system::error_code ec;
// Folders can have ADS streams too
if (!(boost::filesystem::is_regular_file(path, ec) ||
boost::filesystem::is_directory(path, ec))) {
continue;
}
enumerateStreams(results, path.string());
}

// Resolve directories for EQUALS and LIKE operations.
auto directories = context.constraints["directory"].getAll(EQUALS);
context.expandConstraints(
"directory",
LIKE,
directories,
([&](const std::string& pattern, std::set<std::string>& out) {
std::vector<std::string> patterns;
auto status =
resolveFilePattern(pattern, patterns, GLOB_FOLDERS | GLOB_NO_CANON);
if (status.ok()) {
for (const auto& resolved : patterns) {
out.insert(resolved);
}
}
return status;
}));

// Now loop through constraints using the directory column constraint.
for (const auto& directory_string : directories) {
if (!isReadable(directory_string) || !isDirectory(directory_string)) {
continue;
}

std::vector<std::string> files;
if (listFilesInDirectory(directory_string, files).ok()) {
for (const auto& file : files) {
enumerateStreams(results, file);
}
}
}
return results;
}
} // namespace tables
} // namespace osquery
1 change: 1 addition & 0 deletions specs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ function(generateNativeTables)
"sleuthkit/device_hash.table:linux,macos,windows"
"sleuthkit/device_partitions.table:linux,macos,windows"
"user_groups.table:linux,macos,windows"
"windows/ads.table:windows"
"windows/background_activities_moderator.table:windows"
"windows/bitlocker_info.table:windows"
"windows/chassis_info.table:windows"
Expand Down
13 changes: 13 additions & 0 deletions specs/windows/ads.table
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
table_name("ads")
description("Returns the stream names and values for files (similar to posix extended attributes).")
schema([
Column("path", TEXT, "Absolute file path", required=True),
Column("directory", TEXT, "Directory of file(s)", required=True),
Column("key", TEXT, "Name of the value generated from the stream"),
Column("value", TEXT, "The parsed information from the attribute"),
Column("base64", INTEGER, "1 if the value is base64 encoded else 0"),
])
implementation("system/windows/ads@genAds")
examples([
"select * from ads where path = 'C:\\Users\\admin\\Downloads\\test.exe'"
])
1 change: 1 addition & 0 deletions tests/integration/tables/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ function(generateTestsIntegrationTablesTestsTest)

elseif(DEFINED PLATFORM_WINDOWS)
set(platform_source_files
ads.cpp
appcompat_shims.cpp
arp_cache.cpp
authenticode.cpp
Expand Down
37 changes: 37 additions & 0 deletions tests/integration/tables/ads.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2014-present, The osquery authors
*
* This source code is licensed as defined by the LICENSE file found in the
* root directory of this source tree.
*
* SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only)
*/

// Sanity check integration test for ads
// Spec file: specs/windows/ads.table

#include <osquery/tests/integration/tables/helper.h>

namespace osquery {
namespace table_tests {

class ads : public testing::Test {
protected:
void SetUp() override {
setUpEnvironment();
}
};

TEST_F(ads, test_sanity) {

auto const data = execute_query("select * from ads where path = ''");

ValidationMap row_map =
{ {"path", NormalType} {"directory", NormalType} {"key", NormalType} {
"value", NormalType} {"base64", IntType} }

validate_rows(data, row_map);
}

} // namespace table_tests
} // namespace osquery

0 comments on commit 4f3eec5

Please sign in to comment.