//go:build windows package guard import ( "log" "sync" "syscall" "time" "unsafe" ) const ( smCMonitors = 80 eventSystemDesktopSwitch = 0x0020 wineventOutofcontext = 0x0000 wmWtsSessionChange = 0x02B1 wtsConsoleDisconnect = 0x2 wtsRemoteDisconnect = 0x3 wtsSessionLogoff = 0x6 wtsSessionLock = 0x7 ) var ( user32 = syscall.NewLazyDLL("user32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll") wtsapi32 = syscall.NewLazyDLL("wtsapi32.dll") procGetSystemMetrics = user32.NewProc("GetSystemMetrics") procSetWinEventHook = user32.NewProc("SetWinEventHook") procUnhookWinEvent = user32.NewProc("UnhookWinEvent") procGetMessageW = user32.NewProc("GetMessageW") procTranslateMessage = user32.NewProc("TranslateMessage") procDispatchMessageW = user32.NewProc("DispatchMessageW") procCreateWindowExW = user32.NewProc("CreateWindowExW") procDefWindowProcW = user32.NewProc("DefWindowProcW") procRegisterClassExW = user32.NewProc("RegisterClassExW") procDestroyWindow = user32.NewProc("DestroyWindow") procGetCurrentProcessId = kernel32.NewProc("GetCurrentProcessId") procProcessIdToSessionId = kernel32.NewProc("ProcessIdToSessionId") procWTSRegisterSessionNotification = wtsapi32.NewProc("WTSRegisterSessionNotification") procWTSUnRegisterSessionNotification = wtsapi32.NewProc("WTSUnRegisterSessionNotification") ) type wndclassEx struct { Size uint32 Style uint32 WndProc uintptr ClsExtra int32 WndExtra int32 Instance syscall.Handle Icon syscall.Handle Cursor syscall.Handle Background syscall.Handle MenuName *uint16 ClassName *uint16 IconSm syscall.Handle } type point struct { X, Y int32 } type msg struct { Hwnd syscall.Handle Message uint32 WParam uintptr LParam uintptr Time uint32 Pt point } var ( guardOnce sync.Once guardViolation func(string) guardStop chan struct{} guardBaselineUser string guardBaselineSession uint32 desktopHook uintptr guardClassAtom uint16 suppressMu sync.Mutex suppressViolationsUntil time.Time ) // SuppressFor tạm không thoát app khi WebView/Explorer chuyển màn hình nội bộ. func SuppressFor(d time.Duration) { if d <= 0 { return } suppressMu.Lock() next := time.Now().Add(d) if next.After(suppressViolationsUntil) { suppressViolationsUntil = next } suppressMu.Unlock() } func violationsSuppressed() bool { suppressMu.Lock() defer suppressMu.Unlock() return time.Now().Before(suppressViolationsUntil) } // Start giám sát môi trường Windows — vi phạm thì gọi onViolation (đổi user, đa màn hình, đổi desktop ảo). func Start(onViolation func(reason string)) { guardOnce.Do(func() { if onViolation == nil { return } guardViolation = onViolation guardStop = make(chan struct{}) guardBaselineUser = currentUsername() guardBaselineSession = currentSessionID() if monitorCount() > 1 { onViolation("Phát hiện nhiều hơn 1 màn hình. Vui lòng chỉ dùng một màn hình khi chạy Simple Care.") return } go pollLoop() go runMessageWindow() }) } func Stop() { if guardStop != nil { close(guardStop) } } func pollLoop() { ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() for { select { case <-guardStop: return case <-ticker.C: checkEnvironment() } } } func checkEnvironment() { if guardViolation == nil || violationsSuppressed() { return } if n := monitorCount(); n > 1 { guardViolation("Phát hiện nhiều hơn 1 màn hình. Vui lòng rút/bật tắt màn hình phụ.") return } user := currentUsername() if user != "" && guardBaselineUser != "" && user != guardBaselineUser { guardViolation("Phát hiện đổi tài khoản Windows. Ứng dụng sẽ thoát.") return } sid := currentSessionID() if sid != 0 && guardBaselineSession != 0 && sid != guardBaselineSession { guardViolation("Phiên đăng nhập Windows đã thay đổi. Ứng dụng sẽ thoát.") } } func triggerViolation(reason string) { if violationsSuppressed() { log.Printf("[GUARD] suppressed: %s", reason) return } if guardViolation != nil { guardViolation(reason) } } func monitorCount() int { n, _, _ := procGetSystemMetrics.Call(smCMonitors) return int(n) } func currentSessionID() uint32 { pid, _, _ := procGetCurrentProcessId.Call() var sid uint32 procProcessIdToSessionId.Call(pid, uintptr(unsafe.Pointer(&sid))) return sid } func currentUsername() string { if u, ok := syscall.Getenv("USERNAME"); ok { return u } return "" } func runMessageWindow() { className, _ := syscall.UTF16PtrFromString("SimpleCareGuardWnd") hInstance := syscall.Handle(0) wndProc := syscall.NewCallback(guardWndProc) wc := wndclassEx{ Size: uint32(unsafe.Sizeof(wndclassEx{})), WndProc: wndProc, Instance: hInstance, ClassName: className, } atom, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))) if atom == 0 { log.Println("[GUARD] RegisterClassEx failed") return } guardClassAtom = uint16(atom) title, _ := syscall.UTF16PtrFromString("SimpleCareGuard") hwnd, _, _ := procCreateWindowExW.Call( 0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), 0, 0, 0, 0, 0, 0, 0, uintptr(hInstance), 0, ) if hwnd == 0 { log.Println("[GUARD] CreateWindowEx failed") return } defer procDestroyWindow.Call(hwnd) procWTSRegisterSessionNotification.Call(hwnd, 0) desktopHook, _, _ = procSetWinEventHook.Call( eventSystemDesktopSwitch, eventSystemDesktopSwitch, 0, syscall.NewCallback(desktopSwitchCallback), 0, 0, wineventOutofcontext, ) defer func() { if desktopHook != 0 { procUnhookWinEvent.Call(desktopHook) } procWTSUnRegisterSessionNotification.Call(hwnd) }() var m msg for { select { case <-guardStop: return default: } ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0) if ret == 0 || ret == ^uintptr(0) { return } procTranslateMessage.Call(uintptr(unsafe.Pointer(&m))) procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m))) } } func guardWndProc(hwnd, msg, wParam, lParam uintptr) uintptr { switch uint32(msg) { case wmWtsSessionChange: switch uint32(wParam) { case wtsSessionLock, wtsSessionLogoff, wtsConsoleDisconnect, wtsRemoteDisconnect: triggerViolation("Phiên Windows bị khóa, đăng xuất hoặc chuyển người dùng. Ứng dụng sẽ thoát.") } } r, _, _ := procDefWindowProcW.Call(hwnd, msg, wParam, lParam) return r } func desktopSwitchCallback(hWinEventHook, event, hwnd, idObject, idChild, idEventThread, dwmsEventTime uintptr) uintptr { if event == eventSystemDesktopSwitch { triggerViolation("Không được chuyển Desktop ảo (Win+Tab). Ứng dụng sẽ thoát.") } return 0 }