Техническая поддержка :

Современные решения

для защиты Windows программ

и восстановления исходного кода
Автор: bagie . Дата публикации: 15.09.2006

Пакер – это просто!

Вступление

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

Но целью данной статьи является не описание пакеров, а их написание. Приложив немного усилий и изучив данный материал, а также другие источники, которые сочтет нужными, читатель получит основы работы с PE-файлами, поймет основные принципы, заложенные в упаковщиках и протекторах, а также сможет написать простой пакер своими собственными руками. Все необходимые пояснение будут даны по мере изложения материала. Однако подразумевается что читатель все же знаком с низкоуровневым языком программирования – ассемблером и знает хотя бы основы программирования на нем. В данном случае для написания будет использован flat assembler или просто FASM. Нам понадобится версия не ниже 1.66, а скачать сиё замечательно творение можно тут: http://flatassembler.net/. Итак, начнем.

Краткий обзор PE-формата

PE (Portable Executable) – это формат исполняемых файлов операционных систем Windows и представляет он собой особую структуру данных, содержащую служебную информацию, используемую загрузчиком операционной системы, а также непосредственно программный код и другие данные, используемые самой программой. Сама организация данных в PE-файлах очень проста и является достаточно документированной, поэтому её особо расписывать не имеет смысла. Лично я пользовался шпаргалкой «ФОРМАТ ИСПОЛНЯЕМЫХ ФАЙЛОВ Portable Executables» by Hard Wisdom. Еще можно рекомендовать для прочтения цикл статей «Об упаковщиках в последний раз». Однако, нам, без знания хотя бы основ, написать упаковщик попросту нереально, поэтому вкратце все же пробежимся по внутреннему устройству PE-формата и выявим наиболее значимые для нас поля и структуры.

DOS-заглушка
ДОС-программа, обычно выводящая на экран что-то типа "This program cannot be run in DOS mode." и завершающая свою работу.
\
РЕ-заголовок
Заголовок РЕ-формата. Содержит базовую информацию, необходимую загрузчику операционной системы.
\
Заголовки секци
N-количество секций, количество которых указано в РЕ-заголовке. Само кол-во может быть разным и принимать значение от 1 до 65535 теоретически (хотя здесь возможно я и неправ, но всё равно больше 15-20 секций в РЕ-файле я не встречал).
\
Данные секций
Непосредственно программный код и данные. Описываются в заголовках соответсвующих им секций.

В начале любого PE-файла находится DOS-заглушка. В принципе она нас особо не интересует, кроме двух полей. В самом начале файла находится сигнатура ’MZ’ (2 байта), говорящая о том, что наш файл является исполняемым, то бишь exe. Почему ‘MZ’, а не ‘XY’, например? Все дело в том, что она представляет собой инициалы одного из разработчиков операционной системы MS-DOS 2.0 Марка Збиковски и знаменита тем, что ни одна инструкция процессоров семейства Intel x86 с нее не начинается. В свое время эта ее особенность давала загрузчику исполняемых файлов MS-DOS возможность отличать exe-файлы, которые появились только во второй версии MS-DOS, от com-файлов. Но это мы отвлеклись... Вторым важным полем в заголовке DOS-заглушки является поле IMAGE_DOS_HEADER._lfanew (4 байта), находящееся по смещению 3Ch от начала файла. В нем
указано реальное смещение PE-заголовка, по которому он находится в программном файле. В нем находится множество полей, большинство которых не несут или почти не несут никакой информационной нагрузки. По этой причине для нас будут важны только несколько полей, но при желании Вы все же можете найти и почитать полное описание PE-заголовка и всего формата в целом.

CODE NOW!
Смещение/Размер/Название/Описание

00h DWORD Signature Сигнатура PE-файла. Должна быть равна ‘PE00’.
06h WORD Number of Количество секций в файле.
sections
28h DWORD Entry point Относительный виртуальный адрес точки входа (Relative Virtual
RVA Address). Грубо говоря, после загрузки в память, управление
получит программный код, находящийся по этому адресу в памяти.
34h DWORD Image base Начальный или базовый адрес, по которому будет спроецирован
наш файл в виртуальное адресное пространство после загрузки.
38h DWORD Section Значение, используемое для выравнивания виртуальных
alignment размеров секций. Обычно имеет значение 0x1000.
3Ch DWORD File alignment Значение, используемое для выравнивания физических размеров
секций на диске. Обычно имеет значение 0x200.
50h DWORD Size of image Размер всего образа загружаемого файла вместе с заголовками.
78h 16x(2xDWORD) Data Первая директория размером 2xDWORD, содержащая
directories информацию о специальных объектах – директориях, всего их
16, но обычно присутствует только директория импорта,
ресурсов, экспорта, релоков, tls и отладочной информации.
Остальные, в принципе, не используются.



Если Вы ничего не поняли о значении полей PE-заголовка, то еще раз рекомендую почитать об этом в
документации к PE-формату. Тут же стоит отметить, что почти во всех заголовках указывается относительный виртуальный адрес, который представляет собой относительное значение от поля ImageBase. Иными словами VA (Virtual Address) = RVA (Relative Virtual Address) + ImageBase.

Сразу после PE-заголовка идут заголовки секций, количество которых указано в поле NumberOfSections. Они представляют следующую структуру.

CODE NOW!
Смещение/Размер/Название/Описание

00h 8 Name ASCII-строка, заканчивающаяся нулем и содержащая
название секции. Если длина имени равна 8, то в конце
нуль не ставится.
08h DWORD Virtual size Виртуальный размер секции.
0Ch DWORD Virtual address Виртуальный адрес начала секции в памяти.
10h DWORD Size of raw data Размер физических данных секции на диске.
14h DWORD Pointer to raw data Смещение, указывающее на данные секции в файле.
18h 3xDWORD Reserved Не используется, по крайней мере, в экзешниках.
24h DWORD Characteristics Дополнительные флаги секции, позволяющие установить
атрибуты защиты, такие как чтение, запись, исполнение
кода и др.



После этого размер всех заголовков, выровненный до большего значения FileAlignment (обычно это 0x200) представляет собой значение поля SizeOfHeaders и одновременно это значение указывает на начало первой секции в файле. Чаще всего это значение бывает 0x400 или 0x1000, но это совсем не обязательно. После чего данные секций идут вплотную друг к другу, а в самом конце PE-файла могут находиться любые данные, которые называются оверлеем или экстра-данными. Обычно оверлей в PE-фалах отсутствует, но например некоторые программы или инсталляторы используют его, храня там свои данные.

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




С чего начать?

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

Для работы с файлами и в данном случае тем более, предпочтительнее использовать файл-мэппинг –
технологию позволяющую спроецировать файл в адресное пространство своего процесса и работать с файлом как с куском памяти. Т.е. читать и писать мы будем в память, и без использования API-функций ReadFile и WriteFile, что существенно упрощает и ускоряет весь процесс написания, тем более на ассемблере. Также стоит создавать сначала резервную копию файла, открытую только для чтения, а создавать в новый файл. Это позволит в случае возникновения какой-либо ошибки всегда сделать откат проведенных изменений. Ниже приведен код функции воплощающий все эти веселости в реальность.

CODE NOW!
; это чтобы не писать десять раз параметры для msgbox
proc ShowError,Msg
invoke MessageBox,[hWnd],[Msg],__msg_caption,MB_ICONERROR
ret
endp

; опишем некоторые флаги для нашей функции
CPE_MAKEBACKUP = $00000001 ; надо создать резервную копию
CPE_SAVEOVERLAY = $00000002 ; сохранить оверлей
CPE_PACKALLRSRC = $00000004 ; упаковать всю секцию ресурсов

proc CompressFile,Filename,Flags
locals
buffer db MAX_PATH dup (?)
fBackup dd ?
fPacked dd ?
mBackup dd ?
mPacked dd ?
pBackup dd ?
pPacked dd ?
filesize dd ?
endl

push ebx esi edi

xor eax,eax
mov [pBackup],eax
mov [pPacked],eax
dec eax
mov [mBackup],eax
mov [mPacked],eax
; сделаем резервную копию файла
lea ebx,[buffer]
invoke lstrcpy,ebx,[Filename]
call @f
db ’.bak’,0
@@: invoke lstrcat,ebx;,’.bak’
invoke SetFileAttributes,ebx,0
invoke CopyFile,[Filename],ebx,0

; установим финальный обработчик исключений на всякий случай....
mov [safeEbp],ebp
invoke SetUnhandledExceptionFilter,@_critical_error

; откроем бэкап для чтения
invoke CreateFile,ebx,GENERIC_READ,0,0,OPEN_EXISTING,0,0
mov [fBackup],eax
cmp eax,INVALID_HANDLE_VALUE
jne @f
stdcall ShowError,__msg_read_error
jmp @_pack_error

@@: invoke GetFileSize,[fBackup],0
cmp eax,1024
jge @f
stdcall ShowError,__msg_size_error
jmp @_pack_error
@@: mov [filesize],eax

; теперь смаппим файл
invoke СreateFileMapping,[fBackup],0,PAGE_READONLY,0,[filesize],0
mov [mBackup],eax
or eax,eax
jne @f
stdcall ShowError,__msg_map_error
jmp @_pack_error
@@: invoke MapViewOfFile,[mBackup],FILE_MAP_READ,0,0,0
mov [pBackup],eax
or eax,eax
jne @f
stdcall ShowError,__msg_map_error
jmp @_pack_error
@@:
; проверка файла на валидность

; MZ сигнатура
cmp [eax+IMAGE_DOS_HEADER.e_magic],IMAGE_DOS_SIGNATURE
je @f
stdcall ShowError,__msg_pefile_error
jmp @_pack_error
@@:
mov esi,[eax+IMAGE_DOS_HEADER._lfanew]
add esi,[pBackup]
; лучше проверить, а то вдруг esi указывает в никуда, например если файл не PE
invoke IsBadReadPtr,esi,4
or eax,eax
je @f
stdcall ShowError,__msg_pefile_error
jmp @_pack_error
@@:

; PE сигнатура
cmp [esi+IMAGE_NT_HEADERS.Signature],IMAGE_NT_SIGNATURE
je @f
stdcall ShowError,__msg_pefile_error
jmp @_pack_error
@@:

; пока наш пакер не будет уметь паковать DLL, поэтому известим об этом народ O
test [esi+IMAGE_NT_HEADERS.FileHeader.Characteristics],IMAGE_FILE_DLL
je @f
stdcall ShowError,__msg_dllfile_error
jmp @_pack_error
@@:

; посмотрим сколько секций находится в файле, если 1..64 то нормально
; до 64 потому что довольно подозрительно если файле очень много секций,
; но если хотите, то можете убрать такую проверку
movzx ecx,[esi+IMAGE_NT_HEADERS.FileHeader.NumberOfSections]
or ecx,ecx
jne @f
stdcall ShowError,__msg_pefile_error
jmp @_pack_error
@@: cmp ecx,64
jle @f
stdcall ShowError,__msg_pefile_error
jmp @_pack_error
@@:

; теперь откроем оригинальный файл на запись....

invoke CreateFile,[Filename],GENERIC_READ+GENERIC_WRITE,0,0,CREATE_ALWAYS,0,0
mov [fPacked],eax
cmp eax,INVALID_HANDLE_VALUE
jne @f
stdcall ShowError,__msg_write_error
jmp @_pack_error
; SetEndOfFile в данном случае позволяет создать новый файл нужного нам размера
; но забитого нулями, для того чтобы можно было проще создать упакованный файл с нуля
@@: invoke SetEndOfFile,[fPacked]
mov eax,[filesize]
add eax,loader_size
invoke CreateFileMapping,[fPacked],0,PAGE_READWRITE,0,eax,0
mov [mPacked],eax
or eax,eax
jne @f
stdcall ShowError,__msg_map_error
jmp @_pack_error
@@: invoke MapViewOfFile,[mPacked],FILE_MAP_READ+FILE_MAP_WRITE,0,0,0
mov [pPacked],eax
or eax,eax
jne @f
stdcall ShowError,__msg_map_error
jmp @_pack_error
@@:

; тут будут все основные веселости, но пока оставим это место в покое
; ********************************************************************
; …
; ********************************************************************

xor ebx,ebx
inc ebx
jmp @f

; если мы тут то возникла ошибка :(
@_pack_error:
xor ebx,ebx
and [Flags],not(CPE_MAKEBACKUP)
@@:

; установим обработчик исключений по умолчанию
invoke SetUnhandledExceptionFilter,0

; закроем все файлы
invoke UnmapViewOfFile,[pPacked]
invoke CloseHandle,[mPacked]
invoke SetFilePointer,[fPacked],[filesize],0,FILE_BEGIN
invoke SetEndOfFile,[fPacked]
invoke CloseHandle,[fPacked]

invoke UnmapViewOfFile,[pBackup]
invoke CloseHandle,[mBackup]
invoke CloseHandle,[fBackup]

; если ebx = 0, то была ошибка и восстанавливаем файл из копии
or ebx,ebx
jne @f
invoke SetFileAttributes,[Filename],0
lea eax,[buffer]
invoke CopyFile,eax,[Filename],0
@@:

; а надо ли нам создавать резервную копию, если нет, то удалим её
test [Flags],CPE_MAKEBACKUP
jne @f
lea eax,[buffer]
invoke DeleteFile,eax
@@:
mov eax,ebx

pop edi esi ebx
ret
; это наш обработчик исключений.
; если мы здесь значит все очень даже печально
; но мы не будем позориться и выведем красивое сообщение об ошибке
; и отменим упаковку
@_critical_error:
stdcall ShowError,__msg_critical_error
mov eax,[esp+4]
mov eax,[eax+EXCEPTION_POINTERS.ContextRecord]
mov [eax+CONTEXT.Eip],@_pack_error
mov edx,[safeEbp]
mov [eax+CONTEXT.Ebp],edx
xor eax,eax
dec eax
retn 4
endp



Тут вроде все понятно. Создали шаблон для манипуляции с нашим файлом. Также походу проверили файл на “корректность”. Но, пожалуй, стоит добавить еще одну проверку. А что если файл уже запакован чем-то? Может быть тогда выводить сообщение что мол так и так, файл мэйби пакед, и ду ю хотеть продолжать? Но тогда встает вопрос, каким образом это узнать. Самый простой способ – упаковать файл любым быстром алгоритмом и сравнить упакованный и распакованный размер. Если отличается не очень сильно, то наверно все-таки файл запакован. Но этот метод плох тем, что “по-быстрому” упаковать не получится. Гораздо компактнее, быстрее и лучше высчитать энтропию секции кода, в которую указывает EntryPoint. Что такое энтропия и с чем её едят, Вы можете узнать, прочитав статью «Об упаковщиках в последний раз. Часть 2». Там очень даже хорошо расписано, как произвести сей несложный математический расчет. Но нам опять же не нужна теория – подавай исходный код. В общем, без чудесных FPU-инструкций математического сопроцессора, нам не обойтись. Ниже приведен исходный код функции, считающей энтропию блока данных и выносящую вердикт – упакован или нет. Однако этот вердикт будет наверняка, и поэтому особенно доверять ему не стоит.

CODE NOW!
proc IsDataCompressed,buff,size
; возвращает: "0" – если не сжато, "1" – если сжато, "-1" – если возникла ошибка
locals
ftable dd 256 dup (?)
fstate db 128 dup (?)
entropy dd ?
endl
push ebx
cmp [size],0
je @_idc_error
invoke IsBadReadPtr,[buff],[size]
or eax,eax
jne @_idc_error
lea eax,[ftable]
invoke RtlZeroMemory,eax,256*4
xor eax,eax
mov ebx,[buff]
mov ecx,[size]
lea edx,[ftable]
@@: mov al,[ebx]
inc dword [edx+eax*4]
inc ebx
loop @b
lea eax,[fstate]
fsave [eax]
xor eax,eax
mov ecx,256
fldz
@@: fild dword [edx+eax]
ftst
mov ebx,eax
fnstsw ax
and ah,$40
mov eax,ebx
jne @_idc_ft_zero
fild dword [size]
fdivp st1,st0
fld1
fld st1
fyl2x
fabs
push ecx
mov ecx,[edx+eax]
@_idc_add:
fadd st2,st0
loop @_idc_add
pop ecx
fxch st2
ffree st1
ffree st2
add eax,4
loop @b
jmp @f
@_idc_ft_zero:
ffree st0
fincstp
add eax,4
loop @b
@@: frndint
lea eax,[entropy]
fist dword [eax]
lea eax,[fstate]
frstor [eax]
mov eax,[size]
shl eax,3
xor edx,edx
mov ecx,[entropy]
or ecx,ecx
jne @f
inc ecx
@@: div ecx
xor ebx,ebx
cmp eax,2
mov eax,ebx
jge @f
shr ecx,3
cmp edx,ecx
jg @f
inc eax
@@: pop ebx
ret
@_idc_error:
pop ebx
xor eax,eax
dec eax
ret
endp



Теперь добавим еще эту проверку в наш вышеописанный код и в случае подозрения на упакованность
спросим пользователя о дальнейших действиях.

CODE NOW!
; сначала вычислим секцию кода (ту, в которую указывает EntryPoint)
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint]
; функция ImageRvaToSection полезна тем
; что позволяет получить по любому RVA, указатель на заголовок соответствующей секции
; эту процедуру можно легко написать и самому, но нам так быстрее, проще и компактнее
invoke ImageRvaToSection,esi,[pBackup],eax
or eax,eax
jne @f
; если тут, то EntryPoint указывает в никуда или в PE-заголовок
; но в любом случае нам такой файл не интересен, поскольку он либо не валидный
; либо уже пожат чем-либо
stdcall ShowError,__msg_epinhdr_error
jmp @_pack_error

@@:
; получим физическое местоположение секции кода в файле
mov edx,[eax+IMAGE_SECTION_HEADER.PointerToRawData]
; округлим физическое смещение до меньшего значения, кратного 0x200
; загрузчик операционной системы таким же образом получает PointerToRawData любой секции
and dx,$FE00
; а дальше получаем физический размер секции кода
mov ecx,[eax+IMAGE_SECTION_HEADER.SizeOfRawData]
add edx,[pBackup]
; проверим. пожато?
stdcall IsDataCompressed,edx,ecx
cmp eax,-1
je @f
or eax,eax
je @f
; спросим, продолжать ли упаковку если есть подозрение на упакованность
invoke
MessageBox,[hWnd],__msg_packed_error,__msg_caption,MB_ICONQUESTION+MB_YESNO+MB_DEFBUTTON2
cmp eax,IDYES
jne @_pack_error
; если будем все таки продолжать упаковку подозрительного файла
; то в таком случае принудительно создадим бэкап
or [Flags],CPE_MAKEBACKUP
@@:



Таким образом, теперь у нас все готово чтобы начать писать основной код нашего будущего пакера. А
именно: пересоберем заново файл и за одним упакуем его секции, а затем припишем к концу получившегося файла секцию с кодом нашего распаковщика и загрузчика. Ну что же, приступим.

Обработка и сжатие файла

Чтобы начать писать наш пакер дальше мы должны сначала определиться с библиотекой сжатия, которую будем использовать в своей программе. Как бы ни говорили и не опускали популярную библиотеку сжатия aPLib (http://www.ibsensoftware.com/), предпочтение в данном случае мы отдадим именно ей... Если не согласен, тогда вали отсюда и не читай эту статью... OOO... Ну а если серьезно, то в реальных разработках лучше использовать хотя бы LZMA или LZSS, а исходники распаковщика LZMA на ассемблере можно найти вместе с сорцами пакера Packman 1.0.

Теперь, определившись с движком упаковки нам можно начать писать... Стоп. А что писать? Мы ведь не
знаем, как оно работает. Правда? Принцип работы пакера прост. Вот краткая последовательность действий, необходимая для упаковки PE-файла, но конечно паковать тоже можно по-разному, например весь файл целиком, по секциям, все секции одним потоком и т.д. В данном случае мы выбираем сжатие по секциям. Т.е. каждая секция упаковывается по отдельности. Мы сжимаем физические данные секций на диске и пересобираем из этих сжатых секций новый PE-файл. А точнее, изменяем физический размер каждой обработанной нами секции (SizeOfRawData) на новый, сжатый, а также правим смещение в файле (PointerToRawData) у каждой секции, кроме
первой. Виртуальные адреса и размеры секций трогать не следует. Теперь вопрос. А как это все распакуется? Следовательно, нам нужно дописать к файлу код распаковщика. В нашем пакере мы оформим это дело в виде отдельной секции, хотя можно просто дописать к последней секции или засунуть код в заголовки или неиспользуемые места в секциях, в их конце. Также можно поизвращаться и засунуть загрузчик в начало файла, а в памяти расположить в конце образа, однако это будет являться извращением над PE-форматом. Если оформить это все в виде последовательности действий или некоторого алгоритма, то получим вот что:

1. проверяем файл
2. собственно сжатие данных файла, а именно:
a. цикл по всем секциям и их сжатие
b. правка полей SizeOfRawData и PointerToRawData заголовка каждой секции
c. тут же лучше выставить атрибуты секции на запись и чтение (Characteristics)
d. попутно пересчитаем размер образа, складывая и выравнивая VirtualSize секций
3. увеличим значение поля NumberOfSections на 1
4. добавим еще один заголовок для нашей секции
5. увеличим размер предварительно рассчитанного нами образа на значение SizeOfHeaders + VirtualSize
нашей новой секции загрузчика – и это всё будет окончательный новый размер образа (SizeOfImage)
6. дописываем код нашей секции
7. правим поле EntryPoint на начало секции загрузчика, чтобы он получил управление первым

Это конечно же очень сокращенный алгоритм простого упаковщика. В реальности это будет выглядеть чуть-чуть посложнее и могут возникнут некоторые проблемы. Например, на мой взгляд, лучше в данном случае не править PE-файл, а пересобрать его с нуля. Так намного легче и понятнее и поэтому мы так и поступим. Но все-таки на словах это все звучит не очень интересно, поэтому уже начнем писать код. Начнем с того, что нам понадобится выравнивать физические и виртуальные адреса до значения, кратного FileAlignment и SectionAlignment, но поскольку все-таки FileAlignment обычно 0x200, а SectionAlignment – 0x1000, напишем макрос или процедуру (кому как нравится) для выравнивания значений.

CODE NOW!
; выравнивает значение в eax до значения кратного 0x200 в большую сторону
proc Align_0x200
; -> EAX Value
or eax,eax
jne @f
retn
@@: test eax,$FFFFFE00
je @f
test ax,$01FF
jne @f
retn
@@: and ax,$FE00
add eax,$200
retn
endp

; выравнивает значение в eax до значения кратного 0x1000 в большую сторону
proc Align_0x1000
; -> EAX Value
or eax,eax
jne @f

retn
@@: test eax,$FFFFF000
je @f
test ax,$0FFF
jne @f
retn
@@: and ax,$F000
add eax,$1000
retn
endp



Тут же, сразу, пока не забыли, напишем еще одну процедуру, которая нам понадобится, для упаковки куска памяти, а для нас этот кусок памяти будет секция.

CODE NOW!
; функция вернет в eax новый (сжатый) размер данных
proc CompressSection,dest,source,size
push ecx esi edi
invoke _aP_workmem_size,[size]
mov esi,eax
invoke VirtualAlloc,0,esi,MEM_COMMIT,PAGE_READWRITE
mov edi,eax
invoke _aP_pack,[source],[dest],[size],edi,0,0
push eax
invoke VirtualFree,edi,esi,MEM_DECOMMIT
pop eax edi esi ecx
ret
endp



Ну а теперь начнем создавать новый PE-файл. Сначала опишем DOS-заглушку и будем вставлять её в
начало нового файла. Эта заглушка стандартна и в изменениях не нуждается. Сразу после неё должен идти PE-заголовок.

CODE NOW!
; это все мы опишем в новой секции ‘.data’
section ‘.data’ data readable writeable

DosStub:
db $4D,$5A,$80,$00,$01,$00,$00,$00,$04,$00,$10,$00,$FF,$FF,$00,$00
db $40,$01,$00,$00,$00,$00,$00,$00,$40,$00,$00,$00,$00,$00,$00,$00
db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$80,$00,$00,$00
db $0E,$1F,$BA,$0E,$00,$B4,$09,$CD,$21,$B8,$01,$4C,$CD,$21,$54,$68
db $69,$73,$20,$70,$72,$6F,$67,$72,$61,$6D,$20,$63,$61,$6E,$6E,$6F
db $74,$20,$62,$65,$20,$72,$75,$6E,$20,$69,$6E,$20,$44,$4F,$53,$20
db $6D,$6F,$64,$65,$2E,$0D,$0A,$24,$00,$00,$00,$00,$00,$00,$00,$00
@@: SizeOfDosStub = @b – DosStub



Далее продолжим писать код упаковщика.

CODE NOW!
; тут будут все основные веселости…
; продолжение для процедуры CompressFile; начало смотри выше
; ********************************************************************

mov edi,[pPacked]
; esi – указатель на PE-заголовок, открытого для чтения файла
; edi – указатель на PE-заголовок нашего нового файла

; заглушку править не надо, а вставлять как есть
; поэтому просто скопируем её без изменений
invoke RtlMoveMemory,edi,DosStub,SizeOfDosStub
add edi,SizeOfDosStub

; тут создадим некоторые поля PE-заголовка
mov [edi+IMAGE_NT_HEADERS.Signature],IMAGE_NT_SIGNATURE
; так надо O
mov [edi+IMAGE_NT_HEADERS.FileHeader.Machine],IMAGE_FILE_MACHINE_I386
movzx eax,[esi+IMAGE_NT_HEADERS.FileHeader.NumberOfSections]
inc ax ; увеличили NumberOfSections

mov [edi+IMAGE_NT_HEADERS.FileHeader.NumberOfSections],ax
; это тоже надо O
mov
[edi+IMAGE_NT_HEADERS.FileHeader.SizeOfOptionalHeader],IMAGE_SIZEOF_NT_OPTIONAL_HEADER
; добавим атрибуты файла типа xxx_STRIPPED
movzx eax,[esi+IMAGE_NT_HEADERS.FileHeader.Characteristics]
or ax,IMAGE_FILE_RELOCS_STRIPPED+IMAGE_FILE_DEBUG_STRIPPED
mov [edi+IMAGE_NT_HEADERS.FileHeader.Characteristics],ax
; и это надо бы O
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.Magic],IMAGE_NT_OPTIONAL_HDR_MAGIC
; базу модуля просто скопируем из старого файла
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.ImageBase]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.ImageBase],eax
; эти поля выставим принудительно, но заполним стандартными значениями
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.SectionAlignment],$1000
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.FileAlignment],$200
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.MajorOperatingSystemVersion],1
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.MajorSubsystemVersion],4
; это тоже просто скопируем из старого файла
movzx eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.Subsystem]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.Subsystem],ax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfStackReserve]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfStackReserve],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfStackCommit]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfStackCommit],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeapReserve]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeapReserve],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeapCommit]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeapCommit],eax
; это количество дата-директорий в файле, а их обычно 16
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.NumberOfRvaAndSizes],$10
; все остальные нужные нам поля тоже просто скопируем
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryExport.VirtualAddress]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryExport.VirtualAddress],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryExport.Size]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryExport.Size],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryResource.VirtualAddress]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryResource.VirtualAddress],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryResource.Size]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryResource.Size],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryTls.VirtualAddress]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryTls.VirtualAddress],eax
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryTls.Size]
mov [edi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryTls.Size],eax



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

CODE NOW!
; определим некоторые локальные переменные
xor eax,eax
; delta – смещение каждой следующей секции в файле
; после того как произойдет их смещение в файле. вначале delta естественно будет 0
mov [delta],eax
; это для RVA секций TLS, ресурсов и релоков соответственно
mov [tls_section],eax
mov [rsrc_section],eax
mov [reloc_section],eax
; переменная для подсчета нового значения размера образа (SizeOfImage)
mov [imagesize],eax

; получим RVA секции где находится TLS; в дальнейшем это нам понадобится
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryTls.VirtualAddress]
invoke ImageRvaToSection,esi,[pBackup],eax
or eax,eax
je @f

mov eax,[eax+IMAGE_SECTION_HEADER.VirtualAddress]
mov [tls_section],eax
@@:
; получим RVA секции где находятся ресурсы
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryResource.VirtualAddress]
invoke ImageRvaToSection,esi,[pBackup],eax
or eax,eax
je @f
mov eax,[eax+IMAGE_SECTION_HEADER.VirtualAddress]
mov [rsrc_section],eax
@@:
; получим RVA секции где находятся релоки
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryBaseReloc.VirtualAddress]
invoke ImageRvaToSection,esi,[pBackup],eax
or eax,eax
je @f
mov eax,[eax+IMAGE_SECTION_HEADER.VirtualAddress]
mov [reloc_section],eax
@@:
push edi
; edi – указатель на начало заголовков секций нового файла
; ebx – указатель на начало заголовков секций исходного файла
lea edi,[edi+IMAGE_SIZEOF_IMAGE_NT_HEADERS]
lea ebx,[esi+IMAGE_SIZEOF_IMAGE_NT_HEADERS]
; а ecx – количество секций
movzx ecx,[esi+IMAGE_NT_HEADERS.FileHeader.NumberOfSections]

так как может получиться смещение физических адресов секций,
; то сохраним также старый размер заголовков и потом подправим оффсеты если нужно
mov eax,[ebx+IMAGE_SECTION_HEADER.PointerToRawData]
mov [oldhdrsz],eax

; непосредственно цикл по заголовкам секций и их копирование в новый файл
@_sect_init_loop:
; выравниваем виртуальный размер секции, чтобы было красиво
mov eax,[ebx+IMAGE_SECTION_HEADER.VirtualSize]
call Align_0x1000
mov [edi+IMAGE_SECTION_HEADER.VirtualSize],eax
; ведем пересчет размера образа, складывая выровненные виртуальные размеры секций
add [imagesize],eax

; физический размер секции не трогаем
mov eax,[ebx+IMAGE_SECTION_HEADER.SizeOfRawData]
mov [edi+IMAGE_SECTION_HEADER.SizeOfRawData],eax

; смещение секции (физическое положение) в файле просто копируем
; но если произошло смещение секций вследствие удаления ненужных данных
; например если мы удалили одну секцию с релоками
; то все последующие секции окажутся смещены на физический размер удаленной секции
; поэтому определим переменную delta, которая будет содержать значение
; на которое произошло смещение PointerToRawData последующих секций
; в начале delta = 0, поэтому изменения PointerToRawData при delta = 0 не будет
mov eax,[ebx+IMAGE_SECTION_HEADER.PointerToRawData]
or eax,eax
je @f
sub eax,[delta]
@@: mov [edi+IMAGE_SECTION_HEADER.PointerToRawData],eax

; атрибуты секции ставим на запись и чтение
mov eax,[ebx+IMAGE_SECTION_HEADER.Characteristics]
or eax,IMAGE_SCN_MEM_READ+IMAGE_SCN_MEM_WRITE
and eax,not(IMAGE_SCN_MEM_SHARED)
mov [edi+IMAGE_SECTION_HEADER.Characteristics],eax

; виртуальные адреса секций просто скопируем
mov eax,[ebx+IMAGE_SECTION_HEADER.VirtualAddress]
mov [edi+IMAGE_SECTION_HEADER.VirtualAddress],eax

; а это не секция, содержащая ресурсы?

cmp eax,[rsrc_section]
jne @f
; если это секция с ресурсами, то дадим ей имя ’.rsrc’
mov [edi+IMAGE_SECTION_HEADER.Name],’.’
mov dword [edi+IMAGE_SECTION_HEADER.Name+1],’rsrc’
@@:
; а это не секция содержащая релоки?
cmp eax,[reloc_section]
jne @f
; если эта секция содержит релоки, то можно её удалить из файла
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryBaseReloc.Size]
call Align_0x200
cmp eax,[edi+IMAGE_SECTION_HEADER.SizeOfRawData]
jne @f
mov eax,[edi+IMAGE_SECTION_HEADER.SizeOfRawData]
; при удалении секции последующие за ней будут смещены
; поэтому delta будет содержать это смещение (размер удаляемой секции)
mov [delta],eax
xor eax,eax
mov [edi+IMAGE_SECTION_HEADER.SizeOfRawData],eax
@@:
add ebx,IMAGE_SIZEOF_SECTION_HEADER
add edi,IMAGE_SIZEOF_SECTION_HEADER
loop @_sect_init_loop
; цикл копирования секций закончен

; теперь нам следует добавить также заголовок для нашей секции загрузчика
mov [edi+IMAGE_SECTION_HEADER.Name],’.’
mov dword [edi+IMAGE_SECTION_HEADER.Name+1],’text’ ; пусть имя секции будет ’.text’
; а её физический размер равен размеру загрузчика
mov eax,loader_size
mov [edi+IMAGE_SECTION_HEADER.SizeOfRawData],eax
; виртуальный же размер новой секции – это выровненный до 0x1000 физический
call Align_0x1000
mov [edi+IMAGE_SECTION_HEADER.VirtualSize],eax
; увеличим новый размер образа еще на VirtualSize новой секции
add [imagesize],eax
; физическое смещение новой секции в файле можно вычислить таким образом
; PointerToRawData последней секции + выровненный SizeOfRawData последней секции
mov eax,[edi-IMAGE_SIZEOF_SECTION_HEADER+IMAGE_SECTION_HEADER.PointerToRawData]
and ax,$FE00
add eax,[edi-IMAGE_SIZEOF_SECTION_HEADER+IMAGE_SECTION_HEADER.SizeOfRawData]
call Align_0x200
mov [edi+IMAGE_SECTION_HEADER.PointerToRawData],eax
; виртуальный адрес новой секции вычисляется как VirtualAddress последней секции
; + выровненный VirtualSize последней секции
mov eax,[edi-IMAGE_SIZEOF_SECTION_HEADER+IMAGE_SECTION_HEADER.VirtualAddress]
add eax,[edi-IMAGE_SIZEOF_SECTION_HEADER+IMAGE_SECTION_HEADER.VirtualSize]
mov [edi+IMAGE_SECTION_HEADER.VirtualAddress],eax
; атрибуты секции также – чтение и запись
mov [edi+IMAGE_SECTION_HEADER.Characteristics],IMAGE_SCN_MEM_READ+IMAGE_CN_MEM_WRITE

; точка входа (EntryPoint) в файле должна указывать на секцию загрузчика
; иначе говоря, новое значение EntryPoint = VirtualAddress новой секции
mov edx,[esp]
mov [edx+IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint],eax
; теперь можно определить новый размер заголовков и образа
; новый размер заголовков вычисляется как полученный размер всех заголовков (DOS,PE)
; и выравнивается до значения кратного 0x200
lea eax,[edi+IMAGE_SIZEOF_SECTION_HEADER]
sub eax,[pPacked]
call Align_0x200
; новый размер образа теперь можно получить добавив к уже предварительно рассчитанному
; SizeOfImage полученный размер заголовков и выровнив сумму до значения, кратного 0x1000
mov [edx+IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeaders],eax
call Align_0x1000
add eax,[imagesize]
mov [edx+IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage],eax


; теперь, поскольку при добавлении новой секции в файл
; размер PE-заголовка мог измениться, то лучше всего заного пересчитать
; все указатели на физические данные (PointerToRawData) каждой секции
; а для этого нам необходимо узнать смещение на которое изменится PointerToRawData секций
; его просто получить если вычесть из нового SizeOfHeaders старый размер заголовков
mov ecx,[oldhdrsz]
sub ecx,[edx+IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeaders]
mov [delta],ecx

mov edi,[esp]

; второй цикл по секциям PE-файла
; здесь будет происходить непосредственно упаковка данных секций
lea edi,[edi+IMAGE_SIZEOF_IMAGE_NT_HEADERS]
lea ebx,[esi+IMAGE_SIZEOF_IMAGE_NT_HEADERS]
movzx ecx,[esi+IMAGE_NT_HEADERS.FileHeader.NumberOfSections]
; edi – указатель на начало заголовков секций нового файла
; ebx – указатель на начало заголовков секций исходного файла
; ecx – количество секций

@_sect_pack_loop:
mov edx,[ebx+IMAGE_SECTION_HEADER.PointerToRawData]
add edx,[pBackup]
mov eax,[edi+IMAGE_SECTION_HEADER.PointerToRawData]
or eax,eax
je @f
sub eax,[delta] ; уменьшаем PointerToRawData на значение delta
@@: mov [edi+IMAGE_SECTION_HEADER.PointerToRawData],eax
add eax,[pPacked]
push eax
mov eax,[edi+IMAGE_SECTION_HEADER.VirtualAddress]
; это секция, содержащая TLS? если так, то не будем её упаковывать
cmp eax,[tls_section]
je @_copy_section ; TLS секцию просто скопируем в файл без сжатия
; это секция, содержащая ресурсы? если так, то не будем её упаковывать
cmp eax,[rsrc_section]
jne @_pack_section ; если секция не содержит ресурсы, то сожмем её
; если все же было принудительно выставлено сжатие всей секции ресурсов, то будем сжимать
test [Flags],CPE_PACKALLRSRC
je @_copy_section
; упаковка секции и копирование её в новый файл
;-----------------------------------------------
@_pack_section:
pop eax
stdcall CompressSection,eax,edx,[edi+IMAGE_SECTION_HEADER.SizeOfRawData]
cmp eax,APLIB_ERROR
je @_pack_error
call Align_0x200
mov edx,[edi+IMAGE_SECTION_HEADER.SizeOfRawData]
mov [edi+IMAGE_SECTION_HEADER.SizeOfRawData],eax
xchg eax,edx
call Align_0x200
sub eax,edx
add [delta],eax
;-----------------------------------------------
; чтобы знать распаковщику, что секция была подвергнута сжатию мы “пометим” её
; и возьмем для этого неиспользуемое в обычных экзешниках поле заголовка
; таким образом, наш загрузчик впоследствии сможет легко опознать, что секция была сжата
or [edi+IMAGE_SECTION_HEADER.NumberOfLinenumbers],$FFFF ; флаг сжатия секции
add ebx,IMAGE_SIZEOF_SECTION_HEADER
add edi,IMAGE_SIZEOF_SECTION_HEADER
loop @_sect_pack_loop
jmp @_done_packing
; простое копирование секции в новый файл как есть, без сжатия
;-----------------------------------------------
@_copy_section:
pop eax
push ecx
invoke RtlMoveMemory,eax,edx,[edi+IMAGE_SECTION_HEADER.SizeOfRawData]

pop ecx
add ebx,IMAGE_SIZEOF_SECTION_HEADER
add edi,IMAGE_SIZEOF_SECTION_HEADER
loop @_sect_pack_loop
;-----------------------------------------------
@_done_packing:
pop eax

; теперь подправим также заголовок новой секции загрузчика
mov eax,[edi+IMAGE_SECTION_HEADER.PointerToRawData]
or eax,eax
je @f
sub eax,[delta]
@@: mov [edi+IMAGE_SECTION_HEADER.PointerToRawData],eax
add eax,[pPacked]
push eax
; и скопируем данные загрузчика (последней секции) в новый упакованный файл
; loader_proc – это процедура, содержащая код загрузчика
invoke RtlMoveMemory,eax,loader_proc,loader_size
pop eax
add eax,loader_size

; определим реальный размер исходного файла без оверлея
; в PE-файле это можно высчитать, сложив PointerToRawData+SizeOfRawData последней секции
mov ecx,[filesize]
mov edx,[ebx-IMAGE_SIZEOF_SECTION_HEADER+IMAGE_SECTION_HEADER.PointerToRawData]
add edx,[ebx-IMAGE_SIZEOF_SECTION_HEADER+IMAGE_SECTION_HEADER.SizeOfRawData]
sub ecx,edx
add edx,[pBackup]

; теперь определим также реальный размер получившегося сжатого файла (без оверлея)
; и установим новый размер файла
mov ebx,eax
sub ebx,[pPacked]
mov [filesize],ebx

; если в параметрах функции CompressFile был указан флаг CPE_SAVEOVERLAY
; то сохраним в новый файл и оверлей, если он присутствует
test [Flags],CPE_SAVEOVERLAY
je @f
add [filesize],ecx
invoke RtlMoveMemory,eax,edx,ecx
@@:
; *********************************************************************



Теперь в принципе все. Данный код будет упаковывать все секции в файле, за исключением секции, содержащей TLS и секции ресурсов. Однако все же стоит пояснить некоторые моменты, которые возможно непонятны читателю. Во-первых, что такое TLS, которое неоднократно упоминалось и почему в данном случае мы не трогаем секцию, содержащую TLS или ресурсы. TLS (Thread Local Storage) - локальная область данных цепочек (нитей), представляющая особый блок данных. Каждая цепочка получает собственный блок при своем создании. Они предназначены для того, чтобы потоки в приложении могли временно хранить свои данные. В любом случае, они обычно (но не обязательно) оформляются в виде отдельной секции, и трогать нам такую секцию в
простейшем случае попросту не целесообразно и не удобно. С ресурсами все гораздо проще. Если мы упакуем секцию ресурсов целиком, то лишимся возможности лицезреть в PE-файле иконки и информацию о его версии, а также потеряем то, ради чего в ресурсы добавляют Manifest – в Windows XP по прежнему будут уныло смотреться кнопки, меню и другие элементы интерфейса программы, как в старых версиях Windows. Однако может быть в файле и не нужны иконки, манифесты и информация о версии (или их попросту нет) и в этом случае лучше спросить пользователя желает ли он упаковать ресурсы целиком. Конечно этот подход не очень удачный. Нормальные пакеры упаковывают ресурсы в файле по-умному, а точнее не жмут эти самые RT_ICON, RT_GROUP_ICON, RT_MANIFEST и т.д. Они сжимают ресурсы выборочно. Но в нашем простейшем упаковщике
данная ”фича” будет ни к чему – для того, чтобы понять принцип работы упаковщика и написать его самому будет и этого достаточно. И поскольку мы еще всего-навсего на полпути к окончанию нашей работы, то пора бы и продолжить задуманное. Теперь, когда вроде бы файл упакован, то все просто – написать код распаковщика и загрузчика не трудно подумает читатель. Но не тут то было. Все что мы уже придумали и реализовали - это весьма примитивные действия над PE-форматом и сильного удовлетворения от них не испытываешь. Самое интересное – написание загрузчика, поэтому приступим.

Написание загрузчика

Что мы должны сделать, для того чтобы наша сжатая программа запустилась на выполнение и нормально
заработала? Наверное, стоит сначала расписать алгоритм работы простейшего загрузчика для нашего пакера, а уже затем продолжить писать.

Во-первых, нужно распаковать все данные. В нашем случае будет опять цикл по всем секциям и их распаковка. Здесь уже нет привязки к физическим адресам, и секция будет распаковываться сразу в памяти. А точнее процедура распаковки данных, сжатых aPLib (_aP_depack) требует всего 2 параметра: указатель на сжатые данные (ImageBase + VirtualAddress секции) и указатель, куда эти данные следует распаковать. Поэтому распаковка данных в памяти будет идти так: в цикле по секциям мы должны определить по флагу, который сами выставляли в заголовке секции, если оная в свое время была отправлена на компрессию к aPLib. В качестве флага мы использовали поле NumberOfLinenumbers, но могли использовать еще, например, поля PointerToRelocations,
PointerToLinenumbers или NumberOfRelocations. Если секция сжата, то мы должны выделить временный буфер с помощью функции VirtualAlloc и скопировать сжатую секцию в этот буфер. Затем вызвать процедуру распаковки _aP_depack и передать её в качестве параметров адрес буфера со сжатыми данными и виртуальный адрес секции для помещения туда распакованных данных. Всё. На этом процесс распаковки файла в памяти будет закончен и... программа все равно работать не будет O... Все дело в том, что опять же в простейшем случае нам предстоит еще заполнить IAT адресами импортируемых нашей программой функций. Что такое IAT? Это Import Address Table, а по-русски – таблица адресов импорта. Данная таблица представляет собой массив из 4-байтных ячеек
переменной длины, в которых находятся адреса внешних импортируемых функций, находящихся в библиотеках (и не только). Когда в программе происходит вызов, например MessageBoxA, то внешне это выглядит примерно так: CALL [00403426], где 00403426 – это адрес одной из ячеек IAT, в которой хранится адрес функции MessageBoxA. Поэтому заполнять IAT адресами жизненно необходимо, иначе программа, скорее всего, не будет работать нормально и упадет с исключением, типа “Access violation at address 0x00000000”, так как вначале IAT будет заполнена нулями. При “обычной” загрузке приложений эту функцию берет на себя системный загрузчик, а получает он эту информацию из таблицы импорта, находящуюся в файле. Но поскольку мы упаковали код, данные и еще бог знает что (за исключением ресурсов и TLS, кто еще помнит), то тут нам и придется все делать “ручками” и восстанавливать IAT. Для начала подробно изучим структуру таблицы импорта. Она представляет из себя следующую структуру:

CODE NOW!
struct IMAGE_IMPORT_DIRECTORY_ENTRY
ImportLookUp dd ?
TimeDateStamp dd ?
ForwardChain dd ?
NameRVA dd ?
AddresTableRVA dd ?
ends



Точнее это не есть структура таблицы импорта, а только её вхождение. Таких вхождений может быть
несколько, по одному на один подключаемый модуль. Например, у нас программа использует KERNEL32.DLL, USER32.DLL, то вхождений, т.е. таких табличек может быть две, но может быть и 3 и 4 вхождения (по несколько на один модуль, но это для нас уже не имеет значения). Все кончается тем, что в конце идет такая же табличка из 5 DWORD’ов, заполненная нулями. Она и поясняет, что вхождения таблицы импорта закончились. ImportLookUp – относительный виртуальный адрес (RVA) таблицы, содержащей имена импортируемых функций. Иначе говоря, он указывает на массив 4-байтных ячеек (адресов на ASCIIZ-строку, содержащую имя импортируемой функции). Однако тут стоит заметить, что имя идет не сразу в виде строки, а после 2-байтного префикса – так называемого Hint’а. Т.е. наглядно эту таблицу можно отобразить следующим образом -> смотрите файл scheme.1.gif.

Последняя запись в ImportLookUp Table, равная нулю означает то, что таблица закончилась.

AddresTableRVA – это поле содержит RVA таблицы адресов импорта (IAT), той самой, про которую мы
говорили выше. Она представляет собой таблицу, связанную с ImportLookUp Table неявным образом. Т.е. мы берем имя функции из ImportLookUp Table, находим её адрес с помощью API-функции GetProcAddress или другим способом, а затем помещаем этот адрес в соответствующую ячейку IAT. Также очень часто поле ImportLookUp в структуре IMAGE_IMPORT_DIRECTORY_ENTRY имеет значение 0 (например, такое можно встретить в Delphi-приложениях) или указывает на одну и ту же таблицу что и AddresTableRVA. Это означает то, что ImportLookUp Table и IAT физически представляют одну и ту же таблицу и изначально в IAT находятся указатели на имена функции, вместо реальных адресов установленных линкером. Это можно увидеть на рисунке, где показана структура импорта программы, написанной на Delphi 7. ImportLookUp чаще всего обозначают как OriginalFirstThunk, а AddressTable как FirstThunk, но мне нравится первое обозначение. Еще стоит отметить, что
импортирование функции может быть по ординалу, т.е. ImportLookUp Table может содержать не RVA, ссылающиеся на имена, а ординал. Это можно определить по 31 биту, установленному в 1. В таком случае младшее слово (2 байта) и есть ординал импортируемой функции -> смотрите файл Imports.gif.

NameRVA – это адрес ASCIIZ-строки, содержащей имя библиотеки, из которой происходит импортирование необходимых программе функций.

TimeDateStamp – является довольно интересным полем. Если TimeDateStamp = 0, то загрузчик
операционной системы обрабатывает импорт “как обычно”. Если же TimeDateStamp = -1, то в таком случае идет привязка через так называемые BoundImport’ы. Однако для нас всё это не имеет никакого практического значения, так как настройку импорта мы будем писать сами.

ForwardChain – 32-битный индекс форвардера. позволяет передавать импорт в другие библиотеки, но обычно не используется и имеет значение 0 или -1 и для нас интереса также не представляет. В принципе, таким образом, мы разобрали структуру таблицы импорта в PE-файле, но для нас главной задачей является, прежде всего, написание кода, восстанавливающего адреса в IAT. Нижеприведенный код позволяет реализовать следующее.

CODE NOW!
mov ebp,0x400000 ; сохраним в ebp базу нашего модуля (ImageBase)
mov esi,0x3000 ; тут мы должны положить в esi RVA таблицы импорта
add esi,ebp
; цикл по структурам IMAGE_IMPORT_DIRECTORY_ENTRY, пока NameRVA <> 0
; можно не проверять все 5 DWORD’ов на 0, так как если уже NameRVA = 0
; то скорее всего и остальные тоже 0
@_next_import_entry:
mov ecx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.NameRVA]
or ecx,ecx
je @_done
add ecx,ebp
mov edi,ecx
push edi
; получим хэндл модуля
call dword [ebx+_get_module_handle-loader_proc] ; call GetModuleHandle
or eax,eax
jne @f

push edi
; если модуль еще не загружен, то загрузим его
call dword [ebx+_load_library-loader_proc] ; call LoadLibraryA
@@: or eax,eax
jne @f
push MB_ICONERROR
push 0
push edi
push HWND_DESKTOP
; если LoadLibrary не “прошла”, то выдадим ошибку что мол такая-то DLL не найдена
call dword [ebx+_message_box-loader_proc] ; call MessageBoxA
push 126
; и в этом случае завершим процесс с кодом 126 (ERROR_MOD_NOT_FOUND)
call dword [ebx+_exit_process-loader_proc] ; call ExitProcess
@@: mov edi,eax
mov ecx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.ImportLookUp]
or ecx,ecx
jne @f
mov ecx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.AddresTableRVA]
@@: jecxz @f
add ecx,ebp
; теперь ecx – указатель на табличку ImportLookUp
; а edx - указатель на IAT
mov edx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.AddresTableRVA]
add edx,ebp
@_process_iat:
mov eax,[ecx]
or eax,eax
je @f
; определим по ординалу ли импортируется функция (старший бит = 1)
test eax,$80000000
je @_by_name
and eax,$0000FFFF
jmp @_iat_common
@_by_name:
add eax,ebp
add eax,2
@_iat_common:
push ecx edx
push eax
push edi
; получаем адрес нужной функции с помощью GetProcAddress
call dword [ebx+_get_proc_address-loader_proc] ; call GetProcAddress
pop edx ecx
mov [edx],eax
add ecx,4
add edx,4
jmp @_process_iat
@@: add esi,sizeof.IMAGE_IMPORT_DIRECTORY_ENTRY
jmp @_next_import_entry
@_done:



Этот небольшой код позволяет заполнить IAT. Он несколько упрощен, по сравнению с системным загрузчиком, но вполне работоспособен на большинстве файлов, а точнее на всех, которые я испытывал. Тут все понятно, за исключением, пожалуй, одного факта – вызовы API-функций происходят несколько “странным” образом. Дело в том, что для того чтобы мы в загрузчике тоже могли использовать API, нам надо объявить свою таблицу импорта, в которой будут находиться необходимые нам функции, а точнее, нам нужны только их адреса, как и любой другой программе. Поэтому нам необходимо писать загрузчик в шелл-код стиле, точно также, как пишутся Win32-вирусы. Смысл оного заключается в том, что мы оперируем не абсолютными адресами, а относительными. Т.е. Вначале нашего загрузчика напишем такой код:

CODE NOW!
loader_proc:
call @f
@@: pop ebx
lea ebx,[ebx-@b+loader_proc]



Это будет означать, что мы получим в ebx адрес loader_proc или адрес начала кода нашего загрузчика.
Однако такой прием достаточно распространен и в особых комментариях не нуждается. Теперь можно привести

полный листинг загрузчика, который будет распаковывать секции, обрабатывать импорты и передавать управление основной программе.

CODE NOW!
; ################################# loader here #################################

loader_proc:
pushad
call @f
@@: pop ebx
lea ebx,[ebx-@b+loader_proc]
push 0
call dword [ebx+_get_module_handle-loader_proc]
mov ebp,eax
mov edi,[ebp+IMAGE_DOS_HEADER._lfanew]
lea esi,[ebp+edi]
lea edi,[esi+IMAGE_SIZEOF_IMAGE_NT_HEADERS]
mov eax,[esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryResource.VirtualAddress]
or eax,eax
jne @f
lea eax,[ebx+_dummy_var-loader_proc]
push eax
push PAGE_READWRITE
push $1000
push ebp
call dword [ebx+_virtual_protect-loader_proc]
or eax,eax
je @f
db $B8; mov eax,xxxxxxxx
_resource_rva:
dd ?
mov [esi+IMAGE_NT_HEADERS.OptionalHeader.DataDirectoryResource.VirtualAddress],eax
@@: movzx esi,[esi+IMAGE_NT_HEADERS.FileHeader.NumberOfSections]
dec esi
@_ldr_unpack_loop:
mov eax,[edi+IMAGE_SECTION_HEADER.SizeOfRawData]
or eax,eax
je @f
movzx eax,[edi+IMAGE_SECTION_HEADER.NumberOfLinenumbers] ; compressed flag
or eax,eax
je @f
push PAGE_READWRITE
push MEM_COMMIT
push [edi+IMAGE_SECTION_HEADER.SizeOfRawData]
push 0
call dword [ebx+_virtual_alloc-loader_proc]
push eax
mov edx,eax
mov eax,ebp
add eax,[edi+IMAGE_SECTION_HEADER.VirtualAddress]
mov ecx,[edi+IMAGE_SECTION_HEADER.SizeOfRawData]
call _move
mov eax,[esp]
mov edx,ebp
add edx,[edi+IMAGE_SECTION_HEADER.VirtualAddress]
call _aP_depack
pop eax
push MEM_DECOMMIT
push [edi+IMAGE_SECTION_HEADER.SizeOfRawData]
push eax
call dword [ebx+_virtual_free-loader_proc]
@@: add edi,IMAGE_SIZEOF_SECTION_HEADER
dec esi
jne @_ldr_unpack_loop
db $BE ; mov esi,xxxxxxxx
_orig_import_rva:
dd ?
add esi,ebp
@_next_import_entry:
mov ecx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.NameRVA]
or ecx,ecx
je @_done
add ecx,ebp
mov edi,ecx
push edi
call dword [ebx+_get_module_handle-loader_proc]
or eax,eax
jne @f
push edi
call dword [ebx+_load_library-loader_proc]
@@: or eax,eax
jne @f
push MB_ICONERROR
push 0
push edi
push HWND_DESKTOP
call dword [ebx+_message_box-loader_proc]
push 126
call dword [ebx+_exit_process-loader_proc]
@@: mov edi,eax
mov ecx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.ImportLookUp]
or ecx,ecx
jne @f
mov ecx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.AddresTableRVA]
@@: jecxz @f
add ecx,ebp
mov edx,[esi+IMAGE_IMPORT_DIRECTORY_ENTRY.AddresTableRVA]
add edx,ebp
@_process_iat:
mov eax,[ecx]
or eax,eax
je @f
test eax,$80000000
je @_by_name
and eax,$0000FFFF
jmp @_iat_common
@_by_name:
add eax,ebp
add eax,2
@_iat_common:
push ecx edx
push eax
push edi
call dword [ebx+_get_proc_address-loader_proc]
pop edx ecx
mov [edx],eax
add ecx,4
add edx,4
jmp @_process_iat
@@: add esi,sizeof.IMAGE_IMPORT_DIRECTORY_ENTRY
jmp @_next_import_entry
@_done:
popad
db $68 ; push xxxxxxxx
loader_oep_addr:
dd ?
ret
; *****************************************
; процедура, ”по-быстрому” копирующая память
_move:
; -> EAX Pointer to source
; -> EDX Pointer to destination
; -> ECX Count
push esi edi
mov esi,eax
mov edi,edx
mov eax,ecx
cmp edi,esi
ja @@down

je @@exit
sar ecx,2
js @@exit
rep movsd
mov ecx,eax
and ecx,3
rep movsb
jmp @@exit
@@down:
lea esi,[esi+ecx-4]
lea edi,[edi+ecx-4]
sar ecx,2
js @@exit
std
rep movsd
mov ecx,eax
and ecx,3
add esi,3
add edi,3
rep movsb
cld
@@exit:
pop edi esi
ret
; *****************************************
; процедура распаковки данных, сжатых aPLib
_aP_depack:
; -> EAX Pointer to source
; -> EDX Pointer to destination
push ebp ebx esi edi
mov esi,eax
mov edi,edx
cld
mov dl,$80
xor ebx,ebx
@_literal:
movsb
mov bl,2
@_nexttag:
call @_getbit
jnc @_literal
xor ecx,ecx
call @_getbit
jnc @_codepair
xor eax,eax
call @_getbit
jnc @_shortmatch
mov bl,2
inc ecx
mov al,10h
@_getmorebits:
call @_getbit
adc al,al
jnc @_getmorebits
jnz @_domatch
stosb
jmp short @_nexttag
@_codepair:
call @_getgamma_no_ecx
sub ecx,ebx
jnz @_normalcodepair
call @_getgamma
jmp short @_domatch_lastpos
@_shortmatch:
lodsb
shr eax,1
jz @_donedepacking
adc ecx,ecx
jmp short @_domatch_with_2inc

@_normalcodepair:
xchg eax,ecx
dec eax
shl eax,8
lodsb
call @_getgamma
cmp eax,32000
jae @_domatch_with_2inc
cmp ah,5
jae @_domatch_with_inc
cmp eax,$7F
ja @_domatch_new_lastpos
@_domatch_with_2inc:
inc ecx
@_domatch_with_inc:
inc ecx
@_domatch_new_lastpos:
xchg eax,ebp
@_domatch_lastpos:
mov eax,ebp
mov bl,1
@_domatch:
push esi
mov esi,edi
sub esi,eax
rep movsb
pop esi
jmp short @_nexttag
@_getbit:
add dl,dl
jnz @_stillbitsleft
mov dl,[esi]
inc esi
adc dl,dl
@_stillbitsleft:
ret
@_getgamma:
xor ecx,ecx
@_getgamma_no_ecx:
inc ecx
@_getgammaloop:
call @_getbit
adc ecx,ecx
call @_getbit
jc @_getgammaloop
ret
@_donedepacking:
pop edi esi ebx ebp
ret
; *****************************************
_tls_index_var:
dd ?
_dummy_var:
dd ?
; *****************************************
; здесь расположена таблица импорта, построенная вручную
; и необходимая нам для работы с некоторыми API-функциями
_import_table:
dd 0,0,0
_kernel32_name_rva:
dd 0
_kernel32_iat_rva:
dd 0
dd 0,0,0
_user32_name_rva:
dd 0
_user32_iat_rva:
dd 0
dd 0,0,0,0,0

;-----------------------------------
; IAT для KERNEL32.DLL
_kernel32_iat:
_load_library:
dd ?
_get_module_handle:
dd ?
_get_proc_address:
dd ?
_virtual_alloc:
dd ?
_virtual_protect:
dd ?
_virtual_free:
dd ?
_exit_process:
dd ?
dd 0
; IAT для USER32.DLL
_user32_iat:
_message_box:
dd ?
dd 0
;-----------------------------------
; имена импортируемых нами функций
_kernel32_name:
db ’KERNEL32.DLL’,0
_load_library_name:
dw 0
db ’LoadLibraryA’,0,0
_get_module_handle_name:
dw 0
db ’GetModuleHandleA’,0,0
_get_proc_address_name:
dw 0
db ’GetProcAddress’,0,0
_virtual_alloc_name:
dw 0
db ’VirtualAlloc’,0,0
_virtual_protect_name:
dw 0
db ’VirtualProtect’,0,0
_virtual_free_name:
dw 0
db ’VirtualFree’,0
_exit_process_name:
dw 0
db ’ExitProcess’,0
_user32_name:
db ’USER32.DLL’,0
_message_box_name:
dw 0
db ’MessageBoxA’,0
;-----------------------------------
@@: import_size = @b - _import_table
; *****************************************
@@: loader_size = @b - loader_proc

; ####################################################################################



Вот в принципе и все! Теперь можно попробовать получившийся упаковщик на деле. К статье прилагается мой недавний open-source проект – SimplePack. Это простой упаковщик EXE-файлов, который был написан специально для того, чтобы можно было использовать его в качестве простейшего примера упаковки исполняемых файлов. Конечно, он нуждается в доработке, но уже со стороны читателя. Ведь тебя это всё заинтересовало? Не так ли?! Может быть кто-нибудь, прочитав данную статью и напишет новый пакер, а может и нет. Но всё равно, дерзайте!

Вместо заключения

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

Вообще-то я расскажу в общих чертах, чем отличается упаковка DLL-файла от обычного EXE. Чтобы ответить на этот вопрос нужно ответить на вопрос – чем отличается DLL от EXE. В первую очередь возможностью экспортировать функции, но это не обязательно, так как в EXE-файлах тоже могут находиться экспорты. С точки зрения упаковки DLL, для нас будут важны только несколько основных отличий.

Во-первых, точка входа в DLL и EXE несколько отличается. В обычных “экзешниках” точка входа в программу представляет собой процедуру EntryPoint(). Т.е. тут без комментариев. В DLL же файле точка входа представляет собой процедуру DllEntryPoint(hInstance:HMODULE,dwReason:DWORD,lpReserved:DWORD). Т.е.
загрузчик кладет в стек еще 3 параметра; важными для нас из них являются hInstance – реальный базовый адрес нашего модуля в памяти и dwReason – определяющая событие, которое произошло когда была вызвана DllEntryPoint. Так как DllEntryPoint вызывается каждый раз, когда библиотека была приаттачена\деаттачена к процессу или потоку, то каждый раз производя одни и те же действия мы попросту убьём процесс. А точнее представьте себе, что мы распаковали данные, настроили импорты, сделали еще бог знает что и затем нашу DLL выгружают. Естественно снова загрузчик передает управление DllEntryPoint и код нашего загрузчика снова начинает распаковывать, восстанавливать импорты и опять делать бог знает что. В общем, здесь ,скорее всего,
мы получим большой и хороший Access violation или еще какую-нибудь ошибку. Чтобы такого не произошло, то нужно просто сравнивать в самом начале DllEntryPoint что dwReason = DLL_PROCESS_ATTACH и если это не так, то возвращать управление загрузчику системы и не предпринимать тут никаких самодеятельных операций. Во-вторых, поскольку все-таки мы упаковываем все секции в файле, акромя TLS и ресурсов, то в список исключений еще можно добавить и секцию с экспортами. Или еще как вариант, можно секцию с экспортами пожать, но в таком случае воссоздать таблицу экспорта где-нибудь в другом месте, например в секции нашего загрузчика.

В третьих. DLL может грузиться не только по адресу, который указан в ImageBase PE-заголовка. В DLL-
файлах есть такие понятия как предпочитаемый и реальный базовый адрес. Соответственно, чтобы было возможно скорректировать абсолютные адреса (которые будут неверны, если DLL будет загружена по другому адресу в памяти), используемые в файле, была придумана такая вещь, как релоки. Расписывать что они из себя представляют и с чем их едят я не буду – иди читай документацию к PE-формату. Однако для усвоения полученного материала все же приведу код, который умеет настраивать релоки в PE-файле. Однако если ты все же еще не удосужился прочитать справку по таблице релоков, то, скорее всего и ничего не поймешь, для чего это все нужно и как работает.

CODE NOW!
mov edi,xxxxxxxx ; нужно положить в edi предпочитаемую ImageBase
mov esi,xxxxxxxx ; а в edi RVA таблицы релоков
mov ecx,xxxxxxxx ; и также в ecx – размер таблицы релоков (размер директории)
mov ebx,xxxxxxxx ; ebx – реальный базовый адрес (hInstance) модуля
or esi,esi
je @_ldr_reloc_skip
jecxz @_ldr_reloc_skip
add esi,ebx ; reloc va
sub edi,ebx ; delta
@_ldr_reloc_next_page:
mov eax,[esi+IMAGE_FIXUPS_DIRECTORY.PageRVA]
or eax,eax
je @_ldr_reloc_skip
add eax,ebx
push esi ecx
mov ecx,[esi+IMAGE_FIXUPS_DIRECTORY.BlockSize]
sub ecx,8
add esi,8
xor edx,edx
@@: mov dx,[esi]
or dx,dx
je @f
and dx,$0FFF
sub [eax+edx],edi
add esi,2
sub ecx,2
jne @b
@@: pop ecx esi
sub ecx,[esi+IMAGE_FIXUPS_DIRECTORY.BlockSize]
add esi,[esi+IMAGE_FIXUPS_DIRECTORY.BlockSize]

or ecx,ecx
jne @_ldr_reloc_next_page
@_ldr_reloc_skip:



Всё. Данный код хотя и не учитывает тип настройки, типа HIGHGLOW(3), ABSOLUTE(0) и т.д., но все равно нормально работает. Если читатель захочет добавить еще и упаковку DLL к своему будущему пакеру, то ему нужно все же придерживаться, как минимум, еще и этих трех вышесказанных правил. На этом статью можно считать законченной и удачного всем вам пакерописания. O O O

Отдельно хочу выразить благодарность всем членам команды TEAM-X, особенно 6aHguT’у и также Guru.eXe, за то, что они отнеслись к моим разработкам, в частности к этой статье и пакеру, так или иначе, вполне доброжелательно.


Комментарии

Добавил: Admin Дата: 23.09.2006

Рулезный набор статей от команды в которой состоит автор статьи:

http://www.south-heaven.org/t53tutorials/t53.tuts.Pack12.rar

Лидер тимы - Grim Fandango - первоклассний дизайнер ascii и HiRes'а


Добавление комментария


Ваше имя (на форуме):

Ваш пароль (на форуме):

Комментарии могут добавлять только пользователи,
зарегистрированные на форуме данного сайта. Если Вы не
зарегистрированы, то сначала зарегистрируйтесь тут

Комментарий:





Главная     Программы     Статьи     Разное     Форум     Контакты