Dishonored 2 - VoidEngine (id Tech 5)

Post here (make sure thread doesn't exist first) any type of tutorials: text, images, videos or oriented discussions on specific games. No online-related discussions/posts OR warez!
Post Reply
User avatar
SunBeam
Trouble Makers
Trouble Makers
Posts: 1498
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 220

Dishonored 2 - VoidEngine (id Tech 5)

Post by SunBeam » Tue May 15, 2018 12:53 pm

[B][U]EPISODE 1[/U][/B]



Hello folks.



This post aims to collect a ton shit of information on the engine this game runs on (note [B]Death of the Outsider[/B] uses the same engine). I've been doing some comparison lately between [B]Doom 2016[/B]'s engine (id Tech 6) and this one, in hopes of identifying similarities and approaches to enable console or execute console commands/CVars. I was successful to some extent, whereas now I can execute stuff (hope I can also dump the console, even though it seems disabled or I yet don't know how to bring it down; [I]activateConsole 0.5[/I] doesn't seem to work).



You will find a lot of gibberish or information that doesn't make any sense to you, but you can always leave feedback. Remember, this is a collection post, not a tutorial or "how to" (even though at times you may find directions, code samples for breaking, etc.).



I'm currently playing on the latest Steam build to date:



- top bar says Dishonored 2 using VoidEngine v1.77.7.0

- Dishonored2.exe is 114 MB in size (I have it dumped for historical/analysis purposes or AOB scanning)



Tools I use:



- Cheat Engine to hook bits and pieces

- x64dbg attached to the game process [I]after[/I] launch



Let the spam begin!



• console Exec function starts here:



[img]https://i.imgur.com/U4x5MB9.png[/img]



• and this is the data of RCX pointer in dump:



[img]https://i.imgur.com/d4zOSZl.png[/img]



• considering there's no actual console to execute something in, I have to emulate the console text buffer with the appropriate information:



p+0x18: pointer to string (leads to 0x28) with trailing

p+0x20: size of string + 1

p+0x28: is where our text will need filling-in



• Cheat Engine quick script to hook the spot and easily edit string and size:

[code=CEA]

[ENABLE]



alloc( Hook, 0x1000, Dishonored2.exe )

label( back )

label( pBase )

registersymbol( pBase )



Hook:

mov [pBase],rcx

mov eax,[rcx+20]

mov r14,rcx

jmp back



pBase:

dq 0



Dishonored2.exe+41877EF:

jmp Hook

db 90

back:



[DISABLE]



Dishonored2.exe+41877EF:

db 8B 41 20 49 89 CE



unregistersymbol( pBase )

dealloc( Hook )



{

// ORIGINAL CODE - INJECTION POINT: "Dishonored2.exe"+41877EF



"Dishonored2.exe"+41877DA: CC - int 3

"Dishonored2.exe"+41877DB: CC - int 3

"Dishonored2.exe"+41877DC: CC - int 3

"Dishonored2.exe"+41877DD: CC - int 3

"Dishonored2.exe"+41877DE: CC - int 3

"Dishonored2.exe"+41877DF: CC - int 3

"Dishonored2.exe"+41877E0: 41 56 - push r14

"Dishonored2.exe"+41877E2: B8 40 14 00 00 - mov eax,00001440

"Dishonored2.exe"+41877E7: E8 D4 25 4D 01 - call Dishonored2.exe+5659DC0

"Dishonored2.exe"+41877EC: 48 29 C4 - sub rsp,rax

// ---------- INJECTING HERE ----------

"Dishonored2.exe"+41877EF: 8B 41 20 - mov eax,[rcx+20]

"Dishonored2.exe"+41877F2: 49 89 CE - mov r14,rcx

// ---------- DONE INJECTING ----------

"Dishonored2.exe"+41877F5: C1 E0 03 - shl eax,03

"Dishonored2.exe"+41877F8: 85 C0 - test eax,eax

"Dishonored2.exe"+41877FA: 0F 8E E2 02 00 00 - jng Dishonored2.exe+4187AE2

"Dishonored2.exe"+4187800: CC - int 3

"Dishonored2.exe"+4187801: 89 9C 24 50 14 00 00 - mov [rsp+00001450],ebx

"Dishonored2.exe"+4187808: 48 89 AC 24 58 14 00 00 - mov [rsp+00001458],rbp

"Dishonored2.exe"+4187810: 48 89 B4 24 60 14 00 00 - mov [rsp+00001460],rsi

"Dishonored2.exe"+4187818: 48 89 BC 24 68 14 00 00 - mov [rsp+00001468],rdi

"Dishonored2.exe"+4187820: 31 ED - xor ebp,ebp

"Dishonored2.exe"+4187822: 41 39 6E 10 - cmp [r14+10],ebp

}

[/code]

• now pBase can be used with ease, as follows:



[img]https://i.imgur.com/DNhsO9m.png[/img]



• note size of my string + 1 is 0x1B:



[img]https://i.imgur.com/5VVVs2R.png[/img]



• once set, x64dbg breaks and I can continue the trace; parsing's finished here:



[img]https://i.imgur.com/9GUKf3L.png[/img]



• execution continues with calling a member function, at this location:



[img]https://i.imgur.com/YUKiD1q.png[/img]



• inside this function, things are pretty much self-explanatory after the analysis:



[img]https://i.imgur.com/zHhQeCa.png[/img]



• the engine loops a list of available console commands to find the one I've sent for processing; the list of all available ones:



[spoiler]

testStatsEnd

testStatsBegin

playEvents

recordEvents

deleteGenerated

leaveGameToMainMenu

leaveGame

disconnect

restartmaphere

restartmap

map

campaign

stripStrings

reloadTextLanguage

vid_restart

find

writeConfig

exit

quit

reloadModels

listModels

rp

testImage

colorGradingShot

envshot

rawscreenshot

screenshot

cubeshot

buddha

god

resetViewParms

clearDebugPoints

popDebugPoint

pushDebugPoint

randomTest

entityListenerStats

debugEntityByName

prevAI

nextAI

nextActiveAI

gameError

clearLights

popLight

listLines

blinkline

removeline

addarrow

addline

reloadEntity

killEntity

damage

trigger

teleport

setviewpos

getviewpos

where

reexportDecls

touchDecl

unregisterDeclFolder

listDecls

writeImage

listImages

copy

path

dirtree

dir

makeDeclTree

arkIggySubtitleLevel

arkSwitchToBindingSet

arkSwitchToDefaultBindingSet

arkBind

arkBindSecondary

arkUnbind

arkUnbindCurrentSet

arkUnbindKeyboard

arkUnbindMouse

arkUnbindJoypad

arkUnbindAll

arkListBindings

idStudio

writeEntitiesFile

writeEntitiesFileWithError

clear

print

history

clearHistory

con_watch

con_unwatch

toggle

cvarAdd

cvarRandom

addWrap

addClamp

cvarMultiply

reset

listCvars

cvar_restart

cvar_resetcheats

cvarsModified

scheduleVideo

ark_settings_save

RestartFromMemoryCheckpoint

RestartFromCheckpoint

SaveGame

LoadGame

TestFx

Ansel_PlayerVisible

wait

parse

echo

vstr

exec

listCmds

[/spoiler]



• if command is valid, JMP RAX takes you to the actual function getting executed for the particular command (note that some of the content is disabled in the "retail" build; so either fool around with it or try to [I]unlock[/I] it)



• note how in the loop, RDI+8 holds the pointer to the string, while RDI+10 contains the actual function address to be executed, passed on later to RAX for JMP RAX ;)



• else, if it's a CVar, the CALL QWORD PTR [RAX+0x78] will do the job of finding it and processing it for you



That's as much as I have for now.



BR,

Sun

User avatar
SunBeam
Trouble Makers
Trouble Makers
Posts: 1498
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 220

Dishonored 2 - VoidEngine (id Tech 5)

Post by SunBeam » Tue May 15, 2018 1:14 pm

[B][U]EPISODE 2[/U][/B]



A bit of info on [B]Doom 2016[/B]. My target, alongside Dishonored 2. According to id Tech 6 engine, I got to patch this location so all commands and CVars become available for use:



[img]https://i.imgur.com/7pYVmIz.png[/img]

[code=CEA]

DOOMx64.exe+15E7A70 - 89 91 A8000200 - mov [rcx+000200A8],edx

DOOMx64.exe+15E7A76 - C3 - ret

[/code]

to

[code=CEA]

DOOMx64.exe+15E7A70 - C7 81 A8000200 00000000 - mov [rcx+000200A8],0

DOOMx64.exe+15E7A7A - C3 - ret

[/code]

If I hadn't done that: 1. the CVar would not be available in the list; 2. I would've seen something like this (I forced the check not to jump):



[img]https://i.imgur.com/pUS9rqB.png[/img]



CALL [RAX+10] above TEST EAX,EAX leads to this location:



[code=CEA]

DOOMx64.exe+15E3B60 - 8B 81 A8000200 - mov eax,[rcx+000200A8]

DOOMx64.exe+15E3B66 - C3 - ret

[/code]

0 will be read (due to my patch), thus the code doesn't need to check CVar flags. [I]Dishonored 2[/I] doesn't have this kind of limitation, or at least not coded this way (there might be a hardcoded BOOL, but as far as I remember, it's 0 by default).



So now, assuming there are no restrictions (just imagine the CALL + TEST didn't exist), this happens:



[img]https://i.imgur.com/SnaFsmI.png[/img]



Well, I wanted to know why. For that I used the console command called [I]listCvars[/I]:



[img]https://i.imgur.com/YOzAtxp.png[/img]



Aside from listing ~6500 CVars, as opposed to seveal hundred in restricted mode, I learn that I can use some switches. Let's see what's going on with [B]g_infiniteAmmo[/B]. Normal typing and Enter-ing in the console shows this:



[img]https://i.imgur.com/K8eZcLz.png[/img]



If you now use individual switches for listCvars, you get these:



[img]https://i.imgur.com/4T5a3IA.png[/img]



[img]https://i.imgur.com/22uIc9b.png[/img]



[img]https://i.imgur.com/lrQ0Bdp.png[/img]



Where I'm going with this: if you check the screenshot above and assume there are no restrictions, then what we want is to set g_infiniteAmmo as a NC (no cheat) CVar ;) I don't know what B means, but MO stands from "modified".



Back to x64dbg:



• console Exec function starts here:



[img]https://i.imgur.com/GbcbitF.png[/img]



[img]https://i.imgur.com/tMUHZgp.png[/img]



• proceeding in, returns this (I've commented code along the way):



[img]https://i.imgur.com/nv8V4Qk.png[/img]



[img]https://i.imgur.com/3UHEIX5.png[/img]



• since I typed in g_infiniteAmmo, I will focus on the "process CVar" branch; so:



[code=CEA]

00007FF60D35520A | 48 8B 0D 57 A4 6A 02 | MOV RCX,QWORD PTR DS:[7FF60F9FF668] | <-- process CVars

00007FF60D355211 | 48 8B 01 | MOV RAX,QWORD PTR DS:[RCX] |

00007FF60D355214 | 48 8B D7 | MOV RDX,RDI |

00007FF60D355217 | FF 90 88 00 00 00 | CALL QWORD PTR DS:[RAX+88] |

[/code]





• inside this CALL, I continued till here:



[code=CEA]

00007FF60D2E0FD0 | 48 89 5C 24 08 | MOV QWORD PTR SS:[RSP+8],RBX |

00007FF60D2E0FD5 | 57 | PUSH RDI |

00007FF60D2E0FD6 | 48 83 EC 30 | SUB RSP,30 |

00007FF60D2E0FDA | 83 3A 00 | CMP DWORD PTR DS:[RDX],0 |

00007FF60D2E0FDD | 48 8B DA | MOV RBX,RDX |

00007FF60D2E0FE0 | 7E 06 | JLE doomx64.7FF60D2E0FE8 |

00007FF60D2E0FE2 | 48 8B 52 08 | MOV RDX,QWORD PTR DS:[RDX+8] | [rdx+8]:"g_infiniteAmmo"

00007FF60D2E0FE6 | EB 07 | JMP doomx64.7FF60D2E0FEF |

00007FF60D2E0FE8 | 48 8D 15 19 DF 81 00 | LEA RDX,QWORD PTR DS:[7FF60DAFEF08] |

00007FF60D2E0FEF | 48 8B 01 | MOV RAX,QWORD PTR DS:[RCX] |

00007FF60D2E0FF2 | FF 50 20 | CALL QWORD PTR DS:[RAX+20] |

00007FF60D2E0FF5 | 48 8B F8 | MOV RDI,RAX |

[/code]





The CALL [RAX+20] above returns the CVar information buffer. In my case, I got this (followed RAX to dump):



[img]https://i.imgur.com/nIkbfKd.png[/img]



One thing to notice, if you check my red rectangle, is after it, another similar block starts. So it's fair to assume at this point the CVar block is 0x90 big.



• continuing the trace, typing "g_infiniteAmmo 1", I got to here:



[img]https://i.imgur.com/ItwTF51.png[/img]



• entering the last CALL in this function (the one above MOV AL,1), leads to here:



[img]https://i.imgur.com/8Xc2xLF.png[/img]



Engine reads some stuff from offset 0x58; could this be that "flag" information? Considering the check's result would pop the "developer cvar" message to console, I'd say yes. See here:



[img]https://i.imgur.com/Td6ctih.png[/img]



• the byte's value is 0x9. Now, back in Dishonored 2 I noticed this value is sometimes 0x11. Let's try it and see what the console spits when using "- flags" switch:



[img]https://i.imgur.com/7CsPBXp.png[/img]



[img]https://i.imgur.com/uhbryj9.png[/img]



Yup, bingo. So, 0x9 (or 0x?9) would be a cheat flag, whereas 0x11 (or variations, remember engine does a TEST) tells it it's a non-cheat CVar :D



Then I wanted to see what happens next, where exactly is the value being set or under which form (it can be a 0/1 toggle or a flag switch); I remembered from Dishonored 2 that engine will change a string value inside the CVar structure and set a "modified" flag; that's about it; let's see.



• once more, the original buffer (the original value was 0x4 - got changed to 0xC when modifying the CVar, so I put it back - while the 0x11 is the change we made earlier; talking about the values in red):



[img]https://i.imgur.com/dJfCnCb.png[/img]



• the routine of processing the CVar starts here:



[img]https://i.imgur.com/yZ1VTwL.png[/img]



• when at second breakpoint (00007FF60D2F8671 - MOV EDX,EBP), this has happened in the structure:



[img]https://i.imgur.com/WVqdYNK.png[/img]



Remember value was 0 previously? Moving on.



• past the CALL and hitting the next breakpoint:



[img]https://i.imgur.com/G1mBJfB.png[/img]



Those were 0 as well before the execution.



• lastly, another change happens, whereas 0x4 becomes 0xC past this OR:



[img]https://i.imgur.com/K1aYOsx.png[/img]



This is where I'm at with the analysis. Determining if the engine uses that 1 value or 00 00 80 3F (which is 1.0 as float) or not.



• how do I know if the CVar works or not? well:



[I]g_infiniteAmmo 0[/I]



[img]https://i.imgur.com/oFOszON.png[/img]



[I]g_infiniteAmmo 1[/I]



[img]https://i.imgur.com/LBdl8gp.png[/img]



• considering "listCvars - type g_infiniteAmmo" told me it's a BOOL value, I tend to believe the 0x1 BOOL we saw being set earlier will be the one being checked; I've set a hardware breakpoint (on access) on it and this happened as soon as I resumed the game:



[img]https://i.imgur.com/NJP6K3K.png[/img]



• keeping in mind right now I have the CVar disabled, I went ahead and patched that JE you see there to NOPs, just to see what happens :D Bingo!



[img]https://i.imgur.com/mVhMe9a.png[/img]



That concludes both my goal and analysis :D Time to see why the fucker Dishonored 2 won't set the CVars as supposed :P



BR,

Sun

User avatar
SunBeam
Trouble Makers
Trouble Makers
Posts: 1498
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 220

Dishonored 2 - VoidEngine (id Tech 5)

Post by SunBeam » Tue May 15, 2018 1:21 pm

[B][U]EPISODE 3[/U][/B]



Back to Dishonored 2:



• the execution flow passes through this location:



[img]https://i.imgur.com/kI78PlX.png[/img]



• buffer for [I]ark_god_player[/I] CVar, having sent "ark_god_player 1" to the console's exec function:



[img]https://i.imgur.com/KziRD80.png[/img]



Observations:



- the flag byte is at offset 0x48 (MOV ECX, DWORD PTR [RBX+0x48]) - notice I've already set it from 0x29 (original) to 0x11 (no cheat);

- there's no CALL [RAX+10], but a simple static BOOL compare: [B]CMP DWORD PTR [0x143408018],0[/B] (this is the restriction BOOL); if not set to 1, it will then query the CVar's flag



• continuing the trace shows everything works like normal; flag is checked, value is set properly, etc. - everything you've seen in above post:



[img]https://i.imgur.com/jIC671K.png[/img]



Time to find out if that 0x1 BOOL is being accessed by any piece of code. Set a hardware breakpoint (on access) on it and resumed the game; Blinked to a high place and dropped to the ground; no hit :(



So.. I'm guessing what they mean by "not available in retail" is all checks in executable code for cheat CVars were removed when game got compiled as a release build.



Lastly, and this is where I stop with this game:



• [I]listCvars[/I] command leads to this piece of code, while tracing:



[img]https://i.imgur.com/xz3mrLz.png[/img]



• check RAX, remove breakpoint after hit and make note of the address (in my case it was 0x189CA9BE0); the address is a pointer to an ordered list of all pointers to CVar structures



• feed this script to CE's Lua Engine (Ctrl+L; paste; Execute), changing the address to your own:



[code=Lua]

for i = 0,0x3240,8 do

local x = readQword(0x189CA9BE0+i)

y = x + 0x30 -- CVar name

z = x + 0x38 -- CVar value

y = readString(readQword(y),6000)

z = readString(readQword(z),6000)

print( string.format( "%s = %s", y, z ) )

t = x + 0x40 -- CVar description (if any)

t = readString(readQword(t),6000)

print( string.format( "%s
", t ) )

end

[/code]





• you should see something like this scrolling down:



[img]https://i.imgur.com/vqLgux9.png[/img]



• if anyone wants to try these out, feel free to comb the list of all 1608 CVars: [URL]https://pastebin.com/1DezCKBT[/URL]



..and this is a cropped-up exec function, written in Lua (to run in Lua Engine window; Ctrl+L):



[code=Lua]

function exec( s )



local szString = readQword( readQword( getAddress( "pBase" ) ) + 0x18 )

local szSize = readQword( getAddress( "pBase" ) ) + 0x20

writeString( szString, s )

local size = string.len( s )

writeBytes( szString + size, 0 )

writeInteger( szSize, size + 1 )



end



exec("listCmds")

[/code]





"Hook ConsoleExec" script needs to be enabled, for it to work.



BR,

Sun

SendMe
Table Makers
Table Makers
Posts: 272
Joined: Fri Feb 02, 2018 5:58 pm
Reputation: 12

Dishonored 2 - VoidEngine (id Tech 5)

Post by SendMe » Wed May 16, 2018 3:22 am

I'd like to say I really appreciate this tutorial, Sun. I remember when we first met not too long ago, I asked you how I would go about learning stuff beyond simple memory scanning/value searching and you stated Dishonored is a great place to begin.
Last edited by SendMe on Thu Jan 01, 1970 12:00 am, edited 1 time in total.

Post Reply

Who is online

Users browsing this forum: No registered users