Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial LKM support #441

Merged
merged 13 commits into from
Feb 29, 2024
Merged
7 changes: 5 additions & 2 deletions .github/workflows/acceptance-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
run: docker run --rm cwe_checker /bin/echo | grep -q CWE676
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
Expand Down
29 changes: 17 additions & 12 deletions src/caller/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BareMetalConfig> =
args.bare_metal_config.as_ref().map(|config_path| {
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/cwe_checker_lib/src/checkers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub struct RuntimeMemoryImage {
pub memory_segments: Vec<MemorySegment>,
/// Endianness
pub is_little_endian: bool,
/// True iff we are analyzing a Linux loadable kernel module.
pub is_lkm: bool,
}

impl RuntimeMemoryImage {
Expand All @@ -18,31 +20,22 @@ impl RuntimeMemoryImage {
RuntimeMemoryImage {
memory_segments: Vec::new(),
is_little_endian,
is_lkm: false,
}
}

/// Generate a runtime memory image for a given binary.
///
/// The function can parse ELF and PE files as input.
pub fn new(binary: &[u8]) -> Result<Self, Error> {
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() {
Expand All @@ -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)
Expand All @@ -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<Self, Error> {
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<Self, Error> {
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:
Expand Down Expand Up @@ -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<u64, Error> {
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 {
Expand Down Expand Up @@ -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::*};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ impl RuntimeMemoryImage {
},
],
is_little_endian: true,
is_lkm: false,
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/cwe_checker_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions src/cwe_checker_lib/src/utils/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> = 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),
Enkelmann marked this conversation as resolved.
Show resolved Hide resolved
// 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<u8> = binary[program_header.file_range()].to_vec();
Expand Down
7 changes: 5 additions & 2 deletions src/cwe_checker_lib/src/utils/ghidra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 0 additions & 19 deletions src/cwe_checker_lib/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64, Error> {
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")),
}
}
Loading
Loading