Clan tags in Counter-Strike: Global Offensive are tied to groups you've joined in the Steam Community. These groups can either be public or require an invitation to join, on top of this, the tags are unique to each group so if you're not a member you won't be able to use that tag, right? Well.. not exactly.

This is going to be an easy one if you've already read my tutorials on basic internal hacking and the later one on CreateMove hooking. As always, full source code of the final product will be available at the bottom of the post. Let's begin!

The SendClanTag function and you

Ever been in matchmaking and saw someone constantly changing their clan tag? The typical way of doing this is with the cl_clanid console variable. Passing a community group ID will change the clan tag in-game in the same way the game options menu does. By binding this to, for example, movement keys, the clan tag will be changed as you walk around.

However, this only works with manual input and you still won't be able to use a tag from an invite only group. Well, you would think that, right? Nope, turns out there aren't any checks if you delve a bit deeper, find the underlying function and call it yourself. In fact, you can do some weird stuff with this but more on that later.

Since we're not really sure where to start, let's open up engine_client.so in IDA and look for the cl_clanid string for now. It's likely that there is some kind of callback function that is invoked every time the variable is changed.

cl_clanid constructor disassembly
The old Mac binaries with symbols come in handy once again!

After following the reference, we've arrived here. It looks like this is where some console variables (or ConVars) are created. We can see the description for cl_clanid as it appears in the game and the default value of 0.

] help cl_clanid
"cl_clanid" = "0" archive user - Current clan ID for name decoration

After decompiling, we can clearly see the name, default value and help description being passed to a ConVar constructor.

ConVar::ConVar(&cl_clanid, "cl_clanid", "0", 656, "Current clan ID for name decoration", CL_ClanIdChanged);

Let's take a look at the Source SDK 2013 code now. A quick search brings us to the convar.cpp file. Here we can see a variety of constructor functions, most importantly, one that matches the prototype of the above call. If we ignore the pointer to cl_clanid then we can see the arguments are: name, default value, flags, help string and a FnChangeCallback_t callback function.

After following the function reference to CL_ClanIdChanged we've found where, presumably, the validation process takes place. There's a lot of stuff here involving some interfacing with Steam but something more interesting sticks out.

__text:000DD686 E8 15 00 00 00    call    __ZL11SendClanTagPKcS0_ ; SendClanTag(char const*,char const*)

That sounds promising, doesn't it? Let's decompile it and take a look.

int __fastcall SendClanTag(const char *a1, char *a2) {
  char *v2; // ST1C_4@1
  const char *v3; // ebx@1
  KeyValues *v4; // esi@1
  unsigned __int32 v6; // [sp+4h] [bp-24h]@0

  v2 = a2;
  v3 = a1;
  v4 = (KeyValues *)KeyValues::operator new((KeyValues *)&unk_24, v6);
  KeyValues::KeyValues(v4, "ClanTagChanged");
  KeyValues::SetString(v4, "tag", v3);
  KeyValues::SetString(v4, "name", v2);
  return (*(int (__cdecl **)(_DWORD *, KeyValues *))(*engineClient + 752))(engineClient, v4);
}

We can see this creates a KeyValues message called ClanTagChanged, sets the tag key to the first argument and the name key to the second argument. This is then sent off to a function in the engineClient class.

If we look for functions in IVEngineClient that take a KeyValues* argument we find this:

// Sends a key values server command, not allowed from scripts execution
// Params:
//  pKeyValues  - key values to be serialized and sent to server
//          the pointer is deleted inside the function: pKeyValues->deleteThis()
virtual void ServerCmdKeyValues( KeyValues *pKeyValues ) = 0;

Looks pretty good to me. Let's switch to the Linux binaries and search for ClanTagChanged, the message string that we found in the SendClanTag function we just decompiled.

.rodata:B14187 43 6C 61 6E 54 61 67 43+aClantagchanged db 'ClanTagChanged',0; DATA XREF: sub_3AFB40+24

Now that we've found the function at engine_client.so+3AFB40 we're almost ready to test it. We'll need the base address of the engine_client.so library, the prototype of SendClanTag and somewhere to invoke it, I'll be using CreateMove.

typedef void (*SendClanTagFn) (const char*, const char*);
bool hkCreateMove(void* thisptr, float flInputSampleTime, CUserCmd* cmd) {
  static auto SendClanTag = reinterpret_cast<SendClanTagFn>(engine_client + 0x3AFB40);

  if (cmd && cmd->command_number) {
    SendClanTag("[me@aixxe.net]", "aixxe");
  }

  return clientmode_hook->GetOriginalFunction<CreateMoveFn>(25)(thisptr, flInputSampleTime, cmd);
}

Since this has previous been covered, I'm assuming that you've already found the base address, hooked CreateMove and loaded your library into the game. If you've done all that, hop into a server and take a look at the scoreboard.

Breakin' the rules.

Great success! Now you can make a pattern of the function and scan for it at runtime instead of hardcoding the address. I've included my one below which has been tested on the current and previous five versions of engine_client.so.

55 48 89 E5 48 89 5D E8 4C 89 65 F0 49 89 FC BF 48 00 00 00 4C 89 6D F8 48 83 EC 20 49
— Regular or 'IDA-style' signature.
\x55\x48\x89\xE5\x48\x89\x5D\xE8\x4C\x89\x65\xF0\x49\x89\xFC\xBF\x48\x00\x00\x00\x4C\x89\x6D\xF8\x48\x83\xEC\x20\x49
— Code-style signature.
xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
— Code-style mask.

Experimenting with this function can produce some interesting results. For example, use a line break character and you'll suddenly disappear from the scoreboard and kill feed. Your name won't show up when an enemy targets you either.

Next up, trick your friends into thinking you work at Valve! Since we're bypassing the check we can use the tag of any group without being a member. Just make sure you encode the string correctly for fancy characters.

SendClanTag("[VALV\xE1\xB4\xB1]", "");
Nobody will ever know.

The rest is up to you and whatever you can fit in those 15 characters.

The final example code with everything ready to use can be found on over here.

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