# Simple Manual Map Injector loads a DLL into a process without the loader knowing

> Simple Manual Map Injector is a C++ command line injector that maps a DLL into a running or newly spawned process by resolving its imports and relocations itself rather than going through LoadLibrary, and on x64 it goes further and registers the module's thread local storage with ntdll so that thread_local variables and C++ exceptions work. It is MIT licensed, x86 and x64, and it ships a test suite of purpose-built DLLs.

**TheCruZ/Simple-Manual-Map-Injector** — Simple C++ DLL Manual Map Injector For x86 and x64

- Repository: https://github.com/TheCruZ/Simple-Manual-Map-Injector
- Stars: 530 · Forks: 120
- Language: C++
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/thecruz-simple-manual-map-injector

## What manual mapping is, and why the project exists

Simple Manual Map Injector is a C++ tool for x86 and x64 that maps a DLL into a target process by hand rather than by asking the Windows loader to do it.

The distinction is the whole point. The normal way to load a library into a running process is to call LoadLibrary, which hands the file to the system loader, which maps the sections, resolves the imports, applies the relocations, registers the module with the loader's list and then calls the entry point. A manual mapper does each of those steps itself, in its own code, against a target process it has opened with the usual process APIs.

The reasons to do that are the reasons the security and tooling worlds care about, and they are legitimate ones. The system loader keeps a record of every module it has loaded, and anything that inspects that record will not see a manually mapped DLL. A target that restricts LoadLibrary will accept a manual map. And the process is reversible in the sense that you control exactly what gets resolved and when.

The project states its scope plainly in the README's bullet list, and the features are about the mapping mechanics rather than about any particular use. It removes the PE header and some sections, which is configurable. It has configurable DllMain parameters, defaulting to DLL_PROCESS_ATTACH. And it has adjustable section protections, also configurable.

The top-level entries show the shape of the work. There is a Visual Studio solution, Manual Map Injector.sln, with the project directory alongside it, a tests/ directory, and three prebuilt test DLLs at the root: hello-world-x64-with-exception.dll, hello-world-x64.dll and hello-world-x86.dll.

The licence is MIT, the last push was on 2026-10-15, and the releases are v2.0.0 from 2026-10-09, v1.2.0 from 2026-05-28 and v1.1.0 from 2021-10-28, so the project has been dormant for years and is now active again.

## Thread local storage, which is the hard part

Most manual mappers stop at imports and relocations. This one goes further on x64 and implements thread local storage, and the README is unusually explicit about how.

Full TLS support, x64 only: the host resolves ntdll!LdrpHandleTlsData by pattern-scan and the shellcode hands ntdll a minimal fake LDR_DATA_TABLE_ENTRY so the DLL ends up in LdrpTlsList. From then on ntdll reserves _tls_index, allocates the per-thread TLS block in every current and future thread, and frees them cleanly in LdrShutdownThread. The injector then fires the DLL's AddressOfCallBacks[] for DLL_PROCESS_ATTACH.

Read that sequence carefully, because it explains the whole design. A module with static TLS has to appear in ntdll's TLS list, and ntdll's TLS list is internal and undocumented. So the injector constructs a minimal entry that looks enough like a real one, splices it into the list, and then lets ntdll do what ntdll does: allocate the per-thread blocks for existing threads and for threads created later, and release them at thread exit.

Doing the allocation itself would be the alternative, and it would break the first time the target created a new thread. Letting ntdll do it means the module behaves like a properly loaded one as far as the TLS machinery is concerned.

The AddressOfCallBacks firing at the end matters too. A DLL with dynamic initialisation has a constructor that has to run, and the TLS callback array is where that happens for a manually mapped module.

The reason this is x64-only is stated in its own section. The host resolves ntdll!LdrpHandleTlsData by pattern-scanning x64 ntdll, with the byte pattern given as "44 8D ? 09 [5-24] B2 01 48 8B ? 30 E8" followed by walking back to CC CC CC padding. The README then says, in as many words, feel free to share full logic and pattern for x86. That is an open request rather than a hidden limitation, and it tells you the x86 path is the one piece of work that has not been done.

## C++ exceptions under both runtime libraries

The second hard feature is making the C++ runtime work inside a manually mapped module, and the solution here is a trampoline.

C++ exceptions, x64, under /MT and /MD, via a trampoline that intercepts the DLL's imported RtlPcToFileHeader so the static _CxxThrowException of /MT gets the correct ImageBase.

The problem is specific and worth understanding. When the C++ runtime throws, it needs to work out which module the instruction pointer belongs to, in order to find the right exception handling data. It does that by walking the list of loaded modules. A manually mapped DLL is not on that list, so the lookup fails, and the exception either crashes or is reported against the wrong module.

The static _CxxThrowException is the wrinkle. Under /MT, the C++ runtime is statically linked into the DLL, so the throwing code is a copy inside the module, and it has no way to reach the runtime instance in the host. Under /MD, the runtime is shared and the host's copy is used, which is why the problem is described as arising with the static runtime.

Intercepting RtlPcToFileHeader solves it by making the lookup return the right answer for the mapped module. Since the trampoline is part of the injector, it knows the module's real base address, and it can supply that in place of the address the system would have computed from a module list that does not contain the DLL.

The compatibility matrix lists the exception variants separately, and there are five of them on x64: /EHsc with /MD, /EHa with /MD, /EHsc with /MT, /EHa with /MT, and /EHac with /MT. All five work. /EHac is the least common of the three, and having it in the matrix is the sort of detail that only appears in a project that was built by someone who hit the failures rather than read about them.

The matrix also lists SEH __try/__except as x64 only, which is a separate mechanism from C++ exceptions and is listed separately for that reason.

## Two ways to use it, and why spawn is preferred

The usage section has two command forms, and the difference between them is not cosmetic.

The first attaches to something that is already running:

```text
Injector.exe dll_path [target]
```

The target argument is either a process name such as notepad.exe or a decimal PID. The README is precise about the resolution order: pure-digit arguments are tried as a PID first and fall back to the process-name search if nothing lives at that PID.

The second form launches the target itself:

```text
Injector.exe --spawn dll_path exe_path
```

It launches exe_path suspended, injects the DLL with TLS registered via LdrpHandleTlsData and TLS callbacks fired, and resumes the main thread.

And then the reason, which is the most useful sentence in the README: spawn is preferred over attaching to a running process when the DLL uses thread_local, since every existing thread of the target then gets its TLS block provisioned by ntdll in one shot.

That is a direct consequence of how the TLS registration works. Attaching to a running process means the injector has to cope with threads that already exist, and the per-thread TLS blocks for those threads are a separate problem from the blocks for threads created later. Spawning suspended means there is exactly one thread, and by the time it is resumed ntdll has already been told to provision for it. The mode removes a whole class of problem rather than solving it.

The argument that the injector takes is also worth noting, since it determines the target's behaviour. The default is DLL_PROCESS_ATTACH, which is the normal case for a plugin that wants to run its setup code when loaded. But it is configurable, so a DLL built to do something other than initialise on load is possible, and the configurability is there for a reason the README does not spell out.

For the PE header removal, the reasoning is the same as the module list point: a mapped DLL that still has a valid PE header in memory can be found by anything scanning for it. Removing the header and some sections, and adjusting section protections, are all ways of reducing the module's footprint in the target.

## The test DLLs, which say more about the project than the feature list

The Devs section lists what the repository includes for development, and the test DLLs are the interesting part.

Hello World test DLLs come from a separate repository, a well-known one by carterjones. That is the sanity check: a DLL that logs from DllMain, mapped in, and you confirm it ran.

Then there are three purpose-built ones. tests/tls-test-dll is the TLS test, covering POD, arrays, alignas, big structs, __declspec(thread), dynamic init with constructor, and multi-thread isolation. The documentation notes that both x86 and x64 builds compile, but only the x64 injection actually sets static TLS up, which points back at the Why TLS is x64-only section rather than leaving it as a puzzle.

tests/exc-test-dll is the C++ exception test, covering polymorphism, rethrow, current_exception, nested exceptions, lambdas, threads and exports. It is built for every combination of /EHsc, /EHa and /EHac with both /MT and /MD, and the repository points at build_all.ps1 with an -Arch argument of x64, x86 or both as the way to run that matrix.

tests/trivial-dll is the minimal DllMain-logging DLL used as a mapping sanity check.

Three kinds of test DLL, and each one corresponds to a feature that is hard to get right. The TLS test with alignas and big structs is checking alignment handling for per-thread data. The multi-thread isolation case is checking that two threads do not share a thread_local variable. The exception test with nested exceptions and rethrow is checking that the runtime's unwinder can walk the stack correctly in a module it does not know about, which is exactly what the RtlPcToFileHeader trampoline is there to make work.

Two more development notes. You can define DISABLE_OUTPUT to silence the console logs from injector.cpp, which matters when you are injecting into something that is already writing to the terminal. And main.cpp is described as a usable example of the injector flow, so the entry point is meant to be read as documentation as well as run.

## Conclusion

Simple Manual Map Injector suits a developer who needs to load a module into a process it did not start, for testing instrumentation or a plugin that has to survive a target that does not load libraries normally, and the spawn mode with TLS registered in one shot is the reason to use it over a simpler injector. It is the wrong tool on x86 if your DLL uses thread local storage or C++ exceptions, because the TLS path exists only on x64 and the repository says so plainly, and it is the wrong tool if you need a supported, signed loading path. Before you rely on it, run the included tests: tests/tls-test-dll, tests/exc-test-dll and tests/trivial-dll exist precisely to check that the mapping and the TLS registration actually behave.

## FAQ

### What is Simple Manual Map Injector?

It is a C++ command line tool for x86 and x64 that loads a DLL into a target process by resolving its imports and relocations itself instead of going through LoadLibrary. It removes the PE header and some sections, supports configurable DllMain parameters and adjustable section protections, and on x64 it registers the module's thread local storage with ntdll.

### How do I run the injector?

Use Injector.exe dll_path [target] to attach to a running process, where the target is a process name or a decimal PID, with pure-digit arguments tried as a PID first. Use Injector.exe --spawn dll_path exe_path to launch the target suspended, inject, and resume it, which the documentation prefers when the DLL uses thread_local.

### Why does thread local storage support only work on x64?

The host resolves ntdll!LdrpHandleTlsData by pattern-scanning x64 ntdll with the byte pattern 44 8D ? 09 [5-24] B2 01 48 8B ? 30 E8, then walks back to CC CC CC padding. The README asks readers to share the full logic and pattern for x86, which is the piece of work that has not been done.

### How does the injector make C++ exceptions work inside a mapped DLL?

Through a trampoline that intercepts the DLL's imported RtlPcToFileHeader so that the static _CxxThrowException of the /MT runtime gets the correct ImageBase. The compatibility matrix lists x64 support for exceptions under /EHsc, /EHa and /EHac with both /MT and /MD, and for SEH __try/__except.

### What test DLLs does the repository include?

tests/tls-test-dll covers POD, arrays, alignas, big structs, __declspec(thread), dynamic init with constructor and multi-thread isolation. tests/exc-test-dll covers polymorphism, rethrow, current_exception, nested exceptions, lambdas, threads and exports across every /EHsc, /EHa and /EHac combination with /MT and /MD. tests/trivial-dll is a minimal DllMain-logging DLL used as a mapping sanity check, and the Hello World DLLs are from another repository.

### What licence is Simple Manual Map Injector released under?

MIT, with a LICENSE file at the repository root. The most recent release is v2.0.0 from 2026-10-09, after v1.2.0 from 2026-05-28 and v1.1.0 from 2021-10-28, and the last push to the repository was on 2026-10-15.

## Sources

- [Issues](https://github.com/TheCruZ/Simple-Manual-Map-Injector/issues)
- [License: MIT](https://github.com/TheCruZ/Simple-Manual-Map-Injector/blob/master/LICENSE)
- [README](https://github.com/TheCruZ/Simple-Manual-Map-Injector/blob/master/README.md)
- [Releases](https://github.com/TheCruZ/Simple-Manual-Map-Injector/releases)
- [TheCruZ/Simple-Manual-Map-Injector on GitHub](https://github.com/TheCruZ/Simple-Manual-Map-Injector)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/thecruz-simple-manual-map-injector
