├── Sample1 ├── src │ ├── com │ │ └── nosecurecode │ │ │ ├── libghidra │ │ │ └── Demo.java │ └── lib │ │ └── ghidra.jar.txt ├── .idea │ ├── description.html │ ├── project-template.xml │ ├── vcs.xml │ ├── encodings.xml │ ├── modules.xml │ ├── misc.xml │ ├── uiDesigner.xml │ └── workspace.xml └── GhidraDemo.iml ├── Sample2 ├── src │ ├── com │ │ └── nosecurecode │ │ │ ├── libghidra │ │ │ └── Demo.java │ └── lib │ │ └── ghidra.jar.txt ├── .idea │ ├── description.html │ ├── project-template.xml │ ├── vcs.xml │ ├── encodings.xml │ ├── modules.xml │ ├── misc.xml │ ├── uiDesigner.xml │ └── workspace.xml └── GhidraDemo.iml ├── Media └── Sample1.PNG ├── GhidraLib ├── LibProgramHandler.java ├── LibHeadlessTimedTaskMonitor.java ├── LibHeadlessErrorLogger.java ├── LibGhidra.java ├── LibHeadlessOptions.java ├── LibHeadlessScript.java └── LibHeadlessAnalyzer.java └── README.md /Sample1/src/com/nosecurecode/libghidra: -------------------------------------------------------------------------------- 1 | ../../../../GhidraLib -------------------------------------------------------------------------------- /Sample2/src/com/nosecurecode/libghidra: -------------------------------------------------------------------------------- 1 | ../../../../GhidraLib -------------------------------------------------------------------------------- /Media/Sample1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nshalabi/Coding-Ghidra/HEAD/Media/Sample1.PNG -------------------------------------------------------------------------------- /Sample1/.idea/description.html: -------------------------------------------------------------------------------- 1 | Simple Java application that includes a class with main() method -------------------------------------------------------------------------------- /Sample2/.idea/description.html: -------------------------------------------------------------------------------- 1 | Simple Java application that includes a class with main() method -------------------------------------------------------------------------------- /Sample1/.idea/project-template.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sample2/.idea/project-template.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sample1/src/lib/ghidra.jar.txt: -------------------------------------------------------------------------------- 1 | Run buildGhidraJar.bat batch file located in Ghidera archive under the "support" folder, then copy the generated ghidra.jar to this folder -------------------------------------------------------------------------------- /Sample2/src/lib/ghidra.jar.txt: -------------------------------------------------------------------------------- 1 | Run buildGhidraJar.bat batch file located in Ghidera archive under the "support" folder, then copy the generated ghidra.jar to this folder -------------------------------------------------------------------------------- /Sample1/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample2/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample2/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample1/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample2/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample1/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample2/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sample1/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GhidraLib/LibProgramHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | This interface implements the callback functionality, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com 3 | */ 4 | 5 | 6 | package com.nosecurecode.libghidra; 7 | 8 | import ghidra.program.model.listing.Program; 9 | 10 | /** 11 | * Implement this interface in the class that need to have a callback after Ghidra processing 12 | */ 13 | public interface LibProgramHandler { 14 | public void PostProcessHandler(Program program); 15 | } 16 | -------------------------------------------------------------------------------- /Sample2/GhidraDemo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sample1/GhidraDemo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sample1/src/com/nosecurecode/Demo.java: -------------------------------------------------------------------------------- 1 | /* 2 | PROGRAM : Coding Ghidra - Sample 1 3 | AUTHOR : NADER SHALLABI 4 | 5 | This sample code is free for use, redistribution and/or 6 | modification without any explicit permission from the author. 7 | 8 | This sample code is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY, implied or explicit. 10 | */ 11 | 12 | package com.nosecurecode; 13 | 14 | import ghidra.program.model.listing.Function; 15 | import ghidra.program.model.listing.FunctionIterator; 16 | import ghidra.program.model.listing.Program; 17 | import com.nosecurecode.libghidra.*; 18 | 19 | /** 20 | * Demo: Dump program external functions, this program demonstrates basic usage of the Coding-Ghidra library 21 | * The sample demonstrates looking for commonly used anti-debugging APIs, such as "IsDebuggerPresent" API function 22 | */ 23 | public class Demo implements LibProgramHandler { 24 | public static void main(String args[]) throws Exception { 25 | 26 | // Option 1 to call headless analyzer using a full command 27 | String headlessCmd = "/Users/nadershallabi/ghidra/projects Project1 -import /Users/nadershallabi/Downloads/1.exe -overwrite"; 28 | 29 | // We need an instance of this class to pass the analyzed program handler 30 | Demo ghidraLibraryDemo = new Demo(); 31 | 32 | // This will kickoff the analysis 33 | LibGhidra.runHeadlessCmd(headlessCmd, ghidraLibraryDemo); 34 | 35 | // Option 2 would be to call the analyzer passing command arguments: 36 | // LibGhidra.runHeadlessCmd(args, ghidraLibraryDemo); 37 | } 38 | 39 | /** 40 | * Use the passed Program instance to dump program imports 41 | * The code highlights the IsDebuggerPresent function 42 | * @param program 43 | */ 44 | @Override 45 | public void PostProcessHandler(Program program) { 46 | 47 | // Get a list of external functions used 48 | FunctionIterator externalFunctions = program.getListing().getExternalFunctions(); 49 | 50 | System.out.println("\033[1;33m"); 51 | System.out.println("================"); 52 | System.out.println("PROGRAM IMPORTS:"); 53 | System.out.println("================"); 54 | 55 | // Print all functions in the program: [return type] [calling convention] [function name] 56 | // Highlight the IsDebuggerPresent() API function 57 | 58 | System.out.println("\033[0;33m"); 59 | 60 | while (externalFunctions.hasNext()) { 61 | Function function = externalFunctions.next(); 62 | if (function.getName().equals("IsDebuggerPresent")) { 63 | System.out.print("\033[1;31m"); 64 | } else { 65 | System.out.print("\033[0;33m"); 66 | } 67 | System.out.println( 68 | function.getReturnType() + " " + function.getCallingConvention() + " " + function.getName() 69 | ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /GhidraLib/LibHeadlessTimedTaskMonitor.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 | 17 | /* 18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com 19 | */ 20 | 21 | package com.nosecurecode.libghidra; 22 | 23 | import java.util.Timer; 24 | import java.util.TimerTask; 25 | 26 | import ghidra.util.exception.CancelledException; 27 | import ghidra.util.task.CancelledListener; 28 | import ghidra.util.task.TaskMonitor; 29 | 30 | /** 31 | * Monitor used by Headless Analyzer for "timeout" functionality 32 | */ 33 | public class LibHeadlessTimedTaskMonitor implements TaskMonitor { 34 | 35 | private Timer timer = new Timer(); 36 | private volatile boolean isCancelled; 37 | 38 | LibHeadlessTimedTaskMonitor(int timeoutSecs) { 39 | isCancelled = false; 40 | timer.schedule(new TimeOutTask(), timeoutSecs * 1000); 41 | } 42 | 43 | private class TimeOutTask extends TimerTask { 44 | @Override 45 | public void run() { 46 | LibHeadlessTimedTaskMonitor.this.cancel(); 47 | } 48 | } 49 | 50 | @Override 51 | public boolean isCancelled() { 52 | return isCancelled; 53 | } 54 | 55 | @Override 56 | public void setShowProgressValue(boolean showProgressValue) { 57 | // stub 58 | } 59 | 60 | @Override 61 | public void setMessage(String message) { 62 | // stub 63 | } 64 | 65 | @Override 66 | public String getMessage() { 67 | return null; 68 | } 69 | 70 | @Override 71 | public void setProgress(long value) { 72 | // stub 73 | } 74 | 75 | @Override 76 | public void initialize(long max) { 77 | // stub 78 | } 79 | 80 | @Override 81 | public void setMaximum(long max) { 82 | // stub 83 | } 84 | 85 | @Override 86 | public long getMaximum() { 87 | return 0; 88 | } 89 | 90 | @Override 91 | public void setIndeterminate(boolean indeterminate) { 92 | // stub 93 | } 94 | 95 | @Override 96 | public boolean isIndeterminate() { 97 | return false; 98 | } 99 | 100 | @Override 101 | public void checkCanceled() throws CancelledException { 102 | if (isCancelled()) { 103 | throw new CancelledException(); 104 | } 105 | } 106 | 107 | @Override 108 | public void incrementProgress(long incrementAmount) { 109 | // stub 110 | } 111 | 112 | @Override 113 | public long getProgress() { 114 | return 0; 115 | } 116 | 117 | @Override 118 | public void cancel() { 119 | timer.cancel(); // Terminate the timer thread 120 | isCancelled = true; 121 | } 122 | 123 | @Override 124 | public void addCancelledListener(CancelledListener listener) { 125 | // stub 126 | } 127 | 128 | @Override 129 | public void removeCancelledListener(CancelledListener listener) { 130 | // stub 131 | } 132 | 133 | @Override 134 | public void setCancelEnabled(boolean enable) { 135 | // stub 136 | } 137 | 138 | @Override 139 | public boolean isCancelEnabled() { 140 | return true; 141 | } 142 | 143 | @Override 144 | public void clearCanceled() { 145 | isCancelled = false; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /GhidraLib/LibHeadlessErrorLogger.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 | 17 | /* 18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com 19 | */ 20 | 21 | package com.nosecurecode.libghidra; 22 | 23 | import java.io.*; 24 | 25 | import ghidra.util.ErrorLogger; 26 | 27 | /** 28 | * Custom headless error logger which is used when log4j is disabled. 29 | */ 30 | class LibHeadlessErrorLogger implements ErrorLogger { 31 | 32 | private PrintWriter logWriter; 33 | 34 | LibHeadlessErrorLogger(File logFile) { 35 | if (logFile != null) { 36 | setLogFile(logFile); 37 | } 38 | } 39 | 40 | synchronized void setLogFile(File logFile) { 41 | try { 42 | if (logFile == null) { 43 | if (logWriter != null) { 44 | writeLog("INFO", "File logging disabled"); 45 | logWriter.close(); 46 | logWriter = null; 47 | } 48 | return; 49 | } 50 | PrintWriter w = new PrintWriter(new FileWriter(logFile)); 51 | if (logWriter != null) { 52 | writeLog("INFO ", "Switching log file to: " + logFile); 53 | logWriter.close(); 54 | } 55 | logWriter = w; 56 | } 57 | catch (IOException e) { 58 | System.err.println("Failed to open log file " + logFile + ": " + e.getMessage()); 59 | } 60 | } 61 | 62 | private synchronized void writeLog(String line) { 63 | if (logWriter == null) { 64 | return; 65 | } 66 | logWriter.println(line); 67 | } 68 | 69 | private synchronized void writeLog(String level, String[] lines) { 70 | if (logWriter == null) { 71 | return; 72 | } 73 | for (String line : lines) { 74 | writeLog(level + " " + line); 75 | } 76 | logWriter.flush(); 77 | } 78 | 79 | private synchronized void writeLog(String level, String text) { 80 | if (logWriter == null) { 81 | return; 82 | } 83 | writeLog(level, chopLines(text)); 84 | } 85 | 86 | private synchronized void writeLog(String level, String text, Throwable throwable) { 87 | if (logWriter == null) { 88 | return; 89 | } 90 | writeLog(level, chopLines(text)); 91 | for (StackTraceElement element : throwable.getStackTrace()) { 92 | writeLog(level + " " + element.toString()); 93 | } 94 | logWriter.flush(); 95 | } 96 | 97 | private String[] chopLines(String text) { 98 | text = text.replace("\r", ""); 99 | return text.split("\n"); 100 | } 101 | 102 | @Override 103 | public void debug(Object originator, Object message) { 104 | // TODO for some reason debug is off 105 | // writeLog("DEBUG", message.toString()); 106 | } 107 | 108 | @Override 109 | public void debug(Object originator, Object message, Throwable throwable) { 110 | // TODO for some reason debug is off 111 | // writeLog("DEBUG", message.toString(), throwable); 112 | } 113 | 114 | @Override 115 | public void error(Object originator, Object message) { 116 | writeLog("ERROR", message.toString()); 117 | } 118 | 119 | @Override 120 | public void error(Object originator, Object message, Throwable throwable) { 121 | writeLog("ERROR", message.toString(), throwable); 122 | } 123 | 124 | @Override 125 | public void info(Object originator, Object message) { 126 | writeLog("INFO ", message.toString()); 127 | } 128 | 129 | @Override 130 | public void info(Object originator, Object message, Throwable throwable) { 131 | // TODO for some reason tracing is off 132 | // writeLog("INFO ", message.toString(), throwable); 133 | } 134 | 135 | @Override 136 | public void trace(Object originator, Object message) { 137 | // TODO for some reason tracing i soff 138 | // writeLog("TRACE", message.toString()); 139 | } 140 | 141 | @Override 142 | public void trace(Object originator, Object message, Throwable throwable) { 143 | // TODO for some reason tracing is off 144 | // writeLog("TRACE", message.toString(), throwable); 145 | } 146 | 147 | @Override 148 | public void warn(Object originator, Object message) { 149 | writeLog("WARN ", message.toString()); 150 | } 151 | 152 | @Override 153 | public void warn(Object originator, Object message, Throwable throwable) { 154 | writeLog("WARN ", message.toString(), throwable); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coding Ghidra - Library and Samples 2 | 3 | Ghidra Java code can be bundled in a **Java Archive** file (**JAR**), this makes it easy to use Ghidra as a library for more advanced reverse engineering tasks, in this repository you will find samples to demonstrate how to use Ghidra as a disassembler library. 4 | 5 | ## Prerequisites 6 | 7 | The projects in this repository were created using IntelliJ IDEA, a Community Edition is available for free at [www.jetbrains.com](www.jetbrains.com), additionally, the samples need to be bundled with Ghidra JAR file, you will need to build this file yourself, run `buildGhidraJar.bat` batch file located in Ghidera archive under the `support` folder, then copy the generated ghidra.jar to projects lib file, for example, Sample1\src\lib. 8 | 9 | **UPDATE**: With Ghidra 10.1.1 there were many breaking changes in code tha was fixed, however, for building the JAR file, an additional step is needed: 10 | 11 | Before building the JAR file, edit the file `{GHIDRA_FOLDER}/Ghidra/Features/ByteViewer/Module.manifest` and **delete** the line (the only line) in the file **"EXCLUDE FROM GHIDRA JAR: true"**, then run the JAR build sxcript, if this step is missing, the headless analzer will fail to load due to a class loading error (since the mentioned library is missing from the JAR archive). 12 | 13 | Please note that the JAR file does not include all of Ghidra modules, if you want to include all modules, then adjust the manifest of each library as previously suggested and customize the `{GHIDRA_FOLDER}/Ghidra/Features/Base/ghidra_scripts/BuildGhidraJarScript.java` by removing the comment next to the **"addAllModules()"** call and then run the JAR build script. 14 | 15 | ## Library Design 16 | 17 | Ghidra supports a headless analysis mode that can be used to automate many of Ghidra's functionality, this mode can also be used with Ghidra plugins. The **GhidraLib** is a Java wrapper to Ghidra headless mode, most of the code is a copy from the existing Ghidra headless code branch with modifications to help hook the analysis process using a callback interface to enable access to analysis data, the headless analysis process can be started with two methods: 18 | 19 | ``` 20 | public static void runHeadlessCmd(String headlessCmd, LibProgramHandler handler) 21 | 22 | public static void runHeadlessCmd(String [] headlessCmdArgs, LibProgramHandler handler 23 | ``` 24 | 25 | Both methods are identical, the only difference is in the way the headless command is passed. The first method uses a command line in precisely the same format of the headless analyzer, for example, the following command string imports a binary /binaries/binary1.exe to a local Ghidra Project named Project1. Analysis is on by default. 26 | 27 | `/Users/user/ghidra/projects Project1 -import /binaries/binary1.exe` 28 | 29 | The second format uses string tokens instead, for example: 30 | 31 | `{"/Users/user/ghidra/projects", "-import", "/binaries/binary1.exe"}` 32 | 33 | The second argument is the callback (handler), this handler is an instance of an object that implements the **LibProgramHandler** interface and it's only method: 34 | 35 | ``` 36 | public interface LibProgramHandler { 37 | public void PostProcessHandler(Program program); 38 | } 39 | ``` 40 | 41 | When invoking headless analysis using any of the previous methods, the analysis code will pass an instance of **ghidra.program.model.listing.Program** object immediately after the binary analysis is done, but before it fully returns to invoking code (if handler parameter is not null), this object is the outcome of the headless analysis. As an example, the following code uses this object to dump all program imports: 42 | 43 | ``` 44 | // Get a list of external functions used 45 | FunctionIterator externalFunctions = program.getListing().getExternalFunctions(); 46 | 47 | // Print all functions in the program: [return type] [calling convention] [function name] 48 | while (externalFunctions.hasNext()) { 49 | 50 | Function function = externalFunctions.next(); 51 | 52 | System.out.println( function.getReturnType() + " " + function.getCallingConvention() + " " + function.getName() ); 53 | } 54 | ``` 55 | 56 | ## Objective 57 | 58 | The headless mode combined with Ghidra plugins, are powerful automation tools, the GhidraLibrary inherits all those features and allows for it to be easily embedded in your own, standalone applications or integration layer with other applications/solutions. 59 | 60 | ## Roadmap 61 | 62 | 1. Add more samples and use cases. 63 | 2. Add more event handlers (for example, PreProcessHandler, Pre/PostScriptRunHandler). 64 | 65 | More samples will be added in the future, and all are welcome to contribute. 66 | 67 | --- 68 | 69 | ## Sample 1 70 | 71 | Demonstrates basic usage of library initialization, loading a binary file for analysis and listing all external functions 72 | 73 | ![Sample1](https://github.com/nshalabi/Coding-Ghidra/blob/master/Media/Sample1.PNG "Sample1") 74 | 75 | ## Sample 2 76 | 77 | Using Ghidra in Vulnerability Research, based on the [Ghidra Plugin Development for Vulnerability Research - Part-1](https://www.somersetrecon.com/blog/2019/ghidra-plugin-development-for-vulnerability-research-part-1) article. The code demonstrates using cross references to identify code units calling external functions. 78 | 79 | **Sample output** 80 | 81 | [Function]>-----Address of calling function----->[External Function] 82 | 83 | `[FUN_00403ed2]>-----00403ee7----->[lstrcpynA]` 84 | 85 | `[FUN_00405a0c]>-----00405a19----->[lstrcpynA]` 86 | 87 | # License 88 | 89 | ``` 90 | Copyright 2019 Nader Shallabi. All rights reserved. 91 | 92 | "CODING GHIDRA" CAN BE COPIED AND/OR DISTRIBUTED WITHOUT ANY EXPRESS PERMISSION OF NADER SHALLABI. 93 | 94 | THIS SOFTWARE IS PROVIDED BY NADER SHALLABI ''AS IS'' AND ANY EXPRESS OR IMPLIED 95 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 96 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NADER SHALLABI 97 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 98 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 99 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 100 | AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 101 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 102 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 103 | 104 | The views and conclusions contained in the software and documentation are those of the authors and 105 | should not be interpreted as representing official policies, either expressed or implied, of Nader Shallabi. 106 | ``` 107 | -------------------------------------------------------------------------------- /Sample2/src/com/nosecurecode/Demo.java: -------------------------------------------------------------------------------- 1 | /* 2 | PROGRAM : Coding Ghidra - Sample 2 3 | AUTHOR : NADER SHALLABI 4 | 5 | This sample code is free for use, redistribution and/or 6 | modification without any explicit permission from the author. 7 | 8 | This sample code is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY, implied or explicit. 10 | */ 11 | 12 | package com.nosecurecode; 13 | 14 | import ghidra.program.flatapi.FlatProgramAPI; 15 | import ghidra.program.model.address.Address; 16 | import ghidra.program.model.listing.*; 17 | import com.nosecurecode.libghidra.*; 18 | import ghidra.program.model.symbol.ExternalReference; 19 | import ghidra.program.model.symbol.Reference; 20 | import ghidra.program.model.symbol.ReferenceIterator; 21 | 22 | import java.util.Map; 23 | import java.util.*; 24 | 25 | /** 26 | * Demo: Identify vulnerable runtime functions and cross-reference them with parent functions 27 | */ 28 | public class Demo implements LibProgramHandler { 29 | public static void main(String args[]) throws Exception { 30 | 31 | // Option 1 to call headless analyzer using a full command 32 | String headlessCmd = "/Users/nadershallabi/ghidra/projects ProjectCrossReferences -import /Users/nadershallabi/Downloads/CoreFTPServer505.exe -overwrite"; 33 | 34 | // We need an instance of this class to pass the analyzed program handler 35 | Demo ghidraLibraryDemo = new Demo(); 36 | 37 | // This will kickoff the analysis 38 | LibGhidra.runHeadlessCmd(headlessCmd, ghidraLibraryDemo); 39 | } 40 | 41 | /** 42 | * Sample based on https://www.somersetrecon.com/blog/2019/ghidra-plugin-development-for-vulnerability-research-part-1 43 | * @param program 44 | */ 45 | @Override 46 | public void PostProcessHandler(Program program) { 47 | 48 | System.out.println("\033[1;33m"); 49 | System.out.println("================================"); 50 | System.out.println("PROCESSING PROGRAM : " + program.getName()); 51 | System.out.println("================================"); 52 | 53 | String [] sinks_array = new String[] { 54 | "strcpy", 55 | "memcpy", 56 | "gets", 57 | "memmove", 58 | "scanf", 59 | "strcpyA", 60 | "strcpyW", 61 | "wcscpy", 62 | "_tcscpy", 63 | "_mbscpy", 64 | "StrCpy", 65 | "StrCpyA", 66 | "StrCpyW", 67 | "lstrcpy", 68 | "lstrcpyA", 69 | "lstrcpyW", 70 | "lstrcpynA" 71 | }; 72 | 73 | List sinks = Arrays.asList(sinks_array); 74 | 75 | Hashtable>> sink_dic = new Hashtable>>(); 76 | ArrayList duplicate = new ArrayList(); 77 | 78 | Listing listing = program.getListing(); 79 | InstructionIterator ins_list = listing.getInstructions(true); 80 | 81 | // iterate over each instruction 82 | while (ins_list.hasNext()) { 83 | Instruction ins = ins_list.next(); 84 | String mnemonic = ins.getMnemonicString(); 85 | Object[] ops = ins.getOpObjects(0); 86 | if (mnemonic.equals("CALL")) { 87 | Object target_addr = ops[0]; 88 | String func_name = null; 89 | 90 | ExternalReference ref = null; 91 | if (target_addr instanceof Address) { 92 | CodeUnit code_unit = listing.getCodeUnitAt((Address) target_addr); 93 | if (code_unit != null) 94 | ref = code_unit.getExternalReference(0); 95 | if (ref != null) 96 | func_name = ref.getLabel(); 97 | else { 98 | Function func = listing.getFunctionAt((Address) target_addr); 99 | func_name = func.getName(); 100 | } 101 | } 102 | 103 | // check if function name is in our sinks list 104 | if (sinks.contains(func_name) && !duplicate.contains(func_name)) { 105 | duplicate.add(func_name); 106 | ReferenceIterator references = program.getReferenceManager().getReferencesTo((Address) target_addr); 107 | 108 | for (Reference ref_var : references) { 109 | Address call_addr = ref_var.getFromAddress(); 110 | Object sink_addr = ops[0]; 111 | String parent_func_name = new FlatProgramAPI(program).getFunctionBefore(call_addr).getName(); 112 | 113 | // check sink dictionary for parent function name 114 | if (!sink_dic.containsKey(parent_func_name)) { 115 | Vector> function_address_pair = new Vector>(); 116 | function_address_pair.add(new AbstractMap.SimpleEntry(func_name, call_addr.toString())); 117 | sink_dic.put(parent_func_name, function_address_pair); 118 | } 119 | else { 120 | sink_dic.get(parent_func_name).add(new AbstractMap.SimpleEntry(func_name, call_addr.toString())); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | printDiscoveredFunctions(sink_dic); 128 | 129 | System.out.println("~==============================="); 130 | System.out.println("~ END OF PROCESSING PROGRAM : " + program.getName()); 131 | System.out.println("~==============================="); 132 | System.out.println("\033[0;33m"); 133 | } 134 | 135 | /** 136 | * Prints functions list (findings) 137 | * @param sink_dic 138 | */ 139 | private void printDiscoveredFunctions(Hashtable>> sink_dic) { 140 | for (String parent_func_name : sink_dic.keySet()) { 141 | Vector> item = sink_dic.get(parent_func_name); 142 | for (int i = 0; i < item.size(); i++) { 143 | Map.Entry item2 = item.get(i); 144 | String func_name = item2.getKey(); 145 | String call_addr = item2.getValue(); 146 | System.out.println("[" + parent_func_name + "]>-----" + call_addr + "----->[" + func_name + "]"); 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sample2/.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Sample1/.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Sample1/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | this.gp = createGhidraTestProject(projectName); 19 | createGhidraTestProject 20 | programManager 21 | 22 | 23 | 24 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 1552242709104 75 | 89 | 90 | 91 | 92 | 94 | 95 | 97 | 98 | 99 | 100 | 101 | jar://$PROJECT_DIR$/src/lib/ghidra.jar!/ghidra/framework/HeadlessGhidraApplicationConfiguration.class 102 | 33 103 | 105 | 106 | jar://$PROJECT_DIR$/src/lib/ghidra.jar!/ghidra/util/classfinder/ClassSearcher.class 107 | 141 108 | 110 | 111 | jar://$PROJECT_DIR$/src/lib/ghidra.jar!/ghidra/util/classfinder/ClassSearcher.class 112 | 145 113 | 115 | 116 | file://$PROJECT_DIR$/src/com/nosecurecode/Demo.java 117 | 46 118 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | program.getListing().getFunctions(true).next() 131 | JAVA 132 | EXPRESSION 133 | 134 | 135 | program.getListing().getFunctions(true) 136 | JAVA 137 | EXPRESSION 138 | 139 | 140 | program.getListing() 141 | JAVA 142 | EXPRESSION 143 | 144 | 145 | externalFunctions. 146 | JAVA 147 | EXPRESSION 148 | 149 | 150 | externalFunctions.next() 151 | JAVA 152 | EXPRESSION 153 | 154 | 155 | externalFunctions.hasNext() 156 | JAVA 157 | EXPRESSION 158 | 159 | 160 | path.substring(1) 161 | JAVA 162 | EXPRESSION 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 177 | 178 | 179 | 180 | 181 | 182 | No facets are configured 183 | 184 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 200 | 201 | 202 | 203 | 204 | 205 | 11 206 | 207 | 212 | 213 | 214 | 215 | 216 | 217 | GhidraDemo 218 | 219 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 235 | 236 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /Sample2/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 40 | 41 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 1552242709104 87 | 104 | 105 | 106 | 107 | 109 | 110 | 111 | 112 | program.getListing().getFunctions(true).next() 113 | JAVA 114 | EXPRESSION 115 | 116 | 117 | program.getListing().getFunctions(true) 118 | JAVA 119 | EXPRESSION 120 | 121 | 122 | program.getListing() 123 | JAVA 124 | EXPRESSION 125 | 126 | 127 | externalFunctions. 128 | JAVA 129 | EXPRESSION 130 | 131 | 132 | externalFunctions.next() 133 | JAVA 134 | EXPRESSION 135 | 136 | 137 | externalFunctions.hasNext() 138 | JAVA 139 | EXPRESSION 140 | 141 | 142 | path.substring(1) 143 | JAVA 144 | EXPRESSION 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 159 | 160 | 161 | 162 | 163 | 164 | No facets are configured 165 | 166 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 182 | 183 | 184 | 185 | 186 | 187 | 11 188 | 189 | 194 | 195 | 196 | 197 | 198 | 199 | GhidraDemo 200 | 201 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 217 | 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /GhidraLib/LibGhidra.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 | 17 | /* 18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com 19 | */ 20 | 21 | package com.nosecurecode.libghidra; 22 | 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.net.URI; 26 | import java.net.URISyntaxException; 27 | import java.net.URL; 28 | import java.util.*; 29 | 30 | import generic.stl.Pair; 31 | import ghidra.app.util.opinion.Loader; 32 | import ghidra.framework.OperatingSystem; 33 | import ghidra.framework.Platform; 34 | import ghidra.framework.model.DomainFolder; 35 | import ghidra.framework.protocol.ghidra.Handler; 36 | import ghidra.util.Msg; 37 | import ghidra.util.exception.InvalidInputException; 38 | 39 | /** 40 | * Launcher entry point for running headless Ghidra. 41 | */ 42 | public class LibGhidra { 43 | 44 | private static final int EXIT_CODE_ERROR = 1; 45 | 46 | /** 47 | * Runs headless command 48 | * @param headlessCmd 49 | * @param handler 50 | * @throws Exception 51 | */ 52 | public static void runHeadlessCmd(String headlessCmd, 53 | LibProgramHandler handler) throws Exception { 54 | new LibGhidra(headlessCmd.split("\\s+"), handler); 55 | } 56 | 57 | /** 58 | * Run heqdless command (command line arguments) 59 | * @param headlessCmdArgs 60 | * @param handler 61 | * @throws Exception 62 | */ 63 | public static void runHeadlessCmd(String [] headlessCmdArgs, 64 | LibProgramHandler handler) throws Exception { 65 | new LibGhidra(headlessCmdArgs, handler); 66 | } 67 | 68 | /** 69 | * This is the main entry point 70 | * @param args 71 | * @param handler 72 | * @throws Exception 73 | */ 74 | private LibGhidra(String args[], LibProgramHandler handler) throws Exception { 75 | String projectName = null; 76 | String rootFolderPath = null; 77 | URL ghidraURL = null; 78 | List filesToImport = new ArrayList<>(); 79 | int optionStartIndex; 80 | 81 | // Make sure there are arguments 82 | if (args.length < 1) { 83 | usage(); 84 | } 85 | 86 | // Ghidra URL handler registration 87 | Handler.registerHandler(); 88 | 89 | if (args[0].startsWith("ghidra:")) { 90 | optionStartIndex = 1; 91 | try { 92 | ghidraURL = new URI(args[0]).toURL(); 93 | } 94 | catch (URISyntaxException e) { 95 | System.err.println("Invalid Ghidra URL: " + args[0]); 96 | usage(); 97 | } 98 | } 99 | else { 100 | if (args.length < 2) { 101 | usage(); 102 | } 103 | optionStartIndex = 2; 104 | String projectNameAndFolder = args[1]; 105 | 106 | // Check to see if projectName uses back-slashes (likely if they are using Windows) 107 | projectNameAndFolder = projectNameAndFolder.replaceAll("\\\\", DomainFolder.SEPARATOR); 108 | projectName = projectNameAndFolder; 109 | 110 | rootFolderPath = "/"; 111 | int folderIndex = projectNameAndFolder.indexOf(DomainFolder.SEPARATOR); 112 | if (folderIndex == 0) { 113 | System.err.println(args[1] + " is an invalid project_name/folder_path."); 114 | usage(); 115 | } 116 | else if (folderIndex > 0) { 117 | projectName = projectNameAndFolder.substring(0, folderIndex); 118 | rootFolderPath = projectNameAndFolder.substring(folderIndex); 119 | } 120 | } 121 | 122 | // Determine the desired logging. 123 | File logFile = null; 124 | File scriptLogFile = null; 125 | for (int argi = optionStartIndex; argi < args.length; argi++) { 126 | if (checkArgument("-log", args, argi)) { 127 | logFile = new File(args[++argi]); 128 | } 129 | else if (checkArgument("-scriptlog", args, argi)) { 130 | scriptLogFile = new File(args[++argi]); 131 | } 132 | } 133 | 134 | // Instantiate new headless analyzer and parse options. 135 | LibHeadlessAnalyzer analyzer = 136 | LibHeadlessAnalyzer.getLoggableInstance(logFile, scriptLogFile, true, handler); 137 | LibHeadlessOptions options = analyzer.getOptions(); 138 | parseOptions(options, args, optionStartIndex, ghidraURL, filesToImport); 139 | 140 | // Do the headless processing 141 | try { 142 | if (ghidraURL != null) { 143 | analyzer.processURL(ghidraURL, filesToImport); 144 | } 145 | else { 146 | analyzer.processLocal(args[0], projectName, rootFolderPath, filesToImport); 147 | } 148 | } 149 | catch (Throwable e) { 150 | Msg.error(LibHeadlessAnalyzer.class, 151 | "Abort due to Headless analyzer error: " + e.getMessage(), e); 152 | System.exit(EXIT_CODE_ERROR); 153 | } 154 | } 155 | 156 | /** 157 | * Parses the command line arguments and uses them to set the headless options. 158 | * 159 | * @param options The headless options to set. 160 | * @param args The command line arguments to parse. 161 | * @param startIndex The index into the args array of where to start parsing. 162 | * @param ghidraURL The ghidra server url to connect to, or null if not using a url. 163 | * @param filesToImport A list to put files to import into. 164 | * @throws InvalidInputException if an error occurred parsing the arguments or setting 165 | * the options. 166 | */ 167 | private void parseOptions(LibHeadlessOptions options, String[] args, int startIndex, URL ghidraURL, 168 | List filesToImport) throws InvalidInputException { 169 | 170 | String loaderName = null; 171 | List> loaderArgs = new LinkedList<>(); 172 | String languageId = null; 173 | String compilerSpecId = null; 174 | String keystorePath = null; 175 | String serverUID = null; 176 | boolean allowPasswordPrompt = false; 177 | List> preScripts = new LinkedList<>(); 178 | List> postScripts = new LinkedList<>(); 179 | 180 | for (int argi = startIndex; argi < args.length; argi++) { 181 | 182 | String arg = args[argi]; 183 | if (checkArgument("-log", args, argi)) { 184 | // Already processed 185 | argi++; 186 | } 187 | else if (checkArgument("-scriptlog", args, argi)) { 188 | // Already processed 189 | argi++; 190 | } 191 | else if (arg.equalsIgnoreCase("-overwrite")) { 192 | options.enableOverwriteOnConflict(true); 193 | } 194 | else if (arg.equalsIgnoreCase("-noanalysis")) { 195 | options.enableAnalysis(false); 196 | } 197 | else if (arg.equalsIgnoreCase("-deleteproject")) { 198 | options.setDeleteCreatedProjectOnClose(true); 199 | } 200 | else if (checkArgument("-loader", args, argi)) { 201 | loaderName = args[++argi]; 202 | } 203 | else if (arg.startsWith(Loader.COMMAND_LINE_ARG_PREFIX)) { 204 | if (args[argi + 1].startsWith("-")) { 205 | throw new InvalidInputException(args[argi] + " expects value to follow."); 206 | } 207 | loaderArgs.add(new Pair<>(arg, args[++argi])); 208 | } 209 | else if (checkArgument("-processor", args, argi)) { 210 | languageId = args[++argi]; 211 | } 212 | else if (checkArgument("-cspec", args, argi)) { 213 | compilerSpecId = args[++argi]; 214 | } 215 | else if (checkArgument("-prescript", args, argi)) { 216 | String scriptName = args[++argi]; 217 | String[] scriptArgs = getSubArguments(args, argi); 218 | argi += scriptArgs.length; 219 | preScripts.add(new Pair<>(scriptName, scriptArgs)); 220 | } 221 | else if (checkArgument("-postscript", args, argi)) { 222 | String scriptName = args[++argi]; 223 | String[] scriptArgs = getSubArguments(args, argi); 224 | argi += scriptArgs.length; 225 | postScripts.add(new Pair<>(scriptName, scriptArgs)); 226 | } 227 | else if (checkArgument("-scriptPath", args, argi)) { 228 | options.setScriptDirectories(args[++argi]); 229 | } 230 | else if (checkArgument("-propertiesPath", args, argi)) { 231 | options.setPropertiesFileDirectories(args[++argi]); 232 | } 233 | else if (checkArgument("-import", args, argi)) { 234 | File inputFile = new File(args[++argi]); 235 | if (!inputFile.isDirectory() && !inputFile.isFile()) { 236 | throw new InvalidInputException( 237 | inputFile.getAbsolutePath() + " is not a valid directory or file."); 238 | } 239 | 240 | LibHeadlessAnalyzer.checkValidFilename(inputFile); 241 | 242 | filesToImport.add(inputFile); 243 | 244 | // Keep checking for OS-expanded files 245 | String nextArg; 246 | 247 | while (argi < (args.length - 1)) { 248 | nextArg = args[++argi]; 249 | 250 | // Check if next argument is a parameter 251 | if (nextArg.charAt(0) == '-') { 252 | argi--; 253 | break; 254 | } 255 | 256 | File otherFile = new File(nextArg); 257 | if (!otherFile.isFile() && !otherFile.isDirectory()) { 258 | throw new InvalidInputException( 259 | otherFile.getAbsolutePath() + " is not a valid directory or file."); 260 | } 261 | 262 | LibHeadlessAnalyzer.checkValidFilename(otherFile); 263 | 264 | filesToImport.add(otherFile); 265 | } 266 | } 267 | else if ("-connect".equals(args[argi])) { 268 | if ((argi + 1) < args.length) { 269 | arg = args[argi + 1]; 270 | if (!arg.startsWith("-")) { 271 | // serverUID is optional argument after -connect 272 | serverUID = arg; 273 | ++argi; 274 | } 275 | } 276 | } 277 | else if ("-commit".equals(args[argi])) { 278 | String comment = null; 279 | if ((argi + 1) < args.length) { 280 | arg = args[argi + 1]; 281 | if (!arg.startsWith("-")) { 282 | // comment is optional argument after -commit 283 | comment = arg; 284 | ++argi; 285 | } 286 | } 287 | options.setCommitFiles(true, comment); 288 | } 289 | else if (checkArgument("-keystore", args, argi)) { 290 | keystorePath = args[++argi]; 291 | File keystore = new File(keystorePath); 292 | if (!keystore.isFile()) { 293 | throw new InvalidInputException( 294 | keystore.getAbsolutePath() + " is not a valid keystore file."); 295 | } 296 | } 297 | else if (arg.equalsIgnoreCase("-p")) { 298 | allowPasswordPrompt = true; 299 | } 300 | else if ("-analysisTimeoutPerFile".equalsIgnoreCase(args[argi])) { 301 | options.setPerFileAnalysisTimeout(args[++argi]); 302 | } 303 | else if ("-process".equals(args[argi])) { 304 | if (options.runScriptsNoImport) { 305 | throw new InvalidInputException( 306 | "The -process option may only be specified once."); 307 | } 308 | String processBinary = null; 309 | if ((argi + 1) < args.length) { 310 | arg = args[argi + 1]; 311 | if (!arg.startsWith("-")) { 312 | // processBinary is optional argument after -process 313 | processBinary = arg; 314 | ++argi; 315 | } 316 | } 317 | options.setRunScriptsNoImport(true, processBinary); 318 | } 319 | else if ("-recursive".equals(args[argi])) { 320 | options.enableRecursiveProcessing(true); 321 | } 322 | else if ("-readOnly".equalsIgnoreCase(args[argi])) { 323 | options.enableReadOnlyProcessing(true); 324 | } 325 | else if (checkArgument("-max-cpu", args, argi)) { 326 | String cpuVal = args[++argi]; 327 | try { 328 | options.setMaxCpu(Integer.parseInt(cpuVal)); 329 | } 330 | catch (NumberFormatException nfe) { 331 | throw new InvalidInputException("Invalid value for max-cpu: " + cpuVal); 332 | } 333 | } 334 | else if ("-okToDelete".equalsIgnoreCase(args[argi])) { 335 | options.setOkToDelete(true); 336 | } 337 | else { 338 | throw new InvalidInputException("Bad argument: " + arg); 339 | } 340 | } 341 | 342 | // Set up pre and post scripts 343 | options.setPreScriptsWithArgs(preScripts); 344 | options.setPostScriptsWithArgs(postScripts); 345 | 346 | // Set loader and loader args 347 | options.setLoader(loaderName, loaderArgs); 348 | 349 | // Set user-specified language and compiler spec 350 | options.setLanguageAndCompiler(languageId, compilerSpecId); 351 | 352 | // Set up optional Ghidra Server authenticator 353 | try { 354 | options.setClientCredentials(serverUID, keystorePath, allowPasswordPrompt); 355 | } 356 | catch (IOException e) { 357 | throw new InvalidInputException( 358 | "Failed to install Ghidra Server authenticator: " + e.getMessage()); 359 | } 360 | 361 | // If -process was specified, inputFiles must be null or inputFiles.size must be 0. 362 | // Otherwise when not in -process mode, inputFiles can be null or inputFiles.size can be 0, 363 | // only if there are scripts to be run. 364 | if (options.runScriptsNoImport) { 365 | 366 | if (filesToImport != null && filesToImport.size() > 0) { 367 | System.err.print("Must use either -process or -import parameters, but not both."); 368 | System.err.print(" -process runs scripts over existing program(s) in a project, " + 369 | "whereas -import"); 370 | System.err.println(" imports new programs and runs scripts and/or analyzes them " + 371 | "after import."); 372 | System.exit(EXIT_CODE_ERROR); 373 | } 374 | 375 | if (options.overwrite) { 376 | Msg.warn(LibHeadlessAnalyzer.class, 377 | "The -overwrite parameter does not apply to -process mode. Ignoring overwrite " + 378 | "and continuing."); 379 | } 380 | 381 | if (options.readOnly && options.okToDelete) { 382 | System.err.println("You have specified the conflicting parameters -readOnly and " + 383 | "-okToDelete. Please pick one and try again."); 384 | System.exit(EXIT_CODE_ERROR); 385 | } 386 | } 387 | else { 388 | if (filesToImport == null || filesToImport.size() == 0) { 389 | if (options.preScripts.isEmpty() && options.postScripts.isEmpty()) { 390 | System.err.println("Nothing to do ... must specify -import, -process, or " + 391 | "prescript and/or postscript."); 392 | System.exit(EXIT_CODE_ERROR); 393 | } 394 | else { 395 | Msg.warn(LibHeadlessAnalyzer.class, 396 | "Neither the -import parameter nor the -process parameter was specified; " + 397 | "therefore, the specified prescripts and/or postscripts will be " + 398 | "executed without any type of program context."); 399 | } 400 | } 401 | } 402 | 403 | if (options.commit) { 404 | if (options.readOnly) { 405 | System.err.println("Can not use -commit and -readOnly at the same time."); 406 | System.exit(EXIT_CODE_ERROR); 407 | } 408 | } 409 | 410 | // Implied commit, only if not in process mode 411 | if (!options.commit && ghidraURL != null) { 412 | if (!options.readOnly) { 413 | // implied commit 414 | options.setCommitFiles(true, null); 415 | } 416 | else { 417 | Msg.warn(LibHeadlessAnalyzer.class, 418 | "-readOnly mode is on: for -process, changes will not be saved."); 419 | } 420 | } 421 | } 422 | 423 | /** 424 | * Prints out the usage details and exits the Java application with an exit code that 425 | * indicates error. 426 | * 427 | * @param execCmd the command used to run the headless analyzer from the calling method. 428 | */ 429 | public static void usage(String execCmd) { 430 | System.out.println("Headless Analyzer Usage: " + execCmd); 431 | System.out.println(" [/]"); 432 | System.out.println( 433 | " | ghidra://[:]/[/]"); 434 | System.out.println( 435 | " [[-import [|]+] | [-process []]]"); 436 | System.out.println(" [-preScript ]"); 437 | System.out.println(" [-postScript ]"); 438 | System.out.println(" [-scriptPath \"[;...]\"]"); 439 | System.out.println(" [-propertiesPath \"[;...]\"]"); 440 | System.out.println(" [-scriptlog ]"); 441 | System.out.println(" [-log ]"); 442 | System.out.println(" [-overwrite]"); 443 | System.out.println(" [-recursive]"); 444 | System.out.println(" [-readOnly]"); 445 | System.out.println(" [-deleteProject]"); 446 | System.out.println(" [-noanalysis]"); 447 | System.out.println(" [-processor ]"); 448 | System.out.println(" [-cspec ]"); 449 | System.out.println(" [-analysisTimeoutPerFile ]"); 450 | System.out.println(" [-keystore ]"); 451 | System.out.println(" [-connect ]"); 452 | System.out.println(" [-p]"); 453 | System.out.println(" [-commit [\"\"]]"); 454 | System.out.println(" [-okToDelete]"); 455 | System.out.println(" [-max-cpu ]"); 456 | System.out.println(" [-loader ]"); 457 | // ** NOTE: please update 'analyzeHeadlessREADME.html' if changing command line parameters ** 458 | 459 | if (Platform.CURRENT_PLATFORM.getOperatingSystem() != OperatingSystem.WINDOWS) { 460 | System.out.println(); 461 | System.out.println( 462 | " - All uses of $GHIDRA_HOME or $USER_HOME in script path must be" + 463 | " preceded by '\\'"); 464 | } 465 | System.out.println(); 466 | System.out.println( 467 | "Please refer to 'analyzeHeadlessREADME.html' for detailed usage examples " + 468 | "and notes."); 469 | 470 | System.out.println(); 471 | System.exit(EXIT_CODE_ERROR); 472 | } 473 | 474 | private void usage() { 475 | usage("analyzeHeadless"); 476 | } 477 | 478 | private String[] getSubArguments(String[] args, int argi) { 479 | List subArgs = new LinkedList<>(); 480 | int i = argi + 1; 481 | while (i < args.length && !args[i].startsWith("-")) { 482 | subArgs.add(args[i++]); 483 | } 484 | return subArgs.toArray(new String[0]); 485 | } 486 | 487 | private boolean checkArgument(String optionName, String[] args, int argi) 488 | throws InvalidInputException { 489 | // everything after this requires an argument 490 | if (!optionName.equalsIgnoreCase(args[argi])) { 491 | return false; 492 | } 493 | if (argi + 1 == args.length) { 494 | throw new InvalidInputException(optionName + " requires an argument"); 495 | } 496 | return true; 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /GhidraLib/LibHeadlessOptions.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 | 17 | /* 18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com 19 | */ 20 | 21 | package com.nosecurecode.libghidra; 22 | 23 | import java.io.IOException; 24 | import java.util.*; 25 | 26 | import generic.jar.ResourceFile; 27 | import generic.stl.Pair; 28 | import ghidra.app.util.opinion.Loader; 29 | import ghidra.app.util.opinion.LoaderService; 30 | import ghidra.framework.client.HeadlessClientAuthenticator; 31 | import ghidra.program.model.lang.*; 32 | import ghidra.program.util.DefaultLanguageService; 33 | import ghidra.util.exception.InvalidInputException; 34 | 35 | /** 36 | * Options for headless analyzer. 37 | *

38 | * Option state may be adjusted to reflect assumed options 39 | * during processing. If multiple invocations of either 40 | * {@link LibHeadlessAnalyzer#processLocal(String, String, String, List)} or 41 | * {@link LibHeadlessAnalyzer#processURL(java.net.URL, List)} are performed, 42 | * these options should be reset and adjusted as necessary. 43 | */ 44 | 45 | public class LibHeadlessOptions { 46 | 47 | // -process and -import 48 | String domainFileNameToProcess; // may include pattern 49 | boolean runScriptsNoImport; 50 | 51 | // -preScript 52 | List> preScripts; 53 | Map preScriptFileMap; 54 | 55 | // -postScript 56 | List> postScripts; 57 | Map postScriptFileMap; 58 | 59 | // -scriptPath 60 | List scriptPaths; 61 | 62 | // -propertiesPath 63 | List propertiesFileStrPaths; 64 | List propertiesFilePaths; 65 | 66 | // -overwrite 67 | boolean overwrite; 68 | 69 | // -recursive 70 | boolean recursive; 71 | 72 | // -readOnly 73 | boolean readOnly; 74 | 75 | // -deleteProject 76 | boolean deleteProject; 77 | 78 | // -noanalysis 79 | boolean analyze; 80 | 81 | // -processor 82 | Language language; 83 | 84 | // -cspec 85 | CompilerSpec compilerSpec; 86 | 87 | // -analysisTimeoutPerFile 88 | int perFileTimeout; 89 | 90 | // -keystore 91 | String keystore; 92 | 93 | // -connect 94 | String connectUserID; 95 | 96 | // -p 97 | boolean allowPasswordPrompt; 98 | 99 | // -commit 100 | boolean commit; 101 | String commitComment; 102 | 103 | // -okToDelete 104 | boolean okToDelete; 105 | 106 | // -max-cpu 107 | int maxcpu; 108 | 109 | // -loader 110 | Class loaderClass; 111 | List> loaderArgs; 112 | 113 | // ------------------------------------------------------------------------------------------- 114 | 115 | /** 116 | * Creates a new headless options object with default settings. 117 | */ 118 | LibHeadlessOptions() { 119 | reset(); 120 | } 121 | 122 | /** 123 | * Resets the options to its default settings. 124 | */ 125 | public void reset() { 126 | domainFileNameToProcess = null; 127 | runScriptsNoImport = false; 128 | preScripts = new LinkedList<>(); 129 | preScriptFileMap = null; 130 | postScripts = new LinkedList<>(); 131 | postScriptFileMap = null; 132 | scriptPaths = null; 133 | propertiesFileStrPaths = new ArrayList<>(); 134 | propertiesFilePaths = new ArrayList<>(); 135 | overwrite = false; 136 | recursive = false; 137 | readOnly = false; 138 | deleteProject = false; 139 | analyze = true; 140 | language = null; 141 | compilerSpec = null; 142 | perFileTimeout = -1; 143 | keystore = null; 144 | connectUserID = null; 145 | allowPasswordPrompt = false; 146 | commit = false; 147 | commitComment = null; 148 | okToDelete = false; 149 | maxcpu = 0; 150 | loaderClass = null; 151 | loaderArgs = null; 152 | } 153 | 154 | /** 155 | * Set to run scripts (and optionally, analysis) without importing a 156 | * program. Scripts will run on specified folder or program that already 157 | * exists in the project. 158 | * 159 | * @param runScriptsOnly if true, no imports will occur and scripts 160 | * (and analysis, if enabled) will run on the specified existing program 161 | * or directory of programs. 162 | * @param filename name of specific project file or folder to be processed (the location 163 | * is passed in elsewhere by the user). If null, user has not specified 164 | * a file to process -- therefore, the entire directory will be processed. 165 | * The filename should not include folder path elements which should be 166 | * specified separately via project or URL specification. 167 | * @throws IllegalArgumentException if the specified filename is invalid and contains the 168 | * path separator character '/'. 169 | */ 170 | public void setRunScriptsNoImport(boolean runScriptsOnly, String filename) { 171 | if (filename != null) { 172 | filename = filename.trim(); 173 | if (filename.indexOf("/") >= 0) { 174 | throw new IllegalArgumentException("invalid filename specified"); 175 | } 176 | } 177 | this.runScriptsNoImport = runScriptsOnly; 178 | this.domainFileNameToProcess = filename; 179 | } 180 | 181 | /** 182 | * Set the ordered list of scripts to execute immediately following import and 183 | * prior to analyzing an imported program. If import not performed, 184 | * these scripts will execute once prior to any post-scripts. 185 | * 186 | * @param preScripts list of script names 187 | */ 188 | public void setPreScripts(List preScripts) { 189 | List> preScriptsEmptyArgs = new LinkedList<>(); 190 | for (String preScript : preScripts) { 191 | preScriptsEmptyArgs.add(new Pair<>(preScript, new String[0])); 192 | } 193 | setPreScriptsWithArgs(preScriptsEmptyArgs); 194 | } 195 | 196 | /** 197 | * Set the ordered list of scripts and their arguments to execute immediately following import 198 | * and prior to analyzing an imported program. If import not performed, 199 | * these scripts will execute once prior to any post-scripts. 200 | * 201 | * @param preScripts list of script names/script argument pairs 202 | */ 203 | public void setPreScriptsWithArgs(List> preScripts) { 204 | this.preScripts = preScripts; 205 | this.preScriptFileMap = null; 206 | } 207 | 208 | /** 209 | * Set the ordered list of scripts to execute immediately following import and 210 | * and analysis of a program. If import not performed, 211 | * these scripts will execute once following any pre-scripts. 212 | * 213 | * @param postScripts list of script names 214 | */ 215 | public void setPostScripts(List postScripts) { 216 | List> postScriptsEmptyArgs = new LinkedList<>(); 217 | for (String postScript : postScripts) { 218 | postScriptsEmptyArgs.add(new Pair<>(postScript, new String[0])); 219 | } 220 | setPostScriptsWithArgs(postScriptsEmptyArgs); 221 | } 222 | 223 | /** 224 | * Set the ordered list of scripts to execute immediately following import and 225 | * and analysis of a program. If import not performed, 226 | * these scripts will execute once following any pre-scripts. 227 | * 228 | * @param postScripts list of script names/script argument pairs 229 | */ 230 | public void setPostScriptsWithArgs(List> postScripts) { 231 | this.postScripts = postScripts; 232 | this.postScriptFileMap = null; 233 | } 234 | 235 | /** 236 | * Set the script source directories to be searched for secondary scripts. 237 | * The default set of enabled script directories within the Ghidra installation 238 | * will be appended to the specified list of newPaths. 239 | * Individual Paths may be constructed relative to Ghidra installation directory, 240 | * User home directory, or absolute system paths. Examples: 241 | *

242 | 	 *     Path.GHIDRA_HOME + "/Ghidra/Features/Base/ghidra_scripts"
243 | 	 *     Path.USER_HOME + "/Ghidra/Features/Base/ghidra_scripts"
244 | 	 *     "/shared/ghidra_scripts"
245 | 	 * 
246 | * 247 | * @param newPaths list of directories to be searched. 248 | */ 249 | public void setScriptDirectories(List newPaths) { 250 | scriptPaths = newPaths; 251 | } 252 | 253 | /** 254 | * List of valid script directory paths separated by a ';'. 255 | * The default set of enabled script directories within the Ghidra installation 256 | * will be appended to the specified list of newPaths. 257 | * Individual Paths may be constructed relative to Ghidra installation directory, 258 | * User home directory, or absolute system paths. Examples: 259 | *
260 | 	 * 		Path.GHIDRA_HOME + "/Ghidra/Features/Base/ghidra_scripts"
261 | 	 *      Path.USER_HOME + "/Ghidra/Features/Base/ghidra_scripts"
262 | 	 *		"/shared/ghidra_scripts"
263 | 	 * 
264 | * @param paths semicolon (';') separated list of directory paths 265 | */ 266 | public void setScriptDirectories(String paths) { 267 | String[] pathArray = paths.split(";"); 268 | setScriptDirectories(Arrays.asList(pathArray)); 269 | } 270 | 271 | /** 272 | * Sets a single location for .properties files associated with GhidraScripts. 273 | * 274 | * Typically, .properties files should be located in the same directory as their corresponding 275 | * scripts. However, this method may need to be used when circumstances make it impossible to 276 | * have both files in the same directory (i.e., if the scripts are included in ghidra.jar). 277 | * 278 | * @param path location of .properties file(s) 279 | */ 280 | public void setPropertiesFileDirectory(String path) { 281 | propertiesFileStrPaths = new ArrayList<>(); 282 | propertiesFileStrPaths.add(path); 283 | } 284 | 285 | /** 286 | * Sets one or more locations to find .properties files associated with GhidraScripts. 287 | * 288 | * Typically, .properties files should be located in the same directory as their corresponding 289 | * scripts. However, this method may need to be used when circumstances make it impossible to 290 | * have both files in the same directory (i.e., if the scripts are included in ghidra.jar). 291 | * 292 | * @param newPaths potential locations of .properties file(s) 293 | */ 294 | public void setPropertiesFileDirectories(List newPaths) { 295 | propertiesFileStrPaths = newPaths; 296 | } 297 | 298 | /** 299 | * List of valid .properties file directory paths, separated by a ';'. 300 | * 301 | * Typically, .properties files should be located in the same directory as their corresponding 302 | * scripts. However, this method may need to be used when circumstances make it impossible to 303 | * have both files in the same directory (i.e., if the scripts are included in ghidra.jar). 304 | * 305 | * @param paths String representation of directories (each separated by ';') 306 | */ 307 | public void setPropertiesFileDirectories(String paths) { 308 | String[] pathArray = paths.split(";"); 309 | setPropertiesFileDirectories(Arrays.asList(pathArray)); 310 | } 311 | 312 | /** 313 | * During import, the default behavior is to skip the import if a conflict occurs 314 | * within the destination folder. This method can be used to force the original 315 | * conflicting file to be removed prior to import. 316 | * If the pre-existing file is versioned, the commit option must also be 317 | * enabled to have the overwrite remove the versioned file. 318 | * 319 | * @param enabled if true conflicting domain files will be removed from the 320 | * project prior to importing the new file. 321 | */ 322 | public void enableOverwriteOnConflict(boolean enabled) { 323 | this.overwrite = enabled; 324 | } 325 | 326 | /** 327 | * This method can be used to enable recursive processing of files during 328 | * -import or -process modes. In order for recursive processing of files to 329 | * occur, the user must have specified a directory (and not a specific file) 330 | * for the Headless Analyzer to import or process. 331 | * 332 | * @param enabled if true, enables recursive processing 333 | */ 334 | public void enableRecursiveProcessing(boolean enabled) { 335 | this.recursive = enabled; 336 | } 337 | 338 | /** 339 | * When readOnly processing is enabled, any changes made by script or analyzers 340 | * are discarded when the Headless Analyzer exits. When used with import mode, 341 | * the imported program file will not be saved to the project or repository. 342 | * 343 | * @param enabled if true, enables readOnly processing or import 344 | */ 345 | public void enableReadOnlyProcessing(boolean enabled) { 346 | this.readOnly = enabled; 347 | } 348 | 349 | /** 350 | * Set project delete flag which allows temporary projects created 351 | * to be deleted upon completion. This option has no effect if a 352 | * Ghidra URL or an existing project was specified. This option 353 | * will be assumed when importing with the readOnly option enabled. 354 | * 355 | * @param enabled if true a created project will be deleted when 356 | * processing is complete. 357 | */ 358 | public void setDeleteCreatedProjectOnClose(boolean enabled) { 359 | this.deleteProject = enabled; 360 | } 361 | 362 | /** 363 | * Auto-analysis is enabled by default following import. This method can be 364 | * used to change the enablement of auto-analysis. 365 | * 366 | * @param enabled True if auto-analysis should be enabled; otherwise, false. 367 | */ 368 | public void enableAnalysis(boolean enabled) { 369 | this.analyze = enabled; 370 | } 371 | 372 | /** 373 | * Sets the language and compiler spec from the provided input. Any null value will attempt 374 | * a "best-guess" if possible. 375 | * 376 | * @param languageId The language to set. 377 | * @param compilerSpecId The compiler spec to set. 378 | * @throws InvalidInputException if the language and compiler spec combination is not valid. 379 | */ 380 | public void setLanguageAndCompiler(String languageId, String compilerSpecId) 381 | throws InvalidInputException { 382 | if (languageId == null && compilerSpecId == null) { 383 | return; 384 | } 385 | if (languageId == null) { 386 | throw new InvalidInputException("Compiler spec specified without specifying language."); 387 | } 388 | try { 389 | language = 390 | DefaultLanguageService.getLanguageService().getLanguage(new LanguageID(languageId)); 391 | if (compilerSpecId == null) { 392 | compilerSpec = language.getDefaultCompilerSpec(); 393 | } 394 | else { 395 | compilerSpec = language.getCompilerSpecByID(new CompilerSpecID(compilerSpecId)); 396 | } 397 | } 398 | catch (LanguageNotFoundException e) { 399 | language = null; 400 | compilerSpec = null; 401 | throw new InvalidInputException("Unsupported language: " + languageId); 402 | } 403 | catch (CompilerSpecNotFoundException e) { 404 | language = null; 405 | compilerSpec = null; 406 | throw new InvalidInputException("Compiler spec \"" + compilerSpecId + 407 | "\" is not supported for language \"" + languageId + "\""); 408 | } 409 | } 410 | 411 | /** 412 | * Set analyzer timeout on a per-file basis. 413 | * 414 | * @param stringInSecs timeout value in seconds (as a String) 415 | * @throws InvalidInputException if the timeout value was not a valid value 416 | */ 417 | public void setPerFileAnalysisTimeout(String stringInSecs) throws InvalidInputException { 418 | try { 419 | perFileTimeout = Integer.parseInt(stringInSecs); 420 | } 421 | catch (NumberFormatException nfe) { 422 | throw new InvalidInputException( 423 | "'" + stringInSecs + "' is not a valid integer representation."); 424 | } 425 | } 426 | 427 | public void setPerFileAnalysisTimeout(int secs) { 428 | perFileTimeout = secs; 429 | } 430 | 431 | /** 432 | * Set Ghidra Server client credentials to be used with "shared" projects. 433 | * 434 | * @param userID optional userId to use if server permits the user to use 435 | * a userId which differs from the process owner name. 436 | * @param keystorePath file path to keystore file containing users private key 437 | * to be used with PKI or SSH based authentication. 438 | * @param allowPasswordPrompt if true the user may be prompted for passwords 439 | * via the console (stdin). Please note that the Java console will echo 440 | * the password entry to the terminal which may be undesirable. 441 | * @throws IOException if an error occurs while opening the specified keystorePath. 442 | */ 443 | public void setClientCredentials(String userID, String keystorePath, 444 | boolean allowPasswordPrompt) throws IOException { 445 | this.connectUserID = userID; 446 | this.keystore = keystorePath; 447 | this.allowPasswordPrompt = allowPasswordPrompt; 448 | HeadlessClientAuthenticator.installHeadlessClientAuthenticator(userID, keystorePath, 449 | allowPasswordPrompt); 450 | } 451 | 452 | /** 453 | * Enable committing of processed files to the repository which backs the specified 454 | * project. 455 | * 456 | * @param commit if true imported files will be committed 457 | * @param comment optional comment to use when committing 458 | */ 459 | public void setCommitFiles(boolean commit, String comment) { 460 | this.commit = commit; 461 | this.commitComment = comment; 462 | } 463 | 464 | public void setOkToDelete(boolean deleteOk) { 465 | okToDelete = deleteOk; 466 | } 467 | 468 | /** 469 | * Sets the maximum number of cpu cores to use during headless processing. 470 | * 471 | * @param cpu The maximum number of cpu cores to use during headless processing. 472 | * Setting it to 0 or a negative integer is equivalent to setting it to 1. 473 | */ 474 | public void setMaxCpu(int cpu) { 475 | this.maxcpu = cpu; 476 | System.setProperty("cpu.core.limit", Integer.toString(cpu)); 477 | 478 | } 479 | 480 | /** 481 | * Sets the loader to use for imports, as well as any loader-specific arguments. A null loader 482 | * will attempt "best-guess" if possible. Loader arguments are not supported if a "best-guess" 483 | * is made. 484 | * 485 | * @param loaderName The name (simple class name) of the loader to use. 486 | * @param loaderArgs A list of loader-specific arguments. Could be null if there are none. 487 | * @throws InvalidInputException if an invalid loader name was specified, or if loader arguments 488 | * were specified but a loader was not. 489 | */ 490 | public void setLoader(String loaderName, List> loaderArgs) 491 | throws InvalidInputException { 492 | if (loaderName != null) { 493 | this.loaderClass = LoaderService.getLoaderClassByName(loaderName); 494 | if (this.loaderClass == null) { 495 | throw new InvalidInputException("Invalid loader name specified: " + loaderName); 496 | } 497 | this.loaderArgs = loaderArgs; 498 | } 499 | else { 500 | if (loaderArgs != null && loaderArgs.size() > 0) { 501 | throw new InvalidInputException( 502 | "Loader arguments defined without a loader being specified."); 503 | } 504 | this.loaderClass = null; 505 | this.loaderArgs = null; 506 | } 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /GhidraLib/LibHeadlessScript.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 | 17 | /* 18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com 19 | */ 20 | 21 | package com.nosecurecode.libghidra; 22 | 23 | import java.io.IOException; 24 | 25 | import generic.jar.ResourceFile; 26 | import ghidra.app.script.*; 27 | import ghidra.framework.model.DomainFolder; 28 | import ghidra.util.InvalidNameException; 29 | 30 | /** 31 | * This class is analogous to GhidraScript, except that is only meant to be used with 32 | * the HeadlessAnalyzer. That is, if a user writes a script that extends HeadlessScript, 33 | * it should only be run in the Headless environment. 34 | */ 35 | public abstract class LibHeadlessScript extends GhidraScript { 36 | 37 | /** 38 | * Options for controlling disposition of program after the current script completes. 39 | */ 40 | public enum LibHeadlessContinuationOption { 41 | /** 42 | * Continue running scripts and/or analysis; -import and -process 43 | * modes complete normally. 44 | */ 45 | CONTINUE, 46 | 47 | /** 48 | * Continue running scripts and/or analysis; 49 | * -import mode does not save program, 50 | * -process mode deletes program. 51 | */ 52 | CONTINUE_THEN_DELETE, 53 | 54 | /** 55 | * Abort any scripts or analysis that come after this script; 56 | * -import mode does not save program, -process mode deletes program. 57 | */ 58 | ABORT_AND_DELETE, 59 | 60 | /** 61 | * Abort any scripts or analysis that come after this script; -import mode does 62 | * save program (but it may not be processed completely), 63 | * -process mode completes normally, minus scripts or analysis that 64 | * runs after the ABORT request. 65 | */ 66 | ABORT 67 | } 68 | 69 | private LibHeadlessAnalyzer headless = null; 70 | 71 | private LibHeadlessContinuationOption currentOption = LibHeadlessContinuationOption.CONTINUE; 72 | private LibHeadlessContinuationOption scriptSetOption = null; 73 | 74 | private boolean runningInnerScript = false; 75 | 76 | // This is necessary because it determine when we nullify the 'scriptSetOption' variable 77 | private void setRunningInnerScript(boolean b) { 78 | runningInnerScript = b; 79 | } 80 | 81 | /** 82 | * Sets the current headless instance -- doing so gives the user the ability to manipulate 83 | * headless analyzer-specific parameters. 84 | *

85 | * This method is declared with no access modifier to only allow package-level (no subclass) 86 | * access. This method is meant to only be used by the HeadlessAnalyzer class. 87 | * 88 | * @param ha HeadlessAnalyzer instance 89 | */ 90 | void setHeadlessInstance(LibHeadlessAnalyzer ha) { 91 | headless = ha; 92 | } 93 | 94 | /** 95 | * Sets the "beginning-of-script" continuation status. 96 | *

97 | * This method is declare with no access modifier to only allow package-level (no 98 | * subclass) access. This method is meant to only be used by the HeadlessAnalyzer class. 99 | * 100 | * @param option initial continuation option for this script 101 | */ 102 | void setInitialContinuationOption(LibHeadlessContinuationOption option) { 103 | currentOption = option; 104 | } 105 | 106 | /** 107 | * Returns the final resolved continuation option (after script processing is done). 108 | *

109 | * The continuation option specifies whether to continue or abort follow-on processing, 110 | * and whether to delete or keep the current program. 111 | *

112 | * This method is declared with no access modifier to only allow package-level (no 113 | * subclass) access. This method is meant to only be used by the HeadlessAnalyzer class. 114 | * 115 | * @return the script's final HeadlessContinuationOption 116 | */ 117 | LibHeadlessContinuationOption getContinuationOption() { 118 | return currentOption; 119 | } 120 | 121 | /** 122 | * Checks to see if this script is running in headless mode (it should be!). 123 | *

124 | * This method should be called at the beginning of every public method in HeadlessScript 125 | * that accesses HeadlessAnalyzer methods (for instance, 'headless.isAnalysisEnabled()'). 126 | * The call to this method can not be placed in the constructor, because 'setHeadlessInstance', 127 | * which connects the script with the current headless instance, is not called until after the 128 | * call to the constructor. 129 | * 130 | * @throws ImproperUseException if not in headless mode or headless instance not set 131 | */ 132 | private void checkHeadlessStatus() throws ImproperUseException { 133 | if (headless == null || !isRunningHeadless()) { 134 | throw new ImproperUseException("This method can only be used in the headless case!"); 135 | } 136 | } 137 | 138 | /** 139 | * Stores a key/value pair in the HeadlessAnalyzer instance for later use. 140 | *

141 | * This method, along with the 'getStoredHeadlessValue' method, is useful for debugging and 142 | * testing the Headless Analyzer (when the user has directly instantiated the HeadlessAnalyzer 143 | * instead of running it from analyzeHeadless.sh or analyzeHeadless.bat). This method is 144 | * intended to allow a HeadlessScript to store variables that reflect the current state of 145 | * processing (at the time the script is being run). Storing variables in the HeadlessAnalyzer 146 | * instance may be the only way to access the state of processing during cases when the user 147 | * is forced to run in -readOnly mode, or if there is a value that is only accessible at the 148 | * scripts stage. 149 | * 150 | * @param key storage key in String form 151 | * @param value value to store 152 | * @throws ImproperUseException if not in headless mode or headless instance not set 153 | * @see #getStoredHeadlessValue(String) 154 | * @see #headlessStorageContainsKey(String) 155 | */ 156 | public void storeHeadlessValue(String key, Object value) throws ImproperUseException { 157 | checkHeadlessStatus(); 158 | headless.addVariableToStorage(key, value); 159 | } 160 | 161 | /** 162 | * Get stored value by key from the HeadlessAnalyzer instance. 163 | *

164 | * This method, along with the 'storedHeadlessValue' method, is useful for debugging and 165 | * testing the Headless Analyzer (when the user has directly instantiated the HeadlessAnalyzer 166 | * instead of running it from analyzeHeadless.sh or analyzeHeadless.bat). This method is 167 | * intended to allow a HeadlessScript to store variables that reflect the current state of 168 | * processing (at the time the script is being run). Storing variables in the HeadlessAnalyzer 169 | * instance may be the only way to access the state of processing during cases when the user 170 | * is forced to run in -readOnly mode, or if there is a value that is only accessible at the 171 | * scripts stage. 172 | * 173 | * @param key key to retrieve the desired stored value 174 | * @return stored Object, or null if none exists for that key 175 | * @throws ImproperUseException if not in headless mode or headless instance not set 176 | * @see #storeHeadlessValue(String, Object) 177 | * @see #headlessStorageContainsKey(String) 178 | */ 179 | public Object getStoredHeadlessValue(String key) throws ImproperUseException { 180 | checkHeadlessStatus(); 181 | return headless.getVariableFromStorage(key); 182 | } 183 | 184 | /** 185 | * Returns whether the specified key was stored in the HeadlessAnalyzer instance. 186 | * 187 | * @param key value of key to check for in Headless Analyzer instance 188 | * @return true if the specified key exists 189 | * @throws ImproperUseException if not in headless mode or headless instance not set 190 | * @see #storeHeadlessValue(String, Object) 191 | * @see #getStoredHeadlessValue(String) 192 | */ 193 | public boolean headlessStorageContainsKey(String key) throws ImproperUseException { 194 | checkHeadlessStatus(); 195 | return headless.storageContainsKey(key); 196 | } 197 | 198 | /** 199 | * Sets the continuation option for this script 200 | *

201 | * The continuation option specifies whether to continue or abort follow-on processing, 202 | * and whether to delete or keep the current program. 203 | * 204 | * @param option HeadlessContinuationOption set by this script 205 | * @see #getHeadlessContinuationOption() 206 | */ 207 | public void setHeadlessContinuationOption(LibHeadlessContinuationOption option) { 208 | scriptSetOption = option; 209 | } 210 | 211 | /** 212 | * Returns the continuation option for the current script (if one has not been set in this 213 | * script, the option defaults to CONTINUE). 214 | *

215 | * The continuation option specifies whether to continue or abort follow-on processing, 216 | * and whether to delete or keep the current program. 217 | * 218 | * @return the current HeadlessContinuationOption 219 | * @see #setHeadlessContinuationOption(LibHeadlessContinuationOption) 220 | */ 221 | public LibHeadlessContinuationOption getHeadlessContinuationOption() { 222 | if (scriptSetOption == null) { 223 | return LibHeadlessContinuationOption.CONTINUE; 224 | } 225 | 226 | return scriptSetOption; 227 | } 228 | 229 | /** 230 | * Enables or disables analysis according to the passed-in boolean value. 231 | *

232 | * A script that calls this method should run as a 'preScript', since preScripts 233 | * execute before analysis would typically run. Running the script as a 'postScript' 234 | * is ineffective, since the stage at which analysis would have happened has already 235 | * passed. 236 | *

237 | * This change will persist throughout the current HeadlessAnalyzer session, unless 238 | * changed again (in other words, once analysis is enabled via script for one program, 239 | * it will also be enabled for future programs in the current session, unless changed). 240 | * 241 | * @param b true to enable analysis, false to disable analysis 242 | * @throws ImproperUseException if not in headless mode or headless instance not set 243 | * @see #isHeadlessAnalysisEnabled() 244 | */ 245 | public void enableHeadlessAnalysis(boolean b) throws ImproperUseException { 246 | checkHeadlessStatus(); 247 | 248 | headless.getOptions().enableAnalysis(b); 249 | } 250 | 251 | /** 252 | * Returns whether analysis is currently enabled or disabled in the HeadlessAnalyzer. 253 | * 254 | * @return whether analysis has been enabled or not 255 | * @throws ImproperUseException if not in headless mode or headless instance not set 256 | * @see #enableHeadlessAnalysis(boolean) 257 | */ 258 | public boolean isHeadlessAnalysisEnabled() throws ImproperUseException { 259 | checkHeadlessStatus(); 260 | 261 | return headless.getOptions().analyze; 262 | } 263 | 264 | /** 265 | * Returns whether the headless analyzer is currently set to -import mode or not (if not, 266 | * it is in -process mode). The use of -import mode implies that binaries are actively being 267 | * imported into the project (with optional scripts/analysis). The use of -process mode implies 268 | * that existing project files are being processed (using scripts and/or analysis). 269 | * 270 | * @return whether we are in -import mode or not 271 | * @throws ImproperUseException if not in headless mode or headless instance not set 272 | */ 273 | public boolean isImporting() throws ImproperUseException { 274 | checkHeadlessStatus(); 275 | 276 | return !headless.getOptions().runScriptsNoImport; 277 | } 278 | 279 | /** 280 | * Changes the path in the Ghidra project where imported files are saved. 281 | * The passed-in path is assumed to be relative to the project root. For example, 282 | * if the directory structure for the Ghidra project looks like this: 283 | * 284 | *

285 | 	 * 		MyGhidraProject:
286 | 	 * 		  /dir1
287 | 	 * 		    /innerDir1
288 | 	 * 		    /innerDir2
289 | 	 * 
290 | * 291 | * Then the following usage would ensure that any files imported after this call would 292 | * be saved in the MyGhidraProject:/dir1/innerDir2 folder. 293 | *
294 | 	 * 		setHeadlessImportDirectory("dir1/innerDir2");
295 | 	 * 
296 | * In contrast, the following usages would add new folders to the Ghidra project and save 297 | * the imported files into the newly-created path: 298 | *
299 | 	 * 		setHeadlessImportDirectory("innerDir2/my/folder");
300 | 	 * 
301 | * changes the directory structure to: 302 | *
303 | 	 * 		MyGhidraProject:
304 | 	 * 		  /dir1
305 | 	 * 		    /innerDir1
306 | 	 * 		    /innerDir2
307 | 	 * 		      /my
308 | 	 * 		        /folder
309 | 	 * 
310 | * and: 311 | *
312 | 	 * 		setHeadlessImportDirectory("newDir/saveHere");
313 | 	 * 
314 | * changes the directory structure to: 315 | *
316 | 	 * 		MyGhidraProject:
317 | 	 * 		  /dir1
318 | 	 * 		    /innerDir1
319 | 	 * 			/innerDir2
320 | 	 *		  /newDir
321 | 	 * 		    /saveHere
322 | 	 * 
323 | * As in the examples above, if the desired folder does not already exist, it is created. 324 | *

325 | * A change in the import save folder will persist throughout the current HeadlessAnalyzer 326 | * session, unless changed again (in other words, once the import directory has been changed, 327 | * it will remain the 'save' directory for import files in the current session, unless changed). 328 | *

329 | * To revert back to the default import location (that which was specified via command line), 330 | * pass the null object as the argument to this method, as below: 331 | *

332 | 	 * 		setHeadlessImportDirectory(null);	// Sets import save directory to default
333 | 	 * 
334 | * If a file with the same name already exists in the desired location, it will only be 335 | * overwritten if "-overwrite" is true. 336 | *

337 | * This method is only applicable when using the HeadlessAnalyzer -import mode and 338 | * is ineffective in -process mode. 339 | * 340 | * @param importDir the absolute path (relative to root) where inputs will be saved 341 | * @throws ImproperUseException if not in headless mode or headless instance not set 342 | * @throws IOException if there are issues creating the folder 343 | * @throws InvalidNameException if folder name is invalid 344 | */ 345 | public void setHeadlessImportDirectory(String importDir) 346 | throws ImproperUseException, IOException, InvalidNameException { 347 | checkHeadlessStatus(); 348 | 349 | // Do nothing if not importing -- we don't want to have arbitrary folders 350 | // created when not being used! 351 | 352 | if (!headless.getOptions().runScriptsNoImport) { 353 | DomainFolder saveFolder = null; 354 | 355 | if (importDir != null) { 356 | 357 | if (!importDir.startsWith("/")) { 358 | importDir = "/" + importDir; 359 | } 360 | 361 | // Add ending slash so the dir gets created for server projects 362 | if (!importDir.endsWith("/")) { 363 | importDir += "/"; 364 | } 365 | 366 | // Gets folder -- creates path if it doesn't already exist 367 | saveFolder = headless.getDomainFolder(importDir, true); 368 | } 369 | 370 | headless.setSaveFolder(saveFolder); 371 | } 372 | } 373 | 374 | /** 375 | * Returns whether analysis for the current program has timed out. 376 | *

377 | * Analysis will time out only in the case where: 378 | *

    379 | *
  1. the users has set an analysis timeout period using the -analysisTimeoutPerFile 380 | * parameter
  2. 381 | *
  3. analysis is enabled and has completed
  4. 382 | *
  5. the current script is being run as a postScript (since postScripts run after 383 | * analysis)
  6. 384 | *
385 | * 386 | * @return whether analysis timeout occurred 387 | * @throws ImproperUseException if not in headless mode or headless instance not set 388 | */ 389 | public boolean analysisTimeoutOccurred() throws ImproperUseException { 390 | checkHeadlessStatus(); 391 | return headless.checkAnalysisTimedOut(); 392 | } 393 | 394 | @Override 395 | public void runScript(String scriptName, String[] scriptArguments, GhidraState scriptState) 396 | throws Exception { 397 | 398 | boolean isHeadlessScript = false; 399 | 400 | if (scriptSetOption != null) { 401 | resolveContinuationOptionWith(scriptSetOption); 402 | scriptSetOption = null; 403 | } 404 | ResourceFile scriptSource = GhidraScriptUtil.findScriptByName(scriptName); 405 | if (scriptSource != null) { 406 | GhidraScriptProvider provider = GhidraScriptUtil.getProvider(scriptSource); 407 | 408 | if (provider == null) { 409 | throw new IOException("Attempting to run subscript '" + scriptName + 410 | "': unable to run this script type."); 411 | } 412 | 413 | GhidraScript script = provider.getScriptInstance(scriptSource, writer); 414 | isHeadlessScript = script instanceof LibHeadlessScript ? true : false; 415 | 416 | if (potentialPropertiesFileLocs.size() > 0) { 417 | script.setPotentialPropertiesFileLocations(potentialPropertiesFileLocs); 418 | } 419 | 420 | if (scriptState == state) { 421 | updateStateFromVariables(); 422 | } 423 | 424 | if (isHeadlessScript) { 425 | ((LibHeadlessScript) script).setHeadlessInstance(headless); 426 | ((LibHeadlessScript) script).setRunningInnerScript(true); 427 | } 428 | 429 | script.setScriptArgs(scriptArguments); 430 | 431 | script.execute(scriptState, monitor, writer); 432 | 433 | if (scriptState == state) { 434 | loadVariablesFromState(); 435 | } 436 | 437 | // Resolve continuations options, if they have changed 438 | if (isHeadlessScript) { 439 | LibHeadlessContinuationOption innerScriptOpt = 440 | ((LibHeadlessScript) script).getHeadlessContinuationOption(); 441 | 442 | if (innerScriptOpt != null) { 443 | resolveContinuationOptionWith(innerScriptOpt); 444 | } 445 | 446 | ((LibHeadlessScript) script).setRunningInnerScript(false); 447 | } 448 | 449 | return; 450 | } 451 | 452 | throw new IllegalArgumentException("Script does not exist: " + scriptName); 453 | } 454 | 455 | @Override 456 | public void cleanup(boolean success) { 457 | resolveContinuationOption(); 458 | 459 | if (!runningInnerScript) { 460 | scriptSetOption = null; 461 | } 462 | } 463 | 464 | private void resolveContinuationOption() { 465 | resolveContinuationOptionWith(scriptSetOption); 466 | } 467 | 468 | /** 469 | * Resolve continuation options according to the table in 'analyzeHeadlessREADME.html'. 470 | * (See "Multiple Scripts" section). 471 | * 472 | * @param opt continuation option to combine with current continuation option 473 | */ 474 | private void resolveContinuationOptionWith(LibHeadlessContinuationOption opt) { 475 | 476 | if (opt == null) { 477 | return; 478 | } 479 | 480 | switch (currentOption) { 481 | 482 | case CONTINUE: 483 | currentOption = opt; 484 | break; 485 | 486 | case CONTINUE_THEN_DELETE: 487 | switch (opt) { 488 | case ABORT: 489 | 490 | case ABORT_AND_DELETE: 491 | currentOption = LibHeadlessContinuationOption.ABORT_AND_DELETE; 492 | break; 493 | 494 | default: 495 | break; 496 | } 497 | break; 498 | 499 | case ABORT_AND_DELETE: 500 | // nothing changes 501 | break; 502 | 503 | case ABORT: 504 | // nothing changes 505 | break; 506 | } 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /GhidraLib/LibHeadlessAnalyzer.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 | 17 | /* 18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com 19 | */ 20 | 21 | package com.nosecurecode.libghidra; 22 | 23 | import java.io.*; 24 | import java.net.*; 25 | import java.util.*; 26 | import java.util.regex.Pattern; 27 | 28 | import generic.jar.ResourceFile; 29 | import generic.stl.Pair; 30 | import generic.util.Path; 31 | import ghidra.GhidraApplicationLayout; 32 | import ghidra.GhidraJarApplicationLayout; 33 | import ghidra.app.plugin.core.analysis.AutoAnalysisManager; 34 | import ghidra.app.plugin.core.osgi.BundleHost; 35 | import ghidra.app.script.*; 36 | import com.nosecurecode.libghidra.LibHeadlessScript.LibHeadlessContinuationOption; 37 | import ghidra.app.util.importer.AutoImporter; 38 | import ghidra.app.util.importer.MessageLog; 39 | import ghidra.app.util.opinion.BinaryLoader; 40 | import ghidra.framework.*; 41 | import ghidra.framework.client.ClientUtil; 42 | import ghidra.framework.client.RepositoryAdapter; 43 | import ghidra.framework.data.*; 44 | import ghidra.framework.model.*; 45 | import ghidra.framework.project.DefaultProject; 46 | import ghidra.framework.project.DefaultProjectManager; 47 | import ghidra.framework.protocol.ghidra.*; 48 | import ghidra.framework.remote.User; 49 | import ghidra.framework.store.LockException; 50 | import ghidra.framework.store.local.LocalFileSystem; 51 | import ghidra.program.database.ProgramContentHandler; 52 | import ghidra.program.database.ProgramDB; 53 | import ghidra.program.model.address.AddressSetView; 54 | import ghidra.program.model.listing.Program; 55 | import ghidra.program.util.GhidraProgramUtilities; 56 | import ghidra.program.util.ProgramLocation; 57 | import ghidra.util.*; 58 | import ghidra.util.exception.*; 59 | import ghidra.util.task.TaskMonitor; 60 | import utilities.util.FileUtilities; 61 | 62 | /** 63 | * The class used kick-off and interact with headless processing. All headless options have been 64 | * broken out into their own class: {@link LibHeadlessOptions}. This class is intended to be used 65 | * one of two ways: 66 | *
    67 | *
  • Used by {@link LibGhidra} to perform headless analysis based on arguments specified 68 | * on the command line.
  • 69 | *
  • Used by another tool as a library to perform headless analysis.
  • 70 | *
71 | *

72 | * Note: This class is not thread safe. 73 | */ 74 | public class LibHeadlessAnalyzer { 75 | 76 | private static LibHeadlessAnalyzer instance; 77 | 78 | private LibHeadlessOptions options; 79 | private HeadlessGhidraProjectManager projectManager; 80 | private Project project; 81 | private boolean analysisTimedOut; 82 | private DomainFolder saveDomainFolder; 83 | private Map storage; 84 | private URLClassLoader classLoaderForDotClassScripts; 85 | 86 | private LibProgramHandler programHandler = null; 87 | 88 | /** 89 | * Gets a headless analyzer, initializing the application if necessary with the specified 90 | * logging parameters. An {@link IllegalStateException} will be thrown if the application has 91 | * already been initialized or a headless analyzer has already been retrieved. In these cases, 92 | * the headless analyzer should be gotten with {@link LibHeadlessAnalyzer#getInstance(LibProgramHandler)}. 93 | * 94 | * @param logFile The desired application log file. If null, no application logging will take place. 95 | * @param scriptLogFile The desired scripting log file. If null, no script logging will take place. 96 | * @param useLog4j true if log4j is to be used; otherwise, false. If this class is being used by 97 | * another tool as a library, using log4j might interfere with that tool. 98 | * @return An instance of a new headless analyzer. 99 | * @throws IllegalStateException if an application or headless analyzer instance has already been initialized. 100 | * @throws IOException if there was a problem reading the application.properties file. 101 | */ 102 | public static LibHeadlessAnalyzer getLoggableInstance(File logFile, File scriptLogFile, 103 | boolean useLog4j, LibProgramHandler handler) throws IllegalStateException, IOException { 104 | 105 | // Prevent more than one headless analyzer from being instantiated. Too much about it 106 | // messes with global system settings, so under the current design of Ghidra, allowing 107 | // more than one to exist could result in unpredictable behavior. 108 | if (instance != null) { 109 | throw new IllegalStateException( 110 | "A headless analzyer instance has already been retrieved. " + 111 | "Use HeadlessAnalyzer.getInstance() to get it."); 112 | } 113 | 114 | // Cannot set logging because application has already been initialized. 115 | if (Application.isInitialized()) { 116 | throw new IllegalStateException( 117 | "Logging cannot be set because the application has already been initialized. " + 118 | "Use HeadlessAnalyzer.getInstance() to get the headless analyzer."); 119 | } 120 | 121 | // Initialize application with the provided logging parameters 122 | ApplicationConfiguration configuration = new HeadlessGhidraApplicationConfiguration(); 123 | if (useLog4j) { 124 | if (logFile != null) { 125 | configuration.setApplicationLogFile(logFile); 126 | } 127 | if (scriptLogFile != null) { 128 | configuration.setScriptLogFile(scriptLogFile); 129 | } 130 | } 131 | else { 132 | configuration.setInitializeLogging(false); 133 | Msg.setErrorLogger(new LibHeadlessErrorLogger(logFile)); 134 | } 135 | Application.initializeApplication(getApplicationLayout(), configuration); 136 | 137 | // Instantiate and return singleton headless analyzer 138 | instance = new LibHeadlessAnalyzer(); 139 | 140 | // Set our program handler 141 | instance.programHandler = handler; 142 | 143 | return instance; 144 | } 145 | 146 | /** 147 | * Gets a headless analyzer instance, with the assumption that the application has already been 148 | * initialized. If this is called before the application has been initialized, it will 149 | * initialize the application with no logging. 150 | * 151 | * @return An instance of a new headless analyzer. 152 | * @throws IOException if there was a problem reading the application.properties file (only possible 153 | * if the application had not be initialized). 154 | */ 155 | public static LibHeadlessAnalyzer getInstance(LibProgramHandler handler) throws IOException { 156 | 157 | // Prevent more than one headless analyzer from being instantiated. Too much about it 158 | // messes with global system settings, so under the current design of Ghidra, allowing 159 | // more than one to exist could result in unpredictable behavior. 160 | if (instance != null) { 161 | return instance; 162 | } 163 | 164 | // Initialize application (if necessary) 165 | if (!Application.isInitialized()) { 166 | ApplicationConfiguration configuration = new HeadlessGhidraApplicationConfiguration(); 167 | configuration.setInitializeLogging(false); 168 | Msg.setErrorLogger(new LibHeadlessErrorLogger(null)); 169 | Application.initializeApplication(getApplicationLayout(), configuration); 170 | } 171 | 172 | // Instantiate and return singleton headless analyzer 173 | instance = new LibHeadlessAnalyzer(); 174 | 175 | // Set our program handler 176 | instance.programHandler = handler; 177 | 178 | return instance; 179 | } 180 | 181 | /** 182 | * Gets the appropriate Ghidra application layout for this headless analyzer. 183 | *

184 | * The headless analyzer can be used in both "normal" mode and single jar mode, so 185 | * we need to use the appropriate layout for either case. 186 | * 187 | * @return The appropriate Ghidra application layout for this headless analyzer. 188 | * @throws IOException if there was a problem getting an appropriate application layout. 189 | */ 190 | private static GhidraApplicationLayout getApplicationLayout() throws IOException { 191 | GhidraApplicationLayout layout; 192 | try { 193 | layout = new GhidraApplicationLayout(); 194 | } 195 | catch (IOException e) { 196 | layout = new GhidraJarApplicationLayout(); 197 | 198 | } 199 | return layout; 200 | } 201 | 202 | /** 203 | * Creates a new headless analyzer object with default settings. 204 | */ 205 | private LibHeadlessAnalyzer() { 206 | // Create default options which the caller can later set prior to processing. 207 | options = new LibHeadlessOptions(); 208 | 209 | // Ghidra URL handler registration. There's no harm in doing this more than once. 210 | Handler.registerHandler(); 211 | 212 | // Ensure that we are running in "headless mode", preventing Swing-based methods from 213 | // running (causing headless operation to lose focus). 214 | System.setProperty("java.awt.headless", "true"); 215 | System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.TRUE.toString()); 216 | 217 | // Put analyzer in its default state 218 | reset(); 219 | } 220 | 221 | /** 222 | * Resets the state of the headless analyzer to the default settings. 223 | */ 224 | public void reset() { 225 | options.reset(); 226 | project = null; 227 | analysisTimedOut = false; 228 | saveDomainFolder = null; 229 | storage = new HashMap<>(); 230 | classLoaderForDotClassScripts = null; 231 | } 232 | 233 | /** 234 | * Gets the headless analyzer's options. 235 | * 236 | * @return The headless analyer's options. 237 | */ 238 | public LibHeadlessOptions getOptions() { 239 | return options; 240 | } 241 | 242 | /** 243 | * Process the optional import file/directory list and process each imported file: 244 | *

    245 | *
  1. execute ordered list of pre-scripts
  2. 246 | *
  3. perform auto-analysis if not disabled
  4. 247 | *
  5. execute ordered list of post-scripts
  6. 248 | *
249 | * If no import files or directories have been specified the ordered list 250 | * of pre/post scripts will be executed once. 251 | * 252 | * @param ghidraURL ghidra URL for existing server repository and optional 253 | * folder path 254 | * @param filesToImport directories and files to be imported (null or empty 255 | * is acceptable if we are in -process mode) 256 | * @throws IOException if there was an IO-related problem 257 | * @throws URISyntaxException specified URL is invalid 258 | */ 259 | public void processURL(URL ghidraURL, List filesToImport) 260 | throws IOException, URISyntaxException { 261 | 262 | if (options.readOnly && options.commit) { 263 | Msg.error(this, 264 | "Abort due to Headless analyzer error: The requested readOnly option is in conflict " + 265 | "with the commit option"); 266 | return; 267 | } 268 | 269 | if (!"ghidra".equals(ghidraURL.getProtocol())) { 270 | throw new URISyntaxException(ghidraURL.toString(), "Unsupported repository URL"); 271 | } 272 | 273 | if (GhidraURL.isLocalProjectURL(ghidraURL)) { 274 | Msg.error(this, 275 | "Ghidra URL command form does not supported local project URLs (ghidra:/path...)"); 276 | return; 277 | } 278 | 279 | String path = ghidraURL.getPath(); 280 | if (path == null) { 281 | throw new URISyntaxException(ghidraURL.toString(), "Unsupported repository URL"); 282 | } 283 | 284 | path = path.trim(); 285 | if (path.length() == 0) { 286 | throw new URISyntaxException(ghidraURL.toString(), "Unsupported repository URL"); 287 | } 288 | 289 | if (!options.runScriptsNoImport) { // Running in -import mode 290 | if ((filesToImport == null || filesToImport.size() == 0) && 291 | options.preScripts.isEmpty() && options.postScripts.isEmpty()) { 292 | Msg.warn(this, "REPORT: Nothing to do ... must specify files for import."); 293 | return; 294 | } 295 | 296 | if (!path.endsWith("/")) { 297 | // force explicit folder path so that non-existent folders are created on import 298 | ghidraURL = new URI("ghidra", null, ghidraURL.getHost(), ghidraURL.getPort(), path + "/", null, null).toURL(); 299 | } 300 | } 301 | else { // Running in -process mode 302 | if (path.endsWith("/") && path.length() > 1) { 303 | ghidraURL = new URI("ghidra", null, ghidraURL.getHost(), ghidraURL.getPort(), 304 | path.substring(0, path.length() - 1), null, null).toURL(); 305 | } 306 | } 307 | 308 | List parsedScriptPaths = parseScriptPaths(options.scriptPaths); 309 | GhidraScriptUtil.initialize(new BundleHost(), parsedScriptPaths); 310 | try { 311 | showConfiguredScriptPaths(); 312 | compileScripts(); 313 | 314 | Msg.info(LibHeadlessAnalyzer.class, "HEADLESS: execution starts"); 315 | 316 | GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection(); 317 | c.setReadOnly(options.readOnly); // writable repository connection 318 | 319 | if (c.getRepositoryName() == null) { 320 | throw new MalformedURLException("Unsupported repository URL: " + ghidraURL); 321 | } 322 | 323 | Msg.info(this, "Opening ghidra repository project: " + ghidraURL); 324 | Object obj = c.getContent(); 325 | if (!(obj instanceof GhidraURLWrappedContent)) { 326 | throw new IOException( 327 | "Connect to repository folder failed. Status code: " + c.getStatusCode()); 328 | } 329 | GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj; 330 | Object content = null; 331 | try { 332 | content = wrappedContent.getContent(this); 333 | if (!(content instanceof DomainFolder)) { 334 | throw new IOException("Connect to repository folder failed"); 335 | } 336 | 337 | DomainFolder folder = (DomainFolder) content; 338 | project = new HeadlessProject(getProjectManager(), c); 339 | 340 | if (!checkUpdateOptions()) { 341 | return; // TODO: Should an exception be thrown? 342 | } 343 | 344 | if (options.runScriptsNoImport) { 345 | processNoImport(folder.getPathname()); 346 | } 347 | else { 348 | processWithImport(folder.getPathname(), filesToImport); 349 | } 350 | } 351 | finally { 352 | if (content != null) { 353 | wrappedContent.release(content, this); 354 | } 355 | if (project != null) { 356 | project.close(); 357 | } 358 | } 359 | } 360 | finally { 361 | GhidraScriptUtil.dispose(); 362 | } 363 | } 364 | 365 | /** 366 | * Process the optional import file/directory list and process each imported file: 367 | *
    368 | *
  1. execute ordered list of pre-scripts
  2. 369 | *
  3. perform auto-analysis if not disabled
  4. 370 | *
  5. execute ordered list of post-scripts
  6. 371 | *
372 | * If no import files or directories have been specified the ordered list 373 | * of pre/post scripts will be executed once. 374 | * 375 | * @param projectLocation directory path of project 376 | * If project exists it will be opened, otherwise it will be created. 377 | * @param projectName project name 378 | * @param rootFolderPath root folder for imports 379 | * @param filesToImport directories and files to be imported (null or empty is acceptable if 380 | * we are in -process mode) 381 | * @throws IOException if there was an IO-related problem 382 | */ 383 | public void processLocal(String projectLocation, String projectName, String rootFolderPath, 384 | List filesToImport) throws IOException { 385 | 386 | if (options.readOnly && options.commit) { 387 | Msg.error(this, 388 | "Abort due to Headless analyzer error: The requested readOnly option is " + 389 | "in conflict with the commit option"); 390 | return; 391 | } 392 | 393 | // If not importing, remove trailing slash so that non-existent folders aren't created 394 | if (options.runScriptsNoImport) { 395 | if ((rootFolderPath.endsWith("/")) && (rootFolderPath.length() > 1)) { 396 | rootFolderPath = rootFolderPath.substring(0, rootFolderPath.length() - 1); 397 | } 398 | } 399 | else { 400 | // If we are importing, need some files to import or at least a script to run! 401 | if ((filesToImport == null || filesToImport.size() == 0) && 402 | options.preScripts.isEmpty() && options.postScripts.isEmpty()) { 403 | Msg.warn(this, "REPORT: Nothing to do ... must specify file(s) for import."); 404 | return; 405 | } 406 | 407 | // If importing, add trailing slash if it isn't there so that non-existent folders are created 408 | if (!rootFolderPath.endsWith("/")) { 409 | rootFolderPath += "/"; 410 | } 411 | } 412 | 413 | List parsedScriptPaths = parseScriptPaths(options.scriptPaths); 414 | GhidraScriptUtil.initialize(new BundleHost(), parsedScriptPaths); 415 | try { 416 | showConfiguredScriptPaths(); 417 | compileScripts(); 418 | 419 | Msg.info(LibHeadlessAnalyzer.class, "HEADLESS: execution starts"); 420 | 421 | File dir = new File(projectLocation); 422 | ProjectLocator locator = new ProjectLocator(dir.getAbsolutePath(), projectName); 423 | 424 | if (locator.getProjectDir().exists()) { 425 | project = openProject(locator); 426 | } 427 | else { 428 | if (options.runScriptsNoImport) { 429 | Msg.error(this, "Could not find project: " + locator + 430 | " -- should already exist in -process mode."); 431 | throw new IOException("Could not find project: " + locator); 432 | } 433 | 434 | if (!options.runScriptsNoImport && options.readOnly) { 435 | // assume temporary when importing with readOnly option 436 | options.deleteProject = true; 437 | } 438 | 439 | Msg.info(this, "Creating " + (options.deleteProject ? "temporary " : "") + 440 | "project: " + locator); 441 | project = getProjectManager().createProject(locator, null, false); 442 | } 443 | 444 | try { 445 | 446 | if (!checkUpdateOptions()) { 447 | return; // TODO: Should an exception be thrown? 448 | } 449 | 450 | if (options.runScriptsNoImport) { 451 | processNoImport(rootFolderPath); 452 | } 453 | else { 454 | processWithImport(rootFolderPath, filesToImport); 455 | } 456 | } 457 | finally { 458 | project.close(); 459 | if (!options.runScriptsNoImport && options.deleteProject) { 460 | FileUtilities.deleteDir(locator.getProjectDir()); 461 | locator.getMarkerFile().delete(); 462 | } 463 | } 464 | } 465 | finally { 466 | GhidraScriptUtil.dispose(); 467 | } 468 | } 469 | 470 | /** 471 | * Checks to see if the most recent analysis timed out. 472 | * 473 | * @return true if the most recent analysis timed out; otherwise, false. 474 | */ 475 | public boolean checkAnalysisTimedOut() { 476 | return analysisTimedOut; 477 | } 478 | 479 | void setSaveFolder(DomainFolder domFolder) { 480 | saveDomainFolder = domFolder; 481 | 482 | if (domFolder != null) { 483 | Msg.info(this, "Save location changed to: " + domFolder.getPathname()); 484 | } 485 | } 486 | 487 | void addVariableToStorage(String nameOfVar, Object valOfVar) { 488 | if (storage.containsKey(nameOfVar)) { 489 | Msg.warn(this, "Overwriting existing storage variable: " + nameOfVar); 490 | } 491 | 492 | storage.put(nameOfVar, valOfVar); 493 | } 494 | 495 | Set getStorageKeys() { 496 | return storage.keySet(); 497 | } 498 | 499 | Object getVariableFromStorage(String nameOfVar) { 500 | if (!storage.containsKey(nameOfVar)) { 501 | Msg.warn(this, "The storage variable '" + nameOfVar + 502 | "' does not exist in HeadlessAnalyzer storage."); 503 | return null; 504 | } 505 | 506 | return storage.get(nameOfVar); 507 | } 508 | 509 | /** 510 | * Get/Create specified folder path within project 511 | * 512 | * @param folderPath the folder path within the project 513 | * @param create if true, folder will be created if it does not exist 514 | * @return DomainFolder for specified path 515 | * @throws InvalidNameException if folder name is invalid 516 | * @throws IOException if folder can not be created 517 | */ 518 | DomainFolder getDomainFolder(String folderPath, boolean create) 519 | throws IOException, InvalidNameException { 520 | 521 | DomainFolder domFolder = project.getProjectData().getFolder(folderPath); 522 | 523 | if (create && domFolder == null) { 524 | // Create any folder that doesn't exist 525 | String cleanPath = folderPath.replaceAll("^" + DomainFolder.SEPARATOR + "+", ""); 526 | cleanPath = cleanPath.replaceAll(DomainFolder.SEPARATOR + "+$", ""); 527 | 528 | String[] subfolders = cleanPath.split(DomainFolder.SEPARATOR + "+"); 529 | 530 | int folderIndex = 0; 531 | String currPath = DomainFolder.SEPARATOR + subfolders[folderIndex]; 532 | 533 | DomainFolder testFolder = project.getProjectData().getFolder(currPath); 534 | DomainFolder baseFolder = null; 535 | 536 | // Stay in loop while we see folders that exist 537 | while ((testFolder != null) && (folderIndex < (subfolders.length - 1))) { 538 | folderIndex++; 539 | baseFolder = testFolder; 540 | testFolder = baseFolder.getFolder(subfolders[folderIndex]); 541 | } 542 | 543 | // If none of the folders exist, create new files starting from the root 544 | if (folderIndex == 0) { 545 | baseFolder = project.getProjectData().getRootFolder(); 546 | } 547 | 548 | // Since this method is only called by import, we create any folder that 549 | // does not exist. 550 | for (int i = folderIndex; i < subfolders.length; i++) { 551 | baseFolder = baseFolder.createFolder(subfolders[i]); 552 | Msg.info(this, "Created project folder: " + subfolders[i]); 553 | } 554 | 555 | domFolder = baseFolder; 556 | } 557 | 558 | return domFolder; 559 | } 560 | 561 | boolean storageContainsKey(String nameOfVar) { 562 | return storage.containsKey(nameOfVar); 563 | } 564 | 565 | /** 566 | * Runs the specified script with the specified state. 567 | * 568 | * @param scriptState State representing environment variables that the script is able 569 | * to access. 570 | * @param script Script to be run. 571 | * @return whether the script successfully completed running 572 | */ 573 | private boolean runScript(GhidraState scriptState, GhidraScript script) { 574 | if (script instanceof LibHeadlessScript) { 575 | ((LibHeadlessScript) script).setHeadlessInstance(this); 576 | } 577 | 578 | ResourceFile srcFile = script.getSourceFile(); 579 | String scriptName = 580 | srcFile != null ? srcFile.getAbsolutePath() : (script.getClass().getName() + ".class"); 581 | 582 | try { 583 | PrintWriter writer = new PrintWriter(System.out); 584 | Msg.info(this, "SCRIPT: " + scriptName); 585 | script.execute(scriptState, TaskMonitor.DUMMY, writer); 586 | writer.flush(); 587 | } 588 | catch (Exception exc) { 589 | Program prog = scriptState.getCurrentProgram(); 590 | String path = (prog != null ? " ( " + prog.getExecutablePath() + " ) " : ""); 591 | String logErrorMsg = 592 | "REPORT SCRIPT ERROR: " + path + " " + scriptName + " : " + exc.getMessage(); 593 | Msg.error(this, logErrorMsg, exc); 594 | return false; 595 | } 596 | 597 | return true; 598 | } 599 | 600 | /** 601 | * Check file update options (i.e., readOnly, commit) and change defaults if needed. 602 | * @return true if OK to continue 603 | */ 604 | private boolean checkUpdateOptions() { 605 | 606 | boolean isImport = !options.runScriptsNoImport; 607 | boolean commitAllowed = isCommitAllowed(); 608 | 609 | if (options.readOnly) { 610 | String readOnlyError = 611 | "Abort due to Headless analyzer error: The requested -readOnly option " + 612 | "is in conflict with the "; 613 | 614 | if (options.commit) { 615 | Msg.error(this, readOnlyError + "-commit option."); 616 | return false; 617 | } 618 | 619 | if (options.okToDelete) { 620 | Msg.error(this, readOnlyError + "-okToDelete option."); 621 | return false; 622 | } 623 | } 624 | 625 | if (options.commit && !commitAllowed) { 626 | Msg.error(this, 627 | "Commit to repository not possible (due to permission or connection issue)"); 628 | return false; 629 | } 630 | 631 | if (project.getProjectLocator().isTransient()) { 632 | if (!options.commit) { 633 | if (commitAllowed && !options.readOnly) { 634 | Msg.info(this, 635 | "When processing a URL, -commit is automatically enabled unless -readOnly mode " + 636 | "is specified. Enabling -commit and continuing."); 637 | options.commit = true; 638 | } 639 | } 640 | } 641 | 642 | if (options.overwrite) { 643 | if (!isImport) { 644 | Msg.info(this, 645 | "Ignoring -overwrite because it is not applicable to -process mode."); 646 | } 647 | else if (options.readOnly) { 648 | Msg.info(this, 649 | "Ignoring -overwrite because it is not applicable to -readOnly import mode."); 650 | options.overwrite = false; 651 | } 652 | } 653 | 654 | return true; 655 | } 656 | 657 | private boolean isCommitAllowed() { 658 | RepositoryAdapter repository = project.getRepository(); 659 | if (repository == null) { 660 | return true; 661 | } 662 | try { 663 | repository.connect(); 664 | if (!repository.isConnected()) { 665 | return false; 666 | } 667 | User user = repository.getUser(); 668 | if (!user.hasWritePermission()) { 669 | Msg.warn(this, "User '" + user.getName() + 670 | "' does not have write permission to repository - commit not allowed"); 671 | return false; 672 | } 673 | return true; 674 | } 675 | catch (IOException e) { 676 | Msg.error(this, "Repository connection failed (" + repository.getServerInfo() + 677 | ") - commit not allowed"); 678 | return false; 679 | } 680 | } 681 | 682 | private List parseScriptPaths(List scriptPaths) { 683 | if (scriptPaths == null) { 684 | return null; 685 | } 686 | List parsedScriptPaths = new ArrayList<>(); 687 | for (String path : scriptPaths) { 688 | ResourceFile pathFile = Path.fromPathString(path); 689 | String absPath = pathFile.getAbsolutePath(); 690 | if (pathFile.exists()) { 691 | parsedScriptPaths.add(absPath); 692 | } 693 | else { 694 | 695 | Msg.warn(this, "REPORT: Could not find -scriptPath entry, skipping: " + absPath); 696 | } 697 | } 698 | return parsedScriptPaths; 699 | } 700 | 701 | private void showConfiguredScriptPaths() { 702 | StringBuffer buf = new StringBuffer("HEADLESS Script Paths:"); 703 | for (ResourceFile dir : GhidraScriptUtil.getScriptSourceDirectories()) { 704 | buf.append("\n "); 705 | buf.append(dir.getAbsolutePath()); 706 | } 707 | Msg.info(LibHeadlessAnalyzer.class, buf.toString()); 708 | } 709 | 710 | private ResourceFile findScript(String scriptName) { 711 | ResourceFile scriptSource = new ResourceFile(scriptName); 712 | scriptSource = scriptSource.getCanonicalFile(); 713 | if (scriptSource.exists()) { 714 | return scriptSource; 715 | } 716 | scriptSource = GhidraScriptUtil.findScriptByName(scriptName); 717 | if (scriptSource != null) { 718 | return scriptSource; 719 | } 720 | throw new IllegalArgumentException("Script not found: " + scriptName); 721 | } 722 | 723 | /** 724 | * Checks the script name to ensure it exists. If the script type has a GhidraScriptProvider 725 | * (any type of script but .class), then return the ResourceFile that represents that script. 726 | * 727 | * If the script is a class file, return null (one class loader is stored to allow the 728 | * Headless Analyzer to find all the class files). 729 | * 730 | * GhidraScript is not instantiated here, because it is important that each script be 731 | * instantiated at the time it's used. If a GhidraScript object is re-used, this causes 732 | * problems where GhidraScript variables aren't being re-initialized at each use of the script. 733 | * 734 | * @param scriptName The name of the script to check 735 | * @return ResourceFile representing the source file, or null (if script is a .class file) 736 | */ 737 | private ResourceFile checkScript(String scriptName) { 738 | 739 | // Check for pre-compiled GhidraScript (e.g., my.package.Impl.class) 740 | String classExtension = ".class"; 741 | 742 | if (scriptName.endsWith(classExtension)) { 743 | String className = 744 | scriptName.substring(0, scriptName.length() - classExtension.length()); 745 | try { 746 | 747 | // Create a classloader that contains all the ghidra_script paths (especially the one 748 | // specified in -scriptPath!) 749 | List dirs = GhidraScriptUtil.getScriptSourceDirectories(); 750 | List urls = new ArrayList<>(); 751 | 752 | for (ResourceFile dir : dirs) { 753 | try { 754 | urls.add(dir.toURL()); 755 | } 756 | catch (MalformedURLException e) { 757 | // Do nothing. If can't make a URL out of the dir, don't add it. 758 | } 759 | } 760 | 761 | classLoaderForDotClassScripts = 762 | URLClassLoader.newInstance(urls.toArray(new URL[0])); 763 | 764 | Class c = Class.forName(className, true, classLoaderForDotClassScripts); 765 | 766 | if (GhidraScript.class.isAssignableFrom(c)) { 767 | // No issues, but return null, which signifies we don't actually have a 768 | // ResourceFile to associate with the script name 769 | return null; 770 | } 771 | 772 | Msg.error(this, 773 | "REPORT SCRIPT ERROR: java class '" + className + "' is not a GhidraScript"); 774 | } 775 | catch (ClassNotFoundException e) { 776 | Msg.error(this, 777 | "REPORT SCRIPT ERROR: java class not found for '" + className + "'"); 778 | } 779 | throw new IllegalArgumentException("Invalid script: " + scriptName); 780 | } 781 | 782 | try { 783 | ResourceFile scriptSource = findScript(scriptName); 784 | GhidraScriptProvider provider = GhidraScriptUtil.getProvider(scriptSource); 785 | 786 | if (provider == null) { 787 | throw new IOException("Missing plugin needed to run scripts of this type. Please " + 788 | "ensure you have installed the necessary plugin."); 789 | } 790 | 791 | return scriptSource; 792 | } 793 | catch (Exception | NoClassDefFoundError exc) { 794 | String logErrorMsg = "REPORT SCRIPT ERROR: " + scriptName + " : " + exc.getMessage(); 795 | Msg.error(this, logErrorMsg); 796 | } 797 | throw new IllegalArgumentException("Invalid script: " + scriptName); 798 | } 799 | 800 | /** 801 | * Creates mapping from script name to actual Script object 802 | * 803 | * @param scriptsList List of scripts 804 | * @return mapping of script name to its associated Script object 805 | */ 806 | private Map checkScriptsList(List> scriptsList) { 807 | Map map = new HashMap<>(); 808 | for (Pair scriptPair : scriptsList) { 809 | String scriptName = scriptPair.first; 810 | ResourceFile scriptFile = checkScript(scriptName); 811 | map.put(scriptName, scriptFile); 812 | } 813 | return map; 814 | } 815 | 816 | private void compileScripts() throws IOException { 817 | 818 | // Check that given locations for .properties files are valid 819 | if (options.propertiesFileStrPaths.size() > 0) { 820 | 821 | options.propertiesFilePaths.clear(); 822 | 823 | for (String path : options.propertiesFileStrPaths) { 824 | Path currPath = new Path(path, true, false, true); 825 | 826 | ResourceFile resource = currPath.getPath(); 827 | 828 | if (!resource.isDirectory()) { 829 | throw new IOException("Properties file path: '" + path + 830 | "' either does not exist, " + "or is not a valid directory."); 831 | } 832 | 833 | if (currPath.isEnabled() && !options.propertiesFilePaths.contains(resource)) { 834 | options.propertiesFilePaths.add(resource); 835 | } 836 | } 837 | } 838 | 839 | if (options.preScriptFileMap == null) { 840 | options.preScriptFileMap = checkScriptsList(options.preScripts); 841 | } 842 | 843 | if (options.postScriptFileMap == null) { 844 | options.postScriptFileMap = checkScriptsList(options.postScripts); 845 | } 846 | } 847 | 848 | /** 849 | * Run a list of scripts 850 | * 851 | * @param scriptsList list of script names to run 852 | * @param scriptFileMap mapping of script names to Script objects 853 | * @param scriptState the GhidraState to be passed into each script 854 | * @param continueOption option that could have been set by script(s) 855 | * @return option that could have been set by script(s) 856 | */ 857 | private LibHeadlessContinuationOption runScriptsList(List> scriptsList, 858 | Map scriptFileMap, GhidraState scriptState, 859 | LibHeadlessContinuationOption continueOption) { 860 | 861 | ResourceFile currScriptFile; 862 | LibHeadlessContinuationOption retOption = continueOption; 863 | 864 | boolean scriptSuccess; 865 | boolean isHeadlessScript = false; 866 | String scriptName = ""; 867 | GhidraScript currScript; 868 | 869 | try { 870 | for (Pair scriptPair : scriptsList) { 871 | scriptName = scriptPair.first; 872 | String[] scriptArgs = scriptPair.second; 873 | 874 | // For .class files, there is no ResourceFile mapping. Need to load from the 875 | // stored 'classLoaderForDotClassScripts' 876 | if (scriptName.endsWith(".class")) { 877 | 878 | if (classLoaderForDotClassScripts == null) { 879 | throw new IllegalArgumentException("Invalid script: " + scriptName); 880 | } 881 | 882 | String className = scriptName.substring(0, scriptName.length() - 6); 883 | Class c = Class.forName(className, true, classLoaderForDotClassScripts); 884 | 885 | // Get parent folder to pass to GhidraScript 886 | File parentFile = new File(c.getResource(c.getSimpleName() + ".class").toURI()) 887 | .getParentFile(); 888 | 889 | currScript = (GhidraScript) c.getConstructor().newInstance(); 890 | currScript.setScriptArgs(scriptArgs); 891 | 892 | if (options.propertiesFilePaths.size() > 0) { 893 | currScript.setPotentialPropertiesFileLocations(options.propertiesFilePaths); 894 | } 895 | 896 | currScript.setPropertiesFileLocation(parentFile.getAbsolutePath(), className); 897 | } 898 | else { 899 | currScriptFile = scriptFileMap.get(scriptName); 900 | 901 | // GhidraScriptProvider case 902 | GhidraScriptProvider provider = GhidraScriptUtil.getProvider(currScriptFile); 903 | PrintWriter writer = new PrintWriter(System.out); 904 | currScript = provider.getScriptInstance(currScriptFile, writer); 905 | currScript.setScriptArgs(scriptArgs); 906 | 907 | if (options.propertiesFilePaths.size() > 0) { 908 | currScript.setPotentialPropertiesFileLocations(options.propertiesFilePaths); 909 | } 910 | } 911 | 912 | isHeadlessScript = currScript instanceof LibHeadlessScript ? true : false; 913 | 914 | if (isHeadlessScript) { 915 | ((LibHeadlessScript) currScript).setInitialContinuationOption(retOption); 916 | } 917 | 918 | scriptSuccess = runScript(scriptState, currScript); 919 | 920 | if (isHeadlessScript) { 921 | if (scriptSuccess) { 922 | retOption = ((LibHeadlessScript) currScript).getContinuationOption(); 923 | 924 | // If script wants to abort, return without running any scripts that follow 925 | if ((retOption == LibHeadlessContinuationOption.ABORT) || 926 | (retOption == LibHeadlessContinuationOption.ABORT_AND_DELETE)) { 927 | return retOption; 928 | } 929 | 930 | } 931 | else { 932 | // If script did not run successfully, abort further processing automatically 933 | Msg.warn(this, 934 | "Script does not exist or encountered problems; further processing is aborted."); 935 | 936 | return LibHeadlessContinuationOption.ABORT; 937 | } 938 | } 939 | } 940 | } 941 | catch (Exception exc) { 942 | String logErrorMsg = "REPORT SCRIPT ERROR: " + scriptName + " : " + exc.getMessage(); 943 | Msg.error(this, logErrorMsg, exc); 944 | } 945 | 946 | return retOption; 947 | } 948 | 949 | private GhidraState getInitialProgramState(Program program) { 950 | ProgramLocation location = null; 951 | AddressSetView initializedMem = program.getMemory().getLoadedAndInitializedAddressSet(); 952 | if (!initializedMem.isEmpty()) { 953 | location = new ProgramLocation(program, initializedMem.getMinAddress()); 954 | } 955 | return new GhidraState(null, project, program, location, null, null); 956 | } 957 | 958 | /** 959 | *{@literal Run prescripts -> analysis -> postscripts (any of these steps is optional).} 960 | * @param fileAbsolutePath Path of the file to analyze. 961 | * @param program The program to analyze. 962 | * @return true if the program file should be kept. If analysis or scripts have marked 963 | * the program as temporary changes should not be saved. Returns false in 964 | * these cases: 965 | * - One of the scripts sets the Headless Continuation Option to "ABORT_AND_DELETE" or 966 | * "CONTINUE_THEN_DELETE". 967 | */ 968 | private boolean analyzeProgram(String fileAbsolutePath, Program program) { 969 | 970 | analysisTimedOut = false; 971 | 972 | AutoAnalysisManager mgr = AutoAnalysisManager.getAnalysisManager(program); 973 | mgr.initializeOptions(); 974 | 975 | GhidraState scriptState = null; 976 | LibHeadlessContinuationOption scriptStatus = LibHeadlessContinuationOption.CONTINUE; 977 | 978 | boolean abortProcessing = false; 979 | boolean deleteProgram = false; 980 | 981 | if (!options.preScripts.isEmpty()) { 982 | // create one state, in case each script might want to modify it to pass information 983 | scriptState = getInitialProgramState(program); 984 | 985 | scriptStatus = runScriptsList(options.preScripts, options.preScriptFileMap, scriptState, 986 | scriptStatus); 987 | } 988 | 989 | switch (scriptStatus) { 990 | case ABORT_AND_DELETE: 991 | abortProcessing = true; 992 | deleteProgram = true; 993 | break; 994 | 995 | case CONTINUE_THEN_DELETE: 996 | abortProcessing = false; 997 | deleteProgram = true; 998 | break; 999 | 1000 | case ABORT: 1001 | abortProcessing = true; 1002 | deleteProgram = false; 1003 | break; 1004 | 1005 | default: 1006 | // do nothing 1007 | } 1008 | 1009 | if (abortProcessing) { 1010 | Msg.info(this, "Processing aborted as a result of pre-script."); 1011 | return !deleteProgram; 1012 | } 1013 | 1014 | int txId = program.startTransaction("Analysis"); 1015 | try { 1016 | if (options.analyze) { 1017 | Msg.info(this, "ANALYZING all memory and code: " + fileAbsolutePath); 1018 | mgr.initializeOptions(); 1019 | 1020 | // Note: Want to analyze regardless of whether we have already analyzed or not 1021 | // (user could have changed options). 1022 | mgr.reAnalyzeAll(null); 1023 | 1024 | if (options.perFileTimeout == -1) { 1025 | mgr.startAnalysis(TaskMonitor.DUMMY); // kick start 1026 | 1027 | Msg.info(this, "REPORT: Analysis succeeded for file: " + fileAbsolutePath); 1028 | 1029 | GhidraProgramUtilities.markProgramAnalyzed(program); 1030 | } 1031 | else { 1032 | LibHeadlessTimedTaskMonitor timerMonitor = 1033 | new LibHeadlessTimedTaskMonitor(options.perFileTimeout); 1034 | mgr.startAnalysis(timerMonitor); 1035 | 1036 | if (timerMonitor.isCancelled()) { 1037 | Msg.error(this, "REPORT: Analysis timed out at " + options.perFileTimeout + 1038 | " seconds. Processing not completed for file: " + fileAbsolutePath); 1039 | 1040 | // If no further scripts, just return the current program disposition 1041 | if (options.postScripts.isEmpty()) { 1042 | return !deleteProgram; 1043 | } 1044 | 1045 | analysisTimedOut = true; 1046 | } 1047 | else { 1048 | // If timeout didn't already happen at this point, cancel the monitor 1049 | timerMonitor.cancel(); 1050 | 1051 | Msg.info(this, "REPORT: Analysis succeeded for file: " + fileAbsolutePath); 1052 | GhidraProgramUtilities.markProgramAnalyzed(program); 1053 | } 1054 | } 1055 | } 1056 | } 1057 | finally { 1058 | program.endTransaction(txId, true); 1059 | } 1060 | 1061 | if (!options.postScripts.isEmpty()) { 1062 | 1063 | if (scriptState == null) { 1064 | scriptState = getInitialProgramState(program); 1065 | } 1066 | 1067 | scriptStatus = runScriptsList(options.postScripts, options.postScriptFileMap, 1068 | scriptState, scriptStatus); 1069 | 1070 | switch (scriptStatus) { 1071 | case ABORT_AND_DELETE: 1072 | abortProcessing = true; 1073 | deleteProgram = true; 1074 | break; 1075 | 1076 | case CONTINUE_THEN_DELETE: 1077 | abortProcessing = false; 1078 | deleteProgram = true; 1079 | break; 1080 | 1081 | case ABORT: 1082 | abortProcessing = true; 1083 | // If deleteProgram is already true, don't change it to false 1084 | // (basically, leave as-is) 1085 | break; 1086 | 1087 | default: 1088 | // Do nothing, assume want to carry over options from before 1089 | 1090 | } 1091 | 1092 | if (abortProcessing) { 1093 | Msg.info(this, "Processing aborted as a result of post-script."); 1094 | } 1095 | else if (options.analyze && !options.postScripts.isEmpty()) { 1096 | Msg.info(this, "ANALYZING changes made by post scripts: " + fileAbsolutePath); 1097 | txId = program.startTransaction("Post-Analysis"); 1098 | try { 1099 | mgr.startAnalysis(TaskMonitor.DUMMY); // kick start 1100 | } 1101 | finally { 1102 | program.endTransaction(txId, true); 1103 | } 1104 | Msg.info(this, "REPORT: Post-analysis succeeded for file: " + fileAbsolutePath); 1105 | } 1106 | 1107 | } 1108 | 1109 | // Our hook after the analysis 1110 | if (programHandler != null) { 1111 | programHandler.PostProcessHandler(program); 1112 | } 1113 | 1114 | return !deleteProgram; 1115 | } 1116 | 1117 | private void processFileNoImport(DomainFile domFile) throws IOException { 1118 | 1119 | if (domFile.isHijacked()) { 1120 | Msg.error(this, 1121 | "Skipped processing for " + domFile.getPathname() + " -- file is hijacked"); 1122 | return; 1123 | } 1124 | 1125 | if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { 1126 | return; // skip non-Program files 1127 | } 1128 | 1129 | Program program = null; 1130 | boolean keepFile = true; // if false file should be deleted after release 1131 | boolean terminateCheckoutWhenDone = false; 1132 | 1133 | boolean readOnlyFile = options.readOnly || domFile.isReadOnly(); 1134 | 1135 | try { 1136 | // Exclusive checkout required when commit option specified 1137 | if (!readOnlyFile) { 1138 | if (domFile.isVersioned()) { 1139 | if (!domFile.isCheckedOut()) { 1140 | if (!domFile.checkout(options.commit, TaskMonitor.DUMMY)) { 1141 | Msg.warn(this, "Skipped processing for " + domFile.getPathname() + 1142 | " -- failed to get exclusive file checkout required for commit"); 1143 | return; 1144 | } 1145 | } 1146 | else if (options.commit && !domFile.isCheckedOutExclusive()) { 1147 | Msg.error(this, "Skipped processing for " + domFile.getPathname() + 1148 | " -- file is checked-out non-exclusive (commit requires exclusive checkout)"); 1149 | return; 1150 | } 1151 | } 1152 | terminateCheckoutWhenDone = true; 1153 | } 1154 | 1155 | program = (Program) domFile.getDomainObject(this, true, false, TaskMonitor.DUMMY); 1156 | 1157 | Msg.info(this, "REPORT: Processing project file: " + domFile.getPathname()); 1158 | 1159 | // This method already takes into account whether the user has set the "noanalysis" 1160 | // flag or not 1161 | keepFile = analyzeProgram(domFile.getPathname(), program) || readOnlyFile; 1162 | 1163 | if (!keepFile) { 1164 | program.setTemporary(true); // don't save changes 1165 | if (!options.okToDelete) { 1166 | // Don't remove file unless okToDelete was specified 1167 | Msg.warn(this, "Due to script activity, " + domFile.getPathname() + 1168 | " deletion was requested but denied -- 'okToDelete' parameter was not specified"); 1169 | keepFile = true; 1170 | } 1171 | } 1172 | 1173 | if (readOnlyFile) { 1174 | if (program.isChanged()) { 1175 | Msg.info(this, "REPORT: Discarding changes to the following read-only file: " + 1176 | domFile.getPathname()); 1177 | } 1178 | return; 1179 | } 1180 | 1181 | if (program.isTemporary()) { 1182 | if (program.isChanged()) { 1183 | Msg.info(this, 1184 | "REPORT: Discarding changes to the following file as a result of script activity: " + 1185 | domFile.getPathname()); 1186 | } 1187 | return; 1188 | } 1189 | 1190 | if (domFile.canSave()) { 1191 | domFile.save(TaskMonitor.DUMMY); 1192 | Msg.info(this, 1193 | "REPORT: Save succeeded for processed file: " + domFile.getPathname()); 1194 | } 1195 | if (program.isChanged()) { 1196 | Msg.error(this, 1197 | "REPORT: Error trying to save changes to file: " + domFile.getPathname()); 1198 | } 1199 | 1200 | if (options.commit) { 1201 | 1202 | AutoAnalysisManager.getAnalysisManager(program).dispose(); 1203 | program.release(this); 1204 | program = null; 1205 | 1206 | // Only commit if it's a shared project. 1207 | commitProgram(domFile); 1208 | } 1209 | } 1210 | catch (VersionException e) { 1211 | 1212 | if (e.isUpgradable()) { 1213 | Msg.error(this, 1214 | domFile.getPathname() + 1215 | ": this file was created with an older version of Ghidra. Automatic " + 1216 | "upgrading of the file to the current version is possible, but " + 1217 | "requires an exclusive check-out of the file. Please check out the file " + 1218 | " using the Ghidra GUI and then re-run Headless."); 1219 | } 1220 | else { 1221 | Msg.error(this, domFile.getPathname() + 1222 | ": this file was created with a newer version of Ghidra, and can not be processed."); 1223 | } 1224 | } 1225 | catch (CancelledException e) { 1226 | // This can never happen because there is no user interaction in headless! 1227 | } 1228 | catch (Exception exc) { 1229 | Msg.error(this, domFile.getPathname() + " Error during analysis: " + exc.getMessage(), 1230 | exc); 1231 | } 1232 | finally { 1233 | 1234 | if (program != null) { 1235 | AutoAnalysisManager.getAnalysisManager(program).dispose(); 1236 | program.release(this); 1237 | program = null; 1238 | } 1239 | 1240 | if (!readOnlyFile) { // can't change anything if read-only file 1241 | 1242 | // Undo checkout of it is still checked-out and either the file is to be 1243 | // deleted, or we just checked it out and file changes have been committed 1244 | if (domFile.isCheckedOut()) { 1245 | if (!keepFile || 1246 | (terminateCheckoutWhenDone && !domFile.modifiedSinceCheckout())) { 1247 | domFile.undoCheckout(false); 1248 | } 1249 | } 1250 | 1251 | if (!keepFile) { 1252 | deleteDomainFile(domFile); 1253 | } 1254 | } 1255 | } 1256 | } 1257 | 1258 | private void deleteDomainFile(DomainFile domFile) { 1259 | if (domFile.isCheckedOut()) { 1260 | Msg.error(this, "Failed to delete file as requested due to pre-existing checkout: " + 1261 | domFile.getPathname()); 1262 | return; 1263 | } 1264 | 1265 | try { 1266 | domFile.delete(); 1267 | } 1268 | catch (IOException e) { 1269 | Msg.error(this, "Failed to delete file as requested - " + e.getMessage() + ": " + 1270 | domFile.getPathname()); 1271 | } 1272 | } 1273 | 1274 | /** 1275 | * Process all files within parentFolder which satisfies the specified filenamePattern. 1276 | * If filenamePattern is null, all files will be processed 1277 | * @param parentFolder domain folder to be searched 1278 | * @param filenamePattern filename pattern or null for all files 1279 | * @return true if one or more files processed 1280 | * @throws IOException if an IO problem occurred. 1281 | */ 1282 | private boolean processFolderNoImport(DomainFolder parentFolder, Pattern filenamePattern) 1283 | throws IOException { 1284 | 1285 | if (parentFolder.isEmpty()) { 1286 | return false; 1287 | } 1288 | 1289 | boolean filesProcessed = false; 1290 | 1291 | for (DomainFile domFile : parentFolder.getFiles()) { 1292 | if (filenamePattern == null || filenamePattern.matcher(domFile.getName()).matches()) { 1293 | if (ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { 1294 | filesProcessed = true; 1295 | processFileNoImport(domFile); 1296 | } 1297 | } 1298 | } 1299 | 1300 | if (options.recursive) { 1301 | for (DomainFolder folder : parentFolder.getFolders()) { 1302 | filesProcessed |= processFolderNoImport(folder, filenamePattern); 1303 | } 1304 | } 1305 | 1306 | return filesProcessed; 1307 | } 1308 | 1309 | /** 1310 | * Process the specified filename within parentFolder. 1311 | * @param parentFolder domain folder to be searched 1312 | * @param filename name of file to be imported 1313 | * @return true if one or more files processed 1314 | * @throws IOException if an IO problem occurred. 1315 | */ 1316 | private boolean processFolderNoImport(DomainFolder parentFolder, String filename) 1317 | throws IOException { 1318 | 1319 | if (parentFolder.isEmpty()) { 1320 | return false; 1321 | } 1322 | 1323 | boolean filesProcessed = false; 1324 | 1325 | DomainFile domFile = parentFolder.getFile(filename); 1326 | if (ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { 1327 | filesProcessed = true; 1328 | processFileNoImport(domFile); 1329 | } 1330 | 1331 | if (options.recursive) { 1332 | for (DomainFolder folder : parentFolder.getFolders()) { 1333 | filesProcessed |= processFolderNoImport(folder, filename); 1334 | } 1335 | } 1336 | 1337 | return filesProcessed; 1338 | } 1339 | 1340 | private void processNoImport(String rootFolderPath) throws IOException { 1341 | 1342 | storage.clear(); 1343 | 1344 | DomainFolder domFolder = project.getProjectData().getFolder(rootFolderPath); 1345 | if (domFolder == null) { 1346 | throw new IOException("Specified project folder not found: " + rootFolderPath); 1347 | } 1348 | 1349 | Pattern filenamePattern = null; 1350 | if (options.domainFileNameToProcess != null) { 1351 | filenamePattern = createFilenamePattern(options.domainFileNameToProcess); 1352 | } 1353 | 1354 | boolean filesProcessed = false; 1355 | if (filenamePattern == null && options.domainFileNameToProcess != null) { 1356 | // assume domainFileNameToProcess was a specific filename and not a pattern 1357 | filesProcessed = processFolderNoImport(domFolder, options.domainFileNameToProcess); 1358 | } 1359 | else { 1360 | filesProcessed = processFolderNoImport(domFolder, filenamePattern); 1361 | } 1362 | 1363 | if (!filesProcessed) { 1364 | if (options.domainFileNameToProcess != null) { 1365 | throw new IOException("Requested project program file(s) not found: " + 1366 | options.domainFileNameToProcess); 1367 | } 1368 | throw new IOException("No program files found within specified project folder: " + 1369 | domFolder.getPathname()); 1370 | } 1371 | } 1372 | 1373 | private Pattern createFilenamePattern(String name) { 1374 | 1375 | if ((name.indexOf('*') == -1) && (name.indexOf('?') == -1)) { 1376 | // not a 'search' pattern 1377 | return null; 1378 | } 1379 | 1380 | // If surrounded by single-quotes, strip them, as to not interfere with the Pattern 1381 | if ((name.startsWith("\'")) && (name.endsWith("\'"))) { 1382 | name = name.substring(1, name.length() - 1); 1383 | } 1384 | 1385 | // Find files that match the wildcard pattern 1386 | Pattern p = UserSearchUtils.createSearchPattern(name, true); 1387 | return p; 1388 | } 1389 | 1390 | private boolean checkOverwrite(DomainFile df) throws IOException { 1391 | if (options.overwrite) { 1392 | try { 1393 | if (df.isHijacked()) { 1394 | Msg.error(this, 1395 | "REPORT: Found conflicting program file in project which is hijacked - overwrite denied: " + 1396 | df.getPathname()); 1397 | return false; 1398 | } 1399 | if (df.isVersioned()) { 1400 | if (!options.commit) { 1401 | Msg.error(this, 1402 | "REPORT: Found conflicting versioned program file in project with changes - overwrite denied when commit disabled: " + 1403 | df.getPathname()); 1404 | return false; 1405 | } 1406 | if (df.isCheckedOut()) { 1407 | df.undoCheckout(false); 1408 | } 1409 | } 1410 | try { 1411 | df.delete(); 1412 | } 1413 | catch (IOException e) { 1414 | Msg.error(this, "REPORT: Failed to remove conflicting program file (" + 1415 | e.getMessage() + "): " + df.getPathname()); 1416 | return false; 1417 | } 1418 | } 1419 | catch (UserAccessException e) { 1420 | Msg.error(this, 1421 | "REPORT: Found conflicting program file in project which user is unable to overwrite: " + 1422 | df.getPathname()); 1423 | return false; 1424 | } 1425 | Msg.warn(this, 1426 | "REPORT: Removed conflicting program file from project: " + df.getPathname()); 1427 | } 1428 | else { 1429 | Msg.error(this, 1430 | "REPORT: Found conflicting program file in project: " + df.getPathname()); 1431 | return false; 1432 | } 1433 | return true; 1434 | } 1435 | 1436 | private void commitProgram(DomainFile df) throws IOException { 1437 | 1438 | RepositoryAdapter rep = project.getRepository(); 1439 | if (rep != null) { 1440 | try { 1441 | rep.connect(); 1442 | } 1443 | catch (IOException e) { 1444 | ClientUtil.handleException(rep, e, "Connect", null); 1445 | } 1446 | if (!rep.isConnected()) { 1447 | Msg.error(this, 1448 | df.getPathname() + ": File check-in failed - repository connection error"); 1449 | throw new IOException( 1450 | df.getPathname() + ": File check-in failed - repository connection error"); 1451 | } 1452 | } 1453 | 1454 | if (df.canAddToRepository()) { 1455 | try { 1456 | df.addToVersionControl(options.commitComment, false, TaskMonitor.DUMMY); 1457 | Msg.info(this, "REPORT: Added file to repository: " + df.getPathname()); 1458 | } 1459 | catch (IOException e) { 1460 | Msg.error(this, df.getPathname() + ": File check-in failed - " + e.getMessage()); 1461 | throw e; 1462 | } 1463 | catch (CancelledException e) { 1464 | // this can never happen because there is no user interaction in headless! 1465 | } 1466 | } 1467 | else if (df.canCheckin()) { 1468 | try { 1469 | df.checkin(new CheckinHandler() { 1470 | @Override 1471 | public boolean keepCheckedOut() throws CancelledException { 1472 | return true; 1473 | } 1474 | 1475 | @Override 1476 | public String getComment() throws CancelledException { 1477 | return options.commitComment; 1478 | } 1479 | 1480 | @Override 1481 | public boolean createKeepFile() throws CancelledException { 1482 | return false; 1483 | } 1484 | }, TaskMonitor.DUMMY); 1485 | Msg.info(this, "REPORT: Committed file changes to repository: " + df.getPathname()); 1486 | } 1487 | catch (IOException e) { 1488 | Msg.error(this, df.getPathname() + ": File check-in failed - " + e.getMessage()); 1489 | throw e; 1490 | } 1491 | catch (VersionException e) { 1492 | Msg.error(this, 1493 | df.getPathname() + ": File check-in failed - version error occurred"); 1494 | } 1495 | catch (CancelledException e) { 1496 | // this can never happen because there is no user interaction in headless! 1497 | } 1498 | } 1499 | else { 1500 | Msg.error(this, df.getPathname() + ": Unable to commit file"); 1501 | } 1502 | } 1503 | 1504 | private boolean processFileWithImport(File file, String folderPath) { 1505 | 1506 | Msg.info(this, "IMPORTING: " + file.getAbsolutePath()); 1507 | 1508 | Program program = null; 1509 | 1510 | try { 1511 | String dfName = null; 1512 | DomainFile df = null; 1513 | DomainFolder domainFolder = null; 1514 | try { 1515 | // Gets parent folder for import (creates path if doesn't exist) 1516 | domainFolder = getDomainFolder(folderPath, false); 1517 | 1518 | dfName = file.getName(); 1519 | 1520 | if (dfName.toLowerCase().endsWith(".gzf") || 1521 | dfName.toLowerCase().endsWith(".xml")) { 1522 | // Use filename without .gzf 1523 | int index = dfName.lastIndexOf('.'); 1524 | dfName = dfName.substring(0, index); 1525 | } 1526 | 1527 | if (!options.readOnly) { 1528 | if (domainFolder != null) { 1529 | df = domainFolder.getFile(dfName); 1530 | } 1531 | if (df != null && !checkOverwrite(df)) { 1532 | return false; 1533 | } 1534 | df = null; 1535 | } 1536 | 1537 | program = loadProgram(file); 1538 | if (program == null) { 1539 | return false; 1540 | } 1541 | 1542 | // Check if there are defined memory blocks; abort if not (there is nothing 1543 | // to work with!) 1544 | if (program.getMemory().getAllInitializedAddressSet().isEmpty()) { 1545 | Msg.error(this, "REPORT: Error: No memory blocks were defined for file '" + 1546 | file.getAbsolutePath() + "'."); 1547 | return false; 1548 | } 1549 | } 1550 | catch (Exception exc) { 1551 | Msg.error(this, "REPORT: " + exc.getMessage(), exc); 1552 | exc.printStackTrace(); 1553 | return false; 1554 | } 1555 | 1556 | Msg.info(this, 1557 | "REPORT: Import succeeded with language \"" + 1558 | program.getLanguageID().getIdAsString() + "\" and cspec \"" + 1559 | program.getCompilerSpec().getCompilerSpecID().getIdAsString() + 1560 | "\" for file: " + file.getAbsolutePath()); 1561 | 1562 | boolean doSave; 1563 | try { 1564 | 1565 | doSave = analyzeProgram(file.getAbsolutePath(), program) && !options.readOnly; 1566 | 1567 | if (!doSave) { 1568 | program.setTemporary(true); 1569 | } 1570 | 1571 | // The act of marking the program as temporary by a script will signal 1572 | // us to discard any program changes. 1573 | if (program.isTemporary()) { 1574 | if (options.readOnly) { 1575 | Msg.info(this, "REPORT: Discarded file import due to readOnly option: " + 1576 | file.getAbsolutePath()); 1577 | } 1578 | else { 1579 | Msg.info(this, "REPORT: Discarded file import as a result of script " + 1580 | "activity or analysis timeout: " + file.getAbsolutePath()); 1581 | } 1582 | return true; 1583 | } 1584 | 1585 | try { 1586 | if (saveDomainFolder != null) { 1587 | 1588 | df = saveDomainFolder.getFile(dfName); 1589 | 1590 | // Return if file already exists and overwrite == false 1591 | if (df != null && !checkOverwrite(df)) { 1592 | return false; 1593 | } 1594 | 1595 | domainFolder = saveDomainFolder; 1596 | } 1597 | else if (domainFolder == null) { 1598 | domainFolder = getDomainFolder(folderPath, true); 1599 | } 1600 | df = domainFolder.createFile(dfName, program, TaskMonitor.DUMMY); 1601 | Msg.info(this, "REPORT: Save succeeded for file: " + df.getPathname()); 1602 | 1603 | if (options.commit) { 1604 | 1605 | AutoAnalysisManager.getAnalysisManager(program).dispose(); 1606 | program.release(this); 1607 | program = null; 1608 | 1609 | commitProgram(df); 1610 | } 1611 | } 1612 | catch (IOException e) { 1613 | e.printStackTrace(); 1614 | throw new IOException("Cannot create file: " + domainFolder.getPathname() + 1615 | DomainFolder.SEPARATOR + dfName, e); 1616 | } 1617 | } 1618 | catch (Exception exc) { 1619 | String logErrorMsg = 1620 | file.getAbsolutePath() + " Error during analysis: " + exc.getMessage(); 1621 | Msg.info(this, logErrorMsg); 1622 | return false; 1623 | } 1624 | finally { 1625 | if (program != null) { 1626 | AutoAnalysisManager.getAnalysisManager(program).dispose(); 1627 | } 1628 | } 1629 | 1630 | return true; 1631 | } 1632 | finally { 1633 | // Program must be released here, since the AutoAnalysisManager uses program to 1634 | // call dispose() in the finally() block above. 1635 | if (program != null) { 1636 | program.release(this); 1637 | program = null; 1638 | } 1639 | } 1640 | } 1641 | 1642 | private Program loadProgram(File file) throws VersionException, InvalidNameException, 1643 | DuplicateNameException, CancelledException, IOException { 1644 | 1645 | MessageLog messageLog = new MessageLog(); 1646 | Program program = null; 1647 | 1648 | // NOTE: we must pass a null DomainFolder to the AutoImporter so as not to 1649 | // allow the DomainFile to be saved at this point. DomainFile should be 1650 | // saved after all applicable analysis/scripts are run. 1651 | 1652 | if (options.loaderClass == null) { 1653 | // User did not specify a loader 1654 | if (options.language == null) { 1655 | program = AutoImporter.importByUsingBestGuess(file, null, null, this, messageLog, 1656 | TaskMonitor.DUMMY).getPrimaryDomainObject(); 1657 | } 1658 | else { 1659 | program = AutoImporter.importByLookingForLcs(file, null, null, options.language, 1660 | options.compilerSpec, this, messageLog, TaskMonitor.DUMMY).getPrimaryDomainObject(); 1661 | } 1662 | } 1663 | else { 1664 | // User specified a loader 1665 | if (options.language == null) { 1666 | program = AutoImporter.importByUsingSpecificLoaderClass(file, null, null, 1667 | options.loaderClass, options.loaderArgs, this, messageLog, TaskMonitor.DUMMY).getPrimaryDomainObject(); 1668 | } 1669 | else { 1670 | program = AutoImporter.importByUsingSpecificLoaderClassAndLcs(file, null, null, 1671 | options.loaderClass, options.loaderArgs, options.language, options.compilerSpec, 1672 | this, messageLog, TaskMonitor.DUMMY).getPrimaryDomainObject(); 1673 | } 1674 | } 1675 | 1676 | if (program == null) { 1677 | Msg.error(this, "The AutoImporter could not successfully load " + 1678 | file.getAbsolutePath() + 1679 | " with the provided import parameters. Please ensure that any specified" + 1680 | " processor/cspec arguments are compatible with the loader that is used during" + 1681 | " import and try again."); 1682 | 1683 | if (options.loaderClass != null && options.loaderClass != BinaryLoader.class) { 1684 | Msg.error(this, 1685 | "NOTE: Import failure may be due to missing opinion for \"" + 1686 | options.loaderClass.getSimpleName() + 1687 | "\". If so, please contact Ghidra team for assistance."); 1688 | } 1689 | 1690 | return null; 1691 | } 1692 | 1693 | return program; 1694 | } 1695 | 1696 | private void processWithImport(File file, String folderPath, boolean isFirstTime) 1697 | throws IOException { 1698 | 1699 | boolean importSucceeded; 1700 | 1701 | if (file.isFile()) { 1702 | 1703 | importSucceeded = processFileWithImport(file, folderPath); 1704 | 1705 | // Check to see if there are transient programs lying around due 1706 | // to programs not being released during Importing 1707 | List domainFileContainer = new ArrayList<>(); 1708 | TransientDataManager.getTransients(domainFileContainer); 1709 | if (domainFileContainer.size() > 0) { 1710 | TransientDataManager.releaseFiles(this); 1711 | } 1712 | 1713 | if (!importSucceeded) { 1714 | Msg.error(this, "REPORT: Import failed for file: " + file.getAbsolutePath()); 1715 | } 1716 | 1717 | return; 1718 | } 1719 | 1720 | // Looks inside the folder if one of two situations is applicable: 1721 | // - If user supplied a directory to import, and it is currently being 1722 | // processed (if so, this will be the first time that this method is called) 1723 | // - If -recursive is specified 1724 | if ((isFirstTime) || (!isFirstTime && options.recursive)) { 1725 | // Otherwise, is a directory 1726 | Msg.info(this, "REPORT: Importing all files from " + file.getName()); 1727 | 1728 | File dirFile = file; 1729 | 1730 | if (!folderPath.endsWith(DomainFolder.SEPARATOR)) { 1731 | folderPath += DomainFolder.SEPARATOR; 1732 | } 1733 | 1734 | String subfolderPath = folderPath + file.getName(); 1735 | 1736 | String[] names = dirFile.list(); 1737 | if (names != null) { 1738 | Collections.sort(Arrays.asList(names)); 1739 | for (String name : names) { 1740 | if (name.charAt(0) == '.') { 1741 | Msg.warn(this, "Ignoring file '" + name + "'."); 1742 | continue; 1743 | } 1744 | file = new File(dirFile, name); 1745 | 1746 | // Even a directory name has to have valid characters -- 1747 | // can't create a folder if it's not valid 1748 | try { 1749 | checkValidFilename(file); 1750 | processWithImport(file, subfolderPath, false); 1751 | } 1752 | catch (InvalidInputException e) { 1753 | // Just move on if not valid 1754 | } 1755 | } 1756 | } 1757 | } 1758 | } 1759 | 1760 | private void processWithImport(String folderPath, List inputDirFiles) throws IOException { 1761 | 1762 | storage.clear(); 1763 | 1764 | if (inputDirFiles != null && !inputDirFiles.isEmpty()) { 1765 | Msg.info(this, "REPORT: Processing input files: "); 1766 | Msg.info(this, " project: " + project.getProjectLocator()); 1767 | for (File f : inputDirFiles) { 1768 | processWithImport(f, folderPath, true); 1769 | } 1770 | } 1771 | else { 1772 | //no input, just run the scripts 1773 | 1774 | //create one state, in case each script might want to modify it to pass information 1775 | GhidraState scriptState = new GhidraState(null, project, null, null, null, null); 1776 | 1777 | LibHeadlessContinuationOption scriptStatus = LibHeadlessContinuationOption.CONTINUE; 1778 | 1779 | scriptStatus = runScriptsList(options.preScripts, options.preScriptFileMap, scriptState, 1780 | scriptStatus); 1781 | 1782 | // Since there is no program, "DELETE" is meaningless here. 1783 | // If status asks for ABORT, then don't continue running the postscript. 1784 | switch (scriptStatus) { 1785 | case ABORT: 1786 | case ABORT_AND_DELETE: 1787 | return; 1788 | 1789 | default: 1790 | // Just continue 1791 | } 1792 | 1793 | runScriptsList(options.postScripts, options.postScriptFileMap, scriptState, 1794 | scriptStatus); 1795 | } 1796 | } 1797 | 1798 | private Project openProject(ProjectLocator locator) throws IOException { 1799 | Project tempProject; 1800 | 1801 | if (options.deleteProject) { 1802 | Msg.warn(this, "Project already exists and will not be deleted: " + locator); 1803 | options.deleteProject = false; 1804 | } 1805 | 1806 | Msg.info(this, "Opening existing project: " + locator); 1807 | try { 1808 | tempProject = new HeadlessProject(getProjectManager(), locator); 1809 | } 1810 | catch (NotOwnerException e) { 1811 | throw new IOException(e); 1812 | } 1813 | catch (LockException e) { 1814 | throw new IOException(e); 1815 | } 1816 | 1817 | return tempProject; 1818 | 1819 | } 1820 | 1821 | /** 1822 | * Checks to make sure the given file contains only valid characters in its name. 1823 | * 1824 | * @param currFile The file to check. 1825 | * @throws InvalidInputException if the given file contains invalid characters in it. 1826 | */ 1827 | static void checkValidFilename(File currFile) throws InvalidInputException { 1828 | boolean isDir = currFile.isDirectory(); 1829 | String filename = currFile.getName(); 1830 | 1831 | for (int i = 0; i < filename.length(); i++) { 1832 | char c = filename.charAt(i); 1833 | if (!LocalFileSystem.isValidNameCharacter(c)) { 1834 | if (isDir) { 1835 | throw new InvalidInputException("The directory '" + filename + 1836 | "' contains the invalid characgter: \'" + c + 1837 | "\' and can not be created in the project (full path: " + 1838 | currFile.getAbsolutePath() + 1839 | "). To allow successful import of the directory and its contents, please rename the directory."); 1840 | } 1841 | throw new InvalidInputException( 1842 | "The file '" + filename + "' contains the invalid character: \'" + c + 1843 | "\' and can not be imported (full path: " + currFile.getAbsolutePath() + 1844 | "). Please rename the file."); 1845 | } 1846 | } 1847 | } 1848 | 1849 | private HeadlessGhidraProjectManager getProjectManager() { 1850 | if (projectManager == null) { 1851 | projectManager = new HeadlessGhidraProjectManager(); 1852 | } 1853 | return projectManager; 1854 | } 1855 | 1856 | /** 1857 | * Ghidra project class required to gain access to specialized project constructor 1858 | * for URL connection. 1859 | */ 1860 | private static class HeadlessProject extends DefaultProject { 1861 | 1862 | HeadlessProject(HeadlessGhidraProjectManager projectManager, GhidraURLConnection connection) 1863 | throws IOException { 1864 | super(projectManager, (DefaultProjectData) connection.getProjectData()); 1865 | } 1866 | 1867 | HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator) 1868 | throws NotOwnerException, LockException, IOException { 1869 | super(projectManager, projectLocator, false); 1870 | } 1871 | } 1872 | 1873 | private static class HeadlessGhidraProjectManager extends DefaultProjectManager { 1874 | // this exists just to allow access to the constructor 1875 | } 1876 | } 1877 | --------------------------------------------------------------------------------