Investigating an early-2010s gaming DRM system: Part 1
Background
This post concerns a DRM system used in a PC gaming platform introduced in the early 2010s. The particular DRM system is not relevant and will not be identified, but will be familiar to many.
One function of the DRM system is to require a user to have a separate piece of social networking software running in the background, even if a valid licence is present and social networking features are otherwise not required.
It is known that this function is controlled by a configuration file bundled with each game, here denoted Foo.bar.
Static data analysis
Below is (a mock-up of) a Foo.bar file:
00000000: 84c4 3851 3a20 0217 003b 6d2b 0604 1f09 ..8Q: ...;m+....
00000010: 031a 2614 514f 5346 4450 5657 5150 4a55 ..&.QOSFDPVWQPJU
00000020: 5958 5e5f 595b 425d 4140 4745 4146 4d04 YX^_Y[B]A@GEAFM.
00000030: 0c0b 0707 150a 0810 1315 4108 001f 1313 ..........A.....
00000040: 0116 141b 1a1a 3b01 034b 0e06 0509 0d1f ......;..K......
00000050: 0c0e 0a09 0b2c 1207 4d04 0c0b 0707 150a .....,..M.......
00000060: 0810 1315 3207 1b7a 381c 0000 140d 0e06 ....2..z8.......
00000070: 0021 0f14 1c1b 054b 514d 231a 0305 3d03 .!.....KQM#...=.
00000080: 111b 320d 000d 040a 3159 452e 3921 3e17 ..2.....1YE.9!>.
00000090: 1c04 141f 3d15 1804 3457 6e36 1317 1806 ....=...4Wn6....
000000a0: 181f 0909 2a06 0305 001c 0755 5c42 2736 ....*......U\B'6
000000b0: 2835 1e1b 1d0f 0622 0c03 0a26 4578 (5....."...&Ex
The data is obviously not human-readable, and no obvious opcode/length combinations appear to apply to interpreting the data.
Notice that the string 4D 04 0C 0B 07 07 15 0A 08 10 13 15
is repeated twice (at 0x002E and 0x0058). This is an initial indication of there being some structure to this data. The reason for this will become apparent.
Curiously, except for the first four bytes, the remainder of the bytes are all at most 0x7F, i.e. 7-bit values. Packing these 7-bit values into 8-bit bytes yields only more gibberish. Perhaps this observation has something to do with ASCII?
Below is (a mock-up of) a Foo2.bar file from a second game:
00000000: b2b9 ca72 3a20 0217 003b 6d2b 0604 1f09 ...r: ...;m+....
00000010: 031a 2614 514f 5346 4450 5650 5457 4a55 ..&.QOSFDPVPTWJU
00000020: 5958 5c58 5554 425d 4140 4640 4444 4d04 YX\XUTB]A@F@DDM.
00000030: 0c0b 0707 150a 0810 1315 325c 3014 145e ..........2\0..^
00000040: 151b 1a03 0311 0604 1c1f 1136 5834 0809 ...........6X4..
00000050: 4209 1f1e 1012 0617 0018 1b1d 3a54 380e B...........:T8.
00000060: 1b46 0d03 020c 0e02 1313 090c 0c3e 503c .F...........>P<
00000070: 0d11 4a01 0706 080a 1e0f 0f15 0808 2d41 ..J...........-A
00000080: 2b05 0d4e 050b 0a04 061a 0b0b 1114 1431 +..N...........1
00000090: 5d2f 1217 7924 070e 0616 0711 320e 1c05 ]/..y$......2...
000000a0: 0f4b 514d 4c29 1f1e 3012 0637 0018 3b3d .KQML)..0..7..;=
000000b0: 4554 4562 2004 1818 0c02 0315 1536 1a07 ETEb ........6..
000000c0: 0113 0d43 5945 2b12 1b1d 251b 0903 3d00 ...CYE+...%...=.
000000d0: 1318 131f 2244 4d26 3129 361f 141c 0c07 ...."DM&1)6.....
000000e0: 250d 000b 3944 7b21 0604 050e 1017 0101 %...9D{!........
000000f0: 220e 1b1d 1804 1f4d 534f 3423 3f20 0d06 "......MSO4#? ..
00000100: 1507 0e2a 040b 023e 5d60 ...*...>]`
There are obvious similarities between these two files. Bytes 0x0004 to 0x0109 are exactly the same. Additionally, the 4D 04
… string is repeated again at 0x002E (although not at 0x0058). This is again suggestive of the underlying structure.
It is also obvious, examining these two files, that the first four bytes are probably a checksum of some sort, probably CRC32 or a variant thereof.
Fuzzing
To investigate the apparent checksum, we can see what happens when we try modifying bytes of the data. Taking Foo.bar and zeroing out any of the first four bytes causes the game to report that the file is missing or invalid. Zeroing out the fifth byte causes the same message.
However, zeroing out any of the next three bytes does not produce that error (although it does, understandably, prevent the game from launching). Zeroing out the next byte produces the missing or invalid error. This pattern repeats, with only the first of every four payload bytes appearing to contribute to the checksum (with one exception of the byte at 0x0009 – the reason for this will also become apparent).
Debugging and disassembly
We now turn to debugging and disassembly of the game itself. The DRM libraries appear to contain anti-debugging logic, but this was not triggered by Wine's winedbg debugger, and so did not present a problem.
Firstly, because we know that the DRM system accesses the Foo.bar file, we run the executable under Wine with WINEDEBUG=+file
, to log all the filesystem-related kernel calls.
Searching the output for references to Foo.bar yields:
003a:trace:file:GetFileAttributesExW L"C:\\Program Files (x86)\\FooBar Games\\FooBarBazXY\\Binaries\\Foo.bar" 0 0x8deba8
003a:trace:file:RtlDosPathNameToNtPathName_U_WithStatus (L"C:\\Program Files (x86)\\FooBar Games\\FooBarBazXY\\Binaries\\Foo.bar",0x8deae0,(nil),(nil))
003a:trace:file:RtlGetFullPathName_U (L"C:\\Program Files (x86)\\FooBar Games\\FooBarBazXY\\Binaries\\Foo.bar" 520 0x8de854 (nil))
003a:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\Program Files (x86)\\FooBar Games\\FooBarBazXY\\Binaries\\Foo.bar" -> "/home/runassudo/.PlayOnLinux/wineprefix/tmp/dosdevices/c:/Program Files (x86)/FooBar Games/FooBarBazXY/Binaries/Foo.bar"
…
003a:trace:file:GetFileAttributesExW L"C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar" 0 0x8dfbe8
003a:trace:file:RtlDosPathNameToNtPathName_U_WithStatus (L"C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar",0x8dfb20,(nil),(nil))
003a:trace:file:RtlGetFullPathName_U (L"C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar" 520 0x8df894 (nil))
003a:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar" -> "/home/runassudo/.PlayOnLinux/wineprefix/tmp/dosdevices/c:/Program Files (x86)/FooBar Games/FooBarBazXY/Binaries/Foo.bar"
003a:trace:file:CreateFileW L"C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar" GENERIC_READ FILE_SHARE_READ FILE_SHARE_WRITE creation 3 attributes 0x1
003a:trace:file:RtlDosPathNameToNtPathName_U_WithStatus (L"C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar",0x8dfb04,(nil),(nil))
003a:trace:file:RtlGetFullPathName_U (L"C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar" 520 0x8df854 (nil))
003a:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Foo.bar" -> "/home/runassudo/.PlayOnLinux/wineprefix/tmp/dosdevices/c:/Program Files (x86)/FooBar Games/FooBarBazXY/Binaries/Foo.bar"
003a:trace:file:CreateFileW returning 0xb4
003a:trace:file:ReadFile 0xb4 0x6e9998 4096 0x8dfbc0 (nil)
Note the call to CreateFileW, which presumably opens a handle to the Foo.bar file for reading. We can now run the game binary under winedbg and attach a breakpoint to this kernel call.
However, it appears that, although the game contains DRM-related DLLs, the actual DRM logic is performed by a separate executable (say, DRMUI.exe) which is launched as a child process by the game binary.
Using regedit
, we browse to HKEY_CURRENT_USER/Software/Wine/WineDbg and set AlsoDebugProcChild to 1. This (poorly-documented) flag will tell winedbg to also debug child processes, which it does not do by default.
Now we can run the game under winedbg:
$ WINEDEBUG=-all winedbg FooBarBazX.exe
WineDbg starting on pid 003e
0x000000007b465d91: movl 0xffffff24(%ebp),%esi
Wine-dbg>c
0x000000007b465d91: movl 0xffffff24(%ebp),%esi
Wine-dbg>
Now we are within DRMUI.exe.
Wine-dbg>break CreateFileW
Breakpoint 1 at 0x000000007b442120 CreateFileW in kernel32
Wine-dbg>c
Stopped on breakpoint 1 at 0x000000007b442120 CreateFileW in kernel32
Wine-dbg>
Now let's examine which file is being opened.
Wine-dbg>info regs
Register dump:
CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
EIP:7b442120 ESP:008dfb5c EBP:008dfbd8 EFLAGS:00000212( - -- I -A- - )
EAX:7d306bb9 EBX:7d3a5000 ECX:008dfbf0 EDX:00000040
ESI:00008000 EDI:00000001
Wine-dbg>x/x $ecx
006e9650
Wine-dbg>x/16x 0x006e9650
0x00000000006e9650: 003a0043 0050005c 004f0052 007e0047
0x00000000006e9660: 00500035 005c0032 0052004f 00470049
0x00000000006e9670: 0052007e 00550050 004d005c 00530041
0x00000000006e9680: 007e0053 005a0049 005c0050 00690042
Wine-dbg>x/u 0x006e9650
C:\PROG~5P2\FOOB~RPU\FOOB~IZP\Binaries\Foo.bar
Wine-dbg>
It looks like this is our file! We can now check the backtrace to see where this is being called from.
Wine-dbg>bt
Backtrace:
=>0 0x000000007b442120 CreateFileW() in kernel32 (0x00000000008dfbd8)
1 0x000000007d31e11f MSVCRT__wsopen+0x4e() in msvcr100 (0x00000000008dfc38)
2 0x000000007d322d1f MSVCRT__wfsopen+0x7e() in msvcr100 (0x00000000008dfc98)
3 0x0000000000425b35 in drmui (+0x25b34) (0x00000000006e9918)
4 0x000000000050005c in drmui (+0x10005b) (0x00000000003a0043)
Wine-dbg>
Let's now open DRMUI.exe in a disassembler (I'm using IDA Free) and navigate to 0x00425b35. This appears to be a rather uninteresting helper function which opens the file, and the final entry in the backtrace above is not actually correct, so let's step up through the program until we get to the calling function.
Wine-dbg>fin
0x000000007d31dcab MSVCRT__wsopen_dispatch+0x33b in msvcr100: movl %eax,0xffffffc0(%ebp)
Wine-dbg>fin
0x000000007d31e11f MSVCRT__wsopen+0x4f in msvcr100: addl $32,%esp
Wine-dbg>fin
0x000000007d322d1f MSVCRT__wfsopen+0x7f in msvcr100: addl $16,%esp
Wine-dbg>fin
0x0000000000425b35: addl $12,%esp
Wine-dbg>fin
0x0000000000431d3e: testb %al,%al
Wine-dbg>
Navigating to 0x00431d3e in IDA:
Now this subroutine looks very interesting. There are references to error strings about failure to read the Foo.bar file, invalid CRC, and so on.
Zooming in on the particular section which references the CRC:
The first block, at loc_431E46, contains a conditional jump to a block which references an error string about invalid CRCs. This condition is based on comparing a value in memory with eax, which is the return value from sub_43EC40. Presumably, then, this subroutine is responsible for computing the CRC of the Foo.bar file.
The second block, at loc_431E80, contains a reference to a 21-character gibberish ASCII string. Further investigation of the subroutines called in this block reveals that this is the key for a simple XOR cipher used to encrypt the payload of the Foo.bar file.
Navigating to the sub_43EB80 referenced by the CRC block:
And to the sub_43EC40 also referenced by that block:
This is a straightforward implementation of a big-endian CRC32 scheme, with an initial register value of 0x0, nil final XOR, and standard polynomial 0x04C11DB7. In reveng notation, this is reveng -b -w 32 -p 04C11DB7 -i 00000000 -x 00000000
.
Note, however, the line add edx, 4
. edx is the pointer to the current byte, so this code (whether inadvertently or deliberately is difficult to say) skips three out of every four bytes, explaining the result from earlier.
This is easily reimplemented in Python:
# sub_43EB80
def create_table():
a = []
for i in range(256):
k = i
k = (k << 24) & 0xFFFFFFFF
for _ in range(8):
if k & (1 << 31) == 0: # if i MSB not set
k = (k << 1) & 0xFFFFFFFF
else:
k = ((k << 1) & 0xFFFFFFFF) ^ 0x4C11DB7
a.append(k)
return a
crc_table = create_table()
# sub_43EC40
def crc(buf):
a = 0
# Only check every fourth byte(!)
for i in range(0, len(buf), 4):
a = ((a << 8) & 0xFFFFFFFF) ^ crc_table[((a >> 24) ^ buf[i]) & 0xFF]
return a
crc_val = crc(data)
Putting it all together
We now have all the information required to decrypt a Foo.bar file. After stripping the four-byte CRC and applying the simple XOR encryption, we reveal the ASCII payload – a simple INI file:
[Base]
ContentId = 2114455,2114456,2115653,foobarbazxy,foobarbazxy_de,foobarbazxy_fr,foobarbazxy_it
InstalledDistro = MustOpenSocialV1,DRMSystemNameV4
SupportedDistros = DRMSystemNameV4
(The sample from the beginning of this post used the fake key abcdefghijklmnopqrstu
.)
This explains the observations earlier. The repeated hex strings were from repeated strings in the payload which happened to align at the same point in the XOR key. The limited 7-bit range of values was because both the payload and XOR key were only in 7-bit ASCII. The reason why zeroing the byte at 0x0009 resulted in an error was because this byte encoded the ]
from the first line of the payload, probably interfering with the INI parser.
(Edit: This process of looking for repeating strings in encrypted data is not dissimilar to Kasiski examination, used to defeat simple ciphers from as early as 1846!)
Because only simple XOR and CRC32 is used, this particular component of the DRM system is easily defeated by, for example, removing the MustOpenSocialV1,
string, redoing the XOR, and recomputing the CRC32 using the code above.