diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ec69969 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,216 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- N/A + +## [0.2.0] - 2021-04-20 + +### Added + +- Project changelog +- Linux LD_PRELOAD/LD_AUDIT library: Generic hook +- Linux LD_PRELOAD/LD_AUDIT library: Support for 40 hooks including Execution and Filesystem hooks +- Database-driven design +- Settings +- Commands to modify WhiteBeam settings, toggle hooks, and load SQL +- Modular action framework (compile time reflection), 12 actions +- Modular hash framework (compile time reflection), added hashing algorithms (ARGON2ID, BLAKE3, SHA-3) +- Hybrid hashing +- Recovery secret + +### Changed + +- Linux LD_PRELOAD/LD_AUDIT library: LD_AUDIT loader +- Replaced SodiumOxide with pure Rust audited cryptography library (RustCrypto) +- Improved whitelisting system +- Updated to latest dependencies + +### Removed + +- SHA-2 hash family + +### Security +- A user with local access to a server running WhiteBeam could bypass whitelisting functionality + Fixed in 0.2.0: https://github.com/WhiteBeamSec/WhiteBeam/security/advisories/GHSA-7wf6-3j4p-jm8x + +## [0.1.3] - 2020-03-25 + +### Added + +- Linux installer +- Linux LD_PRELOAD library: tests + +### Changed + +- Linux LD_PRELOAD library: refactored fexecve +- Project is now fully Rust +- Relicensed as CC-BY-NC +- Updated to latest dependencies + +### Removed + +- Dependency on GNU Make + +### Fixed + +- execl* corrected + +## [0.1.2] - 2020-03-08 + +### Added + +- Baselines +- Copyright, organization +- Hashing standardized to libsodium default (SHA3 removed) +- Linux LD_PRELOAD library: new hook templates, refactored hooks + +### Removed + +- Linux LD_PRELOAD library: original hook template + +## [0.1.1] - 2020-02-01 + +### Added + +- Exception handling +- Many new CLI arguments +- WhiteBeam service: updated to be asynchronous + +### Changed + +- Updated to latest dependencies + +### Fixed + +- Correct OS encoding of strings +- WhiteBeam service: execution log API restricted to localhost + +## [0.1.0] - 2019-12-26 + +### Added + +- libsodium cryptography +- Project code restructured into workspaces +- WhiteBeam service: encrypted API route, public key API route + +### Changed + +- Updated to latest dependencies + +### Fixed + +- Linux LD_PRELOAD library: warn on seccomp usage (fix scheduled) +- Optimized memory usage + +## [0.0.9] - 2019-11-20 + +### Added + +- CLI --status argument for monitoring service health +- Database initialization routines +- Dynamic whitelists +- Initial release binaries provided + +### Changed + +- Updated to latest dependencies + +### Fixed + +- execl* corrected + +## [0.0.8] - 2019-10-15 + +### Added + +- Cross platform support for uptime, locating data files +- Database functions, objects are now platform-independent +- Linux LD_PRELOAD library: hooks structured to be modular +- Prototype whitelist functionality working +- WhiteBeam library targets nightly Rust for variadic function support +- WhiteBeam service: startup script for Linux + +## [0.0.7] - 2019-09-02 + +### Added + +- Linux LD_PRELOAD library: file descriptor support +- Linux LD_PRELOAD library: hooks for exec family + +### Fixed + +- Error handling for hashing + +## [0.0.6] - 2019-08-31 + +### Added + +- Whitelisting and hashing of authorized executables + +### Fixed + +- Refactored library HTTP requests to reduce crashes + +### Security +- If the LD_PRELOAD/LD_AUDIT environment variables were defined to a nonexecutable + shared object library, execution of non-whitelisted library functions was possible. + Fixed in 0.0.6: https://github.com/WhiteBeamSec/WhiteBeam/security/advisories/GHSA-mm3f-f5hg-p2hv + +## [0.0.5] - 2019-08-26 + +### Added + +- Created bug bounty +- Linux LD_PRELOAD library: Execution logging +- Reduced file size of release binaries +- WhiteBeam service: API endpoint to process executions (log/exec) + +## [0.0.4] - 2019-08-10 + +### Added + +- WhiteBeam service/CLI + +## [0.0.3] - 2019-06-23 + +### Added + +- Linux LD_PRELOAD library: execve support +- Linux LD_PRELOAD library: test case for execve + +## [0.0.2] - 2019-05-20 + +### Added + +- Linux LD_PRELOAD library: working function interposition +- Project code structured to be modular + +## [0.0.1] - 2019-05-20 + +### Added + +- Project license + +[unreleased]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.1.3...v0.2.0 +[0.1.3]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.9...v0.1.0 +[0.0.9]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.8...v0.0.9 +[0.0.8]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.7...v0.0.8 +[0.0.7]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.6...v0.0.7 +[0.0.6]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.5...v0.0.6 +[0.0.5]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.4...v0.0.5 +[0.0.4]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.3...v0.0.4 +[0.0.3]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.2...v0.0.3 +[0.0.2]: https://github.com/WhiteBeamSec/WhiteBeam/compare/v0.0.1...v0.0.2 +[0.0.1]: https://github.com/WhiteBeamSec/WhiteBeam/releases/tag/v0.0.1 diff --git a/README.md b/README.md index 544da85..be00602 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,62 @@ Transparent endpoint security --- -

Coming soon: 0.2

+## Features + +* Block and detect advanced attacks +* Modern audited cryptography: [RustCrypto](https://github.com/RustCrypto) for hashing and encryption +* Highly compatible: Development focused on all platforms (incl. legacy) and architectures +* Source available: Audits welcome +* Reviewed by security researchers with combined 100+ years of experience + +## In Action + +* [Video demonstration of detection and prevention capabilities](TODO) +* [Recorded attacks against the WhiteBeam 0.2 honeypot](TODO) [ LIVE ] + +TODO: New video coming soon! + +[![asciicast](https://asciinema.org/a/296135.svg)](https://asciinema.org/a/296135) + +## Installation + +### From Repositories + +TODO: Repositories + +### From Packages (Linux) + +TODO: Using your package manager of choice (on Ubuntu/Debian (apt/snap classic)/Gentoo (emerge)/Arch (pacman AUR)/RHEL/Amazon Linux/Rocky Linux (yum)/OpenSUSE/etc.), details on installing `whitebeam` package. + +**Important**: Always ensure the downloaded file hash matches official hashes ([How-to](https://github.com/WhiteBeamSec/WhiteBeam/wiki/Verifying-file-hashes)). + +https://github.com/WhiteBeamSec/WhiteBeam/releases + +### From Source (Linux) + +1. Run tests (_Optional_): + * `cargo run test` +2. Compile: + * `cargo run build` +3. Install WhiteBeam: + * `cargo run install` + +## Quick start +1. Become root (`sudo -s`/`su root`) +2. Set a recovery secret. You'll be able to use this with `whitebeam --auth` to make changes to the system: `whitebeam --setting RecoverySecret mask` + +### How to Detect Attacks with WhiteBeam +Multiple guides are provided depending on your preference. [Contact us](info@whitebeamsec.com) so we can help you integrate WhiteBeam with your environment. +1. [Serverless guide](TODO), for passive review +2. [osquery Fleet setup guide](TODO), for passive review +3. [WhiteBeam Server setup guide](TODO), for active response + +### How to Prevent Attacks with WhiteBeam +1. Become root (`sudo -s`/`su root`) +2. Download default whitelists for your platform: + * `whitebeam --load Base` +3. Review the baseline after a minimum of 24 hours: + * `whitebeam --baseline` +4. Add trusted behavior to the whitelist, following the [whitelisting guide](TODO) +5. Enable WhiteBeam prevention: + * `whitebeam --setting Prevention true` \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index ccf7bee..bd00392 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,19 +12,24 @@ Please email us at security@whitebeamsec.com. We request at least the following gpg --keyserver hkp://pgp.mit.edu:80 --recv-keys 4A3F1233C01563F808B8355125ECFD172151528B -**Current security vulnerability rewards** (will be provided at the discretion of the lead developers): +**Current security vulnerability rewards** -| Vulnerability | Reward | -| --------------------------------------------- | -------------- | -| Remote code execution (RCE) | $1000, Credits | -| Local privilege escalation (LPE) | $500, Credits | -| Bypass whitelisting on chal.whitebeamsec.com | $250, Credits | -| Cryptographic vulnerability | $150, Credits | -| Remote denial of service (DoS), service crash | $25 | +Rewards will be provided at the discretion of the lead developers. All vulnerabilities must be demonstrated in the challenge environment to be eligible for payment. + +| Vulnerability | Reward | +| ------------------------------------------------------------------------------------------- | -------------- | +| Remote code execution (RCE) | $5000, Credits | +| Local privilege escalation (LPE) | $2000, Credits | +| Bypass whitelisting\* ([Try the challenge!](https://challenge.whitebeamsec.com)) | $1000, Credits | +| Cryptographic vulnerability | $250, Credits | +| WhiteBeam service crash (DoS) | $50 | + +\* Must be a program presently whitelisted by WhiteBeam Security, Inc. exhibiting documented behavior or a common OS kernel/dynamic linker feature that bypasses WhiteBeam. Please report vulnerabilities in third party software to their respective vendors. Past security advisories can be found here: https://github.com/WhiteBeamSec/WhiteBeam/security/advisories We would like to thank the following security researchers for their contributions to WhiteBeam's security: -* gemini -* brianx +| Researchers | Date | :trophy: | +| -------------------- | ----------- | ------------------ | +| *gemini*, *brianx* | Nov 6, 2019 | [WhiteBeam 0.0.5](https://github.com/WhiteBeamSec/WhiteBeam/security/advisories/GHSA-mm3f-f5hg-p2hv) | diff --git a/src/application/Cargo.toml b/src/application/Cargo.toml index ff4317c..a079468 100644 --- a/src/application/Cargo.toml +++ b/src/application/Cargo.toml @@ -1,7 +1,7 @@ # General info [package] name = "whitebeam" -version = "0.1.3" +version = "0.2.0" authors = ["WhiteBeam Security, Inc."] edition = "2018" @@ -13,18 +13,26 @@ path = "main.rs" # Cross-platform dependencies [dependencies] libc = { version = "0.2" } -sodiumoxide = { version = "0.2" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } -rusqlite = { version = "0.21", features = ["bundled"] } +rusqlite = { version = "0.25", features = ["bundled"] } hex = { version = "0.4" } clap = { version = "2.33" } -tokio = { version = "0.2", features = ["macros"] } -warp = { version = "0.2" } -rpassword = { version = "4.0" } -cli-table = { version = "0.3" } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +warp = { version = "0.3" } +reqwest = { version = "0.11", features = ["blocking"] } +rpassword = { version = "5.0" } +cli-table = { version = "0.4" } +linkme = { version = "0.2" } +automod = { version = "1.0" } +rand = { version = "0.7" } +glob = { version = "0.3" } +goblin = { version = "0.4" } +# Cryptographic dependencies +sha3 = { version = "0.9" } +blake3 = { version = "0.3" } +argon2 = { version = "0.1" } +crypto_box = { version = "0.5" } -# Windows dependencies -[target.'cfg(target_os = "windows")'.dependencies.kernel32-sys] -version = "0.2" -default-features = false +[features] +whitelist_test = [] diff --git a/src/application/common/api/log.rs b/src/application/common/api/log.rs index a465693..1b59bb9 100644 --- a/src/application/common/api/log.rs +++ b/src/application/common/api/log.rs @@ -1,9 +1,10 @@ +// TODO: Log failures // Database use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use crate::common::db; -// POST /log/exec -pub async fn log_exec(exec: db::LogExecObject, addr: Option) -> Result { +// POST /log +pub async fn log(log: db::LogObject, addr: Option) -> Result { let localhost = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let remote_addr = match addr { Some(inetaddr) => inetaddr.ip(), @@ -13,7 +14,16 @@ pub async fn log_exec(exec: db::LogExecObject, addr: Option) -> Resu return Err(warp::reject::not_found()); } // Input to this function is untrusted - let conn: rusqlite::Connection = db::db_open(); - db::insert_exec(&conn, exec); + let conn: rusqlite::Connection = match db::db_open(false) { + Ok(c) => c, + Err(_) => return Err(warp::reject::not_found()) + }; + let log_level = match db::get_log_level(&conn) { + Ok(l) => l, + Err(_) => return Err(warp::reject::not_found()) + }; + if log_level >= log.class { + let _res = db::insert_log(&conn, log); + } return Ok(warp::reply()); } diff --git a/src/application/common/api/mod.rs b/src/application/common/api/mod.rs index db1f418..bed883f 100644 --- a/src/application/common/api/mod.rs +++ b/src/application/common/api/mod.rs @@ -1,3 +1,4 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use warp::Filter; // Endpoints @@ -5,21 +6,20 @@ mod status; mod log; mod service; -pub async fn serve() { +pub async fn serve(service_port: u16) { // GET /status let status_route = warp::get() .and(warp::path("status")) .and(warp::path::end()) .map(status::status); - // POST /log/exec {"program":"whoami","hash":"..","uid":1000,"ts":1566162863,"success":true} - let log_exec_route = warp::post() + // POST /log {"class":1,"log":"..","ts":1566162863} + let log_route = warp::post() .and(warp::path("log")) - .and(warp::path("exec")) .and(warp::path::end()) .and(warp::body::json()) .and(warp::addr::remote()) - .and_then(log::log_exec); + .and_then(log::log); // GET /service/public_key let service_public_key_route = warp::get() @@ -37,8 +37,9 @@ pub async fn serve() { .map(service::encrypted); let routes = status_route - .or(log_exec_route) + .or(log_route) .or(service_public_key_route) .or(service_encrypted_route); - warp::serve(routes).run(([0, 0, 0, 0], 11998)).await; + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), service_port); + warp::serve(routes).run(socket).await; } diff --git a/src/application/common/api/service.rs b/src/application/common/api/service.rs index 0ac192c..2d1ef6f 100644 --- a/src/application/common/api/service.rs +++ b/src/application/common/api/service.rs @@ -1,12 +1,24 @@ +// TODO: Always return JSON so errors can be parsed, set JSON header + // Database use crate::common::db; // Public key encryption and signatures use crate::common::crypto; fn set_console_secret(secret: &str, expiry: &str) -> String { - let conn: rusqlite::Connection = db::db_open(); - db::update_config(&conn, "console_secret", secret); - db::update_config(&conn, "console_secret_expiry", expiry); + let conn: rusqlite::Connection = match db::db_open(false) { + Ok(c) => c, + Err(_) => return String::from("OK") + }; + // Functionality temporarily disabled until crypto.rs is audited + //db::update_setting(&conn, "console_secret", secret); + //db::update_setting(&conn, "console_secret_expiry", expiry); + String::from("OK") +} + +fn query(statement: &str) -> String { + // TODO: Dispatch to integrations + // Integrations to read WhiteBeam's database are currently provided for osquery String::from("OK") } @@ -18,7 +30,7 @@ fn invalid_request() -> String { // GET /service/public_key pub async fn public_key() -> Result { match crypto::get_client_public_key() { - Ok(client_public_key) => Ok(hex::encode(client_public_key)), + Ok(client_public_key) => Ok(hex::encode(client_public_key.as_bytes())), Err(_e) => return Err(warp::reject::not_found()) } } @@ -30,6 +42,7 @@ pub fn encrypted(crypto_box_object: crypto::CryptoBox) -> impl warp::Reply { Err(_e) => return invalid_request() }; match server_message.action.as_ref() { + "query" => query(&server_message.parameters[0]), "set_console_secret" => set_console_secret(&server_message.parameters[0], &server_message.parameters[1]), _ => invalid_request() diff --git a/src/application/common/crypto.rs b/src/application/common/crypto.rs index 0291702..bf35fa4 100644 --- a/src/application/common/crypto.rs +++ b/src/application/common/crypto.rs @@ -17,37 +17,38 @@ use std::{error::Error, path::Path, fmt::Write as FmtWrite, num::ParseIntError}; -use sodiumoxide::crypto::{box_, - box_::curve25519xsalsa20poly1305::*}; +use crypto_box::{ChaChaBox, PublicKey, SecretKey, aead::{Aead, Nonce}, KEY_SIZE}; +pub const NONCE_SIZE: usize = 24; +// TODO: Test, probably doesn't work as-is. Especially the ChaChaBox without the postfix tag: https://stackoverflow.com/a/62140062 +// TODO: Refactor // TODO: Log errors +// TODO: Offer option of XSALSA20POLY1305 (check EncryptAlgorithm in database) /* Keys */ -fn key_array_from_slice(bytes: &[u8]) -> [u8; SECRETKEYBYTES] { - let mut array = [0; SECRETKEYBYTES]; +fn key_array_from_slice(bytes: &[u8]) -> [u8; KEY_SIZE] { + let mut array = [0; KEY_SIZE]; let bytes = &bytes[..array.len()]; // Panics if not enough data array.copy_from_slice(bytes); array } fn generate_client_private_key(save_path: &Path) -> Result<(), std::io::Error> { - let (_public_key, private_key) = box_::gen_keypair(); - let private_key_bytes: &[u8] = private_key.as_ref(); - let mut key_file = platform::path_open_secure(save_path); + let mut rng = rand::thread_rng(); + let private_key = SecretKey::generate(&mut rng); + let private_key_bytes: &[u8; KEY_SIZE] = &private_key.to_bytes(); + let mut key_file = platform::path_open_secure(save_path)?; Ok(key_file.write_all(private_key_bytes)?) } fn get_server_public_key() -> Result> { - let conn: rusqlite::Connection = db::db_open(); - let public_key_string: String = db::get_config(&conn, String::from("server_key")); - let public_key_bytes: &[u8] = &hex::decode(&public_key_string)?; - match PublicKey::from_slice(public_key_bytes) { - Some(public_key) => Ok(public_key), - None => Err("Invalid server public key".into()) - } + let conn: rusqlite::Connection = db::db_open(false)?; + let public_key_string: String = db::get_setting(&conn, String::from("ServerPublicKey"))?; + let public_key_bytes: [u8; KEY_SIZE] = key_array_from_slice(hex::decode(&public_key_string)?.as_slice()); + Ok(PublicKey::from(public_key_bytes)) } fn get_client_public_private_key() -> Result<(PublicKey, SecretKey), Box> { @@ -59,8 +60,8 @@ fn get_client_public_private_key() -> Result<(PublicKey, SecretKey), Box = Vec::new(); key_file.read_to_end(&mut private_key_bytes)?; - let private_key_array: [u8; SECRETKEYBYTES] = key_array_from_slice(&private_key_bytes); - let private_key = SecretKey(private_key_array); + let private_key_array: [u8; KEY_SIZE] = key_array_from_slice(&private_key_bytes); + let private_key = SecretKey::from(private_key_array); let public_key = private_key.public_key(); Ok((public_key, private_key)) } @@ -81,10 +82,10 @@ fn json_encode_message(action: String, parameters: Vec) -> Result Result { let crypto_box_object = CryptoBox { - pubkey: pubkey, - nonce: nonce, - ciphertext: ciphertext + pubkey, + nonce, + ciphertext }; Ok(serde_json::to_string(&crypto_box_object)?) } @@ -116,11 +117,11 @@ fn json_decode_crypto_box(json: String) -> Result Ok(crypto_box_object) } -fn nonce_array_from_slice(bytes: &[u8]) -> Result<[u8; NONCEBYTES], String> { - if bytes.len() != NONCEBYTES { +fn nonce_array_from_slice(bytes: &[u8]) -> Result<[u8; NONCE_SIZE], String> { + if bytes.len() != NONCE_SIZE { return Err("Invalid nonce".into()); } - let mut array = [0; NONCEBYTES]; + let mut array = [0; NONCE_SIZE]; let bytes = &bytes[..array.len()]; array.copy_from_slice(bytes); Ok(array) @@ -130,17 +131,24 @@ fn nonce_array_from_slice(bytes: &[u8]) -> Result<[u8; NONCEBYTES], String> { Encryption */ -fn generate_ciphertext(plaintext: &[u8], nonce: Nonce) -> Result, Box> { +fn generate_ciphertext(plaintext: &[u8], nonce: &[u8]) -> Result, Box> { let (_client_public_key, client_private_key) = get_client_public_private_key()?; let server_public_key = get_server_public_key()?; - Ok(box_::seal(plaintext, &nonce, &server_public_key, &client_private_key)) + let server_box = ChaChaBox::new(&server_public_key, &client_private_key); + let nonce_obj = Nonce::from_slice(nonce); + match server_box.encrypt(&nonce_obj, plaintext) { + Ok(ciphertext) => Ok(ciphertext), + Err(_e) => return Err("Could not generate ciphertext".into()) + } } -fn decrypt_server_ciphertext(ciphertext: &[u8], nonce: Nonce) -> Result, Box> { +fn decrypt_server_ciphertext(ciphertext: &[u8], nonce: &[u8]) -> Result, Box> { let (_client_public_key, client_private_key) = get_client_public_private_key()?; let server_public_key = get_server_public_key()?; + let client_box = ChaChaBox::new(&server_public_key, &client_private_key); + let nonce_obj = Nonce::from_slice(nonce); // Verification and decryption - match box_::open(ciphertext, &nonce, &server_public_key, &client_private_key) { + match client_box.decrypt(&nonce_obj, ciphertext) { Ok(plaintext) => Ok(plaintext), Err(_e) => return Err("Invalid ciphertext".into()) } @@ -158,24 +166,24 @@ pub fn get_client_public_key() -> Result> { pub fn generate_crypto_box_message(action: String, parameters: Vec) -> Result> { let (client_public_key, _client_private_key) = get_client_public_private_key()?; let message = json_encode_message(action, parameters)?; - let nonce = box_::gen_nonce(); - let ciphertext: Vec = generate_ciphertext(message.as_bytes(), nonce)?; - Ok(json_encode_crypto_box(hex::encode(client_public_key), hex::encode(nonce), hex::encode(ciphertext))?) + let mut rng = rand::thread_rng(); + let nonce = crypto_box::generate_nonce(&mut rng); + let nonce_slice = nonce.as_slice(); + let ciphertext: Vec = generate_ciphertext(message.as_bytes(), nonce_slice)?; + Ok(json_encode_crypto_box(hex::encode(client_public_key.as_bytes()), hex::encode(nonce), hex::encode(ciphertext))?) } pub fn process_request(crypto_box_object: CryptoBox) -> Result> { - let conn: rusqlite::Connection = db::db_open(); + let conn: rusqlite::Connection = db::db_open(false)?; // Ignore replayed messages - if db::get_seen_nonce(&conn, &crypto_box_object.nonce) { + if db::get_seen_nonce(&conn, &crypto_box_object.nonce)? { return Err("Invalid message".into()); } - let nonce_array: [u8; NONCEBYTES] = nonce_array_from_slice(&hex::decode(crypto_box_object.nonce)?)?; - let nonce = Nonce(nonce_array); let plaintext: String = String::from( std::str::from_utf8( &decrypt_server_ciphertext( &hex::decode(&crypto_box_object.ciphertext)?, - nonce + crypto_box_object.nonce.as_bytes() )? )? ); diff --git a/src/application/common/db.rs b/src/application/common/db.rs index e46536c..f14a885 100644 --- a/src/application/common/db.rs +++ b/src/application/common/db.rs @@ -13,47 +13,111 @@ use rusqlite::{params, Connection}; use serde::{Serialize, Deserialize}; #[derive(Deserialize, Serialize)] -pub struct LogExecObject { - pub program: String, - pub hash: String, - pub uid: u32, - pub ts: u32, - pub success: bool +pub struct LogObject { + pub class: i64, + pub log: String, + pub ts: u32 } -#[derive(Deserialize)] -pub struct ConfigEntry { - pub server_ip: String, - pub server_key: String, - pub server_type: String, - pub enabled: String +pub struct HookRow { + pub id: i64, + pub enabled: bool, + pub class: String, + pub library: String, + pub symbol: String, + pub args: String, } -pub struct WhitelistResult { - pub program: String, - pub allow_unsafe: bool, - pub hash: String +#[derive(Clone)] +pub struct WhitelistRow { + pub class: String, + pub id: i64, + pub path: String, + pub value: String +} + +#[derive(Clone)] +pub struct RuleRow { + pub library: String, + pub symbol: String, + pub arg: String, + pub actions: String } pub struct BaselineResult { - pub program: String, - pub total_blocked: u32 + pub log: String, + pub total: u32 +} + +pub fn get_setting(conn: &Connection, param: String) -> Result> { + // TODO: Log errors + Ok(conn.query_row("SELECT value FROM Setting WHERE param = ?", params![param], |r| r.get(0))?) } -pub fn get_config(conn: &Connection, config_param: String) -> String { +pub fn get_whitelist(conn: &Connection) -> Result, Box> { // TODO: Log errors - conn.query_row("SELECT config_value FROM config WHERE config_param = ?", params![config_param], |r| r.get(0)) - .expect("WhiteBeam: Could not query configuration") + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT WhitelistClass.class, Whitelist.id, Whitelist.path, Whitelist.value + FROM Whitelist + INNER JOIN WhitelistClass ON Whitelist.class = WhitelistClass.id")?; + let result_iter = stmt.query_map(params![], |row| { + Ok(WhitelistRow { + class: row.get(0)?, + id: row.get(1)?, + path: row.get(2)?, + value: row.get(3)? + }) + })?; + for result in result_iter { + result_vec.push(result?); + } + Ok(result_vec) } -pub fn get_dyn_whitelist(conn: &Connection) -> Result, Box> { - let mut result_vec: Vec = Vec::new(); - let mut stmt = conn.prepare("SELECT program, allow_unsafe, hash FROM whitelist")?; +pub fn get_hooks_pretty(conn: &Connection) -> Result, Box> { + // TODO: Log errors + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT Hook.id, Hook.enabled, HookClass.class, Hook.library || ' (' || HookLanguage.language || ')' AS library, Hook.symbol, GROUP_CONCAT('(' || Datatype.datatype || ') ' || Argument.name, ', ') AS args + FROM Hook + INNER JOIN HookClass ON Hook.class = HookClass.id + INNER JOIN HookLanguage ON Hook.language = HookLanguage.id + INNER JOIN Argument ON Hook.id = Argument.hook + INNER JOIN Datatype ON Argument.datatype = Datatype.id + WHERE Argument.parent IS NULL + GROUP BY Hook.id + ORDER BY Hook.id, Argument.position")?; + let result_iter = stmt.query_map(params![], |row| { + Ok(HookRow { + id: row.get(0)?, + enabled: row.get(1)?, + class: row.get(2)?, + library: row.get(3)?, + symbol: row.get(4)?, + args: row.get(5)? + }) + })?; + for result in result_iter { + result_vec.push(result?); + } + Ok(result_vec) +} + +pub fn get_rules_pretty(conn: &Connection) -> Result, Box> { + // TODO: Log errors + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT Hook.library, Hook.symbol, IIF(Rule.positional, Argument.name, '-') AS arg, GROUP_CONCAT(Action.name, ', ') AS actions + FROM Rule + INNER JOIN Action ON Rule.action = Action.id + INNER JOIN Argument on Rule.arg = Argument.id + INNER JOIN Hook on Argument.hook = Hook.id + GROUP BY Hook.id, Rule.positional, arg + ORDER BY Hook.id, Rule.id")?; let result_iter = stmt.query_map(params![], |row| { - Ok(WhitelistResult { - program: row.get(0)?, - allow_unsafe: row.get(1)?, - hash: row.get(2)? + Ok(RuleRow { + library: row.get(0)?, + symbol: row.get(1)?, + arg: row.get(2)?, + actions: row.get(3)? }) })?; for result in result_iter { @@ -64,15 +128,15 @@ pub fn get_dyn_whitelist(conn: &Connection) -> Result, Box< pub fn get_baseline(conn: &Connection) -> Result, Box> { let mut result_vec: Vec = Vec::new(); - let mut stmt = conn.prepare("SELECT program, count(program) AS total_blocked - FROM exec_log - WHERE success=false - GROUP BY program - ORDER BY total_blocked DESC")?; + let mut stmt = conn.prepare("SELECT log, count(log) AS total + FROM Log + WHERE log LIKE 'Blocked%' + GROUP BY log + ORDER BY total DESC")?; let result_iter = stmt.query_map(params![], |row| { Ok(BaselineResult { - program: row.get(0)?, - total_blocked: row.get(1)? + log: row.get(0)?, + total: row.get(1)? }) })?; for result in result_iter { @@ -81,190 +145,104 @@ pub fn get_baseline(conn: &Connection) -> Result, Box bool { - get_config(conn, String::from("enabled")) == String::from("true") +pub fn get_log_level(conn: &Connection) -> Result> { + match get_setting(&conn, String::from("LogVerbosity")) { + Ok(level) => Ok(level.parse().unwrap_or(1)), + // TODO: Log errors + Err(_) => Ok(1) + } } -pub fn get_valid_auth_string(conn: &Connection, auth: &str) -> bool { - let auth_hash: String = hash::common_hash_password(auth); - let console_secret_expiry: u32 = match get_config(conn, String::from("console_secret_expiry")).parse() { - Ok(v) => v, - Err(_e) => return false - }; - let time_now = time::get_timestamp(); - if console_secret_expiry == 0 || - console_secret_expiry >= time_now { - return get_config(conn, String::from("console_secret")) == String::from(auth_hash); - } - false +pub fn get_prevention(conn: &Connection) -> Result> { + Ok(get_setting(conn, String::from("Prevention"))? == String::from("true")) } -pub fn get_valid_auth_env(conn: &Connection) -> bool { - match env::var("WB_AUTH") { - Ok(val) => { - get_valid_auth_string(conn, &val) - } - Err(_e) => { - false - } +pub fn get_service_port(conn: &Connection) -> Result> { + match get_setting(&conn, String::from("ServicePort")) { + Ok(port) => Ok(port.parse().unwrap_or(11998)), + // TODO: Log errors + Err(_) => Ok(11998) } } -pub fn get_seen_nonce(conn: &Connection, nonce: &str) -> bool { - delete_all_expired_nonce(conn); - // TODO: Log errors - let count: i64 = conn.query_row("SELECT count(*) FROM nonce_hist WHERE nonce = ?", params![nonce], |r| r.get(0)) - .expect("WhiteBeam: Could not query nonce history"); - count > 0 +pub fn get_valid_auth_string(conn: &Connection, auth: &str) -> Result> { + // TODO: Support more than ARGON2ID + //let algorithm = get_setting(&conn, String::from("SecretAlgorithm"))?; + let argon2 = argon2::Argon2::default(); + let console_secret = get_setting(conn, String::from("ConsoleSecret"))?; + let recovery_secret = get_setting(conn, String::from("RecoverySecret"))?; + let console_secret_pwhash: Option = match argon2::PasswordHash::new(&console_secret) { + Ok(pwhash) => Some(pwhash), + Err(_) => None + }; + let recovery_secret_pwhash: Option = match argon2::PasswordHash::new(&recovery_secret) { + Ok(pwhash) => Some(pwhash), + Err(_) => None + }; + let auth_string = auth.to_owned(); + let auth_bytes = auth_string.as_bytes(); + let console_secret_expiry: Option = match get_setting(conn, String::from("ConsoleSecretExpiry"))?.parse() { + Ok(v) => Some(v), + Err(_e) => None + }; + let time_now = time::get_timestamp(); + if console_secret_expiry.is_some() + && (console_secret_expiry.unwrap() == 0 || console_secret_expiry.unwrap() >= time_now) + && console_secret_pwhash.is_some() + && argon2::PasswordVerifier::verify_password(&argon2, auth_bytes, &console_secret_pwhash.unwrap()).is_ok() { + return Ok(true) + } else if recovery_secret_pwhash.is_some() + && argon2::PasswordVerifier::verify_password(&argon2, auth_bytes, &recovery_secret_pwhash.unwrap()).is_ok() { + return Ok(true) + } + Ok(false) } -pub fn insert_config(conn: &Connection, config_param: &str, config_value: &str) { - let _res = conn.execute( - "INSERT INTO config (config_param, config_value) - VALUES (?1, ?2)", - params![config_param, config_value] - ); +pub fn get_valid_auth_env(conn: &Connection) -> Result> { + get_valid_auth_string(conn, &env::var("WB_AUTH")?) } -pub fn insert_whitelist(conn: &Connection, program: &str, allow_unsafe: bool, hash: &str) { - // TODO: Verify no duplicate value exists - let _res = conn.execute( - "INSERT INTO whitelist (program, allow_unsafe, hash) - VALUES (?1, ?2, ?3)", - params![program, allow_unsafe, hash] - ); +pub fn get_seen_nonce(conn: &Connection, nonce: &str) -> Result> { + // TODO: Log errors + let count: i64 = conn.query_row("SELECT count(*) FROM NonceHistory WHERE nonce = ?", params![nonce], |r| r.get(0))?; + Ok(count > 0) } -pub fn insert_exec(conn: &Connection, exec: LogExecObject) { - let _res = conn.execute( - "INSERT INTO exec_log (program, hash, uid, ts, success) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![exec.program, exec.hash, exec.uid, exec.ts, exec.success] - ); +pub fn insert_setting(conn: &Connection, param: &str, value: &str) -> Result { + conn.execute("INSERT INTO Setting (param, value) VALUES (?1, ?2)", params![param, value]) } -pub fn update_config(conn: &Connection, config_param: &str, config_value: &str) { - let _res = conn.execute( - "UPDATE config - SET config_value = ?2 - WHERE config_param = ?1", - params![config_param, config_value] - ); +pub fn insert_whitelist(conn: &Connection, class: &str, path: &str, value: &str) -> Result { + conn.execute("INSERT OR REPLACE INTO Whitelist (path, value, class) VALUES (?1, ?2, (SELECT id from WhitelistClass WHERE class=?3))", params![path, value, class]) } -pub fn delete_whitelist(conn: &Connection, program: &str) { - let _res = conn.execute("DELETE FROM whitelist WHERE program = ?1", - params![program]); +pub fn insert_log(conn: &Connection, log: LogObject) -> Result { + conn.execute("INSERT INTO Log (class, log, ts) VALUES (?1, ?2, ?3)", params![log.class, log.log, log.ts]) } -fn delete_all_expired_nonce(conn: &Connection) { - let _res = conn.execute("DELETE FROM nonce_hist WHERE ts < strftime('%s','now')-3600", - rusqlite::NO_PARAMS); +pub fn update_setting(conn: &Connection, param: &str, value: &str) -> Result { + conn.execute("INSERT OR REPLACE INTO Setting (param, value) VALUES (?1, ?2)", params![param, value]) } -fn db_init(conn: &Connection) -> Result<(), Box> { - let _res = conn.execute( - "CREATE TABLE config ( - id INTEGER PRIMARY KEY, - config_param TEXT NOT NULL, - config_value TEXT NOT NULL - )", - rusqlite::NO_PARAMS - ); - let _res = conn.execute( - "CREATE TABLE modules ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT TRUE - )", - rusqlite::NO_PARAMS - ); - let _res = conn.execute( - "CREATE TABLE exec_log ( - id INTEGER PRIMARY KEY, - program TEXT NOT NULL, - hash TEXT NOT NULL, - uid UNSIGNED INTEGER NOT NULL, - ts INTEGER NOT NULL, - success BOOLEAN NOT NULL - )", - rusqlite::NO_PARAMS - ); - let _res = conn.execute( - "CREATE TABLE error_log ( - id INTEGER PRIMARY KEY, - file TEXT NOT NULL, - line INTEGER NOT NULL, - column INTEGER NOT NULL, - desc TEXT, - ts INTEGER NOT NULL, - fatal BOOLEAN NOT NULL - )", - rusqlite::NO_PARAMS - ); - let _res = conn.execute( - "CREATE TABLE auth_log ( - id INTEGER PRIMARY KEY, - source TEXT NOT NULL, - desc TEXT, - ts INTEGER NOT NULL, - success BOOLEAN NOT NULL - )", - rusqlite::NO_PARAMS - ); - let _res = conn.execute( - "CREATE TABLE whitelist ( - id INTEGER PRIMARY KEY, - program TEXT NOT NULL, - allow_unsafe BOOLEAN NOT NULL DEFAULT FALSE, - hash TEXT NOT NULL - )", - rusqlite::NO_PARAMS - ); - let _res = conn.execute( - "CREATE TABLE nonce_hist ( - id INTEGER PRIMARY KEY, - nonce TEXT NOT NULL, - ts INTEGER NOT NULL - )", - rusqlite::NO_PARAMS - ); - let config_path: &Path = &platform::get_data_file_path("init.json"); - let init_config: bool = config_path.exists(); - if init_config { - // TODO: Validate init.json, log errors - let init_file = std::fs::File::open(config_path)?; - let json: ConfigEntry = serde_json::from_reader(init_file)?; - insert_config(conn, "server_ip", &json.server_ip); - insert_config(conn, "server_key", &json.server_key); - insert_config(conn, "server_type", &json.server_type); - insert_config(conn, "enabled", &json.enabled); - insert_config(conn, "console_secret", "undefined"); - insert_config(conn, "console_secret_expiry", "-1"); - std::fs::remove_file(config_path)?; - } else { - insert_config(conn, "server_ip", "undefined"); - insert_config(conn, "server_key", "undefined"); - insert_config(conn, "server_type", "undefined"); - insert_config(conn, "enabled", "false"); - insert_config(conn, "console_secret", "undefined"); - insert_config(conn, "console_secret_expiry", "-1"); - } - Ok(()) +pub fn update_hook_class_enabled(conn: &Connection, class: &str, enabled: bool) -> Result { + conn.execute("UPDATE Hook SET enabled = ?2 WHERE class = (SELECT id from HookClass WHERE class=?1)", params![class, enabled]) } -pub fn db_open() -> Connection { - let db_path: &Path = &platform::get_data_file_path("database.sqlite"); - // TODO: Log errors - Connection::open(db_path).expect("WhiteBeam: Could not open database") +pub fn delete_whitelist(conn: &Connection, id: u32) -> Result { + conn.execute("DELETE FROM Whitelist WHERE id = ?1", params![id]) } -pub fn db_optionally_init() { +pub fn db_open(force: bool) -> Result { let db_path: &Path = &platform::get_data_file_path("database.sqlite"); - let run_init: bool = !db_path.exists(); - let conn = db_open(); - if run_init { - // TODO: Log errors - db_init(&conn).expect("WhiteBeam: Failed to initialize database") + let no_db: bool = !db_path.exists(); + // TODO: Log instead? + if no_db && !force { + return Err("No database file found".to_string()); + } + match Connection::open(db_path) { + Ok(conn) => Ok(conn), + Err(_e) => { + return Err("Could not open database file".to_string()); + } } } diff --git a/src/application/common/hash.rs b/src/application/common/hash.rs deleted file mode 100644 index 13378fb..0000000 --- a/src/application/common/hash.rs +++ /dev/null @@ -1,55 +0,0 @@ -use sodiumoxide::crypto::hash; -use std::{fs, io, io::Read, ffi::OsStr}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::os::unix::io::FromRawFd; -#[cfg(target_os = "windows")] -use std::os::windows::io::FromRawHandle; - -fn common_hash_algo() -> sodiumoxide::crypto::hash::State { - hash::State::new() -} - -pub fn hash_null() -> String { - hex::encode(vec![0; hash::DIGESTBYTES]) -} - -pub fn common_hash_password(input: &str) -> String { - // TODO: Use pwhash - hex::encode(hash::hash(input.as_bytes())) -} - - -pub fn common_hash_data(reader: R) -> String { - let buf_size = 32768; - let mut buf: Vec = Vec::with_capacity(buf_size); - let mut hash_state = common_hash_algo(); - let mut limited_reader = reader.take(buf_size as u64); - loop { - match limited_reader.read_to_end(&mut buf) { - Ok(0) => break, - Ok(_) => { - hash_state.update(&buf[..]); - buf.clear(); - limited_reader = limited_reader.into_inner().take(buf_size as u64); - } - Err(_err) => return hash_null(), - } - } - hex::encode(hash_state.finalize()) -} - -pub fn common_hash_fd(fd: i32) -> String { - #[cfg(target_os = "windows")] - unimplemented!("WhiteBeam: File handles are not currently supported"); - #[cfg(any(target_os = "linux", target_os = "macos"))] - let file = unsafe { fs::File::from_raw_fd(fd) }; - common_hash_data(file) -} - -pub fn common_hash_file(path: &OsStr) -> String { - let file = match fs::File::open(&path) { - Err(_why) => return hash_null(), - Ok(file) => file - }; - common_hash_data(file) -} diff --git a/src/application/common/hash/hashes/argon2id.rs b/src/application/common/hash/hashes/argon2id.rs new file mode 100644 index 0000000..d2d4fcc --- /dev/null +++ b/src/application/common/hash/hashes/argon2id.rs @@ -0,0 +1,20 @@ +use argon2::PasswordHasher; +use rand::RngCore; +#[macro_use] +build_hash! { ARGON2ID (reader, salt_opt) { + let mut password: String = String::new(); + reader.read_to_string(&mut password).expect("WhiteBeam: Could not read password buffer"); + let salt: String = match salt_opt { + Some(val) => val, + None => { + let mut rng = rand::thread_rng(); + let mut bytes = [0u8; argon2::password_hash::Salt::recommended_len()]; + rng.fill_bytes(&mut bytes); + String::from(argon2::password_hash::SaltString::b64_encode(&bytes).expect("WhiteBeam: Salt string invariant violated").as_str()) + } + }; + // Argon2 with default params (Argon2id v19) + let argon2 = argon2::Argon2::default(); + // Hash password to PHC string ($argon2id$v=19$...) + argon2.hash_password_simple(password.as_bytes(), salt.as_ref()).unwrap().to_string() +}} diff --git a/src/application/common/hash/hashes/blake3.rs b/src/application/common/hash/hashes/blake3.rs new file mode 100644 index 0000000..78814bf --- /dev/null +++ b/src/application/common/hash/hashes/blake3.rs @@ -0,0 +1,20 @@ +#[macro_use] +build_hash! { BLAKE3 (reader, _salt_opt) { + let digestbytes = 32; + let buf_size = 32768; + let mut buf: Vec = Vec::with_capacity(buf_size); + let mut hash_state = blake3::Hasher::new(); + let mut limited_reader = reader.take(buf_size as u64); + loop { + match limited_reader.read_to_end(&mut buf) { + Ok(0) => break, + Ok(_) => { + hash_state.update(&buf[..]); + buf.clear(); + limited_reader = limited_reader.into_inner().take(buf_size as u64); + } + Err(_err) => return "00".repeat(digestbytes), + } + } + hash_state.finalize().to_hex().to_string() +}} diff --git a/src/application/common/hash/hashes/sha3_256.rs b/src/application/common/hash/hashes/sha3_256.rs new file mode 100644 index 0000000..51742ab --- /dev/null +++ b/src/application/common/hash/hashes/sha3_256.rs @@ -0,0 +1,21 @@ +use sha3::Digest; +#[macro_use] +build_hash! { SHA3_256 (reader, _salt_opt) { + let digestbytes = 32; + let buf_size = 32768; + let mut buf: Vec = Vec::with_capacity(buf_size); + let mut hash_state = sha3::Sha3_256::new(); + let mut limited_reader = reader.take(buf_size as u64); + loop { + match limited_reader.read_to_end(&mut buf) { + Ok(0) => break, + Ok(_) => { + hash_state.update(&buf[..]); + buf.clear(); + limited_reader = limited_reader.into_inner().take(buf_size as u64); + } + Err(_err) => return "00".repeat(digestbytes), + } + } + format!("{:x}", hash_state.finalize()) +}} diff --git a/src/application/common/hash/hashes/sha3_512.rs b/src/application/common/hash/hashes/sha3_512.rs new file mode 100644 index 0000000..7621021 --- /dev/null +++ b/src/application/common/hash/hashes/sha3_512.rs @@ -0,0 +1,21 @@ +use sha3::Digest; +#[macro_use] +build_hash! { SHA3_512 (reader, _salt_opt) { + let digestbytes = 64; + let buf_size = 32768; + let mut buf: Vec = Vec::with_capacity(buf_size); + let mut hash_state = sha3::Sha3_512::new(); + let mut limited_reader = reader.take(buf_size as u64); + loop { + match limited_reader.read_to_end(&mut buf) { + Ok(0) => break, + Ok(_) => { + hash_state.update(&buf[..]); + buf.clear(); + limited_reader = limited_reader.into_inner().take(buf_size as u64); + } + Err(_err) => return "00".repeat(digestbytes), + } + } + format!("{:x}", hash_state.finalize()) +}} diff --git a/src/application/common/hash/mod.rs b/src/application/common/hash/mod.rs new file mode 100644 index 0000000..d7608a5 --- /dev/null +++ b/src/application/common/hash/mod.rs @@ -0,0 +1,43 @@ +use std::io::Read; + +pub struct HashObject { + pub alias: &'static str, + pub function: fn(&mut dyn Read, Option) -> String +} + +// Hash template +macro_rules! build_hash { + ($alias:ident ($reader:ident, $salt_opt:ident) $body:block) => { + use std::io::Read; + #[allow(non_snake_case)] + #[allow(unused_assignments)] + #[allow(unused_mut)] + pub fn $alias ($reader: &mut dyn Read, $salt_opt: Option) -> String { + $body + } + #[linkme::distributed_slice(crate::common::hash::HASH_INDEX)] + pub static HASH: crate::common::hash::HashObject = crate::common::hash::HashObject { alias: stringify!($alias), function: $alias }; + }; +} + +// Load hash modules +// TODO: Make sure this doesn't conflict with crate namespace +mod hashes { + automod::dir!(pub "src/application/common/hash/hashes"); +} + +// Collect hashes in distributed slice +#[linkme::distributed_slice] +pub static HASH_INDEX: [HashObject] = [..]; + +pub fn process_hash(reader: &mut dyn Read, algorithm: &str, salt_opt: Option) -> String { + // TODO: Consider removing reference here + match HASH_INDEX.iter().find(|a| format!("Hash/{}", a.alias.replace("_", "-")) == algorithm) { + Some(hash) => {(hash.function)(reader, salt_opt)} + None => panic!("WhiteBeam: Invalid hash algorithm: {}", algorithm) + } +} + +pub fn hash_is_null(input: &str) -> bool { + input.chars().collect::>().iter().all(|&c| c=='0') && (input.len() > 0) +} diff --git a/src/application/common/http.rs b/src/application/common/http.rs deleted file mode 100644 index 3d9bc89..0000000 --- a/src/application/common/http.rs +++ /dev/null @@ -1,408 +0,0 @@ -use std::io::{BufReader, BufWriter, Error, ErrorKind, Read, Write}; -use std::net::{TcpStream, ToSocketAddrs}; -use std::time::Duration; -use std::collections::BTreeMap; -use std::fmt; - -// Based heavily on minreq (https://github.com/neonmoe/minreq) -// One major difference is that WhiteBeam uses BTreeMap instead of a HashMap, otherwise bash -// preloads the Rust cdylib and internally causes a double free (bug), which segfaults WhiteBeam. - -pub type URL = String; - -#[derive(Clone, PartialEq, Debug)] -pub enum Method { - Get, - Post, - Custom(String), -} - -impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Method::Get => write!(f, "GET"), - Method::Post => write!(f, "POST"), - Method::Custom(ref s) => write!(f, "{}", s), - } - } -} - -pub struct Request { - pub method: Method, - pub host: URL, - resource: URL, - headers: BTreeMap, - body: Option, - pub timeout: Option, - pub redirects: Vec, -} - -impl Request { - pub fn new>(method: Method, url: T) -> Request { - let (host, resource) = parse_url(url.into()); - Request { - method, - host, - resource, - headers: BTreeMap::new(), - body: None, - timeout: None, - redirects: Vec::new(), - } - } - - pub fn with_header, U: Into>(mut self, key: T, value: U) -> Request { - self.headers.insert(key.into(), value.into()); - self - } - - pub fn with_body>(mut self, body: T) -> Request { - let body = body.into(); - let body_length = body.len(); - self.body = Some(body); - self.with_header("Content-Length", format!("{}", body_length)) - } - - pub fn with_json( - mut self, - body: &T, - ) -> Result { - self.headers.insert( - "Content-Type".to_string(), - "application/json; charset=UTF-8".to_string(), - ); - Ok(self.with_body(serde_json::to_string(&body)?)) - } - - pub fn with_timeout(mut self, timeout: u64) -> Request { - self.timeout = Some(timeout); - self - } - - pub fn send(self) -> Result { - Connection::new(self).send() - } - - pub fn to_string(&self) -> String { - let mut http = String::new(); - http += &format!( - "{} {} HTTP/1.1\r\nHost: {}\r\n", - self.method, self.resource, self.host - ); - for (k, v) in &self.headers { - http += &format!("{}: {}\r\n", k, v); - } - http += "\r\n"; - if let Some(ref body) = &self.body { - http += body; - } - http - } - - pub fn redirect_to(&mut self, url: URL) { - self.redirects - .push(create_url(&self.host, &self.resource)); - - let (host, resource) = parse_url(url); - self.host = host; - self.resource = resource; - } -} - -pub struct Response { - pub status_code: i32, - pub reason_phrase: String, - pub headers: BTreeMap, - pub body: String, - pub body_bytes: Vec, -} - -impl Response { - pub fn from_bytes(bytes: Vec) -> Response { - let (status_code, reason_phrase) = parse_status_line(&bytes); - let (headers, body_bytes) = parse_http_response_content(&bytes); - Response { - status_code, - reason_phrase, - headers, - body: std::str::from_utf8(&body_bytes).unwrap_or("").to_owned(), - body_bytes, - } - } - - pub fn json<'a, T>(&'a self) -> Result - where - T: serde::de::Deserialize<'a>, - { - serde_json::from_str(&self.body) - } -} - -fn create_url(host: &str, resource: &str) -> URL { - let prefix = "http://"; - return format!("{}{}{}", prefix, host, resource); -} - -fn parse_url(url: URL) -> (URL, URL) { - let mut first = URL::new(); - let mut second = URL::new(); - let mut slashes = 0; - for c in url.chars() { - if c == '/' { - slashes += 1; - } else if slashes == 2 { - first.push(c); - } - if slashes >= 3 { - second.push(c); - } - } - if second.is_empty() { - second += "/"; - } - if !first.contains(':') { - first += ":80"; - } - (first, second) -} - -pub fn parse_status_line(http_response: &[u8]) -> (i32, String) { - let (line, _) = split_at(http_response, "\r\n"); - if let Ok(line) = std::str::from_utf8(line) { - let mut split = line.split(' '); - if let Some(code) = split.nth(1) { - if let Ok(code) = code.parse::() { - if let Some(reason) = split.next() { - return (code, reason.to_string()); - } - } - } - } - (503, "Server did not provide a status line".to_string()) -} - -fn parse_http_response_content(http_response: &[u8]) -> (BTreeMap, Vec) { - let (headers_text, body) = split_at(http_response, "\r\n\r\n"); - - let mut headers = BTreeMap::new(); - let mut status_line = true; - if let Ok(headers_text) = std::str::from_utf8(headers_text) { - for line in headers_text.lines() { - if status_line { - status_line = false; - continue; - } else if let Some((key, value)) = parse_header(line) { - headers.insert(key, value); - } - } - } - - (headers, body.to_vec()) -} - -fn split_at<'a>(bytes: &'a [u8], splitter: &str) -> (&'a [u8], &'a [u8]) { - for i in 0..bytes.len() - splitter.len() { - if let Ok(s) = std::str::from_utf8(&bytes[i..i + splitter.len()]) { - if s == splitter { - return (&bytes[..i], &bytes[i + splitter.len()..]); - } - } - } - (bytes, &[]) -} - -pub fn parse_header(line: &str) -> Option<(String, String)> { - if let Some(index) = line.find(':') { - let key = line[..index].trim().to_string(); - let value = line[(index + 1)..].trim().to_string(); - Some((key, value)) - } else { - None - } -} - -pub struct Connection { - request: Request, - timeout: Option, -} - -impl Connection { - pub fn new(request: Request) -> Connection { - let timeout = request.timeout; - Connection { request, timeout } - } - - pub fn send(self) -> Result { - let bytes = self.request.to_string().into_bytes(); - - let tcp = create_tcp_stream(&self.request.host, self.timeout)?; - - let mut stream = BufWriter::new(tcp); - stream.write_all(&bytes)?; - - let tcp = stream.into_inner()?; - let mut stream = BufReader::new(tcp); - // TODO: Simplify - match read_from_stream(&mut stream, false) { - Ok(response) => handle_redirects(self, Response::from_bytes(response)), - Err(err) => match err.kind() { - ErrorKind::WouldBlock | ErrorKind::TimedOut => Err(Error::new( - ErrorKind::TimedOut, - format!( - "Request timed out! Timeout: {:?}", - stream.get_ref().read_timeout() - ), - )), - _ => Err(err), - }, - } - } -} - -fn handle_redirects(connection: Connection, response: Response) -> Result { - let status_code = response.status_code; - match status_code { - 301 | 302 | 303 | 307 => { - let url = match response.headers.get("Location") { - Some(location) => location, - None => return Err(Error::new( - ErrorKind::Other, - "'Location' header missing in redirect.", - )) - }; - let mut request = connection.request; - - if request.redirects.contains(&url) { - Err(Error::new(ErrorKind::Other, "Infinite redirection loop.")) - } else { - request.redirect_to(url.clone()); - if status_code == 303 { - match request.method { - Method::Post => { - request.method = Method::Get; - } - _ => {} - } - } - - request.send() - } - } - - _ => Ok(response), - } -} - -fn create_tcp_stream(host: A, timeout: Option) -> Result -where - A: ToSocketAddrs, -{ - let stream = TcpStream::connect(host)?; - if let Some(secs) = timeout { - let dur = Some(Duration::from_secs(secs)); - stream.set_read_timeout(dur)?; - stream.set_write_timeout(dur)?; - } - Ok(stream) -} - -fn read_from_stream(stream: T, head: bool) -> Result, Error> { - let mut response = Vec::new(); - let mut response_length = None; - let mut chunked = false; - let mut expecting_chunk_length = false; - let mut byte_count = 0; - let mut last_newline_index = 0; - let mut blank_line = false; - let mut status_code = None; - - for byte in stream.bytes() { - let byte = byte?; - response.push(byte); - byte_count += 1; - if byte == b'\n' { - if status_code.is_none() { - status_code = Some(parse_status_line(&response).0); - } - - if blank_line { - if let Some(code) = status_code { - if head || code / 100 == 1 || code == 204 || code == 304 { - response_length = Some(response.len()); - } - } - if response_length.is_none() { - if let Ok(response_str) = std::str::from_utf8(&response) { - let len = get_response_length(response_str); - response_length = Some(len); - if len > response.len() { - response.reserve(len - response.len()); - } - } - } - } else if let Ok(new_response_length_str) = - std::str::from_utf8(&response[last_newline_index..]) - { - if expecting_chunk_length { - expecting_chunk_length = false; - - if let Ok(n) = usize::from_str_radix(new_response_length_str.trim(), 16) { - response.truncate(last_newline_index); - byte_count = last_newline_index; - if n == 0 { - break; - } else { - response_length = Some(byte_count + n + 2); - } - } - } else if let Some((key, value)) = parse_header(new_response_length_str) { - if key.trim() == "Transfer-Encoding" && value.trim() == "chunked" { - chunked = true; - } - } - } - - blank_line = true; - last_newline_index = byte_count; - } else if byte != b'\r' { - blank_line = false; - } - - if let Some(len) = response_length { - if byte_count >= len { - if chunked { - expecting_chunk_length = true; - } else { - break; - } - } - } - } - - Ok(response) -} - -fn get_response_length(response: &str) -> usize { - let mut byte_count = 0; - for line in response.lines() { - byte_count += line.len() + 2; - if line.starts_with("Content-Length: ") { - if let Ok(length) = line[16..].parse::() { - byte_count += length; - } - } - } - byte_count -} - -pub fn create_request>(method: Method, url: T) -> Request { - Request::new(method, url.into()) -} - -pub fn get>(url: T) -> Request { - create_request(Method::Get, url) -} - -pub fn post>(url: T) -> Request { - create_request(Method::Post, url) -} diff --git a/src/application/common/mod.rs b/src/application/common/mod.rs index d42f709..482edc6 100644 --- a/src/application/common/mod.rs +++ b/src/application/common/mod.rs @@ -6,7 +6,5 @@ pub mod api; pub mod hash; // Time functions pub mod time; -// Service communication -pub mod http; // Public key encryption and signatures pub mod crypto; diff --git a/src/application/main.rs b/src/application/main.rs index cbe5fb2..1d47eff 100644 --- a/src/application/main.rs +++ b/src/application/main.rs @@ -1,9 +1,13 @@ +// TODO: Non-zero exit codes for all errors +// TODO: Update SettingsModified use clap::{Arg, App, AppSettings}; -use cli_table::{format::{CellFormat, Justify}, - Cell, Row, Table}; -use std::ffi::OsStr; -use std::env; -use std::process::Command; +use cli_table::{format::{Justify, Separator}, print_stdout, CellStruct, Cell, Style, Table, TableStruct, Color}; +use std::{env, + error::Error, + ffi::OsStr, + fmt::{self, Debug, Display}, + io::{self, Read}, + process::Command}; pub mod platforms; #[cfg(target_os = "windows")] @@ -15,114 +19,364 @@ use platforms::macos as platform; // Platform independent features pub mod common; -fn run_auth() { +// Support functions +fn valid_auth() -> Result> { // TODO: Log - let password = match rpassword::read_password_from_tty(Some("Password: ")) { - Ok(p) => p, - Err(_e) => { - eprintln!("WhiteBeam: Could not read password"); - return; + let conn: rusqlite::Connection = common::db::db_open(false)?; + if common::db::get_prevention(&conn)? { + if !common::db::get_valid_auth_env(&conn).unwrap_or(false) { + return Ok(false); + } + } + return Ok(true); +} + +// Methods +fn run_add(class: &OsStr, path: &OsStr, value: Option<&OsStr>) -> Result<(), Box> { + if !valid_auth()? { return Err("WhiteBeam: Authorization failed".into()); } + let conn: rusqlite::Connection = common::db::db_open(false)?; + let class_string = String::from(class.to_str().ok_or(String::from("Invalid UTF-8 provided"))?); + let path_string = String::from(path.to_str().ok_or(String::from("Invalid UTF-8 provided"))?); + let algorithm = format!("Hash/{}", common::db::get_setting(&conn, String::from("HashAlgorithm"))?); + let mut added_whitelist_entries: Vec<(String, String, String)> = vec![]; + // Convenience shortcuts occur when value is none + match value { + Some(v) => { + let v_str: &str = v.to_str().ok_or(String::from("Invalid UTF-8 provided"))?; + added_whitelist_entries.push((class_string.clone(), path_string.clone(), String::from(v_str))); + println!("WhiteBeam: Allowing new {} ({}) for {}", &class_string, v_str, &path_string); + }, + None => { + let class_str: &str = &class_string; + match class_str { + "Filesystem/Path/Executable" => { + added_whitelist_entries.push((class_string.clone(), String::from("ANY"), path_string.clone())); + let hash: String = common::hash::process_hash(&mut std::fs::File::open(&path_string)?, &algorithm, None); + if common::hash::hash_is_null(&hash) { + return Err("WhiteBeam: No such file or directory".into()); + } + added_whitelist_entries.push((algorithm.clone(), path_string.clone(), hash.clone())); + let all_library_paths: Vec = platform::recursive_library_scan(&path_string, None, None).unwrap_or(vec![]).iter() + // Always allowed in Essential, no need to whitelist these + .filter(|lib| !(lib.contains("libc.so.6") + ||lib.contains("libdl.so.2") + ||lib.contains("libpthread.so.0") + ||lib.contains("libgcc_s.so.1") + ||lib.contains("librt.so.1") + ||lib.contains("libm.so.6") + ||lib.contains("libwhitebeam") + ||lib.contains("ld-linux"))) + .map(|lib| String::from(lib)) + .collect(); + let all_library_names: Vec = all_library_paths.iter() + .filter_map(|lib| std::path::Path::new(lib).file_name()) + .filter_map(|filename| filename.to_str()) + .map(|filename_str| String::from(filename_str)) + .collect(); + for lib_name in all_library_names.iter() { + added_whitelist_entries.push((String::from("Filesystem/Path/Library"), path_string.clone(), String::from(lib_name))); + } + for lib_path in all_library_paths.iter() { + added_whitelist_entries.push((String::from("Filesystem/Path/Library"), path_string.clone(), String::from(lib_path))); + } + println!("WhiteBeam: Adding {} ({}: {}) to whitelist", &path_string, &algorithm[5..], &hash); + }, + _ => { return Err("WhiteBeam: Missing parameters for 'add' argument".into()); } + } } }; - let conn: rusqlite::Connection = common::db::db_open(); - if !common::db::get_valid_auth_string(&conn, &password) { - eprintln!("WhiteBeam: Authorization failed"); - return; + for entry in added_whitelist_entries.iter() { + let _res = common::db::insert_whitelist(&conn, &(entry.0), &(entry.1), &(entry.2)); + } + Ok(()) +} + +fn run_auth() -> Result<(), Box> { + // TODO: Log + let password: String = rpassword::read_password_from_tty(Some("Password: "))?; + let conn: rusqlite::Connection = common::db::db_open(false)?; + if !common::db::get_valid_auth_string(&conn, &password)? { + return Err("WhiteBeam: Authorization failed".into()); } println!("WhiteBeam: Opening administrative shell"); let mut command = Command::new("/bin/sh"); if let Ok(mut child) = command.env("WB_AUTH", &password) .spawn() { - match child.wait() { - Ok(_c) => (), - Err(_e) => eprintln!("WhiteBeam: Session isn't running") - }; + child.wait()?; println!("WhiteBeam: Session closed"); } else { - eprintln!("WhiteBeam: Administrative shell failed to start"); + return Err("WhiteBeam: Administrative shell failed to start".into()); } + Ok(()) } -fn run_add(program: &OsStr, allow_unsafe: bool) { - // TODO: Log - let conn: rusqlite::Connection = common::db::db_open(); - if common::db::get_enabled(&conn) { - if !common::db::get_valid_auth_env(&conn) { - eprintln!("WhiteBeam: Authorization failed"); - return; - } - } - // TODO: Whitelist more than individual files - let hash: String = common::hash::common_hash_file(program); - if hash == common::hash::hash_null() { - eprintln!("WhiteBeam: No such file or directory"); - return; - } - let program_str = program.to_string_lossy(); - println!("WhiteBeam: Adding {} (SHA-512: {}) to whitelist", &program_str, hash); - common::db::insert_whitelist(&conn, &program_str, allow_unsafe, &hash); +fn run_baseline() -> Result<(), Box> { + // TODO: Filter terminal escape sequences + let conn: rusqlite::Connection = common::db::db_open(false)?; + let table_struct: TableStruct = { + let table: Vec> = common::db::get_baseline(&conn).unwrap_or(Vec::new()).iter() + .map(|entry| vec![ + entry.log.clone().cell(), + entry.total.clone().cell(), + ]) + .collect(); + table.table() + .title(vec![ + "Log".cell().bold(true), + "Total Blocked".cell().bold(true) + ]) + .separator( + Separator::builder() + .title(Some(Default::default())) + .row(None) + .column(Some(Default::default())) + .build(), + ) + }; + Ok(print_stdout(table_struct)?) } -fn run_remove(program: &str) { - // TODO: Log - let conn: rusqlite::Connection = common::db::db_open(); - if common::db::get_enabled(&conn) { - if !common::db::get_valid_auth_env(&conn) { - eprintln!("WhiteBeam: Authorization failed"); - return; +fn run_disable(class: &OsStr) -> Result<(), Box> { + if !valid_auth()? { return Err("WhiteBeam: Authorization failed".into()); } + let conn: rusqlite::Connection = common::db::db_open(false)?; + let class_str = class.to_str().ok_or(String::from("Invalid UTF-8 provided"))?; + println!("WhiteBeam: Disabling hooks in '{}' class", class_str); + let _res = common::db::update_hook_class_enabled(&conn, class_str, false); + Ok(()) +} + +fn run_enable(class: &OsStr) -> Result<(), Box> { + if !valid_auth()? { return Err("WhiteBeam: Authorization failed".into()); } + let conn: rusqlite::Connection = common::db::db_open(false)?; + let class_str = class.to_str().ok_or(String::from("Invalid UTF-8 provided"))?; + println!("WhiteBeam: Enabling hooks in '{}' class", class_str); + let _res = common::db::update_hook_class_enabled(&conn, class_str, true); + Ok(()) +} + +fn run_list(param: &OsStr) -> Result<(), Box> { + // TODO: Zero argument case + // TODO: Add hook class + let conn: rusqlite::Connection = common::db::db_open(false)?; + let param_str = param.to_str().ok_or(String::from("Invalid UTF-8 provided"))?; + let table_struct: TableStruct = match param_str { + "whitelist" => { + // TODO: Highlight path == "ANY" && value == "ANY" in red + // TODO: Highlight writable directories containing an executable or library path in red + let table: Vec> = common::db::get_whitelist(&conn).unwrap_or(Vec::new()).iter() + .map(|entry| vec![ + entry.id.clone().cell(), + entry.class.clone().cell(), + entry.path.clone().cell(), + entry.value.clone().cell() + ]) + .collect(); + table.table() + .title(vec![ + "ID".cell().bold(true), + "Class".cell().bold(true), + "Path".cell().bold(true), + "Value".cell().bold(true) + ]) + .separator( + Separator::builder() + .title(Some(Default::default())) + .row(None) + .column(Some(Default::default())) + .build(), + ) + }, + "hooks" => { + let table: Vec> = common::db::get_hooks_pretty(&conn).unwrap_or(Vec::new()).iter() + .map(|entry| vec![ + entry.id.clone().cell(), + { + let enabled = entry.enabled.clone(); + if enabled { + enabled.cell().foreground_color(Some(Color::Green)) + } else { + enabled.cell().foreground_color(Some(Color::Red)) + } + }, + entry.class.clone().cell().justify(Justify::Center), + entry.library.clone().cell(), + entry.symbol.clone().cell(), + entry.args.clone().cell() + ]) + .collect(); + table.table() + .title(vec![ + "ID".cell().bold(true), + "Enabled".cell().bold(true), + "Class".cell().bold(true).justify(Justify::Center), + "Library".cell().bold(true), + "Symbol".cell().bold(true), + "Arguments".cell().bold(true) + ]) + .separator( + Separator::builder() + .title(Some(Default::default())) + .row(None) + .column(Some(Default::default())) + .build(), + ) + }, + "rules" => { + // TODO: Columns for actions, separate tables for different classes (easier to follow) + let table: Vec> = common::db::get_rules_pretty(&conn).unwrap_or(Vec::new()).iter() + .map(|entry| vec![ + entry.library.clone().cell(), + entry.symbol.clone().cell(), + entry.arg.clone().cell(), + entry.actions.clone().cell() + ]) + .collect(); + table.table() + .title(vec![ + "Library".cell().bold(true), + "Symbol".cell().bold(true), + "Argument".cell().bold(true), + "Actions".cell().bold(true) + ]) + .separator( + Separator::builder() + .title(Some(Default::default())) + .row(None) + .column(Some(Default::default())) + .build(), + ) + }, + _ => { + return Err("WhiteBeam: Invalid parameter for 'list' argument".into()); } - } - common::db::delete_whitelist(&conn, program); + }; + Ok(print_stdout(table_struct)?) } -fn run_list() { - let conn: rusqlite::Connection = common::db::db_open(); - let whitelist = common::db::get_dyn_whitelist(&conn).unwrap_or(Vec::new()); - let justify_right = CellFormat::builder().justify(Justify::Right).build(); - let bold = CellFormat::builder().bold(true).build(); - let mut table_vec: Vec = Vec::new(); - table_vec.push(Row::new(vec![ - Cell::new("Path", bold), - Cell::new("Unsafe Env", bold) - ])); - for result in whitelist { - table_vec.push(Row::new(vec![ - Cell::new(&result.program, Default::default()), - Cell::new(&result.allow_unsafe, justify_right) - ])); +fn run_load(path: &OsStr) -> Result<(), Box> { + match valid_auth() { + Ok(is_valid) => { + if !is_valid { + return Err("WhiteBeam: Authorization failed".into()); + } + } + Err(desc) => { + let desc_str: String = desc.to_string(); + // Allow database to be initialized for the first time + // TODO: Audit denial of service attacks against --load (e.g. max opened files, exhausting memory) + if (desc_str != "No database file found") && + (desc_str != "Query returned no rows") { + return Err(desc); + } + } } - let table = match Table::new(table_vec, cli_table::format::BORDER_COLUMN_TITLE) { - Ok(table_obj) => table_obj, - Err(_e) => { - eprintln!("WhiteBeam: Could not create table"); - return; - } + let conn: rusqlite::Connection = common::db::db_open(true)?; + let path_str = path.to_str().ok_or(String::from("Invalid UTF-8 provided"))?; + let base_version: String = platform::parse_os_version()?; + if (path_str == "stdin") || (path_str == "-") { + println!("WhiteBeam: Loading SQL from standard input"); + let mut buffer = String::new(); + std::io::stdin().read_to_string(&mut buffer)?; + conn.execute_batch(&buffer)?; + return Ok(()); + } + // Try reading a file + if let Ok(buffer) = std::fs::read_to_string(&path) { + println!("WhiteBeam: Loading SQL from local file '{}'", path_str); + conn.execute_batch(&buffer)?; + return Ok(()); + } + // Try loading from repository + let repository = match common::db::get_setting(&conn, String::from("Repository")) { + Ok(repo) => repo, + // TODO: Package Schema, Default, and Essential + Err(_) => String::from("https://github.com/WhiteBeamSec/SQL/blob/master") }; - let _res = table.print_stdout(); + let mut url_common: String = format!("{}/sql/common/{}.sql", repository, path_str); + let mut url_platform: String = format!("{}/sql/platforms/{}/{}.sql", repository, std::env::consts::OS, path_str); + let mut url_base: String = format!("{}/sql/platforms/{}/base/{}.sql", repository, std::env::consts::OS, base_version); + if repository.starts_with("https://github.com/") { + url_common.push_str("?raw=true"); + url_platform.push_str("?raw=true"); + url_base.push_str("?raw=true"); + } + // TODO: Identify ourselves with a user agent + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build()?; + if path_str == "Base" { + // Base whitelist + let response_base = client.get(&url_base).send()?; + if response_base.status().is_success() { + println!("WhiteBeam: Loading '{}' ({}) from repository", path_str, base_version); + let buffer = response_base.text()?; + conn.execute_batch(&buffer)?; + return Ok(()); + } + return Err("WhiteBeam: Failed to load SQL from all available sources".into()); + } + let response_common = client.get(&url_common).send()?; + if response_common.status().is_success() { + println!("WhiteBeam: Loading '{}' from repository", path_str); + let buffer = response_common.text()?; + conn.execute_batch(&buffer)?; + return Ok(()); + } + let response_platform = client.get(&url_platform).send()?; + if response_platform.status().is_success() { + println!("WhiteBeam: Loading '{}' from repository", path_str); + let buffer = response_platform.text()?; + conn.execute_batch(&buffer)?; + return Ok(()) + } else { + return Err("WhiteBeam: Failed to load SQL from all available sources".into()); + } } -async fn run_service() { - common::db::db_optionally_init(); - common::api::serve().await; +fn run_remove(id: u32) -> Result<(), Box> { + if !valid_auth()? { return Err("WhiteBeam: Authorization failed".into()); } + let conn: rusqlite::Connection = common::db::db_open(false)?; + let _res = common::db::delete_whitelist(&conn, id); + Ok(()) } -fn run_enable() { - println!("WhiteBeam: Enabling WhiteBeam"); - let conn: rusqlite::Connection = common::db::db_open(); - common::db::update_config(&conn, "enabled", "true"); +#[tokio::main] +async fn run_service() -> Result<(), Box> { + //common::db::db_optionally_init(); + let conn: rusqlite::Connection = common::db::db_open(false)?; + let service_port: u16 = common::db::get_service_port(&conn)?; + common::api::serve(service_port).await; + Ok(()) } -fn run_disable() { - // TODO: Log - let conn: rusqlite::Connection = common::db::db_open(); - if common::db::get_enabled(&conn) { - if !common::db::get_valid_auth_env(&conn) { - eprintln!("WhiteBeam: Authorization failed"); - return; - } +fn run_setting(param: &OsStr, value: Option<&OsStr>) -> Result<(), Box> { + if !valid_auth()? { return Err("WhiteBeam: Authorization failed".into()); } + let conn: rusqlite::Connection = common::db::db_open(false)?; + let param_str = param.to_str().ok_or(String::from("Invalid UTF-8 provided"))?; + if value.is_none() { + println!("{}", common::db::get_setting(&conn, String::from(param_str))?); + return Ok(()); + } + let mut val: String = match value.unwrap().to_str().ok_or(String::from("Invalid UTF-8 provided"))? { + "mask" => { + let value_orig: String = rpassword::read_password_from_tty(Some("Value: "))?; + let value_confirm: String = rpassword::read_password_from_tty(Some("Confirm: "))?; + if value_orig == value_confirm { + value_orig + } else { + return Err("WhiteBeam: Values did not match".into()); + } + }, + v => String::from(v) + }; + if ((param == "RecoverySecret") || (param == "ConsoleSecret")) && (val != String::from("undefined")) { + let algorithm = format!("Hash/{}", common::db::get_setting(&conn, String::from("SecretAlgorithm"))?); + let mut val_bytes: &[u8] = unsafe { &(val.as_bytes_mut()) }; + let hash: String = common::hash::process_hash(&mut val_bytes, &algorithm, None); + val = hash; } - println!("WhiteBeam: Disabling WhiteBeam"); - common::db::update_config(&conn, "enabled", "false"); + let _res = common::db::update_setting(&conn, param_str, &val); + Ok(()) } fn run_start() { @@ -130,146 +384,177 @@ fn run_start() { platform::start_service(); } -fn run_stop() { - // TODO: Log - let conn: rusqlite::Connection = common::db::db_open(); - if common::db::get_enabled(&conn) { - if !common::db::get_valid_auth_env(&conn) { - eprintln!("WhiteBeam: Authorization failed"); - return; - } +fn run_status() -> Result<(), Box> { + let conn: rusqlite::Connection = common::db::db_open(false)?; + let service_port: u16 = common::db::get_service_port(&conn)?; + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(1)) + .build()?; + if let Ok(_response) = client.get(&format!("http://127.0.0.1:{}/status", service_port)).send() { + println!("WhiteBeam: OK"); + } else { + eprintln!("WhiteBeam: Failed to communicate with WhiteBeam service"); } + Ok(()) +} + +fn run_stop() -> Result<(), Box> { + if !valid_auth()? { return Err("WhiteBeam: Authorization failed".into()); } println!("WhiteBeam: Stopping WhiteBeam service"); platform::stop_service(); + Ok(()) } -fn run_baseline() { - let conn: rusqlite::Connection = common::db::db_open(); - let whitelist = common::db::get_baseline(&conn).unwrap_or(Vec::new()); - let justify_right = CellFormat::builder().justify(Justify::Right).build(); - let bold = CellFormat::builder().bold(true).build(); - let mut table_vec: Vec = Vec::new(); - table_vec.push(Row::new(vec![ - Cell::new("Path", bold), - Cell::new("Total Blocked", bold) - ])); - for result in whitelist { - table_vec.push(Row::new(vec![ - Cell::new(&result.program, Default::default()), - Cell::new(&result.total_blocked, justify_right) - ])); +pub struct MainError(Box); + +impl>> From for MainError { + fn from(e: E) -> Self { + MainError(e.into()) } - let table = match Table::new(table_vec, cli_table::format::BORDER_COLUMN_TITLE) { - Ok(table_obj) => table_obj, - Err(_e) => { - eprintln!("WhiteBeam: Could not create table"); - return; - } - }; - let _res = table.print_stdout(); } -fn run_status() { - if let Ok(_response) = common::http::get("http://127.0.0.1:11998/status") - .with_timeout(1) - .send() { - println!("WhiteBeam: OK"); - } else { - eprintln!("WhiteBeam: Failed to communicate with WhiteBeam service"); +impl Debug for MainError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.0, f)?; + let mut source = self.0.source(); + while let Some(error) = source { + write!(f, "\nCaused by: {}", error)?; + source = error.source(); + } + Ok(()) } } -#[tokio::main] -async fn main() { +fn main() -> Result<(), MainError> { + // TODO: List enabled/disabled hook classes or individual hooks let matches = App::new("WhiteBeam") .setting(AppSettings::ArgRequiredElseHelp) .version(env!("CARGO_PKG_VERSION")) - .about("Open source EDR ( https://github.com/WhiteBeamSec/WhiteBeam )") - .arg(Arg::with_name("auth") - .long("auth") - .takes_value(false) - .help("Authenticate for access to privileged commands")) + .about("https://github.com/WhiteBeamSec/WhiteBeam") .arg(Arg::with_name("add") .long("add") .takes_value(true) - .help("Add a whitelisted path or executable (+auth when enabled)") + .multiple(true) + .help("Add policy to whitelist (+auth with Prevention)") .value_name("path")) - .arg(Arg::with_name("unsafe") - .long("unsafe") + .arg(Arg::with_name("auth") + .long("auth") .takes_value(false) - .help("Allow use of unsafe environment variables (with --add, +auth when enabled)")) - .arg(Arg::with_name("remove") - .long("remove") + .help("Authenticate for access to privileged commands")) + .arg(Arg::with_name("baseline") + .long("baseline") + .takes_value(false) + .help("View statistics of failed operations")) + .arg(Arg::with_name("disable") + .long("disable") .takes_value(true) - .help("Remove a whitelisted path or executable (+auth when enabled)") - .value_name("path")) + .help("Disable a class of hooks (+auth with Prevention)")) + .arg(Arg::with_name("enable") + .long("enable") + .takes_value(true) + .help("Enable a class of hooks (+auth with Prevention)")) .arg(Arg::with_name("list") .long("list") - .takes_value(false) - .help("View whitelist policy on this host")) + .takes_value(true) + .help("List hooks, rules, or whitelist policy on this host")) + .arg(Arg::with_name("load") + .long("load") + .takes_value(true) + .help("Load SQL from standard input, a file, or repository (+auth with Prevention)")) + .arg(Arg::with_name("remove") + .long("remove") + .takes_value(true) + .help("Remove a whitelist rule by id (+auth with Prevention)") + .value_name("id")) .arg(Arg::with_name("service") .long("service") .takes_value(false) .hidden(true)) - .arg(Arg::with_name("enable") - .long("enable") - .takes_value(false) - .help("Enable application whitelisting")) - .arg(Arg::with_name("disable") - .long("disable") - .takes_value(false) - .help("Disable application whitelisting (+auth)")) + .arg(Arg::with_name("setting") + .long("setting") + .takes_value(true) + .multiple(true) + .help("Modify or view WhiteBeam client settings (+auth with Prevention)")) .arg(Arg::with_name("start") .long("start") .takes_value(false) .help("Start the WhiteBeam service")) - .arg(Arg::with_name("stop") - .long("stop") - .takes_value(false) - .help("Stop the WhiteBeam service (+auth)")) - .arg(Arg::with_name("baseline") - .long("baseline") - .takes_value(false) - .help("Print execution statistics for non-whitelisted binaries")) .arg(Arg::with_name("status") .long("status") .takes_value(false) .help("View status of the WhiteBeam client")) + .arg(Arg::with_name("stop") + .long("stop") + .takes_value(false) + .help("Stop the WhiteBeam service (+auth with Prevention)")) .get_matches(); - if matches.is_present("auth") { - run_auth(); - } else if matches.is_present("add") { - match matches.value_of_os("add") { - Some(path) => run_add(path, matches.is_present("unsafe")), + if matches.is_present("add") { + match matches.values_of_os("add") { + Some(vals) => { + let mut vals_iter = vals.clone(); + // TODO: Refactor + if vals_iter.len() == 3 { + // TODO: Error handling + let class: &OsStr = vals_iter.next().ok_or(String::from("Missing class for 'add' argument"))?; + let path: &OsStr = vals_iter.next().ok_or(String::from("Missing path for 'add' argument"))?; + let value: &OsStr = vals_iter.next().ok_or(String::from("Missing value for 'add' argument"))?; + run_add(class, path, Some(value))? + } else if vals_iter.len() == 2 { + let class: &OsStr = vals_iter.next().ok_or(String::from("Missing class for 'add' argument"))?; + let path: &OsStr = vals_iter.next().ok_or(String::from("Missing path for 'add' argument"))?; + run_add(class, path, None)? + } else { + return Err("WhiteBeam: Insufficient parameters for 'add' argument".into()); + } + }, None => { - eprintln!("WhiteBeam: Missing value for 'add' argument"); - return; + return Err("WhiteBeam: Missing parameters for 'add' argument".into()); } }; + } else if matches.is_present("auth") { + run_auth()?; + } else if matches.is_present("baseline") { + run_baseline()?; + } else if matches.is_present("disable") { + run_disable(matches.value_of_os("disable").ok_or(String::from("WhiteBeam: Missing parameter for 'disable' argument"))?)?; + } else if matches.is_present("enable") { + run_enable(matches.value_of_os("enable").ok_or(String::from("WhiteBeam: Missing parameter for 'enable' argument"))?)?; + } else if matches.is_present("list") { + run_list(matches.value_of_os("list").ok_or(String::from("WhiteBeam: Missing parameter for 'list' argument"))?)?; + } else if matches.is_present("load") { + run_load(matches.value_of_os("load").unwrap_or(OsStr::new("stdin")))?; } else if matches.is_present("remove") { - match matches.value_of("remove") { - Some(path) => run_remove(path), + run_remove(matches.value_of("remove").ok_or(String::from("WhiteBeam: Missing parameter for 'remove' argument"))?.parse::()?)?; + } else if matches.is_present("service") { + run_service(); + } else if matches.is_present("setting") { + match matches.values_of_os("setting") { + Some(vals) => { + let mut vals_iter = vals.clone(); + // TODO: Refactor + if vals_iter.len() == 2 { + // TODO: Error handling + let param: &OsStr = vals_iter.next().ok_or(String::from("Missing parameter for 'setting' argument"))?; + let value: &OsStr = vals_iter.next().ok_or(String::from("Missing value for 'setting' argument"))?; + run_setting(param, Some(value))? + } else if vals_iter.len() == 1 { + let param: &OsStr = vals_iter.next().ok_or(String::from("Missing parameter for 'setting' argument"))?; + run_setting(param, None)? + } else { + return Err("WhiteBeam: Insufficient parameters for 'setting' argument".into()); + } + }, None => { - eprintln!("WhiteBeam: Missing value for 'remove' argument"); - return; + return Err("WhiteBeam: Missing parameters for 'setting' argument".into()); } }; - } else if matches.is_present("list") { - run_list(); - } else if matches.is_present("service") { - run_service().await; - } else if matches.is_present("enable") { - run_enable(); - } else if matches.is_present("disable") { - run_disable(); } else if matches.is_present("start") { run_start(); - } else if matches.is_present("stop") { - run_stop(); - } else if matches.is_present("baseline") { - run_baseline(); } else if matches.is_present("status") { - run_status(); + run_status()?; + } else if matches.is_present("stop") { + run_stop()?; } + Ok(()) } diff --git a/src/application/platforms/linux/mod.rs b/src/application/platforms/linux/mod.rs index 95338e1..1adaa83 100644 --- a/src/application/platforms/linux/mod.rs +++ b/src/application/platforms/linux/mod.rs @@ -1,4 +1,7 @@ -use std::fs::File; +use goblin::elf; +use std::{error::Error, + fs::File, + io::BufRead}; use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; @@ -31,16 +34,122 @@ pub fn stop_service() { } pub fn get_data_file_path(data_file: &str) -> PathBuf { + #[cfg(feature = "whitelist_test")] + let data_path: String = format!("{}/target/release/examples/", env!("PWD")); + #[cfg(not(feature = "whitelist_test"))] let data_path: String = String::from("/opt/WhiteBeam/data/"); let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() + PathBuf::from(data_file_path) } -pub fn path_open_secure(file_path: &Path) -> File { - std::fs::OpenOptions::new() +pub fn path_open_secure(file_path: &Path) -> Result { + Ok(std::fs::OpenOptions::new() .create(true) .write(true) .mode(0o700) - .open(file_path) - .expect(&format!("WhiteBeam: Could not securely open path {}", file_path.to_string_lossy())) + .open(file_path)?) +} + + +pub fn parse_ld_so_conf(path: &str) -> std::io::Result> { + let mut paths = Vec::new(); + + let f = File::open(path)?; + let f = std::io::BufReader::new(&f); + for line in f.lines() { + let line = line?; + let line = line.trim(); + if line.starts_with("#") { + continue; + } + if line == "" { + continue; + } + + if line.contains(" ") { + if line.starts_with("include ") { + for entry in glob::glob(line.split(" ").last().unwrap()).expect("Failed to read glob pattern") { + paths.extend(parse_ld_so_conf(&entry.unwrap().to_string_lossy().into_owned())?); + } + + } + } else { + paths.push(line.to_owned()); + } + } + Ok(paths) +} + +pub fn library_scan(elf_path_str: &str, search_paths: Vec) -> Result, Box> { + // TODO: DT_RPATH/DT_RUNPATH + let elf_path = std::path::Path::new(&elf_path_str); + let elf_buffer = std::fs::read(elf_path)?; + let elf_parsed = elf::Elf::parse(&elf_buffer)?; + let mut collected_library_paths: Vec = vec![]; + let collected_libraries: Vec = elf_parsed.libraries.iter().map(|s| s.to_string()).collect(); + for lib in collected_libraries.iter() { + for search_path in search_paths.iter() { + let search_path_string = format!("{}/{}", &search_path, &lib); + let search_path_expanded = std::path::Path::new(&search_path_string); + if search_path_expanded.exists() { + collected_library_paths.push(search_path_string); + break + } + } + } + Ok(collected_library_paths) +} + +pub fn recursive_library_scan(elf_path_str: &str, collected_library_paths_opt: Option>, search_paths_opt: Option>) -> Result, Box> { + // Recursively collect DT_NEEDED libraries + let search_paths: Vec = match search_paths_opt { + Some(paths) => paths, + None => { + // TODO: Missing default library_paths for 64 bit? + parse_ld_so_conf("/etc/ld.so.conf").unwrap_or(vec![String::from("/lib"), String::from("/usr/lib")]) + } + }; + let mut collected_library_paths: Vec = match collected_library_paths_opt { + Some(lib_paths) => lib_paths, + None => { + vec![] + } + }; + let elf_library_paths: Vec = library_scan(elf_path_str, search_paths.clone())?; + for lib_path in elf_library_paths.iter() { + if collected_library_paths.contains(lib_path) { + continue; + } + collected_library_paths.push(String::from(lib_path)); + for lib_dep in recursive_library_scan(lib_path, Some(collected_library_paths.clone()), Some(search_paths.clone()))?.iter() { + if collected_library_paths.contains(lib_dep) { + continue; + } + collected_library_paths.push(String::from(lib_dep)); + } + } + Ok(collected_library_paths) +} + +pub fn parse_os_version() -> Result> { + let file = std::fs::read_to_string("/etc/os-release")?; + let mut distro = String::from(""); + let mut version = String::from(""); + for line in file.lines() { + if line.starts_with("ID=") { + distro = os_version_value(line)?; + } else if line.starts_with("VERSION_ID=") { + version = os_version_value(line)?; + } + } + Ok(format!("{}_{}", distro, version)) +} + +fn os_version_value(line: &str) -> Result> { + let idx = line.find('=').unwrap(); + let mut value = &line[idx+1..]; + if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 { + value = &value[1..value.len()-1]; + } + Ok(value.to_string()) } diff --git a/src/application/platforms/macos/mod.rs b/src/application/platforms/macos/mod.rs index a34a692..de59786 100644 --- a/src/application/platforms/macos/mod.rs +++ b/src/application/platforms/macos/mod.rs @@ -10,9 +10,12 @@ pub fn stop_service() { } pub fn get_data_file_path(data_file: &str) -> PathBuf { + #[cfg(feature = "whitelist_test")] + let data_path: String = format!("{}/target/release/examples/", env!("PWD")); + #[cfg(not(feature = "whitelist_test"))] let data_path: String = String::from("/Applications/WhiteBeam/data/"); let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() + PathBuf::from(data_file_path) } pub fn path_open_secure(file_path: &Path) { diff --git a/src/application/platforms/windows/mod.rs b/src/application/platforms/windows/mod.rs index b79de73..f8396f5 100644 --- a/src/application/platforms/windows/mod.rs +++ b/src/application/platforms/windows/mod.rs @@ -11,11 +11,15 @@ pub fn stop_service() { } pub fn get_data_file_path(data_file: &str) -> PathBuf { - // TODO: Change this when registry and environment are secured - //Path::new(env::var("ProgramFiles").unwrap_or("C:\\ProgramFiles").push_str("\\WhiteBeam\\data\\")) + // TODO: Use PWD for Powershell with feature="whitelist_test"? + // TODO: May change this when registry and environment are secured + //PathBuf::from(env::var("ProgramFiles").unwrap_or("C:\\ProgramFiles").push_str("\\WhiteBeam\\data\\")) + #[cfg(feature = "whitelist_test")] + let data_path: String = format!("{}\\target\\release\\examples\\", env!("CD")); + #[cfg(not(feature = "whitelist_test"))] let data_path: String = String::from("C:\\Program Files\\WhiteBeam\\data\\"); let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() + PathBuf::from(data_file_path) } pub fn path_open_secure(file_path: &Path) { diff --git a/src/installer/Cargo.toml b/src/installer/Cargo.toml index 3da863e..7a7abc8 100644 --- a/src/installer/Cargo.toml +++ b/src/installer/Cargo.toml @@ -1,7 +1,7 @@ # General info [package] name = "whitebeam-installer" -version = "0.1.3" +version = "0.2.0" authors = ["WhiteBeam Security, Inc."] edition = "2018" @@ -13,8 +13,3 @@ path = "main.rs" # Cross-platform dependencies [dependencies] libc = { version = "0.2" } - -# Windows dependencies -[target.'cfg(target_os = "windows")'.dependencies.kernel32-sys] -version = "0.2" -default-features = false diff --git a/src/installer/common/db.rs b/src/installer/common/db.rs new file mode 100644 index 0000000..7aaa485 --- /dev/null +++ b/src/installer/common/db.rs @@ -0,0 +1,44 @@ +#[cfg(target_os = "windows")] +use crate::platforms::windows as platform; +#[cfg(target_os = "linux")] +use crate::platforms::linux as platform; +#[cfg(target_os = "macos")] +use crate::platforms::macos as platform; +use std::{error::Error, + io::Write, + path::PathBuf, + process::Command, + process::Stdio}; + +fn db_init() -> Result<(), Box> { + db_load("Schema")?; + db_load("Default")?; + Ok(()) +} + +pub fn db_optionally_init(release: &str) -> Result<(), Box> { + let is_test: bool = release == "test"; + let db_path: PathBuf = platform::get_data_file_path("database.sqlite", release); + // Always reinitialize database for testing + if is_test && (&db_path).exists() { + std::fs::remove_file(&db_path)?; + } + let run_init: bool = is_test || !((&db_path).exists()); + if run_init { + // TODO: Log errors + db_init()? + } + Ok(()) +} + +pub fn db_load(sql_path: &str) -> std::io::Result<()> { + let bin_target_path: PathBuf = PathBuf::from(format!("{}/target/release/whitebeam", env!("PWD"))); + let mut child = Command::new(bin_target_path).args(&["--load", sql_path]).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; + // TODO: _output, debugging information follows: + let output = child.wait_with_output()?; + print!("stdout: {}", std::str::from_utf8(&output.stdout).unwrap()); + if output.stderr.len() > 0 { + eprint!("stderr: {}", std::str::from_utf8(&output.stderr).unwrap()); + } + Ok(()) +} diff --git a/src/installer/common/mod.rs b/src/installer/common/mod.rs new file mode 100644 index 0000000..6eb7c56 --- /dev/null +++ b/src/installer/common/mod.rs @@ -0,0 +1,2 @@ +// Database +pub mod db; diff --git a/src/installer/main.rs b/src/installer/main.rs index 5c55044..a930115 100644 --- a/src/installer/main.rs +++ b/src/installer/main.rs @@ -1,8 +1,11 @@ +// TODO: Cross platform, tests, replace install.sh, add sqlite config for osquery/osquery pkgs + use std::{env, ffi::OsStr, fs, - path::Path, + path::PathBuf, process::Command}; +pub mod common; pub mod platforms; #[cfg(target_os = "windows")] use platforms::windows as platform; @@ -28,115 +31,101 @@ pub fn pretty_bytes(num: f64) -> String { } fn build(args: Vec) { + // TODO: Consistent naming: binary and application platform::check_build_environment(); - let (mut compile_bin, mut compile_lib) = (true, true); - if (args.len()-1) > 1 { - let subcommand: &str = &(&args[2].to_lowercase()); - match subcommand { - "binary" => { - compile_lib = false; - }, - "library" => { - compile_bin = false; - }, - _ => { - eprintln!("WhiteBeam: Invalid subcommand. Valid subcommands are: binary library"); - return; - } - } + if args.len() <= 2 { + // By default, build both the release library and binary + build(vec![String::from("whitebeam-installer"), String::from("build"), String::from("binary")]); + build(vec![String::from("whitebeam-installer"), String::from("build"), String::from("library")]); + return; } - if compile_lib { - println!("Building library"); - let _exit_status_lib = Command::new("cargo") - .arg("+nightly").arg("build").arg("--package").arg("libwhitebeam").arg("--lib").arg("--release") - .env("RUSTFLAGS", "-C link-arg=-s") - .status() - .expect("Failed to execute cargo command"); - match fs::metadata("./target/release/libwhitebeam.so") { - Ok(meta) => println!("Completed. Size: {}", pretty_bytes(meta.len() as f64)), - Err(_e) => println!("Failed to stat ./target/release/libwhitebeam.so") - } - } - if compile_bin { - let _exit_status_bin = Command::new("cargo") - .arg("+stable").arg("build").arg("--package").arg("whitebeam").arg("--bin").arg("whitebeam").arg("--release") - .env("RUSTFLAGS", "-C link-arg=-s") - .status() - .expect("Failed to execute cargo command"); - match fs::metadata("./target/release/whitebeam") { - Ok(meta) => println!("Completed. Size: {}", pretty_bytes(meta.len() as f64)), - Err(_e) => println!("Failed to stat ./target/release/whitebeam") + // TODO: Replace with https://github.com/rust-lang/cargo/blob/master/src/doc/src/reference/unstable.md#profile-strip-option once stabilized + let mut cargo_command = Command::new("cargo"); + cargo_command.env("RUSTFLAGS", "-C link-arg=-s"); + let lib_target_path: PathBuf = PathBuf::from(format!("{}/target/release/libwhitebeam.so", env!("PWD"))); + let bin_target_path: PathBuf = PathBuf::from(format!("{}/target/release/whitebeam", env!("PWD"))); + let subcommand: &str = &(&args[2].to_lowercase()); + let current_target_path = match subcommand { + "binary" => { + println!("Building binary"); + cargo_command.args(&["build", "--package", "whitebeam", "--bin", "whitebeam", "--release"]); + bin_target_path + }, + "library" => { + println!("Building library"); + cargo_command.args(&["+nightly", "build", "--package", "libwhitebeam", "--lib", "--release"]); + lib_target_path + }, + "binary-test" => { + println!("Building test binary"); + cargo_command.args(&["build", "--package", "whitebeam", "--bin", "whitebeam", "--release", + "--manifest-path", "./src/application/Cargo.toml", "--features", "whitelist_test"]); + bin_target_path + }, + "library-test" => { + println!("Building test library"); + cargo_command.args(&["+nightly", "build", "--package", "libwhitebeam", "--lib", "--release", + "--manifest-path", "./src/library/Cargo.toml", "--features", "whitelist_test"]); + lib_target_path + }, + _ => { + eprintln!("WhiteBeam: Invalid subcommand. Valid subcommands are: binary library binary-test library-test"); + return; } + }; + cargo_command.status().expect("WhiteBeam: Failed to execute cargo command"); + match fs::metadata(¤t_target_path) { + Ok(meta) => println!("WhiteBeam: Completed. Size: {}", pretty_bytes(meta.len() as f64)), + Err(_e) => println!("WhiteBeam: Failed to stat {}", (¤t_target_path).display()) } } -fn test(_args: Vec) { - // TODO: Verify we're in the right directory - println!("Building test library"); - let _exit_status_lib = Command::new("cargo") - .arg("+nightly").arg("build").arg("--package").arg("libwhitebeam").arg("--lib").arg("--release") - // Arguments for testing - .arg("--manifest-path").arg("./src/library/Cargo.toml").arg("--features").arg("whitelist_test") - .env("RUSTFLAGS", "-C link-arg=-s") - .status() - .expect("Failed to execute cargo command"); - match fs::metadata("./target/release/libwhitebeam.so") { - Ok(meta) => println!("Completed. Size: {}", pretty_bytes(meta.len() as f64)), - Err(_e) => println!("Failed to stat ./target/release/libwhitebeam.so") - } - let libwhitebeam_file = Command::new(platform::search_path(OsStr::new("file")).unwrap()) - .arg("./target/release/libwhitebeam.so") - .output() - .expect("Failed to execute file command"); - println!("{}", String::from_utf8_lossy(&libwhitebeam_file.stdout).trim_end()); - println!("Exported symbols:"); - let libwhitebeam_objdump = Command::new(platform::search_path(OsStr::new("objdump")).unwrap()) - .arg("-T").arg("-j").arg(".text").arg("./target/release/libwhitebeam.so") - .output() - .expect("Failed to execute objdump command"); - let libwhitebeam_objdump_string = String::from_utf8_lossy(&libwhitebeam_objdump.stdout); - let mut modules: Vec<&str> = Vec::new(); - for line in libwhitebeam_objdump_string.lines() { - if line.contains(".text") && !line.contains("rust_eh_personality") { - modules.push(line.split_ascii_whitespace().last().unwrap()); - } - } - for module in &modules { - println!("* {}", module); - } +fn test(args: Vec) { + // TODO: More error handling + build(vec![String::from("whitebeam-installer"), String::from("build"), String::from("library-test")]); + build(vec![String::from("whitebeam-installer"), String::from("build"), String::from("binary-test")]); println!("Testing:"); + // Initialize test database + common::db::db_optionally_init(&args[1].to_lowercase()).expect("WhiteBeam: Failed to initialize test database"); + // Load platform-specific Essential hooks through whitebeam command + common::db::db_load("Essential").expect("WhiteBeam: Failed to load Essential hooks"); + // Load platform-specific test data through whitebeam command + common::db::db_load("Test").expect("WhiteBeam: Failed to load test data"); + // Compile tests let _exit_status_tests = Command::new("cargo") - .arg("+stable").arg("build").arg("--package").arg("libwhitebeam-tests").arg("--release") - .env("RUSTFLAGS", "-C link-arg=-s") + .arg("build").arg("--package").arg("libwhitebeam-tests").arg("--release") + // TODO: Replace with https://github.com/rust-lang/cargo/blob/master/src/doc/src/reference/unstable.md#profile-strip-option once stabilized + .env("RUSTFLAGS", "-C link-arg=-s -Z plt=yes") .status() - .expect("Failed to execute cargo command"); - for module in &modules { - // TODO: fexecve in Linux tests - if module == &"fexecve" { - eprintln!("Skipping fexecve"); - continue; - } - for test_type in &["positive", "negative"] { - let exit_status_module = Command::new("./target/release/libwhitebeam-tests") - .arg(module).arg(test_type) - .env("LD_PRELOAD", "./target/release/libwhitebeam.so") - .status() - .expect("Failed to execute cargo command"); - // TODO: Use OS temp directory/directory relative to cwd instead of hardcoding /tmp/ - if test_type == &"positive" { - // Positive test - assert!(exit_status_module.success()); - let contents = fs::read_to_string("/tmp/test_result").expect("Could not read test result file"); - assert_eq!(contents, String::from("./target/release/libwhitebeam.so")); - fs::remove_file("/tmp/test_result").expect("Failed to remove /tmp/test_result"); - } else { - // Negative test - // TODO: assert!(!exit_status_module.success()); - assert_eq!(Path::new("/tmp/test_result").exists(), false); - } - println!("{} passed ({} test).", module, test_type); - } - } + .expect("WhiteBeam: Failed to execute cargo command"); + // Set a test recovery secret + let _exit_status_secret = Command::new(format!("{}/target/release/whitebeam", env!("PWD"))) + .args(&["--setting", "RecoverySecret", "test"]) + .status() + .expect("WhiteBeam: Failed to execute whitebeam command"); + // Enable prevention + let _exit_status_prevention = Command::new(format!("{}/target/release/whitebeam", env!("PWD"))) + .args(&["--setting", "Prevention", "true"]) + .status() + .expect("WhiteBeam: Failed to execute whitebeam command"); + // Run tests + let _exit_status_tests = Command::new(&format!("{}/target/release/libwhitebeam-tests", env!("PWD"))) + .env("LD_PRELOAD", &format!("{}/target/release/libwhitebeam.so", env!("PWD"))) + .status() + .expect("WhiteBeam: Failed to execute libwhitebeam-tests command"); + // Disable prevention + let _exit_status_disable = Command::new(format!("{}/target/release/whitebeam", env!("PWD"))) + .args(&["--setting", "Prevention", "false"]) + .env("WB_AUTH", "test") + .status() + .expect("WhiteBeam: Failed to execute whitebeam command"); + // Reset recovery secret + let _exit_status_reset = Command::new(format!("{}/target/release/whitebeam", env!("PWD"))) + .args(&["--setting", "RecoverySecret", "undefined"]) + .status() + .expect("WhiteBeam: Failed to execute whitebeam command"); + // TODO: Test actions + // TODO: Make sure SQL schema/defaults exist // TODO: Test binary (e.g. ./target/release/whitebeam || true) } @@ -150,8 +139,8 @@ fn clean(_args: Vec) { let _clean_result = Command::new(platform::search_path(OsStr::new("cargo")).unwrap()) .arg("clean") .output() - .expect("Failed to execute cargo command"); - fs::remove_file("Cargo.lock").expect("Failed to remove Cargo.lock"); + .expect("WhiteBeam: Failed to execute cargo command"); + fs::remove_file("Cargo.lock").expect("WhiteBeam: Failed to remove Cargo.lock"); } fn main() { diff --git a/src/installer/platforms/linux/mod.rs b/src/installer/platforms/linux/mod.rs index a808880..28cc613 100644 --- a/src/installer/platforms/linux/mod.rs +++ b/src/installer/platforms/linux/mod.rs @@ -2,16 +2,17 @@ use std::{env, ffi::OsStr, ffi::OsString, - fs, os::unix::ffi::OsStrExt, - path::Path, path::PathBuf, process::Command}; -pub fn get_data_file_path(data_file: &str) -> PathBuf { - let data_path: String = String::from("/opt/WhiteBeam/data/"); +pub fn get_data_file_path(data_file: &str, release: &str) -> PathBuf { + let data_path: String = match release { + "test" => format!("{}/target/release/examples/", env!("PWD")), + _ => String::from("/opt/WhiteBeam/data/") + }; let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() + PathBuf::from(data_file_path) } pub fn get_current_uid() -> u32 { @@ -57,31 +58,34 @@ pub fn check_build_environment() { let missing_requirement = requirement.to_string_lossy(); // Give general advice for how to satisfy the missing requirement if missing_requirement == "cc" { - eprintln!("cc not found in PATH, consider running: apt update && apt install -y build-essential"); + eprintln!("WhiteBeam: cc not found in PATH, consider running: apt update && apt install -y build-essential"); } else if missing_requirement == "rustup" { - eprintln!("rustup not found in PATH, consider running: wget -q --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs -O - | sh /dev/stdin -y && source $$HOME/.cargo/env"); + eprintln!("WhiteBeam: rustup not found in PATH, consider running: wget -q --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs -O - | sh /dev/stdin -y && source $$HOME/.cargo/env"); } else if missing_requirement == "pkg-config" { - eprintln!("pkg-config not found in PATH, consider running: apt update && apt install -y pkg-config libssl-dev"); + eprintln!("WhiteBeam: pkg-config not found in PATH, consider running: apt update && apt install -y pkg-config libssl-dev"); } else { // Reserved for future dependencies - eprintln!("{} not found in PATH", missing_requirement); + eprintln!("WhiteBeam: {} not found in PATH", missing_requirement); } std::process::exit(1); } } + // Toolchains can be more than just "stable" and "nightly" (Docker containers use the Rust version number) + /* let rustup_toolchains = Command::new(search_path(OsStr::new("rustup")).unwrap()) .arg("toolchain") .arg("list") .output() - .expect("Failed to execute rustup command"); + .expect("WhiteBeam: Failed to execute rustup command"); let rustup_toolchains_string = String::from_utf8_lossy(&rustup_toolchains.stdout); if !rustup_toolchains_string.contains("stable") { - eprintln!("No stable Rust found in toolchain, consider running: rustup toolchain install stable"); + eprintln!("WhiteBeam: No stable Rust found in toolchain, consider running: rustup toolchain install stable"); std::process::exit(1); } else if !rustup_toolchains_string.contains("nightly") { - eprintln!("No nightly Rust found in toolchain, consider running: rustup toolchain install nightly"); + eprintln!("WhiteBeam: No nightly Rust found in toolchain, consider running: rustup toolchain install nightly"); std::process::exit(1); } + */ } pub fn run_install() { @@ -89,96 +93,36 @@ pub fn run_install() { let sudo_path = match search_path(OsStr::new("sudo")) { Some(path) => path, None => { - eprintln!("Insufficient privileges for installation of WhiteBeam and no sudo present"); + eprintln!("WhiteBeam: Insufficient privileges for installation of WhiteBeam and no sudo present"); return; } }; - let program = env::current_exe().expect("Failed to determine path to current executable"); + let program = env::current_exe().expect("WhiteBeam: Failed to determine path to current executable"); Command::new(sudo_path) .arg(program) .arg("install") - .status().expect("Child process failed to start."); + .status().expect("WhiteBeam: Child process failed to start."); return; } println!("Installing"); - let service = r#"#!/bin/bash -# WhiteBeam service -# chkconfig: 345 20 80 -# description: WhiteBeam service -# processname: whitebeam - -SERVICE_PATH="/opt/WhiteBeam/" - -SERVICE=whitebeam -SERVICEOPTS="--service" - -NAME=whitebeam -DESC="WhiteBeam service" -PIDFILE=/opt/WhiteBeam/data/$NAME.pid -SCRIPTNAME=/etc/init.d/$NAME - -case "$1" in -start) - printf "%-50s" "Starting $NAME..." - cd $SERVICE_PATH - PID=`$SERVICE $SERVICEOPTS > /dev/null 2>&1 & echo $!` - #echo "Saving PID" $PID " to " $PIDFILE - if [ -z $PID ]; then - printf "%s\n" "Fail" - else - echo $PID > $PIDFILE - printf "%s\n" "Ok" - fi -;; -status) - printf "%-50s" "Checking $NAME..." - if [ -f $PIDFILE ]; then - PID=`cat $PIDFILE` - if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then - printf "%s\n" "Process dead but pidfile exists" - else - echo "Running" - fi - else - printf "%s\n" "Service not running" - fi -;; -stop) - printf "%-50s" "Stopping $NAME" - PID=`cat $PIDFILE` - cd $SERVICE_PATH - if [ -f $PIDFILE ]; then - kill -HUP $PID - printf "%s\n" "Ok" - rm -f $PIDFILE - else - printf "%s\n" "pidfile not found" - fi -;; - -restart) - $0 stop - $0 start -;; - -*) - echo "Usage: $0 {status|start|stop|restart}" - exit 1 -esac"#; - fs::write("/etc/init.d/whitebeam", service).expect("Unable to add WhiteBeam service"); // TODO: Use Rust instead of coreutils Command::new(search_path(OsStr::new("bash")).unwrap()) .arg("-c") - .arg("mkdir -p /opt/WhiteBeam/; - cp ./target/release/whitebeam /opt/WhiteBeam/whitebeam; + .arg("mkdir -p /opt/WhiteBeam/data/; + cp ./src/installer/platforms/linux/resources/service.sh /etc/init.d/whitebeam; cp ./target/release/libwhitebeam.so /opt/WhiteBeam/libwhitebeam.so; - mkdir /opt/WhiteBeam/data/; + cp ./target/release/whitebeam /opt/WhiteBeam/whitebeam; + ln -s /etc/init.d/whitebeam /etc/rc3.d/S01whitebeam; + ln -s /opt/WhiteBeam/libwhitebeam.so /lib/libwhitebeam.so; ln -s /opt/WhiteBeam/whitebeam /usr/local/bin/whitebeam; chmod 775 /etc/init.d/whitebeam; - ln -s /etc/init.d/whitebeam /etc/rc3.d/S01whitebeam; + chmod 4555 /opt/WhiteBeam/libwhitebeam.so; + whitebeam --load Schema; + whitebeam --load Default; + whitebeam --load Essential; /etc/init.d/whitebeam start; - echo '/opt/WhiteBeam/libwhitebeam.so' | tee -a /etc/ld.so.preload") + echo '/lib/libwhitebeam.so' | tee -a /etc/ld.so.preload") .status() - .expect("Installation failed"); + .expect("WhiteBeam: Installation failed"); println!("Installation complete"); } diff --git a/src/installer/platforms/linux/resources/service.sh b/src/installer/platforms/linux/resources/service.sh new file mode 100644 index 0000000..98f905f --- /dev/null +++ b/src/installer/platforms/linux/resources/service.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# WhiteBeam service +# chkconfig: 345 20 80 +# description: WhiteBeam service +# processname: whitebeam + +SERVICE_PATH="/opt/WhiteBeam/" + +SERVICE=whitebeam +SERVICEOPTS="--service" + +NAME=whitebeam +DESC="WhiteBeam service" +PIDFILE=/opt/WhiteBeam/data/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +case "$1" in +start) +printf "%-50s" "Starting $NAME..." +cd $SERVICE_PATH +PID=`$SERVICE $SERVICEOPTS > /dev/null 2>&1 & echo $!` +#echo "Saving PID" $PID " to " $PIDFILE + if [ -z $PID ]; then + printf "%s\n" "Fail" + else + echo $PID > $PIDFILE + printf "%s\n" "Ok" + fi +;; +status) + printf "%-50s" "Checking $NAME..." + if [ -f $PIDFILE ]; then + PID=`cat $PIDFILE` + if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then + printf "%s\n" "Process dead but pidfile exists" + else + echo "Running" + fi + else + printf "%s\n" "Service not running" + fi +;; +stop) + printf "%-50s" "Stopping $NAME" + PID=`cat $PIDFILE` + cd $SERVICE_PATH + if [ -f $PIDFILE ]; then + kill -HUP $PID + printf "%s\n" "Ok" + rm -f $PIDFILE + else + printf "%s\n" "pidfile not found" + fi +;; + +restart) + $0 stop + $0 start +;; + +*) + echo "Usage: $0 {status|start|stop|restart}" + exit 1 +esac diff --git a/src/installer/platforms/macos/mod.rs b/src/installer/platforms/macos/mod.rs index 99610ac..9a07ed8 100644 --- a/src/installer/platforms/macos/mod.rs +++ b/src/installer/platforms/macos/mod.rs @@ -1,12 +1,14 @@ // Load OS-specific modules -use std::{path::Path, - path::PathBuf}; +use std::path::PathBuf; -pub fn get_data_file_path(data_file: &str) -> PathBuf { - let data_path: String = String::from("/Applications/WhiteBeam/data/"); +pub fn get_data_file_path(data_file: &str, release: &str) -> PathBuf { + let data_path: String = match release { + "test" => format!("{}/target/release/examples/", env!("PWD")), + _ => String::from("/Applications/WhiteBeam/data/") + }; let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() + PathBuf::from(data_file_path) } pub fn check_build_environment() { diff --git a/src/installer/platforms/windows/mod.rs b/src/installer/platforms/windows/mod.rs index 777f696..29a0ac1 100644 --- a/src/installer/platforms/windows/mod.rs +++ b/src/installer/platforms/windows/mod.rs @@ -1,15 +1,18 @@ // Load OS-specific modules //use std::env; -use std::{path::Path, - path::PathBuf}; +use std::path::PathBuf; -pub fn get_data_file_path(data_file: &str) -> PathBuf { - // TODO: Change this when registry and environment are secured - //Path::new(env::var("ProgramFiles").unwrap_or("C:\\ProgramFiles").push_str("\\WhiteBeam\\data\\")) - let data_path: String = String::from("C:\\Program Files\\WhiteBeam\\data\\"); +pub fn get_data_file_path(data_file: &str, release: &str) -> PathBuf { + // TODO: Use PWD for Powershell with feature="whitelist_test"? + // TODO: May change this when registry and environment are secured + //PathBuf::from(env::var("ProgramFiles").unwrap_or("C:\\ProgramFiles").push_str("\\WhiteBeam\\data\\")) + let data_path: String = match release { + "test" => format!("{}\\target\\release\\examples\\", env!("CD")), + _ => String::from("C:\\Program Files\\WhiteBeam\\data\\") + }; let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() + PathBuf::from(data_file_path) } pub fn check_build_environment() { diff --git a/src/library/Cargo.toml b/src/library/Cargo.toml index f25c3a8..1261ef6 100644 --- a/src/library/Cargo.toml +++ b/src/library/Cargo.toml @@ -1,7 +1,7 @@ # General info [package] name = "libwhitebeam" -version = "0.1.3" +version = "0.2.0" authors = ["WhiteBeam Security, Inc."] edition = "2018" @@ -14,16 +14,16 @@ crate-type = ["cdylib"] # Cross-platform dependencies [dependencies] libc = { version = "0.2" } -sodiumoxide = { version = "0.2" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } -rusqlite = { version = "0.21", features = ["bundled"] } -hex = { version = "0.4" } - -# Windows dependencies -[target.'cfg(target_os = "windows")'.dependencies.kernel32-sys] -version = "0.2" -default-features = false +rusqlite = { version = "0.25", features = ["bundled"] } +linkme = { version = "0.2" } +automod = { version = "1.0" } +glob = { version = "0.3" } +# Cryptographic dependencies +sha3 = { version = "0.9" } +blake3 = { version = "0.3" } +argon2 = { version = "0.1" } [features] whitelist_test = [] diff --git a/src/library/common/action/actions/add_environment.rs b/src/library/common/action/actions/add_environment.rs new file mode 100644 index 0000000..5a2c2f2 --- /dev/null +++ b/src/library/common/action/actions/add_environment.rs @@ -0,0 +1,19 @@ +#[macro_use] +build_action! { AddEnvironment (_src_prog, hook, _arg_id, args, do_return, return_value) { + if !((&hook.symbol).contains("exec") && (&hook.library).contains("libc.so")) { + unimplemented!("WhiteBeam: AddEnvironment action is unsupported outside of Execution hooks"); + } + let new_arg = crate::common::db::ArgumentRow { + hook: hook.id, + parent: None, + id: -1, + position: args.len() as i64, + real: unsafe { platform::environ() } as usize, + datatype: String::from("StringArray"), + pointer: true, + signed: false, + variadic: false, + array: true + }; + args.push(new_arg); +}} diff --git a/src/library/common/action/actions/add_flags.rs b/src/library/common/action/actions/add_flags.rs new file mode 100644 index 0000000..5e8c80e --- /dev/null +++ b/src/library/common/action/actions/add_flags.rs @@ -0,0 +1,45 @@ +#[macro_use] +build_action! { AddFlags (_src_prog, hook, _arg_id, args, do_return, return_value) { + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + let num_args = args.len(); + let flags = match (library, symbol) { + // Filesystem + ("/lib/x86_64-linux-gnu/libc.so.6", "creat") | + ("/lib/x86_64-linux-gnu/libc.so.6", "creat64") => { + libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "lchown") => { + libc::AT_SYMLINK_NOFOLLOW + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "rmdir") => { + libc::AT_REMOVEDIR + }, + _ => 0 + } as usize; + let position = match (library, symbol) { + // Filesystem + ("/lib/x86_64-linux-gnu/libc.so.6", "creat") | + ("/lib/x86_64-linux-gnu/libc.so.6", "creat64") => { + if num_args == 3 { + 2 + } else { + num_args + } + }, + _ => num_args + } as usize; + let new_arg = crate::common::db::ArgumentRow { + hook: hook.id, + parent: None, + id: -1, + position: position as i64, + real: flags, + datatype: String::from("IntegerSigned"), + pointer: false, + signed: true, + variadic: false, + array: false + }; + args.insert(position, new_arg); +}} diff --git a/src/library/common/action/actions/canonicalize_path.rs b/src/library/common/action/actions/canonicalize_path.rs new file mode 100644 index 0000000..088fd5c --- /dev/null +++ b/src/library/common/action/actions/canonicalize_path.rs @@ -0,0 +1,30 @@ +#[macro_use] +build_action! { CanonicalizePath (_src_prog, hook, arg_id, args, do_return, return_value) { + // TODO: Don't fatal if the Path cannot be canonicalized (return -1/0). NULL handling for dlopen? + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + let file_index = args.iter().position(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment"); + let file_argument: crate::common::db::ArgumentRow = args[file_index].clone(); + let file_value = file_argument.real as *const libc::c_char; + let new_file_value: std::ffi::OsString = match (library, symbol) { + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlopen") | + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlmopen") => { + // TODO: Remove dependency on procfs here + let fd: libc::c_int = unsafe { libc::open(file_value, libc::O_PATH) }; + let canonical_path = platform::canonicalize_fd(fd as i32).expect("WhiteBeam: Lost track of environment"); + canonical_path.into_os_string() + }, + _ => { + let file_osstring = unsafe { crate::common::convert::c_char_to_osstring(file_value) }; + match platform::search_path(&file_osstring) { + Some(abspath) => abspath.as_os_str().to_owned(), + None => { + unsafe { libc::exit(127) }; + } + } + } + }; + let new_file_value_cstring: Box = Box::new(crate::common::convert::osstr_to_cstring(&new_file_value).expect("WhiteBeam: Unexpected null reference")); + args[file_index].datatype = String::from("String"); + args[file_index].real = Box::leak(new_file_value_cstring).as_ptr() as usize; +}} diff --git a/src/library/common/action/actions/combine_directory.rs b/src/library/common/action/actions/combine_directory.rs new file mode 100644 index 0000000..3df9a82 --- /dev/null +++ b/src/library/common/action/actions/combine_directory.rs @@ -0,0 +1,62 @@ +pub fn normalize_path(path: &std::path::Path) -> std::path::PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ std::path::Component::Prefix(..)) = components.peek().cloned() { + components.next(); + std::path::PathBuf::from(c.as_os_str()) + } else { + std::path::PathBuf::new() + }; + + for component in components { + match component { + std::path::Component::Prefix(..) => unreachable!(), + std::path::Component::RootDir => { + ret.push(component.as_os_str()); + } + std::path::Component::CurDir => {} + std::path::Component::ParentDir => { + ret.pop(); + } + std::path::Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +#[macro_use] +build_action! { CombineDirectory (_src_prog, hook, arg_id, args, do_return, return_value) { + let dirfd_index = args.iter().position(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment"); + let dirfd_argument: crate::common::db::ArgumentRow = args[dirfd_index].clone(); + let path_argument: crate::common::db::ArgumentRow = args[dirfd_index+1].clone(); + let dirfd_value = dirfd_argument.real as libc::c_int; + let path_value = path_argument.real as *const libc::c_char; + // TODO: Error handling + let path_string = unsafe { String::from(std::ffi::CStr::from_ptr(path_value).to_str().expect("WhiteBeam: Unexpected null reference")) }; + if !(path_string.contains("/") || path_string.contains("..")) { + return (hook, args, do_return, return_value); + } + let mut path_new: std::path::PathBuf = match dirfd_value { + libc::AT_FDCWD => std::env::current_dir().expect("WhiteBeam: Lost track of environment"), + _ => platform::canonicalize_fd(dirfd_value as i32).expect("WhiteBeam: Lost track of environment") + }; + path_new.push(std::path::PathBuf::from(path_string)); + let path_new_normal: std::path::PathBuf = normalize_path(&path_new); + // TODO: Error handling + let filename_new: &std::ffi::OsStr = (&path_new_normal).file_name().unwrap_or(&std::ffi::OsStr::new(".")); + let filename_new_cstring: Box = Box::new(crate::common::convert::osstr_to_cstring(filename_new).expect("WhiteBeam: Unexpected null reference")); + let path_new_parent: std::path::PathBuf = match (&path_new_normal).parent() { + Some(f) => f.to_owned(), + None => std::path::PathBuf::from("/") + }; + let dirfd_new_cstring: std::ffi::CString = crate::common::convert::osstr_to_cstring((&path_new_parent).as_os_str()).expect("WhiteBeam: Unexpected null reference"); + // TODO: Don't we need a post action to close() this fd when the dirfd orig != dirfd new? + let fd: libc::c_int = unsafe { libc::open(dirfd_new_cstring.as_ptr(), libc::O_PATH) }; + if fd >= 0 { + args[dirfd_index].real = fd as usize; + args[dirfd_index+1].real = Box::leak(filename_new_cstring).as_ptr() as usize; + } + do_return = true; + return_value = -1; +}} diff --git a/src/library/common/action/actions/consume_variadic.rs b/src/library/common/action/actions/consume_variadic.rs new file mode 100644 index 0000000..bffaa9c --- /dev/null +++ b/src/library/common/action/actions/consume_variadic.rs @@ -0,0 +1,40 @@ +#[macro_use] +build_action! { ConsumeVariadic (_src_prog, hook, arg_id, args, do_return, return_value) { + let variadic_start = args.iter().position(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment"); + let variadic_start_id: i64 = args[variadic_start].id; + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + let va_arg_iter: Vec<&crate::common::db::ArgumentRow> = args.iter().filter(|arg| arg.variadic && (arg.id == variadic_start_id)).collect(); + let va_arg_iter_len = va_arg_iter.len(); + match (library, symbol) { + ("/lib/x86_64-linux-gnu/libc.so.6", "execl") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execle") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execlp") => { + assert!(va_arg_iter_len > 0, "WhiteBeam: Insufficient arguments to ConsumeVariadic action"); + let mut argv_vec: Vec<*const libc::c_char> = Vec::new(); + for arg in va_arg_iter { + argv_vec.push(arg.real as *const libc::c_char); + } + args[variadic_start].real = Box::leak(argv_vec.into_boxed_slice()).as_ptr() as usize; + args[variadic_start].datatype = String::from("StringArray"); + args[variadic_start].variadic = false; + args[variadic_start].array = true; + args.retain(|arg| !(arg.variadic && (arg.id == variadic_start_id))); + // TODO: Update the position of the following arguments + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "open") | + ("/lib/x86_64-linux-gnu/libc.so.6", "open64") | + ("/lib/x86_64-linux-gnu/libc.so.6", "openat") | + ("/lib/x86_64-linux-gnu/libc.so.6", "openat64") => { + assert!(va_arg_iter_len > 0, "WhiteBeam: Insufficient arguments to ConsumeVariadic action"); + let flags = args[variadic_start-1].real as libc::c_int; + let has_variadic_arg: bool = ((flags) & libc::O_CREAT) != 0 || ((flags) & libc::O_TMPFILE) == libc::O_TMPFILE; + if !(has_variadic_arg) { + args.retain(|arg| !(arg.variadic && (arg.id == variadic_start_id))); + } else { + args.truncate((args.len()-va_arg_iter_len)+1) + } + }, + _ => { unimplemented!("WhiteBeam: The '{}' symbol (from {}) is not supported by the ConsumeVariadic action", symbol, library) } + }; +}} diff --git a/src/library/common/action/actions/filter_environment.rs b/src/library/common/action/actions/filter_environment.rs new file mode 100644 index 0000000..0d785e0 --- /dev/null +++ b/src/library/common/action/actions/filter_environment.rs @@ -0,0 +1,117 @@ +#[macro_use] +build_action! { FilterEnvironment (_src_prog, hook, arg_id, args, do_return, return_value) { + // Enforce LD_AUDIT, LD_BIND_NOT, WB_PROG + // TODO: Avoid leaking memory (NB: this action is often called before execve on Linux) + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + let envp_index: usize = { + // Non-positional functions + match (library, symbol) { + ("/lib/x86_64-linux-gnu/libc.so.6", "execl") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execlp") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execv") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execvp") => { + args.iter().position(|arg| arg.id == -1).expect("WhiteBeam: Lost track of environment") + } + _ => { + args.iter().position(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment") + } + } + }; + let envp_argument: crate::common::db::ArgumentRow = args[envp_index].clone(); + let envp = envp_argument.real as *const *const libc::c_char; + let orig_env_vec = unsafe { + let mut env: Vec<(&std::ffi::OsStr, &std::ffi::OsStr)> = Vec::new(); + if !(envp.is_null()) { + let mut envp_iter = envp; + while !(*envp_iter).is_null() { + let input = std::ffi::CStr::from_ptr(*envp_iter).to_bytes(); + if !input.is_empty() { + match input[1..].iter().position(|&x| x == b'=').map(|p| p + 1) { + Some(p) => { + env.push((crate::common::convert::u8_slice_as_os_str(&input[..p]), + crate::common::convert::u8_slice_as_os_str(&input[p + 1..]))); + }, + None => { + // TODO: Log + } + }; + } + envp_iter = envp_iter.add(1); + } + } + env + }; + let mut update_ld_audit: bool = false; + let mut update_ld_bind_not: bool = false; + // TODO: Support more platforms here + let rtld_audit_lib_path = crate::platforms::linux::get_rtld_audit_lib_path(); + let new_ld_audit_var: std::ffi::OsString = match orig_env_vec.iter().find(|var| var.0 == "LD_AUDIT") { + Some(val) => { + if crate::common::convert::osstr_split_at_byte(val.1, b':').0 == rtld_audit_lib_path { + std::ffi::OsString::new() + } else { + update_ld_audit = true; + let mut new_ld_audit_osstring = std::ffi::OsString::from("LD_AUDIT="); + new_ld_audit_osstring.push(rtld_audit_lib_path.as_os_str()); + new_ld_audit_osstring.push(std::ffi::OsStr::new(":")); + new_ld_audit_osstring.push(val.1); + new_ld_audit_osstring + } + } + None => { + update_ld_audit = true; + let mut new_ld_audit_osstring = std::ffi::OsString::from("LD_AUDIT="); + new_ld_audit_osstring.push(rtld_audit_lib_path.as_os_str()); + new_ld_audit_osstring + } + }; + let new_ld_bind_not_var: std::ffi::OsString = match orig_env_vec.iter().find(|var| var.0 == "LD_BIND_NOT") { + Some(val) => { + if val.1 != "1" { + update_ld_bind_not = true; + std::ffi::OsString::from("LD_BIND_NOT=1") + } else { + std::ffi::OsString::new() + } + } + None => { + update_ld_bind_not = true; + std::ffi::OsString::from("LD_BIND_NOT=1") + } + }; + let mut env_vec: Vec<*const libc::c_char> = Vec::new(); + let program_path: std::ffi::OsString = platform::canonicalize_fd(args[0].real as i32).expect("WhiteBeam: Lost track of environment").into_os_string(); + if update_ld_audit { + // TODO: Log null reference, process errors + let new_ld_audit_cstring: Box = Box::new(crate::common::convert::osstr_to_cstring(&new_ld_audit_var).expect("WhiteBeam: Unexpected null reference")); + // TODO: Check whitelist for path + env_vec.push(Box::leak(new_ld_audit_cstring).as_ptr()); + } + if update_ld_bind_not { + // TODO: Log null reference, process errors + let new_ld_bind_not_cstring: Box = Box::new(crate::common::convert::osstr_to_cstring(&new_ld_bind_not_var).expect("WhiteBeam: Unexpected null reference")); + env_vec.push(Box::leak(new_ld_bind_not_cstring).as_ptr()); + } + let mut program_path_env: std::ffi::OsString = std::ffi::OsString::from("WB_PROG="); + program_path_env.push(&program_path); + let program_path_env_cstring: Box = Box::new(crate::common::convert::osstr_to_cstring(&program_path_env).expect("WhiteBeam: Unexpected null reference")); + env_vec.push(Box::leak(program_path_env_cstring).as_ptr()); + unsafe { + if !(envp.is_null()) { + let mut envp_iter = envp; + while !(*envp_iter).is_null() { + if let Some(key_value) = crate::common::convert::parse_env_single(std::ffi::CStr::from_ptr(*envp_iter).to_bytes()) { + if (!(update_ld_audit) && (key_value.0 == std::ffi::OsString::from("LD_AUDIT"))) + || (!(update_ld_bind_not) && (key_value.0 == std::ffi::OsString::from("LD_BIND_NOT"))) + || ((key_value.0 != std::ffi::OsString::from("LD_AUDIT")) && (key_value.0 != std::ffi::OsString::from("LD_BIND_NOT")) && (key_value.0 != std::ffi::OsString::from("WB_PROG"))) { + env_vec.push(*envp_iter); + } + } + envp_iter = envp_iter.offset(1); + } + } + } + env_vec.push(std::ptr::null()); + args[envp_index].real = Box::leak(env_vec.into_boxed_slice()).as_ptr() as usize; +}} diff --git a/src/library/common/action/actions/open_file_descriptor.rs b/src/library/common/action/actions/open_file_descriptor.rs new file mode 100644 index 0000000..55eff83 --- /dev/null +++ b/src/library/common/action/actions/open_file_descriptor.rs @@ -0,0 +1,64 @@ +#[macro_use] +build_action! { OpenFileDescriptor (_src_prog, hook, arg_id, args, do_return, return_value) { + // TODO: No O_CLOEXEC leads to inherited fd's in children + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + let file_index = args.iter().position(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment"); + let file_argument: crate::common::db::ArgumentRow = args[file_index].clone(); + let file_value = file_argument.real as *const libc::c_char; + let flags: i32 = match (library, symbol) { + // Execution: handled by default case + // Filesystem + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen64") => { + let mode_osstring: std::ffi::OsString = unsafe { crate::common::convert::c_char_to_osstring(args[file_index+1].clone().real as *const libc::c_char) }; + let mode_string = mode_osstring.into_string().expect("WhiteBeam: Unexpected null reference"); + // Ignore ",ccs=?" + let mode_no_ccs = mode_string.splitn(2, ",").next().expect("WhiteBeam: Unexpected null reference"); + let mut glibc_extensions = 0; + if mode_no_ccs.contains("e") { glibc_extensions |= libc::O_CLOEXEC }; + if mode_no_ccs.contains("x") { glibc_extensions |= libc::O_EXCL }; + let mode_clean = mode_no_ccs.replace(&['b', 'c', 'e', 'm', 'x'][..], ""); + // fopen() mode => open() flags + let regular_flags: i32 = match mode_clean.as_ref() { + "r" => libc::O_RDONLY, + "w" => libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC, + "a" => libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND, + "r+" => libc::O_RDWR, + "w+" => libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC, + "a+" => libc::O_RDWR | libc::O_CREAT | libc::O_APPEND, + _ => { + do_return = true; + return_value = 0; + unsafe { *platform::errno_location() = libc::EINVAL }; + return (hook, args, do_return, return_value); + } + }; + regular_flags | glibc_extensions + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "truncate") => { + let length: i64 = args[file_index+1].clone().real as i64; + match length { + 0 => libc::O_WRONLY | libc::O_TRUNC, + _ => libc::O_WRONLY + } + }, + _ => libc::O_PATH + }; + let fd: libc::c_int = unsafe { libc::open(file_value, flags) }; + if fd >= 0 { + args[file_index].datatype = String::from("IntegerSigned"); + args[file_index].real = fd as usize; + return (hook, args, do_return, return_value); + } + do_return = true; + match (library, symbol) { + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen64") => { + return_value = 0; + } + _ => { + return_value = -1; + } + }; +}} diff --git a/src/library/common/action/actions/redirect_function.rs b/src/library/common/action/actions/redirect_function.rs new file mode 100644 index 0000000..b8f3e39 --- /dev/null +++ b/src/library/common/action/actions/redirect_function.rs @@ -0,0 +1,55 @@ +#[macro_use] +build_action! { RedirectFunction (_src_prog, hook, _arg_id, args, do_return, return_value) { + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + hook.symbol = match (library, symbol) { + // Execution + ("/lib/x86_64-linux-gnu/libc.so.6", "execl") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execle") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execlp") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execv") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execve") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execvp") | + ("/lib/x86_64-linux-gnu/libc.so.6", "execvpe") => { + String::from("fexecve") + }, + // Filesystem + ("/lib/x86_64-linux-gnu/libc.so.6", "truncate") => { + String::from("ftruncate") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen64") => { + String::from("fdopen") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "symlink") => { + String::from("symlinkat") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "unlink") | + ("/lib/x86_64-linux-gnu/libc.so.6", "rmdir") => { + String::from("unlinkat") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "link") => { + String::from("linkat") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "rename") => { + String::from("renameat") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "chown") | + ("/lib/x86_64-linux-gnu/libc.so.6", "lchown") => { + String::from("fchownat") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "chmod") => { + String::from("fchmodat") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "creat") | + ("/lib/x86_64-linux-gnu/libc.so.6", "open") | + ("/lib/x86_64-linux-gnu/libc.so.6", "creat64") | + ("/lib/x86_64-linux-gnu/libc.so.6", "open64") => { + String::from("openat") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "mknod") => { + String::from("mknodat") + }, + _ => { unimplemented!("WhiteBeam: The '{}' symbol (from {}) is not supported by the RedirectFunction action", symbol, library) } + }; +}} diff --git a/src/library/common/action/actions/split_file_path.rs b/src/library/common/action/actions/split_file_path.rs new file mode 100644 index 0000000..269fb02 --- /dev/null +++ b/src/library/common/action/actions/split_file_path.rs @@ -0,0 +1,72 @@ +pub fn normalize_path(path: &std::path::Path) -> std::path::PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ std::path::Component::Prefix(..)) = components.peek().cloned() { + components.next(); + std::path::PathBuf::from(c.as_os_str()) + } else { + std::path::PathBuf::new() + }; + + for component in components { + match component { + std::path::Component::Prefix(..) => unreachable!(), + std::path::Component::RootDir => { + ret.push(component.as_os_str()); + } + std::path::Component::CurDir => {} + std::path::Component::ParentDir => { + ret.pop(); + } + std::path::Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +#[macro_use] +build_action! { SplitFilePath (_src_prog, hook, arg_id, args, do_return, return_value) { + let path_index = args.iter().position(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment"); + let path_argument: crate::common::db::ArgumentRow = args[path_index].clone(); + let path_value = path_argument.real as *const libc::c_char; + let path_osstring = unsafe { crate::common::convert::c_char_to_osstring(path_value) }; + let path_pathbuf: std::path::PathBuf = std::path::PathBuf::from(path_osstring); + let path_normal: std::path::PathBuf = normalize_path(&path_pathbuf); + // TODO: Error handling + let basename: &std::ffi::OsStr = (&path_normal).file_name().unwrap_or(&std::ffi::OsStr::new(".")); + let basename_cstring: Box = Box::new(crate::common::convert::osstr_to_cstring(basename).expect("WhiteBeam: Unexpected null reference")); + // TODO: Provide top level directory function in platform + let dirfd: std::path::PathBuf = match (&path_normal).parent() { + Some(f) => { + if f == std::path::Path::new("") { + std::env::current_dir().expect("WhiteBeam: Lost track of environment") + } else { + f.to_owned() + } + }, + None => std::path::PathBuf::from("/") + }; + let dirfd_cstring: std::ffi::CString = crate::common::convert::osstr_to_cstring((&dirfd).as_os_str()).expect("WhiteBeam: Unexpected null reference"); + let fd: libc::c_int = unsafe { libc::open(dirfd_cstring.as_ptr(), libc::O_PATH) }; + if fd >= 0 { + args[path_index].datatype = String::from("IntegerSigned"); + args[path_index].real = fd as usize; + let new_arg = crate::common::db::ArgumentRow { + hook: hook.id, + parent: None, + id: -1, + position: path_index as i64, + real: Box::leak(basename_cstring).as_ptr() as usize, + datatype: String::from("String"), + pointer: true, + signed: false, + variadic: false, + array: false + }; + args.insert(path_index+1, new_arg); + return (hook, args, do_return, return_value); + } + do_return = true; + return_value = -1; +}} diff --git a/src/library/common/action/actions/verify_can_execute.rs b/src/library/common/action/actions/verify_can_execute.rs new file mode 100644 index 0000000..44eb54b --- /dev/null +++ b/src/library/common/action/actions/verify_can_execute.rs @@ -0,0 +1,67 @@ +#[macro_use] +build_action! { VerifyCanExecute (src_prog, hook, arg_id, args, do_return, return_value) { + // TODO: Depending on LogVerbosity, log all use of this action + // TODO: Use OsString? + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + // Permit execution if not running in prevention mode + if !(crate::common::db::get_prevention()) { + return (hook, args, do_return, return_value); + } + // Permit authorized execution + if crate::common::db::get_valid_auth_env() { + return (hook, args, do_return, return_value); + } + let any = String::from("ANY"); + let class = match (library, symbol) { + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlopen") | + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlmopen") => { + String::from("Filesystem/Path/Library") + }, + _ => String::from("Filesystem/Path/Executable") + }; + let all_allowed_executables: Vec = { + let whitelist_cache_lock = crate::common::db::WL_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + whitelist_cache_lock.iter().filter(|whitelist| (whitelist.class == class) && ((whitelist.path == src_prog) || (whitelist.path == any))).map(|whitelist| whitelist.value.clone()).collect() + }; + // Permit ANY + if all_allowed_executables.iter().any(|executable| executable == &any) { + return (hook, args, do_return, return_value); + } + let argument: crate::common::db::ArgumentRow = args.iter().find(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment").clone(); + let target_executable: String = match (library, symbol) { + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlopen") | + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlmopen") => { + if argument.real == 0 { + return (hook, args, do_return, return_value); + } + unsafe { String::from(std::ffi::CStr::from_ptr(argument.real as *const libc::c_char).to_str().expect("WhiteBeam: Unexpected null reference")) } + }, + _ => { + let canonical_path = platform::canonicalize_fd(argument.real as i32).expect("WhiteBeam: Lost track of environment"); + canonical_path.into_os_string().into_string().expect("WhiteBeam: Unexpected null reference") + } + }; + // Permit whitelisted executables + if all_allowed_executables.iter().any(|executable| executable == &target_executable) { + return (hook, args, do_return, return_value); + } + // Deny by default + event::send_log_event(event::LogClass::Warn as i64, format!("Blocked {} from executing {} (VerifyCanExecute)", &src_prog, &target_executable)); + eprintln!("WhiteBeam: {}: Permission denied", &target_executable); + if (&hook.symbol).contains("exec") && (&hook.library).contains("libc.so") { + // Terminate the child process + unsafe { libc::exit(126) }; + } + do_return = true; + match (library, symbol) { + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlopen") | + ("/lib/x86_64-linux-gnu/libdl.so.2", "dlmopen") => { + // TODO: dlerror? + return_value = 0; + }, + _ => { + return_value = -1; + } + }; +}} diff --git a/src/library/common/action/actions/verify_can_write.rs b/src/library/common/action/actions/verify_can_write.rs new file mode 100644 index 0000000..1ef13e5 --- /dev/null +++ b/src/library/common/action/actions/verify_can_write.rs @@ -0,0 +1,120 @@ +#[macro_use] +build_action! { VerifyCanWrite (src_prog, hook, arg_id, args, do_return, return_value) { + let directory_index = args.iter().position(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment"); + let directory_argument: crate::common::db::ArgumentRow = args[directory_index].clone(); + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + if !(crate::common::db::get_prevention()) { + return (hook, args, do_return, return_value); + } + // Permit authorized execution + if crate::common::db::get_valid_auth_env() { + return (hook, args, do_return, return_value); + } + let is_read_only: bool = match (library, symbol) { + ("/lib/x86_64-linux-gnu/libc.so.6", "fdopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen64") => { + let mode = args[1].real as *const libc::c_char; + let mode_string = String::from(unsafe { std::ffi::CStr::from_ptr(mode) }.to_str().expect("WhiteBeam: Unexpected null reference")); + if !(mode_string.contains("w") || + mode_string.contains("a") || + mode_string.contains("+")) { + true + } else { + false + } + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "open") | + ("/lib/x86_64-linux-gnu/libc.so.6", "open64") | + ("/lib/x86_64-linux-gnu/libc.so.6", "openat") | + ("/lib/x86_64-linux-gnu/libc.so.6", "openat64") => { + let flags = args[2].real as libc::c_int; + if !(((flags & libc::O_RDWR) > 0) || + ((flags & libc::O_WRONLY) > 0) || + ((flags & libc::O_CREAT) > 0) || + ((flags & libc::O_TMPFILE) > 0) || + ((flags & libc::O_APPEND) > 0)) { + true + } else { + false + } + }, + _ => false + }; + // Permit read-only + if is_read_only { + return (hook, args, do_return, return_value); + } + let any = String::from("ANY"); + let class = String::from("Filesystem/Directory/Writable"); + let all_allowed_directories: Vec = { + let whitelist_cache_lock = crate::common::db::WL_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + whitelist_cache_lock.iter().filter(|whitelist| (whitelist.class == class) && ((whitelist.path == src_prog) || (whitelist.path == any))).map(|whitelist| whitelist.value.clone()).collect() + }; + // Permit ANY + if all_allowed_directories.iter().any(|directory| directory == &any) { + return (hook, args, do_return, return_value); + } + // NB: Do not dereference paths here + let canonical_path = platform::canonicalize_fd(directory_argument.real as i32).expect("WhiteBeam: Lost track of environment"); + // Minor performance hit by defining here instead of match statement + let parent: std::path::PathBuf = match (&canonical_path).parent() { + Some(f) => f.to_owned(), + None => std::path::PathBuf::from("/") + }; + let mut filename: String = String::from("."); + let mut target_directory: String = match (library, symbol) { + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen64") | + ("/lib/x86_64-linux-gnu/libc.so.6", "truncate") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fchmod") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fchown") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fdopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "ftruncate") => { + // This function passes file descriptors + filename = String::from((&canonical_path).file_name().unwrap_or(&std::ffi::OsStr::new(".")).to_str().expect("WhiteBeam: Unexpected null reference")); + parent.into_os_string().into_string().expect("WhiteBeam: Unexpected null reference") + }, + ("/lib/x86_64-linux-gnu/libc.so.6", "fchownat") | + ("/lib/x86_64-linux-gnu/libc.so.6", "linkat") => { + let flags = args.last().expect("WhiteBeam: Lost track of environment"); + if (flags.real as i32 & libc::AT_EMPTY_PATH) > 0 { + filename = String::from((&canonical_path).file_name().unwrap_or(&std::ffi::OsStr::new(".")).to_str().expect("WhiteBeam: Unexpected null reference")); + parent.into_os_string().into_string().expect("WhiteBeam: Unexpected null reference") + } else { + filename = unsafe { String::from(std::ffi::CStr::from_ptr(args[directory_index+1].real as *const libc::c_char).to_str().expect("WhiteBeam: Unexpected null reference")) }; + canonical_path.into_os_string().into_string().expect("WhiteBeam: Unexpected null reference") + } + }, + _ => { + // This function passes directory file descriptors + filename = unsafe { String::from(std::ffi::CStr::from_ptr(args[directory_index+1].real as *const libc::c_char).to_str().expect("WhiteBeam: Unexpected null reference")) }; + canonical_path.into_os_string().into_string().expect("WhiteBeam: Unexpected null reference") + } + }; + target_directory.push('/'); + let full_path = format!("{}{}", target_directory, filename); + // Special cases. We don't want to whitelist /dev (although pts and related subdirectories are fine). + if (full_path == "/dev/tty") || (full_path == "/dev/null") { + return (hook, args, do_return, return_value); + } + // Permit whitelisted directories + if all_allowed_directories.iter().any(|directory| glob::Pattern::new(directory).expect("WhiteBeam: Invalid glob pattern").matches(&target_directory)) { + return (hook, args, do_return, return_value); + } + // Deny by default + event::send_log_event(event::LogClass::Warn as i64, format!("Blocked {} from writing to {} (VerifyCanWrite)", &src_prog, &target_directory)); + eprintln!("WhiteBeam: {}: Permission denied", &full_path); + do_return = true; + match (library, symbol) { + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fopen64") | + ("/lib/x86_64-linux-gnu/libc.so.6", "fdopen") => { + return_value = 0; + } + _ => { + return_value = -1; + } + }; +}} diff --git a/src/library/common/action/actions/verify_file_hash.rs b/src/library/common/action/actions/verify_file_hash.rs new file mode 100644 index 0000000..fcd4a1c --- /dev/null +++ b/src/library/common/action/actions/verify_file_hash.rs @@ -0,0 +1,60 @@ +use std::io::prelude::*; + +fn fail(library: &str, symbol: &str, argument_path: &str) { + if symbol.contains("exec") && library.contains("libc.so") { + // Terminate the child process + eprintln!("WhiteBeam: {}: Permission denied", argument_path); + unsafe { libc::exit(126) }; + } else { + unimplemented!("WhiteBeam: The '{}' symbol (from {}) is not supported by the VerifyFileHash action", symbol, library); + } +} + +#[macro_use] +build_action! { VerifyFileHash (src_prog, hook, arg_id, args, do_return, return_value) { + // TODO: Depending on LogVerbosity, log all use of this action + // NB: For Execution hooks, system executables that aren't read world may be whitelisted as ANY + if !(crate::common::db::get_prevention()) { + return (hook, args, do_return, return_value); + } + // Permit authorized execution + if crate::common::db::get_valid_auth_env() { + return (hook, args, do_return, return_value); + } + let library: &str = &hook.library; + let symbol: &str = &hook.symbol; + let any = String::from("ANY"); + let class = String::from("Hash/"); + let argument_path = { + let argument: crate::common::db::ArgumentRow = args.iter().find(|arg| arg.id == arg_id).expect("WhiteBeam: Lost track of environment").clone(); + let canonical_path = platform::canonicalize_fd(argument.real as i32).expect("WhiteBeam: Lost track of environment"); + canonical_path.into_os_string().into_string().expect("WhiteBeam: Unexpected null reference") + }; + let all_allowed_hashes: Vec<(String, String)> = { + let whitelist_cache_lock = crate::common::db::WL_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + whitelist_cache_lock.iter().filter(|whitelist| (whitelist.class.starts_with(&class)) && ((whitelist.path == argument_path) || (whitelist.path == any))).map(|whitelist| (whitelist.class.clone(), whitelist.value.clone())).collect() + }; + // Permit ANY + if all_allowed_hashes.iter().any(|hash_tuple| hash_tuple.1 == any) { + return (hook, args, do_return, return_value); + } + // Permit whitelisted file hashes (consecutively). This allows hybrid hashing schemes for additional security (e.g. SHA3 and BLAKE3). + let hash_count = all_allowed_hashes.len(); + let mut argument_file: std::fs::File = match std::fs::File::open(&argument_path) { + Ok(f) => f, + Err(_e) => { + fail(library, symbol, &argument_path); + unreachable!("WhiteBeam: Lost track of environment"); + } + }; + let passed_all: bool = all_allowed_hashes.iter().all(|hash_tuple| { + argument_file.seek(std::io::SeekFrom::Start(0)).expect("WhiteBeam: VerifyFileHash failed to seek in target file"); + hash_tuple.1 == crate::common::hash::process_hash(&mut argument_file, &(hash_tuple.0), None) + }); + if (hash_count > 0) && passed_all { + return (hook, args, do_return, return_value); + } + // Deny by default + event::send_log_event(event::LogClass::Warn as i64, format!("Blocked {} due to incorrect hash of {} (VerifyFileHash)", &src_prog, &argument_path)); + fail(library, symbol, &argument_path); +}} diff --git a/src/library/common/action/mod.rs b/src/library/common/action/mod.rs new file mode 100644 index 0000000..920fe3e --- /dev/null +++ b/src/library/common/action/mod.rs @@ -0,0 +1,52 @@ +use crate::common::db; + +pub struct ActionObject { + pub alias: &'static str, + pub function: fn(String, db::HookRow, i64, Vec, bool, isize) -> (db::HookRow, Vec, bool, isize) +} + +// Action template +macro_rules! build_action { + ($alias:ident ($src_prog:ident, $hook:ident, $arg_id:ident, $args:ident, $do_return:ident, $return_value:ident) $body:block) => { + #[allow(unused_imports)] + use crate::common::event; + #[cfg(target_os = "windows")] + #[allow(unused_imports)] + use crate::platforms::windows as platform; + #[cfg(target_os = "linux")] + #[allow(unused_imports)] + use crate::platforms::linux as platform; + #[cfg(target_os = "macos")] + #[allow(unused_imports)] + use crate::platforms::macos as platform; + #[allow(non_snake_case)] + #[allow(unused_assignments)] + #[allow(unused_mut)] + pub fn $alias ($src_prog: String, mut $hook: crate::common::db::HookRow, $arg_id: i64, mut $args: Vec, mut $do_return: bool, mut $return_value: isize) -> (crate::common::db::HookRow, Vec, bool, isize) { + $body + ($hook, $args, $do_return, $return_value) + } + #[linkme::distributed_slice(crate::common::action::ACTION_INDEX)] + pub static ACTION: crate::common::action::ActionObject = crate::common::action::ActionObject { alias: stringify!($alias), function: $alias }; + }; +} + +// Load action modules +mod actions { + automod::dir!(pub "src/library/common/action/actions"); +} + +// Collect actions in distributed slice +#[linkme::distributed_slice] +pub static ACTION_INDEX: [ActionObject] = [..]; + +pub fn process_action(src_prog: String, rule: db::RuleRow, hook: db::HookRow, args: Vec) -> (db::HookRow, Vec, bool, isize) { + let action: &str = &rule.action; + let arg_id: i64 = rule.arg; + let do_return = false; + let return_value = 0 as isize; + match ACTION_INDEX.iter().find(|a| a.alias == action) { + Some(action) => {(action.function)(src_prog, hook, arg_id, args, do_return, return_value)} + None => panic!("WhiteBeam: Invalid action: {}", action) + } +} diff --git a/src/library/common/convert.rs b/src/library/common/convert.rs new file mode 100644 index 0000000..dec8d8b --- /dev/null +++ b/src/library/common/convert.rs @@ -0,0 +1,67 @@ +use libc::c_char; +use std::{ffi::CStr, + ffi::CString, + ffi::NulError, + ffi::OsStr, + ffi::OsString, + os::unix::ffi::OsStrExt, + os::unix::ffi::OsStringExt}; + +// TODO: impl/trait? Extend types? .into()? 0.2.1 + +pub unsafe fn c_char_to_osstring(char_ptr: *const c_char) -> OsString { + match char_ptr.is_null() { + true => OsString::new(), + false => { + let program_c_str: &CStr = CStr::from_ptr(char_ptr); + OsStr::from_bytes(program_c_str.to_bytes()).to_owned() + } + } +} + +pub fn osstr_to_cstring(osstr_input: &OsStr) -> Result { + CString::new(osstr_input.as_bytes()) +} + +pub fn osstr_split_at_byte(osstr_input: &OsStr, byte: u8) -> (&OsStr, &OsStr) { + for (i, b) in osstr_input.as_bytes().iter().enumerate() { + if b == &byte { + return (OsStr::from_bytes(&osstr_input.as_bytes()[..i]), + OsStr::from_bytes(&osstr_input.as_bytes()[i + 1..])); + } + } + (&*osstr_input, OsStr::from_bytes(&osstr_input.as_bytes()[osstr_input.len()..osstr_input.len()])) +} + +pub fn parse_env_single(input: &[u8]) -> Option<(OsString, OsString)> { + // TODO: Windows support + // TODO: Test for environment without = + if input.is_empty() { + return None; + } + let pos = input[1..].iter().position(|&x| x == b'=').map(|p| p + 1); + pos.map(|p| { + ( + OsStringExt::from_vec(input[..p].to_vec()), + OsStringExt::from_vec(input[p + 1..].to_vec()), + ) + }) +} + +pub unsafe fn parse_env_collection(envp: *const *const c_char) -> Vec<(OsString, OsString)> { + let mut env: Vec<(OsString, OsString)> = Vec::new(); + if !(envp.is_null()) { + let mut envp_iter = envp; + while !(*envp_iter).is_null() { + if let Some(key_value) = parse_env_single(CStr::from_ptr(*envp_iter).to_bytes()) { + env.push(key_value); + } + envp_iter = envp_iter.add(1); + } + } + env +} + +pub fn u8_slice_as_os_str(s: &[u8]) -> &OsStr { + unsafe { &*(s as *const [u8] as *const OsStr) } +} diff --git a/src/library/common/db.rs b/src/library/common/db.rs index 2528f74..ffe5fb1 100644 --- a/src/library/common/db.rs +++ b/src/library/common/db.rs @@ -8,29 +8,114 @@ use crate::common::hash; use crate::common::time; use std::{env, error::Error, - path::Path}; -use rusqlite::{params, Connection}; + path::Path, + lazy::SyncLazy, + sync::Mutex}; +use rusqlite::{params, Connection, OpenFlags}; -pub struct WhitelistResult { - pub program: String, - pub allow_unsafe: bool, - pub hash: String +// TODO: Hashmap/BTreemap to avoid race conditions, clean up of pthread_self() keys: +// Timestamp attribute, vec. len>0, check timestamp, pthread_equal, RefCell/Cell (?) +pub static HOOK_CACHE: SyncLazy>> = SyncLazy::new(|| Mutex::new(vec![])); +pub static ARG_CACHE: SyncLazy>> = SyncLazy::new(|| Mutex::new(vec![])); +pub static WL_CACHE: SyncLazy>> = SyncLazy::new(|| Mutex::new(vec![])); +pub static RULE_CACHE: SyncLazy>> = SyncLazy::new(|| Mutex::new(vec![])); +// TODO: BTreemap for Settings? +pub static SET_CACHE: SyncLazy>> = SyncLazy::new(|| Mutex::new(vec![])); +// TODO: Cache rotation + +#[derive(Clone)] +pub struct HookRow { + pub language: String, + pub library: String, + pub symbol: String, + pub id: i64 +} + +#[derive(Clone)] +pub struct ArgumentRow { + pub hook: i64, + pub parent: Option, + pub id: i64, + pub position: i64, + pub real: usize, + pub datatype: String, + pub pointer: bool, + pub signed: bool, + pub variadic: bool, + pub array: bool } -pub fn get_config(conn: &Connection, config_param: String) -> String { +#[derive(Clone)] +pub struct WhitelistRow { + pub class: String, + pub path: String, + pub value: String +} + +#[derive(Clone)] +pub struct RuleRow { + pub arg: i64, + pub action: String +} + +#[derive(Clone)] +pub struct SettingRow { + pub param: String, + pub value: String +} + +pub fn db_open() -> Result { + let db_path: &Path = &platform::get_data_file_path("database.sqlite"); + // TODO: Fix segmentation fault + //let no_db: bool = !db_path.exists(); + //if no_db { + // return Err("No database file found".to_string()); + //} + match Connection::open_with_flags(db_path, OpenFlags::SQLITE_OPEN_READ_ONLY) { + Ok(conn) => Ok(conn), + Err(_e) => { + return Err("Could not open database file".to_string()); + } + } +} + +pub fn get_hook_view(conn: &Connection) -> Result, Box> { // TODO: Log errors - conn.query_row("SELECT config_value FROM config WHERE config_param = ?", params![config_param], |r| r.get(0)) - .expect("WhiteBeam: Could not query configuration") + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT language, library, symbol, id FROM HookView")?; + let result_iter = stmt.query_map(params![], |row| { + Ok(HookRow { + language: row.get(0)?, + library: row.get(1)?, + symbol: row.get(2)?, + id: row.get(3)? + }) + })?; + for result in result_iter { + result_vec.push(result?); + } + Ok(result_vec) } -pub fn get_dyn_whitelist(conn: &Connection) -> Result, Box> { - let mut result_vec: Vec = Vec::new(); - let mut stmt = conn.prepare("SELECT program, allow_unsafe, hash FROM whitelist")?; +pub fn get_argument_view(conn: &Connection) -> Result, Box> { + // TODO: Log errors + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT hook, parent, id, position, datatype, pointer, signed, variadic, array FROM ArgumentView")?; let result_iter = stmt.query_map(params![], |row| { - Ok(WhitelistResult { - program: row.get(0)?, - allow_unsafe: row.get(1)?, - hash: row.get(2)? + Ok(ArgumentRow { + hook: row.get(0)?, + parent: match row.get(1) { + Ok(id) => {Some(id)} + Err(_) => {None} + }, + id: row.get(2)?, + position: row.get(3)?, + real: 0 as usize, + datatype: row.get(4)?, + pointer: row.get(5)?, + signed: row.get(6)?, + variadic: row.get(7)?, + array: row.get(8)? }) })?; for result in result_iter { @@ -39,45 +124,151 @@ pub fn get_dyn_whitelist(conn: &Connection) -> Result, Box< Ok(result_vec) } -pub fn get_enabled(conn: &Connection) -> bool { - get_config(conn, String::from("enabled")) == String::from("true") +pub fn get_whitelist_view(conn: &Connection) -> Result, Box> { + // TODO: Log errors + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT class, path, value FROM WhitelistView")?; + let result_iter = stmt.query_map(params![], |row| { + Ok(WhitelistRow { + class: row.get(0)?, + path: row.get(1)?, + value: row.get(2)? + }) + })?; + for result in result_iter { + result_vec.push(result?); + } + Ok(result_vec) } -pub fn get_valid_auth_string(conn: &Connection, auth: &str) -> bool { - let auth_hash: String = hash::common_hash_password(auth); - let console_secret_expiry: u32 = match get_config(conn, String::from("console_secret_expiry")).parse() { - Ok(v) => v, - Err(_e) => return false +pub fn get_rule_view(conn: &Connection) -> Result, Box> { + // TODO: Log errors + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT arg, action FROM RuleView")?; + let result_iter = stmt.query_map(params![], |row| { + Ok(RuleRow { + arg: row.get(0)?, + action: row.get(1)? + }) + })?; + for result in result_iter { + result_vec.push(result?); + } + Ok(result_vec) +} + +pub fn get_setting_table(conn: &Connection) -> Result, Box> { + // TODO: Log errors + let mut result_vec: Vec = Vec::new(); + let mut stmt = conn.prepare("SELECT param, value FROM Setting")?; + let result_iter = stmt.query_map(params![], |row| { + Ok(SettingRow { + param: row.get(0)?, + value: row.get(1)? + }) + })?; + for result in result_iter { + result_vec.push(result?); + } + Ok(result_vec) +} + +pub fn populate_cache() -> Result<(), Box> { + let conn = db_open()?; + // Hook cache + { + let mut hook_cache_lock = HOOK_CACHE.lock()?; + hook_cache_lock.clear(); + for row in get_hook_view(&conn)? { + hook_cache_lock.push(row); + } + }; + // Argument cache + { + let mut arg_cache_lock = ARG_CACHE.lock()?; + arg_cache_lock.clear(); + for row in get_argument_view(&conn)? { + arg_cache_lock.push(row); + } + }; + // Whitelist cache + { + let mut wl_cache_lock = WL_CACHE.lock()?; + wl_cache_lock.clear(); + for row in get_whitelist_view(&conn)? { + wl_cache_lock.push(row); + } + }; + // Rule cache + { + let mut rule_cache_lock = RULE_CACHE.lock()?; + rule_cache_lock.clear(); + for row in get_rule_view(&conn)? { + rule_cache_lock.push(row); + } + }; + // Setting cache + { + let mut set_cache_lock = SET_CACHE.lock()?; + set_cache_lock.clear(); + for row in get_setting_table(&conn)? { + set_cache_lock.push(row); + } + }; + Ok(()) +} + +pub fn get_setting(param: String) -> String { + // TODO: Log errors + let set_cache_lock = SET_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + let setting_option: Option<&SettingRow> = set_cache_lock.iter().find(|setting| setting.param == param); + let setting_row_cloned: SettingRow = setting_option.expect("WhiteBeam: Lost track of environment").clone(); + (&setting_row_cloned.value).to_owned() +} + +pub fn get_prevention() -> bool { + get_setting(String::from("Prevention")) == String::from("true") +} + +pub fn get_valid_auth_string(auth: String) -> bool { + // TODO: Support more than ARGON2ID + //let algorithm = get_setting(&conn, String::from("SecretAlgorithm"))?; + let argon2 = argon2::Argon2::default(); + let console_secret = get_setting(String::from("ConsoleSecret")); + let recovery_secret = get_setting(String::from("RecoverySecret")); + let console_secret_pwhash: Option = match argon2::PasswordHash::new(&console_secret) { + Ok(pwhash) => Some(pwhash), + Err(_) => None + }; + let recovery_secret_pwhash: Option = match argon2::PasswordHash::new(&recovery_secret) { + Ok(pwhash) => Some(pwhash), + Err(_) => None + }; + let auth_bytes = auth.as_bytes(); + let console_secret_expiry: Option = match get_setting(String::from("ConsoleSecretExpiry")).parse() { + Ok(v) => Some(v), + Err(_e) => None }; let time_now = time::get_timestamp(); - if console_secret_expiry == 0 || - console_secret_expiry >= time_now { - return get_config(conn, String::from("console_secret")) == String::from(auth_hash); + if console_secret_expiry.is_some() + && (console_secret_expiry.unwrap() == 0 || console_secret_expiry.unwrap() >= time_now) + && console_secret_pwhash.is_some() + && argon2::PasswordVerifier::verify_password(&argon2, auth_bytes, &console_secret_pwhash.unwrap()).is_ok() { + return true + } else if recovery_secret_pwhash.is_some() + && argon2::PasswordVerifier::verify_password(&argon2, auth_bytes, &recovery_secret_pwhash.unwrap()).is_ok() { + return true } false } -pub fn get_valid_auth_env(conn: &Connection) -> bool { +pub fn get_valid_auth_env() -> bool { match env::var("WB_AUTH") { Ok(val) => { - get_valid_auth_string(conn, &val) + get_valid_auth_string(val) } Err(_e) => { false } } } - -pub fn db_open() -> Result { - let db_path: &Path = &platform::get_data_file_path("database.sqlite"); - let no_db: bool = !db_path.exists(); - if no_db { - return Err("No database file found".to_string()); - } - match Connection::open(db_path) { - Ok(conn) => Ok(conn), - Err(_e) => { - return Err("Could not open database file".to_string()); - } - } -} diff --git a/src/library/common/event.rs b/src/library/common/event.rs index baa6c6e..18ea1c2 100644 --- a/src/library/common/event.rs +++ b/src/library/common/event.rs @@ -1,21 +1,20 @@ -#[cfg(target_os = "windows")] -use crate::platforms::windows as platform; -#[cfg(target_os = "linux")] -use crate::platforms::linux as platform; -#[cfg(target_os = "macos")] -use crate::platforms::macos as platform; use serde::{Deserialize, Serialize}; -use std::ffi::OsStr; -use crate::common::http; -use crate::common::time; +use crate::common::{db, http, time}; #[derive(Deserialize, Serialize)] -struct LogExecObject { - program: String, - hash: String, - uid: u32, - ts: u32, - success: bool +struct LogObject { + class: i64, + log: String, + ts: u32 +} + +pub enum LogClass { + Off = 1, + Error, // 2 + Warn, // 3 + Info, // 4 + Debug, // 5 + Trace // 6 } fn get_timeout() -> u64 { @@ -23,31 +22,32 @@ fn get_timeout() -> u64 { 1 } -pub fn send_exec_event(uid: u32, program: &OsStr, hash: &str, success: bool) { - let program_string = program.to_string_lossy().to_string(); - let ts = time::get_timestamp(); - let log = LogExecObject { - program: program_string, - hash: hash.to_string(), - uid: uid, - ts: ts, - success: success - }; +pub fn send_log_event(class: i64, log: String) { if cfg!(feature = "whitelist_test") { return; } - // https://github.com/WhiteBeamSec/WhiteBeam/blob/master/src/library/common/whitelist.rs#L59 - match platform::get_uptime() { - Ok(uptime) => { - if uptime.as_secs() < (60*5) { - return; - } - }, - Err(e) => eprintln!("WhiteBeam: {}", e) + let log_level: i64 = match db::get_setting(String::from("LogVerbosity")).parse() { + Ok(level) => level, + // TODO: Log errors + Err(_) => 1 + }; + if log_level < class { + return; + } + let ts = time::get_timestamp(); + let log_object = LogObject { + class, + log, + ts + }; + let service_port: i32 = match db::get_setting(String::from("ServicePort")).parse() { + Ok(port) => port, + // TODO: Log errors + Err(_) => 11998 }; - let request = match http::post("http://127.0.0.1:11998/log/exec") + let request = match http::post(format!("http://127.0.0.1:{}/log", service_port)) .with_timeout(get_timeout()) - .with_json(&log) { + .with_json(&log_object) { Ok(json_data) => json_data, Err(_e) => { eprintln!("WhiteBeam: Failed to serialize JSON"); diff --git a/src/library/common/hash.rs b/src/library/common/hash.rs deleted file mode 100644 index 13378fb..0000000 --- a/src/library/common/hash.rs +++ /dev/null @@ -1,55 +0,0 @@ -use sodiumoxide::crypto::hash; -use std::{fs, io, io::Read, ffi::OsStr}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::os::unix::io::FromRawFd; -#[cfg(target_os = "windows")] -use std::os::windows::io::FromRawHandle; - -fn common_hash_algo() -> sodiumoxide::crypto::hash::State { - hash::State::new() -} - -pub fn hash_null() -> String { - hex::encode(vec![0; hash::DIGESTBYTES]) -} - -pub fn common_hash_password(input: &str) -> String { - // TODO: Use pwhash - hex::encode(hash::hash(input.as_bytes())) -} - - -pub fn common_hash_data(reader: R) -> String { - let buf_size = 32768; - let mut buf: Vec = Vec::with_capacity(buf_size); - let mut hash_state = common_hash_algo(); - let mut limited_reader = reader.take(buf_size as u64); - loop { - match limited_reader.read_to_end(&mut buf) { - Ok(0) => break, - Ok(_) => { - hash_state.update(&buf[..]); - buf.clear(); - limited_reader = limited_reader.into_inner().take(buf_size as u64); - } - Err(_err) => return hash_null(), - } - } - hex::encode(hash_state.finalize()) -} - -pub fn common_hash_fd(fd: i32) -> String { - #[cfg(target_os = "windows")] - unimplemented!("WhiteBeam: File handles are not currently supported"); - #[cfg(any(target_os = "linux", target_os = "macos"))] - let file = unsafe { fs::File::from_raw_fd(fd) }; - common_hash_data(file) -} - -pub fn common_hash_file(path: &OsStr) -> String { - let file = match fs::File::open(&path) { - Err(_why) => return hash_null(), - Ok(file) => file - }; - common_hash_data(file) -} diff --git a/src/library/common/hash/hashes/argon2id.rs b/src/library/common/hash/hashes/argon2id.rs new file mode 100644 index 0000000..33f5380 --- /dev/null +++ b/src/library/common/hash/hashes/argon2id.rs @@ -0,0 +1,12 @@ +use argon2::PasswordHasher; +#[macro_use] +build_hash! { ARGON2ID (reader, salt_opt) { + let mut password: String = String::new(); + reader.read_to_string(&mut password).expect("WhiteBeam: Could not read password buffer"); + assert!(salt_opt.is_some()); + let salt: String = salt_opt.unwrap(); + // Argon2 with default params (Argon2id v19) + let argon2 = argon2::Argon2::default(); + // Hash password to PHC string ($argon2id$v=19$...) + argon2.hash_password_simple(password.as_bytes(), salt.as_ref()).unwrap().to_string() +}} diff --git a/src/library/common/hash/hashes/blake3.rs b/src/library/common/hash/hashes/blake3.rs new file mode 100644 index 0000000..78814bf --- /dev/null +++ b/src/library/common/hash/hashes/blake3.rs @@ -0,0 +1,20 @@ +#[macro_use] +build_hash! { BLAKE3 (reader, _salt_opt) { + let digestbytes = 32; + let buf_size = 32768; + let mut buf: Vec = Vec::with_capacity(buf_size); + let mut hash_state = blake3::Hasher::new(); + let mut limited_reader = reader.take(buf_size as u64); + loop { + match limited_reader.read_to_end(&mut buf) { + Ok(0) => break, + Ok(_) => { + hash_state.update(&buf[..]); + buf.clear(); + limited_reader = limited_reader.into_inner().take(buf_size as u64); + } + Err(_err) => return "00".repeat(digestbytes), + } + } + hash_state.finalize().to_hex().to_string() +}} diff --git a/src/library/common/hash/hashes/sha3_256.rs b/src/library/common/hash/hashes/sha3_256.rs new file mode 100644 index 0000000..51742ab --- /dev/null +++ b/src/library/common/hash/hashes/sha3_256.rs @@ -0,0 +1,21 @@ +use sha3::Digest; +#[macro_use] +build_hash! { SHA3_256 (reader, _salt_opt) { + let digestbytes = 32; + let buf_size = 32768; + let mut buf: Vec = Vec::with_capacity(buf_size); + let mut hash_state = sha3::Sha3_256::new(); + let mut limited_reader = reader.take(buf_size as u64); + loop { + match limited_reader.read_to_end(&mut buf) { + Ok(0) => break, + Ok(_) => { + hash_state.update(&buf[..]); + buf.clear(); + limited_reader = limited_reader.into_inner().take(buf_size as u64); + } + Err(_err) => return "00".repeat(digestbytes), + } + } + format!("{:x}", hash_state.finalize()) +}} diff --git a/src/library/common/hash/hashes/sha3_512.rs b/src/library/common/hash/hashes/sha3_512.rs new file mode 100644 index 0000000..7621021 --- /dev/null +++ b/src/library/common/hash/hashes/sha3_512.rs @@ -0,0 +1,21 @@ +use sha3::Digest; +#[macro_use] +build_hash! { SHA3_512 (reader, _salt_opt) { + let digestbytes = 64; + let buf_size = 32768; + let mut buf: Vec = Vec::with_capacity(buf_size); + let mut hash_state = sha3::Sha3_512::new(); + let mut limited_reader = reader.take(buf_size as u64); + loop { + match limited_reader.read_to_end(&mut buf) { + Ok(0) => break, + Ok(_) => { + hash_state.update(&buf[..]); + buf.clear(); + limited_reader = limited_reader.into_inner().take(buf_size as u64); + } + Err(_err) => return "00".repeat(digestbytes), + } + } + format!("{:x}", hash_state.finalize()) +}} diff --git a/src/library/common/hash/mod.rs b/src/library/common/hash/mod.rs new file mode 100644 index 0000000..c4159e5 --- /dev/null +++ b/src/library/common/hash/mod.rs @@ -0,0 +1,43 @@ +use std::io::Read; + +pub struct HashObject { + pub alias: &'static str, + pub function: fn(&mut dyn Read, Option) -> String +} + +// Hash template +macro_rules! build_hash { + ($alias:ident ($reader:ident, $salt_opt:ident) $body:block) => { + use std::io::Read; + #[allow(non_snake_case)] + #[allow(unused_assignments)] + #[allow(unused_mut)] + pub fn $alias ($reader: &mut dyn Read, $salt_opt: Option) -> String { + $body + } + #[linkme::distributed_slice(crate::common::hash::HASH_INDEX)] + pub static HASH: crate::common::hash::HashObject = crate::common::hash::HashObject { alias: stringify!($alias), function: $alias }; + }; +} + +// Load hash modules +// TODO: Make sure this doesn't conflict with crate namespace +mod hashes { + automod::dir!(pub "src/library/common/hash/hashes"); +} + +// Collect hashes in distributed slice +#[linkme::distributed_slice] +pub static HASH_INDEX: [HashObject] = [..]; + +pub fn process_hash(reader: &mut dyn Read, algorithm: &str, salt_opt: Option) -> String { + // TODO: Consider removing reference here + match HASH_INDEX.iter().find(|a| format!("Hash/{}", a.alias.replace("_", "-")) == algorithm) { + Some(hash) => {(hash.function)(reader, salt_opt)} + None => panic!("WhiteBeam: Invalid hash algorithm: {}", algorithm) + } +} + +pub fn hash_is_null(input: &str) -> bool { + input.chars().collect::>().iter().all(|&c| c=='0') && (input.len() > 0) +} diff --git a/src/library/common/mod.rs b/src/library/common/mod.rs index b0dced3..935e73b 100644 --- a/src/library/common/mod.rs +++ b/src/library/common/mod.rs @@ -1,8 +1,10 @@ +// Datatype conversion functions +pub mod convert; // Database pub mod db; -// Whitelist -pub mod whitelist; -// Hashing algorithm +// Actions +pub mod action; +// Hashing pub mod hash; // Time functions pub mod time; diff --git a/src/library/common/whitelist.rs b/src/library/common/whitelist.rs deleted file mode 100644 index 4980389..0000000 --- a/src/library/common/whitelist.rs +++ /dev/null @@ -1,101 +0,0 @@ -#[cfg(target_os = "windows")] -use crate::platforms::windows as platform; -#[cfg(target_os = "linux")] -use crate::platforms::linux as platform; -#[cfg(target_os = "macos")] -use crate::platforms::macos as platform; -use crate::common::db; -use std::{ffi::OsStr, ffi::OsString}; - -// Hardcoded whitelist data for setup -fn get_hardcoded_env_blacklist() -> Vec { - vec!( - OsString::from("LD_PRELOAD"), - OsString::from("LD_AUDIT"), - OsString::from("LD_LIBRARY_PATH") - ) -} - -fn get_hardcoded_whitelist() -> Vec<(OsString, bool, String)> { - #[cfg(feature = "whitelist_test")] - return vec!( - (OsString::from("/bin/bash"), true, String::from("ANY")), - // Test seccomp - (OsString::from("/usr/bin/man"), true, String::from("ANY")) - ); - #[cfg(not(feature = "whitelist_test"))] - return vec!( - // Tuple of (permitted program, allow unsafe environment variables, SHA-512 hexdigest) - // Shells - (OsString::from("/bin/bash"), false, String::from("ANY")), - (OsString::from("/bin/sh"), false, String::from("ANY")), - // WhiteBeam - (OsString::from("/opt/WhiteBeam/whitebeam"), false, String::from("ANY")), - (OsString::from("/usr/local/bin/whitebeam"), false, String::from("ANY")) - ) -} - -pub fn is_whitelisted(program: &OsStr, env: &Vec<(OsString, OsString)>, hexdigest: &str) -> bool { - let hardcoded_env_blacklist = get_hardcoded_env_blacklist(); - let hardcoded_whitelist = get_hardcoded_whitelist(); - let mut unsafe_env = false; - for env_var in env { - if hardcoded_env_blacklist.contains(&env_var.0) { - unsafe_env = true; - break; - } - } - // Permit hardcoded application whitelist - for (allowed_program, allow_unsafe, allowed_hash) in hardcoded_whitelist.iter() { - if (&program == allowed_program) && - ((&unsafe_env == allow_unsafe) || cfg!(feature = "whitelist_test")) && - ((&hexdigest == allowed_hash) || (allowed_hash == "ANY")) { - return true; - } - } - if cfg!(feature = "whitelist_test") { - return false; - } - // Introduced limitation: - // WhiteBeam is permissive for up to 5 minutes after boot to avoid interfering with the boot - // process. While attackers should not be able to reboot a system due to whitelisting policy, - // this is a weakness while WhiteBeam is actively developed. Alternatives include: - // 1. Whitelisting all binaries by default, including malware (other EDR software use - // this approach, maintaining a large database of permitted executables) - // 2. Require a reboot to baseline systems (which may interfere with production systems) - // Feedback/ideas welcome: https://github.com/WhiteBeamSec/WhiteBeam/issues - match platform::get_uptime() { - Ok(uptime) => { - if uptime.as_secs() < (60*5) { - return true; - } - }, - Err(e) => eprintln!("WhiteBeam: {}", e) - }; - let conn = match db::db_open() { - Ok(c) => c, - Err(e) => { - // No dynamic whitelist present, deny by default - eprintln!("WhiteBeam: {}", e); - return false; - } - }; - // Permit execution if running in disabled mode - if !(db::get_enabled(&conn)) { - return true; - } - // Permit authorized execution - if db::get_valid_auth_env(&conn) { - return true; - } - // Permit user application whitelist - for dyn_result in db::get_dyn_whitelist(&conn).unwrap_or(Vec::new()).iter() { - if (&program == &OsStr::new(&dyn_result.program)) && - (&unsafe_env == &dyn_result.allow_unsafe) && - ((&hexdigest == &dyn_result.hash) || (&dyn_result.hash == &"ANY")) { - return true; - } - } - // Deny by default - false -} diff --git a/src/library/lib.rs b/src/library/lib.rs index 7d4f6b1..7aae76d 100644 --- a/src/library/lib.rs +++ b/src/library/lib.rs @@ -1,4 +1,11 @@ +// TODO: Eliminate dependency on nightly +// Once cell: https://github.com/rust-lang/rust/issues/74465 +// Once cell can be easily removed, we're just keeping it here in case it gets stabilized +// before variadic functions: https://stackoverflow.com/a/27826181 +#![feature(once_cell)] +// Variadic functions: https://github.com/rust-lang/rust/issues/44930 #![feature(c_variadic)] +//#![feature(asm)] pub mod platforms; // Platform independent features pub mod common; diff --git a/src/library/platforms/linux/hooks/execl.rs b/src/library/platforms/linux/hooks/execl.rs deleted file mode 100644 index 896d0e7..0000000 --- a/src/library/platforms/linux/hooks/execl.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[macro_use] -/* - int execl(const char *path, const char *arg, ... - /* (char *) NULL */); -*/ -build_variadic_exec_hook! { - hook execl (program, args, envp) - custom_routine {} -} diff --git a/src/library/platforms/linux/hooks/execle.rs b/src/library/platforms/linux/hooks/execle.rs deleted file mode 100644 index 00b365f..0000000 --- a/src/library/platforms/linux/hooks/execle.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[macro_use] -/* - int execle(const char *path, const char *arg, ... - /*, (char *) NULL, char * const envp[] */); -*/ -build_variadic_exec_hook! { - hook execle (program, args, envp) - custom_routine { - // Populate envp - let envp_arg: isize = args.arg(); - envp = envp_arg as *const *const libc::c_char; - } -} diff --git a/src/library/platforms/linux/hooks/execlp.rs b/src/library/platforms/linux/hooks/execlp.rs deleted file mode 100644 index f9a399c..0000000 --- a/src/library/platforms/linux/hooks/execlp.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[macro_use] -/* - int execlp(const char *path, const char *arg, ... - /* (char *) NULL */); -*/ -build_variadic_exec_hook! { - hook execlp (program, args, envp) - custom_routine { - // Repopulate program - let absolute_path = match crate::platforms::linux::search_path(&program) { - Some(abspath) => abspath, - None => { - *crate::platforms::linux::errno_location() = libc::ENOENT; - return -1 } - }; - program = absolute_path.as_os_str().to_owned(); - } -} diff --git a/src/library/platforms/linux/hooks/execv.rs b/src/library/platforms/linux/hooks/execv.rs deleted file mode 100644 index 7448866..0000000 --- a/src/library/platforms/linux/hooks/execv.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_use] -/* - int execv(const char *path, char *const argv[]); -*/ -build_exec_hook! { - hook execv (program) - custom_routine {} -} diff --git a/src/library/platforms/linux/hooks/execve.rs b/src/library/platforms/linux/hooks/execve.rs deleted file mode 100644 index 334f4ae..0000000 --- a/src/library/platforms/linux/hooks/execve.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[macro_use] -/* - int execve(const char *path, char *const argv[], - char *const envp[]); -*/ -build_exec_hook! { - hook execve (program, envp) - custom_routine { - // Warn that legacy versions of man-db must disable seccomp - // TODO: Hook proper function - if program == "/usr/bin/man" { - let needle = std::ffi::OsString::from("MAN_DISABLE_SECCOMP"); - let mut disable_defined = false; - let man_env = crate::platforms::linux::parse_env_collection(envp); - for env_var in man_env { - if env_var.0 == needle { - disable_defined = true; - break; - } - } - if !(disable_defined) { - eprintln!("WhiteBeam: Legacy man-db versions require MAN_DISABLE_SECCOMP=1") - } - } - } -} diff --git a/src/library/platforms/linux/hooks/execvp.rs b/src/library/platforms/linux/hooks/execvp.rs deleted file mode 100644 index 6d996e2..0000000 --- a/src/library/platforms/linux/hooks/execvp.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[macro_use] -/* - int execvp(const char *file, char *const argv[]); -*/ -build_exec_hook! { - hook execvp (program) - custom_routine { - // Repopulate program - let absolute_path = match crate::platforms::linux::search_path(&program) { - Some(abspath) => abspath, - None => { - *crate::platforms::linux::errno_location() = libc::ENOENT; - return -1 } - }; - program = absolute_path.as_os_str().to_owned(); - } -} diff --git a/src/library/platforms/linux/hooks/execvpe.rs b/src/library/platforms/linux/hooks/execvpe.rs deleted file mode 100644 index 24571d0..0000000 --- a/src/library/platforms/linux/hooks/execvpe.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[macro_use] -/* - int execvpe(const char *path, char *const argv[], - char *const envp[]); -*/ -build_exec_hook! { - hook execvpe (program, envp) - custom_routine { - // Repopulate program - let absolute_path = match crate::platforms::linux::search_path(&program) { - Some(abspath) => abspath, - None => { - *crate::platforms::linux::errno_location() = libc::ENOENT; - return -1 } - }; - program = absolute_path.as_os_str().to_owned(); - } -} diff --git a/src/library/platforms/linux/hooks/fexecve.rs b/src/library/platforms/linux/hooks/fexecve.rs deleted file mode 100644 index 2bfa6c7..0000000 --- a/src/library/platforms/linux/hooks/fexecve.rs +++ /dev/null @@ -1,18 +0,0 @@ -/* - int fexecve(int fd, char *const argv[], char *const envp[]); -*/ -#[no_mangle] -pub unsafe extern "C" fn fexecve(fd: libc::c_int, argv: *const *const libc::c_char, envp: *const *const libc::c_char) -> libc::c_int { - let program = std::ffi::OsStr::new("fd"); - let env = crate::platforms::linux::parse_env_collection(envp); - let hexdigest = crate::common::hash::common_hash_fd(fd); - let uid = crate::platforms::linux::get_current_uid(); - // Permit/deny execution - if !crate::common::whitelist::is_whitelisted(program, &env, &hexdigest) { - crate::common::event::send_exec_event(uid, program, &hexdigest, false); - *crate::platforms::linux::errno_location() = libc::EACCES; - return -1 - } - crate::common::event::send_exec_event(uid, program, &hexdigest, true); - call_real!{ fexecve (fd: libc::c_int, argv: *const *const libc::c_char, envp: *const *const libc::c_char) -> libc::c_int } -} diff --git a/src/library/platforms/linux/hooks/mod.rs b/src/library/platforms/linux/hooks/mod.rs deleted file mode 100644 index b9803f5..0000000 --- a/src/library/platforms/linux/hooks/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Each hook uses a template -#[macro_use] -mod template; - -/* -exec hooks: Required -+--------+-------------------------------------------------------------------------------------+ -| Letter | Meaning | -+--------+-------------------------------------------------------------------------------------+ -| e | Takes an extra argument to provide the environment of the new program | -| l | Takes the arguments of the new program as a variable-length argument list | -| p | Searches the PATH environment variable to find the program if a path isn't provided | -| v | Takes an array parameter to specify the argv[] array of the new program | -+--------+-------------------------------------------------------------------------------------+ -*/ - -// TODO: Use context hooks to guard against TOCTOU. -// TODO: Whitelist libraries, RPATH -mod execl; -mod execle; -mod execlp; -mod execv; -mod execve; -mod execvp; -mod execvpe; -mod fexecve; - -/* -TODO: open hooks: Required -Protect mem, disk, and other system files using open mode -O_RDWR or O_WRONLY is prohibited, including implicitly (creat) -*/ -// mod creat -// mod creat64 -// mod fopen -// mod fopen64 -// mod freopen -// mod freopen64 -// mod open -// mod open64 -// mod openat -// mod openat64 -// mod open_by_handle_at - -/* -TODO: context hooks: Required -(sym)link/unlink*, *chmod*, rename*, makedev/makenod*, mount, -attr/acl hooks for various filesystems, *init_module, chroot: Optional -*/ - -/* -TODO: socket/SSL hooks: Optional -*/ - -/* -TODO: memory protection hooks: Optional -*/ diff --git a/src/library/platforms/linux/hooks/template.rs b/src/library/platforms/linux/hooks/template.rs deleted file mode 100644 index 14f6c81..0000000 --- a/src/library/platforms/linux/hooks/template.rs +++ /dev/null @@ -1,114 +0,0 @@ -#[macro_export] - -macro_rules! call_real { - ($func_name:ident ( $($v:ident : $t:ty),* ) -> $rt:ty) => { - static mut REAL: *const u8 = 0 as *const u8; - static mut ONCE: ::std::sync::Once = ::std::sync::Once::new(); - ONCE.call_once(|| { - REAL = crate::platforms::linux::dlsym_next(concat!(stringify!($func_name), "\0")); - }); - let rust_func: unsafe extern "C" fn( $($v : $t),* ) -> $rt = std::mem::transmute(REAL); - rust_func( $($v),* ) - } -} - -// Exec hook template -macro_rules! build_exec_hook { - (hook $func_name:ident ($program: ident) custom_routine $body:block) => { - #[no_mangle] - #[allow(unused_mut)] - pub unsafe extern "C" fn $func_name (mut path: *const libc::c_char, argv: *const *const libc::c_char) -> libc::c_int { - let envp: *const *const libc::c_char = std::ptr::null(); - let mut $program = crate::platforms::linux::c_char_to_osstring(path); - $body - let program_c_str = match crate::platforms::linux::osstr_to_cstring(&$program) { - Err(_why) => { - *crate::platforms::linux::errno_location() = libc::ENOENT; - return -1 }, - Ok(res) => res - }; - path = program_c_str.as_ptr() as *const libc::c_char; - let hexdigest = crate::common::hash::common_hash_file(&$program); - let env = crate::platforms::linux::parse_env_collection(envp); - let uid = crate::platforms::linux::get_current_uid(); - // Permit/deny execution - if !crate::common::whitelist::is_whitelisted(&$program, &env, &hexdigest) { - crate::common::event::send_exec_event(uid, &$program, &hexdigest, false); - *crate::platforms::linux::errno_location() = libc::EACCES; - return -1 - } - crate::common::event::send_exec_event(uid, &$program, &hexdigest, true); - call_real!{ $func_name (path: *const libc::c_char, argv: *const *const libc::c_char) -> libc::c_int } - } - }; - (hook $func_name:ident ($program: ident, $envp:ident) custom_routine $body:block) => { - #[no_mangle] - #[allow(unused_assignments)] - #[allow(unused_mut)] - pub unsafe extern "C" fn $func_name (mut path: *const libc::c_char, argv: *const *const libc::c_char, $envp: *const *const libc::c_char) -> libc::c_int { - let mut $program = crate::platforms::linux::c_char_to_osstring(path); - $body - let program_c_str = match crate::platforms::linux::osstr_to_cstring(&$program) { - Err(_why) => { - *crate::platforms::linux::errno_location() = libc::ENOENT; - return -1 }, - Ok(res) => res - }; - path = program_c_str.as_ptr() as *const libc::c_char; - let hexdigest = crate::common::hash::common_hash_file(&$program); - let env = crate::platforms::linux::parse_env_collection($envp); - let uid = crate::platforms::linux::get_current_uid(); - // Permit/deny execution - if !crate::common::whitelist::is_whitelisted(&$program, &env, &hexdigest) { - crate::common::event::send_exec_event(uid, &$program, &hexdigest, false); - *crate::platforms::linux::errno_location() = libc::EACCES; - return -1 - } - crate::common::event::send_exec_event(uid, &$program, &hexdigest, true); - call_real!{ $func_name (path: *const libc::c_char, argv: *const *const libc::c_char, $envp: *const *const libc::c_char) -> libc::c_int } - } - }; -} - -// Variadic exec hook template -macro_rules! build_variadic_exec_hook { - (hook $func_name:ident ($program: ident, $args:ident, $envp:ident) custom_routine $body:block) => { - #[no_mangle] - #[allow(unused_assignments)] - #[allow(unused_mut)] - pub unsafe extern "C" fn $func_name (mut path: *const libc::c_char, mut $args: ...) -> libc::c_int { - // Populate argv - let mut arg_vec: Vec<*const libc::c_char> = Vec::new(); - let mut next_argv: isize = $args.arg(); - let mut ptr_to_next_argv = next_argv as *const libc::c_char; - while !(ptr_to_next_argv).is_null() { - arg_vec.push(ptr_to_next_argv); - next_argv = $args.arg(); - ptr_to_next_argv = next_argv as *const libc::c_char; - } - arg_vec.push(std::ptr::null()); - let argv: *const *const libc::c_char = (&arg_vec).as_ptr() as *const *const libc::c_char; - let mut $envp: *const *const libc::c_char = crate::platforms::linux::environ(); - let mut $program = crate::platforms::linux::c_char_to_osstring(path); - $body - let program_c_str = match crate::platforms::linux::osstr_to_cstring(&$program) { - Err(_why) => { - *crate::platforms::linux::errno_location() = libc::ENOENT; - return -1 }, - Ok(res) => res - }; - path = program_c_str.as_ptr() as *const libc::c_char; - let hexdigest = crate::common::hash::common_hash_file(&$program); - let env = crate::platforms::linux::parse_env_collection($envp); - let uid = crate::platforms::linux::get_current_uid(); - // Permit/deny execution - if !crate::common::whitelist::is_whitelisted(&$program, &env, &hexdigest) { - crate::common::event::send_exec_event(uid, &$program, &hexdigest, false); - *crate::platforms::linux::errno_location() = libc::EACCES; - return -1 - } - crate::common::event::send_exec_event(uid, &$program, &hexdigest, true); - call_real!{ execve (path: *const libc::c_char, argv: *const *const libc::c_char, $envp: *const *const libc::c_char) -> libc::c_int } - } - }; -} diff --git a/src/library/platforms/linux/mod.rs b/src/library/platforms/linux/mod.rs index 1466c7e..3329832 100644 --- a/src/library/platforms/linux/mod.rs +++ b/src/library/platforms/linux/mod.rs @@ -1,55 +1,539 @@ // Load OS-specific modules -mod hooks; +use crate::common::{action, + convert, + db}; use libc::{c_char, c_int, c_void}; -use std::{env, - mem, +use std::{collections::BTreeMap, + env, ffi::CStr, ffi::CString, - ffi::NulError, ffi::OsStr, ffi::OsString, os::unix::ffi::OsStrExt, - os::unix::ffi::OsStringExt, - path::Path, path::PathBuf, - time::Duration}; + lazy::SyncLazy, + sync::Mutex}; + +const LA_FLG_BINDTO: libc::c_uint = 0x01; +const LA_FLG_BINDFROM: libc::c_uint = 0x02; + +// TODO: Hashmap/BTreemap to avoid race conditions, clean up of pthread_self() keys: +// Timestamp attribute, vec. len>0, check timestamp, pthread_equal, RefCell/Cell (?) +static CUR_PROG: SyncLazy> = SyncLazy::new(|| Mutex::new(OsString::new())); +static LIB_MAP: SyncLazy>> = SyncLazy::new(|| Mutex::new(BTreeMap::new())); +static FN_STACK: SyncLazy>> = SyncLazy::new(|| Mutex::new(vec![])); +// TODO: Library cookie Hashmap/BTreemap + +// LinkMap TODO: Review mut, assign libc datatypes? +#[repr(C)] +pub struct LinkMap { + pub l_addr: usize, + pub l_name: *const libc::c_char, + pub l_ld: usize, + pub l_next: *mut LinkMap, + pub l_prev: *mut LinkMap +} + +#[repr(C)] +pub struct Elf32_Sym { + pub st_name: u32, + pub st_value: u32, + pub st_size: u32, + pub st_info: u8, + pub st_other: u8, + pub st_shndx: u16 +} + +#[repr(C)] +pub struct Elf64_Sym { + pub st_name: u32, + pub st_info: u8, + pub st_other: u8, + pub st_shndx: u16, + pub st_value: u64, + pub st_size: u64 +} + +// Debug: Cause a breakpoint exception by invoking the `int3` instruction. +//pub fn int3() { unsafe { asm!("int3"); } } + +// init_rtld_audit_interface +// Initializes WhiteBeam as an LD_AUDIT library +#[used] +#[allow(non_upper_case_globals)] +#[link_section = ".init_array"] +static init_rtld_audit_interface: unsafe extern "C" fn(libc::c_int, *const *const libc::c_char, *const *const libc::c_char) = { + #[link_section = ".text.startup"] + unsafe extern "C" fn init_rtld_audit_interface(argc: libc::c_int, argv: *const *const libc::c_char, envp: *const *const libc::c_char) { + let mut update_ld_audit: bool = false; + let mut update_ld_bind_not: bool = false; + let mut wb_prog_present: bool = false; + let rtld_audit_lib_path = get_rtld_audit_lib_path(); + // la_symbind*() doesn't get called when LD_BIND_NOW is set + // More info: https://sourceware.org/bugzilla/show_bug.cgi?id=23734 + if env::var_os("LD_BIND_NOW").is_some() { + // Technically we're looking for a non-empty string here, but instead we deny it altogether + panic!("WhiteBeam: LD_BIND_NOW restricted"); + } + let new_ld_audit_var: OsString = match env::var_os("LD_AUDIT") { + Some(val) => { + if convert::osstr_split_at_byte(&val, b':').0 == rtld_audit_lib_path { + OsString::new() + } else { + update_ld_audit = true; + let mut new_ld_audit_osstring = OsString::from("LD_AUDIT="); + new_ld_audit_osstring.push(rtld_audit_lib_path.as_os_str()); + new_ld_audit_osstring.push(OsStr::new(":")); + new_ld_audit_osstring.push(val); + new_ld_audit_osstring + } + } + None => { + update_ld_audit = true; + let mut new_ld_audit_osstring = OsString::from("LD_AUDIT="); + new_ld_audit_osstring.push(rtld_audit_lib_path.as_os_str()); + new_ld_audit_osstring + } + }; + let new_ld_bind_not_var: OsString = match env::var_os("LD_BIND_NOT") { + Some(val) => { + if val != OsString::from("1") { + update_ld_bind_not = true; + OsString::from("LD_BIND_NOT=1") + } else { + OsString::new() + } + } + None => { + update_ld_bind_not = true; + OsString::from("LD_BIND_NOT=1") + } + }; + // This variable is protected by WhiteBeam's Essential hooks/rules + let program_path: OsString = match env::var_os("WB_PROG") { + Some(val) => { + wb_prog_present = true; + let mut cur_prog_lock = CUR_PROG.lock().expect("WhiteBeam: Failed to lock mutex"); + cur_prog_lock.clear(); + cur_prog_lock.push(&val); + val + }, + None => { + // TODO: Is this mounted early enough? May need some combination of the canonicalized argv[0] and exe + match std::fs::read_link("/proc/self/exe") { + Ok(v) => { + v.into_os_string() + }, + Err(_e) => { + panic!("WhiteBeam: Lost track of environment"); + } + } + } + }; + // Populate cache + db::populate_cache().expect("WhiteBeam: Could not access database"); + if !(update_ld_audit) && !(update_ld_bind_not) { + // Nothing to do, continue execution + if wb_prog_present { + env::remove_var("WB_PROG"); + } + return; + } + // TODO: Log null reference, process errors + let mut env_vec: Vec<*const libc::c_char> = Vec::new(); + let mut new_ld_audit_cstring: CString = CString::new("").expect("WhiteBeam: Unexpected null reference"); + let mut new_ld_bind_not_cstring: CString = CString::new("").expect("WhiteBeam: Unexpected null reference"); + if update_ld_audit { + // TODO: Log null reference, process errors + new_ld_audit_cstring = convert::osstr_to_cstring(&new_ld_audit_var).expect("WhiteBeam: Unexpected null reference"); + env_vec.push(new_ld_audit_cstring.as_ptr()); + } + if update_ld_bind_not { + // TODO: Log null reference, process errors + new_ld_bind_not_cstring = convert::osstr_to_cstring(&new_ld_bind_not_var).expect("WhiteBeam: Unexpected null reference"); + env_vec.push(new_ld_bind_not_cstring.as_ptr()); + } + let mut program_path_env: OsString = OsString::from("WB_PROG="); + program_path_env.push(&program_path); + let program_path_env_cstring = convert::osstr_to_cstring(&program_path_env).expect("WhiteBeam: Unexpected null reference"); + env_vec.push(program_path_env_cstring.as_ptr()); + let program_path_cstring = convert::osstr_to_cstring(&program_path).expect("WhiteBeam: Unexpected null reference"); + if !(envp.is_null()) { + let mut envp_iter = envp; + while !(*envp_iter).is_null() { + if let Some(key_value) = convert::parse_env_single(CStr::from_ptr(*envp_iter).to_bytes()) { + if (!(update_ld_audit) && (key_value.0 == "LD_AUDIT")) + || (!(update_ld_bind_not) && (key_value.0 == "LD_BIND_NOT")) + || ((key_value.0 != "LD_AUDIT") && (key_value.0 != "LD_BIND_NOT") && (key_value.0 != "WB_PROG")) { + env_vec.push(*envp_iter); + } + } + envp_iter = envp_iter.offset(1); + } + } + env_vec.push(std::ptr::null()); + let new_envp: *const *const libc::c_char = (&env_vec).as_ptr() as *const *const libc::c_char; + // Drop any setuid privileges + let uid = libc::getuid(); + let gid = libc::getgid(); + libc::setresuid(uid, uid, uid); + libc::setresgid(gid, gid, gid); + libc::execve(program_path_cstring.as_ptr(), argv, new_envp); + } + init_rtld_audit_interface +}; + +fn get_argc(args: Vec) -> usize { + let mut argc = 0; + for arg in args { + if arg.parent.is_none() { + argc += 1; + } + } + argc +} + +#[allow(unused_mut)] +unsafe extern "C" fn generic_hook (mut arg1: usize, mut args: ...) -> isize { + // TODO: Test zero argument case + /* + Notes on limitations of WhiteBeam's generic Linux hook, planned to be resolved in future versions of WhiteBeam: + - Can receive any function call and arguments, but hardcoded to call functions with up to 6 arguments + (supports 1,587 out of 1,589 glibc functions) + - 6 out of 1,589 glibc functions are unsupported due to no VaList equivalent + (argp_failure, fcntl, ioctl, makecontext, strfmon, syscall, and ulimit) + - No known security implications while Execution and Filesystem hooks are enforcing prevention mode + */ + // Program + let src_prog: String = { CUR_PROG.lock().expect("WhiteBeam: Failed to lock mutex").clone().into_string().expect("WhiteBeam: Invalid executable name") }; + // Hook + let stack_hook: (i64, usize) = { FN_STACK.lock().expect("WhiteBeam: Failed to lock mutex").pop().expect("WhiteBeam: Lost track of environment") }; + let stack_hook_id = stack_hook.0; + let stack_hook_real = stack_hook.1; + let mut hook: db::HookRow = { + let hook_cache_lock = db::HOOK_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + let hook_option = hook_cache_lock.iter().find(|hook| hook.id == stack_hook_id); + hook_option.expect("WhiteBeam: Lost track of environment").clone() + }; + let hook_orig = hook.clone(); + // Arguments + // TODO: Create Rust structures here with generic T and enum of Datatype rather than passing pointers and leaking memory + // Converted back into respective C datatypes when Actions are completed + // https://doc.rust-lang.org/book/ch10-01-syntax.html + // https://stackoverflow.com/questions/40559931/vector-store-mixed-types-of-data-in-rust + let mut arg_vec: Vec = { + let arg_cache_lock = db::ARG_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + arg_cache_lock.iter().filter(|arg| arg.hook == stack_hook_id).map(|arg| arg.clone()).collect() + }; + // TODO: Pass by reference/slice + let mut argc: usize = get_argc(arg_vec.clone()); + // FIXME: Refactor block, this won't work for edge cases + if argc > 0 { + let mut current_arg_idx = 0; + arg_vec[current_arg_idx].real = arg1 as usize; + current_arg_idx += 1; + for i in current_arg_idx..argc { + if arg_vec[current_arg_idx].variadic { + // TODO: arg_vec.splice() + let mut loops: usize = 0; + let mut do_break: bool = false; + let mut next_argv: usize = args.arg(); + while !(do_break) { + // Excess parameters are truncated in ConsumeVariadic action + if next_argv == 0 { + do_break = true; + } + if loops == 0 { + arg_vec[i].real = next_argv; + } else { + let mut cloned_arg = arg_vec[i].clone(); + cloned_arg.real = next_argv; + current_arg_idx += 1; + arg_vec.insert(current_arg_idx, cloned_arg); + } + if do_break { + break; + } + next_argv = args.arg(); + loops += 1; + } + current_arg_idx += 1; + } else { + arg_vec[current_arg_idx].real = args.arg(); + current_arg_idx += 1; + } + } + } + // Rules + let mut rules: Vec = { + let rule_cache_lock = db::RULE_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + let all_arg_ids: Vec = arg_vec.iter().map(|arg| arg.id).collect(); + rule_cache_lock.iter().filter(|rule| all_arg_ids.contains(&rule.arg)).map(|rule| rule.clone()).collect() + }; + // Actions + for rule in rules { + // TODO: Eliminate redundancy + // TODO: Is clone needed? + let (hook_new, arg_vec_new, do_return, return_value) = action::process_action(src_prog.clone(), rule.clone(), hook.clone(), arg_vec.clone()); + hook = hook_new; + arg_vec = arg_vec_new; + if do_return { + return return_value; + } + }; + // Dispatch + // FIXME: Bug in some *64 functions like open64 => openat and fopen64 => fdopen + let real = match hook_orig.symbol.as_ref() { + "open64" => libc::openat as *const u8, + _ => dlsym_next_relative(&hook.symbol, stack_hook_real) + }; + let hooked_fn_zargs_real: unsafe extern "C" fn() -> isize = std::mem::transmute(real); + let hooked_fn_margs_real: unsafe extern "C" fn(arg1: usize, args: ...) -> isize = std::mem::transmute(real); + // TODO: Pass by reference/slice + argc = get_argc(arg_vec.clone()); + let ret: isize = match argc { + 0 => hooked_fn_zargs_real(), + 1 => hooked_fn_margs_real(arg_vec[0].real), + 2 => hooked_fn_margs_real(arg_vec[0].real, arg_vec[1].real), + 3 => hooked_fn_margs_real(arg_vec[0].real, arg_vec[1].real, arg_vec[2].real), + 4 => hooked_fn_margs_real(arg_vec[0].real, arg_vec[1].real, arg_vec[2].real, arg_vec[3].real), + 5 => hooked_fn_margs_real(arg_vec[0].real, arg_vec[1].real, arg_vec[2].real, arg_vec[3].real, arg_vec[4].real), + 6 => hooked_fn_margs_real(arg_vec[0].real, arg_vec[1].real, arg_vec[2].real, arg_vec[3].real, arg_vec[4].real, arg_vec[5].real), + // Unsupported + _ => panic!("WhiteBeam: Unsupported operation"), + }; + // TODO: Replace below with post action framework (0.2.1 - 0.2.2) + // TODO: May need fopen/fopen64 => fdopen + match (hook_orig.symbol.as_ref(), hook.symbol.as_ref()) { + ("symlink", "symlinkat") => { + libc::close(arg_vec[1].real as i32); + }, + ("link", "linkat") | + ("rename", "renameat") => { + libc::close(arg_vec[0].real as i32); + libc::close(arg_vec[2].real as i32); + }, + ("unlink", "unlinkat") | + ("rmdir", "unlinkat") | + ("chown", "fchownat") | + ("lchown", "fchownat") | + ("chmod", "fchmodat") | + ("creat", "openat") | + ("open", "openat") | + ("creat64", "openat") | + ("open64", "openat") | + ("mknod", "mknodat") | + ("truncate", "ftruncate") => { + libc::close(arg_vec[0].real as i32); + }, + _ => () + }; + ret +} + +// la_version +#[no_mangle] +unsafe extern "C" fn la_version(version: libc::c_uint) -> libc::c_uint { + version +} + +// la_objsearch +#[no_mangle] +unsafe extern "C" fn la_objsearch(name: *const libc::c_char, _cookie: libc::uintptr_t, _flag: libc::c_uint) -> *const libc::c_char { + if !(crate::common::db::get_prevention()) { + return name; + } + // Permit authorized execution + if crate::common::db::get_valid_auth_env() { + return name; + } + let src_prog: String = { CUR_PROG.lock().expect("WhiteBeam: Failed to lock mutex").clone().into_string().expect("WhiteBeam: Invalid executable name") }; + let any = String::from("ANY"); + let class = String::from("Filesystem/Path/Library"); + let all_allowed_libraries: Vec = { + let whitelist_cache_lock = crate::common::db::WL_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + whitelist_cache_lock.iter().filter(|whitelist| (whitelist.class == class) && ((whitelist.path == src_prog) || (whitelist.path == any))).map(|whitelist| whitelist.value.clone()).collect() + }; + // Permit ANY + if all_allowed_libraries.iter().any(|library| library == &any) { + return name; + } + let target_library = String::from(CStr::from_ptr(name).to_str().expect("WhiteBeam: Unexpected null reference")); + // Permit whitelisted libraries + if all_allowed_libraries.iter().any(|library| library == &target_library) { + return name; + } + // Deny by default + crate::common::event::send_log_event(crate::common::event::LogClass::Warn as i64, format!("Blocked {} from executing {} (la_objsearch)", &src_prog, &target_library)); + 0 as *const libc::c_char +} + +// la_objopen +#[no_mangle] +unsafe extern "C" fn la_objopen(map: *const LinkMap, _lmid: libc::c_long, cookie: libc::uintptr_t) -> libc::c_uint { + //libc::printf("WhiteBeam objopen: %s\n\0".as_ptr() as *const libc::c_char, (*map).l_name); + let library_string = String::from(CStr::from_ptr((*map).l_name).to_str().expect("WhiteBeam: Unexpected null reference")); + { + LIB_MAP.lock().unwrap().insert(cookie, library_string); + } + LA_FLG_BINDTO | LA_FLG_BINDFROM +} + +// la_objclose +// TODO: Remove key *cookie from LIB_MAP + +// la_symbind32 +#[no_mangle] +unsafe extern "C" fn la_symbind32(sym: *const Elf32_Sym, _ndx: libc::c_uint, + _refcook: *const libc::uintptr_t, _defcook: *const libc::uintptr_t, + _flags: *const libc::c_uint, symname: *const libc::c_char) -> libc::uintptr_t { + //libc::printf("WhiteBeam symbind32: %s\n\0".as_ptr() as *const libc::c_char, symname); + (*(sym)).st_value as usize +} + +// la_symbind64 +#[no_mangle] +unsafe extern "C" fn la_symbind64(sym: *const Elf64_Sym, _ndx: libc::c_uint, + refcook: *const libc::uintptr_t, defcook: *const libc::uintptr_t, + _flags: *const libc::c_uint, symname: *const libc::c_char) -> libc::uintptr_t { + // Warning: The Rust standard library is not guaranteed to be available during this function + //libc::printf("WhiteBeam symbind64: %s\n\0".as_ptr() as *const libc::c_char, symname); + let symbol_string = String::from(CStr::from_ptr(symname).to_str().expect("WhiteBeam: Unexpected null reference")); + let mut library_string: String = String::new(); + let mut calling_library_string: String = String::new(); + { + let lib_map_lock = LIB_MAP.lock().expect("WhiteBeam: Failed to lock mutex"); + calling_library_string = lib_map_lock.get(&(refcook as usize)).unwrap_or(&String::from("")).clone(); + library_string = lib_map_lock.get(&(defcook as usize)).unwrap_or(&String::from("")).clone(); + }; + // FIXME: Hack around libpam issue + if (calling_library_string == "/lib/x86_64-linux-gnu/libpam.so.0") && (symbol_string == "dlopen") { + return (*(sym)).st_value as usize; + } + { + let hook_cache_lock = db::HOOK_CACHE.lock().expect("WhiteBeam: Failed to lock mutex"); + let hook_cache_iter = hook_cache_lock.iter(); + for hook in hook_cache_iter { + // TODO: Library match + if (hook.symbol == symbol_string) && (hook.library == library_string) { + //libc::printf("WhiteBeam hook: %s\n\0".as_ptr() as *const libc::c_char, symname); + { + let real = (*(sym)).st_value as usize; + FN_STACK.lock().unwrap().push((hook.id, real)); + }; + return generic_hook as usize + } + } + }; + (*(sym)).st_value as usize +} #[link(name = "dl")] extern "C" { - fn dlsym(handle: *const c_void, symbol: *const c_char) -> *const c_void; + fn dladdr1(addr: *const c_void, info: *mut libc::Dl_info, extra_info: *mut *mut c_void, flags: c_int) -> c_int; } -const RTLD_NEXT: *const c_void = -1isize as *const c_void; - -pub unsafe fn dlsym_next(symbol: &'static str) -> *const u8 { - let ptr = dlsym(RTLD_NEXT, symbol.as_ptr() as *const c_char); +pub unsafe fn dlsym_next(symbol: &str) -> *const u8 { + let symbol_cstring: CString = CString::new(symbol).expect("WhiteBeam: Unexpected null reference"); + let ptr = libc::dlsym(libc::RTLD_NEXT, symbol_cstring.as_ptr() as *const c_char); if ptr.is_null() { panic!("WhiteBeam: Unable to find underlying function for {}", symbol); } ptr as *const u8 } +#[allow(non_snake_case)] +pub unsafe fn dlsym_next_relative(symbol: &str, real_addr: usize) -> *const u8 { + // real_addr.base+dlsym_addr.st_addr + // TODO: dlopen(NULL)? + let RTLD_DL_SYMENT: libc::c_int = 1; + let symbol_cstring: CString = CString::new(symbol).expect("WhiteBeam: Unexpected null reference"); + let mut dl_info_dlsym = libc::Dl_info { + dli_fname: core::ptr::null(), + dli_fbase: core::ptr::null_mut(), + dli_sname: core::ptr::null(), + dli_saddr: core::ptr::null_mut(), + }; + let mut dl_info_real = libc::Dl_info { + dli_fname: core::ptr::null(), + dli_fbase: core::ptr::null_mut(), + dli_sname: core::ptr::null(), + dli_saddr: core::ptr::null_mut(), + }; + let mut dl_info_verify = libc::Dl_info { + dli_fname: core::ptr::null(), + dli_fbase: core::ptr::null_mut(), + dli_sname: core::ptr::null(), + dli_saddr: core::ptr::null_mut(), + }; + let mut extra_info_dlsym = std::mem::MaybeUninit::<*mut Elf64_Sym>::uninit(); + let dlsym_addr = libc::dlsym(libc::RTLD_NEXT, symbol_cstring.as_ptr() as *const c_char); + if dlsym_addr.is_null() { + panic!("WhiteBeam: Unable to find underlying function for {}", symbol); + } + let real_addr_base: usize = match libc::dladdr(real_addr as *const c_void, &mut dl_info_real as *mut libc::Dl_info) { + 0 => panic!("WhiteBeam: dladdr failed"), + _ => dl_info_real.dli_fbase as usize + }; + let dlsym_addr_st_addr: usize = match dladdr1(dlsym_addr as *const c_void, &mut dl_info_dlsym as *mut libc::Dl_info, extra_info_dlsym.as_mut_ptr() as *mut *mut libc::c_void, RTLD_DL_SYMENT) { + 0 => panic!("WhiteBeam: dladdr1 failed"), + _ => { + let extra_info_dlsym_init = extra_info_dlsym.assume_init(); + (*extra_info_dlsym_init).st_value as usize + } + }; + let calculated_addr = (real_addr_base+dlsym_addr_st_addr) as *const u8; + match libc::dladdr(calculated_addr as *const c_void, &mut dl_info_verify as *mut libc::Dl_info) { + 0 => panic!("WhiteBeam: dladdr failed"), + _ => { + if !(dl_info_verify.dli_sname.is_null()) { + let sname = String::from(CStr::from_ptr(dl_info_verify.dli_sname).to_str().expect("WhiteBeam: Unexpected null reference")); + assert_eq!(symbol, &sname) + } else { + // Fallback on RTLD_NEXT + // TODO: Determine why this gets called + return dlsym_next(symbol); + } + } + }; + calculated_addr +} + pub fn get_data_file_path(data_file: &str) -> PathBuf { + #[cfg(feature = "whitelist_test")] + let data_path: String = format!("{}/target/release/examples/", env!("PWD")); + #[cfg(not(feature = "whitelist_test"))] let data_path: String = String::from("/opt/WhiteBeam/data/"); let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() + PathBuf::from(data_file_path) } -pub fn get_uptime() -> Result { - let mut info: libc::sysinfo = unsafe { mem::zeroed() }; - let ret = unsafe { libc::sysinfo(&mut info) }; - if ret == 0 { - Ok(Duration::from_secs(info.uptime as u64)) - } else { - Err("sysinfo() failed".to_string()) - } +pub fn get_rtld_audit_lib_path() -> PathBuf { + #[cfg(feature = "whitelist_test")] + let rtld_audit_lib_path = PathBuf::from(format!("{}/target/release/libwhitebeam.so", env!("PWD"))); + #[cfg(not(feature = "whitelist_test"))] + let rtld_audit_lib_path = PathBuf::from(String::from("/lib/libwhitebeam.so")); + rtld_audit_lib_path } pub unsafe fn errno_location() -> *mut c_int { libc::__errno_location() } +pub fn canonicalize_fd(fd: i32) -> Option { + // TODO: Better validation here + if (0 <= fd && fd <= 1024) { + // TODO: Remove dependency on procfs + return std::fs::read_link(format!("/proc/self/fd/{}", fd)).ok(); + } + None +} + +pub fn get_current_gid() -> u32 { + unsafe { libc::getgid() } +} + pub fn get_current_uid() -> u32 { unsafe { libc::getuid() } } @@ -77,8 +561,19 @@ pub fn search_path(program: &OsStr) -> Option { } for mut path in paths { path.push(program); - if path.exists() && path.is_file() { - return Some(path); + let mut stat_struct: libc::stat = unsafe { std::mem::zeroed() }; + let c_path = convert::osstr_to_cstring(path.as_os_str()).expect("WhiteBeam: Unexpected null reference"); + let path_stat = match unsafe { libc::stat(c_path.as_ptr(), &mut stat_struct) } { + 0 => Ok(stat_struct), + _ => Err(unsafe { errno_location() }), + }; + match path_stat { + Ok(valid_path) => { + if (valid_path.st_mode & libc::S_IFMT) == libc::S_IFREG { + return Some(path); + } + } + Err(_) => {} } } None @@ -90,44 +585,3 @@ pub unsafe fn environ() -> *const *const c_char { } environ } - -fn parse_env_single(input: &[u8]) -> Option<(OsString, OsString)> { - if input.is_empty() { - return None; - } - let pos = input[1..].iter().position(|&x| x == b'=').map(|p| p + 1); - pos.map(|p| { - ( - OsStringExt::from_vec(input[..p].to_vec()), - OsStringExt::from_vec(input[p + 1..].to_vec()), - ) - }) -} - -unsafe fn parse_env_collection(envp: *const *const c_char) -> Vec<(OsString, OsString)> { - let mut env: Vec<(OsString, OsString)> = Vec::new(); - if !(envp.is_null()) { - let mut envp_iter = envp; - while !(*envp_iter).is_null() { - if let Some(key_value) = parse_env_single(CStr::from_ptr(*envp_iter).to_bytes()) { - env.push(key_value); - } - envp_iter = envp_iter.offset(1); - } - } - env -} - -unsafe fn c_char_to_osstring(char_ptr: *const c_char) -> OsString { - match char_ptr.is_null() { - true => OsString::new(), - false => { - let program_c_str: &CStr = CStr::from_ptr(char_ptr); - OsStr::from_bytes(program_c_str.to_bytes()).to_owned() - } - } -} - -fn osstr_to_cstring(osstr_input: &OsStr) -> Result { - CString::new(osstr_input.as_bytes()) -} diff --git a/src/library/platforms/macos/mod.rs b/src/library/platforms/macos/mod.rs index d9434ae..27d75e7 100644 --- a/src/library/platforms/macos/mod.rs +++ b/src/library/platforms/macos/mod.rs @@ -1,34 +1,13 @@ // Load OS-specific modules // TODO: DYLD_INSERT_LIBRARIES globally -use std::{mem, - path::Path, - path::PathBuf, - time::Duration}; +use std::path::PathBuf; pub fn get_data_file_path(data_file: &str) -> PathBuf { + #[cfg(feature = "whitelist_test")] + let data_path: String = format!("{}/target/release/examples/", env!("PWD")); + #[cfg(not(feature = "whitelist_test"))] let data_path: String = String::from("/Applications/WhiteBeam/data/"); let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() -} - -pub fn get_uptime() -> Result { - let mut request = [libc::CTL_KERN, libc::KERN_BOOTTIME]; - let mut boottime: libc::timeval = unsafe { mem::zeroed() }; - let mut size: libc::size_t = mem::size_of_val(&boottime) as libc::size_t; - let ret = unsafe { - libc::sysctl( - &mut request[0], - 2, - &mut boottime as *mut libc::timeval as *mut libc::c_void, - &mut size, - std::ptr::null_mut(), - 0, - ) - }; - if ret == 0 { - Ok((time::now().to_timespec() - time::Timespec::new(boottime.tv_sec, boottime.tv_usec * 1000))) - } else { - Err("sysctl() failed".to_string()) - } + PathBuf::from(data_file_path) } diff --git a/src/library/platforms/windows/mod.rs b/src/library/platforms/windows/mod.rs index 716df42..c3a067c 100644 --- a/src/library/platforms/windows/mod.rs +++ b/src/library/platforms/windows/mod.rs @@ -2,19 +2,16 @@ // TODO: AppCert DLLs //use std::env; -use std::{path::Path, - path::PathBuf, - time::Duration}; +use std::path::PathBuf; pub fn get_data_file_path(data_file: &str) -> PathBuf { - // TODO: Change this when registry and environment are secured - //Path::new(env::var("ProgramFiles").unwrap_or("C:\\ProgramFiles").push_str("\\WhiteBeam\\data\\")) + // TODO: Use PWD for Powershell with feature="whitelist_test"? + // TODO: May change this when registry and environment are secured + //PathBuf::from(env::var("ProgramFiles").unwrap_or("C:\\ProgramFiles").push_str("\\WhiteBeam\\data\\")) + #[cfg(feature = "whitelist_test")] + let data_path: String = format!("{}\\target\\release\\examples\\", env!("CD")); + #[cfg(not(feature = "whitelist_test"))] let data_path: String = String::from("C:\\Program Files\\WhiteBeam\\data\\"); let data_file_path = data_path + data_file; - Path::new(&data_file_path).to_owned() -} - -pub fn get_uptime() -> Result { - let ret: u64 = unsafe { kernel32::GetTickCount64() }; - Ok(Duration::from_millis(ret)) + PathBuf::from(data_file_path) } diff --git a/src/library/tests/Cargo.toml b/src/library/tests/Cargo.toml index 01a4cd3..07fdb6a 100644 --- a/src/library/tests/Cargo.toml +++ b/src/library/tests/Cargo.toml @@ -1,7 +1,7 @@ # General info [package] name = "libwhitebeam-tests" -version = "0.1.3" +version = "0.2.0" authors = ["WhiteBeam Security, Inc."] edition = "2018" @@ -12,9 +12,4 @@ path = "main.rs" # Cross-platform dependencies [dependencies] -libc = { version = "0.2" } - -# Windows dependencies -[target.'cfg(target_os = "windows")'.dependencies.kernel32-sys] -version = "0.2" -default-features = false +libc = { version = "~0.2.90" } diff --git a/src/library/tests/main.rs b/src/library/tests/main.rs index 822d0a7..046bef8 100644 --- a/src/library/tests/main.rs +++ b/src/library/tests/main.rs @@ -1,5 +1,3 @@ -use std::env; - pub mod platforms; #[cfg(target_os = "windows")] use platforms::windows as platform; @@ -9,12 +7,5 @@ use platforms::linux as platform; use platforms::macos as platform; fn main() { - let args: Vec = env::args().collect(); - if (args.len()-1) > 1 { - let test = &args[1].to_lowercase(); - let test_type = &args[2].to_lowercase(); - platform::run_test(test, test_type); - } else { - eprintln!("WhiteBeam: No test or test type provided"); - } + platform::run_tests(); } diff --git a/src/library/tests/platforms/linux/mod.rs b/src/library/tests/platforms/linux/mod.rs index 15ca446..35fc52f 100644 --- a/src/library/tests/platforms/linux/mod.rs +++ b/src/library/tests/platforms/linux/mod.rs @@ -1,21 +1,13 @@ // Load OS-specific modules #[macro_use] mod template; -use libc::{c_char, c_int}; -use std::env; -use std::ffi::CString; - - -extern "C" { - pub fn execl(path: *const c_char, args: ...) -> c_int; - pub fn execle(path: *const c_char, args: ...) -> c_int; - pub fn execlp(file: *const c_char, args: ...) -> c_int; - pub fn execv(path: *const c_char, argv: *const *const c_char) -> c_int; - pub fn execve(path: *const c_char, argv: *const *const c_char, envp: *const *const c_char) -> c_int; - pub fn execvp(file: *const c_char, argv: *const *const c_char) -> c_int; - pub fn execvpe(file: *const c_char, argv: *const *const c_char, envp: *const *const c_char) -> c_int; - pub fn fexecve(fd: c_int, argv: *const *const c_char, envp: *const *const c_char) -> c_int; -} +use libc::{execv, execve, execvp, execvpe, execl, execle, execlp, fexecve, + creat, creat64, fdopen, fopen, fopen64, open, open64, openat, openat64, + chmod, fchmod, fchmodat, chown, lchown, fchown, fchownat, + link, linkat, symlink, symlinkat, rename, renameat, renameat2, + rmdir, unlink, unlinkat, truncate, ftruncate, mknod, mknodat}; +use std::os::linux::fs::MetadataExt; +use std::{env, ffi::CString}; test_exec_hook! { test execv (test_execv, mod_env: false, mod_path: false) } test_exec_hook! { test execve (test_execve, mod_env: true, mod_path: false) } @@ -24,27 +16,215 @@ test_exec_hook! { test execvpe (test_execvpe, mod_env: true, mod_path: true) } test_variadic_exec_hook! { test execl (test_execl, mod_env: false, mod_path: false) } test_variadic_exec_hook! { test execle (test_execle, mod_env: true, mod_path: false) } test_variadic_exec_hook! { test execlp (test_execlp, mod_env: false, mod_path: true) } -//test_exec_hook! { test fexecve (test_fexecve, mod_env: false, mod_path: false) } +test_exec_hook! { test fexecve (test_fexecve, mod_env: true, mod_path: false) } +// TODO: For Filesystem tests check test_type, /var/tmp or /dev/shm for denied writes +fn test_creat(test_type: &str) -> i32 { + let _e = std::fs::remove_file("/tmp/test_result_fs"); + unsafe { libc::creat("/tmp/test_result_fs\0".as_ptr() as *const libc::c_char, libc::S_IRUSR | libc::S_IWUSR) } + // TODO: Close fd +} +fn test_creat64(test_type: &str) -> i32 { + let _e = std::fs::remove_file("/tmp/test_result_fs"); + unsafe { libc::creat64("/tmp/test_result_fs\0".as_ptr() as *const libc::c_char, libc::S_IRUSR | libc::S_IWUSR) } + // TODO: Close fd +} +fn test_fdopen(test_type: &str) -> i32 { + let fd = unsafe { libc::open("/tmp/test_result_fs\0".as_ptr() as *const libc::c_char, libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC, libc::S_IRUSR | libc::S_IWUSR | libc::S_IRGRP | libc::S_IROTH as libc::mode_t) }; + let open_file = unsafe { libc::fdopen(fd, "w\x00".as_ptr() as *const libc::c_char) }; + let bytes_written: usize = unsafe { libc::fwrite("test\x00".as_ptr() as *mut libc::c_void, 1 as libc::size_t, 5 as libc::size_t, open_file) }; + if unsafe { libc::fclose(open_file) } == -1 { + return -1 + }; + return bytes_written as i32 +} +fn test_fopen(test_type: &str) -> i32 { + return -1 +} +fn test_fopen64(test_type: &str) -> i32 { + return -1 +} +fn test_open(test_type: &str) -> i32 { + return -1 +} +fn test_open64(test_type: &str) -> i32 { + return -1 +} +fn test_openat(test_type: &str) -> i32 { + return -1 +} +fn test_openat64(test_type: &str) -> i32 { + return -1 +} +fn test_chmod(test_type: &str) -> i32 { + return -1 +} +fn test_fchmod(test_type: &str) -> i32 { + return -1 +} +fn test_fchmodat(test_type: &str) -> i32 { + return -1 +} +fn test_chown(test_type: &str) -> i32 { + return -1 +} +fn test_lchown(test_type: &str) -> i32 { + return -1 +} +fn test_fchown(test_type: &str) -> i32 { + return -1 +} +fn test_fchownat(test_type: &str) -> i32 { + return -1 +} +fn test_link(test_type: &str) -> i32 { + return -1 +} +fn test_linkat(test_type: &str) -> i32 { + return -1 +} +fn test_symlink(test_type: &str) -> i32 { + return -1 +} +fn test_symlinkat(test_type: &str) -> i32 { + return -1 +} +fn test_rename(test_type: &str) -> i32 { + return -1 +} +fn test_renameat(test_type: &str) -> i32 { + return -1 +} +fn test_renameat2(test_type: &str) -> i32 { + return -1 +} +fn test_rmdir(test_type: &str) -> i32 { + return -1 +} +fn test_unlink(test_type: &str) -> i32 { + return -1 +} +fn test_unlinkat(test_type: &str) -> i32 { + return -1 +} +fn test_truncate(test_type: &str) -> i32 { + return -1 +} +fn test_ftruncate(test_type: &str) -> i32 { + return -1 +} +fn test_mknod(test_type: &str) -> i32 { + return -1 +} +fn test_mknodat(test_type: &str) -> i32 { + return -1 +} + +#[derive(Debug)] +struct MetadataExtEq(Option); + +impl PartialEq for MetadataExtEq { + fn eq(&self, other: &MetadataExtEq) -> bool { + if (&self).0.is_some() && (&other).0.is_some() { + let m1 = (&self).0.as_ref().unwrap(); + let m2 = (&other).0.as_ref().unwrap(); + return (m1.st_mode() == m2.st_mode()) + && (m1.st_uid() == m2.st_uid()) && (m1.st_gid() == m2.st_gid()) + && (m1.st_atime() == m2.st_atime()) && (m1.st_atime_nsec() == m2.st_atime_nsec()) + && (m1.st_mtime() == m2.st_mtime()) && (m1.st_mtime_nsec() == m2.st_mtime_nsec()) + && (m1.st_ctime() == m2.st_ctime()) && (m1.st_ctime_nsec() == m2.st_ctime_nsec()) + } + false + } +} -pub fn run_test(test: &str, test_type: &str) { - if test == "execv" { - test_execv(test_type); - } else if test == "execve" { - test_execve(test_type); - } else if test == "execvp" { - test_execvp(test_type); - } else if test == "execvpe" { - test_execvpe(test_type); - } else if test == "execl" { - test_execl(test_type); - } else if test == "execle" { - test_execle(test_type); - } else if test == "execlp" { - test_execlp(test_type); - } /* else if test == "fexecve" { - test_fexecve(test_type); - } */ else { - eprintln!("WhiteBeam: No test found for {}", test); - return; +pub fn run_tests() { + // TODO: Refactor to be similar to action framework, including positive and negative functions (compatability tests), benchmark tests, and vulnerability tests + let tests: Vec<&str> = vec!["positive", "negative"]; + let modules: Vec<&str> = vec!["execl", "execle", "execlp", "execv", "execve", "execvp", "execvpe", "fexecve", + "creat", "creat64", /*"fdopen", "fopen", "fopen64", "open", "open64", "openat", "openat64", + "chmod", "fchmod", "fchmodat", "chown", "lchown", "fchown", "fchownat", + "link", "linkat", "symlink", "symlinkat", "rename", "renameat", "renameat2", + "rmdir", "unlink", "unlinkat", "truncate", "ftruncate", "mknod", "mknodat"*/]; + #[cfg(not(target_os = "linux"))] + unimplemented!("WhiteBeam: Tests on non-Linux platforms are not currently supported"); + for module in modules.clone() { + println!("WhiteBeam: Testing {}", module); + let is_execution = module.contains("exec"); + if !(is_execution) { + // Filesystem hook, create a test file + let _e = std::fs::remove_file("/tmp/test_result_fs"); + std::fs::File::create("/tmp/test_result_fs").unwrap(); + } + let original_metadata = MetadataExtEq(std::fs::metadata("/tmp/test_result_fs").ok()); + for test_type in tests.clone() { + let exit_status_child = match module { + "execv" => test_execv(test_type), + "execve" => test_execve(test_type), + "execvp" => test_execvp(test_type), + "execvpe" => test_execvpe(test_type), + "execl" => test_execl(test_type), + "execle" => test_execle(test_type), + "execlp" => test_execlp(test_type), + "fexecve" => test_fexecve(test_type), + "creat" => test_creat(test_type), + "creat64" => test_creat64(test_type), + "fdopen" => test_fdopen(test_type), + "fopen" => test_fopen(test_type), + "fopen64" => test_fopen64(test_type), + "open" => test_open(test_type), + "open64" => test_open64(test_type), + "openat" => test_openat(test_type), + "openat64" => test_openat64(test_type), + "chmod" => test_chmod(test_type), + "fchmod" => test_fchmod(test_type), + "fchmodat" => test_fchmodat(test_type), + "chown" => test_chown(test_type), + "lchown" => test_lchown(test_type), + "fchown" => test_fchown(test_type), + "fchownat" => test_fchownat(test_type), + "link" => test_link(test_type), + "linkat" => test_linkat(test_type), + "symlink" => test_symlink(test_type), + "symlinkat" => test_symlinkat(test_type), + "rename" => test_rename(test_type), + "renameat" => test_renameat(test_type), + "renameat2" => test_renameat2(test_type), + "rmdir" => test_rmdir(test_type), + "unlink" => test_unlink(test_type), + "unlinkat" => test_unlinkat(test_type), + "truncate" => test_truncate(test_type), + "ftruncate" => test_ftruncate(test_type), + "mknod" => test_mknod(test_type), + "mknodat" => test_mknodat(test_type), + _ => {eprintln!("WhiteBeam: No test found for {}", &module); -1} + }; + match (test_type, is_execution) { + ("positive", true) => { + // Positive Execution test + assert!(exit_status_child >= 0, "WhiteBeam: {} failed ({} test): exit code {}", module, test_type, exit_status_child); + let contents = std::fs::read_to_string("/tmp/test_result").expect("WhiteBeam: Could not read test result file"); + assert_eq!(contents, format!("{}/target/release/libwhitebeam.so", env!("PWD"))); + std::fs::remove_file("/tmp/test_result").expect("WhiteBeam: Failed to remove /tmp/test_result"); + }, + ("negative", true) => { + // Negative Execution test + // TODO: assert!(!exit_status_module.success()); + assert_eq!(std::path::Path::new("/tmp/test_result").exists(), false); + }, + ("positive", false) => { + // Positive Filesystem test + let new_metadata = MetadataExtEq(std::fs::metadata("/tmp/test_result_fs").ok()); + assert_ne!(original_metadata, new_metadata); + }, + ("negative", false) => { + // TODO: Negative Filesystem test + //let new_metadata = MetadataExtEq(std::fs::metadata("/tmp/test_result_fs").ok()); + //assert_eq!(original_metadata, new_metadata); + }, + _ => println!("WhiteBeam: Unknown test type") + }; + println!("WhiteBeam: {} passed ({} test).", module, test_type); + } } + println!("WhiteBeam: All tests passed") } diff --git a/src/library/tests/platforms/linux/template.rs b/src/library/tests/platforms/linux/template.rs index 7696aa6..a313d8d 100644 --- a/src/library/tests/platforms/linux/template.rs +++ b/src/library/tests/platforms/linux/template.rs @@ -2,31 +2,30 @@ macro_rules! exec_hook_template { (test $func_name:ident ($test_name:ident, mod_env: $mod_env:expr, mod_path: $mod_path:expr, $path:ident, $args:ident) custom_routine $body:block) => { - fn $test_name(test_type: &str) { - let ($path, flags, command, bash, sh); - if !($mod_path) { - bash = "/bin/bash"; - sh = "/bin/sh"; - } else { - bash = "bash"; - sh = "sh"; - } + fn $test_name(test_type: &str) -> i32 { + let ($path, flags); + // TODO: Copy bash to /tmp instead of using dash + let (bash, dash) = match $mod_path { + true => ("bash", "dash"), + false => ("/bin/bash", "/bin/dash") + }; if test_type == "positive" { - $path = CString::new(bash).expect("CString::new failed"); + $path = CString::new(bash).expect("WhiteBeam: CString::new failed"); } else if test_type == "negative" { - $path = CString::new(sh).expect("CString::new failed"); + $path = CString::new(dash).expect("WhiteBeam: CString::new failed"); } else { eprintln!("WhiteBeam: Invalid test type. Valid tests are: positive negative"); - return; - } - flags = CString::new("-c").expect("CString::new failed"); - if !($mod_env) { - command = CString::new("echo -n $LD_PRELOAD > /tmp/test_result").expect("CString::new failed"); - } else { - env::set_var("WB_TEST", "invalid"); - command = CString::new("echo -n $WB_TEST > /tmp/test_result").expect("CString::new failed"); + return -1; } - let $args: Vec<*const c_char> = vec!($path.as_ptr(), + flags = CString::new("-c").expect("WhiteBeam: CString::new failed"); + let command = match $mod_env { + true => { + env::set_var("WB_TEST", "invalid"); + CString::new("echo -n $WB_TEST > /tmp/test_result").expect("WhiteBeam: CString::new failed") + }, + false => CString::new("echo -n $LD_PRELOAD > /tmp/test_result").expect("WhiteBeam: CString::new failed") + }; + let $args: Vec<*const libc::c_char> = vec!($path.as_ptr(), flags.as_ptr(), command.as_ptr(), std::ptr::null()); @@ -36,20 +35,70 @@ macro_rules! exec_hook_template { } macro_rules! test_exec_hook { + (test fexecve ($test_name:ident, mod_env: true, mod_path: $mod_path:expr)) => { + exec_hook_template! { test fexecve ($test_name, mod_env: true, mod_path: $mod_path, path, args) + custom_routine { + let pid = unsafe { libc::fork() }; + match pid { + -1 => {return -1}, + 0 => { + let wb_test_env = CString::new(format!("WB_TEST={}/target/release/libwhitebeam.so", env!("PWD"))).expect("WhiteBeam: CString::new failed"); + let env_vec: Vec<*const libc::c_char> = vec!(wb_test_env.as_ptr(), + std::ptr::null()); + let fd: libc::c_int = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) }; + if fd < 0 { + return -1 + } + unsafe { fexecve(fd, args.as_ptr(), env_vec.as_ptr()); } + return -1 + }, + _ => { + let status = 0 as *mut i32; + unsafe {libc::waitpid(pid, status, 0);} + return status as i32 + } + } + } + } + }; (test $func_name:ident ($test_name:ident, mod_env: true, mod_path: $mod_path:expr)) => { exec_hook_template! { test $func_name ($test_name, mod_env: true, mod_path: $mod_path, path, args) custom_routine { - let wb_test_env = CString::new("WB_TEST=./target/release/libwhitebeam.so").expect("CString::new failed"); - let env_vec: Vec<*const c_char> = vec!(wb_test_env.as_ptr(), - std::ptr::null()); - unsafe { $func_name(path.as_ptr(), args.as_ptr(), env_vec.as_ptr()); } + let pid = unsafe { libc::fork() }; + match pid { + -1 => {return -1}, + 0 => { + let wb_test_env = CString::new(format!("WB_TEST={}/target/release/libwhitebeam.so", env!("PWD"))).expect("WhiteBeam: CString::new failed"); + let env_vec: Vec<*const libc::c_char> = vec!(wb_test_env.as_ptr(), + std::ptr::null()); + unsafe { $func_name(path.as_ptr(), args.as_ptr(), env_vec.as_ptr()); } + return -1 + }, + _ => { + let status = 0 as *mut i32; + unsafe {libc::waitpid(pid, status, 0);} + return status as i32 + } + } } } }; (test $func_name:ident ($test_name:ident, mod_env: false, mod_path: $mod_path:expr)) => { exec_hook_template! { test $func_name ($test_name, mod_env: false, mod_path: $mod_path, path, args) custom_routine { - unsafe { $func_name(path.as_ptr(), args.as_ptr()); } + let pid = unsafe { libc::fork() }; + match pid { + -1 => {return -1}, + 0 => { + unsafe { $func_name(path.as_ptr(), args.as_ptr()); } + return -1 + }, + _ => { + let status = 0 as *mut i32; + unsafe {libc::waitpid(pid, status, 0);} + return status as i32 + } + } } } }; @@ -59,26 +108,50 @@ macro_rules! test_variadic_exec_hook { (test $func_name:ident ($test_name:ident, mod_env: true, mod_path: $mod_path:expr)) => { exec_hook_template! { test $func_name ($test_name, mod_env: true, mod_path: $mod_path, path, args) custom_routine { - let wb_test_env = CString::new("WB_TEST=./target/release/libwhitebeam.so").expect("CString::new failed"); - let env_vec: Vec<*const c_char> = vec!(wb_test_env.as_ptr(), - std::ptr::null()); - unsafe { $func_name(path.as_ptr(), - args[0], - args[1], - args[2], - args[3], - env_vec.as_ptr()); } + let pid = unsafe { libc::fork() }; + match pid { + -1 => {return -1}, + 0 => { + let wb_test_env = CString::new(format!("WB_TEST={}/target/release/libwhitebeam.so", env!("PWD"))).expect("WhiteBeam: CString::new failed"); + let env_vec: Vec<*const libc::c_char> = vec!(wb_test_env.as_ptr(), + std::ptr::null()); + unsafe { $func_name(path.as_ptr(), + args[0], + args[1], + args[2], + args[3], + env_vec.as_ptr()); } + return -1 + }, + _ => { + let status = 0 as *mut i32; + unsafe {libc::waitpid(pid, status, 0);} + return status as i32 + } + } } } }; (test $func_name:ident ($test_name:ident, mod_env: false, mod_path: $mod_path:expr)) => { exec_hook_template! { test $func_name ($test_name, mod_env: false, mod_path: $mod_path, path, args) custom_routine { - unsafe { $func_name(path.as_ptr(), - args[0], - args[1], - args[2], - args[3]); } + let pid = unsafe { libc::fork() }; + match pid { + -1 => {return -1}, + 0 => { + unsafe { $func_name(path.as_ptr(), + args[0], + args[1], + args[2], + args[3]); } + return -1 + }, + _ => { + let status = 0 as *mut i32; + unsafe {libc::waitpid(pid, status, 0);} + return status as i32 + } + } } } };