A Somewhat Unexpected Update
Ah what joy. A new update for Assassin’s Creed: Valhalla has been released upon the masses. I’m sure most gamers, upon seeing an update for a game they enjoy queued for download, rejoice with much latent glee. Not this grumpy fellow, as any update to an Omnified game most of the time means that the binary has been altered, resulting in some inevitable breakage for our hacks.
I typically will prevent any updates from being downloaded while I’m streaming an Omnified game, however that dratted Uplay refused to launch the game unless I updated the game binaries. Perhaps if I left the launcher in offline mode it would’ve never known about the update. Well. That is neither here nor there now.
Here are the official patch notes, and they look to be describing a mighty fine set of changes indeed. Isn’t that just special. No time for dawdling — time to get to updating our hacks! Let’s pray to whatever-is-up-there that we’re able to locate all the areas of code we were injecting into previously.
Updating the Player Data Retrieval Code
The first item on the list for updating is the code responsible for retrieving all pertinent data associated with our player. This code grabs the player’s root structure, health structure, and location structure. Very essential and very much the first thing we’ll go after.
The unique array of bytes we had listed for the bit of code we were hooking into before was 2B 45 BC 89 44 24 58
. Hopefully the updates to the application haven’t eaten up this area of the code.
Luckily it looks like our code is still present in the binary. Woot. Let’s update our hook then so that it gets injected into the proper place.
Updated Player Data Retrieval Hook
// Creates pointers for the player's root, health, and location // structures. // UNIQUE AOB: 8B BB 38 01 00 00 75 define(omniPlayerHook, "ACValhalla.exe" + 2195AC3) assert(omniPlayerHook, 8B BB 38 01 00 00) alloc(getPlayer,$1000, omniPlayerHook) alloc(player,8) alloc(playerHealth,8) alloc(playerLocation,8) registersymbol(omniPlayerHook) registersymbol(player) registersymbol(playerHealth) registersymbol(playerLocation) getPlayer: push rax push rbx push rcx mov rax,playerHealth mov [rax],rbx // Root structure can be found at [playerHealth+70]. mov rax,player mov rcx,[rbx+70] mov [rax],rcx // Location structure aligned for movement can be found // at [player+1D0]. mov rax,playerLocation mov rbx,[rcx+1D0] mov [rax],rbx pop rcx pop rbx pop rax getPlayerOriginalCode: mov edi,[rbx+00000138] jmp getPlayerReturn omniPlayerHook: jmp getPlayer nop getPlayerReturn:
Excellent! Now, to get to all the code that will really be a pain to go look for again.
Updating the Apocalypse System’s Hooks
Our Apocalypse initiation point is located smack dab in the middle of the damage application code for the player. This would be quite the pain to have to find again. The unique array of bytes for this is 2B 45 BC 89 44 24 58
. Let’s hope for the best.
Searching for this array yields back a single result, perfect! None of the updates in the patch touched the code that applies damage to creatures. Let’s get that hook updated then.
Updated Apocalypse Initiation Point Hook
// Initiates the Apocalypse system. // Unique AOB: 2B 45 BC 89 44 24 58 define(omnifyApocalypseHook, "ACValhalla.exe" + 2149247) assert(omnifyApocalypseHook, 2B 45 BC 89 44 24 58) alloc(initiateApocalypse,$1000, omnifyApocalypseHook) registersymbol(omnifyApocalypseHook) initiateApocalypse: pushf // Backing up a few SSE registers to hold converted floating points. sub rsp,10 movdqu [rsp],xmm0 sub rsp,10 movdqu [rsp],xmm1 sub rsp,10 movdqu [rsp],xmm2 // Backing up the rbx register as it will be overwritten by // the Apocalypse register. We don't care about the rax register // as we'll be changing it anyway. push rbx // Backing up a working register to aid in value calculations, etc. push rcx // If we're freezing from cold water, the bits at [rsp+452] will be // 0x14. //mov rcx,[rsp+452] //cmp rcx,0x14 //je initiateApocalypseCheck // Damage from arrows lack a root structure damage source, however // the bits at [rsp+452] will be non-zero. //cmp rcx,0 //jne skipLocationCheck // Let's grab the damage source right now before the stack shifts // anymore. mov rcx,[rsp+44A] // If no root structure is listed as the damage source, this means // that the damage is from an ambient source (i.e. falling), which // we want to prevent from triggering the Apocalypse system. //cmp rcx,0 //je initiateApocalypseExit skipLocationCheck: // We'll convert the working health and damage amount to floats. cvtsi2ss xmm0,eax cvtsi2ss xmm1,[rbp-44] // If the player is the one receiving the damage, then the rdi register // will point to the player's location structure. mov rbx,playerLocation cmp [rbx],rdi je initiatePlayerApocalypse // The player is the one damaging the NPC if the player's root structure // is listed as the damage source. mov rbx,player cmp [rbx],rcx je initiateEnemyApocalypse jmp initiateApocalypseExit initiatePlayerApocalypse: // Realign the player's coordinates so it begins at the X coordinate. mov rcx,[rbx] lea rax,[rcx+50] // Convert the player's maximum health to floating point. mov rbx,playerHealth mov rcx,[rbx] cvtsi2ss xmm2,[rcx+13C] // Push the damage amount parameter. sub rsp,8 movd [rsp],xmm1 // Push the working health value parameter. sub rsp,8 movd [rsp],xmm0 // Push the maximum health value parameter. sub rsp,8 movd [rsp],xmm2 // Push the aligned coordinates struct parameter. push rax call executePlayerApocalypse jmp initiateApocalypseUpdateDamage initiateEnemyApocalypse: // Push the damage amount parameter. sub rsp,8 movd [rsp],xmm1 // Push the working health value parameter. sub rsp,8 movd [rsp],xmm0 call executeEnemyApocalypse initiateApocalypseUpdateDamage: // Convert the updated damage amount to an integer. movd xmm0,eax cvtss2si eax,xmm0 mov [rbp-44],eax // Convert the updated working health value to an integer. movd xmm0,ebx cvtss2si eax,xmm0 initiateApocalypseExit: pop rbx pop rcx movdqu xmm2,[rsp] add rsp,10 movdqu xmm1,[rsp] add rsp,10 movdqu xmm0,[rsp] add rsp,10 initiateApocalypseOriginalCode: popf sub eax,[rbp-44] mov [rsp+58],eax jmp initiateApocalypseReturn omnifyApocalypseHook: jmp initiateApocalypse nop 2 initiateApocalypseReturn: negativeVerticalDisplacementEnabled: dd 0 teleportitisDisplacementX: dd (float)5.0 yIsVertical: dd 0 extraDamageX: dd (float)2.0
In addition to the initiation point hook, we also have some code being inserted into the game that removes its one hit kill protection, which is a terrible thing really (the one hit kill protection, not my code duh). A world without one hit kills would be an awful world to live in!
The unique array of bytes we wrote down for this was 0F B6 8D EC 00 00 00
, so let’s cross our fingers once again. Unfortunately, for this, we get two results back. So, we need to do some guessing. Disassembling the first result, I see an idiv
instruction, indicating that some integer division is going on.
This is something that I do remember occurring nearby the hook we made to prevent the one hit kill protection from going off (it was the integer division that was essentially testing whether or not our health was below the safe threshold), so we’ll update our code to inject into where the first result is pointing to.
Updated Depussification Hook
// Bypasses the one hit kill protection system. // Unique AOB: 0F B6 8D EC 00 00 00 define(omnifyDepussifyGameHook, "ACValhalla.exe" + 20EBE76) assert(omnifyDepussifyGameHook, 0F B6 8D EC 00 00 00) alloc(depussifyGame,$1000, omnifyDepussifyGameHook) registersymbol(omnifyDepussifyGameHook) depussifyGame: // This is a hardcoded value used as a divisor applied against // the player's maximum health to see if the player is protected // from being one shot. By increasing this to max we essentially // make all values of health unsafe. mov [rbp+EC],0xFF depussifyGameOriginalCode: movzx ecx,byte ptr [rbp+000000EC] jmp depussifyGameReturn omnifyDepussifyGameHook: jmp depussifyGame nop 2 depussifyGameReturn:
And we’re not quite done yet. We had another hook in place to allow for coordinate changes stemming from teleportitis to persist despite being in the middle of a damage-related animation. The unique array of bytes for this is 0F 28 48 30 0F 29 49 50
. Crossing my fingers.
Only one result back. Fantastic. Time to update this then.
Updated Teleportitis Overwrite Bypass Hook
// Bypasses a single location update pulse so that coordinates changes // from a teleportitis effect are not overwritten. // Unique AOB: 0F 28 48 30 0F 29 49 50 define(omnifyBypassTeleportitisOverwriteHook, "ACValhalla.exe" + 7E878A) assert(omnifyBypassTeleportitisOverwriteHook,0F 28 48 30 0F 29 49 50) alloc(bypassTeleportitisOverwrite,$1000,omnifyBypassTeleportitisOverwriteHook) registersymbol(omnifyBypassTeleportitisOverwriteHook) bypassTeleportitisOverwrite: pushf // Ensure that the player location struct has been initialized. push rax mov rax,playerLocation cmp [rax],0 pop rax je bypassTeleportitisOverwriteOriginalCode // Backup registers used to hold the player's location structures. push rax push rbx push rdx // Backup the "updated location" structure address. mov rdx,rax mov rax,playerLocation mov rbx,[rax] // If the player is the one moving, we make sure that any new coordinates // resulting from a recent teleport stick for one additional location // update tick. cmp rbx,rcx jne bypassTeleportitisOverwriteExit mov rax,teleported cmp [rax],1 jne bypassTeleportitisOverwriteExit // Clear the flag so this only happens once. mov [rax],0 // Take the current coordinates and store them in the "updated location" // structure used in the original code as the source for the new coordinates. movups xmm1,[rcx+50] movups [rdx+30],xmm1 bypassTeleportitisOverwriteExit: // Restore backed up values. pop rdx pop rbx pop rax bypassTeleportitisOverwriteOriginalCode: popf movaps xmm1,[rax+30] movaps [rcx+50],xmm1 jmp bypassTeleportitisOverwriteReturn omnifyBypassTeleportitisOverwriteHook: jmp bypassTeleportitisOverwrite nop 3 bypassTeleportitisOverwriteReturn:
That’s it for the Apocalypse system.
Updating the Predator System’s Hooks
Next up to bat are hooks related to the Predator system. Fortunately, for this game-neutral system, there is only a single hook to update: its initiation point. This resides right where the movement application code is, which wasn’t the easiest thing to find, of course.
The unique array of bytes for this is 0F 10 56 50 4C 8D 4C 24 30
. Please, please, give me some good results here.
Viola! A single result is returned. Let’s update our hook to work here.
Updated Predator Initiation Point Hook
// Initiates the Predator system. // Unique AOB:0F 58 C3 0F 11 06 F3 // xmm0: Target's current coordinate values. // xmm3: Target's movement offsets. define(omnifyPredatorHook, "ACValhalla.exe" + F0CA43) assert(omnifyPredatorHook, 0F 58 C3 0F 11 06) alloc(initiatePredator,$1000, omnifyPredatorHook) alloc(identityValue,8) alloc(playerSpeedX,8) registersymbol(omnifyPredatorHook) registersymbol(playerSpeedX) initiatePredator: pushf // Ensure that the player location struct has been initialized. push rax mov rax,playerLocation cmp [rax],0 pop rax je initiatePredatorOriginalCode // Backing up a few SSE registers we'll be using to perform a little // memory management. sub rsp,10 movdqu [rsp],xmm0 sub rsp,10 movdqu [rsp],xmm1 // Backing up the registers used by the Predator system to hold its // return values. push rax push rbx push rcx // If it is the player moving, we'll want to avoid Predator execution // and simply apply the player speed modifier. // An interim data structure linking to the creature's location // structure can be found on the stack. mov rax,[rsp+14A] // The location structure is found at the 0x18 offset of this // interim structure. push rcx lea rcx,[rax+18] call checkBadPointer cmp rcx,0 pop rcx jne initiatePredatorCleanup mov rbx,[rax+18] mov rax,playerLocation cmp rbx,[rax] je applyPlayerSpeed initiatePredatorExecute: // The player's coordinates are pushed as the first parameter. mov rax,playerLocation mov rbx,[rax] push [rbx+50] push [rbx+58] // Parameters values are expected to have been pushed to the stack as // if a m32 operand was used. Since two of the parameter sets are on // SSE registers, we'll need to split low and high words, and then // dump them onto the stack as quadwords (low and then high). movhlps xmm1,xmm0 // Lets push the target's coordinates as the second parameter. sub rsp,8 movq [rsp],xmm0 sub rsp,8 movq [rsp],xmm1 // An identity matrix is used as we have not made the determination as // of yet as to whether the game uses true scaling or not. // We pass this as the third parameter. movss xmm0,[identityValue] shufps xmm0,xmm0,0 sub rsp,10 movdqu [rsp],xmm0 // Now we split up the movement offset SSE register. movhlps xmm1,xmm3 // Lets push the target's movement offsets as the final parameter. sub rsp,8 movq [rsp],xmm3 sub rsp,8 movq [rsp],xmm1 // Time to execute dat Predator system. call executePredator jmp initiatePredatorExit applyPlayerSpeed: // We'll be applying the player speed multiplier to all movement offsets // except the Z (vertical) offset. sub rsp,10 // We want to preserve the Z offset before applying the multiplier. movups [rsp],xmm3 mov ecx,[rsp+8] movss xmm0,[playerSpeedX] shufps xmm0,xmm0,0 mulps xmm3,xmm0 movups [rsp],xmm3 // Load the modifier X and Y offsets. mov eax,[rsp] mov ebx,[rsp+4] add rsp,10 initiatePredatorExit: // We'll need to make some room on the stack for temporary use so // we can load updated values back onto the SSE register used by // the original code to apply the movement offsets. sub rsp,10 // Start with the original movement offsets. movups [rsp],xmm3 // Apply the updated offsets. mov [rsp],eax mov [rsp+4],ebx mov [rsp+8],ecx movups xmm3,[rsp] add rsp,10 initiatePredatorCleanup: // Restore backed up values. pop rcx pop rbx pop rax movdqu xmm1,[rsp] add rsp,10 movdqu xmm0,[rsp] add rsp,10 initiatePredatorOriginalCode: popf addps xmm0,xmm3 movups [rsi],xmm0 jmp initiatePredatorReturn omnifyPredatorHook: jmp initiatePredator nop initiatePredatorReturn: identityValue: dd (float)1.0 playerSpeedX: dd (float)1.0 skipBoostY: dd 0 skipBoostZ: dd 1
We’re actually almost done! All that remains is the Abomnification system. Hopefully not much damage was done here.
Updating the Abomnification System Hooks
As we discussed at length earlier, Assassin’s Creed: Valhalla lacks built-in easy scaling, which means that two hooks were needed in order to implement the Abomnification system. The first, the initiation point for the Abomnification system, was placed into a coordinate polling function for NPCs. We wrote down its unique array of bytes as 0F 10 56 50 4C 8D 4C 24 30
.
Searching for that, it looks like we have a single hit! Yay! Hopefully we are able to make our hacks compatible with this latest version without incident. Let’s update our initiation point hook then.
Updated Abomnification Initiation Point Hook
// Initiates the Abomnification system. // UNIQUE AOB: 0F 10 56 50 4C 8D 4C 24 30 define(omnifyAbomnificationHook, "ACValhalla.exe" + 2823120) assert(omnifyAbomnificationHook, 0F 10 56 50 4C 8D 4C 24 30) alloc(initiateAbomnification,$1000, omnifyAbomnificationHook) registersymbol(omnifyAbomnificationHook) initiateAbomnification: pushf // Ensure that the player location struct has been initialized. push rax mov rax,playerLocation cmp [rax],0 pop rax je initiateAbomnificationOriginalCode // Back up the registers used to hold return values. push rax push rbx push rcx // Ensure that the player isn't going to be Abomnified. mov rax,playerLocation mov rbx,[rax] cmp rbx,rsi je initiateAbomnificationExit // Push the identifying address parameter and call the Abomnification system. push rsi call executeAbomnification initiateAbomnificationExit: // Restore the preserved values on the stack. pop rcx pop rbx pop rax initiateAbomnificationOriginalCode: popf movups xmm2,[rsi+50] lea r9,[rsp+30] jmp initiateAbomnificationReturn omnifyAbomnificationHook: jmp initiateAbomnification nop 4 initiateAbomnificationReturn: abominifyWidthResultUpper: dd #250 abominifyHeightResultUpper: dd #160 abominifyDepthResultUpper: dd #200 unnaturalSmallX: dd (float)0.5625
Fantabulous. All that remains now is the hook for applying the Abomnification generated scale multipliers. This was located in some rendering code, so for the sake out my sanity, let’s hope that we can find it again. The unique array of bytes for this was written down as 44 0F 59 05 15 B0 2C 03
.
And…no results. Crap! Did the patch actually change something as low level as the rendering code we were hooking into? Looking at the actual original instructions that were at play here, I quickly realized that had nothing to do with it at all.
mulps xmm8,[ACValhalla.exe+415A470]
We were hooking into the above instruction. If you can’t remember, this instruction is taking what I termed as the “global scale multipliers” and multiplying them against some vertex offset values loaded into xmm8
. These global scale multipliers were being stored and loaded from static memory. Well, of course there would be zero guarantee that this address wouldn’t change between versions.
In fact, you would be reasonably safe if you bet money on the chance that its address would change. So, note to self: don’t use the unique array of bytes for an instruction referencing static memory as a means to locate code in the future. Luckily for me, I had this lovely hackpad website I could use, which had a nice article on implementing the Abomnification system into this game, with screenshots showing code nearby the hook.
Using these screenshots, I found the proper place to insert the hook post-patch, and I wrote down a new unique array of bytes for code nearby, but not on top of where we were hooking into.
Updated Abomnification Scale Application Hook
// Applies Abomnification generated scale multipliers. // Unique AOB: 0F 11 62 10 4C 03 C1 define(omnifyApplyAbomnificationHook, "ACValhalla.exe" + E8F813) assert(omnifyApplyAbomnificationHook, 44 0F 59 05 55 DC 2E 03) alloc(applyAbomnification,$1000, omnifyApplyAbomnificationHook) registersymbol(omnifyApplyAbomnificationHook) applyAbomnification: pushf // Ensure that the player location struct has been initialized. push rax mov rax,playerLocation cmp [rax],0 pop rax je applyAbomnificationOriginalCode // Backing up an SSE register to hold our multiplier. sub rsp,10 movdqu [rsp],xmm0 // Backing up the registers used to hold return values by the // Abomnification system. push rax push rbx push rcx // A rendering supplementary data structure is stored on the stack. mov rax,[rsp+6A] // If the bytes at 0x14 are not zeroed, then the rendering data structure // is not pointing to a location structure. mov rbx,[rax+14] cmp ebx,0 jne applyAbomnificationExit // The location structure is found here, however we need to check that // it's a valid pointer, as there is still a chance for some junk data here. mov rbx,[rax+18] push rcx lea rcx,[rbx] call checkBadPointer cmp ecx,0 pop rcx jne applyAbomnificationExit // Ensure that the player isn't going to be Abomnified. mov rax,playerLocation cmp rbx,[rax] je applyAbomnificationExit // Push the identifying address parameter and get the Abomnified scales. push rbx call getAbomnifiedScales // Make some room on the stack so we can construct the multiplier SSE register. sub rsp,10 movss xmm0,[identityValue] shufps xmm0,xmm0,0 movups [rsp],xmm0 mov [rsp],eax mov [rsp+4],ecx mov [rsp+8],ebx movups xmm0,[rsp] add rsp,10 // Apply the Abomnified scale multipliers to the register that will be applied // against the global scale parameters. mulps xmm8,xmm0 applyAbomnificationExit: // Restore the preserved values on the stack. pop rcx pop rbx pop rax movdqu xmm0,[rsp] add rsp,10 applyAbomnificationOriginalCode: popf mulps xmm8,[ACValhalla.exe+417D470] jmp applyAbomnificationReturn omnifyApplyAbomnificationHook: jmp applyAbomnification nop 3 applyAbomnificationReturn:
Whew. We avoided having to have to look for that crap again.
Omnified AC: Valhalla is 1.1.1 Ready!
Hooray! Hopefully there won’t be another patch for a little bit. Time to enjoy the updates along with the rest of the world.
Until next time, take care.
~Omni