Page cover image

TTPs: Embedding Payloads with MSFVenom (x64)

Demonstrating a workflow to achieve embeded payloads on x64 executables using MSFVenom, BinaryNinja, and x64Dbg

Part One: Introduction

In this article, we're going to follow the pattern that we followed with our last article covering embedded payloads with MSFVenom. If you haven't checked out the x86 article on this topic I recommend that you do, this writeup will be an extension of the concepts we covered in that on.

Just a refresher, the goal of this writeup is to demonstrate embedding an MSFVenom payload into a pre-existing executable. MSFVenom provides the general mechanics for achieving this, but it does fall short in some respects with x64 executables.

Except in this case there's a known problem. Embedded payloads in x64 do not work properly. In this article we'll explore why that is and a technique we can apply to fix this.

Part Two: Getting Started

We're going to use a "safe" template program.

/* 
* By 0xTriboulet
* "good.exe" program
* 1/12/23
* compile with: x86_64-w64-mingw32-g++ good.cpp -o good_x64.exe -Wl,-subsystem,windows
*/

#include <stdio.h>
#include <Windows.h>

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "kernel32.lib")

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow) {
	MessageBox(NULL, "This is a safe program!", "Safe!", 0x0);
	return 0;
}

Now that we have our safe code, we can take our executable and embed a payload inside of it with the following command on our Kali machine.

msfvenom -p windows/x64/exec CMD="calc.exe" -x good_x64.exe -f exe -k -o even_better_x64.exe EXITFUNC=thread

Note: The documentation states that the exe-only option should be used, but that option does not support the execution of our payload in a new thread (running the payload AND running the original program functionality does not work).

Note cont.: This happens because the exe-only option clobbers the _start() function and several of the other functions inside of our executable in order to achieve consistent payload execution.

Now, transitioning back to our Windows machine and trying to run our payload we notice that our executable doesn't work!

Part Three: What's the issue!?

Lets put the executable in BinaryNinja and see if we get a clue.

Looking at the _start() function in good_x64.exe we see normal startup behavior, nearly identical to what we saw in the x86 version of this program.

And inside of even_better_x64.exe, the _start() function displays the exact behavior that the we would expect.

The MSFVenom source code validates that this is the assembly we should be seeing in the _start() stub, and based on the behavior we reversed in the (x86) article we should have a working payload.

If we open up the even_better_x64.exe in x64dbg, we get a clue.

It looks like instead of loading the address of GetProcAddress, we load the address of a __C_specific_handler. That's weird. If we execute the program until that part of the code, we see that GetProcAddress is not in rax like we would expect.

Looking at the function address, it looks like the address that the program is pointing at is correct, but the correct data is not located at that address!

It's a little bit difficult to demonstrate with screenshots, but if we open up even_better_64.exe in PE-Bear, we can see that even_better_x64.exe has an import address collision at offset 81f8. At that offset GetProcAddress is loaded AND __C_specific_handler get loaded at runtime.

So when we run our program, __C_specific_handler overrides GetProcAddress and causes the issues we've seen above.

We can manually correct these functions in PE-Bear by simply changing the FirstThunk of msvcrt.dll

Once we've made this change, we can save a new copy of our executable as even_better_x64_PATCHED.exe. We see that now we successfully load the address of GetProcAddress during runtime!

Part Four: But WAIT! There's more!

If we now try to run the program inside of our debugger, we receive an access violation.

There's a pretty easy way to remediate this: we patch the program to jump to our main function at the end of the _start() function.

So we take this address:

And we patch this jmp instruction at the end of the entry stub such that we jmp straight into main(). You can do this with the debugger or disassembler of your choice.

Part Five: ???

[Intentionally left blank]

Part Six: Profit

And if you did this correctly, the program should work! Our embedded x64 payload now runs as a seperate thread inside a pre-existing binary.

This works because our template program (good_x64.exe) is pretty simple and does not rely on robust setup protocols, error handling, or really any functionality that gets setup between the _start() and main() functions.

Part Seven: Conclusion

In this writeup we discussed a method of making embeded x64 MSFVenom payloads viable by making a couple of corrections to our executable. This discussion was limited to a very simple executable template and a very simple payload. More complicated programs will necessairily require more effort to reverse engineer and patch.

We discussed a couple of the issues and developed practical solutions but we glossed over the granular details about why these issues were happening. The details about these errors are buried in the injection mechanics of MSFVenom, but the short story is that MSFVenom does not appropriately inject additional functions into the imports section of our executable. We were able to correct for this by using PE-Bear to manually overwrite the PE header.

Additionally the initiallization stub injected into our payload does not appear to provide robust on/off ramping. We bypassed this instability by jumping straight to main() from the end of the MSFVenom stub.

Overall, we validated a powerful capability provided by MSFVenom and found a work around for its current limited support for x64 embedded payloads.

References:

Last updated