├── .gitignore ├── README.md ├── doc ├── examples.md ├── rules.md └── usage.md ├── il2cpp-modder.js ├── il2cpp-modder.sublime-project ├── index.js ├── install.sh ├── package-lock.json ├── package.json ├── rules.js ├── src ├── domain │ ├── dumpReader.js │ └── projectGenerators │ │ ├── dllInjector.js │ │ ├── gameModderDll.js │ │ ├── index.js │ │ ├── projectGenerator.js │ │ └── templates │ │ ├── gameModder │ │ ├── commandLoop.js │ │ ├── commandLoopHeader.js │ │ ├── dllMain.js │ │ ├── hooking.js │ │ ├── hookingHeader.js │ │ ├── modelsHeader.js │ │ ├── mods │ │ │ ├── fixedReturnValue.js │ │ │ ├── hookUtils.js │ │ │ ├── index.js │ │ │ ├── pathMemoryHack.js │ │ │ ├── replaceArguments.js │ │ │ └── replaceImplementation.js │ │ ├── trampolineHook.js │ │ ├── trampolineHookHeader.js │ │ ├── utils.js │ │ └── utilsHeader.js │ │ └── injector │ │ └── injector.js └── entryPoints │ ├── generateDllInjectionProjects │ ├── generateDllInjectionProjects.js │ └── index.js │ └── generateDump │ ├── chooseFile.bat │ ├── chooseFolder.bat │ ├── generateDump.js │ └── index.js └── testFiles ├── dump.cs └── rules.js /.gitignore: -------------------------------------------------------------------------------- 1 | #C++ 2 | 3 | # Prerequisites 4 | *.d 5 | 6 | # Compiled Object files 7 | *.slo 8 | *.lo 9 | *.o 10 | *.obj 11 | 12 | # Precompiled Headers 13 | *.gch 14 | *.pch 15 | 16 | # Compiled Dynamic libraries 17 | *.so 18 | *.dylib 19 | *.dll 20 | 21 | # Fortran module files 22 | *.mod 23 | *.smod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | # NodeJS 36 | # Logs 37 | logs 38 | *.log 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | lerna-debug.log* 43 | 44 | # Diagnostic reports (https://nodejs.org/api/report.html) 45 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 46 | 47 | # Runtime data 48 | pids 49 | *.pid 50 | *.seed 51 | *.pid.lock 52 | 53 | # Directory for instrumented libs generated by jscoverage/JSCover 54 | lib-cov 55 | 56 | # Coverage directory used by tools like istanbul 57 | coverage 58 | *.lcov 59 | 60 | # nyc test coverage 61 | .nyc_output 62 | 63 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 64 | .grunt 65 | 66 | # Bower dependency directory (https://bower.io/) 67 | bower_components 68 | 69 | # node-waf configuration 70 | .lock-wscript 71 | 72 | # Compiled binary addons (https://nodejs.org/api/addons.html) 73 | build/Release 74 | 75 | # Dependency directories 76 | node_modules/ 77 | jspm_packages/ 78 | 79 | # Snowpack dependency directory (https://snowpack.dev/) 80 | web_modules/ 81 | 82 | # TypeScript cache 83 | *.tsbuildinfo 84 | 85 | # Optional npm cache directory 86 | .npm 87 | 88 | # Optional eslint cache 89 | .eslintcache 90 | 91 | # Microbundle cache 92 | .rpt2_cache/ 93 | .rts2_cache_cjs/ 94 | .rts2_cache_es/ 95 | .rts2_cache_umd/ 96 | 97 | # Optional REPL history 98 | .node_repl_history 99 | 100 | # Output of 'npm pack' 101 | *.tgz 102 | 103 | # Yarn Integrity file 104 | .yarn-integrity 105 | 106 | # dotenv environment variables file 107 | .env 108 | .env.test 109 | 110 | # parcel-bundler cache (https://parceljs.org/) 111 | .cache 112 | .parcel-cache 113 | 114 | # Next.js build output 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | .nuxt 120 | dist 121 | 122 | # Gatsby files 123 | .cache/ 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | # https://nextjs.org/blog/next-9-1#public-directory-support 126 | # public 127 | 128 | # vuepress build output 129 | .vuepress/dist 130 | 131 | # Serverless directories 132 | .serverless/ 133 | 134 | # FuseBox cache 135 | .fusebox/ 136 | 137 | # DynamoDB Local files 138 | .dynamodb/ 139 | 140 | # TernJS port file 141 | .tern-port 142 | 143 | # Stores VSCode versions used for testing VSCode extensions 144 | .vscode-test 145 | 146 | # yarn v2 147 | .yarn/cache 148 | .yarn/unplugged 149 | .yarn/build-state.yml 150 | .yarn/install-state.gz 151 | .pnp.* 152 | 153 | 154 | #Sublime 155 | *.sublime-workspace 156 | output 157 | *il2cppdumper-output -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # il2cpp-modder 2 | Generate DLL injection templates for reverse engineering and modding Unity il2cpp games. 3 | 4 | # Motivation 5 | So you've been researching how to do some hacky stuff in Unity games and stumbled upon the amazing [IL2CppDumper](https://github.com/perfare/il2cppdumper). 6 | You have learned how to get and analyze the `dump.cs` and now **you know exactly _what_ you want to do, but you do not know _how_ to do it**. 7 | 8 | You know that you have to override some method, replace the value of some field in an object or something like that, but your low level programming skills with all that nasty pointer arithmetic and assembly are not quite there yet (don't worry, mine barely are, just practice and you'll get better!). 9 | 10 | Well this is the project for you! il2cpp-modder lets you describre **what** to do and it will generate all the code to do it. That's it, **you don't have to program at all!** 11 | 12 | # Built in mods 13 | - [Make a method return a fixed value](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#fixed-return-value). Some ideas: 14 | - Make a validation always return true or false. 15 | - Make a getter always return 0 or a really high number. 16 | - Skip validation methods. 17 | - [Replace the arguments for a method call](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#replace-arguments). Some ideas: 18 | - Make a setter always set 999999 coins to your player. 19 | - [Set the value of an object field](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#path-memory-hack). Some ideas: 20 | - Keep your player health always at 100%. 21 | - Change your player height at any moment to any value. 22 | - [Replace the implementation of a method](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#replace-implementation). 23 | - If the other mods just don't cut it, you can just replace the whole thing for something different that suits your needs! The sky is the limit. You do need to program C++ for this though... 24 | 25 | # Requirements 26 | - [NodeJS](https://nodejs.org/en/download/) 27 | - [IL2CppDumper](https://github.com/perfare/il2cppdumper) (add to PATH!) 28 | - Visual Studio (Or any C++ compiler) 29 | 30 | # Installation 31 | ``` 32 | git clone https://github.com/juanmjacobs/il2cpp-modder 33 | cd il2cpp-modder 34 | sh install.sh 35 | ``` 36 | 37 | # Usage 38 | See the [usage documentation](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/usage.md) 39 | 40 | # Contributing 41 | 42 | Issues and PRs are welcome! 43 | -------------------------------------------------------------------------------- /doc/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | Here are some examples of cool stuff you can do with the built in mods! 3 | 4 | ## Fixed return value 5 | - Scenario: there is a class `FancyStuff` with a `bool IsUnlocked()` method. Let's mod it so that it **always returns true.** 6 | 7 | ``` 8 | module.exports = { 9 | /*...game and dump sections omitted for brevity, just keep the autogenerated ones!...*/ 10 | "hooks": { 11 | methods: [{ 12 | className: "FancyStuff", 13 | name: "IsUnlocked", 14 | trampolineHookBytes: 6, 15 | mods: [{ 16 | type: "fixedReturnValue", 17 | args: "true" 18 | }] 19 | }] 20 | }, 21 | } 22 | ``` 23 | 24 | - Scenario: there is a class `ValidatedStuff` with a `void ValidateSomething()` method. Let's mod it so that the **validation does nothing and just let's you through.** 25 | ``` 26 | module.exports = { 27 | /*...game and dump sections omitted for brevity, just keep the autogenerated ones!...*/ 28 | "hooks": { 29 | methods: [{ 30 | className: "ValidatedStuff", 31 | name: "ValidateSomething", 32 | trampolineHookBytes: 6, 33 | mods: [{ 34 | type: "fixedReturnValue", 35 | args: "" 36 | }] 37 | }] 38 | }, 39 | } 40 | ``` 41 | 42 | ## Replace arguments 43 | - Scenario: there is a class `Player` with a `void SetCoins(int coins)` method. Let's mod it so that the setter **always sets your coins to 999999.** 44 | ``` 45 | module.exports = { 46 | /*...game and dump sections omitted for brevity, just keep the autogenerated ones!...*/ 47 | "hooks": { 48 | methods: [{ 49 | className: "Player", 50 | name: "SetCoins", 51 | trampolineHookBytes: 6, 52 | mods: [{ 53 | type: "replaceArguments", 54 | args: ["999999"] 55 | }] 56 | }] 57 | }, 58 | } 59 | ``` 60 | 61 | - Scenario: there is a class `Player` with a `void DoCoolStuff(int parameter1, int parameter2, bool parameter3)` method. Let's mod it so that the we keep the `parameter1` as is, but replace `parameter2` by `0` and `parameter3` by `true` 62 | ``` 63 | module.exports = { 64 | /*...game and dump sections omitted for brevity, just keep the autogenerated ones!...*/ 65 | "hooks": { 66 | methods: [{ 67 | className: "Player", 68 | name: "DoCoolStuff", 69 | trampolineHookBytes: 6, 70 | mods: [{ 71 | type: "replaceArguments", 72 | args: ["paramter1", "0", "true"] 73 | }] 74 | }] 75 | }, 76 | } 77 | ``` 78 | 79 | ## Path memory hack 80 | 81 | - Scenario: there is a class `Fighter` with a field `FighterStatus Status`, that has a field `float Health`. Let's mod it so that the `Fighter.Status.Health` field is **always at 100**. 82 | 83 | ``` 84 | module.exports = { 85 | /*...game and dump sections omitted for brevity, just keep the autogenerated ones!...*/ 86 | "hooks": { 87 | methods: [{ 88 | className: "Fighter", 89 | name: "AnyMethodInFighterClass", 90 | trampolineHookBytes: 6, 91 | mods: [{ 92 | type: "pathMemoryHack", 93 | args: { 94 | paths: [{ 95 | name: "The health of the fighter", //Optional, a descriptive name for the path 96 | path: "Status.Health", 97 | value: "100" 98 | }] 99 | } 100 | }] 101 | }] 102 | }, 103 | } 104 | ``` 105 | 106 | If you'd like to set different values for that field in different times, just don't specify the `value` property in the hook. You will be able to insert the value you want via de mod console at any given time. 107 | 108 | ### Why are we hooking `AnyMethodInFighterClass`? 109 | This mod is not a method hook but a path memory hook. Nevertheless, we need to hook a method to get a reference to the object you'd like to mod: the `this` pointer. You do this by hooking _any method in the Fighter class_. Try to choose one that gets called often (most of them are) so you get the pointer quickly! 110 | 111 | ## Replace implementation 112 | - Scenario: there is a really complex class `ComplexClass` with a really `int ComplexMethod(int anIntParam)` method. You want to mod it, but the **basic built in mods are not enough**. Let's mod it has a new implementation. 113 | This one is more advanced, as you need to write valid C++ code. The function signature (parameters + return) gets autogenerated for you to avoid any mistakes. You just have to take care of the function body. 114 | You have access to the parameters of the original function with the same names. 115 | If you need to use the original function behaviour, you can call it via `original_ComplexClass_ComplexMethod(anInt);` or more generally `original_[class name]_[method name]([parameters]);` 116 | 117 | ``` 118 | module.exports = { 119 | /*...game and dump sections omitted for brevity, just keep the autogenerated ones!...*/ 120 | "hooks": { 121 | methods: [{ 122 | className: "FancyStuff", 123 | name: "IsUnlocked", 124 | trampolineHookBytes: 6, 125 | mods: [{ 126 | type: "fixedReturnValue", 127 | args: ` 128 | bool someFancyBoolean = anIntParam > 10; 129 | if(someFancyBoolean) return 1; 130 | //Call original ComplexClass.ComplexMethod with another argument 131 | int aNewArgument = 123; 132 | original_ComplexClass_ComplexMethod(aNewArgument); 133 | /*keep doing fancy c++ stuff*/ 134 | return 2; 135 | ` 136 | }] 137 | }] 138 | }, 139 | } 140 | ``` 141 | 142 | # Notes 143 | - You can hook all the instance methods you want in all public classes, concrete or abstract. 144 | - Static methods and field hooks are currently not supported. 145 | - Even though the field `mods` is an array, currently just one mod per method is supported. 146 | -------------------------------------------------------------------------------- /doc/rules.md: -------------------------------------------------------------------------------- 1 | # Rules 2 | il2cpp-modder needs a rules file to know what you'd like to mod. 3 | 4 | This is the structure for the file: 5 | ``` 6 | module.exports = { 7 | game: { 8 | path: "/absolute/path/to/game", //optional 9 | exeName: "A Nice Game.exe" 10 | }, 11 | "output": "path/to/output/dir" //optional, default "output" 12 | "dump": { 13 | path: "/absolute/path/to/dump.cs" 14 | }, 15 | "hooks": { 16 | methods: [{ //Change some game functionality! 17 | className: "ClassToMod", 18 | name: "MethodToMod", 19 | trampolineHookBytes: 6, //See trampoline hook bytes 20 | mods: [{ //See Mod Types 21 | type: "aModType", 22 | args: "modTypeArguments", 23 | }] 24 | }] 25 | }, 26 | } 27 | ``` 28 | There are several mod types built into il2cpp-modder. Be sure to check some [examples](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md) 29 | 30 | ## Mod Types 31 | 32 | - fixedReturnValue: 33 | - Description: Always return a fixed value. (eg, always return true, false, 0, 9999, etc) 34 | - args: the value to return (String) (ex 'true', '0.0f', '"some string"') 35 | - [Example](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#fixed-return-value) 36 | 37 | - replaceArguments: 38 | - Description: Call original method with replaced arguments. (ex, always call setter with true, false, 0, etc) 39 | - args: the replaced arguments (Array) (ex `["firstArgument", "0f", "true"]`) 40 | - [Example](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#replace-arguments) 41 | 42 | - pathMemoryHack: 43 | - Description: Lets you set an object field to a certain value. 44 | - args: An array of paths to memory hack! Values can be set automatically or via the mod console. 45 | - [Example](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#path-memory-hack) 46 | 47 | - replaceImplementation: 48 | - Description: Just replace the whole thing 49 | - args: the new c++ implementation (String) (ex `"return theOriginalParameter + 10;"`) 50 | - [Example](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/examples.md#replace-implementation) 51 | 52 | ## Trampoline hook bytes 53 | First of all, [WTF is a trampoline hook?](https://stackoverflow.com/a/9336549) 54 | 55 | _TLDR lazy tip: This number is usually < 16. If you don't feel like going to the trouble of finding the trampolineHookBytes, just start at 5 and keep adding 1 until it works._ 56 | 57 | This might be the trickiest part! Hopefully in the future this parameter will be automatically calculated so you don't have to supply it. 58 | 59 | _Why start at 5?_ 60 | 61 | A `jmp dir` instruction takes 5 bytes, 1 for the `jmp` and 4 for the `dir` (32 bits address). 62 | So the `trampolineHookBytes` should be setted to the amount of bytes of complete assembly instructions (opcode + operands) of your function so that there is more than 5 bytes. 63 | I know, it sounds confusing! Lets see an example: 64 | Fire up your [Cheat Engine](https://www.cheatengine.org/downloads.php) -> attach your process -> memory view -> CTRL G -> paste the `GameAssembly.dll + RVA of the function`. Remember, the RVA of the function is on dump.cs. 65 | It should look something like this 66 | ![Cheat engine screenshot](https://i.imgur.com/ho5aAuw.png) 67 | Now we should look at the `Bytes` column. We can see that our function starts with 68 | ``` 69 | 55 70 | 8B EC 71 | 83 EC 14 72 | 80 3D 1DA7C205 00 73 | ... 74 | ``` 75 | Each line represents a complete assembly instruction (opcode + operands). And remember, two characters = 1 byte. 76 | So let's add up the number of bytes of each line until we have more than 5! 77 | ``` 78 | 55 1 byte --> 1. That's < 5, not enough bytes! 79 | 8B EC 2 bytes --> 2 + 1 = 3. That's < 5, not enough bytes! 80 | 83 EC 14 3 bytes --> 2 + 1 + 3 = 6. 6 is > 5, so our magic number is 6! 81 | 80 3D 1DA7C205 00 82 | ... 83 | ``` 84 | So we set trampolineHookBytes to 6. 85 | 86 | ## Advanced modding 87 | 88 | You can use the output of il2cpp-modder as a baseline for more sophisticated mods! Just edit and add whatever you want. -------------------------------------------------------------------------------- /doc/usage.md: -------------------------------------------------------------------------------- 1 | # Usage steps 2 | This steps will guide you to generate your DLL injection solution! You will end up with two files 3 | - `injector.exe`: Performs DLL injection in the game 4 | - `gameModder.dll`: Contains all the logic for your mods 5 | 6 | To generate the files, you need to follow this steps 7 | 1. [Generate a dump.cs](#generate-dump) 8 | 2. [Define your rules and run the il2cpp-modder generator](#rules) 9 | 3. [Compile the output](#compiling) 10 | 4. [Execute your mod](#executing) 11 | 12 | ## Generate dump 13 | In this step we are going to run the Il2CppDumper to get our `dump.cs` file. 14 | You can generate the `dump.cs` and a base [rules.js](#rules) by running 15 | ```bash 16 | il2cpp-modder generate-dump 17 | ``` 18 | You will be prompted for the game folder (where the game `.exe` is located). 19 | 20 | If you don't want to be prompted, you can supply the game folder as an argument 21 | ```bash 22 | il2cpp-modder generate-dump [game folder] 23 | ``` 24 | If everything is where it should be, that's it! You should now have an output directory called `[game]-il2cppdumper-output` including the `dump.cs`. Also, a base `rules.js` will be generated for you! 25 | 26 | If a file needed for the Il2CppDumper analysis is missing from its usual location, you will be prompted for it 27 | - Il2CppDumper.exe: Only if it is not on PATH. It is located on your Il2CppDumper installation folder 28 | - GameAssembly.dll: It is usually in the game folder 29 | - global-metadata.dat : It is usually in the [game folder]/[gameName_Data]/il2cpp_data/Metadata 30 | 31 | 32 | ## Rules 33 | You need a [rules](https://github.com/juanmjacobs/il2cpp-modder/tree/main/doc/rules.md) file telling `il2cpp-modder` what you'd like to mod! 34 | Once you have your rules ready, run the generator! 35 | ```bash 36 | il2cpp-modder rules.js 37 | ``` 38 | You should now have all your dll and dll injection code in your defined output folder. 39 | 40 | ## Compiling 41 | You can compile the files with the tool of your choice. Here are the steps for Visual Studio! 42 | - To get the `injector.exe` 43 | - Create a new C++ console project and name it `injector`. 44 | - Make it always run as administrator! (properties -> Linker -> Manifest File -> UAC Execution Level -> requireAdministrator) 45 | - Set strings to multibyte (properties -> Advanced -> Character Set -> Use Multi-Byte Character Set) 46 | - Add the `output/injector` files to the project. 47 | - Compile it and the `injector.exe` will be in the `Debug` folder. 48 | - To get the `gameModder.dll` 49 | - Create a new C++ Dynamic Linked Library project and name it `gameModder`. 50 | - Add the `output/gameModder` files to the project. 51 | - Compile it and the `gameModder.dll` will be in the `Debug` folder. 52 | 53 | _TIP: Be sure to add the `.cpp` as source files and the `.h` files as header files_ 54 | _TIP: In some versions of visual studio pch.h is not included. Just delete the include "pch.h" from the sources_ 55 | 56 | 57 | ## Executing 58 | - Copy the `injector.exe` and `gameModder.dll` to the game folder 59 | - Run the game executable as usual 60 | - Run the injector.exe 61 | 62 | That's it! Your methods will be automatically hooked as per your rule definitions. A modding console will appear for manual memory hacks and monitoring. 63 | -------------------------------------------------------------------------------- /il2cpp-modder.js: -------------------------------------------------------------------------------- 1 | const generateDump = require("./src/entryPoints/generateDump"); 2 | const generateDllInjectionProjects = require("./src/entryPoints/generateDllInjectionProjects"); 3 | 4 | const cliArgument = process.argv[2]; 5 | if(!cliArgument) { 6 | console.log("Please supply generate-dump or rules file.\nExamples\n il2cpp-modder generate-dump\n il2cpp-modder myRules.js"); 7 | return; 8 | } 9 | if(cliArgument == "generate-dump") { 10 | return generateDump(); 11 | } else { 12 | return generateDllInjectionProjects(cliArgument); 13 | } -------------------------------------------------------------------------------- /il2cpp-modder.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "follow_symlinks": true, 5 | "path": ".", 6 | "folder_exclude_patterns": ["compiled","public",".*", "ci", "dist", "node_modules", "bower_components", "client/bower_components"], 7 | "file_exclude_patterns": ["*.sublime-workspace"] 8 | } 9 | ], 10 | "settings": { 11 | "tab_size": 2, 12 | "translate_tabs_to_spaces": true 13 | } 14 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | module.exports = require("./il2cpp-modder"); 3 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install 4 | npm install -g . 5 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "il2cpp-modder", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bluebird": { 8 | "version": "3.7.2", 9 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 10 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 11 | }, 12 | "lodash": { 13 | "version": "4.17.20", 14 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 15 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 16 | }, 17 | "mkdirp": { 18 | "version": "1.0.4", 19 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 20 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "il2cpp-modder", 3 | "version": "1.0.0", 4 | "description": "Generate CPP source for modding il2cpp games via dll injection", 5 | "main": "il2cpp-modder", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/juanmjacobs/il2cpp-modder.git" 12 | }, 13 | "keywords": [ 14 | "il2cpp", 15 | "game", 16 | "unity", 17 | "modding" 18 | ], 19 | "author": "Juan Jacobs", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/juanmjacobs/il2cpp-modder/issues" 23 | }, 24 | "homepage": "https://github.com/juanmjacobs/il2cpp-modder#readme", 25 | "dependencies": { 26 | "bluebird": "^3.7.2", 27 | "lodash": "^4.17.20", 28 | "mkdirp": "^1.0.4" 29 | }, 30 | "bin": { 31 | "il2cpp-modder": "index.js" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rules.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | game: { 3 | path: "C:\\Users\\juan\\Desktop\\jackbox\\Among.Us.v2020.12.9s.Incl.Pets\\Among.Us.v2020.12.9s.Incl.Pets\\Among Us", 4 | exeName: "Among Us" 5 | }, 6 | dump: { 7 | path: "C:\\Users\\juan\\producteca\\il2cpp-modder\\Among_Us-il2cppdumper-output\\dump.cs", 8 | }, 9 | hooks: { 10 | methods: [{ 11 | className: "FFGALNAPKCD", 12 | name: "GetTruePosition", 13 | trampolineHookBytes: 6, 14 | mods: [{ //See Mod Types 15 | type: "pathMemoryHack", 16 | args: { 17 | paths: [{ 18 | name: "Player speed", 19 | path: "MyPhysics.Speed" 20 | },{ 21 | name: "Player is moveable", 22 | path: "moveable", 23 | value: "true" 24 | }] 25 | } 26 | }] 27 | }] 28 | }, 29 | output: "output" //optional 30 | } -------------------------------------------------------------------------------- /src/domain/dumpReader.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const _ = require("lodash"); 3 | const Promise = require("bluebird"); 4 | const { isPathMemoryHack } = require("./projectGenerators/templates/gameModder/mods/hookUtils"); 5 | 6 | const throwError = message => { 7 | console.log("\n\n", "[ERROR]", message, "\n\n"); 8 | throw new Error(message); 9 | } 10 | 11 | module.exports = class DumpReader { 12 | static load({ dump }) { 13 | return fs.readFileAsync(dump.path) 14 | .then(dumpFile => dumpFile.toString("utf-8")) 15 | .then(dump => new DumpReader({ dump })); 16 | } 17 | constructor(it) { 18 | _.assign(this, it); 19 | this.dumpLines = this.dump.split("\n"); 20 | this.methodInfo = this.methodInfo.bind(this); 21 | } 22 | 23 | methodInfo(options) { 24 | const { className, name, mods } = options; 25 | const classIndex = this._findClassIndex(className); 26 | const { index, line, classLines } = this._findMethodInClass(className, name); 27 | const relativeRvaIndex = index - 1; 28 | const rva = classLines[relativeRvaIndex].split(":")[1].split(" ")[1].trim(); 29 | const methodParts = line.split("("); 30 | const returnType = methodParts[0].split(" ").map(it => it.trim()).filter(it => !_(["virtual", "override", "async"]).includes(it))[1]; 31 | 32 | const parameters = methodParts[1].split(")")[0].split(",") 33 | .map(it => it.trim()) 34 | .map(it => { 35 | const [ type, name ] = it.split(" "); 36 | return { type, name }; 37 | }) 38 | .filter(it => it.type && it.name); 39 | 40 | console.log(`Found method ${line} in line ${classIndex + index + 1}. RVA: ${rva}`); 41 | const paths = isPathMemoryHack(options) ? this._paths(options) : []; 42 | return { ...options, methodIndex: index, rva, classIndex, relativeRvaIndex, parameters, returnType, paths }; 43 | } 44 | 45 | _paths({ className, name, mods: [ { args: { paths } } ] }) { 46 | const validPaths = paths.map((path) => { 47 | try { 48 | return this._pathInfo({ entryClass: className, ...path }) 49 | } catch(e) { 50 | return null; 51 | } 52 | }) 53 | .filter(it => it); 54 | 55 | if(_.isEmpty(validPaths)) { 56 | throwError(`No valid paths for mod ${className}.${name}`); 57 | } 58 | return validPaths; 59 | } 60 | 61 | _pathInfo(options) { 62 | const { entryClass, path } = options; 63 | const fieldNames = path.split("."); 64 | const fields = []; 65 | fieldNames.forEach((field, i) => { //TODO: To reduce? 66 | const className = i == 0 ? entryClass : fields[i - 1].type; 67 | const fieldInfo = this._fieldInfo(className, field); 68 | fields.push(fieldInfo); 69 | }); 70 | return { ...options, fields }; 71 | } 72 | 73 | _fieldInfo(className, field) { 74 | console.log(`Searching field ${field} of class ${className}`); 75 | const classIndex = this._findClassIndex(className); 76 | const { index, line } = this._findFieldInClass(className, field); 77 | console.log(`Found field ${field} of class ${className}: ${line} in line ${classIndex + index + 1}`) 78 | const offset = line.split("//")[1].trim(); 79 | const type = _(line.split(field)[0].split(" ")).compact().last().trim(); 80 | return { className, field, type, offset }; 81 | } 82 | 83 | _findClassIndex(className) { 84 | const __notFound = () => { 85 | throwError(`Class ${className} not found in dump.cs`); 86 | }; 87 | try { 88 | const classIndex = _.findIndex(this.dumpLines, it => 89 | _([this._classDefinition(className), this._abstractClassDefinition(className)]) 90 | .some(definition => _.includes(it, definition)) 91 | ); 92 | if(classIndex == -1) __notFound() 93 | console.log(`Found class ${className} in line ${classIndex + 1}`) 94 | return classIndex; 95 | } catch (e) { 96 | __notFound() 97 | } 98 | } 99 | 100 | _findFieldInClass(className, field) { 101 | return this._findInClass(className, `${field};`); 102 | } 103 | 104 | _findMethodInClass(className, method) { 105 | return this._findInClass(className, `${method}(`); 106 | } 107 | 108 | _findInClass(className, search) { 109 | const classLines = this._classLines(className); 110 | const index = _.findIndex(classLines, it => _.includes(it, search)); 111 | if(index == -1) { 112 | throwError(`${search} not found in ${className}`); 113 | } 114 | return { index, line: classLines[index], classLines }; 115 | } 116 | 117 | _classLines(className) { 118 | const classBody = this.dump.split(this._classDefinition(className))[1] || this.dump.split(this._abstractClassDefinition(className))[1]; 119 | const lines = classBody.split("\n"); 120 | return lines; 121 | } 122 | 123 | _classDefinition(className) { 124 | return `public class ${className} `; 125 | } 126 | 127 | _abstractClassDefinition(className) { 128 | return `public abstract class ${className} `; 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /src/domain/projectGenerators/dllInjector.js: -------------------------------------------------------------------------------- 1 | const ProjectGenerator = require("./projectGenerator"); 2 | const injectorTemplate = require("./templates/injector/injector"); 3 | 4 | module.exports = class DllInjector extends ProjectGenerator { 5 | constructor(rules) { 6 | super(rules) 7 | } 8 | 9 | generate(metadata) { 10 | this.writeFile("injector", "injector.cpp", injectorTemplate(this.rules, metadata)); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/domain/projectGenerators/gameModderDll.js: -------------------------------------------------------------------------------- 1 | const Promise = require("bluebird"); 2 | const ProjectGenerator = require("./projectGenerator"); 3 | const dllMainTemplate = require("./templates/gameModder/dllMain"); 4 | const utilsTemplate = require("./templates/gameModder/utils"); 5 | const utilsHeaderTemplate = require("./templates/gameModder/utilsHeader"); 6 | const trampolineHookTemplate = require("./templates/gameModder/trampolineHook"); 7 | const trampolineHookHeaderTemplate = require("./templates/gameModder/trampolineHookHeader"); 8 | const hookingTemplate = require("./templates/gameModder/hooking"); 9 | const hookingHeaderTemplate = require("./templates/gameModder/hookingHeader"); 10 | const commandLoopTemplate = require("./templates/gameModder/commandLoop"); 11 | const commandLoopHeaderTemplate = require("./templates/gameModder/commandLoopHeader"); 12 | const modelsHeaderTemplate = require("./templates/gameModder/modelsHeader"); 13 | 14 | module.exports = class GameModderDll extends ProjectGenerator { 15 | constructor(rules) { 16 | super(rules) 17 | } 18 | 19 | generate(metadata) { 20 | return Promise.map([ 21 | { name: "dllmain.cpp", content: dllMainTemplate(this.rules, metadata) }, 22 | { name: "utils.cpp", content: utilsTemplate(this.rules, metadata) }, 23 | { name: "utils.h", content: utilsHeaderTemplate(this.rules, metadata) }, 24 | { name: "trampolineHook.cpp", content: trampolineHookTemplate(this.rules, metadata) }, 25 | { name: "trampolineHook.h", content: trampolineHookHeaderTemplate(this.rules, metadata) }, 26 | { name: "hooking.cpp", content: hookingTemplate(this.rules, metadata)}, 27 | { name: "hooking.h", content: hookingHeaderTemplate(this.rules, metadata)}, 28 | { name: "commandLoop.cpp", content: commandLoopTemplate(this.rules, metadata)}, 29 | { name: "commandLoop.h", content: commandLoopHeaderTemplate(this.rules, metadata)}, 30 | { name: "models.h", content: modelsHeaderTemplate(this.rules, metadata)}, 31 | ], ({ name, content }) => this.writeFile("gameModder", name, content)); 32 | } 33 | } -------------------------------------------------------------------------------- /src/domain/projectGenerators/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DllInjector: require("./dllInjector"), 3 | GameModderDll: require("./gameModderDll"), 4 | } -------------------------------------------------------------------------------- /src/domain/projectGenerators/projectGenerator.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const Promise = require("bluebird"); 4 | const mkdirp = require('mkdirp') 5 | Promise.promisifyAll(fs); 6 | 7 | module.exports = class ProjectGenerator { 8 | constructor(rules) { 9 | this.rules = rules; 10 | this.output = rules.output || "output"; 11 | ; //possible race condition 12 | } 13 | 14 | generate(it) { 15 | console.log("Generating project with metadata", JSON.stringify(it)); 16 | } 17 | 18 | writeFile(directory, name, content) { 19 | console.log("About to write", directory, name) 20 | return this._createOutputDirectory(directory) 21 | .then(directoryPath => path.join(directoryPath, name)) 22 | .then(filePath => fs.writeFileAsync(filePath, content)); 23 | } 24 | 25 | _createOutputDirectory(directory) { 26 | const outputPath = path.join(this.output, directory); 27 | return Promise.try(() => mkdirp(outputPath)) 28 | .thenReturn(outputPath); 29 | } 30 | 31 | 32 | } -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/commandLoop.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const { pathMemoryHackHooks, hookDataThis, hookDataPath, pathFinalType, cppParameterType } = require("./mods/hookUtils"); 3 | 4 | const _variableName = sentence => sentence.split("=")[0].split(" ")[1].trim(); 5 | const _pathName = ({ name, path }) => name || `${path.entryClass}.${path}`; 6 | 7 | const _traversePath = (hook, path) => { 8 | const { fields } = path; 9 | const fieldType = `${_.last(fields).type}*`; //TODO EXTRACT LOGIC MODELSHEADER.JS 10 | const fieldName = hookDataPath(hook, fields); //TODO EXTRACT LOGIC MODELSHEADER.JS 11 | const initialPointer = hookDataThis(hook); 12 | const hookedInitialPointer = `(*hookedData).${initialPointer}`; 13 | const indirectionSentences = [`uintptr_t ${initialPointer} = ${hookedInitialPointer};`] 14 | fields.forEach(({ field, offset, type }, i) => { 15 | const pointerType = i == fields.length - 1? type : "uintptr_t"; 16 | const previousVariableName = _variableName(indirectionSentences[i]); 17 | const indirectionSentence = `${pointerType}* ${previousVariableName}_${field} = (${pointerType}*)(${i?"*":""}${previousVariableName} + ${offset});`; 18 | indirectionSentences.push(indirectionSentence); 19 | }) 20 | return ` 21 | //Traverse ${path.entryClass}.${path.path} 22 | if(${hookedInitialPointer}) { 23 | ${indirectionSentences.join("\n\t\t")} 24 | (*hookedData).${fieldName} = ${_variableName(_.last(indirectionSentences))}; 25 | } 26 | `; 27 | } 28 | 29 | const _availableCommand = (hook, path, i) => `${i+1}) Change ${_pathName(path)}`; 30 | 31 | const _commandCase = (hook, path, i) => { 32 | const name = _pathName(path); 33 | const property = hookDataPath(hook, path.fields); 34 | const type = cppParameterType(pathFinalType(path)); 35 | const inputType = type == "bool" ? "int" : type; 36 | const scanfFormat = { 37 | float: "%f", 38 | int: "%d", 39 | "char*": "%s" 40 | }[inputType] || "%d"; 41 | 42 | const hookedPointer = `populatedData.${property}`; 43 | const target = `*(${hookedPointer})`; 44 | return ` 45 | case ${i + 1}: // Change ${name} 46 | { 47 | if(!${hookedPointer}) 48 | { 49 | printf("${name} not hooked yet!"); 50 | break; 51 | } 52 | printf("Your current ${name} is: ${scanfFormat}\\n", ${target}); 53 | ${inputType} newValue; 54 | printf("Enter new ${name}: "); 55 | scanf_s("${scanfFormat}", &newValue); 56 | ${target} = newValue; 57 | break; 58 | }` 59 | } 60 | 61 | const _autoValueSetterLogger = (hook, path, i) => { 62 | return `printf("Will keep '${_pathName(path)}' set at '${path.value}'\\n");`; 63 | } 64 | 65 | const _autoValueSetter = (hook, path, i) => { 66 | const name = _pathName(path); 67 | const property = hookDataPath(hook, path.fields); 68 | const type = cppParameterType(pathFinalType(path)); 69 | const pointer = `populatedData.${property}`; 70 | return ` 71 | if (${pointer}) 72 | { 73 | *(${pointer}) = ${path.value}; 74 | }`; 75 | } 76 | 77 | module.exports = (rules, metadata) => { 78 | const hooks = pathMemoryHackHooks(metadata); 79 | const populateHookedPaths = _(hooks).flatMap(hook => hook.paths.map(path =>_traversePath(hook, path))).value(); 80 | const availableCommands = _(hooks).flatMap(hook => hook.paths.filter(it => !it.value).map((path, i) => _availableCommand(hook, path, i))).value(); 81 | const commandCases = _(hooks).flatMap(hook => hook.paths.filter(it => !it.value).map((path, i) => _commandCase(hook, path, i) )).value(); 82 | const autoValueSetterLoggers = _(hooks).flatMap(hook => hook.paths.filter(it => it.value).map((path, i) => _autoValueSetterLogger(hook, path, i) )).value(); 83 | const autoValueSetters = _(hooks).flatMap(hook => hook.paths.filter(it => it.value).map((path, i) => _autoValueSetter(hook, path, i) )).value(); 84 | const inputLoop = !_.isEmpty(commandCases)? `int command = 0; 85 | while (true) 86 | { 87 | command = 0; 88 | printf("\\nAvailable commands:\\n ${availableCommands.join("\\n\\n ")}\\n"); 89 | printf("\\n\\n\\nEnter a command: \\n"); 90 | cin >> command; 91 | populateHookedPaths(hookedData); 92 | HookedData populatedData = *hookedData; 93 | if (command != 0) 94 | { 95 | switch (command) 96 | { 97 | ${commandCases.join("\n\t")} 98 | default: 99 | printf("Invalid command\\n"); 100 | break; 101 | } 102 | } 103 | }` : ""; 104 | 105 | return `#include "pch.h" 106 | #include "models.h" 107 | #include "hooking.h" 108 | #include "utils.h" 109 | #include 110 | #include 111 | using namespace std; 112 | 113 | void populateHookedPaths(HookedData* hookedData) 114 | { 115 | ${populateHookedPaths.join("\n\t")} 116 | } 117 | 118 | void autoSetValues(HookedData* hookedData) 119 | { 120 | ${autoValueSetterLoggers.join("\n\t")} 121 | while (true) 122 | { 123 | populateHookedPaths(hookedData); 124 | HookedData populatedData = *hookedData; 125 | //Constantly set values 126 | ${autoValueSetters.join("\n\t")} 127 | } 128 | } 129 | 130 | void commandLoop(HookedData* hookedData) 131 | { 132 | printf("[] Assembly located at: %x\\n", (*hookedData).assembly); 133 | printf("\\nCommands will not work until pointers are populated!\\n\\n"); 134 | ${!_.isEmpty(autoValueSetters)? "thread autoSetValuesThread(autoSetValues, hookedData);" : ""} 135 | ${inputLoop} 136 | 137 | } 138 | 139 | void insertConsole() 140 | { 141 | AllocConsole(); 142 | FILE* f = new FILE(); 143 | freopen_s(&f, "CONOUT$", "w", stdout); 144 | freopen_s(&f, "CONIN$", "r", stdin); 145 | printf("[*] Injected...\\n"); 146 | uintptr_t* player = nullptr; 147 | HookedData hookedData = HookedData(); 148 | printf("[*] hookedData... %x\\n", &hookedData); 149 | 150 | hookData(&hookedData); 151 | commandLoop(&hookedData); 152 | } 153 | ` 154 | } 155 | -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/commandLoopHeader.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => `#pragma once 2 | void insertConsole(); 3 | ` -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/dllMain.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => `#include "pch.h" 2 | #include "commandLoop.h" 3 | #include 4 | 5 | 6 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 7 | { 8 | switch (ul_reason_for_call) 9 | { 10 | case DLL_PROCESS_ATTACH: 11 | { 12 | DisableThreadLibraryCalls(hModule); 13 | // Spawn a new thread for our console/cheats when we attach to the process 14 | HANDLE hThread = CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)insertConsole, hModule, 0, 0); 15 | if (hThread != nullptr) 16 | CloseHandle(hThread); 17 | break; 18 | } 19 | case DLL_THREAD_ATTACH: 20 | case DLL_THREAD_DETACH: 21 | case DLL_PROCESS_DETACH: 22 | break; 23 | } 24 | return TRUE; 25 | } 26 | ` 27 | -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/hooking.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const MODS = require("./mods"); 3 | const { hookFunctionName } = require("./mods/hookUtils"); 4 | 5 | const _toHook = (options) => { 6 | const { className, name, mods, trampolineHookBytes } = options; 7 | const functionName = hookFunctionName(options); 8 | const mod = mods[0]; //only one mod supported at the time 9 | console.log("mod",JSON.stringify(mod)) 10 | const definition = MODS[mod.type](options, mod, functionName); 11 | const invocation = `original_${functionName} = (t${functionName})TrampolineHook(${functionName}, hacked_${functionName}, ${trampolineHookBytes || "6"});`; 12 | return { definition, invocation }; 13 | } 14 | 15 | const _toHooks = (rules, metadata) => { 16 | const hooks = metadata.methodHooks.map(_toHook); 17 | const __join = property => _.map(hooks, property).join("\n"); 18 | return { definitions: __join("definition"), invocations: __join("invocation") }; 19 | } 20 | 21 | module.exports = (rules, metadata) => { 22 | const { definitions, invocations } = _toHooks(rules, metadata); 23 | return `#include "pch.h" 24 | #include "models.h" 25 | #include "trampolineHook.h" 26 | #include 27 | 28 | HookedData *myHookedData = nullptr; 29 | uintptr_t assemblyAddress = (uintptr_t) GetModuleHandleW(L"GameAssembly.dll"); 30 | ${definitions} 31 | 32 | void hookData(HookedData* hookedData) { 33 | myHookedData = hookedData; 34 | (*hookedData).assembly = assemblyAddress; 35 | ${invocations} 36 | } 37 | `; 38 | } -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/hookingHeader.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => `#pragma once 2 | #include "models.h" 3 | void hookData(HookedData*); 4 | ` -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/modelsHeader.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const { hookDataThis, hookDataPath, pathMemoryHackHooks, pathFinalType } = require("./mods/hookUtils"); 3 | 4 | const savedThisPointers = hooks => hooks 5 | .map(hookDataThis) 6 | .map(it => `uintptr_t ${it};`) 7 | 8 | 9 | const savedPaths = hooks => _(hooks) 10 | .flatMap(hook => hook.paths.map(({ fields }) => { 11 | const fieldType = `${pathFinalType({ fields })}*`; 12 | const fieldName = hookDataPath(hook, fields); 13 | return `${fieldType} ${fieldName};`; 14 | })) 15 | .value() 16 | 17 | module.exports = (rules, metadata) => { 18 | const hooks = pathMemoryHackHooks(metadata); 19 | 20 | return `#pragma once 21 | #include "pch.h" 22 | 23 | struct HookedData { 24 | uintptr_t assembly; 25 | ${savedThisPointers(hooks).join("\n\t")} 26 | ${savedPaths(hooks).join("\n\t")} 27 | }; 28 | ` 29 | } -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/mods/fixedReturnValue.js: -------------------------------------------------------------------------------- 1 | const { buildHook } = require("./hookUtils"); 2 | 3 | const fixedReturnValue = (options, mod, functionName) => { 4 | const { name } = options; 5 | const hackedBody = () => `return ${mod.args};`; 6 | const definition = buildHook(options, hackedBody); 7 | return definition; 8 | } 9 | 10 | module.exports = fixedReturnValue; -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/mods/hookUtils.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | 3 | const TYPE_MAPPINGS = { 4 | boolean: "bool", 5 | string: "char*", 6 | object: "void*", 7 | byte: "BYTE" 8 | } 9 | const startsWithUpperCase = text => text[0] == text[0].toUpperCase() 10 | 11 | const cppParameterType = type => { //TODO: improve type mappings 12 | const mappedType = TYPE_MAPPINGS[type]; 13 | if(mappedType) return mappedType; 14 | if(startsWithUpperCase(type)) return "void*"; 15 | return type; 16 | } 17 | 18 | const hookFunctionName = ({ className, name }) => `${className}_${name}` 19 | 20 | const buildHook = (options, hackedBody) => { 21 | const { name, rva, parameters, returnType } = options; 22 | const functionName = hookFunctionName(options); 23 | const cppReturnType = cppParameterType(returnType); 24 | const cppParameters = !_.isEmpty(parameters)? parameters.map(parameter => `${cppParameterType(parameter.type)} ${parameter.name}`) : []; 25 | const allCppParameters = ["void* thisReference"].concat(cppParameters).join(", "); 26 | return `//--------${functionName} hook------------ 27 | typedef ${cppReturnType} (*t${functionName})(${allCppParameters}); 28 | uintptr_t ${functionName}RVA = ${rva}; 29 | t${functionName} ${functionName} = (t${functionName})(assemblyAddress + ${functionName}RVA); 30 | t${functionName} original_${functionName}; 31 | ${cppReturnType} hacked_${functionName}(${allCppParameters}) 32 | { 33 | ${hackedBody()} 34 | } 35 | `; 36 | } 37 | 38 | const _hookDataSuffix = ({ className }, properties) => [ className ].concat(properties).join("_"); 39 | const hookDataThis = (options) => _hookDataSuffix(options, [options.name, "this"]); 40 | const hookDataPath = (options, fields) => _hookDataSuffix(options, fields.map(it => it.field)); 41 | const isPathMemoryHack = it =>_.some(it.mods, { type: "pathMemoryHack" }); 42 | const pathMemoryHackHooks = metadata => metadata.methodHooks.filter(isPathMemoryHack); 43 | const pathFinalType = ({ fields }) => _.last(fields).type; 44 | 45 | module.exports = { buildHook, hookDataThis, hookDataPath, cppParameterType, isPathMemoryHack, pathMemoryHackHooks, pathFinalType, hookFunctionName }; -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/mods/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | replaceImplementation: require("./replaceImplementation"), 3 | fixedReturnValue: require("./fixedReturnValue"), 4 | replaceArguments: require("./replaceArguments"), 5 | pathMemoryHack: require("./pathMemoryHack") 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/mods/pathMemoryHack.js: -------------------------------------------------------------------------------- 1 | const { buildHook, hookDataThis } = require("./hookUtils"); 2 | 3 | const pathMemoryHack = (options, mod, functionName) => { 4 | const { className, name, rva } = options; 5 | const property = hookDataThis(options); 6 | const thisName = `${name}This`; 7 | const hackedBody = () => ` 8 | uintptr_t ${thisName} = (uintptr_t)thisReference; 9 | if ((*myHookedData).${property} != ${thisName}) 10 | { 11 | printf("\\n(Reassigning ${property} from %x to %x)\\n", (*myHookedData).${property}, ${thisName}); 12 | (*myHookedData).${property} = ${thisName}; 13 | } 14 | return original_${functionName}(thisReference);`; 15 | 16 | const definition = buildHook(options, hackedBody) 17 | return definition; 18 | } 19 | 20 | module.exports = pathMemoryHack; -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/mods/replaceArguments.js: -------------------------------------------------------------------------------- 1 | const { buildHook } = require("./hookUtils"); 2 | 3 | const replaceArguments = (options, mod, functionName) => { 4 | const { name } = options; 5 | const hackedBody = () => `return original_${functionName}(thisReference, ${mod.args.join(", ")});` 6 | const definition = buildHook(options, hackedBody); 7 | return definition; 8 | } 9 | 10 | module.exports = replaceArguments; -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/mods/replaceImplementation.js: -------------------------------------------------------------------------------- 1 | const { buildHook } = require("./hookUtils"); 2 | 3 | const replaceImplementation = (options, mod, functionName) => { 4 | const { name } = options; 5 | const hackedBody = () => mod.args; 6 | const definition = buildHook(options, hackedBody); 7 | return definition; 8 | } 9 | 10 | module.exports = replaceImplementation; -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/trampolineHook.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => `#include "pch.h" 2 | 3 | BYTE JMP = 0xE9; 4 | BYTE NOP = 0x90; 5 | 6 | BYTE* CopyFunctionBeginningToGateway(void* sourceFunctionPointer, unsigned int trampolineBytes) 7 | { 8 | //Create Gateway. 9 | BYTE* gateway = (BYTE*)VirtualAlloc(0, trampolineBytes, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); 10 | 11 | //Write stolen bytes. 12 | memcpy_s(gateway, trampolineBytes, sourceFunctionPointer, trampolineBytes); 13 | return gateway; 14 | } 15 | 16 | void AddJmpToRestOfOriginalFunction(BYTE* gateway, void* sourceFunctionPointer, unsigned int trampolineBytes) 17 | { 18 | // Write jmp instruction 19 | *(gateway + trampolineBytes) = JMP; 20 | 21 | //Get the gateway to destination address (operand for jmp) 22 | uintptr_t gatewayRelativeAddr = (BYTE*)sourceFunctionPointer - gateway - 5; 23 | 24 | //Write address of gateway to jmp 25 | *(uintptr_t*)((uintptr_t)gateway + trampolineBytes + 1) = gatewayRelativeAddr; 26 | } 27 | 28 | void ReplaceOriginalFunctionForHackedFunction(void* sourceFunctionPointer, void* hackedFunctionPointer, unsigned int trampolineBytes) 29 | { 30 | if (trampolineBytes < 5) return; 31 | //The original function should jump to our hacked function. 32 | 33 | //Make the sourceFunction beginning memory writable 34 | DWORD currentProtection; 35 | VirtualProtect(sourceFunctionPointer, trampolineBytes, PAGE_EXECUTE_READWRITE, ¤tProtection); 36 | 37 | //Fill sourceFunction beginning memory with some good ol' NOPs 38 | memset(sourceFunctionPointer, NOP, trampolineBytes); 39 | 40 | //Write the jmp at the start of the source function 41 | *(BYTE*)sourceFunctionPointer = JMP; 42 | 43 | //Calculate the relaive address of our hacked function (operand for jmp) 44 | uintptr_t relativeAddress = (uintptr_t)hackedFunctionPointer - (uintptr_t)sourceFunctionPointer - 5; 45 | 46 | //Write address of hacked function to jmp 47 | *(uintptr_t*)((BYTE*)sourceFunctionPointer + 1) = relativeAddress; 48 | 49 | //Restore the original memory protection 50 | VirtualProtect(sourceFunctionPointer, trampolineBytes, currentProtection, ¤tProtection); 51 | } 52 | 53 | BYTE* TrampolineHook(void* sourceFunctionPointer, void* hackedFunctionPointer, unsigned int trampolineBytes) 54 | { 55 | if (trampolineBytes < 5) return 0; 56 | BYTE* gateway = CopyFunctionBeginningToGateway(sourceFunctionPointer, trampolineBytes); 57 | AddJmpToRestOfOriginalFunction(gateway, sourceFunctionPointer, trampolineBytes); 58 | ReplaceOriginalFunctionForHackedFunction(sourceFunctionPointer, hackedFunctionPointer, trampolineBytes); 59 | return gateway; 60 | }` -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/trampolineHookHeader.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => `#pragma once 2 | #include "pch.h" 3 | 4 | BYTE* TrampolineHook(void* sourceFunctionPointer, void* hackedFunctionPointer, unsigned int trampolineBytes); 5 | ` 6 | -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => `#include "pch.h" 2 | #include 3 | #include 4 | 5 | std::wstring s2ws(const std::string& s) 6 | { 7 | int len; 8 | int slength = (int)s.length() + 1; 9 | len = MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, 0, 0); 10 | wchar_t* buf = new wchar_t[len]; 11 | MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, buf, len); 12 | std::wstring r(buf); 13 | delete[] buf; 14 | return r; 15 | } 16 | 17 | void ErrorExit(LPTSTR lpszFunction) 18 | { 19 | // Retrieve the system error message for the last-error code 20 | 21 | LPVOID lpMsgBuf; 22 | LPVOID lpDisplayBuf; 23 | DWORD dw = GetLastError(); 24 | 25 | FormatMessage( 26 | FORMAT_MESSAGE_ALLOCATE_BUFFER | 27 | FORMAT_MESSAGE_FROM_SYSTEM | 28 | FORMAT_MESSAGE_IGNORE_INSERTS, 29 | NULL, 30 | dw, 31 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 32 | (LPTSTR)&lpMsgBuf, 33 | 0, NULL); 34 | 35 | // Display the error message and exit the process 36 | 37 | lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, 38 | (lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR)); 39 | StringCchPrintf((LPTSTR)lpDisplayBuf, 40 | LocalSize(lpDisplayBuf) / sizeof(TCHAR), 41 | TEXT("%s failed with error %d: %s"), 42 | lpszFunction, dw, lpMsgBuf); 43 | MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK); 44 | 45 | LocalFree(lpMsgBuf); 46 | LocalFree(lpDisplayBuf); 47 | ExitProcess(dw); 48 | } 49 | 50 | void log(std::string text) { 51 | MessageBox(0, s2ws(text).c_str(), L"Worker", MB_OK); 52 | } 53 | ` -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/gameModder/utilsHeader.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => `#pragma once 2 | #include "pch.h" 3 | #include 4 | #include 5 | 6 | std::wstring s2ws(const std::string& s); 7 | void ErrorExit(LPTSTR lpszFunction); 8 | void log(std::string text); 9 | ` -------------------------------------------------------------------------------- /src/domain/projectGenerators/templates/injector/injector.js: -------------------------------------------------------------------------------- 1 | module.exports = (rules, metadata) => { 2 | const exeName = `${rules.game.exeName.toLowerCase().replace(".exe","")}.exe`; 3 | return `#include 4 | #include 5 | #include 6 | 7 | DWORD GetProcId(const char* procName) 8 | { 9 | DWORD procId = 0; 10 | HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 11 | 12 | if (hSnap != INVALID_HANDLE_VALUE) 13 | { 14 | PROCESSENTRY32 procEntry; 15 | procEntry.dwSize = sizeof(procEntry); 16 | 17 | if (Process32First(hSnap, &procEntry)) 18 | { 19 | do 20 | { 21 | if (!_stricmp(procEntry.szExeFile, procName)) 22 | { 23 | procId = procEntry.th32ProcessID; 24 | break; 25 | } 26 | } while (Process32Next(hSnap, &procEntry)); 27 | } 28 | } 29 | CloseHandle(hSnap); 30 | return procId; 31 | } 32 | 33 | int main() 34 | { 35 | const char* dllPath = "gameModder.dll"; 36 | const char* procName = "${exeName}"; 37 | DWORD procId = 0; 38 | 39 | while (!procId) 40 | { 41 | printf("Process not found yet for executable ${rules.game.exeName}\\n"); 42 | procId = GetProcId(procName); 43 | Sleep(1000); 44 | } 45 | 46 | HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, 0, procId); 47 | 48 | if (hProc && hProc != INVALID_HANDLE_VALUE) 49 | { 50 | void* loc = VirtualAllocEx(hProc, 0, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); 51 | 52 | WriteProcessMemory(hProc, loc, dllPath, strlen(dllPath) + 1, 0); 53 | 54 | HANDLE hThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, loc, 0, 0); 55 | 56 | if (hThread) 57 | { 58 | CloseHandle(hThread); 59 | } 60 | } 61 | 62 | if (hProc) 63 | { 64 | CloseHandle(hProc); 65 | } 66 | return 0; 67 | }` 68 | } 69 | -------------------------------------------------------------------------------- /src/entryPoints/generateDllInjectionProjects/generateDllInjectionProjects.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const _ = require("lodash"); 3 | const path = require("path"); 4 | const Promise = require("bluebird"); 5 | const DumpReader = require("../../domain/dumpReader"); 6 | const ProjectGenerators = require("../../domain/projectGenerators"); 7 | Promise.promisifyAll(fs); 8 | 9 | module.exports = generateDllInjectionProjects = (rulesPath) => { 10 | 11 | const absolutePath = path.join(process.cwd(), rulesPath); 12 | const rules = require(absolutePath); 13 | 14 | const dllInjectorGenerator = new ProjectGenerators.DllInjector(rules); 15 | const gameModderGenerator = new ProjectGenerators.GameModderDll(rules); 16 | if(!rules.game || !rules.dump || !rules.hooks || !rules.hooks.methods) { 17 | console.log("Please supply a valid rules file. Check the docs if you need to!") 18 | return; 19 | } 20 | DumpReader.load(rules) 21 | .then(dumpReader => { 22 | return Promise.props({ 23 | methodHooks: Promise.map(rules.hooks.methods, methodHook => Promise.try(() => dumpReader.methodInfo(methodHook)).reflect()) 24 | .filter(it => it.isFulfilled()) 25 | .map(it => it.value()) 26 | }) 27 | }) 28 | .tap(({ methodHooks }) => { 29 | if(_.isEmpty(methodHooks)) { 30 | console.log(`No valid method hooks found in ${absolutePath}. Aborting.`); 31 | process.exit(0); 32 | } 33 | }) 34 | .tap(it => dllInjectorGenerator.generate(it)) 35 | .tap(it => gameModderGenerator.generate(it)) 36 | } 37 | -------------------------------------------------------------------------------- /src/entryPoints/generateDllInjectionProjects/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./generateDllInjectionProjects"); -------------------------------------------------------------------------------- /src/entryPoints/generateDump/chooseFile.bat: -------------------------------------------------------------------------------- 1 | <# : chooser.bat 2 | :: launches a File... Open sort of file chooser and outputs choice(s) to the console 3 | :: https://stackoverflow.com/a/15885133/1683264 4 | 5 | @echo off 6 | setlocal 7 | 8 | for /f "delims=" %%I in ('powershell -noprofile "iex (${%~f0} | out-string)"') do ( 9 | echo %%~I 10 | ) 11 | goto :EOF 12 | 13 | : end Batch portion / begin PowerShell hybrid chimera #> 14 | 15 | Add-Type -AssemblyName System.Windows.Forms 16 | $f = new-object Windows.Forms.OpenFileDialog 17 | $f.InitialDirectory = pwd 18 | $f.Filter = "All Files (*.*)|*.*" 19 | $f.ShowHelp = $true 20 | $f.Multiselect = $false 21 | [void]$f.ShowDialog() 22 | if ($f.Multiselect) { $f.FileNames } else { $f.FileName } -------------------------------------------------------------------------------- /src/entryPoints/generateDump/chooseFolder.bat: -------------------------------------------------------------------------------- 1 | :: chooseFolder.bat 2 | :: launches a folder chooser and outputs choice to the console 3 | :: https://stackoverflow.com/a/15885133/1683264 4 | 5 | @echo off 6 | setlocal 7 | 8 | set "psCommand="(new-object -COM 'Shell.Application')^ 9 | .BrowseForFolder(0,'Select your game folder',0,0).self.path"" 10 | 11 | for /f "usebackq delims=" %%I in (`powershell %psCommand%`) do set "folder=%%I" 12 | 13 | setlocal enabledelayedexpansion 14 | echo !folder! 15 | endlocal -------------------------------------------------------------------------------- /src/entryPoints/generateDump/generateDump.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const _ = require("lodash"); 3 | const path = require("path"); 4 | const Promise = require("bluebird"); 5 | const { exec } = require("child_process"); 6 | const execAsync = command => { 7 | if(!command.startsWith("\"") && !command.endsWith("\"")) { 8 | command = "\"" + command + "\""; 9 | } 10 | console.log("Executing:\n "+ command+"\n"); 11 | return Promise.promisify(exec)(command); 12 | }; 13 | Promise.promisifyAll(fs); 14 | 15 | const promptChooseFolderDialog = () => execAsync(path.join(__dirname, "chooseFolder.bat")).then(it => it.trim()); 16 | const promptChooseFileDialog = () => execAsync(path.join(__dirname, "chooseFile.bat")).then(it => it.trim()); 17 | const lastPart = aPath => _.last(aPath.split(path.sep)); 18 | 19 | const fileExistsOrPrompt = (gameName, filePath) => { 20 | console.log("Searching for", filePath); 21 | const fileName = lastPart(filePath); 22 | return Promise.try(() => fs.existsSync(filePath)) 23 | .then(exists => { 24 | if(exists) { 25 | console.log(filePath, "found!") 26 | return filePath; 27 | } 28 | console.log(filePath, `not found, please select a the ${fileName} for ${gameName}!`); 29 | return promptChooseFileDialog(); 30 | }) 31 | } 32 | 33 | const generateRules = (gamePath, gameName, dumpPath) => { 34 | console.log("Generating rules.js!"); 35 | const __escapePath = aPath => aPath.replace(/\\/g,"\\\\"); 36 | const rulesContent = `module.exports = { 37 | game: { 38 | path: "${__escapePath(gamePath)}", 39 | exeName: "${gameName}" 40 | }, 41 | dump: { 42 | path: "${__escapePath(dumpPath)}", 43 | }, 44 | hooks: { 45 | methods: [{ 46 | className: "SomeClass", 47 | name: "SomeMethod", 48 | trampolineHookBytes: 6, 49 | mods: [{ 50 | type: "modType", 51 | args: "modArgs" 52 | }] 53 | }] 54 | }, 55 | output: "output" //optional 56 | }` 57 | return fs.writeFileAsync("rules.js", rulesContent); 58 | } 59 | 60 | const il2CppDumper = (gameName, { gameAssemblyDllPath, globalMetadataDatPath }) => { 61 | console.log("Trying to execute Il2CppDumper.exe in PATH"); 62 | const dumperOutput = process.argv[4] || `${gameName.replace(/ /g, "_")}-il2cppdumper-output`; 63 | const execDumper = il2CppDumperExe => { 64 | if(!il2CppDumperExe) { return Promise.reject("No Il2CppDumper executable supplied!"); } 65 | console.log("Creating output directory", dumperOutput); 66 | return execAsync(`rm -rf "${dumperOutput}"`).reflect() 67 | .then(() => execAsync(`mkdir "${dumperOutput}"` ).reflect()) 68 | .then(inspection => console.log(`output directory ${dumperOutput} ${inspection.isFulfilled()? "" :" not "}created`)) 69 | .then(() => execAsync(`${il2CppDumperExe} "${gameAssemblyDllPath}" "${globalMetadataDatPath}" "${dumperOutput}"`)) 70 | .tap(it => console.log(`\nIl2CppDumper output: \n${it}\n`)); 71 | 72 | }; 73 | return execDumper("Il2CppDumper.exe") 74 | .catch(e => /not recognized|no se reconoce/gi.test(e.toString()), () => { 75 | console.log('Il2CppDumper.exe not found in PATH! Please select your Il2CppDumper.exe'); 76 | return promptChooseFileDialog() 77 | .then(it => `"${it.trim()}"`) 78 | .tap(execDumper) 79 | }) 80 | .catch(e => {}) 81 | .thenReturn(path.join(process.cwd(), dumperOutput, "dump.cs")) 82 | .finally(() => console.log(`Done! Check ${dumperOutput} for the results`)); 83 | } 84 | 85 | module.exports = () => { 86 | console.log("Will try to generate dump.cs using Il2CppDumper!") 87 | const $gameFolder = process.argv[3] ? Promise.resolve(process.argv[3]) : promptChooseFolderDialog(); 88 | let gameName, gameFolder; 89 | return $gameFolder 90 | .tap(folder => console.log("Game located at:", folder)) 91 | .tap(folder => { 92 | gameFolder = folder; 93 | gameName = _.last(folder.split(path.sep)); 94 | }) 95 | .tap(() => console.log('Game name', gameName)) 96 | .then(folder => { 97 | const gameAssemblyDll = path.join(folder, "GameAssembly.dll"); 98 | const globalMetadataDat = path.join(folder, `${gameName}_Data`, "il2cpp_data", "Metadata", "global-metadata.dat"); 99 | return Promise.props({ 100 | gameAssemblyDllPath: fileExistsOrPrompt(gameName, gameAssemblyDll), 101 | globalMetadataDatPath: fileExistsOrPrompt(gameName, globalMetadataDat) 102 | }) 103 | }) 104 | .tap(filePaths => console.log('Will use the following paths for Il2CppDumper', filePaths)) 105 | .then(filePaths => il2CppDumper(gameName, filePaths)) 106 | .tap(dumpPath => generateRules(gameFolder, gameName, dumpPath)) 107 | } 108 | -------------------------------------------------------------------------------- /src/entryPoints/generateDump/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./generateDump"); -------------------------------------------------------------------------------- /testFiles/rules.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "game": { 3 | path: "path/to/game", 4 | exeName: "Among Us" 5 | }, 6 | //"output": "path/to/output/dir" //optional, default "output" 7 | "dump": { 8 | path: __dirname + "/dump.cs", 9 | }, 10 | "hooks": { 11 | methods: [{ 12 | className: "FFGALNAPKCD", 13 | name: "GetTruePosition", 14 | trampolineHookBytes: 6, 15 | mods: [{ //See Mod Types 16 | type: "pathMemoryHack", 17 | args: { 18 | paths: [{ 19 | name: "Player speed", 20 | path: "MyPhysics.Speed" 21 | },{ 22 | name: "Player is moveable", 23 | path: "moveable", 24 | value: "true" 25 | }] 26 | } 27 | }] 28 | }, { 29 | className: "MLPJGKEACMM", 30 | name: "SetCoolDown", 31 | trampolineHookBytes: 11, 32 | mods: [{ 33 | type: "replaceArguments", 34 | args: ["JECMPCICNEB", "0.0f"] 35 | }] 36 | }] 37 | }, 38 | } --------------------------------------------------------------------------------