Port to FreeBSD with security parity and static binary support
ober
925900b4df4d12fe2bb6f74f39ec43a078e5880f
--- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,9 @@ If the Docker build fails, fix it before doing anything else. ```bash make # Show help / list targets make build # Compile all .sls → .so modules -make docker # Build fully static binary in Docker -make secmon-musl-local # Build static binary locally (needs musl) +make secmon-static # Build static binary on FreeBSD +make docker # Build fully static binary in Docker (Linux) +make secmon-musl-local # Build static binary locally (Linux, needs musl) make clean # Remove all build artifacts ``` @@ -31,6 +32,9 @@ Reimplementation of the Rust security monitoring agent (~/mine/secmon) in Scheme - Crypto: Uses `(std crypto native-rust)` — Rust `ring` library via FFI. **Never use `(std crypto digest)` — it shells out to `openssl`.** - SQLite: Uses `(std db sqlite-native)` — Rust `rusqlite` via FFI - Prelude: Always use `(jerboa prelude clean)`, never `(jerboa prelude)` (has a bug with `while` export) +- Platform: FreeBSD uses `(secmon platform freebsd)`, Linux uses `(secmon platform linux)` +- Monitors use `on-freebsd?` runtime check: `(string-contains (symbol->string (machine-type)) "fb")` +- FreeBSD errno: use `__error` (not `__errno_location`), no `prctl`, no `/proc/self/status` ## Architecture @@ -38,7 +42,7 @@ Reimplementation of the Rust security monitoring agent (~/mine/secmon) in Scheme lib/secmon/ ├── crypto/ # ECIES encryption, PSK auth, key generation ├── monitor/ # 16 security monitors + events + suspicious patterns -├── platform/ # Linux /proc abstraction +├── platform/ # Platform abstraction (linux.sls, freebsd.sls) ├── buffer/ # Encrypted event ring buffer ├── server/ # TCP listener + wire protocol ├── storage/ # SQLite event store --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ SCHEME ?= scheme -JERBOA ?= $(HOME)/mine/jerboa/lib +JERBOA ?= $(HOME)/jerboa/lib LIBDIRS = lib:$(JERBOA) NATIVE_RS ?= $(HOME)/mine/jerboa/jerboa-native-rs/target/release export LD_LIBRARY_PATH := $(NATIVE_RS):$(LD_LIBRARY_PATH) -.PHONY: all help build compile clean test keygen agent collector analyze docker secmon-musl secmon-musl-local verify-harden +.PHONY: all help build compile clean test keygen agent collector analyze docker secmon-musl secmon-musl-local secmon-static verify-harden all: help @@ -13,8 +13,9 @@ help: @echo "" @echo "Build:" @echo " make build Compile all .sls → .so modules" - @echo " make docker Build fully static binary in Docker" - @echo " make secmon-musl-local Build static binary locally (needs musl)" + @echo " make secmon-static Build static binary on FreeBSD" + @echo " make docker Build fully static binary in Docker (Linux)" + @echo " make secmon-musl-local Build static binary locally (Linux, needs musl)" @echo " make clean Remove all build artifacts" @echo "" @echo "Run:" @@ -49,6 +50,12 @@ collector: analyze: $(SCHEME) --libdirs $(LIBDIRS) --script bin/analyze.ss $(ARGS) +# ─── FreeBSD Static Binary ─────────────────────────────────────────────────── + +secmon-static: + @echo "=== Building static secmon-agent for FreeBSD ===" + ./build-secmon-static.sh + # ─── musl Static Binary ───────────────────────────────────────────────────── # Default: build via Docker for reproducibility and correct toolchain deps. # Use `make secmon-musl-local` to build directly on the host (requires musl-gcc, --- a/bin/agent.ss +++ b/bin/agent.ss @@ -10,7 +10,7 @@ (secmon server listener) (secmon monitor events) (secmon platform provider) - (secmon platform linux) + (secmon platform freebsd) (secmon monitor process) (secmon monitor network) (secmon monitor files) @@ -61,8 +61,8 @@ [psk-auth (make-psk-auth psk)] [buffer (make-event-buffer encryptor (agent-config-max-buffer-size config))] - [proc-provider (create-linux-process-provider)] - [net-provider (create-linux-network-provider)] + [proc-provider (create-freebsd-process-provider)] + [net-provider (create-freebsd-network-provider)] [hostname ((process-provider-get-hostname proc-provider))] [poll-ms (agent-config-poll-interval-ms config)]) --- a/build-all.ss +++ b/build-all.ss @@ -9,7 +9,7 @@ (secmon monitor suspicious) ;; Platform (secmon platform provider) - (secmon platform linux) + (secmon platform freebsd) ;; Buffer (secmon buffer ring) ;; Server new file mode 100755 --- /dev/null +++ b/build-secmon-static.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# build-secmon-static.sh — Build static secmon-agent binary on FreeBSD +set -eu + +JERBOA_LIB="${JERBOA_DIR:-$HOME/jerboa/lib}" +NATIVE_RS="${NATIVE_RS:-$HOME/jerboa/jerboa-native-rs/target/release}" + +echo "=== Building secmon-agent (FreeBSD static) ===" +echo " Jerboa lib: $JERBOA_LIB" +echo " Native RS: $NATIVE_RS" +echo "" + +export LD_LIBRARY_PATH="${NATIVE_RS}:${LD_LIBRARY_PATH:-}" + +scheme -q --libdirs "lib:${JERBOA_LIB}" < build-secmon-static.ss + +echo "" +echo "=== Build complete ===" +if [ -f secmon-agent ]; then + echo " Size: $(ls -lh secmon-agent | awk '{print $5}')" + file secmon-agent +fi new file mode 100644 --- /dev/null +++ b/build-secmon-static.ss @@ -0,0 +1,490 @@ +#!chezscheme +;;; build-secmon-static.ss — Build a fully static secmon-agent binary on FreeBSD +;;; +;;; Usage: scheme -q --libdirs lib:<jerboa-lib> < build-secmon-static.ss +;;; +;;; This script: +;;; 1. Stages jerboa modules with load-shared-object patched out +;;; 2. Compiles all jerboa + secmon modules from staged/patched sources +;;; 3. Creates boot file + optimized program .so +;;; 4. Generates C files with embedded boot data + FFI symbol registration +;;; 5. Compiles C with cc against Chez's scheme.h +;;; 6. Links fully static binary with libkernel.a + libjerboa_native.a +;;; +;;; The resulting secmon-agent binary has zero runtime dependencies. + +(import + (except (chezscheme) void box box? unbox set-box! + andmap ormap iota last-pair find + 1+ 1- fx/ fx1+ fx1- + error error? raise with-exception-handler identifier? + hash-table? make-hash-table) + (jerboa build)) + +;; ========== Locate directories ========== + +(define chez-lib-dir + (or (getenv "CHEZ_LIB_DIR") + "/usr/local/lib/csv10.3.0/ta6fb")) + +(printf "Chez lib dir: ~a~n" chez-lib-dir) + +(define jerboa-dir + (or (getenv "JERBOA_DIR") + (let ([home (getenv "HOME")]) + (or (let ([p (format "~a/jerboa/lib" home)]) + (and (file-exists? p) p)) + (format "~a/mine/jerboa/lib" home))))) + +(define native-lib-path + (or (getenv "NATIVE_LIB") + (format "~a/jerboa/jerboa-native-rs/target/release/libjerboa_native.a" + (getenv "HOME")))) + +(define has-native-lib? (file-exists? native-lib-path)) +(unless has-native-lib? + (printf "ERROR: libjerboa_native.a not found at ~a~n" native-lib-path) + (printf "Build with: cd ~/jerboa/jerboa-native-rs && cargo build --release~n") + (exit 1)) + +(printf "Native lib: ~a~n~n" native-lib-path) + +;; ========== Step 0: Stage and patch jerboa modules for static builds ========== + +(printf "[0/7] Patching jerboa modules for static build (no dlopen)...~n") + +(define jerboa-stage (format "~a/jerboa-stage" (current-directory))) +(system (format "rm -rf '~a'" jerboa-stage)) +(system (format "mkdir -p '~a'" jerboa-stage)) + +;; Copy the jerboa lib tree contents to staging +(system (format "cp -a '~a/.' '~a/'" jerboa-dir jerboa-stage)) + +;; Patch all load-shared-object calls in staged copies +(system (format "find '~a' -name '*.sls' -exec sed -i '' 's/(load-shared-object [^)]*)/(void)/g' {} +" jerboa-stage)) +;; Delete pre-compiled .so/.wpo files to force recompilation from patched sources +(system (format "find '~a' -name '*.so' -delete" jerboa-stage)) +(system (format "find '~a' -name '*.wpo' -delete" jerboa-stage)) + +(printf " Patched jerboa sources staged in ~a~n" jerboa-stage) + +;; ========== Override library-directories ========== + +(define build-lib-dirs + (list (cons "lib" "lib") + (cons jerboa-stage jerboa-stage))) + +;; ========== Step 1: Compile jerboa boot modules from staged sources ========== + +(printf "~n[1/7] Compiling jerboa modules (patched, from stage)...~n") + +(define jerboa-boot-modules + '("jerboa/core" "jerboa/runtime" + "std/error" "std/format" "std/sort" "std/pregexp" "std/sugar" + "std/misc/string" "std/misc/list" "std/misc/alist" "std/misc/thread" + "std/misc/ports" + "std/foreign" + "std/os/path" "std/os/file-info" + "std/text/hex" "std/text/json" + "std/match2" + "std/crypto/native-rust" + "std/db/sqlite-native" + "std/gambit-compat" + "jerboa/ffi" + "jerboa/prelude/clean")) + +(parameterize ([compile-imported-libraries #t] + [optimize-level 2] + [generate-inspector-information #f] + [library-directories build-lib-dirs]) + (for-each + (lambda (m) + (let ([sls (format "~a/~a.sls" jerboa-stage m)]) + (when (file-exists? sls) + (printf " Compiling ~a~n" m) + (compile-library sls)))) + jerboa-boot-modules)) + +;; ========== Step 2: Compile all secmon modules ========== + +(printf "~n[2/7] Compiling secmon modules...~n") + +(parameterize ([optimize-level 2] + [generate-inspector-information #f] + [compile-imported-libraries #t] + [library-directories build-lib-dirs]) + ;; Obfuscate macro (needed by many modules — compile first) + (printf " Compiling lib/secmon/stealth/obfuscate.sls~n") + (compile-library "lib/secmon/stealth/obfuscate.sls") + + ;; Crypto modules (dependency order) + (for-each + (lambda (m) + (let ([path (format "lib/secmon/crypto/~a.sls" m)]) + (printf " Compiling ~a~n" path) + (compile-library path))) + '("keys" "ecies" "psk")) + + ;; Monitor events (base types first) + (printf " Compiling lib/secmon/monitor/events.sls~n") + (compile-library "lib/secmon/monitor/events.sls") + (printf " Compiling lib/secmon/monitor/suspicious.sls~n") + (compile-library "lib/secmon/monitor/suspicious.sls") + + ;; Platform — FreeBSD instead of Linux + (for-each + (lambda (m) + (let ([path (format "lib/secmon/platform/~a.sls" m)]) + (printf " Compiling ~a~n" path) + (compile-library path))) + '("provider" "freebsd")) + + ;; Buffer + (printf " Compiling lib/secmon/buffer/ring.sls~n") + (compile-library "lib/secmon/buffer/ring.sls") + + ;; Server + (for-each + (lambda (m) + (let ([path (format "lib/secmon/server/~a.sls" m)]) + (printf " Compiling ~a~n" path) + (compile-library path))) + '("protocol" "listener")) + + ;; Config + (printf " Compiling lib/secmon/config.sls~n") + (compile-library "lib/secmon/config.sls") + + ;; Storage + (printf " Compiling lib/secmon/storage/store.sls~n") + (compile-library "lib/secmon/storage/store.sls") + + ;; All monitor modules + (for-each + (lambda (m) + (let ([path (format "lib/secmon/monitor/~a.sls" m)]) + (printf " Compiling ~a~n" path) + (compile-library path))) + '("process" "network" "files" "auth" "kernel" "cron" + "container" "dns" "rootkit" "persistence" "revshell" + "lateral" "logtamper" "webshell" "podman" "selinux")) + + ;; Stealth modules + (for-each + (lambda (m) + (let ([path (format "lib/secmon/stealth/~a.sls" m)]) + (printf " Compiling ~a~n" path) + (compile-library path))) + '("anti-debug" "masquerade" "env-sanitize" "integrity" "init"))) + +;; ========== Step 3: Compile agent program ========== + +(printf "~n[3/7] Compiling bin/agent.ss (optimize-level 3)...~n") + +(parameterize ([compile-imported-libraries #t] + [optimize-level 3] + [cp0-effort-limit 500] + [cp0-score-limit 50] + [cp0-outer-unroll-limit 1] + [commonization-level 4] + [enable-unsafe-application #t] + [enable-unsafe-variable-reference #t] + [enable-arithmetic-left-associative #t] + [debug-level 0] + [generate-inspector-information #f] + [library-directories build-lib-dirs]) + (compile-program "bin/agent.ss")) + +;; ========== Step 4: Create libs-only boot file ========== + +(printf "~n[4/7] Creating boot file...~n") + +(define boot-libs + (append + ;; Jerboa runtime + stdlib (from STAGE dir — patched, no load-shared-object) + (map (lambda (m) (format "~a/~a.so" jerboa-stage m)) + '("jerboa/core" + "jerboa/runtime" + "jerboa/ffi" + "jerboa/prelude/clean" + "std/error" + "std/format" + "std/sort" + "std/pregexp" + "std/sugar" + "std/match2" + "std/misc/string" + "std/misc/list" + "std/misc/alist" + "std/misc/thread" + "std/misc/ports" + "std/foreign" + "std/os/path" + "std/os/file-info" + "std/text/hex" + "std/text/json" + "std/gambit-compat" + "std/crypto/native-rust" + "std/db/sqlite-native")) + ;; Secmon modules (obfuscate first — many modules depend on it) + (list "lib/secmon/stealth/obfuscate.so") + (map (lambda (m) (format "lib/secmon/crypto/~a.so" m)) + '("keys" "ecies" "psk")) + (list "lib/secmon/monitor/events.so" + "lib/secmon/monitor/suspicious.so") + (map (lambda (m) (format "lib/secmon/platform/~a.so" m)) + '("provider" "freebsd")) + (list "lib/secmon/buffer/ring.so" + "lib/secmon/server/protocol.so" + "lib/secmon/server/listener.so" + "lib/secmon/config.so" + "lib/secmon/storage/store.so") + (map (lambda (m) (format "lib/secmon/monitor/~a.so" m)) + '("process" "network" "files" "auth" "kernel" "cron" + "container" "dns" "rootkit" "persistence" "revshell" + "lateral" "logtamper" "webshell" "podman" "selinux")) + (map (lambda (m) (format "lib/secmon/stealth/~a.so" m)) + '("anti-debug" "masquerade" "env-sanitize" "integrity" "init")))) + +;; Verify all .so files exist +(for-each + (lambda (so) + (unless (file-exists? so) + (printf "ERROR: Missing boot file dependency: ~a~n" so) + (exit 1))) + boot-libs) + +(apply make-boot-file "secmon-agent.boot" '("scheme" "petite") boot-libs) +(printf " Created secmon-agent.boot~n") + +;; ========== Step 5: Generate C code ========== + +(printf "~n[5/7] Generating C code with embedded boot files + FFI symbols...~n") + +(define petite-boot-path (format "~a/petite.boot" chez-lib-dir)) +(define scheme-boot-path (format "~a/scheme.boot" chez-lib-dir)) + +;; Generate the C source +(call-with-output-file "secmon-program.c" + (lambda (out) + ;; Headers + (display "#include \"scheme.h\"\n" out) + (display "#include <string.h>\n" out) + (display "#include <stdlib.h>\n" out) + (display "#include <stdio.h>\n" out) + (display "#include <signal.h>\n" out) + (display "#include <unistd.h>\n" out) + (display "#include <errno.h>\n" out) + (display "#include <fcntl.h>\n" out) + (display "#include <sys/types.h>\n" out) + (display "#include <sys/socket.h>\n" out) + (display "#include <sys/wait.h>\n" out) + (display "#include <sys/mman.h>\n" out) + (display "#include <sys/ptrace.h>\n" out) + (display "#include <sys/sysctl.h>\n" out) + (display "#include <netinet/in.h>\n" out) + (display "#include <arpa/inet.h>\n\n" out) + + ;; Embed boot files as C arrays + (display (file->c-array petite-boot-path "petite_boot") out) + (newline out) + (display (file->c-array scheme-boot-path "scheme_boot") out) + (newline out) + (display (file->c-array "secmon-agent.boot" "app_boot") out) + (newline out) + + ;; Embed the compiled program + (display (file->c-array "bin/agent.so" "program_so") out) + (newline out) + + ;; Extern declarations for Rust native library symbols + (display "/* Rust native library symbols (libjerboa_native.a) */\n" out) + (for-each + (lambda (sym) + (fprintf out "extern int ~a();\n" sym)) + '("jerboa_last_error" + "jerboa_sha1" "jerboa_sha256" "jerboa_sha384" "jerboa_sha512" + "jerboa_random_bytes" + "jerboa_hmac_sha256" "jerboa_hmac_sha256_verify" + "jerboa_timing_safe_equal" + "jerboa_aead_seal" "jerboa_aead_open" + "jerboa_chacha20_seal" "jerboa_chacha20_open" + "jerboa_scrypt" + "jerboa_pbkdf2_derive" "jerboa_pbkdf2_verify" + "jerboa_x25519_generate_keypair" + "jerboa_x25519_public_from_private" + "jerboa_x25519_diffie_hellman" + "jerboa_hkdf_sha256" + ;; SQLite + "jerboa_sqlite_open" "jerboa_sqlite_close" "jerboa_sqlite_exec" + "jerboa_sqlite_prepare" "jerboa_sqlite_finalize" "jerboa_sqlite_reset" + "jerboa_sqlite_bind_int" "jerboa_sqlite_bind_double" + "jerboa_sqlite_bind_text" "jerboa_sqlite_bind_blob" + "jerboa_sqlite_bind_null" + "jerboa_sqlite_step" + "jerboa_sqlite_column_count" "jerboa_sqlite_column_type" + "jerboa_sqlite_column_int" "jerboa_sqlite_column_double" + "jerboa_sqlite_column_text" "jerboa_sqlite_column_blob" + "jerboa_sqlite_column_name" + "jerboa_sqlite_last_insert_rowid" "jerboa_sqlite_changes" + "jerboa_sqlite_errmsg" + ;; Process control + "jerboa_proc_self_exe" "jerboa_kill_probe" + "jerboa_prctl_set_name" "jerboa_mlockall" + ;; FreeBSD security + "jerboa_freebsd_is_traced" "jerboa_freebsd_process_count" + "jerboa_setproctitle")) + (newline out) + + ;; FFI symbol registration function + (display "static void register_ffi_symbols(void) {\n" out) + ;; libc symbols (needed by secmon monitors and jerboa std) + (for-each + (lambda (sym) + (fprintf out " Sforeign_symbol(\"~a\", (void*)~a);\n" sym sym)) + '("socket" "connect" "setsockopt" "bind" "listen" "accept" + "fork" "ptrace" "waitpid" "_exit" "kill" + "getpid" "getppid" "mlockall" + "readlink" "unsetenv" "access" + "__error" + "read" "write" "close")) + ;; Rust native symbols + (for-each + (lambda (sym) + (fprintf out " Sforeign_symbol(\"~a\", (void*)~a);\n" sym sym)) + '("jerboa_last_error" + "jerboa_sha1" "jerboa_sha256" "jerboa_sha384" "jerboa_sha512" + "jerboa_random_bytes" + "jerboa_hmac_sha256" "jerboa_hmac_sha256_verify" + "jerboa_timing_safe_equal" + "jerboa_aead_seal" "jerboa_aead_open" + "jerboa_chacha20_seal" "jerboa_chacha20_open" + "jerboa_scrypt" + "jerboa_pbkdf2_derive" "jerboa_pbkdf2_verify" + "jerboa_x25519_generate_keypair" + "jerboa_x25519_public_from_private" + "jerboa_x25519_diffie_hellman" + "jerboa_hkdf_sha256" + "jerboa_sqlite_open" "jerboa_sqlite_close" "jerboa_sqlite_exec" + "jerboa_sqlite_prepare" "jerboa_sqlite_finalize" "jerboa_sqlite_reset" + "jerboa_sqlite_bind_int" "jerboa_sqlite_bind_double" + "jerboa_sqlite_bind_text" "jerboa_sqlite_bind_blob" + "jerboa_sqlite_bind_null" + "jerboa_sqlite_step" + "jerboa_sqlite_column_count" "jerboa_sqlite_column_type" + "jerboa_sqlite_column_int" "jerboa_sqlite_column_double" + "jerboa_sqlite_column_text" "jerboa_sqlite_column_blob" + "jerboa_sqlite_column_name" + "jerboa_sqlite_last_insert_rowid" "jerboa_sqlite_changes" + "jerboa_sqlite_errmsg" + ;; FreeBSD-specific + "jerboa_proc_self_exe" "jerboa_kill_probe" + "jerboa_prctl_set_name" "jerboa_mlockall" + "jerboa_freebsd_is_traced" "jerboa_freebsd_process_count" + "jerboa_setproctitle")) + (display "}\n\n" out) + + ;; dlopen/dlsym stubs — needed because Chez tries to use them + ;; even when all symbols are pre-registered via Sforeign_symbol + (display "/* dlopen/dlsym stubs for static builds */\n" out) + (display "void *dlopen(const char *file, int mode) {\n" out) + (display " (void)file; (void)mode;\n" out) + (display " return (void*)1; /* non-NULL = success */\n" out) + (display "}\n\n" out) + + (display "void *dlsym(void *handle, const char *name) {\n" out) + (display " (void)handle; (void)name;\n" out) + (display " return NULL;\n" out) + (display "}\n\n" out) + + (display "int dlclose(void *handle) {\n" out) + (display " (void)handle;\n" out) + (display " return 0;\n" out) + (display "}\n\n" out) + + (display "char *dlerror(void) {\n" out) + (display " return \"dlopen not supported in static build\";\n" out) + (display "}\n\n" out) + + ;; Main function: bootstrap Chez and run the agent + ;; Uses Sscheme_script for threading support (unlike Sscheme_program + ;; which evaluates at heap build time before threads are ready). + ;; Writes program .so to a temp file, loads it, then unlinks. + (display "int main(int argc, const char *argv[]) {\n" out) + (display " Sscheme_init(NULL);\n" out) + (display " Sregister_boot_file_bytes(\"petite\", petite_boot, petite_boot_len);\n" out) + (display " Sregister_boot_file_bytes(\"scheme\", scheme_boot, scheme_boot_len);\n" out) + (display " Sregister_boot_file_bytes(\"secmon\", app_boot, app_boot_len);\n" out) + (display " Sbuild_heap(NULL, NULL);\n\n" out) + (display " /* Register FFI symbols after heap is built */\n" out) + (display " register_ffi_symbols();\n\n" out) + ;; Write program .so to temp file and load via Sscheme_script + (display " /* Write program to temp file for Sscheme_script (threading support) */\n" out) + (display " char tmppath[] = \"/tmp/.secmon-XXXXXX\";\n" out) + (display " int fd = mkstemp(tmppath);\n" out) + (display " if (fd >= 0) {\n" out) + (display " write(fd, program_so, program_so_len);\n" out) + (display " close(fd);\n" out) + (display " const char *script_args[] = { argv[0] };\n" out) + (display " Sscheme_script(tmppath, 1, script_args);\n" out) + (display " unlink(tmppath);\n" out) + (display " }\n\n" out) + (display " Sscheme_deinit();\n" out) + (display " return 0;\n" out) + (display "}\n" out)) + 'replace) + +(printf " Generated secmon-program.c~n") + +;; ========== Step 6: Compile C ========== + +(printf "~n[6/7] Compiling C...~n") + +(define scheme-h-dir chez-lib-dir) + +(let ([cmd (format "cc -c -O2 -I'~a' -o secmon-program.o secmon-program.c" scheme-h-dir)]) + (printf " ~a~n" cmd) + (unless (= (system cmd) 0) + (printf "ERROR: C compilation failed~n") + (exit 1))) + +;; ========== Step 7: Link static binary ========== + +(printf "~n[7/7] Linking static binary...~n") + +(define libkernel (format "~a/libkernel.a" chez-lib-dir)) +(define libz (let ([p (format "~a/libz.a" chez-lib-dir)]) + (if (file-exists? p) p #f))) +(define liblz4 (let ([p (format "~a/liblz4.a" chez-lib-dir)]) + (if (file-exists? p) p #f))) + +(let* ([libs (filter values + (list libkernel libz liblz4 native-lib-path))] + [lib-flags (apply string-append + (map (lambda (l) (format " '~a'" l)) libs))] + [cmd (format "cc -static secmon-program.o~a -lncursesw -lm -lthr -lc -o secmon-agent" + lib-flags)]) + (printf " ~a~n" cmd) + (unless (= (system cmd) 0) + (printf "ERROR: Linking failed~n") + (exit 1))) + +;; Strip the binary +(printf "~n Stripping...~n") +(system "strip secmon-agent") + +;; Generate integrity hash +(printf " Generating SHA256 hash...~n") +(system "sha256 secmon-agent > secmon-agent.sha256") + +;; Verify +(printf "~n=== Build complete ===~n") +(system "ls -lh secmon-agent") +(system "file secmon-agent") + +;; Cleanup +(printf "~n Cleaning up staging directory...~n") +(system (format "rm -rf '~a'" jerboa-stage)) +(system "rm -f secmon-program.c secmon-program.o secmon-agent.boot") + +(printf "~nDone.~n") --- a/lib/secmon/monitor/auth.sls +++ b/lib/secmon/monitor/auth.sls @@ -33,6 +33,7 @@ (cond [(file-exists? "/var/log/auth.log") "/var/log/auth.log"] [(file-exists? "/var/log/secure") "/var/log/secure"] + [(file-exists? "/var/log/messages") "/var/log/messages"] ;; FreeBSD fallback [else #f])) (define (file-size-safe path) @@ -166,8 +167,40 @@ (vector-for-each (lambda (k) (hashtable-delete! known-users k)) keys)) (for-each (lambda (u) (hashtable-set! known-users u #t)) current-users)))) - ;; Read utmp binary file (384-byte records, ut_type=7 means USER_PROCESS) + ;; Read logged-in users. Uses `who` command on FreeBSD (different utmp format), + ;; binary utmp parsing on Linux. + (define on-freebsd? + (string-contains (symbol->string (machine-type)) "fb")) + (define (read-utmp-users) + (if on-freebsd? + (read-utmp-users-who) + (read-utmp-users-linux))) + + ;; Cross-platform: parse `who` output + (define (read-utmp-users-who) + (guard (e [#t '()]) + (let ([output (shell-output "who 2>/dev/null")]) + (filter-map + (lambda (line) + (let ([parts (filter (lambda (s) (not (string=? s ""))) + (string-split (string-trim line) #\space))]) + (and (not (null? parts)) (car parts)))) + (filter (lambda (s) (not (string=? s ""))) + (string-split output #\newline)))))) + + (define (shell-output cmd) + (guard (e [#t ""]) + (let-values ([(to-stdin from-stdout from-stderr pid) + (open-process-ports cmd 'line (current-transcoder))]) + (close-port to-stdin) + (let ([output (get-string-all from-stdout)]) + (close-port from-stdout) + (close-port from-stderr) + output)))) + + ;; Linux: binary utmp parsing (384-byte records, ut_type=7 means USER_PROCESS) + (define (read-utmp-users-linux) (guard (e [#t '()]) (let ([path (if (file-exists? "/var/run/utmp") "/var/run/utmp" "/run/utmp")]) (if (not (file-exists? path)) '() @@ -183,7 +216,6 @@ (let ([ut-type (bytevector-s32-ref bv offset (endianness little))]) (if (= ut-type 7) ;; USER_PROCESS (let ([user (extract-utmp-string bv (+ offset 8) 32)]) - (loop (+ offset record-size)) (cons user (loop (+ offset record-size)))) (loop (+ offset record-size))))))))))))) --- a/lib/secmon/monitor/container.sls +++ b/lib/secmon/monitor/container.sls @@ -7,6 +7,9 @@ (secmon monitor suspicious) (secmon stealth obfuscate)) + (define on-freebsd? + (string-contains (symbol->string (machine-type)) "fb")) + ;; Container escape monitor: detects escape attempts from containers/jails (define (spawn-container-escape-monitor emit! poll-ms hostname) (fork-thread @@ -40,49 +43,72 @@ (list (obfstr "/var/run/docker.sock") (obfstr "/run/docker.sock") (obfstr "/var/run/containerd/containerd.sock"))) - ;; Check capabilities - (let ([caps (read-effective-caps)]) - (when caps - ;; Full caps = privileged container - (when (and (not reported-privileged) - (or (= caps #x3fffffffff) - (= caps #xffffffffffffffff))) - (set! reported-privileged #t) - (emit! (make-escape-event hostname "privileged_container" - "" 'critical))) - ;; Check individual dangerous caps - (for-each - (lambda (cap-pair) - (let ([bit (car cap-pair)] [name (cdr cap-pair)]) - (when (and (not (zero? (bitwise-and caps - (bitwise-arithmetic-shift-left 1 bit)))) - (not (hashtable-ref reported-caps name #f))) - (hashtable-set! reported-caps name #t) - (emit! (make-escape-event hostname "dangerous_capability" - name 'high))))) - dangerous-capabilities)))) + ;; Check capabilities / jail restrictions + (if on-freebsd? + ;; FreeBSD: check jail security restrictions via sysctl + (check-jail-restrictions emit! hostname reported-caps) + ;; Linux: check Linux capabilities + (let ([caps (read-effective-caps)]) + (when caps + ;; Full caps = privileged container + (when (and (not reported-privileged) + (or (= caps #x3fffffffff) + (= caps #xffffffffffffffff))) + (set! reported-privileged #t) + (emit! (make-escape-event hostname "privileged_container" + "" 'critical))) + ;; Check individual dangerous caps + (for-each + (lambda (cap-pair) + (let ([bit (car cap-pair)] [name (cdr cap-pair)]) + (when (and (not (zero? (bitwise-and caps + (bitwise-arithmetic-shift-left 1 bit)))) + (not (hashtable-ref reported-caps name #f))) + (hashtable-set! reported-caps name #t) + (emit! (make-escape-event hostname "dangerous_capability" + name 'high))))) + dangerous-capabilities))))) (sleep (make-time 'time-duration (* (mod poll-ms 1000) 1000000) (quotient poll-ms 1000))) (loop))))))) (define (is-containerized?) - ;; Check for container indicators - (or (file-exists? (obfstr "/.dockerenv")) - (file-exists? (obfstr "/run/.containerenv")) - (guard (e [#t #f]) - (let ([cgroup (read-file-safe (obfstr "/proc/1/cgroup"))]) - (and cgroup - (or (string-contains cgroup (obfstr "docker")) - (string-contains cgroup (obfstr "lxc")) - (string-contains cgroup (obfstr "kubepods")))))))) + (if on-freebsd? + ;; FreeBSD: check if running inside a jail + (guard (e [#t #f]) + (let ([output (shell-output "sysctl -n security.jail.jailed 2>/dev/null")]) + (string=? (string-trim output) "1"))) + ;; Linux: check for container indicators + (or (file-exists? (obfstr "/.dockerenv")) + (file-exists? (obfstr "/run/.containerenv")) + (guard (e [#t #f]) + (let ([cgroup (read-file-safe (obfstr "/proc/1/cgroup"))]) + (and cgroup + (or (string-contains cgroup (obfstr "docker")) + (string-contains cgroup (obfstr "lxc")) + (string-contains cgroup (obfstr "kubepods"))))))))) (define (read-mounts) (guard (e [#t '()]) - (let ([lines (local-read-file-lines (obfstr "/proc/mounts"))]) - (filter-map - (lambda (line) - (let ([parts (string-split line #\space)]) - (and (>= (length parts) 2) (cadr parts)))) - lines)))) + (if on-freebsd? + ;; FreeBSD: parse mount command output + (let ([lines (shell-output-lines "/sbin/mount 2>/dev/null")]) + (filter-map + (lambda (line) + ;; Format: /dev/ada0p2 on / (ufs, local, ...) + (let ([on-pos (string-contains line " on ")]) + (and on-pos + (let ([rest (substring line (+ on-pos 4) (string-length line))]) + (let ([space (string-contains rest " (")]) + (if space (substring rest 0 space) + (string-trim rest))))))) + lines)) + ;; Linux: parse /proc/mounts + (let ([lines (local-read-file-lines (obfstr "/proc/mounts"))]) + (filter-map + (lambda (line) + (let ([parts (string-split line #\space)]) + (and (>= (length parts) 2) (cadr parts)))) + lines))))) (define (suspicious-mount? mount) (or (string-prefix? (obfstr "/host") mount) @@ -90,6 +116,32 @@ (string-contains mount (obfstr "docker.sock")) (string-contains mount (obfstr "containerd.sock")))) + ;; FreeBSD jail restriction checks via sysctl security.jail.* + ;; Jails that have relaxed restrictions are dangerous — equivalent to + ;; Linux's dangerous capabilities. + (define jail-restriction-checks + ;; (sysctl-name . description) — value "1" means ALLOWED (dangerous) + '(("security.jail.allow_raw_sockets" . "raw_sockets") + ("security.jail.mount_allowed" . "mount_allowed") + ("security.jail.chflags_allowed" . "chflags_allowed") + ("security.jail.sysvipc_allowed" . "sysvipc_allowed"))) + + (define (check-jail-restrictions emit! hostname reported-caps) + (guard (e [#t (void)]) + (for-each + (lambda (check) + (let ([sysctl-name (car check)] + [cap-name (cdr check)]) + (guard (e [#t (void)]) + (let ([output (shell-output + (format "/sbin/sysctl -n ~a 2>/dev/null" sysctl-name))]) + (when (string=? (string-trim output) "1") + (unless (hashtable-ref reported-caps cap-name #f) + (hashtable-set! reported-caps cap-name #t) + (emit! (make-escape-event hostname "dangerous_jail_permission" + cap-name 'high)))))))) + jail-restriction-checks))) + (define (read-effective-caps) (guard (e [#t #f]) (let ([lines (local-read-file-lines (obfstr "/proc/self/status"))]) @@ -115,6 +167,20 @@ (string-split content #\newline)) '())))) + (define (shell-output cmd) + (guard (e [#t ""]) + (let-values ([(to-stdin from-stdout from-stderr pid) + (open-process-ports cmd 'line (current-transcoder))]) + (close-port to-stdin) + (let ([output (get-string-all from-stdout)]) + (close-port from-stdout) + (close-port from-stderr) + output)))) + + (define (shell-output-lines cmd) + (filter (lambda (s) (not (string=? s ""))) + (string-split (shell-output cmd) #\newline))) + (define (make-escape-event hostname detection-type detail severity) (let ([data (make-hashtable string-hash string=?)]) (hashtable-set! data "detection_type" detection-type) --- a/lib/secmon/monitor/kernel.sls +++ b/lib/secmon/monitor/kernel.sls @@ -6,12 +6,15 @@ (secmon monitor events) (secmon monitor suspicious)) + (define on-freebsd? + (string-contains (symbol->string (machine-type)) "fb")) + ;; Kernel module monitor: detects module loads/unloads (define (spawn-kernel-module-monitor emit! poll-ms hostname) (fork-thread (lambda () (let ([known-modules (make-hashtable string-hash string=?)]) - ;; Baseline from /proc/modules + ;; Baseline (for-each (lambda (mod) (hashtable-set! known-modules (car mod) (cadr mod))) @@ -45,14 +48,57 @@ (define (read-kernel-modules) (guard (e [#t '()]) - (let ([lines (local-read-file-lines "/proc/modules")]) + (if on-freebsd? + (read-kernel-modules-freebsd) + (read-kernel-modules-linux)))) + + ;; Linux: parse /proc/modules + (define (read-kernel-modules-linux) + (let ([lines (local-read-file-lines "/proc/modules")]) + (filter-map + (lambda (line) + (let ([parts (string-split line #\space)]) + (and (>= (length parts) 2) + (list (car parts) + (or (string->number (cadr parts)) 0))))) + lines))) + + ;; FreeBSD: parse kldstat output + ;; Format: Id Refs Address Size Name + ;; 1 1 0xffffffff80200000 1a3b2f0 kernel + (define (read-kernel-modules-freebsd) + (let ([lines (shell-output-lines "/sbin/kldstat 2>/dev/null")]) + (if (null? lines) '() (filter-map (lambda (line) - (let ([parts (string-split line #\space)]) - (and (>= (length parts) 2) - (list (car parts) - (or (string->number (cadr parts)) 0))))) - lines)))) + (let ([parts (filter (lambda (s) (not (string=? s ""))) + (string-split (string-trim line) #\space))]) + (and (>= (length parts) 5) + ;; Skip header (first field is "Id") + (string->number (car parts)) + (let ([name (list-ref parts 4)] + [size-hex (list-ref parts 3)]) + (list name + (or (string->number size-hex 16) 0)))))) + (cdr lines))))) + + (define (shell-output-lines cmd) + (guard (e [#t '()]) + (let ([output (shell-output cmd)]) + (if (string=? output "") + '() + (filter (lambda (s) (not (string=? s ""))) + (string-split output #\newline)))))) + + (define (shell-output cmd) + (guard (e [#t ""]) + (let-values ([(to-stdin from-stdout from-stderr pid) + (open-process-ports cmd 'line (current-transcoder))]) + (close-port to-stdin) + (let ([output (get-string-all from-stdout)]) + (close-port from-stdout) + (close-port from-stderr) + output)))) (define (local-read-file-lines path) (guard (e [#t '()]) --- a/lib/secmon/monitor/rootkit.sls +++ b/lib/secmon/monitor/rootkit.sls @@ -7,6 +7,9 @@ (secmon stealth obfuscate) (secmon platform provider)) + (define on-freebsd? + (string-contains (symbol->string (machine-type)) "fb")) + ;; Rootkit monitor: detects hidden processes via /proc vs kill() discrepancy (define (spawn-rootkit-monitor proc-provider emit! poll-ms hostname) (fork-thread @@ -22,19 +25,20 @@ ;; Build visible set (for-each (lambda (pid) (hashtable-set! visible-set pid #t)) visible-pids) - ;; Collect known thread IDs - (for-each - (lambda (pid) - (guard (e [#t (void)]) - (let ([task-dir (format "/proc/~a/task" pid)]) - (when (file-exists? task-dir) - (for-each - (lambda (tid-str) - (let ([tid (string->number tid-str)]) - (when tid - (hashtable-set! thread-set tid #t)))) - (directory-list task-dir)))))) - visible-pids) + ;; Collect known thread IDs (Linux only — FreeBSD threads share PIDs) + (unless on-freebsd? + (for-each + (lambda (pid) + (guard (e [#t (void)]) + (let ([task-dir (format "/proc/~a/task" pid)]) + (when (file-exists? task-dir) + (for-each + (lambda (tid-str) + (let ([tid (string->number tid-str)]) + (when tid + (hashtable-set! thread-set tid #t)))) + (directory-list task-dir)))))) + visible-pids)) ;; Probe PID ranges (let ([hidden-pids '()]) ;; 1. PIDs 1-1000 @@ -71,7 +75,7 @@ (emit! (make-rootkit-event hostname pid "hidden_process" 'critical)))) hidden-pids)) - ;; Check /proc/loadavg discrepancy + ;; Check loadavg discrepancy (check-loadavg-discrepancy visible-pids emit! hostname))) (sleep (make-time 'time-duration (* (mod poll-ms 1000) 1000000) (quotient poll-ms 1000))) (loop)))))) @@ -82,34 +86,60 @@ (let ([result (kill-fn pid 0)]) (or (= result 0) ;; Check errno for EPERM (exists but permission denied) - (let ([errno-fn (foreign-procedure "__errno_location" () uptr)]) - (let ([errno-ptr (errno-fn)]) - (= (foreign-ref 'int errno-ptr 0) 1)))))) ;; EPERM = 1 + (if on-freebsd? + (let ([errno-fn (foreign-procedure "__error" () uptr)]) + (let ([errno-ptr (errno-fn)]) + (= (foreign-ref 'int errno-ptr 0) 1))) ;; EPERM = 1 + (let ([errno-fn (foreign-procedure "__errno_location" () uptr)])