diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18dc39f..82747a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh release delete 1.0.5 --cleanup-tag --yes || echo "No release or tag found for 1.0.5" + gh release delete 1.1.0 --cleanup-tag --yes || echo "No release or tag found for 1.1.0" - name: Create Release id: create_release @@ -55,13 +55,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: 1.0.5 - release_name: DllShimmer 1.0.5 + tag_name: 1.1.0 + release_name: DllShimmer 1.1.0 body: | - DllShimmer Weaponize DLL hijacking easily. Backdoor any function in any DLL. - - - [x] GitHub Actions workflow improvements - - [x] Windows build improvements + - [x] Dynamic linking is now cached (both LoadLibraryA and GetProcAddress). Performance improved. + - [x] Better debug log format: timestamp added. + - [x] New parameter: `--debug-file`. Save debug logs to file. + - [x] README updated. draft: false prerelease: false diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a00b2c..a5226a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,8 @@ "files.associations": { "*.cpp.template": "cpp", "*.sh.template": "shellscript", - "functional": "cpp" + "functional": "cpp", + "*.template": "cpp", + "cstdarg": "cpp" } } \ No newline at end of file diff --git a/README.md b/README.md index 6e8ac5f..181dbc5 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,15 @@ Example: Parameters: -**`-i / --input `** [required] +**`-i / --input `** [required] The original DLL that you want to backdoor. -**`-o / --output `** [required] +**`-o / --output `** [required] The path to the directory where DllShimmer will save all generated files. -**`-x / --original-path `** [required] +**`-x / --original `** [required] In case of dynamic linking (default) provide the path where the proxy DLL will find the original DLL on the target system. @@ -67,7 +67,17 @@ This technique has some serious limitations compared to dynamic linking: However, static linking may be more stealthy and natural in some scenarios. -By default, DllShimmer always uses dynamic linking with the `LoadLibraryA()` and `GetProcAddress()` functions. +Default: DllShimmer always uses dynamic linking with the `LoadLibraryA()` and `GetProcAddress()` functions. + +**`--debug-file `** [optional] + +Save debug logs to a file. Logs are written to a file on an ongoing basis while the program is running. If selected, logs are not printed to STDOUT. + +Default: DllShimmer always writes debug logs to STDOUT. + +Example debug output: + +![Example debug output](_img/img-2.png) ## Limitations @@ -78,6 +88,12 @@ By default, DllShimmer always uses dynamic linking with the `LoadLibraryA()` and ## Troubleshooting +Before you start troubleshooting: + +1. Read "Limitations". +2. Make sure you don't use static linking (`--static`). It's easier to debug with dynamic linking (default). +3. Save debug output to file (`--debug-file`). + ### _In the generated `.cpp` file, I don't see all the exported functions from the original DLL._ Functions defined in the original DLL as “forwarded” are not included in the `.cpp` file. However, they are visible in the `.def` file. They will also be exported after compilation, exactly as in the original DLL. @@ -102,5 +118,3 @@ In case of static linking, we really only have one option: ## TODO - Cache LoadLibraryA() and GetProcAddress() pointers not to call WinAPI every time (better performance and more stealthy). -- Improve the shim template code (leave as little code in the macro as possible. Is the macro actually required now when we use args/params trick?) -- Maybe move boilerplate code into header file? diff --git a/_img/img-2.png b/_img/img-2.png new file mode 100644 index 0000000..73b58dc Binary files /dev/null and b/_img/img-2.png differ diff --git a/cli/cli.go b/cli/cli.go index c6c8699..2afc133 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -8,11 +8,12 @@ import ( ) type CliFlags struct { - Input string - Output string - OriginalPath string - Mutex bool - Static bool + Input string + Output string + Original string + Mutex bool + Static bool + DebugFile string } func IsValidDllName(filename string) bool { @@ -41,8 +42,11 @@ func ParseCli() *CliFlags { flag.StringVar(&flags.Output, "o", "", "") flag.StringVar(&flags.Output, "output", "", "") - flag.StringVar(&flags.OriginalPath, "x", "", "") - flag.StringVar(&flags.OriginalPath, "original-path", "", "") + flag.StringVar(&flags.Original, "x", "", "") + flag.StringVar(&flags.Original, "original", "", "") + + flag.StringVar(&flags.DebugFile, "d", "", "") + flag.StringVar(&flags.DebugFile, "debug-file", "", "") flag.BoolVar(&flags.Mutex, "m", false, "") flag.BoolVar(&flags.Mutex, "mutex", false, "") @@ -54,10 +58,11 @@ func ParseCli() *CliFlags { fmt.Println() fmt.Println("Usage:") fmt.Println() - fmt.Printf(" %-26s %s\n", "-i, --input ", "Input DLL file (required)") - fmt.Printf(" %-26s %s\n", "-o, --output ", "Output directory (required)") - fmt.Printf(" %-26s %s\n", "-x, --original-path ", "Path to original DLL on target (required)") + fmt.Printf(" %-26s %s\n", "-i, --input ", "Input DLL file (required)") + fmt.Printf(" %-26s %s\n", "-o, --output ", "Output directory (required)") + fmt.Printf(" %-26s %s\n", "-x, --original ", "Path to original DLL on target (required)") fmt.Printf(" %-26s %s\n", "-m, --mutex", "Multiple execution prevention (default: false)") + fmt.Printf(" %-26s %s\n", " --debug-file ", "Save debug logs to a file (default: stdout)") fmt.Printf(" %-26s %s\n", " --static", "Static linking to original DLL via IAT (default: false)") fmt.Printf(" %-26s %s\n", "-h, --help", "Show this help") fmt.Println() @@ -71,12 +76,12 @@ func ParseCli() *CliFlags { flag.Parse() - if flags.Input == "" || flags.Output == "" || flags.OriginalPath == "" { + if flags.Input == "" || flags.Output == "" || flags.Original == "" { flag.Usage() os.Exit(1) } - if flags.Static && !IsValidDllName(flags.OriginalPath) { + if flags.Static && !IsValidDllName(flags.Original) { fmt.Fprintf(os.Stderr, "[!] Invalid '-x' parameter value:\n") fmt.Fprintf(os.Stderr, "In case of static linking enabled '-x' parameter must be valid Windows DLL file name with no path information. E.g. kernel32.dll, user32.dll.") os.Exit(1) diff --git a/dll/dll.go b/dll/dll.go index 3dfdaf6..b24ac1a 100644 --- a/dll/dll.go +++ b/dll/dll.go @@ -15,15 +15,15 @@ type ExportedFunction struct { type Dll struct { Name string - OriginalPath string + Original string ExportedFunctions []ExportedFunction } -func ParseDll(path string, originalPath string) *Dll { +func ParseDll(path string, original string) *Dll { var dll Dll dll.Name = filepath.Base(path) - dll.OriginalPath = originalPath + dll.Original = original pe, err := peparser.New(path, &peparser.Options{}) if err != nil { diff --git a/main.go b/main.go index 7ff1617..8956b25 100644 --- a/main.go +++ b/main.go @@ -18,12 +18,12 @@ func main() { cli.PrintBanner() out := output.Output{ - Dll: dll.ParseDll(flags.Input, flags.OriginalPath), + Dll: dll.ParseDll(flags.Input, flags.Original), OutputDir: filepath.Clean(flags.Output), TemplatesFS: &templatesFS, } - out.CreateCodeFile(flags.Mutex, flags.Static) + out.CreateCodeFiles(flags.Mutex, flags.DebugFile, flags.Static) out.CreateDefFile() out.CreateCompileScript(flags.Static) @@ -35,7 +35,7 @@ func main() { fmt.Println("Success! What to do next?") fmt.Println() fmt.Printf(" 1. Jump into the '%s/' directory.\n", out.OutputDir) - fmt.Printf(" 2. Add your backdoor to the '%s' file.\n", out.GetCodeFileName()) + fmt.Printf(" 2. Add your backdoor to the '%s' file.\n", out.GetCppCodeFileName()) fmt.Printf(" 3. Compile project using the '%s' script.\n", out.GetCompileScriptName()) fmt.Println() } diff --git a/output/output.go b/output/output.go index b028486..35f81c7 100644 --- a/output/output.go +++ b/output/output.go @@ -23,10 +23,14 @@ func (o *Output) GetDefFileName() string { return o.Dll.Name + ".def" } -func (o *Output) GetCodeFileName() string { +func (o *Output) GetCppCodeFileName() string { return o.Dll.Name + ".cpp" } +func (o *Output) GetHdrCodeFileName() string { + return "dllshimmer.h" +} + func (o *Output) GetCompileScriptName() string { return "compile.sh" } @@ -40,10 +44,12 @@ func (o *Output) GetLibFileName() string { } type CodeFileParams struct { - Functions []dll.ExportedFunction - OriginalPath string - Mutex bool - DllName string + Functions []dll.ExportedFunction + Original string + Mutex bool + DllName string + DebugFile string + IsStaticLinked bool } func (o *Output) GetTemplate(filename string) *template.Template { @@ -56,34 +62,33 @@ func (o *Output) GetTemplate(filename string) *template.Template { return template.Must(template.New("new").Parse(string(content))) } -func (o *Output) CreateCodeFile(mutex bool, isStaticLinked bool) { - templateFile := "dynamic-shim.cpp.template" - if isStaticLinked { - templateFile = "static-shim.cpp.template" +func (o *Output) CreateCodeFiles(mutex bool, debugFile string, isStaticLinked bool) { + params := CodeFileParams{ + Functions: o.Dll.ExportedFunctions, + Original: sanitizePathForInjection(o.Dll.Original), + Mutex: mutex, + DllName: o.Dll.Name, + DebugFile: sanitizePathForInjection(debugFile), + IsStaticLinked: isStaticLinked, } - tmpl := o.GetTemplate(templateFile) - outputPath := filepath.Join(o.OutputDir, o.GetCodeFileName()) - - f, err := os.Create(outputPath) - if err != nil { - log.Fatalf("[!] Error while creating '%s' file: %v", outputPath, err) - } - defer f.Close() + o.createCppCodeFile(params) + o.createHdrCodeFile(params) +} - params := CodeFileParams{ - Functions: o.Dll.ExportedFunctions, - OriginalPath: sanitizePathForInjection(o.Dll.OriginalPath), - Mutex: mutex, - DllName: o.Dll.Name, +func (o *Output) createCppCodeFile(params CodeFileParams) { + templateFile := "dynamic-shim.cpp.template" + if params.IsStaticLinked { + templateFile = "static-shim.cpp.template" } - err = tmpl.Execute(f, params) - if err != nil { - log.Fatalf("[!] Error of template engine: %v", err) - } + outputPath := filepath.Join(o.OutputDir, o.GetCppCodeFileName()) + createFileFromTemplate(o, templateFile, outputPath, params) +} - fmt.Printf("[+] '%s' file created\n", outputPath) +func (o *Output) createHdrCodeFile(params CodeFileParams) { + outputPath := filepath.Join(o.OutputDir, o.GetHdrCodeFileName()) + createFileFromTemplate(o, "dllshimmer.h.template", outputPath, params) } func (o *Output) CreateDefFile() { @@ -112,8 +117,8 @@ func (o *Output) CreateDefFile() { func (o *Output) CreateLibFile() { var def def.DefFile - // In case of static linking OriginalPath is DLL name itself - def.DllName = o.Dll.OriginalPath + // In case of static linking Original is DLL name itself + def.DllName = o.Dll.Original for _, function := range o.Dll.ExportedFunctions { if function.Forwarder == "" { @@ -166,7 +171,7 @@ func (o *Output) CreateCompileScript(isStaticLinked bool) { defer f.Close() params := CompileScriptParams{ - Code: o.GetCodeFileName(), + Code: o.GetCppCodeFileName(), Def: o.GetDefFileName(), Output: o.GetOutputDllName(), IsStaticLinked: isStaticLinked, diff --git a/output/utils.go b/output/utils.go index 6c80986..7777050 100644 --- a/output/utils.go +++ b/output/utils.go @@ -1,7 +1,29 @@ package output -import "strings" +import ( + "fmt" + "log" + "os" + "strings" +) func sanitizePathForInjection(path string) string { return strings.ReplaceAll(path, "\\", "\\\\") } + +func createFileFromTemplate[K interface{}](o *Output, template string, outputPath string, params K) { + tmpl := o.GetTemplate(template) + + f, err := os.Create(outputPath) + if err != nil { + log.Fatalf("[!] Error while creating '%s' file: %v", outputPath, err) + } + defer f.Close() + + err = tmpl.Execute(f, params) + if err != nil { + log.Fatalf("[!] Error of template engine: %v", err) + } + + fmt.Printf("[+] '%s' file created\n", outputPath) +} diff --git a/templates/dllshimmer.h.template b/templates/dllshimmer.h.template new file mode 100644 index 0000000..51ba456 --- /dev/null +++ b/templates/dllshimmer.h.template @@ -0,0 +1,123 @@ +#pragma once + +// #------------------------------------------------------------------# +// | "DON'T TOUCH" ZONE | +// | (auto generated) | +// #------------------------------------------------------------------# + +#include +#include +#include +#include +#include + + +#define T UINT64 +#define PARAMS T a1, T a2, T a3, T a4, T a5, T a6, T a7, T a8, T a9, T a10, T a11, T a12 +#define ARGS a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 + +typedef T (*FuncPtr)(PARAMS); + +typedef struct { + FILE *dbgOut; + HMODULE module; + std::unordered_map functions; +} Ctx; + +Ctx gCtx = { .dbgOut = NULL, .module = NULL }; + +void initDbg() { + if (gCtx.dbgOut != NULL) return; + +{{- if gt (len .DebugFile) 0 }} + + gCtx.dbgOut = fopen("{{.DebugFile}}", "w"); + if (!gCtx.dbgOut) { + MessageBoxA( + NULL, + "fopen({{.DebugFile}}) failed", + "DllShimmer", + MB_OK | MB_ICONINFORMATION + ); + } + +{{- else }} + + gCtx.dbgOut = stdout; + +{{- end }} +} + +char gTimeBuf[9]; // "HH:MM:SS" + null + +char *getCurrentTime() { + time_t t = time(NULL); + struct tm lt; + localtime_s(<, &t); + + strftime(gTimeBuf, sizeof(gTimeBuf), "%H:%M:%S", <); + + return gTimeBuf; +} + +void dbgf(const char *fmt, ...) { + if (gCtx.dbgOut == NULL) { + initDbg(); + } + + va_list ap; + va_start(ap, fmt); + + fprintf(gCtx.dbgOut, "[DBG] {{.DllName}} | %s | ", getCurrentTime()); + vfprintf(gCtx.dbgOut, fmt, ap); + fprintf(gCtx.dbgOut, "\n"); + fflush(gCtx.dbgOut); + + va_end(ap); +} + +void dbgCurrentDirectory() { + char buf[MAX_PATH]; + DWORD len = GetCurrentDirectoryA(MAX_PATH, buf); + if (len == 0 || len >= MAX_PATH) { + dbgf("GetCurrentDirectoryA failed"); + return; + } + + dbgf("\tCurrent directory: '%s'", buf); +} + +FuncPtr getProxyFunc(const char *funcName) { + // Module pointer is cached + if (gCtx.module == NULL) { + gCtx.module = LoadLibraryA("{{.Original}}"); + if (gCtx.module == NULL) { + dbgf("LoadLibraryA({{.Original}}) failed"); + dbgf("\tError code: %lu", GetLastError()); + dbgCurrentDirectory(); + + return NULL; + } + } + + std::string strFuncName(funcName); + + // Function pointer is cached + if (gCtx.functions.find(strFuncName) != gCtx.functions.end()) { + return gCtx.functions[strFuncName]; + } + + FuncPtr pFunc = (FuncPtr)GetProcAddress(gCtx.module, funcName); + if (pFunc == NULL) { + dbgf("GetProcAddress(%s, {{.Original}}) failed", funcName); + dbgf("\tError code: %lu", GetLastError()); + } + + gCtx.functions[strFuncName] = pFunc; + + return pFunc; +} + +#define MUTEX(name) (CreateMutexA(NULL, TRUE, name) && GetLastError() != ERROR_ALREADY_EXISTS) + +#define PROXY_FUNCTION(funcName) getProxyFunc(funcName)(ARGS); diff --git a/templates/dynamic-shim.cpp.template b/templates/dynamic-shim.cpp.template index 68285fb..8cb9ece 100644 --- a/templates/dynamic-shim.cpp.template +++ b/templates/dynamic-shim.cpp.template @@ -3,70 +3,17 @@ // Author: Print3M (print3m.github.io/) {{- $r := . }} +#include "dllshimmer.h" #include #include -#include -// Put your imports here... - -// #------------------------------------------------------------------# -// | "DON'T TOUCH" ZONE | -// | (auto generated) | -// #------------------------------------------------------------------# - -#define MUTEX(name) \ - (CreateMutexA(NULL, TRUE, name) && GetLastError() != ERROR_ALREADY_EXISTS) - -#define ARGS_COUNT 12 - -typedef uint64_t (*Func12)( - uint64_t, uint64_t, uint64_t, uint64_t, - uint64_t, uint64_t, uint64_t, uint64_t, - uint64_t, uint64_t, uint64_t, uint64_t -); - -#define T UINT64 -#define PARAMS T a1, T a2, T a3, T a4, T a5, T a6, T a7, T a8, T a9, T a10, T a11, T a12 -#define ARGS a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 - -void PrintCurrentDirectoryA() { - char buf[MAX_PATH]; - DWORD len = GetCurrentDirectoryA(MAX_PATH, buf); - if (len == 0 || len >= MAX_PATH) { - printf("\tGetCurrentDirectoryA failed\n"); - return; - } - printf("\tCurrent directory: '%s'\n", buf); -} - -#define PROXY_FUNCTION(function) \ - \ - HMODULE hModule = LoadLibraryA("{{.OriginalPath}}"); \ - if (hModule == NULL) { \ - printf("[!] DismCore.dll: LoadLibraryA(DismCore2.dll) failed\n"); \ - printf("\tError code: %lu\n", GetLastError()); \ - PrintCurrentDirectoryA(); \ - } \ - \ - Func12 pFunction = (Func12) GetProcAddress(hModule, function); \ - if (pFunction == NULL) { \ - printf( \ - "[!] {{.DllName}}: GetProcAddress(%s, {{.OriginalPath}}) failed\n", \ - function ); \ - printf("\tError code: %lu\n", GetLastError()); \ - } \ - \ - return pFunction(ARGS); \ - -// #------------------------------------------------------------------# -// | END OF "DON'T TOUCH" ZONE | -// #------------------------------------------------------------------# {{- range $i, $v := .Functions }} + {{- if eq (len $v.Forwarder) 0 }} extern "C" UINT64 {{$v.Name}}Fwd(PARAMS) { #ifdef DEBUG - printf("[+] {{$r.DllName}}: {{$v.Name}} called\n"); + dbgf("{{$v.Name}} called"); #endif {{ if $r.Mutex }} if (MUTEX("Global\\{{$v.Name}}__{{$i}}")) { @@ -76,19 +23,20 @@ extern "C" UINT64 {{$v.Name}}Fwd(PARAMS) { // Put your code here... {{- end }} - PROXY_FUNCTION("{{$v.Name}}"); + return PROXY_FUNCTION("{{$v.Name}}"); } {{- end }} + {{- end }} BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { - + switch (fdwReason) { case DLL_PROCESS_ATTACH: { #ifdef DEBUG - printf("[+] {{.DllName}}: DLL_PROCESS_ATTACH event\n"); + dbgf("DLL_PROCESS_ATTACH"); #endif } case DLL_THREAD_ATTACH: diff --git a/templates/static-shim.cpp.template b/templates/static-shim.cpp.template index a1818e6..d84a3e4 100644 --- a/templates/static-shim.cpp.template +++ b/templates/static-shim.cpp.template @@ -14,30 +14,13 @@ {{- range .Functions}} #define {{.Name}} {{.Name}}Original {{- end}} +#include "dllshimmer.h" #include {{- range .Functions}} #undef {{.Name}} {{- end}} {{ $r := . }} -void PrintCurrentDirectoryA() { - char buf[MAX_PATH]; - DWORD len = GetCurrentDirectoryA(MAX_PATH, buf); - if (len == 0 || len >= MAX_PATH) { - printf("\tGetCurrentDirectoryA failed\n"); - return; - } - printf("\tCurrent directory: '%s'\n", buf); -} - - -#define MUTEX(name) \ - (CreateMutexA(NULL, TRUE, name) && GetLastError() != ERROR_ALREADY_EXISTS) - -#define T UINT64 -#define PARAMS T a1, T a2, T a3, T a4, T a5, T a6, T a7, T a8, T a9, T a10, T a11, T a12 -#define ARGS a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 - // #------------------------------------------------------------------# // | END OF "DON'T TOUCH" ZONE | // #------------------------------------------------------------------# @@ -49,7 +32,7 @@ extern "C" __declspec(dllimport) UINT64 {{$v.Name}}(PARAMS); extern "C" UINT64 {{$v.Name}}Fwd(PARAMS) { #ifdef DEBUG - printf("[+] {{$r.DllName}}: {{$v.Name}} called\n"); + dbgf("{{$v.Name}} called"); #endif {{ if $r.Mutex }} if (MUTEX("Global\\{{$v.Name}}__{{$i}}")) { @@ -71,8 +54,8 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: { #ifdef DEBUG - printf("[+] {{.DllName}}: DLL_PROCESS_ATTACH event\n"); - PrintCurrentDirectoryA(); + dbgf("DLL_PROCESS_ATTACH event"); + dbgCurrentDirectory(); #endif } case DLL_THREAD_ATTACH: