[FEATURE] In Thread Scan: report all suspicious areas, dump only the strongest candidate (#135)

* [FEATURE] In thread scan: store multiple suspicious areas

* [REFACT] Minimize VirtualQueryEx on suspicious area: reuse stored

* [BUGFIX] Fixed the report formatting

* [FEATURE] Changed formatting of suspicious areas in the JSON

* [BUGFIX] Fixed scoring the results

* [BUGFIX] Fixed Debug build. Changed adding indicators to the report

* [BUGFIX] Added exclusion for syscall integrity check. Changes in report filtering

* Small fix in the json reporting of suspected addresses

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
hasherezade
2026-05-30 23:30:42 +02:00
committed by GitHub
parent e92da72954
commit 79c058e2e1
5 changed files with 257 additions and 105 deletions
+93 -54
View File
@@ -11,6 +11,7 @@
extern pesieve::SyscallTable g_SyscallTable;
#define ENTROPY_THRESHOLD 3.0
#define ENTROPY_ENC_THRESHOLD 6.0
//#define NO_ENTROPY_CHECK
#ifdef _DEBUG
@@ -185,6 +186,11 @@ bool pesieve::ThreadScanner::checkReturnAddrIntegrity(IN const std::vector<ULONG
}
if (this->info.ext.wait_reason == UserRequest) {
if (syscallFuncName.rfind("NtDxgkGet", 0) == 0 &&
lastFuncCalled.rfind("NtWaitFor", 0) == 0)
{
return true;
}
if (syscallFuncName.find("WaitFor", 0) != std::string::npos &&
(lastFuncCalled.find("WaitFor", 0) != std::string::npos))
{
@@ -568,37 +574,45 @@ void pesieve::ThreadScanner::printThreadInfo(const pesieve::util::thread_info& t
std::cout << "\n";
}
bool pesieve::ThreadScanner::fillAreaStats(ThreadScanReport* my_report)
bool pesieve::ThreadScanner::fillAreaStats(SuspAddrReport* report)
{
if (!my_report) return false;
if (!report) return false;
ULONG_PTR end_va = (ULONG_PTR)my_report->module + my_report->moduleSize;
MemPageData mem(this->processHandle, this->isReflection, (ULONG_PTR)my_report->module, end_va);
ULONG_PTR end_va = (ULONG_PTR)report->module + report->moduleSize;
MemPageData mem(this->processHandle, this->isReflection, (ULONG_PTR)report->module, end_va);
if (!mem.fillInfo() || !mem.load()) {
return false;
}
my_report->is_code = util::is_code(mem.loadedData.getData(true), mem.loadedData.getDataSize(true));
report->is_code = util::is_code(mem.loadedData.getData(true), mem.loadedData.getDataSize(true));
AreaStatsCalculator calc(mem.loadedData);
return calc.fill(my_report->stats, nullptr);
return calc.fill(report->stats, nullptr);
}
bool pesieve::ThreadScanner::reportSuspiciousAddr(ThreadScanReport* my_report, ULONGLONG susp_addr)
{
MEMORY_BASIC_INFORMATION page_info = { 0 };
if (!get_page_details(processHandle, (LPVOID)susp_addr, page_info)) {
SuspAddrReport* susRep = my_report->findAreaForAddress(susp_addr);
if (!susRep) {
MEMORY_BASIC_INFORMATION page_info = { 0 };
if (!get_page_details(processHandle, (LPVOID)susp_addr, page_info)) {
return false;
}
if (!(page_info.State & MEM_COMMIT)) {
return false;
}
ULONGLONG base = (ULONGLONG)page_info.BaseAddress;
susRep = new SuspAddrReport(base, page_info.RegionSize, page_info.AllocationProtect);
susRep->curr_protection = page_info.Protect;
my_report->suspAreaReports[base] = susRep;
fillAreaStats(susRep);
}
if (!susRep) {
return false;
}
if (!(page_info.State & MEM_COMMIT)) {
return false;
}
ULONGLONG base = (ULONGLONG)page_info.BaseAddress;
my_report->module = (HMODULE)base;
my_report->moduleSize = page_info.RegionSize;
my_report->alloc_protection = page_info.AllocationProtect;
my_report->curr_protection = page_info.Protect;
my_report->susp_addr = susp_addr;
susRep->addSuspAddr(susp_addr);
#ifdef _DEBUG
susRep->print();
#endif //_DEBUG
my_report->status = SCAN_SUSPICIOUS;
fillAreaStats(my_report);
return true;
}
@@ -640,9 +654,7 @@ bool pesieve::ThreadScanner::scanRemoteThreadCtx(HANDLE hThread, ThreadScanRepor
if (is_unnamed) {
my_report.indicators.insert(THI_SUS_IP);
if (reportSuspiciousAddr(&my_report, cDetails.rip)) {
if (my_report.status == SCAN_SUSPICIOUS) {
my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
}
my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
}
}
@@ -653,18 +665,10 @@ bool pesieve::ThreadScanner::scanRemoteThreadCtx(HANDLE hThread, ThreadScanRepor
#endif //_SHOW_THREAD_INFO
//automatically verifies if the address is legit:
if (reportSuspiciousAddr(&my_report, addr)) {
if (my_report.status == SCAN_SUSPICIOUS) {
my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
#ifdef _DEBUG
std::cout << "[@]" << std::dec << tid << " : " << "Suspicious, possible shc: " << std::hex << addr << " Entropy: " << std::dec << my_report.stats.entropy << " : " << my_report.is_code << std::endl;
std::cout << "[@]" << std::dec << tid << " : " << "Suspicious, possible shc: " << std::hex << addr << std::endl;
#endif //_DEBUG
if (my_report.is_code) {
break;
}
#ifdef _SHOW_THREAD_INFO
std::cout << "Found! " << std::hex << addr << "\n";
#endif //_SHOW_THREAD_INFO
}
}
}
@@ -677,18 +681,11 @@ bool pesieve::ThreadScanner::scanRemoteThreadCtx(HANDLE hThread, ThreadScanRepor
std::cout << "Return addr: " << std::hex << ret_addr << "\n";
printResolvedAddr(ret_addr);
#endif //_SHOW_THREAD_INFO
if (is_unnamed && reportSuspiciousAddr(&my_report, (ULONGLONG)ret_addr)) {
if (is_unnamed) {
my_report.indicators.insert(THI_SUS_RET);
if (my_report.status == SCAN_SUSPICIOUS) {
if (reportSuspiciousAddr(&my_report, (ULONGLONG)ret_addr)) {
my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
}
else {
my_report.status = SCAN_SUSPICIOUS;
if (my_report.stats.entropy < 1) { // discard, do not dump
my_report.module = 0;
my_report.moduleSize = 0;
}
}
}
}
@@ -725,7 +722,7 @@ bool pesieve::ThreadScanner::filterDotNet(ThreadScanReport& my_report)
switch (indicator) {
case THI_SUS_CALLSTACK_SHC:
case THI_SUS_IP:
case THI_SUS_RET:
case THI_SUS_START:
dnet_common++;
break;
default:
@@ -739,6 +736,25 @@ bool pesieve::ThreadScanner::filterDotNet(ThreadScanReport& my_report)
return false;
}
static int scoreArea(const SuspAddrReport& area)
{
int score = 0;
if (area.is_code) {
score += 10;
}
if (area.stats.isFilled()) {
if (area.stats.entropy >= ENTROPY_THRESHOLD) {
score += 10;
}
if (area.stats.entropy >= ENTROPY_ENC_THRESHOLD) {
score += 10;
}
}
return score;
}
bool pesieve::ThreadScanner::assessIndicators(HANDLE hThread, ThreadScanReport& my_report)
{
if (my_report.status == SCAN_NOT_SUSPICIOUS) {
@@ -748,9 +764,31 @@ bool pesieve::ThreadScanner::assessIndicators(HANDLE hThread, ThreadScanReport&
my_report.status = SCAN_NOT_SUSPICIOUS;
return true;
}
if (filterDotNet(my_report)) {
return true;
}
ULONGLONG best_base = 0;
size_t best_module_size = 0;
int best_score = 0;
double best_entropy = 0;
for (auto itr1 = my_report.suspAreaReports.begin(); itr1 != my_report.suspAreaReports.end(); ++itr1) {
ULONGLONG base = itr1->first;
const SuspAddrReport* rep = itr1->second;
if (!rep) continue;
const int score = scoreArea(*rep);
if (score == 0) continue;
if (score > best_score
|| (score == best_score && rep->stats.entropy > best_entropy))
{
best_base = rep->module;
best_module_size = rep->moduleSize;
best_score = score;
best_entropy = rep->stats.entropy;
}
}
std::set< ThSusIndicator> validatedIndicators;
for (auto itr = my_report.indicators.begin();
@@ -758,23 +796,25 @@ bool pesieve::ThreadScanner::assessIndicators(HANDLE hThread, ThreadScanReport&
++itr)
{
const ThSusIndicator& indicator = *itr;
if (!my_report.susp_addr) {
validatedIndicators.insert(indicator);
continue;
}
if (my_report.stats.isFilled()
&& my_report.stats.entropy >= ENTROPY_THRESHOLD)
{
switch (indicator) {
case THI_SUS_START:
case THI_SUS_CALLSTACK_SHC:
if (best_base) {
validatedIndicators.insert(indicator);
}
break;
default:
validatedIndicators.insert(indicator);
break;
}
}
if (!validatedIndicators.size()) {
my_report.status = SCAN_NOT_SUSPICIOUS;
return true;
}
return false;
my_report.module = (HMODULE)best_base;
my_report.moduleSize = best_module_size;
return true;
}
void pesieve::ThreadScanner::initReport(ThreadScanReport& my_report)
@@ -817,10 +857,9 @@ ThreadScanReport* pesieve::ThreadScanner::scanRemote()
bool is_unnamed = !isAddrInNamedModule(info.start_addr);
if (is_unnamed) {
my_report->indicators.insert(THI_SUS_START);
if (reportSuspiciousAddr(my_report, info.start_addr)) {
if (my_report->status == SCAN_SUSPICIOUS) {
my_report->indicators.insert(THI_SUS_START);
}
my_report->indicators.insert(THI_SUS_CALLSTACK_SHC);
}
}
if (!should_scan_context(info)) {
+159 -46
View File
@@ -66,6 +66,114 @@ namespace pesieve {
} ctx_details;
class SuspAddrReport {
public:
SuspAddrReport(ULONGLONG _module = 0 , size_t _moduleSize = 0, DWORD _allocProtection = 0)
: module(_module), moduleSize(_moduleSize),
alloc_protection(_allocProtection), curr_protection(0),
is_code(false)
{
}
void print()
{
std::cout << __FUNCTION__ << " : " << std::hex << module << " : " << moduleSize
<< " alloc_protect: " << alloc_protection
<< " curr_protect: " << curr_protection
<< " is_code: " << is_code
<< " entropy: " << stats.entropy
<< " susp_addr [";
for (auto itr = suspAddresses.begin(); itr != suspAddresses.end(); ++itr) {
ULONGLONG susp_addr = *itr;
std::cout << " " << susp_addr;
}
std::cout << " ]" << std::endl;
}
const bool toJSON(std::stringstream& outs, size_t level, const pesieve::t_json_level& jdetails) const
{
if (addressesToJSON(outs, level, jdetails)) {
outs << ",\n";
}
moduleInfoToJSON(outs, level, jdetails);
return true;
}
void addSuspAddr(ULONGLONG addr)
{
this->suspAddresses.insert(addr);
}
bool addressesToJSON(std::stringstream& outs, size_t level, const pesieve::t_json_level& jdetails) const
{
if (!suspAddresses.size()) {
return false;
}
std::string addrTypeStr = "susp_return_addr";
if (this->module && this->moduleSize) {
addrTypeStr = "susp_addr";
}
OUT_PADDED(outs, level, "\"" << addrTypeStr << "\" : [");
OUT_PADDED(outs, level, "\"" << addrTypeStr << "\" : [");
bool isFirst = true;
for (auto itr = suspAddresses.begin(); itr != suspAddresses.end(); ++itr) {
ULONGLONG susp_addr = *itr;
if (!susp_addr) continue;
if (!isFirst) {
outs << ", ";
}
outs << "\"" << std::hex << susp_addr << "\"";
isFirst = false;
}
outs << "]";
return true;
}
bool moduleInfoToJSON(std::stringstream& outs, size_t level, const pesieve::t_json_level& jdetails) const
{
if (!this->module) {
return false;
}
OUT_PADDED(outs, level, "\"module\" : ");
outs << "\"" << std::hex << (ULONGLONG)module << "\"";
if (moduleSize) {
outs << ",\n";
OUT_PADDED(outs, level, "\"module_size\" : ");
outs << "\"" << std::hex << (ULONGLONG)moduleSize << "\"";
}
outs << ",\n";
if (alloc_protection != curr_protection) {
OUT_PADDED(outs, level, "\"alloc_protection\" : ");
outs << "\"" << std::hex << alloc_protection << "\"";
outs << ",\n";
OUT_PADDED(outs, level, "\"curr_protection\" : ");
outs << "\"" << std::hex << curr_protection << "\"";
}
else {
OUT_PADDED(outs, level, "\"protection\" : ");
outs << "\"" << std::hex << curr_protection << "\"";
}
outs << ",\n";
OUT_PADDED(outs, level, "\"is_code\" : ");
outs << "\"" << std::dec << is_code << "\"";
if (stats.isFilled()) {
outs << ",\n";
stats.toJSON(outs, level);
}
outs << "\n";
return true;
}
ULONG_PTR module;
size_t moduleSize;
std::set<ULONGLONG> suspAddresses;
DWORD alloc_protection;
DWORD curr_protection;
AreaEntropyStats stats;
bool is_code;
};
//! A report from the thread scan, generated by ThreadScanner
class ThreadScanReport : public ModuleScanReport
{
@@ -80,13 +188,37 @@ namespace pesieve {
ThreadScanReport(DWORD _tid)
: ModuleScanReport(0, 0),
tid(_tid),
susp_addr(0), alloc_protection(0), curr_protection(0), stack_ptr(0),
tid(_tid), stack_ptr(0),
thread_state(THREAD_STATE_UNKNOWN),
thread_wait_reason(0), thread_wait_time(0), is_code(false)
thread_wait_reason(0), thread_wait_time(0)
{
}
~ThreadScanReport()
{
for (auto itr = this->suspAreaReports.begin(); itr != this->suspAreaReports.end(); ++itr) {
SuspAddrReport* rep = itr->second;
delete rep;
}
suspAreaReports.clear();
}
SuspAddrReport* findAreaForAddress(const ULONGLONG &susp_addr)
{
auto found = this->suspAreaReports.find(susp_addr);
if (found != this->suspAreaReports.end()) {
return found->second;
}
for (auto itr = this->suspAreaReports.begin(); itr != this->suspAreaReports.end(); ++itr) {
SuspAddrReport* rep = itr->second;
if (!rep) continue;
if (susp_addr >= rep->module && susp_addr < (rep->module + rep->moduleSize)) {
return rep;
}
}
return nullptr;
}
const virtual void callstackToJSON(std::stringstream& outs, size_t level, const pesieve::t_json_level& jdetails)
{
bool printCallstack = (jdetails >= JSON_DETAILS) ? true : false;
@@ -113,42 +245,18 @@ namespace pesieve {
outs << "\"" << std::hex << addr;
auto sItr = this->addrToSymbol.find(addr);
if (sItr != this->addrToSymbol.end()) {
outs << ";" << sItr->second;
const std::string &sym = sItr->second;
if (!sym.empty()) {
outs << ";" << sym;
}
}
outs << "\"";
}
outs << "]";
}
}
}
const bool moduleInfoToJSON(std::stringstream& outs, size_t level, const pesieve::t_json_level& jdetails)
{
if (!this->module) {
return false;
}
outs << ",\n";
OUT_PADDED(outs, level, "\"module\" : ");
outs << "\"" << std::hex << (ULONGLONG)module << "\"";
if (moduleSize) {
outs << ",\n";
OUT_PADDED(outs, level, "\"module_size\" : ");
outs << "\"" << std::hex << (ULONGLONG)moduleSize << "\"";
}
outs << ",\n";
OUT_PADDED(outs, level, "\"alloc_protection\" : ");
outs << "\"" << std::hex << alloc_protection << "\"";
outs << ",\n";
OUT_PADDED(outs, level, "\"curr_protection\" : ");
outs << "\"" << std::hex << curr_protection << "\"";
if (stats.isFilled()) {
outs << ",\n";
stats.toJSON(outs, level);
}
return true;
}
const bool threadInfoToJSON(std::stringstream& outs, size_t level, const pesieve::t_json_level& jdetails)
{
OUT_PADDED(outs, level, "\"state\" : ");
@@ -218,17 +326,27 @@ namespace pesieve {
outs << ",\n";
indicatorsToJSON(outs, level, jdetails);
if (susp_addr) {
if (!suspAreaReports.empty()) {
outs << ",\n";
if (this->module && this->moduleSize) {
OUT_PADDED(outs, level, "\"susp_addr\" : ");
OUT_PADDED(outs, level, "\"susp_areas\" : [\n");
bool isFirst = true;
for (const auto& entry : suspAreaReports) {
const SuspAddrReport* suspr = entry.second;
if (!suspr) {
continue;
}
if (!isFirst) {
outs << ",\n";
}
OUT_PADDED(outs, level + 1, "{\n");
suspr->toJSON(outs, level + 2, jdetails);
OUT_PADDED(outs, level + 1, "}");
isFirst = false;
}
else {
OUT_PADDED(outs, level, "\"susp_return_addr\" : ");
}
outs << "\"" << std::hex << susp_addr << "\"";
outs << "\n";
OUT_PADDED(outs, level, "]");
}
moduleInfoToJSON(outs, level, jdetails);
}
const virtual bool toJSON(std::stringstream& outs, size_t level, const pesieve::t_json_level &jdetails)
@@ -241,9 +359,6 @@ namespace pesieve {
}
DWORD tid;
ULONGLONG susp_addr;
DWORD alloc_protection;
DWORD curr_protection;
ULONGLONG stack_ptr;
DWORD thread_state;
DWORD thread_wait_reason;
@@ -256,9 +371,7 @@ namespace pesieve {
std::map<ULONGLONG, std::string> addrToSymbol;
std::set<ULONGLONG> shcCandidates;
std::set<ThSusIndicator> indicators;
AreaEntropyStats stats;
bool is_code;
std::map<ULONGLONG, SuspAddrReport*> suspAreaReports;
};
//! A scanner for threads
@@ -292,7 +405,7 @@ namespace pesieve {
bool checkReturnAddrIntegrity(IN const std::vector<ULONGLONG>& callStack, IN OUT ThreadScanReport& my_report);
bool fillAreaStats(ThreadScanReport* my_report);
bool fillAreaStats(SuspAddrReport* my_report);
bool reportSuspiciousAddr(ThreadScanReport* my_report, ULONGLONG susp_addr);
bool filterDotNet(ThreadScanReport& my_report);
+1 -1
View File
@@ -35,7 +35,7 @@ namespace pesieve {
double entropy;
protected:
const virtual void fieldsToJSON(std::stringstream& outs, size_t level)
virtual void fieldsToJSON(std::stringstream& outs, size_t level) const
{
OUT_PADDED(outs, level, "\"area_start\" : ");
outs << "\"" << std::hex << area_start << "\"";
+2 -2
View File
@@ -129,7 +129,7 @@ namespace pesieve {
lastStr.clear();
}
const virtual void fieldsToJSON(std::stringstream& outs, size_t level)
virtual void fieldsToJSON(std::stringstream& outs, size_t level) const
{
OUT_PADDED(outs, level, "\"offset\" : ");
outs << std::hex << "\"" << offset << "\"";
@@ -208,7 +208,7 @@ namespace pesieve {
return true;
}
const virtual void fieldsToJSON(std::stringstream& outs, size_t level)
virtual void fieldsToJSON(std::stringstream& outs, size_t level) const
{
OUT_PADDED(outs, level, "\"full_area\" : {\n");
currArea.fieldsToJSON(outs, level + 1);
+2 -2
View File
@@ -35,7 +35,7 @@ namespace pesieve {
area_size++;
}
const virtual void fieldsToJSON(std::stringstream& outs, size_t level) = 0;
virtual void fieldsToJSON(std::stringstream& outs, size_t level) const = 0;
bool isFilled() const
{
@@ -46,7 +46,7 @@ namespace pesieve {
virtual bool fillSettings(StatsSettings* _settings) { return false; }
const virtual bool toJSON(std::stringstream& outs, size_t level)
virtual bool toJSON(std::stringstream& outs, size_t level) const
{
if (!isFilled()) {
return false;