├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md ├── RELEASE.md ├── docs └── todo.py ├── img └── logo.jpg ├── invade ├── __init__.py ├── main.py ├── version.py └── winapi.py └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm 2 | .idea/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chad Gosselin 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Invade 2 | 3 | ![Invade](https://i.imgur.com/nGSXlnr.jpg "Invade") 4 | 5 | Invade is a Python 3 library for interacting with Windows processes. Common uses: software security and malware research, reverse engineering, and PoCs. 6 | 7 | [https://github.com/cgio/invade](https://github.com/cgio/invade) 8 | 9 | [https://pypi.org/project/invade](https://pypi.org/project/invade) 10 | 11 | 12 | **There are four classes in main.py:** 13 | 14 | * **Me:** for operating environment info 15 | * **Scout:** for process discovery 16 | * **Target:** for target process info 17 | * **Tool:** for main operation 18 | 19 | **Common use case overview:** 20 | 21 | 1. Create an instance of Me and check the operating environment for compatibility. 22 | 2. Use Scout to get a list of active processes and the desired PID (process identifier). 23 | 3. Instantiate Target using the PID obtained by Scout. 24 | 4. Check Target instance properties for information about the target process. 25 | 5. Interact with the target process using Tool methods. 26 | 27 | Another common use case is Invade's relatively fast byte pattern search with wildcard support. Operation is similar to [IDA's](https://www.hex-rays.com) "sequence of bytes" search. Use Tool.search_file_pattern() to search through a file on disk. 28 | 29 | Tool.memory_read_pointers() is also useful. With it, you can read through a series of dynamically allocated memory pointers in another process. The method accepts a string containing a start address and relative pointers with common arithmetic operators. 30 | 31 | **Refer to main.py for additional information and usage instructions.** 32 | 33 | **Refer to RELEASE.md for release notes.** 34 | 35 | ## Installation 36 | Python 3.6+ is required 37 | 38 | `pip install invade` 39 | 40 | Install Keystone for Python. See [Python module for Windows - Binaries](http://www.keystone-engine.org/download/). 41 | 42 | Install Capstone for Python. See [Python module for Windows - Binaries](https://www.capstone-engine.org/download.html). 43 | 44 | ## Files 45 | Inside /invade: 46 | * **main.py:** contains all main code and classes 47 | * **winapi.py:** contains Windows API code 48 | * **version.py:** contains version information 49 | 50 | ## Example Projects 51 | * [invade_debug_32](https://github.com/cgio/invade_debug_32): Windows x86 32-bit non-attaching debug tool 52 | * [invade_keepass](https://github.com/cgio/invade_keepass): KeePass password exfiltration 53 | 54 | ## Authors 55 | Chad Gosselin ([https://github.com/cgio](https://github.com/cgio)) 56 | 57 | ## Credits 58 | Thank you to the following projects: 59 | 60 | * [Keystone](https://github.com/keystone-engine/keystone): assembler framework 61 | * [Capstone](https://github.com/aquynh/capstone): disassembler framework 62 | * [Cuckoo Sandbox](https://github.com/cuckoosandbox/cuckoo): malware analysis framework 63 | * [pefile](https://github.com/erocarrera/pefile): PE file framework 64 | 65 | ## License 66 | This project is licensed under the MIT License. See [LICENSE.md](LICENSE.md) for details. This project is for educational purposes only. Use at your own risk. -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Release Notes 2 | 3 | #### 0.0.6 4 | **Sep. 18, 2018** 5 | * Update method names and args to use "mc" (for machine code) instead of "opcodes" 6 | * Add support for Capstone (https://github.com/aquynh/capstone) 7 | * Add Tool.get_asm() to convert machine code to assembly 8 | * Add partial process name pattern matching to Tool.get_pids_by_process_name() 9 | * Add case_sensitive and contains args to Scout init 10 | * Add Tool.close_handle() 11 | * Add process handle close/cleanup to Target() 12 | * Add Tool.convert_int_pointer_to_str_hex() 13 | * Add is_x64 arg to Tool.get_mc() 14 | * Add Tool.close_handle() 15 | * Add constant X86_MC_INSN_MAX 16 | * Change Tool.get_mc() to @staticmethod 17 | * Rename Tool.get_mc_len() to Tool.get_mc_size() 18 | * Update Tool.get_file_version() for pefile library update 19 | * Fix Tool.is_process_x64() to correctly return False for 32-bit processes 20 | * Remove self.is_x64 from Tool() -------------------------------------------------------------------------------- /docs/todo.py: -------------------------------------------------------------------------------- 1 | # TODO(project author): In invade.main.Target, write function for injecting and executing DLL in target's memory 2 | -------------------------------------------------------------------------------- /img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgio/invade/060aa33ebfbc37386a91f963836ef4afa8510180/img/logo.jpg -------------------------------------------------------------------------------- /invade/__init__.py: -------------------------------------------------------------------------------- 1 | """invade __init__ module. 2 | 3 | Exports main, winapi, and version. 4 | """ 5 | 6 | from .main import * 7 | 8 | name = "invade" 9 | -------------------------------------------------------------------------------- /invade/main.py: -------------------------------------------------------------------------------- 1 | """invade main module. 2 | 3 | Classes: 4 | Me: Contains information about the operating environment. 5 | Scout: Contains information about active processes. 6 | Target: Contains information about the target process. 7 | Tool: Contains common and miscellaneous methods. 8 | 9 | Classes Me, Scout, and Target should only contain methods that will never 10 | conceivably be used outside of each class. The Tool class should contain 11 | miscellaneous methods that are freely and commonly used. As a result, the 12 | Tool class is lengthy, but easy to navigate because of the method naming 13 | convention. 14 | """ 15 | 16 | 17 | import sys 18 | import platform 19 | import struct 20 | import binascii 21 | import uuid 22 | import subprocess 23 | 24 | import pefile # https://github.com/erocarrera/pefile 25 | import capstone # https://github.com/aquynh/capstone 26 | import keystone # https://github.com/keystone-engine/keystone 27 | 28 | from .winapi import * 29 | from .version import * 30 | 31 | 32 | # Constants 33 | 34 | X86_MC_INSN_MAX = 15 # Max number of bytes for a single x86 instruction 35 | 36 | 37 | # Classes 38 | 39 | class Me(object): 40 | """Contains information about the operating environment. 41 | 42 | Attributes: 43 | is_windows (bool): True if OS is Windows. 44 | is_x64 (bool): True if this process is 64-bit. 45 | is_windows_x64 (bool): True if this process is 64-bit. 46 | is_windows_admin (bool): True if this process is running with admin 47 | privileges. 48 | is_debug_privilege_enabled (bool): True if this process has granted 49 | itself debug privileges successfully. 50 | 51 | """ 52 | 53 | def __init__(self): 54 | self.version = VERSION 55 | self.is_windows = Tool.is_windows() 56 | self.is_x64 = Tool.is_x64() 57 | self.is_windows_x64 = Tool.is_windows_x64() 58 | self.is_windows_admin = Tool.is_windows_admin() 59 | self.is_debug_privilege_enabled = False 60 | if Tool.set_debug_privilege(): 61 | self.is_debug_privilege_enabled = True 62 | else: 63 | self.is_debug_privilege_enabled = False 64 | 65 | 66 | class Scout(object): 67 | """Contains information about active processes. 68 | 69 | Typically, only one instance of Scout is used. However, you may want to 70 | use multiple Scout instances to compare process lists obtained at 71 | different times. 72 | 73 | Args: 74 | process_name (str): Name of the process, e.g. 'calc.exe'. 75 | case_sensitive (bool, optional): Whether the process name comparison 76 | should be case-sensitive. 77 | contains (bool, optional): Allows partial matching. If True, a partial 78 | process_name may return results. 79 | report_errors (bool): If True, errors will print. 80 | 81 | Attributes: 82 | process_name (str, optional): Name of the process, e.g. 'calc.exe'. 83 | report_errors (bool, optional): If True, errors will print. 84 | processes (list): A list of all running processes and their PIDs. 85 | pids (list): A list of all running process PIDs. 86 | 87 | """ 88 | 89 | def __init__(self, process_name=None, case_sensitive=False, contains=False, 90 | report_errors=True): 91 | self.process_name = process_name 92 | self.report_errors = report_errors 93 | self.processes = None 94 | self.processes = Tool.get_processes(True, report_errors) 95 | self.pids = None 96 | if process_name: 97 | self.pids = Tool.get_pids_by_process_name(process_name, 98 | self.processes, 99 | case_sensitive, 100 | contains) 101 | 102 | 103 | class Target(object): 104 | """Contains information about the target process. 105 | 106 | Target is typically used after finding the desired process with Scout. 107 | 108 | Args: 109 | pid (int): The PID of the process. Use Scout to obtain the PID. 110 | process_access_rights (int): Access rights for opening the process. 111 | process_name (str, optional): The name of the process, e.g. 'calc.exe'. 112 | report_errors (bool, optional): If True, errors are printed. 113 | 114 | Attributes: 115 | report_errors (bool): If True, errors are printed. 116 | pid (int): The PID of the process. 117 | process_access_rights (int): Access rights for opening the process. 118 | process_handle (int): The handle of the process. 119 | is_active (bool): If True, the process is running. 120 | is_x64 (bool): If True, the process is x64. 121 | base_address (int): The main executable's memory address. 122 | path (str): The file path of the process. 123 | version_info (list): File version information. 124 | entry_point_address (int): The relative address of the executable's 125 | entry point. 126 | process_name (str): The name of the process, e.g. 'calc.exe'. 127 | report_errors (bool): If True, errors are printed. 128 | 129 | """ 130 | 131 | def __enter__(self): 132 | pass 133 | 134 | def __exit__(self, exc_type, exc_val, exc_tb): 135 | try: 136 | if self.process_handle: 137 | Tool.close_handle(self.process_handle) 138 | except AttributeError: 139 | pass 140 | 141 | def __init__(self, pid, process_access_rights=PROCESS_ALL_ACCESS, 142 | process_name=None, report_errors=True): 143 | self._is_active = None 144 | self.report_errors = report_errors 145 | self.process_name = process_name 146 | self.pid = pid 147 | self.process_access_rights = process_access_rights 148 | self.process_handle = \ 149 | Tool.get_process_handle_by_pid(pid, process_access_rights) 150 | self.is_x64 = None 151 | self.base_address = None 152 | self.path = None 153 | self.version_info = None 154 | self.entry_point_address = None 155 | 156 | if self.process_handle != 0: 157 | self.is_x64 = Tool.is_process_x64(self.process_handle) 158 | self.path = Tool.get_process_path_by_handle(self.process_handle, 159 | False) 160 | if not self.path: 161 | self.path = Tool.get_process_path_by_pid(self.pid) 162 | if not self.process_name: 163 | if self.path: 164 | self.process_name = self.path[self.path.rfind('\\') + 1:] 165 | if self.process_name: 166 | self.base_address = Tool.get_module_address( 167 | self.process_handle, self.process_name) 168 | if self.base_address: 169 | if len(self.base_address) > 1 and self.report_errors: 170 | # Not common, but possible 171 | print( 172 | f'Warning: multiple instances of {process_name} ' 173 | f'found in process') 174 | # Use first base address 175 | self.base_address = self.base_address[0] 176 | if self.path: 177 | self.version_info = Tool.pe_get_file_version(self.path) 178 | self.entry_point_address = \ 179 | Tool.pe_get_entry_point_address(self.path) 180 | else: 181 | self.process_handle = False 182 | 183 | @property 184 | def is_active(self): 185 | return self._is_active 186 | 187 | @is_active.getter 188 | def is_active(self): 189 | if hasattr(self, 'process_handle'): 190 | return Tool.is_process_active(self.process_handle) 191 | 192 | 193 | class Tool(object): 194 | """Contains common and miscellaneous methods. 195 | 196 | """ 197 | 198 | @staticmethod 199 | def memory_allocate(process_handle, size, 200 | protection=PAGE_EXECUTE_READWRITE, address=None, 201 | top_down=False): 202 | """Allocate a region of memory in the target process. 203 | 204 | Note: 205 | The PAGE_EXECUTE_READWRITE argument is default because it's 206 | convenient for shellcode to read and write to/from the same 207 | allocated area, even if potentially volatile in practice. 208 | """ 209 | allocation_type = DWORD(MEM_COMMIT) 210 | if top_down: 211 | allocation_type = DWORD(MEM_COMMIT | MEM_TOP_DOWN) 212 | return VirtualAllocEx(process_handle, LPVOID(address), size, 213 | allocation_type, DWORD(protection)) 214 | 215 | @staticmethod 216 | def memory_free(process_handle, address, size=0, free_type=MEM_RELEASE): 217 | """Deallocate a region of memory at a specified address and size. 218 | 219 | Note: 220 | MEM_RELEASE is likely the most common free type to be used with 221 | this module. From MSDN: "If the dwFreeType parameter is 222 | MEM_RELEASE, dwSize must be 0 (zero)." 223 | """ 224 | VirtualFreeEx = kernel32.VirtualFreeEx 225 | if free_type == MEM_RELEASE: 226 | size = 0 227 | return VirtualFreeEx(process_handle, LPVOID(address), size, 228 | DWORD(free_type)) 229 | 230 | @staticmethod 231 | def memory_protect(process_handle, address, size, 232 | protection=PAGE_EXECUTE_READWRITE): 233 | """Change a memory region's protection. 234 | 235 | Args: 236 | process_handle (int): A handle to the process. 237 | address (int): The memory region start address. 238 | size (int): The size of the region. 239 | protection (int, optional): The type of protection (a Windows 240 | memory protection constant). Default value is 241 | PAGE_EXECUTE_READWRITE because this is the most suitable for 242 | injected executable code / memory manipulation. 243 | 244 | Returns: 245 | int: The previous memory protection (a Windows memory protection 246 | constant). 247 | """ 248 | protection_old = VirtualProtectEx(process_handle, LPVOID(address), 249 | size, protection, PDWORD(DWORD())) 250 | try: 251 | return protection_old.contents.value 252 | except AttributeError: 253 | pass 254 | 255 | @staticmethod 256 | def memory_read(process_handle, address, size, return_str=False): 257 | """Read data from a process's memory at a specified address and size. 258 | 259 | Args: 260 | process_handle (int): A handle to the process. 261 | address (int): The memory address to read at. 262 | size (int): Number of bytes to read. 263 | return_str (bool, optional): If True, the read bytes are returned 264 | as an uppercase string. Otherwise, they are returned as bytes. 265 | 266 | Returns: 267 | str: The bytes read from memory. 268 | """ 269 | if not size: 270 | return 271 | 272 | data = create_string_buffer(size) 273 | number_of_bytes_read = c_size_t() 274 | 275 | if ReadProcessMemory(process_handle, LPVOID(address), data, size, 276 | byref(number_of_bytes_read)): 277 | if return_str: 278 | return data.raw.hex().upper() 279 | else: 280 | return data.raw 281 | return 282 | 283 | @staticmethod 284 | def memory_read_pointers(process_handle, pointers, pointer_size, 285 | report_errors=True): 286 | """Read through a series of memory pointers in another process. 287 | 288 | Args: 289 | process_handle (int): The handle of the process. 290 | pointers (str): A string representation of pointers, 291 | comma separated, to be sequentially read. The syntax is 292 | similar to assembly pointers. Addition, subtraction, 293 | multiplication, and division are supported using these 294 | operators: + - * /. 295 | pointer_size (int): The size of a memory pointer in the process. 296 | Use 4 if the process architecture is x86. Use 8 if x64. 297 | report_errors (bool, optional): If True, errors are printed. 298 | 299 | Returns: 300 | int: Final read address/value on success. 301 | 302 | Examples: 303 | The following explains pointers argument further: 304 | 305 | '[0x10000000],[+0x100]' Means to take the value at 0x10000000, 306 | add 100 bytes, go to that address, then return that value at 307 | that address. 308 | 309 | '[0x10000000],+0x100' means to take the value at 0x10000000, 310 | add 100 bytes, and return the value at that address. 311 | """ 312 | if not pointer_size: 313 | if report_errors: 314 | print('Error: invalid pointer size') 315 | return 316 | 317 | pointers = pointers.replace(' ', '') 318 | pointers = pointers.split(',') 319 | # if len(pointers) < 2: 320 | # if report_errors: 321 | # print('Error: invalid pointer sequence') 322 | # return 323 | 324 | i = -1 325 | current_address = None 326 | for s in pointers: 327 | i += 1 # Iteration count, starting with pointer item 0 328 | if '[' in s: 329 | # It's a pointer 330 | for c in '[]': # Remove [] 331 | s = s.replace(c, '') 332 | operators = ['+', '-', '*', '/'] 333 | if any(o in s for o in operators): 334 | # It's an address pointer 335 | if current_address is None: 336 | # It's the first item 337 | if report_errors: 338 | print('Error: address sequence cannot begin with ' 339 | 'an address pointer') 340 | return 341 | 342 | operator = '+' # Assume addition by default 343 | if '-' in s: 344 | operator = '-' 345 | elif '*' in s: 346 | operator = '*' 347 | elif '/' in s: 348 | operator = '//' 349 | 350 | if '0x' in s: 351 | try: 352 | s = '0x' + str(format(int(s, 0), 'x')) 353 | except ValueError: 354 | if report_errors: 355 | print(f'Error: error parsing item {i}') 356 | return 357 | else: 358 | try: 359 | s = str(int(s, 0)) 360 | except ValueError: 361 | if report_errors: 362 | print(f'Error: error parsing item {i}') 363 | return 364 | 365 | # Avoid -- (addition) by removing minus symbol 366 | # Let operator do the work instead 367 | s = s.replace('-', '') 368 | 369 | try: 370 | current_address = eval( 371 | str(current_address) + operator + s) 372 | except NameError: 373 | if report_errors: 374 | print(f'Error: error parsing item {i}') 375 | return 376 | 377 | current_address = Tool.memory_read(process_handle, 378 | current_address, 379 | pointer_size, False) 380 | if not current_address: 381 | if report_errors: 382 | print(f'Error: memory read failure for item {i}') 383 | return 384 | 385 | current_address = int.from_bytes(current_address, 386 | byteorder='little', 387 | signed=False) 388 | 389 | else: 390 | # It's a regular pointer 391 | try: 392 | s = int(s, 0) 393 | except ValueError: 394 | if report_errors: 395 | print(f'Error: error parsing item {i}') 396 | return 397 | 398 | current_address = Tool.memory_read(process_handle, s, 399 | pointer_size, False) 400 | if not current_address: 401 | if report_errors: 402 | print(f'Error: memory read failure for item {i}') 403 | return 404 | 405 | current_address = int.from_bytes(current_address, 406 | byteorder='little', 407 | signed=False) 408 | 409 | else: 410 | # It's an address 411 | if current_address is None: 412 | # It's the first item 413 | try: 414 | current_address = int(s, 0) 415 | except ValueError: 416 | if report_errors: 417 | print(f'Error: error parsing item {i}') 418 | return 419 | 420 | else: 421 | # Assume addition by default 422 | operator = '+' 423 | if '-' in s: 424 | operator = '-' 425 | elif '*' in s: 426 | operator = '*' 427 | elif '/' in s: 428 | operator = '//' 429 | 430 | # Remove operators 431 | for c in '+-*/': 432 | s = s.replace(c, '') 433 | 434 | try: 435 | current_address = eval( 436 | str(current_address) + operator + s) 437 | except NameError: 438 | if report_errors: 439 | print(f'Error: error parsing item {i}') 440 | return 441 | 442 | return current_address 443 | 444 | @staticmethod 445 | def memory_write(process_handle, address, data, 446 | restore_memory_protection=True, report_errors=True): 447 | """Write data to a process's memory at a specified address. 448 | 449 | Args: 450 | process_handle (int): A handle to the process. 451 | address (int): The memory address to write to. 452 | data (str or bytes): Bytes to write, e.g. '9090' or b'\x90\x90'. 453 | restore_memory_protection (bool, optional): If True, the memory 454 | region's protection is restored after writing. Otherwise, the 455 | region's protection is set to PAGE_EXECUTE_READWRITE. 456 | report_errors (bool, optional): If True, errors are printed. 457 | 458 | Returns: 459 | int: The number of bytes written on success. 460 | """ 461 | if hasattr(data, 'decode'): 462 | data = create_string_buffer(data)[:-1] 463 | elif type(data) is str: 464 | # Remove whitespace characters 465 | data = ''.join(data.split()) 466 | try: 467 | data = create_string_buffer(bytes.fromhex(data))[:-1] 468 | except ValueError: 469 | if report_errors: 470 | print('Error: a non-hexadecimal character was used') 471 | return 472 | else: 473 | if report_errors: 474 | print('Error: lpBuffer must be bytes or str type') 475 | return 476 | size = len(data) 477 | if not size: 478 | return 479 | 480 | number_of_bytes_written = c_size_t() 481 | 482 | old_protection = Tool.memory_protect(process_handle, address, size) 483 | 484 | result = WriteProcessMemory(process_handle, LPVOID(address), data, 485 | size, byref(number_of_bytes_written)) 486 | 487 | if not result: 488 | return 489 | 490 | if restore_memory_protection: 491 | Tool.memory_protect(process_handle, address, size, old_protection) 492 | 493 | return number_of_bytes_written.value 494 | 495 | @staticmethod 496 | def search_file_pattern(path, pattern, start_address=0, chunk_size=0, 497 | chunk_limit=0, find_all=True): 498 | """ 499 | Yields addresses of matching byte patterns inside a file. 500 | 501 | Supports wildcard bytes, a starting address, reading in chunks, and 502 | read limits. 503 | 504 | Args: 505 | path (str): A file path. 506 | pattern (str): A sequence of bytes, e.g. 'FF ?? E8 26 23 D7'. 507 | '??' represents a single byte wildcard. Spaces between bytes 508 | are optional. 509 | start_address (int, optional): Search start address. 510 | chunk_size (int, optional): The read length per chunk. By default, 511 | chunks are not used and the entire file is read into memory. 512 | chunk_limit (int, optional): The maximum number of chunks to read. 513 | By default, all chunks are read. 514 | find_all (bool, optional): If True, all found instances are 515 | yielded. If False, only the first found instance is yielded 516 | and searching stops. 517 | 518 | Yields: 519 | int: Found address. 520 | 521 | Example: 522 | import invade 523 | found_addresses = [] 524 | for found_address in tool.search_file_pattern( 525 | r'C:\target.exe', 526 | '?? 01 55 ?? ?? 4B 20 1E 1D ?? 15', 527 | start_address=0x1000, 528 | chunk_size=1024, 529 | chunk_limit=10, 530 | find_all=False): 531 | found_addresses.append(found_address) 532 | for address in found_addresses: 533 | print(hex(address)) 534 | """ 535 | 536 | # Remove whitespace characters 537 | pattern = ''.join(pattern.split()) 538 | 539 | # If no path, invalid pattern, or pattern is all wildcards. 540 | if not path or len(pattern) < 2 or len( 541 | pattern) % 2 != 0 or pattern.count('?') == len(pattern): 542 | return 543 | 544 | # If chunk_size is 0, the whole file should be read. 545 | if chunk_size == 0: 546 | chunk_size = -1 547 | 548 | # Simplifies later chunk_size comparison. 549 | if chunk_limit == 0: 550 | chunk_limit = -1 551 | 552 | # Get largest segment bytes. 553 | pattern_largest_segment = list(filter(None, pattern.split('??'))) 554 | pattern_largest_segment.sort(key=len, reverse=True) 555 | pattern_largest_segment = pattern_largest_segment[0] 556 | pattern_largest_segment_position = pattern.index( 557 | pattern_largest_segment) // 2 558 | pattern_largest_segment = bytes.fromhex(pattern_largest_segment) 559 | 560 | # Search method 1 (no wildcards). 561 | if pattern.count('?') == 0: 562 | pattern_bytes = bytes.fromhex(pattern) 563 | chunk_position = 0 564 | with open(path, 'rb') as f: 565 | if start_address > 0: 566 | f.seek(start_address) 567 | while True: 568 | if chunk_limit > 0: 569 | if chunk_position / chunk_size >= chunk_limit: 570 | return 571 | try: 572 | data = f.read(chunk_size) 573 | except MemoryError: 574 | return 575 | if not data: 576 | return 577 | i = 0 578 | found_position = 0 579 | while True: 580 | try: 581 | found_position = data.index(pattern_bytes, 582 | found_position + i) 583 | if chunk_size > 0: 584 | yield chunk_position + found_position + \ 585 | start_address 586 | else: 587 | yield found_position + start_address 588 | if find_all is False: 589 | return 590 | except ValueError: 591 | break 592 | i += 1 593 | chunk_position += chunk_size 594 | continue 595 | 596 | # Create a list of wildcard positions. 597 | pattern_wildcard_positions = [] 598 | for i in range(0, len(pattern), 2): 599 | pattern_byte = pattern[i:i + 2] 600 | if pattern_byte == '??': 601 | pattern_wildcard_positions.append(i // 2) 602 | 603 | # Remove wildcards from pattern string and convert to bytes. 604 | pattern_len = len(pattern) // 2 605 | pattern_bytes = pattern.replace('?', '') 606 | pattern_bytes = bytes.fromhex(pattern_bytes) 607 | 608 | # Search method 2 (wildcards). 609 | possible_positions = [] 610 | end_of_file = False 611 | first_result = True 612 | chunk_position = 0 613 | 614 | with open(path, 'rb') as f: 615 | if start_address > 0: 616 | f.seek(start_address) 617 | while not end_of_file: 618 | if chunk_limit > 0: 619 | if chunk_position / chunk_size >= chunk_limit: 620 | return 621 | try: 622 | data = f.read(chunk_size) 623 | except MemoryError: 624 | return 625 | if not data: 626 | end_of_file = True 627 | chunk_search = True 628 | while chunk_search: 629 | try: 630 | if first_result is True: 631 | possible_positions.append( 632 | data.index(pattern_largest_segment)) 633 | first_result = False 634 | else: 635 | possible_positions.append( 636 | data.index(pattern_largest_segment, 637 | possible_positions[-1] + 1)) 638 | except ValueError: 639 | if chunk_size > 0: 640 | chunk_position += chunk_size 641 | chunk_search = False 642 | for possible_position in possible_positions: 643 | possible_position -= pattern_largest_segment_position 644 | match_count = 0 645 | pattern_bytes_pos = 0 646 | data_offset_pos = 0 647 | i = 0 648 | while i < pattern_len: 649 | if i in pattern_wildcard_positions: 650 | match_count += 1 651 | data_offset_pos += 1 652 | i += 1 653 | continue 654 | if possible_position < 0: 655 | possible_position = 0 656 | if pattern_bytes[pattern_bytes_pos] == data[ 657 | possible_position + data_offset_pos]: 658 | match_count += 1 659 | data_offset_pos += 1 660 | pattern_bytes_pos += 1 661 | i += 1 662 | continue 663 | i += 1 664 | if match_count == pattern_len: 665 | if find_all is True: 666 | if chunk_size > 0: 667 | yield chunk_position + possible_position + \ 668 | start_address - chunk_size 669 | else: 670 | yield possible_position + start_address 671 | else: 672 | yield possible_position + chunk_position + \ 673 | start_address - chunk_size 674 | return 675 | possible_positions = [] 676 | first_result = True 677 | return 678 | 679 | @staticmethod 680 | def run_app(path, wait=False): 681 | """Run an application. 682 | 683 | If wait is False, execution will resume when the program or file is 684 | closed. 685 | """ 686 | if not wait: 687 | subprocess.Popen(path) # Default, no wait 688 | return True 689 | subprocess.run(path) # Waits until completion 690 | return True 691 | 692 | @staticmethod 693 | def create_random_str_hex(length=32, lowercase=True): 694 | """Create random hex string. Default length is 32 characters. 695 | 696 | Example output: '409b9a6713b9469aac473755c8cc064a' 697 | """ 698 | str_hex = '' 699 | i = 0 700 | while i < length: 701 | str_hex += str(uuid.uuid4()).replace('-', '') 702 | i += len(str_hex) 703 | if lowercase is False: 704 | str_hex = str_hex.upper() 705 | return str_hex[:length] 706 | 707 | @staticmethod 708 | def pe_get_entry_point_address(path): 709 | """Get an executable's entry point address via PE header.""" 710 | pe = pefile.PE(path, fast_load=True) 711 | try: 712 | return pe.OPTIONAL_HEADER.AddressOfEntryPoint 713 | except (AttributeError, KeyError): 714 | return 715 | 716 | @staticmethod 717 | def pe_get_rva_diff(path): 718 | """Get the RVA difference between file in memory vs. file on disk. 719 | 720 | 0xc00 (3072) is a common return value. 721 | """ 722 | pe = pefile.PE(path, fast_load=True) 723 | try: 724 | return pe.OPTIONAL_HEADER.BaseOfCode - \ 725 | pe.OPTIONAL_HEADER.SizeOfHeaders 726 | except (AttributeError, KeyError): 727 | return 728 | 729 | @staticmethod 730 | def pe_get_file_version(path): 731 | """Get an executable's version information via PE header.""" 732 | version_info = { 733 | 'file_version_1': None, 734 | 'product_version_1': None, 735 | 'file_version_2': None, 736 | 'product_version_2': None, 737 | 'major_image_version': None, 738 | 'minor_image_version': None, 739 | } 740 | 741 | f = pefile.PE(path, fast_load=True) 742 | f.parse_data_directories( 743 | directories=[ 744 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']]) 745 | 746 | try: 747 | for a in f.FileInfo: 748 | for b in a: 749 | if b.name == 'StringFileInfo': 750 | for c in b.StringTable: 751 | version_info['file_version_1'] = \ 752 | c.entries[b'FileVersion'].decode() 753 | version_info['product_version_1'] = \ 754 | c.entries[b'ProductVersion'].decode() 755 | break 756 | except (AttributeError, KeyError): 757 | pass 758 | 759 | try: 760 | file_version_ms = f.VS_FIXEDFILEINFO.FileVersionMS 761 | file_version_ls = f.VS_FIXEDFILEINFO.FileVersionLS 762 | version_info['file_version_2'] = \ 763 | f'{file_version_ms >> 16}' \ 764 | f'.{file_version_ms & 0xFFFF}' \ 765 | f'.{file_version_ls >> 16}' \ 766 | f'.{file_version_ls & 0xFFFF}' 767 | product_version_ms = f.VS_FIXEDFILEINFO.ProductVersionMS 768 | product_version_ls = f.VS_FIXEDFILEINFO.ProductVersionLS 769 | version_info['product_version_2'] = \ 770 | f'{product_version_ms >> 16}' \ 771 | f'.{product_version_ms & 0xFFFF}' \ 772 | f'.{product_version_ls >> 16}' \ 773 | f'.{product_version_ls & 0xFFFF}' 774 | except (AttributeError, KeyError): 775 | pass 776 | 777 | try: 778 | version_info['major_image_version'] = \ 779 | f.OPTIONAL_HEADER.MajorImageVersion 780 | version_info['minor_image_version'] = \ 781 | f.OPTIONAL_HEADER.MinorImageVersion 782 | except (AttributeError, KeyError): 783 | pass 784 | 785 | return version_info 786 | 787 | @staticmethod 788 | def pe_get_export_symbol_address(symbol, path, include_base=False, 789 | case_sensitive=False): 790 | """Get a symbol's address inside a PE file. 791 | 792 | Commonly used to find the address of a function inside a DLL file. 793 | 794 | Args: 795 | symbol (str): The name of the symbol, i.e. 'MessageBoxW'. 796 | path (str): Path to PE file. 797 | include_base (bool, optional): If True, the executable's base 798 | address is is added to the symbol's address. 799 | case_sensitive (bool, optional): Whether the process name 800 | comparison should be case-sensitive. 801 | 802 | Returns: 803 | list: A list of symbol addresses. 804 | """ 805 | pe = pefile.PE(path, fast_load=True) 806 | pe.parse_data_directories(directories=[ 807 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT']]) 808 | symbol_addresses = [] 809 | for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols: 810 | try: 811 | if exp.name.decode().lower() == symbol.lower(): 812 | if case_sensitive: 813 | if exp.name.decode() == symbol: 814 | if include_base: 815 | symbol_addresses.append( 816 | exp.address + pe.OPTIONAL_HEADER.ImageBase) 817 | else: 818 | symbol_addresses.append(exp.address) 819 | else: 820 | if include_base: 821 | symbol_addresses.append( 822 | exp.address + pe.OPTIONAL_HEADER.ImageBase) 823 | else: 824 | symbol_addresses.append(exp.address) 825 | except AttributeError: 826 | pass 827 | if symbol_addresses: 828 | return symbol_addresses 829 | 830 | @staticmethod 831 | def pe_get_code_cave_end_of_section(path, section_name='.text'): 832 | """Get the end-of-section code cave info for a specific PE section. 833 | 834 | A code cave is a portion of memory that can be used for executing 835 | custom (injected) instructions. The end of a PE section often contains 836 | a block of zeroed memory. This area of memory can be easier to use 837 | because there is often no need to allocate new memory or change 838 | memory region protection. The information returned from this function 839 | can be used to find a home for injected code. A list is returned 840 | because it is possible that multiple sections with the same name are 841 | present. 842 | 843 | Note: 844 | Packed files with altered section information may be incompatible. 845 | 846 | Args: 847 | path (str): Windows path to PE file. 848 | section_name (str, optional): Name of section, defaults to '.text'. 849 | 850 | Returns: 851 | list: Contains code cave information, i.e. 852 | [{'start': int, 'end': int, 'size': int}] 853 | """ 854 | pe = pefile.PE(path, fast_load=True) 855 | section_code_cave = [] 856 | for section in pe.sections: 857 | try: 858 | if section.Name.decode().strip('\0') == section_name: 859 | section_alignment = pe.OPTIONAL_HEADER.SectionAlignment 860 | section_virtual_size = section.Misc_VirtualSize 861 | section_virtual_address = section.VirtualAddress 862 | 863 | section_code_cave_start = \ 864 | section_virtual_size + section_virtual_address 865 | section_code_cave_end = int(section_alignment * round( 866 | float(section_code_cave_start) / section_alignment) + 867 | section_alignment - 1) 868 | section_code_cave_size = \ 869 | section_code_cave_end - section_code_cave_start 870 | 871 | section_code_cave.append({'start': section_code_cave_start, 872 | 'end': section_code_cave_end, 873 | 'size': section_code_cave_size}) 874 | except AttributeError: 875 | pass 876 | if section_code_cave: 877 | return section_code_cave 878 | 879 | @staticmethod 880 | def get_asm(is_x64, mc, address=0): 881 | """Get assembly from Capstone for a string of machine code bytes. 882 | 883 | Args: 884 | is_x64 (bool): Generate assembly in 64-bit or 32-bit mode. 885 | mc (str): A string of machine code such as '55 8B EC 56'. 886 | address (int, optional): The address of where the code would be. 887 | This typically would be a memory address to ensure correct 888 | instruction calculation. 889 | 890 | Returns: 891 | list: A list of dicts containing disassembly information. 892 | """ 893 | try: 894 | mc = bytes(bytearray.fromhex(mc)) 895 | except ValueError: 896 | return 897 | if type(is_x64) is not bool: 898 | return 899 | mode = capstone.CS_MODE_64 900 | if not is_x64: 901 | mode = capstone.CS_MODE_32 902 | cs = capstone.Cs(capstone.CS_ARCH_X86, mode) 903 | asm = [] 904 | try: 905 | for (address, size, mnemonic, op_str) in \ 906 | cs.disasm_lite(mc, address): 907 | asm.append({ 908 | 'addr': address, 909 | 'size': size, 910 | 'asm': f'{mnemonic} {op_str}' 911 | }) 912 | return asm 913 | except capstone.CsError: 914 | return 915 | 916 | @staticmethod 917 | def get_mc(is_x64, asm, address=0): 918 | """Get machine code bytes from Keystone for a string of assembly 919 | instructions. 920 | 921 | Args: 922 | is_x64 (bool): Generate machine code in 64-bit or 32-bit mode. 923 | asm (str): A string of assembly. Use ; or \n to delimit commands, 924 | e.g. 'mov eax, ebx;jmp 0xd46694'. 925 | address (int, optional): The address of where the code would be. 926 | This typically would be a memory address to ensure correct 927 | instruction calculation. 928 | 929 | Returns: 930 | str: A string of machine code bytes on success. 931 | """ 932 | if type(asm) is not str: 933 | return 934 | if type(is_x64) is not bool: 935 | return 936 | mode = keystone.KS_MODE_64 937 | if not is_x64: 938 | mode = keystone.KS_MODE_32 939 | try: 940 | ks = keystone.Ks(keystone.KS_ARCH_X86, mode) 941 | asm_bytes = asm.encode() 942 | mc, mc_size = ks.asm(asm_bytes, address) 943 | result = Tool.convert_list_int_to_str_hex(mc) 944 | if result: 945 | return result 946 | except keystone.KsError: 947 | return 948 | 949 | @staticmethod 950 | def get_mc_size(s): 951 | """Get the number of bytes in a hex string.""" 952 | s = ''.join(s.split()) 953 | if Tool.is_str_hexadecimal(s): 954 | return int(len(s) / 2) 955 | return len(s) 956 | 957 | @staticmethod 958 | def get_processes(file_name_only=True, report_errors=True): 959 | """Get a list of all running processes and their PIDs. 960 | 961 | Args: 962 | file_name_only (bool, optional): return only file names, no paths. 963 | report_errors (bool, optional): if True, errors are printed. 964 | 965 | Returns: 966 | list: A list containing containing process names and PIDs on 967 | success. 968 | """ 969 | try: 970 | EnumProcesses = psapi.EnumProcesses 971 | except AttributeError: 972 | EnumProcesses = kernel32.EnumProcesses 973 | 974 | process_buffer_size = 8192 975 | process_buffer_size_current = process_buffer_size 976 | process_buffer_size_max = 10 * 1024 * 1024 977 | 978 | while True: 979 | pProcessIds = (DWORD * process_buffer_size)() 980 | pBytesReturned = DWORD() 981 | if EnumProcesses(byref(pProcessIds), ctypes.sizeof(pProcessIds), 982 | byref(pBytesReturned)): 983 | if pBytesReturned.value == process_buffer_size_current: 984 | process_buffer_size_current += process_buffer_size 985 | elif process_buffer_size_current >= process_buffer_size_max: 986 | if report_errors: 987 | print( 988 | 'Error: number of processes exceeds buffer limit') 989 | return 990 | else: 991 | break 992 | else: 993 | if report_errors: 994 | print('Error: EnumProcesses') 995 | return 996 | 997 | QueryFullProcessImageName = kernel32.QueryFullProcessImageNameW 998 | processes = [] 999 | 1000 | for i in range(pBytesReturned.value // sizeof(DWORD)): 1001 | dwProcessId = pProcessIds[i] 1002 | hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, 1003 | dwProcessId) 1004 | if hProcess: 1005 | lpExeName = (c_wchar * MAX_PATH)() 1006 | lpdwSize = DWORD(MAX_PATH) 1007 | if QueryFullProcessImageName(hProcess, 0, byref(lpExeName), 1008 | byref(lpdwSize)): 1009 | lpExeName = lpExeName.value 1010 | if file_name_only: 1011 | lpExeName = lpExeName[lpExeName.rfind('\\') + 1:] 1012 | processes.append([lpExeName, dwProcessId]) 1013 | CloseHandle(hProcess) 1014 | return processes 1015 | 1016 | @staticmethod 1017 | def get_process_handle_by_pid(pid, process_access_rights): 1018 | """Get process's handle via the process's PID.""" 1019 | return OpenProcess(process_access_rights, False, pid) 1020 | 1021 | @staticmethod 1022 | def get_process_path_by_handle(process_handle, report_errors=True): 1023 | """Get processes's path via the process's handle.""" 1024 | try: 1025 | GetModuleFileNameEx = psapi.GetModuleFileNameExW 1026 | except AttributeError: 1027 | GetModuleFileNameEx = kernel32.GetModuleFileNameExW 1028 | 1029 | lpFilename = (c_wchar * MAX_PATH)() 1030 | nSize = sizeof(lpFilename) 1031 | 1032 | if GetModuleFileNameEx(process_handle, None, lpFilename, nSize): 1033 | return lpFilename.value 1034 | else: 1035 | if report_errors: 1036 | print('Error: cannot determine file path') 1037 | return 1038 | 1039 | @staticmethod 1040 | def get_process_path_by_pid(pid, report_errors=True): 1041 | """Get process's path via the process's PID.""" 1042 | QueryFullProcessImageName = kernel32.QueryFullProcessImageNameW 1043 | hProcess = Tool.get_process_handle_by_pid(pid, 1044 | PROCESS_QUERY_INFORMATION) 1045 | dwFlags = DWORD() 1046 | lpExeName = (c_wchar * MAX_PATH)() 1047 | lpdwSize = DWORD(sizeof(lpExeName)) 1048 | if QueryFullProcessImageName(hProcess, dwFlags, lpExeName, 1049 | byref(lpdwSize)): 1050 | CloseHandle(hProcess) 1051 | return lpExeName.value 1052 | CloseHandle(hProcess) 1053 | if report_errors: 1054 | print('Error: cannot determine file path') 1055 | return 1056 | 1057 | @staticmethod 1058 | def get_pids_by_process_name(process_name, process_list, 1059 | case_sensitive=False, contains=False): 1060 | """Get a list of PIDs for the given process name. 1061 | 1062 | Args: 1063 | process_name (str): The name of process, i.e. 'calc.exe'. 1064 | process_list (list): A list of processes obtained from 1065 | get_processes(). 1066 | case_sensitive (bool, optional): Whether the process name 1067 | comparison should be case-sensitive. 1068 | contains (bool, optional): Allows partial matching. If True, a 1069 | partial process_name may return results. 1070 | 1071 | Returns: 1072 | list: A list of int PIDs on success. 1073 | """ 1074 | pids = [] 1075 | for i in range(len(process_list)): 1076 | if case_sensitive: 1077 | if contains: 1078 | if process_name in process_list[i][0]: 1079 | pids.append(process_list[i][1]) 1080 | else: 1081 | if process_list[i][0] == process_name: 1082 | pids.append(process_list[i][1]) 1083 | else: 1084 | if contains: 1085 | if process_name.lower() in process_list[i][0].lower(): 1086 | pids.append(process_list[i][1]) 1087 | else: 1088 | if process_list[i][0].lower() == process_name.lower(): 1089 | pids.append(process_list[i][1]) 1090 | if pids: 1091 | return pids 1092 | else: 1093 | return 1094 | 1095 | @staticmethod 1096 | def get_modules(process_handle, file_name_only=True, 1097 | report_errors=True): 1098 | """Get a list of module information for a given process (each module's 1099 | base address, size, etc.). 1100 | 1101 | Note: 1102 | EnumProcessModulesEx will fail when called on a 64-bit target 1103 | from 32-bit. 1104 | 1105 | Args: 1106 | process_handle (int): A process handle. 1107 | file_name_only (bool, optional): If True, list will contain only 1108 | file names; no paths. 1109 | report_errors (bool, optional): If True, errors are printed. 1110 | 1111 | Returns: 1112 | list: A list of strings on success. 1113 | 1114 | """ 1115 | try: 1116 | EnumProcessModulesEx = psapi.EnumProcessModulesEx 1117 | except AttributeError: 1118 | EnumProcessModulesEx = kernel32.EnumProcessModulesEx 1119 | 1120 | module_buffer_size = 1024 1121 | lpcbNeeded = DWORD(module_buffer_size) 1122 | data_type_size = sizeof(HMODULE) 1123 | 1124 | while True: 1125 | lphModule = (HMODULE * (module_buffer_size // data_type_size))() 1126 | if EnumProcessModulesEx(process_handle, byref(lphModule), 1127 | lpcbNeeded, 1128 | byref(lpcbNeeded), LIST_MODULES_ALL): 1129 | if lpcbNeeded.value <= module_buffer_size: 1130 | break 1131 | module_buffer_size = lpcbNeeded.value 1132 | else: 1133 | if report_errors: 1134 | print('Error: EnumProcessModulesEx') 1135 | return 1136 | 1137 | try: 1138 | GetModuleInformation = psapi.GetModuleInformation 1139 | except AttributeError: 1140 | GetModuleInformation = kernel32.GetModuleInformation 1141 | 1142 | module_list = [] 1143 | 1144 | for hModule in lphModule: 1145 | # GetMappedFileName is better than GetModuleFileNameEx in 1146 | # this instance. 1147 | try: 1148 | GetMappedFileName = psapi.GetMappedFileNameW 1149 | except AttributeError: 1150 | GetMappedFileName = kernel32.GetMappedFileNameW 1151 | 1152 | lpFilename = (c_wchar * MAX_PATH)() 1153 | nSize = sizeof(lpFilename) 1154 | 1155 | if GetMappedFileName(process_handle, LPVOID(hModule), lpFilename, 1156 | nSize): 1157 | 1158 | lpmodinfo = MODULEINFO() 1159 | if GetModuleInformation(process_handle, LPVOID(hModule), 1160 | byref(lpmodinfo), sizeof(lpmodinfo)): 1161 | 1162 | if file_name_only: 1163 | lpFilename = lpFilename.value[ 1164 | lpFilename.value.rfind('\\') + 1:] 1165 | 1166 | module_list.append( 1167 | [lpFilename, lpmodinfo.lpBaseOfDll, 1168 | lpmodinfo.SizeOfImage, 1169 | lpmodinfo.EntryPoint]) 1170 | 1171 | else: 1172 | if report_errors: 1173 | print('Error: GetModuleInformation') 1174 | 1175 | else: 1176 | if report_errors: 1177 | pass 1178 | # This will likely occur multiple times if not admin. 1179 | # print('Error: GetMappedFileName') 1180 | 1181 | if module_list: 1182 | return module_list 1183 | 1184 | @staticmethod 1185 | def get_module_address(process_handle, module_name, case_sensitive=False): 1186 | """Get a list of base addresses for the given module name. 1187 | 1188 | Note: 1189 | Multiple base addresses will be returned when modules with the 1190 | same name are present. 1191 | """ 1192 | results = [] 1193 | process_modules = Tool.get_modules(process_handle) 1194 | if process_modules: 1195 | for a in process_modules: 1196 | if case_sensitive: 1197 | if a[0] == module_name: 1198 | results.append(a[1]) 1199 | else: 1200 | if a[0].lower() == module_name.lower(): 1201 | results.append(a[1]) 1202 | if results: 1203 | return results 1204 | 1205 | @staticmethod 1206 | def get_module_path(process_handle, module_name, dos_paths=False, 1207 | case_sensitive=False): 1208 | """Get a list of paths for a given module name. 1209 | 1210 | Using a supplied process handle and the name of a module (usually a 1211 | .dll file), the Windows path to the file us returned. A list is 1212 | always returned because it is always possible that multiple instances 1213 | of a module are present in memory. 1214 | 1215 | Args: 1216 | process_handle (int): A process handle. 1217 | module_name (str): The module's file name, e.g. 'kernel32.dll'. 1218 | dos_paths (bool, optional): If False, regular Windows paths are 1219 | returned. If True, MS-DOS device names are returned. 1220 | case_sensitive (bool, optional): Whether the module name 1221 | comparison should be case-sensitive. 1222 | 1223 | Returns: 1224 | list: A list of module names on success. 1225 | """ 1226 | module_paths_dos = [] 1227 | process_modules = Tool.get_modules(process_handle, False) 1228 | if process_modules: 1229 | for m in process_modules: 1230 | if case_sensitive: 1231 | if m[0].value[m[0].value.rfind('\\') + 1:] == module_name: 1232 | module_paths_dos.append(m[0].value) 1233 | else: 1234 | if m[0].value[m[0].value.rfind( 1235 | '\\') + 1:].lower() == module_name.lower(): 1236 | module_paths_dos.append(m[0].value) 1237 | 1238 | if dos_paths: 1239 | if module_paths_dos: 1240 | return module_paths_dos 1241 | return 1242 | 1243 | module_paths = [] 1244 | for m in module_paths_dos: 1245 | module_path = Tool.convert_dos_path_to_drive_path(m) 1246 | if module_path: 1247 | module_paths.append(module_path) 1248 | else: 1249 | return 1250 | if module_paths: 1251 | return module_paths 1252 | return 1253 | 1254 | @staticmethod 1255 | def get_drive_paths(report_errors=True): 1256 | """Get a list of drive paths in multiple formats. 1257 | 1258 | A list is returned containing each drive's assigned letter, MS-DOS 1259 | device name and volume GUID. This function is Primarily used by 1260 | Tool.convert_dos_path_to_drive_path() and can also be used for 1261 | determining the number of connected drives. 1262 | 1263 | Args: 1264 | report_errors (bool, optional): If True, errors will print. 1265 | 1266 | Returns: 1267 | list: Contains drive information. 1268 | """ 1269 | volume_guid_paths = [] 1270 | 1271 | FindFirstVolume = kernel32.FindFirstVolumeW 1272 | FindNextVolume = kernel32.FindNextVolumeW 1273 | FindVolumeClose = kernel32.FindVolumeClose 1274 | 1275 | FindFirstVolume.restype = HANDLE 1276 | lpszVolumeName = (c_wchar * MAX_PATH)() 1277 | cchBufferLength = DWORD(sizeof(lpszVolumeName)) 1278 | hFindVolume = FindFirstVolume(lpszVolumeName, cchBufferLength) 1279 | if hFindVolume == INVALID_HANDLE_VALUE: 1280 | if report_errors: 1281 | print('Error: FindFirstVolume returned an invalid handle') 1282 | return 1283 | volume_guid_paths.append(lpszVolumeName.value) 1284 | 1285 | # FindNextVolume only seems to work when HANDE restype is applied and 1286 | # its return value is cast to HANDLE. 1287 | hFindVolume = ctypes.cast(hFindVolume, HANDLE) 1288 | while FindNextVolume(hFindVolume, lpszVolumeName, cchBufferLength): 1289 | if lpszVolumeName.value[:4] != '\\\\?\\' or lpszVolumeName.value[ 1290 | -1:] != '\\': 1291 | continue 1292 | volume_guid_paths.append(lpszVolumeName.value) 1293 | 1294 | if not len(volume_guid_paths): 1295 | if report_errors: 1296 | print('Error: no volumes present') 1297 | return 1298 | 1299 | FindVolumeClose(hFindVolume) 1300 | 1301 | volume_path_names = [] 1302 | GetVolumePathNamesForVolumeName = \ 1303 | kernel32.GetVolumePathNamesForVolumeNameW 1304 | lpszVolumePathNames = ( 1305 | c_wchar * MAX_PATH)() 1306 | cchBufferLength = DWORD(sizeof(lpszVolumePathNames)) 1307 | lpcchReturnLength = DWORD() 1308 | 1309 | for volume_guid_path in volume_guid_paths: 1310 | if GetVolumePathNamesForVolumeName(volume_guid_path, 1311 | lpszVolumePathNames, 1312 | cchBufferLength, 1313 | lpcchReturnLength): 1314 | volume_path_names.append(lpszVolumePathNames.value) 1315 | 1316 | if not len(volume_path_names): 1317 | if report_errors: 1318 | print('Error: no volumes present') 1319 | return 1320 | 1321 | QueryDosDevice = kernel32.QueryDosDeviceW 1322 | lpTargetPath = (c_wchar * MAX_PATH)() 1323 | ucchMax = sizeof(lpTargetPath) 1324 | dos_names = [] 1325 | for volume_guid_path in volume_guid_paths: 1326 | if QueryDosDevice(volume_guid_path[4:-1], lpTargetPath, ucchMax): 1327 | dos_names.append(lpTargetPath.value) 1328 | 1329 | if len(volume_guid_paths) != len(volume_path_names) != len(dos_names): 1330 | if report_errors: 1331 | print('Error: device inconsistency') 1332 | return 1333 | 1334 | return [[volume_path_names[i], dos_names[i], volume_guid_paths[i]] for 1335 | i in range(len(volume_guid_paths))] 1336 | 1337 | @staticmethod 1338 | def set_debug_privilege(pid=None): 1339 | """Set the debug privilege to allow greater access to other processes. 1340 | 1341 | This function is adapted from Cuckoo Sandbox: 1342 | https://cuckoosandbox.org. Licensed under the GNU General Public 1343 | License v3 (GPL-3). Changes: made function independent and compatible. 1344 | """ 1345 | if pid is None: 1346 | h_process = kernel32.GetCurrentProcess() 1347 | else: 1348 | h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid) 1349 | 1350 | if not h_process: 1351 | return 1352 | 1353 | h_current_token = HANDLE() 1354 | if not advapi32.OpenProcessToken(h_process, 1355 | TOKEN_ALL_ACCESS, 1356 | pointer(h_current_token)): 1357 | return 1358 | 1359 | se_original_luid = LUID() 1360 | if not advapi32.LookupPrivilegeValueW(None, 'SeDebugPrivilege', 1361 | pointer(se_original_luid)): 1362 | return 1363 | 1364 | luid_attributes = LUID_AND_ATTRIBUTES() 1365 | luid_attributes.Luid = se_original_luid 1366 | luid_attributes.Attributes = SE_PRIVILEGE_ENABLED 1367 | token_privs = TOKEN_PRIVILEGES() 1368 | token_privs.PrivilegeCount = 1 1369 | token_privs.Privileges = luid_attributes 1370 | 1371 | if not advapi32.AdjustTokenPrivileges(h_current_token, False, 1372 | pointer(token_privs), 1373 | 0, None, None): 1374 | return 1375 | 1376 | CloseHandle(h_current_token) 1377 | CloseHandle(h_process) 1378 | return True 1379 | 1380 | @staticmethod 1381 | def close_handle(handle): 1382 | return CloseHandle(handle) 1383 | 1384 | @staticmethod 1385 | def is_windows(): 1386 | return sys.platform == 'win32' 1387 | 1388 | @staticmethod 1389 | def is_windows_x64(): 1390 | return platform.machine().endswith('64') 1391 | 1392 | @staticmethod 1393 | def is_windows_admin(): 1394 | try: 1395 | return bool(shell32.IsUserAnAdmin()) 1396 | except AttributeError: 1397 | return None # In case of deprecation 1398 | 1399 | @staticmethod 1400 | def is_x64(): 1401 | return True if sys.maxsize > 2 ** 32 else False 1402 | 1403 | @staticmethod 1404 | def is_process_x64(process_handle): 1405 | """Check if target process is x64 or x86. For insight, see MSDN's 1406 | description of IsWow64Process. 1407 | """ 1408 | try: 1409 | IsWow64Process = kernel32.IsWow64Process 1410 | except AttributeError: 1411 | return 1412 | 1413 | _is_wow64 = BOOL(False) 1414 | _is_windows_x64 = Tool.is_windows_x64() 1415 | 1416 | if IsWow64Process(process_handle, byref(_is_wow64)): 1417 | if _is_windows_x64 is False: 1418 | return False 1419 | elif _is_wow64.value: 1420 | return False 1421 | return True 1422 | return 1423 | 1424 | @staticmethod 1425 | def is_process_active(process_handle): 1426 | """Check if process with the given process handle is active.""" 1427 | GetExitCodeProcess = kernel32.GetExitCodeProcess 1428 | lpExitCode = DWORD() 1429 | if GetExitCodeProcess(process_handle, pointer(lpExitCode)): 1430 | if lpExitCode.value != STILL_ACTIVE: 1431 | return False 1432 | return True 1433 | 1434 | @staticmethod 1435 | def is_service_active(service_name): 1436 | """Check if a service is active.""" 1437 | try: 1438 | result = subprocess.run( 1439 | f'sc query "{service_name}" | find "RUNNING"', 1440 | shell=True, check=True, stdout=subprocess.PIPE) 1441 | if not result: 1442 | return False 1443 | if 'RUNNING' in str(result.stdout): 1444 | return True 1445 | return False 1446 | except (subprocess.CalledProcessError, AttributeError): 1447 | return False 1448 | 1449 | @staticmethod 1450 | def is_valid_address_x86(i): 1451 | """Check if int is within a valid Windows x86 memory range.""" 1452 | return 0x10000 <= i <= 0x7FFFFFFF 1453 | 1454 | @staticmethod 1455 | def is_valid_address_x64(i): 1456 | """Check if int is within a valid Windows x64 memory range.""" 1457 | return 0x10000 <= i < 0x7FFFFFFFFFFF 1458 | 1459 | @staticmethod 1460 | def is_str_hexadecimal(s): 1461 | """Check if a string is a hexadecimal byte string. 1462 | 1463 | For example, '909090' returns True. 1464 | 1465 | Note: 1466 | By design, hexadecimal strings with a length of 1 will return 1467 | False. isalnum() will check for operators like + and - that would 1468 | normally successfully validate. 1469 | """ 1470 | if s.isalnum(): 1471 | if len(s) % 2 == 0: 1472 | try: 1473 | return isinstance(int(s, 16), int) 1474 | except ValueError: 1475 | pass 1476 | return False 1477 | 1478 | @staticmethod 1479 | def convert_dos_path_to_drive_path(dos_path, report_errors=True): 1480 | drive_paths = Tool.get_drive_paths(report_errors) 1481 | for drive_path in drive_paths: 1482 | try: 1483 | if dos_path.index(drive_path[1]) == 0: 1484 | return dos_path.replace(drive_path[1], 1485 | drive_path[0]).replace('\\\\', 1486 | '\\') 1487 | except ValueError: 1488 | continue 1489 | return 1490 | 1491 | @staticmethod 1492 | def convert_list_int_to_str_hex(l): 1493 | """Converts a list of int values into a string of hex values. 1494 | 1495 | Useful for converting the return value of Keystone's asm() method into 1496 | a str that can be used when writing memory. 1497 | """ 1498 | return ''.join('%02X' % i for i in l) 1499 | 1500 | @staticmethod 1501 | def convert_int_pointer_to_str_hex(i): 1502 | """Converts a memory pointer integer to hex pointer string. 1503 | 1504 | For example, if i = 73588229205, '5544332211' is returned. The int is 1505 | converted to hex string (with '0x' omitted) and then reversed. This is 1506 | useful for obtaining hex pointers which are represented in memory in 1507 | little endian format. Note that if the hex string has an odd length 1508 | (e.g. '22b'), a '0' is padded to the last byte string so that '0b22' is 1509 | returned. 1510 | """ 1511 | str_hex = f'{i:02x}' 1512 | if len(str_hex) % 2: 1513 | str_hex = str_hex[:-1] + '0' + str_hex[-1:] 1514 | str_hex = bytearray.fromhex(str_hex) 1515 | str_hex.reverse() 1516 | return str_hex.hex() 1517 | 1518 | @staticmethod 1519 | def convert_bytes_pointer_to_int(b): 1520 | """Converts a memory pointer into an integer. 1521 | 1522 | For example, a pointer to 0x500432 in memory would look like this in 1523 | Windows x64: 32 04 50 00 00 00 00 00 1524 | This function will convert that data, obtained from 1525 | ReadProcessMemory(), i.e.: b'\x32\x04\x50\x00\x00\x00\x00\x00' to: 1526 | 5243954 (an int) which == 0x500432. This 0x500432 int can then be used 1527 | as needed. 1528 | 1529 | Args: 1530 | b (bytes): A bytes object, e.g. b'\x32\x04\x50\x00\x00\x00\x00\x00' 1531 | Returns: 1532 | int: The memory address pointer represented by b. 1533 | """ 1534 | new_address = '' 1535 | for byte in b: 1536 | new_address += hex(byte)[2:].zfill(2) 1537 | return int(''.join(reversed( 1538 | [new_address[i:i + 2] for i in range(0, len(new_address), 2)])), 1539 | 16) 1540 | 1541 | @staticmethod 1542 | def convert_float_to_str_hex(f): 1543 | """Convert float to hex string. 1544 | 1545 | For example, 1.0 returns '0x3f800000' 1546 | """ 1547 | return hex(struct.unpack('