diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b0eb43..7f4554a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,4 +64,5 @@ jobs: - name: Test ${{ matrix.build-type }} with sanitizers set ${{ matrix.sanitizers }} run: | cmake --preset ci + cmake --build ./builds/ci/ --target decompile_headless_docker ctest --preset ci --build-config ${{ matrix.build-type }} diff --git a/scripts/ghidra/.dockerignore b/scripts/ghidra/.dockerignore deleted file mode 100644 index 5511b20..0000000 --- a/scripts/ghidra/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -.ghidra -ghidra_scripts diff --git a/scripts/ghidra/PatchestryScript.java b/scripts/ghidra/PatchestryScript.java index 936a498..5f9f889 100644 --- a/scripts/ghidra/PatchestryScript.java +++ b/scripts/ghidra/PatchestryScript.java @@ -114,7 +114,7 @@ private void serializeToFile(Path file, Function function) throws Exception { println("Serializing function: " + function.getName() + " @ " + function.getEntryPoint()); serializer.serialize(function).close(); } - + private void runHeadless() throws Exception { final var args = getScriptArgs(); if (args.length != 2) { @@ -123,10 +123,10 @@ private void runHeadless() throws Exception { println("\tOUTPUT_FILE"); return; } - + final var functionName = args[0]; final var outputPath = args[1]; - + final var functions = getGlobalFunctions(functionName); if (functions.isEmpty()) { println("Function not found: " + functionName); @@ -144,12 +144,12 @@ private void runGUI() throws Exception { final var curFunction = getFunctionContaining(currentAddress); final var functionName = curFunction.getName(); final var jsonPath = Files.createTempFile(functionName + '.', ".patchestry.json"); - + serializeToFile(jsonPath, curFunction); final var mlirPath = Files.createTempFile(functionName + '.', ".patchestry.out"); final var binaryPath = "patchestry"; - + final var cmd = new ArrayList(); cmd.add(binaryPath); cmd.add(jsonPath.toString()); diff --git a/scripts/ghidra/decompile.sh b/scripts/ghidra/decompile-entrypoint.sh similarity index 70% rename from scripts/ghidra/decompile.sh rename to scripts/ghidra/decompile-entrypoint.sh index e40c5d3..7bf13a7 100644 --- a/scripts/ghidra/decompile.sh +++ b/scripts/ghidra/decompile-entrypoint.sh @@ -7,8 +7,6 @@ # the LICENSE file found in the root directory of this source tree. # -set -x - if [ "$#" -lt 3 ]; then echo "Usage: $0 " exit 1 @@ -18,14 +16,6 @@ INPUT_PATH=$1 FUNCTION_NAME=$2 OUTPUT_PATH=$3 -# Running with non-root user may cause permission issue on ubuntu -# because binded directory will have root permission. -# This is a hacky fix to avoid the issue. It can be avoided -# by switching to using docker volume. -if [ ! -w ${OUTPUT_PATH} ]; then - sudo chown ${USER}:${USER} ${OUTPUT_PATH} -fi - # Create a new Ghidra project and import the file ${GHIDRA_HEADLESS} ${GHIDRA_PROJECTS} patchestry-decompilation \ -readOnly -deleteProject \ @@ -34,7 +24,6 @@ ${GHIDRA_HEADLESS} ${GHIDRA_PROJECTS} patchestry-decompilation \ $FUNCTION_NAME \ $OUTPUT_PATH - # Check if the decompile script was successful if [ $? -ne 0 ]; then echo "Decompilation failed" diff --git a/scripts/ghidra/decompile-headless.dockerfile b/scripts/ghidra/decompile-headless.dockerfile index aa40490..e1b80fb 100644 --- a/scripts/ghidra/decompile-headless.dockerfile +++ b/scripts/ghidra/decompile-headless.dockerfile @@ -2,14 +2,12 @@ FROM eclipse-temurin:17 AS base FROM base AS build -# Set environment variables for Ghidra ENV GHIDRA_VERSION=11.1.2 ENV GHIDRA_RELEASE_TAG=20240709 ENV GHIDRA_PACKAGE=ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_RELEASE_TAG} ENV GHIDRA_SHA256=219ec130b901645779948feeb7cc86f131dd2da6c36284cf538c3a7f3d44b588 ENV GHIDRA_REPOSITORY=https://github.com/NationalSecurityAgency/ghidra -# Update and install necessary packages RUN apt-get update && apt-get install -y \ wget \ ca-certificates \ @@ -17,7 +15,6 @@ RUN apt-get update && apt-get install -y \ --no-install-recommends && \ apt-get clean && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives -# Download and verify Ghidra package RUN wget --progress=bar:force -O /tmp/ghidra.zip ${GHIDRA_REPOSITORY}/releases/download/Ghidra_${GHIDRA_VERSION}_build/${GHIDRA_PACKAGE}.zip && \ echo "${GHIDRA_SHA256} /tmp/ghidra.zip" | sha256sum -c - @@ -27,7 +24,6 @@ RUN unzip /tmp/ghidra.zip -d /tmp && \ chmod +x /ghidra/ghidraRun && \ rm -rf /var/tmp/* /tmp/* /ghidra/docs /ghidra/Extensions/Eclipse /ghidra/licenses -# Clean up RUN apt-get purge -y --auto-remove wget ca-certificates unzip && \ apt-get clean @@ -46,36 +42,20 @@ RUN adduser --shell /sbin/nologin --disabled-login --gecos "" user && \ adduser user sudo && \ echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers -# Switch to the newly created user USER user -# Set working directory for the user WORKDIR /home/user/ -# Copy Ghidra from the build stage -COPY --from=build /ghidra ghidra - -# Create projects and scripts directory -RUN mkdir ghidra_projects ghidra_scripts +RUN mkdir -p /home/user/ghidra_projects /home/user/ghidra_scripts -# Copy the Java and script files into the appropriate directories +COPY --from=build /ghidra ghidra COPY PatchestryScript.java ghidra_scripts/ -COPY decompile.sh decompile.sh - -# Make the decompile script executable -USER root -RUN chmod +x decompile.sh -USER user - -# Copy the .dockerignore file (if necessary) -COPY .dockerignore .dockerignore +COPY --chown=user:user --chmod=755 decompile-entrypoint.sh . -# Set environment variable for Ghidra home directory ENV GHIDRA_HOME=/home/user/ghidra ENV GHIDRA_SCRIPTS=/home/user/ghidra_scripts ENV GHIDRA_PROJECTS=/home/user/ghidra_projects ENV GHIDRA_HEADLESS=${GHIDRA_HOME}/support/analyzeHeadless ENV USER=user -# Set the entrypoint -ENTRYPOINT ["/home/user/decompile.sh"] +ENTRYPOINT ["/home/user/decompile-entrypoint.sh"] diff --git a/scripts/ghidra/decompile-headless.sh b/scripts/ghidra/decompile-headless.sh index 6f06e6d..1561ef1 100755 --- a/scripts/ghidra/decompile-headless.sh +++ b/scripts/ghidra/decompile-headless.sh @@ -6,26 +6,125 @@ # This source code is licensed in accordance with the terms specified in # the LICENSE file found in the root directory of this source tree. # -SCRIPTS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -if [ "$#" -lt 3 ]; then - echo "Usage: $0 " +show_help() { + echo "Usage: $0 [OPTIONS]" + echo + echo "Options:" + echo " -h, --help Show this help message and exit" + echo " -i, --input Path to the input file" + echo " -f, --function Name of the function to decompile" + echo " -o, --output Path to the output file where results will be saved" + echo " -v, --verbose Enable verbose output" + echo " -t, --interactive Start Docker container in interactive mode" + echo +} + +INPUT_PATH="" +FUNCTION_NAME="" +OUTPUT_PATH="" +VERBOSE=false +INTERACTIVE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + -i|--input) + INPUT_PATH="$2" + shift 2 + ;; + -f|--function) + FUNCTION_NAME="$2" + shift 2 + ;; + -o|--output) + OUTPUT_PATH="$2" + shift 2 + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -t|--interactive) + INTERACTIVE=true + shift + ;; + *) + echo "Unknown option: $1" + show_help + exit 1 + ;; + esac +done + +if [ -z "$INPUT_PATH" ]; then + echo "Error: Missing required option: -i, --input " + exit 1 +fi + +if [ -z "$FUNCTION_NAME" ]; then + echo "Error: Missing required option: -f, --function " exit 1 fi -INPUT_PATH=$1 -FUNCTION_NAME=$2 -OUTPUT_PATH=$3 +if [ -z "$OUTPUT_PATH" ]; then + echo "Error: Missing required option: -o, --output " + exit 1 +fi + +if [ "$VERBOSE" = true ]; then + echo "Input file: $INPUT_PATH" + echo "Function name: $FUNCTION_NAME" + echo "Output file: $OUTPUT_PATH" +fi + +if [ ! -e "$OUTPUT_PATH" ]; then + if [ "$VERBOSE" = true ]; then + echo "Creating output file: $OUTPUT_PATH" + fi + touch "$OUTPUT_PATH" +fi + +if [ "$VERBOSE" = true ]; then + echo "Running Docker container..." +fi + +absolute_path() { + if [[ "$1" = /* ]]; then + echo "$1" + else + echo "$(pwd)/$1" + fi +} + +INPUT_ABS_PATH=$(absolute_path "$INPUT_PATH") +OUTPUT_ABS_PATH=$(absolute_path "$OUTPUT_PATH") + +RUN="docker run --rm \ + -v \"$INPUT_ABS_PATH:/input.o\" \ + -v \"$OUTPUT_ABS_PATH:/output.json\" \ + trailofbits/patchestry-decompilation:latest" + +if file "$INPUT_PATH" | grep -q "Mach-O"; then + FUNCTION_NAME="_$FUNCTION_NAME" +fi + +if [ "$INTERACTIVE" = true ]; then + RUN=$(echo "$RUN" | sed 's/docker run --rm/docker run -it --rm --entrypoint \/bin\/bash/') +else + RUN="$RUN /input.o \"$FUNCTION_NAME\" /output.json" +fi + +if [ "$VERBOSE" = true ]; then + echo "Running Docker container with the following command:" + echo "$RUN" +fi -# Make sure $OUTPUT_PATH exists and is empty so that it can be -# mounted to the container -if [ ! -f $OUTPUT_PATH ]; then - touch $OUTPUT_PATH +if [ "$VERBOSE" = true ]; then + echo "Starting Docker container..." fi -truncate -s 0 $OUTPUT_PATH -docker run --rm \ - -v $INPUT_PATH:/input \ - -v $OUTPUT_PATH:/output \ - trailofbits/patchestry-decompilation:latest \ - /input $FUNCTION_NAME /output +eval "$RUN"