Skip to content

Instantly share code, notes, and snippets.

@dtrugman
Last active January 27, 2021 14:59
Show Gist options
  • Save dtrugman/90cca8ceed0586889ddc75631de120d0 to your computer and use it in GitHub Desktop.
Save dtrugman/90cca8ceed0586889ddc75631de120d0 to your computer and use it in GitHub Desktop.
Python-based real-time process memory reader
#!/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