- Manual PE mapping (no
LoadLibrarytraces)- x64 — full unwind table registration via
RtlInsertInvertedFunctionTable(withRtlAddFunctionTablefallback) - x86 — SEH validation via
RtlInsertInvertedFunctionTable(handles modern Win11 24H2 internal__fastcallconvention)
- x64 — full unwind table registration via
- 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 bothmain-style (console subsystem) andWinMain-style (GUI subsystem) entries
- DLLs invoked as
- 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<uintptr_t, std::string>— no exceptions, clear error messages
- 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
x64:
cmake --preset windows-debug-vcpkg
cmake --build cmake-build/build/windows-debug-vcpkgx86:
cmake --preset windows-debug-vcpkg-x86
cmake --build cmake-build/build/windows-debug-vcpkg-x86Native injection requires matching bitness: an x86 build of yail injects x86 PEs into x86 (WOW64) processes, and an x64 build injects x64 PEs into x64 processes. An x64 build can also inject x86 PEs into x86 targets using its embedded x86 loader.
Examples build by default. Disable with -DYAIL_BUILD_EXAMPLES=OFF.
#include <yail/yail.hpp>
auto result = yail::manual_map_injection_from_file("my.dll", "target.exe");
if (!result)
std::println("Failed: {}", result.error());
else
std::println("Loaded at 0x{:x}", result.value());auto result = yail::manual_map_injection_from_file("my.dll", GetCurrentProcessId());Same API — auto-detection picks the right entry-point shape:
auto result = yail::manual_map_injection_from_file("my.exe", GetCurrentProcessId());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 lettingmain/WinMainreturn — e.g.ExitThread(0)from the entry, like the bundledtest_exe. GetModuleHandle(nullptr)inside the injected EXE returns the host image base, not the mapped one.WinMain'shInstanceis correct (it comes from__ImageBase, which is relocated), but APIs that readPEB->ImageBaseAddressare not.
std::vector<uint8_t> bytes = /* ... */;
auto result = yail::manual_map_injection_from_raw(bytes, "target.exe");The normal APIs detect x86 payloads and use the embedded x86 loader automatically. No helper process is launched:
auto result = yail::manual_map_injection_from_raw(bytes, "x86-target.exe");namespace yail
{
// Both functions accept DLLs and EXEs (matched by IMAGE_FILE_DLL).
// Native PE machine type must match the build. An x64 build also accepts
// I386 PEs when the target is a WOW64 process.
std::expected<uintptr_t, std::string>
manual_map_injection_from_file(std::string_view pe_path, std::uintptr_t process_id);
std::expected<uintptr_t, std::string>
manual_map_injection_from_file(std::string_view pe_path, std::string_view process_name);
std::expected<uintptr_t, std::string>
manual_map_injection_from_raw(const std::span<std::uint8_t>& raw_pe, std::uintptr_t process_id);
std::expected<uintptr_t, std::string>
manual_map_injection_from_raw(const std::span<std::uint8_t>& 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.
The returned address is not a loader-managed HMODULE. A DLL mapped by yail is absent from the Windows loader module list, so passing that address to FreeLibrary or FreeLibraryAndExitThread is invalid. yail does not currently provide manual unmapping. A mapped DLL must stop its work and return, or be loaded with LoadLibrary when OS-managed unload is required.
find_package(yail CONFIG REQUIRED)
target_link_libraries(my_target PRIVATE yail::yail)The examples/ directory contains:
| Target | Purpose |
|---|---|
loader |
Manual-maps a PE (DLL or EXE) into the current process. loader.exe <path>. |
remote_loader |
Manual-maps into a target process by name. remote_loader.exe <dll> <process.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:
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 checksThe library locates two non-exported ntdll routines by byte signatures:
LdrpHandleTlsData— used to register static TLS for the mapped imageRtlInsertInvertedFunctionTable— 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 <addr>), 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*.
The shellcode implementation is kept as a disabled reference block in source/yail.cpp. After changing it, temporarily enable the block, rebuild both loader targets, refresh the embedded loaders, then disable the block again:
python tools/generate_shellcode.py cmake-build/build/windows-release-vcpkg/loader.exe cmake-build/build/windows-release-vcpkg-x86/loader.exe source/shellcode.hpp