Add utility classes for Ghidra
Bug: 276748671
Test: Build CTS, run a test using GhidraUtils class and observe behavior.
Change-Id: Idbfe357559b303d113be343c164ad8d658708719
Merged-In: Idbfe357559b303d113be343c164ad8d658708719
diff --git a/libraries/sts-common-util/host-side/Android.bp b/libraries/sts-common-util/host-side/Android.bp
index 495caf3..aa9689f 100644
--- a/libraries/sts-common-util/host-side/Android.bp
+++ b/libraries/sts-common-util/host-side/Android.bp
@@ -18,6 +18,7 @@
java_library_host {
name: "sts-host-util",
+ java_resources: [":sts_host_util_java_resources"],
defaults: ["cts_error_prone_rules"],
srcs: [
@@ -40,6 +41,13 @@
],
}
+// Workaround: java_resource_dirs ignores *.java files
+filegroup {
+ name: "sts_host_util_java_resources",
+ path: "res/",
+ srcs: ["res/**/*.java"],
+}
+
java_library_host {
name: "sts-libtombstone_proto-java",
visibility: [
diff --git a/libraries/sts-common-util/host-side/res/ghidra-scripts/FunctionOffsetPostScript.java b/libraries/sts-common-util/host-side/res/ghidra-scripts/FunctionOffsetPostScript.java
new file mode 100644
index 0000000..8aa5649
--- /dev/null
+++ b/libraries/sts-common-util/host-side/res/ghidra-scripts/FunctionOffsetPostScript.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ghidra.app.script.GhidraScript;
+import ghidra.program.model.listing.Function;
+import ghidra.program.model.listing.FunctionIterator;
+
+import java.io.ObjectOutputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class FunctionOffsetPostScript extends GhidraScript {
+
+ public void run() throws Exception {
+ String spaceSeparatedFunctionNames = propertiesFileParams.getValue("functionNames");
+ List<String> listOfFunctions = Arrays.asList(spaceSeparatedFunctionNames.split("\\s+"));
+ List<BigInteger> output = new ArrayList<>();
+
+ // Find the function offsets
+ for (String function : listOfFunctions) {
+ FunctionIterator functionIterator = currentProgram.getListing().getFunctions(true);
+ BigInteger offset = null;
+ while (functionIterator.hasNext()) {
+ Function nextFunction = functionIterator.next();
+ if (!nextFunction.getName().equals(function)) {
+ continue; // Skip to the next iteration if the function name doesn't match
+ }
+
+ // If the function name matches, calculate the offset
+ offset =
+ nextFunction
+ .getEntryPoint()
+ .subtract(currentProgram.getImageBase().getOffset())
+ .getOffsetAsBigInteger();
+ break;
+ }
+
+ // 'output' is appended in the same order as 'listOfFunctions' contains the function
+ // names. If an offset is not found, null is appended.
+ output.add(offset);
+ }
+ try (Socket socket =
+ new Socket(
+ "localhost",
+ Integer.parseInt(propertiesFileParams.getValue("port")));
+ ObjectOutputStream outputStream =
+ new ObjectOutputStream(socket.getOutputStream()); ) {
+ outputStream.writeObject(output);
+ }
+ }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraFunctionOffsets.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraFunctionOffsets.java
new file mode 100644
index 0000000..a48b4eb
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraFunctionOffsets.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.android.sts.common.GhidraScriptRunner.POST_SCRIPT_TIMEOUT;
+
+import static java.util.stream.Collectors.joining;
+
+import com.android.tradefed.device.ITestDevice;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.ObjectInputStream;
+import java.math.BigInteger;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A utility class containing constants and functions for a Ghidra post-script (located at
+ * res/ghidra-scripts/FunctionOffsetPostScript.java) used to get function offsets
+ */
+public class GhidraFunctionOffsets {
+ public static final String POST_SCRIPT_CLASS_NAME = "FunctionOffsetPostScript";
+ public static final String POST_SCRIPT_CONTENT_RESOURCE_PATH =
+ "ghidra-scripts/" + POST_SCRIPT_CLASS_NAME + ".java";
+
+ /**
+ * Converts the output of {@code getFunctionOffsets} to a string with space separated offsets.
+ *
+ * @param device The ITestDevice to pull the binary from.
+ * @param binaryName The name of the binary file present in device.
+ * @param binaryPath The path to the binary file in device .
+ * @param callingClass The name of the calling class.
+ * @param functions The list of function names.
+ * @return A string containing space separated function offsets.
+ */
+ public static String getFunctionOffsetsAsCmdLineArgs(
+ ITestDevice device,
+ String binaryName,
+ String binaryPath,
+ String callingClass,
+ List<String> functions,
+ String analyzeHeadlessPath)
+ throws Exception {
+ return String.join(
+ " ",
+ getFunctionOffsets(
+ device,
+ binaryName,
+ binaryPath,
+ callingClass,
+ functions,
+ analyzeHeadlessPath)
+ .stream()
+ .map(BigInteger::toString)
+ .toArray(String[]::new));
+ }
+
+ /**
+ * Retrieves the function offsets from the given binary.
+ *
+ * @param device The ITestDevice to pull the binary from.
+ * @param binaryName The name of the binary file present in device.
+ * @param binaryPath The path to the binary file in device .
+ * @param callingClass The name of the calling class.
+ * @param functions The list of function names.
+ * @return A list of BigIntegers containing function offsets in the same order as @param
+ * functions.
+ */
+ public static List<BigInteger> getFunctionOffsets(
+ ITestDevice device,
+ String binaryName,
+ String binaryPath,
+ String callingClass,
+ List<String> functions,
+ String analyzeHeadlessPath)
+ throws Exception {
+ // Set up a server socket to listen for output from post script
+ final Map<String, List<BigInteger>> mapOfResults = new HashMap<String, List<BigInteger>>();
+ final Semaphore outputReceived = new Semaphore(0);
+ final ServerSocket serverSocket = new ServerSocket(0);
+ serverSocket.setReuseAddress(true /* on */);
+ serverSocket.setSoTimeout((int) POST_SCRIPT_TIMEOUT);
+ int port = serverSocket.getLocalPort();
+ Thread clientThread =
+ new Thread(
+ () -> {
+ try (Socket clientSocket =
+ serverSocket.accept(); // blocks till connected
+ ObjectInputStream inputStream =
+ new ObjectInputStream(
+ clientSocket.getInputStream()); ) {
+ mapOfResults.put(
+ callingClass, (List<BigInteger>) inputStream.readObject());
+ } catch (Exception e) {
+ mapOfResults.put(callingClass, null);
+ } finally {
+ outputReceived.release();
+ }
+ });
+
+ GhidraScriptRunner ghidraScriptRunner =
+ new GhidraScriptRunner(
+ device,
+ binaryName,
+ binaryPath,
+ callingClass,
+ POST_SCRIPT_CLASS_NAME + ".properties",
+ null,
+ POST_SCRIPT_CLASS_NAME + ".java",
+ analyzeHeadlessPath)
+ .postScript(
+ readResource(POST_SCRIPT_CONTENT_RESOURCE_PATH),
+ new HashMap<String, String>(
+ ImmutableMap.of(
+ "functionNames",
+ String.join(" ", functions),
+ "port",
+ String.valueOf(port))));
+ try (AutoCloseable ghidraScriptRunnerAc = ghidraScriptRunner.run()) {
+ clientThread.start();
+ if (!outputReceived.tryAcquire(POST_SCRIPT_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ throw new TimeoutException(
+ "FunctionOffsetPostScript timed out. Output of Ghidra: "
+ + ghidraScriptRunner.getOutputStream().toString("UTF-8"));
+ }
+ return mapOfResults.get(callingClass);
+ }
+ }
+
+ private static String readResource(String fullResourceName) throws Exception {
+ try (InputStream in =
+ GhidraFunctionOffsets.class
+ .getClassLoader()
+ .getResourceAsStream(fullResourceName)) {
+ return new BufferedReader(new InputStreamReader(in))
+ .lines()
+ .collect(joining(System.lineSeparator()));
+ }
+ }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraPreparer.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraPreparer.java
new file mode 100644
index 0000000..1faf062
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraPreparer.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.android.tradefed.util.FileUtil.chmodRWXRecursively;
+import static com.android.tradefed.util.FileUtil.createNamedTempDir;
+import static com.android.tradefed.util.FileUtil.recursiveDelete;
+import static com.android.tradefed.util.ZipUtil.extractZip;
+
+import com.android.sts.common.GitHubUtils.GitHubRepo;
+import com.android.sts.common.util.GhidraBusinessLogicHandler;
+import com.android.tradefed.build.BuildRetrievalError;
+import com.android.tradefed.build.FileDownloadCache;
+import com.android.tradefed.build.FileDownloadCacheFactory;
+import com.android.tradefed.build.IFileDownloader;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.BaseTargetPreparer;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.net.HttpHelper;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+import java.util.zip.ZipFile;
+
+/** Preparer to download Ghidra, unzip it and set the path to the executable */
+@OptionClass(alias = "ghidra-preparer")
+public class GhidraPreparer extends BaseTargetPreparer {
+ private static final Object LOCK_SETUP = new Object(); // Locks the setup
+ private static final String GHIDRA_REPO_OWNER = "NationalSecurityAgency";
+ private static final String GHIDRA_REPO_NAME = "ghidra";
+ public static final String PROPERTY_KEY = "ghidra_analyze_headless";
+ private File mGhidraZipDir = null; // Stores the ghidra zip directory
+ private File mGhidraZipFile = null; // Stores the ghidra zip file
+ private File mCacheDir = null; // Refers to the ghidra cache directory
+ private URI mGhidraZipUri = null; // Stores the url to download ghidra zip
+ private String mPreviousPropertyVal = null;
+
+ @Option(
+ name = "ghidra-tag",
+ description = "Overrides 'gitReleaseTagName' of GhidraBusinessLogicHandler.")
+ private String mTagName = null;
+
+ @Option(
+ name = "ghidra-asset",
+ description = "Overrides 'gitReleaseAssetName' of GhidraBusinessLogicHandler.")
+ private String mAssetName = null;
+
+ /** {@inheritDoc} */
+ @Override
+ public void setUp(TestInformation testInformation)
+ throws DeviceNotAvailableException, BuildError, TargetSetupError {
+ synchronized (LOCK_SETUP) {
+ try {
+ // Fetch value of property 'ghidra_analyze_headless'
+ mPreviousPropertyVal = testInformation.properties().get(PROPERTY_KEY);
+
+ // Fetch the ghidra zip name and url to download ghidra
+ Map.Entry<String, URI> assetNameToUri = getZipNameAndUri();
+ String ghidraZipName = assetNameToUri.getKey();
+ mGhidraZipUri = assetNameToUri.getValue();
+
+ // Create required directories, download ghidra and extract the zip at
+ // /tmp/tradefed_ghidra/<mGhidraZipDir>/<ghidra_zip_here>
+ File ghidraDir = Paths.get("tradefed_ghidra", ghidraZipName).toFile();
+ mGhidraZipDir = createNamedTempDir(ghidraDir.getPath());
+ mCacheDir = createNamedTempDir("ghidra_cache");
+
+ // If 'analyzeHeadless' already exists add path to properties and return.
+ String analyzeHeadlessPath = getAnalyzeHeadlessPath();
+ if (analyzeHeadlessPath != null) {
+ testInformation.properties().put(PROPERTY_KEY, analyzeHeadlessPath);
+ return;
+ }
+
+ // Download and extract ghidra zip
+ downloadAndExtractGhidra();
+
+ // Add path of 'analyzeHeadless' to properties
+ analyzeHeadlessPath = getAnalyzeHeadlessPath();
+ if (analyzeHeadlessPath != null) {
+ testInformation.properties().put(PROPERTY_KEY, analyzeHeadlessPath);
+ } else {
+ throw new TargetSetupError(
+ String.format(
+ "Failed to fetch 'analyzeHeadless' location. Ghidra"
+ + " 'analyzeHeadless' is not found. Please download ghidra"
+ + " manually at /tmp/tradefed_ghidra/%s/ from %s",
+ assetNameToUri.getKey() /* Ghidra zip name */,
+ assetNameToUri.getValue() /* Download link */));
+ }
+ } catch (Exception e) {
+ // Remove the ghidra directory for the current version
+ if (mGhidraZipDir != null) {
+ recursiveDelete(mGhidraZipDir);
+ mGhidraZipDir = null;
+ }
+
+ // Remove the cache directory
+ if (mCacheDir != null) {
+ recursiveDelete(mCacheDir);
+ mCacheDir = null;
+ }
+ throw new TargetSetupError(
+ "Set up failed.", e, null /* deviceDescriptor */, false /* deviceSide */);
+ }
+ }
+ }
+
+ private void downloadAndExtractGhidra() throws Exception {
+ // Download Ghidra zip
+ mGhidraZipFile = new File(mGhidraZipDir, mGhidraZipDir.getName());
+ if (!mGhidraZipFile.exists()) {
+ FileDownloadCache fileDownloadCache =
+ FileDownloadCacheFactory.getInstance().getCache(mCacheDir);
+ fileDownloadCache.fetchRemoteFile(
+ new GhidraFileDownloader(), mGhidraZipUri.toString(), mGhidraZipFile);
+ }
+
+ // Unzip Ghidra zip and delete the zip
+ extractZip(new ZipFile(mGhidraZipFile), mGhidraZipDir);
+ recursiveDelete(mGhidraZipFile);
+
+ // Chmod rwx 'mGhidraZipDir'
+ chmodRWXRecursively(mGhidraZipDir);
+ }
+
+ @Override
+ public void tearDown(TestInformation testInformation, Throwable e) {
+ // Restore the previous property value
+ if (mPreviousPropertyVal != null) {
+ testInformation.properties().put(PROPERTY_KEY, mPreviousPropertyVal);
+ } else {
+ testInformation.properties().remove(PROPERTY_KEY);
+ }
+ }
+
+ /** Fetch the first entry from the map of assetName to URI */
+ private Map.Entry<String, URI> getZipNameAndUri() throws IOException, URISyntaxException {
+ Optional<String> ghidraReleaseTagName = GhidraBusinessLogicHandler.getGitReleaseTagName();
+ Optional<String> ghidraReleaseAssetName = GhidraBusinessLogicHandler.getReleaseAssetName();
+ if (mTagName != null) {
+ ghidraReleaseTagName = Optional.of(mTagName);
+ }
+ if (mAssetName != null) {
+ ghidraReleaseAssetName = Optional.of(mAssetName);
+ }
+
+ // Fetch the assetName to uri map
+ Map<String, URI> mapOfAssetNameToUris =
+ new GitHubRepo(GHIDRA_REPO_OWNER, GHIDRA_REPO_NAME)
+ .getReleaseAssetUris(ghidraReleaseTagName);
+
+ // Get map entry corresponding to 'ghidraReleaseAssetName'
+ if (ghidraReleaseAssetName.isPresent()) {
+ String assetName = ghidraReleaseAssetName.get();
+ URI assetUri = mapOfAssetNameToUris.get(assetName);
+ if (assetUri == null) {
+ throw new IllegalStateException(
+ "The asset name:" + assetName + " was not found in ghidra release.");
+ }
+ return new AbstractMap.SimpleEntry(assetName, assetUri);
+ }
+
+ // Throw if more than one entry was found in the map
+ if (mapOfAssetNameToUris.size() != 1) {
+ throw new IllegalStateException(
+ "More than one entries found in 'mapOfAssetNameToUris'. Entries: "
+ + mapOfAssetNameToUris.toString());
+ }
+
+ // Return the first entry from the map
+ return mapOfAssetNameToUris.entrySet().iterator().next();
+ }
+
+ /** If found returns the path to 'analyzeHeadless' else returns null. */
+ private String getAnalyzeHeadlessPath() {
+ Optional<Path> pathToAnalyzeHeadless = Optional.empty();
+ try (Stream<Path> walkStream = Files.walk(mGhidraZipDir.toPath())) {
+ pathToAnalyzeHeadless =
+ walkStream
+ .filter(path -> path.toFile().isFile())
+ .filter(path -> path.toString().endsWith("/analyzeHeadless"))
+ .findFirst();
+ } catch (Exception e) {
+ // Ignore exceptions
+ }
+ return pathToAnalyzeHeadless.isPresent() ? pathToAnalyzeHeadless.get().toString() : null;
+ }
+
+ private static class GhidraFileDownloader implements IFileDownloader {
+
+ /** {@inheritDoc} */
+ @Override
+ public File downloadFile(String remoteFilePath) throws BuildRetrievalError {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void downloadFile(String relativeRemotePath, File destFile)
+ throws BuildRetrievalError {
+ try {
+ // Download ghidra zip
+ new HttpHelper().doGet(relativeRemotePath, new FileOutputStream(destFile));
+ } catch (Exception e) {
+ throw new BuildRetrievalError("Downloading ghidra zip failed.", e);
+ }
+ }
+ }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraScriptRunner.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraScriptRunner.java
new file mode 100644
index 0000000..6911e9b
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraScriptRunner.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.android.sts.common.CommandUtil.runAndCheck;
+import static com.android.tradefed.util.FileUtil.chmodRWXRecursively;
+import static com.android.tradefed.util.FileUtil.createNamedTempDir;
+import static com.android.tradefed.util.FileUtil.recursiveDelete;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.RunUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+
+/** class for running Ghidra scripts. */
+public class GhidraScriptRunner {
+ public static final long POST_SCRIPT_TIMEOUT = 90 * 1000L; // 90 seconds
+ private ByteArrayOutputStream mOutputOfGhidra;
+ private ITestDevice mDevice;
+ private String mBinaryName;
+ private String mBinaryPath;
+ private String mAnalyzeHeadlessPath;
+ private Optional<String> mPropertiesFileName = Optional.empty();
+ private Optional<String> mPreScriptFileName = Optional.empty();
+ private Optional<String> mPostScriptFileName = Optional.empty();
+ private Optional<String> mPreScriptContent = Optional.empty();
+ private Optional<String> mPostScriptContent = Optional.empty();
+ private Map<String, String> mPropertiesFileContentsMap = Collections.emptyMap();
+ private String mCallingClass;
+ private boolean mEnableAnalysis = false;
+ private File mPulledLibSaveFolder;
+
+ /**
+ * Constructor for GhidraScriptRunner. When using this constructor from the same class
+ * concurrently, make sure to append a unique suffix to the {@code callingClass} parameter to
+ * avoid conflicts.
+ *
+ * @param device The ITestDevice to pull the binary from.
+ * @param binaryName The name of the binary file present in device.
+ * @param binaryPath The path to the binary file in device .
+ * @param callingClass The name of the calling class.
+ * @param propertiesFileName The file name of properties file associated with post script. eg.
+ * function_offset_post_script.properties
+ * @param preScriptFileName The file name of pre script file.
+ * @param postScriptFileName The file name of post script file. eg.
+ * function_offset_post_script.java
+ */
+ public GhidraScriptRunner(
+ ITestDevice device,
+ String binaryName,
+ String binaryPath,
+ String callingClass,
+ String propertiesFileName,
+ String preScriptFileName,
+ String postScriptFileName,
+ String analyzeHeadlessPath) {
+ mDevice = device;
+ mBinaryName = binaryName;
+ mBinaryPath = binaryPath;
+ mCallingClass = callingClass;
+ mAnalyzeHeadlessPath = analyzeHeadlessPath;
+ mPropertiesFileName = Optional.ofNullable(propertiesFileName);
+ mPreScriptFileName = Optional.ofNullable(preScriptFileName);
+ mPostScriptFileName = Optional.ofNullable(postScriptFileName);
+ }
+
+ /**
+ * Set analysis flag during Ghidra script execution.
+ *
+ * @return This GhidraScriptRunner instance with analysis enabled.
+ */
+ public GhidraScriptRunner enableAnalysis() {
+ mEnableAnalysis = true;
+ return this;
+ }
+
+ /**
+ * Return ByteArrayOutputStream.
+ *
+ * @return mOutputOfGhidra.
+ */
+ public ByteArrayOutputStream getOutputStream() {
+ return mOutputOfGhidra;
+ }
+
+ /**
+ * Specify a post-script its properties to be executed after Ghidra analysis.
+ *
+ * @param contents The contents of the post-script.
+ * @param propertiesContent The map of key value pairs to write in properties file
+ * @return This GhidraScriptRunner instance with post-script enabled and configured.
+ */
+ public GhidraScriptRunner postScript(String contents, Map<String, String> propertiesContent) {
+ mPostScriptContent = Optional.ofNullable(contents);
+
+ if (!propertiesContent.isEmpty()) {
+ mPropertiesFileContentsMap = propertiesContent;
+ }
+ return this;
+ }
+
+ /**
+ * Specify a pre-script to be executed before Ghidra analysis.
+ *
+ * @param contents The contents of the pre-script.
+ * @return This GhidraScriptRunner instance with pre-script enabled and configured.
+ */
+ public GhidraScriptRunner preScript(String contents) {
+ mPreScriptContent = Optional.ofNullable(contents);
+ return this;
+ }
+
+ /**
+ * Run Ghidra with the specified options and scripts.
+ *
+ * @return an AutoCloseable for cleaning up temporary files after script execution.
+ */
+ public AutoCloseable run() throws Exception {
+ return runWithTimeout(POST_SCRIPT_TIMEOUT);
+ }
+
+ /**
+ * Run Ghidra with the specified options and scripts, with a timeout.
+ *
+ * @param timeout The timeout value in milliseconds.
+ * @return an AutoCloseable for cleaning up temporary files after script execution.
+ */
+ public AutoCloseable runWithTimeout(long timeout) throws Exception {
+ try {
+ // Get the language using readelf
+ String deviceSerial = mDevice.getSerialNumber().replace(":", "");
+ String pulledLibSaveFolderString = mCallingClass + "_" + deviceSerial + "_files";
+ String language = getLanguage(mDevice, mBinaryPath, mBinaryName);
+ mPulledLibSaveFolder =
+ createNamedTempDir(
+ Paths.get(mAnalyzeHeadlessPath).getParent().toFile(),
+ pulledLibSaveFolderString);
+
+ // Pull binary from the device to the folder
+ if (!mDevice.pullFile(
+ mBinaryPath + "/" + mBinaryName,
+ new File(mPulledLibSaveFolder + "/" + mBinaryName))) {
+ throw new Exception(
+ "Pulling " + mBinaryPath + "/" + mBinaryName + " was not successful");
+ }
+
+ // Create script related files and chmod rwx them
+ if (!mPropertiesFileContentsMap.isEmpty() && mPropertiesFileName.isPresent()) {
+ createPropertiesFile(
+ mPulledLibSaveFolder,
+ mPropertiesFileName.get(),
+ mPropertiesFileContentsMap);
+ }
+ if (mPreScriptContent.isPresent() && mPreScriptFileName.isPresent()) {
+ createScriptFile(
+ mPulledLibSaveFolder, mPreScriptFileName.get(), mPreScriptContent.get());
+ }
+ if (mPostScriptContent.isPresent() && mPostScriptFileName.isPresent()) {
+ createScriptFile(
+ mPulledLibSaveFolder, mPostScriptFileName.get(), mPostScriptContent.get());
+ }
+ if (!chmodRWXRecursively(mPulledLibSaveFolder)) {
+ throw new Exception("chmodRWX failed for " + mPulledLibSaveFolder.toString());
+ }
+
+ // Analyze the pulled binary using Ghidra headless analyzer
+ List<String> cmd =
+ createCommandList(
+ mAnalyzeHeadlessPath,
+ mCallingClass,
+ deviceSerial,
+ mPulledLibSaveFolder.getPath(),
+ mBinaryName,
+ mPreScriptFileName.isPresent() ? mPreScriptFileName.get() : "",
+ mPostScriptFileName.isPresent() ? mPostScriptFileName.get() : "",
+ language,
+ mEnableAnalysis);
+ mOutputOfGhidra = new ByteArrayOutputStream();
+ Process ghidraProcess = RunUtil.getDefault().runCmdInBackground(cmd, mOutputOfGhidra);
+ if (!ghidraProcess.isAlive()) {
+ throw new Exception("Ghidra process died. Output:" + mOutputOfGhidra);
+ }
+
+ if (mOutputOfGhidra.toString("UTF-8").contains("Enter path to JDK home directory")) {
+ throw new Exception(
+ "JDK 17+ (64-bit) not found in the system PATH. Please add it to your"
+ + " PATH environment variable.");
+ }
+ return () -> recursiveDelete(mPulledLibSaveFolder);
+ } catch (Exception e) {
+ recursiveDelete(mPulledLibSaveFolder);
+ throw e;
+ }
+ }
+
+ /**
+ * Creates a ghidra script file with the specified content in the given folder.
+ *
+ * @param folder The folder where the script file will be created.
+ * @param fileName The name of the script file.
+ * @param content The content to be written into the script file.
+ */
+ private static void createScriptFile(File folder, String fileName, String content)
+ throws Exception {
+ try (FileWriter fileWriter = new FileWriter(new File(folder, fileName))) {
+ fileWriter.write(content);
+ }
+ }
+
+ /**
+ * Creates a ghidra script properties file with the specified content in the given folder.
+ *
+ * @param folder The folder where the script file will be created.
+ * @param fileName The name of the script file.
+ * @param map The map of key value pairs to be written into the properties file.
+ */
+ private static void createPropertiesFile(File folder, String fileName, Map<String, String> map)
+ throws Exception {
+ File propertiesFile = new File(folder, fileName);
+ try (FileWriter fileWriter = new FileWriter(propertiesFile);
+ FileInputStream fileInputStream = new FileInputStream(propertiesFile)) {
+ propertiesFile.createNewFile();
+
+ Properties properties = new Properties();
+ if (propertiesFile.exists()) {
+ properties.load(fileInputStream);
+ } else {
+ throw new Exception("Unable to create ghidra script properties file");
+ }
+
+ // Populate properties file
+ map.forEach(properties::setProperty);
+ properties.store(fileWriter, "");
+ }
+ }
+
+ /**
+ * Uses the value under 'Machine' from the output of 'readelf -h' command on the specified
+ * binary file to determine the '-processor' parameter (language id) to be passed when invoking
+ * ghidra.
+ *
+ * <p>For e.g. readelf -h <binary_name>
+ *
+ * <p>ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
+ *
+ * <p>[...]
+ *
+ * <p>Machine: arm64 <--- ABI or processor type
+ *
+ * <p>Version: 0x1
+ *
+ * <p>[...]
+ *
+ * <p>The language id can be found in processor-specific .ldefs files located at:
+ * https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Processors/
+ * <proc_name>/data/languages/<proc_name>.ldefs
+ *
+ * <p>where 'proc_name' can be AARCH64, ARM, x86 which are the only ABIs that Android currently
+ * supports as per https://developer.android.com/ndk/guides/abis
+ *
+ * <p>For e.g. Following code snippet from AARCH64.ldefs shows the language 'id' of 'AARCH64'
+ * machine type ie 'arm64'.
+ *
+ * <p><language processor="AARCH64" endian="little" size="64" variant="v8A" version="1.6"
+ * slafile="AARCH64.sla" processorspec="AARCH64.pspec" manualindexfile="../manuals/AARCH64.idx"
+ * id="AARCH64:LE:64:v8A"> <--- 'id' denotes the language id.
+ *
+ * <p>TODO: Utilize ghidra pre script for setting language automatically.
+ *
+ * @param device The ITestDevice representing the testing device.
+ * @param binaryPath The path to the directory containing the binary file.
+ * @param binaryName The name of the binary file.
+ * @return The language of the binary in the format "ARCH:ENDIAN:BITS:VARIANT"
+ */
+ private static String getLanguage(ITestDevice device, String binaryPath, String binaryName)
+ throws Exception {
+ String language =
+ runAndCheck(
+ device,
+ "readelf -h " + binaryPath + "/" + binaryName + " | grep Machine")
+ .getStdout()
+ .trim()
+ .split(":\\s*")[1]
+ .trim();
+ switch (language) {
+ case "arm":
+ return "ARM:LE:32:v8";
+ case "arm64":
+ return "AARCH64:LE:64:v8A";
+ case "386":
+ return "x86:LE:32:default";
+ case "x86-64":
+ return "x86:LE:64:default";
+ case "riscv":
+ return "RISCV:LE:64:RV64GC";
+ default:
+ throw new Exception("Unsupported Machine: " + language);
+ }
+ }
+
+ /**
+ * Creates a list of command-line arguments for invoking Ghidra's analyzeHeadless tool.
+ *
+ * @param ghidraBinaryLocation The analyzerHeadless location.
+ * @param callingClass The name of the calling class.
+ * @param deviceSerialNumber The serial number of the target device.
+ * @param tempFileName The temporary folder name in which target binary is pulled.
+ * @param binaryName The name of the binary file to run analyzeHeadless on.
+ * @param preScriptFileName The file name of the pre script.
+ * @param postScriptFileName The file name of the post script.
+ * @param lang The processor language for analysis. eg ARM:LE:32:v8
+ * @param analysis Flag indicating whether analysis should be performed.
+ * @return A list of command-line arguments for Ghidra analyzeHeadless tool.
+ */
+ private static List<String> createCommandList(
+ String ghidraBinaryLocation,
+ String callingClass,
+ String deviceSerialNumber,
+ String tempFileName,
+ String binaryName,
+ String preScriptFileName,
+ String postScriptFileName,
+ String lang,
+ boolean analysis) {
+ boolean preScript = !preScriptFileName.isEmpty();
+ boolean postScript = !postScriptFileName.isEmpty();
+ return List.of(
+ ghidraBinaryLocation,
+ tempFileName,
+ callingClass + "_ghidra_project_" + deviceSerialNumber,
+ "-import",
+ tempFileName + "/" + binaryName,
+ "-scriptPath",
+ tempFileName,
+ postScript ? "-postScript" : "",
+ postScript ? postScriptFileName : "",
+ preScript ? "-preScript" : "",
+ preScript ? preScriptFileName : "",
+ "-processor",
+ lang,
+ analysis ? "" : "-noanalysis",
+ "-deleteProject");
+ }
+}
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/GhidraBusinessLogicHandler.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/GhidraBusinessLogicHandler.java
new file mode 100644
index 0000000..1b3e0c1
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/GhidraBusinessLogicHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import java.util.Optional;
+
+/** GCL-accessible Business Logic utility for GhidraPreparer */
+public class GhidraBusinessLogicHandler {
+ private static String gitReleaseTagName; // eg. "Ghidra_11.0.1_build"
+ private static String gitReleaseAssetName; // eg. "ghidra_11.0.1_PUBLIC_20240130.zip"
+
+ public static Optional<String> getGitReleaseTagName() {
+ return Optional.ofNullable(gitReleaseTagName);
+ }
+
+ public static Optional<String> getReleaseAssetName() {
+ return Optional.ofNullable(gitReleaseAssetName);
+ }
+
+ public void setGitReleaseTagName(String tagName, String assetName) {
+ gitReleaseTagName = tagName;
+ gitReleaseAssetName = assetName;
+ }
+}