├── .gitignore ├── LICENSE ├── README.md ├── memrepl.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Agustin Gianni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memrepl 2 | 3 | `memrepl` is a `frida` based script that aims to help a researcher in the task of exploitation of memory corruption related bugs. 4 | 5 | The idea is that the researcher can perform database like `queries` to get information about the contents and layout of the memory of a program. To perform these queries, `memrepl` exposes several global functions listed bellow: 6 | 7 | - `memory_list`: query current memory segments. 8 | - `memory_search`: search for a given value. 9 | - `memory_read`: read from a memory address. 10 | - `memory_write`: write to a memory address. 11 | - `memory_search_pointer`: search any pointers starting from a given address. 12 | 13 | ## Installation 14 | 15 | ``` 16 | # Install `pip` if not installed. 17 | $ easy_install pip 18 | 19 | # Install `virtualenv` if not installed. 20 | $ pip install virtualenv 21 | 22 | # Create a virtual python environment. 23 | $ virtualenv venv_memrepl 24 | 25 | # Activate the environment (POSIX system). 26 | $ source ./venv_memrepl/bin/activate 27 | 28 | # Install `memrepl` into the virtual environment. 29 | $ python setup.py install 30 | 31 | ``` 32 | ### Dependencies 33 | All the requirements will be installed automatically using python's `setuptools`. 34 | - `python` 35 | - `pip` 36 | - `virtualenv (optional)` 37 | - `frida` 38 | - `ipython` 39 | - `hexdump` 40 | 41 | ## Usage 42 | 43 | Execute `memrepl` with `-h` to get help: 44 | 45 | ``` 46 | $ memrepl -h 47 | usage: memrepl [-h] [-V] (-p PROC_PID | -n PROC_NAME | -l) [-d DEVICE] 48 | [-m MOD_NAMES] 49 | 50 | Memory Grip. 51 | 52 | optional arguments: 53 | -h, --help show this help message and exit 54 | -V, --version show program's version number and exit 55 | -p PROC_PID Process PID. 56 | -n PROC_NAME Process name (follows unix wildcard patterns). 57 | -l Display running processes. 58 | -d DEVICE Select a device by ID. Specify `list` to get a list of 59 | available devices. 60 | -m MOD_NAMES Specify zero or more modules that need to be loaded in the 61 | target process. 62 | ``` 63 | 64 | ### Attaching to a process by pid 65 | 66 | ``` 67 | $ memrepl -p 39718 68 | Using device Device(id="local", name="Local System", type='local'). 69 | Attaching to process pid `39718`. 70 | Attaching to process `39718`. 71 | 72 | ____________________ 73 | < Welcome to MemREPL > 74 | -------------------- 75 | \ ^__^ 76 | \ (oo)\_______ 77 | (__)\ )\/\ 78 | ||----w | 79 | || || 80 | 81 | Avaliable commands: 82 | 83 | memory_list: list memory regions in the attached program 84 | memory_search: search for a given value 85 | memory_read: read from a given address 86 | memory_write: write to a given address 87 | 88 | Use help(command_name) to see how to use the command. 89 | 90 | 91 | In [1]: 92 | ``` 93 | 94 | ## Getting help while on the REPL loop 95 | 96 | Each exported function has a help message defined that can be read by using python's `help` function. Each help messages contains usage examples. 97 | 98 | ``` 99 | In [10]: help(memory_read) 100 | Help on function memory_read in module memrepl: 101 | 102 | memory_read(value_format, address, size=32) 103 | Examples: 104 | memory_read("u8", 0xcafecafe) 105 | memory_read("u16", 0xcafecafe) 106 | memory_read("u32", 0xcafecafe) 107 | memory_read("u64", 0xcafecafe) 108 | memory_read("hex", 0xcafecafe, 4) 109 | memory_read("bytes", 0xcafecafe, 4) 110 | memory_read("BBII", 0xcafecafe) 111 | ``` 112 | 113 | ## Listing memory 114 | 115 | **Exported function signature:** `memory_list(protection="---")` 116 | 117 | ### Listing all segments 118 | To list all the segments present in the target process use the `memory_list` function without an argument: 119 | ``` 120 | In [5]: memory_list() 121 | 0: 0x000000010a4f5000 - 0x000000010a4f6000 ( 4096 / 0x00001000) next=0x0000000000000000 r-x 122 | 1: 0x000000010a4f6000 - 0x000000010a4f7000 ( 4096 / 0x00001000) next=0x0000000000000000 rw- 123 | 2: 0x000000010a4f7000 - 0x000000010a4fa000 ( 12288 / 0x00003000) next=0x0000000000000000 r-- 124 | 3: 0x000000010a4fa000 - 0x000000010a4fc000 ( 8192 / 0x00002000) next=0x0000000000000000 rw- 125 | ... 126 | ``` 127 | 128 | `memory_list` allows a `permission` agument that serves as a match filter, allowing the researcher to filter those segments he is interested in. For instance: 129 | 130 | ### Executable segments 131 | ``` 132 | In [7]: memory_list("x") 133 | 0: 0x000000010a4f5000 - 0x000000010a4f6000 ( 4096 / 0x00001000) next=0x0000000000007000 r-x 134 | 1: 0x000000010a4fd000 - 0x000000010a4fe000 ( 4096 / 0x00001000) next=0x000000000000a000 r-x 135 | 2: 0x000000010a508000 - 0x000000010a738000 ( 2293760 / 0x00230000) next=0x0000000000037000 r-x 136 | 3: 0x000000010a76f000 - 0x000000010a78c000 ( 118784 / 0x0001d000) next=0x0000000000091000 r-x 137 | ... 138 | ``` 139 | 140 | ### RWX segments 141 | 142 | ``` 143 | In [8]: memory_list("rwx") 144 | 0: 0x00007fffe8dac000 - 0x00007fffe8dad000 ( 4096 / 0x00001000) next=0x000000000001c000 rwx /private/var/db/dyld/dyld_shared_cache_x86_64h 145 | 1: 0x00007fffe8dc9000 - 0x00007fffe8dca000 ( 4096 / 0x00001000) next=0x00000000000bc000 rwx /private/var/db/dyld/dyld_shared_cache_x86_64h 146 | 2: 0x00007fffe8e86000 - 0x00007fffe8e87000 ( 4096 / 0x00001000) next=0x0000000000000000 rwx /private/var/db/dyld/dyld_shared_cache_x86_64h 147 | ``` 148 | 149 | ## Searching memory 150 | 151 | **Exported function signature:** `memory_search(value_format, value, out_format="hex", out_size=32)` 152 | 153 | ### Example search expressions 154 | 155 | ``` 156 | memory_search("u8", 0xca) 157 | memory_search("u16", 0xcafe) 158 | memory_search("u32", 0xcafedead) 159 | memory_search("u64", 0xcafecafecafecafe) 160 | memory_search("hex", "ca fe ca fe") 161 | memory_search("bytes", "\xca\xfe\xca\xfe") 162 | ``` 163 | 164 | ### Example search 165 | 166 | ``` 167 | # Search for the string "CAFE" repeated 8 times. 168 | In [12]: memory_search("bytes", "CAFE" * 8) 169 | Found @ 0x000026412eceeeb0 170 | 00000000: 43 41 46 45 43 41 46 45 43 41 46 45 43 41 46 45 CAFECAFECAFECAFE 171 | 00000010: 43 41 46 45 43 41 46 45 43 41 46 45 43 41 46 45 CAFECAFECAFECAFE 172 | ... 173 | Got 203 results. 174 | 175 | # Search for a pointer to the found string. 176 | In [13]: string_address = 0x000026412eceeeb0 177 | In [14]: memory_search("u64", string_address) 178 | Found @ 0x0000000115f1b6d8 179 | 00000000: B0 EE CE 2E 41 26 00 00 50 01 00 00 E5 E5 E5 E5 ....A&..P....... 180 | 00000010: B8 2B 7C 19 01 00 00 00 00 14 A1 2E 41 26 00 00 .+|.........A&.. 181 | ``` 182 | 183 | ## Reading memory 184 | 185 | **Exported function signature:** `memory_read(value_format, address, size=32)` 186 | 187 | ``` 188 | # Reading possible object that points to our address. 189 | In [15]: object_address = 0x0000000115f1b6d8 190 | 191 | # Read a couple QWORDs before the object to see whats there. 192 | In [16]: memory_read("hex", object_address - 8 * 4) 193 | Read @ 0x0000000115f1b6b8 194 | 00000000: B8 2B 7C 19 01 00 00 00 40 14 A1 2E 41 26 00 00 .+|.....@...A&.. 195 | 00000010: 40 00 00 00 E5 E5 E5 E5 B8 2B 7C 19 01 00 00 00 @........+|..... 196 | 197 | # Looks like the format is pointer|pointer|uint32|uint32|pointer 198 | In [17]: memory_read("PPIIP", object_address - 8 * 4) 199 | Read @ 0x0000000115f1b6b8 200 | 0x00000001197c2bb8 0x000026412ea11440 0x00000040 0xe5e5e5e5 0x00000001197c2bb8 201 | ``` 202 | 203 | ## Searching for pointers 204 | 205 | **Exported function signature:** `memory_search_pointer(address, protection)` 206 | 207 | The main usage of this function is to search for things to overwrite. Basically one can search for pointers to things that may be useful while exploiting bugs. Two cases come to mind: 208 | 209 | - Pointers to data (to create infoleaks) 210 | - Pointers to code (to get code execution) 211 | 212 | ### Example: looking for the position of a function pointer to overwrite. 213 | 214 | ``` 215 | In [18]: memory_search_pointer(object_address, "x") 216 | Found pointer @ 0x0000000115f36e48 = 0x00000001192bfde8 to segment 0x0000000117cbf000 - 0x0000000119598000 r-x 217 | 218 | In [19]: 0x0000000115f36e48 - object_address 219 | Out[19]: 112496 220 | 221 | In [20]: function_pointer_address = 0x0000000115f36e48 222 | ``` 223 | 224 | ## Writing memory 225 | 226 | **Exported function signature:** `memory_write(value_format, address, value)` 227 | 228 | ``` 229 | In [21]: memory_write("u64", function_pointer_address, 0xdeadbeef) 230 | 231 | # In another console with `lldb` attached: 232 | (lldb) c 233 | Process 39718 resuming 234 | Process 39718 stopped 235 | * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0xdeadbeef) 236 | 237 | (lldb) register read rip 238 | rip = 0x00000000deadbeef 239 | ``` -------------------------------------------------------------------------------- /memrepl.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import struct 3 | import fnmatch 4 | import binascii 5 | import argparse 6 | import collections 7 | 8 | import frida 9 | import hexdump 10 | import IPython 11 | 12 | parser = argparse.ArgumentParser(description="Memory Grip.") 13 | 14 | # Show the program version. 15 | parser.add_argument('-V', '--version', action="version", 16 | version="%(prog)s 0.1") 17 | 18 | # Required, mutually exclusive options. 19 | group = parser.add_mutually_exclusive_group(required=True) 20 | 21 | # Specify the process to which we will attach by PID. 22 | group.add_argument("-p", action="store", 23 | dest="proc_pid", type=int, help="Process PID.") 24 | 25 | # Specify the process to which we will attach by NAME. 26 | group.add_argument("-n", action="store", dest="proc_name", 27 | help="Process name (follows unix wildcard patterns).") 28 | 29 | # List running processes. 30 | group.add_argument("-l", action="store_true", 31 | dest="show_processes", help="Display running processes.") 32 | 33 | # Add an option to select a device. 34 | parser.add_argument("-d", action="store", dest="device", default="local", 35 | help="Select a device by ID. Specify `list` to get a list of available devices.") 36 | 37 | # Specify zero or mode modules. 38 | parser.add_argument("-m", action="append", dest="mod_names", default=[], 39 | help="Specify zero or more modules that need to be loaded in the target process.") 40 | 41 | # Parse command line arguments. 42 | args = parser.parse_args() 43 | 44 | # Show available devices. 45 | if args.device == "list": 46 | print "Available devices:" 47 | print " %-10s %s" % ("ID", "Name") 48 | for device in frida.enumerate_devices(): 49 | print " %-10s %s" % (device.id, device.name) 50 | 51 | sys.exit() 52 | 53 | # Lookup the desired device. 54 | if args.device: 55 | devs = [dev.id for dev in frida.enumerate_devices()] 56 | if args.device not in devs: 57 | print "Invalid device id `%s`." % args.device 58 | sys.exit(-1) 59 | 60 | # Get the device. 61 | device = frida.get_device(args.device) 62 | 63 | print "Using device %r." % device 64 | 65 | # Show processes. 66 | if args.show_processes: 67 | # Enumerate process and sort them by pid in ascending order. 68 | processes = sorted(device.enumerate_processes(), reverse=True) 69 | 70 | # Show a tabel with the devices processes. 71 | print "Local processes list:" 72 | print " %-6s %s" % ("PID", "Name") 73 | for process in processes: 74 | print " %-6d %s" % (process.pid, process.name) 75 | 76 | sys.exit() 77 | 78 | # Select the correct process to attach. 79 | if args.proc_pid: 80 | print "Attaching to process pid `%d`." % args.proc_pid 81 | target_process = args.proc_pid 82 | 83 | elif args.proc_name: 84 | # Get the list of local processes. 85 | processes = sorted(device.enumerate_processes(), reverse=True) 86 | 87 | # Filter processes that match our name. 88 | processes = [proc for proc in processes if fnmatch.fnmatch( 89 | proc.name, args.proc_name)] 90 | 91 | # Process name does not match any running processes. 92 | if len(processes) == 0: 93 | print "Invalid process name `%s`." % args.proc_name 94 | sys.exit(-1) 95 | 96 | # More than one process is available. 97 | if len(processes) > 1: 98 | print "Multiple processes (%d) available." % len(processes) 99 | 100 | # Found a single module to attach to. 101 | found = False 102 | 103 | # Find which module 104 | for proc in processes: 105 | if not args.mod_names: 106 | break 107 | 108 | # Temporarily attach to the process to get a module list. 109 | session = frida.attach(proc.pid) 110 | 111 | # Search if one of the specified modules is loaded in the target. 112 | modules = [str(module.name) for module in session.enumerate_modules()] 113 | if any(mod_name in modules for mod_name in args.mod_names): 114 | print "Process `%s:%d` matches module list." % (proc.name, proc.pid) 115 | target_process = proc.pid 116 | found = True 117 | break 118 | 119 | session.detach() 120 | 121 | if not found: 122 | proc = processes[0] 123 | print "Defaulting to first process `%s:%d`." % (proc.name, proc.pid) 124 | target_process = proc.pid 125 | 126 | else: 127 | print "I need either a PID or a process name." 128 | parser.print_usage() 129 | sys.exit(-1) 130 | 131 | 132 | def string_to_int(value): 133 | try: 134 | ret = int(value) 135 | 136 | except ValueError: 137 | ret = int(value, 16) 138 | 139 | return ret 140 | 141 | 142 | def string_to_hex(value): 143 | # Convert to hex form. 144 | value = binascii.hexlify(value) 145 | return " ".join(value[i:i + 2] for i in range(0, len(value), 2)) 146 | 147 | 148 | def format_size(format, size=-1): 149 | if format == "u8": 150 | return 1 151 | elif format == "u16": 152 | return 2 153 | elif format == "u32": 154 | return 4 155 | elif format == "u64": 156 | return 8 157 | elif format in ["hex", "bytes"]: 158 | return size 159 | 160 | return struct.calcsize(format) 161 | 162 | 163 | def format_value(format, value): 164 | if format == "u8": 165 | return struct.pack("B", value) 166 | elif format == "u16": 167 | return struct.pack("H", value) 168 | elif format == "u32": 169 | return struct.pack("I", value) 170 | elif format == "u64": 171 | return struct.pack("Q", value) 172 | elif format == "hex": 173 | return binascii.unhexlify(value.replace(" ", "")) 174 | 175 | return value 176 | 177 | 178 | def format_string(data, format): 179 | if format == "hex": 180 | return hexdump.hexdump(data, result="return") 181 | 182 | elif format == "u8": 183 | format = "B" 184 | 185 | elif format == "u16": 186 | format = "H" 187 | 188 | elif format == "u32": 189 | format = "I" 190 | 191 | elif format == "u64": 192 | format = "Q" 193 | 194 | out = [] 195 | unpacked_data = struct.unpack(format, data) 196 | for d, f in zip(unpacked_data, format): 197 | size = struct.calcsize(f) 198 | if isinstance(d, int) or isinstance(d, long): 199 | if size == 1: 200 | out.append("0x%.2x" % d) 201 | elif size == 2: 202 | out.append("0x%.4x" % d) 203 | elif size == 4: 204 | out.append("0x%.8x" % d) 205 | elif size == 8: 206 | out.append("0x%.16x" % d) 207 | 208 | else: 209 | out.append(str(d)) 210 | 211 | return " ".join(out) 212 | 213 | 214 | script_code = """ 215 | 'use strict'; 216 | 217 | function searchMemory(pattern) { 218 | var results = []; 219 | var ranges = Process.enumerateRangesSync({ protection: 'rw-', coalesce: true }); 220 | for (var i = 0; i < ranges.length; i++) { 221 | var range = ranges[i]; 222 | var matches = Memory.scanSync(range.base, range.size, pattern); 223 | for (var r = 0; r < matches.length; r++) { 224 | results.push(matches[r].address); 225 | } 226 | } 227 | 228 | return results; 229 | } 230 | 231 | function readMemory(address, size) { 232 | return Memory.readByteArray(ptr(address), size); 233 | } 234 | 235 | function writeMemory(address, value) { 236 | Memory.writeByteArray(ptr(address), value) 237 | } 238 | 239 | function listMemory(protection) { 240 | return Process.enumerateRangesSync({ 241 | protection: protection, 242 | coalesce: true 243 | }); 244 | } 245 | 246 | rpc.exports = { 247 | searchMemory: searchMemory, 248 | readMemory: readMemory, 249 | writeMemory: writeMemory, 250 | listMemory: listMemory 251 | }; 252 | """ 253 | 254 | __banner__ = """ 255 | ____________________ 256 | < Welcome to MemREPL > 257 | -------------------- 258 | \\ ^__^ 259 | \\ (oo)\\_______ 260 | (__)\\ )\\/\\ 261 | ||----w | 262 | || || 263 | """ 264 | 265 | __header__ = "Avaliable commands:\n\n" 266 | __header__ += "\n".join([ 267 | "memory_list: list memory regions in the attached program", 268 | "memory_search: search for a given value", 269 | "memory_read: read from a given address", 270 | "memory_write: write to a given address" 271 | ]) 272 | 273 | __header__ += "\n\nUse help(command_name) to see how to use the command.\n" 274 | 275 | 276 | class MemoryGrip: 277 | def __init__(self, target_process): 278 | # Attach to the target process. 279 | self.session = frida.attach(target_process) 280 | 281 | # Load the script in the target process. 282 | self.script = self.session.create_script(script_code) 283 | self.script.load() 284 | 285 | def memory_list(self, protection): 286 | def convert(segment): 287 | out = {} 288 | out["start"] = string_to_int(segment["base"]) 289 | out["size"] = segment["size"] 290 | out["end"] = out["start"] + out["size"] 291 | out["protection"] = segment["protection"] 292 | 293 | try: 294 | out["filename"] = segment["file"]["path"] 295 | 296 | except KeyError: 297 | out["filename"] = "-" 298 | 299 | return out 300 | 301 | return map(convert, self.script.exports.list_memory(protection)) 302 | 303 | def memory_search(self, value): 304 | # Frida expects the values to be in "hex" format. 305 | value = string_to_hex(value) 306 | return map(string_to_int, self.script.exports.search_memory(value)) 307 | 308 | def memory_read(self, address, size): 309 | return self.script.exports.read_memory(address, size) 310 | 311 | def memory_write(self, address, value): 312 | value = map(ord, list(value)) 313 | return self.script.exports.write_memory(address, value) 314 | 315 | def run(self): 316 | IPython.embed(header=__header__, banner1=__banner__) 317 | 318 | # Detach from the process. 319 | print "Detaching from the target process." 320 | self.session.detach() 321 | return 322 | 323 | 324 | # Global instance of our class. 325 | memory_grip = None 326 | 327 | 328 | def memory_list(protection="---"): 329 | """ 330 | 331 | """ 332 | global memory_grip 333 | 334 | results = memory_grip.memory_list(protection) 335 | n = len(str(len(results))) 336 | for i, result in enumerate(results): 337 | start = result["start"] 338 | size = result["size"] 339 | end = result["end"] 340 | prot = result["protection"] 341 | filename = result["filename"] 342 | 343 | try: 344 | next_result = results[i + 1] 345 | next_start = next_result["start"] 346 | 347 | except IndexError: 348 | next_start = end 349 | 350 | # Calculate the gap 351 | gap = next_start - end 352 | 353 | prefix = "{i:{width}d}:".format(width=n, i=i) 354 | 355 | print "%s 0x%.16x - 0x%.16x (%10u / 0x%.8x) next=0x%.16x %3s %s " % ( 356 | prefix, start, end, size, size, gap, prot, filename 357 | ) 358 | 359 | print "Got %u results." % len(results) 360 | 361 | 362 | def memory_search(value_format, value, out_format="hex", out_size=32): 363 | """ 364 | Examples: 365 | memory_search("u8", 0xca) 366 | memory_search("u16", 0xcafe) 367 | memory_search("u32", 0xcafedead) 368 | memory_search("u64", 0xcafecafecafecafe) 369 | memory_search("hex", "ca fe ca fe") 370 | memory_search("bytes", "\xca\xfe\xca\xfe") 371 | """ 372 | global memory_grip 373 | 374 | # Convert the value to the right representation. 375 | value = format_value(value_format, value) 376 | results = memory_grip.memory_search(value) 377 | 378 | # Calculate the number of bytes we need to represent the output. 379 | size = format_size(out_format, out_size) 380 | 381 | # Collect results offsets. 382 | results_offsets = [] 383 | 384 | # For each `result`, dump with the given format. 385 | for i, result in enumerate(results): 386 | try: 387 | next_result_offset = results[i + 1] - result 388 | results_offsets.append(next_result_offset) 389 | 390 | except IndexError: 391 | next_result_offset = 0 392 | 393 | # Read `size` bytes from `result` address. 394 | data = memory_grip.memory_read(result, size) 395 | print "Address=0x%.16x next_result_offset=0x%.8x" % (result, next_result_offset) 396 | print format_string(data, out_format) 397 | print 398 | 399 | print "Got %u results." % len(results) 400 | 401 | print "More common results deltas:" 402 | for offset, count in collections.Counter(results_offsets).most_common(8): 403 | if count <= 1: 404 | break 405 | 406 | print " offset=0x%.8x count=%u" % (offset, count) 407 | 408 | 409 | def memory_read(value_format, address, size=32, count=1): 410 | """ 411 | Examples: 412 | memory_read("u8", 0xcafecafe) 413 | memory_read("u16", 0xcafecafe) 414 | memory_read("u32", 0xcafecafe) 415 | memory_read("u64", 0xcafecafe) 416 | memory_read("hex", 0xcafecafe, 4) 417 | memory_read("bytes", 0xcafecafe, 4) 418 | memory_read("BBII", 0xcafecafe) 419 | """ 420 | global memory_grip 421 | 422 | # Calculate the size of the read based on the format string. 423 | size = format_size(value_format, size) 424 | for i in xrange(0, count): 425 | caddr = address + (i * size) 426 | data = memory_grip.memory_read(caddr, size) 427 | print "Read @ 0x%.16x:\n%s" % (caddr, format_string(data, value_format)) 428 | 429 | 430 | def memory_write(value_format, address, value, count=1): 431 | """ 432 | Examples: 433 | memory_write("u8", 0xdeadbeef, 0xca) 434 | memory_write("u16", 0xdeadbeef, 0xcafe) 435 | memory_write("u32", 0xdeadbeef, 0xcafecafe) 436 | memory_write("u64", 0xdeadbeef, 0xcafecafecafecafe) 437 | memory_write("hex", 0xdeadbeef, "ca fe ca fe") 438 | memory_write("bytes", 0xdeadbeef, "\xca\xfe\xca\xfe") 439 | """ 440 | global memory_grip 441 | 442 | value = format_value(value_format, value) 443 | size = len(value) 444 | for i in xrange(0, count): 445 | caddr = address + (i * size) 446 | memory_grip.memory_write(caddr, value) 447 | 448 | 449 | def memory_search_pointer(start_address, protection): 450 | """ 451 | Start a search from `start_address` looking for pointers to segments with 452 | `permission`. The search will stop at the end of the segment. 453 | 454 | Searching for function pointers: 455 | 456 | memory_search_pointer(valid_address, "x") 457 | """ 458 | def compare_protection(p1, p2): 459 | """ 460 | Check that `p1` contains `p2`. 461 | """ 462 | p1 = p1.replace("-", "") 463 | p2 = p2.replace("-", "") 464 | return set(p2) <= set(p1) 465 | 466 | def get_segment(segments, address): 467 | """ 468 | Get the segment that contains `address`. 469 | """ 470 | for segment in segments: 471 | if address >= segment["start"] and address < segment["end"]: 472 | return segment 473 | 474 | return None 475 | 476 | # Get all the segments. 477 | segments = memory_grip.memory_list("") 478 | 479 | # Find the segment that contains `start_address`. 480 | selected_segment = get_segment(segments, start_address) 481 | if not selected_segment: 482 | print "No valid segment was found." 483 | return 484 | 485 | print "Working on segment %r" % selected_segment 486 | 487 | # Filter target segments. 488 | segments = [segment for segment in segments if compare_protection( 489 | segment["protection"], protection)] 490 | 491 | # Read segments data and break it into aligned pointers. 492 | data = memory_grip.memory_read( 493 | selected_segment["start"], selected_segment["size"]) 494 | pointer_size = struct.calcsize("P") 495 | fmt = "P" * (len(data) / pointer_size) 496 | pointers = struct.unpack(fmt, data) 497 | 498 | # For each pointer, get its segment. 499 | ret = [] 500 | for i, pointer in enumerate(pointers): 501 | segment = get_segment(segments, pointer) 502 | if segment: 503 | address = selected_segment["start"] + (i * pointer_size) 504 | ret.append((address, pointer, segment)) 505 | 506 | for address, pointer, segment in ret: 507 | if address < start_address: 508 | continue 509 | 510 | print "Found pointer @ 0x%.16x = 0x%.16x to segment 0x%.16x - 0x%.16x %3s %s" % ( 511 | address, pointer, segment["start"], segment["end"], segment["protection"], segment["filename"] 512 | ) 513 | 514 | 515 | def main(): 516 | global memory_grip 517 | 518 | # Attach to the target process and enter the REPL. 519 | print "Attaching to process `%d`." % target_process 520 | memory_grip = MemoryGrip(target_process) 521 | memory_grip.run() 522 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="memrepl", 5 | version="1.0", 6 | url="https://github.com/agustingianni/memrepl", 7 | author="Agustin Gianni", 8 | author_email="agustin.gianni@gmail.com", 9 | description=("Memory inspection REPL interface"), 10 | license="MIT", 11 | keywords="memory debugger repl reverse engineering", 12 | py_modules=["memrepl"], 13 | install_requires=[ 14 | "frida", 15 | "ipython<6.0", # iPython 6 needs python 3. 16 | "hexdump" 17 | ], 18 | entry_points=""" 19 | [console_scripts] 20 | memrepl=memrepl:main 21 | """ 22 | ) 23 | --------------------------------------------------------------------------------