sosIsos0k
Ссылка на задание http://f8tasks.ru/challenges#sosIsos0k-42
| Category | Reverse |
| Difficulty | Hard |
| Technique | Go Binary Reverse + AES-256 ECB Partial-Key Brute-force |
| Flag | flag{Heinz_Doofenshmirtz} |
Recon
Программа что-то знает. Что именно она хочет от тебя — узнай сам.
Артефакты: бинарь с псевдослучайным именем (напр. TTc7w6nB) — каждый участник получает уникальное имя файла. Далее используем <binary> как заглушку.
file <binary>
strings -n 4 <binary> | head -50
<binary>: ELF 64-bit LSB executable, x86-64, not stripped, Go build ID: ...
You know what I want from you:
DECRYPTED:
cipher/crypt.Decrypt
main.main
encoding/hex.Decode
crypto/aes.NewCipher
Ключевые наблюдения:
- Бинарь — Go (ELF 64-bit), not stripped — символы функций сохранены.
cipher/crypt.Decryptвиден вstrings— программа расшифровывает, а не проверяет пароль.DECRYPTED:в выводе — программа печатает результат для любого ввода, даже неправильного.encoding/hex.Decode,crypto/aes.NewCipher— импорты стандартных Go-пакетов; алгоритм уже в имени.
Проверка поведения:
echo 'AAAA' | ./<binary>
You know what I want from you:
DECRYPTED: ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
Вывод: бинарь берёт символы ввода как часть ключа AES, расшифровывает фиксированный шифртекст и печатает результат. Задача — найти ввод, при котором вывод будет осмысленным читаемым текстом.
Анализ Go-бинаря
nm — утилита для чтения таблицы символов объектного файла. В не-stripped бинаре она показывает все функции без декомпилятора:
nm -an <binary> | grep -E 'main\.|cipher/crypt\.'
main.main
cipher/crypt.Decrypt
Go сохраняет полное имя пакета: cipher/crypt — внутренний пакет задачи с единственной функцией Decrypt.
Смотрим, откуда Decrypt вызывается, и что происходит внутри:
objdump -d --demangle <binary> | grep -E 'runtime\.concatstring|encoding/hex|crypto/aes\.NewCipher|fmt\.Fprintln'
runtime.concatstring3
runtime.concatstring4
encoding/hex.Decode
crypto/aes.NewCipher
fmt.Fprintln
Порядок вызовов → реконструированный алгоритм Decrypt:
| Шаг | Вызов | Что делает |
|---|---|---|
| 1 | concatstring3/4 | склеивает части ключа со строками из .rodata |
| 2 | hex.Decode | переводит hex-строку шифртекста в байты |
| 3 | aes.NewCipher | создаёт AES-шифр на 32-байтовом ключе (AES-256) |
| 4 | Decrypt + Fprintln | расшифровывает блок и печатает DECRYPTED: ... |
Восстановление констант через gdb
gdb (GNU Debugger) умеет читать строки из секций бинаря по адресу, не запуская программу:
gdb -q -batch <binary> \
-ex 'x/29cb 0x4b578f' \
-ex 'x/5cb 0x4b1159' \
-ex 'x/5cb 0x4b11e0' \
-ex 'x/25cb 0x4b4aad'
8648a0cc3125e23e3ee4318000000 ← окончание hex-строки шифртекста
efe0a ← начало hex-строки шифртекста
ytWHy ← средняя часть ключа
JiQ0pMZ8DOQ8iwpldfCEGJ7EA ← префикс ключа (25 символов)
Из main.main видно, как ввод пользователя встраивается в ключ:
Где $a$ и $b$ — первый и второй символы нашего ввода. 30 из 32 байт ключа уже известны.
Brute-force двух символов
AES-256 (Advanced Encryption Standard, 256 бит) — симметричный блочный шифр: если есть ключ и шифртекст — расшифровать тривиально. Здесь нужно лишь найти 2 неизвестных байта.
Пространство перебора: $95 \times 95 = 9025$ комбинаций печатаемых ASCII-символов (коды 32–126). Перебирается за долю секунды.
Критерий победы: все 16 байт расшифрованного блока — печатаемые символы (32–126).
Ручная проверка гипотезы $a = $ I, $b = $ 0:
key = ("JiQ0pMZ8DOQ8iwpldfCEGJ7EA" + "I" + "ytWHy" + "0").encode()
len(key) # → 32 (ровно AES-256)
Automation
from Crypto.Cipher import AES
# Первый 16-байтовый блок шифртекста — извлечён из бинаря через gdb
CIPHERTEXT = bytes.fromhex("efe0acb2b8648a0cc3125e23e3ee4318")
# 30 из 32 байт ключа уже известны из констант бинаря
PREFIX = "JiQ0pMZ8DOQ8iwpldfCEGJ7EA" # 25 символов
MIDDLE = "ytWHy" # 5 символов
def is_printable(data: bytes) -> bool:
# Блок «говорящий», если все его байты — печатаемые ASCII (32–126)
return all(32 <= byte < 127 for byte in data)
for first in range(32, 127): # перебор первого символа: 95 вариантов
for second in range(32, 127): # перебор второго символа: 95 вариантов
# Собираем 32-байтовый ключ AES-256
key = (PREFIX + chr(first) + MIDDLE + chr(second)).encode()
# ECB-режим: каждый блок расшифровывается независимо, IV не нужен
block = AES.new(key, AES.MODE_ECB).decrypt(CIPHERTEXT)
if is_printable(block):
print(f"{chr(first)}{chr(second)} → {block.decode('latin1')}")
pip install pycryptodome
python3 solve.py
# I0 → Heinz_Doofenshmi
Верификация на оригинальном бинаре:
echo 'I0' | ./<binary>
# You know what I want from you:
# DECRYPTED: Heinz_Doofenshmirtz
Key Takeaways
- Go-бинарь без strip — это открытая карта.
nm --demangleиobjdumpмгновенно выдают имена функций;cipher/crypt.Decryptсразу указал на AES без полной декомпиляции. - AES не взламывают — сокращают пространство. Из 32 байт ключа 30 восстановлены из констант бинаря; 2 неизвестных дают 9 025 вариантов — это меньше секунды перебора на Python.
- Ловушка задачи — искать
Wrong / Correct. Программа не проверяет пароль: она выводитDECRYPTED:для любого ввода. Новичок тратит время на поискstrcmp, а задача решается с другой стороны — находим ввод, при котором результат осмыслен.
