Skip to content

Commit 4e43375

Browse files
authored
Feature flags (#17)
* include `Syringe.h` * init feature flags * use double quotes for consistency * friendship ended with `__declspec(selectany)`, `inline` is my new best friend iow it didn't compile * add docs
1 parent ba05ddb commit 4e43375

4 files changed

Lines changed: 488 additions & 0 deletions

File tree

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,46 @@ syringe.exe game.exe --nowait
7878

7979
These options can be combined to precisely control debugger lifetime and process synchronization behavior.
8080

81+
# Feature Flags API
82+
83+
SyringeEx uses a feature flags system to signal capability support to injected DLLs. This allows DLLs to verify that the running Syringe version supports the features they depend on, enabling version-aware behavior and graceful fallbacks for older installations.
84+
85+
Current closed-source version of Syringe is considered a baseline (SyringeEx has the features reimplemented, namely: multithreaded hook support, skipping handshakes that refuse Ares (Yuri's Revenge engine extension DLL) to load on Steam version of Yuri's Revenge). Everything beyond that must be declared as a feature flag, and DLLs that want to utilize the new features must check for the presence of required features before using them, and either refuse to load or provide fallback behavior if the features are not supported.
86+
87+
## How It Works
88+
89+
When a DLL is loaded, Syringe resolves exported boolean symbols from the `SyringeFeatures` namespace and sets them to `true` if the feature is supported. DLLs default these flags to `false`, so older Syringe versions that don't know about the flags will leave them unchanged.
90+
91+
## Using Feature Flags in Your DLL
92+
93+
```cpp
94+
#include <Syringe.h>
95+
96+
void YourDLL::SomeLoadCode()
97+
{
98+
if (SyringeFeatures::ZFPreservation)
99+
{
100+
// This Syringe version preserves the Zero Flag, so we can safely hook conditional instructions
101+
}
102+
else
103+
{
104+
// Fallback for older versions or bail out if this feature is critical
105+
}
106+
}
107+
```
108+
109+
## Current Feature Flags
110+
111+
- `ESPModification` - Adds an ability for DLLs to modify the stack pointer (ESP) across hooks to be able to exit on addresses with a different stack depth than the hook entry point
112+
- `ZFPreservation` - Indicates that the Zero Flag (ZF) is preserved after hook execution, allowing to hook on conditional instructions
113+
114+
## Adding New API Features
115+
116+
All API enhancements beyond original Syringe must be declared as feature flags.
117+
118+
To add a new feature:
119+
120+
1. Add a new boolean to [`SyringeFeatures` namespace](include/Syringe.h)
121+
2. Add the symbol name to `FeatureFlagNames[]` in [`SyringeDebugger.h`](SyringeDebugger.h)
122+
3. Document the feature and its behavior in this README
123+

SyringeDebugger.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <fstream>
1212
#include <memory>
1313
#include <numeric>
14+
#include <set>
1415

1516
#include <DbgHelp.h>
1617

@@ -145,6 +146,73 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent)
145146
Log::WriteLine(__FUNCTION__ ": Finished retrieving proc addresses.");
146147
bDLLsLoaded = true;
147148

149+
if (!v_FeatureFlags.empty())
150+
{
151+
Log::WriteLine(__FUNCTION__ ": Starting feature flags resolution...");
152+
loop_FeatureFlags = v_FeatureFlags.begin();
153+
auto const& entry = *loop_FeatureFlags;
154+
PatchMem(&GetData()->LibName, entry.lib, MaxNameLength);
155+
PatchMem(&GetData()->ProcName, entry.symbol, MaxNameLength);
156+
157+
context.Eip = reinterpret_cast<DWORD>(&GetData()->LoadLibraryFunc);
158+
}
159+
else
160+
{
161+
bFeaturesSet = true;
162+
context.Eip = reinterpret_cast<DWORD>(pcEntryPoint);
163+
}
164+
}
165+
166+
// single step mode
167+
context.EFlags |= 0x100;
168+
context.ContextFlags = CONTEXT_CONTROL;
169+
SetThreadContext(currentThread, &context);
170+
171+
threadInfo.lastBP = exceptAddr;
172+
173+
return DBG_CONTINUE;
174+
}
175+
176+
// set feature flags in loaded DLLs
177+
if (!bFeaturesSet)
178+
{
179+
// restore
180+
PatchMem(exceptAddr, &Breakpoints[exceptAddr].original_opcode, 1);
181+
182+
// read the resolved address of the feature flag in the target process
183+
void* flagAddr = nullptr;
184+
ReadMem(&GetData()->ProcAddress, &flagAddr, 4);
185+
186+
if (flagAddr)
187+
{
188+
BYTE const trueVal = 1;
189+
PatchMem(flagAddr, &trueVal, 1);
190+
Log::WriteLine(
191+
__FUNCTION__ ": Set feature flag \"%s\" in \"%s\" at 0x%08X",
192+
loop_FeatureFlags->symbol, loop_FeatureFlags->lib, flagAddr);
193+
}
194+
else
195+
{
196+
Log::WriteLine(
197+
__FUNCTION__ ": Feature flag \"%s\" not exported by \"%s\", skipping.",
198+
loop_FeatureFlags->symbol, loop_FeatureFlags->lib);
199+
}
200+
201+
++loop_FeatureFlags;
202+
203+
if (loop_FeatureFlags != v_FeatureFlags.end())
204+
{
205+
auto const& entry = *loop_FeatureFlags;
206+
PatchMem(&GetData()->LibName, entry.lib, MaxNameLength);
207+
PatchMem(&GetData()->ProcName, entry.symbol, MaxNameLength);
208+
209+
context.Eip = reinterpret_cast<DWORD>(&GetData()->LoadLibraryFunc);
210+
}
211+
else
212+
{
213+
Log::WriteLine(__FUNCTION__ ": Finished setting feature flags.");
214+
bFeaturesSet = true;
215+
148216
context.Eip = reinterpret_cast<DWORD>(pcEntryPoint);
149217
}
150218

@@ -505,6 +573,7 @@ void SyringeDebugger::Run(std::string_view const arguments)
505573
// breakpoints for DLL loading and proc address retrieving
506574
bDLLsLoaded = false;
507575
bHooksCreated = false;
576+
bFeaturesSet = false;
508577
loop_LoadLibrary = v_AllHooks.end();
509578

510579
// set breakpoint
@@ -779,6 +848,29 @@ void SyringeDebugger::FindDLLs()
779848
}
780849

781850
Log::WriteLine(__FUNCTION__ ": Done (%d hooks added).", v_AllHooks.size());
851+
852+
// build feature flag entries for each unique DLL
853+
v_FeatureFlags.clear();
854+
{
855+
std::set<std::string> uniqueLibs;
856+
for (auto const& hook : v_AllHooks)
857+
{
858+
if (uniqueLibs.insert(hook->lib).second)
859+
{
860+
for (auto const& flagName : FeatureFlagNames)
861+
{
862+
FeatureFlagEntry entry{};
863+
strncpy_s(entry.lib, hook->lib, MaxNameLength - 1);
864+
flagName.copy(entry.symbol, MaxNameLength - 1);
865+
v_FeatureFlags.push_back(entry);
866+
}
867+
}
868+
}
869+
}
870+
871+
Log::WriteLine(
872+
__FUNCTION__ ": %d feature flag entries prepared.",
873+
v_FeatureFlags.size());
782874
Log::WriteLine();
783875
}
784876

SyringeDebugger.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ class SyringeDebugger
143143
std::vector<Hook*> v_AllHooks;
144144
std::vector<Hook*>::iterator loop_LoadLibrary;
145145

146+
// feature flags
147+
static constexpr std::string_view FeatureFlagNames[] = {
148+
"ESPModification",
149+
"ZFPreservation",
150+
};
151+
152+
struct FeatureFlagEntry
153+
{
154+
char lib[MaxNameLength];
155+
char symbol[MaxNameLength];
156+
};
157+
158+
std::vector<FeatureFlagEntry> v_FeatureFlags;
159+
std::vector<FeatureFlagEntry>::iterator loop_FeatureFlags;
160+
bool bFeaturesSet{ false };
161+
146162
// syringe
147163
std::string exe;
148164
std::vector<std::string> dlls{};

0 commit comments

Comments
 (0)