From 9cf60924142c4124a50822c1e3bd85cf31ae9a91 Mon Sep 17 00:00:00 2001 From: jxuanli <65455765+jxuanli@users.noreply.github.com> Date: Tue, 26 Aug 2025 02:56:31 -0700 Subject: [PATCH] Improving ghidra decompilation support (#3269) * decompiling kernel funcs * accounting for KASLR * handling kernel modules * cleaning up * doc * changes based on comments --- docs/configuration/config.md | 24 ++++++------- pwndbg/aglib/elf.py | 8 +++++ pwndbg/aglib/file.py | 2 +- pwndbg/aglib/kernel/__init__.py | 4 +-- pwndbg/commands/kmod.py | 10 ++++-- pwndbg/commands/radare2.py | 9 +---- pwndbg/commands/rizin.py | 9 +---- pwndbg/ghidra.py | 63 ++++++++++++++++++--------------- pwndbg/radare2.py | 36 ++++++++++++++++--- pwndbg/rizin.py | 35 +++++++++++++++--- 10 files changed, 127 insertions(+), 73 deletions(-) diff --git a/docs/configuration/config.md b/docs/configuration/config.md index 3e7c3de36..c09bc39d8 100644 --- a/docs/configuration/config.md +++ b/docs/configuration/config.md @@ -399,6 +399,18 @@ Display internal event debugging info. ---------- +## **decompiler** + + +Framework that your ghidra plugin installed. + + + +**Default:** 'radare2' +**Valid values:** 'radare2', 'rizin' + +---------- + ## **default-visualize-chunk-number** @@ -828,18 +840,6 @@ Whether to show call arguments below instruction. ---------- -## **r2decompiler** - - -Framework that your ghidra plugin installed. - - - -**Default:** 'radare2' -**Valid values:** 'radare2', 'rizin' - ----------- - ## **safe-linking** diff --git a/pwndbg/aglib/elf.py b/pwndbg/aglib/elf.py index 20e09fcf3..648e67ce4 100644 --- a/pwndbg/aglib/elf.py +++ b/pwndbg/aglib/elf.py @@ -202,6 +202,14 @@ def get_containing_sections(elf_filepath: str, elf_loadaddr: int, vaddr: int): return sections +def get_vmlinux_unrand_base(elf_filepath: str): + elf = get_elf_info(elf_filepath) + for seg in elf.segments: + if seg["p_type"] == "PT_LOAD": + return seg["p_vaddr"] + return None + + def dump_section_by_name( filepath: str, section_name: str, try_local_path: bool = False ) -> Tuple[int, int, bytes] | None: diff --git a/pwndbg/aglib/file.py b/pwndbg/aglib/file.py index b9f62ddac..84e900962 100644 --- a/pwndbg/aglib/file.py +++ b/pwndbg/aglib/file.py @@ -90,7 +90,7 @@ def get_file(path: str, try_local_path: bool = False) -> str: path = path[7:] # len('target:') == 7 local_path = path - if not pwndbg.aglib.remote.is_remote(): + if not pwndbg.aglib.remote.is_remote() or pwndbg.aglib.qemu.is_qemu_kernel(): if not os.path.exists(local_path): raise OSError(f"File '{local_path}' does not exist", errno.ENOENT) diff --git a/pwndbg/aglib/kernel/__init__.py b/pwndbg/aglib/kernel/__init__.py index 2766d2086..51daebac8 100644 --- a/pwndbg/aglib/kernel/__init__.py +++ b/pwndbg/aglib/kernel/__init__.py @@ -268,7 +268,7 @@ class ArchOps(ABC): return arch_paginginfo().vmemmap @property - def kbase(self) -> int: + def kbase(self) -> int | None: return arch_paginginfo().kbase @property @@ -365,7 +365,7 @@ class x86_64Ops(x86Ops): return pwndbg.dbg.selected_inferior().create_value(per_cpu_addr) def virt_to_phys(self, virt: int) -> int: - if virt < self.kbase: + if self.kbase is None or virt < self.kbase: return (virt - self.page_offset) % (1 << 64) return ((virt - self.kbase) + self.phys_base) % (1 << 64) diff --git a/pwndbg/commands/kmod.py b/pwndbg/commands/kmod.py index 9bc0c3045..c5495cc05 100644 --- a/pwndbg/commands/kmod.py +++ b/pwndbg/commands/kmod.py @@ -82,9 +82,13 @@ def kmod(module_name=None, path=None) -> None: table.append([f"{addr:#x}", name, size, "-"]) if path is not None: if len(table) == 1: - pwndbg.dbg.selected_inferior().add_symbol_file(path, table[0][0]) - return - if len(table) > 1: + addr = table[0][0] + pwndbg.dbg.selected_inferior().add_symbol_file(path, addr) + if pwndbg.config.decompiler == "radare2": + pwndbg.radare2.r2cmd(["o", path, addr]) + elif pwndbg.config.decompiler == "rizin": + pwndbg.rizin.rzcmd(["o", path, addr]) + elif len(table) > 1: print(M.warn("Multiple modules detected with the given filter")) else: print(M.warn("No modules detected with the given filter.")) diff --git a/pwndbg/commands/radare2.py b/pwndbg/commands/radare2.py index b55d5ea27..b76f46313 100644 --- a/pwndbg/commands/radare2.py +++ b/pwndbg/commands/radare2.py @@ -9,7 +9,6 @@ import pwndbg.aglib.proc import pwndbg.aglib.regs import pwndbg.commands import pwndbg.radare2 -from pwndbg.color import message from pwndbg.commands import CommandCategory parser = argparse.ArgumentParser(description="Launches radare2.") @@ -97,10 +96,4 @@ pwndbg> r2pipe pdf @ sym.main ) @pwndbg.commands.OnlyWithFile def r2pipe(arguments) -> None: - try: - r2 = pwndbg.radare2.r2pipe() - print(r2.cmd(" ".join(arguments))) - except ImportError: - print(message.error("Could not import r2pipe python library. Is it installed?")) - except Exception as e: - print(message.error(e)) + print(pwndbg.radare2.r2cmd(arguments)) diff --git a/pwndbg/commands/rizin.py b/pwndbg/commands/rizin.py index 184e6b50e..6fd418e65 100644 --- a/pwndbg/commands/rizin.py +++ b/pwndbg/commands/rizin.py @@ -9,7 +9,6 @@ import pwndbg.aglib.proc import pwndbg.aglib.regs import pwndbg.commands import pwndbg.rizin -from pwndbg.color import message from pwndbg.commands import CommandCategory parser = argparse.ArgumentParser(description="Launches rizin.") @@ -102,10 +101,4 @@ pwndbg> rzpipe pdf @ sym.main ) @pwndbg.commands.OnlyWithFile def rzpipe(arguments) -> None: - try: - rz = pwndbg.rizin.rzpipe() - print(rz.cmd(" ".join(arguments))) - except ImportError: - print(message.error("Could not import rzpipe python library. Is it installed?")) - except Exception as e: - print(message.error(e)) + print(pwndbg.rizin.rzcmd(arguments)) diff --git a/pwndbg/ghidra.py b/pwndbg/ghidra.py index 59873817e..c64e9ca0d 100644 --- a/pwndbg/ghidra.py +++ b/pwndbg/ghidra.py @@ -17,10 +17,9 @@ import pwndbg.rizin if pwndbg.dbg.is_gdblib_available(): import pwndbg.gdblib.symbol -from pwndbg.color import message -r2decompiler = pwndbg.config.add_param( - "r2decompiler", +decompiler = pwndbg.config.add_param( + "decompiler", "radare2", "framework that your ghidra plugin installed", param_class=pwndbg.lib.config.PARAM_ENUM, @@ -28,18 +27,6 @@ r2decompiler = pwndbg.config.add_param( ) -@pwndbg.config.trigger(r2decompiler) -def set_r2decompiler() -> None: - if r2decompiler.value in ["radare2", "rizin"]: - return - print( - message.warn( - f"Invalid r2decompiler: `{r2decompiler.value}`, please select from radare2 or rizin" - ) - ) - r2decompiler.revert_default() - - def decompile(func=None): """ Return the source of the given function decompiled by ghidra. @@ -49,22 +36,24 @@ def decompile(func=None): Raises Exception if any fatal error occurs. """ + func_specified = func is not None try: - if r2decompiler == "radare2": - r2 = pwndbg.radare2.r2pipe() - # LD -> list supported decompilers (e cmd.pdc=?) - # Outputs for example: pdc\npdg - if "pdg" not in r2.cmd("LD").split("\n"): - raise Exception("radare2 plugin r2ghidra must be installed and available from r2") - else: - assert r2decompiler == "rizin" - r2 = pwndbg.rizin.rzpipe() - # Lc -> list core plugins - if "ghidra" not in r2.cmd("Lc"): - raise Exception("rizin plugin rzghidra must be installed and available from rz") + if decompiler == "radare2": + r = pwndbg.radare2.r2pipe() + elif decompiler == "rizin": + r = pwndbg.rizin.rzpipe() except ImportError: raise Exception("r2pipe or rzpipe not available, but required for r2/rz->ghidra bridge") + if pwndbg.aglib.qemu.is_qemu_kernel(): + pc = pwndbg.aglib.regs[pwndbg.aglib.regs.current.pc] + if func is None: + func = pwndbg.aglib.symbol.resolve_addr(pc) + if func is not None: + func = func.split("+")[0] + if func is not None: + func = f"sym.{func}" + if not func: func = ( hex(pwndbg.aglib.regs[pwndbg.aglib.regs.current.pc]) @@ -72,12 +61,14 @@ def decompile(func=None): else "main" ) - src = r2.cmdj("pdgj @ " + func) + r.cmd(f"afr @ {func}") + src = r.cmdj("pdgj @ " + func) if not src: raise Exception(f"Decompile command failed, check if '{func}' is a valid target") current_line_marker = "/*%%PWNDBG_CODE_MARKER%%*/" source = src.get("code", "") + closest_line = 1 # If not running there is no current pc to mark if pwndbg.aglib.proc.alive: @@ -85,6 +76,8 @@ def decompile(func=None): closest = 0 for off in (a.get("offset", 0) for a in src.get("annotations", [])): + if off == 0 and pc > 0x1000: + continue if abs(pc - closest) > abs(pc - off): closest = off pos_annotations = sorted( @@ -94,7 +87,7 @@ def decompile(func=None): # Append code prefix marker for the current line and replace it later if pos_annotations: - curline = source.count("\n", 0, pos_annotations[0]["start"]) + curline = closest_line = source.count("\n", 0, pos_annotations[0]["start"]) source = source.split("\n") line = source[curline] if line.startswith(" "): @@ -114,4 +107,16 @@ def decompile(func=None): # Replace code prefix marker after syntax highlighting source = source.replace(current_line_marker, C.prefix(pwndbg.config.code_prefix), 1) + + if not func_specified: + source = source.split("\n") + n = int(pwndbg.config.context_code_lines) + + # Compute the line range + start = max(closest_line - 1 - n // 2, 0) + end = min(closest_line - 1 + n // 2 + 1, len(source)) + + # split the code + source = source[start:end] + source = "\n".join(source) return source diff --git a/pwndbg/radare2.py b/pwndbg/radare2.py index d3ca1f0e6..0f4be0955 100644 --- a/pwndbg/radare2.py +++ b/pwndbg/radare2.py @@ -6,6 +6,7 @@ from __future__ import annotations import pwndbg.aglib.elf import pwndbg.lib.cache +from pwndbg.color import message @pwndbg.lib.cache.cache_until("start", "objfile") @@ -29,9 +30,34 @@ def r2pipe(): import r2pipe - flags = ["-e", "io.cache=true"] - if pwndbg.aglib.elf.get_elf_info(filename).is_pie and pwndbg.aglib.elf.exe(): - flags.extend(["-B", hex(pwndbg.aglib.elf.exe().address)]) - r2 = r2pipe.open(filename, flags=flags) - r2.cmd("aaaa") + if pwndbg.aglib.qemu.is_qemu_kernel(): + flags = ["-e", "bin.cache=true", "-e", "bin.relocs.apply=true"] + if (kbase := pwndbg.aglib.kernel.kbase()) and filename == pwndbg.aglib.proc.exe: + flags.extend( + [ + "-e", + "bin.baddr=" + hex(kbase - pwndbg.aglib.elf.get_vmlinux_unrand_base(filename)), + ] + ) + r2 = r2pipe.open(filename, flags) + else: + flags = ["-e", "io.cache=true"] + if pwndbg.aglib.elf.get_elf_info(filename).is_pie and pwndbg.aglib.elf.exe(): + flags.extend(["-B", hex(pwndbg.aglib.elf.exe().address)]) + r2 = r2pipe.open(filename, flags=flags) + # LD -> list supported decompilers (e cmd.pdc=?) + # Outputs for example: pdc\npdg + if "pdg" not in r2.cmd("LD").split("\n"): + raise Exception("radare2 plugin r2ghidra must be installed and available from r2") return r2 + + +def r2cmd(arguments) -> str: + try: + r2 = r2pipe() + return r2.cmd(" ".join(arguments)) + except ImportError: + return message.error("Could not import r2pipe python library. Is it installed?") + except Exception as e: + return message.error(e) + return "" diff --git a/pwndbg/rizin.py b/pwndbg/rizin.py index 6b24ba419..098a7f771 100644 --- a/pwndbg/rizin.py +++ b/pwndbg/rizin.py @@ -7,6 +7,7 @@ from __future__ import annotations import pwndbg.aglib.elf import pwndbg.dbg import pwndbg.lib.cache +from pwndbg.color import message @pwndbg.lib.cache.cache_until("start", "objfile") @@ -27,9 +28,33 @@ def rzpipe(): import rzpipe - flags = ["-e", "io.cache=true"] - if pwndbg.aglib.elf.get_elf_info(filename).is_pie and pwndbg.aglib.elf.exe(): - flags.extend(["-B", hex(pwndbg.aglib.elf.exe().address)]) - rz = rzpipe.open(filename, flags=flags) - rz.cmd("aaaa") + if pwndbg.aglib.qemu.is_qemu_kernel(): + flags = ["-e", "bin.cache=true", "-e", "bin.relocs.apply=true"] + if (kbase := pwndbg.aglib.kernel.kbase()) and filename == pwndbg.aglib.proc.exe: + flags.extend( + [ + "-e", + "bin.baddr=" + hex(kbase - pwndbg.aglib.elf.get_vmlinux_unrand_base(filename)), + ] + ) + rz = rzpipe.open(filename, flags) + else: + flags = ["-e", "io.cache=true"] + if pwndbg.aglib.elf.get_elf_info(filename).is_pie and pwndbg.aglib.elf.exe(): + flags.extend(["-B", hex(pwndbg.aglib.elf.exe().address)]) + rz = rzpipe.open(filename, flags=flags) + # Lc -> list core plugins + if "ghidra" not in rz.cmd("Lc"): + raise Exception("rizin plugin rzghidra must be installed and available from rz") return rz + + +def rzcmd(arguments) -> str: + try: + rz = rzpipe() + return rz.cmd(" ".join(arguments)) + except ImportError: + return message.error("Could not import rzpipe python library. Is it installed?") + except Exception as e: + return message.error(e) + return ""