-
-
Save Ernegien/bef054a43213c52c2bef8d94bf9f51cb to your computer and use it in GitHub Desktop.
Xbox DVD Authentication NXDK Testbed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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