From 3003881da1cc447a484fd257690f248da68bd953 Mon Sep 17 00:00:00 2001 From: Daniel Oliveira Date: Fri, 14 Apr 2023 19:31:58 +0100 Subject: [PATCH] feat(rops-checker): add rops_check script to measure rops Signed-off-by: Daniel Oliveira --- .github/workflows/templates/c.yaml | 14 +++ Makefile | 3 +- ci.mk | 27 +++++- rops_check.py | 148 +++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100755 rops_check.py diff --git a/.github/workflows/templates/c.yaml b/.github/workflows/templates/c.yaml index 56aa435..6ca41e6 100644 --- a/.github/workflows/templates/c.yaml +++ b/.github/workflows/templates/c.yaml @@ -56,3 +56,17 @@ jobs: with: submodules: recursive - run: make PLATFORM=${{ matrix.platform }} misra-check + + rops: + runs-on: ubuntu-latest + container: baoproject/bao:latest + strategy: + matrix: + platform: ["qemu-aarch64-virt"] + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: recursive + - run: git config --global --add safe.directory /__w/bao-partitioner/bao-partitioner + - run: make PLATFORM=${{ matrix.platform }} rops-check diff --git a/Makefile b/Makefile index 2498806..d51c13f 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,8 @@ include ci.mk python_scripts:= \ $(root_dir)/misra/deviation_suppression.py \ $(root_dir)/license_check.py \ - $(root_dir)/spell_check.py + $(root_dir)/spell_check.py \ + $(root_dir)/rops_check.py $(call ci, pylint, $(python_scripts)) yaml_files:= \ diff --git a/ci.mk b/ci.mk index bdfdf12..95b42a7 100644 --- a/ci.mk +++ b/ci.mk @@ -296,7 +296,7 @@ endef # Assembler Formatting # Provides three make targets: # make asmfmt-check # checks if the provided assembly files are formated correctly -# make asmfmt # formats the provided assembly files +# make asmfmt # formats the provided assembly files # @param space-separated list of assembly files # @example $(call ci, asmfmt, file1.S fil2.S file3.S) @@ -315,5 +315,30 @@ endef ############################################################################# +# ROPs Checking +# Checks if the current working branch as increased or decreased the number of +# ROPs in final binary file. The call to this rule should take into account +# that any env variable that is used in the build command should be passed +# (e.g., PLATFORM=) +# make rops-check ENVAR= +# @param build command of the repo +# @param path to the binary file +# @example $(call ci, rops, make PLATFORM=qemu-aarch64-virt, bin/qemu-aarch64-virt/partitioner.elf) + +rops_check_script:=$(ci_dir)/rops_check.py + +rops-check: + @$(rops_check_script) -b "$(build_cmd)" -x $(exe_path) + +.PHONY: rops-check +non_build_targets+=rops-check + +define rops +build_cmd:=$1 +exe_path:=$2 +endef + +############################################################################# + ci=$(eval $(call $1, $2, $3, $4, $5, $6, $7, $8, $9)) diff --git a/rops_check.py b/rops_check.py new file mode 100755 index 0000000..bfeb7b8 --- /dev/null +++ b/rops_check.py @@ -0,0 +1,148 @@ +#!/bin/python3 + +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Bao Project and Contributors. All rights reserved + +""" +This script compares the number of ROP (Return-Oriented Programming) gadgets in +two different builds from two branches instances of a Git repository. The script +calls the ROPGadget* tool to do the measurements and outputs the difference in +the number of ROP gadgets between the two provide branches. +* github.com/JonathanSalwan/ROPgadget +""" + +import subprocess +import argparse +import sys +import git + +# The percentage threshold for the number of ROP gadgets to be accepted. +PERCENTAGE_THRESHOLD = 10 + +DESCRIPTION = "This script compares the number of ROP (Return-Oriented \ + Programming) gadgets in two different builds from two branches instances\ + of a Git repository. The script calls the ROPGadget* tool to do \ + the measurements and outputs the difference in the number of ROP gadgets\ + between the two provide branches. We are assuming that the binary file \ + is passed in the elf format. \ + * github.com/JonathanSalwan/ROPgadget" + +def parse_args(): + """ + This function parses command-line arguments using the argparse module. It + sets up the argument parser with the specified options and returns the + parsed arguments. + """ + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument("-s", "--source-branch", required=False, + help="Name of source branch to compare against (default: current branch)") + parser.add_argument("-t", "--target-branch", required=False, + help="Name of target branch to compare (default: main)") + parser.add_argument("-b", "--build-cmd", required=True, help="Command to build the repo") + parser.add_argument("-x", "--exe-path", required=True, + help="Path to the executable to measure ROP gadgets in") + parser.add_argument("-p", "--pct", required=False, + help="Percentage threshold for the number of ROP gadgets to be accepted \ + (default: 10%%)") + + args = parser.parse_args() + return args + +def run_cmd(cmd): + """ + This function takes a command as input and executes it using the subprocess + module. It captures the output (stdout) and error (stderr) streams, as well + as the return code. + """ + # Run the command using the subprocess module and capture the output, error, and return code. + with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, + text=True) as process: + + # Wait for the command to complete and retrieve the output and error streams. + output, error = process.communicate() + + # Get the return code of the command. + return_code = process.returncode + + # Return the output, error, and return code as a tuple. + return output, error, return_code + +def measure_rop_gadgets(exe_path): + """ + This function takes a path to a binary as input and uses the ROPGadget tool + to measure the number of ROP gadgets in the binary. + """ + # Run the ROPGadget tool on the binary and capture the output and error streams. + cmd = f'ROPgadget --binary {exe_path}' + stdout, stderr, returncode = run_cmd(cmd) + + if returncode != 0: + raise RuntimeError(f'Error running ROP gadget tool: {stderr}') + + # Search for the "Unique gadgets found:" string + unique_gadgets_line = None + for line in stdout.split("\n"): + if "Unique gadgets found:" in line: + unique_gadgets_line = line + break + + # Extract the integer value after the "Unique gadgets found:" string + unique_gadgets = int(unique_gadgets_line.split(":")[-1].strip()) + + return unique_gadgets + +def main(pct_threshold): + """ + This function is the main entry point of the script. It parses command-line + arguments, measures the number of ROP gadgets in the two branches, and + prints the difference in the number of ROP gadgets between the two branches. + """ + args = parse_args() + + repo = git.Repo('.') # Assumes the current directory is the repository root + + if args.source_branch is None: + if repo.head.is_detached: + args.source_branch = repo.git.rev_parse("HEAD", short=True) + else: + args.source_branch = repo.active_branch.name + + if args.target_branch is None: + args.target_branch = 'main' + + # Checkout target branch, build the software, and measure ROP gadgets + repo.git.checkout(args.target_branch) + run_cmd(args.build_cmd) + target_gadgets = measure_rop_gadgets(args.exe_path) + + print(f'Number of ROP gadgets in target branch {args.target_branch}: {target_gadgets}') + + # Checkout source branch, build the software, and measure ROP gadgets + repo.git.checkout(args.source_branch) + run_cmd(args.build_cmd) + source_gadgets = measure_rop_gadgets(args.exe_path) + + print(f'Number of ROP gadgets in source branch {args.source_branch}: {source_gadgets}') + + # Calculate and print the difference in ROP gadgets + total = source_gadgets - target_gadgets + percentage = (total / target_gadgets) * 100 + + if total > 0: + print(f'ROP gadgets increased by {total} (+{percentage:.2f}%)') + elif total < 0: + print(f'ROP gadgets decreased by {total} ({percentage:.2f}%)') + else: + print('ROP gadgets did not change') + + if args.pct is not None: + pct_threshold = int(args.pct) + + if percentage >= pct_threshold: + print(f'ERROR: ROP gadgets increased by more than {pct_threshold}%', file=sys.stderr) + sys.exit(-1) + else: + sys.exit(0) + +if __name__ == '__main__': + main(PERCENTAGE_THRESHOLD)