Skip to main content

sosIsos0k

Ссылка на задание http://f8tasks.ru/challenges#sosIsos0k-42

CategoryReverse
DifficultyHard
TechniqueGo Binary Reverse + AES-256 ECB Partial-Key Brute-force
Flagflag{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:

ШагВызовЧто делает
1concatstring3/4склеивает части ключа со строками из .rodata
2hex.Decodeпереводит hex-строку шифртекста в байты
3aes.NewCipherсоздаёт AES-шифр на 32-байтовом ключе (AES-256)
4Decrypt + 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 видно, как ввод пользователя встраивается в ключ:

$$\text{key} = \underbrace{\texttt{JiQ0pMZ8DOQ8iwpldfCEGJ7EA}}_{\text{25 байт}} + \underbrace{a}_{\text{1 байт}} + \underbrace{\texttt{ytWHy}}_{\text{5 байт}} + \underbrace{b}_{\text{1 байт}} = 32 \text{ байта}$$

Где $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

  1. Go-бинарь без strip — это открытая карта. nm --demangle и objdump мгновенно выдают имена функций; cipher/crypt.Decrypt сразу указал на AES без полной декомпиляции.
  2. AES не взламывают — сокращают пространство. Из 32 байт ключа 30 восстановлены из констант бинаря; 2 неизвестных дают 9 025 вариантов — это меньше секунды перебора на Python.
  3. Ловушка задачи — искать Wrong / Correct. Программа не проверяет пароль: она выводит DECRYPTED: для любого ввода. Новичок тратит время на поиск strcmp, а задача решается с другой стороны — находим ввод, при котором результат осмыслен.