UNPKG

frida-js

Version:

Pure-JS bindings to control Frida from node.js & browsers.

398 lines (338 loc) 12.4 kB
import type * as stream from 'stream'; import WebSocket = require('isomorphic-ws'); import createWebSocketStream = require('@httptoolkit/websocket-stream'); import dbus = require('@httptoolkit/dbus-native'); import { DBusVariantDict, NestedStringDict, parseDBusVariantDict } from './dbus-value'; export { getFridaReleaseDetails, calculateFridaSRI, downloadFridaServer } from './download-frida'; const DEFAULT_FRIDA_PORT = 27042; const connectFridaWebSocket = async (fridaHost: string, options?: { createConnection?: () => stream.Duplex }) => { const socket = new WebSocket(`ws://${fridaHost}/ws`, options); socket.binaryType = 'arraybuffer'; await new Promise((resolve, reject) => { socket.addEventListener('open', resolve); socket.addEventListener('error', reject); }); return socket; } export async function connect(options: | { host?: string, stream?: undefined } // Note that providing a stream directly is supported on Node only | { stream?: stream.Duplex, host?: undefined }= {} ) { const fridaHost = options.host || `127.0.0.1:${DEFAULT_FRIDA_PORT}`; const webSocket = options.stream ? await connectFridaWebSocket(fridaHost, { createConnection: () => options.stream! }) : await connectFridaWebSocket(fridaHost); const bus = dbus.createClient({ stream: createWebSocketStream(webSocket), direct: true, authMethods: [] }); return new FridaSession(bus); } interface HostSession { QuerySystemParameters(): Promise<DBusVariantDict>; EnumerateProcesses(arg: {}): Promise<Array<[pid: number, name: string]>>; EnumerateApplications(arg: {}): Promise<Array<[id: string, name: string, pid: number | 0]>>; Attach(pid: number, options: {}): Promise<[string]>; Spawn(program: string, options: [ hasArgv: boolean, argv: string[], hasEnvP: boolean, envp: string[], hasEnv: boolean, env: string[], cwd: string, stdio: number, aux: [] ]): Promise<number>; Resume(pid: number): Promise<void>; Kill(pid: number): Promise<void>; } interface AgentSession { CreateScript(script: string, options: {}): Promise<[number]>; LoadScript(scriptId: [number]): Promise<void>; PostMessages(messages: [AgentMessage], batchId: number): Promise<void>; } /** * A message from a Frida script to the runner. * https://github.com/frida/frida-core/blob/main/lib/base/session.vala#L124C2-L146C3 * kind is the AgentMessageKind, "1" is a script message. There is also Debugger but no enum number is specified. * script_id is the script id that sent the message. It is part of the AgentScriptId type. * text is the message in plain text. * has_data is a boolean that indicates if there is data attached to the message. * data is the data attached to the message. It is a byte array. */ type AgentMessage = [kind: number, script_id: number[], text: string, has_data: boolean, data: Buffer | null] /** * A message sent from a script to the agent. * https://github.com/frida/frida-node/blob/main/lib/script.ts#L103-L115 */ export enum MessageType { Send = "send", Error = "error", Log = "log" } export type Message = | ScriptAgentSendMessage | ScriptAgentErrorMessage | ScriptAgentLogMessage; export interface ScriptAgentSendMessage { type: MessageType.Send; payload: any; } export interface ScriptAgentErrorMessage { type: MessageType.Error; description: string; stack?: string; fileName?: string; lineNumber?: number; columnNumber?: number; } export interface ScriptAgentLogMessage { type: MessageType.Log, level: string, payload: string } enum AgentMessageKind { Script = 1, Debugger = 2, } const SUPPORTED_API_VERSIONS = ['17', '16']; export class FridaSession { constructor( private bus: dbus.DBusClient ) {} /** * Disconnect from Frida. Returns a promise that resolves once the connection has been closed. */ async disconnect() { return this.bus.disconnect(); } private async getHostSession() { for (let version of SUPPORTED_API_VERSIONS) { const hostSession = await this.bus .getService(`re.frida.HostSession${version}`) .getInterface<HostSession | undefined>('/re/frida/HostSession', `re.frida.HostSession${version}`); if (hostSession) { return hostSession; } } throw new Error('Could not create Frida host session (Unsupported API version?)'); } private async getAgentSession(sessionId: string, pid: number, hostSession: HostSession) { for (let version of SUPPORTED_API_VERSIONS) { const agentSession = await this.bus .getService(`re.frida.AgentSession${version}`) .getInterface<AgentSession>('/re/frida/AgentSession/' + sessionId, `re.frida.AgentSession${version}`); if (agentSession) { return new FridaAgentSession(this.bus, version, hostSession, pid, sessionId, agentSession); } } throw new Error('Could not create Frida agent session (Unsupported API version?)'); } /** * Query the system parameters of the target Frida server. Returns metadata * as a nested dictionary of strings. */ async queryMetadata(): Promise<NestedStringDict> { const hostSession = await this.getHostSession(); const rawMetadata = await hostSession.QuerySystemParameters(); return parseDBusVariantDict(rawMetadata); } /** * List all running processes accessible to the target Frida server. Returns an array * of { pid, name } objects. */ async enumerateProcesses(): Promise<Array<{ pid: number, name: string }>> { const hostSession = await this.getHostSession(); return (await hostSession.EnumerateProcesses({})).map((proc) => ({ pid: proc[0], name: proc[1] })); } /** * List all installed applications accessible on the target Frida server. Returns an array of * { pid, id, name } objects, where pid is null if the application is not currently running. * * This is only applicable to mobile devices, and will return an empty array everywhere else. */ async enumerateApplications(): Promise<Array<{ pid: number | null, id: string, name: string }>> { const hostSession = await this.getHostSession(); return (await hostSession.EnumerateApplications({})).map((proc) => ({ pid: proc[2] || null, // Not running = 0. We map it to null. id: proc[0], name: proc[1] })); } async attachToProcess(pid: number) { const hostSession = await this.getHostSession(); const [sessionId] = await hostSession.Attach(pid, {}); const agentSession = await this.getAgentSession(sessionId, pid, hostSession); return { session: agentSession }; } /** * Attach to a given pid and inject a Frida script to manipulate the target program. * * Whether you can attach to the process may depend on system configuration. For * Linux specifically, if the process is not a child of your own process, you may * need to run `sudo sysctl kernel.yama.ptrace_scope=0` first. */ async injectIntoProcess(pid: number, fridaScript: string) { const { session } = await this.attachToProcess(pid); const script = await session.createScript(fridaScript); setTimeout(async () => { try { await script.loadScript(); } catch (e) { console.warn(e); } }, 0); return { session, script } } /** * Run arbitrary Node.js code directly within a target Node process. The given * code string will be wrapped with a Frida hook that injects it directly into * the event loop, so it will run immediately. */ async injectIntoNodeJSProcess(pid: number, nodeScript: string) { const fridaScript = require('../scripts/node-js-inject.js') .buildNodeJsInjectionScript(nodeScript); return this.injectIntoProcess(pid, fridaScript); } async spawnPaused(command: string, args: string[] | undefined) { const hostSession = await this.getHostSession(); const argOptions: [boolean, Array<string>] = args ? [true, [command, ...args]] : [false, []]; const pid = await hostSession.Spawn(command, [ ...argOptions, false, [], false, [], "", 0, [] ]); const [sessionId] = await hostSession.Attach(pid, {}); const agentSession = await this.getAgentSession(sessionId, pid, hostSession); return { pid, session: agentSession } } async spawnWithScript(command: string, args: string[] | undefined, fridaScript: string) { const { session, pid } = await this.spawnPaused(command, args); const script = await session.createScript(fridaScript); setTimeout(async () => { try { await script.loadScript(); await session.resume(); } catch (e) { console.warn(e); } }, 0); return { pid, session, script } } } export class FridaAgentSession { constructor( private bus: dbus.DBusClient, private hostVersion: string, private hostSession: HostSession, private pid: number, private sessionId: string, private agentSession: AgentSession, ) {} /** * This method sets up a message handler for messages sent from the agent. * @param cb Callback to be called when a message is received from the agent. */ onMessage(cb: (message: Message) => void) { this.bus.setMethodCallHandler( `/re/frida/AgentMessageSink/${this.sessionId}`, `re.frida.AgentMessageSink${this.hostVersion}`, "PostMessages", [(messages: AgentMessage[]) => { for(const message of messages) { const msg = JSON.parse(message[2]) as Message; switch(message[0]) { // message[0] is the message kind case AgentMessageKind.Script: cb(msg) break; } } }, null] ); } /** * Create a new Frida script within this agent session. * @param script The Frida script in plain text to create. * @param options Options to pass to the script. */ async createScript(script: string, options: {} = {}): Promise<FridaScript> { const [scriptId] = await this.agentSession.CreateScript(script, options); return new FridaScript(this.bus, this.agentSession, [scriptId]); } resume() { return this.hostSession.Resume(this.pid); } kill() { return this.hostSession.Kill(this.pid); } } const ZERO_LENGTH_BUFFER = Buffer.alloc(0); export class FridaScript { constructor( private bus: dbus.DBusClient, private agentSession: AgentSession, private scriptId: [number], ) {} /** * Load the script into the target process. * @returns Promise that resolves when the script is loaded. */ async loadScript() { return this.agentSession.LoadScript(this.scriptId); } /** * Send a message to the script. * @param message - The message object to send, will be JSON stringified. * @param data - Optional binary data to send along with the message. * @returns Promise that resolves when the message is posted. */ async post(message: any, data?: Buffer | null): Promise<void> { return this.agentSession.PostMessages([ [ AgentMessageKind.Script, this.scriptId, JSON.stringify(message), data != null, data ?? ZERO_LENGTH_BUFFER, ] ], 0); } }