@@ -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