Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 107 additions & 3 deletions src/cli/activation_transaction.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#define WIN32_LEAN_AND_MEAN
#endif
#include <aclapi.h>
#include <ntsecapi.h>
#include <sddl.h>
#include <windows.h>
#else
Expand Down Expand Up @@ -424,6 +425,102 @@ static void activation_windows_security_destroy(activation_windows_security_t *s
memset(security, 0, sizeof(*security));
}

/* #1705: THIS machine's built-in Administrator ACCOUNT (RID 500 under the local
* machine account-domain SID, S-1-5-21-<machine>-500) is a trusted owner/grantee,
* mirroring the daemon's win_sid_trusted (src/daemon/ipc.c). It is resolved via
* LSA (the local machine account-domain SID) plus CreateWellKnownSid and compared
* with EqualSid. It is deliberately NOT IsWellKnownSid(WinAccountAdministratorSid)
* and NOT a trailing-RID-500 test: both accept a FOREIGN S-1-5-21-*-500 (a domain
* admin, or another machine's built-in Administrator), opening a cross-machine
* bypass. Resolved once and cached for the process; any LSA or synthesis failure
* caches "none" and grants NO tolerance (fail closed). advapi32 is already loaded
* (this file calls GetSecurityInfo etc.), so the functions are resolved from its
* module handle with no new import. CLI activation is single-threaded by
* contract, so the cache needs no lock. */
typedef NTSTATUS(NTAPI *activation_lsa_open_policy_fn)(PLSA_UNICODE_STRING, PLSA_OBJECT_ATTRIBUTES,
ACCESS_MASK, PLSA_HANDLE);
typedef NTSTATUS(NTAPI *activation_lsa_query_information_policy_fn)(LSA_HANDLE,
POLICY_INFORMATION_CLASS,
PVOID *);
typedef NTSTATUS(NTAPI *activation_lsa_free_memory_fn)(PVOID);
typedef NTSTATUS(NTAPI *activation_lsa_close_fn)(LSA_HANDLE);
typedef BOOL(WINAPI *activation_create_well_known_sid_fn)(WELL_KNOWN_SID_TYPE, PSID, PSID, DWORD *);

static PSID activation_windows_local_admin_sid(void) {
static bool resolved = false;
static PSID cached = NULL;
if (resolved) {
return cached;
}
resolved = true;
HMODULE advapi = GetModuleHandleW(L"advapi32.dll");
if (!advapi) {
return NULL;
}
activation_lsa_open_policy_fn lsa_open =
(activation_lsa_open_policy_fn)(void (*)(void))GetProcAddress(advapi, "LsaOpenPolicy");
activation_lsa_query_information_policy_fn lsa_query =
(activation_lsa_query_information_policy_fn)(void (*)(void))GetProcAddress(
advapi, "LsaQueryInformationPolicy");
activation_lsa_free_memory_fn lsa_free =
(activation_lsa_free_memory_fn)(void (*)(void))GetProcAddress(advapi, "LsaFreeMemory");
activation_lsa_close_fn lsa_close =
(activation_lsa_close_fn)(void (*)(void))GetProcAddress(advapi, "LsaClose");
activation_create_well_known_sid_fn create_sid =
(activation_create_well_known_sid_fn)(void (*)(void))GetProcAddress(advapi,
"CreateWellKnownSid");
if (!lsa_open || !lsa_query || !lsa_free || !lsa_close || !create_sid) {
return NULL;
}
LSA_OBJECT_ATTRIBUTES attributes;
memset(&attributes, 0, sizeof(attributes));
LSA_HANDLE policy = NULL;
/* STATUS_SUCCESS is 0; any other status is treated as failure (fail closed). */
if (lsa_open(NULL, &attributes, POLICY_VIEW_LOCAL_INFORMATION, &policy) != 0 || !policy) {
return NULL;
}
POLICY_ACCOUNT_DOMAIN_INFO *domain = NULL;
if (lsa_query(policy, PolicyAccountDomainInformation, (PVOID *)&domain) == 0 && domain &&
domain->DomainSid && IsValidSid(domain->DomainSid)) {
DWORD needed = 0;
(void)create_sid(WinAccountAdministratorSid, domain->DomainSid, NULL, &needed);
if (needed > 0U) {
PSID admin = malloc(needed);
if (admin &&
create_sid(WinAccountAdministratorSid, domain->DomainSid, admin, &needed) &&
IsValidSid(admin)) {
cached = admin;
} else {
free(admin);
}
}
}
if (domain) {
(void)lsa_free(domain);
}
(void)lsa_close(policy);
return cached;
}

/* #2023/#1686: an owner refusal must name WHICH owner, not just "status -3, os 0".
* The path is already carried by g_activation_refusal_object; this appends the
* offending owner's SID string, mirroring the daemon's ACL/owner diagnostics, so
* the operator sees the exact identity to remove or the directory to move. */
static void activation_windows_note_untrusted_owner(const char *predicate, PSID owner,
DWORD os_error) {
char label[192];
LPSTR owner_text = NULL;
(void)snprintf(
label, sizeof(label), "%s; owner=%s", predicate,
(owner && IsValidSid(owner) && ConvertSidToStringSidA(owner, &owner_text) && owner_text)
? owner_text
: "unresolved-sid");
if (owner_text) {
(void)LocalFree(owner_text);
}
activation_note_refusal(label, os_error);
}

/* Trusted-owner acceptance for SOURCE-side objects: a downloaded release
* bundle is owned by whatever the machine's default-owner policy dictates
* (Administrators on GitHub-runner-class images). Its integrity is enforced
Expand All @@ -440,11 +537,13 @@ static bool activation_windows_owner_is_trusted(HANDLE handle) {
PSECURITY_DESCRIPTOR descriptor = NULL;
DWORD result = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &owner, NULL,
NULL, NULL, &descriptor);
PSID local_admin = activation_windows_local_admin_sid();
bool trusted = result == ERROR_SUCCESS && owner && IsValidSid(owner) &&
(EqualSid(owner, user_sid) || IsWellKnownSid(owner, WinLocalSystemSid) ||
IsWellKnownSid(owner, WinBuiltinAdministratorsSid));
IsWellKnownSid(owner, WinBuiltinAdministratorsSid) ||
(local_admin && EqualSid(owner, local_admin)));
if (!trusted) {
activation_note_refusal("owner-not-trusted", result);
activation_windows_note_untrusted_owner("owner-not-trusted", owner, result);
}
if (descriptor) {
(void)LocalFree(descriptor);
Expand Down Expand Up @@ -522,7 +621,7 @@ static bool activation_windows_owner_is_current(HANDLE handle) {
}
}
if (!same) {
activation_note_refusal("owner-not-current-user", result);
activation_windows_note_untrusted_owner("owner-not-current-user", owner, result);
}
if (descriptor) {
(void)LocalFree(descriptor);
Expand Down Expand Up @@ -596,10 +695,15 @@ static bool activation_windows_acl_check(HANDLE handle, DWORD tolerated_untruste
PSID sid = (PSID)&ace->SidStart;
size_t sid_capacity = (size_t)header->AceSize - sid_offset;
DWORD sid_length = GetSidLengthRequired(((SID *)sid)->SubAuthorityCount);
PSID local_admin = activation_windows_local_admin_sid();
bool trusted = sid_length <= sid_capacity && IsValidSid(sid) &&
GetLengthSid(sid) == sid_length &&
(EqualSid(sid, user_sid) || IsWellKnownSid(sid, WinLocalSystemSid) ||
IsWellKnownSid(sid, WinBuiltinAdministratorsSid) ||
/* #1705: THIS machine's built-in Administrator (RID-500),
* resolved via LSA — never a foreign S-1-5-21-*-500; same
* tolerance as the daemon's win_sid_trusted. */
(local_admin && EqualSid(sid, local_admin)) ||
/* OWNER RIGHTS modulates whoever owns the object; the
* owner is separately validated in every chain that
* reaches here (same tolerance as the daemon IPC and
Expand Down
104 changes: 104 additions & 0 deletions src/daemon/ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -3561,6 +3561,7 @@ bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t *
#include <windows.h>
#include <aclapi.h>
#include <sddl.h>
#include <ntsecapi.h>
#include <fcntl.h>
#include <io.h>
#include <shlobj.h>
Expand Down Expand Up @@ -4218,17 +4219,120 @@ static bool win_sid_is_trusted_installer(const uint8_t *sid, size_t sid_length)
return true;
}

/* #1705: the built-in Administrator ACCOUNT of THIS machine — RID 500 under the
* local machine's own account-domain SID (S-1-5-21-<machine>-500) — is a
* legitimate owner/grantee of directories an elevated install created, even when
* that account has been renamed or is disabled. It is resolved by asking LSA for
* the local machine account-domain SID and synthesizing its RID-500 SID with
* CreateWellKnownSid, then compared with EqualSid.
*
* It is deliberately NOT tested with IsWellKnownSid(sid, WinAccountAdministratorSid)
* and NOT by matching a trailing RID of 500: BOTH of those accept ANY domain's
* -500 — a domain administrator, or another machine's built-in Administrator —
* which is exactly the cross-machine trust escalation this must never open. Only
* THIS machine's -500 is trusted.
*
* Resolved once per process and cached; any LSA or synthesis failure leaves the
* cache NULL and therefore grants NO tolerance at all (fail closed). advapi32 is
* reached through the already-loaded module handle in win_security_t and the
* function pointers are resolved dynamically, matching this file's SID-API style
* and adding no static import. */
typedef NTSTATUS(NTAPI *lsa_open_policy_fn)(PLSA_UNICODE_STRING, PLSA_OBJECT_ATTRIBUTES,
ACCESS_MASK, PLSA_HANDLE);
typedef NTSTATUS(NTAPI *lsa_query_information_policy_fn)(LSA_HANDLE, POLICY_INFORMATION_CLASS,
PVOID *);
typedef NTSTATUS(NTAPI *lsa_free_memory_fn)(PVOID);
typedef NTSTATUS(NTAPI *lsa_close_fn)(LSA_HANDLE);
typedef BOOL(WINAPI *create_well_known_sid_fn)(WELL_KNOWN_SID_TYPE, PSID, PSID, DWORD *);

static INIT_ONCE g_local_admin_sid_once = INIT_ONCE_STATIC_INIT;
static PSID g_local_admin_sid = NULL; /* process-lifetime cache; NULL => no tolerance */

static BOOL CALLBACK win_resolve_local_admin_sid(PINIT_ONCE once, PVOID parameter, PVOID *context) {
(void)once;
(void)context;
win_security_t *security = (win_security_t *)parameter;
if (!security || !security->advapi) {
return TRUE; /* ran once; cache stays NULL (fail closed) */
}
HMODULE advapi = security->advapi;
lsa_open_policy_fn lsa_open =
(lsa_open_policy_fn)(void (*)(void))GetProcAddress(advapi, "LsaOpenPolicy");
lsa_query_information_policy_fn lsa_query = (lsa_query_information_policy_fn)(void (*)(
void))GetProcAddress(advapi, "LsaQueryInformationPolicy");
lsa_free_memory_fn lsa_free =
(lsa_free_memory_fn)(void (*)(void))GetProcAddress(advapi, "LsaFreeMemory");
lsa_close_fn lsa_close = (lsa_close_fn)(void (*)(void))GetProcAddress(advapi, "LsaClose");
create_well_known_sid_fn create_sid =
(create_well_known_sid_fn)(void (*)(void))GetProcAddress(advapi, "CreateWellKnownSid");
if (!lsa_open || !lsa_query || !lsa_free || !lsa_close || !create_sid) {
return TRUE;
}
LSA_OBJECT_ATTRIBUTES attributes;
memset(&attributes, 0, sizeof(attributes));
LSA_HANDLE policy = NULL;
/* STATUS_SUCCESS is 0; any other status (including informational positives) is
* treated as failure, keeping the outcome fail-closed. */
if (lsa_open(NULL, &attributes, POLICY_VIEW_LOCAL_INFORMATION, &policy) != 0 || !policy) {
return TRUE;
}
POLICY_ACCOUNT_DOMAIN_INFO *domain = NULL;
if (lsa_query(policy, PolicyAccountDomainInformation, (PVOID *)&domain) == 0 && domain &&
domain->DomainSid && security->is_valid_sid(domain->DomainSid)) {
DWORD needed = 0;
(void)create_sid(WinAccountAdministratorSid, domain->DomainSid, NULL, &needed);
if (needed > 0U) {
PSID resolved = malloc(needed);
if (resolved &&
create_sid(WinAccountAdministratorSid, domain->DomainSid, resolved, &needed) &&
security->is_valid_sid(resolved)) {
g_local_admin_sid = resolved;
} else {
free(resolved);
}
}
}
if (domain) {
(void)lsa_free(domain);
}
(void)lsa_close(policy);
return TRUE;
}

static PSID win_local_admin_sid(win_security_t *security) {
if (!security || !security->advapi) {
return NULL;
}
(void)InitOnceExecuteOnce(&g_local_admin_sid_once, win_resolve_local_admin_sid, (PVOID)security,
NULL);
return g_local_admin_sid;
}

static bool win_sid_trusted(win_security_t *security, PSID sid) {
if (!security || !sid || !security->is_valid_sid(sid)) {
return false;
}
DWORD sid_length = security->get_length_sid(sid);
PSID local_admin = win_local_admin_sid(security);
return (sid_length > 0U && security->equal_sid(sid, security->user_sid)) ||
security->is_well_known_sid(sid, WinLocalSystemSid) ||
security->is_well_known_sid(sid, WinBuiltinAdministratorsSid) ||
(local_admin && security->equal_sid(sid, local_admin)) ||
win_sid_is_trusted_installer((const uint8_t *)sid, (size_t)sid_length);
}

#ifdef CBM_ENABLE_TEST_SEAMS
bool cbm_daemon_ipc_win_sid_trusted_for_testing(void *sid) {
win_security_t security;
if (!win_security_init(&security)) {
return false;
}
bool trusted = win_sid_trusted(&security, (PSID)sid);
win_security_destroy(&security);
return trusted;
}
#endif

/* AppContainer identities: package SIDs (S-1-15-2-*) and capability SIDs
* (S-1-15-3-*), under the APP_PACKAGE identifier authority (15).
*
Expand Down
7 changes: 7 additions & 0 deletions src/daemon/ipc.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ const char *cbm_daemon_ipc_validation_detail(void);
#ifdef CBM_ENABLE_TEST_SEAMS
/* #1537: seed the detail so a test can prove the CLI refusal surfaces it. */
void cbm_daemon_ipc_set_validation_detail_for_testing(const char *detail);
#ifdef _WIN32
/* #1705: run the daemon's directory-owner/ACE trust predicate against an
* arbitrary SID, so a test can assert THIS machine's built-in Administrator
* (RID-500) is trusted while a foreign S-1-5-21-*-500 is not. Returns false on
* any setup failure. Windows only. */
bool cbm_daemon_ipc_win_sid_trusted_for_testing(void *sid);
#endif
#ifndef _WIN32
/* #1830 seams (POSIX). Override the single-uid user-namespace overflow uid that
* ancestors may be owned by (active=false restores the real /proc-derived
Expand Down
Loading
Loading