Пишем упаковщик по шагам. Шаг пятый. Ресурсы.

Предыдущий шаг здесь.

Пора усовершенствовать наш упаковщик. Он уже способен упаковывать и запускать самые простые бинарники, имеющие лишь таблицу импорта. Бинарники с экспортами, ресурсами, TLS, DLL с релокациями ему пока что не под силу. Нужно над этим работать. Для начала сделаем обработку второй по важности вещи после импортов - директории ресурсов.

Сначала добавим пару полей в структуру packed_file_info:

Эти поля будут хранить информацию об оригинальной директории ресурсов. В распаковщике мы их будем записывать в заголовок PE-файла после распаковки данных секций. Но это еще не все. Ведь мы упаковываем данные всех секций, слепляя их в один большой блок, в том числе и ресурсы. У файла пропадет иконка и информация о версии, если они имелись. Он может вообще не запуститься, если у него имелись какие-то специфические манифесты в ресурсах, определяющие права или библиотеки, требуемые файлу для запуска. Нам необходимо расположить главную иконку приложения, информацию о версии и манифест не упаковывая рядом со сжатыми данными в новой директории ресурсов, выставив на нее указатель в PE-заголовке.

Сначала займемся распаковщиком (проект unpacker), тем более, изменения будут минимальными. Добавим строки аналогично коду для директории импорта:

С распаковщиком все. Переходим к упаковщику (проект simple_pe_packer). Добавим, опять-таки, аналогичные для директории импорта строки для директории ресурсов:

В принципе, этого уже должно быть достаточно, чтобы файл с ресурсами (формами, например) мог запуститься. Проблемы возникнут, если у файла есть xp manifest или что-то подобное. Поэтому нам теперь необходимо пересобрать директорию ресурсов в упаковщике, как я описывал выше. В начало файла main.cpp добавим новый #include <pe_resource_manager.h>. Он необходим для доступа к вспомогательным классам для работы с ресурсами. Код будем добавлять в то место упаковщика, где удаляются секции исходного файла (прямо перед этим действием):

Итак, что же происходит в этом куске кода? Если вкратце, то мы получаем ресурсы оригинального файла и ищем в них:
1. Иконки
2. Манифест
3. Информацию о версии

Эти три вида ресурсов необходимо в неупакованном виде положить в новую директорию ресурсов. Иконку (точнее, группу иконок) из оригинального файла мы возьмем самую первую, именно ее использует Windows как основную иконку файла. Манифест всегда у файла один, и в нем сказано, какие права нужны файлу для запуска и какие DLL-файлы используются. Там может храниться и другая информация. Наконец, информация о версии - это непосредственно информация о файле, которую также можно просмотреть в проводнике Windows.

Я до этого нигде не описывал, как устроена директория ресурсов, поэтому для общего развития приведу эту информацию сейчас. Посмотрим на ресурсы какого-нибудь exe-файла в CFF Explorer'е:

Как видно, ресурсы организованы в виде дерева. В корневой директории сначала располагаются записи, указывающие на тип ресурса. В них вложены директории с записями, содержащими имя или ID ресурса, а в них, в свою очередь, вложены директории с записями, указывающими язык ресурса. Последние содержат директорию данных, которая указывает уже на данные ресурса.

Так вот, в коде выше мы перечисляем все иконки, имеющие ID или имя. Так как в PE-файлах все ресурсы сортируются, причем сначала идут именованные ресурсы, а потом только ресурсы с ID, исходя из этого, мы и ищем самую первую иконку. Она и будет иконкой приложения, и ее мы добавляем в новую директорию ресурсов, сохраняя ее имя/ID и язык. Аналогично поступаем с манифестом и информацией о версии - у всех файлов не больше одной такой записи, причем всегда неименованные, т.е. имеющие ID, поэтому берем и сохраняем первую запись манифеста и первую запись информации о версии.

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

Дело в том, что все пересборщики в моей библиотеке для работы с PE автоматически уберут все нулевые байты с конца секции, в которую пересобирают импорты/экспорты/ресурсы и т.д., при условии, что секция последняя в файле. Это совершенно адекватное поведение, позволяющее уменьшить размер файла в пределах файлового выравнивания, так как загрузчик все равно нулевые байты восстановит, но уже в памяти, в количестве [выровненный виртуальный размер секции] минус [физический размер секции]. Таким образом, если у нас какие-то структуры внутри секции заканчиваются нулевым элементом (например, завершающий дескриптор таблицы импортов), физически в файл он записан не будет, если эта таблица импортов собирается в последней секции PE-файла. С этим, кстати, связаны интересные баги в таких редакторах, как CFF Explorer, он опирается только на физические данные файла, а не на виртуальные. Поэтому он может криво отобразить ту же таблицу импорта с отрезанными нулевыми байтами в конце.

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

Идем дальше - пересобираем ресурсы:

Здесь все ясно - если какие-то ресурсы есть, мы их записываем в новый PE-файл. Располагаем мы их в самом конце добавленной секции, сразу за импортами. Так как секция последняя на момент пересборки ресурсов, она расширится автоматически.

Осталось убрать одну строчку, которая убирает директорию ресурсов:

Все готово! Можно проверить упаковщик на любом exe-файле, имеющем импорты, ресурсы и даже релокации (хотя мы их пока что не обрабатываем). Давайте возьмем какой-нибудь проект на MSVC++ с MFC и посмотрим с помощью CFF Explorer'а, какие ресурсы в нем были изначально:

После упаковки имеем:

Итак, осталась одна группа иконок с несколькими иконками в ней (иконки приложения), манифест (configuration files) и информация о версии. Разумеется, упакованный файл успешно запускается!

Полный солюшен для этого шага: Own PE Packer step 5

Пишем упаковщик по шагам. Шаг пятый. Ресурсы.: 10 комментариев

  1. мегареспект! При такой паковке ресурсов антивирусы (надеюсь) не будут срать кирпичами при виде пакованного твоим пакером файла. Самое интересное впереди, с нетерпением жду продолжения.
    С уважением!

  2. Можно в перспективе вкрячить что-нить в духе:

    {0x01
    ,0x90 // NOP
    ,0x90 // NOP
    ,0x00},

    {0x01
    ,0xF9 // STC
    ,0xF9 // STC
    ,0x00},

    {0x01
    ,0xF8 // CLC
    ,0xF8 // CLC
    ,0x00},

    {0x02
    ,0xC0FE // INC AL
    ,0xC8FE // DEC AL
    ,0x00},

    {0x02
    ,0x0004 // ADD AL, 0
    ,0x002C // SUB AL, 0
    ,0x02},

    {0x02
    ,0x002C // SUB AL, 0
    ,0x0004 // ADD AL, 0
    ,0x02},

    {0x02
    ,0xC102 // ADD AL, CL
    ,0xC12A // SUB AL, CL
    ,0x00},

    {0x02
    ,0xC12A // SUB AL, CL
    ,0xC102 // ADD AL, CL
    ,0x00},

    {0x02
    ,0x0034 // XOR AL, 0
    ,0x0034 // XOR AL, 0
    ,0x02},

    {0x03
    ,0x00C8C0 // ROR AL, 0
    ,0x00C0C0 // ROL AL, 0
    ,0x01},

    {0x03
    ,0x00C0C0 // ROL AL, 0
    ,0x00C8C0 // ROR AL, 0
    ,0x01},

    {0x03
    ,0xE801EB // Self modifing
    ,0xE801EB // Self modifing
    ,0x00},

    {0x03
    ,0xE901EB // Self modifing
    ,0xE901EB // Self modifing
    ,0x00},

    {0x03
    ,0xC201EB // Self modifing
    ,0xC201EB // Self modifing
    ,0x00}
    };

    Ну и потом:

    {
    command=*Iter;
    _Data=command.Data;
    switch(command.OpCode)
    {
    case 0xC0FE: // INC AL
    _asm
    {
    INC _Value
    }
    break;

    case 0x0004: // ADD AL, 0
    _asm
    {
    MOV CL,_Data
    ADD _Value,CL
    }
    break;

    case 0x002C: // SUB AL, 0
    _asm
    {
    MOV CL,_Data
    SUB _Value,CL
    }
    break;

    case 0xC102: // ADD AL, CL
    _asm
    {
    MOV CL,Count
    ADD _Value,CL
    }
    break;

    case 0xC12A: // SUB AL, CL
    _asm
    {
    MOV CL,Count
    SUB _Value,CL
    }
    break;

    case 0x0034: // XOR AL, 0
    _asm
    {
    MOV CL,_Data
    XOR _Value,CL
    }
    break;

    case 0x00C8C0: // ROR AL, 0
    _asm
    {
    MOV CL,_Data
    ROR _Value,CL
    }
    break;

    case 0x00C0C0: // ROL AL, 0
    _asm
    {
    MOV CL,_Data
    ROL _Value,CL
    }
    break;
    }
    }
    return(_Value);
    }

    Это бы вообще было бы потрясно, чтобы если что - самому подправить код на выходе.
    Как смотришь на это?

      1. Не, я в любом случае буду править, просто думал узнать твое мнение, и как бы ты это реализовал, а потом уже отталкиваясь от этого - плясать дальше.
        Кстати не про антиотладку сейчас думал а о движке трешкода, конечно можно хитрее реализовать, можно вообще сделать, чтобы он подцеплял фейковые сигны из пейдовского user.db.
        Просто мне интересно, как ты видишь готовую версию пакера. Вот и все.

        1. Пока никак. Впереди еще TLS, релокации, экспорты, еще мб что-то по мелочам, потом какой-никакой консольный интерфейс еще надо запилить с опциями пакования...

    1. На самом деле по моим наблюдениям он ставит ту из иконок, которая больше всего подходит по размеру. Зависит от настроек отображения папки ("маленькие значки", "большие значки", "таблица" и т.д.)

  3. А зачем нужно восстанавливать data directories в образе (процессе) после распаковки? Какие нибудь WinAPI функции после загрузки к ним обращаются, или просто для чистоты и полноты восстановленного образа? Всегда ли распаковщик будет работать без восстановленных data directories?

    1. Ну, например, стандартная библиотека visual c++ к ним обращалась, помню. По-моему, оттуда она доставала флаг, DLL образ или обычный исполняемый файл, чтобы поддержать соответствующую процедуру загрузки. Поэтому без восстановленных директорий образ, скорее всего, работать не будет.

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

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