Skip to content

Instantly share code, notes, and snippets.

@Ernegien
Last active November 25, 2021 05:31
Show Gist options
  • Save Ernegien/bef054a43213c52c2bef8d94bf9f51cb to your computer and use it in GitHub Desktop.
Save Ernegien/bef054a43213c52c2bef8d94bf9f51cb to your computer and use it in GitHub Desktop.
Xbox DVD Authentication NXDK Testbed
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <windows.h>
#include <hal/debug.h>
#include <hal/video.h>
#include <hal/xbox.h>
#include <hal/fileio.h>
#include <nxdk/mount.h>
#include <xboxkrnl/xboxkrnl.h>
#include <SDL.h>
// http://www.ioctls.net/
#define IOCTL_SCSI_PASS_THROUGH_DIRECT 0x4D014
#define SCSI_IOCTL_DATA_OUT 0 // write data
#define SCSI_IOCTL_DATA_IN 1 // read data
#define SCSIOP_MODE_SELECT10 0x55 // write page
#define SCSIOP_MODE_SENSE10 0x5A // read page
#define SCSIOP_READ_DVD_STRUCTURE 0xAD // layout + challenge response
#define MODE_PAGE_XBOX_SECURITY 0x3E // mode select/sense page code
SDL_GameController *controller = NULL;
// https://en.wikipedia.org/wiki/SCSI_CDB
// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/shared/scsi.h
// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/shared/ntddscsi.h
// https://github.com/microsoft/Windows-driver-samples/blob/master/storage/miniports/storahci/src/common.c#L514
// https://github.com/microsoft/Windows-driver-samples/blob/master/storage/class/cdrom/src/init.c
// https://github.com/microsoft/Windows-driver-samples/blob/master/storage/class/cdrom/src/ioctl.c
// atapi specifications - https://www.bswd.com/sff8020i.pdf
// ahci?? - https://wiki.osdev.org/AHCI
// https://github.com/XboxDev/cromwell/blob/master/drivers/pci/pci.c#L405
// qemu patch attempt at atapi passthrough
// https://yhbt.net/lore/all/20090705081533.GA32124@lst.de/T/
// https://github.com/brendank310/meta-qemu-1.4-oxt/blob/master/recipes-openxt/qemu-dm/files/atapi-pass-through.patch
// TODO: standard scsi struct sizes appear to be correct, still need to dig into any xbox-specific differences
// NOTE: integers stored in big endian format for scsi data structs (not passthrough?), be sure to use endian enforcement methods in xemu!
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddscsi/ni-ntddscsi-ioctl_scsi_pass_through_direct
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddscsi/ns-ntddscsi-_scsi_pass_through_direct
typedef struct _SCSI_PASS_THROUGH_DIRECT {
USHORT Length;
UCHAR ScsiStatus;
UCHAR PathId;
UCHAR TargetId;
UCHAR Lun;
UCHAR CdbLength;
UCHAR SenseInfoLength;
UCHAR DataIn;
ULONG DataTransferLength;
ULONG TimeOutValue;
PVOID DataBuffer;
ULONG SenseInfoOffset;
UCHAR Cdb[16];
} SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT;
_Static_assert(sizeof(SCSI_PASS_THROUGH_DIRECT) == 44, "sizeof(SCSI_PASS_THROUGH_DIRECT) != 44");
#pragma pack(push, scsi, 1)
// scsi command descriptor blocks
typedef union _CDB {
// used for writing to the dvd security page
struct _MODE_SELECT10 {
UCHAR OperationCode; // 0x55 - SCSIOP_MODE_SELECT10
UCHAR SPBit : 1;
UCHAR Reserved1 : 3;
UCHAR PFBit : 1;
UCHAR LogicalUnitNumber : 3;
UCHAR Reserved2[5];
UCHAR ParameterListLength[2];
UCHAR Control;
} MODE_SELECT10;
// used for reading from the dvd security page
struct _MODE_SENSE10 {
UCHAR OperationCode; // 0x5A - SCSIOP_MODE_SENSE10
UCHAR Reserved1 : 3;
UCHAR Dbd : 1;
UCHAR Reserved2 : 1;
UCHAR LogicalUnitNumber : 3;
UCHAR PageCode : 6;
UCHAR Pc : 2;
UCHAR Reserved3[4];
UCHAR AllocationLength[2];
UCHAR Control;
} MODE_SENSE10;
// used for reading the dvd structure and other general security-related data
struct _READ_DVD_STRUCTURE {
UCHAR OperationCode; // 0xAD - SCSIOP_READ_DVD_STRUCTURE
UCHAR Reserved1 : 5; // offset 0x1
UCHAR Lun : 3;
UCHAR RMDBlockNumber[4]; // offset 0x2
UCHAR LayerNumber; // offset 0x6
UCHAR Format; // offset 0x7
UCHAR AllocationLength[2]; // offset 0x8
UCHAR Reserved3 : 6; // offset 0x10
UCHAR AGID : 2;
UCHAR Control; // offset 0x11
} READ_DVD_STRUCTURE;
} CDB, *PCDB;
typedef struct _MODE_PARAMETER_HEADER10 {
UCHAR ModeDataLength[2];
UCHAR MediumType;
UCHAR DeviceSpecificParameter;
UCHAR Reserved[2];
UCHAR BlockDescriptorLength[2];
} MODE_PARAMETER_HEADER10, *PMODE_PARAMETER_HEADER10;
_Static_assert(sizeof(MODE_PARAMETER_HEADER10) == 8, "sizeof(MODE_PARAMETER_HEADER10) != 8");
// https://xboxdevwiki.net/DVD_Drive
typedef struct _XBOX_DVD_SECURITY_PAGE {
UCHAR PageCode;
UCHAR PageLength;
UCHAR Partition; // 0 - video, 1 - xbox
UCHAR Unk1; // needs to be 1?
UCHAR Authenticated; // set to 1 after first challenge
UCHAR BookTypeAndVersion; // should be 0xD1
UCHAR Unk2; // should be 1?
UCHAR ChallengeSalt; // is this an ID or salt? xboxdevwiki says id
ULONG ChallengeValue;
ULONG ResponseValue;
ULONG Unk3;
} XBOX_DVD_SECURITY_PAGE, *PXBOX_DVD_SECURITY_PAGE;
_Static_assert(sizeof(XBOX_DVD_SECURITY_PAGE) == 20, "sizeof(XBOX_DVD_SECURITY_PAGE) != 20");
// select/sense struct
typedef struct _XBOX_DVD_SECURITY {
MODE_PARAMETER_HEADER10 header;
XBOX_DVD_SECURITY_PAGE page;
} XBOX_DVD_SECURITY, *PXBOX_DVD_SECURITY;
_Static_assert(sizeof(XBOX_DVD_SECURITY) == 28, "sizeof(XBOX_DVD_SECURITY) != 28");
// contains regular dvd structure info as well as additional xbox-specific stuff like sigs and challenges
typedef struct _XBOX_DVD_LAYOUT {
UCHAR data[1636];
} XBOX_DVD_LAYOUT, *PXBOX_DVD_LAYOUT;
_Static_assert(sizeof(XBOX_DVD_LAYOUT) == 1636, "sizeof(XBOX_DVD_LAYOUT) != 1636");
// holds challenge response info
typedef struct _XBOX_DVD_CHALLENGE {
uint8_t type; // Xbox only processes type 1?
uint8_t unk; // salt?
uint32_t value; // challenge value
uint8_t reserved; // unused, always zero?
uint32_t response;
} XBOX_DVD_CHALLENGE, *PXBOX_DVD_CHALLENGE;
_Static_assert(sizeof(XBOX_DVD_CHALLENGE) == 11, "sizeof(XBOX_DVD_CHALLENGE) != 11");
#pragma pack(pop, scsi)
uint16_t bswap16(uint16_t val)
{
return (val >> 8) | (val << 8);
}
void reboot() {
HalWriteSMBusValue(0x20, 2, 0, 1);
}
void assertOrExit(bool isExpected, const char *format, ...);
int getDvdTrayState() {
DWORD state;
HalReadSMBusValue(0x20, 0x3, 0, &state);
return state;
}
void ejectDvdTray(bool confirm) {
HalWriteSMBusValue(0x20, 0xC, 0, 0);
Sleep(250);
if (confirm) {
// wait 5 seconds or until tray state is marked open
for (int i = 0; i < 50; i++)
{
if (getDvdTrayState() == 0x10)
return;
Sleep(100);
}
assertOrExit(false, "Failed to eject DVD tray! (State 0x%X)\n", getDvdTrayState());
}
}
void injectDvdTray(bool confirm) {
HalWriteSMBusValue(0x20, 0xC, 0, 1);
Sleep(250);
if (confirm) {
// wait 30 seconds or until tray state is marked closed with media detected
for (int i = 0; i < 300; i++)
{
if (getDvdTrayState() == 0x60)
return;
Sleep(100);
}
assertOrExit(false, "Failed to detect DVD media! (State 0x%X)\n", getDvdTrayState());
}
}
void waitAndExit() {
ejectDvdTray(false);
debugPrint("\nRemove media and press START to restart...");
do { SDL_GameControllerUpdate(); }
while (!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START));
injectDvdTray(false);
reboot();
while (1);
}
void assertOrExit(bool isExpected, const char *format, ...)
{
char buffer[512];
unsigned short len;
va_list argList;
va_start(argList, format);
vsprintf(buffer, format, argList);
va_end(argList);
if (!isExpected) {
debugPrint("%s", buffer);
return waitAndExit();
}
}
void writeFileBytes(const char* name, const uint8_t* data, uint32_t dataOffset, uint32_t dataLength)
{
debugPrint("Writing %d bytes to \"%s\"\n", dataLength, name);
// create the file
HANDLE handle = CreateFile(name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
assertOrExit(handle != INVALID_HANDLE_VALUE, "File creation failure (0x%08X)\n", GetLastError());
// write to the file
DWORD bytesWritten;
NTSTATUS status = WriteFile(handle, data + dataOffset, dataLength, &bytesWritten, NULL);
NtClose(handle);
assertOrExit(bytesWritten == dataLength, "File write failure (0x%08X)\n", status);
}
// preps the scsi command passthrough and data structs and returns a pointer to the cdb to be filled out by the caller
PCDB prepScsiCmd(PSCSI_PASS_THROUGH_DIRECT scsi, int transferType, PVOID* data, size_t dataLen)
{
ZeroMemory(scsi, sizeof(SCSI_PASS_THROUGH_DIRECT));
ZeroMemory(data, dataLen);
scsi->Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
scsi->DataIn = transferType;
scsi->DataBuffer = data;
scsi->DataTransferLength = dataLen;
return (PCDB)&scsi->Cdb;
}
// xemu atapi.c cmd_mode_sense 0x5A security page code 0x3E
NTSTATUS getDvdSecurity(PDEVICE_OBJECT cdrom, OUT PXBOX_DVD_SECURITY security)
{
SCSI_PASS_THROUGH_DIRECT scsi_cmd;
// prep the scsi command
PCDB read_security_cdb = prepScsiCmd(&scsi_cmd, SCSI_IOCTL_DATA_IN, (PVOID*)security, sizeof(XBOX_DVD_SECURITY));
read_security_cdb->MODE_SENSE10.OperationCode = SCSIOP_MODE_SENSE10;
read_security_cdb->MODE_SENSE10.PageCode = MODE_PAGE_XBOX_SECURITY;
*(uint16_t*)&read_security_cdb->MODE_SENSE10.AllocationLength = bswap16(sizeof(XBOX_DVD_SECURITY)); // big endian (assuming this is ran on x86)
// get dvd security info
return IoSynchronousDeviceIoControlRequest(IOCTL_SCSI_PASS_THROUGH_DIRECT,
cdrom, &scsi_cmd, sizeof(SCSI_PASS_THROUGH_DIRECT), NULL, 0, NULL, FALSE);
}
// xemu atapi.c cmd_mode_select 0x55 security page code 0x3E
NTSTATUS setDvdSecurity(PDEVICE_OBJECT cdrom, PXBOX_DVD_SECURITY_PAGE security_page)
{
SCSI_PASS_THROUGH_DIRECT scsi_cmd;
XBOX_DVD_SECURITY security;
// prep the scsi command
PCDB write_security_cdb = prepScsiCmd(&scsi_cmd, SCSI_IOCTL_DATA_OUT, (PVOID*)&security, sizeof(XBOX_DVD_SECURITY));
*(uint16_t*)&security.header.ModeDataLength = bswap16(sizeof(XBOX_DVD_SECURITY) - 2); // big endian (assuming this is ran on x86)
write_security_cdb->MODE_SELECT10.OperationCode = SCSIOP_MODE_SELECT10;
*(uint16_t*)&write_security_cdb->MODE_SELECT10.ParameterListLength = bswap16(sizeof(XBOX_DVD_SECURITY)); // big endian (assuming this is ran on x86) TODO: wtf is qemu doing with this? figure out where sent page data is going!
security_page->PageCode = MODE_PAGE_XBOX_SECURITY;
security_page->PageLength = sizeof(XBOX_DVD_SECURITY_PAGE) - 2; // NOTE: doesn't need to be byte-swapped because it's not a USHORT
memcpy(&security.page, security_page, sizeof(XBOX_DVD_SECURITY_PAGE));
// TEMP!
//memset(&security.page, 0x11, sizeof(XBOX_DVD_SECURITY_PAGE));
// set dvd security info - currently getting STATUS_DATA_OVERRUN due to xemu side needing correct implementation
return IoSynchronousDeviceIoControlRequest(IOCTL_SCSI_PASS_THROUGH_DIRECT,
cdrom, &scsi_cmd, sizeof(SCSI_PASS_THROUGH_DIRECT), NULL, 0, NULL, FALSE);
}
// xemu atapi.c cmd_read_dvd_structure 0xAD
NTSTATUS getDvdLayout(PDEVICE_OBJECT cdrom, OUT PXBOX_DVD_LAYOUT layout)
{
SCSI_PASS_THROUGH_DIRECT scsi_cmd;
// prep the scsi command
PCDB read_layout_cdb = prepScsiCmd(&scsi_cmd, SCSI_IOCTL_DATA_IN, (PVOID*)layout, sizeof(XBOX_DVD_LAYOUT));
read_layout_cdb->READ_DVD_STRUCTURE.OperationCode = SCSIOP_READ_DVD_STRUCTURE;
*(uint32_t*)&read_layout_cdb->READ_DVD_STRUCTURE.RMDBlockNumber = 0xFFFD02FF;
read_layout_cdb->READ_DVD_STRUCTURE.LayerNumber = 0xFE;
*(uint16_t*)&read_layout_cdb->READ_DVD_STRUCTURE.AllocationLength = bswap16(sizeof(XBOX_DVD_LAYOUT)); // big endian (assuming this is ran on x86)
read_layout_cdb->READ_DVD_STRUCTURE.Control = 0xC0;
// request dvd layout info
return IoSynchronousDeviceIoControlRequest(IOCTL_SCSI_PASS_THROUGH_DIRECT,
cdrom, &scsi_cmd, sizeof(SCSI_PASS_THROUGH_DIRECT), NULL, 0, NULL, FALSE);
}
void verifyChallengeResponse(PDEVICE_OBJECT cdrom, PXBOX_DVD_LAYOUT layout, PXBOX_DVD_CHALLENGE challenge, bool first, bool last)
{
NTSTATUS status;
XBOX_DVD_SECURITY security; // response received
XBOX_DVD_SECURITY_PAGE security_page; // challenge sent
security_page.BookTypeAndVersion = *(uint8_t*)((PUCHAR)layout + 4);
security_page.ChallengeSalt = challenge->unk;
security_page.ChallengeValue = challenge->value;
security_page.Unk1 = 1;
security_page.Unk2 = 1;
if (!first)
{
security_page.Authenticated = 1;
}
if (last)
{
security_page.Partition = 1; // switch to xbox partition
}
debugPrint("Verifying 0x%08X:%02X challenge responds 0x%08X.\n", challenge->value, challenge->unk, challenge->response);
status = setDvdSecurity(cdrom, &security_page);
assertOrExit(status == STATUS_SUCCESS, "Failed to issue challenge! (0x%08X)\n", status); // TODO: overrun *might* be fine here?
status = getDvdSecurity(cdrom, &security);
assertOrExit(status == STATUS_SUCCESS, "Failed to read challenge response! (0x%08X)\n", status);
assertOrExit(security.page.Authenticated == 1 && security.page.ResponseValue == challenge->response,
"Invalid challenge response! (0x%08X)\n", security.page.ResponseValue);
}
void init_sdl() {
int status;
XVideoSetMode(640, 480, 32, REFRESH_DEFAULT);
assertOrExit(SDL_Init(SDL_INIT_GAMECONTROLLER) == STATUS_SUCCESS,
"Failed to initialize SDL input\n");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
if (SDL_IsGameController(i)) {
controller = SDL_GameControllerOpen(i);
debugPrint("Using first detected controller for input\n");
break;
}
}
assertOrExit(controller != NULL, "Couldn't find any joysticks\n");
}
// https://xboxdevwiki.net/DVD_Drive
int main(void) {
ANSI_STRING cdrom_name;
PDEVICE_OBJECT cdrom_device;
NTSTATUS status;
bool success;
SCSI_PASS_THROUGH_DIRECT scsi_cmd;
XBOX_DVD_SECURITY dvd_security;
XBOX_DVD_LAYOUT dvd_layout;
uint8_t sha_ctx[116];
uint8_t sha_hash[20];
uint8_t rc4_ctx[258];
init_sdl();
// dvd eject/inject logic for real hardware
if (HalDiskModelNumber.Buffer[0] != 'Q' ) { // starts with "QEMU" for now
// disable reset on eject
HalWriteSMBusValue(0x20, 0x19, 0, 1);
Sleep(250);
debugPrint("Ejecting DVD tray...");
ejectDvdTray(true);
debugPrint("done!\n");
debugPrint("Press A when media is inserted...");
do { SDL_GameControllerUpdate(); }
while (!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A));
debugPrint("done!\n");
debugPrint("Injecting DVD tray...");
injectDvdTray(true);
debugPrint("done!\n");
}
// prep filesystem for dumps
success = nxMountDrive('C', "\\Device\\Harddisk0\\Partition2");
assertOrExit(success, "Failed to mount C drive! (0x%08X)\n", GetLastError());
CreateDirectory("C:\\backup", NULL);
// get the cdrom device object
RtlInitAnsiString(&cdrom_name, "\\Device\\CdRom0");
status = ObReferenceObjectByName(&cdrom_name, 0, &IoDeviceObjectType, 0, (PVOID*)&cdrom_device);
assertOrExit(status == STATUS_SUCCESS, "Failed to obtain CDROM device object! (0x%08X)\n", status);
// get the initial security info
status = getDvdSecurity(cdrom_device, &dvd_security);
assertOrExit(status == STATUS_SUCCESS, "Failed to read DVD security! (0x%08X)\n", status);
writeFileBytes("C:\\backup\\dvd_security.bin", (uint8_t*)&dvd_security, 0, sizeof(XBOX_DVD_SECURITY));
// get the dvd layout
status = getDvdLayout(cdrom_device, &dvd_layout);
assertOrExit(status == STATUS_SUCCESS, "Failed to read DVD layout! (0x%08X)\n", status);
writeFileBytes("C:\\backup\\dvd_layout.bin", (uint8_t*)&dvd_layout, 0, sizeof(XBOX_DVD_LAYOUT));
// TODO: disc type soft verification
// calculate digest
uint32_t len = 0x4CB;
XcSHAInit((PUCHAR)&sha_ctx);
XcSHAUpdate((PUCHAR)&sha_ctx, (PUCHAR)&len, sizeof(len));
XcSHAUpdate((PUCHAR)&sha_ctx, (PUCHAR)&dvd_layout + 4, len);
XcSHAFinal((PUCHAR)&sha_ctx, (PUCHAR)&sha_hash);
// perform soft validation of digest and signature
if (memcmp(&sha_hash, (PUCHAR)&dvd_layout + 0x4CF, sizeof(sha_hash)) != 0) {
debugPrint("Invalid DVD security digest!\n");
}
if (XcVerifyPKCS1Signature((PUCHAR)&dvd_layout + 0x4E3, (PUCHAR)&XePublicKeyData, (PUCHAR)&sha_hash) == 0) {
debugPrint("Invalid DVD security signature!\n");
}
// decrypt challenge response entries and save again
XcSHAInit((PUCHAR)&sha_ctx);
XcSHAUpdate((PUCHAR)&sha_ctx, (PUCHAR)&dvd_layout + 0x4A3, 0x2C);
XcSHAFinal((PUCHAR)&sha_ctx, (PUCHAR)&sha_hash);
XcRC4Key((PUCHAR)&rc4_ctx, 7, (PUCHAR)&sha_hash);
XcRC4Crypt((PUCHAR)&rc4_ctx, 0xFD, (PUCHAR)&dvd_layout + 0x306);
writeFileBytes("C:\\backup\\dvd_layoutdecrypted.bin", (uint8_t*)&dvd_layout, 0, sizeof(XBOX_DVD_LAYOUT));
// verify challenge response data (Xbox randomizes starting index, we don't care)
// TODO: dvd_layout.data[0x304] should be equal to 1
int challengeEntryCount = dvd_layout.data[0x305]; // TODO: validate min of 1, max of 23
PXBOX_DVD_CHALLENGE challenges = (PXBOX_DVD_CHALLENGE)((PUCHAR)&dvd_layout + 0x306);
bool first = true;
int lastIndex = -1;
for (int i = 0; i < challengeEntryCount; i++)
{
if (challenges[i].type != 1) continue;
verifyChallengeResponse(cdrom_device, &dvd_layout, &challenges[i], first, false);
first = false;
lastIndex = i;
}
assertOrExit(lastIndex != -1, "No valid challenges available to process!\n");
verifyChallengeResponse(cdrom_device, &dvd_layout, &challenges[lastIndex], false, true);
// TODO: other validation that xbox does
debugPrint("DVD verification success!\n");
waitAndExit();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment