1803 lines
70 KiB
C++
1803 lines
70 KiB
C++
// Native SCM boundary host. No Chromium/Electron changes; installation is
|
|
// separate.
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include "service-pins.h"
|
|
#include "wfp-topology.h"
|
|
#include <aclapi.h>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <bcrypt.h>
|
|
#include <cstdio>
|
|
#include <memory>
|
|
#include <sddl.h>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <tlhelp32.h>
|
|
#include <vector>
|
|
#include <windows.h>
|
|
#pragma comment(lib, "advapi32.lib")
|
|
#pragma comment(lib, "bcrypt.lib")
|
|
static constexpr wchar_t SERVICE_NAME[] = L"SafetyNetNetworkBroker";
|
|
static constexpr wchar_t PIPE_NAME[] = L"\\\\.\\pipe\\SafetyNet.Broker.v1";
|
|
struct Handle {
|
|
HANDLE h = nullptr;
|
|
explicit Handle(HANDLE v = nullptr) : h(v) {}
|
|
~Handle() {
|
|
if (h && h != INVALID_HANDLE_VALUE)
|
|
CloseHandle(h);
|
|
}
|
|
Handle(const Handle &) = delete;
|
|
Handle &operator=(const Handle &) = delete;
|
|
operator HANDLE() const { return h; }
|
|
};
|
|
static HANDLE stopEvent;
|
|
static SERVICE_STATUS_HANDLE statusHandle;
|
|
static bool hashFile(const std::wstring &file, const char *expected) {
|
|
Handle input(CreateFileW(file.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
|
|
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT,
|
|
nullptr));
|
|
if (input.h == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
FILE_ATTRIBUTE_TAG_INFO info{};
|
|
if (!GetFileInformationByHandleEx(input, FileAttributeTagInfo, &info,
|
|
sizeof(info)) ||
|
|
(info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT))
|
|
return false;
|
|
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
|
BCRYPT_HASH_HANDLE hash = nullptr;
|
|
bool good = false;
|
|
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr,
|
|
0) < 0)
|
|
return false;
|
|
if (BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0) >= 0) {
|
|
std::array<BYTE, 65536> buffer;
|
|
DWORD read = 0;
|
|
bool okay = true;
|
|
while (true) {
|
|
if (!ReadFile(input, buffer.data(), DWORD(buffer.size()), &read,
|
|
nullptr)) {
|
|
okay = false;
|
|
break;
|
|
}
|
|
if (!read)
|
|
break;
|
|
if (BCryptHashData(hash, buffer.data(), read, 0) < 0) {
|
|
okay = false;
|
|
break;
|
|
}
|
|
}
|
|
std::array<BYTE, 32> digest{};
|
|
if (okay && BCryptFinishHash(hash, digest.data(), 32, 0) >= 0) {
|
|
char hex[65]{};
|
|
for (size_t i = 0; i < 32; i++)
|
|
sprintf_s(hex + i * 2, 3, "%02x", digest[i]);
|
|
good = strcmp(hex, expected) == 0;
|
|
}
|
|
BCryptDestroyHash(hash);
|
|
}
|
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
return good;
|
|
}
|
|
static std::vector<BYTE> tokenInfo(HANDLE token, TOKEN_INFORMATION_CLASS kind) {
|
|
DWORD size = 0;
|
|
GetTokenInformation(token, kind, nullptr, 0, &size);
|
|
if (!size || size > 65536)
|
|
return {};
|
|
std::vector<BYTE> value(size);
|
|
if (!GetTokenInformation(token, kind, value.data(), size, &size))
|
|
return {};
|
|
return value;
|
|
}
|
|
static bool serviceIdentity(HANDLE process = GetCurrentProcess()) {
|
|
Handle token;
|
|
if (!OpenProcessToken(process, TOKEN_QUERY, &token.h))
|
|
return false;
|
|
auto groups = tokenInfo(token, TokenGroups);
|
|
if (groups.empty())
|
|
return false;
|
|
DWORD sidBytes = 0, domainBytes = 0;
|
|
SID_NAME_USE use;
|
|
LookupAccountNameW(nullptr, L"NT SERVICE\\SafetyNetNetworkBroker", nullptr,
|
|
&sidBytes, nullptr, &domainBytes, &use);
|
|
if (!sidBytes || sidBytes > 65536)
|
|
return false;
|
|
std::vector<BYTE> sid(sidBytes);
|
|
std::vector<wchar_t> domain(domainBytes);
|
|
if (!LookupAccountNameW(nullptr, L"NT SERVICE\\SafetyNetNetworkBroker",
|
|
sid.data(), &sidBytes, domain.data(), &domainBytes,
|
|
&use))
|
|
return false;
|
|
auto list = reinterpret_cast<TOKEN_GROUPS *>(groups.data());
|
|
for (DWORD i = 0; i < list->GroupCount; i++)
|
|
if (EqualSid(list->Groups[i].Sid, sid.data()) &&
|
|
(list->Groups[i].Attributes & SE_GROUP_ENABLED) &&
|
|
!(list->Groups[i].Attributes & SE_GROUP_USE_FOR_DENY_ONLY))
|
|
return true;
|
|
return false;
|
|
}
|
|
static std::vector<BYTE> serviceSid() {
|
|
DWORD size = 0, domainSize = 0;
|
|
SID_NAME_USE use;
|
|
LookupAccountNameW(nullptr, L"NT SERVICE\\SafetyNetNetworkBroker", nullptr,
|
|
&size, nullptr, &domainSize, &use);
|
|
if (!size || size > 65536 || domainSize > 32768)
|
|
return {};
|
|
std::vector<BYTE> sid(size);
|
|
std::vector<wchar_t> domain(domainSize);
|
|
if (!LookupAccountNameW(nullptr, L"NT SERVICE\\SafetyNetNetworkBroker",
|
|
sid.data(), &size, domain.data(), &domainSize, &use))
|
|
return {};
|
|
return sid;
|
|
}
|
|
static bool protectedPath(const std::wstring &file) {
|
|
// Resources may be readable by employees, but only SYSTEM/Administrators may
|
|
// modify code. Unknown write trustees fail closed; inheritable-only ACEs are
|
|
// checked on their descendants rather than treated as effective on the
|
|
// parent.
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
PACL dacl = nullptr;
|
|
PSID owner = nullptr;
|
|
DWORD code = GetNamedSecurityInfoW(
|
|
file.c_str(), SE_FILE_OBJECT,
|
|
DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION, &owner, nullptr,
|
|
&dacl, nullptr, &descriptor);
|
|
if (code || !dacl)
|
|
return false;
|
|
BYTE system[SECURITY_MAX_SID_SIZE], admins[SECURITY_MAX_SID_SIZE];
|
|
DWORD ss = sizeof(system), as = sizeof(admins);
|
|
CreateWellKnownSid(WinLocalSystemSid, nullptr, system, &ss);
|
|
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &as);
|
|
bool good = owner && (EqualSid(owner, system) || EqualSid(owner, admins));
|
|
for (DWORD i = 0; i < dacl->AceCount; i++) {
|
|
void *raw = nullptr;
|
|
if (!GetAce(dacl, i, &raw)) {
|
|
good = false;
|
|
break;
|
|
}
|
|
auto header = static_cast<ACE_HEADER *>(raw);
|
|
if (header->AceFlags & INHERIT_ONLY_ACE)
|
|
continue;
|
|
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE) {
|
|
auto ace = static_cast<ACCESS_ALLOWED_ACE *>(raw);
|
|
DWORD write = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA |
|
|
FILE_WRITE_ATTRIBUTES | DELETE | WRITE_DAC | WRITE_OWNER |
|
|
FILE_DELETE_CHILD | GENERIC_WRITE | GENERIC_ALL;
|
|
if ((ace->Mask & write) && !EqualSid(&ace->SidStart, system) &&
|
|
!EqualSid(&ace->SidStart, admins)) {
|
|
good = false;
|
|
break;
|
|
}
|
|
} else if (header->AceType != ACCESS_DENIED_ACE_TYPE) {
|
|
good = false;
|
|
break;
|
|
}
|
|
}
|
|
LocalFree(descriptor);
|
|
return good;
|
|
}
|
|
#include "acceptance-state.h"
|
|
#include "management-credential.h"
|
|
#include "signed-manifest.h"
|
|
struct TrustFloor {
|
|
ULONGLONG release = 0, deployment = 0;
|
|
std::string releaseDigest, deploymentDigest, stateCanonical;
|
|
};
|
|
static TrustFloor trustFloor() {
|
|
HKEY key = nullptr;
|
|
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\SafetyNet\\Trust", 0,
|
|
KEY_READ | READ_CONTROL | KEY_WOW64_64KEY,
|
|
&key) != ERROR_SUCCESS)
|
|
throw std::runtime_error("fixed-trust-unavailable");
|
|
// A writable counter store would turn signature checking into rollback.
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
PACL dacl = nullptr;
|
|
PSID owner = nullptr;
|
|
DWORD status =
|
|
GetSecurityInfo(key, SE_REGISTRY_KEY,
|
|
DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
|
|
&owner, nullptr, &dacl, nullptr, &descriptor);
|
|
BYTE system[SECURITY_MAX_SID_SIZE], admins[SECURITY_MAX_SID_SIZE];
|
|
DWORD ss = sizeof(system), as = sizeof(admins);
|
|
CreateWellKnownSid(WinLocalSystemSid, nullptr, system, &ss);
|
|
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &as);
|
|
bool valid = !status && dacl && owner &&
|
|
(EqualSid(owner, system) || EqualSid(owner, admins));
|
|
auto service = serviceSid();
|
|
if (valid)
|
|
for (DWORD i = 0; i < dacl->AceCount; i++) {
|
|
void *raw = nullptr;
|
|
if (!GetAce(dacl, i, &raw)) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
auto header = static_cast<ACE_HEADER *>(raw);
|
|
if (header->AceFlags & INHERIT_ONLY_ACE)
|
|
continue;
|
|
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE) {
|
|
auto ace = static_cast<ACCESS_ALLOWED_ACE *>(raw);
|
|
if ((ace->Mask &
|
|
(KEY_SET_VALUE | KEY_CREATE_SUB_KEY | DELETE | WRITE_DAC |
|
|
WRITE_OWNER | GENERIC_WRITE | GENERIC_ALL)) &&
|
|
!EqualSid(&ace->SidStart, system) &&
|
|
!EqualSid(&ace->SidStart, admins) &&
|
|
!(service.size() && EqualSid(&ace->SidStart, service.data()) &&
|
|
!(ace->Mask &
|
|
~(KEY_QUERY_VALUE | KEY_SET_VALUE | READ_CONTROL | KEY_NOTIFY |
|
|
KEY_ENUMERATE_SUB_KEYS | SYNCHRONIZE)))) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
} else if (header->AceType != ACCESS_DENIED_ACE_TYPE) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
if (descriptor)
|
|
LocalFree(descriptor);
|
|
DWORD type = 0, size = 0;
|
|
bool okay = valid &&
|
|
RegQueryValueExW(key, L"AcceptanceState", nullptr, &type, nullptr,
|
|
&size) == ERROR_SUCCESS &&
|
|
type == REG_BINARY && size > 0 && size <= 2097152;
|
|
std::string canonical(okay ? size : 0, '\0');
|
|
if (okay)
|
|
okay = RegQueryValueExW(key, L"AcceptanceState", nullptr, &type,
|
|
reinterpret_cast<BYTE *>(canonical.data()),
|
|
&size) == ERROR_SUCCESS &&
|
|
type == REG_BINARY && size == canonical.size();
|
|
RegCloseKey(key);
|
|
if (!okay)
|
|
throw std::runtime_error("fixed-trust-unavailable");
|
|
auto state = acceptance_contract::validate(canonical);
|
|
TrustFloor floor;
|
|
floor.release = release_contract::number(
|
|
state.object.at("release").object.at("minimumSequence"));
|
|
floor.deployment = release_contract::number(
|
|
state.object.at("deployment").object.at("minimumSequence"));
|
|
floor.releaseDigest = release_contract::str(
|
|
state.object.at("release").object.at("acceptedDigest"));
|
|
floor.deploymentDigest = release_contract::str(
|
|
state.object.at("deployment").object.at("acceptedDigest"));
|
|
floor.stateCanonical = canonical;
|
|
return floor;
|
|
}
|
|
static release_contract::Release trustedRelease(const std::wstring &root) {
|
|
auto floor = trustFloor();
|
|
auto release = release_contract::release(
|
|
root, release_contract::unhex(RELEASE_ANCHOR_XY), floor.release,
|
|
floor.releaseDigest);
|
|
auto state = acceptance_contract::validate(floor.stateCanonical);
|
|
if (release.tenant !=
|
|
release_contract::str(state.object.at("deployment").object.at("tenant")))
|
|
throw std::runtime_error("fixed-trust-unavailable");
|
|
return release;
|
|
}
|
|
static std::string currentSid(HANDLE process) {
|
|
Handle token;
|
|
if (!OpenProcessToken(process, TOKEN_QUERY, &token.h))
|
|
throw std::runtime_error("fixed-peer-invalid");
|
|
auto value = tokenInfo(token, TokenUser);
|
|
if (value.empty())
|
|
throw std::runtime_error("fixed-peer-invalid");
|
|
LPSTR text = nullptr;
|
|
if (!ConvertSidToStringSidA(
|
|
reinterpret_cast<TOKEN_USER *>(value.data())->User.Sid, &text))
|
|
throw std::runtime_error("fixed-peer-invalid");
|
|
std::string result = text;
|
|
LocalFree(text);
|
|
return result;
|
|
}
|
|
static Handle *parentProcess(HANDLE process) {
|
|
// Called only with a retained actual process handle; no PID supplied by IPC.
|
|
DWORD pid = GetProcessId(process);
|
|
Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
|
|
PROCESSENTRY32W item{};
|
|
item.dwSize = sizeof(item);
|
|
if (snapshot.h == INVALID_HANDLE_VALUE || !Process32FirstW(snapshot, &item))
|
|
return nullptr;
|
|
do {
|
|
if (item.th32ProcessID == pid) {
|
|
HANDLE parent =
|
|
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE,
|
|
item.th32ParentProcessID);
|
|
if (!parent)
|
|
return nullptr;
|
|
FILETIME childCreated{}, parentCreated{}, exit{}, kernel{}, user{};
|
|
if (!GetProcessTimes(process, &childCreated, &exit, &kernel, &user) ||
|
|
!GetProcessTimes(parent, &parentCreated, &exit, &kernel, &user) ||
|
|
CompareFileTime(&parentCreated, &childCreated) > 0) {
|
|
CloseHandle(parent);
|
|
return nullptr;
|
|
}
|
|
return new Handle(parent);
|
|
}
|
|
} while (Process32NextW(snapshot, &item));
|
|
return nullptr;
|
|
}
|
|
static bool verifiedImage(HANDLE process, const std::wstring &root,
|
|
const char *relative,
|
|
const release_contract::Release &release) {
|
|
std::array<wchar_t, 32768> image{};
|
|
DWORD size = DWORD(image.size());
|
|
std::wstring required =
|
|
root + L"\\" + std::wstring(relative, relative + strlen(relative));
|
|
auto found = release.hashes.find(relative);
|
|
if (found == release.hashes.end() ||
|
|
WaitForSingleObject(process, 0) != WAIT_TIMEOUT ||
|
|
!QueryFullProcessImageNameW(process, 0, image.data(), &size) ||
|
|
CompareStringOrdinal(image.data(), int(size), required.c_str(), -1,
|
|
TRUE) != CSTR_EQUAL ||
|
|
!protectedPath(required) || !protectedPath(root))
|
|
return false;
|
|
return hashFile(required, found->second.c_str());
|
|
}
|
|
static bool sameUserSession(HANDLE left, HANDLE right, bool service = false) {
|
|
Handle lt, rt;
|
|
if (!OpenProcessToken(left, TOKEN_QUERY, <.h) ||
|
|
!OpenProcessToken(right, TOKEN_QUERY, &rt.h))
|
|
return false;
|
|
auto lu = tokenInfo(lt, TokenUser), ru = tokenInfo(rt, TokenUser),
|
|
ls = tokenInfo(lt, TokenSessionId), rs = tokenInfo(rt, TokenSessionId),
|
|
lc = tokenInfo(lt, TokenIsAppContainer),
|
|
rc = tokenInfo(rt, TokenIsAppContainer),
|
|
li = tokenInfo(lt, TokenIntegrityLevel),
|
|
ri = tokenInfo(rt, TokenIntegrityLevel);
|
|
if (lu.empty() || ru.empty() || ls.empty() || rs.empty() || lc.empty() ||
|
|
rc.empty() || li.empty() || ri.empty() ||
|
|
*reinterpret_cast<DWORD *>(lc.data()) ||
|
|
*reinterpret_cast<DWORD *>(rc.data()))
|
|
return false;
|
|
for (const auto *item : {&li, &ri}) {
|
|
auto label = reinterpret_cast<const TOKEN_MANDATORY_LABEL *>(item->data());
|
|
DWORD level = *GetSidSubAuthority(
|
|
label->Label.Sid, *GetSidSubAuthorityCount(label->Label.Sid) - 1);
|
|
if (level < SECURITY_MANDATORY_MEDIUM_RID ||
|
|
level > DWORD(service ? SECURITY_MANDATORY_SYSTEM_RID
|
|
: SECURITY_MANDATORY_HIGH_RID))
|
|
return false;
|
|
}
|
|
return EqualSid(reinterpret_cast<TOKEN_USER *>(lu.data())->User.Sid,
|
|
reinterpret_cast<TOKEN_USER *>(ru.data())->User.Sid) &&
|
|
*reinterpret_cast<DWORD *>(ls.data()) ==
|
|
*reinterpret_cast<DWORD *>(rs.data());
|
|
}
|
|
static bool pipeClient(HANDLE pipe, const std::wstring &root,
|
|
HANDLE *retained = nullptr) {
|
|
ULONG pid = 0;
|
|
if (!GetNamedPipeClientProcessId(pipe, &pid) || !pid)
|
|
return false;
|
|
Handle process(
|
|
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pid));
|
|
if (!process.h || WaitForSingleObject(process, 0) != WAIT_TIMEOUT)
|
|
return false;
|
|
try {
|
|
auto release = trustedRelease(root);
|
|
std::unique_ptr<Handle> parent(parentProcess(process));
|
|
if (!parent ||
|
|
!verifiedImage(process, root, "safetynet-service-host.exe", release) ||
|
|
!verifiedImage(*parent, root, "SafetyNet.exe", release) ||
|
|
!sameUserSession(process, *parent))
|
|
return false;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
Handle actual;
|
|
if (!OpenProcessToken(process, TOKEN_QUERY, &actual.h))
|
|
return false;
|
|
auto user = tokenInfo(actual, TokenUser),
|
|
session = tokenInfo(actual, TokenSessionId),
|
|
container = tokenInfo(actual, TokenIsAppContainer),
|
|
integrity = tokenInfo(actual, TokenIntegrityLevel);
|
|
if (user.empty() || session.empty() || container.empty() ||
|
|
integrity.empty() || *reinterpret_cast<DWORD *>(container.data()))
|
|
return false;
|
|
auto label = reinterpret_cast<TOKEN_MANDATORY_LABEL *>(integrity.data());
|
|
DWORD level = *GetSidSubAuthority(
|
|
label->Label.Sid, *GetSidSubAuthorityCount(label->Label.Sid) - 1);
|
|
if (level < SECURITY_MANDATORY_MEDIUM_RID ||
|
|
level > SECURITY_MANDATORY_HIGH_RID)
|
|
return false;
|
|
if (!ImpersonateNamedPipeClient(pipe))
|
|
return false;
|
|
Handle peer;
|
|
bool opened =
|
|
OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &peer.h) != FALSE;
|
|
bool reverted = RevertToSelf() != FALSE;
|
|
if (!reverted) {
|
|
TerminateProcess(GetCurrentProcess(), ERROR_ACCESS_DENIED);
|
|
return false;
|
|
}
|
|
if (!opened)
|
|
return false;
|
|
auto peerUser = tokenInfo(peer, TokenUser),
|
|
peerSession = tokenInfo(peer, TokenSessionId);
|
|
if (peerUser.empty() || peerSession.empty())
|
|
return false;
|
|
bool authenticated =
|
|
EqualSid(reinterpret_cast<TOKEN_USER *>(user.data())->User.Sid,
|
|
reinterpret_cast<TOKEN_USER *>(peerUser.data())->User.Sid) &&
|
|
*reinterpret_cast<DWORD *>(session.data()) ==
|
|
*reinterpret_cast<DWORD *>(peerSession.data()) &&
|
|
WaitForSingleObject(process, 0) == WAIT_TIMEOUT;
|
|
if (authenticated && retained) {
|
|
*retained = process.h;
|
|
process.h = nullptr;
|
|
}
|
|
return authenticated;
|
|
}
|
|
static bool transfer(HANDLE pipe, bool write, void *bytes, DWORD length,
|
|
DWORD timeout, ULONGLONG *firstByteAt = nullptr) {
|
|
auto cursor = static_cast<BYTE *>(bytes);
|
|
ULONGLONG started = GetTickCount64();
|
|
while (length) {
|
|
Handle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
OVERLAPPED ov{};
|
|
ov.hEvent = event;
|
|
DWORD done = 0;
|
|
BOOL result = write ? WriteFile(pipe, cursor, length, &done, &ov)
|
|
: ReadFile(pipe, cursor, length, &done, &ov);
|
|
if (!result) {
|
|
if (GetLastError() != ERROR_IO_PENDING)
|
|
return false;
|
|
HANDLE waits[] = {event.h, stopEvent};
|
|
ULONGLONG elapsed = GetTickCount64() - started;
|
|
DWORD remaining =
|
|
timeout == INFINITE
|
|
? INFINITE
|
|
: (elapsed >= timeout ? 0 : timeout - DWORD(elapsed));
|
|
DWORD status = WaitForMultipleObjects(2, waits, FALSE, remaining);
|
|
if (status != WAIT_OBJECT_0) {
|
|
CancelIoEx(pipe, &ov);
|
|
WaitForSingleObject(event, INFINITE);
|
|
return false;
|
|
}
|
|
if (!GetOverlappedResult(pipe, &ov, &done, FALSE))
|
|
return false;
|
|
}
|
|
if (!done)
|
|
return false;
|
|
if (firstByteAt && !*firstByteAt)
|
|
*firstByteAt = GetTickCount64();
|
|
if (timeout == INFINITE && done < length) {
|
|
timeout = 15000;
|
|
started = GetTickCount64();
|
|
}
|
|
cursor += done;
|
|
length -= done;
|
|
}
|
|
return true;
|
|
}
|
|
static bool frame(HANDLE pipe, bool write, std::vector<BYTE> &value,
|
|
DWORD maximum, DWORD headerTimeout = 15000) {
|
|
ULONGLONG started = GetTickCount64();
|
|
ULONGLONG firstByteAt = 0;
|
|
DWORD size = write ? DWORD(value.size()) : 0;
|
|
if (!transfer(pipe, write, &size, 4, headerTimeout, &firstByteAt) || !size ||
|
|
size > maximum)
|
|
return false;
|
|
if (headerTimeout == INFINITE)
|
|
started = firstByteAt;
|
|
DWORD budget = headerTimeout == INFINITE ? 15000 : headerTimeout;
|
|
ULONGLONG elapsed = GetTickCount64() - started;
|
|
if (elapsed >= budget)
|
|
return false;
|
|
if (!write)
|
|
value.resize(size);
|
|
return transfer(pipe, write, value.data(), size, budget - DWORD(elapsed));
|
|
}
|
|
static std::wstring rootPath() {
|
|
std::array<wchar_t, 32768> image{};
|
|
DWORD size = GetModuleFileNameW(nullptr, image.data(), DWORD(image.size()));
|
|
if (!size || size >= image.size())
|
|
return {};
|
|
std::wstring root(image.data(), size);
|
|
return root.substr(0, root.find_last_of(L'\\'));
|
|
}
|
|
static bool resources(const std::wstring &root) {
|
|
try {
|
|
if (root.empty() || !protectedPath(root) ||
|
|
!protectedPath(root + L"\\release-envelope.json") ||
|
|
!protectedPath(root + L"\\deployment-envelope.json"))
|
|
return false;
|
|
auto release = trustedRelease(root);
|
|
for (const auto &pin : release.hashes) {
|
|
std::wstring relative(pin.first.begin(), pin.first.end());
|
|
std::replace(relative.begin(), relative.end(), L'/', L'\\');
|
|
std::wstring file = root + std::wstring(1, wchar_t(92)) + relative,
|
|
ancestor = file;
|
|
while (ancestor.size() > root.size()) {
|
|
if (GetFileAttributesW(ancestor.c_str()) &
|
|
FILE_ATTRIBUTE_REPARSE_POINT ||
|
|
!protectedPath(ancestor))
|
|
return false;
|
|
ancestor = ancestor.substr(0, ancestor.find_last_of(L'\\'));
|
|
}
|
|
if (!hashFile(file, pin.second.c_str()))
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
static std::wstring environment(const std::string &sid) {
|
|
std::wstring result;
|
|
for (const wchar_t *name : {L"SystemRoot", L"WINDIR", L"TEMP", L"TMP",
|
|
L"USERPROFILE", L"LOCALAPPDATA", L"APPDATA"}) {
|
|
DWORD size = GetEnvironmentVariableW(name, nullptr, 0);
|
|
if (size && size < 32768) {
|
|
std::vector<wchar_t> value(size);
|
|
GetEnvironmentVariableW(name, value.data(), size);
|
|
result += name;
|
|
result += L'=';
|
|
result += value.data();
|
|
result += L'\0';
|
|
}
|
|
}
|
|
result += L"SAFETYNET_SERVICE_CLIENT_SID=";
|
|
result += std::wstring(sid.begin(), sid.end());
|
|
result += L'\0';
|
|
result += L'\0';
|
|
return result;
|
|
}
|
|
static bool writeChildFrame(HANDLE pipe, const std::vector<BYTE> &value,
|
|
std::atomic<ULONGLONG> &deadline,
|
|
DWORD budget = 15000) {
|
|
deadline = GetTickCount64() + budget;
|
|
DWORD size = DWORD(value.size());
|
|
auto exact = [&](const BYTE *bytes, DWORD length) {
|
|
while (length) {
|
|
DWORD done = 0;
|
|
if (!WriteFile(pipe, bytes, length, &done, nullptr) || !done)
|
|
return false;
|
|
bytes += done;
|
|
length -= done;
|
|
}
|
|
return true;
|
|
};
|
|
if (!exact(reinterpret_cast<const BYTE *>(&size), 4) ||
|
|
!exact(value.data(), size))
|
|
return false;
|
|
deadline = 0;
|
|
return true;
|
|
}
|
|
static bool verifiedServer(HANDLE pipe, const std::wstring &root,
|
|
const release_contract::Release &release,
|
|
HANDLE *retained,
|
|
const char *role = "safetynet-service-host.exe") {
|
|
ULONG pid = 0;
|
|
if (!GetNamedPipeServerProcessId(pipe, &pid) || !pid)
|
|
return false;
|
|
Handle process(
|
|
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pid));
|
|
if (!process.h || !verifiedImage(process, root, role, release) ||
|
|
!serviceIdentity(process))
|
|
return false;
|
|
*retained = process.h;
|
|
process.h = nullptr;
|
|
return true;
|
|
}
|
|
static bool boundedStdioWrite(const std::string &text);
|
|
static int topologySnapshot() {
|
|
try {
|
|
const auto root = rootPath();
|
|
if (!serviceIdentity())
|
|
return ERROR_ACCESS_DENIED;
|
|
auto release = trustedRelease(root);
|
|
std::unique_ptr<Handle> parent(parentProcess(GetCurrentProcess()));
|
|
if (!parent || !serviceIdentity(*parent) ||
|
|
!verifiedImage(GetCurrentProcess(), root, "safetynet-service-host.exe",
|
|
release) ||
|
|
!verifiedImage(*parent, root, "resources/broker/node.exe", release) ||
|
|
!sameUserSession(GetCurrentProcess(), *parent, true))
|
|
return ERROR_ACCESS_DENIED;
|
|
DWORD userLength =
|
|
GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", nullptr, 0);
|
|
if (!userLength || userLength > 256)
|
|
return ERROR_ACCESS_DENIED;
|
|
std::string userSid(userLength, '\0');
|
|
if (!GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", userSid.data(),
|
|
userLength))
|
|
return ERROR_ACCESS_DENIED;
|
|
userSid.resize(userLength - 1);
|
|
auto assignmentFloor = trustFloor();
|
|
release_contract::deployment(root, release, userSid,
|
|
assignmentFloor.deployment,
|
|
assignmentFloor.deploymentDigest);
|
|
auto sid = serviceSid();
|
|
LPWSTR text = nullptr;
|
|
if (sid.empty() || !ConvertSidToStringSidW(sid.data(), &text))
|
|
return ERROR_ACCESS_DENIED;
|
|
std::wstring serviceText = text;
|
|
LocalFree(text);
|
|
auto result = topology_contract::inspect(root, serviceText);
|
|
const std::string json =
|
|
"{\"version\":1,\"brokerPort\":43120,\"accessibleFilters\":" +
|
|
std::to_string(result.accessibleFilters) +
|
|
",\"ownedFilters\":" + std::to_string(result.ownedFilters) +
|
|
",\"ownedVerified\":" + (result.ownedVerified ? "true" : "false") +
|
|
",\"collision\":" + (result.collision ? "true" : "false") +
|
|
",\"bfeRunning\":" + (result.bfeRunning ? "true" : "false") +
|
|
",\"allObjectsReadable\":false,\"code\":" +
|
|
std::to_string(result.code) + "}";
|
|
return boundedStdioWrite(json) ? 0 : ERROR_ACCESS_DENIED;
|
|
} catch (...) {
|
|
return ERROR_ACCESS_DENIED;
|
|
}
|
|
}
|
|
static int trustSnapshot() {
|
|
try {
|
|
const auto root = rootPath();
|
|
if (!serviceIdentity())
|
|
return ERROR_ACCESS_DENIED;
|
|
auto release = trustedRelease(root);
|
|
std::unique_ptr<Handle> parent(parentProcess(GetCurrentProcess()));
|
|
if (!parent || !serviceIdentity(*parent) ||
|
|
!verifiedImage(GetCurrentProcess(), root, "safetynet-service-host.exe",
|
|
release) ||
|
|
!verifiedImage(*parent, root, "resources/broker/node.exe", release) ||
|
|
!sameUserSession(GetCurrentProcess(), *parent, true))
|
|
return ERROR_ACCESS_DENIED;
|
|
DWORD length =
|
|
GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", nullptr, 0);
|
|
if (!length || length > 256)
|
|
return ERROR_ACCESS_DENIED;
|
|
std::string sid(length, '\0');
|
|
if (!GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", sid.data(),
|
|
length))
|
|
return ERROR_ACCESS_DENIED;
|
|
sid.resize(length - 1);
|
|
auto floor = trustFloor();
|
|
release_contract::deployment(root, release, sid, floor.deployment,
|
|
floor.deploymentDigest);
|
|
auto xy = release_contract::unhex(RELEASE_ANCHOR_XY);
|
|
auto der = release_contract::unhex(
|
|
"3059301306072a8648ce3d020106082a8648ce3d03010703420004");
|
|
der.insert(der.end(), xy.begin(), xy.end());
|
|
std::string encoded = release_contract::base64url(der);
|
|
std::replace(encoded.begin(), encoded.end(), '-', '+');
|
|
std::replace(encoded.begin(), encoded.end(), '_', '/');
|
|
while (encoded.size() % 4)
|
|
encoded += '=';
|
|
std::string anchor = "-----BEGIN PUBLIC KEY-----\n";
|
|
for (size_t offset = 0; offset < encoded.size(); offset += 64)
|
|
anchor += encoded.substr(offset, 64) + '\n';
|
|
anchor += "-----END PUBLIC KEY-----\n";
|
|
// All values originate in immutable code/protected Windows state. No
|
|
// registry selector, key, SID or path is accepted from command arguments.
|
|
std::string json =
|
|
"{\"acceptanceState\":" + floor.stateCanonical +
|
|
",\"authenticatedSid\":" + release_contract::escaped(sid) +
|
|
",\"releaseAnchor\":" + release_contract::escaped(anchor) +
|
|
",\"version\":1}";
|
|
return boundedStdioWrite(json) ? 0 : ERROR_ACCESS_DENIED;
|
|
} catch (...) {
|
|
return ERROR_ACCESS_DENIED;
|
|
}
|
|
}
|
|
static bool readStdioFrame(HANDLE input, std::vector<BYTE> &value,
|
|
DWORD maximum, std::atomic<ULONGLONG> &deadline) {
|
|
DWORD size = 0, done = 0;
|
|
if (!ReadFile(input, &size, 4, &done, nullptr) || !done)
|
|
return false;
|
|
deadline = GetTickCount64() + 15000;
|
|
DWORD offset = done;
|
|
while (offset < 4) {
|
|
if (!ReadFile(input, reinterpret_cast<BYTE *>(&size) + offset, 4 - offset,
|
|
&done, nullptr) ||
|
|
!done)
|
|
return false;
|
|
offset += done;
|
|
}
|
|
if (!size || size > maximum)
|
|
return false;
|
|
value.resize(size);
|
|
offset = 0;
|
|
while (offset < size) {
|
|
if (!ReadFile(input, value.data() + offset, size - offset, &done,
|
|
nullptr) ||
|
|
!done)
|
|
return false;
|
|
offset += done;
|
|
}
|
|
deadline = 0;
|
|
return true;
|
|
}
|
|
static int clientStdio() {
|
|
try {
|
|
const auto root = rootPath();
|
|
auto release = trustedRelease(root);
|
|
std::unique_ptr<Handle> parent(parentProcess(GetCurrentProcess()));
|
|
if (!parent ||
|
|
!verifiedImage(GetCurrentProcess(), root, "safetynet-service-host.exe",
|
|
release) ||
|
|
!verifiedImage(*parent, root, "SafetyNet.exe", release) ||
|
|
!sameUserSession(GetCurrentProcess(), *parent))
|
|
return ERROR_ACCESS_DENIED;
|
|
auto floor = trustFloor();
|
|
release_contract::deployment(root, release, currentSid(GetCurrentProcess()),
|
|
floor.deployment, floor.deploymentDigest);
|
|
HANDLE input = GetStdHandle(STD_INPUT_HANDLE),
|
|
output = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
if (GetFileType(input) != FILE_TYPE_PIPE ||
|
|
GetFileType(output) != FILE_TYPE_PIPE)
|
|
return ERROR_ACCESS_DENIED;
|
|
// Connect exactly once, then authenticate THIS handle's server. No probe
|
|
// and reconnect, retry, alternate endpoint, service startup or command
|
|
// selector.
|
|
Handle pipe(CreateFileW(PIPE_NAME, GENERIC_READ | GENERIC_WRITE, 0, nullptr,
|
|
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr));
|
|
if (pipe.h == INVALID_HANDLE_VALUE)
|
|
return ERROR_ACCESS_DENIED;
|
|
Handle server;
|
|
if (!verifiedServer(pipe, root, release, &server.h))
|
|
return ERROR_ACCESS_DENIED;
|
|
Handle stop(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
stopEvent = stop;
|
|
Handle reader;
|
|
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
|
|
GetCurrentProcess(), &reader.h, THREAD_TERMINATE,
|
|
FALSE, 0))
|
|
return ERROR_ACCESS_DENIED;
|
|
std::atomic<bool> good{true};
|
|
std::atomic<ULONGLONG> readDeadline{0}, writeDeadline{0};
|
|
std::thread outputPump([&] {
|
|
while (good) {
|
|
std::vector<BYTE> value;
|
|
if (!frame(pipe, false, value, 65536, INFINITE) ||
|
|
!writeChildFrame(output, value, writeDeadline)) {
|
|
good = false;
|
|
break;
|
|
}
|
|
}
|
|
CancelSynchronousIo(reader);
|
|
SetEvent(stopEvent);
|
|
});
|
|
std::thread watcher([&] {
|
|
while (good) {
|
|
if (WaitForSingleObject(stopEvent, 50) == WAIT_OBJECT_0 ||
|
|
WaitForSingleObject(*parent, 0) != WAIT_TIMEOUT ||
|
|
WaitForSingleObject(server, 0) != WAIT_TIMEOUT ||
|
|
(readDeadline.load() && GetTickCount64() >= readDeadline.load()) ||
|
|
(writeDeadline.load() &&
|
|
GetTickCount64() >= writeDeadline.load())) {
|
|
good = false;
|
|
SetEvent(stopEvent);
|
|
CancelSynchronousIo(reader);
|
|
CancelSynchronousIo(outputPump.native_handle());
|
|
CancelIoEx(pipe, nullptr);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
bool first = true;
|
|
while (good) {
|
|
std::vector<BYTE> value;
|
|
if (!readStdioFrame(input, value, first ? 2097152 : 65536,
|
|
readDeadline) ||
|
|
!frame(pipe, true, value, first ? 2097152 : 65536))
|
|
break;
|
|
first = false;
|
|
}
|
|
good = false;
|
|
SetEvent(stopEvent);
|
|
CancelIoEx(pipe, nullptr);
|
|
CancelSynchronousIo(outputPump.native_handle());
|
|
outputPump.join();
|
|
watcher.join();
|
|
return 0;
|
|
} catch (...) {
|
|
return ERROR_ACCESS_DENIED;
|
|
}
|
|
}
|
|
static bool boundedStdioRead(HANDLE input, std::vector<BYTE> &value,
|
|
DWORD maximum) {
|
|
if (GetFileType(input) != FILE_TYPE_PIPE)
|
|
return false;
|
|
Handle ownThread;
|
|
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
|
|
GetCurrentProcess(), &ownThread.h, THREAD_TERMINATE,
|
|
FALSE, 0))
|
|
return false;
|
|
std::atomic<bool> finished{false};
|
|
std::atomic<ULONGLONG> deadline{GetTickCount64() + 15000};
|
|
const ULONGLONG expires = GetTickCount64() + 15000;
|
|
std::thread watchdog([&] {
|
|
while (!finished) {
|
|
if (GetTickCount64() >= expires) {
|
|
CancelSynchronousIo(ownThread);
|
|
break;
|
|
}
|
|
Sleep(10);
|
|
}
|
|
});
|
|
bool okay = readStdioFrame(input, value, maximum, deadline);
|
|
finished = true;
|
|
watchdog.join();
|
|
return okay;
|
|
}
|
|
static bool boundedStdioWrite(const std::string &text) {
|
|
HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
if (GetFileType(output) != FILE_TYPE_PIPE || text.size() > 2097152)
|
|
return false;
|
|
Handle ownThread;
|
|
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
|
|
GetCurrentProcess(), &ownThread.h, THREAD_TERMINATE,
|
|
FALSE, 0))
|
|
return false;
|
|
std::atomic<bool> finished{false};
|
|
std::atomic<ULONGLONG> deadline{0};
|
|
const ULONGLONG expires = GetTickCount64() + 15000;
|
|
std::thread watchdog([&] {
|
|
while (!finished) {
|
|
if (GetTickCount64() >= expires) {
|
|
CancelSynchronousIo(ownThread);
|
|
break;
|
|
}
|
|
Sleep(10);
|
|
}
|
|
});
|
|
std::vector<BYTE> bytes(text.begin(), text.end());
|
|
bool okay = writeChildFrame(output, bytes, deadline);
|
|
finished = true;
|
|
watchdog.join();
|
|
if (!bytes.empty())
|
|
SecureZeroMemory(bytes.data(), bytes.size());
|
|
return okay;
|
|
}
|
|
static bool ownedAcceptanceMutex(HANDLE mutex,
|
|
const std::vector<BYTE> &service) {
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
PACL dacl = nullptr;
|
|
PSID owner = nullptr;
|
|
DWORD result =
|
|
GetSecurityInfo(mutex, SE_KERNEL_OBJECT,
|
|
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
|
&owner, nullptr, &dacl, nullptr, &descriptor);
|
|
BYTE system[SECURITY_MAX_SID_SIZE], admins[SECURITY_MAX_SID_SIZE],
|
|
localService[SECURITY_MAX_SID_SIZE];
|
|
DWORD ss = sizeof(system), as = sizeof(admins), ls = sizeof(localService);
|
|
CreateWellKnownSid(WinLocalSystemSid, nullptr, system, &ss);
|
|
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &as);
|
|
CreateWellKnownSid(WinLocalServiceSid, nullptr, localService, &ls);
|
|
PSID ownerRights = nullptr;
|
|
if (!ConvertStringSidToSidW(L"S-1-3-4", &ownerRights)) {
|
|
if (descriptor)
|
|
LocalFree(descriptor);
|
|
return false;
|
|
}
|
|
bool valid = result == ERROR_SUCCESS && owner && dacl && service.size() &&
|
|
(EqualSid(owner, localService) ||
|
|
EqualSid(owner, const_cast<BYTE *>(service.data())));
|
|
unsigned ownerRightsCount = 0;
|
|
if (valid)
|
|
for (DWORD index = 0; index < dacl->AceCount; index++) {
|
|
void *raw = nullptr;
|
|
if (!GetAce(dacl, index, &raw)) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
auto header = static_cast<ACE_HEADER *>(raw);
|
|
if (header->AceFlags & INHERIT_ONLY_ACE)
|
|
continue;
|
|
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE) {
|
|
auto ace = static_cast<ACCESS_ALLOWED_ACE *>(raw);
|
|
if (EqualSid(&ace->SidStart, ownerRights)) {
|
|
if (ace->Mask != READ_CONTROL) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
ownerRightsCount++;
|
|
continue;
|
|
}
|
|
if (!EqualSid(&ace->SidStart, system) &&
|
|
!EqualSid(&ace->SidStart, admins) &&
|
|
!EqualSid(&ace->SidStart, const_cast<BYTE *>(service.data()))) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
} else if (header->AceType != ACCESS_DENIED_ACE_TYPE) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
if (descriptor)
|
|
LocalFree(descriptor);
|
|
LocalFree(ownerRights);
|
|
return valid && ownerRightsCount == 1;
|
|
}
|
|
static bool protectedCredentialKey(HKEY key, const std::vector<BYTE> &service) {
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
PACL dacl = nullptr;
|
|
PSID owner = nullptr;
|
|
DWORD status =
|
|
GetSecurityInfo(key, SE_REGISTRY_KEY,
|
|
DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
|
|
&owner, nullptr, &dacl, nullptr, &descriptor);
|
|
BYTE system[SECURITY_MAX_SID_SIZE], admins[SECURITY_MAX_SID_SIZE];
|
|
DWORD ss = sizeof(system), as = sizeof(admins);
|
|
CreateWellKnownSid(WinLocalSystemSid, nullptr, system, &ss);
|
|
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &as);
|
|
SECURITY_DESCRIPTOR_CONTROL control{};
|
|
DWORD revision = 0;
|
|
bool okay = !status && dacl && owner &&
|
|
(EqualSid(owner, system) || EqualSid(owner, admins)) &&
|
|
GetSecurityDescriptorControl(descriptor, &control, &revision) &&
|
|
(control & SE_DACL_PROTECTED);
|
|
if (okay)
|
|
for (DWORD i = 0; i < dacl->AceCount; ++i) {
|
|
void *raw = nullptr;
|
|
if (!GetAce(dacl, i, &raw)) {
|
|
okay = false;
|
|
break;
|
|
}
|
|
auto header = static_cast<ACE_HEADER *>(raw);
|
|
if (header->AceFlags & INHERIT_ONLY_ACE)
|
|
continue;
|
|
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE) {
|
|
auto ace = static_cast<ACCESS_ALLOWED_ACE *>(raw);
|
|
if (!EqualSid(&ace->SidStart, system) &&
|
|
!EqualSid(&ace->SidStart, admins) &&
|
|
(!EqualSid(&ace->SidStart, const_cast<BYTE *>(service.data())) ||
|
|
(ace->Mask & ~(KEY_QUERY_VALUE | KEY_SET_VALUE | READ_CONTROL)))) {
|
|
okay = false;
|
|
break;
|
|
}
|
|
} else if (header->AceType != ACCESS_DENIED_ACE_TYPE) {
|
|
okay = false;
|
|
break;
|
|
}
|
|
}
|
|
if (descriptor)
|
|
LocalFree(descriptor);
|
|
return okay;
|
|
}
|
|
static int managementCredential(bool provision) {
|
|
try {
|
|
const auto root = rootPath();
|
|
if (!serviceIdentity())
|
|
return ERROR_ACCESS_DENIED;
|
|
auto release = trustedRelease(root);
|
|
std::unique_ptr<Handle> parent(parentProcess(GetCurrentProcess()));
|
|
if (!parent || !serviceIdentity(*parent) ||
|
|
!verifiedImage(GetCurrentProcess(), root, "safetynet-service-host.exe",
|
|
release) ||
|
|
!verifiedImage(*parent, root, "resources/broker/node.exe", release) ||
|
|
!sameUserSession(GetCurrentProcess(), *parent, true))
|
|
return ERROR_ACCESS_DENIED;
|
|
DWORD sidLength =
|
|
GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", nullptr, 0);
|
|
if (!sidLength || sidLength > 256)
|
|
return ERROR_ACCESS_DENIED;
|
|
std::string sid(sidLength, '\0');
|
|
if (!GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", sid.data(),
|
|
sidLength))
|
|
return ERROR_ACCESS_DENIED;
|
|
sid.resize(sidLength - 1);
|
|
credential_contract::Wipe frame;
|
|
if (!boundedStdioRead(GetStdHandle(STD_INPUT_HANDLE), frame.bytes,
|
|
provision ? 16384 : 4096))
|
|
return ERROR_ACCESS_DENIED;
|
|
std::string requestText(frame.bytes.begin(), frame.bytes.end());
|
|
credential_contract::WipeText wipeRequest{requestText};
|
|
auto request = credential_contract::parse(requestText);
|
|
SecureZeroMemory(requestText.data(), requestText.size());
|
|
struct SecretCleanup {
|
|
release_contract::Json &value;
|
|
~SecretCleanup() {
|
|
auto found = value.object.find("token");
|
|
if (found != value.object.end() && !found->second.string.empty())
|
|
SecureZeroMemory(found->second.string.data(),
|
|
found->second.string.size());
|
|
}
|
|
} secretCleanup{request};
|
|
if (provision ? !release_contract::exact(
|
|
request, {"deviceId", "origin", "policyVersion",
|
|
"policyKeySha256", "policyDigest",
|
|
"expectedPreviousSHA", "token"})
|
|
: !release_contract::exact(
|
|
request, {"deviceId", "origin", "policyVersion",
|
|
"policyKeySha256", "policyDigest"}))
|
|
return ERROR_ACCESS_DENIED;
|
|
auto service = serviceSid();
|
|
LPWSTR serviceText = nullptr;
|
|
if (service.empty() ||
|
|
!ConvertSidToStringSidW(service.data(), &serviceText))
|
|
return ERROR_ACCESS_DENIED;
|
|
std::wstring security =
|
|
L"D:P(A;;RC;;;OW)(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;" +
|
|
std::wstring(serviceText) + L")";
|
|
LocalFree(serviceText);
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
security.c_str(), SDDL_REVISION_1, &descriptor, nullptr))
|
|
return ERROR_ACCESS_DENIED;
|
|
SECURITY_ATTRIBUTES attributes{sizeof(attributes), descriptor, FALSE};
|
|
Handle mutex(
|
|
CreateMutexW(&attributes, FALSE, L"Global\\SafetyNet.Acceptance.v1"));
|
|
LocalFree(descriptor);
|
|
if (!mutex.h || !ownedAcceptanceMutex(mutex, service) ||
|
|
WaitForSingleObject(mutex, 15000) != WAIT_OBJECT_0)
|
|
return ERROR_ACCESS_DENIED;
|
|
struct Unlock {
|
|
HANDLE h;
|
|
~Unlock() { ReleaseMutex(h); }
|
|
} unlock{mutex.h};
|
|
auto floor = trustFloor();
|
|
release = trustedRelease(root);
|
|
auto assignment = release_contract::deployment(
|
|
root, release, sid, floor.deployment, floor.deploymentDigest);
|
|
const auto metadata = credential_contract::binding(
|
|
request, assignment,
|
|
acceptance_contract::validate(floor.stateCanonical));
|
|
HKEY key = nullptr;
|
|
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
|
L"SOFTWARE\\SafetyNet\\ManagementCredentials", 0,
|
|
KEY_QUERY_VALUE | READ_CONTROL | KEY_WOW64_64KEY |
|
|
(provision ? KEY_SET_VALUE : 0),
|
|
&key))
|
|
return ERROR_ACCESS_DENIED;
|
|
struct CloseKey {
|
|
HKEY h;
|
|
~CloseKey() { RegCloseKey(h); }
|
|
} closeKey{key};
|
|
if (!protectedCredentialKey(key, service))
|
|
return ERROR_ACCESS_DENIED;
|
|
const auto previous = credential_contract::load(key);
|
|
auto store = credential_contract::validateStore(previous);
|
|
auto &records = store.object.at("records").array;
|
|
const auto selected = credential_contract::selectRecord(metadata, records);
|
|
if (provision) {
|
|
if (!acceptance_contract::sha(release_contract::str(
|
|
request.object.at("expectedPreviousSHA"))) ||
|
|
release_contract::digest(previous) !=
|
|
release_contract::str(request.object.at("expectedPreviousSHA")))
|
|
return ERROR_ACCESS_DENIED;
|
|
auto record = metadata;
|
|
release_contract::Json cipher;
|
|
cipher.kind = release_contract::Json::String;
|
|
cipher.string = credential_contract::protect(
|
|
request.object.at("token").string, metadata);
|
|
record.object["protectedToken"] = cipher;
|
|
credential_contract::replaceRecord(records, record);
|
|
const auto candidate = release_contract::canonical(store);
|
|
credential_contract::validateStore(candidate);
|
|
if (RegSetValueExW(key, L"ManagementCredentials", 0, REG_BINARY,
|
|
reinterpret_cast<const BYTE *>(candidate.data()),
|
|
static_cast<DWORD>(candidate.size())) ||
|
|
RegFlushKey(key) || credential_contract::load(key) != candidate)
|
|
return ERROR_ACCESS_DENIED;
|
|
return boundedStdioWrite("{\"persisted\":true,\"stateSha256\":" +
|
|
release_contract::escaped(
|
|
release_contract::digest(candidate)) +
|
|
"}")
|
|
? 0
|
|
: ERROR_ACCESS_DENIED;
|
|
}
|
|
if (selected) {
|
|
const auto &record = *selected;
|
|
auto bound = record;
|
|
bound.object.erase("protectedToken");
|
|
std::string token = credential_contract::unprotect(
|
|
release_contract::str(record.object.at("protectedToken")), bound);
|
|
auto response = request;
|
|
release_contract::Json secret;
|
|
secret.kind = release_contract::Json::String;
|
|
secret.string = token;
|
|
response.object["token"] = secret;
|
|
std::string reply = release_contract::canonical(response);
|
|
SecureZeroMemory(token.data(), token.size());
|
|
SecureZeroMemory(secret.string.data(), secret.string.size());
|
|
SecureZeroMemory(response.object.at("token").string.data(),
|
|
response.object.at("token").string.size());
|
|
bool okay = reply.size() <= 8192 && boundedStdioWrite(reply);
|
|
SecureZeroMemory(reply.data(), reply.size());
|
|
return okay ? 0 : ERROR_ACCESS_DENIED;
|
|
}
|
|
return ERROR_ACCESS_DENIED;
|
|
} catch (...) {
|
|
return ERROR_ACCESS_DENIED;
|
|
}
|
|
}
|
|
static int commitAcceptance() {
|
|
try {
|
|
const auto root = rootPath();
|
|
if (!serviceIdentity())
|
|
return ERROR_ACCESS_DENIED;
|
|
auto release = trustedRelease(root);
|
|
std::unique_ptr<Handle> parent(parentProcess(GetCurrentProcess()));
|
|
if (!parent || !serviceIdentity(*parent) ||
|
|
!verifiedImage(GetCurrentProcess(), root, "safetynet-service-host.exe",
|
|
release) ||
|
|
!verifiedImage(*parent, root, "resources/broker/node.exe", release) ||
|
|
!sameUserSession(GetCurrentProcess(), *parent, true))
|
|
return ERROR_ACCESS_DENIED;
|
|
DWORD size =
|
|
GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", nullptr, 0);
|
|
if (!size || size > 256)
|
|
return ERROR_ACCESS_DENIED;
|
|
std::string sid(size, '\0');
|
|
if (!GetEnvironmentVariableA("SAFETYNET_SERVICE_CLIENT_SID", sid.data(),
|
|
size))
|
|
return ERROR_ACCESS_DENIED;
|
|
sid.resize(size - 1);
|
|
std::vector<BYTE> frameBytes;
|
|
if (!boundedStdioRead(GetStdHandle(STD_INPUT_HANDLE), frameBytes, 2097152))
|
|
return ERROR_ACCESS_DENIED;
|
|
std::string requestText(frameBytes.begin(), frameBytes.end());
|
|
release_contract::Parser parser{requestText};
|
|
auto request = parser.parse();
|
|
if (parser.offset != requestText.size() ||
|
|
!release_contract::exact(request,
|
|
{"expectedPreviousSHA", "candidateState"}) ||
|
|
!acceptance_contract::sha(
|
|
release_contract::str(request.object.at("expectedPreviousSHA"))))
|
|
return ERROR_ACCESS_DENIED;
|
|
auto candidateText =
|
|
release_contract::canonical(request.object.at("candidateState"));
|
|
auto candidate = acceptance_contract::validate(candidateText);
|
|
auto service = serviceSid();
|
|
if (service.empty())
|
|
return ERROR_ACCESS_DENIED;
|
|
LPWSTR serviceText = nullptr;
|
|
if (!ConvertSidToStringSidW(service.data(), &serviceText))
|
|
return ERROR_ACCESS_DENIED;
|
|
std::wstring security =
|
|
L"D:P(A;;RC;;;OW)(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;" +
|
|
std::wstring(serviceText) + L")";
|
|
LocalFree(serviceText);
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
security.c_str(), SDDL_REVISION_1, &descriptor, nullptr))
|
|
return ERROR_ACCESS_DENIED;
|
|
SECURITY_ATTRIBUTES attributes{sizeof(attributes), descriptor, FALSE};
|
|
Handle mutex(
|
|
CreateMutexW(&attributes, FALSE, L"Global\\SafetyNet.Acceptance.v1"));
|
|
LocalFree(descriptor);
|
|
if (!mutex.h || !ownedAcceptanceMutex(mutex, service) ||
|
|
WaitForSingleObject(mutex, 15000) != WAIT_OBJECT_0)
|
|
return ERROR_ACCESS_DENIED;
|
|
// No commit is attempted on an abandoned mutex: a prior writer may have
|
|
// completed its value write without returning a durable receipt.
|
|
struct Unlock {
|
|
HANDLE value;
|
|
~Unlock() { ReleaseMutex(value); }
|
|
} unlock{mutex.h};
|
|
auto floor = trustFloor();
|
|
auto old = acceptance_contract::validate(floor.stateCanonical);
|
|
if (release_contract::digest(floor.stateCanonical) !=
|
|
release_contract::str(request.object.at("expectedPreviousSHA")) ||
|
|
!acceptance_contract::transition(old, candidate))
|
|
return ERROR_ACCESS_DENIED;
|
|
release = trustedRelease(root);
|
|
auto assignment = release_contract::deployment(
|
|
root, release, sid, floor.deployment, floor.deploymentDigest);
|
|
auto releaseJson = release_contract::signedPayload(
|
|
root + L"\\release-envelope.json",
|
|
release_contract::unhex(RELEASE_ANCHOR_XY),
|
|
std::string("SafetyNet Windows release v1\0",
|
|
sizeof("SafetyNet Windows release v1\0") - 1));
|
|
auto deploymentJson = release_contract::signedPayload(
|
|
root + L"\\deployment-envelope.json",
|
|
release_contract::customerKey(release.customerPem),
|
|
std::string("SafetyNet Windows deployment v1\0",
|
|
sizeof("SafetyNet Windows deployment v1\0") - 1),
|
|
262144);
|
|
if (release_contract::number(candidate.object.at("release").object.at(
|
|
"minimumSequence")) != release.sequence ||
|
|
release_contract::str(
|
|
candidate.object.at("release").object.at("acceptedDigest")) !=
|
|
release_contract::digest(
|
|
release_contract::canonical(releaseJson)) ||
|
|
release_contract::number(
|
|
candidate.object.at("deployment").object.at("minimumSequence")) !=
|
|
assignment.deploymentSequence ||
|
|
release_contract::str(
|
|
candidate.object.at("deployment").object.at("acceptedDigest")) !=
|
|
release_contract::digest(
|
|
release_contract::canonical(deploymentJson)))
|
|
return ERROR_ACCESS_DENIED;
|
|
auto key = release_contract::pemDer(assignment.policyPem);
|
|
std::string policyKeyHash =
|
|
release_contract::digest(std::string(key.begin(), key.end()));
|
|
const std::string ownId =
|
|
sid + "|" + assignment.deviceId + "|" + policyKeyHash;
|
|
std::map<std::string, const release_contract::Json *> previous;
|
|
for (const auto &record : old.object.at("policies").array)
|
|
previous.emplace(acceptance_contract::identity(record), &record);
|
|
bool hasOwn = false;
|
|
for (const auto &record : candidate.object.at("policies").array) {
|
|
auto id = acceptance_contract::identity(record);
|
|
if (id == ownId) {
|
|
hasOwn = true;
|
|
if (release_contract::number(record.object.at("minimumVersion")) <
|
|
assignment.minimumVersion)
|
|
return ERROR_ACCESS_DENIED;
|
|
} else {
|
|
auto found = previous.find(id);
|
|
if (found == previous.end() ||
|
|
release_contract::canonical(*found->second) !=
|
|
release_contract::canonical(record))
|
|
return ERROR_ACCESS_DENIED;
|
|
}
|
|
}
|
|
if (!hasOwn)
|
|
return ERROR_ACCESS_DENIED;
|
|
HKEY registry = nullptr;
|
|
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\SafetyNet\\Trust", 0,
|
|
KEY_SET_VALUE | KEY_QUERY_VALUE | KEY_WOW64_64KEY,
|
|
®istry) != ERROR_SUCCESS)
|
|
return ERROR_ACCESS_DENIED;
|
|
// One canonical value, flushed before acknowledgment. These APIs are NOT
|
|
// exercised by the uninstalled tests. Caller must have verified the actual
|
|
// signed policy through service-acceptance before asking to commit.
|
|
DWORD result =
|
|
RegSetValueExW(registry, L"AcceptanceState", 0, REG_BINARY,
|
|
reinterpret_cast<const BYTE *>(candidateText.data()),
|
|
DWORD(candidateText.size()));
|
|
if (result == ERROR_SUCCESS)
|
|
result = RegFlushKey(registry);
|
|
RegCloseKey(registry);
|
|
if (result != ERROR_SUCCESS)
|
|
return ERROR_ACCESS_DENIED;
|
|
auto committed = trustFloor();
|
|
if (committed.stateCanonical != candidateText)
|
|
return ERROR_ACCESS_DENIED;
|
|
return boundedStdioWrite("{\"persisted\":true,\"stateSha256\":" +
|
|
release_contract::escaped(
|
|
release_contract::digest(candidateText)) +
|
|
"}")
|
|
? 0
|
|
: ERROR_ACCESS_DENIED;
|
|
} catch (...) {
|
|
return ERROR_ACCESS_DENIED;
|
|
}
|
|
}
|
|
static bool serve(HANDLE pipe, const std::wstring &root) {
|
|
Handle clientProcess;
|
|
if (!pipeClient(pipe, root, &clientProcess.h) || !resources(root) ||
|
|
!serviceIdentity())
|
|
return false;
|
|
try {
|
|
auto release = trustedRelease(root);
|
|
auto floor = trustFloor();
|
|
release_contract::deployment(root, release, currentSid(clientProcess),
|
|
floor.deployment, floor.deploymentDigest);
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
Handle job(CreateJobObjectW(nullptr, nullptr));
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
|
|
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
if (!job.h || !SetInformationJobObject(job, JobObjectExtendedLimitInformation,
|
|
&limits, sizeof(limits)))
|
|
return false;
|
|
SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE};
|
|
Handle inputRead, inputWrite, outputRead, outputWrite,
|
|
nul(CreateFileW(L"NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
&sa, OPEN_EXISTING, 0, nullptr));
|
|
if (!CreatePipe(&inputRead.h, &inputWrite.h, &sa, 0) ||
|
|
!CreatePipe(&outputRead.h, &outputWrite.h, &sa, 0) ||
|
|
nul.h == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
SetHandleInformation(inputWrite, HANDLE_FLAG_INHERIT, 0);
|
|
SetHandleInformation(outputRead, HANDLE_FLAG_INHERIT, 0);
|
|
SIZE_T bytes = 0;
|
|
InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes);
|
|
std::vector<BYTE> attributes(bytes);
|
|
STARTUPINFOEXW startup{};
|
|
startup.StartupInfo.cb = sizeof(startup);
|
|
startup.lpAttributeList =
|
|
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes.data());
|
|
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &bytes))
|
|
return false;
|
|
HANDLE inherited[] = {inputRead.h, outputWrite.h, nul.h};
|
|
bool configured =
|
|
UpdateProcThreadAttribute(startup.lpAttributeList, 0,
|
|
PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited,
|
|
sizeof(inherited), nullptr, nullptr) != FALSE;
|
|
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
|
startup.StartupInfo.hStdInput = inputRead;
|
|
startup.StartupInfo.hStdOutput = outputWrite;
|
|
startup.StartupInfo.hStdError = nul;
|
|
std::wstring executable = root + L"\\resources\\broker\\node.exe",
|
|
command = L"\"" + executable + L"\" \"" + root +
|
|
L"\\resources\\broker\\apps\\network-"
|
|
L"broker\\service-entry.cjs\" --service-stdio",
|
|
env = environment(currentSid(clientProcess));
|
|
PROCESS_INFORMATION information{};
|
|
BOOL launched =
|
|
configured &&
|
|
CreateProcessW(
|
|
executable.c_str(), command.data(), nullptr, nullptr, TRUE,
|
|
CREATE_SUSPENDED | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT |
|
|
EXTENDED_STARTUPINFO_PRESENT,
|
|
env.data(), root.c_str(), &startup.StartupInfo, &information);
|
|
DeleteProcThreadAttributeList(startup.lpAttributeList);
|
|
if (!launched)
|
|
return false;
|
|
Handle process(information.hProcess), thread(information.hThread);
|
|
if (!AssignProcessToJobObject(job, process)) {
|
|
TerminateProcess(process, 1);
|
|
WaitForSingleObject(process, 5000);
|
|
return false;
|
|
}
|
|
CloseHandle(inputRead.h);
|
|
inputRead.h = nullptr;
|
|
CloseHandle(outputWrite.h);
|
|
outputWrite.h = nullptr;
|
|
if (ResumeThread(thread) == DWORD(-1)) {
|
|
TerminateJobObject(job, 1);
|
|
WaitForSingleObject(process, 5000);
|
|
return false;
|
|
}
|
|
// Native relay carries framed data only. Typed START and v1 sequence/receipt
|
|
// validation belongs to the reviewed constant Node service-stdio adapter.
|
|
Handle serveThread;
|
|
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
|
|
GetCurrentProcess(), &serveThread.h, THREAD_TERMINATE,
|
|
FALSE, 0)) {
|
|
TerminateJobObject(job, 1);
|
|
return false;
|
|
}
|
|
std::atomic<ULONGLONG> writeDeadline{0}, readDeadline{0};
|
|
std::atomic<bool> good{true};
|
|
std::thread watchdog([&] {
|
|
while (good) {
|
|
if (WaitForSingleObject(stopEvent, 50) == WAIT_OBJECT_0 ||
|
|
WaitForSingleObject(clientProcess, 0) != WAIT_TIMEOUT ||
|
|
(writeDeadline.load() != 0 &&
|
|
GetTickCount64() >= writeDeadline.load()) ||
|
|
(readDeadline.load() != 0 &&
|
|
GetTickCount64() >= readDeadline.load())) {
|
|
good = false;
|
|
TerminateJobObject(job, 1);
|
|
CancelSynchronousIo(serveThread);
|
|
CancelIoEx(pipe, nullptr);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
std::thread outgoing([&] {
|
|
while (good) {
|
|
DWORD size = 0, done = 0;
|
|
if (!ReadFile(outputRead, &size, 4, &done, nullptr) || !done) {
|
|
good = false;
|
|
break;
|
|
}
|
|
readDeadline = GetTickCount64() + 15000;
|
|
DWORD headerOffset = done;
|
|
while (headerOffset < 4) {
|
|
if (!ReadFile(outputRead,
|
|
reinterpret_cast<BYTE *>(&size) + headerOffset,
|
|
4 - headerOffset, &done, nullptr) ||
|
|
!done) {
|
|
good = false;
|
|
break;
|
|
}
|
|
headerOffset += done;
|
|
}
|
|
if (!good || !size || size > 65536) {
|
|
good = false;
|
|
break;
|
|
}
|
|
std::vector<BYTE> value(size);
|
|
DWORD offset = 0;
|
|
while (offset < size) {
|
|
if (!ReadFile(outputRead, value.data() + offset, size - offset, &done,
|
|
nullptr) ||
|
|
!done) {
|
|
good = false;
|
|
break;
|
|
}
|
|
offset += done;
|
|
}
|
|
if (!good || !frame(pipe, true, value, 65536)) {
|
|
good = false;
|
|
break;
|
|
}
|
|
readDeadline = 0;
|
|
}
|
|
CancelIoEx(pipe, nullptr);
|
|
});
|
|
bool first = true;
|
|
while (good && WaitForSingleObject(stopEvent, 0) == WAIT_TIMEOUT) {
|
|
std::vector<BYTE> value;
|
|
if (!frame(pipe, false, value, first ? 2097152 : 65536,
|
|
first ? 15000 : INFINITE))
|
|
break;
|
|
first = false;
|
|
if (!writeChildFrame(inputWrite, value, writeDeadline))
|
|
break;
|
|
}
|
|
good = false;
|
|
TerminateJobObject(job, 1);
|
|
CancelSynchronousIo(outgoing.native_handle());
|
|
CancelIoEx(pipe, nullptr);
|
|
outgoing.join();
|
|
watchdog.join();
|
|
return WaitForSingleObject(process, 5000) == WAIT_OBJECT_0;
|
|
}
|
|
static void report(DWORD state, DWORD error = 0) {
|
|
SERVICE_STATUS status{};
|
|
status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
|
|
status.dwCurrentState = state;
|
|
status.dwWin32ExitCode = error;
|
|
status.dwControlsAccepted =
|
|
state == SERVICE_RUNNING ? SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN
|
|
: 0;
|
|
status.dwWaitHint = state == SERVICE_STOP_PENDING ? 5000 : 0;
|
|
SetServiceStatus(statusHandle, &status);
|
|
}
|
|
static DWORD WINAPI control(DWORD code, DWORD, void *, void *) {
|
|
if (code == SERVICE_CONTROL_STOP || code == SERVICE_CONTROL_SHUTDOWN) {
|
|
report(SERVICE_STOP_PENDING);
|
|
SetEvent(stopEvent);
|
|
}
|
|
return NO_ERROR;
|
|
}
|
|
static void WINAPI serviceMain(DWORD, wchar_t **) {
|
|
statusHandle = RegisterServiceCtrlHandlerExW(SERVICE_NAME, control, nullptr);
|
|
if (!statusHandle)
|
|
return;
|
|
report(SERVICE_START_PENDING);
|
|
Handle stop(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
stopEvent = stop;
|
|
std::wstring root = rootPath();
|
|
if (!stop.h || !serviceIdentity() || !resources(root)) {
|
|
report(SERVICE_STOPPED, ERROR_ACCESS_DENIED);
|
|
return;
|
|
}
|
|
PSECURITY_DESCRIPTOR security = nullptr;
|
|
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
L"D:P(A;;GA;;;SY)(A;;0x0012019b;;;AU)", SDDL_REVISION_1, &security,
|
|
nullptr)) {
|
|
report(SERVICE_STOPPED, ERROR_ACCESS_DENIED);
|
|
return;
|
|
}
|
|
SECURITY_ATTRIBUTES sa{sizeof(sa), security, FALSE};
|
|
Handle pipe(CreateNamedPipeW(PIPE_NAME,
|
|
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED |
|
|
FILE_FLAG_FIRST_PIPE_INSTANCE,
|
|
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT |
|
|
PIPE_REJECT_REMOTE_CLIENTS,
|
|
1, 65536, 65536, 15000, &sa));
|
|
LocalFree(security);
|
|
if (pipe.h == INVALID_HANDLE_VALUE) {
|
|
report(SERVICE_STOPPED, ERROR_ACCESS_DENIED);
|
|
return;
|
|
}
|
|
report(SERVICE_RUNNING);
|
|
while (WaitForSingleObject(stopEvent, 0) == WAIT_TIMEOUT) {
|
|
Handle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
OVERLAPPED ov{};
|
|
ov.hEvent = event;
|
|
BOOL connected = ConnectNamedPipe(pipe, &ov);
|
|
DWORD code = connected ? ERROR_SUCCESS : GetLastError();
|
|
if (code == ERROR_IO_PENDING) {
|
|
HANDLE events[] = {event.h, stopEvent};
|
|
if (WaitForMultipleObjects(2, events, FALSE, INFINITE) != WAIT_OBJECT_0) {
|
|
CancelIoEx(pipe, &ov);
|
|
WaitForSingleObject(event, INFINITE);
|
|
break;
|
|
}
|
|
DWORD bytes = 0;
|
|
connected = GetOverlappedResult(pipe, &ov, &bytes, FALSE);
|
|
} else
|
|
connected = code == ERROR_PIPE_CONNECTED;
|
|
if (connected)
|
|
serve(pipe, root);
|
|
DisconnectNamedPipe(pipe);
|
|
}
|
|
report(SERVICE_STOPPED);
|
|
}
|
|
#ifdef SAFETYNET_NATIVE_SELF_TEST
|
|
static bool ownerRightsTest() {
|
|
auto user = currentSid(GetCurrentProcess());
|
|
std::wstring sid(user.begin(), user.end());
|
|
std::wstring security = L"D:P(A;;0x00120001;;;" + sid + L")(A;;RC;;;OW)";
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
security.c_str(), SDDL_REVISION_1, &descriptor, nullptr))
|
|
return false;
|
|
SECURITY_ATTRIBUTES attributes{sizeof(attributes), descriptor, FALSE};
|
|
auto name = L"Local\\SafetyNet.OwnerRights.Proof." +
|
|
std::to_wstring(GetCurrentProcessId());
|
|
Handle mutex(CreateMutexW(&attributes, FALSE, name.c_str()));
|
|
LocalFree(descriptor);
|
|
if (!mutex.h)
|
|
return false;
|
|
Handle denied(OpenMutexW(WRITE_DAC, FALSE, name.c_str()));
|
|
DWORD deniedCode = GetLastError();
|
|
if (denied.h || deniedCode != ERROR_ACCESS_DENIED)
|
|
return false;
|
|
Handle allowed(OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE | READ_CONTROL,
|
|
FALSE, name.c_str()));
|
|
if (!allowed.h)
|
|
return false;
|
|
PSECURITY_DESCRIPTOR actual = nullptr;
|
|
PSID owner = nullptr;
|
|
PACL dacl = nullptr;
|
|
if (GetSecurityInfo(allowed, SE_KERNEL_OBJECT,
|
|
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
|
&owner, nullptr, &dacl, nullptr,
|
|
&actual) != ERROR_SUCCESS)
|
|
return false;
|
|
PSID expected = nullptr;
|
|
ConvertStringSidToSidA(user.c_str(), &expected);
|
|
PSID ownerRights = nullptr;
|
|
ConvertStringSidToSidW(L"S-1-3-4", &ownerRights);
|
|
bool valid = owner && expected && EqualSid(owner, expected);
|
|
unsigned count = 0;
|
|
if (valid && dacl)
|
|
for (DWORD i = 0; i < dacl->AceCount; i++) {
|
|
void *raw = nullptr;
|
|
if (!GetAce(dacl, i, &raw)) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
auto header = static_cast<ACE_HEADER *>(raw);
|
|
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE) {
|
|
auto ace = static_cast<ACCESS_ALLOWED_ACE *>(raw);
|
|
if (EqualSid(&ace->SidStart, ownerRights)) {
|
|
count++;
|
|
if (ace->Mask != READ_CONTROL)
|
|
valid = false;
|
|
}
|
|
}
|
|
}
|
|
if (expected)
|
|
LocalFree(expected);
|
|
if (ownerRights)
|
|
LocalFree(ownerRights);
|
|
if (actual)
|
|
LocalFree(actual);
|
|
return valid && count == 1;
|
|
}
|
|
static bool writeDeadlineTest() {
|
|
Handle read, write;
|
|
if (!CreatePipe(&read.h, &write.h, nullptr, 4096))
|
|
return false;
|
|
std::atomic<ULONGLONG> deadline{0};
|
|
std::vector<BYTE> samplePayload(16, 42);
|
|
if (!writeChildFrame(write, samplePayload, deadline, 30) ||
|
|
deadline.load() != 0)
|
|
return false;
|
|
DWORD done = 0;
|
|
std::array<BYTE, 20> consumed{};
|
|
if (!ReadFile(read, consumed.data(), 20, &done, nullptr) || done != 20)
|
|
return false;
|
|
Sleep(60);
|
|
if (deadline.load() != 0)
|
|
return false; // healthy idle exceeds test budget
|
|
std::atomic<bool> result{true};
|
|
std::vector<BYTE> stalledPayload(262144, 43);
|
|
std::thread blocked(
|
|
[&] { result = writeChildFrame(write, stalledPayload, deadline, 30); });
|
|
for (int i = 0; i < 100 && deadline.load() == 0; i++)
|
|
Sleep(1);
|
|
Sleep(60);
|
|
if (deadline.load() == 0) {
|
|
CancelSynchronousIo(blocked.native_handle());
|
|
blocked.join();
|
|
return false;
|
|
}
|
|
if (!CancelSynchronousIo(blocked.native_handle())) {
|
|
CloseHandle(read.h);
|
|
read.h = nullptr;
|
|
}
|
|
blocked.join();
|
|
return !result && deadline.load() != 0;
|
|
}
|
|
static bool pipeNegativeTest() {
|
|
std::wstring name = L"\\\\.\\pipe\\SafetyNet.NativeTest." +
|
|
std::to_wstring(GetCurrentProcessId());
|
|
PSECURITY_DESCRIPTOR security = nullptr;
|
|
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
L"D:P(A;;GA;;;SY)(A;;0x0012019b;;;AU)", SDDL_REVISION_1, &security,
|
|
nullptr))
|
|
return false;
|
|
SECURITY_ATTRIBUTES sa{sizeof(sa), security, FALSE};
|
|
Handle server(CreateNamedPipeW(name.c_str(),
|
|
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED |
|
|
FILE_FLAG_FIRST_PIPE_INSTANCE,
|
|
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE |
|
|
PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
|
|
2, 4096, 4096, 1000, &sa));
|
|
LocalFree(security);
|
|
if (server.h == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
// Attempt a second instance WITHOUT FIRST_PIPE_INSTANCE: DACL rather than
|
|
// creation-flag coincidence must deny FILE_CREATE_PIPE_INSTANCE to this user.
|
|
Handle attacker(CreateNamedPipeW(name.c_str(), PIPE_ACCESS_DUPLEX,
|
|
PIPE_TYPE_BYTE | PIPE_WAIT, 2, 4096, 4096,
|
|
1000, nullptr));
|
|
if (attacker.h != INVALID_HANDLE_VALUE ||
|
|
GetLastError() != ERROR_ACCESS_DENIED)
|
|
return false;
|
|
Handle client(CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
|
|
nullptr, OPEN_EXISTING, 0, nullptr));
|
|
if (client.h == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
ULONG actualServerPid = 0;
|
|
if (!GetNamedPipeServerProcessId(client, &actualServerPid) ||
|
|
actualServerPid != GetCurrentProcessId())
|
|
return false;
|
|
Handle actualServer(OpenProcess(
|
|
PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, actualServerPid));
|
|
if (!actualServer.h || serviceIdentity(actualServer))
|
|
return false;
|
|
Handle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
OVERLAPPED ov{};
|
|
ov.hEvent = event;
|
|
BOOL connected = ConnectNamedPipe(server, &ov);
|
|
if (!connected && GetLastError() != ERROR_PIPE_CONNECTED)
|
|
return false;
|
|
if (pipeClient(server, rootPath()))
|
|
return false;
|
|
DWORD invalid = 0xffffffff, done = 0;
|
|
if (!WriteFile(client, &invalid, 4, &done, nullptr) || done != 4)
|
|
return false;
|
|
std::vector<BYTE> frameBytes;
|
|
return !frame(server, false, frameBytes, 65536, 1000);
|
|
}
|
|
static int selfTest() {
|
|
Handle stop(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
stopEvent = stop;
|
|
if (!ownerRightsTest())
|
|
return 84;
|
|
if (!pipeNegativeTest())
|
|
return 89;
|
|
if (!writeDeadlineTest())
|
|
return 88;
|
|
if (serviceIdentity())
|
|
return 90;
|
|
Handle job(CreateJobObjectW(nullptr, nullptr));
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
|
|
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits,
|
|
sizeof(limits)))
|
|
return 91;
|
|
auto root = rootPath();
|
|
std::wstring command =
|
|
L"\"" + root + L"\\safetynet-service-host-test.exe\" --job-child";
|
|
STARTUPINFOW startup{};
|
|
startup.cb = sizeof(startup);
|
|
PROCESS_INFORMATION information{};
|
|
if (!CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE,
|
|
CREATE_SUSPENDED | CREATE_NO_WINDOW, nullptr,
|
|
root.c_str(), &startup, &information))
|
|
return 92;
|
|
Handle process(information.hProcess), thread(information.hThread);
|
|
if (!AssignProcessToJobObject(job, process))
|
|
return 93;
|
|
BOOL member = FALSE;
|
|
if (!IsProcessInJob(process, job, &member) || !member)
|
|
return 94;
|
|
ResumeThread(thread);
|
|
Handle descendant;
|
|
for (int attempt = 0; attempt < 100 && !descendant.h; attempt++) {
|
|
Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
|
|
PROCESSENTRY32W item{};
|
|
item.dwSize = sizeof(item);
|
|
if (Process32FirstW(snapshot, &item))
|
|
do {
|
|
if (item.th32ParentProcessID == information.dwProcessId) {
|
|
descendant.h =
|
|
OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION,
|
|
FALSE, item.th32ProcessID);
|
|
break;
|
|
}
|
|
} while (Process32NextW(snapshot, &item));
|
|
if (!descendant.h)
|
|
Sleep(20);
|
|
}
|
|
if (!descendant.h || !IsProcessInJob(descendant, job, &member) || !member)
|
|
return 98;
|
|
if (WaitForSingleObject(process, 50) != WAIT_TIMEOUT)
|
|
return 95;
|
|
CloseHandle(job.h);
|
|
job.h = nullptr;
|
|
if (WaitForSingleObject(process, 5000) != WAIT_OBJECT_0 ||
|
|
WaitForSingleObject(descendant, 5000) != WAIT_OBJECT_0)
|
|
return 96;
|
|
if (hashFile(
|
|
root + L"\\safetynet-service-host-test.exe",
|
|
"0000000000000000000000000000000000000000000000000000000000000000"))
|
|
return 97;
|
|
puts("{\"ownerWriteDacDenied\":true,\"ownerReadSyncModifyAllowed\":true,"
|
|
"\"healthyIdleWriteDeadlineCleared\":true,\"stalledWriteCancelled\":"
|
|
"true,\"sameConnectedHandleServerIdentified\":true,"
|
|
"\"wrongServerServiceIdentityDenied\":true,\"pipeSecondInstanceDenied\":"
|
|
"true,\"unauthorizedClientDenied\":true,\"oversizedFrameDenied\":true,"
|
|
"\"serviceIdentityRejected\":true,\"jobMembership\":true,"
|
|
"\"descendantJobMembership\":true,\"killOnCloseTerminal\":true,"
|
|
"\"wrongHashRejected\":true}");
|
|
return 0;
|
|
}
|
|
#endif
|
|
int wmain(int argc, wchar_t **argv) {
|
|
#ifdef SAFETYNET_NATIVE_SELF_TEST
|
|
if (argc == 2 && !wcscmp(argv[1], L"--verify-credential-selection"))
|
|
return credential_contract::selectionSelfTest() ? 0 : 86;
|
|
if (argc == 2 && !wcscmp(argv[1], L"--verify-state-fixture")) {
|
|
try {
|
|
auto old = acceptance_contract::validate(
|
|
release_contract::read(rootPath() + L"\\acceptance-old.json"));
|
|
auto next = acceptance_contract::validate(
|
|
release_contract::read(rootPath() + L"\\acceptance-candidate.json"));
|
|
return acceptance_contract::transition(old, next) ? 0 : 86;
|
|
} catch (...) {
|
|
return 86;
|
|
}
|
|
}
|
|
if (argc == 2 && !wcscmp(argv[1], L"--verify-equivocation")) {
|
|
try {
|
|
release_contract::release(rootPath(),
|
|
release_contract::unhex(RELEASE_ANCHOR_XY), 2,
|
|
std::string(64, '0'));
|
|
return 85;
|
|
} catch (...) {
|
|
return 86;
|
|
}
|
|
}
|
|
if (argc == 2 && !wcscmp(argv[1], L"--verify-fixture")) {
|
|
try {
|
|
auto manifest = release_contract::release(
|
|
rootPath(), release_contract::unhex(RELEASE_ANCHOR_XY), 1);
|
|
auto assignment = release_contract::deployment(rootPath(), manifest,
|
|
"S-1-5-21-1-2-3-1001", 1);
|
|
if (assignment.minimumVersion != 3 ||
|
|
!hashFile(
|
|
rootPath() + L"\\safetynet-service-host-test.exe",
|
|
manifest.hashes.at("safetynet-service-host-test.exe").c_str()))
|
|
return 87;
|
|
puts("{\"nativeVendorSignatureVerified\":true,"
|
|
"\"nativeDelegatedCustomerSignatureVerified\":true,"
|
|
"\"nativeSidAssignmentVerified\":true}");
|
|
return 0;
|
|
} catch (...) {
|
|
return 86;
|
|
}
|
|
}
|
|
if (argc == 2 && !wcscmp(argv[1], L"--self-test"))
|
|
return selfTest();
|
|
if (argc == 2 && !wcscmp(argv[1], L"--job-grandchild")) {
|
|
Sleep(INFINITE);
|
|
return 0;
|
|
}
|
|
if (argc == 2 && !wcscmp(argv[1], L"--job-child")) {
|
|
std::wstring command =
|
|
L"\"" + rootPath() +
|
|
L"\\safetynet-service-host-test.exe\" --job-grandchild";
|
|
STARTUPINFOW startup{};
|
|
startup.cb = sizeof(startup);
|
|
PROCESS_INFORMATION info{};
|
|
if (!CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE,
|
|
CREATE_NO_WINDOW, nullptr, nullptr, &startup, &info))
|
|
return 99;
|
|
CloseHandle(info.hProcess);
|
|
CloseHandle(info.hThread);
|
|
Sleep(INFINITE);
|
|
return 0;
|
|
}
|
|
#endif
|
|
if (argc == 2 && !wcscmp(argv[1], L"--client-stdio"))
|
|
return clientStdio();
|
|
if (argc == 2 && !wcscmp(argv[1], L"--trust-snapshot"))
|
|
return trustSnapshot();
|
|
if (argc == 2 && !wcscmp(argv[1], L"--topology-snapshot"))
|
|
return topologySnapshot();
|
|
if (argc == 2 && !wcscmp(argv[1], L"--commit-acceptance"))
|
|
return commitAcceptance();
|
|
if (argc == 2 && !wcscmp(argv[1], L"--management-credential"))
|
|
return managementCredential(false);
|
|
if (argc == 2 && !wcscmp(argv[1], L"--provision-management-credential"))
|
|
return managementCredential(true);
|
|
if (argc != 1)
|
|
return ERROR_INVALID_PARAMETER;
|
|
SERVICE_TABLE_ENTRYW table[] = {
|
|
{const_cast<wchar_t *>(SERVICE_NAME), serviceMain}, {nullptr, nullptr}};
|
|
return StartServiceCtrlDispatcherW(table) ? 0 : int(GetLastError());
|
|
}
|