Пишем упаковщик PE-файлов по шагам. Шаг первый.

Раз уж я закончил разработку библиотеки на C++ для работы с PE-файлами, грех не использовать ее в каком-то более-менее серьезном проекте. Поэтому я разработаю с ее помощью упаковщик, поясняя по шагам, что я делаю, а либа на C++ сильно упростит нам жизнь. Итак, с чего же начать разработку упаковщика? Наверное, с выбора какого-нибудь несложного бесплатного алгоритма сжатия. После непродолжительных поисков таковой был мной найден: LZO. Он поддерживает множество различных видов сжатия (можно считать, разновидностей), и LZO1Z999 - самая эффективная по степени сжатия из всех доступных. Это, конечно, не ZIP, но приближается к нему по эффективности: 550-килобайтный файл был сжат zip'ом с максимальной степенью сжатия в 174 килобайта, в то время как LZO сжал тот же файл до 185 килобайтов. Однако у LZO гораздо более быстрый распаковщик. Он также оказался базонезависимым, то есть, его можно разместить по любому виртуальному адресу, и он будет работать без всяких корректировок адресов. Размер распаковщика приятно удивил: Visual Studio с оптимизациями по размеру и отключением исключений и проверок буферов дала результат в 613 байтов кода! Этот алгоритм нам подойдет.

Я начну написание с самых простых упаковщика и распаковщика, постепенно усложняя их. Для начала просто напишем программку, которая загружает PE-файл с помощью моей библиотеки. Упаковщик будем делать для x86-файлов, т.е. за PE+ пока что браться не будем. Итак, сначала вам необходимо будет скачать и скомпилировать в Visual Studio 2008 или 2010 мою библиотеку для работы с Portable Executable. После того, как вы это сделаете, следует создать новый проект. Я назвал его simple_pe_packer и положил в ту же папку, где лежит библиотека:

Настройки компиляции проекта должны совпадать с настройками компиляции библиотеки, иначе не слинкуется:

Для Debug-конфигурации, соответственно, выставим Multi-threaded Debug (/MTd). Теперь необходимо добавить в солюшен проект библиотеки LZO, чтобы было, чем упаковывать данные. Я скачал с сайта автора библиотеку lzo-2.06, распаковал ее в папку с таким же именем в каталоге с моей библиотекой для работы с PE (см. самый первый скриншот), после чего добавил в солюшен simple_pe_packer проект lzo-2.06, добавив в него все *.c и *.h-файлы из каталога lzo-2.06. Не забываем снова выставить настройки компиляции, как на втором скриншоте. Установим у проекта simple_pe_packer зависимость от проекта lzo-2.06 (правой кнопкой мыши - Project Dependencies, если вы используете английскую студию, конечно). Далее, чтобы lzo-2.06 собралось, необходимо добавить include-директорию:

Эту директорию добавляем и в Release, и в Debug-конфигурацию, разумеется. Теперь вернемся к проекту simple_pe_packer. Здесь мы тоже добавим include-директорию:

Она указывает на место, где лежат заголовочные файлы моей библиотеки для работы с PE-файлами. Если вы разложили всё так же, как и я, то у вас пути совпадут с моими. Если нет, то смотрите, как всё лежит у вас.

Теперь мы полностью переходим к проекту simple_pe_packer. Добавляем к файлам исходных кодов новый файл main.cpp, в котором будет код нашего упаковщика. Для начала его код будет таким:

Это программа, которая совершенно ничего не делает. Однако, следует ее скомпилировать, чтобы убедиться, что все пути настроены верно. Если компиляция прошла успешно, идем дальше. Далее я буду приводить только обновляемый код функции main или его части. Сделаем еще одно небольшое действие - откроем x86 PE-файл:

Осталось скомпилировать этот код и запустить его для проверки. Запустим полученный exe-файл будущего упаковщика в консоли, передав ему его же имя для теста:

Как видим, переданный PE-файл успешно открылся и считался. В следующем уроке мы перейдем непосредственно к простейшей упаковке и напишем на MASM32 (или на Си, я еще не решил) стаб распаковщика. А сейчас двинемся дальше. В начале статьи я написал, что алгоритм распаковки LZO1Z999 базонезависим и занимает всего 613 байтов. Как же получить бинарный вариант алгоритма? Давайте создадим новую конфигурацию проектов и назовем ее ReleaseDecompressor - она будет предназначена исключительно для того, чтобы собрать процедуру распаковщика. Делается это через меню Configuration Manager, в левом меню выбираем New..., вводим имя, выбираем Copy settings from: Release и ставим галку Create new project configurations:

Далее переходим к свойствам проекта lzo-2.06. Во вкладке Configuration Properties - General меняем тип исполняемого файла (Configuration type) на Application (.exe). Далее выделяем все .c-файлы в проекте, кроме lzo1z_d1.c (именно в нем содержится реализация нужного нам распаковщика), и заходим в их свойства. Исключаем их из сборки:

Должна получиться такая картина:

Теперь заходим в настройки файла lzo1z_d1.c - того, который мы оставили в сборке. На вкладке C/C++ - Optimization выбираем оптимизацию по размеру (Optimization - Minimize size (/O1)), далее выбираем Favor Size Or Speed - Favor Small code (/Os). Вернемся теперь снова к настройками проекта lzo-2.06, перейдем на вкладку C/C++ - Code Generation. Отключим C++-исключения (Enable C++ Exceptions - No), отключим проверку буферов (Buffer Security Check - No (/GS-)). Далее, на вкладке Linker - Manifest File отключим генерацию манифеста (Generate Manifest - No (/MANIFEST:NO)). На вкладке Linker - Debugging отключим генерирование отладочной информации (Generate Debug Info - No). На вкладке Linker - System можно выставить подсистему Windows (SubSystem - Windows), но это не играет особой роли. На вкладке Linker - Advanced ставим точку входа (Entry Point - lzo1z_decompress), чтобы никакие CRT к результирующему бинарнику не подключались. На этом все, теперь можно собрать проект lzo-2.06. В результате получим маленький (размером 1.5 кб) exe-файл. Открыв его в каком-нибудь PE-просмотрщике, например, в CFF Explorer'е, увидим, что у него нет ни одной директории, что не может не радовать. Нет импортов, нет релокаций (хотя мы их не отключали) - алгоритм полностью базонезависим! Можно увидеть, что виртуальный размер единственной секции с кодом у файла - 0x265 (или 613 байтов):

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

На этом все, до следующего шага!

Для желающих выкладываю готовый проект со всеми установленными настройками и необходимыми файлами (однако, библиотеку для работы с PE вам придется скачать и собрать самостоятельно, расположив проект библиотеки так, как описано в начале статьи): own-packer-step1

Пишем упаковщик PE-файлов по шагам. Шаг первый.: 43 комментария

  1. Спасибо, вот это действительно интересно, т.к. в перспективе можно развивать и развивать, конечно потрясно было бы видеть степень сжатия, как у Upack, но пока и так нормально. Можно будет прикрутить пару антиотладочных фич, интересный ГУИ и поддержку командной строки.
    Еще раз спасибо!

  2. Кстати да, DX, может поднимешь вопрос о повышении степени сжатия, или добавлять несколько алгоритмов сжатия (aPlib, LZMA,Zlib) и опцию --brute, чтобы автоматом прогонял по алгоритмам и использовал уже наилучший результат.
    По поводу антиотладки - можно добавить пару простых хитростей, к примеру:
    invoke CloseHandle,011h
    invoke CloseHandle,012h
    invoke CloseHandle,018h
    invoke CloseHandle,019h
    invoke CloseHandle,01Ah
    Вызовет исключение при закрытии невалидного хендла из ринг0, если отладчик не читает обращения к кривым хендлам.
    Или Апоксовский АнтиОлли:
    .386
    .model flat, stdcall
    option casemap :none ; case sensitive

    include \masm32\include\windows.inc
    include \masm32\include\user32.inc
    include \masm32\include\kernel32.inc

    includelib \masm32\lib\user32.lib
    includelib \masm32\lib\kernel32.lib

    .data
    Alloc dd ?
    OLDProtect dd 02040001h
    msgTitle db "Execution status:",0h
    msgText1 db "No debugger detected!",0h
    msgText2 db "Debugger detected!",0h
    .code

    start:

    ; MASM32 antiOlly example
    ; coded by ap0x
    ; Reversing Labs: ap0x.headcoders.net

    ; The idea is simple. OllyDBG interprets PAGE_GUARD as a
    ; Memory break-point. If we set SEH and execute PAGE_GUARDed
    ; code exception will occure. If debugger is present it
    ; will execute MemBpx and continue executing code after it.
    ; If debugger is not present handleing will be forwarded to SEH.

    ; Setup SEH

    ASSUME FS:NOTHING
    PUSH offset @Check
    PUSH FS:[0]
    MOV FS:[0],ESP

    ; Allocate new space

    PUSH PAGE_READWRITE
    PUSH MEM_COMMIT
    PUSH 10000h
    PUSH 0
    CALL VirtualAlloc

    ; Write RET there
    MOV BYTE PTR[EAX],0C3h
    MOV DWORD PTR[Alloc],EAX

    ; Place Memory break-point
    PUSH offset OLDProtect
    PUSH PAGE_EXECUTE_READ OR PAGE_GUARD
    PUSH 00000010h
    PUSH EAX
    CALL VirtualProtect

    ; Execute
    CALL [Alloc]

    PUSH 30h
    PUSH offset msgTitle
    PUSH offset msgText2
    PUSH 0
    CALL MessageBox

    PUSH 0
    CALL ExitProcess

    ; SEH handler
    @Check:
    POP FS:[0]
    ADD ESP,4

    PUSH 40h
    PUSH offset msgTitle
    PUSH offset msgText1
    PUSH 0
    CALL MessageBox

    PUSH 0
    CALL ExitProcess

    end start

    1. Я сейчас не ставлю цель сделать идеальный алгоритм сжатия, я нашел достаточно хороший, который, по заверениям автора, работает в разы быстрее существующих аналогичных. А антиотладку, может, и добавлю потом какую-нибудь.

      1. Согласен с тобой, на счет скорости и размера загрузчика - тут не поспоришь.
        Это я так - на долгосрочные перспективы.
        Было бы удобно - в этом цикле статей можно совместить инфу для новичков, любителей и профи. И статей за пять - развить полноценный достойный опенсорс пакер\прот.

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

  3. Или на этом:
    Code:
    call GetTickCount
    mov ds:dword_0_9AE104, eax
    _label:
    call GetTickCount
    cmp eax,ds:dword_0_9AE104
    jz _label

    Функция возвращает 0 и получаем бесконечный цикл, старая ИдаСтелс падала.

  4. КАИМИ:
    А потом положить на этот опенсорс, т.к. смысл исключительно обучающий, а длительно заниматься развитием нет смысла по некоторым причинам.
    Ну и что? Да, положить, не положить - а выложить на SF или на git. И все, кому надо - те продолжат. а так да - обучающий характер и получение кайфа от процесса созидания)

    1. Если так, то причем тут антиотладка и алгоритмы сжатия? Статья ведь обучающая, будет описано, как происходит интеграция алгоритма в пакер. Хочется другой алгоритм? Берите сорс и добавляйте свои алгоритмы.
      Хочется антиотладку? --//-- и добавляйте антиотладку.

      Вот upx - пример правильно упаковщика, в частности, из-за того, что есть опция распаковки. Упаковщик же пишем, а не протектор.

      1. Согласен. Но на счет алгоритмов сжатия я упомянул к тому, что всегда хотел понять - как добиваются ТАКОЙ степени сжатия, и почему UPX, являясь стандартом сжатия де-факто - не может догнать тот же упомянутый Upack или FishPP. Поэтому и задал вопрос, подумая, что DX тоже заинтересуется. Антиотладка - это ерунда, если нужно - прикрутить без проблемм, а вот со степенью сжатия - действительно не так все просто.

        1. Автор upack - профессионал в области сжатия информации, да и то в его пакере LZMA использовался помнится плюс достигался небольшой выигрыш за счет хранения части данных в PE заголовке, а так берется IDA, берется upack, рипается алгоритм сжатия (если там что-то отличное от LZMA) и всё.

  5. будет использован пе лоадер или в код пе файла будет встраиваться распаковщик и после распаковки джамп на распакованный код?

  6. Можно вопрос?
    Вот моя папка с проектом:
    lzo-2.06
    pe_lib main.cpp
    2>c:\_temp\_kaimi\step01\simple_pe_packer\simple_pe_packer\main.cpp(55): error C2065: pe32: необъявленный идентификатор
    2>c:\_temp\_kaimi\step01\simple_pe_packer\simple_pe_packer\main.cpp(55): error C2146: синтаксическая ошибка: отсутствие ";" перед идентификатором "image"
    2>c:\_temp\_kaimi\step01\simple_pe_packer\simple_pe_packer\main.cpp(55): error C3861: image: идентификатор не найден
    2>c:\_temp\_kaimi\step01\simple_pe_packer\simple_pe_packer\main.cpp(60): error C4430: отсутствует спецификатор типа - предполагается int. Примечание. C++ не поддерживает int по умолчанию
    2>c:\_temp\_kaimi\step01\simple_pe_packer\simple_pe_packer\main.cpp(60): error C2143: синтаксическая ошибка: отсутствие "," перед "&"
    2>c:\_temp\_kaimi\step01\simple_pe_packer\simple_pe_packer\main.cpp(64): error C2065: e: необъявленный идентификатор
    2>c:\_temp\_kaimi\step01\simple_pe_packer\simple_pe_packer\main.cpp(64): error C2228: выражение слева от ".what" должно представлять класс, структуру или объединение
    2> тип: 'unknown-type'
    2>
    2>СБОЙ построения.
    Не подскажете где накосячил?

  7. ... а нет , сори не дочитал, нужно использовать библиотеку
    с сорсами версии 0.1.11

    Но теперь такая ошибка (((
    2>LINK : fatal error LNK1104: не удается открыть файл "../../Debug/pe_lib.lib"

  8. >Попробуй зайди под sheva740 с паролем который использовал при регистрации
    Не выходит. Можно Перерегистрироваться?

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

  10. Здравствуйте, DX.
    В ходе построения упаковщика (пустая ф-ция main) у меня возникла следующая ошибка:

    2>------ Построение начато: проект: simple_pe_packer, Конфигурация: Debug Win32 ------
    2>Построение начато 23.09.2013 17:01:48.
    2>InitializeBuildStatus:
    2> Обращение к "Debug\simple_pe_packer.unsuccessfulbuild".
    2>ClCompile:
    2> main.cpp
    2>LIBCMTD.lib(wincrt0.obj) : error LNK2019: ссылка на неразрешенный внешний символ [email protected] в функции ___tmainCRTStartup
    2>C:\Users\user1\Documents\Visual Studio 2010\Projects\portable_executable_library\simple_pe_packer\simple_pe_packer\Debug\simple_pe_packer.exe : fatal error LNK1120: 1 неразрешенных внешних элементов
    2>
    2>СБОЙ построения.

    В чём дело и как можно исправить? Жду вашего совета.

  11. dx, с использованием библиотеки LZO проблема, ее лицензия - GNU GPL, а это значит, что использование ее в ПО с закрытым исходным кодом без разрешения автора нарушает лицензию.

    Ты знаешь альтернативу этому алгоритму, сравнимую по скорости и степени сжатия?

    1. Я не писал с ее использованием закрытого ПО, поэтому вопросов не возникало. Наверное, со схожей эффективностью можно заюзать zip/gzip (правда, там придется париться с переделкой кода распаковщика, чтобы он стал базонезависимым, скорее всего, если не найти такую готовую реализацию).

  12. Добрый день не получается собрать библиотеку lzo-2.06
    error LNK2005: __setargv already defined in dict.obj
    error LNK2005: _lzo_wildargv already defined in dict.obj
    error LNK2005: _main already defined in dict.obj
    И так 40 ошибок!

  13. Вроде по шагам все делал, error C1083: Не удается открыть файл включение: pe_lib/pe_bliss.h: No such file or directory
    если не трудно, скиньте папочку с проектом который собирается

    1. PE bliss - это библиотека для работы с PE-файлами. В статье написано, где ее взять и какой версии (https://code.google.com/archive/p/portable-executable-library/). Если цель не обучаться самостоятельно и не разбираться с проблемами, то проще взять проект или собранный упаковщик из последнего урока.

  14. Добрый день! Возможно глупый вопрос будет, но я не нахожу в версии 1.0.0 хедера "pe_32_64.h", есть ли там аналог данного хедера? Или в данному случае годится только версия 0.1.11?

    1. В первом шаге нужно использовать старую версию библиотеки, да. В каком-то из шагов я явно указал, когда перевел код на версию 1.0.0 (кажется, где-то в одном из последних).

  15. Добрый день! Еще один вопрос, при компиляции lzo, а также unpacker (шаг 3) у меня появляется вторая секция .rdata, да и vitrual size .text больше. В чем может быть причина? Я сначала думал, что что-то упустил, но затем скачал ваш архив own-packer-step1, но там у меня такие же результаты. Использую MSVS 2015.

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

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