Serious Sam 3: BFE & Serious Sam 4

Add topics here with methods, analysis, code snippets, mods etc. for a certain game that normally won't make it in the Tables or Requests sections.
Post Reply
User avatar
SunBeam
Administration
Administration
Posts: 4704
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 4287

Serious Sam 3: BFE & Serious Sam 4

Post by SunBeam »

Game Name: Serious Sam 3: BFE & Serious Sam 4
Game Vendor: Steam
Game Version: 261096 (3) | 557897 (4) [ check main menu, top-right ]
Game Process: Sam3.exe | Sam4.exe
Game File Version: -





Hello folks.

I've recently checked the Serious Engine in Serious Sam 4. Since I'm curious how things work, my analysis led to determining several critical functions one may use in reverse-engineering titles built with this Engine. First-up, the version used for 3 & 4 is Serious Engine 3.5. The only difference between them (aside from perhaps additional functions created specifically for 4) is the fact that the version in 3 is x86, while the version used in 4 is x64. I'm assuming all titles between Serious Sam 3 (2011) and Serious Sam 4 (2020) are x64 as well. Serious Sam Fusion 2017, for example, is x64.

Now I understand there are mixed opinions about the game itself, the characters, the story, etc. The goal here is not to open up the topic to shit-posting, but focus on Serious Engine and internals. So if you want to express yourself in the "why? game's shit anyway" manner, do that on another forum. I'll ban those who wanna fuck around and ignore what I just said.

I know you want to hear the thought process, how to do it to get to my results, so I'll try to explain as best as I can, without dragging this too long. I am using [Link] and [Link] combined, though you don't need both. It's easier for me to see a whole large function in one screen with x64dbg, rather than using a large font and scrolling 50 times to get to a function's epilogue.

The initial idea was to find a way to the game's console. Since I've not played the game thus far, the console is there. You can open it with the Tilde (~) key and enable various things, as per [Link]. Note that I've picked a certain article, as most results on google talk only of cht_bEnableCheats=1. There are Developer cheats which can only be activated if cht_bEnableCheats=3. Most of articles out there fail to specify this aspect, which kinda implies there've been copy-pastes of one another.

That being said, there was no point trying to have a go at the game/Engine. Then I noticed that if you enable cheat mode, achievements would be disabled for the current game session. It's annoying to see something like that and while I understand why developers do that, I don't really support it. Especially when this can be disabled. So that was the starting point: how to disable checking for enabled cheats.

Cheats Analysis

The first thing I noticed is when you enable cheats with bool value = 3 (cht_bEnableCheats=3), you see this in your right-hand side:

Image

As you enable cheats, they will be listed in that box, under the header. So I looked for string references in x64dbg while attached to the game (you can also load the .exe directly and do static analysis) and found this:

Image

Then checked out that function and applied knowledge I learned from other Engines. The first logic: cheats would toggle the same memory location with powers of 2. First cheat = 2^0 = 1. Second cheat = 2^1 = 2. And so on. Looked at my function and saw this:

Image

While scrolling.. showed these:

Image

Image

And so on.. Full list being:

Code: Select all

000000014041F16A | F687 CC260000 04       | TEST BYTE PTR DS:[RDI+26CC],4    |
000000014041F171 | 74 49                  | JE sam4.14041F1BC                |
000000014041F173 | 48:8D0D 26463F01       | LEA RCX,QWORD PTR DS:[1418137A0] | 00000001418137A0:"ETRSHud.Cheats.God=God"
..
000000014041F1C9 | F787 CC260000 00010000 | TEST DWORD PTR DS:[RDI+26CC],100 |
000000014041F1D3 | 74 49                  | JE sam4.14041F21E                |
000000014041F1D5 | 48:8D0D DC453F01       | LEA RCX,QWORD PTR DS:[1418137B8] | 00000001418137B8:"ETRSHud.Cheats.InfiniteStamina=Infinite Stamina"
..
000000014041F21E | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F224 | A8 08                  | TEST AL,8                        |
000000014041F226 | 74 4F                  | JE sam4.14041F277                |
000000014041F228 | 48:8D0D B9453F01       | LEA RCX,QWORD PTR DS:[1418137E8] | 00000001418137E8:"ETRSHud.Cheats.Invisible=Invisible"
..
000000014041F271 | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F277 | A8 01                  | TEST AL,1                        |
000000014041F279 | 74 4F                  | JE sam4.14041F2CA                |
000000014041F27B | 48:8D0D 8E453F01       | LEA RCX,QWORD PTR DS:[141813810] | 0000000141813810:"ETRSHud.Cheats.Ghost=Ghost"
..
000000014041F2C4 | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F2CA | A8 02                  | TEST AL,2                        |
000000014041F2CC | 74 4F                  | JE sam4.14041F31D                |
000000014041F2CE | 48:8D0D 5B453F01       | LEA RCX,QWORD PTR DS:[141813830] | 0000000141813830:"ETRSHud.Cheats.Fly=Fly"
..
000000014041F317 | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F31D | A8 40                  | TEST AL,40                       |
000000014041F31F | 74 4F                  | JE sam4.14041F370                |
000000014041F321 | 48:8D0D 20453F01       | LEA RCX,QWORD PTR DS:[141813848] | 0000000141813848:"ETRSHud.Cheats.Turbo=Turbo"
..
000000014041F36A | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F370 | 84C0                   | TEST AL,AL                       |
000000014041F372 | 79 49                  | JNS sam4.14041F3BD               |
000000014041F374 | 48:8D0D ED443F01       | LEA RCX,QWORD PTR DS:[141813868] | 0000000141813868:"ETRSHud.Cheats.AutoAiming=Auto aiming"
..
000000014041F46E | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F474 | A8 20                  | TEST AL,20                       |
000000014041F476 | 74 4F                  | JE sam4.14041F4C7                |
000000014041F478 | 48:8D0D 51443F01       | LEA RCX,QWORD PTR DS:[1418138D0] | 00000001418138D0:"ETRSHud.Cheats.InfiniteAmmo=InfiniteAmmo"
..
000000014041F4C1 | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F4C7 | A8 10                  | TEST AL,10                       |
000000014041F4C9 | 74 4F                  | JE sam4.14041F51A                |
000000014041F4CB | 48:8D0D 2E443F01       | LEA RCX,QWORD PTR DS:[141813900] | 0000000141813900:"ETRSHud.Cheats.UnlimitedTimeInPast=Unlimited Time In Past"
..
000000014041F514 | 8B87 CC260000          | MOV EAX,DWORD PTR DS:[RDI+26CC]  |
000000014041F51A | 0FBAE0 09              | BT EAX,9                         |
000000014041F51E | 73 41                  | JAE sam4.14041F561               |
000000014041F520 | 4C:8D05 C1271E01       | LEA R8,QWORD PTR DS:[141601CE8]  | 0000000141601CE8:"Pass through plasma walls"
..
I tested the values applied to that [RDI+26CC] and with each I noticed they would appear under DEVELOPER CHEATS ENABLED header on-screen. I then wanted to see if really these are the effects and discovered they aren't. [RDI+26CC] is just a visual indicator reflecting which cheat is enabled. The real effect I discovered by looking in memory for CC 26 00 00 (26CC) array of bytes and evaluating the functions containing the instructions using this offset. Will explain after the below detour. For now, you've probably seen my table: viewtopic.php?f=4&t=13876, especially this part:

Image

* * *

I want to pause a bit to explain WHO rdi above is. If you've read my other posts (Horizon Zero Dawn, Mafia: Definitive Edition, Marvel's Avengers, etc.) you will know by now what I will do. I'm going to check out the member-functions table to find a function that leads to a CLASS name. :

Image

The function above is IsInfiniteAmmo. Why? Because, as you can see, there's this: "TEST BYTE PTR DS:[RBX+0x26CC],0x20". And if you look at the screenshot above, from my table, you see that b_chtInfiniteAmmo is a 0x20 toggle. Set a break there and inspect RCX as you fire your gun (I used map 1, pistol). On break, my rcx has this value: 00000000CE047440.

Then follow that in dump:

Image

You can already see a "PlayerPuppet" reference. Then follow in dump the pointer to member-functions located at 0x00: 0000000141811F68. It's highlighted in the screenshot above. And we see this:

Image

Then check the first function in disassembler:

Image

The reason you see that naming in there is I manually named that global pointer, leading to a class. But in reality, right-click the address and choose Follow in dump > Value and you will see this:

Image

So the first member-function leads to a structure that at offset 0x18 contains a pointer to the Class name. We will call this function GetClass. Remember it's at 0x00 in any base structures.

With that in mind.. we're now looking at CPlayerPuppet structure. Which now explains the names you've seen in my table, in the [ Debug ] section. Which means [ CPlayerPuppet + 0x26CC ] is the DWORD value used to check which cheat is enabled.

* * *

Detour over, I found the real cheat effects here:

Image

Image

And so on.

Based on the value stored @ CPlayerPuppet+0x26CC, the above are toggled to 0 or 1. In short, in the current build I'm looking at (557897), these are:

Code: Select all

0000000140151EBF | 8B0D A36E0F02 | MOV ECX,DWORD PTR DS:[<cht_bGiveAll>]     |
0000000140151EC5 | 85C0          | TEST EAX,EAX                              |
0000000140151EC7 | 41:0F45CC     | CMOVNE ECX,R12D                           |
0000000140151ECB | 890D 976E0F02 | MOV DWORD PTR DS:[<cht_bGiveAll>],ECX     |
..
0000000140151EEB | 3905 676E0F02 | CMP DWORD PTR DS:[<cht_bGodMode>],EAX     |
0000000140151EF1 | 0F94C0        | SETE AL                                   |
0000000140151EF4 | 8905 5E6E0F02 | MOV DWORD PTR DS:[<cht_bGodMode>],EAX     |
..
0000000140152012 | 3905 206D0F02 | CMP DWORD PTR DS:[<cht_bTurbo>],EAX       |
0000000140152018 | 0F94C0        | SETE AL                                   |
000000014015201B | 8905 176D0F02 | MOV DWORD PTR DS:[<cht_bTurbo>],EAX       |
..
000000014015203B | 3905 E76C0F02 | CMP DWORD PTR DS:[<cht_bGhost>],EAX       |
0000000140152041 | 0F94C0        | SETE AL                                   |
0000000140152044 | 8905 DE6C0F02 | MOV DWORD PTR DS:[<cht_bGhost>],EAX       |
..
000000014015205F | 3905 C76C0F02 | CMP DWORD PTR DS:[<cht_bFly>],EAX         |
0000000140152065 | 0F94C0        | SETE AL                                   |
0000000140152068 | 8905 BE6C0F02 | MOV DWORD PTR DS:[<cht_bFly>],EAX         |
..
0000000140152083 | 3905 B76C0F02 | CMP DWORD PTR DS:[<cht_bAutoAiming>],EAX  |
0000000140152089 | 0F94C0        | SETE AL                                   |
000000014015208C | 8905 AE6C0F02 | MOV DWORD PTR DS:[<cht_bAutoAiming>],EAX  |
..
00000001401520A7 | 3905 836C0F02 | CMP DWORD PTR DS:[<cht_bInvisible>],EAX   |
00000001401520AD | 0F94C0        | SETE AL                                   |
00000001401520B0 | 8905 7A6C0F02 | MOV DWORD PTR DS:[<cht_bInvisible>],EAX   |
..
Note that I've manually named the static addresses to suit my needs (cht_*). And now you know :P

Ignoring Damage without Cheats

Further debugging cht_bGod and cht_bIgnoreDamageInGod cheats' effects led to discovering that what really happens behind the hood is a flag in the Entity structure receiving damage is being checked for a certain value. Namely 0x7A4 in Entity. And that led me to basically hooking the function that checks this flag BEFORE computing and executing the code performing the damage effects and writing Health. Which happens here:

Image

The above constitutes the hook spot for the Ignore Damage (Perfect God Mode) script in my table, idea being as long as the Entity being processed is NOT CPlayerPuppet, flip the bool at 0x7A4 to 0x1 (which is the value from the TEST check), making us invulnerable. The effect acts like cht_bGod + cht_bIgnoreDamageInGod cheats combined. But since we're not using the actual cheats or their effects, the game will not know we're cheating. Thus no achievements get disabled.

Extra Functions

I then wanted to know how do I get CPlayerPuppet, our Player structure. And found that the Engine is using a certain logic: gets Entity structures by reading an id from some place and feeding this as argument to a certain function. It's always the same function. So.. player, enemies, allies, mech, weapons, etc. - any entity - is generated independently from certain templates. They are then attributed ids which can be used as arguments with the function I mentioned to retrieve the Entity pointer :) Similarly, you can feed an Entity pointer to another function, obtaining the assigned id.

I called these GetEntityById and GetEntityId. And this happens here:

GetEntityById:
Image

GetEntityId:
Image

What you can do with the above functions? Well, this:

GetCurrentWeapon:

Code: Select all

mov [CPlayerPuppetEntity],rcx
mov ecx,[rcx+F88] // dwCurrentWeaponHash
call GetEntityById
mov [CWeaponEntity],rax
GetMechPuppet:

Code: Select all

mov rcx,[CPlayerPuppetEntity]
mov ecx,[rcx+75C] // dwCMechPuppetEntity
call GetEntityById
mov [CMechPuppetEntity],rax
I found the above offsets in CPlayerPuppet by back-tracing from bullets scanned with CE and debugged all the way back to Weapon and how Weapon is retrieved via the GetEntityById function. Same for Mech in maps where you can use one.

Weapon Logic

Now that you have CPlayerPuppet pointer you can get to the currently held weapon. To do so, check out the value stored @ offset 0xF88 in CPlayerPuppet and run it through the GetEntityById function:

Code: Select all

mov [CPlayerPuppetEntity],rcx
mov ecx,[rcx+F88] // dwCurrentWeaponHash
call GetEntityById
mov [CWeaponEntity],rax
Next-up, each Weapon has a Primary and Secondary firing behavior. These are stored at 0x310 and 0x320 in CWeaponEntity. Then each behavior has a WeaponMode. For the Primary, the WeaponMode is a pointer to pointer, for the Secondary just a pointer:

Code: Select all

mov rax,[CWeaponEntity]
cmp rax,rcx
jne short @f     // if not our CWeaponEntity
  movsxd rax,[rcx+330]
  cmp eax,-1
  je short GetPrimaryWeaponModeId_o    // if not a fireable weapon
    // get Primary Mode
	mov rdx,rax
	mov rax,[rcx+310]
	mov rcx,[rax+rdx*8]
    mov [CPrimaryWeaponMode],rcx
	// get Secondary Mode
	mov rcx,[CWeaponEntity]
	mov rcx,[rcx+320]
	test rcx,rcx
    mov [CSecondaryWeaponMode],rcx
@@:
I chose a location where 0x310 offset is read in CWeaponEntity and hooked there. Then the above code. So that explains the Infinite Clip + Fast Fire script. Once you get each WeaponMode, offset 0x10 will always hold a pointer to Params. It's in the Params you can alter the amount being subtracted as you fire, fire rate and other things I didn't fiddle with :P

Disable "Full" Check

When you reach the max of a property, you cannot pick stuff from the ground. Making the map look like a Christmas tree and hard to spot secrets (which usually glow in purple color). Not to mention some events are triggered by picking a certain item from the ground (like a health pack), such as spawning secret mobs or waves of them.

To get this done I used my Armor value. Found its address and debugged it on write. Then set a breakpoint at the instruction writing to it and back-traced as much as I could. I found a spot where, if I tried to pick an Armor pack from the ground, the game wouldn't write to my Armor value. And this happens here:

Image

Notice the "FailedPick" string reference :) To kill this so you can pick anything from the ground (note: there are chests containing ammo; those are not included in this, game will still say "full" there) simply 90E9 the JNE (quick patch for a JMP):

Code: Select all

000000014024E986 | 0F85 AD000000                       | JNE sam4.14024EA39                                         |
to

Code: Select all

000000014024E986 | 90                                  | NOP                                                        |
000000014024E987 | E9 AD000000                         | JMP sam4.14024EA39                                         |
Whatever you pick from the ground is treated as if not a "FailedPick", therefore processed and vanishing. You won't, however, get the amounts past the max caps. This just helps removing pick-able ground items, which are annoying if you have infinite Health and Clip/Ammo.

BR,
Sun

User avatar
SunBeam
Administration
Administration
Posts: 4704
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 4287

Re: Serious Sam 3: BFE & Serious Sam 4

Post by SunBeam »

So with that in mind and having finished Serious Sam 4, I thought I'd drop by and play 3 a little. See if it's as crappy as I thought 4 was (yeah, broke the "no shit-posting" rule). Played half of the first map in "Serious" difficulty, then realized you can trick the fuckers spawning by getting up on ledges or buildings. Then shoot your heart out. At the same time I said enough and let's bring the stuff I learned in 4 done to 3. Opened up x64dbg (x64 one) and tried to find the game window. It was not among the ones in the list. Relaunched with Administrator privileges, still not there.. Then I thought.. "wait a sec, is this x64?". Checked wiki and noticed release date was 2011. Oh shit.. so I fired up x32dbg and now I'm in x86 realm again :D Fun times.

Static Analysis x64 <-> x86

Thought to myself.. "It's gonna take a while to get the same functions I found in 4 mapped in 3, because 4 is x64, 3 is x86. That.. assuming the Engine is the same in terms of code layout". So I looked at the function hooked for damage checking:

Image

Noticed there's a TEST BYTE PTR [r64+offset],1 in there, as well as a CMP DWORD PTR [r64+offset],7. So those would be my references: find a function in x86 that contains a test for 0x1 and a cmp for 0x7 in the chain. Then I thought "why not search for all occurrences of 0x7A4 in Sam 4, then make use of string references around that to find the offset I want in 3?". And found this in 4:

Image

Decided to give "ETRSMeleeMessage=%1 Melee" a chance. So I looked up all string references in 3 and filtered the list to "Melee" word:

Image

Although it's not the same identical string reference, looking in that area showed this:

Image

Boom. Found my offset :) 0x620 in Sam 3; 0x7A4 in Sam 4.

So now I want to find a function that contains a TEST BYTE PTR [r32+620],1 that has a CMP DWORD PTR [r32+offset],7 somewhere in. So I looked for all references to 0x620 offset. There were 240, so checking each, 1 by 1, would take some time. Then I thought "hey, the mnemonic for a TEST shouldn't be different on x86". Meaning I can use the bytes like so:

Code: Select all

00000001402A5A3B | F683 A4070000 01 | TEST BYTE PTR DS:[RBX+7A4],1 |
F6 83 = TEST BYTE PTR
xx xx xx xx = offset
01 = the 1 being TEST-ed

So why not search the same thing on x86? :) Now that I know the offset, I can add it in: F6 ?? 20 06 00 00 01. I skipped the 83, because.. maybe on x86.. the offset being used when Engine got compiled isn't the same as in x64. As in the transition between RBX and EBX isn't the same. And I got this:

Image

2 results. Out of which, the 2nd one was the one I was looking for:

Image

The function has a TEST for 1 (and you can see EBX isn't used here, but ESI, so I wouldn't have found this if I put 0x83 in my array) and a CMP for 7 close to the end. So it has to be it! :P

So there you have it. This is how I found the rest, through resemblance, string references and approximations. And all of this without actively debugging the game; just static analysis between the Sam4.exe and Sam3.exe files.

Mapping Sam4 Functions in Sam3

With the above in mind, I got these spots mapped out:

CheckDamagedEntity:
Image

GetEntityById:
Image

GetEntityId:
Image

GetPlayer hook:
Image

Disable "Full" Check:
Image

" Hey, WTF are these creatures doing here and why aren't they dying? This isn't normal. "

So played through first level and entered that museum where I'm supposed to find some professor. Got all the way through to a sort of catacombs and that's where I met these fucks:

Image

Not 1, but 2. Right before the cut-scene where Sam finds the doctor dead. Fired at them for a while, but nothing happened :) Then I just ignored them and went on my merry way to end the level. Got to some room where some big ass Scorpion King showed:

Image

Firing at the boss teleports you back to the starting point :D So I went back and on my way there I remember at some point the game crashed. Some error in mid-screen and bye bye. It's true I was debugging the game at the time, so it might've been a fluke :P Decided to give google a quick search and found countless posts about some protection Croteam put into the executable. Looked at it and it wasn't anything Steam-VAC-related, nor Denuvo. Of course.. 2011.. don't even remember if Denuvo was alive back then.

Example: [Link]

What irked me the most is the "herd effect". Some fuckwad somewhere said "you get the immortal scorpion if you play a cracked version". So the rest of the fuckwards not feeling cool or not getting a rush from their boring lives decided to follow in. So that's how the rumor spread, creating more retarded people in the process. That's why, while reading, I saw people stating they own the original game, bought on Steam, so that is out of the question. What they omitted to say is they were cheating, just so the community won't brand them as such. The many faces of the hypocrite e-society. Which today we call Reddit.

Bottom line is there's no DRM check, no "cracked version" check. The developers simply chose a range they create a checksum out of and if the resulting value doesn't match the hardcoded one, then THINGS happen. Yes, plural. Because this type of check can be found in several places throughout the code. And because using a trainer will modify executable code.. if that code that's modified happens to be in the hashed ranges.. you get that immortal scorpions and other effects. Whether you play the original or cracked version T_T. I will explain the process in the below. Why? Because I found some interesting aspects and because I fail to understand why Croteam would revolve to such stupid crap to discourage cheating. Especially when they themselves leave the console active with the possibility to enable cheats manually.. But then again.. it's not about cheating, it's about online gaming and leaderboards.

Therefore I decided to give my itching hands a go :)

The Immortal Scorpion

So.. not knowing how this happens, I started from the premise: what do "cracked versions" and "tampered with code" have in common. They're not in the original form the developers intended their executable to be. And how would the developers check this? Well.. by accessing (aka reading) the piece of code that's changed. Considering I own the Steam version, the "cracked" crap wouldn't apply in this scenario. Considering I had 3 CE scripts enabled at the time the scorpions showed up, I know what to do: debug on access a byte of one of the instructions I hooked. See who wants to read it and why.

(in progress)

User avatar
Csimbi
RCE Fanatics
RCE Fanatics
Posts: 878
Joined: Sat Apr 29, 2017 9:04 pm
Reputation: 1203

Re: Serious Sam 3: BFE & Serious Sam 4

Post by Csimbi »

Oh noes!

Post Reply

Who is online

Users browsing this forum: AngeHell