Investigating an early-2010s gaming DRM system: Part 4
Last time, we investigated how an early-2010s gaming DRM system approached machine-based licensing. This time, we'll investigate exactly how the DRM system interacts with the game to accomplish its ends.
Structure of the DRM system
Looking at the game binary, FooBarBazX.exe, for the first time in IDA, we see that there is only one subroutine defined, and the remainder of the .text (code) section is garbled nonsense.
The only visible code in this entire binary is a jump to a Core_Activation_100 function from another file, Activation.dll.
From earlier, we know that the actual DRM logic is performed by a separate DRMUI.exe binary. We can run FooBarBazX.exe with WINEDEBUG=+file,+process
to have a closer look:
0009:trace:file:NtQueryDirectoryFile (0x5c (nil) (nil) (nil) 0x52c174 0x1748e4 0x0000025e 0x00000003 0x00000000 L"DRMUI.exe" 0x00000001
0009:trace:file:read_directory_data_stat found DRMUI.exe
0009:trace:file:append_entry long L"DRMUI.exe" short L"DRMU~B0X.EXE" mask <null>
0009:trace:file:init_cached_dir_data mask L"DRMUI.exe" found 1 files
0009:trace:file:init_cached_dir_data L"DRMUI.exe" L"DRMU~B0X.EXE"
0009:trace:file:NtQueryDirectoryFile => 0 (126)
0009:trace:file:FindNextFileW 0x1748a0 0x52c24c
0009:trace:file:FindNextFileW returning L"DRMUI.exe" (L"DRMU~B0X.EXE")
0009:trace:file:CheckNameLegalDOS8Dot3W (L"DRMUI.exe" (nil) 0 (nil) 0x52c248)
0009:trace:file:GetLongPathNameW returning L"Z:\\home\\runassudo\\.PlayOnLinux\\wineprefix\\tmp\\drive_c\\Program Files (x86)\\FooBar Games\\FooBarBazXY\\Binaries\\Core\\DRMUI.exe"
0009:trace:file:RtlGetFullPathName_U (L"Z:\\home\\runassudo\\.PlayOnLinux\\wineprefix\\tmp\\drive_c\\Program Files (x86)\\FooBar Games\\FooBarBazXY\\Binaries\\Core\\DRMUI" 520 0x52cd44 (nil))
0009:trace:process:CreateProcessInternalW starting L"Z:\\home\\RUNA~HRW\\_PLA~EY4\\WINE~3XF\\tmp\\drive_c\\PROG~5P2\\FOOB~RPU\\FOOB~IZP\\Binaries\\Core\\DRMUI.exe" as Win32 binary (400000-5b9000, x86)
It looks like the process is being spawned with CreateProcessInternalW. With winedbg, we can again find out where this is being called from:
$ WINEDEBUG=-all winedbg FooBarBazX.exe
WineDbg starting on pid 0031
0x000000007b466051: movl 0xffffff24(%ebp),%esi
Wine-dbg>break CreateProcessInternalW
Breakpoint 1 at 0x000000007b467a40 CreateProcessInternalW in kernel32
Wine-dbg>c
Stopped on breakpoint 1 at 0x000000007b467a40 CreateProcessInternalW in kernel32
Wine-dbg>bt
Backtrace:
=>0 0x000000007b467a40 CreateProcessInternalW() in kernel32 (0x000000000052d528)
1 0x000000001000f6d3 in activation (+0xf6d2) (0x000000007b4621a0)
2 0x00000000fff0e483 (0x0000000004244c8d)
Wine-dbg>
So it looks like it is this Activation.dll which is spawning the DRMUI.exe binary. It turns out that DRMUI.exe is a 32-bit application, whereas Activation.dll appears to have a 64-bit version, which I suppose partly explains this indirection.
Inspecting the arguments:
Wine-dbg>x/13x $esp
0x000000000052d4bc: 7b46976d 00000000 00000000 0052d618
0x000000000052d4cc: 00000000 00000000 00000001 00000400
0x000000000052d4dc: 00000000 00000000 0052d5d4 0052d584
0x000000000052d4ec: 00000000
Wine-dbg>x/u 0x0052d618
"Z:\home\RUNA~HRW\_PLA~EY4\WINE~3XF\tmp\drive_c\PROG~5P2\FOOB~RPU\FOOB~IZP\Binaries\\Core\DRMUI.exe" /HDLID=68
Wine-dbg>
Of note is the value of 0x00000001 in the sixth argument to CreateProcessInternalW, which corresponds with the bInheritHandles flag. From the docs for the regular CreateProcessW:
If this parameter is TRUE, each inheritable handle in the calling process is inherited by the new process. If the parameter is FALSE, the handles are not inherited. Note that inherited handles have the same value and access rights as the original handles.
So it looks like the game is using inherited file handles as a form of inter-process communication (IPC). This fits with the /HDLID=68
parameter being passed on the command line, which is probably the file handle number.
Putting this together with what we know from earlier:
What a mess!
Inspecting the initial IPC data
Examining the WINEDEBUG
logs from earlier, there do not appear to be any CreateFileW, ReadFile or WriteFile calls that would explain where this IPC file handle comes from. Through further investigation of Activation.dll, it appears that it is using memory-mapped files through CreateFileMappingA. This has the advantage (for them) of being able to create mappings backed by the system paging file, making the data impenetrable from the outside.
Happily (for us), however, Activation.dll does not appear to do any integrity checking on DRMUI.exe, so we can replace this file with our own to see what is going on. We create a simple C binary to dump the IPC data to a file, and replace the real DRMUI.exe with our stub:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main(int argc, char** argv) {
int ipcid = atoi(argv[1] + 7); /* +7 to get substring */
HANDLE target = CreateFileA("ipc_log.bin", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
char buf[4096];
DWORD dwBytesRead, dwBytesWritten;
while (1) {
ReadFile((HANDLE)(intptr_t)ipcid, buf, sizeof(buf), &dwBytesRead, NULL);
if (dwBytesRead == 0) {
break;
}
WriteFile(target, buf, dwBytesRead, &dwBytesWritten, NULL);
}
CloseHandle(target);
return 0;
}
Examining the result:
00000000: 0200 0000 0000 0000 6162 6366 6566 677b ........abcfefg{
00000010: 2c12 0e0f 181a 0e12 1d17 2306 1a02 0710 ,.........#.....
00000020: 172c 0267 6869 6b53 6c6d 6e7a 2703 1303 .,.ghikSlmnz'...
00000030: 0410 0527 1b01 0613 1309 0b06 0e3c 0c1a ...'.........<..
00000040: 0770 7172 3437 4f3d 3211 0b02 1406 0549 .pqr47O=2......I
00000050: 2c02 0008 1d4f 5809 4a45 5d29 270d 0c26 ,....OX.JE])'..&
00000060: 0414 472f 0807 0e1f 3128 001f 3313 0136 ..G/....1(..3..6
00000070: 141b 3a3a 3827 0f09 091b 030e 1f31 2800 ..::8'.......1(.
00000080: 1f33 1301 3614 1b3a 4d01 1d03 0000 0000 .3..6..:M.......
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
...
00000ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
The first byte, 0x02, looks like it might be some sort of control value like an opcode or length, but after a few null bytes, the data seems at first to be gibberish.
Looking more closely, however, we see that the encrypted data begins with a string that looks rather like the XOR key we discovered in part 1 (in this example, abcdefghijklmnopqrstu
). Note that if the plaintext is a null byte, the XOR encryption will simply be the key. Could this simply be the same XOR cipher as in part 1?
Trying to decrypt the data using that earlier key reveals that, yes, it is!
00000000: 0200 0000 0000 0000 0000 0002 0000 0013 ................
00000010: 4578 6563 7574 6162 6c65 5072 6f63 6573 ExecutableProces
00000020: 7349 6400 0000 0138 0000 0015 5772 6170 sId....8....Wrap
00000030: 7065 6445 7865 6375 7461 626c 6550 6174 pedExecutablePat
00000040: 6800 0000 4743 3a5c 5072 6f67 7261 6d20 h...GC:\Program
00000050: 4669 6c65 7320 2878 3836 295c 466f 6f42 Files (x86)\FooB
00000060: 6172 2047 616d 6573 5c46 6f6f 4261 7242 ar Games\FooBarB
00000070: 617a 5859 5c42 696e 6172 6965 735c 466f azXY\Binaries\Fo
00000080: 6f42 6172 4261 7a58 2e65 7865 0000 0000 oBarBazX.exe....
...
The format appears to be quite simple: a series of key–value pairs, with each string preceded by 4 bytes denoting its length. The value 0x0002 preceding this series (but within the previously-encrypted data) seems to be the number of key–value pairs.
Let us denote the response using something a little more readable:
{
_opcode: 0x02,
ExecutableProcessId: '8',
WrappedExecutablePath: r'C:\Program Files (x86)\FooBar Games\FooBarBazXY\Binaries\FooBarBazX.exe'
}
Examining the response from DRMUI.exe
Armed with this knowledge, we can now construct a simple application to pass this data to DRMUI.exe:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main(int argc, char** argv) {
HANDLE input = CreateFileA("ipc_log.bin", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE ipc = CreateFileA("ipc_tmp.bin", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
/* replay ipc_log.bin */
char buf[4096];
DWORD dwBytesRead, dwBytesWritten;
while (1) {
ReadFile(input, buf, sizeof(buf), &dwBytesRead, NULL);
if (dwBytesRead == 0) {
break;
}
WriteFile(ipc, buf, dwBytesRead, &dwBytesWritten, NULL);
}
CloseHandle(input);
/* create mapping */
SECURITY_ATTRIBUTES sa;
memset(&sa, 0, sizeof(SECURITY_ATTRIBUTES));
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
HANDLE ipc_map = CreateFileMappingA(ipc, &sa, PAGE_READWRITE, 0, 0, NULL); /* with bInheritHandle = TRUE */
/* launch file */
char cmdline[4096];
sprintf(cmdline, "Z:\\path\\to\\DRMUI.exe /HDLID=%d", ipc_map);
STARTUPINFO si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(STARTUPINFO));
si.cb = sizeof(si);
memset(&pi, 0, sizeof(PROCESS_INFORMATION));
CreateProcessA("Z:\\path\\to\\DRMUI.exe", cmdline, NULL, NULL, TRUE, 0, NULL, "Z:\\path\\to\\Binaries", &si, &pi); /* bInheritHandles = TRUE */
CloseHandle(ipc_map);
CloseHandle(ipc);
return 0;
}
Decrypting the response that we receive:
00000000: 0600 0000 0000 0000 0000 0006 0000 0013 ................
00000010: 4578 6563 7574 6162 6c65 5072 6f63 6573 ExecutableProces
00000020: 7349 6400 0000 0138 0000 0003 4b65 7900 sId....8....Key.
00000030: 0000 ac5a 7567 4834 5947 766d 644b 7a31 ...ZugH4YGvmdKz1
00000040: 6b39 4570 3277 7666 4d75 5542 6146 4c45 k9Ep2wvfMuUBaFLE
00000050: 3467 5076 5a48 5557 3762 4158 366f 4252 4gPvZHUW7bAX6oBR
00000060: 4e4a 6870 3935 4472 6873 2b52 6d67 474e NJhp95Drhs+RmgGN
00000070: 4162 7a4e 5736 6c70 6352 5178 5042 372b AbzNW6lpcRQxPB7+
00000080: 6732 3536 4850 646f 4539 5a42 4b46 5979 g256HPdoE9ZBKFYy
00000090: 5638 4f30 782f 5353 6c79 756b 2f55 7371 V8O0x/SSlyuk/Usq
000000a0: 2b55 3332 7858 7968 7359 334d 4572 5544 +U32xXyhsY3MErUD
000000b0: 534c 6875 6277 6238 6477 6a76 4439 3938 SLhubwb8dwjvD998
000000c0: 6351 3279 574b 7077 7a64 5a74 5165 4a32 cQ2yWKpwzdZtQeJ2
000000d0: 5875 5031 7555 5972 5934 5549 513d 3d00 XuP1uUYrY4UIQ==.
000000e0: 0000 0c4c 6175 6e63 6841 6374 696f 6e00 ...LaunchAction.
000000f0: 0000 0132 0000 0015 4c61 756e 6368 4661 ...2....LaunchFa
00000100: 696c 7572 6545 7869 7443 6f64 6500 0000 ilureExitCode...
00000110: 0130 0000 001b 4c69 6365 6e73 6554 696d .0....LicenseTim
00000120: 6552 656d 6169 6e69 6e67 5365 636f 6e64 eRemainingSecond
00000130: 7300 0000 022d 3100 0000 1557 7261 7070 s....-1....Wrapp
00000140: 6564 4578 6563 7574 6162 6c65 5061 7468 edExecutablePath
00000150: 0000 0047 433a 5c50 726f 6772 616d 2046 ...GC:\Program F
00000160: 696c 6573 2028 7838 3629 5c46 6f6f 4261 iles (x86)\FooBa
00000170: 7220 4761 6d65 735c 466f 6f42 6172 4261 r Games\FooBarBa
00000180: 7a58 595c 4269 6e61 7269 6573 5c46 6f6f zXY\Binaries\Foo
00000190: 4261 7242 617a 582e 6578 6500 0000 0000 BarBazX.exe.....
...
Reformulating this:
{
_opcode: 0x06,
ExecutableProcessId: '8',
Key: 'ZugH4YGvmdKz1k9Ep2wvfMuUBaFLE4gPvZHUW7bAX6oBRNJhp95Drhs...',
LaunchAction: '2',
LaunchFailureExitCode: '0',
LicenseTimeRemainingSeconds: '-1',
WrappedExecutablePath: r'C:\Program Files (x86)\FooBar Games\FooBarBazXY\Binaries\FooBarBazX.exe'
}
It looks like the keys are listed in alphabetical order, suggesting that some complex calculations are going on in the background to regenerate this binary data with every communication.
Unfortunately, the DRMUI.exe application does not terminate here, and simply hangs, as if waiting for more input. It seems like the IPC communication is a multi-stage process.
Building an IPC proxy
It looks like we need to combine our two C scripts to extend our DRMUI.exe stub to ferry messages backwards and forwards between the game binary and DRMUI.exe, so that we can capture the entire IPC exchange.
After a few more false starts than I'd like to admit:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <unistd.h>
int main(int argc, char** argv) {
HANDLE logfile = CreateFileA("ipc_log.bin", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
int ipcid = atoi(argv[1] + 7); /* +7 to get substring */
void* view_in = MapViewOfFile((HANDLE)(intptr_t)ipcid, FILE_MAP_ALL_ACCESS, 0, 0, 0);
HANDLE ipc = CreateFileA("ipc_out.bin", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
/* proxy input */
char buf[4096];
int ipcLen = 0;
DWORD dwBytesRead, dwBytesWritten;
while (1) {
ReadFile((HANDLE)(intptr_t)ipcid, buf, sizeof(buf), &dwBytesRead, NULL);
if (dwBytesRead == 0) {
break;
}
WriteFile(ipc, buf, dwBytesRead, &dwBytesWritten, NULL);
WriteFile(logfile, buf, dwBytesRead, &dwBytesWritten, NULL);
ipcLen += dwBytesRead;
}
/* create mapping */
SECURITY_ATTRIBUTES sa;
memset(&sa, 0, sizeof(SECURITY_ATTRIBUTES));
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
HANDLE ipc_map = CreateFileMappingA(ipc, &sa, PAGE_READWRITE, 0, 0, NULL); /* with bInheritHandle = TRUE */
void* view_ipc = MapViewOfFile(ipc_map, FILE_MAP_ALL_ACCESS, 0, 0, 0);
/* launch file */
char cmdline[4096];
sprintf(cmdline, "DRMUI.exe /HDLID=%d", ipc_map);
STARTUPINFO si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(STARTUPINFO));
si.cb = sizeof(si);
memset(&pi, 0, sizeof(PROCESS_INFORMATION));
CreateProcessA("Z:\\path\\to\\DRMUI.orig.exe", cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); /* bInheritHandles = TRUE */
sleep(1);
DWORD code;
while (1) {
/* proxy */
WriteFile(logfile, view_ipc, ipcLen, &dwBytesWritten, NULL);
memcpy(view_in, view_ipc, ipcLen);
sleep(1);
WriteFile(logfile, view_in, ipcLen, &dwBytesWritten, NULL);
memcpy(view_ipc, view_in, ipcLen);
sleep(1);
GetExitCodeProcess(pi.hProcess, &code);
if (code != STILL_ACTIVE) {
WriteFile(logfile, view_ipc, ipcLen, &dwBytesWritten, NULL);
memcpy(view_in, view_ipc, ipcLen);
break;
}
}
CloseHandle(ipc_map);
CloseHandle(ipc);
CloseHandle(pi.hProcess);
CloseHandle(logfile);
return 0;
}
With this code, we can now capture the next, and final, exchange:
Request:
{
_opcode: 0x0a,
ActivationResponse: '0',
ExecutableProcessId: '8',
Key: 'ZugH4YGvmdKz1k9Ep2wvfMuUBaFLE4gPvZHUW7bAX6oBRNJhp95Drhs...',
LaunchAction: '3',
LaunchFailureExitCode: '0',
LaunchFailureReasonCodeExtra: '',
LicenseTimeRemainingSeconds: '-1',
ReasonCodeResponse: '0',
WrappedExecutablePath: r'C:\Program Files (x86)\FooBar Games\FooBarBazXY\Binaries\FooBarBazX.exe'
}
Response:
{
_opcode: 0x0e,
ActivationResponse: '18',
ExecutableProcessId: '8',
Key: 'ZugH4YGvmdKz1k9Ep2wvfMuUBaFLE4gPvZHUW7bAX6oBRNJhp95Drhs...',
LaunchAction: '1',
LaunchFailureExitCode: '0',
LaunchFailureReasonCodeExtra: '',
LicenseTimeRemainingSeconds: '-1',
ReasonCodeResponse: '0',
UIThreadTerminated: 'true',
WrappedExecutablePath: r'C:\Program Files (x86)\FooBar Games\FooBarBazXY\Binaries\FooBarBazX.ex' + '\0'
}
(The final character of WrappedExecutablePath seems to be cut off for some reason, but everything otherwise looks correct.)
So it looks like DRMUI.exe processes all the .bar and .lic files from the previous parts, checks that everything is in order, then simply passes the encryption key for the game data back to the game binary. Further investigation similar to that in part 2 reveals that this is also an AES-128-CBC key – Activation.dll decrypts the import table and game code, reconstructs the import address table, and jumps to the main game entry point.
Putting everything together
In contrast with the reasonably-heavily secured .lic file from earlier, this key channel in the DRM system, DRMUI.exe and its IPC communication with the main game binary, is totally insecure. There are no integrity checks for DRMUI.exe, and the communications are encrypted with a very simple XOR cipher.
Furthermore, as it turns out, FooBarBazX.exe doesn't even particularly care what goes on in the IPC communication, and will happily proceed once receiving the final IPC communication (with opcode 0x0e). Because there are no authentication mechanisms within the IPC protocol, this makes the whole process vulnerable to a simple replay attack. (Indeed, given that the only variable thing in the IPC is the decryption key, which can be extracted from the .lic file, we could generate a valid IPC communication de novo.)
We write an extremely simple 20-line C program to replace DRMUI.exe, simply passing the game binary a copy of the final IPC communication, and hey presto, the game launches without a hiccup.
41 megabytes of DRM down to 20 lines of code. Not bad!