413 lines
15 KiB
Go
413 lines
15 KiB
Go
//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{
|
|
// ── Windows Shell & Explorer ───────────────────────────────────────────────
|
|
"explorer.exe": true, // Windows Explorer / desktop shell
|
|
"shellexperiencehost.exe": true, // Start menu, Action Center shell
|
|
"startmenuexperiencehost.exe": true, // Start menu host
|
|
"searchhost.exe": true, // Windows Search UI
|
|
"searchapp.exe": true, // Windows Search (older)
|
|
"searchindexer.exe": true,
|
|
"sihost.exe": true, // Shell Infrastructure Host (taskbar, notification icons)
|
|
"taskhostw.exe": true, // Task Host Window
|
|
"taskbar.exe": true,
|
|
"lockapp.exe": true, // Lock screen
|
|
"logonui.exe": true, // Login/Logon UI
|
|
"winlogon.exe": true, // Windows Logon Process
|
|
"userinit.exe": true,
|
|
"dwm.exe": true, // Desktop Window Manager — kill = black screen
|
|
"csrss.exe": true, // Client Server Runtime — kill = BSOD
|
|
|
|
// ── UWP / Modern App Infrastructure ───────────────────────────────────────
|
|
"applicationframehost.exe": true, // Host for ALL UWP apps (Camera, Calculator, Photos...)
|
|
"runtimebroker.exe": true, // UWP permission broker
|
|
"backgroundtaskhost.exe": true, // UWP background tasks
|
|
"wwahost.exe": true, // Web app host
|
|
"microsoftedgecp.exe": true, // Edge content process (older)
|
|
"textinputhost.exe": true, // Touch keyboard / handwriting panel
|
|
|
|
// ── Input Methods & Language ───────────────────────────────────────────────
|
|
"ctfmon.exe": true, // Collaborative Translation Framework — input method manager
|
|
"chsime.exe": true, // Chinese IME
|
|
"imetip.exe": true, // IME tip
|
|
"imecmnt.exe": true,
|
|
"googlepinyin.exe": true,
|
|
"baidu.exe": true, // Baidu IME
|
|
"openkey.exe": true, // OpenKey Vietnamese IME
|
|
"openkey64.exe": true,
|
|
"gotiengviet.exe": true, // GoTiengViet
|
|
"unikeyvnt.exe": true, // Unikey
|
|
"unikey.exe": true,
|
|
|
|
// ── Security / Credential / UAC ───────────────────────────────────────────
|
|
"consent.exe": true, // UAC consent dialog — kill breaks all elevation
|
|
"credentialuibroker.exe": true, // Credential UI
|
|
"smartscreen.exe": true, // Windows SmartScreen
|
|
"securityhealthsystray.exe": true, // Windows Security tray icon
|
|
"securityhealthservice.exe": true,
|
|
"wscsvc.exe": true,
|
|
"msseces.exe": true, // Microsoft Security Essentials tray
|
|
"msmpeng.exe": true, // Windows Defender engine (has no GUI but safety)
|
|
"nisSrv.exe": true,
|
|
"antimalware service executable": true, // Window title of Defender
|
|
|
|
// ── Notifications & Action Center ─────────────────────────────────────────
|
|
"notificationplatformcontroller": true,
|
|
|
|
// ── System Tray / Taskbar helpers ─────────────────────────────────────────
|
|
"systemsettings.exe": true, // Windows Settings
|
|
"settingssynchostservice.exe": true,
|
|
"settingssynchost.exe": true,
|
|
"regsvc.exe": true,
|
|
"spoolsv.exe": true,
|
|
"tabtip.exe": true, // Touch keyboard
|
|
"tabtip32.exe": true,
|
|
"onedrive.exe": true, // OneDrive tray (common, safe to keep)
|
|
"onedriveupdater.exe": true,
|
|
|
|
// ── Accessibility ─────────────────────────────────────────────────────────
|
|
"narrator.exe": true,
|
|
"magnify.exe": true,
|
|
"osk.exe": true, // On-Screen Keyboard
|
|
|
|
// ── Audio ─────────────────────────────────────────────────────────────────
|
|
"audiodg.exe": true, // Windows Audio Device Graph — kill = no sound
|
|
"sndvol.exe": true, // Volume mixer
|
|
"cmediaaudiocontrolpanel.exe": true, // C-Media audio panel
|
|
|
|
// ── Windows Update / Store ─────────────────────────────────────────────────
|
|
"wuauclt.exe": true, // Windows Update
|
|
"musnotifyicon.exe": true, // Update tray notification
|
|
"windowsstore.exe": true,
|
|
"winstore.app.exe": true,
|
|
|
|
// ── Drivers / Hardware UI ──────────────────────────────────────────────────
|
|
"nvdisplay.container.exe": true, // NVIDIA display container
|
|
"nvcontainer.exe": true,
|
|
"nvinject.exe": true,
|
|
"nvtelemetrycontainer.exe": true,
|
|
"nvvsvc.exe": true,
|
|
"nvspcaps64.exe": true,
|
|
"geforce experience.exe": true,
|
|
"radeonsoftware.exe": true, // AMD Radeon Software
|
|
"amddvr.exe": true,
|
|
"igfxtray.exe": true, // Intel graphics tray
|
|
"igfxem.exe": true,
|
|
"igfxhk.exe": true,
|
|
"hkcmd.exe": true,
|
|
"atk hub.exe": true, // ASUS ATK
|
|
"atkex.exe": true,
|
|
"asusoptimization.exe": true,
|
|
|
|
// ── Antivirus trays (common, prevent false-kill) ───────────────────────────
|
|
"avastui.exe": true,
|
|
"avgui.exe": true,
|
|
"mbam.exe": true, // Malwarebytes
|
|
"mbamtray.exe": true,
|
|
"bdagent.exe": true, // Bitdefender
|
|
"uiseagnt.exe": true, // Trend Micro
|
|
"eguiproxy.exe": true, // ESET
|
|
|
|
// ── Task Manager & System tools ────────────────────────────────────────────
|
|
"taskmgr.exe": true,
|
|
"resmon.exe": true, // Resource Monitor
|
|
"perfmon.exe": true,
|
|
"mmc.exe": true, // Management Console
|
|
|
|
// ── Terminal / Shell ───────────────────────────────────────────────────────
|
|
"cmd.exe": true,
|
|
"powershell.exe": true,
|
|
"pwsh.exe": true, // PowerShell Core
|
|
"conhost.exe": true, // Console Host
|
|
"windowsterminal.exe": true, // Windows Terminal
|
|
"wt.exe": true,
|
|
"bash.exe": true,
|
|
"git-bash.exe": true,
|
|
"mintty.exe": true, // Git Bash window
|
|
"wsl.exe": true,
|
|
"wslhost.exe": true,
|
|
|
|
// ── Our app + IDE/dev tools ────────────────────────────────────────────────
|
|
"client.exe": true,
|
|
"simple_care_v1.0.exe": true,
|
|
"simple_care.exe": true,
|
|
"wails.exe": true,
|
|
"msedgewebview2.exe": true, // WebView2 runtime (Wails renderer)
|
|
"code.exe": true,
|
|
"cursor.exe": true,
|
|
"windsurf.exe": true,
|
|
"goland.exe": true,
|
|
"goland64.exe": true,
|
|
"idea64.exe": true,
|
|
"clion64.exe": true,
|
|
"webstorm64.exe": true,
|
|
"pycharm64.exe": true,
|
|
"rider64.exe": true,
|
|
"studio64.exe": true,
|
|
"eclipse.exe": true,
|
|
"sublime_text.exe": true,
|
|
"notepad++.exe": true,
|
|
"devenv.exe": true,
|
|
}
|
|
|
|
|
|
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] {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|