Skip to content

Commit 4fddf02

Browse files
authored
Merge pull request #554 from kevross33/patch-477530
Add UnbackedMutexCreation & UnbackedDotNetExecution signature
2 parents 1a925da + f1218ea commit 4fddf02

1 file changed

Lines changed: 148 additions & 0 deletions

File tree

modules/signatures/windows/memory_unbacked_execution.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,151 @@ def on_complete(self):
507507
if self.ret:
508508
self.data.append({"unbacked_memory_protection_alterations": self.protection_events})
509509
return self.ret
510+
511+
512+
class UnbackedMutexCreation(Signature):
513+
name = "unbacked_mutex_creation"
514+
description = "Created or queried a mutex from dynamically allocated (unbacked) memory, indicative of a fileless payload checking or creating an infection marker"
515+
severity = 3
516+
confidence = 100
517+
categories = ["execution", "evasion", "fileless"]
518+
authors = ["Kevin Ross"]
519+
minimum = "1.3"
520+
evented = True
521+
ttps = ["T1055", "T1480"]
522+
523+
filter_apinames = {
524+
"NtAllocateVirtualMemory", "VirtualAlloc", "VirtualAllocEx",
525+
"NtOpenMutant", "NtCreateMutant", "CreateMutexA", "CreateMutexW", "CreateMutexExA", "CreateMutexExW",
526+
"OpenMutexA", "OpenMutexW"
527+
}
528+
529+
def __init__(self, *args, **kwargs):
530+
Signature.__init__(self, *args, **kwargs)
531+
self.ret = False
532+
self.unbacked_ranges = {}
533+
self.mutex_events = set()
534+
535+
def on_call(self, call, process):
536+
api = call["api"]
537+
pid = process.get("process_id")
538+
539+
if api in ("NtAllocateVirtualMemory", "VirtualAlloc", "VirtualAllocEx"):
540+
base_address = self.get_argument(call, "BaseAddress") or self.get_argument(call, "lpAddress")
541+
region_size = self.get_argument(call, "RegionSize") or self.get_argument(call, "dwSize")
542+
if base_address and region_size:
543+
try:
544+
base_val = int(base_address, 16) if isinstance(base_address, str) else int(base_address)
545+
size_val = int(region_size, 16) if isinstance(region_size, str) else int(region_size)
546+
547+
if base_val:
548+
if pid not in self.unbacked_ranges:
549+
self.unbacked_ranges[pid] = []
550+
self.unbacked_ranges[pid].append((base_val, base_val + size_val))
551+
except (ValueError, TypeError):
552+
pass
553+
554+
elif api in ("NtOpenMutant", "NtCreateMutant", "CreateMutexA", "CreateMutexW", "CreateMutexExA", "CreateMutexExW", "OpenMutexA", "OpenMutexW"):
555+
caller_addr = call.get("caller")
556+
557+
if caller_addr and pid in self.unbacked_ranges:
558+
try:
559+
caller_val = int(caller_addr, 16) if isinstance(caller_addr, str) else int(caller_addr)
560+
for start_addr, end_addr in self.unbacked_ranges[pid]:
561+
if start_addr <= caller_val <= end_addr:
562+
mutex_name = self.get_argument(call, "MutexName") or self.get_argument(call, "Name") or self.get_argument(call, "lpName") or "Unknown Mutex"
563+
proc_name = process.get("process_name", "unknown")
564+
565+
event_msg = f"{proc_name} queried/created Mutex '{mutex_name}' from unbacked caller {caller_addr}"
566+
if event_msg not in self.mutex_events:
567+
self.mutex_events.add(event_msg)
568+
self.mark_call()
569+
self.ret = True
570+
break
571+
except (ValueError, TypeError):
572+
pass
573+
574+
def on_complete(self):
575+
if self.ret:
576+
self.data.append({"unbacked_mutex_creation": list(self.mutex_events)})
577+
return self.ret
578+
579+
580+
class UnbackedDotNetExecution(Signature):
581+
name = "unbacked_dotnet_execution"
582+
description = "Attempted to load .NET DLLs or call CLR APIs from dynamically allocated (unbacked) memory, indicative of fileless .NET"
583+
severity = 3
584+
confidence = 100
585+
categories = ["execution", "fileless", "evasion", "dotnet"]
586+
authors = ["Kevin Ross"]
587+
minimum = "1.3"
588+
evented = True
589+
ttps = ["T1055", "T1564"]
590+
591+
filter_apinames = {
592+
"NtAllocateVirtualMemory", "VirtualAlloc", "VirtualAllocEx",
593+
"CLRCreateInstance", "CorBindToRuntimeEx", "CorBindToRuntimeHost", "CorBindToCurrentRuntime",
594+
"LdrLoadDll", "LoadLibraryA", "LoadLibraryW", "LoadLibraryExW"
595+
}
596+
597+
def __init__(self, *args, **kwargs):
598+
Signature.__init__(self, *args, **kwargs)
599+
self.ret = False
600+
self.unbacked_ranges = {}
601+
self.dotnet_events = set()
602+
603+
def on_call(self, call, process):
604+
api = call["api"]
605+
pid = process.get("process_id")
606+
if api in ("NtAllocateVirtualMemory", "VirtualAlloc", "VirtualAllocEx"):
607+
base_address = self.get_argument(call, "BaseAddress") or self.get_argument(call, "lpAddress")
608+
region_size = self.get_argument(call, "RegionSize") or self.get_argument(call, "dwSize")
609+
610+
if base_address and region_size:
611+
try:
612+
base_val = int(base_address, 16) if isinstance(base_address, str) else int(base_address)
613+
size_val = int(region_size, 16) if isinstance(region_size, str) else int(region_size)
614+
615+
if pid not in self.unbacked_ranges:
616+
self.unbacked_ranges[pid] = []
617+
self.unbacked_ranges[pid].append((base_val, base_val + size_val))
618+
except (ValueError, TypeError):
619+
pass
620+
621+
elif api in ("CLRCreateInstance", "CorBindToRuntimeEx", "CorBindToRuntimeHost", "CorBindToCurrentRuntime", "LdrLoadDll", "LoadLibraryA", "LoadLibraryW", "LoadLibraryExW"):
622+
caller_addr = call.get("caller")
623+
624+
if caller_addr and pid in self.unbacked_ranges:
625+
try:
626+
caller_val = int(caller_addr, 16) if isinstance(caller_addr, str) else int(caller_addr)
627+
628+
for start_addr, end_addr in self.unbacked_ranges[pid]:
629+
if start_addr <= caller_val <= end_addr:
630+
proc_name = process.get("process_name", "unknown")
631+
if api in ("CLRCreateInstance", "CorBindToRuntimeEx", "CorBindToRuntimeHost", "CorBindToCurrentRuntime"):
632+
event_msg = f"{proc_name} bootstrapped .NET CLR via API '{api}' from unbacked caller {caller_addr}"
633+
if event_msg not in self.dotnet_events:
634+
self.dotnet_events.add(event_msg)
635+
self.mark_call()
636+
self.ret = True
637+
break
638+
639+
else:
640+
dll_name = self.get_argument(call, "FileName") or self.get_argument(call, "lpLibFileName")
641+
if dll_name and isinstance(dll_name, str):
642+
dll_lower = dll_name.lower()
643+
dotnet_targets = ["mscoree.dll", "mscoreei.dll", "clr.dll", "coreclr.dll", "mscorwks.dll"]
644+
if any(target in dll_lower for target in dotnet_targets):
645+
event_msg = f"{proc_name} manually loaded .NET engine DLL '{dll_name}' from unbacked caller {caller_addr}"
646+
if event_msg not in self.dotnet_events:
647+
self.dotnet_events.add(event_msg)
648+
self.mark_call()
649+
self.ret = True
650+
break
651+
except (ValueError, TypeError):
652+
pass
653+
654+
def on_complete(self):
655+
if self.ret:
656+
self.data.append({"unbacked_dotnet_execution": list(self.dotnet_events)})
657+
return self.ret

0 commit comments

Comments
 (0)