diff --git a/README.md b/README.md index 1128116d..3bb9f062 100755 --- a/README.md +++ b/README.md @@ -31,3 +31,32 @@
pip3 install git+https://github.com/EntySec/Ghost

+ +# Ghost + +Ghost โ€” staged installation framework + +## fast-install + +```bash + +pip install .[vcs] --break + +``` + +## install + +```bash + +python -m venv .venv +source .venv/bin/activate + + +pip install --upgrade pip +pip install .[vcs] + + +ghost + +``` + diff --git a/ghost/__init__.py b/ghost/__init__.py index 5cc61709..fcc907ff 100644 --- a/ghost/__init__.py +++ b/ghost/__init__.py @@ -22,14 +22,4 @@ SOFTWARE. """ -from ghost.core.console import Console - - -def cli() -> None: - """ Ghost Framework command-line interface. - - :return None: None - """ - - console = Console() - console.shell() +__version__ = "โ€ฆ" \ No newline at end of file diff --git a/ghost/__main__.py b/ghost/__main__.py new file mode 100644 index 00000000..0ef3024d --- /dev/null +++ b/ghost/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/ghost/cli.py b/ghost/cli.py new file mode 100644 index 00000000..d5bf9ef7 --- /dev/null +++ b/ghost/cli.py @@ -0,0 +1,5 @@ +from ghost.core.console import Console + +def main() -> int: + Console().shell() + return 0 \ No newline at end of file diff --git a/ghost/core/console.py b/ghost/core/console.py index 1abded23..da7de45e 100644 --- a/ghost/core/console.py +++ b/ghost/core/console.py @@ -23,9 +23,26 @@ """ from badges.cmd import Cmd - from ghost.core.device import Device +from rich.console import Console as RichConsole +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align +from rich import box +from rich.style import Style +from rich.rule import Rule +from rich.padding import Padding +from rich.columns import Columns + +PURPLE = "#7B61FF" +WHITE_ON_PURPLE = Style(color="white", bgcolor=PURPLE, bold=True) +INFO_STYLE = Style(color=PURPLE, bold=True) +WARN_STYLE = Style(color="yellow", bold=True) +ERR_STYLE = Style(color="red", bold=True) +SUCCESS_STYLE = Style(color="green", bold=True) + class Console(Cmd): """ Subclass of ghost.core module. @@ -37,40 +54,141 @@ class Console(Cmd): def __init__(self) -> None: super().__init__( prompt='(%lineghost%end)> ', - intro="""%clear%end + intro=""" .--. .-. .-. : .--': : .' `. : : _ : `-. .--. .--.`. .' : :; :: .. :' .; :`._-.': : `.__.':_;:_;`.__.'`.__.':_; ---=[ %bold%whiteGhost Framework 8.0.0%end ---=[ Developed by EntySec (%linehttps://entysec.com/%end) +--=[ Ghost Framework 8.0.0 +--=[ Developed by EntySec (https://entysec.com/) """ ) self.devices = {} - def do_exit(self, _) -> None: - """ Exit Ghost Framework. + # Force ANSI + TrueColor for Linux/modern terminals + self.rich = RichConsole(force_terminal=True, color_system="truecolor") + self.rich.clear() + self._render_header() + - :return None: None - :raises EOFError: EOF error - """ + def _render_header(self) -> None: + """Render a fancy hacker-style header with tools table and quick help.""" + title = Text("Ghost Framework 8.0.0", style="bold white") + subtitle = Text("Developed by EntySec โ€” https://entysec.com/", style="dim") + + ascii_art = Text( + " .--. .-. .-.\n" + " : .--': : .' `.\n" + " : : _ : `-. .--. .--.`. .'\n" + " : :; :: .. :' .; :`._-.': :\n" + " `.__.':_;:_;`.__.'`.__.':_;", + justify="center", + ) + + left = Panel( + Align.center(ascii_art), + border_style=PURPLE, + box=box.HEAVY, + padding=(0, 2), + title="[bold]GHOST", + subtitle=title, + ) + help_table = Table(title=Text("๐Ÿš€ Ghost Framework Commands", style="bold white on " + PURPLE, justify="center"), + box=box.DOUBLE_EDGE, + border_style=PURPLE, + expand=False, + show_lines=True) + + help_table.add_column("Command", style="bold white on " + "#5A3EFF", no_wrap=True, justify="center") + help_table.add_column("Description", style="italic dim", justify="left") + + commands = [ + ("๐Ÿ”Œ connect :[port]", "Connect to device via ADB (default port 5555)"), + ("๐Ÿ“ฑ devices", "List connected devices"), + ("โŒ disconnect ", "Disconnect device by ID"), + ("๐Ÿ’ฌ interact ", "Interact with a connected device"), + ("๐Ÿ” analyze / an ", "Run Device Analyzer"), + ("๐Ÿ“œ logcat / lc ", "Start live logcat stream"), + ("๐Ÿงน clear", "Clear the terminal screen"), + ("๐Ÿšช exit", "Quit Ghost Framework"), + ("๐Ÿ”„ Index 99", "Return to Menu / Exit (UI helper)") + ] + + ALT_ROW = "#2E2E2E" + for i, (cmd, desc) in enumerate(commands): + style = Style(bgcolor=ALT_ROW) if i % 2 else Style() + help_table.add_row(cmd, desc, style=style) + + right_panel = Panel( + Align.left( + Text.assemble(subtitle, "\n\n", "Theme: ", (PURPLE, "Hacker โ€ข Purple")) + ), + border_style=PURPLE, + box=box.ROUNDED, + padding=(0, 1), + title="[bold]Info", + ) + + header_columns = Columns([left, Panel(help_table, padding=(1, 2), border_style=PURPLE), right_panel]) + self.rich.print(header_columns) + self.rich.print(Rule(style=PURPLE)) + self.rich.print(Align.center(Text("Type [bold]devices[/bold] to list connected devices โ€” Index 99 โ†’ Exit", style=INFO_STYLE))) + self.rich.print() + + def print_empty(self, message: str = "", end: str = "\n") -> None: + """Print a simple message.""" + self.rich.print(message) + def print_information(self, message: str) -> None: + """Print an informational message in a panel.""" + self.rich.print(Panel(Text(message), border_style=PURPLE, title="[bold white]INFO", box=box.MINIMAL)) + + def print_warning(self, message: str) -> None: + """Print a warning message in a panel.""" + self.rich.print(Panel(Text(message), border_style="yellow", title="[bold white]WARNING", box=box.MINIMAL)) + + def print_error(self, message: str) -> None: + """Print an error message in a panel.""" + self.rich.print(Panel(Text(message), border_style="red", title="[bold white]ERROR", box=box.MINIMAL)) + + def print_success(self, message: str) -> None: + """Print a success message in a panel.""" + self.rich.print(Panel(Text(message), border_style="green", title="[bold white]SUCCESS", box=box.MINIMAL)) + + def print_usage(self, usage: str) -> None: + """Print usage information for a command.""" + usage_text = Text.assemble(("Usage: ", "bold"), (usage, "")) + footer = Text("Index 99 โ†’ Return to Menu", style=INFO_STYLE) + self.rich.print(Panel(usage_text, border_style=PURPLE, title="[bold]USAGE", subtitle=footer)) + + def print_process(self, message: str) -> None: + """Show a processing spinner with a message.""" + with self.rich.status(Text(message, style=INFO_STYLE), spinner="bouncingBall", spinner_style=PURPLE): + pass + + def print_table(self, title: str, columns: tuple, *rows) -> None: + """Render a stylized table for lists like connected devices.""" + table = Table(title=title, box=box.SIMPLE_HEAVY, expand=False, border_style=PURPLE) + for col in columns: + table.add_column(str(col), header_style="bold white") + for row in rows: + table.add_row(*[str(x) for x in row]) + footer = Text("Index 99 โ†’ Return to Menu", style=INFO_STYLE) + wrapper = Panel(Padding(table, (0, 1)), subtitle=footer, border_style=PURPLE) + self.rich.print(wrapper) + + def do_exit(self, _) -> None: + """Quit Ghost Framework and disconnect all devices.""" for device in list(self.devices): self.devices[device]['device'].disconnect() del self.devices[device] - raise EOFError def do_connect(self, args: list) -> None: - """ Connect device. - - :param list args: arguments - :return None: None - """ - + """Connect to a device via ADB.""" if len(args) < 2: self.print_usage("connect :[port]") return @@ -93,7 +211,6 @@ def do_connect(self, args: list) -> None: } }) self.print_empty("") - self.print_information( f"Type %greendevices%end to list all connected devices.") self.print_information( @@ -102,37 +219,25 @@ def do_connect(self, args: list) -> None: ) def do_devices(self, _) -> None: - """ Show connected devices. - - :return None: None - """ - + """List all connected devices.""" if not self.devices: self.print_warning("No devices connected.") return devices = [] - for device in self.devices: devices.append( (device, self.devices[device]['host'], self.devices[device]['port'])) - self.print_table("Connected Devices", ('ID', 'Host', 'Port'), *devices) def do_disconnect(self, args: list) -> None: - """ Disconnect device. - - :param list args: arguments - :return None: None - """ - + """Disconnect a connected device by ID.""" if len(args) < 2: self.print_usage("disconnect ") return device_id = int(args[1]) - if device_id not in self.devices: self.print_error("Invalid device ID!") return @@ -141,18 +246,12 @@ def do_disconnect(self, args: list) -> None: self.devices.pop(device_id) def do_interact(self, args: list) -> None: - """ Interact with device. - - :param list args: arguments - :return None: None - """ - + """Interact with a connected device by ID.""" if len(args) < 2: self.print_usage("interact ") return device_id = int(args[1]) - if device_id not in self.devices: self.print_error("Invalid device ID!") return @@ -160,10 +259,10 @@ def do_interact(self, args: list) -> None: self.print_process(f"Interacting with device {str(device_id)}...") self.devices[device_id]['device'].interact() - def shell(self) -> None: - """ Run console shell. - - :return None: None - """ + def do_clear(self, _) -> None: + """Clear the terminal screen.""" + self.rich.clear() + def shell(self) -> None: + """Start the main Ghost Framework loop.""" self.loop() diff --git a/ghost/core/device.py b/ghost/core/device.py index 16fa2201..b03f1617 100644 --- a/ghost/core/device.py +++ b/ghost/core/device.py @@ -23,203 +23,210 @@ """ import os - +import time +import threading from badges.cmd import Cmd - from adb_shell.adb_device import AdbDeviceTcp from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_pythonrsa import PythonRSASigner - from pex.fs import FS +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align -class Device(Cmd, FS): - """ Subclass of ghost.core module. +_PURPLE = "#7B61FF" +_console = Console() - This subclass of ghost.core module is intended for providing - an implementation of device controller. - """ +_MAX_RETRIES = 5 +_RETRY_DELAY = 3 +_HEALTHCHECK_DELAY = 10 - def __init__(self, host: str, port: int = 5555, timeout: int = 10, - key_filename: str = 'key') -> None: - """ Initialize device. - :param str host: device host - :param int port: device port - :param int timeout: connection timeout - :param str key_filename: name of the file containing key - :return None: None - """ +class Device(Cmd, FS): + """Enhanced Device class with auto-reconnect, analyzer & log viewer.""" + def __init__(self, host: str, port: int = 5555, timeout: int = 10, key_filename: str = 'key') -> None: self.host = host self.port = int(port) - self.key_file = key_filename self.device = AdbDeviceTcp(self.host, self.port, default_transport_timeout_s=timeout) + self._reconnect_thread = None + self._stop_reconnect = threading.Event() + super().__init__( prompt=f'(%lineghost%end: %red{self.host}%end)> ', path=[f'{os.path.dirname(os.path.dirname(__file__))}/modules'], device=self ) - def get_keys(self) -> tuple: - """ Get cryptographic keys. + def print_panel(self, message: str, title: str, color: str = _PURPLE) -> None: + _console.print(Panel.fit(Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color)) - :return tuple: public key, private key - """ + def print_process(self, message: str) -> None: self.print_panel(message, "PROCESS", _PURPLE) + def print_success(self, message: str) -> None: self.print_panel(message, "SUCCESS", "green") + def print_error(self, message: str) -> None: self.print_panel(message, "ERROR", "red") + def print_information(self, message: str) -> None: self.print_panel(message, "INFO", _PURPLE) + def print_empty(self): _console.print() + + def get_keys(self) -> tuple: if not os.path.exists(self.key_file): keygen(self.key_file) + with open(self.key_file, 'r') as f: + priv = f.read() + with open(self.key_file + '.pub', 'r') as f: + pub = f.read() + return pub, priv - with open(self.key_file, 'r') as file: - priv = file.read() - with open(self.key_file + '.pub', 'r') as file: - pub = file.read() - return pub, priv + def connect(self, auto_reconnect: bool = True) -> bool: + self._stop_reconnect.clear() + self.print_process(f"Connecting to {self.host}...") + keys = self.get_keys() + signer = PythonRSASigner(*keys) - def send_command(self, command: str, output: bool = True) -> str: - """ Send command to the device. + for attempt in range(1, _MAX_RETRIES + 1): + try: + self.device.connect(rsa_keys=[signer], auth_timeout_s=5) + self.print_success(f"Connected to {self.host}!") + + if auto_reconnect: + self._start_auto_reconnect_thread() + return True + except Exception as e: + self.print_error(f"[Attempt {attempt}/{_MAX_RETRIES}] Connection failed: {e}") + if attempt < _MAX_RETRIES: + self.print_information(f"Retrying in {_RETRY_DELAY} seconds...") + time.sleep(_RETRY_DELAY) + + self.print_error(f"Failed to connect to {self.host} after {_MAX_RETRIES} attempts!") + return False - :param str command: command to send to the device - :param bool output: return command output or not - :return str: empty if output is False otherwise command output - """ + def _start_auto_reconnect_thread(self) -> None: + if self._reconnect_thread and self._reconnect_thread.is_alive(): return + self._reconnect_thread = threading.Thread(target=self.auto_reconnect, daemon=True) + self._reconnect_thread.start() + def auto_reconnect(self) -> None: + while not self._stop_reconnect.is_set(): + try: + self.device.shell("echo ping", transport_timeout_s=3) + except Exception: + self.print_error(f"Lost connection to {self.host}, attempting reconnect...") + if not self.connect(auto_reconnect=False): + self.print_error("Auto-reconnect failed, will retry again...") + else: + self.print_success("Reconnected successfully!") + time.sleep(_HEALTHCHECK_DELAY) + + def disconnect(self) -> None: + self._stop_reconnect.set() + if self._reconnect_thread and self._reconnect_thread.is_alive(): + self._reconnect_thread.join(timeout=2) + try: + self.device.close() + self.print_success(f"Disconnected from {self.host}.") + except Exception: + self.print_error("Failed to disconnect properly!") + + + def send_command(self, command: str, output: bool = True) -> str: try: cmd_output = self.device.shell(command) except Exception: self.print_error("Socket is not connected!") return None - - if output: - return cmd_output - - return "" + return cmd_output if output else "" def list(self, path: str) -> list: - """ List contents of the specified directory. - - :param str path: directory to list contents from - :return list: list of directory contents - """ - try: return self.device.list(path) except Exception: self.print_error("Failed to list directory!") return [] - def connect(self) -> bool: - """ Connect the specified device. - - :return bool: True if connection succeed - """ - - self.print_process(f"Connecting to {self.host}...") - - try: - keys = self.get_keys() - signer = PythonRSASigner(*keys) - - self.device.connect(rsa_keys=[signer], auth_timeout_s=5) - self.print_success(f"Connected to {self.host}!") - - return True - - except Exception: - self.print_error(f"Failed to connect to {self.host}!") - - return False - - def disconnect(self) -> None: - """ Disconnect the specified device. - - :return None: None - """ - - self.device.close() - def download(self, input_file: str, output_path: str) -> bool: - """ Download file from the specified device. - - :param str input_file: path of the file to download - :param str output_path: path to output the file to - :return bool: True if download succeed - """ - exists, is_dir = self.exists(output_path) - if exists: - if is_dir: - output_path = output_path + '/' + os.path.split(input_file)[1] - + if is_dir: output_path += '/' + os.path.split(input_file)[1] try: self.print_process(f"Downloading {input_file}...") self.device.pull(input_file, output_path) - - self.print_process(f"Saving to {output_path}...") self.print_success(f"Saved to {output_path}!") - return True - except Exception: - self.print_error(f"Remote file: {input_file}: does not exist or a directory!") - + self.print_error(f"Remote file {input_file} not found or invalid!") return False def upload(self, input_file: str, output_path: str) -> bool: - """ Upload file to the specified device. - - :param str input_file: path of the file to upload - :param str output_path: path to output the file to - :return bool: True if upload succeed - """ - if self.check_file(input_file): try: self.print_process(f"Uploading {input_file}...") self.device.push(input_file, output_path) - - self.print_process(f"Saving to {output_path}...") self.print_success(f"Saved to {output_path}!") - return True - except Exception: try: - output_path = output_path + '/' + os.path.split(input_file)[1] + output_path += '/' + os.path.split(input_file)[1] self.device.push(input_file, output_path) - except Exception: - self.print_error(f"Remote directory: {output_path}: does not exist!") - + self.print_error(f"Remote directory {output_path} does not exist!") return False def is_rooted(self) -> bool: - """ Check if the specified device is rooted. - - :return bool: True if device is rooted - """ - responder = self.send_command('which su') - - if not responder or responder.isspace(): - return False - - return True + return bool(responder and not responder.isspace()) def interact(self) -> None: - """ Interact with the specified device. - - :return None: None - """ - self.print_success("Interactive connection spawned!") - - self.print_empty() self.print_process("Loading device modules...") - self.print_information(f"Modules loaded: {str(len(self.external))}") self.loop() + + def analyze_device(self): + self.print_process(f"Analyzing {self.host} ...") + try: + props = { + "Manufacturer": self.send_command("getprop ro.product.manufacturer"), + "Model": self.send_command("getprop ro.product.model"), + "Android Version": self.send_command("getprop ro.build.version.release"), + "Security Patch": self.send_command("getprop ro.build.version.security_patch"), + "Architecture": self.send_command("getprop ro.product.cpu.abi"), + "Rooted": "Yes" if self.is_rooted() else "No" + } + + table = Table(title=f"๐Ÿ“ฑ Device Analysis โ€” {self.host}", border_style=_PURPLE) + table.add_column("Property", style="bold white") + table.add_column("Value", style="dim") + + for k, v in props.items(): table.add_row(k, v.strip() if v else "N/A") + _console.print(table) + self.print_success("Analysis complete!") + except Exception as e: + self.print_error(f"Analysis failed: {e}") + + def live_logcat(self): + self.print_information("Starting live logcat stream (Press Ctrl+C to stop)...") + + def stream_logs(): + try: + shell = self.device.shell("logcat -v time", decode=False) + for line in shell: + decoded = line.decode(errors="ignore").strip() + if " E " in decoded: _console.print(Text(decoded, style="red")) + elif " W " in decoded: _console.print(Text(decoded, style="yellow")) + else: _console.print(Text(decoded, style="dim")) + except KeyboardInterrupt: + self.print_process("Log streaming stopped by user.") + except Exception as e: + self.print_error(f"Logcat error: {e}") + + t = threading.Thread(target=stream_logs) + t.daemon = True + t.start() diff --git a/ghost/modules/activity.py b/ghost/modules/activity.py index 2ebd7348..71c24139 100644 --- a/ghost/modules/activity.py +++ b/ghost/modules/activity.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,8 +27,24 @@ def __init__(self): 'NeedsRoot': False }) + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_empty(self, message: str = ""): + panel = Panel.fit( + Align.left(Text(message if message else "")), + title=Text("OUTPUT", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + def run(self, _): self.print_process("Getting activity information...") output = self.device.send_command("dumpsys activity") - self.print_empty(output) + self.print_empty(output) \ No newline at end of file diff --git a/ghost/modules/battery.py b/ghost/modules/battery.py index c81ec8d5..289e6a43 100644 --- a/ghost/modules/battery.py +++ b/ghost/modules/battery.py @@ -4,6 +4,14 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +# Main theme color +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,6 +28,22 @@ def __init__(self): 'NeedsRoot': False }) + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_empty(self, message: str = ""): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text("OUTPUT", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + def run(self, _): self.print_process("Getting battery information...") diff --git a/ghost/modules/download.py b/ghost/modules/download.py index ea030897..faa0f971 100644 --- a/ghost/modules/download.py +++ b/ghost/modules/download.py @@ -4,8 +4,15 @@ """ import os - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +# Main theme color +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,5 +29,36 @@ def __init__(self): 'NeedsRoot': False }) + def print_process(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("PROCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_success(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("SUCCESS", style="bold white on " + _PURPLE), + border_style=_PURPLE + ) + _console.print(panel) + + def print_error(self, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text("ERROR", style="bold white on " + _PURPLE), + border_style="red" + ) + _console.print(panel) + def run(self, args): - self.device.download(args[1], args[2]) + remote_file, local_path = args[1], args[2] + self.print_process(f"Downloading {remote_file}...") + + success = self.device.download(remote_file, local_path) + if success: + self.print_success(f"File saved to {local_path}") + else: + self.print_error("Download failed!") diff --git a/ghost/modules/keyboard.py b/ghost/modules/keyboard.py index de044b8f..ea9804e5 100644 --- a/ghost/modules/keyboard.py +++ b/ghost/modules/keyboard.py @@ -6,8 +6,14 @@ import sys import termios import tty - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -24,6 +30,14 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + @staticmethod def get_char(): fd = sys.stdin.fileno() @@ -35,9 +49,9 @@ def get_char(): termios.tcsetattr(fd, termios.TCSADRAIN, old) def run(self, _): - self.print_process("Interacting with keyboard...") - self.print_success("Interactive connection spawned!") + self.print_panel("PROCESS", "Interacting with keyboard...") + self.print_panel("SUCCESS", "Interactive connection spawned!") + self.print_panel("INFO", "Type text below.", color="cyan") - self.print_information("Type text below.") while True: self.device.send_command(f"input text {self.get_char()}") diff --git a/ghost/modules/list.py b/ghost/modules/list.py index c6685252..fa612b72 100644 --- a/ghost/modules/list.py +++ b/ghost/modules/list.py @@ -4,8 +4,15 @@ """ import datetime - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,15 +29,32 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {_PURPLE}"), + border_style=_PURPLE + ) + _console.print(panel) + + def print_table_rich(self, title: str, headers: tuple, rows: list): + table = Table(title=title, header_style=f"bold {_PURPLE}", border_style=_PURPLE) + for h in headers: + table.add_column(h, style="white", no_wrap=True) + for row in rows: + table.add_row(*[str(i) for i in row]) + _console.print(table) + def run(self, args): - output = self.device.list(args[1]) + self.print_panel("PROCESS", f"Listing directory: {args[1]}") + output = self.device.list(args[1]) if output: headers = ('Name', 'Mode', 'Size', 'Modification Time') - data = list() - + rows = [] for entry in sorted(output): - timestamp = datetime.datetime.fromtimestamp(entry[3]) - data.append((entry[0].decode(), str(entry[1]), str(entry[2]), timestamp)) - - self.print_table(f"Directory {args[1]}", headers, *data) + timestamp = datetime.datetime.fromtimestamp(entry[3]).strftime("%Y-%m-%d %H:%M:%S") + rows.append((entry[0].decode(), str(entry[1]), str(entry[2]), timestamp)) + self.print_table_rich(f"Directory {args[1]}", headers, rows) + else: + self.print_panel("INFO", "No files found in this directory.") diff --git a/ghost/modules/network.py b/ghost/modules/network.py index a84ee2ef..937711b7 100644 --- a/ghost/modules/network.py +++ b/ghost/modules/network.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,96 +27,72 @@ def __init__(self): 'Options': [ ( ('-a', '--arp'), - { - 'help': "Show device ARP table.", - 'action': 'store_true' - } + {'help': "Show device ARP table.", 'action': 'store_true'} ), ( ('-i', '--ipconfig'), - { - 'help': "Show device IP configuration.", - 'action': 'store_true' - } + {'help': "Show device IP configuration.", 'action': 'store_true'} ), ( ('-I', '--iproute'), - { - 'help': "Show device route table.", - 'action': 'store_true' - } + {'help': "Show device route table.", 'action': 'store_true'} ), ( ('-l', '--locate'), - { - 'help': "Show device location.", - 'action': 'store_true' - } + {'help': "Show device location.", 'action': 'store_true'} ), ( ('-s', '--stats'), - { - 'help': "Show device network stats.", - 'action': 'store_true' - } + {'help': "Show device network stats.", 'action': 'store_true'} ), ( ('-p', '--ports'), - { - 'help': "Show device open ports.", - 'action': 'store_true' - } + {'help': "Show device open ports.", 'action': 'store_true'} ), ( ('-S', '--services'), - { - 'help': "Show device services.", - 'action': 'store_true' - } + {'help': "Show device services.", 'action': 'store_true'} ), ( ('-f', '--forwarding'), - { - 'help': "Show device forwarding rules.", - 'action': 'store_true' - } + {'help': "Show device forwarding rules.", 'action': 'store_true'} ) ] }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): outputs = [] if args.arp: - outputs.append( - self.device.send_command('cat /proc/net/arp')) + outputs.append(self.device.send_command('cat /proc/net/arp')) if args.ipconfig: - outputs.append( - self.device.send_command('ip addr show')) + outputs.append(self.device.send_command('ip addr show')) if args.iproute: - outputs.append( - self.device.send_command('ip route show')) + outputs.append(self.device.send_command('ip route show')) if args.locate: - outputs.append( - self.device.send_command('dumpsys location')) + outputs.append(self.device.send_command('dumpsys location')) if args.stats: - outputs.append( - self.device.send_command('cat /proc/net/netstat')) + outputs.append(self.device.send_command('cat /proc/net/netstat')) if args.ports: - outputs.append( - self.device.send_command('busybox netstat -an')) + outputs.append(self.device.send_command('busybox netstat -an')) if args.services: - outputs.append( - self.device.send_command('service list')) + outputs.append(self.device.send_command('service list')) if args.forwarding: - outputs.append( - self.device.send_command('cat /proc/sys/net/ipv4/ip_forward')) + outputs.append(self.device.send_command('cat /proc/sys/net/ipv4/ip_forward')) - self.print_empty('\n'.join(outputs)) + self.print_panel("NETWORK OUTPUT", '\n'.join(outputs)) diff --git a/ghost/modules/openurl.py b/ghost/modules/openurl.py index f6b4b8de..69d774b7 100644 --- a/ghost/modules/openurl.py +++ b/ghost/modules/openurl.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,8 +27,19 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - if not args[1].startswith(("http://", "https://")): - args[1] = "http://" + args[1] + url = args[1] + if not url.startswith(("http://", "https://")): + url = "http://" + url - self.device.send_command(f'am start -a android.intent.action.VIEW -d "{args[1]}"') + self.print_panel("PROCESS", f"Opening URL on device: {url}") + self.device.send_command(f'am start -a android.intent.action.VIEW -d "{url}"') + self.print_panel("SUCCESS", "URL opened successfully!") diff --git a/ghost/modules/press.py b/ghost/modules/press.py index 5235b623..0dfbcd86 100644 --- a/ghost/modules/press.py +++ b/ghost/modules/press.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,8 +27,19 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - if int(args[1]) < 124: - self.device.send_command(f"input keyevent {args[1]}") + keycode = int(args[1]) + if keycode < 124: + self.print_panel("PROCESS", f"Pressing device button with keycode: {keycode}") + self.device.send_command(f"input keyevent {keycode}") + self.print_panel("SUCCESS", "Key event sent successfully!") else: - self.print_error("Invalid keycode!") + self.print_panel("ERROR", "Invalid keycode!", color="red") diff --git a/ghost/modules/screenshot.py b/ghost/modules/screenshot.py index 6b0b3e8b..7c39be3a 100644 --- a/ghost/modules/screenshot.py +++ b/ghost/modules/screenshot.py @@ -4,8 +4,14 @@ """ import os - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,9 +28,24 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - self.print_process(f"Taking screenshot...") + local_path = args[1] + self.print_panel("PROCESS", "Taking screenshot on device...") + self.device.send_command("screencap /data/local/tmp/screenshot.png") + success = self.device.download('/data/local/tmp/screenshot.png', local_path) + + if success: + self.print_panel("SUCCESS", f"Screenshot saved to {local_path}") + else: + self.print_panel("ERROR", "Failed to download screenshot!", color="red") - self.device.download('/data/local/tmp/screenshot.png', args[1]) self.device.send_command("rm /data/local/tmp/screenshot.png") diff --git a/ghost/modules/shell.py b/ghost/modules/shell.py index 35227149..bad6bb45 100644 --- a/ghost/modules/shell.py +++ b/ghost/modules/shell.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,6 +27,16 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message if message else "No output.")), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - output = self.device.send_command(' '.join(args[1:])) - self.print_empty(output) + command = ' '.join(args[1:]) + self.print_panel("PROCESS", f"Executing shell command: {command}") + output = self.device.send_command(command) + self.print_panel("OUTPUT", output) diff --git a/ghost/modules/sleep.py b/ghost/modules/sleep.py index 049b0547..9b8af222 100644 --- a/ghost/modules/sleep.py +++ b/ghost/modules/sleep.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,5 +27,15 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, _): + self.print_panel("PROCESS", "Putting device into sleep mode...") self.device.send_command("input keyevent 26") + self.print_panel("SUCCESS", "Device is now in sleep mode!") diff --git a/ghost/modules/upload.py b/ghost/modules/upload.py index 3bbd4c6d..f28b9c0a 100644 --- a/ghost/modules/upload.py +++ b/ghost/modules/upload.py @@ -4,8 +4,14 @@ """ import os - from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -22,5 +28,20 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - self.device.upload(args[1], args[2]) + local_file, remote_path = args[1], args[2] + self.print_panel("PROCESS", f"Uploading {local_file} to {remote_path}...") + + success = self.device.upload(local_file, remote_path) + if success: + self.print_panel("SUCCESS", f"File uploaded to {remote_path}") + else: + self.print_panel("ERROR", "Upload failed!", color="red") diff --git a/ghost/modules/wifi.py b/ghost/modules/wifi.py index 70b46198..c6dcbf3e 100644 --- a/ghost/modules/wifi.py +++ b/ghost/modules/wifi.py @@ -4,6 +4,13 @@ """ from badges.cmd import Command +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.align import Align + +_PURPLE = "#7B61FF" +_console = Console() class ExternalCommand(Command): @@ -20,11 +27,20 @@ def __init__(self): 'NeedsRoot': False }) + def print_panel(self, title: str, message: str, color: str = _PURPLE): + panel = Panel.fit( + Align.left(Text(message)), + title=Text(title, style=f"bold white on {color}"), + border_style=color + ) + _console.print(panel) + def run(self, args): - if args[1] in ['on', 'off']: - if args[1] == 'on': - self.device.send_command("svc wifi enable") - else: - self.device.send_command("svc wifi disable") + state = args[1].lower() + if state in ['on', 'off']: + action = "enable" if state == 'on' else "disable" + self.print_panel("PROCESS", f"Turning WiFi {state}...") + self.device.send_command(f"svc wifi {action}") + self.print_panel("SUCCESS", f"WiFi turned {state} successfully!") else: - self.print_usage(self.info['Usage']) + self.print_panel("USAGE", f"Usage: {self.info['Usage']}", color="red") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..3b673e40 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ghost" +version = "8.0.0" +description = "Ghost Framework is an Android post-exploitation framework that exploits the Android Debug Bridge to remotely access an Android device." +readme = "README.md" +requires-python = ">=3.7.0" +license = { file = "LICENSE" } +authors = [ + { name = "EntySec", email = "entysec@gmail.com" } +] +urls = { Homepage = "https://github.com/EntySec/Ghost" } + +dependencies = [ + "adb-shell", + "pex @ git+https://github.com/EntySec/Pex", + "badges @ git+https://github.com/EntySec/Badges", + "colorscript @ git+https://github.com/EntySec/ColorScript", +] + +[project.scripts] +ghost = "ghost.cli:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["*"] +exclude = [] + +[tool.setuptools] +include-package-data = true \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 59dad0c1..00000000 --- a/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -MIT License - -Copyright (c) 2020-2024 EntySec - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -from setuptools import setup, find_packages - -setup(name='ghost', - version='8.0.0', - description=( - 'Ghost Framework is an Android post-exploitation framework that exploits the' - ' Android Debug Bridge to remotely access an Android device.' - ), - url='http://github.com/EntySec/Ghost', - author='EntySec', - author_email='entysec@gmail.com', - license='MIT', - python_requires='>=3.7.0', - packages=find_packages(), - include_package_data=True, - entry_points={ - "console_scripts": [ - "ghost = ghost:cli" - ] - }, - install_requires=[ - 'adb-shell', - 'pex @ git+https://github.com/EntySec/Pex', - 'badges @ git+https://github.com/EntySec/Badges', - 'colorscript @ git+https://github.com/EntySec/ColorScript' - ], - zip_safe=False - ) diff --git a/uninstall.py b/uninstall.py new file mode 100644 index 00000000..fb5319b8 --- /dev/null +++ b/uninstall.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +uninstall.py โ€” Ghost cleanup/uninstall script + +- Deletes virtualenv if exists +- Uninstalls Ghost and related Python packages if installed in system Python +- Adds --break-system-packages for system Python to avoid PEP 668 errors +- Interactive prompts with rich +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from typing import List + +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Confirm + +console = Console() +ROOT = Path(__file__).parent.resolve() + +PYTHON_PACKAGES = [ + "ghost", + "adb-shell", + "pex", + "badges", + "colorscript", +] + +DEFAULT_VENV = ROOT / ".venv" + + +def run(cmd: List[str], check: bool = True) -> None: + """Run a shell command and print it.""" + console.log(f"[bold purple]$[/bold purple] {' '.join(cmd)}") + res = subprocess.run(cmd) + if check and res.returncode != 0: + console.print(Panel(f"[red]Command failed: {' '.join(cmd)}[/red]")) + + +def uninstall_packages(python_exe: str, break_system: bool = False) -> None: + """Uninstall all Ghost-related packages.""" + for pkg in PYTHON_PACKAGES: + console.print(Panel(f"Uninstalling package: [bold]{pkg}[/bold]")) + cmd = [python_exe, "-m", "pip", "uninstall", "-y", pkg] + if break_system: + cmd.append("--break-system-packages") + run(cmd) + + +def remove_virtualenv(venv_path: Path) -> None: + """Remove virtualenv folder if exists.""" + if venv_path.exists(): + console.print(Panel(f"Removing virtualenv: [bold]{venv_path}[/bold]")) + import shutil + shutil.rmtree(venv_path) + console.print(Panel(f"[green]Virtualenv removed.[/green]")) + else: + console.print(Panel("[yellow]No virtualenv found.[/yellow]")) + + +def main() -> None: + console.print(Panel("[bold purple]Ghost Uninstaller[/bold purple]\nIndex 99 โ†’ Return to Menu / Exit")) + + use_venv = False + if DEFAULT_VENV.exists(): + use_venv = Confirm.ask(f"Detected virtualenv at {DEFAULT_VENV}. Remove it?", default=True) + if use_venv: + remove_virtualenv(DEFAULT_VENV) + + uninstall_system = Confirm.ask("Do you want to uninstall Ghost Python packages from system interpreter?", default=False) + if uninstall_system: + python_exe = sys.executable + uninstall_packages(python_exe, break_system=True) + + console.print(Panel("[bold green]Uninstall process completed.[/bold green]")) + + +if __name__ == "__main__": + main()