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