Last active
April 4, 2025 12:00
-
-
Save 19h/6d48dccf02daa944dd830a06d2aaf3f6 to your computer and use it in GitHub Desktop.
sub_143434440_hook based on Sycorax's original concept
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; =============== 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 | |
; --------------------------------------------------------------------------- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; ================================================================================================= | |
; 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 | |
; ================================================================================================= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ================================================================================================= | |
// 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