Как стать автором
Обновить

История одного бага или как я ZIP паролил

Время на прочтение4 мин
Количество просмотров7.2K
Это история личного опыта, опыта поиска бага в чужом, старом, неподдерживаемом коде.
Все начиналось как обычно, передо мной стояла простая на первый взгляд задача: сделать упаковку файлов в текущей папке в ZIP архив с определенным паролем на C++/Qt, казалось бы что может быть проще?
Естественно, первый помощник это Google, он и подсказал что существует две Qt библиотеки для работы с ZIP архивами:
QuaZIP и OSDab ZIP, помимо всего, сам Qt поддерживает методы qCompress и qDecompress для упаковки.
Мною было выяснено что методы мне мало подходят, потому что они умеют лишь жать поток, все заголовки и шифрование на совести разработчика. Этот путь был слишком долог и от него я отказался сразу и обратил свое внимание на библиотеки.
OSDaB ZIP пришлось отбросить сразу, не смотря на то, что это отличная библиотека, ее код распространяется только под лицензией GPL, мне же нужно было встроить функционал в проприетарное приложение. К счастью QuaZIP оказался с двумя лицензиями GPL и LGPL. На нем я и остановился. Особо не вникая в его устройство, я набросал простейший класс для работы ним и начал тестировать.


Вот тут и начались проблемы: архив прекрасно создавался и шифровался этой библиотекой, он также хорошо ей распаковывался, но вот незадача: любой другой архиватор отказывался признавать правильный пароль, сообщая, что он не верен. Сначал мной был проверен алгоритм декомпрессии, я размышлял так: если ошибка симметричная, то я не смогу разжать файл созданный и запароленный другим архиватором, однако, библиотека с этим прекрасно справилась. Стало очевидно, что проблема лишь в алгоритме шифрования, но как ее найти?

Для начала, я изучил код, коментарии к коду, и информацию об авторе. Я узнал, что непосредственно классы QuaZIP являются Qt-оберткой над библиотекой MiniZip написанной на чистом C. Быстро убедившись, что QuaZIP'овская обертка к ошибке не имеет никакого отношения, я стал изучать код MiniZip. На первый взгляд все прекрасно работало. И поэтому я решил снова обратиться с вопросом в Google, а также на сайт разработчика этой самой библиотеки. там я нашел два багрепорта без ответа с тем же вопросом, который возник у меня, датированных 2007 годом.

Удручающе.

Чтоже, я принялся за изучение проблемы. Первый файл на который пало подозрение, конечно был crypt.h файл, в котором реализован сам алгоритм шифрования.
Я узнал, что этот файл является слегка адаптированным и переписанным под лицензию BSD файлом crypt.h из пакета программ Info-ZIP (это тот самый zip/unzip которые есть у любого линуксоида в системе), визуально сравнение файлов не показало существеных различий и ошибок.

Тогда я взял в руки описание ZIP формата на сайте PKWARE, любимую Okteta и, создав архив с помощью библиотеки, а также аналогичный с помощью стандартного ZIP, стал их сравнивать.
Картинка оказалось очень интересной, вот она:


Шифрованный ZIP файл имеет достаточно простую структуру:
Весь файл делится на две секции, cекцию с непосредственно содержимым и секцию называемую Central header.
Central header cодержит в себе информацию о файлах, их размере, дате создания, файловых атрибутах и т. д., это необходимо для того, чтобы программа-архиватор могла быстро прочитать информацию о содержимом и отобразить пользователю, не разбирая при этом весь файл архива.
Секция файлов строится таким образом:
[local file header 1]
[12 bytes encrypt header 1]
[file data 1]
[data descriptor 1]
.
.
.
[local file header n]
[12 bytes encrypt header n]
[file data n]
[data descriptor n]


Интересно посмотреть на local file header:
local file header signature 4 bytes (0x04034b50)
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
file name length 2 bytes
extra field length 2 bytes
file name (variable size)
extra field (variable size)

Следует также отметить что любой заголовочный блок начинается с 4-х «магических» байт «PK..», где последние два байта меняются в зависимости от того, какой именно блок начался.

Итак, как видно по скриншоту, расхождение с оригиналом всего одно, и чтобы добиться полного соответствия, я исправил флаговый бит.

И пошел смотреть дальше, естественно 12-байтный криптозаголовок всегда будет разным, потому что при его генерации используется генератор случайных чисел. Содержимое файла совпало по длине — это хороший знак, однако в файле сгенерированном библиотекой напрочь отсутствовал data descriptor. Его я и решил добавить. Как выяснилось пожже это было совершенно не обязательно.

В результате файлы стали абсолютно идентичны по всему, что касается полей заголовков и прочей вспомогательной информации.

Чтобы проверить алгоритм, я решил убрать рандом и заменить его статическим значением в библиотеке и в наборе программ Info-Zip, скомпилировать обе программы и создать ими архивы, пускай эти архивы будут гарантированно не рабочими, но они должны выглядеть идентично если алгоритмы совпадают.

Заменив строку
 c = (rand() >> 7) & 0xff;

в файле crypt.h и в той и в другой программе, я получил два новых архива созданных аналогичным способом и стал исследовать их в Okteta



Видно что файлы опять получились абсолютно разными, но у них появилась новая общая часть — первые 10 байт за локальным заголовком. Т.е. 10 из 12 байт криптозаголовка совпали, а два нет.
Смотрим содержимое crypt.h и видим, что последние два байта генерируются особым образом с помощью некоего ключа crcForCrypting.

    buf[n++] = zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t);
    buf[n++] = zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t);

OK, подставив в обе программы жестко заданный crcForCrypting, я увидел что файлы совпали(!), то есть совсем совпали, как будто это один и тот же файл.
Почувствовав, что истина где-то рядом, я решил изучить каким же образом получается значение переменной crcForCrypting? И выяснил, что в Info-ZIP эта переменная получается сдвигом влево на 16 бит времени создания файла, в то время как в MiniZIP в этой переменной всегда оказывался ноль.

Это оказалось решением проблемы. Добавив необходимый код для заполнения этой переменной и, не забыв вернуть рандом на место, я снова сгенерировал архив с помощью библиотеки, и он успешно распаковался в ark.

В заключение скажу, что списывался с автором QuaZIP в процессе копания, он, кстати, является нашим соотечественником, но помочь мне, к сожалению, ничем не смог. Однако по завершению моих исследований, он принял патч и обещал выпустить новую версию QuaZIP в ближайшем будущем.
На время пока обновление QuaZIP отсутствует, прикладываю к статье diff c патчем на текущую версию.
Теги:
Хабы:
+120
Комментарии34

Публикации