Загадка о файле-невидимке

    image

    Не так давно, закончив работу над очередной статьёй для Хабра, я решил оптравить её на ревью своему знакомому. Сохранив HTML-страницу со всем окружением (картинки, стили etc), я запаковал её в ZIP-архив и отправил адресату. Уже через пять минут я получил фидбек, который, вопреки моим ожиданиям, был связан вовсе не с самой статьёй, а с тем, что архив был абсолютно пустой. Почесав голову и решив, что я затупил с архивированием, я повторил процедуру, убедившись, что выделил все необходимые для запаковывания файлы. Спустя несколько минут знакомый снова разразился удивлённым «Ты что, шутишь?», в то время как я совсем не шутил.

    Я начал собирать воедино все элементы паззла. Во-первых, я выяснил, чем он пытается открыть архив. Вдруг, в качестве viewer'а он использует какую-нибудь третьесортную фигню не пойми от какого разработчика? Однако им оказался дефолтный explorer.exe. Я же пользовался Total Commander'ом как для запаковки, так и для просмотра получившегося архива, и в моём случае он вовсе не был пустым:

    image

    Что, неужели это сборочка xxxWindowsUltimateEditionxxx подкачала? Я попытался открыть тот же самый архив на моём компьютере при помощи explorer.exe и наконец поверил своему знакомому — архив действительно выглядел пустым:

    image

    Кто же виноват в таком поведении? Давайте разберёмся.

    Как протекал процесс, и что из этого вышло, читайте под катом (осторожно, много скриншотов). Перед прочтением данной статьи также настоятельно рекомендую ознакомиться с предыдущими.

    Экспериментальным путём я установил, что проблема воспроизводится как минимум при наличии символа '«' в названии архивируемого файла (например, "«some_file.txt"). Далее я выяснил, что при использовании 7-Zip в качестве архиватора содержимое получившегося архива спокойно отображается в explorer.exe. Проверка «проблемного» архива на ошибки встроенными средствами 7-Zip также ничего не выявила:

    image

    Кстати, Вы обратили внимание, что вместо символов '«' и '»' в оригинальном архиве Total Commander показывает underscore character ('_')? 7-Zip File Manager, в свою очередь, заменил '«' на символ '<':

    image

    Да что же такое? Давайте не будем гадать и посмотрим, чем между собой различаются ZIP-архивы в случае, когда один был запакован встроенным архиватором Total Commander'а, а второй — 7-Zip'ом.

    Для начала создаём минимальный пример, воспроизводящий проблемную ситуацию — я остановился на пустом файле "«some_file.txt" (конечный архив будет называться "«some_file.zip"). Далее архивируем его обоими способами без сжатия и берём в руки XVI32, в котором открываем оба получившихся архива:

    Проблемный архив
    image

    Нормальный архив
    image

    Уже невооружённым взглядом заметно, что их содержимое отличается друг от друга. Впрочем, это не должно пока что вызывать никаких конкретных эмоций, ведь архиваторы вполне могут писать информацию о себе любимых в какие-нибудь «дополнительные» поля. Чтобы убедиться, в чём именно заключается разница между этими файлами, давайте заглянем в спецификацию и на стороннее описание формата ZIP и «разобьём» наши байты на составные части:

    Проблемный архив
    Local file header
    
    50 4B 03 04 -- signature
    14 00 -- PKZip version needed to extract
    02 00 -- General purpose bit flag
    00 00 -- Compression
    83 55 -- Mod. time
    DC 46 -- Mod. date
    00 00 00 00 -- CRC-32 checksum
    00 00 00 00 -- Compressed size
    00 00 00 00 -- Uncompressed size
    0E 00 -- File name len
    00 00 -- Extra field len
    3C 73 6F 6D 65 5F 66 69 6C 65 2E 74 78 74 -- File name ("<some_file.txt")
    
    Central directory file header
    
    50 4B 01 02 -- Signature
    14 00 -- Version
    14 00 -- PRZip version needed to extract
    02 00 -- Flags
    00 00 -- Compression
    83 55 -- Mod. time
    DC 46 -- Mod. date
    00 00 00 00 -- CRC-32 checksum
    00 00 00 00 -- Compressed size
    00 00 00 00 -- Uncompressed size
    0E 00 -- File name len
    00 00 -- Extra field len
    00 00 -- File comm. len
    00 00 -- Number of the disk on which this file exists
    00 00 -- Internal attr.
    20 00 00 00 -- External attr.
    00 00 00 00 -- Offset of local header
    3C 73 6F 6D 65 5F 66 69 6C 65 2E 74 78 74 -- File name ("<some_file.txt")
    
    End of central directory record
    
    50 4B 05 06 -- Signature
    00 00 -- Number of this disk
    00 00 -- Number of the disk on which the central directory starts
    01 00 -- Number of central directory entries on this disk
    01 00 -- Total number of entries in the central directory
    3C 00 00 00 -- Central directory size
    2C 00 00 00 -- Offset of cd wrt to starting disk
    00 00 -- Comment len
    


    Нормальный архив
    Local file header
    
    50 4B 03 04 -- signature
    0A 00 -- PKZip version needed to extract
    00 08 -- General purpose bit flag
    00 00 -- Compression
    84 55 -- Mod. time
    DC 46 -- Mod. date
    00 00 00 00 -- CRC-32 checksum
    00 00 00 00 -- Compressed size
    00 00 00 00 -- Uncompressed size
    0F 00 -- File name len
    00 00 -- Extra field len
    C2 AB 73 6F 6D 65 5F 66 69 6C 65 2E 74 78 74 -- File name ("B«some_file.txt")
    
    Central directory file header
    
    50 4B 01 02 -- Signature
    3F 00 -- Version
    0A 00 -- PRZip version needed to extract
    00 08 -- Flags
    00 00 -- Compression
    84 55 -- Mod. time
    DC 46 -- Mod. date
    00 00 00 00 -- CRC-32 checksum
    00 00 00 00 -- Compressed size
    00 00 00 00 -- Uncompressed size
    0F 00 -- File name len
    24 00 -- Extra field len
    00 00 -- File comm. len
    00 00 -- Number of the disk on which this file exists
    00 00 -- Internal attr.
    20 00 00 00 -- External attr.
    00 00 00 00 -- Offset of local header
    C2 AB 73 6F 6D 65 5F 66 69 6C 65 2E 74 78 74 -- File name ("B«some_file.txt")
    0A 00 20 00 00 00 00 00 01 00 18 00 F0 88 3F D4 6D B1 D0 01 F0 88 3F D4 6D B1 D0 01 F0 88 3F D4 6D B1 D0 01 -- Extra field
    
    End of central directory record
    
    50 4B 05 06 -- Signature
    00 00 -- Number of this disk
    00 00 -- Number of the disk on which the central directory starts
    01 00 -- Number of central directory entries on this disk
    01 00 -- Total number of entries in the central directory
    61 00 00 00 -- Central directory size
    2D 00 00 00 -- Offset of cd wrt to starting disk
    00 00 -- Comment len
    


    Как видите, в случае проблемного архива символ '«' действительно по какой-то причине «превратился» в '<' (0x3C), в то время как в нормальном архиве он продолжает оставаться самим собой (0xC2 0xAB — именно так он представлен в UTF-8).

    А что будет, если мы просто заменим '<' в проблемном архиве на '«', разумеется, попутно изменив значения остальных байт, на которые мы могли повлиять таким образом? Заменяем 0x3C на 0xC2 0xAB (обратите внимание, что сделать это необходимо сразу в двух местах), 0x0E 0x00 (File name len) на 0x0F 0x00 (это также необходимо сделать в двух местах), 0x3C 0x00 0x00 0x00 (Central directory size) на 0x3D 0x00 0x00 0x00 (т.к. мы предыдущими нашими действиями увеличили размер Central directory) и 0x2C 0x00 0x00 0x00 (Offset of cd wrt to strating disk) на 0x2D 0x00 0x00 0x00. В результате должно получиться следующее:

    image

    Открываем получившийся архив в explorer.exe и видим:

    image

    Да, файл теперь виден, но с его названием явно что-то не так. Рыскаем по спецификации в поисках слова «unicode» и встречаем следующее:

    APPENDIX D — Language Encoding (EFS)

    D.1 The ZIP format has historically supported only the original IBM PC character
    encoding set, commonly referred to as IBM Code Page 437. This limits storing
    file name characters to only those within the original MS-DOS range of values
    and does not properly support file names in other character encodings, or
    languages. To address this limitation, this specification will support the
    following change.

    D.2 If general purpose bit 11 is unset, the file name and comment should conform
    to the original ZIP character encoding. If general purpose bit 11 is set, the
    filename and comment must support The Unicode Standard, Version 4.1.0 or
    greater using the character encoding form defined by the UTF-8 storage
    specification. The Unicode Standard is published by the The Unicode
    Consortium (www.unicode.org). UTF-8 encoded data stored within ZIP files
    is expected to not include a byte order mark (BOM)

    Посмотрим, выставлен ли 11 бит поля флагов в наших случаях:

    Проблемный архив
    image

    Нормальный архив
    image

    Давайте выставим этот флаг в случае проблемного файла (Tools -> Bit manipulation)

    image

    и попробуем ещё раз открыть наш архив в explorer.exe:

    image

    Точно, мы забыли, что поля флагов на самом деле два:

    image

    Выставляем необходимый бит и в этом поле и снова открываем наш архив:

    image

    Здорово! Вот только почему Total Commander «превращает» символ '«' в '<'? Для того, чтобы понять это, возьмём в руки OllyDbg и запустим в нём исследуемый нами файловый менеджер. Хотя, подождите, давайте проверим, включена ли технология ASLR для totalcmd.exe. Загружаем его в PE Tools при помощи Alt-1, нажимаем на кнопку «Optional Header» и видим, что база меняться не будет (для более подробного описания данного процесса рекомендую посмотреть предыдущую статью):

    image

    Очевидно, что для создания архива TC в начале должен воспользоваться WinAPI-функцией CreateFile, так что поставим бряки на её вызовах (вероятнее всего, в нашем случае он должен использовать юникодовую версию данной функции):

    image

    Убираем бряки, которые срабатывают на каждое ненужное нам действие (например, на событие получения окном TC фокуса — по адресу 0x00567264):

    image

    Нажимаем Alt-F5 (комбинация клавиш для архивирования файлов в Total Commander'е), жмём на «OK» и оказываемся тут:

    image

    Давайте попробуем понять, выполнил ли уже TC преобразование символа '«' в '<'. Для этого откроем окно «Memory» при помощи Alt-M -> left-click по первой строке -> Ctrl-B -> вводим "<some_file.txt" в поле «ASCII»:

    image

    Нажимаем на кнопку «OK» и видим, что уже на данный момент приложение осуществило своё преобразование:

    image

    Нажимаем Alt-K, чтобы посмотреть на Call Stack:

    image

    Ставим бряки на начало каждой из процедур в списке, нажимаем F9, удаляем получившийся архив и убеждаемся, что в памяти процесса больше нет строки "<some_file.txt". После этого снова запускаем процесс архивирования и останавливаемся в начале первой процедуры из показанного ранее Call Stack'а:

    image

    Снова ищем ту же самую строку во всей памяти процесса и… Находим её:

    image

    Что ж, последний логичный на данный момент вариант — это поставить бряк на начало последней процедуры в Call Stack'е, из которой, собственно, нас сюда и позвали:

    image

    Прыгаем на вызов (right-click по строке с адресом текущей процедуры в окне Call Stack'а -> Show Call), бежим вверх до подсказанного OllyDbg начала процедуры и ставим на него бряк:

    image

    Проделываем те же действия, что и раньше (усиленно нажимаем F9, удаляем архив, проверяем память процесса на отсутствие строки "<some_file.txt", нажимаем Alt-F5 и кнопку «OK») и останавливаемся на только что поставленном бряке. Ищем ту же строку и… Снова находим её:

    image

    Учитывая, что Call Stack на текущий момент пустой, можно предположить, что мы выполняемся в отличном от main thread'а потоке или же попали сюда в результате условного или безусловного перехода. Нажимаем Ctrl-R и видим:

    image

    Прыгаем на единственную ссылку при помощи нажатия клавиши Enter:

    image

    Смотрим, кто в свою очередь ссылается на эту строчку:

    image

    Прыгаем туда:

    image

    Заходим внутрь нескольких процедур и видим вызов WinAPI-функции CreateThread:

    image

    В принципе, убедиться в этом можно было бы и другим способом — для этого достаточно посмотреть на заголовок окна CPU, который в моём случае сообщал, что ID потока равен 0x000013B8:

    image

    При этом в окне «Log», открываемом по нажатию Alt-L, видно, что ID main thread'а равен 0x00001E30:

    image

    Нажимаем Alt-F5, ставим бряки на вызовах CreateThread

    image

    , нажимаем на кнопку «OK» и останавливаемся на уже знакомом нам месте:

    image

    Смотрим на Call Stack и методом «двоичного поиска» (делим кол-во входных параметров пополам и смотрим на результат) раскручиваем цепочку вызовов различных процедур до состояния, когда становится известно, после вызова какой именно из них в памяти процесса появляется строка "<some_file.txt" — ею является процедура, находящаяся по адресу 0x00491780. Посмотрев внимательно на то, что происходит внутри неё, мы можем обнаружить вызов WinAPI-функции CharToOem:

    image

    Согласно официальной документации, данная функция переводит переданную строку в OEM-defined character set, причём, если используется ANSI-версия, мы можем выполнить т.н. «in place translation» (src и dest могут указывать на один и тот же адрес, что избавит от необходимости создавать отдельный буфер для конечной строки), что и происходит в случае TC:

    lpszDst [out]
    Type: LPSTR
    The destination buffer, which receives the translated string. If the CharToOem function is being used as an ANSI function, the string can be translated in place by setting the lpszDst parameter to the same address as the lpszSrc parameter. This cannot be done if CharToOem is being used as a wide-character function

    Да, после её вызова переданный ей буфер уже действительно содержит символ '<' вместо '«':

    image

    Что, будем патчить? А давайте сначала посмотрим на опции архивирования в TC:

    image

    По дефолту опция «Pack Unicode names» установлена в «Ask every time a Unicode name is encountered». Следовательно, TC не посчитал, что встреченное имя является юникодовым. А если попробовать заархивировать файл, например, с китайскими иероглифами?

    image

    Как видите, в таком случае TC отображает окно с сообщением о встреченном имени файла, который содержит символы, отличные от используемой кодовой страницы. В случае с символом '«' такого сообщения не было, вероятнее всего, потому, что многие code page'и (в моём случае, видимо, CP1251) содержат данный символ в отведённых им дополнительных «ячейках». А вот если выставить данную опцию в «All as UTF-8 if at least one contains characters>127», то мы увидим, что наш файл "«some_file.txt" корректно запаковывается и впоследствии отображается в explorer.exe.

    Вы можете спросить «Почему, даже если имя файла сменилось с „«some_file.txt“ на „<some_file.txt“, explorer.exe не смог отобразить его хотя бы с изменившимся именем»? Дело в том, что '<' является одним из символов, которые запрещены для использования в названиях директорий и файлов в случае NTFS:

    The following reserved characters:

    < (less than)
    > (greater than)
    : (colon)
    " (double quote)
    / (forward slash)
    \ (backslash)
    | (vertical bar or pipe)
    ? (question mark)
    * (asterisk)

    Более того, разархивировать такой архив встроенными средствами Windows также не получится. Во-первых, из-за символа '«' в названии самого архива:

    image

    Во-вторых, из-за его содержимого:

    image

    Послесловие


    В абсолютно любом продукте есть баги / фичи (называйте, как хотите), которые могут вылезти в самых неожиданных местах, и чем сложнее программный комплекс, тем, как правило, больше багов в нём можно обнаружить. Не ленитесь изучать причины возникшнего у Вас поведения, ведь вполне возможно, что в процессе исследования приложения Вы узнаете для себя что-нибудь новое.

    Спасибо за внимание, и снова надеюсь, что статья оказалась кому-нибудь полезной.
    Share post

    Comments 30

      +5
      «А вот если выставить данную опцию в «All as UTF-8 if at least one contains characters>127», то мы увидим, что наш файл „«some_file.txt“ корректно запаковывается и впоследствии отображается в explorer.exe.» — спасибо за подсказку, постивил и у себя.

      Интересный детектив получился!
        +18
        Лучше, чем Донцова, однозначно! :-)
          +8
          Дело в том, что '<' является одним из символов, которые запрещены для использования в названиях директорий и файлов в случае NTFS
          Только для подсистемы Win32. Самой файловой системы NTFS это ограничние не касается.
          dubeyko.com/development/FileSystems/NTFS/ntfsdoc.pdf
          web.archive.org/20060213202831/data.linux-ntfs.org/ntfsdoc.html.gz
            0
            Означает ли это, что, имея на руках драйвер ntfs-3g и раздел NTFS, я могу создать в GNU/Linux файлы, которые на Windows никто не сможет не то, что прочесть, но даже увидеть?

              0
              Я записывал на ntfs-диск такие файлы из-под убунты.
              Windows их видит, но скопировать не даёт.
                0
                да. все так и есть. в NTFS только "/" нельзя использовать.
                Я на флешке под линуксом создавал папку "\\:*** или вроде того (точно не помню). проводник ее даже в списке показывал, но ест-но не мог ни зайти, ни удалить эту папку.
                С командной строкой тоже самое.
                  0
                  Помимо / ещё нельзя использовать нулевой байт. Эти два символа — единственные ограничения в POSIX, засунуть прочие управляющие символы вполне можно (и я как‐то случайно забивал NL (новую строку)).
                  0
                  А еще в можно создать папки «CON», «NUL» и прочие, с «запрещенными» именами. Винда их видит, даже открывает содержимое, но скопировать не позволяет.
                0
                выложили бы архивы до кучи
                  +4
                  Это вы, видимо, ещё ":" в имя файла не пихали. Я эти особенности ФС в виндах давно использую.
                    0
                    Что-то у вас на скринах потерялся и последний символ расширения (TXT → TX)
                    image
                    Не только Total Commander делает такие «битые» архивы, но и, собственно, WinRAR 5.
                      +1
                      Точно, спасибо. В тексте написал, а на скриншотах забыл заменить 0x0E 0x00 (File name len) на 0x0F 0x00. Исправил
                      0
                      А ещё у вас в листинге расшифровки байтов нормального архива в комментарии к имени файла указана последовательность символов << (два символа «меньше») вместо кавычки «. Из-за этого я сначала не сразу догнал, куда же делся второй символ «меньше» в дальнейших объяснениях.
                      +5
                      Ничего себе, задротство: О
                      Я бы переименовал как-нибудь файлы, запаковал по-новой, да и всё.

                      Тем не менее, было очень интересно, спасибо!
                        0
                        Уже даже по наличию "<" в имени файла в проблемном архиве можно было догадаться о причине поведения Explorer'а и не заниматься расследованием очевидного.
                          +1
                          Как видно из статьи, очевидное было далеко не очевидным и решение все-таки нашлось, также не самое очевидное.
                        • UFO just landed and posted this here
                            0
                            Что используете вместо TotalCommander'a (если Вы работаете в Windows), если не секрет?
                            Я для себя не смог найти более удобного файлового менеджера, может быть плохо искал.
                            • UFO just landed and posted this here
                                0
                                На первый взгляд тот же тотал, только бесплатный. Спасибо, альтернативы это всегда хорошо.
                                  +7
                                  FAR?
                                0
                                UnrealCommander посмотрите
                                0
                                Странные вещи вы говорите, господин. То, что эта программа использовалась в прошлом веке, вовсе не значит, что она устарела. Последнее обновление тотала было меньше года (вроде бы) назад. И даже тем же mc люди и по сей день активно пользуются.

                                Если вас больше удивляет только ВинРар, то что же не удивляет? 7-Zip? Так он не намного младше Тотала, 97 г. выпуска.
                                • UFO just landed and posted this here
                                    0
                                    Вы, батенька, работаете на компьютере, который работает по принципам, «железным» и программным, разработанным в своей основе в семидесятых-восьмидесятых годах прошлого века. Все последовавшие десятилетия лишь улучшали и расширяли этот функционал.
                                    • UFO just landed and posted this here
                                0
                                Это еще что. У меня одна винда от запакованное другой распаковать не смогла. Пришлось искать машину с архиватором. Запаковывалось на Win 7 pro вроде, распаковывалось на Win 2003 server.
                                  0
                                  Надо проверить как антивирусы с таким архивом работают.
                                    0
                                    В принципе, это же ошибка, что встроенный архиватор подставляет недопустимые для винды символы. Напишите Кристиану (автору TC), в 9-й версии наверняка исправит.

                                    Only users with full accounts can post comments. Log in, please.