mirror of
https://github.com/evilele/EDR-Introspection
synced 2026-06-09 08:11:09 +00:00
748 lines
26 KiB
C++
748 lines
26 KiB
C++
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#include <chrono>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
#include <random>
|
|
#include <regex>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <shared_mutex>
|
|
#include <shlwapi.h>
|
|
#include <tlhelp32.h> // import after windows.h, else all breaks, that's crazy, yo
|
|
|
|
#include "globals.h"
|
|
#include "utils.h"
|
|
|
|
#pragma comment(lib, "Shlwapi.lib")
|
|
|
|
|
|
static const std::string encrypt_password = "much signature bypass, such wow";
|
|
static std::wstring attacks_subfolder = L"attacks\\";
|
|
static const std::string enc_attack_suffix = ".exe.enc";
|
|
|
|
static const std::string dumps_relative_path = "..\\..\\EDRi\\dumps\\";
|
|
static const std::string defender2yara_relative_path = "..\\..\\EDRi\\defender2yara\\";
|
|
static const std::string defender2yara_sigs_url = "https://github.com/t-tani/defender2yara/tree/yara-rules";
|
|
|
|
static const std::string outTypeEvent = "events\\";
|
|
static const std::string outTypeSigs = "signatures\\";
|
|
|
|
static const std::string edr_hooker_base_name = "EDRReflectiveHooker";
|
|
|
|
const std::string hooked_procs_file = "C:\\Users\\Public\\Downloads\\hooked.txt"; // should match the path in EDRReflectiveHooker (dllmain.cpp)
|
|
|
|
static bool initialized_snapshot = false;
|
|
|
|
static std::shared_mutex procs_mutex;
|
|
|
|
// get unix epoch time in nanoseconds (100 ns resolution)
|
|
UINT64 get_ns_time() {
|
|
/*
|
|
ChronoVsFiletime.exe:
|
|
[*] Timing 1000000000 calls each...
|
|
5.59516 ns per call - GetSystemTimeAsFileTime
|
|
26.9772 ns per call - GetSystemTimePreciseAsFileTime
|
|
23.9806 ns per call - chrono::system_clock::now()
|
|
*/
|
|
auto now = std::chrono::system_clock::now();
|
|
return std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
|
|
}
|
|
|
|
// thread-safe storing ProcInfo to global variable, processes found here are assumed to be running since t=0
|
|
void snapshot_procs() {
|
|
initialized_snapshot = true;
|
|
PROCESSENTRY32 pe;
|
|
pe.dwSize = sizeof(PROCESSENTRY32);
|
|
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (Process32First(snapshot, &pe)) {
|
|
std::unique_lock<std::shared_mutex> lock(procs_mutex); // writer lock (one allowed, no readers)
|
|
do {
|
|
std::string exe = wchar2string(pe.szExeFile);
|
|
int pid = pe.th32ProcessID;
|
|
bool to_track = false; // check if this is a process to be tracked
|
|
for (auto& e : g_exes_to_track) {
|
|
if (_stricmp(exe.c_str(), e.c_str()) == 0) {
|
|
to_track = true;
|
|
}
|
|
}
|
|
ProcInfo pi = { pid, MIN_PROC_START, MAX_PROC_END, exe, to_track };
|
|
g_running_procs.push_back(pi);
|
|
} while (Process32Next(snapshot, &pe));
|
|
}
|
|
}
|
|
|
|
// thread-safe retrieving the PID of the first case-insensitive match, ignores other same-named processes
|
|
std::vector<int> get_PID_by_name(const std::string& name, UINT64 timestamp_ns) {
|
|
if (!initialized_snapshot) {
|
|
std::cerr << "[!] Utils: Cannot use get_PID_by_name() before snapshot_procs()\n";
|
|
return {};
|
|
}
|
|
std::vector<int> procs = {};
|
|
std::shared_lock<std::shared_mutex> lock(procs_mutex); // reader lock (multiple allowed when no writers)
|
|
for (auto it = g_running_procs.begin(); it != g_running_procs.end(); ++it) {
|
|
if (_stricmp(it->name.c_str(), name.c_str()) == 0) { // check if name matches
|
|
if (it->start_time < timestamp_ns && it->end_time > timestamp_ns) { // check if timestamp is inside start and end
|
|
procs.push_back(it->PID);
|
|
}
|
|
}
|
|
}
|
|
return procs;
|
|
}
|
|
|
|
// thread-safe adding a proc
|
|
void add_proc(int pid, const std::string& name, UINT64 timestamp_ns, bool to_track) {
|
|
if (!initialized_snapshot) {
|
|
if (g_debug) {
|
|
std::cout << "[~] Utils: New proc " << pid << ":" << name << " started before snapshot was taken";
|
|
std::cout << ", will be ignored here and just added in the snapshot_procs()\n";
|
|
}
|
|
return;
|
|
}
|
|
std::unique_lock<std::shared_mutex> lock(procs_mutex); // writer lock (one allowed, no readers)
|
|
ProcInfo pi = { pid, timestamp_ns, MAX_PROC_END, name, to_track };
|
|
g_running_procs.push_back(pi);
|
|
if (g_debug) {
|
|
std::cout << "[+] Utils: New proc started at runtime: " << pid << ":" << name << "\n";
|
|
}
|
|
}
|
|
|
|
// thread-safe marking the proc termination
|
|
void mark_termination(int pid, UINT64 timestamp_ns) {
|
|
std::unique_lock<std::shared_mutex> lock(procs_mutex); // writer lock (one allowed, no readers)
|
|
ProcInfo* latest_proc = nullptr;
|
|
for (auto it = g_running_procs.begin(); it != g_running_procs.end(); ++it) {
|
|
if (it->PID == pid) {
|
|
if (latest_proc == nullptr || it->start_time > latest_proc->start_time) {
|
|
latest_proc = &(*it);
|
|
}
|
|
}
|
|
}
|
|
if (latest_proc != nullptr) {
|
|
latest_proc->end_time = timestamp_ns;
|
|
}
|
|
else if (g_debug) {
|
|
std::cout << "[~] Utils: Cannot add termination for unregistered proc " << pid << " at " << unix_epoch_ns_to_iso8601(timestamp_ns) << "\n";
|
|
}
|
|
}
|
|
|
|
// retrieving a proc
|
|
std::string get_proc_name(int pid, UINT64 timestamp_ns, UINT64 buffer_ns) {
|
|
if (buffer_ns > MAX_BUFFER_NS) {
|
|
return PROC_NOT_FOUND;
|
|
}
|
|
|
|
std::vector<ProcInfo> potential_matches = {};
|
|
for (auto it = g_running_procs.begin(); it != g_running_procs.end(); ++it) {
|
|
if (it->PID == pid) { // check if pid matches
|
|
if (it->start_time <= timestamp_ns + buffer_ns && it->end_time >= timestamp_ns - buffer_ns) { // check if timestamp is inside start and end (with buffer)
|
|
potential_matches.push_back(*it);
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle 0,1,>1 potential matches
|
|
if (potential_matches.size() == 0) { // no procs found --> search again with more buffer, 0.1ms, 1ms, ... MAX_BUFFER_NS, +1 --> not found
|
|
if (buffer_ns == 0) {
|
|
buffer_ns = RESERVE_NS; // ensure that the buffer grows
|
|
}
|
|
return get_proc_name(pid, timestamp_ns, buffer_ns*10);
|
|
}
|
|
if (potential_matches.size() == 1) { // one proc found --> return it
|
|
return potential_matches.at(0).name;
|
|
}
|
|
if (potential_matches.size() > 1) { // X procs found --> return the process with the closest start / end time to the given timestamp_ns
|
|
UINT64 smallest_diff = MAX_PROC_END;
|
|
ProcInfo closest_proc = {0, 0, 0, PROC_NOT_FOUND, false};
|
|
for (auto it = potential_matches.begin(); it != potential_matches.end(); ++it) {
|
|
UINT64 diff_s = (it->start_time > timestamp_ns) ? it->start_time - timestamp_ns : timestamp_ns - it->start_time;
|
|
UINT64 diff_e = (it->end_time > timestamp_ns) ? it->end_time - timestamp_ns : timestamp_ns - it->end_time;
|
|
UINT64 diff = (diff_s < diff_e) ? diff_s : diff_e;
|
|
if (diff < smallest_diff) {
|
|
smallest_diff = diff;
|
|
closest_proc = *it;
|
|
}
|
|
}
|
|
return closest_proc.name;
|
|
}
|
|
return PROC_NOT_FOUND; // comfort the compiler: "not all control paths return a value"
|
|
}
|
|
|
|
// thread-safe retrieving all processes that were tracked
|
|
std::vector<ProcInfo> get_tracked_procs() {
|
|
std::shared_lock<std::shared_mutex> lock(procs_mutex); // reader lock (multiple allowed when no writers)
|
|
std::vector<ProcInfo> tracked = {};
|
|
std::string debug_s = " ";
|
|
for (auto it = g_running_procs.begin(); it != g_running_procs.end(); ++it) {
|
|
if (it->to_track) {
|
|
tracked.push_back(*it);
|
|
debug_s += it->name + ", ";
|
|
}
|
|
}
|
|
if (g_debug) {
|
|
std::cout << "[+] Utils: Got tracked procs:" << debug_s << "\n";
|
|
}
|
|
return tracked;
|
|
}
|
|
|
|
// check if unnecessary tools are running --> these inflate the output
|
|
std::string unnecessary_tools_running() {
|
|
UINT64 ns = get_ns_time();
|
|
std::string r = "";
|
|
if (!initialized_snapshot) {
|
|
std::cerr << "[!] Utils: Cannot use unnecessary_tools_running() before snapshot_procs()\n";
|
|
return r;
|
|
}
|
|
std::vector<std::string> procs = { "procexp64.exe", "taskmgr.exe", "mintty.exe" };
|
|
for (auto& p : procs) {
|
|
if (!get_PID_by_name(p, ns).empty()) {
|
|
r += p + " ";
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// dump all recorded procs
|
|
void dump_proc_map() {
|
|
std::cout << "[+] ------------------------ Running procs during the tests ------------------------\n";
|
|
// sort procs by PID
|
|
std::sort(g_running_procs.begin(), g_running_procs.end(), [](const ProcInfo& a, const ProcInfo& b) {
|
|
return a.PID < b.PID;
|
|
});
|
|
for (auto& p : g_running_procs) {
|
|
std::cout << std::setfill(' ') << std::setw(5) << p.PID << " : ";
|
|
std::cout << unix_epoch_ns_to_iso8601(p.start_time) << " - " << unix_epoch_ns_to_iso8601(p.end_time);
|
|
std::cout << " : " << p.to_track << "," << p.name << "\n";
|
|
}
|
|
std::cout << "[+] ------------------------ ------------------------------ ------------------------\n";
|
|
}
|
|
|
|
// get random number between 100...999
|
|
std::string get_random_3digit_num() {
|
|
static std::mt19937 rng(std::random_device{}()); // initialize once
|
|
std::uniform_int_distribution<int> dist(100, 999);
|
|
return std::to_string(dist(rng));
|
|
}
|
|
|
|
// encrypt/decrypt a file with a static password
|
|
bool xor_file(std::string in_path, std::string out_path, bool just_copy) {
|
|
// open input file in binary mode
|
|
std::ifstream infile(in_path, std::ios::binary);
|
|
if (!infile) {
|
|
std::cerr << "[!] Utils: Failed to open input file: " << in_path << "\n";
|
|
return false;
|
|
}
|
|
|
|
// read file into buffer
|
|
std::vector<char> buffer((std::istreambuf_iterator<char>(infile)),
|
|
std::istreambuf_iterator<char>());
|
|
infile.close();
|
|
|
|
if (!just_copy) { // xor encrypt with pre-defined password
|
|
for (size_t i = 0; i < buffer.size(); ++i) {
|
|
buffer[i] ^= encrypt_password[i % encrypt_password.size()];
|
|
}
|
|
}
|
|
|
|
// write encrypted data to output file
|
|
std::ofstream outfile(out_path, std::ios::binary);
|
|
if (!outfile) {
|
|
std::cerr << "[!] Utils: Failed to open output file: " << out_path << "\n";
|
|
return false;
|
|
}
|
|
outfile.write(buffer.data(), buffer.size());
|
|
outfile.close();
|
|
return true;
|
|
}
|
|
|
|
bool remove_file(const std::string& path) {
|
|
if (DeleteFileA(path.c_str()) == 0) {
|
|
std::cerr << "[!] Utils: Failed to delete file: " << path << ", error: " << GetLastError() << "\n";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// get the EDRi.exe's base path
|
|
std::wstring get_base_path() {
|
|
wchar_t buffer[MAX_PATH];
|
|
// Get the full path of the executable
|
|
DWORD length = GetModuleFileNameW(NULL, buffer, MAX_PATH);
|
|
if (length == 0) {
|
|
return L"";
|
|
}
|
|
|
|
std::wstring exePath(buffer);
|
|
size_t pos = exePath.find_last_of(L"\\/");
|
|
|
|
// Return the directory part of the executable path
|
|
return exePath.substr(0, pos + 1);
|
|
}
|
|
|
|
std::string get_hook_dll_path(bool minimal_hooks) {
|
|
std::wstring base_path = get_base_path();
|
|
std::string base_name = edr_hooker_base_name;
|
|
if (minimal_hooks) {
|
|
base_name += "-Min";
|
|
}
|
|
return wstring2string(base_path) + base_name + ".dll";
|
|
}
|
|
|
|
std::string resolve_path(std::string relative_path) {
|
|
std::wstring base_path = get_base_path();
|
|
std::string raw = wstring2string(base_path) + relative_path;
|
|
char out[MAX_PATH];
|
|
if (PathCanonicalizeA(out, raw.c_str())) {
|
|
return std::string(out);
|
|
}
|
|
return raw; // fallback if canonicalization fails
|
|
}
|
|
|
|
// calculates the absolute output path for a given name and the static dumps_relative_path
|
|
std::string get_output_path(std::string name, bool eventsType) {
|
|
std::string type;
|
|
std::string fileT;
|
|
if (eventsType) {
|
|
type = outTypeEvent;
|
|
fileT = ".csv";
|
|
}
|
|
else {
|
|
type = outTypeSigs;
|
|
fileT = ".txt";
|
|
}
|
|
return resolve_path(dumps_relative_path + type + name + fileT);
|
|
}
|
|
|
|
// returns the files from /EDRi/attacks
|
|
std::string get_available_attacks() {
|
|
std::ostringstream oss;
|
|
WIN32_FIND_DATA findData;
|
|
HANDLE hFind = INVALID_HANDLE_VALUE;
|
|
|
|
std::wstring searchPath = get_base_path() + attacks_subfolder + L"*";
|
|
hFind = FindFirstFile(searchPath.c_str(), &findData);
|
|
|
|
if (hFind == INVALID_HANDLE_VALUE) {
|
|
return ""; // Directory not found or empty
|
|
}
|
|
|
|
do {
|
|
// Skip directories like "." and ".."
|
|
if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
std::string f = wchar2string(findData.cFileName);
|
|
// check if file ends with correct suffix
|
|
if (f.size() > enc_attack_suffix.size() && std::equal(enc_attack_suffix.rbegin(), enc_attack_suffix.rend(), f.rbegin())) {
|
|
f = f.substr(0, f.find(enc_attack_suffix)); // remove .exe.enc
|
|
oss << "\n" << f;
|
|
}
|
|
}
|
|
} while (FindNextFile(hFind, &findData) != 0);
|
|
|
|
FindClose(hFind);
|
|
return oss.str();
|
|
}
|
|
|
|
|
|
bool is_attack_available(const std::string& attack) {
|
|
std::string attacks = get_available_attacks();
|
|
std::istringstream iss(attacks);
|
|
std::string token;
|
|
while (iss >> token) {
|
|
if (_stricmp(token.c_str(), attack.c_str()) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string get_attack_enc_path(const std::string& attack) {
|
|
std::wstring exe_path = get_base_path();
|
|
return wstring2string(exe_path) + wstring2string(attacks_subfolder) + attack + enc_attack_suffix;
|
|
}
|
|
|
|
std::string check_custum_attack_path(const std::string& path) {
|
|
size_t pos = path.find_last_of('\\');
|
|
if (pos == std::string::npos) {
|
|
return ""; // no backslash --> invalid path
|
|
}
|
|
|
|
std::string folder = path.substr(0, pos + 1);
|
|
std::string name = path.substr(pos + 1);
|
|
|
|
WIN32_FIND_DATAA findData;
|
|
HANDLE hFind = FindFirstFileA(path.c_str(), &findData); // check if file exists
|
|
|
|
if (hFind == INVALID_HANDLE_VALUE) {
|
|
return ""; // does not exist
|
|
}
|
|
|
|
FindClose(hFind);
|
|
return name;
|
|
}
|
|
|
|
|
|
// all stolen from https://github.com/dobin/RedEdr
|
|
std::string wchar2string(const wchar_t* wideString) {
|
|
if (!wideString) {
|
|
return "";
|
|
}
|
|
int sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, wideString, -1, nullptr, 0, nullptr, nullptr);
|
|
if (sizeNeeded <= 0) {
|
|
return "";
|
|
}
|
|
std::string ret(sizeNeeded - 1, 0);
|
|
WideCharToMultiByte(CP_UTF8, 0, wideString, -1, &ret[0], sizeNeeded, nullptr, nullptr);
|
|
return ret;
|
|
}
|
|
|
|
std::string wstring2string(std::wstring& wide_string) {
|
|
if (wide_string.empty()) {
|
|
return "";
|
|
}
|
|
|
|
// Determine the size needed for the UTF-8 buffer
|
|
int size_needed = WideCharToMultiByte(CP_UTF8, 0, wide_string.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
|
|
|
// Allocate the buffer and perform the conversion
|
|
std::string utf8_string(size_needed - 1, '\0'); // Exclude the null terminator
|
|
WideCharToMultiByte(CP_UTF8, 0, wide_string.c_str(), -1, &utf8_string[0], size_needed, nullptr, nullptr);
|
|
|
|
return utf8_string;
|
|
}
|
|
|
|
bool wstring_starts_with(const std::wstring& str, const std::wstring& prefix) {
|
|
if (str.size() < prefix.size()) {
|
|
return false;
|
|
}
|
|
return str.compare(0, prefix.size(), prefix) == 0;
|
|
}
|
|
|
|
UINT64 filetime_to_unix_epoch_ns(__int64 timestamp) {
|
|
if (timestamp < WINDOWS_TICKS_TO_UNIX_EPOCH) {
|
|
return 0; // before Unix epoch
|
|
}
|
|
uint64_t unix_ticks_100ns = static_cast<uint64_t>(timestamp) - WINDOWS_TICKS_TO_UNIX_EPOCH;
|
|
uint64_t unix_ns = unix_ticks_100ns * NS_PER_WINDOWS_TICK;
|
|
return unix_ns;
|
|
}
|
|
|
|
|
|
std::string filetime_to_iso8601(__int64 timestamp) {
|
|
// convert to FILETIME
|
|
FILETIME ft;
|
|
ft.dwLowDateTime = static_cast<DWORD>(timestamp & 0xFFFFFFFF);
|
|
ft.dwHighDateTime = static_cast<DWORD>(timestamp >> 32);
|
|
|
|
SYSTEMTIME stUTC;
|
|
if (!FileTimeToSystemTime(&ft, &stUTC)) {
|
|
return "";
|
|
}
|
|
|
|
// extract the fractional part
|
|
ULARGE_INTEGER uli;
|
|
uli.LowPart = ft.dwLowDateTime;
|
|
uli.HighPart = ft.dwHighDateTime;
|
|
|
|
const unsigned long long TICKS_PER_SEC = 10000000ULL; // 10M * 100ns
|
|
unsigned long long frac_ticks = uli.QuadPart % TICKS_PER_SEC;
|
|
double fractional = static_cast<double>(frac_ticks) / TICKS_PER_SEC;
|
|
|
|
// convert to fractional seconds and format
|
|
std::ostringstream oss;
|
|
oss << std::setfill('0')
|
|
<< std::setw(4) << stUTC.wYear << "-"
|
|
<< std::setw(2) << stUTC.wMonth << "-"
|
|
<< std::setw(2) << stUTC.wDay << " "
|
|
<< std::setw(2) << stUTC.wHour << ":"
|
|
<< std::setw(2) << stUTC.wMinute << ":"
|
|
<< std::setw(2) << stUTC.wSecond << "."
|
|
<< std::setw(7) << std::setfill('0') << static_cast<int>(fractional * 10000000ULL)
|
|
<< "Z";
|
|
|
|
return oss.str();
|
|
}
|
|
|
|
std::string unix_epoch_ns_to_iso8601(uint64_t ns_since_epoch)
|
|
{
|
|
using namespace std::chrono;
|
|
system_clock::duration duration = duration_cast<system_clock::duration>(nanoseconds(ns_since_epoch));
|
|
system_clock::time_point time_point(duration);
|
|
auto in_time_t = system_clock::to_time_t(time_point);
|
|
auto fractional = duration_cast<nanoseconds>(time_point.time_since_epoch()) % 1'000'000'000;
|
|
|
|
// Convert to UTC
|
|
std::tm tm_buf;
|
|
gmtime_s(&tm_buf, &in_time_t);
|
|
|
|
std::ostringstream oss;
|
|
oss << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S")
|
|
<< "." << std::setw(9) << std::setfill('0') << fractional.count()
|
|
<< "Z"; // UTC
|
|
|
|
return oss.str();
|
|
}
|
|
|
|
char* get_memory_region_protect(DWORD protect) {
|
|
const char* memoryProtect;
|
|
switch (protect) {
|
|
case PAGE_EXECUTE:
|
|
memoryProtect = "--X";
|
|
break;
|
|
case PAGE_EXECUTE_READ:
|
|
memoryProtect = "R-X";
|
|
break;
|
|
case PAGE_EXECUTE_READWRITE:
|
|
memoryProtect = "RWX";
|
|
break;
|
|
case PAGE_EXECUTE_WRITECOPY:
|
|
memoryProtect = "EXECUTE_WRITECOPY";
|
|
break;
|
|
case PAGE_NOACCESS:
|
|
memoryProtect = "NOACCESS";
|
|
break;
|
|
case PAGE_READONLY:
|
|
memoryProtect = "R--";
|
|
break;
|
|
case PAGE_READWRITE:
|
|
memoryProtect = "RW-";
|
|
break;
|
|
case PAGE_WRITECOPY:
|
|
memoryProtect = "WRITECOPY";
|
|
break;
|
|
case PAGE_GUARD:
|
|
memoryProtect = "GUARD";
|
|
break;
|
|
case PAGE_NOCACHE:
|
|
memoryProtect = "NOCACHE";
|
|
break;
|
|
case PAGE_WRITECOMBINE:
|
|
memoryProtect = "WRITECOMBINE";
|
|
break;
|
|
default:
|
|
memoryProtect = "Unknown";
|
|
break;
|
|
}
|
|
return (char*) memoryProtect;
|
|
}
|
|
|
|
bool filepath_match(std::string path1, std::string path2) {
|
|
auto normalize = [](const std::string& path) -> std::string {
|
|
std::string p = path;
|
|
|
|
// if it starts with "\Device\HarddiskVolumeX", remove it
|
|
const std::string device_prefix = "\\Device\\HarddiskVolume";
|
|
if (p.compare(0, device_prefix.size(), device_prefix) == 0) {
|
|
// find first backslash after volume number
|
|
size_t pos = p.find('\\', device_prefix.size());
|
|
if (pos != std::string::npos) {
|
|
p = p.substr(pos); // keep from first backslash
|
|
}
|
|
else {
|
|
p.clear(); // malformed path
|
|
}
|
|
}
|
|
// if it starts with "C:\" or other drive letter, remove it
|
|
else if (p.size() >= 3 && std::isalpha(p[0]) && p[1] == ':' && p[2] == '\\') {
|
|
p = p.substr(2); // keep from "\..."
|
|
}
|
|
// and lowercase all
|
|
std::transform(p.begin(), p.end(), p.begin(), [](unsigned char c) { return std::tolower(c); });
|
|
return p;
|
|
};
|
|
std::string norm1 = normalize(path1);
|
|
std::string norm2 = normalize(path2);
|
|
return norm1 == norm2;
|
|
}
|
|
|
|
bool launch_as_child(const std::string& path) {
|
|
std::wstring wpath(path.begin(), path.end());
|
|
|
|
SHELLEXECUTEINFOW sei = { sizeof(sei) };
|
|
sei.fMask = SEE_MASK_DEFAULT;
|
|
sei.hwnd = NULL;
|
|
sei.lpVerb = L"open";
|
|
sei.lpFile = wpath.c_str();
|
|
sei.nShow = SW_SHOWNORMAL;
|
|
|
|
if (!ShellExecuteExW(&sei)) {
|
|
DWORD err = GetLastError();
|
|
std::cerr << "[!] Utils: ShellExecuteEx failed: " << err << "\n";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// removes the hooked_procs_file if its out of date (last write time is before the last system boot time -> procs have changed)
|
|
void remove_if_out_of_date() {
|
|
if (GetFileAttributesA(hooked_procs_file.c_str()) == INVALID_FILE_ATTRIBUTES) {
|
|
if (g_debug)
|
|
std::cerr << "[-] Utils: " << hooked_procs_file << " not found\n";
|
|
return;
|
|
}
|
|
|
|
// system uptime -> last boot FILETIME
|
|
ULONGLONG uptimeFt = GetTickCount64() * 10000ULL;
|
|
FILETIME nowFt; GetSystemTimeAsFileTime(&nowFt);
|
|
ULONGLONG now = ((ULONGLONG)nowFt.dwHighDateTime << 32) | nowFt.dwLowDateTime;
|
|
ULONGLONG boot = now - uptimeFt;
|
|
|
|
// get file write time
|
|
HANDLE h = CreateFileA(hooked_procs_file.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, 0);
|
|
if (h == INVALID_HANDLE_VALUE) {
|
|
if (g_debug)
|
|
std::cerr << "[!] Utils: Could not open " << hooked_procs_file << "\n";
|
|
return;
|
|
}
|
|
FILETIME writeFt;
|
|
if (!GetFileTime(h, nullptr, nullptr, &writeFt)) {
|
|
CloseHandle(h);
|
|
if (g_debug)
|
|
std::cerr << "[!] Utils: GetFileTime failed on " << hooked_procs_file << "\n";
|
|
return;
|
|
}
|
|
CloseHandle(h);
|
|
|
|
ULONGLONG write = ((ULONGLONG)writeFt.dwHighDateTime << 32) | writeFt.dwLowDateTime;
|
|
|
|
// compare & delete
|
|
if (write < boot) {
|
|
std::ofstream ofs(hooked_procs_file, std::ios::trunc);
|
|
if (ofs.is_open()) {
|
|
if (g_debug)
|
|
std::cout << "[-] Utils: Hooked procs in " << hooked_procs_file << " out of date, deleted the file\n";
|
|
return;
|
|
}
|
|
else {
|
|
if (g_debug)
|
|
std::cerr << "[!] Utils: Failed to delete out of date " << hooked_procs_file << "\n";
|
|
}
|
|
}
|
|
else {
|
|
if (g_debug)
|
|
std::cout << "[+] Utils: Hooked procs in " << hooked_procs_file << " up to date, keeping the file\n";
|
|
}
|
|
}
|
|
|
|
std::vector<int> get_hooked_procs() {
|
|
remove_if_out_of_date();
|
|
std::vector<int> hooked_procs;
|
|
std::ifstream ifs(hooked_procs_file);
|
|
if (!ifs.is_open()) {
|
|
std::cerr << "[!] Utils: Failed to open file for reading: " << hooked_procs_file << "\n";
|
|
return hooked_procs;
|
|
}
|
|
int pid;
|
|
while (ifs >> pid) {
|
|
hooked_procs.push_back(pid);
|
|
}
|
|
ifs.close();
|
|
return hooked_procs;
|
|
}
|
|
|
|
void save_hooked_procs(const std::vector<int> hooked_procs) {
|
|
std::ofstream ofs(hooked_procs_file, std::ios::app);
|
|
if (!ofs.is_open()) {
|
|
std::cerr << "[!] Utils: Failed to open file for writing: " << hooked_procs_file << "\n";
|
|
return;
|
|
}
|
|
for (int pid : hooked_procs) {
|
|
ofs << pid << "\n";
|
|
}
|
|
ofs.close();
|
|
}
|
|
|
|
// execute powershell command to download zip and extract
|
|
void update_defender2yara_sigs() {
|
|
std::string full_path = resolve_path(defender2yara_relative_path);
|
|
std::string zip_output_path = full_path + "defender2yara-yara-rules.zip";
|
|
std::string ps_command = "powershell -Command \""
|
|
"$ProgressPreference = 'SilentlyContinue'; " // do not show progress bar
|
|
"$url = 'https://github.com/t-tani/defender2yara/archive/refs/heads/yara-rules.zip'; "
|
|
"$output = '" + zip_output_path + "'; "
|
|
"Invoke-WebRequest -Uri $url -OutFile $output; "
|
|
"Expand-Archive -Path $output -DestinationPath '" + full_path + "' -Force; "
|
|
"Remove-Item $output; "
|
|
"Move-Item -Path '" + full_path + "defender2yara-yara-rules\\*' -Destination '" + full_path + "' -Force; " // move all files up one level
|
|
"Remove-Item -Path '" + full_path + "defender2yara-yara-rules' -Recurse -Force;\"";
|
|
std::cout << "[*] Utils: Updating defender2yara signatures with command: " << ps_command << "\n";
|
|
system(ps_command.c_str());
|
|
}
|
|
|
|
bool read_file(const std::string& path, std::string& output) {
|
|
std::ifstream file(path, std::ios::in | std::ios::binary);
|
|
if (!file) return false;
|
|
std::ostringstream ss;
|
|
ss << file.rdbuf();
|
|
output = ss.str();
|
|
return true;
|
|
}
|
|
|
|
bool extract_rule(const std::string& file_content, const std::string& rule_name, std::string& rule_out) {
|
|
std::string key = "rule " + rule_name;
|
|
size_t pos = file_content.find(key);
|
|
if (pos == std::string::npos) return false;
|
|
|
|
// find opening brace
|
|
size_t brace_open = file_content.find("{", pos);
|
|
if (brace_open == std::string::npos) return false;
|
|
|
|
int brace_count = 0;
|
|
size_t i = brace_open;
|
|
for (; i < file_content.size(); ++i) {
|
|
if (file_content[i] == '{') brace_count++;
|
|
else if (file_content[i] == '}') brace_count--;
|
|
if (brace_count == 0) {
|
|
// include final closing brace
|
|
rule_out = file_content.substr(pos, i - pos + 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool find_rule_in_dir(const std::string& dir, const std::string& rule_name, std::string& result) {
|
|
std::string search = dir + "\\*";
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h = FindFirstFileA(search.c_str(), &fd);
|
|
if (h == INVALID_HANDLE_VALUE) return false;
|
|
|
|
do {
|
|
std::string name = fd.cFileName;
|
|
if (name == "." || name == "..") continue;
|
|
|
|
std::string fullpath = dir + "\\" + name;
|
|
|
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
if (find_rule_in_dir(fullpath, rule_name, result)) return true;
|
|
}
|
|
else {
|
|
// check extension
|
|
if (name.size() > 4) {
|
|
std::string ext = name.substr(name.find_last_of('.') + 1);
|
|
if (ext == "yar" || ext == "yara") {
|
|
std::string content;
|
|
if (read_file(fullpath, content)) {
|
|
if (extract_rule(content, rule_name, result)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
return false;
|
|
}
|
|
|
|
// traverse the defender2yara yara-rules directory and get the complete rule for a given signature name
|
|
std::string get_yara_rule(const std::string rule_name) {
|
|
std::string yara_rules_path = resolve_path(defender2yara_relative_path);
|
|
std::string rule_text = "";
|
|
find_rule_in_dir(yara_rules_path, rule_name, rule_text);
|
|
return rule_text;
|
|
}
|
|
|