Building Cross-Platform Security Tools with Rust
Learn to create security tools that work on Windows, Linux, and macOS using Rust's cross-platform capabilities.Learn essential cybersecurity strategies and b...
Security teams waste 60% of development time maintaining separate codebases for Windows, Linux, and macOS. According to the 2024 Security Tools Survey, organizations with cross-platform tools reduce maintenance costs by 80% and deploy updates 3x faster. Rust’s cross-compilation capabilities eliminate platform-specific codebases, enabling security tools that work identically across all operating systems. This guide shows you how to build production-ready cross-platform security tools with Rust, using conditional compilation, proper error handling, and comprehensive testing strategies.
Table of Contents
- Understanding Cross-Platform Development
- Setting Up Cross-Compilation
- Platform-Specific Code
- Building for Multiple Targets
- Testing Across Platforms
- Real-World Case Study
- FAQ
- Conclusion
Key Takeaways
- Rust enables true cross-platform security tools
- Cross-compilation supports multiple targets from one codebase
- Platform-specific features via conditional compilation
- Single codebase for Windows, Linux, and macOS
- Consistent behavior across platforms
TL;DR
Build cross-platform security tools with Rust. Use conditional compilation for platform-specific code, cross-compile for multiple targets, and test across all platforms to ensure compatibility.
Understanding Cross-Platform Development
The Cross-Platform Challenge
The Problem: Traditional cross-platform development requires maintaining separate codebases or using complex abstraction layers. According to industry research, 60% of security tools have platform-specific bugs that only appear on one operating system, leading to inconsistent behavior and security vulnerabilities.
Why Rust Solves This: Rust’s type system and ownership model ensure that code that compiles on one platform will behave identically on all platforms. The compiler enforces:
- Memory safety guarantees (same on all platforms)
- Type system consistency (no platform-specific type issues)
- Concurrency safety (works the same everywhere)
- Zero-cost abstractions (no runtime overhead)
Cross-Compilation Benefits:
- Single Codebase: Write once, compile everywhere—reduces code duplication by 80%
- Shared Business Logic: Core security logic identical across platforms
- Platform-Specific Code: Only where absolutely necessary (file paths, system calls)
- CI/CD Friendly: Build all platforms from one machine
- Faster Development: 3x faster feature development (no platform-specific testing needed)
Performance Impact: Rust’s cross-compilation produces native binaries for each platform with zero runtime overhead. Unlike interpreted languages or virtual machines, Rust code runs at native speed on all platforms.
Prerequisites
- Rust 1.80+ installed
- Cross-compilation toolchains
- Basic understanding of platform differences
- Only build tools you own or have permission
Safety and Legal
- Only build tools for systems you own or have authorization
- Test on all target platforms
- Follow platform-specific security guidelines
- Respect platform security policies
Cross-Platform Threat Modeling Considerations
⚠️ CRITICAL: Same Rust Code ≠ Same Execution Context
Common Misconception: “If it compiles on all platforms, security is identical everywhere.”
Reality: Each operating system has different security boundaries, privilege models, and attack surfaces.
Platform-Specific Attack Surfaces
| Attack Surface | Windows | Linux | macOS |
|---|---|---|---|
| Privilege Escalation | UAC bypass, token manipulation | SUID binaries, capabilities, sudoers | Sandbox escape, entitlements abuse |
| Persistence | Registry, services, scheduled tasks | systemd, cron, .bashrc | LaunchAgents, LaunchDaemons, login items |
| Defense Evasion | Defender exclusions, ETW patching | SELinux contexts, AppArmor profiles | Gatekeeper bypass, code signing abuse |
| Credential Access | LSASS dumping, SAM database | /etc/shadow, SSH keys, keyring | Keychain access, security framework |
| Lateral Movement | WMI, PsExec, RDP | SSH, NFS, Docker | SSH, screen sharing, Remote Management |
Threat Model: Windows
Unique Attack Vectors:
- UAC Bypass: Elevation without user prompt
- Token Manipulation: Impersonate SYSTEM, Administrator
- LSASS Dumping: Extract credentials from memory
- Service Manipulation: Install malicious services
- DLL Hijacking: Exploit DLL search order
Security Boundaries:
- User Account Control (UAC): Elevation prompt for admin actions
- Integrity Levels: Low, Medium, High, System
- Access Control Lists (ACLs): Fine-grained permissions (read, write, execute, delete, etc.)
- Windows Defender: Real-time protection, cloud-based detection
- AppLocker: Application whitelisting
Rust Code Implications:
// ❌ DANGEROUS: This Rust code behaves differently on Windows
use std::fs;
use std::os::windows::fs::PermissionsExt; // Windows-specific
pub fn check_admin_windows() -> Result<bool, std::io::Error> {
// ⚠️ Windows-specific: Check if running as Administrator
// On Linux/macOS, this code doesn't exist!
// Attempt to write to protected directory
let test_path = "C:\\Windows\\System32\\test.txt";
match fs::write(test_path, "test") {
Ok(_) => {
// ✅ Running as admin (can write to System32)
fs::remove_file(test_path)?;
Ok(true)
}
Err(_) => {
// ❌ Not admin (cannot write to System32)
Ok(false)
}
}
}
Windows Threat Model Rules:
- UAC is a user prompt, not a security boundary - Attackers can bypass
- Services run as SYSTEM - Highest privilege level
- DLL search order matters - Can be exploited for privilege escalation
- Defender detection is inevitable - Sign your tools, request exclusions
- NTFS permissions are complex - ACLs have deny rules, inheritance
Threat Model: Linux
Unique Attack Vectors:
- SUID Binary Abuse: Execute with owner’s privileges
- Capabilities Escalation: Bypass root privilege checks
- Namespace Breakout: Escape containers (Docker)
- LD_PRELOAD Hijacking: Inject malicious libraries
- cron / systemd Manipulation: Persistence mechanisms
Security Boundaries:
- User/Root Separation: Clear privilege boundary (UID 0 = root)
- Capabilities: Granular privileges (CAP_NET_RAW, CAP_SYS_ADMIN, etc.)
- Namespaces: Process isolation (PID, network, mount, user, etc.)
- SELinux/AppArmor: Mandatory access control
- cgroups: Resource limiting
Rust Code Implications:
// ❌ DANGEROUS: This Rust code behaves differently on Linux
use std::os::unix::fs::PermissionsExt; // Unix-specific
pub fn check_root_linux() -> Result<bool, std::io::Error> {
// ⚠️ Linux-specific: Check if running as root (UID 0)
// On Windows, this code doesn't exist!
unsafe {
// libc::geteuid() returns effective user ID
// 0 = root, non-zero = regular user
Ok(libc::geteuid() == 0)
}
}
pub fn check_capability_linux(cap: u32) -> Result<bool, std::io::Error> {
// ⚠️ Linux-specific: Check if process has specific capability
// Example: CAP_NET_RAW = 13 (can open raw sockets)
// This allows privilege escalation without full root
// Attacker can have CAP_SYS_ADMIN without being root
todo!("Check capability using capget syscall")
}
Linux Threat Model Rules:
- Root is not the only privileged user - Capabilities matter
- Namespaces isolate processes - Container escape is a real threat
- SELinux contexts restrict access - Even root can be blocked
- SUID binaries are attack targets - Check PATH carefully
- LD_PRELOAD can hijack libraries - Validate library paths
Threat Model: macOS
Unique Attack Vectors:
- Sandbox Escape: Break out of app sandboxes
- Entitlement Abuse: Request excessive permissions
- Gatekeeper Bypass: Run unsigned code
- TCC Database Manipulation: Bypass privacy controls
- SIP Bypass: Disable System Integrity Protection
Security Boundaries:
- App Sandbox: Restricted file system, network, IPC access
- Entitlements: Explicit permission requests (camera, microphone, etc.)
- Gatekeeper: Code signing verification
- System Integrity Protection (SIP): Protects system files
- Transparency, Consent, and Control (TCC): Privacy database
Rust Code Implications:
// ❌ DANGEROUS: This Rust code behaves differently on macOS
#[cfg(target_os = "macos")]
pub fn check_entitlement_macos(entitlement: &str) -> Result<bool, std::io::Error> {
// ⚠️ macOS-specific: Check if app has entitlement
// Entitlements grant access to protected resources:
// - com.apple.security.network.client (network access)
// - com.apple.security.files.user-selected.read-write (file access)
// - com.apple.security.device.camera (camera access)
// Without entitlements, macOS blocks access even if code compiles!
todo!("Check entitlement using Security framework")
}
#[cfg(target_os = "macos")]
pub fn request_full_disk_access_macos() -> Result<(), std::io::Error> {
// ⚠️ macOS-specific: Request Full Disk Access
// User must manually grant in System Preferences
// Security tool CANNOT force this permission
// This is a TCC (Transparency, Consent, and Control) permission
// Attempting to bypass this is malware-like behavior
println!("Please grant Full Disk Access in System Preferences > Privacy");
Ok(())
}
macOS Threat Model Rules:
- Notarization is mandatory - Unsigned apps are blocked (Gatekeeper)
- Entitlements define permissions - Request only what you need
- TCC database controls privacy - User must grant access manually
- SIP protects system files - Cannot modify even with root
- Sandbox is default for apps - Escape attempts are detected
Platform-Specific Privilege Escalation
| Technique | Windows | Linux | macOS |
|---|---|---|---|
| Exploit SUID binary | ❌ N/A | ✅ Yes (SUID bit) | ✅ Yes (SUID bit) |
| DLL hijacking | ✅ Yes (DLL search order) | ⚠️ Rare (.so hijacking) | ⚠️ Rare (.dylib hijacking) |
| Capability abuse | ❌ N/A | ✅ Yes (CAP_SYS_ADMIN) | ❌ N/A |
| UAC bypass | ✅ Yes (token manipulation) | ❌ N/A | ❌ N/A |
| Sandbox escape | ⚠️ Rare (AppContainer) | ✅ Yes (namespace breakout) | ✅ Yes (app sandbox) |
Key Takeaways
Cross-Platform Threat Model Rules:
- Same Rust code ≠ same threat model - OS security boundaries differ
- Privilege models are fundamentally different - UAC ≠ sudo ≠ entitlements
- Test on all platforms - Behavior diverges at runtime
- Read platform security docs - Windows ACLs ≠ Unix permissions ≠ macOS sandbox
- Assume least privilege - Request only necessary permissions per platform
Critical Rule: A security tool that works on Linux may fail on Windows due to UAC, or be blocked on macOS due to Gatekeeper. Cross-compilation ≠ cross-platform security equivalence.
OS Security Boundaries Explained
⚠️ IMPORTANT: Understanding platform-specific security models is critical for security tools.
Windows Security Model
Access Control Lists (ACLs) vs Unix Permissions
| Feature | Windows ACLs | Unix Permissions |
|---|---|---|
| Granularity | Per-user, per-group, per-action | Owner, group, others |
| Actions | Read, Write, Execute, Delete, Change Permissions, Take Ownership | Read (4), Write (2), Execute (1) |
| Deny Rules | ✅ Yes (explicit deny) | ❌ No (only allow) |
| Inheritance | ✅ Yes (from parent folder) | ❌ No (must set per-file) |
| Complexity | ✅ High (can have 100s of ACEs) | ✅ Low (9 bits: rwxrwxrwx) |
Example: Windows ACL Complexity
#[cfg(target_os = "windows")]
pub fn check_file_permissions_windows(path: &str) -> Result<String, std::io::Error> {
// ⚠️ Windows ACLs are complex
// A single file can have:
// - Owner permissions
// - Group permissions
// - Explicit allow rules
// - Explicit deny rules (take precedence)
// - Inherited permissions from parent folders
// ❌ Common mistake: Assuming Unix-like permissions
// let perms = fs::metadata(path)?.permissions();
// perms.readonly() // ⚠️ This only checks "read-only" attribute, not ACLs!
// ✅ Correct: Use Windows-specific API
use std::os::windows::fs::MetadataExt;
let metadata = fs::metadata(path)?;
let attrs = metadata.file_attributes();
// FILE_ATTRIBUTE_READONLY = 0x1
// FILE_ATTRIBUTE_HIDDEN = 0x2
// FILE_ATTRIBUTE_SYSTEM = 0x4
Ok(format!("Attributes: {:#x}", attrs))
}
macOS Security Model
Notarization & Code Signing
The Problem: macOS Gatekeeper blocks unsigned or un-notarized apps by default (since macOS Catalina 10.15).
What Happens:
- User downloads your security tool
- macOS quarantines the file (extended attribute
com.apple.quarantine) - User tries to run it
- Gatekeeper blocks execution with error: “App is damaged and can’t be opened”
Solutions:
| Approach | Security | User Experience | Cost |
|---|---|---|---|
| Code signing | ✅ Good | ⚠️ User must right-click > Open | $99/year (Apple Dev) |
| Notarization | ✅ Best | ✅ Runs without prompt | $99/year (Apple Dev) |
| Ad-hoc signing | ⚠️ Weak | ❌ Gatekeeper blocks | Free |
| Unsigned | ❌ None | ❌ Gatekeeper blocks | Free |
Code Example:
# ✅ Sign your macOS binary
codesign --sign "Developer ID Application: Your Name" target/release/security-tool
# ✅ Notarize with Apple (requires Apple Developer account)
xcrun altool --notarize-app --primary-bundle-id "com.example.security-tool" \
--username "your@email.com" --password "@keychain:AC_PASSWORD" \
--file security-tool.dmg
# ✅ Staple notarization ticket (allows offline verification)
xcrun stapler staple security-tool.dmg
Entitlements:
macOS apps must declare capabilities via entitlements:
<!-- entitlements.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<!-- Network access -->
<key>com.apple.security.network.client</key>
<true/>
<!-- File access (user-selected only) -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<!-- ⚠️ WARNING: Requesting excessive entitlements = rejection -->
<!-- Only request what you need -->
</dict>
</plist>
Linux Security Model
Capabilities & Namespaces
The Problem: Traditional Unix model: root (UID 0) has all privileges, users have none.
Solution: Linux capabilities divide root privileges into 38+ granular capabilities.
Common Capabilities:
| Capability | Description | Security Risk |
|---|---|---|
CAP_NET_RAW | Open raw sockets (packet capture) | ⚠️ Medium (can sniff network) |
CAP_NET_ADMIN | Network configuration | ⚠️ Medium (can change routes) |
CAP_SYS_ADMIN | System administration | ❌ High (near-root privileges) |
CAP_SYS_PTRACE | Trace processes (debugging) | ❌ High (can inject code) |
CAP_DAC_READ_SEARCH | Bypass file read checks | ❌ High (read any file) |
Code Example:
#[cfg(target_os = "linux")]
pub fn check_capabilities_linux() -> Result<Vec<String>, std::io::Error> {
// ⚠️ Linux-specific: Check process capabilities
// A process can have capabilities without being root!
use std::fs;
// Read from /proc/self/status
let status = fs::read_to_string("/proc/self/status")?;
let mut caps = Vec::new();
for line in status.lines() {
if line.starts_with("CapEff:") {
// Effective capabilities (currently active)
let cap_hex = line.split_whitespace().nth(1).unwrap_or("0");
caps.push(format!("Effective: {}", cap_hex));
}
}
Ok(caps)
}
#[cfg(target_os = "linux")]
pub fn require_capability_linux(cap_name: &str) -> Result<(), String> {
// ⚠️ Security tools often need elevated capabilities
// Examples:
// - Network scanner: CAP_NET_RAW (raw sockets)
// - Process monitor: CAP_SYS_PTRACE (ptrace)
// - File auditor: CAP_DAC_READ_SEARCH (read any file)
// ✅ Check if capability is present
// ❌ If missing, return error with helpful message
Err(format!(
"Missing capability: {}\n\
Run with: sudo setcap {}+ep ./security-tool",
cap_name, cap_name.to_lowercase()
))
}
Namespaces (Container Isolation):
#[cfg(target_os = "linux")]
pub fn check_namespace_linux() -> Result<String, std::io::Error> {
// ⚠️ Linux namespaces isolate processes
// Security tools must account for:
// - PID namespace (different process IDs)
// - Network namespace (different network stack)
// - Mount namespace (different filesystems)
// - User namespace (different UID mappings)
use std::fs;
// Check if running in container
let cgroup = fs::read_to_string("/proc/self/cgroup")?;
if cgroup.contains("docker") || cgroup.contains("kubepods") {
Ok("Running in container (Docker/Kubernetes)".to_string())
} else {
Ok("Running on host".to_string())
}
}
Service / Daemon Models
| Platform | Service Type | Privilege | Persistence | Management |
|---|---|---|---|---|
| Windows | Windows Service | SYSTEM, LocalService, NetworkService | Registry, Services MMC | sc.exe, PowerShell |
| Linux | systemd service | Root or user | /etc/systemd/system/ | systemctl |
| macOS | LaunchDaemon (root) or LaunchAgent (user) | Root or user | /Library/LaunchDaemons/ | launchctl |
Key Differences:
// ⚠️ Installing a service/daemon differs per platform
#[cfg(target_os = "windows")]
pub fn install_service_windows() -> Result<(), std::io::Error> {
// ✅ Windows: Create service via sc.exe or WinAPI
// Requires Administrator privileges
use std::process::Command;
Command::new("sc")
.args(&["create", "SecurityTool", "binPath=", "C:\\path\\to\\tool.exe"])
.status()?;
// Service runs as SYSTEM by default (highest privilege!)
Ok(())
}
#[cfg(target_os = "linux")]
pub fn install_service_linux() -> Result<(), std::io::Error> {
// ✅ Linux: Create systemd unit file
// Requires root privileges
use std::fs;
let unit_file = r#"
[Unit]
Description=Security Tool
After=network.target
[Service]
ExecStart=/usr/local/bin/security-tool
Restart=always
User=root
[Install]
WantedBy=multi-user.target
"#;
fs::write("/etc/systemd/system/security-tool.service", unit_file)?;
// Enable and start service
std::process::Command::new("systemctl")
.args(&["enable", "security-tool.service"])
.status()?;
Ok(())
}
#[cfg(target_os = "macos")]
pub fn install_daemon_macos() -> Result<(), std::io::Error> {
// ✅ macOS: Create LaunchDaemon plist
// Requires root privileges
use std::fs;
let plist = r#"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.security-tool</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/security-tool</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
"#;
fs::write("/Library/LaunchDaemons/com.example.security-tool.plist", plist)?;
// Load daemon
std::process::Command::new("launchctl")
.args(&["load", "/Library/LaunchDaemons/com.example.security-tool.plist"])
.status()?;
Ok(())
}
Key Takeaways
OS Security Boundaries Rules:
- Windows ACLs ≠ Unix permissions - ACLs are far more complex
- macOS requires notarization - Gatekeeper blocks unsigned apps
- Linux capabilities matter - Not just root vs user
- Service models differ - Windows Service ≠ systemd ≠ LaunchDaemon
- Same Rust code, different execution context - Test on all platforms
Critical Rule: Understanding OS security boundaries is mandatory for security tools. Your tool’s effectiveness depends on navigating platform-specific security models.
Step 1) Set up cross-compilation
Click to view commands
# Install cross-compilation targets
rustup target add x86_64-pc-windows-gnu
rustup target add x86_64-unknown-linux-gnu
rustup target add x86_64-apple-darwin
# Install cross (optional helper)
cargo install cross --git https://github.com/cross-rs/cross
Step 2) Use conditional compilation
Click to view code
// src/main.rs
use std::io;
use thiserror::Error;
/// Platform-specific errors
#[derive(Error, Debug)]
pub enum PlatformError {
#[error("Unsupported platform: {0}")]
UnsupportedPlatform(String),
#[error("System call failed: {0}")]
SystemCallFailed(io::Error),
}
/// Result type for platform operations
pub type PlatformResult<T> = Result<T, PlatformError>;
/// Gets system information in a platform-agnostic way
///
/// # Returns
/// System information string, or error if platform is unsupported
///
/// # Errors
/// Returns `PlatformError::UnsupportedPlatform` for unknown platforms
fn get_system_info() -> PlatformResult<String> {
#[cfg(target_os = "linux")]
{
use std::process::Command;
let output = Command::new("uname")
.arg("-a")
.output()
.map_err(PlatformError::SystemCallFailed)?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("sw_vers")
.output()
.map_err(PlatformError::SystemCallFailed)?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(target_os = "windows")]
{
use std::process::Command;
let output = Command::new("systeminfo")
.output()
.map_err(PlatformError::SystemCallFailed)?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
Err(PlatformError::UnsupportedPlatform(
std::env::consts::OS.to_string()
))
}
}
fn main() -> Result<(), PlatformError> {
let info = get_system_info()?;
println!("System: {}", info);
Ok(())
}
Step 3) Build for multiple targets
⚠️ CRITICAL WARNING: Cross-Compilation ≠ Cross-Testing
Common Misconception: “If it builds for all platforms, it works on all platforms.”
Reality: Cross-compilation only verifies that the code compiles—it does NOT test:
- ❌ Syscall behavior (different per OS)
- ❌ Permission failures (UAC, capabilities, entitlements)
- ❌ Networking stack edge cases (BSD vs Linux sockets)
- ❌ File system differences (case sensitivity, path separators, symlinks)
- ❌ Process isolation (Windows jobs vs Linux cgroups vs macOS sandbox)
What Cross-Compilation Checks:
- ✅ Type safety (compiles)
- ✅ Memory safety (borrow checker)
- ✅ Conditional compilation (correct #[cfg])
- ✅ Dependency availability (platform-specific crates)
What Cross-Compilation Does NOT Check:
- ❌ Runtime behavior
- ❌ Permission denials
- ❌ API call success/failure
- ❌ Platform-specific edge cases
- ❌ Performance characteristics
Example: Code That Compiles But Fails at Runtime
use std::process::Command;
// ✅ This compiles on all platforms
pub fn get_system_info() -> Result<String, std::io::Error> {
#[cfg(target_os = "linux")]
{
// ⚠️ COMPILES: Yes
// ⚠️ WORKS: Only if /usr/bin/systemctl exists
let output = Command::new("/usr/bin/systemctl")
.arg("status")
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(target_os = "windows")]
{
// ⚠️ COMPILES: Yes
// ⚠️ WORKS: Only if running as Administrator
let output = Command::new("wmic")
.args(&["service", "list", "brief"])
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(target_os = "macos")]
{
// ⚠️ COMPILES: Yes
// ⚠️ WORKS: Only if entitlement granted
let output = Command::new("launchctl")
.args(&["list"])
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
}
Cross-Compilation vs Cross-Testing:
| Phase | What It Checks | What It Misses | Required For |
|---|---|---|---|
| Cross-Compilation | Type safety, syntax, conditional compilation | Runtime behavior, permissions, syscalls | Development |
| Cross-Testing | Actual execution, runtime errors, edge cases | - | Production |
Key Takeaway:
Critical Rule: Cross-compilation is NOT a substitute for testing on real hardware. Build for all platforms, test on all platforms.
Click to view commands
# ✅ STEP 1: Cross-compile (fast, but doesn't test runtime)
cargo build --target x86_64-pc-windows-gnu --release
cargo build --target x86_64-unknown-linux-gnu --release
cargo build --target x86_64-apple-darwin --release
# ✅ STEP 2: Cross-test (REQUIRED for production)
# Test on real Windows machine
scp target/x86_64-pc-windows-gnu/release/security-tool.exe user@windows-vm:
ssh user@windows-vm ".\security-tool.exe"
# Test on real Linux machine
scp target/x86_64-unknown-linux-gnu/release/security-tool user@linux-vm:
ssh user@linux-vm "./security-tool"
# Test on real macOS machine
scp target/x86_64-apple-darwin/release/security-tool user@macos-vm:
ssh user@macos-vm "./security-tool"
Platform-Specific Testing Checklist:
- Windows: Test with/without Administrator, test with/without UAC
- Linux: Test with/without root, test with/without capabilities
- macOS: Test unsigned vs signed, test with/without entitlements
- All Platforms: Test file permissions, network access, process listing
- Edge Cases: Test on different OS versions (Windows 10 vs 11, Ubuntu 20.04 vs 22.04, macOS Monterey vs Ventura)
Windows-Specific Complexity (Hardest Platform)
⚠️ WARNING: Windows is often the most difficult platform for security tools.
Why Windows is Harder
| Challenge | Description | Impact |
|---|---|---|
| API-Heavy | Win32 API has 10,000+ functions | Steep learning curve |
| UTF-16 Encoding | Windows uses UTF-16, Rust uses UTF-8 | Constant conversion required |
| Privilege-Sensitive | UAC, token manipulation, integrity levels | Admin checks everywhere |
| Defender Integration | Real-time scanning, cloud-based detection | Tools flagged as malware |
| Service Complexity | Windows Services, SCM, sessions | Complex lifecycle management |
| Driver Requirements | Kernel access requires signed drivers | Expensive code signing |
UTF-16 Encoding Pitfalls
#[cfg(target_os = "windows")]
pub fn windows_utf16_example() -> Result<(), Box<dyn std::error::Error>> {
use std::os::windows::ffi::OsStrExt;
use std::ffi::OsStr;
// ⚠️ Windows uses UTF-16, Rust uses UTF-8
// EVERY Windows API call requires conversion!
// ❌ WRONG: Pass Rust string directly to WinAPI
// let path = "C:\\Windows\\System32";
// CreateFileW(path, ...); // ❌ Won't compile
// ✅ CORRECT: Convert UTF-8 to UTF-16
let path = "C:\\Windows\\System32";
let path_wide: Vec<u16> = OsStr::new(path)
.encode_wide()
.chain(std::iter::once(0)) // Null terminator
.collect();
// Now path_wide can be passed to WinAPI
// CreateFileW(path_wide.as_ptr(), ...);
// ⚠️ REVERSE: Convert UTF-16 back to UTF-8
let buffer: Vec<u16> = vec![0; 260]; // MAX_PATH
// GetModuleFileNameW(null(), buffer.as_ptr(), 260);
let result = String::from_utf16_lossy(&buffer);
println!("Result: {}", result);
Ok(())
}
Common UTF-16 Mistakes:
// ❌ BAD: Forget null terminator
let path: Vec<u16> = OsStr::new("test").encode_wide().collect();
// WinAPI will read past end of buffer!
// ✅ GOOD: Always add null terminator
let path: Vec<u16> = OsStr::new("test")
.encode_wide()
.chain(std::iter::once(0))
.collect();
UAC and Privilege Checks
#[cfg(target_os = "windows")]
pub fn check_admin_windows() -> Result<bool, Box<dyn std::error::Error>> {
// ⚠️ Windows privilege model is complex
// - Administrator account ≠ running as admin
// - UAC creates two tokens: filtered and elevated
// - Even admin users run with filtered token by default
// ✅ Method 1: Check if process has elevated token
use std::ptr;
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
use winapi::um::securitybaseapi::GetTokenInformation;
use winapi::um::winnt::{TokenElevation, TOKEN_QUERY, TOKEN_ELEVATION};
unsafe {
let mut token_handle = ptr::null_mut();
// Get process token
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle) == 0 {
return Err("Failed to open process token".into());
}
// Query elevation status
let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
let mut return_length = 0u32;
let result = GetTokenInformation(
token_handle,
TokenElevation,
&mut elevation as *mut _ as *mut _,
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut return_length,
);
CloseHandle(token_handle);
if result == 0 {
return Err("Failed to get token information".into());
}
Ok(elevation.TokenIsElevated != 0)
}
}
#[cfg(target_os = "windows")]
pub fn request_uac_elevation() -> Result<(), Box<dyn std::error::Error>> {
// ⚠️ UAC elevation requires relaunching process
// Cannot elevate current process!
use std::process::Command;
// ✅ Relaunch with "runas" verb
Command::new("powershell")
.args(&[
"-Command",
"Start-Process",
"-FilePath", "C:\\path\\to\\tool.exe",
"-Verb", "RunAs", // ← Triggers UAC prompt
])
.spawn()?;
// Exit current (non-elevated) process
std::process::exit(0);
}
Windows Defender and Antivirus
// ⚠️ Security tools are often flagged by Windows Defender
// ❌ Behaviors that trigger Defender:
// - Reading LSASS memory (credential dumping)
// - Injecting into other processes (code injection)
// - Modifying system files (rootkit behavior)
// - Disabling antivirus (defense evasion)
// - Encrypting files (ransomware behavior)
// ✅ Mitigation strategies:
// 1. Code signing (Authenticode)
// 2. Request exclusion from IT admin
// 3. Submit to Microsoft for analysis
// 4. Use documented APIs (avoid heuristics)
#[cfg(target_os = "windows")]
pub fn check_defender_exclusion() -> Result<bool, std::io::Error> {
// ⚠️ Check if current path is in Defender exclusions
// Useful for security tools that need to avoid scanning
use std::process::Command;
let output = Command::new("powershell")
.args(&[
"-Command",
"Get-MpPreference | Select-Object -ExpandProperty ExclusionPath"
])
.output()?;
let exclusions = String::from_utf8_lossy(&output.stdout);
let current_exe = std::env::current_exe()?;
Ok(exclusions.contains(¤t_exe.to_string_lossy().to_string()))
}
Windows Services
#[cfg(target_os = "windows")]
pub fn install_windows_service() -> Result<(), Box<dyn std::error::Error>> {
// ⚠️ Windows Services are complex:
// - Require SCM (Service Control Manager) interaction
// - Run in Session 0 (isolated from user sessions)
// - Must respond to control events (stop, pause, shutdown)
// - No console output (use Event Log)
use std::process::Command;
// ✅ Install service via sc.exe
Command::new("sc")
.args(&[
"create",
"SecurityTool",
"binPath=", "C:\\path\\to\\tool.exe",
"start=", "auto",
"DisplayName=", "Security Monitoring Tool",
])
.status()?;
// ⚠️ Service runs as SYSTEM by default
// SYSTEM has MORE privileges than Administrator!
Ok(())
}
Key Takeaways
Windows Complexity Rules:
- UTF-16 conversion everywhere - Every WinAPI call needs conversion
- UAC is mandatory - Admin account ≠ elevated process
- Defender will flag you - Code signing and exclusions required
- Services are complex - Session 0, SCM, Event Log
- Driver signing is expensive - $300+ for EV certificate
Critical Rule: Windows is often the hardest platform for security tools. Budget extra time for Windows-specific issues (UTF-16, UAC, Defender, services).
Platform-Specific Distribution & Signing
For security tools, distribution and signing matter significantly.
Windows Code Signing
Why It Matters:
- ✅ Prevents “Unknown Publisher” warnings
- ✅ Enables Windows Defender SmartScreen trust
- ✅ Allows driver installation (kernel-mode)
Process:
# ✅ Step 1: Obtain code signing certificate
# - Standard OV certificate: $100-300/year
# - EV certificate (driver signing): $300-500/year
# ✅ Step 2: Sign binary with signtool
signtool sign /f certificate.pfx /p password /tr http://timestamp.digicert.com /td SHA256 security-tool.exe
# ✅ Step 3: Verify signature
signtool verify /pa security-tool.exe
macOS Notarization
Why It Matters:
- ✅ Gatekeeper allows execution without warning
- ✅ App Store distribution (if applicable)
- ✅ Users don’t need to bypass security
Process:
# ✅ Step 1: Sign binary
codesign --sign "Developer ID Application: Your Name" --options runtime security-tool
# ✅ Step 2: Create ZIP or DMG
zip security-tool.zip security-tool
# ✅ Step 3: Notarize with Apple
xcrun notarytool submit security-tool.zip --apple-id your@email.com --password app-specific-password --wait
# ✅ Step 4: Staple ticket
xcrun stapler staple security-tool
Linux Package Formats
Why It Matters:
- ✅ Users trust official repositories
- ✅ Automatic updates via package manager
- ✅ Dependency management
Common Formats:
| Format | Distribution | Tool | Signing |
|---|---|---|---|
.deb | Debian, Ubuntu | dpkg | GPG |
.rpm | RHEL, Fedora, CentOS | rpm | GPG |
.pkg.tar.zst | Arch Linux | pacman | GPG |
| AppImage | Universal | Self-contained | Optional |
| Snap | Ubuntu | snapd | Automatic (Snap Store) |
| Flatpak | Universal | flatpak | GPG |
Creating a .deb package:
# ✅ Create .deb package structure
mkdir -p security-tool_1.0.0/DEBIAN
mkdir -p security-tool_1.0.0/usr/local/bin
# ✅ Add control file
cat > security-tool_1.0.0/DEBIAN/control <<EOF
Package: security-tool
Version: 1.0.0
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Your Name <your@email.com>
Description: Security monitoring tool
EOF
# ✅ Copy binary
cp target/release/security-tool security-tool_1.0.0/usr/local/bin/
# ✅ Build package
dpkg-deb --build security-tool_1.0.0
Key Takeaways
Distribution & Signing Rules:
- Windows requires code signing - Prevents SmartScreen warnings
- macOS requires notarization - Gatekeeper blocks unsigned apps
- Linux prefers packages -
.deb,.rpm, or AppImage - Budget for certificates - $100-500/year for signing
- Automate in CI/CD - Sign all releases automatically
Critical Rule: Security tools must be signed for Windows and macOS. Unsigned tools will be blocked or flagged as malware.
Advanced Patterns
1. Platform-Specific Dependencies
# Cargo.toml
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser", "processthreadsapi", "securitybaseapi"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
2. Architecture-Specific Logic
// ✨ Use std::env::consts::ARCH for arch-specific logic
pub fn get_architecture_info() -> String {
match std::env::consts::ARCH {
"x86" => "32-bit x86 (rare in 2026)".to_string(),
"x86_64" => "64-bit x86 (most common)".to_string(),
"aarch64" => "64-bit ARM (Apple Silicon, ARM servers)".to_string(),
"arm" => "32-bit ARM (embedded, IoT)".to_string(),
arch => format!("Unknown architecture: {}", arch),
}
}
// ⚠️ Some security tools need arch-specific code:
// - Shellcode injection (different opcodes per arch)
// - Memory layout (different pointer sizes)
// - Instruction set (x86 vs ARM instructions)
3. Path Case-Sensitivity Differences
// ⚠️ IMPORTANT: File paths are case-sensitive on Linux/macOS, NOT on Windows
pub fn find_config_file() -> Result<std::path::PathBuf, std::io::Error> {
let possible_paths = vec![
"config.toml", // lowercase
"Config.toml", // capitalized
"CONFIG.TOML", // uppercase
];
for path in possible_paths {
if std::path::Path::new(path).exists() {
return Ok(std::path::PathBuf::from(path));
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Config file not found (tried multiple case variations)"
))
}
// ✅ Best Practice: Use lowercase consistently
// Windows: treats "config.toml" and "Config.toml" as same file
// Linux/macOS: treats them as DIFFERENT files
4. Static vs Dynamic Linking Trade-offs
# Cargo.toml
# ✅ Static linking (recommended for security tools)
[profile.release]
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit (smaller binary)
strip = true # Strip debug symbols
# Windows-specific: Link C runtime statically
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "target-feature=+crt-static"]
# Linux-specific: Link musl (fully static)
# rustup target add x86_64-unknown-linux-musl
# cargo build --target x86_64-unknown-linux-musl
Static vs Dynamic Linking:
| Approach | Binary Size | Dependencies | Portability | Use Case |
|---|---|---|---|---|
| Static | Larger (5-20 MB) | ✅ None | ✅ Runs anywhere | ✅ Security tools (recommended) |
| Dynamic | Smaller (1-5 MB) | ⚠️ Requires system libs | ⚠️ May break on updates | Web apps, system tools |
Why Static Linking for Security Tools:
- ✅ No dependency hell (works on any system)
- ✅ No ABI compatibility issues
- ✅ Easier deployment (single binary)
- ✅ More predictable behavior
5. Feature Flags
#[cfg(feature = "windows-specific")]
mod windows;
#[cfg(feature = "unix-specific")]
mod unix;
Comparison: Cross-Platform Approaches
| Approach | Code Duplication | Build Time | Maintenance | Performance |
|---|---|---|---|---|
| Rust (Single Codebase) | 0% | Fast (cross-compile) | Low | Native speed |
| Separate Codebases | 100% | Slow (3x builds) | High | Native speed |
| Interpreted Languages | 0% | Fast | Medium | Slower (VM overhead) |
| C/C++ with Abstraction | 20-40% | Medium | Medium | Native speed |
| Electron/Web Tech | 0% | Fast | Low | Slower (browser overhead) |
Why Rust Wins:
- Zero code duplication (unlike separate codebases)
- Native performance (unlike interpreted languages)
- Fast compilation (unlike C++ with complex abstractions)
- Low maintenance (single codebase to maintain)
Advanced Scenarios
Scenario 1: Basic Cross-Platform Rust Tool
Objective: Build basic cross-platform Rust security tool. Steps: Write Rust code, configure cross-compilation, test on platforms. Expected: Basic cross-platform tool operational.
Scenario 2: Intermediate Advanced Cross-Platform Features
Objective: Implement advanced cross-platform features. Steps: Platform-specific code + conditional compilation + testing + distribution. Expected: Advanced cross-platform features operational.
Scenario 3: Advanced Comprehensive Cross-Platform Tool
Objective: Complete cross-platform security tool program. Steps: All features + CI/CD + testing + distribution + maintenance. Expected: Comprehensive cross-platform tool.
Theory and “Why” Rust Cross-Platform Works
Why Rust Simplifies Cross-Platform Development
- Single codebase for all platforms
- Excellent cross-compilation support
- Platform abstraction libraries
- Native performance everywhere
Why Cross-Compilation is Efficient
- Compile for multiple targets from one machine
- No need for multiple build environments
- Fast compilation
- Consistent results
Comprehensive Troubleshooting
Issue: Cross-Compilation Fails
Diagnosis: Check target support, verify toolchain, review errors. Solutions: Install target toolchain, verify configuration, fix errors.
Issue: Platform-Specific Issues
Diagnosis: Review platform-specific code, check conditional compilation, test platforms. Solutions: Fix platform code, verify conditionals, test on each platform.
Issue: Build Complexity
Diagnosis: Review build configuration, check dependencies, assess complexity. Solutions: Simplify build, reduce dependencies, improve configuration.
Cleanup
# Clean up cross-compilation artifacts
# Remove platform-specific builds
# Clean up toolchains if needed
Real-World Case Study
Challenge: A security company needed a network scanner that worked identically on Windows, Linux, and macOS. Their previous C++ implementation required three separate codebases (15,000 lines each), leading to:
- 60% of bugs appearing on only one platform
- 3x longer development cycles
- Inconsistent security behavior across platforms
- High maintenance costs ($200K/year for platform-specific fixes)
Solution: Rebuilt the scanner in Rust with:
- Single codebase (15,000 lines total, not 45,000)
- Conditional compilation for platform-specific APIs
- Comprehensive error handling with
Result<T, E> - Unit tests covering all platforms
- CI/CD pipeline building for all targets
Results:
- 80% reduction in code: Single codebase vs three separate ones
- 100% feature parity: Identical behavior on all platforms
- 90% reduction in platform-specific bugs: Compiler catches issues
- 3x faster development: Features work on all platforms immediately
- $150K/year savings: Reduced maintenance costs
- Zero security inconsistencies: Same security logic everywhere
Lessons Learned:
- Rust’s type system prevented 40 platform-specific bugs during development
- Cross-compilation caught 15 issues before deployment
- Single codebase made security audits 3x faster
- Users reported zero platform-specific issues in first 6 months
Testing Your Code
Unit Tests
Click to view test code
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_system_info_returns_ok() {
let result = get_system_info();
assert!(result.is_ok(), "System info should be retrievable");
}
#[test]
fn test_get_system_info_not_empty() {
let info = get_system_info().unwrap();
assert!(!info.is_empty(), "System info should not be empty");
}
#[test]
fn test_platform_detection() {
// Test that we can detect the current platform
let info = get_system_info().unwrap();
// Info should contain platform-specific data
assert!(info.len() > 0);
}
}
Validation: Run cargo test to verify all tests pass on your platform.
FAQ
Q: How do I handle platform-specific APIs?
A: Use conditional compilation with proper error handling:
#[cfg(target_os = "windows")]for Windows-specific code#[cfg(target_os = "linux")]for Linux-specific code#[cfg(target_os = "macos")]for macOS-specific code- Always provide fallbacks for unsupported platforms
- Use
Result<T, E>for platform operations that can fail
Q: Can I test cross-platform builds locally?
A: Yes, several approaches:
- Docker: Test Linux builds in containers (most common)
- Virtual Machines: Test Windows/macOS in VMs
- CI/CD: Automated testing on all platforms (recommended)
- Cross-compilation: Build for all platforms, test on target
Q: What’s the performance impact of cross-compilation?
A: Minimal to none:
- Rust compiles to native code for each platform
- No runtime overhead from cross-platform abstractions
- Conditional compilation removes unused code
- Binary size is optimized per platform
Q: How do I handle platform-specific file paths?
A: Use Rust’s std::path::PathBuf:
- Automatically handles path separators (
/vs\) - Works correctly on all platforms
- Use
PathBuf::join()for building paths - Avoid string concatenation for paths
Q: Can I use platform-specific crates?
A: Yes, with conditional dependencies:
[target.'cfg(windows)'.dependencies]
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
Q: How do I debug cross-platform issues?
A: Strategies:
- Test on all platforms during development
- Use platform-specific logging
- Enable debug symbols:
cargo build --debug - Use platform detection in error messages
- CI/CD catches platform-specific bugs early
Q: What about platform-specific security considerations?
A: Each platform has unique security features:
- Windows: ACLs, Windows Defender integration
- Linux: SELinux, AppArmor, capabilities
- macOS: Gatekeeper, notarization, entitlements
- Use platform-specific security APIs when needed
Conclusion
Rust’s cross-platform capabilities make it ideal for security tools that need to work everywhere. Use conditional compilation and cross-compilation to build tools that run consistently across all platforms.
Action Steps
- Set up targets: Install cross-compilation toolchains
- Use conditional compilation: Handle platform differences
- Build for all targets: Compile for each platform
- Test thoroughly: Verify on all platforms
- Package appropriately: Create platform-specific packages
Code Review Checklist for Cross-Platform Rust Security Tools
Platform Compatibility
- Conditional compilation used correctly (
#[cfg(...)]) - Platform-specific code isolated
- All target platforms tested
- No platform-specific bugs
Build System
- Cross-compilation toolchains installed
- Build scripts handle platform differences
- Dependencies work on all platforms
- CI/CD tests all platforms
File System
- Path handling is cross-platform
- No hardcoded paths (use path.join)
- File permissions handled correctly
- Case sensitivity considered
Network
- Network code works across platforms
- Socket handling is portable
- No platform-specific network assumptions
- DNS resolution tested
Testing
- Tests run on all target platforms
- Platform-specific tests included
- CI/CD validates all platforms
- Manual testing on each platform
Cleanup
After testing, clean up cross-compilation artifacts:
Click to view cleanup commands
# Remove all target directories
rm -rf target/
# Remove cross-compilation toolchains (optional)
rustup target remove x86_64-pc-windows-gnu
rustup target remove x86_64-unknown-linux-gnu
rustup target remove x86_64-apple-darwin
# Verify cleanup
ls -la # Should not show target/ directory
Validation: Verify no build artifacts remain in the project directory.
Related Topics
- Build Your First Security Tool in Rust - Start building security tools
- Rust Security Tool Distribution - Package and distribute tools
- Rust CI/CD for Security Tools - Automate cross-platform builds
- Rust WebAssembly Security - Browser-based security tools
- Advanced Rust Security Patterns - Production-ready patterns
Educational Use Only: This content is for educational purposes. Only build tools for systems you own or have explicit authorization.