srounet/Pymem

Optimize Memory Reading / Writing using C types

qb-0 opened this issue · 7 comments

qb-0 commented

The current behavior of pymem is to read / write bytes into a buffer and unpacking / packing the binary data with struct. This could be optimized by copying the bytes into a ctype (with memmove) and using the value field. This way the whole unpacking / packing process can be skipped.

def read(handle: int, address: int, c_type, get_py_value=True) -> Any:
    size = ctypes.sizeof(c_type)
    buff = ctypes.create_string_buffer(size)
    if ReadProcessMemory(handle, ctypes.c_void_p(address), ctypes.byref(buff), size, None) == 0:
        raise OSError(GetLastError())
    ctypes.memmove(ctypes.byref(c_type), ctypes.byref(buff), size)
    if get_py_value:
        return c_type.value
    return c_type

Reading an Integer would then look like:

def read_int(handle, address):
    return read(handle, address, ctypes.c_int())

My current tests on Assault Cube show some small speedup's on simple memory operations.

pyMem (Local: {'name': 'unarmed', 'health': 100, 'pos_x': 129.7091827392578, 'pos_y': 192.5406494140625, 'pos_z': 5.5}): Memory: 1.8688466548919678 sec
cTypes Mem (Local: {'name': 'unarmed', 'health': 100, 'pos_x': 129.7091827392578, 'pos_y': 192.5406494140625, 'pos_z': 5.5}): Memory: 1.6034820079803467 sec

The real advantage shows up when you read whole c structures which could be used this way.

cTypes Mem Struct (Local: {'name': 'unarmed', 'health': 100, 'pos_x': 129.7091827392578, 'pos_y': 192.5406494140625, 'pos_z': 5.5}): Memory: 0.3543844223022461 sec

The whole project and the testing file can be reviewed here: https://github.com/qb-0/RW-Mem-C

seems like a good idea to me

made a new branch to track work on this and added the read methods in https://github.com/srounet/Pymem/tree/impl-%23101

qb-0 commented

I just figured out that you don't even need to create a buffer and move the memory. You can pass the c type directly to ReadProcessMemory. Increases the performance even more.

def read(handle: int, address: int, c_type, get_py_value=True) -> Any:
    size = ctypes.sizeof(c_type)
    if ReadProcessMemory(handle, ctypes.c_void_p(address), ctypes.byref(c_type), size, None) == 0:
        raise OSError(GetLastError())
    if get_py_value:
        return c_type.value
    return c_type

nice, also did you see a performance improvement when using this method for writing?

qb-0 commented

I didn't tried yet. I could write up some tests tomorrow. But I guess the result will be pretty much the same as you don't need to create a byte array from the python value:

def write(handle, address: int, data: Any) -> int:
    result = ctypes.c_size_t()
    WriteProcessMemory(
        handle, 
        ctypes.cast(address, ctypes.c_void_p), 
        ctypes.cast(ctypes.byref(data), ctypes.c_void_p), 
        size, 
        ctypes.byref(result)
    )
    return result.value

def write_int(handle, address, value):
    return write(handle, address, ctypes.c_int(value)) > 0

I'll remove the buffer from the read method and add write tomorrow, then just need to test it and it should be good to go

qb-0 commented

So this are my results for writing.

[R] pyMem (Local: {'name': 'unarmed', 'health': 100, 'pos_x': 182.0, 'pos_y': 73.0, 'pos_z': 3.5}): Memory: 1.7718544006347656 sec
[W] pyMem 2.9343247413635254 sec
[R] cTypes Mem (Local: {'name': 'unarmed', 'health': 101, 'pos_x': 182.0, 'pos_y': 73.0, 'pos_z': 3.5}): Memory: 1.0322694778442383 sec
[W] cTypes Mem 2.876091957092285 sec
[R] cTypes Mem Struct (Local: {'name': 'unarmed', 'health': 102, 'pos_x': 182.0, 'pos_y': 73.0, 'pos_z': 3.5}): Memory: 0.24591755867004395 sec
[W] cTypes Mem Struct 0.7125911712646484 sec

https://github.com/qb-0/RW-Mem-C/blob/master/compare_speed.py