[Bug 50417] New: Multiple game launchers protected by Game Protect Kit (GPK) crash on startup (dummy PEB->KernelCallbackTable needed)(Dragon Nest, Age of Wushu)

WineHQ Bugzilla wine-bugs at winehq.org
Mon Dec 28 14:45:52 CST 2020


https://bugs.winehq.org/show_bug.cgi?id=50417

            Bug ID: 50417
           Summary: Multiple game launchers protected by Game Protect Kit
                    (GPK) crash on startup (dummy PEB->KernelCallbackTable
                    needed)(Dragon Nest, Age of Wushu)
           Product: Wine
           Version: 6.0-rc4
          Hardware: x86-64
                OS: Linux
            Status: NEW
          Severity: normal
          Priority: P2
         Component: ntdll
          Assignee: wine-bugs at winehq.org
          Reporter: focht at gmx.net
      Distribution: ---

Hello folks,

there already exist a small number of bug reports for these games. For example
bug 27716 ("Dragon Nest (Chinese): crashes before login screen") from 2011. I'm
pretty sure the original problem(s) can't be reproduced anymore since the
launcher and protection technology evolved a lot (almost 10 years). 

I'm not going to bury my analysis in questionable/messed up bug reports. I did
that mistake many times before.

Anyway, lets hop into this interesting one which took some hours to figure out.

NOTE: When starting the client installer it churns CPU for ~2 minutes until
actually starts installing (extracting files). Just be patient.

--- snip ---
$ pwd
/home/focht/.wine/drive_c/Program Files (x86)/DN/DragonNest

$ WINEDEBUG=+seh,+loaddll,+relay wine ./DNLauncher.exe >>log.txt 2>&1
...
0020:0024:Call KERNEL32.LoadLibraryW(0121d3e8 L"winmm.dll") ret=0309df3c
0020:0024:Ret  KERNEL32.LoadLibraryW() retval=01b70000 ret=0309df3c
0020:0024:Call KERNEL32.GetModuleFileNameW(01b70000,0121d11c,00000104)
ret=02f7fec2
0020:0024:Ret  KERNEL32.GetModuleFileNameW() retval=0000001d ret=02f7fec2
0020:0024:Call KERNEL32.CreateFileW(0121d11c
L"C:\\windows\\system32\\WINMM.dll",80000000,00000001,00000000,00000003,00000080,00000000)
ret=030a879b
0020:0024:Ret  KERNEL32.CreateFileW() retval=000000fc ret=030a879b
0020:0024:Call KERNEL32.GetFileSize(000000fc,0121d118) ret=02f329ff
0020:0024:Ret  KERNEL32.GetFileSize() retval=000ae000 ret=02f329ff
0020:0024:Call KERNEL32.VirtualAlloc(00000000,000ae000,00001000,00000040)
ret=0306b045
0020:0024:Ret  KERNEL32.VirtualAlloc() retval=03480000 ret=0306b045
0020:0024:Call KERNEL32.ReadFile(000000fc,03480000,000ae000,0121d118,00000000)
ret=02eb9651
0020:0024:Ret  KERNEL32.ReadFile() retval=00000001 ret=02eb9651
0020:0024:Call KERNEL32.CloseHandle(000000fc) ret=0300e8bd
0020:0024:Ret  KERNEL32.CloseHandle() retval=00000001 ret=0300e8bd
0020:0024:Call KERNEL32.VirtualFree(03480000,00000000,00008000) ret=03091019
0020:0024:Ret  KERNEL32.VirtualFree() retval=00000001 ret=03091019
...
--- snip ---

GPK does various consistency checks for loaded modules. The above log snippet
is repeated for a client-coded list of dlls. Since Wine evolved a lot this
year, these kind of checks no longer a problem (bug 15437 et al.)

Disassembly just for documentation in case GPK chokes on one of the few non-PE
"fake" Wine dlls which is currently not the case.

--- snip ---
0300E8CB | bt ax,si                          |
0300E8CF | sbb ax,bx                         |
0300E8D2 | push 8000                         |
0300E8D7 | btr eax,ecx                       |
0300E8DA | and ah,E4                         |
0300E8DD | shrd ax,dx,52                     |
0300E8E2 | push 0                            |
0300E8E4 | test ax,6289                      |
0300E8E8 | ror ax,9B                         |
0300E8EC | sbb ah,ch                         |
0300E8EE | mov eax,dword ptr ds:[ecx+esi+8]  | PE->TimeDateStamp disk image
0300E8F2 | test dh,E7                        |
0300E8F5 | stc                               |
0300E8F6 | push esi                          |
0300E8F7 | cmp eax,dword ptr ds:[edx+ebx+8]  | PE->TimeDateStamp loaded dll
0300E8FB | jmp gt.3051676                    |
...
03091004 | mov eax,dword ptr ds:[ecx+esi+58] | PE->CheckSum disk image
03091008 | cmc                               |
03091009 | cmp eax,dword ptr ds:[edx+ebx+58] | PE->CheckSum loaded dll
0309100D | jne gt.305167C                    |
--- snip ---

Then we arrive at this place:

--- snip ---
...
0024:Call KERNEL32.GetModuleHandleW(0121cfdc
L"C:\\windows\\system32\\kernel32.dll") ret=03058688
0024:Ret  KERNEL32.GetModuleHandleW() retval=7b600000 ret=03058688
0024:Call KERNEL32.VirtualAlloc(00000000,00001000,00203000,00000040)
ret=02fc7281
0024:Call
ntdll.NtAllocateVirtualMemory(ffffffff,0121cf6c,00000000,0121cf70,00203000,00000040)
ret=7b0296c1
0024:Ret  ntdll.NtAllocateVirtualMemory() retval=00000000 ret=7b0296c1
0024:Ret  KERNEL32.VirtualAlloc() retval=02290000 ret=02fc7281
0024:trace:seh:dispatch_exception code=c0000005 flags=0 addr=02E5997A
ip=02e5997a tid=0024
0024:trace:seh:dispatch_exception  info[0]=00000000
0024:trace:seh:dispatch_exception  info[1]=00000000
0024:trace:seh:dispatch_exception  eax=00001000 ebx=7ffde000 ecx=00001000
edx=00001000 esi=00000000 edi=02290000
0024:trace:seh:dispatch_exception  ebp=0121d414 esp=0121cfb4 cs=0023 ds=002b
es=002b fs=0063 gs=006b flags=00210207
0024:trace:seh:call_vectored_handlers calling handler at 7B00F270 code=c0000005
flags=0
0024:trace:seh:call_vectored_handlers handler at 7B00F270 returned 0
0024:trace:seh:call_stack_handlers calling handler at 02E6E52C code=c0000005
flags=0
0024:Call KERNEL32.GetLastError() ret=02e5d5b5
0024:Ret  KERNEL32.GetLastError() retval=00000000 ret=02e5d5b5
...
wine: Unhandled page fault on read access to 00000000 at address 02E5997A
(thread 0024), starting debugger...
--- snip ---

Disassembly of crash site:

--- snip ---
02E59950 | push edi                      |
02E59951 | push esi                      |
02E59952 | mov esi,dword ptr ss:[esp+10] | NULL = src buf
02E59956 | mov ecx,dword ptr ss:[esp+14] | 0x1000 = copy count
02E5995A | mov edi,dword ptr ss:[esp+C]  | 02290000 = dest buf
02E5995E | mov eax,ecx                   |
02E59960 | mov edx,ecx                   |
02E59962 | add eax,esi                   |
02E59964 | cmp edi,esi                   |
02E59966 | jbe gt.2E59970                |
02E59968 | cmp edi,eax                   |
02E5996A | jb gt.2E59CD8                 |
02E59970 | bt dword ptr ds:[2EAA834],1   |
02E59978 | jae gt.2E59981                |
02E5997A | rep movsb                     |
02E5997C | jmp gt.2E59C98                |
02E59981 | cmp ecx,80                    |
02E59987 | jb gt.2E59B5B                 |
02E5998D | mov eax,edi                   |
02E5998F | xor eax,esi                   |
02E59991 | test eax,F                    |
...
02E59C98 | mov eax,dword ptr ss:[esp+C]  |
02E59C9C | pop esi                       |
02E59C9D | pop edi                       |
02E59C9E | ret                           |
--- snip ---

Unlike the crash site, most GPK code is heavily obfuscated which makes
debugging a lot more annoying.

I've noticed in one of the earlier checks (gazillion instructions before this
crash) that some PEB fields were of interest to GPK. Instead of spending hours
on debugging non-linear code flows, lots of asm continuations and virtualized
code/registers let the debugger do more work by using some advanced
functionality of debuggers.

Since hardware breakpoints are fast but limited in size I used the much slower
guard page based memory breakpoint type along with a complex break / log
condition on the PEB page.

Address=0x7FFDE000 (PEB)
Range=0x1000 (page)
Condition=(breakif(0), logif(EIP > 0x2E30000 && EIP < 0x10000000, "PEB access
to  {$breakpointexceptionaddress} from {EIP}")

What it basically does: the breakpoint triggers on any PEB read access but only
logs if the memory access came from a certain EIP range (where the protection
module is usually mapped), re-arms the guard page and resumes execution.

Turns out being quite unstable, leading to abrupt (silent) process termination
depending when it was armed. I've yet to investigate why. Thread termination /
APC handling seems to play a role.

Anyway, by using a combination of two breakpoints, one for breaking when a
specific dll was loaded and then arming the complex memory breakpoint I got
lucky one time:

--- snip ---
...
DebugString: "FileName:"
DebugString: "C:\Program Files (x86)\DN\DragonNest\gpk\Sddyn_01.dll"
DebugString: "MD5 Value:"
DebugString: "BE47074011E4A04B8C7106EC4FA892EB"
DebugString: "Download MD5 Value:"
DebugString: "BE47074011E4A04B8C7106EC4FA892EB"
DebugString: "Downloaded MD5 Value:"
DebugString: "BE47074011E4A04B8C7106EC4FA892EB"
DebugString: "Download MD5 Value:"
DebugString: "BE47074011E4A04B8C7106EC4FA892EB"
DebugString: "FileName:"
DebugString: "C:\Program Files (x86)\DN\DragonNest\gpk\splash.jpg"
DebugString: "MD5 Value:"
DebugString: "1E0DBD851EB8E88F05D699799BAD7963"
DebugString: "Download MD5 Value:"
DebugString: "1E0DBD851EB8E88F05D699799BAD7963"
DebugString: "Downloaded MD5 Value:"
DebugString: "1E0DBD851EB8E88F05D699799BAD7963"
DebugString: "Download MD5 Value:"
DebugString: "1E0DBD851EB8E88F05D699799BAD7963"
DLL Unloaded: 02250000 gpkup.dll
DLL Loaded: 02250000
Z:\home\focht\projects\wine\mainline-install-6.0-rc4-x86_64\lib\wine\psapi.dll
DLL Loaded: 02E30000 C:\Program Files (x86)\DN\DragonNest\GPK\GT.dll
Breakpoint at 02274080 (DllMain (wintrust.dll)) set!
DLL Loaded: 02260000
Z:\home\focht\projects\wine\mainline-install-6.0-rc4-x86_64\lib\wine\wintrust.dll
DLL Breakpoint (DLL Load and unload): Module wintrust.dll
INT3 breakpoint "DllMain (wintrust.dll)" at <wintrust._DllMainCRTStartup at 12>
(02274080)!
Memory breakpoint enabled!
PEB access to 7FFDE02C from 309F6D4
--- snip ---

Disassembly of the offender:

--- snip ---
...
0309F6D0 | mov edx,dword ptr ss:[ebp] | 0x7FFDE02C
0309F6D4 | mov eax,dword ptr ds:[edx] | PEB->KernelCallbackTable
0309F6D6 | add ch,D8                  |
0309F6D9 | btr cx,di                  |
0309F6DD | mov dword ptr ss:[ebp],eax |
0309F6E1 | mov ecx,dword ptr ds:[edi] |
0309F6E3 | jmp gt.307BD90             |
--- snip ---

The pointer was stored in some rather obfuscated place and later ended up as
parameter for the function call that caused the crash.

Wine doesn't implement the concept of 'KernelCallbackTable' hence the field is
NULL by design.

--- snip ---
$ ==>    7FFDE000  00010000
$+4      7FFDE004  00000000
$+8      7FFDE008  00400000
$+C      7FFDE00C  7BC6D2B4 <&ldr>
$+10     7FFDE010  00113430
$+14     7FFDE014  00000000
$+18     7FFDE018  00110000
$+1C     7FFDE01C  7BC6D2E4 <&peb_lock>
$+20     7FFDE020  00000000
$+24     7FFDE024  00000000
$+28     7FFDE028  00000000
$+2C     7FFDE02C  00000000 ; KernelCallbackTable
$+30     7FFDE030  00000000
$+34     7FFDE034  00000000
$+38     7FFDE038  00000000
$+3C     7FFDE03C  00000000
$+40     7FFDE040  7BC6ED64 <&tls_bitmap>
$+44     7FFDE044  0000001F
$+48     7FFDE048  00000000
$+4C     7FFDE04C  00000000
$+50     7FFDE050  00000000
$+54     7FFDE054  00000000
--- snip ---

There are a lot of mysteries and articles about the purpose of this table on
the Internet. Fortunately there is no need to dive into internals /
implementation details. Although it's useful know that 'user32.dll' initializes
the table and provides a number of callbacks to be called from the kernel side.

https://source.winehq.org/git/wine.git/blob/e377786a71c3b6eab5bc11c0b1c9c7c3dc309398:/dlls/ntdll/loader.c#l3961

For testing purpose I allocated a page in 'process_init' and set the pointer in
'PEB->KernelCallbackTable'. Note, this is not really correct but the protection
doesn't seem to check/care if the table address belongs to 'user32.dll' module. 

Furthermore I used my favorite 0xcafebabe pattern to initialize the table
entries. The "magic" pattern serves two purposes:

* allows to determine if any of the entries has been overwritten
* allows to easier identify invocations of callbacks

The latter one is not applicable here since Wine doesn't implement
'KernelCallbackTable' concept. It's still handy in other scenarios.

By using memory (guard page) breakpoints on the newly allocated
'KernelCallbackTable' buffer one can figure out who wants to peek there and for
what reason. Turns out GPK makes a copy of the existing table, hooks one entry
and then sets 'PEB->KernelCallbackTable' to the copy.

--- snip ---
02FA4C6C | test ebp,ebx               | EAX = 02290000 = copy
02FA4C6E | mov dword ptr ds:[edx],eax | EDX = 7FFDE02C (KernelCallbackTable)
02FA4C70 | bt ax,sp                   |
02FA4C74 | stc                        |
02FA4C75 | mov eax,dword ptr ds:[edi] |
02FA4C77 | stc                        |
02FA4C78 | add edi,4                  |
02FA4C7E | test dx,bp                 |
02FA4C81 | xor eax,ebx                |
02FA4C83 | jmp gt.2F46E4E             |
--- snip ---

A memory dump reveals that our nice babe has been "tainted" by a bad guy _oO_

--- snip ---
$ ==>    02290000    CAFEBABE
$+4      02290004    CAFEBABE
$+8      02290008    CAFEBABE
$+C      0229000C    CAFEBABE
$+10     02290010    CAFEBABE
$+14     02290014    CAFEBABE
$+18     02290018    CAFEBABE
$+1C     0229001C    CAFEBABE
...
$+100    02290100    CAFEBABE
$+104    02290104    CAFEBABE
$+108    02290108    02E44920 ; GPK callback hook
$+10C    0229010C    CAFEBABE
$+110    02290110    CAFEBABE
...
$+FF8    02290FF8    CAFEBABE
$+FFC    02290FFC    CAFEBABE
--- snip ---

Table entry 0x0108 seems to be of interest to GPK.

The handler is a very long chain of obfuscated code / asm continuations. But
that doesn't matter as of now. It seems the launcher is fine with just being
able to set up a modified copy. Maybe it expects the callback being called at
one point but I ran into couple of other issues.

--- snip ---
02E44920 | E9 17562100      | jmp gt.3059F3C                 |
...
03059F3C | 55               | push ebp                       |
03059F3D | 66:C1C5 9A       | rol bp,9A                      |
03059F41 | 9F               | lahf                           |
03059F42 | 8BEC             | mov ebp,esp                    |
03059F44 | F5               | cmc                            |
03059F45 | 6A FE            | push FFFFFFFE                  |
03059F47 | C1E8 32          | shr eax,32                     |
03059F4A | 66:23C2          | and ax,dx                      |
03059F4D | 68 A860FC02      | push gt.2FC60A8                |
03059F52 | 66:0FB6C0        | movzx ax,al                    |
03059F56 | 66:0FA4E0 E9     | shld ax,sp,E9                  |
03059F5B | 0FBCC7           | bsf eax,edi                    |
03059F5E | 68 B097E502      | push gt.2E597B0                |
03059F63 | 0AE2             | or ah,dl                       |
03059F65 | F7C6 D4530E53    | test esi,530E53D4              |
03059F6B | 64:A1 00000000   | mov eax,dword ptr fs:[0]       |
03059F71 | 66:3BFD          | cmp di,bp                      |
03059F74 | F8               | clc                            |
03059F75 | 80F9 B2          | cmp cl,B2                      |
03059F78 | 50               | push eax                       |
03059F79 | 83EC 38          | sub esp,38                     |
03059F7C | C0E8 BF          | shr al,BF                      |
03059F7F | C1F0 48          | shl eax,48                     |
03059F82 | 53               | push ebx                       |
03059F83 | 56               | push esi                       |
03059F84 | 0FABE0           | bts eax,esp                    |
03059F87 | C0D0 A1          | rcl al,A1                      |
03059F8A | 80E4 81          | and ah,81                      |
03059F8D | 57               | push edi                       |
03059F8E | C0E4 AC          | shl ah,AC                      |
03059F91 | A1 207CEA02      | mov eax,dword ptr ds:[2EA7C20] |
03059F96 | 3145 F8          | xor dword ptr ss:[ebp-8],eax   |
03059F99 | 66:0FBAF7 3B     | btr di,3B                      |
03059F9E | E9 31040000      | jmp gt.305A3D4                 |
...
--- snip ---

By providing a dummy table, the crash is prevented and the launcher runs a bit
further.

It later fails to start a kernel service which seems to be bug 49346. Although
I think the driver issue is now different (half a year later).

--- snip ---
...
00fc:Call KERNEL32.CreateFileW(02e6f9e0
L"\\\\.\\SDGGameLoader",00000000,00000003,00000000,00000003,00000000,00000000)
ret=02ffe7ab
...
00fc:Ret  KERNEL32.CreateFileW() retval=ffffffff ret=02ffe7ab 
...
00fc:Call KERNEL32.CopyFileW(0121baec L"C:\\Program Files
(x86)\\DN\\DragonNest\\GPK\\gpe2.e",0121c8fc L"C:\\Program Files
(x86)\\DN\\DragonNest\\GPK\\SDGame32.sys",00000000) ret=02fb385f 
...
00fc:Ret  KERNEL32.CopyFileW() retval=00000001 ret=02fb385f
...
00fc:Call advapi32.CreateServiceW(00193668,02e6fe00 L"SDGame32",02e6fe00
L"SDGame32",000f01ff,00000001,00000003,00000001,0121c8fc L"C:\\Program Files
(x86)\\DN\\DragonNest\\GPK\\SDGame32.sys",00000000,00000000,00000000,00000000,00000000)
ret=02fa6372 
...
--- snip ---

Anyway, that would be another story, another day.

To be honest I think making GPK (Game Protect Kit) working with Wine will be
hard, if at all. It's uses pretty much the same techniques like a rootkit /
ring0 malware with kernel and userspace parts.

===

Downloads:

Small "web" downloader:

http://dn.clientdown.sdo.com/Dn_Download/DN_407_downloader_signed.exe

Full client:

http://dn.clientdown.sdo.com/Ver.407Full/DragonNest_v407_Setup.exe
http://dn.clientdown.sdo.com/Ver.407Full/DragonNest_v407.7z.001
http://dn.clientdown.sdo.com/Ver.407Full/DragonNest_v407.7z.002
http://dn.clientdown.sdo.com/Ver.407Full/DragonNest_v407.7z.003

---

$ sha1sum DN_407_downloader_signed.exe 
a42ec8020a3301f621806423154eb69153727a48  DN_407_downloader_signed.exe

$ du -sh DN_407_downloader_signed.exe 
3.6M    DN_407_downloader_signed.exe

$ sha1sum DragonNest_v407*
833939e2f029e6ec4b20a1048901742087ac24a2  DragonNest_v407.7z.001
9b94d45f95b3e145f1a370b76d51cee9676395f0  DragonNest_v407.7z.002
f2b46a763099848f8e26253811ebc4caf336c11f  DragonNest_v407.7z.003
4afc1de3968cf4f3c710a11b7be83f18cb0353d8  DragonNest_v407_Setup.exe

$ du -sh DragonNest_v407*
4.0G    DragonNest_v407.7z.001
4.0G    DragonNest_v407.7z.002
2.2G    DragonNest_v407.7z.003
9.5M    DragonNest_v407_Setup.exe

$ wine --version
wine-6.0-rc4

Regards

-- 
Do not reply to this email, post in Bugzilla using the
above URL to reply.
You are receiving this mail because:
You are watching all bug changes.


More information about the wine-bugs mailing list