From 72124eb74772c0ba8726d8200951d136314c38b3 Mon Sep 17 00:00:00 2001 From: Angelpro09_Dev Date: Sat, 8 Aug 2026 15:37:48 +0200 Subject: [PATCH 1/4] Add unauthenticated disk support Lets a disk that never passed the security-sector check be used as internal storage on retail 17559. SataDiskInitialize creates all twelve hard disk device objects before the authentication gate, and only fills in their geometry and clears DO_DEVICE_INITIALIZING after it. When authentication fails the gate branches straight to the epilogue, so the devices exist but stay half-initialised -- which is why ObReferenceObjectByName answers STATUS_NO_SUCH_DEVICE for them while PhysicalDisk, completed before the gate, keeps working. Patching SataDiskAuthenticateDevice cannot help: the gate is evaluated once at boot, and XeUnshackle already applies that patch anyway. So instead of fighting authentication, finish the initialisation it skipped -- walk the \Device\Harddisk0 object directory to reach the devices, write each partition's geometry, clear the flag, and announce the disk. The geometry is what SataDiskInitialize's own post-gate block computes, recovered by disassembling it. The resulting layout is contiguous and Partition1 lands on 0x130EB0000, the offset Bad Storage already hardcodes. Tested on real hardware with a Kingston SA400S37 240GB. --- README.md | 17 ++ src/BadStorage-DLL/BadStorage/BadStorage.cpp | 36 +++- src/BadStorage-DLL/BadStorage/BadStorage.h | 5 +- .../BadStorage/BadStorage.vcxproj | 2 + .../BadStorage/UnauthenticatedDisk.cpp | 156 ++++++++++++++++++ .../BadStorage/UnauthenticatedDisk.h | 100 +++++++++++ src/BadStorage-DLL/BadStorage/XenonExports.h | 1 + src/BadStorage-XEX/BadStorage/BadStorage.cpp | 33 +++- src/BadStorage-XEX/BadStorage/BadStorage.h | 5 +- .../BadStorage/BadStorage.vcxproj | 2 + .../BadStorage/UnauthenticatedDisk.cpp | 156 ++++++++++++++++++ .../BadStorage/UnauthenticatedDisk.h | 100 +++++++++++ src/BadStorage-XEX/BadStorage/XenonExports.h | 1 + 13 files changed, 607 insertions(+), 7 deletions(-) create mode 100644 src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.cpp create mode 100644 src/BadStorage-DLL/BadStorage/UnauthenticatedDisk.h create mode 100644 src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.cpp create mode 100644 src/BadStorage-XEX/BadStorage/UnauthenticatedDisk.h diff --git a/README.md b/README.md index cadc32b..46527a8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,23 @@ 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). + +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..47074f3 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.cpp +++ b/src/BadStorage-DLL/BadStorage/BadStorage.cpp @@ -3,6 +3,7 @@ #include "stdafx.h" #include "BadStorage.h" +#include "UnauthenticatedDisk.h" VOID Print(const PCHAR Format, ...) { @@ -149,15 +150,44 @@ EXTERN_C BOOLEAN Execute(PBOOLEAN IsRetailFormatted) //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); + 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"BadStorage: Unauthenticated disk mounted.", 0); + if (IsRetailFormatted != NULL) *IsRetailFormatted = TRUE; + return TRUE; } PDEVICE_OBJECT phyDiskDeviceObject = NULL; 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..341c7c9 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj +++ b/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj @@ -158,6 +158,7 @@ + @@ -166,6 +167,7 @@ Create + 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..e8b2211 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.cpp +++ b/src/BadStorage-XEX/BadStorage/BadStorage.cpp @@ -3,6 +3,7 @@ #include "stdafx.h" #include "BadStorage.h" +#include "UnauthenticatedDisk.h" VOID Print(const PCHAR Format, ...) { @@ -141,14 +142,42 @@ VOID __cdecl main() //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); + 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"BadStorage: Unauthenticated disk mounted.", 0); return; } 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..3250b6a 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj +++ b/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj @@ -141,6 +141,7 @@ + @@ -149,6 +150,7 @@ Create + 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 From 38917e5ec6433b4284c139991608c0e731b37efb Mon Sep 17 00:00:00 2001 From: Angelpro09_Dev Date: Sat, 8 Aug 2026 15:37:48 +0200 Subject: [PATCH 2/4] Report bypass status instead of a formatting error When the unauthenticated path fails, the disk is not necessarily formatted wrongly, so telling the user to reflash with FATXplorer was misleading. Say what actually happened instead, and keep that advice only where it still applies. On success, state plainly that the bypass ran and the disk mounted, so it is obvious from the console that the feature is doing something. --- src/BadStorage-DLL/BadStorage/BadStorage.cpp | 4 ++-- src/BadStorage-XEX/BadStorage/BadStorage.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BadStorage-DLL/BadStorage/BadStorage.cpp b/src/BadStorage-DLL/BadStorage/BadStorage.cpp index 47074f3..b83cf96 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.cpp +++ b/src/BadStorage-DLL/BadStorage/BadStorage.cpp @@ -164,7 +164,7 @@ EXTERN_C BOOLEAN Execute(PBOOLEAN IsRetailFormatted) ULONG initialisedCount = 0; if (!InitializeUnauthenticatedDisk(&initialisedCount)) { - 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; } @@ -185,7 +185,7 @@ EXTERN_C BOOLEAN Execute(PBOOLEAN IsRetailFormatted) //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"BadStorage: Unauthenticated disk mounted.", 0); + 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; } diff --git a/src/BadStorage-XEX/BadStorage/BadStorage.cpp b/src/BadStorage-XEX/BadStorage/BadStorage.cpp index e8b2211..aed48e0 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.cpp +++ b/src/BadStorage-XEX/BadStorage/BadStorage.cpp @@ -156,7 +156,7 @@ VOID __cdecl main() ULONG initialisedCount = 0; if (!InitializeUnauthenticatedDisk(&initialisedCount)) { - 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; } @@ -177,7 +177,7 @@ VOID __cdecl main() //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"BadStorage: Unauthenticated disk mounted.", 0); + XNotifyQueueUI(XNOTIFYUI_TYPE_AVOID_REVIEW, XUSER_INDEX_ANY, XNOTIFYUI_PRIORITY_HIGH, L"Bad Storage: Auth bypass active - internal disk mounted.", 0); return; } From d0ea855793dbd725b12a66fdeca020af14bc1fcf Mon Sep 17 00:00:00 2001 From: Angelpro09_Dev Date: Sat, 8 Aug 2026 15:37:49 +0200 Subject: [PATCH 3/4] Add on-console formatting, held on LT Recovers a drive already flashed by SSD Maker without needing its undo.bin. Such a drive authenticates as whatever official model it was flashed as, so the console only ever sees that model's capacity - a 240GB SSD flashed as 120GB yields 107GiB of content partition instead of 218GiB. Writing fresh filesystems alone would not help, because the security sector still declares the flashed model and SataDiskInitialize sizes the partitions from it on every boot. So this invalidates that sector as well, after which the disk stops authenticating and the bypass lays the partitions out across the whole drive. The sector is at byte offset 0x2000 on PhysicalDisk, read straight out of SataDiskAuthenticateDevice. It is not sixteen sectors from the end of the drive - that figure appears in some notes, and wiping there changes nothing. Also reports the wasted space when it detects a flashed drive it is not being asked to reformat, instead of silently skipping it. Confirmed on hardware: 107GiB -> 218GiB on a Kingston SA400S37 240GB. --- src/BadStorage-DLL/BadStorage/BadStorage.cpp | 45 +++- .../BadStorage/BadStorage.vcxproj | 2 + src/BadStorage-DLL/BadStorage/FatxFormat.cpp | 212 ++++++++++++++++++ src/BadStorage-DLL/BadStorage/FatxFormat.h | 78 +++++++ src/BadStorage-XEX/BadStorage/BadStorage.cpp | 47 +++- .../BadStorage/BadStorage.vcxproj | 2 + src/BadStorage-XEX/BadStorage/FatxFormat.cpp | 212 ++++++++++++++++++ src/BadStorage-XEX/BadStorage/FatxFormat.h | 78 +++++++ 8 files changed, 673 insertions(+), 3 deletions(-) create mode 100644 src/BadStorage-DLL/BadStorage/FatxFormat.cpp create mode 100644 src/BadStorage-DLL/BadStorage/FatxFormat.h create mode 100644 src/BadStorage-XEX/BadStorage/FatxFormat.cpp create mode 100644 src/BadStorage-XEX/BadStorage/FatxFormat.h diff --git a/src/BadStorage-DLL/BadStorage/BadStorage.cpp b/src/BadStorage-DLL/BadStorage/BadStorage.cpp index b83cf96..17b3bb6 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.cpp +++ b/src/BadStorage-DLL/BadStorage/BadStorage.cpp @@ -4,6 +4,7 @@ #include "stdafx.h" #include "BadStorage.h" #include "UnauthenticatedDisk.h" +#include "FatxFormat.h" VOID Print(const PCHAR Format, ...) { @@ -145,6 +146,25 @@ 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. @@ -238,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.vcxproj b/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj index 341c7c9..55fa8b7 100644 --- a/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj +++ b/src/BadStorage-DLL/BadStorage/BadStorage.vcxproj @@ -159,6 +159,7 @@ + @@ -168,6 +169,7 @@ + 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-XEX/BadStorage/BadStorage.cpp b/src/BadStorage-XEX/BadStorage/BadStorage.cpp index aed48e0..75c1eff 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.cpp +++ b/src/BadStorage-XEX/BadStorage/BadStorage.cpp @@ -4,6 +4,7 @@ #include "stdafx.h" #include "BadStorage.h" #include "UnauthenticatedDisk.h" +#include "FatxFormat.h" VOID Print(const PCHAR Format, ...) { @@ -137,6 +138,25 @@ 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. @@ -228,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.vcxproj b/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj index 3250b6a..279a3e7 100644 --- a/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj +++ b/src/BadStorage-XEX/BadStorage/BadStorage.vcxproj @@ -142,6 +142,7 @@ + @@ -151,6 +152,7 @@ + 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); From 24719e7aa98c4c636212f1beee07641822838934 Mon Sep 17 00:00:00 2001 From: Angelpro09_Dev Date: Sat, 8 Aug 2026 15:37:49 +0200 Subject: [PATCH 4/4] Document the real capacity range and the HDD entry point conflict Testing now covers a 240GB SSD plus older mechanical drives at 300GB, 500GB and several sizes in between, up to 1TB, all mounting at full capacity. That also removes the ~500GB ceiling the notes previously claimed. It never applied here: the partition map is computed from the drive's reported sector count, not from a declared model size, so there is nothing bounding it the way retail is bounded. Also record that a disk set up this way cannot serve an entry point that reads from the internal drive, such as BadAvatarHDD. The requirements are mutually exclusive - the entry point has to be readable in a stock state, and an unauthenticated disk has no published filesystem until the bypass has run, which itself needs an exploit already running. USB-based entry points are unaffected. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 46527a8..34f3a86 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,17 @@ It works by finishing the partition device initialisation that `SataDiskInitiali 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.