From c5ebb6bb537761a8800dba439b0e91198bba2e06 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 15 Mar 2026 06:09:10 +0000 Subject: [PATCH] Block /dev/mem and /dev/kmem via eBPF LSM --- CHANGELOG.md | 16 +++++++ README.md | 6 +-- cheats/Makefile | 3 +- cheats/dev_mem_reader.c | 89 +++++++++++++++++++++++++++++++++++++ daemon/bpf_event_convert.c | 1 + daemon/main.c | 2 + ebpf/owlbear_common.bpf.h | 1 + ebpf/owlbear_lsm.bpf.c | 42 +++++++++++++---- include/owlbear_events.h | 1 + scripts/verify.sh | 51 +++++++++++++++++++-- tests/test_bpf_loader | Bin 21176 -> 21240 bytes tests/test_bpf_loader.c | 23 ++++++++++ 12 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 cheats/dev_mem_reader.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d111e..7153e8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0] - 2026-03-15 + +### Added +- **WP1c: Block /dev/mem and /dev/kmem via eBPF LSM** (`ebpf/owlbear_lsm.bpf.c`): extends `file_open` hook to unconditionally block `/dev/mem` and `/dev/kmem` opens with -EPERM. Physical memory access bypasses all process-level protections, so no PID allowlist check is performed. Emits `OWL_EVENT_DEV_MEM_ACCESS` (severity CRITICAL, target_pid=0). Detail string distinguishes `/dev/mem` vs `/dev/kmem`. +- `OWL_EVENT_DEV_MEM_ACCESS` (0x0106) in shared event header and BPF common header (breaking: new event type) +- `cheats/dev_mem_reader.c`: attack program that attempts to open `/dev/mem` and `/dev/kmem` and read 256 bytes of raw physical memory +- `tests/test_bpf_loader.c`: conversion test for `OWL_EVENT_DEV_MEM_ACCESS` (102 total tests) +- `scripts/verify.sh`: baseline and protected phase assertions for dev_mem_reader, handles CONFIG_STRICT_DEVMEM (EACCES) gracefully + +### Changed +- `daemon/bpf_event_convert.c`: `OWL_EVENT_DEV_MEM_ACCESS` falls through to memory payload conversion +- `daemon/main.c`: `event_type_str()` and `print_event()` handle `DEV_MEM_ACCESS` +- `cheats/Makefile`: builds `dev_mem_reader.bin` +- `scripts/verify.sh`: version bumped to v2.0.0 +- `README.md`: updated status (v2.0.0, 102 tests, 9 attack programs), added /dev/mem blocking + ## [1.2.0] - 2026-03-15 ### Added diff --git a/README.md b/README.md index a3777a6..a679d76 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ Not production software. Runs on Graviton3 (c7g.large), Ubuntu 24.04, kernel 6.17. - **kernel/** - loadable module. Kprobes on ptrace, /proc/pid/mem, process_vm_readv/writev, mmap, module load/unload. ARM64 system register monitoring. Chardev for event delivery. -- **ebpf/** - BPF LSM hooks returning -EPERM (ptrace_access_check, file_open, file_mprotect). Tracepoints. Kprobe on do_init_module. Ring buffer to userspace. +- **ebpf/** - BPF LSM hooks returning -EPERM (ptrace_access_check, file_open for /proc/pid/mem + /dev/mem + /dev/kmem, file_mprotect). Tracepoints. Kprobe on do_init_module. Ring buffer to userspace. - **daemon/** - epoll on chardev + BPF ring buffer. Policy engine. Signature scanner. CRC32 code integrity. Self-protection watchdog. TracerPid debugger detection. - **game/** - ncurses test target. Mutable state, function pointers, exported address. -- **cheats/** - 8 attack programs: process_vm_readv, /proc/pid/mem, ptrace read, ptrace write, process_vm_writev, LD_PRELOAD, mprotect injection, debug registers. +- **cheats/** - 9 attack programs: process_vm_readv, /proc/pid/mem, /dev/mem, ptrace read, ptrace write, process_vm_writev, LD_PRELOAD, mprotect injection, debug registers. - **platform/** - Lambda + API Gateway + DynamoDB telemetry receiver. - **scripts/verify.sh** - E2E test. Baseline (cheats succeed) vs protected (cheats blocked). Machine-generated results. ## Status -v1.2.0. 101 unit tests, 11 suites. 30/31 E2E pass on Graviton3. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, process_vm_writev. Module can't be unloaded while daemon runs. TracerPid polling detects debuggers attached before daemon start. +v2.0.0. 102 unit tests, 11 suites. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, /dev/mem, /dev/kmem, process_vm_writev. Module can't be unloaded while daemon runs. TracerPid polling detects debuggers attached before daemon start. Prototype limitations: linear signature scan, CRC32 not cryptographic, no fleet management. diff --git a/cheats/Makefile b/cheats/Makefile index 2ecb69d..5224fb8 100644 --- a/cheats/Makefile +++ b/cheats/Makefile @@ -19,7 +19,8 @@ CHEAT_SRCS := mem_reader.c \ mprotect_injector.c \ mprotect_inject_via_ptrace.c \ ld_preload_hook.c \ - debug_reg_setter.c + debug_reg_setter.c \ + dev_mem_reader.c CHEAT_BINS := $(CHEAT_SRCS:.c=.bin) CHEAT_DEPS := $(CHEAT_SRCS:.c=.d) diff --git a/cheats/dev_mem_reader.c b/cheats/dev_mem_reader.c new file mode 100644 index 0000000..576b8cf --- /dev/null +++ b/cheats/dev_mem_reader.c @@ -0,0 +1,89 @@ +/* + * dev_mem_reader - Test cheat using /dev/mem + * + * Attempts to open /dev/mem and read the first 256 bytes of physical + * memory. This bypasses all process-level protections (ptrace hooks, + * /proc/pid/mem blocks, process_vm_* interception) because it accesses + * raw physical memory directly. + * + * Should be blocked by owlbear's eBPF LSM file_open hook. + * + * Also attempts /dev/kmem if /dev/mem is blocked, to verify both + * paths are covered. + * + * Usage: dev_mem_reader + */ + +#include +#include +#include +#include +#include +#include + +static int try_dev_path(const char *path) +{ + int fd; + unsigned char buf[256]; + ssize_t n; + + printf("[dev_mem_reader] Opening %s...\n", path); + + fd = open(path, O_RDONLY); + if (fd < 0) { + int err = errno; + fprintf(stderr, "[dev_mem_reader] open(%s) failed: %s (errno=%d)\n", + path, strerror(err), err); + return -err; + } + + n = read(fd, buf, sizeof(buf)); + if (n < 0) { + int err = errno; + fprintf(stderr, "[dev_mem_reader] read(%s) failed: %s (errno=%d)\n", + path, strerror(err), err); + close(fd); + return -err; + } + + printf("[CHEAT] Read %zd bytes from %s\n", n, path); + close(fd); + return 0; +} + +int main(void) +{ + int rc; + + printf("[dev_mem_reader] Attempting raw physical memory access\n"); + printf("[dev_mem_reader] This should be blocked by eBPF LSM\n\n"); + + rc = try_dev_path("/dev/mem"); + if (rc == 0) + return EXIT_SUCCESS; + + if (rc == -EPERM) { + fprintf(stderr, "[dev_mem_reader] Blocked by anti-cheat (EPERM)\n"); + } else if (rc == -EACCES) { + fprintf(stderr, "[dev_mem_reader] Blocked by CONFIG_STRICT_DEVMEM (EACCES)\n"); + } else if (rc == -ENOENT) { + fprintf(stderr, "[dev_mem_reader] /dev/mem does not exist\n"); + } + + /* Also try /dev/kmem */ + printf("\n"); + rc = try_dev_path("/dev/kmem"); + if (rc == 0) + return EXIT_SUCCESS; + + if (rc == -EPERM) + fprintf(stderr, "[dev_mem_reader] /dev/kmem also blocked (EPERM)\n"); + else if (rc == -EACCES) + fprintf(stderr, "[dev_mem_reader] /dev/kmem blocked by kernel config (EACCES)\n"); + else if (rc == -ENOENT) + fprintf(stderr, "[dev_mem_reader] /dev/kmem does not exist\n"); + + /* Exit 0 if at least one path was accessible, 1 otherwise. + * The E2E script checks stderr for EPERM vs EACCES. */ + return EXIT_FAILURE; +} diff --git a/daemon/bpf_event_convert.c b/daemon/bpf_event_convert.c index d79250f..1fb17b6 100644 --- a/daemon/bpf_event_convert.c +++ b/daemon/bpf_event_convert.c @@ -51,6 +51,7 @@ int owl_bpf_event_convert(const void *bpf_data, size_t bpf_size, case OWL_EVENT_VM_READV_ATTEMPT: case OWL_EVENT_VM_WRITEV_ATTEMPT: case OWL_EVENT_MPROTECT_EXEC: + case OWL_EVENT_DEV_MEM_ACCESS: out->payload.memory.caller_pid = bev->pid; memcpy(out->payload.memory.caller_comm, bev->comm, sizeof(out->payload.memory.caller_comm) < sizeof(bev->comm) diff --git a/daemon/main.c b/daemon/main.c index 904a1de..f6802df 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -99,6 +99,7 @@ static const char *event_type_str(uint32_t type) case OWL_EVENT_VM_WRITEV_ATTEMPT: return "VM_WRITEV_ATTEMPT"; case OWL_EVENT_EXEC_MMAP: return "EXEC_MMAP"; case OWL_EVENT_MPROTECT_EXEC: return "MPROTECT_EXEC"; + case OWL_EVENT_DEV_MEM_ACCESS: return "DEV_MEM_ACCESS"; case OWL_EVENT_MODULE_LOAD: return "MODULE_LOAD"; case OWL_EVENT_MODULE_UNKNOWN: return "MODULE_UNKNOWN"; case OWL_EVENT_CODE_INTEGRITY_FAIL: return "CODE_INTEGRITY_FAIL"; @@ -181,6 +182,7 @@ static void print_event(const struct owlbear_event *ev, FILE *out) case OWL_EVENT_VM_READV_ATTEMPT: case OWL_EVENT_VM_WRITEV_ATTEMPT: case OWL_EVENT_MPROTECT_EXEC: + case OWL_EVENT_DEV_MEM_ACCESS: fprintf(out, " caller_pid=%u caller=%s", ev->payload.memory.caller_pid, ev->payload.memory.caller_comm); diff --git a/ebpf/owlbear_common.bpf.h b/ebpf/owlbear_common.bpf.h index f1c389c..05fbe7f 100644 --- a/ebpf/owlbear_common.bpf.h +++ b/ebpf/owlbear_common.bpf.h @@ -32,6 +32,7 @@ #define OWL_EVENT_VM_WRITEV_ATTEMPT 0x0103 #define OWL_EVENT_EXEC_MMAP 0x0104 #define OWL_EVENT_MPROTECT_EXEC 0x0105 +#define OWL_EVENT_DEV_MEM_ACCESS 0x0106 #define OWL_EVENT_MODULE_LOAD 0x0200 #define OWL_SEV_INFO 0 diff --git a/ebpf/owlbear_lsm.bpf.c b/ebpf/owlbear_lsm.bpf.c index a4790a9..aaa6beb 100644 --- a/ebpf/owlbear_lsm.bpf.c +++ b/ebpf/owlbear_lsm.bpf.c @@ -8,7 +8,7 @@ * * Hooks: * lsm/ptrace_access_check — block ptrace on protected PID - * lsm/file_open — block /proc//mem access + * lsm/file_open — block /proc//mem, /dev/mem, /dev/kmem * lsm/mmap_file — monitor executable mmap in game * * Requires: CONFIG_BPF_LSM=y in kernel config. @@ -44,11 +44,12 @@ int BPF_PROG(owl_ptrace_check, struct task_struct *child, unsigned int mode) /* ------------------------------------------------------------------------- * LSM: file_open * - * Called when a file is opened. We check if the file is /proc//mem - * for a protected PID. If so, deny access from non-whitelisted callers. + * Called when a file is opened. Blocks: + * /dev/mem, /dev/kmem — unconditional (physical memory = system-wide threat) + * /proc//mem — if PID is protected and caller not whitelisted * * Path checking in BPF is limited — we read the dentry name and check - * if it equals "mem", then verify the parent directory is a protected PID. + * parent/grandparent to distinguish /dev/mem from /proc//mem. * ----------------------------------------------------------------------- */ SEC("lsm/file_open") @@ -67,15 +68,20 @@ int BPF_PROG(owl_file_open, struct file *file) if (!dentry) return 0; - /* Check if filename is "mem" */ + /* Check if filename is "mem" or "kmem" */ dname = BPF_CORE_READ(dentry, d_name); bpf_probe_read_kernel_str(name_buf, sizeof(name_buf), dname.name); - if (name_buf[0] != 'm' || name_buf[1] != 'e' || - name_buf[2] != 'm' || name_buf[3] != '\0') + bool is_mem = (name_buf[0] == 'm' && name_buf[1] == 'e' && + name_buf[2] == 'm' && name_buf[3] == '\0'); + bool is_kmem = (name_buf[0] == 'k' && name_buf[1] == 'm' && + name_buf[2] == 'e' && name_buf[3] == 'm' && + name_buf[4] == '\0'); + + if (!is_mem && !is_kmem) return 0; - /* Get parent directory name (should be the PID) */ + /* Get parent directory name */ parent = BPF_CORE_READ(dentry, d_parent); if (!parent) return 0; @@ -84,6 +90,26 @@ int BPF_PROG(owl_file_open, struct file *file) bpf_probe_read_kernel_str(parent_buf, sizeof(parent_buf), parent_name.name); + /* + * /dev/mem or /dev/kmem — unconditional block. + * Physical memory access threatens ALL processes. + */ + bool is_dev_parent = (parent_buf[0] == 'd' && parent_buf[1] == 'e' && + parent_buf[2] == 'v' && parent_buf[3] == '\0'); + + if (is_dev_parent && (is_mem || is_kmem)) { + caller_pid = bpf_get_current_pid_tgid() >> 32; + emit_event(OWL_EVENT_DEV_MEM_ACCESS, OWL_SEV_CRITICAL, + caller_pid, 0, + is_kmem ? "BPF LSM: /dev/kmem blocked" + : "BPF LSM: /dev/mem blocked"); + return -EPERM; + } + + /* Only "mem" is relevant under /proc; kmem handled above */ + if (!is_mem) + return 0; + /* * Verify parent's parent is "proc" — this distinguishes * /proc//mem from other files named "mem". diff --git a/include/owlbear_events.h b/include/owlbear_events.h index ddac378..ef25098 100644 --- a/include/owlbear_events.h +++ b/include/owlbear_events.h @@ -50,6 +50,7 @@ enum owlbear_event_type { OWL_EVENT_VM_WRITEV_ATTEMPT = 0x0103, OWL_EVENT_EXEC_MMAP = 0x0104, OWL_EVENT_MPROTECT_EXEC = 0x0105, + OWL_EVENT_DEV_MEM_ACCESS = 0x0106, /* Integrity checks (0x02xx) */ OWL_EVENT_MODULE_LOAD = 0x0200, diff --git a/scripts/verify.sh b/scripts/verify.sh index f373c50..35bf17a 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -297,7 +297,7 @@ preflight() { | grep -o '"accountId" *: *"[^"]*"' | cut -d'"' -f4 || echo "local") cat > "${OUT_DIR}/summary.txt" <
/dev/null && \ + ! grep -q "EPERM" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then + assert_skip "baseline/dev_mem_reader" "CONFIG_STRICT_DEVMEM blocks /dev/mem (EACCES)" + elif grep -q "No such file" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then + assert_skip "baseline/dev_mem_reader" "/dev/mem does not exist" + else + assert_fail "baseline/dev_mem_reader should succeed or skip without module" "exit_code=${rc}" + fi + capture_dmesg > "${phase_dir}/dmesg_after.txt" dmesg_since "${phase_mark}" "${phase_dir}/dmesg_phase_diff.txt" @@ -767,6 +784,34 @@ phase_protected() { assert_pass "protected/mprotect_inject_via_ptrace ptrace denied (EPERM)" fi + # --- dev_mem_reader --- + run_cheat_captured "${phase_dir}" "dev_mem_reader" \ + "${cheats_dir}/dev_mem_reader.bin" + + # Assertion 1: open blocked with EPERM (our hook) or EACCES (kernel config) + if grep -q "EPERM" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then + assert_pass "protected/dev_mem_reader blocked by eBPF LSM (EPERM)" + elif grep -q "EACCES\|Permission denied" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then + assert_skip "protected/dev_mem_reader" "CONFIG_STRICT_DEVMEM blocks before LSM (EACCES)" + elif grep -q "No such file" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then + assert_skip "protected/dev_mem_reader" "/dev/mem does not exist" + else + assert_fail "protected/dev_mem_reader should be blocked" \ + "exit_code=$(cat "${phase_dir}/dev_mem_reader/exit_code")" + fi + + # Assertion 2: DEV_MEM_ACCESS event in daemon log + if grep -q "EPERM" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then + if [ -f "${phase_dir}/daemon.log" ] && \ + grep -q "DEV_MEM_ACCESS" "${phase_dir}/daemon.log" 2>/dev/null; then + assert_pass "protected/dev_mem_reader DEV_MEM_ACCESS event in daemon log" + else + assert_fail "protected/dev_mem_reader DEV_MEM_ACCESS missing from daemon log" + fi + elif grep -q "EACCES\|Permission denied\|No such file" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then + assert_skip "protected/dev_mem_reader event check" "kernel blocked before LSM hook" + fi + # Check daemon log for BLOCK entries if enforce mode if [ -f "${phase_dir}/daemon.log" ]; then if grep -q "\[ENFORCE\].*\[BLOCK\]" "${phase_dir}/daemon.log" 2>/dev/null; then @@ -917,7 +962,7 @@ FOOTER main() { echo "" echo -e "${BOLD}================================================${NC}" - echo -e "${BOLD} Owlbear E2E Verification (v1.2.0)${NC}" + echo -e "${BOLD} Owlbear E2E Verification (v2.0.0)${NC}" echo -e "${BOLD} Evidence Package Builder${NC}" echo -e "${BOLD}================================================${NC}" echo "" diff --git a/tests/test_bpf_loader b/tests/test_bpf_loader index b273e3909b629bb3cb9d63e2da4471fa1c1f2abd..b205a6982085de84cf162140b6688d5493860035 100755 GIT binary patch delta 3724 zcmZWsdr%Zd9-bK(UWzOWvM$dB zdwgH_Z)bXYFPsr4&WP<9(!ymOvj@Wt#tG~?7)Oj^cIdG{etXPQD&~mEmOOqt*&gC< z;Sf0?-H6^+-1zSF{K9oZ5h?=Ay{n7QW+2#_Qu`o7p_8 zbE+V?n&1DgqPW>;KW=w5cj4CVZSIDl%OiDBsfCS>jfFVGC18G2rM zBbtPH3T}BFP*b1ik}LDbwWtI!h}@6LD13u+{2Hlo1p9NG{F!q2tQyB45aYORC1uei zg}Zt6e!coIL+T9TSoJ2ox=O2l*-E~SE`DhbuNL*{ss7cS52^MD+QjGo6A;t>4q#{t zoNn78S08$FllR64vsxl!N$Iw|oVnSct*V_NbzU0TWNOwcXy2M3gO`Q0fX&Ui&45`FhM=9}V*&1;#5F_pem`t2IN~#j9 zGZIi^Z?kn>1hep$)s!F4VoV6>PRJ~4;c^BhLLZ*(ObrQzB-Dj8CV?~YL7`C#Gw&jMn^DFBm+&yJAWi3L)}2uVz`N?U`- ziln85r&NBtDg4Q=WJu$`&-mx){6L(44jD))DeUCcUro`fuQQ}p|CUw%DNC#F0bt+0z%FW0MA`Bx9$quLAg>TIn#HH%!cyx=^|s|Ug~?H?FYwYRhCKhM^*JwQzR zEWqT>XkQP#O1_B@QOxS0pLr+a^126 zu(_|ng0e?p+SCF$AKy-{4=c*OM8vP_@iB(fseg(%qCV!6gJ)CGN103%nsF+E#{N|2tu!X0wWi712NGz0eqwQiE_(MKy{%# zI;V4$zlM6uTmD$DxRkv)?M5j^FixX6I$`jEwI%i*qzEG#& z>i{WxkP7vLo+k-{ubzu8=w*fSBkf0KPL!^gN&{TB(!P_;b$JMCa-8a=-X|-*hCn1FkP{OlgxXtW13;ld(%1|C@NllQB z-XR~Q{yy1JB;e@NxVb0*X^o*pbZg%^_1@aKGC?KoF2d-~E^1CQo*rq|T&s@ht+wQ5Igb&c>sd5LBD%CcuHIdJE| zZFzOe_6@Js)(GTOZg;#Oyju5K)rRfccGd1^sM0E{H*BcgxsyC+T~E5K&pPQ5F@m;v zP*JGZ0oa?c?XVND!VN{~f*FD}U;(}VP?TKQZ_to(*gbfj>R{i*{?iV72|tw|(|i2d z_QO7p{cQ|31Sbsx`f>o*g(xJeCFY^m~`Z&n~(p9 zO6pdxXxIjP+Q~qkNg5a=!+D9~ZW5ZGgw|$alt^iQQpzwU_8dJX+ol=bl8VEF0u}^L z4Gs~|7GVx16gomS=f?&u2gDC+3m`p6l|CS60b2pb9+0o{V;A)R+A;a6r?_6$xxPmH z?MI68JJ=^$TSRDi2GSmf5LZ!w)R5)*qUcVblT8SD=a4K32I21(+~B&zjX*m_{U0QA z$LFelT=iF{aEDj*N28N8KVv3$yn%aqErJac96_pp&f6q`dGh>1cp`e!XpFI~s( zOv3RpzdXR#7*mv>{iV>NyIKAW;jlyVaC?NDE;flhZ0sWg#U}I0070+_W@flS z68=c(|JT<~)#YJCQ%J8ACCP!FTekCJ^wY0rRqd|YZ4Kn~Qj_PMr5^-%7A(6JAXUba zSVwl4)|rMZQsF3(k|c7{ksYahPntbM{^rPu?8ZMGdTI8)$6T(6;r%jqC%~kV)QYSy ZLkagx2_$PP7KFvU$ZumsvUXYG{{R}cP%!`i delta 2681 zcmZuz4NOy46n^iO2c-hi0(Pwf0W0#OK!FW~bs+*;0Rw6_C($KLjnm96DjTS5bzw7f zRR`W=QRe(v;{46jHCfn}D2TCQro&~KOrxOD`S?-!aTC$x?YsBAR~xdM^qlXWbME)u zb8q`zM-TBWhxo>1VYH+v^_JoCB#w>ClZc5=548m;Q=4}_pTQ?|*_G*dU#En*IObz7iJl}rpaIPWFS6S*r+Fa6Z03WgM@R37!d{Mz5rxl$k#%pr4u8v z>=VbbIF85EBMt)9xAJcBK7i9xJi5}E7BnpxxoT1RV1(>TunWsZ$kl{s;oA^$KOtp7 zHF97rH);Hp45|DNhADqO$P|7%5aUk<7|(xdm@H3p2oHkU#abs^d-S==OnF^KUL;}+}Y%2bYr9SZpEQSkr-mH-} z0Z6N`VUM^I@blK{cq|M1|E)!Xv_B5{fe5Yfkie}!e#k^+mKEoM&nM=<(%})4fM_Ew zF&0Sr=`j_x0;-}N5gO@XhSbwM08%D;)SoAT`K^kmk`;fSNkv;>711An*y$YwkXHN) zO_%r`uqwLIPnsP_Iztm(?9WdL%!etW6)Yyc4L+Y(2CIm^4#Y%X0g#%X&_=TW6-}3D z^^=|rr#-8kwmeWf9b-}Y!#y6D-;F$CPgqRsn?XgpU=`6zKuoj^KuUi~Mb7}LjkftY zU8RAfMVjbAe?e|w-lm8iU@`F{5c*stl!QUw-N&RZ0`~CSF1%U+@ zQQ#3xAo7HVQ5$1`O}Xkm=e~jsdIC4Nm(}*Al4mKIrUEm$yz_{o)hvfRjC3?1zp zN1r6l`T@PO{=B~3+mth^C!HbryE69^U*=&l%N|C)v6t#_A|%C`NDNt47k!=K>&!WC z$?_@qi|}3WUGRf6-Yv_!;q5)LtcUp1yRw`NZ@!N!hCe6C^1JYtu#X0K51KpzUxM4X z2yevqPl9*j*`r5K)p&a)!_yIseH_n?q33d_LlRZVq>m}E-t-&*(eWRC*0hR%7?jzP5 zYw&x33H>B5$C|$#a1;Fz(PJr&h}TtKHOYI<@psU|Hq_B^3=0GUSVa`4{Y6oUxW(jPp_yMpuEK7kR%}LPJUJ~y@iPfuWafWl zIUlhXnJrh4GYLknm0f|x!Et}Enn%nEqzRQVI3g1|Dx0QMu_FOYBW>WEMQiv{GK9)X zR2l^fr>}UyN6~7=vx#QG#APa3^^X_VKo%C8Ep}BU9{(Na(6+UMe2VNIHCvUSWZAU= z?jxs*&HOQ{&Yvf@i_Jo}&O26olK1{L@378WTiT-&_QjCO@^pjNXloMLRX$g^#*^#i z=|=4kHD5<86&c32@Slec&Hw6MS|Rh{Eus>pS8F4y7Nr?Ri