Пишем собственный упаковщик. Шаг 12 — Работа над ошибками

pack

Благодаря одному товарищу из комментариев в предыдущих постах касательно упаковщика выявился один занятный баг в коде, который я поспешил исправить. Комментатор, не разобравшись в деталях работы пакера, утверждал, что код, запакованный им, окажется неспособным работать с SEH при условии, что DEP включен. При таких условиях код работал хорошо (так как весь распакованный код в памяти находится в пределах одной-единственной секции PE-файла, помеченной как исполняемая. UPX имеет такую же логику работы.). Но случайно была обнаружена следующая ошибка: если программа собрана в MSVC++, использует SEH и имеет релокации, при первом же возникшем исключении она упадет с некоторой долей вероятности (точнее, если файл загрузился не по своему базовому адресу). DEP тут, конечно же, не при чем. Все дело оказалось в злополучной директории IMAGE_LOAD_CONFIG_DIRECTORY. Она создается линкером Visual Studio. Из полезной информации она содержит таблицу адресов (RVA) SE-обработчиков и указатель на внутреннюю переменную crt __security_cookie. Как выяснилось, эта директория нужна не только для внутренностей CRT (хотя она, кажется, как раз хотела плевать на эту структуру), но и системному загрузчику (по крайней мере, в Win7. WinXP, похоже, тоже плевал на эту директорию). Упаковщик, который я описываю, перемещает эту директорию в другую секцию (см. здесь). Поэтому исправить ошибку можно, добавив в таблицу релокаций, создаваемую упаковщиком, несколько записей, которые будут править адреса, указывающие на security cookie и таблицу обработчиков SEH, чтобы он мог считать интересующую его информацию из этой директории на этапе загрузки.

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

Кстати, насчет библиотеки PE Bliss. Сейчас в свободное время я допиливаю новую версию, в которой появятся следующие дополнительные возможности (список примерный и может измениться):
- высокоуровневая работа с дополнительными типами ресурсов PE-файлов;
- детальный разбор .NET-бинарников (метаданные, сигнатуры, ресурсы);
- обертка над библиотекой на C++/CLI, которая позволит .NET-разработчикам удобно использовать функционал библиотеки в софте на C# или Visual Basic .NET.

Скачать исходники упаковщика: packer source
Скачать собранный вариант: packer binary

UPDATE 24.05.2016: доработки генерации таблиц релокации. В некоторых случаях (например, когда TLS-данных слишком много и присутствует директория load config, адреса в таблице релокации могли переполняться, и файл после упаковки не работал).

Пишем собственный упаковщик. Шаг 12 — Работа над ошибками: 56 комментариев

  1. Доброго времени суток.
    Подскажи пожалуйста при компиляции этого проэкта с новой библиотекой "PE Bliss" проблемы с бустом не может найти вод такую либу "libboost_program_options-vc100-mt-s-1_53.lib"
    Написано чтоб она подлинковалось нужно в проперти проэкта Configuration Properties/"C/C++"/Code Generation/Runtime Library/ стояло /MD
    Но эт вроде не есть хорошо

    1. Нужно, чтобы стояло /MT. Буст должен быть собран тоже статически. Ну или /MD и буст динамически, но тогда у exe-файла будут зависимости от DLL-файлов Visual Studio Runtime, поэтому я не использую такой вариант.

    2. Dx, доброго времени суток. Пишу диплом связанный с .net pe и хотел бы с вами проконсультироваться по этому поводу. Мне нужно разобраться во всех аспектах .net pe и я особо не понимаю многих вещей , которые мне говорит мой дипломный руководитель. Можно ли как то с вами связаться вне этого блога? Заранее спасибо

      1. Я не смогу оказать консультации по .NET-формату PE-файлов, потому что уже сам его забыл (да и не особо разбирался). Могу порекомендовать хорошую статью, которую сам когда-то читал: https://www.codeproject.com/Articles/12585/The-NET-File-Format
        Еще можно читануть исходники .NET, они же теперь в открытом доступе: https://github.com/dotnet
        Возможно, появились с того времени, как я смотрел этот формат, какие-то еще хорошие статьи по этому поводу, стоит поискать.

  2. Наконец то у меня нашлось время поиграться с этой тулзой ;)

    Скомпилил, значит, я динамическую библиотеку с "хитрыми секциями". Запустил. И...

    1. Как я и ожидал, результирующая библиотека работает как задумывалось. А вот "хитрые секции" не работают. :(

    "Хитрой секцией" в моем случае была только одна:
    ".sdata"
    VirtSize: 00008000h VirtAddr: 00009000h
    raw data offs: 00000000h raw data size: 00000000h
    relocation offs: 00000000h relocations: 00000000h
    line # offs: 00000000h line #'s: 00000000h
    characteristics: D0000080h
    UNINITIALIZED_DATA SHARED READ WRITE ALIGN_DEFAULT(16)

    2. Заглянул в процесс (с помощью процесс эксплорера) и посмотрел на мою либу - результат не важнецкий. Впрочем я и этого ожидал.

    Вместо ожидаемых 6 секций с "правильными" флагами протекции памяти:
    MyLib.dll: Image (Commit), 0x6b5b0000, 4 kB, R
    MyLib.dll: Image (Commit), 0x6b5b1000, 32 kB, RX
    MyLib.dll: Image (Commit), 0x6b5b9000, 32 kB, RW
    MyLib.dll: Image (Commit), 0x6b5c1000, 8 kB, R
    MyLib.dll: Image (Commit), 0x6b5c3000, 8 kB, RW
    MyLib.dll: Image (Commit), 0x6b5c5000, 8 kB, R

    получил небезопасного вида картину:
    MyLib.dll: Image (Commit), 0x6b5b0000, 4 kB, R
    MyLib.dll: Image (Commit), 0x6b5b1000, 92 kB, RWX

    Итого:
    1. Не "работают" расшаренные секции файла
    2. Исполняемый модуль стал небезопасным - т.е. подверженным удачным атакам эксплоитами

    Не думаю, что Вы сможете починить пункт 1 и не станете чинить пункт 2.

    1. И еще один момент - модуль в памяти "пожирнел" на 1 страницу.
      Это конечно не страшно, мне попадались "экземпляры", утяжеляющие на 0,5МБ ;)

    2. Да, починить первое вряд ли будет возможно. Интересно, кстати, как с shared-секциями работает UPX, портит ли он их. Второй пункт связан с первым, потому что упаковщик весь код собирает в одну секцию с атрибутами RWX, а потом просто восстанавливает таблицу секций. Как вариант, можно было паковать секции по отдельности, и тут на блоге где-то даже лежит старый вариант пакера, который так делает, но тогда результат по эффективности сжатия будет хуже, хотя атрибуты секций при этом сохранятся.

      1. Насчет 2 - Вам просто следовало бы сохранить еще и флаги защиты в блоках информации о секциях. А потом в лоадере их выставить с помощью VirtualProtect, разумеется после распаковки :)

      2. Давече глянул и "старый вариант пакера" - результат монопенисуальный: во все секции, которые он упаковал добавил флаг "W" (типа чтоб можно было распаковать). Хоть он и восстановил флаги в РЕ-заголовке, но с флагами протекции памяти ничего не сделал.

  3. Доброго времени суток!
    Подскажите, какие особенности формата PE64 необходимо учитывать при создании упаковщика?

    1. В целом отличий от PE32 нет, за исключением того, что виртуальные адреса (VA) занимают не 4 байта, а 8.
      Ну и еще PE64 по-другому работает SEH, там используются Exception tables, и это отдельная директория в PE-файле. С этим стоит разбираться отдельно, но не думаю, что это слишком сложно, потому что в MSDN есть полная документация на этот счет.

      1. @dx, Здраствуйте! Есть ли рабочая версия пакера PE64? Какие оссобенности в написании распаковщика для 64битных файлов?

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

  4. dx, подскажи плиз.

    Если я коментирую вот эту часть:
    //Пересоберем ресурсы, если есть, что пересобирать
    if(!new_root_dir.get_entry_list().empty())
    rebuild_resources(image, new_root_dir, added_section, added_section.get_raw_data().size());
    анпакер падает. апи в анпакере сам ищу, хендл kernel32 получаю из PEB.
    что не так делаю?

    1. Ну, если комментируешь, то нужно тогда выпилить таблицу импортов у упакованного файла, раз сам их ищешь.
      Добавь что-то вроде
      image.remove_directory(IMAGE_DIRECTORY_ENTRY_IMPORT);
      ближе к концу.

  5. Спасибо за интересные статьи, очень много нового узнал. Библиотека PE-Bliss тоже порадовала. Где кошельки для перечисления благодарности? =)

  6. В статьях написано , что либа допилена до стадии 1.9 . Но скачать можно только 1.0.0 от ноября 12 . Почему так ?

      1. Воистину 0.1.9 . Нолик я в начале как-то проигнорировал . Извиняйте за глупый конфуз . Вопрос можно удалить .

    1. Нет, потому что в этом нет смысла в целом, не удастся сохранить оверлей по тому же смещению, по которому он располагался до упаковки.

  7. Немного поковыряв код распаковщика столкнулся с тем, что при загрузке упакованного exe возникает ошибка доступа по адрессу TlsIndex (поле tls_index в packed_file_info) еще до запуска кода. В упаковщике код не трогал (атрибуты .rsrc RWE ).
    Есть мысли?

    1. Были такие проблемы на Win10 кажется. Когда упаковщик делал, не было еще возможности проверить на десятке. А сейчас уже лень разбираться, где баг.

      1. Проблема возникает на Win7. Стоит размеру кода стаба стать больше 4Кб. Пробовал вырубать tls - но что-то портит память секции распаковщика.

        1. Я пробовал в свое время разные бинари с tls, всё было нормально. Сложно сказать, в чем проблема. У меня такой баг не проявлялся, возможно, это зависит еще и от компилятора (каким компилятором, кстати, исходный файл собран?). Нужно убедиться, что tls index корректен, и адрес, по которому он указывает, существует в бинарнике после его загрузки.

  8. Упаковываемый файл собран компилятором среды Borland Delphi 7. Проект упаковщика собирался компилятором Visual Studio 2008.

    Ошибка возникает на этапе загрузки exe в память...

    http://rghost.ru/6WKfQd5sk

    1. В чем проблема найти ошибку, если это так принципиально? Ну либо воспользоваться каким-нибудь UPX'ом, если упаковщик неважен.

      1. Хочется найти программу с открытым кодом либо написать самому средство защиты программы с привязкой к железу и паролю пользователя. Первое не попадалось (может Вы встречали?), а защите с закрытыми исходниками я не доверяю. Второе интереснее, но опыта ноль.

        1. Это упаковщик, а не протектор. Снять его не составляет никакой проблемы. А для тех, для кого это может представлять проблему, они и, не знаю, добавленный в код защищаемого софта пароль на открытие не снимут.

      1. В упаковщик практически нет, только в части точки входа

        image.set_ep(image.rva_from_section_offset(unpacker_added_section, entry_point_offset));

        и выкинут boost.

        1. А распаковщик сильные изменения претерпел? Может быть, указаны/вычислены некорректные адреса для tls? Может быть, 2008-я студия генерирует код по-другому (я компилировал, вроде бы, всё это в 2010-й), поэтому какие-то оффсеты поменялись. Оригинальный упаковщик нормально пакует файл?

  9. В студии 2010 экспресс те же проблемы. Оригинальный упаковщик все сжимает нормально. Тут похоже беда не в коде как таковом. Достаточно в распаковщике закоментировать функцию извлечения серийника материнки и все начинает работать. Ошибки Все каким-то образом завязано на размер распаковщика.

    О полноценном протекторе речи нет - планируется только зашифровать код после сжатия.

    1. Я и говорил о том, что при изменении размера упаковщика меняются адреса всяких ресурсов и индексов. Там же они часточно были просчитаны вручную, надо их правильно пересчитывать. Они точно правильно вычислены?

    2. В бинарнике с rghost вижу как минимум то, что TLS Data некорректная, отличается от оригинального бинарника. Почему-то записались неправильные данные.

      Данные TLS пишутся вот здесь:

      //Дополняем секцию данными для инициализации
      //локальной памяти потока
      unpacker_added_section.get_raw_data() += tls->get_raw_data();

    3. Да, неплохо было бы получить модифицированный код упаковщика и распаковщика, так сложно сказать, в чем проблема.

        1. Посмотрел. Проблема в том, что таблица релокаций, которая строится упаковщиком, некорректная. Последние три элемента должны корректировать VA-адреса из структуры TLS (а именно, StartAddressOfRawData, EndAddressOfRawData и AddressOfIndex). Так как бинарник имеет релокации, загрузчик его самовольно перемещает, как захочет, а эти три поля остаются необработанными (релокации применяются черт знает к каким адресам). В итоге загрузчик пытается осуществить запись по VA-адресу AddressOfIndex, а он не пересчитан, отсюда и ошибка access violation - такого адреса не существует. Если убрать релокации, то всё начинает работать, что ожидаемо.

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

          Ну и да, напомню еще, что данные TLS (RawData) после упаковки борландовского бинарника оказываются кривыми, но такой косяк происходит и в оригинальном упаковщике. Посмотрю попозже, возможно, я просто не помню, как это должно работать.

          1. Посмотрел, с raw data всё нормально, борланд в приложенном бинарнике проставил сырым данным TLS длину 10, а размер секции .tls равен нулю. Получается, все 10 байтов - это нули. В упакованном бинарнике так и оказывается. Итого, нужно только понять, почему твой код некорректно рассчитывает адреса релокаций для TLS.

        2. Интересно стало, в чем проблема с релокациями, нашлась ошибка. Упаковщик создавал таблицу релокаций для адреса, куда загрузчик должен был прописать адрес точки входа, и для всех полей таблицы TLS. Если упаковщик оказывается слишком большого размера, то значения полей в таблице релокаций переполняются (т.к. от RVA каждой таблицы есть возможность отступить всего на 2047 байтов вперед - размер поля смещения в таблице всего 11 байтов). Решение: создать несколько таблиц релокаций для потенциально сильно удаленных друг от друга областей.

          Я это проделал, вот твой файл с изменениями: http://rghost.net/private/69FjLyGFp/be9750e494f044be2f0b30daf886d794
          Скоро обновлю оригинальный упаковщик в статье (так как в нем такая проблема тоже теоретически может возникнуть с директорией load config).

          Ну и еще: в твоем распаковщике empty_tls_callback_offset, похоже, некорректный. В оригинале он указывает на инструкцию ret 0xc, а в твоем на набор каких-то левых инструкций. Это уже исправляй сам.

        3. Кстати, хотел спросить, как ты отлаживаешь упаковщик, если он в конфигурации Debug у тебя даже не собирается? +)

  10. Спасибо! Я признаться увидел что смещение ограничено, и уже задумался как сдвинуть tls в начало секции распаковщика, что оказалось геморно в плане учета в коде распаковщика...

    Отлаживать в visual studio 2008 не приходилось, т.к. проблема возникла еще в visual studio 2010 express, но подумал что накосячил в настройках и решил пересобрать проект в более привычной visual studio 2008 и вычистить настройки проекта. Когда и здесь не заработало - понял что проблема глубже..

    за empty_tls_callback_offset спасибо, вчера только сдвинул инструкцию и просчитался...

    1. Рекомендую пользоваться CFF Explorer'ом, очень удобный софт для того, чтобы посмотреть, что получилось после упаковки. Достаточно приятные фичи - конвертация адресов с просмотром в hex-редакторе, что по ним лежит, ну и quick disassembler для дизассемблирования по конкретным адресам.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *