diff --git a/.gitignore b/.gitignore index 9fb5bc4..9f86440 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ __pycache__/ # Distribution / packaging .Python env/ -build/ +Test/build/ develop-eggs/ dist/ downloads/ @@ -100,5 +100,5 @@ ENV/ # mypy .mypy_cache/ -#PyCharm -.idea/ \ No newline at end of file +# PyCharm +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..335e28b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Victor Santiago + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 91253ce..5fbaf27 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ReadWriteMemory ### Description -This ReadWriteMemory Module is made on Python for reading and writing to the memory of any process. This module does not required any extra modules only uses standard Python lib’s like ctypes. +The ReadWriteMemory Class is made on Python for reading and writing to the memory of any process. This Class does not depend any extra modules only uses standard Python lib’s like ctypes. --- @@ -23,26 +23,116 @@ OS: Windows 7, 8 and 10
--- -### Usage - -### Import Class ReadWriteMemory() +## Usage +### Import the Class ```python from ReadWriteMemory import ReadWriteMemory +``` +### Instantiate the Class +```python rwm = ReadWriteMemory() ``` ---- +### Get a Process by name +```python +process = rwm.get_process_by_name('ac_client.exe') +``` + +### Get a Process by ID +```python +process = rwm.get_process_by_name(1337) +``` + +### Print the Process information +```python +print(process.__dict__) +``` + +### Print the Process HELP docs +```python +help(process) +``` + +### Open the Process +To be able to read or write to the process's memory first you need to call the open() method. +```python +process.open() +``` + +### Set the pointers for example: to get health, ammo and grenades +The offsets must be a list in the correct order, if the address does not have any offsets then just pass the address. You need to pass two arguments, first the process address as hex and a list of offsets as hex. +```python +health_pointer = process.get_pointer(0x004e4dbc, offsets=[0xf4]) +ammo_pointer = process.get_pointer(0x004df73c, offsets=[0x378, 0x14, 0x0]) +grenade_pointer = process.get_pointer(0x004df73c, offsets=[0x35c, 0x14, 0x0]) +``` + +### Read the values for the health, ammo and grenades from the Process's memory +```python +health = process.read(health_pointer) +ammo = process.read(ammo_pointer) +grenade = process.read(grenade_pointer) +``` + +### Print the health, ammo and grenade values +```python +print({'Health': health, 'Ammo': ammo, 'Grenade': grenade}) +``` + +### Write some random values for health, ammo and grenade to the Process's memory +```python +process.write(health_pointer, randint(1, 100)) +process.write(ammo_pointer, randint(1, 20)) +process.write(grenade_pointer, randint(1, 5)) +``` -### Get the process PID by the process name - ReadWriMemory.get_process_id_by_name(process_name: str) +### Close the Process's handle when you are done using it. +```python +process.close() +``` +### Examples +Check out the code inside the Test folder on the python file named testing_script.py. +The AssaultCube game used for this test is version v1.1.0.4 If you use a different version then you will have to use CheatEngine to find the memory addresses. +[https://github.com/assaultcube/AC/releases/tag/v1.1.0.4](https://github.com/assaultcube/AC/releases/tag/v1.1.0.4)
+For more examples check out the AssaultCube game trainer: +[https://github.com/vsantiago113/ACTrainer](https://github.com/vsantiago113/ACTrainer) ```python from ReadWriteMemory import ReadWriteMemory +from random import randint rwm = ReadWriteMemory() +process = rwm.get_process_by_name('ac_client.exe') +process.open() -pid = rwm.get_process_id_by_name('ac_client.exe') -``` +print('\nPrint the Process information.') +print(process.__dict__) + +health_pointer = process.get_pointer(0x004e4dbc, offsets=[0xf4]) +ammo_pointer = process.get_pointer(0x004df73c, offsets=[0x378, 0x14, 0x0]) +grenade_pointer = process.get_pointer(0x004df73c, offsets=[0x35c, 0x14, 0x0]) +print(health_pointer) + +health = process.read(health_pointer) +ammo = process.read(ammo_pointer) +grenade = process.read(grenade_pointer) + +print('\nPrinting the current values.') +print({'Health': health, 'Ammo': ammo, 'Grenade': grenade}) + +process.write(health_pointer, randint(1, 100)) +process.write(ammo_pointer, randint(1, 20)) +process.write(grenade_pointer, randint(1, 5)) + +health = process.read(health_pointer) +ammo = process.read(ammo_pointer) +grenade = process.read(grenade_pointer) + +print('\nPrinting the new modified random values.') +print({'Health': health, 'Ammo': ammo, 'Grenade': grenade}) + +process.close() -### How to open the process - ReadWriMemory.open() +``` \ No newline at end of file diff --git a/ReadWriteMemory/__init__.py b/ReadWriteMemory/__init__.py index 02c075c..735f952 100644 --- a/ReadWriteMemory/__init__.py +++ b/ReadWriteMemory/__init__.py @@ -1,3 +1,4 @@ +from typing import Any, List, NewType import os.path import ctypes import ctypes.wintypes @@ -11,60 +12,85 @@ MAX_PATH = 260 -class ReadWriteMemory: - def __init__(self): - self.process_id = None - self.process_name = None - self.handle_process = None +class ReadWriteMemoryError(Exception): + pass - def get_process_id_by_name(self, process_name): - if not process_name.endswith('.exe'): - process_name = process_name + '.exe' - process_ids, bytes_returned = self.enumerate_processes() +class Process(object): + """ + The Process class holds the information about the requested process. + """ + def __init__(self, name: str = '', pid: int = -1, handle: int = -1, error_code: str = None): + """ + :param name: The name of the executable file for the specified process. + :param pid: The process ID. + :param handle: The process handle. + :param error_code: The error code from a process failure. + """ + self.name = name + self.pid = pid + self.handle = handle + self.error_code = error_code - for index in range(int(bytes_returned / ctypes.sizeof(ctypes.wintypes.DWORD)))[:-1]: - process_id = process_ids[index] - handle_process = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, process_id) - if handle_process: - image_file_name = (ctypes.c_char * MAX_PATH)() - if ctypes.windll.psapi.GetProcessImageFileNameA(handle_process, image_file_name, MAX_PATH) > 0: - filename = os.path.basename(image_file_name.value) - if filename.decode('utf-8') == process_name: - self.process_id = process_id - self.process_name = process_name - return process_id - self.close() + def __repr__(self) -> str: + return f'{self.__class__.__name__}: "{self.name}"' - @staticmethod - def enumerate_processes(): - count = 32 - while True: - process_ids = (ctypes.wintypes.DWORD * count)() - cb = ctypes.sizeof(process_ids) - bytes_returned = ctypes.wintypes.DWORD() - if ctypes.windll.Psapi.EnumProcesses(ctypes.byref(process_ids), cb, ctypes.byref(bytes_returned)): - if bytes_returned.value < cb: - return process_ids, bytes_returned.value - else: - count *= 2 + def open(self) -> bool: + """ + Open the process with the Query, Operation, Read and Write permissions and return the process handle. - def open(self): + :return: True if the handle exists if not return False + """ dw_desired_access = (PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE) b_inherit_handle = False - handle_process = ctypes.windll.kernel32.OpenProcess(dw_desired_access, b_inherit_handle, self.process_id) - self.handle_process = handle_process - return handle_process + self.handle = ctypes.windll.kernel32.OpenProcess(dw_desired_access, b_inherit_handle, self.pid) + if self.handle: + return True + else: + return False - def close(self): - ctypes.windll.kernel32.CloseHandle(self.handle_process) + def close(self) -> int: + """ + Closes the handle of the process. + + :return: The last error code from the result after an attempt to close the handle. + """ + ctypes.windll.kernel32.CloseHandle(self.handle) return self.get_last_error() @staticmethod - def get_last_error(): + def get_last_error() -> int: + """ + Get the last error code. + + :return: The last error code. + """ return ctypes.windll.kernel32.GetLastError() - def get_pointer(self, lp_base_address, offsets=None): + @property + def is_running(self): + """ + Check if the process is still running. + + :note: If the process ID changed this method will return False and a new handle need to be created. + + :return: True if the process is running and False if is not running or the process ID changed. + """ + self.handle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, self.pid) + if self.handle: + return True + else: + return False + + def get_pointer(self, lp_base_address: hex, offsets: List[hex] = ()) -> int: + """ + Get the pointer of a given address. + + :param lp_base_address: The address from where you want to get the pointer. + :param offsets: a list of offets. + + :return: The pointer of a give address. + """ temp_address = self.read(lp_base_address) pointer = 0x0 if not offsets: @@ -75,29 +101,127 @@ def get_pointer(self, lp_base_address, offsets=None): temp_address = self.read(pointer) return pointer - def read(self, lp_base_address): + def read(self, lp_base_address: int) -> Any: + """ + Read data from the process's memory. + + :param lp_base_address: The process's pointer + + :return: The data from the process's memory if succeed if not raises an exception. + """ try: read_buffer = ctypes.c_uint() lp_buffer = ctypes.byref(read_buffer) n_size = ctypes.sizeof(read_buffer) lp_number_of_bytes_read = ctypes.c_ulong(0) - ctypes.windll.kernel32.ReadProcessMemory(self.handle_process, lp_base_address, lp_buffer, + ctypes.windll.kernel32.ReadProcessMemory(self.handle, lp_base_address, lp_buffer, n_size, lp_number_of_bytes_read) return read_buffer.value - except (BufferError, ValueError, TypeError): - self.close() - error = 'Handle Closed, Error', self.handle_process, self.get_last_error() - return error + except (BufferError, ValueError, TypeError) as error: + if self.handle: + self.close() + self.error_code = self.get_last_error() + error = {'msg': str(error), 'Handle': self.handle, 'PID': self.pid, + 'Name': self.name, 'ErrorCode': self.error_code} + ReadWriteMemoryError(error) + + def write(self, lp_base_address: int, value: int) -> bool: + """ + Write data to the process's memory. - def write(self, lp_base_address, value): + :param lp_base_address: The process' pointer. + :param value: The data to be written to the process's memory + + :return: It returns True if succeed if not it raises an exception. + """ try: write_buffer = ctypes.c_uint(value) lp_buffer = ctypes.byref(write_buffer) n_size = ctypes.sizeof(write_buffer) lp_number_of_bytes_written = ctypes.c_ulong(0) - ctypes.windll.kernel32.WriteProcessMemory(self.handle_process, lp_base_address, lp_buffer, + ctypes.windll.kernel32.WriteProcessMemory(self.handle, lp_base_address, lp_buffer, n_size, lp_number_of_bytes_written) - except (BufferError, ValueError, TypeError): - self.close() - error = 'Handle Closed, Error', self.handle_process, self.get_last_error() - return error + return True + except (BufferError, ValueError, TypeError) as error: + if self.handle: + self.close() + self.error_code = self.get_last_error() + error = {'msg': str(error), 'Handle': self.handle, 'PID': self.pid, + 'Name': self.name, 'ErrorCode': self.error_code} + ReadWriteMemoryError(error) + + +class ReadWriteMemory: + """ + The ReadWriteMemory Class is used to read and write to the memory of a running process. + """ + def __init__(self): + self.process = Process() + + def get_process_by_name(self, process_name: str) -> "Process": + """ + :description: Get the process by the process executabe\'s name and return a Process object. + + :param process_name: The name of the executable file for the specified process for example, my_program.exe. + + :return: A Process object containing the information from the requested Process. + """ + if not process_name.endswith('.exe'): + self.process.name = process_name + '.exe' + + process_ids = self.enumerate_processes() + + for process_id in process_ids: + self.process.handle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, process_id) + if self.process.handle: + image_file_name = (ctypes.c_char * MAX_PATH)() + if ctypes.windll.psapi.GetProcessImageFileNameA(self.process.handle, image_file_name, MAX_PATH) > 0: + filename = os.path.basename(image_file_name.value) + if filename.decode('utf-8') == process_name: + self.process.pid = process_id + self.process.name = process_name + return self.process + self.process.close() + + raise ReadWriteMemoryError(f'Process "{self.process.name}" not found!') + + def get_process_by_id(self, process_id: int) -> "Process": + """ + :description: Get the process by the process ID and return a Process object. + + :param process_id: The process ID. + + :return: A Process object containing the information from the requested Process. + """ + + self.process.handle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, process_id) + if self.process.handle: + image_file_name = (ctypes.c_char * MAX_PATH)() + if ctypes.windll.psapi.GetProcessImageFileNameA(self.process.handle, image_file_name, MAX_PATH) > 0: + filename = os.path.basename(image_file_name.value) + self.process.pid = process_id + self.process.name = filename.decode('utf-8') + self.process.close() + return self.process + else: + raise ReadWriteMemoryError(f'Unable to get the executable\'s name for PID={self.process.pid}!') + + raise ReadWriteMemoryError(f'Process "{self.process.pid}" not found!') + + @staticmethod + def enumerate_processes() -> list: + """ + Get the list of running processes ID's from the current system. + + :return: A list of processes ID's + """ + count = 32 + while True: + process_ids = (ctypes.wintypes.DWORD * count)() + cb = ctypes.sizeof(process_ids) + bytes_returned = ctypes.wintypes.DWORD() + if ctypes.windll.Psapi.EnumProcesses(ctypes.byref(process_ids), cb, ctypes.byref(bytes_returned)): + if bytes_returned.value < cb: + return list(set(process_ids)) + else: + count *= 2 diff --git a/Test/testing_script.py b/Test/testing_script.py new file mode 100644 index 0000000..0f3a7cb --- /dev/null +++ b/Test/testing_script.py @@ -0,0 +1,34 @@ +from ReadWriteMemory import ReadWriteMemory +from random import randint + +rwm = ReadWriteMemory() +process = rwm.get_process_by_name('ac_client.exe') +process.open() + +print('\nPrint the Process information.') +print(process.__dict__) + +health_pointer = process.get_pointer(0x004e4dbc, offsets=[0xf4]) +ammo_pointer = process.get_pointer(0x004df73c, offsets=[0x378, 0x14, 0x0]) +grenade_pointer = process.get_pointer(0x004df73c, offsets=[0x35c, 0x14, 0x0]) +print(health_pointer) + +health = process.read(health_pointer) +ammo = process.read(ammo_pointer) +grenade = process.read(grenade_pointer) + +print('\nPrinting the current values.') +print({'Health': health, 'Ammo': ammo, 'Grenade': grenade}) + +process.write(health_pointer, randint(1, 100)) +process.write(ammo_pointer, randint(1, 20)) +process.write(grenade_pointer, randint(1, 5)) + +health = process.read(health_pointer) +ammo = process.read(ammo_pointer) +grenade = process.read(grenade_pointer) + +print('\nPrinting the new modified random values.') +print({'Health': health, 'Ammo': ammo, 'Grenade': grenade}) + +process.close() diff --git a/requirements.txt b/requirements.txt index 8b13789..e69de29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +0,0 @@ - diff --git a/setup.py b/setup.py index 2e4c762..d1364c2 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ from distutils.core import setup setup(name='ReadWriteMemory', - version='0.1.2', - description='ReadWriteMemory wrapper to work with Windows Processes Memory.', + version='0.1.4', + description='ReadWriteMemory Class to work with Windows process memory and hacking video games.', author='Victor M Santiago', author_email='vsantiago113sec@gmail.com', python_requires='>=3.4.0',