This repository has been archived on 2024-09-30. You can view files and clone it, but cannot push or open issues/pull-requests.
hook-frida/venv/Lib/site-packages/frida_tools/pull.py

209 lines
7.1 KiB
Python

import argparse
import codecs
import os
import sys
import time
import typing
from threading import Thread
from typing import Any, AnyStr, List, Mapping, Optional
import frida
from colorama import Fore, Style
from frida_tools.application import ConsoleApplication
from frida_tools.stream_controller import StreamController
from frida_tools.units import bytes_to_megabytes
def main() -> None:
app = PullApplication()
app.run()
class PullApplication(ConsoleApplication):
def _add_options(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument("files", help="remote files to pull", nargs="+")
def _usage(self) -> str:
return "%(prog)s [options] REMOTE... LOCAL"
def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
paths = options.files
if len(paths) == 1:
self._remote_paths = paths
self._local_paths = [os.path.join(os.getcwd(), basename_of_unknown_path(paths[0]))]
elif len(paths) == 2:
remote, local = paths
self._remote_paths = [remote]
if os.path.isdir(local):
self._local_paths = [os.path.join(local, basename_of_unknown_path(remote))]
else:
self._local_paths = [local]
else:
self._remote_paths = paths[:-1]
local_dir = paths[-1]
local_filenames = map(basename_of_unknown_path, self._remote_paths)
self._local_paths = [os.path.join(local_dir, filename) for filename in local_filenames]
self._script: Optional[frida.core.Script] = None
self._stream_controller: Optional[StreamController] = None
self._total_bytes = 0
self._time_started: Optional[float] = None
self._failed_paths = []
def _needs_target(self) -> bool:
return False
def _start(self) -> None:
try:
self._attach(0)
data_dir = os.path.dirname(__file__)
with codecs.open(os.path.join(data_dir, "fs_agent.js"), "r", "utf-8") as f:
source = f.read()
def on_message(message: Mapping[Any, Any], data: Any) -> None:
self._reactor.schedule(lambda: self._on_message(message, data))
assert self._session is not None
script = self._session.create_script(name="pull", source=source)
self._script = script
script.on("message", on_message)
self._on_script_created(script)
script.load()
self._stream_controller = StreamController(
self._post_stream_stanza,
self._on_incoming_stream_request,
on_stats_updated=self._on_stream_stats_updated,
)
worker = Thread(target=self._perform_pull)
worker.start()
except Exception as e:
self._update_status(f"Failed to pull: {e}")
self._exit(1)
return
def _stop(self) -> None:
if self._stream_controller is not None:
self._stream_controller.dispose()
def _perform_pull(self) -> None:
error = None
try:
assert self._script is not None
self._script.exports_sync.pull(self._remote_paths)
except Exception as e:
error = e
self._reactor.schedule(lambda: self._on_pull_finished(error))
def _on_pull_finished(self, error: Optional[Exception]) -> None:
for path, state in self._failed_paths:
if state == "partial":
try:
os.unlink(path)
except:
pass
if error is None:
self._render_summary_ui()
else:
self._print_error(str(error))
success = len(self._failed_paths) == 0 and error is None
status = 0 if success else 1
self._exit(status)
def _render_progress_ui(self) -> None:
assert self._stream_controller is not None
megabytes_received = bytes_to_megabytes(self._stream_controller.bytes_received)
total_megabytes = bytes_to_megabytes(self._total_bytes)
if total_megabytes != 0 and megabytes_received <= total_megabytes:
self._update_status(f"Pulled {megabytes_received:.1f} out of {total_megabytes:.1f} MB")
else:
self._update_status(f"Pulled {megabytes_received:.1f} MB")
def _render_summary_ui(self) -> None:
assert self._time_started is not None
duration = time.time() - self._time_started
if len(self._remote_paths) == 1:
prefix = f"{self._remote_paths[0]}: "
else:
prefix = ""
assert self._stream_controller is not None
sc = self._stream_controller
bytes_received = sc.bytes_received
megabytes_per_second = bytes_to_megabytes(bytes_received) / duration
self._update_status(
"{}{} file{} pulled. {:.1f} MB/s ({} bytes in {:.3f}s)".format(
prefix,
sc.streams_opened,
"s" if sc.streams_opened != 1 else "",
megabytes_per_second,
bytes_received,
duration,
)
)
def _on_message(self, message: Mapping[Any, Any], data: Any) -> None:
handled = False
if message["type"] == "send":
payload = message["payload"]
ptype = payload["type"]
if ptype == "stream":
stanza = payload["payload"]
assert self._stream_controller is not None
self._stream_controller.receive(stanza, data)
handled = True
elif ptype == "pull:status":
self._total_bytes = payload["total"]
self._time_started = time.time()
self._render_progress_ui()
handled = True
elif ptype == "pull:io-error":
index = payload["index"]
self._on_io_error(self._remote_paths[index], self._local_paths[index], payload["error"])
handled = True
if not handled:
self._print(message)
def _on_io_error(self, remote_path, local_path, error) -> None:
self._print_error(f"{remote_path}: {error}")
self._failed_paths.append((local_path, "partial"))
def _post_stream_stanza(self, stanza, data: Optional[AnyStr] = None) -> None:
self._script.post({"type": "stream", "payload": stanza}, data=data)
def _on_incoming_stream_request(self, label: str, details) -> typing.BinaryIO:
local_path = self._local_paths[int(label)]
try:
return open(local_path, "wb")
except Exception as e:
self._print_error(str(e))
self._failed_paths.append((local_path, "unopened"))
raise
def _on_stream_stats_updated(self) -> None:
self._render_progress_ui()
def _print_error(self, message: str) -> None:
self._print(Fore.RED + Style.BRIGHT + message + Style.RESET_ALL, file=sys.stderr)
def basename_of_unknown_path(path: str) -> str:
return path.replace("\\", "/").rsplit("/", 1)[-1]
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass