├── Module.manifest ├── src └── main │ ├── resources │ ├── _ghidraal_initctx.r │ ├── _ghidraal_initctx.rb │ ├── _ghidraal_initscript.rb │ ├── _ghidraal_initctx.js │ ├── _ghidraal_initscript.js │ ├── images │ │ └── README.txt │ ├── _ghidraal_initscript.py │ ├── _ghidraal_initscript.r │ └── _ghidraal_initctx.py │ └── java │ └── ghidraal │ ├── GhidraalPluginPackage.java │ ├── GhidraalInitializer.java │ ├── LangInfo.java │ ├── langs │ ├── RubyLangInfo.java │ ├── Python3LangInfo.java │ ├── NodeJSLangInfo.java │ └── RscriptLangInfo.java │ ├── GhidraalScriptProviderBase.java │ ├── Util.java │ ├── GhidraalScript.java │ ├── ScriptingContext.java │ ├── GhidraalPlugin.java │ └── GhidraalConsole.java ├── .gitignore ├── extension.properties ├── ghidra_scripts ├── basic.js ├── basic.r ├── basic.py ├── basic.rb ├── import_demo.py ├── import_demo2.py ├── basic_graphics.r └── askscript3.py ├── LICENSE ├── env.sh └── README.md /Module.manifest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initctx.r: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initctx.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initscript.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initctx.js: -------------------------------------------------------------------------------- 1 | const global = (0,eval)("this"); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .classpath 3 | .project 4 | .settings/ 5 | bin/ 6 | build/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initscript.js: -------------------------------------------------------------------------------- 1 | for(x in _ghidra_api) { 2 | global[x]=_ghidra_api[x]; 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /extension.properties: -------------------------------------------------------------------------------- 1 | name=Ghidraal 2 | description=Ghidra GraalVM Scripting Extension 3 | author=Jason P. Leasure 4 | createdOn= 5 | version=@extversion@ 6 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initscript.py: -------------------------------------------------------------------------------- 1 | g=globals() 2 | for m in dir(_ghidra_api): 3 | if m=='print': 4 | continue 5 | if not m in g: 6 | v=getattr(_ghidra_api,m) 7 | g[m]=v 8 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initscript.r: -------------------------------------------------------------------------------- 1 | #attach(_ghidra_api) 2 | 3 | for(n in names(_ghidra_api)) { 4 | if(!exists(n,envir=.GlobalEnv)) 5 | assign(n, _ghidra_api[n] ,envir=.GlobalEnv); 6 | } 7 | -------------------------------------------------------------------------------- /ghidra_scripts/basic.js: -------------------------------------------------------------------------------- 1 | // Basic Javascript demo 2 | //@category Ghidraal 3 | currentProgram.getFunctionManager().getFunctions(true).forEach( f => { 4 | printf('%s %s\n', f.getEntryPoint(), f.getName()); 5 | }); 6 | -------------------------------------------------------------------------------- /ghidra_scripts/basic.r: -------------------------------------------------------------------------------- 1 | # Basic R demo 2 | #@category Ghidraal 3 | 4 | currentProgram$getFunctionManager()$getFunctions(TRUE)$forEach( function(f) { 5 | printf('%s %s\n', f$getEntryPoint(), f$getName()); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /ghidra_scripts/basic.py: -------------------------------------------------------------------------------- 1 | # Basic Python 3 demo 2 | #@category Ghidraal 3 | 4 | def dump(f): 5 | print(f'{f.getEntryPoint().toString()} {f.getName()}') 6 | 7 | currentProgram.getFunctionManager().getFunctions(True).forEach(dump) 8 | 9 | import sys 10 | printf('sys.version = %s\n' % sys.version) 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/GhidraalPluginPackage.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import ghidra.framework.plugintool.util.PluginPackage; 4 | 5 | public class GhidraalPluginPackage extends PluginPackage { 6 | 7 | public GhidraalPluginPackage() { 8 | super("Ghidraal", null, "Ghidra scripting with GraalVM"); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /ghidra_scripts/basic.rb: -------------------------------------------------------------------------------- 1 | # Basic ruby demo 2 | #@category Ghidraal 3 | 4 | print <<"foo", <<"bar" # you can stack them 5 | I said foo. 6 | foo 7 | I said bar. 8 | bar 9 | 10 | #s=$gs.askString("gimme string","for ruby") 11 | #puts "got #{s}" 12 | 13 | $currentProgram.getFunctionManager.getFunctions(true).forEach(-> f { 14 | puts "#{f.getEntryPoint.toString} #{f.getName}" 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/GhidraalInitializer.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import ghidra.framework.ModuleInitializer; 4 | import ghidra.util.SystemUtilities; 5 | 6 | public class GhidraalInitializer implements ModuleInitializer { 7 | 8 | @Override 9 | public void run() { 10 | if (SystemUtilities.isInHeadlessMode()) { 11 | GhidraalPlugin.injectGhidraalProviders(); 12 | } 13 | } 14 | 15 | @Override 16 | public String getName() { 17 | return "Ghidraal Extension Module"; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ghidra_scripts/import_demo.py: -------------------------------------------------------------------------------- 1 | # Demonstration of Ghidraal's import magic. 2 | #@category Ghidraal 3 | 4 | from ghidra.util import Msg 5 | Msg.showInfo(None, None, '1', '1') 6 | 7 | from ghidra.util import Msg as MyMsg 8 | MyMsg.showInfo(None, None, '2', '2') 9 | 10 | import ghidra.util.Msg 11 | ghidra.util.Msg.showInfo(None, None, '3', '3') 12 | 13 | import ghidra.util.Msg as MyOtherMsg 14 | MyOtherMsg.showInfo(None, None, '4', '4') 15 | 16 | import ghidra.util 17 | ghidra.util.Msg.showInfo(None, None, '5', '5') 18 | 19 | import ghidra.util as myutil 20 | myutil.Msg.showInfo(None, None, '6', '6') 21 | 22 | 23 | -------------------------------------------------------------------------------- /ghidra_scripts/import_demo2.py: -------------------------------------------------------------------------------- 1 | # Demonstration of imports _without_ Ghidraal's import magic. 2 | #@category Ghidraal 3 | 4 | _ghidraal_use_jythonic_imports = False 5 | # subsequent import statements rely on GraalPython's Python module "java" to 6 | # import Java packages and classes 7 | 8 | from java.ghidra.util import Msg 9 | Msg.showInfo(None, None, '1', '1') 10 | 11 | from java.ghidra.util import Msg as MyMsg 12 | MyMsg.showInfo(None, None, '2', '2') 13 | 14 | import java.ghidra.util.Msg 15 | java.ghidra.util.Msg.showInfo(None, None, '3', '3') 16 | 17 | import java.ghidra.util.Msg as MyOtherMsg 18 | MyOtherMsg.showInfo(None, None, '4', '4') 19 | 20 | import java.ghidra.util 21 | java.ghidra.util.Msg.showInfo(None, None, '5', '5') 22 | 23 | import java.ghidra.util as myutil 24 | myutil.Msg.showInfo(None, None, '6', '6') 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/LangInfo.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | abstract public class LangInfo { 7 | 8 | static public final String API_VARNAME = "_ghidra_api"; 9 | 10 | final String extension; 11 | final String langId; 12 | final String comment; 13 | final Map options; 14 | 15 | protected LangInfo(String extension, String langId, String comment, String... options) { 16 | this.extension = extension; 17 | this.langId = langId; 18 | this.comment = comment; 19 | this.options = new HashMap(); 20 | for (int i = 0; i < options.length - 1; i += 2) { 21 | this.options.put(options[i], options[i + 1]); 22 | } 23 | } 24 | 25 | protected GhidraalScript newScript() { 26 | return new GhidraalScript(this); 27 | } 28 | 29 | protected ScriptingContext newScriptingContext() { 30 | return new ScriptingContext(this); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/java/ghidraal/langs/RubyLangInfo.java: -------------------------------------------------------------------------------- 1 | package ghidraal.langs; 2 | 3 | import org.graalvm.polyglot.Context; 4 | import org.graalvm.polyglot.Value; 5 | 6 | import ghidraal.LangInfo; 7 | import ghidraal.ScriptingContext; 8 | 9 | public class RubyLangInfo extends LangInfo { 10 | 11 | public RubyLangInfo() { 12 | super(".rb", "ruby", "#"); 13 | } 14 | 15 | public static void main(String[] args) { 16 | Context ctx = Context.newBuilder("ruby").allowAllAccess(true).build(); 17 | Value pb = ctx.getBindings("ruby"); 18 | for (String k : pb.getMemberKeys()) { 19 | System.err.printf("%s\n", k); 20 | } 21 | 22 | Value pgb = ctx.getPolyglotBindings(); 23 | pgb.putMember("v", 7); 24 | ctx.eval("ruby", "v=Polyglot.import('v')\n"); 25 | } 26 | 27 | @Override 28 | protected ScriptingContext newScriptingContext() { 29 | return new RubyScriptingContext(); 30 | } 31 | 32 | class RubyScriptingContext extends ScriptingContext { 33 | public RubyScriptingContext() { 34 | super(RubyLangInfo.this); 35 | } 36 | 37 | @Override 38 | protected void putGlobal(String identifier, Object value) { 39 | polyglotBindings.putMember(identifier, value); 40 | ctx.eval("ruby", "$" + identifier + "=Polyglot.import('" + identifier + "')\n"); 41 | } 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /ghidra_scripts/basic_graphics.r: -------------------------------------------------------------------------------- 1 | # Basic R graphics demo 2 | #@category Ghidraal 3 | 4 | library(grid) 5 | 6 | # install from R repl with 7 | # install.packages('ggplot2') 8 | library(ggplot2) 9 | 10 | # create image and register graphics 11 | imageClass <- java.type('java.awt.image.BufferedImage') 12 | image <- new(imageClass, 1024, 1024, imageClass$TYPE_INT_RGB); 13 | graphics <- image$getGraphics() 14 | graphics$setBackground(java.type('java.awt.Color')$white); 15 | grDevices:::awt(image$getWidth(), image$getHeight(), graphics) 16 | 17 | # draw the image 18 | df <- data.frame(name=character(0), addr=integer(0), size=integer(0), stringsAsFactors=FALSE) 19 | it<-currentProgram$getFunctionManager()$getFunctions(TRUE)$iterator() 20 | 21 | while(it$hasNext()) { 22 | f<-it['next'](); 23 | df[nrow(df)+1,]<-list(f$getName(), f$getEntryPoint()$getOffset(), f$getBody()$getNumAddresses()) 24 | } 25 | 26 | p <- ggplot(df, aes(x=size)) + geom_histogram() 27 | 28 | print(p) 29 | 30 | # open frame with image 31 | imageIcon <- new("javax.swing.ImageIcon", image) 32 | label <- new("javax.swing.JLabel", imageIcon) 33 | panel <- new("javax.swing.JPanel") 34 | panel$add(label) 35 | frame <- new("javax.swing.JFrame") 36 | frame$setMinimumSize(new("java.awt.Dimension", 37 | image$getWidth()+20, image$getHeight()+40)) 38 | frame$add(panel) 39 | frame$setVisible(T) 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/langs/Python3LangInfo.java: -------------------------------------------------------------------------------- 1 | package ghidraal.langs; 2 | 3 | import java.io.IOException; 4 | import java.util.Collections; 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.Stream; 8 | 9 | import org.graalvm.polyglot.PolyglotException; 10 | import org.graalvm.polyglot.Value; 11 | import org.graalvm.polyglot.Context.Builder; 12 | 13 | import ghidraal.*; 14 | 15 | public class Python3LangInfo extends LangInfo { 16 | 17 | public Python3LangInfo() { 18 | super(".py", "python", "#"); 19 | } 20 | 21 | @Override 22 | protected ScriptingContext newScriptingContext() { 23 | return new Python3ScriptingContext(); 24 | } 25 | 26 | private final class Python3ScriptingContext extends ScriptingContext { 27 | private Python3ScriptingContext() { 28 | super(Python3LangInfo.this); 29 | } 30 | 31 | @Override 32 | protected void buildAndInit(Builder builder) throws IOException { 33 | builder.option("python.ForceImportSite", "true"); 34 | super.buildAndInit(builder); 35 | } 36 | 37 | @Override 38 | public Set getMembersFromIntrospection(String varName) { 39 | try { 40 | Stream stream = Util.asStream(eval("dir(" + varName + ")")); 41 | if (stream != null) { 42 | return stream.map(v -> v.asString()).collect(Collectors.toSet()); 43 | } 44 | } 45 | catch (PolyglotException e) { 46 | // 47 | } 48 | return Collections.emptySet(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/GhidraalScriptProviderBase.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import java.io.*; 4 | 5 | import generic.jar.ResourceFile; 6 | import ghidra.app.script.GhidraScript; 7 | import ghidra.app.script.GhidraScriptProvider; 8 | 9 | // class name mustn't end with provider, or ClassSearcher will find it 10 | public class GhidraalScriptProviderBase extends GhidraScriptProvider { 11 | final LangInfo langInfo; 12 | 13 | public GhidraalScriptProviderBase(LangInfo langInfo) { 14 | this.langInfo = langInfo; 15 | } 16 | 17 | @Override 18 | public String getDescription() { 19 | return "graal" + langInfo.langId; 20 | } 21 | 22 | @Override 23 | public void createNewScript(ResourceFile newScript, String category) throws IOException { 24 | PrintWriter writer = new PrintWriter(new FileWriter(newScript.getFile(false))); 25 | writeHeader(writer, category); 26 | writer.println(""); 27 | writeBody(writer); 28 | writer.println(""); 29 | writer.close(); 30 | } 31 | 32 | @Override 33 | public String getCommentCharacter() { 34 | return langInfo.comment; 35 | } 36 | 37 | @Override 38 | public String getExtension() { 39 | return langInfo.extension; 40 | } 41 | 42 | @Override 43 | public GhidraScript getScriptInstance(ResourceFile sourceFile, PrintWriter writer) 44 | throws ClassNotFoundException, InstantiationException, IllegalAccessException { 45 | GhidraalScript scr = langInfo.newScript(); 46 | scr.setSourceFile(sourceFile); 47 | return scr; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/langs/NodeJSLangInfo.java: -------------------------------------------------------------------------------- 1 | package ghidraal.langs; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Path; 5 | import java.util.Collections; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | import org.graalvm.polyglot.PolyglotException; 11 | import org.graalvm.polyglot.Value; 12 | 13 | import ghidraal.*; 14 | 15 | public class NodeJSLangInfo extends LangInfo { 16 | 17 | static private String[] getOptions() { 18 | Path langDir = Path.of(System.getProperty("java.home") + "/languages"); 19 | Path npmDir = langDir.resolve("nodejs/npm"); 20 | if (!Files.isDirectory(npmDir)) { 21 | npmDir = langDir.resolve("js/npm"); 22 | if (!Files.isDirectory(npmDir)) { 23 | npmDir = null; 24 | } 25 | } 26 | if (npmDir != null) { 27 | // @formatter:off 28 | return new String[] { 29 | "js.commonjs-require", "true", 30 | "js.commonjs-require-cwd", npmDir.toString() 31 | }; 32 | // @formatter:on 33 | } 34 | return new String[] {}; 35 | } 36 | 37 | public NodeJSLangInfo() { 38 | super(".js", "js", "//", getOptions()); 39 | } 40 | 41 | @Override 42 | protected ScriptingContext newScriptingContext() { 43 | return new NodeJSScriptContext(); 44 | } 45 | 46 | class NodeJSScriptContext extends ScriptingContext { 47 | public NodeJSScriptContext() { 48 | super(NodeJSLangInfo.this); 49 | } 50 | 51 | @Override 52 | public Set getMembersFromIntrospection(String varName) { 53 | try { 54 | Stream stream = Util.asStream(eval(varName + ".keys()")); 55 | if (stream != null) { 56 | return stream.map(v -> v.asString()).collect(Collectors.toSet()); 57 | } 58 | } 59 | catch (PolyglotException e) { 60 | // 61 | } 62 | return Collections.emptySet(); 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright(C) 2021, Institute for Defense Analyses 2 | 4850 Mark Center Drive, Alexandria, VA; 703-845-2500 3 | This material may be reproduced by or for the US Government 4 | pursuant to the copyright license under the clauses at DFARS 5 | 252.227-7013 and 252.227-7014. 6 | 7 | 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | * Redistributions of source code must retain the above copyright 13 | notice, this list of conditions and the following disclaimer. 14 | * Redistributions in binary form must reproduce the above copyright 15 | notice, this list of conditions and the following disclaimer in the 16 | documentation and/or other materials provided with the distribution. 17 | * Neither the name of the copyright holder nor the 18 | names of its contributors may be used to endorse or promote products 19 | derived from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT HOLDER NOR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 26 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 30 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 32 | OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/Util.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import java.io.*; 4 | import java.util.*; 5 | import java.util.function.Consumer; 6 | import java.util.stream.Stream; 7 | import java.util.stream.StreamSupport; 8 | 9 | import org.graalvm.polyglot.Value; 10 | 11 | public class Util { 12 | public static Iterable asIterable(Value arr) { 13 | if (arr.hasArrayElements()) { 14 | return () -> { 15 | return new Iterator() { 16 | int i = 0; 17 | 18 | @Override 19 | public Value next() { 20 | return arr.getArrayElement(i++); 21 | } 22 | 23 | @Override 24 | public boolean hasNext() { 25 | return i < arr.getArraySize(); 26 | } 27 | }; 28 | }; 29 | } 30 | return null; 31 | } 32 | 33 | public static Stream asStream(Value arr) { 34 | if (!arr.hasArrayElements()) { 35 | return null; 36 | } 37 | return StreamSupport.stream( 38 | new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.ORDERED) { 39 | int i = 0; 40 | 41 | public boolean tryAdvance(Consumer action) { 42 | if (i < arr.getArraySize()) { 43 | action.accept(arr.getArrayElement(i++)); 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | public void forEachRemaining(Consumer action) { 50 | while (i < arr.getArraySize()) 51 | action.accept(arr.getArrayElement(i++)); 52 | } 53 | }, false); 54 | } 55 | 56 | static public OutputStream asOutputStream(final Writer w) { 57 | return new OutputStream() { 58 | @Override 59 | public void write(int b) throws IOException { 60 | w.write(b); 61 | } 62 | }; 63 | } 64 | 65 | static public InputStream wrap(String pre, InputStream stream, String post) { 66 | Vector v = new Vector<>(2); 67 | v.addElement(new ByteArrayInputStream(pre.getBytes())); 68 | v.addElement(stream); 69 | v.addElement(new ByteArrayInputStream(post.getBytes())); 70 | return new SequenceInputStream(v.elements()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/GhidraalScript.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import java.io.InputStream; 4 | import java.io.OutputStream; 5 | import java.time.Duration; 6 | 7 | import org.graalvm.polyglot.PolyglotException; 8 | 9 | import generic.jar.ResourceFile; 10 | import ghidra.app.script.GhidraScript; 11 | import ghidra.app.services.ConsoleService; 12 | import ghidra.util.SystemUtilities; 13 | 14 | public class GhidraalScript extends GhidraScript { 15 | final LangInfo langInfo; 16 | 17 | protected String storedContextName; 18 | 19 | public GhidraalScript(LangInfo langInfo) { 20 | this.langInfo = langInfo; 21 | storedContextName = "ghidraaal_" + langInfo.extension + "_stored_context"; 22 | } 23 | 24 | @Override 25 | protected void run() throws Exception { 26 | if (SystemUtilities.isInHeadlessMode()) { 27 | doRun(System.in, System.out, System.err); 28 | } 29 | else { 30 | ConsoleService consoleservice = state.getTool().getService(ConsoleService.class); 31 | try { 32 | doRun(null, Util.asOutputStream(consoleservice.getStdOut()), 33 | Util.asOutputStream(consoleservice.getStdErr())); 34 | } 35 | catch (PolyglotException e) { 36 | e.printStackTrace(consoleservice.getStdErr()); 37 | } 38 | catch (Exception e) { 39 | e.printStackTrace(); 40 | } 41 | } 42 | } 43 | 44 | protected void doRun(InputStream in, OutputStream out, OutputStream err) throws Exception { 45 | try (ScriptingContext ctx = langInfo.newScriptingContext()) { 46 | ctx.init(in, out, err); 47 | 48 | ctx.putGlobal(LangInfo.API_VARNAME, this); 49 | ctx.putGlobal("tool", state.getTool()); 50 | ctx.putGlobal("currentProgram", currentProgram); 51 | ctx.putGlobal("currentAddress", currentAddress); 52 | ctx.putGlobal("currentLocation", currentLocation); 53 | ctx.putGlobal("currentHighlight", currentHighlight); 54 | ctx.putGlobal("monitor", monitor); 55 | 56 | ctx.evalResource("_ghidraal_initscript"); 57 | 58 | ResourceFile f = getSourceFile(); 59 | ctx.evalWithReporting(f.getName(), f.getInputStream()); 60 | 61 | ctx.ctx.interrupt(Duration.ofSeconds(1)); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/langs/RscriptLangInfo.java: -------------------------------------------------------------------------------- 1 | package ghidraal.langs; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.Collections; 6 | import java.util.Set; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | import org.graalvm.polyglot.PolyglotException; 13 | import org.graalvm.polyglot.Value; 14 | 15 | import ghidraal.*; 16 | 17 | public class RscriptLangInfo extends LangInfo { 18 | 19 | public RscriptLangInfo() { 20 | super(".r", "R", "#"); 21 | } 22 | 23 | @Override 24 | protected ScriptingContext newScriptingContext() { 25 | return new RscriptScriptingContext(); 26 | } 27 | 28 | class RscriptScriptingContext extends ScriptingContext { 29 | public RscriptScriptingContext() { 30 | super(RscriptLangInfo.this); 31 | } 32 | 33 | @Override 34 | protected void evalWithReporting(String n, InputStream s) throws IOException { 35 | // @formatter:off 36 | super.evalWithReporting(n, 37 | Util.wrap("tryCatch(" + 38 | "(function(){", s,"})()," + 39 | "warning=function(w) {"+ 40 | API_VARNAME+"$printf('warning: %s\\n', w);" + 41 | "traceback()" + 42 | "}, " + 43 | "error=function(e) {"+ 44 | API_VARNAME+"$printf('error: %s\\n', e);" + 45 | "traceback()" + 46 | "}" + 47 | ");")); 48 | // @formatter:on 49 | } 50 | 51 | private Pattern completionPattern = 52 | Pattern.compile(".*?(?:([a-zA-Z0-9._@$]*)([@$.]))?([a-zA-Z0-9_]*)$"); 53 | 54 | protected CompletionData matchCompletionPattern(String cmd) { 55 | Matcher matcher = completionPattern.matcher(cmd); 56 | if (matcher.matches()) { 57 | return new CompletionData(matcher.group(1), matcher.group(2), matcher.group(3)); 58 | } 59 | return null; 60 | } 61 | 62 | @Override 63 | public Set getMembersFromIntrospection(String varName) { 64 | try { 65 | Stream stream = Util.asStream(eval("names(" + varName + ")")); 66 | if (stream != null) { 67 | return stream.map(v -> v.asString()).collect(Collectors.toSet()); 68 | } 69 | } 70 | catch (PolyglotException e) { 71 | // 72 | } 73 | return Collections.emptySet(); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/resources/_ghidraal_initctx.py: -------------------------------------------------------------------------------- 1 | import java 2 | 3 | def ghidraal_import(name, globals=None, locals=None, fromlist=(), level=0): 4 | if _ghidraal_use_jythonic_imports: 5 | try: 6 | return _ghidraal_original_import(name, globals=globals, locals=locals, fromlist=fromlist, level=level) 7 | except ModuleNotFoundError as module_not_found: 8 | for pn in _ghidraal_package_names: 9 | # hide packages from the Jython JAR (see import logic in 10 | # Python's standard copy module, for example) 11 | if pn.startswith('org.python.'): 12 | continue 13 | namedot = name + '.' 14 | if pn == name or pn.startswith(namedot): 15 | # *** looks like name is a Java package name 16 | if fromlist is None or len(fromlist)==0: # peel off first bit _after_ java 17 | b = _ghidraal_original_import('java.%s' % name, globals=globals, locals=locals, fromlist=fromlist, level=level) 18 | parts = name.split('.') 19 | return getattr(b,parts[0]) 20 | else: 21 | return _ghidraal_original_import('java.%s' % name, globals=globals, locals=locals, fromlist=fromlist, level=level) 22 | # try for a Java class name 23 | parts = name.split('.')[:1] 24 | if len(parts) > 0: 25 | if '.'.join(parts) in _ghidraal_package_names: 26 | try: 27 | t = java.type(name) 28 | # *** looks like name is a Java class name 29 | if fromlist is None or len(fromlist)==0: # peel off first bit _after_ java 30 | b = _ghidraal_original_import('java.%s' % name, globals=globals, locals=locals, fromlist=fromlist, level=level) 31 | parts = name.split('.') 32 | return getattr(b,parts[0]) 33 | else: 34 | return _ghidraal_original_import('java.%s' % name, globals=globals, locals=locals, fromlist=fromlist, level=level) 35 | except BaseException: 36 | pass # fall through 37 | raise module_not_found 38 | else: 39 | return _ghidraal_original_import(name, globals=globals, locals=locals, fromlist=fromlist, level=level) 40 | 41 | if '_ghidraal_use_jythonic_imports' not in globals(): 42 | import builtins 43 | # fetch packages visible to GhidraScript's ClassLoader 44 | gs_classloader = java.type('ghidra.app.script.GhidraScript')['class'].getClassLoader() 45 | _ghidraal_package_names = [p.getName() for p in gs_classloader.getDefinedPackages()] 46 | _ghidraal_original_import = builtins.__import__ 47 | _ghidraal_use_jythonic_imports = True 48 | builtins.__import__ = ghidraal_import 49 | 50 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | # Graalvm sourcable script 2 | ## To download GraalVM and install some components: 3 | ## from a bash shell 4 | ## 1. create a new directory, e.g. 5 | ## mkdir ~/graalvm 6 | ## 2. copy or link this script to that new directory, e.g. 7 | ## cp ~/git/graalvm/env.sh ~/graalvm 8 | ## # OR 9 | ## ln-s ~/git/graalvm/env.sh ~/graalvm 10 | ## 3. from that directory, source env.sh 11 | ## cd ~/graalvm 12 | ## . env.sh 13 | ## 4. environment should be set to use GraalVM 14 | ## which java # ~/graalvm/jdk/bin/java 15 | ## which lli # ~/graalvm/jdk/bin/lli 16 | ## gu list # lists components including Graal.js, FastR, and Graal.Python 17 | ## 18 | ## To setup environment after install, source ~/jdk/env.sh from anywhere. 19 | 20 | D="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd -P "$( dirname "$SOURCE" )" && pwd )" 21 | 22 | ver=21.1.0 23 | tarball="graalvm-ce-java11-linux-amd64-${ver}.tar.gz" 24 | linkname="jdk" 25 | jdk_dirname="graalvm-ce-java11-${ver}" 26 | 27 | 28 | install=1 29 | clean=0 30 | toolchain_path=1 31 | 32 | if [ $# -gt 0 ]; then 33 | case $1 in 34 | reinstall) clean=1;install=1;; 35 | clean) clean=1;install=0;; 36 | no-toolchain-path) toolchain_path=0;; 37 | *) echo "Unrecognized argument \"$1\"";return 1;; 38 | esac 39 | fi 40 | 41 | if [ $clean -eq 1 ]; then 42 | echo "Cleaning GraalVM $ver env" 43 | pushd "$D" > /dev/null 44 | rm -f "${tarball}" "${linkname}" 45 | rm -rf "${jdk_dirname}" 46 | unset _GRAAL_ENV 47 | popd > /dev/null 48 | fi 49 | 50 | if [ $install -ne 1 ]; then 51 | return 0; 52 | fi 53 | 54 | 55 | ## add environment variable to notify on repeated sourcing times 56 | if [ ! -z "${_GRAAL_ENV}" ]; then 57 | echo "GraalVM $ver env already set: \"${_GRAAL_ENV}\"" 58 | return 1 59 | fi 60 | export _GRAAL_ENV="$D" 61 | 62 | 63 | ## if this version isn't installed, install it 64 | if [ ! -d "$D/graalvm-ce-java11-${ver}" ]; then 65 | echo "Installing GraalVM $ver env" 66 | pushd "$D" > /dev/null 67 | ## cleanup existing symlink 68 | if [ -f "${linkname}" ]; then 69 | if [ -L "${linkname}" -a -d "${linkname}" ]; then 70 | rm -f "${linkname}" 71 | else 72 | echo "\"${linkname}\" exists, but isn't a link to a directory" 73 | return 1 74 | fi 75 | fi 76 | wget "https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-${ver}/${tarball}" 77 | tar -zxvf "${tarball}" 78 | ln -s "${jdk_dirname}" "${linkname}" 79 | ./${linkname}/bin/gu install llvm-toolchain native-image nodejs python ruby R wasm 80 | rm -f "${tarball}" 81 | popd > /dev/null 82 | fi 83 | 84 | echo "Setting environment for GraalVM $ver" 85 | 86 | ## assuming env exists, set vars 87 | export PATH="$D/${linkname}/bin:$PATH" 88 | export JAVA_HOME="$D/${linkname}" 89 | export LD_LIBRARY_PATH="$D/${linkname}/languages/R/lib:$D/${linkname}/languages/llvm/native/lib:$LD_LIBRARY_PATH" 90 | 91 | if [ $toolchain_path -eq 1 ]; then 92 | ## add toolchain to path 93 | lli_path=$(which lli) 94 | if [ ! -x "${lli_path}" ] ; then 95 | unset _GRAAL_ENV 96 | echo " lli executable missing!" 97 | echo " Try to reinstall GraalVM $ver env with" 98 | echo " cd \"$D\"" 99 | echo " . env.sh reinstall" 100 | return 1 101 | fi 102 | 103 | export LLVM_TOOLCHAIN=$(lli --print-toolchain-path) 104 | export PATH="$LLVM_TOOLCHAIN:$PATH" 105 | fi 106 | 107 | 108 | -------------------------------------------------------------------------------- /ghidra_scripts/askscript3.py: -------------------------------------------------------------------------------- 1 | # An example of asking for user input. (from Examples.Python run through 2to3) 2 | 3 | # Note the ability to pre-populate values for some of these variables when AskScript.properties file exists. 4 | # Also notice how the previous input is saved. 5 | 6 | # DISCLAIMER: This is a recreation of a Java Ghidra script for example 7 | # use only. Please run the Java version in a production environment. 8 | 9 | #@category Ghidraal 10 | 11 | from ghidra.framework.model import DomainFile 12 | from ghidra.framework.model import DomainFolder 13 | from ghidra.program.model.address import Address 14 | from ghidra.program.model.lang import LanguageCompilerSpecPair 15 | from ghidra.program.model.listing import Program 16 | from ghidra.util import Msg 17 | from java.util import Arrays 18 | 19 | from java.lang import IllegalArgumentException 20 | 21 | # The presence of the AskScript.properties file in the same location (as AskScript.java) 22 | # allows for the following behavior: 23 | # - GUI: if applicable, auto-populates the input field with the value in the 24 | # .properties file (the first time that input field appears) 25 | # - Headless: uses the value in the .properties file for the variable assigned to the 26 | # corresponding askXxx() method in the GhidraScript. 27 | try: 28 | file1 = askFile("FILE", "Choose file:") 29 | print("file was: " + str(file1)) 30 | 31 | directory1 = askDirectory("Directory", "Choose directory:") 32 | print("directory was: " + str(directory1)) 33 | 34 | lang = askLanguage("Language Picker", "I want this one!") 35 | print("language was: " + lang.toString()) 36 | 37 | domFolder = askProjectFolder("Please pick a domain folder!") 38 | print("domFolder was: " + domFolder.getName()) 39 | 40 | int1 = askInt("integer 1", "enter integer 1") 41 | int2 = askInt("integer 2", "enter integer 2") 42 | print("int1 + int2 = " + str(int1 + int2)) 43 | 44 | long1 = askLong("long 1", "enter long 1") 45 | long2 = askLong("long 2", "enter long 2") 46 | print("long1 + long2 = " + str(long1 + long2)) 47 | 48 | address1 = askAddress("address 1", "enter address 1") 49 | address2 = askAddress("address 2", "enter address 2") 50 | print("address1 + address2 = " + address1.add(address2.getOffset()).toString()) 51 | 52 | #bytes = askBytes("bytes", "enter byte pattern") 53 | #for b in bytes: 54 | # print "b = " + str(b & 0xff) 55 | 56 | prog = askProgram("Please choose a program to open.") 57 | print("Program picked: " + prog.getName()) 58 | 59 | domFile = askDomainFile("Which domain file would you like?") 60 | print("Domain file: " + domFile.getName()) 61 | 62 | d1 = askDouble("double 1", "enter double 1") 63 | d2 = askDouble("double 2", "enter double 2") 64 | print("d1 + d2 = " + str(d1 + d2)) 65 | 66 | myStr = askString("String Specification", "Please type a string: ") 67 | myOtherStr = askString("Another String Specification", "Please type another string: ", "replace me!") 68 | print("You typed: " + myStr + " and " + myOtherStr) 69 | 70 | choice = askChoice("Choice", "Please choose one", [ "grumpy", "dopey", "sleepy", "doc", "bashful" ], "bashful") 71 | print("Choice? " + choice) 72 | 73 | # nb: GraalPython doesn't allow access to the Python list from the swing thread, 74 | # so we convert it with java.util.Arrays.asList 75 | choices1 = askChoices("Choices 1", "Please choose one or more numbers.", Arrays.asList([ 1, 2, 3, 4, 5, 6 ])) 76 | print("Choices 1: ") 77 | for intChoice in choices1: 78 | print(str(intChoice) + " ") 79 | print("") 80 | 81 | choices2 = askChoices("Choices 2", "Please choose one or more of the following.", 82 | Arrays.asList([ 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 ]), ["Part 1", "Part 2", "Part 3", "Part 4", "Part 5", "Part 6" ]) 83 | print("Choices 2: ") 84 | for intChoice in choices2: 85 | print(str(intChoice) + " ") 86 | print("") 87 | 88 | yesOrNo = askYesNo("yes or no", "is this a yes/no question?") 89 | print("Yes or No? " + str(yesOrNo)) 90 | 91 | except IllegalArgumentException as error: 92 | Msg.warn(self, "Error during headless processing: " + error.toString()) 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/ScriptingContext.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import java.io.*; 4 | import java.util.Collections; 5 | import java.util.Set; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | import org.graalvm.polyglot.*; 10 | import org.graalvm.polyglot.Context.Builder; 11 | 12 | import resources.ResourceManager; 13 | 14 | /** 15 | * wrapper of GraalVM context with higher level abstractions for scripting. 16 | */ 17 | public class ScriptingContext implements AutoCloseable { 18 | protected Context ctx; 19 | 20 | protected Value polyglotBindings; 21 | protected Value globalBindings; 22 | 23 | final protected LangInfo langInfo; 24 | 25 | public ScriptingContext(LangInfo langInfo) { 26 | this.langInfo = langInfo; 27 | } 28 | 29 | public Value eval(String string) { 30 | return ctx.eval(langInfo.langId, string); 31 | } 32 | 33 | public Value evalInputStream(String name, InputStream inputStream) throws IOException { 34 | try (InputStreamReader reader = new InputStreamReader(inputStream)) { 35 | Source init_source = 36 | Source.newBuilder(langInfo.langId, reader, name).cached(false).build(); 37 | return ctx.eval(init_source); 38 | } 39 | } 40 | 41 | protected void evalResource(String resource) throws IOException { 42 | resource = resource + langInfo.extension; 43 | evalInputStream(resource, ResourceManager.getResourceAsStream(resource)); 44 | } 45 | 46 | protected void evalWithReporting(String scriptName, InputStream scriptContents) 47 | throws IOException { 48 | evalInputStream(scriptName, scriptContents); 49 | } 50 | 51 | protected void putGlobal(String identifier, Object value) { 52 | globalBindings.putMember(identifier, value); 53 | } 54 | 55 | public Value getGlobalObject() { 56 | return globalBindings; 57 | } 58 | 59 | /** 60 | * initialize fields assuming ctx is defined 61 | */ 62 | protected void initFields() { 63 | polyglotBindings = ctx.getPolyglotBindings(); 64 | globalBindings = ctx.getBindings(langInfo.langId); 65 | } 66 | 67 | protected void buildAndInit(Builder builder) throws IOException { 68 | ctx = builder.build(); 69 | evalResource("_ghidraal_initctx"); 70 | initFields(); 71 | } 72 | 73 | public void init(InputStream stdin, OutputStream stdOut, OutputStream stdErr) 74 | throws IOException { 75 | Builder builder = Context.newBuilder(langInfo.langId) 76 | // .engine(shared_engine) // doesn't seem to improve script startup 77 | .allowAllAccess(true) 78 | .out(stdOut) 79 | .err(stdErr) 80 | .options(langInfo.options); 81 | 82 | if (stdin != null) { 83 | builder.in(stdin); 84 | } 85 | 86 | buildAndInit(builder); 87 | } 88 | 89 | /** using introspection (e.g. not Value.getMemberKeys) return members of {@code varName}. 90 | * 91 | * @param varName e.g. "a.b.c" 92 | * @return a set of member names 93 | */ 94 | public Set getMembersFromIntrospection(String varName) { 95 | return Collections.emptySet(); 96 | } 97 | 98 | public static class CompletionData { 99 | final String varName; 100 | final String accessor; 101 | final String memberPrefix; 102 | 103 | public CompletionData(String varName, String accessor, String memberPrefix) { 104 | this.varName = varName; 105 | this.accessor = accessor; 106 | this.memberPrefix = memberPrefix; 107 | 108 | } 109 | } 110 | 111 | private Pattern completionPattern = 112 | Pattern.compile(".*?(?:([a-zA-Z0-9._$]*)(\\.))?([a-zA-Z0-9_$]*)$"); 113 | 114 | /** For completions - find the longest variable name and method/field prefix in {@code cmd}. 115 | * 116 | *

E.g. in Python if cmd is 117 | *

118 | 	 *    blah blah foo.bar
119 | 	 * 
120 | * return ("foo", ".", "bar"). 121 | *
122 | * 123 | *

If the member prefix appears to be global, the varName and accessor are null, e.g. in Python 124 | *

125 | 	 *   blah bar
126 | 	 * 
127 | * return (null, null, "bar") 128 | * 129 | * @param cmd a (partial) command 130 | * @return a CompletionData object with varName, accessor, and memberPrefix or null if no match was found 131 | */ 132 | protected CompletionData matchCompletionPattern(String cmd) { 133 | Matcher matcher = completionPattern.matcher(cmd); 134 | if (matcher.matches()) { 135 | return new CompletionData(matcher.group(1), matcher.group(2), matcher.group(3)); 136 | } 137 | return null; 138 | } 139 | 140 | @Override 141 | public void close() { 142 | ctx.close(true); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ghidraal 2 | 3 | A Ghidra extension for scripting with GraalVM languages, including Javascript, Python3, R, and Ruby. 4 | 5 | ## setup 6 | 7 | GraalVM is a drop in replacement for OpenJDK with some extra powers. 8 | 9 | **Ghidraal will only work when Ghidra is run within a GraalVM!!!** 10 | 11 | 12 | 1. download GraalVM (20.1+) and set up set `JAVA_HOME`, `PATH`, etc. You can 13 | copy and source [env.sh](/env.sh) for a hopefully painless install, e.g. 14 | ```bash 15 | mkdir ~/graalvm 16 | cp env.sh ~/graalvm 17 | cd ~/graalvm 18 | . env.sh 19 | ``` 20 | 21 | 2. build or download the Ghidraal extension 22 | - to build, you'll need gradle 5+. Create a symlink to your Ghidra 23 | installation and run gradle. The extension will be generated in `dist`, 24 | e.g. 25 | ```bash 26 | # in the directory containing your checkout of this repo 27 | ln -s ~/ghidra_9.*_PUBLIC ghidra 28 | . ~/graalvm/env.sh # build requires GraalVM 29 | gradle 30 | ls dist/ 31 | ``` 32 | - or download a [release](/../../releases) 33 | 34 | 3. Run ghidra with GraalVM and install the extension 35 | ```bash 36 | . ~/graalvm/env.sh 37 | ~/ghidra_9.*_PUBLIC/ghidraRun 38 | ``` 39 | From the main window, select `File->Install Extensions...`, click the `+`, 40 | and select the Ghidraal release zip file from the repo `dist` directory. 41 | 42 | 4. Restart Ghidra 43 | 44 | 5. Open a program. If not prompted to, select `File->Configure...`, under 45 | `Experimental` select `Configure`, and make sure `GhidraalPlugin` is 46 | checked. 47 | 48 | ## usage 49 | 50 | There are some extremely basic scripts for each supported language in the 51 | [ghidra_scripts](/ghidra_scripts) directory. When the extension is installed, 52 | they should become visible in the Script Manager under the Ghidraal category. 53 | 54 | An interactive console for each language is available under the code browser "Window" 55 | menu option. 56 | 57 | ### Python 58 | 59 | Ghidraal hijacks the `.py` extension by removing the Jython script provider. 60 | Disable `GhidraalPlugin` to reenable Jython. 61 | 62 | Ghidra's built in Python scripts are Python 2, so won't necessarily work with 63 | Ghidraal's Python 3. Some can be ported automatically with `2to3`. 64 | 65 | For more on Graal Python, see the 66 | [README.md](https://github.com/oracle/graalpython/blob/master/README.md). 67 | 68 | #### import magic 69 | 70 | Jython provides some import magic so that Java packages and classes can be 71 | imported like Python modules. Ghidraal implements a similar magic to emulate 72 | this behavior (*independent from* the Graal Python `--python.EmulateJython` 73 | switch implementation). See [import_demo.py](ghidra_scripts/import_demo.py). 74 | 75 | To disable Ghidraal's import magic, set the (script/interpreter) global 76 | variable `_ghidraal_use_jythonic_imports` to `False`. See 77 | [import_demo2.py](ghidra_scripts/import_demo2.py). 78 | 79 | #### packages 80 | Ghidraal imports `site` automatically, so installed packages will be available. 81 | 82 | ```bash 83 | # to see what's available 84 | graalpython -m ginstall install --help 85 | # install pandas 86 | graalpython -m ginstall install pandas 87 | 88 | # after an upgrade of GraalVM, it might be necessary to reinstall packages 89 | graalpython -m ginstall uninstall numpy,pandas 90 | graalpython -m ginstall install pandas 91 | 92 | # although pip isn't 100% supported, some packages can be installed, e.g. 93 | graalpython -m ginstall pypi pyelftools 94 | ``` 95 | 96 | 97 | 98 | # Ghidraal changelog 99 | 100 | - ghidraal-0.0.5 101 | - fix npm directory handling for javascript/nodejs 102 | - python 103 | - demo workaround for multi-context accesses in `askscript3.py` 104 | - fix console locking on error 105 | - updates to build.gradle 106 | - add eclipse plugin 107 | - allow for "in tree" build 108 | - add "reinstall" and "clean" commands to env.sh 109 | - graalvm + ghidra version bump 110 | - ghidraal-0.0.4 111 | - don't run GhidraalScripts in swing thread 112 | - fix memory leak from unclosed polyglot contexts 113 | - console 114 | - don't print expression value if it's (equivalent of) null 115 | - add "busy" prompt indicator 116 | - python 117 | - fix Jython import emulation, update docs, add examples 118 | - use option "python.ForceImportSite" to automatically import site.py 119 | - ghidraal-0.0.3 120 | - added support for analyzeHeadless usage 121 | - move example scripts into Ghidraal category 122 | - ghidraal-0.0.2 123 | - added interactive consoles 124 | - made basic.py like the other basic scripts 125 | - ghidraal-0.0.1 126 | - initial 127 | 128 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/GhidraalPlugin.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | import java.util.*; 6 | import java.util.stream.Collectors; 7 | 8 | import org.graalvm.polyglot.Engine; 9 | import org.graalvm.polyglot.Language; 10 | 11 | import docking.ActionContext; 12 | import docking.action.DockingAction; 13 | import docking.action.MenuData; 14 | import ghidra.app.plugin.PluginCategoryNames; 15 | import ghidra.app.plugin.ProgramPlugin; 16 | import ghidra.app.script.GhidraScriptProvider; 17 | import ghidra.app.script.GhidraScriptUtil; 18 | import ghidra.framework.plugintool.PluginInfo; 19 | import ghidra.framework.plugintool.PluginTool; 20 | import ghidra.framework.plugintool.util.PluginStatus; 21 | import ghidra.program.flatapi.FlatProgramAPI; 22 | import ghidraal.langs.*; 23 | 24 | /** 25 | * A simple shim to add script providers to GhidraScriptUtil before they're loaded by 26 | */ 27 | //@formatter:off 28 | @PluginInfo( 29 | status = PluginStatus.STABLE, 30 | packageName = "Ghidraal", 31 | category = PluginCategoryNames.INTERPRETERS, 32 | shortDescription = "GraalVM scripting for Ghidra", 33 | description = "Ghidraal integrates GraalVM scripting languages into Ghidra's scripting manager" 34 | ) 35 | //@formatter:on 36 | public class GhidraalPlugin extends ProgramPlugin { 37 | static boolean TRIED_TO_MODIFY_PROVIDERS = false; 38 | static GhidraScriptProvider REMOVED_JYTHON_PROVIDER = null; 39 | 40 | // @formatter:off 41 | static List langInfos = Arrays.asList( 42 | new Python3LangInfo(), 43 | new NodeJSLangInfo(), 44 | new RscriptLangInfo(), 45 | new RubyLangInfo() 46 | ); 47 | // @formatter:on 48 | 49 | public GhidraalPlugin(PluginTool tool) { 50 | super(tool, true, true); 51 | } 52 | 53 | @Override 54 | public void init() { 55 | super.init(); 56 | 57 | if (!TRIED_TO_MODIFY_PROVIDERS) { 58 | injectGhidraalProviders(); 59 | } 60 | addConsoles(); 61 | } 62 | 63 | /** once per Ghidra instance */ 64 | static void injectGhidraalProviders() { 65 | TRIED_TO_MODIFY_PROVIDERS = true; 66 | 67 | PrintWriter out = new PrintWriter(System.err); 68 | Map allLangs = Engine.newBuilder().build().getLanguages(); 69 | 70 | // remove Provider registered for .py handling 71 | Iterator providerIterator = 72 | GhidraScriptUtil.getProviders().iterator(); 73 | while (providerIterator.hasNext()) { 74 | GhidraScriptProvider provider = providerIterator.next(); 75 | if (provider.getExtension().equals(".py")) { 76 | out.printf("removing jython script provider\n"); 77 | REMOVED_JYTHON_PROVIDER = provider; 78 | providerIterator.remove(); 79 | out.printf("removed jython .py script provider, %s\n", provider); 80 | } 81 | } 82 | 83 | out.printf("adding ghidraal script providers\n"); 84 | 85 | GhidraScriptUtil.getProviders() 86 | .addAll(langInfos.stream() 87 | .filter(li -> allLangs.containsKey(li.langId)) 88 | .map(GhidraalScriptProviderBase::new) 89 | .collect(Collectors.toUnmodifiableList())); 90 | 91 | out.printf("all providers:\n"); 92 | for (GhidraScriptProvider p : GhidraScriptUtil.getProviders()) { 93 | out.printf(" %s %s\n", p.getExtension(), p.getDescription()); 94 | } 95 | } 96 | 97 | void addConsoles() { 98 | for (LangInfo langInfo : langInfos) { 99 | DockingAction action = new DockingAction( 100 | "create_ghidraal_" + langInfo.langId + "_console", this.getName()) { 101 | @Override 102 | public void actionPerformed(ActionContext context) { 103 | new GhidraalConsole(langInfo) { 104 | protected void initializeGraalContext() throws IOException { 105 | super.initializeGraalContext(); 106 | ctx.putGlobal("tool", tool); 107 | ctx.putGlobal("currentProgram", currentProgram); 108 | ctx.putGlobal("currentLocation", currentLocation); 109 | ctx.putGlobal("currentSelection", currentSelection); 110 | ctx.putGlobal("currentHighlight", currentHighlight); 111 | ctx.putGlobal(LangInfo.API_VARNAME, new FlatProgramAPI(currentProgram)); 112 | 113 | ctx.evalResource("_ghidraal_initscript"); 114 | } 115 | 116 | protected void welcome(PrintWriter out) { 117 | super.welcome(out); 118 | out.println(" globals defined: tool, currentProgram, currentLocation"); 119 | out.println( 120 | " and the methods of _ghidra_api, a FlatProgramAPI object for currentProgram"); 121 | 122 | } 123 | }.create(tool); 124 | } 125 | }; 126 | action.setMenuBarData(new MenuData( 127 | new String[] { "&Window", "New ghidraal Console for " + langInfo.langId })); 128 | action.setDescription("Create and show a new ghidraal console for " + langInfo.langId); 129 | tool.addAction(action); 130 | } 131 | } 132 | 133 | @Override 134 | protected void dispose() { 135 | if (TRIED_TO_MODIFY_PROVIDERS) { 136 | List providers = GhidraScriptUtil.getProviders(); 137 | if (REMOVED_JYTHON_PROVIDER != null) { 138 | providers.add(REMOVED_JYTHON_PROVIDER); 139 | REMOVED_JYTHON_PROVIDER = null; 140 | } 141 | Iterator it = GhidraScriptUtil.getProviders().iterator(); 142 | while (it.hasNext()) { 143 | GhidraScriptProvider p = it.next(); 144 | if (p instanceof GhidraalScriptProviderBase) { 145 | it.remove(); 146 | } 147 | } 148 | TRIED_TO_MODIFY_PROVIDERS = false; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/ghidraal/GhidraalConsole.java: -------------------------------------------------------------------------------- 1 | package ghidraal; 2 | 3 | import java.awt.event.*; 4 | import java.io.*; 5 | import java.util.*; 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | 8 | import javax.swing.*; 9 | 10 | import org.graalvm.polyglot.PolyglotException; 11 | import org.graalvm.polyglot.Value; 12 | 13 | import docking.ActionContext; 14 | import docking.action.DockingAction; 15 | import docking.action.ToolBarData; 16 | import ghidra.app.plugin.core.console.CodeCompletion; 17 | import ghidra.app.plugin.core.interpreter.*; 18 | import ghidra.framework.plugintool.ServiceProvider; 19 | import ghidra.util.Msg; 20 | import ghidra.util.Swing; 21 | import ghidraal.ScriptingContext.CompletionData; 22 | import resources.Icons; 23 | 24 | public class GhidraalConsole { 25 | static final String prompt = ">>>"; 26 | static final String morePrompt = "..."; 27 | static final String busyPrompt = "(busy)"; 28 | 29 | final LangInfo langInfo; 30 | 31 | protected ScriptingContext ctx; 32 | 33 | protected InterpreterConsole console; 34 | 35 | protected MyInterpreterConnection interpreter; 36 | 37 | protected MyInputThread inputThread; 38 | 39 | public GhidraalConsole(LangInfo langInfo) { 40 | this.langInfo = langInfo; 41 | } 42 | 43 | protected void initializeGraalContext() throws IOException { 44 | if (ctx != null) { 45 | closeGraalContext(); 46 | } 47 | ctx = langInfo.newScriptingContext(); 48 | ctx.init(console.getStdin(), console.getStdOut(), console.getStdErr()); 49 | } 50 | 51 | protected void closeGraalContext() { 52 | ctx.close(); 53 | ctx = null; 54 | } 55 | 56 | protected void welcome(PrintWriter out) { 57 | out.println("GraalVM Console - " + langInfo.langId); 58 | out.println(" press TAB for member lookup"); 59 | out.println(" press SHIFT-ENTER to continue input on next line"); 60 | } 61 | 62 | // must be run in swing thread 63 | protected void initializeConsole(ServiceProvider serviceProvider) { 64 | console = serviceProvider.getService(InterpreterPanelService.class) 65 | .createInterpreterPanel(interpreter, true); 66 | 67 | PrintWriter out = console.getOutWriter(); 68 | welcome(out); 69 | console.setPrompt(busyPrompt); 70 | console.addFirstActivationCallback(this::onFirstConsoleActivation); 71 | 72 | InterpreterComponentProvider provider = (InterpreterComponentProvider) console; 73 | 74 | DockingAction disposeAction = new DockingAction("Remove Interpreter", provider.getName()) { 75 | @Override 76 | public void actionPerformed(ActionContext context) { 77 | console.dispose(); 78 | inputThread.dispose(); 79 | inputThread = null; 80 | closeGraalContext(); 81 | } 82 | }; 83 | disposeAction.setDescription("Remove interpreter from tool"); 84 | disposeAction.setToolBarData(new ToolBarData(Icons.STOP_ICON, null)); 85 | disposeAction.setEnabled(true); 86 | console.addAction(disposeAction); 87 | 88 | // add a key listener for shift-enter 89 | InterpreterPanel panel = (InterpreterPanel) provider.getComponent(); 90 | JPanel interiorPanel = (JPanel) panel.getComponent(1); 91 | JTextPane inputTextPane = (JTextPane) interiorPanel.getComponent(1); 92 | inputTextPane.addKeyListener(new KeyAdapter() { 93 | @SuppressWarnings("deprecation") 94 | @Override 95 | public void keyPressed(KeyEvent e) { 96 | if (e.getKeyCode() == KeyEvent.VK_ENTER && 97 | e.getModifiersEx() == InputEvent.SHIFT_DOWN_MASK) { 98 | 99 | inputThread.wantsMore.set(true); 100 | 101 | // remove the shift modifier so that the text pane treats this event 102 | // like an enter 103 | e.setModifiers(0); 104 | } 105 | } 106 | }); 107 | } 108 | 109 | void onFirstConsoleActivation() { 110 | if (inputThread != null) { 111 | inputThread.dispose(); 112 | inputThread = null; 113 | } 114 | try { 115 | initializeGraalContext(); 116 | inputThread = new MyInputThread(); 117 | inputThread.start(); 118 | } 119 | catch (IOException e) { 120 | Msg.showError(this, null, "Error initializing GraalVM context", e); 121 | } 122 | } 123 | 124 | public void create(ServiceProvider serviceProvider) { 125 | interpreter = new MyInterpreterConnection(); 126 | 127 | Swing.runNow(() -> { 128 | initializeConsole(serviceProvider); 129 | }); 130 | } 131 | 132 | class MyInterpreterConnection implements InterpreterConnection { 133 | 134 | @Override 135 | public String getTitle() { 136 | return "Ghidraal console for " + langInfo.langId; 137 | } 138 | 139 | @Override 140 | public ImageIcon getIcon() { 141 | return Icons.EMPTY_ICON; 142 | } 143 | 144 | @Override 145 | public List getCompletions(String cmd) { 146 | Value object = null; 147 | String varNameWithAccessor = ""; 148 | String memberPrefix = ""; 149 | String varName = null; 150 | 151 | CompletionData completionData = ctx.matchCompletionPattern(cmd); 152 | if (completionData != null) { 153 | varName = completionData.varName; 154 | memberPrefix = completionData.memberPrefix; 155 | if (varName != null) { 156 | try { 157 | object = ctx.eval(varName); 158 | if (object != null) { 159 | varNameWithAccessor = varName + completionData.accessor; 160 | } 161 | } 162 | catch (PolyglotException e) { 163 | // oh well 164 | } 165 | } 166 | } 167 | 168 | // completionData wasn't found, or we don't trust it since no variable was found 169 | if (object == null) { 170 | varName = null; 171 | object = ctx.getGlobalObject(); 172 | } 173 | 174 | // members are sorted by length, then lexicographically 175 | Set members = new TreeSet<>((a, b) -> { 176 | int c = Integer.compare(a.length(), b.length()); 177 | if (c == 0) 178 | c = a.compareTo(b); 179 | return c; 180 | }); 181 | 182 | members.addAll(object.getMemberKeys()); 183 | if (varName != null) { 184 | members.addAll(ctx.getMembersFromIntrospection(varName)); 185 | } 186 | 187 | // now filter with our prefix and construct CodeCompletion objects 188 | List completions = new ArrayList<>(); 189 | for (String member : members) { 190 | if (member.startsWith(memberPrefix)) { 191 | completions.add(new CodeCompletion(varNameWithAccessor + member, 192 | member.substring(memberPrefix.length()), null)); 193 | } 194 | } 195 | return completions; 196 | } 197 | 198 | } 199 | 200 | class MyInputThread extends Thread { 201 | private AtomicBoolean shouldContinue; 202 | AtomicBoolean wantsMore; 203 | 204 | MyInputThread() { 205 | super("my input thread"); 206 | this.shouldContinue = new AtomicBoolean(true); 207 | this.wantsMore = new AtomicBoolean(false); 208 | } 209 | 210 | @Override 211 | public void run() { 212 | InputStream stdin = console.getStdin(); 213 | console.setPrompt(prompt); 214 | PrintWriter out = console.getOutWriter(); 215 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(stdin))) { 216 | StringBuffer sb = new StringBuffer(); 217 | while (shouldContinue.get()) { 218 | String line; 219 | if (stdin.available() > 0) { 220 | line = reader.readLine(); 221 | } 222 | else { 223 | try { 224 | Thread.sleep(50); 225 | } 226 | catch (InterruptedException e) { 227 | // Nothing to do...just continue. 228 | } 229 | continue; 230 | } 231 | if (wantsMore.get()) { 232 | sb.append(line); 233 | sb.append('\n'); 234 | wantsMore.set(false); 235 | console.setPrompt(morePrompt); 236 | continue; 237 | } 238 | 239 | sb.append(line); 240 | 241 | console.setInputPermitted(false); 242 | console.setPrompt(busyPrompt); 243 | 244 | try { 245 | Value result = ctx.eval(sb.toString()); 246 | if (!result.isNull()) { 247 | out.printf("%s\n", result); 248 | } 249 | } 250 | catch (PolyglotException e) { 251 | e.printStackTrace(console.getErrWriter()); 252 | } 253 | finally { 254 | console.setInputPermitted(true); 255 | } 256 | sb.setLength(0); 257 | console.setPrompt(prompt); 258 | } 259 | } 260 | catch (IOException e) { 261 | Msg.error(MyInputThread.class, 262 | "Internal error reading commands from interpreter console.", e); 263 | } 264 | } 265 | 266 | void dispose() { 267 | shouldContinue.set(false); 268 | } 269 | } 270 | 271 | } 272 | --------------------------------------------------------------------------------