Beginner Guide
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).
# /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 "./")
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;
}
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.
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();
}
}
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;
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);
auto baseAddress = (std::uintptr_t)GetModuleHandleW(nullptr);
auto baseOffset = 0x35409;
auto address = baseAddress + baseOffset;
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
}