PE-sieve
Scans all running processes. Recognizes and dumps a variety of potentially malicious implants (replaced/implanted PEs, shellcodes, hooks, in-memory patches).
Loading...
Searching...
No Matches
thread_scanner.cpp
Go to the documentation of this file.
1#include "thread_scanner.h"
2#include <peconv.h>
3#include "mempage_data.h"
5#include "../utils/ntddk.h"
6#include "../stats/stats.h"
10
12
13#define ENTROPY_THRESHOLD 3.0
14#define ENTROPY_ENC_THRESHOLD 6.0
15//#define NO_ENTROPY_CHECK
16
17#ifdef _DEBUG
18#define _SHOW_THREAD_INFO
19#endif
20
21using namespace pesieve;
22
23//---
24
25namespace pesieve {
26
27 bool is_thread_running(HANDLE hThread)
28 {
29 DWORD exit_code = 0;
30 if (GetExitCodeThread(hThread, &exit_code)) {
31 if (exit_code != STILL_ACTIVE) {
32#ifdef _DEBUG
33 std::cout << " Thread ExitCode: " << std::dec << exit_code << "\n";
34#endif
35 return false;
36 }
37 }
38 return true;
39 }
40
41};
42
43bool get_page_details(HANDLE processHandle, LPVOID start_va, MEMORY_BASIC_INFORMATION& page_info)
44{
45 size_t page_info_size = sizeof(MEMORY_BASIC_INFORMATION);
46 const SIZE_T out = VirtualQueryEx(processHandle, (LPCVOID)start_va, &page_info, page_info_size);
47 const bool is_read = (out == page_info_size) ? true : false;
48 const DWORD error = is_read ? ERROR_SUCCESS : GetLastError();
49 if (error != ERROR_SUCCESS) {
50 //nothing to read
51 return false;
52 }
53 return true;
54}
55
57{
58 switch (thread_wait_reason) {
59 case DelayExecution: return "DelayExecution";
60 case Suspended: return "Suspended";
61 case Executive: return "Executive";
62 case UserRequest: return "UserRequest";
63 case WrUserRequest: return "WrUserRequest";
64 case WrEventPair: return "WrEventPair";
65 case WrQueue: return "WrQueue";
66 case WrAlertByThreadId: return "WrAlertByThreadId";
67 }
68 std::stringstream ss;
69 ss << "Other: " << std::dec << thread_wait_reason;
70 return ss.str();
71}
72
74{
75 switch (thread_state) {
76 case Initialized: return "Initialized";
77 case Ready: return "Ready";
78 case Running: return "Running";
79 case Standby: return "Standby";
80 case Terminated: return "Terminated";
81 case Waiting: return "Waiting";
82 case Transition: return "Transition";
83 case DeferredReady: return "DeferredReady";
84 case GateWaitObsolete: return "GateWaitObsolete";
85 case WaitingForProcessInSwap: return "WaitingForProcessInSwap";
86 }
87 std::stringstream ss;
88 ss << "Other: " << std::dec << thread_state;
89 return ss.str();
90}
91
92//---
93
94std::string pesieve::ThreadScanner::choosePreferredFunctionName(const std::string& dbgSymbol, const std::string& manualSymbol)
95{
96 if (dbgSymbol.empty()) {
97 if (manualSymbol.empty()) {
98 return "";
99 }
100 // Give priority to the manual symbol if the debug symbol is empty
101 return manualSymbol;
102 }
103 // Give priority to the manual symbol if it denotes the actual syscall
104 if (!SyscallTable::isSyscallFunc(dbgSymbol)) {
105 if (SyscallTable::isSyscallFunc(manualSymbol)) {
106 return manualSymbol;
107 }
108 }
109 //otherwise use the debug symbol
110 return dbgSymbol;
111}
112
113bool ThreadScanner::checkReturnAddrIntegrity(IN const std::vector<ULONGLONG>& callStack, IN OUT ThreadScanReport& my_report)
114{
115 if (this->info.last_syscall == INVALID_SYSCALL || !callStack.size() || !info.is_extended || !g_SyscallTable.isReady()) {
116 return true; // skip the check
117 }
118 const ULONGLONG lastCalled = *(callStack.begin());
119 const std::string debugFuncName = (symbols) ? symbols->funcNameFromAddr(lastCalled) : "";
120 const std::string manualSymbol = exportsMap ? resolveLowLevelFuncName(lastCalled) : "";
121 if (debugFuncName.empty() && !exportsMap) {
122 return true; // skip the check
123 }
124 const std::string lastFuncCalled = choosePreferredFunctionName(debugFuncName, manualSymbol);
125 my_report.lastFunction = lastFuncCalled;
126 if (this->info.last_syscall == 0) {
127 if (this->info.ext.wait_reason == Suspended && callStack.size() == 1 && lastFuncCalled == "RtlUserThreadStart") {
128 return true; //normal for suspended threads that just started
129 }
130 return false; // otherwise it is an anomaly
131 }
132
133 // Further checks require symbols:
134 if (!symbols) {
135 return true; // skip the check
136 }
137
138 if (lastFuncCalled.empty()) {
139#ifdef _DEBUG
140 std::cout << "ERR: Can't fetch the name of the last function called!\n";
141#endif
142 return false;
143 }
144
145 // Proceed to check if the last syscall matches the last function called...
146
147#ifndef _WIN64
148 static bool isWow64 = util::is_current_wow64();
149 if (!isWow64 && lastFuncCalled == "KiFastSystemCallRet") {
150 return true;
151 }
152#endif
153 const std::string syscallFuncName = my_report.lastSyscall;
154 if (syscallFuncName.empty()) {
155 return true; // skip the check
156 }
157
158 if (SyscallTable::isSameSyscallFunc(syscallFuncName, lastFuncCalled)) {
159 return true; // valid
160 }
161
162 const ScannedModule* mod = modulesInfo.findModuleContaining(lastCalled);
163 const std::string lastModName = mod ? mod->getModName() : "";
164
165 if (syscallFuncName == "NtCallbackReturn") {
166 if (lastModName == "win32u.dll"
167 || lastModName == "user32.dll" || lastModName == "winsrv.dll") // for Windows7
168 {
169 return true;
170 }
171 }
172 if (!SyscallTable::isSyscallDll(lastModName)) {
173#ifdef _DEBUG
174 std::cout << "[@]" << std::dec << info.tid << " : " << "LastSyscall: " << syscallFuncName << " VS LastCalledAddr: " << std::hex << lastCalled
175 << " : " << lastFuncCalled << "(" << lastModName << "." << manualSymbol << " )" << " DIFFERENT!"
176 << " WaitReason: " << std::dec << ThreadScanReport::translate_wait_reason(this->info.ext.wait_reason) << std::endl;
177#endif //_DEBUG
178 return false;
179 }
180
181 if (this->info.ext.wait_reason == WrUserRequest ||
182 this->info.ext.wait_reason == UserRequest)
183 {
184 if (syscallFuncName.rfind("NtUser", 0) == 0) {
185 if (lastFuncCalled.rfind("NtUser", 0) == 0) return true;
186 if (lastFuncCalled.rfind("NtGdi", 0) == 0) return true;
187 }
188 if (syscallFuncName.rfind("NtGdi", 0) == 0) {
189 if (lastFuncCalled.rfind("NtGdi", 0) == 0) return true;
190 if (lastFuncCalled.rfind("NtUser", 0) == 0) return true;
191 }
192 }
193
194 if (this->info.ext.wait_reason == UserRequest) {
195 if (syscallFuncName.rfind("NtDxgkGet", 0) == 0 &&
196 lastFuncCalled.rfind("NtWaitFor", 0) == 0)
197 {
198 return true;
199 }
200 if (syscallFuncName.find("WaitFor", 0) != std::string::npos &&
201 (lastFuncCalled.find("WaitFor", 0) != std::string::npos))
202 {
203 return true;
204 }
205 if (syscallFuncName == "NtWaitForSingleObject") {
206 if ((lastFuncCalled.rfind("NtQuery", 0) == 0) || lastFuncCalled == "NtDelayExecution") return true;
207 }
208 if (syscallFuncName.rfind("NtUser", 0) == 0 && lastFuncCalled == "NtWaitForWorkViaWorkerFactory") {
209 return true;
210 }
211 if (syscallFuncName.rfind("NtUserModify", 0) == 0 && lastFuncCalled == "NtDeviceIoControlFile") {
212 return true;
213 }
214 }
215
216 if (this->info.ext.wait_reason == WrQueue) {
217 if (syscallFuncName.rfind("NtWaitFor", 0) == 0 && lastFuncCalled == "NtWaitForWorkViaWorkerFactory") {
218 return true;
219 }
220 if (syscallFuncName == "NtWaitForWorkViaWorkerFactory") {
221 if (lastFuncCalled.rfind("NtWaitFor", 0) == 0
222 || lastFuncCalled.rfind("NtUserMsgWaitFor", 0) == 0
223 || lastFuncCalled.rfind("NtUserCreate", 0) == 0)
224 {
225 return true;
226 }
227 }
228 }
229
230 if (this->info.ext.wait_reason == DelayExecution) {
231 if (syscallFuncName == "NtDelayExecution") {
232 if ((lastFuncCalled.rfind("NtUserMsgWaitFor", 0) == 0) || (lastFuncCalled.rfind("NtWaitFor", 0) == 0)) return true;
233 }
234 }
235
236#ifdef _DEBUG
237 std::cout << "[@]" << std::dec << info.tid << " : " << "LastSyscall: " << syscallFuncName << " VS LastCalledAddr: " << std::hex << lastCalled
238 << " : " << lastFuncCalled << "(" << lastModName << "." << manualSymbol << " )" << " DIFFERENT!"
239 << " WaitReason: " << std::dec << ThreadScanReport::translate_wait_reason(this->info.ext.wait_reason) << std::endl;
240#endif //_DEBUG
241#ifdef _SHOW_THREAD_INFO
243 std::cout << "STACK:\n";
244 for (auto itr = callStack.rbegin(); itr != callStack.rend(); ++itr) {
245 ULONGLONG next_return = *itr;
246 symbols->dumpSymbolInfo(next_return);
247 std::cout << "\t";
248 printResolvedAddr(next_return);
249 }
250 std::cout << std::endl;
251#endif //_SHOW_THREAD_INFO
252 return false;
253}
254
255size_t pesieve::ThreadScanner::_analyzeCallStack(IN OUT ctx_details& cDetails, OUT IN std::set<ULONGLONG> &shcCandidates)
256{
257 size_t processedCntr = 0;
258
259 cDetails.is_managed = false;
260 cDetails.is_ret_in_frame = false;
261#ifdef _SHOW_THREAD_INFO
262 std::cout << "\n" << "Stack frame Size: " << std::dec << cDetails.callStack.size() << "\n===\n";
263#endif //_SHOW_THREAD_INFO
264 for (auto itr = cDetails.callStack.rbegin(); itr != cDetails.callStack.rend() ;++itr, ++processedCntr) {
265 const ULONGLONG next_return = *itr;
266 if (cDetails.ret_on_stack == next_return) {
267 cDetails.is_ret_in_frame = true;
268 }
269
270#ifdef _SHOW_THREAD_INFO
271 if (symbols && symbols->IsInitialized()) {
272 symbols->dumpSymbolInfo(next_return);
273 }
274 std::cout << "\t";
275 printResolvedAddr(next_return);
276#endif //_SHOW_THREAD_INFO
277 bool is_curr_shc = false;
278 const ScannedModule* mod = modulesInfo.findModuleContaining(next_return);
279 const std::string mod_name = mod ? mod->getModName() : "";
280 if (mod_name.length() == 0) {
281 if (!cDetails.is_managed) {
282 is_curr_shc = true;
283 shcCandidates.insert(next_return);
284#ifdef _SHOW_THREAD_INFO
285 std::cout << "\t" << std::hex << next_return << " <=== SHELLCODE\n";
286#endif //_SHOW_THREAD_INFO
287 } else {
288#ifdef _SHOW_THREAD_INFO
289 std::cout << "\t" << std::hex << next_return << " <=== .NET JIT\n";
290#endif //_SHOW_THREAD_INFO
291 }
292 }
293 if (!is_curr_shc) {
294 // store the last address, till the first called shellcode:
295 cDetails.last_ret = next_return;
296 }
297 // check if the found shellcode is a .NET JIT:
298 if (mod_name == "clr.dll" || mod_name == "coreclr.dll") {
299 cDetails.is_managed = true;
300#ifdef _SHOW_THREAD_INFO
301 std::cout << "\t" << std::hex << next_return << " <--- .NET\n";
302#endif //_SHOW_THREAD_INFO
303 }
304 }
305#ifdef _SHOW_THREAD_INFO
306 std::cout << "\n===\n";
307#endif //_SHOW_THREAD_INFO
308 return processedCntr;
309}
310
312{
313 const size_t analyzedCount = _analyzeCallStack(my_report.cDetails, my_report.shcCandidates);
314
315 bool checkCalls = true;
316 if (my_report.cDetails.is_managed) {
317 checkCalls = false;
318 }
319 if (info.is_extended) {
320 if (info.ext.wait_reason > WrQueue ||
321 info.ext.wait_reason == WrFreePage || info.ext.wait_reason == WrPageIn || info.ext.wait_reason == WrPoolAllocation ||
322 info.ext.wait_reason == FreePage || info.ext.wait_reason == PageIn || info.ext.wait_reason == PoolAllocation ||
323 info.ext.wait_reason == Suspended)// there can be last func. vs last syscall mismatch in case of suspended threads
324 {
325 checkCalls = false;
326 }
327 }
328 if (checkCalls) {
329 my_report.cDetails.is_ret_as_syscall = checkReturnAddrIntegrity(my_report.cDetails.callStack, my_report);
330 }
331 return analyzedCount;
332}
333
334size_t enum_callstack(IN ProcessSymbolsManager* symbols, const pesieve::ctx_details& cDetails, IN HANDLE hThread, IN LPVOID ctx, DWORD MachineType, std::vector<ULONGLONG>& callStack)
335{
336 if (!ctx || !symbols || !symbols->IsInitialized()) {
337 return 0;
338 }
339 const size_t max_frames = 128;
340
341 size_t fetched = 0;
342 STACKFRAME64 frame = { 0 };
343
344 frame.AddrPC.Offset = cDetails.rip;
345 frame.AddrPC.Mode = AddrModeFlat;
346 frame.AddrStack.Offset = cDetails.rsp;
347 frame.AddrStack.Mode = AddrModeFlat;
348 frame.AddrFrame.Offset = cDetails.rbp;
349 frame.AddrFrame.Mode = AddrModeFlat;
350
351 DWORD64 prevPc = 0;
352 size_t frameCount = 0;
353
354 while (symbols->RunStackWalk64(MachineType, hThread, &frame, ctx, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) {
355
356 DWORD64 next_return = frame.AddrPC.Offset;
357 if (next_return == 0) break;
358
359 if (next_return == prevPc) break;
360
361 prevPc = next_return;
362 if (++frameCount > max_frames) break;
363
364 callStack.push_back(next_return);
365 fetched++;
366 }
367 return fetched;
368}
369
370size_t pesieve::ThreadScanner::fillCallStackInfo(IN HANDLE hThread, IN const LPVOID ctx, IN OUT ctx_details& cDetails)
371{
372 if (!ctx ||
373 !this->symbols || !this->symbols->IsInitialized())
374 {
375 return 0;
376 }
377 std::vector<ULONGLONG> callStack;
378 const DWORD machineType = cDetails.is64b ? IMAGE_FILE_MACHINE_AMD64 : IMAGE_FILE_MACHINE_I386;
379 const size_t fetched = enum_callstack(this->symbols, cDetails, hThread, ctx, machineType, callStack);
380 if (!fetched) {
381 return 0;
382 }
383 cDetails.callStack = callStack;
384 return callStack.size();
385}
386
387namespace pesieve {
388 template <typename PTR_T>
389 bool read_return_ptr(IN HANDLE hProcess, IN OUT ctx_details& cDetails) {
390 PTR_T ret_addr = 0;
391 cDetails.ret_on_stack = 0;
392 if (peconv::read_remote_memory(hProcess, (LPVOID)cDetails.rsp, (BYTE*)&ret_addr, sizeof(ret_addr)) == sizeof(ret_addr)) {
393 cDetails.ret_on_stack = (ULONGLONG)ret_addr;
394 return true;
395 }
396 return false;
397 }
398}; //namespace pesieve
399
400bool pesieve::ThreadScanner::fetchNativeThreadCtxDetails(IN HANDLE hProcess, IN HANDLE hThread, IN OUT ctx_details& cDetails)
401{
402 CONTEXT ctx = { 0 };
403 ctx.ContextFlags = CONTEXT_INTEGER | CONTEXT_CONTROL;
404 if (!GetThreadContext(hThread, &ctx)) {
405 return false;
406 }
407#ifdef _WIN64
408 cDetails.init(true, ctx.Rip, ctx.Rsp, ctx.Rbp);
409 read_return_ptr<ULONGLONG>(hProcess, cDetails);
410#else
411 cDetails.init(false, ctx.Eip, ctx.Esp, ctx.Ebp);
412 read_return_ptr<DWORD>(hProcess, cDetails);
413#endif
414 fillCallStackInfo(hThread, &ctx, cDetails);
415 return true;
416}
417
418#ifdef _WIN64
419bool pesieve::ThreadScanner::fetchWow64ThreadCtxDetails(IN HANDLE hProcess, IN HANDLE hThread, IN OUT ctx_details& cDetails)
420{
421 WOW64_CONTEXT ctx = { 0 };
422 ctx.ContextFlags = WOW64_CONTEXT_INTEGER | WOW64_CONTEXT_CONTROL;
423 if (!pesieve::util::wow64_get_thread_context(hThread, &ctx)) {
424 return false;
425 }
426 cDetails.init(false, ctx.Eip, ctx.Esp, ctx.Ebp);
427 read_return_ptr<DWORD>(hProcess, cDetails);
428 fillCallStackInfo(hThread, &ctx, cDetails);
429 return true;
430}
431#endif
432
433bool pesieve::ThreadScanner::fetchThreadCtxDetails(IN HANDLE hProcess, IN HANDLE hThread, OUT ThreadScanReport& my_report)
434{
435#ifdef _WIN64
436 BOOL is_wow64 = FALSE;
437 pesieve::util::is_process_wow64(hProcess, &is_wow64);
438
439 if (is_wow64) {
440 const bool native_ok = fetchNativeThreadCtxDetails(hProcess, hThread, my_report.nativeWow64Details);
441 my_report.has_native_wow64_context = native_ok;
442
443 const bool wow64_ok = fetchWow64ThreadCtxDetails(hProcess, hThread, my_report.cDetails);
444 if (!wow64_ok && native_ok) {
445 // Preserve a usable primary stack if the WOW64 guest context could not be fetched.
446 // Avoid reporting the same native stack twice.
447 my_report.cDetails = my_report.nativeWow64Details;
448 my_report.has_native_wow64_context = false;
449 }
450#ifdef _DEBUG
451 std::cout << std::dec << "[" << GetThreadId(hThread) << "] Retrieved WOW64 guest callstack: " << my_report.cDetails.callStack.size()
452 << ", native callstack: " << my_report.nativeWow64Details.callStack.size() << std::endl;
453#endif //_DEBUG
454 return wow64_ok || native_ok;
455 }
456#endif
457
458 const bool is_ok = fetchNativeThreadCtxDetails(hProcess, hThread, my_report.cDetails);
459#ifdef _DEBUG
460 std::cout << std::dec << "[" << GetThreadId(hThread) << "] Retrieved callstack: " << my_report.cDetails.callStack.size() << std::endl;
461#endif //_DEBUG
462 return is_ok;
463}
464
466{
467 ScannedModule* mod = modulesInfo.findModuleContaining(addr);
468 if (!mod) return false;
469
470 //the module is named
471 if (mod->getModName().length() > 0) {
472 return true;
473 }
474 return false;
475}
476
477std::string pesieve::ThreadScanner::resolveLowLevelFuncName(IN const ULONGLONG addr, OUT OPTIONAL size_t* displacement)
478{
479 size_t MAX_DISP = 25;
480 if (!exportsMap) {
481 return "";
482 }
483 ScannedModule* mod = modulesInfo.findModuleContaining(addr);
484 if (!mod) {
485 return "";
486 }
488 // not a DLL containing syscalls
489 return "";
490 }
491 size_t _displacement = 0;
492 std::string expName;
493 for (size_t disp = 0; disp < MAX_DISP; disp++) {
494 _displacement = disp;
495 const ULONGLONG va = addr - disp;
496
497 const std::set<peconv::ExportedFunc>* exp_set = exportsMap->find_exports_by_va(va);
498 if (!exp_set) {
499 continue; // no exports at this VA
500 }
501 // walk through the export candicates, and find the most suitable one:
502 for (auto it1 = exp_set->begin(); it1 != exp_set->end(); ++it1) {
503 const peconv::ExportedFunc& exp = *it1;
504 const std::string libName = exp.libName;
505 if (!SyscallTable::isSyscallDll(libName)) {
506 // it is not a low-level export
507 continue;
508 }
509 expName = exp.nameToString();
510 // give preference to the functions naming syscalls:
511 if (SyscallTable::isSyscallFunc(expName)) {
512 if (displacement) {
513 (*displacement) = _displacement;
514 }
515 return expName;
516 }
517 }
518 }
519 // otherwise, return any found
520 if (displacement) {
521 (*displacement) = _displacement;
522 }
523 return expName;
524}
525
526std::string pesieve::ThreadScanner::resolveAddrToString(IN const ULONGLONG addr)
527{
528 ScannedModule* mod = modulesInfo.findModuleContaining(addr);
529 ULONGLONG modBase = 0;
530 if (!mod) {
531 return "";
532 }
533 std::stringstream ss;
534 modBase = mod->getStart();
535 ss << mod->getModName();
536
537 char SEPARATOR = '!';
538 bool is_resolved = false;
539 size_t disp = 0;
540 if (symbols && symbols->IsInitialized()) {
541 std::string funcName = symbols->funcNameFromAddr(addr, &disp);
542 if (!funcName.empty()) {
543 is_resolved = true;
544 ss << SEPARATOR << funcName;
545 }
546 }
547 if (!is_resolved) {
548 std::string funcName = resolveLowLevelFuncName(addr, &disp);
549 if (!funcName.empty()) {
550 is_resolved = true;
551 ss << SEPARATOR << funcName;
552 }
553 }
554 if (is_resolved) {
555 if (disp) {
556 ss << "+0x" << std::hex << disp;
557 }
558 }
559 else {
560 ULONGLONG diff = addr - modBase;
561 ss << "+0x" << std::hex << diff;
562 }
563 return ss.str();
564}
565
567{
568 bool is_resolved = false;
569 std::cout << std::hex << addr;
570 ScannedModule* mod = modulesInfo.findModuleContaining(addr);
571 if (mod) {
572 std::cout << " : " << mod->getModName();
573 is_resolved = true;
574 }
575 std::cout << " : " << resolveLowLevelFuncName(addr);
576 std::cout << std::endl;
577 return is_resolved;
578}
579
581{
582 std::cout << std::dec << "TID: " << threadi.tid << "\n";
583 std::cout << std::hex << "\tStart : ";
585
586 if (threadi.is_extended) {
587 std::cout << std::hex << "\tSysStart: ";
589 if (threadi.last_syscall != INVALID_SYSCALL) {
590 std::cout << "\tLast Syscall: " << std::hex << threadi.last_syscall << " Func: " << g_SyscallTable.getSyscallName(threadi.last_syscall) << std::endl;
591 }
592 std::cout << "\tState: [" << ThreadScanReport::translate_thread_state(threadi.ext.state) << "]";
593 if (threadi.ext.state == Waiting) {
594 std::cout << " Reason: [" << ThreadScanReport::translate_wait_reason(threadi.ext.wait_reason) << "] Time: " << threadi.ext.wait_time;
595 }
596 std::cout << "\n";
597 }
598 std::cout << "\n";
599}
600
602{
603 if (!report) return false;
604
605 ULONG_PTR end_va = (ULONG_PTR)report->module + report->moduleSize;
606 MemPageData mem(this->processHandle, this->isReflection, (ULONG_PTR)report->module, end_va);
607 if (!mem.fillInfo() || !mem.load()) {
608 return false;
609 }
610 report->is_code = util::is_code(mem.loadedData.getData(true), mem.loadedData.getDataSize(true));
612 return calc.fill(report->stats, nullptr);
613}
614
616{
617 SuspAddrReport* susRep = my_report->findAreaForAddress(susp_addr);
618 if (!susRep) {
619 MEMORY_BASIC_INFORMATION page_info = { 0 };
620 if (!get_page_details(processHandle, (LPVOID)susp_addr, page_info)) {
621 return false;
622 }
623 if (!(page_info.State & MEM_COMMIT)) {
624 return false;
625 }
626 ULONGLONG base = (ULONGLONG)page_info.BaseAddress;
627 susRep = new SuspAddrReport(base, page_info.RegionSize, page_info.AllocationProtect);
628 susRep->curr_protection = page_info.Protect;
629 my_report->suspAreaReports[base] = susRep;
630 fillAreaStats(susRep);
631 }
632 if (!susRep) {
633 return false;
634 }
635 susRep->addSuspAddr(susp_addr);
636#ifdef _DEBUG
637 susRep->print();
638#endif //_DEBUG
639 my_report->status = SCAN_SUSPICIOUS;
640 return true;
641}
642
643// if extended info given, allow to filter out from the scan basing on the thread state and conditions
645{
646 if (!info.is_extended) {
647 return true;
648 }
649 const KTHREAD_STATE state = (KTHREAD_STATE)info.ext.state;
650 if (state == Ready) {
651 // Detailed context walking is intentionally limited to dormant threads.
652 // The start-address check still runs for Ready threads in scanRemote(),
653 // but their register and stack snapshots are not stable enough to analyze.
654 return false;
655 }
656 if (state == Terminated) {
657 return false;
658 }
659 if (state == Waiting && info.ext.wait_reason < MaximumWaitReason) {
660 return true;
661 }
662 return false;
663}
664
666{
667 const DWORD tid = info.tid;
668 ctx_details &cDetails = my_report.cDetails;
669
670 const bool is_ok = fetchThreadCtxDetails(processHandle, hThread, my_report);
671 if (!pesieve::is_thread_running(hThread)) {
672 return false;
673 }
674 if (!is_ok || !analyzeCallStackInfo(my_report)) {
675 // could not fetch the thread context and information
676 return false;
677 }
678
679 my_report.stack_ptr = cDetails.rsp;
680 bool is_unnamed = !isAddrInNamedModule(cDetails.rip);
681 if (is_unnamed) {
682 my_report.indicators.insert(THI_SUS_IP);
683 if (reportSuspiciousAddr(&my_report, cDetails.rip)) {
684 my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
685 }
686 }
687
688 for (auto itr = my_report.shcCandidates.begin(); itr != my_report.shcCandidates.end(); ++itr) {
689 const ULONGLONG addr = *itr;
690#ifdef _SHOW_THREAD_INFO
691 std::cout << "Checking shc candidate: " << std::hex << addr << "\n";
692#endif //_SHOW_THREAD_INFO
693 //automatically verifies if the address is legit:
694 if (reportSuspiciousAddr(&my_report, addr)) {
695 my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
696#ifdef _DEBUG
697 std::cout << "[@]" << std::dec << tid << " : " << "Suspicious, possible shc: " << std::hex << addr << std::endl;
698#endif //_DEBUG
699 }
700 }
701
702 if (this->info.is_extended && info.ext.state == Waiting && this->info.ext.wait_reason != Suspended
703 && !cDetails.is_ret_in_frame)
704 {
705 const ULONGLONG ret_addr = cDetails.ret_on_stack;
706 is_unnamed = !isAddrInNamedModule(ret_addr);
707#ifdef _SHOW_THREAD_INFO
708 std::cout << "Return addr: " << std::hex << ret_addr << "\n";
709 printResolvedAddr(ret_addr);
710#endif //_SHOW_THREAD_INFO
711 if (is_unnamed) {
712 my_report.indicators.insert(THI_SUS_RET);
713 if (reportSuspiciousAddr(&my_report, (ULONGLONG)ret_addr)) {
714 my_report.indicators.insert(THI_SUS_CALLSTACK_SHC);
715 }
716 }
717 }
718
719 // other indicators of stack being corrupt:
720
721 if (this->info.is_extended && !my_report.cDetails.is_managed && !my_report.cDetails.is_ret_as_syscall)
722 {
723 my_report.indicators.insert(THI_SUS_CALLS_INTEGRITY);
724 my_report.status = SCAN_SUSPICIOUS;
725 }
726 return (my_report.status == SCAN_SUSPICIOUS) ? true : false;
727}
728
730{
731 if (!isManaged) {
732 return false;
733 }
734
735 size_t dnet_common = 0; //common indicators present in .NET executables
736
737 for (auto itr = my_report.indicators.begin();
738 itr != my_report.indicators.end();
739 ++itr)
740 {
741 const ThSusIndicator& indicator = *itr;
742 switch (indicator) {
744 case THI_SUS_IP:
745 case THI_SUS_START:
746 dnet_common++;
747 break;
748 default:
749 break;
750 }
751 }
752 if (dnet_common == my_report.indicators.size()) {
753 my_report.status = SCAN_NOT_SUSPICIOUS;
754 return true;
755 }
756 return false;
757}
758
759static int scoreArea(const SuspAddrReport& area)
760{
761 int score = 0;
762
763 if (area.is_code) {
764 score += 15;
765 }
766
767 if (area.stats.isFilled()) {
768 if (area.stats.entropy >= ENTROPY_THRESHOLD) {
769 score += 10;
770 }
771 if (area.stats.entropy >= ENTROPY_ENC_THRESHOLD) {
772 score += 10;
773 }
774 }
775 return score;
776}
777
779{
780 if (my_report.status == SCAN_NOT_SUSPICIOUS) {
781 return false;
782 }
783 if (filterDotNet(my_report)) {
784 return true;
785 }
786 ULONGLONG best_base = 0;
787 size_t best_module_size = 0;
788 int best_score = 0;
789 double best_entropy = 0;
790 for (auto itr1 = my_report.suspAreaReports.begin(); itr1 != my_report.suspAreaReports.end(); ++itr1) {
791 ULONGLONG base = itr1->first;
792 const SuspAddrReport* rep = itr1->second;
793 if (!rep) continue;
794
795 const int score = scoreArea(*rep);
796 if (score == 0) continue;
797
798 if (score > best_score
799 || (score == best_score && rep->stats.entropy > best_entropy))
800 {
801 best_base = rep->module;
802 best_module_size = rep->moduleSize;
803 best_score = score;
804 best_entropy = rep->stats.entropy;
805 }
806 }
807
808 std::set< ThSusIndicator> validatedIndicators;
809 for (auto itr = my_report.indicators.begin();
810 itr != my_report.indicators.end();
811 ++itr)
812 {
813 const ThSusIndicator& indicator = *itr;
814 switch (indicator) {
815 case THI_SUS_START:
816 case THI_SUS_IP:
817 case THI_SUS_RET:
819 if (best_base) {
820 validatedIndicators.insert(indicator);
821 }
822 break;
823 default:
824 validatedIndicators.insert(indicator);
825 break;
826 }
827 }
828 if (!validatedIndicators.size()) {
829 my_report.status = SCAN_NOT_SUSPICIOUS;
830 return true;
831 }
832 my_report.module = (HMODULE)best_base;
833 my_report.moduleSize = best_module_size;
834 return true;
835}
836
838{
839 if (!this->info.is_extended) {
840 return;
841 }
842 my_report.thread_state = info.ext.state;
843 my_report.thread_wait_reason = info.ext.wait_reason;
844 my_report.thread_wait_time = info.ext.wait_time;
845 if (this->info.last_syscall != INVALID_SYSCALL) {
846 my_report.lastSyscall = g_SyscallTable.getSyscallName(this->info.last_syscall);
847 }
848}
849
851{
852 for (auto itr = my_report.cDetails.callStack.begin(); itr != my_report.cDetails.callStack.end(); ++itr) {
853 ULONGLONG addr = *itr;
854 my_report.addrToSymbol[addr] = this->resolveAddrToString(addr);
855 }
856 for (auto itr = my_report.nativeWow64Details.callStack.begin(); itr != my_report.nativeWow64Details.callStack.end(); ++itr) {
857 ULONGLONG addr = *itr;
858 my_report.addrToSymbol[addr] = this->resolveAddrToString(addr);
859 }
860}
861
863{
864 if (GetCurrentThreadId() == info.tid) {
865 return nullptr; // do not scan your own thread
866 }
867 ThreadScanReport* my_report = new (std::nothrow) ThreadScanReport(info.tid);
868 if (!my_report) {
869 return nullptr;
870 }
871
872 initReport(*my_report);
873
874#ifdef _SHOW_THREAD_INFO
876#endif // _SHOW_THREAD_INFO
877 // initialize the report with the collected info:
878
879 bool is_unnamed = !isAddrInNamedModule(info.start_addr);
880 if (is_unnamed) {
881 my_report->indicators.insert(THI_SUS_START);
882 if (reportSuspiciousAddr(my_report, info.start_addr)) {
883 my_report->indicators.insert(THI_SUS_CALLSTACK_SHC);
884 }
885 }
887 // proceed with detailed checks:
888 HANDLE hThread = OpenThread(
889 THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | SYNCHRONIZE,
890 FALSE,
891 info.tid
892 );
893 if (hThread) {
894 scanRemoteThreadCtx(hThread, *my_report);
895 CloseHandle(hThread);
896 }
897 else {
898#ifdef _DEBUG
899 std::cerr << "[-] Could not OpenThread. Error: " << GetLastError() << std::endl;
900#endif
901 }
902 }
903 assessIndicators(*my_report);
904 reportResolvedCallstack(*my_report);
905 return my_report;
906}
A class responsible for filling in the statistics with the data from the particular buffer.
Definition stats.h:73
bool fill(AreaStats &stats, StatsSettings *settings)
Definition stats.h:80
bool isFilled() const
Definition stats.h:40
util::ByteBuffer loadedData
Represents a basic info about the scanned module, such as its base offset, size, and the status.
std::string getModName() const
ULONGLONG getStart() const
void addSuspAddr(ULONGLONG addr)
A report from the thread scan, generated by ThreadScanner.
std::map< ULONGLONG, SuspAddrReport * > suspAreaReports
std::set< ThSusIndicator > indicators
std::map< ULONGLONG, std::string > addrToSymbol
static std::string translate_wait_reason(DWORD thread_wait_reason)
SuspAddrReport * findAreaForAddress(const ULONGLONG &susp_addr)
std::set< ULONGLONG > shcCandidates
static std::string translate_thread_state(DWORD thread_state)
virtual ThreadScanReport * scanRemote()
size_t analyzeCallStackInfo(IN OUT ThreadScanReport &my_report)
bool reportSuspiciousAddr(ThreadScanReport *my_report, ULONGLONG susp_addr)
std::string resolveAddrToString(IN ULONGLONG addr)
std::string resolveLowLevelFuncName(IN const ULONGLONG addr, OUT OPTIONAL size_t *disp=nullptr)
size_t _analyzeCallStack(IN OUT ctx_details &cDetails, OUT IN std::set< ULONGLONG > &shcCandidates)
bool checkReturnAddrIntegrity(IN const std::vector< ULONGLONG > &callStack, IN OUT ThreadScanReport &my_report)
void printThreadInfo(const util::thread_info &threadi)
void reportResolvedCallstack(ThreadScanReport &my_report)
bool printResolvedAddr(const ULONGLONG addr)
bool assessIndicators(ThreadScanReport &my_report)
bool fillAreaStats(SuspAddrReport *my_report)
bool filterDotNet(ThreadScanReport &my_report)
bool fetchNativeThreadCtxDetails(IN HANDLE hProcess, IN HANDLE hThread, IN OUT ctx_details &cDetails)
void initReport(ThreadScanReport &my_report)
peconv::ExportsMapper * exportsMap
const util::thread_info & info
bool isAddrInNamedModule(ULONGLONG addr)
static std::string choosePreferredFunctionName(const std::string &dbgSymbol, const std::string &manualSymbol)
size_t fillCallStackInfo(IN HANDLE hThread, const IN LPVOID ctx, IN OUT ctx_details &cDetails)
ProcessSymbolsManager * symbols
bool scanRemoteThreadCtx(HANDLE hThread, ThreadScanReport &my_report)
bool fetchThreadCtxDetails(IN HANDLE hProcess, IN HANDLE hThread, OUT ThreadScanReport &my_report)
bool is_code(const BYTE *loadedData, size_t loadedSize)
BOOL is_process_wow64(IN HANDLE processHandle, OUT BOOL *isProcWow64)
bool is_current_wow64()
struct pesieve::util::_thread_info thread_info
BOOL wow64_get_thread_context(IN HANDLE hThread, IN OUT PWOW64_CONTEXT lpContext)
bool read_return_ptr(IN HANDLE hProcess, IN OUT ctx_details &cDetails)
struct pesieve::_ctx_details ctx_details
A custom structure keeping a fragment of a thread context.
bool is_thread_running(HANDLE hThread)
std::string info()
The string with the basic information about the scanner.
Definition pe_sieve.cpp:276
@ THI_SUS_CALLSTACK_SHC
@ THI_SUS_CALLS_INTEGRITY
pesieve::SyscallTable g_SyscallTable
Definition pe_sieve.cpp:24
std::vector< ULONGLONG > callStack
static bool isSyscallFunc(const std::string &funcName, bool NtOnly=false)
static bool isSameSyscallFunc(const std::string &func1, const std::string &func2)
static bool isSyscallDll(const std::string &libName)
size_t getDataSize(bool trimmed=false) const
Definition byte_buffer.h:55
const BYTE * getData(bool trimmed=false) const
Definition byte_buffer.h:65
Final summary about the scanned process.
#define ENTROPY_ENC_THRESHOLD
bool get_page_details(HANDLE processHandle, LPVOID start_va, MEMORY_BASIC_INFORMATION &page_info)
#define ENTROPY_THRESHOLD
size_t enum_callstack(IN ProcessSymbolsManager *symbols, const pesieve::ctx_details &cDetails, IN HANDLE hThread, IN LPVOID ctx, DWORD MachineType, std::vector< ULONGLONG > &callStack)
bool should_scan_context(const util::thread_info &info)
#define INVALID_SYSCALL
Definition threads_util.h:6