Using the ConfigMgr AdminService to Retrieve BitLocker Recovery Keys and Triggering Key Rotation

The Key to Success is Knowledge

Recently Garth Jones accused me of knowing something that I knew nothing about and I was very offended by that. So much so, that when Bryan Dam came to me demanding to know the keys to BitLocker keys in ConfigMgr, I decided I should figure it out. So I did. Here’s what I know now:

Keying in on the Issue

When trying to automate processes around ConfigMgr, there are Ways to do things then there are Supported Ways to do things. Bryan asked if I knew Supported Ways to extract a Bitlocker recovery key from the ConfigMgr database in a way that marks the key as disclosed and forces the client device to rotate keys. Sure sounded like Garth put him up to this.

The Key to Success

Enter AdminService. You remember that guy right? It’s been a while since I’ve written anything about it and I can honestly say, it has come a LONG way. I may need to update my guide soon. The AdminService is now used as part of the backend that enabled Cloud Attach (formerly Tenant Attach) to integrate into the Microsoft Endpoint Manager Admin Console (we all still just call it Intune :-)).

When you enable Cloud Attach, you get access to ConfigMgr attributes and methods that have always been in-console-only items.

Cloud Attach Resources in the MEM Console
Cloud Attach Resources in the MEM Console

One of these items is the Recovery Keys blade. It allows you to, yep, you guessed it, see BitLocker recovery keys for your ConfigMgr managed devices. When you click the Recovery keys (preview) blade, you will see a list of keys and a link on each one to view the Recovery Key.

RECOVERY KEYS!
RECOVERY KEYS!

When you click on Show recovery key you will be notified that the key will be marked disclosed and that this will trigger a key rotation on the client. This is exactly what we want!

Are you sure you want to trigger key rotation?
Are you sure you want to trigger key rotation?

Look at this beautiful key!
Look at this beautiful key!

What’s great about the AdminService is that it has a verbose log file. You can find the log in the install directory of your SMS Provider server (generally your primary) AdminService.log. If you watch the log as you navigate in the MEM portal and click through to display the recovery key, you will see each call back to the AdminService to retrieve the data which is then displayed in the web console. The logs look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Processing incoming request for resource [https://cm01.asd.net/AdminService/v1.0/Device?$filter=SMSID eq 'GUID:3d03b4dd-2007-47da-af02-83800df961c0'&$select=MachineId,ADSiteName,CNLastOnlineTime,CNLastOfflineTime,CNAccessMP,CurrentLogonUser,CoManaged,CA_IsCompliant,ClientVersion,Domain,IsApproved,IsVirtualMachine,LastPolicyRequest,LastMPServerName,LastActiveTime,DeviceOSBuild,CNIsOnInternet,CNIsOnline,MACAddress,SiteCode], method: [GET], User - [NT AUTHORITY\SYSTEM]
Header: [ServiceNotification]=[**************]
Header: [Authorization]=[**************]
Header: [Host]=[cm01.asd.net]
Context: [RemoteIpAddress]=[fe80::9d59:e444:736c:f5e2%4]
Context: [RemotePort]=[55641]
Context: [ContentType]=[]
Context: [Accept]=[]
Received request from the notification channel.
Successfully validated request from Service Connection Point.
Successfully validated user [10d75920-6cf5-48de-955c-e856de2a39de,S-1-5-21-1909024835-672986419-4090565466-1624] from tenant [**************].
Successfully logged on user using user principal name Adam@ASD.NET.
Provider authentication level and exception list not present or expired. Retrieving from database.
User ASD\Adam is allowed because it is validated with current authentication level Default.
Get all instances of Device.
Completing request with response code [200] reason [OK]

Processing incoming request for resource [https://cm01.asd.net/AdminService/v1.0/Device(16777377)/RecoveryKeys], method: [GET], User - [NT AUTHORITY\SYSTEM]
Header: [ServiceNotification]=[**************]
Header: [Authorization]=[**************]
Header: [Host]=[cm01.asd.net]
Context: [RemoteIpAddress]=[fe80::9d59:e444:736c:f5e2%4]
Context: [RemotePort]=[55646]
Context: [ContentType]=[]
Context: [Accept]=[]
Received request from the notification channel.
Successfully validated request from Service Connection Point.
Successfully validated user [10d75920-6cf5-48de-955c-e856de2a39de,S-1-5-21-1909024835-672986419-4090565466-1624] from tenant [**************].
Successfully logged on user using user principal name Adam@ASD.NET.
Provider authentication level and exception list up to date.
User ASD\Adam is allowed because it is validated with current authentication level Default.
Completing request with response code [200] reason [OK]

Processing incoming request for resource [https://cm01.asd.net/AdminService/v1.0/Device(16777377)/AdminService.GetRecoveryKeyValue], method: [POST], User - [NT AUTHORITY\SYSTEM]
Header: [ServiceNotification]=[**************]
Header: [Content-Length]=[56]
Header: [Content-Type]=[application/json]
Header: [Authorization]=[**************]
Header: [Expect]=[100-continue]
Header: [Host]=[cm01.asd.net]
Context: [RemoteIpAddress]=[fe80::9d59:e444:736c:f5e2%4]
Context: [RemotePort]=[62213]
Context: [ContentType]=[application/json]
Context: [Accept]=[]
Received request from the notification channel.
Successfully validated request from Service Connection Point.
Successfully validated user [10d75920-6cf5-48de-955c-e856de2a39de,S-1-5-21-1909024835-672986419-4090565466-1624] from tenant [**************].
Successfully logged on user using user principal name Adam@ASD.NET.
Provider authentication level and exception list up to date.
User ASD\Adam is allowed because it is validated with current authentication level Default.
Get instance of Device with key '16777377'
User ASD\Adam requests to read value of recovery key 5eb3baec-4e9b-49f4-9aee-90d3554aef05 on device 16777377 (CMCB-WS01).
Completing request with response code [200] reason [OK]

The above log entries show 3 calls to the AdminService and although some of the data is obfuscated, we can piece it all together.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#Using a GET Method:
#Get the Device:
https://cm01.asd.net/AdminService/v1.0/Device?$filter=SMSID eq 'GUID:3d03b4dd-2007-47da-af02-83800df961c0'&$select=MachineId,ADSiteName,CNLastOnlineTime,CNLastOfflineTime,CNAccessMP,CurrentLogonUser,CoManaged,CA_IsCompliant,ClientVersion,Domain,IsApproved,IsVirtualMachine,LastPolicyRequest,LastMPServerName,LastActiveTime,DeviceOSBuild,CNIsOnInternet,CNIsOnline,MACAddress,SiteCode

#Base Device Entity
https://cm01.asd.net/AdminService/v1.0/Device

#ODATA syntax to filter to find the specific device using SMSID and passing in the device GUID
?$filter=SMSID eq 'GUID:3d03b4dd-2007-47da-af02-83800df961c0'

# Select statement to explicitly return device properties. We can exclude this if we just want everything back.
&$select=MachineId,ADSiteName,CNLastOnlineTime,CNLastOfflineTime,CNAccessMP,CurrentLogonUser,CoManaged,CA_IsCompliant,ClientVersion,Domain,IsApproved,IsVirtualMachine,LastPolicyRequest,LastMPServerName,LastActiveTime,DeviceOSBuild,CNIsOnInternet,CNIsOnline,MACAddress,SiteCode
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//Response data. Note that this returned an Array since we used $filter.
{
    "@odata.context": "https://cm01.asd.net/AdminService/v1.0/$metadata#Device(MachineId,ADSiteName,CNLastOnlineTime,CNLastOfflineTime,CNAccessMP,CurrentLogonUser,CoManaged,CA_IsCompliant,ClientVersion,Domain,IsApproved,IsVirtualMachine,LastPolicyRequest,LastMPServerName,LastActiveTime,DeviceOSBuild,CNIsOnInternet,CNIsOnline,MACAddress,SiteCode)",
    "value": [
        {
        "MachineId": 16777377,
        "ADSiteName": "Houston",
        "CNLastOnlineTime": "2021-11-05T21:49:33.287Z",
        "CNLastOfflineTime": "2021-11-06T01:58:14.697Z",
        "CNAccessMP": "CM01.asd.net",
        "CurrentLogonUser": null,
        "CoManaged": 1,
        "CA_IsCompliant": 0,
        "ClientVersion": "5.00.9058.1018",
        "Domain": "ASD",
        "IsApproved": 3,
        "IsVirtualMachine": true,
        "LastPolicyRequest": "2021-11-06T00:32:02Z",
        "LastMPServerName": "CM01.ASD.NET",
        "LastActiveTime": "2021-11-06T00:32:02Z",
        "DeviceOSBuild": "10.0.19043.1288",
        "CNIsOnInternet": false,
        "CNIsOnline": false,
        "MACAddress": "00:15:5D:01:11:C3",
        "SiteCode": "PS1"
        }
    ]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#Using a GET Method:
#Get the list of recovery key ids for the specific device
https://cm01.asd.net/AdminService/v1.0/Device(16777377)/RecoveryKeys

#Base Device Entity
https://cm01.asd.net/AdminService/v1.0/Device

#Device's ResourceId/MachineId from the ConfigMgr database.
(16777377)

#RecoveryKeys entity to retrieve the key ids
/RecoveryKeys
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//Response Data
{
    "@odata.context": "https://cm01.asd.net/AdminService/v1.0/$metadata#RecoveryKey",
    "value": [
        {
        "ItemKey": 16777377,
        "RecoveryKeyId": "5eb3baec-4e9b-49f4-9aee-90d3554aef05",
        "VolumeTypeId": 1
        }
    ]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#Using a POST Method:
#Post data to the GetRecoveryKeyValue action/function to return the recovery key values
https://cm01.asd.net/AdminService/v1.0/Device(16777377)/AdminService.GetRecoveryKeyValue

#Base Device Entity
https://cm01.asd.net/AdminService/v1.0/Device

#Device's ResourceId/MachineId from the ConfigMgr database.
(16777377)

#GetRecoveryKeyValue Action/function to submit a body with the recovery id key from the previous call.
/AdminService.GetRecoveryKeyValue

#Not shown in the logs - since this is a POST method, we need to send in JSON a body. The format for the body is found in the metadata XML I'll show later in the post.

{
    "RecoveryKeyId" : "5eb3baec-4e9b-49f4-9aee-90d3554aef05"
}

#Response Data
{
    "value":"161964-088066-081752-251955-663949-379379-473033-388113"
}

From a client machine, this is equivalent to running manage-bde -protectors -get c: and retrieving the Numerical Password ID and Password as shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
C:\Windows\system32>manage-bde -protectors -get c:
BitLocker Drive Encryption: Configuration Tool version 10.0.19041
Copyright (C) 2013 Microsoft Corporation. All rights reserved.

Volume C: []
All Key Protectors

    Numerical Password:
      ID: {5EB3BAEC-4E9B-49F4-9AEE-90D3554AEF05}
      Password:
        161964-088066-081752-251955-663949-379379-473033-388113

    TPM:
      ID: {0884EEB4-3DE1-46BD-A6D6-258C70714B88}
      PCR Validation Profile:
        7, 11
        (Uses Secure Boot for integrity validation)

The Key to Knowledge is Understanding

As I’ve mentioned in previous posts, you can view the metadata XML for the AdminService by using the following url:

https://cm01.asd.net/AdminService/v1.0/$metadata

The part we care about is this (From CB 2107):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<EntityType Name="RecoveryKey">
    <Key>
        <PropertyRef Name="ItemKey"/>
        <PropertyRef Name="RecoveryKeyId"/>
    </Key>
    <Property Name="ItemKey" Type="Edm.Int32" Nullable="false"/>
    <Property Name="RecoveryKeyId" Type="Edm.String" Nullable="false"/>
    <Property Name="VolumeTypeId" Type="Edm.Byte" Nullable="false"/>
    <NavigationProperty Name="Device" Type="Microsoft.ConfigurationManager.ObjectLibrary.Models.Device" Nullable="false">
        <ReferentialConstraint Property="ItemKey" ReferencedProperty="MachineId"/>
    </NavigationProperty>
</EntityType>

Take note of the NavigationProperty above. This indicates that we can retrieve the recovery key info FROM another entity, in this case, from a Device entity. If we scroll a little more we will see the Action or Function that we want to call GetRecoveryKeyValue

1
2
3
4
5
<Action Name="GetRecoveryKeyValue" IsBound="true">
    <Parameter Name="bindingParameter" Type="Microsoft.ConfigurationManager.ObjectLibrary.Models.Device"/>
    <Parameter Name="RecoveryKeyId" Type="Edm.Guid" Nullable="false"/>
    <ReturnType Type="Edm.String" Unicode="false"/>
</Action>

From this snippet we can see that we call this action on a Device entity bindingParameter and have to pass in a body with parameters RecoveryKeyId. This is the bit that translates to these lines from above:

1
2
3
4
5
https://cm01.asd.net/AdminService/v1.0/Device(16777377)/AdminService.GetRecoveryKeyValue

{
    "RecoveryKeyId" : "5eb3baec-4e9b-49f4-9aee-90d3554aef05"
}

Using Fiddler, you can trigger this command to test it out.

Fiddler doing some coolness!
2021-11-17_19-25-58.png

There can only be one Master Key or is it Key Master?

For this next part I was going to show you how to watch the client rotate the key, but it turns out that my lab’s MBAM instance may no longer be in existence, so instead I’ll direct you over to the always thorough and informative, Niall Brady!

Have you Recovered from all of the Key puns yet?

So after digging through all of these thing we have all of the pieces we need to write script, or build this into an enterprise-class 3rd party product so people can leverage it other places…

I’ve uploaded a copy of my full example script to my GitHub repo, but here are the basics in PowerShell.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#The script assumes you have the resource ID.
param(
    [string]$ServerName
    [int]$ResourceID
)

#Get the Device
$Device = Invoke-RestMethod -Uri "https://$($ServerName)/AdminService/v1.0/Device($($ResourceID))" -Method Get -UseDefaultCredentials

#Get the recovery key IDs
$KeyIDs = Invoke-RestMethod -Uri "https://$($ServerName)/AdminService/v1.0/Device($($ResourceID))/RecoveryKeys" -Method Get -UseDefaultCredentials

#Loop through the Ids and return the Recovery Keys
$KeyIDs.value | ForEach-Object {
    $Body = @{RecoveryKeyId = $_.RecoveryKeyId} | ConvertTo-Json
    $Keys = Invoke-RestMethod -Uri "https://$($ServerName)/AdminService/v1.0/Device($($ResourceID))/AdminService.GetRecoveryKeyValue" -Method Post -Body $Body -UseDefaultCredentials -ContentType "application/json" 
    $Keys
}

#Result
#value
#-----
#161964-088066-081752-251955-663949-379379-473033-388113

KEYping up is Hard to Do

Well that’s pretty much it. Hope you learned something new, I sure did. Thanks Bryan and Garth for the inspiration to give it go. Hope to see you at MMSMOA in May 2022!