August 27, 2026
Finding a 20-Year-Old Call of Duty RCE in an Evening With an LLM
A memory-corruption bug in Call of Duty 1’s networking code, shipped in 2003, was still exploitable in 2026 — and the researcher who found…

By coy0te
5 min read
A memory-corruption bug in Call of Duty 1's networking code, shipped in 2003, was still exploitable in 2026 — and the researcher who found it did the initial triage with an LLM in the space of an evening. That framing is worth sitting with, because it's the part everyone latches onto ("AI found an RCE!") and also the part that's most misleading if you don't look at what the model actually did versus what still required a human staring at a disassembler.
I want to reconstruct the workflow here rather than the specific offsets, because the source material — the r/netsec post pointing at the writeup — is a summary, and I'm not going to invent function addresses, struct layouts, or a CVE number that isn't in front of me. What I can do usefully is lay out how LLM-assisted vulnerability research on legacy protocol code tends to go, mark clearly where the source confirms a detail and where it doesn't, and give you a methodology you can actually repeat against your own targets.
What the source actually says
The writeup, posted by u/wez32, is titled "Pwning Call of Duty 1: a 20-year-old RCE, found in an evening with AI." From the post itself we have three concrete claims: the target is Call of Duty 1 (a 2003 title built on a derivative of the Quake 3 / id Tech 3 engine), the vulnerability is a remote code execution bug, and the discovery-to-working-exploit loop happened in a single evening with AI assistance.
Everything past that — the exact parser, the corruption primitive, the exploitation chain — is not in the material I have. So I'm going to treat the specifics as unconfirmed and talk about the class of bug and the class of workflow, both of which are well understood for this engine lineage.
Why id Tech 3 networking is a target-rich environment
Games built on the Quake 3 engine family, which Call of Duty 1's engine descends from, share a networking model that is genuinely hostile to memory safety. The server and client exchange delta-compressed snapshots and command strings over UDP. A lot of the protocol is text-ish: configstrings, userinfo blobs, and console commands get parsed out of network buffers into fixed-size stack and static buffers. The classic bug shape is a parser that reads an attacker-controlled length or an attacker-controlled string and copies it into a buffer sized for the "normal" case.
This matters for why an LLM is useful here. The vulnerable code is C, it's parsing a semi-structured wire format, and the dangerous operations are named things a model has seen ten thousand times: strcpy, memcpy with a computed length, Q_strncpyz, sscanf into a stack array, MSG_ReadString into a fixed buffer. The engine source lineage is also partly public — id released Quake 3 source — so a model has strong priors about what these functions do and how they're typically misused, even when it's looking at a game that shipped as a closed binary.
That's the leverage. You're not asking the model to invent a novel bug class. You're asking it to pattern-match a known-dangerous shape across a large, boring codebase faster than you'd read it yourself.
Where the LLM earns its keep
The honest version of "found in an evening with AI" is that the model compresses the triage phase. Concretely, the tasks that map well onto an LLM:
Reading decompiler output and giving it back to you in a form a human can reason about. Hex-Rays or Ghidra output for a network parser is a wall of v17, a2, and casts. Handing a function to a model and asking "what wire format does this parse, and where does attacker-controlled length meet a fixed buffer?" turns twenty minutes of manual renaming into a paragraph you can sanity-check.
Ranking candidate sinks. If you can dump every call to strcpy/memcpy/sscanf and the surrounding context, a model is good at triaging "which of these takes data that came off the socket without a bound." That's the difference between reading 300 functions and reading the 6 that matter.
Reconstructing the packet you need to reach a sink. Once you know a bug is in, say, the parsing of a connectionless out-of-band message, the model can help you assemble the \xff\xff\xff\xff-prefixed OOB packet shape that id Tech 3 uses for pre-connection commands, because that format is well-documented and consistent across the engine family.
None of that is the model "finding a bug" autonomously. It's the model doing the reading and the bookkeeping so the human spends their attention on the two or three decisions that actually require judgment.
Where the human still has to reverse
The parts an LLM is bad at on this kind of target are exactly the parts where you can't afford it to be confident and wrong.
Ground truth about the binary. The model can hypothesise that a buffer is 1024 bytes, but the actual stack layout, the actual struct offsets, and whether a bound check exists three call frames up are facts about this binary. You confirm those in a debugger, not a chat window. A model that "reasons" a buffer is unbounded when there's a Q_strncpyzguarding it will send you down a dead end for an hour.
Exploitability versus crashability. Getting a crash is easy. Turning it into RCE means knowing what's adjacent in memory, whether the target has stack cookies or ASLR (a 2003 binary running under a modern OS is an interesting mix — the binary may predate mitigations while the OS enforces DEP/ASLR), and finding the primitive that survives the copy. That's iterative work in a debugger with a real target running.
Controlling the corruption. The gap between "I overflowed a buffer" and "I control RIP/EIP with a payload of my choosing" is where the evening's real time goes. The model can suggest a ROP strategy in the abstract, but it cannot enumerate the gadgets present in this image or verify that your chain lands.
The realistic division of labour, then: the LLM found the candidate fast, and a human confirmed the bug and built the exploit. "Found in an evening" is plausible precisely because the triage — normally the slowest part on unfamiliar code — got cut down hard.
A methodology you can reuse
If you want to run this loop against your own legacy target:
1. Get code into a readable form.
- Ghidra/IDA export of the networking functions, or
- public source for the engine lineage if it exists.
2. Enumerate sinks mechanically (grep / script), don't ask the LLM to find them.
strcpy, strcat, sprintf, memcpy(len), sscanf, alloca(len), *ReadString
3. For each sink, ask the model one narrow question:
"Does the length/string copied here come from network input,
and is it bounded before the copy? Cite the lines."
4. Take the top candidates into a debugger. Verify buffer sizes and
guards against the actual binary. This is non-negotiable.
5. Build the trigger packet using the engine's known wire format
(OOB \xff\xff\xff\xff prefix for connectionless commands on id Tech 3).
6. Do exploitation by hand: gadgets, mitigations, payload — in a debugger.1. Get code into a readable form.
- Ghidra/IDA export of the networking functions, or
- public source for the engine lineage if it exists.
2. Enumerate sinks mechanically (grep / script), don't ask the LLM to find them.
strcpy, strcat, sprintf, memcpy(len), sscanf, alloca(len), *ReadString
3. For each sink, ask the model one narrow question:
"Does the length/string copied here come from network input,
and is it bounded before the copy? Cite the lines."
4. Take the top candidates into a debugger. Verify buffer sizes and
guards against the actual binary. This is non-negotiable.
5. Build the trigger packet using the engine's known wire format
(OOB \xff\xff\xff\xff prefix for connectionless commands on id Tech 3).
6. Do exploitation by hand: gadgets, mitigations, payload — in a debugger.The discipline that makes this work is step 4. Treat every LLM claim about the binary as a hypothesis with a confidence of zero until the debugger confirms it. The model's value is throughput on reading, not authority on facts.
Detection and mitigation
For anyone running these servers — and community servers for 20-year-old shooters are still online — the mitigation story is unglamorous. This engine family has no auto-update, so patching means a community binary patch or nothing. Run the server as an unprivileged user in a container or a jail so an RCE gets you a sandboxed process, not the host. If the binary predates ASLR/DEP awareness, run it on an OS that enforces both at the loader level, which raises the cost of turning a memory-corruption bug into reliable code execution even on an old image.
On the detection side, id Tech 3 connectionless traffic is distinctive: UDP with the four 0xFF lead bytes followed by an ASCII command. Oversized OOB packets, or getstatus/connect/userinfo strings far longer than a legitimate client would send, are a cheap thing to alert on at the network layer. You don't need to understand the specific bug to notice a userinfo blob that's ten times normal length hitting the server port.
The broader takeaway isn't "AI finds RCEs now." It's that the slow, attention-draining part of vuln research on unfamiliar code — reading it — is the part LLMs compress best, and the part where being wrong is cheapest to catch. The exploitation still lived in a debugger, where it always has.