#!/usr/bin/env python3 """ Usage: python3 auto_solve.py --url <...> """ import argparse import json import os import re import subprocess import sys import tempfile import urllib.request import zipfile from pathlib import Path, PurePosixPath from stat import S_IXUSR, S_IXGRP, S_IXOTH HOOK_SOURCE = r""" #define _GNU_SOURCE #include #include #include #include typedef int (*strcmp_t)(const char *, const char *); typedef int (*memcmp_t)(const void *, const void *, size_t); static __thread int rc_in_hook = 0; int strcmp(const char *s1, const char *s2) { static strcmp_t real_strcmp = NULL; if (!real_strcmp) { real_strcmp = (strcmp_t)dlsym(RTLD_NEXT, "strcmp"); } if (!rc_in_hook && s2) { rc_in_hook = 1; dprintf(2, "[RC_HOOK_STR] secret=%s\n", s2); rc_in_hook = 0; } return real_strcmp(s1, s2); } int memcmp(const void *s1, const void *s2, size_t n) { static memcmp_t real_memcmp = NULL; if (!real_memcmp) { real_memcmp = (memcmp_t)dlsym(RTLD_NEXT, "memcmp"); } if (!rc_in_hook && s2 && n <= 1024) { rc_in_hook = 1; dprintf(2, "[RC_HOOK_MEM] len=%zu data=", n); const unsigned char *p = (const unsigned char *)s2; for (size_t i = 0; i < n; ++i) { dprintf(2, "%02x", p[i]); } dprintf(2, "\n"); rc_in_hook = 0; } return real_memcmp(s1, s2, n); } """ MEM_RE = re.compile(r"\[RC_HOOK_MEM\]\s*len=(\d+)\s+data=([0-9a-fA-F]+)") STR_RE = re.compile(r"\[RC_HOOK_STR\]\s*secret=(.+)") DEFAULT_URL = os.environ.get("RC_URL", "URL") def normalize_member_name(name: str) -> str: return PurePosixPath(name).name def request_archive(base_url: str, target_dir: Path) -> tuple[str, Path, list[str]]: url = base_url.rstrip('/') + '/generate' req = urllib.request.Request(url, data=b'', method='POST') try: with urllib.request.urlopen(req) as resp: data = resp.read() cookies = resp.headers.get_all('Set-Cookie') or [] except urllib.error.HTTPError as exc: raise RuntimeError(f"/generate failed: {exc.read().decode(errors='ignore')}") from exc session_id = None for cookie in cookies: parts = cookie.split(';') for part in parts: part = part.strip() if part.startswith('session_id='): session_id = part.split('=', 1)[1] break if session_id: break if not session_id: raise RuntimeError('session_id cookie was not returned by the server') archive_path = target_dir / 'bundle.zip' archive_path.write_bytes(data) extract_dir = target_dir / 'crackmes' extract_dir.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(archive_path, 'r') as zf: archive_order = [] for member in zf.namelist(): normalized = normalize_member_name(member) if normalized and normalized not in archive_order: archive_order.append(normalized) zf.extractall(extract_dir) archive_order = [name for name in archive_order if name in EXTRACTORS] if len(archive_order) != len(EXTRACTORS): missing = set(EXTRACTORS) - set(archive_order) raise RuntimeError(f"unexpected crackme list in archive, missing: {sorted(missing)}") for item in extract_dir.iterdir(): if item.is_file(): mode = item.stat().st_mode | S_IXUSR | S_IXGRP | S_IXOTH item.chmod(mode) return session_id, extract_dir, archive_order def build_hook(temp_dir: Path) -> Path: src = temp_dir / 'rc_hook.c' so = temp_dir / 'rc_hook.so' src.write_text(HOOK_SOURCE) compile_cmd = ['gcc', '-shared', '-fPIC', '-O2', str(src), '-o', str(so), '-ldl'] try: subprocess.run(compile_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except FileNotFoundError as exc: raise RuntimeError('gcc is required to build the hook but was not found') from exc return so def run_binary(binary: Path, hook: Path, payload: bytes, timeout: float = 3.0) -> str: env = os.environ.copy() env['LD_PRELOAD'] = str(hook) proc = subprocess.run( [str(binary)], input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, timeout=timeout, check=False, ) return proc.stderr.decode(errors='ignore') def extract_pass1(binary: Path, hook: Path) -> str: logs = run_binary(binary, hook, b'test\n') m = STR_RE.search(logs) if not m: raise RuntimeError('Failed to capture strcmp secret for crackme1') return m.group(1).strip() def extract_mem_secret(binary: Path, hook: Path, payload: bytes) -> tuple[int, bytes]: logs = run_binary(binary, hook, payload) m = MEM_RE.search(logs) if not m: raise RuntimeError(f'No memcmp hook output for {binary.name}') length = int(m.group(1)) data = bytes.fromhex(m.group(2)) return length, data[:length] def extract_pass2(binary: Path, hook: Path) -> str: for length in range(1, 33): try: _, data = extract_mem_secret(binary, hook, ("A" * length + "\n").encode()) break except RuntimeError: continue else: raise RuntimeError('Unable to trigger memcmp for crackme2') key = b'cyber32' plaintext = bytes(data[i] ^ key[i % len(key)] for i in range(len(data))) return plaintext.decode() def extract_pass3(binary: Path, hook: Path) -> str: payload = b"0" * 64 + b"\n" logs = run_binary(binary, hook, payload) matches = list(MEM_RE.finditer(logs)) if not matches: raise RuntimeError("No memcmp detected in crackme3") for match in reversed(matches): length = int(match.group(1)) hex_data = match.group(2) if length == 32 and len(hex_data) == 64: return hex_data.lower() last = matches[-1] length = int(last.group(1)) data = bytes.fromhex(last.group(2))[:length] return data.hex() def extract_pass4(binary: Path, hook: Path) -> str: _, data = extract_mem_secret(binary, hook, b'A' * 4) return data.hex() EXTRACTORS = { 'crackme1': extract_pass1, 'crackme2': extract_pass2, 'crackme3': extract_pass3, 'crackme4': extract_pass4, } def solve_in_order(crackme_dir: Path, hook: Path, archive_order: list[str]) -> list[str]: answers = [] for name in archive_order: extractor = EXTRACTORS.get(name) if not extractor: raise RuntimeError(f'unknown crackme entry: {name}') binary = crackme_dir / name if not binary.exists(): raise RuntimeError(f'binary {binary} was not extracted') answers.append(extractor(binary, hook)) return answers def submit_answer(base_url: str, session_id: str, combined: str) -> dict: url = base_url.rstrip('/') + '/submit' payload = json.dumps({'answer': combined}).encode() headers = { 'Content-Type': 'application/json', 'Cookie': f'session_id={session_id}', } req = urllib.request.Request(url, data=payload, method='POST', headers=headers) try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as exc: raise RuntimeError(f'/submit failed: {exc.read().decode(errors="ignore")}') from exc def main(): parser = argparse.ArgumentParser(description='Automatic solver for ReverseConveyor') parser.add_argument('--url', default=DEFAULT_URL, help='Base URL of the service (default: %(default)s)') args = parser.parse_args() with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) session_id, crackme_dir, archive_order = request_archive(args.url, tmp_path) hook = build_hook(tmp_path) print(f"[+] Archive order: {' '.join(archive_order)}") parts = solve_in_order(crackme_dir, hook, archive_order) combined = '_'.join(parts) print(f'[+] Answer: {combined}') result = submit_answer(args.url, session_id, combined) if result.get('ok'): print(f"[+] Flag: {result['flag']}") else: print('[-] Server rejected the answer:', result) if __name__ == '__main__': try: main() except Exception as exc: print(f'[!] {exc}', file=sys.stderr) sys.exit(1)