Beginner Guide

4. First Internal Client

@CXCubeHD


Project Setup


Our project will have such structure:

client
├─ src
│  ├─ client
│  │  ├─ entry
│  │  │  ├─ entry.cc
│  │  │  └─ entry.msvc.cxx
│  │  └─ ...
│  ├─ ...
│  └─ client.cc
├─ ext
├─ CMakeLists.txt
└─ ...

Create a shared C++ library (either with CMake or Vcxproj).


This guide uses CMake
# /CMakeLists.txt

cmake_minimum_required(VERSION 3.21)

project(Client)

add_library(client SHARED "src/client.cc")

add_subdirectory(src)

Next we make sure all .cxx files are being compiled in the /src directory. For simplicity, we will add an include directory.

# /src/CMakeLists.txt

file(GLOB_RECURSE client_impl CONFIGURE_DEPENDS "*.cxx")
target_sources(client PUBLIC ${client_impl})

target_include_directories(client PUBLIC "./")

Entry Point


For shared libraries, we use DllMain to define our entry point (Note that Clang or other compilers do this differently!).

// /src/client/entry/entry.cc

#pragma once

namespace Client
{
    auto Entry() -> void
    {
        // Type in your client code here!
        ...
    }
}
// /src/client/entry/entry.msvc.cxx

#include <Windows.h>

#include <client/entry/entry.cc>

static auto Entry_(HMODULE hModule) -> int
{
    Client::Entry();
    FreeLibraryAndExitThread(hModule, 0);
    return 0;
}

static auto WINAPI DllMain(HMODULE hModule, int reason, void* reserved) -> bool
{
    switch (reason)
    {
    case DLL_PROCESS_ATTACH:
        CloseHandle(
            CreateThread(
                nullptr, 0, 
                (LPTHREAD_START_ROUTINE)Entry_, 
                hModule, 0, nullptr));
        break;
    case DLL_PROCESS_DETACH:
        break;
    default:
        break;
    }

    return true;
}

Displaying Console


Now that we have our entry point, we want to test out our client! As of now, compiling and injecting results in nothing. Our goal is now to set up a console, so we can send logs.


Setting up the console requires your game to be installed in dev mode (using a client launcher or manually with an appx). This guide will not show you how to do it, research it online!

Helper functions for creating / destroying console:

// /src/client/helper/console.cc

#pragma once

namespace Client::Helper
{
    auto void CreateConsole() -> void;
    auto void DestroyConsole() -> void;
}
// /src/client/helper/console.cxx

#include <Windows.h>

static FILE* StdIn_ = {};
static FILE* StdOut_ = {};
static FILE* StdErr_ = {};

namespace Client::Helper
{
    auto void CreateConsole() -> void
    {
        if (!::AllocConsole()) 
            throw std::runtime_error("Could not create Console.");
            
        freopen_s(&StdIn_, "CONIN$", "r", stdin);
        freopen_s(&StdOut_, "CONOUT$", "w", stderr);
        freopen_s(&StdErr_, "CONOUT$", "w", stdout);
    }
    
    auto void DestroyConsole() -> void
    {
        fclose(StdIn_);
        fclose(StdOut_);
        fclose(StdErr_);
    
        ::FreeConsole();
    }
}

Entry Code:

// /src/client/entry/entry.cc 

#pragma once

#include <iostream>
#include <chrono>
#include <thread>

#include <client/helper/console.cc>

namespace Client
{
    auto Entry() -> void
    {
        using namespace std::chrono_literals;
    
        Helper::CreateConsole();
        
        std::cout << "Hello World!" << std::endl;
        
        std::this_thread::sleep_for(20s);
        
        Helper::DestroyConsole();
    }
}

Reading / Writing to Address


Since this is an internal client, we can write and read from addresses very easily.

// Address can be either a pointer or an 64 bit integer
*(type*)(address)
// read
int read = *(int*)(0x00018209482930);

// write 
*(int*)(0x00018209482930) = 68;

Writing to Protected Address


Some addresses that have executable code or static constants are most of the time protected, meaning they can not be written to. However, Windows provides APIs to make them writable. Here we will unprotect the memory, write to it and then restore the original protection.

// Let's assume the target has the type int and we want to change it to 10

auto ptr = (void*)address;
auto size = sizeof(int);

uint oldProtection;
VirtualProtect(ptr, size, PAGE_EXECUTE_READWRITE, (PDWORD)&oldProtection);

*(int*)(address) = 10;

VirtualProtect(ptr, size, oldProtection, (PDWORD)&oldProtection);

Getting Program Base Address


auto baseAddress = (std::uintptr_t)GetModuleHandleW(nullptr);

Getting Static Address


auto baseOffset = 0x35409;

auto address = baseAddress + baseOffset;

Multilevel Pointer


A multilevel pointer consists of one static offset and multiple other offsets.

struct MultiLevelPointer
{
    std::uint64_t BaseOffset;
    std::vector<std::uint64_t> Offsets;
};
// This function can be unsafe!
auto FindMultiLevelPointer(std::uintptr_t baseAddress, MultiLevelPointer mlp) -> std::uintptr_t
{
    auto address = baseAddress + mlp.BaseOffset;
    
    for (int i = 0; i < mlptr.Offsets.size(); i++)
    {
        if ((void*)(address) == nullptr) 
            return a;
            
        address += mlp.Offsets[i];
    }
    
    return address;
}
auto mlp = MultiLevelPointer
{
    0x34535,
    {0xF0, 0x69, 0xA}
};

auto result = FindMultiLevelPointer(baseAddress, mlp);

if (result == nullptr)
{
    // failed
}