├── R4Ghidra ├── ghidra_scripts │ ├── README.txt │ └── r4ghidra_headless.py ├── Module.manifest ├── src │ └── main │ │ ├── resources │ │ └── images │ │ │ └── README.txt │ │ ├── java │ │ └── r4ghidra │ │ │ ├── repl │ │ │ ├── config │ │ │ │ ├── R2EvalChangeListener.java │ │ │ │ └── R2EvalConfig.java │ │ │ ├── num │ │ │ │ ├── R2NumCallback.java │ │ │ │ ├── R2NumException.java │ │ │ │ ├── R2MemoryReader.java │ │ │ │ ├── R2GhidraMemoryReader.java │ │ │ │ ├── R2NumUtil.java │ │ │ │ └── R2GhidraSymbolCallback.java │ │ │ ├── filesystem │ │ │ │ ├── R2FileSystemException.java │ │ │ │ ├── R2FileSystem.java │ │ │ │ └── R2SandboxedFileSystem.java │ │ │ ├── R2CommandHandler.java │ │ │ ├── R2CommandException.java │ │ │ ├── handlers │ │ │ │ ├── R2ClearCommandHandler.java │ │ │ │ ├── R2QuitCommandHandler.java │ │ │ │ ├── CommentTypeAdapter.java │ │ │ │ ├── R2BlocksizeCommandHandler.java │ │ │ │ ├── R2CommentCommandHandler.java │ │ │ │ ├── R2AnalyzeCommandHandler.java │ │ │ │ ├── R2EnvCommandHandler.java │ │ │ │ ├── R2JsCommandHandler.java │ │ │ │ ├── R2ShellCommandHandler.java │ │ │ │ ├── R2InfoCommandHandler.java │ │ │ │ ├── R2DecompileCommandHandler.java │ │ │ │ ├── R2EvalCommandHandler.java │ │ │ │ ├── R2SeekCommandHandler.java │ │ │ │ └── R2FlagCommandHandler.java │ │ │ ├── R4CommandInitializer.java │ │ │ ├── R4GhidraHttpHandler.java │ │ │ ├── R2Command.java │ │ │ └── R2OutputFilter.java │ │ │ ├── R4ProgramLocationListener.java │ │ │ ├── R4GhidraState.java │ │ │ ├── R4GhidraServer.java │ │ │ └── R4CommandShellProvider.java │ │ └── help │ │ └── help │ │ ├── topics │ │ ├── ghidrar2web │ │ │ └── help.html │ │ └── r4ghidra │ │ │ └── help.html │ │ └── TOC_Source.xml ├── .sdkmanrc ├── extension.properties ├── lib │ └── README.txt ├── os │ ├── linux_x86_64 │ │ └── README.txt │ ├── mac_x86_64 │ │ └── README.txt │ └── win_x86_64 │ │ └── README.txt ├── data │ └── README.txt ├── .idea │ └── runConfigurations │ │ └── RunGhidra.xml ├── Makefile └── build.gradle ├── doc └── images │ ├── r4ghidra.jpg │ ├── r4ghidra-logo2.jpg │ └── r4ghidra-logo2.png ├── .github ├── dependabot.yml └── workflows │ └── gradle.yml ├── Attic ├── ghidare2.rb ├── ghidra2radare.py ├── GhidraDecompiler.java └── GhidraDecompilerR2.java ├── Makefile ├── examples └── test.r2.js ├── r4g └── README.md /R4Ghidra/ghidra_scripts/README.txt: -------------------------------------------------------------------------------- 1 | Java source directory to hold module-specific Ghidra scripts. 2 | -------------------------------------------------------------------------------- /doc/images/r4ghidra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radareorg/r4ghidra/master/doc/images/r4ghidra.jpg -------------------------------------------------------------------------------- /doc/images/r4ghidra-logo2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radareorg/r4ghidra/master/doc/images/r4ghidra-logo2.jpg -------------------------------------------------------------------------------- /doc/images/r4ghidra-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radareorg/r4ghidra/master/doc/images/r4ghidra-logo2.png -------------------------------------------------------------------------------- /R4Ghidra/Module.manifest: -------------------------------------------------------------------------------- 1 | MODULE FILE LICENSE: See the LICENSE.txt file distributed with this project. 2 | MODULE NAME: R4Ghidra 3 | -------------------------------------------------------------------------------- /R4Ghidra/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 | -------------------------------------------------------------------------------- /R4Ghidra/.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config 2 | # Add key=value pairs of SDKs to use below 3 | java=21.0.1-amzn 4 | gradle=8.5 5 | -------------------------------------------------------------------------------- /R4Ghidra/extension.properties: -------------------------------------------------------------------------------- 1 | name=R4Ghidra 2 | description=Integration between Ghidra and Radare2 3 | author=Radare2 Team 4 | createdOn=2025-07-01 5 | version=@extversion@ 6 | -------------------------------------------------------------------------------- /R4Ghidra/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. -------------------------------------------------------------------------------- /R4Ghidra/os/linux_x86_64/README.txt: -------------------------------------------------------------------------------- 1 | The "os/linux_x86_64" directory is intended to hold Linux native binaries 2 | which this module is dependent upon. This directory may be eliminated for a specific 3 | module if native binaries are not provided for the corresponding platform. 4 | -------------------------------------------------------------------------------- /R4Ghidra/os/mac_x86_64/README.txt: -------------------------------------------------------------------------------- 1 | The "os/mac_x86_64" directory is intended to hold macOS (OS X) native binaries 2 | which this module is dependent upon. This directory may be eliminated for a specific 3 | module if native binaries are not provided for the corresponding platform. 4 | -------------------------------------------------------------------------------- /R4Ghidra/os/win_x86_64/README.txt: -------------------------------------------------------------------------------- 1 | The "os/win_x86_64" directory is intended to hold MS Windows native binaries (.exe) 2 | which this module is dependent upon. This directory may be eliminated for a specific 3 | module if native binaries are not provided for the corresponding platform. 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "##build " 9 | prefix-development: "##build " 10 | labels: 11 | - buildsystem 12 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/config/R2EvalChangeListener.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.config; 2 | 3 | /** Interface for listeners that respond to configuration variable changes */ 4 | public interface R2EvalChangeListener { 5 | 6 | /** 7 | * Called when a configuration variable changes 8 | * 9 | * @param key The variable name 10 | * @param oldValue The previous value 11 | * @param newValue The new value 12 | */ 13 | void onChange(String key, String oldValue, String newValue); 14 | } 15 | -------------------------------------------------------------------------------- /Attic/ghidare2.rb: -------------------------------------------------------------------------------- 1 | require 'r2pipe' 2 | require 'pry' 3 | require 'shellwords' 4 | require 'coderay' 5 | 6 | r2p = R2Pipe.new 7 | 8 | exec = r2p.cmdj('ij')['core']['file'] 9 | offset = r2p.cmdj('sj')[0]['offset'] 10 | 11 | cmd = "analyzeHeadless . Test.gpr -import #{Shellwords.shellescape exec} -postScript GhidraDecompiler.java #{offset.to_s 16} -deleteProject 2>/dev/null" 12 | 13 | `#{cmd}` 14 | 15 | `astyle ./decompiled.c` 16 | x = IO.read "./decompiled.c" 17 | puts CodeRay.scan(x, :c).term 18 | -------------------------------------------------------------------------------- /Attic/ghidra2radare.py: -------------------------------------------------------------------------------- 1 | # This script must be executed by Ghidra 2 | import sys 3 | 4 | sys.path.append('/Library/Python/2.7/site-packages/') 5 | 6 | import r2pipe 7 | 8 | r2 = r2pipe.open("http://localhost:9090") 9 | f = getFirstFunction() 10 | while f is not None: 11 | _ = r2.cmd("f ghidra." + f.getName() + " = 0x" + str(f.getEntryPoint())) 12 | f = getFunctionAfter(f) 13 | 14 | d = getFirstData() 15 | while d is not None: 16 | _ = r2.cmd("CC " + str(d) + " @ 0x" + str(d.getAddress())) 17 | d = getDataAfter(d) 18 | 19 | r2.quit() 20 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/num/R2NumCallback.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.num; 2 | 3 | /** 4 | * Callback interface for resolving symbol names in R2Num expressions. 5 | * 6 | *

This interface allows external components to provide values for symbolic names used in radare2 7 | * numeric expressions, such as function names, variables, etc. 8 | */ 9 | public interface R2NumCallback { 10 | /** 11 | * Resolve a symbol name to its numeric value 12 | * 13 | * @param name The symbol name to resolve 14 | * @return The numeric value of the symbol, or null if the symbol cannot be resolved 15 | */ 16 | Long resolveSymbol(String name); 17 | } 18 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/num/R2NumException.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.num; 2 | 3 | /** Exception thrown during RNum expression evaluation. */ 4 | public class R2NumException extends Exception { 5 | private static final long serialVersionUID = 1L; 6 | 7 | /** 8 | * Create a new RNum exception with a message 9 | * 10 | * @param message The exception message 11 | */ 12 | public R2NumException(String message) { 13 | super(message); 14 | } 15 | 16 | /** 17 | * Create a new RNum exception with a message and cause 18 | * 19 | * @param message The exception message 20 | * @param cause The cause of the exception 21 | */ 22 | public R2NumException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/R4ProgramLocationListener.java: -------------------------------------------------------------------------------- 1 | package r4ghidra; 2 | 3 | import docking.widgets.EventTrigger; 4 | import ghidra.app.util.viewer.listingpanel.ProgramLocationListener; 5 | import ghidra.program.util.ProgramLocation; 6 | import r4ghidra.repl.R2Context; 7 | 8 | public class R4ProgramLocationListener implements ProgramLocationListener { 9 | R2Context context; 10 | 11 | public R4ProgramLocationListener(R2Context context) { 12 | this.context = context; 13 | } 14 | 15 | @Override 16 | public void programLocationChanged(ProgramLocation programLocation, EventTrigger eventTrigger) { 17 | if (context.getEvalConfig().getBool("r4g.location.follow", true)) { 18 | context.setCurrentAddress(programLocation.getAddress()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/num/R2MemoryReader.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.num; 2 | 3 | /** 4 | * Interface for reading memory values in R2Num expressions. 5 | * 6 | *

This interface allows external components to provide memory access functionality for bracketed 7 | * expressions like [addr:size]. 8 | */ 9 | public interface R2MemoryReader { 10 | /** 11 | * Read a value from memory at the specified address with the given size 12 | * 13 | * @param address The memory address to read from 14 | * @param size The size of the memory read in bytes 15 | * @param littleEndian Whether to use little endian byte order 16 | * @return The value read from memory 17 | * @throws Exception If the memory access fails 18 | */ 19 | long readMemory(long address, int size, boolean littleEndian) throws Exception; 20 | } 21 | -------------------------------------------------------------------------------- /R4Ghidra/data/README.txt: -------------------------------------------------------------------------------- 1 | The "data" directory is intended to hold data files that will be used by this module and will 2 | not end up in the .jar file, but will be present in the zip or tar file. Typically, data 3 | files are placed here rather than in the resources directory if the user may need to edit them. 4 | 5 | An optional data/languages directory can exist for the purpose of containing various Sleigh language 6 | specification files and importer opinion files. 7 | 8 | The data/buildLanguage.xml is used for building the contents of the data/languages directory. 9 | 10 | The skel language definition has been commented-out within the skel.ldefs file so that the 11 | skeleton language does not show-up within Ghidra. 12 | 13 | See the Sleigh language documentation (docs/languages/index.html) for details Sleigh language 14 | specification syntax. 15 | -------------------------------------------------------------------------------- /R4Ghidra/ghidra_scripts/r4ghidra_headless.py: -------------------------------------------------------------------------------- 1 | from r4ghidra import R4GhidraServer, R4GhidraState 2 | 3 | from ghidra.program.flatapi import FlatProgramAPI 4 | 5 | import os 6 | import time 7 | 8 | R4GhidraState.api = FlatProgramAPI(currentProgram) 9 | R4GhidraState.r2Seek = R4GhidraState.api.toAddr(0) 10 | 11 | port=9191 12 | if "R4GHIDRA_PORT" in os.environ: 13 | port=int(os.environ["R4GHIDRA_PORT"]) 14 | elif "R2WEB_PORT" in os.environ: # Keep old variable for backwards compatibility 15 | port=int(os.environ["R2WEB_PORT"]) 16 | 17 | print("R4Ghidra Starting server on port %d" % (port)) 18 | R4GhidraServer.start(port) 19 | 20 | # TODO We'll need a HTTP server like Jetty to properly wait() for server stop 21 | while True: 22 | user_input=raw_input("R4Ghidra E(x)it? ") 23 | if user_input == 'x': 24 | R4GhidraServer.stop() 25 | break 26 | 27 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/filesystem/R2FileSystemException.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.filesystem; 2 | 3 | /** 4 | * Exception thrown when a file operation is not allowed by sandbox settings or when there is a 5 | * problem with in-memory file operations. 6 | */ 7 | public class R2FileSystemException extends Exception { 8 | 9 | private static final long serialVersionUID = 1L; 10 | 11 | /** 12 | * Create a new R2FileSystemException with a message 13 | * 14 | * @param message The error message 15 | */ 16 | public R2FileSystemException(String message) { 17 | super(message); 18 | } 19 | 20 | /** 21 | * Create a new R2FileSystemException with a message and cause 22 | * 23 | * @param message The error message 24 | * @param cause The cause of the exception 25 | */ 26 | public R2FileSystemException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/R2CommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl; 2 | 3 | /** 4 | * Interface for all r2 command handlers 5 | * 6 | *

Command handlers are responsible for executing a specific command or family of commands. Each 7 | * handler should implement this interface and be registered with the R2REPLImpl. 8 | */ 9 | public interface R2CommandHandler { 10 | 11 | /** 12 | * Execute a command 13 | * 14 | * @param command The parsed command object 15 | * @param context The execution context 16 | * @return The result of the command execution 17 | * @throws R2CommandException If there's an error during command execution 18 | */ 19 | String execute(R2Command command, R2Context context) throws R2CommandException; 20 | 21 | /** 22 | * Get help information for this command 23 | * 24 | * @return A string containing help information for this command 25 | */ 26 | String getHelp(); 27 | } 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FCNADDR=1000011e8 2 | TESTBIN=$(shell pwd)/test/ls 3 | SCRIPT=R4GhidraServer.java 4 | 5 | all: 6 | $(MAKE) -C R4Ghidra 7 | 8 | oops: 9 | analyzeHeadless . Test.gpr -import $(TESTBIN) -postScript $(SCRIPT) $(FCNADDR) -deleteProject 10 | r2 -caf -i ghidra-output.r2 $(TESTBIN) 11 | 12 | clean mrproper: 13 | $(MAKE) -C R4Ghidra $@ 14 | 15 | R2PM_BINDIR=$(shell r2pm -H R2PM_BINDIR) 16 | 17 | install: 18 | ln -fs $(shell pwd)/r4g $(R2PM_BINDIR)/r4g 19 | mkdir -p ~/ghidra_scripts 20 | ln -fs $(shell pwd)/$(SCRIPT) ~/ghidra_scripts/$(SCRIPT) 21 | $(MAKE) -C R4Ghidra install 22 | 23 | uninstall: 24 | rm -f $(R2PM_BINDIR)/r4g 25 | rm -f $(R2PM_BINDIR)/r2g 26 | $(MAKE) -C R4Ghidra uninstall 27 | 28 | GJF_VERSION=1.28.0 29 | GJF=google-java-format-$(GJF_VERSION)-all-deps.jar 30 | 31 | gjf $(GJF): 32 | wget https://github.com/google/google-java-format/releases/download/v$(GJF_VERSION)/$(GJF) 33 | 34 | indent: $(GJF) 35 | java -jar $(GJF) -i *.java */*.java \ 36 | R4Ghidra/src/main/java/**/*.java 37 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/R4GhidraState.java: -------------------------------------------------------------------------------- 1 | package r4ghidra; 2 | 3 | import ghidra.app.services.CodeViewerService; 4 | import ghidra.program.flatapi.FlatProgramAPI; 5 | import ghidra.program.model.address.Address; 6 | import ghidra.program.util.ProgramLocation; 7 | 8 | /** 9 | * Shared state for R4Ghidra 10 | * 11 | *

This class provides static variables to maintain global state across the R4Ghidra plugin. 12 | * Note: This is suitable for a proof-of-concept implementation. For a more robust solution, 13 | * consider adding proper validation, thread safety, and encapsulation. 14 | */ 15 | public class R4GhidraState { 16 | /** Reference to the Ghidra program API */ 17 | public static FlatProgramAPI api = null; 18 | 19 | public static CodeViewerService codeViewer = null; 20 | 21 | public static void goToLocation(Address a) { 22 | if (R4GhidraState.codeViewer != null) { 23 | R4GhidraState.codeViewer.goTo( 24 | new ProgramLocation(R4GhidraState.api.getCurrentProgram(), a), false); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/help/help/topics/ghidrar2web/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | Skeleton Help File for a Module 13 | 14 | 15 | 16 | 17 |

Skeleton Help File for a Module

18 | 19 |

This is a simple skeleton help topic. For a better description of what should and should not 20 | go in here, see the "sample" Ghidra extension in the Extensions/Ghidra directory, or see your 21 | favorite help topic. In general, language modules do not have their own help topics.

22 | 23 | 24 | -------------------------------------------------------------------------------- /R4Ghidra/.idea/runConfigurations/RunGhidra.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /examples/test.r2.js: -------------------------------------------------------------------------------- 1 | // Example R2Ghidra JavaScript file 2 | // This script demonstrates the capabilities of the 'js' command and r2pipe interface 3 | 4 | // Get current address 5 | var addr = r2.cmd('s').trim(); 6 | console.log('Current address: ' + addr); 7 | 8 | // Seek to 0 9 | r2.cmd('s 0'); 10 | console.log('Moved to address 0x0'); 11 | 12 | // Get program info 13 | var info = r2.cmdj('ij'); 14 | if (info) { 15 | console.log('Binary info:'); 16 | console.log('- Format: ' + info.core.format); 17 | console.log('- Bits: ' + info.core.bits); 18 | console.log('- Architecture: ' + info.core.arch); 19 | } 20 | 21 | // List a few functions 22 | console.log('\nFunctions:'); 23 | var funcs = r2.cmdj('aflj'); 24 | if (funcs && funcs.length > 0) { 25 | // Just show the first 5 functions 26 | var count = Math.min(5, funcs.length); 27 | for (var i = 0; i < count; i++) { 28 | console.log(funcs[i].name + ' @ ' + funcs[i].offset); 29 | } 30 | if (funcs.length > count) { 31 | console.log('... and ' + (funcs.length - count) + ' more'); 32 | } 33 | } 34 | 35 | // Return to the original address 36 | r2.cmd('s ' + addr); 37 | 38 | // Show the result from a custom command 39 | console.log('\nCustom command result:'); 40 | console.log(r2.cmd('?e Hello from JavaScript!')); 41 | 42 | console.log('\nScript execution completed successfully!'); -------------------------------------------------------------------------------- /R4Ghidra/src/main/help/help/topics/r4ghidra/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | R4Ghidra - Radare2/Ghidra Integration 13 | 14 | 15 | 16 | 17 |

R4Ghidra - Radare2/Ghidra Integration

18 | 19 |

R4Ghidra provides integration between Ghidra and Radare2. It allows you to connect Radare2 20 | to a running Ghidra instance, leveraging the power of both tools together. This plugin was 21 | originally known as ghidra-r2web, and has been rebranded and improved as R4Ghidra.

22 | 23 |

Usage

24 | 25 |

The plugin registers a new menu item under the Tools menu of Ghidra's Code Browser to 26 | start/stop the embedded web server. From there you can use r2's connection syntax to connect 27 | to Ghidra from Radare2.

28 | 29 |

Headless Mode

30 | 31 |

For headless usage, see the README.md file in the plugin directory.

32 | 33 | 34 | -------------------------------------------------------------------------------- /R4Ghidra/Makefile: -------------------------------------------------------------------------------- 1 | ifneq ($(shell test -d /snap/ghidra/current && echo snap),) 2 | GHIDRA_PATH=/snap/ghidra/current 3 | else 4 | ifneq ($(shell test -f /var/lib/flatpak/app/org.ghidra_sre.Ghidra/current/active/files/lib/ghidra && flatpak),) 5 | GHIDRA_PATH=/var/lib/flatpak/app/org.ghidra_sre.Ghidra/current/active/files/lib/ghidra 6 | else 7 | GHIDRA_PATH=$(HOME)/Downloads 8 | endif 9 | endif 10 | 11 | GHIDRA_HOME=$(shell cd $(GHIDRA_PATH) ; ls -rt 2> /dev/null | grep ^ghidra_ |grep -v zip | tail -n 1) 12 | GHIDRA_INSTALL_DIR?=$(HOME)/Downloads/$(GHIDRA_HOME) 13 | 14 | ifeq ($(GHIDRA_INSTALL_DIR),) 15 | all: 16 | @echo Cannot find Ghidra in $(GHIDRA_PATH) or GHIDRA_INSTALL_DIR 17 | 18 | else 19 | GHIDRA_INSTALL_DIR?=$(HOME)/Downloads/$(GHIDRA_HOME) 20 | all: 21 | GHIDRA_INSTALL_DIR="$(GHIDRA_INSTALL_DIR)" gradle buildExtension 22 | endif 23 | 24 | javadoc doc: 25 | GHIDRA_INSTALL_DIR="$(GHIDRA_INSTALL_DIR)" gradle javadoc 26 | 27 | install: uninstall 28 | cp -f dist/"$(shell ls -rt dist |grep zip | tail -n1)" $(GHIDRA_INSTALL_DIR)/Extensions/Ghidra 29 | 30 | uninstall: 31 | rm -f "$(GHIDRA_INSTALL_DIR)/Extensions/Ghidra/ghidra"_*R4G* 32 | rm -rf $(HOME)/.config/ghidra/ghidra_*/Extensions/R4Ghidra 33 | 34 | # XXX not working 35 | headless: 36 | $(GHIDRA_PATH)/support/analyzeHeadless /tmp/ test -process /bin/ls -postScript ghidra_scripts/r4ghidra_headless.py 37 | 38 | run: 39 | $(GHIDRA_INSTALL_DIR)/ghidraRun 40 | 41 | clean: 42 | rm -rf .gradle dist build 43 | 44 | indent: 45 | $(MAKE) -C .. indent 46 | # GHIDRA_INSTALL_DIR="$(GHIDRA_INSTALL_DIR)" gradle spotlessApply 47 | 48 | mrproper: clean 49 | rm -rf "$(HOME)/.gradle" 50 | 51 | .PHONY: all clean mrproper indent headless 52 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/R2CommandException.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl; 2 | 3 | /** Exception thrown during command parsing or execution */ 4 | public class R2CommandException extends Exception { 5 | 6 | private static final long serialVersionUID = 1L; 7 | 8 | private int errorCode; 9 | 10 | /** 11 | * Create a new exception with the given message 12 | * 13 | * @param message The error message 14 | */ 15 | public R2CommandException(String message) { 16 | this(1, message); 17 | } 18 | 19 | /** 20 | * Create a new exception with the given error code and message 21 | * 22 | * @param errorCode The error code 23 | * @param message The error message 24 | */ 25 | public R2CommandException(int errorCode, String message) { 26 | super(message); 27 | this.errorCode = errorCode; 28 | } 29 | 30 | /** 31 | * Create a new exception with the given message and cause 32 | * 33 | * @param message The error message 34 | * @param cause The cause of the exception 35 | */ 36 | public R2CommandException(String message, Throwable cause) { 37 | this(1, message, cause); 38 | } 39 | 40 | /** 41 | * Create a new exception with the given error code, message, and cause 42 | * 43 | * @param errorCode The error code 44 | * @param message The error message 45 | * @param cause The cause of the exception 46 | */ 47 | public R2CommandException(int errorCode, String message, Throwable cause) { 48 | super(message, cause); 49 | this.errorCode = errorCode; 50 | } 51 | 52 | /** 53 | * Get the error code 54 | * 55 | * @return The error code 56 | */ 57 | public int getErrorCode() { 58 | return errorCode; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2ClearCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import r4ghidra.R4CommandShellProvider; 4 | import r4ghidra.repl.R2Command; 5 | import r4ghidra.repl.R2CommandException; 6 | import r4ghidra.repl.R2CommandHandler; 7 | import r4ghidra.repl.R2Context; 8 | 9 | /** 10 | * Handler for the 'clear' command 11 | * 12 | *

This command clears the output textarea in the R4Ghidra console shell. 13 | */ 14 | /** 15 | * Handler for the 'clear' command 16 | *

17 | * This command clears the output textarea in the R4Ghidra console shell. 18 | * It provides a way for users to clean the interface during debugging sessions. 19 | */ 20 | public class R2ClearCommandHandler implements R2CommandHandler { 21 | 22 | @Override 23 | public String execute(R2Command command, R2Context context) throws R2CommandException { 24 | // Check if it's a 'clear' command (prefix would be 'c') 25 | if (!command.getPrefix().equals("c") || !command.getSubcommand().equals("lear")) { 26 | throw new R2CommandException("Not a clear command"); 27 | } 28 | 29 | // Get the shell provider from the context 30 | R4CommandShellProvider shellProvider = context.getShellProvider(); 31 | if (shellProvider == null) { 32 | return "Error: Shell provider not available"; 33 | } 34 | 35 | // Clear the output area 36 | shellProvider.clearOutputArea(); 37 | 38 | // Return an empty string since the output will be cleared anyway 39 | return ""; 40 | } 41 | 42 | @Override 43 | public String getHelp() { 44 | StringBuilder help = new StringBuilder(); 45 | help.append("Usage: clear - Clear the console output\n\n"); 46 | help.append("clear Clear the console output area\n"); 47 | help.append("\nExamples:\n"); 48 | help.append("clear Clear all text from the console\n"); 49 | return help.toString(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/num/R2GhidraMemoryReader.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.num; 2 | 3 | import ghidra.program.flatapi.FlatProgramAPI; 4 | import ghidra.program.model.address.Address; 5 | import java.nio.ByteBuffer; 6 | import java.nio.ByteOrder; 7 | import r4ghidra.repl.R2Context; 8 | 9 | /** 10 | * Ghidra implementation of R2MemoryReader interface. 11 | * 12 | *

This class uses the Ghidra API to read memory for bracket expressions in R2Num evaluations. 13 | */ 14 | public class R2GhidraMemoryReader implements R2MemoryReader { 15 | 16 | private R2Context context; 17 | 18 | /** 19 | * Create a new Ghidra memory reader with the specified context 20 | * 21 | * @param context The R2Context to use for memory access 22 | */ 23 | public R2GhidraMemoryReader(R2Context context) { 24 | this.context = context; 25 | } 26 | 27 | /** Read memory value using Ghidra API */ 28 | @Override 29 | public long readMemory(long address, int size, boolean littleEndian) throws Exception { 30 | FlatProgramAPI api = context.getAPI(); 31 | if (api == null) { 32 | throw new Exception("FlatProgramAPI not available in context"); 33 | } 34 | 35 | // Convert the address to a Ghidra Address 36 | Address addr = api.toAddr(address); 37 | 38 | // Read the bytes from memory 39 | byte[] bytes = api.getBytes(addr, size); 40 | // Convert bytes to a long value based on size and endianness 41 | ByteBuffer buffer = ByteBuffer.wrap(bytes); 42 | buffer.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); 43 | 44 | switch (size) { 45 | case 1: 46 | return buffer.get() & 0xFFL; 47 | case 2: 48 | return buffer.getShort() & 0xFFFFL; 49 | case 4: 50 | return buffer.getInt() & 0xFFFFFFFFL; 51 | case 8: 52 | return buffer.getLong(); 53 | default: 54 | throw new Exception("Invalid memory read size: " + size); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/R4GhidraServer.java: -------------------------------------------------------------------------------- 1 | package r4ghidra; 2 | 3 | import com.sun.net.httpserver.HttpExchange; 4 | import com.sun.net.httpserver.HttpHandler; 5 | import com.sun.net.httpserver.HttpServer; 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.net.InetSocketAddress; 9 | import r4ghidra.repl.R4GhidraHttpHandler; 10 | 11 | /** 12 | * HTTP server for R4Ghidra 13 | * 14 | *

Provides an HTTP interface to R4Ghidra commands, allowing external tools like radare2 to 15 | * interact with Ghidra via a web API. 16 | */ 17 | public class R4GhidraServer { 18 | static HttpServer server; 19 | 20 | /** 21 | * Check if the web server is currently running. 22 | * 23 | * @return true if running, false otherwise 24 | */ 25 | public static boolean isRunning() { 26 | return server != null; 27 | } 28 | 29 | static class MyRootHandler implements HttpHandler { 30 | public void handle(HttpExchange t) throws IOException { 31 | 32 | byte[] response = "Hola".getBytes(); 33 | t.sendResponseHeaders(200, response.length); 34 | OutputStream os = t.getResponseBody(); 35 | os.write(response); 36 | os.close(); 37 | } 38 | } 39 | 40 | /** 41 | * Start the HTTP server on the specified port 42 | * 43 | * @param port The port number to listen on 44 | * @throws IOException If an error occurs while starting the server 45 | */ 46 | public static void start(int port) throws IOException { 47 | stop(); 48 | server = HttpServer.create(new InetSocketAddress(port), 0); 49 | server.createContext("/", new MyRootHandler()); 50 | server.createContext("/cmd", new R4GhidraHttpHandler()); 51 | server.setExecutor(null); // creates a default executor 52 | server.start(); 53 | } 54 | 55 | /** Stop the HTTP server if it's running */ 56 | public static void stop() { 57 | if (server != null) { 58 | server.stop(0); 59 | server = null; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/num/R2NumUtil.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.num; 2 | 3 | import ghidra.program.model.address.Address; 4 | import r4ghidra.repl.R2Context; 5 | 6 | /** 7 | * Utility class for working with R2Num expressions. 8 | * 9 | *

This class provides easy access to the R2Num functionality with convenient factory methods and 10 | * helpers. 11 | */ 12 | public class R2NumUtil { 13 | 14 | /** 15 | * Create a fully configured R2Num instance for the given context. 16 | * 17 | * @param context The R2 context to use 18 | * @return A configured R2Num instance ready to use 19 | */ 20 | public static R2Num createR2Num(R2Context context) { 21 | // Create the base RNum 22 | R2Num num = new R2Num(context); 23 | 24 | // Configure with symbol resolver 25 | num.setCallback(new R2GhidraSymbolCallback(context)); 26 | 27 | // Configure with memory reader 28 | num.setMemoryReader(new R2GhidraMemoryReader(context)); 29 | 30 | return num; 31 | } 32 | 33 | /** 34 | * Evaluate a numeric expression with the given context. 35 | * 36 | * @param context The R2 context to use 37 | * @param expr The expression to evaluate 38 | * @return The computed value 39 | * @throws R2NumException If evaluation fails 40 | */ 41 | public static long evaluateExpression(R2Context context, String expr) throws R2NumException { 42 | return createR2Num(context).getValue(expr); 43 | } 44 | 45 | /** 46 | * Evaluate a numeric expression and convert the result to an Address. 47 | * 48 | * @param context The R2 context to use 49 | * @param expr The expression to evaluate 50 | * @return The computed address 51 | * @throws R2NumException If evaluation fails 52 | */ 53 | public static Address evaluateAddress(R2Context context, String expr) throws R2NumException { 54 | long value = evaluateExpression(context, expr); 55 | return context.getAPI().toAddr(value); 56 | } 57 | 58 | /** 59 | * Format a numeric value as a hex string. 60 | * 61 | * @param value The value to format 62 | * @return The formatted hex string (with 0x prefix) 63 | */ 64 | public static String formatHex(long value) { 65 | return "0x" + Long.toHexString(value); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2QuitCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import r4ghidra.repl.R2Command; 4 | import r4ghidra.repl.R2CommandException; 5 | import r4ghidra.repl.R2CommandHandler; 6 | import r4ghidra.repl.R2Context; 7 | import r4ghidra.repl.num.R2NumException; 8 | import r4ghidra.repl.num.R2NumUtil; 9 | 10 | /** 11 | * Handler for the 'q' (quit) command 12 | * 13 | *

This command allows quitting the application with an optional exit code. 14 | */ 15 | public class R2QuitCommandHandler implements R2CommandHandler { 16 | 17 | @Override 18 | public String execute(R2Command command, R2Context context) throws R2CommandException { 19 | // Check if it's a 'q' command 20 | if (!command.hasPrefix("q")) { 21 | throw new R2CommandException("Not a quit command"); 22 | } 23 | 24 | // Special handling for q!! syntax 25 | if (command.getSubcommand().equals("!!")) { 26 | // Exit immediately with code 0 27 | System.exit(0); 28 | return ""; // This won't be reached 29 | } 30 | 31 | // Check if there is an argument to use as exit code 32 | if (command.getArgumentCount() > 0) { 33 | try { 34 | // Use R2NumUtil to evaluate the exit code expression 35 | String exitCodeExpr = command.getFirstArgument("0"); 36 | int exitCode = (int) R2NumUtil.evaluateExpression(context, exitCodeExpr); 37 | System.exit(exitCode); 38 | return ""; // This won't be reached 39 | } catch (R2NumException e) { 40 | throw new R2CommandException("Invalid exit code: " + e.getMessage()); 41 | } 42 | } 43 | 44 | // Default behavior is to show warning message 45 | return "Use q!! to force quit"; 46 | } 47 | 48 | @Override 49 | public String getHelp() { 50 | StringBuilder help = new StringBuilder(); 51 | help.append("Usage: q[!!] [exit_code] - Quit the application\n\n"); 52 | help.append("q Display quit message\n"); 53 | help.append("q!! Quit immediately with exit code 0\n"); 54 | help.append("q [n] Quit with specified exit code\n"); 55 | help.append("\nExamples:\n"); 56 | help.append("q Show the quit message\n"); 57 | help.append("q!! Force quit with exit code 0\n"); 58 | help.append("q 1 Exit with code 1\n"); 59 | help.append("q 0x20 Exit with code 32\n"); 60 | return help.toString(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/num/R2GhidraSymbolCallback.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.num; 2 | 3 | import ghidra.program.model.address.Address; 4 | import ghidra.program.model.listing.Function; 5 | import ghidra.program.model.symbol.Symbol; 6 | import ghidra.program.model.symbol.SymbolTable; 7 | import java.util.List; 8 | import r4ghidra.repl.R2Context; 9 | 10 | /** 11 | * Ghidra implementation of R2NumCallback interface for symbol resolution. 12 | * 13 | *

This class uses the Ghidra API to resolve symbol names to their addresses for use in R2Num 14 | * expressions. 15 | */ 16 | public class R2GhidraSymbolCallback implements R2NumCallback { 17 | private R2Context context; 18 | 19 | /** 20 | * Create a new Ghidra symbol resolver with the specified context 21 | * 22 | * @param context The R2Context to use for symbol resolution 23 | */ 24 | public R2GhidraSymbolCallback(R2Context context) { 25 | this.context = context; 26 | } 27 | 28 | /** Resolve a symbol name to its address value using Ghidra API */ 29 | @Override 30 | public Long resolveSymbol(String name) { 31 | if (context.getAPI() == null) { 32 | return null; 33 | } 34 | 35 | // Check if the name is a variable defined in the R2Context 36 | if (context.hasVariable(name)) { 37 | String value = context.getVariable(name); 38 | try { 39 | // Try to parse the variable as a number 40 | if (value.toLowerCase().startsWith("0x")) { 41 | return Long.parseLong(value.substring(2), 16); 42 | } else { 43 | return Long.parseLong(value); 44 | } 45 | } catch (NumberFormatException e) { 46 | // Not a number, try to resolve as a symbol recursively 47 | return resolveSymbol(value); 48 | } 49 | } 50 | 51 | try { 52 | // Try to resolve as a function name 53 | List functions = context.getAPI().getGlobalFunctions(name); 54 | if (!functions.isEmpty()) { 55 | return functions.get(0).getEntryPoint().getUnsignedOffset(); 56 | } 57 | 58 | // Try to resolve as a symbol 59 | SymbolTable symbolTable = context.getAPI().getCurrentProgram().getSymbolTable(); 60 | java.util.ArrayList symbols = new java.util.ArrayList<>(); 61 | 62 | // Convert SymbolIterator to List 63 | symbolTable.getSymbols(name).forEach(symbols::add); 64 | 65 | if (!symbols.isEmpty()) { 66 | // Return the first matching symbol's address 67 | Address symbolAddr = symbols.get(0).getAddress(); 68 | return symbolAddr.getUnsignedOffset(); 69 | } 70 | 71 | // If it starts with 0x, try to parse as a hex number 72 | if (name.toLowerCase().startsWith("0x")) { 73 | return Long.parseLong(name.substring(2), 16); 74 | } 75 | 76 | // Not found 77 | return null; 78 | 79 | } catch (Exception e) { 80 | // Error during resolution 81 | return null; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/CommentTypeAdapter.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | /** 4 | * Adapter class to handle different versions of CommentType between Ghidra versions. This addresses 5 | * compatibility issues between Ghidra 11.3 and 11.4 where CommentType may have changed from a class 6 | * with constants to an enum. 7 | */ 8 | public class CommentTypeAdapter { 9 | // Comment type constants 10 | /** 11 | * End-of-line comment type (value 0) 12 | */ 13 | public static final int EOL = 0; 14 | /** 15 | * Pre comment type (value 1) 16 | */ 17 | public static final int PRE = 1; 18 | /** 19 | * Post comment type (value 2) 20 | */ 21 | public static final int POST = 2; 22 | /** 23 | * Plate comment type (value 3) 24 | */ 25 | public static final int PLATE = 3; 26 | /** 27 | * Repeatable comment type (value 4) 28 | */ 29 | public static final int REPEATABLE = 4; 30 | 31 | // Cached CommentType object for EOL comments 32 | private static Object eolCommentType = null; 33 | 34 | /** 35 | * Get the appropriate CommentType object/enum for the current Ghidra version 36 | * 37 | * @param commentTypeValue The integer value representing the comment type 38 | * @return The appropriate CommentType object for the current Ghidra version 39 | */ 40 | public static Object getCommentType(int commentTypeValue) { 41 | // For EOL comments, use cached value if available 42 | if (commentTypeValue == EOL && eolCommentType != null) { 43 | return eolCommentType; 44 | } 45 | 46 | Object result = null; 47 | 48 | try { 49 | // First try the enum approach (Ghidra 11.4+) 50 | Class commentTypeClass = Class.forName("ghidra.program.model.listing.CommentType"); 51 | Object[] enumConstants = commentTypeClass.getEnumConstants(); 52 | 53 | if (enumConstants != null && commentTypeValue < enumConstants.length) { 54 | // CommentType is an enum in this version 55 | result = enumConstants[commentTypeValue]; 56 | } else { 57 | // Fall back to constants approach (Ghidra 11.3 and earlier) 58 | switch (commentTypeValue) { 59 | case EOL: 60 | result = commentTypeClass.getField("EOL").get(null); 61 | break; 62 | case PRE: 63 | result = commentTypeClass.getField("PRE").get(null); 64 | break; 65 | case POST: 66 | result = commentTypeClass.getField("POST").get(null); 67 | break; 68 | case PLATE: 69 | result = commentTypeClass.getField("PLATE").get(null); 70 | break; 71 | case REPEATABLE: 72 | result = commentTypeClass.getField("REPEATABLE").get(null); 73 | break; 74 | } 75 | } 76 | } catch (Exception e) { 77 | // If all attempts fail, return null 78 | return null; 79 | } 80 | 81 | // Cache EOL comment type for future use 82 | if (commentTypeValue == EOL) { 83 | eolCommentType = result; 84 | } 85 | 86 | return result; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /r4g: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | HOST=localhost 4 | PORT=9191 5 | 6 | ROOT=$(dirname `readlink $0` 2> /dev/null) 7 | if [ -n "${ROOT}" ]; then 8 | cd "${ROOT}" 9 | fi 10 | r4cmd() { 11 | ARG=`jq -nr --arg x "$@" '$x|@uri'` 12 | curl "http://${HOST}:${PORT}/cmd/$ARG" 13 | rc=$? 14 | echo 15 | exit $rc 16 | } 17 | 18 | if [ -z "$R2PIPE_IN" ]; then 19 | if [ "$1" = cmd ]; then 20 | shift 21 | r4cmd "$@" 22 | fi 23 | echo "# You can run commands in the R4Ghidra webserver like this" 24 | echo 25 | echo "r4g cmd px" 26 | echo 27 | echo "# This program is experimental and requires r2pipe to run" 28 | echo 29 | echo "In the radare2 shell type: r2 -c '#!pipe r4g'" 30 | echo 31 | echo "# Alternatively you can connect with:" 32 | echo 33 | echo "r2 -C http://${HOST}:${PORT}/cmd/" 34 | echo "r2 -c '=+http://${HOST}:${PORT}/cmd/' --" 35 | exit 1 36 | fi 37 | if [ -z "$1" ]; then 38 | echo "Usage: r4g [command]" 39 | echo "Commands:" 40 | echo " cmd [r4cmd] # run r4ghidra commands with curl" 41 | echo " r2 .!r4g r2 # import r4ghidra into cmd.pdc" 42 | echo ' dec [addr] !r4g dec `?v $$` # decompile function at given address' 43 | echo " pull # pull changes from ghidra into r2" 44 | echo " push # push comments and functions names from r2 to ghidra" 45 | echo " client [addr] # call the server to request the decompilation" 46 | echo " server [file]" 47 | # exit 1 48 | fi 49 | 50 | TESTBIN=${R2_FILE} 51 | FCNADDR=${R2_XOFFSET} 52 | 53 | TESTBIN=/bin/ls 54 | case "$1" in 55 | r2) 56 | #echo '"(pdcg,!r4g client `?v $FB`>.a,. .a,rm .a)"' 57 | #echo 'e cmd.pdc=.(pdcg)' 58 | #echo 'e cmd.pdc=$ghidra-dec' 59 | echo '"$pddg*=#!pipe r4g pdd*"' 60 | echo '"$pddg=#!pipe r4g pdd"' 61 | echo 'e cmd.pdd=pddg' 62 | ;; 63 | headless) 64 | echo "Headless r4ghidra is WIP" 65 | TESTBIN="$2" 66 | if [ -z "${TESTBIN}" ]; then 67 | echo "Usage: r4g server /path/to/file" 68 | else 69 | rm -rf Test.* 70 | analyzeHeadless . Test.gpr -import ${TESTBIN} \ 71 | -postScript GhidraDecompilerR2.java -deleteProject 72 | fi 73 | ;; 74 | dec) 75 | TESTBIN=/bin/ls 76 | FCNADDR=$2 77 | echo "FCNADDR=$2" 78 | echo 79 | rm -f decompiled.c 80 | rm -rf Test.* 81 | analyzeHeadless . Test.gpr -import ${TESTBIN} -postScript GhidraDecompiler.java ${FCNADDR} -deleteProject > /dev/null 2>&1 82 | indent decompiled.c 83 | cat decompiled.c 84 | ;; 85 | import|pull) 86 | TESTBIN=/bin/ls 87 | FCNADDR=$2 88 | echo "Assuming you have r2 http server on port 9090" 89 | echo "r2 -e http.port=9191 -c'& =h' /bin/ls" 90 | rm -rf Test.* 91 | analyzeHeadless . Test.gpr -import ${TESTBIN} -postScript ghidra2radare.py -deleteProject > /dev/null 2>&1 92 | cat ghidra-output.r2 93 | ;; 94 | cmd|*) 95 | r4cmd "$@" 96 | ;; 97 | esac 98 | 99 | #analyzeHeadless . Test.gpr -import $(TESTBIN) -postScript ghidra/GhidraDecompiler.java $(FCNADDR) -deleteProject 100 | exit 0 101 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/help/help/TOC_Source.xml: -------------------------------------------------------------------------------- 1 | 2 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/filesystem/R2FileSystem.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.filesystem; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | /** 7 | * Filesystem abstraction for the R2 REPL 8 | * 9 | *

This interface provides methods for interacting with files, with support for sandboxed access 10 | * and in-memory files. 11 | */ 12 | public interface R2FileSystem { 13 | 14 | /** 15 | * Read the contents of a file 16 | * 17 | * @param path The path to the file to read 18 | * @return The contents of the file as a string 19 | * @throws IOException If the file cannot be read 20 | * @throws R2FileSystemException If the operation is not allowed by sandbox settings 21 | */ 22 | String readFile(String path) throws IOException, R2FileSystemException; 23 | 24 | /** 25 | * Write to a file, overwriting any existing content 26 | * 27 | * @param path The path to the file to write 28 | * @param content The content to write to the file 29 | * @throws IOException If the file cannot be written 30 | * @throws R2FileSystemException If the operation is not allowed by sandbox settings 31 | */ 32 | void writeFile(String path, String content) throws IOException, R2FileSystemException; 33 | 34 | /** 35 | * Append to a file 36 | * 37 | * @param path The path to the file to append to 38 | * @param content The content to append to the file 39 | * @throws IOException If the file cannot be appended to 40 | * @throws R2FileSystemException If the operation is not allowed by sandbox settings 41 | */ 42 | void appendFile(String path, String content) throws IOException, R2FileSystemException; 43 | 44 | /** 45 | * Delete a file 46 | * 47 | * @param path The path to the file to delete 48 | * @throws IOException If the file cannot be deleted 49 | * @throws R2FileSystemException If the operation is not allowed by sandbox settings 50 | */ 51 | void deleteFile(String path) throws IOException, R2FileSystemException; 52 | 53 | /** 54 | * Check if a file exists 55 | * 56 | * @param path The path to the file to check 57 | * @return true if the file exists, false otherwise 58 | */ 59 | boolean fileExists(String path); 60 | 61 | /** 62 | * List all files in a directory 63 | * 64 | * @param path The path to the directory to list 65 | * @return A list of file paths in the directory 66 | * @throws IOException If the directory cannot be read 67 | * @throws R2FileSystemException If the operation is not allowed by sandbox settings 68 | */ 69 | List listFiles(String path) throws IOException, R2FileSystemException; 70 | 71 | /** 72 | * List all in-memory files 73 | * 74 | * @return A list of all in-memory file names (without the $ prefix) 75 | */ 76 | List listMemoryFiles(); 77 | 78 | /** 79 | * Check if a path is an in-memory file (starts with $) 80 | * 81 | * @param path The path to check 82 | * @return true if the path is an in-memory file, false otherwise 83 | */ 84 | boolean isMemoryFile(String path); 85 | 86 | /** 87 | * Get the name of an in-memory file without the $ prefix 88 | * 89 | * @param path The path to the in-memory file 90 | * @return The name of the file without the $ prefix 91 | */ 92 | String getMemoryFileName(String path); 93 | } 94 | -------------------------------------------------------------------------------- /R4Ghidra/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 | plugins { 33 | // Add Spotless plugin for code formatting 34 | id 'com.diffplug.spotless' version '6.22.0' 35 | } 36 | 37 | //----------------------START "DO NOT MODIFY" SECTION------------------------------ 38 | def ghidraInstallDir 39 | 40 | if (System.env.GHIDRA_INSTALL_DIR) { 41 | ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR 42 | } 43 | else if (project.hasProperty("GHIDRA_INSTALL_DIR")) { 44 | ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR") 45 | } 46 | else { 47 | ghidraInstallDir = "" 48 | } 49 | 50 | task distributeExtension { 51 | group "Ghidra" 52 | 53 | apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle" 54 | dependsOn ':buildExtension' 55 | } 56 | //----------------------END "DO NOT MODIFY" SECTION------------------------------- 57 | 58 | repositories { 59 | // Declare dependency repositories here. This is not needed if dependencies are manually 60 | // dropped into the lib/ directory. 61 | // See https://docs.gradle.org/current/userguide/declaring_repositories.html for more info. 62 | mavenCentral() 63 | } 64 | 65 | dependencies { 66 | // Any external dependencies added here will automatically be copied to the lib/ directory when 67 | // this extension is built. 68 | implementation 'org.json:json:20240205' 69 | implementation 'org.openjdk.nashorn:nashorn-core:15.4' 70 | } 71 | 72 | // Configuration for Spotless code formatting 73 | spotless { 74 | java { 75 | // Use Google's Java formatter 76 | googleJavaFormat("1.7") 77 | // Format all Java files in the project 78 | target 'src/main/java/**/*.java' 79 | // Use tabs for indentation 80 | indentWithTabs() 81 | } 82 | } 83 | 84 | // Exclude additional files from the built extension 85 | // Ex: buildExtension.exclude '.idea/**' 86 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/R4CommandInitializer.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl; 2 | 3 | import r4ghidra.repl.handlers.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class R4CommandInitializer { 9 | 10 | private static List commandHandlers=null; 11 | 12 | 13 | /** Initialize all command handlers for R4Ghidra */ 14 | public static List getCommandHandlers() { 15 | if (commandHandlers != null){ 16 | return commandHandlers; 17 | } 18 | commandHandlers=new ArrayList(); 19 | // Register all command handlers 20 | commandHandlers.add(new R2SeekCommandHandler()); 21 | commandHandlers.add(new R2PrintCommandHandler()); 22 | // Register the print command handler again with 'x' prefix as an alias for 'px' 23 | commandHandlers.add( 24 | new R2PrintCommandHandler() { 25 | @Override 26 | public String execute(r4ghidra.repl.R2Command command, r4ghidra.repl.R2Context context) 27 | throws r4ghidra.repl.R2CommandException { 28 | // Modify the command to prefix with 'p' to make it look like 'px' 29 | r4ghidra.repl.R2Command modifiedCommand = 30 | new r4ghidra.repl.R2Command( 31 | "p", // Change prefix to 'p' 32 | "x" + command.getSubcommand(), // Prefix subcommand with 'x' 33 | command.getArguments(), // Keep original arguments 34 | command.getTemporaryAddress() // Keep original temporary address 35 | ); 36 | // Execute the modified command through the regular handler 37 | return super.execute(modifiedCommand, context); 38 | } 39 | 40 | @Override 41 | public String getHelp() { 42 | // Return a modified help string that includes the 'x' command 43 | StringBuilder help = new StringBuilder(); 44 | help.append("Usage: x[j] [count]\n"); 45 | help.append(" x [len] print hexdump (alias for px)\n"); 46 | help.append(" xj [len] print hexdump as json (alias for pxj)\n"); 47 | help.append("\nExamples:\n"); 48 | help.append(" x print hexdump using default block size\n"); 49 | help.append(" x 32 print 32 bytes hexdump\n"); 50 | help.append(" xj 16 print 16 bytes hexdump as json\n"); 51 | return help.toString(); 52 | } 53 | }); 54 | commandHandlers.add(new R2BlocksizeCommandHandler()); 55 | // commandHandlers.add(new R2DecompileCommandHandler()); 56 | commandHandlers.add(new R2EnvCommandHandler()); 57 | commandHandlers.add(new R2EvalCommandHandler()); 58 | commandHandlers.add(new R2ShellCommandHandler()); 59 | // Analyze commands: af, afl, afi 60 | commandHandlers.add(new R2AnalyzeCommandHandler()); 61 | commandHandlers.add(new R2InfoCommandHandler()); 62 | commandHandlers.add(new R2CommentCommandHandler()); 63 | commandHandlers.add(new R2FlagCommandHandler()); 64 | commandHandlers.add(new R2QuitCommandHandler()); 65 | commandHandlers.add(new R2ClearCommandHandler()); 66 | 67 | return commandHandlers; 68 | // Note: R2HelpCommandHandler will be created in the CommandShellProvider 69 | // because it needs a reference to the command registry 70 | 71 | // Add more handlers as needed 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2BlocksizeCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | import r4ghidra.repl.R2Command; 6 | import r4ghidra.repl.R2CommandException; 7 | import r4ghidra.repl.R2CommandHandler; 8 | import r4ghidra.repl.R2Context; 9 | import r4ghidra.repl.num.R2NumException; 10 | import r4ghidra.repl.num.R2NumUtil; 11 | 12 | /** 13 | * Handler for the 'b' (blocksize) command 14 | * 15 | *

This command gets and sets the blocksize, which is the default number of bytes used by 16 | * commands that operate on memory when no size is explicitly specified. 17 | */ 18 | /** 19 | * Handler for the 'b' (blocksize) command 20 | *

21 | * This command gets and sets the blocksize, which is the default number of bytes used by 22 | * commands that operate on memory when no size is explicitly specified. The blocksize 23 | * can be increased, decreased, or set to a specific value. 24 | */ 25 | public class R2BlocksizeCommandHandler implements R2CommandHandler { 26 | 27 | @Override 28 | public String execute(R2Command command, R2Context context) throws R2CommandException { 29 | // Check if it's a 'b' command 30 | if (!command.hasPrefix("b")) { 31 | throw new R2CommandException("Not a blocksize command"); 32 | } 33 | 34 | // Get the subcommand 35 | String subcommand = command.getSubcommand(); 36 | 37 | // If no arguments provided, just return the current blocksize 38 | if (subcommand.isEmpty() && command.getArguments().isEmpty()) { 39 | return Integer.toString(context.getBlockSize()) + "\n"; 40 | } 41 | 42 | // Check for b+N or b-N syntax 43 | Pattern increasePattern = Pattern.compile("^\\+(.+)$"); 44 | Pattern decreasePattern = Pattern.compile("^-(.+)$"); 45 | 46 | Matcher increaseMatcher = increasePattern.matcher(subcommand); 47 | Matcher decreaseMatcher = decreasePattern.matcher(subcommand); 48 | 49 | try { 50 | int newBlockSize; 51 | 52 | if (increaseMatcher.matches()) { 53 | // Increase blocksize by N 54 | String valueStr = increaseMatcher.group(1); 55 | long value = R2NumUtil.evaluateExpression(context, valueStr); 56 | newBlockSize = context.getBlockSize() + (int) value; 57 | } else if (decreaseMatcher.matches()) { 58 | // Decrease blocksize by N 59 | String valueStr = decreaseMatcher.group(1); 60 | long value = R2NumUtil.evaluateExpression(context, valueStr); 61 | newBlockSize = context.getBlockSize() - (int) value; 62 | } else { 63 | // Handle direct blocksize setting 64 | String sizeArg = command.getFirstArgument(subcommand); 65 | 66 | if (sizeArg.isEmpty()) { 67 | // Return current blocksize if no argument provided 68 | return Integer.toString(context.getBlockSize()) + "\n"; 69 | } 70 | 71 | // Parse the new blocksize 72 | long value = R2NumUtil.evaluateExpression(context, sizeArg); 73 | newBlockSize = (int) value; 74 | } 75 | 76 | // Ensure blocksize is at least 1 77 | if (newBlockSize < 1) { 78 | newBlockSize = 1; 79 | } 80 | 81 | // Set the new blocksize 82 | context.setBlockSize(newBlockSize); 83 | 84 | // Return the new blocksize 85 | return Integer.toString(newBlockSize) + "\n"; 86 | 87 | } catch (R2NumException e) { 88 | throw new R2CommandException("Invalid blocksize value: " + e.getMessage()); 89 | } 90 | } 91 | 92 | @Override 93 | public String getHelp() { 94 | StringBuilder help = new StringBuilder(); 95 | help.append("Usage: b[+-]\n"); 96 | help.append(" b display current block size\n"); 97 | help.append(" b change block size to bytes\n"); 98 | help.append(" b+ increase blocksize by bytes\n"); 99 | help.append(" b- decrease blocksize by bytes\n"); 100 | help.append("\nExamples:\n"); 101 | help.append(" b show current block size\n"); 102 | help.append(" b 16 set block size to 16\n"); 103 | help.append(" b 0x100 set block size to 256\n"); 104 | help.append(" b+32 increase block size by 32\n"); 105 | help.append(" b-16 decrease block size by 16\n"); 106 | return help.toString(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Build Extension 9 | 10 | on: 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | workflow_dispatch: 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: write 22 | strategy: 23 | max-parallel: 3 24 | matrix: 25 | ghidra: 26 | - "11.4.2" 27 | - "11.4.1" 28 | - "11.4" 29 | - "11.3.2" 30 | - "11.3.1" 31 | - "11.3" 32 | - "11.2.1" 33 | - "11.2" 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v5 37 | 38 | - name: Setup JDK 39 | uses: actions/setup-java@v5 40 | with: 41 | java-version: '21' 42 | distribution: 'temurin' 43 | 44 | - name: Setup Ghidra 45 | uses: antoniovazquezblanco/setup-ghidra@v2.0.12 46 | with: 47 | version: ${{ matrix.ghidra }} 48 | 49 | # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. 50 | # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md 51 | - name: Setup Gradle 52 | uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 53 | with: 54 | gradle-version: '8.5' 55 | 56 | - name: Build with Gradle Wrapper 57 | run: gradle buildExtension 58 | working-directory: ./R4Ghidra 59 | 60 | - name: Upload artifacts 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: R4Ghidra_Ghidra_${{ matrix.ghidra }} 64 | path: R4Ghidra/dist/*.zip 65 | 66 | documentation: 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v5 71 | 72 | - name: Setup JDK 73 | uses: actions/setup-java@v5 74 | with: 75 | java-version: '21' 76 | distribution: 'temurin' 77 | 78 | - name: Setup Ghidra (latest version only) 79 | uses: antoniovazquezblanco/setup-ghidra@v2.0.12 80 | with: 81 | version: "11.4" 82 | 83 | - name: Setup Gradle 84 | uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 85 | with: 86 | gradle-version: '8.5' 87 | 88 | - name: Generate Javadoc 89 | run: gradle javadoc 90 | working-directory: ./R4Ghidra 91 | 92 | - name: Create documentation archive 93 | run: | 94 | mkdir -p docs_archive 95 | cp -r build/docs/javadoc docs_archive/ 96 | cd docs_archive 97 | zip -r ../R4Ghidra-javadoc.zip . 98 | working-directory: ./R4Ghidra 99 | 100 | - name: Upload documentation 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: R4Ghidra-javadoc 104 | path: R4Ghidra/R4Ghidra-javadoc.zip 105 | 106 | code-style: 107 | runs-on: ubuntu-latest 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v5 111 | 112 | - name: Setup JDK 113 | uses: actions/setup-java@v5 114 | with: 115 | java-version: '21' 116 | distribution: 'temurin' 117 | 118 | - name: Run code style check 119 | run: make indent && git diff --exit-code 120 | 121 | release: 122 | runs-on: "ubuntu-latest" 123 | needs: [build, documentation, code-style] 124 | 125 | steps: 126 | - name: Download binaries 127 | uses: actions/download-artifact@v5 128 | 129 | - name: Release 130 | uses: softprops/action-gh-release@v2 131 | if: startsWith(github.ref, 'refs/tags/') 132 | with: 133 | files: | 134 | R4Ghidra_Ghidra_*/*.zip 135 | R4Ghidra-javadoc/*.zip 136 | env: 137 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 138 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/R4GhidraHttpHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl; 2 | 3 | import com.sun.net.httpserver.Headers; 4 | import com.sun.net.httpserver.HttpExchange; 5 | import com.sun.net.httpserver.HttpHandler; 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.nio.ByteBuffer; 9 | import java.nio.CharBuffer; 10 | import java.nio.charset.CharsetDecoder; 11 | import java.nio.charset.MalformedInputException; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.Map; 14 | 15 | /** HTTP handler that processes radare2 commands using the new REPL implementation */ 16 | public class R4GhidraHttpHandler implements HttpHandler { 17 | 18 | private R2REPLImpl repl; 19 | private Map commandRegistry; 20 | 21 | /** Create a new handler */ 22 | /** 23 | * Create a new HTTP handler for R4Ghidra 24 | * 25 | * @param plugin The R4Ghidra plugin instance that provides command handlers 26 | */ 27 | /*public R4GhidraHttpHandler(R4GhidraPlugin plugin) { 28 | commandRegistry = new HashMap<>(); 29 | 30 | repl = new R2REPLImpl(); 31 | repl.registerCommands (plugin.getCommandHandlers()); 32 | }*/ 33 | 34 | public R4GhidraHttpHandler(){ 35 | 36 | repl = new R2REPLImpl(); 37 | repl.registerCommands (R4CommandInitializer.getCommandHandlers()); 38 | } 39 | 40 | @Override 41 | public void handle(HttpExchange exchange) throws IOException { 42 | // Support POST requests (body contains the command) and fallback to GET/query/path 43 | String method = exchange.getRequestMethod(); 44 | String cmd = null; 45 | 46 | if ("POST".equalsIgnoreCase(method)) { 47 | // Read the entire request body as the command (assume UTF-8) 48 | java.io.InputStream is = exchange.getRequestBody(); 49 | try { 50 | byte[] body = is.readAllBytes(); 51 | CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder(); 52 | CharBuffer decodedCharBuffer = charsetDecoder.decode(ByteBuffer.wrap(body)); 53 | cmd = decodedCharBuffer.toString().trim(); 54 | } catch(MalformedInputException mie){ 55 | sendErrorResponse(400, exchange, "Invalid UTF-8 encoding!".getBytes()); 56 | return; 57 | }finally { 58 | is.close(); 59 | } 60 | } else if ("GET".equalsIgnoreCase(method)){ 61 | // Extract command from query string or path (existing behavior) 62 | cmd = exchange.getRequestURI().getQuery(); 63 | if (cmd == null) { 64 | String path = exchange.getRequestURI().getPath(); 65 | if (path.length() > 5) { 66 | cmd = path.substring(5); 67 | } else { 68 | cmd = ""; 69 | } 70 | } 71 | } else { 72 | sendErrorResponse(400, exchange, "Invalid request".getBytes()); 73 | return; 74 | } 75 | 76 | if (cmd == null || cmd.isEmpty()) { 77 | sendErrorResponse(400, exchange, "Empty request".getBytes()); 78 | return; 79 | } 80 | 81 | try { 82 | // Execute the command using our REPL implementation 83 | String result = repl.executeCommand(cmd); 84 | sendResponse(exchange, result.getBytes()); 85 | } catch (Exception e) { 86 | // Handle any unexpected exceptions 87 | sendErrorResponse(500, exchange, ("Error executing command: " + e.getMessage()).getBytes()); 88 | } 89 | } 90 | 91 | private void setResponseHeaders(HttpExchange exchange){ 92 | Headers headers=exchange.getResponseHeaders(); 93 | headers.add("Accept-Charset","UTF-8"); 94 | headers.add("Accept-Encoding","identity"); 95 | } 96 | /** Send a successful response */ 97 | private void sendResponse(HttpExchange exchange, byte[] response) throws IOException { 98 | setResponseHeaders(exchange); 99 | exchange.sendResponseHeaders(200, response.length); 100 | OutputStream os = exchange.getResponseBody(); 101 | os.write(response); 102 | os.close(); 103 | } 104 | 105 | /** Send an error response */ 106 | private void sendErrorResponse(int status, HttpExchange exchange, byte[] response) 107 | throws IOException { 108 | setResponseHeaders(exchange); 109 | exchange.sendResponseHeaders(status, response.length); 110 | OutputStream os = exchange.getResponseBody(); 111 | os.write(response); 112 | os.close(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2CommentCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import ghidra.program.model.address.Address; 4 | // import ghidra.program.model.listing.CommentType; // Replaced with CommentTypeAdapter 5 | //import ghidra.program.model.listing.CommentType; 6 | import ghidra.program.model.listing.Listing; 7 | import ghidra.program.model.listing.Program; 8 | import java.util.Base64; 9 | import r4ghidra.repl.R2Command; 10 | import r4ghidra.repl.R2CommandException; 11 | import r4ghidra.repl.R2CommandHandler; 12 | import r4ghidra.repl.R2Context; 13 | 14 | /** 15 | * Handler for the 'CC' (comment) command family 16 | *

17 | * Provides functionality to set and manipulate comments at specific addresses in the program. 18 | * Currently supports the CCu (unique comment) subcommand, which sets a comment at the current address 19 | * or at a specified address and can handle base64-encoded comments. 20 | */ 21 | public class R2CommentCommandHandler implements R2CommandHandler { 22 | 23 | @Override 24 | public String execute(R2Command command, R2Context context) throws R2CommandException { 25 | // Check if it's a CC command 26 | if (!command.hasPrefix("C")) { 27 | throw new R2CommandException("Not a comment command"); 28 | } 29 | 30 | // Get the subcommand without suffix 31 | String subcommand = command.getSubcommandWithoutSuffix(); 32 | 33 | // Handle different subcommands 34 | switch (subcommand) { 35 | case "Cu": 36 | return executeCCuCommand(command, context); 37 | default: 38 | throw new R2CommandException("Unknown comment subcommand: C" + subcommand); 39 | } 40 | } 41 | 42 | /** 43 | * Execute the CCu command to set a unique comment at the current address Format: CCu [comment] @ 44 | * addr Special format: CCu base64:[encoded] @ addr - decodes base64 content first 45 | */ 46 | private String executeCCuCommand(R2Command command, R2Context context) throws R2CommandException { 47 | // Get the current address (or temporary address if specified) 48 | Address address = 49 | command.hasTemporaryAddress() ? command.getTemporaryAddress() : context.getCurrentAddress(); 50 | 51 | if (address == null) { 52 | throw new R2CommandException("Current address is not set"); 53 | } 54 | 55 | // Check if we have a comment text 56 | if (command.getArgumentCount() < 1) { 57 | throw new R2CommandException("No comment text provided. Usage: CCu [comment] @ addr"); 58 | } 59 | 60 | // Get the comment text (combine all arguments) 61 | StringBuilder commentText = new StringBuilder(); 62 | for (int i = 0; i < command.getArgumentCount(); i++) { 63 | if (i > 0) { 64 | commentText.append(" "); 65 | } 66 | commentText.append(command.getArgument(i, "")); 67 | } 68 | 69 | String comment = commentText.toString(); 70 | 71 | // Check for base64: prefix 72 | if (comment.startsWith("base64:")) { 73 | try { 74 | String base64Content = comment.substring("base64:".length()); 75 | byte[] decodedBytes = Base64.getDecoder().decode(base64Content); 76 | comment = new String(decodedBytes); 77 | } catch (IllegalArgumentException e) { 78 | throw new R2CommandException("Invalid base64 content: " + e.getMessage()); 79 | } 80 | } 81 | 82 | // Get the current program 83 | Program program = context.getAPI().getCurrentProgram(); 84 | if (program == null) { 85 | throw new R2CommandException("No program is open"); 86 | } 87 | 88 | // Get the listing and start a transaction 89 | Listing listing = program.getListing(); 90 | int transactionID = program.startTransaction("Set Comment"); 91 | 92 | // Set the EOL (End of Line) comment at the specified address 93 | // Unique comment means removing any existing comments first 94 | listing.setComment(address, 0, null); // Clear existing comment 95 | listing.setComment(address, 0, comment); 96 | 97 | program.endTransaction(transactionID, true); 98 | return context.formatAddress(address); 99 | } 100 | 101 | @Override 102 | public String getHelp() { 103 | StringBuilder help = new StringBuilder(); 104 | help.append("Usage: C[command][j]\n"); 105 | help.append(" CCu [comment] @ addr add a unique comment at given address\n"); 106 | help.append(" CCu base64:AA== @ addr add comment in base64\n"); 107 | help.append("\nExamples:\n"); 108 | help.append(" CCu function starts here @ 0x1000\n"); 109 | help.append(" CCu base64:aGVsbG8gd29ybGQ= @ 0x2000\n"); 110 | help.append(" CCu important address @ $$ (at current address)\n"); 111 | return help.toString(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Attic/GhidraDecompiler.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019 Guillaume Valadon 2 | // This program is published under a GPLv2 license 3 | 4 | /* 5 | * Decompile a function with Ghidra 6 | * 7 | * analyzeHeadless . Test.gpr -import $BINARY_NAME -postScript GhidraDecompiler.java $FUNCTION_ADDRESS -deleteProject -noanalysis 8 | * 9 | */ 10 | 11 | import ghidra.app.decompiler.ClangLine; 12 | import ghidra.app.decompiler.DecompInterface; 13 | import ghidra.app.decompiler.DecompileResults; 14 | import ghidra.app.decompiler.DecompiledFunction; 15 | import ghidra.app.decompiler.PrettyPrinter; 16 | import ghidra.app.util.headless.HeadlessScript; 17 | import ghidra.program.model.address.Address; 18 | import ghidra.program.model.listing.Function; 19 | import ghidra.program.model.listing.FunctionIterator; 20 | import ghidra.program.model.listing.Listing; 21 | import java.io.FileWriter; 22 | import java.util.ArrayList; 23 | import java.util.Base64; 24 | 25 | public class GhidraDecompiler extends HeadlessScript { 26 | 27 | @Override 28 | public void run() throws Exception { 29 | FileWriter fw = new FileWriter("ghidra-output.r2"); 30 | FileWriter fw_dec = new FileWriter("decompiled.c"); 31 | 32 | // Stop after this headless script 33 | setHeadlessContinuationOption(HeadlessContinuationOption.ABORT); 34 | 35 | // Get the function address from the script arguments 36 | String[] args = getScriptArgs(); 37 | println(String.format("Array length: %d", args.length)); // DEBUG 38 | 39 | if (args.length == 0) { 40 | System.err.println("Please specify a function address!"); 41 | System.err.println("Note: use c0ffe instead of 0xcoffee"); 42 | return; 43 | } 44 | 45 | long functionAddress = 0; 46 | try { 47 | if (args[0].startsWith("0x")) { 48 | functionAddress = Long.parseLong(args[0].substring(2), 16); 49 | } else { 50 | functionAddress = Long.parseLong(args[0], 16); 51 | } 52 | } catch (NumberFormatException e) { 53 | System.err.println(args[0] + " " + e.toString()); 54 | } 55 | println(String.format("Address: %x", functionAddress)); // DEBUG 56 | 57 | DecompInterface di = new DecompInterface(); 58 | println("Simplification style: " + di.getSimplificationStyle()); // DEBUG 59 | println("Debug enables: " + di.debugEnabled()); 60 | 61 | Function f = this.getFunction(functionAddress); 62 | if (f == null) { 63 | System.err.println(String.format("Function not found at 0x%x", functionAddress)); 64 | return; 65 | } 66 | 67 | println(String.format("Decompiling %s() at 0x%x", f.getName(), functionAddress)); 68 | 69 | println("Program: " + di.openProgram(f.getProgram())); // DEBUG 70 | 71 | // Decompile with a 5-seconds timeout 72 | DecompileResults dr = di.decompileFunction(f, 5, null); 73 | println("Decompilation completed: " + dr.decompileCompleted()); // DEBUG 74 | 75 | DecompiledFunction df = dr.getDecompiledFunction(); 76 | println(df.getC()); 77 | 78 | // Print lines prepend with addresses 79 | PrettyPrinter pp = new PrettyPrinter(f, dr.getCCodeMarkup()); 80 | ArrayList lines = pp.getLines(); 81 | 82 | for (ClangLine line : lines) { 83 | long minAddress = Long.MAX_VALUE; 84 | long maxAddress = 0; 85 | for (int i = 0; i < line.getNumTokens(); i++) { 86 | if (line.getToken(i).getMinAddress() == null) { 87 | continue; 88 | } 89 | long addr = line.getToken(i).getMinAddress().getOffset(); 90 | minAddress = addr < minAddress ? addr : minAddress; 91 | maxAddress = addr > maxAddress ? addr : maxAddress; 92 | } 93 | if (maxAddress == 0) { 94 | println(String.format(" - %s", line.toString())); 95 | String comment = line.toString().split(":", 2)[1]; 96 | fw_dec.write(String.format("%s\n", comment)); 97 | } else { 98 | println(String.format("0x%-8x 0x%-8x - %s", minAddress, maxAddress, line.toString())); 99 | try { 100 | String comment = line.toString().split(":", 2)[1]; 101 | System.out.println(comment); 102 | String b64comment = Base64.getEncoder().encodeToString(comment.getBytes()); 103 | fw.write(String.format("CCu base64:%s @ 0x%x\n", b64comment, minAddress)); 104 | fw_dec.write(String.format("%s\n", comment)); 105 | } catch (Exception e) { 106 | System.out.println("ERROR: " + line.toString()); 107 | } 108 | // 0x%-8x 0x%-8x - %s", minAddress, maxAddress, line.toString())); 109 | } 110 | } 111 | fw.close(); 112 | fw_dec.close(); 113 | } 114 | 115 | protected Function getFunction(long address) { 116 | // Logic from https://github.com/cea-sec/Sibyl/blob/master/ext/ghidra/ExportFunction.java 117 | 118 | Listing listing = currentProgram.getListing(); 119 | FunctionIterator iter = listing.getFunctions(true); 120 | while (iter.hasNext() && !monitor.isCancelled()) { 121 | Function f = iter.next(); 122 | if (f.isExternal()) { 123 | continue; 124 | } 125 | 126 | Address entry = f.getEntryPoint(); 127 | if (entry != null && entry.getOffset() == address) { 128 | return f; 129 | } 130 | } 131 | return null; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2AnalyzeCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import ghidra.program.flatapi.FlatProgramAPI; 4 | import ghidra.program.model.address.Address; 5 | import ghidra.program.model.listing.Function; 6 | import ghidra.program.model.listing.Variable; 7 | import java.util.Base64; 8 | import org.json.JSONArray; 9 | import org.json.JSONObject; 10 | import r4ghidra.repl.R2Command; 11 | import r4ghidra.repl.R2CommandException; 12 | import r4ghidra.repl.R2CommandHandler; 13 | import r4ghidra.repl.R2Context; 14 | 15 | /** 16 | * Handler for the 'a' (analyze) command family: af (analyze function), afl (list functions), afi 17 | * (function info) 18 | */ 19 | /** 20 | * Handler for the 'a' (analyze) command family: af (analyze function), afl (list functions), 21 | * and afi (function info). 22 | *

23 | * This class provides functionality to analyze code at the current address, list all functions 24 | * in the program, and display detailed information about the current function. 25 | */ 26 | public class R2AnalyzeCommandHandler implements R2CommandHandler { 27 | 28 | @Override 29 | public String execute(R2Command command, R2Context context) throws R2CommandException { 30 | if (!command.hasPrefix("a")) { 31 | throw new R2CommandException("Not an analyze command"); 32 | } 33 | String sub = command.getSubcommandWithoutSuffix(); 34 | switch (sub) { 35 | case "f": 36 | return handleAf(command, context); 37 | case "fl": 38 | return handleAfl(command, context); 39 | case "fi": 40 | return handleAfi(command, context); 41 | default: 42 | throw new R2CommandException("Unknown analyze subcommand: a" + sub); 43 | } 44 | } 45 | 46 | // Analyze current function (create/disassemble) and show info 47 | private String handleAf(R2Command command, R2Context context) throws R2CommandException { 48 | Address addr = context.getCurrentAddress(); 49 | if (addr == null) { 50 | throw new R2CommandException("Current address is not set"); 51 | } 52 | FlatProgramAPI api = context.getAPI(); 53 | // Disassemble and create function at current address 54 | api.disassemble(addr); 55 | try { 56 | api.createFunction(addr, "ghidra." + context.formatAddress(addr)); 57 | } catch (Exception e) { 58 | // ignore if function already exists or creation failed 59 | } 60 | // After analysis, show function info 61 | return handleAfi(command, context); 62 | } 63 | 64 | // List all functions in program 65 | private String handleAfl(R2Command command, R2Context context) throws R2CommandException { 66 | FlatProgramAPI api = context.getAPI(); 67 | Function f = api.getFirstFunction(); 68 | boolean json = command.hasSuffix('j'); 69 | boolean rad = command.hasSuffix('*'); 70 | if (json) { 71 | JSONArray arr = new JSONArray(); 72 | while (f != null) { 73 | JSONObject obj = new JSONObject(); 74 | obj.put("name", f.getName()); 75 | obj.put("offset", f.getEntryPoint().getOffset()); 76 | arr.put(obj); 77 | f = api.getFunctionAfter(f); 78 | } 79 | return arr.toString() + "\n"; 80 | } else { 81 | StringBuilder sb = new StringBuilder(); 82 | while (f != null) { 83 | if (rad) { 84 | sb.append("f ghidra.") 85 | .append(f.getName()) 86 | .append(" 1 ") 87 | .append(context.formatAddress(f.getEntryPoint())) 88 | .append("\n"); 89 | } else { 90 | sb.append(context.formatAddress(f.getEntryPoint())) 91 | .append(" ") 92 | .append(f.getName()) 93 | .append("\n"); 94 | } 95 | f = api.getFunctionAfter(f); 96 | } 97 | return sb.toString(); 98 | } 99 | } 100 | 101 | // Show info for current function (variables, comment) 102 | private String handleAfi(R2Command command, R2Context context) throws R2CommandException { 103 | FlatProgramAPI api = context.getAPI(); 104 | Address addr = context.getCurrentAddress(); 105 | if (addr == null) { 106 | throw new R2CommandException("Current address is not set"); 107 | } 108 | Function f = api.getFunctionContaining(addr); 109 | if (f == null) { 110 | throw new R2CommandException("Cannot find function at " + context.formatAddress(addr)); 111 | } 112 | try { 113 | // Gather variables and comment 114 | Variable[] vars = f.getAllVariables(); 115 | String comment = f.getComment(); 116 | StringBuilder sb = new StringBuilder(); 117 | // Function entry 118 | sb.append("Function: ") 119 | .append(f.getName()) 120 | .append(" @ ") 121 | .append(context.formatAddress(f.getEntryPoint())) 122 | .append("\n"); 123 | // Parameters and locals 124 | for (Variable v : vars) { 125 | sb.append(v.getName()) 126 | .append(" : ") 127 | .append(v.getDataType().getName()) 128 | .append(" @ offset ") 129 | .append(v.getStackOffset()) 130 | .append("\n"); 131 | } 132 | // Comment (base64-encoded) 133 | if (comment != null && !comment.isEmpty()) { 134 | String b64 = Base64.getEncoder().encodeToString(comment.getBytes()); 135 | sb.append("CCu base64:") 136 | .append(b64) 137 | .append(" @ ") 138 | .append(context.formatAddress(f.getEntryPoint())) 139 | .append("\n"); 140 | } 141 | return sb.toString(); 142 | } catch (Exception e) { 143 | throw new R2CommandException(e.getMessage()); 144 | } 145 | } 146 | 147 | @Override 148 | public String getHelp() { 149 | StringBuilder help = new StringBuilder(); 150 | help.append("Usage: a[f|fl|fi][j*] [args]\n"); 151 | help.append(" af analyze function at current offset\n"); 152 | help.append(" afl list functions\n"); 153 | help.append(" afl* list as r2 commands\n"); 154 | help.append(" afl j list functions as JSON\n"); 155 | help.append(" afi show info for current function\n"); 156 | help.append(" afi j show function info as JSON (not implemented)\n"); 157 | return help.toString(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | r2ghidra logo 2 | 3 | ### Radare For Ghidra 4 | 5 | [![Build Extension](https://github.com/radareorg/r4ghidra/actions/workflows/gradle.yml/badge.svg)](https://github.com/radareorg/r4ghidra/actions/workflows/gradle.yml) 6 | 7 | R4Ghidra provides a standalone radare2 experience inside Ghidra, implemented fully in Java but powered by Ghidra's APIs internally. This plugin allows users to communicate from/to radare2 instances via r2web and r2pipe protocols. 8 | 9 | R4Ghidra supports not just the most common radare2 commands, but also all the handy command tricks you can do with r2 oneliners, including pipes, redirects, iterations, command substitution, file operations and more. The plugin features a complete REPL (Read-Eval-Print Loop) implementation that faithfully reproduces the radare2 command line experience within Ghidra. 10 | 11 | Please use the [Issue tracker](https://github.com/radareorg/ghidra-r2web/issues) for feedback and bug reports! 12 | 13 | #![r4ghidra](doc/images/r4ghidra.jpg) 14 | 15 | ## Build 16 | 17 | To build the plugin, simply run: 18 | 19 | ```bash 20 | make 21 | ``` 22 | 23 | The extension .zip will be created in `dist/` directory. You can also download pre-built releases from the [release page](https://github.com/radareorg/ghidra-r2web/releases). 24 | 25 | To install that extension just run `make install` and that will remove the current r4ghidra plugin in your detected Ghidra installation and place the last build into the ghidra Extensions directory. And you will only need to follow these simple steps: 26 | 27 | 1. Run ./ghidraRun 28 | 2. In **Ghidra Project Manager** choose `File->Install Extensions` 29 | 3. Click on the R4Ghidra plugin, close the window 30 | 4. You will be prompted to restart ghidra. Do it 31 | 5. When loading the project it will prompt you to setup the R4Ghidra plugin 32 | 6. Click in `Tools->R4Ghidra` menu 33 | 34 | ### Debugging Issues 35 | 36 | ghidraRun will start in background mode by default, you must edit the script to replace "bg" with "fg" to see backtraces and other startup debugging logs. 37 | 38 | ### Build Requirements 39 | 40 | - Java21 41 | - GHIDRA 42 | - Gradle 8.x 43 | 44 | ### Ubuntu 45 | 46 | ```bash 47 | sudo apt install openjdk-21-jdk:amd64 48 | sudo snap install ghidra --edge 49 | sudo snap install gradle --edge --classic 50 | make 51 | ``` 52 | 53 | ### IDEA 54 | 55 | A Run Configuration is provided for IntelliJ IDEA. To make it work you should: 56 | 57 | * Run IDEA with the `GHIDRA_INSTALL_DIR` environment variable set to your Ghidra release (not source!) directory. 58 | * Set the location of your Ghidra installation by adding the `GHIDRA_INSTALL_DIR` Path Variable under `File->Settings->Path Variables`. 59 | 60 | If everything is set up correctly IDEA should recognize the Gradle project and load external dependencies referenced by the Run Configuration from the referenced Ghidra directory. If everything is right you should see that `Use classpath of module` is set to `-cp R4Ghidra.main` in the Run Configuration GUI, and no errors are shown. You'll get a `ClassNotFoundException` when trying to use the Run Configuration if external dependencies were not discovered as expected. 61 | 62 | 63 | ## Installation 64 | 65 | ### Install 66 | 67 | 1. In **Ghidra Project Manager** choose `File->Install Extensions` 68 | 2. In the top right corner of the new window click the green plus sign 69 | 3. Choose the R4Ghidra distribution ZIP file from the `dist/` directory or downloaded from the release page 70 | 4. Restart Ghidra as instructed 71 | 5. After restart open the **Code Browser**, which should offer you to configure the new extension 72 | 6. Accept and tick the checkbox next to the plugin name 73 | 74 | If the configuration option is not offered after restart, you can manually enable the plugin: 75 | 1. Use the `File->Configure` menu item 76 | 2. Click the Configure link under Ghidra Core 77 | 3. Find and enable the R4Ghidra plugin in the list 78 | 79 | ### Uninstall 80 | 81 | 1. In **Ghidra Project Manager** choose `File->Install Extensions` 82 | 2. Select R4Ghidra from the list of installed extensions 83 | 3. Click the red X button in the top right corner to uninstall 84 | 4. Restart Ghidra as instructed 85 | 86 | ## Usage 87 | 88 | ### GUI Mode 89 | 90 | The plugin registers a new menu item under the Tools menu of Ghidra's Code Browser to start/stop the embedded web server. Once started, you can: 91 | 92 | 1. Use the built-in r2 REPL directly within Ghidra 93 | 2. Connect from an external radare2 instance using r2's web protocols 94 | 95 | ### Headless Mode 96 | 97 | The Python script provided in the `ghidra_scripts` directory initializes the R4Ghidra server on port 9191 by default. You can change the port by setting the `R4GHIDRA_PORT` environment variable (or `R2WEB_PORT` for backward compatibility). You should provide this script as `-postScript` when launching headless Ghidra: 98 | 99 | ```bash 100 | ./support/analyzeHeadless /path/to/project-dir project-name \ 101 | -process binary_name -postScript /path/to/r4ghidra_headless.py 102 | ``` 103 | 104 | Note: The older script name `r2web_headless.py` is still available for backward compatibility. 105 | 106 | ### R2 Features Support 107 | 108 | R4Ghidra implements a complete radare2 REPL with support for: 109 | 110 | - Common r2 commands (seek, print, analyze, info, etc.) 111 | - Command syntax features (pipes, redirects, command substitution) 112 | - Temporary addressing with @ syntax 113 | - Multiple command execution with @@ syntax 114 | - Shell command execution 115 | - File operations (with sandboxing) 116 | - Environment variables 117 | - Output filtering with grep-like syntax 118 | - Command output formatting (JSON, CSV, etc.) 119 | 120 | For more detailed information about the REPL implementation and supported features, see the REPL documentation in the source code. 121 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2EnvCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import java.util.Map; 4 | import java.util.TreeMap; 5 | import org.json.JSONObject; 6 | import r4ghidra.repl.R2Command; 7 | import r4ghidra.repl.R2CommandException; 8 | import r4ghidra.repl.R2CommandHandler; 9 | import r4ghidra.repl.R2Context; 10 | 11 | /** 12 | * Handler for the '%' command - Manage environment variables 13 | * 14 | *

This handler provides a radare2-compatible interface for working with environment variables: - 15 | * % : List all environment variables - %* : Show environment variables as r2 commands - %j : Show 16 | * environment variables in JSON format - %SHELL : Print value of a specific environment variable - 17 | * %TMPDIR=/tmp : Set environment variable TMPDIR to "/tmp" 18 | */ 19 | public class R2EnvCommandHandler implements R2CommandHandler { 20 | 21 | @Override 22 | public String execute(R2Command command, R2Context context) throws R2CommandException { 23 | // Check if this is a '%' command 24 | if (!command.hasPrefix("%")) { 25 | throw new R2CommandException("Not an environment command"); 26 | } 27 | 28 | // Get the subcommand (the part after %) 29 | String subcommand = command.getSubcommand().trim(); 30 | 31 | // Handle different subcommand types 32 | if (subcommand.isEmpty()) { 33 | // % - List all environment variables 34 | return listEnvironmentVariables(); 35 | } else if (subcommand.equals("*")) { 36 | // %* - Show environment variables as r2 commands 37 | return listEnvironmentVariablesAsCommands(); 38 | } else if (subcommand.equals("j")) { 39 | // %j - Show environment variables in JSON format 40 | return listEnvironmentVariablesAsJson(); 41 | } else if (subcommand.contains("=")) { 42 | // %NAME=VALUE - Set environment variable 43 | return setEnvironmentVariable(subcommand); 44 | } else { 45 | // %NAME - Get specific environment variable 46 | return getEnvironmentVariable(subcommand); 47 | } 48 | } 49 | 50 | /** 51 | * List all environment variables 52 | * 53 | * @return A string with all environment variables and their values 54 | */ 55 | private String listEnvironmentVariables() { 56 | StringBuilder sb = new StringBuilder(); 57 | 58 | // Get all environment variables and sort them alphabetically 59 | Map sortedEnv = new TreeMap<>(System.getenv()); 60 | 61 | // Format the output 62 | for (Map.Entry entry : sortedEnv.entrySet()) { 63 | sb.append(entry.getKey()).append("=").append(entry.getValue()).append("\n"); 64 | } 65 | 66 | return sb.toString(); 67 | } 68 | 69 | /** 70 | * List all environment variables as r2 commands 71 | * 72 | * @return A string with environment variables as r2 commands 73 | */ 74 | private String listEnvironmentVariablesAsCommands() { 75 | StringBuilder sb = new StringBuilder(); 76 | 77 | // Get all environment variables and sort them alphabetically 78 | Map sortedEnv = new TreeMap<>(System.getenv()); 79 | 80 | // Format the output as r2 commands 81 | for (Map.Entry entry : sortedEnv.entrySet()) { 82 | sb.append("%").append(entry.getKey()).append("=").append(entry.getValue()).append("\n"); 83 | } 84 | 85 | return sb.toString(); 86 | } 87 | 88 | /** 89 | * List all environment variables in JSON format 90 | * 91 | * @return A JSON string with all environment variables 92 | */ 93 | private String listEnvironmentVariablesAsJson() { 94 | JSONObject json = new JSONObject(); 95 | 96 | // Get all environment variables 97 | Map env = System.getenv(); 98 | 99 | // Add all variables to JSON 100 | for (Map.Entry entry : env.entrySet()) { 101 | json.put(entry.getKey(), entry.getValue()); 102 | } 103 | 104 | return json.toString(2) + "\n"; 105 | } 106 | 107 | /** 108 | * Set an environment variable 109 | * 110 | * @param expr The expression in format NAME=VALUE 111 | * @return A confirmation message 112 | */ 113 | private String setEnvironmentVariable(String expr) throws R2CommandException { 114 | // Parse the NAME=VALUE expression 115 | int equalIndex = expr.indexOf('='); 116 | String name = expr.substring(0, equalIndex).trim(); 117 | String value = expr.substring(equalIndex + 1).trim(); 118 | 119 | if (name.isEmpty()) { 120 | throw new R2CommandException("Empty variable name"); 121 | } 122 | 123 | try { 124 | // Using reflection to set environment variable (as System.setenv is not available in Java) 125 | // This is a hack that works on most JVMs but is not guaranteed by the JVM specification 126 | Map env = System.getenv(); 127 | 128 | java.lang.reflect.Field field = env.getClass().getDeclaredField("m"); 129 | field.setAccessible(true); 130 | 131 | @SuppressWarnings("unchecked") 132 | Map writableEnv = (Map) field.get(env); 133 | writableEnv.put(name, value); 134 | 135 | return "Environment variable set: " + name + "=" + value + "\n"; 136 | } catch (Exception e) { 137 | throw new R2CommandException( 138 | "Cannot set environment variable: " 139 | + e.getMessage() 140 | + "\nNote: Some JVMs may not allow modifying environment variables at runtime"); 141 | } 142 | } 143 | 144 | /** 145 | * Get the value of a specific environment variable 146 | * 147 | * @param name The name of the environment variable 148 | * @return The value of the environment variable or an error message 149 | */ 150 | private String getEnvironmentVariable(String name) { 151 | String value = System.getenv(name); 152 | 153 | if (value != null) { 154 | return value + "\n"; 155 | } else { 156 | return "Environment variable not found: " + name + "\n"; 157 | } 158 | } 159 | 160 | @Override 161 | public String getHelp() { 162 | StringBuilder help = new StringBuilder(); 163 | help.append("Usage: %[name[=value]] Set each NAME to VALUE in the environment\n"); 164 | help.append("| % list all environment variables\n"); 165 | help.append("| %* show env vars as r2 commands\n"); 166 | help.append("| %j show env vars in JSON format\n"); 167 | help.append("| %SHELL prints SHELL value\n"); 168 | help.append("| %TMPDIR=/tmp sets TMPDIR value to \"/tmp\"\n"); 169 | return help.toString(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Attic/GhidraDecompilerR2.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019 Guillaume Valadon 2 | // This program is published under a GPLv2 license 3 | 4 | /* 5 | * Decompile a function with Ghidra 6 | * 7 | * analyzeHeadless . Test.gpr -import $BINARY_NAME -postScript GhidraDecompilerR2.java $FUNCTION_ADDRESS -deleteProject -noanalysis 8 | * 9 | */ 10 | 11 | import ghidra.app.decompiler.ClangLine; 12 | import ghidra.app.decompiler.DecompInterface; 13 | import ghidra.app.decompiler.DecompileResults; 14 | import ghidra.app.decompiler.DecompiledFunction; 15 | import ghidra.app.decompiler.PrettyPrinter; 16 | import ghidra.app.util.headless.HeadlessScript; 17 | import ghidra.program.model.address.Address; 18 | import ghidra.program.model.listing.Function; 19 | import ghidra.program.model.listing.FunctionIterator; 20 | import ghidra.program.model.listing.Listing; 21 | import java.io.File; 22 | import java.io.FileWriter; 23 | import java.nio.file.*; 24 | import java.util.ArrayList; 25 | import java.util.Base64; 26 | 27 | public class GhidraDecompilerR2 extends HeadlessScript { 28 | static String INPUT = "r2-input"; 29 | static String OUTPUT = "r2-output"; 30 | 31 | private String readCommand() throws Exception { 32 | while (true) { 33 | try { 34 | String data = new String(Files.readAllBytes(Paths.get(INPUT))); 35 | File file = new File(INPUT); 36 | file.delete(); 37 | return data.trim(); 38 | } catch (Exception e) { 39 | } 40 | Thread.sleep(1000); 41 | } 42 | } 43 | 44 | private void writeResult(String output) throws Exception { 45 | FileWriter fw = new FileWriter(OUTPUT); 46 | fw.write(output); 47 | fw.close(); 48 | } 49 | 50 | @Override 51 | public void run() throws Exception { 52 | long functionAddress = main(getScriptArgs()); 53 | if (functionAddress != 0) { 54 | this.decompile(functionAddress); 55 | return; 56 | } 57 | while (true) { 58 | String cmd = readCommand(); 59 | if (cmd == "q") { 60 | break; 61 | } 62 | String[] args = new String[] {cmd}; 63 | functionAddress = main(args); 64 | this.decompile(functionAddress); 65 | } 66 | } 67 | 68 | public long main(String[] args) throws Exception { 69 | // Get the function address from the script arguments 70 | println(String.format("Array length: %d", args.length)); // DEBUG 71 | 72 | if (args.length == 0) { 73 | System.err.println("Please specify a function address!"); 74 | System.err.println("Note: use c0ffe instead of 0xcoffee"); 75 | return 0; 76 | } 77 | 78 | long functionAddress = 0; 79 | try { 80 | if (args[0].startsWith("0x")) { 81 | functionAddress = Long.parseLong(args[0].substring(2), 16); 82 | } else { 83 | functionAddress = Long.parseLong(args[0], 16); 84 | } 85 | } catch (NumberFormatException e) { 86 | System.err.println(args[0] + " " + e.toString()); 87 | } 88 | println(String.format("Address: %x", functionAddress)); // DEBUG 89 | return functionAddress; 90 | } 91 | 92 | public void decompile(long functionAddress) throws Exception { 93 | FileWriter fw = new FileWriter("ghidra-output.r2"); 94 | 95 | // Stop after this headless script 96 | setHeadlessContinuationOption(HeadlessContinuationOption.ABORT); 97 | 98 | DecompInterface di = new DecompInterface(); 99 | println("Simplification style: " + di.getSimplificationStyle()); // DEBUG 100 | println("Debug enables: " + di.debugEnabled()); 101 | 102 | Function f = this.getFunction(functionAddress); 103 | if (f == null) { 104 | System.err.println(String.format("Function not found at 0x%x", functionAddress)); 105 | return; 106 | } 107 | 108 | println(String.format("Decompiling %s() at 0x%x", f.getName(), functionAddress)); 109 | 110 | println("Program: " + di.openProgram(f.getProgram())); // DEBUG 111 | 112 | // Decompile with a 5-seconds timeout 113 | DecompileResults dr = di.decompileFunction(f, 5, null); 114 | println("Decompilation completed: " + dr.decompileCompleted()); // DEBUG 115 | 116 | DecompiledFunction df = dr.getDecompiledFunction(); 117 | println(df.getC()); 118 | 119 | // Print lines prepend with addresses 120 | PrettyPrinter pp = new PrettyPrinter(f, dr.getCCodeMarkup()); 121 | ArrayList lines = pp.getLines(); 122 | 123 | for (ClangLine line : lines) { 124 | long minAddress = Long.MAX_VALUE; 125 | long maxAddress = 0; 126 | for (int i = 0; i < line.getNumTokens(); i++) { 127 | if (line.getToken(i).getMinAddress() == null) { 128 | continue; 129 | } 130 | long addr = line.getToken(i).getMinAddress().getOffset(); 131 | minAddress = addr < minAddress ? addr : minAddress; 132 | maxAddress = addr > maxAddress ? addr : maxAddress; 133 | } 134 | if (maxAddress == 0) { 135 | println(String.format(" - %s", line.toString())); 136 | } else { 137 | println(String.format("0x%-8x 0x%-8x - %s", minAddress, maxAddress, line.toString())); 138 | try { 139 | String comment = line.toString().split(":", 2)[1]; 140 | System.out.println(comment); 141 | String b64comment = Base64.getEncoder().encodeToString(comment.getBytes()); 142 | fw.write(String.format("CCu base64:%s @ 0x%x\n", b64comment, minAddress)); 143 | } catch (Exception e) { 144 | System.out.println("ERROR: " + line.toString()); 145 | } 146 | // 0x%-8x 0x%-8x - %s", minAddress, maxAddress, line.toString())); 147 | } 148 | } 149 | fw.close(); 150 | } 151 | 152 | protected Function getFunction(long address) { 153 | // Logic from https://github.com/cea-sec/Sibyl/blob/master/ext/ghidra/ExportFunction.java 154 | 155 | Listing listing = currentProgram.getListing(); 156 | FunctionIterator iter = listing.getFunctions(true); 157 | while (iter.hasNext() && !monitor.isCancelled()) { 158 | Function f = iter.next(); 159 | if (f.isExternal()) { 160 | continue; 161 | } 162 | 163 | Address entry = f.getEntryPoint(); 164 | if (entry != null && entry.getOffset() == address) { 165 | return f; 166 | } 167 | } 168 | return null; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2JsCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import java.util.function.Function; 4 | import javax.script.Bindings; 5 | import javax.script.ScriptContext; 6 | import javax.script.ScriptEngine; 7 | import javax.script.ScriptException; 8 | import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; 9 | import r4ghidra.repl.R2Command; 10 | import r4ghidra.repl.R2CommandException; 11 | import r4ghidra.repl.R2CommandHandler; 12 | import r4ghidra.repl.R2Context; 13 | import r4ghidra.repl.R2REPLImpl; 14 | 15 | /** 16 | * Handler for the 'js' command 17 | * 18 | *

This command allows users to evaluate JavaScript expressions using the Nashorn engine. It also 19 | * provides an r2pipe-like interface via a global 'r2' object. 20 | */ 21 | public class R2JsCommandHandler implements R2CommandHandler { 22 | 23 | // The JavaScript engine 24 | private ScriptEngine engine; 25 | 26 | // Reference to the REPL for executing r2 commands 27 | private R2REPLImpl repl; 28 | 29 | /** Create a new JavaScript command handler */ 30 | public R2JsCommandHandler() { 31 | // Initialize the Nashorn engine 32 | NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); 33 | engine = factory.getScriptEngine(); 34 | 35 | // Get the singleton instance of the REPL 36 | try { 37 | // Use reflection to access the static instance field 38 | java.lang.reflect.Field instanceField = R2REPLImpl.class.getDeclaredField("instance"); 39 | instanceField.setAccessible(true); 40 | repl = (R2REPLImpl) instanceField.get(null); 41 | } catch (Exception e) { 42 | // If we can't get the instance, create a new one 43 | repl = new R2REPLImpl(); 44 | } 45 | } 46 | 47 | @Override 48 | public String execute(R2Command command, R2Context context) throws R2CommandException { 49 | // Check if this is a 'js' command 50 | if (!command.hasPrefix("js")) { 51 | throw new R2CommandException("Not a JavaScript command"); 52 | } 53 | 54 | String script; 55 | // Get the script from arguments or subcommand 56 | if (command.getArgumentCount() > 0) { 57 | // Join all arguments into one script 58 | script = String.join(" ", command.getArguments()); 59 | } else if (!command.getSubcommand().isEmpty()) { 60 | script = command.getSubcommand(); 61 | } else { 62 | throw new R2CommandException("No JavaScript expression provided"); 63 | } 64 | 65 | try { 66 | // Set up the r2 object with cmd() function before evaluating the script 67 | setupR2Interface(context); 68 | 69 | // Evaluate the script 70 | Object result; 71 | if (command.hasSuffix('e')) { 72 | // Evaluate without printing the result 73 | engine.eval(script); 74 | return ""; 75 | } else { 76 | // Evaluate and return the result 77 | result = engine.eval(script); 78 | 79 | if (result == null) { 80 | return "undefined\n"; 81 | } 82 | 83 | // Format the result based on the suffix 84 | if (command.hasSuffix('j')) { 85 | // JSON output - try to convert the result to JSON 86 | try { 87 | String jsonResult = convertToJSON(result); 88 | return jsonResult + "\n"; 89 | } catch (Exception e) { 90 | return "{\"error\":\"Cannot convert result to JSON\"}\n"; 91 | } 92 | } else { 93 | // Regular output 94 | return result.toString() + "\n"; 95 | } 96 | } 97 | } catch (ScriptException e) { 98 | // Return the JavaScript error 99 | return "JavaScript Error: " + e.getMessage() + "\n"; 100 | } 101 | } 102 | 103 | /** 104 | * Set up the r2pipe-like interface in the JavaScript environment 105 | * 106 | * @param context The R2Context to use for command execution 107 | * @throws ScriptException if the setup fails 108 | */ 109 | private void setupR2Interface(R2Context context) throws ScriptException { 110 | // Create a bindings object for the engine 111 | Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); 112 | 113 | // Create the r2 object 114 | String r2ObjectSetup = 115 | "var r2 = {" 116 | + " cmd: function(cmd) {" 117 | + " return _cmd(cmd);" 118 | + " }," 119 | + " cmdj: function(cmd) {" 120 | + " try {" 121 | + " return JSON.parse(_cmd(cmd + 'j'));" 122 | + " } catch(e) {" 123 | + " return null;" 124 | + " }" 125 | + " }" 126 | + "};"; 127 | 128 | // Define the _cmd function as a Java method reference 129 | bindings.put( 130 | "_cmd", 131 | (Function) 132 | (cmd) -> { 133 | try { 134 | return repl.executeCommand(cmd); 135 | } catch (Exception e) { 136 | return "Error: " + e.getMessage(); 137 | } 138 | }); 139 | 140 | // Evaluate the r2 object setup script 141 | engine.eval(r2ObjectSetup); 142 | } 143 | 144 | /** 145 | * Convert an object to JSON 146 | * 147 | * @param obj The object to convert 148 | * @return A JSON string representation of the object 149 | */ 150 | private String convertToJSON(Object obj) { 151 | if (obj == null) { 152 | return "null"; 153 | } else if (obj instanceof Number || obj instanceof Boolean) { 154 | return obj.toString(); 155 | } else if (obj instanceof String) { 156 | return "\"" + ((String) obj).replace("\"", "\\\"") + "\""; 157 | } else { 158 | // For other objects, try to use JavaScript's JSON.stringify 159 | try { 160 | return (String) engine.eval("JSON.stringify(" + obj.toString() + ")"); 161 | } catch (Exception e) { 162 | return "\"" + obj.toString().replace("\"", "\\\"") + "\""; 163 | } 164 | } 165 | } 166 | 167 | @Override 168 | public String getHelp() { 169 | StringBuilder sb = new StringBuilder(); 170 | sb.append("Usage: js[ej] \n"); 171 | sb.append(" js Evaluate JavaScript expression and print the result\n"); 172 | sb.append(" jse Evaluate JavaScript expression without printing the result\n"); 173 | sb.append(" jsj Evaluate and return result as JSON\n\n"); 174 | sb.append("JavaScript Environment:\n"); 175 | sb.append(" r2.cmd(str) Run r4ghidra command and return output as string\n"); 176 | sb.append(" r2.cmdj(str) Run r4ghidra command with JSON output and parse as object\n\n"); 177 | sb.append("Examples:\n"); 178 | sb.append(" js 1+1 # Print 2\n"); 179 | sb.append(" js r2.cmd('pd 2') # Run 'pd 2' command and print output\n"); 180 | sb.append( 181 | " js r2.cmdj('e.j').configs # Get eval vars as object and access configs property\n"); 182 | sb.append(" js var x=1; x+1 # Local variables work as expected\n"); 183 | sb.append(" js r2.cmd('s 0x100'); r2.cmd('pd 2') # Multiple commands\n"); 184 | return sb.toString(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2ShellCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.concurrent.TimeUnit; 10 | import r4ghidra.repl.R2Command; 11 | import r4ghidra.repl.R2CommandException; 12 | import r4ghidra.repl.R2CommandHandler; 13 | import r4ghidra.repl.R2Context; 14 | 15 | /** Handler for the '!' (shell) command - Execute external shell commands */ 16 | public class R2ShellCommandHandler implements R2CommandHandler { 17 | 18 | @Override 19 | public String execute(R2Command command, R2Context context) throws R2CommandException { 20 | // Check if this is a '!' command 21 | if (!command.hasPrefix("!")) { 22 | throw new R2CommandException("Not a shell command"); 23 | } 24 | 25 | // Get the raw command string and strip the '!' prefix 26 | String cmdLine = command.getPrefix() + command.getSubcommand(); 27 | 28 | // Handle different shell command types 29 | if (cmdLine.startsWith("!!")) { 30 | // !! - Capture output and return it 31 | return executeShellWithCapture(cmdLine.substring(2).trim()); 32 | } else { 33 | // ! - Execute and return exit code 34 | return executeShell(cmdLine.substring(1).trim(), context); 35 | } 36 | } 37 | 38 | /** 39 | * Execute a shell command with output capture 40 | * 41 | * @param shellCmd The shell command to execute 42 | * @return The captured output of the command 43 | */ 44 | private String executeShellWithCapture(String shellCmd) throws R2CommandException { 45 | if (shellCmd.isEmpty()) { 46 | return ""; 47 | } 48 | 49 | try { 50 | // Create process builder with shell command 51 | ProcessBuilder processBuilder = createProcessBuilder(shellCmd); 52 | 53 | // Start the process 54 | Process process = processBuilder.start(); 55 | 56 | // Capture output 57 | StringBuilder output = new StringBuilder(); 58 | try (BufferedReader reader = 59 | new BufferedReader(new InputStreamReader(process.getInputStream()))) { 60 | String line; 61 | while ((line = reader.readLine()) != null) { 62 | output.append(line).append("\n"); 63 | } 64 | } 65 | 66 | // Capture error output 67 | StringBuilder errorOutput = new StringBuilder(); 68 | try (BufferedReader reader = 69 | new BufferedReader(new InputStreamReader(process.getErrorStream()))) { 70 | String line; 71 | while ((line = reader.readLine()) != null) { 72 | errorOutput.append(line).append("\n"); 73 | } 74 | } 75 | 76 | // Wait for process to complete (with timeout) 77 | boolean completed = process.waitFor(60, TimeUnit.SECONDS); 78 | if (!completed) { 79 | process.destroyForcibly(); 80 | throw new R2CommandException("Command execution timed out after 60 seconds"); 81 | } 82 | 83 | // Check exit code 84 | int exitCode = process.exitValue(); 85 | 86 | // If error output exists and exit code is not 0, add it to the output 87 | if (exitCode != 0 && errorOutput.length() > 0) { 88 | output.append("\nError output:\n").append(errorOutput); 89 | } 90 | 91 | return output.toString(); 92 | 93 | } catch (IOException e) { 94 | throw new R2CommandException("IO error executing command: " + e.getMessage()); 95 | } catch (InterruptedException e) { 96 | Thread.currentThread().interrupt(); 97 | throw new R2CommandException("Command execution interrupted"); 98 | } 99 | } 100 | 101 | /** 102 | * Execute a shell command without capturing output This is meant for interactive commands that 103 | * send output directly to the terminal 104 | * 105 | * @param shellCmd The shell command to execute 106 | * @return A simple status message with the exit code 107 | */ 108 | private String executeShell(String shellCmd, R2Context context) throws R2CommandException { 109 | if (shellCmd.isEmpty()) { 110 | // Access command history through the REPL instance 111 | try { 112 | java.lang.reflect.Field field = r4ghidra.repl.R2REPLImpl.class.getDeclaredField("instance"); 113 | field.setAccessible(true); 114 | r4ghidra.repl.R2REPLImpl repl = (r4ghidra.repl.R2REPLImpl) field.get(null); 115 | if (repl != null) { 116 | // Get command history directly from the REPL 117 | return repl.displayCommandHistory(false); 118 | } 119 | } catch (Exception e) { 120 | // If we can't access the REPL, just return a message 121 | } 122 | return "No command history available.\n"; 123 | } 124 | 125 | try { 126 | // Create process builder with shell command 127 | ProcessBuilder processBuilder = createProcessBuilder(shellCmd); 128 | 129 | // Inherit IO streams to allow interactive use 130 | processBuilder.inheritIO(); 131 | 132 | // Start the process 133 | Process process = processBuilder.start(); 134 | 135 | // Wait for process to complete 136 | int exitCode = process.waitFor(); 137 | 138 | // Return simple status message 139 | return "Process exited with code: " + exitCode + "\n"; 140 | 141 | } catch (IOException e) { 142 | throw new R2CommandException("IO error executing command: " + e.getMessage()); 143 | } catch (InterruptedException e) { 144 | Thread.currentThread().interrupt(); 145 | throw new R2CommandException("Command execution interrupted"); 146 | } 147 | } 148 | 149 | /** Create a process builder for the given shell command */ 150 | private ProcessBuilder createProcessBuilder(String shellCmd) { 151 | List command = new ArrayList<>(); 152 | 153 | // Determine shell based on OS 154 | if (System.getProperty("os.name").toLowerCase().contains("win")) { 155 | // Windows 156 | command.add("cmd.exe"); 157 | command.add("/c"); 158 | command.add(shellCmd); 159 | } else { 160 | // Unix-like 161 | command.add("/bin/sh"); 162 | command.add("-c"); 163 | command.add(shellCmd); 164 | } 165 | 166 | // Create and configure process builder 167 | ProcessBuilder processBuilder = new ProcessBuilder(command); 168 | 169 | // Set working directory to current directory 170 | processBuilder.directory(new File(System.getProperty("user.dir"))); 171 | 172 | return processBuilder; 173 | } 174 | 175 | @Override 176 | public String getHelp() { 177 | StringBuilder help = new StringBuilder(); 178 | help.append("Usage: ![!]cmd\n"); 179 | help.append(" ! show command history\n"); 180 | help.append(" !cmd run shell command and redirect output to terminal\n"); 181 | help.append(" !!cmd run shell command and capture output\n"); 182 | help.append(" .!cmd run shell command and execute output as r2 commands\n"); 183 | help.append("\nExamples:\n"); 184 | help.append(" !ls list files in current directory (interactive)\n"); 185 | help.append(" !!ls capture and return the output of ls\n"); 186 | help.append(" !!ls -la | grep \"\\.java$\" run complex shell commands and capture output\n"); 187 | help.append(" .!rabin2 -ri $FILE run external tool and execute its output as r2 script\n"); 188 | return help.toString(); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2InfoCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import ghidra.program.model.address.Address; 4 | import ghidra.program.model.lang.Language; 5 | import ghidra.program.model.listing.Program; 6 | import org.json.JSONObject; 7 | import r4ghidra.repl.R2Command; 8 | import r4ghidra.repl.R2CommandException; 9 | import r4ghidra.repl.R2CommandHandler; 10 | import r4ghidra.repl.R2Context; 11 | 12 | /** 13 | * Handler for the 'i' (info) command family 14 | * 15 | *

This command provides information about the program and its architecture. 16 | */ 17 | public class R2InfoCommandHandler implements R2CommandHandler { 18 | 19 | @Override 20 | public String execute(R2Command command, R2Context context) throws R2CommandException { 21 | // Check if it's an 'i' command 22 | if (!command.hasPrefix("i")) { 23 | throw new R2CommandException("Not an info command"); 24 | } 25 | 26 | // Get the subcommand without suffix 27 | String subcommand = command.getSubcommandWithoutSuffix(); 28 | 29 | // Handle different subcommands or default to basic info 30 | if (subcommand.isEmpty()) { 31 | return executeBasicInfoCommand(command, context); 32 | } else { 33 | switch (subcommand) { 34 | default: 35 | return executeBasicInfoCommand(command, context); 36 | } 37 | } 38 | } 39 | 40 | /** Execute the basic info command to show program information */ 41 | private String executeBasicInfoCommand(R2Command command, R2Context context) 42 | throws R2CommandException { 43 | Program program = context.getAPI().getCurrentProgram(); 44 | if (program == null) { 45 | throw new R2CommandException("No program is loaded"); 46 | } 47 | 48 | Language language = program.getLanguage(); 49 | String processor = language.getProcessor().toString().toLowerCase(); 50 | 51 | // Determine architecture and bits 52 | String arch = "x86"; 53 | String bits = "64"; 54 | 55 | if (processor.equals("aarch64")) { 56 | arch = "arm"; 57 | bits = "64"; 58 | } else if (processor.contains("arm")) { 59 | arch = "arm"; 60 | bits = "32"; 61 | } else if (processor.contains("mips")) { 62 | arch = "mips"; 63 | bits = language.getDefaultSpace().getSize() == 64 ? "64" : "32"; 64 | } else if (processor.contains("ppc") || processor.contains("powerpc")) { 65 | arch = "ppc"; 66 | bits = language.getDefaultSpace().getSize() == 64 ? "64" : "32"; 67 | } else if (processor.contains("x86")) { 68 | arch = "x86"; 69 | bits = language.getDefaultSpace().getSize() == 64 ? "64" : "32"; 70 | } else if (processor.contains("sparc")) { 71 | arch = "sparc"; 72 | bits = language.getDefaultSpace().getSize() == 64 ? "64" : "32"; 73 | } else if (processor.contains("avr")) { 74 | arch = "avr"; 75 | bits = "8"; 76 | } else if (processor.contains("6502")) { 77 | arch = "6502"; 78 | bits = "8"; 79 | } else if (processor.contains("z80")) { 80 | arch = "z80"; 81 | bits = "8"; 82 | } 83 | 84 | // Format output based on suffix 85 | if (command.hasSuffix('j')) { 86 | return formatInfoJson(program, arch, bits, processor); 87 | } 88 | if (command.hasSuffix('*')) { 89 | return formatInfoR2(program, arch, bits, processor); 90 | } 91 | return formatInfoText(program, arch, bits, processor); 92 | } 93 | 94 | /** Format program information as text */ 95 | private String formatInfoR2(Program program, String arch, String bits, String processor) { 96 | StringBuilder sb = new StringBuilder(); 97 | 98 | // Output the r2 commands that would set up the environment 99 | sb.append("e asm.arch=").append(arch).append("\n"); 100 | sb.append("e asm.bits=").append(bits).append("\n"); 101 | sb.append("f base.addr=0x").append(program.getImageBase()).append("\n"); 102 | try { 103 | // Try to get actual entry point from executable path 104 | Address entryPoint = null; 105 | if (program.getExecutablePath() != null) { 106 | entryPoint = program.getImageBase(); 107 | } 108 | 109 | // If no entry point found, use image base 110 | if (entryPoint == null) { 111 | entryPoint = program.getImageBase(); 112 | } 113 | 114 | sb.append("f entry0=0x" + entryPoint + "\n"); 115 | } catch (Exception e) { 116 | // ignored 117 | } 118 | 119 | // Add additional information as comments 120 | sb.append("# cpu ").append(processor).append("\n"); 121 | sb.append("# md5 ").append(program.getExecutableMD5()).append("\n"); 122 | sb.append("# exe ").append(program.getExecutablePath()).append("\n"); 123 | 124 | // Add language information 125 | sb.append("# language ") 126 | .append(program.getLanguage().getLanguageID().getIdAsString()) 127 | .append("\n"); 128 | sb.append("# compiler ").append(program.getCompiler()).append("\n"); 129 | 130 | try { 131 | // Add program size 132 | sb.append("# size ") 133 | .append(program.getMaxAddress().subtract(program.getMinAddress()) + 1) 134 | .append("\n"); 135 | } catch (Exception e) { 136 | sb.append("# " + e.toString()); 137 | } 138 | 139 | return sb.toString(); 140 | } 141 | 142 | /** Format program information as text */ 143 | private String formatInfoText(Program program, String arch, String bits, String processor) { 144 | StringBuilder sb = new StringBuilder(); 145 | 146 | // Output the r2 commands that would set up the environment 147 | sb.append("arch ").append(arch).append("\n"); 148 | sb.append("bits ").append(bits).append("\n"); 149 | sb.append("baddr 0x").append(program.getImageBase()).append("\n"); 150 | // Add additional information as comments 151 | // sb.append("# cpu ").append(processor).append("\n"); 152 | sb.append("md5 ").append(program.getExecutableMD5()).append("\n"); 153 | sb.append("exe ").append(program.getExecutablePath()).append("\n"); 154 | 155 | // Add language information 156 | sb.append("lang ").append(program.getLanguage().getLanguageID().getIdAsString()).append("\n"); 157 | sb.append("comp ").append(program.getCompiler()).append("\n"); 158 | 159 | try { 160 | // Add program size 161 | sb.append("size ") 162 | .append(program.getMemory().getSize()) // getMinAddress and getMaxAddress can point to different AddressSpaces! 163 | .append("\n"); 164 | } catch (Exception e) { 165 | sb.append("# " + e.toString()); 166 | } 167 | 168 | return sb.toString(); 169 | } 170 | 171 | /** Format program information as JSON */ 172 | private String formatInfoJson(Program program, String arch, String bits, String processor) { 173 | JSONObject info = new JSONObject(); 174 | 175 | // Basic architecture info 176 | info.put("arch", arch); 177 | info.put("bits", Integer.parseInt(bits)); 178 | info.put("base", "0x" + Long.toHexString(program.getImageBase().getOffset())); 179 | 180 | // CPU and other information 181 | info.put("cpu", processor); 182 | info.put("md5", program.getExecutableMD5()); 183 | info.put("file", program.getExecutablePath()); 184 | info.put("language", program.getLanguage().getLanguageID().getIdAsString()); 185 | info.put("compiler", program.getCompiler()); 186 | info.put("size", program.getMaxAddress().subtract(program.getMinAddress()) + 1); 187 | 188 | return info.toString(2) + "\n"; 189 | } 190 | 191 | @Override 192 | public String getHelp() { 193 | StringBuilder help = new StringBuilder(); 194 | help.append("Usage: i[j] - Show program information\n\n"); 195 | help.append("i Show basic program information\n"); 196 | help.append("ij Show program information as JSON\n"); 197 | help.append("\nExamples:\n"); 198 | help.append("i Display basic program information\n"); 199 | help.append("ij Display program information as JSON\n"); 200 | return help.toString(); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2DecompileCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import ghidra.app.decompiler.ClangLine; 4 | import ghidra.app.decompiler.DecompInterface; 5 | import ghidra.app.decompiler.DecompileResults; 6 | import ghidra.app.decompiler.PrettyPrinter; 7 | import ghidra.program.model.listing.Function; 8 | import ghidra.program.model.mem.MemoryAccessException; 9 | import ghidra.program.model.symbol.IdentityNameTransformer; 10 | import java.util.ArrayList; 11 | import java.util.Base64; 12 | import org.json.JSONArray; 13 | import org.json.JSONObject; 14 | import r4ghidra.repl.R2Command; 15 | import r4ghidra.repl.R2CommandException; 16 | import r4ghidra.repl.R2CommandHandler; 17 | import r4ghidra.repl.R2Context; 18 | 19 | /** 20 | * Handler for the 'pdd' command - Decompile function at current address 21 | *

22 | * This command uses Ghidra's decompiler to generate C-like pseudocode for the function at the current 23 | * address. The output can be formatted in several ways: standard (with addresses), as radare2 commands, 24 | * as JSON, or in a quiet format (without addresses). 25 | *

26 | * The command supports the following formats: 27 | * - pdd: standard output with addresses 28 | * - pdd*: output as radare2 commands 29 | * - pddj: output as JSON 30 | * - pddq: quiet output (no addresses) 31 | */ 32 | public class R2DecompileCommandHandler implements R2CommandHandler { 33 | 34 | @Override 35 | public String execute(R2Command command, R2Context context) throws R2CommandException { 36 | // Check if this is a 'p' command 37 | if (!command.hasPrefix("p")) { 38 | throw new R2CommandException("Not a print command"); 39 | } 40 | 41 | // Check if it's the 'pdd' subcommand using the base subcommand without suffix 42 | String subcommand = command.getSubcommandWithoutSuffix(); 43 | if (!subcommand.equals("dd")) { 44 | throw new R2CommandException("Not a decompile command"); 45 | } 46 | 47 | try { 48 | // Get function at current address 49 | Function function = context.getAPI().getFunctionContaining(context.getCurrentAddress()); 50 | if (function == null) { 51 | throw new R2CommandException( 52 | "No function at address " + context.formatAddress(context.getCurrentAddress())); 53 | } 54 | 55 | // Decompile the function 56 | ArrayList lines = decompileFunction(function, context); 57 | // Format the output according to the command suffix 58 | Character suffix = command.getCommandSuffix(); 59 | switch (suffix) { 60 | case '*': 61 | return formatAsRadare2Commands(lines); 62 | case 'j': 63 | return formatAsJson(lines, function); 64 | case 'q': 65 | return formatQuiet(lines); 66 | default: 67 | return formatStandard(lines); 68 | } 69 | } catch (MemoryAccessException mae) { 70 | throw new R2CommandException( 71 | "No function at address " + context.formatAddress(context.getCurrentAddress())); 72 | } catch (R2CommandException e) { 73 | throw e; 74 | } catch (Exception e) { 75 | throw new R2CommandException("Decompilation error: " + e.getMessage()); 76 | } 77 | } 78 | 79 | /** Represents a line of decompiled code with its associated address */ 80 | private static class DecompiledLine { 81 | 82 | public long minAddress; 83 | public long maxAddress; 84 | public String codeLine; 85 | 86 | public DecompiledLine(long minAddress, long maxAddress, String codeLine) { 87 | this.minAddress = minAddress; 88 | this.maxAddress = maxAddress; 89 | this.codeLine = codeLine; 90 | } 91 | 92 | public boolean hasAddress() { 93 | return maxAddress > 0; 94 | } 95 | } 96 | 97 | /** Decompile a function and return the lines with address information */ 98 | private ArrayList decompileFunction(Function function, R2Context context) 99 | throws Exception { 100 | ArrayList result = new ArrayList<>(); 101 | 102 | DecompInterface decompInterface = new DecompInterface(); 103 | 104 | // Initialize the decompiler 105 | decompInterface.openProgram(function.getProgram()); 106 | 107 | // Decompile with a 5-seconds timeout 108 | DecompileResults decompileResults = decompInterface.decompileFunction(function, 5, null); 109 | 110 | if (!decompileResults.decompileCompleted()) { 111 | throw new R2CommandException("Decompilation did not complete successfully"); 112 | } 113 | 114 | // Format and extract the decompiled code with addresses 115 | PrettyPrinter prettyPrinter = 116 | new PrettyPrinter( 117 | function, decompileResults.getCCodeMarkup(), new IdentityNameTransformer()); 118 | ArrayList codeLines = new ArrayList<>(prettyPrinter.getLines()); 119 | 120 | for (ClangLine line : codeLines) { 121 | long minAddress = Long.MAX_VALUE; 122 | long maxAddress = 0; 123 | 124 | // Find the min and max addresses for this line 125 | for (int i = 0; i < line.getNumTokens(); i++) { 126 | if (line.getToken(i).getMinAddress() == null) { 127 | continue; 128 | } 129 | long addr = line.getToken(i).getMinAddress().getOffset(); 130 | minAddress = addr < minAddress ? addr : minAddress; 131 | maxAddress = addr > maxAddress ? addr : maxAddress; 132 | } 133 | 134 | // Format the code line 135 | String codeLine = line.toString(); 136 | int colon = codeLine.indexOf(':'); 137 | if (colon != -1) { 138 | codeLine = codeLine.substring(colon + 1); 139 | codeLine = line.getIndentString() + codeLine; 140 | } 141 | 142 | // If no address was found, use maximum value as flag 143 | if (minAddress == Long.MAX_VALUE) { 144 | minAddress = 0; 145 | } 146 | 147 | result.add(new DecompiledLine(minAddress, maxAddress, codeLine)); 148 | } 149 | 150 | return result; 151 | } 152 | 153 | /** Format decompiled lines as radare2 commands */ 154 | private String formatAsRadare2Commands(ArrayList lines) { 155 | StringBuilder result = new StringBuilder(); 156 | 157 | for (DecompiledLine line : lines) { 158 | // Only output lines that have an address associated with them 159 | if (line.hasAddress()) { 160 | // Base64 encode for radare2 comments 161 | String b64comment = Base64.getEncoder().encodeToString(line.codeLine.getBytes()); 162 | result.append(String.format("CCu base64:%s @ 0x%x\n", b64comment, line.minAddress)); 163 | } 164 | } 165 | 166 | return result.toString(); 167 | } 168 | 169 | /** Format decompiled lines as JSON */ 170 | private String formatAsJson(ArrayList lines, Function function) { 171 | JSONObject json = new JSONObject(); 172 | JSONArray linesArray = new JSONArray(); 173 | 174 | // Add function information 175 | json.put("name", function.getName()); 176 | json.put("address", "0x" + Long.toHexString(function.getEntryPoint().getOffset())); 177 | json.put("size", function.getBody().getNumAddresses()); 178 | 179 | // Add decompiled lines 180 | for (DecompiledLine line : lines) { 181 | JSONObject lineObj = new JSONObject(); 182 | lineObj.put("code", line.codeLine); 183 | 184 | if (line.hasAddress()) { 185 | lineObj.put("address", "0x" + Long.toHexString(line.minAddress)); 186 | } 187 | 188 | linesArray.put(lineObj); 189 | } 190 | 191 | json.put("lines", linesArray); 192 | return json.toString() + "\n"; 193 | } 194 | 195 | /** Format decompiled lines in standard format with addresses */ 196 | private String formatStandard(ArrayList lines) { 197 | StringBuilder result = new StringBuilder(); 198 | 199 | for (DecompiledLine line : lines) { 200 | if (line.hasAddress()) { 201 | // Address associated with this line 202 | result.append(String.format("0x%08x %s\n", line.minAddress, line.codeLine)); 203 | } else { 204 | // No address associated with this line 205 | result.append(String.format(" %s\n", line.codeLine)); 206 | } 207 | } 208 | 209 | return result.toString(); 210 | } 211 | 212 | /** Format decompiled lines in quiet mode (no addresses) */ 213 | private String formatQuiet(ArrayList lines) { 214 | StringBuilder result = new StringBuilder(); 215 | 216 | for (DecompiledLine line : lines) { 217 | result.append(line.codeLine).append("\n"); 218 | } 219 | 220 | return result.toString(); 221 | } 222 | 223 | @Override 224 | public String getHelp() { 225 | StringBuilder help = new StringBuilder(); 226 | help.append("Usage: pdd[*jq]\n"); 227 | help.append(" pdd decompile current function\n"); 228 | help.append(" pdd* decompile as radare2 comments\n"); 229 | help.append(" pddj decompile with JSON output\n"); 230 | help.append(" pddq decompile with quiet output (no addresses)\n"); 231 | return help.toString(); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/filesystem/R2SandboxedFileSystem.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.filesystem; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.nio.file.StandardOpenOption; 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | import r4ghidra.repl.R2Context; 15 | 16 | /** 17 | * Implementation of the R2FileSystem interface with sandbox support 18 | * 19 | *

This class provides methods for interacting with files, with support for sandboxed access and 20 | * in-memory files. The sandbox settings in the R2Context are used to determine which operations are 21 | * allowed. 22 | */ 23 | public class R2SandboxedFileSystem implements R2FileSystem { 24 | 25 | // Context with sandbox settings 26 | private R2Context context; 27 | 28 | // In-memory files ($-prefixed) 29 | private Map memoryFiles; 30 | 31 | /** 32 | * Create a new R2SandboxedFileSystem 33 | * 34 | * @param context The R2 context with sandbox settings 35 | */ 36 | public R2SandboxedFileSystem(R2Context context) { 37 | this.context = context; 38 | this.memoryFiles = new HashMap<>(); 39 | } 40 | 41 | @Override 42 | public String readFile(String path) throws IOException, R2FileSystemException { 43 | // Check if it's an in-memory file 44 | if (isMemoryFile(path)) { 45 | return readMemoryFile(path); 46 | } 47 | 48 | // Check sandbox permissions for file read 49 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_FILES)) { 50 | throw new R2FileSystemException("File reading not allowed by sandbox settings"); 51 | } 52 | 53 | // Proceed with regular file read 54 | Path filePath = Paths.get(path); 55 | return Files.readString(filePath); 56 | } 57 | 58 | @Override 59 | public void writeFile(String path, String content) throws IOException, R2FileSystemException { 60 | // Check if it's an in-memory file 61 | if (isMemoryFile(path)) { 62 | writeMemoryFile(path, content); 63 | return; 64 | } 65 | 66 | // Check sandbox permissions for disk write 67 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_DISK)) { 68 | throw new R2FileSystemException("Disk writing not allowed by sandbox settings"); 69 | } 70 | 71 | // Check sandbox permissions for file write 72 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_FILES)) { 73 | throw new R2FileSystemException("File writing not allowed by sandbox settings"); 74 | } 75 | 76 | // Proceed with regular file write 77 | Path filePath = Paths.get(path); 78 | 79 | // Create parent directories if needed 80 | Path parent = filePath.getParent(); 81 | if (parent != null) { 82 | Files.createDirectories(parent); 83 | } 84 | 85 | Files.writeString(filePath, content); 86 | } 87 | 88 | @Override 89 | public void appendFile(String path, String content) throws IOException, R2FileSystemException { 90 | // Check if it's an in-memory file 91 | if (isMemoryFile(path)) { 92 | appendMemoryFile(path, content); 93 | return; 94 | } 95 | 96 | // Check sandbox permissions for disk write 97 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_DISK)) { 98 | throw new R2FileSystemException("Disk writing not allowed by sandbox settings"); 99 | } 100 | 101 | // Check sandbox permissions for file write 102 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_FILES)) { 103 | throw new R2FileSystemException("File writing not allowed by sandbox settings"); 104 | } 105 | 106 | // Proceed with regular file append 107 | Path filePath = Paths.get(path); 108 | 109 | // Create parent directories if needed 110 | Path parent = filePath.getParent(); 111 | if (parent != null) { 112 | Files.createDirectories(parent); 113 | } 114 | 115 | // Use append option when writing 116 | Files.writeString(filePath, content, StandardOpenOption.CREATE, StandardOpenOption.APPEND); 117 | } 118 | 119 | @Override 120 | public void deleteFile(String path) throws IOException, R2FileSystemException { 121 | // Check if it's an in-memory file 122 | if (isMemoryFile(path)) { 123 | deleteMemoryFile(path); 124 | return; 125 | } 126 | 127 | // Check sandbox permissions for disk write 128 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_DISK)) { 129 | throw new R2FileSystemException("Disk modifications not allowed by sandbox settings"); 130 | } 131 | 132 | // Check sandbox permissions for file write 133 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_FILES)) { 134 | throw new R2FileSystemException("File deletion not allowed by sandbox settings"); 135 | } 136 | 137 | // Proceed with regular file delete 138 | Path filePath = Paths.get(path); 139 | Files.delete(filePath); 140 | } 141 | 142 | @Override 143 | public boolean fileExists(String path) { 144 | // Check if it's an in-memory file 145 | if (isMemoryFile(path)) { 146 | return memoryFiles.containsKey(getMemoryFileName(path)); 147 | } 148 | 149 | // For real files, check even with sandbox restrictions 150 | // (we're just checking, not actually accessing the file content) 151 | Path filePath = Paths.get(path); 152 | return Files.exists(filePath); 153 | } 154 | 155 | @Override 156 | public List listFiles(String path) throws IOException, R2FileSystemException { 157 | // Special case for listing memory files 158 | if (path.equals("$") || path.equals("$-")) { 159 | return listMemoryFiles(); 160 | } 161 | 162 | // Check sandbox permissions for file listing 163 | if (context.isSandboxed(R2Context.R_SANDBOX_GRAIN_FILES)) { 164 | throw new R2FileSystemException("File listing not allowed by sandbox settings"); 165 | } 166 | 167 | // Proceed with regular directory listing 168 | Path dirPath = Paths.get(path); 169 | 170 | try (Stream stream = Files.list(dirPath)) { 171 | return stream.map(Path::toString).collect(Collectors.toList()); 172 | } 173 | } 174 | 175 | @Override 176 | public List listMemoryFiles() { 177 | return new ArrayList<>(memoryFiles.keySet()); 178 | } 179 | 180 | @Override 181 | public boolean isMemoryFile(String path) { 182 | return path != null && path.startsWith("$"); 183 | } 184 | 185 | @Override 186 | public String getMemoryFileName(String path) { 187 | if (path == null || !path.startsWith("$")) { 188 | return null; 189 | } 190 | return path.substring(1); // Remove the $ prefix 191 | } 192 | 193 | /** 194 | * Read from an in-memory file 195 | * 196 | * @param path The path to the memory file (including $ prefix) 197 | * @return The contents of the memory file 198 | * @throws R2FileSystemException If the memory file doesn't exist 199 | */ 200 | private String readMemoryFile(String path) throws R2FileSystemException { 201 | String memoryFileName = getMemoryFileName(path); 202 | if (!memoryFiles.containsKey(memoryFileName)) { 203 | throw new R2FileSystemException("Memory file not found: " + path); 204 | } 205 | return memoryFiles.get(memoryFileName); 206 | } 207 | 208 | /** 209 | * Write to an in-memory file 210 | * 211 | * @param path The path to the memory file (including $ prefix) 212 | * @param content The content to write to the memory file 213 | */ 214 | private void writeMemoryFile(String path, String content) { 215 | String memoryFileName = getMemoryFileName(path); 216 | memoryFiles.put(memoryFileName, content); 217 | } 218 | 219 | /** 220 | * Append to an in-memory file 221 | * 222 | * @param path The path to the memory file (including $ prefix) 223 | * @param content The content to append to the memory file 224 | * @throws R2FileSystemException If the memory file doesn't exist 225 | */ 226 | private void appendMemoryFile(String path, String content) throws R2FileSystemException { 227 | String memoryFileName = getMemoryFileName(path); 228 | if (!memoryFiles.containsKey(memoryFileName)) { 229 | // If it doesn't exist, create it 230 | memoryFiles.put(memoryFileName, content); 231 | } else { 232 | // If it exists, append to it 233 | String existingContent = memoryFiles.get(memoryFileName); 234 | memoryFiles.put(memoryFileName, existingContent + content); 235 | } 236 | } 237 | 238 | /** 239 | * Delete an in-memory file 240 | * 241 | * @param path The path to the memory file (including $ prefix) 242 | * @throws R2FileSystemException If the memory file doesn't exist 243 | */ 244 | private void deleteMemoryFile(String path) throws R2FileSystemException { 245 | String memoryFileName = getMemoryFileName(path); 246 | if (!memoryFiles.containsKey(memoryFileName)) { 247 | throw new R2FileSystemException("Memory file not found: " + path); 248 | } 249 | memoryFiles.remove(memoryFileName); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2EvalCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import java.util.Map; 4 | import org.json.JSONArray; 5 | import org.json.JSONObject; 6 | import r4ghidra.repl.R2Command; 7 | import r4ghidra.repl.R2CommandException; 8 | import r4ghidra.repl.R2CommandHandler; 9 | import r4ghidra.repl.R2Context; 10 | import r4ghidra.repl.config.R2EvalConfig; 11 | 12 | /** 13 | * Handler for the 'e' (eval) command 14 | * 15 | *

This command allows users to view and set configuration variables. 16 | */ 17 | public class R2EvalCommandHandler implements R2CommandHandler { 18 | 19 | @Override 20 | public String execute(R2Command command, R2Context context) throws R2CommandException { 21 | // Check if this is an 'e' command 22 | if (!command.hasPrefix("e")) { 23 | throw new R2CommandException("Not an eval command"); 24 | } 25 | // Access the configuration manager 26 | R2EvalConfig config = context.getEvalConfig(); 27 | 28 | // Determine expression: prefer the subcommand (without suffix), else first argument 29 | String rawSub = command.getSubcommandWithoutSuffix().trim(); 30 | String expr; 31 | if (!rawSub.isEmpty()) { 32 | expr = rawSub; 33 | } else if (command.getArgumentCount() > 0) { 34 | expr = command.getFirstArgument(""); 35 | } else { 36 | expr = ""; 37 | } 38 | 39 | // List all variables when no expression provided 40 | if (expr.isEmpty()) { 41 | return formatEvalOutput(config.getAll(), command); 42 | } 43 | // Set variable when expression contains '=' 44 | if (expr.contains("=")) { 45 | return handleSetVariable(expr, config, command); 46 | } 47 | // Query variables (specific or prefix) 48 | return handleQueryVariables(expr, config, command); 49 | } 50 | 51 | /** Handle setting a variable */ 52 | private String handleSetVariable(String subcommand, R2EvalConfig config, R2Command command) 53 | throws R2CommandException { 54 | // Parse key=value format 55 | int equalPos = subcommand.indexOf('='); 56 | String key = subcommand.substring(0, equalPos).trim(); 57 | String value = subcommand.substring(equalPos + 1).trim(); 58 | 59 | // Validate the key 60 | if (key.isEmpty()) { 61 | throw new R2CommandException("Invalid eval key"); 62 | } 63 | 64 | // Special validation for certain keys 65 | if (key.equals("asm.bits")) { 66 | // Check if the value is one of the allowed bit widths 67 | if (!value.equals("8") && !value.equals("16") && !value.equals("32") && !value.equals("64")) { 68 | throw new R2CommandException("Invalid value for asm.bits, must be 8, 16, 32, or 64"); 69 | } 70 | } else if (key.equals("cfg.endian")) { 71 | // Check if the value is one of the allowed endian types 72 | if (!value.equalsIgnoreCase("big") 73 | && !value.equalsIgnoreCase("little") 74 | && !value.equalsIgnoreCase("middle")) { 75 | throw new R2CommandException( 76 | "Invalid value for cfg.endian, must be big, little, or middle"); 77 | } 78 | } else if (key.equals("scr.color")) { 79 | // Check if the value is a valid color level 80 | try { 81 | int colorLevel = Integer.parseInt(value); 82 | if (colorLevel < 0 || colorLevel > 3) { 83 | throw new R2CommandException("Invalid value for scr.color, must be between 0 and 3"); 84 | } 85 | } catch (NumberFormatException e) { 86 | throw new R2CommandException("Invalid value for scr.color, must be between 0 and 3"); 87 | } 88 | } 89 | 90 | // Set the variable 91 | boolean changed = config.set(key, value); 92 | 93 | // Format the result based on the command suffix 94 | if (command.hasSuffix('q')) { 95 | // Quiet output - just return nothing 96 | return ""; 97 | } else if (command.hasSuffix('j')) { 98 | // JSON output 99 | JSONObject json = new JSONObject(); 100 | json.put("key", key); 101 | json.put("value", value); 102 | json.put("changed", changed); 103 | return json.toString() + "\n"; 104 | } else { 105 | // Don't print anything when setting a variable 106 | return ""; 107 | } 108 | } 109 | 110 | /** Handle querying specific variable(s) */ 111 | private String handleQueryVariables(String subcommand, R2EvalConfig config, R2Command command) 112 | throws R2CommandException { 113 | // If the subcommand ends with a dot, treat it as a prefix query 114 | if (subcommand.endsWith(".")) { 115 | String prefix = subcommand; 116 | Map matches = config.getByPrefix(prefix); 117 | 118 | if (matches.isEmpty()) { 119 | return "No matching variables\n"; 120 | } 121 | 122 | return formatEvalOutput(matches, command); 123 | } else { 124 | // Otherwise it's a specific variable query 125 | String key = subcommand; 126 | 127 | if (!config.contains(key)) { 128 | throw new R2CommandException("Unknown eval variable: " + key); 129 | } 130 | 131 | String value = config.get(key); 132 | 133 | // Format the result based on the command suffix 134 | if (command.hasSuffix('q')) { 135 | // Quiet output - just the value 136 | return value + "\n"; 137 | } else if (command.hasSuffix('j')) { 138 | // JSON output 139 | JSONObject json = new JSONObject(); 140 | json.put("key", key); 141 | json.put("value", value); 142 | return json.toString() + "\n"; 143 | } else { 144 | // Standard output 145 | return key + " = " + value + "\n"; 146 | } 147 | } 148 | } 149 | 150 | /** Format the output of eval variables */ 151 | private String formatEvalOutput(Map vars, R2Command command) { 152 | if (command.hasSuffix('j')) { 153 | // JSON output 154 | JSONObject json = new JSONObject(); 155 | JSONArray configs = new JSONArray(); 156 | 157 | for (Map.Entry entry : vars.entrySet()) { 158 | JSONObject configItem = new JSONObject(); 159 | configItem.put("key", entry.getKey()); 160 | configItem.put("value", entry.getValue()); 161 | configs.put(configItem); 162 | } 163 | 164 | json.put("configs", configs); 165 | return json.toString() + "\n"; 166 | } else if (command.hasSuffix('q')) { 167 | // Quiet output - just keys, one per line 168 | StringBuilder sb = new StringBuilder(); 169 | for (String key : vars.keySet()) { 170 | sb.append(key).append("\n"); 171 | } 172 | return sb.toString(); 173 | } else if (command.hasSuffix('*')) { 174 | // R2 commands output 175 | StringBuilder sb = new StringBuilder(); 176 | for (Map.Entry entry : vars.entrySet()) { 177 | sb.append("e ").append(entry.getKey()).append("=").append(entry.getValue()).append("\n"); 178 | } 179 | return sb.toString(); 180 | } else { 181 | // Standard output 182 | StringBuilder sb = new StringBuilder(); 183 | int maxKeyLength = 0; 184 | 185 | // Find the longest key for nice alignment 186 | for (String key : vars.keySet()) { 187 | maxKeyLength = Math.max(maxKeyLength, key.length()); 188 | } 189 | 190 | // Format the output 191 | for (Map.Entry entry : vars.entrySet()) { 192 | sb.append(entry.getKey()); 193 | 194 | // Pad to align the values 195 | int padding = maxKeyLength - entry.getKey().length() + 2; 196 | for (int i = 0; i < padding; i++) { 197 | sb.append(' '); 198 | } 199 | 200 | sb.append("= ").append(entry.getValue()).append("\n"); 201 | } 202 | 203 | return sb.toString(); 204 | } 205 | } 206 | 207 | @Override 208 | public String getHelp() { 209 | StringBuilder sb = new StringBuilder(); 210 | sb.append("Usage: e[*jq] [key[=value]]\n"); 211 | sb.append(" e list all eval configuration variables\n"); 212 | sb.append(" e key get value of configuration variable\n"); 213 | sb.append(" e key=value set value of configuration variable\n"); 214 | sb.append(" e. list all eval vars matching a prefix\n"); 215 | sb.append(" e* list all eval vars as r2 commands\n"); 216 | sb.append(" ej list all eval vars in JSON format\n"); 217 | sb.append(" eq list only variable names, one per line\n\n"); 218 | /* 219 | sb.append("Available variables:\n"); 220 | sb.append(" asm.arch set architecture (x86, arm, etc.)\n"); 221 | sb.append(" asm.bits set architecture bits (8, 16, 32, 64)\n"); 222 | sb.append(" asm.cpu set CPU variant (pentium, cortex, etc.)\n"); 223 | sb.append(" asm.bytes set bytes per instruction for display\n"); 224 | sb.append(" cfg.bigendian set big endian (true/false)\n"); 225 | sb.append(" cfg.endian set endian (big/little/middle)\n"); 226 | sb.append(" cfg.sandbox enable sandbox mode (true/false)\n"); 227 | sb.append(" cfg.sandbox.grain list of sandboxed resources\n"); 228 | sb.append(" r4g.seek.follow follow seek address in Code Viewer (true/false)\n"); 229 | sb.append(" r4g.location.follow follow location of Code Viewer in seek address (true/false)\n"); 230 | sb.append(" scr.color set color level (0-3)\n"); 231 | sb.append(" scr.prompt show prompt (true/false)\n"); 232 | sb.append(" dir.tmp set temporary directory\n"); 233 | sb.append(" http.port set HTTP server port\n"); 234 | sb.append(" io.cache enable I/O caching (true/false)\n"); 235 | */ 236 | return sb.toString(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/config/R2EvalConfig.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.config; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Set; 6 | import java.util.TreeMap; 7 | import r4ghidra.repl.R2Context; 8 | 9 | /** 10 | * Configuration management for R2 eval variables 11 | * 12 | *

This class manages the configuration variables for the R2 environment, similar to how r2's "e" 13 | * command works. It provides variable storage, type conversion, change listeners, and default 14 | * values. 15 | */ 16 | public class R2EvalConfig { 17 | 18 | // The parent context 19 | private R2Context context; 20 | 21 | // Map of configuration variables 22 | private Map config; 23 | 24 | // Map of variable change listeners 25 | private Map listeners; 26 | 27 | // Lock status - when true, config options cannot be created 28 | private boolean locked = false; 29 | 30 | /** 31 | * Create a new configuration manager 32 | * 33 | * @param context The R2 context to associate with 34 | */ 35 | public R2EvalConfig(R2Context context) { 36 | this.context = context; 37 | this.config = new HashMap<>(); 38 | this.listeners = new HashMap<>(); 39 | 40 | // Initialize with default values 41 | initDefaults(); 42 | } 43 | 44 | /** Set default configuration values */ 45 | private void initDefaults() { 46 | // Architecture and assembly settings 47 | set("asm.arch", "x86", false); 48 | set("asm.bits", "32", false); 49 | set("asm.cpu", "default", false); 50 | set("asm.bytes", "16", false); 51 | set("asm.ucase", "false", false); 52 | 53 | // Configuration settings 54 | set("cfg.bigendian", "false", false); 55 | set("cfg.endian", "little", false); 56 | set("cfg.sandbox", "false", false); 57 | set("cfg.sandbox.grain", "rw", false); 58 | 59 | // Screen settings 60 | set("scr.color", "1", false); 61 | set("scr.prompt", "true", false); 62 | set("scr.font", "ST Mono", false); 63 | set("scr.fontsize", "12", false); 64 | set("scr.follow", "false", false); 65 | 66 | // Directory settings 67 | set("dir.tmp", "/tmp", false); 68 | 69 | // HTTP settings 70 | set("http.port", "8080", false); 71 | 72 | // IO settings 73 | set("io.cache", "false", false); 74 | 75 | // R4Ghidra settings 76 | set("r4g.seek.follow", "true", false); 77 | set("r4g.location.follow", "true", false); 78 | } 79 | 80 | /** 81 | * Register a change listgetener for a variable 82 | * 83 | * @param key The variable name 84 | * @param listener The listener to call when the variable changes 85 | */ 86 | public void registerListener(String key, R2EvalChangeListener listener) { 87 | listeners.put(key, listener); 88 | } 89 | 90 | /** 91 | * Get all configuration keys 92 | * 93 | * @return Set of all configuration keys 94 | */ 95 | public Set getKeys() { 96 | return config.keySet(); 97 | } 98 | 99 | /** 100 | * Get all configuration variables as a sorted map 101 | * 102 | * @return Sorted map of all configuration variables 103 | */ 104 | public Map getAll() { 105 | return new TreeMap<>(config); 106 | } 107 | 108 | /** 109 | * Get all configuration variables that start with a prefix 110 | * 111 | * @param prefix The prefix to match 112 | * @return Map of matching configuration variables 113 | */ 114 | public Map getByPrefix(String prefix) { 115 | Map result = new TreeMap<>(); 116 | 117 | for (Map.Entry entry : config.entrySet()) { 118 | if (entry.getKey().startsWith(prefix)) { 119 | result.put(entry.getKey(), entry.getValue()); 120 | } 121 | } 122 | 123 | return result; 124 | } 125 | 126 | /** 127 | * Set a configuration variable 128 | * 129 | * @param key The variable name 130 | * @param value The value to set 131 | * @return true if the value was changed, false otherwise 132 | */ 133 | public boolean set(String key, String value) { 134 | return set(key, value, true); 135 | } 136 | 137 | /** 138 | * Set a configuration variable with an integer value 139 | * 140 | * @param key The variable name 141 | * @param value The integer value to set 142 | * @return true if the value was changed, false otherwise 143 | */ 144 | public boolean set(String key, int value) { 145 | return set(key, Integer.toString(value)); 146 | } 147 | 148 | /** 149 | * Set a configuration variable with a long (uint64) value 150 | * 151 | * @param key The variable name 152 | * @param value The long value to set 153 | * @return true if the value was changed, false otherwise 154 | */ 155 | public boolean set(String key, long value) { 156 | return set(key, Long.toString(value)); 157 | } 158 | 159 | /** 160 | * Set a configuration variable with a boolean value 161 | * 162 | * @param key The variable name 163 | * @param value The boolean value to set 164 | * @return true if the value was changed, false otherwise 165 | */ 166 | public boolean set(String key, boolean value) { 167 | return set(key, value ? "true" : "false"); 168 | } 169 | 170 | /** 171 | * Set a configuration variable with option to trigger listeners 172 | * 173 | * @param key The variable name 174 | * @param value The value to set 175 | * @param triggerListeners Whether to trigger change listeners 176 | * @return true if the value was changed, false otherwise 177 | */ 178 | public boolean set(String key, String value, boolean triggerListeners) { 179 | // Normalize key 180 | key = key.trim().toLowerCase(); 181 | 182 | // If configuration is locked and this is a new key, deny the operation 183 | if (locked && !config.containsKey(key)) { 184 | return false; // Configuration is locked, can't create new keys 185 | } 186 | // Check if the value is actually changing 187 | String oldValue = config.get(key); 188 | if (value.equals(oldValue)) { 189 | return false; // No change 190 | } 191 | 192 | // Update the value 193 | config.put(key, value); 194 | 195 | // Trigger change listener if applicable 196 | if (triggerListeners && listeners.containsKey(key)) { 197 | listeners.get(key).onChange(key, oldValue, value); 198 | } 199 | 200 | return true; 201 | } 202 | 203 | /** 204 | * Get a configuration variable 205 | * 206 | * @param key The variable name 207 | * @return The value, or empty string if not found 208 | */ 209 | public String get(String key) { 210 | return config.getOrDefault(key.trim().toLowerCase(), ""); 211 | } 212 | 213 | /** 214 | * Check if a configuration variable exists 215 | * 216 | * @param key The variable name 217 | * @return true if the variable exists, false otherwise 218 | */ 219 | public boolean contains(String key) { 220 | return config.containsKey(key.trim().toLowerCase()); 221 | } 222 | 223 | /** 224 | * Get a configuration variable as a boolean 225 | * 226 | * @param key The variable name 227 | * @return The boolean value, or false if not a valid boolean 228 | */ 229 | public boolean getBoolean(String key) { 230 | String value = get(key); 231 | 232 | // Check for true/false 233 | if (value.equalsIgnoreCase("true") 234 | || value.equals("1") 235 | || value.equalsIgnoreCase("yes") 236 | || value.equalsIgnoreCase("on") 237 | || value.equalsIgnoreCase("y")) { 238 | return true; 239 | } 240 | 241 | // Check for numeric values > 0 242 | try { 243 | int numValue = Integer.parseInt(value); 244 | if (numValue > 0) { 245 | return true; 246 | } 247 | } catch (NumberFormatException e) { 248 | // Not a number, ignore and continue 249 | } 250 | 251 | // Everything else is false 252 | return false; 253 | } 254 | 255 | /** 256 | * Get a configuration variable as a boolean 257 | * 258 | * @param key The variable name 259 | * @param defaultValue The default value to return if the key doesn't exist or isn't a valid 260 | * boolean 261 | * @return The boolean value, or the default value if not a valid boolean 262 | */ 263 | public boolean getBool(String key, boolean defaultValue) { 264 | if (!contains(key)) { 265 | return defaultValue; 266 | } 267 | return getBoolean(key); 268 | } 269 | 270 | /** 271 | * Get a configuration variable as an integer 272 | * 273 | * @param key The variable name 274 | * @return The integer value, or 0 if not a valid integer 275 | */ 276 | public int getInt(String key) { 277 | try { 278 | return Integer.parseInt(get(key)); 279 | } catch (NumberFormatException e) { 280 | return 0; 281 | } 282 | } 283 | 284 | /** 285 | * Get a configuration variable as a long 286 | * 287 | * @param key The variable name 288 | * @return The long value, or 0 if not a valid long 289 | */ 290 | public long getLong(String key) { 291 | try { 292 | return Long.parseLong(get(key)); 293 | } catch (NumberFormatException e) { 294 | return 0; 295 | } 296 | } 297 | 298 | /** 299 | * Lock the configuration to prevent creation of new keys Only existing keys can be modified after 300 | * locking 301 | */ 302 | public void lock() { 303 | this.locked = true; 304 | } 305 | 306 | /** 307 | * Unlock the configuration to allow creation of new keys This should only be called by plugins or 308 | * extensions 309 | */ 310 | public void unlock() { 311 | this.locked = false; 312 | } 313 | 314 | /** 315 | * Check if the configuration is locked 316 | * 317 | * @return true if locked, false if unlocked 318 | */ 319 | public boolean isLocked() { 320 | return this.locked; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/R2Command.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl; 2 | 3 | import ghidra.program.model.address.Address; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | /** 8 | * Represents a parsed radare2 command 9 | * 10 | *

This class encapsulates all the components of a parsed r2 command, including: - Command prefix 11 | * (first character) - Subcommand (remaining characters before any space) - Arguments (parsed with 12 | * proper handling of quoted strings) - Temporary address for @ syntax 13 | */ 14 | public class R2Command { 15 | private String prefix; 16 | private String subcommand; 17 | private List arguments; 18 | private Address temporaryAddress; 19 | private String multiAddressInfo; // For @@ command syntax 20 | 21 | /** 22 | * Create a new R2Command 23 | * 24 | * @param prefix The command prefix (first character) 25 | * @param subcommand The subcommand (remaining characters) 26 | * @param arguments The parsed arguments 27 | * @param temporaryAddress The temporary address from @ syntax, or null if not present 28 | */ 29 | public R2Command( 30 | String prefix, String subcommand, List arguments, Address temporaryAddress) { 31 | this.prefix = prefix; 32 | this.subcommand = subcommand; 33 | this.arguments = arguments != null ? arguments : Collections.emptyList(); 34 | this.temporaryAddress = temporaryAddress; 35 | this.multiAddressInfo = null; 36 | } 37 | 38 | /** 39 | * Get the command prefix (first character of the command) 40 | * 41 | * @return The command prefix as a string 42 | */ 43 | public String getPrefix() { 44 | return prefix; 45 | } 46 | 47 | /** 48 | * Get the subcommand (everything after the prefix and before any space) 49 | * 50 | * @return The subcommand as a string 51 | */ 52 | public String getSubcommand() { 53 | return subcommand; 54 | } 55 | 56 | /** 57 | * Get the command suffix, which is a special character at the end of the subcommand that 58 | * determines the output format. Returns null if no special suffix is present. 59 | * 60 | *

Common suffixes in radare2: - 'j': JSON output - '*': radare2 commands output - ',': 61 | * CSV/table output - '?': help/documentation - 'q': quiet output - '?*': recursive help 62 | * documentation 63 | * 64 | * @return The command suffix character, or null if none 65 | */ 66 | public Character getCommandSuffix() { 67 | // If no subcommand, return default suffix '\0' 68 | if (subcommand == null || subcommand.isEmpty()) { 69 | return Character.valueOf((char) 0); 70 | } 71 | // Special case for "?*" suffix (recursive help) 72 | if (subcommand.endsWith("?*")) { 73 | return Character.valueOf('*'); // Return '*' for recursive help 74 | } 75 | // Last character of subcommand 76 | char lastChar = subcommand.charAt(subcommand.length() - 1); 77 | // Check if the last character is one of the special suffixes 78 | if (lastChar == 'j' 79 | || lastChar == '*' 80 | || lastChar == ',' 81 | || lastChar == '?' 82 | || lastChar == 'q') { 83 | return Character.valueOf(lastChar); 84 | } 85 | // Default: no suffix, return '\0' 86 | return Character.valueOf((char) 0); 87 | } 88 | 89 | /** 90 | * Get the subcommand without any special suffix character 91 | * 92 | * @return The subcommand with any suffix character removed 93 | */ 94 | public String getSubcommandWithoutSuffix() { 95 | // Special case for "?*" suffix 96 | if (subcommand != null && subcommand.endsWith("?*")) { 97 | return subcommand.substring(0, subcommand.length() - 2); 98 | } 99 | 100 | Character suffix = getCommandSuffix(); 101 | // If no suffix (zero char), return original subcommand 102 | if (suffix.charValue() == (char) 0) { 103 | return subcommand; 104 | } 105 | // Strip one character suffix 106 | return subcommand.substring(0, subcommand.length() - 1); 107 | } 108 | 109 | /** 110 | * Check if the command has a specific suffix 111 | * 112 | * @param suffix The suffix character to check for 113 | * @return true if the command has this suffix, false otherwise 114 | */ 115 | public boolean hasSuffix(char suffix) { 116 | Character commandSuffix = getCommandSuffix(); 117 | return commandSuffix != null && commandSuffix == suffix; 118 | } 119 | 120 | /** 121 | * Check if the command has the recursive help suffix (?*) 122 | * 123 | * @return true if the command has the recursive help suffix, false otherwise 124 | */ 125 | public boolean hasRecursiveHelpSuffix() { 126 | return subcommand != null && subcommand.endsWith("?*"); 127 | } 128 | 129 | /** 130 | * Get all arguments as a list 131 | * 132 | * @return An unmodifiable list of command arguments 133 | */ 134 | public List getArguments() { 135 | return Collections.unmodifiableList(arguments); 136 | } 137 | 138 | /** 139 | * Get a specific argument by index, or defaultValue if the index is out of range 140 | * 141 | * @param index The index of the argument to retrieve 142 | * @param defaultValue The value to return if the index is out of range 143 | * @return The argument at the specified index or the default value 144 | */ 145 | public String getArgument(int index, String defaultValue) { 146 | if (index >= 0 && index < arguments.size()) { 147 | return arguments.get(index); 148 | } 149 | return defaultValue; 150 | } 151 | 152 | /** 153 | * Get the first argument, or defaultValue if there are no arguments 154 | * 155 | * @param defaultValue The value to return if there are no arguments 156 | * @return The first argument or the default value 157 | */ 158 | public String getFirstArgument(String defaultValue) { 159 | return getArgument(0, defaultValue); 160 | } 161 | 162 | /** 163 | * Get the number of arguments 164 | * 165 | * @return The number of arguments 166 | */ 167 | public int getArgumentCount() { 168 | return arguments.size(); 169 | } 170 | 171 | /** 172 | * Check if this command has a temporary address specified via @ syntax 173 | * 174 | * @return True if a temporary address is specified, false otherwise 175 | */ 176 | public boolean hasTemporaryAddress() { 177 | return temporaryAddress != null; 178 | } 179 | 180 | /** 181 | * Get the temporary address specified via @ syntax 182 | * 183 | * @return The temporary address or null if not specified 184 | */ 185 | public Address getTemporaryAddress() { 186 | return temporaryAddress; 187 | } 188 | 189 | /** 190 | * Check if this command matches the given prefix 191 | * 192 | * @param prefix The prefix to check against 193 | * @return True if the command has the specified prefix, false otherwise 194 | */ 195 | public boolean hasPrefix(String prefix) { 196 | return this.prefix.equals(prefix); 197 | } 198 | 199 | /** 200 | * Check if this command matches the given prefix and subcommand 201 | * 202 | * @param prefix The prefix to check against 203 | * @param subcommand The subcommand to check against 204 | * @return True if both the prefix and subcommand match, false otherwise 205 | */ 206 | public boolean matches(String prefix, String subcommand) { 207 | return this.prefix.equals(prefix) && this.subcommand.equals(subcommand); 208 | } 209 | 210 | /** 211 | * Check if the subcommand starts with the given string 212 | * 213 | * @param str The string to check against 214 | * @return True if the subcommand starts with the specified string, false otherwise 215 | */ 216 | public boolean subcommandStartsWith(String str) { 217 | return subcommand.startsWith(str); 218 | } 219 | 220 | /** 221 | * Check if this command uses the @@ syntax for multiple addresses 222 | * 223 | * @return True if this command uses multiple address syntax, false otherwise 224 | */ 225 | public boolean hasMultiAddressInfo() { 226 | return multiAddressInfo != null && !multiAddressInfo.isEmpty(); 227 | } 228 | 229 | /** 230 | * Get the multi-address information (part after @@) for this command 231 | * 232 | * @return The multi-address information string or null if not specified 233 | */ 234 | public String getMultiAddressInfo() { 235 | return multiAddressInfo; 236 | } 237 | 238 | /** 239 | * Set the multi-address information for this command 240 | * 241 | * @param info The multi-address information to set 242 | */ 243 | public void setMultiAddressInfo(String info) { 244 | this.multiAddressInfo = info; 245 | } 246 | 247 | /** Create a string representation of this command */ 248 | @Override 249 | public String toString() { 250 | StringBuilder sb = new StringBuilder(); 251 | sb.append(prefix).append(subcommand); 252 | 253 | for (String arg : arguments) { 254 | sb.append(" "); 255 | // Add quotes if the argument contains spaces 256 | if (arg.contains(" ")) { 257 | sb.append("\"").append(arg).append("\""); 258 | } else { 259 | sb.append(arg); 260 | } 261 | } 262 | 263 | if (temporaryAddress != null) { 264 | sb.append(" @").append(temporaryAddress.toString()); 265 | } 266 | 267 | return sb.toString(); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2SeekCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import ghidra.program.model.address.Address; 4 | import ghidra.program.model.listing.Function; 5 | import ghidra.program.util.ProgramLocation; 6 | import org.json.JSONObject; 7 | import r4ghidra.R4GhidraState; 8 | import r4ghidra.repl.R2Command; 9 | import r4ghidra.repl.R2CommandException; 10 | import r4ghidra.repl.R2CommandHandler; 11 | import r4ghidra.repl.R2Context; 12 | import r4ghidra.repl.num.R2NumException; 13 | import r4ghidra.repl.num.R2NumUtil; 14 | 15 | import java.util.List; 16 | 17 | /** Handler for the 's' (seek) command */ 18 | public class R2SeekCommandHandler implements R2CommandHandler { 19 | 20 | @Override 21 | public String execute(R2Command command, R2Context context) throws R2CommandException { 22 | // Check if it's an 's' command 23 | if (!command.hasPrefix("s")) { 24 | throw new R2CommandException("Not a seek command"); 25 | } 26 | 27 | // Handle the various forms of seek command 28 | String subcommand = command.getSubcommandWithoutSuffix(); 29 | 30 | // Simple 's' with no subcommand - just print current address 31 | if (subcommand.isEmpty() && command.getArgumentCount() == 0) { 32 | Address currentAddr = context.getCurrentAddress(); 33 | return formatResult(currentAddr, context, command); 34 | } 35 | 36 | // 's' with an address argument - set current address 37 | if (subcommand.isEmpty() && command.getArgumentCount() > 0) { 38 | try { 39 | String addrStr = command.getFirstArgument(""); 40 | // Use RNum API to evaluate address expressions 41 | long addrValue = R2NumUtil.evaluateExpression(context, addrStr); 42 | Address newAddr = context.getAPI().toAddr(addrValue); 43 | seekTo(context, newAddr); 44 | return formatResult(newAddr, context, command); 45 | } catch (R2NumException e) { 46 | throw new R2CommandException("Invalid address expression: " + e.getMessage()); 47 | } catch (Exception e) { 48 | throw new R2CommandException("Invalid address: " + command.getFirstArgument("")); 49 | } 50 | } 51 | 52 | // Handle seek subcommands 53 | switch (subcommand) { 54 | // 's..' - seek by replacing lower nibbles 55 | case ".": 56 | if (subcommand.startsWith(".") && subcommand.length() > 1) { 57 | return executeSeekNibblesCommand(command, context); 58 | } 59 | 60 | // 'sb' - seek backward 61 | case "b": 62 | { 63 | try { 64 | String offsetStr = command.getFirstArgument("1"); 65 | // Use RNum API to evaluate offset expressions 66 | long offset = R2NumUtil.evaluateExpression(context, offsetStr); 67 | if (offset <= 0) { 68 | offset = 1; // Default to 1 for non-positive values 69 | } 70 | Address newAddr = context.getCurrentAddress().subtract(offset); 71 | seekTo(context, newAddr); 72 | return formatResult(newAddr, context, command); 73 | } catch (R2NumException e) { 74 | throw new R2CommandException("Invalid offset expression: " + e.getMessage()); 75 | } catch (Exception e) { 76 | throw new R2CommandException("Invalid offset for 'sb' command: " + e.getMessage()); 77 | } 78 | } 79 | 80 | // 'sf' - seek to start of function at current offset (no arguments), or forward by bytes if 81 | // arg supplied 82 | case "f": { 83 | // If no argument, seek to function start 84 | if (command.getArgumentCount() == 0) { 85 | Address current = context.getCurrentAddress(); 86 | if (current == null) { 87 | throw new R2CommandException("Current address is not set"); 88 | } 89 | Function func = context.getAPI().getFunctionContaining(current); 90 | if (func == null) { 91 | throw new R2CommandException("No function found at current address"); 92 | } 93 | Address entry = func.getEntryPoint(); 94 | seekTo(context, entry); 95 | return formatResult(entry, context, command); 96 | } else { 97 | String nameStr = command.getFirstArgument("main"); 98 | List functions = context.getAPI().getGlobalFunctions(nameStr); 99 | if (!functions.isEmpty()) { 100 | Address entry = functions.getFirst().getEntryPoint(); 101 | seekTo(context, entry); 102 | return formatResult(entry, context, command); 103 | } else { 104 | throw new R2CommandException("No function with the given name"); 105 | } 106 | } 107 | } 108 | 109 | // 's-' - seek to previous location 110 | case "-": 111 | context.undoCurrentAddress(); 112 | return context.formatAddress(followCurrentAddress(context)); 113 | 114 | // 's+' - seek to next location 115 | case "+": 116 | context.redoCurrentAddress(); 117 | return context.formatAddress(followCurrentAddress(context)); 118 | 119 | // Other subcommands are not supported 120 | default: 121 | // Check if this is s.. command 122 | if (subcommand.startsWith(".")) { 123 | return executeSeekNibblesCommand(command, context); 124 | } 125 | throw new R2CommandException("Unknown seek subcommand: s" + subcommand); 126 | } 127 | } 128 | 129 | private void seekTo(R2Context context, Address a){ 130 | context.setCurrentAddress(a); 131 | followCurrentAddress(context); 132 | } 133 | 134 | private Address followCurrentAddress(R2Context context){ 135 | Address a = context.getCurrentAddress(); 136 | if (context.getEvalConfig().getBool("r4g.seek.follow", true)) { 137 | R4GhidraState.goToLocation(a); 138 | } 139 | return a; 140 | } 141 | 142 | /** Format the result according to the command suffix */ 143 | private String formatResult(Address address, R2Context context, R2Command command) { 144 | if (command.hasSuffix('j')) { 145 | // JSON output 146 | JSONObject json = new JSONObject(); 147 | json.put("offset", address.getOffset()); 148 | json.put("address", context.formatAddress(address)); 149 | return json.toString() + "\n"; 150 | } else if (command.hasSuffix('q')) { 151 | // Quiet output - just the address with no newline 152 | return context.formatAddress(address); 153 | } else { 154 | // Default output 155 | return context.formatAddress(address) + "\n"; 156 | } 157 | } 158 | 159 | // parseNumericValue method removed as we now use R2NumUtil.evaluateExpression 160 | 161 | /** 162 | * Execute the 's..' command to seek to an address by replacing the lower nibbles This implements 163 | * functionality similar to r_num_tail in radare2 164 | * 165 | * @param command The command object 166 | * @param context The execution context 167 | * @return Formatted result showing the new address 168 | */ 169 | private String executeSeekNibblesCommand(R2Command command, R2Context context) 170 | throws R2CommandException { 171 | // Get the current subcommand (which will start with at least one dot) 172 | String subcommand = command.getSubcommandWithoutSuffix(); 173 | 174 | // Skip any leading dots and spaces 175 | int startIndex = 0; 176 | while (startIndex < subcommand.length() 177 | && (subcommand.charAt(startIndex) == '.' || subcommand.charAt(startIndex) == ' ')) { 178 | startIndex++; 179 | } 180 | 181 | // Extract the hex part 182 | String hexPart = subcommand.substring(startIndex); 183 | 184 | // If there are arguments, use those instead of the subcommand 185 | if (command.getArgumentCount() > 0) { 186 | hexPart = command.getFirstArgument(""); 187 | } 188 | 189 | // Validate we have hex digits 190 | if (hexPart.isEmpty()) { 191 | throw new R2CommandException("Missing hex digits for s.. command"); 192 | } 193 | 194 | // Check if the first character is a valid hex digit 195 | if (!isHexDigit(hexPart.charAt(0))) { 196 | throw new R2CommandException("Invalid hex digits for s.. command"); 197 | } 198 | 199 | try { 200 | // Get the current address 201 | Address currentAddr = context.getCurrentAddress(); 202 | if (currentAddr == null) { 203 | throw new R2CommandException("Current address is not set"); 204 | } 205 | 206 | long currentValue = currentAddr.getOffset(); 207 | 208 | // Calculate new address using tail nibbles 209 | long newAddr = replaceNibbles(currentValue, hexPart); 210 | 211 | // Update current address 212 | Address newAddress = context.getAPI().toAddr(newAddr); 213 | seekTo(context, newAddress); 214 | 215 | // Return formatted result 216 | return formatResult(newAddress, context, command); 217 | } catch (Exception e) { 218 | throw new R2CommandException("Error in s.. command: " + e.getMessage()); 219 | } 220 | } 221 | 222 | /** Check if a character is a valid hex digit */ 223 | private boolean isHexDigit(char c) { 224 | return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); 225 | } 226 | 227 | /** 228 | * Replace lower nibbles of an address with the specified hex digits 229 | * 230 | * @param addr The original address 231 | * @param hex The hex string to use for replacement 232 | * @return The new address value 233 | */ 234 | private long replaceNibbles(long addr, String hex) { 235 | // Calculate the number of nibbles (4 bits each) to replace 236 | int nibbleCount = hex.length(); 237 | 238 | // Create a mask where the upper bits are preserved and lower bits are replaced 239 | long mask = ~0L << (nibbleCount * 4); // equivalent to UT64_MAX << i in C 240 | 241 | // Parse the hex value 242 | long hexValue = Long.parseLong(hex, 16); 243 | 244 | // Combine the preserved upper bits and the new lower bits 245 | return (addr & mask) | hexValue; 246 | } 247 | 248 | @Override 249 | public String getHelp() { 250 | StringBuilder sb = new StringBuilder(); 251 | sb.append("Usage: s[bfpm][j,q] [addr]\n"); 252 | sb.append(" s show current address\n"); 253 | sb.append(" s [addr] seek to address\n"); 254 | sb.append(" s..32a8 seek to same address but replacing the lower nibbles\n"); 255 | sb.append(" sb [delta] seek backward delta bytes\n"); 256 | sb.append(" sf seek to start of current function\n"); 257 | sb.append(" sf [delta] seek forward delta bytes\n"); 258 | sb.append(" s- / s+ seek to previous/next location\n"); 259 | sb.append(" sj show current address as JSON\n"); 260 | sb.append(" sq show current address (quiet mode)\n"); 261 | return sb.toString(); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/handlers/R2FlagCommandHandler.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl.handlers; 2 | 3 | import java.util.Map; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | import org.json.JSONArray; 7 | import org.json.JSONObject; 8 | import r4ghidra.repl.R2Command; 9 | import r4ghidra.repl.R2CommandException; 10 | import r4ghidra.repl.R2CommandHandler; 11 | import r4ghidra.repl.R2Context; 12 | 13 | /** 14 | * Handler for the 'f' (flag) command 15 | * 16 | *

This command allows users to manage flags (bookmarks) in the code. Flags are a way to name 17 | * addresses in radare2. 18 | */ 19 | public class R2FlagCommandHandler implements R2CommandHandler { 20 | 21 | // Pattern for flag set with address (f name=0x123) 22 | private static final Pattern FLAG_SET_ADDR_PATTERN = Pattern.compile("([a-zA-Z0-9._-]+)=(.+)"); 23 | 24 | // Pattern for flag definition with size and address (f name size addr) 25 | private static final Pattern FLAG_DEF_PATTERN = 26 | Pattern.compile("([a-zA-Z0-9._-]+)\\s+(\\d+)\\s+(.+)"); 27 | 28 | // Pattern for flag delete (f-name) 29 | private static final Pattern FLAG_DELETE_PATTERN = Pattern.compile("-(.+)"); 30 | 31 | @Override 32 | public String execute(R2Command command, R2Context context) throws R2CommandException { 33 | // Check if this is an 'f' command 34 | if (!command.hasPrefix("f")) { 35 | throw new R2CommandException("Not a flag command"); 36 | } 37 | 38 | String subcommand = command.getSubcommandWithoutSuffix().trim(); 39 | 40 | // Handle flagspace commands (fs) 41 | if (command.hasPrefix("fs")) { 42 | return handleFlagspace(command, context); 43 | } 44 | 45 | // List all flags when no subcommand is provided 46 | if (subcommand.isEmpty() && command.getArgumentCount() == 0) { 47 | return listFlags(context, command); 48 | } 49 | 50 | // Flag deletion (f-name) 51 | Matcher deleteMatcher = FLAG_DELETE_PATTERN.matcher(subcommand); 52 | if (deleteMatcher.matches()) { 53 | String flagName = deleteMatcher.group(1); 54 | boolean success = context.deleteFlag(flagName); 55 | if (!success) { 56 | throw new R2CommandException("Flag '" + flagName + "' not found"); 57 | } 58 | return ""; // Silent success 59 | } 60 | 61 | // Check for flag definition with size and address (f name size addr) 62 | // This needs to come before the name=addr pattern to correctly handle the size syntax 63 | if (command.getArgumentCount() >= 2) { 64 | String flagName = command.getFirstArgument(""); 65 | String sizeStr = command.getArgument(1, ""); 66 | String addrStr = command.getArgument(2, ""); 67 | 68 | if (!flagName.isEmpty() && !sizeStr.isEmpty() && !addrStr.isEmpty()) { 69 | try { 70 | int size = Integer.parseInt(sizeStr); 71 | long addr = context.parseAddress(addrStr).getOffset(); 72 | boolean success = context.setFlag(flagName, addr, size); 73 | if (!success) { 74 | throw new R2CommandException("Failed to set flag '" + flagName + "'"); 75 | } 76 | return ""; // Silent success 77 | } catch (NumberFormatException e) { 78 | throw new R2CommandException("Invalid flag size: " + sizeStr); 79 | } catch (Exception e) { 80 | throw new R2CommandException("Invalid address: " + addrStr); 81 | } 82 | } 83 | } 84 | 85 | // Flag creation with specific address (f name=0x123) 86 | Matcher addrMatcher = FLAG_SET_ADDR_PATTERN.matcher(subcommand); 87 | if (addrMatcher.matches()) { 88 | String flagName = addrMatcher.group(1); 89 | String addrStr = addrMatcher.group(2); 90 | 91 | try { 92 | // Parse the address using the context's expression evaluator 93 | long addr = context.parseAddress(addrStr).getOffset(); 94 | boolean success = context.setFlag(flagName, addr); 95 | if (!success) { 96 | throw new R2CommandException("Failed to set flag '" + flagName + "'"); 97 | } 98 | return ""; // Silent success 99 | } catch (Exception e) { 100 | throw new R2CommandException("Invalid address: " + addrStr); 101 | } 102 | } 103 | 104 | // Check for flag definition with size and address (f name size addr) 105 | Matcher defMatcher = FLAG_DEF_PATTERN.matcher(subcommand); 106 | if (defMatcher.matches()) { 107 | String flagName = defMatcher.group(1); 108 | int size; 109 | try { 110 | size = Integer.parseInt(defMatcher.group(2)); 111 | } catch (NumberFormatException e) { 112 | throw new R2CommandException("Invalid flag size: " + defMatcher.group(2)); 113 | } 114 | 115 | String addrStr = defMatcher.group(3); 116 | try { 117 | // Parse the address using the context's expression evaluator 118 | long addr = context.parseAddress(addrStr).getOffset(); 119 | boolean success = context.setFlag(flagName, addr, size); 120 | if (!success) { 121 | throw new R2CommandException("Failed to set flag '" + flagName + "'"); 122 | } 123 | return ""; // Silent success 124 | } catch (Exception e) { 125 | throw new R2CommandException("Invalid address: " + addrStr); 126 | } 127 | } 128 | 129 | // Flag creation at current address (f name) 130 | if (!subcommand.isEmpty()) { 131 | boolean success = context.setFlag(subcommand); 132 | if (!success) { 133 | throw new R2CommandException("Failed to set flag '" + subcommand + "'"); 134 | } 135 | return ""; // Silent success 136 | } 137 | 138 | // If we get here, it's an unknown subcommand 139 | throw new R2CommandException("Unknown flag command"); 140 | } 141 | 142 | /** Handle flagspace commands (fs) */ 143 | private String handleFlagspace(R2Command command, R2Context context) throws R2CommandException { 144 | String subcommand = command.getSubcommandWithoutSuffix().trim(); 145 | 146 | // List all flagspaces (fs) 147 | if (subcommand.isEmpty() && command.getArgumentCount() == 0) { 148 | return listFlagspaces(context, command); 149 | } 150 | 151 | // Reset flagspace (fs *) 152 | if (subcommand.equals("*") 153 | || (command.getArgumentCount() > 0 && command.getFirstArgument("").equals("*"))) { 154 | context.setFlagspace("*"); 155 | return ""; // Silent success 156 | } 157 | 158 | // Set flagspace (fs name) 159 | String flagspace = subcommand.isEmpty() ? command.getFirstArgument("") : subcommand; 160 | context.setFlagspace(flagspace); 161 | return ""; // Silent success 162 | } 163 | 164 | /** List all flags */ 165 | private String listFlags(R2Context context, R2Command command) { 166 | Map flags = context.getFlags(); 167 | 168 | // JSON output 169 | if (command.hasSuffix('j')) { 170 | JSONArray jsonFlags = new JSONArray(); 171 | for (Map.Entry entry : flags.entrySet()) { 172 | JSONObject flag = new JSONObject(); 173 | flag.put("name", entry.getKey()); 174 | flag.put("offset", entry.getValue()); 175 | flag.put("address", context.formatAddress(entry.getValue())); 176 | flag.put("size", context.getFlagSize(entry.getKey())); // Include flag size 177 | jsonFlags.put(flag); 178 | } 179 | return jsonFlags.toString() + "\n"; 180 | } 181 | // R2 commands output 182 | else if (command.hasSuffix('*')) { 183 | StringBuilder sb = new StringBuilder(); 184 | for (Map.Entry entry : flags.entrySet()) { 185 | String flagName = entry.getKey(); 186 | sb.append("f ") 187 | .append(flagName) 188 | .append(" ") 189 | .append(context.getFlagSize(flagName)) 190 | .append(" ") 191 | .append(context.formatAddress(entry.getValue())) 192 | .append("\n"); 193 | } 194 | return sb.toString(); 195 | } 196 | // Standard output 197 | else { 198 | if (flags.isEmpty()) { 199 | return "No flags defined\n"; 200 | } 201 | 202 | StringBuilder sb = new StringBuilder(); 203 | int maxNameLength = 0; 204 | 205 | // Find the longest name for nice alignment 206 | for (String name : flags.keySet()) { 207 | maxNameLength = Math.max(maxNameLength, name.length()); 208 | } 209 | 210 | // Format the output 211 | for (Map.Entry entry : flags.entrySet()) { 212 | String flagName = entry.getKey(); 213 | sb.append(context.formatAddress(entry.getValue())); 214 | sb.append(" "); 215 | sb.append(String.format("%3d", context.getFlagSize(flagName))); // Display size 216 | sb.append(" "); 217 | sb.append(flagName); 218 | sb.append("\n"); 219 | } 220 | 221 | return sb.toString(); 222 | } 223 | } 224 | 225 | /** List all flagspaces */ 226 | private String listFlagspaces(R2Context context, R2Command command) { 227 | String[] flagspaces = context.getFlagspaces(); 228 | String currentFlagspace = context.getCurrentFlagspace(); 229 | 230 | // JSON output 231 | if (command.hasSuffix('j')) { 232 | JSONObject json = new JSONObject(); 233 | json.put("selected", currentFlagspace); 234 | 235 | JSONArray spaces = new JSONArray(); 236 | for (String fs : flagspaces) { 237 | spaces.put(fs); 238 | } 239 | json.put("spaces", spaces); 240 | return json.toString() + "\n"; 241 | } 242 | // R2 commands output 243 | else if (command.hasSuffix('*')) { 244 | StringBuilder sb = new StringBuilder(); 245 | sb.append("fs ").append(currentFlagspace).append("\n"); 246 | return sb.toString(); 247 | } 248 | // Standard output 249 | else { 250 | StringBuilder sb = new StringBuilder(); 251 | for (String fs : flagspaces) { 252 | if (fs.equals(currentFlagspace)) { 253 | sb.append("* "); 254 | } else { 255 | sb.append(" "); 256 | } 257 | sb.append(fs).append("\n"); 258 | } 259 | return sb.toString(); 260 | } 261 | } 262 | 263 | @Override 264 | public String getHelp() { 265 | StringBuilder sb = new StringBuilder(); 266 | sb.append("Usage: f[*j] [name] [@ addr]\n"); 267 | sb.append(" f list flags in current flagspace\n"); 268 | sb.append(" f name set flag at current address\n"); 269 | sb.append(" f name=addr set flag at address\n"); 270 | sb.append(" f name size addr set flag with size at address\n"); 271 | sb.append(" f-name remove flag\n"); 272 | sb.append(" f* list flags in r2 commands\n"); 273 | sb.append(" fj list flags in JSON format\n"); 274 | sb.append("\n"); 275 | sb.append("Flagspace management:\n"); 276 | sb.append(" fs list all flagspaces\n"); 277 | sb.append(" fs * select all flagspaces\n"); 278 | sb.append(" fs name select flagspace\n"); 279 | return sb.toString(); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/R4CommandShellProvider.java: -------------------------------------------------------------------------------- 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 | package r4ghidra; 17 | 18 | import docking.ComponentProvider; 19 | import ghidra.util.HelpLocation; 20 | import java.awt.*; 21 | import java.awt.event.*; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import javax.swing.*; 25 | import javax.swing.border.EmptyBorder; 26 | import javax.swing.text.DefaultCaret; 27 | import r4ghidra.repl.R2REPLImpl; 28 | 29 | /** 30 | * Command shell provider for R4Ghidra. Provides a UI for interacting with the R4Ghidra command 31 | * system directly. 32 | */ 33 | public class R4CommandShellProvider extends ComponentProvider { 34 | 35 | // Flag to track if dialog has been shown 36 | private boolean isDialogShown = false; 37 | 38 | // Make this method public for plugin access 39 | /** Close the command shell provider Removes this component provider from the tool */ 40 | public void close() { 41 | getTool().removeComponentProvider(this); 42 | } 43 | 44 | /** 45 | * Get the REPL context for configuration access 46 | * 47 | * @return The current R2Context instance 48 | */ 49 | public r4ghidra.repl.R2Context getREPLContext() { 50 | return repl != null ? repl.getContext() : null; 51 | } 52 | 53 | /** 54 | * Bring the component to the front and ensure it's visible This method will create a dialog 55 | * window if needed 56 | */ 57 | public void toFront() { 58 | // Show this provider as a dockable component in the Ghidra tool 59 | getTool().showComponentProvider(this, true); 60 | } 61 | 62 | private JPanel mainPanel; 63 | // Font to use for shell UI 64 | private Font shellFont; 65 | private JTextArea outputArea; 66 | private JTextField commandField; 67 | private JButton executeButton; 68 | private R2REPLImpl repl; 69 | private ArrayList commandHistory; 70 | private int historyIndex = -1; 71 | 72 | /** 73 | * Constructor 74 | * 75 | * @param plugin The R4GhidraPlugin that owns this provider 76 | * @param title The title of the component 77 | */ 78 | public R4CommandShellProvider(R4GhidraPlugin plugin, String title) { 79 | super(plugin.getTool(), title, title); 80 | // Add this provider to the Window menu 81 | setWindowMenuGroup("R4Ghidra"); 82 | // Determine font: prefer STMono, fallback to monospaced 83 | String desiredFont = "ST Mono"; 84 | boolean hasDesired = 85 | Arrays.asList( 86 | GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()) 87 | .contains(desiredFont); 88 | String fontName = hasDesired ? desiredFont : Font.MONOSPACED; 89 | // shellFont = new Font(fontName, Font.PLAIN, 12); 90 | shellFont = new Font(Font.MONOSPACED, Font.BOLD, 12); 91 | 92 | repl = new R2REPLImpl(); 93 | commandHistory = new ArrayList<>(); 94 | 95 | // Register the shell provider with the REPL context for font updates 96 | repl.getContext().setShellProvider(this); 97 | 98 | repl.registerCommands(plugin.getCommandHandlers()); 99 | buildPanel(); 100 | setHelpLocation(new HelpLocation("R4Ghidra", "CommandShell")); 101 | } 102 | 103 | /** Builds the UI panel with output area and command input field */ 104 | private void buildPanel() { 105 | mainPanel = new JPanel(new BorderLayout(0, 5)); 106 | mainPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); 107 | 108 | // Create the output area (top row) 109 | outputArea = new JTextArea(); 110 | outputArea.setEditable(false); 111 | outputArea.setFont(shellFont); 112 | outputArea.setBackground(Color.BLACK); 113 | outputArea.setForeground(Color.WHITE); 114 | outputArea.setText("R4Ghidra Command Shell\nType commands and press Enter to execute.\n\n"); 115 | 116 | // Auto-scroll to bottom for new content 117 | DefaultCaret caret = (DefaultCaret) outputArea.getCaret(); 118 | caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 119 | 120 | // Add scrollbars to the output area 121 | JScrollPane scrollPane = new JScrollPane(outputArea); 122 | scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); 123 | mainPanel.add(scrollPane, BorderLayout.CENTER); 124 | 125 | // Create the command input panel (bottom row) 126 | JPanel commandPanel = new JPanel(new BorderLayout(5, 0)); 127 | 128 | // Command input field 129 | commandField = new JTextField(); 130 | commandField.setFont(shellFont); 131 | 132 | // Handle Enter key and up/down keys in the command field 133 | commandField.addKeyListener( 134 | new KeyAdapter() { 135 | @Override 136 | public void keyPressed(KeyEvent e) { 137 | if (e.getKeyCode() == KeyEvent.VK_ENTER) { 138 | executeCommand(); 139 | } else if (e.getKeyCode() == KeyEvent.VK_UP 140 | || (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_P)) { 141 | // Up arrow or Ctrl+P: Show previous command 142 | showPreviousCommand(); 143 | } else if (e.getKeyCode() == KeyEvent.VK_DOWN 144 | || (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_N)) { 145 | // Down arrow or Ctrl+N: Show next command 146 | showNextCommand(); 147 | } 148 | } 149 | }); 150 | 151 | // Execute button 152 | executeButton = new JButton("Execute"); 153 | executeButton.addActionListener(e -> executeCommand()); 154 | 155 | // Add components to the command panel 156 | commandPanel.add(commandField, BorderLayout.CENTER); 157 | commandPanel.add(executeButton, BorderLayout.EAST); 158 | 159 | // Add the command panel to the main panel 160 | mainPanel.add(commandPanel, BorderLayout.SOUTH); 161 | } 162 | 163 | /** Execute the command in the command field */ 164 | private void executeCommand() { 165 | String command = commandField.getText().trim(); 166 | if (command.isEmpty()) { 167 | return; 168 | } 169 | 170 | // Add the command to the output area 171 | outputArea.append("> " + command + "\n"); 172 | 173 | // Execute the command 174 | String result = repl.executeCommand(command); 175 | 176 | // Display the result 177 | outputArea.append(result + "\n"); 178 | // Scroll output to bottom 179 | outputArea.setCaretPosition(outputArea.getDocument().getLength()); 180 | 181 | // Add the command to history if it's not empty and not a duplicate of the last command 182 | if (!command.isEmpty()) { 183 | if (commandHistory.isEmpty() 184 | || !command.equals(commandHistory.get(commandHistory.size() - 1))) { 185 | commandHistory.add(command); 186 | } 187 | historyIndex = commandHistory.size(); 188 | } 189 | 190 | // Clear the command field 191 | commandField.setText(""); 192 | 193 | // Request focus back to command field 194 | commandField.requestFocusInWindow(); 195 | } 196 | 197 | /** 198 | * Update the font used in the console 199 | * 200 | * @param newFont The new font to use 201 | */ 202 | public void updateFont(Font newFont) { 203 | if (newFont == null) { 204 | return; 205 | } 206 | 207 | this.shellFont = newFont; 208 | 209 | // Update the font on the UI components 210 | if (outputArea != null) { 211 | outputArea.setFont(newFont); 212 | } 213 | 214 | if (commandField != null) { 215 | commandField.setFont(newFont); 216 | } 217 | } 218 | 219 | /** 220 | * Get the tool frame for dialog positioning 221 | * 222 | * @return The JFrame of the tool 223 | */ 224 | public javax.swing.JFrame getToolFrame() { 225 | return (javax.swing.JFrame) SwingUtilities.getWindowAncestor(getComponent()); 226 | } 227 | 228 | /** Clear the output text area This method is called by the clear command handler */ 229 | public void clearOutputArea() { 230 | if (outputArea != null) { 231 | outputArea.setText(""); 232 | } 233 | } 234 | 235 | /** Show the previous command in the history */ 236 | private void showPreviousCommand() { 237 | if (commandHistory.isEmpty()) { 238 | return; 239 | } 240 | 241 | // If we're at the end of the history, save the current text 242 | if (historyIndex == commandHistory.size()) { 243 | String currentText = commandField.getText().trim(); 244 | if (!currentText.isEmpty()) { 245 | // Temporarily store the current unexecuted text 246 | commandHistory.add(currentText); 247 | // But we'll remove it once we execute a command or leave the field 248 | } 249 | } 250 | 251 | if (historyIndex > 0) { 252 | historyIndex--; 253 | commandField.setText(commandHistory.get(historyIndex)); 254 | // Position cursor at end of text 255 | commandField.setCaretPosition(commandField.getText().length()); 256 | } 257 | } 258 | 259 | /** Show the next command in the history */ 260 | private void showNextCommand() { 261 | if (commandHistory.isEmpty() || historyIndex >= commandHistory.size() - 1) { 262 | // At the end of history, clear the field 263 | if (historyIndex == commandHistory.size() - 1) { 264 | historyIndex = commandHistory.size(); 265 | commandField.setText(""); 266 | } 267 | return; 268 | } 269 | 270 | historyIndex++; 271 | commandField.setText(commandHistory.get(historyIndex)); 272 | // Position cursor at end of text 273 | commandField.setCaretPosition(commandField.getText().length()); 274 | } 275 | 276 | @Override 277 | public JComponent getComponent() { 278 | return mainPanel; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /R4Ghidra/src/main/java/r4ghidra/repl/R2OutputFilter.java: -------------------------------------------------------------------------------- 1 | package r4ghidra.repl; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | 11 | /** 12 | * Implements radare2 output filtering functionality 13 | * 14 | *

This class handles the various ~ filter modifiers for radare2 commands: - ~pattern - Grep 15 | * filter, only shows lines matching the pattern - ~pattern1,pattern2 - Grep filter with multiple 16 | * patterns (OR logic) - ~&pattern1,pattern2 - Grep filter with multiple patterns (AND logic) - ~{} 17 | * - Pretty-prints JSON output - ~? - Counts lines in output (like wc -l) 18 | */ 19 | public class R2OutputFilter { 20 | 21 | // Pattern for detecting filter expressions 22 | private static final Pattern FILTER_PATTERN = 23 | Pattern.compile( 24 | "(.*?)~(&?|!)(\\{\\}|\\?|([^\\s\\[]+)(\\[(\\d+(?:,\\d+)*)\\])?|\\[(\\d+(?:,\\d+)*)\\])"); 25 | 26 | /** 27 | * Extract command and filter from a command string 28 | * 29 | * @param cmdStr The command string that may contain a filter 30 | * @return An array with [command, filter, andLogic, columns] or null if no filter is present 31 | * Where columns is a comma-separated list of column indices or null if not specified 32 | */ 33 | public static String[] extractCommandAndFilter(String cmdStr) { 34 | if (cmdStr == null || cmdStr.isEmpty()) { 35 | return null; 36 | } 37 | 38 | // Special case for help command 39 | if (cmdStr.equals("~?")) { 40 | return new String[] {"", "?", "false", null}; 41 | } else if (cmdStr.equals("~&?")) { 42 | return new String[] {"", "?", "true", null}; 43 | } 44 | 45 | // Check if the command contains a filter 46 | Matcher matcher = FILTER_PATTERN.matcher(cmdStr); 47 | if (matcher.matches()) { 48 | String command = matcher.group(1).trim(); 49 | String operator = matcher.group(2); // "&", "!" or empty 50 | String filter = matcher.group(3); 51 | boolean isAndLogic = "&".equals(operator); 52 | boolean isNegationLogic = "!".equals(operator); 53 | 54 | // Extract column specification if present 55 | String columns = null; 56 | if (matcher.group(6) != null) { // Pattern with text and brackets: mov[0] 57 | columns = matcher.group(6); 58 | filter = matcher.group(4); // Just the pattern part without brackets 59 | } else if (matcher.group(7) != null) { // Pattern with only brackets: [0] 60 | columns = matcher.group(7); 61 | filter = ""; // No pattern, show all lines but filter columns 62 | } 63 | 64 | return new String[] { 65 | command, filter, String.valueOf(isAndLogic), columns, String.valueOf(isNegationLogic) 66 | }; 67 | } 68 | 69 | return null; 70 | } 71 | 72 | /** 73 | * Apply a filter to command output 74 | * 75 | * @param output The command output to filter 76 | * @param filter The filter to apply 77 | * @param useAndLogic Whether to use AND logic for multiple patterns 78 | * @param columns Column specification (comma-separated list of column indices) or null 79 | * @return The filtered output 80 | */ 81 | /** 82 | * Apply filtering to command output 83 | * 84 | * @param output The command output to filter 85 | * @param filter The filter pattern to apply 86 | * @param useAndLogic Whether to use AND logic for multiple patterns (true) or OR logic (false) 87 | * @param columns The column indices to extract, comma-separated 88 | * @param useNegationLogic Whether to negate the filter pattern matches 89 | * @return The filtered output 90 | */ 91 | public static String applyFilter( 92 | String output, String filter, boolean useAndLogic, String columns, boolean useNegationLogic) { 93 | // Handle empty output 94 | if (output == null || output.isEmpty()) { 95 | return ""; 96 | } 97 | 98 | // Handle empty filter with no columns 99 | if ((filter == null || filter.isEmpty()) && (columns == null || columns.isEmpty())) { 100 | return output; 101 | } 102 | 103 | // Handle line count filter (~? or ~&?) 104 | if (filter.equals("?")) { 105 | return countLines(output); 106 | } 107 | 108 | // Handle JSON pretty print filter (~{} or ~&{}) 109 | if (filter.equals("{}")) { 110 | return prettyPrintJson(output); 111 | } 112 | 113 | // First apply grep filter if there is one 114 | String filteredOutput; 115 | if (filter != null && !filter.isEmpty()) { 116 | filteredOutput = grepLines(output, filter, useAndLogic, useNegationLogic); 117 | } else { 118 | filteredOutput = output; 119 | } 120 | 121 | // Then apply column filter if specified 122 | if (columns != null && !columns.isEmpty()) { 123 | return filterColumns(filteredOutput, columns); 124 | } 125 | 126 | return filteredOutput; 127 | } 128 | 129 | /** 130 | * Apply a filter to command output (backward compatibility) 131 | * 132 | * @param output The command output to filter 133 | * @param filter The filter to apply 134 | * @return The filtered output 135 | */ 136 | public static String applyFilter(String output, String filter) { 137 | // Check if the filter includes the ! operator for negation 138 | boolean useNegation = filter != null && filter.startsWith("!"); 139 | // Strip the ! prefix if present 140 | String actualFilter = useNegation && filter.length() > 1 ? filter.substring(1) : filter; 141 | 142 | return applyFilter( 143 | output, 144 | actualFilter, 145 | false, 146 | null, 147 | useNegation); // Default to OR logic, detected negation, no columns 148 | } 149 | 150 | /** 151 | * Apply a filter to command output (backward compatibility) 152 | * 153 | * @param output The command output to filter 154 | * @param filter The filter to apply 155 | * @param useAndLogic Whether to use AND logic for multiple patterns 156 | * @return The filtered output 157 | */ 158 | public static String applyFilter(String output, String filter, boolean useAndLogic) { 159 | // Check if the filter includes the ! operator for negation 160 | boolean useNegation = filter != null && filter.startsWith("!"); 161 | // Strip the ! prefix if present 162 | String actualFilter = useNegation && filter.length() > 1 ? filter.substring(1) : filter; 163 | 164 | return applyFilter( 165 | output, actualFilter, useAndLogic, null, useNegation); // No columns, detected negation 166 | } 167 | 168 | /** 169 | * Count lines in output 170 | * 171 | * @param output The output to count lines in 172 | * @return The number of lines as a string 173 | */ 174 | private static String countLines(String output) { 175 | // Split by newlines and count non-empty lines 176 | String[] lines = output.split("\n"); 177 | return String.valueOf(lines.length); 178 | } 179 | 180 | /** 181 | * Pretty print JSON output 182 | * 183 | * @param output The JSON output to pretty print 184 | * @return The pretty printed JSON 185 | */ 186 | private static String prettyPrintJson(String output) { 187 | try { 188 | // Try to parse as JSON object 189 | try { 190 | JSONObject jsonObject = new JSONObject(output.trim()); 191 | return jsonObject.toString(2); 192 | } catch (JSONException e) { 193 | // Try to parse as JSON array 194 | JSONArray jsonArray = new JSONArray(output.trim()); 195 | return jsonArray.toString(2); 196 | } 197 | } catch (JSONException e) { 198 | // If not valid JSON, return the original output 199 | return "Error: Invalid JSON format\n" + output; 200 | } 201 | } 202 | 203 | /** 204 | * Grep lines matching one or more patterns 205 | * 206 | * @param output The output to grep 207 | * @param patternStr The pattern(s) to match, comma-separated for multiple patterns 208 | * @param useAndLogic Whether to use AND logic (all patterns must match) instead of OR logic 209 | * @param useNegationLogic Whether to use negation logic (exclude lines that match) 210 | * @return The filtered output 211 | */ 212 | private static String grepLines( 213 | String output, String patternStr, boolean useAndLogic, boolean useNegationLogic) { 214 | String[] lines = output.split("\n"); 215 | List matchedLines = new ArrayList<>(); 216 | 217 | // Check if we have multiple patterns (comma-separated) 218 | String[] patterns = patternStr.split(","); 219 | 220 | // Convert each pattern to a regex pattern 221 | List regexPatterns = new ArrayList<>(); 222 | for (String pattern : patterns) { 223 | regexPatterns.add(Pattern.compile(convertGlobToRegex(pattern))); 224 | } 225 | 226 | // Check each line against all patterns with appropriate logic 227 | for (String line : lines) { 228 | boolean shouldAdd = false; 229 | 230 | if (useAndLogic) { 231 | // AND logic - all patterns must match 232 | boolean allMatch = true; 233 | for (Pattern pattern : regexPatterns) { 234 | Matcher matcher = pattern.matcher(line); 235 | if (!matcher.find()) { 236 | allMatch = false; 237 | break; 238 | } 239 | } 240 | shouldAdd = allMatch; 241 | } else { 242 | // OR logic - at least one pattern must match 243 | boolean anyMatch = false; 244 | for (Pattern pattern : regexPatterns) { 245 | Matcher matcher = pattern.matcher(line); 246 | if (matcher.find()) { 247 | anyMatch = true; 248 | break; 249 | } 250 | } 251 | shouldAdd = anyMatch; 252 | } 253 | 254 | // If using negation logic, invert the result 255 | if (useNegationLogic) { 256 | shouldAdd = !shouldAdd; 257 | } 258 | 259 | if (shouldAdd) { 260 | matchedLines.add(line); 261 | } 262 | } 263 | 264 | // Join matched lines 265 | return String.join("\n", matchedLines); 266 | } 267 | 268 | /** 269 | * Convert a glob pattern to a regex pattern 270 | * 271 | * @param glob The glob pattern 272 | * @return The regex pattern 273 | */ 274 | private static String convertGlobToRegex(String glob) { 275 | StringBuilder regex = new StringBuilder(); 276 | 277 | // Handle common glob patterns 278 | if (glob.startsWith("^")) { 279 | // Beginning of line anchor 280 | regex.append("^"); 281 | glob = glob.substring(1); 282 | } 283 | 284 | if (glob.endsWith("$")) { 285 | // End of line anchor 286 | glob = glob.substring(0, glob.length() - 1); 287 | regex.append(Pattern.quote(glob)).append("$"); 288 | } else { 289 | // Normal case - convert * to .* 290 | String quoted = Pattern.quote(glob); 291 | quoted = quoted.replace("*", "\\E.*\\Q"); 292 | regex.append(quoted); 293 | } 294 | 295 | return regex.toString(); 296 | } 297 | 298 | /** Get help information about filter syntax */ 299 | /** 300 | * Filter specific columns from the output 301 | * 302 | * @param output The output text to filter 303 | * @param columnsSpec Comma-separated list of column indices (0-based) 304 | * @return The filtered output containing only the specified columns 305 | */ 306 | private static String filterColumns(String output, String columnsSpec) { 307 | // Parse column indices 308 | String[] columnIndicesStr = columnsSpec.split(","); 309 | int[] columnIndices = new int[columnIndicesStr.length]; 310 | 311 | for (int i = 0; i < columnIndicesStr.length; i++) { 312 | try { 313 | columnIndices[i] = Integer.parseInt(columnIndicesStr[i]); 314 | } catch (NumberFormatException e) { 315 | // Invalid column index, default to 0 316 | columnIndices[i] = 0; 317 | } 318 | } 319 | 320 | // Split output by lines and process each line 321 | String[] lines = output.split("\n"); 322 | StringBuilder result = new StringBuilder(); 323 | 324 | for (String line : lines) { 325 | if (line.trim().isEmpty()) { 326 | continue; 327 | } 328 | 329 | // Split the line by whitespace 330 | String[] columns = line.trim().split("\\s+"); 331 | 332 | // Extract the specified columns 333 | StringBuilder filteredLine = new StringBuilder(); 334 | boolean first = true; 335 | 336 | for (int colIndex : columnIndices) { 337 | if (colIndex >= 0 && colIndex < columns.length) { 338 | if (!first) { 339 | filteredLine.append(" "); 340 | } 341 | filteredLine.append(columns[colIndex]); 342 | first = false; 343 | } 344 | } 345 | 346 | // Add the filtered line to the result if it's not empty 347 | if (filteredLine.length() > 0) { 348 | result.append(filteredLine).append("\n"); 349 | } 350 | } 351 | 352 | return result.toString(); 353 | } 354 | 355 | /** 356 | * Get help text for the filter syntax 357 | * 358 | * @return A string containing help information about filter syntax 359 | */ 360 | public static String getFilterHelp() { 361 | StringBuilder sb = new StringBuilder(); 362 | sb.append("Output Filter Syntax:\n"); 363 | sb.append(" command~pattern grep: filter lines matching pattern\n"); 364 | sb.append(" command~pattern1,pattern2,... grep: filter lines matching any pattern (OR)\n"); 365 | sb.append(" command~&pattern1,pattern2,... grep: filter lines matching all patterns (AND)\n"); 366 | sb.append( 367 | " command~!pattern grep: filter lines NOT matching pattern (negation)\n"); 368 | sb.append( 369 | " command~!pattern1,pattern2,... grep: filter lines NOT matching any pattern (negated" 370 | + " OR)\n"); 371 | sb.append(" command~pattern[N] column: filter lines and show only column N\n"); 372 | sb.append(" command~[N] column: show only column N from all lines\n"); 373 | sb.append( 374 | " command~pattern[N,M,...] column: show columns N, M, etc. from matching lines\n"); 375 | sb.append(" command~{} json: pretty print JSON output\n"); 376 | sb.append(" command~? count: count number of lines (wc -l)\n"); 377 | sb.append("\nPattern modifiers:\n"); 378 | sb.append(" ^pattern match at start of line\n"); 379 | sb.append(" pattern$ match at end of line\n"); 380 | sb.append(" pat*tern glob-style wildcard matching\n"); 381 | sb.append("\nExamples:\n"); 382 | sb.append(" pd~call,mov show lines containing either 'call' OR 'mov'\n"); 383 | sb.append(" pd~&mov,rax show lines containing both 'mov' AND 'rax'\n"); 384 | sb.append(" pd~!call show lines NOT containing 'call'\n"); 385 | sb.append(" pd~mov[0] show first column of lines containing 'mov'\n"); 386 | sb.append(" afl~[1] show only the second column of function list\n"); 387 | return sb.toString(); 388 | } 389 | } 390 | --------------------------------------------------------------------------------