Низкоуровневое программирование для Дzenствующих

Вид материалаДокументы

Содержание


Volodya/HI-TECH, NEOx/UINC.RU Об упаковщиках в последний раз (часть 2.1)
1. Требования повышаются!2. Вместо введения3. Как Windows работает с секциями PE-файла
Вместо введения
Символы отладки и эталонный отладчик
TIB *Self; //fs:[18] /*вот из-за этого поля вся и путаница - это указатель на начало структуры TIB
ReactOS (teb.h
LordPe, что он вам покажет в поле SizeOfImage
TEB (и входящая в нее структура TIB
TEB" только тогда, когда смещение в fs
Native API
SDT - это команда ntcall
NT_TIB в составе KPCR
Eprocess, * peprocess
Как Windows работает с секциями PE-файла
DDK и интернетом, расправиться не так уж и сложно. Код, правда, насыщен функциями для работы с IRQL
Крис Касперски ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ (избранное)
Подобный материал:
1   ...   27   28   29   30   31   32   33   34   ...   42

Volodya/HI-TECH, NEOx/UINC.RU

Об упаковщиках в последний раз

(часть 2.1)


“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

Главный корректор:
Edmond/HI-TECH
1. Требования повышаются!
2. Вместо введения
3. Как Windows работает с секциями PE-файла

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


Итак, мы продолжаем. Подразумевается, что вы ознакомились с первой частью статьи и теперь владеете минимумом теории. Однако мы еще более повышаем требования. Теперь, для успешного и полного понимания 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 по этому адресу:
ссылка скрыта установить и корректно настроить их. Обязательно учтите – символы должны ТОЧНО соответствовать вашей ОС и установленному SP. Для этого лучше использовать технологии MS. NuMega Symbol Retriever показал себя нестабильно работающей утилитой. Поэтому не поленитесь, если есть такая возможность, сгрузить файлы ручками, прогнать их через NMS-транслятор и проверить командной table, все ли у нас в порядке. Кстати, если по каким-то, одним разработчикам понятным причинам, Soft-Ice ну никак не желает откликаться на bpx, возможно, установка nms-символов на ntoskrnl сможет помочь.

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

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

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

Известно, что kd требует установки двух машин, соединенных между собой. Марк Руссинович разработал утилиту LiveKd (доступна на ссылка скрыта и ссылка скрыта), которая позволяет запускать kd на одной и той же машине. Для отладки, разумеется, можно и нужно применять Soft-Ice, однако, когда возникает необходимость подглядеть какую-то структуру или адрес функции - лучше kd нет ничего (кроме, ессно, ручек и дизассемблера). kd доступен по адресу: ссылка скрыта

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

Очень важными структурами пользовательского режима являются TIB, TEB и PEB. Последний мы в этой статье затрагивать не будем, а касательно двух первых - Thread Environment Block и Thread Information Block - необходимо развеять кое-какую путаницу. Здесь мы хотели бы вам сказать, что читать НЕ стоит! Не стоит читать статью Питрека - ссылка скрыта и главы «Обработка исключений в реальном и защищенном режимах», «Как противостоять трассировке» и «Как противостоять контрольным точкам останова» и «Как обнаружить отладку средствами 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, т.е. является ее надмножеством. Где-то так (заметьте, полные описания структур мы не приводим, для этого есть исходники к книге Шрайбера на ссылка скрыта и кода 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;

}

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



рис 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 с ссылка скрыта или обзавестись Visual Studio .NET 2002+):

struct _PEB_LDR_DATA {
/*некорректно названа у Шрайбера*/
// non-static data --------------------------------
/**/ /*|0x4|*/ unsigned long Length;
/**/ /*|0x1|*/ unsigned char Initialized;
/**/ /*|0x4|*/ void* SsHandle;
/*смысл полей ниже одинаков – они все показывают на одну и ту же структуру,

просто упорядочены по-разному*/
/**/ /*|0x8|*/

struct _LIST_ENTRY InLoadOrderModuleList;
/**/ /*|0x8|*/

struct _LIST_ENTRY InMemoryOrderModuleList;
/**/ /*|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 {
/*на ссылка скрыта

структура ошибочно названа
_LDR_MODULE*/
/**/ /*|0x8|*/ struct _LIST_ENTRY InLoadOrderLinks;
/**/ /*|0x8|*/ struct _LIST_ENTRY InMemoryOrderLinks;
/**/ /*|0x8|*/ struct _LIST_ENTRY InInitializationOrderLinks;
/**/ /*|0x4|*/ void* DllBase;
/**/ /*|0x4|*/ void* EntryPoint;

//давайте поиграемся с этим полем
/**/ /*|0x4|*/ unsigned long SizeOfImage;
/**/ /*|0x8|*/ struct _UNICODE_STRING FullDllName;
/**/ /*|0x8|*/ struct _UNICODE_STRING BaseDllName;
/**/ /*|0x4|*/ unsigned long Flags;
/**/ /*|0x2|*/ unsigned short LoadCount;
/**/ /*|0x2|*/ unsigned short TlsIndex;
/**/ /*|0x8|*/ struct _LIST_ENTRY HashLinks;
/**/ /*|0x4|*/ void* SectionPointer;
/**/ /*|0x4|*/ unsigned long CheckSum;
/**/ /*|0x4|*/ unsigned long TimeDateStamp;
/**/ /*|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. За теорией - к Руссиновичу. Не имеете возможность купить эту книгу - вот линк на статью по теме ссылка скрыта. Обязательно учтите, что 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 в своем замечательном сборничке очень подробно расписал что к чему - сборничек можно слить с ссылка скрыта. Глава - "Интерфейс системных вызовов". Там предельно подробно рассказывается, как обработчик находит нужную функцию. Единственное что, имеет смысл привести описания структур 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;



рис 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) - ссылка скрыта.
Также очень неплохо было бы ознакомится с
ссылка скрыта - Undocumented Windows NT - очень сильная книжка, хотя немного и устарела. Возможно, имеет смысл скомпоновать ее главы в виде .chm-формата и поместить на ссылка скрыта.

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

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

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

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



рис 3

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

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

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

mov eax, ds:0FFDFF020h ;KPRCB по адресу FFDFF120
inc dword ptr [eax+4A8h]
;KPRCB.KeExceptionDispatchCount – да,

;увы и ах, документированная в ntddk.h структура KPRCB, конечно, что-то
;описывает, но опять-таки НЕ ПОЛНОСТЬЮ!
;лезьте в pdbdump и отчет о файле ntoskrnlsym.pdb – там много чего есть!

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

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

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

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

typedef struct _EPROCESS
{
/*000*/ KPROCESS Pcb;
...
/*214*/ struct _WIN32_PROCESS *Win32Process;
/*как видите, многие поля названы просто по порядку, не более*/
/*218*/ DWORD d218;
/*21C*/ DWORD d21C;
/*220*/ DWORD d220;
/*224*/ DWORD d224;
/*228*/ DWORD d228;
/*22C*/ DWORD d22C;
/*230*/ PVOID Wow64;
/*234*/ DWORD d234;
/*238*/ IO_COUNTERS

IoCounters;
/*268*/ DWORD d268;
/*26C*/ DWORD d26C;
/*270*/ DWORD d270;
/*274*/ DWORD d274;
/*278*/ DWORD d278;
/*27C*/ DWORD d27C;
/*280*/ DWORD d280;
/*284*/ DWORD d284;
/*288*/ }
EPROCESS,
* PEPROCESS,
**PPEPROCESS;

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

/**/ /*|0x4|*/ void* Win32Process;
/**/ /*|0x4|*/ struct _EJOB* Job;
/**/ /*|0x4|*/ unsigned long JobStatus;
/**/ /*|0x8|*/ struct _LIST_ENTRY JobLinks;
/**/ /*|0x4|*/ void* LockedPagesList;
/**/ /*|0x4|*/ void* SecurityPort;
/**/ /*|0x4|*/ struct _UNICODE_STRING* AuditImageName;
/**/ /*|0x4|*/ struct _WOW64_PROCESS* Wow64Process;
/**/ /*|0x8|*/ union _LARGE_INTEGER ReadOperationCount;
/**/ /*|0x8|*/ union _LARGE_INTEGER WriteOperationCount;
/**/ /*|0x8|*/ union _LARGE_INTEGER OtherOperationCount;
/**/ /*|0x8|*/ union

_LARGE_INTEGER ReadTransferCount;
/**/ /*|0x8|*/ union _LARGE_INTEGER WriteTransferCount;
/**/ /*|0x8|*/ union _LARGE_INTEGER OtherTransferCount;
/**/ /*|0x4|*/ unsigned long CommitChargeLimit;
/**/ /*|0x4|*/ unsigned long CommitChargePeak;
/**/ /*|0x8|*/ struct _LIST_ENTRY ThreadListHead;
/**/ /*|0x4|*/ struct _RTL_BITMAP* VadPhysicalPagesBitMap;
/**/ /*|0x4|*/ unsigned long VadPhysicalPages;
/**/ /*|0x4|*/ unsigned long AweLock;

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

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

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

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

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

Как 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. Материалы можно слить с ссылка скрыта. Кода 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 - ссылка скрыта - «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), то вот замечательный линк: ссылка скрыта

(продолжение следует...)


Крис Касперски

ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

(избранное)

(продолжение)