One more thing I want to show you and I'm done with this title. This one thing should apply to any other UE4 game out there, considering most CheatManager functions are C++ compiled code now. I wanted to find out why
fly works in
We Happy Few and doesn't in the games I started analyzing (Darksiders 3, OVERKILL's The Walking Dead). I've created a similar DLL for WHF to dump Names/Objects) and started my analysis.
Let's dive in - - warning: long post below.
Fired up We Happy Few (will shorten it to WHF). Started game, got past the first few actions till I could move around. Injected the DLL to dump Names/Objects in .txt files, then attached x64dbg.
fly can be found via string references and is here:
-- wrapper --
-- actual function --
The parkour goes like this:
- RCX is:
Code: Select all
[154909] GlimpseCheatManager GlimpsePrologue_Arthur.GlimpsePrologue_Arthur.PersistentLevel.DefaultPlayerController_C_1.GlimpseCheatManager_1 0x000002225976D400
So.. a CheatManager pointer.
- after the first MOV, RAX becomes:
Code: Select all
[154882] DefaultPlayerController_C GlimpsePrologue_Arthur.GlimpsePrologue_Arthur.PersistentLevel.DefaultPlayerController_C_1 0x000002223DBCEA70
- at the TEST instruction, RBX is:
Code: Select all
[185737] ArthurCharacter_C GlimpsePrologue_Arthur.GlimpsePrologue_Arthur.PersistentLevel.ArthurCharacter_C_1 0x00000222508CC000
The next part is not of interest, as it fetches the console interface, the position in the console at which to write "You feel much lighter" string and writing it to the actual console. What is important starts here:
Notice how the engine loads-up that string -> "/Script/Engine". A CALL follows inside of which we see:
And that CALL returns:
Code: Select all
[000165] Class Engine.Character 0x0000022209D65100
A loop follows:
Code: Select all
00007FF77217C3A0 | 48:8B4B 10 | MOV RCX,QWORD PTR DS:[RBX+10] | rcx:L"/Script/Engine"
00007FF77217C3A4 | 4C:8D80 88000000 | LEA R8,QWORD PTR DS:[RAX+88] |
00007FF77217C3AB | 49:6340 08 | MOVSXD RAX,DWORD PTR DS:[R8+8] |
00007FF77217C3AF | 3B81 90000000 | CMP EAX,DWORD PTR DS:[RCX+90] |
00007FF77217C3B5 | 7F 30 | JG glimpsegame.7FF77217C3E7 |
00007FF77217C3B7 | 48:8BD0 | MOV RDX,RAX |
00007FF77217C3BA | 48:8B81 88000000 | MOV RAX,QWORD PTR DS:[RCX+88] |
00007FF77217C3C1 | 4C:3904D0 | CMP QWORD PTR DS:[RAX+RDX*8],R8 |
00007FF77217C3C5 | 75 20 | JNE glimpsegame.7FF77217C3E7 |
00007FF77217C3C7 | 48:8BCB | MOV RCX,RBX | rcx:L"/Script/Engine"
00007FF77217C3CA | E8 D1F44D00 | CALL glimpsegame.7FF77265B8A0 |
RBX is ArthurCharacter_C. At 0x10 offset inside this structure we find:
Code: Select all
[113385] BlueprintGeneratedClass ArthurCharacter.ArthurCharacter_C 0x0000022232313000
RAX is our fetched Class; RAX is adjusted to Class+0x88. We then see a DWORD is read from this location+0x8. In my case, the value is 0x3. Then this value is checked against [BlueprintGeneratedClass+90] (the class at 0x10 from earlier). The value I found there is 0x7. As long as EAX < [RCX+90] (3 < 7), the JG will not jump. So the next block executes:
Code: Select all
00007FF77217C3B7 | 48:8BD0 | MOV RDX,RAX |
00007FF77217C3BA | 48:8B81 88000000 | MOV RAX,QWORD PTR DS:[RCX+88] |
00007FF77217C3C1 | 4C:3904D0 | CMP QWORD PTR DS:[RAX+RDX*8],R8 |
00007FF77217C3C5 | 75 20 | JNE glimpsegame.7FF77217C3E7 |
A pointer is now read from 0x88 offset of the BlueprintGeneratedClass. We then see the CMP indicates that 0x3 value is actually an offset; and that is because of this setup:
Code: Select all
00007FF77217C3C1 | 4C:3904D0 | CMP QWORD PTR DS:[RAX+RDX*8],R8 |
RDX*8 indicates QWORD offsetting.
As long as the two pointers match, the next block executes; else, JNE hops to exit.
So, we're here:
Code: Select all
00007FF77217C3C7 | 48:8BCB | MOV RCX,RBX |
00007FF77217C3CA | E8 D1F44D00 | CALL glimpsegame.7FF77265B8A0 |
RBX moved into RCX; RCX becomes:
Code: Select all
[185737] ArthurCharacter_C GlimpsePrologue_Arthur.GlimpsePrologue_Arthur.PersistentLevel.ArthurCharacter_C_1 0x00000222508CC000
Updated the parkour:
Let's see if any other registers are used inside the next CALL, aside from RCX:
Doesn't look like. RCX is the one we need; RDX is filled from that static. And R8 is NULL-ed past the first CALL inside this function. Now let's see what's written to RDX
This represented the reason I started writing this.
OK. So RDX is 0x1619. Keep this in mind for now. You'll soon learn what's the use of it and where can we find it
Continuing with the execution, I peeked inside the next CALL and saw this:
Interesting; could it be that.. what the CALL does is to actually retrieve an UFunction? Let's F8 over it and check the result (the result of a function, if returning anything, is stored in RAX) -> so my RAX is . Looking it up in the .txt file I see that it is:
Code: Select all
[016707] Function Engine.Character.ClientCheatFly 0x0000022211737440
Ta-naaa
Now.. remember that 0x1619? Let's take a look at this UFunction in memory:
So offset 0x18 contains the look-up ID we want
Continuing with the code, we're here:
Code: Select all
00007FF77265B8BC | 48:8BD0 | MOV RDX,RAX |
00007FF77265B8BF | 45:33C0 | XOR R8D,R8D |
00007FF77265B8C2 | 48:8BCF | MOV RCX,RDI |
00007FF77265B8C5 | 48:8B83 A8010000 | MOV RAX,QWORD PTR DS:[RBX+1A8] |
00007FF77265B8CC | 48:8B5C24 30 | MOV RBX,QWORD PTR SS:[RSP+30] |
00007FF77265B8D1 | 48:83C4 20 | ADD RSP,20 |
00007FF77265B8D5 | 5F | POP RDI |
00007FF77265B8D6 | 48:FFE0 | JMP RAX |
RAX is moved to RDX, so this becomes the 2nd (or first parameter if RCX == __this) of the next function to be executed; R8 is NULLed (this is the 3rd parameter); RCX is updated with RDI, so it becomes ArthurCharacter_C. Then RAX is updated with the function address JMP RAX will hop to, RBX restored and function epilogue run to restore stack. Then..
JMP RAX takes us here:
I'll stop the explanations here. Why? Because this function will execute the UFunction
So, in pseudo-code, we have this:
ExecUFunction( ArthurCharacter_C, Function Engine.Character.ClientCheatFly, 0 ). Where:
- ExecUFunction == 00007FF7720709E0
- ArthurCharacter_C == 00000222508CC000
- Function Engine.Character.ClientCheatFly == 0000022211737440
In my case.
Conclusions:
- to find the pointer to the base Class, look at the first part of your UFunction -> Function Engine.
Character.bla; remember the function referencing "Character"? (4th screenshot in this post)
- to find the function ID, get the address from our .txt file, browse it in memory and fetch it from 0x18 offset
- you'll have to manually track down the FindUFunction address; the ExecUFunction can be obtained as member-function in the UObject (0x1A8 is the offset)
- lastly, you need to make this simple logical correspondence to determine which UObject the function is applied to -> function is called "Engine.Character.ClientCheatFly"; OK.. so.. Character.. who is the Character? Well, can it be.. ArthurCharacter_C?
Apply the same logic to other functions you want to run. Note that some may have parameters; in which case, R8 will not have to be NULL (if one parameter the function is called with). If multiple parameters, you need to use the stack.
I'll post later a thread handler that allows you to run SetFuryGodMode UFunction in Darksiders 3 as proof of concept
BR,
Sun