//! This module implements a check for CWE-243: Creation of chroot Jail Without Changing Working Directory. //! //! Creating a chroot Jail without changing the working directory afterwards does //! not prevent access to files outside of the jail. //! //! See <https://cwe.mitre.org/data/definitions/243.html> for detailed a description. //! //! ## How the check works //! //! According to <http://www.unixwiz.net/techtips/chroot-practices.html>, there are //! several ways to achieve the safe creation of a chroot jail. //! One can either call chdir after chroot //! or, if chdir is called before chroot, drop priviledges after the chroot call. //! The functions used to drop priviledges are configurable in config.json. //! We check whether each function that calls //! chroot is using one of these safe call sequences to create the chroot jail. //! If not, a warning is emitted. //! //! ## False Positives //! //! None known. //! //! ## False Negatives //! //! We do not check whether the parameters to chdir, chroot and the priviledge dropping functions //! are suitable to create a safe chroot jail. use crate::analysis::graph::Node; use crate::intermediate_representation::*; use crate::prelude::*; use crate::utils::graph_utils::is_sink_call_reachable_from_source_call; use crate::utils::log::{CweWarning, LogMessage}; use crate::utils::symbol_utils::find_symbol; use crate::CweModule; pub static CWE_MODULE: CweModule = CweModule { name: "CWE243", version: "0.2", run: check_cwe, }; /// The configuration struct contains the list of functions /// that are assumed to be used to correctly drop priviledges after a `chroot` call. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)] pub struct Config { priviledge_dropping_functions: Vec<String>, } /// Check whether the given block calls the given TID. /// If yes, return the TID of the jump term that contains the call. fn blk_calls_tid(blk: &Term<Blk>, tid: &Tid) -> Option<Tid> { for jmp in blk.term.jmps.iter() { match &jmp.term { Jmp::Call { target, .. } if target == tid => { return Some(jmp.tid.clone()); } _ => (), } } None } /// Check whether the given `sub` calls both the `chdir_tid` /// and at least one of the `priviledge_dropping_tids`. /// If yes, return true. fn sub_calls_chdir_and_priviledge_dropping_func( sub: &Term<Sub>, chdir_tid: &Tid, priviledge_dropping_tids: &[Tid], ) -> bool { let mut is_chdir_called = false; for blk in sub.term.blocks.iter() { if blk_calls_tid(blk, chdir_tid).is_some() { is_chdir_called = true; break; } } if !is_chdir_called { return false; } for blk in sub.term.blocks.iter() { if priviledge_dropping_tids .iter() .any(|tid| blk_calls_tid(blk, tid).is_some()) { return true; } } false } /// Generate a CWE warning for a CWE hit. fn generate_cwe_warning(sub: &Term<Sub>, callsite: &Tid) -> CweWarning { CweWarning::new( CWE_MODULE.name, CWE_MODULE.version, format!( "(The program utilizes chroot without dropping privileges and/or changing the directory) at {} ({})", callsite.address, sub.term.name )) .tids(vec![format!("{}", callsite)]) .addresses(vec![callsite.address.clone()]) .symbols(vec![sub.term.name.clone()]) } /// Run the check. /// /// For each call to `chroot` we check /// - that it is either followed by a call to `chdir` in the same function /// - or that the same function contains calls to `chdir` /// and a call to a function that can be used to drop priviledges. /// /// If both are false, we assume that the chroot-jail is insecure and report a CWE hit. pub fn check_cwe( analysis_results: &AnalysisResults, cwe_params: &serde_json::Value, ) -> (Vec<LogMessage>, Vec<CweWarning>) { let project = analysis_results.project; let graph = analysis_results.pointer_inference.unwrap().get_graph(); let config: Config = serde_json::from_value(cwe_params.clone()).unwrap(); let priviledge_dropping_tids: Vec<Tid> = config .priviledge_dropping_functions .into_iter() .filter_map(|func_name| { if let Some((tid, _)) = find_symbol(&project.program, &func_name) { Some(tid.clone()) } else { None } }) .collect(); let chroot_tid = match find_symbol(&project.program, "chroot") { Some((tid, _)) => tid.clone(), None => return (Vec::new(), Vec::new()), // chroot is never called by the program }; let mut cwe_warnings = Vec::new(); for node in graph.node_indices() { if let Node::BlkEnd(blk, sub) = graph[node] { if let Some(callsite_tid) = blk_calls_tid(blk, &chroot_tid) { if let Some(chdir_tid) = find_symbol(&project.program, "chdir").map(|(tid, _)| tid.clone()) { if graph.neighbors(node).count() > 1 { panic!("Malformed Control flow graph: More than one edge for extern function call") } let chroot_return_to_node = graph.neighbors(node).next().unwrap(); // If chdir is called after chroot, we assume a secure chroot jail. if is_sink_call_reachable_from_source_call( graph, chroot_return_to_node, &chroot_tid, &chdir_tid, ) .is_none() { // If chdir is not called after chroot, it has to be called before it. // Additionally priviledges must be dropped to secure the chroot jail in this case. if !sub_calls_chdir_and_priviledge_dropping_func( sub, &chdir_tid, &priviledge_dropping_tids[..], ) { cwe_warnings.push(generate_cwe_warning(sub, &callsite_tid)); } } } else { // There is no chdir symbol, so the chroot jail cannot be secured. cwe_warnings.push(generate_cwe_warning(sub, &callsite_tid)); } } } } (Vec::new(), cwe_warnings) }