Stealing and faking Azure AD device identities By About Dr Nestori Syynimaa (@DrAzureAD) Published: 2022-02-15 · Archived: 2026-04-05 17:20:19 UTC Introduction Accessing the certificate and keys Device Certificate (dkpub / dkpriv) Transport keys (tkpub / tkpriv) Round 1 Round 2 Decrypting private keys Stealing the device identity Device Certificate and keys Transport keys Detecting Using the stolen device identity Faking device identity Summary References In my previous blog posts I’ve covered details on PRTs, BPRTs, device compliance, and Azure AD device join. https://aadinternals.com/post/deviceidentity/ Page 1 of 19 In this blog, I’ll show how to steal identities of existing Azure AD joined devices, and how to fake identies of non-AAD joined Windows devices with AADInternals v0.6.6. Introduction As described in my earlier blog post, when the device is joined or registered to AAD, two set of keys are created. These key sets are Device key (dkpub/dkpriv) and Transport key (tkpub/tkpriv). Both public keys (dkpub and tkpub) are sent to Azure AD. Public and private keys are stored in the device, either on disk (encrypted with DPAPI) or in TPM. Thanks to tools like Mimikatz, I knew that those keys could be exported from the devices! However, this requires two things: The target computer is NOT using TPM The attacker has local admin permissions to target computer Accessing the certificate and keys The first task of the journey was to find out is it really possible to export the keys. To do that, I needed to find the keys! Luckily, Microsoft have a great document showing the locations of keys. Microsoft legacy CryptoAPI CSP: Key type Directories User private %APPDATA%\Microsoft\Crypto\RSA\User SID\ %APPDATA%\Microsoft\Crypto\DSS\User SID Local system private %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-18\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-18 Local service private %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-19\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-19 Network service private %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-20\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-20 Shared private %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\MachineKeys Microsoft Cryptography Next Generation (CNG): https://aadinternals.com/post/deviceidentity/ Page 2 of 19 Key type Directory User private %APPDATA%\Microsoft\Crypto\Keys Local system private %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\SystemKeys Local service private %WINDIR%\ServiceProfiles\LocalService Network service private %WINDIR%\ServiceProfiles\NetworkService Shared private %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\Keys Device Certificate (dkpub / dkpriv) I already knew that the Device Certificate of Azure AD joined computer is located in Personal store of Local Computer. The subject of that certificate matches the Device Id of that device. https://aadinternals.com/post/deviceidentity/ Page 3 of 19 There are other device related information stored to the certificate in Object Identifiers (OIDs). The Device Registration (DRS) protocol documentation has a list of some of them, but not all, so I had to do some research on those too. Here is what I found: OID Value type Value 1.2.840.113556.1.5.284.2 Guid DeviceId 1.2.840.113556.1.5.284.3 Guid ObjectId 1.2.840.113556.1.5.284.5 Guid TenantId 1.2.840.113556.1.5.284.7 String Join type: 0 = registered 1 = joined 1.2.840.113556.1.5.284.8 String Tenant region: AF = Africa AS = Asia AP = Australia/Pasific EU = Europe ME = Middle East NA = North America SA = South America The OID values are DER encoded. The first byte 0x04 means BITSTRING, and the second byte the length of length in bytes (0x80 = LENGTH, 0x01 = one byte, 0x80+0x01=0x81). The third is the length of the data in bytes, and the remaining bytes the actual data. For instance, the tenant id is just a byte array presentation of guid object, where the bytes are grouped differently: https://aadinternals.com/post/deviceidentity/ Page 4 of 19 But how does Windows know which certificate to use as a Device Certificate? And where the private key is stored? Most of you already know that dsregcmd /status can be used to show details about AAD Joined and AAD Registered devices similar to this (not all information shown): 1+----------------------------------------------------------------------+ 2| Device State | 3+----------------------------------------------------------------------+ 4 5 AzureAdJoined : YES 6 EnterpriseJoined : NO 7 DomainJoined : NO 8 Device Name : AADJoin02 9 10+----------------------------------------------------------------------+ 11| Device Details | 12+----------------------------------------------------------------------+ 13 14 DeviceId : ea77c7d5-7b2f-4567-bf0c-c0a4ceb8b679 15 Thumbprint : CEC55C2566633AC8DA3D9E3EAD98A599084D0C4C https://aadinternals.com/post/deviceidentity/ Page 5 of 19 16 DeviceCertificateValidity : [ 2022-01-28 11:15:49.000 UTC -- 2032-01-28 11:45:49.000 UTC ] 17 KeyContainerId : 0ad54eab-ba59-4d5b-8ee6-be18fd62b881 18 KeyProvider : Microsoft Software Key Storage Provider 19 TpmProtected : NO 20 DeviceAuthStatus : SUCCESS 21 22+----------------------------------------------------------------------+ 23| Tenant Details | 24+----------------------------------------------------------------------+ 25 26 TenantName : Contoso 27 TenantId : c5ff949d-2696-4b68-9e13-055f19ed2d51 28 Idp : login.windows.net 29 AuthCodeUrl : https://login.microsoftonline.com/c5ff949d-2696-4b68-9e13-055f19ed2d51/oauth2/aut 30 AccessTokenUrl : https://login.microsoftonline.com/c5ff949d-2696-4b68-9e13-055f19ed2d51/oauth2/tok 31 MdmUrl : 32 MdmTouUrl : 33 MdmComplianceUrl : 34 SettingsUrl : 35 JoinSrvVersion : 2.0 36 JoinSrvUrl : https://enterpriseregistration.windows.net/EnrollmentServer/device/ 37 JoinSrvId : urn:ms-drs:enterpriseregistration.windows.net 38 KeySrvVersion : 1.0 39 KeySrvUrl : https://enterpriseregistration.windows.net/EnrollmentServer/key/ 40 KeySrvId : urn:ms-drs:enterpriseregistration.windows.net 41 WebAuthNSrvVersion : 1.0 42 WebAuthNSrvUrl : https://enterpriseregistration.windows.net/webauthn/c5ff949d-2696-4b68-9e13-055f1 43 WebAuthNSrvId : urn:ms-drs:enterpriseregistration.windows.net 44 DeviceManagementSrvVer : 1.0 45 DeviceManagementSrvUrl : https://enterpriseregistration.windows.net/manage/c5ff949d-2696-4b68-9e13-055f19e 46 DeviceManagementSrvId : urn:ms-drs:enterpriseregistration.windows.net 47 48+----------------------------------------------------------------------+ 49| User State | 50+----------------------------------------------------------------------+ 51 52 NgcSet : NO 53 WorkplaceJoined : NO 54 WamDefaultSet : NO 55 56+----------------------------------------------------------------------+ 57| SSO State | 58+----------------------------------------------------------------------+ 59 60 AzureAdPrt : NO 61 AzureAdPrtAuthority : 62 EnterprisePrt : NO https://aadinternals.com/post/deviceidentity/ Page 6 of 19 63 EnterprisePrtAuthority : 64 65+----------------------------------------------------------------------+ 66| Diagnostic Data | 67+----------------------------------------------------------------------+ 68 69 AadRecoveryEnabled : NO 70 Executing Account Name : AADJOIN02\PCUser 71 KeySignTest : PASSED The output shows some interesting things, like thumbprint matching the Device Certificate thumbprint (line 15), tenant id (line 27) and KeySignTest result (line 71). So, time to start up Process Monitor to see what happens when the dsregcmd /status is executed. Searching for thubmprint revealed that desregcmd.exe was accessing the following registry keys/values: This tells us that there is a registry key matching the certificate thumbprint: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo\ Next, I found another registry key, containing most of the Tenant details shown by dsregcmd: This tells us that there is a registry key matching the tenant id: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo\ https://aadinternals.com/post/deviceidentity/ Page 7 of 19 While browsing down the procmon output, I found that lsass.exe was first reading the Device Certificate and then read a file from folder that was NOT one of the CNG key stores: So lsass.exe must be reading something from the certificate that tells where the key is stored. After some intensive Googling, I found at there is some information about the private key that could be read. The following PowerShell script dumps the unique name of the private key of the Device Certificate. # Read the certificate $certificate = Get-Item Cert:\LocalMachine\My\CEC55C2566633AC8DA3D9E3EAD98A599084D0C4C # Dump the unique name of private key [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate).key.uni The output shows that the unique matches the key file from above! 8bff0b7f02f6256b521de95a77d4e70d_321154c9-4462-4db7-aa81-81912067ab9a This tells us that dkpub and dkpriv are stored to: %ALLUSERSPROFILE%\Microsoft\Crypto\Keys\ Note! For AAD Registered devices, dkpub and dkpriv are stored to: %APPDATA%\Microsoft\Crypto\Keys\ Transport keys (tkpub / tkpriv) Finding the location of tkpub and tkpriv was way more harder than for dkpub and dkpriv. Round 1 I searched the procmon output for “transportkey” and found that lsass.exe was accessing the following registry key to read SoftwareKeyTransportKey. https://aadinternals.com/post/deviceidentity/ Page 8 of 19 Next I noticed that lsass.exe was looping through the files at SystemKeys until it seemed to find the correct key file. However, the file name did not match anything I had seen in registry. So how did lsass.exe know which to choose? Opening the key file in my favourite hex editor HxD showed that the key file had a unicode string matching the SoftwareKeyTransportKey! At this point I thought that I had all I needed and jumped to decrypting the private keys and implemented the functions to AADInternals. However, everything worked only for one tenant ☹ Round 2 After doing some further testing, it turned out that the registry paths where the key filename was stored were NOT constants, but they had dependencies on the user (for AAD Registered device) and the tenant. It took me almost a https://aadinternals.com/post/deviceidentity/ Page 9 of 19 month to figure out how to “calculate” the registry keys. And the fact that AAD Joined and AAD Registered were using different registry keys didn’t made it any easier. So, it was time to bring in the big guns! I started Process Monitor and let in ran while I AAD Registered a device. I didn’t find anything new though (except totally different registry key name). However, checking the call stack revealed calls fo NgcPregenKey function of ngcpopkeysrv.dll. Next, I fired up my old friend API Monitor and decided to boldly go where no one should ever go: monitor lsass.exe during the AAD Register process 😱 I selected all possible APIs, hooked to lsass.exe and registered the device to AAD. After that, I detached from the lsass.exe. At this point, Windows announced that it didn’t liked that and told me I had one minute to save my work before reboot 🥶 Luckily, I managed to save the API Monitor capture and started to study it. I searched for the first part of the registry path shown in the procmon dump above (“ad8098d0”) and got a match! https://aadinternals.com/post/deviceidentity/ Page 10 of 19 Once again a reference to ngcpopkeysrv.dll. With high hopes, I opened the file in dnSpy but it was not a .NET dll 😒 The last hope was Ghidra, which I had just recently installed. After I had it up and running and the dll was loaded, I started by searching for CryptBinaryToStringW and found a match! I started to work backwards to find which functions were calling this one. As Ghidra names all the functions as FUN_xxx (even there is nothing fun about Ghidra!), I renamed functions for something more meaningful, like xConvertBinaryToString above. Finally, I found a location where I saw something hard coded passed to one of the functions: https://aadinternals.com/post/deviceidentity/ Page 11 of 19 So, the string “login.live.com” was passed as unicode string to a function I renamed to xConvertValueToHexString. Before calling the function I renamed to xConvertBinaryToString, there was a call to BCryptHash. It seems that Ghidra messed that call somehow, as the parameters did not make any sense. https://aadinternals.com/post/deviceidentity/ Page 12 of 19 As all the registry keys were 64 charactes long, the hash had to be SHA256. So, I quickly created a PowerShell script that read all the values from JoinInfo and TenantInfo, converted to unicode byte array, and calculated the SHA256 hashes. Profit ! 💰💰💰 For Azure AD Joined devices, the first key under PerDeviceKeyTransportKey is IdpDomain from JoinInfo. This is always login.windows.net. The second key under that is TenantId. The transport key name of AAD Joined device is located to: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\PerDeviceKeyTransportKey\< For Azure AD Registered devices I found out that one part was UserEmail from JoinInfo. I still had to do some more digging as there was still one part missing. I found the last hint from the procmon output. There was a call to memcpy a couple of lines before call to CryptBinaryStringW. For me, it seemed a partial SID. After a quick test with PowerShell I could confirm that the missing part was indeed the SID of the current user! The transport key name of AAD Registered device is located to: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Cryptography\Ngc\KeyTransportKey\\