1 /*
2  * Copyright (C) 2024 The Android Open Source Project
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 package com.android.sts.common;
18 
19 import static com.android.sts.common.CommandUtil.runAndCheck;
20 import static com.android.tradefed.util.FileUtil.chmodRWXRecursively;
21 import static com.android.tradefed.util.FileUtil.createNamedTempDir;
22 import static com.android.tradefed.util.FileUtil.recursiveDelete;
23 
24 import com.android.tradefed.device.ITestDevice;
25 import com.android.tradefed.util.RunUtil;
26 
27 import java.io.ByteArrayOutputStream;
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.FileWriter;
31 import java.nio.file.Paths;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Optional;
36 import java.util.Properties;
37 
38 /** class for running Ghidra scripts. */
39 public class GhidraScriptRunner {
40     public static final long POST_SCRIPT_TIMEOUT = 90 * 1000L; // 90 seconds
41     private ByteArrayOutputStream mOutputOfGhidra;
42     private ITestDevice mDevice;
43     private String mBinaryName;
44     private String mBinaryFile;
45     private String mAnalyzeHeadlessPath;
46     private Optional<String> mPropertiesFileName = Optional.empty();
47     private Optional<String> mPreScriptFileName = Optional.empty();
48     private Optional<String> mPostScriptFileName = Optional.empty();
49     private Optional<String> mPreScriptContent = Optional.empty();
50     private Optional<String> mPostScriptContent = Optional.empty();
51     private Map<String, String> mPropertiesFileContentsMap = Collections.emptyMap();
52     private String mCallingClass;
53     private boolean mEnableAnalysis = false;
54     private File mPulledLibSaveFolder;
55 
56     /**
57      * Constructor for GhidraScriptRunner. When using this constructor from the same class
58      * concurrently, make sure to append a unique suffix to the {@code callingClass} parameter to
59      * avoid conflicts.
60      *
61      * @param device The ITestDevice to pull the binary from.
62      * @param binaryFile The binary file in device.
63      * @param callingClass The name of the calling class.
64      * @param propertiesFileName The file name of properties file associated with post script. eg.
65      *     function_offset_post_script.properties
66      * @param preScriptFileName The file name of pre script file.
67      * @param postScriptFileName The file name of post script file. eg.
68      *     function_offset_post_script.java
69      */
GhidraScriptRunner( ITestDevice device, File binaryFile, String callingClass, String propertiesFileName, String preScriptFileName, String postScriptFileName, String analyzeHeadlessPath)70     public GhidraScriptRunner(
71             ITestDevice device,
72             File binaryFile,
73             String callingClass,
74             String propertiesFileName,
75             String preScriptFileName,
76             String postScriptFileName,
77             String analyzeHeadlessPath) {
78         mDevice = device;
79         mBinaryName = binaryFile.getName();
80         mBinaryFile = binaryFile.toString();
81         mCallingClass = callingClass;
82         mAnalyzeHeadlessPath = analyzeHeadlessPath;
83         mPropertiesFileName = Optional.ofNullable(propertiesFileName);
84         mPreScriptFileName = Optional.ofNullable(preScriptFileName);
85         mPostScriptFileName = Optional.ofNullable(postScriptFileName);
86     }
87 
88     /**
89      * Set analysis flag during Ghidra script execution.
90      *
91      * @return This GhidraScriptRunner instance with analysis enabled.
92      */
enableAnalysis()93     public GhidraScriptRunner enableAnalysis() {
94         mEnableAnalysis = true;
95         return this;
96     }
97 
98     /**
99      * Return ByteArrayOutputStream.
100      *
101      * @return mOutputOfGhidra.
102      */
getOutputStream()103     public ByteArrayOutputStream getOutputStream() {
104         return mOutputOfGhidra;
105     }
106 
107     /**
108      * Specify a post-script its properties to be executed after Ghidra analysis.
109      *
110      * @param contents The contents of the post-script.
111      * @param propertiesContent The map of key value pairs to write in properties file
112      * @return This GhidraScriptRunner instance with post-script enabled and configured.
113      */
postScript(String contents, Map<String, String> propertiesContent)114     public GhidraScriptRunner postScript(String contents, Map<String, String> propertiesContent) {
115         mPostScriptContent = Optional.ofNullable(contents);
116 
117         if (!propertiesContent.isEmpty()) {
118             mPropertiesFileContentsMap = propertiesContent;
119         }
120         return this;
121     }
122 
123     /**
124      * Specify a pre-script to be executed before Ghidra analysis.
125      *
126      * @param contents The contents of the pre-script.
127      * @return This GhidraScriptRunner instance with pre-script enabled and configured.
128      */
preScript(String contents)129     public GhidraScriptRunner preScript(String contents) {
130         mPreScriptContent = Optional.ofNullable(contents);
131         return this;
132     }
133 
134     /**
135      * Run Ghidra with the specified options and scripts.
136      *
137      * @return an AutoCloseable for cleaning up temporary files after script execution.
138      */
run()139     public AutoCloseable run() throws Exception {
140         return runWithTimeout(POST_SCRIPT_TIMEOUT);
141     }
142 
143     /**
144      * Run Ghidra with the specified options and scripts, with a timeout.
145      *
146      * @param timeout The timeout value in milliseconds.
147      * @return an AutoCloseable for cleaning up temporary files after script execution.
148      */
runWithTimeout(long timeout)149     public AutoCloseable runWithTimeout(long timeout) throws Exception {
150         try {
151             // Get the language using readelf
152             String deviceSerial = mDevice.getSerialNumber().replace(":", "");
153             String pulledLibSaveFolderString = mCallingClass + "_" + deviceSerial + "_files";
154             String language = getLanguage(mDevice, mBinaryFile);
155             mPulledLibSaveFolder =
156                     createNamedTempDir(
157                             Paths.get(mAnalyzeHeadlessPath).getParent().toFile(),
158                             pulledLibSaveFolderString);
159 
160             // Pull binary from the device to the folder
161             if (!mDevice.pullFile(
162                     mBinaryFile, new File(mPulledLibSaveFolder + "/" + mBinaryName))) {
163                 throw new Exception("Pulling " + mBinaryFile + " was not successful");
164             }
165 
166             // Create script related files and chmod rwx them
167             if (!mPropertiesFileContentsMap.isEmpty() && mPropertiesFileName.isPresent()) {
168                 createPropertiesFile(
169                         mPulledLibSaveFolder,
170                         mPropertiesFileName.get(),
171                         mPropertiesFileContentsMap);
172             }
173             if (mPreScriptContent.isPresent() && mPreScriptFileName.isPresent()) {
174                 createScriptFile(
175                         mPulledLibSaveFolder, mPreScriptFileName.get(), mPreScriptContent.get());
176             }
177             if (mPostScriptContent.isPresent() && mPostScriptFileName.isPresent()) {
178                 createScriptFile(
179                         mPulledLibSaveFolder, mPostScriptFileName.get(), mPostScriptContent.get());
180             }
181             if (!chmodRWXRecursively(mPulledLibSaveFolder)) {
182                 throw new Exception("chmodRWX failed for " + mPulledLibSaveFolder.toString());
183             }
184 
185             // Analyze the pulled binary using Ghidra headless analyzer
186             List<String> cmd =
187                     createCommandList(
188                             mAnalyzeHeadlessPath,
189                             mCallingClass,
190                             deviceSerial,
191                             mPulledLibSaveFolder.getPath(),
192                             mBinaryName,
193                             mPreScriptFileName.isPresent() ? mPreScriptFileName.get() : "",
194                             mPostScriptFileName.isPresent() ? mPostScriptFileName.get() : "",
195                             language,
196                             mEnableAnalysis);
197             mOutputOfGhidra = new ByteArrayOutputStream();
198             Process ghidraProcess = RunUtil.getDefault().runCmdInBackground(cmd, mOutputOfGhidra);
199             if (!ghidraProcess.isAlive()) {
200                 throw new Exception("Ghidra process died. Output:" + mOutputOfGhidra);
201             }
202 
203             if (mOutputOfGhidra.toString("UTF-8").contains("Enter path to JDK home directory")) {
204                 throw new Exception(
205                         "JDK 17+ (64-bit) not found in the system PATH. Please add it to your"
206                                 + " PATH environment variable.");
207             }
208             return () -> recursiveDelete(mPulledLibSaveFolder);
209         } catch (Exception e) {
210             recursiveDelete(mPulledLibSaveFolder);
211             throw e;
212         }
213     }
214 
215     /**
216      * Creates a ghidra script file with the specified content in the given folder.
217      *
218      * @param folder The folder where the script file will be created.
219      * @param fileName The name of the script file.
220      * @param content The content to be written into the script file.
221      */
createScriptFile(File folder, String fileName, String content)222     private static void createScriptFile(File folder, String fileName, String content)
223             throws Exception {
224         try (FileWriter fileWriter = new FileWriter(new File(folder, fileName))) {
225             fileWriter.write(content);
226         }
227     }
228 
229     /**
230      * Creates a ghidra script properties file with the specified content in the given folder.
231      *
232      * @param folder The folder where the script file will be created.
233      * @param fileName The name of the script file.
234      * @param map The map of key value pairs to be written into the properties file.
235      */
createPropertiesFile(File folder, String fileName, Map<String, String> map)236     private static void createPropertiesFile(File folder, String fileName, Map<String, String> map)
237             throws Exception {
238         File propertiesFile = new File(folder, fileName);
239         try (FileWriter fileWriter = new FileWriter(propertiesFile);
240                 FileInputStream fileInputStream = new FileInputStream(propertiesFile)) {
241             propertiesFile.createNewFile();
242 
243             Properties properties = new Properties();
244             if (propertiesFile.exists()) {
245                 properties.load(fileInputStream);
246             } else {
247                 throw new Exception("Unable to create ghidra script properties file");
248             }
249 
250             // Populate properties file
251             map.forEach(properties::setProperty);
252             properties.store(fileWriter, "");
253         }
254     }
255 
256     /**
257      * Uses the value under 'Machine' from the output of 'readelf -h' command on the specified
258      * binary file to determine the '-processor' parameter (language id) to be passed when invoking
259      * ghidra.
260      *
261      * <p>For e.g. readelf -h <binary_name>
262      *
263      * <p>ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
264      *
265      * <p>[...]
266      *
267      * <p>Machine: arm64 <--- ABI or processor type
268      *
269      * <p>Version: 0x1
270      *
271      * <p>[...]
272      *
273      * <p>The language id can be found in processor-specific .ldefs files located at:
274      * https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Processors/
275      * <proc_name>/data/languages/<proc_name>.ldefs
276      *
277      * <p>where 'proc_name' can be AARCH64, ARM, x86 which are the only ABIs that Android currently
278      * supports as per https://developer.android.com/ndk/guides/abis
279      *
280      * <p>For e.g. Following code snippet from AARCH64.ldefs shows the language 'id' of 'AARCH64'
281      * machine type ie 'arm64'.
282      *
283      * <p><language processor="AARCH64" endian="little" size="64" variant="v8A" version="1.6"
284      * slafile="AARCH64.sla" processorspec="AARCH64.pspec" manualindexfile="../manuals/AARCH64.idx"
285      * id="AARCH64:LE:64:v8A"> <--- 'id' denotes the language id.
286      *
287      * <p>TODO: Utilize ghidra pre script for setting language automatically.
288      *
289      * @param device The ITestDevice representing the testing device.
290      * @param binaryFile The path to the binary file.
291      * @return The language of the binary in the format "ARCH:ENDIAN:BITS:VARIANT"
292      */
getLanguage(ITestDevice device, String binaryFile)293     private static String getLanguage(ITestDevice device, String binaryFile) throws Exception {
294         String language =
295                 runAndCheck(device, "readelf -h " + binaryFile + " | grep Machine")
296                         .getStdout()
297                         .trim()
298                         .split(":\\s*")[1]
299                         .trim();
300         switch (language) {
301             case "arm":
302                 return "ARM:LE:32:v8";
303             case "arm64":
304                 return "AARCH64:LE:64:v8A";
305             case "386":
306                 return "x86:LE:32:default";
307             case "x86-64":
308                 return "x86:LE:64:default";
309             case "riscv":
310                 return "RISCV:LE:64:RV64GC";
311             default:
312                 throw new Exception("Unsupported Machine: " + language);
313         }
314     }
315 
316     /**
317      * Creates a list of command-line arguments for invoking Ghidra's analyzeHeadless tool.
318      *
319      * @param ghidraBinaryLocation The analyzerHeadless location.
320      * @param callingClass The name of the calling class.
321      * @param deviceSerialNumber The serial number of the target device.
322      * @param tempFileName The temporary folder name in which target binary is pulled.
323      * @param binaryName The name of the binary file to run analyzeHeadless on.
324      * @param preScriptFileName The file name of the pre script.
325      * @param postScriptFileName The file name of the post script.
326      * @param lang The processor language for analysis. eg ARM:LE:32:v8
327      * @param analysis Flag indicating whether analysis should be performed.
328      * @return A list of command-line arguments for Ghidra analyzeHeadless tool.
329      */
createCommandList( String ghidraBinaryLocation, String callingClass, String deviceSerialNumber, String tempFileName, String binaryName, String preScriptFileName, String postScriptFileName, String lang, boolean analysis)330     private static List<String> createCommandList(
331             String ghidraBinaryLocation,
332             String callingClass,
333             String deviceSerialNumber,
334             String tempFileName,
335             String binaryName,
336             String preScriptFileName,
337             String postScriptFileName,
338             String lang,
339             boolean analysis) {
340         boolean preScript = !preScriptFileName.isEmpty();
341         boolean postScript = !postScriptFileName.isEmpty();
342         return List.of(
343                 ghidraBinaryLocation,
344                 tempFileName,
345                 callingClass + "_ghidra_project_" + deviceSerialNumber,
346                 "-import",
347                 tempFileName + "/" + binaryName,
348                 "-scriptPath",
349                 tempFileName,
350                 postScript ? "-postScript" : "",
351                 postScript ? postScriptFileName : "",
352                 preScript ? "-preScript" : "",
353                 preScript ? preScriptFileName : "",
354                 "-processor",
355                 lang,
356                 analysis ? "" : "-noanalysis",
357                 "-deleteProject");
358     }
359 }
360