Hacking Star Wars Battlefront II (Classic)

Foreword

Already done this but I wanna get it out before I forget everything.
I don’t know exactly how I got some addresses/functions but I’ll use the most direct way that I’ll find now. I sat way longer on this than it’s here explained in one sentence.

Reverse Engineering is trying out many things, till one succeeds. So try harder (and maybe take a break).
The order of finding those things are probably wrong too.

What is Star Wars Battlefront II

SWBF2 is a first/third person shooter set in the Star Wars Universe. Developed by Pandemic Studios and published by LucasArts in 2004 for PS2, Xbox and Windows. The player can choose between two teams (total 4 factions) and 6 classes (Infantry, Heavy, Engineer, Sniper and 2 unique). Each faction includes a hero character, which can be unlocked through points (and not like 80€ for some sense of pride and accomplishment).

Okay enough circlejerking. If you don’t know the Battlefront series you’ve missed out. There are also space battles or something like that.

Starting out

I’m using as always Ghidra (for static analysis), Cheat Engine (dynamic analysis) and sometimes ReClass.NET (memory struct dissector). I started out by dumping the game (which I didn’t use) and putting the executables into Ghidra, only to notice there is no real difference ‘cause it’s not packed or anything.
After enough time to watch some videos it finally analyzed, so let’s get into hacking.

Referencing Strings

It’s a good idea to start by looking at some strings:
Under all those we find Lua 5.0.2, lua_debug> .
Does this game use lua? For what? Is the whole game written in lua? Or only some parts?

If you didn’t know: Lua is a scripting language that’s often used in games to enable scripting/modding. Some noticable games that use this are World Of Warcraft (for the UI) and Garry’s Mod (almost the whole game).
A quick gurgle shows that all the missions are written in lua. There are even modding tools. What a shame that today nobody does that anymore today.

But luckily for us that means that many variables that are passed to lua are referenced as strings too.

Here something I found quickly:

  iVar4 = iHeroRespawnVal;
  iVar3 = iHeroRespawn;
  iVar2 = iHeroUnlockVal;
  iVar1 = iHeroTeam;
  iVar5 = iHeroUnlock;
  if (iHeroUnlock != 3) {
    iVar5 = 5;
  }
  if (DAT_003e8238 < 4) {
    local_8 = 1;
  }
  else {
    local_8 = 7;
    if (DAT_003e8238 < 7) {
      local_8 = 4;
    }
  }
  FUN_0029bdb0(DAT_01a579a0);
  FUN_001a14d0("iHeroUnlock",iVar5);
  FUN_001a14d0("iHeroUnlockVal",iVar2);
  FUN_001a14d0("iHeroPlayer",local_8);
  FUN_001a14d0("iHeroRespawn",iVar3);
  FUN_001a14d0("iHeroRespawnVal",iVar4);
  FUN_001a14d0("iHeroTeam",iVar1);

We can change iHeroUnlockVal ingame to 0 and instantly become Darth Maul! That’s a sense of accomplishment!
Joining a multiplayer server shows that we can’t do this there though… Seems like those are server side variables.

There is also a huge list of functions that are passed to lua:

Lua Functions

Found the Entity Class

Some of the interesting functions are GetEntityPtr, GetEntityClassPtr. Let’s set a breakpoint there and get the return value. Put it in ReClass and find out the struct:

EntitySoldier

We also find there the health and stamina. We can also change it: Unlimited Power!
Search for a list with cheat engine (search for pointers) and we find a list! But only the players are listed? And if we have several how do we find out which one we are?

Joining a multiplayer server shows the dissappointing truth: There are only like 3 entities there and those aren’t updated. Again a serverside variable.
That’s why you should host a dedicated server and join with your client.

Where to go from there

But I’ve got a idea: Let’s search for our health and find other variables that contain that. Then join another server and hope one of those stayed. And we found one I called lovingly smthPlayerWeaponsHealthStaminaVector (because it’s referenced in a vector deconstructor):

SmthPlayerVector

After that I had a small mental breakdown. All those classes I dissected are useless? We can reuse them but I didn’t know that then.

The next day

Let’s cross reference that vector: We find a function we call “level.hints” and inside that:

Vector Cross Reference

A address… Put it into ReClass and RTTI shows the class is called CameraManager. Jackpot? Apparently:

Camera Manager

There are apparently several cameras and all of them are here, including positions.

Now we only need a entitylist…

The real entity list

I don’t know exactly how I got this list, but here is one possible way

Let’s xref the camera manager and look for loops and other interesting addresses:

Character List

Here some address is used in a for loop (uVar5 = i). If we take a look at it in ReClass we see that it’s a array (actually vector) filled with the ‘Character’ class. This class has all we need! Our name, the entity class (the EntitySoldier from before), the team, etc…

Character

It even has all the bots and works in multiplayer. One small problem:
Our EntitySoldier class doesn’t match up. RTTI says those are both called EntitySoldier though.
Inside the one found in Character (called it PlayerEntity), we find a pointer to a “valid” EntitySoldier though, as well some other info like position rotation etc.

But how find we out at which index we are? Is there like a localPlayer pointer or a localPlayerIndex?
It’s quite simple: The local player is always at 0 (according to my findings).

Enough researching

Time to write shit. I always start with the overlay because it’s mostly c+p. I’m using imgui as a ui library and minhook to hook functions (because it’s a lightweight detours variant). The game uses dx9 to render, so it should be just copying my usual dx9 hook right? Not exactly. I’m using the steam overlay (gameoverlayrenderer.dll) to render my stuff, so I don’t need to search any dxdevice or render functions (post by aixxe).

But let’s hook it:

Hooks::PresentAddr = (std::uintptr_t)(Utils::PatternScan(GetModuleHandle("gameoverlayrenderer.dll"), Signatures::Present) + 2);
Hooks::ResetAddr = (std::uintptr_t)(Utils::PatternScan(GetModuleHandle("gameoverlayrenderer.dll"), Signatures::Reset) + 2);

// Init Detours
MH_Initialize();

// Hooks
MH_CreateHook(**(void***)Hooks::PresentAddr, &Hooks::hkPresent, (void**)(&Hooks::oPresent));
MH_CreateHook(**(void***)Hooks::ResetAddr, &Hooks::hkReset, (void**)(&Hooks::oReset));

MH_EnableHook(MH_ALL_HOOKS);

I initialize in hkPresent and swap the WndProc with ours:

HRESULT __stdcall Hooks::hkPresent(IDirect3DDevice9* thisptr, const RECT* src, const RECT* dest, HWND wndOverride, const RGNDATA* dirtyRegion)
{
	static bool initialized = false;

	if (!initialized)
	{
		InitImGui(thisptr);

		initialized = true;
	}
	
	ImGui_ImplDX9_NewFrame();
	ImGui_ImplWin32_NewFrame();
	ImGui::NewFrame();
	// Start here with custom stuff

	...

	// Custom stuff done here
	ImGui::EndFrame();

	// Rendering
	ImGui::Render();
	ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());

	return Hooks::oPresent(thisptr, src, dest, wndOverride, dirtyRegion);
}

void InitImGui(IDirect3DDevice9* device)
{
	EnumWindows(findGameHwnd, GetCurrentProcessId()); // sets Hooks::oHwnd by iterating through windows and comparing process ids
	ImGui::CreateContext();

	ImGui_ImplWin32_Init(Hooks::oHwnd);
	ImGui_ImplDX9_Init(device);

	...

	oWndProc = reinterpret_cast<WNDPROC>(SetWindowLongPtr(Hooks::oHwnd, GWLP_WNDPROC, LONG_PTR(Hooks::hkWndProc)));
}

LRESULT __stdcall Hooks::hkWndProc(HWND window, UINT message_type, WPARAM w_param, LPARAM l_param)
{
	ImGui_ImplWin32_WndProcHandler(window, message_type, w_param, l_param);

	// Handle Key Press Events here
    ...

	return CallWindowProc(oWndProc, window, message_type, w_param, l_param);
};

This should draw on screen and let the menu accept input.

First Draw

As expected it works; but my mouse input doesn’t. What’s up with that? Put this into hkWndProc and set a breakpoint.

if (message_type == WM_LBUTTONDOWN)
    auto a = 0;

And it never breaks… uhh fuck.

Some searching in Ghidra and we find the problem

Direct Input

Seems like direct input is used to handle input (duh) and prevents the mouse messages from getting through.
Some ideas:

  • Hook DirectInput (never done that)
  • Passthrough the mouse messages to wndproc
  • Don’t use a mouse based menu

I looked into directinput shortly but there are no good tutorials, only smth like: copy this and you’re done. I couldn’t even find anything where the input is handled by directinput, because every stackoverflow thread said ‘don’t use directinput’. I hate all of you.

Let’s do that later

Push that idea to a later time and make a non-mouse based menu (kinda like MasterLooser’s).

I whip out some shitty code and transfer wndproc input directly to the menu.

void Menu::Input(WPARAM param)
{
	switch (param)
	{
		case VK_UP:
		case VK_DOWN:
		{
			itemIndex += (param == VK_UP ? -1 : 1);
			// Normalize
			if (itemIndex < 0) //went down
				itemIndex = items.size() - 1;
			if (itemIndex >= items.size()) //went up
				itemIndex = 0;

			break;
		}
		case VK_LEFT:
		case VK_RIGHT:
		{
			if (itemIndex < 0 || itemIndex >= items.size())
				itemIndex = 0;

			MenuItem item = items[itemIndex];

			int val = *(int*)item.val;
			val += (param == VK_LEFT) ? -1 : 1;
			if (val < item.min) //went down
				val = item.max;
			if (val > item.max) // went up
				val = item.min;
			*(int*)item.val = val;
			break;
		}
	}
}

The code isn’t really important here, but the friends we made on the way.

I’d like to add a picture here but my visual studio just decided to fuck off.

Well that worked out great right? Time to make a wallhack. What do we need for that?

  • A renderer
  • Some Math
  • Our eye position
  • The enemy position
  • The view matrix
  • The screen resolution

As a renderer I’m using some imgui renderer I stole. It just draws a giant transparent window and then uses imgui to draw lines and text on there.

For a world2screen function: You do some fucky wucky math and hopefully your input position (the enemy position) is now screen coordinates.

For that we need a view matrix though. There is a easy way to find it:

  • Attach cheat engine
  • Look up (ingame, that’s important)
  • search for 1.0f
  • Look down
  • search for -1.0f
  • repeat till you get some addresses

Problem with this game: You can’t look fully up. Maybe that’s why stormtroopers have such bad aim…
The plan here is:

  • Search for a initial unknown float value
  • Look a bit more up
  • Search for a increased value
  • Look down
  • Search for a decreased value
  • Repeat

You should get around 10-20 addresses. I just searched those and where they get written from till I found one that get’s calculated with some d3d functions.

D3DXMatrixMultiply(&uStack224,&viewTransformationMatrix,&projectionTransformationMatrix);
_viewMatrix = CONCAT88(uStack216,CONCAT44(uStack220,uStack224));

Combine that with the CameraManager position and the enemy position from the loop and voila:

ESP

It works! Too good…

Mistakes

One mistake I made:
I used
(CameraManager*)GetRelativeOffset(Offsets::cameraManager)
to get the CameraManager instead of
*(CameraManager**)GetRelativeOffset(Offsets::cameraManager).
Impressively it still worked (except when dead or in third person).

Another problem is: When no text is on the currently rendered my text is just a colored square (not my image, but same error):

Square Text

You can see the bounding box of the character but not the character itself.
And this is where I was stuck for too long.
I’m no expert in DirectX or OpenGL so I can’t do much on these errors except google and trial-and-error. Some people had the same thing but I found no real solution. Probably something wrong with the render target or render state.

I tried looking into where the text gets drawed or the render state gets set, but to no avail.

I found these nice tools like PIX, apitrace and 3DReaperDX, which are probably really good tools if they work…
Worked on other dx9 applications but not on swbf2.

Then I came up with the idea of looking on Github for Star Wars Battlefront II and how other people render stuff.
The problem of having two games with the same name is that the newer one gets always found and many forums only have info on the new one.
On page 5+ I found this repo by someone who was probably sent by some divine force.
It’s a patch to play the game on dx11 and do something with shaders. They even used imgui to draw a debug menu!

This commit called “temp hack fix for ImGui rendering” added three lines of code to the renderstate init:

g_pd3dDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
g_pd3dDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);

I only use two because the other one didn’t make any difference (hopefully).

And he hooked DirectInput! I’ll just yoink that…

DirectInput Hook

To hook DirectInput you apparently just need to do:

  • Call DirectInput8Create
  • Call dinput->CreateDevice for Mouse/Keyboard, depending on what you want to hook
  • Hook GetDeviceState/SetCooperativeLevel (if you need it)
LPDIRECTINPUT8 dinput;
auto result = DirectInput8Create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8A, (void**)&dinput, nullptr);

if (FAILED(result))
	return;

IDirectInputDevice8A* device;
dinput->CreateDevice(GUID_SysMouse, &device, nullptr);

auto func = (void*)get_virtual(device, 9);
MH_CreateHook(func, Hooks::hkGetDeviceState, (void**)&Hooks::oGetDeviceState);

And then the hkGetDeviceState (he wrote it so nice that I just stole it)

HRESULT __stdcall Hooks::hkGetDeviceState(IDirectInputDevice8A* const self, DWORD cbData, LPVOID lpvData)
{
	const auto devtype = getDeviceType(*self);

	if (UI::_visible) 
	{
		if (devtype == DI8DEVTYPE_MOUSE && cbData == sizeof(DIMOUSESTATE2))
		{
			DIMOUSESTATE2 state{};

			Hooks::oGetDeviceState(self, sizeof(state), &state);

			auto& io = ImGui::GetIO();

			std::transform(std::begin(state.rgbButtons),
				std::begin(state.rgbButtons) +
				min(std::distance(std::begin(io.MouseDown),
					std::end(io.MouseDown)),
					std::distance(std::begin(state.rgbButtons),
						std::end(state.rgbButtons))),
				std::begin(io.MouseDown),
				[](const auto v) { return v != 0; });

			io.MouseWheel += static_cast<float>(state.lZ);
		}

		std::memset(lpvData, 0, cbData);

		return DI_OK;
	}

	return Hooks::oGetDeviceState(self, cbData, lpvData);
}

Now we can finally get rid of that text ui and go back to the nice old one. I should probably redesign it. I’ll do it later…

I won’t

Afterword

I actually wanted to write a aimbot and some other stuff, but then Terraria 1.4 came out and destroyed my sleep cycle.
Also I had way too much fun playing around while testing out stuff and laughing about references.
The prequels have such memorable memeable quotes…

Bonus

Some Cheat Engine Scripts I wrote to iterate over the Entity List (the wrong one though)

function toHex(int)
         return string.format("%02X", int)
end

base = getAddress("BattlefrontII.exe")
print("Base: " .. toHex(base))
address=0x1ba8848+base
print("EntityList: " .. toHex(address))
address = readPointer(address)
count = readInteger(address + 4) + 1
print("Count: " .. count)
cur = address + 0xc

for i=1,count do
      obj = readPointer(cur)
      name = obj + 0x8
      name = readPointer(name)
      name = readPointer(name + 0x20)
      nameStr = readString(name,0x20,true)
      if nameStr then
         print(toHex(cur) .. ": " .. nameStr)
      else
          print("-" .. toHex(obj))
      end
      cur = cur + 0x1c
end