Unpacking the AAD Broker LocalState Cache

Nov 3 2025
Share
By: Jack Ullrich • 9 min read

TL;DR: This post documents the AAD Broker’s storage format, how to unpack it, and discusses potential security implications. An accompanying reference source is also made available at: https://github.com/jackullrich/AADBrokerDecrypt

Intro

The Azure AD Broker (AAD Broker) is a component of Entra ID that orchestrates Azure AD sign-in, device-bound primary refresh token (PRT) handling, and application token issuance exposed by Windows Runtime (WinRT) APIs. In this post, we’ll map the broker’s on-disk cache and show how to unpack its file formats. Additionally, we offer a brief discussion of the security considerations of the cache contents. Let’s begin by taking a look at the filesystem structure of the LocalState Cache.

Note: I conducted all research on Windows 11 24H2 (26100.4946).

Filesystem Structure

The LocalState cache is located at: %LocalAppData%\Packages\Microsoft.AAD.BrokerPlugin_<PublisherId>\LocalState

The <PublisherId> value is a Base32 encoding of the first eight bytes of a SHA256 hash of the publisher string. Microsoft published AAD Broker and the publisher string used to calculate the package id is: CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US.

This procedure is outlined in the Windows 8.1 Enterprise Device Management Protocol specification and implemented in the reference source for this post.

A directory listing of the LocalState cache might appear as something like this:

Naming Convention

The naming convention for these files is a prefix followed by a 24-character token derived from identity material for users, PRT authorities, applications, and application authorities.

  • u_ = User principal name (UPN)
  • p_ = PRT authority
  • c_ = Client (Application Id)
  • a_ = Authority

The string derivation functionality was reversed from ClientCache::GetPRTFileName and ClientCache::GetClientFileName in AAD.Core.dll. Each label is a 24-character lowercase token produced by StringUtility::FileSysName. This function takes the input string (UPN for u_, client/app ID for c_, authority URL for a_, and authority host for p_), normalizes it, calculates the SHA-1 hash, then encodes the first 15 bytes with the broker’s custom Base32-hex alphabet. The broker writes paths like u_<UPNID>\c_<CLIENTID>\a_<AUTHID>.<ext> and u_<UPNID>\p_<HOSTID>, where <ext> reflects the logon type (def/pwd/ngc/scd/fido). A full reimplementation is available in the accompanying GitHub repository.

Unpacking Procedure

All AAD Broker cache files go through a similar packing procedure. The packing procedure is spread across a few classes within AAD.Core.dll. Let’s reverse how it works.

Opening up any of the packed files in a hex editor we are met with an identifiable sequence. A UTF-8 byte order mark (BOM) followed by an ASCII string.

  • First 3 bytes: UTF-8 BOM
    • Marker to indicate the file is encoded in UTF-8 (EF BB BF)
  • Header: 3-1 or 3-0
    • 3-1 – The file contents are packed and encrypted
    • 3-0 – The file contents contain a hash value

The 3-0 header was not observed on the analyzed machine, but is present in the AAD.Core.dll packing code.

Strip the UTF-8 BOM and version tag, and what remains is a Base64-encoded string. Decoding that string yields an ASN.1 blob, which is the standard format for CNG/DPAPI-protected data.

In order to decrypt the blob we will need the following fields of the ASN1 blob:

  • Key encrypting key (KEK – 262 bytes) – DPAPI Protected
  • Content encrypting key (CEK – 40 bytes) – RFC 3394 wrapped key with KEK
  • Initialization Vector (IV – 12 bytes) – AES-GCM can use a 12 byte IV
  • Encrypted Content (variable byte length) – AES-GCM encrypted content

Start by unprotecting the KEK in the user’s security context, as specified within the CNG blob. Use the recovered KEK to unwrap the CEK. Since AES-GCM requires an authentication tag (MAC), which is appended to the ciphertext, you can then decrypt the encrypted content with the unwrapped CEK, the IV, and the tag.

After decryption, we can see the following contents:

At first, the data doesn’t reveal much, but further reversing of AAD.Core.dll!Packer::V3::Pack shows a call to a compression routine, giving us a clue.

Skipping the first four bytes as some unknown header value, we can see a zlib header.

Compression Method and Flags (CMF)Flag
0x78 0x01Fastest compression
0x78 0x9CDefault compression (common)
0x78 0xDABest compression

After inflating the remainder of the file (excluding the header), the output begins to take on a more recognizable structure. In a hex editor, strings start to appear, though additional processing is still required.

At this point, it becomes necessary to understand where the packed (or rather, serialized) data comes from. The data is processed by a serialized class (i.e., AuthenticationContext), from AAD.Core.dll. In the pseudocode below, we first inspect the serialization call site and then reconstruct the deserialization control flow.

There are two different serialization (and thus deserialization procedures). By far the more commonly observed procedure was the JSON serialization. The JSON blob has a simple header:

The other magic values (5-18) correspond to a custom binary serialization format that primarily relies on length-prefixed strings. Unlike the JSON case, there isn’t a size field that describes the full payload. Instead, the first field after the magic value is itself a length-prefixed string. Rather than fully reversing this serialization scheme, a more practical approach is to simply extract the strings directly from the binary blob. A naive extraction algorithm is implemented in the repository, but string extraction utilities such as strings from Sysinternals may also be used.

Unpacked PRT Data

We’ll begin by examining the unpacked PRT file (p_) contents.

There is a lot of identity metadata material here. Of note are the fields pertaining to the PRT. The PRT value (prt.val above) is opaque and cannot be decrypted on the machine; however, the PRT session key (prt.sk_val) is a base64 encoded blob. Decoded from base64, the session key has an unknown 8 byte header followed by a legacy DPAPI blob protected with a machine key. Once unprotected, the blob reveals a structured payload containing the material necessary to derive the PRT session key.

Which can be described by the following structure.

The session key GUID value was also observed in the registry.

Session key derivation is implemented in the reference source for this post.

Alternatively, using mimikatz: mimikatz.exe dpapi::cloudapkd /keyvalue:<OpaqueKey> /keyname:<SK-GUID>

Application and Authority Files

Within the AAD Broker cache, the structure follows a consistent naming scheme:

  • Application folders are prefixed with c_.
  • Authority files inside those folders are prefixed with a_.

Each c_ folder corresponds to a single application (client), and within it, each a_ file corresponds to one authority used by that application. If an application trusts multiple authorities, the folder will contain multiple a_ files.

The authority (a_) files are per-authority token caches for a given application. If an attacker can read an a_ file they can immediately reuse any stored access tokens and can often extend access by using refresh tokens found in the same file.

The same reversing process is used for authority files as is used for the PRT file, provided that the 3-1 header is present. Upon reversal of the packing algorithm you will discover different identity material than was present in the PRT JSON.

Examining the tkn and id_tkn fields reveal themselves to be JSON web tokens (JWTs), which might look similar to this:

Authority files and the nested JWT content will vary slightly between files.

Authority files also make use of magic values other than 13, indicating that their contents are serialized in a custom binary format. The serialized data is the same underlying class, just not in JSON format. Originally, I was thinking it could be BSON, but it does not appear to be. The values are stored as length-prefixed strings. Instead of reversing the binary serialization completely, it’s easier to simply extract the strings directly from the binary blob. The code for this naive string extraction is in the GitHub repository.

Security Considerations

Persistent Identity Material

  • AAD Broker’s LocalState cache contains PRTs, application tokens, and refresh tokens.
  • PRT values are opaque; however their session keys (prt.sk_val) are present in the cache. Session keys are bound to the TPM, preventing replay value elsewhere. However, an attacker with local SYSTEM context may derive usable session keys.
  • Authority files store access/refresh tokens directly, tied to specific client applications.

Potential for Silent, Long-Lived Access

  • If an attacker derives a valid session key, they can impersonate the broker to renew tokens without user interaction.
  • This effectively bypasses MFA after initial authentication, enabling long-lived persistence in cloud resources.
  • Authority files with cached refresh tokens similarly allow token minting for specific apps.

Reconnaissance

The LocalCache contains a significant amount of structured metadata.

  • User identifiers: UPNs, OIDs, tenant IDs and on-prem SIDs that map identities to tenants and hosts.
  • Application identifiers: client/app IDs (GUIDs) and sometimes the app_displayname are embedded in JWTs or cached tokens.
  • Service scope information: scp/wids claims and tkn contents that list permissions granted to apps (e.g., Files.Read, Sites.Read.All, User.Read).
  • Device linkage: device IDs and cnf.tbh (token binding) markers indicating whether tokens are bound to a device key.
  • Token lifetimes and audiences: iat, exp, aud, and appid are exposed in cached JWTs.

Conclusion

Overall, the LocalState cache is poorly documented. It stores structured identity and application metadata that may be leveraged for reconnaissance. Refresh tokens or derived session keys should enable long-lived access, though the ability to fully derive or replay session keys depends on local protections such as TPM-bound keys and having sufficient privileges. Further research into what is and what is not possible with the metadata is needed.

The accompanying reference source for this blog post may be found on GitHub: https://github.com/jackullrich/AADBrokerDecrypt

References