@
Cielos:
Well, well.. my last debugging session aimed to determine how the game calculates or fetches
CSrvPlayerFightAdrenalin. As you may have noticed, the
Adrenaline doesn't have an on-access instruction constantly ticking it. Bar is accessed/written to when you actually use it; or trigger an update event, so to say. So.. what I tried at first was to break on each member-function till the next block of functions. In most of today's games you have the first 2 quad-pointers (offset 0x0 and 0x8) as the constructor/destructor in the class; so it's easy to spot by checking the function's code if you've crossed the block's boundary to another block.
Here's how to debug it:
Code: Select all
function _readInteger( Input )
-- thanks, Pox!
local Value = readInteger( Input )
if Value < 0x80000000 then return Value
else return Value - 0x100000000 end
end
function GetName( input )
local addr = readQword( input )
addr = addr + 0x38 -- GetName; 0x30 for ACS; 0x38 for ACO
addr = readQword( addr )
if readBytes( addr, 1 ) == 0xE9 then
addr = addr + _readInteger( addr + 0x1 ) + 0x5
end
addr = addr + _readInteger( addr + 0x3 ) + 0x7
addr = readQword( addr )
print( string.format( "IStruct: 0x%X", input ) )
print( string.format( "IName: 0x%X", addr ) )
local str = readString( readQword( addr + 0x18 ) )
print( string.format( "ObjStr: %s", str ) )
addr = readInteger( addr + 0x24 )
print( string.format( "ObjHash: 0x%X", addr ) )
end
GetName( 0xyour_pointer )
We'll use the helper I wrote above. And feed it the pointer to
CSrvPlayerFightAdrenalin, which we can get from this location:
Code: Select all
ACOdyssey.exe+21D5C15 - 89 BB 78020000 - mov [rbx+00000278],edi
To get RBX, just use an Ability (I'm using Bull Rush for this example). And this will happen:
Now if I feed that RBX to the script in Lua, I get this:
And if we check that pointer in Memory View, bottom part:
Offset 0x0 contains the pointer to
member-functions, which, when checking the memory of, shows this table of quads (function addresses):
So, now, if I check the first function in top part, I see this:
That's the class constructor/destructor, if I recall. Then at offset
0x38 we have - what I call as -
GetName. The reason I named it like that is this:
That offset parkour logic leads to a name in structures that are normalized; at times you may need to offset the pointer a bit more (which the engine does quite weird) to get to that member-functions list. Bottom-line: the base pointer isn't always acquired directly; there's offsetting involved, but you can trace that out. See example below:
Code: Select all
GetCSrvPlayerFightStamina:
sub rsp,28
mov rcx,[pCharacterAI]
test rcx,rcx
je short @f
mov rcx,[rcx+40]
mov [rsp+20],rbx
mov rbx,[ACOdyssey.exe+56F2990]
mov rax,[rcx]
call qword ptr [rax+A0]
movzx ecx,byte ptr [rbx+49]
mov rbx,[rsp+20]
mov rax,[rax]
mov rcx,[rax+rcx*8]
test rcx,rcx
je short @f
mov rax,rcx
add rax,-0F8 // see this offset? well, that's why :P
add rsp,28
ret
@@:
xor eax,rax
add rsp,28
ret
Similarly, the function at offset
0x40 I named GetHash; because it does this:
With that out of the way, now time to find the length of this vtable. I started looking manually for functions like the one I showed above, at 0x0, and landed here:
So, if 0x180 is another GetName, then it means we've crossed the boundary to another block of member-functions. GetName is the function at offset 0x38 in the block, thus 0x180-0x38 = 0x148. This is where the new block of member-functions would start:
Now I know the CSrvPlayerFightAdrenalin's block of member-functions is 0x140 big. That gives us 0x140/8 = 0x28 = 40 functions. Out of which, most you'll see lead to "XOR EAX,EAX/RET"
What I did next was to break on almost all functions I found useful in this block; just to see if and when does the game break/uses them. But I applied a different logic: why not check-up on a GetName or GetHash function when you move from game to main menu and main menu to game world. Just to see what's going on, trying to determine how Anvil allocates it and where I might find it.
When moving from main menu to game world, I got these breaking:
So, member-function at offset 0x60. Then I got several hits on GetName and GetHash, but didn't find useful in the analysis.
Back to this function at offset 0x60; with CE frozen, do a Ctrl+G and type in [rsp] / Enter. You'll land here; scroll a bit:
I got intrigued by this piece of code:
Code: Select all
ACOdyssey.exe+EF917AD - 8B B7 94010000 - mov esi,[rdi+00000194]
ACOdyssey.exe+EF917B3 - 48 8B 9F 8C010000 - mov rbx,[rdi+0000018C]
ACOdyssey.exe+EF917BA - C1 EE 11 - shr esi,11 { 17 }
ACOdyssey.exe+EF917BD - C1 E6 03 - shl esi,03 { 3 }
ACOdyssey.exe+EF917C0 - 48 01 DE - add rsi,rbx
ACOdyssey.exe+EF917C3 - 48 39 F3 - cmp rbx,rsi
ACOdyssey.exe+EF917C6 - 74 17 - je ACOdyssey.exe+EF917DF
What this does is to fetch the base pointer to another vtable; then uses esi to calculate table size and computes the right or bottom boundary of the table. Of course I now got curious who RDI is and what this table contains:
Nice;
CharacterAI. I already have a method to fetch this in my Cheat Handler
Let's take a look at the table:
- offset 0x194 contains this value: 00240012
- offset 0x18C contains this pointer: 000000092D918830
Doing the math gives this: shl( shr 00240012,11 ), 3 == 0x90. So table we look at is this one:
As you can see, it contains several pointers
I wonder what we can use to inspect them
The Lua helper. Doing this I got the following:
- 0x0: CSrvPlayerNPCAwareness
- 0x8: CSrvNavigation
- 0x10: CSrvFightLogic
- 0x18: CSrvAnimatedAction
- 0x20: CSrvKillStreak
- 0x28: CSrvPlayerFightAdrenalin
..
..
- 0x88: CSrvMeditation
So our
CSrvPlayerFightAdrenalin can be retrieved with the help of
CharacterAI Offsets 0x18C and 0x194 contain the start vtable pointer and size; we can then write a loop that iterates the pointers from start till end of table, looking for the one whose GetHash is 0x377E5659. This hash is also stored in memory, so you don't need to actually call-in member-function 0x40. See this:
Code: Select all
print( string.format( "IStruct: 0x%X", input ) )
print( string.format( "IName: 0x%X", addr ) )
local str = readString( readQword( addr + 0x18 ) )
print( string.format( "ObjStr: %s", str ) )
addr = readInteger( addr + 0x24 )
print( string.format( "ObjHash: 0x%X", addr ) )
Here's how you'd do it with Lua, based on current 1.0.3 (I will use raw address calling):
Code: Select all
function lshift(x, by)
return x * 2 ^ by
end
function rshift(x, by)
return math.floor(x / 2 ^ by)
end
local GameProcess = "ACOdyssey.exe" -- our game
local GameModule = getAddress( GameProcess ) -- get ModuleBase
print( string.format( "ModuleBase: 0x%X", GameModule ) )
local GetWorld = GameModule + 0x183DA50 -- this function retrieves a static, World
--print( string.format( "0x%X", GetWorld ) )
local World = executeCode( GetWorld ) -- fetch it
print( string.format( "World: 0x%X", World ) )
local Entity = readQword( World + 0x90 ) -- read Entity from World
print( string.format( "Entity: 0x%X", Entity ) )
local GetBhvAssassin = GameModule + 0x13DEBC0
local BhvAssassin = executeCode( GetBhvAssassin, Entity ) -- get BhvAssassin using Entity
print( string.format( "BhvAssassin: 0x%X", BhvAssassin ) )
local CharacterAI = readQword( BhvAssassin + 0x28 ) -- read CharacterAI
print( string.format( "CharacterAI: 0x%X", CharacterAI ) )
print( "" )
-- time to have the fun I mentioned above
local TableStart = readQword( CharacterAI + 0x18C )
print( string.format( "TableStart: 0x%X", TableStart ) )
local TableIntSize = readInteger( CharacterAI + 0x194 )
--print( string.format( "0x%X", TableIntSize ) )
local TableSize = lshift( rshift( TableIntSize, 0x11 ), 0x3 )
print( string.format( "TableSize: 0x%X", TableSize ) )
local TableEnd = TableStart + TableSize
print( string.format( "TableStart: 0x%X", TableEnd ) )
print( "" )
local i = 0x0
local j = 0x0
for i = TableStart,TableEnd-8,8 do
local p = readQword( i )
--print( string.format( "0x%X: 0x%X", j, p ) )
-- read pointer to member-functions
local MainPointer = readQword( p )
--print( string.format( "0x%X", MainPointer ) )
-- get functions at 0x38 and 0x40
local GetName = readQword( MainPointer + 0x38 )
--print( string.format( "0x%X", GetName ) )
local GetHash = readQword( MainPointer + 0x40 )
--print( string.format( "0x%X", GetHash ) )
-- get hash via executing the function
local HashA = executeCode( GetHash )
--print( string.format( "0x%X", HashA ) )
-- run GetName then obtain the string from the result
local fName = executeCode( GetName )
--print( string.format( "0x%X", fName ) )
-- get hash via reading it from Name
local HashB = readInteger( fName + 0x24 )
--print( string.format( "0x%X", HashB ) )
Name = readString( readQword( fName + 0x18 ), 6000 )
--print( Name )
print( string.format( "0x%X - - | IStruct: 0x%X | IName: 0x%X | ObjStr: %s | ObjHash: 0x%X", j, p, fName, Name, HashB ) )
-- increase iterator
j = j + 0x8
end
Result:
If you wanna use this, you'll need a thread to call these up or simply push/pop registers in and out in your hooks.
BR,
Sun
P.S.: Ooops, I did it again