https://bugs.winehq.org/show_bug.cgi?id=36486
Bug ID: 36486 Summary: Age of Wushu: frequent display of in-game "Don't speed up!" dialog tip disrupts gameplay (Wine fails kernel32.GetTickCount API entry hook check) Product: Wine Version: 1.7.19 Hardware: x86 OS: Linux Status: NEW Severity: normal Priority: P2 Component: kernel32 Assignee: wine-bugs@winehq.org Reporter: focht@gmx.net
Hello folks,
during investigation of bug 36465 I found this goodie, deserving it's own bug of course :)
Searching the net reveals similar reports for Windows and MAC:
http://www.ageofwushu.com/forums/viewtopic.php?f=29&t=7339
http://www.ageofwushu.com/forums/viewtopic.php?f=30&t=14469
http://portingteam.com/topic/9284-age-of-wushu-dont-speed-up-tip-spam/
This disguised message is in fact the result of a failed API hook check.
The vendor obviously doesn't want to go into technical details/reveal this, see the support forums/site for answers to this problem.
Enter Wine and get the answers :)
One hint is contained in the game log file 'bin/trace.log'
With relay thunks emitted (+relay channel):
--- snip --- ... [2014-05-18 22:01:13.573] Time Func Pos Begin QueryPerformanceCounter=54 QueryPerformanceFrequency=54 GetTickCount=54 timeGetTime=54 timeSetEvent=54 SetTimer=54 ... --- snip ---
without relay thunks:
--- snip --- [2014-05-18 22:23:02.818] Time Func Pos Begin QueryPerformanceCounter=8d QueryPerformanceFrequency=8d GetTickCount=55 timeGetTime=55 timeSetEvent=8d SetTimer=55 --- snip ---
The hex codes are the first opcode byte of each API entry.
The protection code makes a snapshot of certain API entries on startup. The entry bytes are stored for later comparison to detect dynamic hooking (through dll injection for example).
--- snip --- ... 0056:Call KERNEL32.GetModuleHandleA(11a8eef0 "kernel32.dll") ret=1134c3b1 0056:Ret KERNEL32.GetModuleHandleA() retval=7b810000 ret=1134c3b1 0056:Call KERNEL32.GetProcAddress(7b810000,11a8eed4 "QueryPerformanceCounter") ret=1134c3c3 0056:Ret KERNEL32.GetProcAddress() retval=7b824afc ret=1134c3c3 0056:Call KERNEL32.GetModuleHandleA(11a8eef0 "kernel32.dll") ret=1134c3d9 0056:Ret KERNEL32.GetModuleHandleA() retval=7b810000 ret=1134c3d9 0056:Call KERNEL32.GetProcAddress(7b810000,11a8eeb4 "QueryPerformanceFrequency") ret=1134c3e5 0056:Ret KERNEL32.GetProcAddress() retval=7b824b14 ret=1134c3e5 0056:Call KERNEL32.GetModuleHandleA(11a8eef0 "kernel32.dll") ret=1134c3fb 0056:Ret KERNEL32.GetModuleHandleA() retval=7b810000 ret=1134c3fb 0056:Call KERNEL32.GetProcAddress(7b810000,11a8eea4 "GetTickCount") ret=1134c407 0056:Ret KERNEL32.GetProcAddress() retval=7b82374c ret=1134c407 0056:Call KERNEL32.GetModuleHandleA(11a8ee98 "WINMM.dll") ret=1134c41d 0056:Ret KERNEL32.GetModuleHandleA() retval=f7220000 ret=1134c41d 0056:Call KERNEL32.GetProcAddress(f7220000,11a8ee88 "timeGetTime") ret=1134c429 0056:Ret KERNEL32.GetProcAddress() retval=7b82374c ret=1134c429 0056:Call KERNEL32.GetModuleHandleA(11a8ee98 "WINMM.dll") ret=1134c43f 0056:Ret KERNEL32.GetModuleHandleA() retval=f7220000 ret=1134c43f 0056:Call KERNEL32.GetProcAddress(f7220000,11a8ee78 "timeSetEvent") ret=1134c44b 0056:Ret KERNEL32.GetProcAddress() retval=f7226d44 ret=1134c44b 0056:Call KERNEL32.GetModuleHandleA(11a8ee68 "USER32.dll") ret=1134c461 0056:Ret KERNEL32.GetModuleHandleA() retval=7eca0000 ret=1134c461 0056:Call KERNEL32.GetProcAddress(7eca0000,11a8ee5c "SetTimer") ret=1134c46d 0056:Ret KERNEL32.GetProcAddress() retval=7ecb0798 ret=1134c46d ... --- snip ---
Runtime check of API entries every two seconds:
--- snip --- ... 0056:Call KERNEL32.GetModuleHandleA(11a8eef0 "kernel32.dll") ret=1134c4f5 0056:Ret KERNEL32.GetModuleHandleA() retval=7b810000 ret=1134c4f5 0056:Call KERNEL32.GetProcAddress(7b810000,11a8eed4 "QueryPerformanceCounter") ret=1134c505 0056:Ret KERNEL32.GetProcAddress() retval=7b824afc ret=1134c505 0056:Call KERNEL32.GetModuleHandleA(11a8eef0 "kernel32.dll") ret=1134c2fb 0056:Ret KERNEL32.GetModuleHandleA() retval=7b810000 ret=1134c2fb 0056:Call KERNEL32.GetProcAddress(7b810000,11a8eeb4 "QueryPerformanceFrequency") ret=1134c30b 0056:Ret KERNEL32.GetProcAddress() retval=7b824b14 ret=1134c30b 0056:Call KERNEL32.GetModuleHandleA(11a8eef0 "kernel32.dll") ret=1134c2fb 0056:Ret KERNEL32.GetModuleHandleA() retval=7b810000 ret=1134c2fb 0056:Call KERNEL32.GetProcAddress(7b810000,11a8eea4 "GetTickCount") ret=1134c30b 0056:Ret KERNEL32.GetProcAddress() retval=7b82374c ret=1134c30b 0056:Call KERNEL32.GetModuleHandleA(11a8ee98 "WINMM.dll") ret=1134c32b 0056:Ret KERNEL32.GetModuleHandleA() retval=f7220000 ret=1134c32b 0056:Call KERNEL32.GetProcAddress(f7220000,11a8ee88 "timeGetTime") ret=1134c33b 0056:Ret KERNEL32.GetProcAddress() retval=7b82374c ret=1134c33b 0056:Call KERNEL32.GetModuleHandleA(11a8ee98 "WINMM.dll") ret=1134c2fb 0056:Ret KERNEL32.GetModuleHandleA() retval=f7220000 ret=1134c2fb 0056:Call KERNEL32.GetProcAddress(f7220000,11a8ee78 "timeSetEvent") ret=1134c30b 0056:Ret KERNEL32.GetProcAddress() retval=f7226d44 ret=1134c30b 0056:Call KERNEL32.GetModuleHandleA(11a8ee68 "USER32.dll") ret=1134c2fb 0056:Ret KERNEL32.GetModuleHandleA() retval=7eca0000 ret=1134c2fb 0056:Call KERNEL32.GetProcAddress(7eca0000,11a8ee5c "SetTimer") ret=1134c30b 0056:Ret KERNEL32.GetProcAddress() retval=7ecb0798 ret=1134c30b ... --- snip ---
Following is the list of API functions and their opcode checks. call/jmp opcode bytes are treated as hook.
--- snip --- kernel32.dll!QueryPerformanceCounter -> [0]=0xE8 | [0]=0xE9 | [0]=0xFF | ![0]=<snapshot>
kernel32.dll!QueryPerformanceFrequency -> [0]=0xE8 | [0]=0xE9 | [0]=0xFF | ![0]=<snapshot>
kernel32.dll!GetTickCount -> [0]=0xE8 | [0]=0xE9 | [0]=0xFF | ![0]=<snapshot>
WINMM.dll!timeGetTime -> [0]=0xE8 | [0]=0xE9 | [0]=0xFF | ![0]=<snapshot> | [7]=0xE8 | [7]=0xE9 | [7]=0xFF
WINMM.dll!timeSetEvent -> [0]=0xE8 | [0]=0xE9 | [0]=0xFF | ![0]=<snapshot>
user32.dll!SetTimer -> [0]=0xE8 | [0]=0xE9 | [0]=0xFF | ![0]=<snapshot> --- snip ---
The 'WINMM.dll!timeGetTime' entry point gets a special treatment and this the problem.
Wine 'winmm.spec'
--- snip --- @ stdcall timeGetTime() kernel32.GetTickCount --- snip ---
Forwarded. Dumping the target with Winedbg gives:
--- snip --- Wine-dbg>x/10b GetTickCount 0x7b8480d9 GetTickCount: 55 89 e5 53 83 e4 f0 e8 ab 75 --- snip ---
Disassembly:
--- snip --- 7B8480D9 55 PUSH EBP 7B8480DA 89E5 MOV EBP,ESP 7B8480DC 53 PUSH EBX 7B8480DD 83E4 F0 AND ESP,FFFFFFF0 7B8480E0 E8 AB75FDFF CALL KERNEL32.__x86.get_pc_thunk.bx 7B8480E5 81C3 1B2F0700 ADD EBX,72F1B 7B8480EB E8 72FFFFFF CALL KERNEL32.GetTickCount64 7B8480F0 8B5D FC MOV EBX,DWORD PTR SS:[EBP-4] 7B8480F3 C9 LEAVE 7B8480F4 C3 RETN --- snip ---
*eeek* .. 'entry[7]' has indeed value 0xE8 hence the check fails. The PIC code (setup of PIC register) in function prolog causes the harm here.
If you avoid the call opcode at 'entry[7]' everything is fine (for example making this entry hotpatchable, inline GetTickCount64, use wrapper).
I already tested this. Wine code is no longer (mis)detected as hook and the game runs fine without any "Speedup" spam.
$ sha1sum AgeofWushu_download.exe a7101c50ce8deb33008da4ce044fca5e3add721d AgeofWushu_download.exe
$ du -sh AgeofWushu_download.exe 1.9M AgeofWushu_download.exe
$ wine --version wine-1.7.19
Regards