RCE Endeavors 😅

October 17, 2022

TED: The Tiny Exfiltrating Debugger

TED is a limited general purpose reverse engineering API, and hybrid debugger, that allows for inspection and modification of a program’s inner workings. TED carries out its functionality by being injected into a target process and starting a gRPC server, which clients can then connect to. There are a large set of services available to perform typical operations such as enumerating loaded modules, reading and writing memory, enabling and disabling breakpoints, disassembling address ranges, loading and unloading other modules, and more.

From a high level, TED works as follows:

A host application is responsible for loading the TED Client Bridge DLL, which has a set of APIs that handle client message building and server response parsing. After the Client Bridge DLL has been successfully loaded, the host application can use a helper API to inject the TED Core DLL into a target application. Assuming the DLL injection happened successfully, the Core DLL will launch a gRPC server listening on localhost:50051. The client can then invoke desired gRPC services and have them execute in the context of the target application. Service definitions are provided in a TED.proto file; they are either basic request-response, or a streaming service for receiving breakpoint events and their associated information.

After the client and server have both been loaded successfully, performing communication between the two is rather straightforward. The host application will create a client connection to the underlying server via a TED_CreateClient call. This will return an opaque handle to the caller that will then be passed to all subsequent APIs. Once the host application is done with the connection, it can call TED_DestroyClient to officially close the connection and clean up. This patterns follows for the rest of the APIs; all APIs that return a response have a corresponding TED_DestroyX API that will clean up allocated memory.

The code snippet below demonstrates injecting the TED Core DLL into another process, creating a client to establish communication, and receiving a response from the server that contains information about all loaded modules in the target process.

// Load client API and resolve functions
auto moduleHandle{ TED_LoadClientAPI("TEDClientAPI.dll") };
TED_ResolveClientFunctions(moduleHandle);

// Get process id of target
DWORD targetProcessId{};
GetWindowThreadProcessId(FindWindow(nullptr, L"Untitled - Notepad"), &targetProcessId);
	
// Get full path of TED Core DLL so that target process can load it
std::array<char, MAX_PATH> currentDirectoryPath{};
auto size{ GetModuleFileNameA(nullptr, currentDirectoryPath.data(), MAX_PATH) };
std::string coreDllPath{ currentDirectoryPath.data(), size };
coreDllPath = coreDllPath.substr(0, coreDllPath.find_last_of("\\")) + std::string{"\\TEDCore.dll"};

TED_InjectIntoProcess(targetProcessId, coreDllPath.c_str());

// Create the client
auto client{ TED_CreateClientFnc("localhost:50051") };
	
// Get the modules
auto response{ TED_GetModulesFnc(client) };

// Enumerate over the response
for (size_t i{ 0 }; i < response->moduleInfoCount; i++) {
	const auto* moduleInfo{ response->moduleInfo[i] };
	std::cout << std::format("Module name: {}\tModule base address: {:X}\tModule size: {:X}",
		moduleInfo->name,
		moduleInfo->baseAddress,
		moduleInfo->size)
	<< std::endl;

	for (size_t j{ 0 }; j < moduleInfo->executableSectionCount; j++) {
		const auto* executableSection{ response->moduleInfo[i]->executableSections[j] };
		std::cout << std::format("Section: {}\tSection base address: {:X}\tSection size: {:X}",
			executableSection->name,
			executableSection->address,
			executableSection->size)
		<< std::endl;
	}
}

// Clean up modules response
TED_DestroyModulesFnc(response);

// Clean up client
TED_DestroyClientFnc(client);

Running this code while having Notepad open will produce the following output:

Module name: C:\Program Files\WindowsApps\Microsoft.WindowsNotepad_11.2208.25.0_x64__8wekyb3d8bbwe\Notepad\Notepad.exe Module base address: 7FF60B660000        Module size: 84000
Section: .text  Section base address: 7FF60B661000      Section size: 49C00
Module name: C:\WINDOWS\SYSTEM32\ntdll.dll      Module base address: 7FFF70250000       Module size: 214000
Section: .text  Section base address: 7FFF70251000      Section size: 12D000
Section: PAGE   Section base address: 7FFF7037E000      Section size: 1000
Section: RT     Section base address: 7FFF7037F000      Section size: 1000
Section: fothk  Section base address: 7FFF70380000      Section size: 1000
Module name: C:\WINDOWS\System32\KERNEL32.DLL   Module base address: 7FFF6EC80000       Module size: C2000
Section: .text  Section base address: 7FFF6EC81000      Section size: 80000
Module name: C:\WINDOWS\System32\KERNELBASE.dll Module base address: 7FFF6DCB0000       Module size: 39C000
Section: .text  Section base address: 7FFF6DCB1000      Section size: 189000

...

As a quick aside to the above code snippet, the memory management can be made more simple. Since the client APIs are exported with C linkage, there need to be corresponding cleanup functions for memory that gets allocated during a request-response flow. However, in a host application, these can be wrapped with smart pointers to ensure that no memory will get leaked. Below is an example of this, and is the preferred way to develop against the library:

// Create the client
std::unique_ptr<TED_Client, TED_DestroyClientFncPtr> client{
	TED_CreateClientFnc("localhost:50051"),
	TED_DestroyClientFnc };
	
// Get the modules
std::unique_ptr<TED_GetModulesResponse, TED_DestroyModulesFncPtr> response{
	TED_GetModulesFnc(client.get()),
	TED_DestroyModules };

// "client and "response" memory will be freed when they go out of scope

The next example shows how to set a breakpoint and subscribe for a streaming response from the server. This will set a breakpoint on a commonly used function in UI applications: DispatchMessage. When this function is executed, the server will capture information about the program’s state, and send it to the client. The client then prints out the results to the console. Since the service returns a streaming response, the client continues to listen to the stream and prints out messages as they come in. On the server, there is a persistent connection established and breakpoint information events are sent to the clients as they are received.

// Load client API and resolve functions
auto moduleHandle{ TED_LoadClientAPI("TEDClientAPI.dll") };
TED_ResolveClientFunctions(moduleHandle);

// Get process id of target
DWORD targetProcessId{};
GetWindowThreadProcessId(FindWindow(nullptr, L"Untitled - Notepad"), &targetProcessId);

// Get full path of TED Core DLL so that target process can load it
std::array<char, MAX_PATH> currentDirectoryPath{};
auto size{ GetModuleFileNameA(nullptr, currentDirectoryPath.data(), MAX_PATH) };
std::string coreDllPath{ currentDirectoryPath.data(), size };
coreDllPath = coreDllPath.substr(0, coreDllPath.find_last_of("\\")) + std::string{ "\\TEDCore.dll" };

TED_InjectIntoProcess(targetProcessId, coreDllPath.c_str());

// Create the client
std::unique_ptr<TED_Client, TED_DestroyClientFncPtr> client{
	TED_CreateClientFnc("localhost:50051"),
	TED_DestroyClientFnc };

// Create the reader
std::unique_ptr<TED_BreakpointReader, TED_DestroyBreakpointReaderFncPtr> reader{
	TED_CreateBreakpointReaderFnc(client.get()),
	TED_DestroyBreakpointReaderFnc };

// Set a breakpoint on a DispatchMessageW
std::unique_ptr<TED_GenericResponse, TED_DestroyGenericFncPtr> breakpoint{
	TED_EnableBreakCallByAddressFnc(client.get(), (uint64_t)DispatchMessageW),
	TED_DestroyGenericFnc };

while (true) {
	std::unique_ptr<TED_BreakpointResponse, TED_DestroyBreakpointFncPtr> response{
		TED_GetBreakpointFnc(client.get(), reader.get()),
		TED_DestroyBreakpointFnc };
	if (response != nullptr) {

		std::cout << "Read breakpoint event\t"
			<< std::format("Process ID: {}\tThread ID: {}\tSource Address: {:X}\tDestination Address: {:X}",
				response->processId, response->threadId, response->sourceAddress, response->destinationAddress)
			<< std::endl;

		std::cout << "Context: \n";
			// Output context information here (see proto definition)

		std::cout << "Call stack: \n";
		if (response->callStack.stackFramesCount > 0) {
			for (size_t i{ 0 }; i < response->callStack.stackFramesCount; i++) {
				const auto* stackFrame = response->callStack.stackFrames[i];
				std::cout
					<< std::format("RIP:{:X} Return Address:{:X} Frame Pointer:{:X} Stack Pointer:{:X}",
						stackFrame->rip,
						stackFrame->returnAddress,
						stackFrame->framePointer,
						stackFrame->stackPointer)
					<< std::format(" Param 1:{:X} Param 2:{:X} Param 3:{:X} Param 4:{:X}",
						stackFrame->parameters[0],
						stackFrame->parameters[1],
						stackFrame->parameters[2],
						stackFrame->parameters[3]);
				if (stackFrame->symbols.functionNameLength > 0) {
					std::cout << " " << stackFrame->symbols.functionName;
				}

				std::cout << std::endl;
			}
		}
	}
}

Running the above code will produce the following output:

Read breakpoint event   Process ID: 78648       Thread ID: 73480        Source Address: 7FFF6EE17A20    Destination Address: 7FFF6EE17A22
Context:
Call stack:
Read breakpoint event   Process ID: 78648       Thread ID: 73480        Source Address: 7FFF6EE17A20    Destination Address: 7FFF6EE17A22
Context:
Call stack:
Read breakpoint event   Process ID: 78648       Thread ID: 73480        Source Address: 7FFF6EE17A20    Destination Address: 7FFF6EE17A22
Context:
Call stack:

The output should flood the screen as you scroll through the Notepad UI since DispatchMessage will be constantly invoked. As the output seems to hint at, it is possible to get context and call stack information back to the caller. This can be done by setting the appropriate options to have the Core DLL return this information back to the client.

// Set options
TED_Options breakpointOptions{};
breakpointOptions.returnCallStack = true;
breakpointOptions.returnContext = true;
breakpointOptions.returnSymbolInfo = true;

std::unique_ptr<TED_GenericResponse, TED_DestroyGenericFncPtr> options{
	TED_SetOptionsFnc(client.get(), &breakpointOptions),
	TED_DestroyGenericFnc };

After setting these options and re-running, the output contains call stack and symbol information:

Read breakpoint event   Process ID: 59016       Thread ID: 16644        Source Address: 7FFF6EE17A20    Destination Address: 7FFF6EE17A22
Context:
Call stack:
RIP:7FFF6EE17A22 Return Address:7FF60B6794D5 Frame Pointer:0 Stack Pointer:12748FFC68 Param 1:15D25B5 Param 2:15D25B5 Param 3:12748FFCD9 Param 4:0 DispatchMessageW
RIP:7FF60B6794D5 Return Address:7FF60B6A5DBA Frame Pointer:0 Stack Pointer:12748FFC70 Param 1:1 Param 2:1 Param 3:0 Param 4:0
RIP:7FF60B6A5DBA Return Address:7FFF6EC9244D Frame Pointer:0 Stack Pointer:12748FFD40 Param 1:0 Param 2:0 Param 3:0 Param 4:0
RIP:7FFF6EC9244D Return Address:7FFF702ADF78 Frame Pointer:0 Stack Pointer:12748FFD80 Param 1:0 Param 2:0 Param 3:0 Param 4:0 BaseThreadInitThunk
RIP:7FFF702ADF78 Return Address:0 Frame Pointer:0 Stack Pointer:12748FFDB0 Param 1:0 Param 2:0 Param 3:0 Param 4:0 RtlUserThreadStart

To show some of the power of the TED API, I have developed a client application that will log all CALL instructions that a target application executes. I then used this application against Portal 2, attaching to client.dll, with the goal of finding the code that is executed when a player shoots their portal gun.

To do this, I first set the Call Logger application to automatically disable incoming breakpoints (Options -> Auto disable incoming from the menu). This allows you to filter out irrelevant events – and there will be plenty – when searching. After clearing the breakpoints window (Filter -> Clear from the menu), running around in-game, clearing again, going back to the game and running around again, I got to a point where most of the irrelevant breakpoints were automatically disabled and the breakpoints window was not being flooded with new rows.

At this point, I turned off the auto disable functionality, and began shooting the portal gun in the game. In the breakpoints window, I tracked the hit counts of source addresses and found those that incremented by one when I shot the portal gun. Right clicking a row and selecting “Copy addresses” from the context menu gave me the appropriate address information:

Source: 0x2ba8c0dc (client.dll+0x59c0dc) 	 Destination: 0x2bac93b0 (client.dll+0x5d93b0)

The next step is to restart Portal 2, attach x64dbg to it, and navigate to client.dll+0x59c0dc. Doing so gets you to the following place:

2E22C0C7 | 8B4D 18                  | mov ecx,dword ptr ss:[ebp+18]           |
2E22C0CA | 8B55 14                  | mov edx,dword ptr ss:[ebp+14]           |
2E22C0CD | 8B06                     | mov eax,dword ptr ds:[esi]              |
2E22C0CF | 51                       | push ecx                                |
2E22C0D0 | 8B4D 08                  | mov ecx,dword ptr ss:[ebp+8]            |
2E22C0D3 | 52                       | push edx                                |
2E22C0D4 | 8B50 74                  | mov edx,dword ptr ds:[eax+74]           |
2E22C0D7 | 53                       | push ebx                                |
2E22C0D8 | 57                       | push edi                                |
2E22C0D9 | 51                       | push ecx                                |
2E22C0DA | 8BCE                     | mov ecx,esi                             |
2E22C0DC | FFD2                     | call edx                                |
2E22C0DE | 5F                       | pop edi                                 |
2E22C0DF | 5E                       | pop esi                                 |
2E22C0E0 | 5B                       | pop ebx                                 |
2E22C0E1 | 5D                       | pop ebp                                 |
2E22C0E2 | C2 1400                  | ret 14                                  |

The call instruction at 0x2E22C0DC is the one that was triggered when the portal gun was fired. It naturally makes sense to set a breakpoint in x64dbg on this address. Doing so and navigating back to the game shows no effects. However, when the portal gun is fired, the breakpoint gets triggered, which lines up with what the Call Logger showed. When the breakpoint is hit, the following program state is present.

These registers don’t make much immediate sense, but EDI and EBX look interesting as they are the second and third arguments to the function and have integer values. Clicking to shoot several times shows that these values stay consistent. However, there is a change when you choose to shoot a blue portal versus an orange one (left click versus right click). Clicking the right mouse button to shoot gives the following values when the breakpoint gets hit:

The value in EBX has changed from 3 to 2, and the value in EDI has changed from 0x1C to 0xF8.

Looking up above, EBX and EDI are derived from what gets stored in EAX, which gets passed into the function that we’re looking at.

287EC0B8 | 8B45 10                  | mov eax,dword ptr ss:[ebp+10]           |
287EC0BB | 03C0                     | add eax,eax                             |
287EC0BD | 03C0                     | add eax,eax                             |
287EC0BF | 2BD8                     | sub ebx,eax                             |
287EC0C1 | 03F8                     | add edi,eax                             |
287EC0C3 | 85DB                     | test ebx,ebx                            |
287EC0C5 | 74 17                    | je client.287EC0DE                      |

But before getting side tracked further reverse engineering, the functionality of the Call Logger does seem to have been validated, and within around 5 minutes of playing around with it, the location of code responsible for handling a weapon fire event has been found. This is a rather quick discovery; the alternative usually being to set breakpoints on Windows APIs related to key presses and tracing from there to find the relevant application code.

The source code for the TED API, Call Logger, and other demo applications is available on GitHub. Visual Studio is used as the build IDE for the TED API and demo applications; the solution can opened in Visual Studio and 32 or 64-bit binaries can be built. As a prerequisite to building, a few external packages will need to be installed. The preferred way to do this is via vcpkg, and steps are shown to install the dependencies below:

git clone https://github.com/microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat

.\vcpkg install concurrentqueue:x64-windows-static
.\vcpkg install capstone:x64-windows-static
.\vcpkg install capstone[x86]:x64-windows-static
.\vcpkg install grpc:x64-windows-static

.\vcpkg install concurrentqueue:x86-windows-static
.\vcpkg install capstone:x86-windows-static
.\vcpkg install capstone[x86]:x86-windows-static
.\vcpkg install grpc:x86-windows-static

.\vcpkg integrate install

The Call Logger application is built with Qt 6 and can be loaded in Qt Creator (preferred), or built with CMake. A release version of the Call Logger for 32 and 64-bit applications can be found on the releases page of the TED GitHub repository.

August 1, 2022

Creating an ESP: Conclusion (4/4)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 8:43 PM

Table of Contents:

That wraps it up for how to create an extra-sensory perception (ESP) hack. Two important concepts were introduced in this series: the world-to-screen transformation, and hooking the underlying graphics API in order to draw information on the game’s screen. Both of these concepts are applicable outside of just developing ESP hacks. World-to-screen, although common in ESPs, can also be used in making bots for when you want your character to react to what is on the screen, i.e. moving towards, or away, from an enemy. Hooking the graphics API has tons of applications, including legitimate ones like drawing third-party overlays on the game’s screen.

The availability of the Source SDK code was a great help throughout both this series and in the creating an aimbot one. By having the source available, we were able to more easily reverse engineer the relevant interfaces and obtain pointers to them at runtime, and we were also able to lift the code responsible for performing the world-to-screen transformation.

The full source code for the ESP hack that was developed throughout this series is available on GitHub.

Creating an ESP: Drawing (3/4)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 8:42 PM

Table of Contents:

Now that we are at a point where we can get screen coordinates for an entity, the drawing part should be simple. We will start off with a very basic approach: drawing externally. This will be done by calling the DrawText function in GDI. We can create a function to achieve this as follows:

void DrawTextGDI(const Vector2& screenPosition, const std::string text) {

    auto windowHandle = FindWindow(L"Valve001", L"HALF-LIFE 2 - Direct3D 9");
    auto windowDC = GetDC(windowHandle);

    SetBkColor(windowDC, RGB(0, 0, 0));
    SetBkMode(windowDC, TRANSPARENT);
    SetTextColor(windowDC, RGB(0xFF, 0xA5, 0x00));

    RECT rect{};
    DrawTextA(windowDC, text.c_str(), text.length(), &rect, DT_CALCRECT);
    auto size{ rect.right -= rect.left };
    rect.left = static_cast<LONG>(screenPosition.x - size / 2.0f);
    rect.right = static_cast<LONG>(screenPosition.x + size / 2.0f);
    rect.top = static_cast<LONG>(screenPosition.y - 20);
    rect.bottom = rect.top + size;

    DrawTextA(windowDC, text.c_str(), -1, &rect, DT_NOCLIP);
}

Here we find the game window, get the device context, and draw the text. We can write a function to loop through the entity list and draw the text above enemy entities.

void DrawEnemyEntityText() {

    auto* serverEntity{ reinterpret_cast<IServerEntity*>(
    GetServerTools()->FirstEntity()) };

    if (serverEntity != nullptr) {
        do {
            auto* modelName{ serverEntity->GetModelName().ToCStr() };
            if (modelName != nullptr) {
                auto entityName{ std::string{GetEntityName(serverEntity)} };

                if (IsEntityEnemy(entityName)) {

                    auto enemyEyePosition{ GetEyePosition(serverEntity) };
                    Vector2 screenPosition{};
                    auto shouldDraw{ WorldToScreen(enemyEyePosition, screenPosition) };

                    if (shouldDraw) {
                        DrawTextGDI(screenPosition, entityName);
                    }
                }
            }

            serverEntity = reinterpret_cast<IServerEntity*>(
                GetServerTools()->NextEntity(serverEntity));

        } while (serverEntity != nullptr);
    }
}

Calling this function in a loop and looking at the results, we see the following

The results look pretty good. However, if your refresh rate is high enough, there will be a very noticeable flicker in the text. What is happening is that the game’s rendering is conflicting with our drawing; we are trying to constantly draw something on the screen (our text) and the game engine is also trying to constantly draw something on the screen (the player’s view). There are a couple of ways to get around this: you can use SetWindowLongPtr to subclass the window and install a new window procedure. This will allow you to handle WM_PAINT messages and draw your text. Or you can create an entirely new window to draw on and keep it activated at the foreground, though this approach has problems with games running in full screen mode.

Ideally we would want to render our text the same way that the game renders its graphics. This is possible, but it will require additional work. To have the game render our text, we will need to hook in to the function that gets executed after rendering. In Direct3D9 games this is the EndScene function, and will be our target. Fortunately, finding this function is pretty easy. Since Microsoft ships symbols for d3d9.dll, we can attach a debugger, load the symbols, and get the address.

From here we can create a function pointer to it as we did earlier for the interfaces

DWORD_PTR GetEndSceneAddress() {

    constexpr auto globalEndSceneOffset = 0x5C0B0;
    auto endSceneAddress{ reinterpret_cast<DWORD_PTR>(
    GetModuleHandle(L"d3d9.dll")) + globalEndSceneOffset };

    return endSceneAddress;
}

Now that the function address is known, we can install a hook on it. To do this, I will be reusing the HookEngine class from an earlier article series. The hook logic will be pretty simple: we will call the DrawEntityEnemyText function as before and then call the original EndScene function.

HRESULT WINAPI EndSceneHook(IDirect3DDevice9* device) {

    DrawEnemyEntityText(device);

    using EndSceneFnc = HRESULT(WINAPI*)(IDirect3DDevice9* device);
    auto original{ (EndSceneFnc)HookEngine::GetOriginalAddressFromHook(EndSceneHook) };
    HRESULT result{};
    if (original != nullptr) {
        result = original(device);
    }

    return result;
}

In DrawEnemyEntityText, instead of calling DrawTextGDI, we will write a new function, DrawTextD3D9, which will draw text using Direct3D APIs.

ID3DXFont* GetFont(IDirect3DDevice9* device) {

    static ID3DXFont* font{};

    if (font != nullptr) {
        return font;
    }

    if (device == nullptr) {
        std::cerr << "No device to create font for."
            << std::endl;
        return nullptr;
    }

    D3DXFONT_DESC fontDesc {
        .Height = 30,
        .Width = 0,
        .Weight = FW_REGULAR,
        .MipLevels = 0,
        .Italic = false,
        .CharSet = DEFAULT_CHARSET,
        .OutputPrecision = OUT_DEFAULT_PRECIS,
        .Quality = DEFAULT_QUALITY,
        .PitchAndFamily = DEFAULT_PITCH | FF_DONTCARE,
        .FaceName = L"Consolas"
    };

    auto result{ D3DXCreateFontIndirect(device, &fontDesc, &font) };
    if (FAILED(result))
    {
        std::cerr << "Could not create font. Error = "
            << std::hex << result
            << std::endl;
    }

    return font;
}

void DrawTextD3D9(const Vector2& screenPosition, const std::string text, IDirect3DDevice9* device) {

    RECT rect{};
    GetFont(device)->DrawTextA(nullptr, text.c_str(), text.length(), &rect, DT_CALCRECT, D3DCOLOR_XRGB(0, 0, 0));
    int size{ rect.right -= rect.left };
    rect.left = static_cast<LONG>(screenPosition.x - size / 2.0f);
    rect.right = static_cast<LONG>(screenPosition.x + size / 2.0f);
    rect.top = static_cast<LONG>(screenPosition.y - 20);
    rect.bottom = rect.top + size;
    GetFont(device)->DrawTextA(nullptr, text.c_str(), -1, &rect, DT_NOCLIP, D3DCOLOR_XRGB(0xFF, 0xA5, 0x00));
}

The DrawTextD3D9 function looks very close to DrawTextGDI, but it performs its drawing on the game’s IDirect3DDevice9 device instead of directly on top of the window. As a result, we are rendering our text in the same rendering pipeline, and the text flicker will not be present. You can see the before and after below.

We now have a functional proof of concept that performs world-to-screen transformation of entities and displays text above their heads. The steps shown throughout this series are common to all ESP hacks and can be used as a reference in building more complex ones.

Creating an ESP: World To Screen (2/4)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 8:41 PM

Table of Contents:

The first step for developing an ESP hack is being able to draw information on the screen at a specified position. While this seems like it should be easy at first, it is actually a rather complicated process. This is because objects exist in different vector spaces than the one that corresponds to your screen.

There is the local space, which puts the object’s center at the origin; world space, which is a common space that all objects live in; view space, which centers the camera at the origin and looks forward; clip space, which map objects to a fixed-size plane so that clipping can be done; and lastly, screen space, which is the window that is the two dimensional (x, y) space that corresponds to the window.

Going from one vector space to another involves multiplying the coordinates in one space by a transformation matrix to get the coordinates in the other space. For example, if you have coordinates in world space and you want to map them to view space, you would multiply those coordinates by a transformation matrix called a view matrix. Because matrix multiplication is associative, you can create a matrix that performs multiple transformations in one multiplication operation. This is where the world-to-screen matrix comes in: this matrix is a combination of a view matrix and a projection matrix whose purpose is to take three dimensional world space coordinates and map them to a two dimensional clip space. You can then transform the coordinates in the clip space to normalized device coordinates and adjust for the screen’s aspect ratio to get (x, y) coordinates in screen space.

Games will store some, or all, of these transformation matrices somewhere. Typically you will be able to find a world-to-screen matrix directly instead of having to find the view and projection matrices since a world-to-screen transformation happens so often. The process of reverse engineering a game to find these matrices is rather tedious: you will spend a lot of time panning your camera view around while scanning the process memory for values that you expect. For example, if you are looking directly down followed by directly up, you might expect the matrix to have values in [0, 1] or [-1, 1] within it. There are other ways such as trying to derive the matrix from your camera’s position and angles, and then scanning for that as well. All of these approaches are rather involved and feature a lot of trial and error; there can be an entire series of posts dedicated to reverse engineering view matrices in games.

This tedium will be removed in this series because the Source SDK is open source and provides an interface that allows users to get the world to screen matrix. As before, we will get a pointer to this interface and be able to access the WorldToScreenMatrix function. This is done by scanning for “VEngineClient014” in the referenced strings of the running game. After attaching a debugger and searching, we can find it pretty quickly

Looking at how it’s used, we can easily obtain the function pointer to the global interface

As in the previous series, we can construct the function to retrieve the interface as such

IVEngineClient* GetClientEngine() {

    constexpr auto globalGetClientEngineOffset{ 0xA3B30 };
    static GetClientEngineFnc getClientEngineFnc{ GetFunctionPointer<GetClientEngineFnc>(
        "engine.dll", globalGetClientEngineOffset) };

    return getClientEngineFnc();
}

Now the fun can begin. We have the ability to get the world-to-screen matrix, but we don’t know how to perform the actual transformation. There is an example of the world-to-screen matrix being used to perform a transformation in the ScreenTransform function. This function passes in the world-to-screen matrix to the FrustumTransform function, which is the function that performs the actual transformation. The FrustumTransform function performs the matrix multiplication to transform between the vector spaces, and in this case, it will transform the input point in world space to a (x, y) position in clip space. To go from clip space to screen space, we need to see how ScreenTransform is called. Fortunately, there is a helpful function called GetVectorInScreenSpace that shows the viewport transformation to screen space.

For our purposes, we can lift these from the Source SDK and incorporate them into the ESP hack with minor modifications. FrustumTransformation will stay more or less as it was

bool FrustomTransform(const VMatrix& worldToSurface, const Vector3& point, Vector2& screen) {

    screen.x = worldToSurface.m[0][0] * point.x + worldToSurface.m[0][1] * point.y + worldToSurface.m[0][2] * point.z + worldToSurface.m[0][3];
    screen.y = worldToSurface.m[1][0] * point.x + worldToSurface.m[1][1] * point.y + worldToSurface.m[1][2] * point.z + worldToSurface.m[1][3];
    auto w = worldToSurface.m[3][0] * point.x + worldToSurface.m[3][1] * point.y + worldToSurface.m[3][2] * point.z + worldToSurface.m[3][3];

    bool facing{};
    if (w < 0.001f)
    {
        facing = false;
        screen.x *= 100000;
        screen.y *= 100000;
    }
    else
    {
        facing = true;
        float invw = 1.0f / w;
        screen.x *= invw;
        screen.y *= invw;
    }

    return facing;
}

and we can modify GetVectorInScreenSpace slightly to only return true for positions that are visible in screen space. The function has been renamed to WorldToScreen for more clarity as well

bool WorldToScreen(const Vector3& position, Vector2& screenPosition) {

    auto worldToScreenMatrix{ GetClientEngine()->WorldToScreenMatrix() };

    auto facing{ FrustomTransform(worldToScreenMatrix, position, screenPosition) };

    int screenWidth{}, screenHeight{};
    GetClientEngine()->GetScreenSize(screenWidth, screenHeight);
    screenPosition.x = 0.5f * (1.0f + screenPosition.x) * screenWidth;
    screenPosition.y = 0.5f * (1.0f - screenPosition.y) * screenHeight;

    auto visible{ (screenPosition.x >= 0 && screenPosition.x <= screenWidth) &&
        screenPosition.y >= 0 && screenPosition.y <= screenHeight };
    if (!facing || !visible)
    {
        screenPosition.x = -640;
        screenPosition.y = -640;
        return false;
    }

    return true;
}

We can modify the aimbot code to print out the (x, y) screen coordinates of the closest enemy to the console to test this function out. After making the appropriate adjustments, we get the following results

which seem like legitimate numbers given the window size and resolution. The transformation appears to be successful! We are now able to translate an enemy’s world position to screen space. The first step in developing the ESP hack is done, now on to drawing.

Creating an ESP: Introduction (1/4)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 8:40 PM

Extra-Sensory Perception (ESP) hacks are a type of game hack that involve showing information to the player that they would not normally see. For example, these types of hacks might display an enemies’ position, their distance from the player, their health, what weapon they are using, and so on. They can also be more elaborate and change the enemies’ model to a more visible color, or draw a bounding box around them. This is all done with the purpose of providing the player with additional information that would not normally be visible to them. A few examples of ESP hacks are shown below which demonstrate this.

This next series of posts will go over how these hacks are created and provide a working proof of concept for an ESP targeting Half-Life 2. The series will start off by talking about a world-to-screen transformation: how models with a three dimensional position in the world get transformed to a two dimensional x and y coordinates system on your screen. Having established that, the series will then go over how to draw information on the game’s screen, and wrap up by showing an ESP proof of concept that draws some text over enemy models.

These posts will heavily leverage what was covered in the creating an aimbot series. It is recommended to read that first to get a better understanding of the Source SDK. Code from that series will also be heavily re-used for this series.

A link to each post is provided below:

Older Posts »

Powered by WordPress