//go:build windows package blocker import ( "log" "os" "path/filepath" "strings" "sync" "syscall" "unsafe" ) var ( user32 = syscall.NewLazyDLL("user32.dll") procEnumWindows = user32.NewProc("EnumWindows") procIsWindowVisible = user32.NewProc("IsWindowVisible") procGetWindowTextW = user32.NewProc("GetWindowTextW") procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW") procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") procGetWindow = user32.NewProc("GetWindow") procGetWindowLongW = user32.NewProc("GetWindowLongW") procGetAncestor = user32.NewProc("GetAncestor") dwmapi = syscall.NewLazyDLL("dwmapi.dll") procDwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute") kernel32 = syscall.NewLazyDLL("kernel32.dll") ) const ( GW_OWNER = 4 WS_EX_TOOLWINDOW = 0x00000080 DWMWA_CLOAKED = 14 ) func isRealGUIWindow(hwnd uintptr) bool { // 1. Phải đang hiển thị ret, _, _ := procIsWindowVisible.Call(hwnd) if ret == 0 { return false } // 2. Không được có chủ sở hữu (phải là cửa sổ chính - top-level) owner, _, _ := procGetWindow.Call(hwnd, GW_OWNER) if owner != 0 { return false } // 3. Không phải là tool window (WS_EX_TOOLWINDOW) gwlExStyle := int32(-20) // GWL_EXSTYLE = -20 style, _, _ := procGetWindowLongW.Call(hwnd, uintptr(gwlExStyle)) if (style & WS_EX_TOOLWINDOW) != 0 { return false } // 4. Không bị cloaked bởi DWM (ví dụ: app UWP bị treo/chạy ngầm, màn hình ảo) var cloaked uint32 hr, _, _ := procDwmGetWindowAttribute.Call(hwnd, DWMWA_CLOAKED, uintptr(unsafe.Pointer(&cloaked)), 4) if hr == 0 && cloaked != 0 { return false } return true } type WindowInfo struct { PID uint32 Title string ProcessName string } func getProcessMap() (map[uint32]string, map[uint32]uint32, error) { snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) if err != nil { return nil, nil, err } defer syscall.CloseHandle(snapshot) var pe syscall.ProcessEntry32 pe.Size = uint32(unsafe.Sizeof(pe)) err = syscall.Process32First(snapshot, &pe) if err != nil { return nil, nil, err } pm := make(map[uint32]string) parents := make(map[uint32]uint32) for { name := syscall.UTF16ToString(pe.ExeFile[:]) pm[pe.ProcessID] = name parents[pe.ProcessID] = pe.ParentProcessID err = syscall.Process32Next(snapshot, &pe) if err != nil { break } } return pm, parents, nil } func getWindowText(hwnd uintptr) string { length, _, _ := procGetWindowTextLengthW.Call(hwnd) if length == 0 { return "" } buf := make([]uint16, length+1) procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), length+1) return syscall.UTF16ToString(buf) } var ( enumWindowsMutex sync.Mutex enumWindowsList []WindowInfo enumProcessMap map[uint32]string enumParentMap map[uint32]uint32 ) var enumWindowsCallback = syscall.NewCallback(func(hwnd uintptr, lParam uintptr) uintptr { defer func() { if r := recover(); r != nil { log.Printf("[BLOCKER] Callback panic recovered: %v", r) } }() if !isRealGUIWindow(hwnd) { return 1 } // Kiểm tra xem chủ sở hữu gốc (root owner) của cửa sổ này có thuộc về app ta hay không rootHwnd, _, _ := procGetAncestor.Call(hwnd, 3) // GA_ROOTOWNER = 3 if rootHwnd != 0 { var rootPid uint32 procGetWindowThreadProcessId.Call(rootHwnd, uintptr(unsafe.Pointer(&rootPid))) if rootPid == uint32(os.Getpid()) { return 1 // Cửa sổ thuộc về WebView2 / app của ta, bỏ qua không quét } } title := getWindowText(hwnd) if title == "" { return 1 } var pid uint32 procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid))) procName := enumProcessMap[pid] if procName == "" { procName = "Unknown" } enumWindowsList = append(enumWindowsList, WindowInfo{ PID: pid, Title: title, ProcessName: procName, }) return 1 }) func EnumerateGUIWindows() ([]WindowInfo, map[uint32]uint32, error) { pMap, parentMap, err := getProcessMap() if err != nil { return nil, nil, err } enumWindowsMutex.Lock() defer enumWindowsMutex.Unlock() enumWindowsList = make([]WindowInfo, 0, 100) enumProcessMap = pMap enumParentMap = parentMap procEnumWindows.Call(enumWindowsCallback, 0) // Clean up map reference so GC can reclaim it enumProcessMap = nil enumParentMap = nil // Copy to a new slice to return safely res := make([]WindowInfo, len(enumWindowsList)) copy(res, enumWindowsList) return res, parentMap, nil } var systemAllowed = map[string]bool{ "explorer.exe": true, "taskmgr.exe": true, "cmd.exe": true, "powershell.exe": true, "conhost.exe": true, "client.exe": true, // App Wails của ta "simple_care_v1.0.exe": true, "wails.exe": true, "msedgewebview2.exe": true, // WebView2 runtime của Wails "code.exe": true, // VSCode "cursor.exe": true, // Cursor "windsurf.exe": true, // Windsurf "goland.exe": true, // GoLand "goland64.exe": true, // GoLand "idea64.exe": true, // IntelliJ IDEA "clion64.exe": true, // CLion "webstorm64.exe": true, // WebStorm "pycharm64.exe": true, // PyCharm "rider64.exe": true, // Rider "studio64.exe": true, // Android Studio "eclipse.exe": true, // Eclipse "sublime_text.exe": true, // Sublime Text "notepad++.exe": true, // Notepad++ "devenv.exe": true, // Visual Studio "git-bash.exe": true, // Git Bash "bash.exe": true, // Bash } func isDescendant(pid, targetPid uint32, parentMap map[uint32]uint32) bool { curr := pid for i := 0; i < 16; i++ { parent, ok := parentMap[curr] if !ok || parent == 0 { return false } if parent == targetPid { return true } curr = parent } return false } func (b *Blocker) checkAndKill() { b.mu.Lock() keywords := make([]string, len(b.allowedKeywords)) copy(keywords, b.allowedKeywords) b.mu.Unlock() // Nếu không cấu hình keyword thì không chặn gì cả if len(keywords) == 0 { return } currentExec := "" if execPath, err := os.Executable(); err == nil { currentExec = strings.ToLower(filepath.Base(execPath)) } windows, parentMap, err := EnumerateGUIWindows() if err != nil { return } myPid := uint32(os.Getpid()) for _, w := range windows { pNameLower := strings.ToLower(w.ProcessName) wTitleLower := strings.ToLower(w.Title) // 1. Luôn cho phép hệ thống/app cốt lõi hoặc chính tiến trình này (bao gồm tiến trình con/cháu, và đổi tên) isOurApp := w.PID == myPid || isDescendant(w.PID, myPid, parentMap) if isOurApp || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") || strings.Contains(wTitleLower, "antigravity") { continue } // 2. Kiểm tra xem có chứa bất kỳ từ khóa nào được cho phép không allowed := false for _, kw := range keywords { if matchesAllowedKeyword(kw, pNameLower, wTitleLower) { allowed = true break } } // 3. Nếu không nằm trong whitelist, tắt ứng dụng if !allowed { if b.OnBlocked != nil { b.OnBlocked(w.ProcessName, w.Title) } log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d, Title: %s)", w.ProcessName, w.PID, w.Title) h, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, w.PID) if err == nil { errTerm := syscall.TerminateProcess(h, 0) _ = syscall.CloseHandle(h) if errTerm == nil && b.OnKill != nil { b.OnKill(w.ProcessName, w.Title) } } } } }