CVE-2019-1172 Disclosure of Azure AD personal account auth token to malicious websites when using the recommended browser extension 2/2 feature image

Second part of the article about CVE-2019-1172 where we will deep-dive through reverse-engineering to finally understand how it works and discover the cause of the vulnerability. We will also review the patch via binary diffing.

This is part 2 of this 2-parts article:

More about BrowserCore 🔗

Reminder: BrowserCore is a Windows executable registered as a “Native Messaging host” in Chrome. The Chrome extensions communicate with BrowserCore which in turns does something, that we will discover later!

How Chrome launches BrowserCore.exe 🔗

Using Process Explorer we can see BrowserCore.exe launched by Chrome:

This happens very quickly when a SSO log-in occurs, or when we manually call BrowserCore from Chrome. As the process is very short-lived, tools such as process explorer / task manager might miss it. The best solution to catch it is to use Process Monitor with an include filter on the “cmd.exe” and “BrowserCore.exe” process names.

Communication with Native Messaging host 🔗

I was curious to see how we can communicate directly with BrowserCore.exe, without using Chrome. If you try to launch it you will see that it expects some console input (stdin) and in most cases it will return you an error and exit… The reason is that it expects messages with a specific format described here: https://developer.chrome.com/apps/nativeMessaging#native-messaging-host-protocol

Here is a small Python 3 script which implements this protocol with a few interesting messages:

#!/usr/bin/env python3

from struct import pack
from subprocess import Popen, PIPE, STDOUT

messages = (
    """{"method": "GetSupportedUrls"}""",
    """{"method": "GetCookie", "uri": "https://login.live.com/", "sender": "https://login.live.com/"}""",
    """{"method": "GetCookie", "uri": "https://login.microsoftonline.com/", "sender": "https://login.microsoftonline.com/"}"""
)

for msg in messages:
    msg = msg.encode("utf-8")
    size = pack("@L", len(msg))  # native byte-order, unsigned long (32 bits)

    p = Popen(['C:\\Program Files\\Windows Security\\BrowserCore\\BrowserCore.exe'], stdout=PIPE, stdin=PIPE,
              stderr=STDOUT)
    print("\nPress ENTER to send message...")
    input()

    print("Sending...")
    stdout = p.communicate(input=size + msg)[0]

    print("We got:")
    print(stdout.strip())

print("Bye")

Other available methods 🔗

In the first part of this article, we discovered the “GetCookie(s)” method of BrowserCore. But what if there were other methods? After a quick reverse engineering of BrowserCore.exe with your favorite tool, we can discover in the code of JsonRequestManager::ProcessRequest, a third method: “GetSupportedUrls”.

Here is what it returns when calling BrowserCore directly from Chrome:

Two URLs are returned in this example. Based on my observations on a few devices, we can conclude from the presence of “https://login.microsoftonline.com” that this device is joined or registered to Azure AD. Otherwise there is only “https://login.live.com”! I did not encounter a case where the result was empty.

BrowserCore.exe contains two methods in charge of collecting these URLs from various locations in the registry:

  • LoginUriUtility::GetSupportedUrisForAAD (for Azure AD accounts)

  • LoginUriUtility::GetSupportedUrisForMSA (for Microsoft personal accounts, “MSA”)

Next I wanted to understand how BrowserCore implements the “GetCookie” method. Was it able to go into the system cookie jar and fetch the relevant ones? The answer can be found by reverse-engineering its implementation in the CookieInfoRequest::ProcessRequest method.

It begins with a few checks of the “uri” and “sender” request parameters with the CookieInfoRequest::ValidateRequest and PolicyUtility::ValidateEnterprisePolicy methods. The first one only seems to check the format and the second does more checks.

Then we notice a call to CoCreateInstance which is used to instantiate COM objects. Its first parameter is the CLSID = {A9927F85-A304-4390-8B23-A75F1C668600}, and we also have the interface identifier = {CDAECE56-4EDF-43DF-B113-88E4556FA1BB}.

⚠️ Depending on the reverse-engineering tool you use, the GUID value might not be properly decoded. So you must pay attention to the binary encoding Microsoft decided to use which is called mixed-endian as explained on Wikipedia.

We have the confirmation when using API Monitor on BrowserCore:

You can find this CLSID in the registry and a reference to its implementation in “%SystemRoot%\System32\MicrosoftAccountTokenProvider.dll”:

You will also find references to the Windows 10 SDK when you search online for the {A9927F85-A304-4390-8B23-A75F1C668600} CLSID. We discover that it is called ProofOfPossessionCookieInfoManagerLib:

library ProofOfPossessionCookieInfoManagerLib
{
    [
        uuid(A9927F85-A304-4390-8B23-A75F1C668600)
    ]
    coclass ProofOfPossessionCookieInfoManager
    {
        [default] interface IProofOfPossessionCookieInfoManager;
    };
};

And that the referenced interface is GetCookieInfoForUri:

//
// Interface to get information for creating proof of possession cookies. 
// 
[
    object,
    uuid(CDAECE56-4EDF-43DF-B113-88E4556FA1BB),
    pointer_default(unique)
]
interface IProofOfPossessionCookieInfoManager : IUnknown 
{
    // 
    // Gets cookie information corresponding to the supplied URI.
    // A case sensitive string search is performed on the supplied URI.
    // Free the returned array using FreeProofOfPossessionCookieInfoArray().
    // 
    HRESULT GetCookieInfoForUri(
        [in]                        LPCWSTR                             uri,
        [out]                       DWORD*                              cookieInfoCount,
        [out, size_is(,*cookieInfoCount)] ProofOfPossessionCookieInfo** cookieInfo);
};

Everything makes sense, isn’t it? 😃

Update: in 2021, Firefox 91 implemented Windows SSO too. It is built-in without any extension. The code directly calls this COM method.

Logs 🔗

Every error that I triggered when trying variants of parameters when calling BrowserCore was logged in the Windows event logs under “Microsoft\Windows\AAD\Operational”:

MicrosoftAccountTokenProvider 🔗

We just discovered that BrowserCore invokes the MicrosoftAccountTokenProvider through COM to obtain the requested cookies. This COM object is implemented by MicrosoftAccountTokenProvider.dll stored in System32. The DLL does not export any symbols except for the classic ones… This research was the first time I really had the occasion to work around COM objects, so my understanding is very limited. I did not take the time to learn more, and I used my favorite technique in this case: looking at the strings and the code that reference them! And indeed there are many references to “token” and “cookie” 🙂

My second favorite technique is to use dynamic analysis with Sysinternals Process Monitor and API monitor. I choose to use them to discover which interesting files and registry keys are touched by MicrosoftAccountTokenProvider. One must remember that this COM object is actually running in the BrowserCore.exe process.

Azure AD account 🔗

We notice that the following registry locations are queried:

We get confirmation that they are related to Azure AD accounts:

We observe a few differences on a different machine:

Wondering about this “CookieStore” component?

I did not push further… Then we notice, just after, some lines mentioning Internet Explorer then another relevant registry path:

This is indeed where the content of my authentication cookies is stored:

Microsoft personal account (MSA) 🔗

Here is how the login URL is queried:

For the fun, I modified this value in registry and we can clearly see that we can set a different domain as authorized:

Summary as communications overview 🔗

This diagram shows the different components we explained above, how they are interconnected and communicate:

Patch analysis 🔗

Now that the issue is patched by Microsoft I am curious to see what was changed 🤔 The following screenshots are based on Windows 10 1903 Professional x64. You might observe different results on a different system…

Windows 10 updates are cumulative so many files are modified and not all of them are relevant with this vulnerability. However, we can quickly see after applying the update that BrowserCore.exe is not modified, whereas MicrosoftAccountTokenProvider.dll is changed, which makes totally sense 😉

Here are this file properties before and after:

By the way, were you aware about this relation between the full Windows version number and some system files such as this one? Before applying the September update, the Windows version was 18362.267 (18362 = 1903, cf. Windows 10 version history) but the file version was 10.0.18362.1. Based on my observation it means that it was not yet modified since the beginning of Windows 10 1903. However after applying the update, the Windows version becomes 18362.295, and since the file is modified, its version becomes synchronized: 10.0.18362.295.

Now we need to understand inside this DLL file what has changed… I use BinDiff for the first time and after taking the time to install and understand it, it gives me a very good insight! 💡 All functions have 1.00 similarity while only one has 0.98 similarity:

Its name is “TokenProviderImplementation::GetCookieInfoForUriInternal()” which seems interesting indeed! The main difference in both versions is around calls to “TokenProviderImplementation::MatchAadLogonUrl()” which is even more interesting… It is only called 1 time before the patch (in “GetCookieInfoForUriInternal()”), versus 2 times after:

If we dig deeper we see that, where “MatchAadLogonUrl()” call was added, the “wcsstr” function was used instead. It is used to return “a pointer to the first occurrence of a search string in a string”…

The bug is now clear, and the unfortunate thing is that the proper function to match the URL was indeed present and already used, but not everywhere. Lesson learned! 📒

Further reading 🔗

This was the second part of the article, in case you missed it do not forget to read the first part where I explain the process of discovering the vulnerability.

This topic was also covered recently by two great researchers with different approaches and outcomes:

External references 🔗