From bd3e775e67dbc72a24723e4ab6af3268d9af6b3a Mon Sep 17 00:00:00 2001
From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com>
Date: Thu, 21 Mar 2024 15:09:05 -0400
Subject: [PATCH] Windows MDM Fix Manual Detection (#17721)
#15565
Replace the use of the isFederated registry key with a keys that check
for AAD (Azure Active Directory, now Entra ID)
Federated enrollment (`isFederated`) seems to be when windows uses a
Discovery MDM endpoint to get its policy and management endpoint
configuration. This is always the case when a client is enrolled with
fleet, so installations always show up as automatic.
It's being replaced by a different key, `AADResourceID`, which appears
to identify the resource that controls the automated deployment. In my
tests it only appears to be populated when the computer is enrolled
through automated deployments. This key appears on both Windows 10 and
11.
There is a similar key, `AADTenantID`, which appears to identify the
client (tenant) to the Azure cloud. I haven't seen this ID in our
systems, so it is likely exclusively used in Azure. Both this key and
`AADResourceID` seem to always be set at the same time, so we only
check for the `AADResourceID`.
I've also added documentation on the registry keys I've analyzed for future reference.
---
changes/15565-windows-automatic-enrollment | 1 +
cmd/osquery-perf/agent.go | 4 +-
.../windows-mdm-glossary-and-protocol.md | 84 ++++++++++++++++++-
docs/Using Fleet/Understanding-host-vitals.md | 8 +-
server/service/osquery_utils/queries.go | 8 +-
server/service/osquery_utils/queries_test.go | 24 +++---
6 files changed, 106 insertions(+), 23 deletions(-)
create mode 100644 changes/15565-windows-automatic-enrollment
diff --git a/changes/15565-windows-automatic-enrollment b/changes/15565-windows-automatic-enrollment
new file mode 100644
index 0000000000..a89e709468
--- /dev/null
+++ b/changes/15565-windows-automatic-enrollment
@@ -0,0 +1 @@
+- Fix a bug where all Windows MDM enrollments were detected as automatic
diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go
index 719b59b23c..756f6f0cd5 100644
--- a/cmd/osquery-perf/agent.go
+++ b/cmd/osquery-perf/agent.go
@@ -1514,12 +1514,12 @@ func (a *agent) mdmWindows() []map[string]string {
if !a.mdmEnrolled() {
return []map[string]string{
// empty service url means not enrolled
- {"is_federated": "0", "discovery_service_url": "", "provider_id": "", "installation_type": "Client"},
+ {"aad_resource_id": "", "discovery_service_url": "", "provider_id": "", "installation_type": "Client"},
}
}
return []map[string]string{
{
- "is_federated": "0",
+ "aad_resource_id": "",
"discovery_service_url": a.serverAddress,
"provider_id": fleet.WellKnownMDMFleet,
"installation_type": "Client",
diff --git a/docs/Contributing/windows-mdm-glossary-and-protocol.md b/docs/Contributing/windows-mdm-glossary-and-protocol.md
index 03d5283a48..94dc6095db 100644
--- a/docs/Contributing/windows-mdm-glossary-and-protocol.md
+++ b/docs/Contributing/windows-mdm-glossary-and-protocol.md
@@ -57,6 +57,88 @@ The certificate created through the WSTEP process is used to authenticate mTLS b
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mdm/33769a92-ac31-47ef-ae7b-dc8501f7104f
+## MDM Device Registration Summary
+
+https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrd/296ebf70-bd4b-489e-a531-460d8ef7519b
+
+# Registry
+
+- `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Enrollments\`
+ Each enrollment gets its own subdirectory with a UUID as a key,
+ inside each directory is a set of keys associated with that enrollment
+ - `CurCryptoProvider`
+ Often `Microsoft Software Key Storage Provider`
+ Cryptographic Key storage provider
+ - `CurKeyContainer`
+ Key within key provider
+ - `DiscoveryServiceFullURL`
+ MDM Discovery service URL
+ - `DMPCertThumbPrint`
+ According to [this blog post](https://call4cloud.nl/2022/10/fullmetal-certificate-the-revenge-of-renewal/), this is the thumbprint of your MDM device certificate
+ - `EnrollmentFlags`
+ See [this link](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xcep/cd22d3a0-f469-4a44-95ed-d10ce4dc2063) for details
+
+ | Integer value | Meaning |
+ |---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+ | 0x00000001 | Instructs the client and CA to include an S/MIME extension, as specified in [RFC4262]. |
+ | 0x00000008 | Instructs the CA to append the issued certificate to the userCertificate attribute, on the user object in Active Directory. |
+ | 0x00000010 | Instructs the CA to check the user's userCertificate attribute in Active Directory, as specified in [RFC4523], for valid certificates that match the template enrolled for. |
+ | 0x00000040 | This flag instructs clients to sign the renewal request using the private key of the existing certificate. For more information, see [MS-WCCE] section 3.2.2.6.2.1.4.5.6. This flag also instructs the CA to process the renewal requests as specified in [MS-WCCE] section 3.2.2.6.2.1.4.5.6. |
+ | 0x00000100 | Instructs the client to get a user's consent before attempting to enroll for a certificate based on the specified template. |
+ | 0x00000400 | Instructs the client to delete any expired, revoked, or renewed certificate from the user's certificate stores. |
+ | 0x00002000 | This flag instructs the client to reuse the private key for a smart card–based certificate renewal if it is unable to create a new private key on the card. |
+ - `EnrollmentState`
+ The best documentation we can find is [here](https://learn.microsoft.com/en-us/graph/api/resources/intune-shared-enrollmentstate?view=graph-rest-beta)
+
+ | Member | Value | Description |
+ |--------------|-------|--------------------------------------------------------------------------------------------------------------------|
+ | unknown | 0 | Device enrollment state is unknown |
+ | enrolled | 1 | Device is Enrolled. |
+ | pendingReset | 2 | Enrolled but it's enrolled via enrollment profile and the enrolled profile is different from the assigned profile. |
+ | failed | 3 | Not enrolled and there is enrollment failure record. |
+ | notContacted | 4 | Device is imported but not enrolled. |
+ | blocked | 5 | Device is enrolled as userless, but is blocked from moving to user enrollment because the app failed to install. |
+
+ - `EnrollmentType`
+ According to [this PDF](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-MDE2/%5BMS-MDE2%5D.pdf) it can have three different values.
+
+ Device, Full, and AppManaged
+
+ From what I've seen, value 6 on AAD, 1 on manual
+ - `isFederated`
+ According to [this web page](https://learn.microsoft.com/en-us/windows/client-management/federated-authentication-device-enrollment), being federated means that the MDM
+ endpoints and details were fetched from a Discovery endpoint,
+ instead of being manually installed. The page does not make mention
+ of the specific registry key, but we are making an assumption that
+ it means the same thing.
+ - `ProviderID`
+ Set during enrollment. In our case it's the word "Fleet".
+ - `RenewalPeriod`
+ Set during enrollment. Period to renew WSTEP certificate.
+ - `RenewErrorCode`
+ Presumably set if there is an error renewing WSTEP certificate.
+ - `RenewROBOSupport`
+ According to [this post](https://call4cloud.nl/2022/10/fullmetal-certificate-the-revenge-of-renewal/) this means "Request On Behalf Of".
+ It seems to have to do with automatic certificate renewal
+ - `RenewStatus`
+ Status of the renewal
+ - `RenewTimestamp`
+ Presumably the timestamp of the last renewal
+ - `RootCertThumbPrint`
+ The thumbprint of the WSTEP root certificate
+ - `SID`
+ Security Identifier
+ - `UPN`
+ User Principal Name of the user that enrolled the device
+ - `AADResourceID`
+ Appears to be the domain of the server managing the enrollment,
+ always appears to be present on machines enrolled through Microsoft
+ Entra (Azure Active Directory)
+ - `AADTenantID`
+ Also related to Azure Active Directory, and also appears to be
+ present at the same time as AADResourceID.
+- `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\Diagnostics\AutoPilot`
+ Autopilot provisioning diagnostic data
-
\ No newline at end of file
+
diff --git a/docs/Using Fleet/Understanding-host-vitals.md b/docs/Using Fleet/Understanding-host-vitals.md
index 10cc3f8548..47dca9f92c 100644
--- a/docs/Using Fleet/Understanding-host-vitals.md
+++ b/docs/Using Fleet/Understanding-host-vitals.md
@@ -176,10 +176,10 @@ WITH registry_keys AS (
enrollment_info AS (
SELECT
MAX(CASE WHEN name = 'UPN' THEN data END) AS upn,
- MAX(CASE WHEN name = 'IsFederated' THEN data END) AS is_federated,
MAX(CASE WHEN name = 'DiscoveryServiceFullURL' THEN data END) AS discovery_service_url,
MAX(CASE WHEN name = 'ProviderID' THEN data END) AS provider_id,
- MAX(CASE WHEN name = 'EnrollmentState' THEN data END) AS state
+ MAX(CASE WHEN name = 'EnrollmentState' THEN data END) AS state,
+ MAX(CASE WHEN name = 'AADResourceID' THEN data END) AS aad_resource_id
FROM registry_keys
GROUP BY key
),
@@ -190,7 +190,7 @@ WITH registry_keys AS (
LIMIT 1
)
SELECT
- e.is_federated,
+ e.aad_resource_id,
e.discovery_service_url,
e.provider_id,
i.installation_type
@@ -374,7 +374,7 @@ SELECT * FROM os_version LIMIT 1
- Query:
```sql
SELECT os.name, r.data as display_version, k.version
- FROM
+ FROM
registry r,
os_version os,
kernel_info k
diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go
index 27ec3932db..030294f5ed 100644
--- a/server/service/osquery_utils/queries.go
+++ b/server/service/osquery_utils/queries.go
@@ -473,10 +473,10 @@ var extraDetailQueries = map[string]DetailQuery{
enrollment_info AS (
SELECT
MAX(CASE WHEN name = 'UPN' THEN data END) AS upn,
- MAX(CASE WHEN name = 'IsFederated' THEN data END) AS is_federated,
MAX(CASE WHEN name = 'DiscoveryServiceFullURL' THEN data END) AS discovery_service_url,
MAX(CASE WHEN name = 'ProviderID' THEN data END) AS provider_id,
- MAX(CASE WHEN name = 'EnrollmentState' THEN data END) AS state
+ MAX(CASE WHEN name = 'EnrollmentState' THEN data END) AS state,
+ MAX(CASE WHEN name = 'AADResourceID' THEN data END) AS aad_resource_id
FROM registry_keys
GROUP BY key
),
@@ -487,7 +487,7 @@ var extraDetailQueries = map[string]DetailQuery{
LIMIT 1
)
SELECT
- e.is_federated,
+ e.aad_resource_id,
e.discovery_service_url,
e.provider_id,
i.installation_type
@@ -1612,7 +1612,7 @@ func directIngestMDMWindows(ctx context.Context, logger log.Logger, host *fleet.
serverURL := data["discovery_service_url"]
if serverURL != "" {
enrolled = true
- if isFederated := data["is_federated"]; isFederated == "1" {
+ if data["aad_resource_id"] != "" {
// NOTE: We intentionally nest this condition to eliminate `enrolled == false && automatic == true`
// as a possible status for Windows hosts (which would be otherwise be categorized as
// "Pending"). Currently, the "Pending" status is supported only for macOS hosts.
diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go
index 022c00aa7d..4a6da7a2ad 100644
--- a/server/service/osquery_utils/queries_test.go
+++ b/server/service/osquery_utils/queries_test.go
@@ -692,7 +692,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "",
- "is_federated": "1",
+ "aad_resource_id": "https://example.com",
"provider_id": "Some_ID",
"installation_type": "Client",
},
@@ -703,7 +703,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
wantServerURL: "",
},
{
- name: "off missing is_federated and server url",
+ name: "off missing aad_resource_id and server url",
data: []map[string]string{
{
"provider_id": "Some_ID",
@@ -728,7 +728,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://example.com",
- "is_federated": "1",
+ "aad_resource_id": "https://example.com",
"provider_id": "Some_ID",
"installation_type": "Client",
},
@@ -743,7 +743,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://example.com",
- "is_federated": "0",
+ "aad_resource_id": "",
"provider_id": "Local_Management",
"installation_type": "Client",
},
@@ -754,7 +754,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
wantServerURL: "https://example.com",
},
{
- name: "on manual missing is_federated",
+ name: "on manual missing aad_resource_id",
data: []map[string]string{
{
"discovery_service_url": "https://example.com",
@@ -772,7 +772,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://example.com",
- "is_federated": "1",
+ "aad_resource_id": "https://example.com",
"provider_id": "Some_ID",
"installation_type": "Windows SeRvEr 99.9",
},
@@ -790,7 +790,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://jumpcloud.com",
- "is_federated": "0",
+ "aad_resource_id": "",
"provider_id": "Local_Management",
"installation_type": "Client",
},
@@ -806,7 +806,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://airwatch.com",
- "is_federated": "0",
+ "aad_resource_id": "",
"provider_id": "Local_Management",
"installation_type": "Client",
},
@@ -822,7 +822,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://awmdm.com",
- "is_federated": "0",
+ "aad_resource_id": "",
"provider_id": "Local_Management",
"installation_type": "Client",
},
@@ -838,7 +838,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://microsoft.com",
- "is_federated": "0",
+ "aad_resource_id": "",
"provider_id": "Local_Management",
"installation_type": "Client",
},
@@ -854,7 +854,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://fleetdm.com",
- "is_federated": "0",
+ "aad_resource_id": "",
"provider_id": "Local_Management",
"installation_type": "Client",
},
@@ -871,7 +871,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
data: []map[string]string{
{
"discovery_service_url": "https://myinstall.local",
- "is_federated": "0",
+ "aad_resource_id": "",
"provider_id": "Fleet",
"installation_type": "Client",
},