@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.7
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/CHANGELOG.en.md +44 -1
- package/CHANGELOG.md +44 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +57 -0
- package/lib/client.js +2678 -276
- package/lib/client.js.map +1 -1
- package/lib/command-CESuwlVt.js +1306 -0
- package/lib/command.js +2 -2
- package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
- package/lib/fs.js +2 -1
- package/lib/index.js +10 -8
- package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +7 -2
- package/lib/method-wrapper-CFY3p3j2.js +26 -0
- package/lib/process-fence.js +280 -0
- package/lib/providers.js +87 -0
- package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
- package/lib/registry.js +2 -2
- package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
- package/lib/sandbox-Kup4_ZIb.js +747 -0
- package/lib/sandbox.js +2 -1
- package/lib/scope.js +229 -20
- package/lib/types/client/RootSwitcher.d.ts +63 -0
- package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
- package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
- package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
- package/lib/types/client/files-tree/adapter.d.ts +94 -0
- package/lib/types/client/files-tree/directory-node.d.ts +62 -0
- package/lib/types/client/files-tree/face.d.ts +107 -0
- package/lib/types/client/files-tree/locales.d.ts +10 -0
- package/lib/types/client/files-tree/primitives.d.ts +33 -0
- package/lib/types/client/files-tree/store.d.ts +81 -0
- package/lib/types/client/locales.d.ts +56 -10
- package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
- package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
- package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
- package/lib/types/client/open-in-app/adapter.d.ts +46 -0
- package/lib/types/client/open-in-app/applications.d.ts +4 -0
- package/lib/types/client/open-in-app/controller.d.ts +53 -0
- package/lib/types/client/open-in-app/locales.d.ts +10 -0
- package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
- package/lib/types/client/open-in-app/open-path.d.ts +13 -0
- package/lib/types/client/panel-client.d.ts +2 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +16 -3
- package/lib/types/compat/client-icons.d.ts +18 -0
- package/lib/types/compat/dsh-version.d.ts +1 -1
- package/lib/types/compat/open-in-app-routes.d.ts +38 -0
- package/lib/types/compat/process-entrypoints.d.ts +31 -0
- package/lib/types/compat/process-result.d.ts +14 -0
- package/lib/types/compat/pty-marker.d.ts +83 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/compat/storage-zod.d.ts +65 -0
- package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
- package/lib/types/compat/windows-native.d.ts +68 -0
- package/lib/types/compat/windows-path-guard.d.ts +56 -0
- package/lib/types/compat/workspace-files.d.ts +2 -0
- package/lib/types/contract.d.ts +31 -19
- package/lib/types/dialects.d.ts +99 -3
- package/lib/types/fs.d.ts +32 -10
- package/lib/types/index.d.ts +7 -2
- package/lib/types/process-fence.d.ts +54 -0
- package/lib/types/profile-isolation.d.ts +178 -0
- package/lib/types/providers.d.ts +70 -0
- package/lib/types/registry.d.ts +132 -31
- package/lib/types/roots.d.ts +100 -13
- package/lib/types/sandbox.d.ts +59 -3
- package/lib/types/scope.d.ts +200 -14
- package/lib/types/windows-broker.d.ts +17 -0
- package/lib/types/windows-deny.d.ts +1 -0
- package/lib/types/windows-profile.d.ts +10 -13
- package/lib/types/windows-runner-contract.d.ts +97 -0
- package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
- package/lib/windows-runner.js +1256 -13
- package/lib/workspace-files.js +138 -3
- package/lib/workspace-routing-YPxaQ4V3.js +26 -0
- package/package.json +84 -62
- package/scripts/windows-broker/install.ps1 +89 -0
- package/scripts/windows-broker/rights.cs +58 -0
- package/scripts/windows-broker/service.cs +323 -0
- package/scripts/windows-broker/verify-install.ps1 +80 -0
- package/lib/command-CTdNvwt1.js +0 -791
- package/lib/method-wrapper-Zc-JlDP3.js +0 -51
- package/lib/sandbox-BRXl_FpF.js +0 -132
- package/lib/types/client/RootFileTree.d.ts +0 -12
- package/lib/types/panel-files.d.ts +0 -3
- package/lib/windows-profile-CiFsFYIV.js +0 -216
- package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// A narrow privileged service: mirror the authenticated process identity and add
|
|
2
|
+
// only deny-only normal groups. It never opens files or launches commands.
|
|
3
|
+
using System;
|
|
4
|
+
using System.ComponentModel;
|
|
5
|
+
using System.IO;
|
|
6
|
+
using System.IO.Pipes;
|
|
7
|
+
using System.Runtime.InteropServices;
|
|
8
|
+
using System.Security.AccessControl;
|
|
9
|
+
using System.Security.Principal;
|
|
10
|
+
using System.ServiceProcess;
|
|
11
|
+
using System.Text;
|
|
12
|
+
using System.Text.RegularExpressions;
|
|
13
|
+
using System.Threading;
|
|
14
|
+
using System.Threading.Tasks;
|
|
15
|
+
|
|
16
|
+
public sealed class RootDenyBroker : ServiceBase {
|
|
17
|
+
// Same bounded IPC contract as MAX_WINDOWS_DENY_ROOTS in the client.
|
|
18
|
+
const int MaxDenyRoots = 256;
|
|
19
|
+
const int MaxRequestBytes = 32768;
|
|
20
|
+
readonly string sid, pipeName;
|
|
21
|
+
volatile bool stopping;
|
|
22
|
+
NamedPipeServerStream listener;
|
|
23
|
+
readonly SemaphoreSlim slots = new SemaphoreSlim(8);
|
|
24
|
+
RootDenyBroker(string sid, string name) {
|
|
25
|
+
this.sid = new SecurityIdentifier(sid).Value;
|
|
26
|
+
pipeName = name;
|
|
27
|
+
ServiceName = name;
|
|
28
|
+
CanStop = true;
|
|
29
|
+
}
|
|
30
|
+
public static void Main(string[] args) {
|
|
31
|
+
if (args.Length != 2 || !Regex.IsMatch(args[1], @"^DshMultiRootBroker-[a-f0-9]{16}$"))
|
|
32
|
+
throw new ArgumentException("Expected installation SID and pipe name");
|
|
33
|
+
ServiceBase.Run(new RootDenyBroker(args[0], args[1]));
|
|
34
|
+
}
|
|
35
|
+
protected override void OnStart(string[] args) {
|
|
36
|
+
EnablePrivilege("SeTcbPrivilege");
|
|
37
|
+
EnablePrivilege("SeDebugPrivilege");
|
|
38
|
+
EnablePrivilege("SeImpersonatePrivilege");
|
|
39
|
+
Task.Run((Action)Serve);
|
|
40
|
+
}
|
|
41
|
+
protected override void OnStop() {
|
|
42
|
+
stopping = true;
|
|
43
|
+
if (listener != null) listener.Dispose();
|
|
44
|
+
}
|
|
45
|
+
void Serve() {
|
|
46
|
+
var security = new PipeSecurity();
|
|
47
|
+
security.SetAccessRuleProtection(true, false);
|
|
48
|
+
security.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User, PipeAccessRights.FullControl, AccessControlType.Allow));
|
|
49
|
+
security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier("S-1-5-18"), PipeAccessRights.FullControl, AccessControlType.Allow));
|
|
50
|
+
// The runner reads the kernel attribute back; a spoofed ACK cannot authorize a launch.
|
|
51
|
+
security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(sid), PipeAccessRights.ReadWrite | PipeAccessRights.Synchronize, AccessControlType.Allow));
|
|
52
|
+
security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier("S-1-5-2"), PipeAccessRights.FullControl, AccessControlType.Deny));
|
|
53
|
+
while (!stopping) {
|
|
54
|
+
NamedPipeServerStream pipe = null;
|
|
55
|
+
try {
|
|
56
|
+
slots.Wait();
|
|
57
|
+
pipe = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 8, PipeTransmissionMode.Byte,
|
|
58
|
+
PipeOptions.Asynchronous, 4096, 4096, security);
|
|
59
|
+
listener = pipe;
|
|
60
|
+
pipe.WaitForConnection();
|
|
61
|
+
var connected = pipe;
|
|
62
|
+
pipe = null;
|
|
63
|
+
Task.Run(() => { try { Handle(connected); } finally { connected.Dispose(); slots.Release(); } });
|
|
64
|
+
} catch { if (pipe != null) pipe.Dispose(); slots.Release(); if (stopping) return; Thread.Sleep(100); }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
void Handle(NamedPipeServerStream pipe) {
|
|
68
|
+
try {
|
|
69
|
+
// A pipe impersonation context exists only after its first read.
|
|
70
|
+
// Reading a bounded request performs no privileged operation.
|
|
71
|
+
var read = Task.Run(() => ReadRequest(pipe));
|
|
72
|
+
if (!read.Wait(5000)) throw new System.TimeoutException("Request timeout");
|
|
73
|
+
// Authenticate the pipe token, not a SID or PID supplied in data.
|
|
74
|
+
string caller = null;
|
|
75
|
+
pipe.RunAsClient(() => { caller = WindowsIdentity.GetCurrent().User.Value; });
|
|
76
|
+
if (caller != sid) throw new UnauthorizedAccessException("Caller SID mismatch");
|
|
77
|
+
uint pid;
|
|
78
|
+
Check(GetNamedPipeClientProcessId(pipe.SafePipeHandle.DangerousGetHandle(), out pid), "GetNamedPipeClientProcessId");
|
|
79
|
+
string[] fields = read.Result.Split('|');
|
|
80
|
+
bool assigning = fields.Length == 4 && fields[0] == "3";
|
|
81
|
+
if ((!assigning && (fields.Length != 3 || fields[0] != "2")) || !Regex.IsMatch(fields[1], @"^[0-9]{1,20}$")
|
|
82
|
+
|| (assigning && !Regex.IsMatch(fields[2], @"^[0-9]{1,20}$")))
|
|
83
|
+
throw new ArgumentException("Invalid request");
|
|
84
|
+
string[] roots = fields[fields.Length - 1].Split(',');
|
|
85
|
+
if (roots.Length < 1 || roots.Length > MaxDenyRoots) throw new ArgumentException("Invalid root count");
|
|
86
|
+
foreach (var root in roots)
|
|
87
|
+
if (!Regex.IsMatch(root, @"^S-1-4-[0-9]{1,10}-[0-9]{1,10}-[0-9]{1,10}-[0-9]{1,10}-3$"))
|
|
88
|
+
throw new ArgumentException("Invalid deny identity");
|
|
89
|
+
IntPtr process = OpenProcess(0x1040, false, pid);
|
|
90
|
+
if (process == IntPtr.Zero) throw new Win32Exception();
|
|
91
|
+
IntPtr token = IntPtr.Zero, processToken = IntPtr.Zero, minted = IntPtr.Zero;
|
|
92
|
+
try {
|
|
93
|
+
// SeDebugPrivilege opens the process, not its token object. Acquire
|
|
94
|
+
// only QUERY/QUERY_SOURCE while impersonating the authenticated
|
|
95
|
+
// pipe caller, then revert before any privileged minting.
|
|
96
|
+
pipe.RunAsClient(() => { Check(OpenProcessToken(process, 0x18, out processToken), "OpenProcessToken(client)"); });
|
|
97
|
+
if (TokenSid(processToken) != sid)
|
|
98
|
+
throw new UnauthorizedAccessException("Caller process must be an unrestricted installation identity");
|
|
99
|
+
RefuseRestrictedSource(processToken);
|
|
100
|
+
if (assigning) {
|
|
101
|
+
AssignToChild(process, processToken, pid, fields[1], fields[2], roots, pipe);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
Check(DuplicateHandle(process, new IntPtr(unchecked((long)UInt64.Parse(fields[1]))), GetCurrentProcess(), out token,
|
|
105
|
+
8, false, 0), "DuplicateHandle(token)");
|
|
106
|
+
bool equal;
|
|
107
|
+
NtCheck(NtCompareTokens(processToken, token, out equal), "NtCompareTokens");
|
|
108
|
+
if (!equal) throw new UnauthorizedAccessException("Source is not the caller process token");
|
|
109
|
+
minted = MintDenyToken(processToken, roots);
|
|
110
|
+
IntPtr remote;
|
|
111
|
+
// Only rights needed to derive and protect the launch token; no
|
|
112
|
+
// group/privilege adjustment or owner changes.
|
|
113
|
+
Check(DuplicateHandle(GetCurrentProcess(), minted, process, out remote,
|
|
114
|
+
0x6008B, false, 0), "DuplicateHandle(reply)"); // WRITE_DAC | READ_CONTROL | QUERY | DUPLICATE | ASSIGN_PRIMARY | ADJUST_DEFAULT
|
|
115
|
+
try { Reply(pipe, "OK|" + unchecked((ulong)remote.ToInt64()) + "\n"); }
|
|
116
|
+
catch {
|
|
117
|
+
IntPtr cleanup;
|
|
118
|
+
if (DuplicateHandle(process, remote, GetCurrentProcess(), out cleanup, 0, false, 1)) CloseHandle(cleanup);
|
|
119
|
+
throw;
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
if (minted != IntPtr.Zero) CloseHandle(minted);
|
|
123
|
+
if (token != IntPtr.Zero) CloseHandle(token);
|
|
124
|
+
if (processToken != IntPtr.Zero) CloseHandle(processToken);
|
|
125
|
+
CloseHandle(process);
|
|
126
|
+
}
|
|
127
|
+
} catch (Exception e) { try { Reply(pipe, "ERROR:" + e.GetType().Name + ":" + e.Message.Replace('\n', ' ').Replace('\r', ' ') + "\n"); } catch {} }
|
|
128
|
+
}
|
|
129
|
+
void AssignToChild(IntPtr caller, IntPtr source, uint pid, string processField, string threadField, string[] roots, NamedPipeServerStream pipe) {
|
|
130
|
+
IntPtr child = IntPtr.Zero, thread = IntPtr.Zero, original = IntPtr.Zero, token = IntPtr.Zero;
|
|
131
|
+
try {
|
|
132
|
+
Check(DuplicateHandle(caller, new IntPtr(unchecked((long)UInt64.Parse(processField))), GetCurrentProcess(), out child,
|
|
133
|
+
0x600, false, 0), "DuplicateHandle(child)"); // SET_INFORMATION | QUERY_INFORMATION
|
|
134
|
+
Check(DuplicateHandle(caller, new IntPtr(unchecked((long)UInt64.Parse(threadField))), GetCurrentProcess(), out thread,
|
|
135
|
+
0x2C0, false, 0), "DuplicateHandle(primary thread)"); // QUERY | SET_TOKEN | DIRECT_IMPERSONATION
|
|
136
|
+
ProcessBasicInformation info; int length;
|
|
137
|
+
NtCheck(NtQueryInformationProcess(child, 0, out info, Marshal.SizeOf(typeof(ProcessBasicInformation)), out length), "NtQueryInformationProcess(child)");
|
|
138
|
+
if (info.ParentId.ToUInt64() != pid || info.ProcessId.ToUInt64() == pid
|
|
139
|
+
|| GetProcessIdOfThread(thread) != info.ProcessId.ToUInt64())
|
|
140
|
+
throw new UnauthorizedAccessException("Not a direct caller-owned child thread");
|
|
141
|
+
pipe.RunAsClient(() => { Check(OpenProcessToken(child, 8, out original), "OpenProcessToken(child)"); });
|
|
142
|
+
bool equal; NtCheck(NtCompareTokens(source, original, out equal), "NtCompareTokens(child)");
|
|
143
|
+
if (!equal) throw new UnauthorizedAccessException("Child identity does not match its caller");
|
|
144
|
+
token = MintDenyToken(source, roots);
|
|
145
|
+
ProtectToken(token, TokenSid(source));
|
|
146
|
+
EnablePrivilege("SeAssignPrimaryTokenPrivilege");
|
|
147
|
+
var access = new ProcessAccessToken { Token = token, Thread = thread };
|
|
148
|
+
// The kernel refuses assignment after the initial thread starts.
|
|
149
|
+
// No argv, executable, environment or path enters this operation.
|
|
150
|
+
NtCheck(NtSetInformationProcess(child, 9, ref access, Marshal.SizeOf(typeof(ProcessAccessToken))), "NtSetInformationProcess(access token)");
|
|
151
|
+
Reply(pipe, "OK\n");
|
|
152
|
+
} finally {
|
|
153
|
+
foreach (var h in new[] { token, original, thread, child }) if (h != IntPtr.Zero) CloseHandle(h);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
static void ProtectToken(IntPtr token, string user) {
|
|
157
|
+
var descriptor = new RawSecurityDescriptor("D:(D;;0xc00e0;;;" + user + ")(A;;GA;;;" + user + ")(A;;RC;;;OW)");
|
|
158
|
+
var bytes = new byte[descriptor.DiscretionaryAcl.BinaryLength]; descriptor.DiscretionaryAcl.GetBinaryForm(bytes, 0);
|
|
159
|
+
IntPtr acl = Marshal.AllocHGlobal(bytes.Length);
|
|
160
|
+
try {
|
|
161
|
+
Marshal.Copy(bytes, 0, acl, bytes.Length);
|
|
162
|
+
uint code = SetSecurityInfo(token, 6, 4, IntPtr.Zero, IntPtr.Zero, acl, IntPtr.Zero);
|
|
163
|
+
if (code != 0) throw new Win32Exception((int)code, "SetSecurityInfo(token)");
|
|
164
|
+
} finally { Marshal.FreeHGlobal(acl); }
|
|
165
|
+
}
|
|
166
|
+
static string ReadRequest(Stream stream) {
|
|
167
|
+
var data = new MemoryStream();
|
|
168
|
+
for (int n = 0; n < MaxRequestBytes; n++) {
|
|
169
|
+
int value = stream.ReadByte();
|
|
170
|
+
if (value < 0) throw new IOException("Disconnected");
|
|
171
|
+
if (value == 10) return Encoding.ASCII.GetString(data.ToArray());
|
|
172
|
+
if (value < 32 || value > 126) throw new ArgumentException("Invalid byte");
|
|
173
|
+
data.WriteByte((byte)value);
|
|
174
|
+
}
|
|
175
|
+
throw new ArgumentException("Request too large");
|
|
176
|
+
}
|
|
177
|
+
static void Reply(Stream stream, string value) { var bytes = Encoding.UTF8.GetBytes(value); stream.Write(bytes, 0, bytes.Length); stream.Flush(); }
|
|
178
|
+
static string TokenSid(IntPtr token) {
|
|
179
|
+
int length;
|
|
180
|
+
GetTokenInformation(token, 1, IntPtr.Zero, 0, out length);
|
|
181
|
+
if (length < IntPtr.Size || length > 65536) throw new ArgumentException("Invalid token");
|
|
182
|
+
IntPtr data = Marshal.AllocHGlobal(length);
|
|
183
|
+
try { Check(GetTokenInformation(token, 1, data, length, out length), "GetTokenInformation(user)"); return new SecurityIdentifier(Marshal.ReadIntPtr(data)).Value; }
|
|
184
|
+
finally { Marshal.FreeHGlobal(data); }
|
|
185
|
+
}
|
|
186
|
+
// UAC-filtered callers (TokenHasRestrictions) are accepted: minting only
|
|
187
|
+
// mirrors the caller's own groups, privileges and label, so no authority is
|
|
188
|
+
// gained. Real restricting SID lists and AppContainer stay refused.
|
|
189
|
+
static void RefuseRestrictedSource(IntPtr token) {
|
|
190
|
+
if (IsTokenRestricted(token))
|
|
191
|
+
throw new UnauthorizedAccessException("Caller process token carries restricting SIDs");
|
|
192
|
+
var data = Query(token, 29);
|
|
193
|
+
try {
|
|
194
|
+
if (Marshal.ReadInt32(data) != 0)
|
|
195
|
+
throw new UnauthorizedAccessException("AppContainer caller unsupported");
|
|
196
|
+
} finally { Marshal.FreeHGlobal(data); }
|
|
197
|
+
}
|
|
198
|
+
static IntPtr Query(IntPtr token, int kind) {
|
|
199
|
+
int length;
|
|
200
|
+
GetTokenInformation(token, kind, IntPtr.Zero, 0, out length);
|
|
201
|
+
if (length < 0 || length > 1048576) throw new ArgumentException("Invalid token information size kind=" + kind + " length=" + length);
|
|
202
|
+
// Empty claim/group channels can report zero bytes on the sizing call.
|
|
203
|
+
// A real second query must still succeed; never infer absence from an error.
|
|
204
|
+
int capacity = Math.Max(length, 16);
|
|
205
|
+
var data = Marshal.AllocHGlobal(capacity);
|
|
206
|
+
Marshal.Copy(new byte[capacity], 0, data, capacity);
|
|
207
|
+
try { Check(GetTokenInformation(token, kind, data, capacity, out length), "GetTokenInformation(" + kind + ")"); return data; }
|
|
208
|
+
catch { Marshal.FreeHGlobal(data); throw; }
|
|
209
|
+
}
|
|
210
|
+
static void RejectInheritedAttributes(IntPtr source) {
|
|
211
|
+
int length;
|
|
212
|
+
NtQuerySecurityAttributesToken(source, IntPtr.Zero, 0, IntPtr.Zero, 0, out length);
|
|
213
|
+
if (length < 16 || length > 1048576) throw new ArgumentException("Invalid local attributes size");
|
|
214
|
+
var data = Marshal.AllocHGlobal(length);
|
|
215
|
+
try {
|
|
216
|
+
NtCheck(NtQuerySecurityAttributesToken(source, IntPtr.Zero, 0, data, length, out length), "NtQuerySecurityAttributesToken(source)");
|
|
217
|
+
uint count = unchecked((uint)Marshal.ReadInt32(data, 4));
|
|
218
|
+
if (Marshal.ReadInt16(data) != 1 || count > 128) throw new ArgumentException("Invalid local attributes");
|
|
219
|
+
var array = Marshal.ReadIntPtr(data, 8);
|
|
220
|
+
for (int i = 0; i < count; i++) {
|
|
221
|
+
// Native TOKEN_SECURITY_ATTRIBUTE_V1: UNICODE_STRING Name,
|
|
222
|
+
// type/reserved, flags, count, values (40 bytes on 64-bit).
|
|
223
|
+
if ((Marshal.ReadInt32(array, i * 40 + 20) & 1) == 0)
|
|
224
|
+
throw new UnauthorizedAccessException("Inheritable local security attributes unsupported");
|
|
225
|
+
}
|
|
226
|
+
} finally { Marshal.FreeHGlobal(data); }
|
|
227
|
+
}
|
|
228
|
+
static IntPtr MintDenyToken(IntPtr source, string[] roots) {
|
|
229
|
+
EnablePrivilege("SeCreateTokenPrivilege");
|
|
230
|
+
var memory = new System.Collections.Generic.List<IntPtr>();
|
|
231
|
+
IntPtr token = IntPtr.Zero;
|
|
232
|
+
try {
|
|
233
|
+
Func<int, IntPtr> query = kind => { var p = Query(source, kind); memory.Add(p); return p; };
|
|
234
|
+
// Fail closed for token types whose full authority cannot be mirrored here.
|
|
235
|
+
if (Marshal.ReadInt32(query(29)) != 0)
|
|
236
|
+
throw new UnauthorizedAccessException("AppContainer source unsupported");
|
|
237
|
+
// NtCreateTokenEx cannot silently discard enterprise/device claims.
|
|
238
|
+
// This narrow broker accepts only sources without those channels.
|
|
239
|
+
if (Marshal.ReadInt32(query(33), 4) != 0 || Marshal.ReadInt32(query(34), 4) != 0
|
|
240
|
+
|| Marshal.ReadInt32(query(37)) != 0)
|
|
241
|
+
throw new UnauthorizedAccessException("Claim-bearing source unsupported");
|
|
242
|
+
RejectInheritedAttributes(source);
|
|
243
|
+
var user = query(1); var originalGroups = query(2); var privileges = query(3);
|
|
244
|
+
var owner = query(4); var primary = query(5); var dacl = query(6); var tokenSource = query(7);
|
|
245
|
+
var statistics = (TokenStatistics)Marshal.PtrToStructure(query(10), typeof(TokenStatistics));
|
|
246
|
+
var session = query(12); var mandatory = query(27);
|
|
247
|
+
int count = Marshal.ReadInt32(originalGroups), stride = Marshal.SizeOf(typeof(SidAttributes));
|
|
248
|
+
if (count < 0 || count > 4096) throw new ArgumentException("Invalid group count");
|
|
249
|
+
int offset = IntPtr.Size == 8 ? 8 : 4;
|
|
250
|
+
var groups = Marshal.AllocHGlobal(offset + (count + roots.Length) * stride); memory.Add(groups);
|
|
251
|
+
Marshal.WriteInt32(groups, count + roots.Length);
|
|
252
|
+
for (int i = 0; i < count; i++) {
|
|
253
|
+
var group = (SidAttributes)Marshal.PtrToStructure(IntPtr.Add(originalGroups, offset + i * stride), typeof(SidAttributes));
|
|
254
|
+
Marshal.StructureToPtr(group, IntPtr.Add(groups, offset + i * stride), false);
|
|
255
|
+
}
|
|
256
|
+
for (int i = 0; i < roots.Length; i++) {
|
|
257
|
+
var identity = new SecurityIdentifier(roots[i]); var bytes = new byte[identity.BinaryLength]; identity.GetBinaryForm(bytes, 0);
|
|
258
|
+
var p = Marshal.AllocHGlobal(bytes.Length); memory.Add(p); Marshal.Copy(bytes, 0, p, bytes.Length);
|
|
259
|
+
// Private intermediate token only. Convert these normal groups
|
|
260
|
+
// to deny-only before any handle can leave the broker.
|
|
261
|
+
Marshal.StructureToPtr(new SidAttributes { Sid = p, Attributes = 7 }, IntPtr.Add(groups, offset + (count + i) * stride), false);
|
|
262
|
+
}
|
|
263
|
+
var qos = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SecurityQualityOfService))); memory.Add(qos);
|
|
264
|
+
Marshal.StructureToPtr(new SecurityQualityOfService { Length = Marshal.SizeOf(typeof(SecurityQualityOfService)), ImpersonationLevel = 2, ContextTrackingMode = 1 }, qos, false);
|
|
265
|
+
var attributes = new ObjectAttributes { Length = Marshal.SizeOf(typeof(ObjectAttributes)), SecurityQualityOfService = qos };
|
|
266
|
+
NtCheck(NtCreateTokenEx(out token, 0xF01FF, ref attributes, 1,
|
|
267
|
+
ref statistics.AuthenticationId, ref statistics.ExpirationTime, user, groups, privileges,
|
|
268
|
+
IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, mandatory, owner, primary, dacl, tokenSource), "NtCreateTokenEx");
|
|
269
|
+
IntPtr narrowed;
|
|
270
|
+
Check(CreateRestrictedToken(token, 1, (uint)roots.Length, IntPtr.Add(groups, offset + count * stride),
|
|
271
|
+
0, IntPtr.Zero, 0, IntPtr.Zero, out narrowed), "CreateRestrictedToken(broker deny groups)");
|
|
272
|
+
CloseHandle(token); token = narrowed;
|
|
273
|
+
Check(SetTokenInformation(token, 12, session, 4), "SetTokenInformation(session)");
|
|
274
|
+
var result = token; token = IntPtr.Zero; return result;
|
|
275
|
+
} finally {
|
|
276
|
+
if (token != IntPtr.Zero) CloseHandle(token);
|
|
277
|
+
foreach (var p in memory) Marshal.FreeHGlobal(p);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
static void NtCheck(int code, string name) {
|
|
281
|
+
if (code < 0) { int error = unchecked((int)RtlNtStatusToDosError(code)); throw new Win32Exception(error, name + " NTSTATUS=" + unchecked((uint)code).ToString("x") + " win32=" + error); }
|
|
282
|
+
}
|
|
283
|
+
static void Check(bool ok, string name) { if (!ok) { int error = Marshal.GetLastWin32Error(); throw new Win32Exception(error, name + " win32=" + error); } }
|
|
284
|
+
static void EnablePrivilege(string name) {
|
|
285
|
+
IntPtr token; Check(OpenProcessToken(GetCurrentProcess(), 0x28, out token), "OpenProcessToken(service)");
|
|
286
|
+
try { Luid luid; Check(LookupPrivilegeValue(null, name, out luid), "LookupPrivilegeValue");
|
|
287
|
+
var privileges = new TokenPrivileges { Count = 1, Luid = luid, Attributes = 2 };
|
|
288
|
+
Check(AdjustTokenPrivileges(token, false, ref privileges, 0, IntPtr.Zero, IntPtr.Zero), "AdjustTokenPrivileges");
|
|
289
|
+
int error = Marshal.GetLastWin32Error(); if (error != 0) throw new Win32Exception(error, "EnablePrivilege(" + name + ") win32=" + error);
|
|
290
|
+
} finally { CloseHandle(token); }
|
|
291
|
+
}
|
|
292
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessAccessToken { public IntPtr Token, Thread; }
|
|
293
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessBasicInformation { public int ExitStatus; public IntPtr Peb; public UIntPtr Affinity; public int Priority; public UIntPtr ProcessId, ParentId; }
|
|
294
|
+
[DllImport("ntdll.dll")] static extern int NtQueryInformationProcess(IntPtr process, int kind, out ProcessBasicInformation info, int size, out int length);
|
|
295
|
+
[DllImport("ntdll.dll")] static extern int NtSetInformationProcess(IntPtr process, int kind, ref ProcessAccessToken info, int size);
|
|
296
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern uint GetProcessIdOfThread(IntPtr thread);
|
|
297
|
+
[DllImport("advapi32.dll")] static extern uint SetSecurityInfo(IntPtr handle, int kind, uint info, IntPtr owner, IntPtr group, IntPtr dacl, IntPtr sacl);
|
|
298
|
+
[StructLayout(LayoutKind.Sequential)] struct SidAttributes { public IntPtr Sid; public uint Attributes; }
|
|
299
|
+
[StructLayout(LayoutKind.Sequential)] struct ObjectAttributes { public int Length; public IntPtr RootDirectory, ObjectName; public uint Attributes; public IntPtr SecurityDescriptor, SecurityQualityOfService; }
|
|
300
|
+
[StructLayout(LayoutKind.Sequential)] struct SecurityQualityOfService { public int Length, ImpersonationLevel; public byte ContextTrackingMode, EffectiveOnly; }
|
|
301
|
+
[StructLayout(LayoutKind.Sequential)] struct TokenStatistics { public Luid TokenId, AuthenticationId; public long ExpirationTime; public int Type, ImpersonationLevel; public uint DynamicCharged, DynamicAvailable, GroupCount, PrivilegeCount; public Luid ModifiedId; }
|
|
302
|
+
[DllImport("ntdll.dll")] static extern int NtCreateTokenEx(out IntPtr token, uint access, ref ObjectAttributes attributes, int type,
|
|
303
|
+
ref Luid authId, ref long expiration, IntPtr user, IntPtr groups, IntPtr privileges, IntPtr userClaims, IntPtr deviceClaims,
|
|
304
|
+
IntPtr deviceGroups, IntPtr mandatory, IntPtr owner, IntPtr primary, IntPtr dacl, IntPtr source);
|
|
305
|
+
[DllImport("ntdll.dll")] static extern int NtCompareTokens(IntPtr first, IntPtr second, [MarshalAs(UnmanagedType.U1)] out bool equal);
|
|
306
|
+
[DllImport("ntdll.dll")] static extern uint RtlNtStatusToDosError(int status);
|
|
307
|
+
[DllImport("ntdll.dll")] static extern int NtQuerySecurityAttributesToken(IntPtr token, IntPtr names, uint count, IntPtr data, int size, out int length);
|
|
308
|
+
[StructLayout(LayoutKind.Sequential)] struct Luid { public uint Low; public int High; }
|
|
309
|
+
[StructLayout(LayoutKind.Sequential)] struct TokenPrivileges { public uint Count; public Luid Luid; public uint Attributes; }
|
|
310
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool GetNamedPipeClientProcessId(IntPtr pipe, out uint pid);
|
|
311
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
|
|
312
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
|
313
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool DuplicateHandle(IntPtr source, IntPtr handle, IntPtr target, out IntPtr duplicate, uint access, bool inherit, uint options);
|
|
314
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
315
|
+
[DllImport("advapi32.dll", SetLastError=true)] static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token);
|
|
316
|
+
[DllImport("advapi32.dll", SetLastError=true)] static extern bool GetTokenInformation(IntPtr token, int kind, IntPtr data, int size, out int length);
|
|
317
|
+
[DllImport("advapi32.dll", SetLastError=true)] static extern bool SetTokenInformation(IntPtr token, int kind, IntPtr data, int size);
|
|
318
|
+
[DllImport("advapi32.dll", SetLastError=true)] static extern bool CreateRestrictedToken(IntPtr token, uint flags, uint disableCount, IntPtr disableSids,
|
|
319
|
+
uint deleteCount, IntPtr deletePrivileges, uint restrictCount, IntPtr restrictSids, out IntPtr result);
|
|
320
|
+
[DllImport("advapi32.dll")] static extern bool IsTokenRestricted(IntPtr token);
|
|
321
|
+
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool LookupPrivilegeValue(string system, string name, out Luid luid);
|
|
322
|
+
[DllImport("advapi32.dll", SetLastError=true)] static extern bool AdjustTokenPrivileges(IntPtr token, bool disable, ref TokenPrivileges privileges, int size, IntPtr previous, IntPtr returned);
|
|
323
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Native lifecycle evidence on an elevated, disposable Windows CI host.
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
$installer = Join-Path $PSScriptRoot 'install.ps1'
|
|
4
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
5
|
+
$sha = [Security.Cryptography.SHA256]::Create()
|
|
6
|
+
try { $hash = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($sid)))).Replace('-', '').ToLower().Substring(0,16) }
|
|
7
|
+
finally { $sha.Dispose() }
|
|
8
|
+
$name = "DshMultiRootBroker-$hash"
|
|
9
|
+
$serviceSid = [regex]::Match((sc.exe showsid $name | Out-String), 'S-1-5-80-(?:[0-9]+-){4}[0-9]+').Value
|
|
10
|
+
$record = Join-Path (Join-Path $env:ProgramFiles $name) 'added-rights.json'
|
|
11
|
+
$required = @('SeServiceLogonRight','SeCreateTokenPrivilege','SeAssignPrimaryTokenPrivilege','SeTcbPrivilege','SeDebugPrivilege','SeImpersonatePrivilege')
|
|
12
|
+
function Read-Policy {
|
|
13
|
+
$file = Join-Path $env:TEMP ([Guid]::NewGuid().ToString() + '.inf')
|
|
14
|
+
try {
|
|
15
|
+
secedit.exe /export /cfg $file /areas USER_RIGHTS /quiet | Out-Null
|
|
16
|
+
if ($LASTEXITCODE -ne 0) { throw 'Cannot export account policy.' }
|
|
17
|
+
$policy = @{}
|
|
18
|
+
foreach ($line in Get-Content $file) {
|
|
19
|
+
if ($line -match '^(Se\w+)\s*=\s*(.*)$') { $policy[$Matches[1]] = @($Matches[2].Split(',') | ForEach-Object { $_.Trim().TrimStart('*') } | Where-Object { $_ } | Sort-Object) }
|
|
20
|
+
}
|
|
21
|
+
return $policy
|
|
22
|
+
} finally { Remove-Item $file -Force -ErrorAction SilentlyContinue }
|
|
23
|
+
}
|
|
24
|
+
function Assert-Rights($policy, $expected) {
|
|
25
|
+
foreach ($right in $required) {
|
|
26
|
+
if (($policy[$right] -contains $serviceSid) -ne ($expected -contains $right)) { throw "Unexpected service policy for $right" }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
# Capture the host's installed state; the installer must not touch other SIDs.
|
|
30
|
+
$installed = Read-Policy
|
|
31
|
+
Assert-Rights $installed $required
|
|
32
|
+
$added = [string[]]@(Get-Content -Raw $record | ConvertFrom-Json)
|
|
33
|
+
$preexisting = @($required | Where-Object { $added -notcontains $_ })
|
|
34
|
+
& $installer -Uninstall
|
|
35
|
+
$baseline = Read-Policy
|
|
36
|
+
Assert-Rights $baseline $preexisting
|
|
37
|
+
if (Get-Service $name -ErrorAction SilentlyContinue) { throw 'Service remains after uninstall.' }
|
|
38
|
+
if (Test-Path (Split-Path $record)) { throw 'Installation directory remains after uninstall.' }
|
|
39
|
+
foreach ($right in $required) {
|
|
40
|
+
if ((@($installed[$right] | Where-Object { $_ -ne $serviceSid }) -join ',') -ne (@($baseline[$right] | Where-Object { $_ -ne $serviceSid }) -join ',')) { throw "Other account policy changed for $right" }
|
|
41
|
+
}
|
|
42
|
+
# Simulate pre-existing policy on this dedicated account. Installation may only
|
|
43
|
+
# record/revoke its own additions. Preserve the original baseline on cleanup.
|
|
44
|
+
$seed = [DshBrokerRights]::Install($serviceSid)
|
|
45
|
+
[DshBrokerRights]::Remove($serviceSid, [string[]]@($seed | Where-Object { $_ -ne 'SeServiceLogonRight' }))
|
|
46
|
+
try {
|
|
47
|
+
# Inject each post-grant failure through PowerShell's command resolution.
|
|
48
|
+
# Include a partial staging record, then verify actual LSA policy and SCM.
|
|
49
|
+
foreach ($step in @('Set-Content', 'Move-Item', 'Start-Service')) {
|
|
50
|
+
Set-Item -Path "Function:$step" -Value {
|
|
51
|
+
Microsoft.PowerShell.Management\Set-Content -LiteralPath "$record.pending" -Value '['
|
|
52
|
+
throw 'Injected installation failure'
|
|
53
|
+
}
|
|
54
|
+
$failed = $false
|
|
55
|
+
try { & $installer }
|
|
56
|
+
catch {
|
|
57
|
+
if ($_.Exception.Message -notlike '*Injected installation failure*') { throw }
|
|
58
|
+
$failed = $true
|
|
59
|
+
} finally { Remove-Item -Path "Function:$step" -Force }
|
|
60
|
+
if (-not $failed) { throw "Failure injection did not run: $step" }
|
|
61
|
+
Assert-Rights (Read-Policy) @($preexisting + 'SeServiceLogonRight')
|
|
62
|
+
if (Test-Path $record) { throw 'Rollback retained a committed rights record.' }
|
|
63
|
+
if (Test-Path "$record.pending") { throw 'Rollback retained a partial rights record.' }
|
|
64
|
+
if (Get-Service $name -ErrorAction SilentlyContinue) { throw 'Rollback retained the failed service.' }
|
|
65
|
+
Write-Output "Verified rollback after $step failure"
|
|
66
|
+
}
|
|
67
|
+
& $installer
|
|
68
|
+
Assert-Rights (Read-Policy) $required
|
|
69
|
+
if (@(Get-Content -Raw $record | ConvertFrom-Json) -contains 'SeServiceLogonRight') { throw 'Installer recorded a pre-existing right.' }
|
|
70
|
+
& $installer # Upgrade must preserve the same pre-existing right.
|
|
71
|
+
if (@(Get-Content -Raw $record | ConvertFrom-Json) -contains 'SeServiceLogonRight') { throw 'Upgrade recorded a pre-existing right.' }
|
|
72
|
+
& $installer -Uninstall
|
|
73
|
+
Assert-Rights (Read-Policy) @($preexisting + 'SeServiceLogonRight')
|
|
74
|
+
} finally {
|
|
75
|
+
& $installer -Uninstall
|
|
76
|
+
if ($seed -contains 'SeServiceLogonRight') { [DshBrokerRights]::Remove($serviceSid, [string[]]@('SeServiceLogonRight')) }
|
|
77
|
+
}
|
|
78
|
+
Assert-Rights (Read-Policy) $preexisting
|
|
79
|
+
& $installer
|
|
80
|
+
Write-Output 'Broker lifecycle verified: install, upgrade, uninstall, pre-existing rights preserved; other accounts unchanged.'
|