diff --git a/test/pyxtest/proto/screensaver.py b/test/pyxtest/proto/screensaver.py index ea452be87..1eea9ae95 100644 --- a/test/pyxtest/proto/screensaver.py +++ b/test/pyxtest/proto/screensaver.py @@ -7,6 +7,9 @@ from dataclasses import dataclass # ScreenSaver minor opcodes ScreenSaverQueryVersion = 0 +ScreenSaverSelectInput = 1 +ScreenSaverSetAttributes = 3 +ScreenSaverUnsetAttributes = 4 ScreenSaverSuspend = 5 @@ -27,3 +30,73 @@ class SuspendRequest: length, self.suspend, ) + + +@dataclass +class SetAttributesRequest: + """ScreenSaverSetAttributes request (28 bytes = 7 words, mask=0). + + xScreenSaverSetAttributesReq: + reqType(1) + saverReqType(1) + length(2) + drawable(4) + x(2) + y(2) + width(2) + height(2) + borderWidth(2) + c_class(1) + depth(1) + visualID(4) + mask(4) + [values...] + """ + + opcode: int + drawable: int + x: int = 0 + y: int = 0 + width: int = 100 + height: int = 100 + border_width: int = 0 + c_class: int = 0 # CopyFromParent + depth: int = 0 # CopyFromParent + visual_id: int = 0 # CopyFromParent + mask: int = 0 + values: bytes = b"" + + def to_bytes(self, byte_order: str = "<") -> bytes: + total_bytes = 28 + len(self.values) + pad_len = (4 - total_bytes % 4) % 4 + total_bytes += pad_len + length = total_bytes // 4 + + header = struct.pack( + f"{byte_order}BBHIhhHHHBBII", + self.opcode, + ScreenSaverSetAttributes, + length, + self.drawable, + self.x, + self.y, + self.width, + self.height, + self.border_width, + self.c_class, + self.depth, + self.visual_id, + self.mask, + ) + return header + self.values + b"\x00" * pad_len + + +@dataclass +class UnsetAttributesRequest: + """ScreenSaverUnsetAttributes request (8 bytes = 2 words).""" + + opcode: int + drawable: int + + def to_bytes(self, byte_order: str = "<") -> bytes: + return struct.pack( + f"{byte_order}BBH I", + self.opcode, + ScreenSaverUnsetAttributes, + 2, # length = 2 words + self.drawable, + ) diff --git a/test/pyxtest/proto/x11.py b/test/pyxtest/proto/x11.py index 5358bf942..6a0b40dc5 100644 --- a/test/pyxtest/proto/x11.py +++ b/test/pyxtest/proto/x11.py @@ -10,6 +10,11 @@ CreateWindow = 1 CreatePixmap = 53 InternAtom = 16 QueryExtension = 98 +ForceScreenSaverOpcode = 115 + + +ScreenSaverReset = 0 +ScreenSaverActive = 1 def _pad(data: bytes) -> bytes: @@ -116,3 +121,18 @@ class QueryExtensionRequest: ) + padded ) + + +@dataclass +class ForceScreenSaver: + """X11 ForceScreenSaver request.""" + + mode: int + + def to_bytes(self, byte_order: str = "<") -> bytes: + return struct.pack( + f"{byte_order}BBH", + ForceScreenSaverOpcode, + self.mode, + 1, # length = 1 word + ) diff --git a/test/pyxtest/test_screensaver.py b/test/pyxtest/test_screensaver.py index cfcb7655f..eb43b85ae 100644 --- a/test/pyxtest/test_screensaver.py +++ b/test/pyxtest/test_screensaver.py @@ -6,10 +6,19 @@ import time import pytest -from proto import screensaver +from proto import screensaver, x11 from xclient import Extension +@pytest.fixture +def screensaver_xclient(xclient): + """Provide an xclient with the MIT-SCREEN-SAVER extension queried.""" + ext = xclient.query_extension(Extension.MIT_SCREEN_SAVER) + if not ext: + pytest.skip("MIT-SCREEN-SAVER extension not available") + return xclient, ext.opcode + + class TestScreenSaverSuspend: """Tests for SProcScreenSaverSuspend vulnerabilities.""" @@ -44,3 +53,70 @@ class TestScreenSaverSuspend: assert xserver.is_alive, ( "Server crashed - SProcScreenSaverSuspend (CVE-2021-4010)" ) + + +class TestCreateSaverWindow: + """Tests for CreateSaverWindow use-after-free via CheckScreenPrivate.""" + + @pytest.mark.asan + def test_create_saver_window_uaf(self, xserver, screensaver_xclient): + """ + ZDI-CAN-30168: CreateSaverWindow stores pPriv in a local + variable at function entry. When an existing saver window is + being replaced, it sets pPriv->hasWindow = FALSE and calls + CheckScreenPrivate(). If pPriv->attr is NULL (cleared by a + prior UnsetAttributes), pPriv->events is NULL, and + pPriv->installedMap is None, CheckScreenPrivate frees pPriv + and sets the screen private to NULL. The function then + dereferences the freed pPriv->attr pointer on the next line. + + Attack sequence: + 1. SetAttributes (creates pPriv with pPriv->attr set) + 2. ForceScreenSaver(Active) (creates saver window) + 3. UnsetAttributes (sets pPriv->attr = NULL) + 4. ForceScreenSaver(Active) (re-enters CreateSaverWindow → UAF) + + Fixed by re-fetching pPriv from the screen private after + CheckScreenPrivate returns. + """ + conn, opcode = screensaver_xclient + + # Step 1: SetAttributes(root, 100x100, mask=0) + # Creates pPriv with pPriv->attr set. + req = screensaver.SetAttributesRequest( + opcode=opcode, + drawable=conn.root_window, + width=100, + height=100, + mask=0, + ) + conn.send_request(req.to_bytes()) + conn.flush_responses(timeout=0.5) + + # Step 2: ForceScreenSaver(Active) + # Activates the screen saver, creating the saver window. + req = x11.ForceScreenSaver(mode=x11.ScreenSaverActive) + conn.send_request(req.to_bytes()) + conn.flush_responses(timeout=0.5) + + time.sleep(0.2) + + # Step 3: UnsetAttributes(root) + # Sets pPriv->attr = NULL but does not destroy the saver window. + req = screensaver.UnsetAttributesRequest( + opcode=opcode, + drawable=conn.root_window, + ) + conn.send_request(req.to_bytes()) + conn.flush_responses(timeout=0.5) + + # Step 4: ForceScreenSaver(Active) again → triggers UAF. + # CreateSaverWindow: cleanup block frees pPriv via + # CheckScreenPrivate, then reads pPriv->attr from freed memory. + req = x11.ForceScreenSaver(mode=x11.ScreenSaverActive) + conn.send_request(req.to_bytes()) + time.sleep(0.5) + + assert xserver.is_alive, ( + "Server crashed - CreateSaverWindow UAF (ZDI-CAN-30168)" + )