Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory scanner for any program in Python 3

I am trying to create a memory scanner, similar to the Cheat Engine, but only for extracting information. So, given a Process ID — in this case the PID of "notepad.exe" — I would like to know which specific addresses belong to the program that I am scanning.

Trying to look for examples, I noticed someone attempting to scan every address from one point to another. But it's too slow. Then I tried to create a batch size to scan just a part of the memory.

The problem is: if the size is too short, it'll take too long still. Otherwise, if the size is too long, it's possible to lose many addresses, because even the result of ReadMemoryScan is false for the first address, the next one might be true. Here is my example.

from ctypes import wintypes
from sys import stdout

import ctypes
import psutil

write = stdout.write
import numpy as np

def get_client_pid(process_name):
    for process in psutil.process_iter():
        if process.name() == process_name:
            return int(process.pid)   

pid = get_client_pid("notepad.exe")

if pid == None:
    sys.exit()

k32 = ctypes.WinDLL("kernel32", use_last_error=True)

OpenProcess = k32.OpenProcess
OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
OpenProcess.restype = wintypes.HANDLE

ReadProcessMemory = k32.ReadProcessMemory

ReadProcessMemory.argtypes = [
    wintypes.HANDLE,
    wintypes.LPCVOID,
    wintypes.LPVOID,
    ctypes.c_size_t,
    ctypes.POINTER(ctypes.c_size_t)
]
ReadProcessMemory.restype = wintypes.BOOL

GetLastError = k32.GetLastError
GetLastError.argtypes = None
GetLastError.restype = wintypes.DWORD

CloseHandle = k32.CloseHandle
CloseHandle.argtypes = [wintypes.HANDLE]
CloseHandle.restype = wintypes.BOOL

processHandle = OpenProcess(0x10, False, int(pid))

# addr = 0x0FFFFFFFFFFF

data = ctypes.c_ulonglong()
bytesRead = ctypes.c_ulonglong()

start = 0x000000000000
end =   0x7fffffffffff
batch_size = 2 ** 13

MemoryData = np.zeros(batch_size, "l")
Size = MemoryData.itemsize * MemoryData.size

Data_address = []
index = 0

for address in range(start, end, batch_size):
    
    result = ReadProcessMemory(
        processHandle, 
        ctypes.c_void_p(address), 
        MemoryData.ctypes.data, 
        Size, ctypes.byref(bytesRead)
    )

    if result: # Save the address
        Data_address.extend(list(range(address, address + batch_size)))
        
error = GetLastError()
CloseHandle(processHandle)

I decided searching from 0x000000000000 to 0x7fffffffffff because it seems Cheat Engine scans this size. Are there things that I could do to improve the search performance?

like image 718
Cristhian Grael Avatar asked Jul 26 '26 16:07

Cristhian Grael


1 Answers

You may use my PyMemoryEditor package for scanning the process as well as easily read and write to the process memory. This package supports Windows and Linux and works similar to the Cheat Engine, also having all scan types, allowing you to search for integers, float, booleans, bytes and strings. See the example bellow:

from PyMemoryEditor import OpenProcess, ScanTypesEnum

value_content = 9712
value_size = 4  # Amount of bytes (in this case, 4 bytes)
pytype = int    # Type of the value in Python
scan_type = ScanTypesEnum.EXACT_VALUE

with OpenProcess(process_name = "notepad.exe") as process:
    for address in process.search_by_value(pytype, value_size, value_content, scan_type):
        print("Found address", address, "for the value", value_content)

What this method is doing internally is getting all memory regions of the process and trying to find the value in a frame of size value_size at each memory region. So you don't have to search your whole memory. Just search on the memory pages of the target process.

This method works very quick for the scan types EXACT_VALUE and NOT_EXACT_VALUE, because it implements the KMP (Knuth–Morris–Pratt) Algorithm for searching, that has complexity O(n + m) where n is the memory region size and m is the size of the value in bytes.

You may even check out the scanning progress setting the progress_information as True. This way, the method search_by_value will return the progress of the scanning for each found address.

for address, info process.search_by_value(..., progress_information = True):
    template = "Address: 0x{:<10X} | Progress: {:.1f}%"
    progress = info["progress"] * 100
    
    print(template.format(address, progress))

For reading and writing process memory, you should use the methods below:

value = process.read_process_memory(address, int, 4)

new_value = value + 1

process.write_process_memory(address, int, 4, new_value)

But you can also use the method search_by_addresses(...) if you have a large number of addresses that must be read. This way, you reduce the frequency of system calls.

like image 55
JeanExtreme002 Avatar answered Jul 29 '26 06:07

JeanExtreme002



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!