9remote 2.1.21 → 2.2.4
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.
- package/dist/assets/tray.ps1 +8 -0
- package/dist/bin/desktop-bridge.cs +212 -0
- package/dist/bin/desktop-elevate.cs +166 -0
- package/dist/cli.cjs +91 -90
- package/dist/ptyDaemon.cjs +6 -7
- package/dist/server.cjs +168 -97
- package/dist/ui/assets/{index-BRA8JHNZ.js → index-DviNN66Q.js} +2958 -3882
- package/dist/ui/assets/{index-Cq9ojg5L.css → index-ulRsGAYR.css} +8 -5
- package/dist/ui/index.html +2 -2
- package/package.json +5 -3
package/dist/assets/tray.ps1
CHANGED
|
@@ -70,6 +70,14 @@ $script:menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
|
70
70
|
$script:notifyIcon.ContextMenuStrip = $script:menu
|
|
71
71
|
$script:items = @()
|
|
72
72
|
|
|
73
|
+
# Left-click also opens the menu (right-click is the Windows default)
|
|
74
|
+
$script:notifyIcon.Add_MouseClick({
|
|
75
|
+
param($sender, $e)
|
|
76
|
+
if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
|
|
77
|
+
$script:menu.Show([System.Windows.Forms.Cursor]::Position, [System.Windows.Forms.ToolStripDropDownDirection]::BelowRight)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
73
81
|
function Write-Event($obj) {
|
|
74
82
|
$json = $obj | ConvertTo-Json -Compress
|
|
75
83
|
[Console]::Out.WriteLine($json)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Desktop bridge daemon (runs as Local System). Named pipe server "9remote-desktop".
|
|
2
|
+
// Protocol (line-based ASCII):
|
|
3
|
+
// STATE -> "DESKTOP <name>" (name == "Winlogon" when on the login screen)
|
|
4
|
+
// TYPE <text> -> clears the field, types text via SendInput + VkKeyScanW, Enter; replies "OK"
|
|
5
|
+
// csc -nologo -target:winexe -out:desktop-bridge.exe desktop-bridge.cs
|
|
6
|
+
using System;
|
|
7
|
+
using System.Diagnostics;
|
|
8
|
+
using System.IO;
|
|
9
|
+
using System.IO.Pipes;
|
|
10
|
+
using System.Runtime.InteropServices;
|
|
11
|
+
using System.Security.AccessControl;
|
|
12
|
+
using System.Security.Principal;
|
|
13
|
+
using System.Threading;
|
|
14
|
+
|
|
15
|
+
class DesktopBridge {
|
|
16
|
+
[DllImport("user32.dll", SetLastError = true)] static extern uint SendInput(uint n, INPUT[] p, int cb);
|
|
17
|
+
[DllImport("user32.dll", SetLastError = true)] static extern IntPtr OpenInputDesktop(uint f, bool inh, uint da);
|
|
18
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool SetThreadDesktop(IntPtr h);
|
|
19
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool CloseDesktop(IntPtr h);
|
|
20
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool GetUserObjectInformationW(IntPtr o, int i, IntPtr p, uint len, out uint need);
|
|
21
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern short VkKeyScanW(char c);
|
|
22
|
+
|
|
23
|
+
// GENERIC_ALL required — GENERIC_READ silently drops keystrokes into Winlogon desktop.
|
|
24
|
+
const uint GENERIC_ALL = 0x10000000;
|
|
25
|
+
const uint INPUT_KEYBOARD = 1;
|
|
26
|
+
const uint KEYEVENTF_KEYUP = 0x0002;
|
|
27
|
+
const int UOI_NAME = 2;
|
|
28
|
+
const ushort VK_BACK = 0x08;
|
|
29
|
+
const ushort VK_RETURN = 0x0D;
|
|
30
|
+
const ushort VK_SHIFT = 0x10;
|
|
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;
|
|
45
|
+
|
|
46
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
47
|
+
struct MOUSEINPUT { public int dx, dy; public uint mouseData, dwFlags, time; public IntPtr dwExtraInfo; }
|
|
48
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
49
|
+
struct KEYBDINPUT { public ushort wVk, wScan; public uint dwFlags, time; public IntPtr dwExtraInfo; }
|
|
50
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
51
|
+
struct HARDWAREINPUT { public uint uMsg; public ushort wParamL, wParamH; }
|
|
52
|
+
[StructLayout(LayoutKind.Explicit)]
|
|
53
|
+
struct MKH {
|
|
54
|
+
[FieldOffset(0)] public MOUSEINPUT mi;
|
|
55
|
+
[FieldOffset(0)] public KEYBDINPUT ki;
|
|
56
|
+
[FieldOffset(0)] public HARDWAREINPUT hi;
|
|
57
|
+
}
|
|
58
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
59
|
+
struct INPUT { public uint type; public MKH u; }
|
|
60
|
+
|
|
61
|
+
static string NameOf(IntPtr h) {
|
|
62
|
+
uint need;
|
|
63
|
+
GetUserObjectInformationW(h, UOI_NAME, IntPtr.Zero, 0, out need);
|
|
64
|
+
if (need == 0) return "unknown";
|
|
65
|
+
IntPtr buf = Marshal.AllocHGlobal((int)need);
|
|
66
|
+
try {
|
|
67
|
+
GetUserObjectInformationW(h, UOI_NAME, buf, need, out need);
|
|
68
|
+
return Marshal.PtrToStringUni(buf);
|
|
69
|
+
} finally { Marshal.FreeHGlobal(buf); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static string ActiveDesktop() {
|
|
73
|
+
IntPtr h = OpenInputDesktop(0, false, GENERIC_ALL);
|
|
74
|
+
if (h == IntPtr.Zero) return "none";
|
|
75
|
+
string n = NameOf(h);
|
|
76
|
+
CloseDesktop(h);
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static void AttachActive() {
|
|
81
|
+
IntPtr h = OpenInputDesktop(0, false, GENERIC_ALL);
|
|
82
|
+
if (h != IntPtr.Zero) { SetThreadDesktop(h); CloseDesktop(h); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Log to worker.log beside the exe — tailed by the agent so SendInput failures
|
|
86
|
+
// (wrong session, wrong desktop, UIPI block) surface without a console.
|
|
87
|
+
// NOTE: csc v4.0.30319 is C# 5.0 — no string interpolation ($""), no expression-bodied members.
|
|
88
|
+
static string LOG = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "worker.log");
|
|
89
|
+
static void L(string m) {
|
|
90
|
+
try { File.AppendAllText(LOG, "[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + m + "\n"); } catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Atomic down+up in ONE SendInput call — matches the .docs/login pattern that
|
|
94
|
+
// was verified working. The Winlogon credential UI can drop keystrokes when
|
|
95
|
+
// key-down and key-up are sent in separate SendInput calls.
|
|
96
|
+
static void SendVK(ushort vk) {
|
|
97
|
+
INPUT[] inp = new INPUT[2];
|
|
98
|
+
inp[0].type = INPUT_KEYBOARD; inp[0].u.ki.wVk = vk;
|
|
99
|
+
inp[1].type = INPUT_KEYBOARD; inp[1].u.ki.wVk = vk; inp[1].u.ki.dwFlags = KEYEVENTF_KEYUP;
|
|
100
|
+
uint r = SendInput((uint)inp.Length, inp, Marshal.SizeOf(typeof(INPUT)));
|
|
101
|
+
if (r == 0) L("SendInput FAIL vk=0x" + vk.ToString("X2") + " winerr=" + Marshal.GetLastWin32Error());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Single event (down or up) for modifier-hold sequences (shifted chars).
|
|
105
|
+
static void SendKey(ushort vk, bool up) {
|
|
106
|
+
INPUT inp = new INPUT { type = INPUT_KEYBOARD };
|
|
107
|
+
inp.u.ki.wVk = vk;
|
|
108
|
+
if (up) inp.u.ki.dwFlags = KEYEVENTF_KEYUP;
|
|
109
|
+
INPUT[] arr = new INPUT[] { inp };
|
|
110
|
+
uint r = SendInput(1, arr, Marshal.SizeOf(typeof(INPUT)));
|
|
111
|
+
if (r == 0) L("SendKey FAIL vk=0x" + vk.ToString("X2") + " up=" + up + " winerr=" + Marshal.GetLastWin32Error());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// VkKeyScanW high byte: bit0 = Shift, bit1 = Ctrl, bit2 = Alt. Only Shift handled here.
|
|
115
|
+
static void TypeText(string text) {
|
|
116
|
+
int sess = Process.GetCurrentProcess().SessionId;
|
|
117
|
+
string beforeDesk = ActiveDesktop();
|
|
118
|
+
L("TYPE session=" + sess + " desktop=" + beforeDesk + " len=" + text.Length);
|
|
119
|
+
AttachActive();
|
|
120
|
+
string afterDesk = ActiveDesktop();
|
|
121
|
+
if (afterDesk != beforeDesk) L(" attach flipped desktop " + beforeDesk + " -> " + afterDesk);
|
|
122
|
+
for (int i = 0; i < 30; i++) { SendVK(VK_BACK); Thread.Sleep(20); }
|
|
123
|
+
Thread.Sleep(80);
|
|
124
|
+
foreach (char c in text) {
|
|
125
|
+
short k = VkKeyScanW(c);
|
|
126
|
+
ushort vk = (ushort)(k & 0xFF);
|
|
127
|
+
bool shift = (k & 0x0100) != 0;
|
|
128
|
+
// Never log the char or its vk — TYPE carries the user's password.
|
|
129
|
+
if (shift) {
|
|
130
|
+
SendKey(VK_SHIFT, false);
|
|
131
|
+
SendKey(vk, false); Thread.Sleep(25); SendKey(vk, true);
|
|
132
|
+
SendKey(VK_SHIFT, true);
|
|
133
|
+
} else {
|
|
134
|
+
SendVK(vk); // atomic — same path as .docs/login for digits
|
|
135
|
+
}
|
|
136
|
+
Thread.Sleep(30);
|
|
137
|
+
}
|
|
138
|
+
Thread.Sleep(80);
|
|
139
|
+
SendVK(VK_RETURN);
|
|
140
|
+
L("TYPE done");
|
|
141
|
+
}
|
|
142
|
+
|
|
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); }
|
|
166
|
+
try { L("start session=" + Process.GetCurrentProcess().SessionId + " pid=" + Process.GetCurrentProcess().Id + " user=" + WindowsIdentity.GetCurrent().Name); } catch {}
|
|
167
|
+
// ACL: SYSTEM + Administrators + Authenticated Users so the user-scope agent can connect.
|
|
168
|
+
var sec = new PipeSecurity();
|
|
169
|
+
sec.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
|
170
|
+
sec.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
|
171
|
+
sec.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
|
172
|
+
|
|
173
|
+
while (true) {
|
|
174
|
+
NamedPipeServerStream srv;
|
|
175
|
+
try {
|
|
176
|
+
srv = new NamedPipeServerStream(PIPE, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 0, sec);
|
|
177
|
+
} catch {
|
|
178
|
+
Thread.Sleep(1000); continue;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
srv.WaitForConnection();
|
|
182
|
+
var sr = new StreamReader(srv);
|
|
183
|
+
var sw = new StreamWriter(srv) { AutoFlush = true };
|
|
184
|
+
string line;
|
|
185
|
+
while ((line = sr.ReadLine()) != null) {
|
|
186
|
+
line = line.Trim();
|
|
187
|
+
if (line.Length == 0) continue;
|
|
188
|
+
if (line == "VERSION") {
|
|
189
|
+
sw.WriteLine("VERSION " + VERSION);
|
|
190
|
+
} else if (line == "STATE") {
|
|
191
|
+
sw.WriteLine("DESKTOP " + ActiveDesktop());
|
|
192
|
+
} else if (line.StartsWith("TYPE ")) {
|
|
193
|
+
TypeText(line.Substring(5));
|
|
194
|
+
sw.WriteLine("OK");
|
|
195
|
+
} else if (line == "STOP") {
|
|
196
|
+
sw.WriteLine("OK");
|
|
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.
|
|
200
|
+
try { srv.Dispose(); } catch { }
|
|
201
|
+
Environment.Exit(0);
|
|
202
|
+
} else {
|
|
203
|
+
sw.WriteLine("ERR unknown");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
} finally {
|
|
208
|
+
try { srv.Dispose(); } catch { }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Desktop elevate launcher (run as admin via UAC). Spawns desktop-bridge.exe as
|
|
2
|
+
// Local System inside the console session so it can SendInput into the Winlogon
|
|
3
|
+
// (login) desktop. Worker path defaults to the launcher's own directory.
|
|
4
|
+
// csc -nologo -target:winexe -out:desktop-elevate.exe desktop-elevate.cs
|
|
5
|
+
using System;
|
|
6
|
+
using System.Diagnostics;
|
|
7
|
+
using System.IO;
|
|
8
|
+
using System.Runtime.InteropServices;
|
|
9
|
+
using System.Threading;
|
|
10
|
+
|
|
11
|
+
class DesktopElevate {
|
|
12
|
+
[DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId();
|
|
13
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
|
14
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr OpenProcess(uint da, bool inh, int pid);
|
|
15
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
|
|
16
|
+
[DllImport("advapi32.dll", SetLastError = true)] static extern bool OpenProcessToken(IntPtr h, uint da, out IntPtr t);
|
|
17
|
+
[DllImport("advapi32.dll", SetLastError = true)] static extern bool DuplicateTokenEx(IntPtr ex, uint da, IntPtr sa, int imp, int t, out IntPtr dup);
|
|
18
|
+
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern bool LookupPrivilegeValueW(string sys, string name, out LUID luid);
|
|
19
|
+
[DllImport("advapi32.dll", SetLastError = true)] static extern bool AdjustTokenPrivileges(IntPtr h, bool dis, ref TOKEN_PRIVILEGES np, int len, IntPtr p, IntPtr l);
|
|
20
|
+
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
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);
|
|
25
|
+
[DllImport("userenv.dll", SetLastError = true)] static extern bool CreateEnvironmentBlock(out IntPtr env, IntPtr hToken, bool inherit);
|
|
26
|
+
[DllImport("userenv.dll")] static extern bool DestroyEnvironmentBlock(IntPtr env);
|
|
27
|
+
|
|
28
|
+
const uint TOKEN_QUERY = 0x0008;
|
|
29
|
+
const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
|
|
30
|
+
const uint TOKEN_DUPLICATE = 0x0002;
|
|
31
|
+
const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
|
|
32
|
+
const uint TOKEN_ALL_ACCESS = 0xF01FF;
|
|
33
|
+
const uint PROCESS_QUERY_INFORMATION = 0x0400;
|
|
34
|
+
const int SecurityImpersonation = 2;
|
|
35
|
+
const int TokenPrimary = 1;
|
|
36
|
+
const uint SE_PRIVILEGE_ENABLED = 0x00000002;
|
|
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
|
+
}
|
|
46
|
+
|
|
47
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
48
|
+
struct LUID { public uint LowPart; public int HighPart; }
|
|
49
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
50
|
+
struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; }
|
|
51
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
52
|
+
struct TOKEN_PRIVILEGES { public uint PrivilegeCount; public LUID_AND_ATTRIBUTES Privileges; }
|
|
53
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
54
|
+
struct STARTUPINFO {
|
|
55
|
+
public int cb; public string lpReserved; public string lpDesktop; public string lpTitle;
|
|
56
|
+
public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags;
|
|
57
|
+
public short wShowWindow, cbReserved2; public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
|
|
58
|
+
}
|
|
59
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
60
|
+
struct PROCESS_INFORMATION { public IntPtr hProcess, hThread; public int dwProcessId, dwThreadId; }
|
|
61
|
+
|
|
62
|
+
static void EnablePriv(IntPtr h, string n) {
|
|
63
|
+
LUID l;
|
|
64
|
+
if (!LookupPrivilegeValueW(null, n, out l)) return;
|
|
65
|
+
TOKEN_PRIVILEGES tp;
|
|
66
|
+
tp.PrivilegeCount = 1;
|
|
67
|
+
tp.Privileges.Luid = l;
|
|
68
|
+
tp.Privileges.Attributes = SE_PRIVILEGE_ENABLED;
|
|
69
|
+
AdjustTokenPrivileges(h, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
|
|
70
|
+
}
|
|
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
|
+
|
|
88
|
+
static void Main() {
|
|
89
|
+
uint sid = 0xFFFFFFFF;
|
|
90
|
+
L("start pid=" + Process.GetCurrentProcess().Id + " session=" + Process.GetCurrentProcess().SessionId);
|
|
91
|
+
|
|
92
|
+
IntPtr hSelf;
|
|
93
|
+
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hSelf)) {
|
|
94
|
+
EnablePriv(hSelf, "SeImpersonatePrivilege");
|
|
95
|
+
EnablePriv(hSelf, "SeAssignPrimaryTokenPrivilege");
|
|
96
|
+
EnablePriv(hSelf, "SeIncreaseQuotaPrivilege");
|
|
97
|
+
// Required to rewrite a token's session id (see the SetTokenInformation below).
|
|
98
|
+
EnablePriv(hSelf, "SeTcbPrivilege");
|
|
99
|
+
CloseHandle(hSelf);
|
|
100
|
+
}
|
|
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.
|
|
107
|
+
int wpid = -1;
|
|
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);
|
|
116
|
+
}
|
|
117
|
+
if (wpid < 0) return;
|
|
118
|
+
|
|
119
|
+
IntPtr hp = OpenProcess(PROCESS_QUERY_INFORMATION, false, wpid);
|
|
120
|
+
if (hp == IntPtr.Zero) return;
|
|
121
|
+
|
|
122
|
+
IntPtr ht;
|
|
123
|
+
if (!OpenProcessToken(hp, TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY, out ht)) { CloseHandle(hp); return; }
|
|
124
|
+
|
|
125
|
+
IntPtr dup;
|
|
126
|
+
if (!DuplicateTokenEx(ht, TOKEN_ALL_ACCESS, IntPtr.Zero, SecurityImpersonation, TokenPrimary, out dup)) { CloseHandle(ht); CloseHandle(hp); return; }
|
|
127
|
+
|
|
128
|
+
IntPtr env;
|
|
129
|
+
if (!CreateEnvironmentBlock(out env, dup, false)) { CloseHandle(dup); CloseHandle(ht); CloseHandle(hp); return; }
|
|
130
|
+
|
|
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; }
|
|
136
|
+
|
|
137
|
+
var si = new STARTUPINFO { cb = Marshal.SizeOf(typeof(STARTUPINFO)) };
|
|
138
|
+
si.lpDesktop = @"winsta0\default";
|
|
139
|
+
|
|
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.
|
|
154
|
+
PROCESS_INFORMATION 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()));
|
|
161
|
+
|
|
162
|
+
DestroyEnvironmentBlock(env);
|
|
163
|
+
if (ok) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
|
|
164
|
+
CloseHandle(dup); CloseHandle(ht); CloseHandle(hp);
|
|
165
|
+
}
|
|
166
|
+
}
|