├── README.md └── CobaltSentry.go /README.md: -------------------------------------------------------------------------------- 1 | # Cobalt Sentry 2 | 3 | Cobalt Sentry is a memory scanning tool designed to detect Cobalt Strike beacons and other stealthy malware techniques, such as Hell's Gate and Heaven's Gate, in running processes on Windows systems. 4 | 5 | ## Features 6 | - Scans all running processes or a specific PID. 7 | - Detects Cobalt Strike beacons using various techniques, including: 8 | - SleepMask detection (high entropy memory regions). 9 | - Mask detection (XORed). 10 | - BeaconGate (API redirection techniques). 11 | - UDRL (Userland Reflective Loader) detection. 12 | - XOR-encoded beacon configuration scanning. 13 | - Hell's Gate and Heaven's Gate syscall manipulation detection. 14 | - Supports multi-threaded scanning for efficiency. 15 | 16 | ## Requirements 17 | - Windows operating system. 18 | - Go installed (1.18 or later). 19 | - Administrator privileges for accessing process memory. 20 | 21 | ## Installation 22 | Clone the repository and build the tool: 23 | ```sh 24 | git clone https://github.com/MazX0p/CobaltSentry.git 25 | cd CobaltSentry 26 | go mod init CobaltSentry 27 | go mod tidy 28 | 29 | ``` 30 | 31 | Build the executable: 32 | ```sh 33 | GOOS=windows GOARCH=amd64 go build -o CobaltSentry.exe CobaltSentry.go 34 | ``` 35 | 36 | ## Usage 37 | Run the tool with the following options: 38 | 39 | Scan all running processes: 40 | ```sh 41 | CobaltSentry.exe -all 42 | ``` 43 | 44 | Scan a specific process by PID: 45 | ```sh 46 | CobaltSentry.exe -pid 47 | ``` 48 | 49 | ## Example Output 50 | 51 | ![image](https://github.com/user-attachments/assets/658a4520-1198-45d9-a76a-68af6ebdc892) 52 | 53 | 54 | ``` 55 | ############################################ 56 | # (\\(\\ # 57 | # ( -.-) # 58 | # o((\")(") # 59 | # # 60 | # C O B A L T S E N T R Y # 61 | # Cobalt Strike & Hell's Gate Scanner # 62 | # Created by Mohamed Alzhrani (0xmaz) # 63 | ############################################ 64 | 65 | Scanning in progress... 66 | [!] Suspicious activity detected in PID 1234 at address 0x7ffabcd1234 It Maybe: 67 | - SleepMask Detected: High entropy in memory 68 | - UDRL Detected: Modified or missing PE header 69 | - BeaconGate Detected: API call proxying found 70 | ``` 71 | 72 | ## Disclaimer 73 | This tool is intended for security research and educational purposes only. The author is not responsible for any misuse or damage caused by this tool. 74 | 75 | ## Author 76 | - **Mohamed Alzhrani (0xmaz)** 77 | 78 | ## Contributions 79 | Contributions and improvements are welcome. Feel free to submit pull requests or open issues. 80 | 81 | ## Contact 82 | For inquiries or suggestions, please reach out via GitHub issues. 83 | 84 | -------------------------------------------------------------------------------- /CobaltSentry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "flag" 7 | "math" 8 | "sync" 9 | //"encoding/binary" 10 | "syscall" 11 | "os/signal" 12 | "os" 13 | "unsafe" 14 | "github.com/briandowns/spinner" 15 | "time" 16 | 17 | ps "github.com/mitchellh/go-ps" 18 | ) 19 | 20 | 21 | const ( 22 | PROCESS_VM_READ = 0x0010 23 | PROCESS_QUERY_INFORMATION = 0x0400 24 | MEM_COMMIT = 0x1000 25 | PAGE_EXECUTE_READWRITE = 0x40 26 | PAGE_READWRITE = 0x04 27 | PAGE_READONLY = 0x02 28 | PAGE_EXECUTE = 0x10 29 | ) 30 | 31 | var ( 32 | modkernel32 = syscall.NewLazyDLL("kernel32.dll") 33 | procVirtualQueryEx = modkernel32.NewProc("VirtualQueryEx") 34 | procReadProcessMemory = modkernel32.NewProc("ReadProcessMemory") 35 | wg sync.WaitGroup 36 | mu sync.Mutex 37 | ) 38 | 39 | // MEMORY_BASIC_INFORMATION structure 40 | type MEMORY_BASIC_INFORMATION struct { 41 | BaseAddress uintptr 42 | AllocationBase uintptr 43 | AllocationProtect uint32 44 | RegionSize uintptr 45 | State uint32 46 | Protect uint32 47 | Type uint32 48 | } 49 | 50 | // List of known syscall SSNs for detection 51 | var knownSSNs = map[string]byte{ 52 | //"NtCreateThreadEx": 0xC2, 53 | //"NtWriteVirtualMemory": 0x3A, 54 | //"NtAllocateVirtualMemory": 0x18, 55 | //"NtOpenProcess": 0x26, 56 | "NtFreeVirtualMemory": 0x1E, 57 | //"NtProtectVirtualMemory": 0x50, 58 | } 59 | 60 | var knownSyscallOpcodes = []byte{0x4C, 0x8B, 0xD1, 0xB8} 61 | 62 | func isCurrentProcess(pid int) bool { 63 | return pid == os.Getpid() 64 | } 65 | 66 | var xorPatterns = []struct { 67 | pattern []byte 68 | mask []byte 69 | }{ 70 | {[]byte{0x31, 0xC0, 0x48, 0x89, 0x00, 0x48, 0x83, 0x00}, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}}, 71 | {[]byte{0x32, 0xC0, 0x83, 0xF8, 0x00, 0x89, 0x00}, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00}}, 72 | {[]byte{0x33, 0xC9, 0x48, 0x83, 0xC1}, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}, 73 | } 74 | 75 | func isWindows(process ps.Process) bool{ 76 | legitimateProcesses := map[string]bool{ 77 | "services.exe": true, 78 | "svchost.exe": true, 79 | "lsass.exe": true, 80 | "wininit.exe": true, 81 | "winlogon.exe": true, 82 | "csrss.exe": true, 83 | "smss.exe": true, 84 | "dllhost.exe": true, 85 | "taskhostw.exe": true, 86 | "conhost.exe": true, 87 | "System": true, 88 | "SystemSettings.exe": true, 89 | "OneDrive.exe": true, 90 | "taskmgr.exe": true, 91 | "vmtoolsd.exe": true, 92 | "msedge.exe": true, 93 | "chrome.exe": true, 94 | "firefox.exe": true, 95 | "smartscreen.exe": true, 96 | "FileCoAuth.exe": true, 97 | "RuntimeBroker.exe": true, 98 | "ApplicationFrameHost.exe": true, 99 | "SearchApp.exe": true, 100 | "explore.exe": true, 101 | "explorer.exe": true, 102 | "sihost.exe": true, 103 | "WmiApSrv.exe": true, 104 | "PhoneExperienceHost.exe": true, 105 | "CalculatorApp.exe": true, 106 | "TextInputHost.exe": true, 107 | "StartMenuExperienceHost.exe": true, 108 | "ProcessHacker.exe": true, 109 | } 110 | if process.PPid() == 0 || legitimateProcesses[process.Executable()] { 111 | return true 112 | } 113 | return false 114 | } 115 | 116 | func matchPattern(data, pattern, mask []byte) bool { 117 | if len(data) < len(pattern) { 118 | return false 119 | } 120 | for i := 0; i <= len(data)-len(pattern); i++ { 121 | match := true 122 | for j := range pattern { 123 | if mask[j] == 0xFF && data[i+j] != pattern[j] { 124 | match = false 125 | break 126 | } 127 | } 128 | if match { 129 | return true 130 | } 131 | } 132 | return false 133 | } 134 | 135 | func scanBuffer(buffer []byte) bool { 136 | for _, p := range xorPatterns { 137 | if matchPattern(buffer, p.pattern, p.mask) { 138 | return true 139 | } 140 | } 141 | return false 142 | } 143 | // VirtualQueryEx wrapper 144 | func VirtualQueryEx(hProcess syscall.Handle, lpAddress uintptr, lpBuffer unsafe.Pointer, dwLength uintptr) (int, error) { 145 | ret, _, err := procVirtualQueryEx.Call(uintptr(hProcess), lpAddress, uintptr(lpBuffer), dwLength) 146 | if ret == 0 { 147 | return 0, err 148 | } 149 | return int(ret), nil 150 | } 151 | 152 | // ReadProcessMemory wrapper 153 | func ReadProcessMemory(hProcess syscall.Handle, lpBaseAddress uintptr, lpBuffer unsafe.Pointer, nSize uintptr, lpNumberOfBytesRead *uintptr) (bool, error) { 154 | ret, _, err := procReadProcessMemory.Call(uintptr(hProcess), lpBaseAddress, uintptr(lpBuffer), nSize, uintptr(unsafe.Pointer(lpNumberOfBytesRead))) 155 | if ret == 0 { 156 | return false, err 157 | } 158 | return true, nil 159 | } 160 | 161 | // Calculate Shannon entropy (used for SleepMask detection) 162 | func calculateEntropy(data []byte) float64 { 163 | if len(data) == 0 { 164 | return 0 165 | } 166 | 167 | frequency := make(map[byte]float64) 168 | for _, b := range data { 169 | frequency[b]++ 170 | } 171 | 172 | var entropy float64 173 | dataLen := float64(len(data)) 174 | for _, count := range frequency { 175 | prob := count / dataLen 176 | entropy -= prob * math.Log2(prob) 177 | } 178 | return entropy 179 | } 180 | 181 | // Check for Cobalt Strike Beacon XOR-encoded configuration (common 4-byte XOR key) 182 | func isBeaconConfig(data []byte) bool { 183 | if len(data) < 4 { 184 | return false 185 | } 186 | 187 | // Common XOR keys (e.g., 0x2e, 0x2f, 0x69, 0x6e) 188 | xorKeys := [][]byte{{0x2e, 0x2f, 0x69, 0x6e}, {0x1e, 0x2d, 0x3c, 0x4b}} 189 | for _, key := range xorKeys { 190 | decrypted := make([]byte, len(data)) 191 | for i := 0; i < len(data); i++ { 192 | decrypted[i] = data[i] ^ key[i%4] 193 | } 194 | 195 | } 196 | return false 197 | } 198 | 199 | 200 | var detectedPIDs = sync.Map{} // Map to track detected PIDs 201 | // Function to detect Hell's Gate/Heaven's Gate technique 202 | func detectHellShell(pid int,data []byte) bool { 203 | if p, err := ps.FindProcess(pid); err == nil && isWindows(p){ 204 | return false 205 | } 206 | detected := false 207 | for _, ssn := range knownSSNs { 208 | stub := append([]byte{0x31, 0xC0, 0x48, 0x89}, ssn) // Match initialization like StartCall 209 | if bytes.Contains(data, stub) || bytes.Contains(data, []byte{0xFF, 0x25}) { // Detect jump to CallAddr 210 | if _, loaded := detectedPIDs.LoadOrStore(pid, true); !loaded { 211 | return true 212 | detected = true 213 | break 214 | } 215 | 216 | } 217 | if detected { 218 | break 219 | } 220 | } 221 | return false 222 | } 223 | 224 | 225 | 226 | // Detect modified or missing PE headers (UDRL detection) 227 | func isSuspiciousPEHeader(data []byte) bool { 228 | //if len(data) < 64 { 229 | // return true // Minimum PE header size is typically larger than 64 bytes 230 | // } 231 | 232 | // Validate MZ header 233 | // if string(data[:2]) != "MZ" { 234 | // return true 235 | // } 236 | 237 | // Extract PE header offset 238 | //peOffset := binary.LittleEndian.Uint32(data[0x3C:0x40]) 239 | // if peOffset < 0x40 || peOffset > uint32(len(data)-4) { 240 | // return true // Offset is out of range 241 | //} 242 | 243 | // Validate PE signature 244 | // if string(data[peOffset:peOffset+4]) != "PE\x00\x00" { 245 | // return true 246 | // } 247 | 248 | // Additional checks for suspicious fields (optional) 249 | // Check for invalid machine type or sections 250 | // machineType := binary.LittleEndian.Uint16(data[peOffset+4 : peOffset+6]) 251 | // if machineType != 0x14c && machineType != 0x8664 { // 0x14c = x86, 0x8664 = x64 252 | // return true 253 | // } 254 | 255 | // numberOfSections := binary.LittleEndian.Uint16(data[peOffset+6 : peOffset+8]) 256 | // if numberOfSections == 0 || numberOfSections > 96 { // Unrealistic section count 257 | // return true 258 | // } 259 | 260 | return false 261 | } 262 | 263 | // Detect SleepMask by identifying high-entropy encrypted memory regions 264 | func isEncryptedMemory(data []byte) bool { 265 | entropy := calculateEntropy(data) 266 | return entropy > 8.5 // High entropy suggests encryption 267 | } 268 | 269 | // Detect API redirection used in BeaconGate 270 | func isBeaconGateRedirected(data []byte) bool { 271 | suspiciousAPIs := [][]byte{ 272 | //[]byte("VirtualAlloc"), 273 | []byte("CreateThread"), 274 | []byte("InternetConnectA"), 275 | //[]byte("LoadLibrary"), 276 | //[]byte("GetProcAddress"), 277 | } 278 | for _, api := range suspiciousAPIs { 279 | if bytes.Contains(data, api) { 280 | return true 281 | } 282 | } 283 | return false 284 | } 285 | 286 | // Detect manual import resolution (UDRL indicator) 287 | func isManualImportResolution(data []byte) bool { 288 | return bytes.Contains(data, []byte("ResolveImports"))// || 289 | //bytes.Contains(data, []byte("LoadLibraryA")) || 290 | //bytes.Contains(data, []byte("GetProcAddress")) 291 | } 292 | 293 | // Detect relocation handling (UDRL indicator) 294 | func isRelocationHandling(data []byte) bool { 295 | return bytes.Contains(data, []byte("ProcessRelocations")) 296 | } 297 | 298 | 299 | // Scan memory for Beacon techniques 300 | func scanForBeacon(pid int, data []byte, addr uintptr, handle syscall.Handle) { 301 | if isCurrentProcess(pid) { 302 | return // Skip scanning the current process 303 | } 304 | suspiciousPE := isSuspiciousPEHeader(data) 305 | encryptedMemory := isEncryptedMemory(data) 306 | apiRedirection := isBeaconGateRedirected(data) 307 | manualImport := isManualImportResolution(data) 308 | relocationHandling := isRelocationHandling(data) 309 | reBeaconConfig := isBeaconConfig(data) 310 | rescanBuffer := scanBuffer(data) 311 | redetectHellShell := detectHellShell(pid, data) 312 | 313 | if reBeaconConfig || encryptedMemory || apiRedirection || manualImport || relocationHandling || suspiciousPE || redetectHellShell { 314 | mu.Lock() 315 | fmt.Printf("[!] Suspicious activity detected in PID %d at address 0x%x It Maybe:\n", pid, addr) 316 | 317 | if encryptedMemory { 318 | fmt.Println(" - SleepMask Detected: High entropy in memory") 319 | } 320 | if suspiciousPE { 321 | fmt.Println(" - UDRL Detected: Modified or missing PE header") 322 | } 323 | if apiRedirection { 324 | fmt.Println(" - BeaconGate Detected: API call proxying found") 325 | } 326 | if manualImport { 327 | fmt.Println(" - UDRL Detected: Manual import resolution observed") 328 | } 329 | if relocationHandling { 330 | fmt.Println(" - UDRL Detected: Manual relocation processing detected") 331 | } 332 | if reBeaconConfig { 333 | fmt.Println(" - Beacon configuration: Beacon XOR-encoded configuration Detected") 334 | } 335 | if rescanBuffer { 336 | fmt.Println(" - Masked Beacon Detected: Beacon XOR-encoded Mask Detected") 337 | } 338 | if redetectHellShell { 339 | fmt.Println(" - Suspicious syscall detected: Hell's Gate/Heaven's Gate detected") 340 | } 341 | mu.Unlock() 342 | } 343 | } 344 | 345 | // Scan a process's memory 346 | func scanProcessMemory(process ps.Process) { 347 | defer wg.Done() 348 | 349 | pid := process.Pid() 350 | handle, err := syscall.OpenProcess(PROCESS_VM_READ|PROCESS_QUERY_INFORMATION, false, uint32(pid)) 351 | if err != nil { 352 | return 353 | } 354 | defer syscall.CloseHandle(handle) 355 | var addr uintptr 356 | var memInfo MEMORY_BASIC_INFORMATION 357 | 358 | for { 359 | bytesReturned, err := VirtualQueryEx(handle, addr, unsafe.Pointer(&memInfo), unsafe.Sizeof(memInfo)) 360 | if err != nil || memInfo.RegionSize == 0 || bytesReturned == 0 { 361 | break 362 | } 363 | 364 | if memInfo.State == MEM_COMMIT && (memInfo.Protect == PAGE_EXECUTE_READWRITE || memInfo.Protect == PAGE_READWRITE) { 365 | buffer := make([]byte, memInfo.RegionSize) 366 | var bytesRead uintptr 367 | success, err := ReadProcessMemory(handle, addr, unsafe.Pointer(&buffer[0]), uintptr(len(buffer)), &bytesRead) 368 | if err == nil && success { 369 | scanForBeacon(pid, buffer[:bytesRead], addr, handle) 370 | } 371 | } 372 | addr += memInfo.RegionSize 373 | } 374 | } 375 | func printBanner() { 376 | fmt.Println("############################################") 377 | fmt.Println("# (\\(\\ #") 378 | fmt.Println("# ( -.-) #") 379 | fmt.Println("# o((\")(\") #") 380 | fmt.Println("# #") 381 | fmt.Println("# C O B A L T S E N T R Y #") 382 | fmt.Println("# Cobalt Strike & Hell's Gate Scanner #") 383 | fmt.Println("# Created by Mohamed Alzhrani (0xmaz) #") 384 | fmt.Println("############################################") 385 | } 386 | 387 | func startLoading() { 388 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 389 | s.Prefix = "Scanning in progress... \n" 390 | s.Start() 391 | 392 | c := make(chan os.Signal, 1) 393 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 394 | 395 | go func() { 396 | fmt.Println("\nPress 'q' then Enter to stop scanning...") 397 | var input string 398 | for { 399 | fmt.Scanln(&input) 400 | if input == "q" { 401 | s.Stop() 402 | fmt.Println("\n[+] Scanning stopped by user.") 403 | os.Exit(0) 404 | } 405 | } 406 | }() 407 | 408 | <-c // Wait for termination signal 409 | s.Stop() 410 | fmt.Println("\n[+] Scanning terminated.") 411 | os.Exit(0) 412 | } 413 | 414 | func main() { 415 | printBanner() 416 | 417 | scanAll := flag.Bool("all", false, "Scan all processes") 418 | pid := flag.Int("pid", 0, "Scan a single process by PID") 419 | flag.Parse() 420 | 421 | if *scanAll { 422 | processes, err := ps.Processes() 423 | if err != nil { 424 | fmt.Printf("Failed to list processes: %v\n", err) 425 | return 426 | } 427 | for _, process := range processes { 428 | wg.Add(1) 429 | go scanProcessMemory(process) 430 | } 431 | } else if *pid > 0 { 432 | process, err := ps.FindProcess(*pid) 433 | if err != nil || process == nil { 434 | fmt.Printf("Failed to find process with PID %d\n", *pid) 435 | return 436 | } 437 | wg.Add(1) 438 | go scanProcessMemory(process) 439 | } else { 440 | fmt.Println("Usage: ") 441 | flag.PrintDefaults() 442 | return 443 | } 444 | go startLoading() 445 | wg.Wait() 446 | } 447 | --------------------------------------------------------------------------------