В продолжение темы об упаковщиках..
В первой части речь шла о том, как можно создать файл с геном репродукции, который по требованию порождал-бы бесчисленное количество своих воинов. Эта армия может решать определённый круг задач, а само требование может быть каким угодно, например привязка по времени, или какому-нибудь системному событию. Более того, способ позволяет создавать т.н. "файлы-матрёшки", размножение которых отследить будет довольно сложно.
Суть в том, что в экзе-файлах имеются нулевые "байты выравнивания секций" – в спецификации на РЕ-файл их называют File-Alignment. Они дополняют хвост полезного кода и данных до кратного значения (обычно 200h, 512-байт). При транспортировке файла эти нули представляют собой бесполезный балласт, и чтобы при инжекте эмбриона в матку мы не получили "слона в посудной лавке", было решено избавиться от них, а при распаковке файла в память – опять восстанавливать. Для упаковки бинарника в кокон использовался примитивный алгоритм сжатия RLE, который даёт не плохие результаты при кодировании исполняемых файлов типа EXE\DLL\SYS.
Алгоритм RLE основан на байт-маркерах, при помощи которых можно закодировать последовательный 127-байтный блок одинаковых байт, буквально в двух байтах. При кодировании цепочек, первый байт в маркере выступает в роли счётчика, а второй байт – непосредственно повторяющееся значение. Для неодинаковой последовательности применяются 1-байтные маркеры, которые тупо выступают в роли счётчика их длинны.
Некоторые поправки
В предыдущей части был упущен один важный момент! При распаковки данных, обязательно нужно знать размер исходного файла в несжатом виде, чтобы выделить под него соответствующее кол-во виртуальной памяти функцией VirtualAlloc(). Это мы должны были учесть при сжатии файла, сразу прошив его размер в выходной поток первым двордом. С учётом этого грешка, усовершенствованный вариант упаковщика будет выглядеть так:
Посмотрим на рисунок ниже.. Здесь я упаковал произвольный файл trace.exe по указанному алго и на выходе получил packed.bin. Теперь открыл оба варианта в HEX-редакторе, и все маркеры как буйки всплыли на поверхность. Каждый из маркеров нижнего окна соответствует цепочки байт верхнего (распределены по цветам). Если во-входном потоке данных упаковщик обнаруживает НЕ одинаковую последовательность, то вставляет 1-байтный маркер со-сброшенным своим старшим битом. При взведённом старшем бите, значение маркера получается больше
Пропустив дворд с размером файла, возьмём первый\красный маркер
Судя по этому рисунку, на неодинаковую цепочку расходуется один лишний байт-маркер. Соответственно если-бы в этом файле не было цепочек одинаковых байт, то мы оказались-бы в минусе и файл вместо того-чтобы сжаться, наоборот-бы разбух. Но благодаря серому блоку нулей верхнего окна, нам удалось сжать этот фрагмент с 59 байт
Инжект сжатого бинарника в файл-носитель
Теперь пришло время поместить сжатый файл в экзешник, который будет выступать в роли матки-носителя. Для этого можно использовать HEX-редактор и выстроить это дело вручную, однако FASM предоставляет нам более изящное средство – это (говорящая за себя) его директива FILE (в других ассемблерах она числится как "INCBIN"). Вот как её описывает документация к фасму:
В своём примере, мы подключаем весь сжатый бинарник, поэтому параметры нам не нужны. Мы выделим под него отдельную секцию-данных, и по размеру бинарника компилятор сам выставит требуемый размер этой секции (напомню, что на системах х32 макс.размер программных секций может достигать 4Gb). Хоть параметры данной директивы fasm'a сейчас нам не интересны, на всякий\пожарный приведём несколько её вариаций:
Как видим, директива довольно универсальна и позволяет подключать к нашим проектам не только бины, но и файлы mp3, видео, картинки и любые другие форматы. Здесь дело в том, как мы будем обрабатывать эти данные. Например, подключённые таким способом мультимедийные файлы, мы можем запускать непосредственно из своего кода функцией Exec() и они прекрасно отработают. Другими словами, в своё распоряжении получаем встроенный ресурс.
Алгоритм работы декодера
Теперь, когда в своей тушке мы имеем файл и знаем алгоритм его сжатия, написать для него распаковщик не составит особого труда. Тут можно быть уверенным на все 100, что если не учитывать первый дворд с размером файла, то непосредственно сами упакованные данные обязательно начинаются с маркера (в данном случае 05, см.рис.выше), а дальше – эти маркеры как бусы могут быть разбросаны по всему коду в произвольном порядке. То-есть главное заполучить первый маркер, и мы автоматом будем натыкаться на остальные.
Если нашей целью раньше был коммунизм, то на текущий момент – это поиск маркеров в потоке данных. Значит берём первый дворд из упакованных данных (размер файла в несжатом виде) и по его значению выделяем память функцией VirtualAlloc(). Теперь в эту область памяти будем распаковывать байтики, после чего скинем их в отдельный EXE-файл. Как обычно, исходник полностью прокомментирован, поэтому вопросов возникнуть не должно, ..а если и возникнут – всегда можно расчехлить отладчик:
Из этого исходника видно, что процесс распаковки кода намного проще, чем процедура его сжатия. Алгоритм развёртывания не имеет права быть тормознутым и обязан работать быстро. После создания файла на диске, можно его сразу запустить или оставить это действо до лучших времён, поместив его, например, в автозагрузку или куда-нибудь к чёрту на кулички – здесь всё зависит от фантазии.
Послесловие..
В этой статье мы рассмотрели самый одноклеточный алгоритм сжатия RLE – Run Length Encoding. Он абсолютно не приспособлен для сжатия например текста, для которого лучшим выбором будет LZ со-своим словарём, или Хаффман с его бинарным древом. Отдельного внимания заслуживают и промышленные упаковщики типа: UPX, Aspack, Asprotect, Crunch, Armadillo и многие другие, только это уже из другой оперы и на их изучение можно потратить всю оставшуюся жизнь. Кого интересует эта тема, советую почитать статью из трёх частей Володи с WASM'a – она называется "Об упаковщиках в последний раз". Всем удачи и до скорого!
В первой части речь шла о том, как можно создать файл с геном репродукции, который по требованию порождал-бы бесчисленное количество своих воинов. Эта армия может решать определённый круг задач, а само требование может быть каким угодно, например привязка по времени, или какому-нибудь системному событию. Более того, способ позволяет создавать т.н. "файлы-матрёшки", размножение которых отследить будет довольно сложно.
Суть в том, что в экзе-файлах имеются нулевые "байты выравнивания секций" – в спецификации на РЕ-файл их называют 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-байтным, характеризуя флаг одинаковой последовательности. Вот пример:Пропустив дворд с размером файла, возьмём первый\красный маркер
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 – она называется "Об упаковщиках в последний раз". Всем удачи и до скорого!