Ban detection in Apex Legends

During my weekly browsing session on a popular game hacking forum, i came across a rather interesting inquiry. An individual wanted to find a way for detecting when his cheat users got banned mid game. Surprisingly he was being honest and mentioned his use case is for a paid cheat service.

But why would p2c developers be interested in attempting to do such thing? My assumption is they want to gather statistics on how many of their users get their game accounts banned. If you think about it, such metric could actually be very valuable to cheat developers and sellers. Some benefits of this include:

  • being able to sense when a potential detection has happened.
  • being able to discover deviations from expected ban percentage.
  • being able to visualize all of aforementioned data for analytical work.

Now after justifying my time wasted wisely spent on this little project, let’s get to work!

Step 1. Analysis of game routines

In order to get started, we need to do some logical thinking first. What happens when you get banned mid game in Apex Legends? You will get dropped back into the main menu, and receive an error prompt which may look similar to this screenshot:

alt text

From my previous work on this game, i know that this message in above picture is localized using #ANTICHEAT_BANNED text sequence. Your game client will translate it to your ingame language, using Apex Legends’ localization system. You can confirm this by looking for banned screenshots online, and looking at chinese/russian results for example. You will see the message they are getting is different from yours.

If you search inside R5Apex for "Disconnect: %s.\n" text sequence, you will find two routines referencing it. We only care about one of them, which looks like following pseudo code:

// #STR: "Server shutting down", "-quitonservershutdown", "#ANTICHEAT_AUTH_FAILED", "Disconnect: %s.\n"
void CClientStateMain::OnDisconnect(void* inst, const char *pszReason)
{
  if ( *(int *)(inst + 0xAC) > 0 )
  {
    pszFormatter = "%s";

    if ( *pszReason != '#' )
      pszFormatter = "Disconnect: %s.\n";

    R5::PushDisconnectError(1u, pszFormatter, pszReason);

    // ...
}

Take a brief look at this routine. It’s being called on every disconnect. Be it for local reasons like connection timed out, or server enforced reasons like out of sync error, idle kick, banned, etc. It doesn’t matter here, you disconnect and execution will land here at some point. You may have guessed it already, but our function of interest is named PushDisconnectError. It’s a wrapper which calls into another function of interest, which exclusively seems to handle disconnects. See below for pseudo code:

// #STR: "disconnect", "disconnect.%s:1|c\n", "#DISCONNECT_IDLE", "#DISCONNECT_TIMEDOUT_SUSPEND", "DisconnectError", "Disconnect:"
__int64 OnDisconnect(__int64 a1, __int64 a2, va_list a3, ...)()
{
  va_start(va, a3);
  v3 = (int)a3;
  v5 = sub_1401E1420();
  v6 = someStdFormatFunc(*v5 | 1u, (unsigned int)buffer, 1024, v3, 0i64, (__int64)va);
  if ( v6 < 0 )
    v6 = -1;
  v7 = v16;
  if ( (unsigned int)v6 > 0x3FF )
    v7 = 0;
  v16 = v7;
  memcpy(&g_pszLastErrorDisconnectReason, buffer, 256i64);

  // ...
}

Awesome! We are lucky in a sense that it copies over the last disconnect reason into a global C-string array with a length of 256 bytes. We are pretty much done here, as all we need to do now is to log this buffer and inspect it’s contents during a ban.

I haven’t had time to actually grab a banned account, but there are two possibilities here. It might contain an already localized String, which in case of a ban, would be "The client's game acount has been banned". Your second option would be that it contains the corresponding localization string BEFORE localization has taken place, in that case this buffer would contain "#ANTICHEAT_BANNED" instead.

Your safest bet would probably be simply doing an case-insensitive strstr on this buffer, and check for "banned"substring sequence. If you want to do it properly, i’d check for your disconnect reason before localization. During my own testing, this buffer always contained a disconnect reason which was not localized yet.

Although i am pretty sure it’s always a pre-localized reason, you can take it as an exercise to double verify, if you wish.

Step 2. Implementing our knowledge into practice

Let’s write a simple routine which will parse your game client’s last disconnect reason, and check whether it’s from a ban or not. Do keep in mind that for externals, you will have to loop this until you get a valid disconnect reason. I’d recommend pairing this with a signonstate check, it will be 0 when you are in main menu, where you get the ban prompt. This way you will not waste any processor cycles when it’s not necessary:

bool IsBanned( HANDLE hApex )
{
    char reason[256];
    memset(reason, 0, sizeof(reason));
    if (mem.read(hApex, reason, 0x1423FE7A0, sizeof(reason)))
    {
        return !strcmp(reason, "#ANTICHEAT_BANNED");
    }

    return false;
}

Again, make sure to not spam this, and check for your client’s signonstate value.

If you wish to check out the corresponding thread on said game hacking forum, feel free to do so. I am Borken04 on there, in case you are wondering.

Thanks for reading!