diff --git a/agents/tracer/agent.ts b/agents/tracer/agent.ts index f0715091..a106e1a1 100644 --- a/agents/tracer/agent.ts +++ b/agents/tracer/agent.ts @@ -57,9 +57,14 @@ class Agent { 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(name, script); + handler[0] = newHandler[0]; + handler[1] = newHandler[1]; + } else { + const newHandler = this.parseInstructionHandler(name, script); + handler[0] = newHandler[0]; + } } updateHandlerConfig(id: TraceTargetId, config: HandlerConfig) { @@ -138,10 +143,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--; } @@ -151,10 +155,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++; } @@ -186,10 +189,7 @@ class Agent { 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) { @@ -213,6 +213,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); @@ -283,6 +288,7 @@ class Agent { } private async traceNativeTargets(targets: NativeTargets): Promise { + const insnGroups = new Map(); const cGroups = new Map(); const objcGroups = new Map(); const swiftGroups = new Map(); @@ -290,6 +296,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; @@ -311,6 +320,7 @@ class Agent { } const [cIds, objcIds, swiftIds] = await Promise.all([ + this.traceNativeEntries("insn", insnGroups), this.traceNativeEntries("c", cGroups), this.traceNativeEntries("objc", objcGroups), this.traceNativeEntries("swift", swiftGroups), @@ -319,7 +329,7 @@ class Agent { return [...cIds, ...objcIds, ...swiftIds]; } - private async traceNativeEntries(flavor: "c" | "objc" | "swift", groups: NativeTargetScopes): Promise { + private async traceNativeEntries(flavor: NativeTargetType, groups: NativeTargetScopes): Promise { if (groups.size === 0) { return []; } @@ -336,7 +346,7 @@ class Agent { scopes.push({ name, members: items.map(item => item[0]), - addresses: items.map(item => item[1].toString()), + addresses: items.map(item => item[1].toString()) }); this.nextId += items.length; } @@ -345,16 +355,21 @@ 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(displayName, scripts[offset]) + : this.parseFunctionHandler(displayName, scripts[offset]); this.handlers.set(id, handler); try { - Interceptor.attach(address, this.makeNativeListenerCallbacks(id, handler)); + Interceptor.attach(address, isInstruction + ? this.makeNativeInstructionListener(id, handler as TraceInstructionHandler) + : this.makeNativeFunctionListener(id, handler as TraceFunctionHandler)); } catch (e: any) { send({ type: "agent:warning", @@ -406,7 +421,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(fullName, scripts[offset]); this.handlers.set(id, handler); const dispatcher: Java.MethodDispatcher = C[bareName]; @@ -425,7 +440,7 @@ class Agent { }); } - private makeNativeListenerCallbacks(id: TraceTargetId, handler: TraceHandler): InvocationListenerCallbacks { + private makeNativeFunctionListener(id: TraceTargetId, handler: TraceFunctionHandler): InvocationListenerCallbacks { const agent = this; return { @@ -440,7 +455,16 @@ class Agent { }; } - 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[]) { @@ -448,7 +472,7 @@ class Agent { }; } - private handleJavaInvocation(id: TraceTargetId, method: Java.Method, handler: TraceHandler, instance: Java.Wrapper, args: any[]): any { + 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, ">"); @@ -460,8 +484,8 @@ class Agent { return (replacementRetval !== undefined) ? replacementRetval : retval; } - private invokeNativeHandler(id: TraceTargetId, callback: TraceEnterHandler | TraceLeaveHandler, config: HandlerConfig, - 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); @@ -503,7 +527,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); @@ -515,20 +539,37 @@ class Agent { return depth; } - private parseHandler(name: string, script: string): TraceHandler { - const id = `/handlers/${name}.js`; + private parseFunctionHandler(name: string, script: string): TraceFunctionHandler { try { - const h = Script.evaluate(id, script); + const h = this.parseHandlerScript(name, script); return [h.onEnter ?? noop, h.onLeave ?? noop, makeDefaultHandlerConfig()]; } catch (e: any) { send({ type: "agent:warning", - message: `${id}: ${e.message}` + message: `${name}: ${e.message}` }); return [noop, noop, makeDefaultHandlerConfig()]; } } + private parseInstructionHandler(name: string, script: string): TraceInstructionHandler { + try { + const onHit = this.parseHandlerScript(name, script); + return [onHit, makeDefaultHandlerConfig()]; + } catch (e: any) { + send({ + type: "agent:warning", + message: `${name}: ${e.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}!*`)) { @@ -565,6 +606,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.name, `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) { @@ -946,6 +996,7 @@ type TraceSpecScope = | "module" | "function" | "relative-function" + | "absolute-instruction" | "imports" | "objc-method" | "swift-func" @@ -959,19 +1010,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 TargetType = NativeTargetType | "java"; type ScopeName = string; type MemberName = string | [string, string]; +type NativeTargetType = "insn" | "c" | "objc" | "swift"; type NativeTargets = Map; -type NativeTarget = ["c" | "objc" | "swift", ScopeName, MemberName]; +type NativeTarget = [type: NativeTargetType, scope: ScopeName, name: MemberName]; type NativeTargetScopes = Map; -type NativeItem = [MemberName, NativePointer]; +type NativeItem = [name: MemberName, address: NativePointer]; type NativeId = string; interface JavaTargetGroup { @@ -1016,11 +1079,14 @@ type Caller = string | null; type Backtrace = string[] | null; type Message = string; -type TraceHandler = [onEnter: TraceEnterHandler, onLeave: TraceLeaveHandler, config: HandlerConfig]; +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; diff --git a/apps/tracer/src/App.tsx b/apps/tracer/src/App.tsx index 5fba1e1a..e7a281e4 100644 --- a/apps/tracer/src/App.tsx +++ b/apps/tracer/src/App.tsx @@ -47,6 +47,8 @@ export default function App() { stageItems, stagedItems, commitItems, + + addInstructionHook, } = useModel(); const captureBacktracesSwitchRef = useRef(null); const [selectedTabId, setSelectedTabId] = useState("events"); @@ -78,13 +80,13 @@ export default function App() { ); const disassemblyView = ( - + ); return ( <> - +
- + setSelectedTabId(tabId as string)} animate={false}> diff --git a/apps/tracer/src/DisassemblyView.tsx b/apps/tracer/src/DisassemblyView.tsx index e6db7261..4b4f3c8d 100644 --- a/apps/tracer/src/DisassemblyView.tsx +++ b/apps/tracer/src/DisassemblyView.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; export interface DisassemblyViewProps { target?: DisassemblyTarget; + onAddInstructionHook: AddInstructionHookHandler; } export type DisassemblyTarget = FunctionTarget | InstructionTarget; @@ -20,7 +21,9 @@ export interface InstructionTarget { address: string; } -export default function DisassemblyView({ target }: DisassemblyViewProps = {}) { +export type AddInstructionHookHandler = (address: bigint) => void; + +export default function DisassemblyView({ target, onAddInstructionHook }: DisassemblyViewProps) { const [r2Output, setR2Output] = useState([]); const [isLoading, setIsLoading] = useState(false); const highlightedAddressAnchorRef = useRef(null); @@ -79,7 +82,14 @@ export default function DisassemblyView({ target }: DisassemblyViewProps = {}) { const addressMenu = useMemo(() => ( - + { + const address = BigInt(highlightedAddressAnchorRef.current!.innerText); + onAddInstructionHook(address); + }} + /> ), [handleAddressMenuClose]); diff --git a/apps/tracer/src/model.ts b/apps/tracer/src/model.ts index bbebfba1..3103ebd7 100644 --- a/apps/tracer/src/model.ts +++ b/apps/tracer/src/model.ts @@ -106,6 +106,14 @@ export function useModel() { sendJsonMessage({ type: "targets:commit", id }); } + function addInstructionHook(address: bigint) { + const spec = [ + ["include", TraceSpecScope.AbsoluteInstruction, "0x" + address.toString(16)], + ]; + sendJsonMessage({ type: "targets:stage", profile: { spec } }); + sendJsonMessage({ type: "targets:commit", id: null }); + } + useEffect(() => { if (lastJsonMessage === null) { return; @@ -187,12 +195,15 @@ export function useModel() { stageItems, stagedItems, commitItems, + + addInstructionHook, }; } export enum TraceSpecScope { Function = "function", RelativeFunction = "relative-function", + AbsoluteInstruction = "absolute-instruction", Imports = "imports", Module = "module", ObjcMethod = "objc-method", diff --git a/frida_tools/tracer.py b/frida_tools/tracer.py index 8e1e0eb6..c5fdc22c 100644 --- a/frida_tools/tracer.py +++ b/frida_tools/tracer.py @@ -744,10 +744,18 @@ 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 """\ +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":