Investigating an early-2010s gaming DRM system: Part 3
Last time, we investigated how an early-2010s gaming DRM system stored licences for games. This time, we'll investigate how those licences are tied to particular devices.
From last time, we know that the licence file contains an encrypted XML payload:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<License xmlns="http://foobar/license">
<CipherKey>ZugH4YGvmdKz1k9Ep2wvfMuUBaFLE4gPvZHUW7bAX6oBRNJhp95Drhs+RmgGNAbzNW6lpcRQxPB7+g256HPdoE9ZBKFYyV8O0x/SSlyuk/Usq+U32xXyhsY3MErUDSLhubwb8dwjvD998cQ2yWKpwzdZtQeJ2XuP1uUYrY4UIQ==</CipherKey>
<MachineHash>49c36d61ff9ee6e08463cd2a815cb194f42a76a8</MachineHash>
<ContentId>2114455</ContentId>
<UserId>1234567890123</UserId>
<GrantTime>2018-12-02T12:34:56Z</GrantTime>
<StartTime>2018-12-02T12:34:56Z</StartTime>
</License>
The MachineHash in this licence file is oestensibly some sort of hardware fingerprint to tie the licence to a particular computer. So how is this fingerprint calculated?
String analysis
Looking through the strings in the DRMUI.exe file, we find some references to ‘GetMachineID’ and some other interesting strings:
ROOT\CIMV2, Win32_OperatingSystem, Win32_BIOS and Win32_BaseBoard are strings relating to WMI (Windows Management Instrumentation). Select * from
is, then, a portion of a query in WQL (Windows Management Instrumentation Query Language).
However, there are no obvious references to these particular strings from within the code. The actual WMI queries must be obfuscated in some way. There is, however, a reference to the GetMachineID string in sub_439E60:
This is a large subroutine which references some promising-looking strings. But where in this subroutine are the WMI queries? We can again turn to winedbg and WINEDEBUG
to find out.
Identifying WMI queries in winedbg
Firstly, though, we need to identify the debug channel winedbg uses for WMI queries, as this is not documented on the WineHQ debug channels page. To do this, we can run a query in SimpleWMIView using WINEDEBUG=+all
, and look for WMI-related debug output. This results in lots of junk output to sift through, but we eventually find:
00a3:trace:wbemprox:DllGetClassObject {4590f811-1d3a-11d0-891f-00aa004b2e24} {00000001-0000-0000-c000-000000000046} 0x3381b8
00a3:trace:wbemprox:wbemprox_cf_CreateInstance (nil) {dc12a687-737f-11cf-884d-00aa004b2e24} 0x3381b4
...
00a3:trace:wbemprox:wbem_locator_ConnectServer 0x1af2f0, L"root\\CIMV2", (null), (null), (null), 0x00000000, (null), (nil), 0x3382b4)
...
00a3:trace:wbemprox:wbem_services_GetObject 0x1c8c80, L"Win32_OperatingSystem", 0x00000000, (nil), 0x33828c, (nil)
...
00a3:trace:wbemprox:create_class_object L"Win32_OperatingSystem", 0x33828c
...
00a3:trace:wbemprox:wbem_services_ExecQuery 0x1c8c80, L"WQL", L"SELECT * FROM Win32_OperatingSystem", 0x00000030, (nil), 0x3382b8
So it looks like the debug channel of note is wbemprox.
Locating the WMI queries
Unfortunately, it does not appear possible to set breakpoints inside any of these wbemprox functions. However, using the debug output as a guide, we can step through sub_439E60 using WINEDEBUG=-all,+wbemprox
and find where the calls to wbemprox are being executed.
Gradually stepping through the subroutine, we identify that all the queries are happening inside a call to sub_439550:
This is a large, fairly linear function, with a number of similar-looking parts.
Stepping through this function, we find that the WMI queries are being performed in a small chunk at the top of the function, with a separate subroutine for each:
$ WINEDEBUG=-all,+wbemprox winedbg FooBarBazX.exe
WineDbg starting on pid 00ab
0x000000007b465d91: movl 0xffffff24(%ebp),%esi
Wine-dbg>c
0x000000007b465d91: movl 0xffffff24(%ebp),%esi
Wine-dbg>break *0x439631
Breakpoint 1 at 0x0000000000439631
Wine-dbg>c
00c5:trace:wbemprox:DllGetClassObject {4590f811-1d3a-11d0-891f-00aa004b2e24} {00000001-0000-0000-c000-000000000046} 0x8dd1f8
00c5:trace:wbemprox:wbemprox_cf_CreateInstance (nil) {dc12a687-737f-11cf-884d-00aa004b2e24} 0x8dd1f4
00c5:trace:wbemprox:WbemLocator_create (0x8dd198)
00c5:trace:wbemprox:WbemLocator_create returning iface 0x18aa80
00c5:trace:wbemprox:wbem_locator_QueryInterface 0x18aa80 {dc12a687-737f-11cf-884d-00aa004b2e24} 0x8dd1f4
00c5:trace:wbemprox:wbem_locator_ConnectServer 0x18aa80, L"ROOT\\CIMV2", (null), (null), (null), 0x00000000, (null), (nil), 0x8dd450)
00c5:trace:wbemprox:WbemServices_create (0x8dd450)
00c5:trace:wbemprox:WbemServices_create returning iface 0x18aab0
00c5:trace:wbemprox:wbem_services_QueryInterface 0x18aab0 {0000013d-0000-0000-c000-000000000046} 0x8dd268
00c5:fixme:wbemprox:client_security_SetBlanket 0x7c6ff938, 0x18aab0, 10, 0, (null), 3, 3, (nil), 0x00000000
00c5:fixme:wbemprox:client_security_Release 0x7c6ff938
Stopped on breakpoint 1 at 0x0000000000439631
Wine-dbg>info reg
Register dump:
CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
EIP:00439631 ESP:008dd438 EBP:00000001 EFLAGS:00000202( - -- I - - - )
EAX:0018aab0 EBX:00000000 ECX:008dd784 EDX:00000000
ESI:000006da EDI:0000000f
Wine-dbg>break *0x439636
Breakpoint 2 at 0x0000000000439636
Wine-dbg>c
00c5:trace:wbemprox:wbem_services_ExecQuery 0x18aab0, L"WQL", L"Select * from Win32_BaseBoard", 0x00000030, (nil), 0x8dd148
00c5:trace:wbemprox:grab_table returning 0x7c6ff320
00c5:trace:wbemprox:parse_query wql_parse returned 0
00c5:trace:wbemprox:EnumWbemClassObject_create 0x8dd148
00c5:trace:wbemprox:EnumWbemClassObject_create returning iface 0x18bdc0
00c5:trace:wbemprox:enum_class_object_Next 0x18bdc0, -1, 1, 0x8dd150, 0x8dd154
00c5:trace:wbemprox:create_class_object L"Win32_BaseBoard", 0x8dd150
00c5:trace:wbemprox:create_class_object returning iface 0x18bdd8
00c5:trace:wbemprox:class_object_Get 0x18bdd8, L"SerialNumber", 00000000, 0x8dd138, (nil), (nil)
00c5:trace:wbemprox:class_object_Release destroying 0x18bdd8
00c5:trace:wbemprox:enum_class_object_Release destroying 0x18bdc0
Stopped on breakpoint 2 at 0x0000000000439636
Wine-dbg>x/16b 0x008dd784
0x00000000008dd784: 4e 00 6f 00 6e 00 65 00 00 00 31 00 00 00 6d 00
Wine-dbg>x/u 0x008dd784
None
Wine-dbg>
Each of these subroutines appears to be obfuscated, containing a number of nonsense magic numbers. Thankfully, however, this is irrelevant, since the debugging output tells us what is being looked up.
Armed with this information, we can begin labelling each of the local variables in the function, as above. Here, wmi_baseb_sn, etc., will contain the UTF-16 strings returned from the WMI interface.
Putting together the hardware information
The next reference to these variables is a few lines down:
This code loops through wmi_baseb_manuf, inspecting every other byte until reaching a null byte, and incrementing ecx each iteration. Clearly, this is a naïve strlen (wcslen) implementation, counting the number of characters in wmi_baseb_manuf.
Next we have:
This code calls fn_437D30_concat with arguments being a new pointer (fingerp_str), a constant (0x6DA), wmi_baseb_manuf and the previously-calculated length of wmi_baseb_manuf. By stepping through this code, we see that this subroutine copies wmi_baseb_manuf into fingerp_str:
Wine-dbg>break *0x43970c
Breakpoint 3 at 0x000000000043970c
Wine-dbg>c
...
Stopped on breakpoint 3 at 0x000000000043970c
Wine-dbg>info reg
Register dump:
CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
EIP:0043970c ESP:008dd434 EBP:00000001 EFLAGS:00000246( - -- I Z- -P- )
EAX:008dd6de EBX:00000000 ECX:008dd6bc EDX:008ddf38
ESI:000006da EDI:0000000f
Wine-dbg>x/16b 0x008ddf38
0x00000000008ddf38: 00 00 00 00 77 be d9 f7 07 00 00 00 52 28 cb 7b
Wine-dbg>si
0x0000000000437d30: movl 0xc(%esp),%ecx
Wine-dbg>fin
0x0000000000439711: movl 0xb00(%esp),%esi
Wine-dbg>x/16b 0x008ddf38
0x00000000008ddf38: 49 00 6e 00 74 00 65 00 6c 00 20 00 43 00 6f 00
Wine-dbg>x/u 0x008ddf38
Intel Corporation
Wine-dbg>
Looking at the code, note that eax is now a pointer to the end of fingerp_str. Therefore, we can see that the code then calculates the length in bytes of fingerp_str (sub eax, ecx
), divides it by 2 to get the number of characters (sar eax, 1
), and updates the local variable fingerp_length with this value.
The code then checks if fingerp_length >= 0x6DA, and if so, skips the next block, which is:
This code adds 0x3B (ASCII ;
) to the end of fingerp_str, and increments fingerp_length accordingly. Based on the comparison, it would appear that 0x6DA is the maximum length for fingerp_str.
These blocks, adding strings to the end of fingerp_str and adding semicolons, then repeats for wmi_baseb_sn, wmi_bios_manuf, wmi_bios_sn, wmi_os_install and wmi_os_sn. At the end of the process, we have, as expected:
Wine-dbg>break *0x43999b
Breakpoint 4 at 0x000000000043999b
Wine-dbg>c
Stopped on breakpoint 4 at 0x000000000043999b
Wine-dbg>x/u 0x008ddf38
Intel Corporation;None;The Wine Project;0;20140101000000.000000+000;12345-OEM-1234567-12345;
Wine-dbg>
Next we have a larger block of code:
This code begins by calling sub_438100, passing it 4 arguments:
- fingerp_str
- a pointer to the end of fingerp_str (fingerp_str + 2 × fingerp_str_length)
- a new pointer (fingerp_str_utf8)
- the value fingerp_str_utf8 + 0x6DA (if our presumption about 0x6DA being the maximum length is correct, a pointer to the maximum end for the fingerp_str_utf8 string)
sub_438100 in turn calls sub_437DE0, which is a fairly large function that contains comparisons to a number of values, like 0x800, 0xFFFF, 0x1FFFFF, 0x3FFFFFF and 0x7FFFFFFF:
These seem to be Unicode-related numbers, determining the number of bytes to use in UTF-8 encoding. Stepping through the function, it appears that this function converts the UTF-16 fingerp_str into UTF-8 at fingerp_str_utf8. The null terminator is added back in sub_439550 (mov [ecx], bl
):
Wine-dbg>break *0x4399d2
Breakpoint 5 at 0x00000000004399d2
Wine-dbg>c
Stopped on breakpoint 5 at 0x00000000004399d2
Wine-dbg>info reg
Register dump:
CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
EIP:004399d2 ESP:008dd434 EBP:00000001 EFLAGS:00000207( - -- I - -P-C)
EAX:008dd460 EBX:00000000 ECX:008dd454 EDX:008ddff0
ESI:00000696 EDI:0000000f
Wine-dbg>x/x $ecx
008dd854
Wine-dbg>x/16b 0x008dd854
0x00000000008dd854: 00 00 16 00 26 00 00 00 00 11 70 29 cf 03 6c fd
Wine-dbg>si
0x0000000000438100: pushl %ebx
Wine-dbg>fin
0x00000000004399d7: movl 0x20(%esp),%ecx
Wine-dbg>si
0x00000000004399db: movb %bl,0x0(%ecx)
Wine-dbg>si
0x00000000004399dd: addl $16,%esp
Wine-dbg>x/16b 0x008dd854
0x00000000008dd854: 49 6e 74 65 6c 20 43 6f 72 70 6f 72 61 74 69 6f
Wine-dbg>x/s 0x008dd854
Intel Corporation;None;The Wine Project;0;20140101000000.000000+000;12345-OEM-1234567-12345;
Wine-dbg>
Next we again have a straightforward strlen implementation for fingerp_str_utf8, this time checking every byte because the data is now in UTF-8:
Next we have another chunk of code that, stepping through the code and inspecting memory addresses, grabs some non-WMI information:
Using WINEDEBUG=+all
, we can determine that sub_438790 calls EnumDisplayDevicesW, but the exact function of sub_438150 remains unclear.
We then have a fairly convoluted chunk of code:
This computes the length of the string returned from sub_438150, and appends it to fingerp_str_utf8 (observing the maximum length of 0x6DA), adding semicolons and null terminators, and updating the relevant length variables.
This code essentially repeats for the result from EnumDisplayDevicesW.
The next block of code, inspecting the relevant values returned in the debugger, obviously gets some CPU information:
This is followed by more generic string length and concatenation operations.
And finally, the result is copied to the destination buffer passed as an argument, and the function returns:
Inspecting the final value of fingerp_str_utf8:
Wine-dbg>fin
0x0000000000439f0f: addl $8,%esp
Wine-dbg>x/s 0x008dd854
Intel Corporation;None;The Wine Project;0;20140101000000.000000+000;12345-OEM-1234567-12345;0;PCI\VEN_0000&DEV_0000;GenuineIntel;bfebfbff;7ffafbff;Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz;
Wine-dbg>
And, indeed, this matches with the MachineHash from the licence file:
$ echo -n 'Intel Corporation;None;The Wine Project;0;20140101000000.000000+000;12345-OEM-1234567-12345;0;PCI\VEN_0000&DEV_0000;GenuineIntel;bfebfbff;7ffafbff;Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz;' | shasum
49c36d61ff9ee6e08463cd2a815cb194f42a76a8 -
Aside: Validity of data returned
Running under Wine, all values returned are obviously generic placeholders with little fingerprinting value; however, the CPU information appears to be valid. GenuineIntel
is the CPU vendor_id and Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
is the CPU model name:
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
...
bfebfbff
and 7ffafbff
seem to be derived from the raw CPUID information, specifically the ecx and edx feature flags from eax = 1 (Processor Info and Feature Bits):
$ cpuid -1 -r
CPU:
0x00000000 0x00: eax=0x00000016 ebx=0x756e6547 ecx=0x6c65746e edx=0x49656e69
0x00000001 0x00: eax=0x000906e9 ebx=0x00100800 ecx=0x7ffafbff edx=0xbfebfbff
...
Next part
Investigating the DRM system's interaction with the game binary