Dechaining via WMI

WMI is Microsoft's implementation of WBEM and CIM developed and published by DMTF. WMI provides means of gathering and sending management information in an enterprise environment.

WMI is a com based service which provides various variations of access to system related information.

WMI introduces something called a "managed object." This is a component on the machine that is managed by WMI. A managed object is a logical or physical part of a computer, such as a hard disk drive, network adapter, database system, operating system, process, or service. Applications that retrieve info from these managed objects are called "consumers."

Consumers query managed objects from these things called "providers." A provider is either a user-mode COM DLL or a kernel driver which expose . The default WMI providers are called the "standard providers", meaning that we can create our own provider and register it with WMI. This WMI provider is contains an associated GUID which is registered in the registry.

More specifically WMI providers provide information about managed objects as instances of classes, consumers do not access these instances directly, consumers query WMI, and WMI then forwards that query to the provider. The provider then processes the query and sends info back to WMI, in which WMI sends it back to the consumer again.

These queries happen in WQL, which is basically SQL for WMI. If you have programmed in SQL before, you will notice that the syntax is very similar.

An example of a WQL query is this:

SELECT * FROM Win32_Process WHERE Name LIKE "%chrome%"

This query will return all running processes where the executable names is chrome.

The diagram below shows the relationship of the WMI infrastructure and WMI providers and managed objects.

Examples of WMI providers are the Registry provider:

Which helps you access data in the registry. This provider only has one WMI class:

which is StdRegProv. This class has many methods which we can use.

WMI Providers also contain a Managed Object Format file which defines the classes which the provider returns.

Classes are also categorized hierarchically into namespaces. All namespaces derive from the ROOT namespace and Microsoft uses ROOT\CIMV2 as the default namespace.

WMI settings are stored here:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WBEM

the WMI service is implemented as a service process within SVCHOST. This service interacts with consumers through the COM interface. Providers register their location with the WMI service, which allows WMI to route data requests.

WMI also uses ETW to record its own service activity.

For more information, please check out the Microsoft documentation:

For this lab(ish), we will just be using the standard providers to make any process of our choice spawn under wmiPRVSE.exe service.(in c/c++)

Implementation

Since we are dealing with COM, we must setup COM by calling CoInitializeEx. If you don't recall, COM is a a language agnostic library which allows application in different programming languages to interact with each other.

The steps to create a WMI application are to:

  1. Initialize COM

  2. Create a connection to a WMI namespace

  3. Set security levels of the WMI connection

  4. Use the COM interfaces(functionality)

  5. cleanup and shutdown your application properly

Microsoft already has a code implementation on how to create a process using WMI, the final code is here(stolen from MS)

#define _WIN32_DCOM

#include <iostream>
using namespace std;
#include <comdef.h>
#include <Wbemidl.h>

#pragma comment(lib, "wbemuuid.lib")

int main(int iArgCnt, char** argv)
{
    HRESULT hres;

    // Step 1: --------------------------------------------------
    // Initialize COM. ------------------------------------------

    hres = CoInitializeEx(0, COINIT_MULTITHREADED);
    if (FAILED(hres))
    {
        cout << "Failed to initialize COM library. Error code = 0x"
            << hex << hres << endl;
        return 1;                  // Program has failed.
    }

    // Step 2: --------------------------------------------------
    // Set general COM security levels --------------------------

    hres = CoInitializeSecurity(
        NULL,
        -1,                          // COM negotiates service
        NULL,                        // Authentication services
        NULL,                        // Reserved
        RPC_C_AUTHN_LEVEL_DEFAULT,   // Default authentication 
        RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
        NULL,                        // Authentication info
        EOAC_NONE,                   // Additional capabilities 
        NULL                         // Reserved
    );


    if (FAILED(hres))
    {
        cout << "Failed to initialize security. Error code = 0x"
            << hex << hres << endl;
        CoUninitialize();
        return 1;                      // Program has failed.
    }

    // Step 3: ---------------------------------------------------
    // Obtain the initial locator to WMI -------------------------

    IWbemLocator* pLoc = NULL;

    hres = CoCreateInstance(
        CLSID_WbemLocator,
        0,
        CLSCTX_INPROC_SERVER,
        IID_IWbemLocator, (LPVOID*)&pLoc);

    if (FAILED(hres))
    {
        cout << "Failed to create IWbemLocator object. "
            << "Err code = 0x"
            << hex << hres << endl;
        CoUninitialize();
        return 1;                 // Program has failed.
    }

    // Step 4: ---------------------------------------------------
    // Connect to WMI through the IWbemLocator::ConnectServer method

    IWbemServices* pSvc = NULL;

    // Connect to the local root\cimv2 namespace
    // and obtain pointer pSvc to make IWbemServices calls.
    hres = pLoc->ConnectServer(
        _bstr_t(L"ROOT\\CIMV2"),
        NULL,
        NULL,
        0,
        NULL,
        0,
        0,
        &pSvc
    );

    if (FAILED(hres))
    {
        cout << "Could not connect. Error code = 0x"
            << hex << hres << endl;
        pLoc->Release();
        CoUninitialize();
        return 1;                // Program has failed.
    }

    cout << "Connected to ROOT\\CIMV2 WMI namespace" << endl;


    // Step 5: --------------------------------------------------
    // Set security levels for the proxy ------------------------

    hres = CoSetProxyBlanket(
        pSvc,                        // Indicates the proxy to set
        RPC_C_AUTHN_WINNT,           // RPC_C_AUTHN_xxx 
        RPC_C_AUTHZ_NONE,            // RPC_C_AUTHZ_xxx 
        NULL,                        // Server principal name 
        RPC_C_AUTHN_LEVEL_CALL,      // RPC_C_AUTHN_LEVEL_xxx 
        RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
        NULL,                        // client identity
        EOAC_NONE                    // proxy capabilities 
    );

    if (FAILED(hres))
    {
        cout << "Could not set proxy blanket. Error code = 0x"
            << hex << hres << endl;
        pSvc->Release();
        pLoc->Release();
        CoUninitialize();
        return 1;               // Program has failed.
    }

    // Step 6: --------------------------------------------------
    // Use the IWbemServices pointer to make requests of WMI ----

    // set up to call the Win32_Process::Create method
    BSTR MethodName = SysAllocString(L"Create");
    BSTR ClassName = SysAllocString(L"Win32_Process");

    IWbemClassObject* pClass = NULL;
    hres = pSvc->GetObject(ClassName, 0, NULL, &pClass, NULL);

    IWbemClassObject* pInParamsDefinition = NULL;
    hres = pClass->GetMethod(MethodName, 0,
        &pInParamsDefinition, NULL);

    IWbemClassObject* pClassInstance = NULL;
    hres = pInParamsDefinition->SpawnInstance(0, &pClassInstance);

    // Create the values for the in parameters
    VARIANT varCommand;
    varCommand.vt = VT_BSTR;
    varCommand.bstrVal = _bstr_t(L"notepad.exe");

    // Store the value for the in parameters
    hres = pClassInstance->Put(L"CommandLine", 0,
        &varCommand, 0);
    wprintf(L"The command is: %s\n", V_BSTR(&varCommand));

    // Execute Method
    IWbemClassObject* pOutParams = NULL;
    hres = pSvc->ExecMethod(ClassName, MethodName, 0,
        NULL, pClassInstance, &pOutParams, NULL);

    if (FAILED(hres))
    {
        cout << "Could not execute method. Error code = 0x"
            << hex << hres << endl;
        VariantClear(&varCommand);
        SysFreeString(ClassName);
        SysFreeString(MethodName);
        pClass->Release();
        pClassInstance->Release();
        pInParamsDefinition->Release();
        pOutParams->Release();
        pSvc->Release();
        pLoc->Release();
        CoUninitialize();
        return 1;               // Program has failed.
    }

    // To see what the method returned,
    // use the following code.  The return value will
    // be in &varReturnValue
    VARIANT varReturnValue;
    hres = pOutParams->Get(_bstr_t(L"ReturnValue"), 0,
        &varReturnValue, NULL, 0);


    // Clean up
    //--------------------------
    VariantClear(&varCommand);
    VariantClear(&varReturnValue);
    SysFreeString(ClassName);
    SysFreeString(MethodName);
    pClass->Release();
    pClassInstance->Release();
    pInParamsDefinition->Release();
    pOutParams->Release();
    pLoc->Release();
    pSvc->Release();
    CoUninitialize();
    return 0;
}

This part of the code includes the libraries to compile correctly:

#define _WIN32_DCOM

#include <iostream>
using namespace std;
#include <comdef.h>
#include <Wbemidl.h>

#pragma comment(lib, "wbemuuid.lib")

We then call CoInitializeEx to setup COM

    hres = CoInitializeEx(0, COINIT_MULTITHREADED);
    if (FAILED(hres))
    {
        cout << "Failed to initialize COM library. Error code = 0x"
            << hex << hres << endl;
        return 1;                  // Program has failed.
    }

We then setup the COM security levels with CoInitializeSecurity. here, RPC_C_AUTHN_LEVEL_DEFAULT and RPC_C_IMPL_LEVEL_IMPERSONATE are the only parameters to be set.

    hres = CoInitializeSecurity(
        NULL,
        -1,                          // COM negotiates service
        NULL,                        // Authentication services
        NULL,                        // Reserved
        RPC_C_AUTHN_LEVEL_DEFAULT,   // Default authentication 
        RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
        NULL,                        // Authentication info
        EOAC_NONE,                   // Additional capabilities 
        NULL                         // Reserved
    );


    if (FAILED(hres))
    {
        cout << "Failed to initialize security. Error code = 0x"
            << hex << hres << endl;
        CoUninitialize();
        return 1;                      // Program has failed.
    }

after COM and COM-security have been set up, the class:IWbemLocator is used to access the WMI service, we initialize the IWebLocator interface through a call to CoCreateInstance.

    IWbemLocator* pLoc = NULL;

    hres = CoCreateInstance(
        CLSID_WbemLocator,
        0,
        CLSCTX_INPROC_SERVER,
        IID_IWbemLocator, (LPVOID*)&pLoc);

    if (FAILED(hres))
    {
        cout << "Failed to create IWbemLocator object. "
            << "Err code = 0x"
            << hex << hres << endl;
        CoUninitialize();
        return 1;                 // Program has failed.
    }

IWbemLocator::ConnectServer connects to the specified host for WMI. Here, we will just use the local WMI service.

IWbemLocator::ConnectServer is defined with:

HRESULT ConnectServer( const BSTR strNetworkResource, const BSTR strUser,
                       const BSTR strPassword,const BSTR strLocale,
                       LONG lSecurityFlags, const BSTR strAuthority,
                       IWbemContext* pCtx, IWbemServices** ppNamespace )
  1. The first parameter specifies the CIM namespace the consumer connects to, like \\server\namespace... If the namespace is no the local machine, server can be omitted, like namespace\namespace1...

  2. user and pass are in the second and third parameters, in the scenario if a local machine, they must be set to NULL or else they will fail

  3. the fourth parameter if set to null, the current local will be used. If it is not NULL, this must be a valid BSTR

  4. The fifth parameter if set to 0, results in ConnectServer returning only after the connection to the server is established, This could result in your program ceasing to respond indefinitely if the server is broken. The following list lists the other valid values for lSecurityFlags. Note that you can also set it to WBEM_FLAG_USE_MAX_WAIT, which guarantees the call to return within two minutes. This safeguards consumers against blocking indefinitely if the targeted machine is down

Our call looks like this:

   IWbemServices* pSvc = NULL;

    // Connect to the local root\cimv2 namespace
    // and obtain pointer pSvc to make IWbemServices calls.
    hres = pLoc->ConnectServer(
        _bstr_t(L"ROOT\\CIMV2"),
        NULL,
        NULL,
        0,
        NULL,
        0,
        0,
        &pSvc
    );

    if (FAILED(hres))
    {
        cout << "Could not connect. Error code = 0x"
            << hex << hres << endl;
        pLoc->Release();
        CoUninitialize();
        return 1;                // Program has failed.
    }

    cout << "Connected to ROOT\\CIMV2 WMI namespace" << endl;

Once you have received a pointer to the IWbemServices proxy, you will then have to set the security settings on the proxy to access WMI. Here, we call it so that impersonation of the client occurs on the proxy.

    hres = CoSetProxyBlanket(
        pSvc,                        // Indicates the proxy to set
        RPC_C_AUTHN_WINNT,           // RPC_C_AUTHN_xxx 
        RPC_C_AUTHZ_NONE,            // RPC_C_AUTHZ_xxx 
        NULL,                        // Server principal name 
        RPC_C_AUTHN_LEVEL_CALL,      // RPC_C_AUTHN_LEVEL_xxx 
        RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
        NULL,                        // client identity
        EOAC_NONE                    // proxy capabilities 
    );

    if (FAILED(hres))
    {
        cout << "Could not set proxy blanket. Error code = 0x"
            << hex << hres << endl;
        pSvc->Release();
        pLoc->Release();
        CoUninitialize();
        return 1;               // Program has failed.
    }

After that, we can finally access the Process::create method which will create a process under the WMI service.

We will use the IWbemServices pointer to make requests to WMI, we will use IWbemServices::ExecMethod to call Win32_Process::Create.

If the provider supports any in or out parameters, then the values of the parameters must be given to the IWbemClassObject pointers. For in parameters, you must spawn an instance of the in parameter definitions, and set the values of these new instances.

Process::Create needs a CommandLine in parameter to execute. As seen in its prototype:

uint32 Create(
  [in]  string               CommandLine,
  [in]  string               CurrentDirectory,
  [in]  Win32_ProcessStartup ProcessStartupInformation,
  [out] uint32               ProcessId
);

Here, we will create a IWbemClassPointer, spawn a new instance of Win32_Process::Create, and set the value of CommandLine to notepad.exe.

// Set up to call the Win32_Process::Create method
BSTR MethodName = SysAllocString(L"Create");
BSTR ClassName = SysAllocString(L"Win32_Process");

IWbemClassObject* pClass = NULL;

hres = pSvc->GetObject(ClassName, 0, NULL, &pClass, NULL); // Remember that pSvc is IWbemServices Pointer, we get the Win32Process Classname

IWbemClassObject* pInParamsDefinition = NULL;
hres = pClass->GetMethod(MethodName, 0, 
    &pInParamsDefinition, NULL); // We get the Create Method name

IWbemClassObject* pClassInstance = NULL;
hres = pInParamsDefinition->SpawnInstance(0, &pClassInstance); // Spawn new instance of Win32_Process::Create

// Create the values for the in-parameters
VARIANT varCommand;
varCommand.vt = VT_BSTR;
varCommand.bstrVal = _bstr_t(L"notepad.exe");

// Stores notepade in the CommandLine parameter
hres = pClassInstance->Put(L"CommandLine", 0,
    &varCommand, 0);
wprintf(L"The command is: %s\n", V_BSTR(&varCommand));

We will then handle the out-parameters, giving it to an IWbemClassObject pointer. We will use the GET method and store it in a VARIANT variable so we can display it back to the user

// Executes Win32_Process::create, and stores OutParams as a ClassObject pointer
IWbemClassObject* pOutParams = NULL;
hres = pSvc->ExecMethod(ClassName, MethodName, 0, 
    NULL, pClassInstance, &pOutParams, NULL);

// store the result in a Variant variable
VARIANT varReturnValue;
hres = pOutParams->Get(_bstr_t(L"ReturnValue"), 0, 
    &varReturnValue, NULL, 0);

We will then implement some error checking if anything fails, this simply prints out the error code and cleans and shuts down.

We release any open COM interfaces, free our allocated memory, call CoUninitialize, and exit our application:

    if (FAILED(hres))
    {
        cout << "Could not execute method. Error code = 0x"
            << hex << hres << endl;
        VariantClear(&varCommand);
        SysFreeString(ClassName);
        SysFreeString(MethodName);
        pClass->Release();
        pClassInstance->Release();
        pInParamsDefinition->Release();
        pOutParams->Release();
        pSvc->Release();
        pLoc->Release();
        CoUninitialize();
        return 1;               // Program has failed.
    }

Note that this is also what we do at the end of our code even if it works:

    // Clean up
    //--------------------------
    VariantClear(&varCommand);
    VariantClear(&varReturnValue);
    SysFreeString(ClassName);
    SysFreeString(MethodName);
    pClass->Release();
    pClassInstance->Release();
    pInParamsDefinition->Release();
    pOutParams->Release();
    pLoc->Release();
    pSvc->Release();
    CoUninitialize();
    return 0;

Observations

Let's compile the code above and see it in action!

We can see we spawned the WmiPrvSe service in Process Hacker like so:

And our notepad with WMiPrvSe.exe as its parent

We have successfuly spoofed notepad's parent to be the WMI service!

But there is a weird thing I found in which I don't have enough knowledge to explain why this happens.

After around one minute and thirty seconds, this popups up about notepad.exe

As we can see, it has a "Non-existent" parent.

ETW also has a WMI provider we can enable. We will use Event Viewer for this.

Lets enable tracing for the WMI-Activity, and see what kind of logs we get when we run our ppid spoofing executable:

Once enabled, lets run our executable.

...

If everything works, you should get a good amount of logs from one executable. I was too lazy to take screenshots of the logs, so I'll leave this as an "exercise" to the reader to test out.

TODO:

  • How can we detect this?

  • Does this trigger Image Load callbacks?

  • why does the WMI service quit after some time?

Last updated