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

для защиты 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-заголовка и всего формата в целом.

Смещение/Размер/Название/Описание 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. Они представляют следующую структуру.

Смещение/Размер/Название/Описание 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, что существенно упрощает и ускоряет весь процесс написания, тем более на ассемблере. Также стоит создавать сначала резервную копию файла, открытую только для чтения, а создавать в новый файл. Это позволит в случае возникновения какой-либо ошибки всегда сделать откат проведенных изменений. Ниже приведен код функции воплощающий все эти веселости в реальность.

; это чтобы не писать десять раз параметры для 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-инструкций математического сопроцессора, нам не обойтись. Ниже приведен исходный код функции, считающей энтропию блока данных и выносящую вердикт – упакован или нет. Однако этот вердикт будет наверняка, и поэтому особенно доверять ему не стоит.

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

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

; сначала вычислим секцию кода (ту, в которую указывает 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, напишем макрос или процедуру (кому как нравится) для выравнивания значений.

; выравнивает значение в 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

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

; функция вернет в 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-заголовок.

; это все мы опишем в новой секции ‘.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

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

; тут будут все основные веселости… ; продолжение для процедуры 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-заголовка, которые можно было выставить сразу, то
можно начинать предварительную подготовку секций. Иначе говоря, мы скопируем все заголовки секций из старого файла в новый, попутно выставим атрибуты секций на чтение и запись, добавим заголовок для нашей новой секции, а за одним еще и проведем некоторую оптимизацию в файле (удалим релоки в файле и подровняем физический размер секций до минимально возможного).

; определим некоторые локальные переменные 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. Для начала подробно изучим структуру таблицы импорта. Она представляет из себя следующую структуру:

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. Нижеприведенный код позволяет реализовать следующее.

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-вирусы. Смысл оного заключается в том, что мы оперируем не абсолютными адресами, а относительными. Т.е. Вначале нашего загрузчика напишем такой код:

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

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

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

; ################################# 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-файле. Однако если ты все же еще не удосужился прочитать справку по таблице релоков, то, скорее всего и ничего не поймешь, для чего это все нужно и как работает.

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 к своему будущему пакеру, то ему нужно все же придерживаться, как минимум, еще и этих трех вышесказанных правил. На этом статью можно считать законченной и удачного всем вам пакерописания.

Комментарии

отсутствуют

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


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

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

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

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