2017-12-19 00:04:06 +00:00
|
|
|
/**
|
2017-04-21 02:00:26 +00:00
|
|
|
* Copyright (c) 2014-present, Facebook, Inc.
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
2017-12-19 00:04:06 +00:00
|
|
|
* This source code is licensed under both the Apache 2.0 license (found in the
|
|
|
|
* LICENSE file in the root directory of this source tree) and the GPLv2 (found
|
|
|
|
* in the COPYING file in the root directory of this source tree).
|
|
|
|
* You may select, at your option, one of the above-listed licenses.
|
2017-04-21 02:00:26 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
#ifdef WIN32
|
|
|
|
#define _WIN32_DCOM
|
2018-01-07 04:26:36 +00:00
|
|
|
|
2017-04-21 02:00:26 +00:00
|
|
|
#include <Windows.h>
|
|
|
|
#endif
|
|
|
|
|
2017-10-23 05:00:29 +00:00
|
|
|
#include <boost/algorithm/string.hpp>
|
2017-04-21 02:00:26 +00:00
|
|
|
|
2017-08-20 09:44:38 +00:00
|
|
|
#include <osquery/database.h>
|
2017-05-26 18:19:43 +00:00
|
|
|
#include <osquery/distributed.h>
|
2017-04-21 02:00:26 +00:00
|
|
|
#include <osquery/flags.h>
|
|
|
|
#include <osquery/logger.h>
|
2017-05-26 18:19:43 +00:00
|
|
|
#include <osquery/system.h>
|
2017-04-21 02:00:26 +00:00
|
|
|
|
|
|
|
#include "osquery/carver/carver.h"
|
|
|
|
#include "osquery/core/conversions.h"
|
2018-05-02 01:54:23 +00:00
|
|
|
#include "osquery/core/hashing.h"
|
2017-04-21 02:00:26 +00:00
|
|
|
#include "osquery/core/json.h"
|
|
|
|
#include "osquery/filesystem/fileops.h"
|
|
|
|
#include "osquery/remote/serializers/json.h"
|
|
|
|
#include "osquery/remote/utility.h"
|
|
|
|
|
|
|
|
namespace fs = boost::filesystem;
|
|
|
|
|
|
|
|
namespace osquery {
|
|
|
|
|
|
|
|
DECLARE_string(tls_hostname);
|
|
|
|
|
|
|
|
/// Session creation endpoint for forensic file carve
|
|
|
|
CLI_FLAG(string,
|
|
|
|
carver_start_endpoint,
|
|
|
|
"",
|
|
|
|
"TLS/HTTPS init endpoint for forensic carver");
|
|
|
|
|
|
|
|
/// Data aggregation endpoint for forensic file carve
|
|
|
|
CLI_FLAG(
|
|
|
|
string,
|
|
|
|
carver_continue_endpoint,
|
|
|
|
"",
|
|
|
|
"TLS/HTTPS endpoint that receives carved content after session creation");
|
|
|
|
|
|
|
|
/// Size of blocks used for POSTing data back to remote endpoints
|
|
|
|
CLI_FLAG(uint32,
|
|
|
|
carver_block_size,
|
|
|
|
8192,
|
|
|
|
"Size of blocks used for POSTing data back to remote endpoints");
|
|
|
|
|
|
|
|
CLI_FLAG(bool,
|
|
|
|
disable_carver,
|
|
|
|
true,
|
|
|
|
"Disable the osquery file carver (default true)");
|
|
|
|
|
2017-05-26 18:19:43 +00:00
|
|
|
CLI_FLAG(bool,
|
|
|
|
carver_disable_function,
|
|
|
|
FLAGS_disable_carver,
|
|
|
|
"Disable the osquery file carver function (default true)");
|
2017-04-21 02:00:26 +00:00
|
|
|
|
2017-07-31 18:11:45 +00:00
|
|
|
CLI_FLAG(bool,
|
|
|
|
carver_compression,
|
|
|
|
false,
|
|
|
|
"Compress archives using zstd prior to upload (default false)");
|
|
|
|
|
2017-06-30 05:13:09 +00:00
|
|
|
DECLARE_uint64(read_max);
|
|
|
|
|
2017-04-21 02:00:26 +00:00
|
|
|
/// Helper function to update values related to a carve
|
|
|
|
void updateCarveValue(const std::string& guid,
|
|
|
|
const std::string& key,
|
|
|
|
const std::string& value) {
|
|
|
|
std::string carve;
|
|
|
|
auto s = getDatabaseValue(kCarveDbDomain, kCarverDBPrefix + guid, carve);
|
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to update status of carve in database " << guid;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
JSON tree;
|
|
|
|
s = tree.fromString(carve);
|
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to parse carve entries: " << s.what();
|
2017-04-21 02:00:26 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
tree.add(key, value);
|
|
|
|
|
|
|
|
std::string out;
|
|
|
|
s = tree.toString(out);
|
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to serialize carve entries: " << s.what();
|
|
|
|
}
|
2017-04-21 02:00:26 +00:00
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
s = setDatabaseValue(kCarveDbDomain, kCarverDBPrefix + guid, out);
|
2017-04-21 02:00:26 +00:00
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to update status of carve in database " << guid;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-05 22:14:21 +00:00
|
|
|
Carver::Carver(const std::set<std::string>& paths,
|
|
|
|
const std::string& guid,
|
2017-10-25 02:55:05 +00:00
|
|
|
const std::string& requestId)
|
|
|
|
: InternalRunnable("Carver") {
|
2017-04-21 02:00:26 +00:00
|
|
|
status_ = Status(0, "Ok");
|
|
|
|
for (const auto& p : paths) {
|
|
|
|
carvePaths_.insert(fs::path(p));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct the uri we post our data back to:
|
|
|
|
startUri_ = TLSRequestHelper::makeURI(FLAGS_carver_start_endpoint);
|
|
|
|
contUri_ = TLSRequestHelper::makeURI(FLAGS_carver_continue_endpoint);
|
|
|
|
|
|
|
|
// Generate a unique identifier for this carve
|
|
|
|
carveGuid_ = guid;
|
|
|
|
|
2017-05-05 22:14:21 +00:00
|
|
|
// Stash the work ID to be POSTed with the carve initial request
|
|
|
|
requestId_ = requestId;
|
|
|
|
|
2017-04-21 02:00:26 +00:00
|
|
|
// TODO: Adding in a manifest file of all carved files might be nice.
|
|
|
|
carveDir_ =
|
|
|
|
fs::temp_directory_path() / fs::path(kCarvePathPrefix + carveGuid_);
|
|
|
|
auto ret = fs::create_directory(carveDir_);
|
|
|
|
if (!ret) {
|
|
|
|
status_ = Status(1, "Failed to create carve file store");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store the path to our archive for later exfiltration
|
2017-05-23 19:42:27 +00:00
|
|
|
archivePath_ = carveDir_ / fs::path(kCarveNamePrefix + carveGuid_ + ".tar");
|
2017-07-31 18:11:45 +00:00
|
|
|
compressPath_ =
|
|
|
|
carveDir_ / fs::path(kCarveNamePrefix + carveGuid_ + ".tar.zst");
|
2017-04-21 02:00:26 +00:00
|
|
|
|
|
|
|
// Update the DB to reflect that the carve is pending.
|
|
|
|
updateCarveValue(carveGuid_, "status", "PENDING");
|
|
|
|
};
|
|
|
|
|
|
|
|
Carver::~Carver() {
|
|
|
|
fs::remove_all(carveDir_);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Carver::start() {
|
|
|
|
// If status_ is not Ok, the creation of our tmp FS failed
|
|
|
|
if (!status_.ok()) {
|
|
|
|
LOG(WARNING) << "Carver has not been properly constructed";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (const auto& p : carvePaths_) {
|
2017-06-30 05:13:09 +00:00
|
|
|
// Ensure the file is a flat file on disk before carving
|
2017-09-25 04:24:31 +00:00
|
|
|
PlatformFile pFile(p, PF_OPEN_EXISTING | PF_READ);
|
2017-06-30 05:13:09 +00:00
|
|
|
if (!pFile.isValid() || isDirectory(p)) {
|
|
|
|
VLOG(1) << "File does not exist on disk or is subdirectory: " << p;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Status s = carve(p);
|
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to carve file " << p << " " << s.getMessage();
|
2017-04-21 02:00:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::set<fs::path> carvedFiles;
|
|
|
|
for (const auto& p : platformGlob((carveDir_ / "*").string())) {
|
|
|
|
carvedFiles.insert(fs::path(p));
|
|
|
|
}
|
|
|
|
|
2017-07-31 18:11:45 +00:00
|
|
|
auto s = archive(carvedFiles, archivePath_);
|
2017-04-21 02:00:26 +00:00
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to create carve archive: " << s.getMessage();
|
|
|
|
updateCarveValue(carveGuid_, "status", "ARCHIVE FAILED");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-07-31 18:11:45 +00:00
|
|
|
fs::path uploadPath;
|
|
|
|
if (FLAGS_carver_compression) {
|
|
|
|
uploadPath = compressPath_;
|
|
|
|
s = compress(archivePath_, compressPath_);
|
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to compress carve archive: " << s.getMessage();
|
|
|
|
updateCarveValue(carveGuid_, "status", "COMPRESS FAILED");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
uploadPath = archivePath_;
|
|
|
|
}
|
|
|
|
|
2017-09-25 04:24:31 +00:00
|
|
|
PlatformFile uploadFile(uploadPath, PF_OPEN_EXISTING | PF_READ);
|
2017-07-31 18:11:45 +00:00
|
|
|
updateCarveValue(carveGuid_, "size", std::to_string(uploadFile.size()));
|
|
|
|
|
|
|
|
std::string uploadHash =
|
|
|
|
(uploadFile.size() > FLAGS_read_max)
|
|
|
|
? "-1"
|
|
|
|
: hashFromFile(HashType::HASH_TYPE_SHA256, uploadPath.string());
|
|
|
|
if (uploadHash == "-1") {
|
|
|
|
VLOG(1)
|
|
|
|
<< "Archive file size exceeds read max, skipping integrity computation";
|
|
|
|
}
|
|
|
|
updateCarveValue(carveGuid_, "sha256", uploadHash);
|
|
|
|
|
|
|
|
s = postCarve(uploadPath);
|
2017-04-21 02:00:26 +00:00
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to post carve: " << s.getMessage();
|
|
|
|
updateCarveValue(carveGuid_, "status", "DATA POST FAILED");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Status Carver::carve(const boost::filesystem::path& path) {
|
2017-09-25 04:24:31 +00:00
|
|
|
PlatformFile src(path, PF_OPEN_EXISTING | PF_READ);
|
|
|
|
PlatformFile dst(carveDir_ / path.leaf(), PF_CREATE_NEW | PF_WRITE);
|
2017-04-21 02:00:26 +00:00
|
|
|
|
|
|
|
if (!dst.isValid()) {
|
|
|
|
return Status(1, "Destination tmp FS is not valid.");
|
|
|
|
}
|
|
|
|
|
|
|
|
auto blkCount = ceil(static_cast<double>(src.size()) /
|
|
|
|
static_cast<double>(FLAGS_carver_block_size));
|
|
|
|
|
|
|
|
std::vector<char> inBuff(FLAGS_carver_block_size, 0);
|
|
|
|
for (size_t i = 0; i < blkCount; i++) {
|
|
|
|
inBuff.clear();
|
|
|
|
auto bytesRead = src.read(inBuff.data(), FLAGS_carver_block_size);
|
2017-08-05 01:22:10 +00:00
|
|
|
if (bytesRead > 0) {
|
|
|
|
auto bytesWritten = dst.write(inBuff.data(), bytesRead);
|
|
|
|
if (bytesWritten < 0) {
|
|
|
|
return Status(1, "Error writing bytes to tmp fs");
|
|
|
|
}
|
2017-04-21 02:00:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Status(0, "Ok");
|
|
|
|
};
|
|
|
|
|
|
|
|
Status Carver::postCarve(const boost::filesystem::path& path) {
|
2018-03-01 00:36:24 +00:00
|
|
|
Request<TLSTransport, JSONSerializer> startRequest(startUri_);
|
2018-02-07 20:24:57 +00:00
|
|
|
startRequest.setOption("hostname", FLAGS_tls_hostname);
|
2017-04-21 02:00:26 +00:00
|
|
|
|
|
|
|
// Perform the start request to get the session id
|
2017-09-25 04:24:31 +00:00
|
|
|
PlatformFile pFile(path, PF_OPEN_EXISTING | PF_READ);
|
2017-04-21 02:00:26 +00:00
|
|
|
auto blkCount =
|
|
|
|
static_cast<size_t>(ceil(static_cast<double>(pFile.size()) /
|
|
|
|
static_cast<double>(FLAGS_carver_block_size)));
|
2018-03-01 00:36:24 +00:00
|
|
|
JSON startParams;
|
2017-04-21 02:00:26 +00:00
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
startParams.add("block_count", blkCount);
|
|
|
|
startParams.add("block_size", size_t(FLAGS_carver_block_size));
|
|
|
|
startParams.add("carve_size", pFile.size());
|
|
|
|
startParams.add("carve_id", carveGuid_);
|
|
|
|
startParams.add("request_id", requestId_);
|
|
|
|
startParams.add("node_key", getNodeKey("tls"));
|
2017-04-21 02:00:26 +00:00
|
|
|
|
|
|
|
auto status = startRequest.call(startParams);
|
|
|
|
if (!status.ok()) {
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The call succeeded, store the session id for future posts
|
2018-03-01 00:36:24 +00:00
|
|
|
JSON startRecv;
|
2017-04-21 02:00:26 +00:00
|
|
|
status = startRequest.getResponse(startRecv);
|
|
|
|
if (!status.ok()) {
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
auto it = startRecv.doc().FindMember("session_id");
|
|
|
|
if (it == startRecv.doc().MemberEnd()) {
|
2017-04-21 02:00:26 +00:00
|
|
|
return Status(1, "No session_id received from remote endpoint");
|
|
|
|
}
|
2018-03-01 00:36:24 +00:00
|
|
|
if (!it->value.IsString()) {
|
|
|
|
return Status(1, "Invalid session_id received from remote endpoint");
|
|
|
|
}
|
2017-04-21 02:00:26 +00:00
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
std::string session_id = it->value.GetString();
|
|
|
|
if (session_id.empty()) {
|
|
|
|
return Status(1, "Empty session_id received from remote endpoint");
|
|
|
|
}
|
|
|
|
|
|
|
|
Request<TLSTransport, JSONSerializer> contRequest(contUri_);
|
2018-02-07 20:24:57 +00:00
|
|
|
contRequest.setOption("hostname", FLAGS_tls_hostname);
|
2017-04-21 02:00:26 +00:00
|
|
|
for (size_t i = 0; i < blkCount; i++) {
|
|
|
|
std::vector<char> block(FLAGS_carver_block_size, 0);
|
|
|
|
auto r = pFile.read(block.data(), FLAGS_carver_block_size);
|
|
|
|
|
|
|
|
if (r != FLAGS_carver_block_size && r > 0) {
|
|
|
|
// resize the buffer to size we read as last block is likely smaller
|
|
|
|
block.resize(r);
|
|
|
|
}
|
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
JSON params;
|
|
|
|
params.add("block_id", i);
|
|
|
|
params.add("session_id", session_id);
|
|
|
|
params.add("request_id", requestId_);
|
|
|
|
params.add("data", base64Encode(std::string(block.begin(), block.end())));
|
2017-04-21 02:00:26 +00:00
|
|
|
|
|
|
|
// TODO: Error sending files.
|
|
|
|
status = contRequest.call(params);
|
|
|
|
if (!status.ok()) {
|
|
|
|
VLOG(1) << "Post of carved block " << i
|
|
|
|
<< " failed: " << status.getMessage();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateCarveValue(carveGuid_, "status", "SUCCESS");
|
|
|
|
return Status(0, "Ok");
|
|
|
|
};
|
2017-05-26 18:19:43 +00:00
|
|
|
|
|
|
|
Status carvePaths(const std::set<std::string>& paths) {
|
2018-03-01 00:36:24 +00:00
|
|
|
Status s;
|
2017-05-26 18:19:43 +00:00
|
|
|
auto guid = generateNewUUID();
|
2017-05-27 06:55:51 +00:00
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
JSON tree;
|
|
|
|
tree.add("carve_guid", guid);
|
|
|
|
tree.add("time", getUnixTime());
|
|
|
|
tree.add("status", "STARTING");
|
|
|
|
tree.add("sha256", "");
|
|
|
|
tree.add("size", -1);
|
2017-05-27 06:55:51 +00:00
|
|
|
|
2017-05-26 18:19:43 +00:00
|
|
|
if (paths.size() > 1) {
|
2018-03-01 00:36:24 +00:00
|
|
|
tree.add("path", boost::algorithm::join(paths, ","));
|
2017-05-26 18:19:43 +00:00
|
|
|
} else {
|
2018-03-01 00:36:24 +00:00
|
|
|
tree.add("path", *(paths.begin()));
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string out;
|
|
|
|
s = tree.toString(out);
|
|
|
|
if (!s.ok()) {
|
|
|
|
VLOG(1) << "Failed to serialize carve paths: " << s.what();
|
|
|
|
return s;
|
2017-05-26 18:19:43 +00:00
|
|
|
}
|
2017-05-27 06:55:51 +00:00
|
|
|
|
2018-03-01 00:36:24 +00:00
|
|
|
s = setDatabaseValue(kCarveDbDomain, kCarverDBPrefix + guid, out);
|
2017-05-26 18:19:43 +00:00
|
|
|
if (!s.ok()) {
|
|
|
|
return s;
|
|
|
|
} else {
|
|
|
|
auto requestId = Distributed::getCurrentRequestId();
|
|
|
|
Dispatcher::addService(std::make_shared<Carver>(paths, guid, requestId));
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|
2017-05-23 19:42:27 +00:00
|
|
|
} // namespace osquery
|