Files
Geroi-Kodeksa/WitheredFlower-forensic/README.md
2026-03-02 21:44:22 +03:00

10 KiB
Raw Blame History

WitheredFlower Forensic

На входе нам дается evidence.zip, внутри которого расположены: backup.ab и папка Logs.

Подготовка

Извлекаем backup.ab через abe.jar (его скачиваем с интернета)

После этого основные пути:

  • логи: .\Logs\...
  • данные приложений: .\ab_extracted\...

Таск 1. BRAND / MODEL / BUILD_ID

  1. Открываем Logs/ADBReport/device_info.txt.
  2. Поиск по файлу:
    • ro.product.manufacturer
    • ro.product.model
  3. Открываем Logs/DumpSysReport/package.txt.
  4. Поиск по файлу: buildFingerprint=.
  5. Из buildFingerprint берём BUILD_ID (часть вида BP2A...).

Флаг: caplag{SAMSUNG/SM-A346E/BP2A.250605.031.A3}

Таск 2. Время последней загрузки + Realtime

  1. Открываем Logs/DumpSysReport/meminfo.txt, в первой строке видим: Realtime: 479801602 ms
  2. Открываем Logs/DumpSysReport/sensorservice.txt, берём:
    • Captured at: 04:00:17.895
    • дату из строк wall=02-10 ... (месяц-день)
  3. Год берём из системных логов с ISO-датой (например, Logs/ADBReport/device_state.txt), здесь это 2026.
  4. Считаем в Python:
from datetime import datetime, timedelta

capture = datetime.fromisoformat("2026-02-10 04:00:17.895")
realtime_ms = 479_801_602
realtime_sec = realtime_ms // 1000

boot_raw = capture - timedelta(seconds=realtime_sec)
boot_rounded = (boot_raw + timedelta(milliseconds=500)).replace(microsecond=0)

days = realtime_sec // 86400
rem = realtime_sec % 86400
hours = rem // 3600
minutes = (rem % 3600) // 60
seconds = rem % 60

print("boot_raw:", boot_raw)
print("boot_rounded:", boot_rounded.strftime("%Y-%m-%d %H:%M:%S"))
print("realtime_DDHHMMSS:", f"{days:02d}:{hours:02d}:{minutes:02d}:{seconds:02d}")

Получаем:

  • boot_rounded = 2026-02-04 14:43:37
  • realtime_DDHHMMSS = 05:13:16:41

Флаг: caplag{2026-02-04/14:43:37_05:13:16:41}

Таск 3. Самый большой файл в Download + SHA-256

  1. Открываем папку ab_extracted/shared/0/Download.
  2. Сортируем файлы по размеру по убыванию.
  3. Самый большой файл: Telegram.apk.
  4. Считаем SHA-256 вручную через 7-Zip:
    • 7-Zip -> CRC SHA -> SHA-256
    • копируем значение

Флаг: caplag{4d3400b9330b2a7d93683fd03ddda41aa144c65f74eb27e3a1786b21397ec7d1}

Таск 4. Самый ранний Wi-Fi BSSID + адрес

  1. Открываем Logs/DumpSysReport/wifi.txt.
  2. Видим, что чаще всего встречается BSSID 00:1C:10:BC:B4:03, он и есть самый ранний
  3. Открываем сайт 3wifi.dev, вбиваем в поиск этот MAC адрес, находим коорды 38.939159, -77.163895
  4. По картам находим адрес дома (1305 BALLANTRAE FARM DRIVE MC LEAN VA 22101)

Флаг: caplag{00:1C:10:BC:B4:03_1305BALLANTRAEFARMDRIVEMCLEANVA22101}

Таск 5. Год постройки, стоимость земли на 2017 год, площадь бассейна в sq ft.

По описанию таска нам нужно найти информацию о соседнем доме с таким же номером, 1305 Ballantrae Ct, McLean, VA 22101

  1. Открываем поиск по адресу на сайте https://icare.fairfaxcounty.gov/ffxcare/search/commonsearch.aspx?mode=address
  2. Вбиваем все данные, получаем pdf документ со всей информацией о доме.
  3. В PDF находим данны о стоимости, площади бассейна и т.д.

Флаг: caplag{1965_887000_720}

Таск 6. Package name + device name

  1. Открываем Logs/ADBReport/packages_thirdparty.txt, ищем projectflower:
    • получаем package name: com.projectflower.agency
  2. Открываем Logs/DumpSysReport/wifi.txt, ищем wifi_p2p_device_name:
    • получаем device name: a-272-8

Флаг: caplag{com.projectflower.agency_a-272-8}

Таск 7. Время первой установки ProjectFlower

  1. Подтверждаем appId для нужного пакета:

    • Logs/DumpSysReport/package.txt
    • по поиску Package [com.projectflower.agency] и appId=10336
  2. Открываем Logs/ADBReport/device_state.txt, ищем Package add: uid=10336.

  3. Среди найденных строк выбираем самое раннее время по timestamp в начале строки.

Самый ранний timestamp: 2026-02-10T02:50:38.948756.

Формат: 2026-02-10/02:50:38

Флаг: caplag{2026-02-10/02:50:38}

Таск 8. Основной sync endpoint

  1. Идём в ab_extracted/apps/com.projectflower.agency/a/.
  2. Открываем base.apk архиватором (или копируем как base.zip и распаковываем).
  3. Читаем файл assets/protocol/kappa.cfg.

Берём:

  • endpoint_primary=wss://relay-core.projectflower.agency/sync
  • port_primary=443

Флаг: caplag{wss://relay-core.projectflower.agency/sync:443}

Таск 9. PIN + SecretCode агента T-4A1KD

PIN

  1. Открываем .\ab_extracted\apps\com.projectflower.agency\a\base.apk в jadx-gui.
  2. Переходим в класс com.projectflower.agency.security.PinManager (или делаем поиск по строке pin_hash и открываем класс-обработчик PIN).
  3. Открываем метод initializeDefaultsIfMissing и/или hashPin.
  4. В initializeDefaultsIfMissing видно, какое 6-значное значение передаётся как дефолтный PIN при инициализации.
  5. В hashPin подтверждаем алгоритм: PBKDF2WithHmacSHA256, 12000 раундов.
  6. Проверочный state лежит в .\ab_extracted\apps\com.projectflower.agency\sp\pf_secure_state.xml (pin_salt, pin_hash), что совпадает с логикой из кода.
  7. Из кода получаем PIN: 473029.

SecretCode

  1. Открываем .\ab_extracted\apps\com.projectflower.agency\db\flowerline.db в DB Browser for SQLite.
  2. Выполняем SQL:
SELECT id, keyPartA, keyPartB FROM chat_threads;
SELECT chatId, bodyCipher, iv FROM messages WHERE senderId='T-4A1KD' AND isEncrypted=1;
  1. Для найденного chatId собираем материал ключа:
    • key_material = keyPartA + "::" + keyPartB + "::orchid/signal/v4"
  2. Расшифровываем bodyCipher (AES-GCM, nonce = iv, AAD пустой) и получаем plaintext.
import base64, hashlib, sqlite3
from pathlib import Path
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

db = Path(r".\ab_extracted\apps\com.projectflower.agency\db\flowerline.db")
con = sqlite3.connect(db)
con.row_factory = sqlite3.Row
cur = con.cursor()

cur.execute("SELECT chatId, bodyCipher, iv FROM messages WHERE senderId='T-4A1KD' AND isEncrypted=1 LIMIT 1")
msg = cur.fetchone()

cur.execute("SELECT keyPartA, keyPartB FROM chat_threads WHERE id=?", (msg["chatId"],))
thr = cur.fetchone()

key_material = f'{thr["keyPartA"]}::{thr["keyPartB"]}::orchid/signal/v4'
key = hashlib.sha256(key_material.encode()).digest()
pt = AESGCM(key).decrypt(base64.b64decode(msg["iv"]), base64.b64decode(msg["bodyCipher"]), None)
print(pt.decode())

Получаем: Code: 19XQF

Флаг: caplag{473029_19XQF}

Таск 10. Восстановление удалённого сообщения + SHA-256

  1. В DB Browser находим удалённое сообщение через ссылку tombstoneRef:
SELECT id, chatId, senderId, tombstoneRef
FROM messages
WHERE senderId='Petal' AND tombstoneRef IS NOT NULL;
  1. По найденному tombstoneRef достаём шифртекст:
SELECT id, messageId, cipherText, iv
FROM tombstones
WHERE id = '<tombstoneRef из шага 1>';
  1. Достаём параметры для ключа:

    • fragment из .\apk_unpack\assets\protocol\kappa.cfg
    • cursor_seed из .\ab_extracted\apps\com.projectflower.agency\sp\pf_ops_seed.xml
  2. Формируем ключ:

    • key = SHA256("PLF-omega-" + fragment + "kappa-19" + seed[:8])
  3. Расшифровываем AES-GCM (cipherText, iv, AAD пустой), получаем строку формата:

    • messageId|chatId|original_body
  4. Считаем SHA-256 только от original_body.

import base64, hashlib, re, sqlite3
from pathlib import Path
from zipfile import ZipFile
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

base = Path(r".")
db = base / "ab_extracted" / "apps" / "com.projectflower.agency" / "db" / "flowerline.db"
seed_xml = (base / "ab_extracted" / "apps" / "com.projectflower.agency" / "sp" / "pf_ops_seed.xml").read_text(encoding="utf-8")
cfg_path = base / "apk_unpack" / "assets" / "protocol" / "kappa.cfg"

if cfg_path.exists():
    cfg = cfg_path.read_text(encoding="utf-8")
else:
    apk = base / "ab_extracted" / "apps" / "com.projectflower.agency" / "a" / "base.apk"
    with ZipFile(apk, "r") as z:
        cfg = z.read("assets/protocol/kappa.cfg").decode("utf-8")

seed = re.search(r'<string name="cursor_seed">([^<]+)</string>', seed_xml).group(1)
fragment = re.search(r"^fragment=(.+)$", cfg, re.M).group(1).strip()

con = sqlite3.connect(db)
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute("SELECT tombstoneRef FROM messages WHERE senderId='Petal' AND tombstoneRef IS NOT NULL ORDER BY createdAt LIMIT 1")
ref = cur.fetchone()["tombstoneRef"]
cur.execute("SELECT cipherText, iv FROM tombstones WHERE id=?", (ref,))
t = cur.fetchone()

key = hashlib.sha256(f"PLF-omega-{fragment}kappa-19{seed[:8]}".encode()).digest()
pt = AESGCM(key).decrypt(base64.b64decode(t["iv"]), base64.b64decode(t["cipherText"]), None).decode()
original_body = pt.split("|", 2)[2]
print(hashlib.sha256(original_body.encode()).hexdigest())

Флаг: caplag{8eddf96d2de82df880e42163a792338c067b65f07ab1d167f9ffec9fe12ceaea}