Skip to content

Instantly share code, notes, and snippets.

@19h
Last active April 4, 2025 12:00
Show Gist options
  • Save 19h/6d48dccf02daa944dd830a06d2aaf3f6 to your computer and use it in GitHub Desktop.
Save 19h/6d48dccf02daa944dd830a06d2aaf3f6 to your computer and use it in GitHub Desktop.
sub_143434440_hook based on Sycorax's original concept
; =============== S U B R O U T I N E =======================================
; __int64 __fastcall sub_143434440(__int64, unsigned int)
sub_143434440 proc near ; CODE XREF: sub_14342B5D0+96↑p
; sub_1434E8C80+24↓p
; DATA XREF: ...
var_38 = dword ptr -38h
var_34 = dword ptr -34h
var_30 = qword ptr -30h
var_28 = qword ptr -28h
var_20 = qword ptr -20h
var_18 = qword ptr -18h
var_10 = qword ptr -10h
arg_0 = qword ptr 8
arg_8 = dword ptr 10h
89 54 24 10 mov [rsp+arg_8], edx
48 89 4C 24 08 mov [rsp+arg_0], rcx
48 83 EC 58 sub rsp, 58h
48 8B 44 24 60 mov rax, [rsp+58h+arg_0]
48 05 48 01 00 00 add rax, 148h
48 89 44 24 40 mov [rsp+58h+var_18], rax
8B 44 24 68 mov eax, [rsp+58h+arg_8]
48 89 44 24 38 mov [rsp+58h+var_20], rax
48 8B 44 24 40 mov rax, [rsp+58h+var_18]
48 89 44 24 28 mov [rsp+58h+var_30], rax
48 8B 44 24 28 mov rax, [rsp+58h+var_30]
48 8B 4C 24 28 mov rcx, [rsp+58h+var_30]
48 8B 09 mov rcx, [rcx]
48 8B 40 08 mov rax, [rax+8]
48 2B C1 sub rax, rcx
48 99 cqo
B9 E0 01 00 00 mov ecx, 1E0h
48 F7 F9 idiv rcx
48 3B 44 24 38 cmp rax, [rsp+58h+var_20]
77 01 ja short loc_143434496
CC int 3 ; Trap to Debugger
loc_143434496: ; CODE XREF: sub_143434440+53↑j
48 69 44 24 38 E0 01 00 00 imul rax, [rsp+58h+var_20], 1E0h
48 8B 4C 24 28 mov rcx, [rsp+58h+var_30]
48 03 01 add rax, [rcx]
48 89 44 24 30 mov [rsp+58h+var_28], rax
48 8B 44 24 30 mov rax, [rsp+58h+var_28]
48 89 44 24 48 mov [rsp+58h+var_10], rax
48 8B 4C 24 60 mov rcx, [rsp+58h+arg_0]
E8 10 72 FF FF call sub_14342B6D0
48 8B 44 24 48 mov rax, [rsp+58h+var_10]
48 8B D0 mov rdx, rax
E8 D3 97 15 FE call sub_14158DCA0
C5 FA 11 44 24 20 vmovss [rsp+58h+var_38], xmm0
48 8B 44 24 30 mov rax, [rsp+58h+var_28]
48 83 C0 28 add rax, 28h ; '('
C5 FA 10 0D A4 70 AF 04 vmovss xmm1, cs:dword_147F2B588
48 8B C8 mov rcx, rax
E8 D4 75 64 FF call sub_142A7BAC0
0F B6 C0 movzx eax, al
85 C0 test eax, eax
75 10 jnz short loc_143434503
C5 FA 10 44 24 20 vmovss xmm0, [rsp+58h+var_38]
C5 F8 2F 05 87 70 AF 04 vcomiss xmm0, cs:dword_147F2B588
73 06 jnb short loc_143434509
loc_143434503: ; CODE XREF: sub_143434440+B1↑j
33 C0 xor eax, eax
EB 56 jmp short loc_14343455D
; ---------------------------------------------------------------------------
EB 54 jmp short loc_14343455D
; ---------------------------------------------------------------------------
loc_143434509: ; CODE XREF: sub_143434440+C1↑j
48 8B 44 24 30 mov rax, [rsp+58h+var_28]
48 83 C0 28 add rax, 28h ; '('
C5 FA 10 4C 24 20 vmovss xmm1, [rsp+58h+var_38]
48 8B C8 mov rcx, rax
E8 10 47 77 FF call sub_142BA8C30
C5 FA 11 44 24 24 vmovss [rsp+58h+var_34], xmm0
C5 FA 10 44 24 24 vmovss xmm0, [rsp+58h+var_34]
C5 F8 2F 05 3C 76 B6 04 vcomiss xmm0, cs:dword_147F9BB70
76 09 jbe short loc_14343453F
B8 03 00 00 00 mov eax, 3
EB 20 jmp short loc_14343455D
; ---------------------------------------------------------------------------
EB 1E jmp short loc_14343455D
; ---------------------------------------------------------------------------
loc_14343453F: ; CODE XREF: sub_143434440+F4↑j
C5 FA 10 44 24 24 vmovss xmm0, [rsp+58h+var_34]
C5 F8 2F 05 83 F7 AF 04 vcomiss xmm0, cs:dword_147F33CD0
76 09 jbe short loc_143434558
B8 02 00 00 00 mov eax, 2
EB 07 jmp short loc_14343455D
; ---------------------------------------------------------------------------
EB 05 jmp short loc_14343455D
; ---------------------------------------------------------------------------
loc_143434558: ; CODE XREF: sub_143434440+10D↑j
B8 01 00 00 00 mov eax, 1
loc_14343455D: ; CODE XREF: sub_143434440+C5↑j
; sub_143434440+C7↑j ...
48 83 C4 58 add rsp, 58h
C3 retn
sub_143434440 endp
; ---------------------------------------------------------------------------
; =================================================================================================
; Function Analysis: sub_143434440 (Likely: GetShieldFaceStatus)
; =================================================================================================
;
; Purpose:
; This function determines the operational status of a specific shield face belonging to a
; shield component object. It takes the shield component's base pointer and the index of the
; desired face as input. It calculates the memory address of the shield face structure,
; reads its current health and related metrics, performs comparisons against thresholds,
; and returns an integer status code (likely 0, 1, 2, or 3).
;
; Relation to Hook Library:
; This is the function targeted by the C++ shield modifier library. The library aims to
; intercept execution *before* this function reads the shield health, modify the health value
; in memory to a high value, and then allow this function to proceed. By reading the modified
; high health, this function will be tricked into returning a "healthy" status code (likely 3).
;
; The C++ library uses an instruction hook targeting the sequence starting around offset +97h
; (0x143434440 + 0x97 = 0x1434344D7, the `add rax, 28h` instruction) and validates the location
; by checking for the `vcomiss xmm0, cs:dword_147F9BB70` instruction shortly after.
;
; Parameters (Fastcall Convention):
; RCX (__int64 a1): Pointer to the base of the shield component structure.
; EDX (unsigned int a2): Zero-based index of the shield face to query within the component.
;
// Return Value (RAX):
; __int64 (effectively int): Status code:
; - 0: Shield face is down or disabled (health <= 0 or other condition).
; - 1: Shield face health is critical (positive but <= low threshold).
; - 2: Unknown/Unreachable? (Requires healthRatio > highThreshold but <= lowThreshold).
; - 3: Shield face is damaged but operational (health > low threshold).
;
; Noteworthy Aspects:
; - Performs bounds checking on the shield face index.
; - Calls helper functions (sub_14158DCA0, sub_142A7BAC0, sub_142BA8C30) to get metrics,
; check health against zero, and calculate health ratio.
; - Uses specific memory offsets (0x148, 0x150, 0x1E0, 0x28, 0x30) consistent with those
; used in the C++ abstraction layer.
; - Uses floating-point comparisons (vcomiss) against constants stored in memory.
; - Includes a debug break (`int 3`) for out-of-bounds index access.
; - Contains a seemingly redundant or unreachable path leading to return value 2.
;
; =================================================================================================
; Function Signature: __int64 __fastcall sub_143434440(__int64 pShieldComponent, unsigned int shieldFaceIndex)
sub_143434440 proc near ; CODE XREF: sub_14342B5D0+96↑p - Called within a loop iterating faces.
; CODE XREF: sub_1434E8C80+24↓p - Another potential caller.
; DATA XREF: ... (Indicates references from data sections, possibly vtables or function pointers)
; === Stack Frame Setup ===
; Define local variables and arguments relative to RSP before modification.
; These names are provided by the decompiler based on usage.
var_38 = dword ptr -38h ; Stack location for storing the result of sub_14158DCA0 (effective max health metric)
var_34 = dword ptr -34h ; Stack location for storing the calculated health ratio
var_30 = qword ptr -30h ; Stack location storing the address of the list start pointer (pShieldComponent + 0x148)
var_28 = qword ptr -28h ; Stack location storing the calculated pointer to the specific shield face (pCurrentShieldFace)
var_20 = qword ptr -20h ; Stack location storing the input shieldFaceIndex (zero-extended to 64-bit)
var_18 = qword ptr -18h ; Stack location storing the address of the list start pointer (pShieldComponent + 0x148) - used temporarily?
var_10 = qword ptr -10h ; Stack location storing pCurrentShieldFace - used temporarily?
arg_0 = qword ptr 8 ; Stack location where input RCX (pShieldComponent) is saved
arg_8 = dword ptr 10h ; Stack location where input EDX (shieldFaceIndex) is saved
; Save incoming arguments (RCX, EDX) to the stack shadow space / home space.
89 54 24 10 mov [rsp+arg_8], edx ; Save shieldFaceIndex (EDX) to [rsp+10h]
48 89 4C 24 08 mov [rsp+arg_0], rcx ; Save pShieldComponent (RCX) to [rsp+8h]
; Allocate stack space for local variables (0x58 bytes).
48 83 EC 58 sub rsp, 58h ; Adjust stack pointer down
; === Calculate Shield Face List Boundaries and Perform Bounds Check ===
; This block calculates the number of shield faces available in the component
; and checks if the requested index is valid.
48 8B 44 24 60 mov rax, [rsp+58h+arg_0] ; Load pShieldComponent from stack into RAX
48 05 48 01 00 00 add rax, 148h ; RAX = pShieldComponent + 0x148 (Address of the list start pointer)
; Corresponds to Offsets::ShieldComponent_ShieldFaceListPtr
48 89 44 24 40 mov [rsp+58h+var_18], rax ; Store address of list start pointer in var_18
8B 44 24 68 mov eax, [rsp+58h+arg_8] ; Load shieldFaceIndex from stack into EAX
48 89 44 24 38 mov [rsp+58h+var_20], rax ; Store shieldFaceIndex (zero-extended to RAX) in var_20
48 8B 44 24 40 mov rax, [rsp+58h+var_18] ; Reload address of list start pointer into RAX
48 89 44 24 28 mov [rsp+58h+var_30], rax ; Store address of list start pointer in var_30 (persistent storage for this frame)
; Calculate number of faces: (ListEndPtrValue - ListStartPtrValue) / StructSize
48 8B 44 24 28 mov rax, [rsp+58h+var_30] ; Load address of list start pointer into RAX
48 8B 4C 24 28 mov rcx, [rsp+58h+var_30] ; Load address of list start pointer into RCX
48 8B 09 mov rcx, [rcx] ; RCX = *ListStartPtrAddr (Actual pFaceListStart value)
48 8B 40 08 mov rax, [rax+8] ; RAX = *(ListStartPtrAddr + 8) = *ListEndPtrAddr (Actual pFaceListEnd value)
; Offset +8 corresponds to Offsets::ShieldComponent_ShieldFaceListEndPtr (0x150 = 0x148 + 8)
48 2B C1 sub rax, rcx ; RAX = pFaceListEnd - pFaceListStart (listSizeBytes)
48 99 cqo ; Sign-extend RAX into RDX:RAX for 64-bit division
B9 E0 01 00 00 mov ecx, 1E0h ; ECX = ShieldFace_StructSize (480 bytes)
; Corresponds to Offsets::ShieldFace_StructSize
48 F7 F9 idiv rcx ; RDX:RAX / RCX -> Quotient (numFaces) in RAX, Remainder in RDX
; RAX = listSizeBytes / ShieldFace_StructSize
; Bounds check: Is shieldFaceIndex < numFaces?
48 3B 44 24 38 cmp rax, [rsp+58h+var_20] ; Compare numFaces (RAX) with shieldFaceIndex (var_20)
77 01 ja short loc_143434496 ; Jump if numFaces > shieldFaceIndex (Index is valid)
; Index out of bounds: Trigger debug break.
CC int 3 ; Trap to Debugger if index >= numFaces
; === Calculate Address of Specific Shield Face ===
loc_143434496: ; Label jumped to if index is within bounds
; Calculate offset: shieldFaceIndex * ShieldFace_StructSize
48 69 44 24 38 E0 01 00 00 imul rax, [rsp+58h+var_20], 1E0h ; RAX = shieldFaceIndex * 480
; Calculate final address: pFaceListStart + offset
48 8B 4C 24 28 mov rcx, [rsp+58h+var_30] ; Load address of list start pointer into RCX
48 03 01 add rax, [rcx] ; RAX = (shieldFaceIndex * 480) + pFaceListStart
; RAX now holds pCurrentShieldFace
48 89 44 24 30 mov [rsp+58h+var_28], rax ; Store pCurrentShieldFace in var_28
; === Call Helper Functions (Side Effects / Metric Calculation) ===
; Store pCurrentShieldFace temporarily for RDX argument setup
48 8B 44 24 30 mov rax, [rsp+58h+var_28] ; Load pCurrentShieldFace into RAX
48 89 44 24 48 mov [rsp+58h+var_10], rax ; Store pCurrentShieldFace in var_10
; Call sub_14342B6D0 (CalculateShieldComponentProperty) - Result seems unused here.
48 8B 4C 24 60 mov rcx, [rsp+58h+arg_0] ; Load pShieldComponent into RCX (Arg1)
E8 10 72 FF FF call sub_14342B6D0 ; Call the function
; Call sub_14158DCA0 (GetEffectiveMaxHealthMetric)
48 8B 44 24 48 mov rax, [rsp+58h+var_10] ; Load pCurrentShieldFace from var_10 into RAX
48 8B D0 mov rdx, rax ; Move pCurrentShieldFace into RDX (Arg2)
; Arg1 (XMM0) is implicitly passed from previous state (unclear source here)
E8 D3 97 15 FE call sub_14158DCA0 ; Call the function, result (metric) returned in XMM0
C5 FA 11 44 24 20 vmovss [rsp+58h+var_38], xmm0 ; Store the returned metric (float) in var_38
; === First Health Check: Is Shield Down or Disabled? ===
; This block corresponds to the target of the C++ instruction hook.
; --- Start of Hooked Sequence ---
48 8B 44 24 30 mov rax, [rsp+58h+var_28] ; Load pCurrentShieldFace into RAX
48 83 C0 28 add rax, 28h ; '(' ; RAX = pCurrentShieldFace + 0x28 (pFaceCheckBase)
; Corresponds to Offsets::ShieldFace_CheckOffset
; <<< C++ Instruction Hook Target Address >>>
; --- End of Hooked Sequence Start ---
; Prepare arguments for sub_142A7BAC0 (IsShieldHealthZeroOrLess)
C5 FA 10 0D A4 70 AF 04 vmovss xmm1, cs:dword_147F2B588 ; Load float constant (likely 0.0f) into XMM1 (Arg2 - threshold)
; Address 0x147F2B588 is version specific.
48 8B C8 mov rcx, rax ; Move pFaceCheckBase (RAX) into RCX (Arg1)
; Call the health check function
E8 D4 75 64 FF call sub_142A7BAC0 ; Checks if health at [RCX+8] (pCurrentShieldFace + 0x30) <= XMM1 (0.0f)
; Returns boolean result in AL.
; Check the result from sub_142A7BAC0
0F B6 C0 movzx eax, al ; Zero-extend the boolean result (AL) into EAX
85 C0 test eax, eax ; Check if EAX is zero (ZF=1 if zero)
75 10 jnz short loc_143434503 ; Jump if EAX is Non-Zero (meaning health <= 0 was TRUE)
; --- Health > 0 Path ---
; If health > 0, check if it's *also* greater than the 0.0f threshold loaded earlier.
; This seems redundant given the previous check, possibly a compiler artifact or defensive check.
C5 FA 10 44 24 20 vmovss xmm0, [rsp+58h+var_38] ; Load the stored metric (denominator) into XMM0 - ??? Why metric here?
; *Correction*: This loads the metric, but the comparison below uses a different constant.
; This load seems misplaced or its result unused immediately.
C5 F8 2F 05 87 70 AF 04 vcomiss xmm0, cs:dword_147F2B588 ; Compare XMM0 (metric?) with 0.0f constant.
; This comparison's purpose is unclear in this path.
73 06 jnb short loc_143434509 ; Jump if XMM0 >= 0.0f (Not Below) - Assuming metric is usually positive.
; --- Handle Shield Down Case (Result from sub_142A7BAC0 was TRUE) ---
loc_143434503: ; Label jumped to if health <= 0
33 C0 xor eax, eax ; Set EAX = 0 (Return status code 0: Down/Disabled)
EB 56 jmp short loc_14343455D ; Jump to function epilogue
; --- Unreachable Code? ---
; These two jumps seem unreachable based on the preceding logic flow.
EB 54 jmp short loc_14343455D ; Dead code?
EB 1E jmp short loc_14343455D ; Dead code?
; === Second Health Check: Calculate Ratio and Compare to Low Threshold ===
loc_143434509: ; Label jumped to if health > 0 (and metric >= 0)
; Prepare arguments for sub_142BA8C30 (CalculateHealthRatio)
48 8B 44 24 30 mov rax, [rsp+58h+var_28] ; Load pCurrentShieldFace into RAX
48 83 C0 28 add rax, 28h ; '(' ; RAX = pFaceCheckBase (pCurrentShieldFace + 0x28)
C5 FA 10 4C 24 20 vmovss xmm1, [rsp+58h+var_38] ; Load the stored metric (denominator) into XMM1 (Arg2)
48 8B C8 mov rcx, rax ; Move pFaceCheckBase into RCX (Arg1)
; Call the ratio calculation function
E8 10 47 77 FF call sub_142BA8C30 ; Calculates ratio = health[RCX+8] / metric[XMM1]. Result in XMM0.
; Store and reload the calculated health ratio
C5 FA 11 44 24 24 vmovss [rsp+58h+var_34], xmm0 ; Store health ratio in var_34
C5 FA 10 44 24 24 vmovss xmm0, [rsp+58h+var_34] ; Load health ratio back into XMM0
; Compare health ratio to the low threshold constant
C5 F8 2F 05 3C 76 B6 04 vcomiss xmm0, cs:dword_147F9BB70 ; Compare ratio (XMM0) with low threshold constant
; Address 0x147F9BB70 is version specific.
; <<< Contextual Validation Instruction Target >>>
76 09 jbe short loc_14343453F ; Jump if ratio <= lowThreshold (Below or Equal)
; --- Handle Damaged Case (Ratio > Low Threshold) ---
B8 03 00 00 00 mov eax, 3 ; Set EAX = 3 (Return status code 3: Damaged)
EB 20 jmp short loc_14343455D ; Jump to function epilogue
; --- Unreachable Code? ---
EB 1E jmp short loc_14343455D ; Dead code?
; === Third Health Check: Compare Ratio to High Threshold (Potentially Redundant Path) ===
loc_14343453F: ; Label jumped to if ratio <= lowThreshold
; Compare health ratio to the high threshold constant
C5 FA 10 44 24 24 vmovss xmm0, [rsp+58h+var_34] ; Load health ratio into XMM0
C5 F8 2F 05 83 F7 AF 04 vcomiss xmm0, cs:dword_147F33CD0 ; Compare ratio (XMM0) with high threshold constant
; Address 0x147F33CD0 is version specific.
; This comparison seems logically strange here, as we already know ratio <= lowThreshold.
76 09 jbe short loc_143434558 ; Jump if ratio <= highThreshold (Below or Equal)
; --- Handle Unknown/Unreachable Case (Ratio > High Threshold but <= Low Threshold?) ---
B8 02 00 00 00 mov eax, 2 ; Set EAX = 2 (Return status code 2: Unknown/Unreachable?)
EB 07 jmp short loc_14343455D ; Jump to function epilogue
; --- Unreachable Code? ---
EB 05 jmp short loc_14343455D ; Dead code?
; === Handle Critical Case (Ratio <= Low Threshold and <= High Threshold) ===
loc_143434558: ; Label jumped to if ratio <= highThreshold (and implicitly <= lowThreshold)
B8 01 00 00 00 mov eax, 1 ; Set EAX = 1 (Return status code 1: Critical)
; Falls through to epilogue
; === Function Epilogue ===
loc_14343455D: ; Common exit point for all return paths
; Deallocate stack space reserved in the prologue.
48 83 C4 58 add rsp, 58h ; Restore stack pointer
; Return control to the caller. The return value is in EAX.
C3 retn
sub_143434440 endp ; End of procedure definition
; =================================================================================================
; End of Analysis: sub_143434440
; =================================================================================================
// =================================================================================================
// Based on analysis of Sycorax's original concept
// =================================================================================================
//
// == PURPOSE ==
//
// This Dynamic Link Library (DLL) is designed to be injected into the Star Citizen game process
// (`StarCitizen.exe`). Its primary objective is to modify the perceived status of ship shields.
// It achieves this by intercepting (hooking) a specific sequence of instructions within a game
// function responsible for querying the status of individual shield faces (identified as
// `sub_143434440` during analysis, likely conceptually `GetShieldFaceStatus`).
//
// The core mechanism involves:
// 1. Identifying the target instruction sequence using a byte pattern (signature).
// 2. Verifying the identified location using contextual analysis of nearby instructions to
// ensure the correct function instance is targeted, mitigating issues with non-unique
// signatures.
// 3. Placing a detour (hook) at the validated instruction sequence using the MinHook library.
// 4. When the game executes the hooked instructions, the detour function (`HookedInstructionSequence`)
// is called instead.
// 5. The detour function calculates the memory address of the specific shield face being queried
// for the current call.
// 6. It then forcibly overwrites the 'current health' value in the shield face's memory structure
// with a high value (either fixed or based on a multiplier of its maximum health).
// 7. Finally, it executes the original game instructions (via a MinHook trampoline) that were
// overwritten by the hook.
//
// The result is that when the original game code proceeds to read the shield's health to
// determine its status (e.g., Down, Critical, Damaged), it reads the artificially high value
// set by the hook. This effectively masks the true health state, preventing the game systems
// (like UI or AI) that rely on this status check from recognizing low-health or disabled shields.
//
// == ABSTRACTION ==
//
// To improve code readability and encapsulate the low-level memory manipulation, this implementation
// utilizes a C++ abstraction layer consisting of `ShieldComponent` and `ShieldFace` classes. These
// classes wrap the raw game memory pointers and provide methods (e.g., `GetCurrentHealth`,
// `SetCurrentHealth`) that internally handle offset calculations and memory access. Crucially,
// these methods also incorporate Structured Exception Handling (`__try`/`__except`) to gracefully
// manage potential memory access violations, enhancing runtime stability if offsets become invalid.
//
// == TARGET GAME VERSION (Analysis Based On) ==
//
// Star Citizen v4.1.0-live.9659838
// All offsets, signatures, and contextual heuristics are derived from the analysis of this specific
// game version. They are extremely likely to be invalid in other versions.
//
// == WARNING - CRITICAL LIMITATIONS & RISKS ==
//
// 1. **EXTREME FRAGILITY TO GAME UPDATES:** This DLL relies fundamentally on hardcoded memory
// offsets (within the abstraction layer), a specific instruction sequence signature, and
// contextual code patterns (relative instruction positions, constant values). ANY game update,
// patch, hotfix, or even minor recompilation by the developers has a VERY HIGH probability
// of invalidating these assumptions, causing the DLL to fail, malfunction, or crash the game.
// This approach CANNOT provide true resilience against game updates without constant
// reverse engineering and maintenance.
//
// 2. **NOT TRUE INVULNERABILITY:** This modification only manipulates the *status reporting*
// mechanism intercepted by the hook. It does *not* alter the core damage calculation logic
// of the game. Sufficiently high burst damage may still deplete the actual shield health
// instantly, potentially causing shield collapse or other negative effects before the status
// check occurs. Furthermore, physical collisions bypass this mechanism entirely and can
// still destroy the ship.
//
// 3. **AFFECTS ALL ENTITIES:** The current implementation lacks specific logic to identify whether
// the shield component being processed belongs to the local player's ship. Therefore, it will
// apply the health modification logic to *any* shield face processed by the hooked function
// in the game world (e.g., other player ships, AI ships, potentially station shields if they
// use the same code path). Implementing player-only targeting requires significant additional
// reverse engineering to identify player entity structures and ownership relationships.
//
// 4. **ANTI-CHEAT DETECTION RISK:** Injecting DLLs, hooking game functions, and modifying game
// memory are activities commonly targeted by anti-cheat systems (like Easy Anti-Cheat used
// by Star Citizen). Using this DLL, especially in online modes (Persistent Universe), carries
// a significant risk of account suspension or banning. This implementation is intended for
// educational purposes and offline testing (e.g., Arena Commander offline modes). USE ONLINE
// AT YOUR OWN EXTREME RISK.
//
// 5. **CONTEXTUAL HEURISTIC RELIABILITY:** The contextual validation relies on assumptions about
// nearby code structure and constant values (e.g., the low threshold float value). While
// designed to improve uniqueness, these heuristics can also fail or become ambiguous after
// game updates, potentially leading to incorrect hooking or failure to find the target.
//
// =================================================================================================
// == Standard & Windows Headers ==
#include <windows.h> // Core Windows API functions (Handles, Threads, Memory, etc.)
#include <psapi.h> // Process Status API (for GetModuleInformation)
#include <iostream> // Standard I/O streams (for console logging)
#include <vector> // Standard dynamic array (for pattern bytes and matches)
#include <string> // Standard string manipulation
#include <sstream> // Standard string stream (for formatting)
#include <iomanip> // Standard I/O manipulators (hex, uppercase for logging)
#include <excpt.h> // Structured Exception Handling (__try, __except)
#include <optional> // Standard optional type (for safe return values from abstraction)
#include <cmath> // Standard math functions (isnan, isinf for float checks)
// == Third-Party Headers ==
#include "MinHook.h" // MinHook hooking library (ensure header is accessible)
// == Linker Directives ==
#pragma comment(lib, "Psapi.lib") // Link against Psapi.lib for GetModuleInformation
#pragma comment(lib, "MinHook.x64.lib") // Link against the 64-bit MinHook library
// (Ensure path is correct for your build setup, e.g., "./MinHook.x64.lib")
// =================================================================================================
// Global Constants and Configuration
// =================================================================================================
// --- Target Process ---
const char* TARGET_PROCESS_NAME = "StarCitizen.exe"; // The executable name of the target game process
// --- Runtime Configuration ---
// Structure to hold settings that can potentially be modified (e.g., via config file - not implemented here)
struct RuntimeConfig {
// Shield Health Modification Mode:
bool useFixedValue = false; // If true, set health to 'fixedShieldValue'. If false, use 'maxHealth * shieldHealthMultiplier'.
float fixedShieldValue = 10000.0f; // The fixed health value to apply if useFixedValue is true.
int shieldHealthMultiplier = 100; // The multiplier applied to max health if useFixedValue is false. (Must be > 0).
// Master Control & Safety:
bool enableModification = true; // Global switch to enable/disable the core shield modification logic. Can be set to false on error.
bool enableDebugLogging = true; // Enable verbose debug messages to the console.
// User Interaction:
int unloadKey = VK_END; // Virtual-Key code for the key that triggers DLL unload. (VK_END is the 'End' key).
};
// Global instance of the runtime configuration.
RuntimeConfig g_Config;
// --- Reverse Engineered Offsets (Internal Implementation Detail) ---
// These offsets represent the memory layout of game structures as determined by reverse engineering
// the target game version. They are encapsulated within the abstraction layer but remain critical
// and highly version-dependent.
namespace Offsets {
// Offset within the Shield Component structure (base pointer `pShieldComponent_raw`)
// to the pointer (*uintptr_t) pointing to the start of the shield face array/list.
constexpr size_t ShieldComponent_ShieldFaceListPtr = 0x148; // Decimal: 328
// Offset within the Shield Component structure to the pointer (*uintptr_t) pointing
// to the memory location *after* the end of the shield face array/list. Used for size calculation.
constexpr size_t ShieldComponent_ShieldFaceListEndPtr = 0x150; // Decimal: 336
// The size in bytes of a single Shield Face structure within the array/list.
// Used for calculating the address of a specific face via indexing.
constexpr size_t ShieldFace_StructSize = 0x1E0; // Decimal: 480
// Offset within a single Shield Face structure (relative to its base pointer)
// to the float value representing its current health.
constexpr size_t ShieldFace_CurrentHealth = 0x30; // Decimal: 48
// Offset within a single Shield Face structure (relative to its base pointer)
// to the float value representing its maximum health.
constexpr size_t ShieldFace_MaxHealth = 0x48; // Decimal: 72
// Offset within a single Shield Face structure used as a base for certain checks
// in the original game code (e.g., passed to sub_142A7BAC0 and sub_142BA8C30).
// Current health is at +0x08 relative to this offset.
constexpr size_t ShieldFace_CheckOffset = 0x28; // Decimal: 40
} // namespace Offsets
// --- Signatures for Pattern Scanning (Internal Implementation Detail) ---
// Primary Signature: Targets the desired instruction sequence within sub_143434440.
// Sequence: mov rax,[rsp+??] / add rax, 28h / vmovss xmm1, [rip+Const0] / mov rcx, rax / call CheckFunc
// This signature might match multiple locations in the game code.
const std::string TARGET_INSTRUCTION_SIGNATURE =
"48 8B 44 24 ?? " // mov rax, [rsp+??] (Load pShieldFace base pointer from stack, offset wildcarded)
"48 83 C0 28 " // add rax, 28h (Calculate base + offset for health checks)
"C5 FA 10 0D ?? ?? ?? ?? " // vmovss xmm1, [rip+??] (Load float constant (likely 0.0f) for comparison, address wildcarded)
"48 8B C8 " // mov rcx, rax (Move calculated pointer (base+0x28) into rcx for call)
"E8 ?? ?? ?? ??"; // call sub_142A7BAC0 (Call the first health check function, offset wildcarded)
// Context Signature: Used to validate matches of the primary signature.
// Targets the 'vcomiss xmm0, dword ptr [rip+offset]' instruction which compares the health ratio
// against the low health threshold constant shortly after the primary sequence.
const std::string CONTEXT_VCOMISS_LOW_THRESHOLD_SIG = "C5 F8 2F 05"; // Opcode bytes for vcomiss xmm0, mem32 [rip+rel32]
// --- Context Validation Heuristics ---
// Defines the maximum number of bytes to search forward from a primary signature match
// to find the contextual validation signature (vcomiss).
constexpr size_t CONTEXT_SEARCH_RANGE = 100;
// Heuristic bounds for the value of the low health threshold constant referenced by the
// contextual vcomiss instruction. Used to increase confidence that we've found the correct location.
// Assumes the threshold is a small positive value, greater than zero but significantly less than 1.0.
constexpr float LOW_THRESHOLD_MIN_VALUE = 0.0001f; // Must be greater than zero
constexpr float LOW_THRESHOLD_MAX_VALUE = 0.5f; // Must be reasonably small (e.g., less than 50%)
// =================================================================================================
// Typedefs and Global Variables
// =================================================================================================
// --- Function Pointer Typedefs ---
// Typedef for the original function signature (`sub_143434440`).
// This defines the expected parameters and return type for both the original function
// and the trampoline function provided by MinHook.
// Parameters:
// pShieldComponent: __int64 - Pointer to the shield component object.
// shieldFaceIndex: unsigned int - Index of the specific shield face.
// Returns:
// __int64 - Status code (likely 0, 1, 2, or 3 based on analysis).
using FnGetShieldFaceStatus = __int64(__fastcall*)(
__int64 pShieldComponent,
unsigned int shieldFaceIndex
);
// --- Global Variables ---
// Pointer to the MinHook trampoline.
// When the hook is installed, this variable will store the address of a small function
// generated by MinHook. Calling this function executes the original game instructions
// that were overwritten by the hook's jump instruction, and then jumps back to the
// original function's code flow immediately after the hooked sequence.
// It must be called with the original function's arguments.
FnGetShieldFaceStatus g_OriginalTrampoline = nullptr;
// Handle to this DLL module. Stored in DllMain and used by the main thread
// for unloading the library via FreeLibraryAndExitThread.
HMODULE g_hModule = nullptr;
// =================================================================================================
// Logging Utilities
// =================================================================================================
// Enum to categorize log message severity.
enum LogLevel {
LOG_DEBUG, // Verbose messages for development and tracing.
LOG_INFO, // General informational messages about progress.
LOG_SUCCESS, // Indicates successful operations (e.g., hook installed).
LOG_WARNING, // Indicates potential issues or non-critical errors.
LOG_ERROR // Indicates critical errors that may prevent functionality.
};
/**
* @brief Logs a message to the console with a level prefix.
* Filters debug messages based on global configuration. Errors/Warnings go to cerr.
* @param level The severity level of the message.
* @param message The message string to log.
*/
void Log(LogLevel level, const std::string& message) {
// Skip debug messages if disabled in config
if (level == LOG_DEBUG && !g_Config.enableDebugLogging) {
return;
}
// Determine the prefix string based on the log level.
const char* prefix = "[INFO] ";
switch (level) {
case LOG_DEBUG: prefix = "[DEBUG] "; break;
case LOG_INFO: prefix = "[INFO] "; break;
case LOG_SUCCESS: prefix = "[SUCCESS] "; break;
case LOG_WARNING: prefix = "[WARNING] "; break;
case LOG_ERROR: prefix = "[ERROR] "; break;
}
// Use standard error stream for warnings and errors, standard output otherwise.
// Ensure streams are flushed immediately for real-time visibility in console.
if (level == LOG_ERROR || level == LOG_WARNING) {
std::cerr << prefix << message << std::endl << std::flush;
} else {
std::cout << prefix << message << std::endl << std::flush;
}
}
/**
* @brief Formats a pointer value as a hexadecimal string (e.g., "0xABCDEF01").
* @param val The pointer or address value to format.
* @return A string representing the value in uppercase hexadecimal format with "0x" prefix.
*/
std::string PtrToHexStr(uintptr_t val) {
std::stringstream ss;
ss << "0x" << std::hex << std::uppercase << val;
return ss.str();
}
// =================================================================================================
// Memory Safety (Structured Exception Handling)
// =================================================================================================
/**
* @brief SEH filter function specifically for handling memory access violations.
* This function is used within __try/__except blocks when accessing potentially invalid
* game memory (e.g., through pointers derived from reverse-engineered offsets).
* @param exceptionCode The system code of the exception that occurred.
* @param exceptionInfo Pointer to an EXCEPTION_POINTERS structure containing details about the exception.
* @return EXCEPTION_EXECUTE_HANDLER if it's an Access Violation (to execute the __except block).
* @return EXCEPTION_CONTINUE_SEARCH for any other type of exception (let other handlers process it).
*/
int HandleMemoryAccessException(unsigned int exceptionCode, PEXCEPTION_POINTERS exceptionInfo) {
// Check if the exception code corresponds to an Access Violation.
if (exceptionCode == EXCEPTION_ACCESS_VIOLATION) {
// Log details about the access violation, including the address that caused the fault.
// ExceptionInformation[0] indicates read (0) or write (1) violation.
// ExceptionInformation[1] contains the virtual address that caused the fault.
uintptr_t accessAddress = (exceptionInfo && exceptionInfo->ExceptionRecord)
? exceptionInfo->ExceptionRecord->ExceptionInformation[1]
: 0; // Default if info is unavailable
Log(LOG_ERROR, "[SEH] Access Violation accessing memory at " + PtrToHexStr(accessAddress) +
". Game structure/offsets likely changed.");
// Instruct the system to execute the corresponding __except block.
return EXCEPTION_EXECUTE_HANDLER;
}
// If it's not an Access Violation, let the system continue searching for other handlers.
Log(LOG_WARNING, "[SEH] Caught non-AV exception: " + std::to_string(exceptionCode));
return EXCEPTION_CONTINUE_SEARCH;
}
// =================================================================================================
// Abstraction Layer: Shield System Interface
// =================================================================================================
// This layer provides a more object-oriented and safer way to interact with the game's
// shield data structures, hiding the raw pointer arithmetic and offset details.
/**
* @class ShieldFace
* @brief Represents a single shield face in the game's memory.
* Provides methods to safely access and modify properties like current and maximum health.
* Encapsulates pointer arithmetic, offset usage, and memory access safety checks (SEH).
*/
class ShieldFace {
private:
// The raw memory address pointing to the beginning of this shield face's data structure.
// A value of 0 indicates an invalid or uninitialized state.
uintptr_t m_pFaceData;
public:
/**
* @brief Constructs a ShieldFace object associated with a specific memory address.
* @param pFaceData The memory address of the shield face structure.
*/
explicit ShieldFace(uintptr_t pFaceData) : m_pFaceData(pFaceData) {
// Log(LOG_DEBUG, "[ShieldFace] Created instance for address " + PtrToHexStr(m_pFaceData));
}
/**
* @brief Default constructor for creating an invalid ShieldFace object.
*/
ShieldFace() : m_pFaceData(0) {
// Log(LOG_DEBUG, "[ShieldFace] Created invalid instance (null pointer).");
}
/**
* @brief Checks if the ShieldFace object currently points to a non-null memory address.
* Does not guarantee the memory is actually valid or accessible.
* @return True if the internal pointer is not null, false otherwise.
*/
bool IsValid() const {
return m_pFaceData != 0;
}
/**
* @brief Safely retrieves the current health value of the shield face.
* Uses SEH to protect against memory access violations if offsets or the base pointer are invalid.
* @return An std::optional<float> containing the health value if successful, or std::nullopt on error.
*/
std::optional<float> GetCurrentHealth() const {
if (!IsValid()) {
Log(LOG_DEBUG, "[ShieldFace::GetCurrentHealth] Called on invalid instance.");
return std::nullopt;
}
std::optional<float> result = std::nullopt;
__try {
// Calculate address: Base + Offset
float* healthPtr = reinterpret_cast<float*>(m_pFaceData + Offsets::ShieldFace_CurrentHealth);
// Dereference pointer to read value
result = *healthPtr;
// Log(LOG_DEBUG, "[ShieldFace::GetCurrentHealth] Read value " + std::to_string(result.value_or(NAN)) + " from " + PtrToHexStr(reinterpret_cast<uintptr_t>(healthPtr)));
} __except (HandleMemoryAccessException(GetExceptionCode(), GetExceptionInformation())) {
// Exception occurred during memory access. Logged by the filter.
// Result remains std::nullopt.
Log(LOG_ERROR, "[ShieldFace::GetCurrentHealth] Failed to read health for face " + PtrToHexStr(m_pFaceData));
}
return result;
}
/**
* @brief Safely retrieves the maximum health value of the shield face.
* Uses SEH to protect against memory access violations.
* @return An std::optional<float> containing the max health value if successful, or std::nullopt on error.
*/
std::optional<float> GetMaxHealth() const {
if (!IsValid()) {
Log(LOG_DEBUG, "[ShieldFace::GetMaxHealth] Called on invalid instance.");
return std::nullopt;
}
std::optional<float> result = std::nullopt;
__try {
// Calculate address: Base + Offset
float* maxHealthPtr = reinterpret_cast<float*>(m_pFaceData + Offsets::ShieldFace_MaxHealth);
// Dereference pointer to read value
result = *maxHealthPtr;
// Log(LOG_DEBUG, "[ShieldFace::GetMaxHealth] Read value " + std::to_string(result.value_or(NAN)) + " from " + PtrToHexStr(reinterpret_cast<uintptr_t>(maxHealthPtr)));
} __except (HandleMemoryAccessException(GetExceptionCode(), GetExceptionInformation())) {
// Exception occurred. Logged by the filter. Result remains std::nullopt.
Log(LOG_ERROR, "[ShieldFace::GetMaxHealth] Failed to read max health for face " + PtrToHexStr(m_pFaceData));
}
return result;
}
/**
* @brief Safely sets the current health value of the shield face.
* Uses SEH to protect against memory access violations during the write operation.
* @param newHealth The new health value to write.
* @return True if the write operation was successful (or seemed successful within the __try block),
* false if the instance was invalid or a memory access exception occurred.
*/
bool SetCurrentHealth(float newHealth) {
if (!IsValid()) {
Log(LOG_DEBUG, "[ShieldFace::SetCurrentHealth] Called on invalid instance.");
return false;
}
bool success = false;
__try {
// Calculate address: Base + Offset
float* healthPtr = reinterpret_cast<float*>(m_pFaceData + Offsets::ShieldFace_CurrentHealth);
// Dereference pointer to write value
*healthPtr = newHealth;
success = true; // Assume success if no exception occurs
// Log(LOG_DEBUG, "[ShieldFace::SetCurrentHealth] Wrote value " + std::to_string(newHealth) + " to " + PtrToHexStr(reinterpret_cast<uintptr_t>(healthPtr)));
} __except (HandleMemoryAccessException(GetExceptionCode(), GetExceptionInformation())) {
// Exception occurred during memory write. Logged by the filter.
// Success remains false.
Log(LOG_ERROR, "[ShieldFace::SetCurrentHealth] Failed to write health for face " + PtrToHexStr(m_pFaceData));
}
return success;
}
};
/**
* @class ShieldComponent
* @brief Represents a shield component in the game's memory, containing multiple shield faces.
* Provides methods to safely access the list of faces and retrieve individual ShieldFace objects.
* Encapsulates pointer arithmetic, offset usage, and memory access safety checks (SEH).
*/
class ShieldComponent {
private:
// The raw memory address pointing to the beginning of this shield component's data structure.
// A value of 0 indicates an invalid or uninitialized state.
uintptr_t m_pComponentData;
public:
/**
* @brief Constructs a ShieldComponent object associated with a specific memory address.
* @param pComponentData The memory address of the shield component structure.
*/
explicit ShieldComponent(uintptr_t pComponentData) : m_pComponentData(pComponentData) {
// Log(LOG_DEBUG, "[ShieldComponent] Created instance for address " + PtrToHexStr(m_pComponentData));
}
/**
* @brief Checks if the ShieldComponent object currently points to a non-null memory address.
* Does not guarantee the memory is actually valid or accessible.
* @return True if the internal pointer is not null, false otherwise.
*/
bool IsValid() const {
return m_pComponentData != 0;
}
/**
* @brief Safely retrieves the number of shield faces associated with this component.
* Calculates the size of the face list using start/end pointers and divides by the face structure size.
* Uses SEH to protect against memory access violations when reading list pointers.
* @return An std::optional<size_t> containing the number of faces if successful, or std::nullopt on error.
*/
std::optional<size_t> GetNumberOfFaces() const {
if (!IsValid()) {
Log(LOG_DEBUG, "[ShieldComponent::GetNumberOfFaces] Called on invalid instance.");
return std::nullopt;
}
std::optional<size_t> result = std::nullopt;
__try {
// Calculate addresses of the pointers to the list start and end
uintptr_t* pFaceListStartPtrAddr = reinterpret_cast<uintptr_t*>(m_pComponentData + Offsets::ShieldComponent_ShieldFaceListPtr);
uintptr_t* pFaceListEndPtrAddr = reinterpret_cast<uintptr_t*>(m_pComponentData + Offsets::ShieldComponent_ShieldFaceListEndPtr);
// Dereference pointers to get the actual start and end addresses of the list. (Potential AV point)
uintptr_t pFaceListStart = *pFaceListStartPtrAddr;
uintptr_t pFaceListEnd = *pFaceListEndPtrAddr;
// Validate the retrieved list pointers and structure size
if (pFaceListStart && pFaceListEnd && pFaceListEnd >= pFaceListStart && Offsets::ShieldFace_StructSize > 0) {
size_t listSizeBytes = pFaceListEnd - pFaceListStart;
result = listSizeBytes / Offsets::ShieldFace_StructSize; // Calculate number of faces
// Log(LOG_DEBUG, "[ShieldComponent::GetNumberOfFaces] List: " + PtrToHexStr(pFaceListStart) + " - " + PtrToHexStr(pFaceListEnd) + ", Size: " + std::to_string(listSizeBytes) + ", NumFaces: " + std::to_string(result.value_or(0)));
} else {
// Log if pointers seem invalid or calculation would be nonsensical
Log(LOG_WARNING, "[ShieldComponent::GetNumberOfFaces] Invalid face list pointers or size for component " + PtrToHexStr(m_pComponentData) +
" (Start: " + PtrToHexStr(pFaceListStart) + ", End: " + PtrToHexStr(pFaceListEnd) + ")");
}
} __except (HandleMemoryAccessException(GetExceptionCode(), GetExceptionInformation())) {
// Exception occurred reading list pointers. Logged by the filter.
// Result remains std::nullopt.
Log(LOG_ERROR, "[ShieldComponent::GetNumberOfFaces] Failed to read face list pointers for component " + PtrToHexStr(m_pComponentData));
}
return result;
}
/**
* @brief Safely retrieves a specific ShieldFace object by its index within the component's list.
* Performs bounds checking using GetNumberOfFaces().
* @param index The zero-based index of the desired shield face.
* @return A ShieldFace object. If the index is out of bounds, the component is invalid, or an error
* occurs while retrieving list info, the returned ShieldFace object will be invalid (check with IsValid()).
*/
ShieldFace GetShieldFace(unsigned int index) const {
if (!IsValid()) {
Log(LOG_DEBUG, "[ShieldComponent::GetShieldFace] Called on invalid instance.");
return ShieldFace(); // Return invalid face
}
// Get the number of faces, handling potential errors
std::optional<size_t> numFacesOpt = GetNumberOfFaces();
// Check if number of faces could be determined and if index is within bounds
if (!numFacesOpt.has_value()) {
Log(LOG_WARNING, "[ShieldComponent::GetShieldFace] Could not determine number of faces for component " + PtrToHexStr(m_pComponentData));
return ShieldFace(); // Return invalid face
}
if (index >= numFacesOpt.value()) {
Log(LOG_WARNING, "[ShieldComponent::GetShieldFace] Index " + std::to_string(index) +
" is out of bounds (NumFaces: " + std::to_string(numFacesOpt.value()) + ").");
return ShieldFace(); // Return invalid face
}
// If bounds check passed, calculate the address of the specific face.
// SEH protection for reading the list start pointer is implicitly handled by GetNumberOfFaces().
// We still need SEH here in case the component pointer itself was initially invalid but somehow passed IsValid().
uintptr_t pFaceData = 0;
__try {
uintptr_t pFaceListStart = *reinterpret_cast<uintptr_t*>(m_pComponentData + Offsets::ShieldComponent_ShieldFaceListPtr);
if (!pFaceListStart) { // Should have been caught earlier, but defensive check
Log(LOG_ERROR, "[ShieldComponent::GetShieldFace] Face list start pointer is null despite passing earlier checks.");
return ShieldFace();
}
pFaceData = pFaceListStart + (static_cast<uintptr_t>(index) * Offsets::ShieldFace_StructSize);
// Log(LOG_DEBUG, "[ShieldComponent::GetShieldFace] Calculated face " + std::to_string(index) + " address: " + PtrToHexStr(pFaceData));
} __except (HandleMemoryAccessException(GetExceptionCode(), GetExceptionInformation())) {
Log(LOG_ERROR, "[ShieldComponent::GetShieldFace] Exception calculating face address for index " + std::to_string(index));
return ShieldFace(); // Return invalid face on error
}
// Return a ShieldFace object wrapping the calculated address.
return ShieldFace(pFaceData);
}
};
// =================================================================================================
// Hook Function (Detour Logic)
// =================================================================================================
/**
* @brief The detour function that replaces the identified instruction sequence.
*
* This function is executed when the game attempts to run the instructions at the hooked address.
* It uses the abstraction layer (`ShieldComponent`, `ShieldFace`) to safely interact with game memory,
* modifies the shield health, and then calls the trampoline to resume original game execution.
*
* @param pShieldComponent_raw The first argument (__int64, passed via RCX) of the original function (`sub_143434440`).
* @param shieldFaceIndex The second argument (unsigned int, passed via EDX) of the original function.
* @return __int64 The final return value produced by the original `sub_143434440` function after its execution resumes via the trampoline.
*/
__int64 __fastcall HookedInstructionSequence(__int64 pShieldComponent_raw, unsigned int shieldFaceIndex) {
// --- Pre-Modification Check ---
// If the modification logic has been disabled (e.g., due to a previous error),
// immediately execute the original game code via the trampoline without attempting modification.
if (!g_Config.enableModification) {
// Ensure the trampoline pointer is valid before calling
if (!g_OriginalTrampoline) {
Log(LOG_ERROR, "HookedInstructionSequence: Modification disabled, but trampoline is NULL!");
// Cannot proceed safely. What to return? Returning 0 might be safest guess.
return 0;
}
return g_OriginalTrampoline(pShieldComponent_raw, shieldFaceIndex);
}
// --- Shield Modification Logic ---
// Wrap the core logic in a try block, although the abstraction layer also has internal SEH.
// This provides an additional layer of safety for the logic flow itself.
try
{
// 1. Create Abstraction Objects:
// Instantiate ShieldComponent using the raw pointer from the game.
ShieldComponent component(pShieldComponent_raw);
// Basic check on the component pointer provided by the game.
if (!component.IsValid()) {
Log(LOG_DEBUG, "HookedInstructionSequence called with invalid component pointer: " + PtrToHexStr(pShieldComponent_raw));
// Cannot proceed. Execute original code via trampoline.
if (!g_OriginalTrampoline) return 0; // Safety check
return g_OriginalTrampoline(pShieldComponent_raw, shieldFaceIndex);
}
// Retrieve the specific ShieldFace object using the index.
// The GetShieldFace method handles internal pointer validation and bounds checking.
ShieldFace face = component.GetShieldFace(shieldFaceIndex);
// 2. Validate Retrieved ShieldFace:
// Check if the ShieldFace object is valid. It might be invalid if the index was
// out of bounds or if memory errors occurred within GetShieldFace/GetNumberOfFaces.
if (face.IsValid()) {
// 3. Read Health Values Safely:
// Use the abstraction layer methods, which return std::optional and handle SEH.
std::optional<float> currentHealthOpt = face.GetCurrentHealth();
std::optional<float> maxHealthOpt = face.GetMaxHealth();
// 4. Check if Health Reads Succeeded:
if (currentHealthOpt.has_value() && maxHealthOpt.has_value()) {
float currentHealth = currentHealthOpt.value();
float maxHealth = maxHealthOpt.value();
// Sanitize maxHealth to prevent division by zero or negative results later.
if (maxHealth <= 0.0f) {
Log(LOG_DEBUG, "[Hook] Max health for face " + std::to_string(shieldFaceIndex) + " is non-positive ("+ std::to_string(maxHealth) +"). Using 1.0f default.");
maxHealth = 1.0f;
}
// 5. Determine Target Health:
// Calculate the desired health value based on the runtime configuration.
float targetHealth = 0.0f;
if (g_Config.useFixedValue) {
targetHealth = g_Config.fixedShieldValue;
} else {
// Ensure multiplier is positive before applying.
int multiplier = (g_Config.shieldHealthMultiplier > 0) ? g_Config.shieldHealthMultiplier : 1;
targetHealth = maxHealth * static_cast<float>(multiplier);
}
// 6. Apply Health Modification:
// If the current health is below the target, attempt to set it using the abstraction.
if (currentHealth < targetHealth) {
// The SetCurrentHealth method returns false if an exception occurred during the write.
if (!face.SetCurrentHealth(targetHealth)) {
// Write failed. Log the error and disable further modifications for safety.
Log(LOG_ERROR, "Failed to set health for face [" + std::to_string(shieldFaceIndex) +
"] via abstraction. Disabling modifications.");
g_Config.enableModification = false;
} else {
// Write was successful (or at least no exception occurred).
Log(LOG_DEBUG, "Set shield face [" + std::to_string(shieldFaceIndex) +
"] health to " + std::to_string(targetHealth) + " before original instructions.");
}
}
// ======================================================= //
// TODO: Player Identification Logic Implementation Point //
// //
// To restrict this modification only to the player's ship, //
// logic needs to be inserted *before* the call to //
// `face.SetCurrentHealth(targetHealth)`. This logic would //
// involve: //
// 1. Obtaining the Entity ID of the local player. //
// 2. Obtaining the Entity ID of the owner of the current //
// `component` (pShieldComponent_raw). //
// 3. Comparing the two IDs. //
// 4. Only proceeding with `SetCurrentHealth` if they match.//
// This requires extensive additional reverse engineering. //
// ======================================================= //
} else {
// Failed to read either current or max health (error logged by abstraction).
// Disable modifications to prevent repeated errors.
Log(LOG_ERROR, "Failed to read health values for face [" + std::to_string(shieldFaceIndex) +
"] via abstraction. Disabling modifications.");
g_Config.enableModification = false;
}
} else {
// GetShieldFace returned an invalid object (e.g., index out of bounds).
// A warning should have been logged internally by ShieldComponent.
Log(LOG_DEBUG, "Skipping modification for invalid face object at index " + std::to_string(shieldFaceIndex));
}
}
catch (...) {
// Catch any unexpected C++ exceptions within the hook logic itself.
Log(LOG_ERROR, "Caught unexpected C++ exception in HookedInstructionSequence. Disabling modifications.");
g_Config.enableModification = false;
// Fall through to call trampoline if possible.
}
// --- Execute Original Game Code ---
// Regardless of whether modification occurred or succeeded (unless trampoline is null),
// call the trampoline function. This executes the original game instructions that were
// overwritten by the hook and allows the game to continue its normal execution flow.
// Pass the original arguments received by the hook.
if (!g_OriginalTrampoline) {
Log(LOG_ERROR, "HookedInstructionSequence: Cannot execute original code, trampoline is NULL!");
// Critical error. Return a default value (e.g., 0 for shield down) as a fallback.
return 0;
}
return g_OriginalTrampoline(pShieldComponent_raw, shieldFaceIndex);
}
// =================================================================================================
// Pattern Scanning & Contextual Validation
// =================================================================================================
/**
* @brief Parses a signature string (e.g., "AA BB ?? CC") into bytes and a mask.
* Handles single-byte ("A") or double-byte ("AA") hex tokens and wildcards ("?" or "??").
* @param signatureString The input signature string with space-separated tokens.
* @param patternBytes Output vector to store parsed bytes (0 for wildcards).
* @param patternMask Output string to store the mask ('x' for byte, '?' for wildcard).
* @return True if parsing was successful, false on any error (invalid token, format).
*/
bool ParseSignature(const std::string& signatureString, std::vector<uint8_t>& patternBytes, std::string& patternMask) {
patternBytes.clear();
patternMask.clear();
std::istringstream iss(signatureString);
std::string token;
while (iss >> token) {
if (token == "?" || token == "??") {
patternBytes.push_back(0); // Use 0 as placeholder byte for wildcards
patternMask.push_back('?');
} else if (token.length() > 0 && token.length() <= 2 &&
std::all_of(token.begin(), token.end(), ::isxdigit)) // Check if valid hex token
{
try {
patternBytes.push_back(static_cast<uint8_t>(std::stoul(token, nullptr, 16)));
patternMask.push_back('x'); // Mark as exact match byte
} catch (const std::invalid_argument& e) {
Log(LOG_ERROR, "[ParseSignature] Invalid hex token format: '" + token + "' - " + e.what());
return false;
} catch (const std::out_of_range& e) {
Log(LOG_ERROR, "[ParseSignature] Hex token out of range: '" + token + "' - " + e.what());
return false;
}
} else {
Log(LOG_ERROR, "[ParseSignature] Invalid token encountered: '" + token + "'");
return false; // Invalid token format
}
}
// Final validation: Ensure pattern and mask have the same non-zero length.
if (patternBytes.size() != patternMask.length() || patternBytes.empty()) {
Log(LOG_ERROR, "[ParseSignature] Parsed signature is empty or mask/pattern length mismatch.");
return false;
}
return true;
}
/**
* @brief Scans a given memory region for all occurrences of a specified byte pattern.
* Uses SEH to handle potential memory read errors during the scan.
* @param searchBase The starting virtual address of the memory region to scan.
* @param searchSize The total size (in bytes) of the memory region.
* @param patternBytes A vector containing the byte sequence to search for (with 0 for wildcards).
* @param patternMask A string mask ('x' for exact match, '?' for wildcard) corresponding to patternBytes.
* @return A std::vector containing the starting addresses of all found occurrences of the pattern. Returns empty vector on error or if not found.
*/
std::vector<uintptr_t> FindAllPatternMatches(uintptr_t searchBase, DWORD searchSize, const std::vector<uint8_t>& patternBytes, const std::string& patternMask) {
std::vector<uintptr_t> matches;
// Basic input validation
if (!searchBase || searchSize == 0 || patternBytes.empty() || patternBytes.size() != patternMask.length()) {
Log(LOG_ERROR, "[FindAllPatternMatches] Invalid parameters provided.");
return matches; // Return empty vector
}
size_t patternLength = patternMask.length();
// Check if the search region is large enough to contain the pattern
if (searchSize < patternLength) {
Log(LOG_WARNING, "[FindAllPatternMatches] Search size (" + std::to_string(searchSize) +
") is smaller than pattern length (" + std::to_string(patternLength) + ").");
return matches; // Cannot possibly find the pattern
}
// Iterate through the memory region, byte by byte
for (DWORD i = 0; i <= searchSize - patternLength; ++i) {
bool found = true;
// Compare the pattern at the current position (searchBase + i)
for (size_t j = 0; j < patternLength; ++j) {
__try {
// Check if the mask requires an exact match ('x') and if the bytes differ
if (patternMask[j] != '?' && patternBytes[j] != *reinterpret_cast<uint8_t*>(searchBase + i + j)) {
found = false;
break; // Mismatch found, break inner loop and move to next 'i'
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
// Memory read error (e.g., accessing unmapped or protected page)
found = false;
Log(LOG_DEBUG, "[FindAllPatternMatches] SEH caught memory read error at address " + PtrToHexStr(searchBase + i + j));
// Advance 'i' past the byte that caused the error to avoid getting stuck
i += j;
// Check if advancing 'i' pushes us beyond the searchable limit
if (i > searchSize - patternLength) {
goto end_scan; // Exit outer loop if we've gone too far
}
break; // Break inner loop
}
}
// If the inner loop completed without mismatch (and no exceptions that set found=false)
if (found) {
matches.push_back(searchBase + i); // Add the starting address of the match
Log(LOG_DEBUG, "[FindAllPatternMatches] Found potential match at " + PtrToHexStr(searchBase + i));
// Optional: If overlapping matches are undesirable, advance 'i' significantly.
// i += patternLength - 1; // Advance to the byte after the current match
}
}
end_scan: // Label for goto from exception handler
return matches;
}
/**
* @brief Validates a potential hook target address by analyzing the surrounding code context.
* Specifically checks for a subsequent 'vcomiss xmm0, [rip+offset]' instruction that references
* a floating-point constant matching heuristic criteria for a "low health threshold".
* @param candidateAddress The starting address of a matched primary instruction signature.
* @param primarySigLength The length (in bytes) of the primary instruction signature.
* @param moduleBase The base address of the module being scanned (for bounds checking).
* @param moduleSize The total size (in bytes) of the module.
* @return True if the context strongly suggests this is the correct target location, false otherwise.
*/
bool ValidateHookContext(uintptr_t candidateAddress, size_t primarySigLength, uintptr_t moduleBase, DWORD moduleSize) {
// Define the memory range immediately following the primary signature match to search for the context clue.
uintptr_t searchStart = candidateAddress + primarySigLength;
uintptr_t searchEnd = searchStart + CONTEXT_SEARCH_RANGE;
// Clamp the search range to stay within the module's boundaries.
if (searchEnd > moduleBase + moduleSize) {
searchEnd = moduleBase + moduleSize;
}
// If the search range is invalid or empty, validation fails.
if (searchStart >= searchEnd) {
Log(LOG_DEBUG, "[ValidateHookContext] Context search range invalid for candidate " + PtrToHexStr(candidateAddress));
return false;
}
// Parse the context signature (opcode for vcomiss xmm0, mem32 [rip+rel32])
std::vector<uint8_t> contextBytes;
std::string contextMask;
// This parsing should always succeed if the hardcoded signature is correct.
if (!ParseSignature(CONTEXT_VCOMISS_LOW_THRESHOLD_SIG, contextBytes, contextMask)) {
Log(LOG_ERROR, "[ValidateHookContext] Failed to parse internal context signature. This is a developer error.");
return false;
}
size_t contextSigLength = contextBytes.size(); // Should be 4 bytes for C5 F8 2F 05
// Scan the context range for the vcomiss instruction bytes.
// We need space for the opcode (contextSigLength) + the 4-byte relative offset.
for (uintptr_t currentAddr = searchStart; (currentAddr + contextSigLength + 4) <= searchEnd; ++currentAddr) {
bool contextMatch = true;
// Check if the opcode bytes match at the current address.
for (size_t j = 0; j < contextSigLength; ++j) {
__try {
if (contextMask[j] != '?' && contextBytes[j] != *reinterpret_cast<uint8_t*>(currentAddr + j)) {
contextMatch = false;
break;
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
contextMatch = false;
Log(LOG_DEBUG, "[ValidateHookContext] SEH caught memory read error scanning context at " + PtrToHexStr(currentAddr + j));
// Advance outer loop counter past the error location
currentAddr += j;
if ((currentAddr + contextSigLength + 4) > searchEnd) goto end_context_scan;
break; // Break inner loop
}
}
// If the vcomiss opcode was found...
if (contextMatch) {
Log(LOG_DEBUG, "[ValidateHookContext] Found context signature (vcomiss) at " + PtrToHexStr(currentAddr));
// ...proceed to read the relative offset and the referenced constant value.
uintptr_t instructionEnd = currentAddr + contextSigLength; // Address immediately after the opcode bytes
int32_t relativeOffset = 0;
uintptr_t constantAddress = 0;
float constantValue = NAN; // Initialize to Not-a-Number
__try {
// Read the 32-bit signed relative offset from memory.
relativeOffset = *reinterpret_cast<int32_t*>(instructionEnd);
// Calculate the absolute address of the constant using RIP-relative addressing.
// RIP points to the instruction *after* the current one.
// The length of 'vcomiss xmm0, [rip+rel32]' is 8 bytes (4 opcode + 4 offset).
constantAddress = currentAddr + 8 + relativeOffset;
// Basic sanity check: Is the calculated constant address within the module?
if (constantAddress >= moduleBase && constantAddress < (moduleBase + moduleSize - sizeof(float))) {
// Read the float value from the calculated address.
constantValue = *reinterpret_cast<float*>(constantAddress);
Log(LOG_DEBUG, "[ValidateHookContext] Calculated constant address: " + PtrToHexStr(constantAddress) + ", Value: " + std::to_string(constantValue));
} else {
Log(LOG_WARNING, "[ValidateHookContext] Calculated constant address " + PtrToHexStr(constantAddress) + " is out of module bounds.");
continue; // Address is invalid, continue searching for another vcomiss in the context range.
}
} __except (HandleMemoryAccessException(GetExceptionCode(), GetExceptionInformation())) {
// Memory error reading the offset or the constant value itself.
Log(LOG_WARNING, "[ValidateHookContext] Exception reading offset/constant for vcomiss at " + PtrToHexStr(currentAddr));
continue; // Try finding another vcomiss instance within the context range.
}
// Apply Heuristic Check: Does the constant value look like a low threshold?
// Check if it's a valid, positive float within the defined heuristic range.
if (!std::isnan(constantValue) && !std::isinf(constantValue) &&
constantValue > LOW_THRESHOLD_MIN_VALUE && constantValue < LOW_THRESHOLD_MAX_VALUE)
{
// Validation successful! We found the expected instruction referencing a plausible constant.
Log(LOG_INFO, "[ValidateHookContext] Context VALIDATED for candidate " + PtrToHexStr(candidateAddress) +
". Found vcomiss referencing plausible threshold " + std::to_string(constantValue) +
" at " + PtrToHexStr(constantAddress));
return true; // This candidate is considered valid.
} else {
// The constant value doesn't match the heuristic.
Log(LOG_DEBUG, "[ValidateHookContext] Context check failed for candidate " + PtrToHexStr(candidateAddress) +
". Found vcomiss at " + PtrToHexStr(currentAddr) + " referencing non-characteristic float " + std::to_string(constantValue));
// Continue searching the rest of the context range for this candidate, just in case
// there's another vcomiss that *does* match.
}
} // end if(contextMatch)
} // end for loop scanning context range
end_context_scan:
// If the loop completes without returning true, no valid context was found for this candidate.
Log(LOG_DEBUG, "[ValidateHookContext] Context check FAILED for candidate " + PtrToHexStr(candidateAddress) + ". No validating context found in range.");
return false;
}
/**
* @brief Orchestrates finding the hook target address.
* 1. Scans for all occurrences of the primary instruction signature.
* 2. For each match, performs contextual validation using ValidateHookContext.
* 3. Returns the address only if *exactly one* match passes validation.
* @param modInfo MODULEINFO structure containing the target module's base address and size.
* @return The validated, unique hook target address, or 0 if no unique target is found.
*/
uintptr_t FindUniqueHookTarget(const MODULEINFO& modInfo) {
// Step 1: Parse the primary instruction signature.
std::vector<uint8_t> patternBytes;
std::string patternMask;
if (!ParseSignature(TARGET_INSTRUCTION_SIGNATURE, patternBytes, patternMask)) {
Log(LOG_ERROR, "[FindUniqueHookTarget] Failed to parse primary instruction signature.");
return 0; // Cannot proceed
}
size_t primarySigLength = patternBytes.size();
Log(LOG_INFO, "[FindUniqueHookTarget] Primary signature parsed (" + std::to_string(primarySigLength) + " bytes).");
// Step 2: Find all potential matches for the primary signature.
Log(LOG_INFO, "[FindUniqueHookTarget] Scanning module for all occurrences of the primary signature...");
std::vector<uintptr_t> matches = FindAllPatternMatches(
reinterpret_cast<uintptr_t>(modInfo.lpBaseOfDll),
modInfo.SizeOfImage,
patternBytes,
patternMask
);
Log(LOG_INFO, "[FindUniqueHookTarget] Found " + std::to_string(matches.size()) + " potential match(es).");
// Step 3: Validate each match using contextual analysis.
uintptr_t validatedTarget = 0; // Store the address if validated
int validCount = 0; // Count how many matches pass validation
Log(LOG_INFO, "[FindUniqueHookTarget] Performing contextual validation on potential matches...");
for (uintptr_t candidate : matches) {
Log(LOG_DEBUG, "[FindUniqueHookTarget] Validating candidate address: " + PtrToHexStr(candidate));
// Call the validation function for the current candidate address.
if (ValidateHookContext(candidate, primarySigLength, reinterpret_cast<uintptr_t>(modInfo.lpBaseOfDll), modInfo.SizeOfImage)) {
// If validation passes, store the address and increment the count.
validatedTarget = candidate;
validCount++;
Log(LOG_INFO, "[FindUniqueHookTarget] Candidate " + PtrToHexStr(candidate) + " PASSED validation.");
} else {
Log(LOG_INFO, "[FindUniqueHookTarget] Candidate " + PtrToHexStr(candidate) + " FAILED validation.");
}
}
// Step 4: Determine the final result based on the validation count.
if (validCount == 1) {
// Exactly one match passed validation - this is the desired outcome.
Log(LOG_SUCCESS, "[FindUniqueHookTarget] Unique hook target successfully validated at: " + PtrToHexStr(validatedTarget));
return validatedTarget;
} else if (validCount == 0) {
// No matches passed validation.
if (matches.empty()) {
Log(LOG_ERROR, "[FindUniqueHookTarget] Primary signature not found in the module.");
} else {
Log(LOG_ERROR, "[FindUniqueHookTarget] Found " + std::to_string(matches.size()) + " primary signature match(es), but NONE passed contextual validation.");
}
Log(LOG_ERROR, "[FindUniqueHookTarget] Cannot identify hook target. Possible reasons: Game update, incorrect signatures, failed context heuristic.");
return 0;
} else { // validCount > 1
// Multiple matches passed validation - the context heuristic is not specific enough.
Log(LOG_ERROR, "[FindUniqueHookTarget] Found " + std::to_string(validCount) + " matches that passed contextual validation. Result is AMBIGUOUS.");
Log(LOG_ERROR, "[FindUniqueHookTarget] Cannot reliably identify the correct hook target. Context heuristic needs refinement or primary signature needs to be more specific.");
return 0;
}
}
// =================================================================================================
// Hook Management (MinHook Integration)
// =================================================================================================
/**
* @brief Initializes the MinHook library and installs the hook at the specified target address.
* Creates a detour to HookedInstructionSequence and stores the trampoline pointer in g_OriginalTrampoline.
* @param targetAddress The validated memory address where the hook should be placed (start of the instruction sequence).
* @return True if MinHook initialization, hook creation, and hook enabling were all successful, false otherwise.
*/
bool InstallHook(uintptr_t targetAddress) {
Log(LOG_INFO, "Attempting to install instruction hook at " + PtrToHexStr(targetAddress));
// Step 1: Initialize the MinHook library. Must be called before any other MinHook functions.
MH_STATUS status = MH_Initialize();
if (status != MH_OK) {
Log(LOG_ERROR, "MinHook initialization failed: " + std::string(MH_StatusToString(status)));
return false;
}
Log(LOG_DEBUG, "MinHook initialized successfully.");
// Step 2: Create the hook.
// - pTarget: The address in the game code where the detour will be placed.
// - pDetour: A pointer to our replacement function (HookedInstructionSequence).
// - ppOriginal: A pointer to a variable (g_OriginalTrampoline) where MinHook will store the address
// of the trampoline function (which executes the original instructions).
status = MH_CreateHook(
reinterpret_cast<LPVOID>(targetAddress), // Target address in game code
reinterpret_cast<LPVOID>(&HookedInstructionSequence), // Our detour function
reinterpret_cast<LPVOID*>(&g_OriginalTrampoline) // Store trampoline address here
);
if (status != MH_OK) {
Log(LOG_ERROR, "Instruction hook creation failed: " + std::string(MH_StatusToString(status)));
MH_Uninitialize(); // Clean up MinHook if hook creation failed
return false;
}
Log(LOG_DEBUG, "Instruction hook created successfully. Trampoline at: " + PtrToHexStr(reinterpret_cast<uintptr_t>(g_OriginalTrampoline)));
// Step 3: Enable the hook.
// This activates the detour by patching the game code at the target address.
status = MH_EnableHook(reinterpret_cast<LPVOID>(targetAddress));
if (status != MH_OK) {
Log(LOG_ERROR, "Instruction hook enable failed: " + std::string(MH_StatusToString(status)));
// If enabling failed, try to remove the hook entry before cleaning up MinHook.
MH_RemoveHook(reinterpret_cast<LPVOID>(targetAddress));
MH_Uninitialize();
return false;
}
Log(LOG_SUCCESS, "Instruction hook installed and enabled successfully at " + PtrToHexStr(targetAddress));
return true;
}
/**
* @brief Disables all installed hooks and uninitializes the MinHook library.
* Should be called before the DLL unloads to restore original game code.
*/
void RemoveHook() {
Log(LOG_INFO, "Attempting to disable and remove hooks...");
// Disable all hooks created by MinHook in this process.
// MH_ALL_HOOKS is a special value to target all hooks.
MH_STATUS status = MH_DisableHook(MH_ALL_HOOKS);
// It's okay if hooks were already disabled (MH_ERROR_DISABLED). Log other errors as warnings.
if (status != MH_OK && status != MH_ERROR_DISABLED) {
Log(LOG_WARNING, "Failed to disable hooks cleanly: " + std::string(MH_StatusToString(status)));
// Continue with uninitialization regardless.
} else {
Log(LOG_DEBUG, "All hooks disabled.");
}
// Uninitialize the MinHook library. This removes all hooks and frees resources used by MinHook.
status = MH_Uninitialize();
if (status != MH_OK) {
// Log failure as a warning, as we are likely shutting down anyway.
Log(LOG_WARNING, "MinHook uninitialization failed: " + std::string(MH_StatusToString(status)));
} else {
Log(LOG_DEBUG, "MinHook uninitialized.");
}
Log(LOG_INFO, "Hook removal process completed.");
}
// =================================================================================================
// Main Initialization and Control Thread
// =================================================================================================
/**
* @brief The main worker thread for the DLL.
* This thread is spawned by DllMain during process attachment. It handles all significant
* initialization tasks (console, module scanning, pattern scanning, validation, hook installation)
* and the shutdown process (waiting for unload key, removing hooks, cleaning up resources).
* This avoids performing complex operations within the DllMain loader lock.
* @param lpReserved Parameter passed from CreateThread, expected to be the HMODULE of this DLL.
* @return DWORD Thread exit code (0 indicates successful completion).
*/
DWORD WINAPI MainInitializationThread(LPVOID lpReserved) {
// Store the DLL's module handle for later use (e.g., unloading).
g_hModule = static_cast<HMODULE>(lpReserved);
// --- Phase 1: Initialization and Console Setup ---
FILE* pConsoleStream = nullptr; // File stream pointer for console redirection
try {
// Allocate a console window for displaying logs.
if (!AllocConsole()) {
// If console allocation fails, we can't log easily. Proceed without logging.
// Consider alternative logging (e.g., file) in a real application.
} else {
// Redirect standard output (stdout) and standard error (stderr) streams
// to the newly allocated console window.
freopen_s(&pConsoleStream, "CONOUT$", "w", stdout);
freopen_s(&pConsoleStream, "CONOUT$", "w", stderr);
// Clear any potential error states on the standard streams.
std::cout.clear();
std::cerr.clear();
}
// Log initial banner and warnings.
Log(LOG_INFO, "======================================================");
Log(LOG_INFO, " Star Citizen Shield Modifier (Contextual Hook)");
Log(LOG_INFO, "======================================================");
Log(LOG_INFO, "Build Time: " __DATE__ " " __TIME__);
Log(LOG_INFO, "Target Module: " + std::string(TARGET_PROCESS_NAME));
Log(LOG_WARNING, "DISCLAIMER: This tool relies on reverse-engineered data specific");
Log(LOG_WARNING, " to game version ~4.1.0-live.9659838. It WILL likely");
Log(LOG_WARNING, " break after game updates. Use with extreme caution.");
Log(LOG_WARNING, " Modifies ALL entities, not just player. Online use");
Log(LOG_WARNING, " carries significant BAN RISK.");
std::cout << std::endl;
// --- Phase 2: Target Module Acquisition ---
Log(LOG_INFO, "Acquiring handle for target module: " + std::string(TARGET_PROCESS_NAME));
HMODULE hTargetModule = GetModuleHandleA(TARGET_PROCESS_NAME);
if (!hTargetModule) {
Log(LOG_ERROR, "Target module not found in current process.");
Log(LOG_ERROR, "Ensure the DLL is injected *after* the game has fully loaded.");
goto cleanup_and_exit; // Use goto for centralized cleanup
}
Log(LOG_INFO, "Target module found. Base Address: " + PtrToHexStr(reinterpret_cast<uintptr_t>(hTargetModule)));
// --- Phase 3: Get Module Information ---
Log(LOG_INFO, "Retrieving module information (size, entry point)...");
MODULEINFO modInfo{}; // Structure to hold module base address and size
if (!GetModuleInformation(GetCurrentProcess(), hTargetModule, &modInfo, sizeof(modInfo))) {
Log(LOG_ERROR, "Failed to get module information. Error code: " + std::to_string(GetLastError()));
goto cleanup_and_exit;
}
Log(LOG_INFO, "Module Size: " + std::to_string(modInfo.SizeOfImage) + " bytes.");
// --- Phase 4: Find Unique Hook Target ---
// This function performs the primary signature scan AND contextual validation.
uintptr_t targetAddress = FindUniqueHookTarget(modInfo);
// Check if a unique, validated target was found.
if (!targetAddress) {
// FindUniqueHookTarget logs detailed errors internally.
Log(LOG_ERROR, "Failed to identify a unique and valid hook target. Cannot proceed.");
goto cleanup_and_exit;
}
// targetAddress now holds the validated starting address of the instruction sequence.
// --- Phase 5: Install Hook ---
// Attempt to install the hook at the validated address.
if (!InstallHook(targetAddress)) {
// InstallHook logs detailed errors internally.
Log(LOG_ERROR, "Hook installation failed. Aborting initialization.");
// InstallHook should attempt MinHook cleanup on failure.
goto cleanup_and_exit;
}
// Hook installation successful.
// --- Phase 6: Active Phase - Wait for Unload Signal ---
Log(LOG_INFO, "Hook is now ACTIVE.");
Log(LOG_INFO, "Press the [" + std::to_string(g_Config.unloadKey) + "] key (END by default) to safely unload the DLL.");
// Loop indefinitely, checking for the unload key press.
while (true) {
// GetAsyncKeyState checks the current state AND if the key was pressed since the last call.
// We check the high-order bit (0x8000) for current press state.
if (GetAsyncKeyState(g_Config.unloadKey) & 0x8000) {
Log(LOG_INFO, "Unload key detected. Initiating shutdown sequence...");
break; // Exit the loop to proceed to cleanup
}
// Sleep briefly to prevent the loop from consuming excessive CPU.
Sleep(100); // Check roughly 10 times per second
}
} catch (const std::exception& e) {
// Catch standard C++ exceptions that might occur during setup.
Log(LOG_ERROR, "Caught C++ exception during initialization: " + std::string(e.what()));
// Proceed to cleanup.
} catch (...) {
// Catch any other unexpected exceptions.
Log(LOG_ERROR, "Caught unknown exception during initialization.");
// Proceed to cleanup.
}
// --- Phase 7: Cleanup and Exit ---
cleanup_and_exit: // Label for goto jumps on critical errors
Log(LOG_INFO, "Performing cleanup...");
// Remove hooks and uninitialize MinHook (safe to call even if not initialized/installed).
RemoveHook();
Log(LOG_INFO, "Cleanup complete. Detaching DLL...");
// Flush console streams before closing the console.
std::cout << std::flush;
std::cerr << std::flush;
// Close the console file stream if it was opened.
if (pConsoleStream) {
fclose(pConsoleStream);
}
// Free the allocated console window.
FreeConsole();
// Unload this DLL from the process and terminate this thread cleanly.
// Pass the module handle stored earlier.
FreeLibraryAndExitThread(g_hModule, 0);
// The thread terminates here. Return code is technically unused as the thread exits.
return 0;
}
// =================================================================================================
// DLL Entry Point (DllMain)
// =================================================================================================
/**
* @brief The standard entry point for the DLL.
* This function is called by the Windows loader when the DLL is attached to or detached from a process.
* Minimal work should be done here, especially during DLL_PROCESS_ATTACH, to avoid deadlocks
* related to the loader lock. Complex initialization is deferred to MainInitializationThread.
*
* @param hModule Handle to the DLL module instance.
* @param ul_reason_for_call Specifies the reason the entry point is being called (e.g., process attach/detach).
* @param lpReserved Reserved parameter; specific meaning depends on ul_reason_for_call.
* @return BOOL Returns TRUE to indicate success. Returning FALSE during DLL_PROCESS_ATTACH prevents the DLL from loading.
*/
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
// Called when the DLL is first loaded into the process address space.
// Disable notifications for thread attach/detach events (DLL_THREAD_ATTACH, DLL_THREAD_DETACH).
// This can slightly improve performance and reduce potential loader lock contention.
DisableThreadLibraryCalls(hModule);
// Create a new thread to execute the main initialization and control logic.
// Pass the DLL's module handle (hModule) as the parameter to the thread function.
// It's crucial *not* to perform lengthy operations like pattern scanning or hook installation
// directly within DllMain during process attach due to the loader lock.
HANDLE hThread = CreateThread(
nullptr, // Default security attributes
0, // Default stack size
MainInitializationThread, // Thread function to execute
hModule, // Parameter passed to the thread function (our module handle)
0, // Default creation flags (run immediately)
nullptr // Thread ID (not needed)
);
// Check if thread creation was successful.
if (hThread == nullptr) {
// Critical error: Failed to create the main worker thread.
// Log an error (though console might not be set up yet).
// Display a message box as a last resort.
MessageBoxA(NULL, "FATAL ERROR: Failed to create initialization thread.", "Shield Modifier DLL Load Error", MB_ICONERROR | MB_OK);
// Return FALSE to prevent the DLL from loading into the process.
return FALSE;
}
// If thread creation succeeded, we don't need to wait for it or manage it further.
// Close the handle to the thread object to release system resources associated with it.
// The thread itself will continue to run independently.
CloseHandle(hThread);
break;
case DLL_THREAD_ATTACH:
// Called when a new thread is created in the process (after the initial DLL_PROCESS_ATTACH).
// Not used in this implementation.
break;
case DLL_THREAD_DETACH:
// Called when a thread exits cleanly (before DLL_PROCESS_DETACH if it's the last thread).
// Not used in this implementation.
break;
case DLL_PROCESS_DETACH:
// Called when the DLL is being unloaded from the process address space.
// This can happen during normal process termination or if FreeLibrary is called.
// lpReserved indicates if termination is normal (non-NULL) or abrupt (NULL).
//
// IMPORTANT: Performing complex cleanup here (like removing hooks via MinHook)
// is generally unreliable, especially during abrupt termination. The main thread's
// explicit cleanup triggered by the unload key is the preferred method.
// We log the event for debugging purposes.
Log(LOG_DEBUG, "DLL_PROCESS_DETACH received. lpReserved: " + PtrToHexStr(reinterpret_cast<uintptr_t>(lpReserved)));
break;
}
// Indicate successful handling of the DllMain call.
return TRUE;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment