Skip to content

Feature Request: Add relocation/rebasing tools for runtime-vs-static address mapping #449

Description

@12UE

Motivation

Currently ida-pro-mcp has no built-in tools for handling address relocation — a critical workflow when doing dynamic + static hybrid analysis. The typical scenario:

  1. Load a binary in IDA (static base, e.g. 0x140000000)
  2. Launch the same binary in a debugger (x64dbg/x64wrt/WinDbg) where it loads at a runtime base (e.g. 0x7FFE12340000)
  3. Use xrefs_to, decompile, disasm via MCP to analyze the binary

Without relocation tools, the agent must manually compute address offsets each time it converts between IDA addresses and debugger addresses. This is error-prone and wastes tokens on repeated LLM arithmetic. Three missing primitives would fix this cleanly:

  • Calculate the delta without modifying the IDB (read-only, safe)
  • Convert addresses in both directions (IDA ↔ runtime)
  • Optionally rebase the entire IDB to match runtime (persistent, for longer sessions)

Proposed Tools

A new module src/ida_pro_mcp/ida_mcp/api_relocation.py with four tools:

from typing import Annotated, TypedDict, NotRequired
import idaapi
from .rpc import tool
from .sync import idasync
from .utils import parse_address


class RelocationDeltaResult(TypedDict):
    """Result of calculating the delta between static and runtime base."""
    ida_imagebase: str       # e.g. "0x140000000"
    runtime_base: str        # e.g. "0x7ffe12340000"
    delta: str               # e.g. "0x7fbe12340000" (signed hex)
    delta_int: int           # signed integer delta
    idb_path: str            # current IDB path for context
    input_file: str          # original input file name


class AddressConversionResult(TypedDict):
    """Result of converting a single address between IDA and runtime."""
    input_address: str       # e.g. "0x140001000"
    output_address: str      # e.g. "0x7ffe12341000"
    direction: str           # "ida_to_runtime" or "runtime_to_ida"
    delta: str               # applied delta
    references: NotRequired[list[dict]]
    # For ida_to_runtime: optionally list code refs at the converted address
    # For runtime_to_ida: include function, segment, and xref info


class RebaseResult(TypedDict):
    """Result of rebasing the IDB."""
    old_imagebase: str
    new_imagebase: str
    delta: str
    delta_int: int
    ok: bool
    warning: NotRequired[str]


@tool
def calculate_relocation_delta(
    runtime_base: Annotated[
        str,
        "Runtime base address from debugger, in hex (e.g. '0x7FFE12340000' or '7FFE12340000')"
    ],
) -> RelocationDeltaResult:
    """Calculate the relocation delta between IDA's static base and a runtime base.

    Pure read-only operation — does NOT modify the IDB. Use this FIRST to
    verify the offset before converting addresses or rebasing.

    Returns the signed delta so the agent can understand both the magnitude
    and direction of the relocation. A positive delta means the runtime base
    is higher than the IDA base (common for ASLR'd 64-bit binaries).

    Example:
      IDA imagebase: 0x140000000
      Runtime base:  0x7FFE12340000
      Delta:         0x7FBE12340000 (positive, runtime > IDA)
    """
    try:
        rt_base = int(runtime_base, 0)
    except ValueError as e:
        return {
            "ida_imagebase": hex(idaapi.get_imagebase()),
            "runtime_base": runtime_base,
            "delta": "0x0",
            "delta_int": 0,
            "idb_path": "",
            "input_file": "",
        }  # error case handled by MCP framework

    ida_base = idaapi.get_imagebase()
    delta = rt_base - ida_base

    import idc
    import ida_nalt

    return {
        "ida_imagebase": hex(ida_base),
        "runtime_base": hex(rt_base),
        "delta": hex(delta),
        "delta_int": delta,
        "idb_path": idc.get_idb_path() or "",
        "input_file": ida_nalt.get_input_file_path() or "",
    }


@tool
def convert_ida_to_runtime(
    ida_address: Annotated[
        str,
        "IDA static address (hex), e.g. '0x140001000' or 'main' (resolved via name)"
    ],
    runtime_base: Annotated[
        str,
        "Runtime base address from debugger, in hex (e.g. '0x7FFE12340000')"
    ],
) -> AddressConversionResult:
    """Convert an IDA address to its runtime address in the debugged process.

    Resolves named symbols (e.g. 'main', 'DllMain') to their IDA addresses
    automatically. Use this when the agent needs to tell the debugger where
    to set breakpoints or read memory.

    The result includes optional code reference info so the agent can
    immediately understand the context at the converted address.
    """
    try:
        ida_ea = parse_address(ida_address)
    except Exception:
        # Fallback: try name resolution
        ea = idaapi.get_name_ea(idaapi.BADADDR, str(ida_address))
        if ea == idaapi.BADADDR:
            # Return error in result dict pattern
            return {
                "input_address": str(ida_address),
                "output_address": "0x0",
                "direction": "ida_to_runtime",
                "delta": "0x0",
            }
        ida_ea = ea

    rt_base = int(runtime_base, 0)
    delta = rt_base - idaapi.get_imagebase()
    rt_ea = ida_ea + delta

    result: AddressConversionResult = {
        "input_address": hex(ida_ea),
        "output_address": hex(rt_ea),
        "direction": "ida_to_runtime",
        "delta": hex(delta),
    }

    # Enrich with context if possible
    try:
        import idautils
        refs = []
        for xr in idautils.XrefsFrom(ida_ea, 0):
            if len(refs) >= 10:
                break
            fn = idaapi.get_func(xr.to)
            refs.append({
                "addr": hex(xr.to),
                "name": (idaapi.get_name(xr.to) or hex(xr.to)),
                "type": "code" if xr.iscode else "data",
            })
        if refs:
            result["references"] = refs
    except Exception:
        pass

    return result


@tool
def convert_runtime_to_ida(
    runtime_address: Annotated[
        str,
        "Runtime address from debugger, in hex (e.g. '0x7FFE12341000')"
    ],
    runtime_base: Annotated[
        str,
        "Runtime base address (module base in debugger), in hex"
    ],
) -> AddressConversionResult:
    """Convert a debugger runtime address back to an IDA static address.

    Use this when the debugger hits a breakpoint or shows a callstack
    entry, and the agent needs to decompile or analyze that location
    in IDA. Includes function/segment/xref context at the resolved IDA
    address to save round-trips.
    """
    try:
        rt_ea = int(runtime_address, 0)
    except ValueError:
        return {
            "input_address": str(runtime_address),
            "output_address": "0x0",
            "direction": "runtime_to_ida",
            "delta": "0x0",
        }

    rt_base = int(runtime_base, 0)
    delta = rt_base - idaapi.get_imagebase()
    ida_ea = rt_ea - delta

    result: AddressConversionResult = {
        "input_address": hex(rt_ea),
        "output_address": hex(ida_ea),
        "direction": "runtime_to_ida",
        "delta": hex(delta),
    }

    # Enrich with segment and function context
    try:
        refs = []
        seg = idaapi.getseg(ida_ea)
        if seg:
            refs.append({
                "addr": hex(ida_ea),
                "segment": idaapi.get_segm_name(seg),
                "permissions": f"{'r' if seg.perm & 4 else ''}{'w' if seg.perm & 2 else ''}{'x' if seg.perm & 1 else ''}",
            })
        func = idaapi.get_func(ida_ea)
        if func:
            refs.append({
                "addr": hex(ida_ea),
                "function": idaapi.get_name(func.start_ea) or hex(func.start_ea),
                "offset_from_start": hex(ida_ea - func.start_ea),
            })
        if refs:
            result["references"] = refs
    except Exception:
        pass

    return result


@tool
@idasync
def set_relocation_base(
    runtime_base: Annotated[
        str,
        "New runtime base to rebase the IDB to, in hex (e.g. '0x7FFE12340000')"
    ],
) -> RebaseResult:
    """Permanently rebase the IDB to match a runtime base address.

    THIS MODIFIES THE DATABASE. After rebasing, all IDA addresses will
    match the debugger's view without needing conversion. Uses
    idaapi.rebase_program() with MSF_NOFIX=0 to update all references.

    Warning: this is a destructive operation on the IDB. Consider saving
    a copy first, or using calculate_relocation_delta + conversion tools
    instead for read-only workflows.

    Only call this when the agent and user agree that a persistent rebase
    is appropriate (e.g., for a long analysis session of a relocated DLL).
    """
    old_base = idaapi.get_imagebase()
    new_base = int(runtime_base, 0)
    delta = new_base - old_base

    if delta == 0:
        return {
            "old_imagebase": hex(old_base),
            "new_imagebase": hex(new_base),
            "delta": "0x0",
            "delta_int": 0,
            "ok": True,
            "warning": "No rebase needed — runtime base matches IDA imagebase",
        }

    ok = idaapi.rebase_program(delta, 0)  # MSF_NOFIX = 0

    return {
        "old_imagebase": hex(old_base),
        "new_imagebase": hex(idaapi.get_imagebase()),
        "delta": hex(delta),
        "delta_int": delta,
        "ok": ok == 0,  # rebase_program returns 0 on success
    }

Integration

One line addition to src/ida_pro_mcp/ida_mcp/__init__.py:

 from . import api_core
 from . import api_analysis
 from . import api_memory
 from . import api_types
 from . import api_modify
 from . import api_stack
 from . import api_debug
 from . import api_python
 from . import api_resources
 from . import api_survey
 from . import api_composite
+from . import api_relocation
 from . import trace as trace
 from . import api_sigmaker

Design Decisions

  • Tools 1-3 (calculate_relocation_delta, convert_*) are NOT decorated with @idasync — they don't modify IDA state and can run without the main thread lock. This avoids blocking other MCP operations.
  • set_relocation_base IS @idasync decorated — it mutates the entire IDB and must run on IDA's main thread.
  • Error handling follows the existing pattern — returns structured dicts instead of raising exceptions. The MCP framework converts typed dicts with error fields appropriately.
  • Address resolution is flexibleconvert_ida_to_runtime accepts both hex addresses and symbol names via the existing parse_address() utility.
  • Context enrichment is best-effort — the references field in conversion results is populated when possible but never blocks on failure (wrapped in try/except).

Real-World Use Case

Agent workflow with x64dbg:

1. calculate_relocation_delta("0x7FFE12340000")
   → {delta: "0x7fbe12340000", delta_int: 54879283003392}
   Agent knows: IDA base 0x140000000 → runtime base 0x7FFE12340000

2. decompile("DllGetClassObject")
   → Finds interesting code at IDA 0x140001234

3. convert_ida_to_runtime("0x140001234", "0x7FFE12340000")
   → {input: "0x140001234", output: "0x7ffe12341234"}
   Agent: "Set breakpoint at 0x7ffe12341234 in x64dbg"

4. [Debugger hits breakpoint at 0x7ffe12345000]

5. convert_runtime_to_ida("0x7ffe12345000", "0x7FFE12340000")
   → {input: "0x7ffe12345000", output: "0x140005000",
      references: [{function: "sub_140005000", offset_from_start: "0x0"}]}
   Agent: "Analyzing sub_140005000..."
   decompile("0x140005000")

Precedence

These tools have been working reliably in a custom MCP server for several months (used daily with x64dbg + OpenCode). The implementation is based on idaapi.rebase_program() and simple address arithmetic — no fragile hacks.

Checklist

  • Works in both GUI and headless (idalib-mcp) modes — only depends on idaapi.get_imagebase() and idaapi.rebase_program()
  • No new dependencies — all imports are from existing ida_* modules and the repository's own utils.py
  • Matches existing code style — TypedDict results, @tool + @idasync decorators, Annotated parameters
  • Backward compatible — zero changes to existing APIs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions