Disabling Automatic Synchronization of IDA Cursor with GDB Current Address in pwndbg

TOC

  1. 1. Background
  2. 2. Process
  3. 3. Conclusion

The IDA support in pwndbg has significantly boosted my efficiency in reverse engineering, but its automatic synchronization of the GDB address and IDA cursor has had a counterproductive effect for me.

Background

Every time I use ida_script.py, the automatic synchronization of the GDB address and the IDA cursor in pwndbg doesn’t feel quite right. Sometimes, when I’m examining a different function and I step through in GDB, the IDA cursor jumps to the current GDB address. However, I don’t want it to switch to the GDB address as this interrupts my analysis of other functions.

Since I find this feature unsuitable for me, I decided to disable the automatic synchronization. I believe the built-in j command already meets my needs, allowing me to manually trigger synchronization rather than having it occur automatically.

Process

I am not a core developer of pwndbg and am unfamiliar with its main logic. However, I don’t need to understand its entire architecture; I just want to disable the automatic cursor synchronization feature.

First, I searched for IDA-related strings to find the main module responsible for this feature.

$ grep -w ida `find . | grep -E 'py$'`
./ida_script.py: "ida": idaapi.get_kernel_version(),
./pwndbg/arguments.py:import pwndbg.ida
./pwndbg/arguments.py: typename = pwndbg.ida.GetType(target)
./pwndbg/enhance.py: if pwndbg.ida.available() and not pwndbg.ida.GetFunctionName(value):
./pwndbg/emu/emulator.py: if pwndbg.ida.available() and not pwndbg.ida.GetFunctionName(value):
./pwndbg/commands/context.py:import pwndbg.ida
./pwndbg/commands/context.py: if not pwndbg.ida.available():
./pwndbg/commands/context.py: code = pwndbg.ida.decompile_context(pwndbg.gdblib.regs.pc, n)
./pwndbg/commands/ida.py:import pwndbg.ida
./pwndbg/commands/ida.py:@pwndbg.ida.withIDA
./pwndbg/commands/ida.py: pwndbg.ida.Jump(pc)
./pwndbg/commands/ida.py:@pwndbg.commands.ArgparsedCommand("Save the ida database.", category=CommandCategory.INTEGRATIONS)
./pwndbg/commands/ida.py:@pwndbg.ida.withIDA
./pwndbg/commands/ida.py: if not pwndbg.ida.available():
./pwndbg/commands/ida.py: path = pwndbg.ida.GetIdbPath()
./pwndbg/commands/ida.py: pwndbg.ida.SaveBase(path)
./pwndbg/commands/ida.py: backups = os.path.join(dirname, "ida-backup")
./pwndbg/commands/ida.py: pwndbg.ida.SaveBase(full_path)
./pwndbg/commands/ida.py:def ida(name):
./pwndbg/commands/ida.py: """Evaluate ida.LocByName() on the supplied value."""
./pwndbg/commands/ida.py: result = pwndbg.ida.LocByName(name)
./pwndbg/commands/ida.py: raise ValueError("ida.LocByName(%r) == BADADDR" % name)
./pwndbg/commands/version.py:import pwndbg.ida
./pwndbg/commands/version.py: ida_versions = pwndbg.ida.get_ida_versions()
./pwndbg/commands/version.py: ida_version = f"IDA PRO: {ida_versions['ida']}"
./pwndbg/commands/__init__.py: import pwndbg.commands.ida
./pwndbg/disasm/__init__.py:import pwndbg.ida
./pwndbg/ida.py: "ida-rpc-host", "127.0.0.1", "ida xmlrpc server address"
./pwndbg/ida.py:ida_rpc_port = pwndbg.gdblib.config.add_param("ida-rpc-port", 31337, "ida xmlrpc server port")
./pwndbg/ida.py: "ida-enabled", False, "whether to enable ida integration"
./pwndbg/ida.py: "ida-timeout", 2, "time to wait for ida xmlrpc in seconds"
./pwndbg/ida.py: + message.hint("set ida-timeout <new-timeout-in-seconds>")
./pwndbg/ida.py: + message.hint("set ida-enabled off")
./pwndbg/gdblib/nearpc.py:import pwndbg.ida
./pwndbg/gdblib/nearpc.py: ColorParamSpec("ida-anterior", "bold", "color for nearpc command (IDA anterior)"),
./pwndbg/gdblib/nearpc.py: pre = pwndbg.ida.Anterior(instr.address)
./pwndbg/gdblib/symbol.py:import pwndbg.ida
./pwndbg/gdblib/symbol.py: res = pwndbg.ida.Name(address) or pwndbg.ida.GetFuncOffset(address)
./tests/gdb-tests/tests/test_loads.py: "pwndbg: created $rebase, $base, $ida GDB functions (can be used with print/break)",

From this, I deduced that ./pwndbg/commands/ida.py contains the main logic.

Since the cursor synchronization function and the j command are equivalent, I inferred that their implementation logic is also equivalent.

j                                  Synchronize IDA's cursor with GDB

I located the implementation logic through the string Synchronize IDA's cursor with GDB.

@pwndbg.commands.ArgparsedCommand(
"Synchronize IDA's cursor with GDB.", category=CommandCategory.INTEGRATIONS
)
@pwndbg.commands.OnlyWhenRunning
@pwndbg.gdblib.events.stop
@pwndbg.ida.withIDA
def j(*args) -> None:
"""
Synchronize IDA's cursor with GDB
"""
try:
pc = int(gdb.selected_frame().pc())
pwndbg.ida.Jump(pc)
except Exception:
pass

This command uses a lower-level xmlrpc call to invoke IDA’s jumpto function.

@pwndbg.decorators.only_after_first_prompt()
@pwndbg.gdblib.config.trigger(ida_rpc_host, ida_rpc_port, ida_enabled, ida_timeout)
def init_ida_rpc_client() -> None:
global _ida, _ida_last_exception, _ida_last_connection_check

if not ida_enabled:
return

now = time.time()
if _ida is None and (now - _ida_last_connection_check) < int(ida_timeout) + 5:
return

addr = f"http://{ida_rpc_host}:{ida_rpc_port}"

_ida = xmlrpc.client.ServerProxy(addr)

...

@withIDA
@takes_address
def Jump(addr):
# uses C++ api instead of idc one to avoid activating the IDA window
return _ida.jumpto(addr, -1, 0)

To confirm if this is the underlying code for automatic cursor synchronization, we need to set a breakpoint in the def j(*args) function and observe its call.

For demonstration purposes, I’ll use traceback to show the call.

@pwndbg.commands.ArgparsedCommand(
"Synchronize IDA's cursor with GDB.", category=CommandCategory.INTEGRATIONS
)
@pwndbg.commands.OnlyWhenRunning
@pwndbg.gdblib.events.stop
@pwndbg.ida.withIDA
def j(*args) -> None:
"""
Synchronize IDA's cursor with GDB
"""
try:
pc = int(gdb.selected_frame().pc())
+ import traceback
+ traceback.print_exc()
pwndbg.ida.Jump(pc)
except Exception:
pass

Then trigger the automatic cursor synchronization function. If it outputs call information, it means we’ve found the corresponding code. The result is as follows:

------- tip of the day (disable with set show-tips off) -------
Use GDB's dprintf command to print all calls to given function. E.g. dprintf malloc, "malloc(%p)\n", (void*)$rdi will print all malloc calls
File "/usr/share/pwndbg/pwndbg/gdblib/prompt.py", line 56, in initial_hook
prompt_hook(*a)
File "/usr/share/pwndbg/pwndbg/gdblib/prompt.py", line 73, in prompt_hook
pwndbg.gdblib.events.after_reload(start=cur is None)
File "/usr/share/pwndbg/pwndbg/gdblib/events.py", line 191, in after_reload
f()
File "/usr/share/pwndbg/pwndbg/gdblib/events.py", line 118, in caller
func()
File "/usr/share/pwndbg/pwndbg/ida.py", line 134, in __call__
return self.fn(*args, **kwargs)
File "/usr/share/pwndbg/pwndbg/commands/ida.py", line 31, in j
traceback.print_stack()

The result shows that the automatic cursor synchronization does trigger this code. Based on the call information, we trace back to find the trigger logic.

After checking the prompt.py and events.py files without finding the trigger logic, I guessed that it might have registered relevant events. The most related one is @pwndbg.gdblib.events.stop. Removing this event will disable the automatic cursor synchronization feature.

Conclusion

To disable the automatic cursor synchronization, you only need to add the @pwndbg.gdblib.events.stop annotation. Here is the implementation logic:

sed -i 's/@pwndbg.gdblib.events.stop/#@pwndbg.gdblib.events.stop/' pwndbg/commands/ida.py

After applying this change, the automatic synchronization between IDA’s cursor and GDB’s current address will be disabled, allowing you to control synchronization manually with the j command when needed.