Files
luma/Agent/core/resolver.ts

341 lines
11 KiB
TypeScript

import type _Java from "frida-java-bridge";
export interface ResolveRequest {
scope: TargetScope;
query: string;
}
export type TargetScope =
| "function"
| "module"
| "imports"
| "relative-function"
| "absolute-instruction"
| "objc-method"
| "swift-func"
| "java-method"
| "debug-symbol";
export interface ResolvedTarget {
scope: TargetScope;
displayName: string;
detail: string | null;
address: string;
anchor: AnchorJSON;
}
export type AnchorJSON =
| { type: "absolute"; address: string }
| { type: "moduleOffset"; name: string; offset: number }
| { type: "moduleExport"; name: string; export: string }
| { type: "objcMethod"; selector: string }
| { type: "swiftFunc"; module: string; function: string }
| { type: "javaMethod"; className: string; methodName: string }
| { type: "debugSymbol"; name: string };
export async function resolveTargets(request: ResolveRequest): Promise<ResolvedTarget[]> {
const { scope, query } = request;
switch (scope) {
case "function":
return resolveFunctionScope(query);
case "module":
return resolveModuleScope(query);
case "imports":
return resolveImportsScope(query);
case "relative-function":
return resolveRelativeFunctionScope(query);
case "absolute-instruction":
return resolveAbsoluteInstructionScope(query);
case "objc-method":
return resolveObjcMethodScope(query);
case "swift-func":
return resolveSwiftFuncScope(query);
case "java-method":
return await resolveJavaMethodScope(query);
case "debug-symbol":
return resolveDebugSymbolScope(query);
}
}
export async function lookupAnchorAddress(anchor: AnchorJSON): Promise<string | null> {
return resolveAnchor(anchor)?.toString() ?? null;
}
export function resolveAnchor(anchor: AnchorJSON): NativePointer | null {
switch (anchor.type) {
case "absolute":
return ptr(anchor.address);
case "moduleOffset": {
const module = Process.findModuleByName(anchor.name);
return module?.base.add(anchor.offset) ?? null;
}
case "moduleExport": {
const module = Process.findModuleByName(anchor.name);
return module?.findExportByName(anchor.export) ?? null;
}
case "objcMethod":
return firstMatchAddress(getObjcResolver().enumerateMatches(anchor.selector));
case "swiftFunc":
return firstMatchAddress(
getSwiftResolver().enumerateMatches(`functions:${anchor.module}!${anchor.function}`)
);
case "debugSymbol": {
const matches = DebugSymbol.findFunctionsMatching(anchor.name);
return matches.length > 0 ? matches[0] : null;
}
case "javaMethod":
return null;
}
}
function resolveFunctionScope(query: string): ResolvedTarget[] {
const pattern = query.includes("!") ? query : `*!${query}`;
const matches = getModuleResolver().enumerateMatches(`exports:${pattern}`);
return dedupeMatchesByAddress(matches).map(moduleExportTargetFromMatch);
}
function dedupeMatchesByAddress(matches: ApiResolverMatch[]): ApiResolverMatch[] {
const byAddress = new Map<string, ApiResolverMatch[]>();
for (const match of matches) {
const key = match.address.toString();
const bucket = byAddress.get(key);
if (bucket === undefined) {
byAddress.set(key, [match]);
} else {
bucket.push(match);
}
}
return Array.from(byAddress.values()).map(pickDefiningMatch);
}
function pickDefiningMatch(candidates: ApiResolverMatch[]): ApiResolverMatch {
if (candidates.length === 1) {
return candidates[0];
}
const address = candidates[0].address;
const owning = candidates.find(c => {
const module = Process.findModuleByName(splitScopedName(c.name)[0]);
if (module === null) {
return false;
}
const base = module.base;
return address.compare(base) >= 0 && address.compare(base.add(module.size)) < 0;
});
return owning ?? candidates[0];
}
function resolveModuleScope(query: string): ResolvedTarget[] {
return getModuleResolver()
.enumerateMatches(`exports:${query}!*`)
.map(moduleExportTargetFromMatch);
}
function resolveImportsScope(query: string): ResolvedTarget[] {
const pattern = query.length > 0 ? query : Process.enumerateModules()[0].path;
return getModuleResolver()
.enumerateMatches(`imports:${pattern}!*`)
.map(moduleExportTargetFromMatch);
}
function resolveRelativeFunctionScope(query: string): ResolvedTarget[] {
const { module, offset } = parseRelativeFunctionPattern(query);
const base = Process.findModuleByName(module);
if (base === null) {
return [];
}
const address = base.base.add(offset);
return [{
scope: "relative-function",
displayName: `${module}+0x${offset.toString(16)}`,
detail: null,
address: address.toString(),
anchor: { type: "moduleOffset", name: module, offset },
}];
}
function resolveAbsoluteInstructionScope(query: string): ResolvedTarget[] {
const address = ptr(query);
return [{
scope: "absolute-instruction",
displayName: address.toString(),
detail: null,
address: address.toString(),
anchor: { type: "absolute", address: address.toString() },
}];
}
function resolveObjcMethodScope(query: string): ResolvedTarget[] {
return getObjcResolver().enumerateMatches(query).map(match => ({
scope: "objc-method",
displayName: match.name,
detail: null,
address: match.address.toString(),
anchor: { type: "objcMethod", selector: match.name },
}));
}
function resolveSwiftFuncScope(query: string): ResolvedTarget[] {
return getSwiftResolver().enumerateMatches(`functions:${query}`).map(match => {
const [moduleName, funcName] = splitScopedName(match.name);
return {
scope: "swift-func",
displayName: funcName,
detail: moduleName,
address: match.address.toString(),
anchor: { type: "swiftFunc", module: moduleName, function: funcName },
};
});
}
async function resolveJavaMethodScope(query: string): Promise<ResolvedTarget[]> {
const Java = await loadJavaBridge();
const targets: ResolvedTarget[] = [];
Java.perform(() => {
for (const group of Java.enumerateMethods(query)) {
for (const klass of group.classes) {
for (const methodName of klass.methods) {
const bareName = bareJavaMethodName(methodName);
targets.push({
scope: "java-method",
displayName: `${klass.name}.${bareName}`,
detail: klass.name,
address: "0x0",
anchor: { type: "javaMethod", className: klass.name, methodName: bareName },
});
}
}
}
});
return targets;
}
export async function loadJavaBridge(): Promise<typeof _Java> {
const cached = cachedJavaBridge;
if (cached !== null) {
return requireAvailable(cached);
}
let bridge: typeof _Java;
try {
const mod = await import("frida-java-bridge");
bridge = (mod as any).default ?? mod;
} catch (e) {
throw new Error("The 'frida-java-bridge' package is required for Java tracing.");
}
cachedJavaBridge = bridge;
return requireAvailable(bridge);
}
function requireAvailable(bridge: typeof _Java): typeof _Java {
if (!bridge.available) {
throw new Error("No Java runtime detected in this process.");
}
return bridge;
}
function bareJavaMethodName(name: string): string {
const paren = name.indexOf("(");
return (paren < 0) ? name : name.substring(0, paren);
}
function resolveDebugSymbolScope(query: string): ResolvedTarget[] {
return DebugSymbol.findFunctionsMatching(query).map(address => {
const symbol = DebugSymbol.fromAddress(address);
const name = symbol.name ?? address.toString();
return {
scope: "debug-symbol",
displayName: name,
detail: symbol.moduleName ?? null,
address: address.toString(),
anchor: { type: "debugSymbol", name },
};
});
}
function moduleExportTargetFromMatch(match: ApiResolverMatch): ResolvedTarget {
const [moduleName, symbolName] = splitScopedName(match.name);
return {
scope: "function",
displayName: symbolName,
detail: moduleName,
address: match.address.toString(),
anchor: { type: "moduleExport", name: moduleName, export: symbolName },
};
}
function firstMatchAddress(matches: ApiResolverMatch[]): NativePointer | null {
return matches.length > 0 ? matches[0].address : null;
}
function splitScopedName(name: string): [string, string] {
const bang = name.indexOf("!");
if (bang < 0) {
return ["", name];
}
return [name.substring(0, bang), name.substring(bang + 1)];
}
function parseRelativeFunctionPattern(pattern: string): { module: string; offset: number } {
const bang = pattern.indexOf("!");
if (bang < 0) {
throw new Error("Expected MODULE!OFFSET");
}
const module = pattern.substring(0, bang);
const offset = parseInt(pattern.substring(bang + 1), 16);
if (isNaN(offset)) {
throw new Error("Invalid offset (expected hex)");
}
return { module, offset };
}
let cachedModuleResolver: ApiResolver | null = null;
let cachedObjcResolver: ApiResolver | null = null;
let cachedSwiftResolver: ApiResolver | null = null;
let cachedJavaBridge: typeof _Java | null = null;
function getModuleResolver(): ApiResolver {
if (cachedModuleResolver === null) {
cachedModuleResolver = new ApiResolver("module");
}
return cachedModuleResolver;
}
function getObjcResolver(): ApiResolver {
if (cachedObjcResolver === null) {
try {
cachedObjcResolver = new ApiResolver("objc");
} catch (e) {
throw new Error("Objective-C runtime is not available in this process");
}
}
return cachedObjcResolver;
}
function getSwiftResolver(): ApiResolver {
if (cachedSwiftResolver === null) {
try {
cachedSwiftResolver = new ApiResolver("swift" as ApiResolverType);
} catch (e) {
throw new Error("Swift runtime is not available in this process");
}
}
return cachedSwiftResolver;
}