Files
luma/Agent/core/modules.ts
Ole André Vadla Ravnås 21121accb1 disassembler: Ask the agent for functions only
Analysis pulled every export, import and symbol across the wire
and then kept the handful that name code -- 87k rows for a
kernel, each carrying a type and an address as text.

Ask for what it uses instead: the offset, name and source of each
function the module knows about, chosen where the rows already
are.
2026-09-09 23:27:01 +02:00

352 lines
10 KiB
TypeScript

export interface ModuleSymbolBundle {
exports: ExportEntry[];
imports: ImportEntry[];
symbols: SymbolEntry[];
}
export interface ExportEntry {
type: ModuleExportType;
name: string;
address: string;
}
export interface ImportEntry {
type?: ModuleImportType;
name: string;
module?: string;
address?: string;
slot?: string;
}
export interface SymbolEntry {
name: string;
type: ModuleSymbolType;
address: string;
isGlobal: boolean;
size?: number;
sectionID?: string;
sectionProtection?: PageProtection;
}
export interface ModuleRangeEntry {
offset: string;
size: number;
protection: string;
}
export interface ProcessRangeEntry {
base: string;
size: number;
protection: string;
filePath: string | null;
}
export function enumerateModuleRanges(name: string): ModuleRangeEntry[] {
const module = Process.getModuleByName(name);
return module.enumerateRanges('---').map(r => ({
offset: '0x' + r.base.sub(module.base).toString(16),
size: r.size,
protection: r.protection,
}));
}
export function findRangeByAddress(address: string): ProcessRangeEntry | null {
const r = Process.findRangeByAddress(ptr(address));
if (r === null) return null;
return {
base: r.base.toString(),
size: r.size,
protection: r.protection,
filePath: r.file ? r.file.path : null,
};
}
export function getModuleIdentity(name: string): string | null {
const module = Process.getModuleByName(name);
const base = module.base;
const magic = base.readU32();
if (magic === 0xfeedfacf || magic === 0xfeedface) {
const ncmds = base.add(magic === 0xfeedfacf ? 16 : 16).readU32();
const headerSize = magic === 0xfeedfacf ? 32 : 28;
let cursor = base.add(headerSize);
for (let i = 0; i < ncmds; i++) {
const cmd = cursor.readU32();
const cmdsize = cursor.add(4).readU32();
if (cmd === 0x1b) {
const bytes = cursor.add(8).readByteArray(16);
if (bytes !== null) {
return Array.from(new Uint8Array(bytes))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
}
cursor = cursor.add(cmdsize);
}
}
return null;
}
export interface ModuleFunctionEntry {
offset: number;
name: string;
source: "exported" | "symbol";
}
export function enumerateModuleFunctions(name: string): ModuleFunctionEntry[] {
const index = getModuleIndex(name);
const { base, size } = index.module;
const lower = base;
const upper = base.add(size);
const functions: ModuleFunctionEntry[] = [];
const taken = new Set<number>();
for (const row of collectedRows(index, "exports") as ModuleExportDetails[]) {
const { address } = row;
if (row.type !== "function" || address.compare(lower) < 0 || address.compare(upper) >= 0) {
continue;
}
const offset = address.sub(lower).toUInt32();
taken.add(offset);
functions.push({ offset, name: row.name, source: "exported" });
}
for (const row of collectedRows(index, "symbols") as ModuleSymbolDetails[]) {
const { address } = row;
if (!holdsCode(row) || address.compare(lower) <= 0 || address.compare(upper) >= 0) {
continue;
}
const offset = address.sub(lower).toUInt32();
if (taken.has(offset)) {
continue;
}
taken.add(offset);
functions.push({ offset, name: row.name, source: "symbol" });
}
return functions;
}
function holdsCode(row: ModuleSymbolDetails): boolean {
const { type, section } = row;
if (type === "undefined") {
return false;
}
if (type === "function") {
return true;
}
return section?.protection.includes("x") ?? false;
}
export function enumerateModuleSymbols(name: string): ModuleSymbolBundle {
const index = getModuleIndex(name);
return {
exports: reported(index, "exports") as ExportEntry[],
imports: reported(index, "imports") as ImportEntry[],
symbols: reported(index, "symbols") as SymbolEntry[],
};
}
export type SymbolCategory = "exports" | "imports" | "symbols";
export interface SymbolQueryRequest {
module: string;
category: SymbolCategory;
query: string;
offset: number;
limit: number;
}
export interface SymbolPage {
rows: object[];
matched: number;
offset: number;
counts: { exports: number; imports: number; symbols: number };
}
export function queryModuleSymbols(request: SymbolQueryRequest): SymbolPage {
const index = getModuleIndex(request.module);
const rows = sortedRows(index, request.category);
const needle = request.query.toLowerCase();
const matched = (needle === "") ? rows : rowsMatching(index, request.category, needle);
const matchedCount = matched.length;
const offset = Math.max(0, Math.min(request.offset, matchedCount - 1));
return {
rows: matched.slice(offset, offset + request.limit)
.map(row => reportedRow(row, request.category)),
matched: matchedCount,
offset,
counts: {
exports: collectedRows(index, "exports").length,
imports: collectedRows(index, "imports").length,
symbols: collectedRows(index, "symbols").length,
},
};
}
type SymbolRow = ModuleExportDetails | ModuleImportDetails | ModuleSymbolDetails;
interface CategoryIndex {
rows: SymbolRow[];
sorted?: SymbolRow[];
searchable?: string[];
}
interface ModuleIndex {
module: Module;
categories: Map<SymbolCategory, CategoryIndex>;
}
const moduleIndexCache = new Map<string, ModuleIndex>();
function getModuleIndex(name: string): ModuleIndex {
let index = moduleIndexCache.get(name);
if (index === undefined) {
index = { module: Process.getModuleByName(name), categories: new Map() };
moduleIndexCache.set(name, index);
}
return index;
}
function rowsMatching(index: ModuleIndex, category: SymbolCategory, needle: string): SymbolRow[] {
const rows = sortedRows(index, category);
const searchable = searchableText(index, category);
const matched: SymbolRow[] = [];
for (let i = 0; i !== rows.length; i++) {
if (searchable[i].includes(needle)) {
matched.push(rows[i]);
}
}
return matched;
}
function sortedRows(index: ModuleIndex, category: SymbolCategory): SymbolRow[] {
const entry = collected(index, category);
if (entry.sorted !== undefined) {
return entry.sorted;
}
const { rows } = entry;
const width = String(rows.length).length;
const decorated = rows.map((row, at) => row.name + "\u0000" + String(at).padStart(width, "0"));
decorated.sort();
const sorted: SymbolRow[] = new Array(rows.length);
for (let i = 0; i !== decorated.length; i++) {
const decoration = decorated[i];
sorted[i] = rows[+decoration.slice(decoration.lastIndexOf("\u0000") + 1)];
}
entry.sorted = sorted;
return sorted;
}
function searchableText(index: ModuleIndex, category: SymbolCategory): string[] {
const entry = collected(index, category);
if (entry.searchable !== undefined) {
return entry.searchable;
}
const rows = sortedRows(index, category);
const searchable = new Array<string>(rows.length);
for (let i = 0; i !== rows.length; i++) {
searchable[i] = searchableRow(rows[i], category);
}
entry.searchable = searchable;
return searchable;
}
function searchableRow(row: SymbolRow, category: SymbolCategory): string {
if (category === "exports") {
const e = row as ModuleExportDetails;
return (e.name + "\u0000" + e.type + "\u0000" + e.address).toLowerCase();
}
if (category === "imports") {
const i = row as ModuleImportDetails;
return (i.name + "\u0000" + (i.module ?? "") + "\u0000" + (i.type ?? "") + "\u0000"
+ (i.address ?? "")).toLowerCase();
}
const s = row as ModuleSymbolDetails;
return (s.name + "\u0000" + s.type + "\u0000" + (s.section?.id ?? "") + "\u0000"
+ s.address).toLowerCase();
}
function collectedRows(index: ModuleIndex, category: SymbolCategory): SymbolRow[] {
return collected(index, category).rows;
}
function collected(index: ModuleIndex, category: SymbolCategory): CategoryIndex {
let entry = index.categories.get(category);
if (entry === undefined) {
entry = { rows: collectRows(index.module, category) };
index.categories.set(category, entry);
}
return entry;
}
function collectRows(module: Module, category: SymbolCategory): SymbolRow[] {
if (category === "exports") {
return module.enumerateExports();
}
if (category === "imports") {
const seen = new Set<string>();
const rows: ModuleImportDetails[] = [];
for (const row of module.enumerateImports()) {
const identity = row.name + "\u0000" + (row.module ?? "");
if (seen.has(identity)) {
continue;
}
seen.add(identity);
rows.push(row);
}
return rows;
}
return module.enumerateSymbols().filter(({ address }) => !address.isNull());
}
function reported(index: ModuleIndex, category: SymbolCategory): object[] {
return sortedRows(index, category).map(row => reportedRow(row, category));
}
function reportedRow(row: SymbolRow, category: SymbolCategory): object {
if (category === "exports") {
const { type, name, address } = row as ModuleExportDetails;
return { type, name, address: address.toString() };
}
if (category === "imports") {
const { name, type, module: origin, address, slot } = row as ModuleImportDetails;
const reported: ImportEntry = { name };
if (type !== undefined) reported.type = type;
if (origin !== undefined) reported.module = origin;
if (address !== undefined) reported.address = address.toString();
if (slot !== undefined) reported.slot = slot.toString();
return reported;
}
const { name, type, address, isGlobal, size, section } = row as ModuleSymbolDetails;
const reported: SymbolEntry = { name, type, address: address.toString(), isGlobal };
if (size !== undefined) reported.size = size;
if (section !== undefined) {
reported.sectionID = section.id;
reported.sectionProtection = section.protection;
}
return reported;
}