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
+ }
+ }
}
}
};