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@winehq.org Reporter: focht@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@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/e377786a71c3b6eab5bc11c0b1c9c7c3...
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