diff --git a/README.md b/README.md index cadc32b..34f3a86 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,34 @@ The DLL is not a system module. It should be loaded, executed, and then unloaded **For those who do not code and just want to download/use Bad Storage:** it is built into the latest version of XeUnshackle, so just download that and it is all you need. Make sure to format your drive using FATXplorer as well. +## Fork: unauthenticated disk support + +This fork adds support for disks that never passed the console's security-sector check — no +Microsoft signature, no SSD Maker, no HDD Maker. Format the drive as a normal Xbox 360 hard disk +with FATXplorer and it mounts. + +It works by finishing the partition device initialisation that `SataDiskInitialize` skips when +authentication fails, rather than trying to defeat the authentication check itself (which cannot +work — that gate is evaluated once at boot, before any exploit payload runs). + +Capacity comes from the drive's own reported sector count rather than any declared model size, so +there is no retail-style size ceiling. Tested from a 240 GB SSD up to 1 TB mechanical disks. + +It can also reformat the disk from the console itself by holding LT, which recovers a drive already +flashed by SSD Maker even without its `undo.bin`. + +Note that a disk set up this way cannot serve an exploit entry point that reads from the internal +drive, such as BadAvatarHDD — an unauthenticated disk has no readable filesystem until the bypass +has run, and the bypass needs an exploit to already be running. USB-based entry points are +unaffected. + +Requires a retail console on kernel 17559 with Bad Update + XeUnshackle. See the +[wiki](https://github.com/Angelpro09xd/BadStorage/wiki) for how it works, the kernel internals it relies on, and its limitations. + +Upstream Bad Storage behaviour is unchanged: on an authenticated, BSTOR-formatted disk this fork +does exactly what it always did. + + # Compiling The recommended development environment is: diff --git a/src/BadStorage-DLL/BadStorage/BadStorage.cpp b/src/BadStorage-DLL/BadStorage/BadStorage.cpp index 1539f27..17b3bb6 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.cpp +++ b/src/BadStorage-DLL/BadStorage/BadStorage.cpp @@ -3,6 +3,8 @@ #include "stdafx.h" #include "BadStorage.h" +#include "UnauthenticatedDisk.h" +#include "FatxFormat.h" VOID Print(const PCHAR Format, ...) { @@ -144,20 +146,68 @@ EXTERN_C BOOLEAN Execute(PBOOLEAN IsRetailFormatted) return FALSE; } + //Held left trigger means "reformat this disk". Checked before anything else, because on an + //SSD Maker flashed drive the disk still authenticates at its flashed size and the code below + //would happily carry on using the smaller layout. + if (IsFormatRequested()) + { + Print("Left trigger held, reformatting the internal disk."); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Formatting internal disk, do not power off.", 0); + + if (FormatInternalDisk()) + { + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Disk formatted at full size. Reboot and run the exploit again.", 0); + } + else + { + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage FAILURE: Format did not complete.", 0); + } + return FALSE; + } + if ((XboxHardwareInfo->Flags & XBOX_HW_FLAG_HDD) != XBOX_HW_FLAG_HDD) { //If this value got filled in, it means there is a disk. if (*(PULONG)SataDiskUserAddressableSectors_Offset == 0) { + //Nothing was identified on the SATA bus at all. That is a lower-level problem than + //authentication, so there is nothing to recover here. Print("No disk."); XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: No internal disk connected.", 0); + return FALSE; } - else + + //A disk is present and identified, it just never passed the security-sector check, so the + //kernel left every partition device object half-initialised. Finish them off. + Print("Disk did not authenticate, initialising its partition devices."); + + ULONG initialisedCount = 0; + if (!InitializeUnauthenticatedDisk(&initialisedCount)) { - Print("Disk not genuine."); - XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: Disk not genuine/flashed. Flash using FATXplorer.", 0); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage FAILURE: Could not initialise the internal disk.", 0); + return FALSE; } - return FALSE; + + //Tell the dashboard the hard disk is there now. + XContent_DEVICEADDREMOVETASK addTask; + addTask.pszDevicePath = PARTITION_1_PATH; + addTask.Action = DEVICESTATE_ADD; + addTask.DeviceType = XCONTENTDEVICETYPE_HDD; + if (!((pfnXContentDeviceProcessAddRemove)XContentDeviceProcessAddRemove_Offset)(&addTask)) + { + Print("XContent::DeviceProcessAddRemove (add) returned FALSE."); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: Failed to process HDD addition.", 0); + return FALSE; + } + + ((pfnBroadcastStorageDevicesChanged)BroadcastStorageDevicesChanged_Offset)(); + + //The remainder of Bad Storage resizes a BSTOR formatted disk. A retail-formatted disk is + //fully usable at this point, so stop here. + Print("Unauthenticated disk mounted (%u partition devices initialised).", initialisedCount); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Auth bypass active - internal disk mounted.", 0); + if (IsRetailFormatted != NULL) *IsRetailFormatted = TRUE; + return TRUE; } PDEVICE_OBJECT phyDiskDeviceObject = NULL; @@ -208,8 +258,29 @@ EXTERN_C BOOLEAN Execute(PBOOLEAN IsRetailFormatted) if (!CheckBSTOR(diskHandle)) { if (IsRetailFormatted != NULL) *IsRetailFormatted = TRUE; - //Opting to not show this to avoid it being annoying. - //XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: Disk is not formatted for Bad Storage. Reformat using FATXplorer.", 0); + + //A disk flashed by SSD Maker authenticates, but it does so by presenting itself as an + //official Xbox 360 drive, so the console only ever sees that model's capacity. Point out + //when that is costing a meaningful amount of space. + // + //Deliberately only a message. The partition length cannot simply be raised here: FATX does + //not record its own volume size, so the kernel derives the cluster count - and with it the + //size of the allocation table - from the partition length the driver reports. Enlarging the + //partition under a volume that was formatted smaller moves where the data area is expected + //to start and corrupts the whole thing. Bad Storage can enlarge a BSTOR volume safely only + //because FATXplorer sizes its allocation table for the full capacity up front. + LONGLONG driveSize = (LONGLONG)(*(PULONG)SataDiskUserAddressableSectors_Offset) * 512; + LONGLONG claimed = p1DiskPartitionInfo->StartingOffset.QuadPart + p1DiskPartitionInfo->PartitionLength.QuadPart; + LONGLONG unused = driveSize - claimed; + + //A quarter of the drive is enough to be worth telling someone about. + if (unused > driveSize / 4) + { + Print("Drive is %I64d MB but only %I64d MB is in use; %I64d MB unreachable because of the flashed disk identity.", + driveSize / (1024 * 1024), claimed / (1024 * 1024), unused / (1024 * 1024)); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Drive is larger than its flashed size. Reformat with FATXplorer to use all of it.", 0); + } + goto cleanupAndExit; } diff --git a/src/BadStorage-DLL/BadStorage/BadStorage.h b/src/BadStorage-DLL/BadStorage/BadStorage.h index e03316e..11de84e 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.h +++ b/src/BadStorage-DLL/BadStorage/BadStorage.h @@ -41,4 +41,7 @@ const UCHAR BADSTORAGE_DID_PATCH_DATA[8] = { 0x3B, 0xC0, 0x00, 0x01, 0x48, 0x00, const PCHAR PHYSICAL_DISK_PATH = "\\Device\\Harddisk0\\PhysicalDisk"; const PCHAR PARTITION_0_PATH = "\\Device\\Harddisk0\\Partition0"; const PCHAR PARTITION_1_PATH = "\\Device\\Harddisk0\\Partition1"; -const PCHAR PARTITION_1_PATH_FILE = "\\Device\\Harddisk0\\Partition1\\"; \ No newline at end of file +const PCHAR PARTITION_1_PATH_FILE = "\\Device\\Harddisk0\\Partition1\\"; + +//Defined in BadStorage.cpp; declared here so other translation units can log too. +VOID Print(const PCHAR Format, ...); diff --git a/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj b/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj index 5eeeba7..55fa8b7 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj +++ b/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj @@ -158,6 +158,8 @@ + + @@ -166,6 +168,8 @@ Create + + diff --git a/src/BadStorage-DLL/BadStorage/FatxFormat.cpp b/src/BadStorage-DLL/BadStorage/FatxFormat.cpp new file mode 100644 index 0000000..7b4dfa6 --- /dev/null +++ b/src/BadStorage-DLL/BadStorage/FatxFormat.cpp @@ -0,0 +1,212 @@ +// On-console FATX formatting for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#include "stdafx.h" +#include "FatxFormat.h" +#include "BadStorage.h" +#include "UnauthenticatedDisk.h" + +//Written in chunks so the FAT can be cleared without allocating anything large. +#define FORMAT_CHUNK_SIZE 0x10000 +static BYTE g_FormatChunk[FORMAT_CHUNK_SIZE]; + +//One entry per partition the retail layout defines. Offsets are absolute byte positions on the +//drive, so everything is written through PhysicalDisk rather than through the partitions +//themselves - which is essential here, because while the disk still authenticates as a smaller +//model the partition devices do not even span the area being written. +typedef struct _FORMAT_PARTITION { + const CHAR* Name; + LONGLONG Offset; + LONGLONG Length; //0 means "everything left on the drive" +} FORMAT_PARTITION; + +BOOLEAN IsFormatRequested(void) +{ + //Any connected pad counts, so it does not matter which port is used. + for (DWORD i = 0; i < 4; i++) + { + XINPUT_STATE state; + ZeroMemory(&state, sizeof(state)); + if (XInputGetState(i, &state) == ERROR_SUCCESS) + { + //Well past the trigger's resting position, so a resting pad cannot trip it. + if (state.Gamepad.bLeftTrigger > 200) return TRUE; + } + } + return FALSE; +} + +//Opens the whole drive for writing. Partitions are deliberately not used - see the note above. +static HANDLE OpenPhysicalDiskForWrite(void) +{ + OBJECT_STRING str; + RtlInitAnsiString(&str, PHYSICAL_DISK_PATH); + + OBJECT_ATTRIBUTES oa; + InitializeObjectAttributes(&oa, &str, OBJ_CASE_INSENSITIVE, NULL, NULL); + + HANDLE handle; + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtOpenFile(&handle, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, &oa, &iosb, + FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SYNCHRONOUS_IO_NONALERT); + if (!NT_SUCCESS(status)) + { + Print("Could not open the disk for writing: 0x%08X", status); + return INVALID_HANDLE_VALUE; + } + return handle; +} + +static BOOLEAN WriteAt(HANDLE Disk, LONGLONG Offset, PVOID Buffer, ULONG Length) +{ + IO_STATUS_BLOCK iosb; + LARGE_INTEGER offset; + offset.QuadPart = Offset; + + NTSTATUS status = NtWriteFile(Disk, NULL, NULL, NULL, &iosb, Buffer, Length, &offset); + if (!NT_SUCCESS(status)) + { + Print("Write failed at %I64d (%u bytes): 0x%08X", Offset, Length, status); + return FALSE; + } + return TRUE; +} + +//Clears a span by writing zeroes over it. +static BOOLEAN ZeroRange(HANDLE Disk, LONGLONG Offset, LONGLONG Length) +{ + ZeroMemory(g_FormatChunk, sizeof(g_FormatChunk)); + + LONGLONG written = 0; + while (written < Length) + { + ULONG chunk = (ULONG)((Length - written) > FORMAT_CHUNK_SIZE ? FORMAT_CHUNK_SIZE : (Length - written)); + if (!WriteAt(Disk, Offset + written, g_FormatChunk, chunk)) return FALSE; + written += chunk; + } + return TRUE; +} + +/* + Writes one FATX volume. + + Layout, matching what a retail-formatted partition reads back as: + 0x0000 volume header + 0x1000 allocation table + 0x1000 + fatSize data area, cluster 1 first, holding the root directory + + The cluster count depends on the size of the allocation table, which itself depends on the + cluster count, so it is solved by iterating twice - which is enough to settle. +*/ +static BOOLEAN FormatPartition(HANDLE Disk, const CHAR* Name, LONGLONG Offset, LONGLONG Length) +{ + const ULONG clusterBytes = FATX_SECTORS_PER_CLUSTER * 512; + + ULONG clusterCount = (ULONG)((Length - FATX_HEADER_SIZE) / clusterBytes); + ULONG entrySize = (clusterCount >= FATX_FAT16_CLUSTER_LIMIT) ? 4 : 2; + LONGLONG fatSize = ((LONGLONG)clusterCount * entrySize + 0xFFF) & ~0xFFFLL; + + //Settle it now that the table's size is known. + clusterCount = (ULONG)((Length - FATX_HEADER_SIZE - fatSize) / clusterBytes); + entrySize = (clusterCount >= FATX_FAT16_CLUSTER_LIMIT) ? 4 : 2; + fatSize = ((LONGLONG)clusterCount * entrySize + 0xFFF) & ~0xFFFLL; + + LONGLONG dataStart = Offset + FATX_HEADER_SIZE + fatSize; + + Print("Formatting %s at %I64d, %I64d MB, %u clusters, %u-bit table (%I64d KB)", + Name, Offset, Length / (1024 * 1024), clusterCount, entrySize * 8, fatSize / 1024); + + //Header. Unused space in it reads as 0xFF on a retail volume. + ZeroMemory(g_FormatChunk, FATX_HEADER_SIZE); + memset(g_FormatChunk, 0xFF, FATX_HEADER_SIZE); + PULONG header = (PULONG)g_FormatChunk; + header[0] = FATX_MAGIC; + //Derived from the drive's own reported size so two partitions never share an id. + header[1] = (ULONG)(Offset >> 12) ^ (ULONG)Length ^ 0x5A4D0000; + header[2] = FATX_SECTORS_PER_CLUSTER; + header[3] = FATX_ROOT_CLUSTER; + if (!WriteAt(Disk, Offset, g_FormatChunk, FATX_HEADER_SIZE)) return FALSE; + + //Allocation table: everything free except the reserved entry and the root directory's chain. + if (!ZeroRange(Disk, Offset + FATX_HEADER_SIZE, fatSize)) return FALSE; + + ZeroMemory(g_FormatChunk, FORMAT_CHUNK_SIZE); + if (entrySize == 4) + { + PULONG fat = (PULONG)g_FormatChunk; + fat[0] = FATX_FAT_RESERVED0; + fat[1] = FATX_FAT_END_OF_CHAIN; + } + else + { + PUSHORT fat = (PUSHORT)g_FormatChunk; + fat[0] = (USHORT)FATX_FAT_RESERVED0; + fat[1] = (USHORT)FATX_FAT_END_OF_CHAIN; + } + if (!WriteAt(Disk, Offset + FATX_HEADER_SIZE, g_FormatChunk, 0x1000)) return FALSE; + + //Root directory. An unused FATX directory entry starts with 0xFF. + memset(g_FormatChunk, 0xFF, clusterBytes > FORMAT_CHUNK_SIZE ? FORMAT_CHUNK_SIZE : clusterBytes); + if (!WriteAt(Disk, dataStart, g_FormatChunk, clusterBytes)) return FALSE; + + return TRUE; +} + +BOOLEAN FormatInternalDisk(void) +{ + ULONG sectors = *(PULONG)SataDiskUserAddressableSectors_Offset; + if (sectors == 0) + { + Print("No disk identified, nothing to format."); + return FALSE; + } + + LONGLONG driveSize = (LONGLONG)sectors * 512; + if (driveSize <= (LONGLONG)UD_CONTENT_PARTITION_OFFSET) + { + Print("Disk is too small for the standard partition layout."); + return FALSE; + } + + Print("Formatting the internal disk: %I64d MB. Everything on it will be lost.", + driveSize / (1024 * 1024)); + + HANDLE disk = OpenPhysicalDiskForWrite(); + if (disk == INVALID_HANDLE_VALUE) return FALSE; + + BOOLEAN ok = TRUE; + + //Invalidate the security sector first. Until this is gone the disk keeps authenticating as + //whatever model it was flashed as, and the partitions get sized to that model again on the + //next boot no matter how the filesystems look. + Print("Clearing the security sector at 0x%X.", SECURITY_SECTOR_OFFSET); + if (!ZeroRange(disk, SECURITY_SECTOR_OFFSET, SECURITY_SECTOR_LENGTH)) ok = FALSE; + + //Same layout the kernel itself uses, so a formatted disk matches what the bypass will publish. + static const FORMAT_PARTITION partitions[] = { + { "Cache0", UD_CACHE0_OFFSET, UD_CACHE_LENGTH }, + { "Cache1", UD_CACHE1_OFFSET, UD_CACHE_LENGTH }, + { "SystemURLCachePartition", UD_SYSTEM_URL_CACHE_OFFSET, UD_SYSTEM_URL_CACHE_LENGTH }, + { "TitleURLCachePartition", UD_TITLE_URL_CACHE_OFFSET, UD_TITLE_URL_CACHE_LENGTH }, + { "SystemExtPartition", UD_SYSTEM_EXT_OFFSET, UD_SYSTEM_EXT_LENGTH }, + { "SystemAuxPartition", UD_SYSTEM_AUX_OFFSET, UD_SYSTEM_AUX_LENGTH }, + { "SystemPartition", UD_SYSTEM_PARTITION_OFFSET, UD_SYSTEM_PARTITION_LENGTH }, + { "Partition1", UD_CONTENT_PARTITION_OFFSET, 0 }, + }; + + for (int i = 0; ok && i < (int)(sizeof(partitions) / sizeof(partitions[0])); i++) + { + LONGLONG length = partitions[i].Length != 0 + ? partitions[i].Length + : driveSize - partitions[i].Offset; + + if (!FormatPartition(disk, partitions[i].Name, partitions[i].Offset, length)) ok = FALSE; + } + + NtClose(disk); + + if (ok) Print("Format complete. Reboot, then run the exploit again to pick up the full size."); + else Print("Format did not complete."); + + return ok; +} diff --git a/src/BadStorage-DLL/BadStorage/FatxFormat.h b/src/BadStorage-DLL/BadStorage/FatxFormat.h new file mode 100644 index 0000000..e27532e --- /dev/null +++ b/src/BadStorage-DLL/BadStorage/FatxFormat.h @@ -0,0 +1,78 @@ +// On-console FATX formatting for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#pragma once + +/* + Formats the internal disk from the console itself, so the result is guaranteed to match what + the kernel expects without needing a PC. + + Why this exists + --------------- + A drive flashed by SSD Maker presents itself as an official Xbox 360 model, so the console only + ever sees that model's capacity - a 240 GB SSD flashed as a 120 GB disk gives about 107 GiB of + content partition instead of 218 GiB. Reformatting with FATXplorer on a PC fixes that, but + undoing an SSD Maker flash normally wants the undo.bin that was produced at flash time, and not + everyone still has it. + + Two things have to happen, not one + ---------------------------------- + Simply writing fresh FATX volumes is not enough. The security sector still declares the smaller + model, so on the next boot the disk authenticates, the bypass never engages, and + SataDiskInitialize sizes the partitions to the flashed identity again regardless of what the + filesystem looks like. + + So this also invalidates the security sector. Once it no longer authenticates, the bypass takes + over and lays out the partitions across the whole drive. + + That means the sequence is: + + 1. Hold LT at boot -> security sector wiped, partitions formatted for the full drive + 2. Reboot + 3. Disk no longer authenticates -> bypass runs -> full capacity available + + This is destructive. Everything on the disk is lost. + + Verifying the result + -------------------- + After formatting, connect the drive to a PC and open it in FATXplorer. If it shows a valid + volume at the full size, the layout is right. Do that before putting anything on it. +*/ + +#include "XenonExports.h" + +// FATX volume header, at offset 0 of every partition. Values are big-endian DWORDs, matching what +// a retail-formatted partition reads back as. +#define FATX_MAGIC 0x58544146 // 'X','T','A','F' +#define FATX_HEADER_SIZE 0x1000 +#define FATX_SECTORS_PER_CLUSTER 32 // 16 KB clusters, what retail uses +#define FATX_ROOT_CLUSTER 1 + +// A FAT holding this many clusters or more needs 32-bit entries instead of 16-bit. +#define FATX_FAT16_CLUSTER_LIMIT 0xFFF0 + +#define FATX_FAT_RESERVED0 0xFFFFFFF8 // media descriptor +#define FATX_FAT_END_OF_CHAIN 0xFFFFFFFF + +// Byte offset of the sector the kernel authenticates against, and how much of it it reads. +// Taken from SataDiskAuthenticateDevice itself (0x8015D9E4 onwards): it loads 0x2000 as a 64-bit +// offset, asks for 0x200 bytes via IoSynchronousFsdRequest on the PhysicalDisk device, feeds the +// result to XeKeysSetKey and compares the digest. +// +// Note this is NOT "sixteen sectors from the end of the drive". That was an unverified guess in +// the project notes, and wiping there does nothing at all - the disk keeps authenticating. +#define SECURITY_SECTOR_OFFSET 0x2000 +#define SECURITY_SECTOR_LENGTH 0x200 + +/* + Wipes the security sector and writes fresh FATX volumes sized for the whole drive. + + Returns TRUE only if every partition was written. On failure it stops at the first error rather + than leaving a partially formatted disk behind unreported. +*/ +BOOLEAN FormatInternalDisk(void); + +/* + TRUE while the left trigger is held. Used to gate the format behind a deliberate action. +*/ +BOOLEAN IsFormatRequested(void); diff --git a/src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.cpp b/src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.cpp new file mode 100644 index 0000000..715c844 --- /dev/null +++ b/src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.cpp @@ -0,0 +1,156 @@ +// Unauthenticated disk support for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#include "stdafx.h" +#include "UnauthenticatedDisk.h" +#include "BadStorage.h" + +// Directory entry names are counted, not NUL-terminated. +static BOOLEAN NameEquals(const CHAR* Buffer, USHORT Length, const CHAR* Expected) +{ + USHORT i = 0; + for (; i < Length; i++) + { + if (Expected[i] == '\0' || Buffer[i] != Expected[i]) return FALSE; + } + return Expected[i] == '\0'; +} + +// The geometry SataDiskInitialize would have written for a given partition. +// Returns FALSE for names that are not hard disk partitions. +static BOOLEAN GetPartitionGeometry(const CHAR* Name, USHORT NameLength, LONGLONG DiskSize, PLONGLONG Offset, PLONGLONG Length) +{ + if (NameEquals(Name, NameLength, "Partition0")) { *Offset = 0; *Length = DiskSize; } + else if (NameEquals(Name, NameLength, "Partition1")) { *Offset = UD_CONTENT_PARTITION_OFFSET; *Length = DiskSize - UD_CONTENT_PARTITION_OFFSET; } + else if (NameEquals(Name, NameLength, "Cache0")) { *Offset = UD_CACHE0_OFFSET; *Length = UD_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "Cache1")) { *Offset = UD_CACHE1_OFFSET; *Length = UD_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "DumpPartition")) { *Offset = UD_DUMP_OFFSET; *Length = UD_DUMP_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemURLCachePartition")) { *Offset = UD_SYSTEM_URL_CACHE_OFFSET; *Length = UD_SYSTEM_URL_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "TitleURLCachePartition")) { *Offset = UD_TITLE_URL_CACHE_OFFSET; *Length = UD_TITLE_URL_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemExtPartition")) { *Offset = UD_SYSTEM_EXT_OFFSET; *Length = UD_SYSTEM_EXT_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemAuxPartition")) { *Offset = UD_SYSTEM_AUX_OFFSET; *Length = UD_SYSTEM_AUX_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemPartition")) { *Offset = UD_SYSTEM_PARTITION_OFFSET; *Length = UD_SYSTEM_PARTITION_LENGTH; } + else if (NameEquals(Name, NameLength, "WindowsPartition")) { *Offset = 0; *Length = 0; } + else return FALSE; + + return TRUE; +} + +// Confirms a device really did become reachable by name. +static BOOLEAN DeviceResolves(const PSZ Path) +{ + PDEVICE_OBJECT device = NULL; + OBJECT_STRING str; + RtlInitAnsiString(&str, Path); + + NTSTATUS status = ObReferenceObjectByName(&str, 0, IoDeviceObjectType, NULL, (PVOID*)&device); + if (!NT_SUCCESS(status) || device == NULL) + { + Print("%s did not resolve: 0x%08X", Path, status); + return FALSE; + } + + ObDereferenceObject(device); + return TRUE; +} + +BOOLEAN InitializeUnauthenticatedDisk(ULONG* FixedCount) +{ + if (FixedCount != NULL) *FixedCount = 0; + + // The drive's own reported capacity. Zero means the console never identified a disk at all, + // which is a lower-level problem than authentication and not something this can repair. + ULONG sectors = *(PULONG)SataDiskUserAddressableSectors_Offset; + if (sectors == 0) + { + Print("No disk identified, nothing to initialise."); + return FALSE; + } + + LONGLONG diskSize = (LONGLONG)sectors * 512; + Print("Unauthenticated disk: %I64d bytes (%I64d MB).", diskSize, diskSize / (1024 * 1024)); + + // A disk smaller than the fixed system partition area cannot use this layout. + if (diskSize <= (LONGLONG)UD_CONTENT_PARTITION_OFFSET) + { + Print("Disk is too small for the standard partition layout."); + return FALSE; + } + + // The partition devices are unreachable by name while DO_DEVICE_INITIALIZING is set, so go + // through the object directory instead. + PVOID directory = NULL; + OBJECT_STRING directoryStr; + RtlInitAnsiString(&directoryStr, HARDDISK0_DIRECTORY_PATH); + + NTSTATUS status = ObReferenceObjectByName(&directoryStr, 0, ObDirectoryObjectType, NULL, &directory); + if (!NT_SUCCESS(status) || directory == NULL) + { + Print("ObReferenceObjectByName failed on %s: 0x%08X", HARDDISK0_DIRECTORY_PATH, status); + return FALSE; + } + + ULONG fixed = 0; + PULONG buckets = (PULONG)directory; + + for (ULONG bucket = 0; bucket < OBJECT_DIRECTORY_BUCKETS; bucket++) + { + ULONG entry = buckets[bucket]; + + // Bounded so a corrupted chain cannot spin forever. + for (ULONG depth = 0; entry != 0 && depth < 32; depth++) + { + USHORT nameLength = *(PUSHORT)(entry + OBJDIR_ENTRY_NAMELEN); + PCHAR nameBuffer = *(PCHAR*)(entry + OBJDIR_ENTRY_NAMEBUF); + ULONG next = *(PULONG)(entry + OBJDIR_ENTRY_NEXT); + + if (nameLength != 0 && nameLength < 64 && nameBuffer != NULL) + { + PDEVICE_OBJECT device = (PDEVICE_OBJECT)(entry + OBJDIR_ENTRY_TO_OBJECT); + + // Only touch devices still stuck mid-initialisation. On an authenticated disk + // there are none, so this whole loop does nothing. + if ((device->Flags & DO_DEVICE_INITIALIZING_360) == DO_DEVICE_INITIALIZING_360) + { + LONGLONG offset = 0, length = 0; + if (GetPartitionGeometry(nameBuffer, nameLength, diskSize, &offset, &length)) + { + PPARTITION_INFORMATION info = &((PSATA_DISK_EXTENSION)device->DeviceExtension)->PartitionInformation; + info->StartingOffset.QuadPart = offset; + info->PartitionLength.QuadPart = length; + + // The kernel's own final step for each device. Its instruction is + // "rlwinm rX, rX, 0, 28, 26", a wrapped mask whose only effect is to + // clear this bit. + device->Flags &= ~DO_DEVICE_INITIALIZING_360; + + fixed++; + Print("Initialised %.*s: offset %I64d, length %I64d", nameLength, nameBuffer, offset, length); + } + } + } + + entry = next; + } + } + + ObDereferenceObject(directory); + + if (FixedCount != NULL) *FixedCount = fixed; + + if (fixed == 0) + { + Print("No uninitialised partition devices found."); + return FALSE; + } + + // These two are what the dashboard actually needs, so they decide success. + if (!DeviceResolves(PARTITION_0_PATH) || !DeviceResolves(PARTITION_1_PATH)) + { + Print("Partitions were written but still do not resolve."); + return FALSE; + } + + Print("Initialised %u partition devices.", fixed); + return TRUE; +} diff --git a/src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.h b/src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.h new file mode 100644 index 0000000..fb73240 --- /dev/null +++ b/src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.h @@ -0,0 +1,100 @@ +// Unauthenticated disk support for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#pragma once + +/* + Lets a disk that never passed the console's security-sector check be used as internal storage. + + Background + ---------- + During boot, SataDiskInitialize does two separate things, in this order: + + 1. BEFORE the authentication gate, it creates all twelve hard disk device objects + (PhysicalDisk, Partition0, Partition1, Cache0/1, DumpPartition, SystemPartition, ...) + by calling SataDiskCreateDevice -> IoCreateDevice. + + 2. AFTER the gate, it fills in each device's PartitionInformation and clears + DO_DEVICE_INITIALIZING on it. + + When SataDiskAuthenticateDevice fails, the gate branches straight to the function epilogue, + so step 2 never runs. The device objects therefore exist, but with a zeroed + PartitionInformation and DO_DEVICE_INITIALIZING still set. A device in that state makes + ObReferenceObjectByName return STATUS_NO_SUCH_DEVICE (0xC000000E) - note that this is *not* + STATUS_OBJECT_NAME_NOT_FOUND: the name resolves fine, it is the device that gets refused. + + PhysicalDisk is the one exception and stays usable, because it is completed before the gate. + + Approach + -------- + Patching SataDiskAuthenticateDevice to return TRUE does not help. That gate is evaluated once, + early in boot, long before any exploit payload runs, and there is no warm reboot that would + re-run it while keeping patches in memory. (XeUnshackle already applies exactly that patch as + part of its Freeboot set, and it still is not enough.) + + Instead, this finishes the initialisation the gate skipped: reach the device objects by walking + the \Device\Harddisk0 object directory - they cannot be reached by name, precisely because of + the flag - write each partition's geometry, and clear DO_DEVICE_INITIALIZING. + + Everything written here is what SataDiskInitialize's own post-gate block computes; it was + recovered by disassembling that block instruction by instruction. Two independent checks say + the values are right: the resulting layout is perfectly contiguous, and Partition1's offset + comes out as 0x130EB0000, the same constant Bad Storage already hardcodes. + + Scope + ----- + This addresses authentication only. It requires a disk the console can already talk to and + identify, i.e. one that answers the ATA commands and reports its sector count. A drive whose + controller the console cannot negotiate with at all leaves SataDiskUserAddressableSectors at + zero, and there is nothing here to complete. +*/ + +#include "XenonExports.h" + +// On this kernel DO_DEVICE_INITIALIZING is 0x10, not the 0x80 it is on Windows. +// From IoCreateDevice at 0x8006B00C: "li r11, 0x10" then "stw r11, 0x14(r10)". +#define DO_DEVICE_INITIALIZING_360 0x10 + +// The object directory is a 13-bucket hash table of chained entries. +// From NtQueryDirectoryObject's own walk at 0x8008A22C-0x8008A264. +#define OBJECT_DIRECTORY_BUCKETS 13 + +// Offsets within a directory entry, same source. +#define OBJDIR_ENTRY_NEXT 0x00 +#define OBJDIR_ENTRY_NAMELEN 0x08 +#define OBJDIR_ENTRY_NAMEBUF 0x0C + +// Distance from a directory entry to the object body it refers to. +// Measured on hardware rather than assumed: the PhysicalDisk entry sat at 0x3A0A7C88 while +// ObReferenceObjectByName returned 0x3A0A7CA8 for the same name. +#define OBJDIR_ENTRY_TO_OBJECT 0x20 + +// Byte offset of the content partition, and the fixed geometry of the system partitions. +// All derived from SataDiskInitialize 0x8015DF28-0x8015E0D0. +#define UD_CACHE0_OFFSET 0x80000ULL +#define UD_CACHE1_OFFSET 0x80080000ULL +#define UD_CACHE_LENGTH 0x80000000ULL +#define UD_DUMP_OFFSET 0x100080000ULL +#define UD_DUMP_LENGTH 0x20E30000ULL +#define UD_SYSTEM_URL_CACHE_OFFSET 0x100080000ULL +#define UD_SYSTEM_URL_CACHE_LENGTH 0x6000000ULL +#define UD_TITLE_URL_CACHE_OFFSET 0x106080000ULL +#define UD_TITLE_URL_CACHE_LENGTH 0x2000000ULL +#define UD_SYSTEM_EXT_OFFSET 0x10C080000ULL +#define UD_SYSTEM_EXT_LENGTH 0xCE30000ULL +#define UD_SYSTEM_AUX_OFFSET 0x118EB0000ULL +#define UD_SYSTEM_AUX_LENGTH 0x8000000ULL +#define UD_SYSTEM_PARTITION_OFFSET 0x120EB0000ULL +#define UD_SYSTEM_PARTITION_LENGTH 0x10000000ULL +#define UD_CONTENT_PARTITION_OFFSET 0x130EB0000ULL + +#define HARDDISK0_DIRECTORY_PATH "\\Device\\Harddisk0" + +/* + Completes the partition device objects the authentication gate left half-initialised. + + Returns TRUE only if Partition0 and Partition1 both resolve by name afterwards, which is the + real proof that the devices became usable. Safe to call when nothing needs fixing: devices that + are already initialised are skipped, so a second call is a no-op. +*/ +BOOLEAN InitializeUnauthenticatedDisk(ULONG* FixedCount); diff --git a/src/BadStorage-DLL/BadStorage/XenonExports.h b/src/BadStorage-DLL/BadStorage/XenonExports.h index 80d1028..6fcbbef 100644 --- a/src/BadStorage-DLL/BadStorage/XenonExports.h +++ b/src/BadStorage-DLL/BadStorage/XenonExports.h @@ -765,6 +765,7 @@ EXTERN_C extern PXBOX_KRNL_VERSION XboxKrnlVersion; extern PXBOX_HARDWARE_INFO XboxHardwareInfo; extern PVOID IoDeviceObjectType; + extern PVOID ObDirectoryObjectType; HRESULT WINAPI diff --git a/src/BadStorage-XEX/BadStorage/BadStorage.cpp b/src/BadStorage-XEX/BadStorage/BadStorage.cpp index 967b483..75c1eff 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.cpp +++ b/src/BadStorage-XEX/BadStorage/BadStorage.cpp @@ -3,6 +3,8 @@ #include "stdafx.h" #include "BadStorage.h" +#include "UnauthenticatedDisk.h" +#include "FatxFormat.h" VOID Print(const PCHAR Format, ...) { @@ -136,19 +138,66 @@ VOID __cdecl main() return; } + //Held left trigger means "reformat this disk". Checked before anything else, because on an + //SSD Maker flashed drive the disk still authenticates at its flashed size and the code below + //would happily carry on using the smaller layout. + if (IsFormatRequested()) + { + Print("Left trigger held, reformatting the internal disk."); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Formatting internal disk, do not power off.", 0); + + if (FormatInternalDisk()) + { + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Disk formatted at full size. Reboot and run the exploit again.", 0); + } + else + { + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage FAILURE: Format did not complete.", 0); + } + return; + } + if ((XboxHardwareInfo->Flags & XBOX_HW_FLAG_HDD) != XBOX_HW_FLAG_HDD) { //If this value got filled in, it means there is a disk. if (*(PULONG)SataDiskUserAddressableSectors_Offset == 0) { + //Nothing was identified on the SATA bus at all. That is a lower-level problem than + //authentication, so there is nothing to recover here. Print("No disk."); XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: No internal disk connected.", 0); + return; } - else + + //A disk is present and identified, it just never passed the security-sector check, so the + //kernel left every partition device object half-initialised. Finish them off. + Print("Disk did not authenticate, initialising its partition devices."); + + ULONG initialisedCount = 0; + if (!InitializeUnauthenticatedDisk(&initialisedCount)) { - Print("Disk not genuine."); - XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: Disk not genuine/flashed. Flash using FATXplorer.", 0); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage FAILURE: Could not initialise the internal disk.", 0); + return; } + + //Tell the dashboard the hard disk is there now. + XContent_DEVICEADDREMOVETASK addTask; + addTask.pszDevicePath = PARTITION_1_PATH; + addTask.Action = DEVICESTATE_ADD; + addTask.DeviceType = XCONTENTDEVICETYPE_HDD; + if (!((pfnXContentDeviceProcessAddRemove)XContentDeviceProcessAddRemove_Offset)(&addTask)) + { + Print("XContent::DeviceProcessAddRemove (add) returned FALSE."); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: Failed to process HDD addition.", 0); + return; + } + + ((pfnBroadcastStorageDevicesChanged)BroadcastStorageDevicesChanged_Offset)(); + + //The remainder of Bad Storage resizes a BSTOR formatted disk. A retail-formatted disk is + //fully usable at this point, so stop here. + Print("Unauthenticated disk mounted (%u partition devices initialised).", initialisedCount); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Auth bypass active - internal disk mounted.", 0); return; } @@ -199,7 +248,32 @@ VOID __cdecl main() if (!CheckBSTOR(diskHandle)) { - XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: Disk is not formatted for Bad Storage. Reformat using FATXplorer.", 0); + //A disk flashed by SSD Maker authenticates, but it does so by presenting itself as an + //official Xbox 360 drive, so the console only ever sees that model's capacity. Point out + //when that is costing a meaningful amount of space. + // + //Deliberately only a message. The partition length cannot simply be raised here: FATX does + //not record its own volume size, so the kernel derives the cluster count - and with it the + //size of the allocation table - from the partition length the driver reports. Enlarging the + //partition under a volume that was formatted smaller moves where the data area is expected + //to start and corrupts the whole thing. Bad Storage can enlarge a BSTOR volume safely only + //because FATXplorer sizes its allocation table for the full capacity up front. + LONGLONG driveSize = (LONGLONG)(*(PULONG)SataDiskUserAddressableSectors_Offset) * 512; + LONGLONG claimed = p1DiskPartitionInfo->StartingOffset.QuadPart + p1DiskPartitionInfo->PartitionLength.QuadPart; + LONGLONG unused = driveSize - claimed; + + //A quarter of the drive is enough to be worth telling someone about. + if (unused > driveSize / 4) + { + Print("Drive is %I64d MB but only %I64d MB is in use; %I64d MB unreachable because of the flashed disk identity.", + driveSize / (1024 * 1024), claimed / (1024 * 1024), unused / (1024 * 1024)); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Drive is larger than its flashed size. Reformat with FATXplorer to use all of it.", 0); + } + else + { + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"BadStorage FAILURE: Disk is not formatted for Bad Storage. Reformat using FATXplorer.", 0); + } + goto cleanupAndExit; } diff --git a/src/BadStorage-XEX/BadStorage/BadStorage.h b/src/BadStorage-XEX/BadStorage/BadStorage.h index e03316e..11de84e 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.h +++ b/src/BadStorage-XEX/BadStorage/BadStorage.h @@ -41,4 +41,7 @@ const UCHAR BADSTORAGE_DID_PATCH_DATA[8] = { 0x3B, 0xC0, 0x00, 0x01, 0x48, 0x00, const PCHAR PHYSICAL_DISK_PATH = "\\Device\\Harddisk0\\PhysicalDisk"; const PCHAR PARTITION_0_PATH = "\\Device\\Harddisk0\\Partition0"; const PCHAR PARTITION_1_PATH = "\\Device\\Harddisk0\\Partition1"; -const PCHAR PARTITION_1_PATH_FILE = "\\Device\\Harddisk0\\Partition1\\"; \ No newline at end of file +const PCHAR PARTITION_1_PATH_FILE = "\\Device\\Harddisk0\\Partition1\\"; + +//Defined in BadStorage.cpp; declared here so other translation units can log too. +VOID Print(const PCHAR Format, ...); diff --git a/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj b/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj index ffc38e2..279a3e7 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj +++ b/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj @@ -141,6 +141,8 @@ + + @@ -149,6 +151,8 @@ Create + + diff --git a/src/BadStorage-XEX/BadStorage/FatxFormat.cpp b/src/BadStorage-XEX/BadStorage/FatxFormat.cpp new file mode 100644 index 0000000..7b4dfa6 --- /dev/null +++ b/src/BadStorage-XEX/BadStorage/FatxFormat.cpp @@ -0,0 +1,212 @@ +// On-console FATX formatting for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#include "stdafx.h" +#include "FatxFormat.h" +#include "BadStorage.h" +#include "UnauthenticatedDisk.h" + +//Written in chunks so the FAT can be cleared without allocating anything large. +#define FORMAT_CHUNK_SIZE 0x10000 +static BYTE g_FormatChunk[FORMAT_CHUNK_SIZE]; + +//One entry per partition the retail layout defines. Offsets are absolute byte positions on the +//drive, so everything is written through PhysicalDisk rather than through the partitions +//themselves - which is essential here, because while the disk still authenticates as a smaller +//model the partition devices do not even span the area being written. +typedef struct _FORMAT_PARTITION { + const CHAR* Name; + LONGLONG Offset; + LONGLONG Length; //0 means "everything left on the drive" +} FORMAT_PARTITION; + +BOOLEAN IsFormatRequested(void) +{ + //Any connected pad counts, so it does not matter which port is used. + for (DWORD i = 0; i < 4; i++) + { + XINPUT_STATE state; + ZeroMemory(&state, sizeof(state)); + if (XInputGetState(i, &state) == ERROR_SUCCESS) + { + //Well past the trigger's resting position, so a resting pad cannot trip it. + if (state.Gamepad.bLeftTrigger > 200) return TRUE; + } + } + return FALSE; +} + +//Opens the whole drive for writing. Partitions are deliberately not used - see the note above. +static HANDLE OpenPhysicalDiskForWrite(void) +{ + OBJECT_STRING str; + RtlInitAnsiString(&str, PHYSICAL_DISK_PATH); + + OBJECT_ATTRIBUTES oa; + InitializeObjectAttributes(&oa, &str, OBJ_CASE_INSENSITIVE, NULL, NULL); + + HANDLE handle; + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtOpenFile(&handle, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, &oa, &iosb, + FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SYNCHRONOUS_IO_NONALERT); + if (!NT_SUCCESS(status)) + { + Print("Could not open the disk for writing: 0x%08X", status); + return INVALID_HANDLE_VALUE; + } + return handle; +} + +static BOOLEAN WriteAt(HANDLE Disk, LONGLONG Offset, PVOID Buffer, ULONG Length) +{ + IO_STATUS_BLOCK iosb; + LARGE_INTEGER offset; + offset.QuadPart = Offset; + + NTSTATUS status = NtWriteFile(Disk, NULL, NULL, NULL, &iosb, Buffer, Length, &offset); + if (!NT_SUCCESS(status)) + { + Print("Write failed at %I64d (%u bytes): 0x%08X", Offset, Length, status); + return FALSE; + } + return TRUE; +} + +//Clears a span by writing zeroes over it. +static BOOLEAN ZeroRange(HANDLE Disk, LONGLONG Offset, LONGLONG Length) +{ + ZeroMemory(g_FormatChunk, sizeof(g_FormatChunk)); + + LONGLONG written = 0; + while (written < Length) + { + ULONG chunk = (ULONG)((Length - written) > FORMAT_CHUNK_SIZE ? FORMAT_CHUNK_SIZE : (Length - written)); + if (!WriteAt(Disk, Offset + written, g_FormatChunk, chunk)) return FALSE; + written += chunk; + } + return TRUE; +} + +/* + Writes one FATX volume. + + Layout, matching what a retail-formatted partition reads back as: + 0x0000 volume header + 0x1000 allocation table + 0x1000 + fatSize data area, cluster 1 first, holding the root directory + + The cluster count depends on the size of the allocation table, which itself depends on the + cluster count, so it is solved by iterating twice - which is enough to settle. +*/ +static BOOLEAN FormatPartition(HANDLE Disk, const CHAR* Name, LONGLONG Offset, LONGLONG Length) +{ + const ULONG clusterBytes = FATX_SECTORS_PER_CLUSTER * 512; + + ULONG clusterCount = (ULONG)((Length - FATX_HEADER_SIZE) / clusterBytes); + ULONG entrySize = (clusterCount >= FATX_FAT16_CLUSTER_LIMIT) ? 4 : 2; + LONGLONG fatSize = ((LONGLONG)clusterCount * entrySize + 0xFFF) & ~0xFFFLL; + + //Settle it now that the table's size is known. + clusterCount = (ULONG)((Length - FATX_HEADER_SIZE - fatSize) / clusterBytes); + entrySize = (clusterCount >= FATX_FAT16_CLUSTER_LIMIT) ? 4 : 2; + fatSize = ((LONGLONG)clusterCount * entrySize + 0xFFF) & ~0xFFFLL; + + LONGLONG dataStart = Offset + FATX_HEADER_SIZE + fatSize; + + Print("Formatting %s at %I64d, %I64d MB, %u clusters, %u-bit table (%I64d KB)", + Name, Offset, Length / (1024 * 1024), clusterCount, entrySize * 8, fatSize / 1024); + + //Header. Unused space in it reads as 0xFF on a retail volume. + ZeroMemory(g_FormatChunk, FATX_HEADER_SIZE); + memset(g_FormatChunk, 0xFF, FATX_HEADER_SIZE); + PULONG header = (PULONG)g_FormatChunk; + header[0] = FATX_MAGIC; + //Derived from the drive's own reported size so two partitions never share an id. + header[1] = (ULONG)(Offset >> 12) ^ (ULONG)Length ^ 0x5A4D0000; + header[2] = FATX_SECTORS_PER_CLUSTER; + header[3] = FATX_ROOT_CLUSTER; + if (!WriteAt(Disk, Offset, g_FormatChunk, FATX_HEADER_SIZE)) return FALSE; + + //Allocation table: everything free except the reserved entry and the root directory's chain. + if (!ZeroRange(Disk, Offset + FATX_HEADER_SIZE, fatSize)) return FALSE; + + ZeroMemory(g_FormatChunk, FORMAT_CHUNK_SIZE); + if (entrySize == 4) + { + PULONG fat = (PULONG)g_FormatChunk; + fat[0] = FATX_FAT_RESERVED0; + fat[1] = FATX_FAT_END_OF_CHAIN; + } + else + { + PUSHORT fat = (PUSHORT)g_FormatChunk; + fat[0] = (USHORT)FATX_FAT_RESERVED0; + fat[1] = (USHORT)FATX_FAT_END_OF_CHAIN; + } + if (!WriteAt(Disk, Offset + FATX_HEADER_SIZE, g_FormatChunk, 0x1000)) return FALSE; + + //Root directory. An unused FATX directory entry starts with 0xFF. + memset(g_FormatChunk, 0xFF, clusterBytes > FORMAT_CHUNK_SIZE ? FORMAT_CHUNK_SIZE : clusterBytes); + if (!WriteAt(Disk, dataStart, g_FormatChunk, clusterBytes)) return FALSE; + + return TRUE; +} + +BOOLEAN FormatInternalDisk(void) +{ + ULONG sectors = *(PULONG)SataDiskUserAddressableSectors_Offset; + if (sectors == 0) + { + Print("No disk identified, nothing to format."); + return FALSE; + } + + LONGLONG driveSize = (LONGLONG)sectors * 512; + if (driveSize <= (LONGLONG)UD_CONTENT_PARTITION_OFFSET) + { + Print("Disk is too small for the standard partition layout."); + return FALSE; + } + + Print("Formatting the internal disk: %I64d MB. Everything on it will be lost.", + driveSize / (1024 * 1024)); + + HANDLE disk = OpenPhysicalDiskForWrite(); + if (disk == INVALID_HANDLE_VALUE) return FALSE; + + BOOLEAN ok = TRUE; + + //Invalidate the security sector first. Until this is gone the disk keeps authenticating as + //whatever model it was flashed as, and the partitions get sized to that model again on the + //next boot no matter how the filesystems look. + Print("Clearing the security sector at 0x%X.", SECURITY_SECTOR_OFFSET); + if (!ZeroRange(disk, SECURITY_SECTOR_OFFSET, SECURITY_SECTOR_LENGTH)) ok = FALSE; + + //Same layout the kernel itself uses, so a formatted disk matches what the bypass will publish. + static const FORMAT_PARTITION partitions[] = { + { "Cache0", UD_CACHE0_OFFSET, UD_CACHE_LENGTH }, + { "Cache1", UD_CACHE1_OFFSET, UD_CACHE_LENGTH }, + { "SystemURLCachePartition", UD_SYSTEM_URL_CACHE_OFFSET, UD_SYSTEM_URL_CACHE_LENGTH }, + { "TitleURLCachePartition", UD_TITLE_URL_CACHE_OFFSET, UD_TITLE_URL_CACHE_LENGTH }, + { "SystemExtPartition", UD_SYSTEM_EXT_OFFSET, UD_SYSTEM_EXT_LENGTH }, + { "SystemAuxPartition", UD_SYSTEM_AUX_OFFSET, UD_SYSTEM_AUX_LENGTH }, + { "SystemPartition", UD_SYSTEM_PARTITION_OFFSET, UD_SYSTEM_PARTITION_LENGTH }, + { "Partition1", UD_CONTENT_PARTITION_OFFSET, 0 }, + }; + + for (int i = 0; ok && i < (int)(sizeof(partitions) / sizeof(partitions[0])); i++) + { + LONGLONG length = partitions[i].Length != 0 + ? partitions[i].Length + : driveSize - partitions[i].Offset; + + if (!FormatPartition(disk, partitions[i].Name, partitions[i].Offset, length)) ok = FALSE; + } + + NtClose(disk); + + if (ok) Print("Format complete. Reboot, then run the exploit again to pick up the full size."); + else Print("Format did not complete."); + + return ok; +} diff --git a/src/BadStorage-XEX/BadStorage/FatxFormat.h b/src/BadStorage-XEX/BadStorage/FatxFormat.h new file mode 100644 index 0000000..e27532e --- /dev/null +++ b/src/BadStorage-XEX/BadStorage/FatxFormat.h @@ -0,0 +1,78 @@ +// On-console FATX formatting for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#pragma once + +/* + Formats the internal disk from the console itself, so the result is guaranteed to match what + the kernel expects without needing a PC. + + Why this exists + --------------- + A drive flashed by SSD Maker presents itself as an official Xbox 360 model, so the console only + ever sees that model's capacity - a 240 GB SSD flashed as a 120 GB disk gives about 107 GiB of + content partition instead of 218 GiB. Reformatting with FATXplorer on a PC fixes that, but + undoing an SSD Maker flash normally wants the undo.bin that was produced at flash time, and not + everyone still has it. + + Two things have to happen, not one + ---------------------------------- + Simply writing fresh FATX volumes is not enough. The security sector still declares the smaller + model, so on the next boot the disk authenticates, the bypass never engages, and + SataDiskInitialize sizes the partitions to the flashed identity again regardless of what the + filesystem looks like. + + So this also invalidates the security sector. Once it no longer authenticates, the bypass takes + over and lays out the partitions across the whole drive. + + That means the sequence is: + + 1. Hold LT at boot -> security sector wiped, partitions formatted for the full drive + 2. Reboot + 3. Disk no longer authenticates -> bypass runs -> full capacity available + + This is destructive. Everything on the disk is lost. + + Verifying the result + -------------------- + After formatting, connect the drive to a PC and open it in FATXplorer. If it shows a valid + volume at the full size, the layout is right. Do that before putting anything on it. +*/ + +#include "XenonExports.h" + +// FATX volume header, at offset 0 of every partition. Values are big-endian DWORDs, matching what +// a retail-formatted partition reads back as. +#define FATX_MAGIC 0x58544146 // 'X','T','A','F' +#define FATX_HEADER_SIZE 0x1000 +#define FATX_SECTORS_PER_CLUSTER 32 // 16 KB clusters, what retail uses +#define FATX_ROOT_CLUSTER 1 + +// A FAT holding this many clusters or more needs 32-bit entries instead of 16-bit. +#define FATX_FAT16_CLUSTER_LIMIT 0xFFF0 + +#define FATX_FAT_RESERVED0 0xFFFFFFF8 // media descriptor +#define FATX_FAT_END_OF_CHAIN 0xFFFFFFFF + +// Byte offset of the sector the kernel authenticates against, and how much of it it reads. +// Taken from SataDiskAuthenticateDevice itself (0x8015D9E4 onwards): it loads 0x2000 as a 64-bit +// offset, asks for 0x200 bytes via IoSynchronousFsdRequest on the PhysicalDisk device, feeds the +// result to XeKeysSetKey and compares the digest. +// +// Note this is NOT "sixteen sectors from the end of the drive". That was an unverified guess in +// the project notes, and wiping there does nothing at all - the disk keeps authenticating. +#define SECURITY_SECTOR_OFFSET 0x2000 +#define SECURITY_SECTOR_LENGTH 0x200 + +/* + Wipes the security sector and writes fresh FATX volumes sized for the whole drive. + + Returns TRUE only if every partition was written. On failure it stops at the first error rather + than leaving a partially formatted disk behind unreported. +*/ +BOOLEAN FormatInternalDisk(void); + +/* + TRUE while the left trigger is held. Used to gate the format behind a deliberate action. +*/ +BOOLEAN IsFormatRequested(void); diff --git a/src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.cpp b/src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.cpp new file mode 100644 index 0000000..715c844 --- /dev/null +++ b/src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.cpp @@ -0,0 +1,156 @@ +// Unauthenticated disk support for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#include "stdafx.h" +#include "UnauthenticatedDisk.h" +#include "BadStorage.h" + +// Directory entry names are counted, not NUL-terminated. +static BOOLEAN NameEquals(const CHAR* Buffer, USHORT Length, const CHAR* Expected) +{ + USHORT i = 0; + for (; i < Length; i++) + { + if (Expected[i] == '\0' || Buffer[i] != Expected[i]) return FALSE; + } + return Expected[i] == '\0'; +} + +// The geometry SataDiskInitialize would have written for a given partition. +// Returns FALSE for names that are not hard disk partitions. +static BOOLEAN GetPartitionGeometry(const CHAR* Name, USHORT NameLength, LONGLONG DiskSize, PLONGLONG Offset, PLONGLONG Length) +{ + if (NameEquals(Name, NameLength, "Partition0")) { *Offset = 0; *Length = DiskSize; } + else if (NameEquals(Name, NameLength, "Partition1")) { *Offset = UD_CONTENT_PARTITION_OFFSET; *Length = DiskSize - UD_CONTENT_PARTITION_OFFSET; } + else if (NameEquals(Name, NameLength, "Cache0")) { *Offset = UD_CACHE0_OFFSET; *Length = UD_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "Cache1")) { *Offset = UD_CACHE1_OFFSET; *Length = UD_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "DumpPartition")) { *Offset = UD_DUMP_OFFSET; *Length = UD_DUMP_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemURLCachePartition")) { *Offset = UD_SYSTEM_URL_CACHE_OFFSET; *Length = UD_SYSTEM_URL_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "TitleURLCachePartition")) { *Offset = UD_TITLE_URL_CACHE_OFFSET; *Length = UD_TITLE_URL_CACHE_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemExtPartition")) { *Offset = UD_SYSTEM_EXT_OFFSET; *Length = UD_SYSTEM_EXT_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemAuxPartition")) { *Offset = UD_SYSTEM_AUX_OFFSET; *Length = UD_SYSTEM_AUX_LENGTH; } + else if (NameEquals(Name, NameLength, "SystemPartition")) { *Offset = UD_SYSTEM_PARTITION_OFFSET; *Length = UD_SYSTEM_PARTITION_LENGTH; } + else if (NameEquals(Name, NameLength, "WindowsPartition")) { *Offset = 0; *Length = 0; } + else return FALSE; + + return TRUE; +} + +// Confirms a device really did become reachable by name. +static BOOLEAN DeviceResolves(const PSZ Path) +{ + PDEVICE_OBJECT device = NULL; + OBJECT_STRING str; + RtlInitAnsiString(&str, Path); + + NTSTATUS status = ObReferenceObjectByName(&str, 0, IoDeviceObjectType, NULL, (PVOID*)&device); + if (!NT_SUCCESS(status) || device == NULL) + { + Print("%s did not resolve: 0x%08X", Path, status); + return FALSE; + } + + ObDereferenceObject(device); + return TRUE; +} + +BOOLEAN InitializeUnauthenticatedDisk(ULONG* FixedCount) +{ + if (FixedCount != NULL) *FixedCount = 0; + + // The drive's own reported capacity. Zero means the console never identified a disk at all, + // which is a lower-level problem than authentication and not something this can repair. + ULONG sectors = *(PULONG)SataDiskUserAddressableSectors_Offset; + if (sectors == 0) + { + Print("No disk identified, nothing to initialise."); + return FALSE; + } + + LONGLONG diskSize = (LONGLONG)sectors * 512; + Print("Unauthenticated disk: %I64d bytes (%I64d MB).", diskSize, diskSize / (1024 * 1024)); + + // A disk smaller than the fixed system partition area cannot use this layout. + if (diskSize <= (LONGLONG)UD_CONTENT_PARTITION_OFFSET) + { + Print("Disk is too small for the standard partition layout."); + return FALSE; + } + + // The partition devices are unreachable by name while DO_DEVICE_INITIALIZING is set, so go + // through the object directory instead. + PVOID directory = NULL; + OBJECT_STRING directoryStr; + RtlInitAnsiString(&directoryStr, HARDDISK0_DIRECTORY_PATH); + + NTSTATUS status = ObReferenceObjectByName(&directoryStr, 0, ObDirectoryObjectType, NULL, &directory); + if (!NT_SUCCESS(status) || directory == NULL) + { + Print("ObReferenceObjectByName failed on %s: 0x%08X", HARDDISK0_DIRECTORY_PATH, status); + return FALSE; + } + + ULONG fixed = 0; + PULONG buckets = (PULONG)directory; + + for (ULONG bucket = 0; bucket < OBJECT_DIRECTORY_BUCKETS; bucket++) + { + ULONG entry = buckets[bucket]; + + // Bounded so a corrupted chain cannot spin forever. + for (ULONG depth = 0; entry != 0 && depth < 32; depth++) + { + USHORT nameLength = *(PUSHORT)(entry + OBJDIR_ENTRY_NAMELEN); + PCHAR nameBuffer = *(PCHAR*)(entry + OBJDIR_ENTRY_NAMEBUF); + ULONG next = *(PULONG)(entry + OBJDIR_ENTRY_NEXT); + + if (nameLength != 0 && nameLength < 64 && nameBuffer != NULL) + { + PDEVICE_OBJECT device = (PDEVICE_OBJECT)(entry + OBJDIR_ENTRY_TO_OBJECT); + + // Only touch devices still stuck mid-initialisation. On an authenticated disk + // there are none, so this whole loop does nothing. + if ((device->Flags & DO_DEVICE_INITIALIZING_360) == DO_DEVICE_INITIALIZING_360) + { + LONGLONG offset = 0, length = 0; + if (GetPartitionGeometry(nameBuffer, nameLength, diskSize, &offset, &length)) + { + PPARTITION_INFORMATION info = &((PSATA_DISK_EXTENSION)device->DeviceExtension)->PartitionInformation; + info->StartingOffset.QuadPart = offset; + info->PartitionLength.QuadPart = length; + + // The kernel's own final step for each device. Its instruction is + // "rlwinm rX, rX, 0, 28, 26", a wrapped mask whose only effect is to + // clear this bit. + device->Flags &= ~DO_DEVICE_INITIALIZING_360; + + fixed++; + Print("Initialised %.*s: offset %I64d, length %I64d", nameLength, nameBuffer, offset, length); + } + } + } + + entry = next; + } + } + + ObDereferenceObject(directory); + + if (FixedCount != NULL) *FixedCount = fixed; + + if (fixed == 0) + { + Print("No uninitialised partition devices found."); + return FALSE; + } + + // These two are what the dashboard actually needs, so they decide success. + if (!DeviceResolves(PARTITION_0_PATH) || !DeviceResolves(PARTITION_1_PATH)) + { + Print("Partitions were written but still do not resolve."); + return FALSE; + } + + Print("Initialised %u partition devices.", fixed); + return TRUE; +} diff --git a/src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.h b/src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.h new file mode 100644 index 0000000..fb73240 --- /dev/null +++ b/src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.h @@ -0,0 +1,100 @@ +// Unauthenticated disk support for Bad Storage. +// Copyright © 2026. Licensed under the same MIT terms as Bad Storage. + +#pragma once + +/* + Lets a disk that never passed the console's security-sector check be used as internal storage. + + Background + ---------- + During boot, SataDiskInitialize does two separate things, in this order: + + 1. BEFORE the authentication gate, it creates all twelve hard disk device objects + (PhysicalDisk, Partition0, Partition1, Cache0/1, DumpPartition, SystemPartition, ...) + by calling SataDiskCreateDevice -> IoCreateDevice. + + 2. AFTER the gate, it fills in each device's PartitionInformation and clears + DO_DEVICE_INITIALIZING on it. + + When SataDiskAuthenticateDevice fails, the gate branches straight to the function epilogue, + so step 2 never runs. The device objects therefore exist, but with a zeroed + PartitionInformation and DO_DEVICE_INITIALIZING still set. A device in that state makes + ObReferenceObjectByName return STATUS_NO_SUCH_DEVICE (0xC000000E) - note that this is *not* + STATUS_OBJECT_NAME_NOT_FOUND: the name resolves fine, it is the device that gets refused. + + PhysicalDisk is the one exception and stays usable, because it is completed before the gate. + + Approach + -------- + Patching SataDiskAuthenticateDevice to return TRUE does not help. That gate is evaluated once, + early in boot, long before any exploit payload runs, and there is no warm reboot that would + re-run it while keeping patches in memory. (XeUnshackle already applies exactly that patch as + part of its Freeboot set, and it still is not enough.) + + Instead, this finishes the initialisation the gate skipped: reach the device objects by walking + the \Device\Harddisk0 object directory - they cannot be reached by name, precisely because of + the flag - write each partition's geometry, and clear DO_DEVICE_INITIALIZING. + + Everything written here is what SataDiskInitialize's own post-gate block computes; it was + recovered by disassembling that block instruction by instruction. Two independent checks say + the values are right: the resulting layout is perfectly contiguous, and Partition1's offset + comes out as 0x130EB0000, the same constant Bad Storage already hardcodes. + + Scope + ----- + This addresses authentication only. It requires a disk the console can already talk to and + identify, i.e. one that answers the ATA commands and reports its sector count. A drive whose + controller the console cannot negotiate with at all leaves SataDiskUserAddressableSectors at + zero, and there is nothing here to complete. +*/ + +#include "XenonExports.h" + +// On this kernel DO_DEVICE_INITIALIZING is 0x10, not the 0x80 it is on Windows. +// From IoCreateDevice at 0x8006B00C: "li r11, 0x10" then "stw r11, 0x14(r10)". +#define DO_DEVICE_INITIALIZING_360 0x10 + +// The object directory is a 13-bucket hash table of chained entries. +// From NtQueryDirectoryObject's own walk at 0x8008A22C-0x8008A264. +#define OBJECT_DIRECTORY_BUCKETS 13 + +// Offsets within a directory entry, same source. +#define OBJDIR_ENTRY_NEXT 0x00 +#define OBJDIR_ENTRY_NAMELEN 0x08 +#define OBJDIR_ENTRY_NAMEBUF 0x0C + +// Distance from a directory entry to the object body it refers to. +// Measured on hardware rather than assumed: the PhysicalDisk entry sat at 0x3A0A7C88 while +// ObReferenceObjectByName returned 0x3A0A7CA8 for the same name. +#define OBJDIR_ENTRY_TO_OBJECT 0x20 + +// Byte offset of the content partition, and the fixed geometry of the system partitions. +// All derived from SataDiskInitialize 0x8015DF28-0x8015E0D0. +#define UD_CACHE0_OFFSET 0x80000ULL +#define UD_CACHE1_OFFSET 0x80080000ULL +#define UD_CACHE_LENGTH 0x80000000ULL +#define UD_DUMP_OFFSET 0x100080000ULL +#define UD_DUMP_LENGTH 0x20E30000ULL +#define UD_SYSTEM_URL_CACHE_OFFSET 0x100080000ULL +#define UD_SYSTEM_URL_CACHE_LENGTH 0x6000000ULL +#define UD_TITLE_URL_CACHE_OFFSET 0x106080000ULL +#define UD_TITLE_URL_CACHE_LENGTH 0x2000000ULL +#define UD_SYSTEM_EXT_OFFSET 0x10C080000ULL +#define UD_SYSTEM_EXT_LENGTH 0xCE30000ULL +#define UD_SYSTEM_AUX_OFFSET 0x118EB0000ULL +#define UD_SYSTEM_AUX_LENGTH 0x8000000ULL +#define UD_SYSTEM_PARTITION_OFFSET 0x120EB0000ULL +#define UD_SYSTEM_PARTITION_LENGTH 0x10000000ULL +#define UD_CONTENT_PARTITION_OFFSET 0x130EB0000ULL + +#define HARDDISK0_DIRECTORY_PATH "\\Device\\Harddisk0" + +/* + Completes the partition device objects the authentication gate left half-initialised. + + Returns TRUE only if Partition0 and Partition1 both resolve by name afterwards, which is the + real proof that the devices became usable. Safe to call when nothing needs fixing: devices that + are already initialised are skipped, so a second call is a no-op. +*/ +BOOLEAN InitializeUnauthenticatedDisk(ULONG* FixedCount); diff --git a/src/BadStorage-XEX/BadStorage/XenonExports.h b/src/BadStorage-XEX/BadStorage/XenonExports.h index 80d1028..6fcbbef 100644 --- a/src/BadStorage-XEX/BadStorage/XenonExports.h +++ b/src/BadStorage-XEX/BadStorage/XenonExports.h @@ -765,6 +765,7 @@ EXTERN_C extern PXBOX_KRNL_VERSION XboxKrnlVersion; extern PXBOX_HARDWARE_INFO XboxHardwareInfo; extern PVOID IoDeviceObjectType; + extern PVOID ObDirectoryObjectType; HRESULT WINAPI