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

Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
151
476
Допустим имеем файл и нужно перенести его в своей программе на чужую машину. Если этот файл будет внешним (на подобии библиотеки или драйвера), то сразу бросится в глаза. А что если зашить его в своё тело и создавать при запуске основного приложения? Благо производительность современных процессоров это позволяет и жертва даже не успеет понять в чём дело. Другими словами получим матку, которая будет порождать файлы.

Всё-бы хорошо, только таскать в себе готовые бинарники типа exe\dll в несжатом виде тоже не лучшая идея – одних только "байтов выравнивания" в них под 50%, а для использования готовых архиваторов нужен к нему распаковщик. Так-что иметь в запазухе обычный зиппер это всегда гуд, а данная статья (надеюсь) поможет вам разобраться в его деталях. Буду использовать ассемблер FASM, двоичный редактор HxD, ну и виндовый кальк в инженерном виде.
----------------------------------------


Общие положения

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

Процент избыточности привязан к типу данных, где место на олимпе занимают как-раз двоичные программы, ..потом идёт видео, графика и на последнем месте файлы типа *.txt. К примеру, если открыть любую программу или видос в каком-нибудь HEX-редакторе, можно чётко наблюдать в нём цепочки повторяющихся байт, которые так и хочется закодировать в формате "Байт-->Повторов". Поскольку здесь я планирую "поженить" именно две прожки, то мне как-нельзя лучше подходит алгоритм сжатия RLE, что переводится как Run-Length-Encoding.

null.png


В природе существуют ещё два алго сжатия – это "Лемпеля-Зива" (LZ) и нашумевший алгоритм "Хаффмана". Чтобы добиться наилучших результатов и создать универсальный паккер для любых типов данных, обычно три эти алгоритма комбинируют в один флакон. Однако для нашего случая будет достаточно только самого простого RLE, который мы поместим прямо в программу-матку.

В алгоритме RLE, для кодирования цепочек одинаковых байт (не обязательно нулей), применяют байт-маркеры. К примеру, маркер 05h,30h заставит декодер архиватора отправить на выход 5 байт со-значением 0х30, и т.д. (первый байт является счётчиком). Если в бинарнике байты следуют в разнобой, то они вообще никак не кодируются. Таким образом, на белом коне здесь маркеры, которые своим 2-байтным значением способны сжать болото из 127-ми повторяющихся байт. Но почему именно из 127, ведь максимальное значение байта равно 256? Здесь нужно остановиться и вспомнить, как процессор кодирует числа со-знаком..


Бит "SF" в регистре флагов процессора EFLAGS

Примечательным фактом в программировании является то, что логические элементы процессора не понимают отрицательных чисел – для них все числа положительные. Когда перед программистом встаёт какая-нибудь математическая задача, то ответственность по контролю за отрицательными числами полностью ложится на его собственные плечи. Для этого инженеры ввели во-флаговый регистр EFLAGS соответствующий бит и назвали его "знаковым" SF – Sign Flag. Он занял позицию (7) в этом регистре, что демонстрирует рисунок ниже (позаимствован из руководства Intel том.3):

EFLAGS_SF.png


В большинстве случаях, на флаги процессора воздействуют арифметические инструкции типа ADD\SUB\MUL\DIV, логические инструкции XOR\AND\OR\NOT\NEG\INC\DEC, инструкции сдвигов и ротации SHR\SHL\ROR\ROL и некоторые другие. Это даём нам возможность при арифметических операциях контролировать флаг CF переноса битов из младшего байта в старший, флаг чётности битов в байте PF, флаг переноса битов из младшей тетрады байта в старшую AF, флаг нуля ZF, а так-же упомянутый флаг отрицательного числа SF.

Таким образом, аппаратное обеспечение устанавливает флаг знака SF в единицу, если последняя арифметическая операция привела к отрицательному значению. В противном случае (для положительного результата) SF сбрасывается в 0. Отрицательное число характеризует самый старший бит результата. В зависимости от размера операндов инструкции, значимым битом является бит 7 (для байтов), бит 15 (для слов word), бит 31 (для двойных слов dword) или бит 63 (для четверных слов qword).

Поскольку мы планируем искать цепочку байт (а не слов или двойных слов), то соответственно должны контролировать бит 7. Для того-чтобы указать знак числа, достаточно одного разряда (бита). В зависимости от того, как мы воспринимаем число, значения байта может лежать в диапазоне от -127..+127 (байт со-знаком), или 0..255 (байт без знака). Вот пример:

C-подобный:
Байт со-знаком: 10110111 = -55
Байт со-знаком: 00110111 = +55
Байт без знака: 10110111 = 183
                |
                +---> знаковый бит

Детали реализации алгоритма RLE

Допустим задана последовательность байт ниже, которая подлежит сжатию:

C-подобный:
Кодируемая строка:
-----------------------------------------------
41 41 41 41 42 42 42 42 42 43 43 44 45 46 47 48  AAAABBBBBCCDEFGH
Алгоритм RLE советует нам заменить её структурой, где указывается значение кодируемого байта и счётчик его повторений. Если под счётчик использовать 1 байт, то мы сможем закодировать в нём 256 одинаковых элементов – далее будем называть этот "байт-маркером". Здесь напрашивается решение – вставлять маркер перед каждой последовательностью, например так:

C-подобный:
Закодированный вариант:
-------------------------------------------
04.41 05.42 02.43 01.44 01.45 01.46 01.47 01.48 .A.B.C.D.E.F.G.H
-----------------------------------
Коэффициент сжатия: 16 (вход) / 16 (выход) = 1% (нет сжатия)
В данном случаем, маркером является каждый\первый байт пары (я отделил его точками). Работающий по такому алго декодер читает данные с входного потока словами, и использует старший байт в качестве счётчика для младшего. Всё идёт по-плану, пока цепочка не нарушится, после чего нам приходится тратить на каждый байт один маркер, и весь выигрыш вылетает в трубу.

Для решения этой проблемы, условимся воспринимать маркер, как байт со-знаком. То-есть мы задействуем семь младших бит для счётчика повторений 0..127, а самый старший 8-ой бит – будет флагом. Не смотря на то, что таким образом мы потеряли в длине 127 против 255, зато теперь получили логику и сможем ответить уже на два вопроса: "байты в строке дублируются (1), или-же идут в разнобой (0)?".

В качестве примера к вышесказанному, попробуем закодировать по алгоритму RLE тот-же блок данных, только теперь в байт-маркере старший бит будет служить флагом, повторяется цепочка, или нет:

C-подобный:
;// диапазон: 00h..7Fh = 127 -> счётчик произвольных байт;
;// диапазон: 80h..FFh = 127 -> счётчик одинаковых байт.

// Кодируемая строка:
-----------------------------------------------
41 41 41 41 42 42 42 42 42 43 43 44 45 46 47 48   AAAABBBBBCCDEFGH
// Закодированный вариант:
-------------------------------------------
84 41 85 42 82 43 05 44 45 46 47 48   „A…B‚C.DEFGH
-----------------------------------
// Коэффициент сжатия: 16 / 12 = 1,33%
Рассмотрим его подробней..
Декодер начинает чтение закодированных данных и тут-же врезается в первый маркер со-значением 0х84 (1000.0100b, т.е. старший бит взведён в единицу). Логической инструкцией XOR он проверяет его на знак и делает вывод, что значение следующего за ним байта нужно воспринимать как цепочку. Теперь, декодер вычитает константу 0х80 от маркера, и получает счётчик последовательности (здесь 4 или 0000.0100b). Следуя своему алгоритму, теперь декодер читает следующий байт 0х41, как перед ним восстают все данные для декодирования данной последовательности.

Но что будет, когда декодер нарвётся (в примере выше) на маркер со-значением 0х05 ? Как говорилось выше, установленный в нуль знаковый бит является флагом нарушения последовательности, поэтому он просто сделает своё значение счётчиком, и отправит все последующие за собой байты, в выходной поток.

Как-видим, логика алгоритма RLE проста до неприличия, однако при удачных обстоятельствах она позволяет сжать 127 повторяющихся байт буквально до 2-х байт. Если в процессе кодирования данных алгоритм обнаружит во-входном потоке цепочку более чем из 127 байт, то он должен будет вставить ещё один маркер, указав в нём длину торчащего наружу хвоста примерно так:

C-подобный:
130-байтная цепочка нулей: FF 00 83 00
260-байтная цепочка троек: FF 03 FF 03 85 03
Обзор выше доказывает, что от алгоритма RLE можно получить хороший выхлоп только при сжатии последовательности одинаковых байт. В принципе, в исполняемых файлах типа exe\dll таких последовательностей хоть отбавляй, а для текущей задачи по слиянию двух файлов нам больше и не нужно – главное чтобы код удовлетворял требованиям.


Практическая часть – воплощение задуманного

В примере ниже продемонстрируем программу, которая выведет виндовое "окно для выбора файлов", после чего упакует его по описанному алгоритму RLE. Как упоминалось в начале статьи, декодерирование и распаковкой будет заниматься у нас программа-матка, поэтому декодер мы поместим не в эту программу, а в программу-носитель (хотя можно было сделать и универсальную).

Для запроса файла для упаковки, задействуем API-функцию GetOpenFileName(). Эта функция BOOL и возвращает EAX=1, если нам "выпадет решка" и нуль, если файл открыть не удастся. У неё всего один аргумент – указатель на довольно информативную структуру "OpenFileName". Внутри этой структуры мы получим имя открываемого файла, со-всей его подноготной. Полное описание данной функции можно найти :

C-подобный:
GetOpenFileName (
    LPOPENFILENAME  ofn )     //; линк на структуру "OpenFileName"
После того-как файл откроем, выделяем под него память функцией VirtualAlloc() и скопируем в этот регион памяти всё содержимое файла. На следующем шаге выделяем ещё одну область памяти, куда будем сбрасывать зашифрованные данные. Ну и в конце вычисляем размер упакованных данных и записываем их в выходной файл "packed.bin" (выбрать по вкусу). Сам алгоритм сжатия прокомментирован по максимуму, поэтому не вижу смысла повторяться – вот пример:

C-подобный:
format   PE gui
include  'win32ax.inc'
;//-------
.data
ofn      OPENFILENAME
fName    db  128 dup(0)
inHndl   dd  0
fOut     db  'packed.bin',0
outHndl  dd  0
filter   db  'Программы',0
         db  '*.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)
inpBuff  dd  0
outBuff  dd  0
fSize    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     [inHndl],eax                 ;// запомнить его дескриптор
;// Запрашиваем его размер                   ;//
        invoke  GetFileSize,eax,0            ;//
        mov     [fSize],eax                  ;// запомнить размер файла
;// Выделяем буфер для чтения файла          ;//
        invoke  VirtualAlloc,0,[fSize],0x3000,4
        mov     [inpBuff],eax                ;// его адрес
;// Читаем в выделенный буфер весь файл      ;//
        invoke  _lread,[inHndl],[inpBuff],[fSize]
;// Создаём выходной файл "packet.bin"       ;//
        invoke  _lcreat,fOut,0               ;//
        mov     [outHndl],eax                ;// его дескриптор
;// Выделяем под него буфер                  ;//
        invoke  VirtualAlloc,0,[fSize],0x3000,4
        mov     [outBuff],eax                ;// его адреc.

;//============= КОДЕР ==========================
;// Теперь всё готово и можно паковать.
;// Байт-маркер в AH, ECX = длина файла
;// ESI = источник, EDI = приёмник
;//-----------------------------------------------
@pack:  mov     ecx,[fSize]     ;// кол-во данных в буфере
        mov     edi,[outBuff]   ;// адрес приёмника (для записи в файл)
        mov     esi,[inpBuff]   ;// адрес источника данных
@find:  xor     eax,eax         ;// очищаем маркер в AH!
        lodsb                   ;// берём в AL очередной байт из ESI
        cmp     al,byte[esi]    ;// сравниваем его со-следующим байтом
        jnz     @hash           ;// нет одинаковой цепочки! (мусор)
        call    dubleCount      ;// иначе: на процедуру подсчёта одинаковой цепочки
        or      ecx,ecx         ;// конец файла? (проверить ECX на нуль)
        jnz     @find           ;// нет - мотаем..
        jmp     @exit           ;// иначе: на выход!

;// Процедура подсчёта длины НЕодинаковой последовательности.
;// Как найдём одинаковую цепочку - выходим.
;// Нужно проверять маркер на переполнение,
;// если он достигнет 0х7F, вставляем его в выходной поток.
;//---------------------------------
@hash:  inc    ah               ;// считаем в маркер длину цепочки
        cmp    ah,7Fh           ;// тест на знаковое переполнение
        jz     @save1           ;// вставить, если заполнился!
        lodsb                   ;// иначе: возьмём следующий байт с потока(IN)
        cmp    al,byte[esi]     ;// сравниваем его с соседним
        je     @save1           ;// одинаковая цепочка! пора на выход.
        loop   @hash            ;// иначе: продолжаем считать

@save1: shr    ax,8             ;// AL = маркер, AH = 0 (заодно обновили его)
        stosb                   ;// вставляем маркер в поток(OUT)!
        push   ecx esi          ;// внешний счётчик и указатель
        mov    ecx,eax          ;// EСХ = длина цепочки неодинаковых байт
        sub    esi,ecx          ;// вернём указатель источника назад
        dec    esi              ;//   ..коррекция.
        rep    movsb            ;// скопировать EСХ-байт из IN в OUT!
        pop    esi ecx          ;// восстановить внешний цикл!
        loop   @find            ;// продолжаем сжимать блок..
;// Процедура "dubleCount", для подсчёта одинаковой цепочки байт.
;// С ней дела обстоят чуть проще..
;//--------------------------------
dubleCount:
        or    ah,10000000b      ;// взводим старший (знаковый) бит в маркере
@dup:   inc   ah                ;// считаем длину одинаковой цепочки
        cmp   ah,0              ;// проверка на переполнение маркера!
        jnz   @ok               ;// переход, если в маркере есть место
        rol   ax,8              ;// иначе: AL = маркер, AH = значение байта
        stosw                   ;// записать маркер и значение, в поток(OUT)
        mov   ah,80h            ;// обновляем маркер!
@ok:    lodsb                   ;// читаем дальше поток(IN)..
        cmp   al,byte[esi]      ;// сравниваем соседние байты
        jnz   @save2            ;// нет последовательности!
        loop  @dup              ;// всё идёт по-плану..
@save2: rol   ax,8              ;// цепочка прервалась!
        stosw                   ;// вставить маркер,
        ret                     ;//   ..и выйти из процедуры.
;// ============= КОНЕЦ КОДЕРА =======================
;// Запишем упакованные данные в файл.
;// Для начала нужно вычислить их размер.
@exit:  nop                      ;//
        mov      ecx,edi         ;// ECX = хвост потока(OUT)
        sub      ecx,[outBuff]   ;// отнять от него начало
        push     ecx             ;// запомнить разницу для вывода на экран
        invoke  _lwrite,[outHndl],[outBuff],ecx  ;// запись сжатых данных в файл!
;// Прибить дескрипторы всех открытых файлов
        invoke  _lclose,[inHndl]
        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                                       ;//
zip_00.png


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

Ondrik8

prodigy
Green Team
08.11.2016
1 045
3 034
тема хорошая! продолжайте! буду рад увидеть продолжение, но чую тема попахивает, как собрать "локер"
 
  • Нравится
Реакции: Marylin
Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
151
476
но чую тема попахивает, как собрать "локер"
Можно найти массу применений этой технике, в том числе и локеры. Только я не сторонник всяко рода вредительств, поэтому хожу вокруг этой темы, предлагая лишь мутные идеи. Кстати в экзе-файлы можно внедрять готовые HTML-паги и выводить их в модальные окна как сплэш-страницы (типа окошек крэкми) - для это нужно использовать т.н. . Этой теме планирую посвятить отдельную статью.
 
Ondrik8

Ondrik8

prodigy
Green Team
08.11.2016
1 045
3 034
О-о-о-о-о, а вот это уже интересно!) а то все hta & HTA))

так же считаю , что если добавите к этой теме : упаковка + что бы на телеграмм прилетали нужные файлы)) это уже как бы есть в природе) да и у меня в арсенале, думаю будет другим интересно)
 
Последнее редактирование:
Мы в соцсетях: