From c7e31bc95c4d47fb8ab337795b8daf0502593fac Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Sat, 22 Aug 2026 01:58:52 +0300 Subject: [PATCH 1/6] Enforce W^X for generated code and enable exploit mitigations --- Debugger.vcxproj | 12 ++++++------ Handle.h | 15 ++++++++++++++- SyringeDebugger.cpp | 21 +++++++++++++++++++-- SyringeDebugger.h | 8 +++++++- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/Debugger.vcxproj b/Debugger.vcxproj index 120d7c3..143d585 100644 --- a/Debugger.vcxproj +++ b/Debugger.vcxproj @@ -74,7 +74,7 @@ true false stdcpp20 - false + true true true @@ -89,8 +89,8 @@ true .\Debug\Syringe.pdb Windows - false - + true + true MachineX86 true true @@ -121,7 +121,7 @@ /Zc:threadSafeInit- /Zc:throwingNew /Gw %(AdditionalOptions) false stdcpp20 - false + true true $(ProjectDir)external\include;%(AdditionalIncludeDirectories) @@ -133,8 +133,8 @@ .\Release\Syringe.exe .\Release\Syringe.pdb Windows - false - + true + true MachineX86 true true diff --git a/Handle.h b/Handle.h index d98fed5..ca476e6 100644 --- a/Handle.h +++ b/Handle.h @@ -176,7 +176,7 @@ struct VirtualMemoryHandle { if (process && size) { - this->Value = VirtualAllocEx(process, address, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + this->Value = VirtualAllocEx(process, address, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); } } @@ -217,6 +217,19 @@ struct VirtualMemoryHandle return static_cast(this->Value); } + bool protect(SIZE_T size, DWORD protection) const noexcept + { + DWORD oldProtection; + return this->Value && this->Process + && VirtualProtectEx(this->Process, this->Value, size, protection, &oldProtection) != FALSE; + } + + bool flush_instruction_cache(SIZE_T size) const noexcept + { + return this->Value && this->Process + && FlushInstructionCache(this->Process, this->Value, size) != FALSE; + } + void clear() noexcept { VirtualMemoryHandle(std::move(*this)); diff --git a/SyringeDebugger.cpp b/SyringeDebugger.cpp index 72d70bc..e2ade7b 100644 --- a/SyringeDebugger.cpp +++ b/SyringeDebugger.cpp @@ -57,6 +57,15 @@ VirtualMemoryHandle SyringeDebugger::AllocMem(void* address, size_t size) throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); } +void SyringeDebugger::MakeExecutable(VirtualMemoryHandle const& memory, size_t size) +{ + if (!memory.protect(size, PAGE_EXECUTE_READ) + || !memory.flush_instruction_cache(size)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } +} + bool SyringeDebugger::SetBP(void* address) { // save overwritten code and set INT 3 @@ -610,7 +619,11 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) p_code += sizeof(jmp_back); auto const actual_sz = static_cast(p_code - code.data()); - PatchMem(base, code.data(), actual_sz); + if (!PatchMem(base, code.data(), actual_sz)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } + MakeExecutable(it.second.p_caller_code, sz); // dump /* @@ -831,7 +844,11 @@ void SyringeDebugger::Run(std::string_view const arguments) ApplyPatch(data.data() + 0x13, &GetData()->ProcName); ApplyPatch(data.data() + 0x1A, pImGetProcAddress); ApplyPatch(data.data() + 0x1F, &GetData()->ProcAddress); - PatchMem(pAlloc, data.data(), data.size()); + if (!PatchMem(pAlloc, data.data(), data.size())) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } + MakeExecutable(pAlloc, AllocData::CodeSize); Log::WriteLine(__FUNCTION__ ": pcLoadLibrary = 0x%08X", &GetData()->LoadLibraryFunc); diff --git a/SyringeDebugger.h b/SyringeDebugger.h index a56eb65..143e306 100644 --- a/SyringeDebugger.h +++ b/SyringeDebugger.h @@ -6,6 +6,7 @@ #include "PortableExecutable.h" #include "Log.h" +#include #include #include #include @@ -91,6 +92,7 @@ class SyringeDebugger // memory VirtualMemoryHandle AllocMem(void* address, size_t size); + void MakeExecutable(VirtualMemoryHandle const& memory, size_t size); bool PatchMem(void* address, void const* buffer, DWORD size); bool ReadMem(void const* address, void* buffer, DWORD size); @@ -203,13 +205,17 @@ class SyringeDebugger // data addresses struct AllocData { - static constexpr auto CodeSize = 0x40u; + // Keep executable loader code on its own page. The fields below remain + // writable because they are used to exchange data with the debuggee. + static constexpr auto CodeSize = 0x1000u; std::byte LoadLibraryFunc[CodeSize]; void* ProcAddress; char LibName[MaxNameLength]; char ProcName[MaxNameLength]; }; + static_assert(offsetof(AllocData, ProcAddress) == AllocData::CodeSize); + AllocData* GetData() const noexcept { return reinterpret_cast(pAlloc.get()); From c1a92f6549fdb55788e5e9bd9f164730f7d394f3 Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:46:09 +0300 Subject: [PATCH 2/6] Enforce W^X in the debuggee --- SyringeDebugger.cpp | 112 +++++++++++++++++++++++++++++++++++--------- SyringeDebugger.h | 20 ++++---- 2 files changed, 101 insertions(+), 31 deletions(-) diff --git a/SyringeDebugger.cpp b/SyringeDebugger.cpp index e2ade7b..3b81c69 100644 --- a/SyringeDebugger.cpp +++ b/SyringeDebugger.cpp @@ -12,14 +12,81 @@ #include #include #include +#include #include using namespace std; +namespace +{ +class ProcThreadAttributeList +{ +public: + explicit ProcThreadAttributeList(DWORD const attributeCount) + { + SIZE_T size = 0; + InitializeProcThreadAttributeList(nullptr, attributeCount, 0, &size); + if (size == 0) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, "process attribute list"); + } + + storage.resize(size); + auto const candidate = reinterpret_cast(storage.data()); + if (!InitializeProcThreadAttributeList(candidate, attributeCount, 0, &size)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, "process attribute list"); + } + + value = candidate; + } + + ProcThreadAttributeList(ProcThreadAttributeList const&) = delete; + ProcThreadAttributeList& operator=(ProcThreadAttributeList const&) = delete; + + ~ProcThreadAttributeList() + { + if (value) + { + DeleteProcThreadAttributeList(value); + } + } + + LPPROC_THREAD_ATTRIBUTE_LIST get() const noexcept + { + return value; + } + +private: + std::vector storage; + LPPROC_THREAD_ATTRIBUTE_LIST value{ nullptr }; +}; +} + void SyringeDebugger::DebugProcess(std::string_view const arguments) { - STARTUPINFO startupInfo{ sizeof(startupInfo) }; + STARTUPINFOEX startupInfo{}; + startupInfo.StartupInfo.cb = sizeof(startupInfo); + + ProcThreadAttributeList attributeList{ 1 }; + startupInfo.lpAttributeList = attributeList.get(); + + DWORD64 mitigationPolicy = + PROCESS_CREATION_MITIGATION_POLICY_DEP_ENABLE | + PROCESS_CREATION_MITIGATION_POLICY_DEP_ATL_THUNK_ENABLE; + + if (!UpdateProcThreadAttribute( + startupInfo.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, + &mitigationPolicy, + sizeof(mitigationPolicy), + nullptr, + nullptr)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } SetEnvironmentVariable("_NO_DEBUG_HEAP", "1"); @@ -28,8 +95,8 @@ void SyringeDebugger::DebugProcess(std::string_view const arguments) if (CreateProcess( exe.c_str(), command_line.data(), nullptr, nullptr, false, - DEBUG_ONLY_THIS_PROCESS | CREATE_SUSPENDED, - nullptr, nullptr, &startupInfo, &pInfo) == FALSE) + DEBUG_ONLY_THIS_PROCESS | CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT, + nullptr, nullptr, &startupInfo.StartupInfo, &pInfo) == FALSE) { throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); } @@ -396,7 +463,7 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) PatchMem(&GetData()->LibName, hook->lib, MaxNameLength); PatchMem(&GetData()->ProcName, hook->proc, MaxNameLength); - context.Eip = reinterpret_cast(&GetData()->LoadLibraryFunc); + context.Eip = reinterpret_cast(GetLoaderCode()); } else { @@ -411,7 +478,7 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) PatchMem(&GetData()->LibName, entry.lib, MaxNameLength); PatchMem(&GetData()->ProcName, entry.symbol, MaxNameLength); - context.Eip = reinterpret_cast(&GetData()->LoadLibraryFunc); + context.Eip = reinterpret_cast(GetLoaderCode()); } else { @@ -463,7 +530,7 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) PatchMem(&GetData()->LibName, entry.lib, MaxNameLength); PatchMem(&GetData()->ProcName, entry.symbol, MaxNameLength); - context.Eip = reinterpret_cast(&GetData()->LoadLibraryFunc); + context.Eip = reinterpret_cast(GetLoaderCode()); } else { @@ -803,17 +870,18 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) void SyringeDebugger::Run(std::string_view const arguments) { - constexpr auto AllocDataSize = sizeof(AllocData); - Log::WriteLine( __FUNCTION__ ": Running process to debug. cmd = \"%s %.*s\"", exe.c_str(), printable(arguments)); DebugProcess(arguments); - Log::WriteLine(__FUNCTION__ ": Allocating 0x%u bytes...", AllocDataSize); - pAlloc = AllocMem(nullptr, AllocDataSize); + Log::WriteLine(__FUNCTION__ ": Allocating 0x%u bytes for loader code...", LoaderCodeSize); + pLoaderCode = AllocMem(nullptr, LoaderCodeSize); + Log::WriteLine(__FUNCTION__ ": pLoaderCode = 0x%08X", pLoaderCode.get()); - Log::WriteLine(__FUNCTION__ ": pAlloc = 0x%08X", pAlloc.get()); + Log::WriteLine(__FUNCTION__ ": Allocating 0x%u bytes for exchange data...", sizeof(ExchangeData)); + pExchangeData = AllocMem(nullptr, sizeof(ExchangeData)); + Log::WriteLine(__FUNCTION__ ": pExchangeData = 0x%08X", pExchangeData.get()); // write DLL loader code Log::WriteLine(__FUNCTION__ ": Writing DLL loader & caller code..."); @@ -836,21 +904,21 @@ void SyringeDebugger::Run(std::string_view const arguments) INT3, NOP // int3 and some padding }; - std::array data; - static_assert(AllocData::CodeSize >= sizeof(cLoadLibrary)); - ApplyPatch(data.data(), cLoadLibrary); - ApplyPatch(data.data() + 0x04, &GetData()->LibName); - ApplyPatch(data.data() + 0x0A, pImLoadLibrary); - ApplyPatch(data.data() + 0x13, &GetData()->ProcName); - ApplyPatch(data.data() + 0x1A, pImGetProcAddress); - ApplyPatch(data.data() + 0x1F, &GetData()->ProcAddress); - if (!PatchMem(pAlloc, data.data(), data.size())) + std::array code{}; + static_assert(LoaderCodeSize >= sizeof(cLoadLibrary)); + ApplyPatch(code.data(), cLoadLibrary); + ApplyPatch(code.data() + 0x04, &GetData()->LibName); + ApplyPatch(code.data() + 0x0A, pImLoadLibrary); + ApplyPatch(code.data() + 0x13, &GetData()->ProcName); + ApplyPatch(code.data() + 0x1A, pImGetProcAddress); + ApplyPatch(code.data() + 0x1F, &GetData()->ProcAddress); + if (!PatchMem(pLoaderCode, code.data(), code.size())) { throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); } - MakeExecutable(pAlloc, AllocData::CodeSize); + MakeExecutable(pLoaderCode, code.size()); - Log::WriteLine(__FUNCTION__ ": pcLoadLibrary = 0x%08X", &GetData()->LoadLibraryFunc); + Log::WriteLine(__FUNCTION__ ": pcLoadLibrary = 0x%08X", GetLoaderCode()); // breakpoints for DLL loading and proc address retrieving bDLLsLoaded = false; diff --git a/SyringeDebugger.h b/SyringeDebugger.h index 143e306..69efa33 100644 --- a/SyringeDebugger.h +++ b/SyringeDebugger.h @@ -188,7 +188,8 @@ class SyringeDebugger void* pcEntryPoint{ nullptr }; void* pImLoadLibrary{ nullptr }; void* pImGetProcAddress{ nullptr }; - VirtualMemoryHandle pAlloc; + VirtualMemoryHandle pLoaderCode; + VirtualMemoryHandle pExchangeData; DWORD dwTimeStamp{ 0u }; DWORD dwExeSize{ 0u }; DWORD dwExeCRC{ 0u }; @@ -203,22 +204,23 @@ class SyringeDebugger bool bAVLogged{ false }; // data addresses - struct AllocData + static constexpr auto LoaderCodeSize = 0x1000u; + + struct ExchangeData { - // Keep executable loader code on its own page. The fields below remain - // writable because they are used to exchange data with the debuggee. - static constexpr auto CodeSize = 0x1000u; - std::byte LoadLibraryFunc[CodeSize]; void* ProcAddress; char LibName[MaxNameLength]; char ProcName[MaxNameLength]; }; - static_assert(offsetof(AllocData, ProcAddress) == AllocData::CodeSize); + ExchangeData* GetData() const noexcept + { + return reinterpret_cast(pExchangeData.get()); + }; - AllocData* GetData() const noexcept + BYTE* GetLoaderCode() const noexcept { - return reinterpret_cast(pAlloc.get()); + return pLoaderCode.get(); }; struct HookBuffer From c301bf389e59d0f5300ef921902364084c92c37b Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:46:09 +0300 Subject: [PATCH 3/6] Eliminate build warnings across configurations --- Syringe.sln | 12 ++++++------ SyringeDebugger.cpp | 1 - SyringeDebugger.h | 14 ++++++++------ Tests.vcxproj | 46 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/Syringe.sln b/Syringe.sln index 9fea6de..6904527 100644 --- a/Syringe.sln +++ b/Syringe.sln @@ -29,12 +29,12 @@ Global {DC2E7848-31D1-43EA-90D5-A5F1FB28E8AC}.Release|Mixed Platforms.Build.0 = Release|Win32 {DC2E7848-31D1-43EA-90D5-A5F1FB28E8AC}.Release|Win32.ActiveCfg = Release|Win32 {DC2E7848-31D1-43EA-90D5-A5F1FB28E8AC}.Release|Win32.Build.0 = Release|Win32 - {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Any CPU.ActiveCfg = Release|Win32 - {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Any CPU.Build.0 = Release|Win32 - {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Mixed Platforms.ActiveCfg = Release|Win32 - {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Mixed Platforms.Build.0 = Release|Win32 - {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Win32.ActiveCfg = Release|Win32 - {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Win32.Build.0 = Release|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Any CPU.Build.0 = Debug|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Win32.ActiveCfg = Debug|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Win32.Build.0 = Debug|Win32 {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|Any CPU.ActiveCfg = Release|Win32 {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|Mixed Platforms.ActiveCfg = Release|Win32 {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|Mixed Platforms.Build.0 = Release|Win32 diff --git a/SyringeDebugger.cpp b/SyringeDebugger.cpp index 3b81c69..44ed29f 100644 --- a/SyringeDebugger.cpp +++ b/SyringeDebugger.cpp @@ -865,7 +865,6 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) return DBG_EXCEPTION_NOT_HANDLED; } - return DBG_CONTINUE; } void SyringeDebugger::Run(std::string_view const arguments) diff --git a/SyringeDebugger.h b/SyringeDebugger.h index 69efa33..dbe61be 100644 --- a/SyringeDebugger.h +++ b/SyringeDebugger.h @@ -48,23 +48,25 @@ class SyringeDebugger std::string_view const flagView = flag; // parse all -i=filename_to_inject from flags - if (auto const pos = flagView.find(INCLUDE_FLAG); pos != std::string_view::npos) + if (auto const includePos = flagView.find(INCLUDE_FLAG); + includePos != std::string_view::npos) { - dlls.emplace_back(flagView.begin() + pos + INCLUDE_FLAG.size(), flagView.end()); + dlls.emplace_back( + flagView.begin() + includePos + INCLUDE_FLAG.size(), flagView.end()); } - else if (auto const pos = flagView.find(DETACH_FLAG); pos != std::string_view::npos) + else if (flagView.find(DETACH_FLAG) != std::string_view::npos) { bDetachWhenDone = true; } - else if (auto const pos = flagView.find(NODETACH_FLAG); pos != std::string_view::npos) + else if (flagView.find(NODETACH_FLAG) != std::string_view::npos) { bDetachWhenDone = false; } - else if (auto const pos = flagView.find(NOWAIT_FLAG); pos != std::string_view::npos) + else if (flagView.find(NOWAIT_FLAG) != std::string_view::npos) { bWaitForProcessEnd = false; } - else if (auto const pos = flagView.find(HANDSHAKES_FLAG); pos != std::string_view::npos) + else if (flagView.find(HANDSHAKES_FLAG) != std::string_view::npos) { bHandshakes = true; } diff --git a/Tests.vcxproj b/Tests.vcxproj index fd8f7e2..798aabd 100644 --- a/Tests.vcxproj +++ b/Tests.vcxproj @@ -1,6 +1,10 @@ + + Debug + Win32 + Release Win32 @@ -12,6 +16,12 @@ Tests + + Application + v143 + false + MultiByte + Application v143 @@ -22,15 +32,51 @@ + + + + + $(SolutionDir)tests\bin\Debug\ + $(SolutionDir)tests\obj\Debug\ + false + $(SolutionDir)tests\bin\ $(SolutionDir)tests\obj\ false + + + Disabled + WIN32;_DEBUG;_CONSOLE;NOMINMAX;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;SYRINGE_TESTING;%(PreprocessorDefinitions) + MultiThreadedDebug + Level4 + StreamingSIMDExtensions + /Zc:threadSafeInit- /Zc:throwingNew /Gw %(AdditionalOptions) + true + false + stdcpp20 + false + true + $(SolutionDir);$(SolutionDir)external\include;%(AdditionalIncludeDirectories) + $(IntDir)\%(RelativeDir) + $(IntDir)\%(RelativeDir) + + + $(OutDir)Tests.exe + Console + false + MachineX86 + false + false + kernel32.lib;user32.lib;gdi32.lib;advapi32.lib;shell32.lib;dbghelp.lib;%(AdditionalDependencies) + true + + Full From f6172b17015c4f714ac4032957b808b2146a4580 Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:23:37 +0300 Subject: [PATCH 4/6] Harden runtime code patching and drop Vista support --- Syringe.exe.manifest | 2 -- SyringeDebugger.cpp | 62 +++++++++++++++++++++++++++++++++++++------- SyringeDebugger.h | 1 + 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/Syringe.exe.manifest b/Syringe.exe.manifest index 64afe81..70e7fd3 100644 --- a/Syringe.exe.manifest +++ b/Syringe.exe.manifest @@ -31,8 +31,6 @@ - - diff --git a/SyringeDebugger.cpp b/SyringeDebugger.cpp index 44ed29f..bbdb256 100644 --- a/SyringeDebugger.cpp +++ b/SyringeDebugger.cpp @@ -20,6 +20,12 @@ using namespace std; namespace { +bool IsRunningUnderWine() +{ + auto const ntdll = GetModuleHandleW(L"ntdll.dll"); + return ntdll && GetProcAddress(ntdll, "wine_get_version"); +} + class ProcThreadAttributeList { public: @@ -102,6 +108,14 @@ void SyringeDebugger::DebugProcess(std::string_view const arguments) } workingHandle = pInfo.hProcess; + + if (IsRunningUnderWine()) + { + Log::WriteLine( + __FUNCTION__ ": Warning: Wine does not currently apply the child-process " + "DEP mitigation attribute. W^X therefore depends on the target executable " + "being linked with NX_COMPAT."); + } } bool SyringeDebugger::PatchMem(void* address, void const* buffer, DWORD size) @@ -109,6 +123,12 @@ bool SyringeDebugger::PatchMem(void* address, void const* buffer, DWORD size) return (WriteProcessMemory(workingHandle, address, buffer, size, nullptr) != FALSE); } +bool SyringeDebugger::PatchCode(void* address, void const* buffer, DWORD size) +{ + return PatchMem(address, buffer, size) + && FlushInstructionCache(workingHandle, address, size) != FALSE; +} + bool SyringeDebugger::ReadMem(void const* address, void* buffer, DWORD size) { return (ReadProcessMemory(workingHandle, address, buffer, size, nullptr) != FALSE); @@ -140,7 +160,7 @@ bool SyringeDebugger::SetBP(void* address) { auto const buffer = INT3; ReadMem(address, &opcode, 1); - return PatchMem(address, &buffer, 1); + return PatchCode(address, &buffer, 1); } return true; @@ -428,14 +448,20 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) { auto const buffer = INT3; context.EFlags &= ~0x100; - PatchMem(threadInfo.lastBP, &buffer, 1); + if (!PatchCode(threadInfo.lastBP, &buffer, 1)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } } // load DLLs and retrieve proc addresses if (!bDLLsLoaded) { // restore - PatchMem(exceptAddr, &Breakpoints[exceptAddr].original_opcode, 1); + if (!PatchCode(exceptAddr, &Breakpoints[exceptAddr].original_opcode, 1)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } if (loop_LoadLibrary == v_AllHooks.end()) { @@ -501,7 +527,10 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) if (!bFeaturesSet) { // restore - PatchMem(exceptAddr, &Breakpoints[exceptAddr].original_opcode, 1); + if (!PatchCode(exceptAddr, &Breakpoints[exceptAddr].original_opcode, 1)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } // read the resolved address of the feature flag in the target process void* flagAddr = nullptr; @@ -717,7 +746,10 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) ApplyPatch(code.data(), jmp); ApplyPatch(code.data() + 0x01, rel2); - PatchMem(p_original_code, code.data(), code.size()); + if (!PatchCode(p_original_code, code.data(), static_cast(code.size()))) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } } Log::Flush(); @@ -726,7 +758,10 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) } // restore - PatchMem(exceptAddr, &Breakpoints[exceptAddr].original_opcode, 1); + if (!PatchCode(exceptAddr, &Breakpoints[exceptAddr].original_opcode, 1)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } // single step mode context.EFlags |= 0x100; @@ -753,7 +788,10 @@ DWORD SyringeDebugger::HandleException(DEBUG_EVENT const& dbgEvent) { auto const buffer = INT3; auto const& threadInfo = Threads[dbgEvent.dwThreadId]; - PatchMem(threadInfo.lastBP, &buffer, 1); + if (!PatchCode(threadInfo.lastBP, &buffer, 1)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } HANDLE hThread = threadInfo.Thread; CONTEXT context; @@ -926,7 +964,10 @@ void SyringeDebugger::Run(std::string_view const arguments) loop_LoadLibrary = v_AllHooks.end(); // set breakpoint - SetBP(pcEntryPoint); + if (!SetBP(pcEntryPoint)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } DEBUG_EVENT dbgEvent; ResumeThread(pInfo.hThread); @@ -1029,7 +1070,10 @@ void SyringeDebugger::RemoveBP(LPVOID const address, bool const restoreOpcode) { if (restoreOpcode) { - PatchMem(address, &i->second.original_opcode, 1); + if (!PatchCode(address, &i->second.original_opcode, 1)) + { + throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); + } } Breakpoints.erase(i); diff --git a/SyringeDebugger.h b/SyringeDebugger.h index dbe61be..9534d49 100644 --- a/SyringeDebugger.h +++ b/SyringeDebugger.h @@ -96,6 +96,7 @@ class SyringeDebugger VirtualMemoryHandle AllocMem(void* address, size_t size); void MakeExecutable(VirtualMemoryHandle const& memory, size_t size); bool PatchMem(void* address, void const* buffer, DWORD size); + bool PatchCode(void* address, void const* buffer, DWORD size); bool ReadMem(void const* address, void* buffer, DWORD size); // syringe From 70d6a8b48ce832ebbd3bd75e845cd30f779f31b5 Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Mon, 24 Aug 2026 02:18:54 +0300 Subject: [PATCH 5/6] Limit DEP hardening to Syringe --- Syringe.exe.manifest | 2 + SyringeDebugger.cpp | 87 ++------------------------------------------ 2 files changed, 6 insertions(+), 83 deletions(-) diff --git a/Syringe.exe.manifest b/Syringe.exe.manifest index 70e7fd3..64afe81 100644 --- a/Syringe.exe.manifest +++ b/Syringe.exe.manifest @@ -31,6 +31,8 @@ + + diff --git a/SyringeDebugger.cpp b/SyringeDebugger.cpp index bbdb256..4962e51 100644 --- a/SyringeDebugger.cpp +++ b/SyringeDebugger.cpp @@ -18,81 +18,10 @@ using namespace std; -namespace -{ -bool IsRunningUnderWine() -{ - auto const ntdll = GetModuleHandleW(L"ntdll.dll"); - return ntdll && GetProcAddress(ntdll, "wine_get_version"); -} - -class ProcThreadAttributeList -{ -public: - explicit ProcThreadAttributeList(DWORD const attributeCount) - { - SIZE_T size = 0; - InitializeProcThreadAttributeList(nullptr, attributeCount, 0, &size); - if (size == 0) - { - throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, "process attribute list"); - } - - storage.resize(size); - auto const candidate = reinterpret_cast(storage.data()); - if (!InitializeProcThreadAttributeList(candidate, attributeCount, 0, &size)) - { - throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, "process attribute list"); - } - - value = candidate; - } - - ProcThreadAttributeList(ProcThreadAttributeList const&) = delete; - ProcThreadAttributeList& operator=(ProcThreadAttributeList const&) = delete; - - ~ProcThreadAttributeList() - { - if (value) - { - DeleteProcThreadAttributeList(value); - } - } - - LPPROC_THREAD_ATTRIBUTE_LIST get() const noexcept - { - return value; - } - -private: - std::vector storage; - LPPROC_THREAD_ATTRIBUTE_LIST value{ nullptr }; -}; -} - void SyringeDebugger::DebugProcess(std::string_view const arguments) { - STARTUPINFOEX startupInfo{}; - startupInfo.StartupInfo.cb = sizeof(startupInfo); - - ProcThreadAttributeList attributeList{ 1 }; - startupInfo.lpAttributeList = attributeList.get(); - - DWORD64 mitigationPolicy = - PROCESS_CREATION_MITIGATION_POLICY_DEP_ENABLE | - PROCESS_CREATION_MITIGATION_POLICY_DEP_ATL_THUNK_ENABLE; - - if (!UpdateProcThreadAttribute( - startupInfo.lpAttributeList, - 0, - PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, - &mitigationPolicy, - sizeof(mitigationPolicy), - nullptr, - nullptr)) - { - throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); - } + STARTUPINFO startupInfo{}; + startupInfo.cb = sizeof(startupInfo); SetEnvironmentVariable("_NO_DEBUG_HEAP", "1"); @@ -101,21 +30,13 @@ void SyringeDebugger::DebugProcess(std::string_view const arguments) if (CreateProcess( exe.c_str(), command_line.data(), nullptr, nullptr, false, - DEBUG_ONLY_THIS_PROCESS | CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT, - nullptr, nullptr, &startupInfo.StartupInfo, &pInfo) == FALSE) + DEBUG_ONLY_THIS_PROCESS | CREATE_SUSPENDED, + nullptr, nullptr, &startupInfo, &pInfo) == FALSE) { throw_lasterror_or(ERROR_ERRORS_ENCOUNTERED, exe); } workingHandle = pInfo.hProcess; - - if (IsRunningUnderWine()) - { - Log::WriteLine( - __FUNCTION__ ": Warning: Wine does not currently apply the child-process " - "DEP mitigation attribute. W^X therefore depends on the target executable " - "being linked with NX_COMPAT."); - } } bool SyringeDebugger::PatchMem(void* address, void const* buffer, DWORD size) From aefdc5b63075e169950d9288fed5fbf7067fe0a5 Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:32:17 +0300 Subject: [PATCH 6/6] Derive loader allocation size from generated code --- SyringeDebugger.cpp | 17 +++++------------ SyringeDebugger.h | 3 --- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/SyringeDebugger.cpp b/SyringeDebugger.cpp index 4962e51..7348cbb 100644 --- a/SyringeDebugger.cpp +++ b/SyringeDebugger.cpp @@ -833,18 +833,10 @@ void SyringeDebugger::Run(std::string_view const arguments) exe.c_str(), printable(arguments)); DebugProcess(arguments); - Log::WriteLine(__FUNCTION__ ": Allocating 0x%u bytes for loader code...", LoaderCodeSize); - pLoaderCode = AllocMem(nullptr, LoaderCodeSize); - Log::WriteLine(__FUNCTION__ ": pLoaderCode = 0x%08X", pLoaderCode.get()); - - Log::WriteLine(__FUNCTION__ ": Allocating 0x%u bytes for exchange data...", sizeof(ExchangeData)); - pExchangeData = AllocMem(nullptr, sizeof(ExchangeData)); - Log::WriteLine(__FUNCTION__ ": pExchangeData = 0x%08X", pExchangeData.get()); - // write DLL loader code Log::WriteLine(__FUNCTION__ ": Writing DLL loader & caller code..."); - static BYTE const cLoadLibrary[] = { + static constexpr BYTE cLoadLibrary[] = { 0x50, // push eax 0x51, // push ecx 0x52, // push edx @@ -862,9 +854,10 @@ void SyringeDebugger::Run(std::string_view const arguments) INT3, NOP // int3 and some padding }; - std::array code{}; - static_assert(LoaderCodeSize >= sizeof(cLoadLibrary)); - ApplyPatch(code.data(), cLoadLibrary); + auto code = std::to_array(cLoadLibrary); + pLoaderCode = AllocMem(nullptr, code.size()); + pExchangeData = AllocMem(nullptr, sizeof(ExchangeData)); + ApplyPatch(code.data() + 0x04, &GetData()->LibName); ApplyPatch(code.data() + 0x0A, pImLoadLibrary); ApplyPatch(code.data() + 0x13, &GetData()->ProcName); diff --git a/SyringeDebugger.h b/SyringeDebugger.h index 9534d49..e7e603e 100644 --- a/SyringeDebugger.h +++ b/SyringeDebugger.h @@ -206,9 +206,6 @@ class SyringeDebugger bool bAVLogged{ false }; - // data addresses - static constexpr auto LoaderCodeSize = 0x1000u; - struct ExchangeData { void* ProcAddress;