rule GATS_AiTM_shimjs
{
    meta:
        description = "Detects shim.js from the 'gats' AiTM kit (pedrojorge.info cluster, 2026-05). Includes Google context strings and backend API strings for extra specificity."
        author = "osint-lab"
        date = "2026-08-17"
        modified = "2026-10-04"
        version = "7"
        reference = "https://www.pedrojorge.info/posts/2026/05/dkim-spf-dmarc-phishing-google/"
        hash_sha256 = "9322530b01376deb2cac6c35ad34a5e5405fdedb4a26fd71d7cbd9c778274a6e"
        confidence = "high"
        status = "experimental"
        tlp = "CLEAR"
        mitre = "T1566.002, T1111"
        level = "detection"
        fp_validation = "TP=1/1 known sample (all 15 declared string identifiers match, 36 occurrences in total, byte-offset verified). FP=0/58,208 real-world .js/.mjs/.cjs/.ts/.jsx files, independent re-verification (2026-08-22, covering /home + /usr/share + /usr/lib) -- consistent with earlier smaller passes (FP=0/3000+, FP=0/2330 x2, 2026-08-21). Also measured against a 57,809-file non-JS corpus (md/html/json/xml/txt/log/py, 2026-08-22): 37 hits, ALL confirmed as this lab's own threat-intel material (this post, review reports, session caches) quoting the strings, not the actual sample -- zero third-party non-JS false positives -- see scope caveat and WARNING."
        scope = "Detects this specific shim.js build via literal strings, not a structural signature of the framework. Renaming the 'gats' codename removes every $s* string, but does NOT invalidate every leg: Leg 2 ($h1 and 1 of ($s*, $a*, $k1)) still fires on $h1 plus any single $a*/$k1 match, none of which contain 'gats' -- empirically confirmed ($h1+$a3, $h1+$a2, $h1+$k1 all fire with zero 'gats' strings present). Fully evading the rule requires renaming the codename AND changing the header comment ($h1); $k1 is irrelevant once both are gone, since Leg 4 also needs 2 $s* (v7 correction, verified with yara 4.5.5 on the real sample: gats renamed + $h1 changed + $k1 kept = no match). $a1 (/api/sessions/:id/status), $g1 ([jsname=\"P1ekSe\"]) and $h1 itself all match, but their ONLY occurrence in the sample is inside the file's header comment block: $a1's real runtime call is a template literal (`/api/sessions/${s.id}/status`), which YARA cannot match literally, and $g1/$h1 simply aren't reused in code. $k1 ('Kameleo profile') is likewise comment-only (offsets 0x222f/0x251e). A minifier that strips comments (the common default, e.g. Terser) removes $a1/$g1/$h1/$k1 from a build without touching the behavior they describe. The remaining Google context strings ($g2-$g5) are specific to this clone build and DO survive comment-stripping, since they're also present in runtime code; if the actor changes the login template, they break before $s1-$s4/$a1-$a3 do. $a2 was originally declared as the literal path '/api/sessions/:id/action', which does NOT exist anywhere in the sample -- only as the same kind of unmatchable template literal; replaced with 'change_account', a literal action value that does exist in the sample (line 235) and is part of the kit's own action vocabulary (retry_email/change_account/retry). All strings are plain ascii/UTF-8 (confirmed with 'file' against the sample) -- the 'wide' modifier is not used, for lack of any UTF-16 evidence in this cluster. Does not verify file format. v6 removes the former Leg 4 (anchor + a single $a1/$a2 match): it was dead code, fully subsumed by Leg 1 -- any input satisfying it already satisfies Leg 1, since the anchor plus that one $a1/$a2 match are already 2 distinct members of Leg 1's own pool. WARNING: being literal kit strings, the rule also fires on TEXT describing the kit (writeups, threat-intel reports, this very post) -- do not use it to scan documentation/CTI corpora expecting a match to mean the sample is present; it is only reliable against code/binaries."

    strings:
        // Kit identifiers -- "gats" codename and state elements
        $s1 = "gats-session" ascii
        $s2 = "gats-bouncing" ascii
        $s3 = "gats-bouncer-style" ascii
        $s4 = "window.gats" ascii
        $s5 = "gatsCb" ascii

        // Google context -- selectors and paths from the cloned login flow.
        // $g1 exists ONLY inside the header comment (see $h1) -- same
        // comment-only fragility as $a1/$h1/$k1. $g2-$g5 are also present
        // in real code and survive comment-stripping.
        $g1 = "[jsname=\"P1ekSe\"]" ascii
        $g2 = "[jsname=\"OQ2Y6\"]" ascii
        $g3 = "sf-hidden" ascii
        $g4 = "/v3/signin/identifier" ascii
        $g5 = "/v3/signin/challenge/pwd" ascii

        // Kit backend API -- $a1/$a3 are literal paths, present verbatim in
        // the sample; $a2 is a literal action value, NOT a path (the real
        // "/api/sessions/:id/action" path never appears as a literal string
        // in this sample, only as a template literal with interpolation --
        // see scope for detail)
        $a1 = "/api/sessions/:id/status" ascii
        $a2 = "change_account" ascii
        $a3 = "/api/sessions" ascii

        // File header -- the most distinctive string, unique to the kit.
        // Comment-only occurrence, like $a1/$g1/$k1: lives in the file's
        // header block, not in runtime code -- see scope.
        $h1 = "shared client glue loaded by every sign-in template" ascii

        // Kameleo -- reference to the anti-detect browser used by the actor
        // (confirmed in comments in the real file, offsets 0x222f and 0x251e
        // of the original sample with $c1="Kameleo"; "Kameleo profile" is the
        // exact phrase appearing in both comments)
        $k1 = "Kameleo profile" ascii

    condition:
        // Leg 1: strong anchor ($s1/$s4) + at least one more string from the
        // pool (identity, API, Kameleo). The anchor itself counts toward the
        // "2 of" quantifier, so the effective minimum is anchor + 1 support,
        // not anchor + 2 as an earlier version of this comment implied --
        // e.g. $s1+$s2, or $k1+$s1, are enough. Still blocks a weak pair
        // like $s2+$s3 from firing without an anchor.
        (($s1 or $s4) and 2 of ($s*, $a*, $k1))

        // Leg 2: header string + at least one supporting string.
        // $h1 no longer fires alone.
        or ($h1 and 1 of ($s*, $a*, $k1))

        // Leg 3: strong anchor + 2 Google context strings. Confirms the file
        // carrying the kit identifiers is actually tied to the Google login
        // impersonation, not just a script reusing similar names elsewhere.
        or (1 of ($s1, $s4) and 2 of ($g*))

        // Leg 4: explicit mention of the Kameleo profile + 2 kit identifiers.
        // Rules out "Kameleo profile" alone (e.g. in documentation about the
        // anti-detect browser itself) triggering the rule. (Former Leg 4,
        // anchor + single $a1/$a2 match, was removed in v6: it was dead
        // code, fully subsumed by Leg 1 -- see scope.)
        or ($k1 and 2 of ($s*))
}
