[<prev] [next>] [day] [month] [year] [list]
Message-ID: <7A0vEDdoG5h4mMwb2CVlws_zCU02jBBLof-sd-FSDlMANf_snKfz7FoGCR9bVuiWh9KeZkAnNPyPToU1M4SWYszAm9Ycuf5ukxJe0CWnXhQ=@protonmail.com>
Date: Fri, 14 Feb 2025 04:56:08 +0000
From: Gabriel Valachi via Fulldisclosure <fulldisclosure@...lists.org>
To: "fulldisclosure@...lists.org" <fulldisclosure@...lists.org>
Subject: [FD] [CVE-2024-54756] GZDoom <= 4.13.1 Arbitrary Code Execution via
Malicious ZScript
In GZDoom 4.13.1 and below, there is a vulnerability involving array sizes in ZScript, the game engine's primary scripting language. It is possible to dynamically allocate an array of 1073741823 dwords, permitting access to the rest of the heap from the start of the array and causing a second array declared in the same function to overlap with this huge array. The result is an exploit chain that allows arbitrary code execution through a malicious ZScript file, embedded in a WAD or PK3 file that can be distributed among the Doom community as a mod, mapset, etc.
MITRE has reserved CVE-2024-54756 for this.
This vulnerability affects GZDoom 4.13.1 and earlier, and most likely LZDoom is also affected. The devs have been notified privately, and it should be patched in 4.13.2 onward. This vulnerability was announced in the ZDoom Discord chatroom, but to the best of my knowledge nowhere else publicly.
I've already posted a writeup and proof of concept for Linux on GitHub:
https://github.com/Chainmanner/GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
Nevertheless, in the interest of preservation through duplication, the writeup in Markdown format (README.md) is attached to this email, and also attached are the PoC's zscript.zs and MAPINFO files.
To summarize the vulnerability and what it can lead to:
* In a ZScript function, one can allocate an array of 1073741823 or more 32-bit integers. Normally the contents of an array should be initialized, but for some reason they are not for arrays this large. Every element of this array, from the start of the array to the end of the heap, can be read and written.
* If a second array of reasonable size is allocated after the unreasonably huge array, both arrays will overlap. Particularly useful if the second array contains object pointers, as those cannot be arbitrarily set; by being able to modify those pointers directly, that gives the attacker an arbitrary read/write primitive to anywhere in addressable memory.
* An arbitrary execute primitive is possible by including a function pointer in the object pointed to in the second overlapping array and modifying it directly.
* ZScript can sometimes be JIT-compiled for performance improvements, and Asmjit is used to accomplish this. Unfortunately, it's misconfigured in a way that causes three RWX memory maps to be present, meaning an attacker can just write shellcode to one of them and execute it if they can find one such page.
* ZScript code is presumed to be JIT-compiled deterministically and in-order. Therefore, an attacker can defeat ASLR and locate any of the RWX pages by identifying the offsets of ZScript functions within any such pages and scanning the heap for pointers having such offsets (plus filtering for plausible addresses above, say, 0x600000000000). The longer the list of such pointers, the more likely an RWX page will be found.
The exploit is thus:
* Create an array of 1073741823 32-bit integers.
* Find an RWX region's address by scanning the heap from the huge array's start, and comparing each 64-bit integer's non-random bits with those in the known ZScript function offset list, while also rejecting addresses under e.g. 0x600000000000.
* Allocate a second array of two object pointers: one being the arbitrary read/write primitive, the second being the arbitrary execute.
* Prepare the arbitrary execute primitive to point to where the shellcode will be.
* Write the shellcode and any necessary data to the RWX region.
* Execute the shellcode.
The code is all executed in a static event handler called when the engine starts, before the main menu is reached.
A few other things I should note follow.
I unfortunately didn't study GZDoom's source code enough to fully understand why this huge array vulnerability happens; however, based on some informal testing, I have strong reason to believe it's due to an integer overflow, specifically an unsigned integer being treated as signed. I support this theory with the following:
* 1073741823 * 4 (the size of a 32-bit int) = 4294967292 = 0xfffffffc = -4 when considered signed
* Arrays are supposed to be initialized to all-zeros.
* No crash occurs when the array contains 1073741823 integers, but if the array has 536870912 ints, a crash occurs - namely a segmentation fault that occurs _before_ the start of the heap.
This is pure speculation, though. I should have spent the time to properly understand how this vulnerability came about so that I could suggest a proper fix, but I got too excited finding a way to chain the bug into an arbitrary code execution that I didn't. I'll do it better the next time I find and officially report a vulnerability.
The PoC I created isn't very reliable. It doesn't have many ZScript function offsets and I filter out pointers under 0x560000000000, not under 0x600000000000 as I suggest above. One may have to correct that and run the exploit several times for it to work. Still, given that it does work with ASLR enabled at least sometimes, I'd say it ultimately still proves the concept.
There are two other possible vulnerabilities found: a format string vulnerability in common/fonts/font.cpp/FFont::FFont(), and a strcpy() on a size-unchecked string in gamedata/statistics.cpp/LevelStatEntry(). Low-hanging fruit, but ultimately I couldn't find a way to exploit them into something serious on a modern machine, and for that reason I didn't think to request CVE IDs for them. The strcpy() bug has also been fixed in 4.13.2, but the format string has not been; if it's any consolation, the format string vulnerability seems hard to exploit given the function is a less feature-capable implementation of snprintf().
This is the first time I've found, disclosed, and requested a CVE ID for a vulnerability. If I made mistakes, sorry.
- Chainmanner
Download attachment "MAPINFO" of type "application/octet-stream" (58 bytes)
View attachment "README.md" of type "text/markdown" (16023 bytes)
Download attachment "zscript.zs" of type "application/octet-stream" (19041 bytes)
Download attachment "signature.asc" of type "application/pgp-signature" (604 bytes)
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/
Powered by blists - more mailing lists