https://bugs.winehq.org/show_bug.cgi?id=40623
--- Comment #19 from roman@hargrave.info roman@hargrave.info --- (In reply to Michael Müller from comment #18)
Created attachment 54540 [details] Hack to prevent crash
I debugged the crash some days ago. The problem with the Denuvo protection is that it contains a lot of intentional broken code that is called as soon as the system notices that something is going wrong. It is therefore possible that the crash happens on purpose and that the currently crashing code path should not be reached at all if everything works fine. Nevertheless, I analyzed the crash and attached a hack to to work around it.
The following assembler code leads to the crash. I added some annotations to explain the behavior.
mov rax,qword ptr gs:[60] ; TEB->PEB mov qword ptr ss:[rsp+C80],rbx mov qword ptr ss:[rsp+C68],rbp
mov rcx,qword ptr ds:[rax+18] ; PEB->LdrData
mov qword ptr ss:[rsp+C60],rsi mov qword ptr ss:[rsp+C58],rdi mov r9,qword ptr ds:[rcx+20] ; LdrData->InMemoryOrderModuleList (list entry)
mov qword ptr ss:[rsp+C50],r12 xor edi,edi sub r9,10 ; -> LDR_MODULE
mov qword ptr ss:[rsp+C48],r14 mov qword ptr ss:[rsp+C40],r15 cmp qword ptr ds:[r9+30],rdi ; LDR_MODULE->BaseAddress ; The comparison is a bit misleading, as the BaseAddress ; can never be zero. The idea is that if we reach the end ; of the list, we get a pointer to LdrData->InMemoryOrderModuleList ; again. The offset of LDR_MODULE->BaseAddress now matches the ; PEB_LDR_DATA->EntryInProgress offset. This value should be zero ; and wine never changes it anyway. The iteration therefore stops ; when we reach this entry again.
; Reached end of list je doomx64.159E4907E ; --> Crash
; BeginHash: mov r10,qword ptr ds:[r9+60] ; LDR_MODULE->BaseDllName.Buffer mov edx,edi ; edx will contain the hash, initialize with 0 mov r8,rdi movzx ecx,word ptr ds:[r10] ; Check if BaseDllName is empty test cx,cx je doomx64.159E49075 ; --> NextEntry
; ConverToLowerCase: lea eax,dword ptr ds:[rcx-41] ; - 'A' cmp ax,19 ja doomx64.159E49055 ; --> HashChar add cx,20 ; tolower
; HashChar: inc r8 movzx eax,cx movzx ecx,word ptr ds:[r10+r8*2] ; ecx -> next char
; The actual "hashing" of the name add edx,eax ; edx += char add edx,edx ; edx *= 2
; Reached end of string? test cx,cx jne doomx64.159E49048 ; no --> ConverToLowerCase cmp edx,C8A08 ; check hash against 0xC8A08 je doomx64.159E49178 ; --> found
; NextEntry: mov r9,qword ptr ds:[r9] ; InLoadOrderModuleList.Flink cmp qword ptr ds:[r9+30],rdi ; LDR_MODULE->BaseAddress ; Reached end of list? jne doomx64.159E49036 ; no --> BeginHash
; Crash: ; This function is meant to crash. It calls rsi (=rdi), ; which is already set to zero when the whole code path is ; reached. This is most probably just broken code to confuse us. mov rsi,rdi lea rax,qword ptr ss:[rsp+C98] lea rdx,qword ptr ds:[159DDCE20] mov r9d,20219 xor r8d,r8d mov rcx,FFFFFFFF80000001 mov qword ptr ss:[rsp+20],rax call rsi ; rsi = rdi = 0 test eax,eax
The whole algorithm just searches for a specific dll using a hash. The enumeration matches the following C code:
TEB *teb = (void*) NtCurrentTeb(); PEB *peb = teb->Peb;
PEB_LDR_DATA *ldr_data = peb->LdrData; LIST_ENTRY *entry = ldr_data->InMemoryOrderModuleList.Flink; LDR_MODULE *mod = (void*) ((char*) entry - 0x10);
while (mod->BaseAddress) { /* hash mod->BaseDllName.Buffer */ mod = (void*)mod->InLoadOrderModuleList.Flink; }
The assembler code searches for the hash 0xC8A08 which corresponds to advapi32.dll. The dll is loaded directly by doom through the PE import section. So why does it crash? Well, if you look at the enumeration you will see that it starts the search using the InMemoryOrderModuleList list but continues using the InLoadOrderModuleList list. The search only succeeds if the first entry in InMemoryOrderModuleList (i.e. the dll loaded at the lowest address) is loaded before advapi32. Otherwise the code intentionally crashes.
I currently don't have a windows machine for testing but it would be great if someone could attach a debugger on windows and take a look at the loaded libraries and their addresses. This way we can check if the whole code is garbage to confuse people or if the necessary conditions are met on windows.
I wrote a hack to pin advapi32.dll as first entry of the InMemoryOrderModuleList. This prevents the crash and Doom uses advapi32 to read some registry keys. It also loads the dbdata license file. When this is done nothing interesting happens any more. I guess that the license check fails, but I haven't debugged it any further since I don't have enough time at the moment. It would also make sense to check the theory on windows first to prevent wasting time on some intentionally broken code.
If someone wants to debug this any further, the attached hack should help. I wrote it against Staging (to use WINEDEBUG=+DOOMx64.exe:relay for generating relay logs without debugging Steam), but it should also apply on plain wine.
Thanks for the input Michael. If it's truly the case that WINE is simply not similar enough to a Windows environment for Denuvo to be "OK", and not the case that WINE is not implemented behaviour required for the basic functioning of Denuvo in itself, that would mean that a simple set of patched could potentially make this work. Unfortunately I can't do the suggested testing, but I would like to note that debugging Denuvo (or running it in a VM) may lead to unexpected results.