Jordan Savant # Software Engineer

Mod Against the Machine - DLL Injection / Internal Process Modding

Read Mod Against the Machine - External Process Modding to get a better understanding of this part.

Concept

Many of the operations are similar to the external process modifications, but instead of running as an external process modifying the instructions or memory of another process, we modify the memory from within the process directly.

This is done through dynamically injecting a DLL library into the runtime process we want to modify, which runs in a thread and can access the memory of the host process in a much more performant manner. Once the DLL is injected, most operations are very similar for modifying memory.

But what about the DLL Injection portion? How do we get a DLL injected into another process? There are several techniques and in this article we will do so in a similar fashion to external process modding. We run an outside executable that can find the process and dynamically load our DLL process into the memory of the other process and invoke it.

Memory Layout Graphic

Kernel32.dll is loaded into every process and since the space is virtual its loaded in the same location of memory on every process. As such we can invoke it as a remote thread function on the game process which will load the DLL correctly into the game's process.

1. DLL Code

This is a DLL Module created in Visual Studio that will invoke a thread once the DLL is loaded into the memory of the process. The thread will affect memory and instructions similar to Part 1, but does so with the memory using C++ operators as expected, no longer using Read/WriteProcessMemory() but assigning it directly with memset or =

// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"
#include "main.h"

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        CloseHandle(CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)HackThread, hModule, 0, nullptr));
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
// main.h
#pragma once
#include "stdafx.h"

DWORD WINAPI HackThread(HMODULE hModule);
// main.cpp
#include "stdafx.h"
#include <iostream>
#include "mem.h"

DWORD WINAPI HackThread(HMODULE hModule)
{
    //Create Console
    AllocConsole();
    FILE* f;
    freopen_s(&f, "CONOUT$", "w", stdout);

    std::cout << "OG for a fee, stay sippin' fam\n";

    uintptr_t moduleBase = (uintptr_t)GetModuleHandle(L"ac_client.exe");

    //calling it with NULL also gives you the address of the .exe module
    moduleBase = (uintptr_t)GetModuleHandle(NULL);

    bool bHealth = false, bAmmo = false, bRecoil = false;

    while (true)
    {
        if (GetAsyncKeyState(VK_END) & 1)
        {
            break;
        }

        if (GetAsyncKeyState(VK_NUMPAD1) & 1)
            bHealth = !bHealth;

        if (GetAsyncKeyState(VK_NUMPAD2) & 1)
        {
            bAmmo = !bAmmo;
        }

        //no recoil NOP
        if (GetAsyncKeyState(VK_NUMPAD3) & 1)
        {
            bRecoil = !bRecoil;

            if (bRecoil)
            {
                mem::Nop((BYTE*)(moduleBase + 0x63786), 10);
            }

            else
            {
                //50 8D 4C 24 1C 51 8B CE FF D2 the original stack setup and call
                mem::Patch((BYTE*)(moduleBase + 0x63786), (BYTE*)"\x50\x8D\x4C\x24\x1C\x51\x8B\xCE\xFF\xD2", 10);
            }
        }

        //need to use uintptr_t for pointer arithmetic later
        uintptr_t* localPlayerPtr = (uintptr_t*)(moduleBase + 0x10F4F4);

        //continuous writes / freeze

        if (localPlayerPtr)
        {
            if (bHealth)
            {

                //*localPlayerPtr = derference the pointer, to get the localPlayerAddr
                // add 0xF8 to get health address
                //cast to an int pointer, this pointer now points to the health address
                //derference it and assign the value 1337 to the health variable it points to
                *(int*)(*localPlayerPtr + 0xF8) = 1337;
            }

            if (bAmmo)
            {
                //We aren't external now, we can now efficiently calculate all pointers dynamically
                //before we only resolved pointers when needed for efficiency reasons
                //we are executing internally, we can calculate everything when needed
                uintptr_t ammoAddr = mem::FindDMAAddy(moduleBase + 0x10F4F4, { 0x374, 0x14, 0x0 });
                int* ammo = (int*)mem::FindDMAAddy(moduleBase + 0x10F4F4, { 0x374, 0x14, 0x0 });
                *ammo = 1337;

                //or just
                *(int*)mem::FindDMAAddy(moduleBase + 0x10F4F4, { 0x374, 0x14, 0x0 }) = 1337;
            }

        }
        Sleep(5);
    }

    fclose(f);
    FreeConsole();
    FreeLibraryAndExitThread(hModule, 0);
    return 0;
}
//mem.h
#pragma once
#include "stdafx.h"
#include <windows.h>
#include <vector>

namespace mem
{
    void Patch(BYTE* dst, BYTE* src, unsigned int size);
    void PatchEx(BYTE* dst, BYTE* src, unsigned int size, HANDLE hProcess);
    void Nop(BYTE* dst, unsigned int size);
    void NopEx(BYTE* dst, unsigned int size, HANDLE hProcess);
    uintptr_t FindDMAAddy(uintptr_t ptr, std::vector<unsigned int> offsets);
}
//mem.cpp
#include "stdafx.h"
#include "mem.h"

void mem::Patch(BYTE* dst, BYTE* src, unsigned int size)
{
    DWORD oldprotect;
    VirtualProtect(dst, size, PAGE_EXECUTE_READWRITE, &oldprotect);

    memcpy(dst, src, size);
    VirtualProtect(dst, size, oldprotect, &oldprotect);
}

void mem::PatchEx(BYTE* dst, BYTE* src, unsigned int size, HANDLE hProcess)
{
    DWORD oldprotect;
    VirtualProtectEx(hProcess, dst, size, PAGE_EXECUTE_READWRITE, &oldprotect);
    WriteProcessMemory(hProcess, dst, src, size, nullptr);
    VirtualProtectEx(hProcess, dst, size, oldprotect, &oldprotect);
}

void mem::Nop(BYTE* dst, unsigned int size)
{
    DWORD oldprotect;
    VirtualProtect(dst, size, PAGE_EXECUTE_READWRITE, &oldprotect);
    memset(dst, 0x90, size);
    VirtualProtect(dst, size, oldprotect, &oldprotect);
}

void mem::NopEx(BYTE* dst, unsigned int size, HANDLE hProcess)
{
    BYTE* nopArray = new BYTE[size];
    memset(nopArray, 0x90, size);

    PatchEx(dst, nopArray, size, hProcess);
    delete[] nopArray;
}

uintptr_t mem::FindDMAAddy(uintptr_t ptr, std::vector<unsigned int> offsets)
{
    uintptr_t addr = ptr;
    for (unsigned int i = 0; i < offsets.size(); ++i)
    {
        addr = *(uintptr_t*)addr;
        addr += offsets[i];
    }
    return addr;
}
// proc.h
#pragma once
#include <vector>
#include <windows.h>
#include <TlHelp32.h>

DWORD GetProcId(const wchar_t* procName);
uintptr_t GetModuleBaseAddress(DWORD procId, const wchar_t* modName);
uintptr_t FindDMAAddy(HANDLE hProc, uintptr_t ptr, std::vector<unsigned int> offsets);
//proc.cpp
#include "stdafx.h"
#include "proc.h"

DWORD GetProcId(const wchar_t* procName)
{
    DWORD procId = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnap != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32 procEntry;
        procEntry.dwSize = sizeof(procEntry);

        if (Process32First(hSnap, &procEntry))
        {
            do
            {
                if (!_wcsicmp(procEntry.szExeFile, procName))
                {
                    procId = procEntry.th32ProcessID;
                    break;
                }
            } while (Process32Next(hSnap, &procEntry));

        }
    }
    CloseHandle(hSnap);
    return procId;
}

uintptr_t GetModuleBaseAddress(DWORD procId, const wchar_t* modName)
{
    uintptr_t modBaseAddr = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procId);
    if (hSnap != INVALID_HANDLE_VALUE)
    {
        MODULEENTRY32 modEntry;
        modEntry.dwSize = sizeof(modEntry);
        if (Module32First(hSnap, &modEntry))
        {
            do
            {
                if (!_wcsicmp(modEntry.szModule, modName))
                {
                    modBaseAddr = (uintptr_t)modEntry.modBaseAddr;
                    break;
                }
            } while (Module32Next(hSnap, &modEntry));
        }
    }
    CloseHandle(hSnap);
    return modBaseAddr;
}

uintptr_t FindDMAAddy(HANDLE hProc, uintptr_t ptr, std::vector<unsigned int> offsets)
{
    uintptr_t addr = ptr;
    for (unsigned int i = 0; i < offsets.size(); ++i)
    {
        ReadProcessMemory(hProc, (BYTE*)addr, &addr, sizeof(addr), 0);
        addr += offsets[i];
    }
    return addr;
}
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//

#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>
#include <iostream>
#include <vector>

// reference additional headers your program requires here
// targetver.h
#pragma once

// Including SDKDDKVer.h defines the highest available Windows platform.

// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.

#include <SDKDDKVer.h>

2. DLL Injection

How do we get the DLL into the game? We will use an external loader program to inject it into memory and invoke it.

The external loader program must run as an administrator in this example which can be set with VS under Properties > Linker > Manifest File > UAC Execution Level > requireAdministrator

This simple injector allocates memory in the game and stores the string path the DLL's location on disk. Then it creates and invokes a thread in the game, passing a pointer to the LoadLibrary() function to run as a thread, with the DLL path string as a parameter.

So the game process itself loads the DLL library via thread that we commanded through our external injector.

(Note: other methods exist to load the DLL into memory directly and invoke it)

//main.cpp
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>

// get a handle to the game process
DWORD GetProcId(const char* procName)
{
    DWORD procId = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (hSnap != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32 procEntry;
        procEntry.dwSize = sizeof(procEntry);

        if (Process32First(hSnap, &procEntry))
        {
            do
            {
                if (!_stricmp(procEntry.szExeFile, procName))
                {
                    procId = procEntry.th32ProcessID;
                    break;
                }
            } while (Process32Next(hSnap, &procEntry));
        }
    }
    CloseHandle(hSnap);
    return procId;
}

int main()
{
    const char* dllPath = "C:\\Users\\me\\Desktop\\dll.dll";
    const char* procName = "csgo.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)
    {

        // allocate memory that is the size of a maximum file path in windows
        // then write the DLL path into that portion of memory
        void* loc = VirtualAllocEx(hProc, 0, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

        WriteProcessMemory(hProc, loc, dllPath, strlen(dllPath) + 1, 0);

        // invoke LoadLibrary within the game process passing the dll location string as a parameter
        HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, loc, 0, 0);

        if (hThread)
        {
            CloseHandle(hThread);
        }
    }

    if (hProc)
    {
        CloseHandle(hProc);
    }
    return 0;
}

3. Understanding Virtual Address Space

The last part covered the basic memory layout of a C++ program but we must abstract this to a higher level to understand the Windows memory layout for a process so that we can better understand how DLL injection is working.

A Virtual Address Space is a process by which Windows abstracts real physical memory addresses away from the processes that use memory. Each process (in 32 bit Windows) is given the illusion of up to 4GB of sequential memory it can use to run. In reality a Page system will swap real process memory back and forth from Disk and RAM to accommodate processes that use more memory than is available in RAM.

Memory Layout

TEB is Thread Data and PEB is process data that is accessible from the process and gives information about the invoking environment. DLLs are also loaded into memory in their designated area for a process and this all sits below the allocated memory for the actual processes instructions and variables.

More Reading

https://github.com/techiew/EldenRingModLoader

http://blog.opensecurityresearch.com/2013/01/windows-dll-injection-basics.html

https://github.com/stephenfewer/ReflectiveDLLInjection

https://arvanaghi.com/blog/dll-injection-using-loadlibrary-in-C/

https://github.com/Arvanaghi/Windows-DLL-Injector