379 lines
13 KiB
C++
379 lines
13 KiB
C++
#pragma once
|
|
// clang-format off
|
|
#include <windows.h>
|
|
#include <sddl.h>
|
|
#include <objbase.h>
|
|
#include <fwpmu.h>
|
|
// clang-format on
|
|
#include <string>
|
|
#include <vector>
|
|
#include <windows.h>
|
|
#pragma comment(lib, "fwpuclnt.lib")
|
|
#pragma comment(lib, "ole32.lib")
|
|
namespace topology_contract {
|
|
static constexpr UINT16 BROKER_PORT = 43120;
|
|
static GUID guid(const wchar_t *text) {
|
|
GUID value{};
|
|
if (FAILED(CLSIDFromString(text, &value)))
|
|
throw 1;
|
|
return value;
|
|
}
|
|
struct Memory {
|
|
void *p = nullptr;
|
|
~Memory() {
|
|
if (p)
|
|
FwpmFreeMemory0(&p);
|
|
}
|
|
};
|
|
struct Engine {
|
|
HANDLE h = nullptr;
|
|
~Engine() {
|
|
if (h)
|
|
FwpmEngineClose0(h);
|
|
}
|
|
};
|
|
struct Enumeration {
|
|
HANDLE engine, h = nullptr;
|
|
explicit Enumeration(HANDLE e) : engine(e) {}
|
|
~Enumeration() {
|
|
if (h)
|
|
FwpmFilterDestroyEnumHandle0(engine, h);
|
|
}
|
|
};
|
|
struct Result {
|
|
DWORD code = ERROR_ACCESS_DENIED;
|
|
UINT32 accessibleFilters = 0;
|
|
UINT32 ownedFilters = 0;
|
|
bool ownedVerified = false;
|
|
bool collision = false;
|
|
bool bfeRunning = false;
|
|
// Enumeration omits unreadable objects even at successful EOF. Never infer
|
|
// this from token elevation, caller JSON or the number of returned rows.
|
|
bool allObjectsReadable = false;
|
|
};
|
|
static bool blobEqual(const FWP_BYTE_BLOB *actual,
|
|
const std::vector<BYTE> &expected) {
|
|
return actual && actual->data && actual->size == expected.size() &&
|
|
memcmp(actual->data, expected.data(), expected.size()) == 0;
|
|
}
|
|
struct ExpectedCondition {
|
|
GUID field;
|
|
FWP_MATCH_TYPE match;
|
|
FWP_DATA_TYPE type;
|
|
UINT32 value;
|
|
std::vector<BYTE> bytes;
|
|
};
|
|
static bool marker(const FWP_BYTE_BLOB &value) {
|
|
static constexpr char expected[] = "SafetyNet.NetworkBoundary.v1";
|
|
return value.data && value.size == sizeof(expected) - 1 &&
|
|
memcmp(value.data, expected, sizeof(expected) - 1) == 0;
|
|
}
|
|
using SecurityQuery = DWORD(WINAPI *)(HANDLE, const GUID *,
|
|
SECURITY_INFORMATION, PSID *, PSID *,
|
|
PACL *, PACL *, PSECURITY_DESCRIPTOR *);
|
|
static bool protectedObject(HANDLE engine, const GUID &key,
|
|
SecurityQuery query) {
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
PACL dacl = nullptr;
|
|
PSID owner = nullptr;
|
|
DWORD code = query(engine, &key,
|
|
OWNER_SECURITY_INFORMATION | DACL_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 = !code && dacl && owner &&
|
|
(EqualSid(owner, system) || EqualSid(owner, admins)) &&
|
|
GetSecurityDescriptorControl(descriptor, &control, &revision) &&
|
|
(control & SE_DACL_PROTECTED);
|
|
const DWORD write = FWPM_ACTRL_ADD | FWPM_ACTRL_ADD_LINK |
|
|
FWPM_ACTRL_BEGIN_WRITE_TXN | FWPM_ACTRL_WRITE | DELETE |
|
|
WRITE_DAC | WRITE_OWNER | GENERIC_ALL | GENERIC_WRITE;
|
|
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 ((ace->Mask & write) && !EqualSid(&ace->SidStart, system) &&
|
|
!EqualSid(&ace->SidStart, admins)) {
|
|
okay = false;
|
|
break;
|
|
}
|
|
} else if (header->AceType != ACCESS_DENIED_ACE_TYPE) {
|
|
okay = false;
|
|
break;
|
|
}
|
|
}
|
|
if (descriptor)
|
|
FwpmFreeMemory0(reinterpret_cast<void **>(&descriptor));
|
|
return okay;
|
|
}
|
|
struct Rule {
|
|
GUID key, layer;
|
|
FWP_ACTION_TYPE action;
|
|
UINT64 weight;
|
|
std::vector<ExpectedCondition> conditions;
|
|
};
|
|
static bool matches(const FWPM_FILTER0 &actual, const Rule &rule,
|
|
const GUID &provider, const GUID &sublayer) {
|
|
if (!actual.providerKey || !IsEqualGUID(*actual.providerKey, provider) ||
|
|
!IsEqualGUID(actual.subLayerKey, sublayer) ||
|
|
!IsEqualGUID(actual.layerKey, rule.layer) ||
|
|
actual.action.type != rule.action ||
|
|
actual.flags != FWPM_FILTER_FLAG_PERSISTENT ||
|
|
actual.numFilterConditions != rule.conditions.size() ||
|
|
!actual.filterCondition)
|
|
return false;
|
|
if (rule.weight) {
|
|
if (actual.weight.type != FWP_UINT64 || !actual.weight.uint64 ||
|
|
*actual.weight.uint64 != rule.weight)
|
|
return false;
|
|
} else if (actual.weight.type != FWP_EMPTY)
|
|
return false;
|
|
for (size_t i = 0; i < rule.conditions.size(); ++i) {
|
|
const auto &e = rule.conditions[i];
|
|
const auto &a = actual.filterCondition[i];
|
|
if (!IsEqualGUID(a.fieldKey, e.field) || a.matchType != e.match ||
|
|
a.conditionValue.type != e.type)
|
|
return false;
|
|
switch (e.type) {
|
|
case FWP_UINT8:
|
|
if (a.conditionValue.uint8 != e.value)
|
|
return false;
|
|
break;
|
|
case FWP_UINT16:
|
|
if (a.conditionValue.uint16 != e.value)
|
|
return false;
|
|
break;
|
|
case FWP_UINT32:
|
|
if (a.conditionValue.uint32 != e.value)
|
|
return false;
|
|
break;
|
|
case FWP_BYTE_BLOB_TYPE:
|
|
if (!blobEqual(a.conditionValue.byteBlob, e.bytes))
|
|
return false;
|
|
break;
|
|
case FWP_SECURITY_DESCRIPTOR_TYPE:
|
|
if (!blobEqual(a.conditionValue.sd, e.bytes))
|
|
return false;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
static std::vector<BYTE> appId(const std::wstring &path) {
|
|
Memory memory;
|
|
FWP_BYTE_BLOB *blob = nullptr;
|
|
if (FwpmGetAppIdFromFileName0(path.c_str(), &blob))
|
|
throw 1;
|
|
memory.p = blob;
|
|
if (!blob || !blob->data || blob->size < 8 || blob->size > 65536)
|
|
throw 1;
|
|
return {blob->data, blob->data + blob->size};
|
|
}
|
|
static Result inspect(const std::wstring &root,
|
|
const std::wstring &serviceSid) {
|
|
Result result;
|
|
try {
|
|
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
|
|
if (!manager) {
|
|
result.code = GetLastError();
|
|
return result;
|
|
}
|
|
SC_HANDLE bfe = OpenServiceW(manager, L"BFE", SERVICE_QUERY_STATUS);
|
|
SERVICE_STATUS_PROCESS status{};
|
|
DWORD needed = 0;
|
|
result.bfeRunning = bfe &&
|
|
QueryServiceStatusEx(bfe, SC_STATUS_PROCESS_INFO,
|
|
reinterpret_cast<BYTE *>(&status),
|
|
sizeof(status), &needed) &&
|
|
status.dwCurrentState == SERVICE_RUNNING;
|
|
if (bfe)
|
|
CloseServiceHandle(bfe);
|
|
CloseServiceHandle(manager);
|
|
if (!result.bfeRunning)
|
|
return result;
|
|
const GUID provider = guid(L"{2a7bcbf2-e4f5-4e94-bd33-d5442a3bef50}"),
|
|
sub = guid(L"{403a69b8-fc7c-460a-a247-72e0d72e6bf8}");
|
|
const GUID c4 = FWPM_LAYER_ALE_AUTH_CONNECT_V4,
|
|
c6 = FWPM_LAYER_ALE_AUTH_CONNECT_V6,
|
|
r4 = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
|
|
r6 = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
|
|
const auto browser = appId(root + L"\\SafetyNet.exe"),
|
|
broker = appId(root + L"\\resources\\broker\\node.exe");
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
ULONG size = 0;
|
|
std::wstring sddl = L"D:(A;;CC;;;" + serviceSid + L")";
|
|
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
sddl.c_str(), SDDL_REVISION_1, &descriptor, &size))
|
|
throw 1;
|
|
std::vector<BYTE> sd(reinterpret_cast<BYTE *>(descriptor),
|
|
reinterpret_cast<BYTE *>(descriptor) + size);
|
|
LocalFree(descriptor);
|
|
auto scalar = [](GUID field, FWP_MATCH_TYPE match, FWP_DATA_TYPE type,
|
|
UINT32 value) {
|
|
return ExpectedCondition{field, match, type, value, {}};
|
|
};
|
|
auto bytes = [](GUID field, FWP_DATA_TYPE type,
|
|
const std::vector<BYTE> &value) {
|
|
return ExpectedCondition{field, FWP_MATCH_EQUAL, type, 0, value};
|
|
};
|
|
const auto app = bytes(FWPM_CONDITION_ALE_APP_ID, FWP_BYTE_BLOB_TYPE,
|
|
browser),
|
|
tcp = scalar(FWPM_CONDITION_IP_PROTOCOL, FWP_MATCH_EQUAL,
|
|
FWP_UINT8, 6),
|
|
local = scalar(FWPM_CONDITION_IP_REMOTE_ADDRESS, FWP_MATCH_EQUAL,
|
|
FWP_UINT32, 0x7f000001);
|
|
const std::vector<ExpectedCondition> tuple = {
|
|
tcp,
|
|
scalar(FWPM_CONDITION_IP_LOCAL_ADDRESS, FWP_MATCH_EQUAL, FWP_UINT32,
|
|
0x7f000001),
|
|
scalar(FWPM_CONDITION_IP_LOCAL_PORT, FWP_MATCH_EQUAL, FWP_UINT16,
|
|
BROKER_PORT)};
|
|
auto permit = tuple;
|
|
permit.push_back(
|
|
bytes(FWPM_CONDITION_ALE_APP_ID, FWP_BYTE_BLOB_TYPE, broker));
|
|
permit.push_back(
|
|
bytes(FWPM_CONDITION_ALE_USER_ID, FWP_SECURITY_DESCRIPTOR_TYPE, sd));
|
|
const std::vector<Rule> rules = {
|
|
{guid(L"{4ac4e1d1-f08b-50fe-83fe-1872672c42b9}"),
|
|
c4,
|
|
FWP_ACTION_BLOCK,
|
|
0,
|
|
{app, scalar(FWPM_CONDITION_IP_PROTOCOL, FWP_MATCH_NOT_EQUAL,
|
|
FWP_UINT8, 6)}},
|
|
{guid(L"{07130743-8828-58b5-9476-3a0c2a001796}"),
|
|
c4,
|
|
FWP_ACTION_BLOCK,
|
|
0,
|
|
{app, tcp,
|
|
scalar(FWPM_CONDITION_IP_REMOTE_ADDRESS, FWP_MATCH_NOT_EQUAL,
|
|
FWP_UINT32, 0x7f000001)}},
|
|
{guid(L"{51401be2-30ce-5808-878f-7e181d0f6fde}"),
|
|
c4,
|
|
FWP_ACTION_BLOCK,
|
|
0,
|
|
{app, tcp, local,
|
|
scalar(FWPM_CONDITION_IP_REMOTE_PORT, FWP_MATCH_NOT_EQUAL, FWP_UINT16,
|
|
BROKER_PORT)}},
|
|
{guid(L"{1ee3c54d-84f8-5461-bd50-b74f32335ff3}"),
|
|
c6,
|
|
FWP_ACTION_BLOCK,
|
|
0,
|
|
{app}},
|
|
{guid(L"{a71f6123-ef64-5836-90ee-f624c6841f18}"),
|
|
r4,
|
|
FWP_ACTION_BLOCK,
|
|
0,
|
|
{app}},
|
|
{guid(L"{e537682c-948d-5bbf-a231-5db3ba896eda}"),
|
|
r6,
|
|
FWP_ACTION_BLOCK,
|
|
0,
|
|
{app}},
|
|
{guid(L"{dcbbb5f1-5936-5978-8349-21a2e791ffbf}"), r4, FWP_ACTION_PERMIT,
|
|
2, permit},
|
|
{guid(L"{7c4dba0f-904a-5685-a3ed-ae774a45136a}"), r4, FWP_ACTION_BLOCK,
|
|
1, tuple}};
|
|
Engine engine;
|
|
result.code = FwpmEngineOpen0(nullptr, RPC_C_AUTHN_WINNT, nullptr, nullptr,
|
|
&engine.h);
|
|
if (result.code)
|
|
return result;
|
|
Enumeration enumeration(engine.h);
|
|
Memory providerMemory;
|
|
FWPM_PROVIDER0 *p = nullptr;
|
|
result.code = FwpmProviderGetByKey0(engine.h, &provider, &p);
|
|
providerMemory.p = p;
|
|
if (result.code)
|
|
return result;
|
|
if (p->flags != FWPM_PROVIDER_FLAG_PERSISTENT || !p->serviceName ||
|
|
wcscmp(p->serviceName, L"SafetyNetNetworkBroker") ||
|
|
!marker(p->providerData) ||
|
|
!protectedObject(engine.h, provider,
|
|
FwpmProviderGetSecurityInfoByKey0)) {
|
|
result.code = ERROR_INVALID_DATA;
|
|
return result;
|
|
}
|
|
result.code =
|
|
FwpmFilterCreateEnumHandle0(engine.h, nullptr, &enumeration.h);
|
|
if (result.code)
|
|
return result;
|
|
std::vector<GUID> seen;
|
|
for (;;) {
|
|
Memory memory;
|
|
FWPM_FILTER0 **entries = nullptr;
|
|
UINT32 count = 0;
|
|
result.code =
|
|
FwpmFilterEnum0(engine.h, enumeration.h, 128, &entries, &count);
|
|
memory.p = entries;
|
|
if (result.code)
|
|
return result;
|
|
if (count > 128 || result.accessibleFilters + count > 65536) {
|
|
result.code = ERROR_MORE_DATA;
|
|
return result;
|
|
}
|
|
result.accessibleFilters += count;
|
|
for (UINT32 i = 0; i < count; ++i) {
|
|
const auto &f = *entries[i];
|
|
if (IsEqualGUID(f.subLayerKey, sub) ||
|
|
(f.providerKey && IsEqualGUID(*f.providerKey, provider))) {
|
|
bool okay = false;
|
|
for (const auto &rule : rules)
|
|
if (IsEqualGUID(f.filterKey, rule.key) &&
|
|
matches(f, rule, provider, sub))
|
|
okay = true;
|
|
if (!okay || !protectedObject(engine.h, f.filterKey,
|
|
FwpmFilterGetSecurityInfoByKey0)) {
|
|
result.code = ERROR_INVALID_DATA;
|
|
return result;
|
|
}
|
|
seen.push_back(f.filterKey);
|
|
++result.ownedFilters;
|
|
} else if (IsEqualGUID(f.layerKey, c4) || IsEqualGUID(f.layerKey, c6) ||
|
|
IsEqualGUID(f.layerKey, r4) || IsEqualGUID(f.layerKey, r6)) {
|
|
Memory sm;
|
|
FWPM_SUBLAYER0 *s = nullptr;
|
|
result.code = FwpmSubLayerGetByKey0(engine.h, &f.subLayerKey, &s);
|
|
sm.p = s;
|
|
if (result.code)
|
|
return result;
|
|
if (s->weight == 65535)
|
|
result.collision = true;
|
|
}
|
|
}
|
|
if (count < 128)
|
|
break;
|
|
}
|
|
Memory sm;
|
|
FWPM_SUBLAYER0 *s = nullptr;
|
|
result.code = FwpmSubLayerGetByKey0(engine.h, &sub, &s);
|
|
sm.p = s;
|
|
if (result.code)
|
|
return result;
|
|
result.ownedVerified =
|
|
marker(s->providerData) &&
|
|
protectedObject(engine.h, sub, FwpmSubLayerGetSecurityInfoByKey0) &&
|
|
result.ownedFilters == 8 && !result.collision && s->weight == 65535 &&
|
|
s->providerKey && IsEqualGUID(*s->providerKey, provider) &&
|
|
s->flags == FWPM_SUBLAYER_FLAG_PERSISTENT;
|
|
result.code = result.ownedVerified ? ERROR_SUCCESS : ERROR_INVALID_DATA;
|
|
} catch (...) {
|
|
result.code = ERROR_INVALID_DATA;
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace topology_contract
|