├── sample-config.ini ├── Injector ├── Injector.csproj ├── Logger.cs ├── Program.cs ├── InjectorOptions.cs └── Injector.cs ├── CHANGELOG.md ├── LICENSE ├── Injector.sln ├── README.md ├── .gitlab-ci.yml └── .gitignore /sample-config.ini: -------------------------------------------------------------------------------- 1 | ;All keys are optional. Defaults will be used if available 2 | ;Uncomment keys you want to use (remove the semi-colon) 3 | 4 | [Config] 5 | ;pid=1234 6 | ;process=program.exe 7 | ;start=launcher.exe 8 | ;process-restarts=false 9 | ;win=false 10 | ;delay=5000 11 | ;wait-for-dlls=required 12 | ;multi-dll-delay=1000 13 | ;timeout=10000 14 | ;quiet=false 15 | ;interactive=false 16 | ;verbose=false 17 | ;no-pause-on-error=false 18 | ;dlls=some_key another_key path/to/dll 19 | 20 | [DLL] 21 | ;some_key=path/to/dll 22 | ;another_key=path/to/dll 23 | ;required=path/to/required/dll -------------------------------------------------------------------------------- /Injector/Injector.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | Injector.Program 7 | 8 | 9 | Injector 10 | 11 | LICENSE 12 | https://github.com/turkoid/Injector 13 | Inject dlls into processes 14 | AnyCPU;x64 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.4.0 - 2020-11-22 4 | 5 | ### Changes 6 | 7 | - `-c/--config` will default to `config.ini` or `config/*.ini` located in the same directory as the executable 8 | 9 | ## v1.3.0 - 2020-01-09 10 | 11 | ### Changes 12 | 13 | - Injector will no pause on errors by default. Use new CLI option `--no-pause-on-error` to override this behavior 14 | 15 | ## v1.2.0 - 2020-01-06 16 | 17 | ### Added 18 | 19 | - New command line option `--process-restarts` that informs the injector the process restarts itself 20 | - New command line option `--wait-for-dlls`. Used to let the injector know to monitor for the specified dlls before attempting to inject 21 | - Injector will try its best to detect when a process has initialized by monitoring DLLs loaded into the process 22 | 23 | ### Changes 24 | 25 | - Injector will detect invalid DLL paths sooner 26 | 27 | ## v1.1.0 - 2019-12-27 28 | 29 | ### Changes 30 | 31 | - Auto detects Microsoft store apps, so command line/config option is not explicitly needed 32 | 33 | ## v1.0.0 - 2019-12-11 34 | 35 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 turkoid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Injector.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29509.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Injector", "Injector\Injector.csproj", "{EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{12634420-DB3F-43E8-9273-D9B761064D59}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | .gitlab-ci.yml = .gitlab-ci.yml 12 | CHANGELOG.md = CHANGELOG.md 13 | LICENSE = LICENSE 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "configs", "configs", "{A7D16816-0B33-44E3-9BE7-C0D4C91B8795}" 18 | ProjectSection(SolutionItems) = preProject 19 | debug-configs\debug.ini = debug-configs\debug.ini 20 | sample-config.ini = sample-config.ini 21 | EndProjectSection 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Debug|x64 = Debug|x64 27 | Release|Any CPU = Release|Any CPU 28 | Release|x64 = Release|x64 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Debug|x64.ActiveCfg = Debug|x64 34 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Debug|x64.Build.0 = Debug|x64 35 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Release|x64.ActiveCfg = Release|x64 38 | {EB85EFDF-FA65-4130-98BA-0D3AB8ECEFED}.Release|x64.Build.0 = Release|x64 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | GlobalSection(NestedProjects) = preSolution 44 | {A7D16816-0B33-44E3-9BE7-C0D4C91B8795} = {12634420-DB3F-43E8-9273-D9B761064D59} 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {68C4B84D-5072-409E-860A-1A1893B648E4} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /Injector/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Injector 5 | { 6 | internal class Logger 7 | { 8 | public enum LoggingLevel 9 | { 10 | DEBUG, 11 | INFO, 12 | WARN, 13 | ERROR 14 | } 15 | 16 | private const string LOG_FILENAME = "injector.log"; 17 | private static Logger _logger; 18 | 19 | private Logger() 20 | { 21 | InternalLog(LoggingLevel.DEBUG, "Logger initialized", append: false); 22 | } 23 | 24 | public static Logger Instance() 25 | { 26 | if (_logger == null) 27 | { 28 | _logger = new Logger(); 29 | } 30 | return _logger; 31 | } 32 | 33 | private void InternalLog(LoggingLevel level, string message, bool quiet = false, bool append = true) 34 | { 35 | if (level == LoggingLevel.ERROR) 36 | { 37 | Console.Error.WriteLine(message); 38 | } 39 | else if ((Program.Options?.Verbose ?? false) || !quiet && level != LoggingLevel.DEBUG) 40 | { 41 | Console.WriteLine(message); 42 | } 43 | 44 | using (var w = new StreamWriter(LOG_FILENAME, append)) 45 | { 46 | w.WriteLine($"{DateTime.Now.ToString("s")} | {level.ToString("g")}: {message}"); 47 | } 48 | } 49 | 50 | public void Log(LoggingLevel level, string message, bool quiet) 51 | { 52 | InternalLog(level, message, quiet); 53 | } 54 | 55 | public void Log(LoggingLevel level, string message) 56 | { 57 | Log(level, message, Program.Options?.Quiet ?? false); 58 | } 59 | 60 | public void Debug(string message) 61 | { 62 | Log(LoggingLevel.DEBUG, message); 63 | } 64 | 65 | public void Info(string message, bool quiet) 66 | { 67 | Log(LoggingLevel.INFO, message, quiet); 68 | } 69 | 70 | public void Info(string message) 71 | { 72 | Info(message, Program.Options?.Quiet ?? false); 73 | } 74 | 75 | public void Warn(string message, bool quiet) 76 | { 77 | Log(LoggingLevel.WARN, message, quiet); 78 | } 79 | 80 | public void Warn(string message) 81 | { 82 | Warn(message, Program.Options?.Quiet ?? false); 83 | } 84 | 85 | public void Error(string message) 86 | { 87 | Log(LoggingLevel.ERROR, message); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /Injector/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Runtime.InteropServices; 5 | using CommandLine; 6 | using CommandLine.Text; 7 | 8 | namespace Injector 9 | { 10 | internal class Program 11 | { 12 | private static readonly Logger logger = Logger.Instance(); 13 | private static ParserResult parserResult; 14 | public static InjectorOptions Options { get; set; } 15 | 16 | private static void Main(string[] args) 17 | { 18 | // dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true 19 | try 20 | { 21 | var parser = new Parser(with => with.HelpWriter = null); 22 | parserResult = parser.ParseArguments(args); 23 | parserResult 24 | .WithParsed( 25 | opts => 26 | { 27 | Options = opts; 28 | logger.Debug($"args: {string.Join(' ', args)}"); 29 | opts.UpdateFromFile(); 30 | opts.Log(); 31 | opts.Validate(); 32 | 33 | var injector = new Injector(opts); 34 | injector.Inject(); 35 | } 36 | ) 37 | .WithNotParsed(errs => DisplayHelp(parserResult, errs)); 38 | } 39 | catch (Exception ex) 40 | { 41 | HandleException("An unknown error occurred. See log for details", ex); 42 | } 43 | 44 | WaitForUserInput(); 45 | } 46 | 47 | public static void DisplayHelp(ParserResult result, IEnumerable errs) 48 | { 49 | HelpText helpText = null; 50 | if (errs.IsVersion()) 51 | { 52 | helpText = HelpText.AutoBuild(result); 53 | } 54 | else 55 | { 56 | helpText = HelpText.AutoBuild( 57 | result, 58 | help => 59 | { 60 | help.AdditionalNewLineAfterOption = false; 61 | help.Copyright = ""; 62 | return HelpText.DefaultParsingErrorsHandler(result, help); 63 | }, 64 | e => e 65 | ); 66 | } 67 | 68 | if (errs.IsVersion() || errs.IsHelp()) 69 | { 70 | Console.WriteLine(helpText); 71 | } 72 | else 73 | { 74 | Console.Error.WriteLine(helpText); 75 | } 76 | } 77 | 78 | public static void WaitForUserInput(bool isError = false) 79 | { 80 | var interactive = Options?.Interactive ?? false; 81 | var noPauseOnError = Options?.NoPauseOnError ?? false; 82 | if (interactive || isError && !noPauseOnError) 83 | { 84 | Console.WriteLine("Press any key to exit..."); 85 | Console.ReadKey(true); 86 | } 87 | } 88 | 89 | private static void InternalErrorHandler(string errorMessage, string debugMessage = null) 90 | { 91 | logger.Error(errorMessage); 92 | if (!string.IsNullOrEmpty(debugMessage)) 93 | { 94 | logger.Debug(debugMessage); 95 | } 96 | 97 | logger.Info("See log for more details"); 98 | WaitForUserInput(true); 99 | logger.Debug("Exiting..."); 100 | Environment.Exit(1); 101 | } 102 | 103 | public static void HandleWin32Error(string errorMessage) 104 | { 105 | var exception = new Win32Exception(Marshal.GetLastWin32Error()); 106 | var debugMessage = $"Code: {exception.ErrorCode}, Message: {exception.Message}"; 107 | InternalErrorHandler(errorMessage, debugMessage); 108 | } 109 | 110 | public static void HandleError(string errorMessage) 111 | { 112 | InternalErrorHandler(errorMessage); 113 | } 114 | 115 | public static void HandleException(string errorMessage, Exception ex) 116 | { 117 | var debugMessage = ex.ToString(); 118 | InternalErrorHandler(errorMessage, debugMessage); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Injector 2 | 3 | [![Build status](https://img.shields.io/gitlab/pipeline/turkoid/Injector/master)](https://gitlab.com/turkoid/Injector/commits/master) 4 | 5 | Commandline tool that injects one or more DLLs into a process. Optionally also starts that process before injecting. 6 | 7 | ## Download 8 | 9 | Download latest official release [here](https://github.com/turkoid/Injector/releases/latest) 10 | 11 | Download latest dev build [here](https://gitlab.com/turkoid/Injector/pipelines/master/latest) 12 | 13 | ## Features 14 | 15 | * Completely command line driven. No GUI 16 | * Most command-line options supported in config file 17 | * Can inject multiple dlls with one call 18 | * Can be specified to start the process before injecting (Supports Microsoft Store apps too) 19 | * Delays can be adjusted to provide stable injections 20 | * Can wait for specific dlls to be loaded before injecting 21 | * Smart injection delay waits for all DLLs to load 22 | 23 | ## Command-line Options 24 | 25 | * `-p | --pid` 26 | 27 | The process id of the process to inject into 28 | 29 | * `-x | --process` 30 | 31 | The process name of the process to inject into. Alternatively, can be the name of the true process after using `--start` 32 | 33 | * `-s | --start` 34 | 35 | The path to process to start. Supports Microsoft store apps (see below) 36 | 37 | * `--process-restarts` 38 | 39 | If passed, then the injector will explicily see if the process restarts itself. Explicitly passing this saves some time, but the injector can autodetect this 40 | 41 | * `-w | --win` 42 | 43 | If passed, then the process to start is a Microsoft store app (Default: `false`) 44 | 45 | * `-d | --delay` 46 | 47 | Specifies the delay, in milliseconds, after the process has started and initialization has completed, before the injection process starts (Default: `5000`) 48 | 49 | * `--wait-for-dlls` 50 | 51 | DLLs that should be loaded before attempting to inject. Multiple DLLs should be separated by space. Full paths can be used or config keys (see below) 52 | 53 | * `-m | --multi-dll-delay` 54 | 55 | Specifies the delay, in milliseconds, betweeen injecting multiple delays (Default: `1000`) 56 | 57 | * `-t | --timeout` 58 | 59 | Specifies the timeout, in milliseconds, when attempting to find the process by name (Default: `30000`) 60 | 61 | * `-c | --config` 62 | 63 | Path to the config file. Options defined in the config file override command-line args (Default: `config.ini` or `config/*.ini`) 64 | 65 | * `-q | --quiet` 66 | 67 | If passed, then only errors are printed to the console. Everything is still printed to the log file (Default: `false`) 68 | 69 | * `-i | --interactive` 70 | 71 | If passed, then the program will pause before exiting, including errors (Default: `false`) 72 | 73 | * `-e | --no-pause-on-error` 74 | 75 | If passed, then the program will not pause on errors (Default: `false`) 76 | 77 | * `-v | --verbose` 78 | 79 | If passed, then all messages, including debug messages, will be printed to the console (Default: `false`) 80 | 81 | * `DLLS (pos. 0)` 82 | 83 | DLLs to inject into the process. Multiple DLLs should be separated by space. Full paths can be used or config keys (see below) 84 | 85 | 86 | ## Microsoft Store Apps 87 | 88 | To start a Microsoft Store App, you need to use a specific format. Follow the instructions on the microsoft community site [here](https://answers.microsoft.com/en-us/windows/forum/windows_10-windows_store/starting-windows-10-store-app-from-the-command/836354c5-b5af-4d6c-b414-80e40ed14675?auth=1). 89 | 90 | Use the `PackageFamilyName!Applicationid`. Don't include the `explorer.exe shell:appsFolder\`. So from the article, you would pass `Microsoft.BingWeather_8wekyb3d8bbwe!App`. 91 | 92 | The tool is smart enough to detect if you are you are trying to start a Microsoft app by detecting an exclamation point. So passing `-w | --win` or `win=true` in the config file, is not necessary. However, if for some reason you had a file named exactly like the store in the injector's working directory, then you would need to specicy the command line option. 93 | 94 | ## Config File 95 | 96 | All options can be defined using an INI config file. Allows you to easily switch between configs. Values in the file override those passed in the command line. 97 | 98 | ``` 99 | [Config] 100 | pid=1234 101 | process=program.exe 102 | start=launcher.exe 103 | process-restarts=false 104 | win=false 105 | delay=5 106 | wait-for-dlls=required_dll 107 | multi-dll-delay=1 108 | timeout=3 109 | quiet=false 110 | interactive=false 111 | verbose=false 112 | dlls=some_key another_key path\to\dll 113 | 114 | [DLL] 115 | some_key=path\to\dll 116 | another_key=path\to\another\dll 117 | required_dll=path\to\required\dll 118 | ``` 119 | 120 | You can specify easy to use keys instead of the the full paths to the DLLs when using the command line or config file. You can mix keys and paths, as well. 121 | 122 | Note: If no config file is passed as an argument, the injector will default to `config.ini` in the working directory or the first `*.ini` file located in the `config` subfolder. 123 | 124 | ## Troubleshooting 125 | 126 | **It keeps crashing!** 127 | 128 | You might need to adjust delays. Try and use the injector after starting the program manually and waiting till you feel the program is in a state that it has fully initialized (The injector is smart enough to not start the process, if specified, when it's already running). 129 | 130 | If the injector works, then adjust the delays. The most effective one is `delay` 131 | 132 | If the injector doesn't work, sometimes it helps to adjust the order of the injection, if using more than one DLL. 133 | 134 | Unfortunately, if the injection still fails, but works with other injection utilities, then open an issue and I'll try and take a look at it. 135 | 136 | **I specified a Microsoft store app, but it doesn't start** 137 | 138 | Make sure the use the format specified above. 139 | 140 | ## Disclaimer 141 | 142 | Use this tool at your own risk. I am not responsible if you inject unsafe DLLs or use this for malicious purposes. This was originally developed to make an easy to use launcher for injecting safe dlls into "The Outer Worlds" game. Any games that use anti-cheat systems, can and probably will detect this common method of injection. 143 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - deploy 4 | 5 | variables: &global_variables 6 | VERSION: 1.4.0 7 | GITHUB_API_URL: https://api.github.com 8 | GITLAB_API_URL: https://gitlab.com/api/v4 9 | INJECTOR_PROJECT_ID: 16080289 10 | INJECTOR_CONFIG_PROJECT_ID: 16080290 11 | 12 | .build: 13 | stage: build 14 | image: mcr.microsoft.com/dotnet/core/sdk:3.1-alpine 15 | before_script: &build_before_script 16 | - apk update 17 | - apk add zip 18 | script: 19 | - "dotnet publish -r win-x64 -c Release -o publish /p:PublishSingleFile=true /p:PublishWithAspNetCoreTargetManifest=false" 20 | - BUILD_METADATA_PART=$([ -n "$BUILD_METADATA" ] && echo "+$BUILD_METADATA" || echo "") 21 | - ARCHIVE_NAME=injector-$ARCHIVE_VERSION-win64$BUILD_METADATA_PART.zip 22 | - "zip -j $ARCHIVE_NAME LICENSE README.md CHANGELOG.md sample-config.ini publish/Injector.exe" 23 | - "echo download: $GITLAB_API_URL/projects/$INJECTOR_PROJECT_ID/jobs/artifacts/$CI_COMMIT_REF_NAME/raw/$ARCHIVE_NAME?job=$CI_JOB_NAME" 24 | variables: &build_variables 25 | <<: *global_variables 26 | artifacts: 27 | paths: 28 | - injector-*.zip 29 | 30 | master: 31 | extends: .build 32 | only: 33 | - master 34 | before_script: 35 | - *build_before_script 36 | - ARCHIVE_VERSION="$VERSION" 37 | - BUILD_METADATA="$CI_JOB_ID" 38 | variables: 39 | <<: *build_variables 40 | artifacts: 41 | expire_in: 6 months 42 | 43 | dev: 44 | extends: .build 45 | only: 46 | - branches 47 | except: 48 | refs: 49 | - master 50 | variables: 51 | - $CI_COMMIT_TAG =~ /v\d+\.\d+\.\d+/ 52 | before_script: 53 | - *build_before_script 54 | - ARCHIVE_VERSION="$CI_COMMIT_REF_SLUG" 55 | - BUILD_METADATA="$CI_JOB_ID" 56 | variables: 57 | <<: *build_variables 58 | artifacts: 59 | expire_in: 1 day 60 | 61 | release: 62 | extends: .build 63 | only: 64 | variables: 65 | - $CI_COMMIT_TAG =~ /v\d+\.\d+\.\d+/ 66 | before_script: 67 | - *build_before_script 68 | - ARCHIVE_VERSION="${CI_COMMIT_TAG:1}" 69 | - BUILD_METADATA= 70 | variables: 71 | <<: *build_variables 72 | artifacts: 73 | expire_in: 1 month 74 | 75 | .gen_changelog_fragment: &gen_changelog_fragment 76 | - | 77 | # Parse changelog 78 | # 79 | fragment_start="## $CI_COMMIT_TAG" 80 | fragment_end="## v" 81 | sed -n "/^$fragment_start/,/^$fragment_end/ {/^$fragment_start/ {p;n}; /^$fragment_end/ q; p}" CHANGELOG.md >changelog_fragment 82 | cat changelog_fragment 83 | 84 | .check_release: &check_release 85 | - | 86 | # Check if release already exists 87 | # 88 | url=$GITHUB_API_URL/repos/turkoid/$CI_PROJECT_NAME/releases/tags/$CI_COMMIT_TAG 89 | headers='-H "Accept: application/vnd.github.v3+json"' 90 | opts='-s -w "%{http_code}" -o response' 91 | echo curl $opts $headers $url 92 | rc=$(eval curl $opts $headers $url) 93 | echo http_code=$rc 94 | if [ $rc -eq 200 ]; then 95 | release_id=$(cat response | jq -r '.id // empty') 96 | upload_url=$(cat response | jq -r '.upload_url // empty') 97 | upload_url=${upload_url%\{*} 98 | download_url=$(cat response | jq -r '.assets[0].browser_download_url // empty') 99 | echo Release already exists 100 | elif [ $rc -eq 404 ]; then 101 | release_id= 102 | upload_url= 103 | download_url= 104 | else 105 | cat response | jq . 106 | return 1 107 | fi 108 | 109 | .create_github_release: &update_github_release 110 | - | 111 | # Create release 112 | # 113 | if [ -z "$upload_url" ]; then 114 | url=$GITHUB_API_URL/repos/turkoid/$CI_PROJECT_NAME/releases 115 | headers='-H "Accept: application/vnd.github.v3+json"' 116 | headers=$headers' -H "Authorization: token $GITHUB_API_TOKEN"' 117 | data=$(jq -n -c --arg tag_name "$CI_COMMIT_TAG" --arg body "$(cat changelog_fragment)" '{"tag_name": $tag_name, "body": $body}') 118 | opts='-s -w "%{http_code}" -o response' 119 | echo curl $opts $headers --data "'$data'" $url 120 | rc=$(eval curl $opts $headers --data "'$data'" $url) 121 | echo http_code=$rc 122 | if [ $rc -ne 201 ]; then 123 | cat response | jq . 124 | return 1 125 | fi 126 | release_id=$(cat response | jq -r '.id // empty') 127 | upload_url=$(cat response | jq -r '.upload_url // empty') 128 | upload_url=${upload_url%\{*} 129 | fi 130 | echo release_id=$release_id 131 | echo upload_url=$upload_url 132 | - | 133 | # Sanity check 134 | # 135 | if [ -z "$release_id" ] || [ -z "$upload_url" ]; then 136 | echo "Something went wrong" 137 | return 1 138 | fi 139 | 140 | .upload_release_artifacts: &upload_release_artifacts 141 | - | 142 | # Upload release artifacts 143 | # 144 | if [ -z "$download_url" ]; then 145 | url=$upload_url?name=$ARCHIVE_NAME 146 | headers='-H "Accept: application/vnd.github.v3+json"' 147 | headers=$headers' -H "Authorization: token $GITHUB_API_TOKEN"' 148 | headers=$headers' -H "Content-Type: application/zip"' 149 | opts='-s -w "%{http_code}" -o response' 150 | echo curl $opts $headers --data-binary "@$ARCHIVE_NAME" $url 151 | rc=$(eval curl $opts $headers --data-binary "@$ARCHIVE_NAME" $url) 152 | echo http_code=$rc 153 | if [ $rc -ne 201 ]; then 154 | cat response | jq . 155 | return 1 156 | fi 157 | download_url=$(cat response | jq -r '.browser_download_url // empty') 158 | fi 159 | echo download_url=$download_url 160 | - | 161 | # Sanity check 162 | # 163 | if [ -z "$download_url" ]; then 164 | echo "Something went wrong" 165 | return 1 166 | fi 167 | 168 | .deploy: 169 | stage: deploy 170 | image: alpine:latest 171 | only: 172 | variables: 173 | - $CI_COMMIT_TAG =~ /v\d+\.\d+\.\d+/ 174 | before_script: 175 | - | 176 | # Verify artifacts 177 | # 178 | VERSION=${CI_COMMIT_TAG:1} 179 | ARCHIVE_NAME=$(ls -1 | grep -m1 injector-$VERSION.*.zip || true) 180 | if [ ! -f "$ARCHIVE_NAME" ]; then 181 | ls -1 182 | echo 'ERROR: injector-$VERSION.*.zip missing' 183 | return 1 184 | fi 185 | - apk update 186 | - apk add jq curl 187 | variables: &deploy_variables 188 | <<: *global_variables 189 | 190 | github_release: 191 | extends: .deploy 192 | script: 193 | - *gen_changelog_fragment 194 | - *check_release 195 | - *update_github_release 196 | - *upload_release_artifacts 197 | variables: 198 | <<: *deploy_variables 199 | 200 | injector_file_bundles: 201 | extends: .deploy 202 | stage: deploy 203 | script: 204 | - | 205 | # Trigger downstream pipeline 206 | # 207 | url=$GITLAB_API_URL/projects/$INJECTOR_CONFIG_PROJECT_ID/trigger/pipeline 208 | form_data='-F "token=$CI_JOB_TOKEN"' 209 | form_data=$form_data' -F "ref=master"' 210 | form_data=$form_data' -F "variables[INJECTOR_RELEASE_TAG]=$CI_COMMIT_TAG"' 211 | opts='-s -w "%{http_code}" -o response -X POST' 212 | echo curl $opts $form_data $url 213 | rc=$(eval curl $opts $form_data $url) 214 | echo http_code=$rc 215 | if [ $rc -ne 201 ]; then 216 | cat response | jq . 217 | return 1 218 | fi 219 | variables: 220 | <<: *deploy_variables 221 | GIT_STRATEGY: none 222 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/csharp,visualstudio 3 | # Edit at https://www.gitignore.io/?templates=csharp,visualstudio 4 | 5 | ### Csharp ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # JustCode is a .NET coding add-in 135 | .JustCode 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Visual Studio code coverage results 148 | *.coverage 149 | *.coveragexml 150 | 151 | # NCrunch 152 | _NCrunch_* 153 | .*crunch*.local.xml 154 | nCrunchTemp_* 155 | 156 | # MightyMoose 157 | *.mm.* 158 | AutoTest.Net/ 159 | 160 | # Web workbench (sass) 161 | .sass-cache/ 162 | 163 | # Installshield output folder 164 | [Ee]xpress/ 165 | 166 | # DocProject is a documentation generator add-in 167 | DocProject/buildhelp/ 168 | DocProject/Help/*.HxT 169 | DocProject/Help/*.HxC 170 | DocProject/Help/*.hhc 171 | DocProject/Help/*.hhk 172 | DocProject/Help/*.hhp 173 | DocProject/Help/Html2 174 | DocProject/Help/html 175 | 176 | # Click-Once directory 177 | publish/ 178 | 179 | # Publish Web Output 180 | *.[Pp]ublish.xml 181 | *.azurePubxml 182 | # Note: Comment the next line if you want to checkin your web deploy settings, 183 | # but database connection strings (with potential passwords) will be unencrypted 184 | *.pubxml 185 | *.publishproj 186 | 187 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 188 | # checkin your Azure Web App publish settings, but sensitive information contained 189 | # in these scripts will be unencrypted 190 | PublishScripts/ 191 | 192 | # NuGet Packages 193 | *.nupkg 194 | # NuGet Symbol Packages 195 | *.snupkg 196 | # The packages folder can be ignored because of Package Restore 197 | **/[Pp]ackages/* 198 | # except build/, which is used as an MSBuild target. 199 | !**/[Pp]ackages/build/ 200 | # Uncomment if necessary however generally it will be regenerated when needed 201 | #!**/[Pp]ackages/repositories.config 202 | # NuGet v3's project.json files produces more ignorable files 203 | *.nuget.props 204 | *.nuget.targets 205 | 206 | # Microsoft Azure Build Output 207 | csx/ 208 | *.build.csdef 209 | 210 | # Microsoft Azure Emulator 211 | ecf/ 212 | rcf/ 213 | 214 | # Windows Store app package directories and files 215 | AppPackages/ 216 | BundleArtifacts/ 217 | Package.StoreAssociation.xml 218 | _pkginfo.txt 219 | *.appx 220 | *.appxbundle 221 | *.appxupload 222 | 223 | # Visual Studio cache files 224 | # files ending in .cache can be ignored 225 | *.[Cc]ache 226 | # but keep track of directories ending in .cache 227 | !?*.[Cc]ache/ 228 | 229 | # Others 230 | ClientBin/ 231 | ~$* 232 | *~ 233 | *.dbmdl 234 | *.dbproj.schemaview 235 | *.jfm 236 | *.pfx 237 | *.publishsettings 238 | orleans.codegen.cs 239 | 240 | # Including strong name files can present a security risk 241 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 242 | #*.snk 243 | 244 | # Since there are multiple workflows, uncomment next line to ignore bower_components 245 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 246 | #bower_components/ 247 | 248 | # RIA/Silverlight projects 249 | Generated_Code/ 250 | 251 | # Backup & report files from converting an old project file 252 | # to a newer Visual Studio version. Backup files are not needed, 253 | # because we have git ;-) 254 | _UpgradeReport_Files/ 255 | Backup*/ 256 | UpgradeLog*.XML 257 | UpgradeLog*.htm 258 | ServiceFabricBackup/ 259 | *.rptproj.bak 260 | 261 | # SQL Server files 262 | *.mdf 263 | *.ldf 264 | *.ndf 265 | 266 | # Business Intelligence projects 267 | *.rdl.data 268 | *.bim.layout 269 | *.bim_*.settings 270 | *.rptproj.rsuser 271 | *- [Bb]ackup.rdl 272 | *- [Bb]ackup ([0-9]).rdl 273 | *- [Bb]ackup ([0-9][0-9]).rdl 274 | 275 | # Microsoft Fakes 276 | FakesAssemblies/ 277 | 278 | # GhostDoc plugin setting file 279 | *.GhostDoc.xml 280 | 281 | # Node.js Tools for Visual Studio 282 | .ntvs_analysis.dat 283 | node_modules/ 284 | 285 | # Visual Studio 6 build log 286 | *.plg 287 | 288 | # Visual Studio 6 workspace options file 289 | *.opt 290 | 291 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 292 | *.vbw 293 | 294 | # Visual Studio LightSwitch build output 295 | **/*.HTMLClient/GeneratedArtifacts 296 | **/*.DesktopClient/GeneratedArtifacts 297 | **/*.DesktopClient/ModelManifest.xml 298 | **/*.Server/GeneratedArtifacts 299 | **/*.Server/ModelManifest.xml 300 | _Pvt_Extensions 301 | 302 | # Paket dependency manager 303 | .paket/paket.exe 304 | paket-files/ 305 | 306 | # FAKE - F# Make 307 | .fake/ 308 | 309 | # CodeRush personal settings 310 | .cr/personal 311 | 312 | # Python Tools for Visual Studio (PTVS) 313 | __pycache__/ 314 | *.pyc 315 | 316 | # Cake - Uncomment if you are using it 317 | # tools/** 318 | # !tools/packages.config 319 | 320 | # Tabs Studio 321 | *.tss 322 | 323 | # Telerik's JustMock configuration file 324 | *.jmconfig 325 | 326 | # BizTalk build output 327 | *.btp.cs 328 | *.btm.cs 329 | *.odx.cs 330 | *.xsd.cs 331 | 332 | # OpenCover UI analysis results 333 | OpenCover/ 334 | 335 | # Azure Stream Analytics local run output 336 | ASALocalRun/ 337 | 338 | # MSBuild Binary and Structured Log 339 | *.binlog 340 | 341 | # NVidia Nsight GPU debugger configuration file 342 | *.nvuser 343 | 344 | # MFractors (Xamarin productivity tool) working folder 345 | .mfractor/ 346 | 347 | # Local History for Visual Studio 348 | .localhistory/ 349 | 350 | # BeatPulse healthcheck temp database 351 | healthchecksdb 352 | 353 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 354 | MigrationBackup/ 355 | 356 | ### VisualStudio ### 357 | 358 | # User-specific files 359 | launchSettings.json 360 | debug-configs/ 361 | 362 | # User-specific files (MonoDevelop/Xamarin Studio) 363 | 364 | # Mono auto generated files 365 | 366 | # Build results 367 | 368 | # Visual Studio 2015/2017 cache/options directory 369 | # Uncomment if you have tasks that create the project's static files in wwwroot 370 | 371 | # Visual Studio 2017 auto generated files 372 | 373 | # MSTest test Results 374 | 375 | # NUnit 376 | 377 | # Build Results of an ATL Project 378 | 379 | # Benchmark Results 380 | 381 | # .NET Core 382 | 383 | # StyleCop 384 | 385 | # Files built by Visual Studio 386 | 387 | # Chutzpah Test files 388 | 389 | # Visual C++ cache files 390 | 391 | # Visual Studio profiler 392 | 393 | # Visual Studio Trace Files 394 | 395 | # TFS 2012 Local Workspace 396 | 397 | # Guidance Automation Toolkit 398 | 399 | # ReSharper is a .NET coding add-in 400 | 401 | # JustCode is a .NET coding add-in 402 | 403 | # TeamCity is a build add-in 404 | 405 | # DotCover is a Code Coverage Tool 406 | 407 | # AxoCover is a Code Coverage Tool 408 | 409 | # Visual Studio code coverage results 410 | 411 | # NCrunch 412 | 413 | # MightyMoose 414 | 415 | # Web workbench (sass) 416 | 417 | # Installshield output folder 418 | 419 | # DocProject is a documentation generator add-in 420 | 421 | # Click-Once directory 422 | 423 | # Publish Web Output 424 | # Note: Comment the next line if you want to checkin your web deploy settings, 425 | # but database connection strings (with potential passwords) will be unencrypted 426 | 427 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 428 | # checkin your Azure Web App publish settings, but sensitive information contained 429 | # in these scripts will be unencrypted 430 | 431 | # NuGet Packages 432 | # NuGet Symbol Packages 433 | # The packages folder can be ignored because of Package Restore 434 | # except build/, which is used as an MSBuild target. 435 | # Uncomment if necessary however generally it will be regenerated when needed 436 | # NuGet v3's project.json files produces more ignorable files 437 | 438 | # Microsoft Azure Build Output 439 | 440 | # Microsoft Azure Emulator 441 | 442 | # Windows Store app package directories and files 443 | 444 | # Visual Studio cache files 445 | # files ending in .cache can be ignored 446 | # but keep track of directories ending in .cache 447 | 448 | # Others 449 | 450 | # Including strong name files can present a security risk 451 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 452 | 453 | # Since there are multiple workflows, uncomment next line to ignore bower_components 454 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 455 | 456 | # RIA/Silverlight projects 457 | 458 | # Backup & report files from converting an old project file 459 | # to a newer Visual Studio version. Backup files are not needed, 460 | # because we have git ;-) 461 | 462 | # SQL Server files 463 | 464 | # Business Intelligence projects 465 | 466 | # Microsoft Fakes 467 | 468 | # GhostDoc plugin setting file 469 | 470 | # Node.js Tools for Visual Studio 471 | 472 | # Visual Studio 6 build log 473 | 474 | # Visual Studio 6 workspace options file 475 | 476 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 477 | 478 | # Visual Studio LightSwitch build output 479 | 480 | # Paket dependency manager 481 | 482 | # FAKE - F# Make 483 | 484 | # CodeRush personal settings 485 | 486 | # Python Tools for Visual Studio (PTVS) 487 | 488 | # Cake - Uncomment if you are using it 489 | # tools/** 490 | # !tools/packages.config 491 | 492 | # Tabs Studio 493 | 494 | # Telerik's JustMock configuration file 495 | 496 | # BizTalk build output 497 | 498 | # OpenCover UI analysis results 499 | 500 | # Azure Stream Analytics local run output 501 | 502 | # MSBuild Binary and Structured Log 503 | 504 | # NVidia Nsight GPU debugger configuration file 505 | 506 | # MFractors (Xamarin productivity tool) working folder 507 | 508 | # Local History for Visual Studio 509 | 510 | # BeatPulse healthcheck temp database 511 | 512 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 513 | 514 | # End of https://www.gitignore.io/api/csharp,visualstudio 515 | -------------------------------------------------------------------------------- /Injector/InjectorOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using CommandLine; 7 | using CommandLine.Text; 8 | using IniParser; 9 | using IniParser.Model; 10 | 11 | namespace Injector 12 | { 13 | public class InjectorOptions 14 | { 15 | public const uint DEFAULT_INJECTION_DELAY = 5000; 16 | public const uint DEFAULT_INJECTION_LOOP_DELAY = 1000; 17 | public const uint DEFAULT_TIMEOUT = 30000; 18 | private static readonly Logger logger = Logger.Instance(); 19 | 20 | [Option('p', "pid", HelpText = "The process id of the process to inject into")] 21 | public uint? ProcessId { get; set; } 22 | 23 | [Option('x', "process", HelpText = "The process name of the process to inject into")] 24 | public string ProcessName { get; set; } 25 | 26 | [Option('s', "start", HelpText = "Path to the process to start")] 27 | public string StartProcess { get; set; } 28 | 29 | [Option("process-restarts", Default = false, HelpText = "Indicates the process restarts")] 30 | public bool ProcessRestarts { get; set; } 31 | 32 | [Option('w', "win", Default = false, HelpText = "Indicates the process to start is a windows app")] 33 | public bool IsWindowsApp { get; set; } 34 | 35 | [Option('d', "delay", Default = DEFAULT_INJECTION_DELAY, HelpText = "Delay(ms) after starting process to start injection")] 36 | public uint InjectionDelay { get; set; } 37 | 38 | [Option("wait-for-dlls", HelpText = "The paths or config keys of DLLs the injector should wait to be loaded before attempting to inject")] 39 | public IEnumerable WaitDlls { get; set; } 40 | 41 | [Option( 42 | 'm', 43 | "multi-dll-delay", 44 | Default = DEFAULT_INJECTION_LOOP_DELAY, 45 | HelpText = "Delay(ms) between injecting multiple DLLs" 46 | )] 47 | public uint InjectLoopDelay { get; set; } 48 | 49 | [Option( 50 | 't', 51 | "timeout", 52 | Default = DEFAULT_TIMEOUT, 53 | HelpText = "Timeout(ms) when finding process by name" 54 | )] 55 | public uint Timeout { get; set; } 56 | 57 | [Option('c', "config", HelpText = "Path to config file to use")] 58 | public string ConfigFile { get; set; } 59 | 60 | [Option('q', "quiet", Default = false, HelpText = "Suppress console messages. Errors are still printed to the console")] 61 | public bool Quiet { get; set; } 62 | 63 | [Option('v', "verbose", Default = false, HelpText = "All messages including debug messages are printed to the console")] 64 | public bool Verbose { get; set; } 65 | 66 | [Option('i', "interactive", Default = false, HelpText = "Whether to wait for user input")] 67 | public bool Interactive { get; set; } 68 | 69 | [Option('e', "no-pause-on-error", Default = false, HelpText = "Whether to not pause on errors")] 70 | public bool NoPauseOnError { get; set; } 71 | 72 | [Value( 73 | 0, 74 | MetaName = "DLLs", 75 | HelpText = "The paths or config keys to the DLLs to inject. Order determines injection order. Use 'dlls' for the config file" 76 | )] 77 | public IEnumerable Dlls { get; set; } 78 | 79 | [Usage(ApplicationAlias = "injector")] 80 | public static IEnumerable Examples 81 | { 82 | get 83 | { 84 | var shortOptions = new UnParserSettings(); 85 | shortOptions.PreferShortName = true; 86 | return new List 87 | { 88 | new Example("Inject a dll into a process matching pid", new InjectorOptions {ProcessId = 1234, Dlls = new List {"path\\to\\some.dll"}}), 89 | new Example( 90 | "Start a process and inject multiple DLLs", 91 | shortOptions, 92 | new InjectorOptions {StartProcess = "process.exe", Dlls = new List {"path\\to\\some.dll", "path\\to\\another.dll"}} 93 | ), 94 | new Example( 95 | "Start a Microsoft store app and use config file keys for the DLLs", 96 | shortOptions, 97 | new InjectorOptions 98 | { 99 | StartProcess = "Microsoft!App", 100 | IsWindowsApp = true, 101 | ProcessName = "process.exe", 102 | ConfigFile = "config.ini", 103 | Dlls = new List {"key1", "key2"} 104 | } 105 | ) 106 | }; 107 | } 108 | } 109 | 110 | private IniData _Config { get; set; } 111 | 112 | public IniData Config 113 | { 114 | get 115 | { 116 | if (_Config == null) 117 | { 118 | if (ConfigFile == null) 119 | { 120 | ConfigFile = "config.ini"; 121 | if (!File.Exists("config.ini")) 122 | { 123 | logger.Debug("config.ini file not found, trying config/*.ini"); 124 | try 125 | { 126 | ConfigFile = Directory.GetFiles("config", "*.ini").FirstOrDefault(); 127 | } 128 | catch (DirectoryNotFoundException ex) 129 | { 130 | logger.Debug("No config folder found"); 131 | } 132 | } 133 | } 134 | 135 | if (string.IsNullOrWhiteSpace(ConfigFile)) 136 | { 137 | Program.HandleError("No config file specified/found"); 138 | } 139 | else 140 | { 141 | var configFileInfo = new FileInfo(ConfigFile); 142 | var parser = new FileIniDataParser(); 143 | logger.Info($"Reading from config file: {ConfigFile}"); 144 | if (!File.Exists(configFileInfo.FullName)) 145 | { 146 | Program.HandleError($"Config file not found: {configFileInfo.FullName}"); 147 | } 148 | 149 | try 150 | { 151 | _Config = parser.ReadFile(ConfigFile); 152 | } 153 | catch (Exception ex) 154 | { 155 | Program.HandleException("There was an issue parsing the config file!", ex); 156 | } 157 | } 158 | } 159 | 160 | return _Config; 161 | } 162 | } 163 | 164 | private dynamic ParseConfigValue(string key, Type type, dynamic defaultValue) 165 | { 166 | if (Config.TryGetKey($"Config.{key}", out var value)) 167 | { 168 | logger.Debug($"Using config value for {key}"); 169 | value = value.Trim(); 170 | if (!value.Equals("")) 171 | { 172 | if (type == typeof(List<>)) 173 | { 174 | var values = value.Split(' ').ToList(); 175 | return values; 176 | } 177 | 178 | return Convert.ChangeType(value, type); 179 | } 180 | } 181 | 182 | return defaultValue; 183 | } 184 | 185 | public void UpdateFromFile() 186 | { 187 | if (Config != null) 188 | { 189 | logger.Debug("Overriding command line args with config values"); 190 | ProcessId = ParseConfigValue("pid", typeof(uint), ProcessId); 191 | ProcessName = ParseConfigValue("process", typeof(string), ProcessName); 192 | StartProcess = ParseConfigValue("start", typeof(string), StartProcess); 193 | ProcessRestarts = ParseConfigValue("process-restarts", typeof(bool), ProcessRestarts); 194 | IsWindowsApp = ParseConfigValue("win", typeof(bool), IsWindowsApp); 195 | InjectionDelay = ParseConfigValue("delay", typeof(uint), InjectionDelay); 196 | WaitDlls = ParseConfigValue("wait-for-dlls", typeof(List<>), WaitDlls); 197 | InjectLoopDelay = ParseConfigValue("multi-dll-delay", typeof(uint), InjectLoopDelay); 198 | Timeout = ParseConfigValue("timeout", typeof(uint), Timeout); 199 | Quiet = ParseConfigValue("quiet", typeof(bool), Quiet); 200 | Verbose = ParseConfigValue("verbose", typeof(bool), Verbose); 201 | Interactive = ParseConfigValue("interactive", typeof(bool), Verbose); 202 | NoPauseOnError = ParseConfigValue("no-pause-on-error", typeof(bool), Verbose); 203 | Dlls = ParseConfigValue("dlls", typeof(List<>), Dlls); 204 | } 205 | } 206 | 207 | public FileInfo GetDllInfo(string key) 208 | { 209 | string filePath = null; 210 | if (Config != null) 211 | { 212 | filePath = Config.GetKey($"DLL.{key}")?.Trim(); 213 | } 214 | 215 | return new FileInfo(string.IsNullOrEmpty(filePath) ? key : filePath); 216 | } 217 | 218 | public void Validate() 219 | { 220 | if (InjectionDelay == 0) 221 | { 222 | logger.Warn("No delay specified before attempting to inject. The process could crash"); 223 | } 224 | 225 | if (InjectLoopDelay == 0) 226 | { 227 | logger.Warn("No delay between injecting multiple DLLs. The process could crash"); 228 | } 229 | 230 | if (ProcessId != null && (ProcessName != null || StartProcess != null)) 231 | { 232 | Program.HandleError("--pid and --process/--start cannot be combined"); 233 | } 234 | 235 | if (Quiet && Verbose) 236 | { 237 | Program.HandleError("--quiet and --verbose cannot be combined"); 238 | } 239 | 240 | if (Interactive && NoPauseOnError) 241 | { 242 | Program.HandleError("--interactive and --no-pause-on-error cannot be combined"); 243 | } 244 | 245 | if ((Dlls?.Count() ?? 0) == 0) 246 | { 247 | Program.HandleError("No DLLs to inject"); 248 | } 249 | 250 | foreach (var dll in Dlls) 251 | { 252 | var dllInfo = GetDllInfo(dll); 253 | if (!File.Exists(dllInfo.FullName)) 254 | { 255 | Program.HandleError($"DLL not found: {dllInfo.FullName}"); 256 | } 257 | } 258 | 259 | var existingWaitDlls = new List(); 260 | foreach (var dll in WaitDlls) 261 | { 262 | var dllInfo = GetDllInfo(dll); 263 | if (File.Exists(dllInfo.FullName)) 264 | { 265 | existingWaitDlls.Add(dll); 266 | } 267 | else 268 | { 269 | logger.Warn($"Wait DLL not found: {dllInfo.FullName}"); 270 | } 271 | } 272 | 273 | WaitDlls = existingWaitDlls; 274 | } 275 | 276 | public void Log() 277 | { 278 | var sb = new StringBuilder(); 279 | sb.AppendLine("Injector Options:"); 280 | sb.AppendLine($" pid={ProcessId}"); 281 | sb.AppendLine($" process={ProcessName}"); 282 | sb.AppendLine($" start={StartProcess}"); 283 | sb.AppendLine($" process-restarts={ProcessRestarts}"); 284 | sb.AppendLine($" win={IsWindowsApp}"); 285 | sb.AppendLine($" delay={InjectionDelay}"); 286 | sb.AppendLine($" wait-for-dlls={string.Join(' ', WaitDlls)}"); 287 | sb.AppendLine($" multi-dll-delay={InjectLoopDelay}"); 288 | sb.AppendLine($" timeout={Timeout}"); 289 | sb.AppendLine($" quiet={Quiet}"); 290 | sb.AppendLine($" verbose={Verbose}"); 291 | sb.AppendLine($" interactive={Interactive}"); 292 | sb.AppendLine($" no-pause-on-error={NoPauseOnError}"); 293 | sb.AppendLine($" dlls={string.Join(' ', Dlls)}"); 294 | logger.Debug(sb.ToString()); 295 | } 296 | } 297 | } -------------------------------------------------------------------------------- /Injector/Injector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using System.Threading; 8 | 9 | namespace Injector 10 | { 11 | internal class Injector 12 | { 13 | private const int PROCESS_CREATE_THREAD = 0x0002; 14 | private const int PROCESS_QUERY_INFORMATION = 0x0400; 15 | private const int PROCESS_VM_OPERATION = 0x0008; 16 | private const int PROCESS_VM_WRITE = 0x0020; 17 | private const int PROCESS_VM_READ = 0x0010; 18 | 19 | private const uint MEM_COMMIT = 0x00001000; 20 | private const uint MEM_RESERVE = 0x00002000; 21 | private const uint MEM_RELEASE = 0x00008000; 22 | private const uint PAGE_READWRITE = 4; 23 | 24 | private const int OPEN_PROCESS = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | 25 | PROCESS_VM_WRITE | PROCESS_VM_READ; 26 | 27 | private const uint MEM_CREATE = MEM_COMMIT | MEM_RESERVE; 28 | private static readonly Logger logger = Logger.Instance(); 29 | 30 | private readonly InjectorOptions opts; 31 | 32 | public Injector(InjectorOptions opts) 33 | { 34 | this.opts = opts; 35 | } 36 | 37 | [DllImport("kernel32.dll", SetLastError = true)] 38 | private static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); 39 | 40 | [DllImport("kernel32.dll", SetLastError = true)] 41 | private static extern int CloseHandle(IntPtr hObject); 42 | 43 | [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] 44 | private static extern IntPtr GetModuleHandle(string lpModuleName); 45 | 46 | [DllImport("kernel32", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Ansi)] 47 | private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); 48 | 49 | [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] 50 | private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, 51 | uint dwSize, uint flAllocationType, uint flProtect); 52 | 53 | [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] 54 | private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType); 55 | 56 | [DllImport("kernel32.dll", SetLastError = true)] 57 | private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, 58 | out UIntPtr lpNumberOfBytesWritten); 59 | 60 | [DllImport("kernel32.dll", SetLastError = true)] 61 | private static extern IntPtr CreateRemoteThread(IntPtr hProcess, 62 | IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, 63 | uint dwCreationFlags, IntPtr lpThreadId); 64 | 65 | public void Inject() 66 | { 67 | Process process = null; 68 | if (opts.ProcessId != null) 69 | { 70 | // find process by pid 71 | var pid = opts.ProcessId.Value; 72 | try 73 | { 74 | logger.Info("Attempting to find running process by id..."); 75 | process = Process.GetProcessById((int) opts.ProcessId.Value); 76 | } 77 | catch (ArgumentException) 78 | { 79 | Program.HandleError($"Could not find process id {pid}"); 80 | } 81 | } 82 | else if (opts.StartProcess != null) 83 | { 84 | logger.Debug("Checking if process has already started"); 85 | process = FindProcessByName( 86 | opts.ProcessName == null 87 | ? opts.StartProcess 88 | : opts.ProcessName 89 | ); 90 | if (process == null) 91 | { 92 | // starts a process 93 | if (!File.Exists(opts.StartProcess)) 94 | { 95 | if (opts.StartProcess.Contains('!')) 96 | { 97 | logger.Debug("Could not find the process to start, but it could be a Microsoft store app"); 98 | opts.IsWindowsApp = true; 99 | } 100 | else 101 | { 102 | Program.HandleError($"{opts.StartProcess} not found. Ensure the path is correct"); 103 | } 104 | } 105 | 106 | var app = opts.StartProcess; 107 | var app_args = ""; 108 | if (opts.IsWindowsApp) 109 | { 110 | logger.Debug("Process to start is a Microsoft store app"); 111 | app = "explorer.exe"; 112 | app_args = $"shell:AppsFolder\\{opts.StartProcess}"; 113 | } 114 | 115 | logger.Info($"Starting {opts.StartProcess}"); 116 | process = Process.Start(app, app_args); 117 | 118 | if (opts.ProcessName != null) 119 | { 120 | // if the exe to inject to is different than the one started 121 | logger.Debug("Waiting for real process to start..."); 122 | process = WaitForProcess(opts.ProcessName, opts.Timeout); 123 | } 124 | 125 | if (opts.ProcessRestarts) 126 | { 127 | logger.Debug("Waiting for original process to exit"); 128 | process = WaitForProcessRestart(process, opts.Timeout); 129 | opts.ProcessRestarts = false; 130 | } 131 | else 132 | { 133 | // set this to true, so we can attempt to wait for a restart even if the option is not set 134 | opts.ProcessRestarts = true; 135 | } 136 | } 137 | else 138 | { 139 | logger.Info("Process already started."); 140 | } 141 | } 142 | else if (opts.ProcessName != null) 143 | { 144 | logger.Info("Attempting to find running process by name..."); 145 | process = WaitForProcess(opts.ProcessName, opts.Timeout); 146 | } 147 | 148 | if (process == null) 149 | { 150 | Program.HandleError("No process to inject."); 151 | } 152 | 153 | var dlls = BuildDllInfos(opts.Dlls); 154 | var wait_dlls = BuildDllInfos(opts.WaitDlls); 155 | 156 | if (opts.StartProcess == null) 157 | { 158 | logger.Debug("Not delaying before injection. Process already started."); 159 | } 160 | else 161 | { 162 | logger.Info("Waiting for process to fully load"); 163 | try 164 | { 165 | WaitForDlls(process, wait_dlls, opts.InjectionDelay); 166 | } 167 | catch (Exception) when (process.HasExited && opts.ProcessRestarts) 168 | { 169 | logger.Debug("It seems the process exited while waiting for it to initialize"); 170 | process = WaitForProcessRestart(process, opts.Timeout); 171 | //if we fail here, then injector exits with error 172 | WaitForDlls(process, wait_dlls, opts.InjectionDelay); 173 | } 174 | } 175 | 176 | if (process.WaitForInputIdle()) 177 | { 178 | logger.Info($"Injecting {dlls.Count} DLL(s) into {process.ProcessName} ({process.Id})"); 179 | InjectIntoProcess(process, dlls.ToArray(), opts.InjectLoopDelay); 180 | } 181 | } 182 | 183 | private List BuildDllInfos(IEnumerable option_dlls) 184 | { 185 | var dllInfos = new List(); 186 | foreach (var dll in option_dlls) 187 | { 188 | var dllInfo = opts.GetDllInfo(dll); 189 | dllInfos.Add(dllInfo); 190 | } 191 | 192 | return dllInfos; 193 | } 194 | 195 | private void InjectIntoProcess(Process process, FileInfo[] dlls, uint delay = InjectorOptions.DEFAULT_INJECTION_LOOP_DELAY) 196 | { 197 | logger.Debug("Opening handle to process"); 198 | var procHandle = OpenProcess(OPEN_PROCESS, false, process.Id); 199 | if (procHandle == IntPtr.Zero) 200 | { 201 | Program.HandleWin32Error($"Unable to open {process.ProcessName}. Make sure to start the tool with Administrator privileges"); 202 | } 203 | 204 | logger.Debug("Retrieving the memory address to LoadLibraryA"); 205 | var loadLibraryAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA"); 206 | if (loadLibraryAddr == IntPtr.Zero) 207 | { 208 | Program.HandleWin32Error("Unable not retrieve the address for LoadLibraryA"); 209 | } 210 | 211 | var dllIndex = 1; 212 | foreach (var dll in dlls) 213 | { 214 | logger.Info($"Attempting to inject DLL, {dllIndex} of {dlls.Length}, {dll.Name}..."); 215 | var size = (uint) (dll.FullName.Length + 1); 216 | logger.Debug("Allocating memory in the process to write the DLL path"); 217 | var allocMemAddress = VirtualAllocEx(procHandle, IntPtr.Zero, size, MEM_CREATE, PAGE_READWRITE); 218 | if (allocMemAddress == IntPtr.Zero) 219 | { 220 | Program.HandleWin32Error("Unable to allocate memory in the process. Make sure to start the tool with Administrator privileges"); 221 | } 222 | 223 | logger.Debug("Writing the DLL path in the process memory"); 224 | var result = WriteProcessMemory( 225 | procHandle, 226 | allocMemAddress, 227 | Encoding.Default.GetBytes(dll.FullName), 228 | size, 229 | out var bytesWritten 230 | ); 231 | if (!result) 232 | { 233 | Program.HandleWin32Error("Failed to write the DLL path into the memory of the process"); 234 | } 235 | 236 | logger.Debug("Creating remote thread. This is where the magic happens!"); 237 | var threadHandle = CreateRemoteThread( 238 | procHandle, 239 | IntPtr.Zero, 240 | 0, 241 | loadLibraryAddr, 242 | allocMemAddress, 243 | 0, 244 | IntPtr.Zero 245 | ); 246 | if (procHandle == IntPtr.Zero) 247 | { 248 | Program.HandleWin32Error("Unable to create a remote thread in the process. Failed to inject the dll"); 249 | } 250 | 251 | logger.Debug("Waiting for DLL to load..."); 252 | while (!IsDllLoaded(process, dll)) 253 | { 254 | Thread.Sleep(100); 255 | } 256 | 257 | if (process.WaitForInputIdle()) 258 | { 259 | logger.Debug("Closing remote thread"); 260 | CloseHandle(threadHandle); 261 | logger.Debug("Freeing memory"); 262 | VirtualFreeEx(procHandle, allocMemAddress, UIntPtr.Zero, MEM_RELEASE); 263 | } 264 | 265 | if (dllIndex < dlls.Length) 266 | { 267 | if (delay == 0) 268 | { 269 | logger.Debug("No delay between next DLL injection"); 270 | } 271 | else 272 | { 273 | logger.Debug($"Delaying next DLL injection by {delay} ms"); 274 | Thread.Sleep((int) delay); 275 | } 276 | } 277 | 278 | dllIndex++; 279 | 280 | logger.Info("Injected!"); 281 | } 282 | 283 | logger.Debug("Closing handle to process"); 284 | CloseHandle(procHandle); 285 | } 286 | 287 | private Process FindProcessByName(string name) 288 | { 289 | name = Path.GetFileNameWithoutExtension(name); 290 | logger.Debug($"Finding processes matching '{name}'"); 291 | var processes = Process.GetProcessesByName(name); 292 | if (processes.Length == 1) 293 | { 294 | logger.Debug("Found one match!"); 295 | return processes[0]; 296 | } 297 | 298 | if (processes.Length > 1) 299 | { 300 | Program.HandleError($"Too many processes matching {name}"); 301 | } 302 | 303 | logger.Debug("No process found matching the supplied name"); 304 | return null; 305 | } 306 | 307 | private Process WaitForProcess(string name, uint timeout = InjectorOptions.DEFAULT_TIMEOUT) 308 | { 309 | Process process = null; 310 | var timeout_counter = (int) timeout; 311 | var polling_rate = 500; 312 | 313 | logger.Debug($"Waiting for process '{name}'"); 314 | while (timeout_counter > 0) 315 | { 316 | process = FindProcessByName(name); 317 | if (process != null) 318 | { 319 | logger.Debug("Process found!"); 320 | break; 321 | } 322 | 323 | timeout_counter -= polling_rate; 324 | Thread.Sleep(polling_rate); 325 | } 326 | 327 | if (process == null) 328 | { 329 | Program.HandleError($"Timed out waiting for process '{name}'"); 330 | } 331 | 332 | return process; 333 | } 334 | 335 | private Process WaitForProcessRestart(Process process, uint timeout = InjectorOptions.DEFAULT_TIMEOUT) 336 | { 337 | if (process.WaitForExit((int) timeout)) 338 | { 339 | logger.Debug("Waiting for process to restart with new pid"); 340 | var processPath = opts.ProcessName == null 341 | ? opts.StartProcess 342 | : opts.ProcessName; 343 | process = WaitForProcess(processPath, opts.Timeout); 344 | } 345 | else 346 | { 347 | logger.Debug("Process may have already exited"); 348 | } 349 | 350 | return process; 351 | } 352 | 353 | private bool IsDllLoaded(Process process, FileInfo dll) 354 | { 355 | process.Refresh(); 356 | foreach (ProcessModule processModule in process.Modules) 357 | { 358 | if (processModule.FileName.Equals(dll.FullName, StringComparison.OrdinalIgnoreCase)) 359 | { 360 | return true; 361 | } 362 | } 363 | 364 | return false; 365 | } 366 | 367 | private void WaitForDlls(Process process, List waitDlls, uint timeout = InjectorOptions.DEFAULT_INJECTION_DELAY) 368 | { 369 | var loadedModules = new Dictionary>(); 370 | 371 | var timeout_counter = (int) timeout; 372 | var polling_rate = 100; 373 | 374 | while (timeout_counter > 0 && process.WaitForInputIdle()) 375 | { 376 | var modulesChanged = false; 377 | process.Refresh(); 378 | foreach (ProcessModule processModule in process.Modules) 379 | { 380 | var moduleLoaded = false; 381 | if (!loadedModules.ContainsKey(processModule.ModuleName)) 382 | { 383 | logger.Debug($"Loaded {processModule.ModuleName} - {processModule.FileName}"); 384 | loadedModules[processModule.ModuleName] = new List(); 385 | moduleLoaded = true; 386 | } 387 | else if (!loadedModules[processModule.ModuleName].Contains(processModule.FileName.ToLower())) 388 | { 389 | logger.Debug($"Changed {processModule.ModuleName} - {processModule.FileName}"); 390 | moduleLoaded = true; 391 | } 392 | 393 | if (moduleLoaded) 394 | { 395 | loadedModules[processModule.ModuleName].Add(processModule.FileName.ToLower()); 396 | foreach (var waitDll in waitDlls) 397 | { 398 | if (processModule.FileName.Equals(waitDll.FullName, StringComparison.OrdinalIgnoreCase)) 399 | { 400 | logger.Info($"Wait DLL loaded: {waitDll.FullName}"); 401 | waitDlls.Remove(waitDll); 402 | break; 403 | } 404 | } 405 | 406 | modulesChanged = true; 407 | } 408 | } 409 | 410 | if (modulesChanged) 411 | { 412 | logger.Debug("timeout reset since modules changed"); 413 | timeout_counter = (int) timeout; 414 | } 415 | 416 | timeout_counter -= polling_rate; 417 | Thread.Sleep(polling_rate); 418 | } 419 | 420 | if (waitDlls.Count > 0) 421 | { 422 | logger.Warn("Not all wait DLLs found. Continuing with injection. See log for details"); 423 | foreach (var waitDll in waitDlls) 424 | { 425 | logger.Debug($"wait DLL not found: {waitDll.FullName}"); 426 | } 427 | } 428 | else 429 | { 430 | logger.Info("Process modules possibly fully loaded"); 431 | } 432 | } 433 | } 434 | } --------------------------------------------------------------------------------