Process Injection (classic)
What is Process Injection?
->Process injection is a technique used to inject code into the memory space of a process, or like sneaking some code into a running app so it can hide and do things without being noticed (something we will discuss further). This allows the code to be executed without the permission of the targeted process, which may help it evade security defenses—or maybe not, it depends. One of the common injection methods includes DLL injection, where the DLL (Dynamic Link Library) is forced into the target process, and code hollowing, where [x] code is replaced with [y] instructions or another code.
Memory [ Some Theory :) ]
Virtual Memory
Before getting into all of this, we should understand some concepts such as virtual memory, pages, and memory permissions and flags.
So, let’s begin with virtual memory. You may ask yourself, what is this thing? Virtual memory is a feature. Why? Because it helps to manage and use memory more efficiently without relying solely on physical memory to run programs. What does that mean, and why is it more efficient? This feature uses extra storage space as if it were additional RAM. But where is this additional “RAM” coming from? Simple: the disk.
Now, our computer has a limited amount of RAM. When you run several programs at once, they all need memory to run, right?
When the RAM fills up, the computer can’t keep everything there, so it uses a part of your HDD or SSD to store some information temporarily. This part is called virtual memory.
The computer will move less-used information from RAM to this virtual memory on our storage disk. This process is called paging. When you need the information again, the computer will bring it back into RAM and may move other information out to make space.
Thanks to this, we don’t have a strict limit on memory use, which in the past led to inefficient resource management and other issues.
One thing we should keep in mind is that accessing data from a hard drive is slower than accessing it from RAM. (Not crucial for our work, but worth noting! :)
Now, I’m not going to dive deep into this topic. Another thing related to virtual memory is virtual addresses. Yes, it is a set of ranges of virtual addresses that an operating system makes available to a process (thanks Wikipedia definition). These addresses are used to find and access data in memory. Instead of working with the actual physical memory location, the OS translates the virtual address to the real physical address. ;)
VirtualAllocEx function
Okay, now that we know something about paging and virtual memory, let’s move on to allocation because we need to allocate some code. What’s the point of allocating? Let’s put it this way: each process has its own memory space, like a private apartment, where it stores all its data and code. Normally, programs aren’t allowed to enter each other’s “apartments,” which keeps them from messing with each other’s stuff.
The function I mentioned earlier lets one program (the “injector”) reserve some space in another program’s memory. When VirtualAlloc creates space in the target program, it can request specific permissions, such as full access, read, write, or execute. This means the code in this memory can not only sit there but also run within the program.
VirtualProtect
There is another function that we need called VirtualProtect. We’re not going to stretch this discussion too much now, but in order to write data and make changes, we need something that will “relock” the memory with new permissions. This allows changes like switching read-only to executable or vice versa. So, when injecting code into another process, after writing the code into the target process’s memory, you need to use VirtualProtect to ensure that the memory is marked as executable. This step is crucial because, by default, memory might not have the right permission to run the injected code.
Memory Flags
Flag | Description |
---|---|
MEM_COMMIT | Allocates physical storage in memory for the specified pages. |
MEM_RESERVE | Reserves a range of virtual address space without allocating physical storage. |
MEM_RESET | Indicates that the memory pages will be reset to zero. |
MEM_RELEASE | Releases a range of pages, freeing the resources associated with it. |
PAGE_READONLY | Pages can be read, but not written to or executed. |
PAGE_READWRITE | Pages can be read and written to, but not executed. |
PAGE_EXECUTE | Pages can be executed, but not read or written to. |
PAGE_EXECUTE_READ | Pages can be executed and read, but not written to. |
PAGE_EXECUTE_READWRITE | Pages can be read, written to, and executed. |
PAGE_NOACCESS | Pages cannot be read or written to; access is denied. |
Something Practical
The address of our x value: 0x0061FF04
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
DWORD GetProcId(const char* proc) {
DWORD procId = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap != INVALID_HANDLE_VALUE) {
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnap, &procEntry)) {
do {
if (!_stricmp(procEntry.szExeFile, proc)) {
procId = procEntry.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &procEntry));
}
CloseHandle(hSnap);
}
return procId;
}
int main()
{
const char* dllPath = "C:\\Users\\name\\source\\repos\\proc_injec\\Release\\lol.dll";
const char* procName = "main.exe";
DWORD procId = 0;
while (!procId)
{
procId = GetProcId(procName);
Sleep(30);
}
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, 0, procId);
if (hProc && hProc != INVALID_HANDLE_VALUE)
{
void* loc = VirtualAllocEx(hProc, 0, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProc, loc, dllPath, strlen(dllPath) + 1, 0);
HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, loc, 0, 0);
if (hThread)
{
CloseHandle(hThread);
}
}
if (hProc)
{
CloseHandle(hProc);
}
return 0;
}
Explanation
Now, here we have 2 main things, GetProcId and the stuff inside main . Explaining the whole code would be such a waste of time actually because we know almost everything. But let’s explain the GetProcId function which is important here.
DWORD GetProcId(const char* proc) {
DWORD procId = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap != INVALID_HANDLE_VALUE) {
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnap, &procEntry)) {
do {
if (!_stricmp(procEntry.szExeFile, proc)) {
procId = procEntry.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &procEntry));
}
CloseHandle(hSnap);
}
return procId;
}
We have the function CreateToolhelp32Snapshot
with two arguments; the important one is TH32CS_SNAPPROCESS
, which indicates that we want to take a snapshot of all processes, followed by a condition to ensure that our HANDLE
is not invalid.
PROCESSENTRY32
is a structure used to store information about a process, which is why we are going to use it. The member dwSize
must be set to the size of PROCESSENTRY32
. You may ask yourself why. It’s quite simple: this is a requirement to ensure that the structure is correctly initialized and that the API we use (WinAPI) works properly.
After that, we iterate over processes until we find the target process. Process32First is a function that retrieves information about the first process in the snapshot and fills a structure, which in this case is procEntry.
_stricmp
is used to compare two strings: the target process that we declared and the current process from the snapshot. It’s similar to an if statement; if s1 == s2, we have found our process.
Process32Next retrieves information about the next process in the snapshot and updates the procEntry structure.
Now we know how GetProcId
works.
Last part of our dll injector
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, 0, procId);
if (hProc && hProc != INVALID_HANDLE_VALUE)
{
void* loc = VirtualAllocEx(hProc, 0, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProc, loc, dllPath, strlen(dllPath) + 1, 0);
HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, loc, 0, 0);
if (hThread)
{
CloseHandle(hThread);
}
}
This part is much easier we already know what VirtualAllocEx,
But, OpenProcess
is called to obtain a handle to the process [procId], and we specify that the handle should have full access;
VirtualAllocEx
- allocated memory in the addressspace of our program;
-0
we let the function to decide at which address the code should be allocated
-MAX_PATH
specifies the size of the memoery block to allocate that should be enough to sotre the path of our DLL
-MEM_COMMIT | MEM_RESERVE
- | means is a combo , check the flags in the table :)
-PAGE_EXECUTE_READWRITE
- same, there is a table above which all of those flags.
*loc -> points to the adress of the allocated memoery in our process.
WriteProcessMemory(hProc, loc, dllPath, strlen(dllPath) + 1, 0);
-loc
- the address in our process where the data will be written
-dllPath
- :)), is there any point to explain this one?
strlen(dllPath+1)
- this is the size of the data to write + null terminator
and the last one is used to indicate that we dont need any special options.
HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, loc, 0, 0);
CreateRemoteThread
- is used to create a thread in our process that will execute a specific function.
- (LPTHREAD_START_ROUTINE)LoadLibraryA
- this ia function that will be called to create a thread. LoadLibraryA
is used to load the specified DLL :) [easy and fast explanation]
Our DLL
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <iostream>
#include <Windows.h>
#include "mem.h"
DWORD WINAPI OurThread(HMODULE hModule)
{
// Create Console
AllocConsole();
FILE* f;
freopen_s(&f, "CONOUT$", "w", stdout);
// We get the module base by obtaining the module handle and casting it to uintptr_t.
uintptr_t moduleBase = (uintptr_t)GetModuleHandle(L"main.exe");
//we calculate the offset where the value of x is located
uintptr_t x = moduleBase + 0x00007000;
x = (x + 0x0);
x = x + 0x30;
x = x + 0x104;
x = x + 0x2C;
x = x + 0x1A4;
x = x + 0x548;
std::cout << "Initial address (x): " << std::hex << x << std::dec << std::endl;
while (true)
{
if (GetAsyncKeyState('M') & 0x8000)
{
int currentValue = 0;
//we read the value, 2nd param. - from where to read
//currentValue this is a pointer to the variable where the read value will be stored
//3rd param - this specifies the size of the memory to read
ReadProcessMemory(GetCurrentProcess(), (LPCVOID)x, ¤tValue, sizeof(currentValue), nullptr);
std::cout << "Current value at x: " << currentValue << std::endl;
currentValue += 1;
//Is likely the same as the one from ReadProc but
//2nd param the address where we want to write
WriteProcessMemory(GetCurrentProcess(), (LPVOID)x, ¤tValue, sizeof(currentValue), nullptr);
Sleep(10);
// Cleanup
fclose(f);
FreeConsole();
FreeLibraryAndExitThread(hModule, 0);
return 0;
}
}
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// Create the HackThread and detach immediately
CloseHandle(CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)OurThread, hModule, 0, nullptr));
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Just a reminder,I set my character set to Multi-Byte.
If you dont set your character set from Visual Studio you need to use wchar_t
, where you indicate that for handling wide characters.
Also if you use wchar_t
dont use _stricmp
but _wcsicmp
.
-you need to run the injector using admin rights. :)
Result:
Resources
https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants
Would you like to support me?
If you find my content helpful and would like to support my work, consider visiting my Patreon page.
Your support means the world to me and helps keep this work going!