diff --git a/README.md b/README.md index b97fdf1..08d07ff 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,86 @@ -# fridump3 -Fridump is an open source memory dumping tool, primarily aimed to penetration testers and developers. Fridump is using the Frida framework to dump accessible memory addresses from any platform supported. It can be used from a Windows, Linux or Mac OS X system to dump the memory of an iOS, Android or Windows application. +# Fridump 1.5 -This project is based on the following project: [https://github.com/Nightbringer21/fridump](https://github.com/Nightbringer21/fridump) and the pending PR concerning the python3 support (especially from [georgepetz](https://github.com/georgepetz) . Additionally I added the network support in addition to the USB support. +basically I just fixed some syntax errors and changed the print statements and progress bars to make them cleaner and more in line with python3 -FYI: I will destroy this repo is the Fridump author will integrate the pending PR concerning Python3 support. +![a screen capture of the revamped tool](capture.gif "Capture") + +![the helptext](help_text.png "Helptext") + + +--- + +# Fridump +Fridump (v0.1) is an open source memory dumping tool, primarily aimed to penetration testers and developers. Fridump is using the Frida framework to dump accessible memory addresses from any platform supported. It can be used from a Windows, Linux or Mac OS X system to dump the memory of an iOS, Android or Windows application. Usage --- -``` -usage: fridump [-h] [-o dir] [-u] [-H HOST] [-v] [-r] [-s] [--max-size bytes] process - -positional arguments: - process the process that you will be injecting to - -optional arguments: - -h, --help show this help message and exit - -o dir, --out dir provide full output directory path. (def: 'dump') - -u, --usb device connected over usb - -H HOST, --host HOST device connected over IP - -v, --verbose verbose - -r, --read-only dump read-only parts of memory. More data, more errors - -s, --strings run strings on all dump files. Saved in output dir. - --max-size bytes maximum size of dump file in bytes (def: 20971520) -``` +How to: + + fridump [-h] [-o dir] [-U] [-v] [-r] [-s] [--max-size bytes] process + +The following are the main flags that can be used with fridump: + + positional arguments: + process the process that you will be injecting to + + optional arguments: + -h, --help show this help message and exit + -o dir, --out dir provide full output directory path. (def: 'dump') + -U, --usb device connected over usb + -v, --verbose verbose + -r, --read-only dump read-only parts of memory. More data, more errors + -s, --strings run strings on all dump files. Saved in output dir. + --max-size bytes maximum size of dump file in bytes (def: 20971520) + +To find the name of a local process, you can use: + + frida-ps +For a process that is running on a USB connected device, you can use: + + frida-ps -U + +Examples: + + fridump -U Safari - Dump the memory of an iOS device associated with the Safari app + fridump -U -s com.example.WebApp - Dump the memory of an Android device and run strings on all dump files + fridump -r -o [full_path] - Dump the memory of a local application and save it to the specified directory + +More examples can be found [here](http://pentestcorner.com/introduction-to-fridump/) + +Installation +--- +To install Fridump you just need to clone it from git and run it: + + git clone https://github.com/Nightbringer21/fridump.git + + python fridump.py -h + +Pre-requisites +--- +To use fridump you need to have frida installed on your python environment and frida-server on the device you are trying to dump the memory from. +The easiest way to install frida on your python is using pip: + + pip install frida + +More information on how to install Frida can be found [here](http://www.frida.re/docs/installation/) + +For iOS, installation instructions can be found [here](http://www.frida.re/docs/ios/). + +For Android, installation instructions can be found [here](http://www.frida.re/docs/android/). + +Note: On Android devices, make sure that the frida-server binary is running as root! + +Disclaimer +--- +* This is version 0.1 of the software, so I expect some bugs to be present +* I am not a developer, so my coding skills might not be the best + +This tool has been tested on a Windows 7 and a Mac OS X laptop, dumping the memory of: +* an iPad Air 2 running iOS 8.2 +* a Galaxy Tab running Cyanogenmod 4.4.4 +* a Windows 7 laptop. + +Therefore, if this tool is not working for you, I apologise and I will try to fix it. + +Any suggestions and comments are welcome! diff --git a/__pycache__/dumper.cpython-312.pyc b/__pycache__/dumper.cpython-312.pyc new file mode 100644 index 0000000..bedac44 Binary files /dev/null and b/__pycache__/dumper.cpython-312.pyc differ diff --git a/__pycache__/utils.cpython-312.pyc b/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..86438a4 Binary files /dev/null and b/__pycache__/utils.cpython-312.pyc differ diff --git a/capture.gif b/capture.gif new file mode 100644 index 0000000..1f344c2 Binary files /dev/null and b/capture.gif differ diff --git a/dumper.py b/dumper.py index ced3637..cc1bbe5 100644 --- a/dumper.py +++ b/dumper.py @@ -3,24 +3,23 @@ # Reading bytes from session and saving it to a file - -def dump_to_file(agent, base, size, error, directory): +def dump_to_file(agent,base,size,error,directory): try: - filename = str(base) + '_dump.data' - dump = agent.read_memory(base, size) - f = open(os.path.join(directory, filename), 'wb') + filename = str(base)+'_dump.data' + dump = agent.read_memory(base, size) + f = open(os.path.join(directory,filename), 'wb') f.write(dump) f.close() return error except Exception as e: - logging.debug(str(e)) - print("Oops, memory access violation!") - return error + logging.debug("[!]"+str(e)) + logging.debug("Memory access violation") + return error -# Read bytes that are bigger than the max_size value, split them into chunks and save them to a file +#Read bytes that are bigger than the max_size value, split them into chunks and save them to a file def splitter(agent,base,size,max_size,error,directory): - times = size//max_size + times = size/max_size diff = size % max_size if diff == 0: logging.debug("Number of chunks:"+str(times+1)) @@ -29,11 +28,12 @@ def splitter(agent,base,size,max_size,error,directory): global cur_base cur_base = int(base,0) - for time in range(times): - # logging.debug("Save bytes: "+str(cur_base)+" till "+str(hex(cur_base+max_size))) + for time in range(int(times)): + logging.debug("Save bytes: "+str(cur_base)+" till "+str(cur_base+max_size)) dump_to_file(agent, cur_base, max_size, error, directory) cur_base = cur_base + max_size if diff != 0: - # logging.debug("Save bytes: "+str(hex(cur_base))+" till "+str(hex(cur_base+diff))) + logging.debug("Save bytes: "+str(hex(cur_base))+" till "+str(hex(cur_base+diff))) dump_to_file(agent, cur_base, diff, error, directory) + diff --git a/fridump3.py b/fridump3.py index f113090..3ef3de1 100644 --- a/fridump3.py +++ b/fridump3.py @@ -7,69 +7,86 @@ import utils import argparse import logging - -logo = """ - ______ _ _ - | ___| (_) | | - | |_ _ __ _ __| |_ _ _ __ ___ _ __ - | _| '__| |/ _` | | | | '_ ` _ \| '_ \\ - | | | | | | (_| | |_| | | | | | | |_) | - \_| |_| |_|\__,_|\__,_|_| |_| |_| .__/ - | | - |_| +from rich_argparse import ArgumentDefaultsRichHelpFormatter +from rich.progress import track +from rich.logging import RichHandler +from rich.console import Console +from rich.text import Text +import numpy as np +import matplotlib, re + +console = Console() +cmap = matplotlib.colormaps["rainbow_r"] +logo = r""" + ______ _ _ __ _____ + | ____| (_) | | /_ | | ____| + | |__ _ __ _ __| |_ _ _ __ ___ _ __ | | | |__ + | __| '__| |/ _` | | | | '_ ` _ \| '_ \| | |___ \ + | | | | | | (_| | |_| | | | | | | |_) | |_ ___) | + |_| |_| |_|\__,_|\__,_|_| |_| |_| .__/|_(_)____/ + | | + |_| """ +length = max([len(a) for a in logo.split("\n")]) +# print(length) +gradient = np.linspace(0, 1, length) + +newbanner = Text("") +for line in [a for a in logo.split("\n") if a!=""]: + newline = Text("") + for (i,chr) in enumerate([a for a in re.split(r"(.)", line) if a!='']): + colorhex = matplotlib.colors.rgb2hex(cmap(gradient[i])) + newline.append(f"{chr}", style=f"{colorhex}") + newbanner.append(newline) + newbanner.append(Text("\n")) + +console = Console(color_system="truecolor") # Main Menu def MENU(): parser = argparse.ArgumentParser( prog='fridump', - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=ArgumentDefaultsRichHelpFormatter, description=textwrap.dedent("")) - parser.add_argument( - 'process', help='the process that you will be injecting to') - parser.add_argument('-o', '--out', type=str, help='provide full output directory path. (def: \'dump\')', - metavar="dir") - parser.add_argument('-u', '--usb', action='store_true', + parser.add_argument('process', + help='the process that you will be injecting to') + parser.add_argument('-o', '--out', type=str, metavar="dir", + help='provide full output directory path. (def: \'dump\')') + parser.add_argument('-U', '--usb', action='store_true', help='device connected over usb') - parser.add_argument('-H', '--host', type=str, - help='device connected over IP') - parser.add_argument('-v', '--verbose', action='store_true', help='verbose') + parser.add_argument('-v', '--verbose', action='store_true', + help='verbose') parser.add_argument('-r', '--read-only', action='store_true', help="dump read-only parts of memory. More data, more errors") parser.add_argument('-s', '--strings', action='store_true', help='run strings on all dump files. Saved in output dir.') - parser.add_argument('--max-size', type=int, help='maximum size of dump file in bytes (def: 20971520)', - metavar="bytes") + parser.add_argument('--max-size', type=int, metavar="bytes", + help='maximum size of dump file in bytes (def: 20971520)') args = parser.parse_args() return args -print(logo) +console.print(newbanner) arguments = MENU() # Define Configurations -APP_NAME = utils.normalize_app_name(appName=arguments.process) +APP_NAME = arguments.process DIRECTORY = "" USB = arguments.usb -NETWORK=False DEBUG_LEVEL = logging.INFO STRINGS = arguments.strings MAX_SIZE = 20971520 PERMS = 'rw-' -if arguments.host is not None: - NETWORK=True - IP=arguments.host - if arguments.read_only: PERMS = 'r--' if arguments.verbose: DEBUG_LEVEL = logging.DEBUG -logging.basicConfig(format='%(levelname)s:%(message)s', level=DEBUG_LEVEL) +logging.basicConfig(format='%(message)s', level=DEBUG_LEVEL, handlers=[RichHandler(rich_tracebacks=True)]) # Start a new Session @@ -77,12 +94,11 @@ def MENU(): try: if USB: session = frida.get_usb_device().attach(APP_NAME) - elif NETWORK: - session = frida.get_device_manager().add_remote_device(IP).attach(APP_NAME) else: session = frida.attach(APP_NAME) except Exception as e: - print(str(e)) + logging.error("Can't connect to App. Have you connected the device?") + logging.debug(str(e)) sys.exit() @@ -90,39 +106,41 @@ def MENU(): if arguments.out is not None: DIRECTORY = arguments.out if os.path.isdir(DIRECTORY): - print("Output directory is set to: " + DIRECTORY) + logging.info("Output directory is set to: " + DIRECTORY) else: - print("The selected output directory does not exist!") + logging.error("The selected output directory does not exist!") sys.exit(1) else: - print("Current Directory: " + str(os.getcwd())) + logging.info("Current Directory: " + str(os.getcwd())) DIRECTORY = os.path.join(os.getcwd(), "dump") - print("Output directory is set to: " + DIRECTORY) + logging.info("Output directory is set to: " + DIRECTORY) if not os.path.exists(DIRECTORY): - print("Creating directory...") + logging.info("Creating directory...") os.makedirs(DIRECTORY) -mem_access_viol = "" +mem_access_viol = "cum" -print("Starting Memory dump...") +logging.info("Starting Memory dump...") -def on_message(message, data): - print("[on_message] message:", message, "data:", data) +script = session.create_script( + """'use strict'; + + rpc.exports = { + enumerateRanges: function (prot) { + return Process.enumerateRangesSync(prot); + }, + readMemory: function (address, size) { + return Memory.readByteArray(ptr(address), size); + } + }; + """) -script = session.create_script("""'use strict'; +def on_message(message, data): + logging.info(message) -rpc.exports = { - enumerateRanges: function (prot) { - return Process.enumerateRangesSync(prot); - }, - readMemory: function (address, size) { - return Memory.readByteArray(ptr(address), size); - } -}; -""") -script.on("message", on_message) +script.on('message', on_message) script.load() agent = script.exports_sync @@ -131,35 +149,37 @@ def on_message(message, data): if arguments.max_size is not None: MAX_SIZE = arguments.max_size -i = 0 -l = len(ranges) +# i = 0 +# l = len(ranges) # Performing the memory dump -for range in ranges: - logging.debug("Base Address: " + str(range["base"])) + +for range in track(ranges, description="[green]"+"Dumping memory...".ljust(32)): + base = range["base"] + size = range["size"] + + logging.debug("Base Address: " + str(base)) logging.debug("") - logging.debug("Size: " + str(range["size"])) - if range["size"] > MAX_SIZE: + logging.debug("Size: " + str(size)) + + + if size > MAX_SIZE: logging.debug("Too big, splitting the dump into chunks") mem_access_viol = dumper.splitter( - agent, range["base"], range["size"], MAX_SIZE, mem_access_viol, DIRECTORY) + agent, base, size, MAX_SIZE, mem_access_viol, DIRECTORY) continue mem_access_viol = dumper.dump_to_file( - agent, range["base"], range["size"], mem_access_viol, DIRECTORY) - i += 1 - utils.printProgress(i, l, prefix='Progress:', suffix='Complete', bar=50) + agent, base, size, mem_access_viol, DIRECTORY) + # Run Strings if selected if STRINGS: files = os.listdir(DIRECTORY) i = 0 l = len(files) - print("Running strings on all files:") - for f1 in files: + # print("Running strings on all files:") + for f1 in track(files, description="[green]"+"Running strings on all files...".ljust(32)): utils.strings(f1, DIRECTORY) - i += 1 - utils.printProgress(i, l, prefix='Progress:', - suffix='Complete', bar=50) -print("Finished!") -#raw_input('Press Enter to exit...') + +console.print("[green]:heavy_check_mark: Finished!") \ No newline at end of file diff --git a/help_text.png b/help_text.png new file mode 100644 index 0000000..ea5a0fc Binary files /dev/null and b/help_text.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..557e597 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +frida==16.3.3 +matplotlib==3.10.1 +numpy==2.2.4 +rich==13.9.4 +rich_argparse==1.7.0 diff --git a/utils.py b/utils.py index 13784fa..ddccbc9 100644 --- a/utils.py +++ b/utils.py @@ -3,16 +3,14 @@ import logging import os import re +from io import open # Progress bar function - - -def printProgress(times, total, prefix='', suffix='', decimals=2, bar=100): +def printProgress (times, total, prefix ='', suffix ='', decimals = 2, bar = 100): filled = int(round(bar * times / float(total))) percents = round(100.00 * (times / float(total)), decimals) bar = '#' * filled + '-' * (bar - filled) - sys.stdout.write('%s [%s] %s%s %s\r' % - (prefix, bar, percents, '%', suffix)), + sys.stdout.write('%s [%s] %s%s %s\r' % (prefix, bar, percents, '%', suffix)), sys.stdout.flush() if times == total: print("\n") @@ -23,17 +21,13 @@ def strings(filename, directory, min=4): strings_file = os.path.join(directory, "strings.txt") path = os.path.join(directory, filename) with open(path, encoding='Latin-1') as infile: - str_list = re.findall("[A-Za-z0-9/\-:;.,_$%'!()[\]<> \#]+", infile.read()) + str_list = re.findall("[\x20-\x7E]+\x00", infile.read()) with open(strings_file, "a") as st: for string in str_list: if len(string) > min: logging.debug(string) st.write(string + "\n") -# Normalize thw namo of application works better on frida -def normalize_app_name(appName=str): - try: - appName = int(appName) - except ValueError: - pass - return appName +# Method to receive messages from Javascript API calls +def on_message(message, data): + print("[on_message] message:", message, "data:", data)