4.3 KiB
Digital Fingerprint
Есть два файла: hashlib_custom.py с реализацией кастомного CaPlagHash64 и verify.py, который регистрирует и проверяет решение. По названию как будто бы нужно найти коллизию, но это не все. Если просто собрать две разные строки с одинаковым хешем и отправить, verify.py скажет, что «ты близко», но флаг не отдаст. Читаем верификатор внимательнее и видим вторую проверку:
| Требование | Описание |
|---|---|
| Префикс | оба сообщения начинаются с CAPLAG: |
| Различие | msg1 != msg2 |
| Коллизия | caplag_hash(msg1) == caplag_hash(msg2) |
| CRC-фильтр | CRC32(msg) & 0xff == 0 для обоих |
Решение
Начнём с хеша — его нужно разобрать и найти, где он прогибается. Функция сжатия:
\mathrm{compress}(s, b) = \bigl((s \cdot A + b) \oplus B\bigr) \bmod 2^{64}
XOR с константой B вроде бы делает функцию нелинейной, но сам B фиксирован — значит, это просто сдвиг, и структура остаётся почти линейной. Можно обойтись без перебора.
Multi-block collision в Merkle–Damgård-хеше. При фиксированном состоянии
sфункция\mathrm{compress}(s, b) = (s \cdot A + b) \oplus B— биекция поbс обратимой структурой. Для любых двух состояний после первого блока всегда можно подобрать второй блок, чтобы их уравнять. Коллизия строится алгебраически за одно вычисление.
Строим коллизию из двух блоков после общего префикса CAPLAG:\x00. Префикс одинаковый, значит после первого блока состояние тоже одинаковое:
s_0 = \mathrm{compress}(\mathrm{IV},\, \mathrm{prefix\_block})
Выбираем два разных блока b_{1a} и b_{1b}, получаем два разных состояния s_{1a} и s_{1b}. Теперь хотим, чтобы следующий раунд их обратно уравнял:
\mathrm{compress}(s_{1a}, b_{2a}) = \mathrm{compress}(s_{1b}, b_{2b})
Раскрываем формулу сжатия — и получаем красивую связь:
b_{2b} = b_{2a} + (s_{1a} - s_{1b}) \cdot A \pmod{2^{64}}
То есть b_{1a}, b_{1b} и b_{2a} можно брать любые, а b_{2b} просто вычисляется.
Вторая часть — CRC32. Просто дописать хвост, чтобы подогнать CRC, не получится: сломается сам хеш. Идем в лоб, коллизии строятся мгновенно, поэтому гоняем генератор в цикле и ждём, пока оба сообщения случайно попадут под CRC32(msg) & 0xff == 0. Вероятность для одной пары:
P = \frac{1}{256} \cdot \frac{1}{256} = \frac{1}{65536}
Отправляем подходящую её в verify.py, получаем флаг.
verify.pyсчитает флаг как функцию от найденногоcollision_hash, так что его конкретное значение зависит от того, какую именно коллизию вы нашли. Поэтому флаг - регулярное выражение.
Флаг
caplag{...} (значение зависит от найденной коллизии, принимается по regex)