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
Administration
Administration
Posts: 4703
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 4287

Dishonored 2 - VoidEngine (id Tech 5)

Post by SunBeam »

EPISODE 1

Hello folks.

This post aims to collect a ton shit of information on the engine this game runs on (note Death of the Outsider uses the same engine). I've been doing some comparison lately between Doom 2016'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; activateConsole 0.5 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 after launch

Let the spam begin!

• console Exec function starts here:

Image

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

Image

• 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: Select all

[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
}
• now pBase can be used with ease, as follows:

Image

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

Image

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

Image

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

Image

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

Image

• 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
• 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 unlock 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
Administration
Administration
Posts: 4703
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 4287

Dishonored 2 - VoidEngine (id Tech 5)

Post by SunBeam »

EPISODE 2

A bit of info on Doom 2016. 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:

Image

Code: Select all

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

Code: Select all

DOOMx64.exe+15E7A70 - C7 81 A8000200 00000000 - mov [rcx+000200A8],0
DOOMx64.exe+15E7A7A - C3                      - ret
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):

Image

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

Code: Select all

DOOMx64.exe+15E3B60 - 8B 81 A8000200        - mov eax,[rcx+000200A8]
DOOMx64.exe+15E3B66 - C3                    - ret
0 will be read (due to my patch), thus the code doesn't need to check CVar flags. Dishonored 2 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:

Image

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

Image

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 g_infiniteAmmo. Normal typing and Enter-ing in the console shows this:

Image

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

Image

Image

Image

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:

Image

Image

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

Image

Image

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

Code: Select all

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

• inside this CALL, I continued till here:

Code: Select all

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                            |

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

Image

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:

Image

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

Image

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:

Image

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

Image

Image

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

Image

• the routine of processing the CVar starts here:

Image

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

Image

Remember value was 0 previously? Moving on.

• past the CALL and hitting the next breakpoint:

Image

Those were 0 as well before the execution.

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

Image

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:

g_infiniteAmmo 0

Image

g_infiniteAmmo 1

Image

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

Image

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

Image

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
Administration
Administration
Posts: 4703
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 4287

Dishonored 2 - VoidEngine (id Tech 5)

Post by SunBeam »

EPISODE 3

Back to Dishonored 2:

• the execution flow passes through this location:

Image

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

Image

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: CMP DWORD PTR [0x143408018],0 (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:

Image

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:

listCvars command leads to this piece of code, while tracing:

Image

• 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: Select all

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

• you should see something like this scrolling down:

Image

• if anyone wants to try these out, feel free to comb the list of all 1608 CVars: [Link]

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

Code: Select all

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

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

BR,
Sun

User avatar
Send
Table Makers
Table Makers
Posts: 331
Joined: Fri Feb 02, 2018 5:58 pm
Reputation: 191

Dishonored 2 - VoidEngine (id Tech 5)

Post by Send »

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