9remote 2.2.1 → 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,19 @@ class DesktopBridge {
29
29
  const ushort VK_RETURN = 0x0D;
30
30
  const ushort VK_SHIFT = 0x10;
31
31
  const string PIPE = "9remote-desktop";
32
+ // Bump on every change to this file. The agent reads the same constant out of
33
+ // the shipped .cs and compares it against what the running worker reports, so
34
+ // a stale worker is detected without relying on file mtimes. Same contract as
35
+ // DAEMON_VERSION for the pty daemon.
36
+ const string VERSION = "2";
37
+ // Global\ (not Local\): the boot task runs in session 0 while a user-triggered
38
+ // launcher runs in the console session — a per-session mutex would not see across them.
39
+ const string MUTEX_NAME = "Global\\9remote-desktop-bridge";
40
+ // How long a starting worker waits for a shutting-down one to release the lock.
41
+ const int HANDOFF_WAIT_MS = 10000;
42
+
43
+ // Static so the GC never collects it while Main loops forever.
44
+ static Mutex _instanceLock;
32
45
 
33
46
  [StructLayout(LayoutKind.Sequential)]
34
47
  struct MOUSEINPUT { public int dx, dy; public uint mouseData, dwFlags, time; public IntPtr dwExtraInfo; }
@@ -112,7 +125,7 @@ class DesktopBridge {
112
125
  short k = VkKeyScanW(c);
113
126
  ushort vk = (ushort)(k & 0xFF);
114
127
  bool shift = (k & 0x0100) != 0;
115
- L(" char='" + c + "' vk=0x" + vk.ToString("X2") + " shift=" + shift);
128
+ // Never log the char or its vk TYPE carries the user's password.
116
129
  if (shift) {
117
130
  SendKey(VK_SHIFT, false);
118
131
  SendKey(vk, false); Thread.Sleep(25); SendKey(vk, true);
@@ -128,6 +141,28 @@ class DesktopBridge {
128
141
  }
129
142
 
130
143
  static void Main() {
144
+ // Single-instance guard. The pipe is created with maxInstances=1, so a second
145
+ // worker could never serve — it would spin in the retry loop forever while
146
+ // holding a lock on this .exe, blocking every future csc rebuild.
147
+ //
148
+ // Held in a static field, not a local: GC.KeepAlive only protects up to the
149
+ // call, after which nothing references a local mutex for the rest of this
150
+ // infinite loop — the finalizer would release it mid-run and let a second
151
+ // worker in. A static root lives as long as the process.
152
+ // Never let the guard itself kill the worker: if the mutex can't be created
153
+ // or opened (ACL, exotic session), carry on unguarded — the pipe's own
154
+ // maxInstances=1 still prevents two workers from serving at once.
155
+ try {
156
+ bool isNew;
157
+ _instanceLock = new Mutex(true, MUTEX_NAME, out isNew);
158
+ if (!isNew) {
159
+ // The previous worker may be shutting down right now (agent issued STOP,
160
+ // then relaunched us via the task). Wait for it to release rather than
161
+ // exiting into a gap where no worker is running at all.
162
+ if (!_instanceLock.WaitOne(HANDOFF_WAIT_MS)) { L("another worker still holds the lock — exiting"); return; }
163
+ L("took over from a previous instance");
164
+ }
165
+ } catch (Exception e) { L("mutex guard unavailable: " + e.Message); }
131
166
  try { L("start session=" + Process.GetCurrentProcess().SessionId + " pid=" + Process.GetCurrentProcess().Id + " user=" + WindowsIdentity.GetCurrent().Name); } catch {}
132
167
  // ACL: SYSTEM + Administrators + Authenticated Users so the user-scope agent can connect.
133
168
  var sec = new PipeSecurity();
@@ -150,17 +185,20 @@ class DesktopBridge {
150
185
  while ((line = sr.ReadLine()) != null) {
151
186
  line = line.Trim();
152
187
  if (line.Length == 0) continue;
153
- if (line == "STATE") {
188
+ if (line == "VERSION") {
189
+ sw.WriteLine("VERSION " + VERSION);
190
+ } else if (line == "STATE") {
154
191
  sw.WriteLine("DESKTOP " + ActiveDesktop());
155
192
  } else if (line.StartsWith("TYPE ")) {
156
193
  TypeText(line.Substring(5));
157
194
  sw.WriteLine("OK");
158
195
  } else if (line == "STOP") {
159
196
  sw.WriteLine("OK");
160
- // Non-zero exit so the scheduled task's restart-on-failure policy
161
- // fires and re-launches the worker (with the freshly rebuilt exe).
197
+ // Clean exit: STOP is an intentional shutdown (user toggled Off, or
198
+ // the agent is freeing the locked exe for a rebuild). Restarting here
199
+ // would defeat both. The boot task brings it back next reboot.
162
200
  try { srv.Dispose(); } catch { }
163
- Environment.Exit(1);
201
+ Environment.Exit(0);
164
202
  } else {
165
203
  sw.WriteLine("ERR unknown");
166
204
  }
@@ -6,6 +6,7 @@ using System;
6
6
  using System.Diagnostics;
7
7
  using System.IO;
8
8
  using System.Runtime.InteropServices;
9
+ using System.Threading;
9
10
 
10
11
  class DesktopElevate {
11
12
  [DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId();
@@ -18,6 +19,9 @@ class DesktopElevate {
18
19
  [DllImport("advapi32.dll", SetLastError = true)] static extern bool AdjustTokenPrivileges(IntPtr h, bool dis, ref TOKEN_PRIVILEGES np, int len, IntPtr p, IntPtr l);
19
20
  [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
20
21
  static extern bool CreateProcessWithTokenW(IntPtr h, uint logonFlags, string app, string cmd, uint flags, IntPtr env, string dir, ref STARTUPINFO si, out PROCESS_INFORMATION pi);
22
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
23
+ static extern bool CreateProcessAsUserW(IntPtr h, string app, string cmd, IntPtr pa, IntPtr ta, bool inh, uint flags, IntPtr env, string dir, ref STARTUPINFO si, out PROCESS_INFORMATION pi);
24
+ [DllImport("advapi32.dll", SetLastError = true)] static extern bool SetTokenInformation(IntPtr h, int cls, ref uint val, int len);
21
25
  [DllImport("userenv.dll", SetLastError = true)] static extern bool CreateEnvironmentBlock(out IntPtr env, IntPtr hToken, bool inherit);
22
26
  [DllImport("userenv.dll")] static extern bool DestroyEnvironmentBlock(IntPtr env);
23
27
 
@@ -31,6 +35,14 @@ class DesktopElevate {
31
35
  const int TokenPrimary = 1;
32
36
  const uint SE_PRIVILEGE_ENABLED = 0x00000002;
33
37
  const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
38
+ const int TokenSessionId = 12;
39
+
40
+ // Launcher failures are otherwise silent (every error path just returns), and
41
+ // the only symptom downstream is "no worker". Log beside the exe like the worker.
42
+ static string LOG = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "launcher.log");
43
+ static void L(string m) {
44
+ try { File.AppendAllText(LOG, "[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + m + "\n"); } catch {}
45
+ }
34
46
 
35
47
  [StructLayout(LayoutKind.Sequential)]
36
48
  struct LUID { public uint LowPart; public int HighPart; }
@@ -57,20 +69,50 @@ class DesktopElevate {
57
69
  AdjustTokenPrivileges(h, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
58
70
  }
59
71
 
72
+ // Highest desktop-bridge-<n>.exe in dir, or null. Numeric compare — a string
73
+ // sort would rank "9" above "10" and pin the worker to an old build forever.
74
+ static string PickNewestWorker(string dir) {
75
+ string best = null;
76
+ int bestV = -1;
77
+ try {
78
+ foreach (var f in Directory.GetFiles(dir, "desktop-bridge-*.exe")) {
79
+ var name = Path.GetFileNameWithoutExtension(f);
80
+ int v;
81
+ if (!int.TryParse(name.Substring("desktop-bridge-".Length), out v)) continue;
82
+ if (v > bestV) { bestV = v; best = f; }
83
+ }
84
+ } catch { return null; }
85
+ return best;
86
+ }
87
+
60
88
  static void Main() {
61
- uint sid = WTSGetActiveConsoleSessionId();
89
+ uint sid = 0xFFFFFFFF;
90
+ L("start pid=" + Process.GetCurrentProcess().Id + " session=" + Process.GetCurrentProcess().SessionId);
62
91
 
63
92
  IntPtr hSelf;
64
93
  if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hSelf)) {
65
94
  EnablePriv(hSelf, "SeImpersonatePrivilege");
66
95
  EnablePriv(hSelf, "SeAssignPrimaryTokenPrivilege");
67
96
  EnablePriv(hSelf, "SeIncreaseQuotaPrivilege");
97
+ // Required to rewrite a token's session id (see the SetTokenInformation below).
98
+ EnablePriv(hSelf, "SeTcbPrivilege");
68
99
  CloseHandle(hSelf);
69
100
  }
70
101
 
102
+ // At boot this launcher runs before the console session finishes coming up:
103
+ // WTSGetActiveConsoleSessionId can return 0xFFFFFFFF (no session attached)
104
+ // and winlogon.exe for that session may not exist yet. Poll for up to 2min
105
+ // instead of exiting — a one-shot check would leave the worker dead until
106
+ // the next reboot, which is the very failure this task exists to prevent.
71
107
  int wpid = -1;
72
- foreach (var p in Process.GetProcesses()) {
73
- if (p.ProcessName.Equals("winlogon", StringComparison.OrdinalIgnoreCase) && p.SessionId == sid) { wpid = p.Id; break; }
108
+ for (int attempt = 0; attempt < 120 && wpid < 0; attempt++) {
109
+ sid = WTSGetActiveConsoleSessionId();
110
+ if (sid != 0xFFFFFFFF) {
111
+ foreach (var p in Process.GetProcesses()) {
112
+ if (p.ProcessName.Equals("winlogon", StringComparison.OrdinalIgnoreCase) && p.SessionId == sid) { wpid = p.Id; break; }
113
+ }
114
+ }
115
+ if (wpid < 0) Thread.Sleep(1000);
74
116
  }
75
117
  if (wpid < 0) return;
76
118
 
@@ -86,19 +128,36 @@ class DesktopElevate {
86
128
  IntPtr env;
87
129
  if (!CreateEnvironmentBlock(out env, dup, false)) { CloseHandle(dup); CloseHandle(ht); CloseHandle(hp); return; }
88
130
 
89
- // Worker lives beside this launcher exe.
90
- string app = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "desktop-bridge.exe");
91
- if (!File.Exists(app)) { DestroyEnvironmentBlock(env); CloseHandle(dup); CloseHandle(ht); CloseHandle(hp); return; }
131
+ // Worker lives beside this launcher exe, named desktop-bridge-<version>.exe.
132
+ // Version-stamped so the agent can build a new one without overwriting (and
133
+ // locking) the copy currently running this boot then picks up the newest.
134
+ string app = PickNewestWorker(AppDomain.CurrentDomain.BaseDirectory);
135
+ if (app == null) { DestroyEnvironmentBlock(env); CloseHandle(dup); CloseHandle(ht); CloseHandle(hp); return; }
92
136
 
93
137
  var si = new STARTUPINFO { cb = Marshal.SizeOf(typeof(STARTUPINFO)) };
94
138
  si.lpDesktop = @"winsta0\default";
95
139
 
96
- // CreateProcessWithTokenW: the launcher itself runs elevated in the console
97
- // session (spawned via Start-Process -Verb RunAs from the agent), so the
98
- // worker inherits the console session and SendInput reaches the user's
99
- // Winlogon desktop. Same pattern as .docs/login/launcher.cs.
140
+ // Pin the token to the console session. The duplicated winlogon token already
141
+ // carries it, but this is the one thing that must not be wrong — a worker in
142
+ // the wrong session still starts, still answers the pipe, and SendInput still
143
+ // reports success, while every keystroke lands on session 0's Winlogon desktop
144
+ // instead of the screen the user is looking at. Needs SeTcbPrivilege.
145
+ if (!SetTokenInformation(dup, TokenSessionId, ref sid, sizeof(uint)))
146
+ L("SetTokenInformation(session=" + sid + ") failed winerr=" + Marshal.GetLastWin32Error());
147
+
148
+ // CreateProcessAsUserW, not CreateProcessWithTokenW: the latter routes through
149
+ // the Secondary Logon service, which places the child in the CALLER's session
150
+ // and ignores the token's. That was invisible while the launcher was started by
151
+ // UAC from the console session — under the AtStartup task the launcher runs in
152
+ // session 0, and the worker went with it. CreateProcessAsUserW honours the
153
+ // token's session; SYSTEM already holds the required SeAssignPrimaryTokenPrivilege.
100
154
  PROCESS_INFORMATION pi;
101
- bool ok = CreateProcessWithTokenW(dup, 0, app, "\"" + app + "\"", CREATE_UNICODE_ENVIRONMENT, env, AppDomain.CurrentDomain.BaseDirectory, ref si, out pi);
155
+ bool ok = CreateProcessAsUserW(dup, app, "\"" + app + "\"", IntPtr.Zero, IntPtr.Zero, false, CREATE_UNICODE_ENVIRONMENT, env, AppDomain.CurrentDomain.BaseDirectory, ref si, out pi);
156
+ if (!ok) {
157
+ L("CreateProcessAsUserW failed winerr=" + Marshal.GetLastWin32Error() + " — falling back to CreateProcessWithTokenW");
158
+ ok = CreateProcessWithTokenW(dup, 0, app, "\"" + app + "\"", CREATE_UNICODE_ENVIRONMENT, env, AppDomain.CurrentDomain.BaseDirectory, ref si, out pi);
159
+ }
160
+ L((ok ? "spawned " : "spawn FAILED ") + app + " session=" + sid + (ok ? " pid=" + pi.dwProcessId : " winerr=" + Marshal.GetLastWin32Error()));
102
161
 
103
162
  DestroyEnvironmentBlock(env);
104
163
  if (ok) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }