Important Security Notice - Remote Execution Vulnerabilities Found in IW4x

During a security assessment, a community member made an important discovery that affected the security of the IW4x client. This blog post aims to inform all IW4x users about the identified remote execution vulnerabilities, their potential impact, and the necessary steps to stay safe while using the client.

THE VULNERABLE VERSIONS
The affected versions of IW4x are between 0.5.0 and 0.7.7 (inclusive). Any version released within this range is confirmed to be vulnerable. We conducted a preliminary analysis, and it was determined that both vulnerabilities could be exploited with a single click, meaning an unintentional action by the user, such as joining a malicious server or downloading a harmful mod, could trigger the remote execution.

VERIFY THE IW4x CLIENT VERSION

  • Open the command prompt or terminal
  • Locate the IW4x installation folder containing iw4x.exe
  • Execute the following command: iw4x.exe -version, on Linux you might want to execute it using WINE (wine iw4x.exe -version)

Possible output (latest version provided by AlterWare)

IW4y r4364 (built Jul 31 2023 11:27:06)
Revision: 4364 - develop

If you find that you are running a vulnerable version, update immediately using the provided trusted sources.

Question: Why is there a y instead of an x?
Answer: It’s unrelated to this matter, a new letter was chosen to replace the x after the XLabs shutdown. The y is of a sarcastic nature and does not affect the client in any way beyond appearances.

TIPS TO STAY SAFE (ON A VULNERABLE VERSION)

  • Do not install any mod or GSC script from an untrusted source. Untrusted sources include YouTube videos. The most popular attack vector could be an infected trickshot GSC mod menu as those are the most popular. The target audience is not able to audit the code themselves, leaving them exposed to any exploit. You should only download mods or GSC scripts when you have access to the source code, which can be rendered available on a website like GitHub.
  • Do not join any “new server” you have never played on before.
  • Possibly, do not join any server at all from the server list as servers can, have been, and will be spoofed. The player count can be faked to be higher than it actually is in order to attract more players and deliver malicious payloads. Go to your trusted or favourite clan’s website and manually connect to their server using the connect command. You can do that by opening the game console by pressing ~ and typing: connect IP:port, for example: connect 127.0.0.1:28960. Replace the IP with the IP of the server you are trying to join and add the correct port. If you need help with the step above, ask the moderators or staff members of your favourite clan as they can assist you on how to join their server whilst using this particular method.

You should also follow the advice above after you have updated or discovered your IW4x version is not vulnerable.

UPDATE YOUR IW4x INSTALL
Only trust the following separate sources:

CONCLUSIONS
This brings the total of discovered RCEs found on IW4x to three
The publicly disclosed RCE is the openLink RCE which is not related to this report.
The openLink was promptly removed and it’s documented in the official IW4x changelog.
The two RCEs concerning this post are not documented, they have been inadvertently patched by myself at one point in late December 2022 after I fixed a patch that looked ‘odd’ and was not doing what it was supposed to do in an efficient manner.
The two new RCEs were disclosed privately in a secure manner and will stay private until enough time has passed to allow everyone to update.

During the last two years of IW4x, before the XLabs shut down, many other potentially exploitable functions/patches have been fixed or removed entirely. For example, the HttpGet & HttpCancel were removed due to being potentially exploitable by allowing an attacker to use them as part of an attack chain that could lead to malware being downloaded from a remote server. They are not in itself exploitable but those functions can be used in harmful ways.

All security concerns that were raised up until today are addressed in the latest IW4x release.
Please update IW4x and follow the tips to stay safe.

2 Likes

Since some time has passed since this blog post was written, I think it’s safe to disclose what was really going on with IW4x. Additionally, I completely forgot about one of the two RCEs I mentioned in the previous post. Therefore, I’d better write about the one I still remember before it slips my mind as well.

In a previous IW4x patch, a jump hook was placed at a function called va.

va is a function used since Quake III to format a string quickly. It avoids initializing a local buffer and using sprintf with its return value. It’s essentially a utility function similar to the modern std::format

const char* s = va("%s", input);

Internally, va has two thread-local buffers of 1024 characters. It uses the buffers sequentially and overwrites what was previously written to the buffers throughout the life cycle of the calling thread.

enum
{
    THREAD_VALUE_PROF_STACK = 0x0,
    THREAD_VALUE_VA = 0x1,
    THREAD_VALUE_COM_ERROR = 0x2,
    THREAD_VALUE_TRACE = 0x3,
    THREAD_VALUE_CMD = 0x4,
    THREAD_VALUE_COUNT,
};

struct va_info_t
{
  char va_string[2][1024];
  int index;
};

char* va(const char* format, ...)
{
  va_list ap;
  va_start(ap, format);

  va_info_t* info = (va_info_t*)Sys_GetValue(THREAD_VALUE_VA);
  int index = info->index;
  info->index = (index + 1) % 2;
  char* buf = info->va_string[index];
  int len = vsnprintf(buf, 1024, format, ap);
  va_end(ap);

  buf[1023] = '\0';
  if (len < 0 || len >= 1024)
  {
    buf[1023] = '\0';
    Com_Error(ERR_DROP, "\x15Attempted to overrun string in call to va()");
  }

  return buf;
}

As we can observe, the function will terminate the game if a buffer overflow is detected.

Throughout IW4x’s life cycle, the developers made a series of patches to the game. When they added the IW4x embedded web server to allow for easier mod downloading, there was an issue. As IW4x was building a response to send to the users wanting to download mods, it was making a call to va from a separate thread (the Mongoose thread) which did not initialize the internal va_info_t structure that is returned by Sys_GetValue resulting in a crash.
They fixed the issue by placing a jump hook at the va function, completely bypassing it and replacing it with IW4x’s own implementation, which uses its own set of thread-local buffers that are automatically initialized (fun fact from momo5502: [MS] Implement on-demand TLS initialization for Microsoft CXX ABI · llvm/llvm-project@072e2a7 · GitHub).

IW4x’s own implementation of va allowed for an unlimited buffer size. This seemingly harmless design flaw meant that not only could the client itself use va without restrictions, but because the developers of IW4x completely replaced va with this flawed version, it also meant that the game could use va with unlimited buffer size.

This led to a catastrophic sequence of events:

First of all, the IW developers assumed that va could never exceed 1024 characters and if it did, the game would instantly terminate because of the Com_Error call. By hooking va, this assumption was no longer true.

A malicious attack was detected in September 2022. A user using a simple fuzzer discovered that they could send users connecting to their malicious server an OOB packet:

error\n<error string>

The string exceeded 1024 characters. The game uses localized strings so that some specific errors can be displayed in the user’s native language.

In the function SEH_LocalizeTextMessage, the game will attempt to localize a string from the malicious OOB packet that is larger than 1024 bytes, and a stack buffer overflow will occur, leading to a remote execution vulnerability

The malicious attacker used this to install a tracking program on the victim’s machine that pinged back the attacker’s server to confirm the infection took place and then self-destructed. No users were actually harmed, but this could have turned out much worse.

Various users were saved by Windows Defender or their antivirus software, and the tracking program did not run on their machines.

The malicious attacker later decided to contact the IW4x development team, and the vulnerability was fixed in September 2022.

The vulnerability was fixed by using the game’s own va implementation in critical functions, and by simply initializing the va_info_t structure from new threads.

#define Com_InitThreadData()                                                             \
{                                                                                        \
	static Game::ProfileStack profile_stack{};                                           \
	static Game::va_info_t va_info{};                                                    \
	static jmp_buf g_com_error{};                                                        \
	static Game::TraceThreadInfo g_trace_thread_info{};                                  \
	static void* g_thread_values[Game::THREAD_VALUE_COUNT]{};                            \
	*(Game::Sys::GetTls<void*>(Game::Sys::TLS_OFFSET::THREAD_VALUES)) = g_thread_values; \
	Game::Sys_SetValue(Game::THREAD_VALUE_PROF_STACK, &profile_stack);                   \
	Game::Sys_SetValue(Game::THREAD_VALUE_VA, &va_info);                                 \
	Game::Sys_SetValue(Game::THREAD_VALUE_COM_ERROR, &g_com_error);                      \
	Game::Sys_SetValue(Game::THREAD_VALUE_TRACE, &g_trace_thread_info);                  \
}

We also enabled DEP by default.

Important notes:

  • By default, iw4x.exe does not have DEP enabled.
  • Apparently, games released before Black Ops 3 do not have ASLR enabled (the official dedicated server release for BO3 still doesn’t have ASLR).
  • IW/3arc/Sledgehammer did not enable stack cookies on ship builds of their games until Infinite Warfare/Black Ops 3.

This means that, at the time of the attack, there was nothing to stop a stack-based buffer overflow from occurring.

PC debug builds of CoD games seem to always have stack cookies enabled.
Ship builds of the Mac release of some old CoD games also have stack cookies.

Even if iw4x.dll is a module that uses stack cookies and has ASLR, the base game does not. Therefore, a vulnerability found inside the game’s own code is much more dangerous.

There is room for speculation as to why, despite IW and 3arc using stack cookies on debug builds of their games, they turned them off on ship builds.

There are anecdotal accounts of people contacting the security team at Activision and reporting various security vulnerabilities (SV_SteamAuthClient RCE) and pleading with them to enable stack cookies. However, they never recompiled old CoD games with them or enabled ASLR.

The theory that I choose to believe is that old versions of Visual Studio 2005, 2008, and 2010, which were used for old CoD games, had issues when compiling with ASLR. I don’t think these issues persisted in later versions of Visual Studio; however, I can imagine if IW/3arc had a bad experience with ASLR in any of these old versions of Visual Studio, they were reluctant to enable it even on newer versions, leaving many CoD titles without it.

Additionally, it’s possible they somehow believed that on old hardware, stack cookies would cause performance issues, which is why they disabled it on ship builds.

However, it’s also possible they viewed stack cookies as something that aids with debugging faulty programs, and therefore a feature that only belongs to debug builds and not ship builds—a very uninformed thought process, but I think this is the most likely scenario.

Here is why I believe that. Let’s look at the T5 Dedicated server debug build:

void SV_SteamAuthClient(netadr_t from, msg_t *msg)
{
  unsigned __int64 clientID;
  unsigned int authBlobLen;
  char authBlob[2048];

  clientID = MSG_ReadInt64(msg);
  authBlobLen = MSG_ReadShort(msg);

  // IW/3arc use a custom assert function
  assert(authBlobLen < sizeof( authBlob ));

  MSG_ReadData(msg, authBlob, authBlobLen);
  LiveSteam_Server_ClientSteamAuthentication(clientID, from, authBlob, authBlobLen);
}

This shows the RCE could have been prevented if the assertion, which is not compiled in ship builds, had an additional in-code check that was later added after the RCE was patched.

assert(authBlobLen < sizeof( authBlob )); // for debugging
if (size >= sizeof( authBlob )) // for ship builds
{
  return;
}

This is a recurring theme for many other vulnerabilities found in various CoD titles. If their developers did not rely so much on assertions, they would have prevented many RCEs.

In Black Ops 3, they finally started to do something about this issue, but the solution was still quite undesirable and honestly extremely baffling:

memcpy(dest, src, len);
if (len >= dest_size)
{
  _report_rangecheckfailure();
  __debugbreak();
}

For some reason, in ship builds of Black Ops 3, they perform a range check after a memcpy is performed and not before. I find that to be unacceptable.

Anyhow, if the length exceeds the size of the destination buffer, the internal MSVC function _report_rangecheckfailure is called. This function does different things depending on whether a debugger is attached or not, and then the intrinsic __debugbreak is called (int 3 in x86 assembly).

Because of Arxan’s DRM, you cannot attach a debugger; therefore, this causes the game to crash. I don’t know if 3arc would have been able to debug crashes caused by their own code on ship builds like this, especially with Arxan in the way, but it seems debugging wasn’t their strongest suit as their developers added 4 RCEs to the game.

2 Likes

Here is a simple re-implementation of va (C++ 14):

#include <iostream>

#include <cstdarg>
#include <cstdio>

template <class Type, std::size_t n>
constexpr std::size_t ARRAY_COUNT(Type(&)[n]) { return n; }

enum
{
    THREAD_CONTEXT_MAIN,
    // .. other game threads,
    THREAD_CONTEXT_COUNT,
    THREAD_CONTEXT_INVALID = -1,
};

enum
{
    THREAD_VALUE_PROF_STACK = 0x0,
    THREAD_VALUE_VA = 0x1,
    THREAD_VALUE_COM_ERROR = 0x2,
    THREAD_VALUE_TRACE = 0x3,
    THREAD_VALUE_CMD = 0x4,
    THREAD_VALUE_COUNT,
};

void* g_threadValues[THREAD_CONTEXT_COUNT][THREAD_VALUE_COUNT];
thread_local void** g_dwTlsIndex;

struct va_info_t
{
    char va_string[2][1024];
    int index;
};

static va_info_t va_info[THREAD_CONTEXT_COUNT];

void Sys_SetValue(int valueIndex, void* data)
{
    void** threadValues = g_dwTlsIndex;
    threadValues[valueIndex] = data;
}

void* Sys_GetValue(int valueIndex)
{
    void** threadValues = g_dwTlsIndex;
    return threadValues[valueIndex];
}

void Com_InitThreadData(int threadContext)
{
    Sys_SetValue(THREAD_VALUE_VA, &va_info[threadContext]);
}

char* va(const char* format, ...)
{
    va_list ap;
    va_start(ap, format);

    va_info_t* info = (va_info_t*)Sys_GetValue(THREAD_VALUE_VA);
    int index = info->index;
    info->index = (index + 1) % ARRAY_COUNT(info->va_string);
    char* buf = info->va_string[index];
    int len = vsnprintf(buf, sizeof(info->va_string[0]), format, ap);
    va_end(ap);

    buf[sizeof(info->va_string[0]) - 1] = '\0';
    if (len < 0 || len >= sizeof(info->va_string[0]))
    {
        buf[sizeof(info->va_string[0]) - 1] = '\0';
        // Com_Error(ERR_DROP, "\x15Attempted to overrun string in call to va()");
        // x15 and x14 are instructions for the function that localizes strings
        std::cerr << "Attempted to overrun string in call to va()";
        // Com_Error would normally cause exit() to be called after a MessabeBox is displayed
    }

    return buf;
}

int main()
{
    // Sys_InitMainThread
    g_dwTlsIndex = g_threadValues[THREAD_CONTEXT_MAIN];
    Com_InitThreadData(THREAD_CONTEXT_MAIN);
    
    std::cout << va("Hello %s", "World") << "\n";

    return 0;
}
1 Like

I have to apologize. In my previous post, I mixed up the va design flaw with the SEH_LocalizeTextMessage vulnerability. Unfortunately, two years have passed, and I clearly lost a few neurons by providing support on the Discord server and mixed things up a bit.

It turns out va, despite being extremely insecure at the time, had almost nothing to do with the RCE vulnerability of September 2022 (maybe it was the other RCE I forgot about).

The issue is that the game can receive UDP packets as large as 0x20000 bytes and does little to no bounds checking in some cases.

If I’m right this time, just to sum things up: SEH_LocalizeTextMessage uses strcpy to copy data from one buffer to another, and if the string is larger than 1024 bytes, a buffer overflow occurs.

1 Like