287 lines
11 KiB
Python
287 lines
11 KiB
Python
def main() -> None:
|
|
import argparse
|
|
import functools
|
|
import json
|
|
import math
|
|
import platform
|
|
import sys
|
|
from base64 import b64encode
|
|
from typing import List, Tuple, Union
|
|
|
|
try:
|
|
import termios
|
|
import tty
|
|
except:
|
|
pass
|
|
|
|
import _frida
|
|
|
|
from frida_tools.application import ConsoleApplication
|
|
|
|
class PSApplication(ConsoleApplication):
|
|
def _add_options(self, parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument(
|
|
"-a",
|
|
"--applications",
|
|
help="list only applications",
|
|
action="store_true",
|
|
dest="list_only_applications",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"-i",
|
|
"--installed",
|
|
help="include all installed applications",
|
|
action="store_true",
|
|
dest="include_all_applications",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"-j",
|
|
"--json",
|
|
help="output results as JSON",
|
|
action="store_const",
|
|
dest="output_format",
|
|
const="json",
|
|
default="text",
|
|
)
|
|
|
|
def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
|
|
if options.include_all_applications and not options.list_only_applications:
|
|
parser.error("-i cannot be used without -a")
|
|
self._list_only_applications = options.list_only_applications
|
|
self._include_all_applications = options.include_all_applications
|
|
self._output_format = options.output_format
|
|
self._terminal_type, self._icon_size = self._detect_terminal()
|
|
|
|
def _usage(self) -> str:
|
|
return "%(prog)s [options]"
|
|
|
|
def _start(self) -> None:
|
|
if self._list_only_applications:
|
|
self._list_applications()
|
|
else:
|
|
self._list_processes()
|
|
|
|
def _list_processes(self) -> None:
|
|
if self._output_format == "text" and self._terminal_type == "iterm2":
|
|
scope = "full"
|
|
else:
|
|
scope = "minimal"
|
|
|
|
try:
|
|
assert self._device is not None
|
|
processes = self._device.enumerate_processes(scope=scope)
|
|
except Exception as e:
|
|
self._update_status(f"Failed to enumerate processes: {e}")
|
|
self._exit(1)
|
|
return
|
|
|
|
if self._output_format == "text":
|
|
if len(processes) > 0:
|
|
pid_column_width = max(map(lambda p: len(str(p.pid)), processes))
|
|
icon_width = max(map(compute_icon_width, processes))
|
|
name_column_width = icon_width + max(map(lambda p: len(p.name), processes))
|
|
|
|
header_format = "%" + str(pid_column_width) + "s %s"
|
|
self._print(header_format % ("PID", "Name"))
|
|
self._print(f"{pid_column_width * '-'} {name_column_width * '-'}")
|
|
|
|
line_format = "%" + str(pid_column_width) + "d %s"
|
|
name_format = "%-" + str(name_column_width - icon_width) + "s"
|
|
|
|
for process in sorted(processes, key=functools.cmp_to_key(compare_processes)):
|
|
if icon_width != 0:
|
|
icons = process.parameters.get("icons", None)
|
|
if icons is not None:
|
|
icon = self._render_icon(icons[0])
|
|
else:
|
|
icon = " "
|
|
name = icon + " " + name_format % process.name
|
|
else:
|
|
name = name_format % process.name
|
|
|
|
self._print(line_format % (process.pid, name))
|
|
else:
|
|
self._log("error", "No running processes.")
|
|
elif self._output_format == "json":
|
|
result = []
|
|
for process in sorted(processes, key=functools.cmp_to_key(compare_processes)):
|
|
result.append({"pid": process.pid, "name": process.name})
|
|
self._print(json.dumps(result, sort_keys=False, indent=2))
|
|
|
|
self._exit(0)
|
|
|
|
def _list_applications(self) -> None:
|
|
if self._output_format == "text" and self._terminal_type == "iterm2":
|
|
scope = "full"
|
|
else:
|
|
scope = "minimal"
|
|
|
|
try:
|
|
assert self._device is not None
|
|
applications = self._device.enumerate_applications(scope=scope)
|
|
except Exception as e:
|
|
self._update_status(f"Failed to enumerate applications: {e}")
|
|
self._exit(1)
|
|
return
|
|
|
|
if not self._include_all_applications:
|
|
applications = list(filter(lambda app: app.pid != 0, applications))
|
|
|
|
if self._output_format == "text":
|
|
if len(applications) > 0:
|
|
pid_column_width = max(map(lambda app: len(str(app.pid)), applications))
|
|
icon_width = max(map(compute_icon_width, applications))
|
|
name_column_width = icon_width + max(map(lambda app: len(app.name), applications))
|
|
identifier_column_width = max(map(lambda app: len(app.identifier), applications))
|
|
|
|
header_format = (
|
|
"%"
|
|
+ str(pid_column_width)
|
|
+ "s "
|
|
+ "%-"
|
|
+ str(name_column_width)
|
|
+ "s "
|
|
+ "%-"
|
|
+ str(identifier_column_width)
|
|
+ "s"
|
|
)
|
|
self._print(header_format % ("PID", "Name", "Identifier"))
|
|
self._print(f"{pid_column_width * '-'} {name_column_width * '-'} {identifier_column_width * '-'}")
|
|
|
|
line_format = "%" + str(pid_column_width) + "s %s %-" + str(identifier_column_width) + "s"
|
|
name_format = "%-" + str(name_column_width - icon_width) + "s"
|
|
|
|
for app in sorted(applications, key=functools.cmp_to_key(compare_applications)):
|
|
if icon_width != 0:
|
|
icons = app.parameters.get("icons", None)
|
|
if icons is not None:
|
|
icon = self._render_icon(icons[0])
|
|
else:
|
|
icon = " "
|
|
name = icon + " " + name_format % app.name
|
|
else:
|
|
name = name_format % app.name
|
|
|
|
if app.pid == 0:
|
|
self._print(line_format % ("-", name, app.identifier))
|
|
else:
|
|
self._print(line_format % (app.pid, name, app.identifier))
|
|
|
|
elif self._include_all_applications:
|
|
self._log("error", "No installed applications.")
|
|
else:
|
|
self._log("error", "No running applications.")
|
|
elif self._output_format == "json":
|
|
result = []
|
|
if len(applications) > 0:
|
|
for app in sorted(applications, key=functools.cmp_to_key(compare_applications)):
|
|
result.append({"pid": (app.pid or None), "name": app.name, "identifier": app.identifier})
|
|
self._print(json.dumps(result, sort_keys=False, indent=2))
|
|
|
|
self._exit(0)
|
|
|
|
def _render_icon(self, icon) -> str:
|
|
return "\033]1337;File=inline=1;width={}px;height={}px;:{}\007".format(
|
|
self._icon_size, self._icon_size, b64encode(icon["image"]).decode("ascii")
|
|
)
|
|
|
|
def _detect_terminal(self) -> Tuple[str, int]:
|
|
icon_size = 0
|
|
|
|
if not self._have_terminal or self._plain_terminal or platform.system() != "Darwin":
|
|
return ("simple", icon_size)
|
|
|
|
fd = sys.stdin.fileno()
|
|
old_attributes = termios.tcgetattr(fd)
|
|
try:
|
|
tty.setraw(fd)
|
|
new_attributes = termios.tcgetattr(fd)
|
|
new_attributes[3] = new_attributes[3] & ~termios.ICANON & ~termios.ECHO
|
|
termios.tcsetattr(fd, termios.TCSANOW, new_attributes)
|
|
|
|
sys.stdout.write("\033[1337n")
|
|
sys.stdout.write("\033[5n")
|
|
sys.stdout.flush()
|
|
|
|
response = self._read_terminal_response("n")
|
|
if response not in ("0", "3"):
|
|
self._read_terminal_response("n")
|
|
|
|
if response.startswith("ITERM2 "):
|
|
version_tokens = response.split(" ", 1)[1].split(".", 2)
|
|
if len(version_tokens) >= 2 and int(version_tokens[0]) >= 3:
|
|
sys.stdout.write("\033[14t")
|
|
sys.stdout.flush()
|
|
height_in_pixels = int(self._read_terminal_response("t").split(";")[1])
|
|
|
|
sys.stdout.write("\033[18t")
|
|
sys.stdout.flush()
|
|
height_in_cells = int(self._read_terminal_response("t").split(";")[1])
|
|
|
|
icon_size = math.ceil((height_in_pixels / height_in_cells) * 1.77)
|
|
|
|
return ("iterm2", icon_size)
|
|
|
|
return ("simple", icon_size)
|
|
finally:
|
|
termios.tcsetattr(fd, termios.TCSANOW, old_attributes)
|
|
|
|
def _read_terminal_response(self, terminator: str) -> str:
|
|
sys.stdin.read(1)
|
|
sys.stdin.read(1)
|
|
result = ""
|
|
while True:
|
|
ch = sys.stdin.read(1)
|
|
if ch == terminator:
|
|
break
|
|
result += ch
|
|
return result
|
|
|
|
def compare_applications(a: _frida.Application, b: _frida.Application) -> int:
|
|
a_is_running = a.pid != 0
|
|
b_is_running = b.pid != 0
|
|
if a_is_running == b_is_running:
|
|
if a.name > b.name:
|
|
return 1
|
|
elif a.name < b.name:
|
|
return -1
|
|
else:
|
|
return 0
|
|
elif a_is_running:
|
|
return -1
|
|
else:
|
|
return 1
|
|
|
|
def compare_processes(a: _frida.Process, b: _frida.Process) -> int:
|
|
a_has_icon = "icons" in a.parameters
|
|
b_has_icon = "icons" in b.parameters
|
|
if a_has_icon == b_has_icon:
|
|
if a.name > b.name:
|
|
return 1
|
|
elif a.name < b.name:
|
|
return -1
|
|
else:
|
|
return 0
|
|
elif a_has_icon:
|
|
return -1
|
|
else:
|
|
return 1
|
|
|
|
def compute_icon_width(item: Union[_frida.Application, _frida.Process]) -> int:
|
|
for icon in item.parameters.get("icons", []):
|
|
if icon["format"] == "png":
|
|
return 4
|
|
return 0
|
|
|
|
app = PSApplication()
|
|
app.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
pass
|