From cdc13cc99b819818d7057923ee3ab65acd9348bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Andr=C3=A9=20Vadla=20Ravn=C3=A5s?= Date: Sun, 6 Oct 2024 19:40:53 +0200 Subject: [PATCH] tracer: Add a basic memory view Wired up in the disassembly view for potential addresses. --- apps/tracer/src/App.css | 4 + apps/tracer/src/App.tsx | 13 ++- apps/tracer/src/DisassemblyView.tsx | 137 ++++++++++++++++++++-------- apps/tracer/src/EventView.tsx | 7 +- apps/tracer/src/MemoryView.css | 9 ++ apps/tracer/src/MemoryView.tsx | 49 ++++++++++ 6 files changed, 177 insertions(+), 42 deletions(-) create mode 100644 apps/tracer/src/MemoryView.css create mode 100644 apps/tracer/src/MemoryView.tsx diff --git a/apps/tracer/src/App.css b/apps/tracer/src/App.css index 07626e94..1af2280b 100644 --- a/apps/tracer/src/App.css +++ b/apps/tracer/src/App.css @@ -88,3 +88,7 @@ .disassembly-view { flex: 1; } + +.memory-view { + flex: 1; +} diff --git a/apps/tracer/src/App.tsx b/apps/tracer/src/App.tsx index 1ad8926b..ce8bf317 100644 --- a/apps/tracer/src/App.tsx +++ b/apps/tracer/src/App.tsx @@ -4,6 +4,7 @@ import DisassemblyView, { type DisassemblyTarget } from "./DisassemblyView.tsx"; import EventView from "./EventView.tsx"; import HandlerEditor from "./HandlerEditor.tsx"; import HandlerList from "./HandlerList.tsx"; +import MemoryView from "./MemoryView.tsx"; import { useModel } from "./model.js"; import { BlueprintProvider, @@ -55,6 +56,7 @@ export default function App() { const captureBacktracesSwitchRef = useRef(null); const [selectedTabId, setSelectedTabId] = useState("events"); const [disassemblyTarget, setDisassemblyTarget] = useState(); + const [memoryLocation, setMemoryLocation] = useState(); const connectionError = lostConnection ? { + setSelectedTabId("memory"); + setMemoryLocation(address); + }} onAddInstructionHook={addInstructionHook} onSymbolicate={symbolicate} /> ); + const memoryView = ( + + ); + return ( <> @@ -139,7 +149,7 @@ export default function App() { setDisassemblyTarget({ type: (selectedHandler!.flavor === "insn") ? "instruction" : "function", name: selectedHandler!.display_name, - address: selectedHandler!.address! + address: BigInt(selectedHandler!.address!) }); }} > @@ -167,6 +177,7 @@ export default function App() { setSelectedTabId(tabId as string)} animate={false}> + diff --git a/apps/tracer/src/DisassemblyView.tsx b/apps/tracer/src/DisassemblyView.tsx index 1364027a..272c3423 100644 --- a/apps/tracer/src/DisassemblyView.tsx +++ b/apps/tracer/src/DisassemblyView.tsx @@ -9,6 +9,7 @@ export interface DisassemblyViewProps { handlers: Handler[]; onSelectTarget: SelectTargetRequestHandler; onSelectHandler: SelectHandlerRequestHandler; + onSelectMemoryLocation: SelectMemoryLocationRequestHandler; onAddInstructionHook: AddInstructionHookRequestHandler; onSymbolicate: SymbolicateRequestHandler; } @@ -18,23 +19,25 @@ export type DisassemblyTarget = FunctionTarget | InstructionTarget; export interface FunctionTarget { type: "function"; name?: string; - address: string; + address: bigint; } export interface InstructionTarget { type: "instruction"; - address: string; + address: bigint; } export type SelectTargetRequestHandler = (target: DisassemblyTarget) => void; export type SelectHandlerRequestHandler = (id: HandlerId) => void; +export type SelectMemoryLocationRequestHandler = (address: bigint) => void; export type AddInstructionHookRequestHandler = (address: bigint) => void; export type SymbolicateRequestHandler = (addresses: bigint[]) => Promise; -const ADDRESS_PATTERN = /\b0x[0-9a-f]+\b/; +const HEXLITERAL_PATTERN = /\b0x[0-9a-f]+\b/g; const MINIMUM_PAGE_SIZE = 4096; -export default function DisassemblyView({ target, handlers, onSelectTarget, onSelectHandler, onAddInstructionHook, onSymbolicate }: DisassemblyViewProps) { +export default function DisassemblyView({ target, handlers, onSelectTarget, onSelectHandler, onSelectMemoryLocation, onAddInstructionHook, + onSymbolicate }: DisassemblyViewProps) { const containerRef = useRef(null); const [data, setData] = useState(null); const seenAddressesRef = useRef(new Set()); @@ -62,7 +65,7 @@ export default function DisassemblyView({ target, handlers, onSelectTarget, onSe const addressesToResolve: bigint[] = []; const seenAddresses = seenAddressesRef.current; const { operations } = result; - for (const match of result.html.matchAll(new RegExp(ADDRESS_PATTERN, "g"))) { + for (const match of result.html.matchAll(HEXLITERAL_PATTERN)) { const val = BigInt(match[0]); if (val < MINIMUM_PAGE_SIZE || seenAddresses.has(val) || operations.has(val)) { continue; @@ -120,13 +123,37 @@ export default function DisassemblyView({ target, handlers, onSelectTarget, onSe .split("
") .map(line => { let address: bigint | null = null; - line = line.replace(ADDRESS_PATTERN, rawAddress => { - address = BigInt(rawAddress); - const handler = handlerByAddress.get(address); - const attrs = (handler !== undefined) - ? ` class="disassembly-address-has-handler" data-handler="${handler.id}"` - : ""; - return `${rawAddress}`; + let branchTarget: { address: string, label: string } | null = null; + + let n = 0; + line = line.replace(HEXLITERAL_PATTERN, rawValue => { + const value = BigInt(rawValue); + n++; + + if (n === 1) { + address = value; + + const op = operations.get(address); + if (op !== undefined) { + const jump = op.jump; + if (jump !== undefined) { + branchTarget = { + address: jump, + label: op.disasm.split(" ")[1] + }; + } + } + + const handler = handlerByAddress.get(value); + const attrs = (handler !== undefined) + ? ` class="disassembly-address-has-handler" data-handler="${handler.id}"` + : ""; + return `${rawValue}`; + } else if (branchTarget === null && value >= MINIMUM_PAGE_SIZE) { + return `${rawValue}`; + } + + return rawValue; }); if (address !== null) { @@ -136,7 +163,7 @@ export default function DisassemblyView({ target, handlers, onSelectTarget, onSe if (targetAddress !== undefined) { const targetLabel = op.disasm.split(" ")[1]; line = line.replace(targetLabel, _ => { - return `${targetLabel}`; + return `${targetLabel}`; }); } } @@ -148,7 +175,7 @@ export default function DisassemblyView({ target, handlers, onSelectTarget, onSe setR2Output(lines); }, [handlers, data]); - const handleAddressMenuClose = useCallback(() => { + const handleMenuClose = useCallback(() => { hideContextMenu(); highlightedAddressAnchorRef.current!.classList.remove("disassembly-menu-open"); @@ -181,6 +208,30 @@ export default function DisassemblyView({ target, handlers, onSelectTarget, onSe ), [onSelectHandler]); + const unknownAddressMenu = useMemo(() => ( + + { + const address = BigInt(highlightedAddressAnchorRef.current!.getAttribute("data-value")!); + onSelectMemoryLocation(address); + }} + /> + { + const address = BigInt(highlightedAddressAnchorRef.current!.getAttribute("data-value")!); + onSelectTarget({ + type: "instruction", + address + }); + }} + /> + + ), [onSelectHandler]); + const handleAddressClick = useCallback((event: React.MouseEvent) => { const target = event.target; if (!(target instanceof HTMLAnchorElement)) { @@ -189,33 +240,43 @@ export default function DisassemblyView({ target, handlers, onSelectTarget, onSe 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; + const context = target.getAttribute("data-context")!; + switch (context) { + case "address": + case "value": { + showContextMenu({ + content: (context === "address") + ? (target.hasAttribute("data-handler") ? hookedAddressMenu : unhookedAddressMenu) + : unknownAddressMenu, + onClose: handleMenuClose, + targetOffset: { + left: event.clientX, + top: event.clientY + }, + }); + + highlightedAddressAnchorRef.current = target; + target.classList.add("disassembly-menu-open"); + + break; } + case "branch": { + const branchTarget = target.getAttribute("data-target")!; + 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; - } + onSelectTarget({ + type: (target.getAttribute("data-type") === "call") ? "function" : "instruction", + address: BigInt(branchTarget) + }); - 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]); + break; + } + } + }, [handleMenuClose, unhookedAddressMenu]); if (isLoading) { return ( diff --git a/apps/tracer/src/EventView.tsx b/apps/tracer/src/EventView.tsx index 426a0b4f..8426627c 100644 --- a/apps/tracer/src/EventView.tsx +++ b/apps/tracer/src/EventView.tsx @@ -14,7 +14,7 @@ export interface EventViewProps { } export type EventActionHandler = (handlerId: HandlerId, eventIndex: number) => void; -export type DisassembleHandler = (address: string) => void; +export type DisassembleHandler = (address: bigint) => void; export type SymbolicateHandler = (addresses: bigint[]) => Promise; const NON_BLOCKING_SPACE = "\u00A0"; @@ -106,7 +106,7 @@ export default function EventView({ Caller - + ) : null @@ -115,7 +115,8 @@ export default function EventView({ Backtrace - {backtrace.map((address, i) => )} diff --git a/apps/tracer/src/MemoryView.css b/apps/tracer/src/MemoryView.css new file mode 100644 index 00000000..86ac4695 --- /dev/null +++ b/apps/tracer/src/MemoryView.css @@ -0,0 +1,9 @@ +.memory-view { + padding: 5px; + overflow: auto; + font-family: monospace; + font-size: 10px; + background-color: #1e1e1e; + color: #e4e4e4; + user-select: text; +} diff --git a/apps/tracer/src/MemoryView.tsx b/apps/tracer/src/MemoryView.tsx new file mode 100644 index 00000000..d9298c35 --- /dev/null +++ b/apps/tracer/src/MemoryView.tsx @@ -0,0 +1,49 @@ +import "./MemoryView.css"; +import { Spinner } from "@blueprintjs/core"; +import { useR2 } from "@frida/react-use-r2"; +import { useEffect, useState } from "react"; + +export interface MemoryViewProps { + address?: bigint; +} + +export default function MemoryView({ address }: MemoryViewProps) { + const [data, setData] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const { executeR2Command } = useR2(); + + useEffect(() => { + if (address === undefined) { + return; + } + + let ignore = false; + setIsLoading(true); + + async function start() { + const data = await executeR2Command(`x @ 0x${address!.toString(16)}`); + if (ignore) { + return; + } + + setData(data); + setIsLoading(false); + } + + start(); + + return () => { + ignore = true; + }; + }, [address, executeR2Command]); + + if (isLoading) { + return ( + + ); + } + + return ( +
+ ); +}