Init. commit
This commit is contained in:
204
forensic-missing-colleague/solve/solver.py
Normal file
204
forensic-missing-colleague/solve/solver.py
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import base64
|
||||
import csv
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
PUBLIC_DIR = Path(__file__).resolve().parent.parent / "public"
|
||||
|
||||
|
||||
def extract_part1_br34d():
|
||||
"""
|
||||
Часть 1: 'br34d'
|
||||
Цепочка: resume.pdf -> ID сотрудника 'NT-3893'
|
||||
business_card.png -> ИНН 7707083893 (последние 4 цифры = 3893, подтверждает связь с NordTech)
|
||||
commits.log -> тикет NT-3893 ссылается на 'module-br34d'
|
||||
"""
|
||||
print("[Шаг 1] Извлечение части 1: br34d")
|
||||
|
||||
employee_id = "NT-3893"
|
||||
print(f" [1a] resume.pdf — ID сотрудника: {employee_id}")
|
||||
print(" [1b] business_card.png — ИНН: 7707083893 (последние 4 цифры = 3893, совпадает)")
|
||||
|
||||
commits = (PUBLIC_DIR / "commits.log").read_text()
|
||||
# Ищем коммит вида: feat(NT-3893): integrate module-<название>
|
||||
pattern = rf"feat\({employee_id}\): integrate (module-\w+)"
|
||||
match = re.search(pattern, commits)
|
||||
if match:
|
||||
module_name = match.group(1)
|
||||
part1 = module_name.replace("module-", "") # Убираем префикс, оставляем только кодовое слово
|
||||
print(f" [1c] commits.log: '{match.group(0)}'")
|
||||
print(f" Часть 1: '{part1}'")
|
||||
return part1
|
||||
return None
|
||||
|
||||
|
||||
def extract_part2_crumbs():
|
||||
"""
|
||||
Часть 2: 'crumbs' (метод второго раунда)
|
||||
Цепочка: profile_photo.jpg GPS -> 55.7616N, 37.6385E
|
||||
postal_codes.csv -> находим почтовый индекс 101000, соответствующий этим координатам
|
||||
столбец sector_code = '6372756d6273' -> hex в ASCII = 'crumbs'
|
||||
"""
|
||||
print("\n[Шаг 2] Извлечение части 2: crumbs (через поиск по почтовому индексу)")
|
||||
|
||||
# Шаг 2a: Извлекаем GPS-координаты из EXIF-данных фотографии
|
||||
import piexif
|
||||
|
||||
img_path = str(PUBLIC_DIR / "profile_photo.jpg")
|
||||
exif = piexif.load(img_path)
|
||||
|
||||
def parse_gps_coord(coord_data, ref):
|
||||
"""Конвертируем GPS из формата DMS (градусы/минуты/секунды) в десятичные градусы."""
|
||||
degrees = coord_data[0][0] / coord_data[0][1]
|
||||
minutes = coord_data[1][0] / coord_data[1][1]
|
||||
seconds = coord_data[2][0] / coord_data[2][1]
|
||||
result = degrees + minutes / 60 + seconds / 3600
|
||||
if ref in [b'S', b'W']: # Южная широта и западная долгота — отрицательные
|
||||
result = -result
|
||||
return result
|
||||
|
||||
gps = exif.get("GPS", {})
|
||||
lat = parse_gps_coord(gps[piexif.GPSIFD.GPSLatitude], gps[piexif.GPSIFD.GPSLatitudeRef])
|
||||
lon = parse_gps_coord(gps[piexif.GPSIFD.GPSLongitude], gps[piexif.GPSIFD.GPSLongitudeRef])
|
||||
print(f" [2a] GPS: {lat:.4f}N, {lon:.4f}E (район Чистопрудного бульвара)")
|
||||
|
||||
# Шаг 2b: Ищем ближайшую точку в таблице почтовых индексов по манхэттенскому расстоянию
|
||||
csv_path = PUBLIC_DIR / "postal_codes.csv"
|
||||
best_match = None
|
||||
best_dist = float('inf')
|
||||
|
||||
with open(csv_path) as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rlat = float(row['latitude'])
|
||||
rlon = float(row['longitude'])
|
||||
# Манхэттенское расстояние — достаточно для грубого геопоиска
|
||||
dist = abs(rlat - lat) + abs(rlon - lon)
|
||||
if dist < best_dist:
|
||||
best_dist = dist
|
||||
best_match = row
|
||||
|
||||
print(f" [2b] Ближайший индекс: {best_match['postal_code']} ({best_match['district']}, dist={best_dist:.4f})")
|
||||
print(f" sector_code: {best_match['sector_code']}")
|
||||
|
||||
# Шаг 2c: Декодируем hex-строку sector_code в ASCII — это и есть часть флага
|
||||
hex_str = best_match['sector_code']
|
||||
decoded = bytes.fromhex(hex_str).decode('ascii')
|
||||
print(f" [2c] hex -> ASCII: '{decoded}'")
|
||||
return decoded
|
||||
|
||||
|
||||
def extract_part3_l34d():
|
||||
"""
|
||||
Часть 3: 'l34d'
|
||||
Цепочка: commits.log -> ссылка на коммит SHA 'a7c3e91' во внешнем репозитории
|
||||
repo_snapshot.txt -> коммит a7c3e91 содержит base64-строку
|
||||
base64-декодирование -> 'module-l34d-integration-v2.1.0'
|
||||
"""
|
||||
print("\n[Шаг 3] Извлечение части 3: l34d")
|
||||
|
||||
commits = (PUBLIC_DIR / "commits.log").read_text()
|
||||
# Ищем отсылку к внешнему репозиторию вида: "See commit <sha> in <repo>"
|
||||
ref_match = re.search(r"See commit\s+(\w+)\s+in\s+([\w\-\.\/]+)", commits, re.DOTALL)
|
||||
if not ref_match:
|
||||
return None
|
||||
|
||||
sha_prefix = ref_match.group(1) # Короткий SHA (7 символов)
|
||||
repo = ref_match.group(2)
|
||||
print(f" [3a] commits.log ссылается на {sha_prefix} в {repo}")
|
||||
|
||||
snapshot = (PUBLIC_DIR / "repo_snapshot.txt").read_text()
|
||||
# Находим весь блок коммита — от нашего SHA до следующего коммита
|
||||
commit_pattern = rf"commit {sha_prefix}\w*\n.*?(?=\ncommit [a-f0-9]{{40}}|\Z)"
|
||||
commit_match = re.search(commit_pattern, snapshot, re.DOTALL)
|
||||
if not commit_match:
|
||||
return None
|
||||
|
||||
commit_block = commit_match.group(0)
|
||||
# Ищем все потенциальные base64-строки длиной от 20 символов
|
||||
b64_pattern = r'[A-Za-z0-9+/]{20,}={0,2}'
|
||||
b64_matches = re.findall(b64_pattern, commit_block)
|
||||
|
||||
for b64_str in b64_matches:
|
||||
try:
|
||||
decoded = base64.b64decode(b64_str).decode("utf-8", errors="ignore")
|
||||
if "module-" in decoded:
|
||||
# Из строки вида "module-l34d-integration-v2.1.0" берём только кодовое слово
|
||||
mod_match = re.search(r"module-(\w+)", decoded)
|
||||
if mod_match:
|
||||
part3 = mod_match.group(1).split("-")[0] # Только первый сегмент до дефиса
|
||||
print(f" [3b] base64: {b64_str}")
|
||||
print(f" decoded: {decoded}")
|
||||
print(f" Часть 3: '{part3}'")
|
||||
return part3
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def extract_part4_h0m3():
|
||||
"""
|
||||
Часть 4: 'h0m3' (метод второго раунда)
|
||||
Цепочка: browser_render.png -> LSB-стеганография в красном канале
|
||||
Извлекаем LSB из первых пикселей -> 'h0m3'
|
||||
|
||||
"""
|
||||
print("\n[Шаг 4] Извлечение части 4: h0m3 (через LSB-стеганографию)")
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
img = Image.open(str(PUBLIC_DIR / "browser_render.png"))
|
||||
pixels = np.array(img)
|
||||
|
||||
# Разворачиваем красный канал (индекс 0) в одномерный массив
|
||||
flat_r = pixels[:, :, 0].flatten()
|
||||
|
||||
# Читаем байты: каждые 8 последовательных LSB пикселей = 1 символ
|
||||
result = bytearray()
|
||||
for byte_idx in range(100): # Ограничение на 100 байт во избежание зависания
|
||||
byte_val = 0
|
||||
for bit_idx in range(8):
|
||||
pixel_idx = byte_idx * 8 + bit_idx
|
||||
byte_val = (byte_val << 1) | (flat_r[pixel_idx] & 1)
|
||||
if byte_val == 0:
|
||||
break # Нулевой байт — признак конца скрытых данных
|
||||
result.append(byte_val)
|
||||
|
||||
decoded = result.decode('utf-8', errors='replace')
|
||||
print(f" [4a] LSB из красного канала (сырые байты): {result}")
|
||||
print(f" [4b] Декодировано: '{decoded}'")
|
||||
|
||||
# Данные могут начинаться с маркера трассировки вида [N/4] — удаляем его
|
||||
import re
|
||||
m = re.match(r'\[\d/\d\](.*)', decoded)
|
||||
if m:
|
||||
decoded = m.group(1)
|
||||
print(f" [4c] После удаления маркера трассировки: '{decoded}'")
|
||||
return decoded
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("OSINT-задание «Пропавший коллега» — Решение (R2)")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
part1 = extract_part1_br34d()
|
||||
part2 = extract_part2_crumbs()
|
||||
part3 = extract_part3_l34d()
|
||||
part4 = extract_part4_h0m3()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Сборка флага")
|
||||
print("=" * 60)
|
||||
print(f" Часть 1 (commits.log через NT-3893): {part1}")
|
||||
print(f" Часть 2 (sector_code почтового индекса): {part2}")
|
||||
print(f" Часть 3 (base64 из внешнего репозитория): {part3}")
|
||||
print(f" Часть 4 (LSB-стего в browser_render.png): {part4}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user