From 1ae7c5712dec1b22e7ba176821371be8a05dea0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Andr=C3=A9=20Vadla=20Ravn=C3=A5s?= Date: Sat, 21 Sep 2024 00:50:27 +0200 Subject: [PATCH] tracer: Add static analysis UI features powered by r2 - Support capturing backtraces, enabled on a per handler basis. - Show event's caller (or backtrace, if available), and support disassembling each location. - Support adding an instruction-level probe by clicking on an address in the disassembly view. - Support jumping to the handler code by clicking on an address in the disassembly view. - Improve styling. --- agents/tracer/agent.ts | 345 +++++++++++++++------ apps/tracer/package-lock.json | 27 ++ apps/tracer/package.json | 2 + apps/tracer/src/AddTargetsDialog.tsx | 4 + apps/tracer/src/App.css | 45 ++- apps/tracer/src/App.tsx | 315 ++++++++----------- apps/tracer/src/DisassemblyView.css | 18 ++ apps/tracer/src/DisassemblyView.tsx | 250 +++++++++++++++ apps/tracer/src/EventView.css | 74 ++++- apps/tracer/src/EventView.tsx | 153 ++++++++-- apps/tracer/src/HandlerEditor.tsx | 38 ++- apps/tracer/src/HandlerList.tsx | 6 +- apps/tracer/src/index.css | 1 + apps/tracer/src/model.ts | 439 ++++++++++++++++++++++++++- apps/tracer/vite.config.ts | 27 +- frida_tools/tracer.py | 230 +++++++++----- 16 files changed, 1569 insertions(+), 405 deletions(-) create mode 100644 apps/tracer/src/DisassemblyView.css create mode 100644 apps/tracer/src/DisassemblyView.tsx diff --git a/agents/tracer/agent.ts b/agents/tracer/agent.ts index 1c35938f..59bba212 100644 --- a/agents/tracer/agent.ts +++ b/agents/tracer/agent.ts @@ -1,5 +1,8 @@ +const MAX_HANDLERS_PER_REQUEST = 1000; + class Agent { private handlers = new Map(); + private nativeTargets = new Set(); private stagedPlanRequest: TracePlanRequest | null = null; private stackDepth = new Map(); private traceState: TraceState = {}; @@ -24,7 +27,7 @@ class Agent { try { (1, eval)(script.source); } catch (e: any) { - throw new Error(`Unable to load ${script.filename}: ${e.stack}`); + throw new Error(`unable to load ${script.filename}: ${e.stack}`); } } @@ -34,21 +37,44 @@ class Agent { message: e.message }); }); + + return { + id: Process.id, + platform: Process.platform, + arch: Process.arch, + pointer_size: Process.pointerSize, + page_size: Process.pageSize, + main_module: Process.mainModule, + }; } dispose() { this.flush(); } - update(id: TraceTargetId, name: string, script: HandlerScript) { + updateHandlerCode(id: TraceTargetId, name: string, script: HandlerScript) { const handler = this.handlers.get(id); if (handler === undefined) { - throw new Error("Invalid target ID"); + throw new Error("invalid target ID"); } - const newHandler = this.parseHandler(name, script); - handler[0] = newHandler[0]; - handler[1] = newHandler[1]; + if (handler.length === 3) { + const newHandler = this.parseFunctionHandler(script, id, name, this.onTraceError); + handler[0] = newHandler[0]; + handler[1] = newHandler[1]; + } else { + const newHandler = this.parseInstructionHandler(script, id, name, this.onTraceError); + handler[0] = newHandler[0]; + } + } + + updateHandlerConfig(id: TraceTargetId, config: HandlerConfig) { + const handler = this.handlers.get(id); + if (handler === undefined) { + throw new Error("invalid target ID"); + } + + handler[2] = config; } async stageTargets(spec: TraceSpec): Promise { @@ -75,7 +101,7 @@ class Agent { return items; } - async commitTargets(id: StagedItemId | null): Promise { + async commitTargets(id: StagedItemId | null): Promise { const request = this.stagedPlanRequest!; this.stagedPlanRequest = null; @@ -84,18 +110,54 @@ class Agent { plan = this.cropStagedPlan(plan, id); } - const nativeIds = await this.traceNativeTargets(plan.native); + const errorEvents: TraceError[] = []; + const onError: TraceErrorEventHandler = e => { + errorEvents.push(e); + }; + + const nativeIds = await this.traceNativeTargets(plan.native, onError); let javaIds: TraceTargetId[] = []; if (plan.java.length !== 0) { javaIds = await new Promise((resolve, reject) => { Java.perform(() => { - this.traceJavaTargets(plan.java).then(resolve, reject); + this.traceJavaTargets(plan.java, onError).then(resolve, reject); }); }); } - return [...nativeIds, ...javaIds]; + return { + ids: [...nativeIds, ...javaIds], + errors: errorEvents, + }; + } + + readMemory(address: string, size: number): ArrayBuffer | null { + try { + return ptr(address).readByteArray(size); + } catch (e) { + return null; + } + } + + resolveAddresses(addresses: string[]): string[] { + let cachedModules: ModuleMap | null = null; + return addresses + .map(ptr) + .map(DebugSymbol.fromAddress) + .map(sym => { + if (sym.name === null) { + if (cachedModules === null) { + cachedModules = new ModuleMap(); + } + const module = cachedModules.find(sym.address); + if (module !== null) { + return `${module.name}!${sym.address.sub(module.base)}`; + } + } + return sym; + }) + .map(s => s.toString()); } private cropStagedPlan(plan: TracePlan, id: StagedItemId): TracePlan { @@ -110,10 +172,9 @@ class Agent { const croppedMethods = new Map([[methodName, methodNameOrSignature]]); const croppedClass: JavaTargetClass = { methods: croppedMethods }; const croppedGroup: JavaTargetGroup = { loader: group.loader, classes: new Map([[className, croppedClass]]) }; - return { - native: new Map(), - java: [croppedGroup], - }; + const croppedPlan = new TracePlan(); + croppedPlan.java.push(croppedGroup); + return croppedPlan; } candidateId--; } @@ -123,10 +184,9 @@ class Agent { candidateId = 1; for (const [k, v] of plan.native.entries()) { if (candidateId === id) { - return { - native: new Map([[k, v]]), - java: [], - }; + const croppedPlan = new TracePlan(); + croppedPlan.native.set(k, v); + return croppedPlan; } candidateId++; } @@ -137,12 +197,12 @@ class Agent { private async start(spec: TraceSpec) { const onJavaReady = async (plan: TracePlan) => { - await this.traceJavaTargets(plan.java); + await this.traceJavaTargets(plan.java, this.onTraceError); }; const request = await this.createPlan(spec, onJavaReady); - await this.traceNativeTargets(request.plan.native); + await this.traceNativeTargets(request.plan.native, this.onTraceError); send({ type: "agent:initialized" @@ -156,12 +216,17 @@ class Agent { }); } + private onTraceError: TraceErrorEventHandler = ({ id, name, message }) => { + send({ + type: "agent:warning", + id, + message: `Skipping "${name}": ${message}` + }); + }; + private async createPlan(spec: TraceSpec, onJavaReady: (plan: TracePlan) => Promise = async () => {}): Promise { - const plan: TracePlan = { - native: new Map(), - java: [] - }; + const plan = new TracePlan(); const javaEntries: [TraceSpecOperation, TraceSpecPattern][] = []; for (const [operation, scope, pattern] of spec) { @@ -185,6 +250,11 @@ class Agent { this.includeRelativeFunction(pattern, plan); } break; + case "absolute-instruction": + if (operation === "include") { + this.includeAbsoluteInstruction(ptr(pattern), plan); + } + break; case "imports": if (operation === "include") { this.includeImports(pattern, plan); @@ -215,6 +285,12 @@ class Agent { } } + for (const address of plan.native.keys()) { + if (this.nativeTargets.has(address)) { + plan.native.delete(address); + } + } + let javaStartRequest: Promise; let javaStartDeferred = true; if (javaEntries.length > 0) { @@ -254,7 +330,8 @@ class Agent { return { plan, ready: javaStartRequest }; } - private async traceNativeTargets(targets: NativeTargets): Promise { + private async traceNativeTargets(targets: NativeTargets, onError: TraceErrorEventHandler): Promise { + const insnGroups = new Map(); const cGroups = new Map(); const objcGroups = new Map(); const swiftGroups = new Map(); @@ -262,6 +339,9 @@ class Agent { for (const [id, [type, scope, name]] of targets.entries()) { let entries: Map; switch (type) { + case "insn": + entries = insnGroups; + break; case "c": entries = cGroups; break; @@ -283,15 +363,17 @@ class Agent { } const [cIds, objcIds, swiftIds] = await Promise.all([ - this.traceNativeEntries("c", cGroups), - this.traceNativeEntries("objc", objcGroups), - this.traceNativeEntries("swift", swiftGroups), + this.traceNativeEntries("insn", insnGroups, onError), + this.traceNativeEntries("c", cGroups, onError), + this.traceNativeEntries("objc", objcGroups, onError), + this.traceNativeEntries("swift", swiftGroups, onError), ]); return [...cIds, ...objcIds, ...swiftIds]; } - private async traceNativeEntries(flavor: "c" | "objc" | "swift", groups: NativeTargetScopes): Promise { + private async traceNativeEntries(flavor: NativeTargetFlavor, groups: NativeTargetScopes, onError: TraceErrorEventHandler): + Promise { if (groups.size === 0) { return []; } @@ -307,7 +389,8 @@ class Agent { for (const [name, items] of groups.entries()) { scopes.push({ name, - members: items.map(item => item[0]) + members: items.map(item => item[0]), + addresses: items.map(item => item[1].toString()) }); this.nextId += items.length; } @@ -316,21 +399,24 @@ class Agent { const ids: TraceTargetId[] = []; let offset = 0; + const isInstruction = flavor === "insn"; for (const items of groups.values()) { for (const [name, address] of items) { const id = baseId + offset; const displayName = (typeof name === "string") ? name : name[1]; - const handler = this.parseHandler(displayName, scripts[offset]); + const handler = isInstruction + ? this.parseInstructionHandler(scripts[offset], id, displayName, onError) + : this.parseFunctionHandler(scripts[offset], id, displayName, onError); this.handlers.set(id, handler); + this.nativeTargets.add(address.toString()); try { - Interceptor.attach(address, this.makeNativeListenerCallbacks(id, handler)); - } catch (e: any) { - send({ - type: "agent:warning", - message: `Skipping "${name}": ${e.message}` - }); + Interceptor.attach(address, isInstruction + ? this.makeNativeInstructionListener(id, handler as TraceInstructionHandler) + : this.makeNativeFunctionListener(id, handler as TraceFunctionHandler)); + } catch (e) { + onError({ id, name: displayName, message: (e as Error).message }); } ids.push(id); @@ -340,7 +426,7 @@ class Agent { return ids; } - private async traceJavaTargets(groups: JavaTargetGroup[]): Promise { + private async traceJavaTargets(groups: JavaTargetGroup[], onError: TraceErrorEventHandler): Promise { const baseId = this.nextId; const scopes: HandlerRequestScope[] = []; const request: HandlerRequest = { @@ -377,7 +463,7 @@ class Agent { for (const [bareName, fullName] of methods.entries()) { const id = baseId + offset; - const handler = this.parseHandler(fullName, scripts[offset]); + const handler = this.parseFunctionHandler(scripts[offset], id, fullName, onError); this.handlers.set(id, handler); const dispatcher: Java.MethodDispatcher = C[bareName]; @@ -396,20 +482,31 @@ class Agent { }); } - private makeNativeListenerCallbacks(id: TraceTargetId, handler: TraceHandler): InvocationListenerCallbacks { + private makeNativeFunctionListener(id: TraceTargetId, handler: TraceFunctionHandler): InvocationListenerCallbacks { const agent = this; return { onEnter(args) { - agent.invokeNativeHandler(id, handler[0], this, args, ">"); + const [onEnter, _, config] = handler; + agent.invokeNativeHandler(id, onEnter, config, this, args, ">"); }, onLeave(retval) { - agent.invokeNativeHandler(id, handler[1], this, retval, "<"); + const [_, onLeave, config] = handler; + agent.invokeNativeHandler(id, onLeave, config, this, retval, "<"); } }; } - private makeJavaMethodWrapper(id: TraceTargetId, method: Java.Method, handler: TraceHandler): Java.MethodImplementation { + private makeNativeInstructionListener(id: TraceTargetId, handler: TraceInstructionHandler): InstructionProbeCallback { + const agent = this; + + return function (args) { + const [onHit, config] = handler; + agent.invokeNativeHandler(id, onHit, config, this, args, "|"); + }; + } + + private makeJavaMethodWrapper(id: TraceTargetId, method: Java.Method, handler: TraceFunctionHandler): Java.MethodImplementation { const agent = this; return function (...args: any[]) { @@ -417,35 +514,41 @@ class Agent { }; } - private handleJavaInvocation(id: TraceTargetId, method: Java.Method, handler: TraceHandler, instance: Java.Wrapper, args: any[]): any { - this.invokeJavaHandler(id, handler[0], instance, args, ">"); + private handleJavaInvocation(id: TraceTargetId, method: Java.Method, handler: TraceFunctionHandler, instance: Java.Wrapper, args: any[]): any { + const [onEnter, onLeave, config] = handler; + + this.invokeJavaHandler(id, onEnter, config, instance, args, ">"); const retval = method.apply(instance, args); - const replacementRetval = this.invokeJavaHandler(id, handler[1], instance, retval, "<"); + const replacementRetval = this.invokeJavaHandler(id, onLeave, config, instance, retval, "<"); return (replacementRetval !== undefined) ? replacementRetval : retval; } - private invokeNativeHandler(id: TraceTargetId, callback: TraceEnterHandler | TraceLeaveHandler, context: InvocationContext, param: any, cutPoint: CutPoint) { + private invokeNativeHandler(id: TraceTargetId, callback: TraceEnterHandler | TraceLeaveHandler | TraceProbeHandler, + config: HandlerConfig, context: InvocationContext, param: any, cutPoint: CutPoint) { const timestamp = Date.now() - this.started; const threadId = context.threadId; const depth = this.updateDepth(threadId, cutPoint); + const caller = context.returnAddress.toString(); + const backtrace = config.capture_backtraces ? Thread.backtrace(context.context).map(p => p.toString()) : null; const log = (...message: string[]) => { - this.emit([id, timestamp, threadId, depth, message.join(" ")]); + this.emit([id, timestamp, threadId, depth, caller, backtrace, message.join(" ")]); }; callback.call(context, log, param, this.traceState); } - private invokeJavaHandler(id: TraceTargetId, callback: TraceEnterHandler | TraceLeaveHandler, instance: Java.Wrapper, param: any, cutPoint: CutPoint) { + private invokeJavaHandler(id: TraceTargetId, callback: TraceEnterHandler | TraceLeaveHandler, config: HandlerConfig, + instance: Java.Wrapper, param: any, cutPoint: CutPoint) { const timestamp = Date.now() - this.started; const threadId = Process.getCurrentThreadId(); const depth = this.updateDepth(threadId, cutPoint); const log = (...message: string[]) => { - this.emit([id, timestamp, threadId, depth, message.join(" ")]); + this.emit([id, timestamp, threadId, depth, null, null, message.join(" ")]); }; try { @@ -466,7 +569,7 @@ class Agent { let depth = depthEntries.get(threadId) ?? 0; if (cutPoint === ">") { depthEntries.set(threadId, depth + 1); - } else { + } else if (cutPoint === "<") { depth--; if (depth !== 0) { depthEntries.set(threadId, depth); @@ -478,20 +581,32 @@ class Agent { return depth; } - private parseHandler(name: string, script: string): TraceHandler { - const id = `/handlers/${name}.js`; + private parseFunctionHandler(script: string, id: TraceTargetId, name: string, onError: TraceErrorEventHandler): TraceFunctionHandler { try { - const h = Script.evaluate(id, script); - return [h.onEnter ?? noop, h.onLeave ?? noop]; - } catch (e: any) { - send({ - type: "agent:warning", - message: `${id}: ${e.message}` - }); - return [noop, noop]; + const h = this.parseHandlerScript(name, script); + return [h.onEnter ?? noop, h.onLeave ?? noop, makeDefaultHandlerConfig()]; + } catch (e) { + onError({ id, name, message: (e as Error).message }); + return [noop, noop, makeDefaultHandlerConfig()]; } } + private parseInstructionHandler(script: string, id: TraceTargetId, name: string, onError: TraceErrorEventHandler): + TraceInstructionHandler { + try { + const onHit = this.parseHandlerScript(name, script); + return [onHit, makeDefaultHandlerConfig()]; + } catch (e) { + onError({ id, name, message: (e as Error).message }); + return [noop, makeDefaultHandlerConfig()]; + } + } + + private parseHandlerScript(name: string, script: string): any { + const id = `/handlers/${name}.js`; + return Script.evaluate(id, script); + } + private includeModule(pattern: string, plan: TracePlan) { const { native } = plan; for (const m of this.getModuleResolver().enumerateMatches(`exports:${pattern}!*`)) { @@ -528,6 +643,15 @@ class Agent { plan.native.set(address.toString(), ["c", e.module, `sub_${e.offset.toString(16)}`]); } + private includeAbsoluteInstruction(address: NativePointer, plan: TracePlan) { + const module = plan.modules.find(address); + if (module !== null) { + plan.native.set(address.toString(), ["insn", module.path, `insn_${address.sub(module.base).toString(16)}`]); + } else { + plan.native.set(address.toString(), ["insn", "", `insn_${address.toString(16)}`]); + } + } + private includeImports(pattern: string, plan: TracePlan) { let matches: ApiResolverMatch[]; if (pattern === null) { @@ -727,10 +851,11 @@ async function getHandlers(request: HandlerRequest): Promise { const { type, flavor, baseId } = request; - const pendingScopes = request.scopes.slice().map(({ name, members }) => { + const pendingScopes = request.scopes.slice().map(({ name, members, addresses }) => { return { name, - members: members.slice() + members: members.slice(), + addresses: addresses?.slice(), }; }); let id = baseId; @@ -744,29 +869,17 @@ async function getHandlers(request: HandlerRequest): Promise { }; let size = 0; - for (const { name, members: pendingMembers } of pendingScopes) { - const curMembers: MemberName[] = []; + for (const { name, members: pendingMembers, addresses: pendingAddresses } of pendingScopes) { + const n = Math.min(pendingMembers.length, MAX_HANDLERS_PER_REQUEST - size); + if (n === 0) { + break; + } curScopes.push({ name, - members: curMembers + members: pendingMembers.splice(0, n), + addresses: pendingAddresses?.splice(0, n), }); - - let exhausted = false; - for (const member of pendingMembers) { - curMembers.push(member); - - size++; - if (size === 1000) { - exhausted = true; - break; - } - } - - pendingMembers.splice(0, curMembers.length); - - if (exhausted) { - break; - } + size += n; } while (pendingScopes.length !== 0 && pendingScopes[0].members.length === 0) { @@ -786,6 +899,12 @@ async function getHandlers(request: HandlerRequest): Promise { }; } +function makeDefaultHandlerConfig(): HandlerConfig { + return { + capture_backtraces: false, + }; +} + function receiveResponse(type: string): Promise { return new Promise(resolve => { recv(type, (response: T) => { @@ -914,6 +1033,7 @@ type TraceSpecScope = | "module" | "function" | "relative-function" + | "absolute-instruction" | "imports" | "objc-method" | "swift-func" @@ -927,19 +1047,31 @@ interface TracePlanRequest { ready: Promise; } -interface TracePlan { - native: NativeTargets; - java: JavaTargetGroup[]; +class TracePlan { + native: NativeTargets = new Map(); + java: JavaTargetGroup[] = []; + + #cachedModules: ModuleMap | null = null; + + get modules(): ModuleMap { + let modules = this.#cachedModules; + if (modules === null) { + modules = new ModuleMap(); + this.#cachedModules = modules; + } + return modules; + } } -type TargetType = "c" | "objc" | "swift" | "java"; +type TargetFlavor = NativeTargetFlavor | "java"; type ScopeName = string; type MemberName = string | [string, string]; +type NativeTargetFlavor = "insn" | "c" | "objc" | "swift"; type NativeTargets = Map; -type NativeTarget = ["c" | "objc" | "swift", ScopeName, MemberName]; +type NativeTarget = [type: NativeTargetFlavor, scope: ScopeName, name: MemberName]; type NativeTargetScopes = Map; -type NativeItem = [MemberName, NativePointer]; +type NativeItem = [name: MemberName, address: NativePointer]; type NativeId = string; interface JavaTargetGroup { @@ -953,36 +1085,56 @@ type JavaClassName = string; type JavaMethodName = string; type JavaMethodNameOrSignature = string; +type TraceErrorEventHandler = (error: TraceError) => void; +interface TraceError { + id: TraceTargetId; + name: string; + message: string; +} + type StagedItem = [id: StagedItemId, scope: ScopeName, member: MemberName]; type StagedItemId = number; +interface CommitResult { + ids: TraceTargetId[]; + errors: TraceError[]; +} interface HandlerRequest { type: "handlers:get", - flavor: TargetType; + flavor: TargetFlavor; baseId: TraceTargetId; scopes: HandlerRequestScope[]; } interface HandlerRequestScope { name: string; members: MemberName[]; + addresses?: string[]; } interface HandlerResponse { scripts: HandlerScript[]; } type HandlerScript = string; +interface HandlerConfig { + capture_backtraces: boolean; +} type TraceTargetId = number; -type TraceEvent = [TraceTargetId, Timestamp, ThreadId, Depth, Message]; +type TraceEvent = [TraceTargetId, Timestamp, ThreadId, Depth, Caller, Backtrace, Message]; type Timestamp = number; type Depth = number; +type Caller = string | null; +type Backtrace = string[] | null; type Message = string; -type TraceHandler = [TraceEnterHandler, TraceLeaveHandler]; +type TraceHandler = TraceFunctionHandler | TraceInstructionHandler; +type TraceFunctionHandler = [onEnter: TraceEnterHandler, onLeave: TraceLeaveHandler, config: HandlerConfig]; +type TraceInstructionHandler = [onHit: TraceProbeHandler, config: HandlerConfig]; type TraceEnterHandler = (log: LogHandler, args: any[], state: TraceState) => void; type TraceLeaveHandler = (log: LogHandler, retval: any, state: TraceState) => any; +type TraceProbeHandler = (log: LogHandler, args: any[], state: TraceState) => void; -type CutPoint = ">" | "<"; +type CutPoint = ">" | "|" | "<"; type LogHandler = (...message: string[]) => void; @@ -991,7 +1143,10 @@ const agent = new Agent(); rpc.exports = { init: agent.init.bind(agent), dispose: agent.dispose.bind(agent), - update: agent.update.bind(agent), + updateHandlerCode: agent.updateHandlerCode.bind(agent), + updateHandlerConfig: agent.updateHandlerConfig.bind(agent), stageTargets: agent.stageTargets.bind(agent), - commitTargets: agent.commitTargets.bind(agent,) + commitTargets: agent.commitTargets.bind(agent), + readMemory: agent.readMemory.bind(agent), + resolveAddresses: agent.resolveAddresses.bind(agent), }; diff --git a/apps/tracer/package-lock.json b/apps/tracer/package-lock.json index 9b877f52..7897da18 100644 --- a/apps/tracer/package-lock.json +++ b/apps/tracer/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@blueprintjs/core": "^5.12.0", + "@frida/react-use-r2": "^1.0.2", "@monaco-editor/react": "^4.6.0", "monaco-editor": "^0.51.0", "react": "^18.3.1", @@ -19,6 +20,7 @@ "use-debounce": "^10.0.3" }, "devDependencies": { + "@types/node": "^22.7.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -539,6 +541,14 @@ "node": ">=12" } }, + "node_modules/@frida/react-use-r2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@frida/react-use-r2/-/react-use-r2-1.0.2.tgz", + "integrity": "sha512-Ygx2dLCRYZIoI7PbtDtMenjtfw26PmskqGtIGMm2qyL8lyZgR9XNnUYHWrtpLqO+fvjCtutDbnewIgYP8UcmNg==", + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -727,6 +737,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -1508,6 +1528,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", diff --git a/apps/tracer/package.json b/apps/tracer/package.json index 1da09dc7..e03f4ba8 100644 --- a/apps/tracer/package.json +++ b/apps/tracer/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@blueprintjs/core": "^5.12.0", + "@frida/react-use-r2": "^1.0.2", "@monaco-editor/react": "^4.6.0", "monaco-editor": "^0.51.0", "react": "^18.3.1", @@ -20,6 +21,7 @@ "use-debounce": "^10.0.3" }, "devDependencies": { + "@types/node": "^22.7.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", diff --git a/apps/tracer/src/AddTargetsDialog.tsx b/apps/tracer/src/AddTargetsDialog.tsx index 52331113..71d21225 100644 --- a/apps/tracer/src/AddTargetsDialog.tsx +++ b/apps/tracer/src/AddTargetsDialog.tsx @@ -106,6 +106,8 @@ function labelForTraceSpecScope(scope: TraceSpecScope) { return "Function"; case TraceSpecScope.RelativeFunction: return "Relative Function"; + case TraceSpecScope.AbsoluteInstruction: + return "Instruction"; case TraceSpecScope.Imports: return "All Module Imports"; case TraceSpecScope.Module: @@ -127,6 +129,8 @@ function placeholderForTraceTargetSpecScope(scope: TraceSpecScope) { return "[Module!]Function"; case TraceSpecScope.RelativeFunction: return "Module!Offset"; + case TraceSpecScope.AbsoluteInstruction: + return "0x1234"; case TraceSpecScope.Imports: case TraceSpecScope.Module: return "Module"; diff --git a/apps/tracer/src/App.css b/apps/tracer/src/App.css index bbad2b34..07626e94 100644 --- a/apps/tracer/src/App.css +++ b/apps/tracer/src/App.css @@ -30,20 +30,61 @@ padding: 5px; } -.work-area { +.editor-area { display: flex; flex: 1; flex-direction: column; + white-space: nowrap; } -.work-area section { +.editor-area section { flex: 1; } +.editor-area .bp5-button:focus { + outline: 0; +} + +.editor-area section.editor-toolbar { + display: flex; + justify-content: space-between; + flex: 0; + padding-right: 10px; +} + +.editor-toolbar .bp5-switch { + padding-top: 10px; +} + .monaco-editor { position: absolute !important; } +.bottom-tabs { + display: flex; + flex-direction: column; + flex: 1; +} + +.bottom-tabs .bp5-tab-list { + padding-left: 10px; +} + +.bottom-tabs .bp5-tab:focus { + outline: 0; +} + +.bottom-tab-panel { + display: flex; + margin-top: 0; + flex: 1; + overflow: hidden; +} + .event-view { flex: 1; } + +.disassembly-view { + flex: 1; +} diff --git a/apps/tracer/src/App.tsx b/apps/tracer/src/App.tsx index 68e612b2..078e0941 100644 --- a/apps/tracer/src/App.tsx +++ b/apps/tracer/src/App.tsx @@ -1,195 +1,180 @@ import "./App.css"; import AddTargetsDialog from "./AddTargetsDialog.tsx"; +import DisassemblyView, { type DisassemblyTarget } from "./DisassemblyView.tsx"; import EventView from "./EventView.tsx"; import HandlerEditor from "./HandlerEditor.tsx"; import HandlerList from "./HandlerList.tsx"; -import { - Event, - Handler, - HandlerId, - ScopeId, - StagedItem, - StagedItemId, - TraceSpecScope -} from "./model.js"; +import { useModel } from "./model.js"; import { BlueprintProvider, Callout, Button, ButtonGroup, + Switch, + Tabs, + Tab, } from "@blueprintjs/core"; -import { useEffect, useState } from "react"; -import { Resplit } from 'react-resplit'; -import useWebSocket, { ReadyState } from "react-use-websocket"; +import { useRef, useState } from "react"; +import { Resplit } from "react-resplit"; export default function App() { - const [spawnedProgram, setSpawnedProgram] = useState(null); - const [handlers, setHandlers] = useState([]); - const [selectedScope, setSelectedScope] = useState(""); - const [selectedHandler, setSelectedHandler] = useState(-1); - const [handlerCode, setHandlerCode] = useState(""); - const [draftedCode, setDraftedCode] = useState(""); - const [addingTargets, setAddingTargets] = useState(false); - const [events, setEvents] = useState([]); - const [latestMatchingEventIndex, setLatestMatchingEventIndex] = useState(null); - const [highlightedEventIndex, setHighlightedEventIndex] = useState(null); - const [stagedItems, setStagedItems] = useState([]); - const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket( - (import.meta.env.MODE === "development") - ? "ws://localhost:1337" - : `ws://${window.location.host}`); - - function handleHandlerSelection(id: HandlerId) { - setSelectedHandler(id); - setHighlightedEventIndex(null); - sendJsonMessage({ type: "handler:load", id }); - } - - function deploy(code: string) { - setHandlerCode(code); - setDraftedCode(code); - sendJsonMessage({ type: "handler:save", id: selectedHandler, code }); - } - - function handleEventActivation(id: HandlerId) { - const handler = handlers.find(h => h.id === id); - setSelectedScope(handler!.scope); - handleHandlerSelection(id); - } - - function handleAddTargetsClose() { - setAddingTargets(false); - setStagedItems([]); - } - - function handleAddTargetsQuery(scope: TraceSpecScope, query: string) { - if (query.length === 0 || query === "*") { - return; - } - - const spec = [ - ["include", scope, query], - ]; - sendJsonMessage({ type: "targets:stage", profile: { spec } }); - } - - function handleAddTargetsCommit(id: StagedItemId | null) { - handleAddTargetsClose(); - sendJsonMessage({ type: "targets:commit", id }); - } - - function handleRespawnRequest() { - sendJsonMessage({ type: "tracer:respawn" }); - } - - useEffect(() => { - if (lastJsonMessage === null) { - return; - } - - switch (lastJsonMessage.type) { - case "tracer:sync": - setSpawnedProgram(lastJsonMessage.spawned_program); - setHandlers(lastJsonMessage.handlers); - break; - case "handlers:add": - setHandlers(handlers.concat(lastJsonMessage.handlers)); - break; - case "handler:loaded": { - const { code } = lastJsonMessage; - setHandlerCode(code); - setDraftedCode(code); - break; - } - case "targets:staged": - setStagedItems(lastJsonMessage.items); - break; - case "events:add": - setEvents(events.concat(lastJsonMessage.events)); - break; - default: - console.log("TODO:", lastJsonMessage); - break; - } - - }, [lastJsonMessage]); - - useEffect(() => { - for (let i = events.length - 1; i !== -1; i--) { - const event = events[i]; - if (event[0] === selectedHandler) { - setLatestMatchingEventIndex(i); - return; - } - } - setLatestMatchingEventIndex(null); - }, [selectedHandler, events]); - - const connectionError = (readyState === ReadyState.CLOSED) + const { + lostConnection, + + spawnedProgram, + respawn, + + handlers, + selectedScope, + selectScope, + selectedHandler, + setSelectedHandlerId, + handlerCode, + draftedCode, + setDraftedCode, + deployCode, + captureBacktraces, + setCaptureBacktraces, + + events, + latestMatchingEventIndex, + selectedEventIndex, + setSelectedEventIndex, + + addingTargets, + startAddingTargets, + finishAddingTargets, + stageItems, + stagedItems, + commitItems, + + addInstructionHook, + + symbolicate, + } = useModel(); + const captureBacktracesSwitchRef = useRef(null); + const [selectedTabId, setSelectedTabId] = useState("events"); + const [disassemblyTarget, setDisassemblyTarget] = useState(); + + const connectionError = lostConnection ? : null; + const eventView = ( + { + setSelectedHandlerId(handlerId); + setSelectedEventIndex(eventIndex); + }} + onDeactivate={() => { + setSelectedEventIndex(null); + }} + onDisassemble={address => { + setSelectedTabId("disassembly"); + setDisassemblyTarget({ type: "instruction", address }); + }} + onSymbolicate={symbolicate} + /> + ); + + const disassemblyView = ( + + ); + return ( <> - +
setSelectedScope(scope)} - selectedHandler={selectedHandler} - onHandlerSelect={handleHandlerSelection} + onScopeSelect={selectScope} + selectedHandler={selectedHandler?.id ?? null} + onHandlerSelect={setSelectedHandlerId} /> - - {(spawnedProgram !== null) ? : null} + + {(spawnedProgram !== null) ? : null}
-
+
{connectionError} - - + + + + setCaptureBacktraces(captureBacktracesSwitchRef.current!.checked)} > - Deploy - - - + Capture Backtraces + +
- - + + setSelectedTabId(tabId as string)} animate={false}> + + +
@@ -197,37 +182,3 @@ export default function App() { ); } - -type TracerMessage = - | TracerSyncMessage - | HandlersAddMessage - | HandlerLoadedMessage - | TargetsStagedMessage - | EventsAddMessage - ; - -interface TracerSyncMessage { - type: "tracer:sync"; - spawned_program: string | null; - handlers: Handler[]; -} - -interface HandlersAddMessage { - type: "handlers:add"; - handlers: Handler[]; -} - -interface HandlerLoadedMessage { - type: "handler:loaded"; - code: string; -} - -interface TargetsStagedMessage { - type: "targets:staged"; - items: StagedItem[]; -} - -interface EventsAddMessage { - type: "events:add"; - events: Event[]; -} \ No newline at end of file diff --git a/apps/tracer/src/DisassemblyView.css b/apps/tracer/src/DisassemblyView.css new file mode 100644 index 00000000..c9e6c4bf --- /dev/null +++ b/apps/tracer/src/DisassemblyView.css @@ -0,0 +1,18 @@ +.disassembly-view { + padding: 5px; + overflow: auto; + font-family: monospace; + font-size: 10px; + background-color: #1e1e1e; + color: #e4e4e4; + user-select: text; +} + +a.disassembly-address-has-handler { + font-weight: bold; + color: white; +} + +a.disassembly-menu-open { + background-color: #ef6456; +} diff --git a/apps/tracer/src/DisassemblyView.tsx b/apps/tracer/src/DisassemblyView.tsx new file mode 100644 index 00000000..c94e1938 --- /dev/null +++ b/apps/tracer/src/DisassemblyView.tsx @@ -0,0 +1,250 @@ +import "./DisassemblyView.css"; +import { Handler, HandlerId } from "./model.js"; +import { useR2 } from "@frida/react-use-r2"; +import { hideContextMenu, Menu, MenuItem, showContextMenu, Spinner } from "@blueprintjs/core"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +export interface DisassemblyViewProps { + target?: DisassemblyTarget; + handlers: Handler[]; + onSelectTarget: SelectTargetRequestHandler; + onSelectHandler: SelectHandlerRequestHandler; + onAddInstructionHook: AddInstructionHookRequestHandler; +} + +export type DisassemblyTarget = FunctionTarget | InstructionTarget; + +export interface FunctionTarget { + type: "function"; + name?: string; + address: string; +} + +export interface InstructionTarget { + type: "instruction"; + address: string; +} + +export type SelectTargetRequestHandler = (target: DisassemblyTarget) => void; +export type SelectHandlerRequestHandler = (id: HandlerId) => void; +export type AddInstructionHookRequestHandler = (address: bigint) => void; + +export default function DisassemblyView({ target, handlers, onSelectTarget, onSelectHandler, onAddInstructionHook }: DisassemblyViewProps) { + const containerRef = useRef(null); + const [rawR2Output, setRawR2Output] = useState(""); + const [r2Ops, setR2Ops] = useState(new Map()); + const [r2Output, setR2Output] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const highlightedAddressAnchorRef = useRef(null); + const { executeR2Command } = useR2(); + + useEffect(() => { + if (target === undefined) { + return; + } + + let ignore = false; + setIsLoading(true); + + const t = target; + + async function start() { + const command = [ + `s ${target!.address}`, + ] + if (t.type === "function") { + command.push(...["af-", "af"]); + if (t.name !== undefined) { + command.push("afn base64:" + btoa(t.name)); + } + command.push(...["pdf", "pdfj"]); + } else { + command.push(...["pd", "pdj"]); + } + + let result = await executeR2Command(command.join(";")); + if (ignore) { + return; + } + if (result.startsWith("{")) { + result = await executeR2Command("pd; pdj"); + if (ignore) { + return; + } + } + + const lines = result.trimEnd().split("\n"); + + setRawR2Output(lines.slice(0, lines.length - 1).join("\n")); + + const meta = JSON.parse(lines[lines.length - 1]); + const opItems: R2Operation[] = Array.isArray(meta) ? meta : meta.ops; + const opByAddress = new Map(opItems.map(op => [BigInt(op.offset), op])); + setR2Ops(opByAddress); + + setIsLoading(false); + } + + start(); + + return () => { + ignore = true; + }; + }, [target]); + + useEffect(() => { + let lines: string[]; + if (rawR2Output.length > 0) { + const handlerByAddress = handlers.reduce((result, handler) => { + const { address } = handler; + if (address === null) { + return result; + } + return result.set(BigInt(address), handler); + }, new Map()); + + lines = + rawR2Output + .split("
") + .map(line => { + let address: bigint | null = null; + line = line.replace(/\b0x[0-9a-f]+\b/, rawAddress => { + address = BigInt(rawAddress); + const handler = handlerByAddress.get(address); + const attrs = (handler !== undefined) + ? ` class="disassembly-address-has-handler" data-handler="${handler.id}"` + : ""; + return `${rawAddress}`; + }); + + if (address !== null) { + const op = r2Ops.get(address); + if (op !== undefined) { + const targetAddress = op.jump; + if (targetAddress !== undefined) { + const targetLabel = op.disasm.split(" ")[1]; + line = line.replace(targetLabel, _ => { + return `${targetLabel}`; + }); + } + } + } + + return line; + }); + } else { + lines = []; + } + + setR2Output(lines); + }, [handlers, rawR2Output]); + + const handleAddressMenuClose = useCallback(() => { + hideContextMenu(); + + highlightedAddressAnchorRef.current!.classList.remove("disassembly-menu-open"); + highlightedAddressAnchorRef.current = null; + }, []); + + const unhookedAddressMenu = useMemo(() => ( + + { + const address = BigInt(highlightedAddressAnchorRef.current!.innerText); + onAddInstructionHook(address); + }} + /> + + ), [onAddInstructionHook]); + + const hookedAddressMenu = useMemo(() => ( + + { + const id: HandlerId = parseInt(highlightedAddressAnchorRef.current!.getAttribute("data-handler")!) + onSelectHandler(id); + }} + /> + + ), [onSelectHandler]); + + const handleAddressClick = useCallback((event: React.MouseEvent) => { + const target = event.target; + if (!(target instanceof HTMLAnchorElement)) { + return; + } + + event.preventDefault(); + + const branchTarget = target.getAttribute("data-target"); + if (branchTarget !== null) { + const anchor = containerRef.current!.querySelector(`a[data-address="${branchTarget}"]`); + if (anchor !== null) { + anchor.scrollIntoView(); + return; + } + + onSelectTarget({ + type: (target.getAttribute("data-type") === "call") ? "function" : "instruction", + address: branchTarget + }); + return; + } + + showContextMenu({ + content: target.hasAttribute("data-handler") ? hookedAddressMenu : unhookedAddressMenu, + onClose: handleAddressMenuClose, + targetOffset: { + left: event.clientX, + top: event.clientY + }, + }); + + highlightedAddressAnchorRef.current = target; + target.classList.add("disassembly-menu-open"); + }, [handleAddressMenuClose, unhookedAddressMenu]); + + if (isLoading) { + return ( + + ); + } + + return ( +
+ {r2Output.map((line, i) =>
)} +
+ ); +} + +/* +interface R2Function { + name: string; + size: string; + addr: string; + ops: R2Operation[]; +} +*/ + +interface R2Operation { + offset: string; + esil: string; + refptr: number; + fcn_addr: string; + fcn_last: string; + size: number; + opcode: string; + disasm: string; + bytes: string; + family: string; + type: string; + type_num: string; + type2_num: string; + jump?: string; + fail?: string; + reloc: boolean; +} diff --git a/apps/tracer/src/EventView.css b/apps/tracer/src/EventView.css index e332f7ca..816e1563 100644 --- a/apps/tracer/src/EventView.css +++ b/apps/tracer/src/EventView.css @@ -1,40 +1,86 @@ .event-view { padding: 5px; - overflow: scroll; + overflow: auto; font-family: monospace; - font-size: 10px; + font-size: 12px; background-color: #1e1e1e; color: #e4e4e4; + user-select: text; } .event-heading { - margin-left: 65px; + margin-left: 104px; +} + +.event-item { + display: grid; + grid-template-columns: min-content; + white-space: nowrap; +} + +.event-summary { + grid-row: 1; + grid-column: 1; +} + +.event-details { + grid-row: 2; + grid-column: 1; + padding-right: 24px !important; } .event-timestamp { + padding: 0 10px; color: #555; - margin-right: 5px; - vertical-align: top; + vertical-align: middle; text-align: right; } .event-message { - min-height: 0; - padding: 0 5px; - text-align: left; - font-size: 10px; + font-size: 12px; font-weight: bold; - white-space: pre-line; } -.event-highlighted { - background-color: #EF6456; +.event-message:focus { + outline: 0; +} + +.event-selected .event-summary { + background-color: #ef6456; } -.event-highlighted .event-timestamp { +.event-selected .event-timestamp { color: white !important; } -.event-highlighted .event-message { +.event-selected .event-message { color: white !important; } + +.event-item .bp5-card { + color: #1e1e1e; +} + +.event-details td { + padding: 5px; +} + +.event-details td:nth-child(1) { + vertical-align: top; + text-align: right; + font-weight: bold; +} + +.event-details td:nth-child(2) { + display: flex; + flex-direction: column; + gap: 4px; +} + +.event-details .bp5-button { + font-size: 10px; +} + +.event-dismiss { + margin-top: 10px; +} diff --git a/apps/tracer/src/EventView.tsx b/apps/tracer/src/EventView.tsx index 01a79c7d..9543834c 100644 --- a/apps/tracer/src/EventView.tsx +++ b/apps/tracer/src/EventView.tsx @@ -1,23 +1,37 @@ import "./EventView.css"; import { Event, HandlerId } from "./model.js"; -import { Button } from "@blueprintjs/core"; -import { useEffect, useRef } from "react"; +import { Button, Card } from "@blueprintjs/core"; +import { ReactElement, useEffect, useRef, useState } from "react"; import { useStayAtBottom } from "react-stay-at-bottom"; export interface EventViewProps { events: Event[]; - highlightedIndex: number | null; - onActivate: ActivateEventHandler; + selectedIndex: number | null; + onActivate: EventActionHandler; + onDeactivate: EventActionHandler; + onDisassemble: DisassembleHandler; + onSymbolicate: SymbolicateHandler; } -export type ActivateEventHandler = (id: HandlerId) => void; +export type EventActionHandler = (handlerId: HandlerId, eventIndex: number) => void; +export type DisassembleHandler = (address: string) => void; +export type SymbolicateHandler = (addresses: string[]) => Promise; const NON_BLOCKING_SPACE = "\u00A0"; const INDENT = NON_BLOCKING_SPACE.repeat(3) + "|" + NON_BLOCKING_SPACE; -export default function EventView({ events, highlightedIndex = null, onActivate }: EventViewProps) { +export default function EventView({ + events, + selectedIndex = null, + onActivate, + onDeactivate, + onDisassemble, + onSymbolicate, +}: EventViewProps) { const containerRef = useRef(null); - const highlightedRef = useRef(null); + const selectedRef = useRef(null); + const [selectedCallerSymbol, setSelectedCallerSymbol] = useState(""); + const [selectedBacktraceSymbols, setSelectedBacktraceSymbols] = useState(null); let lastTid: number | null = null; useStayAtBottom(containerRef, { @@ -26,13 +40,99 @@ export default function EventView({ events, highlightedIndex = null, onActivate }); useEffect(() => { - highlightedRef.current?.scrollIntoView({ block: "center" }); - }, [highlightedRef, highlightedIndex]); + const item = selectedRef.current; + if (item === null) { + return; + } + const itemRect = item.getBoundingClientRect(); + const containerRect = containerRef.current!.getBoundingClientRect(); + if (itemRect.top >= containerRect.top && itemRect.bottom <= containerRect.bottom) { + return; + } + item.scrollIntoView({ block: "center" }); + }, [selectedRef, selectedIndex]); + + useEffect(() => { + setSelectedCallerSymbol(null); + setSelectedBacktraceSymbols(null); + }, [selectedIndex]); + + useEffect(() => { + if (selectedIndex === null) { + return; + } + + const [_targetId, _timestamp, _threadId, _depth, caller, backtrace, _message, _style] = events[selectedIndex]; + let ignore = false; + + async function symbolicate() { + if (caller !== null && backtrace === null) { + const [symbol] = await onSymbolicate([caller]); + if (!ignore) { + setSelectedCallerSymbol(symbol); + } + } + + if (backtrace !== null) { + const symbols = await onSymbolicate(backtrace); + if (!ignore) { + setSelectedBacktraceSymbols(symbols); + } + } + } + + symbolicate(); + + return () => { + ignore = true; + }; + }, [events, selectedIndex, onSymbolicate]) + + let selectedEventDetails: ReactElement | undefined; + if (selectedIndex !== null) { + const [targetId, _timestamp, threadId, _depth, caller, backtrace, _message, _style] = events[selectedIndex]; + + selectedEventDetails = ( + + + + + + + + + {(caller !== null && backtrace === null) ? ( + + + + + ) : null + } + {(backtrace !== null) ? ( + + + + + ) : null + } + +
Thread ID0x{threadId.toString(16)} +
Caller + +
Backtrace + {backtrace.map((address, i) => )} +
+ +
+ ); + } return (
{ - events.reduce((result, [targetId, timestamp, threadId, depth, message, style], i) => { + events.reduce((result, [targetId, timestamp, threadId, depth, _caller, _backtrace, message, style], i) => { let timestampStr = timestamp.toString(); const timestampPaddingNeeded = Math.max(6 - timestampStr.length, 0); for (let i = 0; i !== timestampPaddingNeeded; i++) { @@ -50,24 +150,31 @@ export default function EventView({ events, highlightedIndex = null, onActivate lastTid = threadId; } - const isHighlighted = i === highlightedIndex; + const isSelected = i === selectedIndex; + const eventClasses = ["event-item"]; + if (isSelected) { + eventClasses.push("event-selected"); + } result.push(
- {timestampStr} ms - {INDENT.repeat(depth)} - +
+ {timestampStr} ms + {INDENT.repeat(depth)} + +
+ {isSelected ? selectedEventDetails : null}
); diff --git a/apps/tracer/src/HandlerEditor.tsx b/apps/tracer/src/HandlerEditor.tsx index ea2ac64d..11cd4116 100644 --- a/apps/tracer/src/HandlerEditor.tsx +++ b/apps/tracer/src/HandlerEditor.tsx @@ -4,7 +4,7 @@ import type monaco from "monaco-editor"; import { useEffect, useState } from "react"; export interface HandlerEditorProps { - handlerId: HandlerId; + handlerId: HandlerId | null; handlerCode: string; onChange: CodeEventHandler; onSave: CodeEventHandler; @@ -20,7 +20,7 @@ export default function HandlerEditor({ handlerId, handlerCode, onChange, onSave const editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = { automaticLayout: true, - readOnly: handlerId === -1, + readOnly: handlerId === null, readOnlyMessage: { value: "Cannot edit without a handler selected" }, }; @@ -66,18 +66,21 @@ async function handleEditorWillMount(monaco: any) { const typingsResponse = await fetch("https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/frida-gum/index.d.ts"); const typingsContent = await typingsResponse.text(); monaco.languages.typescript.typescriptDefaults.addExtraLib(typingsContent + ` - declare function defineHandler(handler: TraceScriptHandler): TraceScriptHandler; - - interface TraceScriptHandler { + declare function defineHandler(handler: TraceHandler): TraceHandler; + + type TraceHandler = FunctionTraceHandler | InstructionTraceHandler; + + interface FunctionTraceHandler { /** * Called synchronously when about to call the traced function. * - * @this {object} - Object allowing you to store state for use in onLeave. + * @this {InvocationContext} - Object with useful properties, where you may also add properties + * of your own for use in onLeave. * @param {function} log - Call this function with a string to be presented to the user. * @param {array} args - Function arguments represented as an array of NativePointer objects. * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. - * @param {object} state - Object allowing you to keep state across function calls. + * @param {object} state - Object allowing you to keep state across handlers. * Only one JavaScript function will execute at a time, so do not worry about race-conditions. * However, do not use this to store function arguments across onEnter/onLeave, but instead * use "this" which is an object for keeping state local to an invocation. @@ -89,14 +92,29 @@ async function handleEditorWillMount(monaco: any) { * * See onEnter for details. * - * @this {object} - Object allowing you to access state stored in onEnter. + * @this {InvocationContext} - Object with useful properties, including any extra properties + * added by your onEnter code. * @param {function} log - Call this function with a string to be presented to the user. * @param {NativePointer} retval - Return value represented as a NativePointer object. - * @param {object} state - Object allowing you to keep state across function calls. + * @param {object} state - Object allowing you to keep state across handlers. */ onLeave?(this: InvocationContext, log: TraceLogFunction, retval: InvocationReturnValue, state: TraceScriptState): void; } - + + /** + * Called synchronously when about to execute the traced instruction. + * + * @this {InvocationContext} - Object with useful properties. + * @param {function} log - Call this function with a string to be presented to the user. + * @param {array} args - When the traced instruction is the first instruction of a function, + * use this parameter to access its arguments represented as an array of NativePointer objects. + * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. + * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. + * @param {object} state - Object allowing you to keep state across handlers. + * Only one JavaScript function will execute at a time, so do not worry about race-conditions. + */ + type InstructionTraceHandler = (this: InvocationContext, log: TraceLogFunction, args: InvocationArguments, state: TraceScriptState) => void; + type TraceLogFunction = (...args: any[]) => void; interface TraceScriptState { diff --git a/apps/tracer/src/HandlerList.tsx b/apps/tracer/src/HandlerList.tsx index e559e28f..8907612a 100644 --- a/apps/tracer/src/HandlerList.tsx +++ b/apps/tracer/src/HandlerList.tsx @@ -7,7 +7,7 @@ export interface HandlerListProps { handlers: Handler[]; selectedScope: ScopeId; onScopeSelect: ScopeEventHandler; - selectedHandler: HandlerId; + selectedHandler: HandlerId | null; onHandlerSelect: HandlerEventHandler; } @@ -56,7 +56,7 @@ export default function HandlerList({ handlers, selectedScope, onScopeSelect, se } useEffect(() => { - if (mouseInsideNode) { + if (selectedHandler === null || mouseInsideNode) { return; } @@ -75,7 +75,7 @@ export default function HandlerList({ handlers, selectedScope, onScopeSelect, se requestAnimationFrame(scrollIntoView); function scrollIntoView() { - tree!.getNodeContentElement(selectedHandler)?.scrollIntoView({ block: "center" }); + tree!.getNodeContentElement(selectedHandler!)?.scrollIntoView({ block: "center" }); } return () => { diff --git a/apps/tracer/src/index.css b/apps/tracer/src/index.css index 34e5dc54..8f48982d 100644 --- a/apps/tracer/src/index.css +++ b/apps/tracer/src/index.css @@ -1,5 +1,6 @@ html, body, #root { height: 100%; + user-select: none; } #root { diff --git a/apps/tracer/src/model.ts b/apps/tracer/src/model.ts index a6851d7c..504433bc 100644 --- a/apps/tracer/src/model.ts +++ b/apps/tracer/src/model.ts @@ -1,6 +1,302 @@ +import { useR2, type Platform, type Architecture } from "@frida/react-use-r2"; +//import r2WasmUrl from "@frida/react-use-r2/dist/r2.wasm?url"; +import { OverlayToaster } from "@blueprintjs/core"; +import { useCallback, useEffect, useRef, useState } from "react"; +import useWebSocket, { ReadyState } from "react-use-websocket"; + +//console.log("r2WasmUrl:", r2WasmUrl); + +const SOCKET_URL = (import.meta.env.MODE === "development") + ? "ws://localhost:1337" + : `ws://${window.location.host}`; + +export function useModel() { + const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(SOCKET_URL); + const rpcStateRef = useRef({ + pendingRequests: new Map>(), + nextRequestId: 1, + }); + + const lostConnection = readyState === ReadyState.CLOSED; + + const [spawnedProgram, setSpawnedProgram] = useState(null); + const [process, setProcess] = useState(null); + + const [handlers, setHandlers] = useState([]); + const [selectedScope, setSelectedScope] = useState(""); + const [selectedHandlerId, setSelectedHandlerId] = useState(null); + const [selectedHandler, setSelectedHandler] = useState(null); + const [handlerCode, setHandlerCode] = useState(""); + const [draftedCode, setDraftedCode] = useState(""); + const [captureBacktraces, _setCaptureBacktraces] = useState(false); + const [events, setEvents] = useState([]); + const [latestMatchingEventIndex, setLatestMatchingEventIndex] = useState(null); + const [selectedEventIndex, setSelectedEventIndex] = useState(null); + + const [addingTargets, setAddingTargets] = useState(false); + const [stagedItems, setStagedItems] = useState([]); + + const cachedSymbolsRef = useRef(new Map()); + + const request = useCallback((type: T, payload: RequestPayload[T]): Promise => { + const rpcState = rpcStateRef.current; + const id = rpcState.nextRequestId++; + return new Promise((resolve, reject) => { + rpcState.pendingRequests.set(id, { resolve, reject }); + sendJsonMessage({ type, id, payload }); + }); + }, [sendJsonMessage]); + + const onR2ReadRequest = useCallback(async (address: bigint, size: number) => { + const result = await request("memory:read", { + address: "0x" + address.toString(16), + size + }); + return (result !== null) ? new Uint8Array(result) : null; + }, [request]); + + useR2({ + source: (process !== null) + ? { + platform: process.platform, + arch: process.arch, + pointerSize: process.pointer_size, + pageSize: process.page_size, + onReadRequest: onR2ReadRequest + } + : undefined, + }); + + const respawn = useCallback(async () => { + await request("tracer:respawn", {}); + }, [request]); + + const selectScope = useCallback((id: ScopeId) => { + setSelectedScope(id); + }, []); + + useEffect(() => { + const id = selectedHandlerId; + if (id === null) { + return; + } + + const handler = handlers.find(h => h.id === id)!; + setSelectedScope(handler.scope); + setSelectedHandler(handler); + _setCaptureBacktraces(false); + + let ignore = false; + + async function loadCodeAndConfig() { + const { code, config } = await request("handler:load", { id: id! }); + if (!ignore) { + setHandlerCode(code); + setDraftedCode(code); + _setCaptureBacktraces(config.capture_backtraces); + } + } + + loadCodeAndConfig(); + + return () => { + ignore = true; + }; + }, [selectedHandlerId, handlers, request]); + + const deployCode = useCallback(async (code: string) => { + setHandlerCode(code); + setDraftedCode(code); + await request("handler:save", { id: selectedHandler!.id, code }); + }, [request, selectedHandler]); + + const setCaptureBacktraces = useCallback(async (enabled: boolean) => { + _setCaptureBacktraces(enabled); + await request("handler:configure", { + id: selectedHandler!.id, + parameters: { + capture_backtraces: enabled + } + }); + }, [request, selectedHandler]); + + const startAddingTargets = useCallback(() => { + setAddingTargets(true); + }, []); + + const finishAddingTargets = useCallback(() => { + setAddingTargets(false); + setStagedItems([]); + }, []); + + const stageItems = useCallback(async (scope: TraceSpecScope, query: string) => { + if (query.length === 0 || query === "*") { + return; + } + + const { items } = await request("targets:stage", { + profile: { + spec: [ + ["include", scope, query], + ] + } + }); + setStagedItems(items); + }, [request]); + + const commitItems = useCallback(async (id: StagedItemId | null) => { + finishAddingTargets(); + await request("targets:commit", { id }); + }, [finishAddingTargets, request]); + + const addInstructionHook = useCallback(async (address: bigint) => { + await request("targets:stage", { + profile: { + spec: [ + ["include", TraceSpecScope.AbsoluteInstruction, "0x" + address.toString(16)], + ] + } + }); + const { ids, errors } = await request("targets:commit", { id: null }); + if (ids.length === 0) { + return; + } + if (errors.length !== 0) { + const toaster = await OverlayToaster.createAsync({ position: "top" }); + toaster.show({ + intent: "danger", + icon: "error", + message: "Failed to add instruction hook: " + errors[0].message + }); + return; + } + setSelectedHandlerId(ids[0]); + }, [request]); + + const symbolicate = useCallback(async (addresses: string[]): Promise => { + const cache = cachedSymbolsRef.current; + + const result = addresses.map(address => cache.get(address) ?? null); + + const missingIndices = result.reduce((acc, element, i) => { + if (element === null) { + acc.push(i); + } + return acc; + }, [] as number[]); + if (missingIndices.length !== 0) { + const missingAddresses = missingIndices.map(i => addresses[i]); + const { names } = await request("symbols:resolve-addresses", { addresses: missingAddresses }); + names.forEach((name, i) => { + cache.set(missingAddresses[i], name); + result[missingIndices[i]] = name; + }); + } + + return result as string[]; + }, [request]); + + useEffect(() => { + if (lastJsonMessage === null) { + return; + } + + switch (lastJsonMessage.type) { + case "tracer:sync": + setSpawnedProgram(lastJsonMessage.spawned_program); + setProcess(lastJsonMessage.process); + setHandlers(lastJsonMessage.handlers); + break; + case "handlers:add": + setHandlers(handlers.concat(lastJsonMessage.handlers)); + break; + case "events:add": + setEvents(events.concat(lastJsonMessage.events)); + break; + case "request:result": + case "request:error": + const { id, payload } = lastJsonMessage; + const pendingRequests = rpcStateRef.current.pendingRequests; + + const entry = pendingRequests.get(id); + if (entry === undefined) { + return; + } + pendingRequests.delete(id); + + if (lastJsonMessage.type === "request:result") { + entry.resolve(payload); + } else { + const e = new Error(payload.message); + e.stack = payload.stack; + entry.reject(e); + } + + break; + default: + console.log("TODO:", lastJsonMessage); + break; + } + }, [lastJsonMessage]); + + useEffect(() => { + if (selectedHandler !== null) { + const selectedHandlerId = selectedHandler.id; + for (let i = events.length - 1; i !== -1; i--) { + const event = events[i]; + if (event[0] === selectedHandlerId) { + setLatestMatchingEventIndex(i); + return; + } + } + } + setLatestMatchingEventIndex(null); + }, [selectedHandler, events]); + + return { + lostConnection, + + spawnedProgram, + respawn, + process, + + handlers, + selectedScope, + selectScope, + selectedHandler, + setSelectedHandlerId, + handlerCode, + draftedCode, + setDraftedCode, + deployCode, + captureBacktraces, + setCaptureBacktraces, + + events, + latestMatchingEventIndex, + selectedEventIndex, + setSelectedEventIndex, + + addingTargets, + startAddingTargets, + finishAddingTargets, + stageItems, + stagedItems, + commitItems, + + addInstructionHook, + + symbolicate, + }; +} + +type TraceSpec = TraceSpecItem[]; +type TraceSpecItem = [TraceSpecOperation, TraceSpecScope, TraceSpecPattern]; +type TraceSpecOperation = "include" | "exclude"; export enum TraceSpecScope { Function = "function", RelativeFunction = "relative-function", + AbsoluteInstruction = "absolute-instruction", Imports = "imports", Module = "module", ObjcMethod = "objc-method", @@ -8,19 +304,160 @@ export enum TraceSpecScope { JavaMethod = "java-method", DebugSymbol = "debug-symbol", } +type TraceSpecPattern = string; export interface Handler { id: HandlerId; + flavor: TargetFlavor; scope: ScopeId; display_name: string; + address: string | null; } export type HandlerId = number; +export type TargetFlavor = "insn" | "c" | "objc" | "swift" | "java"; export type ScopeId = string; +interface HandlerConfig { + capture_backtraces: boolean; +} + export type StagedItem = [id: StagedItemId, scope: ScopeName, member: MemberName]; export type StagedItemId = number; export type ScopeName = string; export type MemberName = string | [string, string]; -export type Event = [targetId: HandlerId, timestamp: number, threadId: number, depth: number, message: string, style: string[]]; +export type Event = [ + targetId: HandlerId, + timestamp: number, + threadId: number, + depth: number, + caller: string | null, + backtrace: string[] | null, + message: string, + style: string[] +]; + +export interface ProcessDetails { + id: number; + platform: Platform; + arch: Architecture; + pointer_size: number; + page_size: number; + main_module: NativeModule; +} + +export interface NativeModule { + base: string; + name: string; + path: string; + size: number; +} + +interface RpcState { + pendingRequests: Map>; + nextRequestId: number; +} + +type RequestType = keyof RequestPayload; +type RequestId = number; +interface RequestPayload { + "tracer:respawn": {}; + "handler:load": { + id: HandlerId; + }; + "handler:save": { + id: HandlerId; + code: string; + }; + "handler:configure": { + id: HandlerId; + parameters: Record; + }; + "targets:stage": { + profile: { + spec: TraceSpec; + }; + }; + "targets:commit": { + id: StagedItemId | null; + }; + "memory:read": { + address: string; + size: number; + }; + "symbols:resolve-addresses": { + addresses: string[]; + }; +} +interface ResponsePayload { + "tracer:respawn": void; + "handler:load": { + code: string; + config: HandlerConfig; + }; + "handler:save": void; + "handler:configure": void; + "targets:stage": { + items: StagedItem[]; + }; + "targets:commit": { + ids: HandlerId[]; + errors: TraceError[]; + }; + "memory:read": number[] | null; + "symbols:resolve-addresses": { + names: string[]; + }; +} + +interface TraceError { + id: HandlerId; + name: string; + message: string; +} + +interface ResponseCallbacks { + resolve(payload: ResponsePayload[T]): void; + reject(error: Error): void; +} + +type TracerMessage = + | TracerSyncMessage + | HandlersAddMessage + | EventsAddMessage + | RequestResultMessage + | RequestErrorMessage + ; + +interface TracerSyncMessage { + type: "tracer:sync"; + spawned_program: string | null; + process: ProcessDetails; + handlers: Handler[]; +} + +interface HandlersAddMessage { + type: "handlers:add"; + handlers: Handler[]; +} + +interface EventsAddMessage { + type: "events:add"; + events: Event[]; +} + +interface RequestResultMessage { + type: "request:result"; + id: RequestId; + payload: ResponsePayload[T]; +} + +interface RequestErrorMessage { + type: "request:error"; + id: RequestId; + payload: { + message: string; + stack: string; + }; +} diff --git a/apps/tracer/vite.config.ts b/apps/tracer/vite.config.ts index 5a6daa5d..a3c045e1 100644 --- a/apps/tracer/vite.config.ts +++ b/apps/tracer/vite.config.ts @@ -1,8 +1,29 @@ -import { defineConfig } from "vite"; +import fs from "fs"; +import path from "path"; +import { defineConfig, Plugin } from "vite"; import react from "@vitejs/plugin-react"; +const R2_WASM_PATH = path.join(import.meta.dirname, "node_modules", "@frida", "react-use-r2", "dist", "r2.wasm"); + +const r2WasmPlugin: Plugin = { + name: "r2-wasm-plugin", + configureServer(server) { + server.middlewares.use((req, res, next) => { + if (req.originalUrl?.endsWith("/r2.wasm")) { + const data = fs.readFileSync(R2_WASM_PATH); + res.setHeader("Content-Length", data.length); + res.setHeader("Content-Type", "application/wasm"); + res.end(data, "binary"); + return; + } + next(); + }); + }, +}; + export default defineConfig({ - plugins: [react()], + plugins: [react(), r2WasmPlugin], + assetsInclude: "**/*.wasm", build: { rollupOptions: { output: { @@ -12,5 +33,5 @@ export default defineConfig({ assetFileNames: "assets/[name].[ext]" } } - } + }, }); diff --git a/frida_tools/tracer.py b/frida_tools/tracer.py index 7805426b..95dff763 100644 --- a/frida_tools/tracer.py +++ b/frida_tools/tracer.py @@ -34,6 +34,7 @@ def main() -> None: import json + import traceback from colorama import Fore, Style @@ -45,7 +46,7 @@ def __init__(self) -> None: self._handlers = OrderedDict() self._ui_port = 1337 self._ui_zip = ZipFile(Path(__file__).parent / "tracer_ui.zip", "r") - self._ui_sockets: Set[websockets.asyncio.server.ServerConnection] = set() + self._ui_socket_handlers: Set[UISocketHandler] = set() self._ui_worker = None self._asyncio_loop = None self._palette = ["cyan", "magenta", "yellow", "green", "red", "blue"] @@ -251,15 +252,15 @@ def on_trace_error(self, message: str) -> None: def on_trace_events(self, raw_events) -> None: events = [ - (target_id, timestamp, thread_id, depth, message, self._get_style(thread_id)) - for target_id, timestamp, thread_id, depth, message in raw_events + (target_id, timestamp, thread_id, depth, caller, backtrace, message, self._get_style(thread_id)) + for target_id, timestamp, thread_id, depth, caller, backtrace, message in raw_events ] self._asyncio_loop.call_soon_threadsafe( lambda: self._asyncio_loop.create_task(self._broadcast_trace_events(events)) ) no_attributes = Style.RESET_ALL - for target_id, timestamp, thread_id, depth, message, style in events: + for target_id, timestamp, thread_id, depth, caller, backtrace, message, style in events: if self._output is not None: self._output.append(message + "\n") elif self._quiet: @@ -287,7 +288,8 @@ def on_trace_handler_load(self, target: TraceTarget, handler: str, source: str) self._print('%s: Loaded handler at "%s"' % (target, source.replace("\\", "\\\\"))) def _register_handler(self, target: TraceTarget, source: str) -> None: - self._handlers[target.identifier] = (target, source) + config = {"capture_backtraces": False} + self._handlers[target.identifier] = (target, source, config) def _get_style(self, thread_id): style = self._style_by_thread_id.get(thread_id, None) @@ -325,68 +327,23 @@ async def _handle_websocket_connection(self, websocket: websockets.asyncio.serve if self._tracer is None: return - self._ui_sockets.add(websocket) + handler = UISocketHandler(self, websocket) + self._ui_socket_handlers.add(handler) try: - message = { - "type": "tracer:sync", - "spawned_program": self._spawned_argv[0] if self._spawned_argv is not None else None, - "handlers": [target.to_json() for target, source in self._handlers.values()], - } - await websocket.send(json.dumps(message)) - - while True: - message = json.loads(await websocket.recv()) - mtype = message["type"] - if mtype == "tracer:respawn": - self._reactor.schedule(self._respawn) - elif mtype == "handler:load": - target, source = self._handlers[message["id"]] - await websocket.send( - json.dumps( - { - "type": "handler:loaded", - "code": self._repo.ensure_handler(target), - } - ) - ) - elif mtype == "handler:save": - target, source = self._handlers[message["id"]] - self._repo.update_handler(target, message["code"]) - elif mtype == "targets:stage": - profile = TracerProfile(list(map(tuple, message["profile"]["spec"]))) - try: - items = self._tracer.stage_targets(profile) - except: - continue - await websocket.send( - json.dumps( - { - "type": "targets:staged", - "items": items, - } - ) - ) - elif mtype == "targets:commit": - target_ids = self._tracer.commit_targets(message["id"]) - message = { - "type": "handlers:add", - "handlers": [self._handlers[target_id][0].to_json() for target_id in target_ids], - } - await websocket.send(json.dumps(message)) + await handler.process_messages() except: - pass + traceback.print_exc() + # pass finally: - self._ui_sockets.remove(websocket) + self._ui_socket_handlers.remove(handler) async def _broadcast_trace_events(self, events): - for websocket in self._ui_sockets: - await websocket.send( - json.dumps( - { - "type": "events:add", - "events": events, - } - ) + for handler in self._ui_socket_handlers: + await handler.post( + { + "type": "events:add", + "events": events, + } ) def _handle_asset_request( @@ -428,6 +385,98 @@ def _handle_asset_request( return response + class UISocketHandler: + def __init__(self, app: TracerApplication, socket: websockets.asyncio.server.ServerConnection) -> None: + self.app = app + self.socket = socket + + async def process_messages(self) -> None: + app = self.app + + await self.post( + { + "type": "tracer:sync", + "spawned_program": app._spawned_argv[0] if app._spawned_argv is not None else None, + "process": app._tracer.process, + "handlers": [target.to_json() for target, _, _ in app._handlers.values()], + } + ) + + while True: + request = json.loads(await self.socket.recv()) + request_id = request.get("id") + + try: + handle_request = getattr(self, "_on_" + request["type"].replace(":", "_").replace("-", "_"), None) + if handle_request is None: + raise NameError("unsupported request type") + result = await handle_request(request["payload"]) + except Exception as e: + if request_id is not None: + await self.post( + { + "type": "request:error", + "id": request_id, + "payload": { + "message": str(e), + "stack": traceback.format_exc(), + }, + } + ) + continue + + if request_id is not None: + await self.post({"type": "request:result", "id": request_id, "payload": result}) + + async def post(self, message: dict) -> None: + await self.socket.send(json.dumps(message)) + + async def _on_tracer_respawn(self, _: dict) -> None: + self.app._reactor.schedule(self.app._respawn) + + async def _on_handler_load(self, payload: dict) -> None: + target, source, config = self.app._handlers[payload["id"]] + return {"code": self.app._repo.ensure_handler(target), "config": config} + + async def _on_handler_save(self, payload: dict) -> None: + target, _, _ = self.app._handlers[payload["id"]] + self.app._repo.update_handler(target, payload["code"]) + + async def _on_handler_configure(self, payload: dict) -> None: + identifier = payload["id"] + _, _, config = self.app._handlers[identifier] + for k, v in payload["parameters"].items(): + config[k] = v + self.app._tracer.update_handler_config(identifier, config) + + async def _on_targets_stage(self, payload: dict) -> None: + profile = TracerProfile(list(map(tuple, payload["profile"]["spec"]))) + items = self.app._tracer.stage_targets(profile) + return { + "items": items, + } + + async def _on_targets_commit(self, payload: dict) -> None: + result = self.app._tracer.commit_targets(payload["id"]) + target_ids = result["ids"] + + await self.post( + { + "type": "handlers:add", + "handlers": [self.app._handlers[target_id][0].to_json() for target_id in target_ids], + } + ) + + return result + + async def _on_memory_read(self, payload: dict) -> None: + data = self.app._tracer.read_memory(payload["address"], payload["size"]) + return list(data) if data is not None else None + + async def _on_symbols_resolve_addresses(self, payload: dict) -> None: + names = self.app._tracer.resolve_addresses(payload["addresses"]) + return {"names": names} + app = TracerApplication() app.run() @@ -519,6 +568,7 @@ def __init__( init_scripts=[], log_handler: Callable[[str, str], None] = None, ) -> None: + self.main_module = None self._reactor = reactor self._repository = repository self._profile = profile @@ -540,7 +590,7 @@ def on_load(*args) -> None: self._repository.on_load(on_load) def on_update(target, handler, source) -> None: - self._agent.update(target.identifier, target.display_name, handler) + self._agent.update_handler_code(target.identifier, target.display_name, handler) self._repository.on_update(on_update) @@ -563,7 +613,7 @@ def on_update(target, handler, source) -> None: self._agent = script.exports_sync raw_init_scripts = [{"filename": script.filename, "source": script.source} for script in self._init_scripts] - self._agent.init(stage, parameters, raw_init_scripts, self._profile.spec) + self.process = self._agent.init(stage, parameters, raw_init_scripts, self._profile.spec) def stop(self) -> None: self._repository.close() @@ -576,12 +626,21 @@ def stop(self) -> None: pass self._script = None + def update_handler_config(self, identifier: int, config: dict) -> None: + return self._agent.update_handler_config(identifier, config) + def stage_targets(self, profile: TracerProfile) -> List: return self._agent.stage_targets(profile.spec) - def commit_targets(self, identifier: Optional[int]) -> List: + def commit_targets(self, identifier: Optional[int]) -> dict: return self._agent.commit_targets(identifier) + def read_memory(self, address: str, size: int) -> bytes: + return self._agent.read_memory(address, size) + + def resolve_addresses(self, addresses: List[str]) -> List[str]: + return self._agent.resolve_addresses(addresses) + def _on_message(self, message, data, ui) -> None: handled = False @@ -602,8 +661,8 @@ def _on_message(self, message, data, ui) -> None: def _try_handle_message(self, mtype, params, data, ui) -> False: if mtype == "events:add": events = [ - (target_id, timestamp, thread_id, depth, message) - for target_id, timestamp, thread_id, depth, message in params["events"] + (target_id, timestamp, thread_id, depth, caller, backtrace, message) + for target_id, timestamp, thread_id, depth, caller, backtrace, message in params["events"] ] ui.on_trace_events(events) return True @@ -619,16 +678,20 @@ def _try_handle_message(self, mtype, params, data, ui) -> False: next_id = base_id for scope in params["scopes"]: scope_name = scope["name"] + addresses = scope.get("addresses") + i = 0 for member_name in scope["members"]: if isinstance(member_name, list): - name, display_name = member_name + name, display_name, address = member_name else: name = member_name display_name = member_name - target = TraceTarget(next_id, flavor, scope_name, name, display_name) + address = int(addresses[i], 16) if addresses is not None else None + target = TraceTarget(next_id, flavor, scope_name, name, display_name, address) next_id += 1 handler = repo.ensure_handler(target) scripts.append(handler) + i += 1 self._script.post(response) @@ -661,9 +724,16 @@ class TraceTarget: scope: str name: str display_name: str + address: Optional[int] def to_json(self) -> dict: - return {"id": self.identifier, "scope": self.scope, "display_name": self.display_name} + return { + "id": self.identifier, + "flavor": self.flavor, + "scope": self.scope, + "display_name": self.display_name, + "address": hex(self.address) if self.address is not None else None, + } def __str__(self) -> str: return self.display_name @@ -710,10 +780,26 @@ def _notify_update(self, target: TraceTarget, handler: str, source: str) -> None self._on_update_callback(target, handler, source) def _create_stub_handler(self, target: TraceTarget, decorate: bool) -> str: + if target.flavor == "insn": + return self._create_stub_instruction_handler(target, decorate) if target.flavor == "java": return self._create_stub_java_handler(target, decorate) - else: - return self._create_stub_native_handler(target, decorate) + return self._create_stub_native_handler(target, decorate) + + def _create_stub_instruction_handler(self, target: TraceTarget, decorate: bool) -> str: + return """\ +/* + * Auto-generated by Frida. + * + * For full API reference, see: https://frida.re/docs/javascript-api/ + */ + +defineHandler(function (log, args, state) { + log(`%(display_name)s hit! sp=${this.context.sp}`); +}); +""" % { + "display_name": target.display_name + } def _create_stub_native_handler(self, target: TraceTarget, decorate: bool) -> str: if target.flavor == "objc":