Breaking bad CD-ROM DRM Protection
While everybody in my area are preparing for Passover and doing some cleaning, I came across a bunch of CD-ROMs that have be left on a wall near my house for the public. These CDs contained movies that are directed at the Jewish orthodox community in my area. I really did not have an interest in these CDs, as they are very poor quality and have an atrocious plot. But while browsing through the titles, I noticed a small anti-piracy warning printed on the cover stating that “Any attempt at copying this disk will cause damage to the disk drive”. How will that work exactly? Since I was not near my computer (yes, it has a CD drive) I inspected the disk visually. I found that some of the CDs had 1-4 dimples on the reflective side and some didn’t. I picked a few with and without the dimples and decided to take a look at them at home.
When I got back home, I put the CDs into the computer and tried to play them. On all the CDs, I noticed there were a bunch of EXE/PE files and a larger ~600MB file probably containing the movie. The first thing I tried was to play the movies on the CDs. I noticed there were 2 types of CDs, some simply contained a MPEG-1 sequence file (with a mx
extension) and 3 PE files (mpfull.exe
, Player.exe
and run.dll
) but others were significantly more complicated with 15 different PE files and the suspected video file (the largest 600MB file) was in an unknown format with a grt
extension. I decided to start from the CDs that had the plain MPEG-1 sequence on them, I opened the file in VLC and started playing them, the movie played for a while and then got stuck at around the 10 minute mark, I checked dmesg
and as expected, the CD drive was stuck trying to read a bad sector. This was expected due to the dimples on the CDs surface, but raises the question how can a standard user play these movies without it getting stuck?
The dmesg errors warning about bad sectors:
[ +7.067739] sr 2:0:0:0: [sr0] tag#30 FAILED Result: hostbyte=DID_OK driverbyte=DRIVER_SENSE
[ +0.000008] sr 2:0:0:0: [sr0] tag#30 Sense Key : Medium Error [current]
[ +0.000004] sr 2:0:0:0: [sr0] tag#30 Add. Sense: L-EC uncorrectable error
[ +0.000006] sr 2:0:0:0: [sr0] tag#30 CDB: Read(10) 28 00 00 00 c4 f8 00 00 02 00
[ +0.000004] print_req_error: I/O error, dev sr0, sector 201696
[ +0.000011] Buffer I/O error on dev sr0, logical block 50424, async page read
[ +0.000007] Buffer I/O error on dev sr0, logical block 50425, async page read
Next, I wanted to try and play the other set of CDs that had the 15 PE files on it. As I mentioned before, the CDs contained a large file with a grt
extension that I could not open with VLC. I ran file 03.grt
but only got 03.grt: data
, I also run binwalk 03.grt
just in case but all it found was some false-positives. Reading the file with hexdump -Cn512 03.grt
also did not really help, but it did give me clue that the file has some structure, as it started with the magic letters GT
. Looking back at the CD cover, the company logo that was making these movies did say גרינטק
(GreenTech
in hebrew) and the EXE file included in the CD was called GRTPlayer.exe
, so GT
makes sense to be some king of magic based on the company initials. Playing the movies with the included GRTPlayer seemed to work just fine, this is true for the previous type of CD I wrote about.
The built it GRTPlater.exe playing the movie (stopped at the anti-piracy warning, notifying me there is enough space in hell for me too :O )
After looking at the 2 types of formats the company makes, I’m left with 2 big questions:
- What is this mysterious
GRT
movie format? - How do I break the DRM and copy these movies?
I decided to work on the GRT
format question first, since the PE files on the CD are small (all totaling 11.9MB) I could copy them to the computer without worrying about the bad sectors. I then started to filter the files and come up with a list of files that are most probable of holding the key. I used strings
on the files and grep
ed for strings such as grt,greentech and more. Most of the files were easy do dismiss, as they all had standard names and strings that marked them as standard Borland C++ libraries. I was left with 6 files that seemed to be custom Borland package file:
- GRTPlayer.exe
- GRTPlayerPkg.bpl
- Krs.bpl
- KrsBase.bpl
- KrsLocal.bpl
- XMLServer.bpl
- DShow.bpl
Another interesting thing about the files, was the bpl
extension and strings allowed me to identify the langugae and environment the program was built in as Borland C++, and the file named DShow.bpl
gave away the fact that the media player was using Microsoft DirectShow. I started up IDA and loaded GRTPlayer.exe
as this is the main player file, Without even needing to search to far, I found in the WinMain
initialization of the ECX
and EDX
parameters with a TKrsGrtPlayerForm
as the object and GrtPlayerUnit
as a parameter. Looking at the TGrtPlayerForm
callbacks, the FormCreate
method is called when the window is being created and from there, some custom code can already be seen. The first thing that TGrtPlayerForm::FormCreate
does is to check weather a video file was specified. If a video file was not specified, it searches for a *.grt
file in the EXEs working directory and will use that file as it’s media source. Once a video file path is available the method continues and calls two more methods from the DShow library TKrsCustomDShowPlayer::Open
and then TKrsCustomDShowPlayer::Play
. Following the execution path in the DShow library, the mentioned methods use a TKrsGrtPlayer
object from the TGrtPlayerForm
to open the media file and read it.
After following the execution path into the GRTPlayerPkg.bpl
library, it is clear that all the logic for handling the file is located in it. Looking at the virtual table of the TKrsGrtPlayer
object, there are some interesting methods handling the file. The TKrsGrtPlayer::Render
method is the first method to be called from the main executable, this method accepts a System::AnsiString
as a parameter and checks weather the string ends with a .grt
extension. If the string is not a GRT file, the method TKrsCustomDShowPlayer::Render
is called (located in DShow) and is used a a standard media player. But, if the file has a .grt
extension, then TKrsGrtPlayer
kicks in and initializes the custom media player.
The next thing that can be learned from the custom init code is that the movie expected to play is indeed a standard MPEG-1 stream, this is clear in the initialization of DirectShow (as seen below) as the inilization uses MEDIATYPE_Stream
and the subtype as MEDIASUBTYPE_MPEG1System
. At this point, I know the GRT file is a standard MPEG-1 stream, but looking throught the file with hexdump
I could still not find any matching MPEG-1 headers. But not fat down, I found the first breakthrough that yielded results. I found the head parsing method.
The first thing the header parsing method does, it to call CreateFileA
with standard parameter to open a file for reading. Next, it reads 2 bytes of the file and compares them to GT
, as expected it’s checking the header magic. After that, it reads in a DWORD and compares it to 1
. I suspect this is a version check, but in all files that I have the field is always 1
. Now is where things get interesting. It read another DWORD but thins time it uses this DWORD as a counter for a loop. While counting down, for each iteration the code reads in 8 bytes (split into 2 DWORDS) and adds them to a list with Classes::TList::Add
. Once done it returns to the callee and continues. At this point I wrote a python function to parse the head to see if the list of values made any sense.
import struct
def parseGRTHeader(fd):
fd.seek(0)
magic, version, chunk_list_len = struct.unpack('<2sII', fd.read(10))
if magic != 'GT':
raise Exception('Not a GRT file, bad magic in header (should be GT)')
if version != 1:
raise Exception('Bad version field in header (should be 1)')
print("Header: {0}\nVersion: {1}\nChucks: {2}".format( \
magic, version, chunk_list_len))
return [struct.unpack('<II', fd.read(8)) for _ in range(chunk_list_len)]
In [75]: f = open('03.grt', 'rb')
In [76]: clist = parseGRTHeader(f)
Header: GT
Version: 1
Chucks: 907
In [77]: len(clist)
Out[77]: 907
In [78]: clist[:10]
Out[78]:
[(671797238, 684549),
(85300020, 584604),
(211306286, 668986),
(95893255, 821944),
(160460287, 578712),
(158481151, 553283),
(460379005, 648556),
(255951063, 840734),
(526785614, 985286),
(43287269, 660861)]
After reading the header and the list, I still did not get anything that looked like a MPEG-1 stream. So I decided to take a new approach. I searched the import table and looked for references to any methods that are relevant and deal with lists. I found a method that used Contnrs::TOrderedList::Count
on the list, and as long as the list was not empty, it set an event. Then, by cross referencing the list object offset and the event object offset with other methods, I found a loop which loops as long as list count is non zero. Inside this loop, a call to a different readFile
wrapper was done, this time within a wrapper that XORed the output of the read function with 0xFF, this XORed read method was called from several callbacks of the player object. Inside the warped read method the list is usedand for each entry, the player also passes the first list parameter to SetFilePointer
and the second list parameter as the nNumberOfBytesToRead
for readFile
. After this, the purpose of the list was clear and the file could be reconstructed from scratch.
I wrote another simple python method that reads the data and will XOR it and store it back into a file.
def convertGRTData(fd_in, fd_out, chunk_list):
for chunk in chunk_list:
fd_in.seek(chunk[0])
idata = fd_in.read(chunk[1])
fd_out.write(''.join(chr(ord(c) ^ 0xff) for c in idata))
After running the python script I had a fully working MPEG-1 stream that could be played in any player. The reason the movie time in the VLC shot is longer than the movie time in the following image is because it’s a different movie, same format.
Now that I have defeated the GRT format, how can I copy these movies without the CD drive getting stuck on a bad sector? Well, the answer is simple. Using the following python code, I checked all the sections in the list and checked if the entire size of the GRT movie file is covered by the list in the GRT header. As the code output suggested, it was not all mapped in the list. With this “hole” in the file, the player seeks past it while playing and never tries to read the bad sectors, but a CD cloning program will fail and get stuck reading the CD. As I can now parse the GT header myself and skip the hole, I can copy the movie directly from the CD to a convenient mpg
file on my computer.
# https://stackoverflow.com/a/1094933
def sizeof_fmt(num, suffix='B'):
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
def findHolesInList(clist):
d = dict(clist)
s = sorted(d.keys())
for i in range(len(s)-1):
if (s[i]+d[s[i]]) != s[i+1]:
print("Hole found: start={0}, size={1}".format(\
s[i]+d[s[i]], sizeof_fmt(s[i+1]-(s[i]+d[s[i]]))))
Hole found: start=977755, size=25.7MiB
Now here comes the kicker, I really don’t have anything to do with these movies. They are at a terrible 352x288 resolution, the actors and acting are far worse than anything you can imagine and the plot (that does not always exist) is terrible and full of doctrine, not exactly the movies I prefer to pirate. But, Why not break the DRM anyway? If anyone finds a GRT file they want to convert, I have a Github repo with a simple and dirty converter written in C (much much faster than the python shown here) GRTConvert repo.