import argparse import codecs import os import platform from typing import Dict, List, Tuple import frida from frida_tools.application import ConsoleApplication def main() -> None: app = CreatorApplication() app.run() class CreatorApplication(ConsoleApplication): def _usage(self) -> str: return "%(prog)s [options] -t agent|cmodule" def _add_options(self, parser: argparse.ArgumentParser) -> None: default_project_name = os.path.basename(os.getcwd()) parser.add_argument( "-n", "--project-name", help="project name", dest="project_name", default=default_project_name ) parser.add_argument("-o", "--output-directory", help="output directory", dest="outdir", default=".") parser.add_argument("-t", "--template", help="template file: cmodule|agent", dest="template", default=None) def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: parsed_args = parser.parse_args() if not parsed_args.template: parser.error("template must be specified") impl = getattr(self, "_generate_" + parsed_args.template, None) if impl is None: parser.error("unknown template type") self._generate = impl self._project_name = options.project_name self._outdir = options.outdir def _needs_device(self) -> bool: return False def _start(self) -> None: (assets, message) = self._generate() outdir = self._outdir for name, data in assets.items(): asset_path = os.path.join(outdir, name) asset_dir = os.path.dirname(asset_path) try: os.makedirs(asset_dir) except: pass with codecs.open(asset_path, "wb", "utf-8") as f: f.write(data) self._print("Created", asset_path) self._print("\n" + message) self._exit(0) def _generate_agent(self) -> Tuple[Dict[str, str], str]: assets = {} assets[ "package.json" ] = f"""{{ "name": "{self._project_name}-agent", "version": "1.0.0", "description": "Frida agent written in TypeScript", "private": true, "main": "agent/index.ts", "scripts": {{ "prepare": "npm run build", "build": "frida-compile agent/index.ts -o _agent.js -c", "watch": "frida-compile agent/index.ts -o _agent.js -w" }}, "devDependencies": {{ "@types/frida-gum": "^18.3.1", "@types/node": "^18.14.0", "frida-compile": "^16.1.8" }} }} """ assets[ "tsconfig.json" ] = """\ { "compilerOptions": { "target": "es2020", "lib": ["es2020"], "allowJs": true, "noEmit": true, "strict": true, "esModuleInterop": true, "moduleResolution": "node16" } } """ assets[ "agent/index.ts" ] = """\ import { log } from "./logger.js"; const header = Memory.alloc(16); header .writeU32(0xdeadbeef).add(4) .writeU32(0xd00ff00d).add(4) .writeU64(uint64("0x1122334455667788")); log(hexdump(header.readByteArray(16) as ArrayBuffer, { ansi: true })); Process.getModuleByName("libSystem.B.dylib") .enumerateExports() .slice(0, 16) .forEach((exp, index) => { log(`export ${index}: ${exp.name}`); }); Interceptor.attach(Module.getExportByName(null, "open"), { onEnter(args) { const path = args[0].readUtf8String(); log(`open() path="${path}"`); } }); """ assets[ "agent/logger.ts" ] = """\ export function log(message: string): void { console.log(message); } """ assets[".gitignore"] = "/node_modules/\n" message = """\ Run `npm install` to bootstrap, then: - Keep one terminal running: npm run watch - Inject agent using the REPL: frida Calculator -l _agent.js - Edit agent/*.ts - REPL will live-reload on save Tip: Use an editor like Visual Studio Code for code completion, inline docs, instant type-checking feedback, refactoring tools, etc. """ return (assets, message) def _generate_cmodule(self) -> Tuple[Dict[str, str], str]: assets = {} assets[ "meson.build" ] = f"""\ project('{self._project_name}', 'c', default_options: 'buildtype=release', ) shared_module('{self._project_name}', '{self._project_name}.c', name_prefix: '', include_directories: include_directories('include'), ) """ assets[ self._project_name + ".c" ] = """\ #include static void frida_log (const char * format, ...); extern void _frida_log (const gchar * message); void init (void) { frida_log ("init()"); } void finalize (void) { frida_log ("finalize()"); } void on_enter (GumInvocationContext * ic) { gpointer arg0; arg0 = gum_invocation_context_get_nth_argument (ic, 0); frida_log ("on_enter() arg0=%p", arg0); } void on_leave (GumInvocationContext * ic) { gpointer retval; retval = gum_invocation_context_get_return_value (ic); frida_log ("on_leave() retval=%p", retval); } static void frida_log (const char * format, ...) { gchar * message; va_list args; va_start (args, format); message = g_strdup_vprintf (format, args); va_end (args); _frida_log (message); g_free (message); } """ assets[".gitignore"] = "/build/\n" session = frida.attach(0) script = session.create_script("rpc.exports.getBuiltins = () => CModule.builtins;") self._on_script_created(script) script.load() builtins = script.exports_sync.get_builtins() script.unload() session.detach() for name, data in builtins["headers"].items(): assets["include/" + name] = data system = platform.system() if system == "Windows": module_extension = "dll" elif system == "Darwin": module_extension = "dylib" else: module_extension = "so" cmodule_path = os.path.join(self._outdir, "build", self._project_name + "." + module_extension) message = f"""\ Run `meson build && ninja -C build` to build, then: - Inject CModule using the REPL: frida Calculator -C {cmodule_path} - Edit *.c, and build incrementally through `ninja -C build` - REPL will live-reload whenever {cmodule_path} changes on disk """ return (assets, message) if __name__ == "__main__": try: main() except KeyboardInterrupt: pass