Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 15 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions src/helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 40 additions & 3 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/mshell.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -584,6 +585,7 @@ typedef struct {
bool panicked;
bool safe_mode;
bool elevated;
bool uiaccess;

} MShell;

Expand Down Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion src/mshell.rc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions src/mshell.uiaccess.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<!--
uiAccess variant of the manifest. Build it with `make UIACCESS=1`.

uiAccess="true" lets an unelevated mshell drive windows owned by higher-
integrity (elevated) processes WITHOUT the mshelld.exe helper, and keeps the
keyboard hook working while such a window has focus. The cost is strict:

* Windows will only grant the flag to a binary that is Authenticode-signed
by a trusted certificate AND installed under a protected path
(C:\Program Files\... or C:\Windows\System32\...). An unsigned or
wrongly-located uiAccess binary will FAIL to launch, or launch without
the flag — so this is NOT the default build, and never ship it unsigned.
* Being started as the Winlogon shell is an untested launch path for the
flag; verify with `mshell.exe --version` / the startup log line that says
whether uiAccess was actually granted before relying on it.

See INSTALL.md, "uiAccess".
-->

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">

<assemblyIdentity type="win32" name="mshell.mshell" version="1.0.0.0"
processorArchitecture="amd64"/>

<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="true"/>
</requestedPrivileges>
</security>
</trustInfo>

<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor, system</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
</windowsSettings>
</application>

<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>

</assembly>
42 changes: 42 additions & 0 deletions src/mshelld.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
21 changes: 14 additions & 7 deletions src/proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

#include <stdint.h>

#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 {
Expand All @@ -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;
Loading
Loading