Init. import
This commit is contained in:
13
ReverseConveyor-PPC/README.md
Normal file
13
ReverseConveyor-PPC/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## Информация для участников
|
||||
> Каждый запуск ReverseConveyor — как смена на ночном заводе
|
||||
|
||||
## Выдать участникам
|
||||
Удалённый сервис: `http://<host>:8080`.
|
||||
(Дополнительных файлов не требуется.)
|
||||
|
||||
## Решение
|
||||
Основная идея — реверснуть все 4 крякми, найти закономерность, понять паттерн (крякми каждый раз одинаковые - пароли разные). Автоматически достать секреты: перехватить `strcmp`/`memcmp` через `LD_PRELOAD`, извлечь случайные байты и отправить правильный ответ.
|
||||
Готовый скрипт: [solve/auto_solve.py](solve/auto_solve.py).
|
||||
|
||||
## Флаг
|
||||
`caplag{1_L0v3_R3V3rs3_3sp3C1411Y_4Ut0M4t10N}`
|
||||
270
ReverseConveyor-PPC/solve/auto_solve.py
Normal file
270
ReverseConveyor-PPC/solve/auto_solve.py
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/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 <dlfcn.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user