├── decompilers ├── d2d_ghidra │ ├── data │ │ ├── .keep │ │ └── readme_0.png │ ├── Module.manifest │ ├── lib │ │ ├── commons-logging-1.1.jar │ │ ├── ws-commons-util-1.0.2.jar │ │ ├── xmlrpc-client-3.1.1.jar │ │ ├── xmlrpc-common-3.1.1.jar │ │ ├── xmlrpc-server-3.1.1.jar │ │ ├── xmlrpc-client-3.1.1-javadoc.jar │ │ ├── xmlrpc-client-3.1.1-sources.jar │ │ ├── xmlrpc-common-3.1.1-javadoc.jar │ │ ├── xmlrpc-common-3.1.1-sources.jar │ │ ├── xmlrpc-server-3.1.1-javadoc.jar │ │ ├── xmlrpc-server-3.1.1-sources.jar │ │ └── README.txt │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ └── images │ │ │ │ │ ├── README.txt │ │ │ │ │ └── binsync.png │ │ │ └── java │ │ │ │ └── decomp2dbg │ │ │ │ ├── D2DPluginPackage.java │ │ │ │ ├── D2DGhidraProcessorFactoryFactory.java │ │ │ │ ├── D2DGhidraServer.java │ │ │ │ ├── D2DGhidraServerAPI.java │ │ │ │ └── D2DPlugin.java │ │ └── test │ │ │ └── java │ │ │ └── README.test.txt │ ├── extension.properties │ ├── README.md │ └── build.gradle ├── d2d_ida │ ├── d2d_ida │ │ ├── __init__.py │ │ ├── plugin.py │ │ └── server.py │ └── d2d_ida.py ├── d2d_binja │ ├── __init__.py │ ├── plugin.json │ ├── d2d_binja.py │ └── server.py ├── d2d_angr │ ├── __init__.py │ ├── d2d_angr.py │ └── server.py └── server_template.py ├── decomp2dbg ├── clients │ ├── __init__.py │ ├── gdb │ │ ├── __init__.py │ │ ├── gef_client.py │ │ ├── pwndbg_client.py │ │ ├── decompiler_pane.py │ │ ├── utils.py │ │ ├── symbol_mapper.py │ │ └── gdb_client.py │ └── client.py ├── __init__.py ├── __main__.py ├── installer.py └── utils.py ├── assets ├── decomp2dbg.png └── decomp2dbg_old.png ├── testing └── binaries │ ├── beanstalk │ ├── fauxware │ ├── gopher_coin │ ├── objdump_ubuntu18 │ └── objdump_ubuntu20 ├── MANIFEST.in ├── .gitignore ├── .github └── workflows │ ├── release.yml │ └── ghidra-build.yml ├── setup.cfg ├── d2d.py ├── LICENSE ├── setup.py └── README.md /decompilers/d2d_ghidra/data/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/Module.manifest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /decompilers/d2d_ida/d2d_ida/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /decompilers/d2d_binja/__init__.py: -------------------------------------------------------------------------------- 1 | from .d2d_binja import * 2 | -------------------------------------------------------------------------------- /decompilers/d2d_angr/__init__.py: -------------------------------------------------------------------------------- 1 | from .d2d_angr import Decomp2DbgPlugin 2 | -------------------------------------------------------------------------------- /decomp2dbg/clients/__init__.py: -------------------------------------------------------------------------------- 1 | from .gdb import GDBClient, GDBDecompilerClient 2 | -------------------------------------------------------------------------------- /decomp2dbg/clients/gdb/__init__.py: -------------------------------------------------------------------------------- 1 | from .gdb_client import GDBClient, GDBDecompilerClient 2 | -------------------------------------------------------------------------------- /assets/decomp2dbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/assets/decomp2dbg.png -------------------------------------------------------------------------------- /assets/decomp2dbg_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/assets/decomp2dbg_old.png -------------------------------------------------------------------------------- /testing/binaries/beanstalk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/testing/binaries/beanstalk -------------------------------------------------------------------------------- /testing/binaries/fauxware: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/testing/binaries/fauxware -------------------------------------------------------------------------------- /testing/binaries/gopher_coin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/testing/binaries/gopher_coin -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include d2d.py 4 | recursive-include decompilers *.py *.json 5 | -------------------------------------------------------------------------------- /testing/binaries/objdump_ubuntu18: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/testing/binaries/objdump_ubuntu18 -------------------------------------------------------------------------------- /testing/binaries/objdump_ubuntu20: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/testing/binaries/objdump_ubuntu20 -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/data/readme_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/data/readme_0.png -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/commons-logging-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/commons-logging-1.1.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/ws-commons-util-1.0.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/ws-commons-util-1.0.2.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-client-3.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-client-3.1.1.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-common-3.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-common-3.1.1.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-server-3.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-server-3.1.1.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/main/resources/images/README.txt: -------------------------------------------------------------------------------- 1 | The "src/resources/images" directory is intended to hold all image/icon files used by 2 | this module. 3 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-client-3.1.1-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-client-3.1.1-javadoc.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-client-3.1.1-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-client-3.1.1-sources.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-common-3.1.1-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-common-3.1.1-javadoc.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-common-3.1.1-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-common-3.1.1-sources.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-server-3.1.1-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-server-3.1.1-javadoc.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/xmlrpc-server-3.1.1-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/lib/xmlrpc-server-3.1.1-sources.jar -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/main/resources/images/binsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahaloz/decomp2dbg/HEAD/decompilers/d2d_ghidra/src/main/resources/images/binsync.png -------------------------------------------------------------------------------- /decompilers/d2d_ida/d2d_ida.py: -------------------------------------------------------------------------------- 1 | def PLUGIN_ENTRY(*args, **kwargs): 2 | from d2d_ida.plugin import Decomp2DBGPlugin 3 | 4 | return Decomp2DBGPlugin(*args, **kwargs) 5 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/extension.properties: -------------------------------------------------------------------------------- 1 | name=@extname@ 2 | description=decomp2dbg Ghidra interface for syncing symbols to decompiler 3 | author=mahaloz 4 | createdOn= 5 | version=@extversion@ 6 | -------------------------------------------------------------------------------- /decomp2dbg/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.14.0" 2 | 3 | try: 4 | from .clients.client import DecompilerClient 5 | from .clients import GDBClient, GDBDecompilerClient 6 | except ImportError: 7 | pass 8 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/test/java/README.test.txt: -------------------------------------------------------------------------------- 1 | The "test" directory is intended to hold unit test cases. The package structure within 2 | this folder should correspond to that found in the "src" folder. 3 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/lib/README.txt: -------------------------------------------------------------------------------- 1 | The "lib" directory is intended to hold Jar files which this module is dependent upon. Jar files 2 | may be placed in this directory manually, or automatically by maven via the dependencies block 3 | of this module's build.gradle file. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | *.egg-info 3 | build/* 4 | *.pyc 5 | *.gdb_history 6 | pyrightconfig.json 7 | decomp2dbg/decompilers 8 | decomp2dbg/d2d.py 9 | decompilers/*/.gradle/ 10 | decompilers/*/build/ 11 | decompilers/*/dist/ 12 | decompilers/d2d_ghidra/.project 13 | decompilers/d2d_ghidra/.settings/ 14 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/main/java/decomp2dbg/D2DPluginPackage.java: -------------------------------------------------------------------------------- 1 | package decomp2dbg; 2 | 3 | import javax.swing.Icon; 4 | 5 | import ghidra.framework.plugintool.util.PluginPackage; 6 | import resources.ResourceManager; 7 | 8 | /** 9 | * The {@link PluginPackage} for the {@value #NAME} 10 | */ 11 | public class D2DPluginPackage extends PluginPackage { 12 | public static final String NAME = "decomp2dbg decompiler server"; 13 | public static final String DESCRIPTION = "These plugins are for connecting a debugger to the decompiler syms"; 14 | 15 | /* 16 | protected D2DPluginPackage(String name, Icon icon, String description, int priority) { 17 | super(NAME, icon, DESCRIPTION, priority); 18 | } 19 | */ 20 | 21 | public D2DPluginPackage() { 22 | super(NAME, ResourceManager.loadImage("data/d2d.png"), DESCRIPTION, DEVELOPER_PRIORITY); 23 | } 24 | } -------------------------------------------------------------------------------- /decomp2dbg/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from .installer import D2dInstaller 4 | 5 | 6 | def main(): 7 | parser = argparse.ArgumentParser( 8 | description=""" 9 | The decomp2dbg Command Line Util. 10 | """, 11 | epilog=""" 12 | Examples: 13 | decomp2dbg --install 14 | """ 15 | ) 16 | parser.add_argument( 17 | "--install", action="store_true", help=""" 18 | Install the decomp2dbg core to supported decompilers as plugins. This option will start an interactive 19 | prompt asking for install paths for all supported decompilers. Each install path is optional and 20 | will be skipped if not path is provided during install. 21 | """ 22 | ) 23 | args = parser.parse_args() 24 | 25 | if args.install: 26 | D2dInstaller().install() 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/main/java/decomp2dbg/D2DGhidraProcessorFactoryFactory.java: -------------------------------------------------------------------------------- 1 | package decomp2dbg; 2 | 3 | import org.apache.xmlrpc.XmlRpcException; 4 | import org.apache.xmlrpc.XmlRpcRequest; 5 | import org.apache.xmlrpc.server.RequestProcessorFactoryFactory; 6 | 7 | public class D2DGhidraProcessorFactoryFactory implements RequestProcessorFactoryFactory { 8 | private final RequestProcessorFactory factory = new D2DGhidraProcessorFactory(); 9 | private final D2DGhidraServerAPI api; 10 | 11 | public D2DGhidraProcessorFactoryFactory(D2DGhidraServerAPI api) { 12 | this.api = api; 13 | } 14 | 15 | @Override 16 | public RequestProcessorFactory getRequestProcessorFactory(Class aClass) 17 | throws XmlRpcException { 18 | return factory; 19 | } 20 | 21 | private class D2DGhidraProcessorFactory implements RequestProcessorFactory { 22 | @Override 23 | public Object getRequestProcessor(XmlRpcRequest xmlRpcRequest) 24 | throws XmlRpcException { 25 | return api; 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v**" 7 | 8 | jobs: 9 | 10 | release-github: 11 | name: Create Github Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Create Release 17 | uses: ncipollo/release-action@v1 18 | with: 19 | generateReleaseNotes: true 20 | 21 | release-pypi: 22 | name: Release pypi package 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout source 26 | uses: actions/checkout@v2 27 | - name: Setup Python 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: '3.8' 31 | - name: Install build 32 | run: pip install build 33 | - name: Build dists 34 | run: python -m build 35 | - name: Release to PyPI 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = decomp2dbg 3 | version = attr: decomp2dbg.__version__ 4 | url = https://github.com/mahaloz/decomp2dbg 5 | classifiers = 6 | License :: OSI Approved :: BSD License 7 | Programming Language :: Python :: 3 8 | Programming Language :: Python :: 3.6 9 | license = BSD 2 Clause 10 | license_files = LICENSE 11 | description = Symbol syncing framework for decompilers and debuggers 12 | long_description = file: README.md 13 | long_description_content_type = text/markdown 14 | 15 | [options] 16 | install_requires = 17 | sortedcontainers 18 | pyelftools 19 | libbs>=0.12.0 20 | 21 | python_requires = >= 3.5 22 | include_package_data = True 23 | packages = find: 24 | 25 | [options.package_data] 26 | decomp2dbg = 27 | d2d.py 28 | decompilers/*.py 29 | decompilers/d2d_angr/*.py 30 | decompilers/d2d_binja/*.py 31 | decompilers/d2d_binja/*.json 32 | decompilers/d2d_ida/*.py 33 | decompilers/d2d_ida/d2d_ida/*.py 34 | 35 | [options.entry_points] 36 | console_scripts = 37 | decomp2dbg = decomp2dbg.__main__:main 38 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/README.md: -------------------------------------------------------------------------------- 1 | # d2d-ghidra-plugin 2 | This is the plugin code for the Ghidra-side server of [decomp2dbg](https://github.com/mahaloz/decomp2dbg). 3 | Please look at the decomp2dbg project docs for more info. 4 | 5 | ## Installing 6 | 1. Follow the install steps at [decomp2dbg#install](https://github.com/mahaloz/decomp2dbg#install) using the built-in script 7 | 2. Start Ghidra 8 | 3. On the Project Window, click the tab `File->Install Extensions...` 9 | ![](./data/readme_0.png) 10 | 11 | 4. Search in the filter for "d2d", and enable it by clickng on the box 12 | - If you don't see the d2d plugin, download the latest release from [here](https://github.com/mahaloz/decomp2dbg/releases/latest/download/d2d-ghidra-plugin.zip) and add it manually to the Extensions/Ghidra folder in your install. 13 | 14 | 5. Restart Ghidra 15 | 6. Open a binary, now click `File->Configure...` tab and enable `decomp2dbg` plugin. 16 | 17 | You are done! Now you should be able to hit `Ctrl+Shift+D` to start the decomp2dbg server config. 18 | You can also find it in the `Tools` tab when you have a binary open. 19 | -------------------------------------------------------------------------------- /decompilers/d2d_binja/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginmetadataversion" : 2, 3 | "name": "decomp2dbg", 4 | "type": ["core"], 5 | "api": ["python3"], 6 | "description": "Adds support for Binary Ninja decompilation in your debugger, curently only supports GDB", 7 | "longdescription": "decomp2dbg will host the decompilation and symbols present in your Binary Ninja View over an XMLRPC server which can bee hooked from within your debugger. As you change you decompilation, so will the debuggers view.", 8 | "license": { 9 | "name": "BSD 2-clause", 10 | "text": "" 11 | }, 12 | "platforms" : ["Darwin", "Linux", "Windows"], 13 | "installinstructions" : { 14 | "Darwin" : "Follow the README to install the debugger side of the plugin.", 15 | "Linux" : "Follow the README to install the debugger side of the plugin.", 16 | "Windows" : "Follow the README to install the debugger side of the plugin." 17 | }, 18 | "dependencies": { 19 | "pip": [], 20 | "apt": [], 21 | "installers": [], 22 | "other": [] 23 | }, 24 | "version": "3.11.0", 25 | "author": "Zion Basque (@mahaloz)", 26 | "minimumbinaryninjaversion": 1200 27 | } 28 | -------------------------------------------------------------------------------- /d2d.py: -------------------------------------------------------------------------------- 1 | # 2 | # ██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ 3 | # ██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚════██╗██╔══██╗██╔══██╗██╔════╝ 4 | # ██║ ██║█████╗ ██║ ██║ ██║██╔████╔██║██████╔╝ █████╔╝██║ ██║██████╔╝██║ ███╗ 5 | # ██║ ██║██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██╔═══╝ ██║ ██║██╔══██╗██║ ██║ 6 | # ██████╔╝███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ███████╗██████╔╝██████╔╝╚██████╔╝ 7 | # ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ 8 | # by mahaloz 9 | # 10 | 11 | # discover interface 12 | is_gdb = True 13 | try: 14 | import gdb 15 | except ImportError: 16 | is_gdb = False 17 | 18 | if is_gdb: 19 | _globals = globals() 20 | if "gef" in _globals: 21 | from decomp2dbg.clients.gdb.gef_client import GEFClient 22 | GEFClient(register_external_context_pane, gef_print, gef) 23 | elif "pwndbg" in _globals: 24 | from decomp2dbg.clients.gdb.pwndbg_client import PwndbgClient 25 | PwndbgClient() 26 | else: 27 | from decomp2dbg.clients.gdb.gdb_client import GDBClient 28 | GDBClient() 29 | else: 30 | raise Exception("Unsupported debugger type detected, decomp2dbg will not run!") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Zion Leonahenahe Basque 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /decomp2dbg/clients/gdb/gef_client.py: -------------------------------------------------------------------------------- 1 | from .gdb_client import GDBClient 2 | from .utils import is_remote_debug, checksec 3 | 4 | 5 | class GEFClient(GDBClient): 6 | def __init__(self, ctx_pane_registrar, gef_print, gef_ref): #gef_config, gef_memory): 7 | super(GEFClient, self).__init__() 8 | self.ctx_pane_registrar = ctx_pane_registrar 9 | self.dec_pane.print = gef_print 10 | self.gef_config = gef_ref.config 11 | self.gef_ref = gef_ref 12 | 13 | def register_decompiler_context_pane(self, decompiler_name): 14 | self.ctx_pane_registrar("decompilation", self.dec_pane.display_pane, self.dec_pane.title) 15 | 16 | def deregister_decompiler_context_pane(self, decompiler_name): 17 | self.gef_config["context.layout"] = self.gef_config["context.layout"].replace(" decompilation", "") 18 | 19 | def find_text_segment_base_addr(self, is_remote=False): 20 | if is_remote: 21 | elf_file = str(self.gef_ref.session.remote.lfile) 22 | elf_virtual_path = str(self.gef_ref.session.remote.file) 23 | else: 24 | elf_file = str(self.gef_ref.session.file) 25 | elf_virtual_path = str(self.gef_ref.session.file) 26 | 27 | vmmap = self.gef_ref.memory.maps 28 | base_address = min(x.page_start for x in vmmap if x.path == elf_virtual_path) 29 | 30 | return base_address 31 | 32 | @property 33 | def is_pie(self): 34 | elf_file = self.gef_ref.session.remote.lfile if is_remote_debug() else self.gef_ref.session.file 35 | checksec_status = checksec(str(elf_file)) 36 | return checksec_status["PIE"] # if pie we will have offset instead of abs address. 37 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/main/java/decomp2dbg/D2DGhidraServer.java: -------------------------------------------------------------------------------- 1 | package decomp2dbg; 2 | 3 | import org.apache.xmlrpc.XmlRpcException; 4 | import org.apache.xmlrpc.server.PropertyHandlerMapping; 5 | import org.apache.xmlrpc.webserver.WebServer; 6 | 7 | 8 | public class D2DGhidraServer { 9 | public D2DPlugin plugin; 10 | public D2DGhidraServerAPI api; 11 | private WebServer server; 12 | public Boolean uiConfiguredCorrectly; 13 | 14 | public int port; 15 | public String host; 16 | 17 | public D2DGhidraServer(String host, int port, D2DPlugin plugin) 18 | { 19 | this.server = new WebServer(port); 20 | this.plugin = plugin; 21 | this.uiConfiguredCorrectly = null; 22 | this.port = port; 23 | this.host = host; 24 | 25 | PropertyHandlerMapping phm = new PropertyHandlerMapping(); 26 | api = new D2DGhidraServerAPI(this); 27 | phm.setRequestProcessorFactoryFactory(new D2DGhidraProcessorFactoryFactory(api)); 28 | phm.setVoidMethodEnabled(true); 29 | 30 | try { 31 | phm.addHandler("d2d", D2DGhidraServerAPI.class); 32 | this.server.getXmlRpcServer().setHandlerMapping(phm); 33 | } catch (XmlRpcException e) { 34 | System.out.println("Error in phm config: " + e); 35 | this.server = null; 36 | } 37 | } 38 | 39 | public Boolean start_server() { 40 | if(this.server == null) { 41 | return false; 42 | } 43 | 44 | try { 45 | this.server.start(); 46 | return true; 47 | } catch (Exception exception){ 48 | System.out.println("Error starting Server: " + exception); 49 | return false; 50 | } 51 | } 52 | 53 | public Boolean stop_server() { 54 | if(this.server == null) 55 | return false; 56 | 57 | this.server.shutdown(); 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/ghidra-build.yml: -------------------------------------------------------------------------------- 1 | name: Ghidra Extension Build 2 | 3 | env: 4 | ghidra-url: https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.3.1_build/ghidra_11.3.1_PUBLIC_20250219.zip 5 | ghidra-zip-filename: ghidra_11.3.1_PUBLIC.zip 6 | ghidra-directory: ghidra_11.3.1_PUBLIC 7 | 8 | on: 9 | workflow_run: 10 | workflows: [ "Release" ] 11 | types: 12 | - completed 13 | pull_request: 14 | branches: [ main ] 15 | paths: ['decompilers/d2d_ghidra/**'] 16 | push: 17 | branches: [ main ] 18 | paths: ['decompilers/d2d_ghidra/**'] 19 | workflow_dispatch: 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: Download Ghidra release 29 | uses: carlosperate/download-file-action@v1.0.3 30 | id: download-ghidra 31 | with: 32 | file-url: ${{ env.ghidra-url }} 33 | file-name: ${{ env.ghidra-zip-filename }} 34 | 35 | - name: Unzip Ghidra 36 | uses: TonyBogdanov/zip@1.0 37 | with: 38 | args: unzip -qq ${{ steps.download-ghidra.outputs.file-path }} -d . 39 | - uses: actions/setup-java@v1 40 | with: 41 | java-version: '21' 42 | 43 | - name: Build extension 44 | uses: eskatos/gradle-command-action@v1 45 | with: 46 | gradle-version: '8.5' 47 | build-root-directory: ${{ github.workspace }}/decompilers/d2d_ghidra/ 48 | arguments: '-PGHIDRA_INSTALL_DIR=${{ github.workspace }}/${{ env.ghidra-directory }}' 49 | distributions-cache-enabled: false 50 | 51 | - name: Rename extension 52 | run: cd decompilers/d2d_ghidra/dist/ && mv *.zip d2d-ghidra-plugin.zip 53 | - name: Upload built extension 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: extension 57 | path: decompilers/d2d_ghidra/dist/d2d-ghidra-plugin.zip 58 | 59 | release: 60 | runs-on: ubuntu-latest 61 | if: ${{ github.event_name == 'workflow_run' }} 62 | needs: build 63 | 64 | steps: 65 | - name: Download built extension 66 | uses: actions/download-artifact@v4 67 | with: 68 | name: extension 69 | - name: Upload To Github Release 70 | uses: xresloader/upload-to-github-release@v1.3.12 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | file: "*.zip" 75 | update_latest_release: true 76 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/build.gradle: -------------------------------------------------------------------------------- 1 | /* ### 2 | * IP: GHIDRA 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // Builds a Ghidra Extension for a given Ghidra installation. 17 | // 18 | // An absolute path to the Ghidra installation directory must be supplied either by setting the 19 | // GHIDRA_INSTALL_DIR environment variable or Gradle project property: 20 | // 21 | // > export GHIDRA_INSTALL_DIR= 22 | // > gradle 23 | // 24 | // or 25 | // 26 | // > gradle -PGHIDRA_INSTALL_DIR= 27 | // 28 | // Gradle should be invoked from the directory of the project to build. Please see the 29 | // application.gradle.version property in /Ghidra/application.properties 30 | // for the correction version of Gradle to use for the Ghidra installation you specify. 31 | 32 | //----------------------START "DO NOT MODIFY" SECTION------------------------------ 33 | def ghidraInstallDir 34 | 35 | if (System.env.GHIDRA_INSTALL_DIR) { 36 | ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR 37 | } 38 | else if (project.hasProperty("GHIDRA_INSTALL_DIR")) { 39 | ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR") 40 | } 41 | 42 | if (ghidraInstallDir) { 43 | apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle" 44 | } 45 | else { 46 | throw new GradleException("GHIDRA_INSTALL_DIR is not defined!") 47 | } 48 | //----------------------END "DO NOT MODIFY" SECTION------------------------------- 49 | 50 | repositories { 51 | // Declare dependency repositories here. This is not needed if dependencies are manually 52 | // dropped into the lib/ directory. 53 | // See https://docs.gradle.org/current/userguide/declaring_repositories.html for more info. 54 | // Ex: mavenCentral() 55 | } 56 | 57 | dependencies { 58 | //implementation 'org.apache.xmlrpc:xmlrpc:3.1.3' 59 | // Any external dependencies added here will automatically be copied to the lib/ directory when 60 | // this extension is built. 61 | } 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-class-docstring 2 | import os 3 | import platform 4 | import shutil 5 | from pathlib import Path 6 | import sys 7 | from distutils.util import get_platform 8 | from distutils.command.build import build as st_build 9 | 10 | from setuptools import setup 11 | from setuptools.command.develop import develop as st_develop 12 | 13 | 14 | def _copy_decomp_plugins(): 15 | local_plugins = Path("decompilers").absolute() 16 | decomp2dbg_loc = Path("decomp2dbg").absolute() 17 | pip_e_plugins = decomp2dbg_loc.joinpath("decompilers").absolute() 18 | 19 | local_d2d = Path("d2d.py").absolute() 20 | 21 | # clean the install location of symlink or folder 22 | try: 23 | shutil.rmtree(pip_e_plugins, ignore_errors=True) 24 | except Exception as e: 25 | print(f"Exception occurred while removing {pip_e_plugins}: {e}") 26 | 27 | try: 28 | os.unlink(pip_e_plugins) 29 | os.unlink(decomp2dbg_loc.joinpath(local_d2d.name)) 30 | except Exception as e: 31 | print(f"Exception occurred during cleaning of install location: {e}") 32 | 33 | # first attempt a symlink, if it works, exit early 34 | try: 35 | os.symlink(local_plugins, pip_e_plugins, target_is_directory=True) 36 | os.symlink(local_d2d, decomp2dbg_loc.joinpath(local_d2d.name)) 37 | return 38 | except Exception as e: 39 | print(f"Exception occured during symlinking process: {e}") 40 | 41 | # copy if symlinking is not available on target system 42 | try: 43 | shutil.copytree("decompilers", "decomp2dbg/decompilers") 44 | shutil.copy("d2d.py", "decomp2dbg/d2d.py") 45 | except Exception as e: 46 | print(f"Exception occurred during copying process: {e}") 47 | 48 | class build(st_build): 49 | def run(self, *args): 50 | self.execute(_copy_decomp_plugins, (), msg="Copying plugins") 51 | super().run(*args) 52 | 53 | class develop(st_develop): 54 | def run(self, *args): 55 | self.execute(_copy_decomp_plugins, (), msg="Linking or copying local plugins folder") 56 | super().run(*args) 57 | 58 | 59 | cmdclass = { 60 | "build": build, 61 | "develop": develop, 62 | } 63 | 64 | if 'bdist_wheel' in sys.argv and '--plat-name' not in sys.argv: 65 | sys.argv.append('--plat-name') 66 | name = get_platform() 67 | if 'linux' in name: 68 | sys.argv.append('manylinux2014_' + platform.machine()) 69 | else: 70 | # https://www.python.org/dev/peps/pep-0425/ 71 | sys.argv.append(name.replace('.', '_').replace('-', '_')) 72 | 73 | setup(cmdclass=cmdclass) 74 | -------------------------------------------------------------------------------- /decomp2dbg/clients/client.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import xmlrpc.client 3 | 4 | 5 | def only_if_connected(f): 6 | @functools.wraps(f) 7 | def _only_if_connected(self, *args, **kwargs): 8 | if self.connected: 9 | return f(self, *args, **kwargs) 10 | 11 | return _only_if_connected 12 | 13 | 14 | class DecompilerClient: 15 | def __init__(self, name="decompiler", host="localhost", port=3662, native_sym_support=True): 16 | self.name = name 17 | self.host = host 18 | self.port = port 19 | self.native_sym_support = native_sym_support 20 | self.server = None 21 | 22 | # 23 | # Server Ops 24 | # 25 | 26 | @property 27 | def connected(self): 28 | return True if self.server else False 29 | 30 | def connect(self, name=None, host=None, port=None) -> bool: 31 | """ 32 | Connects to the remote decompiler. 33 | """ 34 | self.name = name or self.name 35 | host = host or self.host 36 | port = port or self.port 37 | 38 | # create a decompiler server connection and test it 39 | retry = True 40 | try: 41 | self.server = xmlrpc.client.ServerProxy("http://{:s}:{:d}".format(host, port)) 42 | self.server.ping() 43 | retry = False 44 | except: 45 | pass 46 | 47 | # the connection could fail because its a Ghidra connection on endpoint d2d 48 | if retry: 49 | try: 50 | self.server = xmlrpc.client.ServerProxy("http://{:s}:{:d}".format(host, port)).d2d 51 | self.server.ping() 52 | except (ConnectionRefusedError, AttributeError) as e: 53 | self.server = None 54 | # if we fail here, we fail overall 55 | return False 56 | 57 | self.decompiler_connected() 58 | return True 59 | 60 | def decompiler_connected(self): 61 | pass 62 | 63 | def decompiler_disconnected(self): 64 | pass 65 | 66 | # 67 | # Decompiler Interface 68 | # 69 | 70 | @only_if_connected 71 | def disconnect(self): 72 | try: 73 | self.server.disconnect() 74 | except Exception: 75 | pass 76 | 77 | self.server = None 78 | self.decompiler_disconnected() 79 | 80 | @only_if_connected 81 | def decompile(self, addr): 82 | return self.server.decompile(addr) 83 | 84 | @only_if_connected 85 | def function_data(self, addr): 86 | return self.server.function_data(addr) 87 | 88 | @property 89 | @only_if_connected 90 | def function_headers(self): 91 | return self.server.function_headers() 92 | 93 | @property 94 | @only_if_connected 95 | def global_vars(self): 96 | return self.server.global_vars() 97 | 98 | @property 99 | @only_if_connected 100 | def structs(self): 101 | return self.server.structs() 102 | 103 | @property 104 | @only_if_connected 105 | def breakpoints(self): 106 | return self.server.breakpoints() 107 | 108 | # 109 | # Client Setters 110 | # 111 | 112 | def update_global_vars(self): 113 | raise NotImplementedError 114 | 115 | def update_function_headers(self): 116 | raise NotImplementedError 117 | 118 | def update_function_data(self, addr): 119 | raise NotImplementedError 120 | -------------------------------------------------------------------------------- /decomp2dbg/clients/gdb/pwndbg_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pwndbg 4 | 5 | from .gdb_client import GDBClient 6 | from .decompiler_pane import DecompilerPane 7 | from ...utils import * 8 | import xmlrpc.client 9 | 10 | 11 | class PwndbgDecompilerPane(DecompilerPane): 12 | def __init__(self, decompiler): 13 | super(PwndbgDecompilerPane, self).__init__(decompiler) 14 | 15 | def decompilation_text(self): 16 | """ 17 | Display the current decompilation, with an arrow next to the current line. 18 | """ 19 | output = [] 20 | 21 | if not self.decompiler.connected: 22 | return output 23 | 24 | if not self.ready_to_display: 25 | err("Unable to decompile function") 26 | return output 27 | 28 | # configure based on source config 29 | past_lines_color = "gray" 30 | nb_lines = 6 31 | cur_line_color = "green" 32 | 33 | if len(self.decomp_lines) < nb_lines: 34 | nb_lines = len(self.decomp_lines) 35 | 36 | # use GEF source printing method 37 | for i in range(self.curr_line - nb_lines + 1, self.curr_line + nb_lines): 38 | if i < 0: 39 | continue 40 | 41 | if i < self.curr_line: 42 | output += ["{}".format(Color.colorify(" {:4d}\t {:s}".format(i + 1, self.decomp_lines[i], ), past_lines_color))] 43 | 44 | if i == self.curr_line: 45 | prefix = "{}{:4d}\t ".format(RIGHT_ARROW[1:], i + 1) 46 | output += [Color.colorify("{}{:s}".format(prefix, self.decomp_lines[i]), cur_line_color)] 47 | 48 | if i > self.curr_line: 49 | try: 50 | output += [" {:4d}\t {:s}".format(i + 1, self.decomp_lines[i], )] 51 | except IndexError: 52 | break 53 | return output 54 | 55 | def context_gdecompiler(self, target=sys.stdout, with_banner=True, width=None): 56 | failed = True 57 | try: 58 | # triggers an update as well 59 | pane_title = self.title() 60 | failed = False 61 | except Exception as e: 62 | pane_title = f"decompiler: {e}" 63 | 64 | if pane_title is None: 65 | return [] 66 | 67 | banner = [pwndbg.ui.banner(pane_title, target=target, width=width)] if with_banner else [] 68 | if failed: 69 | return banner + ["decompilation error"] 70 | 71 | return banner + self.decompilation_text() 72 | 73 | 74 | class PwndbgClient(GDBClient): 75 | def __init__(self): 76 | super(PwndbgClient, self).__init__() 77 | self.dec_pane = PwndbgDecompilerPane(self.dec_client) 78 | 79 | # if we are connected to ghidra 80 | if not isinstance(self.dec_client.server, xmlrpc.client.ServerProxy): 81 | # reset the type handlers pwndbg adds 82 | xmlrpc.client.Marshaller.dispatch[type(0)] = xmlrpc.client.Marshaller.dump_long 83 | 84 | def register_decompiler_context_pane(self, decompiler_name): 85 | pwndbg.commands.context.context_sections["g"] = self.dec_pane.context_gdecompiler 86 | pwndbg.commands.config_context_sections = pwndbg.lib.config.Parameter( 87 | f'context-sections', 88 | f'regs disasm code gdecompiler stack backtrace expressions', 89 | f'which context sections are displayed (controls order)' 90 | ) 91 | 92 | def deregister_decompiler_context_pane(self, decompiler_name): 93 | del pwndbg.commands.context.context_sections["g"] 94 | pwndbg.commands.config_context_sections = pwndbg.lib.config.Parameter( 95 | f'context-sections', 96 | f'regs disasm code ghidra stack backtrace expressions', 97 | f'which context sections are displayed (controls order)' 98 | ) 99 | -------------------------------------------------------------------------------- /decomp2dbg/installer.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from urllib.request import urlretrieve 3 | 4 | from libbs.plugin_installer import LibBSPluginInstaller, PluginInstaller 5 | 6 | 7 | class D2dInstaller(LibBSPluginInstaller): 8 | def __init__(self): 9 | super().__init__(targets=PluginInstaller.DECOMPILERS + PluginInstaller.DEBUGGERS) 10 | pkg_files = self.find_pkg_files("decomp2dbg") 11 | if pkg_files is None: 12 | raise RuntimeError("Failed to find decomp2dbg package files! Please reinstall or file an issue.") 13 | 14 | self.plugins_path = pkg_files / "decompilers" 15 | 16 | def display_prologue(self): 17 | print(textwrap.dedent(""" 18 | Now installing... 19 | __ ___ ____ 20 | ____/ /__ _________ ____ ___ ____ |__ \ ____/ / /_ ____ _ 21 | / __ / _ \/ ___/ __ \/ __ `__ \/ __ \__/ // __ / __ \/ __ `/ 22 | / /_/ / __/ /__/ /_/ / / / / / / /_/ / __// /_/ / /_/ / /_/ / 23 | \__,_/\___/\___/\____/_/ /_/ /_/ .___/____/\__,_/_.___/\__, / 24 | /_/ /____/ 25 | The Decompiler to Debugger Bridge 26 | """)) 27 | 28 | def install_gdb(self, path=None, interactive=True): 29 | path = super().install_gdb(path=None) 30 | if path is None: 31 | return None 32 | 33 | d2d_script_path_pkg = self.plugins_path.parent.joinpath("d2d.py") 34 | with open(path, "r") as fp: 35 | init_contents = fp.read() 36 | 37 | write_str = f"source {str(d2d_script_path_pkg.absolute())}" 38 | if write_str in init_contents: 39 | self.warn("gdbinit already contains d2d source...") 40 | return None 41 | 42 | with open(path, "a") as fp: 43 | fp.write(f"\n{write_str}\n") 44 | 45 | return path 46 | 47 | def install_ida(self, path=None, interactive=True): 48 | ida_plugin_path = super().install_ida(path=path) 49 | if ida_plugin_path is None: 50 | return 51 | 52 | src_d2d_ida_pkg = self.plugins_path.joinpath("d2d_ida").joinpath("d2d_ida") 53 | src_d2d_ida_py = self.plugins_path.joinpath("d2d_ida").joinpath("d2d_ida.py") 54 | dst_d2d_ida_pkg = ida_plugin_path.joinpath("d2d_ida") 55 | dst_d2d_ida_py = ida_plugin_path.joinpath("d2d_ida.py") 56 | self.link_or_copy(src_d2d_ida_pkg, dst_d2d_ida_pkg, is_dir=True) 57 | self.link_or_copy(src_d2d_ida_py, dst_d2d_ida_py) 58 | return dst_d2d_ida_pkg 59 | 60 | def install_angr(self, path=None, interactive=True): 61 | angr_plugin_path = super().install_angr(path=path) 62 | if angr_plugin_path is None: 63 | return None 64 | 65 | src_d2d_angr_pkg = self.plugins_path.joinpath("d2d_angr") 66 | dst_d2d_angr_pkg = angr_plugin_path.joinpath("d2d_angr") 67 | self.link_or_copy(src_d2d_angr_pkg, dst_d2d_angr_pkg, is_dir=True) 68 | return dst_d2d_angr_pkg 69 | 70 | def install_ghidra(self, path=None, interactive=True): 71 | ghidra_path = super().install_ghidra(path=path) 72 | if ghidra_path is None: 73 | return None 74 | 75 | download_url = "https://github.com/mahaloz/decomp2dbg/releases/latest/download/d2d-ghidra-plugin.zip" 76 | dst_path = ghidra_path.joinpath("d2d-ghidra-plugin.zip") 77 | urlretrieve(download_url, dst_path) 78 | return dst_path 79 | 80 | def install_binja(self, path=None, interactive=True): 81 | binja_plugin_path = super().install_binja(path=path) 82 | if binja_plugin_path is None: 83 | return None 84 | 85 | src_path = self.plugins_path.joinpath("d2d_binja") 86 | dst_path = binja_plugin_path.joinpath("d2d_binja") 87 | self.link_or_copy(src_path, dst_path, is_dir=True) 88 | return dst_path 89 | -------------------------------------------------------------------------------- /decompilers/d2d_binja/d2d_binja.py: -------------------------------------------------------------------------------- 1 | # 2 | # ██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ 3 | # ██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚════██╗██╔══██╗██╔══██╗██╔════╝ 4 | # ██║ ██║█████╗ ██║ ██║ ██║██╔████╔██║██████╔╝ █████╔╝██║ ██║██████╔╝██║ ███╗ 5 | # ██║ ██║██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██╔═══╝ ██║ ██║██╔══██╗██║ ██║ 6 | # ██████╔╝███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ███████╗██████╔╝██████╔╝╚██████╔╝ 7 | # ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ 8 | # 9 | 10 | import threading 11 | import traceback 12 | 13 | try: 14 | from PySide2.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QMessageBox, \ 15 | QGridLayout 16 | from PySide2.QtGui import Qt, QKeySequence 17 | 18 | except ImportError: 19 | from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QMessageBox, \ 20 | QGridLayout 21 | from PySide6.QtGui import Qt, QKeySequence 22 | 23 | 24 | from binaryninjaui import ( 25 | UIContext, 26 | UIAction, 27 | UIActionHandler, 28 | Menu, 29 | ) 30 | from binaryninja import core_version 31 | 32 | from .server import BinjaDecompilerServer 33 | 34 | # 35 | # UI 36 | # 37 | 38 | class ConfigDialog(QDialog): 39 | def __init__(self, bv, parent=None): 40 | super().__init__(parent) 41 | self.bv = bv 42 | self.setWindowTitle("Configure decomp2dbg") 43 | self._main_layout = QVBoxLayout() 44 | self._host_edit = None # type:QLineEdit 45 | self._port_edit = None # type:QLineEdit 46 | 47 | self._init_widgets() 48 | self.setLayout(self._main_layout) 49 | self.show() 50 | 51 | def _init_widgets(self): 52 | upper_layout = QGridLayout() 53 | 54 | host_label = QLabel(self) 55 | host_label.setText("Host") 56 | self._host_edit = QLineEdit(self) 57 | self._host_edit.setText("localhost") 58 | row = 0 59 | upper_layout.addWidget(host_label, row, 0) 60 | upper_layout.addWidget(self._host_edit, row, 1) 61 | row += 1 62 | 63 | port_label = QLabel(self) 64 | port_label.setText("Port") 65 | self._port_edit = QLineEdit(self) 66 | self._port_edit.setText("3662") 67 | upper_layout.addWidget(port_label, row, 0) 68 | upper_layout.addWidget(self._port_edit, row, 1) 69 | row += 1 70 | 71 | # buttons 72 | self._ok_button = QPushButton(self) 73 | self._ok_button.setText("OK") 74 | self._ok_button.setDefault(True) 75 | self._ok_button.clicked.connect(self._on_ok_clicked) 76 | cancel_button = QPushButton(self) 77 | cancel_button.setText("Cancel") 78 | cancel_button.clicked.connect(self._on_cancel_clicked) 79 | 80 | buttons_layout = QHBoxLayout() 81 | buttons_layout.addWidget(self._ok_button) 82 | buttons_layout.addWidget(cancel_button) 83 | 84 | # main layout 85 | self._main_layout.addLayout(upper_layout) 86 | self._main_layout.addLayout(buttons_layout) 87 | 88 | # 89 | # Event handlers 90 | # 91 | 92 | def _on_ok_clicked(self): 93 | host = self._host_edit.text() 94 | port = self._port_edit.text() 95 | 96 | if not host: 97 | QMessageBox(self).critical(None, "Invalid host", 98 | "Host cannot be empty." 99 | ) 100 | return 101 | 102 | if not port: 103 | QMessageBox(self).critical(None, "Invalid port", 104 | "Port cannot be empty" 105 | ) 106 | return 107 | 108 | decomp_server = BinjaDecompilerServer(self.bv) 109 | t = threading.Thread(target=decomp_server.start_xmlrpc_server, kwargs={'host': host, 'port': int(port)}) 110 | t.daemon = True 111 | try: 112 | t.start() 113 | except Exception as e: 114 | QMessageBox(self).critical(None, "Error starting decomp2dbg Server", str(e)) 115 | traceback.print_exc() 116 | return 117 | 118 | self.close() 119 | 120 | def _on_cancel_clicked(self): 121 | self.close() 122 | 123 | # 124 | # Plugin 125 | # 126 | 127 | class BinjaPlugin: 128 | def __init__(self): 129 | # controller stored by a binary view 130 | self._init_ui() 131 | 132 | def _init_ui(self): 133 | # config dialog 134 | configure_d2d_id = "decomp2dbg: Configure..." 135 | UIAction.registerAction(configure_d2d_id, QKeySequence(Qt.CTRL | Qt.SHIFT | Qt.Key_D)) 136 | UIActionHandler.globalActions().bindAction( 137 | configure_d2d_id, UIAction(self._launch_config) 138 | ) 139 | 140 | target_menu = "Tools" if int(core_version()[4:][:4]) < 3505 else "Plugins" 141 | Menu.mainMenu(target_menu).addAction(configure_d2d_id, "decomp2dbg") 142 | 143 | def _launch_config(self, bn_context): 144 | bv = bn_context.binaryView 145 | 146 | # configure 147 | dialog = ConfigDialog(bv) 148 | dialog.exec_() 149 | 150 | 151 | BinjaPlugin() 152 | -------------------------------------------------------------------------------- /decompilers/d2d_angr/d2d_angr.py: -------------------------------------------------------------------------------- 1 | # 2 | # ██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ 3 | # ██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚════██╗██╔══██╗██╔══██╗██╔════╝ 4 | # ██║ ██║█████╗ ██║ ██║ ██║██╔████╔██║██████╔╝ █████╔╝██║ ██║██████╔╝██║ ███╗ 5 | # ██║ ██║██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██╔═══╝ ██║ ██║██╔══██╗██║ ██║ 6 | # ██████╔╝███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ███████╗██████╔╝██████╔╝╚██████╔╝ 7 | # ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ 8 | # angr-management server 9 | # 10 | # by clasm, 2021. 11 | # 12 | 13 | import threading 14 | import traceback 15 | 16 | from PySide2.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QMessageBox, QGridLayout 17 | from angrmanagement.plugins import BasePlugin 18 | from angrmanagement.ui.workspace import Workspace 19 | 20 | from .server import AngrDecompilerServer 21 | 22 | # 23 | # UI 24 | # 25 | 26 | 27 | class ConfigDialog(QDialog): 28 | def __init__(self, instance, parent=None): 29 | super().__init__(parent) 30 | self.setWindowTitle("Configure Decomp2GEF") 31 | self._main_layout = QVBoxLayout() 32 | self._instance = instance 33 | self._host_edit = None # type:QLineEdit 34 | self._port_edit = None # type:QLineEdit 35 | 36 | self._init_widgets() 37 | self.setLayout(self._main_layout) 38 | self.show() 39 | 40 | def _init_widgets(self): 41 | upper_layout = QGridLayout() 42 | 43 | host_label = QLabel(self) 44 | host_label.setText("Host") 45 | self._host_edit = QLineEdit(self) 46 | self._host_edit.setText("localhost") 47 | row = 0 48 | upper_layout.addWidget(host_label, row, 0) 49 | upper_layout.addWidget(self._host_edit, row, 1) 50 | row += 1 51 | 52 | port_label = QLabel(self) 53 | port_label.setText("Port") 54 | self._port_edit = QLineEdit(self) 55 | self._port_edit.setText("3662") 56 | upper_layout.addWidget(port_label, row, 0) 57 | upper_layout.addWidget(self._port_edit, row, 1) 58 | row += 1 59 | 60 | # buttons 61 | self._ok_button = QPushButton(self) 62 | self._ok_button.setText("OK") 63 | self._ok_button.setDefault(True) 64 | self._ok_button.clicked.connect(self._on_ok_clicked) 65 | cancel_button = QPushButton(self) 66 | cancel_button.setText("Cancel") 67 | cancel_button.clicked.connect(self._on_cancel_clicked) 68 | 69 | buttons_layout = QHBoxLayout() 70 | buttons_layout.addWidget(self._ok_button) 71 | buttons_layout.addWidget(cancel_button) 72 | 73 | # main layout 74 | self._main_layout.addLayout(upper_layout) 75 | self._main_layout.addLayout(buttons_layout) 76 | 77 | # 78 | # Event handlers 79 | # 80 | 81 | def _on_ok_clicked(self): 82 | host = self._host_edit.text() 83 | port = self._port_edit.text() 84 | 85 | if not host: 86 | QMessageBox(self).critical(None, "Invalid host", 87 | "Host cannot be empty." 88 | ) 89 | return 90 | 91 | if not port: 92 | QMessageBox(self).critical(None, "Invalid port", 93 | "Port cannot be empty" 94 | ) 95 | return 96 | 97 | decomp_server = AngrDecompilerServer(self._instance) 98 | t = threading.Thread(target=decomp_server.start_xmlrpc_server, kwargs={"host": host, "port": int(port, 0)}) 99 | t.daemon = True 100 | try: 101 | t.start() 102 | except Exception as e: 103 | QMessageBox(self).critical(None, "Error starting Decomp2GEF Server", str(e)) 104 | traceback.print_exc() 105 | return 106 | 107 | self.close() 108 | 109 | def _on_cancel_clicked(self): 110 | self.close() 111 | 112 | 113 | class Decomp2DbgPlugin(BasePlugin): 114 | def __init__(self, workspace: Workspace): 115 | """ 116 | The entry point for the Decomp2Gef plugin. 117 | @param workspace: an AM _workspace (usually found in _instance) 118 | """ 119 | super().__init__(workspace) 120 | self._workspace = workspace 121 | self._instance = workspace.instance 122 | 123 | MENU_BUTTONS = ('Configure Decomp2GEF...', ) 124 | MENU_CONFIG_ID = 0 125 | 126 | def handle_click_menu(self, idx): 127 | # sanity check on menu selection 128 | if idx < 0 or idx >= len(self.MENU_BUTTONS): 129 | return 130 | 131 | if self.workspace.instance.project.am_none: 132 | return 133 | 134 | mapping = { 135 | self.MENU_CONFIG_ID: self.open_config_dialog, 136 | } 137 | 138 | # call option mapped to each menu pos 139 | mapping.get(idx)() 140 | 141 | def open_config_dialog(self): 142 | if self.workspace.instance.project.am_none: 143 | # project does not exist yet 144 | return 145 | 146 | config = ConfigDialog(self._instance) 147 | config.exec_() 148 | -------------------------------------------------------------------------------- /decompilers/server_template.py: -------------------------------------------------------------------------------- 1 | from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler 2 | 3 | 4 | class RequestHandler(SimpleXMLRPCRequestHandler): 5 | rpc_paths = ("/RPC2",) 6 | 7 | 8 | class DecompilerServer: 9 | def __init__(self, host=None, port=None): 10 | self.host = host 11 | self.port = port 12 | 13 | # 14 | # Public API 15 | # 16 | 17 | def decompile(self, addr: int): 18 | """ 19 | Takes an addr which may be in a function. If addr is not in a function, a dict with the defined 20 | parameters below should be returned with None for each value. Decompilation should be the decompilation 21 | string of the function. curr_line should be the line number of that decompilation, starting at 0. 22 | 23 | Always returns a dict with the defined keys below, which may have None as their values. 24 | """ 25 | resp = { 26 | "decompilation": str, 27 | "curr_line": int, 28 | "func_name": str 29 | } 30 | 31 | return resp 32 | 33 | def function_data(self, addr: int): 34 | """ 35 | Returns stack vars and func args 36 | 37 | """ 38 | # For maximum cooperation, you may use int(x, 0) in the client 39 | # to convert the stringified number to an int, whatever 40 | # base it is in. 41 | resp = { 42 | "reg_vars": [ 43 | { 44 | "name": "some_var_name", 45 | "type": "some_type", 46 | "reg_name": "something_like_r12", 47 | }, # ... 48 | ], 49 | "stack_vars": [ 50 | { 51 | "name": "example_name", 52 | "type": "some_type", 53 | # Either from_sp or from_frame will be non-None (a stringified number). 54 | # Both are positive numbers. 55 | # Generally, from_frame will be 0 for the return address. 56 | "from_sp": "16", # None | str 57 | "from_frame": None # None | str 58 | }, # ... 59 | ], 60 | } 61 | 62 | return resp 63 | 64 | def function_headers(self): 65 | resp = { 66 | # 0xdeadbeef 67 | "3735928559": { 68 | "name": str, 69 | "size": int 70 | }, 71 | } 72 | 73 | return resp 74 | 75 | def global_vars(self): 76 | resp = { 77 | "3735928559": { 78 | "name": str 79 | }, 80 | } 81 | 82 | return resp 83 | 84 | def structs(self): 85 | resp = { 86 | "example_struct_name": { 87 | "size": int, 88 | "example_member_name": { 89 | "offset": int, 90 | "size": int 91 | }, 92 | }, 93 | } 94 | 95 | return resp 96 | 97 | def breakpoints(self): 98 | resp = { 99 | "3735928559": bool, 100 | "3735928560": bool, 101 | } 102 | 103 | return resp 104 | 105 | def binary_path(self) -> str: 106 | """ 107 | Get the filesystem path of the binary being decompiled. 108 | """ 109 | return "" 110 | 111 | def versions(self) -> dict[str, str]: 112 | """ 113 | Get version information about the decompiler environment. 114 | """ 115 | resp = { 116 | # the name of the decompiler 117 | "name": "ida", 118 | # the version of the decompiler 119 | "version": "9.2", 120 | # the version of the runtime it uses 121 | # (ghidra should set "java" instead of "python") 122 | "python": "3.13.7", 123 | # any decompiler-specific auxiliary stuff 124 | "hexrays": "1337.42", 125 | } 126 | return resp 127 | 128 | def focus_address(self, addr: int) -> bool: 129 | """ 130 | Focus the given address in the GUI of the decompiler. If possible, 131 | don't switch the window focus. 132 | 133 | Returns: 134 | True if successful, otherwise False 135 | """ 136 | return False 137 | 138 | # 139 | # XMLRPC Server 140 | # 141 | 142 | def ping(self): 143 | return True 144 | 145 | def start_xmlrpc_server(self, host="localhost", port=3662): 146 | """ 147 | Initialize the XMLRPC thread. 148 | """ 149 | host = host or self.host 150 | port = port or self.port 151 | 152 | print("[+] Starting XMLRPC server: {}:{}".format(host, port)) 153 | server = SimpleXMLRPCServer( 154 | (host, port), 155 | requestHandler=RequestHandler, 156 | logRequests=False, 157 | allow_none=True 158 | ) 159 | server.register_introspection_functions() 160 | server.register_function(self.decompile) 161 | server.register_function(self.function_headers) 162 | server.register_function(self.function_data) 163 | server.register_function(self.global_vars) 164 | server.register_function(self.structs) 165 | server.register_function(self.breakpoints) 166 | server.register_function(self.binary_path) 167 | server.register_function(self.versions) 168 | server.register_function(self.focus_address) 169 | server.register_function(self.ping) 170 | print("[+] Registered decompilation server!") 171 | while True: 172 | server.handle_request() 173 | -------------------------------------------------------------------------------- /decomp2dbg/clients/gdb/decompiler_pane.py: -------------------------------------------------------------------------------- 1 | from ...utils import * 2 | from .utils import pc 3 | 4 | 5 | class DecompilerPane: 6 | def __init__(self, decompiler, printer=pprint): 7 | self.decompiler: "GDBDecompilerClient" = decompiler 8 | 9 | self.ready_to_display = False 10 | self.decomp_lines = [] 11 | self.curr_line = -1 12 | self.curr_func = "" 13 | self.print = printer 14 | 15 | # XXX: this needs to be removed in the future 16 | self.stop_global_import = False 17 | 18 | def update_event(self, pc_): 19 | if (self.decompiler.gdb_client.base_addr_end is not None) and \ 20 | (self.decompiler.gdb_client.base_addr_start is not None): 21 | if pc_ > self.decompiler.gdb_client.base_addr_end or pc_ < self.decompiler.gdb_client.base_addr_start: 22 | return False 23 | 24 | 25 | rebased_pc = self.decompiler.rebase_addr(pc_) 26 | 27 | # update all known function names 28 | self.decompiler.update_symbols() 29 | 30 | # decompile the current pc location 31 | try: 32 | resp = self.decompiler.decompile(rebased_pc) 33 | except OverflowError as e: 34 | warn( 35 | f"Decompiler failed to get a response from decompiler on " 36 | f"{hex(rebased_pc) if isinstance(rebased_pc, int) else rebased_pc} with: {e}" 37 | f", are you in a library function?" 38 | ) 39 | return False 40 | except Exception as e: 41 | warn(f"Decompiler failed to get a response from decompiler on " 42 | f"{hex(rebased_pc) if isinstance(rebased_pc,int) else rebased_pc} with: {e}") 43 | return False 44 | 45 | # set the decompilation for next use in display_pane 46 | decompilation = resp['decompilation'] 47 | if not decompilation: 48 | warn("Decompiler server sent back a response without decompilation lines for " 49 | f"{hex(rebased_pc) if isinstance(rebased_pc,int) else rebased_pc}") 50 | return False 51 | self.decomp_lines = decompilation 52 | last_line = self.curr_line 53 | self.curr_line = resp["curr_line"] 54 | if self.curr_line == -1: 55 | self.curr_line = last_line if last_line is not None else 0 56 | 57 | self.curr_func = resp["func_name"] 58 | 59 | # update the data known in the function (stack variables) 60 | self.decompiler.update_function_data(rebased_pc) 61 | return True 62 | 63 | def display_pane(self): 64 | """ 65 | Display the current decompilation, with an arrow next to the current line. 66 | """ 67 | if not self.decompiler.connected: 68 | return 69 | 70 | if not self.ready_to_display: 71 | err("Unable to decompile function") 72 | return 73 | 74 | # configure based on source config 75 | past_lines_color = "gray" 76 | nb_lines = 6 77 | cur_line_color = "green" 78 | 79 | if len(self.decomp_lines) < nb_lines: 80 | nb_lines = len(self.decomp_lines) 81 | 82 | # use GEF source printing method 83 | for i in range(self.curr_line - nb_lines + 1, self.curr_line + nb_lines): 84 | if i < 0: 85 | continue 86 | 87 | if i < self.curr_line: 88 | self.print( 89 | "{}".format(Color.colorify(" {:4d}\t {:s}".format(i + 1, self.decomp_lines[i], ), past_lines_color)) 90 | ) 91 | 92 | if i == self.curr_line: 93 | prefix = "{}{:4d}\t ".format(RIGHT_ARROW[1:], i + 1) 94 | self.print(Color.colorify("{}{:s}".format(prefix, self.decomp_lines[i]), cur_line_color)) 95 | 96 | if i > self.curr_line: 97 | try: 98 | self.print(" {:4d}\t {:s}".format(i + 1, self.decomp_lines[i], )) 99 | except IndexError: 100 | break 101 | return 102 | 103 | def title(self): 104 | """ 105 | Special note: this function is always called before display_pane 106 | """ 107 | if not self.decompiler.connected: 108 | return None 109 | 110 | self.ready_to_display = self.update_event(pc()) 111 | 112 | if self.ready_to_display: 113 | title = "decompiler:{:s}:{:s}:{:d}".format(self.decompiler.name, self.curr_func, self.curr_line+1) 114 | else: 115 | title = None 116 | 117 | return title 118 | 119 | def display_pane_and_title(self, *args, **kwargs): 120 | 121 | # 122 | # title 123 | # 124 | 125 | title_ = self.title() 126 | 127 | line_color = "gray" 128 | msg_color = "cyan" 129 | tty_rows, tty_columns = get_terminal_size() 130 | 131 | if title_ is None: 132 | self.print(Color.colorify(HORIZONTAL_LINE * tty_columns, line_color)) 133 | else: 134 | trail_len = len(title_) + 6 135 | title = "" 136 | title += Color.colorify("{:{padd}<{width}} ".format("", 137 | width=max(tty_columns - trail_len, 0), 138 | padd=HORIZONTAL_LINE), 139 | line_color) 140 | title += Color.colorify(title_, msg_color) 141 | title += Color.colorify(" {:{padd}<4}".format("", padd=HORIZONTAL_LINE), 142 | line_color) 143 | self.print(title) 144 | 145 | # 146 | # decompilation 147 | # 148 | 149 | self.display_pane() 150 | self.print(Color.colorify(HORIZONTAL_LINE * tty_columns, line_color)) 151 | -------------------------------------------------------------------------------- /decomp2dbg/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple, Any 2 | import platform 3 | import struct 4 | from functools import lru_cache 5 | import pathlib 6 | import os 7 | 8 | LEFT_ARROW = " \u2190 " 9 | RIGHT_ARROW = " \u2192 " 10 | DOWN_ARROW = "\u21b3" 11 | HORIZONTAL_LINE = "\u2500" 12 | VERTICAL_LINE = "\u2502" 13 | CROSS = "\u2718 " 14 | TICK = "\u2713 " 15 | ANSI_SPLIT_RE = r"(\033\[[\d;]*m)" 16 | 17 | 18 | class Color: 19 | """Used to colorify terminal output.""" 20 | colors = { 21 | "normal": "\033[0m", 22 | "gray": "\033[1;38;5;240m", 23 | "light_gray": "\033[0;37m", 24 | "red": "\033[31m", 25 | "green": "\033[32m", 26 | "yellow": "\033[33m", 27 | "blue": "\033[34m", 28 | "pink": "\033[35m", 29 | "cyan": "\033[36m", 30 | "bold": "\033[1m", 31 | "underline": "\033[4m", 32 | "underline_off": "\033[24m", 33 | "highlight": "\033[3m", 34 | "highlight_off": "\033[23m", 35 | "blink": "\033[5m", 36 | "blink_off": "\033[25m", 37 | } 38 | 39 | # Support NO_COLOR environment variable (https://no-color.org/) 40 | _no_color = bool(os.environ.get("NO_COLOR")) 41 | 42 | @staticmethod 43 | def redify(msg: str) -> str: return Color.colorify(msg, "red") 44 | @staticmethod 45 | def greenify(msg: str) -> str: return Color.colorify(msg, "green") 46 | @staticmethod 47 | def blueify(msg: str) -> str: return Color.colorify(msg, "blue") 48 | @staticmethod 49 | def yellowify(msg: str) -> str: return Color.colorify(msg, "yellow") 50 | @staticmethod 51 | def grayify(msg: str) -> str: return Color.colorify(msg, "gray") 52 | @staticmethod 53 | def light_grayify(msg: str) -> str: return Color.colorify(msg, "light_gray") 54 | @staticmethod 55 | def pinkify(msg: str) -> str: return Color.colorify(msg, "pink") 56 | @staticmethod 57 | def cyanify(msg: str) -> str: return Color.colorify(msg, "cyan") 58 | @staticmethod 59 | def boldify(msg: str) -> str: return Color.colorify(msg, "bold") 60 | @staticmethod 61 | def underlinify(msg: str) -> str: return Color.colorify(msg, "underline") 62 | @staticmethod 63 | def highlightify(msg: str) -> str: return Color.colorify(msg, "highlight") 64 | @staticmethod 65 | def blinkify(msg: str) -> str: return Color.colorify(msg, "blink") 66 | 67 | @staticmethod 68 | def colorify(text: str, attrs: str) -> str: 69 | """Color text according to the given attributes.""" 70 | 71 | if Color._no_color: 72 | return str(text) 73 | 74 | colors = Color.colors 75 | msg = [colors[attr] for attr in attrs.split() if attr in colors] 76 | msg.append(str(text)) 77 | if colors["highlight"] in msg: msg.append(colors["highlight_off"]) 78 | if colors["underline"] in msg: msg.append(colors["underline_off"]) 79 | if colors["blink"] in msg: msg.append(colors["blink_off"]) 80 | msg.append(colors["normal"]) 81 | return "".join(msg) 82 | 83 | 84 | def get_terminal_size() -> Tuple[int, int]: 85 | """Return the current terminal size.""" 86 | if platform.system() == "Windows": 87 | from ctypes import windll, create_string_buffer 88 | hStdErr = -12 89 | herr = windll.kernel32.GetStdHandle(hStdErr) 90 | csbi = create_string_buffer(22) 91 | res = windll.kernel32.GetConsoleScreenBufferInfo(herr, csbi) 92 | if res: 93 | _, _, _, _, _, left, top, right, bottom, _, _ = struct.unpack("hhhhHhhhhhh", csbi.raw) 94 | tty_columns = right - left + 1 95 | tty_rows = bottom - top + 1 96 | return tty_rows, tty_columns 97 | else: 98 | return 600, 100 99 | else: 100 | import fcntl 101 | import termios 102 | try: 103 | tty_rows, tty_columns = struct.unpack("hh", fcntl.ioctl(1, termios.TIOCGWINSZ, "1234")) 104 | return tty_rows, tty_columns 105 | except OSError: 106 | return 600, 100 107 | 108 | 109 | def pprint(*args: str, end="\n", sep=" ", **kwargs: Any) -> None: 110 | """Wrapper around print(), using string buffering feature.""" 111 | parts = args 112 | print(*parts, sep=sep, end=end, **kwargs) 113 | return 114 | 115 | 116 | def titlify(text: str, color: Optional[str] = None, msg_color: Optional[str] = None) -> str: 117 | """Print a centered title.""" 118 | cols = get_terminal_size()[1] 119 | nb = (cols - len(text) - 2) // 2 120 | if color is None: 121 | color = "gray" 122 | if msg_color is None: 123 | msg_color = "cyan" 124 | 125 | msg = [Color.colorify(f"{HORIZONTAL_LINE * nb} ", color), 126 | Color.colorify(text, msg_color), 127 | Color.colorify(f" {HORIZONTAL_LINE * nb}", color)] 128 | return "".join(msg) 129 | 130 | 131 | def err(msg: str) -> None: 132 | pprint(f"{Color.colorify('[!]', 'bold red')} {msg}") 133 | 134 | 135 | def warn(msg: str) -> None: 136 | pprint(f"{Color.colorify('[*]', 'bold yellow')} {msg}") 137 | 138 | 139 | def ok(msg: str) -> None: 140 | pprint(f"{Color.colorify('[+]', 'bold green')} {msg}") 141 | 142 | 143 | def info(msg: str) -> None: 144 | pprint(f"{Color.colorify('[+]', 'bold blue')} {msg}") 145 | 146 | 147 | def gef_pystring(x: bytes) -> str: 148 | """Returns a sanitized version as string of the bytes list given in input.""" 149 | res = str(x, encoding="utf-8") 150 | substs = [("\n", "\\n"), ("\r", "\\r"), ("\t", "\\t"), ("\v", "\\v"), ("\b", "\\b"), ] 151 | for x, y in substs: res = res.replace(x, y) 152 | return res 153 | -------------------------------------------------------------------------------- /decompilers/d2d_ida/d2d_ida/plugin.py: -------------------------------------------------------------------------------- 1 | # 2 | # ██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ 3 | # ██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚════██╗██╔══██╗██╔══██╗██╔════╝ 4 | # ██║ ██║█████╗ ██║ ██║ ██║██╔████╔██║██████╔╝ █████╔╝██║ ██║██████╔╝██║ ███╗ 5 | # ██║ ██║██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██╔═══╝ ██║ ██║██╔══██╗██║ ██║ 6 | # ██████╔╝███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ███████╗██████╔╝██████╔╝╚██████╔╝ 7 | # ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ 8 | # 9 | 10 | import traceback 11 | import threading 12 | import re 13 | 14 | from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QMessageBox, QGridLayout 15 | 16 | import idaapi, ida_idp, idc 17 | 18 | IDA_VERSION = idaapi.get_kernel_version() 19 | 20 | from .server import IDADecompilerServer 21 | 22 | decomp_server: IDADecompilerServer = None 23 | 24 | 25 | # 26 | # Update Hooks 27 | # 28 | 29 | class IDBHooks(ida_idp.IDB_Hooks): 30 | def __init__(self): 31 | ida_idp.IDB_Hooks.__init__(self) 32 | 33 | @staticmethod 34 | def is_new_type_system(): 35 | major, minor = re.match(r"(\d+)\.(\d+)", IDA_VERSION).groups() 36 | major, minor = int(major), int(minor) 37 | return (major, minor) >= (8, 4) 38 | 39 | def is_type_change_on_ea(self, ea): 40 | if not self.is_new_type_system(): 41 | import ida_struct, ida_enum 42 | return bool(ida_struct.is_member_id(ea) or ida_struct.get_struc(ea) or ida_enum.get_enum_name(ea)) 43 | else: 44 | # In the new type system, type changes do not map directly to an ea (triggering renamed hook) 45 | return False 46 | 47 | def renamed(self, ea, new_name, local_name): 48 | if self.is_type_change_on_ea(ea): 49 | return 0 50 | 51 | if decomp_server is None: 52 | return 0 53 | 54 | relative_ea: str = str(decomp_server.rebase_addr(ea, down=True)) 55 | 56 | # renaming a function header 57 | ida_func = idaapi.get_func(ea) 58 | if ida_func and ida_func.start_ea == ea: 59 | decomp_server.cache["function_headers"][relative_ea]["name"] = new_name 60 | return 0 61 | 62 | # assume we are renaming a global var of some sort 63 | try: 64 | decomp_server.cache["global_vars"][relative_ea]["name"] = new_name 65 | except KeyError: 66 | # okay its not a global 67 | pass 68 | 69 | return 0 70 | 71 | 72 | # 73 | # UI 74 | # 75 | 76 | class ConfigDialog(QDialog): 77 | def __init__(self, change_hook, parent=None): 78 | super().__init__(parent) 79 | self.change_hook = change_hook 80 | self.setWindowTitle("Configure Decomp2DBG") 81 | self._main_layout = QVBoxLayout() 82 | self._host_edit = None # type:QLineEdit 83 | self._port_edit = None # type:QLineEdit 84 | 85 | self._init_widgets() 86 | self.setLayout(self._main_layout) 87 | self.show() 88 | 89 | def _init_widgets(self): 90 | upper_layout = QGridLayout() 91 | 92 | host_label = QLabel(self) 93 | host_label.setText("Host") 94 | self._host_edit = QLineEdit(self) 95 | self._host_edit.setText("localhost") 96 | row = 0 97 | upper_layout.addWidget(host_label, row, 0) 98 | upper_layout.addWidget(self._host_edit, row, 1) 99 | row += 1 100 | 101 | port_label = QLabel(self) 102 | port_label.setText("Port") 103 | self._port_edit = QLineEdit(self) 104 | self._port_edit.setText("3662") 105 | upper_layout.addWidget(port_label, row, 0) 106 | upper_layout.addWidget(self._port_edit, row, 1) 107 | row += 1 108 | 109 | # buttons 110 | self._ok_button = QPushButton(self) 111 | self._ok_button.setText("OK") 112 | self._ok_button.setDefault(True) 113 | self._ok_button.clicked.connect(self._on_ok_clicked) 114 | cancel_button = QPushButton(self) 115 | cancel_button.setText("Cancel") 116 | cancel_button.clicked.connect(self._on_cancel_clicked) 117 | 118 | buttons_layout = QHBoxLayout() 119 | buttons_layout.addWidget(self._ok_button) 120 | buttons_layout.addWidget(cancel_button) 121 | 122 | # main layout 123 | self._main_layout.addLayout(upper_layout) 124 | self._main_layout.addLayout(buttons_layout) 125 | 126 | # 127 | # Event handlers 128 | # 129 | 130 | def _on_ok_clicked(self): 131 | global decomp_server 132 | 133 | host = self._host_edit.text() 134 | port = self._port_edit.text() 135 | 136 | if not host: 137 | QMessageBox(self).critical(None, "Invalid host", 138 | "Host cannot be empty." 139 | ) 140 | return 141 | 142 | if not port: 143 | QMessageBox(self).critical(None, "Invalid port", 144 | "Port cannot be empty" 145 | ) 146 | return 147 | 148 | decomp_server = IDADecompilerServer() 149 | t = threading.Thread(target=decomp_server.start_xmlrpc_server, kwargs={'host': host, 'port': int(port)}) 150 | t.daemon = True 151 | try: 152 | t.start() 153 | # start hooks on good connection 154 | self.change_hook.hook() 155 | except Exception as e: 156 | QMessageBox(self).critical(None, "Error starting Decomp2DBG Server", str(e)) 157 | traceback.print_exc() 158 | return 159 | 160 | self.close() 161 | 162 | def _on_cancel_clicked(self): 163 | self.close() 164 | 165 | 166 | # 167 | # Action Handlers 168 | # 169 | 170 | class IDAActionHandler(idaapi.action_handler_t): 171 | def __init__(self, action, plugin, typ): 172 | super(IDAActionHandler, self).__init__() 173 | self.action = action 174 | self.plugin = plugin 175 | self.typ = typ 176 | 177 | def update(self, ctx): 178 | return idaapi.AST_ENABLE_ALWAYS 179 | 180 | 181 | class Decomp2DBGPlugin(idaapi.plugin_t): 182 | """Plugin entry point. Does most of the skinning magic.""" 183 | 184 | flags = idaapi.PLUGIN_FIX 185 | comment = "Syncing decompiler info to GDB" 186 | help = "Decomp2DBG Help" 187 | wanted_name = "Decomp2DBG: configure" 188 | wanted_hotkey = "Ctrl-Shift-D" 189 | 190 | def __init__(self, *args, **kwargs): 191 | idaapi.plugin_t.__init__(self) 192 | self.change_hook = IDBHooks() 193 | 194 | def init(self): 195 | return idaapi.PLUGIN_KEEP 196 | 197 | def run(self, arg): 198 | self.open_config_dialog() 199 | 200 | def open_config_dialog(self): 201 | dialog = ConfigDialog(self.change_hook) 202 | dialog.exec_() 203 | 204 | def term(self): 205 | pass 206 | 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # decomp2dbg 2 | 3 | Reverse engineering involves both static (decompiler) and dynamic (debugger) analysis, yet we often 4 | use these analyses without sharing knowledge between the two. In the case of reversing static binaries, 5 | context switching between debugger assembly and the symbols you have reversed in decompilation can be inefficient. 6 | 7 | decomp2dbg aims to shorten the gap of context switching between decompiler and debugger by introducing a generic 8 | API for decompiler-to-debugger symbol syncing. In effect, giving the reverser the power of their debugger with 9 | the symbols and decompilation lines they recover in their decompiler. 10 | 11 | ![decomp2dbg](./assets/decomp2dbg.png) 12 | 13 | Interested in seeing what decomp2dbg looks like in practice? Checkout the recorded [talk at CactusCon 2023](https://youtu.be/-J8fGMt6UmE?t=22442), 14 | featuring debugging a remote arm32 binary from a x64 machine with Ghidra symbols. 15 | 16 | For active help, join the BinSync Discord below, where we answer decomp2dbg questions: 17 | 18 | [![Discord](https://img.shields.io/discord/900841083532087347?label=Discord&style=plastic)](https://discord.gg/wZSCeXnEvR) 19 | 20 | ## Supported Platforms 21 | ### Decompilers 22 | - IDA Pro (>= 7.0): [Demo w/ GEF](https://asciinema.org/a/442740) 23 | - Binary Ninja (>= 2.4): [Demo w/ GEF](https://t.co/M2IZd0fmi3) 24 | - Ghidra (>= 11.3.1): [Demo w/ GEF](https://youtu.be/MK7N7uQTUNY) 25 | - [angr-management](https://github.com/angr/angr-management) (>= 9.0) 26 | 27 | ### Debuggers 28 | - gdb (works best with [GEF](https://github.com/hugsy/gef)) 29 | - GEF 30 | - pwndbg 31 | - vanilla 32 | 33 | ## Install 34 | Install through pip, then use the built-in installer for decompilers: 35 | ```bash 36 | pip3 install decomp2dbg && decomp2dbg --install 37 | ``` 38 | 39 | This will open a prompt where you be asked to input the path to your decompiler and debugger of choice. For Ghidra installs, 40 | you must follow the extra steps to enable extensions [here](https://github.com/mahaloz/decomp2dbg/tree/main/decompilers/d2d_ghidra/README.md). 41 | If you installed the decompiler-side in the Binja Plugin Manager, you still need to install the debugger side with the above. 42 | 43 | **Note**: You may need to allow inbound connections on port 3662, or the port you use, for decomp2dbg to connect 44 | to the decompiler. If you are installing decomp2dbg with GEF or pwndbg it's important that in your `~/.gdbinit` the 45 | `d2d.py` file is sourced after GEF or pwndbg. 46 | 47 | ## Manual Install 48 | 49 | Skip this if you were able to use the above install with no errors. 50 | If you can't use the above built-in script (non-WSL Windows install for the decompiler), follow the steps below: 51 | 52 | If you only need the decompiler side of things, copy the associated decompiler plugin to the 53 | decompiler's plugin folder. Here is how you do it in IDA: 54 | 55 | First, clone the repo: 56 | ``` 57 | git clone https://github.com/mahaloz/decomp2dbg.git 58 | ``` 59 | 60 | Copy all the files in `./decompilers/d2d_ida/` into your ida `plugins` folder: 61 | ```bash 62 | cp -r ./decompilers/d2d_ida/* /path/to/ida/plugins/ 63 | ``` 64 | 65 | If you also need to install the gdb side of things, use the line below: 66 | ```bash 67 | pip3 install . && \ 68 | cp d2d.py ~/.d2d.py && echo "source ~/.d2d.py" >> ~/.gdbinit 69 | ``` 70 | 71 | ## Usage 72 | First, start the decompilation server on your decompiler. You may want to wait 73 | until your decompiler finishes its normal analysis before starting it. After normal analysis, this can be done by using the hotkey `Ctrl-Shift-D`, 74 | or selecting the `decomp2dbg: configure` tab in your associated plugins tab. After starting the server, you should 75 | see a message in your decompiler 76 | ``` 77 | [+] Starting XMLRPC server: localhost:3662 78 | [+] Registered decompilation server! 79 | ``` 80 | 81 | Next, in your debugger, run: 82 | ```bash 83 | decompiler connect 84 | ``` 85 | 86 | If you are running the decompiler on a VM or different machine, you can optionally provide the host and 87 | port to connect to. Here is an example: 88 | ```bash 89 | decompiler connect ida --host 10.211.55.2 --port 3662 90 | ``` 91 | 92 | You can find out how to use all the commands by running the decompiler command with the `--help` flag. 93 | 94 | The first connection can take up to 30 seconds to register depending on the amount of globals in the binary. 95 | If all is well, you should see: 96 | ```bash 97 | [+] Connected to decompiler! 98 | ``` 99 | 100 | If you are using decomp2dbg for a library, i.e. the main binary your debugger attached to is not the binary 101 | you want source for, then you should take a look at the [Advanced Usage - Shared Libs](#shared-libraries) section 102 | of the readme. 103 | 104 | ### Decompilation View 105 | On each breakpoint event, you will now see decompilation printed, and the line you are on associated with 106 | the break address. 107 | 108 | ### Functions and Global Vars 109 | Functions and Global Vars from your decompilation are now mapped into your GDB like normal Source-level 110 | symbols. This means normal GDB commands like printing and examination are native: 111 | ```bash 112 | b sub_46340 113 | x/10i sub_46340 114 | ``` 115 | ```bash 116 | p dword_267A2C 117 | x dword_267A2C 118 | ``` 119 | 120 | ### Stack Vars, Register Vars, Func Args 121 | Some variables that are stored locally in a function are stack variables. For the vars that can be mapped 122 | to the stack or registers, we import them as convenience variables. You can see their contents like a normal GDB convenience 123 | variable: 124 | ```bash 125 | p $v4 126 | ``` 127 | 128 | Stack variables will always store their address on the stack. To see what value is actually in that stack variable, 129 | simply dereference the variable: 130 | ```bash 131 | x $v4 132 | ``` 133 | 134 | This also works with function arguments if applicable (mileage may vary): 135 | ```bash 136 | p $a1 137 | ``` 138 | 139 | Note: `$v4` in this case will only be mapped for as long as you are in the same function. Once you leave the function 140 | it may be unmapped or remapped to another value. 141 | 142 | ## Advanced Usage 143 | ### Shared Libraries 144 | When you want the decompilation (and symbols) displayed for a section of memory which is not the main binary, like when debugging a shared library, you need to do some extra steps. Currently, d2d only supports 1 decompiler connected at a time, which means if you currently have any decompilers connected that is not the library, you need to disconnect it. 145 | 146 | After following the normal setup to have your decompiler running the d2d server for your shared library, you need to manually set the base address for this library and its end address: 147 | 148 | ``` 149 | decompiler connect ida --base-addr-start 0x00007ffff7452000 --base-addr-end 0x00007ffff766d000 150 | ``` 151 | 152 | To find the base address that your library is loaded at in memory, its recommend you use something like the `vmmap` command from GEF to look for the libraries name in the memory space. After connecting with this manually set address, symbols show work like normal d2d. Decompilation will only be printed on the screen when you are in the range of this address space. 153 | 154 | ## Features 155 | - [X] Auto-updating decompilation context view 156 | - [X] Auto-syncing function names 157 | - [X] Breakable/Inspectable symbols 158 | - [X] Auto-syncing stack variable names 159 | - [ ] Auto-syncing structs 160 | - [ ] Online DWARF Creation 161 | - [ ] Function Type Syncing 162 | - [ ] lldb support 163 | - [ ] windbg support 164 | -------------------------------------------------------------------------------- /decompilers/d2d_angr/server.py: -------------------------------------------------------------------------------- 1 | from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler 2 | import os 3 | import sys 4 | 5 | import angr 6 | from angr.analyses.decompiler.structured_codegen import DummyStructuredCodeGenerator 7 | 8 | 9 | 10 | class RequestHandler(SimpleXMLRPCRequestHandler): 11 | rpc_paths = ("/RPC2",) 12 | 13 | 14 | class AngrDecompilerServer: 15 | def __init__(self, instance, host="localhost", port=3662): 16 | self._instance = instance 17 | self._workspace = self._instance.workspace 18 | self.host = host 19 | self.port = port 20 | 21 | # 22 | # Private 23 | # 24 | 25 | def rebase_addr(self, addr, down=False): 26 | rebased = addr 27 | base_addr = self._instance.project.loader.min_addr 28 | 29 | if down: 30 | rebased -= base_addr 31 | elif addr < base_addr: 32 | rebased += base_addr 33 | 34 | return rebased 35 | 36 | def _decompile_function(self, func): 37 | """ 38 | Taken directly from BinSync 39 | """ 40 | # check for known decompilation 41 | available = self._instance.kb.structured_code.available_flavors(func.addr) 42 | should_decompile = False 43 | if 'pseudocode' not in available: 44 | should_decompile = True 45 | else: 46 | cached = self._instance.kb.structured_code[(func.addr, 'pseudocode')] 47 | if isinstance(cached, DummyStructuredCodeGenerator): 48 | should_decompile = True 49 | 50 | if should_decompile: 51 | # recover direct pseudocode 52 | self._instance.project.analyses.Decompiler(func, flavor='pseudocode') 53 | 54 | # attempt to get source code if its available 55 | source_root = None 56 | if self._instance.original_binary_path: 57 | source_root = os.path.dirname(self._instance.original_binary_path) 58 | self._instance.project.analyses.ImportSourceCode(func, flavor='source', source_root=source_root) 59 | 60 | # grab newly cached pseudocode 61 | decomp = self._instance.kb.structured_code[(func.addr, 'pseudocode')].codegen 62 | return decomp 63 | 64 | # 65 | # Public API 66 | # 67 | 68 | def decompile(self, addr: int): 69 | """ 70 | Takes an addr which may be in a function. If addr is not in a function, a dict with the defined 71 | parameters below should be returned with None for each value. Decompilation should be the decompilation 72 | string of the function. curr_line should be the line number of that decompilation, starting at 0. 73 | 74 | Always returns a dict with the defined keys below, which may have None as their values. 75 | """ 76 | resp = { 77 | "decompilation": None 78 | } 79 | 80 | addr = self.rebase_addr(addr) 81 | func_addr = self._instance.cfg.get_any_node(addr, anyaddr=True).function_address 82 | func = self._instance.kb.functions[func_addr] 83 | decomp = self._decompile_function(func) 84 | try: 85 | pos = decomp.map_addr_to_pos.get_nearest_pos(addr) 86 | except Exception as e: 87 | print(f"Failed to get nearest pos: {e} for {hex(addr)}, skipping decomp") 88 | return resp 89 | 90 | size = len(decomp.text) 91 | line_end = decomp.text.find("\n", pos) 92 | line_start = size - decomp.text[::-1].find("\n", size - line_end) 93 | decomp_lines = decomp.text.split('\n') 94 | for idx, line in enumerate(decomp_lines): 95 | if decomp.text[line_start:line_end] in line: 96 | break 97 | else: 98 | return resp 99 | 100 | resp["decompilation"] = decomp_lines 101 | resp["func_name"] = func.name 102 | resp["curr_line"] = idx 103 | return resp 104 | 105 | def function_data(self, addr: int): 106 | """ 107 | Returns stack vars and func args 108 | 109 | """ 110 | resp = { 111 | "reg_vars": [], 112 | "stack_vars": [] 113 | } 114 | 115 | addr = self.rebase_addr(addr) 116 | func_addr = self._instance.cfg.get_any_node(addr, anyaddr=True).function_address 117 | func = self._instance.kb.functions[func_addr] 118 | decomp = self._decompile_function(func) 119 | manager = decomp.cfunc.variable_manager 120 | for var in manager._unified_variables: 121 | if isinstance(var, angr.sim_variable.SimStackVariable): 122 | resp["stack_vars"].append({ 123 | "name": var.name, 124 | "type": manager.get_variable_type(var).c_repr(), 125 | # offset from frame base 126 | "from_frame": str(var.offset), 127 | "from_sp": None, 128 | }) 129 | 130 | return resp 131 | 132 | def function_headers(self): 133 | resp = {} 134 | for addr, func in self._instance.kb.functions.items(): 135 | resp[str(self.rebase_addr(addr, down=True))] = { 136 | "name": func.name, 137 | "size": func.size 138 | } 139 | 140 | return resp 141 | 142 | def global_vars(self): 143 | resp = {} 144 | 145 | return resp 146 | 147 | def structs(self): 148 | resp = {} 149 | 150 | return resp 151 | 152 | def breakpoints(self): 153 | resp = {} 154 | 155 | return resp 156 | 157 | def binary_path(self) -> str: 158 | """ 159 | Get the filesystem path of the binary being decompiled. 160 | """ 161 | return self._instance.project.loader.main_object.binary 162 | 163 | def versions(self) -> dict[str, str]: 164 | """ 165 | Get version information about the decompiler environment. 166 | """ 167 | return { 168 | "name": "angr", 169 | "version": angr.__version__, 170 | "python": sys.version, 171 | } 172 | 173 | 174 | def focus_address(self, addr: int) -> bool: 175 | """ 176 | Focus the given address in the GUI of the decompiler. If possible, 177 | don't switch the window focus. 178 | 179 | Returns: 180 | True if successful, otherwise False 181 | """ 182 | self._workspace.jump_to(self.rebase_addr(addr)) 183 | return True 184 | 185 | # 186 | # XMLRPC Server 187 | # 188 | 189 | def ping(self): 190 | return True 191 | 192 | def start_xmlrpc_server(self, host="localhost", port=3662): 193 | """ 194 | Initialize the XMLRPC thread. 195 | """ 196 | host = host or self.host 197 | port = port or self.port 198 | 199 | print("[+] Starting XMLRPC server: {}:{}".format(host, port)) 200 | server = SimpleXMLRPCServer( 201 | (host, port), 202 | requestHandler=RequestHandler, 203 | logRequests=False, 204 | allow_none=True 205 | ) 206 | server.register_introspection_functions() 207 | server.register_function(self.decompile) 208 | server.register_function(self.function_headers) 209 | server.register_function(self.function_data) 210 | server.register_function(self.global_vars) 211 | server.register_function(self.structs) 212 | server.register_function(self.breakpoints) 213 | server.register_function(self.binary_path) 214 | server.register_function(self.versions) 215 | server.register_function(self.focus_address) 216 | server.register_function(self.ping) 217 | print("[+] Registered decompilation server!") 218 | while True: 219 | server.handle_request() 220 | -------------------------------------------------------------------------------- /decompilers/d2d_binja/server.py: -------------------------------------------------------------------------------- 1 | from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler 2 | import sys 3 | 4 | from binaryninja import SymbolType, EntryRegisterValue 5 | from binaryninja.binaryview import BinaryDataNotification 6 | import binaryninja 7 | 8 | 9 | def rebase_addr(bv, addr: int, rebase_down: bool = False) -> int: 10 | base = bv.start 11 | rebased_addr = addr 12 | if rebase_down: 13 | rebased_addr -= base 14 | elif addr < base: 15 | rebased_addr += base 16 | return rebased_addr 17 | 18 | # 19 | # Binja Hooks 20 | # 21 | 22 | 23 | class DataNotification(BinaryDataNotification): 24 | def __init__(self, bv, server): 25 | super().__init__() 26 | self.bv = bv 27 | self.server = server # type: BinjaDecompilerServer 28 | 29 | def symbol_updated(self, view, sym): 30 | sym_addr: str = str(rebase_addr(self.bv, sym.address, rebase_down=True)) 31 | if sym.type == SymbolType.FunctionSymbol: 32 | self.server.cache["function_headers"][sym_addr]["name"] = sym.name 33 | elif sym.type == SymbolType.DataSymbol: 34 | self.server.cache["global_vars"][sym_addr]["name"] = sym.name 35 | 36 | # 37 | # Server Code 38 | # 39 | 40 | 41 | class RequestHandler(SimpleXMLRPCRequestHandler): 42 | rpc_paths = ("/RPC2",) 43 | 44 | 45 | class BinjaDecompilerServer: 46 | def __init__(self, bv, host=None, port=None): 47 | self.bv = bv 48 | self.host = host 49 | self.port = port 50 | 51 | # save the last line for bugging decomp mapping 52 | self._last_line = 0 53 | 54 | # cache changes so we don't need to regen content 55 | self.cache = { 56 | "global_vars": None, 57 | "function_headers": None 58 | } 59 | 60 | # make the server init cache data once 61 | self.function_headers() 62 | self.global_vars() 63 | 64 | # init hooks for cache 65 | notification = DataNotification(self.bv, self) 66 | self.bv.register_notification(notification) 67 | 68 | 69 | # 70 | # Public API 71 | # 72 | 73 | def decompile(self, addr: int): 74 | resp = { 75 | "decompilation": None, 76 | "curr_line": None, 77 | "func_name": None 78 | } 79 | addr = rebase_addr(self.bv, addr) 80 | funcs = self.bv.get_functions_containing(addr) 81 | if not funcs: 82 | return resp 83 | func = funcs[0] 84 | 85 | decomp = str(func.hlil).split("\n") 86 | if not decomp: 87 | return resp 88 | 89 | resp["decompilation"] = decomp 90 | resp["func_name"] = func.name 91 | 92 | # find the decompiled line closest to the current addr 93 | decomp_lines = func.get_low_level_il_at(addr).hlils 94 | if not decomp_lines: 95 | resp["curr_line"] = self._last_line 96 | return resp 97 | 98 | best_line = min(decomp_lines, key=lambda l: abs(l.address - addr)) 99 | 100 | resp["curr_line"] = best_line.instr_index 101 | self._last_line = resp["curr_line"] if resp["curr_line"] != 0 else self._last_line 102 | return resp 103 | 104 | def function_data(self, addr: int): 105 | """ 106 | Returns stack vars and func args 107 | 108 | """ 109 | resp = { 110 | "reg_vars": [], 111 | "stack_vars": [] 112 | } 113 | 114 | addr = rebase_addr(self.bv, addr) 115 | funcs = self.bv.get_functions_containing(addr) 116 | if not funcs: 117 | return resp 118 | 119 | func = funcs[0] 120 | 121 | # get stack vars 122 | stack_vars = [] 123 | for stack_var in func.stack_layout: 124 | # https://api.binary.ninja/binaryninja.variable-module.html#corevariable 125 | # Doesn't really specify, but the offset is from the frame ptr (near ret addr). 126 | # The value is negative, so we flip it. 127 | stack_vars.append({ 128 | "name": stack_var.name, 129 | "type": str(stack_var.type), 130 | "from_sp": None, 131 | "from_frame": str(-stack_var.storage), 132 | }) 133 | 134 | # get reg vars 135 | reg_vars = [] 136 | for var in func.vars: 137 | if var.source_type != binaryninja.VariableSourceType.RegisterVariableSourceType or not var.name: 138 | continue 139 | 140 | reg_vars.append({ 141 | "name": var.name, 142 | "type": str(var.type), 143 | "reg_name": self.bv.arch.get_reg_name(var.storage), 144 | }) 145 | 146 | resp["reg_vars"] = reg_vars 147 | resp["stack_vars"] = stack_vars 148 | 149 | return resp 150 | 151 | def function_headers(self): 152 | # check if a cache is available 153 | cache_headers = self.cache["function_headers"] 154 | if cache_headers: 155 | return cache_headers 156 | 157 | resp = {} 158 | for func in self.bv.functions: 159 | 160 | # Skip everything besides FunctionSymbol 161 | if func.symbol.type != SymbolType.FunctionSymbol: 162 | continue 163 | 164 | resp[str(rebase_addr(self.bv, func.start, True))] = { 165 | "name": func.name, 166 | "size": func.total_bytes 167 | } 168 | 169 | self.cache["function_headers"] = resp 170 | return resp 171 | 172 | def global_vars(self): 173 | # check if a cache is available 174 | cache_globals = self.cache["global_vars"] 175 | if cache_globals: 176 | return cache_globals 177 | 178 | resp = {} 179 | for addr, var in self.bv.data_vars.items(): 180 | sym = self.bv.get_symbol_at(addr) 181 | name = sym.name if sym else "data_{:x}".format(addr) 182 | 183 | resp[str(rebase_addr(self.bv, addr, True))] = { 184 | "name": name 185 | } 186 | 187 | self.cache["global_vars"] = resp 188 | return resp 189 | 190 | def structs(self): 191 | resp = {} 192 | """ 193 | # tuple of structure name and StructureType 194 | for t in self.bv.types.items(): 195 | struct_name = t[0] 196 | struct = t[1] 197 | resp[struct_name] = { 198 | "size": struct.width 199 | } 200 | for member in struct.members: 201 | resp[struct_name][member.name] = { 202 | "offset": member.offset, 203 | "size": len(member) 204 | } 205 | """ 206 | 207 | return resp 208 | 209 | def breakpoints(self): 210 | resp = {} 211 | return resp 212 | 213 | def binary_path(self) -> str: 214 | """ 215 | Get the filesystem path of the binary being decompiled. 216 | """ 217 | return self.bv.file.original_filename 218 | 219 | def versions(self) -> dict[str, str]: 220 | """ 221 | Get version information about the decompiler environment. 222 | """ 223 | resp = { 224 | # the name of the decompiler 225 | "name": "binaryninja", 226 | # the version of the decompiler 227 | "version": binaryninja.core_version(), 228 | # the version of the runtime it uses 229 | "python": sys.version, 230 | } 231 | return resp 232 | 233 | def focus_address(self, addr: int) -> bool: 234 | """ 235 | Focus the given address in the GUI of the decompiler. If possible, 236 | don't switch the window focus. 237 | 238 | Returns: 239 | True if successful, otherwise False 240 | """ 241 | 242 | addr = rebase_addr(self.bv, addr) 243 | self.bv.navigate(self.bv.view, addr) 244 | return True 245 | 246 | # 247 | # XMLRPC Server 248 | # 249 | 250 | def ping(self): 251 | return True 252 | 253 | def start_xmlrpc_server(self, host="localhost", port=3662): 254 | """ 255 | Initialize the XMLRPC thread. 256 | """ 257 | host = host or self.host 258 | port = port or self.port 259 | 260 | print("[+] Starting XMLRPC server: {}:{}".format(host, port)) 261 | server = SimpleXMLRPCServer( 262 | (host, port), 263 | requestHandler=RequestHandler, 264 | logRequests=False, 265 | allow_none=True 266 | ) 267 | server.register_introspection_functions() 268 | server.register_function(self.decompile) 269 | server.register_function(self.function_headers) 270 | server.register_function(self.function_data) 271 | server.register_function(self.global_vars) 272 | server.register_function(self.structs) 273 | server.register_function(self.breakpoints) 274 | server.register_function(self.binary_path) 275 | server.register_function(self.versions) 276 | server.register_function(self.focus_address) 277 | server.register_function(self.ping) 278 | print("[+] Registered decompilation server!") 279 | while True: 280 | server.handle_request() 281 | -------------------------------------------------------------------------------- /decomp2dbg/clients/gdb/utils.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from typing import Dict, Optional, Sequence, Union, Any, List, Callable 3 | import subprocess 4 | import os 5 | from functools import lru_cache 6 | import functools 7 | import collections 8 | import re 9 | import tempfile 10 | import hashlib 11 | from pathlib import Path 12 | 13 | from elftools.elf.elffile import ELFFile 14 | 15 | from ...utils import gef_pystring, warn, err 16 | 17 | import gdb 18 | 19 | GLOBAL_TMP_DIR = os.path.join(tempfile.gettempdir(), "d2d") 20 | ARCH = None 21 | 22 | 23 | def identify_arch(): 24 | global ARCH 25 | if ARCH: 26 | return ARCH 27 | 28 | with open(get_filepath(), "rb") as fp: 29 | elf = ELFFile(fp) 30 | 31 | ARCH = elf.get_machine_arch() 32 | return ARCH 33 | 34 | 35 | def get_arch_func_args(): 36 | # args taken from GEF 37 | arch_args = { 38 | "x64": ["$rdi", "$rsi", "$rdx", "$rcx", "$r8", "$r9"], 39 | "x86": [f'$esp+{x}' for x in range(0, 28, 4)], 40 | "ARM": ["$r0", "$r1", "$r2", "$r3"], 41 | "SPARC": ["$o0 ", "$o1 ", "$o2 ", "$o3 ", "$o4 ", "$o5 ", "$o7 "], 42 | "MIPS": ["$a0", "$a1", "$a2", "$a3"], 43 | "RISC-V": ["$a0", "$a1", "$a2", "$a3", "$a4", "$a5", "$a6", "$a7"] 44 | } 45 | 46 | arch = identify_arch() 47 | try: 48 | args = arch_args[arch] 49 | except KeyError: 50 | args = [] 51 | 52 | return args 53 | 54 | 55 | def vmmap_base_addrs(): 56 | addr_maps = {} 57 | mappings = gdb.execute("info proc mappings", to_string=True).split("\n") 58 | for mapping in mappings: 59 | try: 60 | addr = int(re.findall(r"0x[0-9a-fA-F]+", mapping)[0], 16) 61 | path = re.findall(r"(/.*)", mapping)[0].strip() 62 | except IndexError: 63 | continue 64 | 65 | # always use the lowest addr 66 | if path in addr_maps or path.startswith("["): 67 | continue 68 | 69 | if addr and path: 70 | addr_maps[path] = addr 71 | 72 | return addr_maps 73 | 74 | 75 | def find_text_segment_base_addr(is_remote=False): 76 | with open(get_filepath(), 'rb') as fp: 77 | binary_hash = hashlib.md5(fp.read()).hexdigest() 78 | 79 | if is_remote: 80 | def _should_hash_cmp(path_name): 81 | return True 82 | else: 83 | def _should_hash_cmp(path_name): 84 | return path_name == get_filepath() 85 | 86 | for path, addr in vmmap_base_addrs().items(): 87 | if _should_hash_cmp(path): 88 | file = download_file(path) if is_remote else path 89 | with open(file, 'rb') as fp: 90 | other_file_hash = hashlib.md5(fp.read()).hexdigest() 91 | 92 | if other_file_hash == binary_hash: 93 | return addr 94 | else: 95 | raise Exception("Unable to find the text segment base addr, please report this!") 96 | 97 | 98 | @lru_cache() 99 | def is_32bit(): 100 | ptr_size = int(gdb.execute("p sizeof(long long)", to_string=True).split("= ")[1].strip(), 0) 101 | if ptr_size == 4: 102 | return True 103 | 104 | return False 105 | 106 | 107 | def pc(): 108 | try: 109 | pc_ = int(gdb.execute("print/x $pc",to_string=True).split(" ")[-1], 16) 110 | except Exception: 111 | pc_ = None 112 | 113 | return pc_ 114 | 115 | 116 | # 117 | # GEF Clone 118 | # 119 | 120 | @lru_cache() 121 | def which(program: str) -> Optional[pathlib.Path]: 122 | """Locate a command on the filesystem.""" 123 | for path in os.environ["PATH"].split(os.pathsep): 124 | dirname = pathlib.Path(path) 125 | fpath = dirname / program 126 | if os.access(fpath, os.X_OK): 127 | return fpath 128 | 129 | raise FileNotFoundError(f"Missing file `{program}`") 130 | 131 | def exec_external(command: Sequence[str], as_list: bool = False, **kwargs: Any) -> Union[str, List[str]]: 132 | """Execute an external command and return the result.""" 133 | res = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=kwargs.get("shell", False)) 134 | return [gef_pystring(_) for _ in res.splitlines()] if as_list else gef_pystring(res) 135 | 136 | @lru_cache(32) 137 | def checksec(filename: str) -> Dict[str, bool]: 138 | """Check the security property of the ELF binary. The following properties are: 139 | - Canary 140 | - NX 141 | - PIE 142 | - Fortify 143 | - Partial/Full RelRO. 144 | Return a dict() with the different keys mentioned above, and the boolean 145 | associated whether the protection was found.""" 146 | readelf = which("readelf") 147 | 148 | def __check_security_property(opt: str, filename: str, pattern: str) -> bool: 149 | cmd = [readelf,] 150 | cmd += opt.split() 151 | cmd += [filename,] 152 | lines = exec_external(cmd, as_list=True) 153 | for line in lines: 154 | if re.search(pattern, line): 155 | return True 156 | return False 157 | 158 | results = collections.OrderedDict() 159 | results["Canary"] = __check_security_property("-rs", filename, r"__stack_chk_fail") is True 160 | has_gnu_stack = __check_security_property("-W -l", filename, r"GNU_STACK") is True 161 | if has_gnu_stack: 162 | results["NX"] = __check_security_property("-W -l", filename, r"GNU_STACK.*RWE") is False 163 | else: 164 | results["NX"] = False 165 | results["PIE"] = __check_security_property("-h", filename, r":.*EXEC") is False 166 | results["Fortify"] = __check_security_property("-s", filename, r"_chk@GLIBC") is True 167 | results["Partial RelRO"] = __check_security_property("-l", filename, r"GNU_RELRO") is True 168 | results["Full RelRO"] = results["Partial RelRO"] and __check_security_property("-d", filename, r"BIND_NOW") is True 169 | return results 170 | 171 | 172 | 173 | @lru_cache() 174 | def is_remote_debug() -> bool: 175 | """"Return True is the current debugging session is running through GDB remote session.""" 176 | return "remote" in gdb.execute("maintenance print target-stack", to_string=True) 177 | 178 | 179 | def pid() -> int: 180 | """Return the PID of the target process.""" 181 | pid_ = gdb.selected_inferior().pid 182 | if not pid_: 183 | pid_ = gdb.selected_thread().ptid[1] 184 | if not pid_: 185 | raise RuntimeError("cannot retrieve PID for target process") 186 | 187 | return pid_ 188 | 189 | 190 | 191 | @lru_cache() 192 | def get_filepath() -> Optional[str]: 193 | """Return the local absolute path of the file currently debugged.""" 194 | filename = gdb.current_progspace().filename 195 | filepath = None 196 | if is_remote_debug(): 197 | # if no filename specified, try downloading target from /proc 198 | if filename is None: 199 | pid_ = pid() 200 | if pid_ > 0: 201 | filepath = download_file(f"/proc/{pid_:d}/exe", use_cache=True) 202 | # if target is remote file, download 203 | elif filename.startswith("target:"): 204 | fname = filename[len("target:") :] 205 | filepath = download_file(fname, use_cache=True, local_name=fname) 206 | 207 | elif filename.startswith(".gnu_debugdata for target:"): 208 | fname = filename[len(".gnu_debugdata for target:") :] 209 | filepath = download_file(fname, use_cache=True, local_name=fname) 210 | else: 211 | filepath = filename 212 | else: 213 | if filename is not None: 214 | filepath = filename 215 | else: 216 | # inferior probably did not have name, extract cmdline from info proc 217 | filepath = get_path_from_info_proc() 218 | 219 | try: 220 | filepath = Path(filepath).resolve() 221 | except Exception: 222 | err(f"Failed to resolve path in get_filepath(): {filepath}, this error is fatal.") 223 | 224 | return str(filepath) if filepath else None 225 | 226 | 227 | def get_path_from_info_proc() -> Optional[str]: 228 | for x in gdb.execute("info proc", to_string=True).splitlines(): 229 | if x.startswith("exe = "): 230 | return x.split(" = ")[1].replace("'", "") 231 | return None 232 | 233 | 234 | def download_file(remote_path: str, use_cache: bool = False, local_name: Optional[str] = None) -> Optional[str]: 235 | """Download filename `remote_path` inside the mirror tree inside the `gef.config["gef.tempdir"]`. 236 | The tree architecture must be `gef.config["gef.tempdir"]/gef//`. 237 | This allow a "chroot-like" tree format.""" 238 | 239 | local_root = pathlib.Path(GLOBAL_TMP_DIR) / str(pid()) 240 | if local_name is None: 241 | local_path = local_root / remote_path.strip(os.sep) 242 | else: 243 | local_path = local_root / local_name.strip(os.sep) 244 | 245 | if use_cache and local_path.exists(): 246 | return str(local_path.absolute()) 247 | 248 | try: 249 | local_path.parent.mkdir(parents=True, exist_ok=True) 250 | gdb.execute(f"remote get {remote_path} {local_path.absolute()}") 251 | local_path = str(local_path.absolute()) 252 | except gdb.error: 253 | # fallback memory view 254 | with open(local_path, "w") as f: 255 | if is_32bit(): 256 | f.write(f"00000000-ffffffff rwxp 00000000 00:00 0 {get_filepath()}\n") 257 | else: 258 | f.write(f"0000000000000000-ffffffffffffffff rwxp 00000000 00:00 0 {get_filepath()}\n") 259 | 260 | except Exception as e: 261 | err(f"download_file() failed: {e}") 262 | local_path = None 263 | 264 | return local_path 265 | 266 | 267 | def is_alive() -> bool: 268 | """Check if GDB is running.""" 269 | try: 270 | return gdb.selected_inferior().pid > 0 271 | except Exception: 272 | return False 273 | 274 | 275 | def only_if_gdb_running(f: Callable) -> Callable: 276 | """Decorator wrapper to check if GDB is running.""" 277 | 278 | @functools.wraps(f) 279 | def wrapper(*args: Any, **kwargs: Any) -> Any: 280 | if is_alive(): 281 | return f(*args, **kwargs) 282 | else: 283 | warn("No debugging session active") 284 | 285 | return wrapper 286 | 287 | -------------------------------------------------------------------------------- /decomp2dbg/clients/gdb/symbol_mapper.py: -------------------------------------------------------------------------------- 1 | from ...utils import * 2 | from .utils import * 3 | import tempfile 4 | from elftools.elf.elffile import ELFFile 5 | import shlex 6 | 7 | import gdb 8 | 9 | 10 | class SymbolMapper: 11 | """ 12 | A binary search dict implementation for ranges. Symbols will map for a range and we need to 13 | be able to lookup addresses in the middle of the range fast 14 | """ 15 | 16 | __slots__ = ( 17 | 'text_base_addr', 18 | '_elf_cache', 19 | '_objcopy', 20 | '_gcc', 21 | '_last_sym_files', 22 | '_sym_file_ctr' 23 | ) 24 | 25 | def __init__(self): 26 | self.text_base_addr = None 27 | self._elf_cache = {} 28 | self._objcopy = None 29 | self._gcc = None 30 | self._last_sym_files = set() 31 | self._sym_file_ctr = 0 32 | 33 | 34 | # 35 | # Native Symbol Support (Linux Only) 36 | # Inspired by Bata24 37 | # 38 | 39 | def add_native_symbols(self, sym_info_list): 40 | """ 41 | Adds a list of symbols to gdb's internal symbol listing. Only function and global symbols are supported. 42 | Symbol info looks like: 43 | (symbol_name: str, base_addr: int, sym_type: str, size: int) 44 | If you don't know the size, pass 0. 45 | 46 | Explanation of how this works: 47 | Adding symbols to GDB is non-trivial, it requires the use of an entire object file. Because of its 48 | difficulty, this is currently only supported on ELFs. When adding a symbol, we use two binutils, 49 | gcc and objcopy. After making a small ELF, we strip it of everything but needed sections. We then 50 | use objcopy to one-by-one add a symbol to the file. Objcopy does not support sizing, so we do a byte 51 | patch on the binary to allow for a real size. Finally, the whole object is read in with the default 52 | gdb command: add-symbol-file. 53 | """ 54 | 55 | if not self.check_native_symbol_support(): 56 | err("Native symbol support not supported on this platform.") 57 | info("If you are on Linux and want native symbol support make sure you have gcc and objcopy.") 58 | return False 59 | 60 | if self.text_base_addr is None: 61 | err("Base address of the binary has not been discovered yet, please run the binary and try again.") 62 | return False 63 | 64 | # info("{:d} symbols will be added".format(len(sym_info_list))) 65 | self._delete_old_sym_files() 66 | 67 | # add each symbol into a mass symbol commit 68 | max_commit_size = 1500 69 | supported_types = ["function", "object"] 70 | 71 | objcopy_cmds = [] 72 | queued_sym_sizes = {} 73 | fname = self._construct_small_elf() 74 | for i, (name, addr, typ, size) in enumerate(sym_info_list): 75 | if typ not in supported_types: 76 | warn(f"Skipping symbol {name}, type is not supported: {typ}") 77 | continue 78 | 79 | # queue the sym for later use 80 | queued_sym_sizes[i % max_commit_size] = size 81 | 82 | # absolute addressing 83 | #if addr >= self.text_base_addr: 84 | # addr_str = "{:#x}".format(addr) 85 | # relative addressing 86 | #else: 87 | 88 | # you always want relative adressing 89 | addr_str = ".text:{:#x}".format(addr) 90 | 91 | # clean name 92 | name = self._clean_string(name) 93 | 94 | # create a symbol command for the symbol 95 | objcopy_cmds.append( 96 | '--add-symbol "{name}={addr_str},global,{type_flag}"'.format( 97 | name=name, addr_str=addr_str, type_flag=typ 98 | ) 99 | ) 100 | 101 | # batch commit 102 | if i > 1 and i % max_commit_size == 0: 103 | # add the queued symbols 104 | self._add_symbol_file(fname, objcopy_cmds, self.text_base_addr, queued_sym_sizes) 105 | 106 | # re-init queues and elf 107 | fname = self._construct_small_elf() 108 | objcopy_cmds = [] 109 | queued_sym_sizes = {} 110 | 111 | # commit remaining symbol commands 112 | if objcopy_cmds: 113 | self._add_symbol_file(fname, objcopy_cmds, self.text_base_addr, queued_sym_sizes) 114 | 115 | return True 116 | 117 | def check_native_symbol_support(self): 118 | # validate binutils bins exist 119 | try: 120 | self._gcc = which("gcc") 121 | self._objcopy = which("objcopy") 122 | except FileNotFoundError as e: 123 | err(f"Binutils binaries not found: {e}") 124 | return False 125 | 126 | return True 127 | 128 | @staticmethod 129 | def _clean_string(string: str): 130 | return re.sub(r'[^\w_. -:]', '_', string) 131 | 132 | def _delete_old_sym_files(self): 133 | for sym_file in self._last_sym_files: 134 | try: 135 | gdb.execute(f"remove-symbol-file {sym_file}") 136 | except Exception as e: 137 | pass 138 | 139 | self._last_sym_files = set() 140 | self._sym_file_ctr = 0 141 | 142 | def _construct_small_elf(self): 143 | if self._elf_cache: 144 | new_name = self._elf_cache["fname"]+str(self._sym_file_ctr) 145 | open(new_name, "wb").write(self._elf_cache["data"]) 146 | self._sym_file_ctr += 1 147 | self._last_sym_files.add(new_name) 148 | return new_name 149 | 150 | # compile a small elf for symbol loading 151 | fd, fname = tempfile.mkstemp(dir="/tmp", suffix=".c") 152 | os.fdopen(fd, "w").write("int main() {}") 153 | # os.system(f"{self._gcc} {fname} -no-pie -o {fname}.debug") 154 | os.system(f"{self._gcc} {fname} -o {fname}.debug") 155 | # destroy the source file 156 | os.unlink(f"{fname}") 157 | 158 | # delete unneeded sections from object file 159 | os.system(f"{self._objcopy} --only-keep-debug {fname}.debug") 160 | os.system(f"{self._objcopy} --strip-all {fname}.debug") 161 | 162 | elf = ELFFile(open(f'{fname}.debug', 'rb')) 163 | 164 | required_sections = [".text", ".interp", ".rela.dyn", ".dynamic", ".bss"] 165 | all_sections = [s.name for s in elf.iter_sections()] 166 | for s_name in all_sections: 167 | # keep some required sections, skip broken ones 168 | if not s_name or s_name in required_sections: 169 | continue 170 | 171 | os.system(f"{self._objcopy} --remove-section={s_name} {fname}.debug 2>/dev/null") 172 | 173 | # cache the small object file for use 174 | self._elf_cache["fname"] = fname + ".debug" 175 | 176 | # add it to known sym files 177 | self._last_sym_files.add(self._elf_cache["fname"]) 178 | 179 | self._elf_cache["data"] = open(self._elf_cache["fname"], "rb").read() 180 | return self._elf_cache["fname"] 181 | 182 | def _force_update_text_size(self, stream, elf, elf_data, new_size): 183 | text_sect = elf.get_section_by_name('.text') 184 | 185 | for count in range(elf['e_shnum']): 186 | sect_off = elf['e_shoff'] + count * elf['e_shentsize'] 187 | stream.seek(sect_off) 188 | section_header = elf.structs.Elf_Shdr.parse_stream(stream) 189 | if section_header['sh_name'] == text_sect['sh_name']: 190 | break 191 | 192 | patch = struct.pack(" object: 56 | output = [None] 57 | 58 | # 59 | # this inline function definition is technically what will execute 60 | # in the context of the main thread. we use this thunk to capture 61 | # any output the function may want to return to the user. 62 | # 63 | 64 | def thunk(): 65 | output[0] = func(*args, **kwargs) 66 | return 1 67 | 68 | if is_mainthread(): 69 | thunk() 70 | else: 71 | idaapi.execute_sync(thunk, sync_type) 72 | 73 | # return the output of the synchronized execution 74 | return output[0] 75 | return wrapper 76 | 77 | 78 | def execute_read(func): 79 | return execute_sync(func, idaapi.MFF_READ) 80 | 81 | 82 | def execute_write(func): 83 | return execute_sync(func, idaapi.MFF_WRITE) 84 | 85 | 86 | def execute_ui(func): 87 | return execute_sync(func, idaapi.MFF_FAST) 88 | 89 | 90 | # 91 | # Decompilation API 92 | # 93 | 94 | class IDADecompilerServer: 95 | def __init__(self, host=None, port=None): 96 | self.host = host 97 | self.port = port 98 | self.cache = { 99 | "global_vars": None, 100 | "function_headers": None 101 | } 102 | self._base_addr = None 103 | 104 | # make the server init cache data once 105 | self.function_headers() 106 | self.global_vars() 107 | 108 | def rebase_addr(self, addr, down=False): 109 | if self._base_addr is None: 110 | self._base_addr = idaapi.get_imagebase() 111 | 112 | rebased = addr 113 | if down: 114 | rebased -= self._base_addr 115 | elif addr < self._base_addr: 116 | rebased += self._base_addr 117 | 118 | return rebased 119 | 120 | 121 | @execute_read 122 | def decompile(self, addr): 123 | addr = self.rebase_addr(addr) 124 | resp = { 125 | "decompilation": None, 126 | "curr_line": None, 127 | "func_name": None, 128 | } 129 | 130 | # get the function 131 | ida_func = ida_funcs.get_func(addr) 132 | if not ida_func: 133 | return resp 134 | 135 | func_addr = ida_func.start_ea 136 | func_name = idc.get_func_name(func_addr) 137 | 138 | # attempt decompilation 139 | try: 140 | cfunc = ida_hexrays.decompile(func_addr) 141 | except Exception as e: 142 | return resp 143 | 144 | # locate decompilation line 145 | item = cfunc.body.find_closest_addr(addr) 146 | y_holder = idaapi.int_pointer() 147 | if not cfunc.find_item_coords(item, None, y_holder): 148 | # if idaapi failes, try a dirty search! 149 | up = cfunc.body.find_closest_addr(addr - 0x10) 150 | if not cfunc.find_item_coords(up, None, y_holder): 151 | down = cfunc.body.find_closest_addr(addr + 0x10) 152 | if not cfunc.find_item_coords(down, None, y_holder): 153 | return resp 154 | 155 | cur_line_num = y_holder.value() 156 | 157 | # decode ida lines 158 | enc_lines = cfunc.get_pseudocode() 159 | decomp_lines = [idaapi.tag_remove(l.line) for l in enc_lines] 160 | 161 | resp["decompilation"] = decomp_lines 162 | resp["curr_line"] = cur_line_num 163 | resp["func_name"] = func_name 164 | 165 | return resp 166 | 167 | @execute_read 168 | def function_data(self, addr): 169 | addr = self.rebase_addr(addr) 170 | resp = { 171 | "stack_vars": None, 172 | "reg_vars": None 173 | } 174 | 175 | # get the function 176 | ida_func = ida_funcs.get_func(addr) 177 | if not ida_func: 178 | return resp 179 | func_addr = ida_func.start_ea 180 | 181 | # attempt decompilation 182 | try: 183 | cfunc = ida_hexrays.decompile(func_addr) 184 | except Exception as e: 185 | return resp 186 | 187 | # get var info 188 | stack_vars = [] 189 | reg_vars = [] 190 | 191 | for var in cfunc.lvars: 192 | if not var.name: 193 | continue 194 | 195 | # stack variables 196 | if var.is_stk_var(): 197 | # https://python.docs.hex-rays.com/ida_typeinf/index.html#ida_typeinf.argloc_t.stkoff 198 | # Doesn't really specify what offset, but testing shows its from RSP. 199 | stack_vars.append({ 200 | "name": var.name, 201 | "type": str(var.type()), 202 | "from_sp": str(var.location.stkoff()), 203 | "from_frame": None 204 | }) 205 | 206 | # register variables 207 | elif var.is_reg_var(): 208 | regnum = var.get_reg1() 209 | reg_name = idaapi.get_mreg_name(regnum, var.width) 210 | 211 | reg_vars.append({ 212 | "name": var.name, 213 | "type": str(var.type()), 214 | "reg_name": reg_name, 215 | }) 216 | 217 | resp["stack_vars"] = stack_vars 218 | resp["reg_vars"] = reg_vars 219 | 220 | return resp 221 | 222 | @execute_read 223 | def function_headers(self): 224 | # check if a cache is available 225 | cache_headers = self.cache["function_headers"] 226 | if cache_headers: 227 | return cache_headers 228 | 229 | resp = {} 230 | # no cache, compute it 231 | for f_addr in idautils.Functions(): 232 | # assure the function is not a linked library function 233 | if not ((idc.get_func_flags(f_addr) & idc.FUNC_LIB) == idc.FUNC_LIB): 234 | func_name = ida_funcs.get_func_name(f_addr) 235 | if not isinstance(func_name, str): 236 | continue 237 | 238 | # double check for .plt or .got names 239 | if func_name.startswith(".") or "@" in func_name: 240 | continue 241 | 242 | func_size = ida_funcs.get_func(f_addr).size() 243 | resp[str(self.rebase_addr(f_addr, down=True))] = { 244 | "name": func_name, 245 | "size": func_size 246 | } 247 | 248 | self.cache["function_headers"] = resp 249 | return resp 250 | 251 | @execute_read 252 | def global_vars(self): 253 | # check if a cache is available 254 | cache_globals = self.cache["global_vars"] 255 | if cache_globals: 256 | return cache_globals 257 | 258 | resp = {} 259 | known_segs = [".data", ".bss"] 260 | for seg_name in known_segs: 261 | seg = idaapi.get_segm_by_name(seg_name) 262 | if not seg: 263 | continue 264 | 265 | for seg_ea in range(seg.start_ea, seg.end_ea): 266 | xrefs = idautils.XrefsTo(seg_ea) 267 | try: 268 | next(xrefs) 269 | except StopIteration: 270 | continue 271 | 272 | name = idaapi.get_name(seg_ea) 273 | if not name: 274 | continue 275 | 276 | resp[str(self.rebase_addr(seg_ea, down=True))] = { 277 | "name": name 278 | } 279 | 280 | self.cache["global_vars"] = resp 281 | return resp 282 | 283 | @execute_read 284 | def structs(self): 285 | resp = {} 286 | for i in range(idaapi.get_struc_qty()): 287 | struct_id = idaapi.get_struc_by_idx(i) 288 | struct = idaapi.get_struc(struct_id) 289 | struct_info = { 290 | "name": idaapi.get_struc_name(struct.id), 291 | "members": [] 292 | } 293 | 294 | for member in struct.members: 295 | member_info = { 296 | "name": idaapi.get_member_name(member.id) 297 | } 298 | 299 | tif = idaapi.tinfo_t() 300 | if idaapi.get_member_tinfo(tif, member): 301 | member_info["type"] = tif.__str__() 302 | member_info["size"] = tif.get_size() 303 | struct_info["members"].append(member_info) 304 | 305 | resp["struct_info"].append(struct_info) 306 | return resp 307 | 308 | def breakpoints(self): 309 | resp = {} 310 | return resp 311 | 312 | @execute_read 313 | def binary_path(self) -> str: 314 | """ 315 | Get the filesystem path of the binary being decompiled. 316 | """ 317 | return ida_nalt.get_input_file_path() 318 | 319 | @execute_read 320 | def versions(self) -> dict[str, str]: 321 | """ 322 | Get version information about the decompiler environment. 323 | """ 324 | resp = { 325 | # the name of the decompiler 326 | "name": "ida", 327 | # the version of the decompiler 328 | "version": idaapi.get_kernel_version(), 329 | # the version of the runtime it uses 330 | "python": sys.version, 331 | # any decompiler-specific auxiliary stuff 332 | "hexrays": idaapi.get_hexrays_version() if idaapi.init_hexrays_plugin() else None, 333 | } 334 | return resp 335 | 336 | @execute_read 337 | def focus_address(self, addr: int) -> bool: 338 | """ 339 | Focus the given address in the GUI of the decompiler. If possible, 340 | don't switch the window focus. 341 | 342 | Returns: 343 | True if successful, otherwise False 344 | """ 345 | addr = self.rebase_addr(addr) 346 | # https://python.docs.hex-rays.com/ida_kernwin/index.html#ida_kernwin.jumpto 347 | # Using C++ api instead of idc one to avoid activating the IDA window 348 | return ida_kernwin.jumpto(addr, -1, 0) 349 | 350 | # 351 | # XMLRPC Server 352 | # 353 | 354 | def ping(self): 355 | return True 356 | 357 | def start_xmlrpc_server(self, host="localhost", port=3662): 358 | """ 359 | Initialize the XMLRPC thread. 360 | """ 361 | host = host or self.host 362 | port = port or self.port 363 | 364 | print("[+] Starting XMLRPC server: {}:{}".format(host, port)) 365 | server = SimpleXMLRPCServer( 366 | (host, port), 367 | requestHandler=RequestHandler, 368 | logRequests=False, 369 | allow_none=True 370 | ) 371 | server.register_introspection_functions() 372 | server.register_function(self.decompile) 373 | server.register_function(self.function_headers) 374 | server.register_function(self.function_data) 375 | server.register_function(self.global_vars) 376 | server.register_function(self.structs) 377 | server.register_function(self.breakpoints) 378 | server.register_function(self.binary_path) 379 | server.register_function(self.versions) 380 | server.register_function(self.focus_address) 381 | server.register_function(self.ping) 382 | print("[+] Registered decompilation server!") 383 | while True: 384 | server.handle_request() 385 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/main/java/decomp2dbg/D2DGhidraServerAPI.java: -------------------------------------------------------------------------------- 1 | package decomp2dbg; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import ghidra.app.decompiler.ClangLine; 8 | import ghidra.app.decompiler.PrettyPrinter; 9 | import ghidra.app.util.bin.MemoryByteProvider; 10 | import ghidra.app.util.bin.format.elf.ElfException; 11 | import ghidra.app.util.bin.format.elf.ElfHeader; 12 | import ghidra.program.model.data.DataTypeComponent; 13 | import ghidra.program.model.listing.Function; 14 | import ghidra.program.model.listing.FunctionIterator; 15 | import ghidra.program.model.symbol.Symbol; 16 | import ghidra.program.model.symbol.SymbolType; 17 | import ghidra.util.Msg; 18 | import ghidra.framework.Application; 19 | 20 | public class D2DGhidraServerAPI { 21 | private final D2DGhidraServer server; 22 | 23 | public D2DGhidraServerAPI(D2DGhidraServer server) { 24 | this.server = server; 25 | } 26 | 27 | /* 28 | * Server Manipulation API 29 | */ 30 | 31 | public Boolean ping() { 32 | return true; 33 | } 34 | 35 | public Boolean stop() { 36 | this.server.stop_server(); 37 | return true; 38 | } 39 | 40 | /* 41 | * 42 | * Decompiler API 43 | * 44 | */ 45 | 46 | public Map decompile(Integer addr) { 47 | Map resp = new HashMap<>(); 48 | resp.put("decompilation", ""); 49 | resp.put("curr_line", -1); 50 | resp.put("func_name", ""); 51 | 52 | var rebasedAddr = this.server.plugin.rebaseAddr(addr, false); 53 | var func = this.server.plugin.getNearestFunction(rebasedAddr); 54 | long rebasedAddrLong = rebasedAddr.getOffset(); 55 | 56 | if(func == null) { 57 | Msg.warn(server, "Failed to find a function by the address " + addr); 58 | return resp; 59 | } 60 | 61 | resp.put("func_name", func.getName()); 62 | 63 | var dec = this.server.plugin.decompileFunc(func); 64 | if(dec == null) { 65 | Msg.warn(server, "Failed to decompile function by the address " + addr); 66 | return resp; 67 | } 68 | 69 | // create a nice string 70 | var decLines = dec.getDecompiledFunction().getC().split("\n"); 71 | resp.put("decompilation", decLines); 72 | 73 | PrettyPrinter pp = new PrettyPrinter(func, dec.getCCodeMarkup(), null); 74 | ArrayList lines = (ArrayList) pp.getLines(); 75 | 76 | // locate the decompilation line 77 | Boolean lineFound = false; 78 | Integer lineNumber = 0; 79 | for (ClangLine line : lines) { 80 | for (int i = 0; i < line.getNumTokens(); i++) { 81 | if (line.getToken(i).getMinAddress() == null) { 82 | continue; 83 | } 84 | long tokenMinAddr = line.getToken(i).getMinAddress().getOffset(); 85 | long tokenMaxAddr = line.getToken(i).getMaxAddress().getOffset(); 86 | if(tokenMinAddr == rebasedAddrLong || tokenMaxAddr == rebasedAddrLong || 87 | (rebasedAddrLong > tokenMinAddr && rebasedAddrLong < tokenMaxAddr)) { 88 | lineFound = true; 89 | lineNumber = line.getLineNumber(); 90 | break; 91 | } 92 | } 93 | 94 | if(lineFound) 95 | break; 96 | } 97 | 98 | // unable to locate the decompilation line 99 | if(!lineFound) 100 | return resp; 101 | 102 | resp.put("curr_line", lineNumber-1); 103 | return resp; 104 | } 105 | 106 | 107 | public Map function_data(Integer addr) { 108 | var cache = this.server.plugin.funcDataCache.get(addr); 109 | if(cache != null) 110 | return (Map) cache; 111 | 112 | var resp = this.server.plugin.getFuncData(addr); 113 | this.server.plugin.funcDataCache.put(addr, resp); 114 | return resp; 115 | } 116 | 117 | public Map function_headers() { 118 | // always check cache first 119 | if(!this.server.plugin.funcSymCache.isEmpty()) 120 | return this.server.plugin.funcSymCache; 121 | 122 | Map resp = new HashMap<>(); 123 | var program = this.server.plugin.getCurrentProgram(); 124 | var fm = program.getFunctionManager(); 125 | FunctionIterator functions = fm.getFunctions(true); 126 | for (Function func : functions) { 127 | if(func == null) 128 | continue; 129 | 130 | Map funcInfo = new HashMap<>(); 131 | funcInfo.put("name", func.getName()); 132 | funcInfo.put("size", (int) func.getBody().getNumAddresses()); 133 | var rebasedAddr = this.server.plugin.rebaseAddr((int) func.getEntryPoint().getOffset(), true); 134 | resp.put("0x" + rebasedAddr.toString(), funcInfo); 135 | 136 | } 137 | 138 | this.server.plugin.funcSymCache = resp; 139 | return resp; 140 | } 141 | 142 | public Map global_vars() { 143 | // check the cache before doing hard work! 144 | if(!this.server.plugin.gVarCache.isEmpty()) 145 | return this.server.plugin.gVarCache; 146 | 147 | // if we are here, this is first connection! 148 | Map resp = new HashMap<>(); 149 | var symTab = this.server.plugin.getCurrentProgram().getSymbolTable(); 150 | for (Symbol sym: symTab.getAllSymbols(true)) { 151 | if (sym.getSymbolType() != SymbolType.LABEL) 152 | continue; 153 | 154 | Map varInfo = new HashMap<>(); 155 | varInfo.put("name", sym.getName()); 156 | var rebasedAddr = this.server.plugin.rebaseAddr((int) sym.getAddress().getOffset(), true); 157 | resp.put("0x" + rebasedAddr.toString(), varInfo); 158 | } 159 | 160 | // cache it for next request 161 | this.server.plugin.gVarCache = resp; 162 | return resp; 163 | } 164 | 165 | public Map structs() { 166 | // check the cache before doing hard work! 167 | if(!this.server.plugin.structCache.isEmpty()) 168 | return this.server.plugin.structCache; 169 | 170 | // if we are here, this is first connection! 171 | ArrayList structInfos = new ArrayList<>(); 172 | var program = this.server.plugin.getCurrentProgram(); 173 | var dtm = program.getDataTypeManager(); 174 | var structs = dtm.getAllStructures(); 175 | 176 | while (structs.hasNext()) { 177 | var struct = structs.next(); 178 | Map structInfo = new HashMap<>(); 179 | structInfo.put("name", struct.getName()); 180 | // Enumerate members 181 | DataTypeComponent[] members = struct.getComponents(); 182 | // For unknown reasons, the API claims that some targets have structures with a crazy number of members (in the millions). 183 | // This causes an infinite loop. 184 | // May be caused by a bug in some other plugin, not sure. 185 | if (members.length > 1000) { 186 | continue; 187 | } 188 | ArrayList memberInfo = new ArrayList<>(); 189 | var unnamedMembers = 0; 190 | for (var member : members) { 191 | Map memberData = new HashMap<>(); 192 | // Some fields don't have names, and the XMLRPC impl does not like that. 193 | // Work around by assigning a surrogate name. 194 | var name = member.getFieldName(); 195 | if (name == null) { 196 | name = "unnamed" + unnamedMembers; 197 | unnamedMembers++; 198 | } 199 | memberData.put("name", name); 200 | memberData.put("size", member.getLength()); 201 | memberData.put("type", member.getDataType().getName()); 202 | memberData.put("offset", member.getOffset()); 203 | memberInfo.add(memberData); 204 | } 205 | structInfo.put("members", memberInfo); 206 | structInfos.add(structInfo); 207 | } 208 | Map resp = new HashMap<>(); 209 | resp.put("struct_info", structInfos); 210 | 211 | // cache it for next request 212 | this.server.plugin.structCache = resp; 213 | return resp; 214 | } 215 | 216 | public Map type_aliases() { 217 | // check the cache before doing hard work! 218 | if(!this.server.plugin.typeAliasCache.isEmpty()) 219 | return this.server.plugin.typeAliasCache; 220 | 221 | // if we are here, this is first connection! 222 | ArrayList aliasInfos = new ArrayList<>(); 223 | var program = this.server.plugin.getCurrentProgram(); 224 | var dtm = program.getDataTypeManager(); 225 | var allTypes = dtm.getAllDataTypes(); 226 | while (allTypes.hasNext()) { 227 | var type = allTypes.next(); 228 | 229 | if (!(type instanceof ghidra.program.model.data.TypeDef)) 230 | continue; 231 | var alias = ((ghidra.program.model.data.TypeDef) type); 232 | 233 | 234 | Map aliasInfo = new HashMap<>(); 235 | aliasInfo.put("name", type.getName()); 236 | aliasInfo.put("type", alias.getDataType().getName()); 237 | aliasInfo.put("size", type.getLength()); 238 | aliasInfos.add(aliasInfo); 239 | } 240 | Map resp = new HashMap<>(); 241 | resp.put("alias_info", aliasInfos); 242 | 243 | // cache it for next request 244 | this.server.plugin.typeAliasCache = resp; 245 | return resp; 246 | } 247 | 248 | public Map unions() { 249 | // check the cache before doing hard work! 250 | if(!this.server.plugin.unionCache.isEmpty()) 251 | return this.server.plugin.unionCache; 252 | 253 | // if we are here, this is first connection! 254 | ArrayList unionInfos = new ArrayList<>(); 255 | var program = this.server.plugin.getCurrentProgram(); 256 | var dtm = program.getDataTypeManager(); 257 | var types = dtm.getAllDataTypes(); 258 | while (types.hasNext()) { 259 | var type = types.next(); 260 | 261 | if (!(type instanceof ghidra.program.model.data.Union)) 262 | continue; 263 | var union = ((ghidra.program.model.data.Union) type); 264 | 265 | Map unionInfo = new HashMap<>(); 266 | unionInfo.put("name", union.getName()); 267 | // Enumerate members 268 | DataTypeComponent[] members = union.getComponents(); 269 | ArrayList memberInfo = new ArrayList<>(); 270 | for (var member : members) { 271 | Map memberData = new HashMap<>(); 272 | memberData.put("name", member.getFieldName()); 273 | memberData.put("size", member.getLength()); 274 | memberData.put("type", member.getDataType().getName()); 275 | memberData.put("offset", member.getOffset()); 276 | memberInfo.add(memberData); 277 | } 278 | unionInfo.put("members", memberInfo); 279 | unionInfos.add(unionInfo); 280 | } 281 | Map resp = new HashMap<>(); 282 | resp.put("union_info", unionInfos); 283 | 284 | // cache it for next request 285 | this.server.plugin.unionCache = resp; 286 | return resp; 287 | } 288 | 289 | public Map enums() { 290 | // check the cache before doing hard work! 291 | if(!this.server.plugin.enumCache.isEmpty()) 292 | return this.server.plugin.enumCache; 293 | 294 | // if we are here, this is first connection! 295 | ArrayList enumInfos = new ArrayList<>(); 296 | var program = this.server.plugin.getCurrentProgram(); 297 | var dtm = program.getDataTypeManager(); 298 | var types = dtm.getAllDataTypes(); 299 | while (types.hasNext()) { 300 | var type = types.next(); 301 | 302 | if (!(type instanceof ghidra.program.model.data.Enum)) 303 | continue; 304 | var enumType = ((ghidra.program.model.data.Enum) type); 305 | 306 | Map enumInfo = new HashMap<>(); 307 | enumInfo.put("name", enumType.getName()); 308 | // Enumerate values 309 | var values = enumType.getValues(); 310 | ArrayList valueInfo = new ArrayList<>(); 311 | for (var value : values) { 312 | Map valueData = new HashMap<>(); 313 | valueData.put("name", enumType.getName(value)); 314 | // Apache XMLRPC doesn't support transport of long values without extensions, 315 | // therefore we have to cast truncate them here. 316 | // Alternatively we could enable extensions by configuring enabledForExtensions = true, 317 | // but some RPC clients may not like it. 318 | if (value > Integer.MAX_VALUE) { 319 | continue; 320 | } 321 | valueData.put("value", (int)value); 322 | valueInfo.add(valueData); 323 | } 324 | enumInfo.put("members", valueInfo); 325 | enumInfos.add(enumInfo); 326 | } 327 | 328 | Map resp = new HashMap<>(); 329 | resp.put("enum_info", enumInfos); 330 | 331 | // cache it for next request 332 | this.server.plugin.enumCache = resp; 333 | return resp; 334 | } 335 | 336 | /// Get the filesystem path of the binary being decompiled. 337 | public String binary_path() { 338 | return this.server.plugin.getCurrentProgram().getExecutablePath(); 339 | } 340 | 341 | /// Get version information about the decompiler environment. 342 | public Map versions() { 343 | Map res = new HashMap<>(); 344 | res.put("name", "ghidra"); 345 | res.put("version", Application.getApplicationVersion().toString()); 346 | res.put("java", System.getProperty("java.version")); 347 | return res; 348 | } 349 | 350 | /// Navigate to this address in the GUI. 351 | public boolean focus_address(Integer addr) { 352 | return this.server.plugin.focus_address(addr); 353 | } 354 | 355 | public Map elf_info() { 356 | if(!this.server.plugin.elfInfoCache.isEmpty()) 357 | return this.server.plugin.elfInfoCache; 358 | 359 | Map elf_info = new HashMap<>(); 360 | 361 | var program = this.server.plugin.getCurrentProgram(); 362 | var provider = new MemoryByteProvider(program.getMemory(), program.getMinAddress()); 363 | ElfHeader header; 364 | try { 365 | header = new ElfHeader(provider, null); 366 | } 367 | catch (ElfException e) { 368 | elf_info.put("error", e.toString()); 369 | return elf_info; 370 | } 371 | 372 | elf_info.put("flags", "0x" + Integer.toHexString(header.e_flags())); 373 | elf_info.put("machine", (int) header.e_machine()); 374 | elf_info.put("is_big_endian", header.isBigEndian()); 375 | elf_info.put("is_32_bit", header.is32Bit()); 376 | elf_info.put("image_base", "0x" + Long.toHexString(program.getMinAddress().getOffset())); 377 | elf_info.put("name", program.getName()); 378 | elf_info.put("error", ""); 379 | 380 | this.server.plugin.elfInfoCache = elf_info; 381 | return elf_info; 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /decomp2dbg/clients/gdb/gdb_client.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import argparse 3 | import contextlib 4 | import io 5 | 6 | import gdb 7 | 8 | from ..client import DecompilerClient 9 | from ...utils import * 10 | from .utils import * 11 | from .symbol_mapper import SymbolMapper 12 | from .decompiler_pane import DecompilerPane 13 | 14 | # 15 | # Decompiler Client Interface 16 | # 17 | 18 | 19 | class GDBDecompilerClient(DecompilerClient): 20 | def __init__(self, gdb_client, name="decompiler", host="127.0.0.1", port=3662): 21 | super(GDBDecompilerClient, self).__init__(name=name, host=host, port=port) 22 | self.gdb_client: "GDBClient" = gdb_client 23 | self.symbol_mapper = SymbolMapper() 24 | self._is_pie = None 25 | self._lvar_bptr = None 26 | 27 | @property 28 | @lru_cache() 29 | def text_base_addr(self): 30 | return self.gdb_client.base_addr_start 31 | 32 | @property 33 | def is_pie(self): 34 | if self._is_pie is None: 35 | self._is_pie = self.gdb_client.is_pie 36 | 37 | return self._is_pie 38 | 39 | def rebase_addr(self, addr, up=False): 40 | corrected_addr = addr 41 | if self.is_pie or self.gdb_client.base_manually_set: 42 | if up: 43 | corrected_addr += self.text_base_addr 44 | else: 45 | corrected_addr -= self.text_base_addr 46 | 47 | return corrected_addr 48 | 49 | def decompiler_connected(self): 50 | self.gdb_client.on_decompiler_connected(self.name) 51 | 52 | def decompiler_disconnected(self): 53 | self.gdb_client.on_decompiler_disconnected(self.name) 54 | 55 | def update_symbols(self): 56 | self.symbol_mapper.text_base_addr = self.text_base_addr 57 | 58 | global_vars, func_headers = self.update_global_vars(), self.update_function_headers() 59 | syms_to_add = [] 60 | sym_name_set = set() 61 | global_var_size = 8 62 | 63 | if not self.native_sym_support: 64 | err("Native symbol support is required to run decomp2dbg, assure you have coreutils installed.") 65 | return False 66 | 67 | # add symbols with native support if possible 68 | for addr, func in func_headers.items(): 69 | syms_to_add.append((func["name"], int(addr, 0), "function", func["size"])) 70 | sym_name_set.add(func["name"]) 71 | 72 | for addr, global_var in global_vars.items(): 73 | clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", global_var['name']) 74 | # never re-add globals with the same name as a func 75 | if clean_name in sym_name_set: 76 | continue 77 | 78 | syms_to_add.append((clean_name, int(addr, 0), "object", global_var_size)) 79 | 80 | try: 81 | self.symbol_mapper.add_native_symbols(syms_to_add) 82 | except Exception as e: 83 | err(f"Failed to set symbols natively: {e}") 84 | self.native_sym_support = False 85 | return False 86 | 87 | return True 88 | 89 | def update_global_vars(self): 90 | return self.global_vars 91 | 92 | def update_function_headers(self): 93 | return self.function_headers 94 | 95 | def _clean_type_str(self, type_str): 96 | if "__" in type_str: 97 | type_str = type_str.replace("__", "") 98 | idx = type_str.find("[") 99 | if idx != -1: 100 | type_str = type_str[:idx] + "_t" + type_str[idx:] 101 | else: 102 | type_str += "_t" 103 | type_str = type_str.replace("unsigned ", "u") 104 | 105 | return type_str 106 | 107 | @lru_cache() 108 | def _ptr_size(self) -> int: 109 | return gdb.lookup_type("void").pointer().sizeof 110 | 111 | def _get_frame(self) -> int | None: 112 | # We want to extract the "frame at" thing. 113 | # pwndbg> info frame 114 | # Stack level 0, frame at 0x7fffffffe130: 115 | # rip = 0x555555555185 in main (main.c:7); saved rip = 0x7ffff7c2773b 116 | # called by frame at 0x7fffffffe1d0 117 | # source language c. 118 | # Arglist at 0x7fffffffe120, args: 119 | # Locals at 0x7fffffffe120, Previous frame's sp is 0x7fffffffe130 120 | # Saved registers: 121 | # rbp at 0x7fffffffe120, rip at 0x7fffffffe128 122 | try: 123 | frame_txt: str = gdb.execute("info frame", to_string=True) 124 | match = re.search(r"frame at (0x[0-9a-fA-F]+):", frame_txt) 125 | if match: 126 | frame_addr = int(match.group(1), 16) 127 | if frame_addr == 0: 128 | # Happens sometimes at the binary entrypoint 129 | return None 130 | # GDB for some reason returns one ptr past retaddr 131 | return frame_addr - self._ptr_size() 132 | return None 133 | except Exception: 134 | return None 135 | 136 | def update_function_data(self, addr): 137 | func_data = self.function_data(addr) 138 | reg_vars = func_data.get("reg_vars", []) 139 | stack_vars = func_data.get("stack_vars", []) 140 | 141 | for var in reg_vars: 142 | name = var["name"] 143 | type_str = self._clean_type_str(var['type']) 144 | reg_name = var['reg_name'] 145 | expr = f"""(({type_str}) (${reg_name}))""" 146 | 147 | try: 148 | gdb.execute(f"set ${name} = {expr}") 149 | type_unknown = False 150 | except Exception: 151 | type_unknown = True 152 | 153 | if type_unknown: 154 | try: 155 | gdb.execute(f"set ${name} = (${reg_name})") 156 | except Exception: 157 | continue 158 | 159 | for stack_var in stack_vars: 160 | var_name = stack_var['name'] 161 | type_str = self._clean_type_str(stack_var['type']) 162 | # Have to use .get() because of ghidra 163 | from_sp_str: None | str = stack_var.get("from_sp") 164 | from_frame_str: None | str = stack_var.get("from_frame") 165 | 166 | # We prefer from sp, because sp always exists while it may be 167 | # hard/unstable/impossible to find the frame 168 | # We don't account for architectures where the stack goes in the 169 | # different direction. 170 | if from_sp_str is not None: 171 | from_sp: int = int(from_sp_str, 0) 172 | try: 173 | gdb.execute(f"set ${var_name} = ({type_str}*) ($sp + {from_sp})") 174 | type_unknown = False 175 | except Exception: 176 | type_unknown = True 177 | 178 | if type_unknown: 179 | try: 180 | gdb.execute(f"set ${var_name} = ($sp + {from_sp})") 181 | except Exception: 182 | continue 183 | else: 184 | if from_frame_str is None: 185 | # Should never happen. 186 | continue 187 | 188 | from_frame: int = int(from_frame_str, 0) 189 | frame_addr: int | None = self._get_frame() 190 | if frame_addr is None: 191 | continue 192 | 193 | try: 194 | gdb.execute(f"set ${var_name} = ({type_str}*) ({frame_addr} - {from_frame})") 195 | type_unknown = False 196 | except Exception: 197 | type_unknown = True 198 | 199 | if type_unknown: 200 | try: 201 | gdb.execute(f"set ${var_name} = ({frame_addr} - {from_frame})") 202 | except Exception: 203 | continue 204 | 205 | 206 | # 207 | # Command Interface 208 | # 209 | 210 | class DecompilerCommand(gdb.Command): 211 | def __init__(self, decompiler, gdb_client): 212 | super(DecompilerCommand, self).__init__("decompiler", gdb.COMMAND_USER) 213 | self.decompiler = decompiler 214 | self.gdb_client = gdb_client 215 | self.arg_parser = self._init_arg_parser() 216 | 217 | @only_if_gdb_running 218 | def invoke(self, arg, from_tty): 219 | raw_args = arg.split() 220 | try: 221 | f = io.StringIO() 222 | with contextlib.redirect_stderr(f): 223 | args = self.arg_parser.parse_args(raw_args) 224 | except SystemExit: 225 | err("Missing or invalid arguments.") 226 | self.arg_parser.print_help() 227 | return 228 | except (RuntimeError, RuntimeWarning): 229 | return 230 | except Exception as e: 231 | err(f"Error parsing args: {e}") 232 | self.arg_parser.print_help() 233 | return 234 | 235 | self._handle_cmd(args) 236 | 237 | @staticmethod 238 | def _init_arg_parser(): 239 | parser = argparse.ArgumentParser() 240 | commands = ["connect", "disconnect", "info"] 241 | parser.add_argument( 242 | 'command', type=str, choices=commands, help=""" 243 | Commands: 244 | [connect]: connects a decompiler by name, with optional host, port, and base address. 245 | [disconnect]: disconnects a decompiler by name, destroyed decompilation panel. 246 | [info]: lists useful info about connected decompilers 247 | """ 248 | ) 249 | parser.add_argument( 250 | 'decompiler_name', type=str, nargs="?", help=""" 251 | The name of the decompiler, which can be anything you like. It's suggested 252 | to use sematic and numeric names like: 'ida2' or 'ghidra1'. Optional when doing 253 | the info command. 254 | """ 255 | ) 256 | parser.add_argument( 257 | '--host', type=str, default="localhost" 258 | ) 259 | parser.add_argument( 260 | '--port', type=int, default=3662 261 | ) 262 | parser.add_argument( 263 | '--base-addr-start', type=lambda x: int(x,0) 264 | ) 265 | parser.add_argument( 266 | '--base-addr-end', type=lambda x: int(x,0) 267 | ) 268 | 269 | return parser 270 | 271 | def _handle_cmd(self, args): 272 | cmd = args.command 273 | handler_str = f"_handle_{cmd}" 274 | handler = getattr(self, handler_str) 275 | handler(args) 276 | 277 | def _handle_connect(self, args): 278 | if not args.decompiler_name: 279 | err("You must provide a decompiler name when using this command!") 280 | return 281 | 282 | if (args.base_addr_start is not None and args.base_addr_end is None) or (args.base_addr_end is not None and args.base_addr_start is None): 283 | err("You must use --base-addr-start and --base-addr-end together") 284 | return 285 | elif args.base_addr_start is not None and args.base_addr_end is not None: 286 | if args.base_addr_start > args.base_addr_end: 287 | err("Your provided base-addr-start must be smaller than your base-addr-end") 288 | return 289 | 290 | self.gdb_client.base_addr_start = args.base_addr_start 291 | self.gdb_client.base_addr_end = args.base_addr_end 292 | self.gdb_client.base_manually_set = True 293 | 294 | self.gdb_client.name = args.decompiler_name 295 | connected = self.decompiler.connect(name=args.decompiler_name, host=args.host, port=args.port) 296 | if not connected: 297 | err("Failed to connect to decompiler!") 298 | return 299 | 300 | info("Connected to decompiler!") 301 | 302 | def _handle_disconnect(self, args): 303 | if not args.decompiler_name: 304 | err("You must provide a decompiler name when using this command!") 305 | return 306 | 307 | self.decompiler.disconnect() 308 | info("Disconnected decompiler!") 309 | 310 | def _handle_info(self, args): 311 | info("Decompiler Info:") 312 | print(textwrap.dedent( 313 | f"""\ 314 | Name: {self.gdb_client.name} 315 | Base Addr Start: {hex(self.gdb_client.base_addr_start) 316 | if isinstance(self.gdb_client.base_addr_start, int) else self.gdb_client.base_addr_start} 317 | Base Addr End: {hex(self.gdb_client.base_addr_end) 318 | if isinstance(self.gdb_client.base_addr_end, int) else self.gdb_client.base_addr_end} 319 | """ 320 | )) 321 | 322 | 323 | class GDBClient: 324 | def __init__(self): 325 | self.dec_client = GDBDecompilerClient(self) 326 | self.cmd_interface = DecompilerCommand(self.dec_client, self) 327 | self.dec_pane = DecompilerPane(self.dec_client) 328 | 329 | self.name = None 330 | self.base_manually_set = False 331 | self.base_addr_start = None 332 | self.base_addr_end = None 333 | 334 | def __del__(self): 335 | del self.cmd_interface 336 | 337 | def register_decompiler_context_pane(self, decompiler_name): 338 | gdb.events.stop.connect(self.dec_pane.display_pane_and_title) 339 | 340 | def deregister_decompiler_context_pane(self, decompiler_name): 341 | gdb.events.stop.disconnect(self.dec_pane.display_pane_and_title) 342 | 343 | def find_text_segment_base_addr(self, is_remote=False): 344 | return find_text_segment_base_addr(is_remote=is_remote) 345 | 346 | @property 347 | def is_pie(self): 348 | checksec_status = checksec(get_filepath()) 349 | return checksec_status["PIE"] # if pie we will have offset instead of abs address. 350 | 351 | # 352 | # Event Handlers 353 | # 354 | 355 | def on_decompiler_connected(self, decompiler_name): 356 | if self.base_addr_start is None: 357 | self.base_addr_start = self.find_text_segment_base_addr(is_remote=is_remote_debug()) 358 | self.dec_client.update_symbols() 359 | self.register_decompiler_context_pane(decompiler_name) 360 | 361 | def on_decompiler_disconnected(self, decompiler_name): 362 | self.deregister_decompiler_context_pane(decompiler_name) 363 | self.name = None 364 | self.base_addr_start = None 365 | self.base_addr_end = None 366 | -------------------------------------------------------------------------------- /decompilers/d2d_ghidra/src/main/java/decomp2dbg/D2DPlugin.java: -------------------------------------------------------------------------------- 1 | package decomp2dbg; 2 | 3 | import java.awt.Event; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import javax.swing.JOptionPane; 10 | import javax.swing.JTextField; 11 | import javax.swing.KeyStroke; 12 | 13 | import docking.ActionContext; 14 | import docking.action.DockingAction; 15 | import docking.action.KeyBindingData; 16 | import docking.action.MenuData; 17 | import ghidra.app.decompiler.DecompInterface; 18 | import ghidra.app.decompiler.DecompileOptions; 19 | import ghidra.app.decompiler.DecompileResults; 20 | import ghidra.app.plugin.PluginCategoryNames; 21 | import ghidra.app.plugin.ProgramPlugin; 22 | import ghidra.app.services.GoToService; 23 | import ghidra.framework.model.DomainObjectChangeRecord; 24 | import ghidra.framework.model.DomainObjectChangedEvent; 25 | import ghidra.framework.model.DomainObjectListener; 26 | import ghidra.framework.plugintool.PluginInfo; 27 | import ghidra.framework.plugintool.PluginTool; 28 | import ghidra.framework.plugintool.util.PluginStatus; 29 | import ghidra.program.database.symbol.CodeSymbol; 30 | import ghidra.program.database.symbol.FunctionSymbol; 31 | import ghidra.program.database.symbol.VariableSymbolDB; 32 | import ghidra.program.model.address.Address; 33 | import ghidra.program.model.listing.Function; 34 | import ghidra.program.model.listing.Program; 35 | import ghidra.program.model.pcode.HighSymbol; 36 | import ghidra.program.util.ProgramChangeRecord; 37 | import ghidra.program.util.ProgramEvent; 38 | import ghidra.program.util.ProgramLocation; 39 | import ghidra.util.Msg; 40 | import ghidra.util.task.ConsoleTaskMonitor; 41 | 42 | @PluginInfo( 43 | status = PluginStatus.RELEASED, 44 | packageName = D2DPluginPackage.NAME, 45 | category = PluginCategoryNames.DEBUGGER, 46 | shortDescription = "Syncronize symbols to a debugger", 47 | description = "This is the server for the decompiler side plugin of decomp2dbg " 48 | + "which connects the decompiler to a debugger and syncronizes symbols " 49 | + "and decompilation lines. See github.com/mahaloz/decomp2dbg for more info." 50 | ) 51 | public class D2DPlugin extends ProgramPlugin implements DomainObjectListener { 52 | private DockingAction configureD2DAction; 53 | private D2DGhidraServer server; 54 | private GoToService goToService; 55 | public Map decompileCache; 56 | public Map gVarCache; 57 | public Map funcSymCache; 58 | public Map funcDataCache; 59 | public Map structCache; 60 | public Map typeAliasCache; 61 | public Map unionCache; 62 | public Map enumCache; 63 | public Map elfInfoCache; 64 | 65 | public D2DPlugin(PluginTool tool) { 66 | super(tool); 67 | 68 | // Add a d2d button to 'Tools' in GUI menu 69 | configureD2DAction = this.createD2DMenuAction(); 70 | tool.addAction(configureD2DAction); 71 | goToService = tool.getService(GoToService.class); 72 | 73 | // cache maps 74 | decompileCache = new HashMap<>(); 75 | gVarCache = new HashMap<>(); 76 | funcSymCache = new HashMap<>(); 77 | funcDataCache = new HashMap<>(); 78 | structCache = new HashMap<>(); 79 | typeAliasCache = new HashMap<>(); 80 | unionCache = new HashMap<>(); 81 | enumCache = new HashMap<>(); 82 | elfInfoCache = new HashMap<>(); 83 | } 84 | 85 | @Override 86 | protected void programActivated(Program program) { 87 | program.addListener(this); 88 | } 89 | 90 | @Override 91 | protected void programDeactivated(Program program) { 92 | program.removeListener(this); 93 | } 94 | 95 | private DockingAction createD2DMenuAction() { 96 | D2DPlugin plugin = this; 97 | configureD2DAction = new DockingAction("decomp2dbg", getName()) { 98 | @Override 99 | public void actionPerformed(ActionContext context) { 100 | plugin.configureD2DServer(); 101 | } 102 | }; 103 | 104 | configureD2DAction.setEnabled(true); 105 | configureD2DAction.setMenuBarData(new MenuData(new String[] {"Tools", "Configure decomp2dbg..." })); 106 | configureD2DAction.setKeyBindingData(new KeyBindingData(KeyStroke.getKeyStroke('D', Event.CTRL_MASK + Event.SHIFT_MASK))); 107 | return configureD2DAction; 108 | } 109 | 110 | private void clearCache() { 111 | this.decompileCache.clear(); 112 | this.gVarCache.clear(); 113 | this.funcSymCache.clear(); 114 | this.funcDataCache.clear(); 115 | this.structCache.clear(); 116 | this.typeAliasCache.clear(); 117 | this.unionCache.clear(); 118 | this.enumCache.clear(); 119 | this.elfInfoCache.clear(); 120 | } 121 | 122 | private void configureD2DServer() { 123 | Msg.info(this, "Configuring decomp2dbg..."); 124 | JTextField hostField = new JTextField("localhost"); 125 | JTextField portField = new JTextField("3662"); 126 | Object[] message = { 127 | "Host: ", hostField, 128 | "Port: ", portField 129 | }; 130 | 131 | int option = JOptionPane.showConfirmDialog(null, message, "Login", JOptionPane.OK_CANCEL_OPTION); 132 | String host; 133 | int port; 134 | if (option == JOptionPane.OK_OPTION) { 135 | host = hostField.getText(); 136 | try { 137 | port = Integer.parseInt(portField.getText()); 138 | } catch(Exception e) { 139 | JOptionPane.showMessageDialog(null, "Unable to parse port: " + e.toString()); 140 | return; 141 | } 142 | } else 143 | return; 144 | 145 | this.server = new D2DGhidraServer(host, port, this); 146 | 147 | try { 148 | this.server.start_server(); 149 | } catch(Exception e) { 150 | JOptionPane.showMessageDialog(null, "Encountered error: " + e.toString()); 151 | return; 152 | } 153 | 154 | this.clearCache(); 155 | 156 | JOptionPane.showMessageDialog(null, "Sever configured and running!"); 157 | } 158 | 159 | /* 160 | * Decompiler Utils 161 | */ 162 | 163 | public Address strToAddr(String addrStr) { 164 | return this.getCurrentProgram().getAddressFactory().getAddress(addrStr); 165 | } 166 | 167 | public Address rebaseAddr(Integer addr, Boolean rebaseDown) { 168 | var program = this.getCurrentProgram(); 169 | var base = (int) program.getImageBase().getOffset(); 170 | Integer rebasedAddr = addr; 171 | if(rebaseDown) { 172 | rebasedAddr -= base; 173 | } 174 | else if(addr < base) { 175 | rebasedAddr += base; 176 | } 177 | 178 | return this.strToAddr(Integer.toHexString(rebasedAddr)); 179 | } 180 | 181 | public Function getNearestFunction(Address addr) { 182 | if(addr == null) { 183 | Msg.warn(this, "Failed to parse Addr string earlier, got null addr."); 184 | return null; 185 | } 186 | 187 | var program = this.getCurrentProgram(); 188 | var funcManager = program.getFunctionManager(); 189 | var func = funcManager.getFunctionContaining(addr); 190 | 191 | return func; 192 | } 193 | 194 | public DecompileResults decompileFunc(Function func) { 195 | var cacheRes = this.decompileCache.get(func.getEntryPoint().getOffset()); 196 | if(cacheRes != null) 197 | return (DecompileResults) cacheRes; 198 | 199 | DecompInterface ifc = new DecompInterface(); 200 | ifc.setOptions(new DecompileOptions()); 201 | ifc.openProgram(this.getCurrentProgram()); 202 | DecompileResults res = ifc.decompileFunction(func, 10, new ConsoleTaskMonitor()); 203 | 204 | // cache it! 205 | this.decompileCache.put(func.getEntryPoint().getOffset(), res); 206 | return res; 207 | } 208 | 209 | public Map getFuncData(Integer addr) { 210 | Map funcInfo = new HashMap<>(); 211 | funcInfo.put("stack_vars", new HashMap<>()); 212 | funcInfo.put("reg_vars", new HashMap<>()); 213 | var func = this.getNearestFunction(this.rebaseAddr(addr, false)); 214 | if(func == null) { 215 | Msg.warn(server, "Failed to find a function by the address " + addr); 216 | return funcInfo; 217 | } 218 | 219 | var dec = this.decompileFunc(func); 220 | if(dec == null) { 221 | Msg.warn(server, "Failed to decompile function by the address " + addr); 222 | return funcInfo; 223 | } 224 | 225 | ArrayList symbols = new ArrayList<>(); 226 | ArrayList regVars = new ArrayList<>(); 227 | ArrayList stackVars = new ArrayList<>(); 228 | dec.getHighFunction().getLocalSymbolMap().getSymbols().forEachRemaining(symbols::add); 229 | for (HighSymbol sym: symbols) { 230 | if(sym.getStorage().isStackStorage()) { 231 | Map sv = new HashMap<>(); 232 | sv.put("name", sym.getName()); 233 | sv.put("type", sym.getDataType().toString()); 234 | // https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/VariableStorage.html#getStackOffset() 235 | // Doesn't really specify offset from what, but testing shows it is from the bottom of the frame 236 | // (near saved RIP). The number is negative so we flip it. 237 | sv.put("from_frame", String.valueOf(-sym.getStorage().getStackOffset())); 238 | // FIXME: how to pass None for from_sp? 239 | stackVars.add(sv); 240 | } 241 | else if(sym.getStorage().isRegisterStorage()) { 242 | Map rv = new HashMap<>(); 243 | rv.put("name", sym.getName()); 244 | rv.put("type", sym.getDataType().toString()); 245 | rv.put("reg_name", sym.getStorage().getRegister().toString().toLowerCase()); 246 | regVars.add(rv); 247 | } 248 | } 249 | funcInfo.put("stack_vars", stackVars); 250 | funcInfo.put("reg_vars", regVars); 251 | 252 | return funcInfo; 253 | } 254 | 255 | public boolean focus_address(Integer addr) { 256 | if (this.goToService == null) { 257 | this.goToService = tool.getService(GoToService.class); 258 | if (this.goToService == null) { 259 | return false; 260 | } 261 | } 262 | 263 | var rebasedAddr = this.rebaseAddr(addr, false); 264 | 265 | // This works but is insanely slow. 266 | // // https://ghidra.re/ghidra_docs/api/ghidra/app/services/GoToService.html#goTo(ghidra.program.model.address.Address) 267 | // And this is the fastest overload of .goTo()! 268 | // https://github.com/NationalSecurityAgency/ghidra/blob/be482754a73ba9713d099565227b7f19d1f3e89c/Ghidra/Features/Base/src/main/java/ghidra/app/util/navigation/GoToServiceImpl.java#L52 269 | 270 | Program prog = this.getCurrentProgram(); 271 | ProgramLocation loc = new ProgramLocation(prog, rebasedAddr); 272 | return goToService.goTo(loc, prog); 273 | } 274 | 275 | /* 276 | * Change Event Handler 277 | */ 278 | 279 | @Override 280 | public void domainObjectChanged(DomainObjectChangedEvent ev) { 281 | // also look at: 282 | // https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java 283 | 284 | ArrayList funcEvents = new ArrayList<>(Arrays.asList( 285 | ProgramEvent.FUNCTION_CHANGED, 286 | ProgramEvent.FUNCTION_BODY_CHANGED, 287 | ProgramEvent.VARIABLE_REFERENCE_ADDED, 288 | ProgramEvent.VARIABLE_REFERENCE_REMOVED 289 | )); 290 | 291 | ArrayList symDelEvents = new ArrayList<>(Arrays.asList( 292 | ProgramEvent.SYMBOL_REMOVED 293 | )); 294 | 295 | ArrayList symChgEvents = new ArrayList<>(Arrays.asList( 296 | ProgramEvent.SYMBOL_ADDED, 297 | ProgramEvent.SYMBOL_RENAMED, 298 | ProgramEvent.SYMBOL_DATA_CHANGED 299 | )); 300 | 301 | ArrayList typeEvents = new ArrayList<>(Arrays.asList( 302 | ProgramEvent.DATA_TYPE_CHANGED, 303 | ProgramEvent.DATA_TYPE_REPLACED, 304 | ProgramEvent.DATA_TYPE_RENAMED, 305 | ProgramEvent.DATA_TYPE_SETTING_CHANGED, 306 | ProgramEvent.DATA_TYPE_MOVED, 307 | ProgramEvent.DATA_TYPE_ADDED 308 | )); 309 | 310 | for (DomainObjectChangeRecord record : ev) { 311 | // only analyze changes to the current program 312 | if( !(record instanceof ProgramChangeRecord) ) 313 | continue; 314 | 315 | ProgramEvent chgType = (ProgramEvent) record.getEventType(); 316 | var pcr = (ProgramChangeRecord) record; 317 | var obj = pcr.getObject(); 318 | var newVal = pcr.getNewValue(); 319 | 320 | /* 321 | * Function Updates 322 | */ 323 | if(funcEvents.contains(chgType)) { 324 | // use record.getSubEvent() when checking if a FUNCTION_CHANGED 325 | // since it will be triggered if the signature of the function changes 326 | var funcAddr = pcr.getStart().getOffset(); 327 | this.funcDataCache.put((int) funcAddr, null); 328 | this.decompileCache.put(funcAddr, null); 329 | } 330 | 331 | /* 332 | * Type updated or created 333 | */ 334 | else if (typeEvents.contains(chgType)) { 335 | // For now, just clear the cache. It'll get regenerated on next call. 336 | // TODO: More fine-grained handling 337 | this.structCache.clear(); 338 | this.typeAliasCache.clear(); 339 | this.unionCache.clear(); 340 | this.enumCache.clear(); 341 | } 342 | 343 | /* 344 | * Symbol Removed (global variable) 345 | */ 346 | else if (symDelEvents.contains(chgType)) { 347 | var addr = pcr.getStart().getOffset(); 348 | var rebasedAddrStr = "0x" + this.rebaseAddr((int) addr, true).toString(); 349 | this.gVarCache.remove(rebasedAddrStr); 350 | } 351 | 352 | /* 353 | * Symbol Updated or Created 354 | */ 355 | else if (symChgEvents.contains(chgType)) { 356 | if (obj == null && newVal != null) 357 | obj = newVal; 358 | 359 | /* 360 | * Stack Variable 361 | */ 362 | if (obj instanceof VariableSymbolDB) { 363 | continue; 364 | } 365 | 366 | /* 367 | * GlobalVar & Label 368 | */ 369 | else if(obj instanceof CodeSymbol codeSymbol) { 370 | if(this.gVarCache.isEmpty()) 371 | continue; 372 | 373 | var sym = codeSymbol; 374 | var newName = sym.getName(); 375 | var addr = sym.getAddress().getOffset(); 376 | 377 | Map varInfo = new HashMap<>(); 378 | varInfo.put("name", newName); 379 | var rebasedAddr = this.rebaseAddr((int) addr, true); 380 | this.gVarCache.put("0x" + rebasedAddr.toString(), varInfo); 381 | } 382 | 383 | /* 384 | * Function Name 385 | */ 386 | else if(obj instanceof FunctionSymbol functionSymbol) { 387 | if(this.funcSymCache.isEmpty()) 388 | continue; 389 | 390 | var sym = functionSymbol; 391 | var newName = sym.getName(); 392 | var addr = sym.getAddress().getOffset(); 393 | 394 | var func = this.getCurrentProgram().getFunctionManager().getFunction(addr); 395 | if(func == null) 396 | continue; 397 | 398 | Map funcInfo = new HashMap<>(); 399 | funcInfo.put("name", newName); 400 | funcInfo.put("size", (int) func.getBody().getNumAddresses()); 401 | var rebasedAddr = this.rebaseAddr((int) addr, true); 402 | this.funcSymCache.put("0x" + rebasedAddr.toString(), funcInfo); 403 | } 404 | else 405 | continue; 406 | 407 | this.decompileCache.clear(); 408 | } 409 | } 410 | } 411 | 412 | } 413 | --------------------------------------------------------------------------------