Статья ZIP'аем файл вручную (часть 2. распаковщик)

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

Суть в том, что в экзе-файлах имеются нулевые "байты выравнивания секций" – в спецификации на РЕ-файл их называют File-Alignment. Они дополняют хвост полезного кода и данных до кратного значения (обычно 200h, 512-байт). При транспортировке файла эти нули представляют собой бесполезный балласт, и чтобы при инжекте эмбриона в матку мы не получили "слона в посудной лавке", было решено избавиться от них, а при распаковке файла в память – опять восстанавливать. Для упаковки бинарника в кокон использовался примитивный алгоритм сжатия RLE, который даёт не плохие результаты при кодировании исполняемых файлов типа EXE\DLL\SYS.

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

Некоторые поправки

В предыдущей части был упущен один важный момент! При распаковки данных, обязательно нужно знать размер исходного файла в несжатом виде, чтобы выделить под него соответствующее кол-во виртуальной памяти функцией VirtualAlloc(). Это мы должны были учесть при сжатии файла, сразу прошив его размер в выходной поток первым двордом. С учётом этого грешка, усовершенствованный вариант упаковщика будет выглядеть так:

C-подобный:
format   PE gui
include  'win32ax.inc'
;//-------
.data
ofn      OPENFILENAME
fName    db  128 dup(0)
fOut     db  'packed.bin',0
filter   db  'Программы',0,'*.exe;*.dll;*.sys;',0,0

capt     db  'Упаковщик EXE-файлов',0
info     db  'Имя файла: %s',13,10
         db  '-----------------------------',13,10
         db  'Размер до сжатия....: %d байт',13,10
         db  'Размер после сжатия: %d байт' ,0
endMes   db  256 dup(0)
fSize    dd  0            ;// размер исходного файла
inpHndl  dd  0            ;// жэндл исходного файла
inpBuff  dd  0            ;//   ..адрес буфера под него
outHndl  dd  0            ;// жэндл сжатого файла
outBuff  dd  0            ;//   ..адрес буфера под него
;//-------
.code
start:
;//======= Подготовительные операции.. ==============
;// перед вызовом функции GetOpenFileName()
;// нужно заполнить некоторые поля структуры "OPENFILENAME" (ofn)
        mov    [ofn.lStructSize],76          ;// её длина в байтах
        mov    [ofn.lpstrFilter],filter      ;// задаём тип открываемых файлов
        mov    [ofn.lpstrFile],fName         ;// куда сохранить имя файла
        mov    [ofn.nMaxFile],512            ;// длинна в байтах имени (ХР и выше)
        mov    [ofn.Flags], OFN_EXPLORER     ;// стиль виндового окна
        invoke  GetOpenFileName, ofn         ;// запросить окно выбора файлов!

;// Открываем выбранный файл                 ;//
        invoke  _lopen,fName,2               ;// 2 = атрибут R\W
        mov     [inpHndl],eax                ;// запомнить его дескриптор
;// Запрашиваем его размер                   ;//
        invoke  GetFileSize,eax,0            ;//
        mov     [fSize],eax                  ;// запомнить размер файла
;// Выделяем буфер для чтения файла          ;//
        invoke  VirtualAlloc,0,[fSize],0x3000,4
        mov     [inpBuff],eax                ;// адрес буфера для чтения
;// Читаем в выделенный буфер весь файл      ;//
        invoke  _lread,[inpHndl],[inpBuff],[fSize]
;// Создаём выходной файл "packet.bin"       ;//
        invoke  _lcreat,fOut,0               ;//
        mov     [outHndl],eax                ;// его дескриптор
;// Выделяем под него буфер                  ;//
        invoke  VirtualAlloc,0,[fSize],0x3000,4
        mov     [outBuff],eax                ;// адрес буфера для записи

;//============= КОДЕР-УПАКОВЩИК =====================
;// Байт-маркер в AH, ECX = длина файла
;// ESI = источник, EDI = приёмник упакованных данных
;//---------------------------------------------------
        mov    esi,[inpBuff]    ;// адрес источника данных
        mov    edi,[outBuff]    ;// адрес приёмника (для записи в файл)
        mov    ecx,[fSize]      ;// кол-во данных в буфере
        mov    [edi],ecx        ;// вставить в поток(OUT) размер файла!!!
        add    edi,4            ;//   ..(сместим указатель приёмника).
        xor    eax,eax          ;// очищаем маркер в AH!
;// Процедура подсчёта длины НЕ одинаковой последовательности.
;// Как найдём одинаковую цепочку, сразу уходим на её подсчёт.
;// Нужно периодически проверять маркер на переполнение,
;// если он достигнет 0х7F, вставляем его в выходной поток и обновляем.
;//---------------------------------
@pack:  cmp    ah,0x7f          ;// тест на знаковое переполнение
        jz     @save1           ;// вставить, если заполнился!
        lodsb                   ;// иначе: возьмём очередной байт с потока(IN)
        cmp    al,byte[esi]     ;// сравнить его со-следующим байтом
        jz     @double          ;// одинаковая цепочка! передать управление.
        inc    ah               ;// иначе: считаем в маркер длину мусора
        loop   @pack            ;// продолжаем считать..
        jmp    @exit            ;// выйти по-окончании (если ECX=0)

;// Обнаружено переполнение маркера - записать и обновить его
@save1: shr    ax,8             ;// AL = маркер (сдвинуть АХ вправо на 8)
        stosb                   ;// вставляем его в поток(OUT)!
        push   ecx esi          ;// запомнить счётчик и указатель
        mov    ecx,eax          ;// EСХ = длина цепочки неодинаковых байт
        sub    esi,ecx          ;// вернём указатель источника назад
        rep    movsb            ;// скопировать EСХ-байт из IN в OUT!
        pop    esi ecx          ;// восстановить внешний цикл!
        loop   @pack            ;// продолжаем сжимать блок..
        jmp    @exit

;// Процедура "doubleCount", для подсчёта одинаковой цепочки байт.
;// Сначала сохраним счётчик неодинаковой последовательности
;//---------------------------------------------------------------
@double:
        dec    esi              ;// прогнорировать предыдущий LODSB
        shr    ax,8             ;// сместить маркер в AL, обнулив по-ходу AH
        stosb                   ;// вставляем маркер из AL в поток(OUT)!
        push   ecx esi          ;// запомнить счётчик и указатель
        mov    ecx,eax          ;// EСХ = длина цепочки неодинаковых байт
        sub    esi,ecx          ;// вернём указатель источника назад
        rep    movsb            ;// скопировать EСХ-байт из IN в OUT!
        pop    esi ecx          ;// восстановить внешний цикл!

;// Только теперь посчёт одинаковой цепочки
        or     ah,0x80          ;// взводим старший (знаковый) бит в маркере
@dup:   inc    ah               ;// считаем длину одинаковых байт
        cmp    ah,0xff          ;// проверка на переполнение маркера!
        jnz    @ok              ;// переход, если в маркере есть место
        xchg   ah,al            ;// иначе: AL = маркер, AH = значение байта
        stosw                   ;// записать маркер и значение, в поток(OUT)
        xor    ah,ah            ;// очистить маркер
        or     ah,80h           ;// взвести в нём старший бит
@ok:    lodsb                   ;// читаем дальше поток(IN)..
        cmp    al,byte[esi]     ;// сравниваем соседние байты
        jnz    @save2           ;// нет последовательности!
        loop   @dup             ;// всё идёт по-плану..
@save2: xchg   ah,al            ;// цепочка прервалась! обменять регистры
        stosw                   ;// вставить маркер в поток(OUT)!
        xor    ax,ax            ;// очистить регистр AX
        inc    ecx              ;//   ..(коррекция длинны файла)
        loop   @pack            ;// повторить весь цикл, пока ECX >0
                                ;// иначе:...
;// ============= КОНЕЦ КОДЕРА =======================
;// Запишем упакованные данные в файл.
;// Для начала нужно вычислить их размер.
@exit:  nop                      ;//
        mov      ecx,edi         ;// ECX = конец потока(OUT)
        sub      ecx,[outBuff]   ;// отнять от него начало
        push     ecx             ;// запомнить разницу для вывода на экран
        invoke  _lwrite,[outHndl],[outBuff],ecx  ;// запись сжатых данных в файл!
;// Прибить дескрипторы всех открытых файлов
        invoke  _lclose,[inpHndl]
        invoke  _lclose,[outHndl]
;// Вывод инфы на экран
        pop      ecx             ;// ECX = размер упакованного файла
        invoke   wsprintf,endMes,info,fName,[fSize],ecx   ;// перевести всё в символы
        invoke   MessageBox,0,endMes,capt,0      ;// боксим в мессагу
        invoke   ExitProcess,0                   ;// Game-Over!!!
.end start                                       ;//

Посмотрим на рисунок ниже.. Здесь я упаковал произвольный файл trace.exe по указанному алго и на выходе получил packed.bin. Теперь открыл оба варианта в HEX-редакторе, и все маркеры как буйки всплыли на поверхность. Каждый из маркеров нижнего окна соответствует цепочки байт верхнего (распределены по цветам). Если во-входном потоке данных упаковщик обнаруживает НЕ одинаковую последовательность, то вставляет 1-байтный маркер со-сброшенным своим старшим битом. При взведённом старшем бите, значение маркера получается больше 0х80 (см.калькулятор), и он становится 2-байтным, характеризуя флаг одинаковой последовательности. Вот пример:

markers.png


Пропустив дворд с размером файла, возьмём первый\красный маркер 05h нижнего окна. Его значение меньше 80h (старший бит сброшен), значит он кодирует НЕ одинаковую последовательность, длинною в 5-байт. Следующий синий маркет имеет 2-байтное значение 8300h, значит это наш клиент с последовательностью из трёх нулей.

Судя по этому рисунку, на неодинаковую цепочку расходуется один лишний байт-маркер. Соответственно если-бы в этом файле не было цепочек одинаковых байт, то мы оказались-бы в минусе и файл вместо того-чтобы сжаться, наоборот-бы разбух. Но благодаря серому блоку нулей верхнего окна, нам удалось сжать этот фрагмент с 59 байт 0x3B, аж до 25-ти 0х19. А ведь здесь до байтов выравнивания-секций мы ещё не дошли и их гораздо больше, чем представленный на скрине серый блок. Так-что профит очевиден в любом случае.


Инжект сжатого бинарника в файл-носитель

Теперь пришло время поместить сжатый файл в экзешник, который будет выступать в роли матки-носителя. Для этого можно использовать HEX-редактор и выстроить это дело вручную, однако FASM предоставляет нам более изящное средство – это (говорящая за себя) его директива FILE (в других ассемблерах она числится как "INCBIN"). Вот как её описывает документация к фасму:

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

В своём примере, мы подключаем весь сжатый бинарник, поэтому параметры нам не нужны. Мы выделим под него отдельную секцию-данных, и по размеру бинарника компилятор сам выставит требуемый размер этой секции (напомню, что на системах х32 макс.размер программных секций может достигать 4Gb). Хоть параметры данной директивы fasm'a сейчас нам не интересны, на всякий\пожарный приведём несколько её вариаций:

C-подобный:
file  'data.bin'          ;// подключить весь файл как двоичные данные;
file  'data.bin':10h      ;// подключить весь файл, начиная со смещения 10h;
file  'data.bin':40h,256  ;// подключить только 256-байт, начиная со смещения 40h.

Как видим, директива довольно универсальна и позволяет подключать к нашим проектам не только бины, но и файлы mp3, видео, картинки и любые другие форматы. Здесь дело в том, как мы будем обрабатывать эти данные. Например, подключённые таким способом мультимедийные файлы, мы можем запускать непосредственно из своего кода функцией Exec() и они прекрасно отработают. Другими словами, в своё распоряжении получаем встроенный ресурс.


Алгоритм работы декодера

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

Если нашей целью раньше был коммунизм, то на текущий момент – это поиск маркеров в потоке данных. Значит берём первый дворд из упакованных данных (размер файла в несжатом виде) и по его значению выделяем память функцией VirtualAlloc(). Теперь в эту область памяти будем распаковывать байтики, после чего скинем их в отдельный EXE-файл. Как обычно, исходник полностью прокомментирован, поэтому вопросов возникнуть не должно, ..а если и возникнут – всегда можно расчехлить отладчик:

C-подобный:
format   PE gui
include  'win32ax.inc'
;//-------
.data
fName   db  'child.exe',0        ;//<---------<-------+
capt    db  'Распаковщик',0            ;//            |
info    db  'Hello World',13,10        ;//            |
        db  'Файл успешно создан!',0   ;//            |
Hndl    dd  0                ;// жэндл нового файла --+
Buff    dd  0                ;// адрес буфера под него
;//-------
section '.pdata' data readable writable  ;// создаём доп.секцию-данных
@binFile:                    ;// метка, определяющая начало внеш.файла
file    'packed.bin'         ;// зашиваем внешний файл в свою тушку!
@binSize  dd  $ - @binFile   ;// вычисляем размер внешнего файла:
                             ;//  ..^^^текущий адрес($) минус начало
;//-------
.code
start:
;// Возьмём длинну неупакованного файла
;// как помним, она лежит в первом дворде бинарника
        mov     eax,[@binFile]
;// Выделяем буфер для распаковки
        invoke  VirtualAlloc,0,eax,0x3000,4
        mov     [Buff],eax        ;// запомнить его адреc
;// Создаём выходной файл "child.exe"
        invoke  _lcreat,fName,0   ;//
        mov     [Hndl],eax        ;// его дескриптор

;//======= ДЕКОДЕР-РАСПАКОВЩИК ==============
;//==========================================
        mov     ecx,[@binSize]   ;// длина упакованных данных
        mov     esi,@binFile+4    ;// их источник (после размера)
        mov     edi,[Buff]        ;// сюда будем распаковывать
        xor     eax,eax           ;// EAX = 0
@find:  lodsb                     ;// берём в AL очередной маркер
        test    al,0x80           ;// проверка знакового бита в нём
        jnz     @dup              ;// если он не нуль, значит это цепочка
                                  ;// иначе: мусор и в AL лежит счётчик.
;// Декодер НЕ одинаковой последовательности
;//---------------------
        push    ecx eax           ;// запомнить счётчики (ECX = внешний цикл)
        mov     ecx,eax           ;// ЕCX = счётчик байт для копирования
        rep     movsb             ;// копируем EСХ-байт из ESI в EDI
        pop     eax ecx           ;// счётчики на родину
        sub     ecx,eax           ;// коррекция длинны внешнего цикла
        jmp     @next             ;// прыгаем вниз..
;// Декодер одинаковой цепочки байт
;// Здесь тоже-самое, только корректируем длину в маркере,
;// читаем следующий байт, и дублируем его в поток(OUT)
;//---------------------
@dup:   sub     al,0x80           ;// отнимаем от маркера константу 80h
        push    ecx               ;// запомнить внешний счётчик
        mov     ecx,eax           ;// ЕСХ = кол-во повторов
        lodsb                     ;// AL = значение повторяющегося байта
        rep     stosb             ;// сбрасываем его в буфер EСХ-раз!
        pop     ecx               ;// восстановить внешний счётчик
        dec     ecx               ;// -1, т.к. здесь маркер 2-байтный
@next:  loop    @find             ;// продолжаем, пока ECX > 0
;//================================================
;//========== КОНЕЦ РАСПАКОВЩИКА ==================
;//
;// Запишем распакованные данные в файл 'child.exe'
@exit:  nop
        mov      ecx,[@binFile]            ;// ECX = размер файла
        invoke  _lwrite,[Hndl],[Buff],ecx  ;// запись данных в файл!
        invoke  _lclose,[Hndl]             ;// закроем его.

        invoke   MessageBox,0,info,capt,0  ;// боксим мессагу
        invoke   ExitProcess,0             ;// Game-Over!!!
.end start                                 ;//

Из этого исходника видно, что процесс распаковки кода намного проще, чем процедура его сжатия. Алгоритм развёртывания не имеет права быть тормознутым и обязан работать быстро. После создания файла на диске, можно его сразу запустить или оставить это действо до лучших времён, поместив его, например, в автозагрузку или куда-нибудь к чёрту на кулички – здесь всё зависит от фантазии.


Послесловие..

В этой статье мы рассмотрели самый одноклеточный алгоритм сжатия RLE – Run Length Encoding. Он абсолютно не приспособлен для сжатия например текста, для которого лучшим выбором будет LZ со-своим словарём, или Хаффман с его бинарным древом. Отдельного внимания заслуживают и промышленные упаковщики типа: UPX, Aspack, Asprotect, Crunch, Armadillo и многие другие, только это уже из другой оперы и на их изучение можно потратить всю оставшуюся жизнь. Кого интересует эта тема, советую почитать статью из трёх частей Володи с WASM'a – она называется "Об упаковщиках в последний раз". Всем удачи и до скорого!
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!