Teeworlds is still a popular game on my university’s LAN parties and it just so happens that another long-awaited LAN party will take place this Friday – this is a great opportunity to play around with the game’s internals!
What is Teeworlds?
There is a public blog as well as a download-section for compiled binaries and source-code on the game’s website and a git-repository on Github. Judging by the dates of the blog-posts and git-commits, development on the game was started sometime around 2007 and is still going.
1. Getting started: Binaries and Resources
I started by simply downloading both binaries (Windows 64-bit) and source code of version 0.6.5 of the game from the download-section.
There are few binaries: mainly the game client and server. I was glad to see that, contrary to games using large engines, code was not being split into separate modules. While this resulted in a larger single binary I did not need to figure out how certain modules were used in different places all over the game and ultimately this made working with the game easier.
Breaking the code down roughly, it is structured in client-side, server-side and shared code which not only makes developing the game easier and avoids a multitude of potential bugs (like differing client/server implementations of data-structures or protocols, hint: DRY principle) but also conveniently played into my hands because I didn’t need to take care of any conversions [of any data-structures] when communicating with servers.
The make-configuration suggests that the game will be either compiled with MSVC or MinGW [for Windows]. Depending on which compiler was used and what flags were passed to it, important and useful information will either be stripped or left in the binary.
2. Static analysis
To gather general information about the file I used FileAlyzer: it parses executables (PE), shows header information, sections, imports, exports, computes hashes, features a hex-viewer and presents general information about files if available. So I loaded the file into the tool. It showed no exports (not unusual for executables), few imports (very unusual for a game with an integrated engine) and most notably, a huge section “/19”.
Inspecting the section in hex view cleared things up. It showed what seemed to be the compiler that was used along with the arguments that were passed to it.
Also it seemed that the compiler left a symbol table in the binary:
2E:5600h: 43 44 61 74 61 57 65 61 70 6F 6E 73 70 65 63 52 CDataWeaponspecR
2E:5610h: 69 66 6C 65 00 18 12 6B 50 25 00 00 09 6A 71 00 ifle...kP%...jq.
2E:5620h: 00 12 6D D1 23 00 00 00 08 6D 5F 52 65 61 63 68 ..mÑ#....m_Reach
2E:5630h: 00 12 6E DD 05 00 00 08 08 6D 5F 42 6F 75 6E 63 ..nÝ.....m_Bounc
2E:5640h: 65 44 65 6C 61 79 00 12 6F C7 00 00 00 0C 08 6D eDelay..oÇ.....m
2E:5650h: 5F 42 6F 75 6E 63 65 4E 75 6D 00 12 70 C7 00 00 _BounceNum..pÇ..
2E:5660h: 00 10 08 6D 5F 42 6F 75 6E 63 65 43 6F 73 74 00 ...m_BounceCost.
2E:5670h: 12 71 DD 05 00 00 14 00 07 43 44 61 74 61 57 65 .qÝ......CDataWe
2E:5680h: 61 70 6F 6E 73 70 65 63 4E 69 6E 6A 61 00 18 12 aponspecNinja...
2E:5690h: 73 B3 25 00 00 09 6A 71 00 00 12 75 D1 23 00 00 s³%...jq...uÑ#..
2E:56A0h: 00 08 6D 5F 44 75 72 61 74 69 6F 6E 00 12 76 C7 ..m_Duration..vÇ
2E:56B0h: 00 00 00 08 08 6D 5F 4D 6F 76 65 74 69 6D 65 00 .....m_Movetime.
2E:56C0h: 12 77 C7 00 00 00 0C 08 6D 5F 56 65 6C 6F 63 69 .wÇ.....m_Veloci
2E:56D0h: 74 79 00 12 78 C7 00 00 00 10 00 07 43 44 61 74 ty..xÇ......CDat
This data represents these structs defined in src\game\generated\client_data.h:
struct CDataWeaponspecRifle
{
CDataWeaponspec* m_pBase;
float m_Reach;
int m_BounceDelay;
int m_BounceNum;
float m_BounceCost;
};
struct CDataWeaponspecNinja
{
CDataWeaponspec* m_pBase;
int m_Duration;
int m_Movetime;
int m_Velocity;
};
I hadn’t worked with GCC compiled binaries before so I didn’t expect to see any of this. Next I loaded the file into IDA and was happy to see that all the functions had correct names assigned to them:
3. Hacking intensifies
Browsing the .bss-section I quickly found some very useful global variables:
.bss:000000000056E0E0 public g_GameClient
.bss:000000000056E0E0 ; CGameClient g_GameClient
.bss:000000000056E0E0 g_GameClient CGameClient <?> ; DATA XREF: Evolve(CNetObj_Character *,int)+46↑o
.bss:000000000056E0E0 ; CGameClient::OnPredict(void)+416↑o ...
“GameClient” sounded promising: one could assume you could use this to get access to a lot of interesting stuff. So I looked up the class in the source-code:
class CGameClient : public IGameClient
{
//[...]
class IEngine *m_pEngine;
class IInput *m_pInput;
class IGraphics *m_pGraphics;
class ITextRender *m_pTextRender;
class IClient *m_pClient;
class ISound *m_pSound;
// etc.
public:
//[...]
CSnapState m_Snap;
//[...]
// hooks
virtual void OnConnected();
virtual void OnRender();
virtual void OnRelease();
virtual void OnInit();
virtual void OnConsoleInit();
virtual void OnStateChange(int NewState, int OldState);
virtual void OnMessage(int MsgId, CUnpacker *pUnpacker);
virtual void OnNewSnapshot();
virtual void OnPredict();
virtual void OnActivateEditor();
virtual int OnSnapInput(int *pData);
virtual void OnShutdown();
virtual void OnEnterGame();
virtual void OnRconLine(const char *pLine);
virtual void OnGameOver();
virtual void OnStartGame();
//[...]
// pointers to all systems
class CGameConsole *m_pGameConsole;
class CBinds *m_pBinds;
class CParticles *m_pParticles;
// etc.
};
Copying the source-code of classes of interest into my own hack allowed me to use their definitions and access instances created by the game within my code. Also I made sure to maintain the class-hierarchy so I would be able to call virtual functions later.
Knowing the exact location of g_GameClient (“teeworlds.exe” + 0x16E0E0) and the exact signatures of functions of interest (e.g. CControls::OnMouseMove(float x, float y)), I could install VMT-hooks (using aixxe’s class which worked great) and test some things out. It took me some time to figure out how specific bits worked (e.g. IGraphics::MapToScreen) but there was a lot of references I could look at that made use of said bits. The following code would hook two functions in the game and print my player’s location in the upper right corner of the screen when I was connected to a gameserver:
typedef void(*OnRender)(void*);
typedef void(*OnMouseMove)(void*, float, float);
VMTHook* hudHook;
CGameClient* pGameClient = (CGameClient*)((char*)GetModuleHandleA("teeworlds.exe") + 0x16e0e0);
CHud* pHud = (CHud*)((char*)GetModuleHandleA("teeworlds.exe") + 0x272280);
bool hooked = false;
void hkRender(void* thisptr)
{
hudHook->GetOriginalFunction<OnRender>(6)(thisptr);
//Copied from hud.cpp: CHud::OnRender()
if (!pGameClient->m_Snap.m_pGameInfoObj) //hud.cpp
return;
float width = 300 * pGameClient->Graphics()->ScreenAspect();
float height = 300.0f;
pGameClient->Graphics()->MapScreen(0.f, 0.f, width, height);
//Custom code:
char buffer[128];
auto localPlayer = GetLocalPlayer();
success = EnemyVisible(vec2(localPlayer->m_Cur.m_X, localPlayer->m_Cur.m_Y), vec2(localPlayer->m_Cur.m_X, localPlayer->m_Cur.m_Y + 20), result);
sprintf_s(buffer, "X: %d, Y: %d", l(int)localPlayer->m_Cur.m_X, (int)localPlayer->m_Cur.m_Y);
pGameClient->TextRender()->Text(0, 10, 10, 8, buffer, -1);
}
void Hook()
{
hudHook = new VMTHook(pHud);
hudHook->HookFunction((void*)hkRender, 6);
}
4. The Hack
I decided that to conclude this project I should write a simple aimbot that aims at any visible (hostile) player. This required iteration of all players and filtering out the local player, inactive and not visible players and team-members. CGameClient::CSnapState holds information about the ID of the local player and an array of all players containing information about status and position of each player. This could be used to filter out the local player and inactive players – in order to filter out team-members I searched the code for “team” and found that g_CGameClient.m_Snap.m_pGameInfoObj->m_GameFlags is set to GAMEFLAG_TEAMS indicating team-based matches.
At first I expected filtering out invisible players to be quite a lot of work as common keywords often used in classes and methods associated with visibility-checks like “trace”, “ray”, “raycast” yielded no results. After some time I stumbled upon server-side code that appears to checks for collision with players.
bool CLaser::HitCharacter(vec2 From, vec2 To)
{
vec2 At;
CCharacter *pOwnerChar = GameServer()->GetPlayerChar(m_Owner);
CCharacter *pHit = GameServer()->m_World.IntersectCharacter(m_Pos, To, 0.f, At, pOwnerChar);
if(!pHit)
return false;
m_From = From;
m_Pos = At;
m_Energy = -1;
pHit->TakeDamage(vec2(0.f, 0.f), GameServer()->Tuning()->m_LaserDamage, m_Owner, WEAPON_RIFLE);
return true;
}
Unfortunately, CGameWorld (the class of IGameServer::m_World) was not available in client-side code and thus a bad choice to go after. Luckily, a few lines under the code posted above there was this call that could maybe be abused for visibility-checks:
if(GameServer()->Collision()->IntersectLine(m_Pos, To, 0x0, &To))
IntersectLine is defined in CCollision and CGameClient conveniently happens to hold a reference to a (global) CCollision instance. The method is not virtual so I could not call it from a vtable but a quick search in IDA led me to the correct address.
typedef int(*IntersectLine)(void*, vec2 pos0, vec2 pos1, vec2* pOutCollision, vec2 *pOutBeforeCollision);
IntersectLine intersect = (IntersectLine)((char*)GetModuleHandleA("teeworlds.exe") + 0x3646A);
bool EnemyVisible(vec2 from, vec2 to, bool& res)
{
res = false;
CCollision* coll = pGameClient->Collision();
if (!coll)
return false;
res = !intersect(coll, from, to, NULL, NULL);
return true;
}
A quick test where I ran the function using my player’s position and a coordinate right below its feet showed that my code was working: when it stood on ground there was collision, when it jumped there was not.
Now all I needed was control over the direction my player looked at. A quick search for “input” led me to CControls that provides OnRender and OnMouseMove methods, allowing for hooking mouse-movement and set the mouse-position myself:
typedef void(*OnRender)(void*);
typedef void(*OnMouseMove)(void*, float, float);
CControls* pControls = (CControls*)((char*)GetModuleHandleA("teeworlds.exe") + 0x2722C0);
VMTHook* controlsHook;
bool aimbot()
{
if (pGameClient->m_Snap.m_pGameInfoObj)
{
vec2 enemyPos(0, 0);
if (GetFirstVisibleEnemy(enemyPos))
{
auto self = GetLocalPlayer();
float x = enemyPos.x - self->m_Cur.m_X;
float y = enemyPos.y - self->m_Cur.m_Y;
controlsHook->GetOriginalFunction<OnMouseMove>(10)(pControls, x, y);
return true;
}
}
return false;
}
void hkMouseMove(void* thisptr, float x, float y)
{
if (!aimbot())
controlsHook->GetOriginalFunction<OnMouseMove>(10)(thisptr, x, y);
}
void hkControlsRender(void* thisptr)
{
aimbot();
controlsHook->GetOriginalFunction<OnRender>(6)(thisptr);
}
void Hook()
{
controlsHook = new VMTHook(pControls);
controlsHook->HookFunction((void*)hkControlsRender, 6);
controlsHook->HookFunction((void*)hkMouseMove, 10);
}
After some assembly of all the above code, this is the working hack:
5. Conclusion
Hacking this game was fun as the symbols that were not stripped made identifying code of interest and finding methods very easy. With the source-code at my disposal this was probably the shortest project I worked on and it was a lot of fun finding stuff and trying it out. I don’t think that there is very much one could hack in this game that would actually result in practical benefits: even an aimbot, usually a very powerful feature in hacks, is of little use in this game as players move quickly while projectiles are rather slow.
I enjoyed working on this a lot and I recommend both tinkering with the game and of course playing it!
Hands On: Dynamic and Static Reverse Engineering
In this article I will show some techniques used in dynamic and static analysis and demonstrate them using a simple console game.
Structural approach of bypassing a subscription system
This guide is not for pirating a game or motivating to do something illegal. It’s purely educational and a representation of how I did a structured analysis of a program based on a real-life example. UPDATE #2 Unfortunately, they fixed nothing and just added obfuscation “to fix the issue”… It took me like 30 minutes to…
Disclosing a Severe Vulnerability in a Unity Game
Disclaimer: this article is about a vulnerability I found in a Unity game. I disclosed this to the developers of the game which approved of this post but asked me to censor it so there would not be instructions to pirating their game associated with its name publicly. Some time ago after a long day…
Awesome stuff, been enjoying reading this a lot! I’ve been playing teeworlds for a while and i’ve been developing cheats as well, tho ive not been using assembler but just the source code itself (i started learning programming with teeworlds a few years ago).
So its really interesting to see your approach on all of this, thanks for sharing! 😀