Об упаковщиках в последний раз. Часть вторая

Обсудить статью на форуме


Свежая 2020 года подборка видеоуроков, инструментов крэкера, книг и статей - здесь.
“That people seeking education should have the opportunity to find it.”
Nick Parlante
“Binary Trees”


Рецензент:
Dr.Golova/UINC.RU

Сорецензенты:
Four-F/HI-TECH, Quantum, Sten, Fixer
1. Требования повышаются!
2. Вместо введения
3. Как Windows работает с секциями PE-файла
4. А запакован ли файл?
5. Тонкости PE-формата
6. OEP и иже с ним
7. Дампер процессов
8. Практический пример: UPX
9. Практический пример: Aspack
10. SEH с точки зрения кракера
11. Немного об антиотладке – в преддверии telock
12. Практический пример: teLock
13. Список литературы
14. Выводы...

Требования повышаются!

Итак, мы продолжаем. Подразумевается, что вы ознакомились с первой частью статьи и теперь владеете минимумом теории. Однако мы еще более повышаем требования. Теперь, для успешного и полного понимания SEH (Structured Exception Handling) и некоторых приемов, тут продемонстрированных, вам придется иметь базовое представление о С++. Мы не будем особо далеко лезть в дебри родовых классов, виртуальных функций и т.п., но самые основы OOП, такие как наследование, вам необходимо (по крайней мере, желательно!) знать.

Предполагается, также, что вы имеете джентльменский набор знаний по защищенному режиму - т.е. знаете, что такое IDT/GDT/PDE/PTE и прочие страшные аббревиатуры.

Кроме того, вам придется хлебнуть информации о недрах Windows для чего ОЧЕНЬ предлагается прочесть какую-нибудь хорошую книжку по системному программированию под Windows – например, Марка Руссиновича или Свена Шрайбера (список рекомендуемой литературы в конце статьи). Также по ходу дела мы постараемся давать линки на статьи (к сожалению, подавляющая масса литературы подобного характера на английском языке, на русском тоже что-то есть, но не слишком много).

Как вы уже, должно быть, догадались, все статьи этого цикла будут напирать на теорию. Мы предпочтем рассмотреть вопрос "почему", а не вопрос "как". Обязательно учтите, что здесь мы не рассматриваем 9x!

Практические примеры требуют воспроизводимости. Это означает, что вы должны иметь возможность воспроизвести примеры, показанные здесь, т.к. возможны недопонимания и все следует проверять практически. Во всех примерах упаковщиков используется один-единственный файл – calc.exe из поставки Windows 2000.

Вместо введения

написано совместно с Four-F

Что представляет собой процесс в Windows? Как запускается программа? Какие объекты создает и поддерживает операционная система? Что такое и как происходит диспетчеризация системных вызовов? Все это - достаточно важные вопросы, которые надо четко себе представлять. Всю оставшуюся часть статьи мы будем глубоко погружаться в мир недокументированных функций Windows, ввиду чего вам настоятельно рекомендуется ознакомится с предложенной литературой. Это статья слишком мала, чтобы детально рассматривать все внутренние системные структуры данных, это не было целью, но вот некоторые нюансы, которые помогут вам сориентироваться в этом, ох каком нелегком, деле, мы здесь продемонстрируем.

Известно, что процессоры Intel предлагают операционной системе 4 кольца защиты, из которых Windows использует только два - кольцо-0 и кольцо-3. Часть структур, сопровождающих программу, создается операционной системой в режиме ядра (ring-0) и программе ring-3 они недоступны. Другая часть создается в кольце-3 и, теоретически, доступна программе, однако документация отсутствует, примеров кода нет, словом MS постаралась, чтобы мы знали как можно меньше о внутренностях операционной системы.

Все трюки, продемонстрированные ниже, нельзя исполнить без файлов символов.

Символы отладки и эталонный отладчик

Если вам в силу тех или иных причин необходимо исследовать Windows, то сразу же возникает необходимость в настройке среды для работы. Очевидно, одного джентельменского набора из IDA/Soft-Ice/HIEW/IceExt и дампера процессов 3-кольца типа PE Tools тут уже недостаточно. Это шаг выше. Поэтому вам потребуется обзавестись символами отладки, которые можно сгрузить с сайта MS по этому адресу:
http://www.microsoft.com/whdc/ddk/debugging/symbolpkg.mspx установить и корректно настроить их. Обязательно учтите – символы должны ТОЧНО соответствовать вашей ОС и установленному SP. Для этого лучше использовать технологии MS. NuMega Symbol Retriever показал себя нестабильно работающей утилитой. Поэтому не поленитесь, если есть такая возможность, сгрузить файлы ручками, прогнать их через NMS-транслятор и проверить командной table, все ли у нас в порядке. Кстати, если по каким-то, одним разработчикам понятным причинам, Soft-Ice ну никак не желает откликаться на bpx, возможно, установка nms-символов на ntoskrnl сможет помочь.

IDA, включая версию 4.5, невероятно глючно накладывает информацию из pdb файла на свою базу, поэтому соизвольте сгрузить либо с сайта datarescue, либо с wasm.ru изумительный и очень шустрый плагин - PDB Plus. Показал себя безукоризненно.

Было бы неплохо также иметь какую-то утилиту, способную извлекать информацию из PDB-файла. Есть и такие – pdbdump – http://sourceforge.net/projects/pdbdump. Просто удивительно, сколько полезной информации можно извлечь из PDB. Нам с вами еще не раз предстоит в этом убедится на протяжении статьи. Наиболее полезный файл из всех – ntosrknlsym.pdb.

Следующая потенциальная проблема – это выбор отладчика, который был бы способен показывать внутренности ОС с достаточно большой степенью достоверности. Разумеется, нативный отладчик должен лучше понимать "свою" ОС, чем это делают все остальные. Разработчики Soft-Ice не раз подчеркивали, что все структуры реверсированы, а это не всегда самый надежный способ. Поэтому эталонным отладчиком можно смело считать отладчик самой MS – MS kd. MS kd тоже не идеален и грешит сокрытием информации (например, об объектах - проблема была описана Шрайбером, который предложил и решение для некоторых частных случаев), но это лучше, чем подавляющее большинство утилит.

Известно, что kd требует установки двух машин, соединенных между собой. Марк Руссинович разработал утилиту LiveKd (доступна на sysinternals.com и wasm.ru), которая позволяет запускать kd на одной и той же машине. Для отладки, разумеется, можно и нужно применять Soft-Ice, однако, когда возникает необходимость подглядеть какую-то структуру или адрес функции - лучше kd нет ничего (кроме, ессно, ручек и дизассемблера). kd доступен по адресу: http://www.microsoft.com/whdc/ddk/debugging/default.mspx

Установка не должна вызвать проблем. Поместите LiveKd в ту же директорию, программа все сделает сама.

Очень важными структурами пользовательского режима являются TIB, TEB и PEB. Последний мы в этой статье затрагивать не будем, а касательно двух первых - Thread Environment Block и Thread Information Block - необходимо развеять кое-какую путаницу. Здесь мы хотели бы вам сказать, что читать НЕ стоит! Не стоит читать статью Питрека - http://www.microsoft.com/msj/archive/S2CE.aspx и главы «Обработка исключений в реальном и защищенном режимах», «Как противостоять трассировке» и «Как противостоять контрольным точкам останова» и «Как обнаружить отладку средствами Windows» из книги Криса Касперски «Фундаментальные основы хакерства». Почему так? По поводу статьи Питрека – уж слишком она устарела. По поводу Касперски – автор не озаботился проверить, а соответствует ли то, что он написал, действительности, дочитайте данную статью до конца и станет ясно почему. Теперь выдержка из статьи Питрека: "The Windows 95 code calls it a Thread Information Block (TIB). In Windows NT, it's called the Thread Environment Block (TEB)." Утверждение неверно. Структура TIB существует и в Windows NT+ и называется _NT_TIB (полностью документирована в winnt.h), а структура TEB (недокументирована) включает в себя структуру TIB, т.е. является ее надмножеством. Где-то так (заметьте, полные описания структур мы не приводим, для этого есть исходники к книге Шрайбера на wasm.ru и кода ReactOS – ссылки в конце статьи):

   typedef struct _NT_TIB {
 /*0*/		struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
 //fs:[0] - рассматривается в главе о SEH
 /*остальные поля, за исключением поля *Self,
 // здесь не рассматриваются - есть статьи Питрека*/
 ...
 /*0x18*/	struct _NT_TIB *Self;	//fs:[18]
 /*вот из-за этого поля вся и путаница - это указатель на начало структуры TIB,
 // и, одновременно, на начало структуры TEB*/
 } NT_TIB;
 typedef NT_TIB *PNT_TIB;
 
 typedef struct _TEB {
 /*0*/		NT_TIB TIB;	//теперь становится очевидным, что TEB вмещает TIB!
 /*0x1С*/	PVOID EnvironmentPointer;
 /*обратите внимание, смещение этого поля - 1Сh,
 // т.к. перед ним идет вся структура TIB*/
 ...
 /*0x2C*/	PPVOID ThreadLocalStorage; //будет рассмотрен подробнее чуть попозже
 /*0x30*/	PPEB Peb; /*см. ниже*/
 /*0x34*/	DWORD LastErrorValue;
 }
 

Ну, поскольку, рисунок всегда нагляднее, вот:

Структура TEB
рис 1

А если и рисунка мало, тогда смотрите на дамп из kd:

kd> !teb
TEB at 7FFDE000
ExceptionList: 6d474 ;ExceptionList – первое поле TIB
...
PEB Address: 7ffdf000
;а это указатель на родимый PEB – по адресу видно, что это структура кольца-3
...

А теперь глянем в Шрайбера (w2k_def.h) касательно PEB. Оговоримся сразу – нас не интересует большинство полей, глянем только на два:

typedef struct _PEB
{
...
/*002*/ BOOLEAN BeingDebugged; /*используется функций IsDebuggerPresent – см. ниже*/
...
/*00C*/ PPROCESS_MODULE_INFO ProcessModuleInfo; /*а вот это – ошибка!*/
...
/*1E8*/} PEB, * PPEB;

Теперь ReactOS (teb.h):

typedef struct _PEB
{
...
UCHAR BeingDebugged; /* 02h */
...
PPEB_LDR_DATA Ldr; /* 0Ch */
...
} PEB;

Видите, структуры различаются. Кто же прав? Ответ нам даст kd:

kd> !peb
PEB at 7FFDF000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: No
ImageBaseAddress: 01000000
Ldr.Initialized: Yes
Ldr.InInitializationOrderModuleList: 71f38 . 76660
Ldr.InLoadOrderModuleList: 71ec0 . 76650
Ldr.InMemoryOrderModuleList: 71ec8 . 76658

Но даже более того! Не стоит полностью доверять и kd. То, что он показывает – верно на 100%, но есть одно маленькое но – он показывает НЕ ВСЕ. Часть структур просто скромно умалчивается. Однако есть одна вещь, которая никогда не солжет – дамп pdb-файла. Мы уже упоминали о pdbdump – давайте им и воспользуемся (не забудьте слить MS DIA SDK с wasm.ru или обзавестись Visual Studio .NET 2002+):

struct _PEB_LDR_DATA {
/*некорректно названа у Шрайбера*/
// non-static data --------------------------------
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Length;
/*<thisrel this+0x4>*/ /*|0x1|*/ unsigned char Initialized;
/*<thisrel this+0x8>*/ /*|0x4|*/ void* SsHandle;
/*смысл полей ниже одинаков – они все показывают на одну и ту же структуру, просто упорядочены по-разному*/
/*<thisrel this+0xc>*/ /*|0x8|*/ struct _LIST_ENTRY InLoadOrderModuleList;
/*<thisrel this+0x14>*/ /*|0x8|*/ struct _LIST_ENTRY InMemoryOrderModuleList;
/*<thisrel this+0x1c>*/ /*|0x8|*/ struct _LIST_ENTRY InInitializationOrderModuleList;
};

Уходим еще глубже - в двусвязные списки LIST_ENTRY. Структура определена в winnt.h как

typedef struct _LIST_ENTRY {
/*в случае одного-единственного элемента в списке Flink/Blink показывают сами на себя*/
struct _LIST_ENTRY *Flink; //forward
struct _LIST_ENTRY *Blink; //backward
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

Указатели, в нашем случае, показывают на структуру LDR_DATA_TABLE_ENTRY:

struct _LDR_DATA_TABLE_ENTRY {
/*на http://undocumented.ntinternals.net/UserMode/Structures/LDR_MODULE.html структура ошибочно названа
_LDR_MODULE*/
/*<thisrel this+0x0>*/ /*|0x8|*/ struct _LIST_ENTRY InLoadOrderLinks;
/*<thisrel this+0x8>*/ /*|0x8|*/ struct _LIST_ENTRY InMemoryOrderLinks;
/*<thisrel this+0x10>*/ /*|0x8|*/ struct _LIST_ENTRY InInitializationOrderLinks;
/*<thisrel this+0x18>*/ /*|0x4|*/ void* DllBase;
/*<thisrel this+0x1c>*/ /*|0x4|*/ void* EntryPoint; //давайте поиграемся с этим полем
/*<thisrel this+0x20>*/ /*|0x4|*/ unsigned long SizeOfImage;
/*<thisrel this+0x24>*/ /*|0x8|*/ struct _UNICODE_STRING FullDllName;
/*<thisrel this+0x2c>*/ /*|0x8|*/ struct _UNICODE_STRING BaseDllName;
/*<thisrel this+0x34>*/ /*|0x4|*/ unsigned long Flags;
/*<thisrel this+0x38>*/ /*|0x2|*/ unsigned short LoadCount;
/*<thisrel this+0x3a>*/ /*|0x2|*/ unsigned short TlsIndex;
/*<thisrel this+0x3c>*/ /*|0x8|*/ struct _LIST_ENTRY HashLinks;
/*<thisrel this+0x3c>*/ /*|0x4|*/ void* SectionPointer;
/*<thisrel this+0x40>*/ /*|0x4|*/ unsigned long CheckSum;
/*<thisrel this+0x44>*/ /*|0x4|*/ unsigned long TimeDateStamp;
/*<thisrel this+0x44>*/ /*|0x4|*/ void* LoadedImports;
};

Для чего мы заставляем вас проходить через это? Смотрите на код, он теперь должен иметь немного больше смысла:

void main(void)
{
__asm
{
mov eax, fs:[30h] ;Teb.Peb
mov eax, [eax+0Ch] ;Peb.Ldr - PEB_LDR_DATA
;не совсем корректно доступаться по указателю списка ;не проверив его сначала,
;но для ясности мы проверку опустим

mov eax, [eax+0Ch] ;Ldr.InLoadOrderModuleList.Flink – сам на себя
lea ebx, [eax+20h] ;LDR_DATA_TABLE_ENTRY.SizeOfImage
add [ebx], 88h ;LDR_DATA_TABLE_ENTRY.SizeOfImage += 0x88
;число выбрано просто так
}
}

Потрассируйте такой код и посмотрите в LordPe, что он вам покажет в поле SizeOfImage... Посмотрели? Угадайте, что будет, если дампер будет пробовать читать память, которой НЕТ? А теперь прогоните через PE Tools… Ну как результат?

Обратите внимание, что структура TEB (и входящая в нее структура TIB) создается Windows для каждого потока в момент его порождения и "сопровождает" поток до прекращения выполнения. Структура эта в кольце-3 доступна через регистр fs. Обратите внимание - мы говорим в кольце-3, т.к. содержимое, доступное через fs, РАЗЛИЧАЕТСЯ в ring-3 и ring-0. Почему так – ответ вы найдете в GDT. К Soft-Ice прилагается шикарная книженция – “Using Soft-Ice”. Есть там и глава – «Exploring Windows NT” где рассматривается, что куда и как отображается, и приведен примерчик.

В остальной части этой статьи мы будем употреблять термин "TEB" только тогда, когда смещение в fs-регистре превысит размер структуры TIB. Пример из книги Джона Роббинса "Отладка приложений": "реализация GetCurrentThreadId (из Windows 2000) получает сначала линейный адрес TIB-блока и затем, в позиции со смещением 0х24 (в TIB-блоке) - фактический идентификатор (ID) потока". Нет такого смещения в TIB-структуре! Нет и не было никогда. А вот в TEB - есть. Удивительно, как много пользы от таких простых знаний. Положим, вас заинтересовала работа функций GetCurrentThreadId, GetLastError и IsDebuggerPresent:

GetCurrentThreadId:
 mov     eax, large fs:18h	;NT_TIB.Self – линейный адрес структуры TEB,
                            ; расположенной в ring-3
 mov     eax, [eax+24h]		;TEB.Cid.UniqueThread
 
 GetLastError:
 mov     eax, large fs:18h 	;NT_TIB.Self
 mov     eax, [eax+34h]  	;TEB.LastErrorValue
 
 
 IsDebuggerPresent
 mov     eax, large fs:18h 	;NT_TIB.Self
 mov     eax, [eax+30h]		;TEB.Peb – извлекается УКАЗАТЕЛЬ на структуру
 movzx   eax, byte ptr [eax+2]	;Peb.BeingDebugged
 

Продолжаем. Следующее, что мы рассмотрим здесь - это интерфейс 2Eh и таблицы системных вызовов. Мы попытаемся отследить путь вызова процедуры вплоть до ядра Windows. За теорией - к Руссиновичу. Не имеете возможность купить эту книгу - вот линк на статью по теме http://www.sysinternals.com/ntw2k/info/ntdll.shtml. Обязательно учтите, что int 2E в XP+ отсутствует! Вместо этого используется команда sysenter.

Итак, мы предполагаем - вам известно, что такое Native API. Когда, скажем, вызывается функция kernel32.dll CreateFile, что происходит потом? Управление передается в ntdll.dll, где имеем код вида:

.text:77F83DA8 _NtCreateFile@44 proc near              ; CODE XREF: .text:77FA0B3Cp
;вот это и есть пример самой настоящей Native API функции
.text:77F83DA8
.text:77F83DA8 arg_0 = byte ptr 4
.text:77F83DA8
.text:77F83DA8 mov eax, 20h ; NtCreateFile
.text:77F83DAD lea edx, [esp+arg_0]
.text:77F83DB1 int 2Eh
.text:77F83DB3 retn 2Ch
.text:77F83DB3 _NtCreateFile@44 endp

Что происходит потом, когда выполняется int 2Eh? Поскольку это прерывание, то оно имеет свой обработчик в IDT. Обратите внимание - все функции ntdll.dll, обращающиеся к ядру, используют int 2E (в Win 2k, в XP+ используется специальная команда PII+ sysenter). Как же обработчик определяет, что делать дальше? Для этого полезем в Soft-Ice:

:idt 2е
Int Type Sel:Offset Attributes Symbol/Owner
IDTbase=80036400 Limit=07FF
002E IntG32 0008:804655CD DPL=3 P _KiSystemService

Т.о. обработчик называется _KiSystemService и сидит в ntoskrnl.exe. Дальше имело бы смысл привести дизассемблированный листинг этой функции, но все это уже сделано за нас - Peter Kosyh в своем замечательном сборничке очень подробно расписал что к чему - сборничек можно слить с wasm.ru. Глава - "Интерфейс системных вызовов". Там предельно подробно рассказывается, как обработчик находит нужную функцию. Единственное что, имеет смысл привести описания структур SDT/SST, в которых обработчик ее ищет, и рисуночек:

typedef struct _SERVICE_DESCRIPTOR_TABLE {
/*SDT доступна через идентификатор ntoskrnl - KeServiceDescriptorTable, заметьте, мы не рассматриваем здесь KeServiceDescriptorTableShadow - это далеко выходит за рамки статьи - подробнее см. великолепную книгу Шрайбера*/
/*0*/ SYSTEM_SERVICE_TABLE ntoskrnl; //SST для ntoskrnl.exe
/*0x10*/ SYSTEM_SERVICE_TABLE win32k; //SST для win32k.sys
/*0x20*/ SYSTEM_SERVICE_TABLE iis;
//SST для MS IIS Server (заполнено, ТОЛЬКО если установлен IIS)
/*0x30*/ SYSTEM_SERVICE_TABLE unused; //не используется
} SERVICE_DESCRIPTOR_TABLE;
typedef struct _SYSTEM_SERVICE_TABLE{
/*в ядре также есть идентификтор KiServiceTable, который является, по сути,
SERVICE_DESCRIPTOR_TABLE.ntoskrnl, остальные здесь не рассматриваются*/
/*0*/
PVOID ServiceTableBase; //указатель на начало таблицы, //содержащей адреса функций
/*4*/ PVOID ServiceCounterTable(0);
/*поле содержит количество вызовов той или иной системной функции и используется только в т.н. checked build версиях ОС, где KiSystemService занимается его заполнением*/
/*8*/
unsigned int NumberOfServices;
/*количество записей в таблице - учтите, что индекс функции (eax) НЕ должен превышать это значение*/
/*0xC*/ PVOID ParamTableBase;
/*если вам интересно, как KiSystemService узнает, сколько параметров принимает функция на стороне ядра, то количество таковых берется как раз отсюда*/
} SYSTEM_SERVICE_TABLE;
SDT / SST
рис 2

Для чего мы все это вам рассказываем? Положим, вас жутко заинтересовал механизм работы NtCreateSection (ZwCreateSection). Никаких проблем. Вы идете в ntdll.dll и находите ее вызов. Хм... Он скатывается к int 2Eh... Что дальше? Ладно, в этом случае все просто - функция экспортируется ядром - ntoskrnl.exe, следовательно, ничто не мешает прийти с дизассемблером и туда. А что вы скажете по поводу NtContinue (в eax - 1Ch)? Точно также - int 2Eh. А вот в таблице экспорта ядра такой функции нет. Тупик? Нет. Используя знания о структуре SDT можно легко отследить место расположения NtContinue в ядре, а затем найти эту функцию в ntoskrnl.exe на диске. Примерно так:

:exp KeService
 ;проверяем наличие такого символа, достаточно частичного имени
ntoskrnl
0008:8046DFA0 KeServiceDescriptorTable ;ага, Soft-Ice знает этот символ
:dd KeServiceDescriptorTable
0023:8046DFA0 804742B8 00000000 000000F8 8047469C .BG..........FG.
/*теперь вам уже известно строение SDT:
(отображена первая SST, принадлежащая ntoskrnl.exe)
804742B8 - соответствует ServiceTableBase
00000000 - соответствует ServiceCounterTable
000000F8 - соответствует NumberOfServices
8047469C - соответствует ParamTableBase
*/

...
:dd KiServiceTable
/*
ServiceTableBase - это, по сути, массив из указателей на функции - void*
*/

0023:804742B8 8049DD52 804AF6C1 804B043A 8050D5B8 R.I...J.:.K...P.
0023:804742C8 804B0470 8045CEA2 8050F7BE 8050F7FE p.K...E...P...P.
0023:804742D8 80494A38 8050A9F2 804ADED8 804FD82D 8JI...P...J.-.O.
...
:u *(KiServiceTable+1c*4) ;ну и где у нас в ServiceTableBase ;лежит элемент с индексом 1Ch?
_NtContinue
0023:804692A0 55 PUSH EBP
;полагаем, пересчитать этот адрес в памяти в реальное смещение ;в файле не составит труда - общая методика ;такова: просто используйте команду mod, вычтите Base address из вашего VA, ;если это необходимо, и можете брать ;дизассемблер и идти в гости
0023:804692A1 8B1D24F1DFFF MOV EBX,[P0BootThread]
0023:804692A7 8B553C MOV EDX,[EBP+3C]
0023:804692AA 899328010000 MOV [EBX+00000128],EDX
0023:804692B0 8BEC MOV EBP,ESP
0023:804692B2 8B4500 MOV EAX,[EBP+00]

Все, описанное выше, можно сделать еще проще. Разумеется, такой мощный инструмент как Soft-Ice имеет средство для просмотра SDT - это команда ntcall (обязательно учтите, что ntcall покажет только функции, принадлежащие ntoskrnl). Единственное что, использование таких команд освобождает от необходимости знать некоторые тонкости работы, что не всегда хорошо, т.к., к примеру, SST может быть использована для антиотладочных процедур почти на самом низком из всех возможных уровней (ниже - только драйвер). Пример - статья Тима Роббинса (Tim Robbins) - http://www.wiretapped.net/~fyre/sst.html.
Также очень неплохо было бы ознакомится с
http://www.windowsitlibrary.com/Documents/Book.cfm?DocumentID=356 - Undocumented Windows NT - очень сильная книжка, хотя немного и устарела. Возможно, имеет смысл скомпоновать ее главы в виде .chm-формата и поместить на wasm.

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

Мы уже упоминали, что содержимое, доступное через fs, различно для ring-0 и ring-3. Как вы теперь понимаете, в ring-3 fs:[0] показывает на структуру TEB. А что же мы видим, к примеру, в этом случае в кольце-0:

mov     eax, large fs:124h
mov al, [ebx+134h]

На что показывает fs:124h при DPL = 0? На что показывает fs:0 при DPL = 0? Все это подробнейшим образом описывает Шрайбер - его книга действительно великолепна. Мы здесь описания структур приводить не будем, достаточно слить некоторые файлы из раздела инструментов на wasm.ru и обзавестить DDK. Однако, предельно кратко, в виде рисунка, показать некоторые вещи стоит. Так нагляднее:

рис 3

Таким образом вы легко сможете преобразовать примеры выше в удобоваримый код, с которым можно сравнительно легко работать дальше:

mov     eax, large fs:124h 	;KTHREAD
mov al, [ebx+KTHREAD.PreviousMode

Помните, что Windows отображает адрес 0xFFDFF000 на fs:[0] (гляньте в GDT!). К примеру, по fs:[50] будет лежать KPCR.DebugActive, по fs:[120] - KPRCB (0xFFDFF020), по fs:[13C] - структура CONTEXT. Единственное, что может смутить - по 0xFFDFF020 лежит указатель на KPRCB, значение которого равно 0xFFDFF120:

mov     eax, ds:0FFDFF020h	;KPRCB по адресу FFDFF120
inc dword ptr [eax+4A8h]
;KPRCB.KeExceptionDispatchCount – да, ;увы и ах, документированная в ntddk.h структура KPRCB, конечно, что-то
;описывает, но опять-таки НЕ ПОЛНОСТЬЮ!
;лезьте в pdbdump и отчет о файле ntoskrnlsym.pdb – там много чего есть!

Еще, вероятно, вас может смутить поле NT_TIB в составе KPCR. Как же так – ведь структура NT_TIB принадлежит кольцу-3! А вот и не совсем так. И кольцо-0 и кольцо-3 владеют каждый по NT_TIB – по одной на брата. Т.о. и в кольце-0 и в кольце-3 fs:[0] показывает на TIB, только в первом случае TIB входит в KPCR и ни о каком TEB речи нет, а вот во втором случае TIB входит в TEB. Вот вам и дамп из kd в доказательство:

kd> !pcr
PCR Processor 0 @ffdff000 ;адресочки-то нулевого колечка, однако
NtTib.ExceptionList: f4347c68
NtTib.StackBase: f4347df0
NtTib.StackLimit: f4344000
...

Учитывайте эти нюансы и все будет ОК.

Очень многое осталось за бортом этого, предельно короткого, обзора. Однако для успешного понимания оставшейся части статьи этого более чем достаточно, при условии, что все понято. Если нет - Руссинович и Шрайбер. Особенно Шрайбер! Только читая его, обязательно учитывайте ГОД написания книги. Например, цитата: "Внутреннее строение структур WIN32_PROCESS и WIN32_THREAD - это еще одна пока что непознанная область Windows 2000, исследовать которую только предстоит". Уже не совсем так. У Шрайбера код выглядит так:

typedef struct _EPROCESS
{
/*000*/ KPROCESS Pcb;
...
/*214*/ struct _WIN32_PROCESS *Win32Process;
/*как видите, многие поля названы просто по порядку, не более*/
/*218*/ DWORD d218;
/*21C*/ DWORD d21C;
/*220*/ DWORD d220;
/*224*/ DWORD d224;
/*228*/ DWORD d228;
/*22C*/ DWORD d22C;
/*230*/ PVOID Wow64;
/*234*/ DWORD d234;
/*238*/ IO_COUNTERS IoCounters;
/*268*/ DWORD d268;
/*26C*/ DWORD d26C;
/*270*/ DWORD d270;
/*274*/ DWORD d274;
/*278*/ DWORD d278;
/*27C*/ DWORD d27C;
/*280*/ DWORD d280;
/*284*/ DWORD d284;
/*288*/ }
EPROCESS,
* PEPROCESS,
**PPEPROCESS;

А теперь посмотрите, СКОЛЬКО информации нам дает pdbdump, написанный позже:

  /*<thisrel this+0x214>*/ /*|0x4|*/ void* Win32Process;
/*<thisrel this+0x218>*/ /*|0x4|*/ struct _EJOB* Job;
/*<thisrel this+0x21c>*/ /*|0x4|*/ unsigned long JobStatus;
/*<thisrel this+0x220>*/ /*|0x8|*/ struct _LIST_ENTRY JobLinks;
/*<thisrel this+0x228>*/ /*|0x4|*/ void* LockedPagesList;
/*<thisrel this+0x22c>*/ /*|0x4|*/ void* SecurityPort;
/*<thisrel this+0x22c>*/ /*|0x4|*/ struct _UNICODE_STRING* AuditImageName;
/*<thisrel this+0x230>*/ /*|0x4|*/ struct _WOW64_PROCESS* Wow64Process;
/*<thisrel this+0x238>*/ /*|0x8|*/ union _LARGE_INTEGER ReadOperationCount;
/*<thisrel this+0x240>*/ /*|0x8|*/ union _LARGE_INTEGER WriteOperationCount;
/*<thisrel this+0x248>*/ /*|0x8|*/ union _LARGE_INTEGER OtherOperationCount;
/*<thisrel this+0x250>*/ /*|0x8|*/ union _LARGE_INTEGER ReadTransferCount;
/*<thisrel this+0x258>*/ /*|0x8|*/ union _LARGE_INTEGER WriteTransferCount;
/*<thisrel this+0x260>*/ /*|0x8|*/ union _LARGE_INTEGER OtherTransferCount;
/*<thisrel this+0x268>*/ /*|0x4|*/ unsigned long CommitChargeLimit;
/*<thisrel this+0x26c>*/ /*|0x4|*/ unsigned long CommitChargePeak;
/*<thisrel this+0x270>*/ /*|0x8|*/ struct _LIST_ENTRY ThreadListHead;
/*<thisrel this+0x278>*/ /*|0x4|*/ struct _RTL_BITMAP* VadPhysicalPagesBitMap;
/*<thisrel this+0x27c>*/ /*|0x4|*/ unsigned long VadPhysicalPages;
/*<thisrel this+0x280>*/ /*|0x4|*/ unsigned long AweLock;

Мы надеемся, что это достаточно эффективный пример. Soft-Ice может использовать свои имена (чего только стоят названия KTEB и UTEB – кого угодно запутать можно), kd может скрыть часть информации (введите команду !processfields и посмотрите как мало она дает), кода ReactOS, временами, выдают ТАКОЕ... Мы можем доверять лишь pdb-файлу и дизассемблеру. Не верьте именам структур, если они недокументрованы – любой их назовет как угодно, придерживайтесь имен самой MS – pdb-файлы не соврут.

Ну, а если вы истинный, то есть, ленивый (это синонимы) программист, то уже должны думать про себя: «Неужели мне, каждый раз, когда я вижу ebx+134h, придется каждый раз делать такие комментарии в IDA (да, кстати, недокументированные функции полностью отсутствуют в этом дизассемблере!)?». Ну, разумеется, нет! Все уже сделано за вас. Озаботьтесь загрузить себе idc-скрипты, описывающие некоторые структуры нулевого кольца с http://www.alkor.ru/~00077500/kb/winnt.htm или, как всегда, с wasm.ru (в последнем случае картина более полная, т.к. Four-F создал замечательный здоровенный idc-скрипт).

Ничто также не мешает вам самим перевести .h файлы в idc-скрипты. Частично проблема решена Леонидом Лисовским (Leonid Lisovsky) в его скрипте h2enum (слить с сайта datarescue), однако более разумным кажется приспособить готовый лексический анализатор для этих целей, к примеру, lex, совместить его с перловским скриптом, который будет подставлять нужные функции IDA и все. Почитать о lex и yacc можно, например, тут: http://www.codeproject.com/cpp/introlexyacc.asp. Хм. Добровольцы?

Также обязательно ознакомьтесь с набором команд IceExt (скачать с http://stenri.pisem.net либо с wasm.ru) – уникальный плагин для Soft-Ice, способный не только выполнять анти-антиотладку, но, к примеру, могущий также показать список PTE, содержание теневой SDT и т.п.

Мы довольно прилично осознаем, что от такого введения недолго и в обморок упасть. Поэтому не торопитесь. Вы, должно быть, уже сообразили, что эту статью нельзя прочесть с наскока – это не бульварное чтиво. В помощь при разборе ассемблерщики пусть возьмут себе уникальный KmdKit by Four-F и внимательно разберутся с файлом w2kundoc.inc. Программисты на С пусть возьмут основательно подправленный Volodya файл Шрайбера w2k_def.h. Оба доступны с wasm.

Как Windows работает с секциями PE-файла

написано совместно с Four-F

В первой части мы представили и должным образом дополнили работу Rustell Osterlund о работе ntdll.dll и тех проверках на валидность PE-файла, которые она выполняет. Теперь пришло время двинуться дальше и рассмотреть, какие ограничения на формат накладывает само ядро Windows. Это означает, что придется идти глубоко – в ntoskrnl.exe. В принципе, новички могут пропустить эту главу, так как она способна запросто привести в ужас кого угодно, кроме самих создателей Windows. Единственное что – в самом ее конце мы описываем практическое использование полученных знаний и реализацию оных в PE Tools.

Продвинутые читатели наверняка знают некоторые подробности об объектах Windows, поэтому смягчать выражения мы особо не будем. Единственный объект, который нас интересует – это объект «секция». Объект полностью недокументирован. Лишь в главе 7 книги Руссиновича есть легкое упоминание о такой вещи и симпатичный рисуночек.

Итак, объект "секция" не стоит путать с термином "секция" из PE-файла, это вовсе не одно и тоже. Section object создается в ЕДИНСТВЕННОМ экземпляре на файл. Доказательство: функция NtCreateSection, вызывается ОДИН раз - в коде лоадера (ntdll.dll) из LdrpCreateDllSection (эта, в свою очередь, из LdrpMapDll), и в коде CreateProcess также единожды, в последовательности:

...
NtOpenFile(...);
...
NtCreateSection(...);
...
NtQuerySection(...);
...

 

Подробнее – Петр Косых aka gloomy. Материалы можно слить с wasm.ru. Кода ReactOS, довольно часто упоминаемые нами в этой статье, по данному поводу можно просто выбросить – там чушь. NtCreateSection является лишь тонкой прослойкой вокруг MmCreateSection, которая и выполняет всю работу по заполнению объекта «секция», работе с PTE, а, точнее, с гиперпространством, проверке валидности, переводу флагов PE-секций в атрибуты структур SUBSECTION (см. ниже) через хитрые массивы ядра и т.п. Внутри MmCreateSection могут вызываться три функции:

  MiCreateDataFileMap
MiCreatePagingFileMap
MiCreateImageFileMap

 

Сначала вызывается MiCreatePagingFileMap. Далее, Windows на основании флагов из структуры CONTROL_AREA (см. рисунок ниже) решает как ей быть дальше – либо работать с файлом как с данными через MiCreateDataFileMap, либо как с исполняемым файлом через MiCreateImageFileMap, принимающей указатель на FILE_OBJECT. Весь процесс этот достаточно сложный, но, быть может, этот рисунок поможет немного разобраться (маленько улучшенная копия оного из книги Соломона-Руссиновича):

рис 4

Все семейство Mi*-функций, активно используемых в Mm-функциях, невероятно интересно. Однако целиком мы его рассматривать не будем. Внутри MiCreateImageFileMap заголовок PE-файла (ТОЛЬКО заголовок) безусловно отображается на гиперпространство по адресу 0x0С050000 (mov eax, 0C0500000h) функцией MiMapImageHeaderInHyperSpace (в функцию жестко зашито значение для отображения). После этого отображенный заголовок принимаются активно проверять – функция MiVerifyImageHeader. А вот эта функция уже безусловно интерестна для нас с вами:

 NTSTATUS MiVerifyImageHeader(PIMAGE_NT_HEADERS pPE, ...)
 {
 	DWORD FileAlign;
 
 	if(pPE->Signature != IMAGE_NT_SIGNATURE)
 	{
 		if(pPE->Signature != IMAGE_OS2_SIGNATURE)
 			return STATUS_INVALID_IMAGE_PROTECT; //0C0000130h
 		else
 		{
       /*...код для проверки формата NE...
       не рассматривается, здесь используются два других параметра функции
       и функция MiCheckDosCalls*/
 			if (NE is invalid)
 				return STATUS_INVALID_IMAGE_WIN_16; //0C0000131
 		}
 	}
 	if (!pPE->FileHeader.Machine)
 	{
 		if(!pPE->FileHeader.SizeOfOptionalHeader)
 			return STATUS_INVALID_IMAGE_PROTECT;
 	}
    //IMAGE_FILE_EXECUTABLE_IMAGE
 	if (!(pPE->FileHeader.Characteristics & 2))
 		return STATUS_INVALID_IMAGE_FORMAT; //0C000007Bh
 	if(!(pPE & 3))
    //проверка на выравнивание на границу DWORD
 		return STATUS_INVALID_IMAGE_FORMAT;
 	if (pPE->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC)
 		return STATUS_INVALID_IMAGE_FORMAT;
 	FileAlign = pPE->OptionalHeader.FileAlignment;
    //проверка на кратность 512 байтам
 	if (!(FileAlign & 0x1FF))
 	{
 		if(FileAlign != pPE->OptionalHeader.SectionAlignment)
 			return STATUS_INVALID_IMAGE_FORMAT;
 	}
 	if (!FileAlign)
 		return STATUS_INVALID_IMAGE_FORMAT;
    //проверка на степень двойки
 	if (!(FileAlign & (FileAlign-1)))
 		return STATUS_INVALID_IMAGE_FORMAT;
 	if (pPE->OptionalHeader.SectionAlignment < FileAlign)
 		return STATUS_INVALID_IMAGE_FORMAT;
 	if (pPE->OptionalHeader.SizeOfImage > 0x77000000)
 		return STATUS_INVALID_IMAGE_FORMAT;
 	return
    (pPE->FileHeader.NumberOfSections > 0x60) ?
    (STATUS_INVALID_IMAGE_FORMAT):(0);
 }
 

В принципе, этот псевдокод особых проблем вызвать не должен. Особое внимание обратите на проверку выравнивания файла и секций.

Для вступления этого вполне достаточно. С оставшейся частью, при большом желании, наличии времени и обладании DDK и интернетом, расправиться не так уж и сложно. Код, правда, насыщен функциями для работы с IRQL и спин-блокировками, однако Соломон и Руссинович достаточно подробно осветили этот вопрос. Также рекомендуем почитать статью Matt - http://www.tuningsoft.com/documents/irql.htm - «Understanding IRQL». Заметьте, MiVerifyImageHeader не единственное место, где MiCreateImageFileMap решает валиден ли образ или нет, однако оставшийся код активно оперирует с гиперпространством, что выводит обсуждение этого вопроса далеко за рамки данной статьи. И соваться туда стоит не раньше, чем прочтете (как следует!) главы Руссиновича о памяти, и всю доступную литературу о PTE/PDE/PFN.

Что до практического применения полученных знаний - NtCreateSection будет использоваться в качестве проверки валидности PE-файла в PE Tools (вероятно, с версии 1.6). Опция - "Validate PE". Псевдокод может выглядеть где-то так:

/*как вы помните из первой части, ntdll.dll БЕЗУСЛОВНО отображается
 на адресное пространство каждого Win32-приложения Windows*/
GetModuleHandle(“ntdll.dll”);
...
/*вызывать только динамически – через GetProcAddress т.к. нам нужна именно платформенная специфичность, поэтому никаких статических линковок с ntdll.lib*/
ZwCrSec = GetProcAddress(..., “ZwCreateSection”);
if (STATUS_CODE = ZwCrSec)
/*значит, ошибка, будем думать, что случилось*/
else
/*все параметры секций PE-файла валидны, содержимое директорий - ?*/
...

Если NtCreateSection вернула что-либо отличное от нуля – файл валидным не является – это невероятно надежный источник проверки валидности файла! Если неуверенно себя чувствуете с нативными приложениями (т.е. программами, использующими ntdll.dll напрямую с помощью статической линковки, или динамически, через GetProcAddress), то вот замечательный линк: http://www.osr.com/ntinsider/1996/native.htm

А запакован ли файл?

написано совместно с Fixer

В виду того, что эта статья позиционируется для взломщика с некоторым опытом, кажется немного наивным поднимать здесь такой вопрос. И в самом деле, для того, чтобы определить, а запакован ли файл, нужен только опыт. Дизассемблируется мусор, редакторы ресурсов отказываются работать и т.д., и т.п. Словом, все симптомы налицо и не о чем тут говорить. Это так. Возражений нет. Однако в рамках этой главы нам бы хотелось показать довольно оригинальный прием программной детекции упакованного файла. Для этого определим понятие "энтропия".

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

Положим, у нас есть строка:

abcdcdaad

Подсчитаем количество вхождений каждого байта. Вот так:

        a = 3
         b = 1
         c = 2
         d = 3
         ------
         9 total

 

Таким образом, мы можем сказать, что частота (т.е. вероятность - мы лезем в статистику) появления данного байта в этой конкретной последовательности составляет:

        a = 3/9 = 0.33...
         b = 1/9 = 0.11...
         c = 2/9 = 0.22...
         d = 3/9 = 0.33...,

где 9 - общая длина последовательности.
Определим теперь энтропию каждого символа по формуле:

 
 entropy = |log2(frequency_of_given_byte)|,
         

где log2 - логарифм по основанию 2.
Таким образом, имеем:

 
         a: |log2(3/9)| = 1,5849625007211561814537389439478
         b: |log2(1/9)| = 3,1699250014423123629074778878956
         c: |log2(2/9)| = 2,1699250014423123629074778878956
         d: |log2(3/9)| = 1,5849625007211561814537389439478,
 
 

Просуммировав сумму всех энтропий, получаем:

 
         1,5849625007211561814537389439478 A
         1,5849625007211561814537389439478 A
         1,5849625007211561814537389439478 A
         3,1699250014423123629074778878956 B
         2,1699250014423123629074778878956 C
         2,1699250014423123629074778878956 C
         1,5849625007211561814537389439478 D
         1,5849625007211561814537389439478 D
         1,5849625007211561814537389439478 D
         ----------------------------------------------
         17,019550008653874177444867327367 бит информации
         

Теоретически, это означает, что данную строку мы могли бы хранить в компьютерной памяти, используя лишь 17 бит информации. Реально же используется 72 - т.е. символов у нас 9, каждый символ - это байт, а байт - 8 бит. Итого 72 = 8*9. Остается сделать последний штрих - подсчитать остаток от деления общего количества бит на "энтропийные" биты (72/17 = 4,23). Выполнив его, мы увидим, что эффективность хранения информации невысока - фактически, имеем разницу в ЧЕТЫРЕ раза.

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

Алгоритм может выглядеть так:

 long *ArrFreq;
 double *aEntropy;
 double msgEntropy=0.0;
 ArrFreq=new long[256];
 aEntropy=new double[256];
 ZeroMemory(ArrFreq,256*sizeof(long));
 ZeroMemory(aEntropy,256*sizeof(double));
 BYTE *pBuff=(BYTE*)Offset;
 long Max=0;
 DWORD i = 0;
 // подсчитаем каждый байт в сегменте (кода, данных и т.п.)
 for (;i<pSegment->Size;i++)
 {
 	ArrFreq[pBuff[i]]++;
 }
 BYTE OpCode=0xff;
 
 for (i=0;i<255;i++)
 {
 	if (ArrFreq[i]>Max)
 	{
 		Max = ArrFreq[i];
 		OpCode=(BYTE)i;
 	}
 	// хранит вероятность появления символа
 	double prob=(double)ArrFreq[i]/(double)pSegment->Size;
 	if (prob)
 	{
 		//подсчитаем энтропию для i-го байта
 		aEntropy[i]=(-log(prob)/log(2))*(double)ArrFreq[i];
 		// и в общую сумму!
 		msgEntropy+=aEntropy[i];
 	}
 }
 
 // теперь в битах
 double DataSize=(double)pSegment->Size*8.0;
 
 // теперь делим, для вычисления остатка
 double CompressionRatio=DataSize/msgEntropy;
 

Данный кусочек кода был любезно предоставлен Manuel Jimenez - автором BDASM (www.bdasm.com) - очень перспективного и быстрого дизассемблера, из которого в будущем может получится достойный соперник IDA! Разумеется, код нельзя просто скомпилировать, однако общий подход он даст.

А особо любознательным расскажем, что идея эта, естественно, отнюдь не нова. Давным-давно криптографы определили понятие "гаммирование" - т.е. наложение какой-либо последовательности байт на текст, чтобы исказить его до неудобочитаемости. Шифр Вернама, перестановки Цезаря - все это призвано было защитить файл от чужих глаз. Однако криптоаналитики придумали подход, который можно назвать "частотным анализом". Т.е., делается предположение о том, что зашифрованный файл содержит в себе осмысленные предложения из такого-то или такого-то языка. И зная вероятности появления символов алфавита в тексте (кстати, как вы думаете, а какая самая часто встречающаяся буква в русском алфавите?), можно попытаться примерно таким же алгоритмом угадать, а что же спрятано за маской? Доказано, что гаммирование принципиально нельзя сломать при условии равенства (и достаточно скрупулезного подбора!) длины последовательности (гаммы), длине шифруемой последовательности. Но даже если это и не так, то, ответьте, что мне мешает заархивировать файл, а уж потом наложить гамму, пусть и неустойчивую? Архивация полностью уничтожит вероятностные распределения букв под маской, делая дешифровку невозможной. Так? А вот и не так! Популярных архиваторов не так уж и много! Опытный криптоаналитик, увидев подобную белиберду, первым делом попробует сжать файл. Как, не сжимается? Ах так! Ну мы тогда...

Впрочем, кажется, мы увлеклись. Итак, определение того, что файл упакован, не займет много времени. Теперь зададим вопрос: "А ЧЕМ упакован файл?". Ответ на подобный вопрос нужен не одним нам с вами. Мировым стандартом (не побоимся этой фразы) считается составление сигнатуры и поиска этой сигнатуры в файле. С точки зрения алгоритмики имеем поиск подстроки в строке.

Приготовление сигнатуры – вещь не сложная и во многом должна определяться квалификацией того человека, который эту сигнатуру составляет. В Pe Tools для этой цели разработана утилита SignMan, следующая версия которой будет основана на очень простом принципе: побайтовом сравнении файлов, запакованных ОДНИМ И ТЕМ ЖЕ упаковщиком с разными опциями:

SetFilePointer(на оффсет, введенный пользователем,
              т.к. отсчитывать от точки входа – неверное решение!);
 
 ... //отвести буферы и т.д., и т.п.
 
 for(int i = 0; i < до какого-то значения, заданного пользователем; i++)
 {
 
 if(byFromFile1[i] == byFromFile2[i])
    {
 	/*хорошо – в отчет*/
    }
 }
 

Сигнатуры хранятся в текстовой форме в файле Sign.txt. Практика показала, что это решение удачно. У многих людей, заинтересованных в судьбе утилиты, нашлось свободное время, чтобы отправить баг-репорт с правками сигнатур и, можно заявить, что файл, на сегодняшний день является достаточно тщательно проверенным не только авторами утилиты, но и многочисленными пользователями. Хотя, разумеется, это не означает, что багов там нет...

Словом, примем сигнатуру для поиска достаточно надежным средством. Осталось определится с тем, как ее искать. Мы трактуем файл как последовательность байт. PE Sniffer, по версию 1.5.х. включительно, является пока еще утилитой-ребенком и от него, ни в коем случае, пока нельзя ожидать многого. Поэтому утилита должна быть переписана с учетом быстрого и в достаточной степени надежного поиска. Итак, поиск может быть разделен на две категории:

1) Точный поиск подстроки в строке –

Байт-в-байт в точке входа – тривиальное strcmp с минимальными трюками по пропуску плавающих байт. Медленно, наивно, не всегда работает. Скажем, ничто не мешает переписать точку входа PE-файла на свой, достаточно безобидный код, который, скажем, делает не больше, чем xchg eax,eax – и этого хватит, чтобы сигнатура не была опознана. Учтите, что автоматический алгоритм детекции секции кода может быть неверным (причины см. в первой части), поэтому надежнее сначала просто спросить у пользователя, какую секцию PE он считает секцией кода. Что мы имеем здесь в более продвинутом плане? Первое - поиск по принципу бинарного дерева – реализован в IDA во FLIRT-алгоритме. Более подробно об этом можно почитать в статье Ильфака Гильфанова о FLIRT. Второе - поиск Бойера-Мура в файле. Рекомендуется прочесть некоторые документы с http://algolist.manual.ru/ или ознакомиться со статьей http://www.rsdn.ru/article/alg/textsearch.xml на RSDN. Хочется сказать спасибо автору статьи - Андрею Боровскому – за такие объяснения, какими они должны быть. Понять алгоритм можно только посидев с карандашом над ним, разрисовав палочки и черточки. Эта статья относится именно к такому классу. Приведенные в ней алгоритмы в несколько более эффективном варианте, переписанные на С, будут использованы в PE Sniffer. В этом случае с файлом работают при помощи MMF-функций.

2) Неточный поиск подстроки в строке –

Поиск Бойера-Мура достаточно быстр за счет построения таблицы смещений и делает меньше сравнений, чем тривиальное strcmp. Однако он подходит лишь для точного поиска образца (или, в улучшенных вариантах, допускает лишь минимальные отклонения, согласно простейшим регулярным выражениям). Здесь же мы ни в коей мере не можем быть уверены, что подстрока (сигнатура) не будет самым злостным образом искажена. Давайте рассмотрим пару примеров и на их основе попытаемся сформулировать ряд правил для написания движка.

Пример: libc.lib – стандартная библиотека языка С. Известно, что код программы начинается не с main, а с *mainCRTStartup (одной из четырех). Поэтому ничто не препятствует поменять код процедуры с такого, например:

 posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA));
 posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
 (void)GetVersionExA(posvi);
 _osplatform = posvi->dwPlatformId;
 _winmajor = posvi->dwMajorVersion;
 _winminor = posvi->dwMinorVersion;
 

на вот такой:

 posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA));
 goto here;
 
 now_here:
 _osplatform = posvi->dwPlatformId;
 _winmajor = posvi->dwMajorVersion;
 _winminor = posvi->dwMinorVersion;
 goto keep_on;
 
 here:
 posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
 (void)GetVersionExA(posvi);
 goto now_here;
 
 keep_on:
 ...
 

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

Реально же имеем следующее:

1) Для каждого пакера существует устойчивая кодовая последовательность именуемая в дальнейшем сигнатурой для которой действуют следующие правила (здесь под элементом сигнатуры подразумевается байт)


a) Элементы сигнатуры не могут меняться местами
b) Элементы сигнатуры не могут быть заменены на другие

2) Между отдельными элементами может присутствовать "шумовой" код (если мы для сигнатуры выберем 1, 5, 10 байты сгенерированные пакером, для уменьшения количества ложных срабатываний, необходимо учесть минимальное расстояние на котором могут встретиться эти элементы сигнатуры друг от друга), а между некоторыми такой код появиться не может (двух и более байтные команды).

Что-то наподобие такого и будет реализовано в новой, уже не совсем детской версии PE Sniffer. Хотя и здесь решение для общего случая, скорее всего, НЕ существует. Уж слишком злопакостно можно исказить сигнатуру при желании, и ни CRC, ни xor-сумма строки, ни побайтовое сравнение по хитрым правилам не помогут. Скажем, пункты 1.a/1.b явно дискуссионны, чего только стоят т.н. stolen bytes в Asprotect. Поэтому если имеете собственное суждение – не стесняйтесь его высказать.

Возможно, самые отчаянные захотят обсудить экзотические методы детекции – применение нейронных сетей, генетические алгоритмы, fuzzy logic и т.п. – это прекрасно. Дерзайте! Учтите, что и OEP (см. главу об OEP) можно искать в памяти по такому же принципу – поиском сигнатур компилятора. Так, к примеру, поступает PEiD в своем genoep.dll – просто-напросто в дампе программы выполняется поиск компиляторных сигнатур, при этом, кстати, опять

Обсуждение статьи: Об упаковщиках в последний раз. Часть вторая >>>


При перепечатке ссылка на https://exelab.ru обязательна.



Видеокурс ВЗЛОМ