From 4231df0d558f01a4fd0602adb2afca41a3f0e7b9 Mon Sep 17 00:00:00 2001 From: Orange Date: Sat, 2 May 2026 22:21:25 +0300 Subject: [PATCH 1/2] added .exe support --- CMakeLists.txt | 12 ++ README.md | 109 +++++++++---- examples/loader.cpp | 4 +- examples/test_exe.cpp | 325 +++++++++++++++++++++++++++++++++++++++ examples/test_winexe.cpp | 153 ++++++++++++++++++ include/yail/yail.hpp | 14 +- source/yail.cpp | 18 ++- 7 files changed, 599 insertions(+), 36 deletions(-) create mode 100644 examples/test_exe.cpp create mode 100644 examples/test_winexe.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 16b6773..bc6e041 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,18 @@ if (YAIL_BUILD_EXAMPLES) set_target_properties(test_dll PROPERTIES CXX_STANDARD 23) target_link_libraries(test_dll PRIVATE yail dbghelp winmm delayimp) target_link_options(test_dll PRIVATE /DELAYLOAD:dbghelp.dll /DELAYLOAD:winmm.dll) + + add_executable(test_exe examples/test_exe.cpp) + set_target_properties(test_exe PROPERTIES CXX_STANDARD 23) + target_link_libraries(test_exe PRIVATE dbghelp winmm delayimp) + target_link_options(test_exe PRIVATE /DELAYLOAD:dbghelp.dll /DELAYLOAD:winmm.dll) + + add_executable(test_winexe examples/test_winexe.cpp) + set_target_properties(test_winexe PROPERTIES CXX_STANDARD 23) + target_link_libraries(test_winexe PRIVATE dbghelp delayimp) + # /SUBSYSTEM:WINDOWS makes the linker pick WinMainCRTStartup as the entry, + # which the CRT uses to dispatch to WinMain (instead of main). + target_link_options(test_winexe PRIVATE /SUBSYSTEM:WINDOWS /DELAYLOAD:dbghelp.dll) endif() diff --git a/README.md b/README.md index 58f1105..79e21cb 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,53 @@ # yail -**Yet Another Injection Library** — a Windows x64 manual-map DLL injection library written in modern C++23. +**Yet Another Injection Library** — a Windows manual-map PE injection library written in modern C++23. Supports both **x64** and **x86**, and can map both **DLLs** and **EXEs** into a target process. ## Features -- x64 PE manual mapping (no `LoadLibrary` traces) -- Static TLS support -- Exception handling (SEH/VEH compatible) -- Heap validation compatibility with UCRT +- Manual PE mapping (no `LoadLibrary` traces) + - **x64** — full unwind table registration via `RtlInsertInvertedFunctionTable` (with `RtlAddFunctionTable` fallback) + - **x86** — SEH validation via `RtlInsertInvertedFunctionTable` (handles modern Win11 24H2 internal `__fastcall` convention) +- Maps both **DLLs** and **EXEs** — auto-detected via `IMAGE_FILE_DLL` + - DLLs invoked as `DllMain(HMODULE, DLL_PROCESS_ATTACH, nullptr)` + - EXEs invoked as `int __cdecl mainCRTStartup(void)` — works with both `main`-style (console subsystem) and `WinMain`-style (GUI subsystem) entries +- Static TLS via signature-scanned `LdrpHandleTlsData` +- TLS callbacks (`.CRT$XLB`) +- Static and delay-loaded imports +- Exception handling (SEH/VEH/C++) compatible with manually-mapped images +- Per-section memory protections (RX, RW, RO, RWX as declared) - Inject by process ID or process name - Load from file path or raw bytes in memory - Returns `std::expected` — no exceptions, clear error messages ## Requirements -- Windows x64 +- Windows 10 / 11 (signature scans target Windows 11 24H2 ntdll by default; older builds may need pattern updates) - C++23 compiler (MSVC recommended) - CMake 3.28+ - vcpkg ## Building +x64: + ```bash -cmake --preset windows-debug -cmake --build cmake-build/build/windows-debug +cmake --preset windows-debug-vcpkg +cmake --build cmake-build/build/windows-debug-vcpkg ``` -To also build the examples: +x86: ```bash -cmake --preset windows-debug -DYAIL_BUILD_EXAMPLES=ON -cmake --build cmake-build/build/windows-debug +cmake --preset windows-debug-vcpkg-x86 +cmake --build cmake-build/build/windows-debug-vcpkg-x86 ``` +The injector and the target image must share bitness — an x86 build of yail injects x86 PEs into x86 (Wow64) processes, an x64 build injects x64 PEs into x64 processes. + +Examples build by default. Disable with `-DYAIL_BUILD_EXAMPLES=OFF`. + ## Usage -### Inject into a process by name +### Inject a DLL into a process by name ```cpp #include @@ -47,26 +60,29 @@ else std::println("Loaded at 0x{:x}", result.value()); ``` -### Inject into a process by PID +### Inject by PID ```cpp auto result = yail::manual_map_injection_from_file("my.dll", GetCurrentProcessId()); ``` -### Inject from raw bytes +### Inject an EXE + +Same API — auto-detection picks the right entry-point shape: ```cpp -std::vector dll_bytes = /* ... */; -auto result = yail::manual_map_injection_from_raw(dll_bytes, "target.exe"); +auto result = yail::manual_map_injection_from_file("my.exe", GetCurrentProcessId()); ``` -## CMake Integration +EXE caveats (apply to both `main` and `WinMain` flavors): +- When the EXE's entry returns, the CRT calls `exit()` → `ExitProcess`. That terminates the **host** process. If you need the host to survive, the injected EXE must avoid letting `main`/`WinMain` return — e.g. `ExitThread(0)` from the entry, like the bundled `test_exe`. +- `GetModuleHandle(nullptr)` inside the injected EXE returns the **host** image base, not the mapped one. `WinMain`'s `hInstance` is correct (it comes from `__ImageBase`, which is relocated), but APIs that read `PEB->ImageBaseAddress` are not. -After installing, consume yail in your project: +### Inject from raw bytes -```cmake -find_package(yail CONFIG REQUIRED) -target_link_libraries(my_target PRIVATE yail::yail) +```cpp +std::vector bytes = /* ... */; +auto result = yail::manual_map_injection_from_raw(bytes, "target.exe"); ``` ## API @@ -74,24 +90,63 @@ target_link_libraries(my_target PRIVATE yail::yail) ```cpp namespace yail { - // Inject from a file path + // Both functions accept DLLs and EXEs (matched by IMAGE_FILE_DLL). + // PE machine type must match the build (x64 build → AMD64 PE, x86 → I386). + std::expected - manual_map_injection_from_file(std::string_view dll_path, std::uintptr_t process_id); + manual_map_injection_from_file(std::string_view pe_path, std::uintptr_t process_id); std::expected - manual_map_injection_from_file(std::string_view dll_path, std::string_view process_name); + manual_map_injection_from_file(std::string_view pe_path, std::string_view process_name); - // Inject from raw bytes std::expected - manual_map_injection_from_raw(const std::span& raw_dll, std::uintptr_t process_id); + manual_map_injection_from_raw(const std::span& raw_pe, std::uintptr_t process_id); std::expected - manual_map_injection_from_raw(const std::span& raw_dll, std::string_view process_name); + manual_map_injection_from_raw(const std::span& raw_pe, std::string_view process_name); } ``` On success, returns the base address of the mapped image in the target process. On failure, returns a string describing the error. +## CMake Integration + +```cmake +find_package(yail CONFIG REQUIRED) +target_link_libraries(my_target PRIVATE yail::yail) +``` + +## Examples + +The `examples/` directory contains: + +| Target | Purpose | +|-----------------|----------------------------------------------------------------------------------------| +| `loader` | Manual-maps a PE (DLL or EXE) into the current process. `loader.exe `. | +| `remote_loader` | Manual-maps into a target process by name. `remote_loader.exe `. | +| `test_dll` | Self-test DLL exercising TLS, SEH, C++ exceptions, delay imports, threading, vtables. | +| `test_exe` | Same battery of tests, but as a console-subsystem EXE entered via `main()`. | +| `test_winexe` | GUI-subsystem EXE entered via `WinMain` — verifies `hInstance`, `lpCmdLine`, `nShowCmd`. | + +Quick verification on either bitness: + +```bash +loader.exe test_dll.dll # 22 tests +loader.exe test_exe.exe # 16 tests + ExitThread keeps the loader alive +loader.exe test_winexe.exe # WinMain path + GUI subsystem checks +``` + +## Signature notes + +The library locates two non-exported ntdll routines by byte signatures: + +- `LdrpHandleTlsData` — used to register static TLS for the mapped image +- `RtlInsertInvertedFunctionTable` — used to make the image's exception/SEH handlers visible to the OS exception dispatcher + +Patterns are versioned per architecture and have been verified on **Windows 11 24H2**. Older Windows builds may require updated signatures — locate the function in WinDbg (`x ntdll!LdrpHandleTlsData`, `uf `), take ~16 unique leading bytes, and add the wildcarded pattern to the corresponding `find_*` array in `source/yail.cpp`. + +On modern x86 ntdll, both functions use `__fastcall` (args in `ECX`/`EDX`) despite their legacy `_Name@N` symbol decoration — the typedef and call sites in the source reflect that. If you target an older x86 Windows where these are still `__stdcall`, you'll need to swap the typedef to `NTAPI*`. + ## License [Zlib](LICENSE) diff --git a/examples/loader.cpp b/examples/loader.cpp index 592add1..1304475 100644 --- a/examples/loader.cpp +++ b/examples/loader.cpp @@ -4,8 +4,8 @@ #include #include int main(int argc, char* argv[]) -{ - std::string dllPath = "test_dll.dll"; + + std::string dllPath = "test_winexe.exe"; if (argc > 1) dllPath = argv[1]; diff --git a/examples/test_exe.cpp b/examples/test_exe.cpp new file mode 100644 index 0000000..5c818d9 --- /dev/null +++ b/examples/test_exe.cpp @@ -0,0 +1,325 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int g_passed = 0; +static int g_total = 0; + +static void Report(const char* name, bool ok) +{ + g_total++; + if (ok) g_passed++; + printf(" [%s] %s\n", ok ? "PASS" : "FAIL", name); +} + +// ========================================================================= +// TLS callback — must fire before main() +// ========================================================================= +static volatile bool g_tlsCallbackFired = false; + +static void NTAPI TlsCallback(PVOID, DWORD dwReason, PVOID) +{ + if (dwReason == DLL_PROCESS_ATTACH) + { + g_tlsCallbackFired = true; + printf("[test_exe] TLS callback fired (DLL_PROCESS_ATTACH)\n"); + } +} + +#ifdef _MSC_VER +#ifdef _WIN64 +#pragma comment(linker, "/INCLUDE:_tls_used") +#else +#pragma comment(linker, "/INCLUDE:__tls_used") +#endif +#pragma section(".CRT$XLB", read) +__declspec(allocate(".CRT$XLB")) PIMAGE_TLS_CALLBACK g_pfnTlsCallback = TlsCallback; +#else +__attribute__((section(".CRT$XLB"))) PIMAGE_TLS_CALLBACK g_pfnTlsCallback = TlsCallback; +#endif + +// ========================================================================= +// Static TLS — __declspec(thread) +// ========================================================================= +static __declspec(thread) int g_tlsInt = 42; +static __declspec(thread) const char* g_tlsStr = "hello from TLS"; +static __declspec(thread) double g_tlsDouble = 3.14; + +static bool TestStaticTLS() +{ + if (g_tlsInt != 42) return false; + if (g_tlsDouble != 3.14) return false; + if (strcmp(g_tlsStr, "hello from TLS") != 0) return false; + + g_tlsInt = 100; + g_tlsDouble = 2.718; + return g_tlsInt == 100 && g_tlsDouble == 2.718; +} + +// ========================================================================= +// Static TLS across threads +// ========================================================================= +static bool TestTLSPerThread() +{ + std::atomic ok1{false}; + std::atomic ok2{false}; + g_tlsInt = 1000; + + std::thread t1([&] { g_tlsInt = 111; ok1 = (g_tlsInt == 111); }); + std::thread t2([&] { g_tlsInt = 222; ok2 = (g_tlsInt == 222); }); + t1.join(); + t2.join(); + + return ok1 && ok2 && g_tlsInt == 1000; +} + +// ========================================================================= +// SEH +// ========================================================================= +static bool TestSEH() +{ + bool caught = false; + __try + { + *reinterpret_cast(nullptr) = 0xDEAD; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + caught = true; + } + return caught; +} + +static bool TestSEHDivZero() +{ + bool caught = false; + __try + { + volatile int a = 1, b = 0; + volatile int c = a / b; + (void)c; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + caught = true; + } + return caught; +} + +// ========================================================================= +// C++ exceptions +// ========================================================================= +static bool TestCppExceptionStd() +{ + try + { + throw std::runtime_error("test_exe runtime error"); + } + catch (const std::exception& e) + { + return std::string(e.what()) == "test_exe runtime error"; + } + return false; +} + +static bool g_dtorRan = false; +struct ScopedGuard +{ + bool* flag; + explicit ScopedGuard(bool* f) : flag(f) { *flag = false; } + ~ScopedGuard() { *flag = true; } +}; + +static bool TestCppExceptionUnwind() +{ + g_dtorRan = false; + try + { + ScopedGuard guard(&g_dtorRan); + throw 42; + } + catch (...) {} + return g_dtorRan; +} + +// ========================================================================= +// Global constructors +// ========================================================================= +struct StaticInit +{ + int magic; + StaticInit() : magic(0xC0DEC0DE) {} +}; +static StaticInit g_staticInit; + +static bool TestGlobalCtors() { return g_staticInit.magic == 0xC0DEC0DE; } + +// ========================================================================= +// Win32 imports +// ========================================================================= +static bool TestImports() +{ + SYSTEM_INFO si{}; + GetSystemInfo(&si); + if (si.dwPageSize == 0) return false; + + HANDLE heap = HeapCreate(0, 0, 0); + if (!heap) return false; + void* p = HeapAlloc(heap, HEAP_ZERO_MEMORY, 256); + if (!p) { HeapDestroy(heap); return false; } + HeapFree(heap, 0, p); + HeapDestroy(heap); + + LARGE_INTEGER freq{}; + return QueryPerformanceFrequency(&freq) && freq.QuadPart != 0; +} + +// ========================================================================= +// STL +// ========================================================================= +static bool TestSTL() +{ + std::vector v = {5, 3, 1, 4, 2}; + std::sort(v.begin(), v.end()); + if (v != std::vector{1, 2, 3, 4, 5}) return false; + + std::string s = "Hello"; + s += ", EXE!"; + if (s != "Hello, EXE!") return false; + + auto ptr = std::make_unique(42); + return ptr && *ptr == 42; +} + +// ========================================================================= +// Floating point +// ========================================================================= +static bool TestFloatingPoint() +{ + volatile double a = 2.0; + if (fabs(sqrt(a) - 1.41421356237) > 1e-6) return false; + + volatile float b = 1.0f; + return fabsf(sinf(b) - 0.841471f) <= 1e-4f; +} + +// ========================================================================= +// Threading + mutex +// ========================================================================= +static bool TestThreading() +{ + std::atomic counter{0}; + std::mutex mtx; + constexpr int N = 8; + + std::vector threads; + threads.reserve(N); + for (int i = 0; i < N; i++) + { + threads.emplace_back([&] { + std::lock_guard lock(mtx); + counter++; + }); + } + for (auto& t : threads) t.join(); + return counter == N; +} + +// ========================================================================= +// Vtable dispatch +// ========================================================================= +struct IShape +{ + virtual int sides() = 0; + virtual ~IShape() = default; +}; +struct Triangle final : IShape { int sides() override { return 3; } }; +struct Hexagon final : IShape { int sides() override { return 6; } }; + +static bool TestVTable() +{ + std::unique_ptr a = std::make_unique(); + std::unique_ptr b = std::make_unique(); + return a->sides() == 3 && b->sides() == 6; +} + +// ========================================================================= +// Delay imports +// ========================================================================= +static bool TestDelayImportDbgHelp() +{ + const DWORD original = SymGetOptions(); + SymSetOptions(original | SYMOPT_UNDNAME); + const DWORD updated = SymGetOptions(); + SymSetOptions(original); + return (updated & SYMOPT_UNDNAME) != 0; +} + +static bool TestDelayImportWinmm() +{ + TIMECAPS tc{}; + if (timeGetDevCaps(&tc, sizeof(tc)) != TIMERR_NOERROR) return false; + return tc.wPeriodMin > 0 && tc.wPeriodMin <= tc.wPeriodMax; +} + +// ========================================================================= +// CRT-populated argc/argv — EXE-specific. +// The CRT entry pulls these from GetCommandLineA(), so they reflect the +// host process's command line, not anything we passed. +// ========================================================================= +static bool TestArgcArgv(int argc, char** argv) +{ + return argc > 0 && argv != nullptr && argv[0] != nullptr; +} + +// ========================================================================= +// EXE entry — invoked by CRT (mainCRTStartup) after init +// ========================================================================= +int main(int argc, char** argv) +{ + printf("========================================\n"); + printf("[test_exe] main() reached (argc=%d)\n", argc); + if (argv && argc > 0 && argv[0]) + printf("[test_exe] argv[0] = %s\n", argv[0]); + printf("========================================\n\n"); + + Report("TLS callback fired", g_tlsCallbackFired); + Report("Static TLS read/write", TestStaticTLS()); + Report("Static TLS per-thread", TestTLSPerThread()); + Report("SEH access violation", TestSEH()); + Report("SEH divide by zero", TestSEHDivZero()); + Report("C++ exception std::exception", TestCppExceptionStd()); + Report("C++ exception stack unwind", TestCppExceptionUnwind()); + Report("Global constructors", TestGlobalCtors()); + Report("Win32 API imports", TestImports()); + Report("STL containers/strings", TestSTL()); + Report("Floating point / math", TestFloatingPoint()); + Report("Threading + mutex", TestThreading()); + Report("Vtable dispatch", TestVTable()); + Report("Delay import DbgHelp", TestDelayImportDbgHelp()); + Report("Delay import Winmm", TestDelayImportWinmm()); + Report("argc/argv populated by CRT", TestArgcArgv(argc, argv)); + + printf("\n========================================\n"); + printf("[test_exe] Results: %d/%d passed\n", g_passed, g_total); + printf("========================================\n"); + + MessageBoxA(nullptr, "All EXE tests have completed!", "yail", MB_OK); + + // Returning from main() would invoke the CRT's exit() -> ExitProcess and + // terminate the host loader process. Exit only this thread instead so the + // loader's WaitForSingleObject wakes and the process keeps running. + ExitThread(0); +} diff --git a/examples/test_winexe.cpp b/examples/test_winexe.cpp new file mode 100644 index 0000000..a741814 --- /dev/null +++ b/examples/test_winexe.cpp @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include +#include + +// __ImageBase is a linker-emitted symbol whose address equals the image base. +// After manual mapping + relocations, taking its address gives the mapped base. +extern "C" IMAGE_DOS_HEADER __ImageBase; + +static int g_passed = 0; +static int g_total = 0; + +static void Report(const char* name, bool ok) +{ + g_total++; + if (ok) g_passed++; + printf(" [%s] %s\n", ok ? "PASS" : "FAIL", name); +} + +// ========================================================================= +// TLS callback — must fire before WinMain +// ========================================================================= +static volatile bool g_tlsCallbackFired = false; + +static void NTAPI TlsCallback(PVOID, DWORD dwReason, PVOID) +{ + if (dwReason == DLL_PROCESS_ATTACH) + { + g_tlsCallbackFired = true; + printf("[test_winexe] TLS callback fired (DLL_PROCESS_ATTACH)\n"); + } +} + +#ifdef _MSC_VER +#ifdef _WIN64 +#pragma comment(linker, "/INCLUDE:_tls_used") +#else +#pragma comment(linker, "/INCLUDE:__tls_used") +#endif +#pragma section(".CRT$XLB", read) +__declspec(allocate(".CRT$XLB")) PIMAGE_TLS_CALLBACK g_pfnTlsCallback = TlsCallback; +#endif + +static __declspec(thread) int g_tlsInt = 1234; + +static bool TestStaticTLS() +{ + if (g_tlsInt != 1234) return false; + g_tlsInt = 5678; + return g_tlsInt == 5678; +} + +static bool TestSEH() +{ + bool caught = false; + __try + { + *reinterpret_cast(nullptr) = 0xDEAD; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + caught = true; + } + return caught; +} + +static bool TestCppException() +{ + try { throw std::runtime_error("winmain ex"); } + catch (const std::exception& e) { return std::string(e.what()) == "winmain ex"; } + return false; +} + +struct StaticInit { int v; StaticInit() : v(0xBADF00D) {} }; +static StaticInit g_staticInit; +static bool TestGlobalCtors() { return g_staticInit.v == 0xBADF00D; } + +static bool TestImports() +{ + SYSTEM_INFO si{}; + GetSystemInfo(&si); + return si.dwPageSize > 0; +} + +static bool TestDelayImportDbgHelp() +{ + const DWORD original = SymGetOptions(); + SymSetOptions(original | SYMOPT_UNDNAME); + const DWORD updated = SymGetOptions(); + SymSetOptions(original); + return (updated & SYMOPT_UNDNAME) != 0; +} + +// ========================================================================= +// WinMain-specific checks +// ========================================================================= +static bool TestHInstanceMatchesImageBase(HINSTANCE hInstance) +{ + // The CRT computes hInstance as &__ImageBase. After manual mapping with + // relocations applied, both should resolve to the mapped image base. + return reinterpret_cast(hInstance) == reinterpret_cast(&__ImageBase); +} + +static bool TestLpCmdLine(LPSTR lpCmdLine) +{ + // The CRT pulls the command line from GetCommandLineA(); lpCmdLine is the + // tail (program name stripped). It must at least be a valid C string. + return lpCmdLine != nullptr; +} + +static bool TestNShowCmd(int nShowCmd) +{ + // SW_HIDE..SW_MAX is a small range; any sane value falls within. + return nShowCmd >= 0 && nShowCmd <= 11; +} + +// ========================================================================= +// WinMain — invoked by WinMainCRTStartup +// ========================================================================= +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) +{ + printf("========================================\n"); + printf("[test_winexe] WinMain reached\n"); + printf(" hInstance = %p\n", static_cast(hInstance)); + printf(" &__ImageBase = %p\n", static_cast(&__ImageBase)); + printf(" hPrevInstance = %p (always null in Win32)\n", static_cast(hPrevInstance)); + printf(" lpCmdLine = \"%s\"\n", lpCmdLine ? lpCmdLine : "(null)"); + printf(" nShowCmd = %d\n", nShowCmd); + printf("========================================\n\n"); + + Report("TLS callback fired", g_tlsCallbackFired); + Report("Static TLS", TestStaticTLS()); + Report("SEH access violation", TestSEH()); + Report("C++ exception", TestCppException()); + Report("Global constructors", TestGlobalCtors()); + Report("Win32 imports", TestImports()); + Report("Delay import DbgHelp", TestDelayImportDbgHelp()); + Report("hInstance == &__ImageBase", TestHInstanceMatchesImageBase(hInstance)); + Report("hPrevInstance is nullptr", hPrevInstance == nullptr); + Report("lpCmdLine non-null", TestLpCmdLine(lpCmdLine)); + Report("nShowCmd in valid range", TestNShowCmd(nShowCmd)); + + printf("\n========================================\n"); + printf("[test_winexe] Results: %d/%d passed\n", g_passed, g_total); + printf("========================================\n"); + + MessageBoxA(nullptr, "WinMain EXE tests have completed!", "yail", MB_OK); + + // ExitThread to keep the host loader process alive — see test_exe.cpp. + ExitThread(0); +} diff --git a/include/yail/yail.hpp b/include/yail/yail.hpp index 8b9c75a..c774bb0 100644 --- a/include/yail/yail.hpp +++ b/include/yail/yail.hpp @@ -10,19 +10,25 @@ namespace yail { + // Accepts both DLLs and EXEs (matched by the IMAGE_FILE_DLL characteristic). + // For EXEs, the CRT entry runs to completion and then calls ExitProcess — + // the host process will terminate when the injected EXE's main() returns. + // GetModuleHandle(nullptr) inside the injected EXE still resolves to the + // host process image, not the manually-mapped one. + [[nodiscard]] std::expected manual_map_injection_from_raw( - const std::span& raw_dll, std::uintptr_t process_id); + const std::span& raw_pe, std::uintptr_t process_id); [[nodiscard]] std::expected manual_map_injection_from_raw( - const std::span& raw_dll, const std::string_view& process_name); + const std::span& raw_pe, const std::string_view& process_name); [[nodiscard]] std::expected manual_map_injection_from_file( - const std::string_view& dll_path, std::uintptr_t process_id); + const std::string_view& pe_path, std::uintptr_t process_id); [[nodiscard]] std::expected manual_map_injection_from_file( - const std::string_view& dll_path, const std::string_view& process_name); + const std::string_view& pe_path, const std::string_view& process_name); } \ No newline at end of file diff --git a/source/yail.cpp b/source/yail.cpp index 2ea7acf..427c2ae 100644 --- a/source/yail.cpp +++ b/source/yail.cpp @@ -319,9 +319,21 @@ namespace // --- Call entry point --- if (nt_headers->OptionalHeader.AddressOfEntryPoint) { - const auto entry_point = reinterpret_cast( - base + nt_headers->OptionalHeader.AddressOfEntryPoint); - entry_point(reinterpret_cast(base), DLL_PROCESS_ATTACH, nullptr); + if (nt_headers->FileHeader.Characteristics & IMAGE_FILE_DLL) + { + const auto entry_point = reinterpret_cast( + base + nt_headers->OptionalHeader.AddressOfEntryPoint); + entry_point(reinterpret_cast(base), DLL_PROCESS_ATTACH, nullptr); + } + else + { + // EXE entry (mainCRTStartup / WinMainCRTStartup) — __cdecl, no args. + // When the entry returns the CRT calls exit() → ExitProcess, terminating + // the host process. GetModuleHandle(NULL) still resolves to the host EXE. + const auto entry_point = reinterpret_cast( + base + nt_headers->OptionalHeader.AddressOfEntryPoint); + entry_point(); + } } return 0; From 6e4c543d44d6e03c4cd2f6df27b9820efb8b4baa Mon Sep 17 00:00:00 2001 From: Orange Date: Sat, 2 May 2026 22:23:47 +0300 Subject: [PATCH 2/2] woops --- examples/loader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/loader.cpp b/examples/loader.cpp index 1304475..1516241 100644 --- a/examples/loader.cpp +++ b/examples/loader.cpp @@ -4,7 +4,7 @@ #include #include int main(int argc, char* argv[]) - +{ std::string dllPath = "test_winexe.exe"; if (argc > 1) dllPath = argv[1];