Last active
January 27, 2021 14:59
-
-
Save dtrugman/90cca8ceed0586889ddc75631de120d0 to your computer and use it in GitHub Desktop.
Python-based real-time process memory reader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
import sys | |
from os import getuid | |
from signal import pthread_kill | |
from argparse import ArgumentParser, RawDescriptionHelpFormatter | |
from binascii import hexlify | |
class MemoryRegion(): | |
def __init__(self, line): | |
self.__line = line | |
tokens = self.__line.split() | |
self.__addresses = tokens[0].split('-') | |
self.__start_address = int(self.__addresses[0], 16) | |
self.__end_address = int(self.__addresses[1], 16) | |
self.__size = self.__end_address - self.__start_address | |
self.__permissions = tokens[1] | |
self.__offset = int(tokens[2], 16) | |
self.__device = tokens[3] | |
self.__inode = tokens[4] | |
self.__target = tokens[5] if len(tokens) == 6 else '' | |
def __str__(self): | |
return '0x{:x}-0x{:x} (0x{:08x}) {} {}'.format( | |
self.__start_address, self.__end_address, self.__size, | |
self.__permissions, self.__target) | |
@property | |
def is_vsyscall(self): | |
return self.__target == '[vsyscall]' | |
@property | |
def is_vvar(self): | |
return self.__target == '[vvar]' | |
@property | |
def is_readable(self): | |
return self.__permissions.startswith('r') | |
@property | |
def start_address(self): | |
return self.__start_address | |
@property | |
def size(self): | |
return self.__size | |
def byte_to_ascii(b): | |
return chr(b) if 0x20 <= b <= 0x7E else '.' | |
def print_memory_pretty(line, addr, width): | |
ascii_dump = ''.join(byte_to_ascii(x) for x in line) | |
hex_width = (width * 4) + (width - 1) | |
hex_dump = ' '.join('0x{:02x}'.format(x) for x in line).ljust(hex_width, ' ') | |
print('0x{:016x} {} {}'.format(addr, hex_dump, ascii_dump)) | |
def print_memory_raw(line): | |
hex_dump = hexlify(bytearray(line)).decode('ascii') | |
print(hex_dump) | |
def print_memory_segment(pid, addr, size, width, raw=False): | |
path = '/proc/{}/mem'.format(pid) | |
with open(path, 'rb') as fin: | |
fin.seek(addr) | |
data = fin.read(size) | |
for i in range(0, size, width): | |
line = data[i : i + width] | |
if raw: | |
print_memory_raw(line) | |
else: | |
print_memory_pretty(line, addr + i, width) | |
def read_process_memory_maps(pid): | |
path = '/proc/{}/maps'.format(pid) | |
with open(path) as fin: | |
maps = fin.readlines() | |
return [MemoryRegion(line) for line in maps] | |
def print_process_memory_maps(args): | |
pid = args.pid | |
regions = read_process_memory_maps(pid) | |
print(*regions, sep = "\n") | |
def print_process_memory_full(args): | |
pid = args.pid | |
width = args.width | |
print('=============================================================') | |
print('Reading entire process memory') | |
print('-------------------------------------------------------------') | |
print('PID: {}'.format(pid)) | |
print('-------------------------------------------------------------') | |
try: | |
regions = read_process_memory_maps(pid) | |
for region in regions: | |
if not region.is_readable or region.is_vvar or region.is_vsyscall: | |
continue | |
print_memory_segment(pid, region.start_address, region.size, width) | |
except OSError as err: | |
print('{}: Memory not mapped'.format(err)) | |
except PermissionError as err: | |
print(err) | |
def print_process_memory_segment(args): | |
pid = args.pid | |
addr = args.addr | |
size = args.size | |
width = args.width | |
raw = args.raw | |
if not raw: | |
print('=============================================================') | |
print('Reading partial process memory') | |
print('-------------------------------------------------------------') | |
print('PID: {}'.format(pid)) | |
print('Address: {0} [0x{0:x}]'.format(addr)) | |
print('Bytes: {0} [0x{0:x}]'.format(size)) | |
print('-------------------------------------------------------------') | |
try: | |
print_memory_segment(pid, addr, size, width, raw) | |
except OSError as err: | |
print('{}: Memory not mapped'.format(err)) | |
except PermissionError as err: | |
print(err) | |
def offset(arg): | |
''' Offset argument parser for parsing addresses and sizes | |
Supports decimal and hex('0x') values | |
''' | |
if arg.startswith('0x'): | |
addr = int(arg, 16) | |
else: | |
addr = int(arg) | |
if addr % 16 != 0: | |
raise ValueError('Must be multiple of 16') | |
return addr | |
def add_common_arguments(parser): | |
parser.add_argument('--output', '-o', type=str, help='Output file') | |
parser.add_argument('--width', '-w', type=int, help='Bytes per line in output (Default: %(default)s)', | |
default=16, required=False) | |
def add_maps_menu(subparsers): | |
parser = subparsers.add_parser("maps", help="List process memory maps") | |
parser.set_defaults(func=print_process_memory_maps) | |
add_common_arguments(parser) | |
def add_dump_segment_menu(subparsers): | |
parser = subparsers.add_parser("dump_segment", help="Dump process memory segment") | |
parser.add_argument('addr', type=offset, help='Memory address to read from (Must be multiple of 16)') | |
parser.add_argument('size', type=offset, help='How many memory bytes to read (Must be multiple of 16)') | |
parser.add_argument('--raw', '-r', help="Print only raw memory in sequential hex format (e.g. For piping into xxd -r -p | xxd -b)", | |
default=False, action='store_true', required=False) | |
parser.set_defaults(func=print_process_memory_segment) | |
add_common_arguments(parser) | |
def add_dump_full_menu(subparsers): | |
parser = subparsers.add_parser("dump_all", help="Dump all process memory regions") | |
parser.set_defaults(func=print_process_memory_full) | |
add_common_arguments(parser) | |
def error(*args, **kwargs): | |
print('*************************************************************') | |
print('** ERROR **') | |
print('*************************************************************') | |
print(*args, **kwargs) | |
def main(): | |
parser = ArgumentParser( | |
prog='Memre', | |
formatter_class=RawDescriptionHelpFormatter, | |
description= \ | |
r''' | |
___ ___ ___ ___ ___ | |
/__/\ / /\ /__/\ / /\ / /\ | |
| |::\ / /:/_ | |::\ / /::\ / /:/_ | |
| |:|:\ / /:/ /\ | |:|:\ / /:/\:\ / /:/ /\ | |
__|__|:|\:\ / /:/ /:/_ __|__|:|\:\ / /:/~/:/ / /:/ /:/_ | |
/__/::::| \:\ /__/:/ /:/ /\ /__/::::| \:\ /__/:/ /:/___ /__/:/ /:/ /\ | |
\ \:\~~\__\/ \ \:\/:/ /:/ \ \:\~~\__\/ \ \:\/:::::/ \ \:\/:/ /:/ | |
\ \:\ \ \::/ /:/ \ \:\ \ \::/~~~~ \ \::/ /:/ | |
\ \:\ \ \:\/:/ \ \:\ \ \:\ \ \:\/:/ | |
\ \:\ \ \::/ \ \:\ \ \:\ \ \::/ | |
\__\/ \__\/ \__\/ \__\/ \__\/ | |
Read raw process memory | |
By Daniel Trugman | |
''') | |
parser.add_argument("pid", type=int, help='Target process') | |
subparsers = parser.add_subparsers(title='Mode', dest='mode') | |
add_maps_menu(subparsers) | |
add_dump_segment_menu(subparsers) | |
add_dump_full_menu(subparsers) | |
args = parser.parse_args() | |
if getuid() != 0: | |
error('Please run as root') | |
return 2 | |
if not args.func: | |
error('Must specify mode: {}'.format(','.join(subparsers.choices.keys()))) | |
return 2 | |
if args.output: | |
sys.stdout = open(args.output, 'w') | |
args.func(args) | |
return 0 | |
if __name__ == '__main__': | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment