@ -2,7 +2,7 @@
Abstract class for cross - platform memory editing .
"""
from typing import List , Tuple
from typing import List , Tuple , Optional , Union , Generator
from abc import ABCMeta , abstractmethod
from contextlib import contextmanager
import copy
@ -13,7 +13,6 @@ from . import utils
from . utils import ctypes_buffer_t
logging . basicConfig ( level = logging . INFO )
logger = logging . getLogger ( __name__ )
@ -23,8 +22,8 @@ class Process(metaclass=ABCMeta):
( i . e . , by reading from or writing to the memory used by a given process ) .
The static methods
Process . list_available_pids ( )
Process . get_pid_by_name ( executable_filename )
` Process . list_available_pids ( ) `
` Process . get_pid_by_name ( executable_filename ) `
can be used to help find the process id ( pid ) of the target process . They are
provided for convenience only ; it is probably better to use the tools built
in to your operating system to discover the pid of the specific process you
@ -32,18 +31,19 @@ class Process(metaclass=ABCMeta):
Once you have found the pid , you are ready to construct an instance of Process
and use it to read and write to memory . Once you are done with the process ,
use . close ( ) to free up the process for access by other debuggers etc .
use ` . close ( ) ` to free up the process for access by other debuggers etc .
` ` `
p = Process ( 1239 )
p . close ( )
` ` `
To read / write to memory , first create a buffer using ctypes :
` ` `
buffer0 = ( ctypes . c_byte * 5 ) ( 39 , 50 , 03 , 40 , 30 )
buffer1 = ctypes . c_ulong ( )
` ` `
and then use
` ` `
p . write_memory ( 0x2fe , buffer0 )
val0 = p . read_memory ( 0x220 , buffer0 ) [ : ]
@ -51,52 +51,52 @@ class Process(metaclass=ABCMeta):
val1a = p . read_memory ( 0x149 , buffer1 ) . value
val2b = buffer1 . value
assert ( val1a == val2b )
` ` `
Searching for a value can be done in a number of ways :
Search a list of addresses :
found_addresses = p . search_addresses ( [ 0x1020 , 0x1030 ] , buffer0 )
` found_addresses = p . search_addresses ( [ 0x1020 , 0x1030 ] , buffer0 ) `
Search the entire memory space :
found_addresses = p . search_all_memory ( buffer0 , writeable_only = False )
` found_addresses = p . search_all_memory ( buffer0 , writeable_only = False ) `
You can also get a list of which regions in memory are mapped ( readable ) :
regions = p . list_mapped_regions ( writeable_only = False )
which can be used along with search_buffer ( . . . ) to re - create . search_all_memory ( . . . ) :
` regions = p . list_mapped_regions ( writeable_only = False ) `
which can be used along with search_buffer ( . . . ) to re - create . search_all_memory ( . . . ) :
` ` `
found = [ ]
for region_start , region_stop in regions :
region_buffer = ( ctypes . c_byte * ( region_stop - region_start ) ) ( )
p . read_memory ( region_start , region_buffer )
found + = utils . search_buffer ( ctypes . c_ulong ( 123456790 ) , region_buffer )
` ` `
Other useful methods include the context manager , implemented as a static method :
` ` `
with Process . open_process ( pid ) as p :
# use p here, no need to call p.close()
` ` `
. get_path ( ) , which reports the path of the executable file which was used
to start the process :
` ` `
executable_path = p . get_path ( )
` ` `
and deref_struct_pointer , which takes a pointer to a struct and reads out the struct members :
` ` `
# struct is a list of (offset, buffer) pairs
struct_defintion = [ ( 0x0 , ctypes . c_ulong ( ) ) ,
( 0x20 , ctypes . c_byte ( ) ) ]
values = p . deref_struct_pointer ( 0x0feab4 , struct_defintion )
` ` `
which is shorthand for
` ` `
struct_addr = p . read_memory ( 0x0feab4 , ctypes . c_void_p ( ) )
values = [ p . read_memory ( struct_addr + 0x0 , ctypes . c_ulong ( ) ) ,
p . read_memory ( struct_addr + 0x20 , ctypes . c_byte ( ) ) ]
` ` `
== == == == == == == == =
Putting all this together , a simple program which alters a magic number in the only running
instance of ' magic.exe ' might look like this :
` ` `
import ctypes
from mem_edit import Process
@ -107,9 +107,9 @@ class Process(metaclass=ABCMeta):
addrs = p . search_all_memory ( magic_number )
assert ( len ( addrs ) == 1 )
p . write_memory ( addrs [ 0 ] , ctypes . c_ulong ( 42 ) )
` ` `
Searching for a value which changes :
` ` `
pid = Process . get_pid_by_name ( ' monitor_me.exe ' )
with Process . open_process ( pid ) as p :
addrs = p . search_all_memory ( ctypes . c_int ( 40 ) )
@ -118,18 +118,19 @@ class Process(metaclass=ABCMeta):
print ( ' Found addresses: ' )
for addr in filtered_addrs :
print ( hex ( addr ) )
` ` `
"""
@abstractmethod
def __init__ ( self , process_id : int ) :
"""
Constructing a Process object prepares the process with specified process_id for
memory editing . Finding the process_id for the process you want to edit is often
memory editing . Finding the ` process_id ` for the process you want to edit is often
easiest using os - specific tools ( or by launching the process yourself , e . g . with
subprocess . Popen ( . . . ) ) .
` subprocess . Popen ( . . . ) ` ) .
: param process_id : Process id ( pid ) of the target process
Args :
process_id : Process id ( pid ) of the target process
"""
pass
@ -140,7 +141,7 @@ class Process(metaclass=ABCMeta):
letting other debuggers attach to it instead .
This function should be called after you are done working with the process
and will no longer need it . See the Process . open_process ( . . . ) context
and will no longer need it . See the ` Process . open_process ( . . . ) ` context
manager to avoid having to call this function yourself .
"""
pass
@ -148,38 +149,45 @@ class Process(metaclass=ABCMeta):
@abstractmethod
def write_memory ( self , base_address : int , write_buffer : ctypes_buffer_t ) :
"""
Write the given buffer to the process ' s address space, starting at base_address.
Write the given buffer to the process ' s address space, starting at ` base_address` .
: param base_address : The address to write at , in the process ' s address space.
: param write_buffer : A ctypes object , for example , ctypes . c_ulong ( 48 ) ,
( ctypes . c_byte * 3 ) ( 43 , 21 , 0xff ) , or a subclass of ctypes . Structure ,
which will be written into memory starting at base_address .
Args :
base_address : The address to write at , in the process ' s address space.
write_buffer : A ctypes object , for example , ` ctypes . c_ulong ( 48 ) ` ,
` ( ctypes . c_byte * 3 ) ( 43 , 21 , 0xff ) ` , or a subclass of ` ctypes . Structure ` ,
which will be written into memory starting at ` base_address ` .
"""
pass
@abstractmethod
def read_memory ( self , base_address : int , read_buffer : ctypes_buffer_t ) - > ctypes_buffer_t :
"""
Read into the given buffer from the process ' s address space, starting at base_address.
Read into the given buffer from the process ' s address space, starting at ` base_address` .
: param base_address : The address to read from , in the process ' s address space.
: param read_buffer : A ctypes object , for example . ctypes . c_ulong ( ) ,
( ctypes . c_byte * 3 ) ( ) , or a subclass of ctypes . Structure , which will be
overwritten with the contents of the process ' s memory starting at base_address.
: returns : read_buffer is returned as well as being overwritten .
Args :
base_address : The address to read from , in the process ' s address space.
read_buffer : A ` ctypes ` object , for example . ` ctypes . c_ulong ( ) ` ,
` ( ctypes . c_byte * 3 ) ( ) ` , or a subclass of ` ctypes . Structure ` , which will be
overwritten with the contents of the process ' s memory starting at `base_address`.
Returns :
` read_buffer ` is returned as well as being overwritten .
"""
pass
@abstractmethod
def list_mapped_regions ( self , writeable_only = True ) - > List [ Tuple [ int , int ] ] :
"""
Return a list of ( start_address , stop_address ) for the regions of the address space
Return a list of ` ( start_address , stop_address ) ` for the regions of the address space
accessible to ( readable and possibly writable by ) the process .
By default , this function does not return non - writeable regions .
: param writeable_only : If True , only return regions which are also writeable .
Default true .
: return : List of ( start_address , stop_address ) for each accessible memory region .
Args :
writeable_only : If ` True ` , only return regions which are also writeable .
Default ` True ` .
Returns :
List of ` ( start_address , stop_address ) ` for each accessible memory region .
"""
pass
@ -188,7 +196,8 @@ class Process(metaclass=ABCMeta):
"""
Return the path to the executable file which was run to start this process .
: return : A string containing the path .
Returns :
A string containing the path .
"""
pass
@ -198,13 +207,14 @@ class Process(metaclass=ABCMeta):
"""
Return a list of all process ids ( pids ) accessible on this system .
: return : List of running process ids .
Returns :
List of running process ids .
"""
pass
@staticmethod
@abstractmethod
def get_pid_by_name ( target_name : str ) - > int or None :
def get_pid_by_name ( target_name : str ) - > Optional [ int ] :
"""
Attempt to return the process id ( pid ) of a process which was run with an executable
file with the provided name . If no process is found , return None .
@ -215,7 +225,12 @@ class Process(metaclass=ABCMeta):
Don ' t rely on this method if you can possibly avoid it, since it makes no
attempt to confirm that it found a unique process and breaks trivially ( e . g . if the
executable file is renamed ) .
: return : Process id ( pid ) of a process with the provided name , or None .
Args :
target_name : Name of the process to find the PID for
Returns :
Process id ( pid ) of a process with the provided name , or ` None ` .
"""
pass
@ -225,34 +240,48 @@ class Process(metaclass=ABCMeta):
) - > List [ ctypes_buffer_t ] :
"""
Take a pointer to a struct and read out the struct members :
` ` `
struct_defintion = [ ( 0x0 , ctypes . c_ulong ( ) ) ,
( 0x20 , ctypes . c_byte ( ) ) ]
values = p . deref_struct_pointer ( 0x0feab4 , struct_defintion )
` ` `
which is shorthand for
` ` `
struct_addr = p . read_memory ( 0x0feab4 , ctypes . c_void_p ( ) )
values = [ p . read_memory ( struct_addr + 0x0 , ctypes . c_ulong ( ) ) ,
p . read_memory ( struct_addr + 0x20 , ctypes . c_byte ( ) ) ]
` ` `
: param base_address : Address at which the struct pointer is located .
: param targets : List of ( offset , read_buffer ) pairs which will be read from the struct .
: return : List of read values corresponding to the provided targets .
Args :
base_address : Address at which the struct pointer is located .
targets : List of ` ( offset , read_buffer ) ` pairs which will be read from the struct .
Return :
List of read values corresponding to the provided targets .
"""
base = self . read_memory ( base_address , ctypes . c_void_p ( ) ) . value
values = [ self . read_memory ( base + offset , buffer ) for offset , buffer in targets ]
return values
def search_addresses ( self , addresses : List [ int ] , needle_buffer : ctypes_buffer_t , verbatim : bool = True ) - > List [ int ] :
def search_addresses ( self ,
addresses : List [ int ] ,
needle_buffer : ctypes_buffer_t ,
verbatim : bool = True ,
) - > List [ int ] :
"""
Search for the provided value at each of the provided addresses , and return the addresses
where it is found .
: param addresses : List of addresses which should be probed .
: param needle_buffer : The value to search for . This should be a ctypes object of the same
sorts as used by . read_memory ( . . . ) , which will be compared to the contents of
Args :
addresses : List of addresses which should be probed .
needle_buffer : The value to search for . This should be a ` ctypes ` object of the same
sorts as used by ` . read_memory ( . . . ) ` , which will be compared to the contents of
memory at each of the given addresses .
: param verbatim : If True , perform bitwise comparison when searching for needle_buffer .
If False , perform utils . ctypes_equal - based comparison . Default True .
: return : List of addresses where the needle_buffer was found .
verbatim : If ` True ` , perform bitwise comparison when searching for ` needle_buffer ` .
If ` False ` , perform ` utils . ctypes_equal ` - based comparison . Default ` True ` .
Returns :
List of addresses where the ` needle_buffer ` was found .
"""
found = [ ]
read_buffer = copy . copy ( needle_buffer )
@ -269,18 +298,25 @@ class Process(metaclass=ABCMeta):
found . append ( address )
return found
def search_all_memory ( self , needle_buffer : ctypes_buffer_t , writeable_only : bool = True , verbatim : bool = True ) - > List [ int ] :
def search_all_memory ( self ,
needle_buffer : ctypes_buffer_t ,
writeable_only : bool = True ,
verbatim : bool = True ,
) - > List [ int ] :
"""
Search the entire memory space accessible to the process for the provided value .
: param needle_buffer : The value to search for . This should be a ctypes object of the same
sorts as used by . read_memory ( . . . ) , which will be compared to the contents of
Args :
needle_buffer : The value to search for . This should be a ctypes object of the same
sorts as used by ` . read_memory ( . . . ) ` , which will be compared to the contents of
memory at each accessible address .
: param writeable_only : If True , only search regions where the process has write access .
Default True .
: param verbatim : If True , perform bitwise comparison when searching for needle_buffer .
If False , perform utils . ctypes_equal - based comparison . Default True .
: return : List of addresses where the needle_buffer was found .
writeable_only : If ` True ` , only search regions where the process has write access .
Default ` True ` .
verbatim : If ` True ` , perform bitwise comparison when searching for ` needle_buffer ` .
If ` False ` , perform ` utils . ctypes_equal - based ` comparison . Default ` True ` .
Returns :
List of addresses where the ` needle_buffer ` was found .
"""
found = [ ]
if verbatim :
@ -299,15 +335,20 @@ class Process(metaclass=ABCMeta):
@classmethod
@contextmanager
def open_process ( cls , process_id : int ) - > ' Process ' :
def open_process ( cls , process_id : int ) - > Generator [ ' Process ' , None , None ] :
"""
Context manager which automatically closes the constructed Process :
` ` `
with Process . open_process ( 2394 ) as p :
# use p here
# no need to run p.close()
` ` `
: param process_id : Process id ( pid ) , passed to the Process constructor .
: return : Constructed Process object .
Args :
process_id : Process id ( pid ) , passed to the Process constructor .
Returns :
Constructed Process object .
"""
process = cls ( process_id )
yield process