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

Дата публикации 6 ноя 2003

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

“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, [eax+134h]

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

рис 3

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

mov     eax, large fs:124h 	;KTHREAD
mov al, [eax+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

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

Энтропия
Боже, как только это слово в термодинамике не обругали. И мера порядка системы, и мера рассеивания энергии, уж чего только там не было! Без сомнения, настоящего физика от нашего определения покоробит, а настоящий математик искренне возмутится. Тем не менее, по дилетантски определим слово "энтропия" как некоторую меру эффективности хранения информации. Для иллюстрирования понятия продемонстрируем следующий пример. Условимся считать файл всего лишь строкой байт, конец которой определяется каким-то загадочным образом самой операционной системой.
        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 – просто-напросто в дампе программы выполняется поиск компиляторных сигнатур, при этом, кстати, опять таки, не учитывается понятие «украденные байты» - stolen bytes – защитный прием некоторых пакеров (Asprotect), при котором куски кода из OEP нагло утаскиваются.

В заключение было бы интерестно рассмотреть – а как же действуют профессиональные антивирусы? Ведь как-то же они предполагают наличие вируса, хотя, зачастую и неверно, давая ложные срабатывания или просто не узнавая его. Однако мир антивирусов практически закрыт от конечного пользователя. И кто-то еще ругает Windows за закрытость кода?

Как правило, в открытую печать не попадает сколь-нибудь ценной технической информации... Что тут говорить. Тем не менее, мы предложим пару линков:

  1. «Heuristic Techniques in AV Solutions: An Overview»
    http://www.securityfocus.com/infocus/1542 - в качестве вступления смотрися неплохо.
    2. «Stripping Down An Av Engine»
  2. http://www.nai.com/common/media/vil/pdf/imuttik_VB_%20conf_2000.pdf – в качестве вступления к следующему линку тоже ничего.
  3. http://clamav.elektrapro.com/ - попытка антивируса с открытым исходым кодом (язык С). Функция эвристики (пока только ребенок) находится в файле matcher.c и использует обобщение алгоритма Кнута-Морриса-Пратта в поиске в односвязном списке – функция cl_scanbuff. Есть и еще несколько подобных антивирусов, но у их основателей хватило ума писать их на Java… Что тут сказать... И это там, где важна скорость и низкоуровневые трюки...
  4. http://www.peterszor.com/ - на удивление приличные статьи о детекции метаморфов, пара статей о Win32 высокого класса. Это достаточно хороший уровень. Очень рекомендуется!

Тонкости PE-формата

Вы решили потратить деньги на приобретение пакера. ОК, выбор ваш. Положим, вы не уверены, что способны написать хорошую защиту, просто нет времени или еще что-то. Тогда проверьте пакер! Хороший криптор должен не только перезагружать Windows если кто-то подошел к монитору, он должен и корректно обрабатывать многие тонкости и сложности PE-формата.

Одним из достаточно простых, но достаточно забавных тестов на качество написанного пакера может быть следующий, почти гениальный, клочок кода:

__declspec(thread ) int i = 1;
__declspec(thread ) int m = 0;

void main(void)
{
/*для нас совершенно неважно, какой именно код тут используется,
он написан просто так, чтобы что-то написать;
что действительно важно, так это объявления переменных.
 Статические объявления заставляют компилятор
создать секцию .tls в результирующем файле*/
printf(“%ld\n”, i*m);
}

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

http://www.microsoft.com/msj/0999/hood/hood0999.aspx

и облегчать жизнь писателям пакеров у нас желания нет. Однако факт остается фактом – многие коммерческие упаковщики не учитывают инициализацию tls-цепочек лоадером, в результате чего запакованный файл падает. Среди таковых и Aspack 2.12, который такой файл даже обработать не может! А люди еще за это и деньги платят...

Более того, можно сделать еще веселее! Положим, мы имеем дело с dll (tls-цепочки используются, в основном, именно в dll) – как прикажете обрабатывать секцию tls, которая подвержена перемещению, т.е. появляются fixup-элементы? В случае exe-файла аналогичного результата можно добиться опцией MS-линкера /FIXED:NO. Так это вообще фантастика! UPX гарантировано обрабатывает такие вещи, а вот некоторые остальные, не будем показывать пальцами...

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

OEP и иже с ним

Для этой главы неплохо было бы выбрать самый простой из всех возможных упаковщик, на котором и проиллюстрировать некоторые закономерности работы. Такой, к счастью, есть. Называется PE Deminisher и доступен для закачки с wasm.ru.

Итак, пакуем наш старый добрый calc.exe и что мы имеем:

  Name   VirtSize   RVA    PhysSize  Offset    Flag
.text 000124EE 00001000 0000782B 00000600 E0000020
.data 000010C0 00014000 000003E1 00008000 C0000040
.rsrc 00002B98 00016000 00002C00 00008400 40000040
.teraphy 00001000 00019000 0000041A 0000B000 C0000040

Очевидно, три секции являются нормальными, четвертая принадлежит упаковщику. Так же очевидно, что, в этом случае, остальные секции (за исключением .rsrc) являются сжатыми по какому-либо алгоритму (в данном случае это apLib, но нам это не важно). Что находится в четвертой секции и почему она расположена именно четвертой? Вот слегка обрезанный отчет HIEW о данном файле:

Name       	RVA      Size
Import 00019391 00000089 ;импорт перенаправлен
Resource 00016000 00002B98 ;ресурсы оставлены
Debug 00001210 0000001C ;старый трюк – см. первую часть
Import Table 00001000 0000020C
;недоработка данного упаковщика – IID ;перенаправлена в секцию .teraphy, IAT оставлена, но не валидна

Использование директории отладки является старым добрым антиотладочным приемом, мы это уже описывали и повторяться неинтерестно. Ресуры тоже уже мало кого удивляют. Любопытнее выглядит изменение RVA директории импорта в секцию пакера. Если глянуть на заголовок (Optional Header), то можно видеть, что и точка входа переориентирована в новую секцию, и количество секций, соответственно, увеличено. Разумеется, изменилось поле SizeOfImage, иначе файл валидным не будет. Кому интерестно видеть все мелочи – воспользуйтесь функцией Compare из Pe Tools. Нам же интерестнее ответить на вопрос: так почему же секция идет четвертой. Если чуточку подумать над проблемкой, то ответ ясен – так легче. Положим, можно поставить и третьей, если не лень пересчитывать ресурсы как директорию и как секцию. А вот первой – ни-ни. Так как слишком уж это дело будет хлопотное... Но если кому-то не лень, что тогда? Смотрите – теоретически невозможно поставить секцию пакера первой – это потребует коррекции ссылок в секции кода и коррекции ссылок между секциями кода и данных и т.п., да и не только. Откуда получить такую информацию? Если файл содержит IMAGE_DIRECTORY_ENTRY_BASERELOC, то тогда, используя информацию оттуда, такое дело возможно, однако методика для общего случая работать не будет.

Что в этом плане можем извлечь мы. Да очень простой, старый и почти безотказно работающий трюк. Только давайте сначала четко определимся с терминами. Итак: OEPoriginal entry point – это не та самая точка входа которая записана в заголовке PE файла (OptionalHeader.AddressOfEntryPoint). OEP - это VA, куда упаковщик передаст управление после полной распаковки файла. Т.е. это - оригинальная точка входа которая была в заголовке PE файла до упаковки. А точка входа в запакованном файле называется EP (Entry Point). Так вот, нетрудно заметить, что прыжок после распаковки всегда будет происходить из области больших адресов в область меньших адресов. На этом механизме и были построены многие OEP-трейсеры – revirgin и icedump в их числе. Очевидно, что если кто-нибудь (гм, например, мы) не поленится написать драйвер, который даже будет не сколько трассировать приложение, сколько просто смотреть за EIP, когда тот будет выходить за пределы секции (секций) упаковщика. Положим, протектор сможет делать ложные прыжки, положив оные в конструкцию try/catch (см. ниже) – но отчет утилиты покажет это все человеку, а уж человек элементарно разберется – какой прыжок ложный, а какой нет. С другой стороны, пакер вполне может применять засечки количество тактов процессора – rdtsc и, чуть что не так, начинать орать. Словом, тут есть над чем подумать...

Работать с dll примерно так же просто. Достаточно давно разработана методика Break & Enter. Смысл ее состоит во влеплении опкода СС (о самом опкоде см. ниже) прямо в EP программы. Известно, что Soft-Ice Symbol Loader часто просто проскакивает мимо EP. Поэтому LordPE и PE Tools лепят СС-байт прямо в EP, предварительно запоминая оригинальный. Все, что остается пользователю – ввести bpint 3 и восстановить старый байт после всплытия Soft-Ice. Скоро будет написан плагин под PE Tools в виде лоадера dll, т.к. dll, с нашей с вами точки зрения, мало чем отличается от exe.

Что до директории импорта – тоже достаточно просто понять, что оригинальная директория импорта остается нетронутой лоадером (он ее просто не видит). Вместо этого пакер сам, после расшифровки содержимого файла, находит эту директорию и в цикле, перед передачей управления на OEP, с помощью GetProcAddress, наполняет ее валидными для данной системы адресами и производит перерасчет RVA на VA (см практический пример с Aspack). Обязательно следует заметить, что и тут прогресс ушел далеко вперед. Современные крипторы уже не используют GetProcAddress. Уж слишком легко нам поставить на нее брейкпоинт и разобраться в логике пакера (см. практические примеры).

bpx на функции API
mov  eax,[KernelBase]
push offset LoadLibraryA
push eax
call GetProcAddress
cmp byte ptr [eax],0cch
je Found_Hook

Код прост, но хорошо отражает суть. Его можно выразить и чуть иначе, например, так:

mov edi, offset на собственную IAT – сканируем первый байт всех адресов 
mov al, 0CCh
repnz scasb ;код скорее, схематичен, ;чем реален, но в сети есть настоящие примеры

а можно и еще парой десятков вариаций. Для его обхода опытные люди используют трюк с bpx API-name + x, где х – число, которое приходится на начало другой инструкции. Важно заметить, что + х может быть не абы каким числом. Это обязательно должно быть поле операнда, иначе исключения #DB не произойдет, произойдет другое исключение :smile3: Скажем, часто предлагается ставить нечто вроде bpx GetProcAddress + 3. В этом случае Soft-Ice превратит символьное имя GetProcAddress в VA (гм, а может и не в VA, бог его знает, что там внутри Soft-Ice происходит – ведь большинство не задумывается ПОЧЕМУ команда bpx на какую-нибудь API-функцию типа MessageBox срабатывает в любом адресном пространстве, т.е. контекстно-неспецифична – под Windows для первых двух гигабайт это попахивает черной магией!), добавит к этому адресу 3 и воткнет туда СС. Будьте готовы – авторам упаковщиков элементарно проверить каждую вызываемую функцию на наличие CC, а тогда ... Тогда можно еще использовать bpm или уходить на уровень NativeAPI. К примеру, для GetProcAddress цепочка выглядит так:

GetProcAddress -> LdrGetProcedureAddress -> LdrpGetProcedureAddress 

Последняя основательно прокомментирована в статье о DLL-лоадере, рассмотренной нами в первой части.

Вместо этого используется более изощренная технология – прямое сканирование директории экспорта целевых dll. Более подробно об этом можно почитать в статье «Win32 Assembly Components» написанной LSD Team. Статью можно скачать с wasm.ru или с сайта команды –
http://lsd-pl.net/projects.html#windowsassembly. Что мы можем сказать по этому поводу? М-м-м... Опять таки, если время сканирования экспорта опирается на rdtsc – тут тяжелее. В общем случае – почему бы, наконец, не раскачать Sten’a на написание нормальной bpr-команды, которую, к сожалению, убрали из Soft-Ice... Присутствие такой bpr-команды позволит ставить точки останова на большие диапазоны памяти – например, на диапазон директории экспорта, и отслеживать обращающиеся к этому диапазону инструкции. Команда должна быть контекстно-специфичной.

Теперь еще один вопрос. Когда надо дампить программу? Почему в подавляющем большинстве статеек по пакерам сказано, что нужно вводить образ в бесконечный цикл? Давайте проясним ситуацию.

Как ни странно, ответ на вопрос – когда нужно дампить программу, часто заключается в самой запакованной программе. Все зависит от того, насколько разумно она проектировалась программистом. Как известно, язык С позволяет создавать статические и глобальные переменные. Оставим спор по поводу необходимости их применения в стороне, а сами зададим вопрос: «а чем от этого плохо нам?». Рассмотрим следующий пример:

#include "windows.h"
static void* h_heap = 0; /*вот в этом вся и соль*/
void main(void)
{
  if (!h_heap)
    h_heap=HeapCreate(0,0x1000,0);
 
  HeapAlloc(h_heap, HEAP_ZERO_MEMORY, 0x500);

  MessageBox(0, "I said NOW!", "Dump me NOW!", MB_OK);

  HeapDestroy(h_heap);
}

Программа запускается и работает абсолютно нормально, но стоит сдампить ее как раз на MessageBox – последствия не заставят себя ждать. Причина в том, что статические и глобальные переменные инициализируются КОМПИЛЯТОРОМ! Следовательно, при нормальном развитии событий переменная имеет свой нолик еще в секции PE-файла и проверка проходит нормально. В сдампленной программе переменная уже заполнена функцией HeapCreate и, следовательно, при следующем запуске будет нам с вами радость.

Так что, в общем случае, рекомендация должна звучать примерно так: секции данных лучше дампить сразу после раскриптовки/распаковки, ибо протектор может их подпортить еще до ОЕР. И это подводит нас к очень важному выводу: не надо полагаться на ImpRec, OEP-finder’ы, TRW с его makepe или что-либо еще – с каждым пакером пока приходится работать индивидуально, т.к. общее решение проблемы пока не разработано.

Дампер процессов

Основой получения списка процессов является функция NtQuerySystemInformation. Читатель может заворчать – вот, опять Native API. Да, именно так. Только учитывайте, что бывают РАЗНЫЕ уровени недокументированности! Скажем, весьма и весьма вероятно, что реализация некоторых Mi*-функций, рассмотренная нами ранее, может и, скорее всего, будет, варьироваться от ОС к ОС и даже от SP к SP, в то время как некоторые Native API функции весьма стабильны и едва ли изменятся в будущем. Во всяком случае, прототип, название и смысл должны сохранится. А смысл этой функции настолько велик, что Шрайбер назвал ее «кладезем» информации о системе. Также Гарри Неббет (Gary Nebbet) неплохо осветил эту тему в 1 главе своей книги. Кое-что из этой информации мы повторим здесь. Приступим:

NTSYSAPI NTSTATUS NTAPI NtQuerySystemInformation (
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
); typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation,
SystemProcessorInformation,
SystemTimeOfDayInformation,
SystemPathInformation,
SystemProcessInformation,
SystemCallCountInformation,
SystemDeviceInformation,
SystemProcessorPerformanceInformation,
SystemFlagsInformation,
SystemCallTimeInformation,
SystemModuleInformation,
SystemLocksInformation,
SystemStackTraceInformation,
SystemPagedPoolInformation,
SystemNonPagedPoolInformation,
SystemHandleInformation,
SystemObjectInformation,
SystemPageFileInformation,
SystemVdmInstemulInformation,
SystemVdmBopInformation,
SystemFileCacheInformation,
SystemPoolTagInformation,
SystemInterruptInformation,
SystemDpcBehaviorInformation,
SystemFullMemoryInformation,
SystemLoadGdiDriverInformation,
SystemUnloadGdiDriverInformation,
SystemTimeAdjustmentInformation,
SystemSummaryMemoryInformation,
SystemNextEventIdInformation,
SystemEventIdsInformation,
SystemCrashDumpInformation,
SystemExceptionInformation,
SystemCrashDumpStateInformation,
SystemKernelDebuggerInformation,
SystemContextSwitchInformation,
SystemRegistryQuotaInformation,
SystemExtendServiceTableInformation,
SystemPrioritySeperation,
SystemPlugPlayBusInformation,
SystemDockInformation,
SystemPowerInformation,
SystemProcessorSpeedInformation,
SystemCurrentTimeZoneInformation,
SystemLookasideInformation
} SYSTEM_INFORMATION_CLASS;

Где, SystemInformationClass – тип требуемой информации, нас интересует только информация о процессах, т.е. SystemInformationClass = 5, SystemInformation – указатель на буфер данных, SystemInformationLength – размер буфера данных, ReturnLength – размер записанных в буфер данных. Если выделенного буфера недостаточно, то в параметр ReturnLength будет возвращён требуемый размер буфера. Рекомендуемый размер = sizeof(SYSTEM_PROCESS_INFORMATION) * 1024, так как вряд ли у кого то будет запущенно 1024 процесса.

Далее если не произошло ошибок, в буфер SystemInformation будет передана структура SYSTEM_PROCESS_INFORMATION, описывающая отдельный процесс в списке. Первым членом структуры (NextEntryOffset) будет смещение на следующий процесс в списке, если же оно равно NULL, значит это последний процесс в списке.

typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
LARGE_INTEGER SpareLi1;
LARGE_INTEGER SpareLi2;
LARGE_INTEGER SpareLi3;
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId; // PID
HANDLE InheritedFromUniqueProcessId;
ULONG HandleCount;
ULONG SpareUl2;
ULONG SpareUl3;
ULONG PeakVirtualSize;
ULONG VirtualSize;
ULONG PageFaultCount;
ULONG PeakWorkingSetSize;
ULONG WorkingSetSize;
ULONG QuotaPeakPagedPoolUsage;
ULONG QuotaPagedPoolUsage;
ULONG QuotaPeakNonPagedPoolUsage;
ULONG QuotaNonPagedPoolUsage;
ULONG PagefileUsage;
ULONG PeakPagefileUsage;
ULONG PrivatePageCount;
SYSTEM_THREAD_INFORMATION TH[1];
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;

Простой пример использования функции, подразумевается, что буфер выделен, и NtQuerySystemInformation вызвалась без ошибок:

SYSTEM_PROCESS_INFORMATION *pSysInfo;
...

NtQuerySystemInformation(…);

while(1)
{
	// Здесь уже можно использовать структуру
	...

	// Проверям – последний это процесс в списке? Если да, то
	// выходим из цикла
if(!pSysInfo->NextEntryOffset)
	break;

// Переходим к следующему процессу
pSysInfo = (SYSTEM_PROCESS_INFORMATION)((PVOID)pSysInfo + pSysInfo->NextEntryOffset);
}

Внимание: важный момент, перед вызовом NtQuerySystemInformation рекомедуется установить своему приложению привилегию отладки программ. Это нужно для получения списка системных процессов (System Idle process). Но это возможно только под правами Администратора. Всё это проделывает функция EnableDebugPrivilege. Чтобы установить привилегию, первый и единственный параметр должен быть равен TRUE, чтобы убрать FALSE.

BOOL EnableDebugPrivilege(BOOL bEnable)
{
HANDLE hToken;
BOOL bOk = FALSE;
if(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
bOk = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return bOk;
}
Получение списка загруженных процессом модулей

Для получения списка модулей служит функция RtlQueryProcessDebugInformation.

RtlQueryProcessDebugInformation(HANDLE hPID, DWORD dwInfo, PVOID pRtlBuff);

где,

  • hPID – идентификатор процесса (PID),
  • dwInfo – код запроса требуемой информации,
  • pRtlBuff – буфер куда будет передана информация о модулях процесса. Ниже приведена структура этого буфера:
typedef struct _DEBUGMODULEINFO
{
DWORD ImageBase;
DWORD ImageSize;
DWORD unknown1;
USHORT DllSequenceNum
USHORT NumDlls;
DWORD GrantedAccess;
CHAR Name[MAX_PATH];
DWORD unknown;
} DEBUGMODULEINFO,PDEBUGMODULEINFO;

Для работы с функцией RtlQueryProcessDebugInformation необходимо создать (RtlCreateQueryDebugBuffer) рабочий буфер для хранения информации о модулях. И после окончания работы, вызвать функцию RtlDestroyQueryDebugBuffer, чтобы освободить этот самый буфер.

PDWORD RtlCreateQueryDebugBuffer(DWORD, DWORD);
DWORD RtlDestroyQueryDebugBuffer(PDWORD);

Пример:


QUERYDEBUGBUFFER *pModuleInfo; // Информация о модулях
DWORD dwNtStatus;              // Код возврата функции
                               // RtlQueryProcessDebugInformation
DWORD dwPID; 	                // PID процесса
// Выделяем буфер
DWORD *pRtlBuffer = RtlCreateQueryDebugBuffer(NULL, NULL);

if(!pRtlBuffer)
{
	// Error!
}
// Запрашиваем информацию о модулях
dwNtStatus = RtlQueryProcessDebugInformation((HANDLE *)dwPID, 0x01, pRtlBuffer);

if(!dwNtStatus)
{
	pModuleInfo = (QUERYDEBUGBUFFER*)pRtlBuffer;

	// Производим перечисление модулей процесса
for(int i = 0; i < pModuleInfo->dwNumNames; i++)
{
	printf(“ImageBase: 0x%0.8Xl”, pModuleInfo[i]->ImageBase);
	printf(“ImageSize: 0x%0.8Xl”, pModuleInfo[i]->ImageSize);
	...
}
}
else if(dwNtStatus == DEBUG_ACCESS_DENIED)
{
// Error
}

// Освобождаем рабочий буфер
RtlDestroyQueryDebugBuffer(pModuleInfo);
Определение ImageBase и ImageSize

ImageBase и ImageSize определяются достаточно легко. Нужно сделать перечисление модулей того процесса, для которого определяются эти значения. И в СПИСКЕ МОДУЛЕЙ найти имя нужного процесса, как правило оно идёт первым. Далее взять значения из структуры DEBUGMODULEINFO:

QUERYDEBUGBUFFER *pModuleInfo;
...
pModuleInfo->ImageBase;
pModuleInfo->ImageSize;

НО! Если процесс использует антидамповые приёмы (один из которых можно посмотреть во введении – нечего было его проскакивать!), то, как правило, ImageSize имеет неправильное значение. Поэтому рекомендуется его считать из PE заголовка процесса, но можно и из файла на диске (последнее даже надежнее). Ниже приведён пример функции считывающей ImageSize из заголовка:

//Первый параметр - это PID процесса, второй  - ImageBase.

DWORD GetRealSizeOfImage(DWORD dwPID, PVOID pModBase)
{
	IMAGE_DOS_HEADER pDosh = {0};
	IMAGE_NT_HEADERS pNT = {0};

	HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, dwPID);
	if(hProcess)
	{
		ReadProcessMemory(hProcess,
                         pModBase,
                         &pDosh,
                         sizeof(IMAGE_DOS_HEADER),
                         NULL);
		if(IMAGE_DOS_SIGNATURE == pDosh.e_magic)
			ReadProcessMemory(hProcess,
                            (PBYTE)pModBase + pDosh.e_lfanew,
                             &pNT,
                            sizeof(IMAGE_NT_HEADERS), NULL);
		CloseHandle(hProcess);
		if(pNT.Signature == IMAGE_NT_SIGNATURE)
			return pNT.OptionalHeader.SizeOfImage; 
	}
	return NULL;
}

Однако даже наличие таких методик в PE Tools все равно не спасет от защиты-драйвера (например, Extreme-Protector). Поэтому, видно, пришло время и PE Tools переходить на драйверный движок, который мало в чем будет опираться на структуры кольца-3. Так правильнее.

Практический пример: UPX

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

UPX является практически единственным исключением в своем роде - полностью открыт исходный код. Это обстоятельство пытаются использовать многие авторы упаковщиков, и распаковщиков тоже. Часть просто молча ворует исходники (GPL-лицензия таки налагает некоторые забавные ограничения, хоть исходный код и открыт), другая часть пытается использовать это знание нам во вред, забывая - что OpenSource - это палка о двух концах. В данной главе принципиально не будет приведено ни единой строчки кода дизассемблера. Зачем? Все есть в кодах UPX. Итак, скачивайте кода с http://upx.sourceforge.net и в директории stub находите файл l_w32pe.asm. Не забудьте заглянуть и в stub.asm. В самом начале данного файла есть забавное предупреждение - 5 минут смеха обеспечены! Тем не менее, ассемблерный листинг стаба проще читается под отладчиком, так как исходники набиты всякими директивами препроцессору С (невидимыми ассемблеру), которые явно мешают уловить суть алгоритма.

UPX полностью пересобирает PE-файл, меняя все, что можно, за исключением ресурсов. К ним UPX относится достаточно бережно. Все остальное программа переводит в свой внутренний формат и сжимает по алгоритму UCL (кстати, вовсе не обязательно именно UCL, есть еще прогрессивный NRV) http://www.oberhumer.com/opensource/ucl.

Как программа переводит эти данные в свой формат описано в файле p_w32pe.cpp (для каждого поддерживаемого формата - свой cpp-файл со своими методами). Масса полезной информации находится также в файлах packer.cpp, packhead.cpp и compress.ch (очень занимательное чтиво). Однако нас это интересует мало, тем более не всякий открытый исходный код - панацея, т.к. за спасибо можно получить только исходники компрессора UCL, а последняя бета-версия UPX использует компрессор NRV (не GPL, однако). Получается, что основная часть упаковщика остаётся за кадром...

В результирующем файле (сколько бы секций ни было в оригинальном) всегда будут только три секции (для версий, отличных от 1.24-1.90, правило, возможно, соблюдаться не будет – кода мы не изучали) -

  1. UPX0 - кладутся tls
  2. UPX1 - fixup-элементы, импорт, экспорт, код и т.п.
  3. UPX2 - ресурсы, однако в действительности этой секции, как правило, нет, т.к. автор утилиты прекрасно знал о том, что имя секции .rsrc очень много значит.

Код стаба - l_w32pe.asm - разжимает секции и обрабатывает директории импорта и fixup-элементов. Заметьте, повторим еще раз - идет обработка директории импорта! Часто встречается утверждение, что, мол, UPX переводит все в свой внутренний формат и, посему, лучше использовать ImpRec и иже с ним. Переводить-то, утилита переводит, да только потом разжать-то ведь надо, и перевести назад – в валидный для ОС формат. Внимательно рассмотрим код l_w32pe.asm по обработке импортов:

/*код стаба построен достаточно хитро - он насыщен инструкциями вроде %endif;
 __PEMAIN01__, где невидима для ассемблера,
  но прекрасно видима препроцессором Си
  - т.о. этот код может (и будет!) различаться для dll/exe и т.п.*/
                pushad
                mov     esi, 'ESI0'                ; VA секции UPX1
                lea     edi, [esi + 'EDI0']        ; VA секции UPX0
...
                push    edi
...
                pop     esi
...
                lea     edi, [esi + 'BIMP']        ;распакованные имена функций
                                                   ;во внутреннем формате UPX

next_dll:
; проверить на конец массива имен – DWORD = 0
                mov     eax, [edi]
                or      eax, eax
; хоть метка и называется imports_done, правильнее - names_done
                jz      imports_done
                mov     ebx, [edi+4]
; имена dll во внутреннем формате UPX
                lea     eax, [eax + esi + 'IMPS']
                add     ebx, esi
; как мы постоянно упоминаем – имена лежат во внутреннем формате и edi
; показывает на два хитрых DWORD’a, 
; на основании второго из них ([edi+4]) вычисляется VA,
; куда GetProcAddress будет класть полученные адреса
                push    eax				
                add     edi, 8
                call    [esi + 'LOAD'] ; LoadLibraryA
                xchg    eax, ebp		; ebp - хендл
next_func:
                ; имя функции
                mov     al, [edi]
; как мы уже упоминали – формат внутренний,
; строки разделены нулями
                inc     edi
                or      al, al
                jz      next_dll
                mov     ecx, edi        

                push    edi            ; имя функции из dll
                dec     eax            ; 0 - разделитель
                repne
                scasb                  ; встать на начало следующего имени

                push    ebp			; хендл
                call    [esi + 'GETP'] ; GetProcAddress
                or      eax, eax
                jz      imp_failed
                mov     [ebx], eax     ; начинаем готовить массив адресов
                add     ebx, 4         ; увеличить VA на sizeof(DWORD)
                jmps    next_func
imp_failed:
		; к ExitProcess
imports_done:
		; к OEP

То, что UPX, фактически, распространяется с открытым кодом, да ещё и для различных форматов исполнимых файлов (Win16, Win32, Posix, MS-DOS и т.д.) серьёзно ограничивает его антиотладочные возможности... Цель UPX заключается в максимальном сжатии файла, но не в противостоянии хакерским усилиям по его распаковке. UPX.EXE поддерживает модификатор -d в командной строке для распаковки своих же файлов и почти всех предыдущих версий UPX включительно. На вопрос "как распаковать UPX?" можно лениво ответить, что мол "UPX -d packed.exe" (зря вы дампер приготовили). Так даже не интересно...

Стоп! Мы забываем про утилиты для защиты UPX, так называемые скрамблеры (scramblers). Скрамблеры пытаются немного замаскировать запакованные файлы, чтобы обмануть UPX. Но мы-то знаем, что перед нами файл, запакованный UPX'ом, потому что нам об этом сообщил идентификатор (sniffer) файлов или в дизассемблере "на глаз" был подмечен стаб UPX'а.

Здесь мы НЕ будем учиться распаковывать UPX. Существуют утилиты, которые прекрасно справляются с этой задачей, без особого участия со стороны пользователя. Здесь мы попытаемся понять принципы защиты UPX от распаковки, которые активно используются скрамблерами, вроде UPX-SCRAMBLER и HidePX. Данные утилиты призваны уберечь поднаготную запакованного экзешника от посторонних глаз, но делают они это не очень эффективно. Скачайте себе любую из них, или обе, или какой-нибудь другой скрамблер и пропустите через него ваш calc.exe. Что, уже не получается распаковать через upx -d? Как уже упоминалось раньше, существуют мощные распаковщики, которым скрамблеры погоды не строят, но наша цель - преодолеть защиту скрамблеров своими руками.
Не поленитесь сравнить calc.exe до и после прохождения через скрамблер. Можете воспользоваться WinHex / File Manager / Compare или другой подобной утилитой (PE Tools и LordPE умеют сравнивать и поля PE-формата – опция – “Compare”). Внимательно изучите листинг расхождений в обоих файлах и вы скоро поймёте, что первостепенные различия связаны с именами секций и сигнатурой UPX. UPX-SCRAMBLER заменяет UPX0 на code и UPX1 на text. HidePX затирает имя UPX0 и заменяет UPX1 на .rdata.

Если бы всё дело было только в именах секций, то для восстановления calc.exe можно было бы просто восстановить имена секций в любом PE-редакторе. Имена секций восстановлены, но там ещё и с сигнатурой что-то не так... Что такое сигнатура в данном случае? Здесь есть два понятия, которые необходимо различать. Под сигнатурой, с точки зрения последовательности байт кода, сама UPX понимает следующее:

/*код взят из файла p_w32pe.cpp метод canUnpack класса PackE32Pe*/
/*этот метод очень важен – именно тут UPX делает проверки
 на количество секций и их имена, проверятся байтовая сигнатура и,
 если что-то не так, бросается исключение с надписью
 «file is modified/hacked/protected; take care!!!»*/

bool PackW32Pe::canUnpack()
{
	...
      static const unsigned char magic[] = "\x8b\x1e\x83\xee\xfc\x11\xdb";
      // mov ebx, [esi];    sub esi, -4;    adc ebx,ebx

}

Очевидно, изменив код этого метода, несложно добится того, что для программы перестанет иметь значение имя секции (количество секций лучше не трогать), об отсутствии или неверном offset’е сигнатуры она станет лишь предупреждать, а не бросать исключение, но это лишь малая часть айсберга, т.к. существует ВТОРАЯ сигнатура! Под второй сигнатурой понимается структура, начинающаяся с “UPX!” (см. таблицу), которую UPX помещает перед сжатой частью файла. Помните, мы говорили, что UPX полностью перестраивает формат файла и граница старого сжатого файла начинается со второй сигнатуры. И, если испорчена она (что и делают скрамблеры), то тогда UPX просто слетит с внутренним исключением. Очевидно, снятие ВТОРОЙ сигнатуры и есть самое главное препятствие. Препятствие ли?

Для нахождения/просмотра/восстановления сигнатуры UPX можно воспользоваться хекс-редактором, но лучше - плагином Uncover UPX для PE Tools. Плагин прилагается к данной главе, так что можете сразу копировать его в каталог Plugins и PE Tools автоматически поместит его в соответствующее меню.

Основная черта данной утилиты заключается в автоматическом пересчёте контрольной суммы (поле CRC) при изменении остальных полей сигнатуры. Плагин также умеет частично или даже полностью восстанавливать сигнатуру после применения скрамблера. В случае с HidePX вам потребуется ввести (исправить) некоторые значения. Итак, что там за поля такие в сигнатуре? Учтите, что все значения отображаются в обратном порядке байт, т.е. в формате little endian, например: 12345678 -> 78563412, ABCDEF -> EFCDAB, ABCD -> CDAB, AB -> AB.

Поле
Размер
(в байтах)
Значение
Magic 4 Последовательность ASCII-символов 'UPX!’
Version 1 Версия упаковщика, например: 0C значит 1.24, 0D - это последняя на данный момент бета 1.90. Если вы пользуетесь UPX v1.90 и подопытный экзешник не очень старый (после 2001), можете спокойно прописать сюда 0D.
Format 1 Для интересующих нас экзешников в формате PE32, это поле всегда равно 09.
Method 1 Наиболее распространённые методы сжатия - это NRV и UCL. Обоим соответствует значение 02.
Level 1 Степень сжатия.
U_adler 4 Контрольная сумма части экзешника в распакованном виде*.
C_adler 4 Контрольная сумма части экзешника в запакованном виде*.
U_len 4 Размер части экзешника в распакованном виде*.
C_len 4 Размер части экзешника в запакованном виде*.
U_file_size 4 Размер распакованного экзешника.
Filter 2 Об этом чуть позже!
CRC 1 Контрольная сумма сигнатуры. Плагин показывает её в виде 16-битного значения, потому что перед CRC идёт дополнительный байт выравнивания.

* Под частью экзешника подразумевается та часть, которая подлежит сжатию/разжатию. В общем, кроме самого UPX’а никто больше не умеет определять точные границы этой части.

Сигнатура UPX для старых NE, линуксовых ELF и т.д. представлена иначе, но нам интересен только формат PE32.

Uncover UPX самостоятельно восстанавливает сигнатуру после UPX-SCRAMBLER, так как данный скрамблер уничтожает сигнатуру частично, но что делать если сигнатура утеряна полностью, как в случае с HidePX? Тогда Uncover UPX заполнит её значениями по умолчанию и нам придётся немного ему помочь, если в том возникнет нужда.

Поле magic, понятное дело, менять не стоит. Поле version обычно оставляется как есть (0C или 0D). Format оставьте со значением 09. Method в 99% случаев равен 02. В level можете поместить любое отличное от нуля значение, так-как распаковщик не обращает внимания на уровень сжатия. Тоже самое относится к полю u_file_size.

Значения в u_adler и c_adler - это контрольные суммы, рассчитанные по алгоритму Марка Адлера (http://www.cdrom.com/pub/infozip/zlib/). Можете посмотреть исходники данного алгоритма, но они нам не помогут... Предполагается, что у нас нет распакованного варианта экзешника. Значит и подсчитать его контрольную сумму, даже зная алгоритм, мы не можем... На самом деле, обе контрольные суммы не влияют на процесс распаковки, т.е. можно просто отключить проверку данных значений внутри UPX. Вы уже скачали исходники UPX и UCL? Кстати, в исходниках UCL есть пример реализации алгоритма М. Адлера, но не будем отвлекаться. В исходниках UPX, в файле packer.cpp есть код следующего содержания:

void Packer::decompress(const upx_bytep in, upx_bytep out,
                        bool verify_checksum)
{
    // verify_checksum = true, т.е. этот код всегда выполняется
    if (verify_checksum)
    {
        unsigned adler = upx_adler32(in,ph.c_len);
        if (adler != ph.c_adler)
            throwChecksumError();
    }

    // Тут происходит вызов 'настоящего' распаковщика
    unsigned new_len = ph.u_len;
    int r = upx_decompress(in,ph.c_len,out,&new_len, ph.method);
    if (r != UPX_E_OK || new_len != ph.u_len)
        throwCompressedDataViolation();

    // опять эти адлеры...
    if (verify_checksum)
    {
        unsigned adler = upx_adler32(out,ph.u_len);
        if (adler != ph.u_adler)
            throwChecksumError();
    }
}

Если отключить обе проверки (до и после распаковки), про адлеры можно будет забыть. Следующий код заодно отключает проверку c_len и u_len, т.е. гонимся за четырьмя зайцами и успешно их ловим:

void Packer::decompress(const upx_bytep in, upx_bytep out,
bool verify_checksum)
{
// Тут происходит вызов 'настоящего' распаковщика
int r = upx_decompress(in,ph.c_len,out,&ph.u_len,ph.method);
}

Правда, даже если вы сможете перекомпилировать этот код, вы получите версию UPX без поддержки NRV. Было бы куда лучше внести эти исправления в последнюю версию UPX (1.90 на данный момент), которая поддерживает сразу UCL и NRV. Где наш дизассемблер? Стоп, перед устранением этого бага в UPX, не забудьте его распаковать (он сам собой и запакован)

Распакованный UPX.EXE (около 327 Кб) грузим в HIEW и задаём поиск 56578B7C241484DB8BF1. Кстати, данная последовательность применительна и к предыдущей версии UPX. Узнаёте следующий код?

53         push  ebx          
8A5C2410 mov bl,[esp][10]
55 push ebp
56 push esi
57 push edi
8B7C2414 mov edi,[esp][14]
84DB test bl,bl
8BF1 mov esi,ecx
74XX je XXX ; это тот if (verify_checksum)

Меняем 74 на EB и первая проверка адлеров решена! Чуть дальше вы встретите вторую проверку контрольной суммы:

7405       je    XXX
E8XXXXFFFF call XXX
84DB test bl,bl
74XX je XXX ; это второй if (verify_checksum)

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

8B561C     mov   edx,[esi][1C]  ; это ph.u_len
8B4614 mov eax,[esi][14]
8B6C2418 mov ebp,[esp][18]
8D4C241C lea ecx,[esp][1C] ; а это new_len
8954241C mov [esp][1C],edx
8B5620 mov edx,[esi][20]
50 push eax ; ph.method
51 push ecx ; &new_len
55 push ebp ; out
52 push edx ; ph.c_len
57 push edi ; in
E8XXXXXXXX call XXX ; upx_decompress

Функции вместо new_len нужно подсунуть адрес ph.u_len. Это можно организовать заменив 8D4C241C на 8D4C261C. Сразу за этим вызовом видим код примерно следующего содержания:

85C0       test  eax,eax
75XX jne XXX ; если r == UPX_E_OK
8B44XXXX mov eax,XXX
8B4EXX mov ecx,XXX
3BC1 cmp eax,ecx
74XX je XXX ; если new_len == ph.u_len

Исправьте второй переход с условного на безусловный. Всё, теперь нам море по колено! Можете вписывать в поля u_adler и c_adler всё, что хотите (хоть FFFFFFFF, чтобы не путаться с little endian) Для полного отключения проверки валидности файла можете ещё удалить сравнение имени первой секции. Это сравнение очень просто найти в... Нет уж, ищите сами! В противном случае, будете и дальше править имена секций в PE Editor.

Что до u_len и c_len, то проверку валидности данных полей вы уже отключили но, в отличии от адлеров, выбор значений u_len и c_len налагает некоторую ответственность. Дело в том, что UPX резервирует два буфера в памяти: один размером с u_len для временного хранения распакованного файла, другой размером с c_len для чтения запакованного содержимого файла. Понятно, что если задать слишком маленькое значение для u_len, то распакованный файл просто не поместится в буфер. С другой стороны, слишком большое значение заставит UPX потреблять больше динамической памяти. Для u_len вполне подойдёт значение 000FFFFF (FFFF0F00 в little endian) для большинства упакованных экзешников.

С c_len чуть сложнее. Опять же, слишком маленькое значение вызовет конфликтную ситуацию с динамической памятью, но слишком большое, кроме излишнего расхода памяти, отрицательно воздействует на сам процесс распаковки. Иначе говоря, фокус с 000FFFFF не пройдёт. Надо подобрать более близкое значение. К счастью, в версии 1.24 выдаётся одно сообщение об ошибке, когда значение меньше правильного и другое - когда больше. В версии 1.90 эта фича отсутствует, но на данный момент HidePX не поддерживает 1.90, а UPX-SCRAMBLER не портит значение c_len. В версии 1.24 даже не нужно задавать абсолютно точное значение c_len - небольшая погрешность спокойно поглощается распаковщиком.

В заключение стоит упомянуть поле filter. В нём обычно хранится значение 260X, где X может быть 0, реже – 1, а еще реже – что-нибудь другое, например, 6, для старых версий UPX. Данное поле заслуживает особого внимания, так как неправильное значение фильтра приводит к неправильной распаковке экзешника, т.е. файл распаковывается, но не запускается! В общем, зря скрамблеры пренебрегают этой записью. Немного подправив значение фильтра можно защитить файл куда эффективнее, хотя от дамперов это всё равно не поможет, но всё-таки...

Теперь давайте проведем два практических примера. Наконец-то!

Итак, UPX-SCRAMBLER. Скачиваем с wasm специальные upx by Quantum/Volodya. Результат:

C:\Downloads\PE\upx>upx1_24.exe -d s.calc.exe 
распакованно мгновенно!

Теперь HidePX.

C:\Downloads\PE\upx>upx1_24.exe -d p.calc.exe
upx1_24: p.calc.exe: CantUnpackException: fillPackHeader: Seems like HidePX...

Поможем нашему UPX. Загружаем PE Tools, запускаем Quantum’овский плагин. Жмем одну-единственную кнопочку – Fix. Повтор:

C:\Downloads\PE\upx>upx1_24.exe -d p.calc.exe 
распакованно мгновенно!

На тот маловероятный случай, что что-то пойдет не так... Хм, а зачем мы вам столько писали, а?

В заключение статьи надо сказать, что некоторые упаковщики довольно бездарно пытаются замаскироваться под UPX. Например, это делает telock. Только вот все они не учитывают одной маленькой тонкости - UPX 1.24+ создает ТОЛЬКО ДВЕ секции - UPX1 и UPX0 - независимо от того, сколько на самом деле секций в файле. А telock этого попросту не учитывает, создавая в некоторых случаях несколько UPX-секций, что сразу бросается в глаза. Гораздо более точным критерием в данном случае можно считать присутствие характерного стаба, ведь он действительно нужен для того, что бы экзешник мог сам себя распаковать в памяти.

В самое заключение главы. Помните, в самом начале мы говорили, что здесь не будет ни строчки дизассемблированного листинга? Так вот, мы соврали. Имеет смысл разобрать один скользкий момент с HIEW. Передача управления на OEP в UPX отображается HIEW (по 6.85 включительно) так:

.0101AFF1: 8903                         mov         [ebx],eax
.0101AFF3: 83C304 add ebx,004 ;"¦"
.0101AFF6: EBE1 jmps .00101AFD9 -----^ (3)
.0101AFF8: FF9690BC0100 call d,[esi][0001BC90]
.0101AFFE: 61 popad
.0101AFFF: E91C74FFFF jmp 0FFFFE820 ;на OEP

Почему же jmp по адресу 0x101AFFF имеет такой странный операнд? Давайте спросим автора HIEWSEN’a. Ответ: «...но такого VA в файле нет, он появится потом, когда UPX память выделит для этого VA, а в файле ничего нет, поэтому hiew просто отсчитывает в глобальных адресах смещение и показывает как есть». Так что это ни в коем случае не баг утилиты. Просто автор очень не хочет включать поддержку многочисленных частных случаев, благодаря чему HIEW был и, пожалуй, так и остается одним из самых быстрых дизассемблеров на сегодяшний день. А уж эта возможность поиска по ассемблерной маске с * и ? – так это вообще фантастика...

Практический пример: Aspack

Что делает новичок, увидев программу, запакованную Aspack? Ну, берется Soft-Ice, берется дампер процессов и, обязательно, Imprec. Потом над всем этим инструментарием начинают интенсивно издеваться – зацикливать на OEP, дампить целиком, посекционно или еще как, немедленно запускать Imprec и вставлять полученный дамп директории импорта в файл. Ну что ж. Можно и так. Работает. Только давайте усложним задание. Положим, дампер процессов не имеет никакого движка по перестройке импорта, а Imprec и иже с ним у нас просто нет. Что тогда?

А вот тут-то и надо рассматривать сам алгоритм работы упаковщика. К счастью, есть и такие статьи. Например: «Исследование алгоритма работы упаковщика ASPack v1.08.03» - довольно толковая статья (когда будете читать – хм, вы уже знаете, что такое KTEB). Несмотря на то, что рассматривается старая версия, в новой (2.12) не так уж и много изменений с нашей точки зрения. Для кросс-проверки и некоторого дополнения сведений можно также проглядеть статью «ASPack 'Spelunking'».

В связи с тем, что есть такие великолепные материалы, мы не станем рассматривать код ASPack подробно. Для начала учтите – что это не криптор, это самый обычный упаковщик. Здесь нет ни антиотладки, ни сколь-нибудь сложных приемов противостояния дизассемблерам. Лишь в самом начале есть нечто, робко напоминающее полиморфный код:

;версия 2.12
.aspack:01019001 pusha
;КРЕПКО запомните эту инструкцию! Проникнитесь! Она нам ох как пригодится!
;да, меж прочим, дизассемблирована эта команда неверно! ;В 32-битном режиме это pushad.

.aspack:01019002 call loc_101900A
.aspack:01019002 ; ------------------------------------------------------------
.aspack:01019007 db 0E9h ; щ
.aspack:01019008 ; ------------------------------------------------------------
.aspack:01019008 jmp short loc_101900E
.aspack:0101900A ; ------------------------------------------------------------
.aspack:0101900A
.aspack:0101900A loc_101900A: ; CODE XREF: start+1p
.aspack:0101900A pop ebp
.aspack:0101900B inc ebp
;++ebp = eip – хороший пример позиционно-независимого кода – PIC
.aspack:0101900B start endp
.aspack:0101900B
.aspack:0101900C push ebp
.aspack:0101900D
.aspack:0101900D locret_101900D: ; CODE XREF: start+7u
.aspack:0101900D retn
;учтите – это не антиотладка, это лишь достаточно красивый ;пример вывертов с ассемблером – смотреть приятно!
.aspack:0101900E ; -------------------------------------------------------------
.aspack:0101900E
.aspack:0101900E loc_101900E: ; CODE XREF: start+7j
.aspack:0101900E call loc_1019014
.aspack:01019014 loc_1019014: ; CODE XREF: .aspack:0101900Ep
.aspack:01019014 pop ebp
;вопрос на засыпку – на что показывает ebp?
;здесь очень удобна возможность IDA переходить по G “+” ; – просто ставьте курсор на нужное смещение и вперед.

Итак, ясно, что ebp показывает на начало кода/данных. В связи с этим весь остальной код теперь достаточно четок и ясен. Полагаем, понять, что делает этот код, после фокуса с ebp, уже предельно просто:

.aspack:01019035     lea     eax, [ebp+42Eh]       ;offset на строку kernel32.dll
.aspack:0101903B push eax
.aspack:0101903C call dword ptr [ebp+0F4Dh] ;вызывается GetModuleHandleA
; явное получение хендла нужно для вызова некоторых ; дополнительных функций – VirtualAlloc/VirtualFree и т.п.

Фокус с

.aspack:0101906C                 lea     eax, [ebp+77h]
.aspack:0101906F jmp eax

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

Теперь давайте посмотрим, как правильно надо сбрасывать дамп из программ, запакованных Aspack. Здесь предельно четко нужно понимать, что нам нужен файл ДО обработки его самим упаковщиков, т.е. СРАЗУ ЖЕ после разжатия. Такой момент существует не для всех упаковщиков, однако для Aspack он проявляется предельно наглядно.

Итак, Aspack выполняет разжимание секций по LZ+Хаффман-подобному алгоритму с использованием VirualAlloc для отведения памяти под временный буфер и VirtualFree для высвобождения этого буфера. В буфер вбрасывается разжатое содержимое, которое копируется двойными словами (хорошая оптимизация!) (и докопируется побайтно при необходимости) на то место, что принадлежит ему по праву – на оригинальный RVA секции. После полного разжатия и копирования Aspack принимается обрабатывать директорию импорта (см. ниже), директорию перемещаемых элементов, вычисляет и предает управление на OEP. Наша задача – сбросить дамп ДО обработки импорта, fixup-элементов и т.п. Сделаем мы это как раз здесь:

.aspack:0101916D        mov     ecx, eax        ; счетчик
.aspack:0101916F mov edi, [esi]
.aspack:01019171 add edi, [ebp+422h] ; ImageBase + section RVA
.aspack:01019177 mov esi, [ebp+152h] ; внутренние таблицы упаковщика
.aspack:0101917D sar ecx, 2 ; копирование двойнми словами – edi - приемник
.aspack:01019180 rep movsd
.aspack:01019182 mov ecx, eax
.aspack:01019184 and ecx, 3
.aspack:01019187 rep movsb ; докопировать хвостик
.aspack:01019189 pop esi
.aspack:0101918A push MEM_RELEASE ; DWORD dwFreeType
.aspack:0101918F push 0 ; в точности после VirtualAllloc
.aspack:01019191 push dword ptr [ebp+152h] ; LPVOID lpAddress
.aspack:01019197 call dword ptr [ebp+551h] ; VirtualFree
.aspack:0101919D add esi, 8
.aspack:010191A0 cmp dword ptr [esi], 0
.aspack:010191A3 jnz unpack_loop ; для каждой секции
.aspack:010191A9 push MEM_RELEASE ;---> это и есть наша цель
.aspack:010191AE push 0
.aspack:010191B0 push dword ptr [ebp+156h]
.aspack:010191B6 call dword ptr [ebp+551h] ; VirtualFree

Адрес 0х010191A9 здесь как раз и является «заветным». Зацикливая программу на этом адресе мы можем быть уверены, что дамп является полностью рабочим и не подвергнут ни обработке импорта, ни чему-либо еще. Ввести программу в бесконечный цикл можно по-разному, к примеру, введите ассемблерную команду jmp eip, воспользуйтесь командой !dump в IceExt, выберите SuspendThread, словом, вам и карты в руки. Мы же здесь, разумеется, объясним как использовать PE Tools.

Итак, после того как jnz не сработает – все секции распакованы и можно приступать. Полный дамп файла с опциями Full Dump: fix header, Full Dump: rebuild image. DumpFix, ValidatePE, RebuildPE. В дампе ручками в PE Editor: Optional Header > пересчитываем: SizeOfImage, SizeOfHeaders, Checksum (при помощи кнопки "?"). Уменьшаем размер. Делаем RebuildPE, с опциями: DumpFix, ValidatePE, RebuildPE.

Файл ПОЧТИ валиден. Осталась мелочь – параметры директории импорта и правка EP. Нам более не нужен код пакера, поэтому EP должна быть переориентирована назад – на OEP и директория импорта должна быть поправлена. Для этого залезем в алгоритм Aspack еще раз. Вспоминаем первую часть и действие функции LdrpSnapIAT, что в ntdll.dll. Все пакеры должны эмулировать ее действие, заключающееся (не только) в превращении RVA полей директории импорта в VA – взгляните на код:

.aspack:01019278       mov     esi, 12A40h     ; RVA на директорию импорта
;обратите внимание – вычисляется упаковщиком ;при обработке файла – гляньте на RVA в нормальном файле
;остается в файле в открытом виде, что очень удобно для поиска по константе

.aspack:0101927D mov edx, [ebp+422h] ; ImageBase
.aspack:01019283 add esi, edx ; в VA (VA = RVA + ImageBase)
.aspack:01019285
.aspack:01019285 process_IID: ; CODE XREF: .aspack:01019395j
.aspack:01019285 mov eax, [esi+IMAGE_IMPORT_DESCRIPTOR.Name]
.aspack:01019288 test eax, eax
.aspack:0101928A jz finish ; конец директории импорта?
.aspack:01019290 add eax, edx ; RVA на имя dll -> в VA на имя dll
.aspack:01019292 mov ebx, eax
.aspack:01019294 push eax
.aspack:01019295 call dword ptr [ebp+0F4Dh] ; GetModuleHandle
.aspack:0101929B test eax, eax
.aspack:0101929D jnz short module_already_loaded
.aspack:0101929F push ebx
.aspack:010192A0 call dword ptr [ebp+0F51h] ; LoadLibraryA
...
.aspack:010192F5 push ebx ; или имя, или ординал
.aspack:010192F6 push dword ptr [ebp+545h] ; handle
.aspack:010192FC call dword ptr [ebp+0F49h] ; GetProcAddress
.aspack:01019302 test eax, eax
.aspack:01019304 pop ebx ; *PIMAGE_THUNK_DATA32
.aspack:01019305 jnz short addr_ok ; функция вернула валидный адрес
...
.aspack:01019376 addr_ok: ; CODE XREF: .aspack:01019305j
.aspack:01019376 ; .aspack:0101935Ej
.aspack:01019376 mov [edi+IMAGE_THUNK_DATA.u1], eax ; +sizeof(IMAGE_THUNK_DATA32)
.aspack:01019378 add dword ptr [ebp+549h], 4
.aspack:0101937F jmp loc_10192B6
; после заполнения IMAGE_THUNK_DATA реальными ; адресами функций и замене RVA на VA
; директория теряет смысл для нас

Итак, RVA директории импорта ясен, осталось узнать размер. Делаем так:

:g 1019278             ;встать на mov esi, 12A40h
:d esi+edx ;видим начало IID и визуально определяем ; конец
001B:01012A40 C0 2B 01 00 FF FF FF FF-FF FF FF FF E6 2C 01 00 .+..........ж,..
001B:01012A50 F4 10 00 00 60 2B 01 00-FF FF FF FF FF FF FF FF ....`+..........
001B:01012A60 08 2E 01 00 94 10 00 00-CC 2A 01 00 FF FF FF FF ....”....*......
;он выглядит так:
:d 010134D0
0023:010134D0 65 01 47 65 74 57 69 6E-64 6F 77 54 65 78 74 57 e.GetWindowTextW
0023:010134E0 00 00 55 53 45 52 33 32-2E 64 6C 6C 00 00 00 00 ..USER32.dll....
0023:010134F0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
:? 10134f0-1012a40 ;вычисляем длину
<ulong> = 0xAB0, 2736, "\n°"

Параметры директории должны выглядеть так: RVA – 0x12A40, Size – 0xAB0. Теперь OEP:

.aspack:0101939A     mov     eax, 12420h     ; OEP RVA –
                                             ; и OEP хранится в чистом виде
.aspack:0101939F push eax
.aspack:010193A0 add eax, [ebp+422h] ; OEP VA = OEP RVA + ImageBase
.aspack:010193A6 pop ecx
.aspack:010193A7 or ecx, ecx
.aspack:010193A9 mov [ebp+3A8h], eax
;заметьте, в коде aspack много подобных инструкций – ;это его отличительная черта, сбросьте флажок Writable у ;секции aspack в вашем файле и немедленно увидите результат
.aspack:010193AF popa
.aspack:010193B0 jnz short loc_10193BA
.aspack:010193B2 mov eax, 1
.aspack:010193B7 retn 0Ch ; оригинальная dll не имела ; точки входа
.aspack:010193BA ; --------------------------------------------------------------
.aspack:010193BA
.aspack:010193BA loc_10193BA: ; CODE XREF: .aspack:010193B0j
.aspack:010193BA push 0
;операнд инструкции push заполняется динамически, ;инструкцией по адресу 010193A9
.aspack:010193BF retn ;на OEP

Меняем RVA OEP на 0х12420. Все. Дамп рабочий и полностью готов. В качестве домашнего задания – попытайтесь удалить секции Aspack. Если с тем, что здесь написано, есть внутренние сомнения – почитайте дополнительно очень хорошую статью: «unpacking files with a .aspr section», а мы, тем временем, рассмотрим один оригинальный, хотя и не новый, прием. Надеемся, вы еще помните, что мы советовали обратить внимание на pusha в начале этой главы. Вы уже знаете, что дампить программу нужно не на OEP, а, желательно, задолго до него. В случае Aspack, где в открытом виде остается директория импорта, перемещаемых элементов, распаковываются секции, до OEP добираться нет нужды. Однако, возможно представить ситуации, когда времени просто нет и нужно добежать. Тогда используйте трюк с bpm esp-4. Немедленно после pushad ставьте эту точку останова. Действие pushad можно глянуть в интеловских талмудах в виде псевдокода. Если лень глядеть, вот:

Temp ? (ESP);
Push(EAX); // 0x1C
Push(ECX); // 0x18
Push(EDX); // 0x14
Push(EBX); // 0x10
Push(Temp); // 0x0C
Push(EBP); // 0x08
Push(ESI); // 0x04
Push(EDI); // 0x00

Точка останова сработает поблизости от OEP – на popad – при вытаскивании регистров назад. Вот здесь:

.aspack:010193AF    popa			;-> точка останова сработает здесь
.aspack:010193B0 jnz short loc_10193BA
.aspack:010193B2 mov eax, 1
.aspack:010193B7 retn 0Ch ; оригинальная dll не имела точки входа
.aspack:010193BA ; ---------------------------------------------------------
.aspack:010193BA
.aspack:010193BA loc_10193BA: ; CODE XREF: .aspack:010193B0j
.aspack:010193BA push 0
.aspack:010193BF retn ;на OEP

Заметим напоследок, что трюк с bpm esp-4 сейчас работает не так уж и часто. Многие крипторы специально осложняют нам жизнь. В некоторых случаях это можно обойти, к примеру, поставив точку останова еще ниже, но с некоторыми крипторами не помогает и это. К примеру, вполне возможно поставить в блок try/catch пример на исчерпание стека в бесконечном цикле, а обработчик исключения должен это дело разобрать. Для успешного понимания таких защит необходимо четко разбираться в SEН – структурных исключениях, что и рассматривается в следующей главе.

SEH с точки зрения кракера

Рискуя повторится, тем не менее, скажем еще раз - для успешного отдирания криптора от файла необходимо неплохо разбираться в принципах работы операционной системы. Одним из таковых, и достаточно интересным, являются SEH - structured exception handling - обработчики исключений. Как всегда, мы не собираемся начинать здесь с азов. Также учтите, мы расписываем только исключения, пришедшие из user mode, т.е. ring-3. Если честно - сеть исключительно бедна сколь-нибудь хорошей информацией, приятное исключение, как всегда, Мэтт Питрек со своей изумительной статьей http://www.microsoft.com/msj/0197/exception/exception.aspx. Если хочется солидной теории – можно глянуть http://www.codeproject.com/cpp/exceptionhandler.asp Также немножечко об этом рассказывает Руссинович, как всегда, на высоте Gary Nebbett, да еще с миру по нитке можно найти в качестве предисловия к некоторым книгам по разработке драйверов. Информации по использованию SEH для облегчения жизни нам при отдирании пакера вообще практически нет - чуть-чуть в некоторых китайских статьях, да на форуме fravia (http://66.98.132.48/forum/) что-то проскакивало. Итак, предполагается, что вы внимательнейшим(!) образом прочли статью Питрека. Теперь взгляните на рисунок:

Упрощённое представление обработки исключений кольца-3 в ОС Windows 2k+
рис 5

Если что-то не ясно - не волнуйтесь, мы детально рассмотрим большинтсво случаев на протяжении этой главы. С оставшимися вы уже сумеете расправиться самостоятельно.

Не имеет смысла особо глубоко вдаваться в тонкости обработки исключений процессором Intel и реализацией этого дела в Win 2k+ (мы рассмотрим это в практическом примере). Достаточно сказать, что в один прекрасный момент времени управление получает функция KiDispatchException (ntoskrnl.exe), псевдокод которой можно найти в исходниках ReactOS (см. первую часть статьи) или в книге Неббета. А уж эта функция и будет решать, что делать - либо отладчику управление передавать, либо SEH-фреймам программы, либо еще что-то. Обработчики SEH, по сути, являются односвязанным списком (Windows никуда и не убегала от фундаментальных типов хранения данных). А это означает многое... Например, ничто не мешает перестроить список, добавив туда свой обработчик. Или, к примеру, слегка изменить тело узла, список это позволяет. Правда, SEH-списки контекстно-специфичны, ну да и что с того? Список SEH-обработчиков немножко нестандартный, вместо указателя на следующий узел, он содержит указатель на узел предыдущий (своего рода стек на основе списка), вот так:

Список обработчиков SEH
рис 6

Можно с полным на то основанием считать использование заведомо сбойных участков кода в SEH-кадрах самым настоящим антиотладочным приемом, который одинаково хорошо работает и как против ring-0 отладчика, и как против ring-3 отладчика. Автор упаковщика встраивает код, который заведомо даст сбой, в SEH-кадр. Примерно так (код из статьи Питрека очень нагляден):

__asm
{/* построить запись EXCEPTION_REGISTRATION:*/
    push    handler      // адрес функции-обработчика исключения
    push    FS:[0]       // адрес предыдущей функции-обработчика
    mov     FS:[0],ESP   // новая запись EXCEPTION_REGISTRATION
}

__asm
{
   db F1;   //такого опкода нет,
            // следовательно, произойдет исключение UD
}

__asm
{/* удалить нашу EXECEPTION_REGISTRATION: */
    mov     eax,[ESP]      // получить указатель на предыдущую запись
    mov     FS:[0], EAX    // установить в списке эту запись
    add     esp, 8         // удалить нашу EXECEPTION_REGISTRATION из стека
}


Что произойдет, когда управление получит код во втором __asm блоке? Произойдет исключение UD - invalid opcode (которое, как известно, сопоставлено с прерыванием int 06 в IDT), и управление получит обработчик _KiTrap06, что в ntoskrnl.exe:

:idt
Int Type Sel:Offset Attributes Symbol/Owner
IDTbase=80036400 Limit=07FF
...
0005 IntG32 0008:80466D06 DPL=0 P _KiTrap05
0006 IntG32 0008:80466E6A DPL=0 P _KiTrap06
:u 08:80466E6A
_KiTrap06
0008:80466E6A F744240800000200 TEST DWORD PTR [ESP+08],00020000
0008:80466E72 0F849F030000 JZ 80467217
0008:80466E78 60 PUSHAD
0008:80466E79 B823000000 MOV EAX,00000023
0008:80466E7E 668ED8 MOV DS,AX
0008:80466E81 668EC0 MOV ES,AX

Далее управление уйдет в KiDispatchException - мы не рассматриваем этот момент подробно. Потом управление уйдет к кольцо-3 - KiUserExceptionDispatcher (ntdll.dll), чтобы никогда более не вернутся в кольцо-0 (ложь, подробнее см. ниже). Функция RtlDispatchException подхватит управление дальше. Учтите, что возврат в функцию KiUserExceptionDispatcher может и не произойти (это ОЧЕНЬ важный момент, подробнее ниже). В нашем примере адрес обработчика выглядит так:

:xframe
xFrame xHandler xTable xScope
------ -------- ------ ------
0x12FFE0 0x7C4FF0B4 0x7C4E8EC8 00
try/except (0000) filter=0x7C519C1C, handler=0x7C519C2D
0x12FFB0 0x411177 0x424078 00
try/except (0000) filter=0x411E73, handler=0x411E8E
;именно 411177 и является адресом обработчика!

Давайте немного разберемся с тем, что здесь написано. Команду Soft-Ice xframe мы описывать не будем - для этого есть документация. В данном конкретном случае команда вывела отчет обо всех SEH-фреймах, зарегистрированных для данного контекста (не забываем, что мы имеем дело с односвязным списком). Обратите внимание, самый первый SEH начинается из TIB. В SoftIce выполните команду thread -x имя_процесса:

;почему бы не взять файл, запакованный telock
:thread -x telock
Extended Thread Info for thread 5B4
KTEB: FDA94A20 TID: 5B4 Process: telock(488)
...
UTEB: 7FFDE000 Except Frame: 0006FFE0 Last Err: 0000007E
;трассируем дальше и доходим до установки нового узла в списке SEH
:thread -x telock

Extended Thread Info for thread 5B4
KTEB: FDA94A20 TID: 5B4 Process: telock(488)
...
UTEB: 7FFDE000 Except Frame: 0006FF74 Last Err: 0000007E
;обратите внимание - адрес изменился! ;Давно известен главный недостаток односвязного списка – невозможность ;быстрого доступа к его произвольному элементу, например, ;хвосту списка, поэтому разработчики Windows поступили
;правильно – держится указатель на хвост, что повышает эффективность ;при добавлении новых элементов

Поле UTEB показывает адрес памяти, по которому в ring-3 расположен TIB, а поле Except Frame показывает адрес ПОСЛЕДНЕГО узла в списке SEHSEH можно также подступится не только через FS:[0], но и через TIB (FS:[18]) в которой, помимо указателя на хвост списка SEH, хранится линейный адрес самой структуры TIB, который можно использовать для доступа к ее полям). Потрассируйте программу, использующую SEH, и обратите внимание на изменение адреса в этом поле.

Самый первый SEH-обработчик ставит недокументированная функция BaseProcessStart (описывается и у Питрека, и у Рихтера), а роль последнего оплота выполняет функция kernel32.dll UnhandledExceptionFilter. Таким образом мы видим, что НИ ОДНО приложение Windows не существует без обработчика исключительных ситуаций по умлочанию (xframe даже определила его тип - try/except, что точно соответствует действительности). Некоторые вирусы используют этот факт для поиска kernel32.dll в памяти. Soft-Ice ставит API-hook (т.е., просто-напросто, лепит опкод СС) на UnhandledExceptionFilter. Немного забавно то, что отладчик нулевого кольца вынужден ставить перехват функции кольца-3, но, как мы уже упоминали, исключение 3-го кольца сначала обрабатывается в ядре, а потом возвращается назад. Т.е. когда управление попадает в ring-3, и начинают вызываться последовательно обработчики SEH, то управление в ring-0, ВОЗМОЖНО, уже не возвращается. Если ни один из обработчиков ошибку не обработал, то вызывается UnhandledExceptionFilter, которая в системе без Soft-Ice после вывода посмертного сообщения просто прибивает процесс. Некоторые пакеры (например, Ultra Protect) проверяют наличие CC в начале этой функции - вот и еще один метод поимки SoftIce (IceExt отсекает этот метод, просто запрещая ставить SoftIce СС на эту функцию).

Сам обработчик, в нашем случае может просто увеличить EIP и вернуть управление назад, так, чтобы перескочить F1. И вот теперь мы, собственно, добрались до кульминационного момента. Когда Windows вызывает обработчик исключения, ему (обработчику) становится доступной структура CONTEXT, дающая возможность менять все общедоступные регистры процессора, включая DR0-DR7.

Итак, заглянув в документацию Intel, вы теперь знаете, что единственная, доступная команда для работы с DR-регистрами - это mov. И в 3-кольце инструкция mov drx, x недоступна! Следовательно, запрос должен уйти в ядро. И этот факт путает многих. Часто встречается утверждение, что обработчик SEH работает в нулевом кольце. Чушь (9x не рассматривается)! Но вот структура CONTEXT, возможно измененная обработчиком, действительно предается в ntoskrnl, где и происходит все самое важное. Еще раз обратимся к статье Питрека:

// заметьте, RtlDispatchException может НЕ вернуть управление 
if ( RtlDispatchException( pExceptRec, pContext ) )
retValue = NtContinue( pContext, 0 );
else
retValue = NtRaiseException( pExceptRec, pContext, 0 );

Вопрос: когда RtlDispatchException возвратит управление, а когда нет? Ответ заключается в самом обработчике прерывания. Если обработчик имеет вид:

  ...
do_something;
...
ret;

то, очевидно, управление будет передано назад. Если же имеем обработчик вида:

   ...
do_something;
...
jmp;

то RtlDispatchException в ntdll.dll управление не вернет! Что это означает? Да то, что если обработчик, к примеру, намеренно изменил DR-регистры через структуру CONTEXT, то и что с того? Да ничего и не произойдет. Кроме того, есть еще и другая причина. За обработчики, не возвращающие управление в ntdll, надо, в буквальном смысле слова, отрывать уши! Почему? Да потому что при таком раскладе список SEH перестанет быть сбалансированным - появятся две новые записи, а именно –

1) запись от RtlpExecuteHandlerForException, которая тоже использует try/catch,

2) запись пользовательского обработчика. Т.е. список будет выглядеть так:

:xframe
xFrame xHandler xTable xScope
------ -------- ------ ------
0x06FFE0 0x7C4FF0B4 0x7C4E8EC8 00
try/except (0000) filter=0x7C519C1C, handler=0x7C519C2D
0x06FF74 0x101A9FE 0x42F042E 16886162 //неубранная запись пользовательского обработчика
// (scope table inaccessible)
0x06FBE8 0x77F951B6 0x06FF74 457852
//неубранная запись обработчика ntdll в RtlpExecuteHandlerForException //"я бы этому дяде с большими ушами, уши бы-то пооткрутил!"
// (scope table inaccessible)

Итак, вывод. Если обработчик намеренно меняет DR-регистры, то управление должно вернуться в ntdll.dll и быть передано функции NtContinue. Теперь мы вплотную подошли к этой любопытной функции. Из функции, реально, документирован только прототип. В кодах ReactOS по поводу NtContinue наблюдается полное отсутствие всякого присутствия. Следующий псевдокод получен путем дизассемблирования и анализа, как всегда:


#define KernelMode  0
#define UserMode    1

/*Нас ни в малейшей степени не интересует возня с
IRQL и спин-блокировками, поэтому далее демонстрируется только
действительно важный, с нашей точки зрения, код. 
Так же учтите, что опущены многие проверки и просто куски кода - в нашу
задачу не входило давать полный разбор NtContinue!*/

NtContinue(PCONTEXT pcontext, bool bAlert)
/*NtContinue из ntoskrnl.exe – Windows 2000 SP 4*/
{
    /*указатель на структуру KTRAP_FRAME - принадлежит KTHREAD
    в кодах ReactOS структура KTRAP_FRAME определена в ke.h*/
    PKTRAP_FRAME pframe;
    /*KTHREAD можно использовать либо из кодов ReactOS, либо
    из кодов w2k_def.h, созданных Шрайбером и выложенных на wasm*/
    KTHREAD kth; /*mov ebx, ds:0FFDFF124h ; KTHREAD*/
    /*однако KPCR/KPRCB предпочтительнее использовать из DDK - как-никак
    официальная документация, хоть и не полная!*/
    KPCR kp;

    if (!KiContinue(pcontext, 0, pframe))
    {
        if (bAlert == TRUE)
            KeTestAlertThread(kth.PreviousMode);

        /*KiServiceExit2*/
        __asm cli;
        if(pframe->Eflags & 0x20000 || !(pframe->Cs & 1))
            while(kth.ApcState.UserApcPending)
            {
                ... /*KiDeliverApc*/
            }
        
        kp.NtTib.ExceptionList = pframe->ExceptionList;
        if (!(kp.DebugActive & 0x0F)) /*mov ebx, large fs:50h ; KPCR.DebugActive*/
        {
            /*вот это невероятно важный момент - см. текст статьи!*/
            if(pframe->Eflags & 0x20000 || !(pframe->Cs & 1))
            /*важность настолько велика,
             что не поленимся привести ассемблерный листинг:
            .text:00465942        test    [ebp+KTRAP_FRAME.EFlags], 20000h
            .text:00465949        jnz     short to_dr2    
            ;to_dr2 соответствует коду в блоке _asm
            .text:0046594B        test    [ebp+KTRAP_FRAME.SegCs], 1
            .text:00465952        jz      short loc_4658F5
            */
            {
                _asm
                {
           /*иными словами, все то, что трудолюбиво наделано 
           в KeContextToKframes запихивается в DR-регистры именно тут,
           т.к.единственная дозволенная инструкция
           для работы с DR-регистрами - mov!*/
           mov     ebx, 0
           mov     esi, [ebp+KTRAP_FRAME.Dr0]
           mov     edi, [ebp+KTRAP_FRAME.Dr1]
           mov     dr7, ebx
           mov     dr0, esi
           mov     ebx, [ebp+KTRAP_FRAME.Dr2]
           mov     dr1, edi
           mov     dr2, ebx
           mov     esi, [ebp+KTRAP_FRAME.Dr3]
           mov     edi, [ebp+KTRAP_FRAME.Dr6]
           mov     ebx, [ebp+KTRAP_FRAME.Dr7]
           mov     dr3, esi
           mov     dr6, edi
           mov     dr7, ebx
                }
            }
        }
        else
            ... /*перезагрузка сегментных регистров
        и общих регистров из KTRAP_FRAME*/
        __asm iretd;

    }
    else /*KiServiceExit*/
    {
        __asm cli;
        if(pframe->Eflags & 0x20000 || !(pframe->Cs & 1))
            while(kth.ApcState.UserApcPending)
            {
                /*KiDeliverApc*/
            }
        kp.NtTib.ExceptionList = pframe->ExceptionList;
        if (!(kp.DebugActive & 0x0F)) /*mov ebx, large fs:50h ; KPCR.DebugActive*/
        {
            if(pframe->Eflags & 0x20000 || !(pframe->Cs & 1))
            {
                __asm
                {
           mov     ebx, 0
           mov     esi, [ebp+KTRAP_FRAME.Dr0]
           mov     edi, [ebp+KTRAP_FRAME.Dr1]
           mov     dr7, ebx
           mov     dr0, esi
           mov     ebx, [ebp+KTRAP_FRAME.Dr2]
           mov     dr1, edi
           mov     dr2, ebx
           mov     esi, [ebp+KTRAP_FRAME.Dr3]
           mov     edi, [ebp+KTRAP_FRAME.Dr6]
           mov     ebx, [ebp+KTRAP_FRAME.Dr7]
           mov     dr3, esi
           mov     dr6, edi
           mov     dr7, ebx
                }
            }
        }
        else
            ... /*перезагрузка сегментных регистров
                и общих регистров из KTRAP_FRAME*/

        __asm sti;
        __asm sysexit;
        __asm iretd;
    }
}

KiContinue(PCONTEXT pcontext, ???, PKTRAP_FRAME pframe)
{
    KTHREAD kth; /*mov eax, large fs:124h*/
    
    if (kth.PreviousMode == UserMode)
        KiContinuePreviousModeUser(pcontext, 0, pframe, kth.PreviousMode);
    else
        KeContextToKframes(pframe, 0, pcontext, pcontext->ContextFlags);
}

KiContinuePreviousModeUser(PCONTEXT context,
                  ???, PKTRAP_FRAME pframe,
                  bool PreviousMode
                  )
{
    PCONTEXT buff;
    
    if(!(context & 3))    //выровнено на границу DWORD?
        ExRaiseDatatypeMisalignment();
    else
    {
        /*довольно глупая проверка*/
if((pContext + sizeof(CONTEXT)) > MmUserProbeAddress)
            ExRaiseAccessViolation();
        memmove(buff,pcontext,sizeof(CONTEXT));
        KeContextToKframes(pframe, 0, buff, pcontext->ContextFlags);
    }
}

/*функция производит большую работу по трудолюбивому перекладыванию содержимого
из структуры CONTEXT в структуру KTRAP_FRAME, попутно делая громадное количество
проверок, поэтому код достаточно тривиален и полностью рассмотрен не будет, нас 
интересует, главным образом, тот клочок KeContextToKframes, что отвечает за
перенос DR-содержимого из CONTEXT в KTRAP_FRAME*/

KeContextToKframes(PKTRAP_FRAME pframe, ???, PCONTEXT pcontext, unsigned flags)
/*к слову сказать, функция SetThreadContext, в конечном итоге, тоже приходит сюда
 – все пути ведут в Рим*/
{
    
    ...
    if(pcontext->ContextFlags)
        (pcontext->Dr0 > MmHighestUserAddress)
            ?(pframe->Dr0 = 0):
            (pframe->Dr0 = pcontext->Dr0);
/*
.text:00430777        mov     ecx, [esi+CONTEXT.Dr0]
.text:0043077A        cmp     MmHighestUserAddress, ecx
.text:00430780        sbb     eax, eax
.text:00430782        not     eax
.text:00430784        and     eax, ecx
*/
    if(pcontext->ContextFlags)
        (pcontext->Dr1 > MmHighestUserAddress)
             ?(pframe->Dr1 = 0):
             (pframe->Dr1 = pcontext->Dr1);
    if(pcontext->ContextFlags)
        (pcontext->Dr2 > MmHighestUserAddress)
             ?(pframe->Dr2 = 0):
             (pframe->Dr2 = pcontext->Dr2);
    pframe->Dr6 = pcontext->Dr6 & 0xE00F;
/*1110000000001111 – B0-B3, BS, BD, BT-флаги*/
    pframe->Dr7 = pcontext->Dr7 & 0x155;  
/*101010101 – L0-L3, LE-флаги – особое внимание на LE-флаг
 – контекстные точки останова*/
/*
.text:004305AA        mov     eax, [esi+CONTEXT.Dr7]
.text:004305AD        and     ax, 155h
...
.text:004305B3        mov     [ebx+KTRAP_FRAME.Dr7], eax
*/
/*чтобы понять смысл – глава 15 3-го тома Intel Manual
 – “debugging and performance monitoring”*/

//хм, медвежья услуга, однако!
KTHREAD.DebugActive = CONTEXT.Dr7 & 0x55 ? 1 : 0;    

/*
.text:004305B8        mov     eax, large fs:124h
.text:004305BE        mov     ecx, eax
.text:004305C0        mov     eax, [esi+CONTEXT.Dr7]
.text:004305C3        and     al, 55h
.text:004305C5        neg     al
.text:004305C7        sbb     eax, eax
.text:004305C9        neg     eax
.text:004305CB        mov     [ecx+KTHREAD.DebugActive], al
*/
    ...
}

Если приведенный выше кусок псевдокода привел вас в ужас - ничего. Все вполне можно уложить в пару фраз. Итак, так или иначе, управление будет передано в KeContextToFrames, единственная задача которой - переложить содержимое полученной извне (это может быть и кольцо-0 в случае драйвера!) структуры CONTEXT в структуру KTRAP_FRAME. Заметьте, структура KTRAP_FRAME еще не означает DR-регистры. Код, перекладывающий содержимое KTRAP_FRAME, полученное после KeContextToFrames, расположен в NtContinue, а точнее, либо в области KiServiceExit, либо в области KiServiceExit2. Внимательнейшим образом рассмотрите инлайн-ассемблер и, самое главное, предшествующую ему проверку.

Что это означает для нас с вами? Смотрите, у пакера нет выбора. Если пакеру необходимо изменить DR-регистр, он ДОЛЖЕН передать управление в ядро, где программа кольца-3 не в силах проконтролировать установку точек останова, и момент передачи управления на код, работающий с DR-регистрами, может быть нами подправлен как нам заблагорассудится!

Очевидно, что если переделать в абсолютный jmp один условный переход, то DR-регистры останутся неискаженными, следовательно, мы свободно сможем пользоваться нашей любимой командой bpm. И все – наполнение мусором DR-регистров (как это любит делать asprotect) нам теперь до лампочки. Неужели все так просто? К сожалению, нет. Да, действительно, нет никаких принципиальных сложностей в изменении условных переходов (скажем, можно привинтить в IceExt команду, которая будет ставить подобную защиту при необходимости и снимать ее, когда отпадет надобность), но и авторы упаковщиков даже не даром свой хлеб едят. Что вы скажете о защите, которая сама ставит точки останова, используя DR-регистры, т.е. трассирует сама себя? Т.е., если DR-содержимое очищено или оставлено без изменений с помощью трюка с NtContinue, точка останова не сработает и программа узнает, что дело плохо. Вероятно, в общем случае, разработать решение этой проблемы не удастся... Подумайте, возможно, именно вы сумеете разобраться как можно проэмулировать этот трюк, если это вообще реализуемо.

Теоретическая глава почти закончена. Единственная вещь, не рассмотреная нами и представляющая определенный практический интерес, называется scope table. Как уже говорилось, Visual C++ ставит дополнительные данные в EXCEPTION_REGISTRATION - добавляются некоторые структуры (конкретнее - см. файл exsup.inc). Далее обратите внимание на код startup процедуры в crt0.c (стандартном startup-коде) - он обернут в _try/catch. Причем этот _try уже идет с дополнительными структурами, т.е. появляется scope table. Скажем, довольно несложно разработать драйвер, который будет трассировать пакер вплоть до момента появления scope table, которое должно означать, что начал исполняться пользовательский код. С другой стороны, что помешает пакеру установить ложную scope table... С третьей стороны - можно просто выдать пользователю отчет, а уж он пусть сам разбирается, что к чему... С четвертой стороны, есть еще Borland и Watcom со своей реализацией, с пятой ... И так до бесконечности.

Спешим «порадовать» еще одной «приятной» новостью – фирма MS в Windows XP ввела так называемые векторные исключения – VEH, имеющие приоритет над SЕH, т.е., выполняющиеся перед ними. Умница Питрек и тут не подкачал – читайте его статью.

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

Немного об антиотладке – в преддверии telock

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

Практически в любой статье по снятию упаковщика рассказано о трюке применения int 1 в SEH-кадре и том, что это помогает обнаружить Soft-Ice. Однако ни слова не сказано о том, ПОЧЕМУ И КАК это происходит. Давайте попробуем разобраться. Хотелось бы сразу сказать, что лезть придется глубоко – в ядро Windows. Итак, поехали!

Очевидно, чтобы избежать антиотладочных фокусов, нужно понимать, что такое отладка и какие средства в нашем распоряжении есть. А это требует чертовски хорошего понимания защищенного режима и обработки прерываний защищенного режима. Значит – интеловские талмуды!

Итак, вполне понятно, что int 1 в качестве антиотладки можно использовать только в SEH-кадрах, иначе не выживет сама программа. Стандартно можно поступить так:

push    handler
push FS:[0]
mov FS:[0],ESP
int 1
;само по себе использование int1 ; – это еще НЕ антиотладка, антиотладка будет дальше
pop dword prt FS:[0]
add esp, 4

Более изощренный код, в котором адрес обработчика исключения уже не столь очевиден, может выглядеть так:

CODE:00401000        call    here
...
here:
; в этом случае весь фокус сводится к действию call, ; загоняющей в стек адрес возврата –
; обработчик расположен по адресу 401005 – см. ниже.
CODE:0040101C push dword ptr fs:0
CODE:00401022 mov fs:0, esp
CODE:00401028 int 1

Так же учтите, что удаление SEH-кадра тоже можно проделывать не только через pop, а и прямыми манипуляциями со стеком – mov esp … Чтобы не поплыть во всем этом, надо четко представлять себе состояние стека в обработчике прерывания. На эту тему есть одна очень хорошая статья –
http://win32assembly.online.fr/Exceptionhandling.html - не поленитесь внимательно посмотреть картинки, или смотрите программу на С ниже в этой же главе. Кстати, переполнение стека – стандартный способ проникновения в систему – можно вызывать и из обработчика SEH. Читните http://www.peterszor.com/blended.pdf - очень познавательно.

А мы, тем временем, продолжим разбор обработчика с int 1. Очень внимательно рассмотрите этот код:

    push xHandler
    assume fs:nothing
    push dword ptr fs:[0]
    mov    fs:[0], esp
    assume fs:error

Int1::
    int 1

    ; Here if i1here is on
ExceptionNotCatched:
    MessageBox - Exception not catched = i1here is on!
    jmp на выход

NticeFound::
    MessageBox - NtIce found
    jmp на выход

NticeNotFound::
    MessageBox - NtIce not found
    jmp на выход

    ; Remove seh-frame  
    assume fs:nothing
    pop fs:[0]
    add esp, sizeof DWORD
    assume fs:error

;
; обработчик исключения - xHandler
;

mov ecx, pContext
mov eax, [pContext.Eip]    ;Eip структуры CONTEXT
cmp eax, Int1        
;вот в этом cmp весь и смысл!
;Поведение процессора(!) будет различаться при установленном Soft-Ice и без него!
;cм. текст статьи!
jnz els
; SoftIce неактивен
mov [pContext.Eip], NticeNotFound
jmp rt
els::
; SoftIce активен
mov [pContext.Eip], NticeFound
rt::
; передать управление на NtContinue
xor eax, eax
ret

В системе БЕЗ Soft-Ice int 1 имеет DPL = 0 (если c IDT чувствуете себя неуверенно – хорошая ссылка: http://www.acm.uiuc.edu/sigops/roll_your_own/i386/idt.html):

[o] INT 00000001 Location: 00000008:80466786 DPL: 00000000 Type: 32bit Interrupt Gate
; адрес показывает на KiTrap01, что в ntoskrnl

Быстренько вспоминаем, что происходит, когда задача с меньшим DPL пытается обратится к сегменту с большим DPL? Правильно – нарушение защиты #GP! В системе БЕЗ Soft-Ice int 1 имеет DPL = 0 и, в случае намеренного (хм, или ненамеренного) сбоя в программе кольца-3 (DPL = 3), будет вызван обработчик KiTrap0D, выполняющий следующие действия:

/*KiTrap0D*/
...
; вычислить адрес сбойной инструкции ; и положить его на вершину стека
.text:00467ECF mov eax, [esp+KTRAP_FRAME.SegCs]
.text:00467ED3 shl eax, 4
.text:00467ED6 add eax, [esp+KTRAP_FRAME.Eip] ; cs+eip
.text:00467EDA xor edx, edx
.text:00467EDC mov ecx, ss:[eax]
.text:00467EDF mov dl, cl
.text:00467EE1 mov dl, ss:OpcodeIndex[edx]
; массив из DWORD – наверняка будет ; любопытен для любого специалиста по опкодам
.text:00467EE8 jmp ss:off_473F04[edx*4]
...

в конечном результате которых мы имеем код 0xC0000005(!) в ExceptionRecord->ExceptionCode! Еще раз – произошло int 1, а имеем – access violation!

Что же произойдет в системе, где Soft-Ice установлен?

[o] INT 00000003 Location: 00000008:F404EF87 DPL: 00000003 Type: 32bit Interrupt Gate
; изменился DPL обработчика и АДРЕС – об адресе см. ниже

Т.о. DPL обработчика int 1 изменился. Что это значит? В системе с Soft-Ice результат будет другим. Ice меняет DPL int 1 с 0 на 3, в результате чего access violation более НЕ происходит, KiTrap0D НЕ вызывается, а имеем KiTrap010x80000004 - STATUS_SINGLE_STEP! Поведение процессора тоже будет отличаться. Теперь EIP показывает не на инструкцию, вызвавшую access violation, а на инструкцию, следующую после нее! Более подробно – интеловские мануалы – разница между trap и fault.

/*KiTrap01*/
.text:004667EF and [ebp+KTRAP_FRAME.EFlags], 0FFFFFEFFh ;eip показывает на инструкцию ПОСЛЕ int 1
.text:004667F6 mov ebx, [ebp+KTRAP_FRAME.Eip]
.text:004667F9 mov eax, STATUS_SINGLE_STEP
.text:004667FE jmp to_CommonDispatchException
;далее управление уйдет на KiDispatchException, ;которая передаст его по подправленному адресу назад

Ну а далее, можно либо проверять флаг, либо переориентировать EIP через структуру CONTEXT.

Нет нужды верить на слово. Все элементарно можно проверить. Вот программа:

#include <windows.h>
#include <stdio.h>

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,	//+0x4
    void * EstablisherFrame,		         //+0x8
    struct _CONTEXT *ContextRecord,             //+0xC
    void * DispatcherContext )           	//+0x10
{

    printf("STATUS: %x, EIP: %x\n",
           ExceptionRecord->ExceptionCode,
           ContextRecord->Eip
           );
/*в случае Soft-Ice EIP показывает на mov eax,[ESP] и статус 80000004,
 без Soft-Ice EIP показывает на int 1 и статус c0000005*/

//раскомментируйте строчку ниже для системы БЕЗ Soft-Ice
//ContextRecord->Eip += 2; /*проскочить int 1 и встать на mov eax,[ESP]*/      
    
    return ExceptionContinueExecution;
}


int main(void)
{
    unsigned long handler = (unsigned long)_except_handler;
    
    __asm
    {
        push    handler        
        push    FS:[0]         
        mov     FS:[0],ESP
        int 1
        mov     eax,[ESP]      
        mov     FS:[0], EAX    
        add     esp, 8         
    }

    return 0;

}

Защитится от этого можно элементарно – пропатчив IDT или поставив IceExt, который восстановит DPL int 1 в прежнее состояние.

Что до int 3 – то само по себе использование int 3 в SEH-кадрах не есть антиотладка – статус будет всегда одним и тем же – STATUS_BREAKPOINT. Специфичной антиотладкой int 3 становится тогда, когда к нему добавляются переданные в ax, dx и т.п. параметры. Например, известный трюк с командой HBOOT. Если в Soft-Ice выполнить данную команду, произойдет мнгновенная перезагрузка. Того же эффекта можно достичь через backdoor интерфейс, что, к примеру, описано у Hex’a (проект xtin.org). Если помните, мы говорили, что int 3 обязательно должен иметь опкод СС и никакого другого. Windows явно рассчитывает на это. Кусок кода из KiTrap03 тому примером:

/*KiTrap03*/
.text:00466AE2 mov ebx, [ebp+KTRAP_FRAME.Eip]
.text:00466AE5 dec ebx
;заметьте, предполагается использование ОДНОБАЙТНОЙ инструкции – eip — ;и это еще раз подтверждает, что MS не ;готова к использованию CD 03 в качестве опкода, ;а лишь понимает СС – это явно указано во 2 томе интела
.text:00466AE6 mov ecx, 3
.text:00466AEB mov eax, STATUS_BREAKPOINT
.text:00466AF0 call CommonDispatchException
;далее управление уйдет на KiDispatchException, ;которая передаст его по подправленному адресу назад, и, если ;опкод был CD 03 – это вызовет повторное исключение

Кстати, Soft-Ice тоже лжет. Выполните команду idt и вы увидите следующее:

...
0001 IntG32 0008:80466786 DPL=3 P _KiTrap01
0002 IntG32 0008:0000145E DPL=0 P
0003 IntG32 0008:80466A5E DPL=3 P _KiTrap03
...

т.е., якобы, ничего не изменилось. В действительности же имеем:

[o] INT 00000001 Location: 00000008:F404EF69 DPL: 00000003
               Type: 32bit Interrupt Gate
[o] INT 00000002 Location: 00000008:F404EF78 DPL: 00000000
               Type: 32bit Interrupt Gate
[o] INT 00000003 Location: 00000008:F404EF87 DPL: 00000003 Type: 32bit Interrupt Gate

т.е. Soft-Ice перехватывает int 1/int 3 (кстати, не только их!) на свои заглушки, а это значит - не стоит всегда верить и Soft-Ice. И вообще, начитавшись всего этого, недолго и параноиком стать. Все врут.

Практический пример: tElock

Эта цель отнюдь не является легкой. В качестве антиотладки очень активно используются SEH-кадры, набитые всякой гадостью. Однако, используемые приемы однотипны. Так, вся защита программы построена на одном веселом принципе. Смотрите:

0008:01019DFE  8B642408            MOV       ESP,[ESP+08]
0008:01019E02 EB0D JMP 01019E11
0008:01019E04 33DB XOR EBX,EBX ;начинаем плясать отсюда
0008:01019E06 64FF33 PUSH DWORD PTR FS:[EBX]
0008:01019E09 648923 MOV FS:[EBX],ESP
0008:01019E0C F1 INVALID
0008:01019E0D F7F3 DIV EBX
;это вызывает исключение и управление переходит в ядро, ;что нам, в данном случае, ни к чему
;поэтому используем команду xframe для определения ;адреса обработчика ; – она работает безукоризненно
;обработчик, в данном случае, расположен по адресу 01019DFE ; – внимательно посмотрите куда ведет безусловный ;переход!
0008:01019E0F EBE8 JMP 01019DF9
0008:01019E11 EB02 JMP 01019E15
;опять в обработчик! Если вы внимательно читали главу о SEН, ;то уже должно быть ясно, ;что оставлять в списке SEН всякое барахло нельзя, ;поэтому программа должна убрать обработчик
0008:01019E13 FF20 JMP [EAX]
0008:01019E15 83F833 CMP EAX,33
;ну очень полезная инструкция – и таких в коде много – автор пытается запутать нас, ;перемешивая реальный код с мусором
0008:01019E18 2BC9 SUB ECX,ECX
0008:01019E1A 648F01 POP DWORD PTR FS:[ECX]
0008:01019E1D 59 POP ECX
0008:01019E1E EB01 JMP 01019E21
;обработчик корректно удален из списка, можно продолжать дальше

Что мы можем этому противопоставить? Очевидно в ядро, наш старый добрый знакомый, нам заходить ни к чему. Поэтому имеет смысл переориентировать EIP на безопасный клочок внутри SEH-кадра и продолжать двигаться дальше. Но это только в теории. На практике, Soft-Ice очень не любит такие конструкции, поэтому проходит через SEH-фреймы пешком очень не рекомендуется. Поскольку строение и принципы работы SEH теперь понятны, постарайтесь всегда узнавать адрес обработчика ДО вхождения в кадр и сразу переориентируйте EIP на него. Только не стоит забывать о такой вещи как балансировка стека и значения регистров. Иначе очень быстро допереориентируетесь...

Мы рассмотрим пример самотрассировки в telock – это высший пилотаж на DR-регистрах, а не просто тупое заполнение их мусором. Итак, приложение третьего кольца действительно может записать что-то в DR-регистры, но только не прямо, а опосредованно - через SetThreadContext или SEН. Значит, мы должны ожидать доступа по CONTEXT.drx и возврата управления в ядро. И это не заставит себя ждать. Вперед:

seg000:0101907A        call    $+5
seg000:0101907F pop ebp ; ebp == eip
seg000:01019080 lea eax, [ebp+46h] ; eax = eip+0x46
seg000:01019083 push eax ; смещение на обработчик
seg000:01019084 xor eax, eax
seg000:01019086 push dword ptr fs:[eax]
seg000:01019089 mov fs:[eax], esp
;лишь команды, помеченные жирным, ;являются значимыми – остальное мусор, но, ;зато, как используется!
seg000:0101908C int 3 ; Trap to Debugger
seg000:0101908D nop
seg000:0101908E mov eax, eax
seg000:01019090 stc ;первая hardware breakpoint
seg000:01019091 nop
seg000:01019092 lea eax, ds:1234h[ebx*2]
seg000:01019099 clc ;вторая hardware breakpoint
seg000:0101909A nop
seg000:0101909B shr ebx, 5
seg000:0101909E cld ;третья hardware breakpoint
seg000:0101909F nop
seg000:010190A0 rol eax, 7
seg000:010190A3 nop ;четвертая hardware breakpoint
seg000:010190A4 nop
seg000:010190A5 xor ebx, ebx ;исключение #DE
seg000:010190A7 div ebx
seg000:010190A9 pop dword ptr fs:0
seg000:010190AF add esp, 4
seg000:010190B2 mov si, 4647h
seg000:010190B6 mov di, 4A4Dh
seg000:010190BA mov al, [ebp+99h]
seg000:010190C0 jmp work_with_PE
;передать управление дальше – на работу с PE-файлом, ;здесь не рассматривается

int 3 вызывает исключение STATUS_BREAKPOINT. Управление переходит в ядро и возвращается на обработчик. В данном случае его адрес - 10190C5. Заглянем туда:


;указатель на структуру ExceptionRecord
seg000:010190C5       mov     eax, [esp+4]
;указатель на структуру CONTEXT
seg000:010190C9       mov     ecx, [esp+0Ch]
seg000:010190CD       inc     [ecx+CONTEXT.Eip]
seg000:010190D3       mov     eax, [eax+EXCEPTION_RECORD.ExceptionCode]
seg000:010190D5       cmp     eax, STATUS_INTEGER_DIVIDE_BY_ZERO
seg000:010190DA       jnz     short loc_1019100
;первое исключение – int 3 не даст нам этот код возврата,
;поэтому в первый раз этот прыжок будет выполнен
;этот код еще долго не получит управления...
seg000:010190DC       inc     [ecx+CONTEXT.Eip]
;EIP теперь показывает на pop dword ptr fs:0 –
; корректный выход из SEH

; ExceptionContinueExecution
seg000:010190E2       xor     eax, eax

; Dr0-3 <- 0
seg000:010190E4       and     [ecx+CONTEXT.Dr0], eax
seg000:010190E7       and     [ecx+CONTEXT.Dr1], eax
seg000:010190EA       and     [ecx+CONTEXT.Dr2], eax
seg000:010190ED       and     [ecx+CONTEXT.Dr3], eax
seg000:010190F0       and     [ecx+CONTEXT.Dr6], 0FFFF0FF0h
; 11111111111111110000111111110000 - флаги B0-B3,
; BD, BS, BT - занулить
seg000:010190F7       and     [ecx+CONTEXT.Dr7], 0DC00h
; 1101110000000000 – флаг GD в 1, R/W в 1
seg000:010190FE       jmp     short ret
seg000:01019100 ; ------------------------------------------------
seg000:01019100 
seg000:01019100 loc_1019100:                  ; CODE XREF: seg000:010190Dj
seg000:01019100       cmp     eax, STATUS_SINGLE_STEP
; опять таки, и это не следствие CC –
; эта ветка должна сработать 4 раза - после отработки всех точек останова 
; т.к. процессор возбуждает int 1
seg000:01019105       jz      short loc_1019113
seg000:01019107       cmp     eax, STATUS_BREAKPOINT
; а вот это то, что произойдет после int 3 
; случилось что-то, что мы не можем обработать ExceptionContinueSearch
seg000:0101910C       jz      short fill_dr
seg000:0101910E       push    1
seg000:01019110       pop     eax
seg000:01019111       jmp     short ret
seg000:01019113 ; ------------------------------------------------
seg000:01019113 
seg000:01019113 loc_1019113:                   ; CODE XREF: seg000:01019105j
seg000:01019113     call    loc_1019119
seg000:01019113 ; ------------------------------------------------
seg000:01019118       db    4 ;     
;этот байт здесь поставлен не случайно –
;модифицируется инструкцией по адресу 0101911A – self-modifying code
seg000:01019119 ; ------------------------------------------------
seg000:01019119 
seg000:01019119 loc_1019119:                  ; CODE XREF: seg000:01019113p
seg000:01019119       pop     eax             ; eax = eip
seg000:0101911A       inc     byte ptr [eax]  ; изменить опкод по 01019118 
seg000:0101911C       sub     eax, eax        ; ExceptionContinueExecution
seg000:0101911E       jmp     short ret
seg000:01019120 ; ------------------------------------------------
seg000:01019120 
seg000:01019120 fill_dr:                 ; CODE XREF: seg000:0101910Cj
;итак, ebp, как вы помните, используется тут
;для относительной адресации от адреса 101907f – см. вход в SEH
;обязательно проверьте эти значения –
;вернитесь к коду внутри SEH-кадра и посмотрите где сработают точки
;останова
seg000:01019120       mov     eax, [ecx+CONTEXT.Ebp] ; 
seg000:01019126       lea     eax, [eax+24h]   ; ebp+24
seg000:01019129       mov     [ecx+CONTEXT.Dr0], eax    ;ставим DR0 на ebp+24
seg000:0101912C       mov     eax, [ecx+CONTEXT.Ebp]
seg000:01019132       lea     eax, [eax+1Fh]   ; ebp+1F                     
seg000:01019135       mov     [ecx+CONTEXT.Dr1], eax    ;ставим DR1 на ebp+1F
seg000:01019138       mov     eax, [ecx+CONTEXT.Ebp]
seg000:0101913E       lea     eax, [eax+1Ah]  ; ebp+1A
seg000:01019141       mov     [ecx+CONTEXT.Dr2], eax    ;ставим DR2 на ebp+1A
seg000:01019144       mov     eax, [ecx+CONTEXT.Ebp]
seg000:0101914A       lea     eax, [eax+11h]  ; ebp+11
seg000:0101914D       mov     [ecx+CONTEXT.Dr3], eax    ;ставим DR3 на ebp+11
seg000:01019150       xor     eax, eax
seg000:01019152       and     [ecx+CONTEXT.Dr6], 0FFFF0FF0h ; 
; 11111111111111110000111111110000 – флаги B0-B3, BD, BS, BT - занулить
seg000:01019159       mov     [ecx+CONTEXT.Dr7], 155h 
; 101010101 – G0, G1, G2, G3 - занулить
seg000:01019160 
seg000:01019160 ret:           ; CODE XREF: seg000:010190FEj
seg000:01019160                ; seg000:01019111j ...
seg000:01019160       retn     ; возвратить управление в ядро на NtContinue
                               ; код действия Windows – в eax
work_with_PE:
...

Если вы все еще с нами – продолжаем! Итак, обработчик int 3 записывает в DR значения ВНУТРИ ТОГО ЖЕ САМОГО кадра SEH. Управление ОПЯТЬ передается в этот же самый злополучный кадр! Четыре раза должны отработать DR-регистры. После четвертого раза происходит исключение типа деление на ноль. И тут-то получает управление ветка, которую можно назвать «на дорожку». Правится EIP, во все DR-регистры прописываются нули, ставятся маски на управляющие DR6-7 и управление идет дальше – на метку work_with_PE. И это все! Больше работы с DR-регистрами в коде telock нет!

Немножко по поводу DR-регистров и Soft-Ice, который действительно является чудом. Dr-регистры явно не отображаются и принудительно могут быть выведены командой CPU. Записать в DR-регистры что-нибудь можно лишь опосредовано – через bpm с явным указанием одного из четырех (DR0-DR3). Кроме того, умничка Soft-Ice умеет отображать модельно-специфичные регистры – MSR. Для этого есть одноименная команда и более усовершенствованный вывод сообщений при срабатывании bpm:

Break due to BP 01: BPMB USER32!MessageBoxA X DR3  (ET=19.91 milliseconds)
MSR LastBranchFromIp=0101A2D0 <- полезный адрес!
MSR LastBranchToIp=77E38098

Читаем в интеловских талмудах – том 3 глава 15 “Debugging and performance monitoring”: “The LastBranchToIP and LastBranchFromIP MSRs are 32-bit registers for recording the instruction pointers for the last branch, interrupt, or exception that the processor took prior to a debug exception being generated”.

Так это же вообще шикарно! LastBranchFromIp – это «откуда», LastBranchToIp – это у нас «куда». Как это можно использовать? Ну... Встретите защиту вида:

/*здесь намеренное использование FPO – frame pointer omission –
 т.е., ebp отсутствует и команда ws не поможет*/
/*более подробно см. статью «Playing with the stack» на http://www.codeproject.com или, к примеру, сие:
http://www.codeguru.com/misc/stackdumper.html */

MessageBoxA(“Да пошел ты...”); //угадайте, что произойдет, // ДАЖЕ если у вас стоит bpx MessageBoxA?
ExitProcess();

и все сразу станет ясно...

Ну а что дальше? Дальше – еще веселее. Безумные SEH-кадры с инсталляцией головы списка наподобие:

seg000:01019B77                 mov     [esp+4], eax
seg000:01019B7B mov fs:0, esp

и int 68h внутри. Однако, смеем надеяться, более проблем они вам не доставят! Да, они головоломны, да, код перенасыщен мусором, да, обилие зашифрованных сегментов – но дорогу осилит идущий! Самое главное – чисто пройти головоломный трюк с DR. Все. После него вы спокойно можете ставить bpm. И не слушайте всяких рассказов, что, мол, bpm в telock ставить нельзя. Можно!

Смело ставьте bpm GetProcAddress x (telock использует его напрямую, без всяких сканирований директории экспорта):

seg000:0101A574              call    dword ptr [ebp+40BAE0h] ; GetProcAddress
seg000:0101A57A
seg000:0101A57A loc_101A57A: ; CODE XREF: seg000:0101A565j
seg000:0101A57A inc eax
seg000:0101A57B dec eax
seg000:0101A57C jnz short good_addr
...
seg000:0101A5B1 good_addr: ; CODE XREF: seg000:0101A57Cj
seg000:0101A5B1 mov [edi], eax

Собственно, не имеет смысла описывать этот цикл. Важно отметить – нет нужды во многочисленных плагинах под ImpRec, возьмите директорию отсюда. Остерегайтесь ставить bpx на API напрямую, используйте bpm или старый трюк с bpx API+x, т.к. telock проверяет вызываемые функции на СС в начале (разумеется, точки останова на Native API никто проверить не сможет). bpm LoadLibraryA и bpm GetProcAddress – наши отправные позиции. Используйте их просто как зацепку, т.к. нам нужна ЧИСТАЯ, НЕИЗМЕНЕННАЯ директория импорта. Куски кода можно просто дампить IceExt по ходу дела и бинарные сегменты дизассемблировать прямо в IDA. Дамп – разбор кода, дамп – разбор кода. Можно, скажем, поступить и еще оригинальнее – известно, что в один прекрасный момент времени директория (да и не только она) является полностью разжатой – просто выполните в Soft-Ice поиск имен библиотек или предполагаемых функций (например, «kernel32.dll»), в памяти и разберитесь на глаз с диапазонами директории). Словом, дорогу осилит идущий.

На вашу совесть также мы оставляем трюк с мьютексами. После прочтения Джеффери Рихтера он не должен быть источником проблем.

Список литературы

Эта статья никогда не была бы написана, если бы не громадный ворох литературы, который был основательно перелопачен. В подавляющем большинстве – это англоязычные книги, лишь для нескольких из них был выполнен перевод. Поэтому, как это не прискорбно, знание языка ОБЯЗАТЕЛЬНО, если хотите быть в потоке.

  1. Соломон, Руссинович, “Inside Windows 2000, 3rd edition” – это теория. Но какая! Здесь есть все. И, если сочетать ее с практическими книгами, вроде 2 и 3 – то результат будет. Есть русскоязычный перевод.
  2. Шрайбер “Недокументированные возможности Windows 2000” – сугубо практическая книга, ценность которой... Мама родная – там все есть. И есть русскоязычный перевод.
  3. Gary Nebbett “Windows NT/2000 Native API Reference” – English only. Перевода нет. Книга описывает прототипы и очень многие структуры данных Native API. Есть практические примеры использования Native API. Опять таки – a must.
  4. Джеффери Рихтер “Windows для профессионалов” – практическая книга. Очень сильная и очень полезная для любого. Есть русскоязычный перевод.

Список литературы ни в коем случае не ограничивается лишь этими 4-мя книгами. Мы старались давать как можно больше линков по ходу статьи – они стоят того, чтобы их прочесть.

Что до литературы по распаковке – такой, к сожалению, нет принципиально. Со статьями тоже напряженка. В подавляющем большинстве статей расписывается лишь как давить на кнопочки, или, что еще лучше – приводятся километровые ассемблерные листинги без комментариев, превращая статью в нечто нечитабельное. Здесь хочется похвалить Касперски, который очень удачно выбирает размер листингов в своих книгах – 10-20 очень тщательно подобранных строк – самое то!

В заключение хотелось бы сказать о третьей части. Готова она будет не скоро и при ее написании мы будем учитывать, что, наконец-то, появились две великолепнейшие статьи - по Asprotect by seeQ и по Armadillo 3.10 by dragon. Обе статьи доступны на xtin.org и являются редким исключением среди большинства статей по упаковщикам. Авторы проделали прекрасную работу. Спасибо им. Теперь вроде все. Да, если есть замечания, пожелания – или NEOx на uin.ru или я на форуме wasm’a готовы их выслушать.

Выводы

Ну что ж. Вот и закончена очередная часть. Будем надеятся, вы усвоили две главные вещи.

Вещь 1: программа исполняется не в вакууме! Windows создает и поддерживает огромное количество сложнейших структур данных, поддерживающих каждый процесс, каждый поток. Чем больше ваше понимание об этих механизмах – тем легче ломать. Применение ImpRec/PE Tools и иже с ними не освобождает от понимания принципов, лежащих в основе этих и многих других утилит.

Вещь 2: с пакером надо РАБОТАТЬ. Наплюйте на файловые анализаторы, generic-анпакеры (например, такие как проект GUW by Christoph Gabler - http://wave.prohosting.com/mackt/main.htm) - это все хорошо, но разработать общее решение пока не удается. Уж больно много всего учитывать надо. Требуется больше времени и куда больше усилий, чтобы разработать общую схему, если она существует.

Благодарности

Информационное поле статьи было чудовищным и неподъемным для одиночки. Мне помогали очень многие люди и всем им я хочу сказать – без вас этой статьи бы НЕ БЫЛО! Мы с Four-F плечом к плечу прошли через структуры кольца-3 и кольца-0 и когда я начинал захлебываться в этом – меня было кому поддержать! Sten всегда откликался на призывы о помощи и мы вели долгие беседы о SEH. Dr.Golova долго и терпеливо вразумлял неуча о трюке с NtContinue, а также долго и нудно многократно вычитывал эту статью в поисках ошибок. Fixer и Alephz были со мной во время написания главы о сигнатурах. Alephz любезно предоставил исходный код СС – утилиты-поисковика сигнатур криптоалгоритмов в исполняемом файле (доступна для скачивания с wasm.ru), а Fixer выработал ряд правил для описания сигнатуры. Умничка Quantum... В одиночку UPX я бы не поднял никогда. А сколько времени он потратил, трудолюбиво переводя с испанского все турториалы Рикардо Нарвахи и сколько времени было потрачено в попытках извлечь оттуда какие-нибудь ценные методики или приемы... The Svin и мр. Хемуль Советикус вместе с masquer крепко пинали за декомпилирование. Broken Sword был моим консультантом по DR-регистрам, а Edmond сумел сверстать эти 40 страниц ужаса. Asterix пинался по поводу Aspack, вынуждая нас с NEOx’ом снова и снова переписывать ту несчастную главу, а seeQ, гм, seeQ просто сказал много хороших слов, в которых я нуждался после пинков Asterix. Спасибо также Kai Zheng, который любезно согласился перевести китайскую статью по telock и директории импорта. Скажите этим людям спасибо! Они много сделали для вас, читатель!

© Volodya / HI-TECH, NEOx / UINC.RU

0 5.418
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532