// YARA rules — Watcher-NetAI cluster (skn loader + astats scanner) // // Author: OHIIHO Research // Created: 2026-05-14 // Reference: Watcher-NetAI / skn public report, OHIIHO Research, 2026-05-14 // // Coverage: // - WatcherNetAI_skn_Scanner — astats / skn module, 10MB Go ELF non-stripped (DWARF intact) // - WatcherNetAI_Loader_Packed_SHA256 — hash-pinned sample rule (not family-wide), UPX-packed loader as delivered by C2 // - WatcherNetAI_Loader_Unpacked — UPX-decompressed loader (custom gopclntab magic) // - WatcherNetAI_LauncherScript — w.sh launcher script (1413 bytes, Romanian comments) // - WatcherNetAI_PersistenceUnit — systemd user unit on victim // // IMPORTANT: the operator's static HTTP authorization token is intentionally // not included. These public rules match on structural and operator-specific // artefacts useful for defense without publishing authentication material. import "elf" import "hash" rule WatcherNetAI_skn_Scanner { meta: description = "Watcher-NetAI cluster — skn module scanner (astats binary, multi-purpose: SYN scan + cred validate + SOCKS5 proxy + password change + HTTPS server)" author = "OHIIHO Research" date = "2026-05-14" version = "1.0" sha256 = "a1297637c8c4300618bf407de15682e26c142ba2028338f101e045ec91f1073d" reference = "Watcher-NetAI / skn public report, OHIIHO Research, 2026-05-14" severity = "high" strings: // Operator source-tree paths (DWARF leak) $src_path = "/root/.wrx/skn/" // Top-level function names (Go symbol table, ASCII) $fn_socks5 = "main.handleSocks5Conn" $fn_pwsmart = "main.changePasswordSmart" $fn_runazSyn = "main.RunAZModeSyn" $fn_runrgSyn = "main.RunRangesModeSyn" $fn_sshcheck = "main.sshcheck" $fn_watchdog = "main.startWatchdog" $fn_fetchdata = "main.fetchData" // Embedded operator strings $vyos_primitive = "configure; set system login user vyos authentication plaintext-password \"" $harvest_sshsv = "/harvest/sshsv" $harvest_users = "/harvest/users" $harvest_sshl = "/harvest/sshlist" $scanfiles_path = ":443/update/scanfiles/" $scanfiles_pass = ":443/update/pass/" // Module identifier (Go buildinfo) $mod_skn = { 73 6B 6E 09 28 64 65 76 65 6C 29 } // "skn\t(devel)" in buildinfo // Dependency signature $dep_fslock = "github.com/juju/fslock" condition: uint32(0) == 0x464C457F // ELF magic and filesize > 5MB and filesize < 25MB and ( // Hard signal: source path + 2 function names ($src_path and 2 of ($fn_*)) // OR: source path + 1 operator-specific embedded string or ($src_path and 1 of ($harvest_sshsv, $harvest_users, $harvest_sshl, $scanfiles_path, $scanfiles_pass, $vyos_primitive)) // OR: module signature + buildinfo dep or ($mod_skn and $dep_fslock) ) } rule WatcherNetAI_Loader_Packed_SHA256 { meta: description = "Watcher-NetAI cluster — hash-pinned sample rule (not family-wide detection) for the UPX-packed loader (.sysmonitor delivered by C2)" author = "OHIIHO Research" date = "2026-05-14" version = "1.0" sha256 = "712737cdde50fcce895f263ea18ed7f8e425dcb72f8ec63101ea0361c6602140" reference = "URLhaus 3793559" severity = "high" note = "This rule is sample-specific. The condition is anchored on the exact SHA256 of the captured loader build. New operator builds of the loader will have new hashes and require their own rule. For a family-wide signature on the unpacked loader, see WatcherNetAI_Loader_Unpacked_CustomGopcln below — it anchors on the custom .gopclntab magic which is toolchain-level rather than per-build." strings: $upx_marker = "UPX!" condition: uint32(0) == 0x464C457F and filesize > 3MB and filesize < 6MB and $upx_marker and hash.sha256(0, filesize) == "712737cdde50fcce895f263ea18ed7f8e425dcb72f8ec63101ea0361c6602140" } rule WatcherNetAI_Loader_Unpacked_CustomGopcln { meta: description = "Watcher-NetAI cluster — unpacked loader with CUSTOM .gopclntab magic (obfuscation marker)" author = "OHIIHO Research" date = "2026-05-14" version = "1.0" sha256 = "7a71a17bde3611a1b147f8946106d6da4d859c0e322fb2bc437cc6c3eddb5390" reference = "Watcher-NetAI / skn public report — loader obfuscation section" severity = "medium" note = "The custom gopclntab magic d6 bb e4 9e is the key obfuscation signal. We have not seen this exact magic value documented in other public CTI literature as of 2026-05-14. Likely a custom obfuscator (beyond Garble)." strings: // The exact custom gopclntab magic — NOT a Go-standard magic value. // Standard Go 1.18+ magic is F1 FF FF FF. This sample uses: $custom_gopclntab_magic = { D6 BB E4 9E 00 00 01 08 } condition: uint32(0) == 0x464C457F and filesize > 10MB and filesize < 20MB and $custom_gopclntab_magic } rule WatcherNetAI_LauncherScript { meta: description = "Watcher-NetAI cluster — w.sh launcher script (role dispatcher, Romanian comments)" author = "OHIIHO Research" date = "2026-05-14" version = "1.0" reference = "Watcher-NetAI / skn public report" severity = "high" strings: $hash_bang = "#!/usr/bin/env bash" $argv_pattern = "\"astats\" \"netai\" \"kstats\" \"ssh 2 ranges\"" $rom_decizie = "# DECIZIE: miner-only vs scan+bot" $rom_doarscan = "# doar scan" $fn_startbg = "start_bg() {" $fn_stopif = "stop_if_running() {" condition: filesize > 800 and filesize < 3000 and $hash_bang and ( $argv_pattern or ($rom_decizie and $rom_doarscan) or ($fn_startbg and $fn_stopif) ) } rule WatcherNetAI_SystemdUserUnit { meta: description = "Watcher-NetAI cluster — systemd user unit dropped by loader non-root branch" author = "OHIIHO Research" date = "2026-05-14" version = "1.0" reference = "Watcher-NetAI / skn public report — field observation" severity = "high" strings: $description = "Description=Watch netai via w.sh" $exec_argv = "ExecStart=/tmp/w.sh \"astats\" \"netai\" \"kstats\" \"ssh 2 ranges\"" $exec_devshm = "ExecStart=/dev/shm/w.sh \"astats\" \"netai\" \"kstats\" \"ssh 2 ranges\"" $wantedby = "WantedBy=default.target" condition: filesize > 100 and filesize < 1000 and $description and ($exec_argv or $exec_devshm) and $wantedby } /* ---------------------------------------------------------------------- * Hunting queries (Sigma/Elastic equivalents) * --------------------------------------------------------------------*/ // File-event hunt : // file.path : "*/.config/systemd/user/watcher-netai.service" // OR file.path : "/dev/shm/astats" // OR file.path : "/tmp/w.sh" AND file.size : [800 TO 3000] // // Process hunt : // process.executable : "/dev/shm/astats" // OR process.command_line : ("*\"astats\" \"netai\" \"kstats\" \"ssh 2 ranges\"*") // // Network hunt (egress) : // destination.ip : "64.89.161.144" // OR destination.domain : ("connexionlost.net" OR "connexionlost.zip") // OR url.path : "/update/scanfiles/*" OR url.path : "/update/pass/*" // OR url.path : "/harvest/sshsv" OR url.path : "/harvest/users" // OR url.path : "/harvest/sshlist" // // HASSH hunt : // ssh.hassh : "0a07365cc01fa9fc82608ba4019af499" // (NB: stock OpenSSH client also matches; combine with destination context)