@wzwjustdoit/dsh-plugin-git-bash 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +154 -2
- package/client.js +736 -0
- package/cordis.patch.yml +27 -0
- package/lib/agents.d.ts +5 -0
- package/lib/agents.d.ts.map +1 -0
- package/lib/agents.js +56 -0
- package/lib/agents.js.map +1 -0
- package/lib/discovery.d.ts +21 -0
- package/lib/discovery.d.ts.map +1 -0
- package/lib/discovery.js +122 -0
- package/lib/discovery.js.map +1 -0
- package/lib/index.d.ts +55 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +369 -0
- package/lib/index.js.map +1 -0
- package/native/CMakeLists.txt +58 -0
- package/native/bin/win32-x64/msys-token-guard-hook.dll +0 -0
- package/native/bin/win32-x64/msys-token-guard.exe +0 -0
- package/native/msys-token-guard-hook.cpp +470 -0
- package/native/msys-token-guard.cpp +671 -0
- package/native/vendor/detours/LICENSE.md +23 -0
- package/native/vendor/detours/README.dsh.md +13 -0
- package/native/vendor/detours/creatwth.cpp +1577 -0
- package/native/vendor/detours/detours.cpp +2364 -0
- package/native/vendor/detours/detours.h +1059 -0
- package/native/vendor/detours/detver.h +27 -0
- package/native/vendor/detours/disasm.cpp +4236 -0
- package/native/vendor/detours/image.cpp +2251 -0
- package/native/vendor/detours/modules.cpp +929 -0
- package/native/vendor/detours/uimports.cpp +269 -0
- package/package.json +120 -4
- package/scripts/build-native.mjs +35 -0
- package/scripts/dsh.desktop.startup/README.md +27 -0
- package/scripts/dsh.desktop.startup/dsh.desktop.startup.gitbash.bat +28 -0
- package/scripts/dsh.desktop.startup/dsh.desktop.startup.gitbash.sh +46 -0
- package/scripts/dsh.desktop.startup/dsh.desktop.startup.gitbash.vbs +0 -0
- package/scripts/dsh.web.startup/README.md +33 -0
- package/scripts/dsh.web.startup/dsh.web.startup.gitbash.bat +28 -0
- package/scripts/dsh.web.startup/dsh.web.startup.gitbash.sh +48 -0
- package/scripts/dsh.web.startup/dsh.web.startup.gitbash.vbs +0 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
#define WIN32_LEAN_AND_MEAN
|
|
2
|
+
#define NOMINMAX
|
|
3
|
+
#include <windows.h>
|
|
4
|
+
#include <aclapi.h>
|
|
5
|
+
#include <sddl.h>
|
|
6
|
+
|
|
7
|
+
#include <algorithm>
|
|
8
|
+
#include <cstdio>
|
|
9
|
+
#include <cstdint>
|
|
10
|
+
#include <cwchar>
|
|
11
|
+
#include <stdexcept>
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <utility>
|
|
14
|
+
#include <vector>
|
|
15
|
+
|
|
16
|
+
#include "detours.h"
|
|
17
|
+
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
constexpr int kUsageExitCode = 64;
|
|
21
|
+
constexpr int kGuardFailureExitCode = 125;
|
|
22
|
+
constexpr wchar_t kReadyHandleEnvironment[] = L"DSH_MSYS_TOKEN_GUARD_READY_HANDLE";
|
|
23
|
+
|
|
24
|
+
class GuardFailure final : public std::runtime_error {
|
|
25
|
+
public:
|
|
26
|
+
GuardFailure(std::string operation, DWORD code)
|
|
27
|
+
: std::runtime_error(std::move(operation)), code_(code) {}
|
|
28
|
+
|
|
29
|
+
[[nodiscard]] DWORD code() const noexcept { return code_; }
|
|
30
|
+
|
|
31
|
+
private:
|
|
32
|
+
DWORD code_;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class UniqueHandle final {
|
|
36
|
+
public:
|
|
37
|
+
UniqueHandle() = default;
|
|
38
|
+
explicit UniqueHandle(HANDLE value) : value_(value) {}
|
|
39
|
+
~UniqueHandle() { reset(); }
|
|
40
|
+
|
|
41
|
+
UniqueHandle(const UniqueHandle&) = delete;
|
|
42
|
+
UniqueHandle& operator=(const UniqueHandle&) = delete;
|
|
43
|
+
|
|
44
|
+
UniqueHandle(UniqueHandle&& other) noexcept : value_(other.release()) {}
|
|
45
|
+
UniqueHandle& operator=(UniqueHandle&& other) noexcept {
|
|
46
|
+
if (this != &other) reset(other.release());
|
|
47
|
+
return *this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[[nodiscard]] HANDLE get() const noexcept { return value_; }
|
|
51
|
+
[[nodiscard]] explicit operator bool() const noexcept {
|
|
52
|
+
return value_ != nullptr && value_ != INVALID_HANDLE_VALUE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
HANDLE release() noexcept {
|
|
56
|
+
HANDLE value = value_;
|
|
57
|
+
value_ = nullptr;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
void reset(HANDLE value = nullptr) noexcept {
|
|
62
|
+
if (*this) CloseHandle(value_);
|
|
63
|
+
value_ = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private:
|
|
67
|
+
HANDLE value_ = nullptr;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class LocalAllocation final {
|
|
71
|
+
public:
|
|
72
|
+
LocalAllocation() = default;
|
|
73
|
+
explicit LocalAllocation(HLOCAL value) : value_(value) {}
|
|
74
|
+
~LocalAllocation() { reset(); }
|
|
75
|
+
|
|
76
|
+
LocalAllocation(const LocalAllocation&) = delete;
|
|
77
|
+
LocalAllocation& operator=(const LocalAllocation&) = delete;
|
|
78
|
+
|
|
79
|
+
[[nodiscard]] HLOCAL get() const noexcept { return value_; }
|
|
80
|
+
[[nodiscard]] explicit operator bool() const noexcept { return value_ != nullptr; }
|
|
81
|
+
|
|
82
|
+
void reset(HLOCAL value = nullptr) noexcept {
|
|
83
|
+
if (value_ != nullptr) LocalFree(value_);
|
|
84
|
+
value_ = value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private:
|
|
88
|
+
HLOCAL value_ = nullptr;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
[[noreturn]] void fail(const char* operation, DWORD code = GetLastError()) {
|
|
92
|
+
throw GuardFailure(operation, code);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
[[noreturn]] void fail_invariant(const char* detail) {
|
|
96
|
+
throw GuardFailure(detail, ERROR_INVALID_SECURITY_DESCR);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
std::vector<BYTE> token_information(HANDLE token, TOKEN_INFORMATION_CLASS info_class) {
|
|
100
|
+
DWORD required = 0;
|
|
101
|
+
if (GetTokenInformation(token, info_class, nullptr, 0, &required) != FALSE) {
|
|
102
|
+
fail_invariant("GetTokenInformation unexpectedly accepted an empty buffer");
|
|
103
|
+
}
|
|
104
|
+
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || required == 0) {
|
|
105
|
+
fail("GetTokenInformation(size)");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
std::vector<BYTE> buffer(required);
|
|
109
|
+
if (GetTokenInformation(token, info_class, buffer.data(), required, &required) == FALSE) {
|
|
110
|
+
fail("GetTokenInformation(data)");
|
|
111
|
+
}
|
|
112
|
+
return buffer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
UniqueHandle open_process_token(HANDLE process, DWORD access, const char* operation) {
|
|
116
|
+
HANDLE token = nullptr;
|
|
117
|
+
if (OpenProcessToken(process, access, &token) == FALSE) fail(operation);
|
|
118
|
+
return UniqueHandle(token);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
std::wstring sid_string(PSID sid) {
|
|
122
|
+
if (sid == nullptr || IsValidSid(sid) == FALSE) fail_invariant("invalid SID in token data");
|
|
123
|
+
LPWSTR raw = nullptr;
|
|
124
|
+
if (ConvertSidToStringSidW(sid, &raw) == FALSE) fail("ConvertSidToStringSidW");
|
|
125
|
+
LocalAllocation value(raw);
|
|
126
|
+
return std::wstring(raw);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
std::vector<std::wstring> restricted_sid_set(HANDLE token) {
|
|
130
|
+
const auto buffer = token_information(token, TokenRestrictedSids);
|
|
131
|
+
const auto* groups = reinterpret_cast<const TOKEN_GROUPS*>(buffer.data());
|
|
132
|
+
if (groups->GroupCount == 0) fail_invariant("token carries no restricting SIDs");
|
|
133
|
+
|
|
134
|
+
std::vector<std::wstring> result;
|
|
135
|
+
result.reserve(groups->GroupCount);
|
|
136
|
+
for (DWORD index = 0; index < groups->GroupCount; ++index) {
|
|
137
|
+
result.push_back(sid_string(groups->Groups[index].Sid));
|
|
138
|
+
}
|
|
139
|
+
std::sort(result.begin(), result.end());
|
|
140
|
+
result.erase(std::unique(result.begin(), result.end()), result.end());
|
|
141
|
+
if (result.size() != groups->GroupCount) fail_invariant("token carries duplicate restricting SIDs");
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
bool default_dacl_allows_restricting_sid(HANDLE token) {
|
|
146
|
+
const auto restricted_buffer = token_information(token, TokenRestrictedSids);
|
|
147
|
+
const auto* restricted = reinterpret_cast<const TOKEN_GROUPS*>(restricted_buffer.data());
|
|
148
|
+
const auto dacl_buffer = token_information(token, TokenDefaultDacl);
|
|
149
|
+
const auto* info = reinterpret_cast<const TOKEN_DEFAULT_DACL*>(dacl_buffer.data());
|
|
150
|
+
PACL dacl = info->DefaultDacl;
|
|
151
|
+
if (dacl == nullptr || IsValidAcl(dacl) == FALSE) fail_invariant("token default DACL is missing or invalid");
|
|
152
|
+
|
|
153
|
+
ACL_SIZE_INFORMATION size_info{};
|
|
154
|
+
if (GetAclInformation(dacl, &size_info, sizeof(size_info), AclSizeInformation) == FALSE) {
|
|
155
|
+
fail("GetAclInformation(TokenDefaultDacl)");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (DWORD ace_index = 0; ace_index < size_info.AceCount; ++ace_index) {
|
|
159
|
+
void* raw_ace = nullptr;
|
|
160
|
+
if (GetAce(dacl, ace_index, &raw_ace) == FALSE) fail("GetAce(TokenDefaultDacl)");
|
|
161
|
+
const auto* header = static_cast<const ACE_HEADER*>(raw_ace);
|
|
162
|
+
if (header->AceType != ACCESS_ALLOWED_ACE_TYPE) continue;
|
|
163
|
+
|
|
164
|
+
const auto* ace = static_cast<const ACCESS_ALLOWED_ACE*>(raw_ace);
|
|
165
|
+
const bool grants_full_access = (ace->Mask & GENERIC_ALL) != 0 ||
|
|
166
|
+
(ace->Mask & FILE_ALL_ACCESS) == FILE_ALL_ACCESS;
|
|
167
|
+
if (!grants_full_access) continue;
|
|
168
|
+
PSID ace_sid = const_cast<DWORD*>(&ace->SidStart);
|
|
169
|
+
if (IsValidSid(ace_sid) == FALSE) fail_invariant("invalid SID in token default DACL");
|
|
170
|
+
|
|
171
|
+
for (DWORD sid_index = 0; sid_index < restricted->GroupCount; ++sid_index) {
|
|
172
|
+
if (EqualSid(ace_sid, restricted->Groups[sid_index].Sid) != FALSE) return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
void verify_token_invariants(HANDLE token, const std::vector<std::wstring>* expected_sids = nullptr) {
|
|
179
|
+
const auto sids = restricted_sid_set(token);
|
|
180
|
+
if (expected_sids != nullptr && sids != *expected_sids) {
|
|
181
|
+
fail_invariant("child restricting SID set differs from the guard token");
|
|
182
|
+
}
|
|
183
|
+
if (!default_dacl_allows_restricting_sid(token)) {
|
|
184
|
+
fail_invariant("token default DACL has no full-access allow ACE for a restricting SID");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
bool default_dacl_covers_user(HANDLE token) {
|
|
189
|
+
const auto user_buffer = token_information(token, TokenUser);
|
|
190
|
+
const auto* user = reinterpret_cast<const TOKEN_USER*>(user_buffer.data());
|
|
191
|
+
if (user->User.Sid == nullptr || IsValidSid(user->User.Sid) == FALSE) {
|
|
192
|
+
fail_invariant("server token carries an invalid user SID");
|
|
193
|
+
}
|
|
194
|
+
const auto dacl_buffer = token_information(token, TokenDefaultDacl);
|
|
195
|
+
const auto* info = reinterpret_cast<const TOKEN_DEFAULT_DACL*>(dacl_buffer.data());
|
|
196
|
+
PACL dacl = info->DefaultDacl;
|
|
197
|
+
if (dacl == nullptr || IsValidAcl(dacl) == FALSE) {
|
|
198
|
+
fail_invariant("server token default DACL is missing or invalid");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ACL_SIZE_INFORMATION size_info{};
|
|
202
|
+
if (GetAclInformation(dacl, &size_info, sizeof(size_info), AclSizeInformation) == FALSE) {
|
|
203
|
+
fail("GetAclInformation(server default DACL)");
|
|
204
|
+
}
|
|
205
|
+
const ACCESS_MASK required = TOKEN_QUERY | TOKEN_ADJUST_DEFAULT;
|
|
206
|
+
for (DWORD ace_index = 0; ace_index < size_info.AceCount; ++ace_index) {
|
|
207
|
+
void* raw_ace = nullptr;
|
|
208
|
+
if (GetAce(dacl, ace_index, &raw_ace) == FALSE) fail("GetAce(server default DACL)");
|
|
209
|
+
const auto* header = static_cast<const ACE_HEADER*>(raw_ace);
|
|
210
|
+
if (header->AceType != ACCESS_ALLOWED_ACE_TYPE) continue;
|
|
211
|
+
|
|
212
|
+
const auto* ace = static_cast<const ACCESS_ALLOWED_ACE*>(raw_ace);
|
|
213
|
+
const bool covers = (ace->Mask & GENERIC_ALL) != 0 || (ace->Mask & required) == required;
|
|
214
|
+
if (!covers) continue;
|
|
215
|
+
PSID ace_sid = const_cast<DWORD*>(&ace->SidStart);
|
|
216
|
+
if (IsValidSid(ace_sid) == FALSE) fail_invariant("invalid SID in server token default DACL");
|
|
217
|
+
if (EqualSid(ace_sid, user->User.Sid) != FALSE) return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// DSH derives every restricted token from the server's own process token, and
|
|
223
|
+
// the restricted DACL starts as a copy of the server's default DACL. Servers
|
|
224
|
+
// started outside MSYS2 can carry a DACL without the user SID, which blocks
|
|
225
|
+
// the guard's workspace-write child-token setup. Stamp the standard shape:
|
|
226
|
+
// an explicit full-access ACE for the user SID, the same normalization the
|
|
227
|
+
// MSYS2 runtime applies to tokens of its own processes. Runs once per server
|
|
228
|
+
// process on an unrestricted token; exits 0 either way.
|
|
229
|
+
int normalize_server_default_dacl() {
|
|
230
|
+
UniqueHandle token = open_process_token(
|
|
231
|
+
GetCurrentProcess(),
|
|
232
|
+
TOKEN_QUERY | TOKEN_ADJUST_DEFAULT,
|
|
233
|
+
"OpenProcessToken(server default DACL)");
|
|
234
|
+
if (default_dacl_covers_user(token.get())) {
|
|
235
|
+
std::fprintf(stdout, "server-dacl=ok\n");
|
|
236
|
+
return ERROR_SUCCESS;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const auto user_buffer = token_information(token.get(), TokenUser);
|
|
240
|
+
const auto* user = reinterpret_cast<const TOKEN_USER*>(user_buffer.data());
|
|
241
|
+
const auto dacl_buffer = token_information(token.get(), TokenDefaultDacl);
|
|
242
|
+
const auto* info = reinterpret_cast<const TOKEN_DEFAULT_DACL*>(dacl_buffer.data());
|
|
243
|
+
|
|
244
|
+
EXPLICIT_ACCESSW grant{};
|
|
245
|
+
grant.grfAccessPermissions = FILE_ALL_ACCESS;
|
|
246
|
+
grant.grfAccessMode = GRANT_ACCESS;
|
|
247
|
+
grant.grfInheritance = NO_INHERITANCE;
|
|
248
|
+
grant.Trustee.pMultipleTrustee = nullptr;
|
|
249
|
+
grant.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
|
|
250
|
+
grant.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
|
251
|
+
grant.Trustee.TrusteeType = TRUSTEE_IS_USER;
|
|
252
|
+
grant.Trustee.ptstrName = static_cast<LPWSTR>(user->User.Sid);
|
|
253
|
+
|
|
254
|
+
PACL extended_dacl = nullptr;
|
|
255
|
+
DWORD status = SetEntriesInAclW(1, &grant, info->DefaultDacl, &extended_dacl);
|
|
256
|
+
if (status != ERROR_SUCCESS) fail("SetEntriesInAclW(server default DACL)", status);
|
|
257
|
+
LocalAllocation extended_owner(extended_dacl);
|
|
258
|
+
|
|
259
|
+
TOKEN_DEFAULT_DACL updated{};
|
|
260
|
+
updated.DefaultDacl = extended_dacl;
|
|
261
|
+
if (SetTokenInformation(token.get(), TokenDefaultDacl, &updated, sizeof(updated)) == FALSE) {
|
|
262
|
+
fail("SetTokenInformation(server default DACL)");
|
|
263
|
+
}
|
|
264
|
+
if (!default_dacl_covers_user(token.get())) {
|
|
265
|
+
fail_invariant("server default DACL lacks user coverage after the grant");
|
|
266
|
+
}
|
|
267
|
+
std::fprintf(stdout, "server-dacl=normalized\n");
|
|
268
|
+
return ERROR_SUCCESS;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
void grant_logon_sid_to_default_dacl(HANDLE process) {
|
|
272
|
+
UniqueHandle token = open_process_token(
|
|
273
|
+
process,
|
|
274
|
+
TOKEN_QUERY | TOKEN_ADJUST_DEFAULT,
|
|
275
|
+
"OpenProcessToken(child default DACL)");
|
|
276
|
+
auto groups_buffer = token_information(token.get(), TokenGroups);
|
|
277
|
+
auto* groups = reinterpret_cast<TOKEN_GROUPS*>(groups_buffer.data());
|
|
278
|
+
PSID logon_sid = nullptr;
|
|
279
|
+
for (DWORD index = 0; index < groups->GroupCount; ++index) {
|
|
280
|
+
if ((groups->Groups[index].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID) {
|
|
281
|
+
logon_sid = groups->Groups[index].Sid;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (logon_sid == nullptr || IsValidSid(logon_sid) == FALSE) {
|
|
286
|
+
fail_invariant("child token carries no valid logon SID");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const auto restricted_buffer = token_information(token.get(), TokenRestrictedSids);
|
|
290
|
+
const auto* restricted = reinterpret_cast<const TOKEN_GROUPS*>(restricted_buffer.data());
|
|
291
|
+
bool logon_is_restricting = false;
|
|
292
|
+
for (DWORD index = 0; index < restricted->GroupCount; ++index) {
|
|
293
|
+
if (EqualSid(logon_sid, restricted->Groups[index].Sid) != FALSE) {
|
|
294
|
+
logon_is_restricting = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!logon_is_restricting) {
|
|
299
|
+
fail_invariant("child logon SID is not a restricting SID");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const auto dacl_buffer = token_information(token.get(), TokenDefaultDacl);
|
|
303
|
+
const auto* current_info = reinterpret_cast<const TOKEN_DEFAULT_DACL*>(dacl_buffer.data());
|
|
304
|
+
if (current_info->DefaultDacl == nullptr || IsValidAcl(current_info->DefaultDacl) == FALSE) {
|
|
305
|
+
fail_invariant("child token default DACL is missing or invalid");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
EXPLICIT_ACCESSW grant{};
|
|
309
|
+
grant.grfAccessPermissions = FILE_ALL_ACCESS;
|
|
310
|
+
grant.grfAccessMode = GRANT_ACCESS;
|
|
311
|
+
grant.grfInheritance = NO_INHERITANCE;
|
|
312
|
+
grant.Trustee.pMultipleTrustee = nullptr;
|
|
313
|
+
grant.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
|
|
314
|
+
grant.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
|
315
|
+
grant.Trustee.TrusteeType = TRUSTEE_IS_GROUP;
|
|
316
|
+
grant.Trustee.ptstrName = static_cast<LPWSTR>(logon_sid);
|
|
317
|
+
|
|
318
|
+
PACL extended_dacl = nullptr;
|
|
319
|
+
DWORD status = SetEntriesInAclW(1, &grant, current_info->DefaultDacl, &extended_dacl);
|
|
320
|
+
if (status != ERROR_SUCCESS) fail("SetEntriesInAclW(logon default DACL)", status);
|
|
321
|
+
LocalAllocation extended_owner(extended_dacl);
|
|
322
|
+
|
|
323
|
+
TOKEN_DEFAULT_DACL updated{};
|
|
324
|
+
updated.DefaultDacl = extended_dacl;
|
|
325
|
+
if (SetTokenInformation(
|
|
326
|
+
token.get(), TokenDefaultDacl, &updated, sizeof(updated)) == FALSE) {
|
|
327
|
+
fail("SetTokenInformation(logon default DACL)");
|
|
328
|
+
}
|
|
329
|
+
verify_token_invariants(token.get());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
bool adjust_default_is_denied(HANDLE process) {
|
|
333
|
+
HANDLE probe = nullptr;
|
|
334
|
+
if (OpenProcessToken(process, TOKEN_ADJUST_DEFAULT, &probe) != FALSE) {
|
|
335
|
+
CloseHandle(probe);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const DWORD code = GetLastError();
|
|
339
|
+
if (code != ERROR_ACCESS_DENIED) fail("OpenProcessToken(TOKEN_ADJUST_DEFAULT probe)", code);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
void deny_adjust_default(HANDLE process, HANDLE query_token) {
|
|
344
|
+
if (adjust_default_is_denied(process)) return;
|
|
345
|
+
|
|
346
|
+
auto user_buffer = token_information(query_token, TokenUser);
|
|
347
|
+
auto* user = reinterpret_cast<TOKEN_USER*>(user_buffer.data());
|
|
348
|
+
if (user->User.Sid == nullptr || IsValidSid(user->User.Sid) == FALSE) {
|
|
349
|
+
fail_invariant("child token user SID is invalid");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
UniqueHandle writable = open_process_token(
|
|
353
|
+
process,
|
|
354
|
+
TOKEN_QUERY | READ_CONTROL | WRITE_DAC,
|
|
355
|
+
"OpenProcessToken(child WRITE_DAC)");
|
|
356
|
+
|
|
357
|
+
PACL current_dacl = nullptr;
|
|
358
|
+
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
359
|
+
DWORD status = GetSecurityInfo(
|
|
360
|
+
writable.get(),
|
|
361
|
+
SE_KERNEL_OBJECT,
|
|
362
|
+
DACL_SECURITY_INFORMATION,
|
|
363
|
+
nullptr,
|
|
364
|
+
nullptr,
|
|
365
|
+
¤t_dacl,
|
|
366
|
+
nullptr,
|
|
367
|
+
&descriptor);
|
|
368
|
+
if (status != ERROR_SUCCESS) fail("GetSecurityInfo(child token)", status);
|
|
369
|
+
LocalAllocation descriptor_owner(descriptor);
|
|
370
|
+
if (current_dacl == nullptr || IsValidAcl(current_dacl) == FALSE) {
|
|
371
|
+
fail_invariant("child token object DACL is missing or invalid");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
EXPLICIT_ACCESSW deny{};
|
|
375
|
+
deny.grfAccessPermissions = TOKEN_ADJUST_DEFAULT;
|
|
376
|
+
deny.grfAccessMode = DENY_ACCESS;
|
|
377
|
+
deny.grfInheritance = NO_INHERITANCE;
|
|
378
|
+
deny.Trustee.pMultipleTrustee = nullptr;
|
|
379
|
+
deny.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
|
|
380
|
+
deny.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
|
381
|
+
deny.Trustee.TrusteeType = TRUSTEE_IS_USER;
|
|
382
|
+
deny.Trustee.ptstrName = static_cast<LPWSTR>(user->User.Sid);
|
|
383
|
+
|
|
384
|
+
PACL hardened_dacl = nullptr;
|
|
385
|
+
status = SetEntriesInAclW(1, &deny, current_dacl, &hardened_dacl);
|
|
386
|
+
if (status != ERROR_SUCCESS) fail("SetEntriesInAclW(child token)", status);
|
|
387
|
+
LocalAllocation hardened_owner(hardened_dacl);
|
|
388
|
+
|
|
389
|
+
status = SetSecurityInfo(
|
|
390
|
+
writable.get(),
|
|
391
|
+
SE_KERNEL_OBJECT,
|
|
392
|
+
DACL_SECURITY_INFORMATION,
|
|
393
|
+
nullptr,
|
|
394
|
+
nullptr,
|
|
395
|
+
hardened_dacl,
|
|
396
|
+
nullptr);
|
|
397
|
+
if (status != ERROR_SUCCESS) fail("SetSecurityInfo(child token)", status);
|
|
398
|
+
|
|
399
|
+
if (!adjust_default_is_denied(process)) {
|
|
400
|
+
fail_invariant("child token still grants TOKEN_ADJUST_DEFAULT after hardening");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
auto query_probe = open_process_token(process, TOKEN_QUERY, "OpenProcessToken(child TOKEN_QUERY probe)");
|
|
404
|
+
verify_token_invariants(query_probe.get());
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
bool needs_quotes(const std::wstring& argument) {
|
|
408
|
+
if (argument.empty()) return true;
|
|
409
|
+
return argument.find_first_of(L" \t\n\v\"") != std::wstring::npos;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
std::wstring quote_argument(const std::wstring& argument) {
|
|
413
|
+
if (!needs_quotes(argument)) return argument;
|
|
414
|
+
|
|
415
|
+
std::wstring quoted;
|
|
416
|
+
quoted.push_back(L'"');
|
|
417
|
+
size_t backslashes = 0;
|
|
418
|
+
for (wchar_t character : argument) {
|
|
419
|
+
if (character == L'\\') {
|
|
420
|
+
++backslashes;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (character == L'"') {
|
|
424
|
+
quoted.append(backslashes * 2 + 1, L'\\');
|
|
425
|
+
quoted.push_back(L'"');
|
|
426
|
+
backslashes = 0;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
quoted.append(backslashes, L'\\');
|
|
430
|
+
backslashes = 0;
|
|
431
|
+
quoted.push_back(character);
|
|
432
|
+
}
|
|
433
|
+
quoted.append(backslashes * 2, L'\\');
|
|
434
|
+
quoted.push_back(L'"');
|
|
435
|
+
return quoted;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
std::wstring command_line(int argc, wchar_t** argv, int first) {
|
|
439
|
+
std::wstring result;
|
|
440
|
+
for (int index = first; index < argc; ++index) {
|
|
441
|
+
if (!result.empty()) result.push_back(L' ');
|
|
442
|
+
result.append(quote_argument(argv[index]));
|
|
443
|
+
}
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
bool is_absolute_windows_path(const wchar_t* path) {
|
|
448
|
+
if (path == nullptr || path[0] == L'\0') return false;
|
|
449
|
+
if (path[0] == L'\\' && path[1] == L'\\') return true;
|
|
450
|
+
const wchar_t drive = path[0];
|
|
451
|
+
return ((drive >= L'A' && drive <= L'Z') || (drive >= L'a' && drive <= L'z')) &&
|
|
452
|
+
path[1] == L':' && (path[2] == L'\\' || path[2] == L'/');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
void terminate_suspended_child(HANDLE process) noexcept {
|
|
456
|
+
if (process == nullptr || process == INVALID_HANDLE_VALUE) return;
|
|
457
|
+
TerminateProcess(process, kGuardFailureExitCode);
|
|
458
|
+
WaitForSingleObject(process, 5'000);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
void await_hook_ready(HANDLE ready_event, HANDLE process) {
|
|
462
|
+
HANDLE handles[] = {ready_event, process};
|
|
463
|
+
const DWORD wait = WaitForMultipleObjects(
|
|
464
|
+
static_cast<DWORD>(std::size(handles)), handles, FALSE, 30'000);
|
|
465
|
+
if (wait == WAIT_OBJECT_0) return;
|
|
466
|
+
|
|
467
|
+
if (wait != WAIT_OBJECT_0 + 1) {
|
|
468
|
+
TerminateProcess(process, kGuardFailureExitCode);
|
|
469
|
+
WaitForSingleObject(process, 5'000);
|
|
470
|
+
}
|
|
471
|
+
const DWORD code = wait == WAIT_FAILED ? GetLastError() :
|
|
472
|
+
wait == WAIT_TIMEOUT ? ERROR_TIMEOUT : ERROR_DLL_INIT_FAILED;
|
|
473
|
+
fail("hook readiness handshake", code);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
std::string hook_library_path() {
|
|
477
|
+
std::vector<wchar_t> module_path(32768);
|
|
478
|
+
const DWORD length = GetModuleFileNameW(
|
|
479
|
+
nullptr, module_path.data(), static_cast<DWORD>(module_path.size()));
|
|
480
|
+
if (length == 0 || length >= module_path.size() - 1) {
|
|
481
|
+
fail("GetModuleFileNameW(guard)", length == 0 ? GetLastError() : ERROR_INSUFFICIENT_BUFFER);
|
|
482
|
+
}
|
|
483
|
+
std::wstring path(module_path.data(), length);
|
|
484
|
+
const size_t separator = path.find_last_of(L"\\/");
|
|
485
|
+
if (separator == std::wstring::npos) fail_invariant("guard executable has no parent directory");
|
|
486
|
+
path.resize(separator + 1);
|
|
487
|
+
path.append(L"msys-token-guard-hook.dll");
|
|
488
|
+
|
|
489
|
+
const DWORD attributes = GetFileAttributesW(path.c_str());
|
|
490
|
+
if (attributes == INVALID_FILE_ATTRIBUTES) fail("GetFileAttributesW(hook DLL)");
|
|
491
|
+
if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
|
|
492
|
+
throw GuardFailure("hook DLL path is a directory", ERROR_DIRECTORY);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
char ansi_path[MAX_PATH]{};
|
|
496
|
+
BOOL used_default = FALSE;
|
|
497
|
+
const int converted = WideCharToMultiByte(
|
|
498
|
+
CP_ACP,
|
|
499
|
+
WC_NO_BEST_FIT_CHARS,
|
|
500
|
+
path.c_str(),
|
|
501
|
+
-1,
|
|
502
|
+
ansi_path,
|
|
503
|
+
static_cast<int>(std::size(ansi_path)),
|
|
504
|
+
nullptr,
|
|
505
|
+
&used_default);
|
|
506
|
+
if (converted <= 0 || used_default != FALSE) {
|
|
507
|
+
fail("WideCharToMultiByte(hook DLL)", converted <= 0 ? GetLastError() : ERROR_NO_UNICODE_TRANSLATION);
|
|
508
|
+
}
|
|
509
|
+
return std::string(ansi_path);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
DWORD launch_guarded(int argc, wchar_t** argv) {
|
|
513
|
+
if (argc == 2 && std::wcscmp(argv[1], L"--normalize-server-dacl") == 0) {
|
|
514
|
+
return normalize_server_default_dacl();
|
|
515
|
+
}
|
|
516
|
+
if (argc == 2 && std::wcscmp(argv[1], L"--probe-current-token") == 0) {
|
|
517
|
+
auto token = open_process_token(GetCurrentProcess(), TOKEN_QUERY, "OpenProcessToken(probe)");
|
|
518
|
+
verify_token_invariants(token.get());
|
|
519
|
+
if (!adjust_default_is_denied(GetCurrentProcess())) {
|
|
520
|
+
fail_invariant("current token still grants TOKEN_ADJUST_DEFAULT");
|
|
521
|
+
}
|
|
522
|
+
std::fprintf(stdout, "token-adjust-default=denied\n");
|
|
523
|
+
return ERROR_SUCCESS;
|
|
524
|
+
}
|
|
525
|
+
if (argc < 3 || std::wcscmp(argv[1], L"--") != 0) {
|
|
526
|
+
std::fprintf(stderr, "usage: msys-token-guard.exe -- <absolute-program> [arguments...]\n");
|
|
527
|
+
return kUsageExitCode;
|
|
528
|
+
}
|
|
529
|
+
if (!is_absolute_windows_path(argv[2])) {
|
|
530
|
+
throw GuardFailure("target program path must be absolute", ERROR_INVALID_NAME);
|
|
531
|
+
}
|
|
532
|
+
const DWORD target_attributes = GetFileAttributesW(argv[2]);
|
|
533
|
+
if (target_attributes == INVALID_FILE_ATTRIBUTES) fail("GetFileAttributesW(target)");
|
|
534
|
+
if ((target_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
|
|
535
|
+
throw GuardFailure("target program is a directory", ERROR_DIRECTORY);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
auto guard_token = open_process_token(GetCurrentProcess(), TOKEN_QUERY, "OpenProcessToken(guard)");
|
|
539
|
+
const auto expected_sids = restricted_sid_set(guard_token.get());
|
|
540
|
+
verify_token_invariants(guard_token.get(), &expected_sids);
|
|
541
|
+
const std::string hook_path = hook_library_path();
|
|
542
|
+
|
|
543
|
+
std::wstring line = command_line(argc, argv, 2);
|
|
544
|
+
std::vector<wchar_t> mutable_line(line.begin(), line.end());
|
|
545
|
+
mutable_line.push_back(L'\0');
|
|
546
|
+
|
|
547
|
+
if (SetEnvironmentVariableW(L"CYGWIN_TESTING", L"1") == FALSE) {
|
|
548
|
+
fail("SetEnvironmentVariableW(CYGWIN_TESTING)");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
SECURITY_ATTRIBUTES event_security{};
|
|
552
|
+
event_security.nLength = sizeof(event_security);
|
|
553
|
+
event_security.bInheritHandle = TRUE;
|
|
554
|
+
UniqueHandle hook_ready(CreateEventW(&event_security, FALSE, FALSE, nullptr));
|
|
555
|
+
if (!hook_ready) fail("CreateEventW(hook readiness)");
|
|
556
|
+
|
|
557
|
+
wchar_t ready_handle[32]{};
|
|
558
|
+
const int ready_length = std::swprintf(
|
|
559
|
+
ready_handle,
|
|
560
|
+
std::size(ready_handle),
|
|
561
|
+
L"%llx",
|
|
562
|
+
static_cast<unsigned long long>(reinterpret_cast<uintptr_t>(hook_ready.get())));
|
|
563
|
+
if (ready_length <= 0 || static_cast<size_t>(ready_length) >= std::size(ready_handle)) {
|
|
564
|
+
fail_invariant("failed to serialize hook readiness handle");
|
|
565
|
+
}
|
|
566
|
+
if (SetEnvironmentVariableW(kReadyHandleEnvironment, ready_handle) == FALSE) {
|
|
567
|
+
fail("SetEnvironmentVariableW(hook readiness)");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
STARTUPINFOW startup{};
|
|
571
|
+
startup.cb = sizeof(startup);
|
|
572
|
+
startup.dwFlags = STARTF_USESTDHANDLES;
|
|
573
|
+
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
574
|
+
startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
575
|
+
startup.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
|
576
|
+
|
|
577
|
+
PROCESS_INFORMATION process_info{};
|
|
578
|
+
const BOOL created = DetourCreateProcessWithDllExW(
|
|
579
|
+
argv[2],
|
|
580
|
+
mutable_line.data(),
|
|
581
|
+
nullptr,
|
|
582
|
+
nullptr,
|
|
583
|
+
TRUE,
|
|
584
|
+
CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW,
|
|
585
|
+
nullptr,
|
|
586
|
+
nullptr,
|
|
587
|
+
&startup,
|
|
588
|
+
&process_info,
|
|
589
|
+
hook_path.c_str(),
|
|
590
|
+
CreateProcessW);
|
|
591
|
+
const DWORD create_error = created == FALSE ? GetLastError() : ERROR_SUCCESS;
|
|
592
|
+
if (SetEnvironmentVariableW(kReadyHandleEnvironment, nullptr) == FALSE) {
|
|
593
|
+
if (created != FALSE) terminate_suspended_child(process_info.hProcess);
|
|
594
|
+
fail("SetEnvironmentVariableW(clear hook readiness)");
|
|
595
|
+
}
|
|
596
|
+
if (created == FALSE) {
|
|
597
|
+
fail("DetourCreateProcessWithDllExW(target, suspended)", create_error);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
UniqueHandle child_process(process_info.hProcess);
|
|
601
|
+
UniqueHandle child_thread(process_info.hThread);
|
|
602
|
+
bool resumed = false;
|
|
603
|
+
try {
|
|
604
|
+
auto child_token = open_process_token(
|
|
605
|
+
child_process.get(), TOKEN_QUERY, "OpenProcessToken(child TOKEN_QUERY)");
|
|
606
|
+
verify_token_invariants(child_token.get(), &expected_sids);
|
|
607
|
+
grant_logon_sid_to_default_dacl(child_process.get());
|
|
608
|
+
deny_adjust_default(child_process.get(), child_token.get());
|
|
609
|
+
|
|
610
|
+
const DWORD previous_suspend_count = ResumeThread(child_thread.get());
|
|
611
|
+
if (previous_suspend_count == static_cast<DWORD>(-1)) fail("ResumeThread(target)");
|
|
612
|
+
if (previous_suspend_count != 1) fail_invariant("target primary thread had an unexpected suspend count");
|
|
613
|
+
resumed = true;
|
|
614
|
+
child_thread.reset();
|
|
615
|
+
await_hook_ready(hook_ready.get(), child_process.get());
|
|
616
|
+
hook_ready.reset();
|
|
617
|
+
|
|
618
|
+
const DWORD wait = WaitForSingleObject(child_process.get(), INFINITE);
|
|
619
|
+
if (wait != WAIT_OBJECT_0) {
|
|
620
|
+
const DWORD code = wait == WAIT_FAILED ? GetLastError() : ERROR_GEN_FAILURE;
|
|
621
|
+
TerminateProcess(child_process.get(), kGuardFailureExitCode);
|
|
622
|
+
WaitForSingleObject(child_process.get(), 5'000);
|
|
623
|
+
fail("WaitForSingleObject(target)", code);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
DWORD exit_code = 0;
|
|
627
|
+
if (GetExitCodeProcess(child_process.get(), &exit_code) == FALSE) {
|
|
628
|
+
fail("GetExitCodeProcess(target)");
|
|
629
|
+
}
|
|
630
|
+
if (exit_code == STILL_ACTIVE) fail_invariant("target remained active after signaling completion");
|
|
631
|
+
return exit_code;
|
|
632
|
+
} catch (...) {
|
|
633
|
+
if (!resumed) terminate_suspended_child(child_process.get());
|
|
634
|
+
throw;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// DSH launches the guard as a console app from a process that usually has no
|
|
639
|
+
// console of its own. In that case Windows allocates a brand-new console
|
|
640
|
+
// window, which flashes on screen for every restricted bash call. Hide only a
|
|
641
|
+
// console that belongs exclusively to this process; never hide a console we
|
|
642
|
+
// share with the user's terminal.
|
|
643
|
+
void hide_private_console_window() noexcept {
|
|
644
|
+
DWORD process_list[2]{};
|
|
645
|
+
const DWORD count = GetConsoleProcessList(process_list, 2);
|
|
646
|
+
if (count == 1) {
|
|
647
|
+
if (HWND console = GetConsoleWindow(); console != nullptr) {
|
|
648
|
+
ShowWindow(console, SW_HIDE);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
} // namespace
|
|
654
|
+
|
|
655
|
+
int wmain(int argc, wchar_t** argv) {
|
|
656
|
+
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
|
|
657
|
+
hide_private_console_window();
|
|
658
|
+
try {
|
|
659
|
+
return static_cast<int>(launch_guarded(argc, argv));
|
|
660
|
+
} catch (const GuardFailure& error) {
|
|
661
|
+
std::fprintf(
|
|
662
|
+
stderr,
|
|
663
|
+
"msys-token-guard: %s failed (Win32 %lu)\n",
|
|
664
|
+
error.what(),
|
|
665
|
+
static_cast<unsigned long>(error.code()));
|
|
666
|
+
return kGuardFailureExitCode;
|
|
667
|
+
} catch (const std::exception& error) {
|
|
668
|
+
std::fprintf(stderr, "msys-token-guard: unexpected failure: %s\n", error.what());
|
|
669
|
+
return kGuardFailureExitCode;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
8
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
9
|
+
the Software without restriction, including without limitation the rights to
|
|
10
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
11
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
12
|
+
so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|