[Bug 40623] DOOM (2016) crashes on launch due to Denuvo copy protection

wine-bugs at winehq.org wine-bugs at winehq.org
Sat May 21 21:51:25 CDT 2016


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

Michael Müller <michael at fds-team.de> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |michael at fds-team.de

--- Comment #18 from Michael Müller <michael at fds-team.de> ---
Created attachment 54540
  --> https://bugs.winehq.org/attachment.cgi?id=54540
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.

-- 
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