From 4a6452a6b69f05418e035d5b11d180833a80da03 Mon Sep 17 00:00:00 2001 From: Xenorf <90411648+Xenorf@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:48:09 +0100 Subject: [PATCH 1/7] package reformat, better logging & error handling --- .gitignore | 174 ++++++++++++++++++++++++++++++ README.md | 30 ++++-- dumper.py | 39 ------- fridump3.py | 165 ----------------------------- fridump3/__main__.py | 12 +++ fridump3/console.py | 234 +++++++++++++++++++++++++++++++++++++++++ fridump3/src/dumper.py | 45 ++++++++ fridump3/src/utils.py | 34 ++++++ poetry.lock | 93 ++++++++++++++++ pyproject.toml | 21 ++++ utils.py | 39 ------- 11 files changed, 637 insertions(+), 249 deletions(-) create mode 100644 .gitignore delete mode 100644 dumper.py delete mode 100644 fridump3.py create mode 100644 fridump3/__main__.py create mode 100644 fridump3/console.py create mode 100644 fridump3/src/dumper.py create mode 100644 fridump3/src/utils.py create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a19790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc diff --git a/README.md b/README.md index b97fdf1..0cf9b67 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,44 @@ # 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. +## Fork information + 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. FYI: I will destroy this repo is the Fridump author will integrate the pending PR concerning Python3 support. -Usage +## Installation + +Simply run one of the following commands: +> :warning: pipx is recommended for system or user wide installations +``` +pipx install git+https://github.com/rootbsd/fridump3 +pip install git+https://github.com/rootbsd/fridump3 +``` + +## Usage --- ``` -usage: fridump [-h] [-o dir] [-u] [-H HOST] [-v] [-r] [-s] [--max-size bytes] process +usage: fridump3 [-h] [-o dir] [-u] [-H HOST] [-r] [-s] [--max-size bytes] + [--log-level {none,debug,info,warning,error,critical}] + [--log-filename LOG_FILENAME] + process positional arguments: process the process that you will be injecting to -optional arguments: +options: -h, --help show this help message and exit - -o dir, --out dir provide full output directory path. (def: 'dump') + -o, --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 + -H, --host HOST device connected over IP -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) + --log-level, -l {none,debug,info,warning,error,critical} + level of debug you wish to display. + --log-filename LOG_FILENAME + output file used to store logs ``` diff --git a/dumper.py b/dumper.py deleted file mode 100644 index ced3637..0000000 --- a/dumper.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import logging - -# Reading bytes from session and saving it to a file - - -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') - f.write(dump) - f.close() - return error - except Exception as e: - logging.debug(str(e)) - print("Oops, 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 - -def splitter(agent,base,size,max_size,error,directory): - times = size//max_size - diff = size % max_size - if diff == 0: - logging.debug("Number of chunks:"+str(times+1)) - else: - logging.debug("Number of chunks:"+str(times)) - 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))) - 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))) - dump_to_file(agent, cur_base, diff, error, directory) diff --git a/fridump3.py b/fridump3.py deleted file mode 100644 index f113090..0000000 --- a/fridump3.py +++ /dev/null @@ -1,165 +0,0 @@ -import textwrap -import frida -import os -import sys -import frida.core -import dumper -import utils -import argparse -import logging - -logo = """ - ______ _ _ - | ___| (_) | | - | |_ _ __ _ __| |_ _ _ __ ___ _ __ - | _| '__| |/ _` | | | | '_ ` _ \| '_ \\ - | | | | | | (_| | |_| | | | | | | |_) | - \_| |_| |_|\__,_|\__,_|_| |_| |_| .__/ - | | - |_| - """ - - -# Main Menu -def MENU(): - parser = argparse.ArgumentParser( - prog='fridump', - formatter_class=argparse.RawDescriptionHelpFormatter, - 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', - 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('-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") - args = parser.parse_args() - return args - - -print(logo) - -arguments = MENU() - -# Define Configurations -APP_NAME = utils.normalize_app_name(appName=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) - - -# Start a new Session -session = None -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)) - sys.exit() - - -# Selecting Output directory -if arguments.out is not None: - DIRECTORY = arguments.out - if os.path.isdir(DIRECTORY): - print("Output directory is set to: " + DIRECTORY) - else: - print("The selected output directory does not exist!") - sys.exit(1) - -else: - print("Current Directory: " + str(os.getcwd())) - DIRECTORY = os.path.join(os.getcwd(), "dump") - print("Output directory is set to: " + DIRECTORY) - if not os.path.exists(DIRECTORY): - print("Creating directory...") - os.makedirs(DIRECTORY) - -mem_access_viol = "" - -print("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.on("message", on_message) -script.load() - -agent = script.exports_sync -ranges = agent.enumerate_ranges(PERMS) - -if arguments.max_size is not None: - MAX_SIZE = arguments.max_size - -i = 0 -l = len(ranges) - -# Performing the memory dump -for range in ranges: - logging.debug("Base Address: " + str(range["base"])) - logging.debug("") - logging.debug("Size: " + str(range["size"])) - if range["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) - 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) - -# 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: - utils.strings(f1, DIRECTORY) - i += 1 - utils.printProgress(i, l, prefix='Progress:', - suffix='Complete', bar=50) -print("Finished!") -#raw_input('Press Enter to exit...') diff --git a/fridump3/__main__.py b/fridump3/__main__.py new file mode 100644 index 0000000..4dfcc87 --- /dev/null +++ b/fridump3/__main__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 + +# Local imports +from fridump3 import console + + +def main() -> None: + console.run() + + +if __name__ == "__main__": + main() diff --git a/fridump3/console.py b/fridump3/console.py new file mode 100644 index 0000000..5780c8d --- /dev/null +++ b/fridump3/console.py @@ -0,0 +1,234 @@ +import argparse +import sys +import textwrap +from pathlib import Path + +import frida +import frida.core +from loguru import logger + +from fridump3.src import dumper, utils + +logo = r""" + ______ _ _ + | ___| (_) | | + | |_ _ __ _ __| |_ _ _ __ ___ _ __ + | _| '__| |/ _` | | | | '_ ` _ \| '_ \\ + | | | | | | (_| | |_| | | | | | | |_) | + \_| |_| |_|\__,_|\__,_|_| |_| |_| .__/ + | | + |_| + """ + + +def set_log_level(level, output_file): + """Sets the log level and log file. + + Args: + level -- the log level to display (default "info") + output_file -- the file used to output logs, including commands + """ + if level.upper() != "NONE": + LOG_FORMAT = "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}" + logger.remove() # Remove any default handlers + logger.add(sys.stderr, format=LOG_FORMAT, level=level.upper()) + if output_file: + logger.debug(f"Initializing logfile") + try: + logger.add( + output_file, + rotation="10 MB", + retention="30 days", + level=level.upper(), + format=LOG_FORMAT, + ) + except PermissionError: + logger.critical( + f"Permission denied to write to {output_file}, continuing without logs" + ) + logger.debug(f"Logger initialized to {level.upper()}") + + +def MENU(): + parser = argparse.ArgumentParser( + prog="fridump3", + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(""), + ) + + parser.add_argument("process", help="name of the process to dump memory from") + parser.add_argument( + "-o", + "--out", + type=str, + help='provide full output directory path. (def: "dump").', + metavar="DUMP_DIRECTORY", + ) + 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( + "-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( + "--log-level", + "-l", + choices=["none", "debug", "info", "warning", "error", "critical"], + default="info", + help="level of debug you wish to display.", + ) + parser.add_argument( + "--log-filename", + default=None, + help="output file used to store logs.", + ) + args = parser.parse_args() + return args + + +def run() -> None: + print(logo) + + arguments = MENU() + + # Define configurations + APP_NAME = utils.normalize_app_name(appName=arguments.process) + DIRECTORY = "" + USB = arguments.usb + IP = None + STRINGS = arguments.strings + MAX_SIZE = 20971520 + PERMS = "rw-" + + if arguments.host is not None: + IP = arguments.host + + if arguments.read_only: + PERMS = "r--" + + set_log_level(arguments.log_level, arguments.log_filename) + + # Start a new session + session = None + try: + if USB: + session = frida.get_usb_device().attach(APP_NAME) + elif IP: + session = frida.get_device_manager().add_remote_device(IP).attach(APP_NAME) + else: + session = frida.attach(APP_NAME) + except Exception as e: + logger.error(str(e)) + sys.exit(1) + + # Selecting output directory + if arguments.out is not None: + DIRECTORY = Path(arguments.out) + if DIRECTORY.is_dir(): + logger.info(f"Output directory is set to: {DIRECTORY}") + else: + current_dir = Path.cwd() + logger.info(f"Current directory: {current_dir}") + DIRECTORY = current_dir / "dump" + logger.info(f"Output directory is set to: {DIRECTORY}") + + if not DIRECTORY.exists(): + logger.info(f"Creating directory {DIRECTORY}") + try: + DIRECTORY.mkdir(parents=True, exist_ok=True) + except PermissionError: + logger.error(f"Permission denied to create {DIRECTORY}") + sys.exit(1) + + mem_access_viol = "" + + logger.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.on("message", on_message) + script.load() + + agent = script.exports_sync + ranges = agent.enumerate_ranges(PERMS) + + if arguments.max_size is not None: + MAX_SIZE = arguments.max_size + + i = 0 + l = len(ranges) + + try: + # Performing the memory dump + for range in ranges: + logger.debug(f"Base address: {str(range["base"])}") + logger.debug(f"Size: {str(range["size"])}") + if range["size"] > MAX_SIZE: + logger.debug("Size is too big, splitting the dump into chunks") + mem_access_viol = dumper.splitter( + agent, + range["base"], + range["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) + print("") + + # Run Strings if selected + if STRINGS: + files = list( + DIRECTORY.iterdir() + ) # Get all items in the directory as Path objects + total_files = len(files) + + logger.info("Running strings on all files") + + for i, file in enumerate(files, start=1): + utils.strings(file.name, DIRECTORY) # Pass only the filename if needed + utils.printProgress( + i, total_files, prefix="Progress:", suffix="Complete", bar=50 + ) + print("") + except KeyboardInterrupt: + # Helps removing the current progress bar before printing the error message + print(" " * 80 + "\r", end="\n", flush=True) + logger.success(f"Exiting gracefully.") + sys.exit(0) + logger.success("Finished!") diff --git a/fridump3/src/dumper.py b/fridump3/src/dumper.py new file mode 100644 index 0000000..ad693d4 --- /dev/null +++ b/fridump3/src/dumper.py @@ -0,0 +1,45 @@ +import sys +from pathlib import Path + +from loguru import logger + + +def dump_to_file(agent, base, size, error, directory): + """Reading bytes from session and saving it to a file""" + try: + filename = str(base) + "_dump.data" + dump = agent.read_memory(base, size) + file_path = Path(directory) / filename + with file_path.open("wb") as f: + f.write(dump) + return error + except PermissionError: + logger.error(f"Permission denied to write to {directory}") + sys.exit(1) + except Exception as e: + # Helps removing the current progress bar before printing the error message + print(" " * 80 + "\r", end="", flush=True) + logger.debug(str(e)) + logger.warning("Oops, memory access violation!") + return error + + +def splitter(agent, base, size, max_size, error, directory): + """Read bytes that are bigger than the max_size value, split them into chunks and save them to a file""" + times = size // max_size + diff = size % max_size + if diff == 0: + logger.debug("Number of chunks:" + str(times + 1)) + else: + logger.debug("Number of chunks:" + str(times)) + global cur_base + cur_base = int(base, 0) + + for time in range(times): + logger.debug(f"Save bytes: {str(cur_base)} till {str(hex(cur_base+max_size))}") + dump_to_file(agent, cur_base, max_size, error, directory) + cur_base = cur_base + max_size + + if diff != 0: + logger.debug(f"Save bytes: {str(cur_base)} till {str(hex(cur_base+diff))}") + dump_to_file(agent, cur_base, diff, error, directory) diff --git a/fridump3/src/utils.py b/fridump3/src/utils.py new file mode 100644 index 0000000..f694603 --- /dev/null +++ b/fridump3/src/utils.py @@ -0,0 +1,34 @@ +import re +from pathlib import Path + +from loguru import logger + + +def printProgress(times, total, prefix="", suffix="", decimals=2, bar=100): + """Print a progress bar""" + filled = int(round(bar * times / float(total))) + percents = round(100.00 * (times / float(total)), decimals) + bar = "#" * filled + "-" * (bar - filled) + print(f"{prefix} [{bar}] {percents}% {suffix} ", end="\r", flush=True) + + +def strings(filename, directory, min=4): + """A very basic implementations of Strings""" + strings_file = Path(directory) / "strings.txt" + path = Path(directory) / filename + with open(path, encoding="Latin-1") as infile: + str_list = re.findall(r"[A-Za-z0-9/\-:;.,_$%'!()[\]<> \#]+", infile.read()) + with open(strings_file, "a") as st: + for string in str_list: + if len(string) > min: + logger.debug(string) + st.write(f"{string}\n") + + +def normalize_app_name(appName=str): + """Normalize the name of application works better on frida""" + try: + appName = int(appName) + except ValueError: + pass + return appName diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..bd5467c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,93 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "frida" +version = "16.6.6" +description = "Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "frida-16.6.6-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:b00a2b5f877cbb4955e72413c1f620dc75498a93d4d9a01f663053f2cfb94992"}, + {file = "frida-16.6.6-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:dcf59869117f4f6eaa6514555fa0a6499b5efa4e095a5785979b057841037f04"}, + {file = "frida-16.6.6-cp37-abi3-manylinux1_i686.whl", hash = "sha256:9dc05717c8079cad31268a9714cc6966931e7400487cfa0c19e6c077dea129ac"}, + {file = "frida-16.6.6-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:288d99fb774bd04b229977bf112878674e851dbb6e14b528aeb04ec085b10a4f"}, + {file = "frida-16.6.6-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:d74bd8de1aebd0acae49c89fad6ace2dddf5852df236b805dc83d5c524745e59"}, + {file = "frida-16.6.6-cp37-abi3-manylinux2014_armv7l.whl", hash = "sha256:853e1e786b24b726ea0e91e6855ca879271bf3dd4265085c019c19d716f489fd"}, + {file = "frida-16.6.6-cp37-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:ab9ca0e41e89cc881dbaa33c724066c9839c1b92a06b9f92fa9ab00dc31d62e0"}, + {file = "frida-16.6.6-cp37-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:47aaacb1246a0f8587a1f4083761189aea39c51ce545cf998076814172d31770"}, + {file = "frida-16.6.6-cp37-abi3-manylinux_2_5_i686.whl", hash = "sha256:9ba5c342fc7a46599a4835e2fb3693342ec732963a0518b2d884d951bb5ea5f4"}, + {file = "frida-16.6.6-cp37-abi3-manylinux_2_5_x86_64.whl", hash = "sha256:f27b230dd243ab3708f4051bcd9567cfe02893a706c5c3fe1573ccc2ef5a2969"}, + {file = "frida-16.6.6-cp37-abi3-win32.whl", hash = "sha256:44b4044625afa60f0799fedecfe0a25dab610556a469151dc02569555d0436a9"}, + {file = "frida-16.6.6-cp37-abi3-win_amd64.whl", hash = "sha256:12d0117ea035bfcb5e4d685e88af02ce815169945702192ea9c21c0ebba42706"}, + {file = "frida-16.6.6.tar.gz", hash = "sha256:384572cd21185e6b152628216e83fcddbb869572ad55609582580494ce8b6c2a"}, +] + +[package.dependencies] +typing_extensions = {version = "*", markers = "python_version < \"3.11\""} + +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +groups = ["main"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.11\"" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.7,<4.0" +content-hash = "ad043c15520028ff97145b4da3fd60cfe2b377ffdb5f81e04c46b38758de6d97" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3ed8f7b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "fridump3" +version = "1.0.0" +description = "A universal memory dumper using Frida for Python 3." +license = {text = "GPL-3.0-or-later"} +readme = "README.md" +requires-python = ">=3.7,<4.0" +dependencies = [ + "frida", + "loguru" +] + +[project.urls] +repository = "https://github.com/rootbsd/fridump3" + +[project.scripts] +fridump3 = 'fridump3.console:run' + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/utils.py b/utils.py deleted file mode 100644 index 13784fa..0000000 --- a/utils.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys -import string -import logging -import os -import re - -# Progress bar function - - -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.flush() - if times == total: - print("\n") - - -# A very basic implementations of Strings -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()) - 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 From 80bd314dcd303b965121525c7b38abb949f8c1ec Mon Sep 17 00:00:00 2001 From: Xenorf <90411648+Xenorf@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:03:03 +0100 Subject: [PATCH 2/7] logger catch --- fridump3/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fridump3/console.py b/fridump3/console.py index 5780c8d..dca4bde 100644 --- a/fridump3/console.py +++ b/fridump3/console.py @@ -101,7 +101,7 @@ def MENU(): args = parser.parse_args() return args - +@logger.catch def run() -> None: print(logo) From b654eb9de32b30fc981473a34c23962ac316fd08 Mon Sep 17 00:00:00 2001 From: Xenorf <90411648+Xenorf@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:19:10 +0100 Subject: [PATCH 3/7] fix logger none --- fridump3/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fridump3/console.py b/fridump3/console.py index dca4bde..82ebdc4 100644 --- a/fridump3/console.py +++ b/fridump3/console.py @@ -28,9 +28,9 @@ def set_log_level(level, output_file): level -- the log level to display (default "info") output_file -- the file used to output logs, including commands """ + logger.remove() # Remove any default handlers if level.upper() != "NONE": LOG_FORMAT = "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}" - logger.remove() # Remove any default handlers logger.add(sys.stderr, format=LOG_FORMAT, level=level.upper()) if output_file: logger.debug(f"Initializing logfile") From d65b7297fd444860e002cac13d1493abb5b40aef Mon Sep 17 00:00:00 2001 From: Xenorf <90411648+Xenorf@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:40:42 +0000 Subject: [PATCH 4/7] Frida 17+ compatibility --- README.md | 16 ++++++++-------- fridump3/console.py | 13 +++++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0cf9b67..fed835f 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ -# fridump3 +# fridump3.1 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. -## Fork information +## Requirements -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. - -FYI: I will destroy this repo is the Fridump author will integrate the pending PR concerning Python3 support. +- **Python** 3.7+ +- **Frida** 17+ ## Installation Simply run one of the following commands: > :warning: pipx is recommended for system or user wide installations ``` -pipx install git+https://github.com/rootbsd/fridump3 -pip install git+https://github.com/rootbsd/fridump3 +pipx install git+https://github.com/Xenorf/fridump3 +pip install git+https://github.com/Xenorf/fridump3 ``` ## Usage + --- ``` @@ -27,7 +27,7 @@ usage: fridump3 [-h] [-o dir] [-u] [-H HOST] [-r] [-s] [--max-size bytes] process positional arguments: - process the process that you will be injecting to + process the process name, not package name that you will be injecting to options: -h, --help show this help message and exit diff --git a/fridump3/console.py b/fridump3/console.py index 82ebdc4..687a32e 100644 --- a/fridump3/console.py +++ b/fridump3/console.py @@ -167,12 +167,13 @@ def on_message(message, data): """'use strict'; rpc.exports = { - enumerateRanges: function (prot) { - return Process.enumerateRangesSync(prot); - }, - readMemory: function (address, size) { - return Memory.readByteArray(ptr(address), size); - } + enumerateRanges: async function (prot) { + const ranges = await Process.enumerateRanges(prot); + return ranges; + }, + readMemory: function (address, size) { + return ptr(address).readByteArray(size); + } }; """ ) From 46d5caf14354a5e51dad919ee6ee99eb1c55871d Mon Sep 17 00:00:00 2001 From: Xenorf <90411648+Xenorf@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:48:26 +0000 Subject: [PATCH 5/7] workflow publish pypi --- .github/python-publish.yml | 70 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/python-publish.yml diff --git a/.github/python-publish.yml b/.github/python-publish.yml new file mode 100644 index 0000000..5fe3fea --- /dev/null +++ b/.github/python-publish.yml @@ -0,0 +1,70 @@ +# This workflow will upload a Python Package to PyPI when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + release-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Build release distributions + run: | + # NOTE: put your own distribution build steps here. + python -m pip install poetry + python -m poetry build + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + pypi-publish: + runs-on: ubuntu-latest + needs: + - release-build + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + # Dedicated environments with protections for publishing are strongly recommended. + # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules + environment: + name: pypi + # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: + # url: https://pypi.org/p/YOURPROJECT + # + # ALTERNATIVE: if your GitHub Release name is the PyPI project version string + # ALTERNATIVE: exactly, uncomment the following line instead: + # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} + + steps: + - name: Retrieve release distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ \ No newline at end of file From b91c3820a3f5395da3d0729612a39a3eaf9bc31f Mon Sep 17 00:00:00 2001 From: Xenorf <90411648+Xenorf@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:49:58 +0000 Subject: [PATCH 6/7] fix workflow --- .github/{ => workflows}/python-publish.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/python-publish.yml (100%) diff --git a/.github/python-publish.yml b/.github/workflows/python-publish.yml similarity index 100% rename from .github/python-publish.yml rename to .github/workflows/python-publish.yml From a7b9a78ce54f87d06a6450c1f4f744e28c000579 Mon Sep 17 00:00:00 2001 From: Xenorf <90411648+Xenorf@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:01:48 +0000 Subject: [PATCH 7/7] published 1.0.0 to pypi --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fed835f..e6503d4 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Fridump is an open source memory dumping tool, primarily aimed to penetration te Simply run one of the following commands: > :warning: pipx is recommended for system or user wide installations ``` -pipx install git+https://github.com/Xenorf/fridump3 -pip install git+https://github.com/Xenorf/fridump3 +pipx install fridump3 +pip install fridump3 ``` ## Usage