Implement ransomware canary

This commit is contained in:
Martin Raiber 2021-01-21 01:33:20 +01:00
parent 2740774c22
commit ad387460e8
15 changed files with 623 additions and 80 deletions

BIN
urbackup/canary.docx Normal file

Binary file not shown.

View File

@ -42,6 +42,7 @@
#include "../urbackupcommon/CompressedPipe2.h"
#include "../urbackupcommon/CompressedPipeZstd.h"
#include "../urbackupcommon/InternetServicePipe2.h"
#include "RansomwareCanary.h"
#include <memory.h>
#include <stdlib.h>
@ -1861,6 +1862,17 @@ void ClientConnector::updateSettings(const std::string &pData, const std::string
db->destroyQuery(q);
}
std::string ransomware_canary_paths;
std::string server_token;
if (new_settings->getValue("ransomware_canary_paths", &ransomware_canary_paths)
&& !ransomware_canary_paths.empty()
&& new_settings->getValue("server_token", &server_token)
&& !server_token.empty())
{
setupRansomwareCanaries(ransomware_canary_paths, server_token,
facet_id, group_offset);
}
std::auto_ptr<ISettingsReader> curr_settings(Server->createFileSettingsReader(settings_fn));
std::vector<std::string> critical_settings;
@ -3391,8 +3403,8 @@ bool ClientConnector::calculateFilehashesOnClient(const std::string& clientsubna
bool ClientConnector::getBackupDest(const std::string& clientsubname, std::string& dest, int facet_id)
{
dest = "raw-file://D:\\tmp\\btrfs.img";
return true;
/*dest = "raw-file://D:\\tmp\\btrfs.img";
return true;*/
if (facet_id == 0)
return false;

View File

@ -0,0 +1,345 @@
#include "RansomwareCanary.h"
#include <thread>
#include "../stringtools.h"
#include "../urbackupcommon/os_functions.h"
#include "../Interface/Server.h"
#include "../Interface/File.h"
#include "../urbackupcommon/glob.h"
#include "../common/miniz.h"
#include "clientdao.h"
#include "database.h"
#ifdef _WIN32
#include <aclapi.h>
#endif
struct SOwner
{
bool has_owner = false;
#ifdef _WIN32
PSID Owner = nullptr;
PSECURITY_DESCRIPTOR sec_d = nullptr;
~SOwner() {
if (sec_d != nullptr)
LocalFree(sec_d);
}
#endif
};
static bool getOwner(const std::string& fn, SOwner& owner)
{
PSID newOwner = nullptr;
PSECURITY_DESCRIPTOR new_sec_d = nullptr;
DWORD rc = GetNamedSecurityInfoW(Server->ConvertToWchar(os_file_prefix(fn)).c_str(),
SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &newOwner, NULL, NULL,
NULL, &new_sec_d);
if (rc == ERROR_SUCCESS)
{
if (owner.sec_d != nullptr)
LocalFree(owner.sec_d);
owner.Owner = newOwner;
owner.sec_d = new_sec_d;
owner.has_owner = true;
}
else if (new_sec_d != nullptr)
{
LocalFree(new_sec_d);
}
return rc == ERROR_SUCCESS;
}
#ifdef _WIN32
HRESULT ModifyPrivilege(
IN LPCTSTR szPrivilege,
IN BOOL fEnable)
{
HRESULT hr = S_OK;
TOKEN_PRIVILEGES NewState;
LUID luid;
HANDLE hToken = NULL;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken))
{
return ERROR_FUNCTION_FAILED;
}
if (!LookupPrivilegeValue(NULL,
szPrivilege,
&luid))
{
CloseHandle(hToken);
return ERROR_FUNCTION_FAILED;
}
NewState.PrivilegeCount = 1;
NewState.Privileges[0].Luid = luid;
NewState.Privileges[0].Attributes =
(fEnable ? SE_PRIVILEGE_ENABLED : 0);
if (!AdjustTokenPrivileges(hToken,
FALSE,
&NewState,
0,
NULL,
NULL))
{
hr = ERROR_FUNCTION_FAILED;
}
CloseHandle(hToken);
return hr;
}
#endif
static bool setOwner(const std::string& fn, const SOwner& owner)
{
if (!owner.has_owner || owner.Owner == nullptr)
return false;
HRESULT hr = ModifyPrivilege(SE_TAKE_OWNERSHIP_NAME, TRUE);
if (!SUCCEEDED(hr))
return false;
std::wstring wfn = Server->ConvertToWchar(os_file_prefix(fn)).c_str();
DWORD rc = SetNamedSecurityInfoW(&wfn[0],
SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, owner.Owner, NULL, NULL,
NULL);
return rc == ERROR_SUCCESS;
}
static size_t zipWrite(void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n)
{
IFsFile* out_file = reinterpret_cast<IFsFile*>(pOpaque);
return out_file->Write(file_ofs, reinterpret_cast<const char*>(pBuf), n);
}
static void setupRansomwareCanaryFile(const std::string& curr_path, const std::string& fn_prefix,
const std::string& server_token, SOwner& owner)
{
std::string out_fn = os_file_prefix(curr_path + fn_prefix + "-" + server_token + ".docx");
if (os_get_file_type(out_fn) != 0)
{
Server->Log("Ransomware canary at \"" + out_fn + "\" already exists");
return;
}
if (!curr_path.empty())
{
std::vector<SFile> sib_files = getFiles(curr_path.substr(0, curr_path.size()-1));
for (SFile& sib_file : sib_files)
{
if (getOwner(curr_path + sib_file.name, owner))
break;
}
}
const std::string canary_path = Server->getServerWorkingDir() + os_file_sep() +
"urbackup" + os_file_sep() + "canary.docx";
mz_zip_archive canary_doc = {};
if (!mz_zip_reader_init_file(&canary_doc, canary_path.c_str(), 0))
{
Server->Log("Error opening canary.docx file for extraction", LL_ERROR);
return;
}
ScopedDeleteFn delete_fn(out_fn+".new");
std::unique_ptr<IFsFile> out_file(Server->openFile(out_fn+".new", MODE_WRITE));
if (out_file.get() == nullptr)
{
Server->Log("Error opening canary out at \"" + out_fn + ".new\". " + os_last_error_str(), LL_ERROR);
return;
}
mz_zip_archive canary_doc_out = {};
canary_doc_out.m_pWrite = zipWrite;
canary_doc_out.m_pIO_opaque = out_file.get();
if (!mz_zip_writer_init(&canary_doc_out, 0))
{
Server->Log("Error init zip at \"" + out_fn + ".new\"", LL_ERROR);
return;
}
for (unsigned int i = 0, num_files = mz_zip_reader_get_num_files(&canary_doc); i < num_files; ++i)
{
std::vector<char> buf(100);
mz_zip_reader_get_filename(&canary_doc, i, buf.data(), buf.size());
std::string curr_fn = buf.data();
if (curr_fn == "word/document.xml")
{
size_t fsize;
void* buf = mz_zip_reader_extract_to_heap(&canary_doc, i, &fsize, 0);
if (buf == nullptr)
{
Server->Log("Error extracting word/document.xml", LL_ERROR);
return;
}
std::string bdata(reinterpret_cast<char*>(buf), fsize);
std::string uuid;
uuid.resize(16);
Server->secureRandomFill(&uuid[0], 16);
bdata = greplace("$RAND$", bytesToHex(uuid), bdata);
if (!mz_zip_writer_add_mem(&canary_doc_out, curr_fn.c_str(),
bdata.data(), bdata.size(), MZ_DEFAULT_COMPRESSION))
{
Server->Log("Error adding modified word/document.xml to doc", LL_ERROR);
return;
}
}
else
{
if (!mz_zip_writer_add_from_zip_reader(&canary_doc_out, &canary_doc, i))
{
Server->Log("Error adding file \"" + curr_fn + "\" to canary out doc", LL_ERROR);
return;
}
}
}
delete_fn.release();
out_file->Sync();
out_file.reset();
if (!os_rename_file(out_fn + ".new", out_fn))
{
Server->Log("Error renaming canary " + out_fn + ".new", LL_ERROR);
return;
}
setOwner(out_fn, owner);
}
static std::string getBackupPath(const std::string& name, int facet, int group)
{
ClientDAO clientdao(Server->getDatabase(Server->getThreadID(), URBACKUPDB_CLIENT));
std::vector<SBackupDir> backupdirs = clientdao.getBackupDirs();
for (SBackupDir& bd: backupdirs)
{
if (bd.facet != facet || bd.group != group)
continue;
if (bd.tname == name)
return bd.path;
}
return std::string();
}
static void setupRansomwareCanariesPath(const std::string& curr_path, const std::vector<std::string> path_components, size_t idx,
const std::string& server_token, int facet, int group, SOwner& owner)
{
if (idx >= path_components.size())
return;
bool last_comp = idx + 1 >= path_components.size();
std::string comp = path_components[idx];
bool create = false;
if (comp[0] == '^'
&& comp.find("*") == std::string::npos)
{
create = true;
comp = comp.substr(1);
}
if (idx == 0)
{
comp = getBackupPath(comp, facet, group);
if (comp.empty())
return;
}
if (!last_comp
&& comp.find("*") != std::string::npos)
{
std::vector<SFile> files = getFiles(curr_path);
for (size_t i = 0; i < files.size(); ++i)
{
if (files[i].isdir
&& amatch(files[i].name.c_str(), comp.c_str()))
{
getOwner(curr_path + files[i].name, owner);
setupRansomwareCanariesPath(curr_path + files[i].name + os_file_sep(),
path_components, idx + 1, server_token, facet, group, owner);
}
}
}
else
{
if (!create
&& !last_comp)
{
getOwner(curr_path + comp, owner);
}
if (!last_comp &&
create &&
!os_directory_exists(os_file_prefix(curr_path + comp)))
{
if (!os_create_dir(os_file_prefix(curr_path + comp)))
{
Server->Log("Error creating directory \"" + curr_path + comp +
"\" for ransomware canary. " + os_last_error_str(), LL_ERROR);
}
}
else if (last_comp)
{
setupRansomwareCanaryFile(curr_path,
comp, server_token, owner);
}
if(!last_comp)
setupRansomwareCanariesPath(curr_path + comp + os_file_sep(), path_components,
idx + 1, server_token, facet, group, owner);
}
}
static void setupRansomwareCanariesInt(const std::string& ransomware_canary_paths, const std::string& server_token,
int facet, int group)
{
std::vector<std::string> paths;
Tokenize(ransomware_canary_paths, paths, ";");
for (std::string path : paths)
{
std::vector<std::string> path_components;
Tokenize(path, path_components, "/");
if (!path_components.empty())
{
SOwner owner;
setupRansomwareCanariesPath(std::string(), path_components, 0, server_token, facet, group, owner);
}
}
}
void setupRansomwareCanaries(const std::string& ransomware_canary_paths, const std::string& server_token,
int facet, int group)
{
std::thread t([ransomware_canary_paths, server_token, facet, group]()
{
setupRansomwareCanariesInt(ransomware_canary_paths, server_token, facet, group);
});
t.detach();
}

View File

@ -0,0 +1,5 @@
#pragma once
#include <string>
void setupRansomwareCanaries(const std::string& ransomware_canary_paths,
const std::string& server_token, int facet, int group);

View File

@ -68,6 +68,7 @@
<ClCompile Include="LocalFullFileBackup.cpp" />
<ClCompile Include="ParallelHash.cpp" />
<ClCompile Include="PersistentOpenFiles.cpp" />
<ClCompile Include="RansomwareCanary.cpp" />
<ClCompile Include="RestoreDownloadThread.cpp" />
<ClCompile Include="RestoreFiles.cpp" />
<ClCompile Include="ServerIdentityMgr.cpp" />
@ -128,6 +129,7 @@
<ClInclude Include="LocalFullFileBackup.h" />
<ClInclude Include="ParallelHash.h" />
<ClInclude Include="PersistentOpenFiles.h" />
<ClInclude Include="RansomwareCanary.h" />
<ClInclude Include="RestoreDownloadThread.h" />
<ClInclude Include="RestoreFiles.h" />
<ClInclude Include="ServerIdentityMgr.h" />

View File

@ -111,6 +111,7 @@ std::vector<std::string> getSettingsList(void)
ret.push_back("hash_threads");
ret.push_back("client_hash_threads");
ret.push_back("image_compress_threads");
ret.push_back("ransomware_canary_paths");
return ret;
}
@ -160,6 +161,7 @@ std::vector<std::string> getClientMergableSettingsList()
ret.push_back("image_letters");
ret.push_back("vss_select_components");
ret.push_back("archive");
ret.push_back("ransomware_canary_paths");
return ret;
}
@ -199,6 +201,7 @@ std::vector<std::string> getOnlyServerClientSettingsList(void)
ret.push_back("alert_params");
ret.push_back("archive");
ret.push_back("client_settings_tray_access_pw");
ret.push_back("ransomware_canary_paths");
return ret;
}

View File

@ -1933,6 +1933,7 @@ void ClientMain::sendSettings(void)
}
}
}
s_settings += "server_token=" + server_token + "\n";
escapeClientMessage(s_settings);
if(!sendClientMessage("SETTINGS "+s_settings, "OK", "Sending settings to client failed", 10000))
{

View File

@ -40,6 +40,7 @@
#include "../urbackupcommon/TreeHash.h"
#include "../common/data.h"
#include "PhashLoad.h"
#include "../urbackupcommon/glob.h"
#ifndef NAME_MAX
#define NAME_MAX _POSIX_NAME_MAX
@ -865,6 +866,12 @@ bool FileBackup::doBackup()
client_main->sendMailToAdmins("Fatal error occurred during backup", ServerLogger::getWarningLevelTextLogdata(logid));
}
if (!checkRansomwareCanaries())
{
ServerLogger::Log(logid, "Ransomware canary check failed", LL_ERROR);
backup_result = false;
}
if((!has_early_error && !backup_result) || disk_error)
{
backup_result = false;
@ -2504,6 +2511,151 @@ bool FileBackup::stopPhashDownloadThread(const std::string& async_id)
return true;
}
bool FileBackup::checkRansomwareCanaryFile(const std::string& last_backuppath, const std::string& curr_path, const std::string& fn_prefix)
{
std::string canary_path = curr_path + fn_prefix + "-" + server_token + ".docx";
std::string curr_canary_path = backuppath + os_file_sep() + canary_path;
std::string last_canary_path = last_backuppath + os_file_sep() + canary_path;
std::unique_ptr<IFile> lastf(Server->openFile(last_canary_path, MODE_READ));
if (lastf.get() == nullptr)
return true;
std::unique_ptr<IFile> currf(Server->openFile(curr_canary_path, MODE_READ));
if (currf.get() == nullptr)
{
ServerLogger::Log(logid, "Ransomware canary at \"" + canary_path + "\" is now missing", LL_ERROR);
return false;
}
std::vector<char> buf1(32768*2);
std::vector<char> buf2(32768 * 2);
while (true)
{
_u32 read1 = lastf->Read(buf1.data(), buf1.size());
_u32 read2 = currf->Read(buf2.data(), buf2.size());
if (read1 != read2)
{
ServerLogger::Log(logid, "Ransomware canary at \"" + canary_path + "\" has changed size", LL_ERROR);
return false;
}
if (read1 == 0)
return true;
if (memcmp(buf1.data(), buf2.data(), read1) != 0)
{
ServerLogger::Log(logid, "Ransomware canary at \"" + canary_path + "\" has changed", LL_ERROR);
return false;
}
}
}
bool FileBackup::checkRansomwareCanariesPath(const std::string& last_backuppath, const std::string& curr_path, const std::vector<std::string> path_components, size_t idx)
{
if (idx >= path_components.size())
return true;
bool last_comp = idx + 1 >= path_components.size();
std::string comp = path_components[idx];
bool create = false;
if (comp[0] == '^'
&& comp.find("*") == std::string::npos)
{
create = true;
comp = comp.substr(1);
}
if (comp=="." || comp == "..")
return false;
std::string curr_backuppath = backuppath + os_file_sep() + curr_path;
std::string last_path = last_backuppath + os_file_sep() + curr_path;
if (!last_comp
&& comp.find("*") != std::string::npos)
{
std::vector<SFile> files = getFiles(last_path);
for (size_t i = 0; i < files.size(); ++i)
{
if (idx == 0
&& files[i].isdir
&& files[i].name == ".hashes")
continue;
if (files[i].isdir
&& amatch(files[i].name.c_str(), comp.c_str()))
{
bool b = checkRansomwareCanariesPath(last_backuppath, curr_path + files[i].name + os_file_sep(),
path_components, idx + 1);
if (!b)
return false;
}
}
}
else
{
if (last_comp)
{
if (!checkRansomwareCanaryFile(last_backuppath, curr_path,
comp))
return false;
}
if (!last_comp)
{
bool b = checkRansomwareCanariesPath(last_backuppath, curr_path + comp + os_file_sep(), path_components, idx + 1);
if (!b)
return false;
}
}
return true;
}
bool FileBackup::checkRansomwareCanariesInt(const std::string& last_backuppath, const std::string& ransomware_canary_paths)
{
std::vector<std::string> paths;
Tokenize(ransomware_canary_paths, paths, ";");
for (std::string path : paths)
{
std::vector<std::string> path_components;
Tokenize(path, path_components, "/");
if (!path_components.empty())
{
bool b = checkRansomwareCanariesPath(last_backuppath, std::string(), path_components, 0);
if (!b)
return false;
}
}
return true;
}
bool FileBackup::checkRansomwareCanaries()
{
if (server_settings->getSettings()->ransomware_canary_paths.empty())
return true;
ServerBackupDao::SLastIncremental last = backup_dao->getLastIncrementalCompleteFileBackup(clientid, group);
if (!last.exists)
return true;
std::string last_backuppath = server_settings->getSettings()->backupfolder + os_file_sep() + clientname + os_file_sep() + last.path;
return checkRansomwareCanariesInt(last_backuppath, server_settings->getSettings()->ransomware_canary_paths);
}
void FileBackup::save_debug_data(const std::string& rfn, const std::string& local_hash, const std::string& remote_hash)
{
ServerLogger::Log(logid, "Local hash: "+local_hash+" remote hash: "+remote_hash, LL_INFO);

View File

@ -250,6 +250,10 @@ protected:
bool loadWindowsBackupComponentConfigXml(FileClient &fc);
bool startPhashDownloadThread(const std::string& async_id);
bool stopPhashDownloadThread(const std::string& async_id);
bool checkRansomwareCanaries();
bool checkRansomwareCanariesInt(const std::string& last_backuppath, const std::string& ransomware_canary_paths);
bool checkRansomwareCanariesPath(const std::string& last_backuppath, const std::string& curr_path, const std::vector<std::string> path_components, size_t idx);
bool checkRansomwareCanaryFile(const std::string& last_backuppath, const std::string& curr_path, const std::string& fn_prefix);
int group;
bool use_tmpfiles;

View File

@ -578,6 +578,8 @@ void ServerSettings::readSettingsDefault(ISettingsReader* settings_default,
readIntClientSetting(q_get_client_setting, "client_hash_threads", &settings->client_hash_threads, false);
settings->image_compress_threads = 0;
readIntClientSetting(q_get_client_setting, "image_compress_threads", &settings->image_compress_threads, false);
readStringClientSetting(q_get_client_setting, "ransomware_canary_paths", std::string(";"), &settings->ransomware_canary_paths, false);
}
void ServerSettings::readSettingsClient(ISettingsReader* settings_client, IQuery* q_get_client_setting)
@ -701,6 +703,8 @@ void ServerSettings::readSettingsClient(ISettingsReader* settings_client, IQuery
readIntClientSetting(q_get_client_setting, "hash_threads", &settings->hash_threads, false);
readIntClientSetting(q_get_client_setting, "client_hash_threads", &settings->client_hash_threads, false);
readIntClientSetting(q_get_client_setting, "image_compress_threads", &settings->image_compress_threads, false);
readStringClientSetting(q_get_client_setting, "ransomware_canary_paths", ";", &settings->ransomware_canary_paths, false);
}
void ServerSettings::readStringClientSetting(IQuery * q_get_client_setting, int clientid, const std::string & name, const std::string & merge_sep, std::string * output, bool allow_client_value)

View File

@ -150,6 +150,7 @@ struct SSettings
int hash_threads;
int client_hash_threads;
int image_compress_threads;
std::string ransomware_canary_paths;
};
struct SLDAPSettings

View File

@ -276,6 +276,7 @@ JSON::Object getJSONClientSettings(IDatabase *db, int t_clientid)
SET_SETTING_INT(hash_threads);
SET_SETTING_INT(client_hash_threads);
SET_SETTING_INT(image_compress_threads);
SET_SETTING_STR(ransomware_canary_paths);
#undef SET_SETTING
return ret;
}
@ -351,6 +352,7 @@ void getGeneralSettings(JSON::Object& obj, IDatabase *db, ServerSettings &settin
SET_SETTING_DB(restore_authkey, std::string());
SET_SETTING_DB(internet_expect_endpoint, std::string());
SET_SETTING_DB(internet_server_bind_port, std::string());
SET_SETTING_DB(ransomware_canary_paths, std::string());
#undef SET_SETTING
#undef SET_SETTING_DB
}

File diff suppressed because one or more lines are too long

View File

@ -4178,7 +4178,8 @@ g.settings_list=[
"download_threads",
"hash_threads",
"client_hash_threads",
"image_compress_threads"
"image_compress_threads",
"ransomware_canary_paths"
];
g.general_settings_list=[
"backupfolder",
@ -4240,7 +4241,8 @@ g.mergable_settings_list=[
"default_dirs",
"image_letters",
"vss_select_components",
"archive"
"archive",
"ransomware_canary_paths"
];
g.client_settings_list=[
"update_freq_incr",

View File

@ -93,6 +93,16 @@
</div>
<div id="backup_dirs_optional_sw" style="display: inline"></div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="ransomware_canary_paths">{tRansomware canary paths:}</label>
<div class="col-sm-6">
<div class="input-group">
<input type="text" class="form-control" id="ransomware_canary_paths" value="{ransomware_canary_paths|s}"/>
<div class="input-group-addon"><a href="help.htm#ransomware_canary_paths" target="_blank">?</a></div>
</div>
</div>
<div id="ransomware_canary_paths_sw" style="display: inline"></div>
</div>
</form>
</div>
</div>