├── 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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
2 |
3 | ### Radare For Ghidra
4 |
5 | [](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 | #
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 |
--------------------------------------------------------------------------------