diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..217310f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,93 @@ +name: build + +on: + push: + branches: + - main + +jobs: + build: + runs-on: windows-latest + defaults: + run: + shell: cmd + + strategy: + matrix: + arch: + - x64 + - x86 + + steps: + - uses: actions/checkout@main + with: + fetch-depth: 1 + submodules: true + + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.arch }} + vsversion: 2022 + + - uses: actions/setup-dotnet@main + with: + dotnet-version: 8.0.x + + - name: Build detours + run: nmake + working-directory: detours + + - name: Build the detours DLL + working-directory: detours-dll + run: | + mkdir bin.${{ matrix.arch }} + pushd bin.${{ matrix.arch }} + cl.exe /nologo /LD /TP /DUNICODE /DWIN32 /D_WINDOWS /EHsc /W4 /WX /Zi /O2 /Ob1 /DNDEBUG /Fodetours.obj /Fddetours.pdb ..\detours.cpp ^ + /link /def:..\detours.def "%GITHUB_WORKSPACE%\detours\lib.${{ matrix.arch }}\detours.lib" + popd + + - name: Apply detours.h patch + if: ${{ matrix.arch == 'x64' }} + shell: pwsh + run: "@(Get-Content \".\\detours\\src\\detours.h\")[0..866+920..1234] | Set-Content \".\\detours\\include\\detours.h\"" + + - name: Build detours metadata + if: ${{ matrix.arch == 'x64' }} + working-directory: detours-meta + run: dotnet build /p:BuildConfig="" + + - name: Build and publish withdll + if: ${{ matrix.arch == 'x64' }} + working-directory: withdll + run: dotnet publish -r win-x64 -c Release -o "%GITHUB_WORKSPACE%\withdll\bin.x64" + + - uses: actions/upload-artifact@main + with: + name: detours-${{ matrix.arch }} + path: | + detours/bin.${{ matrix.arch }}/syelog.exe + detours/bin.${{ matrix.arch }}/syelog.pdb + detours/bin.${{ matrix.arch }}/trcapi*.dll + detours/bin.${{ matrix.arch }}/trcapi*.pdb + detours/bin.${{ matrix.arch }}/trcmem*.dll + detours/bin.${{ matrix.arch }}/trcmem*.pdb + detours/bin.${{ matrix.arch }}/trcreg*.dll + detours/bin.${{ matrix.arch }}/trcreg*.pdb + detours/bin.${{ matrix.arch }}/trcssl*.dll + detours/bin.${{ matrix.arch }}/trcssl*.pdb + detours/bin.${{ matrix.arch }}/trctcp*.dll + detours/bin.${{ matrix.arch }}/trctcp*.pdb + + - uses: actions/upload-artifact@main + with: + name: detours-meta + path: detours-meta/winmd/detours.winmd + if: ${{ matrix.arch == 'x64' }} + + - uses: actions/upload-artifact@main + with: + name: withdll + path: | + withdll/bin.x64/withdll.exe + withdll/bin.x64/withdll.pdb + if: ${{ matrix.arch == 'x64' }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17dc5a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.vs/ + +detours-meta/winmd/ + +bin.*/ + +bin/ +obj/ + +*.csproj.user +withdll/Properties/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1e77cd7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "detours"] + path = detours + url = https://github.com/microsoft/Detours.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa27759 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Sebastian Solnica + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d82e08 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ + +## withdll - a small tool to perform DLL injections + +![build](https://github.com/lowleveldesign/withdll/workflows/build/badge.svg) + +This project is inspired by a sample with the same name from the [Detours repository](https://github.com/microsoft/Detours). I decided to create it as I was missing some features in the Detours' sample (most importantly, a way to inject a DLL into a running process). To make things more interesting, I decided to implement it in C#, using generated Detours bindings and NativeAOT with static detours linking. If you are interested in the binding generation, have a look at [this post](https://lowleveldesign.wordpress.com/2023/11/22/generating-c-bindings-for-native-windows-libraries/) on my blog. + +You may **download the compiled binaries from the [release page](https://github.com/lowleveldesign/withdll/releases)**. Each release also contains compiled Detours sample libraries that are examples of WinAPI functions tracers. I write more on how to use them in [a guide on https://wtrace.net](https://wtrace.net/guides/using-withdll-and-detours-to-trace-winapi/). + +Although withdll is a 64-bit application, it **supports injecting DLLs into both 32-bit and 64-bit processes**. + +Example command lines: + +``` +withdll.exe -d trcapi64.dll C:\Windows\System32\winver.exe +withdll.exe -d trcapi32.dll C:\Windows\SysWow64\winver.exe + +withdll.exe -d trcapi32.dll 1234 +``` + +Additionally, you may install withdll as a **Image File Execution Options debugger** for a given executable, which would allow you to inject a DLL (or DLLs) on every application launch. The **--debug** option is required for this to work so please make sure you add it, for example: + +``` +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\winver.exe] +"Debugger"="c:\\tools\\withdll.exe --debug -d c:\\tools\\trcapi64.dll" +``` diff --git a/build-all.bat b/build-all.bat new file mode 100644 index 0000000..cd29c26 --- /dev/null +++ b/build-all.bat @@ -0,0 +1,48 @@ +@echo off + +set "blddir=%~d0%~p0" + +if not exist "%blddir%detours\src" ( + pushd "%blddir%detours" + git submodule update --init detours + if errorlevel 1 exit /b 1 + popd +) + +set "bldconf=%1" + +echo "Building detours and detours.dll" +cmd /c %blddir%build-detours.bat x86 %bldconf% +if errorlevel 1 exit /b 2 + +cmd /c %blddir%build-detours.bat x64 %bldconf% +if errorlevel 1 exit /b 3 + +echo "Patching Detours header for metadata generation" +powershell -Command "@(Get-Content '%blddir%\detours\src\detours.h')[0..866+920..1234] | Set-Content '%blddir%\detours\include\detours.h'" + +echo "Building Detours metadata" +pushd "%blddir%detours-meta" +dotnet clean +dotnet build /p:BuildConfig="%bldconf%" +if errorlevel 1 ( + popd + exit /b 3 +) +popd + +echo "Building withdll" + +if /I "%bldconf%" == "Debug" ( + set withdllConfig=Debug +) else ( + set withdllConfig=Release +) + +pushd "%blddir%withdll" +dotnet publish -r win-x64 -c %withdllConfig% -o "%blddir%withdll\bin.x64%bldconf%" +if errorlevel 1 ( + popd + exit /b 3 +) +popd diff --git a/build-detours.bat b/build-detours.bat new file mode 100644 index 0000000..402a96b --- /dev/null +++ b/build-detours.bat @@ -0,0 +1,54 @@ +@echo off + +if "%1" == "" ( + set bldarch=x64 +) else ( + set "bldarch=%1" +) + +if /I "%2" == "DEBUG" ( + set DETOURS_CONFIG=Debug +) else ( + set DETOURS_CONFIG= +) + +echo "Building detours (%bldarch%%DETOURS_CONFIG%)" + +set "currdir=%~d0%~p0" + +if EXIST "c:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" ( + call "c:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" %bldarch% +) else ( + call "c:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" %bldarch% +) +if errorlevel 1 exit /b 1 + +pushd "%currdir%detours" +nmake +if errorlevel 1 ( + popd + exit /b 2 +) +popd + +pushd "%currdir%detours-dll" + +if "%DETOURS_CONFIG%" == "Debug" ( + mkdir "bin.%bldarch%%DETOURS_CONFIG%" + cd "bin.%bldarch%%DETOURS_CONFIG%" + + cl.exe /nologo /LD /TP /DUNICODE /DWIN32 /D_WINDOWS /EHsc /W4 /WX /Zi /Ob0 /Od /RTC1 /Fodetours.obj /Fddetours.pdb ..\detours.cpp ^ + /link /def:..\detours.def "%currdir%detours\lib.%bldarch%%DETOURS_CONFIG%\detours.lib" +) else ( + mkdir "bin.%bldarch%" + cd "bin.%bldarch%" + + cl.exe /nologo /LD /TP /DUNICODE /DWIN32 /D_WINDOWS /EHsc /W4 /WX /Zi /O2 /Ob1 /DNDEBUG /Fodetours.obj /Fddetours.pdb ..\detours.cpp ^ + /link /def:..\detours.def "%currdir%detours\lib.%bldarch%\detours.lib" +) +if errorlevel 1 ( + popd + exit /b 2 +) + +popd diff --git a/detours b/detours new file mode 160000 index 0000000..734ac64 --- /dev/null +++ b/detours @@ -0,0 +1 @@ +Subproject commit 734ac64899c44933151c1335f6ef54a590219221 diff --git a/detours-dll/detours.cpp b/detours-dll/detours.cpp new file mode 100644 index 0000000..1a97e05 --- /dev/null +++ b/detours-dll/detours.cpp @@ -0,0 +1,16 @@ +#include + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { + UNREFERENCED_PARAMETER(lpReserved); + + switch (ul_reason_for_call) { + case DLL_PROCESS_ATTACH: + ::DisableThreadLibraryCalls(hModule); + break; + case DLL_PROCESS_DETACH: + break; + default: + break; + } + return TRUE; +} diff --git a/detours-dll/detours.def b/detours-dll/detours.def new file mode 100644 index 0000000..2cd9253 --- /dev/null +++ b/detours-dll/detours.def @@ -0,0 +1,20 @@ +LIBRARY detours +EXPORTS + + DetourTransactionBegin + DetourTransactionAbort + DetourTransactionCommit + DetourTransactionCommitEx + DetourUpdateThread + DetourAttach + DetourDetach + DetourCreateProcessWithDllExW + DetourCreateProcessWithDllsW + DetourFinishHelperProcess + DetourGetContainingModule + DetourEnumerateModules + DetourGetEntryPoint + DetourGetModuleSize + DetourEnumerateExports + DetourEnumerateImports + DetourEnumerateImportsEx diff --git a/detours-meta/generate.proj b/detours-meta/generate.proj new file mode 100644 index 0000000..64d4455 --- /dev/null +++ b/detours-meta/generate.proj @@ -0,0 +1,21 @@ + + + + winmd/detours.winmd + 0.1.0.0 + + + + + + + detours=detours + + + + @(Headers) + Microsoft.Detours + true + + + \ No newline at end of file diff --git a/detours-meta/main.cpp b/detours-meta/main.cpp new file mode 100644 index 0000000..86ddc05 --- /dev/null +++ b/detours-meta/main.cpp @@ -0,0 +1,2 @@ +#include +#include "detours.h" diff --git a/withdll/DllInjection.cs b/withdll/DllInjection.cs new file mode 100644 index 0000000..64c2f60 --- /dev/null +++ b/withdll/DllInjection.cs @@ -0,0 +1,301 @@ +using Microsoft.Win32.SafeHandles; +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.System.Memory; +using Windows.Win32.System.SystemServices; +using Windows.Win32.System.Threading; +using WinProcessModule = System.Diagnostics.ProcessModule; +using PInvokeDetours = Microsoft.Detours.PInvoke; +using PInvokeWin32 = Windows.Win32.PInvoke; + + +namespace withdll +{ + internal static class DllInjection + { + private delegate int QueueApcThread(nint ThreadHandle, nint ApcRoutine, nint ApcArgument1, nint ApcArgument2, nint ApcArgument3); + + private static uint GetModuleExportOffset(string modulePath, string procedureName) + { + using var pereader = new PEReader(File.OpenRead(modulePath)); + + var exportsDirEntry = pereader.PEHeaders.PEHeader!.ExportTableDirectory; + + unsafe + { + var exportsDir = (IMAGE_EXPORT_DIRECTORY*)pereader.GetSectionData(exportsDirEntry.RelativeVirtualAddress).Pointer; + + var functionNamesRvas = new Span(pereader.GetSectionData((int)exportsDir->AddressOfNames).Pointer, + (int)exportsDir->NumberOfNames); + var functionNamesOrdinals = new Span(pereader.GetSectionData((int)exportsDir->AddressOfNameOrdinals).Pointer, + (int)exportsDir->NumberOfNames); + var addressOfFunctions = pereader.GetSectionData((int)exportsDir->AddressOfFunctions).Pointer; + + for (int i = 0; i < functionNamesRvas.Length; i++) + { + var name = Marshal.PtrToStringAnsi((nint)pereader.GetSectionData((int)functionNamesRvas[i]).Pointer); + var index = functionNamesOrdinals[i]; + + if (name == procedureName) + { + return *(uint*)(addressOfFunctions + index * sizeof(uint)); + } + } + } + + return 0; + } + + private static bool Is32BitModule(string modulePath) + { + using var pereader = new PEReader(File.OpenRead(modulePath)); + + return pereader.PEHeaders.PEHeader!.Magic == PEMagic.PE32; + } + + private static bool ExportsOrdinal1(string modulePath) + { + using var pereader = new PEReader(File.OpenRead(modulePath)); + + var exportsDirEntry = pereader.PEHeaders.PEHeader!.ExportTableDirectory; + unsafe + { + var exportsDir = (IMAGE_EXPORT_DIRECTORY*)pereader.GetSectionData(exportsDirEntry.RelativeVirtualAddress).Pointer; + + if (exportsDir->Base == 1) + { + return ((uint*)pereader.GetSectionData((int)exportsDir->AddressOfFunctions).Pointer)[0] != 0; + } + + return false; + } + } + + public static void StartProcessWithDlls(List cmdlineArgs, bool debug, List dllPaths) + { + static char[] ConvertStringListToNullTerminatedArray(IList strings) + { + var chars = new List(strings.Select(a => a.Length + 3 /* two apostrophes and space */).Sum()); + foreach (var s in strings) + { + chars.Add('\"'); + chars.AddRange(s); + chars.Add('\"'); + chars.Add(' '); + } + chars[^1] = '\0'; + return chars.ToArray(); + } + + if (!dllPaths.All(path => ExportsOrdinal1(path))) + { + throw new ArgumentException("All DLLs must export the ordinal #1 function"); + } + + unsafe + { + var startupInfo = new STARTUPINFOW() { cb = (uint)sizeof(STARTUPINFOW) }; + + var cmdline = new Span(ConvertStringListToNullTerminatedArray(cmdlineArgs)); + uint createFlags = debug ? (uint)PROCESS_CREATION_FLAGS.DEBUG_ONLY_THIS_PROCESS : 0; + + var pcstrs = dllPaths.Select(path => new PCSTR((byte*)Marshal.StringToHGlobalAnsi(path))).ToArray(); + + try + { + if (!PInvokeDetours.DetourCreateProcessWithDlls(null, ref cmdline, null, null, false, + createFlags, null, null, startupInfo, out var processInfo, + pcstrs, null)) + { + throw new Win32Exception(); + } + + PInvokeWin32.CloseHandle(processInfo.hThread); + PInvokeWin32.CloseHandle(processInfo.hProcess); + + if (debug) + { + PInvokeWin32.DebugActiveProcessStop(processInfo.dwProcessId); + } + } + finally + { + Array.ForEach(pcstrs, pcstr => Marshal.FreeHGlobal((nint)pcstr.Value)); + } + } + } + + public static void InjectDllsIntoRunningProcess(int pid, List dllPaths) + { + using var remoteProcess = Process.GetProcessById(pid); + + var isWow64Used = Environment.Is64BitProcess && PInvokeWin32.IsWow64Process(remoteProcess.SafeHandle, out var f) && f; + + var updateDllPathIfNeeded = (string dllName) => + { + if (isWow64Used && dllName.EndsWith("64.dll", StringComparison.OrdinalIgnoreCase)) + { + var newDllName = dllName.Substring(0, dllName.Length - 6) + "32.dll"; + return (Path.Exists(newDllName) && Is32BitModule(newDllName)) ? newDllName : dllName; + } + return dllName; + }; + + var remoteNtdllAddress = remoteProcess.Modules.Cast().First( + m => string.Equals(m.ModuleName, "ntdll.dll", StringComparison.OrdinalIgnoreCase)).BaseAddress; + var remoteKernel32Address = remoteProcess.Modules.Cast().First( + m => string.Equals(m.ModuleName, "kernel32.dll", StringComparison.OrdinalIgnoreCase)).BaseAddress; + + QueueApcThread queueApcThreadFunc = isWow64Used ? Imports.RtlQueueApcWow64Thread : Imports.NtQueueApcThread; + + var systemFolderPath = isWow64Used ? Environment.GetFolderPath(Environment.SpecialFolder.SystemX86) : + Environment.GetFolderPath(Environment.SpecialFolder.System); + + var fnRtlExitUserThread = GetModuleExportOffset(Path.Combine(systemFolderPath, "ntdll.dll"), "RtlExitUserThread"); + var fnLoadLibraryW = GetModuleExportOffset(Path.Combine(systemFolderPath, "kernel32.dll"), "LoadLibraryW"); + + using var remoteProcessHandle = PInvokeWin32.OpenProcess_SafeHandle(PROCESS_ACCESS_RIGHTS.PROCESS_CREATE_THREAD | + PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION | PROCESS_ACCESS_RIGHTS.PROCESS_VM_OPERATION | + PROCESS_ACCESS_RIGHTS.PROCESS_VM_WRITE | PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ, false, (uint)remoteProcess.Id); + + var remoteThreadStart = remoteNtdllAddress + (nint)fnRtlExitUserThread; + + if (Imports.RtlCreateUserThread(remoteProcessHandle.DangerousGetHandle(), nint.Zero, true, 0, 0, 0, remoteThreadStart, + nint.Zero, out var remoteThreadHandle, out _) is var status && status != 0) + { + throw new Win32Exception((int)PInvokeWin32.RtlNtStatusToDosError(new NTSTATUS(status))); + } + + try + { + unsafe + { + var allocLength = (nuint)dllPaths.Select(p => (p.Length + 1 /* +1 for null terminator */) * sizeof(char)).Sum(); + var allocAddr = PInvokeWin32.VirtualAllocEx(remoteProcessHandle, null, allocLength, + VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE | VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT, PAGE_PROTECTION_FLAGS.PAGE_READWRITE); + if (allocAddr != null) + { + try + { + var fnLoadLibraryWAddr = (remoteKernel32Address + (nint)fnLoadLibraryW); + var addr = (char*)allocAddr; + foreach (var dllPath in dllPaths) + { + var updatedDllPath = updateDllPathIfNeeded(dllPath); + fixed (void* dllPathPtr = updatedDllPath) + { + // VirtualAllocEx initializes memory to 0 so we don't need to write the null terminator + if (!PInvokeWin32.WriteProcessMemory(remoteProcessHandle, allocAddr, dllPathPtr, + (nuint)(dllPath.Length * sizeof(char)), null)) + { + throw new Win32Exception(); + } + } + + status = queueApcThreadFunc(remoteThreadHandle.DangerousGetHandle(), + fnLoadLibraryWAddr, (nint)addr, nint.Zero, nint.Zero); + if (status != 0) + { + throw new Win32Exception((int)PInvokeWin32.RtlNtStatusToDosError(new NTSTATUS(status))); + } + + addr += dllPath.Length + 1; + } + + // APC is the first thing the new thread executes when resumed + if (PInvokeWin32.ResumeThread(remoteThreadHandle) < 0) + { + throw new Win32Exception(); + } + + if (PInvokeWin32.WaitForSingleObject(remoteThreadHandle, 5000) is var err && err == WAIT_EVENT.WAIT_TIMEOUT) + { + throw new Win32Exception((int)WIN32_ERROR.ERROR_TIMEOUT); + } + else if (err == WAIT_EVENT.WAIT_FAILED) + { + throw new Win32Exception(); + } + } + finally + { + PInvokeWin32.VirtualFreeEx(remoteProcessHandle, allocAddr, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE); + } + } + else + { + throw new Win32Exception(); + } + } + } + finally + { + remoteThreadHandle.Dispose(); + } + } + } + + /// + /// NT function definitions are modified (or not) versions + /// from https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools + /// + /// // Copyright 2019 Google Inc. All Rights Reserved. + /// + /// Licensed under the Apache License, Version 2.0 (the "License"); + /// you may not use this file except in compliance with the License. + /// You may obtain a copy of the License at + /// + /// http://www.apache.org/licenses/LICENSE-2.0 + /// + /// Unless required by applicable law or agreed to in writing, software + /// distributed under the License is distributed on an "AS IS" BASIS, + /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + /// See the License for the specific language governing permissions and + /// limitations under the License. + /// + internal static partial class Imports + { + [StructLayout(LayoutKind.Sequential)] + public struct CLIENT_ID + { + public IntPtr UniqueProcess; + public IntPtr UniqueThread; + } + + [LibraryImport("ntdll.dll")] + internal static partial int RtlCreateUserThread( + nint ProcessHandle, + nint ThreadSecurityDescriptor, + [MarshalAs(UnmanagedType.Bool)] + bool CreateSuspended, + uint ZeroBits, + nuint MaximumStackSize, + nuint CommittedStackSize, + nint StartAddress, + nint Parameter, + out SafeFileHandle ThreadHandle, + out CLIENT_ID ClientId + ); + + [LibraryImport("ntdll.dll")] + public static partial int NtQueueApcThread( + nint ThreadHandle, + nint ApcRoutine, + nint ApcArgument1, + nint ApcArgument2, + nint ApcArgument3 + ); + + [LibraryImport("ntdll.dll")] + internal static partial int RtlQueueApcWow64Thread( + nint ThreadHandle, + nint ApcRoutine, + nint ApcArgument1, + nint ApcArgument2, + nint ApcArgument3 + ); + } +} diff --git a/withdll/NativeMethods.txt b/withdll/NativeMethods.txt new file mode 100644 index 0000000..c976770 --- /dev/null +++ b/withdll/NativeMethods.txt @@ -0,0 +1,23 @@ +// Windows +CreateProcess +OpenProcess +VirtualAllocEx +VirtualFreeEx +WriteProcessMemory +ReadProcessMemory +IsWow64Process +ResumeThread +WaitForSingleObject +RtlNtStatusToDosError +DebugSetProcessKillOnExit +DebugActiveProcessStop + +WIN32_ERROR +NTSTATUS +IMAGE_EXPORT_DIRECTORY +DBG_EXCEPTION_NOT_HANDLED +DBG_CONTINUE +MAX_PATH + +// Detours +DetourCreateProcessWithDllsW diff --git a/withdll/Program.cs b/withdll/Program.cs new file mode 100644 index 0000000..6e001ce --- /dev/null +++ b/withdll/Program.cs @@ -0,0 +1,108 @@ +using System.Diagnostics; +using withdll; + +public static class Program +{ + public static int Main(string[] args) + { + var parsedArgs = ParseArgs(["h", "help", "debug"], args); + + int pid = 0; + if (parsedArgs.ContainsKey("") && parsedArgs[""].Count == 1) + { + int.TryParse(parsedArgs[""][0], out pid); + } + + if (parsedArgs.ContainsKey("h") || parsedArgs.ContainsKey("help") || + !parsedArgs.ContainsKey("") || parsedArgs[""].Count == 0) + { + ShowHelp(); + return 1; + } + + var dllpaths = (parsedArgs.TryGetValue("d", out var d1) ? d1 : new List(0)).Union( + parsedArgs.TryGetValue("dll", out var d2) ? d2 : new List(0)).ToList(); + + try + { + if (pid > 0) + { + DllInjection.InjectDllsIntoRunningProcess(pid, dllpaths); + } + else + { + DllInjection.StartProcessWithDlls(parsedArgs[""], parsedArgs.ContainsKey("debug"), dllpaths); + } + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex}"); + return 1; + } + } + + private static Dictionary> ParseArgs(string[] flagNames, string[] rawArgs) + { + var args = rawArgs.SelectMany(arg => arg.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + bool IsFlag(string v) => Array.IndexOf(flagNames, v) >= 0; + + var result = new Dictionary>(StringComparer.Ordinal); + var lastOption = ""; + var firstFreeArgPassed = false; + + foreach (var arg in args) + { + if (!firstFreeArgPassed && arg.StartsWith("-", StringComparison.Ordinal)) + { + var option = arg.TrimStart('-'); + if (IsFlag(option)) + { + Debug.Assert(lastOption == ""); + result[option] = new(); + } + else + { + Debug.Assert(lastOption == ""); + lastOption = option; + } + } + else + { + // the logic is the same for options (lastOption) and free args + if (result.TryGetValue(lastOption, out var values)) + { + values.Add(arg); + } + else + { + result[lastOption] = new List { arg }; + } + firstFreeArgPassed = lastOption == ""; + lastOption = ""; + } + } + return result; + } + + + private static void ShowHelp() + { + Console.WriteLine($"withdll - injects DLL(s) into a process"); + Console.WriteLine(""" +Copyright (C) 2023 Sebastian Solnica (https://wtrace.net) + +withdll [options] + +Options: + -h, -help Show this help screen + -d, --dll A DLL to inject into the target process (can be set multiple times) + +Examples: + + withdll -d c:\temp\mydll1.dll -d c:\temp\mydll2.dll notepad.exe test.txt + withdll -d c:\temp\mydll.dll 1234 +"""); + } +} + diff --git a/withdll/withdll.csproj b/withdll/withdll.csproj new file mode 100644 index 0000000..8127c55 --- /dev/null +++ b/withdll/withdll.csproj @@ -0,0 +1,27 @@ + + + + Exe + net8.0-windows + 2023 Sebastian Solnica + 1.0.0.0 + enable + enable + true + true + + + + + + + + + + + + all + + + +