diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 4f3388153..4a4a8ee28 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -22,9 +22,12 @@ jobs: - uses: actions/checkout@v3 - name: Build and run docker image for cross compiling run: | - cd test/artificial_samples + pushd test/artificial_samples docker build -t cross_compiling . docker run --rm -v $(pwd)/build:/home/cwe/artificial_samples/build cross_compiling sudo python3 -m SCons + popd + pushd test/lkm_samples + ./build.sh - uses: actions/setup-java@v1 with: java-version: "17.0.x" @@ -56,4 +59,4 @@ jobs: - name: Build the docker image run: docker build -t cwe_checker . - name: Check functionality of the image - run: docker run --rm cwe_checker /bin/echo | grep -q CWE676 \ No newline at end of file + run: docker run --rm cwe_checker /bin/echo | grep -q CWE676 diff --git a/Makefile b/Makefile index dfa58157c..8d4e23826 100644 --- a/Makefile +++ b/Makefile @@ -11,12 +11,19 @@ test: echo "Acceptance test binaries not found. Please see test/artificial_samples/Readme.md for build instructions."; \ exit -1; \ fi + if [ ! -d "test/lkm_samples/build" ]; then \ + echo "Acceptance test LKMs not found. Please see test/lkm_samples/Readme.md for build instructions."; \ + exit -1; \ + fi cargo test --no-fail-fast -p acceptance_tests_ghidra -- --show-output --ignored --test-threads 1 compile_test_files: - cd test/artificial_samples \ + pushd test/artificial_samples \ && docker build -t cross_compiling . \ - && docker run --rm -v $(pwd)/build:/home/cwe/artificial_samples/build cross_compiling sudo /home/cwe/.local/bin/scons + && docker run --rm -v $(pwd)/build:/home/cwe/artificial_samples/build cross_compiling sudo /home/cwe/.local/bin/scons \ + && popd \ + && pushd test/lkm_samples \ + && ./build.sh codestyle-check: cargo fmt -- --check diff --git a/README.md b/README.md index 288ea4853..1d3be40eb 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,11 @@ You can adjust the behavior of most checks via a configuration file located at ` If you modify it, add the command line flag `--config=src/config.json` to tell the *cwe_checker* to use the modified file. For information about other available command line flags you can pass the `--help` flag to the *cwe_checker*. +There is _experimental_ support for the analysis of Linux loadable kernel modules +(LKMs). *cwe_checker* will recognize if you pass an LKM and will execute a +subset of the CWE checks available for user-space programs. Analyses are +configurable via a separate [configuration file](src/lkm_config.json). + If you use the stable version, you can also look at the [online documentation](https://fkie-cad.github.io/cwe_checker/index.html) for more information. ### For Bare-Metal Binaries ### diff --git a/src/caller/src/main.rs b/src/caller/src/main.rs index b52bbf084..b1bed5d0b 100644 --- a/src/caller/src/main.rs +++ b/src/caller/src/main.rs @@ -100,14 +100,6 @@ fn run_with_ghidra(args: &CmdlineArgs) -> Result<(), Error> { return Ok(()); } - // Get the configuration file - let config: serde_json::Value = if let Some(ref config_path) = args.config { - let file = std::io::BufReader::new(std::fs::File::open(config_path).unwrap()); - serde_json::from_reader(file).context("Parsing of the configuration file failed")? - } else { - read_config_file("config.json")? - }; - // Get the bare metal configuration file if it is provided let bare_metal_config_opt: Option = args.bare_metal_config.as_ref().map(|config_path| { @@ -116,18 +108,31 @@ fn run_with_ghidra(args: &CmdlineArgs) -> Result<(), Error> { .expect("Parsing of the bare metal configuration file failed") }); - // Filter the modules to be executed if the `--partial` parameter is set. + let binary_file_path = PathBuf::from(args.binary.clone().unwrap()); + + let (binary, project, mut all_logs) = + disassemble_binary(&binary_file_path, bare_metal_config_opt, args.verbose)?; + + // Filter the modules to be executed. if let Some(ref partial_module_list) = args.partial { filter_modules_for_partial_run(&mut modules, partial_module_list); + } else if project.runtime_memory_image.is_lkm { + modules.retain(|module| cwe_checker_lib::checkers::MODULES_LKM.contains(&module.name)); } else { // TODO: CWE78 is disabled on a standard run for now, // because it uses up huge amounts of RAM and computation time on some binaries. modules.retain(|module| module.name != "CWE78"); } - let binary_file_path = PathBuf::from(args.binary.clone().unwrap()); - let (binary, project, mut all_logs) = - disassemble_binary(&binary_file_path, bare_metal_config_opt, args.verbose)?; + // Get the configuration file. + let config: serde_json::Value = if let Some(ref config_path) = args.config { + let file = std::io::BufReader::new(std::fs::File::open(config_path).unwrap()); + serde_json::from_reader(file).context("Parsing of the configuration file failed")? + } else if project.runtime_memory_image.is_lkm { + read_config_file("lkm_config.json")? + } else { + read_config_file("config.json")? + }; // Generate the control flow graph of the program let (control_flow_graph, mut logs_graph) = graph::get_program_cfg_with_logs(&project.program); diff --git a/src/cwe_checker_lib/src/checkers.rs b/src/cwe_checker_lib/src/checkers.rs index 6950ec5f3..b1ae64374 100644 --- a/src/cwe_checker_lib/src/checkers.rs +++ b/src/cwe_checker_lib/src/checkers.rs @@ -5,6 +5,11 @@ //! but directly incorporated into the [`pointer_inference`](crate::analysis::pointer_inference) module. //! See there for detailed information about this check. +/// Checkers that are supported for Linux kernel modules. +pub const MODULES_LKM: [&str; 9] = [ + "CWE134", "CWE190", "CWE215", "CWE416", "CWE457", "CWE467", "CWE476", "CWE676", "CWE789", +]; + pub mod cwe_119; pub mod cwe_134; pub mod cwe_190; diff --git a/src/cwe_checker_lib/src/intermediate_representation/runtime_memory_image.rs b/src/cwe_checker_lib/src/intermediate_representation/runtime_memory_image.rs index 1a574676d..100c028cb 100644 --- a/src/cwe_checker_lib/src/intermediate_representation/runtime_memory_image.rs +++ b/src/cwe_checker_lib/src/intermediate_representation/runtime_memory_image.rs @@ -9,6 +9,8 @@ pub struct RuntimeMemoryImage { pub memory_segments: Vec, /// Endianness pub is_little_endian: bool, + /// True iff we are analyzing a Linux loadable kernel module. + pub is_lkm: bool, } impl RuntimeMemoryImage { @@ -18,6 +20,7 @@ impl RuntimeMemoryImage { RuntimeMemoryImage { memory_segments: Vec::new(), is_little_endian, + is_lkm: false, } } @@ -25,24 +28,14 @@ impl RuntimeMemoryImage { /// /// The function can parse ELF and PE files as input. pub fn new(binary: &[u8]) -> Result { - let parsed_object = Object::parse(binary)?; - - match parsed_object { - Object::Elf(elf_file) => { - let mut memory_segments = Vec::new(); - for header in elf_file.program_headers.iter() { - if header.p_type == elf::program_header::PT_LOAD { - memory_segments.push(MemorySegment::from_elf_segment(binary, header)); - } + match Object::parse(binary)? { + Object::Elf(elf_file) => match elf_file.header.e_type { + elf::header::ET_REL => Self::from_elf_sections(binary, elf_file), + elf::header::ET_DYN | elf::header::ET_EXEC => { + Self::from_elf_segments(binary, elf_file) } - if memory_segments.is_empty() { - return Err(anyhow!("No loadable segments found")); - } - Ok(RuntimeMemoryImage { - memory_segments, - is_little_endian: elf_file.header.endianness().unwrap().is_little(), - }) - } + ty => Err(anyhow!("Unsupported ELF type: e_type {}", ty)), + }, Object::PE(pe_file) => { let mut memory_segments = Vec::new(); for header in pe_file.sections.iter() { @@ -57,6 +50,7 @@ impl RuntimeMemoryImage { let mut memory_image = RuntimeMemoryImage { memory_segments, is_little_endian: true, + is_lkm: false, }; memory_image.add_global_memory_offset(pe_file.image_base as u64); Ok(memory_image) @@ -65,6 +59,65 @@ impl RuntimeMemoryImage { } } + /// Generate a runtime memory image for an executable ELF file or shared object. + fn from_elf_segments(binary: &[u8], elf_file: elf::Elf) -> Result { + let mut memory_segments = Vec::new(); + + for header in elf_file.program_headers.iter() { + if header.p_type == elf::program_header::PT_LOAD { + memory_segments.push(MemorySegment::from_elf_segment(binary, header)); + } + } + + if memory_segments.is_empty() { + return Err(anyhow!("No loadable segments found")); + } + + Ok(Self { + memory_segments, + is_little_endian: elf_file.header.endianness().unwrap().is_little(), + is_lkm: false, + }) + } + + /// Generate a runtime memory image for a relocatable object file. + /// + /// These files do not contain information about the expected memory layout. + /// Ghidra implements a basic loader that essentially concatenates all + /// `SHF_ALLOC` sections that are not `SHT_NULL`. They are placed in memory + /// as close as possible while respecting their alignment, starting at a + /// fixed address. We start mapping at zero and shift by the actual base + /// address that Ghidra has chosen after running our plugin. + /// + /// NOTE: It is important that this implementation stays in sync with what + /// `processSectionHeaders` in [`ElfProgramBuilder`] does in the cases that + /// we care about. + /// + /// [`ElfProgramBuilder`]: https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/ElfProgramBuilder.java + fn from_elf_sections(binary: &[u8], elf_file: elf::Elf) -> Result { + let mut next_base = 0; + + Ok(Self { + memory_segments: elf_file + .section_headers + .iter() + .filter_map(|section_header| { + if is_loaded(section_header) { + let mem_seg = + MemorySegment::from_elf_section(binary, next_base, section_header); + next_base = mem_seg.base_address + mem_seg.bytes.len() as u64; + Some(mem_seg) + } else { + None + } + }) + .collect(), + is_little_endian: elf_file.header.endianness().unwrap().is_little(), + is_lkm: get_section(".modinfo", &elf_file).is_some() + && get_section(".gnu.linkonce.this_module", &elf_file).is_some(), + }) + } + /// Generate a runtime memory image for a bare metal binary. /// /// The generated runtime memory image contains: @@ -105,9 +158,39 @@ impl RuntimeMemoryImage { MemorySegment::new_bare_metal_ram_segment(ram_base_address, ram_size), ], is_little_endian, + is_lkm: false, }) } + /// Get the base address for the image of a binary when loaded into memory. + pub fn get_base_address(binary: &[u8]) -> Result { + match Object::parse(binary)? { + Object::Elf(elf_file) => match elf_file.header.e_type { + elf::header::ET_REL => Ok(0), + elf::header::ET_DYN | elf::header::ET_EXEC => { + elf_file + .program_headers + .iter() + .find_map(|header| { + let vm_range = header.vm_range(); + if !vm_range.is_empty() + && header.p_type == goblin::elf::program_header::PT_LOAD + { + // The loadable segments have to occur in order in the program header table. + // So the start address of the first loadable segment is the base offset of the binary. + Some(vm_range.start as u64) + } else { + None + } + }) + .context("No loadable segment bounds found.") + } + ty => Err(anyhow!("Unsupported ELF type: e_type {}", ty)), + }, + _ => Err(anyhow!("Binary type not yet supported")), + } + } + /// Return whether values in the memory image should be interpreted in little-endian /// or big-endian byte order. pub fn is_little_endian_byte_order(&self) -> bool { @@ -278,6 +361,23 @@ impl RuntimeMemoryImage { } } +/// Returns the section header of the first section with this name. +fn get_section<'a>(name: &str, elf_file: &'a elf::Elf<'a>) -> Option<&'a elf::SectionHeader> { + let sh_strtab = &elf_file.shdr_strtab; + + elf_file.section_headers.iter().find(|section_header| { + matches!(sh_strtab.get_at(section_header.sh_name), Some(sh_name) if sh_name == name) + }) +} + +/// Returns true iff the section header will be loaded into memory by Ghidra. +#[inline] +fn is_loaded(section_header: &elf::SectionHeader) -> bool { + section_header.is_alloc() + && section_header.sh_type != elf::section_header::SHT_NULL + && section_header.sh_size != 0 +} + #[cfg(test)] mod tests { use crate::{bitvec, intermediate_representation::*}; diff --git a/src/cwe_checker_lib/src/intermediate_representation/term/builder_high_lvl.rs b/src/cwe_checker_lib/src/intermediate_representation/term/builder_high_lvl.rs index f9cc918bc..451fd81ac 100644 --- a/src/cwe_checker_lib/src/intermediate_representation/term/builder_high_lvl.rs +++ b/src/cwe_checker_lib/src/intermediate_representation/term/builder_high_lvl.rs @@ -87,6 +87,7 @@ impl RuntimeMemoryImage { }, ], is_little_endian: true, + is_lkm: false, } } } diff --git a/src/cwe_checker_lib/src/lib.rs b/src/cwe_checker_lib/src/lib.rs index ca517320e..c36745885 100644 --- a/src/cwe_checker_lib/src/lib.rs +++ b/src/cwe_checker_lib/src/lib.rs @@ -30,6 +30,11 @@ through the `--config` command line option. Start by taking a look at the standard configuration file located at `src/config.json` and read the [check-specific documentation](crate::checkers) for more details about each field in the configuration file. +There is _experimental_ support for the analysis of Linux loadable kernel modules +(LKMs). *cwe_checker* will recognize if you pass an LKM and will execute a +subset of the CWE checks available for user-space programs. Analyses are +configurable via a separate configuration file at `src/lkm_config.json`. + ## For bare-metal binaries The cwe_checker offers experimental support for analyzing bare-metal binaries. diff --git a/src/cwe_checker_lib/src/utils/binary.rs b/src/cwe_checker_lib/src/utils/binary.rs index fbea74697..4bf80fa58 100644 --- a/src/cwe_checker_lib/src/utils/binary.rs +++ b/src/cwe_checker_lib/src/utils/binary.rs @@ -70,6 +70,30 @@ pub struct MemorySegment { } impl MemorySegment { + /// Generate a segment from a section header of a relocatable ELF object + /// file. + pub fn from_elf_section( + binary: &[u8], + base_address: u64, + section_header: &elf::SectionHeader, + ) -> Self { + let bytes: Vec = match section_header.file_range() { + Some(range) => binary[range].to_vec(), + // `SHT_NOBITS` + None => vec![0; section_header.sh_size as usize], + }; + let alignment = section_header.sh_addralign.next_power_of_two(); + Self { + bytes, + base_address: base_address.next_multiple_of(alignment), + // ELF format specification does not allow for Declaration of + // sections as non-readable. + read_flag: true, + write_flag: section_header.is_writable(), + execute_flag: section_header.is_executable(), + } + } + /// Generate a segment from a program header of an ELF file. pub fn from_elf_segment(binary: &[u8], program_header: &elf::ProgramHeader) -> MemorySegment { let mut bytes: Vec = binary[program_header.file_range()].to_vec(); diff --git a/src/cwe_checker_lib/src/utils/ghidra.rs b/src/cwe_checker_lib/src/utils/ghidra.rs index ed88e68db..bfc868bfb 100644 --- a/src/cwe_checker_lib/src/utils/ghidra.rs +++ b/src/cwe_checker_lib/src/utils/ghidra.rs @@ -3,7 +3,10 @@ use crate::prelude::*; use crate::utils::binary::BareMetalConfig; use crate::utils::{get_ghidra_plugin_path, read_config_file}; -use crate::{intermediate_representation::Project, utils::log::LogMessage}; +use crate::{ + intermediate_representation::{Project, RuntimeMemoryImage}, + utils::log::LogMessage, +}; use directories::ProjectDirs; use nix::{sys::stat, unistd}; use std::path::{Path, PathBuf}; @@ -54,7 +57,7 @@ fn parse_pcode_project_to_ir_project( .as_ref() .map(|config| config.parse_binary_base_address()); let mut log_messages = pcode_project.normalize(); - let project: Project = match crate::utils::get_binary_base_address(binary) { + let project: Project = match RuntimeMemoryImage::get_base_address(binary) { Ok(binary_base_address) => pcode_project.into_ir_project(binary_base_address), Err(_err) => { if let Some(binary_base_address) = bare_metal_base_address_opt { diff --git a/src/cwe_checker_lib/src/utils/mod.rs b/src/cwe_checker_lib/src/utils/mod.rs index 881028d7f..38fccc043 100644 --- a/src/cwe_checker_lib/src/utils/mod.rs +++ b/src/cwe_checker_lib/src/utils/mod.rs @@ -27,22 +27,3 @@ pub fn get_ghidra_plugin_path(plugin_name: &str) -> std::path::PathBuf { let data_dir = project_dirs.data_dir(); data_dir.join("ghidra").join(plugin_name) } - -/// Get the base address for the image of a binary when loaded into memory. -pub fn get_binary_base_address(binary: &[u8]) -> Result { - use goblin::Object; - match Object::parse(binary)? { - Object::Elf(elf_file) => { - for header in elf_file.program_headers.iter() { - let vm_range = header.vm_range(); - if !vm_range.is_empty() && header.p_type == goblin::elf::program_header::PT_LOAD { - // The loadable segments have to occur in order in the program header table. - // So the start address of the first loadable segment is the base offset of the binary. - return Ok(vm_range.start as u64); - } - } - Err(anyhow!("No loadable segment bounds found.")) - } - _ => Err(anyhow!("Binary type not yet supported")), - } -} diff --git a/src/installer/src/main.rs b/src/installer/src/main.rs index cb50b4fa1..2833699e8 100644 --- a/src/installer/src/main.rs +++ b/src/installer/src/main.rs @@ -30,13 +30,17 @@ struct GhidraConfig { ghidra_path: PathBuf, } -/// Copies src/config.json to specified location +/// Copies the configuration files to the specified location. fn copy_config_json(location: &Path) -> Result<()> { let repo_dir = env::current_dir().unwrap(); std::fs::copy( repo_dir.join("src/config.json"), location.join("config.json"), )?; + std::fs::copy( + repo_dir.join("src/lkm_config.json"), + location.join("lkm_config.json"), + )?; Ok(()) } diff --git a/src/lkm_config.json b/src/lkm_config.json new file mode 100644 index 000000000..a223c864b --- /dev/null +++ b/src/lkm_config.json @@ -0,0 +1,132 @@ +{ + "_comment": "This file is loaded instead of config.json when analyzing an LKM. The analysis of LKMs requires a different set of options compared to the analysis of user-space programs.", + "CWE134": { + "_comment": "Functions that take format string arguments.", + "format_string_symbols": [], + "format_string_index": {} + }, + "CWE190": { + "symbols": [] + }, + "CWE215": { + "symbols": [] + }, + "CWE416": { + "_comment": "Functions that invalidate the pointer passed as the first argument.", + "deallocation_symbols": [], + "always_include_full_path_to_free_site": true + }, + "CWE457": { + "symbols": [] + }, + "CWE467": { + "_comment": "Any function that takes something of type `size_t` could be a possible candidate.", + "symbols": [ + "bcmp", + "memchr", + "memcmp", + "memcpy", + "memmove", + "memscan", + "memset", + "memset16", + "memset32", + "memset64", + "strlcat", + "strlcpy", + "strncasecmp", + "strncat", + "strnchr", + "strnchrnul", + "strncmp", + "strncpy", + "strnlen", + "strnstr", + "strscpy" + ] + }, + "CWE476": { + "_comment": "Any function that possibly returns a NULL value.", + "parameters": [ + "strict_call_policy=true", + "strict_memory_policy=false", + "max_steps=100" + ], + "symbols": [ + "__kmalloc", + "__kmalloc_node", + "__kmalloc_node_track_caller", + "__vcalloc", + "kmalloc_large_node", + "kmalloc_node_trace", + "kmalloc_order", + "kmalloc_order_trace", + "kmalloc_trace", + "kmem_cache_alloc_node", + "kmem_cache_alloc_trace", + "kmemdup", + "kmemdup_nul", + "krealloc", + "kstrdup", + "kstrdup_const", + "kstrndup", + "kvmalloc_node", + "kvmemdup", + "kvrealloc", + "memdup_user_nul", + "strndup_user", + "vcalloc", + "vmalloc_array", + "vmemdup_user" + ] + }, + "CWE676": { + "_comment": "https://github.com/01org/safestringlib/wiki/SDL-List-of-Banned-Functions", + "symbols": [ + "memcmp", + "memcpy", + "memmove", + "memset", + "strcat", + "strcpy", + "strlen", + "strncat", + "strncpy" + ] + }, + "CWE789": { + "_comment": "Allocation functions that accept a size argument.", + "stack_threshold": 7500, + "heap_threshold": 1000000, + "symbols": [] + }, + "Memory": { + "allocation_symbols": [ + "__kmalloc", + "__kmalloc_node", + "__kmalloc_node_track_caller", + "__vcalloc", + "kmalloc_large_node", + "kmalloc_node_trace", + "kmalloc_order", + "kmalloc_order_trace", + "kmalloc_trace", + "kmem_cache_alloc_node", + "kmem_cache_alloc_trace", + "kmemdup", + "kmemdup_nul", + "krealloc", + "kstrdup", + "kstrdup_const", + "kstrndup", + "kvmalloc_node", + "kvmemdup", + "kvrealloc", + "memdup_user_nul", + "strndup_user", + "vcalloc", + "vmalloc_array", + "vmemdup_user" + ] + } +} diff --git a/test/lkm_samples/.gitignore b/test/lkm_samples/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/test/lkm_samples/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/test/lkm_samples/Dockerfile b/test/lkm_samples/Dockerfile new file mode 100644 index 000000000..41e785b7d --- /dev/null +++ b/test/lkm_samples/Dockerfile @@ -0,0 +1,75 @@ +FROM ubuntu:noble as builder + +# This container is used to build the sample kernel modules. + +# Kernel release to build the modules against. When changing the minor or +# major release make sure to update the curl command below. +ARG LX_VER="6.7.6" + +# Install tools required to build a kernel. +RUN set -x && \ + echo 'debconf debconf/frontend select Noninteractive' | \ + debconf-set-selections && \ + apt-get update && \ + apt-get install -y -q apt-utils dialog && \ + apt-get install -y -q \ + aptitude \ + bc \ + bison \ + bsdmainutils \ + clang \ + clang-tools \ + curl \ + flex \ + git \ + libelf-dev \ + libncurses5-dev \ + libssl-dev \ + lld \ + llvm \ + make \ + sparse \ + sudo && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN set -x && \ + useradd -m cwe && \ + echo "cwe:cwe" | \ + chpasswd && \ + adduser cwe sudo && \ + sed -i.bkp -e 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' /etc/sudoers + +USER cwe + +# Download the kernel. +WORKDIR /home/cwe +RUN set -x && \ + curl https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LX_VER}.tar.gz -o linux.tar.gz && \ + tar xf linux.tar.gz && \ + mv linux-${LX_VER} linux + +# Build a minimal kernel with support for external modules. +WORKDIR /home/cwe/linux +ENV ARCH=arm64 +ENV LLVM=1 +COPY modules.config.fragment . +COPY debug.config.fragment . +RUN set -x && \ + make allnoconfig && \ + ./scripts/kconfig/merge_config.sh -n -m .config debug.config.fragment modules.config.fragment && \ + make -j$(nproc) Image modules modules_prepare + +# Build our sample modules. +WORKDIR /home/cwe/build +COPY *.c . +COPY Makefile . +ENV KBUILD_VERBOSE=1 +RUN set -x && \ + mkdir build && \ + make all && \ + for m in $(find . -name '*.ko'); do cp $m "build/${m%.*}_aarch64_clang.ko"; done + +# Copy into a new Docker image to save space. +FROM scratch +COPY --from=builder /home/cwe/build/build /build diff --git a/test/lkm_samples/Makefile b/test/lkm_samples/Makefile new file mode 100644 index 000000000..85a6ab58a --- /dev/null +++ b/test/lkm_samples/Makefile @@ -0,0 +1,9 @@ +obj-m += cwe_467.o +obj-m += cwe_476.o +obj-m += cwe_676.o + +all: + make LLVM=1 ARCH=arm64 -C /home/cwe/linux M=$(PWD) modules + +clean: + make LLVM=1 ARCH=arm64 -C /home/cwe/linux M=$(PWD) clean diff --git a/test/lkm_samples/Readme.md b/test/lkm_samples/Readme.md new file mode 100644 index 000000000..5dbd23360 --- /dev/null +++ b/test/lkm_samples/Readme.md @@ -0,0 +1,16 @@ +# Test Linux Kernel Modules for the Acceptance Test Suite + +For the acceptance test suite of the `cwe_checker`, the C-files inside this +directory have to be compiled for a variety of CPU architectures and +C-compilers. The provided script should be used for the build process. + +## Prerequisites + +- Have Docker installed on your system. + +## Build commands + +Inside this directory run the following commands: +```shell +./build.sh +``` diff --git a/test/lkm_samples/build.sh b/test/lkm_samples/build.sh new file mode 100755 index 000000000..e975d5e4a --- /dev/null +++ b/test/lkm_samples/build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Script that builds the sample kernel modules and places them in ./build. + +set -xeuo pipefail + +DOCKER="sudo -E docker" +NAME=lkm_samples + +$DOCKER build --progress plain -t ${NAME} . + +sudo rm -rf build/ + +# Create a dummy container, copy the modules, and delete it. +ID=$($DOCKER create ${NAME} /does/not/exist) +$DOCKER cp ${ID}:/build . +$DOCKER rm ${ID} +sudo chown $(id -u):$(id -g) -R ./build diff --git a/test/lkm_samples/cwe_467.c b/test/lkm_samples/cwe_467.c new file mode 100644 index 000000000..6d77ead68 --- /dev/null +++ b/test/lkm_samples/cwe_467.c @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include +#include + +const char *long_string = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; +char buf[10]; + +static int simple_sizeof_ptr_02(void) +{ + char *ptr = kmalloc(0x10, __GFP_ZERO); + + strncpy(ptr, long_string, sizeof(ptr)); + + return 42; +} + +static int simple_sizeof_ptr_01(void) +{ + strncpy(buf, long_string, sizeof(&buf)); + + return 42; +} + +static int __init test_init(void) +{ + pr_info("Hello, World\n"); + + simple_sizeof_ptr_01(); + simple_sizeof_ptr_02(); + + return 0; +} + +static void __exit test_exit(void) +{ + pr_info("Goodbye, World\n"); +} + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Valentin Obst"); + +module_init(test_init); +module_exit(test_exit); diff --git a/test/lkm_samples/cwe_476.c b/test/lkm_samples/cwe_476.c new file mode 100644 index 000000000..ba573033e --- /dev/null +++ b/test/lkm_samples/cwe_476.c @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include + +static int simple_null_deref(void) +{ + char *ptr = kmalloc(0x42, __GFP_ZERO); + + pr_info("%c\n", *ptr); + + return 42; +} + +static int simple_not_null_deref(void) +{ + char *ptr = kmalloc(0x42, __GFP_ZERO); + + if (!ptr) + return 1337; + + pr_info("%c\n", *ptr); + + return 42; +} + +static int __init test_init(void) +{ + pr_info("Hello, World\n"); + + simple_not_null_deref(); + simple_null_deref(); + + return 0; +} + +static void __exit test_exit(void) +{ + pr_info("Goodbye, World\n"); +} + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Valentin Obst"); + +module_init(test_init); +module_exit(test_exit); diff --git a/test/lkm_samples/cwe_676.c b/test/lkm_samples/cwe_676.c new file mode 100644 index 000000000..a7d766dbc --- /dev/null +++ b/test/lkm_samples/cwe_676.c @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include +#include + +static char *simple_strlen_strcpy(const char *msg) +{ + char *ptr = kmalloc(strlen(msg), __GFP_ZERO); + + if (!ptr) + return NULL; + + strcpy(ptr, msg); + + return ptr; +} + +static int __init test_init(void) +{ + pr_info("Hello, World\n"); + + char *msg = (char *)((unsigned long)THIS_MODULE + 0x1337); + + pr_info("%s\n", simple_strlen_strcpy(msg)); + + return 0; +} + +static void __exit test_exit(void) +{ + pr_info("Goodbye, World\n"); +} + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Valentin Obst"); + +module_init(test_init); +module_exit(test_exit); diff --git a/test/lkm_samples/debug.config.fragment b/test/lkm_samples/debug.config.fragment new file mode 100644 index 000000000..d8e3d991b --- /dev/null +++ b/test/lkm_samples/debug.config.fragment @@ -0,0 +1,9 @@ +# +# Compile-time checks and compiler options +# +CONFIG_DEBUG_KERNEL=y +CONFIG_DEBUG_INFO=y +CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y +CONFIG_DEBUG_INFO_REDUCED=n +CONFIG_DEBUG_INFO_COMPRESSED_NONE=y +CONFIG_DEBUG_INFO_SPLIT=n diff --git a/test/lkm_samples/modules.config.fragment b/test/lkm_samples/modules.config.fragment new file mode 100644 index 000000000..0427a1b78 --- /dev/null +++ b/test/lkm_samples/modules.config.fragment @@ -0,0 +1,12 @@ +CONFIG_ASM_MODVERSIONS=y +CONFIG_MODULES=y +CONFIG_MODULE_COMPRESS_NONE=y +CONFIG_MODULE_SIG=y +CONFIG_MODULE_SIG_ALL=y +CONFIG_MODULE_SIG_FORCE=y +CONFIG_MODULE_SIG_FORMAT=y +CONFIG_MODULE_SIG_HASH="sha512" +CONFIG_MODULE_SIG_SHA512=y +CONFIG_MODULE_SRCVERSION_ALL=y +CONFIG_MODULE_UNLOAD=y +CONFIG_MODVERSIONS=y diff --git a/test/src/lib.rs b/test/src/lib.rs index b1fef99a9..7c19ad62d 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -14,6 +14,12 @@ pub const COMPILERS: &[&str] = &["gcc", "clang"]; pub const WINDOWS_ARCHITECTURES: &[&str] = &["x64", "x86"]; /// Compilers used for the Windows-based test samples pub const WINDOWS_COMPILERS: &[&str] = &["mingw32-gcc"]; +/// CPU architectures that are supported for Linux kernel modules. +pub const LKM_ARCHITECTURES: &[&str] = &["aarch64"]; +/// Compilers used for the Linux kernel module test samples. +pub const LKM_COMPILERS: &[&str] = &["clang"]; +/// CWEs that are supported for Linux kernel modules. +pub const LKM_CWE: &[&str] = &["cwe_467", "cwe_476", "cwe_676"]; /// A test case containing the necessary information to run an acceptance test. #[derive(Debug, PartialEq, Eq, Hash, Clone)] @@ -28,15 +34,24 @@ pub struct CweTestCase { check_name: &'static str, /// Whether the test case should be skipped skipped: bool, + /// True iff the test binary is a Linux kernel module. + is_lkm: bool, } impl CweTestCase { /// Get the file path of the test binary fn get_filepath(&self) -> String { - format!( - "artificial_samples/build/{}_{}_{}.out", - self.cwe, self.architecture, self.compiler - ) + if self.is_lkm { + format!( + "lkm_samples/build/{}_{}_{}.ko", + self.cwe, self.architecture, self.compiler + ) + } else { + format!( + "artificial_samples/build/{}_{}_{}.out", + self.cwe, self.architecture, self.compiler + ) + } } /// Run the test case and print to the shell, whether the test case succeeded or not. @@ -86,28 +101,39 @@ impl CweTestCase { /// Mark test cases using the given CPU architecture as `skipped`. pub fn mark_architecture_skipped(test_cases: &mut [CweTestCase], arch: &str) { - for test in test_cases.iter_mut() { - if test.architecture == arch { - test.skipped = true; - } - } + mark_skipped_closure(test_cases, |test| test.architecture == arch) } /// Mark test cases using the given compiler as `skipped`. pub fn mark_compiler_skipped(test_cases: &mut [CweTestCase], comp: &str) { - for test in test_cases.iter_mut() { - if test.compiler == comp { - test.skipped = true; - } - } + mark_skipped_closure(test_cases, |test| test.compiler == comp) } /// Mark test cases using the given CPU architecture + compiler combination as `skipped`. pub fn mark_skipped(test_cases: &mut [CweTestCase], value1: &str, value2: &str) { - for test in test_cases.iter_mut() { - if (test.architecture == value1 && test.compiler == value2) + mark_skipped_closure(test_cases, |test| { + (test.architecture == value1 && test.compiler == value2) || (test.architecture == value2 && test.compiler == value1) - { + }) +} + +/// Mark test cases using the given CPU architecture + compiler combination as `skipped` +/// iff they are not Linux kernel modules. +pub fn mark_skipped_user(test_cases: &mut [CweTestCase], value1: &str, value2: &str) { + mark_skipped_closure(test_cases, |test| { + !test.is_lkm + && ((test.architecture == value1 && test.compiler == value2) + || (test.architecture == value2 && test.compiler == value1)) + }) +} + +/// Marks all test cases for which the given callback returns true as `skipped`. +fn mark_skipped_closure(test_cases: &mut [CweTestCase], predicate: F) +where + F: Fn(&CweTestCase) -> bool, +{ + for test in test_cases.iter_mut() { + if predicate(test) { test.skipped = true; } } @@ -115,7 +141,7 @@ pub fn mark_skipped(test_cases: &mut [CweTestCase], value1: &str, value2: &str) /// Return a list with all possible Linux test cases for the given CWE. pub fn linux_test_cases(cwe: &'static str, check_name: &'static str) -> Vec { - new_test_cases(cwe, ARCHITECTURES, COMPILERS, check_name) + new_test_cases(cwe, ARCHITECTURES, COMPILERS, check_name, false) .into_iter() .filter(|test| test.architecture != "ppc" || test.compiler != "clang") .collect() @@ -123,7 +149,23 @@ pub fn linux_test_cases(cwe: &'static str, check_name: &'static str) -> Vec Vec { - new_test_cases(cwe, WINDOWS_ARCHITECTURES, WINDOWS_COMPILERS, check_name) + new_test_cases( + cwe, + WINDOWS_ARCHITECTURES, + WINDOWS_COMPILERS, + check_name, + false, + ) +} + +/// Returns a list with all possible Linux kernel module test cases for the +/// given CWE. +pub fn lkm_test_cases(cwe: &'static str, check_name: &'static str) -> Vec { + if LKM_CWE.contains(&cwe) { + new_test_cases(cwe, LKM_ARCHITECTURES, LKM_COMPILERS, check_name, true) + } else { + Vec::new() + } } /// Generate test cases for all combinations of CPU architecture and compiler given. @@ -132,6 +174,7 @@ pub fn new_test_cases( architectures: &[&'static str], compilers: &[&'static str], check_name: &'static str, + is_lkm: bool, ) -> Vec { let mut vec = Vec::new(); for architecture in architectures { @@ -142,6 +185,7 @@ pub fn new_test_cases( compiler, check_name, skipped: false, + is_lkm, }); } } @@ -152,6 +196,7 @@ pub fn new_test_cases( pub fn all_test_cases(cwe: &'static str, check_name: &'static str) -> Vec { let mut vec = linux_test_cases(cwe, check_name); vec.append(&mut windows_test_cases(cwe, check_name)); + vec.append(&mut lkm_test_cases(cwe, check_name)); vec } @@ -336,7 +381,9 @@ mod tests { #[ignore] fn cwe_215() { let mut error_log = Vec::new(); - let tests = linux_test_cases("cwe_476", "CWE215"); // We use the test binaries of another check here. + // We use the test binaries of another check here. + let mut tests = linux_test_cases("cwe_476", "CWE215"); + tests.extend(lkm_test_cases("cwe_476", "CWE215")); for test_case in tests { let num_expected_occurences = 1; @@ -518,7 +565,7 @@ mod tests { // Only one instance is found. // Other instance cannot be found, since the constant is not defined in the basic block of the call instruction. - mark_skipped(&mut tests, "aarch64", "clang"); + mark_skipped_user(&mut tests, "aarch64", "clang"); mark_skipped(&mut tests, "arm", "clang"); mark_skipped(&mut tests, "mips", "clang"); mark_skipped(&mut tests, "mipsel", "clang"); @@ -620,7 +667,7 @@ mod tests { #[ignore] fn cwe_782() { let mut error_log = Vec::new(); - let tests = new_test_cases("cwe_782", &["x64"], COMPILERS, "CWE782"); + let tests = new_test_cases("cwe_782", &["x64"], COMPILERS, "CWE782", false); for test_case in tests { let num_expected_occurences = 1; if let Err(error) = test_case.run_test("[CWE782]", num_expected_occurences) {