In this post, I'll be touching on some of the concepts of internal hacking, specifically on Linux and how we can modify game behavior by reading, writing memory and intercepting function calls. This is mainly for those who already have some basic experience, likely with game hacking on Windows, and want to build existing or brand new projects on Linux.

Internal will refer to code that operates within the address space of the target process. As opposed to external, which is most commonly a standalone executable that interacts with the game by using remote memory reading and writing APIs, such as ReadProcessMemory on Windows or process_vm_readv on Linux.

What's the plan?

In August 2013, Valve introduced the 'Arms Deal' update for Counter-Strike: Global Offensive. Included in this update were over a hundred weapon skins that could be purchased with real money or obtained through random drops given to players after a match ended. Nowadays, some of the most sought after weapon skins sell for thousands.

But what if you don't want to pay that much to have a fancy gun, or if you want to mix and match skins on different guns? Just mod the game files, right? Well, it might work for some third-party servers but you won't be allowed to participate in any kind of official matchmaking if your checksum doesn't match the server.

One solution, that isn't too complicated, is to create and load a shared library that changes memory inside the game, making our game client think we have items that we don't really own. It's a client-side modification so only you'll see the changes but it'll work on any server. It's a little sketchy though, manipulating the game like this runs the risk of a Valve Anti-Cheat ban. That's one of the reasons we're doing it on Linux though, where the anti-cheat is much weaker.

How's it going to work?

In the Source Engine, networked entities (players, weapons, physics props, ...) contain variables, such as health and position that are replicated from the server to clients. All of these entities can be accessed programatically from the entity list in the form of a class, usually deriving from C_BaseEntity.

Our shared library will be loosely based off my Chameleon project. The concepts are the same, but to keep thing simple we won't include the view model hook or the kill icon event listener hook. For now we'll only be implementing the skin changer. When the library is loaded into the game it will automatically call the function with the constructor attribute which will retrieve a few game interfaces and hook a certain function that is called on every frame.

Every time our hooked function is called, it will retrieve our local player from the entity list, read and iterate over an array of our weapons, and finally, overwrite economy related variables. By doing this at the right time, our replacement variables will be used instead of the ones retrieved from the server.

Okay, let's get started

Just like on Windows, we need to get some important game classes first. Most of the really useful classes can be easily obtained by using CreateInterface, a function that seems to be exported in all game libraries. First, we need to let the compiler know how to call this function. Nothing new just yet.

typedef void* (*CreateInterfaceFn) (const char*, int*);

Here's where things are a little different. Instead of using GetModuleHandle and GetProcAddress we'll be using the dynamic linking loader functions: dlopen and dlsym. Both functions are exposed in the dlfcn.h header so we'll need to include that now. We can use dlopen to retrieve a handle to the desired shared library, then use dlsym for the symbol address of CreateInterface.

void* library = dlopen("./csgo/bin/linux64/client_client.so", RTLD_NOLOAD | RTLD_NOW);
void* symbol = dlsym(library, "CreateInterface");
CreateInterfaceFn client_factory = reinterpret_cast<CreateInterfaceFn>(symbol);

We'll use this in our initialisation function. Feel free to also define a destructor, as I'll briefly cover unhooking later.

int __attribute__((constructor)) startup() {
   return 0;
};

void __attribute__((destructor)) shutdown() {
}; // optional

Alright, now we can call client_factory and get some interfaces from the client_client.so library. The arguments for CreateInterface are an interface version string and a return code pointer. We don't need to use the return code pointer but the interface versions are important. If we search through the Source SDK 2013 code for long enough we'll find a few lines like these.

EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CHLClient, IBaseClientDLL, CLIENT_DLL_INTERFACE_VERSION, gHLClient );

According to this macro, the above line tells us that calling CreateInterface with the string held in CLIENT_DLL_INTERFACE_VERSION would get us the CHLClient class. So we can just call CreateInterface with VClient017 right?

No, wait! We can't be sure that this interface version hasn't changed. The Source SDK 2013 repository is a great reference but we can't blindly copy from it and expect everything to work. Instead, use strings on the file in question to get interface versions. You only need to know the initial characters - in this case 'VClient'.

$ strings client_client.so | grep "VClient"
VClient018

It's always worth double checking these. Now we can get the CHLClient class. We don't actually call anything from this so you can either create an empty class or store it as a generic pointer. Doesn't really matter.

clientdll = reinterpret_cast<CHLClient*>(client_factory("VClient018", nullptr));

Perfect, we've got the class pointer, but what do we do with it? If we take a look at the Valve's Developer Community page on the Frame Order of the Source Engine we can see a certain CHLClient::FrameStageNotify function in there a few times. Seems fairly important, but hmm.. where have I heard that before? The class we just got from CreateInterface wouldn't happen to have that function, would it?

Well.. actually it's not that easy. The above screenshot is from the Mac OS X libraries which, until around April 2015, included all symbols which made reversing a lot easier. Unfortunately, they're long gone now but if you manage to find a copy of them they're still useful as a general guide. If we take a look at the disassembly of CHLClient::FrameStageNotify we can see quite a few string references used by the game's performance profiling functions. We can use these strings as a convenient reference to find the same function in the current client_client.so Linux library.

FrameStageNotify from an old Mac binary with symbols.
FrameStageNotify from a more current Linux binary - note the lack of function and variable names.
The FrameStageNotify function on both Mac OS X and Linux.

If we scroll to the top of this function and follow the cross-reference we'll eventually find ourselves at the virtual method table.

It's a lot easier to find functions with string references.
If you count the functions from the top of the table, FrameStageNotify is the 36th function.

We need two more interfaces before we can do anything. Specifically, the entity list and engine client. IClientEntityList allows us to find a pointer to any entity from its index, while IVEngineClient has a function that returns the index of our player entity.

You'll find that IClientEntityList is also in client_client.so while IVEngineClient can be found in ./bin/linux64/engine_client.so. Versions begin with 'VClientEntityList' and 'VEngineClient'.

Since most of the other functions in these classes are irrelevant to us we can either pad the class with dummy functions or call the functions directly from their index. These helper functions will allow you to do the latter.

inline void**& GetVirtualTable(void* baseclass) {
   return *reinterpret_cast<void***>(baseclass);
}

inline void* GetVirtualFunction(void* vftable, size_t index) {
   return reinterpret_cast<void*>(GetVirtualTable(vftable)[index]);
}

template <typename Fn> inline Fn GetVirtualFunction(void* vftable, size_t index) {
   return reinterpret_cast<Fn>(GetVirtualTable(vftable)[index]);
}

Nothing too complicated to explain here. Once you've called CreateInterface and casted to one of these classes you can call the function like normal. You can find the indexes using IDA and the method above or, god forbid, trial and error. I've included them for these classes to save you some time.

class IClientEntityList {
  public:
    void* GetClientEntity(int index) {
      return GetVirtualFunction<void*(*)(void*, int)>(this, 3)(this, index);
    }
};

class IVEngineClient {
  public:
    int GetLocalPlayer() {
      return GetVirtualFunction<int(*)(void*)>(this, 12)(this);
    }
};

About those entity variables..

Remember those member variables in networked entities that I briefly mentioned? Thanks to the way the data tables work in the Source Engine, as long as you know the variable name you can easily find the offset by checking the disassembly. Let's go over the variables we'll need and why we need them.

m_lifeState from the DT_BasePlayer table. Reading this from our player will determine whether we are alive or not. Naturally, there's no need to call our skin changer when we're dead. We'll implement this in the C_BasePlayer class.

Server-side implementations of an entity should be called CMyEntity, while their client-side equivalents should be C_MyEntity. Theoretically they could be called anything, but some of Valve's code assumes you have followed this convention.
— Networking Entities - Valve Developer Community
lea     rsi, aM_lifestate ; "m_lifeState"
mov     edx, 293h

m_hMyWeapons from the DT_BasePlayer table. An array of 64 handles to weapons we're holding. Pretty important.

mov     edx, 3528h
lea     rsi, aM_hmyweapons ; "m_hMyWeapons"

m_AttributeManager and m_Item from the DT_BaseAttributableItem table. These are specific to certain Source games so you won't find anything about these in the Source SDK 2013 repository. This will be implemented in the C_BaseAttributableItem class.

lea     rsi, aM_attributeman ; "m_AttributeManager"
mov     edx, 34C0h
mov     edx, 60h
mov     r8, cs:off_192C9B8
lea     rsi, aM_item    ; "m_Item"

m_iItemDefinitionIndex from m_Item in the DT_BaseAttributableItem table. Contains a unique identifier for pretty much every item and cosmetic in the game, including weapons, music kits and coins. Can't just use 0x268 here without adding the parent class offsets so add m_AttributeManager and m_Item together and you'll get 0x3788.

lea     rsi, aM_iitemdefinit ; "m_iItemDefinitionIndex"
mov     edx, 268h

m_iEntityQuality from m_Item in the DT_BaseAttributableItem table. Determines the rarity of the entity, such as Souvenir, StatTrak, etc. Mostly copied from Team Fortress 2 so there's a few that aren't normally obtainable such as 'Valve' quality.

lea     rsi, aM_ientityquali ; "m_iEntityQuality"
mov     edx, 26Ch
I haven't included any qualities that didn't change the text.
Changing this only seems to affect the default display name.

m_iItemIDHigh from m_Item in the DT_BaseAttributableItem table. If you don't have a real weapon skin on your account equipped you'll need to set this to -1, otherwise the fallback values won't be used.

lea     rsi, aM_iitemidhigh ; "m_iItemIDHigh"
mov     edx, 280h

m_szCustomName from m_Item in the DT_BaseAttributableItem table. As you might expect, this is the custom name of the weapon. Shows up on a name tag and seems to be 32 bytes in total.

lea     rsi, aM_szcustomname ; "m_szCustomName"
mov     edx, 340h

m_nFallbackPaintKit from the DT_BaseAttributableItem table. This is where the magic really happens. Determines what skin is applied to the weapon, you can forage through items_game.txt for all the paint kit numbers. For example, Dragon Lore is 344, Fade is 38 and Crimson Web is 12.

lea     rsi, aM_nfallbackpai ; "m_nFallbackPaintKit"
mov     edx, 39B0h
Pro tip: Run 'cl_fullupdate' after changing paint kits to see the changes immediately.
You're free to use any paint kit on any weapon. Get creative!

m_nFallbackSeed from the DT_BaseAttributableItem table. The random seed that makes certain patterned paint kits (such as Fade and Crimson Web) look unique.

lea     rsi, aM_nfallbacksee ; "m_nFallbackSeed"
mov     edx, 39B4h

m_flFallbackWear from the DT_BaseAttributableItem table. Float value that determines how worn the weapon skin looks. 0 would be closest to Factory New while 1 would be Battle Scarred. Feel free to go beyond this for some rather interesting results.

lea     rsi, aM_flfallbackwe ; "m_flFallbackWear"
mov     edx, 39B8h
Wear value in steps of .1 from 0.1 to 2.0.

m_nFallbackStatTrak from the DT_BaseAttributableItem table. This is the value for the StatTrak kill counter. Unfortunately it won't increment automatically but you can set it to 1337 if you want to look cool. Requires some extra trickery to get this displaying correctly without the 'user unknown' error but I'll leave that to you - here's a small hint.

lea     rsi, aM_nfallbacksta ; "m_nFallbackStatTrak"
mov     edx, 39BCh

Just to make things easier for you, here are what the preprocessor defines would look like. Of course, this is only accurate for the game version I was using when I wrote this. You'll want to look over client_client.so yourself to make sure everything is correct. There are also ways to automate this with game functions but we can talk about that some other time.

#define m_lifeState 0x293
#define m_hMyWeapons 0x3528
#define m_AttributeManager 0x34C0
#define m_Item 0x60
#define m_iItemDefinitionIndex 0x268
#define m_iEntityQuality 0x26C
#define m_iItemIDHigh 0x280
#define m_szCustomName 0x340
#define m_nFallbackPaintKit 0x39B0
#define m_nFallbackSeed 0x39B4
#define m_flFallbackWear 0x39B8
#define m_nFallbackStatTrak 0x39BC

Looks like that's all of them. That's great and all, but how do we use these? Glad you asked, because it's as simple as adding the offset to the memory address of the entity. Here are some basic classes using the above offsets. They'd usually inherit from C_BaseEntity which inherits from IClientEntity which inherits from.. nevermind.

class C_BasePlayer {
 public:
  unsigned char GetLifeState() {
   return *(unsigned char*)((uintptr_t)this + m_lifeState);
  }

  int* GetWeapons() {
   return (int*)((uintptr_t)this + m_hMyWeapons);
  }
};

class C_BaseAttributableItem {
 public:
  int* GetItemDefinitionIndex() {
   return (int*)((uintptr_t)this + m_AttributeManager + m_Item + m_iItemDefinitionIndex);
  }

  int* GetItemIDHigh() {
   return (int*)((uintptr_t)this + m_AttributeManager + m_Item + m_iItemIDHigh);
  }

  int* GetEntityQuality() {
   return (int*)((uintptr_t)this + m_AttributeManager + m_Item + m_iEntityQuality);
  }

  char* GetCustomName() {
   return (char*)((uintptr_t)this + m_AttributeManager + m_Item + m_szCustomName);
  }

  int* GetFallbackPaintKit() {
   return (int*)((uintptr_t)this + m_nFallbackPaintKit);
  }

  int* GetFallbackSeed() {
   return (int*)((uintptr_t)this + m_nFallbackSeed);
  }

  float* GetFallbackWear() {
   return (float*)((uintptr_t)this + m_flFallbackWear);
  }

  int* GetFallbackStatTrak() {
   return (int*)((uintptr_t)this + m_nFallbackStatTrak);
  }
};

The really important part

As you might already know, some compilers, Microsoft Visual C++ and the GNU C++ Compiler included, add a hidden member variable leading to an array. Inside this array are pointers to each virtual function defined in the class. So, if you were to access the 36th pointer in the CHLClient virtual method table, yep, you guessed it, that would be the FrameStageNotify function.

Let me guess what you're thinking. What if we replaced that pointer with one to our own function? Well, that's exactly what we're going to do. First things first, let's get the virtual method table of our class.

uintptr_t** client_vmt = reinterpret_cast<uintptr_t**>(clientdll);

Now, before we go recklessly making changes we should create a backup of the original table.

uintptr_t* original_client_vmt = *client_vmt;

Good, good. Actually, we have a problem now, we don't know the size of this table meaning we can't replace it with our own one. It's okay though, we can loop through the existing table until we hit the end and use that to calculate the size later.

size_t total_functions = 0;

while ((uintptr_t*)(*client_vmt)[total_functions]) {
   total_functions++;
}

We can create our replacement table now.

uintptr_t* new_client_vmt = new uintptr_t[total_functions];

It's empty at the moment so let's copy the original contents into it.

memcpy(new_client_vmt, original_client_vmt, (sizeof(uintptr_t) * total_functions));

Now we can replace function pointers and when the game calls a function at the index we replaced, our function will be called instead. This is great, but we could potentially crash the game or just cause undesired results by not calling the original function. Not a problem though, after all, we've got the copy of the original virtual method table.

Okay, let's create our replacement function. First, we will need the prototype of the function we're hooking. You can find the values for ClientFrameStage_t in the SDK over here.

typedef void (*FrameStageNotifyFn) (void*, ClientFrameStage_t);

Start off by storing the original FrameStageNotify so we can call it later inside our hook function. We already know it's at the 36th position in the array, so we can only need to cast it to the prototype we just defined.

oFrameStageNotify = reinterpret_cast<FrameStageNotifyFn>(original_client_vmt[36]);

Now we can create our hook function. Right now it won't do anything apart from call the original.

void hkFrameStageNotify(void* thisptr, ClientFrameStage_t stage) {
   return oFrameStageNotify(thisptr, stage);
}

And finally, we overwrite the function pointer and commit the new virtual method table. Don't forget to store the original in the oFrameStageNotify variable otherwise we'll crash immediately, very anti-climactic.

oFrameStageNotify = reinterpret_cast<FrameStageNotifyFn>(original_client_vmt[36]);
new_client_vmt[36] = reinterpret_cast<uintptr_t>(hkFrameStageNotify);
*client_vmt = new_client_vmt;

Now you can restore the table to normal in the destructor function. That is, if you decided to make one.

void __attribute__((destructor)) shutdown() {
   *client_vmt = original_client_vmt;
};

Making our changes

Back to our hook function. We want to perform our changes before calling the original and only during the FRAME_NET_UPDATE_POSTDATAUPDATE_START stage. Refer to the frame order page again if you're wondering why.

while (stage == ClientFrameStage_t::FRAME_NET_UPDATE_POSTDATAUPDATE_START) {

First off, we'll need our local player entity so we can read our weapons.

C_BasePlayer* localplayer = reinterpret_cast<C_BasePlayer*>(entitylist->GetClientEntity(engine->GetLocalPlayer()));

No point in continuing any further if we're not alive or if our pointer was invalid.

if (!localplayer || localplayer->GetLifeState() != LIFE_ALIVE) {
   break;
}

Next, we can finally loop through our weapons.

int* weapons = localplayer->GetWeapons();

if (!weapons) {
   break;
}

I don't even know if you can carry 64 weapons but hey, that's how big the array is.

for (int i = 0; i < 64; i++) {

Now we can get the entity. I haven't had any luck getting IClientEntityList::GetClientEntityFromHandle to work on Linux so I'm using this bitwise workaround thing instead since it seems to work just as well.

if (weapons[i] == -1) {
  continue;
}

C_BaseAttributableItem* weapon = (C_BaseAttributableItem*)entitylist->GetClientEntity(weapons[i] & 0xFFF);

if (!weapon) {
  continue;
}

Time to make our changes. Here's an enum for m_iItemDefinitionIndex just in case you wanted to know what kind of weapon you're writing to.

switch (*weapon->GetItemDefinitionIndex()) {
 // AWP | Dragon Lore
 case WEAPON_AWP:
  *weapon->GetFallbackPaintKit() = 344;
  break;
 
 // Desert Eagle | Conspiracy
 case WEAPON_DEAGLE:
  *weapon->GetFallbackPaintKit() = 351;
  break;
}

*weapon->GetFallbackWear() = 0.f;
*weapon->GetItemIDHigh() = -1;

Name tags can be added like so. Strangely enough, you can even add this to the C4.

snprintf(weapon->GetCustomName(), 32, "%s", "aixxe.net");

Finish off the function by breaking out of the while loop. We only use a loop here for convenience as it allows us to break when something unexpected happens, just a bit shorter than calling the original and returning every time.

Ready to go!

Now that everything is ready to go we're ready to compile our library. At the moment the skins that are applied can't be changed without re-compiling with different settings. I'll leave the dynamic configuration up to you since there are so many ways to achieve it and it comes down to preference in the end.

$ g++ -std=c++0x -Wall -c -fno-use-cxa-atexit -fPIC -c -o test.o test.cc
$ g++ test.o -o test.so -nostartfiles -nostdlib -m64 -shared

Now we can load it into the game. We'll be using a simplified version of the instructions from my shared library loader post.

$ sudo gdb -ex "attach $(pidof csgo_linux64)"
(gdb) call (dlopen)("/home/aixxe/devel/example/test.so", 1)
(gdb) detach

That's everything! I'm sure that I'll end up writing a follow up post expanding the work here into a more featureful project with a knife changer, dynamic offsets and so on. A copy of the finished skin changer code based on everything in this post can be found here along with a minimal Makefile.

Last updated Thursday, 24 February 2022 at 10:33 AM.