From c0611d5fca97d2baa9812fa0c7b22d6796f88c73 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 11:35:28 +0000 Subject: [PATCH] feat(helper): fully manage elevated windows; add opt-in uiAccess build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make mshell own admin-owned windows as completely as ordinary ones, and spell out the anti-cheat-safe posture that keeps this usable on a gaming machine. Nothing here injects into another process or touches the boot chain, so Secure Boot / TPM anti-cheat is unaffected. Helper protocol (proto v5): - PROTO_SHOW: ShowWindow() on a higher-integrity window, so a minimized admin window can actually be restored. ShowWindow can't report a UIPI refusal, so window_unminimize() and window_focus() now confirm the iconic bit cleared and fall back to the elevated helper when it didn't — previously a minimized admin window was unrecoverable with no taskbar. - PROTO_SETSTYLE: set GWL_STYLE/GWL_EXSTYLE cross-integrity, so an admin window loses its title bar and min/max buttons like every other tiled window. window_strip/restore_decorations() use it when the direct call is refused. uiAccess build variant (opt-in, make UIACCESS=1): - Separate uiAccess="true" manifest selected via a windres macro; the default asInvoker build is unchanged so an unsigned shell still boots. - Report the real TokenUIAccess state at startup (the manifest alone does not grant it — the binary must be signed and under Program Files). - INSTALL.md documents the signing / location requirements and the verify step. Also: - Set SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0 process-wide so SDL games stop self-minimizing on focus loss instead of relying on the restore backstop. - probe_uipi: new `manage` subcommand that replays strip/restore/move against an elevated window to confirm on-machine what UIPI refuses. - README: document the no-injection / boot-chain non-goal explicitly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017weNsujuHCgUzdttrry2Qe --- INSTALL.md | 39 ++++++++++++++++ Makefile | 17 ++++++- README.md | 10 ++++ src/helper.c | 26 +++++++++++ src/main.c | 43 ++++++++++++++++-- src/mshell.h | 4 ++ src/mshell.rc | 9 +++- src/mshell.uiaccess.manifest | 52 +++++++++++++++++++++ src/mshelld.c | 42 +++++++++++++++++ src/proto.h | 21 ++++++--- src/window_decor.c | 20 +++++++- src/window_visibility.c | 15 ++++++ src/window_zorder.c | 7 ++- tools/probe_uipi.c | 88 ++++++++++++++++++++++++++++++++++++ 14 files changed, 377 insertions(+), 16 deletions(-) create mode 100644 src/mshell.uiaccess.manifest diff --git a/INSTALL.md b/INSTALL.md index 9007c53..31f449d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -259,6 +259,45 @@ The recommended option above — leave UAC on, let the helper do the privileged work — avoids all of this: the elevated half is a small binary with no config and no scripting, and `init.lua` stays unprivileged. +### uiAccess (advanced — removes the last UIPI gap without elevating) + +There is a third route that needs neither an elevated mshell nor the helper: +build mshell with the **uiAccess** manifest. A uiAccess process may bypass the +UIPI restrictions that stop an unelevated shell from touching elevated windows, +so a single unelevated mshell can tile, restore, re-style and close them **and** +its keyboard hook keeps working while an elevated window has focus — the one +thing the helper cannot do. It stays unelevated, so `init.lua` stays +unprivileged and auto-reload keeps working. + +Windows guards the flag strictly, and these are hard requirements, not +suggestions: + +- **The binary must be Authenticode-signed by a certificate the machine + trusts.** The PKI signature check is always enforced for a uiAccess process. +- **It must live under a protected path** — `C:\Program Files\...` or + `C:\Windows\System32\...`. `C:\mshell` does **not** qualify, so the usual + install location has to change for this build. + +If either is not met, Windows refuses the flag — the process may launch without +it, or fail to launch at all. Because being started as the Winlogon shell is an +unusual launch path for a uiAccess binary, **verify it actually worked**: the +startup log says `uiAccess GRANTED` when the flag is live. If it doesn't, you +are running an ordinary build and should keep the helper. + +Building it: + +``` +make UIACCESS=1 # embeds src/mshell.uiaccess.manifest instead of the default +``` + +then sign `mshell.exe` and install it under `C:\Program Files\mshell\`. For a +personal machine you can create your own certificate, trust it machine-wide, +sign with it, and delete the private key afterwards so nothing else can ever be +signed as mshell; a public release needs a real code-signing certificate. The +default `make` build is unchanged and still boots unsigned from `C:\mshell`, so +nothing here is required — it is the maximal-control option for those who want +it. + ## 5. Turn off the rest of Windows (optional, recommended) mshell itself already eats every `Win`+* combo, Alt+Tab / Alt+Esc / Ctrl+Esc, diff --git a/Makefile b/Makefile index d5b2dbb..41a5d89 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,19 @@ STRICT_WARNINGS = -Wshadow -Wformat=2 -Wvla RCFLAGS = -DVER_MAJOR=$(VER_MAJOR) \ -DVER_MINOR=$(VER_MINOR) \ -DVER_PATCH=$(VER_PATCH) + +# Opt-in uiAccess build: `make UIACCESS=1` embeds the uiAccess="true" manifest +# instead of the default asInvoker one. The resulting binary must be +# Authenticode-signed and installed under Program Files or Windows will not +# honour the flag (it may even refuse to launch) — see src/mshell.uiaccess.manifest +# and INSTALL.md. The default build is unchanged, so an unsigned shell still boots. +UIACCESS ?= +ifneq ($(UIACCESS),) +RCFLAGS += -DMSHELL_MANIFEST='\"mshell.uiaccess.manifest\"' +MANIFEST_FILE = $(SRC_DIR)/mshell.uiaccess.manifest +else +MANIFEST_FILE = $(SRC_DIR)/mshell.exe.manifest +endif LDFLAGS = -luser32 -lgdi32 -lshell32 -lole32 -luuid -ldwmapi -lwtsapi32 \ -ladvapi32 -lpowrprof -lwinhttp -lbcrypt \ -lruntimeobject -lm @@ -223,8 +236,8 @@ $(SRC_DIR)/%.o: $(SRC_DIR)/%.c $(SRC_DIR)/mshell.h @echo " CC $<" $(CC) $(CFLAGS) $(STRICT_WARNINGS) -c -o $@ $< -$(RES_OBJ): $(SRC_DIR)/mshell.rc $(SRC_DIR)/mshell.exe.manifest - @echo " RC $<" +$(RES_OBJ): $(SRC_DIR)/mshell.rc $(MANIFEST_FILE) + @echo " RC $< ($(notdir $(MANIFEST_FILE)))" $(WINDRES) $(RCFLAGS) -I$(SRC_DIR) -O coff -i $< -o $@ $(LUA_DIR)/%.o: $(LUA_DIR)/%.c diff --git a/README.md b/README.md index 7a4883a..b6d5db7 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,16 @@ tiled, driven entirely from the keyboard and configured in Lua. with `mshell.exec("mcapture.exe", "--select")` like any other program. - Single global instance, low-level keyboard hook, out-of-context WinEvent hooks — no DLL injection. +- **Anti-cheat safe by construction.** mshell never injects a DLL, creates a + remote thread, or writes into another process's memory, and it never touches + the boot chain (no kernel driver, no test-signing, so Secure Boot and TPM + stay intact for Vanguard, Javelin, Ricochet, EAC, BattlEye and FACEIT). It + only ever calls public window APIs from its own process, reads a game + process's name and path to match a rule, and — where a window is owned by a + higher-integrity process — asks the elevated helper (`mshelld.exe`) to move, + restore, re-style or close it. This is a deliberate non-goal: mshell will not + gain a feature that requires reaching into another process, because doing so + is exactly what kernel anti-cheat is built to flag. ## Build diff --git a/src/helper.c b/src/helper.c index adec9f3..b70d0b9 100644 --- a/src/helper.c +++ b/src/helper.c @@ -236,6 +236,32 @@ bool helper_close_window(HWND hwnd) { return helper_request(&req, L"a window could not be closed", &warned); } +bool helper_show_window(HWND hwnd, int cmd) { + static bool warned; + + ProtoMsg req = { + .type = PROTO_SHOW, + .version = MSHELLD_PROTO_VERSION, + .hwnd = (uint64_t)(uintptr_t)hwnd, + .flags = (uint32_t)cmd, + }; + return helper_request(&req, L"a window could not be restored", &warned); +} + +bool helper_set_style(HWND hwnd, LONG_PTR style, LONG_PTR exstyle) { + static bool warned; + + ProtoMsg req = { + .type = PROTO_SETSTYLE, + .version = MSHELLD_PROTO_VERSION, + .hwnd = (uint64_t)(uintptr_t)hwnd, + .style = (int64_t)style, + .exstyle = (int64_t)exstyle, + }; + return helper_request(&req, L"a window's frame could not be stripped", + &warned); +} + #define HELPER_TASK_NAME L"mshelld" #define HELPER_SETTLE_MS 1500 #define HELPER_SCHTASKS_TIMEOUT_MS 15000 diff --git a/src/main.c b/src/main.c index 7fd1663..13aae84 100644 --- a/src/main.c +++ b/src/main.c @@ -18,8 +18,35 @@ static bool is_elevated(void) { return ok && elev.TokenIsElevated != 0; } +/* Whether this process actually holds the uiAccess token flag. A build made + with `make UIACCESS=1` only gets it when Windows accepts the manifest, which + needs the binary signed and under Program Files — so this is the authoritative + check, not the manifest. */ +static bool has_uiaccess(void) { + HANDLE token = NULL; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + return false; + + DWORD ui = 0, size = sizeof(ui); + BOOL ok = GetTokenInformation(token, TokenUIAccess, &ui, size, &size); + CloseHandle(token); + return ok && ui != 0; +} + static UINT g_prev_fg_lock_timeout = 0; +/* Programs mshell launches inherit its environment. SDL's default is to + minimize a fullscreen game the moment it loses focus — which fights the + "nothing minimizes" rule, since there is no taskbar to bring it back from, + and mshell would then have to restore it after the fact. Turning the hint + off here stops the minimize happening in the first place, for every SDL1/2/3 + game started from the shell. The variable is a no-op for non-SDL programs. */ +static void set_child_env_defaults(void) { + if (!SetEnvironmentVariableW(L"SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS", L"0")) + log_w(L"could not set SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS for launched " + L"games: %lu", GetLastError()); +} + static LONG WINAPI mshell_crash_handler(EXCEPTION_POINTERS *ep) { static LONG entered = 0; if (InterlockedExchange(&entered, 1)) return EXCEPTION_CONTINUE_SEARCH; @@ -376,14 +403,24 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, update_clear_staged_image(); g.elevated = is_elevated(); - - if (!g.elevated && shell_mode) + g.uiaccess = has_uiaccess(); + + if (g.uiaccess) + log_err(L"uiAccess GRANTED — mshell can manage windows owned by " + L"higher-integrity processes directly, and its keyboard hook " + L"keeps working while one has focus. The mshelld.exe helper is " + L"not needed in this mode."); + else if (!g.elevated && shell_mode) log_w(L"Shell mode at user level (UAC on) — windows owned by elevated " - L"processes can't be managed (UIPI)."); + L"processes can't be managed directly (UIPI); the mshelld.exe " + L"helper covers moving/restoring/closing them. (This is not a " + L"uiAccess build, or Windows declined the flag — see INSTALL.md.)"); if (!SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS)) log_w(L"SetPriorityClass(ABOVE_NORMAL) failed: %lu", GetLastError()); + set_child_env_defaults(); + config_reset(); if (!SystemParametersInfoW(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, diff --git a/src/mshell.h b/src/mshell.h index 832c1e3..6e44018 100644 --- a/src/mshell.h +++ b/src/mshell.h @@ -272,6 +272,7 @@ typedef struct { bool fs_has_prev; bool needs_helper; bool place_refused; + bool unminimize_refused; bool always_on_top; bool layout_hidden; bool urgent; @@ -584,6 +585,7 @@ typedef struct { bool panicked; bool safe_mode; bool elevated; + bool uiaccess; } MShell; @@ -834,6 +836,8 @@ bool helper_available(void); bool helper_set_window_pos(HWND hwnd, int x, int y, int w, int h, UINT flags); bool helper_set_topmost(HWND hwnd, bool on); bool helper_close_window(HWND hwnd); +bool helper_show_window(HWND hwnd, int cmd); +bool helper_set_style(HWND hwnd, LONG_PTR style, LONG_PTR exstyle); typedef enum { PLACE_OK = 0, diff --git a/src/mshell.rc b/src/mshell.rc index 0455b25..3349fb0 100644 --- a/src/mshell.rc +++ b/src/mshell.rc @@ -12,7 +12,14 @@ #define CREATEPROCESS_MANIFEST_RESOURCE_ID 1 #endif -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "mshell.exe.manifest" +/* The manifest file is chosen at build time: the default asInvoker one, or the + uiAccess="true" variant when the Makefile passes MSHELL_MANIFEST (make + UIACCESS=1). windres expands the macro to the quoted file name. */ +#ifndef MSHELL_MANIFEST +#define MSHELL_MANIFEST "mshell.exe.manifest" +#endif + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST MSHELL_MANIFEST VS_VERSION_INFO VERSIONINFO FILEVERSION VER_MAJOR,VER_MINOR,VER_PATCH,0 diff --git a/src/mshell.uiaccess.manifest b/src/mshell.uiaccess.manifest new file mode 100644 index 0000000..952b702 --- /dev/null +++ b/src/mshell.uiaccess.manifest @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + PerMonitorV2, PerMonitor, system + true/pm + + + + + + + + + + + + + + diff --git a/src/mshelld.c b/src/mshelld.c index 4c25e18..539dee9 100644 --- a/src/mshelld.c +++ b/src/mshelld.c @@ -273,6 +273,38 @@ static bool do_close(const ProtoMsg *m) { return PostMessageW(h, WM_CLOSE, 0, 0) != 0; } +/* ShowWindow returns the window's PREVIOUS visibility, not success, so we + confirm the state change ourselves: a restore must clear the iconic bit. */ +static bool do_show(const ProtoMsg *m) { + HWND h = (HWND)(uintptr_t)m->hwnd; + int cmd = (int)m->flags; + if (!h || !IsWindow(h)) return false; + + ShowWindow(h, cmd); + + if (cmd == SW_RESTORE || cmd == SW_SHOWNOACTIVATE || + cmd == SW_SHOW || cmd == SW_SHOWNA) + return !IsIconic(h); + return true; +} + +static bool do_setstyle(const ProtoMsg *m) { + HWND h = (HWND)(uintptr_t)m->hwnd; + if (!h || !IsWindow(h)) return false; + + SetLastError(0); + SetWindowLongPtrW(h, GWL_STYLE, (LONG_PTR)m->style); + SetWindowLongPtrW(h, GWL_EXSTYLE, (LONG_PTR)m->exstyle); + + /* A style change only takes visual effect once the frame is recomputed. */ + SetWindowPos(h, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_NOACTIVATE | SWP_FRAMECHANGED); + + return (GetWindowLongPtrW(h, GWL_STYLE) & 0xFFFFFFFFu) == + ((LONG_PTR)m->style & 0xFFFFFFFFu); +} + static void serve(const Client *c) { bool greeted = false; @@ -319,6 +351,16 @@ static void serve(const Client *c) { else if (!do_close(&in)) out.type = PROTO_FAIL; break; + case PROTO_SHOW: + if (!greeted) out.type = PROTO_FAIL; + else if (!do_show(&in)) out.type = PROTO_FAIL; + break; + + case PROTO_SETSTYLE: + if (!greeted) out.type = PROTO_FAIL; + else if (!do_setstyle(&in)) out.type = PROTO_FAIL; + break; + default: out.type = PROTO_FAIL; break; diff --git a/src/proto.h b/src/proto.h index 485e37f..d56f582 100644 --- a/src/proto.h +++ b/src/proto.h @@ -2,16 +2,18 @@ #include -#define MSHELLD_PROTO_VERSION 4u +#define MSHELLD_PROTO_VERSION 5u #define MSHELLD_PIPE_PREFIX L"\\\\.\\pipe\\mshelld-" typedef enum { - PROTO_HELLO = 1, - PROTO_SETPOS = 2, - PROTO_ZORDER = 3, - PROTO_CLOSE = 5, - PROTO_OK = 6, - PROTO_FAIL = 7, + PROTO_HELLO = 1, + PROTO_SETPOS = 2, + PROTO_ZORDER = 3, + PROTO_CLOSE = 5, + PROTO_OK = 6, + PROTO_FAIL = 7, + PROTO_SHOW = 8, /* ShowWindow(hwnd, (int)flags) on a higher-integrity window */ + PROTO_SETSTYLE = 9, /* set GWL_STYLE/GWL_EXSTYLE, then frame-change, cross-integrity */ } ProtoType; typedef struct { @@ -21,4 +23,9 @@ typedef struct { uint64_t hwnd; int32_t x, y, w, h; uint32_t flags; + + /* PROTO_SETSTYLE: the whole GWL_STYLE / GWL_EXSTYLE words to assert. + 64-bit because LONG_PTR is 64-bit on the only architecture we build. */ + int64_t style; + int64_t exstyle; } ProtoMsg; diff --git a/src/window_decor.c b/src/window_decor.c index 8fd5a7e..8ad5464 100644 --- a/src/window_decor.c +++ b/src/window_decor.c @@ -73,11 +73,21 @@ void window_strip_decorations(HWND hwnd) { SWP_NOACTIVATE | SWP_FRAMECHANGED); bool stripped = !(GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_CAPTION); + + /* A window owned by a higher-integrity process ignores SetWindowLongPtr + from here (UIPI), so it keeps its title bar and min/max buttons. The + elevated helper can set the style for us — it did the frame-change too. */ + if (!stripped) { + stripped = helper_set_style(hwnd, style, exstyle) && + !(GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_CAPTION); + } + if (!stripped && !mw->decor_strip_refused) { mw->decor_strip_refused = true; log_msg(LOG_WARN, L"could not strip decorations from %p — the window " - L"belongs to a higher-integrity process and keeps its " - L"title bar", (void *)hwnd); + L"belongs to a higher-integrity process and no " + L"mshelld.exe is running to do it, so it keeps its " + L"title bar (see INSTALL.md)", (void *)hwnd); } mw->decorations_stripped = stripped; @@ -109,6 +119,12 @@ void window_restore_decorations(HWND hwnd) { SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + /* If the strip went through the helper, the restore must too: a higher- + integrity window would otherwise keep the stripped frame after mshell + exits. */ + if (GetWindowLongPtrW(hwnd, GWL_STYLE) != mw->orig_style) + helper_set_style(hwnd, mw->orig_style, mw->orig_exstyle); + mw->decorations_stripped = false; frame_props_init(); RemovePropW(hwnd, PROP_ORIG_STYLE); diff --git a/src/window_visibility.c b/src/window_visibility.c index 7b56ec8..935a43f 100644 --- a/src/window_visibility.c +++ b/src/window_visibility.c @@ -270,6 +270,21 @@ void window_unminimize(ManagedWindow *mw) { events_suppress_begin(); ShowWindow(mw->hwnd, SW_SHOWNOACTIVATE); + + /* ShowWindow can't report a UIPI refusal: on a window owned by a higher- + integrity process it silently does nothing and the window stays + minimized with no taskbar to bring it back from. Confirm it actually + un-minimized, and hand it to the elevated helper if it didn't. */ + if (IsIconic(mw->hwnd)) { + if (!helper_show_window(mw->hwnd, SW_SHOWNOACTIVATE) && + !mw->unminimize_refused) { + mw->unminimize_refused = true; + log_msg(LOG_WARN, L"could not un-minimize %p — it belongs to a " + L"higher-integrity process and no mshelld.exe is " + L"running to restore it (see INSTALL.md).", + (void *)mw->hwnd); + } + } events_suppress_end(); mw->has_applied = false; } diff --git a/src/window_zorder.c b/src/window_zorder.c index 5922e0f..1f4296b 100644 --- a/src/window_zorder.c +++ b/src/window_zorder.c @@ -207,7 +207,12 @@ static void claim_foreground_rights(void) { void window_focus(HWND hwnd) { if (!hwnd || !IsWindow(hwnd)) return; - if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE); + if (IsIconic(hwnd)) { + ShowWindow(hwnd, SW_RESTORE); + /* A higher-integrity window ignores the call above; let the helper + restore it so focusing a minimized admin window still works. */ + if (IsIconic(hwnd)) helper_show_window(hwnd, SW_RESTORE); + } SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); diff --git a/tools/probe_uipi.c b/tools/probe_uipi.c index b54f109..7be7a61 100644 --- a/tools/probe_uipi.c +++ b/tools/probe_uipi.c @@ -370,6 +370,92 @@ static int cmd_syscommand(int argc, wchar_t **argv) { return pass ? 0 : 1; } +/* Replay the three things mshell does to a managed window — strip the frame, + un-minimize it, move it — directly from this (unelevated) process, so we can + see which ones a higher-integrity window silently refuses. This is the + on-machine confirmation of why elevated windows need the helper or uiAccess. */ +static int cmd_manage(int argc, wchar_t **argv) { + HWND target = resolve_target(argc, argv, 2); + if (!target) { + wprintf(L"FAIL no target: open an elevated window (e.g. Task Manager) " + L"or pass an HWND\n"); + return 2; + } + + wchar_t desc[128]; + describe_window(target, desc, 128); + wprintf(L"target %p %ls\n\n", (void *)target, desc); + + DWORD protect = try_setpos_noop(target); + wprintf(L"guard no-op SetWindowPos error = %ld %ls\n\n", (long)protect, + protect == ERROR_ACCESS_DENIED + ? L"(UIPI-protected: this is the interesting case)" + : L"(NOT protected from here — try an elevated target unelevated)"); + + /* [1] Strip WS_CAPTION, as window_strip_decorations does. */ + wprintf(L"[1] SetWindowLongPtr(GWL_STYLE) to drop WS_CAPTION/SYSMENU/" + L"MIN/MAXBOX\n"); + LONG_PTR style0 = GetWindowLongPtrW(target, GWL_STYLE); + LONG_PTR want = style0 & ~(LONG_PTR)(WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX); + SetLastError(0); + SetWindowLongPtrW(target, GWL_STYLE, want); + DWORD e_style = GetLastError(); + SetWindowPos(target, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + LONG_PTR style1 = GetWindowLongPtrW(target, GWL_STYLE); + bool stripped = !(style1 & WS_CAPTION); + wprintf(L" set error = %lu caption now %ls -> %ls\n", + e_style, stripped ? L"GONE" : L"STILL PRESENT", + stripped ? L"strip SUCCEEDED" : L"strip REFUSED (UIPI)"); + if (stripped) { /* put it back so the probe is non-destructive */ + SetWindowLongPtrW(target, GWL_STYLE, style0); + SetWindowPos(target, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } + wprintf(L"\n"); + + /* [2] Minimize via the window's own system menu (always allowed), then try + to un-minimize it the way mshell does — ShowWindow(SW_SHOWNOACTIVATE). */ + wprintf(L"[2] minimize, then ShowWindow(SW_SHOWNOACTIVATE) to bring it back\n"); + SendMessageTimeoutW(target, WM_SYSCOMMAND, SC_MINIMIZE, 0, + SMTO_ABORTIFHUNG, 1000, NULL); + bool went_down = wait_for(target, is_iconic, 1500); + wprintf(L" minimized = %d\n", went_down); + if (went_down) { + ShowWindow(target, SW_SHOWNOACTIVATE); + bool back = wait_for(target, is_not_iconic, 1500); + wprintf(L" ShowWindow restored = %ls\n", + back ? L"YES — restore works from here" + : L"NO — restore SILENTLY REFUSED (needs helper/uiAccess)"); + if (!back) { /* don't leave the user's window stuck minimized */ + DWORD e_sc = try_post(target, WM_SYSCOMMAND, SC_RESTORE, 0); + wprintf(L" (left minimized; SC_RESTORE post error = %ld)\n", + (long)e_sc); + } + } + wprintf(L"\n"); + + /* [3] Move it 1px and back, as the tiler's SetWindowPos does. */ + wprintf(L"[3] SetWindowPos move (what tiling does)\n"); + RECT r = {0}; + GetWindowRect(target, &r); + SetLastError(0); + BOOL moved = SetWindowPos(target, NULL, r.left + 1, r.top, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + DWORD e_move = moved ? 0 : GetLastError(); + if (moved) SetWindowPos(target, NULL, r.left, r.top, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + wprintf(L" moved = %d error = %ld -> %ls\n\n", moved != 0, (long)e_move, + moved ? L"move allowed" : L"move REFUSED (UIPI)"); + + wprintf(L"VERDICT on a UIPI-protected target, expect strip/restore/move to " + L"be refused from here;\n" + L" that is exactly the gap mshelld.exe (or a uiAccess build) " + L"closes.\n"); + return 0; +} + static const wchar_t *mods_name(UINT mods, wchar_t *buf, size_t cap) { _snwprintf(buf, cap, L"%ls%ls%ls%ls", (mods & MOD_WIN) ? L"Win+" : L"", @@ -743,6 +829,7 @@ static int cmd_hotkey(int argc, wchar_t **argv) { static void usage(void) { wprintf(L"usage: probe_uipi integrity\n" L" probe_uipi syscommand [hwnd] [--close]\n" + L" probe_uipi manage [hwnd]\n" L" probe_uipi hotkey [--mode mshell|pass] [--seconds N] [--stall]\n" L" probe_uipi register-all\n"); } @@ -761,6 +848,7 @@ int wmain(int argc, wchar_t **argv) { if (argc < 2) { usage(); return 2; } if (_wcsicmp(argv[1], L"integrity") == 0) return cmd_integrity(); if (_wcsicmp(argv[1], L"syscommand") == 0) return cmd_syscommand(argc, argv); + if (_wcsicmp(argv[1], L"manage") == 0) return cmd_manage(argc, argv); if (_wcsicmp(argv[1], L"hotkey") == 0) return cmd_hotkey(argc, argv); if (_wcsicmp(argv[1], L"register-all") == 0) return cmd_register_all(); usage();