From 8eaacac69aed8b4625652efbd8564ac8ae7e6f24 Mon Sep 17 00:00:00 2001 From: Cedric DOURLENT Date: Mon, 5 Feb 2024 13:52:50 +0100 Subject: [PATCH] build: add support for checksec.sh based mitigation reports Checksec is a bash script to check the properties of executables (like PIE, RELRO, Canaries, ASLR, Fortify Source). Add an optional build feature, which when enabled is going to create a JSON report about the state of mitigations in the produced binaries. Upstream-Status: Submitted [https://github.com/openwrt/openwrt/pull/14569] Signed-off-by: Cedric DOURLENT --- config/Config-build.in | 7 ++ include/image.mk | 4 + include/package-pack.mk | 13 +++ scripts/checksec-format-output.py | 34 ++++++++ scripts/checksec-mitigation-report.py | 109 ++++++++++++++++++++++++++ tools/Makefile | 1 + 6 files changed, 168 insertions(+) create mode 100755 scripts/checksec-format-output.py create mode 100755 scripts/checksec-mitigation-report.py diff --git a/config/Config-build.in b/config/Config-build.in index a9e968f944..f22d914d7a 100644 --- a/config/Config-build.in +++ b/config/Config-build.in @@ -34,6 +34,13 @@ menu "Global build settings" directory containing Software Bill Of Materials in CycloneDX format. + config JSON_CHECKSEC_MITIGATIONS_REPORT + bool + prompt "Create binaries hardening report (based on checksec.sh)" + help + Create a JSON file *.checksec.json in the build + directory containing the result of checksec. + config ALL_NONSHARED bool "Select all target specific packages by default" select ALL_KMODS diff --git a/include/image.mk b/include/image.mk index 6408719bdc..a42bec7977 100644 --- a/include/image.mk +++ b/include/image.mk @@ -320,6 +320,10 @@ ifneq ($(CONFIG_JSON_CYCLONEDX_SBOM),) $(if $(IB),$(TOPDIR)/.packageinfo, $(TMP_DIR)/.packageinfo) \ $(BIN_DIR)/$(IMG_PREFIX)$(if $(PROFILE_SANITIZED),-$(PROFILE_SANITIZED)).manifest > \ $(BIN_DIR)/$(IMG_PREFIX)$(if $(PROFILE_SANITIZED),-$(PROFILE_SANITIZED)).bom.cdx.json + $(if $(CONFIG_JSON_CHECKSEC_MITIGATIONS_REPORT), \ + $(PYTHON) $(SCRIPT_DIR)/checksec-mitigation-report.py \ + $(BUILD_DIR) \ + $(BIN_DIR)/$(IMG_PREFIX)$(if $(PROFILE_SANITIZED),-$(PROFILE_SANITIZED)).checksec.json) endif endef diff --git a/include/package-pack.mk b/include/package-pack.mk index 5f3ce296ea..77eda0be47 100644 --- a/include/package-pack.mk +++ b/include/package-pack.mk @@ -224,6 +224,19 @@ endif $(if $(PROVIDES),@for pkg in $(filter-out $(1),$(PROVIDES)); do cp $(PKG_INFO_DIR)/$(1).provides $(PKG_INFO_DIR)/$$$$pkg.provides; done) $(CheckDependencies) +# We need to run checksec.sh before stripping for working stack canary check. + ifneq ($$(CONFIG_JSON_CHECKSEC_MITIGATIONS_REPORT),) + ( \ + mkdir -p $(PKG_BUILD_DIR)/CHECKSEC/$(1)/ && \ + checksec --output=json --dir=$$(IDIR_$(1)) > $(PKG_BUILD_DIR)/CHECKSEC/$(1)/checksec_report.json && \ + $(PYTHON) $(SCRIPT_DIR)/checksec-format-output.py \ + $(PKG_BUILD_DIR)/CHECKSEC/$(1)/checksec_report.json \ + $$(IDIR_$(1)) \ + $(PKG_NAME) \ + $(if $(PKG_VERSION),$(PKG_VERSION),"N/A") \ + $(PKG_BUILD_DIR)/CHECKSEC/$(1)/checksec_report_formatted.json \ + ) + endif $(RSTRIP) $$(IDIR_$(1)) ifneq ($$(CONFIG_IPK_FILES_CHECKSUMS),) diff --git a/scripts/checksec-format-output.py b/scripts/checksec-format-output.py new file mode 100755 index 0000000000..98e75209bb --- /dev/null +++ b/scripts/checksec-format-output.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +from pathlib import Path +from sys import argv +import json + +if len(argv) != 6: + print("Usage: checkseck_format_output.py input build_dir package_name package_version output") + exit(1) + +input_path = Path(argv[1]) +builddir_path = argv[2] +name = argv[3] +version = argv[4] +output_path = Path(argv[5]) +output={} + +with open(input_path, "r") as read_file: + output["elflist"]={} + data = json.load(read_file) + del data['dir'] + for k in list(data): + new=k.replace(builddir_path, "") + output["elflist"][new]=data.pop(k) + output["elflist"][new]["rpath"]="no" + output["elflist"][new]["filename"] = output["elflist"][new]["filename"].replace(builddir_path, "") + + + + output['package'] = name + output['version'] = version + with open(output_path, "w") as write_file: + json.dump(output, write_file) + diff --git a/scripts/checksec-mitigation-report.py b/scripts/checksec-mitigation-report.py new file mode 100755 index 0000000000..519621996f --- /dev/null +++ b/scripts/checksec-mitigation-report.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +from pathlib import Path +from sys import argv +import json + +if len(argv) != 3: + print("Usage: checksec_mitigation_report.py build_dir output") + exit(1) + +builddir_path = argv[1] +output_path = Path(argv[2]) + +print(builddir_path) +print(output_path) + + +output = { +} + +with open(output_path, "w") as write_file: + output["packagelist"]=[] + + builddir = Path(builddir_path) + for report in list(builddir.rglob("checksec_report_formatted.json")): + with open(report, "r") as read_file: + data = json.load(read_file) + output["packagelist"].append(data) + json.dump(output, write_file) + +print("#############################################") +print("############# HARDENING RESULTS #############") +print("#############################################") + +# elf list +number_of_elf=0 +for package in output["packagelist"]: + number_of_elf += len(package["elflist"]) + +print("NB_ELF = " + str(number_of_elf)) + +#relro result +number_of_relro=0 +relro_positive_result=["n/a", "full", "partial"] +for package in output["packagelist"]: + for k in list(package["elflist"]): + elf=package["elflist"][k] + if elf["relro"] in relro_positive_result: + number_of_relro+=1 + +# now result +number_of_now=0 +now_positive_result=["n/a", "full"] +for package in output["packagelist"]: + for k in list(package["elflist"]): + elf=package["elflist"][k] + if elf["relro"] in now_positive_result : + number_of_now+=1; + +# stack protector result +number_of_ssp=0 +for package in output["packagelist"]: + for k in list(package["elflist"]): + elf=package["elflist"][k] + if elf["canary"] == "yes": + number_of_ssp+=1; + +# nx result +number_of_nx=0 +for package in output["packagelist"]: + for k in list(package["elflist"]): + elf=package["elflist"][k] + if elf["nx"] == "yes": + number_of_nx+=1; + +# PIE result +number_of_pie=0 +pie_positive_result=["yes", "dso"] +for package in output["packagelist"]: + for k in list(package["elflist"]): + elf=package["elflist"][k] + if elf["pie"] in pie_positive_result: + number_of_pie+=1; + +# rpath +number_of_rpath=0 +for package in output["packagelist"]: + for k in list(package["elflist"]): + elf=package["elflist"][k] + if elf["rpath"] == "yes": + number_of_rpath+=1; + +# fortify +number_of_fortify=0 +for package in output["packagelist"]: + for k in list(package["elflist"]): + elf=package["elflist"][k] + if elf["fortify_source"] != "no" : + number_of_fortify+=1; + + +print('RELRO = {:2.2%}'.format(number_of_relro / number_of_elf)) +print('NOW = {:2.2%}'.format(number_of_now / number_of_elf)) +print('NX = {:2.2%}'.format(number_of_nx / number_of_elf)) +print('PIE = {:2.2%}'.format(number_of_pie / number_of_elf)) +print('SSP = {:2.2%}'.format(number_of_ssp / number_of_elf)) +print('FORTIFY = {:2.2%}'.format(number_of_fortify / number_of_elf)) +print('RPATH = {:2.2%}'.format(number_of_rpath / number_of_elf)) + diff --git a/tools/Makefile b/tools/Makefile index 563b901892..99d5789c39 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -70,6 +70,7 @@ tools-y += util-linux tools-y += xz tools-y += zip tools-y += zlib +tools-$(if $(CONFIG_JSON_CHECKSEC_MITIGATIONS_REPORT),y) += checksec.sh tools-$(if $(CONFIG_BUILD_ALL_HOST_TOOLS),y) += liblzo tools-$(if $(CONFIG_BUILD_ALL_HOST_TOOLS)$(BUILD_B43_TOOLS),y) += b43-tools tools-$(if $(CONFIG_BUILD_ALL_HOST_TOOLS)$(BUILD_BZIP2_TOOLS),y) += bzip2