tag:blogger.com,1999:blog-23744658799493724152024-02-19T14:08:47.526+03:00Блог Rouse_то, о чём вы подозревали, но боялись задуматься
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.comBlogger37125tag:blogger.com,1999:blog-2374465879949372415.post-46609368129740762882023-03-04T20:07:00.007+03:002023-03-05T09:22:47.290+03:00<p> </p><h1 style="text-align: left;">Сканер установленных перехватчиков в памяти процесса</h1><p class="MsoNormal"> </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEieB9s8HFn4iY5eFD6ssk3xEoP5TY2Zuqaeb18jTB6MDmhoqXVp00pw5vujUsvZeYsIMF0uO0HTsw0E9sm384Rhf4N9lhDrbxlnOTAhAEWBYoEl8I2mkM45k7MYAj6qjCe45uIqvDyxXgQv3n9BabnVhFVGgNCKJrmBer54yEAuEuz2wsc7xLejjigF" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="675" data-original-width="1200" height="180" src="https://blogger.googleusercontent.com/img/a/AVvXsEieB9s8HFn4iY5eFD6ssk3xEoP5TY2Zuqaeb18jTB6MDmhoqXVp00pw5vujUsvZeYsIMF0uO0HTsw0E9sm384Rhf4N9lhDrbxlnOTAhAEWBYoEl8I2mkM45k7MYAj6qjCe45uIqvDyxXgQv3n9BabnVhFVGgNCKJrmBer54yEAuEuz2wsc7xLejjigF" width="320" /></a></div><br /><p></p>
<p class="MsoNormal">Думаю, практически у ста процентов читающих данную статью в
компании есть служба технической поддержки, и я думаю что не ошибусь если как
минимум половина из вас писала для своей службы техподдержки вспомогательные
утилиты, наподобие сборщиков системной информации, которые помогают делать
общие выводы о состоянии компьютера пользователя и окружении вашего софта,
запущенного на этом компьютере.<o:p></o:p></p>
<p class="MsoNormal">У наших технарей тоже есть такой инструмент и мне приходится
периодически его расширять под новые изменяющиеся требования, добавляя те или
иные ситуации, которые необходимо прогнать на машине пользователя чтобы
выяснить, корректно ли работает та или иная часть софта на данной конкретной
машине.<o:p></o:p></p>
<p class="MsoNormal">Где-то полгода назад у нас появилась очередная идея, есть
очень много разноплановых ошибок которые достаточно проблематично покрыть
тестами, но по результатам накопленного методом проб и ошибок опыта было
выяснено что большая часть из них происходит по причине вмешательства
стороннего софта в тело нашего процесса. Это могут быть как антивирусы, так и
всякие DLP, а то и вовсе зловреды, которые лезут к нам в процесс, перехватывают
некоторые критичные для выполнения API на себя и в обработчиках перехваченных
функций ломают полностью логику исполнения кода.<o:p></o:p></p>
<p class="MsoNormal">Поэтому было принято решение контролировать такое
вмешательство через одну из утилит, которой пользуется наша служба
техподдержки, и на основе её работы быстро выяснять - кто именно, куда конкретно
влез и главное, что именно он там поломал. <o:p></o:p></p>
<p class="MsoNormal">Собственно идея достаточно простая и она будет развитием моей предыдущей статьи "<a href="http://alexander-bagel.blogspot.com/2013/11/pmm2.html" target="_blank">Карта памяти процесса</a>". Суть её заключается в следующем: чтобы провернуть такой
трюк, нужно уметь самостоятельно рассчитывать все критические адреса в теле
удаленного процесса, знать, что должно находится по этим адресам и в
автоматическом режиме просто пробежаться по ним и проверить, есть ли изменения
или нет.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Правда при общей простоте идеи реализация получилось
достаточно трудоемкая. <br />Самая большая проблема при этом была в том что утилита 32
битная, а софт, работающий у пользователя может быть как 32 бита, так и 64
(второе более вероятно), поэтому для работы с 64 битным процессом пришлось писать соответствующую обвязку.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">И то я бы не сказал, что это решение в финале получилось
полным, т.к. мне стало в какой-то момент времени лень обрабатывать одну из
ситуаций, к решению которой я хотел подключить таблицы контекста активации
процесса, (правда в них, как оказалось, нет нужной мне информации) поэтому там я
выкрутился простым трюком, о котором расскажу чуть позже.<o:p></o:p></p>
<p class="MsoNormal">Короче в итоге получилось такое, как бы это назвать...
антивирусный сканер на минималках :)<o:p></o:p></p>
<p class="MsoNormal">В этой статье я пройдусь по всем этапам построения такого
сканера с нуля, постаравшись подробно описать каждый шаг чтобы было не только
понимание что именно тут происходит, а чтобы вы (при желании, конечно) могли бы
реализовать свой вариант такого сканера, даже не пользуясь моими наработками.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Для каждого из этапов будет свой код, который будет
расширятся от главы к главе, обрастая функционалом, а в самом конце я дам
ссылку на финальную реализацию данного фреймворка, который использую сам у себя
в инструментарии (если вдруг кто захочет просто воспользоваться готовым
решением подключив его к себе в проект).</p><span><a name='more'></a></span><p class="MsoNormal"><br /></p>
<h3 style="text-align: left;">Содержание</h3>
<p class="MsoNormal"><br /></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li><a href="#export">Таблица экспорта</a></li><li><a href="#loader">Работа со списками загрузчика</a></li><li><a href="#heaven">Вызов 64 битного кода из 32 битного контекста</a></li><li><a href="#forvard">Обработка Forward деклараций и анализ таблиц экспорта</a></li><li><a href="#import">Таблица импорта</a></li><li><a href="#apiset">ApiSet редиректы</a></li><li><a href="#delayed">Отложенный импорт</a></li><li><a href="#tls">TLS калбэки и детектирование модификации кода.</a></li><li><a href="#epilog">Сслыки</a></li></ol>
<p></p>
<a name="export"></a>
<p class="MsoNormal"><br /></p>
<p class="MsoNormal"></p><h3 style="text-align: left;">1. Таблица экспорта</h3><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<p class="MsoNormal">Для начала разберем принцип получения экспортируемых
исполняемым файлом (или библиотекой) адресов функций, читая эту информацию
напрямую из образа на жестком диске. Вся эта информация хранится в таблице
экспорта, поэтому с неё и начнем. <o:p></o:p></p>
<p class="MsoNormal">Данная таблица применяется в случае динамической линовки
функций посредством LoadLibrary + GetProcAddress, а также для заполнения таблиц
импорта и отложенного импорта загружаемых библиотек в случае статической
компоновки (на этом пока не акцентируйте внимание).<o:p></o:p></p>
<p class="MsoNormal">Я не буду сильно углубляться по формату РЕ файла, благо на
эту тему есть огромное количество статей (ссылки будут в конце статьи), поэтому
описывать буду только интересующие для текущей задачи моменты. А текущей
задачей будет получить от таблицы экспорта два адреса по каждой экспортируемой
функции.<o:p></o:p></p>
<p class="MsoNormal" style="margin-left: 18pt;"></p><ol style="text-align: left;"><li>Первый это RAW и VA адрес, по
которому размещен код тела функции.</li><li>А второй - RAW и VA адрес, в
котором лежит RVA указатель на первый адрес.</li></ol><p></p><p class="MsoNormal" style="margin-left: 18pt;"><o:p></o:p></p>
<p class="MsoNormal" style="margin-left: 18pt;"><o:p></o:p></p>
<p class="MsoNormal">Именно эти адреса потребуются при анализе памяти так как, когда
осуществляется перехват функции, меняются данные либо по первому адресу
(методом HotPatch или через трамплин, копированием части кода в другую область
и размещением вместо него перехода на перехватчик), либо по второму, заставляя
функцию GetProcAddress возвращать вместо адреса функции адрес перехватчика.<o:p></o:p></p>
<p class="MsoNormal">Да, сразу же оговорюсь, в данной статье будут применяться
три термина относительно адресации:<o:p></o:p></p>
<p class="MsoNormal" style="margin-left: 18pt;"></p><ol style="text-align: left;"><li>RAW адрес - смещение от начала
файла на диске (всегда 4 байта).</li><li>RVA адрес (relative virtual
address), это относительный адрес в том виде, в котором он записан в
исполняемом файле (всегда 4 байта). Как правило он указывается относительно <span lang="EN-US" style="mso-ansi-language: EN-US;">ImageBase</span><span lang="EN-US"> </span>записанном
в РЕ заголовке файла, либо относительно <span lang="EN-US" style="mso-ansi-language: EN-US;">hInstance</span><span lang="EN-US"> </span>файла в памяти приложения, но
бывают и другие варианты, я буду их указывать, когда потребуется.</li><li>VA адрес - реальный адрес в
адресном пространстве процесса. Т.к. нам нужно работать и с 32 битами и с 64,
то он хранится в виде 8-байтного ULONG_PTR64 (UInt64), но содержит в
зависимости от битности процесса либо 32 битный указатель, либо 64 битный. VA
адрес (как правило) рассчитывается исходя из базы загрузки файла
IMAGE_OPTIONAL_HEADERхх.ImageBase и прибавления к ней RVA адреса, но могут быть
и исключения (например структуры ApiSet таблиц, которые рассмотрим немного
позже, содержат RVA адреса относительно своего заголовка, а не ImageBase. </li></ol><p></p><p class="MsoNormal" style="margin-left: 18pt;"><o:p></o:p></p>
<p class="MsoNormal" style="margin-left: 18pt;"><o:p></o:p></p>
<p class="MsoNormal" style="margin-left: 18pt;"><o:p></o:p></p>
<p class="MsoNormal">Выход на таблицу импорта происходит посредством чтения трех
структур, которые всегда присутствуют в начале любого РЕ файла.<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>Чтением структуры IMAGE_DOS_HEADER, с которой начинается
исполняемый файл</li><li>Переходом на смещение IMAGE_DOS_HEADER._lfanew,
указывающий на начало PE заголовка</li><li>Проверкой наличия четырехбайтной сигнатуры
IMAGE_NT_SIGNATURE и чтением идущей за ней структуры IMAGE_FILE_HEADER</li><li>Из этой структуры будут интересны два поля.
NumberOfSections и Machine. В зависимости от значения второго нужно прочитать
идущую следом</li></ol><ul style="text-align: left;"><li>либо<span style="mso-ansi-language: EN-US;"> </span>структуру<span lang="EN-US" style="mso-ansi-language: EN-US;"> IMAGE_OPTIONAL_HEADER32 </span>в<span style="mso-ansi-language: EN-US;"> </span>случае<span style="mso-ansi-language: EN-US;"> </span>если<span lang="EN-US" style="mso-ansi-language: EN-US;">
IMAGE_FILE_HEADER.Machine = IMAGE_FILE_MACHINE_I386</span></li><li>либо<span style="mso-ansi-language: EN-US;"> </span>структуру<span lang="EN-US" style="mso-ansi-language: EN-US;"> IMAGE_OPTIONAL_HEADER64 </span>в<span style="mso-ansi-language: EN-US;"> </span>случае<span style="mso-ansi-language: EN-US;"> </span>если<span lang="EN-US" style="mso-ansi-language: EN-US;">
IMAGE_FILE_HEADER.Machine = IMAGE_FILE_MACHINE_AMD64</span></li></ul><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Вот примерно так, как на картинке<o:p></o:p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOxBta9RKwGsQRhKX9rS6pnrpd1ARnwxPUbVZL1STurc3Q7wsx57YHMklqQLpt075I3du9D4oY1H3X817OhFTFn9dkol8F3TqOGu2oDgx984EdBMfvSDHSWeBsJXoT3s2WvWOpIJF4cxaXLOHQjoh-mm-nOHudWJFR7XKSp9WUxmTni8qApIdmAHpm/s1019/1.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="800" data-original-width="1019" height="251" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOxBta9RKwGsQRhKX9rS6pnrpd1ARnwxPUbVZL1STurc3Q7wsx57YHMklqQLpt075I3du9D4oY1H3X817OhFTFn9dkol8F3TqOGu2oDgx984EdBMfvSDHSWeBsJXoT3s2WvWOpIJF4cxaXLOHQjoh-mm-nOHudWJFR7XKSp9WUxmTni8qApIdmAHpm/s320/1.png" width="320" /></a></div><br /><p class="MsoNormal"><br /></p>
<p class="MsoNormal">Различия структур IMAGE_OPTIONAL_HEADER друг от друга
заключается в размере полей. <o:p></o:p></p>
<p class="MsoNormal">32 битная содержит 4 байтные DWORD, а 64 битная
восьмибайтные ULONGLONG.<o:p></o:p></p>
<p class="MsoNormal">Последним полем IMAGE_OPTIONAL_HEADERхх содержит массив
IMAGE_DATA_DIRECTORY (всего 16 элементов). Вот именно он и будет интересен, а
если конкретнее, то самый первый его элемент, обозначающийся индексом IMAGE_DIRECTORY_ENTRY_EXPORT.<o:p></o:p></p>
<p class="MsoNormal">Но, прежде чем начать работу с этим массивом, необходимо
прочитать и запомнить данные о секциях исполняемого файла. Их количество записано
в поле IMAGE_FILE_HEADER.NumberOfSections, и представляют они из себя массив
структур IMAGE_SECTION_HEADER которые идут сразу после IMAGE_OPTIONAL_HEADERхх.<o:p></o:p></p>
<p class="MsoNormal">Структура<span lang="EN-US" style="mso-ansi-language: EN-US;">
IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT], </span>это<span style="mso-ansi-language: EN-US;"> </span>всего<span style="mso-ansi-language: EN-US;"> </span>два<span style="mso-ansi-language: EN-US;"> </span>поля<span lang="EN-US" style="mso-ansi-language: EN-US;">: <o:p></o:p></span></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>VirtualAddress (DWORD) - RVA адрес директории</li><li>Size (DWORD) - её размер</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">А структура, описывающая секцию (IMAGE_SECTION_HEADER)
предоставляет следующий набор полей (перечислены только требуемые для текущей
задачи):<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>VirtualAddress (DWORD) - RVA адрес секции</li><li>VirtualSize (DWORD) - её размер в адресном пространстве
процесса</li><li>PointerToRawData (DWORD) - RAW адрес секции</li><li>SizeOfRawData (DWORD) - размер RAW данных в файле</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Так вот, если создать пустое консольное приложение и выполнить
над ним описанные выше шаги, то на руках будет структура с (примерно) такими
параметрами:</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><span lang="EN-US" style="mso-ansi-language: EN-US;">IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
= 0x103000<br /></span>IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT].Size
= 0x96</p>
<p class="MsoNormal">В данном случае я говорю про Delphi 10.4, которая создает
исполняемые файлы с тремя экспортируемыми функциями:<o:p></o:p></p>
<p class="MsoNormal"><span lang="EN-US" style="mso-ansi-language: EN-US;">dbkFCallWrapperAddr,
_dbk_fcall_wrapper </span>и<span lang="EN-US" style="mso-ansi-language: EN-US;">
TMethodImplementationIntercept <o:p></o:p></span></p>
<p class="MsoNormal">Чтобы прочитать содержимое таблицы экспорта из файла на
диске, полученный RVA адрес 0x103000 не подходит и нужно его пересчитать в RAW
и вот именно тут пригодится информация из массива IMAGE_SECTION_HEADER, который
содержит данные для преобразования из одного типа указателя в другой.<o:p></o:p></p>
<p class="MsoNormal">Для этого потребуется новый класс <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%201/RawScanner.ModulesData.pas#L39">TRawPEImage</a>.
Этапы чтения структур заголовка (LoadFromImage + LoadNtHeader) и секций
(LoadSections) я пропущу, отмечу только один нюанс.<o:p></o:p></p>
<p class="MsoNormal">Так как подразумевается работа с 64 битным кодом из 32
битного приложения, то поле FNtHeader в данном классе имеет тип TImageNtHeaders64
и при чтении 32 битного заголовка происходит этап конвертации 32 битного
опционального заголовка в 64. Это сделано только для удобства работы.<o:p></o:p></p>
<p class="MsoNormal">Начну сразу с реализации утилитарных функций. Первые две
функции выглядит так:</p>
<pre class="brush:delphi">function TRawPEImage.RvaToVa(RvaAddr: DWORD): ULONG_PTR64;
begin
Result := FImageBase + RvaAddr;
end;
function TRawPEImage.VaToRva(VaAddr: ULONG_PTR64): DWORD;
begin
Result := VaAddr - FImageBase;
end;
</pre><p class="MsoNormal">Собственно это и есть все преобразование из RVA адресации в
VA и наоборот.<o:p></o:p></p>
<p class="MsoNormal">Для преобразования из RVA адреса в RAW потребуется три
вспомогательных функции:<o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.AlignDown(Value, Align: DWORD): DWORD;
begin
Result := Value and not DWORD(Align - 1);
end;
function TRawPEImage.AlignUp(Value, Align: DWORD): DWORD;
begin
if Value = 0 then Exit(0);
Result := AlignDown(Value - 1, Align) + Align;
end;</pre>
<p class="MsoNormal">Эти две функции отвечают за выравнивание, а третья отвечает
за поиск секции, к которой принадлежит RVA адрес:</p>
<pre class="brush:delphi">function TRawPEImage.GetSectionData(RvaAddr: DWORD;
var Data: TSectionData): Boolean;
var
I, NumberOfSections: Integer;
SizeOfRawData, VirtualSize: DWORD;
begin
Result := False;
NumberOfSections := Length(FSections);
for I := 0 to NumberOfSections - 1 do
begin
if FSections[I].SizeOfRawData = 0 then
Continue;
if FSections[I].PointerToRawData = 0 then
Continue;
Data.StartRVA := FSections[I].VirtualAddress;
if FNtHeader.OptionalHeader.SectionAlignment >= DEFAULT_SECTION_ALIGNMENT then
Data.StartRVA := AlignDown(Data.StartRVA, FNtHeader.OptionalHeader.SectionAlignment);
SizeOfRawData := FSections[I].SizeOfRawData;
VirtualSize := FSections[I].Misc.VirtualSize;
// если виртуальный размер секции не указан, то берем его из размера данных
// (см. LdrpSnapIAT или RelocateLoaderSections)
// к которому уже применяется SectionAlignment
if VirtualSize = 0 then
VirtualSize := SizeOfRawData;
if FNtHeader.OptionalHeader.SectionAlignment >= DEFAULT_SECTION_ALIGNMENT then
begin
SizeOfRawData := AlignUp(SizeOfRawData, FNtHeader.OptionalHeader.FileAlignment);
VirtualSize := AlignUp(VirtualSize, FNtHeader.OptionalHeader.SectionAlignment);
end;
Data.Size := Min(SizeOfRawData, VirtualSize);
if (RvaAddr >= Data.StartRVA) and (RvaAddr < Data.StartRVA + Data.Size) then
begin
Data.Index := I;
Result := True;
Break;
end;
end;
end;
</pre>
<p class="MsoNormal">В ней происходит перебор секций, и первым этапом идет
пропуск секций с нулевым размером или отсутствующими данными.<o:p></o:p></p><p class="MsoNormal">Вторым этапом вычисляется нижняя граница секции (её начало)
на основе VirtualAddress с округлением вниз на значение SectionAlignment.<o:p></o:p></p><p class="MsoNormal">Третьим - верхняя граница (её конец) на основе сначала
Misc.VirtualSize с округлением вверх на значение SectionAlignment, а потом
SizeOfRawData с округлением вверх на значение FileAlignment.<o:p></o:p></p><p class="MsoNormal">Важный нюанс, Misc.VirtualSize может быть равен нулю и это
штатное значение, в таком случае в качестве конечного адреса секции берется
значение из SizeOfRawData к которому также применяется округление вверх, но уже
на значение SectionAlignment.<o:p></o:p></p><p class="MsoNormal">Реальный размер секции в адресном пространстве равен
меньшему из двух рассчитанных значений.<o:p></o:p></p><p class="MsoNormal">Последний этап, это проверка - попадает ли переданный RVA
адрес в диапазон адресов секций.<o:p></o:p></p><p class="MsoNormal">
</p><p class="MsoNormal">После того как номер секции, к которой принадлежит RVA адрес
определен, необходимо произвести его конвертацию посредством следующей функции:<o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.RvaToRaw(RvaAddr: DWORD): DWORD;
var
NumberOfSections: Integer;
SectionData: TSectionData;
SizeOfImage: DWORD;
PointerToRawData: DWORD;
begin
Result := 0;
// ... граничные проверки вырезаны
if GetSectionData(RvaAddr, SectionData) then
begin
PointerToRawData := FSections[SectionData.Index].PointerToRawData;
if FNtHeader.OptionalHeader.SectionAlignment >= DEFAULT_SECTION_ALIGNMENT then
PointerToRawData := AlignDown(PointerToRawData, DEFAULT_FILE_ALIGNMENT);
Inc(PointerToRawData, RvaAddr - SectionData.StartRVA);
if PointerToRawData < FSizeOfFileImage then
Result := PointerToRawData;
end;
end;
</pre>
<p class="MsoNormal">В ней сначала происходит проверка редких ситуаций, когда RVA
адрес принадлежит заголовкам файла и второй случай, когда в файле вообще нет
секций (случаи граничные, поэтому рассматривать не буду).<o:p></o:p></p><p class="MsoNormal">После чего происходит сама конвертация, из конвертируемого
RVA адреса вычитается RVA адрес начала секции и к результату прибавляется
PointerToRawData, указывающий на смещение секции относительно начала файла.
Результатом будет RAW адрес, опираясь на который можно прочитать данные из
образа файла на диске.<o:p></o:p></p><p class="MsoNormal">Осталось написать еще одну утилитарную функцию, она
пригодится для работы с адресами директорий, которые TRawPEImage хранит уже
преобразованными в VA.</p>
<pre class="brush:delphi">function TRawPEImage.VaToRaw(VaAddr: ULONG_PTR64): DWORD;
begin
Result := RvaToRaw(VaToRva(VaAddr));
end;
</pre>
<p class="MsoNormal">Если проверить этот код на 'ntdll.dll' то в случае Win11 из
32 битного процесса (при этом будет подгружена библиотека из SysWOW64) данные
будут такие:<o:p></o:p></p><p class="MsoNormal">RVA адрес директории экспорта = 0х110360 (<span lang="EN-US">VA</span> при этом равен 0<span lang="EN-US">x</span>77<span lang="EN-US">A</span>60360)<br />Принадлежит директории '.text', которая начинается с RVA
0x1000, размером 0х122800 байт, PointerToRawData = 0х400.<br />Значит в реальном файле RAW адрес директории экспорта должен
быть равен: 0х110360 - 0x1000 + 0х400 = 0х10F760</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">
<span face=""Calibri",sans-serif" style="font-size: 11pt; line-height: 107%; mso-ansi-language: RU; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-language: AR-SA; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-language: EN-US; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;">Можно для самопроверки открыть эту библиотеку в HEX
редакторе и сравнить с тем, что находится по VA адресу таблицы экспорта в
процессе.</span></p><p class="MsoNormal"></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4OJt__oCOxwLEL-gBf2XUHl1uEKDv7RQa9Tocl3ZlEjOICXLESTIRlJztxknQaKrOj_GzXd3RYKmstIALwtZh_AKqAfnjlk4iZHGlBMg7hRp88nfhZbLKANDYrnMiexLhp5FCT_CBNoYK2xzclTNCfaprjYyTYs5VKoRIPfmP297b7JNUVwMmKPsr/s1515/2.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="581" data-original-width="1515" height="123" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4OJt__oCOxwLEL-gBf2XUHl1uEKDv7RQa9Tocl3ZlEjOICXLESTIRlJztxknQaKrOj_GzXd3RYKmstIALwtZh_AKqAfnjlk4iZHGlBMg7hRp88nfhZbLKANDYrnMiexLhp5FCT_CBNoYK2xzclTNCfaprjYyTYs5VKoRIPfmP297b7JNUVwMmKPsr/s320/2.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><p></p><p class="MsoNormal">Скриншот показал, что данные совпали, значит RAW адрес получен
правильно и пришло время читать саму таблицу экспорта напрямую из файла.<o:p></o:p></p><div class="separator" style="clear: both;">
<p class="MsoNormal">В качестве реципиента я взял маленькую библиотеку для работы
с ACE архивами, так как в ней более наглядно можно увидеть для чего
предназначен каждый список таблицы экспорта. Итак, таблица экспорта функций
начинается со структуры IMAGE_EXPORT_DIRECTORY, которую нужно прочитать самым
первым шагом.<o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.LoadExport(Raw: TStream): Boolean;
var
I, Index: Integer;
LastOffset: Int64;
ImageExportDirectory: TImageExportDirectory;
FunctionsAddr, NamesAddr: array of DWORD;
Ordinals: array of Word;
ExportChunk: TExportChunk;
begin
Result := False;
LastOffset := VaToRaw(ExportDirectory.VirtualAddress);
if LastOffset = 0 then Exit;
Raw.Position := LastOffset;
Raw.ReadBuffer(ImageExportDirectory, SizeOf(TImageExportDirectory));
if ImageExportDirectory.NumberOfFunctions = 0 then Exit;
// читаем префикс для перенаправления через ApiSet,
// он не обязательно будет равен имени библиотеки
// например:
// kernel.appcore.dll -> appcore.dll
// gds32.dll -> fbclient.dll
Raw.Position := RvaToRaw(ImageExportDirectory.Name);
if Raw.Position = 0 then
Exit;
FOriginalName := ReadString(Raw);
// читаем масив Rva адресов функций
SetLength(FunctionsAddr, ImageExportDirectory.NumberOfFunctions);
Raw.Position := RvaToRaw(ImageExportDirectory.AddressOfFunctions);
if Raw.Position = 0 then
Exit;
Raw.ReadBuffer(FunctionsAddr[0], ImageExportDirectory.NumberOfFunctions shl 2);
</pre>
<p class="MsoNormal">Вначале идет проверка, получилось ли преобразовать VA адрес
директории в RAW (т.е. результат работы VaToRaw). Таких проверок будет по коду
много, и чуть позже я покажу в каком случае эти проверки могут сработать.<o:p></o:p></p><p class="MsoNormal">Особо отмечу - все адреса из структуры
IMAGE_EXPORT_DIRECTORY представлены в виде RVA, т.е. для работы с ними всегда
требуется преобразование, либо в RAW если читаем из файла, либо в VA если
читаем из памяти!<o:p></o:p></p><p class="MsoNormal">Следующим шагом идет проверка количества экспортируемых
функций, так как наличие директории экспорта еще не означает что в ней есть
данные.<br />Далее читается имя библиотеки. Сейчас оно не интересно, но будет
использоваться в следующих главах.<br />Ну и последним подготовительным шагом читается массив
адресов экспортируемых функций. Это список DWORD, содержащий RVA адреса
экспортируемых функций.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">
</p><p class="MsoNormal">Вот так выглядит таблица экспорта для библиотеки UNACEV2.DLL<o:p></o:p></p></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWOvsxdpWaFSdKTkadoCfn4QLL4agTbbwxpx9y1mBt173BVd4kGU7iRCGZmZY8B5IBa5OD2S0uJ7-aP-yfz3X2xPMP7SG2DDuwEUl0hmzgBUVCLOFQNoaOSjp0hZc3Pn5yuwD7-aY000YfPfibVYkXzDN5HSlCwOuIfmUtqzbwHbaZOAL1aBcCoaST/s992/3.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="590" data-original-width="992" height="190" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWOvsxdpWaFSdKTkadoCfn4QLL4agTbbwxpx9y1mBt173BVd4kGU7iRCGZmZY8B5IBa5OD2S0uJ7-aP-yfz3X2xPMP7SG2DDuwEUl0hmzgBUVCLOFQNoaOSjp0hZc3Pn5yuwD7-aY000YfPfibVYkXzDN5HSlCwOuIfmUtqzbwHbaZOAL1aBcCoaST/s320/3.png" width="320" /></a></div><p class="MsoNormal">Вначале идет структура IMAGE_EXPORT_DIRECTORY и стрелками я
показал на какие данные ссылаются её поля. Самое важное поля в ней это
NumberOfFunctions + AddressOfFunctions.<o:p></o:p></p>
<p class="MsoNormal">Остальные поля со стрелками NumberOfNames + AddressOfNames +
AddressOfNameOrdinals могут быть равны нулю, но NumberOfFunctions +
AddressOfFunctions обязательно должны присутствовать, т.к. именно они дают
возможность получить доступ к экспортируемым функциям хотя бы по их ORDINAL
значению (т.е. по индексу экспортируемой функции, а не по имени). </p>
<p class="MsoNormal">Итак, синяя стрелка показывает на список экспортируемых
функций. Первая запись:<o:p></o:p></p>
<p class="MsoNormal">03517034: 08 <span lang="EN-US">DB</span>
00 00 <span lang="EN-US">EAT</span><span lang="EN-US"> </span><span lang="EN-US">FuncAddr</span> [1] <span lang="EN-US">UNACEV</span>2.<span lang="EN-US">DLL</span>!<span lang="EN-US">ACEInitDll</span> = [34<span lang="EN-US">DDB</span>08]<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>03517034 - это адрес в котором хранится RVA значение
экспортируемой функции ACEInitDll</li><li>08 DB 00 00 - это 4 байта которые и являются RVA адресом
функции. Если преобразовать их в DWORD это будет означать 0x0000DB08 (числа
хранятся в памяти в little-endian формате, т.е. обратно их привычному представлению).</li><li>EAT FuncAddr [1] - это комментарий к экспортируемой
функции поясняющий тип адреса и в скобках содержащий ORDINAL экспортируемой
функции. Замечу – <span lang="EN-US">Ordinal</span><span lang="EN-US"> </span>это не порядковый номер, это именно индекс функции, который
получается сложением IMAGE_EXPORT_DIRECTORY.<span lang="EN-US">Base</span><span lang="EN-US"> </span>и порядкового номера функции в
таблице адресов. При загрузке функции по ординалу применяется вызов <span lang="EN-US">GetProcAddress</span>(<span lang="EN-US">dll</span>, <span lang="EN-US">MAKEINTRESOURCE</span>(<span lang="EN-US">ordinal</span>)).
В текущей библиотеке он равен единице, а вот в ntdll например он равен восьми.</li><li>UNACEV2.DLL!ACEInitDll = [34DDB08] - сама экспортируемая
функция, а в скобках содержится её VA адрес, который вернет GetProcAddress (VA
= RVA + ImageBase)</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Из четвертого пункта можно понять, что так как RVA равен
0xDB08, а VA адрес равен 0х34DDB08 то Instance библиотеки (адрес, по которому
она загружена) равен 0х034D0000<o:p></o:p></p>
<p class="MsoNormal">Если посмотреть на первый список, то можно заметить, что все
ординалы функций (в первых квадратных скобках) идут по порядку от единицы до
шести. Это условие всегда будет соблюдаться, дело в том, что это (как правило) тот
порядок, в котором они объявлены в коде библиотеки. Вот как они идут в коде,
так обычно и прописываются в списке экспорта (это можно наглядно увидеть по их
RVA или VA значениям, которые идут на увеличение, правда это не всегда так).<o:p></o:p></p>
<p class="MsoNormal">Более интересны два другие списка, в частности список, на
который указывает параметр AddressOfNames. Это список имен функций в количестве
IMAGE_EXPORT_DIRECTORY.NumberOfNames объявленных строго в отсортированном
порядке (обязательное условие), именно по этим именам идет поиск функции в
случае вызова GetProcAddress, а сортировка нужна для ускорения поиска. При этом
- имя функции это такой-же RVA указатель на буфер с именем (строго на Ansi буфер).<o:p></o:p></p>
<p class="MsoNormal">Строго говоря отсортированный список имен функций
практически всегда не будет соответствовать их декларации из первого
(AddressOfFunctions) списка, поэтому для соответствия какое имя какому адресу
соответствует, существует третий список - AddressOfNameOrdinals. Это список
двухбайтовых WORD, в количестве IMAGE_EXPORT_DIRECTORY.NumberOfNames каждый из
которых соответствует (по индексу) такому же имени функции и содержащий индекс
от нуля в самом первом списке AddressOfFunctions.<o:p></o:p></p>
<p class="MsoNormal">Можно проверить это утверждение опираясь на картинку выше:</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"></p><blockquote><p class="MsoNormal">1. имя самой первой функции из списка AddressOfNames:<br />0351704C:
70 70 04 00 EAT Name [5]
UNACEV2.DLL!ACEExtract = [3517070]</p>
<p class="MsoNormal">2. соответствующий ей ординал индекс из списка
AddressOfNameOrdinals<br />03517064:
04 00 EAT Ordinal [5]
UNACEV2.DLL!ACEExtract = 4</p>
<p class="MsoNormal">3. индекс равен четырем. Это реальный индекс от нуля в самом
первом списке RVA адресов функций. Четвертая запись из списка
AddressOfFunctions:<br />03517044: 67 DD 00 00
FuncAddr [5] UNACEV2.DLL!ACEExtract = [34DDD67]</p></blockquote><p class="MsoNormal"></p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Ну а ORDINAL индекс этой функции равен индексу в списке +
база, т.е. пяти. </p><p class="MsoNormal">Хитрый момент - индекс всего два байта!</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Именно из-за этого ограничения ни у кого не получится сделать библиотеку, экспортирующую больше 65535 функций (я знаю людей, которые пробовали).</p>
<p class="MsoNormal">Вот примерно такой-же алгоритм и необходимо написать:<o:p></o:p></p>
<pre class="brush:delphi">// Важный момент!
// Библиотека может вообще не иметь функций экспортируемых по имени,
// только по ординалам. Пример такой библиотеки: mfperfhelper.dll
// Поэтому нужно делать проверку на их наличие
if ImageExportDirectory.NumberOfNames > 0 then
begin
// читаем массив Rva адресов имен функций
SetLength(NamesAddr, ImageExportDirectory.NumberOfNames);
Raw.Position := RvaToRaw(ImageExportDirectory.AddressOfNames);
if Raw.Position = 0 then
Exit;
Raw.ReadBuffer(NamesAddr[0], ImageExportDirectory.NumberOfNames shl 2);
// читаем массив ординалов - индексов через которые имена функций
// связываются с массивом адресов
SetLength(Ordinals, ImageExportDirectory.NumberOfNames);
Raw.Position := RvaToRaw(ImageExportDirectory.AddressOfNameOrdinals);
if Raw.Position = 0 then
Exit;
Raw.ReadBuffer(Ordinals[0], ImageExportDirectory.NumberOfNames shl 1);
// сначала обрабатываем функции экспортируемые по имени
for I := 0 to ImageExportDirectory.NumberOfNames - 1 do
begin
Raw.Position := RvaToRaw(NamesAddr[I]);
if Raw.Position = 0 then Continue;
// два параметра по которым будем искать фактические данные функции
ExportChunk.FuncName := ReadString(Raw);
ExportChunk.Ordinal := Ordinals[I];
// VA адрес в котором должен лежать Rva линк на адрес функции
// именно его изменяют при перехвате функции методом патча
// таблицы экспорта.
ExportChunk.ExportTableVA := RvaToVa(
ImageExportDirectory.AddressOfFunctions + ExportChunk.Ordinal shl 2);
// Смещение в RAW файле по которому лежит Rva линк
ExportChunk.ExportTableRaw := VaToRaw(ExportChunk.ExportTableVA);
// Само RVA значение которое будут подменять
ExportChunk.FuncAddrRVA := FunctionsAddr[ExportChunk.Ordinal];
// VA адрес функции, именно по этому адресу (как правило) устанавливают
// перехватчик методом сплайсинга или хотпатча через трамплин
ExportChunk.FuncAddrVA := RvaToVa(ExportChunk.FuncAddrRVA);
// Raw адрес функции в образе бинарника с которым будет идти проверка
// на измененные инструкции
ExportChunk.FuncAddrRaw := RvaToRaw(ExportChunk.FuncAddrRVA);
// вставляем признак что функция обработана
FunctionsAddr[ExportChunk.Ordinal] := 0;
// переводим в NameOrdinal который прописан в таблице импорта
Inc(ExportChunk.Ordinal, ImageExportDirectory.Base);
// добавляем в общий список для анализа снаружи
Index := FExport.Add(ExportChunk);
// vcl270.bpl спокойно декларирует 4 одинаковых функции
// вот эти '@$xp$39System@%TArray__1$p17System@TMetaClass%'
// с ординалами 7341, 7384, 7411, 7222
// поэтому придется в массиве имен запоминать только самую первую
// ибо линковаться они могут только через ординалы
// upd: а они даже не линкуются, а являются дженериками с линком на класс
// а в таблице экспорта полученном через Symbols присутствует только одна
// с ординалом 7384
FExportIndex.TryAdd(ExportChunk.FuncName, Index);
// индекс для поиска по ординалу
// (если тут упадет с дубликатом, значит что-то не верно зачитано)
FExportOrdinalIndex.Add(ExportChunk.Ordinal, Index);
end;
end;
</pre>
<p class="MsoNormal">Что здесь происходит.<o:p></o:p></p><p class="MsoNormal">Самым первым шагом идет проверка, а есть ли вообще список
имен? Если есть, то происходят все те же самые действия, про которые я
рассказал выше и заполняется структура, в которой будет хранится информация по
каждой экспортируемой функции. Её пока не рассматриваю, она пригодится гораздо
позже, для кода анализатора.<o:p></o:p></p><p class="MsoNormal">Единственно что упомяну, это то, что именно на этом этапе
рассчитывается реальный VA адрес каждой функции на основе Instance библиотеки
переданного в конструкторе класса, и хранится он в ExportChunk.FuncAddrVA, а
также адрес поля в таблице экспорта, в котором этот адрес записан в памяти
процесса, это ExportChunk.ExportTableVA.<o:p></o:p></p><p class="MsoNormal">Оба этих адреса будет контролировать анализатор на следующих
этапах, т.к. изменением значения, на которое указывает ExportTableVA
осуществляется установка перехватчика через правку таблицы экспорта, а правкой
данных, которые указывает FuncAddrVA осуществляется установка перехватчика
прямой правкой кода функции (не важно каким именно способом, через HotPatch или
трамплин или вообще модификация поведения функции посредством изменения её кода
целиком).<o:p></o:p></p><p class="MsoNormal">Для ускорения работы с классом помимо списка экспорта
FExport используются два словаря.<o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>Словарь имен функций FExportIndex</li><li>Словарь ординалов функций FExportOrdinalIndex</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Это уже чисто организационные моменты, тут я их показал для
демонстрации одного момента.<o:p></o:p></p><p class="MsoNormal">Дело в том, что по правилам дубликатов в списке имен
экспорта быть не должно, но как видно по комментарию к коду, <span lang="EN-US">Delphi</span> вполне допускает такие дубликаты,
правда момент заключается в том, что эти имена не принадлежат функциям как
таковым, а указывают на некие публичные структуры. Такое встречается и в
штатных библиотеках, например экспортируемая из ntdll RtlNtdllName фактически
функцией не является, т.к. является просто указателем на строку.<o:p></o:p></p><p class="MsoNormal">Впрочем, такие ситуации будут рассмотрены немного позже,
когда буду расширять код класса, а сейчас остался последний шаг.<o:p></o:p></p><p class="MsoNormal">Данные по функциям экспортирующихся по имени загружены, но
есть очень много библиотек экспортирующих часть функций только по ординалам
(без имени), поэтому третьим шагом нужно обработать оставшиеся функции,
опираясь на значение FunctionsAddr[Index], которое обнуляется для обработанных
ранее функций (или изначально было равно нулю из-за пропуска в списке
ординалов).<o:p></o:p></p><p class="MsoNormal">Кстати, по поводу списка адресов. Данный список может идти с
разрывами, т.е. если простым языком, некоторые функции экспортируемые по
ординалу могут отсутствовать. Тогда вместо RVA такой функции будет записан
ноль, вот как в случае экспорта библиотеки cabinet.dll</p><p class="MsoNormal"><o:p></o:p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzEPQuh4AYQuLG2AWfoey6mE_G8RP19lu5ZzK2gg89i_O8Nz8kuvPvPCPLjq8peA-9KSq4VkCpE1e4zHlVpcL95sY35c-x4aphiSrGruaqniAT2v-jXs0oNOWJdnOS0aXxNujoHpWfCQ5fXi6W4e4-hHeclUk2JnEGtfZnmkDg4CH11k5eLczYXNUj/s989/4.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="615" data-original-width="989" height="199" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzEPQuh4AYQuLG2AWfoey6mE_G8RP19lu5ZzK2gg89i_O8Nz8kuvPvPCPLjq8peA-9KSq4VkCpE1e4zHlVpcL95sY35c-x4aphiSrGruaqniAT2v-jXs0oNOWJdnOS0aXxNujoHpWfCQ5fXi6W4e4-hHeclUk2JnEGtfZnmkDg4CH11k5eLczYXNUj/s320/4.png" width="320" /></a></div><br /><p class="MsoNormal">Именно на такие пропуски и идет закладка, когда я упомянул
что нужно проверять значение FunctionsAddr[Index]. Итак - последний шаг:<o:p></o:p></p>
<pre class="brush:delphi"> // обработка функций экспортирующихся по индексу
for I := 0 to ImageExportDirectory.NumberOfFunctions - 1 do
if FunctionsAddr[I] <> 0 then
begin
// здесь все тоже самое за исключение что у функции нет имени
// и её подгрузка осуществляется по её ординалу, который рассчитывается
// от базы директории экспорта
ExportChunk.FuncAddrRVA := FunctionsAddr[I];
ExportChunk.Ordinal := ImageExportDirectory.Base + DWORD(I);
ExportChunk.FuncName := EmptyStr;
// сами значения рассчитываются как есть, без пересчета в ординал
ExportChunk.ExportTableVA := RvaToVa(
ImageExportDirectory.AddressOfFunctions + DWORD(I shl 2));
ExportChunk.FuncAddrVA := RvaToVa(ExportChunk.FuncAddrRVA);
ExportChunk.FuncAddrRaw := RvaToRaw(ExportChunk.FuncAddrRVA);
// добавляем в общий список для анализа снаружи
Index := FExport.Add(ExportChunk);
// имени нет, поэтому добавляем только в индекс ординалов
FExportOrdinalIndex.Add(ExportChunk.Ordinal, Index);
end;
</pre>
<p class="MsoNormal">По чтению таблицы экспорта всё, точнее это конечно же только
первый этап, но для демонстрации корректности работы класса будет рассмотрен небольшой
тестовый пример. </p>
<pre class="brush:delphi">var
Raw: TRawPEImage;
hLib: THandle;
ExportFunc: TExportChunk;
begin
hLib := GetModuleHandle('ntdll.dll');
Raw := TRawPEImage.Create('c:\windows\system32\ntdll.dll', ULONG64(hLib));
try
for ExportFunc in Raw.ExportList do
if ExportFunc.FuncAddrVA <> ULONG64(GetProcAddress(hLib, PChar(ExportFunc.FuncName))) then
Writeln(ExportFunc.FuncName, ' wrong addr: ', ExportFunc.FuncAddrVA);
finally
Raw.Free;
end;
end.
</pre>
<p class="MsoNormal">В данном примере загружается ntdll.<span lang="EN-US">dll</span> и идет проверка рассчитанного адреса
каждой экспортируемой функции с его реальным значением, полученным через вызов
GetProcAddress.<o:p></o:p></p>
<p class="MsoNormal">Если все сделано правильно, то результатом выполнения будет
только одна строчка<o:p></o:p></p>
<p class="MsoNormal">Export count: 2469<o:p></o:p></p>
<p class="MsoNormal">Полученный в результате код достаточен, чтобы перейти к
следующему этапу, его можно забрать для самостоятельного изучения <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%201" target="_blank">по этой ссылке</a>.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Правда если меня сейчас читают люди, полностью понимающие то,
что происходит в этой главе, пока не начинайте возмущаться что выполнены не все
необходимые шаги по полноценной загрузке таблицы экспорта. Я про это в курсе,
но на данном этапе они пока что не нужны - все будет, но чуть позже. <o:p></o:p></p><p class="MsoNormal"><br /></p>
<a name="loader"></a>
<h3 style="text-align: left;">2. Работа со списками загрузчика</h3>
<p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p> </o:p></p><p class="MsoNormal">Встает вопрос - как получить список загруженных
файлов(библиотек) в удаленное адресное пространство процесса.<o:p></o:p></p><p class="MsoNormal">Вообще, конечно, технически есть вполне себе штатный способ,
через вызов EnumProcessModulesEx, но с ним есть нюанс - он не покажет данные по
64 битным модулям, будучи вызван из 32 битного процесса. <o:p></o:p></p><p class="MsoNormal"><span face=""Calibri",sans-serif" style="font-size: 11pt; line-height: 107%; mso-ansi-language: RU; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-language: AR-SA; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-language: EN-US; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;"></span></p>
<p class="MsoNormal">В этом можно убедиться вот таким кодом:<o:p></o:p></p>
<pre class="brush:delphi"> function EnumProcessModulesEx(hProcess: THandle; lphModule: PHandle;
cb: DWORD; var lpcbNeeded: DWORD; dwFilterFlag: DWORD): BOOL; stdcall;
external 'psapi.dll';
procedure TestEnumSelfModules;
const
LIST_MODULES_ALL = 3;
var
Buff: array of THandle;
Needed: DWORD;
I: Integer;
FileName: array[0..MAX_PATH] of Char;
begin
EnumProcessModulesEx(GetCurrentProcess, nil, 0, Needed, LIST_MODULES_ALL);
SetLength(Buff, Needed shr 2);
if EnumProcessModulesEx(GetCurrentProcess, @Buff[0], Needed, Needed, LIST_MODULES_ALL) then
begin
for I := 0 to Integer(Needed) - 1 do
if Buff[I] <> 0 then
begin
FillChar(FileName, MAX_PATH, 0);
GetModuleFileNameEx(GetCurrentProcess, Buff[I], @FileName[1], MAX_PATH);
Writeln(I, ': ', IntToHex(Buff[I], 1), ' ', string(PChar(@FileName[1])));
end;
end;
end;
</pre>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXVJyrfCJcAFGHwlCD5py-xlMVPCJZFLkmHGMyDAvXY1Kxbxx2IsJruhVa5OoWyzCs4bfHX4FBZzGxY0WpfI8viw-GFD3HPv1Uak_Iay1AGqVmEWEVdPQWTRR5ojD1Z2An8dajNIPdVVYfIv7RNskkehjYXLiOEzmSJDjIyhM6gQXovUJpIORwot1o/s1391/5.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="559" data-original-width="1391" height="129" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXVJyrfCJcAFGHwlCD5py-xlMVPCJZFLkmHGMyDAvXY1Kxbxx2IsJruhVa5OoWyzCs4bfHX4FBZzGxY0WpfI8viw-GFD3HPv1Uak_Iay1AGqVmEWEVdPQWTRR5ojD1Z2An8dajNIPdVVYfIv7RNskkehjYXLiOEzmSJDjIyhM6gQXovUJpIORwot1o/s320/5.png" width="320" /></a></div><br /><p class="MsoNormal">Слева то что он выведет, а справа реальная ситуация.
Отображены только 32 битные модули, загруженные из папки C:\Windows\SysWOW64\
причем из-за работающего редиректа, этот факт прячется и пути к
библиотекам показываются как C:\Windows\System32\ хотя это на самом деле не
так.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">
</p><p class="MsoNormal">Такое ограничение вполне себе понятно, дело в том, что он
возвращает список инстансов, а инстанс это адрес загрузки библиотеки, т.е.
указатель по сути, который для 32 бит может держать только 4 байта и не сможет
вместить в себя полный 64 битный адрес.<o:p></o:p></p><p class="MsoNormal">Давайте сразу же уточню один момент. <o:p></o:p></p><p class="MsoNormal">В 64 битной ОС <b>НЕ СУШЕСТВУЕТ</b> 32 битных процессов! Все
процессы, без исключения, являются 64 битными, и когда стартует 32 битное
приложение, сначала инициализируется 64 битный процесс, в который загружаются
библиотеки WOW64 подсистемы и только потом в него загружаются 32 битные образы,
которые работают с ОС не напрямую, а через WOW64 подсистему, конвертирующую все
32 битные вызовы API в их 64 битные аналоги.<o:p></o:p></p><p class="MsoNormal">Именно поэтому:<o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>В 32 битном процессе постоянно присутствуют загруженные
64 битные библиотеки</li><li>Флаг IMAGE_FILE_LARGE_ADDRESS_AWARE выставленный в РЕ
заголовке предоставляет доступ ко всем 4 гигабайтам памяти, а не к трем, как
это будет на 32 битной OS при включенном PAE (Physical Address Extension) и
флаге /3GB в boot.ini</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Так как анализатору нужны все данные, то получать список
загруженных модулей придется самостоятельно, через списки загрузчика.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Для это нужно произвести небольшую подготовку.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">В модуле <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%202/RawScanner.Types.pas">RawScanner.Types</a>
я декларирую три новых структуры TModuleData, UNICODE_STRING32 и UNICODE_STRING64,
и создаю новый модуль <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%202/RawScanner.Wow64.pas">RawScanner.Wow64</a>.
Он будет содержать все необходимое для работы с 64 битными процессами, а именно
обертки над следующими функциями:<o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>IsWow64Process - для детекта работы WOW64 подсистемы</li><li>Wow64DisableWow64FsRedirection +
Wow64RevertWow64FsRedirection - для отключения и включения редиректа библиотек
из System32 в SysWOW64</li><li>NtWow64QueryInformationProcess64 - аналог функции
NtQueryInformationProcess для работы с 64 битными процессами.</li><li>NtWow64ReadVirtualMemory64 - для чтения памяти удаленного
процесса по 64 битным указателям, недоступным при вызове ReadProcessMemory</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p> </o:p>Так же в модуль <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%202/RawScanner.Utils.pas">RawScanner.Utils</a>
добавляю функцию ReadRemoteMemory, в которой будет автоматически приниматься
решение какой из вызовов использовать для чтения удаленной памяти.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Структура TModuleData и список TModuleList будут хранить
информацию по загруженным модулям и использоваться на следующих этапах.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p> </o:p>Итак, где хранятся все данные о загруженных библиотеках? Их
формирует загрузчик для каждого процесса (причем в двух экземплярах если
процесс 32 битный). Представляет из себя двунаправленный список, доступ к
которому нужно получить из блока окружения процесса, который сам по себе также
представляет структуру, одним из полей которой является поле <a href="https://learn.microsoft.com/ru-ru/windows/win32/api/winternl/ns-winternl-peb">PPEB_LDR_DATA
Ldr</a>. </p><p class="MsoNormal">Чтобы получить данные из списков загрузчика, нужно сначала
научится правильно прочесть адрес начала этих списков.<o:p></o:p></p><p class="MsoNormal">Может быть четыре разных ситуации:<o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>Мы 32 битное приложение, которое будет читать данные из
64 битного</li><li>Мы 64 битное приложение, которое будет читать данные из
32 битного</li><li>Мы приложение, которое будет читать данные из процесса
такой-же битности (т.е. два разных случая под разную битность).</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Для каждого из перечисленных вариантов применяется свой
подход, распишу по шагам.</p><p class="MsoNormal">
Шаг первый, открываем процесс и проверяем его битность:<o:p></o:p></p>
<pre class="brush:delphi">var
hProcess: THandle;
IsWow64Mode: LongBool;
begin
hProcess := OpenProcess(
PROCESS_QUERY_INFORMATION or PROCESS_VM_READ,
False, GetCurrentProcessId);
Wow64Support.IsWow64Process(hProcess, IsWow64Mode);
</pre>
<p class="MsoNormal">Флаг IsWow64Mode будет сигнализировать о том, что удаленный
процесс работает под WOW64 подсистемой.<o:p></o:p></p><p class="MsoNormal">Следующим шагом потребуется декларация структуры блока
окружения процесса (полная структура не нужна, достаточно будет только до поля
загрузчика):</p>
<pre class="brush:delphi"> TPEB = record
InheritedAddressSpace: BOOLEAN;
ReadImageFileExecOptions: BOOLEAN;
BeingDebugged: BOOLEAN;
BitField: BOOLEAN;
Mutant: THandle;
ImageBaseAddress: PVOID;
LoaderData: PVOID; // именно это поле нас и интересует
end;
</pre>
<p class="MsoNormal">После чего потребуются еще две декларации её-же только
строго соответствующие битности списка загрузчика, из которого будет
производится чтение.<o:p></o:p></p><p class="MsoNormal">
</p><p class="MsoNormal">Еще раз заострю внимание, у 32 битного процесса на 64 битной
ОС таких списков будет два, соответственно блоков окружения процесса, через
которые происходит выход на список также будет два, для 64 бит и 32-битный для
WOW64! <o:p></o:p></p>
<pre class="brush:delphi"> TPEB32 = record
InheritedAddressSpace: BOOLEAN;
ReadImageFileExecOptions: BOOLEAN;
BeingDebugged: BOOLEAN;
BitField: BOOLEAN;
Mutant: ULONG;
ImageBaseAddress: ULONG;
LoaderData: ULONG;
end;
TPEB64 = record
InheritedAddressSpace: BOOLEAN;
ReadImageFileExecOptions: BOOLEAN;
BeingDebugged: BOOLEAN;
BitField: BOOLEAN;
Mutant: ULONG_PTR64;
ImageBaseAddress: ULONG_PTR64;
LoaderData: ULONG_PTR64;
end;
</pre>
<p class="MsoNormal">Загрузка данных будет достаточно тривиальная<span lang="EN-US">, </span>посредством вызова<span lang="EN-US"> NtQueryInformationProcess </span>с флагом<span lang="EN-US"> ProcessBasicInformation </span>получаем информацию о процессе<span lang="EN-US">, </span>в которой<span lang="EN-US">, </span>помимо прочего будет поле<span lang="EN-US">
PROCESS_BASIC_INFORMATION.PebBaseAddress, </span>содержащий адрес блока окружения процесса<span lang="EN-US">. <o:p></o:p></span></p><p class="MsoNormal">
</p><p class="MsoNormal">Вот из него и будут читаться нужные данные.<o:p></o:p></p>
<pre class="brush:delphi">function ReadNativePeb(hProcess: THandle; out APeb: TPEB64): Boolean;
const
ProcessBasicInformation = 0;
var
PBI: PROCESS_BASIC_INFORMATION;
dwReturnLength: Cardinal;
NativePeb: TPEB;
begin
Result := NtQueryInformationProcess(hProcess,
ProcessBasicInformation, @PBI, SizeOf(PBI), @dwReturnLength) = 0;
if not Result then
Exit;
Result := ReadRemoteMemory(hProcess, ULONG_PTR64(PBI.PebBaseAddress),
@NativePeb, SizeOf(TPEB));
if Result then
{$IFDEF WIN32}
APeb := Convert32PebTo64(TPEB32(NativePeb));
{$ELSE}
APeb := TPEB64(NativePeb);
{$ENDIF}
end;
</pre>
<p class="MsoNormal">Данная функция всегда возвращает 64 битный PEB, это сделано
только для удобства работы с этой структурой. Если читается 32 битный <span lang="EN-US">PEB</span>, то результат
преобразуется в 64 битный аналог вызовом Convert32PebTo64.<o:p></o:p></p><p class="MsoNormal">Но она покрывает только два случая из четырех
вышеперечисленных.<o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>При чтении 32 битного PEB из 32 битного процесса (при
этом мы сами находимся в 32 битной сборке)</li><li>При чтении 64 битного PEB из 64 битного процесса (при
этом мы сами находимся в 64 битной сборке)</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p>Показываю остальные варианты:<p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p></o:p></p>
<p></p>
<pre class="brush:delphi">function Read64PebFrom32Bit(hProcess: THandle; out APeb: TPEB64): Boolean;
const
ProcessBasicInformation = 0;
var
PBI64: PROCESS_BASIC_INFORMATION64;
dwReturnLength: Cardinal;
begin
Result := Wow64Support.QueryInformationProcess(hProcess,
ProcessBasicInformation, @PBI64, SizeOf(PBI64), dwReturnLength);
if not Result then
Exit;
Result := ReadRemoteMemory(hProcess, PBI64.PebBaseAddress,
@APeb, SizeOf(TPEB64));
end;
</pre>
<p class="MsoNormal">Код практически идентичен предыдущему за одним исключением,
здесь так-же читается информация по процессу с флагом ProcessBasicInformation,
но вызывается уже 64 битный вариант функции через WOW64 хэлпер, который
возвращает информацию именно по 64 битному PEB, поэтому этап конвертации тут
пропущен, ибо этот вызов всегда читает именно 64 битную структуру.<o:p></o:p></p>
<p class="MsoNormal">Данный вызов применяется только в том случае, когда он
вызван из 32 битной сборки и запущен на 64 битной ОС, причем применяется для
чтения 64 битного PEB в процессах любой битности.<o:p></o:p></p>
<p class="MsoNormal">И последний вариант:</p>
<pre class="brush:delphi">function Read32PebFrom64Bit(hProcess: THandle; out APeb: TPEB64): Boolean;
const
ProcessWow64Information = 26;
var
PebWow64BaseAddress: ULONG_PTR;
dwReturnLength: Cardinal;
Peb32: TPEB32;
begin
Result := NtQueryInformationProcess(hProcess,
ProcessWow64Information, @PebWow64BaseAddress, SizeOf(ULONG_PTR),
@dwReturnLength) = 0;
if not Result then
Exit;
Result := ReadRemoteMemory(hProcess, PebWow64BaseAddress,
@Peb32, SizeOf(TPEB32));
if Result then
APeb := Convert32PebTo64(Peb32);
end;
</pre>
<p class="MsoNormal">Этот вызов применяется строго из 64 битной сборки для чтения
32 битного PEB в 32 битном процессе (в 64 битном он отсутствует).<o:p></o:p></p>
<p class="MsoNormal">Из изменений, опять используется вызов
NtQueryInformationProcess но уже с флагом ProcessWow64Information возвращающим
информацию только по 32 битному PEB, поэтому в конце, при успешном чтении
данных, идет этап конвертации в 64 битный аналог.<o:p></o:p></p>
<p class="MsoNormal">Для удобства реализуется вот такой шлюз, который сам будет
вызывать нужный вариант кода в зависимости от текущей сборки приложения.</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi">function ReadPeb(hProcess: THandle; Read32Peb: Boolean; out APeb: TPEB64): Boolean;
begin
ZeroMemory(@APeb, SizeOf(TPEB64));
if Read32Peb then
{$IFDEF WIN32}
Result := ReadNativePeb(hProcess, APeb)
else
Result := Read64PebFrom32Bit(hProcess, APeb);
{$ELSE}
Result := Read32PebFrom64Bit(hProcess, APeb)
else
Result := ReadNativePeb(hProcess, APeb);
{$ENDIF}
end;
</pre>
<p class="MsoNormal">Тест шлюза может выглядеть вот так:<o:p></o:p></p>
<pre class="brush:delphi">var
PEB32, PEB64: TPEB64;
// загружаем блоки окружения процесса (если есть)
ReadPeb(hProcess, True, PEB32);
ReadPeb(hProcess, False, PEB64);
</pre>
<p class="MsoNormal">И вот только теперь, когда код чтения адреса загрузчика
готов, можно приступать к работе с ним, для этого смотрим на картинку:<o:p></o:p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxk-YGKsg9NfeqMddLoLIuawm9PHbS245s4eR4kkvPMuk5ty_8AXvdlJFZPWQAQYB8p9pYxuGbWtnj-9lfwawAJANR2uY1aJFyKIR86eq4rC9ebCtxzEEpFmrB9kttEAmebTueEGHGjE66WFlJ5FPsDTkwlFIJd5n51CrBpmW2Zj5DAI-_sTZZY8vV/s1304/6.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="928" data-original-width="1304" height="228" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxk-YGKsg9NfeqMddLoLIuawm9PHbS245s4eR4kkvPMuk5ty_8AXvdlJFZPWQAQYB8p9pYxuGbWtnj-9lfwawAJANR2uY1aJFyKIR86eq4rC9ebCtxzEEpFmrB9kttEAmebTueEGHGjE66WFlJ5FPsDTkwlFIJd5n51CrBpmW2Zj5DAI-_sTZZY8vV/s320/6.PNG" width="320" /></a></div><br /><p class="MsoNormal">Здесь я взял изображение из WinXP только по той причине, что
в ней списки загрузчика расположены в памяти более компактно, чем в остальных
версиях Windows, в которых они разбросаны по страницам сильно далеко друг от
друга.</p><p class="MsoNormal">Прочитанная ранее структура PEB содержит указатель на
структуру PEB_LDR_DATA, которая содержит ссылки на три публичных
двунаправленных списка, состоящих из элементов LDR_DATA_TABLE_ENTRY.<o:p></o:p></p><p class="MsoNormal">Если посмотрим на декларацию в MSDN, то в ней практически
все поля спрятаны, кроме одного списка, содержащий список загруженных модулей,
отсортированный в порядке их размещения в памяти </p><p class="MsoNormal"><a href="https://learn.microsoft.com/ru-ru/windows/win32/api/winternl/ns-winternl-peb_ldr_data">https://learn.microsoft.com/ru-ru/windows/win32/api/winternl/ns-winternl-peb_ldr_data</a><o:p></o:p></p><p class="MsoNormal">Пропущен список InLoadOrderModuleList (сортировка в порядке
загрузки) и InInitializationOrderModuleList (сортировка в порядке
инициализации).</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Вообще чисто технически, помимо этих трех списков,
операционная система держит еще 32 таких-же, использующихся для ускорения
работы того-же GetModuleHandle (поэтому, когда пишут код, скрывающий себя из
этих трех списков, типа руткит на минималках - всегда забывают еще и про
дополнительные, скрытые).<o:p></o:p></p><p class="MsoNormal">Впрочем, это лирика, сейчас хочу показать нюанс, на котором
многие спотыкаются. А спотыкаются, потому что в MSDN не правильное описание
полей структуры (в смысле комментарий к ним).</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">
</p><p class="MsoNormal">Посмотрите на декларацию структур:<o:p></o:p></p>
<pre class="brush:delphi"> LIST_ENTRY32 = record
FLink, BLink: ULONG;
end;
PEB_LDR_DATA32 = record
Length: ULONG;
Initialized: BOOL;
SsHandle: ULONG;
InLoadOrderModuleList: LIST_ENTRY32;
InMemoryOrderModuleList: LIST_ENTRY32;
InInitializationOrderModuleList: LIST_ENTRY32;
// etc...
end;
LDR_DATA_TABLE_ENTRY32 = record
InLoadOrderLinks: LIST_ENTRY32;
InMemoryOrderLinks: LIST_ENTRY32;
InInitializationOrderLinks: LIST_ENTRY32;
DllBase: ULONG;
EntryPoint: ULONG;
SizeOfImage: ULONG;
FullDllName: UNICODE_STRING32;
BaseDllName: UNICODE_STRING32;
Flags: ULONG;
// etc...
end;
</pre>
<div><br /></div><div><p class="MsoNormal">Смотрите, вот два поля, первое указывает на начало двусвязного
списка, отсортированного в порядке загрузки <span lang="EN-US" style="mso-ansi-language: EN-US;">PEB</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">LDR</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">DATA</span>.<span lang="EN-US" style="mso-ansi-language: EN-US;">InLoadOrderModuleList</span>.<span lang="EN-US" style="mso-ansi-language: EN-US;">FLink</span>, точнее содержит <span lang="EN-US" style="mso-ansi-language: EN-US;">VA</span><span lang="EN-US"> </span>адрес
первой структуры <span lang="EN-US" style="mso-ansi-language: EN-US;">LDR</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">DATA</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">TABLE</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">ENTRY</span> из этого списка.<o:p></o:p></p>
<p class="MsoNormal">По логике и <span lang="EN-US" style="mso-ansi-language: EN-US;">PEB</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">LDR</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">DATA</span>.<span lang="EN-US" style="mso-ansi-language: EN-US;">InMemoryOrderModuleList</span>.<span lang="EN-US" style="mso-ansi-language: EN-US;">FLink</span><span lang="EN-US"> </span>должен указывать на <span lang="EN-US" style="mso-ansi-language: EN-US;">LDR</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">DATA</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">TABLE</span>_<span lang="EN-US" style="mso-ansi-language: EN-US;">ENTRY</span>
другого списка, отсортированного в порядке размещения в памяти, ибо об этом написано
в <span lang="EN-US" style="mso-ansi-language: EN-US;">MSDN</span>.<o:p></o:p></p>
<p class="MsoNormal">Собственно пруф:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh0sjLRiLAod6ZAgK56RoUvO2d4ZMx4zCtQ36e3fiZSx6Y9ni-H9IC9NOGaAMH4wqYutq3NSCbR5yCm3tX8Vr5tmplMLcKn5wremo_nAxlcsbBJ-duodgLKWHF-Q6_dMhwQ31a4II7ZyngeqlH0ECflDXpNS3olEyxQxkakZdDrxqAaaAMdJ-rjzBnm/s907/7.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="833" data-original-width="907" height="294" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh0sjLRiLAod6ZAgK56RoUvO2d4ZMx4zCtQ36e3fiZSx6Y9ni-H9IC9NOGaAMH4wqYutq3NSCbR5yCm3tX8Vr5tmplMLcKn5wremo_nAxlcsbBJ-duodgLKWHF-Q6_dMhwQ31a4II7ZyngeqlH0ECflDXpNS3olEyxQxkakZdDrxqAaaAMdJ-rjzBnm/s320/7.png" width="320" /></a></div><br /><p class="MsoNormal">Однако это не соответствует действительности!</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">На самом деле каждый LIST_ENTRY указывает на самого себя в
следующей структуре, а не на её начало, таким образом:<o:p></o:p></p>
<p class="MsoNormal"></p><ul style="text-align: left;"><li><span lang="EN-US" style="mso-ansi-language: EN-US;">PEB_LDR_DATA.InLoadOrderModuleList.FLink </span>указывает<span style="mso-ansi-language: EN-US;"> </span>на<span lang="EN-US" style="mso-ansi-language: EN-US;"> LDR_DATA_TABLE_ENTRY.InLoadOrderLinks (+0 </span>от<span style="mso-ansi-language: EN-US;"> </span>начала<span lang="EN-US" style="mso-ansi-language: EN-US;"> LDR_DATA_TABLE_ENTRY)<o:p></o:p></span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">PEB_LDR_DATA.InMemoryOrderModuleList.FLink </span>указывает<span style="mso-ansi-language: EN-US;"> </span>на<span lang="EN-US" style="mso-ansi-language: EN-US;"> LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks (+ sizeof(LIST_ENTRY) </span>от<span style="mso-ansi-language: EN-US;"> </span>начала<span lang="EN-US" style="mso-ansi-language: EN-US;"> LDR_DATA_TABLE_ENTRY)<o:p></o:p></span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">PEB_LDR_DATA.InInitializationOrderModuleList.FLink </span>указывает<span style="mso-ansi-language: EN-US;"> </span>на<span lang="EN-US" style="mso-ansi-language: EN-US;"> LDR_DATA_TABLE_ENTRY.InInitializationOrderLinks (+ sizeof(LIST_ENTRY) *
2 </span>от<span style="mso-ansi-language: EN-US;"> </span>начала<span lang="EN-US" style="mso-ansi-language: EN-US;"> LDR_DATA_TABLE_ENTRY)<o:p></o:p></span></li></ul><p></p>
<p class="MsoNormal">Проверьте это по предыдущей картинке, я специально обозначил линки
разными цветами, только красные стрелки идут в начало каждой структуры, синяя
(означающая список InMemoryOrderLinks) идет со сдвигом, а зеленая, означающая
InInitializationOrderLinks мало того что идет с еще большим сдвигом (т.к. она
пропускает два двусвязных списка) так еще и всегда указывает на ntdll первым
элементом списка, всегда пропуская исполняемый файл.<o:p></o:p></p>
<p class="MsoNormal">Для чтения данных загрузчика наиболее удобным будет
InLoadOrderModuleList.<span lang="EN-US" style="mso-ansi-language: EN-US;"><o:p></o:p></span></p>
<pre class="brush:delphi">function TLoaderData.Scan64LdrData(LdrAddr: ULONG_PTR64): Integer;
var
Ldr: PEB_LDR_DATA64;
Entry: LDR_DATA_TABLE_ENTRY64;
Module: TModuleData;
begin
Result := 0;
// читаем первичную структуру для определения начала списка
if not ReadRemoteMemory(FProcess, LdrAddr,
@Ldr, SizeOf(PEB_LDR_DATA64)) then
Exit;
LdrAddr := Ldr.InLoadOrderModuleList.FLink;
// крутим цикл, пока не встретим завершающую структуру
while (ReadRemoteMemory(FProcess, LdrAddr,
@Entry, SizeOf(LDR_DATA_TABLE_ENTRY64))) and (Entry.DllBase <> 0) do
begin
Module.ImageBase := Entry.DllBase;
Module.Is64Image := True;
SetLength(Module.ImagePath, Entry.FullDllName.Length shr 1);
if not ReadRemoteMemory(FProcess, Entry.FullDllName.Buffer,
@Module.ImagePath[1], Entry.FullDllName.Length) then
begin
LdrAddr := Entry.InLoadOrderLinks.FLink;
Continue;
end;
// ...
FModuleList.Add(Module);
LdrAddr := Entry.InLoadOrderLinks.FLink;
Inc(Result);
end;
end;
</pre>
<p class="MsoNormal">Так выглядит цикл чтения данных из списка 64 битного
PEB->Ldr. Очень просто и компактно, условие выхода состоит из проверки
завершающей структуры в списке, у которой все поля равны нулю (в данном случае
проверка идет только по полю Entry.DllBase).<o:p></o:p></p><p class="MsoNormal">Но это для 64 бит, с которым никаких проблем не будет, а вот
с 32 битами все намного хитрее. Дело в том, что прямо сейчас нельзя нормально
подгрузить список библиотек. Посмотрите самую первую картинку в начале главы,
как вы думаете, откуда EnumProcessModulesEx брал информацию о модулях? Все
верно, оттуда же откуда и мы сейчас, из списков загрузчика в удаленном
процессе, и там, в этих списках для 32 битного PEB->Ldr все пути ведут к
c:\windows\system32\ хотя по факту библиотеки грузятся совершенно по другому
пути.<o:p></o:p></p><p class="MsoNormal">Как выкрутится из такой ситуации, будет показано в следующей
главе, а сейчас, чтобы не мешать все в одну кучу, я покажу как можно решить эту
проблему через применение небольшого костыля. Он нужен чтобы показать финальную
работу класса, который был написан в этой главе и далее использоваться не будет.<o:p></o:p></p><p class="MsoNormal">Итак, код чтения списка 32 битного загрузчика (вместе с
костылем) будет выглядеть вот так (целиком код приводить не буду, он похож на
чтение 64 бит).</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">
</p><p class="MsoNormal">Потребуется вот такой вспомогательный костыль, проверяющий -
является ли файл на диске 32 битным:<o:p></o:p></p></div>
<pre class="brush:delphi"> function IsFile32(const FilePath: string): Boolean;
var
DosHeader: TImageDosHeader;
NtHeader: TImageNtHeaders32;
Raw: TBufferedFileStream;
begin
Raw := TBufferedFileStream.Create(FilePath, fmShareDenyWrite);
try
Raw.ReadBuffer(DosHeader, SizeOf(TImageDosHeader));
Raw.Position := DosHeader._lfanew;
Raw.ReadBuffer(NtHeader, SizeOf(TImageNtHeaders32));
Result := NtHeader.FileHeader.Machine = IMAGE_FILE_MACHINE_I386;
finally
Raw.Free;
end;
end;
</pre>
<div>а на место троеточия в коде выше встанет вот такой костыль:</div><div><br /></div>
<pre class="brush:delphi"> // нюанс, 32 битные библиотеки в списке LDR будут прописаны с путем из
// дефолтной системной директории, хотя на самом деле они грузятся
// из SysWow64 папки. Поэтому проверяем, если SysWow64 присутствует
// то все 32 битные пути библиотек меняем на правильный посредством
// вызова GetMappedFileName + нормализация.
// Для 64 битных это делать не имеет смысла, т.к. они грузятся по старшим
// адресам куда не может быть загружена 32 битная библиотека, а по младшим
// мы и сами сможет прочитать данные из 32 битной сборки
if FUse64Addr then
begin
// GetMappedFileName работает с адресами меньше MM_HIGHEST_USER_ADDRESS
// если адрес будет больше - вернется ноль с ошибкой ERROR_INVALID_PARAMETER
if Module.ImageBase < MM_HIGHEST_USER_ADDRESS then
begin
MapedFilePathLen := GetMappedFileName(FProcess, Pointer(Module.ImageBase),
@MapedFilePath[1], MAX_PATH * SizeOf(Char));
if MapedFilePathLen > 0 then
Module.ImagePath := NormalizePath(Copy(MapedFilePath, 1, MapedFilePathLen));
end
else
begin
// а если адрес библиотеки выше допустимого, то будем делать костыль
// проверка, находится ли файл в системной директории?
if Module.ImagePath.StartsWith(Wow64Support.SystemDirectory, True) then
begin
// проверка, есть ли файл на диске и является ли он 32 битным?
if not (FileExists(Module.ImagePath) and IsFile32(Module.ImagePath)) then
begin
// нет, файл отсутствует либо не является 32 битным
// меняем путь на SysWow64 директорию
Module.ImagePath := StringReplace(
Module.ImagePath,
Wow64Support.SystemDirectory,
Wow64Support.SysWow64Directory, [rfIgnoreCase]);
// повторная проверка
if not (FileExists(Module.ImagePath) and IsFile32(Module.ImagePath)) then
// если в SysWow64 нет подходящего файла, чтож - тогда пропускаем его
// потому что мы его всеравно не сможем правильно подгрузить и обработать
Module.ImagePath := EmptyStr;
end;
end;
end;
end;
</pre>
<div>Логика кода банальна, все что можно прочитать через штатный
вызов GetMappedFileName, читаем через него (получая таким образом правильный
путь к загруженной библиотеке).</div><div><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Правда GetMappedFileName требует нормализации пути, т.к.
возвращаемая им строка выглядит как
"\Device\HarddiskVolume1\Windows\SysWOW64\ntdll.dll" и её надо
привести в соответствие букве диска (но это мелочи).<o:p></o:p></p>
<p class="MsoNormal">А вот для всего что прочитать не получится, будет работать
костыль, в котором будет проверка - действительно ли библиотека лежащая в
c:\windows\system32\ является 32 битной, и если нет - то будет вторая проверка,
а есть ли такая же, но уже в syswow64, и если есть - то путь меняется на
"правильный".<o:p></o:p></p>
<p class="MsoNormal">Ну и теперь тестовый код:</p><p class="MsoNormal"><o:p></o:p></p></div><div><p class="MsoNormal"><o:p></o:p></p></div>
<pre class="brush:delphi">var
Loader: TLoaderData;
// полученые адреса загрузчика передаем лоадеру списков
Loader := TLoaderData.Create(hProcess, IsWow64Mode);
try
Loader.Load32LoaderData(PEB32.LoaderData);
Loader.Load64LoaderData(PEB64.LoaderData);
Writeln(0, ': ', IntToHex(Loader.RootModule.ImageBase, 1), ' ', Loader.RootModule.ImagePath);
for var I := 0 to Loader.Modules.Count - 1 do
Writeln(I + 1, ': ', IntToHex(Loader.Modules[I].ImageBase, 1), ' ', Loader.Modules[I].ImagePath);
finally
Loader.Free;
end;
</pre>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJEJL-rnvcPBV_6bdm_OpEGH9A8qD5z5LCbZ9qLpo499XZRa9RAeN3GNDvfjWxsLtSlSnF3bZwwsTVyYtESijUCUsa62RvvLHViG3qMMysMFbsDFzXNIdMCBW9WFWMl6zNCbkmDO5XNvFhUpRkiMbMWukDNuxa0Aov-Vn2PoTzeslzHdesF5xPPhZ3/s1540/8.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="559" data-original-width="1540" height="116" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJEJL-rnvcPBV_6bdm_OpEGH9A8qD5z5LCbZ9qLpo499XZRa9RAeN3GNDvfjWxsLtSlSnF3bZwwsTVyYtESijUCUsa62RvvLHViG3qMMysMFbsDFzXNIdMCBW9WFWMl6zNCbkmDO5XNvFhUpRkiMbMWukDNuxa0Aov-Vn2PoTzeslzHdesF5xPPhZ3/s320/8.png" width="320" /></a></div><br /><div><br /></div><div><p class="MsoNormal">Результат<span style="mso-ansi-language: EN-US;"> </span>совпал<span lang="EN-US" style="mso-ansi-language: EN-US;">, </span>код<span style="mso-ansi-language: EN-US;"> </span>вывел<span style="mso-ansi-language: EN-US;"> </span>все<span lang="EN-US" style="mso-ansi-language: EN-US;"> 27 </span>модулей<span style="mso-ansi-language: EN-US;"> </span>из<span style="mso-ansi-language: EN-US;">
</span>списков<span style="mso-ansi-language: EN-US;"> </span>загрузчика<span lang="EN-US" style="mso-ansi-language: EN-US;">, </span>пропустив<span style="mso-ansi-language: EN-US;"> </span>только<span style="mso-ansi-language: EN-US;"> </span>один<span style="mso-ansi-language: EN-US;"> </span>загруженный<span lang="EN-US" style="mso-ansi-language: EN-US;">
C:\Windows\System32\en-US\kernel32.dll.mui<o:p></o:p></span></p>
<p class="MsoNormal">Но с ним как раз все нормально, т.к. данная библиотека не
является исполняемой и отсутствует в списках загрузчика т.к. загружена с флагом
LOAD_LIBRARY_AS_DATAFILE.</p>
<p class="MsoNormal">Код ко второй главе для самостоятельного изучения <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%202" target="_blank">доступен по этой ссылке</a>.</p>
<p class="MsoNormal">А теперь пришло время разбираться с костылем при чтении
данных из 32 битного загрузчика<o:p></o:p></p></div><p> </p>
<a name="heaven"></a>
<h3 style="text-align: left;">3. Вызов 64 битного кода из 32 битного контекста</h3><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<p class="MsoNormal">Итак, еще раз напомню основной тезис второй главы: в 64
битной OS нет 32 битных процессов. Есть только строго 64 битные, а вся 32
битность эмулируется посредством WOW64 подсистемы.<o:p></o:p></p>
<p class="MsoNormal">Когда вы вызываете какую-либо API из своего 32 битного кода,
управление ей передается не напрямую - происходит этап конвертации параметров в
формат, который требует 64 битный аналог этой API и её прямой вызов.<o:p></o:p></p>
<p class="MsoNormal">Что за конвертация: тут все заключается в соглашении о
вызовах. Как правило это STDCALL который в 32 битах требует передачи всех
параметров через стэк, а вот в 64 битах STDCALL работает немного по-другому, а
именно он похож на вызов с соглашением FASTCALL, где первые четыре параметра
идут через регистры (RCX/RDX/R8/R9) а остальные через стек.<o:p></o:p></p>
<p class="MsoNormal"><a href="https://en.wikipedia.org/wiki/X86_calling_conventions#Microsoft_x64_calling_convention">https://en.wikipedia.org/wiki/X86_calling_conventions#Microsoft_x64_calling_convention</a><o:p></o:p></p>
<p class="MsoNormal">Но есть нюанс, напрямую вызвать 64-битный аналог нельзя.
Дело в том, что любой 32 битный код и 64 битный исполняются в разных
контекстах!<o:p></o:p></p>
<p class="MsoNormal">Если вы откроете свой отладчик и перейдете в режим
ассемблера, то увидите, что селектор сегмента кода CS будет равен 0х23 (для 32
битного приложения), а если мы будем отлаживать 64 битное приложение, то
контекст станет равен 0x33.<o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPiw-eNvz1KCGEaZopnBzBd9RcDztvARQ7o7c-L7kqrvke2SkUDsVBInFDH_68FzPcjYfUoPo41wfZU65qFgUHKxsWDK1SEJlhvMrpNyPg9dbWSnnikz5wgSbx6fd8GIr_uNmjxcuBDtE_KS9lVBxwhXBQ7nOaTADD83OLs-7D3-CQ0T6YLd4SkaPv/s524/9.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="290" data-original-width="524" height="177" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPiw-eNvz1KCGEaZopnBzBd9RcDztvARQ7o7c-L7kqrvke2SkUDsVBInFDH_68FzPcjYfUoPo41wfZU65qFgUHKxsWDK1SEJlhvMrpNyPg9dbWSnnikz5wgSbx6fd8GIr_uNmjxcuBDtE_KS9lVBxwhXBQ7nOaTADD83OLs-7D3-CQ0T6YLd4SkaPv/s320/9.png" width="320" /></a></div><br /> <p></p>
<p class="MsoNormal">В зависимости от операционной системы такое переключение
контекста делается разными способами, но все в итоге приходит к одному - вызову
одной из этих инструкций<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li><span lang="EN-US" style="mso-ansi-language: EN-US;">JMP FAR
0x33:addr</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">CALL FAR
0x33:addr</span></li><li>PUSH 0x33 + PUSH addr + RETF</li></ol><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p>Каждая из этих инструкций передает управление по указанному
адресу с переключением сегмента кода на 64 бита (обратное переключение делается
аналогично, только сегмент равен 0х23).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Давайте посмотрим все это на примере вызова функции
NtQueryVirtualMemory() вызываемой в Windows 8.1 (на Windows 10 и выше картинка
будет немного другая, но суть в итоге не изменится).<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbcaBZT-gn_1Vh5a237WoKxd1rDWNPJrzdQlaLv7uK4-6aDt3pj8F28kwuy2UzKUdggdcP4O6Oy9NdN7rkQohiKjhoXGoZ6a53keU1Q1VWTPYXHsKQ7QPncgyC0b3g8x_ji2xPUzblKWO2oiB4fvpbnx9Tr6hsE5mhaE8DWYTCJOITliAngVPj0yHl/s1724/10.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="836" data-original-width="1724" height="155" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbcaBZT-gn_1Vh5a237WoKxd1rDWNPJrzdQlaLv7uK4-6aDt3pj8F28kwuy2UzKUdggdcP4O6Oy9NdN7rkQohiKjhoXGoZ6a53keU1Q1VWTPYXHsKQ7QPncgyC0b3g8x_ji2xPUzblKWO2oiB4fvpbnx9Tr6hsE5mhaE8DWYTCJOITliAngVPj0yHl/s320/10.png" width="320" /></a></div><br /><p class="MsoNormal">По шагам:</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Функция начинается с инициализации регистра EAX некоей
константой 0х22. Это так называемый SDT индекс (порядковый номер в таблице
системных вызовов, или в оригинале - System Service Dispatch Table). <o:p></o:p></p>
<p class="MsoNormal">Ну точнее как, на самом деле SDT индекс занимает только
младшее слово (16 бит), а вот старшее уже является вторым индексом для WDT (специальная
таблица диспетчеризации Wow64 вызовов, о ней чуть позже).<o:p></o:p></p>
<p class="MsoNormal">Следующим шагом происходит вызов через сегмент FS с адресом
0xC0.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Cегмент FS в случае 32 битного кода всегда указывает на
структуру Thread Environment Block (TEB), т.н. блок окружения потока (в 64
битах за это отвечает GS), а адрес 0xC0 указывает на смещение в этой структуре
от её начала.<o:p></o:p></p>
<p class="MsoNormal">В MSDN полноценной документации по этой структуре вы не
найдете, но (внезапно) она достаточно хорошо документирована в википедии:<o:p></o:p></p>
<p class="MsoNormal"><a href="https://en.wikipedia.org/wiki/Win32_Thread_Information_Block">https://en.wikipedia.org/wiki/Win32_Thread_Information_Block</a><o:p></o:p></p>
<p class="MsoNormal">Эта структура создается для каждого потока вашего
приложения, причем в случае 32 битного приложения на 64 битной Windows,
создается в двух экземплярах (32 и 64 бита соответственно).<o:p></o:p></p>
<p class="MsoNormal">Содержит кучу полезных для работы полей, допустим самое
первое поле 32 битного TEB это указатель на верхушку SEH фреймов (текущей
цепочки обработчиков исключений), через TEB реализованы все threadvar (точнее
через поле TLS слотов) и прочее-прочее... Сейчас нас интересует поле
Wow32Reserved, на которое указывает оффсет 0хС0, которое содержит в себе адрес
функции KiFastSystemCall, состоящей из одной единственной инструкции JMP FAR<o:p></o:p></p>
<p class="MsoNormal">Инструкция KiFastSystemCall, выглядящая как JMP FAR
0x33:0x77C331A4 производит переключение селектора сегмента кода CS в 64 битный
режим с выставлением значения 0х33 и передает управление на 64 битный код, в
функцию CpupReturnFromSimulatedCode.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Не удивляйтесь такому странному названию функции, как я и
говорил - в 64 битной Windows нет 32 битных процессов и эта функция означает
что мы вернулись в родную среду из эмуляции 32 бит.<o:p></o:p></p>
<p class="MsoNormal">CpupReturnFromSimulatedCode, производит переключение на 64
битный стек и сохраняет состояние части регистров в CPUCONTEXT, после чего
происходит передача управления функции TurboDispatchJumpAddressStart().</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Именно в этой функции и происходит работа с старшей частью
регистра EAX, которая была установлена еще на самом первом этапе.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Если вкратце - эта константа (старшая часть - на картинке
выделена оранжевым) является индексом в т.н. Wow64 Dispatch Table (WDT) на
которую указывает регистр R15. Комбинацией этих двух регистров (R15 + старшая
часть EAX, перемещенная в ECX) из данной таблицы выбирается адрес обработчика,
которому и передается управление (конвертация 32 битных параметров для вызова
64 битного аналога функции).<o:p></o:p></p>
<p class="MsoNormal">Точнее как, сама функция TurboDispatchJumpAddressEnd это
такая большая вершимель кода обрабатывающая параметры вызываемой функции тем
или иным способом, а вот WDT это в общем приближении аналог некоей таблицы
switch-case которая располагается (как правило) чуть выше директории экспорта.<o:p></o:p></p>
<p class="MsoNormal">Если подгрузить отладочные символы, то можно увидеть имена
обработчиков, вот я тут выписал несколько из них, чтобы было общее
представление что каждый из них делает:<o:p></o:p></p>
<p class="MsoNormal">ServiceNoTurbo - расположен в самом начале
TurboDispatchJumpAddressEnd (хэндлер по умолчанию для большинства вызовов)</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"></p><ul style="text-align: left;"><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk0Arg</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk0ArgReloadState</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk1ArgSp</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk1ArgNSp</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk2ArgNSpNSp</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">...</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk4ArgSpNSpNSpNSpReloadState</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk4ArgNSpSpNSpNSp</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">Thunk4ArgSpSpSpNSp</span></li><li><span lang="EN-US" style="mso-ansi-language: EN-US;">ThunkNone -
</span>конец<span style="mso-ansi-language: EN-US;"> </span>таблицы<span style="mso-ansi-language: EN-US;"> </span>диспатча<span lang="EN-US" style="mso-ansi-language: EN-US;">, </span>прямой<span style="mso-ansi-language: EN-US;"> </span>переход<span style="mso-ansi-language: EN-US;"> </span>на<span lang="EN-US" style="mso-ansi-language: EN-US;"> INT3<o:p></o:p></span></li></ul><p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFknxOAGIWrDisapPJOOzmJlMwhI4Yw2gJvAO_-_gGSFxe860uZ8sKqr3_7Vwuc9i8_mmzebTP9ssjShh42D_xjFF-k7LjzRA7TCx_P0KvhDrhjXbnvCfYPKVzUIJoNk7V4lrTUvpIculKTBF7WLqGwEHADfwj3XiWZj_SkFW5pTYIvkCxGcjGRtu8/s960/11.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="580" data-original-width="960" height="193" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFknxOAGIWrDisapPJOOzmJlMwhI4Yw2gJvAO_-_gGSFxe860uZ8sKqr3_7Vwuc9i8_mmzebTP9ssjShh42D_xjFF-k7LjzRA7TCx_P0KvhDrhjXbnvCfYPKVzUIJoNk7V4lrTUvpIculKTBF7WLqGwEHADfwj3XiWZj_SkFW5pTYIvkCxGcjGRtu8/s320/11.png" width="320" /></a></div><br /><p class="MsoNormal">Сам же SDT индекс NtQueryVirtualMemory содержится в младшей
части этой некоей константы и равен он 0х22 - именно с этими параметрами и
будет произведет вызов в ядро, что будет соответствовать своему 64 битному
аналогу.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Конкретно для текущего вызова NtQueryVirtualMemory WDT
индекс равен нулю, поэтому посредством таблицы диспетчеризации произойдет
передача управления на хэндлер ServiceNoTurbo в результате чего будет
произведен вызов Wow64SystemServiceEx.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Для справки: В Windows 10 и выше часть этих шагов изменена,
в частности вызов через TEB заменен на аналогичную ему цепочку вызовов
Wow64SystemServiceCall -> KiFastSystemCall, причем KiFastSystemCall теперь
передает управление не напрямую в CpupReturnFromSimulatedCode, а определяет её
адрес через WDT (такая вот оптимизация, видимо в будущем будут еще расширять).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">И вот примерно такая же обвязка сделана для каждой 32-битной
API функции, в том числе и для GetMappedFileName (а если точнее для
NtQueryVirtualMemory которую она использует для своей работы).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Но минус в том, что все эти обработчики из WDT знают, что их
вызвали из 32 битного кода, а стало быть, они точно знают, что 64 битные
указатели к ним прийти не могут.<o:p></o:p></p>
<p class="MsoNormal">А задача стоит, напомню (для решения проблемы, описанной во
второй главе) вызвать полноценный 64 битный аналог функции (именно
NtQueryVirtualMemory), передав в неё полноценный 64 битный указатель.<o:p></o:p></p>
<p class="MsoNormal">Вообще NtQueryVirtualMemory будет в этой статье эдаким
краеугольным камнем, вокруг которого все вертится. Она будет применяться для реализации следующих альтернатив
(имеется ввиду функции, чьи 64 битные аналоги будут вызываться из 32 бит
напрямую):</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>VirtualQueryEx - реализуется через вызов
NtQueryVirtualMemory с флагом MemoryBasicInformation (равным нулю)</li><li>QueryWorkingSet - то же, только флаг MemoryWorkingSetList
(равный единице)</li><li>GetMappedFileName - то же, только флаг
MemoryMappedFilenameInformation (равный двойке)</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Все эти функции (точнее их 64 битные аналоги), нужны для
правильной работы с 64 битными указателями из 32 битного кода. </p>
<p class="MsoNormal">А сейчас надо разобраться с нюансами вызова 64 битных
функций и по сути написать аналог Wow64SystemServiceEx (не полноценный,
конечно, но аналог). </p>
<p class="MsoNormal">Итак, так как нельзя использовать вызов через TEB
посредством сегмента FS (по причине что там ждут 32 битный указатель), нужно как-то определить адрес 64 битной функции в адресном пространстве
нашего процесса.<o:p></o:p></p>
<p class="MsoNormal">Это уже можно сделать, так как:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>известен способ, как получить адрес 64 битной NTDLL (код из
второй главы)</li><li>известен способ, как получить адрес экспортируемой библиотекой
функции (код из первой главы)</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Какие действия еще необходимо сделать для нормального
переключения в 64 бита и переноса результата обратно в 32.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">1. Нужно контролировать стек. В 32 битах стек выравнивается
по границе 4 байт, а вот в 64 битах он выровнен по границе 8 байт, поэтому
перед передачей управления 64 битной функции нужно удостоверится в том что
регистр ESP содержит правильное значение (грубо ESP mod 8 = 0).<o:p></o:p></p>
<p class="MsoNormal">Строго говоря, нужно контролировать значения двух регистров
ESP + EBP, но из-за особенностей формирования 64 битного стекового фрейма,
достаточно будет проконтролировать только ESP, т.к. его значение будет передано
в конечном итоге в RBP и оба регистра будут содержать уже выровненные значения.<o:p></o:p></p>
<p class="MsoNormal">2. Контролировать размеры теневого пространства
(ShadowSpace), используемого для сброса регистров RCX/RDX/R8/R9. Данная область
должна быть всегда выделена на стеке перед вызовом любой 64 битной stdcall
функции, даже в случае, когда она принимает меньшее количество параметров. В
случае если количество параметров больше четырех, размер ShadowSpace
расширяется на их размер.<o:p></o:p></p>
<p class="MsoNormal">3. Перевести 32 битные параметры, лежащие на стеке (из-за
соглашения STDCALL) в требуемый формат 64 битного вызова. <o:p></o:p></p>
<p class="MsoNormal">NtQueryVirtualMemory имеет шесть параметров
<a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryvirtualmemory">https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryvirtualmemory</a><o:p></o:p></p>
<p class="MsoNormal">Первые 4 параметра должны будут идти в регистрах
RCX/RDX/R8/R9, остальные будут размещены на стеке, причем изначально все шесть
параметров будут лежать на 32 битном стеке с оффсетом в 4 байта, а вот при
переносе последних двух на 64 битный стек, нужно учитывать что они должны
располагаться уже с учетом 8 байтного выравнивания.<o:p></o:p></p>
<p class="MsoNormal">4. Если нам нужно передать значения через регистры, можно
воспользоваться стандартными EAX/EBX/ECX/EDX/ESI/EDI которые после переключения
в 64 битный код преобразуются к RAX/RBX/RCX/RDX/RSI/RDI. Тоже будет работать и
при обратном переключении, за исключением что старшие 32 бита каждого регистра
будут отрезаны, т.е. мы сможем вытащить только младшую часть значения 64 битных
регистров (а больше нам и не надо).<o:p></o:p></p>
<p class="MsoNormal">5. 8 байтовый 64 битный результат вполне укладывается в
размер регистра RAX, но для передачи его в 32 бита нужно использовать пару
EAX+EDX где EDX должен содержать старшие 32 бита RAX.<o:p></o:p></p>
<p class="MsoNormal">На этом список требований закончился, можно начинать писать
код. Заголовок функции будет выглядеть так: </p>
<pre class="brush:delphi">function NtQueryVirtualMemory64(FuncRVA: ULONG_PTR64; hProcess: THandle;
BaseAddress: ULONG_PTR64; MemoryInformationClass: DWORD;
MemoryInformation: Pointer; MemoryInformationLength: DWORD;
ReturnLength: PULONG64): NTSTATUS; assembler; stdcall;
asm
end;</pre>
<p class="MsoNormal">К списку параметров добавился адрес функции (первый
параметр), остальные без изменения и соответствуют параметрам оригинальной
функции.<o:p></o:p></p>
<p class="MsoNormal">Думаю, с первым параметром все понятно - это адрес на
который мы должны передать управление в 64 битном коде, и он равен реальному
адресу NtQueryVirtualMemory в 64 битной NTDLL.<o:p></o:p></p>
<p class="MsoNormal">Обратите внимание что первый и третий параметр объявлены как
ULONG_PTR64. Это восьмибайтный тип, именно он будет заменять 64 битные
указатели в 32 битном коде.<o:p></o:p></p>
<p class="MsoNormal">Декларация соглашения stdcall (и наличие хотя бы одного
параметра) заставит <span lang="EN-US" style="mso-ansi-language: EN-US;">Delphi</span><span lang="EN-US"> </span>сгенерировать пролог и эпилог функции, используя который будет
удобно обработать пункт первый из необходимых действий, а именно коррекцию
стека по восьмибайтной границе. Если прямо сейчас вызвать эту заглушку функции,
то пролог и эпилог (в CPU) будут выглядеть следующим образом:</p>
<pre class="brush:asm">asm
// этот код будет сгенерирован Delphi автоматически
// ... пролог ...
push ebp // сохраняется база предыдущего стекового кадра
mov ebp, esp // инициализация базы новой верхушкой стека начинает таким образом новый стековый кадр
// ... и эпилог ...
pop ebp // восстановление базы предыдущего стекового кадра
ret $24 // возврат с коррекцией верхушки стека (ESP) 0х24 = пять параметров по 4 байта + 2 параметра по 8 байт
end;
</pre>
<p class="MsoNormal"><o:p> </o:p>Следующий шаг - добавляем контроль выравнивания 32 битного
стека по границе 8 байт:</p>
<pre class="brush:asm"> mov eax, esp // берем текущее значение Stack Pointer
and eax, 7 // нас интересует значение младших трех бит, отсекаем все лишнее
cmp eax, 0 // проверяем - равно ли получившееся число нулю?
je @stack_aligned
// если не равно, тогда стек не выровнен, в EAX будет оффсет от ESP на сколько
// сдвинулись данные в 32-битном стеке после правки значения ESP
sub esp, eax
@stack_aligned:
// отсюда работаем с выровненным по границе 8 байт стеком
</pre>
<p class="MsoNormal">Важный нюанс, т.к. декларация stdcall, все параметры функции
в текущий момент расположены на 32 битном стеке. Но т.к. мы выполняем коррекцию
верхушки стека, необходимо знать на какое значение была произведена эта
коррекция, чтобы потом из ESP/RSP получить правильные параметры. Именно за это
и отвечает регистр EAX, который будет содержать в себе либо ноль (означающий
что коррекция не производилась) либо другое число (должна быть четверка, но это
не всегда обязательно). Конечно, можно было бы использовать регистр EBP как
фиксированную точку, но нам предстоит переход в 64 битный режим, где также
потребуется сформировать стековый фрейм, а он будет расположен относительно
актуального значения ESP/RSP, так что EBP не подойдет. </p>
<p class="MsoNormal">Чтобы было более понятно, то после контроля выравнивания стека
будет одно из двух состояний:<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0P5jr5Kd6BcHxfDq0bd37u4NU7HNTJos_YLIN6S7sVXU3N1THA6wbCBYRWYeTij7y5N4pjNuaYqYyBcm2naSwSBM2UcqVAnVJ8oc5y5_dFF0Bn1SvWtyH1p7xbv_T85N4woeF0W04XoZZDllMw_z1eRj6D4qU7BB8Dr_YuyRmvbRjJYo36OCZEmBW/s1022/12.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="609" data-original-width="1022" height="191" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0P5jr5Kd6BcHxfDq0bd37u4NU7HNTJos_YLIN6S7sVXU3N1THA6wbCBYRWYeTij7y5N4pjNuaYqYyBcm2naSwSBM2UcqVAnVJ8oc5y5_dFF0Bn1SvWtyH1p7xbv_T85N4woeF0W04XoZZDllMw_z1eRj6D4qU7BB8Dr_YuyRmvbRjJYo36OCZEmBW/s320/12.png" width="320" /></a></div><br /><p class="MsoNormal">Т.е. либо стек УЖЕ был выровнен и тогда коррекция значения
ESP не требуется, и оно будет равно значению EBP. Либо второй вариант - была
произведена коррекция и значение ESP стало меньше EBP на какое-то значение
(обычно на 4).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Главное тут понимать - 64 битный стек начнется ПОСЛЕ ESP, не
важно, была подвижка его значения или нет. </p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Теперь следующий шаг - переключение в 64 битный режим:<o:p></o:p></p>
<pre class="brush:asm"> push $33 // пишем новый сегмент кода
db $E8, 0, 0, 0, 0 // call +5
add [esp], 5 // правим адрес возврата на идущий за retf
retf // дальний возврат со сменой сегмента кода на CS:0х33 + адрес
// начиная отсюда мы в 64 битном режиме!!!
</pre>
<p class="MsoNormal">Данный код рассмотрим более подробно. Ключевой функцией
здесь является RETF, именно она переключает сегментный селектор CS и передает
управление на указанный адрес, причем оба этих значения должны быть расположены
на стеке. И очень важный момент - она всегда работает с блоком строго 8 байт
(сегмент+адрес), не важно из какого режима происходит вызов, из 32 бит или из
64. </p>
<p class="MsoNormal">Выглядит это следующим образом, допустим ESP у нас
изначально равнялся 0x100 (для простоты)<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>push $33 - этим мы разместили 4 байта на стеке с новым
значением селектора сегмента, при этом верхушка стека уменьшилась на эти 4
байта (ESP = 0xFC)</li><li>call +5 - (вызов реализован в виде опкодов, т.к. дельфи
не позволяет написать такую инструкцию прямо в коде), данная инструкция
размещает на стеке 4 байта в качестве адреса возврата, т.е. куда должно
вернуться управление после завершения вызова, и переходит непосредственно на
этот адрес (т.е. сдвигает регистр EIP ровно на указанные 5 байт, которые равны
длине этой инструкции). Таким образом у нас на стеке лежат уже два числа,
первое с контекстом, второе содержит адрес инструкции add (стек теперь равен
ESP = 0xF8)</li><li>add [esp], 5 - увеличивает значение адреса возврата на
стеке, размещенное предыдущим вызовом на пять байт. Пять байт - это общая длина
инструкций add[] (4 байта) и retf (1 байт), таким образом адрес размещенный на
стеке изменился на адрес следующей инструкции после RETF (регистр ESP не
изменился и остался равен ESP = 0xF8)</li><li>retf - переключает селектор сегмента кода на значение
расположенное по адресу [ESP + 4] и передает управление на адрес на который
указывает [ESP], при этом увеличивает значение регистра ESP на использованные в
качестве параметров 8 байт (в итоге вершина стека стала равной изначальному
значению, ESP/RSP = 0x100)</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Как только выполнится RETF - начинается самое интересное.
Delphi не знает, что после RETF у нас выполняется 64 битный код и, если мы
попробуем поставить брякпойнт после инструкции RETF - ничего хорошего из этого
не получится, поэтому лучше даже не пытайтесь. Отладку 64 битного кода нужно
производить именно в 64 битах и только когда удостоверились что он написан
правильно, только тогда его можно переносить внутрь данной функции тщательно
контролируя по опкодам инструкций чтобы ничего не уплыло.<span style="mso-spacerun: yes;"> </span></p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Впрочем (для отладки) можно взять 64 битный WinDbg - он
умеет работать с кодом, который прыгает из 32 бит в 64 и обратно. </p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Теперь приступаем ко второму этапу, переводу параметров,
расположенных на 32 битном стеке в их 64 битный аналог и пишем уже 64 битный
код. </p>
<p class="MsoNormal">Для начала сформируем 64 битный стековый кадр:<o:p></o:p></p>
<pre class="brush:asm"> push ebp // push rbp
db $48 sub esp, $30 // sub rsp, $30
db $48 mov ebp, esp // mov rbp, rsp
</pre>
<p class="MsoNormal">Вот тут такой... тонкий момент. Обратите внимание что я пишу
вроде бы 32 битные инструкции, однако, так как в текущий момент этот код будет
исполнятся в 64 битном режиме, то и интерпретироваться эти инструкции будут
именно как 64 битные, в комментарии справа указано что именно будет выполнять
процессор в этот момент. Единственный момент, это наличие префикса перед второй
и третьей инструкцией ввиде db $48.<o:p></o:p></p>
<p class="MsoNormal">Это так называемый REX префикс. <o:p></o:p></p>
<p class="MsoNormal">Грубо говоря, каждая инструкция - это набор опкодов
выглядящих как префикс + опкод + ModRM + SIB ну и далее...<o:p></o:p></p>
<p class="MsoNormal">Вот поле ModRM у каждой инструкции (где оно присутствует)
используется для 32 битной адресации, а REX префикс позволяет эту адресацию
расширить. Если убрать REX префикс то в 64 битном коде эти инструкции
будут трактоваться абсолютно так-же как и в 32 битном, т.е. работа будет с
регистрами ESP + EBP, а не RSP + RBP. И хотя сейчас эти регистры содержат реально 32 битные
значения, лучше писать все-же правильно (во избежание).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Можно конечно было их написать прямо через опкоды (DB/DW/DD)
но такой вариант реализации мне показался более удобным. </p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Ну и если вернуться к стековому кадру - его наличие
обязательно, т.к. согласно спецификации, не смотря на то что часть из шести
параметров у вызываемой 64 битной NtQueryVirtualMemory пойдет через регистры,
мы обязаны выделить под них место на стеке (т.н. ShadowSpace), этим и
обусловлено наличие инструкции sub rsp, $30 (0х30 = 6 параметров * 8 байт). </p>
<p class="MsoNormal">Следующим шагом получим адрес последнего параметра,
расположенного на 32 битном стеке, а именно ReturnLength и тут нам потребуется
немного математики.<o:p></o:p></p>
<p class="MsoNormal">Смотрите на картинку выше, ReturnLength изначально
располагался по адресу EBP + 0x28.<br />Сам EBP равен значению ESP + коррекция в регистре EAX, т.е.
ReturnLength = ESP + EAX + 0x28;<br />Дальше, мы поместили на стек значение RBP изменив регистр
RSP (в который он преобразовался из ESP после переключения контекста) ровно на
8 байт, после чего зарезервировали место на 64 битном стеке еще под шесть
восьмибайтных параметров. Добавляем эти 8 байт + 6 * 8 (всего 0x38) к
константе, получается вот такой адрес - RSP + RAX + 0x60: </p>
<pre class="brush:asm"> db $48 lea eax, [esp + eax + $60] // lea rax, [rsp + rax + $60]
</pre>
<p class="MsoNormal">Получив правильный адрес параметров на 32 битном стеке,
переносим последние два (ReturnLength + MemoryInformationLength) на 64 битный:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:asm"> // 1. ReturnLength
mov ecx, [eax] // mov ecx, dword ptr [rax]
db $48 mov [esp + $28], ecx // mov [rsp + $28], rcx
// 2. и размер данных "MemoryInformationLength"
mov ecx, [eax - 4] // mov ecx, dword ptr [rax - 4]
db $48 mov [esp + $20], ecx // mov [rsp + $20], rcx
</pre>
<p class="MsoNormal">Смотрите, под 64 битные параметры зарезервировано 0х30 байт,
ReturnLength самый последний, значит он должен быть расположен в последних
восьми байтах, т.е. диапазон от 0x28 до 0x30.<o:p></o:p></p>
<p class="MsoNormal">Предпоследним на стек пойдет MemoryInformationLength со
смещением 0x20 от начала зарезервированного стека.<o:p></o:p></p>
<p class="MsoNormal">И на этом со стеком все - осталось перенести оставшиеся 4
параметра в соответствующие регистры: </p>
<pre class="brush:asm"> // регистр R9 содержит указатель на память (MemoryInformation),
// куда будет помещаться результат
db $44 mov ecx, [eax - 8] // mov r9d, dword ptr [rax - 8]
// регистр R8 содержит идентификатор MemoryInformationClass
db $44 mov eax, [eax - $C] // mov r8d, dword ptr [rax - $С]
// регистр RDX содержит BaseAddress
db $48 mov edx, [eax - $14] // mov rdx, [rax - $14]
// RCX должен содержать hProcess
mov ecx, [eax - $18] // mov ecx, dword ptr [rax - $18]
</pre>
<p class="MsoNormal">Обратите внимание что все параметры за исключением
BaseAddress имеют размер 32 бита поэтому читаются именно как dword ptr[], при
этом когда идет запись в регистр ECX (являющийся младшей частью RCX) старшая
часть регистра RCX очищается. В этом большое отличие от работы с младшими
частями регистра ECX, где при изменении CX/CL/CH старшая часть остается на
месте.<span style="mso-spacerun: yes;"> </span><o:p></o:p></p>
<p class="MsoNormal">На текущий момент можно сказать, что выполнены все те-же
самые подготовительные действия, которые выполняется в функции
Wow64SystemServiceEx, поэтому раз все готово, настал момент вызова 64 битной
NtQueryVirtualMemory:<o:p></o:p></p>
<pre class="brush:asm"> call [eax - $20] // call [rax - $20]
</pre>
<p class="MsoNormal">Всё<span lang="EN-US" style="mso-ansi-language: EN-US;">! </span>Функция
выполнилась, результат выполнения будет размещен в регистре RAX, который после
переключения в 32 битный режим преобразуется в EAX, таким образом будет
содержать в себе результат выполнения 64 битной функции и нам не нужно делать
лишних телодвижений. <o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p>Для возврата результата функции здесь в принципе будет
достаточно того, что вернется в регистре RAX, но чтоб быть последовательным все
же желательно выполнить правильное преобразование в регистровую пару, которую
будет требовать 32 битный код в случае, если будет возвращаться восьмибайтное
значение:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:asm"> db $48 mov edx, eax // mov rdx, rax
db $48, $C1, $EA, $20 // shr rdx, $20
</pre>
<p class="MsoNormal">Младшая часть регистра RAX при переходе в 32 битный режим
преобразуется в значение регистра EAX, а старшая часть, после этих двух строк
будет перемещена в регистр EDX откуда его заберет вызываемый код (если это
потребуется). Правда Delphi не позволяет записать инструкцию SHR в нормальном виде,
поэтому она объявлена как набор опкодов.<o:p></o:p></p>
<p class="MsoNormal">Осталось только подчистить за собой:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:asm"> db $48 lea esp, [ebp + $30] // lea rsp, [rbp + $30]
pop ebp
</pre>
<p class="MsoNormal">Этими двумя строчками схлопывается 64 битный стековый кадр,
он больше не нужен и можно переключаться обратно в 32 бита:<span style="mso-spacerun: yes;"> </span><o:p></o:p></p>
<pre class="brush:asm"> db $E8, 0, 0, 0, 0 // call +5
mov [esp + 4], $23 // mov dword ptr [rsp + 4], $23
add [esp], $0D // add dword ptr [rsp], $0D
retf // дальний возврат со сменой сегмента кода на CS:0х23 + адрес
// начиная отсюда мы опять в 32 битном режиме
</pre>
<p class="MsoNormal">Выход выглядит практически идентично коду входа, за исключением
одного момента. Т.к. вызов CALL происходит в 64 битном режиме, на стек сразу
помещаются 8 байт и дополнительного PUSH уже не требуется, достаточно будет
поправить только текущие значение, а именно [ESP + 4] теперь должен быть равен
0x23 а адрес возврата [ESP] нужно увеличить на 13 байт (длинна трех инструкций
mov + add + retf)<o:p></o:p></p>
<p class="MsoNormal">Не стоит боятся того, что после вызова CALL на стек
изначально был размещен 8 битный указатель, т.к. он расположен в 32 битном
участке кода старшие 32 бита (в которых расположен селектор) будут
гарантировано равны нулю и их можно смело использовать.<o:p></o:p></p>
<p class="MsoNormal">Теперь надо убрать возможные последствия коррекции верхушки
стека, выставив регистру ESP значение равное EBP:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:asm"> mov esp, ebp
</pre>
<p class="MsoNormal">Это всё - дальше отработает эпилог функции, который
подчистит все что осталось и вернет управление вызывающему коду. </p>
<p class="MsoNormal">Теперь можно увидеть, как <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%203/RawScanner.Utils.pas#L44">выглядит
функция целиком</a>. Я специально её оставил в таком виде чтобы можно было её
наглядно "пощупать" в отладчике.<o:p></o:p></p>
<p class="MsoNormal">В следующих главах вместо данной функции будет
использоваться уже функция для <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%204/RawScanner.X64Gates.pas">автогенерации
гейта</a>, которая будет формировать динамически примерно такой-же код, как
описано выше, только немного оптимизированный (с использованием JMP FAR и
прямого CALL) и автоматически производить конвертацию 32 битных параметров в
зависимости от их количества (т.е. её можно применять для генерации переходного
гейта при вызове любой 64 битной функции с соглашением STDCALL, главное
правильно указать количество и размеры её параметров и адрес). </p>
<p class="MsoNormal">Все что было описано выше имеет даже название (не я
придумал) - Heavens Gate :)<o:p></o:p></p>
<p class="MsoNormal">Почему именно так, кто придумал и главное зачем - вопрос не
ко мне. </p>
<p class="MsoNormal">Впрочем, это был только первый шаг, теперь настало время
применить эту функцию к коду из предыдущей главы чтобы убрать
"костыль" и реализовать правильное определение пути загруженной
библиотеки по её инстансу.<o:p></o:p></p>
<p class="MsoNormal">Один из нюансов работы с таким прямым вызовом 64 битной
функции - это то, что адрес буфера, в который функция пишет MemoryInformation,
всегда должен быть выровнен по границе 8 байт, поэтому чтобы не забывать о
таком нюансе проще сразу написать универсальную обертку. Выглядеть<span style="mso-ansi-language: EN-US;"> </span>она<span style="mso-ansi-language: EN-US;">
</span>будет<span style="mso-ansi-language: EN-US;"> </span>вот<span style="mso-ansi-language: EN-US;"> </span>так<span lang="EN-US" style="mso-ansi-language: EN-US;">:<o:p></o:p></span></p>
<pre class="brush:delphi">function GetMappedFileName64(hProcess: THandle; lpv: ULONG_PTR64;
lpFilename: LPCWSTR; nSize: DWORD): DWORD;
...
begin
{$IFDEF WIN32}
Result := 0;
if NtQueryVirtualMemoryAddr <> 0 then
begin
// структура TMappedFileName должна быть выровнена по 8-байтной границе
// поэтому стек не используем, а выделяем принудительно
MappedFileName := VirtualAlloc(nil,
SizeOf(TMappedFileName), MEM_COMMIT, PAGE_READWRITE);
try
Status := NtQueryVirtualMemory64(NtQueryVirtualMemoryAddr, hProcess, lpv,
MemoryMappedFilenameInformation, MappedFileName,
SizeOf(TMappedFileName), @ReturnLength);
if not NT_SUCCESS(Status) then
begin
BaseSetLastNTError(Status);
Exit(0);
end;
nSize := nSize shl 1;
cb := MappedFileName^.ObjectNameInfo.MaximumLength;
if nSize < cb then
cb := nSize;
Move(MappedFileName^.FileName[0], lpFilename^, cb);
if cb = MappedFileName^.ObjectNameInfo.MaximumLength then
Dec(cb, SizeOf(WChar));
Result := cb shr 1;
finally
VirtualFree(MappedFileName, SizeOf(TMappedFileName), MEM_RELEASE);
end;
end
else
{$ENDIF}
Result := GetMappedFileName(hProcess, Pointer(lpv), lpFilename, nSize);
end;
</pre>
<p class="MsoNormal">Этот код практически один в один повторяет реализацию
GetMappedFileName, только использует вызов NtQueryVirtualMemory64 для 32 бит, в
случае если снаружи был назначен её адрес. </p>
<p class="MsoNormal">Теперь можно подправить код в функции <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%203/RawScanner.LoaderData.pas#L159">TLoaderData.Scan32LdrData</a>
удалив оттуда код "костыля" и заменив его на вот такой вызов:<o:p></o:p></p>
<pre class="brush:delphi"> if FUse64Addr then
begin
MapedFilePathLen := GetMappedFileName64(FProcess, Module.ImageBase,
@MapedFilePath[1], MAX_PATH * SizeOf(Char));
if MapedFilePathLen > 0 then
Module.ImagePath := NormalizePath(Copy(MapedFilePath, 1, MapedFilePathLen));
end;
</pre>
<p class="MsoNormal">Последним шагом осталось инициализировать переменную,
содержащую адрес 64 битной NtQueryVirtualMemory, это делается вот таким кодом,
который должен быть вызван перед работой со списками загрузчика из второй главы:<o:p></o:p></p>
<pre class="brush:delphi"> if IsWow64Mode then
begin
LocalLoader := TLoaderData.Create(hProcess, True);
try
if LocalLoader.Load64LoaderData(PEB64.LoaderData) > 0 then
for var Module in LocalLoader.Modules do
begin
if ExtractFileName(Module.ImagePath).ToLower = 'ntdll.dll' then
begin
Wow64Support.DisableRedirection;
try
NtDll := TRawPEImage.Create(Module.ImagePath, Module.ImageBase);
try
Index := NtDll.ExportIndex('NtQueryVirtualMemory');
if Index >= 0 then
SetNtQueryVirtualMemoryAddr(NtDll.ExportList.List[Index].FuncAddrVA);
finally
NtDll.Free;
end;
finally
Wow64Support.EnableRedirection;
end;
Break;
end;
end;
finally
LocalLoader.Free;
end;
end;
</pre>
<p class="MsoNormal">Задача данного кода:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>через 64 битный список загрузчика определить инстанс
NTDLL</li><li>загрузить её и получить адрес NtQueryVirtualMemory</li><li>назначить этот адрес служебной переменной</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Шаги достаточно банальные, за исключением одного
единственного нюанса, а именно - перед загрузкой NTDLL (конкретно перед вызовом
TRawPEImage.Create) нужно ОБЯЗАТЕЛЬНО отключить редирект, в противном случае
будет подгружена 32 битная ntdll из SysWow64 и адрес 64 битной функции будет
определен не верно, в результате чего вызов NtQueryVirtualMemory64 будет всегда
завершаться с AccessViolation внутри NTDLL.</p>
<p class="MsoNormal">Код к третьей главе для самостоятельного изучения <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%203" target="_blank">доступен по этой ссылке</a>.</p><p class="MsoNormal"><br /></p>
<p></p>
<a name="forvard"></a>
<h3 style="text-align: left;">4. Обработка Forward деклараций и анализ таблиц экспорта</h3><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<p class="MsoNormal">Перед тем как приступить к следующему шагу нужно выполнить
некоторые подготовительные действия, чтобы потом на них не отвлекаться.<o:p></o:p></p>
<p class="MsoNormal">Первым делом, весь код из test.dpr предыдущей главы, уходит
в новый класс <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%204/RawScanner.Core.pas#L38">TRawScanner</a>,
задача которого выполнить подготовительные действия (создать нужные классы и
выполнить инициализацию адреса 64 битной NtQueryVirtualMemory). Этот класс
будет входной точкой, через которую будет вестись вся остальная работа с кодом
(для удобства).<o:p></o:p></p>
<p class="MsoNormal">А также нужно сделать класс контейнер <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%204/RawScanner.ModulesData.pas#L110">TRawModules</a>,
который будет хранить в себе список TRawPEImage и предоставлять методы для
работы с ними.<o:p></o:p></p>
<p class="MsoNormal">Шлюз для вызова 64 битной NtQueryVirtualMemory заменен на <a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%204/RawScanner.X64Gates.pas">универсальный
гейт</a> его код я рассматривать не буду, он просто более оптимизирован по
сравнению с кодом шлюза из предыдущей главы и выполняет всю рутинную работу по
подготовке среды окружения и конвертации параметров автоматически.<o:p></o:p></p>
<p class="MsoNormal">У центрального TRawScanner будет, помимо конструктора и деструктора,
всего один основной метод InitFromProcess(PID), а также несколько свойств.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>свойство Modules - которое будет представлять из себя
класс TRawModules</li><li>свойство Analizer - новый класс анализатора, который
будет реализован в этой главе.</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Ну а TRawModules предоставляет методы для добавления новой
библиотеки в общий список (функция AddImage), а также методы быстрого поиска
библиотеки по её hInstance (функция GetModule), и быстрого получения информации
по функции экспортируемой библиотекой (функция GetProcData):</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi"> TRawModules = class
private
FItems: TObjectList<trawpeimage>;
FIndex: TDictionary<string integer="">;
FImageBaseIndex: TDictionary<ulong_ptr64 integer="">;
...
public
function AddImage(const AModule: TModuleData): Integer;
procedure Clear;
function GetModule(AddrVa: ULONG_PTR64): Integer;
function GetProcData(const LibraryName, FuncName: string; Is64: Boolean;
var ProcData: TExportChunk; CheckAddrVA: ULONG_PTR64): Boolean; overload;
function GetProcData(const LibraryName: string; Ordinal: Word;
Is64: Boolean; var ProcData: TExportChunk; CheckAddrVA: ULONG_PTR64): Boolean; overload;
function GetProcData(const ForvardedFuncName: string; Is64: Boolean;
var ProcData: TExportChunk; CheckAddrVA: ULONG_PTR64): Boolean; overload;
property Items: TObjectList<trawpeimage> read FItems;
end;
</trawpeimage></ulong_ptr64></string></trawpeimage></pre>
<p class="MsoNormal">Для реализации быстрой работы функций GetModule и
GetProcData используется два словаря:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>FIndex - словарь с ключом по имени библиотеки</li><li>FImageBaseIndex - словарь с ключом по hInstance
библиотеки</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Теперь вкратце что будет сделано в этой главе, общая задача
выглядит так - имея на руках список загруженных модулей (как исполняемого
файла, так и используемых им библиотек) произвести анализ всех доступных таблиц
экспорта на предмет их соответствия рассчитанным значениям и вывести в лог
измененные. Чтобы подстраховать самих себя потребуется небольшая функция,
которая будет показывать - действительно ли были изменения по указанному адресу
в удаленном адресном пространстве, или код анализатора ошибся. </p>
<p class="MsoNormal">Делается это при помощи API <a href="https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-queryworkingset">QueryWorkingSet</a>.
Суть метода заключается в следующем, когда выделяется память, каждая страница
памяти подгружается в общий пул, так называемый WorkingSet представляющий из
себя список виртуальных страниц которые отображены на адресное пространство
процесса (утрированно, находящихся в физической памяти, и хотя это не совсем
так, но близко), где помимо адреса каждой страницы этот пул содержит информацию
о текущих атрибутах защиты страницы (Protection), флаг - является ли страница
доступной для совместного использования (Shared), а также количество процессов,
совместно использующих данную страницу в текущий момент времени (SharedCount). </p>
<p class="MsoNormal">Когда процесс загружает библиотеку, для большинства из них
(т.н. knowndlls) этап чтения образа библиотеки с диска пропускается и
библиотека мапится в адресное пространство процесса как сегмент, ну это что-то
типа кэширования для ускорения работы. И все страницы памяти, выделенные под
библиотеку, помечаются как общедоступные, т.е. одну и туже страницу (но только
в режиме чтения) может использовать несколько процессов одновременно.<o:p></o:p></p>
<p class="MsoNormal">В случае если грузится обычная библиотека (ну, например,
разработанная вами самостоятельно) в этом случае уже идет её реальное чтение с
диска, НО даже после этого, все её страницы помечаются как общедоступные. </p>
<p class="MsoNormal">Так вот, как только будет произведена попытка записи в любую
из общедоступных страниц, будет создана копия этой страницы и все изменения
будут отображены только в данной копии, а сама страница конечно будет
отсоединена от механизма совместного использования, со сбросом флагов Shared и
SharedCount. Вот именно это и будет определяться в качестве подстраховки.<o:p></o:p></p>
<p class="MsoNormal">Вообще (в качестве справочной информации) QueryWorkingSet
достаточно часто используется в антиотладке, позволяя помимо определения
изменений страниц памяти детектировать работу сканеров памяти, которые не
производят изменений, а просто ищут что-то в нашем процессе.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Делается такой детектор в два этапа:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>Выделяется контрольная страница через VirtualAlloc и её
адрес где-либо сохраняется.</li><li>Далее производится сброс ворксета вызовом
SetProcessWorkingSetSize с указанием вторым и третьим параметров значения
"SIZE_T(-1)"</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">После этого контрольная страница будет выгружена из рабочего
набора и появится в нем только тогда, когда кто-то попробует её прочитать. Так как сами мы читать её не собирались, её появление в
ворксете после очередной проверки (допустим по таймеру) может означать только
одно - нас сканируют снаружи! </p>
<p class="MsoNormal">Теперь к реализации - эта функция работает через представленную
в предыдущей главе NtQueryVirtualMemory64 передавая ей в качестве флага
MemoryWorkingSetList равный единице. Если вызовать QueryWorkingSet из 32
битного кода, то результатом будет информация только по страницам памяти
доступным из 32 бит (до лимита в MM_HIGHEST_USER_ADDRESS), а так как анализатор
интересует полный диапазон страниц, то вызывать нужно именно её 64 битный
аналог (точнее 64 битную NtQueryVirtualMemory).<o:p></o:p></p>
<p class="MsoNormal">Нюанс с вызовом NtQueryVirtualMemory64 в том что т.к. это 64
битная функция, то и все адреса, которые она принимает на вход, должны
соответствовать значениям, которые может принять 64 битный указатель, т.е. все
эти адреса должны быть выравнены по границе 8 байт, в противном случае
NtQueryVirtualMemory64 вернет STATUS_DATATYPE_MISALIGNMENT. Чтобы не забыть про
этот нюанс и не контролировать каждый вызов используется промежуточная обертка:<o:p></o:p></p>
<pre class="brush:delphi">{$IFDEF WIN32}
function InternalNtQueryVirtualMemory64(FuncRVA: ULONG_PTR64; hProcess: THandle;
BaseAddress: ULONG_PTR64; MemoryInformationClass: DWORD;
MemoryInformation: Pointer; MemoryInformationLength: DWORD;
ReturnLength: PULONG64): NTSTATUS;
var
AlignedBuff: Pointer;
begin
AlignedBuff := VirtualAlloc(nil,
MemoryInformationLength, MEM_COMMIT, PAGE_READWRITE);
try
Result := NtQueryVirtualMemory64(FuncRVA, hProcess, BaseAddress,
MemoryInformationClass, AlignedBuff, MemoryInformationLength,
ReturnLength);
Move(AlignedBuff^, MemoryInformation^, MemoryInformationLength);
finally
VirtualFree(AlignedBuff, MemoryInformationLength, MEM_RELEASE);
end;
end;
{$ENDIF}
</pre>
<p class="MsoNormal">Внутри данной обертки нет проверки результата и AlignedBuff
всегда копируется в результирующий буфер MemoryInformation. <o:p></o:p></p>
<p class="MsoNormal">Дело в том, что штатными кодами ошибок для этой функции,
помимо STATUS_SUCCESS является допустим и STATUS_INFO_LENGTH_MISMATCH, который
означает что не хватает размера выделенного буфера, при этом требуемый размер
будет размещен в AlignedBuff и его нужно вернуть вызывающему коду. </p>
<p class="MsoNormal">Сама же реализация функции выглядит вот так:</p>
<pre class="brush:delphi"> function QueryWorkingSet64(hProcess: THandle; pv: Pointer; cb: DWORD): Boolean;
{$IFDEF WIN32}
const
MemoryWorkingSetList = 1;
var
Status: NTSTATUS;
{$ENDIF}
begin
{$IFDEF WIN32}
// если мы в чистой 32 битной ОС то просто производим 32 битный вызов
// с перекидыванием результата в массив с 64 битными адресами
if not Wow64Support.Use64AddrMode then
begin
Result := QueryWorkingSet32(hProcess, pv, cb);
Exit;
end;
// в противном случае нам нужен полный WorkSet с 64 битными страницами
if Assigned(NtQueryVirtualMemory64) then
begin
Status := InternalNtQueryVirtualMemory64(
hProcess, 0, MemoryWorkingSetList, pv, cb, nil);
if NT_SUCCESS(Status) then
Exit(True);
end;
Result := False;
{$ELSE}
Result := QueryWorkingSet(hProcess, pv, cb);
{$ENDIF}
end;
</pre>
<p class="MsoNormal">В ней вначале идет проверка, если мы находимся под чистой 32
битной ОС, то происходит вызов функции QueryWorkingSet32, которая делает вызов
нативной QueryWorkingSet с преобразованием результата вызова в массив 64 битных
указателей.<o:p></o:p></p>
<p class="MsoNormal">В противном случае вызываем NtQueryVirtualMemory64 с флагом
MemoryWorkingSetList. </p>
<p class="MsoNormal">С подготовительными действиями закончили, пришло время
писать код анализатора. В новом модуле RawScanner.Analyzer пишем класс:<o:p></o:p></p>
<pre class="brush:delphi"> TPatchAnalyzer = class
private
...
function CheckPageSharing(AddrVa: ULONG_PTR64;
out SharedCount: Byte): Boolean;
protected
procedure DoModifyed(HookData: THookData);
procedure InitWorkingSet;
procedure ScanExport(Index: Integer; Module: TRawPEImage);
procedure ScanModule(Index: Integer);
public
constructor Create(AProcessHandle: THandle; ARawModules: TRawModules);
destructor Destroy; override;
function Analyze(
AProcessTableHook: TProcessTableHookCallBack;
AProcessCodeHook: TProcessCodeHookCallBack): TAnalizeResult;
end;
</pre>
<p class="MsoNormal">Полностью его рассматривать я не буду, опишу только общий
принцип и узкие моменты.<o:p></o:p></p>
<p class="MsoNormal">В конструктор класса приходит хэндл открытого процесса и
список модулей для анализа (подготовленный классом TRawScanner). Хэндл
потребуется для работы QueryWorkingSet64, ну а список - именно по нему и будет
идти анализ. </p>
<p class="MsoNormal">Единственным методом доступным снаружи является функция
Analyze, которая параметрами принимает два калбэка реализованных во внешне
коде, и именно они будут вызываться в том случае, если анализатор обнаружил какие-то
нарушения в памяти удаленного процесса. Задача данного метода инициализировать
WorkingSet процесса вызовом процедуры InitWorkingSet после чего в цикле вызвать
для каждого модуля из списка TRawModules процедуру ScanModule.<o:p></o:p></p>
<p class="MsoNormal">ScanModule в свою очередь загружает в память образ
сканируемого модуля (в TMemoryStream) и вызывает ScanExport.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">И вот ScanExport нужно рассмотреть более подробно. Задача
данной процедуры состоит в контроле целостности таблицы экспорта в удаленном
адресном пространстве, сравнением значений, записанных в ней с заранее
рассчитанными.<o:p></o:p></p>
<p class="MsoNormal">Если немножко подсократить код функции, то основная её часть
выглядит вот так:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi">procedure TPatchAnalyzer.ScanExport(Index: Integer; Module: TRawPEImage);
begin
ExportDirectory := TRemoteStream.Create(FProcessHandle,
Module.ExportDirectory.VirtualAddress, Module.ExportDirectory.Size);
try
ZeroMemory(@HookData, SizeOf(THookData));
... инициализация структуры
for Exp in Module.ExportList do
begin
...
HookData.RawVA := Exp.FuncAddrVA;
...
if not ExportDirectory.ReadMemory(Exp.ExportTableVA, 4,
@HookData.ExportAdv.ExpRemoteRva) then
Continue;
HookData.RemoteVA :=
HookData.ExportAdv.ExpRemoteRva + Module.ImageBase;
if HookData.RemoteVA <> HookData.RawVA then
begin
if Exp.ForvardedTo <> EmptyStr then
if not FRawModules.GetProcData(Exp.ForvardedTo,
Module.Image64, ForvardedExp, HookData.RemoteVA) then
begin
HookData.Calculated := False;
DoModifyed(HookData);
Continue;
end
else
HookData.RawVA := ForvardedExp.FuncAddrVA;
if HookData.RemoteVA <> HookData.RawVA then
begin
DoModifyed(HookData);
Continue;
end;
end;
end;
finally
ExportDirectory.Free;
end;
end;
</pre>
<p class="MsoNormal">Первым идет вызов TRemoteStream.Create - это простенький
класс представляющий из себя обертку над TMemoryStream и выступающий в качестве
кэша, т.к. в процессе анализа будет много чтений памяти удаленного процесса,
чтобы не вызывать на каждый чих ReadRemoteMemory вся память, из которой
теоретически будет происходить чтение, сразу копируется в этот кэш за один
присест.<o:p></o:p></p>
<p class="MsoNormal">Следующим шагом идет основной цикл, в котором последовательно
выбирается каждая запись из таблицы экспорта переданного на вход модуля
Module.ExportList, при этом для каждой записи заполняется структура, которая в
случае обнаружения несовпадений будет отдаваться наружу в калбэк, для
последующей обработки внешним кодом.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Для каждой записи читается RVA адрес экспортируемой функции
через (рассчитанный еще в TRawPEImage адрес в таблице экспорта)
Exp.ExportTableVA, после чего прочитанный RVA адрес переводится в VA сложением
с базой текущего модуля ImageBase (ну т.е. с его hInstance).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">И делается проверка - если полученное значение не равно
тому, которое рассчитал наш код еще в классе TRawPEImage (вторая глава) то это
может означать три варианта:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>код расчета "правильного" значения ошибочен</li><li>код в таблице экспорта действительно пропатчен и ведет на
установленный извне перехватчик.</li><li>экспортируемая функция перенаправлена, и её реализация
находится в другом модуле</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Первый вариант рассматривать не буду - код правильный (но
это не точно).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Второй вариант вполне допустим, но как правило первое
сравнение не успешно по причине форварда, т.е. перенаправления адреса функции
на совершенно другой модуль. Поэтому следующим шагом происходит поиск адреса
перенаправленной функции и, если получилось его найти - повторное сравнение.<o:p></o:p></p>
<p class="MsoNormal">Ну а если и в этом случае адреса не совпали, тогда уже
вызывается внешний обработчик через DoModifyed. </p>
<p class="MsoNormal">Вот именно в процедуре DoModifyed происходит проверка
шаринга страницы через вызов функции CheckPageSharing и заполнение результата в
структуре, отдаваемой в калбэк. В ней есть нюанс, поэтому рассмотрим её код
поподробней: </p>
<pre class="brush:delphi">function TPatchAnalyzer.CheckPageSharing(AddrVa: ULONG_PTR64;
out SharedCount: Byte): Boolean;
begin
Result := FWorkingSet.TryGetValue(AddrVA and PageMask, SharedCount);
if not Result then
if ReadRemoteMemory(FProcessHandle, AddrVa, @Tmp, 1) then
begin
InitWorkingSet;
Result := FWorkingSet.TryGetValue(AddrVA and PageMask, SharedCount);
end;
end;
</pre>
<p class="MsoNormal">Здесь происходит следующее, сначала происходит попытка найти
информацию о странице, к которой принадлежит адрес, в текущем пуле страниц
ворксета, при этом (т.к. грануляция страниц 4096 байта), применяется маска,
отсекающая младшую часть адреса.<o:p></o:p></p>
<p class="MsoNormal">А вот если страница не нашлась, то происходит попытка её
подгрузки в ворксет чтением одного байта по указанному адресу (чуть выше я
рассказывал про детект сканера памяти, вот тут тоже самое) в результате чего
(если получилось прочитать) повторно перестраиваем ворксет и еще раз пытаемся
получить информацию по странице. </p>
<p class="MsoNormal">Так вот, как только нашлось не совпадение, вызывается
калбэк, назначенный во внешнем коде, в который передается информация в виде
структуры с параметрами, описывающими - что именно наш анализатор нашел и что
ему не нравится. Реализацию калбэка я рассматривать не буду, он достаточно
тривиальный и основная его задача - это вывод результата в форматированном
виде. Код колбэка и вспомогательных функций находится в модуле "<a href="https://github.com/AlexanderBagel/articles/blob/main/raw_scanner/part%204/display_utils.pas#L125">display_utils.pas</a>".<o:p></o:p></p>
<p class="MsoNormal">Давайте посмотрим, как это все будет работать. В коде
демопримера я специально ввел дефайн, чтобы вы смогли посмотреть, как будет
работать код на текущий момент времени, при еще не реализованной обработке
форвард деклараций функций.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Для этого раскоментируйте директиву
DISABLE_FORWARD_PROCESSING в инклуде "defines.inc".<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGJlQuZIvHu2ly6tImrMg7mVr_fZSqkPBhFBBv708doYZwIqWGTaGm4wckrs7p3rXKWDtSvc-tmHz8YLbFR6fHJTorAD_DERtxJB6jbiH2saWu7TzZvcJXABh9sdiIdgbnElEPoC_9YNm6B2_4g7sY1o0cMa5jJFPKnpgPaQ1zQBZ7PZYMxT9vEKip/s1022/13.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="958" data-original-width="1022" height="300" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGJlQuZIvHu2ly6tImrMg7mVr_fZSqkPBhFBBv708doYZwIqWGTaGm4wckrs7p3rXKWDtSvc-tmHz8YLbFR6fHJTorAD_DERtxJB6jbiH2saWu7TzZvcJXABh9sdiIdgbnElEPoC_9YNm6B2_4g7sY1o0cMa5jJFPKnpgPaQ1zQBZ7PZYMxT9vEKip/s320/13.png" width="320" /></a></div><br /><p class="MsoNormal">Вот они все, четыре функции из user32.dll (скриншот снят на
Windows 11, на других ОС список функций может быть другим).</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Анализатор показал, что адреса всех четырех функций ведут
вместо библиотеки user32 (которая их экспортирует) куда-то внутрь ntdll.dll </p>
<p class="MsoNormal">Чтобы было понятней что именно выводит анализатор, то
разъясню:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>сначала пишется тип перехвата (Export/Import/Delay
Import), далее имя библиотеки и функции</li><li>далее идет контрольный статус, получаемый через контроль
шаринга страницы. Status: PATCHED означает что страница была модифицирована.</li><li>Expected: HEX_VALUE - ожидаемый адрес экспортируемой
функции</li><li>present: HEX_VALUE - текущий адрес экспортируемой функции
(если получилось определить - пишется имя модуля, которому принадлежит этот
адрес)</li><li>в заголовке таблицы поле Raw (0xHEX_VALUE) - смещение от
начала файла, где содержится RVA адрес экспортируемой функции</li><li>в последней строке VA адрес в таблице экспорта с записью
об функции, её значение в оригинальном файле и значение в удаленном адресном
пространстве </li></ol><p></p>
<p class="MsoNormal">Можно посмотреть как выглядит таблица экспорта user32.dll
через сторонний инструмент и там увидеть вот такую картинку:<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKTuRJ4jgqmPO1nSsChdyp7TqUk6HDaAzl1s0a0NrP_NDM3Xm9LphRlYdht3Ee0Il_5XbeF9HlFWq0sgUEyel8IReDagO5TnhzwwzvrDZwcmTvEInYHrAhf_y99x68h5Bj8k852uQfxRUz1Z5Zr2kUPS_2QWML4DBuGNC3FeUwHQybA7Tm3DhVV-pt/s1109/14.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="332" data-original-width="1109" height="96" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKTuRJ4jgqmPO1nSsChdyp7TqUk6HDaAzl1s0a0NrP_NDM3Xm9LphRlYdht3Ee0Il_5XbeF9HlFWq0sgUEyel8IReDagO5TnhzwwzvrDZwcmTvEInYHrAhf_y99x68h5Bj8k852uQfxRUz1Z5Zr2kUPS_2QWML4DBuGNC3FeUwHQybA7Tm3DhVV-pt/s320/14.png" width="320" /></a></div><br /><p class="MsoNormal">Да, каждая из показанных четырех функций действительно
перенаправлена, рассмотрим вот эту строчку вывода анализатора поподробней:</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<pre class="brush:delphi">Export modified user32.dll -> DefDlgProcA. Status: PATCHED!
Expected: 0000000076F2A133, present: 0000000077B884A0 --> ntdll.dll
Addr: |Raw (0xA61C4): |Remote:
----------------------------------------------------------------------------------------------------------------------------
0000000076F26DC4|33 A1 0A 00 |A0 84 D0 00
</pre>
<p class="MsoNormal"><span lang="EN-US" style="mso-ansi-language: EN-US;"><o:p> </o:p></span></p>
<p class="MsoNormal">и будем сверять её с текущей записью в таблице экспорта:<o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<pre class="brush:delphi">76F26DC4: A0 84 D0 00 EAT FuncAddr [1669] user32.dll!DefDlgProcA -> NTDLL.NtdllDialogWndProc_A = [77B884A0]
</pre>
<p class="MsoNormal"><span lang="EN-US" style="mso-ansi-language: EN-US;"><o:p> </o:p></span></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>адрес 76F26DC4 - анализатор правильно определил адрес
записи в таблице экспорта и показал, что текущее значение "A0 84 D0
00" не соответствует рассчитанному.</li><li>"present: 0000000077B884A0 --> ntdll.dll"
анализатор правильно определил текущий адрес функции, и он соответствует
"NTDLL.NtdllDialogWndProc_A = [77B884A0]" на которую произошло
перенаправление.</li><li>"Expected: 0000000076F2A133" а давайте посмотрим,
что хранится по адресу, который ожидал анализатор.</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">А там...</p><p class="MsoNormal"><o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_QDvkAB9EWCZrxydBVEsrzwiiZmyG1IbDxcfan-poHzlGEDW74YzcziI7Naw4ncftxGGiL19GKy3V2KMtQ2JrBNaZeNdO1OpAxtaV3JofGEggx-Bq7AfEfMwpLr5NSzsiwy1AaQi_eALBPv9G0v6Hiijt7WCd0SL3Z6OL5pZbl-G6XiH0KBrCBMCX/s786/15.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="512" data-original-width="786" height="208" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_QDvkAB9EWCZrxydBVEsrzwiiZmyG1IbDxcfan-poHzlGEDW74YzcziI7Naw4ncftxGGiL19GKy3V2KMtQ2JrBNaZeNdO1OpAxtaV3JofGEggx-Bq7AfEfMwpLr5NSzsiwy1AaQi_eALBPv9G0v6Hiijt7WCd0SL3Z6OL5pZbl-G6XiH0KBrCBMCX/s320/15.png" width="320" /></a></div><br /><p class="MsoNormal">А там строка "NTDLL.NtdllDialogWndProc_A"!!!
Причем адрес этой строки находится в директории экспорта.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Собственно - это все что нужно знать про перенаправление,
серьезно :)<o:p></o:p></p>
<p class="MsoNormal">Ну а если без шуток, то действительно у большинства функций
RVA адрес в таблице экспорта ведет куда-то внутрь секции кода этого же модуля,
и только у перенаправленных функций он ведет на строку в директории экспорта,
т.е. по факту такую проверку форварда можно сделать прямо в TRawPEImage:<o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.IsExportForvarded(RvaAddr: DWORD): Boolean;
begin
Result := DirectoryIndexFromRva(RvaAddr) = IMAGE_DIRECTORY_ENTRY_EXPORT;
end;
</pre>
<p class="MsoNormal">И этого будет вполне достаточно. <o:p></o:p></p>
<p class="MsoNormal">Вообще, конечно, с форвардом это они удачно придумали, можно
спокойно переносить ранее реализованные куски код тасуя их между модулями
причем без потери совместимости, только нужно учитывать, что функция, на
которую произошло перенаправление, сама может быть перенаправлена!<o:p></o:p></p>
<p class="MsoNormal">Пример такой функции (нашлось в Win11):
USP10.ScriptGetLogicalWidths -> GDI32.ScriptGetLogicalWidths ->
gdi32full.ScriptGetLogicalWidths<o:p></o:p></p>
<p class="MsoNormal">И при расчете адреса экспортируемой функции (точнее
значения, которое должно быть записано в таблице экспорта) форвард нужно
учитывать обязательно. </p>
<p class="MsoNormal">Осталось только реализовать обработку, это делается
буквально добавлением нескольких строк кода в класс TRawPEImage (ориентируйтесь
на директиву DISABLE_FORWARD_PROCESSING). </p>
<p class="MsoNormal">Выглядят<span style="mso-ansi-language: EN-US;"> </span>они<span style="mso-ansi-language: EN-US;"> </span>вот<span style="mso-ansi-language: EN-US;">
</span>так<span lang="EN-US" style="mso-ansi-language: EN-US;">:<o:p></o:p></span></p>
<pre class="brush:delphi"> {$IFNDEF DISABLE_FORWARD_PROCESSING}
if IsExportForvarded(FunctionsAddr[ExportChunk.Ordinal]) then
begin
Raw.Position := ExportChunk.FuncAddrRaw;
if Raw.Position = 0 then Continue;
ExportChunk.OriginalForvardedTo := ReadString(Raw);
ProcessApiSetRedirect(FOriginalName, ExportChunk);
end
else
begin
ExportChunk.OriginalForvardedTo := EmptyStr;
ExportChunk.ForvardedTo := EmptyStr;
end;
{$ENDIF}
</pre>
<p class="MsoNormal">Т.е. проверяем через показанную выше IsExportForvarded,
является ли запись об экспорте перенаправленной, и если да - читаем строку
форварда.<o:p></o:p></p>
<p class="MsoNormal">Этот же код нужно продублировать чуть ниже, где
обрабатываются функции, экспортирующиеся по ординару. </p>
<p class="MsoNormal">Код к четвертой главе для самостоятельного изучения <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%204" target="_blank">доступен по этой ссылке</a>.</p>
<p class="MsoNormal">Пора переходить к таблице импорта, где помимо форварда
деклараций функций появляются редиректы...<o:p></o:p></p><br /><p></p>
<p></p>
<a name="import"></a>
<h3 style="text-align: left;">5. Таблица импорта</h3><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p> </o:p></p><p class="MsoNormal">Импорт в РЕ файлах, в отличие от таблицы экспорта, устроен
немного по-другому. Во-первых, как правило он расположен не в одной, а сразу в
двух директориях. Это директория с дескрипторами импорта
IMAGE_DIRECTORY_ENTRY_IMPORT и директория с адресами импорта IMAGE_DIRECTORY_ENTRY_IAT.<o:p></o:p></p><p class="MsoNormal">Бывают, конечно, исключения, когда он умещается только в
первой директории, но это редкость. В импорте отсутствует какая-то единая
таблица, дело в том что обычно импортируются функции из разных библиотек и для
каждой из этих библиотек в директории импорта создается свой дескриптор.<o:p></o:p></p><p class="MsoNormal">Каждый дескриптор - это структура IMAGE_IMPORT_DESCRIPTOR,
чем больше библиотек используются в импорте РЕ файла, тем больше этих структур
будет объявлено, причем допускается повторное объявление дескриптора на одну и
ту же библиотеку, в котором будет описан другой набор импортируемых функций.<o:p></o:p></p><p class="MsoNormal">Дескрипторы пишутся последовательно один за одним, от начала
директории IMAGE_DIRECTORY_ENTRY_IMPORT, при этом самый последний дескриптор
должен содержать в поле Characteristics значение ноль, что означает конец
списка дескрипторов. </p><p class="MsoNormal">В дескрипторе интересны три поля.<o:p></o:p></p><p class="MsoNormal">1. Name - содержит RVA адрес с Ansi строкой хранящей имя
библиотеки. Т.е. грубо переведя его в VA/Raw можно прочитать например
"kernel32.dll"<o:p></o:p></p><p class="MsoNormal">2. FirstThunk - содержит RVA адрес на первый элемент списка
структур IMAGE_THUNK_DATA, каждая из которых в действительности (и по сути)
структурой не является, а представляет из себя либо DWORD, либо ULONGLONG
который хранит некое число, которое трактуется четырьмя разными способами:<o:p></o:p></p><p class="MsoNormal">А: в случае если в
числе взведен старший бит, т.е. выполняется условие Value and IMAGE_ORDINAL_FLAGXX
<> 0, данное число трактуется как Ordinal, что означает о необходимости
загрузки библиотеки по имени Name и поиска функции через GetProcAddress по её
порядковому номеру в списке экспорта.<o:p></o:p></p><p class="MsoNormal">Б: в случае если
старший бит не взведен, это означает что число содержит RVA адрес на структуру
IMAGE_IMPORT_BY_NAME<o:p></o:p></p><p class="MsoNormal">В: если применяется
связанный импорт (в статье рассмотрен не будет) данное число трактуется как VA
адрес импортируемой функции. Это условие достаточно редко встречается и
применимо только для значений списка, на который указывает FirstThunk, в списке
OriginalFirstThunk не используется.<o:p></o:p></p><p class="MsoNormal">Г: теоретически, в
зависимости от значения ForwarderChain дескриптора, может содержать RVA адрес
строки перенаправления, но на практике я такого никогда не встречал, не видел
упоминания ни в одной статье по РЕ формату и этот вариант рассматривать не буду.<o:p></o:p></p><p class="MsoNormal">Все четыре варианта
актуальны только тогда, когда список читается напрямую из файла. Когда он
читается из памяти запущенного приложения, в нем всегда будут размещены VA
адреса импортируемых функций, модификацией данного списка занимается загрузчик
при старте приложения.</p><p class="MsoNormal">Конец списка
означает элемент со значением ноль.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Кстати, именно этот
список, на который указывает FirstThunk хранится отдельно от директории импорта
в специальной директории IMAGE_DIRECTORY_ENTRY_IAT, так называемой Import
Address Table. Подозреваю что сделано это для того, чтобы не происходило
отсоединения таблицы экспорта от механизма шаринга страниц памяти, вынеся все
изменяемые загрузчиком поля в отдельный блок памяти, в который и происходит
запись актуальных данных. И именно его будет контролировать код анализатора.<o:p></o:p></p><p class="MsoNormal">3. OriginalFirstThunk - все тоже самое что и FirstThunk,
только для списков, на которые указывает это поле каждого дескриптора актуальны
только первые два пункта, т.е. в этих списках никогда не будет VA адресов
функций, ни при чтении из файла на диске, ни при чтении из памяти запущенного
приложения. Только RVA адрес структуры IMAGE_THUNK_DATA или Ordinal.<o:p></o:p></p><p class="MsoNormal">А вот IMAGE_THUNK_DATA это простая структура, которая
состоит из двух полей.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>Hint - порядковый номер импортируемой функции в списке
экспорта библиотеки, по которому её предпочтительней искать. Используется
загрузчиком для ускорения поиска импортируемой функции при инициализации
таблицы импорта.</li><li>Name - Ansi строка, завершающаяся нулем, содержащая имя
функции.</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p> </o:p>Вот картинка чтобы было более наглядно:</p><p class="MsoNormal"><o:p></o:p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPWi5IqzmDHBNb_QUraeagZy16XPbsUaYUmMy7cI11tyhxZdhcyVm_j5L2jdsZg-1dUi1FLpRIbg3JZVKu-ArkWDDtWy1L5MR79ongYErpgPsDlXei9KdYfZ4238su2PThxIO1LwSNHHLMIW6egBF5SQwtN4fuNdHGkIh2UUAW5i0aRdncGqNUCNnj/s978/16.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="712" data-original-width="978" height="233" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPWi5IqzmDHBNb_QUraeagZy16XPbsUaYUmMy7cI11tyhxZdhcyVm_j5L2jdsZg-1dUi1FLpRIbg3JZVKu-ArkWDDtWy1L5MR79ongYErpgPsDlXei9KdYfZ4238su2PThxIO1LwSNHHLMIW6egBF5SQwtN4fuNdHGkIh2UUAW5i0aRdncGqNUCNnj/s320/16.png" width="320" /></a></div><br /><p class="MsoNormal">Синим цветом показаны данные списка OriginalFirstThunk и сам
список, обведенный синим квадратом (обращайте внимание на адреса)</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Зеленым показаны данные списка FirstThunk с реальными VA
адресами импортируемых функций (ну, точнее всего одной функции).<o:p></o:p></p><p class="MsoNormal">Оба списка заканчивается нулем.<o:p></o:p></p><p class="MsoNormal">Фиолетовым показан как осуществляется переход на реальный
адрес с именем библиотеки, а красный квадрат показывает начало единственной
структуры IMAGE_THUNK_DATA, конкретно его поля Hint, которое содержит число 137
(отображенное в подсказке справа).<o:p></o:p></p><p class="MsoNormal">Из всех этих данных контролировать потребуется только
значения, которые содержат списки FirstThunk, т.к. именно правкой этих значений
и осуществляется установка перехватчика правкой таблицы импорта. </p><p class="MsoNormal">Итак, задача - пробежаться по всем дескрипторам директории
импорта и загрузить данные из списков каждого дескриптора.<o:p></o:p></p><p class="MsoNormal">Сразу перейду к
коду<span lang="EN-US">:</span> </p>
<pre class="brush:delphi">function TRawPEImage.LoadImport(Raw: TStream): Boolean;
...
Result := False;
Raw.Position := VaToRaw(FImportDir.VirtualAddress);
if Raw.Position = 0 then Exit;
ZeroMemory(@ImportChunk, SizeOf(TImportChunk));
while (Raw.Read(ImageImportDescriptor, SizeOf(TImageImportDescriptor)) =
SizeOf(TImageImportDescriptor)) and (ImageImportDescriptor.OriginalFirstThunk <> 0) do
begin
// запоминаем адрес следующего дексриптора
NextDescriptorRawAddr := Raw.Position;
// вычитываем имя библиотеки импорт из которой описывает дескриптор
Raw.Position := RvaToRaw(ImageImportDescriptor.Name);
if Raw.Position = 0 then
Exit;
// инициализируем размер записей и флаги
IatDataSize := IfThen(Image64, 8, 4);
OrdinalFlag := IfThen(Image64, IMAGE_ORDINAL_FLAG64, IMAGE_ORDINAL_FLAG32);
// вычитываем все записи описываемые дескриптором, пока не кончатся
IatData := 0;
ImportChunk.ImportTableVA := RvaToVa(ImageImportDescriptor.FirstThunk);
OriginalFirstThunk := RvaToVa(ImageImportDescriptor.OriginalFirstThunk);
if OriginalFirstThunk = 0 then
OriginalFirstThunk := ImportChunk.ImportTableVA;
repeat
LastOffset := VaToRaw(OriginalFirstThunk);
if LastOffset = 0 then
Exit;
Raw.Position := LastOffset;
Raw.ReadBuffer(IatData, IatDataSize);
if IatData <> 0 then
begin
// проверка - идет импорт только по ORDINAL или есть имя функции?
if IatData and OrdinalFlag = 0 then
begin
// имя есть - нужно его вытащить
Raw.Position := RvaToRaw(IatData);
if Raw.Position = 0 then
Exit;
Raw.ReadBuffer(ImportChunk.Ordinal, SizeOf(Word));
ImportChunk.FuncName := ReadString(Raw);
end
else
begin
// имени нет - запоминаем только ordinal функции
ImportChunk.FuncName := EmptyStr;
ImportChunk.Ordinal := IatData and not OrdinalFlag;
end;
FImport.Add(ImportChunk);
Inc(ImportChunk.ImportTableVA, IatDataSize);
Inc(OriginalFirstThunk, IatDataSize);
end;
until IatData = 0;
// переходим к следующему дескриптору
Raw.Position := NextDescriptorRawAddr;
end;
Result := ImageImportDescriptor.OriginalFirstThunk = 0;
end;
</pre>
<p class="MsoNormal">Код начинает выполняться с определения Raw адреса директории
импорта в физическом файле.<o:p></o:p></p><p class="MsoNormal">Скажем так это лишняя перестраховка, т.к. директория импорта
как правило всегда в наличии, отсутствует она только у библиотек которые
содержат исключительно ресурсы и не имеют исполняемого кода, либо у специально
подготовленных исполняемых файлов, в основном написанных на ассемблере, т.к.
штатный компилятор врятли вам позволит сотворить такой трюк.<o:p></o:p></p><p class="MsoNormal">Следующим идет цикл - последовательно считываются все
дескрипторы импорта у каждого из которых читается имя описываемой библиотеки,
выставляются флаги для детектирования Ordinal значений в списках и размер
элементов списка. После чего идет хитрый момент.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">В структуре ImportChunk (которая будет содержать данные по
каждому элементу из всех таблиц импорта) в поле ImportTableVA запоминается
адрес элемента списка из IAT на который указывает FirstThunk (именно его будет
контролировать анализатор), но адрес, по которому будет производится реальное
чтение из списка забирается из списка OriginalFirstThunk, элементы которого
никогда не меняются загрузчиком.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Это сделано по причине, описанной в пункте "В"
чуть выше, а именно потому, что список FirstThunk иногда может содержать VA
значения функций, т.е. заранее подготовленные реальные адреса (так называемая
привязка).<o:p></o:p></p><p class="MsoNormal">Если база загрузки библиотеки равна значению прописанному в
IMAGE_OPTIONAL_HEADER, (плюс выполнятся контрольные проверки из BOUND_IMPORT)
загрузчик в этом случае пропускает всю настройку таблицы импорта, в противном
случае используется список OriginalFirstThunk, на основании данных которого
происходит инициализация списка FirstThunk.<o:p></o:p></p><p class="MsoNormal">Достаточно редко, но встречается такое, что список
OriginalFirstThunk отсутствует. В этом случае его заменяет список FirstThunk и
именно это условие учитывается при начале чтения вот в этом коде:<o:p></o:p></p>
<pre class="brush:delphi"> if OriginalFirstThunk = 0 then
OriginalFirstThunk := ImportChunk.ImportTableVA;
</pre>
<p class="MsoNormal">Если этот список отсутствует, FirstThunk гарантированно
будет содержать только те данные, которые должен был содержать
OriginalFirstThunk, в противном случае, если бы библиотека загрузилась не по
своей базе (или не отработали условия в директории связанного импорта) было бы невозможно
инициализировать IAT, на которую указывает FirstThunk, т.к. в этом списке
отсутствовали бы RVA адреса на IMAGE_THUNK_DATA.<o:p></o:p></p><p class="MsoNormal">Ну и в самом конце проверяется условие окончания списка
(проверкой на ноль) и, если список еще не закончился, читается каждый его
элемент либо как Ordinal (c проверкой через наличие флага OrdinalFlag), либо
как структура IMAGE_THUNK_DATA, после чего все заносится в результирующий список
доступный извне.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Выглядит все гораздо проще чем само объяснение.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Прежде чем приступить к коду анализатора полученного
импорта, нужно учесть следующие два момента.</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">1. Нельзя анализировать таблицы импорта библиотек/исполняемых
файлов, которые должны работать в нулевом кольце. У таких модулей в РЕ
заголовке будет присутствовать флаг IMAGE_SUBSYSTEM_NATIVE. Если вы возразите
что такие модули не могут быть загружены в третьем кольце (в котором работает
весь прикладной код) я отвечу, что это не так. Допустим на Win11 есть такой
системный процесс SearchHost и он использует для своей работы библиотеку
C:\Windows\System32\LegacySystemSettings.dll у которой (внезапно) в импорте
объявлена ntoskrnl.exe экспортирующая функцию wcschr().<o:p></o:p></p><p class="MsoNormal">Естественно, из-за этой записи ntoskrnl будет подгружен в
процесс целиком, но её импорт загрузчик обрабатывать не будет, в противном
случае ему пришлось бы подгрузить всё ядро в третье кольцо, а там много чего
есть, включая статический импорт из драйверов.<o:p></o:p></p><p class="MsoNormal">Поэтому анализатор должен проверять - если на вход пришел
модуль с флагом IMAGE_SUBSYSTEM_NATIVE, обработку его импорта производить не
нужно.<o:p></o:p></p><p class="MsoNormal">2. В процесс могут быть загружены так называемые СОМ+
модули, это библиотеки, содержащие только IL код не выполняемый нативно. <o:p></o:p></p><p class="MsoNormal">У таких библиотек в COR20 заголовке выставлен флаг
COMIMAGE_FLAGS_ILONLY, а так-же у файла в таблице импорта есть единственная
заглушка ведущая на mscoree.dll -> _CorDllMain().<o:p></o:p></p><p class="MsoNormal">Загрузчик, при наличии данного флага, не обрабатывает
таблицу импорта, что можно наглядно увидеть в
"...base\ntdll\ldrapi.c" в функции LdrpLoadDll()<o:p></o:p></p>
<pre class="brush:delphi"> {
// if the image is COR-ILONLY, then don't walk the import descriptor
// as it is assumed that it only imports %windir%\system32\mscoree.dll, otherwise
// walk the import descriptor table of the dll.
}
</pre>
<p class="MsoNormal">При наличии данного флага также не обрабатывается секция
релокации. Однако если библиотека должна загружаться в 32 битное приложение
ILAsm может исключить этот флаг, заменив его на COMIMAGE_FLAGS_32BITREQUIRED.<o:p></o:p></p><p class="MsoNormal">Определить такие библиотеки можно не только читая COM
заголовок. Признак не исполняемого IL образа содержится так-же и в флагах
таблицы загрузчика LDR_DATA_TABLE_ENTRYxx.Flags and LDRP_COR_IMAGE <> 0,
который выставляет при инициализации процесса функция LdrpInitializeProcess().<o:p></o:p></p><p class="MsoNormal">Помимо этого, у всех этих модулей страница, которой
принадлежит адрес точки входа, помечена как не исполняемая.<o:p></o:p></p><p class="MsoNormal">Так как импорт не инициализирован, то такие модули так же
необходимо пропускать при анализе таблиц импорта. </p><p class="MsoNormal">Вот теперь можно писать код для анализатора. В немного
сокращенном виде он выглядит так:<o:p></o:p></p>
<pre class="brush:delphi">procedure TPatchAnalyzer.ScanImport(Index: Integer; Module: TRawPEImage);
function CheckRemoteVA: Boolean;
begin
Result := HookData.Calculated and
(HookData.RemoteVA = HookData.RawVA);
end;
//...
begin
if Module.ImportList.Count = 0 then Exit;
if Module.NtHeader.OptionalHeader.Subsystem = IMAGE_SUBSYSTEM_NATIVE then
begin
Inc(FAnalizeResult.Import.Skipped, Module.ImportList.Count);
Exit;
end;
if Module.ComPlusILOnly then
begin
if Module.EntryPoint <> 0 then
if VirtualQueryEx64(FProcessHandle,
Module.EntryPoint, MBI,
SizeOf(TMemoryBasicInformation64)) = SizeOf(TMemoryBasicInformation64) then
if MBI.Protect and (
PAGE_EXECUTE or
PAGE_EXECUTE_READ or
PAGE_EXECUTE_WRITECOPY or
PAGE_EXECUTE_READWRITE) = 0 then
begin
Inc(FAnalizeResult.Import.Skipped, Module.ImportList.Count);
Exit;
end;
end;
// подгружаем кэш таблицы импорта, обычно она сидит в секции IAT
// но в редких случаях эта секция отсутствует и таблица размещается прямо в секции импорта
CacheVA := IfThen(Module.ImportAddressTable.Size = 0,
Module.ImportDirectory.VirtualAddress, Module.ImportAddressTable.VirtualAddress);
CacheSize := IfThen(Module.ImportAddressTable.Size = 0,
Module.ImportDirectory.Size, Module.ImportAddressTable.Size);
if CacheSize > 0 then
AIat := TRemoteStream.Create(FProcessHandle, CacheVA, CacheSize)
else
Exit;
try
AddrSize := IfThen(Module.Image64, 8, 4);
ZeroMemory(@HookData, SizeOf(THookData));
// ...
for Import in Module.ImportList do
begin
HookData.FuncName := Import.ToString;
HookData.RawVA := 0;
// зачитываем текущий адрес из таблицы импорта
if not AIat.ReadMemory(Import.ImportTableVA, AddrSize, @HookData.RemoteVA) then
Continue;
if Import.FuncName = EmptyStr then
HookData.Calculated := FRawModules.GetProcData(Import.LibraryName,
Import.Ordinal, Module.Image64, Exp, HookData.RemoteVA)
else
HookData.Calculated := FRawModules.GetProcData(Import.LibraryName,
Import.FuncName, Module.Image64, Exp, HookData.RemoteVA);
if HookData.Calculated then
HookData.RawVA := Exp.FuncAddrVA
else
if not CheckRemoteVA then
begin
DoModifyed(HookData);
Continue;
end;
if not CheckRemoteVA then
begin
// если функция перенаправлена, пытаемся её подгрузить
if Exp.ForvardedTo <> EmptyStr then
if not FRawModules.GetProcData(Exp.ForvardedTo,
Module.Image64, Exp, HookData.RemoteVA) then
begin
HookData.Calculated := False;
HookData.ImportAdv.OriginalForvardedTo := Exp.OriginalForvardedTo;
HookData.ImportAdv.ForvardedTo := Exp.ForvardedTo;
DoModifyed(HookData);
Continue;
end
else
begin
HookData.RawVA := Exp.FuncAddrVA;
HookData.ImportAdv.OriginalForvardedTo := EmptyStr;
HookData.ImportAdv.ForvardedTo := EmptyStr;
end;
if not CheckRemoteVA then
DoModifyed(HookData);
end;
end;
finally
AIat.Free;
end;
end;
</pre>
<p class="MsoNormal">В самом начале идут три проверки:<o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>на наличие импорта</li><li>проверка модулей с флагом IMAGE_SUBSYSTEM_NATIVE</li><li>проверка COM+ модулей</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Далее настраивается кэш, тоже самое было при чтении таблиц
экспорта, кэш просто позволит ускорить чтение данных. и он создается с
описанным выше нюансом что директория Import Address Table может отсутствовать,
в этом случае (как правило) таблица импорта располагается прямо в директории
импорта. </p><p class="MsoNormal">После чего идет цикл по всем записям, полученным от всех
дескрипторов импорта текущего модуля.<o:p></o:p></p><p class="MsoNormal">Сначала читается текущее значение в удаленном адресном
пространстве процесса из ранее рассчитанного адреса в списке FirstThunk (этот
адрес хранится в ImportTableVA), а потом производится попытка самостоятельного
поиска записи об экспортируемой функции в загруженных ранее таблицах экспорта
(по Ordinal или по имени). </p><p class="MsoNormal">Если запись нашлась - происходит сравнение результатов,
рассчитанного и актуального значений, а если не нашлась, то вызывается внешний
калбек.<o:p></o:p></p><p class="MsoNormal">Причем если запись нашлась, но адреса не совпали, идет
проверка перенаправления через содержимое поля ForvardedTo и, если оно
заполнено, сравнивается перенаправленная запись. </p><p class="MsoNormal">И вот если сейчас запустить код демонстрационного примера на
Windows XP или Vista то все отработает штатно. Он не выведет никаких ошибок,
просто покажет список загруженных библиотек и остановится, но... ситуация
кардинально поменяется, как только он будет запущен на Windows 7 и выше.<o:p></o:p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk6bCTTn-YdPjbO4nJ1VA3Lrl6FBIKYhynMAOo_UH2PPCHdXxf7acimbr4k2HpciASzG9p5UlGW7hPkS3Gp_hxFd2KLhYddFRqPudej_OeXSsD5fYEFv7H0x-e-MdGBGNQfBnjEpzf17RXxLPyShjMhYj76BVlnphZV5Mm37v12fxfvnphFSrb9GP2/s1218/17.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="512" data-original-width="1218" height="135" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk6bCTTn-YdPjbO4nJ1VA3Lrl6FBIKYhynMAOo_UH2PPCHdXxf7acimbr4k2HpciASzG9p5UlGW7hPkS3Gp_hxFd2KLhYddFRqPudej_OeXSsD5fYEFv7H0x-e-MdGBGNQfBnjEpzf17RXxLPyShjMhYj76BVlnphZV5Mm37v12fxfvnphFSrb9GP2/s320/17.png" width="320" /></a></div><br /><p class="MsoNormal">Множеству записей в импорте анализатор не смог подобрать
соответствующую запись в экспорте, о чем говорит текст "Export record
missing".</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Причем практически все эти записи ведут на отсутствующие на
жестком диске библиотеки "api-ms-win-xxx.dll" или "ext-ms-win-xxx.dll"<o:p></o:p></p><p class="MsoNormal">И такое происходит как у записей в таблицах импорта, так и в
таблицах экспорта, обратите внимание на самую последнюю строчку<span lang="EN-US">:</span> </p><p class="MsoNormal"><span lang="EN-US">Import
modified imm32.dll -> kernel32.GetProcessMitigationPolicy, at address:
7641D0D4<br /></span><span lang="EN-US">Export
record missing, present: 0000000075DECF40, forvarded to
"api-ms-win-core-processthreads-l1-1-1.GetProcessMitigationPolicy"
--> KernelBase.dll</span> </p><p class="MsoNormal">Сам импорт обработан правильно и запись ведет на
kernel32.GetProcessMitigationPolicy, но сама эта функция перенаправлена и ведет
на отсутствующую в текущем списке экспорта библиотеку
"api-ms-win-core-processthreads-l1-1-1.dll", причем по факту текущий
адрес в таблице импорта ведет внутрь "KernelBase.dll".</p><p class="MsoNormal">
</p><p class="MsoNormal">Сейчас будем с этим разбираться, ну а сам код к пятой главе <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%205" target="_blank">доступен по этой ссылке</a>.</p><p class="MsoNormal"><br /></p>
<a name="apiset"></a>
<h3 style="text-align: left;">6. ApiSet редиректы</h3><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<p class="MsoNormal">Начиная с Windows 7 совершилась небольшая революция, в
Windows было внедрено ядро MinWin. Скажем так это минимальная сборка Windows с
самым необходимым набором функционала, что позволило в дальнейшем легко
разрабатывать такие специализированные OS как Windows IoT, взамен Embedded.<o:p></o:p></p>
<p class="MsoNormal">Часть функционала из kernel32/user32/gdi32/etc... переехала
в другие библиотеки, причем в зависимости от типа операционной системы, набор
библиотек с конечным функционалом может меняться. А чтобы прикладной код был
универсален, Microsoft придумала концепцию виртуальных библиотек.<o:p></o:p></p>
<p class="MsoNormal">Брались группы функций, например для работы с файлами
(CreateFile/FindFirstFile/etc...), и переносились в библиотеку, допустим
"api-ms-win-core-file-l1-1-0.dll". И в самом начале, в Windows 7, эти
библиотеки даже присутствовали в "c:\windows\system32\" и их можно
было посмотреть. Сами они вообще не содержали никакого кода, и их задача была в
предоставлении внешнему коду своей таблицы экспорта, в которой были объявлены
записи о всех функциях с их редиректами к библиотеке, содержащей реальный код
функций. Впрочем, их наличие никаких плюсов не давало, т.к. загрузка таких
библиотек через LoadLibrary возвращала ошибку ERROR_DLL_INIT_FAILED, а сейчас,
в Windows 11 этих библиотек вообще нет, да они в принципе и не нужны.<o:p></o:p></p>
<p class="MsoNormal">Как это работает: все функции, реализация которых перенесена
в ядро MinWin (их достаточно большой список), указываются в таблицах
импорта/экспорта не с указанием имени библиотеки, которая их реализует, а с
указанием именно таких промежуточных виртуальных библиотек, именно они были показаны
на последнем скриншоте. Загрузчик при инициализации таблиц импорта/экспорта
производит подмену виртуальных библиотек на реальные, реализующие финальный код
функции после чего процесс может нормально запустится, т.к. все адреса у него
актуализированы.<o:p></o:p></p>
<p class="MsoNormal">А делает он такой редирект на основе специальной служебной
таблицы, называемой ApiSet Map, расположенной в библиотеке apisetschema.dll в виде
отдельной секции ".apiset" причем эта библиотека не подгружается в
процесс, отмапливается только сама секция. И вот формат этой секции
представляет наибольший интерес.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Во-первых, он абсолютно не документирован, во-вторых, на
текущий момент существует три версии данного формата.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"></p><ul style="text-align: left;"><li>версия два, используется в Windows 7 и Windows 8</li><li>версия четыре используется в Windows 8.1</li><li>версия шесть используется в Windows 10 и Windows 11</li></ul><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Для того чтобы код анализатора правильно работал на всех
версиях OS, он должен уметь читать все три формата этой таблицы.<o:p></o:p></p>
<p class="MsoNormal">Ну а в-третьих, данная таблица присутствует в каждом
процессе и нет необходимости её чтения из чужого адресного пространства,
достаточно её прочитать у самого себя. </p>
<p class="MsoNormal">Ссылка на адрес памяти, содержащий отмапленный ApiSet
расположена в блоке окружения процесса (PEB), это именно та структура,
посредством которой происходило чтение данных из таблиц загрузчика. Так как
чтение таблицы может (и будет) происходить из текущего процесса, то получение
необходимого адреса упрощается и сводится всего лишь к паре ассемблерных строк:<o:p></o:p></p>
<pre class="brush:asm">function TApiSetRedirector.GetPEBApiSet: Pointer;
asm
{$IFDEF WIN32}
mov eax, fs:[30h]
mov eax, [eax + 38h]
{$ELSE}
mov rax, gs:[60h]
mov rax, [rax + 68h]
{$ENDIF}
end;
</pre>
<p class="MsoNormal">Здесь, посредством регистра FS (или GS для 64 бит), который
указывает на блок окружения потока (TEB), читается адрес структуры PEB
(смещение поля TEB->ProcessEnvironmentBlock равно 0х30 и 0х60 для 32 и 64
бит соответственно), после чего читается значение поля PEB->ApiSetMap
(смещение 0х38 и 0х68 для 32 и 64 бит соответственно). </p>
<p class="MsoNormal">Сразу же упомяну о двух моментах, чтобы не останавливаться
на них позже. <o:p></o:p></p>
<p class="MsoNormal">1. Все структуры, описанные ниже, содержат адреса в RVA
формате, но базой, от которой идет отсчет RVA адреса, является <b>адрес самой
таблицы</b>, который был получен кодом выше. Т.е. если в какой-то структуре
содержится число 0х123, а таблица размещается в памяти по адресу 0х30000, то
это означает что VA адрес будет равен 0х30123! <o:p></o:p></p>
<p class="MsoNormal">2. Во всех форматах ApiSet таблицы строки хранятся в виде
вот такой структуры:<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetString = record
Offset: ULONG;
Length: USHORT;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Где Offset - это RVA адрес UNICODE строки (не Ansi), а
Length - её длина в байтах (не в символах). </p>
<p class="MsoNormal">После получения адреса ApiSet таблицы нужно определится в
каком формате она представлены, за это отвечает самое первое поле заголовка
таблицы, представляющее из себя LONG, в котором будет записана версия 2, 4 или
6: </p>
<pre class="brush:delphi">procedure TApiSetRedirector.Init;
begin
FApiSetVer := PLONG(FApiSet)^;
case FApiSetVer of
2: Init2;
4: Init4;
6: Init6;
end;
end;
</pre>
<p class="MsoNormal">Начну с самого простого формата за номером два. </p>
<p class="MsoNormal">Он представляет из себя четыре структуры.<o:p></o:p></p>
<p class="MsoNormal"><span lang="EN-US" style="mso-ansi-language: EN-US;">1. </span>Заголовок<span style="mso-ansi-language: EN-US;"> </span>таблицы<span lang="EN-US" style="mso-ansi-language: EN-US;">: <o:p></o:p></span></p>
<pre class="brush:delphi"> TApiSetNameSpace2 = record
Version,
Count: ULONG;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">С него начинается ApiSet таблица второй версии и задача этой
структуры, сообщить, помимо версии, о количестве содержащихся в ней записей о
редиректе. </p>
<p class="MsoNormal">2. Сразу за ней идет массив структур TApiSetNameSpaceEntry2,
в количестве, указанном в заголовке таблицы.<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetNameSpaceEntry2 = record
Name: TApiSetString;
DataOffset: ULONG;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Поле Name, это и есть искомая строка редиректа, выглядит
обычно вот в таком виде с учетом регистра (если прочитать через TApiSetString):
"MS-Win-Core-ErrorHandling-L1-1-0". Этой строке будет соответствовать
запись в таблицах импорта/экспорта примерно такого вида:<o:p></o:p></p>
<p class="MsoNormal"></p><ul style="text-align: left;"><li>или<span lang="EN-US" style="mso-ansi-language: EN-US;"> "api-ms-win-core-errorhandling-l1-1-0.dll" <o:p></o:p></span></li><li>или<span lang="EN-US" style="mso-ansi-language: EN-US;"> "ext-ms-win-core-errorhandling-l1-1-0.dll"<o:p></o:p></span></li></ul><p></p>
<p class="MsoNormal">Я не смог найти информацию по какому правилу добавляется тот
или иной префикс, поэтому при чтении ApiSet таблицы второй (и четвертой)
версии, просто добавляю в словарь сразу две записи с каждым из префиксов. </p>
<p class="MsoNormal">Поле DataOffset - RVA адрес следующей структуры:<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetValueEntry2 = record
NumberOfRedirections: ULONG;
end;
</pre>
<p class="MsoNormal">Она представляет из себя всего одно поле, обозначающее
количество возможных вариантов редиректа, сразу за которой идет массив
структур, описывающих куда именно нужно произвести редирект в количестве
NumberOfRedirections:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi"> TApiSetValueEntryRedirection2 = record
Name: TApiSetString;
Value: TApiSetString;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">И вот тут достаточно интересный момент. Самое первое поле
Name представляет из себя, по сути, фильтр, уточняющий в каких случаях данный
редирект должен применяться и содержит в себе либо имя библиотеки, либо
остается пустым.<o:p></o:p></p>
<p class="MsoNormal">А вот второе поле указывает на какую конкретно библиотеку
произойдет редирект.<br />Давайте посмотрим на картинку:</p><p class="MsoNormal"><o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5OizuRxEBtyW87dOhNtt3ixGSShAqQmqXCE-A1c20jJG5NNg-hfMQ9pSw4ViBPvmWiDXeTfE9f1yyswsosEfs1OMeg30RYn9N_vnCp2ZoKRLZB1u3IeiAYhNC5ykhlpn7Ol16rztQgWSLMowifAS8rbOrupS_MUsqWemJIiP7boWuafPmNP3cA0w8/s1753/18.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="582" data-original-width="1753" height="106" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5OizuRxEBtyW87dOhNtt3ixGSShAqQmqXCE-A1c20jJG5NNg-hfMQ9pSw4ViBPvmWiDXeTfE9f1yyswsosEfs1OMeg30RYn9N_vnCp2ZoKRLZB1u3IeiAYhNC5ykhlpn7Ol16rztQgWSLMowifAS8rbOrupS_MUsqWemJIiP7boWuafPmNP3cA0w8/s320/18.png" width="320" /></a></div><br /><p class="MsoNormal">ApiSetValueEntry2 содержит в себе число два, и сразу за ней
идут две записи, причем первая перенаправляет в kernel32.dll (поле Value), а
вторая в kernelbase.dll, причем у второй указано что Name равен kernel32.dll.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Как это работает: например, nsi.dll имеет в импорте запись
об GetLastError которую экспортирует API-MS-Win-Core-ErrorHandling-L1-1-0.dll<o:p></o:p></p>
<p class="MsoNormal">По умолчанию для всех библиотек включается редирект на
kernel32.dll, но и сам kernel32.dll в таблице импорта имеет запись об
GetLastError (на которую еще и форвард идет из таблицы экспорта), причем из
той-же библиотеки API-MS-Win-Core-ErrorHandling-L1-1-0.dll.<o:p></o:p></p>
<p class="MsoNormal">Вот для такой ситуации в ApiSet включена запись что
kernel32.dll должна быть перенаправлена не в саму себя, а в kernelbase.dll </p>
<p class="MsoNormal">Схематично вторая версия ApiSet таблицы выглядит в виде вот
такого дерева:<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetNameSpace2 // количество виртуальных библиотек
TApiSetNameSpaceEntry2 // вирт библиотека "API-MS-Win-Core-Console-L1-1-0.dll"
TApiSetNameSpaceEntry2 // вирт библиотека "API-MS-Win-Core-DateTime-L1-1-0.dll"
TApiSetNameSpaceEntry2 // вирт библиотека "API-MS-Win-Core-Debug-L1-1-0.dll"
TApiSetNameSpaceEntry2 // вирт библиотека "API-MS-Win-Core-DelayLoad-L1-1-0.dll"
TApiSetNameSpaceEntry2 // вирт библиотека "API-MS-Win-Core-ErrorHandling-L1-1-0.dll"
TApiSetValueEntry2 // количество вариантов редиректов
TApiSetValueEntryRedirection2 // описание редиректа по умолчанию
TApiSetValueEntryRedirection2 // описание дополнительного условия редиректа
...
</pre>
<p class="MsoNormal">И<span style="mso-ansi-language: EN-US;"> </span>код<span style="mso-ansi-language: EN-US;"> </span>её<span style="mso-ansi-language: EN-US;">
</span>чтения<span lang="EN-US" style="mso-ansi-language: EN-US;">:<o:p></o:p></span></p>
<pre class="brush:delphi">procedure TApiSetRedirector.Init2;
...
begin
NameSpaceEntry := PApiSetNameSpaceEntry2(PByte(FApiSet) + SizeOf(TApiSetNameSpace2));
for I := 0 to PApiSetNameSpace2(FApiSet)^.Count - 1 do
begin
LibFrom := GetString(NameSpaceEntry.Name).ToLower;
ValueEntry := Pointer(PByte(FApiSet) + NameSpaceEntry.DataOffset);
EntryRedirection := Pointer(PByte(ValueEntry) + SizeOf(TApiSetValueEntry2));
for A := 0 to ValueEntry.NumberOfRedirections - 1 do
begin
Redirection := GetString(EntryRedirection.Value);
Key := LibFrom + GetString(EntryRedirection.Name);
AddRedirection(Key, Redirection);
Inc(EntryRedirection);
end;
Inc(NameSpaceEntry);
end;
end;
</pre>
<p class="MsoNormal">В этом коде всего два нюанса:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>все RVA адреса пересчитываются в VA сложением с адресом
самой таблицы (эту адресацию я уже упомянул в самом начале, но на всякий
случай).</li><li>формирование ключа для поиска редиректа. Для всех случаев
он хранится как имя виртуальной библиотеки, т.е. "api-ms-win-core-errorhandling-l1-1-0",
а частные случаи для конкретных библиотек, хранятся с указанием имени этой
библиотеки: "api-ms-win-core-errorhandling-l1-1-0kernel32.dll"</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Это пригодится в дальнейшем для быстрого поиска. </p>
<p class="MsoNormal">Теперь формат таблицы за номером четыре, который применяется
в Windows 8.1<br />Заголовок выглядит в виде такой структуры:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi"> TApiSetNameSpace4 = record
Version,
Size,
Flags,
Count: ULONG;
end;
</pre>
<p class="MsoNormal">По сравнению со второй версией добавились поля Size,
содержащий полный размер ApiSet таблицы в байтах, и поле Flags - которое (по
идее) должно содержать какие-то флаги, значения и назначение которых мне не
известны. На моих тестовых стендах это поле всегда было равно нулю. </p>
<p class="MsoNormal">Далее все также идет массив структур TApiSetNameSpaceEntry4,
в количестве, указанном в заголовке таблицы.<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetNameSpaceEntry4 = record
Flags: ULONG;
Name: TApiSetString;
Alias: TApiSetString;
DataOffset: ULONG;
end;
</pre>
<p class="MsoNormal">Здесь появилось два новых поля, назначение которых мне также
не известно, это Flags (содержит либо единицу, либо тройку), и поле Alias -
содержащее сокращенное имя виртуальной библиотеки (применение не известно).
Остальные поля остались старыми и работают также, т.е. DataOffset - это RVA
адрес на структуру:<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetValueEntry4 = record
Flags,
NumberOfRedirections: ULONG;
end;
</pre>
<p class="MsoNormal">Опять, появилось поле с каким-то флагом, что делает - не
понятно (на тесте всегда равен нулю).<o:p></o:p></p>
<p class="MsoNormal">Ну и сразу за ним, также как и во второй версии ApiSet, идет
массив реальных редиректов:<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetValueEntryRedirection4 = record
Flags: ULONG;
Name: TApiSetString;
Value: TApiSetString;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Кроме дополнительного поля флага (который всегда равен
нулю), ничего не поменялось. Таким образом, структура и код чтения четвертой
версии ApiSet таблицы, ничем не отличается, за исключением что используются
новые версии структур с дополнительным и не влияющими ни на что, полями. </p>
<p class="MsoNormal">А вот ApiSet версии шесть, который пошел начиная с Windows
10 и продолжает используется в Windows 11, устроен немножко хитрее.<br />Заголовок выглядит следующим образом:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi"> TApiSetNameSpace6 = record
Version,
Size,
Flags,
Count,
EntryOffset,
HashOffset,
HashFactor: ULONG;
end;
</pre>
<p class="MsoNormal">По сравнению с заголовком четвертой версии добавились три
поля:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>Поле EntryOffset, который является RVA адресом начала
массива редиректов. Раньше они шли сразу за заголовком (впрочем, в шестой
версии тоже идут сразу за ним), но наличие этого поля подразумевает что
ситуация в какой-то момент времени может изменится, и они могут быть перемещены
по любом другому произвольному адресу.</li><li>Поле HashOffset, новое понятие для ApiSet таблиц,
майкрософт решило ускорить работу с таблицей и добавило в неё хэши имен
виртуальных библиотек. Это поле хранит RVA указатель на начало массива структур
описывающих хэш к каждой записи. Если честно мне этот механизм показался не
удобным поэтому в коде я использую обычный словарь, который справляется с
хранением имен библиотек намного удобнее и применим к любой версии ApiSet,
поэтому это поле в коде будет игнорироваться.</li><li>HashFactor - это поле содержит число от которого начинает
считаться хэш, тоже не интересно.</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Сразу за заголовком, ну если быть точнее, по адресу, на
который указывает EntryOffset идет массив структур:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi"> TApiSetNameSpaceEntry6 = record
Flags: ULONG;
Name: TApiSetString;
HashedLength: ULONG;
ValueOffset,
ValueCount: ULONG;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"></p><ul style="text-align: left;"><li>Поле Flags - некоторые источники утверждают, что это поле
хранит флаг Sealed равный единице, за что он отвечает, не известно, но он
выставлен у всех записей.</li><li>Поле Name - это искомое имя виртуальной библиотеки. Причем в
отличии от младших версий, оно представлено целиком со всеми необходимыми
префиксами, например вот так: "api-ms-win-core-errorhandling-l1-1-3"</li><li>Поле HashedLength - а вот это размер имени библиотеки в
байтах, с которой должен сниматься хэш. Очень важное поле, т.к. имя виртуальной
библиотеки, имеет свою версионность, например errorhandling в Windows 7
заканчивался цифрой ноль, на Windows 8.1 вообще присутствует две записи, с
версией ноль и с версией один, в Windows 10 она шла с цифрой два (в самых
первых сборках), а сейчас идет за версией три, как и в Windows 11. Старые же
версии программ содержат привязку к актуальным на момент их сборки
наименованиям виртуальных библиотек, и параметр HashedLength указывает размер
наименования, который будет одинаков для всех без учета версионности, таким
образом давая возможность запуска старым сборкам программ.</li><li>Поле ValueOffset - аналог старого поля DataOffset, содержит
RVA адрес на массив структур редиректа</li><li>Поле ValueCount - новое поле, которое заменяет собой старую
структуру TApiSetValueEntryХ и содержит размер массива редиректов для
виртуальной библиотеки.</li></ul><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<p class="MsoNormal">А вот структура, использующаяся в массиве редиректов не
поменялась и осталась по наполнению полей такая-же как и в версии четыре
формата ApiSet таблицы. Но и в ней произошли небольшие изменения. В предыдущих
версиях формата указывалось имя библиотеки для редиректа (поле Value), и
описывались особые случаи, когда применялся другой тип редиректа (поле Name), в
шестой же версии формата редирект вообще может быть указан пустым.<o:p></o:p></p>
<p class="MsoNormal">Это применимо для модулей ядра, например ntoskrnl.exe
который имеет редирект на виртуальную библиотеку
"ext-ms-win-ntos-ksecurity-l1-1" ну и несколько других сугубо
специфичных для ядра. Такой исполняемый файл может быть подгружен в адресное
пространство процесса (я упоминал про это в пятой главе) но его таблица импорта
будет свернута в самого себя на заглушки, поэтому такие модули с
IMAGE_SUBSYSTEM_NATIVE не обрабатываются и редиректы для таких виртуальных
библиотек пустые. </p>
<p class="MsoNormal">Раз структура, по сути, не поменялась, то показывать её
декларацию я не буду, а вместо неё покажу структуру описывающую хэш имени. <o:p></o:p></p>
<p class="MsoNormal">Она использоваться не будет, поэтому просто для справки.<o:p></o:p></p>
<pre class="brush:delphi"> TApiSetHashEntry6 = record
Hash,
Index: ULONG;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Код чтения шестой версии таблицы такой:<o:p></o:p></p>
<pre class="brush:delphi">procedure TApiSetRedirector.Init6;
...
begin
NameSpaceEntry := PApiSetNameSpaceEntry6(PByte(FApiSet) +
PApiSetNameSpace6(FApiSet)^.EntryOffset);
for I := 0 to PApiSetNameSpace6(FApiSet)^.Count - 1 do
begin
LibFrom := GetString(NameSpaceEntry.Name);
SetLength(LibFrom, NameSpaceEntry.HashedLength div SizeOf(Char));
ValueEntry := Pointer(PByte(FApiSet) + NameSpaceEntry.ValueOffset);
for A := 0 to NameSpaceEntry.ValueCount - 1 do
begin
Redirection := GetString(ValueEntry.Value);
Key := LibFrom + GetString(ValueEntry.Name);
if not Redirection.IsEmpty then
begin
Inc(FUniqueCount);
FData.AddOrSetValue(Key, Redirection);
end;
Inc(ValueEntry);
end;
Inc(NameSpaceEntry);
end;
end;
</pre>
<p class="MsoNormal">Это, собственно, все по поводу чтения, теперь нужно
подключить ApiSet к загрузчику модулей TRawPEImage.<o:p></o:p></p>
<p class="MsoNormal">Для этого необходимы два метода доступных снаружи для
обработки редиректа:<o:p></o:p></p>
<pre class="brush:delphi">function TApiSetRedirector.RemoveSuffix(const Value: string): string;
var
LastSuffixIndex: Integer;
begin
if FApiSetVer = 6 then
begin
LastSuffixIndex := Value.LastDelimiter('-');
if LastSuffixIndex > 0 then
Exit(Copy(Value, 1, LastSuffixIndex));
end;
Result := Value;
end;
function TApiSetRedirector.SchemaPresent(const LibName: string;
var RedirectTo: string): Boolean;
var
Tmp: string;
begin
if FData.Count = 0 then Exit(False);
Tmp := RemoveSuffix(RedirectTo.ToLower);
// сначала получаем с привязкой к текущей библиотеке
Result := FData.TryGetValue(Tmp + LibName.ToLower, RedirectTo);
// а если нет записи, то получаем перенаправление по умолчанию
if not Result then
Result := FData.TryGetValue(Tmp, RedirectTo);
end;
</pre>
<p class="MsoNormal">Функция RemoveSuffix отвечает за изъятие версионной метки из
имени виртуальной библиотеки, в случае если используется ApiSet версии шесть, а
SchemaPresent производит поиск редиректа, соответствующий имени виртуальной
библиотеки в комбинации с именем библиотеки, из которой идет вызов (для
обработки специальных ситуаций, когда должен быть применен отдельный редирект,
а не общий).</p><p class="MsoNormal">Кстати при обработке таблиц экспорта должно использоваться именно то имя библиотеки которое записано в поле ImageExportDirectory.Name, именно по ней производится редирект, а не по текущему имени библиотеки (которая может быть банально переименована - о таком случае).</p>
<p class="MsoNormal">Инициализацию ApiSet нужно произвести при открытии процесса,
для этого в процедуре TRawScanner.InitFromProcess в самом начале объявляется
вызов ApiSetRedirector.LoadApiSet; </p>
<p class="MsoNormal">В загрузчике модулей необходимо добавить метод работы с
редиректором:<o:p></o:p></p>
<pre class="brush:delphi">procedure TRawPEImage.InternalProcessApiSetRedirect(const LibName: string;
var RedirectTo: string);
var
ForvardLibraryName, FuncName: string;
begin
if not ParceForvardedLink(RedirectTo, ForvardLibraryName, FuncName) then
Exit;
ForvardLibraryName := ChangeFileExt(ForvardLibraryName, '');
if ApiSetRedirector.SchemaPresent(LibName, ForvardLibraryName) then
RedirectTo := ChangeFileExt(ForvardLibraryName, '.') + FuncName;
end;
</pre>
<p class="MsoNormal">Его задачей будет преобразовывать вызовы ведущие в
виртуальные библиотеки, на вызовы, идущие к конечным, в которых реализован
реальный код функций.<o:p></o:p></p>
<p class="MsoNormal">Например, при входных данных:<o:p></o:p></p>
<p class="MsoNormal"></p><blockquote>LibName = 'KERNEL32.dll'<br />RedirectTo
= 'api-ms-win-core-libraryloader-l1-1-0.AddDllDirectory'<br /><span lang="EN-US" style="mso-ansi-language: EN-US;">RedirectTo </span>преобразуется<span style="mso-ansi-language: EN-US;"> </span>в<span lang="EN-US" style="mso-ansi-language: EN-US;"> 'kernelbase.AddDllDirectory'</span></blockquote><span lang="EN-US" style="mso-ansi-language: EN-US;"></span><p></p>
<p class="MsoNormal">Ну<span style="mso-ansi-language: EN-US;"> </span>а<span style="mso-ansi-language: EN-US;"> </span>вызов<span style="mso-ansi-language: EN-US;"> </span>этого<span style="mso-ansi-language: EN-US;"> </span>метода<span style="mso-ansi-language: EN-US;"> </span>будет<span style="mso-ansi-language: EN-US;"> </span>размещен<span style="mso-ansi-language: EN-US;"> </span>в<span style="mso-ansi-language: EN-US;"> </span>двух<span style="mso-ansi-language: EN-US;"> </span>заглушках<span lang="EN-US" style="mso-ansi-language: EN-US;">
ProcessApiSetRedirect </span>обрабатывающих<span style="mso-ansi-language: EN-US;">
</span>импорт<span style="mso-ansi-language: EN-US;"> </span>и<span style="mso-ansi-language: EN-US;"> </span>экспорт<span lang="EN-US" style="mso-ansi-language: EN-US;">.</span></p>
<p class="MsoNormal">Теперь, если запустить код демопримера, можно увидеть, что
он отработал без ошибок и вывел в консоль только список загруженных библиотек
без ошибок, которые присутствовали в коде предыдущей главы. </p>
<p class="MsoNormal">Но это еще не все - есть еще таблица отложенного импорта, и
её тоже нужно контролировать. </p>
<p class="MsoNormal">Код к шестой главе для самостоятельного изучения <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%206" target="_blank">доступен по этой ссылке</a>.</p><p class="MsoNormal"><br /></p>
<a name="delayed"></a>
<h3 style="text-align: left;">7. Отложенный импорт</h3><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<p class="MsoNormal">Как мне кажется, с отложенным импортом разработчики PE
формата перемудрили, он избыточен, ведь есть обычная динамическая загрузка библиотек,
в которой можно удобно принять решение что нужно делать, в случае если
требуемая функция (или вообще библиотека целиком) отсутствует. Этот гибкий
механизм зачем-то преобразовали в гораздо менее удобный механизм отложенного
импорта. Его суть заключается в следующем - в PE файле в отдельной директории
строится список дескрипторов отложенного импорта, в которые помещаются данные
об импортируемых функциях "предположительно отсутствующих" на
операционной системе пользователя и эта таблица не обрабатывается загрузчиком,
что позволяет программе запуститься без выдачи сообщения "The procedure
entry point {%FuncName%} could not be located in the dynamic link library
{%LibName%}".<o:p></o:p></p>
<p class="MsoNormal">Изначальные адреса таких функций указывают на код их инициализации,
который должен выполнится при первом вызове такой функции.<o:p></o:p></p>
<p class="MsoNormal">Причем в Microsoft Visual Studio это все документировано и
код такого обработчика доступен как для изучения, так и для модификации под
свои требования, подробнее можно <a href="https://learn.microsoft.com/en-us/cpp/build/reference/understanding-the-helper-function">узнать
в MSDN</a>.<o:p></o:p></p>
<p class="MsoNormal">А вот в Delphi с этим все сложнее, заменить его на свой и
вообще посмотреть, как он работает увы не удастся (разве что только в асм
коде), т.к. он реализован в отсутствующем delayhlp.cpp (название модуля
подозрительно похоже на такой-же аналог у MSVC). Причем это может приводить к
весьма печальным ошибкам, например в ранних версиях Delphi в коде
_delayLoadHelper была допущена критическая ошибка, которую никак нельзя было
исправить на тот момент для 64 битных сборок. Дело в том что код данного стаба
при инициализации адреса функции не сохранял на стеке регистры XMM0..XMM3,
использующиеся для передачи аргументов с плавающей запятой. Это приводило к
невозможности импорта функций с такими параметрами через таблицу отложенного
импорта из-за их порчи, так как GetProcAddress в процессе работы сама меняет
значения этих регистров. </p>
<p class="MsoNormal">Разработчики Wine, зная об этой особенности даже сделали
специальный патч в обвязке kernel:<o:p></o:p></p>
<p class="MsoNormal"><a href="https://gitlab.winehq.org/wine/wine/-/blob/master/dlls/kernel32/module.c " target="_blank">https://gitlab.winehq.org/wine/wine/-/blob/master/dlls/kernel32/module.c </a></p>
<p class="MsoNormal">Впрочем, это лирика, если огрубить, то задача обработчика
инициализации отложенного импорта заключается в вызове
GetModuleHandle/LoadLibrary + GetProcAddress после чего полученный адрес функции
размещается в нужном поле таблицы отложенного экспорта, заменяя текущий,
указывающий на код инициализации.<o:p></o:p></p>
<p class="MsoNormal">Соответственно если первый раз вызов произошел успешно и
адрес функции определился правильно, то повторный вызов функции,
импортирующейся через отложенную таблицу импорта, будет идти уже напрямую,
минуя код инициализации. <o:p></o:p></p>
<p class="MsoNormal">В контексте статьи именно это поле в таблице и будет
интересно, причем надо сразу закладываться что «правильных» значений в этом
поле может быть два: адрес кода инициализации и адрес реальной функции.<o:p></o:p></p>
<p class="MsoNormal">Расположена отложенная таблица импорта в директории
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT и представляет из себя массив дескрипторов:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi"> TImgDelayDescr = record
grAttrs, // attributes
rvaDLLName, // RVA to dll name
rvaHmod, // RVA of module handle
rvaIAT, // RVA of the IAT
rvaINT, // RVA of the INT
rvaBoundIAT, // RVA of the optional bound IAT
rvaUnloadIAT, // RVA of optional copy of original IAT
dwTimeStamp: DWORD; // 0 if not bound,
// O.W. date/time stamp of DLL bound to (Old BIND)
end;
</pre>
<p class="MsoNormal">- Поле grAttrs, содержит флаг типа адресации полей
дескриптора.<o:p></o:p></p>
<p class="MsoNormal">Адреса, содержащиеся в этой структуре, могут быть
представлены в двух видах:<o:p></o:p></p>
<p class="MsoNormal"></p><ol style="text-align: left;"><li>В виде RVA, при этом поле grAttrs будет равно dlattrRva
(равное единице).</li><li>В виде VA указателя (поле grAttrs будет равно нулю),
причем эта привязка будет идти относительно ImageBase указанного в PE заголовке
и, если загрузка произошла по другому адресу, данное значение нужно будет
пересчитать относительно текущей базы.</li></ol><o:p></o:p><p></p>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Т.е. потребуется вот такая утилитарная функция:<o:p></o:p></p>
<pre class="brush:delphi"> function GetRva(Value: ULONG_PTR64): ULONG_PTR64;
const
dlattrRva = 1;
begin
if DelayDescr.grAttrs = dlattrRva then
Result := Value
else
Result := Value - NtHeader.OptionalHeader.ImageBase;
end;
</pre>
<p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Судя по комментариям в MSDN, RVA адресация началась с VC
7.0, раньше писали как есть в виде прямых VA указателей. Ну а в Delphi сразу
формируют в RVA адресации сразу, как только она появилась. </p>
<p class="MsoNormal"></p><ul style="text-align: left;"><li>- Поле rvaDLLName, отвечает за имя библиотеки, из которой
будет производится импорт.</li><li>- Поле rvaHmod - содержит указатель на поле содержащее
hInstance этой библиотеки и используется в коде инициализации при проверке,
загружена ли уже такая библиотека или нет. Если библиотека была загружена и
позже выгружена, поле обнуляется. Можно было бы использовать в коде анализатора
для проверки инициализации дескриптора, но есть нюанс, а именно - если код
инициализации получил инстанс библиотеки через GetModuleHandle и не производил
реальную загрузку библиотеки, это поле останется равным нулю.</li><li>- Поле rvaIAT, указатель на начало массива адресов
импортируемых функций. Именно этот массив будет контролироваться анализатором.
Размер массива и порядок элементов полностью соответствует аналогичному массиву
из поля rvaINT.</li><li>- Поле rvaINT, указатель на начало массива имен
импортируемых функций. Именно по этому массиву определяется количество
импортируемых функций, т.к. он, в отличие от rvaIAT всегда заканчивается пустым
элементом равным нулю. Так-же как и в обычном импорте вместо имени функции в
этом массиве может лежать Ordinal функции вместо имени, для определения
которого используются те-же маски IMAGE_ORDINAL_FLAG64 и IMAGE_ORDINAL_FLAG32.</li><li>- Поле rvaBoundIAT, теоретически должно использоваться для
настройки массива rvaIAT через связанный импорт, но на практике мне такое не
встречалось и буфер, на который указывает данное поле всегда был пуст.</li><li>- rvaUnloadIAT, содержит указатель на динамически
формируемый массив, отвечающий за восстановление полей массива rvaINT при
выгрузке библиотеки, из которой происходил импорт функций. Т.е. если из этой
библиотеки были проинициализированы только две функции из десяти импортируемых,
будет содержать ровно два элемента указывающих на код, отвечающий за сброс
полей rvaINT каждой из функций обратно на код их инициализации.</li><li>- dwTimeStamp, поле относится к связанному импорту
(rvaBoundIAT) и не интересно.</li></ul><o:p></o:p><p></p>
<p class="MsoNormal">Вот так будет выглядеть таблица отложенного импорта у
текущего демопримера:</p><p class="MsoNormal"><o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjc_EQisecf5C0sFq-lKBzURpVcrO8AQJJE5RS4gKu0dYmt5OIbKAV93IlM09dO7ZvoZbA4eAinrxLrZXiVBEu0i9vCPtZDGagX7J2c8um6G19BQGRon7PRbyQEXOQHRd1-BGd_G2lhosf0-kfjfEkc_FCNY2Xr3J_j1wbJw3ZjnOxl-5wCWUcEmtMI/s1235/19.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1002" data-original-width="1235" height="260" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjc_EQisecf5C0sFq-lKBzURpVcrO8AQJJE5RS4gKu0dYmt5OIbKAV93IlM09dO7ZvoZbA4eAinrxLrZXiVBEu0i9vCPtZDGagX7J2c8um6G19BQGRon7PRbyQEXOQHRd1-BGd_G2lhosf0-kfjfEkc_FCNY2Xr3J_j1wbJw3ZjnOxl-5wCWUcEmtMI/s320/19.png" width="320" /></a></div><br /><p class="MsoNormal">Наглядно видно, что массивы rvaIAT от всех трех дескрипторов
объединены в один сплошной (левая стрелка DIAT), поэтому размерность каждого из
массивов устанавливается по rvaINT (правая стрелка DINT). Ну а то, что на
kernel32.dll ссылаются два дескриптора, это уже особенности как обычного
импорта, так и отложенного импорта, такой вот нюанс.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Для чтения директории отложенного импорта первоначально
необходимо получить её адрес:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi"> with FNtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT] do
begin
FDelayDir.VirtualAddress := RvaToVa(VirtualAddress);
FDelayDir.Size := Size;
end;
</pre>
<p class="MsoNormal">После чего можно писать код чтения:<o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.LoadDelayImport(Raw: TStream): Boolean;
...
begin
Result := False;
Raw.Position := VaToRaw(DelayImportDirectory.VirtualAddress);
if Raw.Position = 0 then Exit;
IntData := 0;
DataSize := IfThen(Image64, 8, 4);
ZeroMemory(@ImportChunk, SizeOf(TImportChunk));
ImportChunk.Delayed := True;
OrdinalFlag := IfThen(Image64, IMAGE_ORDINAL_FLAG64, IMAGE_ORDINAL_FLAG32);
Raw.ReadBuffer(DelayDescr, SizeOf(TImgDelayDescr));
while DelayDescr.rvaIAT <> 0 do
begin
NextDescriptorRawAddr := Raw.Position;
Raw.Position := RvaToRaw(GetRva(DelayDescr.rvaDLLName));
if Raw.Position = 0 then Exit;
ImportChunk.OrigLibraryName := ReadString(Raw);
ProcessApiSetRedirect(ImageName, ImportChunk);
IAT := GetRva(DelayDescr.rvaIAT);
INT := GetRva(DelayDescr.rvaINT);
repeat
Raw.Position := RvaToRaw(INT);;
if Raw.Position = 0 then Exit;
Raw.ReadBuffer(IntData, DataSize);
if IntData <> 0 then
begin
if IntData and OrdinalFlag = 0 then
begin
Raw.Position := RvaToRaw(GetRva(IntData));
if Raw.Position = 0 then Exit;
Raw.ReadBuffer(ImportChunk.Ordinal, SizeOf(Word));
ImportChunk.FuncName := ReadString(Raw);
end
else
begin
ImportChunk.FuncName := EmptyStr;
ImportChunk.Ordinal := IntData and not OrdinalFlag;
end;
ImportChunk.ImportTableVA := RvaToVa(IAT);
Raw.Position := VaToRaw(ImportChunk.ImportTableVA);
if Raw.Position = 0 then Exit;
Raw.ReadBuffer(ImportChunk.DelayedIATData, DataSize);
FImport.Add(ImportChunk);
Inc(IAT, DataSize);
Inc(INT, DataSize);
end;
until IntData = 0;
Raw.Position := NextDescriptorRawAddr;
Raw.ReadBuffer(DelayDescr, SizeOf(TImgDelayDescr));
end;
end;
</pre>
<p class="MsoNormal">Так-же как и в обычном импорте происходит первичная
настройка размеров адресов и флагов для детектирования Ordinal значений
функций, после чего идет последовательное чтение дескрипторов и обязательный
контроль редиректа на виртуальные библиотеки, посредством вызова
ProcessApiSetRedirect. У каждого дескриптора зачитывается массив rvaINT по
которому контролируется размер массива rvaIAT (напоминаю - они
синхронизированы). </p>
<p class="MsoNormal">Теперь нужно подключить вызов этой функции в
TRawPEImage.LoadFromImage и внести изменения в код анализатора.<o:p></o:p></p>
<pre class="brush:delphi"> function CheckRemoteVA: Boolean;
begin
if Import.Delayed then
begin
if HookData.Calculated then
Result :=
(HookData.RemoteVA = Import.DelayedIATData) or
(HookData.RemoteVA = HookData.RawVA)
else
Result := HookData.RemoteVA = Import.DelayedIATData;
end
else
Result := HookData.Calculated and
(HookData.RemoteVA = HookData.RawVA);
end;
...
HookData.RemoteVA := 0;
if Import.Delayed then
HookData.RawVA := Import.DelayedIATData
else
HookData.RawVA := 0;
</pre>
<p class="MsoNormal">Основные изменения произошли в CheckRemoteVA в которой
обрабатывается ситуация что каждое поле в таблице может содержать одно из двух
значений, либо указатель на код инициализации (в таком случае это значение
будет равно Import.DelayedIATData) либо указатель на реальную функцию
(HookData.RawVA если таковая будет найдена среди загруженных библиотек). </p>
<p class="MsoNormal">Если прямо сейчас запустить демо-пример, то результат слегка
удивит:<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN-9JOJfaL78lvK_EV54GHC6-qTZs23QBbS36XJekQRLAwsWNnxdc547hjlDTgtm8Yfp8ilG2WXvvsN-f8Ro4jdy9jToBfzX4zuzs5bjeGPHLf-w3BeWJCLgK5JMuS7bSuPFEyh1NI67DH5WOijZ5nYYF2ckt2uBzutlmOmbATyJ-4_zWS2geiBbAj/s1115/20.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="628" data-original-width="1115" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN-9JOJfaL78lvK_EV54GHC6-qTZs23QBbS36XJekQRLAwsWNnxdc547hjlDTgtm8Yfp8ilG2WXvvsN-f8Ro4jdy9jToBfzX4zuzs5bjeGPHLf-w3BeWJCLgK5JMuS7bSuPFEyh1NI67DH5WOijZ5nYYF2ckt2uBzutlmOmbATyJ-4_zWS2geiBbAj/s320/20.png" width="320" /></a></div><br /><p class="MsoNormal">На экране будут записи о том, что практически каждая
импортированная через отложенный импорт функция перехвачена.</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">Можете сами попробовать раскоментировав директиву
IGNORE_RELOCATIONS в начале модуля RawScanner.ModulesData.<o:p></o:p></p>
<p class="MsoNormal">Если проверить содержимое таких полей в памяти процесса на
соответствие тем числам который записаны в теле библиотек, то действительно
будут расхождения:<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjeUgR0Ke_LgPC_a9z-Ljq83m0AVX19g-XtAGw7YtY4ouzCJ-YtfaXeJwacXTwpXJ_lDTWyDSho0P-xseNUxQ2N0vKLxm5a3EXdm-UrF9bVFv41GSlo5RJO4pzYnVsHPLer6LJE_JztmQFfLB9BOk08bRMiqIrUs_Yk7d2y9GLe4UNCN37bBvCACtu0/s959/21.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="622" data-original-width="959" height="208" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjeUgR0Ke_LgPC_a9z-Ljq83m0AVX19g-XtAGw7YtY4ouzCJ-YtfaXeJwacXTwpXJ_lDTWyDSho0P-xseNUxQ2N0vKLxm5a3EXdm-UrF9bVFv41GSlo5RJO4pzYnVsHPLer6LJE_JztmQFfLB9BOk08bRMiqIrUs_Yk7d2y9GLe4UNCN37bBvCACtu0/s320/21.png" width="320" /></a></div><br /><p class="MsoNormal">А расхождения будут по причине того, что изначальные адреса
в массиве rvaIAT которые указывают на код инициализации каждой импортируемой
функции записаны в VA адресации, т.е. с учетом базы модуля. На скриншоте это
наглядно видно, в памяти библиотека расположена по адресу 0х762D0000, но в PE
заголовке её база указана как 0х6B800000 отсюда и разница в значениях, на
которой у анализатора идет промах. </p>
<p class="MsoNormal">Выйти из этой ситуации можно двумя способами, не правильным,
пересчитав адреса руками из старой ImageBase в новую, и правильным - подключив
обработку таблицы релокаций, расположенной в отдельной директории
IMAGE_DIRECTORY_ENTRY_BASERELOC. </p>
<p class="MsoNormal">Как она работает: в коде приложения есть предостаточно мест,
где встречается прямая VA адресация, причем встречается прямо в коде тела
приложения, где налету уже не пересчитать разность баз загрузки, вот тут и
выручает таблица релокаций, которую загрузчик обрабатывает при старте.<o:p></o:p></p>
<p class="MsoNormal">Грубо все адресное пространство процесса разделено на
страницы размером в 4096 байт (диапазон адресов в рамках страницы 0..0xFFF), а
таблица релокаций содержит в себе информацию о всех таких страницам и смещениям
в них (в этом-же диапазоне) по которым будут расположены базозависимые адреса. </p>
<p class="MsoNormal">Выглядит это следующим образом, в директории
IMAGE_DIRECTORY_ENTRY_BASERELOC идет массив структур: </p>
<pre class="brush:delphi"> TImageBaseRelocation = record
VirtualAddress: DWORD;
SizeOfBlock: DWORD;
end;
</pre>
<p class="MsoNormal">Поле VirtualAddress, содержит RVA адрес, который указывает на
конкретную страницу, содержащую адреса требующие коррекции базы.<o:p></o:p></p>
<p class="MsoNormal">Поле SizeOfBlock содержит общий размер (в байтах) всех
смещений в рамках страницы плюс размер самого заголовка. Т.е. грубо если на
странице встречаются шесть адресов которым требуется пересчет, SizeOfBlock =
Count * SizeOf(Word) + SizeOf(TImageBaseRelocation) = 20 байт.<o:p></o:p></p>
<p class="MsoNormal">Сами блоки представляют из себя двухбайтовое значение в
котором старшие 4 бита являются флагом определяющим тип блока, а оставшиеся 12
бит непосредственно офсетом в диапазоне 0..0хFFF. </p>
<p class="MsoNormal">Типов блоков много, но реально в РЕ файле будет всего три: </p>
<pre class="brush:delphi">const
IMAGE_REL_BASED_ABSOLUTE = 0;
IMAGE_REL_BASED_HIGHLOW = 3;
IMAGE_REL_BASED_DIR64 = 10;
</pre>
<p class="MsoNormal">Первый используется для выравнивания и не содержит никакой
полезной информации, а остальные два применяются в 32 битных и 64 битных
образах, указывая на то, что в младших 12 битах содержится офсет. </p>
<p class="MsoNormal">Код, читающий таблицу релокаций достаточно тривиален: </p>
<pre class="brush:delphi">function TRawPEImage.LoadRelocations(Raw: TStream): Boolean;
...
begin
FRelocationDelta := ImageBase - FNtHeader.OptionalHeader.ImageBase;
if not Image64 then
FRelocationDelta := DWORD(FRelocationDelta);
Result := FRelocationDelta = 0;
if Result then Exit;
Reloc := FNtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (Reloc.VirtualAddress = 0) or (Reloc.Size = 0) then Exit;
Raw.Position := RvaToRaw(Reloc.VirtualAddress);
if Raw.Position = 0 then Exit;
MaxPos := Raw.Position + Reloc.Size;
while Raw.Position < MaxPos do
begin
Raw.ReadBuffer(ImageBaseRelocation, SizeOf(TImageBaseRelocation));
Dec(ImageBaseRelocation.SizeOfBlock, SizeOf(TImageBaseRelocation));
for I := 0 to Integer(ImageBaseRelocation.SizeOfBlock shr 1) - 1 do
begin
Raw.ReadBuffer(RelocationBlock, SizeOf(Word));
case RelocationBlock shr 12 of
IMAGE_REL_BASED_HIGHLOW,
IMAGE_REL_BASED_DIR64:
FRelocations.Add(Pointer(RvaToRaw(ImageBaseRelocation.VirtualAddress + RelocationBlock and $FFF)));
end;
end;
end;
Result := True;
end;
</pre>
<p class="MsoNormal">Самой первой строкой рассчитывается дельта, означающая
разницу между текущей базой загрузки и изначально указанной в РЕ заголовке.<o:p></o:p></p>
<p class="MsoNormal">Если разница отсутствует (равна нулю), т.е образ был
загружен по тому адресу, какой указал разработчик при компиляции, то смысла
грузить таблицу релокаций нет, т.к. все базозависимые адреса содержат
актуальные значения.<o:p></o:p></p>
<p class="MsoNormal">Ну а дальше последовательно читаются все записи по каждой
странице, на основе размера, указанного в заголовке каждой
TImageBaseRelocation.<o:p></o:p></p>
<p class="MsoNormal">Реальный адрес, который требует перерасчета, равен RVA
адресу страницы + офсету, указанному в блоке.<o:p></o:p></p>
<p class="MsoNormal">Информация по каждой отдельной странице идет сразу же за
окончанием данных по предыдущей, для выравнивания используются блоки
IMAGE_REL_BASED_ABSOLUTE. </p>
<p class="MsoNormal">После того как таблица релокаций прочитана, её необходимо
применить к текущему образу файла, считанного с диска:<o:p></o:p></p>
<pre class="brush:delphi">procedure TRawPEImage.ProcessRelocations(AStream: TStream);
...
begin
if FRelocationDelta = 0 then Exit;
Reloc := 0;
AddrSize := IfThen(Image64, 8, 4);
for var RawReloc in FRelocations do
begin
AStream.Position := Int64(RawReloc);
AStream.ReadBuffer(Reloc, AddrSize);
Inc(Reloc, FRelocationDelta);
AStream.Position := Int64(RawReloc);
AStream.WriteBuffer(Reloc, AddrSize);
end;
end;
</pre>
<p class="MsoNormal">Код вообще тривиальный, просто бежим по рассчитанным при
чтении таблицы релокаций адресам и прибавляем к каждому дельту.<o:p></o:p></p>
<p class="MsoNormal">И теперь, если добавить перед загрузкой таблицы отложенного
импорта следующий код, то демо-пример заработает так как нужно:<o:p></o:p></p>
<pre class="brush:delphi">procedure TRawPEImage.LoadFromImage;
...
begin
if LoadRelocations(Raw) then
ProcessRelocations(Raw);
LoadDelayImport(Raw);
...
end;
</pre>
<p>Код к седьмой главе для самостоятельного изучения <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%207" target="_blank">доступен по этой ссылке</a>.</p><p><br /></p>
<a name="tls"></a>
<h3 style="text-align: left;">8. TLS калбэки и детектирование модификации кода.</h3><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>
<p class="MsoNormal">На текущем этапе анализатору доступна практически вся
необходимая информация об удаленном процессе, за исключением адресов TLS
калбэков, которые выполняются ДО точки входа каждого модуля. Про них я <a href="http://alexander-bagel.blogspot.com/2016/03/early-execution.html">уже
писал ранее</a> поэтому останавливаться на разъяснениях не буду, сразу покажу
код для получения их адресов. </p>
<p class="MsoNormal">Адреса калбэков расположены в виде массива, на начало
которого указывает структура _IMAGE_TLS_DIRECTORY32/64, а если точнее её поле
AddressOfCallBacks. Данная структура расположена в директории
IMAGE_DIRECTORY_ENTRY_TLS, поэтому первым шагом надо получить её адрес:<o:p></o:p></p>
<pre class="brush:delphi"> with FNtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS] do
begin
FTlsDir.VirtualAddress := RvaToVa(VirtualAddress);
FTlsDir.Size := Size;
end;
</pre>
<p class="MsoNormal">Сама структура выглядит следующим образом (на примере 64
битной её версии):<o:p></o:p></p>
<pre class="brush:delphi"> _IMAGE_TLS_DIRECTORY64 = record
StartAddressOfRawData: ULONGLONG;
EndAddressOfRawData: ULONGLONG;
AddressOfIndex: ULONGLONG; // PDWORD
AddressOfCallBacks: ULONGLONG; // PIMAGE_TLS_CALLBACK *;
SizeOfZeroFill: DWORD;
Characteristics: DWORD;
end;
</pre>
<p class="MsoNormal">Первые четыре поля в действительности являются указателями,
поэтому размер в зависимости от битности образа исполняемого файла меняется (4
и 8 байт).<o:p></o:p></p>
<p class="MsoNormal">Адреса, содержащиеся в данных полях, идут в VA адресации и
загрузчик при старте приложения (точнее при загрузке модуля) производит их
актуализацию посредством секции релоков.<o:p></o:p></p>
<p class="MsoNormal">Поля StartAddressOfRawData, EndAddressOfRawData и
SizeOfZeroFill отвечают за размеры шаблона TLS, это грубо говоря блок данных,
который копируется в TLS секцию каждого потока структуры TEB (Thread
Environment Block) при старте потока.<o:p></o:p></p>
<p class="MsoNormal">Чтобы было понятней - при помощи этого механизма работают
такие вещи как threadvar, т.е. переменные, которые содержат данные,
принадлежащие только текущему потоку.<o:p></o:p></p>
<p class="MsoNormal">AddressOfIndex и Characteristics - тоже относятся к этому
механизму и не интересны, все что нужно получить из этой структуры, это
значение поля AddressOfCallBacks. Он указываем на массив VA адресов заканчивающийся нулем. </p>
<p class="MsoNormal">В памяти процесса это выглядит вот таким образом:<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUTKHShCPph4sCCTdgWea9vNAP0wAdsNbF_fGmOa4ou5gZSfEkmrc4kRUXRLdz6KGZ-tDZ2PieQ51QlMFMz4RpIQym_lc5tTj8cYTnzPcuANoh99mgPr0jVXEPyK3iMYnI3-z_zLYQXPJXpzcqVlYB_X3E97gEruciOjx1S-TlckN1UfyjtpAtARLK/s819/22.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="571" data-original-width="819" height="223" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUTKHShCPph4sCCTdgWea9vNAP0wAdsNbF_fGmOa4ou5gZSfEkmrc4kRUXRLdz6KGZ-tDZ2PieQ51QlMFMz4RpIQym_lc5tTj8cYTnzPcuANoh99mgPr0jVXEPyK3iMYnI3-z_zLYQXPJXpzcqVlYB_X3E97gEruciOjx1S-TlckN1UfyjtpAtARLK/s320/22.png" width="320" /></a></div><br /><p class="MsoNormal">Сам код загрузки TLS калбэков:</p><p class="MsoNormal"><o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.LoadTLS(Raw: TStream): Boolean;
function TlsVaToRva(Value: ULONG_PTR64): DWORD;
begin
Result := Value - NtHeader.OptionalHeader.ImageBase
end;
begin
Result := False;
Raw.Position := VaToRaw(TlsDirectory.VirtualAddress);
if Raw.Position = 0 then Exit;
AddrSize := IfThen(Image64, 8, 4);
// пропускаем 3 поля IMAGE_TLS_DIRECTORYхх:
// StartAddressOfRawData + EndAddressOfRawData + AddressOfIndex
// становясь, таким образом, сразу на позиции AddressOfCallBacks
Raw.Position := Raw.Position + AddrSize * 3;
Counter := 0;
TlsCallbackRva := 0;
// зачитываем значение AddressOfCallBacks
Raw.ReadBuffer(TlsCallbackRva, AddrSize);
// если цепочка колбэков не назначена - выходим
if TlsCallbackRva = 0 then Exit;
// позиционируемся на начало цепочки калбэков
Raw.Position := RvaToRaw(TlsVaToRva(TlsCallbackRva));
if Raw.Position = 0 then Exit;
repeat
Raw.ReadBuffer(TlsCallbackRva, AddrSize);
if TlsCallbackRva <> 0 then
begin
Chunk.EntryPointName := 'Tls Callback ' + IntToStr(Counter);
Chunk.AddrVA := RvaToVa(TlsVaToRva(TlsCallbackRva));
Chunk.AddrRaw := VaToRaw(Chunk.AddrVA);
FEntryPoints.Add(Chunk);
Inc(Counter);
end;
until TlsCallbackRva = 0;
end;
</pre>
<p class="MsoNormal">Он очень простой, читается поле
IMAGE_TLS_DIRECTORYхх.AddressOfCallBacks и если оно не равно нулю, то
вычитываются все адреса, пока не дойдем до нулевого. </p>
<p class="MsoNormal">Теперь несмотря на то, что все необходимые анализатору
адреса получены, нужно выполнить некоторые подготовительные действия.<o:p></o:p></p>
<p class="MsoNormal">Во первых - часть известных анализатору функций не являются
исполняемыми, например экспортируемые NTDLL.DLL функции NlsMbCodePageTag и
NlsMbOemCodePageTag в действительности являются указателями на таблицы кодовых
страниц, которые сформирует загрузчик и в образе данной библиотеки вообще
указывают в пустоту между секциями .data и .pdata, а это приводит к тому что
невозможно узнать их RAW адрес, т.к. он по факту отсутствует, ибо RVA не
принадлежит ни одной из секций.<o:p></o:p></p>
<p class="MsoNormal">Ну или функция RtlNtdllName, которая является указателем на
строку "ntdll.dll". </p>
<p class="MsoNormal">Такие функции нужно отметить, как "не
исполняемые", чтобы анализатор пропускал их при проверке, иначе будет
FalsePositive реакция.<o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.IsExecutable(RvaAddr: DWORD): Boolean;
const
ExecutableCode = IMAGE_SCN_CNT_CODE or IMAGE_SCN_MEM_EXECUTE;
...
begin
Result := GetSectionData(RvaAddr, SectionData);
if Result then
begin
PointerToRawData := FSections[SectionData.Index].PointerToRawData;
if FNtHeader.OptionalHeader.SectionAlignment >= DEFAULT_SECTION_ALIGNMENT then
PointerToRawData := AlignDown(PointerToRawData, DEFAULT_FILE_ALIGNMENT);
Inc(PointerToRawData, RvaAddr - SectionData.StartRVA);
Result :=
(PointerToRawData < FSizeOfFileImage) and
(FSections[SectionData.Index].Characteristics and ExecutableCode = ExecutableCode);
end;
end;
</pre>
<p class="MsoNormal">Задача этой функции проверить, принадлежит ли переданный
адрес какой-либо секции, и если принадлежит, то уточнить - выставлены ли у
секции флаги наличия кода и разрешения на исполнение. Каждая функция, полученная через таблицу экспорта, должна
быть проверена этим кодом. </p>
<p class="MsoNormal">Во-вторых, есть один очень сложный момент, который не
получилось у меня "нормально" решить, дело в том, что в памяти
процесса может быть загружено несколько одинаковых библиотек, но из разных
директорий. Особенно это актуально для comctl32.dll.<o:p></o:p></p>
<p class="MsoNormal">К примеру приложение без манифеста загрузит comctl32.dll
пятой версии, работает и вдруг в какой-то момент времени загружает еще одну
библиотеку, у которой в таблице импорта указана HIMAGELIST_QueryInterface,
которая отсутствует в пятой comctl32, но вполне себе присутствует в шестой, и
именно шестая версия comctl32.dll и будет загружена в адресное пространство
процесса, а весь остальной импорт у этой новой библиотеки будет перенаправлен
либо на comctl32.dll от шестой версии, либо пятой, либо вообще в разнобой (и
такая ситуация встретилась). Все зависит от конкретной реализации загрузчика в
текущей операционной системе.<o:p></o:p></p>
<p class="MsoNormal">Другая ситуация: например я делаю библиотеку A.DLL которая
статически слинкована на библиотеку B.DLL через таблицу импорта, после чего,
делаю две копии библиотеки B.DLL в разных папках и загружаю их обе через
LoadLibrary(), после чего гружу уже A.DLL и тут опять не понятно, в зависимости
от ОС адреса импорта в A.DLL будут направлены либо на первую загруженную B.DLL,
либо на вторую, причем еще есть нюанс - если одна из B.DLL будет расположена в
папке с A.DLL то загрузчик будет линковать импорт уже на неё, причем если она
не загружена - то с одновременной загрузкой. <o:p></o:p></p>
<p class="MsoNormal">А еще же есть Hard-import link, когда у импортируемой
функции указывается либо относительный, либо полный путь к библиотеке, такие
ситуации загрузчик обрабатывает особым способом. </p>
<p class="MsoNormal">В итоге я решил не переусложнять код и поступил следующим
образом. У класса TRawPEImage ввел поле RelocatedImages:
TList<TRawPEImage> а в методе TRawModules.AddImage добавил проверку, если
образ с таким именем уже присутствует в списке загруженных, то он добавляется
не в общий список, а в список RelocatedImages уже присутствующего модуля. После
чего реализовал следующие две функции:<o:p></o:p></p>
<pre class="brush:delphi">function CheckImageAtAddr(Image: TRawPEImage; CheckAddrVA: ULONG_PTR64): Boolean;
begin
Result := (Image.ImageBase < CheckAddrVA) and
(Image.ImageBase + UInt64(Image.VirtualSizeOfImage) > CheckAddrVA);
end;
function TRawPEImage.GetImageAtAddr(AddrVA: ULONG_PTR64): TRawPEImage;
begin
Result := Self;
if (RelocatedImages.Count > 0) and not CheckImageAtAddr(Self, AddrVA) then
begin
for var Index := 0 to RelocatedImages.Count - 1 do
if CheckImageAtAddr(RelocatedImages[Index], AddrVA) then
begin
Result := RelocatedImages[Index];
Break;
end;
end;
end;
</pre>
<p class="MsoNormal">Первая проверяет, принадлежит ли переданный VA адрес области
памяти, в которой располагается идущий первым параметром образ РЕ файла.<o:p></o:p></p>
<p class="MsoNormal">Вторая же просто идет по списку альтернативных образов и
ищет тот, которому принадлежит адрес, возвращая результатом либо себя, либо
перенаправленный РЕ файл.<o:p></o:p></p>
<p class="MsoNormal">Дешево и сердито :) </p>
<p class="MsoNormal">Третий момент, код проверяемой функции может быть очень
маленький, причем сама функция может быть расположена в самом конце секции.
Если проверять модификацию тела функций сверкой первых 64 байт, то может
произойти ситуация что эти 64 байта захватят данные, которые в физическом
образе файла уже принадлежат совершенно другой секции, т.е. когда файл будет
загружен, эти данные будут расположены по совершенно другому адресу, не так как
в бинарном файле.<o:p></o:p></p>
<pre class="brush:delphi"> dclIndyProtocols270.bpl export: Finalize
Expected: B8 1C 4B 42 23 E8 F2 BB FF FF C3 90 ! FF 25 04 A1 42 23 8B C0...
Present: B8 1C 4B 42 23 E8 F2 BB FF FF C3 90 ! 00 00 00 00 00 00 00 00...
</pre>
<p class="MsoNormal">Например, вот так выглядит код функции Finalize, строчка
Expected указывает на данные, которые были прочитаны из образа файла на диске,
а строчка Present на прочитанные из памяти. Восклицательным знаком я отделил данные,
которые относятся к следующей секции. Если вы разбираетесь в машинных кодах то
сразу можете заметить что последние два байта перед восклицательным знаком
соответствуют опкодам инструкций RET И NOP, и можно на их основе узнать об
окончании тела функции, но дело в том что вместо них там может быть и JMP, а
так-же другие варианты передачи управления ранее по коду, да и подключение в
код фреймворка собственного дизассемблера я решил избыточным чтобы разбираться
с такими ситуациями, поэтому поступил проще, а именно добавил следующую
функцию:<o:p></o:p></p>
<pre class="brush:delphi">function TRawPEImage.FixAddrSize(AddrVA: ULONG_PTR64;
var ASize: DWORD): Boolean;
...
begin
AddrRva := VaToRva(AddrVA);
Result := GetSectionData(AddrRva, Data);
if Result then
begin
if Data.StartRVA + Data.Size < AddrRva + ASize then
ASize := Data.StartRVA + Data.Size - AddrRva;
end;
end;
</pre>
<p class="MsoNormal">Её задача проверять выход VA адреса за диапазон секции, и
корректировать переданный размер. </p>
<p class="MsoNormal">И вот только теперь можно расширить анализатор, добавив в
него проверку тела функций на их модификацию:<o:p></o:p></p>
<pre class="brush:delphi">procedure TPatchAnalyzer.CompareBinary(AddrVa: ULONG_PTR64; AddrRaw: DWORD;
const FuncName: string; Module: TRawPEImage);
const
DefaultBuffSize = 64;
...
begin
Inc(FAnalizeResult.Code.Scanned);
SetLength(RawBuff, DefaultBuffSize);
SetLength(RemoteBuff, DefaultBuffSize);
BuffSize := DefaultBuffSize;
Module.FixAddrSize(AddrVA, BuffSize);
// зачитываем блок из файла
FRaw.Position := AddrRaw;
FRaw.ReadBuffer(RawBuff[0], BuffSize);
// и из памяти
if not ReadRemoteMemory(FProcessHandle, AddrVA,
@RemoteBuff[0], BuffSize) then
Exit;
if not CompareMem(@RawBuff[0], @RemoteBuff[0], BuffSize) then
begin
Data.Patched :=
CheckPageSharing(AddrVA, SharedCount) and
(SharedCount = 0);
// блоки не совпали
// отдаем на анализ внешнему обработчику, если таковой назначен
if Assigned(FProcessCodeHook) then
begin
Data.ProcessHandle := FProcessHandle;
Data.Image64 := Module.Image64;
Data.ImageBase := Module.ImageBase;
Data.ExportFunc := ChangeFileExt(Module.ImageName, '.' + FuncName);
Data.AddrVA := AddrVA;
Data.RawOffset := AddrRaw;
Data.Raw := @RawBuff[0];
Data.Remote := @RemoteBuff[0];
Data.BufSize := BuffSize;
FProcessCodeHook(Data);
end;
end;
end;
</pre>
<p class="MsoNormal">Функция очень простая по своей сути, на вход приходит два
адреса, AddrVa - адрес функции в адресном пространстве процесса, AddrRaw -
офсет в образе файла на диске. По этим адресам читается 64 байта (или меньше -
коррекция размера идет через FixAddrSize) после чего оба блока сравниваются.
При расхождении вызывается внешний обработчик, который будет разбираться, что
именно с этими буферами не так. </p>
<p class="MsoNormal">Подключить вызов этой процедуры нужно в двух местах, в
сканировании экспорта, добавив в конец следующий кусок кода:<o:p></o:p></p>
<pre class="brush:delphi"> // 1. Если функция перенаправлена в другой модуль - пропускаем проверку
if Exp.OriginalForvardedTo <> EmptyStr then
begin
Inc(FAnalizeResult.Code.Skipped);
Continue;
end;
// 2. Если функция не содержит кода - пропускаем проверку
if not Exp.Executable then
begin
Inc(FAnalizeResult.Code.Skipped);
Continue;
end;
// после того как удостоверились что запись в таблице экспорта валидная
// и не перенаправлена в другой модуль, то тогда
// проверяем бинарный код функции
CompareBinary(Exp.FuncAddrVA, Exp.FuncAddrRaw, Exp.ToString, Module);
</pre>
<p class="MsoNormal">Этим будет проверятся тело каждой экспортируемой функции
каждого известного анализатору модуля.<o:p></o:p></p>
<p class="MsoNormal">А также сделать новый метод, в котором будет производиться
сканирование всех EntryPoint и TLS калбэков загруженных модулей: </p>
<pre class="brush:delphi">procedure TPatchAnalyzer.ScanEntryPoints(Index: Integer; Module: TRawPEImage);
begin
for var I := 0 to Module.EntryPointList.Count - 1 do
with Module.EntryPointList.List[I] do
CompareBinary(AddrVA, AddrRaw, EntryPointName, Module);
end;
</pre>
<p class="MsoNormal">Причем вызываться она будет для всех модулей, которые не
являются COM+ (содержащими только IL код)<o:p></o:p></p>
<pre class="brush:delphi"> if not Module.ComPlusILOnly then
ScanEntryPoints(Index, Module);
</pre>
<p class="MsoNormal">И в заключение, пожалуй, стоит рассмотреть код самого
калбэка, в котором принимается решение - была ли модификация тела функции или нет.<o:p></o:p></p>
<p class="MsoNormal">Для его работы по-хорошему нужен полноценный дизассемблер,
но в рамках статьи я посчитал это избыточным (она и так вышла достаточно
объемная) поэтому заменил его на простенький дизассемблер длин, взяв первый
попавшийся на GIT и портировав его на Delphi. Он такой - неказистый, ошибается
на некоторых сложных инструкциях, впрочем, его задача попытаться подсказать
коду в калбэке где может находится инструкция RET, означающая конец функции и с
ней он в 98 процентов случаев справляется успешно.<o:p></o:p></p>
<p class="MsoNormal">Код обработчика (если подсократить проверки) будет
следующий:<o:p></o:p></p>
<pre class="brush:delphi">procedure ProcessCodeHook(const Data: TCodeHookData);
...
begin
// поиск конца функции
rawCursor := Data.Raw;
I := Data.BufSize;
while I > 0 do
begin
OpcodeLen := ldisasm(rawCursor, Data.Image64);
Dec(I, OpcodeLen);
// просто ищем инструкцию RET
if (OpcodeLen = 1) and (rawCursor^ = $C3) then
Break;
Inc(rawCursor, OpcodeLen);
end;
if CompareMem(Data.Raw, Data.Remote, Data.BufSize - I) then
Exit;
Writeln(' Expected: ' + ByteToHexStr(Data.Raw, Data.BufSize - I));
Writeln(' Present: ' + ByteToHexStr(Data.Remote, Data.BufSize - I));
end;
</pre>
<p class="MsoNormal">Задача данного кода распознать конец "коротких"
функций (опираясь на дизассемблер длин инструкций) и при нахождении такового
выполнить повторную проверку.<o:p></o:p></p>
<p class="MsoNormal">Дело в том, что, когда идут две три инструкции подряд, из
которых изменена только последняя, без такого дополнительного контроля длины
функции все три будут выведены как измененные, а здесь идет подстраховка для
такой ситуации.<o:p></o:p></p>
<p class="MsoNormal">И теперь, если запустить демо-пример на выполнение из под
отладчика, он (будучи уже настроенным на сканирование процесса родителя)
покажет состояние запущенной Delphi (ну или проводника если запуск будет из под
него).<o:p></o:p></p>
<p class="MsoNormal">Правда придется немного подождать - сканирование такого
тяжелого процесса как bds.exe, с обильным рантаймом, на который нет системного
кэша, не сильно быстрое по времени и может занять секунд 20-30.<o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhx3dXkQQOug9h2ubhd6ImJOCBTSwMJHjD8xon_uMJ7Eg3T8U-d-Qod0YgXZ6WPtZ8DN_hVR0iSNBpNkZLjsDkwI5Me2TG5rrxYsDggBzLOsKBtepbF_kryvDax3_YM1NcKOdZ1mLV0QbhMh0s7yU3VtwfnMLRaH3Annt2jUXvWq8Gyfx_MhJxWsarr/s1189/23.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="831" data-original-width="1189" height="224" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhx3dXkQQOug9h2ubhd6ImJOCBTSwMJHjD8xon_uMJ7Eg3T8U-d-Qod0YgXZ6WPtZ8DN_hVR0iSNBpNkZLjsDkwI5Me2TG5rrxYsDggBzLOsKBtepbF_kryvDax3_YM1NcKOdZ1mLV0QbhMh0s7yU3VtwfnMLRaH3Annt2jUXvWq8Gyfx_MhJxWsarr/s320/23.png" width="320" /></a></div><br /><p class="MsoNormal">Что из неё можно получить:</p><p class="MsoNormal"><o:p></o:p></p>
<p class="MsoNormal">1. самая первая запись говорит о том, что в rtl270.bpl в
таблице импорта перехвачена запись ведущая на kernel32.RaiseException и
перехватчик ведет куда-то вглубь exceptiondiag270.bpl<o:p></o:p></p>
<p class="MsoNormal">Это работа самой delphi, которая при возникновении
исключений в DesignTime наконец то начала показывать хоть какой-то более-менее
читаемый стек, ведущий к ошибке.<o:p></o:p></p>
<p class="MsoNormal">2. далее идут две модифицированных функции из того же
rtl270.bpl, это HandleAutoException и RaiseLastOSError. Это постаралась
установленная на моей машине EurekaLog установив перехватчик прямо в теле
функций, ведущий куда-то вглубь EurekaLogExpert270.bpl. На скриншоте, конечно,
этого не видно, т.к. показаны только модифицированные байты начала функций, но,
если их обработать через дизассемблер, картина станет более понятной.<o:p></o:p></p>
<p class="MsoNormal">3. Ну и наконец кто-то влез в win32debugide270.bpl
модифицировав начало функции TNativeDebugger.DoShowException. Это уже
установленный у меня GExpert балуется, перенаправляя вызов функции вглубь
GExpertsRS104.dll чтобы вместо штатного окошка показывать расширенное с
кнопками "игнорировать данный тип исключения" и т.п. - кстати удобная
вешь!<o:p></o:p></p>
<p class="MsoNormal">Если же подключить полноценный дизассемблер, то все перехваты
будут выведены в более приемлемом виде:</p><p class="MsoNormal"><o:p></o:p></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgneUzWKGCpLAl9Nyfz-wGq_eqMyIZ22rjZZlcmHkFfi3fAqwFgYdVP6TjTr2WQqy9PYFBszeOVHdb6B6coe4sIeljfRr10m7SzM0Vpsc1902QM7-hZi2LgnQj6OeVYLEuk71rQdv2A7zZuq7RgJ8XSBV0fKTRugfv8K-2fYVroce78cvBHmhaMi3wE/s1162/24.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="745" data-original-width="1162" height="205" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgneUzWKGCpLAl9Nyfz-wGq_eqMyIZ22rjZZlcmHkFfi3fAqwFgYdVP6TjTr2WQqy9PYFBszeOVHdb6B6coe4sIeljfRr10m7SzM0Vpsc1902QM7-hZi2LgnQj6OeVYLEuk71rQdv2A7zZuq7RgJ8XSBV0fKTRugfv8K-2fYVroce78cvBHmhaMi3wE/s320/24.png" width="320" /></a></div><br />
<p class="MsoNormal">Вот теперь на руках есть готовый инструмент, который можно в принципе даже в текущем виде спокойно подключить к своему проекту. Попробуйте с ним "поиграться" подключаясь к активным процессам и понаблюдать что именно в них происходит. Иногда встречаются очень интересные и неожиданные вещи.</p><p class="MsoNormal">Одна из них состоит в том - что половина библиотек, использующихся в составе вашего ПО могут быть заменены прямо на лету. Я даже сам не знал о таком поведении, но его легко можно воспроизвести установив редистрибутейлы от майкрософт, которые в процессе установки должны заменить часть библиотек на обновленные аналоги (ну или отработает теневое обновление не требующее перезагрузки Windows). Если в этот момент произвести сканирование памяти процесса, то можно обнаружить что часть библиотек перемаплена на их старые образы, которые ОС перенесла при обновлении в каталог C:\Config.Msi\<br />Например msvcp140.dll вдруг стала C:\Config.Msi\5761a70b.rbf и т.д. а вот в списках загрузчика изменений не произошло и там честно указано что по такому-то адресу лежит msvcp140.dll.<br />Эта ситуация не обрабатывается кодом анализатора, но о ней нужно знать, если вдруг кто захочет добавить детект изменений после обновления библиотек.<br /><br />Ну а код к заключительной главе доступен для самостоятельного изучения <a href="https://github.com/AlexanderBagel/articles/tree/main/raw_scanner/part%208" target="_blank">по этой ссылке</a>.</p><p class="MsoNormal"><br /></p>
<a name="epilog"></a>
<h3 style="text-align: left;">9. В качестве заключения</h3><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p> </o:p></p><p class="MsoNormal">В итоге по шагам, чтобы произвести полноценный анализ
изменений данных в стороннем процессе нужно:<o:p></o:p></p><p class="MsoNormal"></p><ol style="text-align: left;"><li>уметь читать данные невзирая на битность текущего и
удаленного процессов</li><li>уметь читать списки загрузчика из удаленного процесса</li><li>уметь читать таблицы экспорта/импорта/отложенного импорта,
а также точек входа и TLS калбэков</li><li>уметь обрабатывать форвард перенаправления функций и
ApiSet редиректы</li></ol><o:p></o:p><p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Если кто-то будет более подробно разбираться с тематикой
статьи не забывайте, что я специально разбил код на главы и наращивал его от
главы к главе (через Merge можно сравнить две папки и увидеть, что
добавилось/изменилось для каждой главы).</p><p class="MsoNormal"><o:p></o:p></p><h4 style="text-align: left;">Вполне вероятно, что вам пригодятся следующие ссылки: </h4><p class="MsoNormal">По формату исполняемых файлов:<o:p></o:p></p><p class="MsoNormal"><a href="https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/march/inside-windows-an-in-depth-look-into-the-win32-portable-executable-file-format-part-2">An In-Depth Look into the Win32 Portable Executable File Format, Part 2</a><br /><a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format">PE Format</a><br /><a href="https://wasm.in/blogs/ot-zelenogo-k-krasnomu-glava-2-format-ispolnjaemogo-fajla-os-windows-pe32-i-pe64-sposoby-zarazhenija-ispolnjaemyx-fajlov.390/">От зеленого к красному: Глава 2: Формат исполняемого файла ОС Windows. PE32 и PE64. Способы заражения исполняемых файлов.</a></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Дополнительная информация по ApiSet: </p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><a href="https://lucasg.github.io/2017/10/15/Api-set-resolution/">Api set resolution</a><br /><a href="https://blog.quarkslab.com/runtime-dll-name-resolution-apisetschema-part-i.html">Runtime DLL name resolution: ApiSetSchema — Part I</a><br /><a href="https://blog.quarkslab.com/runtime-dll-name-resolution-apisetschema-part-ii.html">Runtime DLL name resolution: ApiSetSchema — Part II</a><br /><a href="https://github.com/lucasg/Dependencies/blob/master/ClrPhlib/include/ApiSet.h">https://github.com/lucasg/Dependencies/blob/master/ClrPhlib/include/ApiSet.h</a></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Немного по работе загрузчика:</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><a href="https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/march/windows-2000-loader-what-goes-on-inside-windows-2000-solving-the-mysteries-of-the-loader">What Goes On Inside Windows 2000: Solving the Mysteries of the Loader</a><br /><a href="https://doxygen.reactos.org/d1/d97/ldrtypes_8h_source.html">https://doxygen.reactos.org/d1/d97/ldrtypes_8h_source.html</a><br /><a href="https://geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntldr/ldr_data_table_entry.htm">LDR_DATA_TABLE_ENTRY</a></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Подробная статья по отложенной загрузке библиотек:</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><a href="https://learn.microsoft.com/en-us/cpp/build/reference/understanding-the-helper-function?view=msvc-170">Understand the delay load helper function</a><o:p></o:p></p><p class="MsoNormal">О порядке поиска библиотек динамической компоновки:</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><a href="https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order">Dynamic-link library search order</a><o:p></o:p></p><p class="MsoNormal">Для любителей погрузиться в тему, информация о методе
динамического переключения контекста кода:</p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal"><a href="https://rce.co/knockin-on-heavens-gate-dynamic-processor-mode-switching/">Knockin’ on Heaven’s Gate – Dynamic Processor Mode Switching</a><br /><a href="https://www.codeproject.com/Articles/5262969/How-to-Hook-64-Bit-Code-from-WOW64-32-Bit-Mode">How to Hook 64-Bit Code from WOW64 32-Bit Mode</a></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">Вообще задумка получилась достаточно удобной, полгода назад,
когда я реализовал первый вариант данного фреймворка я тестировал его на многих
процессах, запущенных у меня на виртуалках и он четко показывал, как тот-же
офис, или браузеры делают у себя внутри "песочницу" чтобы не
допустить побега кода наружу, перехватывая кучу функций на самих себя. Или как
идет работа с отложеным импортом с перенаправлениями на заглушки, просто
выставляющие код ошибки (в дельфи штатно такое не получится сделать - только
ручками). Ну или показывал работу защиты ПО, например высвечивая перехват
DbgUiRemoteBreakin с перенаправлением на TerminateProcess (типа защита от
аттача отладчика к активному процессу) :) </p><p class="MsoNormal">Код для восьмой главы в принципе является абсолютно
самодостаточным и его можно спокойно использовать в собственных проектах, или
можно взять уже расширенную версию данного кода, которую я применяю в своих
проектах. <o:p></o:p></p><p class="MsoNormal">В ней уже есть работа с внешним дизассемблером, логирование,
фильтрация и многое другое.<o:p></o:p></p><p class="MsoNormal">Финальный вариант фреймворка включен в состав более
обширного по возможностям опенсорсного продукта Process Memory Map (PMM),
задача которого максимально знать, что происходит в удаленных процессах и
выводить это в читаемом виде, кстати именно с него делались все скриншоты к
статье.<o:p></o:p></p><p class="MsoNormal"><a href="https://github.com/AlexanderBagel/ProcessMemoryMap">https://github.com/AlexanderBagel/ProcessMemoryMap</a></p><p class="MsoNormal"><o:p></o:p></p><p class="MsoNormal">РММ является одним из моих основных инструментов в
повседневной работе и на текущий момент времени в него добавлена вся
информация, которая когда-либо мне была нужна в процессе разработки. В планах,
конечно, еще много что есть, потихонечку буду расширять, но не все сразу. </p><p class="MsoNormal">Засим откланиваюсь<span lang="EN-US">.</span> </p><p class="MsoNormal">
</p><p class="MsoNormal"><span style="font-size: x-small;">Если вы читаете этот текст, значит вы прочли статью
внимательно и до конца.</span><o:p></o:p></p>
<p class="MsoNormal"><o:p> </o:p></p>Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com0tag:blogger.com,1999:blog-2374465879949372415.post-8982172266733504972016-03-01T19:17:00.000+03:002016-03-01T19:21:55.423+03:00Раннее исполнение кода в Delphi приложениях<div dir="ltr" style="text-align: left;" trbidi="on">
Как вы думаете, сколько кода выполняется до того момента, как приложение запустится, или отладчик передаст управление в ваши руки?<br />
<br />
Много - поверьте мне.<br />
Вы даже чихнуть не успеете, как окажется, что кто-то уже успел поработать в теле вашего процесса и отдал его вам на руки в момент его запуска - работайте и удивляйтесь.<br />
<br />
Нет, не вирусы, зачем? Вполне легальные продукты, контролирующие все и вся.<br />
<br />
Думаете - вру и считаете что при нажатии кнопки F7 (Trace Into) вы полностью контролируете процесс отладки?<br />
Тогда я вас разочарую, есть множество способов выполнить код, до того момента, как вы приступили к его дебагу.<br />
<br />
О нескольких я попробую рассказать.<br />
<br />
<a name='more'></a><br />
<br />
<h3 style="text-align: left;">
0. Теория</h3>
<br />
Начну с небольшой демонстрации.<br />
Давайте создадим новое консольное приложение и запустим его.<br />
Посмотрите на картинку:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUO9HobEs_oVifsBastrRFpK5j9SW5xLBRXpqeIMrThrIjtYQ1SkYlwhyQLrxUAeuZrO9BH0xUS83tCJ69QvED5Q5Uw357AcHJaRSIMCxv3-IbIkXKDSdXvpLS-gXXVf7wSPwjHTtxLiw/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="324" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUO9HobEs_oVifsBastrRFpK5j9SW5xLBRXpqeIMrThrIjtYQ1SkYlwhyQLrxUAeuZrO9BH0xUS83tCJ69QvED5Q5Uw357AcHJaRSIMCxv3-IbIkXKDSdXvpLS-gXXVf7wSPwjHTtxLiw/s640/1.png" width="640" /></a></div>
<br />
Мы еще не успели ничего сделать, а в адресное пространство нашего процесса подгрузилось нормальное такое количество библиотек.<br />
<br />
А почему?<br />
Потому что они импортируются статически посредством таблицы импорта, которая есть в каждом приличном приложении.<br />
<br />
Кто их загружает?<br />
Правильно, загрузчик. Каждая загруженная библиотека - это в принципе самостоятельное приложение, где есть точка входа, те же таблицы импорта/эспорта и функционал.<br />
<br />
В большинстве своем они, конечно, являются именно - библиотеками.<br />
Но, давайте посмотрим на это через карту памяти процесса (рассмотрим браузер Chrome):<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgphAtfd761YefGsbgkEef99FvmWju9EsQG3b64BwJC-aZYrcNm9ZD21igwLxTHWake4jJH1F8JIMtcLd2eMG8XpSGR4gliCU7lDhR9LnbIoEZgfFgkwjdYId4q02RXz5HFRYu2LwYQtc4/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="376" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgphAtfd761YefGsbgkEef99FvmWju9EsQG3b64BwJC-aZYrcNm9ZD21igwLxTHWake4jJH1F8JIMtcLd2eMG8XpSGR4gliCU7lDhR9LnbIoEZgfFgkwjdYId4q02RXz5HFRYu2LwYQtc4/s640/2.png" width="640" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
Ух ты, а что это тут у нас?<br />
Некая хромовская библиотека имеет помимо точки входа (Entry Point) еще аж 3 колбэка, которые будут выполнены до того как Chrome сможет с ней работать.<br />
<br />
Вот они все трое:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPmABf2KK0JpV22v6j7uBw6Y3pr-P2WkHU5PIE3ja4rblIYvEes3lyyQYulwqfOZJZhZwQhxI7ikGDd3Lx8_6Ee0FOtKDWGp2VQIFWORkjc0K5bq1Lsvl0Ye40_7fb1QGeKXmw7JexV-c/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="358" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPmABf2KK0JpV22v6j7uBw6Y3pr-P2WkHU5PIE3ja4rblIYvEes3lyyQYulwqfOZJZhZwQhxI7ikGDd3Lx8_6Ee0FOtKDWGp2VQIFWORkjc0K5bq1Lsvl0Ye40_7fb1QGeKXmw7JexV-c/s640/3.png" width="640" /></a></div>
<br />
<br />
А ведь у каждой библиотеки есть еще и обычная точка входа, которая выполнится после обработки колбэков.<br />
И это все выполнится до того, как мы нажали F7 :)<br />
<br />
<h3 style="text-align: left;">
1. Как работает загрузчик</h3>
<br />
При старте процесса он читает его PE образ и потихонечку начинает подгружать модули один за одним, ориентируясь на таблицу импорта каждого подгруженного модуля.<br />
<br />
К примеру, наше приложение через импорт тянет kernel32.dll (так бывает).<br />
Вот он загрузил наш образ и, не передавая управления на точку входа, грузит kernel32, подгрузив который он анализирует его таблицу импорта и понимает, что нужно в довесок подтянуть еще вот такенный список библиотек<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtJAH9P4rtvxXiEYIDSPmDYnFQhPiZLL7szS38nsuEacyp9SimHZT7swQ4qWkDRqRMOzlbTZQN6BxweY79tADf1Wv1gCoBA4QhtedpeuYVwoLwPHCOAK_Ga5zMGzTgQJX9zJuzMfhao7o/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="542" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtJAH9P4rtvxXiEYIDSPmDYnFQhPiZLL7szS38nsuEacyp9SimHZT7swQ4qWkDRqRMOzlbTZQN6BxweY79tADf1Wv1gCoBA4QhtedpeuYVwoLwPHCOAK_Ga5zMGzTgQJX9zJuzMfhao7o/s640/4.png" width="640" /></a></div>
<br />
Грузит каждую из них, и когда убедился что все что нужно подгружено - начинается магия.<br />
Он смотрит TLS таблицу библиотеки, если она не пуста, передает управление каждому колбэку из таблицы и в завершении всего передает управление на точку входа каждой подгруженной им библиотеки.<br />
<br />
И заметьте - пока что еще мы висим на F7 - код нашей программы не выполняется, но выполняется код из точек входа библиотек и их TLS Callback-ов<br />
<br />
И вот когда все подгружено и время дошло до нас, только тогда загрузчик анализирует нашу TLS таблицу (передавая управление если нужно) и в финале отдает управление на точку входа, где мы и сидим в отладчике :)<br />
<br />
<h3 style="text-align: left;">
2. Раннее исполнения кода в DLLMain</h3>
<br />
Зная, что управление сначала передается на код в библиотеках (слинкованных статически) мы можем сделать вот такой простой трюк для исполнения кода до выхода на точку входа в приложение.<br />
<br />
Пишем код библиотеки.<br />
<br />
<pre class="brush:delphi">library init_lib;
uses
Windows;
{$R *.res}
procedure Foo;
begin
end;
exports
Foo;
begin
MessageBox(0, 'Message from Lib', nil, 0);
end.
</pre>
<br />
Процедура Foo по факту не нужна - она используется только для статической линковки с приложением (чтобы библиотека была заявлена в таблице импорта).<br />
<br />
Ну и теперь сам код приложения:<br />
<br />
<pre class="brush:delphi">program testlib;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows;
procedure Foo; external 'init_lib.dll';
begin
MessageBox(0, 'Message from Test APP', nil, 0);
Foo;
end.
</pre>
<br />
<br />
Вот так это выглядит на практике:<br />
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOnKRi78fp1q9aEtgdaAOMWvw-IN4WN5z4bCWB5FoBhLSlM_Fd0spezD0HvDpjhoIyMpjqc_KcTkbS16uBnygCpde2sqbWAQpO_DHA4z1VaExkVHCgfsp40KMwjnU4XIH-jCCiFJ5eldw/s1600/1.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="330" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOnKRi78fp1q9aEtgdaAOMWvw-IN4WN5z4bCWB5FoBhLSlM_Fd0spezD0HvDpjhoIyMpjqc_KcTkbS16uBnygCpde2sqbWAQpO_DHA4z1VaExkVHCgfsp40KMwjnU4XIH-jCCiFJ5eldw/s640/1.gif" width="640" /></a></div>
<br />
<br />
Если бы мы грузили библиотеку в динамике через LoadLibrary - код в DllMain библиотеки тоже бы выполнился, но!!!<br />
Но уже после точки входа, а это нам не нужно, все таки рассматриваем раннее исполнение кода.<br />
<br />
<div style="text-align: left;">
Зачем это вообще надо?</div>
<br />
Хороший вопрос.<br />
Как правило раннее исполнение кода применяется при реализации антиотладочных и антидамповых трюках.<br />
<br />
С антиотладкой все очень просто, как я показал выше, ваш код выполнится еще до того, как на него получит управление отладчик.<br />
Немного утрирую, конечно - отладчик, естественно, тоже получит уведомление о загрузке библиотеки, но, как правило - это никто не анализирует.<br />
Но находясь на точке входа в библиотеку, вы можете абсолютно легально проверить - а не находимся ли мы под отладкой?<br />
Как это сделать - это вам решать, способов масса.<br />
<br />
А по поводу антидампа - тут тоже все достаточно просто, но на нем я остановлюсь чуть позже.<br />
<br />
<h3 style="text-align: left;">
3. Раннее исполнение кода без библиотеки</h3>
<br />
Дочитали до третьей главы?<br />
Уважаю :)<br />
<br />
Тогда займемся более серьезными вещами.<br />
<br />
Итак, чуть выше я упоминал про TLS Callback - посмотрим что это за зверь.<br />
Грубо говоря, при инициализации каждого потока, управление изначально передается не на TreadProc, а на цепочку его колбэков.<br />
<br />
При запуске вашего приложения (если отбросить нюансы с инициализацией загрузчиком секций, релоков, PEB/TEB) начинает работать главный поток.<br />
Но, перед передачей управления данному потоку, загрузчик первым образом анализирует образ вашего приложения на наличие TLS секции, ожидая там увидеть что-то похожее на вот эту структуру (для 32 бит к примеру - 64 бита не рассматриваю):<br />
<br />
<pre class="brush:delphi"> _IMAGE_TLS_DIRECTORY32 = record
StartAddressOfRawData: DWORD;
EndAddressOfRawData: DWORD;
AddressOfIndex: DWORD; // PDWORD
AddressOfCallBacks: DWORD; // PIMAGE_TLS_CALLBACK *;
SizeOfZeroFill: DWORD;
Characteristics: DWORD;
end;</pre>
<br />
Параметр AddressOfCallBacks указывает на VA адрес (если сильно упрощенно RVA + ImageBase образа) начала цепочки Callback-ов, которые должны быть выполнены, до передачи управления на точку входа в ThreadProc (для главного потока - это точка входа в приложение, т.н. EntryPoint).<br />
<br />
Цепочка - это банально массив адресов каждого колбэка в памяти процесса, завершающийся нулем.<br />
<br />
Помните картинку выше с хромовской библиотекой?<br />
Вот так это выглядит в карте памяти процесса.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWWNsKRsrLC1m0foOGptd7yAzQRz4umX0ZVINJPdHnAeM6sb8CQlIGPJep0wIwH6Ef5SgOWlY-cqct2o6H0dLikVsKc3ZYknn7BNztUgcVF94iJQt0S55w4ekz0jh8VEBeFYRO1pYcgOk/s1600/9.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="166" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWWNsKRsrLC1m0foOGptd7yAzQRz4umX0ZVINJPdHnAeM6sb8CQlIGPJep0wIwH6Ef5SgOWlY-cqct2o6H0dLikVsKc3ZYknn7BNztUgcVF94iJQt0S55w4ekz0jh8VEBeFYRO1pYcgOk/s640/9.png" width="640" /></a></div>
<br />
Синим выделено окончание цепочки TLS колбэков.<br />
Как бы и нам самим такое сделать?<br />
<br />
К примеру в MSVC это выглядит вот так:<br />
<pre class="brush:сpp">#include "windows.h"
// сам колбэк
VOID NTAPI tls_callback(HMODULE hModule,
DWORD ul_reason_for_call,LPVOID lpReserved)
{
MessageBox(0, L"TLS Callback Message", L"", 0);
}
// указываем линкеру что его нужно подключить
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__xl_b")
#pragma section(".CRT$XLY",long,read)
extern "C" __declspec(allocate(".CRT$XLY"))
PIMAGE_TLS_CALLBACK _xl_b = (PIMAGE_TLS_CALLBACK)tls_callback;
int main()
{
MessageBox(0, L"Entry Point Message", L"", 0);
return 0;
}
</pre>
<br />
Если собрать этот код в студии - то сначала отобразится окно:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2lyhR5F1By6LjR91iNr2wcGDBNNK46LARS-9Y-urd_1CjulnBMWBzwxtkUoetgRY2qUkkVW5RwlvZ_WW6KN1IK4nG6GE_9MkU_8OofTwQLjPVX4i-nKI8vcjnfQb4Vd2HXbNKKtgMNwo/s1600/5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2lyhR5F1By6LjR91iNr2wcGDBNNK46LARS-9Y-urd_1CjulnBMWBzwxtkUoetgRY2qUkkVW5RwlvZ_WW6KN1IK4nG6GE_9MkU_8OofTwQLjPVX4i-nKI8vcjnfQb4Vd2HXbNKKtgMNwo/s1600/5.png" /></a></div>
<br />
И только потом мы увидим второе "Entry Point Message".<br />
При билде данного исходного кода студия автоматом сформирует валидную TLS секцию и проинициализирует цепочку колбэков адресом процедуры tls_callback.<br />
<br />
Это в Visual Studio, а вот в Дельфи такого сделать так сходу нельзя - не умеет она, но!!!<br />
<br />
Но есть небольшой трюк, связанный с тем что разработчики компилера Дельфи, уж не знаю по каким причинам, немного нам помогли, создав неинициализированную TLS секцию в исполняемом файле. С какого времени это началось - я не знаю, но начиная с Delphi 2010 такая секция точно присутствует во всех исполняемых файлах.<br />
<br />
А раз у нас на руках есть TLS секция, почему-бы нам ее не проинициализировать самостоятельно?<br />
<br />
Давайте посмотрим что она из себя представляет и накидаем небольшое приложение.<br />
<br />
<pre class="brush:delphi">program test_app;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows;
// данный колбэк будет вызван, если файл будет корректно пропатчен
procedure tls_callback(hModule: HMODULE;
ul_reason_for_call: DWORD; lpReserved: Pointer); stdcall;
begin
if ul_reason_for_call = DLL_PROCESS_ATTACH then
MessageBox(0, 'TLS Callback Message', nil, 0);
end;
const
ptls_callback: Pointer = @tls_callback;
begin
// дабы процедура tls_callback появилась в MAP файле
// нужно чтобы на нее была ссылка, банально вот такая:
if ptls_callback <> nil then
MessageBox(0, 'Entry Point Message', nil, 0);
end.
</pre>
<br />
Давайте его сразу соберем и запустим, на выходе у нас будет только сообщение "Entry Point Message".<br />
<br />
Что собственно и логично, т.к. tls_callback сейчас является просто некоей функцией, о которой лоадер не знает и вызывать ее не собирается.<br />
<br />
Но, нам нужно ее вызвать. А сделать это мы сможем только посредством патча уже собранного файла.<br />
<br />
<h4 style="text-align: left;">
4. Инициализируем TLS секцию</h4>
<br />
Итак, у нас на руках есть исполняемый файл с объявленной функцией tls_callback.<br />
Рядом с ним лежит MAP файл (вы же включили генерацию МАР файла в настройках линкера?)<br />
<br />
Давайте посмотрим на него (покажу только интересующие нас куски мар файла):<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpbY9l422G6KBiFtQf4xnm9cp4P6ibfyQ62IRLy8pkgYB2j4UsIDqe2gIFf-RBMPJS6LuhQ7OZNPPqnRx_eTbvsAOdeMEvtGhWdfy_N_eXzv8MyvdDxCLqkkDlM-HJVXtQZ6-wJR0tEhc/s1600/8.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpbY9l422G6KBiFtQf4xnm9cp4P6ibfyQ62IRLy8pkgYB2j4UsIDqe2gIFf-RBMPJS6LuhQ7OZNPPqnRx_eTbvsAOdeMEvtGhWdfy_N_eXzv8MyvdDxCLqkkDlM-HJVXtQZ6-wJR0tEhc/s1600/8.png" /></a></div>
<br />
Самая последняя строчка - это номер секции и смещение нашей процедуры tls_callback.<br />
Вверху расположены сами секции и их адреса.<br />
<br />
К примеру, мы видим что процедура с именем "tls_callback" расположена в первой секции (индекс секции равен 0001).<br />
Оффсет колбэка относительно начала секции равен 00003208.<br />
Смотрим VA адрес секции за индексом 0001 (вверху) - это секция ".text", содержащая в себе код (на что указывает подсказка о типе данных "CODE"), а ее адрес равен 00401000.<br />
Банально суммируем оба значения (числа 16 битные, т.е. HEX).<br />
<br />
00003208 + 00401000 = 00404208.<br />
<br />
Проверяем.<br />
<div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1w2QMT2ImEKhM_JeKJgi1DlA1Pr1xiTdBA0vZvQNGAoUvQhcwsgTiAm6sw4oThvlii9gzM4rax6T1lImyE-cdhoRGfzaoQjWYiOwxzoz1zRsiDjGqKPCYjpcflrlvjHDGC71VyjNcUAk/s1600/6.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="522" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1w2QMT2ImEKhM_JeKJgi1DlA1Pr1xiTdBA0vZvQNGAoUvQhcwsgTiAm6sw4oThvlii9gzM4rax6T1lImyE-cdhoRGfzaoQjWYiOwxzoz1zRsiDjGqKPCYjpcflrlvjHDGC71VyjNcUAk/s640/6.png" width="640" /></a></div>
<br />
<br />
Да, это наш код.<br />
<br />
<pre class="brush:delphi">procedure tls_callback(DllHandle: Pointer;
Reason: DWORD; Reserved: Pointer); stdcall;
begin
if Reason = DLL_PROCESS_ATTACH then
MessageBox(0, 'TLS Callback Message 1', nil, 0);
end;
</pre>
<br />
Значит напишем небольшую функцию, которая будет получать из МАР файла реальный адрес колбэка по его имени (пусть он так и останется "tls_callback", хотя имя может быть любым).<br />
<br />
Вот это нам пригодится.<br />
<br />
<pre class="brush:delphi">type
TSectionData = record
Index: Integer;
StartAddr: DWORD;
SectionName: ShortString;
end;
TSectionDataList = TList<TSectionData>;
</pre>
<br />
Вот так мы зачитаем всю таблицу секций (банальный парсинг текстового файла):<br />
<br />
<pre class="brush:delphi">function GetSectionDataList(const FilePath: string; var Index: Integer): TSectionDataList;
var
S: TStringList;
Line: string;
Section: TSectionData;
begin
Result := TSectionDataList.Create;
try
S := TStringList.Create;
try
S.LoadFromFile(FilePath);
Index := 0;
Writeln('Ищу таблицу секций...');
while Copy(Trim(S[Index]), 1, 5) <> 'Start' do
Inc(Index);
Inc(Index);
while Trim(S[Index]) <> '' do
begin
Line := Trim(S[Index]);
Section.Index := StrToInt(Copy(Line, 1, 4));
Delete(Line, 1, 5);
Section.StartAddr := StrToInt('$' + Copy(Line, 1, 8));
Delete(Line, 1, 19);
Section.SectionName := ShortString(Trim(Copy(Line, 1, 8)));
Result.Add(Section);
Inc(Index);
end;
Writeln('Всего секций найдно: ', Result.Count);
finally
S.Free;
end;
except
// все исключения глушим. есть коды ошибок
on E: Exception do
Writeln('GetSectionDataList: ' + E.ClassName + ': ' + E.Message);
end;
end;
</pre>
<br />
А вот так найдем адрес нашей функции, ориентируясь на таблицу секций (такой-же парсинг):<br />
<br />
<pre class="brush:delphi">function GetTlsCallbackAddr(const FilePath: string;
SectionDataList: TSectionDataList; Index: Integer): DWORD;
var
S: TStringList;
Line: string;
SectionIndex, TlsAddr: Integer;
begin
Result := 0;
try
S := TStringList.Create;
try
S.LoadFromFile(FilePath);
Writeln('Ищу tls_callback...');
repeat
Line := Trim(S[Index]);
Inc(Index);
if Index = S.Count then Break;
until Pos('tls_callback', Line) <> 0;
if Pos('tls_callback', Line) = 0 then
begin
Writeln('В МАР файле запись о tls_callback не обнаружена');
Exit;
end;
SectionIndex := StrToInt(Copy(Line, 1, 4));
Delete(Line, 1, 5);
TlsAddr := StrToInt('$' + Copy(Line, 1, 8));
Writeln('tls_callback найден, смещение: ', IntToHex(TlsAddr, 8), ', секция: ', SectionIndex);
Writeln('Ищу запись о секции...');
for Index := 0 to SectionDataList.Count - 1 do
if SectionDataList[Index].Index = SectionIndex then
begin
Result := SectionDataList[Index].StartAddr + DWORD(TlsAddr);
Writeln('TLS Callback, найден в секции "', SectionDataList[Index].SectionName,
'", оффсет секции: ', IntToHex(SectionDataList[Index].StartAddr, 8),
', расcчитанный адресc: ', IntToHex(Result, 8));
Break;
end;
if Result = 0 then
Writeln('Секция содержащая tls_callback не найдена')
finally
S.Free;
end;
except
// все исключения глушим. есть коды ошибок
on E: Exception do
Writeln('GetTlsCallbackAddr: ' + E.ClassName + ': ' + E.Message);
end;
end;</pre>
<br />
И вот на руках у нас есть экзешник и точный адрес функции, которая должна быть выполнена в качестве TLS колбэка - осталось только найти адрес IMAGE_TLS_DIRECTORY, которая кстати выглядит вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCkoUMWNhreCq7ntaw6KbhvcQD6M3UsMRpmzxDQ8ANELx_73t_C11dpIHPturH9uaY8h9A741hyphenhyphenXSZPW-aAFcwmxIyzdo1hW-u8F9Cpa7KRm7lA8uV0tTb71-cWS6_gL7FQ66pOzDtBHM/s1600/7.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="318" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCkoUMWNhreCq7ntaw6KbhvcQD6M3UsMRpmzxDQ8ANELx_73t_C11dpIHPturH9uaY8h9A741hyphenhyphenXSZPW-aAFcwmxIyzdo1hW-u8F9Cpa7KRm7lA8uV0tTb71-cWS6_gL7FQ66pOzDtBHM/s640/7.png" width="640" /></a></div>
<br />
<br />
И пропатчить в ней поле AddressOfCallBacks + добавить VA адреса самих колбэков (их может быть несколько, как вы помните).<br />
<br />
Собственно:<br />
<br />
<pre class="brush:delphi">//
// Это простой способ поиска TLS таблицы НО - только в проектах,
// собранных в Delphi 2007 и выше (версии ниже не проверял)
// Если экзешник собран другим компилятором - естественно работать не будет
// но статья не об этом :)
// итак:
// =============================================================================
function GetTlsTableAddr(const FilePath: string): DWORD;
var
F: TFileStream;
DOS: TImageDosHeader;
NT: TImageNtHeaders;
I: Integer;
Section: TImageSectionHeader;
TlsFound: Boolean;
begin
Result := 0;
// открываем файл на чтение
F := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
try
// читаем DOS заголовок, чтобы выйти на NT
F.ReadBuffer(DOS, SizeOf(TImageDosHeader));
F.Position := DOS._lfanew;
// Читаем NT заголовок, чтобы получить количество секций
F.ReadBuffer(NT, SizeOf(TImageNtHeaders));
// читаем секции и ищем TLS
TlsFound := False;
for I := 0 to NT.FileHeader.NumberOfSections - 1 do
begin
F.ReadBuffer(Section, SizeOf(TImageSectionHeader));
if PAnsiChar(@Section.Name[0]) = '.tls' then
begin
TlsFound := True;
// нашли IMAGE_TLS_DIRECTORY, смотрим, заполнен ли адрес TLS?
if Section.PointerToRawData <> 0 then
// если заполнен, то сразу делаем поправку на поле AddressOfCallback
Result := Section.PointerToRawData + HardcodeTLS32Offset;
Break;
end;
end;
// делаем проверку, если нашли TLS то ее адрес обнилен, значит он будет сидеть в следующей секции
// такая вот особенность у Delphi, к примеру XE4 заполняет адрес, а XE8 нет
if TlsFound and (Result = 0) then
begin
F.ReadBuffer(Section, SizeOf(TImageSectionHeader));
Result := Section.PointerToRawData + HardcodeTLS32Offset;
end;
finally
F.Free;
end;
end;
</pre>
<br />
Данная функция возвращает реальный адрес _IMAGE_TLS_DIRECTORY32 в исполняемом файле + сразу возвращает оффсет на поле AddressOfCallBacks.<br />
<br />
Есть несколько оговорок:<br />
Работать будет только в случае Delphi образов (не стал сильно накручивать все нюансы, особенно с небольшой странностью в образе Delphi скомпиленных файлов) и только для 32 битных приложений (за это отвечает константа HardcodeTLS32Offset дающая смещение на поле AddressOfCallBacks, в 64 битах она должна быть равна 24).<br />
Если вдруг заинтересуетесь аналогичным патчем 64-битных образов - пишите, это легко делается, просто не охота раздувать объем статьи :)<br />
<br />
Впрочем - теперь можем патчить:<br />
<br />
<pre class="brush:delphi">// непосредственно патч файла
function Patch(const FilePath, MapPath: string; TlsTable, CallbackAddr: DWORD): Boolean;
var
F: TFileStream;
NewFilePath, BackUpFilePath: string;
OldCallbackTableAddr: DWORD;
begin
Result := False;
try
NewFilePath := ExtractFilePath(FilePath) + 'tls_aded_' +
ExtractFileName(FilePath);
Writeln('Создаю копию файла, путь: ', NewFilePath);
CopyFile(PChar(FilePath), PChar(NewFilePath), False);
F := TFileStream.Create(NewFilePath, fmOpenReadWrite);
try
Writeln('Файл открыт');
F.Position := TlsTable;
// читаем адрес куда ссылался предыдущий колбэк
F.ReadBuffer(OldCallbackTableAddr, 4);
// в delphi образе он ссылается на SizeOfZeroFill структуры IMAGE_TLS_DIRECTORY
// в которой оба последних поля заполнены нулями (якобы нет цепочки колбэков)
// Поэтому не будем портить рабочую структуру и заставим его ссылаться на адрес
// сразу за пределами данной структуры (плюс 2 дворда что в 32 битном, что в 64 битном варианте)
Inc(OldCallbackTableAddr, SizeOf(DWORD) * 2);
F.Position := TlsTable;
// пишем новый адрес на старое место
F.WriteBuffer(OldCallbackTableAddr, 4);
Writeln('Назначен новый адрес цепочки обработчиков, оффсет: ', IntToHex(TlsTable, 8),
', новое значение: ', IntToHex(OldCallbackTableAddr, 8));
// теперь прыгаем на место в которое должен быть записан VA адрес обработчика (не RVA)
// пропускаем SizeOfZeroFill и Characteristics и становимся сразу за ними
F.Position := TlsTable + SizeOf(DWORD) * 3;
// а теперь пишем адрес нашего колбэка
F.WriteBuffer(CallbackAddr, 4);
Writeln('Адрес колбэка выставлен, оффсет: ', IntToHex(TlsTable + SizeOf(DWORD) * 3, 8));
// после чего пишем ноль, для обозначения конца цепочки колбэков
CallbackAddr := 0;
F.WriteBuffer(CallbackAddr, 4);
finally
F.Free;
end;
// если все нормально, то переименовываем обратно
Writeln('Создаю бэкап');
BackUpFilePath := FilePath + '.bak';
DeleteFile(BackUpFilePath);
RenameFile(FilePath, BackUpFilePath);
Writeln('Сохраняю результат');
RenameFile(NewFilePath, FilePath);
Writeln('Все задачи завершены');
Result := True;
except
// все исключения глушим. есть коды ошибок
on E: Exception do
begin
// в случае ошибки подчищаем за собой - возращая все обратно
DeleteFile(NewFilePath);
RenameFile(BackUpFilePath, FilePath);
Writeln('Patch: ' + E.ClassName + ': ' + E.Message);
end;
end;
end;</pre>
<br />
Полный код найдете в <a href="http://rouse.drkb.ru/blog/early_execution.zip" target="_blank">демопримерах</a>, а теперь посмотрим как это работает.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlgPwrtTSfJOW3MsKBgcHI-ieS6TcL4bzt6XY8O_pfNDbQhE8NtrdXBShQJMw5NaUB7QCszqZ2Werlkuhk1X20RTAn72VwXUEcaHQ0PqNEN9bXzixXK0R9zWWPTv40559mTjBUf2BzI6I/s1600/1000.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="330" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlgPwrtTSfJOW3MsKBgcHI-ieS6TcL4bzt6XY8O_pfNDbQhE8NtrdXBShQJMw5NaUB7QCszqZ2Werlkuhk1X20RTAn72VwXUEcaHQ0PqNEN9bXzixXK0R9zWWPTv40559mTjBUf2BzI6I/s640/1000.gif" width="640" /></a></div>
<br />
<br />
Работает?<br />
А куда ж оно денется :)<br />
<br />
<h3 style="text-align: left;">
5. И что с этим делать?</h3>
<br />
С этим нужно работать, к примеру сейчас я продемонстрирую небольшой антидамповый трюк, который можно реализовать при помощи использования TLS колбэка.<br />
<br />
Но тут нужно еще немножко теории, чтобы вы поняли про что я говорю, а именно - для чего вообще реверсер делает дамп процесса?<br />
<br />
Представьте что ваше приложение на диске расположено немного не в том виде, в каком оно будет выглядеть в запущенном виде, ну к примеру оно запаковано, или зашифровано, или у него сбита точка входа каким нибудь вашим хитрым трюком (да в принципе какая по сути разница).<br />
<br />
Суть в что, что реверсер, такой-же ленивый как и мы все, и ему лениво каждый раз при старте дожидаться распаковки/расшифровки - он знает, что в тот момент, когда приложение выйдет на свою оригинальную точку входа (OEP - не путать с EntryPoint), с него будет уже снята вся распаковка/расшифровка и прочее.<br />
<br />
Утрирую конечно, но это чтобы просто на пальцах объяснить для самого простого случая.<br />
<br />
И вот дождавшись выхода на OEP - он дампит процесс, правит таблицу импорта, и потом продолжает работать уже с этим образом, не утруждая себя работой со всем тем навесняком - который присутствовал изначально в вашем приложении.<br />
<br />
Заметьте - он должен дождаться выхода на OEP, чуть раньше или чуть позже дампить процесс бессмысленно (все уплывет).<br />
<br />
И вот тут можем включиться мы с таким вот простым трюком.<br />
<br />
Зная что у нас есть TLS колбэк, который априори выполнится до выхода на точку входа в приложения (не говоря уже про OEP), мы заведем, скажем так - антидамповую метку.<br />
<br />
<pre class="brush:delphi">var
// наша антидамповая метка
anti_dump_mark: DWORD = 0;</pre>
<br />
Вся ее суть заключается в следующем:<br />
Изначально, как вы видите - она инициализируется нулем. Т.е. это штатный запуск приложения.<br />
<br />
Следующим шагом в нашем приложении должен отработать tls_callback, где мы напишем вот такой код:<br />
<br />
<pre class="brush:delphi">const
// некая константа, с которой будем сверятся
valid_anti_dump_mark_value = $DEADBEEF;
// данный колбэк будет вызван, если файл будет корректно пропатчен
procedure tls_callback(hModule: HMODULE;
ul_reason_for_call: DWORD; lpReserved: Pointer); stdcall;
begin
if ul_reason_for_call = DLL_PROCESS_ATTACH then
begin
// делаем проверку - при штатном запуске антидамповая метка должна быть равна нулю
if anti_dump_mark = 0 then
// если это так - инициализируем ее.
// проверкой инициализации будем заниматся на точке входа
anti_dump_mark := valid_anti_dump_mark_value
else
// если же антидамповая метка не равна нулю - значит нас сдампили
// намеренно бьем ее значение, чтобы сработал код на точке входа
anti_dump_mark := 1;
end;
end;</pre>
<br />
Суть вырисовывается?<br />
Ага, т.е. если при старте приложения наша метка равна нулю, то мы инициализируем ее "совершенно секретной константой" valid_anti_dump_mark_value :)<br />
А если нет (в этом случае она скорее всего будет равна как раз константе valid_anti_dump_mark_value) - наоборот убиваем ее значение.<br />
<br />
Поясню - anti_dump_mark сидит в области данных нашего процесса, и реверсер, когда будет дампить процесс, после того как tls_callback инициализировал эту метку, захватит ее значение в новый сдампленный образ.<br />
Соотвественно, при старте - метка anti_dump_mark уже не будет проинициализированна нулем, т.к. в том месте образа, где расположено её изначальное значение уже будет не ноль, а значение константы valid_anti_dump_mark_value.<br />
<br />
Всего-то и осталось что написать код проверки данной метки при старте процесса:<br />
<br />
<pre class="brush:delphi">const
ptls_callback: Pointer = @tls_callback;
begin
// проверяем - что там у нас по антидамповой метке, все ли нормально?
// если все нормально - она должна быть равна константе valid_anti_dump_mark_value
if anti_dump_mark <> valid_anti_dump_mark_value then
begin
// а если не равна - показываем "страшное" сообщение
// такое может произойти только в двух случаях:
// 1. мы забыли проинициализировать TLS Callback (не пропатчили тело экзешника)
// 2. ура - нас сдампипли :)
MessageBox(0, 'Process dumped', nil, 0);
// и уходим по английски, не оборачиваясь ;)
TerminateProcess(GetCurrentProcess, 0);
end;
// дабы процедура tls_callback появилась в MAP файле
// нужно чтобы на нее была ссылка, банально вот такая:
if ptls_callback <> nil then
MessageBox(0, 'All done', nil, 0);
end.</pre>
<br />
Ну, конечно-же не стоит забывать что исполняемый файл должен быть пропатчен, чтобы выполнялась процедура tls_callback, в противном случае антидамповая метка при старте не будет проинициализирована и будет ложное срабатывание - мол нас сдампили :)<br />
<br />
Давайте покажу как это работает в живую:<br />
<div class="separator" style="clear: both; text-align: center;">
<span style="text-align: left;"><br /></span></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgww6j9GOczzxrJRSeYqRCcEBqzCEU7hPpKpXri5tU5zcTUBfJPXyad2YasOVKHqnxycH-_0PZlM0mVEQbeTAPHGJNip1ISpzJFlIw3bGA6eids8o1xDA89AEpmJonNvIMEaYpFCAfVZ_E/s1600/667.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="330" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgww6j9GOczzxrJRSeYqRCcEBqzCEU7hPpKpXri5tU5zcTUBfJPXyad2YasOVKHqnxycH-_0PZlM0mVEQbeTAPHGJNip1ISpzJFlIw3bGA6eids8o1xDA89AEpmJonNvIMEaYpFCAfVZ_E/s640/667.gif" width="640" /></a></div>
<br />
<br />
<h3 style="text-align: left;">
6. В завершение</h3>
<br />
Ну, в принципе - как-то так :)<br />
<br />
Пробуйте - экспериментируйте, здесь на самом деле только верхушка айсберга рассказана.<br />
Вариантов использования колбэков море.<br />
Но, если кому-то пригодится, буду только рад.<br />
<br />
Исходный код к статье забирайте вот тут: <a href="http://rouse.drkb.ru/blog/early_execution.zip" target="_blank">http://rouse.drkb.ru/blog/early_execution.zip</a><br />
Весь код тестировался под следующими версиями Delphi: 2007, 2010, XE4, XE10 (остальных нет в наличии, но думаю что будет работать и там, вероятно даже начиная с Delphi 2005).<br />
<br />
В архиве в папке "bin" лежит уже пропатченный экзешник, показывающий методику антидампа, поэтому Chrome может ругаться на то, что в архиве лежит исполняемый файл.<br />
Не переживайте сильно и смело качайте - конечно же там вирус :)<br />
<br />
Как всегда огромная благодарность форуму "Мастера Дельфи", и особенное спасибо "NoUser", за некоторые моменты :)<br />
<br />
<span style="background-color: white; font-family: "arial" , "tahoma" , "helvetica" , "freesans" , sans-serif; font-size: 13px; line-height: 18px;">---</span><br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<span style="font-size: 12.7273px;">© Александр (Rouse_) Багель</span></div>
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Март, 2016</div>
</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com18tag:blogger.com,1999:blog-2374465879949372415.post-87314155152767295582015-04-29T21:40:00.004+03:002015-04-30T17:13:41.974+03:00Анализ задачи №18 от Александра Алексеева (ака GunSmoker)<div dir="ltr" style="text-align: left;" trbidi="on">
Кажется я первый раз попал в тупик.<br />
Не то, чтобы я сильно умный, но и задачка — не "Балтика 9".<br />
<br />
<a href="http://www.gunsmoker.ru/2015/04/task-18-1.html" target="_blank">Первая часть задачи</a> выглядела вот так:<br />
<br />
Что не так с этим кодом?<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
var
Wnd: HWND;
function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall;
begin
if AWnd = Wnd then
Caption := 'OK';
Result := True;
end;
begin
Wnd := Handle;
EnumWindows(@EnumWindowsProc, 0);
end;
</pre>
<br />
Приступим.<br />
<br />
<a name='more'></a><br />
<br />
Конечно, первое что бросается в глаза — вызов калбэка, в котором происходит обращение к переменной Wnd.<br />
<br />
Жалко, что Delphi позволяет вообще такому компилироваться.<br />
Правда это обусловлено не совсем верной декларацией самой функции EnumWindows.<br />
<br />
Если бы она была объявлена вот так:<br />
<br />
<pre class="brush:delphi">type
TFNWndEnumProc = function(AWnd: HWND; AParam: LPARAM): BOOL; stdcall;
function EnumWindows(lpEnumFunc: TFNWndEnumProc;
lParam: LPARAM): BOOL; stdcall; external user32 name 'EnumWindows';
</pre>
<br />
То ошибка была бы выявлена еще на этапе компиляции и Delphi выдала бы сообщение:<br />
<blockquote class="tr_bq">
[dcc64 Error] Unit1.pas(49): E2094 Local procedure/function 'EnumWindowsProc' assigned to procedure variable</blockquote>
При этом вызывать нужно без взятия указателя, т.е. вот так:<br />
<br />
<pre class="brush:delphi"> EnumWindows(EnumWindowsProc, 0);
</pre>
<br />
Если же мы будем получать адрес функции EnumWindowsProc посредством оператора "@", то даже такая декларация не поможет, т.к. в данном случае мы будем работать с нетипизированным указателем и все проверки на этапе компиляции будут отключены.<br />
<br />
Конечно можно включить в настройках компилятора флаг "Typed @ operator" и получить сообщение:<br />
<blockquote class="tr_bq">
[dcc64 Error] Unit1.pas(45): E2010 Incompatible types: 'TFNWndEnumProc' and 'Pointer'</blockquote>
Но это уже как-то избыточно :)<br />
<br />
Впрочем, так как объявление у EnumWindows другое, и данный код все же компилируется, попробуем разобраться с ним по пунктам:<br />
<ol style="text-align: left;">
<li>Переменная Wnd объявлена как локальная и память под нее выделена на стеке, фрейм которого принадлежит процедуре TForm1.Button1Click.</li>
<li>EnumWindowsProc - это калбэк процедура, с своим собственным стеком, вызываемая не напрямую через EnumWindows, а через еще кучу пертрубаций в User32.dll</li>
<li>EnumWindowsProc пытается получить доступ к Wnd и компилятор пытается этому помочь, ориентируясь на код.</li>
</ol>
Пардоньте - сейчас будет много ассемблера, но демонстрирую:<br />
<br />
Вот так выглядит вызов EnumWindows:<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtTj_UugeGXsFOCyqtqxFiYdLsPe8OgInviig5TTXkC3c4HTBMw9lk0RzV0VDl_ZDc7AuVTcqnkOcezTWBahEf54knKv_AyxoSzgnPjzZn5obvzf42gR0F81qG3GS32S0gyBZiP6JRnb9I/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtTj_UugeGXsFOCyqtqxFiYdLsPe8OgInviig5TTXkC3c4HTBMw9lk0RzV0VDl_ZDc7AuVTcqnkOcezTWBahEf54knKv_AyxoSzgnPjzZn5obvzf42gR0F81qG3GS32S0gyBZiP6JRnb9I/s1600/1.png" /></a>
<br />
<br />
Тут нас интересует значение, помещенное на стек по адресу EBP-4, грубо говоря хэдл, полученный вызовом GetHandle.<br />
На стеке он будет размещен примерно вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaQWqZqvy73o8rIs7fDFbZdV17OA-f3AnPGxiCh_Fjteh_JNORgWBmVUPlLxOVE4eSU-41rWhutQ7JtcewAm0c86TZQWb8_cRdaP1uJ57mzFFA26nQCW3x_7MKmUuYDDjY_8dpiZAzBN8t/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaQWqZqvy73o8rIs7fDFbZdV17OA-f3AnPGxiCh_Fjteh_JNORgWBmVUPlLxOVE4eSU-41rWhutQ7JtcewAm0c86TZQWb8_cRdaP1uJ57mzFFA26nQCW3x_7MKmUuYDDjY_8dpiZAzBN8t/s1600/2.png" /></a></div>
<br />
<br />
Число 12F558 - это адрес локальной переменной Wnd, а 16051A - это хэндл главной формы, т.е. значение самой переменной.
Запомним адрес 12F558 и посмотрим что произойдет при вызове калбэка EnumWindowsProc.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAHgxuHDx9GtKfxjJPJdUuucVWvWM1TRplBJIQSPo41rpmzaJRj7naeQ4l_0RV6p3an4Bo1zGQsylf76UoNadlWGxeFRNEVgN17XdtKZe4UPsv3tjE4ogAd0-_8Eq7nLWozzzRJ9F4Ky6X/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAHgxuHDx9GtKfxjJPJdUuucVWvWM1TRplBJIQSPo41rpmzaJRj7naeQ4l_0RV6p3an4Bo1zGQsylf76UoNadlWGxeFRNEVgN17XdtKZe4UPsv3tjE4ogAd0-_8Eq7nLWozzzRJ9F4Ky6X/s1600/3.png" /></a></div>
<br />
<br />
А вот это уже настоящая беда.<br />
<br />
После генерации стекового фрейма (push+mov), EBP+8 указывает на параметр AWnd калбэка, Но посмотрите как происходит обращение к внешней переменной Wnd, с которой потом будет происходить сравнение.
<br />
<br />
<pre class="brush:delphi">005C734B 8B4510 mov eax,[ebp+$10]
005C734E 8B40FC mov eax,[eax-$04]
</pre>
<br />
Delphi компилятор, ошибшись, сгенерировал асм код получения указателя на верхушку фреймового стека, полагаясь на то, что мы все еще находимся в исполнении процедуры Button1Click, после чего ошибочно начал читать значение локальной переменной Wnd по адресу EAX-4.<br />
<br />
Это грозит - глобальным AV, ведь текущий стек не принадлежит Button1Click и мы реально можем влететь на ошибку как при получении верхушки фрейма, а если повезло и не упали, то упадем на чтении якобы Wnd с битого адреса (если повезет).<br />
<br />
Показываю с использованием карты памяти процесса:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSGLoOHR7waYJZio3vDzKVT_K0O16qHERbawQQ67vYbepsOL9tvJBfbyShhqg6NHyRifXu138uuaWEpQZ5CblsziuAuDA4UzIisNAfNBs9c-34YujtyZ4n_Q3AM9ghOqRi8bgdEK5oLPQQ/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSGLoOHR7waYJZio3vDzKVT_K0O16qHERbawQQ67vYbepsOL9tvJBfbyShhqg6NHyRifXu138uuaWEpQZ5CblsziuAuDA4UzIisNAfNBs9c-34YujtyZ4n_Q3AM9ghOqRi8bgdEK5oLPQQ/s1600/4.png" height="328" width="640" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
Как вы видите, EAX после выполнения первой инструкции, вместо правильного адреса на стеке указывает куда-то вглубь памяти процесса, на которую отмаплен текущий исполняемый файл.<br />
<br />
Можно сказать повезло, могли попасть и в секцию проверки битых указателей (первые 64 кб памяти процесса).<br />
<br />
Сами понимаете что вторая строка (получение значения Wnd) уже как минимум не будет корректной, при работе с такими данными.<br />
А нам нужен был адрес 12F558 - но не судьба.<br />
<br />
Впрочем, перейдем к пункту за номером 4.<br />
А именно к строчке:<br />
<br />
<pre class="brush:delphi"> Caption := 'OK';
</pre>
<br />
Конечно-же в калбэк не передается перемеменна Self, но Delphi компилер не смог предположить такого цимуса, и даже в этом случае пытается выйти на нее.<br />
<br />
<pre class="brush:delphi">005C7356 8B4510 mov eax,[ebp+$10]
005C7359 8B40F8 mov eax,[eax-$08]
</pre>
<br />
Понятно, что такого у него не получится, но... если вдруг он все-же получит какой-то не совсем убитый адрес, то произойдет глобальный бадабум на вызове сеттера Caption, ибо произойдет вызов WindowProc на не понятно каком окне (я даже про VMT пока не говорю).<br />
<br />
<h3 style="text-align: left;">
Часть вторая</h3>
<br />
Если с первой задачей проблем никаких не было — все на виду, то вот <a href="http://www.gunsmoker.ru/2015/04/task-18.html" target="_blank">вторая</a> поставила меня в глобальный тупик.<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall;
var
Wnd: HWND;
begin
Wnd := HWND(AParam);
if AWnd = Wnd then
Result := True
else
Result := False;
end;
var
Wnd: HWND;
begin
Wnd := Handle;
EnumWindows(@EnumWindowsProc, LPARAM(Wnd));
end;
</pre>
<br />
Сразу скажу — на мой взгляд код правильный, но раз есть заковыка (а иначе и не было бы такой задачи поставлено), значит надо анализировать.<br />
Единственный момент с условием выхода из EnumWindowsProc, ибо практически гарантированно, что первое найденное окно не будет хэндлом формы, что означает завершение работы EnumWindowsProc после первого-же сравнения (по False результату).<br />
<br />
Повтыкав минут двадцать в данный код и не обнаружив ошибки, я выкатил его всему нашему IT отделу, с целью - может коллективный разум осилит?<br />
<br />
Через час мы сдались и я начал анализировать уже с точки зрения реверсера.<br />
<br />
Итак, что мы имеем:<br />
<br />
<pre class="brush:delphi">sdf.pas.31: begin
005C7348 55 push ebp
005C7349 8BEC mov ebp,esp
sdf.pas.32: Wnd := HWND(AParam);
005C734B 8B450C mov eax,[ebp+$0c]
sdf.pas.33: if AWnd = Wnd then
005C734E 3B4508 cmp eax,[ebp+$08]
005C7351 7505 jnz $005c7358
sdf.pas.34: Result := True
005C7353 83C8FF or eax,-$01
005C7356 EB02 jmp $005c735a
sdf.pas.36: Result := False;
005C7358 33C0 xor eax,eax
sdf.pas.37: end;
005C735A 5D pop ebp
005C735B C20800 ret $0008
005C735E 8BC0 mov eax,eax
sdf.pas.41: begin
005C7360 55 push ebp
005C7361 8BEC mov ebp,esp
sdf.pas.42: Wnd := Handle;
005C7363 E8248EF5FF call TWinControl.GetHandle
sdf.pas.43: EnumWindows(@EnumWindowsProc, LPARAM(Wnd));
005C7368 50 push eax
005C7369 6848735C00 push $005c7348
005C736E E879BBE4FF call EnumWindows
sdf.pas.44: end;
005C7373 5D pop ebp
005C7374 C3 ret
</pre>
<br />
Первичный анализ асм кода показывает, что использованные локальные перменные Wnd выкинуты оптимизатором, т.е. вот такой вот код будет выдавать абсолютно идентичный асм листинг:<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall;
begin
if AWnd = HWND(AParam) then
Result := True
else
Result := False;
end;
begin
EnumWindows(@EnumWindowsProc, LPARAM(Handle));
end;
</pre>
<br />
Значит заковыка не в использовании локальных переменных.<br />
Вариант с ошибками каста HWND/LPARAM отпадает, тут все правильно.<br />
Вариант с выносом калбэка за тело процедуры асм код не меняет (да и смысл?).<br />
Вариант с неверной декларацией EnumWindowsProc тоже отпадает (спецом исходники винды поднял - может там какой нюанс крылся?)<br />
Значит остается последний вариант: может что-то с условием, как-то странно оно выглядит.<br />
<br />
Переписываю вот так:<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall;
begin
Result := AWnd = HWND(AParam);
end;
begin
EnumWindows(@EnumWindowsProc, LPARAM(Handle));
end;
</pre>
<br />
Выхлоп:<br />
<br />
<pre class="brush:delphi">sdf.pas.29: begin
005C7348 55 push ebp
005C7349 8BEC mov ebp,esp
sdf.pas.30: Result := AWnd = HWND(AParam);
005C734B 8B4508 mov eax,[ebp+$08]
005C734E 3B450C cmp eax,[ebp+$0c]
005C7351 0F94C0 setz al
005C7354 F6D8 neg al
005C7356 1BC0 sbb eax,eax
sdf.pas.31: end;
005C7358 5D pop ebp
005C7359 C20800 ret $0008
sdf.pas.33: begin
005C735C 55 push ebp
005C735D 8BEC mov ebp,esp
005C735F 53 push ebx
005C7360 8BD8 mov ebx,eax
sdf.pas.34: EnumWindows(@EnumWindowsProc, LPARAM(Handle));
005C7362 8BC3 mov eax,ebx
005C7364 E8238EF5FF call TWinControl.GetHandle
005C7369 50 push eax
005C736A 6848735C00 push $005c7348
005C736F E878BBE4FF call EnumWindows
sdf.pas.35: end;
005C7374 5B pop ebx
005C7375 5D pop ebp
005C7376 C3 ret
</pre>
<br />
И опять нет ничего странного.<br />
<br />
Тут я сдулся, ибо если последний вариант кода считать верным (а он верный, иначе даже в MSDN ошибка) то я категорически не понимаю - где здесь тонкий нюанс?<br />
<br />
Но раз GunSmoker выкатил такую задачу - значит что-то тут есть еще такое, а я лично сдаюсь :)<br />
<br />
<b>UPDATE:</b><br />
<br />
Ан нет - рано еще здаваться, как оказалось дело было в том что я забыл про 64 бита.<br />
<br />
Смотрим ASM код функции EnumWindowsProc:<br />
<br />
<pre class="brush:delphi">00000000006960A0 55 push rbp
00000000006960A1 4883EC10 sub rsp,$10
00000000006960A5 488BEC mov rbp,rsp
00000000006960A8 48894D20 mov [rbp+$20],rcx // сохраняем значение AWnd
00000000006960AC 48895528 mov [rbp+$28],rdx // сохраняем значение AParam
00000000006960B0 4C894530 mov [rbp+$30],r8 // и вот пошел ошибочный код, сохраняем R8
hgfjgfjh.pas.55: Wnd := HWND(AParam);
00000000006960B4 488B4530 mov rax,[rbp+$30] // перемещаем значение R8 на верхушку стека
00000000006960B8 48894500 mov [rbp+$00],rax
hgfjgfjh.pas.56: if AWnd = Wnd then
00000000006960BC 488B4528 mov rax,[rbp+$28] // читаем значение AParam
00000000006960C0 483B4500 cmp rax,[rbp+$00] // и сравниваем его с R8, а нужно с AWnd
00000000006960C4 7509 jnz EnumWindowsProc + $2F
hgfjgfjh.pas.57: Result := True
00000000006960C6 C7450CFFFFFFFF mov [rbp+$0c],$ffffffff
00000000006960CD EB07 jmp EnumWindowsProc + $36
hgfjgfjh.pas.59: Result := False;
00000000006960CF C7450C00000000 mov [rbp+$0c],$00000000
hgfjgfjh.pas.60: end;
00000000006960D6 8B450C mov eax,[rbp+$0c]
00000000006960D9 488D6510 lea rsp,[rbp+$10]
00000000006960DD 5D pop rbp
00000000006960DE C3 ret
</pre>
<br />
Вот оно что, Михалыч.<br />
<br />
Причина простая, по умолчанию параметры передаются через регистры RCX, RDX, R8 и т.п.<br />
Так вот для Nested функций, Delphi в регистр RCX помещает адрес вершины стека.<br />
Таким образом оставшиеся два параметра идут в RDX и R8.<br />
А код внутри EnumWindows этого не делает и по честному передает оба параметра через RCX + RDX.<br />
Получается что мы работаем совершенно убитыми данными.<br />
<br />
Как избавится от такого поведения?<br />
Ответ прост - вынести функцию EnumWindowsProc за пределы процедуры TForm1.Button1Click.<br />
<br />
<pre class="brush:delphi">function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall;
var
Wnd: HWND;
begin
Wnd := HWND(AParam);
if AWnd = Wnd then
Result := True
else
Result := False;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Wnd: HWND;
begin
Wnd := Handle;
EnumWindows(@EnumWindowsProc, LPARAM(Wnd));
end;
</pre>
<br />
После чего регистр RCX не будет задействован и сгенерируется правильный ASM код:<br />
<br />
<pre class="brush:delphi">0000000000696030 55 push rbp
0000000000696031 4883EC10 sub rsp,$10
0000000000696035 488BEC mov rbp,rsp
0000000000696038 48894D20 mov [rbp+$20],rcx // сохраняем значение AWnd
000000000069603C 48895528 mov [rbp+$28],rdx // сохраняем значение AParam
hgfjgfjh.pas.31: Wnd := HWND(AParam);
0000000000696040 488B4528 mov rax,[rbp+$28] // перемещаем значение AParam на верхушку стека
0000000000696044 48894500 mov [rbp+$00],rax
hgfjgfjh.pas.32: if AWnd = Wnd then
0000000000696048 488B4520 mov rax,[rbp+$20] // читаем значение AWnd
000000000069604C 483B4500 cmp rax,[rbp+$00] // и сравниваем его с AParam
0000000000696050 7509 jnz EnumWindowsProc + $2B
hgfjgfjh.pas.33: Result := True
0000000000696052 C7450CFFFFFFFF mov [rbp+$0c],$ffffffff
0000000000696059 EB07 jmp EnumWindowsProc + $32
hgfjgfjh.pas.35: Result := False;
000000000069605B C7450C00000000 mov [rbp+$0c],$00000000
hgfjgfjh.pas.36: end;
0000000000696062 8B450C mov eax,[rbp+$0c]
0000000000696065 488D6510 lea rsp,[rbp+$10]
0000000000696069 5D pop rbp
000000000069606A C3 ret
</pre>
<br />
Собственно, как мне подсказали, этот момент описан в документации (о чем я не знал).<br />
<blockquote class="tr_bq">
<span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Nested procedures and functions (routines declared within other routines) cannot be used as procedural values, nor can predefined procedures and functions.</span></blockquote>
<a href="http://docwiki.embarcadero.com/RADStudio/XE8/en/Procedural_Types#Method_Pointers">http://docwiki.embarcadero.com/RADStudio/XE8/en/Procedural_Types#Method_Pointers</a><br />
<br />
Такие вот пироги :)</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com16tag:blogger.com,1999:blog-2374465879949372415.post-60341432438713143962015-04-10T20:25:00.000+03:002015-04-16T12:32:02.457+03:00Чем меня порадовала ХЕ8<div dir="ltr" style="text-align: left;" trbidi="on">
В последнее время я все чаще задумываюсь о смысле апдейта текущей версии Delphi на более новую. Есть ли в этом необходимость?<br />
С учетом, что я разрабатываю 32 битные приложения только под Win - вся эта петрушка в виде возможности разработки под Андроид или iOS прямо на Delphi, ну... скажем так — не сильно востребована.<br />
А вот что более востребовано — так это стабильная работа среды, отсутствие ошибок при работе с дженериками/лямдами и всем тем "синтаксическим сахаром", который внедряется уже какой год.<br />
Вы будете смеяться, но я однажды вообще от инлайнов отказался по многим причинам, в частности одна из них была в том, что кодогенератор выдавал абсолютно невалидный асм код в определенных ситуациях, абсолютно переиначивая всю логику работы inline функции (банально не тот результат возвращала). Кажется это было на 2010 или ХЕ первой.<br />
<br />
Впрочем посмотрим что мы имеем сейчас.<br />
<br />
<a name='more'></a><br />
<br />
Краткий перечень нововведений:<br />
<ul style="text-align: left;">
<li>GetIt package manager for seamless installation of components </li>
<li>DUnitX support </li>
<li>Version Insight support for Mercurial, and improved support for Git and Subversion </li>
<li>IDE configuration migration tool </li>
<li>Faster CHM help </li>
<li>Start here page redesign </li>
<li>Project statistics information </li>
<li>Clipboard history </li>
<li>Multi-paste support </li>
<li>Stack bookmarks </li>
<li>Smart keys </li>
<li>Parenthesis matching </li>
<li>Code structural highlight </li>
<li>Castalia refactorings </li>
<li>Editor selection expansion </li>
<li>Flow controls highlighting </li>
<li>Code navigation toolbar </li>
<li>Smart symbol search </li>
<li>Code analysis to track quality</li>
</ul>
<br />
GetIt - забавная такая приблуда, позволяющая оперативно ставить сторонние пакеты. Но вот вопрос:<br />
А как часто вы ставите сторонние пакеты компонентов?<br />
Лично я, только при переустановке дельфи ставлю VirtualStringTree, плюс раз в полгода обновляю DevExpress (зависит от того, как часто они сами родят новую версию).<br />
Ничего против, конечно, не имею - но GetIt это просто фишка, не имеющая к процессу разработки никакого отношения.<br />
<br />
DUnitX - ну, допустим, юниттестирование должно прививаться разработчикам.<br />
<br />
Интеграция различных систем контроля версий (SVN/Git/HG - Mercurial).<br />
Этот момент спорный - люди годами ими пользуются, в том числе и экспертами, которые уже умеют это все делать. Не факт что удачный ход.<br />
<br />
Тоже относится и к Castalia - первый вопрос будет, для непривычного пользователя: как это отключить? (<a href="http://stackoverflow.com/questions/29520761/how-can-i-disable-castalia-in-xe8">например вот так</a>).<br />
<br />
А что еще осталось?<br />
<br />
<div>
Ну, хелп да редизайн стартовой страницы (зачем вообще об этом упоминать?).</div>
<div>
История буфера - такое уже видели в виде экпертов.<br />
<br />
Остальные фишки - да, интересны, но может взглянем на решарпер (продвинутое расширение IDE для C#) и его возможности? :)<br />
<br />
Ну и какой сделаем вывод - все плохо?<br />
<br />
А вот с этим будет нюанс и для раскрытия оного я процитирую своего коллегу: <a href="http://kazav.blogspot.ru/" target="_blank">Алексея Казанцева</a>.<br />
<br />
<pre class="brush:delphi">program inline_bug;
{$APPTYPE CONSOLE}
{$OPTIMIZATION ON}
{$INLINE ON}
{$R *.res}
uses
SysUtils;
type
Utils = class
type
Namespace = class
class function Comparestring(const ALeft, ARight: string) : Integer; static; inline;
class function CompareText(const ALeft, ARight: string) : Integer; static; inline;
class function Samestring(const ALeft, ARight: string;
ACaseSensitive : Boolean = True) : Boolean; static; inline;
end;
end;
{ Utils.NameSpace }
class function Utils.Namespace.Comparestring(const ALeft, ARight : System.string) : Integer;
begin
Result := SysUtils.CompareStr(ALeft, ARight);
end;
class function Utils.Namespace.CompareText(const ALeft, ARight : string) : Integer;
begin
Result := SysUtils.CompareText(ALeft, ARight);
end;
class function Utils.Namespace.Samestring(const ALeft, ARight : string; ACaseSensitive : Boolean) : Boolean;
begin
if ACaseSensitive then
Result := Utils.Namespace.Comparestring(ALeft, ARight) = 0
else
Result := Utils.Namespace.CompareText(ALeft, Aright) = 0;
end;
var
S1, S2: string;
begin
Utils.Namespace.Comparestring(S1, S2);
Utils.Namespace.Samestring(S1, S2);
S1 := '';
end.
</pre>
<br />
Банально сравниваем две строки.<br />
Но давайте посмотрим на кодогенерацию в продуктах до ХЕ включительно:<br />
<br />
<pre class="brush:asm">inline_bug.dpr.68: Utils.Namespace.CompareString(S1, S2);
0040915A 8B1524E24000 mov edx,[$0040e224]
00409160 A120E24000 mov eax,[$0040e220]
00409165 E8F2C7FFFF call CompareStr
inline_bug.dpr.69: Utils.Namespace.SameString(S1, S2);
0040916A 8B1524E24000 mov edx,[$0040e224]
00409170 A120E24000 mov eax,[$0040e220]
00409175 E8E2C7FFFF call CompareStr
0040917A 85C0 test eax,eax
0040917C 0F94C0 setz al
</pre>
<br />
Абсолютно логичный код - никаких претензий, но посмотрим что будет под XE2:<br />
<br />
<pre class="brush:asm">inline_bug.dpr.68: Utils.Namespace.CompareString(S1, S2);
0041B3AB 8D45EC lea eax,[ebp-$14]
0041B3AE 8B15D82E4200 mov edx,[$00422ed8]
0041B3B4 E84BA4FEFF call @UStrLAsg
0041B3B9 8D45E8 lea eax,[ebp-$18]
0041B3BC 8B15DC2E4200 mov edx,[$00422edc]
0041B3C2 E83DA4FEFF call @UStrLAsg
0041B3C7 8B55E8 mov edx,[ebp-$18]
0041B3CA 8B45EC mov eax,[ebp-$14]
0041B3CD E8F28FFFFF call CompareStr
inline_bug.dpr.69: Utils.Namespace.SameString(S1, S2);
0041B3D2 8D45E4 lea eax,[ebp-$1c]
0041B3D5 8B15D82E4200 mov edx,[$00422ed8]
0041B3DB E824A4FEFF call @UStrLAsg
0041B3E0 8D45E0 lea eax,[ebp-$20]
0041B3E3 8B15DC2E4200 mov edx,[$00422edc]
0041B3E9 E816A4FEFF call @UStrLAsg
0041B3EE 8D45DC lea eax,[ebp-$24]
0041B3F1 8B55E4 mov edx,[ebp-$1c]
0041B3F4 E80BA4FEFF call @UStrLAsg
0041B3F9 8D45D8 lea eax,[ebp-$28]
0041B3FC 8B55E0 mov edx,[ebp-$20]
0041B3FF E800A4FEFF call @UStrLAsg
0041B404 8B55D8 mov edx,[ebp-$28]
0041B407 8B45DC mov eax,[ebp-$24]
0041B40A E8B58FFFFF call CompareStr
0041B40F 85C0 test eax,eax
0041B411 0F94C0 setz al
</pre>
<br />
Страшно, но сейчас посмотрим что выдается под ХЕ4/ХЕ5:<br />
<br />
<pre class="brush:asm">inline_bug.dpr.45: Utils.Namespace.Comparestring(S1, S2);
0041A3E4 8D45EC lea eax,[ebp-$14]
0041A3E7 8B15D81E4200 mov edx,[$00421ed8]
0041A3ED E8F2B6FEFF call @UStrLAsg
0041A3F2 8D45E8 lea eax,[ebp-$18]
0041A3F5 8B15DC1E4200 mov edx,[$00421edc]
0041A3FB E8E4B6FEFF call @UStrLAsg
0041A400 33C0 xor eax,eax
0041A402 55 push ebp
0041A403 683AA44100 push $0041a43a
0041A408 64FF30 push dword ptr fs:[eax]
0041A40B 648920 mov fs:[eax],esp
0041A40E 8B55E8 mov edx,[ebp-$18]
0041A411 8B45EC mov eax,[ebp-$14]
0041A414 E8F782FFFF call CompareStr
0041A419 8945CC mov [ebp-$34],eax
0041A41C 33C0 xor eax,eax
0041A41E 5A pop edx
0041A41F 59 pop ecx
0041A420 59 pop ecx
0041A421 648910 mov fs:[eax],edx
0041A424 6841A44100 push $0041a441
0041A429 8D45EC lea eax,[ebp-$14]
0041A42C E897B3FEFF call @UStrClr
0041A431 8D45E8 lea eax,[ebp-$18]
0041A434 E88FB3FEFF call @UStrClr
0041A439 C3 ret
0041A43A E9ADAAFEFF jmp @HandleFinally
0041A43F EBE8 jmp $0041a429
inline_bug.dpr.46: Utils.Namespace.Samestring(S1, S2);
0041A441 8D45E4 lea eax,[ebp-$1c]
0041A444 8B15D81E4200 mov edx,[$00421ed8]
0041A44A E895B6FEFF call @UStrLAsg
0041A44F 8D45E0 lea eax,[ebp-$20]
0041A452 8B15DC1E4200 mov edx,[$00421edc]
0041A458 E887B6FEFF call @UStrLAsg
0041A45D 8D45DC lea eax,[ebp-$24]
0041A460 E863B3FEFF call @UStrClr
0041A465 8D45D8 lea eax,[ebp-$28]
0041A468 E85BB3FEFF call @UStrClr
0041A46D 8D45D4 lea eax,[ebp-$2c]
0041A470 E853B3FEFF call @UStrClr
0041A475 8D45D0 lea eax,[ebp-$30]
0041A478 E84BB3FEFF call @UStrClr
0041A47D 33C0 xor eax,eax
0041A47F 55 push ebp
0041A480 6828A54100 push $0041a528
0041A485 64FF30 push dword ptr fs:[eax]
0041A488 648920 mov fs:[eax],esp
0041A48B 8D45DC lea eax,[ebp-$24]
0041A48E 8B55E4 mov edx,[ebp-$1c]
0041A491 E84EB6FEFF call @UStrLAsg
0041A496 8D45D8 lea eax,[ebp-$28]
0041A499 8B55E0 mov edx,[ebp-$20]
0041A49C E843B6FEFF call @UStrLAsg
0041A4A1 33C0 xor eax,eax
0041A4A3 55 push ebp
0041A4A4 68DBA44100 push $0041a4db
0041A4A9 64FF30 push dword ptr fs:[eax]
0041A4AC 648920 mov fs:[eax],esp
0041A4AF 8B55D8 mov edx,[ebp-$28]
0041A4B2 8B45DC mov eax,[ebp-$24]
0041A4B5 E85682FFFF call CompareStr
0041A4BA 8945C4 mov [ebp-$3c],eax
0041A4BD 33C0 xor eax,eax
0041A4BF 5A pop edx
0041A4C0 59 pop ecx
0041A4C1 59 pop ecx
0041A4C2 648910 mov fs:[eax],edx
0041A4C5 68E2A44100 push $0041a4e2
0041A4CA 8D45DC lea eax,[ebp-$24]
0041A4CD E8F6B2FEFF call @UStrClr
0041A4D2 8D45D8 lea eax,[ebp-$28]
0041A4D5 E8EEB2FEFF call @UStrClr
0041A4DA C3 ret
0041A4DB E90CAAFEFF jmp @HandleFinally
0041A4E0 EBE8 jmp $0041a4ca
0041A4E2 837DC400 cmp dword ptr [ebp-$3c],$00
0041A4E6 0F9445CB setz byte ptr [ebp-$35]
0041A4EA 33C0 xor eax,eax
0041A4EC 5A pop edx
0041A4ED 59 pop ecx
0041A4EE 59 pop ecx
0041A4EF 648910 mov fs:[eax],edx
0041A4F2 682FA54100 push $0041a52f
0041A4F7 8D45E4 lea eax,[ebp-$1c]
0041A4FA E8C9B2FEFF call @UStrClr
0041A4FF 8D45E0 lea eax,[ebp-$20]
0041A502 E8C1B2FEFF call @UStrClr
0041A507 8D45DC lea eax,[ebp-$24]
0041A50A E8B9B2FEFF call @UStrClr
0041A50F 8D45D8 lea eax,[ebp-$28]
0041A512 E8B1B2FEFF call @UStrClr
0041A517 8D45D4 lea eax,[ebp-$2c]
0041A51A E8A9B2FEFF call @UStrClr
0041A51F 8D45D0 lea eax,[ebp-$30]
0041A522 E8A1B2FEFF call @UStrClr
0041A527 C3 ret
0041A528 E9BFA9FEFF jmp @HandleFinally
0041A52D EBC8 jmp $0041a4f7
redsfds.dpr.47: S1 := '';
0041A52F B8D81E4200 mov eax,$00421ed8
0041A534 E88FB2FEFF call @UStrClr
</pre>
<br />
Впечатлились?<br />
Вот и я тоже, впрочем я не оставлял надежды что когда-то все придет в норму.<br />
И вот вышла ХЕ8 и я сразу попросил проверить данный код, и что вы думаете?<br />
<br />
<pre class="brush:asm">contest.dpr.68: Utils.Namespace.CompareString(S1, S2);
0041B49A 8B15C02E4200 mov edx,[$00422ec0]
0041B4A0 A1BC2E4200 mov eax,[$00422ebc]
0041B4A5 E8827CFFFF call CompareStr
contest.dpr.69: Utils.Namespace.SameString(S1, S2);
0041B4AA 8B15C02E4200 mov edx,[$00422ec0]
0041B4B0 A1BC2E4200 mov eax,[$00422ebc]
0041B4B5 E8727CFFFF call CompareStr
0041B4BA 85C0 test eax,eax
0041B4BC 0F94C0 setz al
contest.dpr.70: s1 := '';
0041B4BF B8BC2E4200 mov eax,$00422ebc
0041B4C4 E8A7A9FEFF call @UStrClr
</pre>
<br />
Бинго.<br />
Кодогенератор отдали в правильные руки и он снова стал работать как нужно.<br />
А еще поправили несколько ошибок в дженериках, критичных лично для меня, но это уже не суть.<br />
<br />
Так вот вопрос, а почему ломали кодогенерацию столько версий подряд?<br />
Через тернии к звездам? :)<br />
<br />
Впрочем, теперь я уверен - на ХЕ8 переходить с ХЕ2 и выше стоит.<br />
<br />
<b>UPDATE:</b><br />
Ан нет - поторопился.<br />
В процессе тестов нашел интересный глюк, который не позволяет мне собрать проект. Причем билдится нормально, а на этапе компиляции происходит вот такое:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen="" class="YOUTUBE-iframe-video" data-thumbnail-src="https://i.ytimg.com/vi/bwAH2P9BnFY/0.jpg" frameborder="0" height="266" src="http://www.youtube.com/embed/bwAH2P9BnFY?feature=player_embedded" width="320"></iframe></div>
<br />
Впрочем, глюк не то чтобы интересный - глюк критический.<br />
<br />
<b>UPDATE2:</b><br />
Накидаю несколько своих мыслей по поводу данной ошибки.<br />
Завтра, конечно попробую выдать на гора что-то воспроизводящееся, но пока что просто суть.<br />
<br />
Сейчас мы сидим на XE4, и на нем наблюдается следующие типы ошибок (на самом главном и достаточно большом проекте под полтора лямов строчек).<br />
Если нажать F9 (просто компиляция) то может произойти:<br />
<br />
<ul style="text-align: left;">
<li>либо Internal Error </li>
<li>либо откроется какой либо модуль, к нему добавится пустая строчка в самом конце модуля и выведется сообщение что не могу данный модуль сохранить (как правило для модулей которые сидят в Programm Files).</li>
<li>либо - все будет хорошо (редкий случай).</li>
</ul>
<br />
Обходится это все обычным ребилдом проекта, после чего компиляция начинает работать нормально.<br />
<br />
Началось это с перехода на ХЕ.<br />
Подозреваю, что проблема из-за того что у нас очень большое количество модулей, ссылающихся друг на друга достаточно капитальное кол-во раз и где-то именно на анализе этих перекрестных ссылок линкер и начинает дурить.<br />
<br />
В данном случае, билд проходит успешно, но при компиляции (жмаем F9) происходит вот такая вот рекурсивная беда.<br />
<br />
Причем в данном случае это просто юнит-тест, который используется только один модуль (связку остальных он сам подтягивает).<br />
Хорошо что хоть с юнитом определился и завтра попробую это дело развязать и понять - что за оно такое.<br />
<br />
<b>UPDATE3:</b><br />
На 24 миллионах строчек скомпилированного кода оно успокоилось :)<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrD4i9JxVZuGx8lFpbhKSccU1Dln-YO0b1OjNDPgIsooUvbPLZVZN1E9IRWd8-dXYrpffluPqhdMDK19hW4pq9YZzfzA47fQF2tphd76pglC5p9NdlhzJ_tDUf1H3W7uGU_6HMnnBL0H49/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrD4i9JxVZuGx8lFpbhKSccU1Dln-YO0b1OjNDPgIsooUvbPLZVZN1E9IRWd8-dXYrpffluPqhdMDK19hW4pq9YZzfzA47fQF2tphd76pglC5p9NdlhzJ_tDUf1H3W7uGU_6HMnnBL0H49/s1600/2.png" height="364" width="640" /></a></div>
<br />
Выдав ошибку:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzRJmrvyu070iFuUAjBxI27a5sPO3_byLO5LQu75j_l-vQRZxRKgmi_A88QtUJ2kiXoPz-84M3Fuop0jBA6l2qyZhzZ6gD2QjkuFV5CtsIu_Qc2AmwFDBI0XshCpqj1raLb8a4a0fwhv4h/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzRJmrvyu070iFuUAjBxI27a5sPO3_byLO5LQu75j_l-vQRZxRKgmi_A88QtUJ2kiXoPz-84M3Fuop0jBA6l2qyZhzZ6gD2QjkuFV5CtsIu_Qc2AmwFDBI0XshCpqj1raLb8a4a0fwhv4h/s1600/3.png" /></a></div>
<br />
Однако ж - час компилилось.<br />
Сурово.<br />
<br />
<b>UPDATE4:</b><br />
Становится все интересней.<br />
В рамках тестирования создал новый VCL проект и подключил к нему один из тестируемых модулей.<br />
Результат компиляции:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4CRJGnSzAcYRvVSC7cANNfeNrebuU0HlWJVSXFO5092q6FoXnimuSe39pLfcpIFzmFcrPnJz6udEi194Iqo_KAODxVikdJ_I90tL9m6JsSgMsi2dM25uLX0qR4x5EDEFohWiIk-_K11jQ/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4CRJGnSzAcYRvVSC7cANNfeNrebuU0HlWJVSXFO5092q6FoXnimuSe39pLfcpIFzmFcrPnJz6udEi194Iqo_KAODxVikdJ_I90tL9m6JsSgMsi2dM25uLX0qR4x5EDEFohWiIk-_K11jQ/s1600/4.png" height="113" width="400" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Копаю дальше.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
<b>UPDATE5:</b></div>
<div class="separator" style="clear: both; text-align: left;">
А еще установщик утверждает что 13 свободных гигабайт ему мало :))</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiawE5beysMgqaS0VbBw3APPP6hgvh3Y8O0jb5guHt0uNMshz6g0HAkWbhqlW3yY7zUP1HY9LwDrOAuXrBNRLRORNNBvL6Fahn9kFBaGgPPbeHDVgC_KjgQCyqnvfcWCLbrWyKQm63VHBEG/s1600/1.bmp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiawE5beysMgqaS0VbBw3APPP6hgvh3Y8O0jb5guHt0uNMshz6g0HAkWbhqlW3yY7zUP1HY9LwDrOAuXrBNRLRORNNBvL6Fahn9kFBaGgPPbeHDVgC_KjgQCyqnvfcWCLbrWyKQm63VHBEG/s1600/1.bmp" height="328" width="640" /></a></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com18tag:blogger.com,1999:blog-2374465879949372415.post-42052711543370724592015-03-31T18:13:00.000+03:002015-03-31T18:21:20.734+03:00Работаем с Compound File<div dir="ltr" style="text-align: left;" trbidi="on">
С составными файлами я работаю давно, больше 15 лет. За все время работы у меня накопилось достаточно информации о плюсах и минусах составных файлов.<br />
С одной стороны они являются действительно очень удобным хранилищем информации, позволяющим менять данные на лету, с другой стороны это удобство частично нивелируется скоростью доступа к данным.<br />
Вообще для чего обычно используют составные файлы?<br />
Для всего, что нужно хранить в некоем контейнере.<br />
К примеру, файлы старых версий Microsoft Office от 97 до 2003 включительно (состоящие на самом деле из нескольких десятков файлов), хранились как раз в составном файле. Сейчас тоже хранятся, только в качестве контейнера используется ZIP.<br />
<br />
Инсталляционные пакеты MSI тоже являются составными файлами, и даже файл кэша эскизов папок Thumbs.db использует этот формат.<br />
<br />
Правда для того же Word есть целый комплекс утилит (Recovery for Word, Word Recovery Toolbox, Munsoft Easy Word Recovery) восстанавливающих, ну или по крайней мере пытающихся восстановить, поврежденные документы. Выводы можете сделать сами.<br />
Хотя, при должной работе с составными файлами проблему их повреждения можно решить (и я покажу как).<br />
<br />
Ну и, конечно же, несомненным плюсом этого формата является то, что внутри хранилища эмулируется полноценная файловая система со своими файлами и папками.<br />
<br />
Кстати, нюанс. Перед началом статьи я провел опрос на нескольких форумах, и выяснилось, что подавляющее большинство разработчиков не работают с составными файлами, причем по простой причине — не слышали что это такое.<br />
Вот сейчас и закроем этот пробел.<br />
<br />
<a name='more'></a><br />
<br />
<h3 style="text-align: left;">
1. Общие сведения о составных файлах и их создании</h3>
<br />
С ходу рассказывать структуру и внутренний формат составного файла я не буду, это лишнее.<br />
Для начала нужно его "пощупать" — что он из себя вообще представляет.<br />
<br />
Поэтому начнем с того, что создадим новый составной файл вызовом StgCreateDocfile.<br />
В uses подключим эту парочку ActiveX и AxCtrls (пригодятся).<br />
А теперь пишем:<br />
<br />
<pre class="brush:delphi">procedure CheckHResult(Code: HRESULT);
begin
if not Succeeded(Code) then
RaiseLastOSError;
end;
var
TestFilePath: string;
WideBuff: WideString;
Root: IStorage;
begin
TestFilePath := ExtractFilePath(ParamStr(0)) + '..\data\simple.bin';
ForceDirectories(ExtractFilePath(TestFilePath));
WideBuff := TestFilePath;
CheckHResult(StgCreateDocfile(@WideBuff[1],
STGM_CREATE or STGM_WRITE or STGM_SHARE_EXCLUSIVE,
0, Root));
</pre>
<br />
Прежде всего, обращу внимание на флаги.<br />
STGM_CREATE и STGM_WRITE — эти два флага используются для создания нового составного файла, причем наличие флага STGM_WRITE в данном случае обязательно (иначе никакого фокуса не получится ©).<br />
<br />
<b>ВАЖНО:</b><br />
А вот с третьим флагом STGM_SHARE_EXCLUSIVE все гораздо хитрее. Его наличие требуется всегда и везде, кроме открытия файла в режиме "только чтение", о чем говорится во второй главе.<br />
Можете проверить самостоятельно в <a href="https://www.hex-rays.com/products/ida/support/download_freeware.shtml" target="_blank">IDA Pro Freeware</a>.<br />
StgCreateDocfile вызывает функцию DfOpenDocfile, из которой происходит вызов VerifyPerms, в которой будет вот такая проверка:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7b8j0kwzh6nmPpanVqDQotTlBGIBWS9M9wGEFd6X0y8TT-VyIPktJTIS1zxiZsrB1KJ5596y5eRBQjD2wowl1CoG6wdFf2K2bJ_tZiXGS6pHVv8JBjFo76RQcjykWGTf7yTkxjF1QJkQ/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7b8j0kwzh6nmPpanVqDQotTlBGIBWS9M9wGEFd6X0y8TT-VyIPktJTIS1zxiZsrB1KJ5596y5eRBQjD2wowl1CoG6wdFf2K2bJ_tZiXGS6pHVv8JBjFo76RQcjykWGTf7yTkxjF1QJkQ/s1600/1.png" /></a></div>
<br />
<br />
По адресу 72554E62 происходит проверка наличия данного флага, и если его вдруг не будет обнаружено, вернется ошибка открытия. Таким образом, одновременное открытие составного файла на запись более одного раза запрещено.<br />
<br />
Для меня было несколько удивительно увидеть такую проверку в третьем кольце и я даже (эксперимента ради) ее занопил, после чего смог открыть файл на запись два раза одновременно. Но — корректно записать в оба файла, естественно, не получилось. :)<br />
<br />
На самом деле это достаточно грамотное решение, из-за самого формата хранения данных, но я на нем остановлюсь чуть позже, ближе к концу статьи.<br />
<br />
Если все проверки прошли успешно и код возврата StgCreateDocfile равен S_OK, то в четвертом параметре данной функции нам вернется интерфейс IStorage, указывающий на корневой элемент составного файла, с которым и будет происходить вся дальнейшая работа.<br />
Что мы можем сделать далее?<br />
<br />
К примеру, создать в корне новый файл (все же у нас файловая система) и записать в него некий блок данных.<br />
Пишем вот такую функцию:<br />
<br />
<pre class="brush:delphi">procedure WriteFile(Storage: IStorage; AName: WideString; Data: AnsiString);
var
Stream: IStream;
OS: TOleStream;
begin
CheckHResult(Storage.CreateStream(@AName[1],
STGM_WRITE or STGM_SHARE_EXCLUSIVE, 0, 0, Stream));
OS := TOleStream.Create(Stream);
try
OS.WriteBuffer(Data[1], Length(Data));
finally
OS.Free;
end;
end;
</pre>
<br />
В ней первым делом создаем новый "файл" вызовом функции Storage.CreateStream. Она практически идентична рассмотренной ранее StgCreateDocfile, только в качестве результата возвращает интерфейс <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa380034(v=vs.85).aspx" target="_blank">IStream</a>, посредством которого будет вестись работа с содержимым файла.<br />
<br />
Обратите внимание на флаги: STGM_SHARE_EXCLUSIVE должен быть указан обязательно, а вторым должен идти (в случае создания) либо STGM_WRITE, либо STGM_READWRITE, но т.к. составной файл был создан с использованием флага STGM_WRITE — используется именно он.<br />
<br />
Для удобства работа с IStream ведется посредством класса-прослойки TOleStream, который и производит запись данных.<br />
<br />
Это, конечно, не принципиальный момент, и можно было воспользоваться вызовом функции Write интерфейса ISequentialStream, наследником которого является IStream, но работать с классом TOleStream проще.<br />
Вызовем реализованную нами ранее функцию:<br />
<pre class="brush:delphi">WriteFile(Root, 'RootFile', 'First file data');
</pre>
<br />
В результате в корне появится файл с именем RootFile и содержимым "First file data".<br />
<br />
<b>ВАЖНО:</b><br />
Здесь есть один нюанс.Имена файлов и папок внутри составного файла не могут превышать длину в 31 юникодных символов (на самом деле не более 32, но нельзя забывать про терминирующий ноль).<br />
<br />
Да, именно так, папку или файл можно зазвать "123", но нельзя: "Мое длинное имя файла и еще много цифр". Более того по спецификации есть набор символов, которые нельзя использовать в наименовании (от 0 до 0x1F).<br />
<br />
Наверное, вы скажете — зачем такие ограничения, а вдруг я хочу развернуть огромную разветвленную файловую систему с огромной глубиной вложенности?<br />
Так не вопрос, в отличие от стандартных файловых ограничений, на вас не действует константа MAX_PATH.<br />
500 вложенных папок с именем "мое большое имя"?<br />
Это легко, таки работаем с виртуальной файловой системой — творите что хотите. :)<br />
<br />
Вернемся к нашим баранам: создадим в корне папку.<br />
<br />
<pre class="brush:delphi"> CheckHResult(Root.CreateStorage('SubFolder',
STGM_WRITE or STGM_SHARE_EXCLUSIVE, 0, 0, Folder));
</pre>
<br />
Код практически аналогичен вызову Storage.CreateStream, только в этот раз мы получим еще один интерфейс IStorage указывающий на только что созданную папку.<br />
<br />
Можем прямо сейчас создать в ней новый файл:<br />
<br />
<pre class="brush:delphi">WriteFile(Folder, 'SubFolderFile', 'Second file data');
</pre>
<br />
Для этого первым параметров укажем не Root, который ссылается на корень, а только что созданный Forder.<br />
<br />
<b>ВАЖНО:</b><br />
А теперь нюанс, если прямо сейчас закроем приложение — данные могут не сохраниться.<br />
Тут на самом деле не все так просто, к примеру, на моей домашней машине такое поведение воспроизводится гарантированно, а на рабочей с точностью наоборот.<br />
<br />
Чтобы гарантировать сохранение данных нужно выполнить следующий код:<br />
<br />
<pre class="brush:delphi">CheckHResult(Root.Commit(STGC_DEFAULT));
</pre>
<br />
После выполнения этого кода все данные будут гарантированно сохранены в файл на диске. Ну а если вы вдруг "внезапно" передумали, можно отменить все изменения, произошедшие с предыдущего коммита, вызвав такой код:<br />
<br />
<pre class="brush:delphi">CheckHResult(Root.Revert);
</pre>
<br />
Кстати, по поводу закрытия файла.<br />
Это делается банальным обниливанием рута, после чего при вызове @IntfClear для интерфейса в переменной Root произойдет разрушение всех остальных интерфейсов в иерархическом порядке.<br />
Что у нас еще осталось?<br />
<br />
Ага, еще методы CopyTo/MoveElementTo/EnumElements и прочее...<br />
С ними разберемся чуть позже, а пока что можно открыть архив, прилагающийся к статье и посмотреть на реализацию описанного выше кода <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">в файле "..\simple\StorageCreateDemo.dpr"</a><br />
<br />
Теперь пробуем всю эту беду прочитать.<br />
<br />
<h3 style="text-align: left;">
2. Чтение составного файла</h3>
<br />
Создадим новый проект, опять подключим ActiveX и AxCtrls и напишем код открытия:<br />
<br />
<pre class="brush:delphi">var
TestFilePath: string;
WideBuff: WideString;
Root: IStorage;
begin
TestFilePath := ExtractFilePath(ParamStr(0)) + '..\data\simple.bin';
WideBuff := TestFilePath;
CheckHResult(StgOpenStorage(@WideBuff[1], nil,
STGM_READ or STGM_SHARE_DENY_WRITE, nil, 0, Root));
</pre>
<br />
Так как доступ на запись нам не нужен, используем флаг STGM_READ и тут у нас есть выбор, использовать STGM_SHARE_DENY_WRITE или все же оставить STGM_SHARE_EXCLUSIVE (какой-то из двух флагов должен быть обязательно).<br />
<br />
Результат выполнения кода — переменная Root, класса IStorage, указывающая на корень.<br />
<br />
Как бы вы выполнили поиск файлов в указанной папке на диске?<br />
Естественно, рекурсивным обходом каталога, используя FindFirstFile.<br />
В данном случае у нас есть что-то похожее: это метод EnumElements интерфейса IStorage, вызов которого выглядит как-то так:<br />
<br />
<pre class="brush:delphi">var
Enum: IEnumStatStg;
begin
CheckHResult(Storage.EnumElements(0, nil, 0, Enum));
</pre>
<br />
Грубо говоря, это аналог вызова FindFirstFile, но тут мы получаем не хэндл, с которым можно работать далее, а интерфейс IEnumStatStg.<br />
<br />
Тут есть один интересный момент, на котором стоит заострить ваше внимание.<br />
Данный интерфейс (при его использовании) будет нам возвращать структуру TStatStg, одним из полей которой будет параметр pwcsName тип которого <b>POleStr</b>.<br />
<br />
Цимус данной ситуации поняли?<br />
<br />
Конечно же, это потенциальный мемлик, ибо OLE никоим разом не знает о существовании нашего родного менеджера памяти и выделяет блок под хранение данной строки своими собственными средствами, через интерфейс IMalloc.<br />
<br />
Если мы не будем обрабатывать данную ситуацию — память приложения потечет как водопад Виктория, но зато будет забавно смотреть на счетчики расхода памяти. :)<br />
<br />
Поэтому первым делом нужно получить ссылку на экземпляр данного интерфейса:<br />
<br />
<pre class="brush:delphi"> if (CoGetMalloc(1, ShellMalloc) <> S_OK) or (ShellMalloc = nil) then
raise Exception.Create('CoGetMalloc failed.');
</pre>
<br />
Он нам потребуется для освобождения выделенной не нами памяти.<br />
Примерно вот так:<br />
<br />
<pre class="brush:delphi">ShellMalloc.Free(TmpElement.pwcsName);
</pre>
<br />
Далее еще один нюанс:<br />
Тип данных в возвращаемой TStatStg может принимать следующие значения:<br />
<ul style="text-align: left;">
<li>STGTY_STORAGE — это папка</li>
<li>STGTY_STREAM — это файл</li>
</ul>
Все остальные варианты чисто служебные и нам не интересны.<br />
<br />
Смотрим как это происходит:<br />
<br />
<pre class="brush:delphi">procedure Enumerate(const Root: string; Storage: IStorage);
var
Enum: IEnumStatStg;
TmpElement: TStatStg;
ShellMalloc: IMalloc;
Fetched: Int64;
Folder: IStorage;
AFile: IStream;
begin
// т.к. работаем с OLE, сразу получим интерфейс на IMalloc
if (CoGetMalloc(1, ShellMalloc) <> S_OK) or (ShellMalloc = nil) then
raise Exception.Create('CoGetMalloc failed.');
// смотрим что нам доступно в данном сторадже
CheckHResult(Storage.EnumElements(0, nil, 0, Enum));
// перечисляем результаты до пока не упремся
Fetched := 1;
while Fetched > 0 do
if Enum.Next(1, TmpElement, @Fetched) = S_OK then
// проверочка (для подстраховки)
if ShellMalloc.DidAlloc(TmpElement.pwcsName) = 1 then
begin
// пишем что нашли
Write('Found: ', Root, '\', AnsiString(TmpElement.pwcsName));
// смотрим тип найденного
case TmpElement.dwType of
// если файл - выводим его имя и его содержимое
STGTY_STREAM:
begin
Writeln(' - file: ', sLineBreak);
CheckHResult(Storage.OpenStream(TmpElement.pwcsName, nil,
STGM_READ or STGM_SHARE_EXCLUSIVE, 0, AFile));
ShowFileData(AFile);
Writeln;
end;
// если папка - выводим ее имя и стартуем рекурсивный поиск уже в ней
STGTY_STORAGE:
begin
Writeln(' - folder');
CheckHResult(Storage.OpenStorage(TmpElement.pwcsName, nil,
STGM_READ or STGM_SHARE_EXCLUSIVE, nil, 0, Folder));
Enumerate(Root + '\' + string(TmpElement.pwcsName), Folder);
end;
else
Writeln('Unsupported type: ', TmpElement.dwType);
end;
// усе, данные нам уже не нужны - освобождаем выделенную память
ShellMalloc.Free(TmpElement.pwcsName);
end;
end;
</pre>
<br />
И теперь давайте посмотрим, что получится при чтении созданного в первой главе файла:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj78GZGULMTDIqJo6vJX55FoFwF3_CFU8KDOfGNWwdNvDBCg9__QL3rZvii1EDDSge2j6l18VJbE09AGCFsy1GUpdHFeCpCnAALU7iYs6lQbDJINtaMpjaSd-PHLSNbww6SYaTeusRjgeA/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj78GZGULMTDIqJo6vJX55FoFwF3_CFU8KDOfGNWwdNvDBCg9__QL3rZvii1EDDSge2j6l18VJbE09AGCFsy1GUpdHFeCpCnAALU7iYs6lQbDJINtaMpjaSd-PHLSNbww6SYaTeusRjgeA/s1600/2.png" /></a></div>
<br />
Собственно, это именно те данные, которые мы записали в первой главе.<br />
<br />
Код данного примера в архиве к статье, <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">по следующему пути "..\simple\StorageReadDemo.dpr"</a><br />
<br />
А теперь посмотрим, как с этим работать немного удобнее.<br />
<br />
<h3 style="text-align: left;">
3. Класс-обертка</h3>
<br />
В свое время мной был разработан небольшой модуль (тысяча строчек с комментариями), в котором реализовано несколько классов, учитывающих все нюансы работы с составными файлами и предоставляющих более удобный механизм работы.<br />
Его вы сможете найти в архиве, <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">в папке "..\StorageReader\FWStorage.pas"</a>.<br />
<br />
В нем есть несколько недочетов. Дело в том, что я забросил его разработку очень давно, поэтому на юникодных версиях Delphi он будет выдавать ворнинги связанные с работой со строками.<br />
<blockquote class="tr_bq">
<blockquote class="tr_bq">
[dcc32 Warning] FWStorage.pas(860): W1057 Implicit string cast from 'AnsiString' to 'string'</blockquote>
<blockquote class="tr_bq">
[dcc32 Warning] uStgReader.pas(102): W1057 Implicit string cast from 'ShortString' to 'string'</blockquote>
</blockquote>
Но при этом он вполне функционален и эти ворнинги никак не скажутся на его работоспособности. (Если честно — лень причесывать еще и их).<br />
<br />
Данный модуль вы можете использовать по своему усмотрению со следующими оговорками.<br />
<br />
Если вы вдруг будете изменять код классов (добавлять рюшечки, править ошибки, если найдете), и потом выкладывать его в интернет, имя автора модуля должно быть сохранено в заголовке.<br />
Я данный модуль уже не сопровождаю (он для меня устарел), поэтому просьбы о его доработке я буду отклонять сразу.<br />
Итак, из данного модуля нас интересует класс TFWStorage, при помощи которого ведется работа с составным файлом, и класс TFWStorageCursor, который является оберткой над IStorage.<br />
Для начала перечислю методы этих классов, а потом дам пример работы с ними.<br />
Итак, класс TFWStorage, он предназначен только для работы с файлом и предоставляет несколько утилитарных методов:<br />
<ul style="text-align: left;">
<li>OpenFile, OpenFileReadOnly - ну тут все понятно, просто открываем составной файл. Оба метода создают и возвращают класс TFWStorageCursor указывающий на корневую директорию файла.</li>
<li>CloseFile - соответственно закрываем открытый ранее файл.</li>
<li>ReConnect - переоткрываем открытый ранее файл. Также возвращает TFWStorageCursor.</li>
<li>Compress - сжимает указанный файл, убирая фрагментированные блоки. Сжимаемый файл не должен быть открыт.</li>
<li>IsStgValidBinaryFmt - проверяет, все ли в порядке с указанным файлом и не разрушена ли его структура. Указанный файл не должен быть открыт.</li>
<li>ForceStorage - создает или открывает папку внутри составного файла по указанному пути. Путь должен быть указан от корня, в качестве разделителя используется "\". Пример: "путь к файлу\Subfolder1\subfolder2\subsubfolder". Возвращает TFWStorageCursor.</li>
</ul>
Т.е. в принципе основная его задача отдать нам экземпляр класса TFWStorageCursor, при помощи которого и будет происходить основная работа с составным файлом.<br />
<br />
Методы у него следующие:<br />
<ul style="text-align: left;">
<li>CreateStorage - создает новую папку внутри текущей и возвращает TFWStorageCursor, указывающий на вновь созданную папку.</li>
<li>OpenStorage - открывает папку внутри текущей. Возвращает TFWStorageCursor, указывающий на открытую папку.</li>
<li>DeleteStorage - удаляет указанную папку внутри текущей.</li>
<li>Copy - копирует указанный файл или папку из текущей папки в другую. Папка, в которую производится копирование, передается в виде класса TFWStorageCursor.</li>
<li>MoveTo - аналогично методу Copy, только копируемый элемент удаляется из текущей папки.</li>
<li>CreateStream - создает пустой файл в текущей папке.</li>
<li>ReadStream - читает содержимое указанного файла.</li>
<li>WriteStream - пишет новые данные в файл. Если файла с таким именем не существует — создает его.</li>
<li>DeleteStream - удаляет файл в текущей папке.</li>
<li>FlushBuffer - сохраняет изменения.</li>
<li>Rename - переименовывает указанный файл или папку в текущей папке.</li>
<li>Enumerate - перечисляет содержимое текущей папки и возвращает результат в виде массива TFWStorageEnum.</li>
<li>Backward - возвращает ссылку на родительскую папку, причем сам разрушается (если только не Root).</li>
<li>Release - разрушает текущий класс.</li>
<li>IsRoot - показывает, указывает ли текущий класс на корневую папку составного файла.</li>
<li>GetName - возвращает имя текущей папки.</li>
<li>Path - возвращает путь к текущей папке.</li>
<li>Storages - список всех дочерних подпапок.</li>
</ul>
Как видите, обертки над IStream нет, работа с этим интерфейсом возложена на методы CreateStream, ReadStream, WriteStream.<br />
<br />
В массиве TFWStorageEnum, который возвращает метод Enumerate, не нужно освобождать память, выделенную под pacsName, это уже сделано, и вы работаете с копией данных, которые хранятся в памяти, выделенной родным менеджером памяти.<br />
Единственный вопрос может вызвать метод Backward, как так — почему он разрушает сам себя?<br />
<br />
А сейчас покажу, это действительно удобно.<br />
Вот, к примеру, если бы нам нужно было открыть такой путь: "путь к файлу\Subfolder1\subfolder2\subsubfolder", что нужно было сделать при использовании обычных интерфейсов из второй главы:<br />
Открыть сам файл и получить интерфейс IStorage указывающий на корень, потом получить IStorage для первой папки, потом для второй и для третьей, которая "subsubfolder" тоже нужно.<br />
Это целых 4 элемента, которые нужно где-то хранить.<br />
<br />
При использовании TFWStorage все становится гораздо проще:<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
var
Path: string;
Storage: TFWStorage;
Root, Folder: TFWStorageCursor;
Data: TStringStream;
begin
Storage := TFWStorage.Create;
try
// запоминаем путь к файлу
Path := ExpandFileName(ExtractFilePath(ParamStr(0)) + '..\data\test.bin');
// открываем составной файл
Storage.OpenFile(Path, True, Root);
// создаем в нем три вложенных друг в друга папки
Storage.ForceStorage(Path + '\Subfolder1\subloder2\subsubfolder', Folder);
Data := TStringStream.Create;
try
// это будут данные для файла
Data.WriteString('new file data.');
// пока не вышли на рут в каждой из папок создаем по файлу
while Folder <> Root do
begin
Folder.WriteStream(Folder.GetName + '_new_file.txt', Data);
// получаем ссылку на папку уровнем выше
Folder.Backward(Folder);
end;
// сохраняем изменения
Root.FlushBuffer;
finally
Data.Free;
end;
finally
Storage.Free;
end;
end;
</pre>
<br />
Вот и все, с точки зрения программирования получилось очень удобно.<br />
<br />
Ну а теперь напишем что-то более серьезное, а именно редактор содержимого составного файла.<br />
<br />
Откройте новый проект и создайте в нем примерно такую форму:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikjS0yxTTXXj-OzpSFhL95zaTSCMJLjezGzUDQQIX7O9buEIG9tWJ71kwnFrQSYLFmgo93HWKzaW-_P9uXG0q2ClPrA3vOQaaTxuFMpWwpvOW7ZUcVSyCZ0nqGrCPm4lxJzIPnE7hxk0I/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikjS0yxTTXXj-OzpSFhL95zaTSCMJLjezGzUDQQIX7O9buEIG9tWJ71kwnFrQSYLFmgo93HWKzaW-_P9uXG0q2ClPrA3vOQaaTxuFMpWwpvOW7ZUcVSyCZ0nqGrCPm4lxJzIPnE7hxk0I/s1600/2.png" height="430" width="640" /></a></div>
<br />
В приват добавим три переменных:<br />
<br />
<pre class="brush:delphi"> private
FCurrentFileName: string;
FStorage: TFWStorage;
FRoot: TFWStorageCursor;
</pre>
<br />
В конструкторе формы пишем такой код:<br />
<br />
<pre class="brush:delphi">procedure TForm1.FormCreate(Sender: TObject);
begin
// указываем текущее имя файла
FCurrentFileName :=
ExpandFileName(ExtractFilePath(ParamStr(0)) + '..\data\simple.bin');
// создаем хранилище
FStorage := TFWStorage.Create;
// открываем файл
OpenFile(False);
end;
</pre>
<br />
Теперь пишем саму процедуру открытия файла, она простая:<br />
<br />
<pre class="brush:delphi">procedure TForm1.OpenFile(CreateNew: Boolean);
begin
// закрываем файл, если он был открыт ранее
FStorage.CloseFile;
// открываем новый файл
FStorage.OpenFile(FCurrentFileName,
CreateNew or not FileExists(FCurrentFileName), FRoot);
Caption := FCurrentFileName;
// ну и выводим содержимое его корня
ShowStorageData(FRoot);
end;
</pre>
<br />
Пока все просто, да? В принципе и весь остальной код будет простеньким.<br />
<br />
Теперь пишем процедуру вывода содержимого папки на экран:<br />
<br />
<pre class="brush:delphi">procedure TForm1.ShowStorageData(AStorage: TFWStorageCursor);
procedure AddItem(const ACaption: string; AIndex: Integer);
begin
with ListView1.Items.Add do
begin
Caption := ACaption;
case AIndex of
-1: ImageIndex := -1;
1:
begin
ImageIndex := 0;
SubItems.Add('Folder');
end
else
ImageIndex := 1;
SubItems.Add('File');
end;
// тип элемента сохраняем в поле Data, где:
// -1 - переход на уровень выше
// 0 - файл
// 1 - папка
// потом будем ориентироваться на это поле
Data := Pointer(AIndex);
end;
end;
var
AData: TFWStorageEnum;
I: Integer;
begin
ListView1.Items.BeginUpdate;
try
ListView1.Items.Clear;
// выводим пункт, через который мы будем переходить на папку выше
// (для корневой директории - избыточно)
if not AStorage.IsRoot then
AddItem('..', -1);
// перечисляем все содержимое папки
AStorage.Enumerate(AData);
// и последовательно выводим в ListView
for I := 0 to AData.Count - 1 do
AddItem(
string(AData.ElementEnum[I].pacsName),
Byte(AData.ElementEnum[I].dwType = STGTY_STORAGE));
finally
ListView1.Items.EndUpdate;
end;
end;
</pre>
<br />
Если все сделали правильно, то запускайте проект, при этом откроется файл "..\data\simple.bin" который мы создавали в первой главе и все должно выглядеть как-то так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhEyhs_ScNZyv88GYxCC7OYpMZnAeGt68_edncq6gLy8gvIGItQ2zvGwyLa8UWjVTpWdvLX3h_hNJFP1jvEIO-QiPNETWTXMa8WiIm0Rfu2XS_9NBiQMksbLeG0ueHuRzI1POFUdTnkGj8/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhEyhs_ScNZyv88GYxCC7OYpMZnAeGt68_edncq6gLy8gvIGItQ2zvGwyLa8UWjVTpWdvLX3h_hNJFP1jvEIO-QiPNETWTXMa8WiIm0Rfu2XS_9NBiQMksbLeG0ueHuRzI1POFUdTnkGj8/s1600/3.png" height="126" width="640" /></a></div>
<br />
Теперь сделаем навигацию по нашему хранилищу.<br />
Логика ее будет простая:<br />
<ul style="text-align: left;">
<li>двойной клик по папке - открываем папку и показываем ее содержимое.</li>
<li>двойной клик по файлу - открываем редактор содержимого файла.</li>
<li>двойной клик по элементу ".." - переходим на уровень выше.</li>
</ul>
Для этого в обработчике события OnDblClick у ListView пишем такой код:<br />
<br />
<pre class="brush:delphi">procedure TForm1.ListView1DblClick(Sender: TObject);
begin
// если элемент не выбран - выходим
if ListView1.Selected = nil then Exit;
// ориентируемся на поле Data выбранного элемента
case Integer(ListView1.Selected.Data) of
-1: // переход на уровень выше
begin
// получаем ссылку на папку уровнем выше
FRoot.Backward(FRoot);
// показываем ее содержимое
ShowStorageData(FRoot);
end;
0: // редактируем файл
EditFile;
1: // открываем папку
begin
// получаем ссылку на выбранную папку
FRoot.OpenStorage(AnsiString(ListView1.Selected.Caption), FRoot);
// показываем ее содержимое
ShowStorageData(FRoot);
end;
end;
end;
</pre>
<br />
Вот теперь можно походить по нашему хранилищу двойными кликами. :)<br />
<br />
Редактирование файла сделаем следующим образом. Подключим к проекту новую форму, добавим на нее кнопку сохранения и кнопку отмены, а также TMemo в котором будет выводиться содержимое файла, после чего напишем такой код:<br />
<br />
<pre class="brush:delphi">procedure TForm1.EditFile;
var
Buff: TMemoryStream;
Data: AnsiString;
begin
Buff := TMemoryStream.Create;
try
// читаем содержимое файла
FRoot.ReadStream(AnsiString(ListView1.Selected.Caption), Buff);
// перекидываем его в строку
if Buff.Size > 0 then
begin
SetLength(Data, Buff.Size);
Buff.Read(Data[1], Buff.Size);
end;
// создаем окно редактора
frmEdit := TfrmEdit.Create(Self);
try
// передаем в Memo зачитанный текст
frmEdit.Memo1.Text := string(Data);
// отображаем диалог
if frmEdit.ShowModal <> mrOk then Exit;
// читаем содержимое из Memo
Buff.Clear;
Data := AnsiString(frmEdit.Memo1.Text);
if Length(Data) > 0 then
Buff.Write(Data[1], Length(Data));
// пишем его обратно
FRoot.WriteStream(AnsiString(ListView1.Selected.Caption), Buff);
// и сохраняем изменения
FRoot.FlushBuffer;
finally
frmEdit.Release;
end;
finally
Buff.Free;
end;
end;
</pre>
<br />
Ну, вот у нас практически полноценный редактор, осталось добавить функционал для кнопок сверху формы.<br />
<br />
Обработчики создания нового составного файла и открытия уже существующего выглядят так:<br />
<br />
<pre class="brush:delphi">procedure TForm1.btnCreateDFaseClick(Sender: TObject);
begin
if SaveDialog1.Execute then
begin
FCurrentFileName := SaveDialog1.FileName;
OpenFile(True);
end;
end;
procedure TForm1.btnOpenDBaseClick(Sender: TObject);
begin
if OpenDialog1.Execute then
begin
FCurrentFileName := OpenDialog1.FileName;
OpenFile(False);
end;
end;
</pre>
<br />
Это будет код кнопок создания новой папки и удаления существующей:<br />
<br />
<pre class="brush:delphi">procedure TForm1.btnAddFolderClick(Sender: TObject);
var
NewFolderName: string;
Tmp: TFWStorageCursor;
begin
if InputQuery('New folder', 'Enter folder name', NewFolderName) then
begin
FRoot.CreateStorage(AnsiString(NewFolderName), Tmp);
FRoot.FlushBuffer;
end;
ShowStorageData(FRoot);
end;
procedure TForm1.btnDelFolderClick(Sender: TObject);
begin
if Application.MessageBox(
PChar(Format('Delete folder: "%s"?', [ListView1.Selected.Caption])),
'Delete folder', MB_ICONQUESTION or MB_YESNO) = ID_YES then
begin
FRoot.DeleteStorage(AnsiString(ListView1.Selected.Caption));
FRoot.FlushBuffer;
ShowStorageData(FRoot);
end;
end;
</pre>
<br />
И тоже самое, только для кнопок открытия и удаления файла<br />
<br />
<pre class="brush:delphi">procedure TForm1.btnAddFileClick(Sender: TObject);
var
NewFileName: string;
begin
if InputQuery('New file', 'Enter file name', NewFileName) then
begin
FRoot.CreateStream(AnsiString(NewFileName));
FRoot.FlushBuffer;
end;
ShowStorageData(FRoot);
end;
procedure TForm1.btnDelFileClick(Sender: TObject);
begin
if Application.MessageBox(
PChar(Format('Delete file: "%s"?', [ListView1.Selected.Caption])),
'Delete file', MB_ICONQUESTION or MB_YESNO) = ID_YES then
begin
FRoot.DeleteStream(AnsiString(ListView1.Selected.Caption));
FRoot.FlushBuffer;
ShowStorageData(FRoot);
end;
end;
</pre>
<br />
Ну вот и все, проще по моему уже некуда :)<br />
<br />
Теперь можно посмотреть, а что-же хранится внутри DOC файла? :)<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirCrvFoh7wtUF47d6cdzKaTGKvUFGJ1W3eJlRiiKm1tcp4Uno_uh1Yk23L83y0GWHNr6_3jJMFo-oWv2F2EnDxrJkBeFyzthnjsUBUde3tL2RPE6Cwv_NMRfamcZ0qQMf3nErmRCGT9BU/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirCrvFoh7wtUF47d6cdzKaTGKvUFGJ1W3eJlRiiKm1tcp4Uno_uh1Yk23L83y0GWHNr6_3jJMFo-oWv2F2EnDxrJkBeFyzthnjsUBUde3tL2RPE6Cwv_NMRfamcZ0qQMf3nErmRCGT9BU/s1600/4.png" height="210" width="640" /></a></div>
<br />
На этом, пожалуй, остановимся и перейдем к описанию различных неприятностей, которые может доставить нам составной файл, а исходный код данного примера вы можете забрать в архиве, <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">по следующему пути: "..\StorageReader\"</a><br />
<br />
<h3 style="text-align: left;">
4. Минусы Compound File</h3>
<br />
Так уж получилось, что при всех своих плюсах, составной файл обладает целым рядом существенных минусов, с которыми вы можете столкнуться при работе.<br />
<br />
Самый первый минус — ограничение на размер имен для файлов и папок.<br />
<br />
Здесь я сделаю небольшое отступление и расскажу вам небольшую историю из собственной практики.<br />
Двенадцатый год я разрабатываю ПО для сметчиков — сметы делаем.<br />
Но у них все хитро, нельзя просто так взять и создать смету. :))<br />
Для начала должна быть указана стройка, у стройки должен быть перечень объектов строительства, а уже непосредственно объекты строительства содержат в себе сметы.<br />
Эта иерархия жесткая, причем есть несколько вариантов, к примеру:<br />
<ul style="text-align: left;">
<li>Стройка → Объект → Смета</li>
<li>Стройка → Очередь → Объект → Смета</li>
<li>Группа строек → Стройка → Очередь → Объект → Смета</li>
</ul>
По факту все эти стройки объекты и прочее (кроме смет) являются не более чем объектами иерархии — по сути папки, но эти папки должны идти в строго определенном порядке, иначе все сломается.<br />
<br />
Если представить что мы сделаем эту иерархию при помощи средств файловой системы, а тип папки (к какому уровню иерархии она принадлежит "стройка/объект/очередь") реализуем, к примеру, обычным INI файлом в ее корне (а-ля thumbs.db), то что нам делать с пользователем, у которого излишне шаловливые ручки, который может прямо в проводнике порушить всю эту структуру?<br />
<br />
Вот именно из этих соображений много лет назад нами и был выбран составной файл как хранилище данных, в который пользователь не сможет влезть и поломать там все.<br />
Используя этот формат хранения, мы можем контролировать нужную иерархию создания папок и не дать пользователю выстрелить себе в ногу.<br />
<br />
Однако вылез нюанс: создавая новую смету в нашем ПО, пользователь почему то старается в названии сметы отобразить полную информацию о том, что она осмечивает.<br />
К примеру: "Капитальный ремонт пути на старогодних материалах. Участок Селэгвож-Чим, 1путь, 142 пк1 - 163 пк10, протяженность 22, 0км".<br />
<br />
Вспоминаем — ограничение на длину имени файла у нас всего 31 символ, а это в крайнем случае: "Капитальный ремонт пути на стар".<br />
Нет, чтобы назвать: "Ремонт путёв".<br />
<br />
И естественно пользователь страшно обидится, если мы зарежем его имя файла по длине пути, поэтому пришлось что-то с этим делать.<br />
<br />
Нами была реализована следующая схема хранения данных:<br />
Что виртуальная папка, что виртуальный файл (виртуальный — отображаемый нашим ПО), в данной схеме на самом деле представлял из себя обычную папку в составном файле. Внутри этой папки лежал файлик "Properties", в котором описывался тип данных с которым мы работаем (если это папка, то, к какому типу в иерархии она принадлежит) а также отображаемое нашим ПО имя (причем ограничение на длину мы сделали аж в 1024 байта — не пожадничали, и с этим будет связана еще одна проблема, на которой я остановлюсь в самом конце).<br />
Если вдруг папка внутри хранилища являлась файлом — то в ней размещался еще один файлик — "Data", где и хранилось содержимое самой сметы.<br />
<br />
А выводилось это все пользователю в нормальном виде:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCTsULLkEbBHHnvzAbIBA8jFweJ6QhY_x3m6CYJutVbBJ3-j7JEgApd7FnuSoYyEruCVfudDXXCjIypJzRqvg3G4qxU38Zxk93LAGnFN5Lx_EgADYZARHCG7g4RV-5Rm093kJ2fXFmNv4/s1600/5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCTsULLkEbBHHnvzAbIBA8jFweJ6QhY_x3m6CYJutVbBJ3-j7JEgApd7FnuSoYyEruCVfudDXXCjIypJzRqvg3G4qxU38Zxk93LAGnFN5Lx_EgADYZARHCG7g4RV-5Rm093kJ2fXFmNv4/s1600/5.png" height="346" width="640" /></a></div>
<br />
Спросите, что мы использовали в качестве имен физических папок в данном случае?<br />
Да банальные усеченные GUID в строковом представлении с небольшой "эквилибристикой" чтобы их можно было впихнуть в пресловутые 31 байт имени папки. :)<br />
<br />
Да и ладно бы с этими 31 байтами имени, через какое-то время работы с данным форматом мы вышли на очередной неприятный минус.<br />
<br />
Вот задумайтесь, понравилось бы вам что ваше ПО может запускаться не меньше 5 минут?<br />
Да-да, я не ошибся, ровно 5 минут, а не секунд. Можно сходить попить чай, пиццу заказать, да и вообще отличное начало дня — ждем старта твоего рабочего инструмента. :)<br />
<br />
Есть у нас такие клиенты, называются "Проектные Институты" — их много по всей России и там работает огромное количество сметчиков. Они все профи и поэтому работают просто с "ОГРОМЕННЕЙШИМ" набором смет постоянно — работа такая.<br />
<br />
И вот однажды нам пришел багрепорт примерно такого плана: "Ребят, мы уже устали ждать запуска вашего ПО, да что ж такое каждый день?".<br />
<br />
А часть таких контор еще и работает с данными, которые нельзя отдавать на сторону (осмечивают некоторые госструктуры), поэтому мы никак не могли понять — откуда тормоза то такие дикие лезут?!!!<br />
<br />
Но однажды повезло, данные были не секретные и нам их предоставили.<br />
И вот, лежит у меня в папке <b>почти 2 гигабайтный файлик</b> с примерно 200 тысячами смет на борту (твою дивизию). Я конечно офигел от такого объема, но...<br />
Но действительно — хочешь не хочешь, файл такого размера быстрее, чем за пять минут просто не открылся (в следующей главе поймете почему).<br />
<br />
Начали тестировать скорость, и опытным путем было установлено: пользователю будет не комфортно работать на объеме составного файла размером уже в 50 мегабайт, ибо проявятся достаточно сильные задержки по две/три секунды на открытии.<br />
<br />
Тесты, конечно, тестами, но делать то что-то надо.<br />
В кратчайшие сроки была реализована сетевая служба, работающая с данными, которые хранились уже не в составном файле, а в базе. Причем, сразу добавили поддержку как банального Firebird/Interbase, так и баз посерьезней — MS SQL/Olracle, и до кучи прикрутили ADO. Еще была написана небольшая утилита, которая конвертировала данные из составного файла в базу.<br />
<br />
Тестируем — летает, мама не горюй, но есть нюанс.<br />
<br />
Нельзя просто так взять и прокинуть все данные из двухгигового файла в базу.<br />
В какой-то момент, при открытии очередной папки посредством OpenStorage, может произойти ошибка открытия, причем в этом случае дальше трепыхаться уже не стоит — любой вызов будет заканчиваться ошибкой.<br />
<br />
Вот именно для таких целей в TFWStorage и были добавлены два метода: ReConnect — посредством которого можно переоткрыть составной файл и метод ForceStorage, посредством которого можно сразу открыть ту папку, на которой произошла ошибка.<br />
<br />
Впрочем: на тот момент времени мы еще недостаточно набили шишек и использовали составной файл в еще одном нашем продукте. Этот проект был информационно справочной системой (ИСС) предоставляющий сметчику доступ к необходимой для него документации (скажем — местячковый аналог MSDN).<br />
<br />
И вот настает момент, ко мне приходит наш технарь, ответственный за наполнение базы и говорит: "база не открывается, я не могу выпустить очередное обновление".<br />
<br />
Начинаю проверять.<br />
Да действительно, база, представляющая из себя тотже составной файл с добавленными в него всеми данными не открывается прямо на этапе вызова StgOpenStorage.<br />
Приплыли...<br />
<br />
Смотрю размер — что-то в районе 4 гигов, но еще не вылезли за лимит.<br />
Методом проб и ошибок выяснили, что похоже дело с нехваткой памяти на этапе открытия файла. Победили переходом на другой формат хранения.<br />
<br />
Кстати, если спросите о времени открытия такого файла — да он открывался за достаточно солидное время, но (как я говорил ранее) — это практически полноценный MSDN для сметчиков и выполнен он был в виде сервиса, постоянно работающего на выделенном сервере. Люди туда стучались по сетке, так что в данном случае это был не сильно принципиальный момент.<br />
<br />
Кстати, натыкался еще на такой интересный глюк:<br />
К примеру мы хотим что-то записать в файл(стрим) и пробуем узнать — существует ли такой вообще, вызовом EnumElements. А IEnumStatStg, возвращаемый данным вызовом не видит такого стрима. После чего мы делаем вызов CreateStream с целью создать его, но нам приходит ошибка.<br />
<br />
В данном случае обойти этот момент довольно просто. Достаточно вызвать DestroyElement для данного стрима и создать его заново вызовом CreateStream, но это уже офигенный такой звонок: "что-то с нашим составным файлом совсем беда".<br />
<br />
А раз с ним вообще очень плохо, то нам нужно научиться вытаскивать из него данные, которые нам еще доступны.<br />
<br />
<h3 style="text-align: left;">
5. Читаем данные в RAW режиме</h3>
<br />
Я думаю, у вас сейчас сложилось такое мнение: Ну, нифига себе, сколько проблем будет при использовании составных файлов? Зачем автор вообще тогда о них рассказывает?<br />
<br />
Это не правильное мнение. Я могу накидать целый ворох ошибок по различным используемым сейчас технологиям, но это не означает что та или иная, изначально была провальна.<br />
<br />
К примеру, если бы вы знали что можно сделать принципиально неудаляемую папку в файловой системе NTFS прямо из третьего кольца и без сильных времязатрат, просто некорректными параметрами вызова соответствующего API — вы же не отказались бы от использования файловой системы? :)<br />
<br />
А составные файлы действительно хороши, но их нужно просто научиться правильно готовить.<br />
<br />
Вообще сейчас хорошее время, MS <a href="https://msdn.microsoft.com/en-us/library/dd942138.aspx" target="_blank">публикует описание своих технологиий</a> в открытом виде, а когда я начинал работать с данным форматом мне было доступно только описание формата из <a href="http://poi.apache.org/poifs/fileformat.html" target="_blank">явовского POIFS</a>, небольшая <a href="http://en.wikipedia.org/wiki/Compound_File_Binary_Format" target="_blank">выдержка из Wiki</a> да еще один файлик с более подробным описанием по структуре POIFS (в частности по министримам), но я его сейчас уже не могу найти (столько лет прошло).<br />
<br />
Вот почему они открыли формат не тогда, когда мне это было нужно? :)<br />
Поэтому пришлось все ковырять самому.<br />
<br />
Смотрим, что из себя представляет составной файл, а именно его заголовок:<br />
<br />
<pre class="brush:delphi"> TPoifsFileHeader = packed record
// Идентификатор. Всегда постоянная (0 x E011CFD0, 0 x E11AB1A1)
_abSig: array [0..7] of Byte;
// Class ID. Устанавливается WriteClassStg, считывается GetClassFile/ReadClassStg.
// Для Excel как правило = 0
_clid: TGUID;
// Младшее значение версии формата.
_uMinorVersion: USHORT;
// Старшее значение версии Dll/формата
_uDllVersion: USHORT;
// 0 x FFFE говорит, что используется Intel нотация
_uByteOrder: USHORT;
// Размер сектора. Обычно равно 9, что указывает на размер 512 байт (2 ^ 9)
_uSectorShift: USHORT;
// Размер мини-сектора. Обычно равно 6, что указывает на размер 64 байт (2 ^ 6)
_uMiniSectorShift: USHORT;
// Зарезервировано, должно быть равно 0
_usReserved: USHORT;
// Зарезервировано, должно быть равно 0
_ulReserved1: ULONG;
// Зарезервировано, должно быть равно 0
_ulReserved2: ULONG;
// Число секторов, в которых размещается FAT.
// Если файл менее 7Мб, то равно 1, если больше, то больше 1 и появляется DIF сектор.
_csectFat: ULONG;
// Номер первого сектора, в котором размещается Property Set Storage
// (еще называют FAT Directory или Root Directory Entry)
_sectDirStart: ULONG;
// Подпись для транзакций.
_signature: ULONG;
// Максимальный размер мини-потока. Обычно 4096
_ulMiniSectorCutoff: ULONG;
// Первый сектор мини-FAT.
// Если (-2), то мини-поток отсутствует.
_sectMiniFatStart: ULONG;
// Число секторов в цепочке мини-FAT. 0, если мини-потока нет
_csectMiniFat: ULONG;
// Первый сектор в DIF цепочке.
// Если файл менее 7Мб, то DIF цепочка отсутствует и значение равно (-2)
_sectDifStart: ULONG;
// число секторов в DIF цепочке.0, если файл < 7Мб
_csectDif: ULONG;
// Номера первых 109 секторов, в которых располагается FAT.
// Если файл менее 7Мб, то сектор один, остальные значение заполняются (-1).
_sectFat: array [0..108] of ULONG;
end;</pre>
<br />
Ну, тут думаю все понятно, все каменты проставлены — эту структуру нам нужно считать в самом начале.<br />
<br />
Единственный момент, в полях _uSectorShift и _uMiniSectorShift записаны степени двойки, значит, нужно их привести к нормальному виду.<br />
<br />
<pre class="brush:delphi">procedure TPoifsFile.InitHeader;
begin
FStream.ReadBuffer(FHeader, SizeOf(TPoifsFileHeader));
FHeader._uSectorShift := Round(IntPower(2, FHeader._uSectorShift));
FHeader._uMiniSectorShift := Round(IntPower(2, FHeader._uMiniSectorShift));
end;
</pre>
<br />
Далее необходимо считать FAT, хранящий данные о файлах, размер которых больше или равен значению поля _ulMiniSectorCutoff в заголовке.<br />
<br />
<pre class="brush:delphi">procedure TPoifsFile.ComposeFAT;
var
I, J, X, FatLength: Integer;
FatBlock: TPoifsFatBlock;
CurrentFat, Offset: Integer;
XFat: array of Integer;
begin
// рассчитываем кол-во элементов FAT (идут блоками по 128 записей)
// если нет DIF сектора, то _csectFat равен единице
FatLength := FHeader._csectFat * 128;
// выделяем память под значение ячеек FAT
SetLength(FFat, FatLength);
// и оффсеты на их значения в файле
SetLength(FFatOffset, FatLength);
// если есть DIF сектор, то FAT располагается только в первых 109 полях
// остальные данные лежат в DIF секторе
for I := 0 to IfThen(FHeader._csectDif > 0, 108, FHeader._csectFat - 1) do
begin
// читаем FAT структуру кусками по 128 записей
FatBlock := TPoifsFatBlock(GetBlock(FHeader._sectFat[I]));
for J := 0 to 127 do
begin
FFat[I * 128 + J] := FatBlock[J];
// не забываем про оффсет каждого блока в файле, пригодится
FFatOffset[I * 128 + J] := FStream.Position - SizeOf(TPoifsBlock);
end;
end;
// смотрим, есть ли DIF сектор
if FHeader._sectDifStart = 0 then Exit;
// если есть, значит надо читать оставшиеся блоки FAT из него
Offset := FHeader._sectDifStart;
// подготавливаем массив XFAT для хранения оффсетов на FAT блоки
SetLength(XFat, 128);
// запоминаем индекс последнего заполненного FAT блока
CurrentFat := 13951; //109 * 128 - 1 BAT
for X := 0 to FHeader._csectDif - 1 do
begin
// ориентируясь на размер сектора (параметр _uSectorShift из заголовка)
// рассчитываем позицию в файле
FStream.Position := GetBlockOffset(Offset);
// читаем смещения оставшихся FAT блоков
FStream.ReadBuffer(XFat[0], 128 * SizeOf(DWORD));
// в самом конце блока оффсетоф
// содержится смещение на начало следующего блока офсетоф
// поэтому крутим цикл без учета последнего блока
for I := 0 to 126 do
begin
// не забываем проверять текущий оффсет, если он отрицателен,
// блоков FAT больше нет
if XFat[I] < 0 then Exit;
// читаем FAT структуру кусками по 128 записей
FatBlock := TPoifsFatBlock(GetBlock(XFat[I]));
for J := 0 to 127 do
begin
Inc(CurrentFat);
FFat[CurrentFat] := FatBlock[J];
FFatOffset[CurrentFat] := FStream.Position - SizeOf(TPoifsBlock);
end;
end;
// новый оффсет берем из последнего элемента
Offset := XFat[127];
end;
end;
</pre>
<br />
Здесь используется структура TPoifsFatBlock, это просто массив из 128 Integer.<br />
А так же функции GetBlockOffset и GetBlock. Они простые по своей сути.<br />
<br />
<pre class="brush:delphi">function TPoifsFile.GetBlockOffset(BlockIndex: Integer): Int64;
begin
Result := HEADER_SIZE + FHeader._uSectorShift * BlockIndex;
end;
function TPoifsFile.GetBlock(Adress: Integer): TPoifsBlock;
begin
FStream.Position := GetBlockOffset(Adress);
FStream.ReadBuffer(Result, SizeOf(TPoifsBlock));
end;
</pre>
<br />
Следующим этапом нужно считать минифат, хранящий данные о файлах, размер которых меньше значению поля _ulMiniSectorCutoff.<br />
<br />
<pre class="brush:delphi">procedure TPoifsFile.ComposeMiniFat;
var
I, CurrChain: Integer;
TmpPosition: int64;
begin
// запоминаем номер первого сектора цепочки блоков минифата
CurrChain := FHeader._sectMiniFatStart;
// выделяем под него память (так-же идет блоками по 128 элементов)
SetLength(FMiniFat, FHeader._csectMiniFat * 128);
I := 0;
while Integer(CurrChain) >= 0 do
begin
// смотрим оффсет сектора
TmpPosition := GetBlockOffset(CurrChain);
// если отрицательный, значит цепочка закончилась
if TmpPosition < 0 then Exit;
//if TmpPosition > FStream.Size then Exit;
FStream.Position := TmpPosition;
// читаем смещения
FStream.ReadBuffer(FMiniFat[I], 512 {128 * SizeOf(DWORD)});
Inc(I, 128);
// индекс нового сектора читаем из FAT
CurrChain := FFat[CurrChain];
end;
end;
</pre>
<br />
Последним этапом нужно зачитать свойства всех файлов и папок.<br />
<br />
Они будут храниться в массиве вот таких структур:<br />
<br />
<pre class="brush:delphi"> TPoifsProperty = packed record // 128 length
// имя файла/папки
Caption: array[0..31] of WChar;
// размер имени
CaptionSize: Word;
// тип элемента STGTY_ХХХ
PropertyType: Byte;
// цвет узла (массив TPoifsProperty представляет из себя Red-Black-Tree)
NodeColor: Byte; // 0 (red) or 1 (black)
// номер предыдущего блока в массиве
PreviousProp: Integer;
// номер следующего блока в массиве
NextProp: Integer;
// номер дочернего блока в массиве
ChildProp: Integer;
Reserved1: TGUID;
UserFlags: DWORD;
// время
ATime: array [0..1] of Int64;
// номер ячейки FAT указывающей на начало блока данных для файла
StartBlock: Integer;
// размер файла
Size: Integer;
Reserved2: DWORD;
end;
TPoifsPropsBlock = array[0..3] of TPoifsProperty;
</pre>
<br />
А читать их будем следующим кодом:<br />
<br />
<pre class="brush:delphi">function TPoifsFile.ReadPropsArray: Boolean;
var
I, J, Len: Integer;
PropsBlock: TPoifsPropsBlock;
begin
Result := True;
// инициализируем размер массива свойств
Len := 0;
// запоминаем номер первого сектора, в котором размещается Property Set Storage
J := FHeader._sectDirStart;
repeat
// читаем свойства блоками по 4 элемента
Inc(Len, 4);
SetLength(FPropsArray, Len);
PropsBlock := TPoifsPropsBlock(GetBlock(J));
for I := 0 to 3 do
FPropsArray[Len - 4 + I] := PropsBlock[I];
// читаем номер следующего сектора из FAT
J := FFat[J];
until J = ENDOFCHAIN;
end;
</pre>
<br />
После этого у нас будет на руках:<br />
<ol style="text-align: left;">
<li>массив значений FAT, каждое из которых содержит в себе номер секции с данными.</li>
<li>массив смещений на данные в файле</li>
<li>массив значений MiniFAT</li>
<li>массив свойств всех файлов</li>
</ol>
Что есть FAT и MiniFAT?<br />
Грубо говоря составной файл представляет собой заголовок и массив секторов данных размером FHeader._uSectorShift, в которых лежит все остальное.<br />
FAT содержит в себе порядок хранения данных в этих секторах (как содержимого файлов, так и сугубо служебных блоков не доступных пользователю).<br />
К примеру, есть у нас файл размером 1 мегабайт, под него выделится ровно 2048 секторов, каждый из которых будет размером в 512 байт (по умолчанию).<br />
Из-за фрагментации данные этого файла не всегда будут идти последовательно и, вполне возможна такая ситуация, что первые 10 секторов будут содержать в себе конец файла, а оставшиеся — его начало.<br />
Чтобы понять, что за чем идет (порядок последовательности) нужно обратится к FAT и полю StartBlock из структуры TPoifsProperty, скомбинировав значения которых мы поймем, какой сектор содержит начало блока данных, а где его продолжение (просто пробежавшись по цепочке FAT).<br />
Причем с минифатом будет все гораздо интереснее, там придется еще делать некоторые манипуляции, но об этом чуть попозже.<br />
<br />
Впрочем, я немного отвлекся.<br />
Для начала нужно решить другую задачу. Все элементы, представленные в массиве TPoifsProperty, связаны друг с другом посредством полей PreviousProp, NextProp и ChildProp, а также NodeColor. Классическое Red-Black-Tree.<br />
Нам нужно из него построить стандартное дерево, восстановив структуру папок и файлов.<br />
<br />
А вот когда мы ее построим, тогда и можно вытащить все данные из файла с сохранением их структуры.<br />
<br />
Создадим новый проект, примерно вот такой:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEglUWXJ0X8LgyEIYFJFrJuKsukWqOyMAAWmhK0_yDKbNxAambyF7-EFE8FazFNqD8nt_YSG-cmbpe2pMiRvOxcKmEFnAFg5cYAE0TyxE0ihUnlBjLvJjJpN0oPGCf5YEbmrWTY0Nobx_fo/s1600/_6.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEglUWXJ0X8LgyEIYFJFrJuKsukWqOyMAAWmhK0_yDKbNxAambyF7-EFE8FazFNqD8nt_YSG-cmbpe2pMiRvOxcKmEFnAFg5cYAE0TyxE0ihUnlBjLvJjJpN0oPGCf5YEbmrWTY0Nobx_fo/s1600/_6.png" height="400" width="330" /></a></div>
<br />
Суть его будет заключаться в следующем: из указанного составного файла он будет извлекать структуру файлов и папок (отобразив ее в TreeView), после чего воспроизведет такую же иерархию папок в указанной директории и вытащит туда же данные всех файлов из хранилища.<br />
<br />
Сделаем это в пять этапов, которые наглядно видны в обработчике кнопки Extract<br />
<br />
<pre class="brush:delphi">begin
FileStream := TFileStream.Create(edSrc.Text, fmOpenReadWrite);
try
AFile := TPoifsFile.Create(FileStream);
try
// читаем данные из составного файла
AFile.LoadFromStream;
ATree := TStorageTree.Create;
try
// Заполняем узлы
for I := 0 to AFile.PropertiesCount - 1 do
ATree.AddNode(I).Data := AFile[I];
// строим перекрестные ссылки
FillAllChilds(0, ATree.GetNode(0).Data.ChildProp);
// выводим ввиде дерева
TreeView1.Items.Clear;
FillTree(nil, 0);
// извлекаем все данные
DebugLog := TStringList.Create;
try
Extract(IncludeTrailingPathDelimiter(edDst.Text), 0);
if DebugLog.Count > 0 then
DebugLog.SaveToFile(IncludeTrailingPathDelimiter(edDst.Text) + 'cannotread.log');
finally
DebugLog.Free;
end;
finally
ATree.Free;
end;
finally
AFile.Free;
end;
finally
FileStream.Free;
end;
end;
</pre>
<br />
Первый этап (чтение данных из файла) у нас уже реализован.<br />
Перейдем ко второму и третьему.<br />
<br />
Я не стал сильно мудрить и для восстановления структуры дерева взял за основу решения граф.<br />
<br />
Идея простая, сначала добавим N узлов к графу, где каждый узел будет отвечать за один из элементов массива TPoifsProperty (собственно это видно в коде, блок "заполняем узлы").<br />
А следующим шагом нужно построить между узлами перекрестные ссылки, кто и на что ссылается.<br />
<br />
Вообще само дерево строится достаточно просто, главное придерживаться нескольких простых правил:<br />
<ul style="text-align: left;">
<li>TPoifsProperty.ChildProp - всегда указывает на первый дочерний элемент в папке (заполнено только у папок)</li>
<li>TPoifsProperty.PreviousProp - указывает на предыдущий элемент в рамках текущей папки.</li>
<li>TPoifsProperty.NextProp - указывает на следующий элемент в рамках текущей папки.</li>
</ul>
Чтобы было нагляднее вот вам картинка:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLRl0Vh1sAMc6y9EDhc0DwG0KKiEZSBCVJUsNJtfD72ifuiw5kxCjsn51q_U0h1E8u9csGJOLPDNOPtruqZ56u2rODYygwuprynABxMD5csLMb-us5UbabVo5iDckmV-5lmn7pkrDN52k/s1600/_7.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLRl0Vh1sAMc6y9EDhc0DwG0KKiEZSBCVJUsNJtfD72ifuiw5kxCjsn51q_U0h1E8u9csGJOLPDNOPtruqZ56u2rODYygwuprynABxMD5csLMb-us5UbabVo5iDckmV-5lmn7pkrDN52k/s1600/_7.png" /></a></div>
Стрелка вниз, это ChildProp, вправо — NextProp, влево — PreviousProp.<br />
В итоге сразу становится понятно, что в корне составного файла расположены два файла и одна папка, внутри которой размещены еще три файла.<br />
<br />
Впрочем, давайте посмотрим, как будет выглядеть третий этап, а именно построение ссылок между узлами графа.<br />
<br />
<pre class="brush:delphi">var
ATree: TStorageTree;
...
procedure FillAllChilds(RootIndex, CurrentIndex: Integer);
var
SubChildIndex: Integer;
RootNode, CurrNode, ChildNode: TStorageElement;
begin
if CurrentIndex < 0 then Exit;
// получаем ссылку на рутовый узел
RootNode := ATree.GetNode(RootIndex);
// получаем ссылку на добавляемый в него узел
CurrNode := ATree.GetNode(CurrentIndex);
if CurrNode = nil then Exit;
// если узел уже обработан - выходим
if CurrNode.Added then Exit;
CurrNode.Added := True;
// добавляем ссылку на него от рута
ATree.AddVector(RootNode, CurrNode);
// и у вновь добавленного вызываем добавление его чайлдов
FillAllChilds(CurrNode.ID, CurrNode.Data.ChildProp);
// теперь смотрим есть ли перед добавленным узлом еще элементы
SubChildIndex := CurrNode.Data.PreviousProp;
while SubChildIndex >= 0 do
begin
// если есть, добавляем их к руту
FillAllChilds(RootIndex, SubChildIndex);
ChildNode := ATree.GetNode(SubChildIndex);
if ChildNode <> nil then
SubChildIndex := ChildNode.Data.PreviousProp
else
SubChildIndex := -1;
end;
// то же самое делаем со всеми элементами, которые идут после текущего узла
SubChildIndex := CurrNode.Data.NextProp;
while SubChildIndex >= 0 do
begin
FillAllChilds(RootIndex, SubChildIndex);
ChildNode := ATree.GetNode(SubChildIndex);
if ChildNode <> nil then
SubChildIndex := ChildNode.Data.NextProp
else
SubChildIndex := -1;
end;
end;
</pre>
<br />
Реализацию класса графа, представленного классом TStorageTree я рассматривать не буду, так как она не относится к теме статьи, код данного класса потом <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">увидите в исходниках</a>.<br />
Сейчас достаточно знать, что у него есть метод GetNode, возвращающий узел графа по его индексу (у которого есть ссылка на элемент массива TPoifsProperty, контролируемый им через свойство Data) и метод AddVector, создающий ссылку между двумя узлами графа.<br />
<br />
Теперь четвертый этап — на основе графа строим дерево папок и файлов.<br />
<br />
<pre class="brush:delphi"> procedure FillTree(Node: TTreeNode; RootNodeIndex: Integer);
var
W: WideString;
TreeNode: TTreeNode;
I: Integer;
RootStorageNode, ChildStorageNode: TStorageElement;
begin
// получаем узел графа
RootStorageNode := ATree.GetNode(RootNodeIndex);
// добавляем его в дерево (назначаем имя и иконку)
W := RootStorageNode.Data.Caption;
TreeNode := TreeView1.Items.AddChildFirst(Node, W);
case RootStorageNode.Data.PropertyType of
STGTY_STORAGE: TreeNode.ImageIndex := 0;
STGTY_STREAM: TreeNode.ImageIndex := 1;
end;
// бежим по ссылкам от узла
for I := 0 to RootStorageNode.VectorCount - 1 do
begin
// смотрим, есть ли линк на дочерний узел (а вдруг мы папка?)
ChildStorageNode := TStorageElement(RootStorageNode.GetVector(I).SlaveNode);
if ChildStorageNode = nil then
Continue;
// если есть ссылка, и ссылка не на папку выше, то пускаем рекурсию
if ChildStorageNode.ID <> RootNodeIndex then
FillTree(TreeNode, ChildStorageNode.ID);
end;
end;
</pre>
<br />
Банальная рекурсия.<br />
Грубо бежим по узлам графа, начиная от корня, и ссылки на подчиненные узлы (получаемые из ребер графа GetVector(I).SlaveNode) являются тем, что хранится в текущей папке.<br />
<br />
Спросите, почему не сделал построение дерева структуры на "Red-Black-Tree" с учетом поля NodeColor?<br />
А шут его знает. Я этот алгоритм писал десяток лет назад и у меня есть золотое правило: "работает — не трогай там ничего". :)<br />
<br />
И вот теперь мы подошли к пятому этапу — извлекаем данные во внешнюю папку.<br />
Но для этого нужно прояснить для себя — как именно можно получить весь набор данных, ассоциированных с файлом.<br />
<br />
Помните что я говорил про секцию FAT — данные не всегда будут идти последовательно и их нужно получать, ориентируясь на индексы секций, прописанных в FAT.<br />
<br />
Смотрите, как можно получить данные "больших файлов" (размер которых больше или равен значению, хранящемуся в поле _ulMiniSectorCutoff заголовка):<br />
<br />
<pre class="brush:delphi">procedure TPoifsFile.GetDataFromStream(ChainStart: ULONG;
NeedLength: DWORD; const Stream: TStream);
begin
Stream.Size := 0;
while (Integer(ChainStart) >= 0) and (Stream.Size < NeedLength) do
begin
// получаем смещение на начало стрима
FStream.Position := GetBlockOffset(ChainStart);
// получаем указатель на следующий сектор
ChainStart := FFat[ChainStart];
// читаем часть данных
Stream.CopyFrom(FStream, FHeader._uSectorShift);
end;
// финальная правка
if Stream.Size > NeedLength then
Stream.Size := NeedLength;
end;
</pre>
<br />
Финальная правка нужна из-за того что данные в секторах хранятся блоками, а реальный размер файла не всегда кратен их размеру.<br />
Согласен, стоит переписать немного, чтобы убрать финальную правку, но — оно мне надо? :)<br />
<br />
Впрочем, по параметрам, нас больше заинтересует ChainStart, в котором нет ничего секретного. Это значение поля StartBlock из структуры TPoifsProperty.<br />
<br />
А вот так получаем данные для файлов меньшего размера, чем _ulMiniSectorCutoff .<br />
<br />
<pre class="brush:delphi">procedure TPoifsFile.GetDataFromMiniStream(ChainStart: ULONG;
NeedLength: DWORD; const Stream: TStream);
var
MiniStreamOffset: DWORD;
RealMiniStreamSector, TmpChain: Integer;
begin
Stream.Size := 0;
while (Integer(ChainStart) >= 0) and (Stream.Size < NeedLength) do
begin
// Смотрим в каком секторе должен располагаться данный Ministream
TmpChain := ChainStart;
RealMiniStreamSector := Properties[0].StartBlock;
while TmpChain >= 8 do
begin
Dec(TmpChain, 8);
RealMiniStreamSector := FFat[RealMiniStreamSector];
end;
// Получаем смещение сектора
MiniStreamOffset := GetBlockOffset(RealMiniStreamSector);
// получаем смещение на начало министрима
FStream.Position := MiniStreamOffset +
(ChainStart mod 8) * FHeader._uMiniSectorShift;
// получаем указатель на следующий блок министрима
ChainStart := FMiniFat[ChainStart];
// читаем часть данных
Stream.CopyFrom(FStream, FHeader._uMiniSectorShift);
end;
// финальная правка
if Stream.Size > NeedLength then
Stream.Size := NeedLength;
end;
</pre>
<br />
Немножко хитрее, да?<br />
Правда, если присмотреться, изменения тут только с расчетом офсета через TmpChain и тот же FAT. Если сектор выбивается за рамки дозволенного (восьмерку) то идем по цепочке FAT начиная от рута, пока TmpChain не станет меньше допустимого значения, ибо ссылка на минифат лежит именно в руте.<br />
<br />
Вот теперь можно написать процедуру извлечения данных любого файла, который мы укажем:<br />
<br />
<pre class="brush:delphi"> procedure GetStorageData(ANode: TStorageElement; const Stream: TStream);
begin
if ANode.Data.Size < Integer(AFile.Header._ulMiniSectorCutoff) then
AFile.GetDataFromMiniStream(ANode.Data.StartBlock, ANode.Data.Size, Stream)
else
AFile.GetDataFromStream(ANode.Data.StartBlock, ANode.Data.Size, Stream);
end;
</pre>
<br />
В данную процедуру мы будем передавать узел графа, который извлекается из составного файла в папку, и на основании его размера, вызывать один из реализованных выше методов.<br />
<br />
Вроде бы практически все, остался последний пятый этап — распаковка всего содержимого составного файла в папку.<br />
<br />
<pre class="brush:delphi"> procedure Extract(Path: string; RootNodeIndex: Integer);
var
W: WideString;
I: Integer;
RootStorageNode, ChildStorageNode: TStorageElement;
F: TFileStream;
begin
RootStorageNode := ATree.GetNode(RootNodeIndex);
W := RootStorageNode.Data.Caption;
case RootStorageNode.Data.PropertyType of
STGTY_STORAGE:
Path := Path + W + '\';
STGTY_STREAM:
begin
try
ForceDirectories(Path);
F := TFileStream.Create(Path + W, fmCreate);
try
GetStorageData(RootStorageNode, F);
finally
F.Free;
end;
except
DebugLog.Add(Path + W);
end;
end;
end;
for I := 0 to RootStorageNode.VectorCount - 1 do
begin
ChildStorageNode := TStorageElement(RootStorageNode.GetVector(I).SlaveNode);
if ChildStorageNode = nil then
Continue;
if ChildStorageNode.ID <> RootNodeIndex then
Extract(Path, ChildStorageNode.ID);
end;
end;
</pre>
<br />
Ну, здесь уже без комментариев. Все мы видели ранее — обычный алгоритм.<br />
<br />
Если запустить созданный нами проект и натравить его на какой-нибудь вордовский документ, то выглядеть будет вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSxwnF1GLo6U5WoDb0seH6ahJ_puh5UJT0_5q1x-II0B2U_EQzjf1xlBySGyQF2Wss6nwCrkwEf6-_6OEBmDKqUa3E5WwdXB5IRYT0yxNTYkJ8-2yDdKigNTBdI-U3jZiMvDokNiZ7t4A/s1600/8.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSxwnF1GLo6U5WoDb0seH6ahJ_puh5UJT0_5q1x-II0B2U_EQzjf1xlBySGyQF2Wss6nwCrkwEf6-_6OEBmDKqUa3E5WwdXB5IRYT0yxNTYkJ8-2yDdKigNTBdI-U3jZiMvDokNiZ7t4A/s1600/8.png" height="400" width="341" /></a></div>
<br />
А в папочке, куда мы это все извлекали, будет такое:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2Y_xGNkLDTEIgGmu0iaHCTdSlyonqiI8qim_qiMeIbP5QkgrcYdqng5iMhWwmEOxRrOf2z7nMD4wKxs_eP9ayz3YXgw5GQOCZM8eBN_HWYYRPY53FN_INKYN93D31A2Vx_AOrm0xAKLs/s1600/9.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2Y_xGNkLDTEIgGmu0iaHCTdSlyonqiI8qim_qiMeIbP5QkgrcYdqng5iMhWwmEOxRrOf2z7nMD4wKxs_eP9ayz3YXgw5GQOCZM8eBN_HWYYRPY53FN_INKYN93D31A2Vx_AOrm0xAKLs/s1600/9.png" height="148" width="640" /></a></div>
<br />
Не все файлы, да?<br />
Ну, тут очень просто, если присмотритесь к именам в дереве, там видны какие-то черточки в виде "|" перед именем файла или непонятные пробелы перед "CompObj".<br />
<br />
Это как раз те зарезервированные под OLE символы, о которых я говорил еще в первой главе (от 0 до 0x1F). Создать файлы с такими символами в наименованиях я не могу, поэтому они пропущены, но данные о них записаны в лог: "cannotread.log".<br />
<br />
Конечно, это можно легко обработать, но для демки пойдет и так.<br />
<br />
Код данного примера <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">в архиве в папке "..\RawStorageReader\"</a>.<br />
<br />
Впрочем, зачем мы это все написали?<br />
Давайте попробуем при помощи нашего приложения, которое мы написали в предыдущей главе открыть вот такой файл: <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">"..\corrupted\corrupted_storage.bin"</a><br />
<br />
Будет как-то так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6lqZW2Fgb7Lkjx8I87gnFE0qT63NP6E_Y6UnUtU8oAgUPl2v2us2HjY2sf1hGHOdw8bXPbNccNjtaPtUzQOEinugbWGlIykJ2ipLTcPA-PpNpM07LWlAMVFSdZflaoAFF7ZoiXwGGDMc/s1600/10.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6lqZW2Fgb7Lkjx8I87gnFE0qT63NP6E_Y6UnUtU8oAgUPl2v2us2HjY2sf1hGHOdw8bXPbNccNjtaPtUzQOEinugbWGlIykJ2ipLTcPA-PpNpM07LWlAMVFSdZflaoAFF7ZoiXwGGDMc/s1600/10.png" height="104" width="640" /></a></div>
<br />
О как, давайте тогда ридером из второй главы, напрямую через API:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipcrEDhG_3CxEGFFvS7DxXNJvr3UOGt6i0T6sbv54lIEUFtYjzzaenAR4dslDDjMBeV6SlucEfoBFR_dynLb8EAswsTtrij7IAtWXyYVkeEkkDbeSGjszk14HYh_E2-tVhhhlVsyRNlHE/s1600/11.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipcrEDhG_3CxEGFFvS7DxXNJvr3UOGt6i0T6sbv54lIEUFtYjzzaenAR4dslDDjMBeV6SlucEfoBFR_dynLb8EAswsTtrij7IAtWXyYVkeEkkDbeSGjszk14HYh_E2-tVhhhlVsyRNlHE/s1600/11.png" height="102" width="640" /></a></div>
<br />
Печаль, тогда посмотрим что за беда, и откроем этот файл уже в RAW режиме:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiluQ8nLQc9Mcwtb54I8t6EDSbB3GANVX6gBW6plpaG-3VCNVKTSAtsar_G662IOkhR0tIUpNz8sguzwoga1u2eip6gnFbFZTAW-CaOoR_4f1iKtqQDEaxqJq7dZcWEUTyBkkBGqYGLDp4/s1600/12.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiluQ8nLQc9Mcwtb54I8t6EDSbB3GANVX6gBW6plpaG-3VCNVKTSAtsar_G662IOkhR0tIUpNz8sguzwoga1u2eip6gnFbFZTAW-CaOoR_4f1iKtqQDEaxqJq7dZcWEUTyBkkBGqYGLDp4/s1600/12.png" height="108" width="640" /></a></div>
<br />
Ага, ошибка чтения, смотрим стек:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4qHORp72nn3b_l4a6sT73xu0CK-0B3MexzL8OhSi13G3M3Ril2491qgg2hqZs9ZSHXieIT3skD18GXXSGxN-maCp3-Y-cys3nFujmdnDfuftxg0Mtl4Q4295BADzLzCeRcU3ZoR0Y0_0/s1600/13.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4qHORp72nn3b_l4a6sT73xu0CK-0B3MexzL8OhSi13G3M3Ril2491qgg2hqZs9ZSHXieIT3skD18GXXSGxN-maCp3-Y-cys3nFujmdnDfuftxg0Mtl4Q4295BADzLzCeRcU3ZoR0Y0_0/s1600/13.png" /></a></div>
<br />
Ошибка на этапе чтения свойств файлов, не смогли выполнить ReadBuffer в функции GetBlock. <br />
Будем решать.<br />
<br />
<h3 style="text-align: left;">
6. Пробуем исправить ошибку в данных и читаем все что доступно.</h3>
<br />
Еще в самом начале статьи я говорил о целой когорте различных "восстанавливателей" документов от Word. Сейчас будем писать что-то наподобие них. :)<br />
<br />
Все эти утилиты работают в двух режимах.<br />
<ul style="text-align: left;">
<li>они знают о формате составного файла.</li>
<li>они знают о формате данных, которые Word хранит в своих стримах.</li>
</ul>
Я, конечно, не обладаю информацией о втором пункте, да и не надо оно мне, а вот о первом, после прочтения пятой главы, вы и сами знаете. :)<br />
<br />
Вообще ошибок связанных с разрушением составного файла может быть всего четыре:<br />
<ul style="text-align: left;">
<li>убит заголовок (как правило, заполнен нулями)</li>
<li>разрушен FAT</li>
<li>не доступны данные TPoifsProperty</li>
<li>изменены данные файлов в секторах</li>
</ul>
Первое — не лечится. Нет, ну может и найдется какой фанат и попробует вычислить значения первых секторов для построения FAT, но я таких не встречал.<br />
<br />
Второе — практически не лечится.<br />
Зная, что FAT содержат номера секторов, мы можем определить разрушение по следующему условию: следующее значение меньше чем константа ENDOFCHAIN (-2) или больше чем размер FAT массива.<br />
Исправить можно изменением значения сбойного блока на константу ENDOFCHAIN, но, как правило, даже после такого вмешательства сектор минифат считается частично, да и массив свойств файлов будет доступен минимально (и то если повезет).<br />
<br />
Третий вариант лечится.<br />
Грубо, смотрим адрес ячейки FAT, где произошла ошибка чтения сектора, и выставляем ей значение ENDOFCHAIN. Этим мы, конечно, отрезаем кусок данных (в 99 процентах случаев убитых), но зато прочитаем то, что реально нам доступно.<br />
<br />
Четвертый вариант не лечится, так как эти данные не принадлежат составному файлу, он просто их хранит, но он их не контролирует (только размер — не более).<br />
<br />
Начинаем анализировать:<br />
По хорошему мы можем справиться только с третьей проблемой, а именно — определить номер сбойного FAT индекса и поправить его.<br />
Зная как работать с данными в RAW режиме, нам это не составит большого труда.<br />
<br />
Для начала изменим процедуру чтения блоков данных:<br />
<br />
<pre class="brush:delphi">function TPoifsFile.GetBlock(Adress: Integer): TPoifsBlock;
var
BlockOffset: Integer;
begin
BlockOffset := GetBlockOffset(Adress);
if BlockOffset < FStream.Size then
begin
FStream.Position := BlockOffset;
FStream.ReadBuffer(Result, SizeOf(TPoifsBlock));
end
else
raise Exception.Create('Wrong block offset at addres: ' + IntToStr(Adress));
end;
</pre>
<br />
Пусть теперь она проверяет оффсеты и поднимет исключение, если вдруг что-то не срослось.<br />
<br />
Второе изменение сделаем в процедуре ReadPropsArray, где будем более строго контролировать состояние FAT массива:<br />
<br />
<pre class="brush:delphi">function TPoifsFile.ReadPropsArray: Boolean;
var
I, J, Len, LastGood: Integer;
PropsBlock: TPoifsPropsBlock;
begin
Result := True;
// инициализируем размер массива свойств
Len := 0;
// запоминаем номер первого сектора, в котором размещается Property Set Storage
J := FHeader._sectDirStart;
LastGood := J;
repeat
if J = FREESECT then
begin
FixFatEntry(LastGood, ENDOFCHAIN);
Break;
end;
// читам свойства блоками по 4 элемента
Inc(Len, 4);
SetLength(FPropsArray, Len);
// читаем с автоматической правкой
try
PropsBlock := TPoifsPropsBlock(GetBlock(J));
except
FixFatEntry(LastGood, ENDOFCHAIN);
Break;
end;
for I := 0 to 3 do
FPropsArray[Len - 4 + I] := PropsBlock[I];
LastGood := J;
// читаем номер следующего сектора из FAT
J := FFat[J];
if J < ENDOFCHAIN then
begin
FixFatEntry(LastGood, ENDOFCHAIN);
Break;
end;
until J = ENDOFCHAIN;
end;
</pre>
<br />
Ну и осталось написать процедуру FixFatEntry:<br />
<br />
<pre class="brush:delphi">procedure TPoifsFile.FixFatEntry(FatIndex, NewValue: Integer);
var
J, Offset: Integer;
begin
// Ищем оффсет в FAT цепочке
J := FatIndex mod 128;
Offset := FFatOffset[FatIndex] + J * 4;
// и пишем вместо сбойного значения новое
FStream.Position := Offset;
FStream.WriteBuffer(NewValue, SizeOf(Integer));
end;
</pre>
<br />
Именно при помощи нее мы будем производить изменения FAT цепочки в оригинальном файле.<br />
<br />
Теперь давайте посмотрим что получилось:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHe6XxRWyZ8orIscPNQLX6OVum5lm78OzB-FNV3d8t1XqfUVLAa6uzSh94n-HtzigJVph7w7mmXgURAl5opoc-m6uZ7xjSDepIHlBHvHaxxmA7w49URQphlUsRCjVfbsWNdanzZulxc1E/s1600/14.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHe6XxRWyZ8orIscPNQLX6OVum5lm78OzB-FNV3d8t1XqfUVLAa6uzSh94n-HtzigJVph7w7mmXgURAl5opoc-m6uZ7xjSDepIHlBHvHaxxmA7w49URQphlUsRCjVfbsWNdanzZulxc1E/s1600/14.png" height="400" width="341" /></a></div>
<br />
Ба... да там целая куча данных :)<br />
Некоторые, конечно, слегка поломаны, но большую часть мы вытащили.<br />
<br />
Вообще, для данного варианта ошибки, править данные в файле через процедуру FixFatEntry не всегда обязательно и можно пропустить вызов FStream.WriteBuffer.<br />
<br />
Дело в том, что даже после такой правки, что утилита из второй главы, что из третьей, откроют такой составной файл, но скажут что он пуст, такая вот закавыка.<br />
<br />
Но зато теперь у вас есть весь набор данных о составном файле :)<br />
<br />
Код данного класса с поддержкой восстановления найдете <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">в архиве по следуюющему пути: "..\RawStorageReader\PoifsWithRepair.pas"</a>.<br />
<br />
Пожалуй, будем закругляться.<br />
<br />
<h3 style="text-align: left;">
7. Выводы и статистика</h3>
<br />
Если честно — я очень сильно боюсь, что своей статьей я вас оттолкнул от использования данной технологии.<br />
<br />
Вся проблема в том, что не нужно допускать одной, самой распространенной ошибки — если приложение, открывшее составной файл на запись, будет некорректно завершено, составной файл <b>однажды</b> (не всегда, но) может быть разрушен.<br />
<br />
Разрушен будет не просто так — это произойдет на этапе записи в файл, которая может внезапно прерваться.<br />
Как думаете, сколько есть программных продуктов, которые отслеживают такую ситуацию?<br />
Если скажете что много, возражу, да и механизм у них один — транзакции, во время которой я вытащу флэшку, на которую сохраняются данные :)<br />
<br />
Хотя, давайте, я вам приведу немного статистики:<br />
Все ошибки, которые я описал в статье, происходили, конечно, не единовременно.<br />
Это накопленная мной база возможных (вероятных) ошибок.<br />
<br />
Согласитесь, даже если мы будем использовать обычный текстовый файл, однажды мы сможем записать в него не совсем те данные, которые хотели.<br />
<br />
А теперь статистика:<br />
За последние 8 с половиной лет (скажем, дата отсчета реализации инструмента чтения составного файла в RAW режиме) у меня есть в наличии ровно 473 файла разрушенных баз, которые прислали нам наши пользователи.<br />
Если брать за среднее количество пользователей тогда и сейчас, и осреднить — получится грубо 150 тысяч станций, на которых ежедневно запускалось наше ПО.<br />
В течении 24 рабочих дней, ежемесячно и в течении 12 месяцев каждый год.<br />
Считаем: 150000 ежедневных запусков * 24 дня * 12 месяцев * 8 лет = 345 с копейками миллионов запусков (усредненное).<br />
<br />
По факту, на руках я имею ровно 473 "поломатых" файла (иногда бывает 1-2 в месяц, иногда месяцами затишье). Из них (этот момент нужно учесть), около сотни были с битым FAT (а как я и говорил — битый фат, крайне плохо).<br />
Так вот, практически вся эта сотня битых на уровне FAT-а файлов — это были удаленные составные файлы, которые потом восстанавливали утилитами типа UnErase и спрашивали: мол что с ним можно сделать? <br />
А с ними делать уже нечего — не мы удаляли, не нам и восстанавливать.<br />
<br />
Поэтому, откинув эту сотню, давайте посмотрим: каков шанс поломки составного файла?<br />
Да всего лишь 1 к миллиону — CD при быстрой скорости записи чаще сбоить будет, чем это число :)<br />
<br />
Не верите?<br />
Напишите тест открытия такого файла с принудительным завершением процесса в нештатном режиме (к примеру, по разрушению стека) и посчитайте сколько файлов разрушиться?<br />
<br />
Думаете это все еще не надежно?<br />
Отлично — скажу следующее: данные файлы работают в рамках транзакционной модели.<br />
Если сможете прямо вот так с ходу набросать код гарантированного "убития" данного файла по некорректному закрытию — с меня печенька :)<br />
<br />
Ну а если думаете, что и этот тест не надежен, то скажу — у оставшихся файлов данные были восстановлены практически целиком, обычно не хватало только последнего кусочка (грубо небольшой части данных сметы, которая была не принципиальна).<br />
<br />
Поэтому запомните два простых правила, чтобы добавить еще немного страховки:<br />
<ol style="text-align: left;">
<li>Не используйте составные файлы в том случае, если ваше приложение может работать с флэшки — убьете файл при вытаскивании флешки, если в этот момент ваше приложение запущено и лочит данный файл.</li>
<li>Ставьте векторные исключения через AddVectoredExceptionHandler. Если у вас ошибка связанная со стеком — ваше приложение рухнет не успев добраться до первого обработчика SEH. А в векторном исключении вы, по крайней мере, корректно закроете файл — большего ведь и не нужно. (Потом при необходимости откроете его заново, если вдруг был False Alarm :)</li>
</ol>
Ну а я заканчиваю свое повествование, а исходный код к статье <a href="http://rouse.drkb.ru/blog/storage.zip" target="_blank">можно забрать тут</a>.<br />
<br />
Как обычно, выражаю Благодарность участникам форума "<a href="http://www.delphimaster.ru/" target="_blank">Мастера Дельфи</a>" за помощь с вычиткой статьи перед публикацией.<br />
<br />
ЗЫ: кстати, по поводу поддержки длинных имен файлов (длиной до 1024 символа). Задумайтесь, а как вы будете распаковывать содержимое составного файла в обычную файловую систему, которая может быть даже не NTFS? :)<br />
<br />
Удачи :)<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<span style="font-size: 12.7272720336914px;">© Александр (Rouse_) Багель</span></div>
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Март, 2015</div>
</div>
<br />
<br /></div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com7tag:blogger.com,1999:blog-2374465879949372415.post-39357297837397200212015-02-22T13:58:00.002+03:002015-05-10T08:48:35.073+03:00Работаем с "заданиями" (Job)<div dir="ltr" style="text-align: left;" trbidi="on">
<div class="tr_bq">
Буквально на неделе на форуме появились два интересных вопроса, ответ на которые был очевиден, но... Программист, как вы знаете, существо с очень пытливым мозгом, он любит различные эксперименты, не смотря на то, что ответ на задачу мог быть уже озвучен :)</div>
<br />
Впрочем, давайте посмотрим на первый вопрос:<br />
<blockquote>
<span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Запускаю в отдельном потоке некий процесс (не мой, переделывать его не имею возможности), который необходимо завершить вместе с завершением основной (моей) программы.</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Если моя программа завершается штатно - то ничего сложного нет. Но если не штатно (пользователь убил через диспетчер задач) - так как быть тут?</span><br />
<span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">В голову пока приходит только CreateRemoteThread+LoadLibrary+моя dll, которая будет следить за основным процессом.</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Подскажите более изящные решения.</span></blockquote>
Попробуем подсказать...<br />
<br />
<a name='more'></a><br />
Конечно же, это называется планирование, и самое первое, что приходит на ум - создать JOB, в который подключить дочерний процесс посредством вызова AssignProcessToJobObject.<br />
<br />
Что есть JOB - по сути это достаточно удобный механизм позволяющий управлять множеством процессов, которые включены в него. У него есть много интересных фишек, но нас интересует только одна - надо закрыть все принадлежащие ему процессы как только JOB будет завершен.<br />
<br />
Делается это достаточно просто:<br />
У структуры TJobObjectExtendedLimitInformation, передаваемой на вход функции SetInformationJobObject, выставляем флаг JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, после чего все дочерние процессы запускаем с флагом CREATE_BREAKAWAY_FROM_JOB при вызове CreateProcess.<br />
<br />
Как-то не понятно?<br />
Тогда по шагам:<br />
<ol style="text-align: left;">
<li>создаем JOB вызовом CreateJobObject</li>
<li>выставляем флаг закрытия всех дочерних процессов вызовом SetInformationJobObject</li>
<li>запускаем дочерние процессы через CreateProcess</li>
<li>каждый дочерний процесс подсоединяем к JOB вызовом AssignProcessToJobObject</li>
<li>работаем как обычно</li>
</ol>
Фишка в том, что JOB существует до тех пор, пока на него есть линк (читай кто-то держит его хэндл). Как только закрывается последний хэндл, JOB умирает и грохает все содержащиеся в нем процессы.<br />
<br />
Но самое главное - если рутовый процесс держащий хэндл JOB-а прибьют через диспетчер задач, JOB все равно завершится, что и требовалось автору вопроса.<br />
<br />
Впрочем, пожалуй хватит балаболить, давайте посмотрим на код.<br />
В качестве дочернего процесса я выбрал штатный калькулятор, основное приложение будет запускать 4 его копии и, при завершении самого себя, будет их прибивать.<br />
<br />
Для начала пишем обвес:<br />
<br />
<pre class="brush:delphi">unit job_api;
interface
// непакованные структуры должны быть выровнены правильно, поэтому включаем директиву
{$Align 8}
uses
Windows;
const
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = $00002000;
JOB_OBJECT_ASSIGN_PROCESS = 1;
CREATE_BREAKAWAY_FROM_JOB = $01000000;
type
PSecurityAttributes = ^TSecurityAttributes;
_SECURITY_ATTRIBUTES = record
nLength: DWORD;
lpSecurityDescriptor: Pointer;
bInheritHandle: BOOL;
end;
TSecurityAttributes = _SECURITY_ATTRIBUTES;
PJobObjectBasicLimitInformation = ^TJobObjectBasicLimitInformation;
TJobObjectBasicLimitInformation = packed record
PerProcessUserTimeLimit: TLargeInteger;
PerJobUserTimeLimit: TLargeInteger;
LimitFlags,
MinimumWorkingSetSize,
MaximumWorkingSetSize,
ActiveProcessLimit,
Affinity,
PriorityClass,
SchedulingClass: DWORD;
end;
TIOCounters = record
ReadOperationCount,
WriteOperationCount,
OtherOperationCount,
ReadTransferCount,
WriteTransferCount,
OtherTransferCount: Int64;
end;
PJobObjectExtendedLimitInformation = ^TJobObjectExtendedLimitInformation;
TJobObjectExtendedLimitInformation = record
BasicLimitInformation: TJobObjectBasicLimitInformation;
IoInfo: TIOCounters;
ProcessMemoryLimit,
JobMemoryLimit,
PeakProcessMemoryUsed,
PeakJobMemoryUsed: DWORD;
end;
TJobObjectInfoClass = (
JobObjectBasicLimitInformation = 2,
JobObjectBasicUIRestrictions = 4,
JobObjectSecurityLimitInformation = 5,
JobObjectEndOfJobTimeInformation = 6,
JobObjectAssociateCompletionPortInformation = 7,
JobObjectExtendedLimitInformation = 9,
JobObjectGroupInformation = 11,
JobObjectNotificationLimitInformation = 12,
JobObjectGroupInformationEx = 14,
JobObjectCpuRateControlInformation = 15
);
function CreateJobObjectA(lpJobAttributes: PSecurityAttributes;
lpName: LPCSTR): THandle; stdcall;
external kernel32 name 'CreateJobObjectA';
function SetInformationJobObject(hJob: THandle;
JobObjectInformationClass: TJobObjectInfoClass;
lpJobObjectInformation: Pointer;
cbJobObjectInformationLength: DWORD): BOOL; stdCall;
external kernel32 Name 'SetInformationJobObject';
function AssignProcessToJobObject(hJob, hProcess: THandle): BOOL; stdcall;
external kernel32 name 'AssignProcessToJobObject';
function OpenJobObject(dwDesiredAccess: DWORD; bInheritHandle: BOOL;
lpName: LPCTSTR): THandle; stdcall; external kernel32 name 'OpenJobObjectA';
procedure RunCalcAndAttachToJob(hJob: THandle);
implementation
uses
SysUtils;
procedure RunCalcAndAttachToJob(hJob: THandle);
var
SI: TStartupInfo;
PI: TProcessInformation;
SystemDir: AnsiString;
begin
ZeroMemory(@SI, SizeOf(TStartupInfo));
SI.cb := SizeOf(TStartupInfo);
SetLength(SystemDir, MAX_PATH);
GetSystemDirectoryA(@SystemDir[1], MAX_PATH);
SystemDir := IncludeTrailingPathDelimiter(PAnsiChar(@SystemDir[1])) + 'calc.exe';
CreateProcessA(PChar(SystemDir), nil, nil, nil, True,
CREATE_BREAKAWAY_FROM_JOB, nil, nil, SI, PI);
CloseHandle(PI.hThread);
AssignProcessToJobObject(hJob, PI.hProcess);
end;
end.
</pre>
<br />
Тут просто декларация структур и API для работы с JOB, плюс процедура запуска калькулятора и аттача его к созданному ранее JOB-у RunCalcAndAttachToJob.<br />
Пока не сильно вникайте в этот код, я на нем остановлюсь ниже.<br />
<br />
А теперь посмотрим как будет выглядеть основное приложение:<br />
<br />
<pre class="brush:delphi">program main_app1;
uses
Windows,
SysUtils,
job_api in '..\common\job_api.pas';
const
JobName = 'MyJobName1';
var
hJob: THandle;
Limit: TJobObjectExtendedLimitInformation;
begin
hJob := CreateJobObjectA(nil, @JobName[1]);
try
if hJob = 0 then
RaiseLastOSError;
ZeroMemory(@Limit, SizeOf(TJobObjectExtendedLimitInformation));
Limit.BasicLimitInformation.LimitFlags := JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation,
@Limit, SizeOf(TJobObjectExtendedLimitInformation));
RunCalcAndAttachToJob(hJob);
RunCalcAndAttachToJob(hJob);
RunCalcAndAttachToJob(hJob);
RunCalcAndAttachToJob(hJob);
MessageBox(0, 'После закрытия окна все дочерние процессы будут завершены.', nil, 0);
finally
CloseHandle(hJob);
end;
end.
</pre>
Ага - это все.<br />
Просто создаем JOB и подключаем к нему процессы калькуляторов.<br />
<br />
Просто?<br />
Конечно.<br />
<br />
А знаете как эта задача была решена в итоге?<br />
Через создание удаленного потока, и не спрашивайте у меня: "почему это было сделано именно через него", не я автор вопроса и не я решал эту задачу :)<br />
<br />
Впрочем...<br />
<br />
<h3 style="text-align: left;">
Вопрос номер 2:</h3>
<blockquote>
<span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Есть программа, которая должна работать в единственном экземпляре и только пока работает хотя бы одна интересующая ее программа.</span><br />
<span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Например, есть программы:</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">A.exe</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">B.exe</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">C.exe</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">и</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Support.exe</span><br />
<span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">A, B и C при запуске запускают Support.exe. Про этом Support.exe должен работать в единственном экземпляре (это легко сделать).</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Но как только закроются все экземпляры A, B и C - то Support тоже должен закрыться. (вот это не совсем понятно как сделать просто).</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">При этом не надо полагаться на то, что A, B или C закроются корректно.</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">А также, если пользователь прибьет Support в диспетчере задач, то это его личное дело.</span><br />
<span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Пока самая простая идея такая:</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">Создать/Открыть именованный объект ядра во всех A, B и C.</span><span style="background-color: #f2f0f0; font-family: Verdana, Arial, Helvetica, Tahoma, sans-serif; font-size: 13.3333330154419px;">А в Support в потоке с интервалом проверять существует ли этот именованный объект.</span></blockquote>
Пробуем решить эту задачу через тот-же JOB.<br />
Здесь задача немного усложняется, т.к. рутовых процессов может быть много, а дочерний - (support.exe) только один.<br />
<br />
Решаем ее по стандартной схеме через наименование, а именно - у функции CreateJobObject вторым параметром идет имя задачи.<br />
Зная это имя, второй процесс может открыть эту задачу и подключить самого себя к JOB-у посредством OpenJobObject и того-же AssignProcessToJobObject.<br />
<br />
В качестве дочернего процесса (который изначально был support.exe) возьмем тот-же калькулятор и пишем логику по таким шагам:<br />
<ol style="text-align: left;">
<li>если при старте мы не можем открыть задачу по ее имени - значит мы первые</li>
<li>если мы первые - создаем задачу и подключаем к ней самого себя и калькулятор</li>
<li>если вдруг такая задача уже существует - просто открываем ее и подключаем самого себя</li>
</ol>
Таким образом, все копии процесса и калькулятор будут работать в рамках одного JOB-а, при завершении которого все оставшиеся процессы будут закрыты.<br />
Нюанс - теперь у нас хэндлов на задачу много (при каждом старте рутового процесса добавляется новый) но закрывать процессы мы можем в произвольном порядке и калькулятор все равно будет жить.<br />
<br />
Смотрим код:<br />
<br />
<pre class="brush:delphi">program main_app2;
uses
Windows,
SysUtils,
job_api in '..\common\job_api.pas';
const
JobName = 'MyJobName2';
procedure WaitStop;
begin
MessageBox(0, PChar('Закройте окно дла завершения процесса: ' +
IntToStr(GetCurrentProcessId)), nil, 0);
end;
function HaseJob: Boolean;
var
hJob: THandle;
begin
hJob := OpenJobObject(JOB_OBJECT_ASSIGN_PROCESS, False, PChar(@JobName[1]));
Result := hJob <> 0;
if Result then
begin
AssignProcessToJobObject(hJob, GetCurrentProcess);
WaitStop;
end;
CloseHandle(hJob);
end;
procedure RunMainApp;
var
hJob: THandle;
Limit: TJobObjectExtendedLimitInformation;
begin
hJob := CreateJobObjectA(nil, @JobName[1]);
try
if hJob = 0 then
RaiseLastOSError;
ZeroMemory(@Limit, SizeOf(TJobObjectExtendedLimitInformation));
Limit.BasicLimitInformation.LimitFlags := JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation,
@Limit, SizeOf(TJobObjectExtendedLimitInformation));
RunCalcAndAttachToJob(hJob);
WaitStop;
finally
CloseHandle(hJob);
end;
end;
begin
if not HaseJob then
RunMainApp;
end.
</pre>
<br />
Функция HaseJob проверяет наличие JOB и, будь таковой, подключает себя к нему.<br />
Ну а если JOB не создан - создаем процесс калькулятор и аттачим его к JOB-у.<br />
<br />
Проверьте - приложение main_app2 можно запустить сколько угодно раз, но калькулятор будет запущен только единожды и закроется только тогда, когда вы закроете самый последний экземпляр main_app2.<br />
<br />
<h3 style="text-align: left;">
Немного комментариев по коду</h3>
<br />
Обратите внимание на выравнивание структур в модуле job_api.<br />
Если структуры TIOCounters или TJobObjectExtendedLimitInformation будут неверно размещены в памяти - SetInformationJobObject выполнится не успешно.<br />
<br />
Забудете флаг CREATE_BREAKAWAY_FROM_JOB - все это не заработает (см. MSDN).<br />
<br />
Есть нюанс с консольками: если вы консоль и дочерние процессы тоже консоль - при закрытии консоли убьете всю кучу процессов. Надо как-то распределять дочерние процессы, к примеру через детач.<br />
<br />
Писал все за вечер на Delphi7 и не проверял на юникодных вариантах Delphi (хард недавно умер - лень все переустанавливать), поэтому под юникодом не ручаюсь за правильность исполнения, но навскидку вроде ничего не пропустил и должно работать.<br />
<br />
Исходный код можно <a href="http://rouse.drkb.ru/files/job.zip" target="_blank">забрать тут</a>.<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<span style="font-size: 12.7272720336914px;">© Александр (Rouse_) Багель</span></div>
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Февраль, 2015</div>
</div>
<br />
<br /></div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com6tag:blogger.com,1999:blog-2374465879949372415.post-19209191993292641852015-01-02T12:11:00.001+03:002015-01-02T12:11:20.524+03:00Ответ на предновогоднюю задачку<div dir="ltr" style="text-align: left;" trbidi="on">
Если вы вдруг заинтересовались <a href="http://alexander-bagel.blogspot.ru/2014/12/blog-post.html" target="_blank">самой задачей</a>, и работаете над ее решением, то вам лучше не читать данный пост, так как в нем я по шагам опишу все проблемы, которые должен решить программист с полным описанием подхода.<br />
<br />
Задача действительно интересная.<br />
На данный момент мне прислали два её решения, причем у каждого оказался свой индивидуальный подход.<br />
<br />
Примерное время на решение, около 3-4 часов (один вечер).<br />
Именно столько было затрачено каждым из решивших задачу программистов, включая меня.<br />
<br />
Ну и ответ на введенное максимальное пороговое число (10 в 12 степени) будет: $259814D6C9AAF914221E (это вам для самопроверки).<br />
<br />
Пора перейти к самой сути.<br />
<br />
<br />
<a name='more'></a><br />
<br />
<h3 style="text-align: left;">
0. Математика длинных чисел:</h3>
<br />
Вспомним саму задачу:<br />
Пользователь вводит некое положительное число N в диапазоне от единицы до 10 в 12 степени включительно. Нужно посчитать сумму остатков от деления по модулю в диапазоне от 1 до N (N mod 1 + N mod 2 + N mod 3 +...+ N mod N).<br />
<br />
Даже в самом первом приближении результат такой суммы не получится сохранить ни в одном из известных Delphi типов, включая UInt64.<br />
<br />
Для того чтобы мы могли работать с такими числами, придется вводить поддержку больших чисел. По сути, (по условию задачи) при работе с большими числами нам будет достаточно написать только поддержку суммирования, что и было мной выполнено в <a href="http://alexander-bagel.blogspot.ru/2014/12/blog-post.html" target="_blank">первом варианте решения</a>. В этом варианте большое число хранилось в виде массива из 16 байт, а суммирование производилось банальной битовой арифметикой посредством полного сумматора. И это даже работает :)<br />
<br />
Проблема в том, что работать это будет очень долго (примерно 3 дня при вводе максимального числа). Да и в действительности такое суммирование можно сделать гораздо проще:<br />
<br />
<pre class="brush:delphi">type
UInt128 = record
Lo, Hi: UInt64;
end;
var
VeriBigInteger: UInt128;
procedure Adder(X: Int64);
var
Tmp: UInt128;
begin
Tmp := VeriBigInteger;
Inc(VeriBigInteger.Lo, X);
if (VeriBigInteger.Lo < Tmp.Lo) or (VeriBigInteger.Lo < X) then
Inc(VeriBigInteger.Hi);
end;
</pre>
<br />
Идея, описанная данном алгоритме простая, попробую ее описать для начала на переменных типа Byte.<br />
<br />
Давайте напишем вот такой код:<br />
<br />
<pre class="brush:delphi">program ByteOverflow;
{$APPTYPE CONSOLE}
var
A: Byte;
begin
A := 255;
Inc(A);
Writeln(A);
end.
</pre>
<br />
Нам выведется число ноль, хотя по факту реальное значение суммы равно 256.<br />
Так получилось из-за того что переменная типа Byte просто не может в себе разместить столь большое число (255 максимум).<br />
Но, мы все-же сможем выполнить такое сложение и учесть возможность переполнения.<br />
Так как мы знаем что проводим только суммирование, можно сделать вывод: как только значение переменной А стало меньше значения, хранившегося в ней ранее, или стало меньше числа, с которым производилось суммирование - произошла операция переполнения.<br />
<br />
Что происходит при переполнении?<br />
В более старший разряд (который мы не можем хранить) добавляется один бит, и вот этот бит нам и нужно учесть (256 = <b>1</b> 0000 0000 в двоичной системе счисления)<br />
<br />
Пишем такой код для проверки:<br />
<br />
<pre class="brush:delphi">program ByteOverflow2;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
MyUInt16 = record
Lo, Hi: Byte;
end;
var
Z: MyUInt16;
procedure PrintMyUInt16;
begin
Write('Result: 0x');
Write(IntToHex(Z.Hi, 2));
Write(IntToHex(Z.Lo, 2));
Writeln;
end;
procedure Adder(X: Byte);
var
Tmp: MyUInt16;
begin
Tmp := Z;
Inc(Z.Lo, X);
// проверяем, не произошло ли переполнения
if (Z.Lo < Tmp.Lo) or (Z.Lo < X) then
// если произошло, увеличиваем старший байт
Inc(Z.Hi);
end;
var
A: Byte;
begin
Adder(255);
Adder(1);
PrintMyUInt16;
Readln;
end.
</pre>
<br />
Что выведет нам в результате число $100 (256 в десятичной, что и ожидалось).<br />
<br />
Почему к старшему байту прибавляется только единица.<br />
Ну тут все просто: нет таких двух чисел одинаковой размерности, сумма которых вызвала бы увеличение старшего байта на 2 и больше.<br />
К примеру 255 + 255, как максимальные значения байта, дадут в итоге всего лишь 510, что в двоичном представлении будет: <b>1 </b>1111 1110.<br />
<br />
Теперь, если этот момент понятен, обратите внимание на самый первый вариант кода - практически полная идентичность, за исключением того, что в этом случае мы работаем уже не с Byte, а с UInt64, все остальное по такому же принципу.<br />
<br />
<h3 style="text-align: left;">
1. Определение порога арифметических прогрессий.</h3>
<br />
Теперь надо разобраться, что из себя представляют все эти числа, которые нам придется суммировать.<br />
<br />
Строим график от 1 до N, где N = 1000.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgs2nX7Tn_dYGAhWGDBYbJ4LSMCdBZ8tN8Dt3JihXIRVI6URYStY-YHcUguswv2RgL4JNvX3W2J6rd46egpr-PUS5ftaG61Cbs_XInHuUTfmU5F2SOFZTPK5DTAvnHgD60ABNKyQdwdlwk/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgs2nX7Tn_dYGAhWGDBYbJ4LSMCdBZ8tN8Dt3JihXIRVI6URYStY-YHcUguswv2RgL4JNvX3W2J6rd46egpr-PUS5ftaG61Cbs_XInHuUTfmU5F2SOFZTPK5DTAvnHgD60ABNKyQdwdlwk/s1600/1.png" height="432" width="640" /></a></div>
<br />
Опираясь на график видим, что, по сути, мы имеем дело с набором арифметических прогрессий, причем самая правая (с конца) идет от нуля (нижний предел прогрессии pMinLimit) с шагом pIncrement (изначально равным единице) ровно до середины максимального значения N - 1 (верхний предел прогрессии pMaxLimit).<br />
<br />
Х = 1000, 999, 998, 997...503, 502, 501<br />
Y = 0, 1, 2, 3...497, 498, 499<br />
<br />
Как только Х достигает 500, начинается новая арифметическая прогрессия, но с шагом pIncrement увеличенным на единицу, причем максимальный порог рассчитывается уже не до половины N - 1, а до третьей части от N минус какое-то число (назовем его пока UnknownValue).<br />
<br />
Основная идея алгоритма строится на том, что не нужно суммировать все числа - достаточно просто просуммировать результаты сумм арифметических прогрессий. Но для этого нужно понять как вычисляется их верхний и нижний пределы.<br />
<br />
Начнем с вычисления верхнего предела прогрессии для каждого шага и посмотрим на значения вершин:<br />
<br />
499, 332, 247, 196, 165, 142, 118, 104, 91, 90, 76 и т.д.<br />
<br />
Вот тут я их выделил:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgteswk7nQ8YT3eT8BNbuhtGaL7Pi1r6Og8s9rGwirmFPtWo-ejjmERpvP-gkY-eHaPFUY7RC1eXewUJ48gzGKXazKsby9LtAIcYIS6NuLO9bJsBPTvWUCHvFTlYnDjXgH7JIJf9_GHj7A/s1600/mod+1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgteswk7nQ8YT3eT8BNbuhtGaL7Pi1r6Og8s9rGwirmFPtWo-ejjmERpvP-gkY-eHaPFUY7RC1eXewUJ48gzGKXazKsby9LtAIcYIS6NuLO9bJsBPTvWUCHvFTlYnDjXgH7JIJf9_GHj7A/s1600/mod+1.png" height="432" width="640" /></a></div>
<br />
<br />
Изначально я предположил что эти числа получаются по следующей формуле:<br />
<br />
<pre class="brush:delphi">pMaxLimit := N div (pIncrement + 1) - pIncrement;
</pre>
<br />
Т.е. на первом шаге pIncrement равен единице, делим N на увеличенное значение шага (на два в первом случае) и от результат вычитаем само значение шага.<br />
<br />
Это работает для следующих чисел:<br />
pIncrement = 1, результат (1000 / 2 - 1) = 499<br />
pIncrement = 3, результат (1000 / 4 - 3) = 247<br />
pIncrement = 4, результат (1000 / 5 - 4) = 196<br />
pIncrement = 7, результат (1000 / 8 - 7) = 118<br />
pIncrement = 9, результат (1000 / 10 - 9) = 91<br />
<br />
Но не работает на следующих:<br />
pIncrement = 2, результат (1000 / 3 - 2) = 331, а должно быть 332, разница в 1<br />
pIncrement = 5, результат (1000 / 6 - 5) = 161, а должно быть 165, разница в 4<br />
pIncrement = 6, результат (1000 / 7 - 6) = 136, а должно быть 142, разница в 6<br />
pIncrement = 8, результат (1000 / 9 - 8) = 103, а должно быть 104, разница в 1<br />
pIncrement = 10, результат (1000 / 11 - 10) = 80, а должно быть 90, разница в 10<br />
pIncrement = 11, результат (1000 / 12 - 11) = 72, а должно быть 76, разница в 4<br />
<br />
и т.д.<br />
<br />
Таким образом нужно как-то понять что же из себя представляет это разница в результатах.<br />
Немного покрутив в голове задачу, пришел к выводу, что разница в значениях всегда равна остатку деления N по модулю на значение pIncrement увеличенное на единицу, т.е.<br />
<br />
pIncrement = 2, Difference = (1000 mod 3) = 1<br />
pIncrement = 5, Difference = (1000 mod 6) = 4<br />
pIncrement = 6, Difference = (1000 mod 7) = 6<br />
pIncrement = 8, Difference = (1000 mod 9) = 1<br />
pIncrement = 10, Difference = (1000 mod 11) = 10<br />
pIncrement = 11, Difference = (1000 mod 12) = 4<br />
<br />
Ну а для тех значений pIncrement, для которых поправок не требуется (1, 3, 4, 7, 9) результат деления по модулю всегда равен нулю.<br />
<br />
Таким образом финальная формула получения максимального значения арифметической прогрессии стала выглядеть вот так:<br />
<br />
<pre class="brush:delphi"> pMaxLimit := Limit div (pIncrement + 1) -
(pIncrement - (Limit mod (pIncrement + 1)));
</pre>
<br />
Нижний лимит прогрессии определяется банально:<br />
<br />
<pre class="brush:delphi">pMinLimit := Limit mod nCount;</pre>
<br />
где nCount, позиция, с которой начинается сама прогрессия.<br />
Поняв как рассчитать верхний и нижний лимиты, можно отбросить суммирование каждого элемента и складывать саму сумму ряда по формуле:<br />
<br />
<pre class="brush:delphi">function summ(a1, an: Int64; n: Integer): Int64;
begin
// если количество элементов ряда равно одному, возвращаем значение как есть
if n = 1 then
Result := a1
else
// в противном случае считаем сумму арифметической прогрессии
Result := (n * (a1 + an)) shr 1;
end;
</pre>
<br />
<h3 style="text-align: left;">
2. Делим прогрессию на блоки</h3>
<br />
Теперь надо разобраться как правильно суммировать результаты сумм прогрессий.<br />
Дело в том что для числа 10 в 12 степени сумма самой правой прогрессии (с шагом 1) не влезет в диапазон Int64 и будет равна следующему числу: <b>0x1A78 </b>4379D963 7EF6BC00.<br />
<br />
Для этого определимся сколько всего остатков деления по модулю влезет в Int64 без переполнения, и напишем такой тест:<br />
<br />
<pre class="brush:delphi">procedure Tst;
var
I, A, Z: Int64;
begin
I := 10000000000000 div 2 - 1;
A := 0;
Z := 0;
while I > 0 do
begin
Inc(Z);
A := A + I;
if A < 0 then
Writeln(Z);
Dec(I);
end;
end;
</pre>
<br />
Зная что работать мы будем с рядами прогрессий, где самая большая будет самой правой, с шагом pIncrement = 1, просто ручками просуммируем значение всего ряда, начиная от самых старших значений.<br />
Как только значение А станет отрицательным, нам выведется число: 1844675.<br />
Подстрахуемся и будем считать что количество таких элементов в два раза меньше и возьмем размер буфера в 900 тысяч элементов.<br />
<br />
Зная что сумма ряда равна сумме частей ряда мы можем дробить длинные ряды на порции по 900 тысяч элементов и считать сумму от них, не выходя за переполнение.<br />
<br />
К примеру ряд (1 + 2 + 3 + 4) = 10, если мы раздробим его на 2 части (1 + 2) и (3 + 4), то их суммы так-же совпадут (что логично :).<br />
<br />
Осталось написать код.<br />
<br />
<h3 style="text-align: left;">
3. Финальное решение задачи.</h3>
<br />
<pre class="brush:delphi">program mod_sum;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
SysUtils;
type
UInt128 = record
Lo, Hi: UInt64;
end;
var
VeryBigInteger: UInt128;
procedure Adder(X: Int64);
var
Tmp: UInt128;
begin
Tmp := VeryBigInteger;
Inc(VeryBigInteger.Lo, X);
if (VeryBigInteger.Lo < Tmp.Lo) or (VeryBigInteger.Lo < X) then
Inc(VeryBigInteger.Hi);
end;
function summ(a1, an: UInt64; n: Integer): UInt64;
begin
// если количество элементов ряда равно одному, возвращаем значение как есть
if n = 1 then
Result := a1
else
// в противном случае считаем сумму арифметической прогрессии
Result := (n * (a1 + an)) shr 1;
end;
procedure Calc(Limit: Int64);
const
MaxProgressionSize = 900000;
var
nCount, // количество не обработанных элементов
pMinLimit, // минимальное значение прогрессии
pMaxLimit, // максимальное значение прогрессии
pIncrement, // дистанция между элементами ряда
pCount, // количество элементов ряда
pSumm, // сумма элементов ряда
TmpMaxLimit: Int64; // временное максимальное значение прогрессии
Start: DWORD;
begin
// первичная инициализация
Start := GetTickCount;
ZeroMemory(@VeryBigInteger, SizeOf(UInt128));
nCount := Limit;
pIncrement := 1;
pMinLimit := 0;
pMaxLimit := Limit div (pIncrement + 1) -
(pIncrement - (Limit mod (pIncrement + 1)));
while nCount > 0 do
begin
// рассчитываем количество элементов ряда
pCount := 1 + (pMaxLimit - pMinLimit) div pIncrement;
// если это число больше чем максимальное кол-во элементов,
// у которых сумма остатков от деления по модулю влезет в Int64
// то считаем сумму ряда частями
while pCount > MaxProgressionSize do
begin
// рассчитываем временный верхний предел ряда
TmpMaxLimit := pMinLimit + (pIncrement * (MaxProgressionSize - 1));
// считаем сумму части
pSumm := summ(pMinLimit, TmpMaxLimit, MaxProgressionSize);
// суммируем
Adder(pSumm);
// правим кол-во оставшихся элементов ряда
Dec(pCount, MaxProgressionSize);
// правим кол-во всех оставшихся элементов
Dec(nCount, MaxProgressionSize);
// сдвигаем нижний предел ряда
pMinLimit := TmpMaxLimit + pIncrement;
end;
// считаем сумму ряда
pSumm := summ(pMinLimit, pMaxLimit, pCount);
// суммируем
Adder(pSumm);
// правим кол-во всех оставшихся элементов
Dec(nCount, pCount);
// если все посчитали - выходим
if nCount = 0 then
Break;
// рассчитываем новый нижний предел ряда
pMinLimit := Limit mod nCount;
// рассчитываем новый максимальный предел ряда
Inc(pIncrement);
pMaxLimit := Limit div (pIncrement + 1) -
(pIncrement - (Limit mod (pIncrement + 1)));
// который, к слову сказать, может стать отрицательным
if pMaxLimit <= 0 then
// в таком случае выставляем его в ноль,
// а pCount на след итерации станет равным единице (что есть правильно)
pMaxLimit := 0;
end;
// ну и выводим результат
Writeln('Result: 0x', IntToHex(VeryBigInteger.Hi, 16), IntToHex(VeryBigInteger.Lo, 16));
Writeln('Time elapsed: ', GetTickCount - Start);
end;
var
N: int64;
begin
try
repeat
Writeln('Input N or zero (0) for exit: ');
Readln(N);
if N > 1000000000000 then
begin
Writeln('Too much value, repeat input.');
Continue;
end;
if N < 0 then
begin
Writeln('Wrong value, repeat input.');
Continue;
end;
if N <> 0 then
begin
Writeln('calculate...');
Calc(N);
end;
until N = 0;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
</pre>
<br />
Выглядит в итоге вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirLoojkTDzQmjz3s3_qIhZFe4GZCT1NSghEGTxKLB-KXlSnooQxGBPqNeOT1e5rcFTBX0WOwsIlnCuQcZZTHvh02CErXFvvfVuF1Zu8vUMhvsNe2kjoqnn1dhLqvl972pzFU6d9vO8Njg/s1600/mod2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirLoojkTDzQmjz3s3_qIhZFe4GZCT1NSghEGTxKLB-KXlSnooQxGBPqNeOT1e5rcFTBX0WOwsIlnCuQcZZTHvh02CErXFvvfVuF1Zu8vUMhvsNe2kjoqnn1dhLqvl972pzFU6d9vO8Njg/s1600/mod2.png" /></a></div>
<br />
В отличие от самого <a href="http://alexander-bagel.blogspot.ru/2014/12/blog-post.html" target="_blank">первого варианта решения</a>, это работает всего за 2 секунды, но...<br />
<br />
<h3 style="text-align: left;">
4. Можно ли оптимизировать?</h3>
<br />
Конечно можно.<br />
Это решение от Бориса Новгородова (MBo):<br />
<br />
<pre class="brush:delphi">program mod_sum2;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
SysUtils;
type
TUInt128 = record
case Integer of
0: (Lo, Hi: UInt64);
1: (LoLo, LoHi, HiLo, HiHi: Cardinal);
2: (Bytes: array [0 .. 15] of Byte);
end;
procedure Add128(var Sum, Addend: TUInt128);
begin
if Sum.Lo >= UInt64($FFFFFFFFFFFFFFFF) - Addend.Lo then
Inc(Sum.Hi);
Sum.Lo := Sum.Lo + Addend.Lo;
Sum.Hi := Sum.Hi + Addend.Hi;
end;
procedure Sub128(var Sum, Addend: TUInt128);
begin
if Sum.Lo < Addend.Lo then
Dec(Sum.Hi);
Sum.Lo := Sum.Lo - Addend.Lo;
Sum.Hi := Sum.Hi - Addend.Hi;
end;
//Sq = Sqr(Value.Lo)
procedure Sqr128(var Sq, Value: TUInt128);
var
ad: TUInt128;
begin
Sq.Hi := UInt64(Value.LoHi) * Value.LoHi;
Sq.Lo := UInt64(Value.LoLo) * Value.LoLo;
ad.Lo := UInt64(Value.LoHi) * Value.LoLo;
ad.Hi := ad.LoHi;
ad.Lo := ad.Lo shl 32;
Add128(Sq, ad);
Add128(Sq, ad);
end;
function SumOfMods128(N: Int64): TUInt128;
var
Minus, Divider: UInt64;
Even: Boolean;
V, Sq: TUInt128;
begin
Result.Lo := 0;
Result.Hi := 0;
Minus := 1;
Divider := 2;
Even := True;
V.Hi := 0;
while Minus < N do begin
V.Lo := (N - Minus) div Divider;
Sqr128(Sq, V);
if Even then
Add128(Result, Sq)
else
Sub128(Result, Sq);
Minus := Minus + Divider;
Divider := Divider + 1;
Even := not Even;
end;
end;
var
N: Int64;
Res128: TUInt128;
Start: DWORD;
begin
Start := GetTickCount;
N := 1000000000000;
Res128 := SumOfMods128(N);
Writeln('Result: 0x', IntToHex(Res128.Hi, 16), IntToHex(Res128.Lo, 16));
Writeln('Time elapsed: ', GetTickCount - Start);
Readln;
end.
</pre>
<br />
Работает почти в 3 раза быстрее моего варианта.<br />
Как работает и почему - я не объясню, я не математик :)<br />
<br />
Единственно дам его пояснение:<br />
<br />
Вот начало ряда сумм S(N) и модули, из которых эти суммы складываются.<br />
<code style="background-color: #f2f0f0; color: navy; font-family: Courier; font-size: 9pt; font-stretch: normal;"><br />N S mods <br />1 0 0 <br />2 0 0 0 <br />3 1 0 1 0<br />4 1 0 0 1 0<br />5 4 0 1 2 1 0<br />6 3 0 0 0 2 1 0<br />7 8 0 1 1 3 2 1 0<br />8 8 0 0 2 0 3 2 1 0<br />9 12 0 1 0 1 4 3 2 1 0<br />10 13 0 0 1 2 0 4 3 2 1 0<br />11 22 0 1 2 3 1 5 4 3 2 1 0<br />12 17 0 0 0 0 2 0 5 4 3 2 1 0<br />13 28 0 1 1 1 3 1 6 5 4 3 2 1 0</code><br />
<br />
Видно, что правая часть представляет собой арифм. последовательность, сумму которой я уже приводил, а вот левая похитрее. Компактного математического выражения не нашел, но эмпирическое исследование привело к следующему:<br />
главный член суммы резко растет на каждом втором (нечетном) числе, рост квадратичный, и это квадрат выражения (N-1) div 2<br />
Q1 = Sqr((N-1) div 2)<br />
Теперь, если вывести разницу Q1 - S(N), то можно увидеть, что она чаще всего скачет на каждом третьем числе, а сдвиг от начала будет уже 3<br />
Q2 = -Sqr((N-3) div 3)<br />
Далее аналогично <br />
Q3 = Sqr((N-6) div 4)<br />
Q4 = -Sqr((N-10) div 5)<br />
и т.д. Числа сдвига - "треугольные", вида k(k-1)/2, где k - знаменатель.<br />
<br />
Итого с использованием длинной арифметики получается функция, которая отличается от такой же для короткой арифметики только типом результата и заменой Sqr на TUint128.Sqr128 (если Multiply реализовать, то в обоих случаях можно одинаково X*X) <br />
<br />
<pre class="brush:delphi">function SumOfMods128(N: Int64): TUInt128;
var
Minus, Divider: UInt64;
Even: Boolean;
begin
Result := 0;
Minus := 1;
Divider := 2;
Even := True;
while Minus < N do begin
if Even then
Result := Result + TUint128.Sqr128((N - Minus) div Divider)
else
Result := Result - TUint128.Sqr128((N - Minus) div Divider);
Minus := Minus + Divider;
Divider := Divider + 1;
Even := not Even;
end;
end;</pre>
<br />
Прониклись? Тогда идем дальше...<br />
<br />
<h3 style="text-align: left;">
5. А можно ли еще ускорить?</h3>
<br />
Конечно можно, и вот вариант решения от Александра Шарахова, более известного как Sha.<br />
<br />
Если не знаете кто это такой, то откройте system.pas и найдите там вот такой блок текста:<br />
<br />
<blockquote class="tr_bq">
(* ***** BEGIN LICENSE BLOCK *****<br />
*<br />
* The function PosEx is licensed under the CodeGear license terms.<br />
*<br />
* The initial developer of the original code is Fastcode<br />
*<br />
* Portions created by the initial developer are Copyright (C) 2002-2004<br />
* the initial developer. All Rights Reserved.<br />
*<br />
* Contributor(s): Aleksandr Sharahov<br />
*<br />
* ***** END LICENSE BLOCK ***** *)</blockquote>
Выглядит его решение вот так:<br />
<br />
<pre class="brush:delphi">program mod_sum3;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
SysUtils;
type
TSum= array[0..3] of int64;
procedure Zero128(var sum: TSum);
begin;
sum[0]:=0;
sum[1]:=0;
sum[2]:=0;
sum[3]:=0;
end;
procedure Add128Progression(var sum: TSum; val, count: int64); //sum:=sum + val * count shr 1;
var
t: int64;
begin;
if val and 1=0 then val:=val shr 1 else count:=count shr 1;
t:=int64(Int64Rec(val).Lo) * Int64Rec(count).Lo;
sum[0]:=sum[0] + Int64Rec(t).Lo;
sum[1]:=sum[1] + Int64Rec(t).Hi;
t:=int64(Int64Rec(val).Lo) * Int64Rec(count).Hi;
sum[1]:=sum[1] + Int64Rec(t).Lo;
sum[2]:=sum[2] + Int64Rec(t).Hi;
t:=int64(Int64Rec(val).Hi) * Int64Rec(count).Lo;
sum[1]:=sum[1] + Int64Rec(t).Lo;
sum[2]:=sum[2] + Int64Rec(t).Hi;
t:=int64(Int64Rec(val).Hi) * Int64Rec(count).Hi;
sum[2]:=sum[2] + Int64Rec(t).Lo;
sum[3]:=sum[3] + Int64Rec(t).Hi;
end;
procedure Norm128(var sum: TSum);
begin;
sum[1]:=sum[1] + Int64Rec(sum[0]).Hi; Int64Rec(sum[0]).Hi:=0;
sum[2]:=sum[2] + Int64Rec(sum[1]).Hi; Int64Rec(sum[1]).Hi:=0;
sum[3]:=sum[3] + Int64Rec(sum[2]).Hi; Int64Rec(sum[2]).Hi:=0;
Int64Rec(sum[3]).Hi:=0;
end;
procedure Add128(var sum: TSum; val: int64); //sum:=sum + val;
begin;
sum[0]:=sum[0] + Int64Rec(val).Lo;
sum[1]:=sum[1] + Int64Rec(val).Hi;
end;
procedure Add128Norm(var sum: TSum; val: int64); //sum:=norm(sum + val);
begin;
Add128(sum, val);
Norm128(sum);
end;
procedure Not128Norm(var sum: TSum); //sum:=norm(not sum);
begin;
Norm128(sum);
Int64Rec(sum[0]).Lo:=not Int64Rec(sum[0]).Lo;
Int64Rec(sum[1]).Lo:=not Int64Rec(sum[1]).Lo;
Int64Rec(sum[2]).Lo:=not Int64Rec(sum[2]).Lo;
Int64Rec(sum[3]).Lo:=not Int64Rec(sum[3]).Lo;
end;
procedure Neg128(var sum: TSum); //sum:=-sum;
begin;
Not128Norm(sum);
Add128(sum, 1);
end;
procedure Sum128(var sum: TSum; n: int64);
var
m, c: cardinal;
i, t: int64;
begin;
Zero128(sum); t:=0;
c:=trunc(sqrt(n+0.0));
m:=c+1;
dec(c, ord(int64(c)*c=n));
while c>1 do begin;
i:=n div c;
t:=t + (cardinal(n)-cardinal(i)*c);
Add128Progression(sum, i+m, i-m+1);
dec(c);
end;
Neg128(sum);
Add128Progression(sum, n-m, n-m+1);
Add128Norm(sum,t);
end;
var
m, n: int64;
sum: TSum;
t: DWORD;
begin
t := GetTickCount;
N := 1000000000000;
Sum128(sum, n);
t:=GetTickCount-t;
Writeln(Format('%dms %.8x %.8x %.8x %.8x',[t, sum[3], sum[2], sum[1], sum[0]]));
Readln;
end.
</pre>
<br />
<b>Обоснование:</b><br />
<blockquote class="tr_bq">
Делим слагаемые на 2 группы: [1 .. trunc(sqrt(N))] и [trunc(sqrt[N))+1 .. N].<br />
<br />
В первой группе слагаемых мало, складываем их все непосредственно.<br />
Заметим, что и их количество, и каждое слагаемое не превышает sqrt(N).<br />
Значит, их сумма не превысит N.<br />
<br />
Вторая группа разбивается на sqrt(N) непересекающихся подгрупп. В первую войдут все номера больше N div 2. Во вторую – больше N div 3, не вошедшие в первую подгруппу. В третью – большие N div 4, не вошедшие в первые две, и т.д.<br />
<br />
Это те самые пики, но считать их неудобно.<br />
Удобнее считать сумму ПЕРЕСЕКАЮШИХСЯ подгрупп. Первая – это вся правая часть от trunc(sqrt(N))+1 до N, вторая – до N div 2 включительно, третья – до N div 3 включительно.<br />
<br />
В этом случае мы считаем общую сумму как прогрессию 0+1+2+…, а потом просто отнимаем превышения результата для каждой пересекающейся подгруппы. Так, например, для всех чисел от trunc(sqrt(N))+1 до N div 2 (это первая итерация) результат будет завышен по крайней мере (k+m) * (k-m+1) shr 1, где(k+m) - сумма первого и последнего элементов прогрессии, (k-m+1) - количество элементов в этой подгруппе. И т.д.</blockquote>
<br />
Ну а работает этот код примерно в районе <b>73 (!!!)</b> миллисекунд :)<br />
Единственное, что могу сказать по поводу этого варианта решения задачи:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhYv9f5KT9Up1SXwsiulSFEYIAeLgbcDw9IBjRP8zMFaOEVW2p0Wo-Jhl-ZZzjXSPc3s-s06xOeDvkr1cfxrmF4XZVtRC-o6cUctUUKUCVVOlRHIo67z6RssLOPQ18U3WrwGAiUcHOhHDM/s1600/mod4.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhYv9f5KT9Up1SXwsiulSFEYIAeLgbcDw9IBjRP8zMFaOEVW2p0Wo-Jhl-ZZzjXSPc3s-s06xOeDvkr1cfxrmF4XZVtRC-o6cUctUUKUCVVOlRHIo67z6RssLOPQ18U3WrwGAiUcHOhHDM/s1600/mod4.jpg" height="213" width="320" /></a></div>
<br />
На сем откланиваюсь, а исходники всех трех примеров <a href="http://rouse.drkb.ru/blog/mod_sum.zip" target="_blank">можно забрать тут</a>.<br />
Всем удачного Нового Года<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<span style="font-size: 12.7272720336914px;">© Александр (Rouse_) Багель</span></div>
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Январь, 2015</div>
</div>
<br />
<br />
<br />
<br /></div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com0tag:blogger.com,1999:blog-2374465879949372415.post-65948448931440720602014-12-25T21:51:00.000+03:002014-12-26T17:16:18.065+03:00Предновогодняя задачка для разминки мозга<div dir="ltr" style="text-align: left;" trbidi="on">
<div>
Пришлось тут на днях решать для студента (первого курса) задачку.</div>
<div>
Дословно выглядит вот так:</div>
<div>
<blockquote class="tr_bq">
2014 ACM-ICPC China Hunan Invitational Programming Contest<br />
There is a simple problem. Given a number N.<br />
You are going to calculate N%1+ N%2+ N%3+...+ N%N.<br />
Input: The length N(1<=N<=10^12).<br />
Output: Answer.<br />
Sample input 5<br />
Sample output 4</blockquote>
</div>
<div>
Вроде ничего сложного, однако-ж результат суммирования по модулю явно не уложится ни в один из поддерживаемых типов (int64 максимум 8 байт).</div>
<div>
<br /></div>
<div>
Сразу уточню: задачка олимпиадная, причем свежего 14-го года :)</div>
<div>
<br /></div>
<div>
<a name='more'></a><br /></div>
<div>
<div>
Конечно странно что такие вещи задают первокурснику, но...</div>
<div>
<br /></div>
<div>
В итоге для обкатки накидал такое решение на двух полусумматорах - банальная битовая арифметика:</div>
</div>
<div>
<br /></div>
<div>
<pre class="brush:cpp">#include "stdafx.h"
// хранилище для оооочень большого числа
unsigned char UInt128[16];
void printUInt128(){
printf("result = 0x");
for (int i = 15; i >= 0; i--)
printf("%hhX", UInt128[i]);
printf("\n");
}
// полный битовый сумматор реализующий таблицу истинности: http://ivatv.narod.ru/zifrovaja_texnika/1_04.htm
bool fullAdder(bool a, bool b, bool p0, bool *p){
// первый полусумматор
bool bRes = false;
*p = false;
if ((a && !b) || (b && !a))
bRes = true;
if (a && b)
*p = true;
if (!p0)
return bRes;
// второй полусумматор
*p = true;
bRes = !bRes;
if (!a && !b)
*p = false;
return bRes;
}
// сумматор на битовых операциях
void add(long long x){
bool pFlag = false;
unsigned long long halfResult = 0;
unsigned long long bigValue;
int i;
bool aBit, bBit, sFlag;
// получаем указатель на массив, содержащий большое число
unsigned long long *p;
p = (unsigned long long*)&UInt128[0];
// берем младшие 8 байт
bigValue = (unsigned long long)(*p);
for (i = 0; i < 64; i++){
// и побитно, посредством полного сумматора складываем два числа
aBit = ((unsigned long long)1 << i & x) > 0;
bBit = ((unsigned long long)1 << i & bigValue) > 0;
sFlag = fullAdder(aBit, bBit, pFlag, &pFlag);
if (sFlag)
halfResult |= (unsigned long long)1 << i;
};
// результат помещаем обратно
*p = halfResult;
// если нет переноса бит от предыдущей операции, то можно выходить
if (!pFlag)
return;
halfResult = 0;
// сдвигаемся на 8 байт
p++;
// берем старшие 8 байт
bigValue = (unsigned long long)(*p);
for (i = 0; i < 64; i++){
// увеличиваем значение опираясь на бит переноса
bBit = ((unsigned long long)1 << i & bigValue) > 0;
sFlag = fullAdder(false, bBit, pFlag, &pFlag);
if (sFlag)
halfResult |= (unsigned long long)1 << i;
};
// результат помещаем обратно
*p = halfResult;
}
int _tmain(int argc, _TCHAR* argv[])
{
unsigned long long a, i;
printf("enter value: ");
scanf("%lld", &a);
printf("calculating...\n");
i = a;
while(i > 0){
add(a % i);
i--;
};
printUInt128();
getchar();
getchar();
return 0;
}
</pre>
<br />
Либо, чтобы было проще понять, вариант на дельфи, но (увы) без коментариев:<br />
<br />
<pre class="brush:delphi">program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
SysUtils,
Math;
type
Int128 = array [0..15] of Byte;
var
VeriBigInteger: Int128;
procedure PrintInt128;
var
I: Integer;
begin
Write('0x');
for I := 15 downto 0 do
Write(IntToHex(VeriBigInteger[I], 2));
Writeln;
end;
function FullAdder(A, B, P0: Boolean; var P: Boolean): Boolean;
begin
Result := False;
P := False;
if A and not B then
Result := True;
if B and not A then
Result := True;
if A and B then
P := True;
if not P0 then Exit;
P := True;
Result := not Result;
if not A and not B then
P := False;
end;
procedure Add(Value: Int64);
var
I: Integer;
ABit, BBit, SFlag, PFlag: Boolean;
HalfResult, BigValue: Int64;
begin
PFlag := False;
HalfResult := 0;
BigValue := PInt64(@VeriBigInteger[0])^;
for I := 0 to 63 do
begin
ABit := (Int64(1) shl I and Value) > 0;
BBit := (Int64(1) shl I and BigValue) > 0;
SFlag := FullAdder(ABit, BBit, PFlag, PFlag);
if SFlag then
HalfResult := HalfResult or (Int64(1) shl I);
end;
PInt64(@VeriBigInteger[0])^ := HalfResult;
HalfResult := 0;
BigValue := PInt64(@VeriBigInteger[8])^;
for I := 0 to 63 do
begin
BBit := (1 shl I and BigValue) > 0;
SFlag := FullAdder(False, BBit, PFlag, PFlag);
if SFlag then
HalfResult := HalfResult or (Int64(1) shl I);
end;
PInt64(@VeriBigInteger[8])^ := HalfResult;
end;
function TstNative(X: int64): int64;
var
I: Int64;
begin
I := X;
Result := 0;
while I > 0 do
begin
Result := Result + (X mod I);
Dec(I);
end;
end;
procedure TstOld(X: int64);
var
I: Int64;
begin
ZeroMemory(@VeriBigInteger[0], SizeOf(Int128));
I := X;
while I > 0 do
begin
Add(X mod I);
Dec(I);
end;
PrintInt128;
end;
var
N: int64;
begin
try
repeat
Readln(N);
Writeln('0x', IntToHex(TstNative(N), 32));
TstOld(N);
until N = 0;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
readln;
end.
</pre>
<br />
Сразу скажу - ЭТО работает, причем работает правильно (можно даже использовать для проверки), однако есть более грамотный и более быстрый вариант решения в 17 строчек кода, вместо процедур Add и FullAdder (на дельфи или на си - кому как удобнее) :)<br />
<br />
Задача - попробовать найти данный вариант решения или предложить еще более оптимальный вариант :)<br />
<br />
С наступающим :)<br />
<br />
UPD: дам подсказку, график прохода по модулю выглядит вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmg6XJukyXLSqVp1ldG8G-fxDx188sF6H8PTyiyHvOlPU9ZJEBult1RqpV3U9KhwHucG7W9nXiDvrNp8kkc40iFtrUu4SCQDVzc0NB-YXr6SRg6a5kDToVeEdHTROLKt_7Y7vzQSc4bOX0/s1600/mod.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmg6XJukyXLSqVp1ldG8G-fxDx188sF6H8PTyiyHvOlPU9ZJEBult1RqpV3U9KhwHucG7W9nXiDvrNp8kkc40iFtrUu4SCQDVzc0NB-YXr6SRg6a5kDToVeEdHTROLKt_7Y7vzQSc4bOX0/s1600/mod.png" height="432" width="640" /></a></div>
<br />
Определение верхнего лимита прогрессии:<br />
<br />
X := Limit div (Z + 1) - (Z - (Limit mod (Z + 1)));<br />
Где:<br />
X - предел положительной вершины<br />
Limit - значение числа N из оригинальной задачи<br />
Z - текущее прирастание прогрессии (т.е. при Z = 1, прогрессия 0, 1, 2... при Z = 5, прогрессия 0, 5, 10... )<br />
<br />
т.е. грубо идем от 0 до 1000 - пики выглядят:<br />
<br />
499, 332, 247, 196, 165 и т.д....</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com17tag:blogger.com,1999:blog-2374465879949372415.post-5368746432404116002014-11-18T18:20:00.003+03:002014-11-18T18:20:49.411+03:00Пара слов о кэшировании данных при чтении и смартпойнтерах<div dir="ltr" style="text-align: left;" trbidi="on">
<div style="text-align: center;">
<span style="background-attachment: initial; background-clip: initial; background-color: #ccffff; background-image: none; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0b0080; cursor: help; font-style: italic;" title="просторечное"><a href="http://ru.wiktionary.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D1%81%D0%BB%D0%BE%D0%B2%D0%B0%D1%80%D1%8C:%D0%A3%D1%81%D0%BB%D0%BE%D0%B2%D0%BD%D1%8B%D0%B5_%D1%81%D0%BE%D0%BA%D1%80%D0%B0%D1%89%D0%B5%D0%BD%D0%B8%D1%8F" style="background-attachment: initial; background-clip: initial; background-image: none; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0b0080; text-decoration: none;" title="Викисловарь:Условные сокращения">Заначка</a> -</span> что-либо прибережённое, припрятанное про запас <span class="example-fullblock">◆ <span class="example-block" style="color: darkgreen;">Опять же всё расставив по местам, как и было, достал из-за трюмо тёткину <span class="example-select" style="background-color: #edf0ff;"><b>заначку</b></span> — вскрытую пачку «Любительских», — закурил. <span class="example-details" style="font-size: smaller;"><i>Андрей Битов, «Сад», 1960–1963 г. </i><small>(цитата из <a class="extiw" href="http://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D1%80%D0%BF%D1%83%D1%81_%D1%80%D1%83%D1%81%D1%81%D0%BA%D0%BE%D0%B3%D0%BE_%D1%8F%D0%B7%D1%8B%D0%BA%D0%B0" style="background-attachment: initial; background-clip: initial; background-image: none; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #663366; text-decoration: none;" title="w:Национальный корпус русского языка">Национального корпуса русского языка</a>, см. <a class="mw-redirect" href="http://ru.wiktionary.org/wiki/%D0%9F%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5:%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BB%D0%B8%D1%82%D0%B5%D1%80%D0%B0%D1%82%D1%83%D1%80%D1%8B#.D0.9F.D0.BE.D0.B8.D1.81.D0.BA_.D0.BF.D0.BE_.D0.BA.D0.BE.D1.80.D0.BF.D1.83.D1.81.D0.B0.D0.BC" style="background-attachment: initial; background-clip: initial; background-image: none; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0b0080; text-decoration: none;" title="Приложение:Список литературы">Список литературы</a>)</small></span></span></span></div>
<div style="text-align: center;">
<span class="example-fullblock"><span class="example-block" style="color: darkgreen;"><span class="example-details" style="font-size: smaller;"><small><br /></small></span></span></span></div>
<div style="text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZ34CABovfsQnd-HhtHmylUFeHzh3IIbQxT6GUwVYf-ZLosfP4VcsdJla-LSnfwe-9I2EJV6dd94WJoRbUpVjwYfUA1hDhSVFu7UGT680XgMfI2-5gjyPZ1HvJ59AnbmkK7Gmvkd_c17o/s1600/joke182467_64eb4cda.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZ34CABovfsQnd-HhtHmylUFeHzh3IIbQxT6GUwVYf-ZLosfP4VcsdJla-LSnfwe-9I2EJV6dd94WJoRbUpVjwYfUA1hDhSVFu7UGT680XgMfI2-5gjyPZ1HvJ59AnbmkK7Gmvkd_c17o/s320/joke182467_64eb4cda.jpg" width="258" /></a></div>
<br />
Я не думаю что сильно ошибусь, если скажу, что у большинства читателей данной статьи на компьютере присутствует папка, в которой хранятся наработки кода, применяющиеся потом в боевых проектах. Маленькие такие кусочки алгоритмов, на которых проверяется сама возможность реализации той или иной идеи. Я их называю "ништячки" :)<br />
Чем больше программист работает по своим задачам, тем больше эта папочка пухнет. Вот моя уже вылезла за пределы семи сотен различных демопримеров.<br />
Но проблема в том, что в 99 процентов случаев все эти "ништячки" пишутся в стол, и о существовании оных наработок знает только владелец данной папки, а ведь там же иногда целые закрома идей, подходов к реализации, алгоритмических трюков, да и просто остановленных на взлете мыслей, которыми не грех бы и поделиться (а вдруг кто-то возьмет да и разовьет подход) :)<br />
<br />
В данной статье я поделюсь тремя наработками, которые вышли как раз из таких вот "папок с ништяками" и уже не первый год применяются в наших боевых проектах.<br />
<br />
<a name='more'></a><br />
<h3 style="text-align: left;">
Начнем, пожалуй, с кэширования</h3>
<br />
Врятли я открою секрет, что побайтовое чтение файла - плохо.<br />
Ну что значит - плохо, да оно работает, и ошибок не выдает, но тормоза... Головки цилиндров и так ишачат как ошпаренные, пытаясь выдать всем страждущим нужные им данные, а тут мы со своим чтением одного байта из файла.<br />
<br />
А зачем мы вообще читаем ровно один байт?<br />
Если немного абстрагироваться от нагрузки на файловую систему и представить что файл, который мы читаем, выглядит как: "байт, содержащий размер блока данных + блок данных, за ним опять байт, содержащий размер блока данных + блок данных" - то все абсолютно логично. В данном случае мы выполняем единственную верную логику, читаем префикс, содержащий размер и сам блок данных, после чего повторяем, пока не уперлись в конец файла.<br />
<br />
Удобно? Даже не может возникнуть вопросов - конечно удобно.<br />
<br />
А что нам приходится делать на самом деле, чтоб уйти от тормозов при чтении:<br />
<ol style="text-align: left;">
<li>Читать сразу большой объем данных во временный буфер;</li>
<li>Реальное чтение производить уже из временного буфера;</li>
<li>А если во временном буфере данных не достаточно, опять их читать из файла и учитывать оффсеты и прочее сопутствующее;</li>
</ol>
И такая вот чехарда с ручным кэшированием в целой куче мест проекта, где требуется работа с файлами.<br />
<br />
Не удобно? Конечно неудобно, хочется такой-же простоты, как в первом варианте.<br />
<br />
Осмыслив суть проблемы, наш коллектив разродился следующей идеей: раз работа с данными идет через наследники от TStream (TFileStream, TWinHTTHStream, TWinFTPStream) - то не написать ли нам кэширующий проксик над самим стримом? Ну а почему бы и нет, не мы же первые - взять, к примеру, за образец тот же TStreamAdapter из System.Classes, выступающий прослойкой между IStream и абстрактным TStream.<br />
Удобная, кстати, вещь - советую :)<br />
<br />
Наш проксик выполнен в виде банального наследника от TStream, так что, при помощи него можно абсолютно свободно контролировать работу с данными любого другого наследника данного класса.<br />
<br />
Вообще реализация таких прокси-стримов, достаточно часто встречается. К примеру, если опустить TStreamAdapter, вам скорее всего будут известны такие классы как TZCompressionStream и TZDecompressionStream из модуля ZLib, которые предоставляют очень удобный способ сжатия и распаковки данных, хранящихся в любом произвольном наследнике TStream. Да я и сам раньше таким баловался, реализовав в свое время достаточно удобный проксик в виде класса <a href="https://github.com/AlexanderBagel/FWZip/blob/master/FWZipStream.pas" target="_blank">TFWZipItemStream</a>, который, пропуская все данные через себя, производит их правку "на лету" и до кучи считает контрольную сумму всех прошедших через него данных.<br />
<br />
Поэтому, взяв на вооружение уже накопленный ранее опыт, был рожден класс TBufferedStream, ну а в качестве уточнения по поводу работы с ним, к декларции класса был сразу прилеплен комментарий: "// типа буферизированное чтение из стрима. ReadOnly!!!"<br />
<br />
Но, прежде чем приступить к изучению кода данного класса, давайте напишем небольшое консольное приложение, которое измеряет нагрузку на приложение при использовании различных варинатов наследников от TStream, по скорости исполнения кода.<br />
<br />
В качестве PayLoad функционала сделаем следующее - вычислим оффсеты на секцию ресурсов каждой библиотеки, размещенной в системной директории (GetSystemDirectory) и засечем время, затраченное на выполнение при помощи TBufferedStream, затем TFileStream, ну и в конце, TMemoryStream.<br />
<br />
Такая последовательность выполнения тестов была выбрана с целью нивелирования влияния кэша файловой системы, т.е. TBufferedStream будет работать с некэшированными данными, а последующие два теста будут (должны) выполнятся существенно быстрее из-за повторного обращения к кэшированным (файловой системой) данным.<br />
<br />
Как думаете, кто победит?<br />
<br />
Впрочем:<br />
<br />
Для начала нам потребуется функция, которая построит список файлов, над которыми будет производится работа:<br />
<br />
<pre class="brush:delphi">function GetSystemRootFiles: TStringList;
var
Path: string;
SR: TSearchRec;
begin
Result := TStringList.Create;
SetLength(Path, MAX_PATH);
GetSystemDirectory(@Path[1], MAX_PATH);
Path := IncludeTrailingPathDelimiter(PChar(Path));
if FindFirst(Path + '*.dll', faAnyFile, SR) = 0 then
try
repeat
if SR.FindData.nFileSizeLow > 1024 * 1024 * 2 then
Result.Add(Path + SR.Name);
until FindNext(SR) <> 0;
finally
FindClose(SR);
end;
end;
</pre>
<br />
В ней создается экземпляр TStringList и заполняется путями к библиотекам, размер которых больше двух мегабайт (для демки - достаточно).<br />
<br />
Следующей функцией выступит общий обвес над стартом каждого теста с замером времени, тоже простенький, по сути:<br />
<br />
<pre class="brush:delphi">function MakeTest(AData: TStringList; StreamType: TStreamClass): DWORD;
var
TotalTime: DWORD;
I: Integer;
AStream: TStream;
begin
Writeln(StreamType.ClassName, ': ');
Writeln('===========================================');
AStream := nil;
TotalTime := GetTickCount;
try
for I := 0 to AData.Count - 1 do
begin
if StreamType = TBufferedStream then
AStream := TBufferedStream.Create(AData[I],
fmOpenRead or fmShareDenyWrite, $4000);
if StreamType = TFileStream then
AStream := TFileStream.Create(AData[I], fmOpenRead or fmShareDenyWrite);
if StreamType = TMemoryStream then
begin
AStream := TMemoryStream.Create;
TMemoryStream(AStream).LoadFromFile(AData[I]);
end;
Write('File: "', AData[I], '" CRC = ');
CalcResOffset(AStream);
end;
finally
Result := GetTickCount - TotalTime;
end;
end;
</pre>
<br />
Сам PayLoad функционал вынесен в модуль common_payload.pas и выглядит в виде процедуры CalcResOffset.<br />
<br />
<pre class="brush:delphi">procedure CalcResOffset(AData: TStream; ReleaseStream: Boolean);
var
IDH: TImageDosHeader;
NT: TImageNtHeaders;
Section: TImageSectionHeader;
I, A, CRC, Size: Integer;
Buff: array [0..65] of Byte;
begin
try
// читаем ImageDosHeader
AData.ReadBuffer(IDH, SizeOf(TImageDosHeader));
// смотрим по сигнатуре, что не ошиблись и работаем с правильным файлом
if IDH.e_magic <> IMAGE_DOS_SIGNATURE then
begin
Writeln('Invalid DOS header');
Exit;
end;
// прыгаем на начало PE заголовка
AData.Position := IDH._lfanew;
// читаем его
AData.ReadBuffer(NT, SizeOf(TImageNtHeaders));
// смотрим по сигнатуре, что не ошиблись и работаем с правильным файлом
if NT.Signature <> IMAGE_NT_SIGNATURE then
begin
Writeln('Invalid NT header');
Exit;
end;
// делаем "быструю" проверку на наличие секции ресурсов
if NT.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = 0 then
begin
Writeln('Resource section not found');
Exit;
end;
// "прыгаем" в начало списка секций
AData.Position :=
IDH._lfanew + SizeOf(TImageFileHeader) + 4 + Nt.FileHeader.SizeOfOptionalHeader;
// перечисляем их до тех пор...
for I := 0 to NT.FileHeader.NumberOfSections - 1 do
begin
AData.ReadBuffer(Section, SizeOf(TImageSectionHeader));
// ...пока не встретим секцию ресурсов
if PAnsiChar(@Section.Name[0]) = '.rsrc' then
begin
// а когда найдем ее - сразу "прыгаем" на ее начало
AData.Position := Section.PointerToRawData;
Break;
end;
end;
// "полезная нагрузка" (PayLoad) - суммируем все байты секции ресурсов
// типа контрольная сумма :)
CRC := 0;
Size := Section.SizeOfRawData div SizeOf(Buff);
for I := 0 to Size - 1 do
begin
AData.ReadBuffer(Buff[0], SizeOf(Buff));
for A := Low(Buff) to High(Buff) do
Inc(CRC, Buff[A]);
end;
Writeln(CRC);
finally
if ReleaseStream then
AData.Free;
end;
end;
</pre>
<br />
Лень было придумывать что-то сложное, наглядно демонстрирующее необходимость чтения файла кусками, поэтому я решил остановиться на работе с секциями PE файла.<br />
Задача даной процедуры - вычислить адрес секции ресурсов (.rsrc) переданного ей файла (в виде стрима) и просто посчитать сумму всех байт, размещенных в даной секции.<br />
В ней сразу видны два, необходимых для работы, чтения буфера с данными (DOS header и PE header), после которых происходит выход на секцию ресурсов, из которой читаются данные кусками по 64 байта и суммируются с результатом.<br />
ЗЫ: да, я в курсе что данные из секции не считаются целиком, т.к. чтение идет блоками и последний, не кратный 64 байтам не считается, но на то это и пример :)<br />
<br />
Запустим эту беду вот таким кодом:<br />
<br />
<pre class="brush:delphi">var
S: TStringList;
A, B, C: DWORD;
begin
try
S := GetSystemRootFiles;
try
//A := MakeTest(S, TBufferedStream);
B := MakeTest(S, TFileStream);
C := MakeTest(S, TMemoryStream);
Writeln('===========================================');
//Writeln('TBufferedStream = ', A);
Writeln('TFileStream = ', B);
Writeln('TMemoryStream = ', C);
finally
S.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
</pre>
<br />
Смотрим результат (на картинке уже включены результаты от TBufferedStream):<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijOhrKh3n9ayGV3UOcV59vARkPEKT-zBc3FflZEFoMPnn5HBQX4xSrg0QRPQIDacsNipJxaj7rHNfgItXds72W3uMU0cFxy2miL1agGnh4-sNsz355PqatjQTXavtoPWOVKWex9kGEdAk/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijOhrKh3n9ayGV3UOcV59vARkPEKT-zBc3FflZEFoMPnn5HBQX4xSrg0QRPQIDacsNipJxaj7rHNfgItXds72W3uMU0cFxy2miL1agGnh4-sNsz355PqatjQTXavtoPWOVKWex9kGEdAk/s1600/1.png" /></a></div>
<br />
TFileStream, как и ожидалось, сильно отстал, а вот TMemoryStream показал результат очень приближенный к результатам еще не рассмотренного нами TBufferedStream.<br />
Ничего страшного, дело в том, что сделал он это с большим оверхедом по памяти, т.к. ему пришлось загружать каждую библиотеку в память приложения (просадка), но догнал по скорости как раз по той же самой причине (уходом от необходимости частого чтения данных с диска).<br />
<br />
А теперь сам TBufferedStream:<br />
<br />
<pre class="brush:delphi"> TBufferedStream = class(TStream)
private
FStream: TStream;
FOwnership: TStreamOwnership;
FPosition: Int64;
FBuff: array of byte;
FBuffStartPosition: Int64;
FBuffSize: Integer;
function GetBuffer_EndPosition: Int64;
procedure SetBufferSize(Value: Integer);
protected
property Buffer_StartPosition: Int64 read FBuffStartPosition;
property Buffer_EndPosition: Int64 read GetBuffer_EndPosition;
function Buffer_Read(var Buffer; Size: LongInt): Longint;
function Buffer_Update: Boolean;
function Buffer_Contains(APosition: Int64): Boolean;
public
constructor Create(AStream: TStream; AOwnership: TStreamOwnership = soReference); overload;
constructor Create(const AFileName: string; Mode: Word; ABuffSize: Integer = 1024 * 1024); overload;
destructor Destroy; override;
function Read(var Buffer; Count: Longint): Longint; override;
function Write(const Buffer; Count: Longint): Longint; override;
function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override;
property BufferSize: Integer read FBuffSize write SetBufferSize;
procedure InvalidateBuffer;
end;
</pre>
<br />
Паблик секция не представляет из себя ничего необычного, все те же перекрытые Read/Write/Seek, как и у любого другого прокси-стрима.<br />
<br />
Весь фокус начинается с вот такой функции:<br />
<br />
<pre class="brush:delphi">function TBufferedStream.Read(var Buffer; Count: Longint): Longint;
var
Readed: Integer;
begin
Result := 0;
while Result < Count do
begin
Readed := Buffer_Read(PAnsiChar(@Buffer)[Result], Count - Result);
Inc(Result, Readed);
if Readed = 0 then
if not Buffer_Update then
Exit;
end;
end;
</pre>
<br />
Как можно понять по коду, мы пытаемся прочитать данные вызовом функции Buffer_Read, которая возвращает их из уже подготовленного кэша, а если не смогли прочитать, производится попытка переинициализации кэша вызовом Buffer_Update.<br />
<br />
Реинициализация кэша выглядит так:<br />
<br />
<pre class="brush:delphi">function TBufferedStream.Buffer_Update: Boolean;
begin
FStream.Position := FPosition;
FBuffStartPosition := FPosition;
SetLength(FBuff, FBuffSize);
SetLength(FBuff, FStream.Read(FBuff[0], FBuffSize));
Result := Length(FBuff) > 0
end;
</pre>
<br />
Т.е. выделяем память под кэш, размером, указанным в свойстве BufferSize класса, после чего производим попытку считать в кэш данные из контролируемого нами стрима.<br />
Если данные считались успешно, правим фактический размер кэша (ибо если хотели считать мегабайт, а всего доступно только 15 байт, то освободим ненужную память, зачем нам лишнее?).<br />
<br />
Операция чтения из кэша так-же проста:<br />
<br />
<pre class="brush:delphi">function TBufferedStream.Buffer_Read(var Buffer; Size: LongInt): Longint;
begin
Result := 0;
if not Buffer_Contains(FPosition) then Exit;
Result := Buffer_EndPosition - FPosition + 1;
if Result > Size then
Result := Size;
Move(FBuff[Integer(FPosition - Buffer_StartPosition)], Buffer, Result);
Inc(FPosition, Result);
end;
</pre>
<br />
Просто проверяем текущую позицию стрима и убеждаемся что мы действительно храним необходимые данные, доступные по данному оффсету, после чего банальным Move перекидываем данные во внешний буфер.<br />
<br />
Остальные методы данного класса чересчур тривиальны, поэтому рассматривать я их не буду, с ними можно будет ознакомится в демопримерах в архиве к статье: "<a href="http://rouse.drkb.ru/blog/store.zip" target="_blank">.\src\bufferedstream\</a>"<br />
<br />
<b>Что в итоге получается:</b><br />
<ol style="text-align: left;">
<li>Класс TBufferedStream имеет гораздо меньший (в разы) оверхед по скорости чтения данных, чем TFileStream, из-за реализованного в нем кэша. Количество операций чтения данных с диска (что само по себе есть достаточно "тяжелая операция") существенно уменьшено.</li>
<li>По этой же причине накладные расходы по скорости гораздо меньше по сравнению с TMemoryStream, т.к. читаются в кэш только нужные данные, а не весь файл целиком.</li>
<li>Оверхед по памяти существенно ниже чем у TMemoryStream, по понятным причинам. Конечно, в данном случае, по затратам на память выиграет TFileStream, но, опять-же, скорость...</li>
<li>Класс предоставляет удобную в использовании прослойку, позволяющую не задумываться о времени жизни контролируемого им стрима и сохраняющую весь необходимый для работы функционал.</li>
</ol>
Понравилось?<br />
Тогда перейдем ко второй части :)<br />
<br />
<h3 style="text-align: left;">
TOnMemoryStream</h3>
<br />
А вот представьте что данные, которые мы хотим прочитать, уже расположены в памяти нашего приложения. Дабы не переусложнять, остановимся опять на тех же библиотеках, рассмотреных в первой части статьи. Чтобы выполнить ту же самую работу, которая была показана в функции CalcResOffset, нам потребуется каким-то образом перекинуть данные о библиотеке в какой-то наследник от TStream (к примеру в тот-же TMemoryStream).<br />
<br />
И что мы сделаем в этом случае?<br />
В 99 процентах случаев, создадим TMemoryStream и вызовем функцию Write(WriteBuffer).<br />
А разве это нормально, ведь мы же по сути просто скопируем данные, которые и так уже у нас есть? И ведь сделаем то мы это по одной единственной причине - для того, чтобы можно было работать с данными посредством привычного нам TStream.<br />
<br />
Чтобы исправить этот лишний оверхед по памяти, и был разработан вот такой простенький класс:<br />
<br />
<pre class="brush:delphi">type
TOnMemoryStream = class(TCustomMemoryStream)
///Работаем на уже выделенном блоке памяти.
///Писать можем только в случае режима not ReadOnly, и только не выходя за пределы буфера
private
FReadOnly: Boolean;
protected
procedure SetSize(NewSize: Longint); override;
public
constructor Create(Ptr: Pointer; Size: Longint; ReadOnlyMode: Boolean = True);
function Write(const Buffer; Count: Longint): Longint; override;
property ReadOnly: Boolean read FReadOnly write FReadOnly;
end;
implementation
{ TOnMemoryStream }
constructor TOnMemoryStream.Create(Ptr: Pointer; Size: Longint; ReadOnlyMode: Boolean = True);
begin
inherited Create;
SetPointer(Ptr, Size);
FReadOnly := ReadOnlyMode;
end;
function TOnMemoryStream.Write(const Buffer; Count: Longint): Longint;
var
Pos: Longint;
begin
if (Position >= 0) and (Count >= 0) and
(not ReadOnly) and (Position + Count <=Size) then
begin
Pos := Position + Count;
Move(Buffer, Pointer(Longint(Memory) + Position)^, Count);
Position := Pos;
Result := Count;
end
else
Result := 0;
end;
procedure TOnMemoryStream.SetSize(NewSize: Longint);
begin
raise Exception.Create('TOnMemoryStream.SetSize can not be called.');
end;
</pre>
<br />
Даже не знаю что можно добавить к этому коду в качестве коментария, поэтому давайте просто посмотрим работу с данным классом.<br />
<br />
<pre class="brush:delphi">program onmemorystream_demo;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
SysUtils,
common_payload in '..\common\common_payload.pas',
OnMemoryStream in 'OnMemoryStream.pas';
var
M: TOnMemoryStream;
begin
try
M := TOnMemoryStream.Create(
Pointer(GetModuleHandle('ntdll.dll')),
1024 * 1024 * 8 {позволяем читать данные в пределах 8 мегабайт});
try
CalcResOffset(M, False);
finally
M.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
</pre>
<div>
<br /></div>
<div>
Здесь все просто - ищем адрес загруженной NTDLL.DLL и читаем ее секцию ресурсов напрямую из памяти, изпользуя все преимущества стрима (и не нужно ничего копировать во временный буфер :).</div>
<div>
<br /></div>
<div>
Теперь несколько коментариев по использовании класса.</div>
<div>
Вообще - он очень приятен, если его применять только в операциях чтения данных, но... как видно по коду, он не запрещает запись данных в контролируемый им блок памяти, а это может грозить большими неприятностями.</div>
<div>
Мы можем легко перезатереть критичные для работы приложения данные, после чего выйти на банальное AV, поэтому в наших проектах использвание этой возможности класса сведено к минимуму (буквально перестраиваем поисковые индексы в нужных местах на заранее выделенном буфере - так просто проще).</div>
<div>
Кстати, именно по этой причине мы отказались от использования Friendly классов, позволяющих получить доступ к вызову TCustomMemoryStream.SetPointer, т.к. в таком случае запись не будет контролироваться вообще никем, что может привести в итоге к хорошему такому "бадабуму".</div>
<div>
<br /></div>
<div>
Исходный код класса и примера можно посмотреть в архиве: "<a href="http://rouse.drkb.ru/blog/store.zip" target="_blank">.src\onmemorystream\</a>"</div>
<div>
<br /></div>
<div>
Впрочем, перейдем к заключающей части статьи.</div>
<div>
<br /></div>
<h3 style="text-align: left;">
Частный случай смартпойнера - SharedPtr</h3>
<div>
<br /></div>
<div>
Сейчас буду учить плохому :)</div>
<div>
<br /></div>
<div>
Давайте посмотрим как в Delphi принято работать с объектами. Обычно это выглядит так:</div>
<div>
<br /></div>
<div>
<div>
<pre class="brush:delphi">var
T: TObject;
begin
T := TObject.Create;
try
// работаем с Т
finally
T.Free;
end;
</pre>
</div>
<div>
<br /></div>
<div>
Новички в языке, конечно, забывают про использование секции финализации, выкатывая перлы вроде этого:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> T := TObject.Create;
// работаем с Т
T.Free;
</pre>
<br /></div>
<div>
А то и вообще, забывая про необходимость освобождения объекта, не говорят объекту Free.</div>
<div>
Некоторые "продвинутые новички" умудряются реализовать даже вот такой "говнокод"</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> try
T := TObject.Create;
// работаем с Т
finally
T.Free;
end;
</pre>
<div>
<br /></div>
<div>
А однажды я встретился и вот с такой реализацией:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> try
finally
T := TObject.Create;
// работаем с Т
T.Free;
end;
</pre>
<div>
<br /></div>
<div>
Ну старался человек - сразу видно :)</div>
<div>
Впрочем, давайте все же остановимся на первом варианте правильного кода.</div>
<div>
Минус у него следующий - если нам потребуется работа с несколькими классами одновременно, нам придется существенно развернуть код из-за множественных использований секций финализации:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">var
T1, T2, T3: TObject;
begin
T1 := TObject.Create;
try
T2 := TObject.Create;
try
T3 := TObject.Create;
try
// работаем со всеми тремя экземплярами Т1/Т2/Т3
finally
T3.Free;
end;
finally
T2.Free;
end;
finally
T1.Free;
end;
</pre>
<div>
<br /></div>
<div>
Есть, конечно, вариант, немножко сомнительный и не используемый мной, но в последнее время достаточно часто встречающийся на просторах интернета:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> T1 := nil;
T2 := nil;
T3 := nil;
try
T1 := TObject.Create;
T2 := TObject.Create;
T3 := TObject.Create;
// работаем со всеми тремя экземплярами Т1/Т2/Т3
finally
T3.Free;
T2.Free;
T1.Free;
end;
</pre>
<div>
<br /></div>
<div>
Из-за первоначальной инициализации каждого объекта в данном случае не произойдет ошибки при вызове Free еще не созданного объекта (если вдруг будет поднято исключение в конструкторе предыдущего), но все равно - выглядит чересчур сомнительно.</div>
<div>
<br /></div>
<div>
А как вы посмотрите на то, если я скажу что вызов метода Free вообще можно не делать?</div>
<div>
Да-да, просто создаем объект и забываем вообще про то, что его нужно разрушать.</div>
<div>
<br /></div>
<div>
Как это выглядит? Да вот так:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> T := TObject.Create;
// работаем с Т
</pre>
<div>
<br /></div>
<div>
Ну конечно прямо вот в таком виде это сделать не получится без мемлика - ну нет у нас сборщика мусора и прочего, но не торопитесь говорить: "Саня - да ты сдурел!"... ибо можно взять идею из других языков программирования и реализовать ее на нашем, "великом и могучем" :)</div>
<div>
<br /></div>
<div>
А идею мы возьмем от SharedPtr: <a href="http://msdn.microsoft.com/en-US/en-en/library/bb982026.aspx" target="_blank">смотрим документацию.</a></div>
<div>
<br /></div>
<div>
Логика данного класса проста - контроль времени жизни объекта посредством подсчета ссылок на него. Благо это мы умеем - есть у нас такой механизм, интерфейсами зовется.</div>
<div>
<br /></div>
<div>
Но не все так просто.</div>
<div>
<br /></div>
<div>
Конечно, с наскока, можно выкатить такую идею - реализуем в классе поддержку IUnknown и все, как только счетчик ссылок на экземпляр класса достигнет нуля - он разрушится.</div>
<div>
Но сделать то это мы сможет только с собственноручно написанными классами, а что делать с тем же TMemoryStream, которому весь этот фень-шуй по барабану, ибо он знать не знает об интерфейсах?</div>
<div>
<br /></div>
<div>
Самое логичное - писать очередной проксик, который будет держать линк на контролируемый им объект и в себе самом будет реализовывать подсчет ссылок, а при своем разрушении будет грохать доверенный ему на хранение объект.</div>
<div>
<br /></div>
<div>
Но тут тоже не все так радужно. Проксик-то мы напишем, да и что там его писать - идея ведь уже озвучена, но будет большая просадка как по памяти, так и по скорости работы с классом, если он будет использовать в качестве механизма подсчета ссылок классический интерфейс, со всем сопуствующим.</div>
<div>
<br /></div>
<div>
Поэтому подойдем к решению задачи с технической стороны и посмотрим на минусы реализации через интерфейс:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">program slowsharedptr;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
Classes,
SysUtils;
type
TObjectDestroyer = class(TInterfacedObject)
private
FObject: TObject;
public
constructor Create(AObject: TObject);
destructor Destroy; override;
end;
TSharedPtr = record
private
FDestroyerObj: TObjectDestroyer;
FDestroyer: IUnknown;
public
constructor Create(const AValue: TObject);
end;
{ TObjectDestroyer }
constructor TObjectDestroyer.Create(AObject: TObject);
begin
inherited Create;
FObject := AObject;
end;
destructor TObjectDestroyer.Destroy;
begin
FObject.Free;
inherited;
end;
{ TSharedPtr }
constructor TSharedPtr.Create(const AValue: TObject);
begin
FDestroyerObj := TObjectDestroyer.Create(AValue);
FDestroyer := FDestroyerObj;
end;
var
I: Integer;
T: DWORD;
begin
ReportMemoryLeaksOnShutdown := True;
try
T := GetTickCount;
for I := 0 to $FFFFFF do
TSharedPtr.Create(TObject.Create);
Writeln(GetTickCount - T);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
</pre>
<div>
<br /></div>
<div>
Временные затраты на исполнение данного кода будут в районе 3525 миллисекунд (запомним это число). </div>
<div>
<br /></div>
<div>
Суть: основную логику релизует класс TObjectDestroyer, который работает с подсчетом ссылок и разрушает переданный ему на хранение объект. TSharedPtr - структура, посредством которой происходит правильная работа со ссылками в тот момент, когда она выходит из области видимости (конечно, в данном случае, можно сделать и без этой структуры, но...).</div>
<div>
Если запустите пример, то увидите, что созданные объекты будут разрушены до завершения работы приложения (впрочем, если бы это было не так, об этом вам явно было бы сообщено, т.к. взведен флаг ReportMemoryLeaksOnShutdown ).</div>
<div>
<br /></div>
<div>
Но давайте разберем подробней - где же здесь может быть ненужный нам оверхед (причем как по памяти, так и по скорости выполнения).</div>
<div>
<br /></div>
<div>
Ну, во-первых - TObjectDestroyer.InstanceSize равен 20.</div>
<div>
Хех, получаем лишние 20 байт памяти на каждый контролируемый нами объект, а с учетом того что гранулярность менеджера памяти в Delphi равна 12 байтам, то теряются не 20 байт, а все 24. Думаете мелочи? Может быть и так - но наш вариант должен выходить (и будет) ровно на 12 байт, ибо если убирать оверхэд - так целиком :)</div>
<div>
<br /></div>
<div>
Вторая проблема - избыточный оверхэд при вызове методов интерфейса.</div>
<div>
Давайте вспомним, как выглядит в памяти VMT объекта, реализующего интерфейс.</div>
<div>
VMT объекта начинается с виртуальных методов самого объекта, включая и перекрытые методы интерфейса, причем эти перекрытые методы не принадлежат интерфейсу.</div>
<div>
И вот только за ними идет VMT методов самого интерфейса, при вызове которых происходит перенаправление (посредством CompilerMagic константы, рассчитываемой для каждого интрефейса на этапе компиляции) на реальный код.</div>
<div>
<br /></div>
<div>
Это можно увидеть наглядно выполнив вот такой код:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">constructor TSharedPtr.Create(const AValue: TObject);
var
I: IUnknown;
begin
FDestroyerObj := TObjectDestroyer.Create(AValue);
I := FDestroyerObj;
I._AddRef;
I._Release;
</pre>
<div>
<br /></div>
<div>
Если посмотреть на ассемблерный листинг, то мы увидим следующее:</div>
<div>
<br /></div>
<div>
<pre class="brush:asm">slowsharedptr.dpr.51: I._AddRef;
004D3C73 8B45F4 mov eax,[ebp-$0c]
004D3C76 50 push eax
004D3C77 8B00 mov eax,[eax]
004D3C79 FF5004 call dword ptr [eax+$04] // нас интересует вот этот вызов
slowsharedptr.dpr.52: I._Release;
004D3C7C 8B45F4 mov eax,[ebp-$0c]
004D3C7F 50 push eax
004D3C80 8B00 mov eax,[eax]
004D3C82 FF5008 call dword ptr [eax+$08] // и вот этот вызов
</pre>
<div>
<br /></div>
<div>
... которые приводят к:</div>
<div>
<br /></div>
<div>
<pre class="brush:asm">004021A3 83442404F8 add dword ptr [esp+$04],-$08 // выход на VMT объекта
004021A8 E93FB00000 jmp TInterfacedObject._AddRef
</pre>
<div>
<br /></div>
<div>
в первом случае, а во втором на:</div>
<div>
<br /></div>
<div>
<div>
<pre class="brush:asm">004021AD 83442404F8 add dword ptr [esp+$04],-$08 // выход на VMT объекта
004021B2 E951B00000 jmp TInterfacedObject._Release
</pre>
<br /></div>
</div>
<div>
Если бы мы наследовались в TObjectDestroyer не от IUnknown, а, к примеру, от IEnumerator, то компилятор автоматом подправил бы адреса выхода на VMT объекта примерно таким образом:</div>
<div>
<br /></div>
<div>
<pre class="brush:asm">004D3A4B 83442404F0 add dword ptr [esp+$04],-$10 // было 8, стало 16
004D3A50 E9CB97F3FF jmp TInterfacedObject._AddRef
004D3A55 83442404F0 add dword ptr [esp+$04],-$10 // т.к. добавились еще несколько функций
004D3A5A E9DD97F3FF jmp TInterfacedObject._Release
</pre>
<div>
<br /></div>
<div>
Именно через такой прыжок компилятор производит вызов методов _AddRef и _Release при изменении счетчика ссылок (к примеру при присвоении интерфейса новой переменной, или при выходе за область видимости).<br />
<br />
Поэтому сейчас будем побеждать всю эту беду и напишем свой собственный интерфейс.</div>
<div>
<br /></div>
<div>
Итак пишем:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> PObjectDestroyer = ^TObjectDestroyer;
TObjectDestroyer = record
strict private
class var VTable: array[0..2] of Pointer;
class function QueryInterface(Self: PObjectDestroyer;
const IID: TGUID; out Obj): HResult; stdcall; static;
class function _AddRef(Self: PObjectDestroyer): Integer; stdcall; static;
class function _Release(Self: PObjectDestroyer): Integer; stdcall; static;
class constructor ClassCreate;
private
FVTable: Pointer;
FRefCount: Integer;
FObj: TObject;
public
class function Create(AObj: TObject): IUnknown; static;
end;
</pre>
<div>
<br /></div>
<div>
Думаете это структура типа record?</div>
<div>
Неа - это самый что ни на есть объект, со своей собственной VMT, расположенной в VTable и размером ровно в 12 байт:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> FVTable: Pointer;
FRefCount: Integer;
FObj: TObject;
</pre>
<br /></div>
<div>
Теперь собственно сама "магия" :)</div>
<div>
<br /></div>
<div>
Инициализация VMT происходит в следующем методе:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">class constructor TObjectDestroyer.ClassCreate;
begin
VTable[0] := @QueryInterface;
VTable[1] := @_AddRef;
VTable[2] := @_Release;
end;
</pre>
<div>
<br /></div>
<div>
Все по канонам, и Delphi даже не заподозрит тут какой-либо подвох, ведь для нее это будет абсолютно валидная VMT, реализованная по всем законам и правилам.</div>
<div>
<br /></div>
<div>
Ну а основной конструктор выглядит так:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">class function TObjectDestroyer.Create(AObj: TObject): IUnknown;
var
P: PObjectDestroyer;
begin
if AObj = nil then Exit(nil);
GetMem(P, SizeOf(TObjectDestroyer));
P^.FVTable := @VTable;
P^.FRefCount := 0;
P^.FObj := AObj;
Result := IUnknown(P);
end;
</pre>
<div>
<br /></div>
<div>
Через GetMem выделяем место под InstanceSize нашего "якобы" класса, не смотря на то, что он в действительности является структурой, после чего инициализируем требуемые поля в виде указателя на VMT, счетчик ссылок и указатель на контролируемый классом объект.</div>
<div>
Причем этим мы сразу обходим оверхэд на вызове InitInstance и сопутстующую ему нагрузку.</div>
<div>
Обратите внимение - результат вызова конструктора - интерфейс IUnknown.</div>
<div>
<br /></div>
<div>
Хак? Конечно. </div>
<div>
Работает? Безусловно :)</div>
<div>
<br /></div>
<div>
Реализация методов QueryInterface, _AddRef и _Release взята от стандартного TIntefacedObject и не интересна. Впрочем QueryInterface в данном подходе по сути избыточен, но раз мы решили делать все по классике, и закладываемся на то, что какой-то "безумный программыст" все равно попробует дернуть данный метод, то оставим его на положенном ему месте (тем более что он и так должен идти первым в VMT интерфейса. Ну не оставлять же вместо него там мусорный указатель?).</div>
<div>
<br /></div>
<div>
Теперь немного поколдуем над структурой, с помощью которой мы обеспечивали контроль за ссылками:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> TSharedPtr<T: class> = record
private
FPtr: IUnknown;
function GetValue: T; inline;
public
class function Create(AObj: T): TSharedPtr<T>; static; inline;
class function Null: TSharedPtr<T>; static;
property Value: T read GetValue;
function Unwrap: T;
end;
</pre>
<div>
<br /></div>
<div>
Немножко поменялся конструктор: </div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">class function TSharedPtr<T>.Create(AObj: T): TSharedPtr<T>;
begin
Result.FPtr := TObjectDestroyer.Create(AObj);
end;
</pre>
<div>
<br /></div>
<div>
Впрочем, суть от этого не изменилась.</div>
<div>
Добавился новый метод, посредством которого можно будет получать доступ, к котролируемому нашим шарепойнтером объекту:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">function TSharedPtr<T>.GetValue: T;
begin
if FPtr = nil then Exit(nil);
Result := T(PObjectDestroyer(FPtr)^.FObj);
end;
</pre>
<div>
<br /></div>
<div>
Ну и две утилитарных процедуры, первая из которых просто уменьшает количество ссылок:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">class function TSharedPtr<T>.Null: TSharedPtr<T>;
begin
Result.FPtr := nil;
end;
</pre>
<div>
<br /></div>
<div>
А вторая отключает контролируемый классом объект от всего этого механизма:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">function TSharedPtr<T>.Unwrap: T;
begin
if FPtr = nil then Exit(nil);
Result := T(PObjectDestroyer(FPtr).FObj);
PObjectDestroyer(FPtr).FObj := nil;
FPtr := nil;
end;
</pre>
<div>
<br /></div>
<div>
Теперь давайте посмотрим - а зачем вообще оно все это нужно?</div>
<div>
Рассмотрим ситуацию:</div>
<div>
Вот, к примеру, создали мы некий экземпляр класса, за которым следит TObjectDestroyer и отдали его наружу, что в этом случае произойдет?</div>
<div>
Правильно - как только завершится выполнение кода процедуры, в которой был создан объект, он будет сразу разрушен и внешний код будет работать с уже убитым указателем.</div>
<div>
Именно для этого и введен класс TSharedPtr<T>, посредством которого можно "прокидывать" данные по процедурам нашего приложения, не боясь преждевременного разрушения объекта. Как только он действительно станет никому не нужен - TObjectDestroyer его моментально грохнет и всем будет нирвана.</div>
<div>
<br /></div>
<div>
Но это еще не все :)</div>
<div>
<br /></div>
<div>
Покрутив реализацию TSharedPtr<T> мы все же пришли к выводу, что она не совсем удачна. И знаете почему? </div>
<div>
А потому что вот такой код конструктора нам показался чересчур избыточным:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">TSharedPtr<TMyObj>.Create(TMyObj.Create);
</pre>
<div>
<br /></div>
<div>
Ага - именно так это и нужно вызывать, но чтобы не пугать неподготовленных к такому счастью программистов, мы решили добавить небольшую оберточку вот такого плана:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> TSharedPtr = record
public
class function Create<T: class>(AObj: T): TSharedPtr<T>; static; inline;
end;
...
class function TSharedPtr.Create<T>(AObj: T): TSharedPtr<T>;
begin
Result.FPtr := TObjectDestroyer.Create(AObj);
end;
</pre>
<div>
<br /></div>
<div>
После которой все стало гораздо приятней, и вызов шарепойнтера стал выглядеть гораздо привычнее, и похожим на создание ранее озвученного проксика:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">TSharedPtr.Create(TObject.Create)
</pre>
<div>
<br /></div>
<div>
Впрочем, хватит разглагольствовать и посмотрим на просадку по времени (а она, конечно, будет):</div>
<div>
<br /></div>
<div>
Пишем код:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">program sharedptr_demo;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
System.SysUtils,
StaredPtr in 'StaredPtr.pas';
const
Count = $FFFFFF;
procedure TestObj;
var
I: Integer;
Start: Cardinal;
Obj: TObject;
begin
Start := GetTickCount;
for I := 0 to Count - 1 do
begin
Obj := TObject.Create;
try
// do nothing...
finally
Obj.Free;
end;
end;
Writeln(PChar('TObject: ' + (GetTickCount - Start).ToString()));
end;
procedure TestAutoDestroy;
var
I: Integer;
Start: Cardinal;
begin
Start := GetTickCount;
for I := 0 to Count - 1 do
TObject.Create.AutoDestroy;
Writeln(PChar('AutoDestroy: ' + (GetTickCount - Start).ToString()));
end;
procedure TestSharedPtr;
var
I: Integer;
Start: Cardinal;
begin
Start := GetTickCount;
for I := 0 to Count - 1 do
TSharedPtr.Create(TObject.Create);
Writeln(PChar('SharedPtr: ' + (GetTickCount - Start).ToString()));
end;
begin
try
TestObj;
TestAutoDestroy;
TestSharedPtr;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
</pre>
<div>
<br /></div>
<div>
И смотрим, что получилось:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgvswNOTeivKrC4rqElns6KdvOiNNBuuu7jNcybixSGkgbXgDvyN09aFtIG0rm6ci5tckUBDwcBS4FBDKktpjX-qRyOY0Izyx769A6AzGTFJwfgezy9jMgbFeWeJe7vuok3lWwHdPPI9Xo/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgvswNOTeivKrC4rqElns6KdvOiNNBuuu7jNcybixSGkgbXgDvyN09aFtIG0rm6ci5tckUBDwcBS4FBDKktpjX-qRyOY0Izyx769A6AzGTFJwfgezy9jMgbFeWeJe7vuok3lWwHdPPI9Xo/s1600/2.png" /></a></div>
<br /></div>
<div>
В первом варианте шарепойнтера была задержка в 3525 миллисекунд, новый вариант выдет число 2917 - не зря старались, получается.</div>
<div>
Однако - что это за AutoDestroy, который обогнал шарепойнтер на целую секунду?</div>
<div>
<br /></div>
<div>
Это хэлпер, и это плохо :(</div>
<div>
Плохо потому, что этот хэлпер реализован над TObject:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi"> TObjectHelper = class helper for TObject
public
function AutoDestroy: IUnknown; inline;
end;
...
{ TObjectHelper }
function TObjectHelper.AutoDestroy: IUnknown;
begin
Result := TObjectDestroyer.Create(Self);
end;
</pre>
<div>
<br /></div>
<div>
Дело в том что, по крайней мере в ХЕ4 все еще не побежден конфликт с пересекающимися хэлперами, т.е. если у вас есть собственный хэлпер над TStream и вы попробуете подключить к нему в пару TObjectHelper - проект не сбилдится.</div>
<div>
Не знаю, решена ли эта проблема в ХЕ7, но в четверке она точно присутствует, и по этой причине мы не используем данный кусок кода, хотя он гораздо производительней, чем использование структуры TSharedPtr.<br />
<br />
Теперь давайте рассмотрим предпоследний момент, о котором я говорил выше, а именно - о реализации прыжка на VMT, для этого напишем две простых процедуры:<br />
<br />
<pre class="brush:delphi">procedure TestInterfacedObjectVMT;
var
I: IUnknown;
begin
I := TInterfacedObject.Create;
end;
</pre>
<div>
<br /></div>
<div>
В самом начале я упоминал, что использорвание простейшего варианта TSharedPtr в самом первом примере немного избыточно. Да, это так, в том случае можно было просто запоминать ссылку на интерфейс в локальной переменной (чем TSharedPtr по сути и занимается, правда немного другим способом);</div>
<div>
<br /></div>
<div>
Итак, смотрим, что происходит в этом варианте кода:</div>
<div>
<br /></div>
<div>
1. Создание объекта и инициализация интерфейса:</div>
<div>
<br /></div>
<div>
<pre class="brush:asm">sharedptr_demo.dpr.60: I := TInterfacedObject.Create;
004192BB B201 mov dl,$01
004192BD A11C1E4000 mov eax,[$00401e1c]
004192C2 E899C5FEFF call TObject.Create
004192C7 8BD0 mov edx,eax
004192C9 85D2 test edx,edx
004192CB 7403 jz $004192d0
004192CD 83EAF8 sub edx,-$08
004192D0 8D45FC lea eax,[ebp-$04]
004192D3 E8C801FFFF call @IntfCopy
</pre>
<div>
<br /></div>
<div>
2. Вызов секции финализации:</div>
<div>
<br /></div>
<div>
<pre class="brush:asm">sharedptr_demo.dpr.61: end;
004192D8 33C0 xor eax,eax
004192DA 5A pop edx
004192DB 59 pop ecx
004192DC 59 pop ecx
004192DD 648910 mov fs:[eax],edx
004192E0 68F5924100 push $004192f5
004192E5 8D45FC lea eax,[ebp-$04]
004192E8 E89B01FFFF call @IntfClear // <<< нас интересует вот этот вызов
004192ED C3 ret
</pre>
<div>
<br />
3. После чего управление передается на @IntfClear, где нас и поджидает озвученный ранее прыжок:<br />
<br />
<pre class="brush:asm">00401DE1 83442404F8 add dword ptr [esp+$04],-$08
00401DE6 E951770000 jmp TInterfacedObject._Release
</pre>
<br />
А что происходит в варинте использования TObjectDestroyer?<br />
<br />
<pre class="brush:delphi">procedure TestSharedPtrVMT;
begin
TObjectDestroyer.Create(TObject.Create);
end;
</pre>
<br />
1. Создание объекта и создание самого TObjectDestroyer:<br />
<br />
<pre class="brush:asm">sharedptr_demo.dpr.66: TObjectDestroyer.Create(TObject.Create);
004D3C27 B201 mov dl,$01
004D3C29 A184164000 mov eax,[$00401684]
004D3C2E E89945F3FF call TObject.Create
004D3C33 8D55FC lea edx,[ebp-$04]
004D3C36 E8B5FBFFFF call TObjectDestroyer.Create
</pre>
<br />
Да, есть оверхед, лишнее действие, как-никак. Впрочем, а что там с разрушением?<br />
<br />
2. Все очень просто:<br />
<br />
<pre class="brush:asm">sharedptr_demo.dpr.67: end;
004D3C3B 33C0 xor eax,eax
004D3C3D 5A pop edx
004D3C3E 59 pop ecx
004D3C3F 59 pop ecx
004D3C40 648910 mov fs:[eax],edx
004D3C43 68583C4D00 push $004d3c58
004D3C48 8D45FC lea eax,[ebp-$04]
004D3C4B E8DC92F3FF call @IntfClear
004D3C50 C3 ret
</pre>
<br />
Практически идентично первому варианту.<br />
Но самое интересное все же произойдет при вызове @IntfClear, он пропустит избыточные прыжки по VMT и передаст управление сразу на class function TObjectDestroyer._Release.<br />
В итоге сэкономили на вызове двух инструкций (add и jmp), но это к сожалению пока что самое минимальное, что можно сделать, т.к. в случае использования проксика - накладные расходы ну просто не избежны :)<br />
<br /></div>
<div>
В завершение, осталось только посмотреть, как использовать механизм автоматического разрушения объекта на практике:</div>
<div>
<br /></div>
<div>
К примеру, создадим файловый стрим и запишем в него некую константу:<br />
<br />
<pre class="brush:delphi">procedure TestWriteBySharedPtr;
var
F: TFileStream;
ConstData: DWORD;
begin
ConstData := $DEADBEEF;
F := TFileStream.Create('data.bin', fmCreate);
TObjectDestroyer.Create(F);
F.WriteBuffer(ConstData, SizeOf(ConstData));
end;
</pre>
<div>
<br /></div>
<div>
Да, это все - время жизни стрима контролируется, и избыточных поползновений не требуется.<br />
В данном случае структура TSharedPtr не используется, т.к. отсутствует необходимость передачи указателя между участками кода и достаточно функционала TObjectDestroyer.<br />
<br /></div>
<div>
А теперь прочитаем значение константы из файла и выведем на экран, причем сразу посмотрим на передачу данных между процедурами.<br />
<br /></div>
<div>
Вот так мы создадим объект, контролируемый шарепойнтером:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">function CreateReadStream: TSharedPtr<TFileStream>;
begin
Result := TSharedPtr.Create(TFileStream.Create('data.bin',
fmOpenRead or fmShareDenyWrite));
end;
</pre>
<div>
<br /></div>
<div>
А так мы получим из этого объекта данные:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">procedure TestReadBySharedPtr;
var
F: TSharedPtr<TFileStream>;
ConstData: DWORD;
begin
F := CreateReadStream;
F.Value.ReadBuffer(ConstData, SizeOf(ConstData));
Writeln(IntToHex(ConstData, 8));
end;
</pre>
<div>
<br /></div>
<div>
Как видите - код практически не изменился, если сравнивать его с классическим подходом к разработке ПО.<br />
<br /></div>
<div>
<b>Плюсы </b>- пропала необходимость использования блоков TRY..FINALLY, код стал менее перегруженным по объему.<br />
<br /></div>
<div>
<b>Минусы </b>- небольшой оверхэд по скорости и немного расширились конструкторы, заставляя нас каждый раз вызывать TSharedPtr.Create (в члучае передачи данных на внешку) или TObjectDestroyer для контроля времени жизни.<br />
Так же появился дополнительный параметр Value, посредством которого можно получить доступ к контролируемому объекту в случае использования TSharedPtr, но к этому достаточно просто привыкнуть, тем более, что это все, на что способна дельфи в плане синтаксического сахара.<br />
<br /></div>
<div>
Хотя я все еще мечтаю что появится DEFAULT метод объекта (или свойство не перечислимого типа), которое можно будет вызывать без указания его имени простым обращением к переменной класа, тогда бы мы объявили свойство Value у класса TSharedPtr дефолтным и работали бы с базовым объектом, даже не зная что он находится под контролем проксика :)</div>
<div>
<br /></div>
<h3 style="text-align: left;">
Выводы</h3>
<div>
<br /></div>
<div>
Вывод один - утомился я все это расписывать :)<br />
<br /></div>
<div>
А если серьезно, все три показанные выше подхода достаточно удобные, по сути, причем первые два я использую практически повсеместно.</div>
<div>
<br /></div>
<div>
С TSharedPtr я, конечно, осторожничаю.<br />
<br /></div>
<div>
Не подумайте что он плох - по другой причине. Мне все еще (за столько-то лет практики) некомфортно наблюдать код без использования секций финализации, хотя задним мозжечком-то я, конечно, понимаю, что все это сработает как надо - но... не привычно.<br />
<br /></div>
<div>
Поэтому TSharedPtr я использую только в нескольких частных случаях - когда нужно отпустить объект на волю во внешний, не контролируемый мной код, хотя мои коллеги придерживаются несколько другой точки зрения и используют его достаточно часто (конечно, не везде, ибо сами видите, что основной его минус - двойная просадка по скорости, как расплата за удобство использования).</div>
<div>
<br /></div>
<div>
И на этом, пожалуй, я закругляюсь.<br />
<br /></div>
<div>
Проверяйте свои закрома - делитесь, ведь там точно есть что-то полезное.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCV2eDfSiwDd-UgSvNrYGy8Xc6HLRsp_9xaPUTcS0Rvyl7Kc7axS2nAxOmWWDgAYjEzHbna2E4VHXsXNQIJrf8D7b6AVsLS3uDo0FBAcMh5LZxdshozTsL1SGPPXmZfgMWU7T2A9-1xx0/s1600/988140.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCV2eDfSiwDd-UgSvNrYGy8Xc6HLRsp_9xaPUTcS0Rvyl7Kc7axS2nAxOmWWDgAYjEzHbna2E4VHXsXNQIJrf8D7b6AVsLS3uDo0FBAcMh5LZxdshozTsL1SGPPXmZfgMWU7T2A9-1xx0/s1600/988140.jpg" height="240" width="320" /></a></div>
<br /></div>
<div>
<span style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.4799995422363px;">Исходный код демопримеров доступен по </span><span style="background-color: white; color: red; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.4799995422363px;"><a href="http://rouse.drkb.ru/blog/store.zip" style="color: #797979; text-decoration: none;" target="_blank">данной ссылке</a></span><span style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.4799995422363px;">.</span><br />
<span style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.4799995422363px;"><br /></span>
<span style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.4799995422363px;">Как всегда, благодарю участников форума "<a href="http://www.delphimaster.ru/" target="_blank">Мастера Дельфи</a>" за вычитку статьи.</span></div>
<div>
<br /></div>
<div>
Удачи!!!</div>
<div>
<br /></div>
<div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
© Коллектив IT отдела МГК "ГРАНД"</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
© Евгений (Jack128) Савин</div>
<div style="margin: 0px;">
© Юрий Федоров</div>
<div style="margin: 0px;">
<span style="font-size: 12.7272720336914px;">© Александр (Rouse_) Багель</span></div>
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Ноябрь, 2014</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com3tag:blogger.com,1999:blog-2374465879949372415.post-13825636355271079412014-10-31T13:19:00.000+03:002014-11-06T14:39:04.813+03:00Поддержка MultiTouch + Gestures в XE4<div dir="ltr" style="text-align: left;" trbidi="on">
<div class="separator" style="clear: both; text-align: center;">
</div>
<blockquote class="tr_bq" style="clear: both; text-align: center;">
<b style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;">Мультитач</b><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;"> (рус. </span><i style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;">Множественное касание</i><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;">) — функция </span><a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D0%BD%D1%81%D0%BE%D1%80%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0" style="background: none rgb(255, 255, 255); color: #0b0080; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start; text-decoration: none;" title="Сенсорная система">сенсорных</a><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;"> </span><a class="mw-redirect" href="https://ru.wikipedia.org/wiki/%D0%A3%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%B0_%D0%B2%D0%B2%D0%BE%D0%B4%D0%B0" style="background: none rgb(255, 255, 255); color: #0b0080; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start; text-decoration: none;" title="Устройства ввода">систем ввода</a><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;"> (</span><a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D0%BD%D1%81%D0%BE%D1%80%D0%BD%D1%8B%D0%B9_%D1%8D%D0%BA%D1%80%D0%B0%D0%BD" style="background: none rgb(255, 255, 255); color: #0b0080; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start; text-decoration: none;" title="Сенсорный экран">сенсорный экран</a><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;">, </span><a class="mw-redirect" href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D0%BD%D1%81%D0%BE%D1%80%D0%BD%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BD%D0%B5%D0%BB%D1%8C" style="background: none rgb(255, 255, 255); color: #0b0080; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start; text-decoration: none;" title="Сенсорная панель">сенсорная панель</a><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;">), осуществляющая одновременное определение </span><a class="mw-redirect" href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BE%D1%80%D0%B4%D0%B8%D0%BD%D0%B0%D1%82%D1%8B" style="background: none rgb(255, 255, 255); color: #0b0080; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start; text-decoration: none;" title="Координаты">координат</a><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;"> двух и более точек </span><a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%81%D0%B0%D0%BD%D0%B8%D0%B5" style="background: none rgb(255, 255, 255); color: #0b0080; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start; text-decoration: none;" title="Касание">касания</a><span style="background-color: white; color: #252525; font-family: sans-serif; font-size: 14px; line-height: 22.3999996185303px; text-align: start;">. © Wiki.</span><span style="text-align: left;"> </span></blockquote>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVVwcDllebaVWL_r1mTvOzlKSLNOVN96RnLVfxuBLTrZuBIbl-R2zJ9dczwP6kCa1TzqPZFeD4lMEC7of8vgw4L_BGMa8NeRcJuWoVp_OQS1-GfwPgtLXaGIxBIFyPJebu5mqVphKbXTc/s1600/logo.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVVwcDllebaVWL_r1mTvOzlKSLNOVN96RnLVfxuBLTrZuBIbl-R2zJ9dczwP6kCa1TzqPZFeD4lMEC7of8vgw4L_BGMa8NeRcJuWoVp_OQS1-GfwPgtLXaGIxBIFyPJebu5mqVphKbXTc/s1600/logo.jpg" height="228" width="400" /></a></div>
<br />
Как-то незаметно для меня прошли все эти новые веяния в виде активных мониторов, на которые пользователь может тыкать пальцами. И знать бы о них не знал, кабы месяца три назад начальство не приобрело себе ноутбук, который можно порвать на две части (экран отдельно, клавиатура отдельно), причем не Surface какой-то, пропиаренный MS, а уже самый проходной у пользователей – от <a href="http://www.asus.com/in-search-of-incredible/ru-ru/asus-transformer-book-t300" target="_blank">ASUS</a>, за гораздо меньшие деньги (относительно).<br />
Да и закуплен был данный девайс не просто так – задача выросла оттуда, откуда и не ждали.<br />
<br />
Де юре: мы проводим огромное количество семинаров в месяц, и вот именно на них, нашим лекторам начали демонстрировать несовместимость нашего ПО с этим пресловутым тачем.<br />
<br />
Де факто: на почту саппорта начали сыпаться гневные письма пользователей плана – "я два раза тапнула, а оно не тапнулось, может не так тапнула?"<br />
А начальство все это скрупулезно отслеживало на своем "порватом" ноутбуке и готовило ТЗ :)<br />
<br />
И вот настал тот день. Мне на рабочий стол был воздвигнут третий монитор, <a href="http://www.ixbt.com/monitor/lg-23et63v.shtml" target="_blank">23 дюйма от LG</a> (с поддержкой Touch ввода аж 10 пальцами) и поставлена задача – это должно заработать в течении трех дней!!!<br />
<br />
А работаю то я в XE4 – беда... ;)<br />
<br />
<a name='more'></a><br />
<h3 style="text-align: left;">
0. Анализ проблемы.</h3>
<br />
Благо я знаком с множеством компетентных товарищей (включая Embarcadero MVP), с которыми можно посоветоваться, с какой стороны вообще подойти к поддержке Touch, но... вычитав досконально линки на технические статьи (присланные ими) о поддержке мультитача, я понял, что в XE4 мне ничего не светит. Доступные мне возможности VCL сильно ограничены.<br />
<br />
Немного почитав конференции Embarcadero я узнал, что мультитач, с некоторыми ограничениями, стал доступен только в XE7 (однако).<br />
<br />
Не уверен что начальство оценило бы, если бы я сказал, что самый простой способ решения задачи выглядит в виде апдейта на ХЕ7 (плюс время затраты на проверку кода на совместимость после апа).<br />
<br />
Поэтому смотрим что мне доступно в XE4:<br />
<b>плюсы:</b><br />
- она знает о жестах (Gesture)<br />
<b>минусы:</b><br />
- она не знает о Touch (знает, но не предоставляет внешнего обработчика)<br />
- она не знает о Gesture при помощи двух точек ввода (двумя и более пальцами).<br />
<br />
А теперь посмотрим что мне не доступно:<br />
<ol style="text-align: left;">
<li>Я не могу расширить класс TRealTimeStylus введением поддержки интерфейса IRealTimeStylus3 до кучи к IStylusAsyncPlugin просто потому, что он спрятан от меня внутри TPlatformGestureEngine аж в <b>strict private type</b> секции класса.</li>
<li>Мне не предоставлен полноценный обработчик сообщения WM_TOUCH, хотя данное сообщение обрабатывается внутри TWinControl.WndProc:</li>
</ol>
<pre class="brush:delphi"> WM_TOUCH:
with FTouchManager do
if (GestureEngine <> nil) and (efTouchEvents in GestureEngine.Flags) then
GestureEngine.Notification(Message);
</pre>
<br />
Как можно увидеть по коду, управление идет сразу на движок распознавания жестов.<br />
Хотя казалось бы – а зачем мне гестуры, если я хочу двигать пять картинок по канвасу в том порядке, который гестуры явно не распознают?<br />
<br />
Конечно, во втором случае я могу сам перекрыть WM_TOUCH, но раз уж кто-то взялся за его обработку и получил данные, почему бы их не отдать наружу, избавив разработчика от повторного дубляжа кода?<br />
<br />
Поэтому зайдем с другого бока.<br />
<br />
<h3 style="text-align: left;">
1. Постановка задачи</h3>
<br />
Наше ПО - это по сути очень сильно навороченный Excel, правда, с заточкой под определенный контингент пользователей, в данном случае сметчиков. Впрочем, немного перефразирую: дистанция между возможностями нашего софта и Excel примерно аналогична разнице между MsPaint и Adobe Photoshop.<br />
Наши пользователи тоже могут реализовать в Excel некий документ в виде сметы, так же как и рисунок в MsPaint. Весь цимус в результате.<br />
<br />
Разработан проект был по иидеологииWYSIWYG, и представляет из себя в 90 процентов случаев некий кастомный класс (от TCustomControl), реализующий грид, в котором пользователь работает, так же как с обычным бумажным документом.<br />
<br />
Выглядит примерно так: (скриншот сделан во время операции DragDrop позиции, на стрелку не обращайте внимания, бо картинка выдрана из какой-то техсопроводиловки и указывает на плавающий Hint, типа фишка :)<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVTc2u46yfFTISLHjqqPiGJYJrxZORNtEkRI-8k_m4jwI5ydt09wN6oUp4qAQxFt4-nDGkuC1YuJeKMCrduNADGVgzH-cYxwYlWpb8JjrY9iOfAdkqUUGV6uaTX2dhMrLc8fGXs4GmOJU/s1600/grand.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVTc2u46yfFTISLHjqqPiGJYJrxZORNtEkRI-8k_m4jwI5ydt09wN6oUp4qAQxFt4-nDGkuC1YuJeKMCrduNADGVgzH-cYxwYlWpb8JjrY9iOfAdkqUUGV6uaTX2dhMrLc8fGXs4GmOJU/s1600/grand.png" height="276" width="640" /></a></div>
<br />
В этом контроле отсутствуют такие стандартные понятия, как скролл. Он, конечно, есть, но им эмулируется манипуляция с колонками в случае подвижки по горизонтали, либо в случае вертикального смещения – переход на следующую строку листа.<br />
Он не воспринимает стандартные сообщения о скролировании.<br />
<br />
В базовом варианте (который выдает ОС) он умеет получать события о клике мышки, эмулируемое системой через тап на тачскрине, и WM_MOUSEMOVE, так же эмулируемое системой через тач.<br />
<br />
<b>А что нужно:</b><br />
<ul style="text-align: left;">
<li>Единственное, что умеет текущий вариант Gesture – тап двумя пальцами для вызова PopupMenu по координатам тапа;</li>
<li>Скролирование влево/право/вверх/вниз посредством свайпа двумя пальцами на тачскрине;</li>
<li>Эмуляция команд "назад/вперед", посредством свайпа тремя пальцами на тачскрине.</li>
</ul>
С учетом того что Gesture в XE4 принципиально не заточены на мультитач (даже на уровне редактора гестур), а задачу решать надо, я грустил целый вечер и... с утра приступил к работе.<br />
<br />
<h3 style="text-align: left;">
2. Используемые термины</h3>
<br />
Как я говорил ранее, я не огромный специалист во всех этих новых веяниях, поэтому в статье я буду оперировать следующими определениями (вполне вероятно, что неправильными):<br />
<br />
<b>Тап </b>- аналог клика мышкой, событие, возникающее при одинарном коротком нажатии пальцем на тачскрин.<br />
<b>Тач </b>(или точка тача) - нечто описывающее ситуацию, когда палец контактирует с тачскрином (и обрабатывается сообщение WM_TOUCH).<br />
<b>Маршрут </b>- список координат, над которыми пользователь провел пальцем (перемещалась точка тача).<br />
<b>Сессия </b>- начинается, когда палец коснулся тачскрина, продолжается, когда пользователь водит по нему, не отпуская пальца, и завершается, когда палец убран. На протяжении сессии строится ее маршрут.<br />
<b>Жест </b>(Gesture)<b> </b>- некий шаблонный эталон маршрута, с которым сравнивается маршрут сессии. К примеру пользователь ткнул пальцем, потянул влево и отпустил - это жест с идентификатором sgiLeft.<br />
<br />
<h3 style="text-align: left;">
3. Разбираемся с обработкой WM_TOUCH</h3>
<br />
Для начала необходимо определиться – а поддерживает ли вообще наше железо мультитач?<br />
Для этого достаточно вызвать GetSystemMetrics с параметром SM_DIGITIZER и проверить результат на наличие двух флагов: NID_READY и NID_MULTI_INPUT.<br />
<br />
Грубо:<br />
<br />
<pre class="brush:delphi"> tData := GetSystemMetrics(SM_DIGITIZER);
if tData and NID_READY <> 0 then
if tData and NID_MULTI_INPUT <> 0 then
... все хорошо, можно работать
</pre>
<br />
К сожалению, если у вас нет устройств с поддержкой мультитача работающих на OS Windows, то дальнейшая часть статьи будет для вас просто теорией, без возможности проверки результата.<br />
<br />
НО!!! Если ваш девайс поддерживает мультитач, то можно попробовать его пощупать. Для этого выберем произвольное окно (к примеру главную форму) и скажем:<br />
<br />
<pre class="brush:delphi"> RegisterTouchWindow(Handle, 0);
</pre>
<br />
Без вызова данной функции наше выбранное окно не будет принимать сообщения WM_TOUCH.<br />
<br />
"Отлучить" окно от получения данного сообщения поможет функция UnregisterTouchWindow.<br />
<br />
Декларируем обработчик сообщения WM_TOUCH.<br />
<br />
<pre class="brush:delphi"> procedure WmTouch(var Msg: TMessage); message WM_TOUCH;
</pre>
<br />
И начинаем разбираться – что он нам вообще дает.<br />
<br />
Итак, параметр WParam данного сообщения содержит количество активных точек тача, о котором нам хочет сообщить система. Причем это число хранится только в нижних двух байтах, что намекает о возможности поддержки системой до 65535 точек ввода.<br />
<br />
Я пытался такое прикинуть – не получилось, бо у меня монитор держит максимум 10 пальцев. Хотя, в этом и есть цимус, если оглядываться на современные фантастические фильмы, где показаны некие виртуальные столы с данными, с которыми работает куча людей, имеющих возможность туда тыкать всеми десятью каждый (ну, к примеру, "Аватар" тот-же, или "Обливион").<br />
<br />
Молодцы, заложились на перспективу, хотя, как оказалось – это уже давно работает и без фильмов, просто я не всегда слежу за новинками. К примеру, вот такой 46 дюймовый девайс был представлен на выставке "Consumer Electronics Show 2011":<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3LvRYkBD6pKIJnRzM7MCFBVEovvZVwKRtU5mpwKmxGGo6Ecx48yMO1tXJl_hrGlJ9ImlYIR-JVLjyzp24ajeg1nI10eGxfwqQp4L37DyWWSzUFrd77TUExzeBhAQGZ_0q8ZktuIwVCMI/s1600/demo.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3LvRYkBD6pKIJnRzM7MCFBVEovvZVwKRtU5mpwKmxGGo6Ecx48yMO1tXJl_hrGlJ9ImlYIR-JVLjyzp24ajeg1nI10eGxfwqQp4L37DyWWSzUFrd77TUExzeBhAQGZ_0q8ZktuIwVCMI/s1600/demo.jpg" height="265" width="400" /></a></div>
<br />
Впрочем, не будем отвлекаться:<br />
А вот LParam данного сообщения является неким хэндлом, через который можно получить более подробную информацию о сообщении посредством вызова функции GetTouchInputInfo.<br />
Если после вызова GetTouchInputInfo повторный вызов данной функции не требуется, то MSDN рекомендует сказать CloseTouchInputHandle, но это не обязательно, т.к. очистка данных в куче все равно произойдет автоматом при передаче управления на DefWindowProc или при попытке отправки данных через SendMessage/PostMessage.<br />
Более <a href="http://msdn.microsoft.com/ru-ru/library/windows/desktop/dd317341(v=vs.85).aspx" target="_blank">подробнее тут</a>.<br />
<br />
Что от нас требует функция GetTouchInputInfo:<br />
<ol style="text-align: left;">
<li>Ей необходим сам хэндл, с которым она будет работать;</li>
<li>Ей необходим выделенный буфер ввиде массива из элементов TTouchInput, в котором она разместит всю информацию о событии;</li>
<li>Размер этого массива;</li>
<li>Размер каждого элемента массива.</li>
</ol>
Опять молодцы: при помощи четвертого пункта, сразу заложились на возможность изменения структуры TTouchInput в следующих версиях ОС (даже интересно, что туда еще можно добавить? :).<br />
<br />
Если сильно грубо, то ее вызов выглядит так:<br />
<br />
<pre class="brush:delphi">var
Count: Integer;
Inputs: array of TTouchInput;
begin
Count := Msg.WParam and $FFFF;
SetLength(Inputs, Count);
if GetTouchInputInfo(Msg.LParam, Count, @Inputs[0], SizeOf(TTouchInput)) then
// ... что-то делаем с полученной информацией
CloseTouchInputHandle(Msg.LParam);
</pre>
<br />
Это все. А теперь попробуем разобраться с данными, которые хранятся в массиве Inputs.<br />
<br />
<h3 style="text-align: left;">
4. Обрабатываем TTouchInput</h3>
<br />
С этого самого момента начинается самое интересное.<br />
<br />
Размер массива TTouchInput зависит от того, сколько пальцев приложено к тачскрину.<br />
Для каждой точки тача (пальца) система генерирует уникальный ID, который не изменяется в течении всей сессии (от момента касания пальцем, до... пока мы его не убрали).<br />
Этот ID отображен на каждый элемент TTouchInput массива и хранится в параметре dwID.<br />
<br />
Кстати о сессиях:<br />
Сессия, это... Ну давайте вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg58EN8uXDYCAmy28wxLH8ovihGGbUINXoINQfvxOWPgs8tCHqpHXkviPJDX-cPyVNC57ODBVyhdu7fFhtwqNLFqBrs4jkjpPZ8Il2SMBR13IEaKJJ0oJkabnsLqiRdR_U3hczql_uafXk/s1600/session.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg58EN8uXDYCAmy28wxLH8ovihGGbUINXoINQfvxOWPgs8tCHqpHXkviPJDX-cPyVNC57ODBVyhdu7fFhtwqNLFqBrs4jkjpPZ8Il2SMBR13IEaKJJ0oJkabnsLqiRdR_U3hczql_uafXk/s1600/session.png" height="227" width="320" /></a></div>
<br />
На картинке отображено ровно 10 сессий (под каждый палец), показан их маршрут (массив точек над которыми перемещался палец в рамках каждой сессии), причем, каждая из сессий еще не завершена (пальцы все еще приложены к тачскрину).<br />
<br />
Впрочем, вернемся обратно к структуре TTouchInput.<br />
По сути, для нормальной работы с тачем от данной структуры нам требуется всего лишь несколько параметров:<br />
<br />
<pre class="brush:delphi"> TOUCHINPUT = record
x: Integer; // абсолютные координаты
y: Integer; // точки тача
hSource: THandle; // хэндл окна, обрабатывающего сообщение
dwID: DWORD; // уникальный идентификатор точки
dwFlags: DWORD; // текущее состояние точки
// все остальное в принципе не нужно
dwMask: DWORD;
dwTime: DWORD;
dwExtraInfo: ULONG_PTR;
cxContact: DWORD;
cyContact: DWORD;
end;
</pre>
<br />
Давайте сразу начнем с реализации демо-приложения.<br />
Создайте новый проект и на главной форме разместите TMemo, в которое быдет выводится лог работы с тачем.<br />
<br />
В конструкторе формы подключаем ее к обработке сообщения WM_TOUCH:<br />
<br />
<pre class="brush:delphi">procedure TdlgSimpleTouchDemo.FormCreate(Sender: TObject);
begin
RegisterTouchWindow(Handle, 0);
end;
</pre>
<br />
Теперь пишем обработчик события:<br />
<br />
<pre class="brush:delphi">procedure TdlgSimpleTouchDemo.WmTouch(var Msg: TMessage);
function FlagToStr(Value: DWORD): string;
begin
Result := '';
if Value and TOUCHEVENTF_MOVE <> 0 then
Result := Result + 'move ';
if Value and TOUCHEVENTF_DOWN <> 0 then
Result := Result + 'down ';
if Value and TOUCHEVENTF_UP <> 0 then
Result := Result + 'up ';
if Value and TOUCHEVENTF_INRANGE <> 0 then
Result := Result + 'ingange ';
if Value and TOUCHEVENTF_PRIMARY <> 0 then
Result := Result + 'primary ';
if Value and TOUCHEVENTF_NOCOALESCE <> 0 then
Result := Result + 'nocoalesce ';
if Value and TOUCHEVENTF_PEN <> 0 then
Result := Result + 'pen ';
if Value and TOUCHEVENTF_PALM <> 0 then
Result := Result + 'palm ';
Result := Trim(Result);
end;
var
InputsCount, I: Integer;
Inputs: array of TTouchInput;
begin
// получаем количество точек тача
InputsCount := Msg.WParam and $FFFF;
// подготавливаем соответствующий массив данных
SetLength(Inputs, InputsCount);
// получаем информацию по текущему событию
if GetTouchInputInfo(Msg.LParam, InputsCount, @Inputs[0], SizeOf(TTouchInput)) then
begin
// закрываем хэндл (а можно и не закрывать)
CloseTouchInputHandle(Msg.LParam);
// выводим информацию на экран
for I := 0 to InputsCount - 1 do
Memo1.Lines.Add(Format('TouchInput №: %d, ID: %d, flags: %s',
[I, Inputs[I].dwID, FlagToStr(Inputs[I].dwFlags)]));
end;
end;
</pre>
<br />
Это все.<br />
<br />
Согласитесь – просто до невозможности. Все данные перед глазами.<br />
Попробуйте поэкспериментировать с этим кодом с использованием тачскрина и вы заметите, что разработчику, помимо привязки к ID каждого тача, передается еще определенный набор флагов, которые выводятся в лог.<br />
По данным лога сразу можно определить начало сессии тача (флаг TOUCHEVENTF_DOWN), перемещение каждого из пальцев по тачскрину (флаг TOUCHEVENTF_MOVE) и завершение сессии (флаг TOUCHEVENTF_UP).<br />
<br />
Выглядит вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='640' height='480' src='https://www.youtube.com/embed/SR0pWZBPks0?feature=player_embedded' frameborder='0'></iframe></div>
<br />
Сразу оговорюсь об одной неприятности: не всегда в обработчик WM_TOUCH будут приходить сообщения от тачскрина с флагами TOUCHEVENTF_DOWN или TOUCHEVENTF_UP. Этот нюанс нужно учитывать при реализации своих "классов-оберток", о которых пойдет речь чуть ниже.<br />
<br />
<b>К примеру:</b><br />
Наше приложение в данный момент отображает PopupMenu – нажатие на тачскрин приведет к его закрытию, но сообщение WM_TOUCH с флагом TOUCHEVENTF_DOWN к нам не придет, хотя последующие, с флагом TOUCHEVENTF_MOVE, мы получим достаточно успешно.<br />
То же относится и к показу PopupMenu в обработчике события TOUCHEVENTF_MOVE.<br />
В данном случае произойдет срыв сессии и сообщения WM_TOUCH с флагом TOUCHEVENTF_UP ждать не стоит.<br />
<br />
Такое поведение наблюдается под Windows 7 (32/64 бита), я даже допускаю – под Windows 8 и выше что-то поменялось, но у меня просто нет возможности проверить это сейчас (лень – второе я :).<br />
<br />
Впрочем, получив представление о том "как это работает", попробуем написать нечто более интересное.<br />
<br />
Исходный код примера в папке "<a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">.\demos\simple\</a>" в архиве с исходниками.<br />
<br />
<h3 style="text-align: left;">
5. Применяем мультач на практике.</h3>
<br />
Мой монитор держит 10 пальцев одновременно, можно даже написать приложение, эмулирующее рояль (правда в рояле есть еще педали и чувствительность к силе нажатия), но зачем идти сразу от сложного?<br />
Самое простое, что пришло мне в голову – это 10 квадратов на канвасе формы, которые я могу двигать во все стороны посредством тача.<br />
Этого вполне достаточно, чтобы "пощупать" мультитач в самом прямом смысле :)<br />
<br />
Создаем новый проект.<br />
<br />
Каждый из квадратов будет описываться в виде такой структуры:<br />
<br />
<pre class="brush:delphi">type
TData = record
Color: TColor;
ARect, StartRect: TRect;
StartPoint: TPoint;
Touched: Boolean;
TouchID: Integer;
end;
</pre>
<br />
По сути, самым важным полем данной структуры является TouchID, все остальное второстепенно.<br />
<br />
Нам нужно где-то хранить данные по каждому квадрату, поэтому объявим их в виде такого массива:<br />
<br />
<pre class="brush:delphi"> FData: array [0..9] of TData;
</pre>
<br />
Ну, и выполним инициализацию:<br />
<br />
<pre class="brush:delphi">procedure TdlgMultiTouchDemo.FormCreate(Sender: TObject);
var
I: Integer;
begin
DoubleBuffered := True;
RegisterTouchWindow(Handle, 0);
Randomize;
for I := 0 to 9 do
begin
FData[I].Color := Random($FFFFFF);
FData[I].ARect.Left := Random(ClientWidth - 100);
FData[I].ARect.Top := Random(ClientHeight - 100);
FData[I].ARect.Right := FData[I].ARect.Left + 100;
FData[I].ARect.Bottom := FData[I].ARect.Top + 100;
end;
end;
</pre>
<br />
А так же их отрисовку на канвасе формы (пока что не анализируйте обработчик FormPaint, мы дойдем до него чуть ниже):<br />
<br />
<pre class="brush:delphi">procedure TdlgMultiTouchDemo.FormPaint(Sender: TObject);
var
I: Integer;
begin
Canvas.Brush.Color := Color;
Canvas.FillRect(ClientRect);
for I := 0 to 9 do
begin
Canvas.Pen.Color := FData[I].Color xor $FFFFFF;
if FData[I].Touched then
Canvas.Pen.Width := 4
else
Canvas.Pen.Width := 1;
Canvas.Brush.Color := FData[I].Color;
Canvas.Rectangle(FData[I].ARect);
end;
end;
</pre>
<br />
Запустите, получится как-то так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEieI0QZqg5L3ETpqXqCdj6jGmp-p04zLjHOUXSIW_iXV6jpp7DAkxRYoN85ygySLMSkFfTADX4_SJwdo6ezVQaQpc5Lp5ZZ56VxvhxmaJ1dPcQsGzCzG0pTWKCX60VmIjXe4gTNX7kOStg/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEieI0QZqg5L3ETpqXqCdj6jGmp-p04zLjHOUXSIW_iXV6jpp7DAkxRYoN85ygySLMSkFfTADX4_SJwdo6ezVQaQpc5Lp5ZZ56VxvhxmaJ1dPcQsGzCzG0pTWKCX60VmIjXe4gTNX7kOStg/s1600/1.png" height="587" width="640" /></a></div>
<br />
Обвес готов, теперь попробуем изменить картинку через обработку WM_TOUCH.<br />
<br />
Все что нам нужно в обработчике, это получить индекс квадрата, над которым пользователь нажал пальцем. Но для начала переведем координаты от каждой точки тача в координаты окна:<br />
<br />
<pre class="brush:delphi"> pt.X := TOUCH_COORD_TO_PIXEL(Inputs[I].x);
pt.Y := TOUCH_COORD_TO_PIXEL(Inputs[I].y);
pt := ScreenToClient(pt);
</pre>
<br />
Имея на руках валидные координаты, мы можем узнать индекс квадрата в массиве, посредством вызова PtInRect.<br />
<br />
<pre class="brush:delphi"> function GetIndexAtPoint(pt: TPoint): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to 9 do
if PtInRect(FData[I].ARect, pt) then
begin
Result := I;
Break;
end;
end;
</pre>
<br />
Когда пользователь только коснулся пальцем тачскрина (учитывая что каждая точка, обладает своим уникальным ID), мы присвоим найденному квадрату данный ID. Пригодится в дальнейшем:<br />
<br />
<pre class="brush:delphi"> if Inputs[I].dwFlags and TOUCHEVENTF_DOWN <> 0 then
begin
Index := GetIndexAtPoint(pt);
if Index < 0 then Continue;
FData[Index].Touched := True;
FData[Index].TouchID := Inputs[I].dwID;
FData[Index].StartRect := FData[Index].ARect;
FData[Index].StartPoint := pt;
Continue;
end;
</pre>
<br />
Это, скажем так, инициализация объекта и начало сессии тача.<br />
<br />
Следущее сообщение, которое мы получим, скорее всего будет WM_TOUCH с флагом TOUCHEVENTF_MOVE.<br />
<br />
<b>Тут нюанс:</b><br />
В первом случае мы искали квадраты по их координатам, а сейчас это будет ошибкой, хотя бы потому, что позиции квадратов на форме могут пересекаться.<br />
Поэтому, в случае MOVE, мы будем искать квадраты по ID тача, который был выставлен через параметр TouchID:<br />
<br />
<pre class="brush:delphi"> function GetIndexFromID(ID: Integer): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to 9 do
if FData[I].TouchID = ID then
begin
Result := I;
Break;
end;
end;
</pre>
<br />
Найдя необходимый нам квадрат, делаем подвижку, ориентируясь на структуру заданную в начале тач сессии:<br />
<br />
<pre class="brush:delphi"> R := FData[Index].StartRect;
OffsetRect(R,
pt.X - FData[Index].StartPoint.X,
pt.Y - FData[Index].StartPoint.Y);
FData[Index].ARect := R;
</pre>
<br />
Ну, и концовка в виде обработки флага TOUCHEVENTF_UP:<br />
<br />
<pre class="brush:delphi"> if Inputs[I].dwFlags and TOUCHEVENTF_UP <> 0 then
begin
FData[Index].Touched := False;
FData[Index].TouchID := -1;
Continue;
end;
</pre>
<div>
<br /></div>
<div>
В которой мы отключаем квадрат от тач сессии и перерисовываем сам канвас.</div>
<div>
<br /></div>
<div>
Крайне простой примерчик, который, однако, работает и денег не просит :)</div>
<div>
Запускайте и тестируйте – получается достаточно забавно:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<object class="BLOGGER-youtube-video" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0" data-thumbnail-src="https://i.ytimg.com/vi/1_BF8AaZkFY/0.jpg" height="480" width="640"><param name="movie" value="https://www.youtube.com/v/1_BF8AaZkFY?version=3&f=user_uploads&c=google-webdrive-0&app=youtube_gdata" /><param name="bgcolor" value="#FFFFFF" /><param name="allowFullScreen" value="true" /><embed width="640" height="480" src="https://www.youtube.com/v/1_BF8AaZkFY?version=3&f=user_uploads&c=google-webdrive-0&app=youtube_gdata" type="application/x-shockwave-flash" allowfullscreen="true"></embed></object></div>
<br />
Просто для "красявости", параметр Touched структуры TData, используется внутри FormPaint и отвечает за присутствие "жирной" рамки вокруг перемещаемого квадрата.</div>
<div>
<br /></div>
<div>
Исходный код примера в папке "<a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">.\demos\multutouch\</a>" в архиве с исходниками.</div>
<div>
<br /></div>
<h3 style="text-align: left;">
6. Разбираемся с Gesture (жестами)</h3>
<div>
<br /></div>
<div>
Мультитач – это только первый шажок, ибо нам хотелось бы работать с мультач-жестами, но...<br />
Давайте, для начала, посмотрим как реализовано в VCL распознавание жеста на основе одной Touch сессии (одним пальцем).<br />
<br />
За это отвечает класс TGestureEngine от которого потребуется, в принципе, только код функции IsGesture().<br />
<br /></div>
<div>
Рассмотрим ее поподробнее:</div>
<div>
<br /></div>
<div>
Она разделена ровно на две части, где первая часть проверяет стандартные жесты в цикле:</div>
<div>
<br />
<pre class="brush:delphi"> // Process standard gestures
if gtStandard in GestureTypes then
</pre>
<br /></div>
<div>
А вторая – некие кастомные жесты, переданные пользователем:<br />
<br />
<pre class="brush:delphi"> // Process custom gestures
if CustomGestureTypes * GestureTypes = CustomGestureTypes then
</pre>
<div>
<br />
Так как кастомные пользовательские жесты нам по определению не нужны, рассмотрим только первую часть функции.</div>
<div>
Основная ее идея выглядит в виде поиска описателя жеста через вызов FindStandardGesture и сравнением его с переданным маршрутом посредством Recognizer.Match.</div>
<div>
<br /></div>
<div>
Все остальные параметры, приходящие в IsGesture, по сути, можно исключить – они являются обвесом функции.</div>
<div>
<br /></div>
<div>
Фишка в том, что Recognizer – это не интерфейс IGestureRecognizer, а VCL обертка.<br />
Вот она-то нам и нужна.<br />
<br />
Но прежде чем перейти к написанию демопримера, нужно разобраться с тем, что из себя представляет сам жест (Gerture):<br />
<br />
Это структура вида:<br />
<br />
<pre class="brush:delphi"> TStandardGestureData = record
Points: TGesturePointArray;
GestureID: TGestureID;
Options: TGestureOptions;
Deviation: Integer;
ErrorMargin: Integer;
end;
</pre>
<br />
<b>Points </b>– это маршрут жеста, с которым сравнивается аналогичный маршрут из touch сессии пользователя.<br />
<b>GestureID </b>– уникальный идентификатор жеста.<br />
В ХЕ4 они перечислены в модуле Vcl.Controls:<br />
<br />
<pre class="brush:delphi">const
// Standard gesture id's
sgiNoGesture = 0;
sgiLeft = 1;
sgiRight = 2;
...
</pre>
<br />
<b>Options </b>- в данном случае они нам не интересны.<br />
<br />
<b>Deviation </b>и <b>ErrorMargin </b>– параметры, указывающие величину, скажем так: "тремора" пальца в процессе жеста. Вряд ли вы сможете провести идеально ровную линию по оси Х влево без изменения позиции по оси Y, поэтому Deviation и ErrorMargin указывают на границы, в рамках которых перемещения точки будут валидны.<br />
<br />
Декларации параметров стандартных жестов можно найти в модуле Vcl.Touch.Gestures:<br />
<br />
<pre class="brush:delphi">{ Standard gesture definitions }
const
PDefaultLeft: array[0..1] of TPoint = ((X:200; Y:0), (X:0; Y:0));
CDefaultLeft: TStandardGestureData = (
GestureID: sgiLeft;
Options: [goUniDirectional];
Deviation: 30;
ErrorMargin: 20);
PDefaultRight: array[0..1] of TPoint = ((X:0; Y:0), (X:200; Y:0));
CDefaultRight: TStandardGestureData = (
GestureID: sgiRight;
Options: [goUniDirectional];
Deviation: 30;
ErrorMargin: 20);
PDefaultUp: array[0..1] of TPoint = ((X:0; Y:200), (X:0; Y:0));
CDefaultUp: TStandardGestureData = (
GestureID: sgiUp;
Options: [goUniDirectional];
Deviation: 30;
ErrorMargin: 20);
...
</pre>
<br />
Таким образом, зная о формате жестов, мы можем самостоятельно в рантайме подготовить собственный вариант жеста, заполнив его маршрут (Points) и выставив уникальный ID.<br />
Впрочем, сейчас нам это не понадобится. Посмотрим, что можно сделать на основе стандартных жестов.<br />
<br />
Пишем самый простой пример, при помощи которого Recognizer возвратит нам ID опознанного им жеста, в котором мы построим 4 массива точек, которые технически будут похожи на те маршруты, которые пользователь будет вводить посредством тачскрина:<br />
<br />
<pre class="brush:delphi">program recognizer_demo;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
Vcl.Controls,
SysUtils,
TypInfo,
Vcl.Touch.Gestures;
type
TPointArray = array of TPoint;
function GetGestureID(Value: TPointArray): Byte;
var
Recognizer: TGestureRecognizer;
GestureID: Integer;
Data: TStandardGestureData;
Weight, TempWeight: Single;
begin
Weight := 0;
Result := sgiNone;
Recognizer := TGestureRecognizer.Create;
try
for GestureID := sgiLeft to sgiDown do
begin
FindStandardGesture(GestureID, Data);
TempWeight := Recognizer.Match(Value, Data.Points, Data.Options,
GestureID, Data.Deviation, Data.ErrorMargin);
if TempWeight > Weight then
begin
Weight := TempWeight;
Result := GestureID;
end;
end;
finally
Recognizer.Free;
end;
end;
const
gesture_id: array [sgiNone..sgiDown] of string =
(
'sgiNone',
'sgiLeft',
'sgiRight',
'sgiUp',
'sgiDown'
);
var
I: Integer;
Data: TPointArray;
begin
SetLength(Data, 11);
// якобы делаем жест вправо
for I := 0 to 10 do
begin
Data[I].X := I * 10;
Data[I].Y := 0;
end;
Writeln(gesture_id[GetGestureID(Data)]);
// якобы делаем жест влево
for I := 0 to 10 do
begin
Data[I].X := 500 - I * 10;
Data[I].Y := 0;
end;
Writeln(gesture_id[GetGestureID(Data)]);
// якобы делаем жест вверх
for I := 0 to 10 do
begin
Data[I].X := 0;
Data[I].Y := 500 - I * 10;
end;
Writeln(gesture_id[GetGestureID(Data)]);
// якобы делаем жест вниз
for I := 0 to 10 do
begin
Data[I].X := 0;
Data[I].Y := I * 10;
end;
Writeln(gesture_id[GetGestureID(Data)]);
Readln;
end.
</pre>
<div>
<br />
После запуска должно выглядеть вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgh4zwVz3fMxPoyQSwUGte4D6HrH3M5fqyNAvTgzSwYm-LBnL4jUgHIDMonrwQPYbN-yqsNt0GCEIdcycL5l9Tj_hUFF3GdwzI7c8BFZb2frQuRZDPKocheSSK5ImcfECPqncOaUd4wvzk/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgh4zwVz3fMxPoyQSwUGte4D6HrH3M5fqyNAvTgzSwYm-LBnL4jUgHIDMonrwQPYbN-yqsNt0GCEIdcycL5l9Tj_hUFF3GdwzI7c8BFZb2frQuRZDPKocheSSK5ImcfECPqncOaUd4wvzk/s1600/2.png" /></a></div>
<br /></div>
Что и предполагалось.<br />
Исходный код примера в папке "<a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">.\demos\recognizer\</a>" в архиве с исходниками.<br />
<br />
А теперь...<br />
<br />
<h3 style="text-align: left;">
7. Распознаем мультитач жесты (Gestures).</h3>
<br />
Данная глава описывает основную идею данной статьи, скажем так – фишку, ради которой и появился весь это текст.<br />
Сейчас – никаких технических деталей, только сам подход:<br />
<br />
<b>Итак, что нам сейчас доступно:</b><br />
<ol style="text-align: left;">
<li>Мы знаем как снимать данные с каждой тач-сессии;</li>
<li>Мы можем распознавать жест каждой тач-сессии.</li>
</ol>
<b>К примеру:</b><br />
<ol style="text-align: left;">
<li>Пользователь нажал пальцем на тачскрин и провел влево;</li>
<li>Мы зафиксировали начало сессии в обработчике ON_TOUCH + TOUCHEVENTF_DOWN, записали все точки маршрута по приходу TOUCHEVENTF_MOVE и в тот момент, когда нам пришел TOUCHEVENTF_UP, передали ранее записанный массив точек функции GetGestureID;</li>
<li>Вывели результат.</li>
</ol>
Но представьте, что пользователь сделал все то же самое, только двумя пальцами одновременно:<br />
<ol style="text-align: left;">
<li>Для каждого пальца мы стартуем собственную сессию;</li>
<li>Пишем ее маршрут;</li>
<li>По завершении каждой сессии передаем ее на распознание жеста.</li>
</ol>
Если ID жестов от двух сессий, произведенных над одним и тем-же окном, совпадут (к примеру, это будет sgiLeft), то мы можем сделать вывод – произошел свайп влево двумя пальцами.<br />
<br />
А что если все точки маршрута сессии содержат одни и те же координаты?<br />
Тогда жеста не было и произошел так называемый тап (одним или многими пальцами).<br />
Причем под данное условие попадет также жест "Press And Tap", при помощи которого обычно отображают PopupMenu.<br />
<br />
Таким образом, с учетом основной постановки задачи, мы можем контролировать все требуемые нам варианты жестов одним, двумя и тремя пальцами (впрочем, хоть всеми десятью).<br />
<br />
А что делать, если жесты от двух сессий не совпали?<br />
Анализировать их, и хотя в текущую постановку задачи это не входит, можно с уверенностью сказать, что жест sgiLeft от первой сессии плюс жест sgiRight от второй, может трактоваться как Zoom. Даже Rotate вполне возможно задетектировать на основе жестов sgiSemiCircleLeft или sgiSemiCircleRight только на основе двух тач сессий.<br />
<br />
Прониклись? :)<br />
<br />
Вот дефолтовый список жестов, которые таким образом легко можно эмулировать:<br />
<a href="http://msdn.microsoft.com/en-us/library/windows/desktop/dd940543(v=vs.85).aspx" target="_blank">Windows Touch Gestures Overview</a><br />
<br />
К сожалению, почему-то все это не реализовано в ХЕ4 и стало доступно только начиная с седьмой версии (и то не уверен что полностью).<br />
<br />
<h3 style="text-align: left;">
8. Техническое планирование движка</h3>
<br />
С теоретической частью закончили, теперь пришла пора все это применить на практике и сразу рассмотреть несколько проблем, встающих перед разработчиком.<br />
<br />
<b>Проблема номер раз:</b><br />
В приложении обычно сотни окон – большинству из них достаточно того, что система генерирует при таче сообщения плана WM_LBUTTONCLICK и прочие, которых для нормального поведения окна достаточно (к примеру для кнопок, эдитов, скролов), но вот для того же SysListView32 скролирование, посредством жеста двумя пальцами, не происходит, ввиду отсутствия генерации сообщения WM_SCROLL. А ведь есть еще и кастомные контролы.<br />
Расширять оконную процедуру каждого окна – слишком много работы, поэтому нужно как-то определиться – какие окна должны поддерживать мультитач, причем сделать это необходимо наиболее универсально.<br />
Отсюда следует: нужен некий менеджер мультитача, в котором окна будут регистрироваться и который будет отвечать за всю работу с мультитачем.<br />
<br />
<b>Проблема номер два:</b><br />
Раз мы пишем нечно универсальное, не переписывая каждый экземпляр TWinControl, то необходимо как-то отслеживать пересоздание окна, благо вызовы RecreateWnd один из штатных механизмом VCL. Если мы не будем этого делать, то при первом же пересоздании окна, ранее зарегистрированный нами TWinControl, перестанет получать сообщения WM_TOUCH и, таким образом, вся работа нашего менеджера будет нивелирована.<br />
<br />
<b>Проблема номер три:</b><br />
Менеджер должен хранить все данные о тач-сессиях и уметь обрабатывать ситуации срыва начала и конца сессий (ибо не всегда приходят уведомления c флагами Down и Up), причем необходимо учитывать, что длина сессии может быть продолжительна по времени, что влечет за собой достаточно большой расход памяти, если сохранять все точки маршрута сессии.<br />
<br />
Еще хотелось бы чтобы менеджер мультитача мог различать жесты в рамках разных окон.<br />
К примеру – если пользователь поставил два пальца в левое окно и два пальца в правое (четыре мультитач сессии), после чего соединил пальцы в центре, левому окну должно прийти уведомление о двупальцевом жесте вправо, а правому о двупальцевом жесте влево.<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4e5YpBSyE7x6c6LpeMi741zE0_mCJ82F30qgpRioYYj7oDglnjOUGrdLdp73sAGLlr57cYOV0F1zEMU03TZGwtuPvJ8B275hdZPAHqP49SLPH8dZUA-Ex_NeFMHSkJhIBd32eBx87PNk/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4e5YpBSyE7x6c6LpeMi741zE0_mCJ82F30qgpRioYYj7oDglnjOUGrdLdp73sAGLlr57cYOV0F1zEMU03TZGwtuPvJ8B275hdZPAHqP49SLPH8dZUA-Ex_NeFMHSkJhIBd32eBx87PNk/s1600/3.png" /></a></div>
Но, к сожалению, это не получится, т.к. сообщение WM_TOUCH будет приходить только тому окну, в котором началась сессия, остальные окна будут игнорироваться.<br />
<br />
<h3 style="text-align: left;">
9. Строим базовый каркас мультитач движка</h3>
<br />
Для начала определимся с нюансами реализации класса.<br />
Технически, самое удобное, с точки зрения внешнего программиста, будет реализация некоего универсального движка, который возьмет на себя всю работу и будет уведомлять разработчика разве что вызовом финальных событий.<br />
<br />
В таком случае, разработчику придется только единожды зарегистрировать нужное окно в движке и анализировать поступающие от него жесты (направленные конкретному окну), обрабатывая нужные. К примеру, эмулируя тот же скролл двупальцевым жестом.<br />
<br />
Сам движок будет реализован ввиде синглтона.<br />
<b>Во первых</b>: нет смысла плодить инстансы класса, которые всегда будут делать одно и тоже. Это не TStringList, заточенный под хранение данных, а все таки движок, реализующий единую логику работы для всех окон проекта.<br />
<b>А во вторых</b>: есть небольшой нюанс в реализации самого движка (о нем чуть позже), из-за которого реализация в виде синглтона будет самой простой, иначе придется кардинально переусложнять логику работы класса.<br />
<br />
Таким образом, движок должен предоставлять:<br />
<ol style="text-align: left;">
<li>Методы регистрации окна и снятия окна с регистрации:</li>
<li>Набор внешних событий, обработчики которых должен реализовать разработчик.</li>
</ol>
Внешние события могут быть примерно такими:<br />
<br />
<b>OnBeginTouch </b>- это событие будет вызываться при получении сообщения WM_TOUCH.<br />
<br />
Поясню: в четвертой главе был приведен следующий код:<br />
<br />
<pre class="brush:delphi"> // получаем количество точек тача
InputsCount := Msg.WParam and $FFFF;
</pre>
<br />
Т.е. реальных точек тача может быть несколько.<br />
Вот об их количестве мы и предупредим разработчика.<br />
<br />
<b>OnTouch </b>- в этом событии мы уведомим разработчика о данных, содержащихся в каждой структуре TTouchInput, только немного в более причесанном виде. (Переведем данные о точке в координаты окна, выставим правильные флаги и прочее, зачем нагружать разработчика избыточной информацией и заставлять его писать избыточный код?)<br />
<br />
<b>OnEndTouch </b>- этим мы скажем что цикл обработки сообщения WM_TOUCH завершен, можно, к примеру, вызвать Repaint.<br />
<br />
<b>OnGecture </b>- а это сообщение разработчик получит тогда, когда движок примет решение что жест распознан.<br />
<br />
Так как класс реализован в виде синглтона, а зарегистрированных в нем окон будет более чем одно, то объявить все четыре события в виде свойств класса не получится.<br />
<br />
Ну точнее как: можно конечно, но второе зарегистрированное окно сразу переназначит обработчики событий на себя и первому придется тихо курить в сторонке.<br />
Поэтому, помимо списка зарегистрированных окон, мы должны держать и обработчики событий движка, которые за ними закреплены.<br />
<br />
Впрочем, попробуем теперь это все реализовать на практике.<br />
<br />
Создайте новый проект и к нему добавьте новый модуль, с названием... ну, к примеру, SimpleMultiTouchEngine.<br />
<br />
Для начала объявим флаги, которые нам интересны при обработке WM_TOUCH:<br />
<br />
<pre class="brush:delphi">type
TTouchFlag =
(
tfMove, // перемещаем точку
tfDown, // создали току тача
tfUp // прекратили работу с точкой
);
TTouchFlags = set of TTouchFlag;
</pre>
<br />
Опишем структуру, которую мы будем передавать на внешку разработчику о каждой точке:<br />
<br />
<pre class="brush:delphi"> TTouchData = record
Index: Integer; // порядковый номер точки в массиве TTouchInput
ID: DWORD; // неизменяющееся ID точки
Position: TPoint; // её координаты относительно окна
Flags: TTouchFlags; // флаги
end;
</pre>
<div>
<br /></div>
<div>
Декларация события OnTouchBegin будет выглядеть так:<br />
<br />
<pre class="brush:delphi"> TTouchBeginEvent = procedure(Sender: TObject; nCount: Integer) of object;
</pre>
<br />
А так будет выглядеть OnTouch:<br />
<br />
<pre class="brush:delphi"> TTouchEvent = procedure(Sender: TObject; Control: TWinControl;
TouchData: TTouchData) of object;
</pre>
<br />
Для OnEndTouch будет достаточно обычного TNotifyEvent.<br />
<br />
Данные о назначенных обработчиках событий, закрепленных за каждым зарегистрированным окном, будут хранится в такой структуре:<br />
<br />
<pre class="brush:delphi"> TTouchHandlers = record
BeginTouch: TTouchBeginEvent;
Touch: TTouchEvent;
EndTouch: TNotifyEvent;
end;
</pre>
<br />
Декларируем новый класс:<br />
<br />
<pre class="brush:delphi"> TSimleMultiTouchEngine = class
private const
MaxFingerCount = 10;
private type
TWindowData = record
Control: TWinControl;
Handlers: TTouchHandlers;
end;
private
FWindows: TList<twindowdata>;
FMultiTouchPresent: Boolean;
protected
procedure DoBeginTouch(Value: TTouchBeginEvent; nCount: Integer); virtual;
procedure DoTouch(Control: TWinControl; Value: TTouchEvent;
TouchData: TTouchData); virtual;
procedure DoEndTouch(Value: TNotifyEvent); virtual;
protected
procedure HandleTouch(Index: Integer; Msg: PMsg);
procedure HandleMessage(Msg: PMsg);
public
constructor Create;
destructor Destroy; override;
procedure RegisterWindow(Value: TWinControl; Handlers: TTouchHandlers);
procedure UnRegisterWindow(Value: TWinControl);
end;
</twindowdata></pre>
<br />
По порядку:<br />
<br />
Константа MaxFingerCount содержит максимальное количество точек тача, с которыми может работать наш класс.<br />
<br />
Структура TWindowData – содержит в себе зарегистрированное окно и список обработчиков, которые назначил программист.<br />
<br />
Поле FWindows: TList<TWindowData> – список зарегистрированных окон и обработчиков, от которого мы и будем плясать на всем протяжении работы с класом.<br />
<br />
Поле FMultiTouchPresent – флаг, инициализирующийся в конструкторе класса.<br />
Содержит True, если наше железо держит мультитач. Опираясь на данный флаг будет отключаться часть логики класса (зачем делать лишние телодвижения тогда, когда мы их выполнить все равно не сможем?).<br />
<br />
Первая protected секция - просто для удобства вынесены все вызовы внешних событий.<br />
<br />
Процедура HandleTouch - основное ядро движка, именно она и отвечает за обработку сообщения WM_TOUCH.<br />
<br />
Процедура HandleMessage - вспомогательная. Ее задача определить к какому из зарегистрированных окон направлено сообщение и вызвать HandleTouch, передав индекс найденного окна.<br />
<br />
Паблик секция – конструктор, деструктор, регистрация окна и снятие его с регистрации.<br />
<br />
Прежде чем приступить к реализации класса, сразу напишем синглтон обвес:<br />
<br />
<pre class="brush:delphi"> function MultiTouchEngine: TSimleMultiTouchEngine;
implementation
var
_MultiTouchEngine: TSimleMultiTouchEngine = nil;
function MultiTouchEngine: TSimleMultiTouchEngine;
begin
if _MultiTouchEngine = nil then
_MultiTouchEngine := TSimleMultiTouchEngine.Create;
Result := _MultiTouchEngine;
end;
...
initialization
finalization
_MultiTouchEngine.Free;
end.
</pre>
<br />
И, в завершении всего, каллбэк ловушки, при помощи которой мы будем получать сообщения WM_TOUCH, отправленные зарегистрированным в движке окнам.<br />
<br />
<pre class="brush:delphi">var
FHook: HHOOK = 0;
function GetMsgProc(nCode: Integer; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall;
begin
if (nCode = HC_ACTION) and (WParam = PM_REMOVE) then
if PMsg(LParam)^.message = WM_TOUCH then
MultiTouchEngine.HandleMessage(PMsg(LParam));
Result := CallNextHookEx(FHook, nCode, WParam, LParam);
end;
</pre>
<br />
На всякий случай, список используемых модулей выглядит так:<br />
<br />
<pre class="brush:delphi">uses
Windows,
Messages,
Classes,
Controls,
Generics.Defaults,
Generics.Collections,
Vcl.Touch.Gestures;
</pre>
<br />
Ну а теперь пройдемся по реализации самого движка. Начнем, пожалуй, с конструктора.<br />
<br />
<pre class="brush:delphi">constructor TSimleMultiTouchEngine.Create;
var
Data: Integer;
begin
// проверяем, есть ли поддержка мультитача
Data := GetSystemMetrics(SM_DIGITIZER);
FMultiTouchPresent :=
(Data and NID_READY <> 0) and (Data and NID_MULTI_INPUT <> 0);
// если нет, то и работать не с чем
if not FMultiTouchPresent then Exit;
// создаем список в котором будем хранить зарегистрированные окна
FWindows := TList<twindowdata>.Create(
// а чтобы IndexOf работал не по всей структуре а только по полю Control
// дописываем свой компаратор
TComparer<twindowdata>.Construct(
function (const A, B: TWindowData): Integer
begin
Result := Integer(A.Control) - Integer(B.Control);
end)
);
end;
</twindowdata></twindowdata></pre>
<br />
Достаточно простенький конструктор без изысков, в комментариях видны все шаги.<br />
Впрочем и деструктор также прост:<br />
<br />
<pre class="brush:delphi">destructor TSimleMultiTouchEngine.Destroy;
begin
if FHook <> 0 then
UnhookWindowsHookEx(FHook);
FWindows.Free;
inherited;
end;
</pre>
<br />
Единственный нюанс деструктора – снятие ловушки, если она была ранее установлена.<br />
<br />
Теперь перейдем к реализации двух единственных публичных процедур, доступных разработчику извне.<br />
<br />
Регистрация окна в движке:<br />
<br />
<pre class="brush:delphi">procedure TSimleMultiTouchEngine.RegisterWindow(Value: TWinControl;
Handlers: TTouchHandlers);
var
WindowData: TWindowData;
begin
// если мультитач не поддерживается - выходим
if not FMultiTouchPresent then Exit;
// для того чтобы IndexOf отработал, инициализируем соответствующее поле структуры
WindowData.Control := Value;
// окно можно зарегистрировать только один раз,
// повторная регистрация не поддерживается
if FWindows.IndexOf(WindowData) < 0 then
begin
// запоминаем список обработчиков
WindowData.Handlers := Handlers;
// подключаем окно к тачу
RegisterTouchWindow(Value.Handle, 0);
// добавляем структуру к общему списку окон
FWindows.Add(WindowData);
end;
// после добавления окна запускаем ловушку
if FHook = 0 then
FHook := SetWindowsHookEx(WH_GETMESSAGE, @GetMsgProc, HInstance, GetCurrentThreadId);
end;
</pre>
<br />
Все прокомментировано, впрочем единственный нюанс с вызовом IndexOf. Для того чтобы он работал не через CompareMem сравнивая две структуры между собой, а только по одному полю структуры (Control) и был реализован TComparer в конструкторе класса списка.<br />
<br />
Как можно увидеть из кода - логика проста, после добавления окна в общий список, класс стартует ловушку WH_GETMESSAGE (если она ранее не была запущена), причем работающую только в пределах текущей нити.<br />
<br />
Отдельно остановлюсь на переменной FMultiTouchPresent.<br />
Как видно из кода, она просто выполняет роль предохранителя, который отключает всю логику работы класса в том случае, если мы не можем сделать ничего полезного.<br />
Если убрать ее, то будет небольшой "overhead" в цикле выборки сообщений <b>каждого окна </b>нашего приложения из-за установленной ловушки в том случае, если наше "железо" вообще не имеет понятия о тачскрине. Оно нам надо?<br />
<br />
Снятие окна с регистрации идет по такому-же принципу, с отключением ловушки, если окон больше нет:<br />
<br />
<pre class="brush:delphi">procedure TSimleMultiTouchEngine.UnRegisterWindow(Value: TWinControl);
var
Index: Integer;
WindowData: TWindowData;
begin
// если мультитач не поддерживается - выходим
if not FMultiTouchPresent then Exit;
// для того чтобы IndexOf отработал, инициализируем соответствующее поле структуры
WindowData.Control := Value;
// ищем окно
Index := FWindows.IndexOf(WindowData);
if Index >= 0 then
// если нашлось, удаляем окно из списка
FWindows.Delete(Index);
// если окон не осталось, то ловушка нам больше не нужна
if FWindows.Count = 0 then
begin
// выключаем ее
UnhookWindowsHookEx(FHook);
FHook := 0;
end;
end;
</pre>
<br />
Собственно вся логика движка проста: приняли окно на регистрацию, запустили ловушку, которая при получении сообщения WM_TOUCH вызывает процедуру HandleMessage, посредством обращения к синглтону класса.<br />
<br />
<pre class="brush:delphi">procedure TSimleMultiTouchEngine.HandleMessage(Msg: PMsg);
var
I: Integer;
begin
for I := 0 to FWindows.Count - 1 do
// ищем индекс окна, которому пришло сообщение
if FWindows[I].Control.Handle = Msg^.hwnd then
begin
// и вызываем основной обработчик сообщения
HandleTouch(I, Msg);
Break;
end;
end;
</pre>
<br />
И вот и центральная процедура класса, вокруг которой крутится вся логика работы:<br />
<br />
<pre class="brush:delphi">procedure TSimleMultiTouchEngine.HandleTouch(Index: Integer; Msg: PMsg);
var
TouchData: TTouchData;
I, InputsCount: Integer;
Inputs: array of TTouchInput;
Flags: DWORD;
begin
// Смотрим, сколько точек тача сейчас активно
InputsCount := Msg^.wParam and $FFFF;
if InputsCount = 0 then Exit;
// Это количество не должно быть более максимально поддерживаемого значения
if InputsCount > MaxFingerCount then
InputsCount := MaxFingerCount;
// получаем информацию по всем точкам тача
SetLength(Inputs, InputsCount);
if not GetTouchInputInfo(Msg^.LParam, InputsCount,
@Inputs[0], SizeOf(TTouchInput)) then Exit;
CloseTouchInputHandle(Msg^.LParam);
// генерируем внешнее событие о начале процедуры
// оповещения об изменениях в точках тача
DoBeginTouch(FWindows[Index].Handlers.BeginTouch, InputsCount);
for I := 0 to InputsCount - 1 do
begin
TouchData.Index := I;
// в выдаваемой наружу структуре указываем ID каждой точки
// она не меняется в течении всей сессии (от Down до Up)
// и к ней можно делать привязку
TouchData.ID := Inputs[I].dwID;
// переводим координаты каждой точки в координаты окна
TouchData.Position.X := TOUCH_COORD_TO_PIXEL(Inputs[I].x);
TouchData.Position.Y := TOUCH_COORD_TO_PIXEL(Inputs[I].y);
TouchData.Position :=
FWindows[Index].Control.ScreenToClient(TouchData.Position);
// заполняем выставленные флаги
TouchData.Flags := [];
Flags := Inputs[I].dwFlags;
if Flags and TOUCHEVENTF_MOVE <> 0 then
Include(TouchData.Flags, tfMove);
if Flags and TOUCHEVENTF_DOWN <> 0 then
Include(TouchData.Flags, tfDown);
if Flags and TOUCHEVENTF_UP <> 0 then
Include(TouchData.Flags, tfUp);
// генерируем внешнее событие о изменении в состоянии каждой конкретной точки
DoTouch(FWindows[Index].Control,
FWindows[Index].Handlers.Touch, TouchData);
end;
// генерируем внешнее событие о завершении процедуры
// оповещения об изменениях в точках тача
DoEndTouch(FWindows[Index].Handlers.EndTouch);
end;
</pre>
<br />
Все это мы уже видели в пятой главе статьи, поэтому давать дополнительные пояснения по коду, смысла не имеет. Перейдем к работе с получившимся мультитач движком.<br />
<br />
Исходный код модуля SimleMultiTouchEngine.pas в папке "<a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">.\demos\multitouch_engine_demo\</a>" в архиве с исходниками.<br />
<br />
<h3 style="text-align: left;">
10. Работаем с TSimleMultiTouchEngine</h3>
<br />
Придумывать что-то новое не будем и попробуем воспроизвести проект из пятой главы, в котором основным изменением будет то, что поддержку мультитача будет осуществлять TSimleMultiTouchEngine.<br />
<br />
В проект, созданный в девятой главе, добавьте декларацию структуры TData и массива FData из пятой главы, а также скопируйте обработчик FormPaint. Это все останется без изменений.<br />
<br />
Объявим два обработчика:<br />
<br />
<pre class="brush:delphi"> procedure OnTouch(Sender: TObject;
Control: TWinControl; TouchData: TTouchData);
procedure OnTouchEnd(Sender: TObject);
</pre>
<br />
В используемые модули подключим SimleMultiTouchEngine и немного изменим конструктор класса:<br />
<br />
<pre class="brush:delphi">procedure TdlgMultiTouchEngineDemo.FormCreate(Sender: TObject);
var
I: Integer;
Handlers: TTouchHandlers;
begin
DoubleBuffered := True;
// RegisterTouchWindow(Handle, 0);
Randomize;
for I := 0 to 9 do
begin
FData[I].Color := Random($FFFFFF);
FData[I].ARect.Left := Random(ClientWidth - 100);
FData[I].ARect.Top := Random(ClientHeight - 100);
FData[I].ARect.Right := FData[I].ARect.Left + 100;
FData[I].ARect.Bottom := FData[I].ARect.Top + 100;
end;
ZeroMemory(@Handlers, SizeOf(TTouchHandlers));
Handlers.Touch := OnTouch;
Handlers.EndTouch := OnTouchEnd;
MultiTouchEngine.RegisterWindow(Self, Handlers);
end;
</pre>
<br />
Изменения, по сути, минимальны, вместо вызова RegisterTouchWindow, мы перекладываем работу на только что реализованный нами MultiTouchEngine.<br />
<br />
Обработчик OnTouchEnd простой:<br />
<br />
<pre class="brush:delphi">procedure TdlgMultiTouchEngineDemo.OnTouchEnd(Sender: TObject);
begin
Repaint;
end;
</pre>
<br />
Просто вызываем перерисовку всей канвы.<br />
<br />
А теперь посмотрим, во что превратился код в обработчике OnTouch (ранее реализованный в обработчике WmTouch):<br />
<br />
<pre class="brush:delphi">procedure TdlgMultiTouchEngineDemo.OnTouch(Sender: TObject;
Control: TWinControl; TouchData: TTouchData);
function GetIndexAtPoint(pt: TPoint): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to 9 do
if PtInRect(FData[I].ARect, pt) then
begin
Result := I;
Break;
end;
end;
function GetIndexFromID(ID: Integer): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to 9 do
if FData[I].TouchID = ID then
begin
Result := I;
Break;
end;
end;
var
Index: Integer;
R: TRect;
begin
if tfDown in TouchData.Flags then
begin
Index := GetIndexAtPoint(TouchData.Position);
if Index < 0 then Exit;
FData[Index].Touched := True;
FData[Index].TouchID := TouchData.ID;
FData[Index].StartRect := FData[Index].ARect;
FData[Index].StartPoint := TouchData.Position;
Exit;
end;
Index := GetIndexFromID(TouchData.ID);
if Index < 0 then Exit;
if tfUp in TouchData.Flags then
begin
FData[Index].Touched := False;
FData[Index].TouchID := -1;
Exit;
end;
if not (tfMove in TouchData.Flags) then Exit;
if not FData[Index].Touched then Exit;
R := FData[Index].StartRect;
OffsetRect(R,
TouchData.Position.X - FData[Index].StartPoint.X,
TouchData.Position.Y - FData[Index].StartPoint.Y);
FData[Index].ARect := R;
end;
</pre>
<br />
Идеология практически не изменилась, но читается он гораздо проще чем в старом варианте.<br />
И самое главное: работает также как и код из пятой главы.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='640' height='480' src='https://www.youtube.com/embed/LCEgn3ybpN4?feature=player_embedded' frameborder='0'></iframe></div>
<br />
<br />
Исходный код примера в папке "<a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">.\demos\multitouch_engine_demo\</a>" в архиве с исходниками.<br />
<br />
Так в чем-же цимус, скорее всего спросите вы. Ведь размер кода в главной форме и алгоритм его работы практически не изменился, плюс до кучи появился дополнительный модуль аж на 277 строчек кода (с коментариями) в виде SimleMultiTouchEngine.pas.<br />
Может проще оставить как есть и реализовывать обработчик WM_TOUCH самостоятельно только там, где это действительно необходимо?<br />
<br />
В принципе так то оно - так, правда этот движок решает только первую задачу из трех, озвученных в восьмой главе.<br />
<br />
И цимус заключается в следующем...<br />
<br />
<h3 style="text-align: left;">
11. Включаем в движок поддержку жестов</h3>
<br />
В реализованном выше MultiTouchEngine нет решения остальных трех пунктов из запланированных проблем, без решения которых он превращается просто в лишний класс в иерархии проекта (конечно теперь этот класс может обеспечить мультитачем всех страждущих, но сути это не меняет).<br />
<br />
Начнем сразу с проблемы за номером три.<br />
<br />
Для начала объявим типы распознаваемых движком жестов и обработчик внешнего события:<br />
<br />
<pre class="brush:delphi"> // типы распознаваемых жестов
TGestureType =
(
gtNone, // жест не распознан
gtTap, gt2Tap, gt3Tap, // обычные тапы (1, 2, 3 пальца)
gtLeft, gtRight, gtUp, gtDown, // свайп в стороны одним пальцем
gt2Left, gt2Right, gt2Up, gt2Down, // свайп в стороны двумя пальцами
gt3Left, gt3Right, gt3Up, gt3Down // свайп в стороны тремя пальцами
);
// декларация обработчика распознанных жестов
TGestureEvent = procedure(Sender: TObject; Control: TWinControl;
GestureType: TGestureType; Position: TPoint; Completed: Boolean) of object;
</pre>
<br />
Наш класс должен будет уметь распознавать 15 различных жестов (если не считать gtNone).<br />
<br />
Обратите внимание на параметр Completed в декларации TGestureEvent. Этот флаг будет сообщать разработчику о завершении жеста (приходе сообщения WM_TOUCH + TOUCHEVENTF_UP ).<br />
<b>Для чего это сделано:</b> к примеру пользователь нажал на тачскрин двумя пальцами и повел их влево, по идее необходимо скролировать окно, но если ждать окончания жеста, то правильного скролирования не получится, поэтому движок мультитача будет периодически генерировать внешнее событие OnGesture в котором можно будет произвести необходимый скрол прямо во время сессии тача. Именно в этом обработчике разработчик сможет понять по параметру Completed - завершен жест или нет (к примеру если нам приходит gtTap, а параметр Completed выставлен в False, то пока что делать ничего не надо и стоит подождать окончания).<br />
<br />
Частота, с которой будет генерироваться событие OnGesture в процессе сессии, напрямую зависит от константы GesturePartSize, которую я установил в 10. Т.е. как только количество точек сессии стало кратно константе (остаток от деления по модулю равен нулю), генерируется событие.<br />
<br />
Данные каждой сессии будут хранится вот в таком массиве:<br />
<br />
<pre class="brush:delphi"> TPointArray = array of TPoint;
</pre>
<br />
Ну а структуру, описывающую каждую сессию, задекларируем вот так:<br />
<br />
<pre class="brush:delphi"> TGestureItem = record
ID, // ID тача, по которому собирается информация
ControlIndex: Integer; // индекс окна, в котором происходит событие тача
Data: TList<tpoint>; // массив точек, по которым прошел тач в течении сессии
Done: Boolean; // флаг указывающий на завершение сессии
end;
</tpoint></pre>
<br />
Осталось, разве что, объявить класс, который будет хранить в себе данные по каждой тач-сессии:<br />
<br />
<pre class="brush:delphi"> // класс, хранящий в себе данные о всех сессиях мультитача
// поддерживает одновременно 10 сессий
TGesturesData = class
...
strict private
// массив данных для каждой сессии
FData: array [0..MaxFingerCount - 1] of TGestureItem;
...
public
...
// старт сессии
procedure StartGesture(ID, ControlIndex: Integer; Value: TPoint);
// добавление новой точки в сессию
function AddPoint(ID: Integer; Value: TPoint): Boolean;
// завершение сессии
procedure EndGesture(ID: Integer);
// очистка ресурсов всех сессий ассоциированных с указаным окном
procedure ClearControlGestures(ControlIndex: Integer);
// функция возвращает маршрут точек сессии в виде массива
function GetGesturePath(ID: Integer): TPointArray;
// индекс окна по которому будет сгенерированно события OnEndAllGestures и OnPartComplete
property LastControlIndex: Integer read FLastControlIndex;
// событие о завершении всех сессий ассоциированных с окном LastControlIndex
property OnEndAllGestures: TGesturesDataEvent read FEndAll write FEndAll;
// событие о достижении лимита GesturePartSize для всех сессий ассоциированных с окном LastControlIndex
property OnPartComplete: TGesturesDataEvent read FPart write FPart;
end;
</pre>
<br />
Это в действительности крайне простой класс, не содержащий никаких особых изысков, поэтому рассматривать реализацию каждой функции не будем, благо вы все сможете увидеть <span style="color: red;"><a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">в примере демопроекта</a></span>, идущего вместе со статьей.<br />
<br />
Вся его задача, это:<br />
<ol style="text-align: left;">
<li>хранить поступающие извне данные, через вызовы StartGesture и AddPoint;</li>
<li>после каждого вызова AddPoint, проверять размер списка Data: TList<TPoint> для каждой сессии, ассоциированной с окном ControlIndex и, по необходимости, вызывать OnPartComplete;</li>
<li>после вызова EndGesture? проверять все сессии с одинаковым ControlIndex и? если они все завершены, вызывать OnEndAllGestures.</li>
</ol>
Это просто хранилище сессий для нашего движка и с хранимым им данными будет работать TGestureRecognizer.<br />
<br />
Расширим наш базовый класс, добавив следующие два поля:<br />
<br />
<pre class="brush:delphi"> // класс для хранения данных по каждой сессии мультитача
FGesturesData: TGesturesData;
// класс распознает жекст в рамках одной сессии
FGestureRecognizer: TGestureRecognizer;
</pre>
<br />
В конструкторе создадим и проинициализируем наше хранилище:<br />
<br />
<pre class="brush:delphi"> FGesturesData := TGesturesData.Create;
FGesturesData.OnEndAllGestures := OnEndAllGestures;
FGesturesData.OnPartComplete := OnPartComplete;
FGestureRecognizer := TGestureRecognizer.Create;
</pre>
<br />
После чего вернемся обратно в методу HandleTouch(), где нам будет необходимо немного расширить код, который отвечал за установку флагов в структуре TouchData:<br />
<br />
<pre class="brush:delphi"> TouchData.Flags := [];
Flags := Inputs[I].dwFlags;
if Flags and TOUCHEVENTF_MOVE <> 0 then
begin
Include(TouchData.Flags, tfMove);
// идет процетура перемещения точки, добавляем новые координаты
// к сесии, ассоциированной с данной точкой
if not FGesturesData.AddPoint(TouchData.ID, TouchData.Position) then
// а если вдруг такая сессия отсутствует, то создаем ее
FGesturesData.StartGesture(TouchData.ID, Index, TouchData.Position);
end;
if Flags and TOUCHEVENTF_DOWN <> 0 then
begin
Include(TouchData.Flags, tfDown);
// пользователь только что нажал на тачустройство,
// стартуем новую сессию с уникальным ID
FGesturesData.StartGesture(TouchData.ID, Index, TouchData.Position);
end;
if Flags and TOUCHEVENTF_UP <> 0 then
begin
Include(TouchData.Flags, tfUp);
// пользователь отпустил палец и завершил работу с сесией
// то-же самое делаем и мы.
// если все сессии связанные с текущим окном завершены,
// то FGesturesData поднимет внутреннее событие о завершении всех сессий
FGesturesData.EndGesture(TouchData.ID);
end;
</pre>
<br />
Собственно, это практически вся активная работа с хранилищем данных для каждой сессии.<br />
Осталось только посмотреть на код реализации обработчиков:<br />
<br />
<pre class="brush:delphi">//
// Пришло внутренне событие о достижении лимита у всех сессий ассоциированным с окном
// Параметр Values содержит ID всех сессий,
// данные с которых будут использоваться для распознования жест
// =============================================================================
procedure TTouchManager.OnPartComplete(Values: TBytes);
var
Position: TPoint;
GestureType: TGestureType;
begin
// смотрим что за жест у нас получился?
GestureType := RecognizeGestures(Values, Position);
// если жест рапознан, то генерируем внешнее событие
if GestureType <> gtNone then
DoGesture(
FWindows[FGesturesData.LastControlIndex].Control,
FWindows[FGesturesData.LastControlIndex].Handlers.Gesture,
GestureType, Position,
// с указанием флага что жест еще продолжается и пока распознан частично
False);
end;
</pre>
и второй вариант, который практически ничем не отличается от первого:<br />
<br />
<pre class="brush:delphi">//
// Пришло внутренне событие о завершении всех сессий ассоциированным с окном
// Параметр Values содержит ID всех завершенных сессий
// =============================================================================
procedure TTouchManager.OnEndAllGestures(Values: TBytes);
var
Position: TPoint;
GestureType: TGestureType;
begin
try
// смотрим что за жест у нас получился?
GestureType := RecognizeGestures(Values, Position);
// если жест рапознан, то генерируем внешнее событие
if GestureType <> gtNone then
DoGesture(
FWindows[FGesturesData.LastControlIndex].Control,
FWindows[FGesturesData.LastControlIndex].Handlers.Gesture,
GestureType, Position,
// с указанием флаза что жест завершен
True);
finally
// в конце очищаем все данные по сессиям
FGesturesData.ClearControlGestures(FGesturesData.LastControlIndex);
end;
end;
</pre>
<br />
Единственно его отличие в том, что освобождаются ресурсы, занятые данными от сессий ассоциированных с окном, над которым жест был распознан.<br />
<br />
Ну и как понятно по коду: вся основная работа идет в функции RecognizeGestures, логику которой я уже описал в седьмой главе.<br />
<br />
Выглядит она вот так:<br />
<br />
<pre class="brush:delphi">//
// Функция распознающая тип жеста на основе маршрутов сессий из TGesturesData
// Параметр Values содержит ID всех сессий,
// данные с которых будут использоваться для распознования жест
// =============================================================================
function TTouchManager.RecognizeGestures(Values: TBytes;
var Position: TPoint): TGestureType;
var
I, A, ValueLen, GestureLen: Integer;
GestureID: Byte;
GesturePath: TPointArray;
NoMove: Boolean;
begin
Result := gtNone;
// смотрим количество сессий, по которым будем рапознавать жест
ValueLen := Length(Values);
// если их больше трех (четыре и больше точки тача), то выходим
if ValueLen > 3 then Exit;
// общая идея такова:
// мы можем распознать ID жеста по одной точке (вызовом GetGestureID),
// к примеру пусть это будет sgiLeft
// то в случае двух и более точек, если у каждой из них ID жеста совпал с первой,
// считаем что этот тот-же жест только выполненный двумя или тремя пальцами
// Единственный нюанс в рапознавании тапов двумя и более пальцами
// Для этого смотрятся все координаты маршрута по каждой точке
// и если эти координаты на всем протяжении не менялись,
// то считаем что операции перемещения не было и произошел обычный тап
GestureID := sgiNoGesture;
NoMove := True;
for I := 0 to ValueLen - 1 do
begin
// итак, получаем маршрут сессии в виде массива TPoint
GesturePath := FGesturesData.GetGesturePath(Values[I]);
GestureLen := Length(GesturePath);
// Если один из маршрутов пуст - выходим, ибо пока что еще нечего распознавать
if GestureLen = 0 then Exit;
// детектируем отсутствие перемещения всех точек тача
if NoMove then
for A := 1 to GestureLen - 1 do
if GesturePath[0] <> GesturePath[A] then
begin
NoMove := False;
Break;
end;
// получаем координаты события.
// вот тут конечно не верно. ибо если точек две и более, то координаты
// берутся только у самой последней, но как бы это более правильно сделать я не знаю
Position := GesturePath[GestureLen - 1];
// запоминаем ID жеста для первой точки
if I = 0 then
GestureID := GetGestureID(GesturePath)
else
// и сравниваем ID жестов для всех остальных точек, они должны совпадать
if GestureID <> GetGestureID(GesturePath) then
Exit;
end;
// ну и на основе ID жеста и количества точек генерируем свой собственный результат
if (GestureID = sgiNoGesture) then
begin
if NoMove then
case ValueLen of
1: Result := gtTap;
2: Result := gt2Tap;
3: Result := gt3Tap;
end;
end
else
begin
Dec(ValueLen);
Result := TGestureType(3 + GestureID + ValueLen * 4);
end;
end;
</pre>
<br />
Этой функции требуется вспомогательная GetGestureID, аналог которой уже был показан в шестой главе.<br />
<br />
После всех этих манипуляций можно сказать, что проблема за номером 3, озвученная в восьмой главе, решена. То есть: мы умеем хранить данные о каждой сессии и, более того, знаем над каким окном она проводится.<br />
<br />
Осталось совсем немного – проблема номер два :)<br />
<br />
<h3 style="text-align: left;">
12. Детектируем пересоздание окна</h3>
<br />
Как я ранее говорил, вызов RecreateWnd, по сути, является штатным механизмом VCL.<br />
Однако он может сильно попортить всю логику работы нашего движка, т.к. при пересоздании окна, пока что никто не производит повторный вызов RegisterTouchWindow на вновь созданный хэндл. Таким образом, хоть окно и продолжает быть зарегистрированным в движке, сообщения WM_TOUCH перестают ему приходить.<br />
<br />
Подойти к решению этой задачи можно несколькими способами: к примеру, раз уж мы установили ловушку, то почему бы не отлавливать сообщения WM_CREATE/WM_DESTROY до кучи к WM_TOUCH?<br />
<br />
А вот не хочу, ибо таких сообщений в рамках GUI потока будет море, а зачем нам ненужный оверхед в цикле выборки сообщений?<br />
<br />
Поэтому зайдем с другой стороны и напишем некий проксик, который будет представлять из себя невидимое окно, которому родителем будет выставлено окно, за которым мы должны следить. В этом случае, при разрушении основного окна, разрушится и окно нашего проксика, что можно задетектировать в обработчике DestroyHandle, а создание окна, после его разрушения поймаем в CreateWnd, где уже будет доступен валидный хэндл родителя, которому можно сказать RegisterTouchWindow, подключив его обратно к получению сообщений WM_TOUCH.<br />
<br />
Выглядит это безобразие следующим образом:<br />
<br />
<pre class="brush:delphi">type
// класс следящий за пересозданием зарегистрированного окна
TWinControlProxy = class(TWinControl)
protected
procedure DestroyHandle; override;
procedure CreateWnd; override;
procedure CreateParams(var Params: TCreateParams); override;
end;
{ TWinControlProxy }
//
// При создании выставляем стиль WS_EX_TRANSPARENT, мы будем не заметными.
// =============================================================================
procedure TWinControlProxy.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_TRANSPARENT;
end;
//
// При создании окна сразу включаем ему поддержку тача
// =============================================================================
procedure TWinControlProxy.CreateWnd;
begin
inherited CreateWnd;
if Parent.HandleAllocated then
RegisterTouchWindow(Parent.Handle, 0);
Visible := False;
end;
//
// При разрушении окна отключаем поддержку тача, дабы не мусорить
// =============================================================================
procedure TWinControlProxy.DestroyHandle;
begin
if Parent.HandleAllocated then
UnregisterTouchWindow(Parent.Handle);
Visible := True;
inherited DestroyHandle;
end;
</pre>
<br />
Этот проксик ничего знать не знает о нашем движке и тихой сапой выполняет всего лишь одну единственную задачу – не допустить отключение окна от тачскрина.<br />
<br />
Для поддержки проксика необходимо немного расширить структуру TWindowData, добавив туда ссылку на ассоциированный с окном прокси:<br />
<br />
<pre class="brush:delphi"> TWindowData = record
Control, Proxy: TWinControl;
</pre>
<br />
После чего немного изменить процедуры регистрации окна:<br />
<br />
<pre class="brush:delphi"> if FWindows.IndexOf(WindowData) < 0 then
begin
// создаем следящий проксик,
// который будет заного подключать окно к тачу после его пересоздания
WindowData.Proxy := TWinControlProxy.Create(Value);
WindowData.Proxy.Parent := Value;
// старый код
WindowData.Handlers := Handlers;
WindowData.LastClickTime := 0;
// подключаем окно к тачу
RegisterTouchWindow(Value.Handle, 0);
FWindows.Add(WindowData);
end;
</pre>
<br />
и снятия окна с регистрации:<br />
<br />
<pre class="brush:delphi"> if Index >= 0 then
begin
// удаляем следящий проксик, он при разрушении отключит окно от тача
FWindows[Index].Proxy.Free;
// старый код
// и удаляем окно из списка
FWindows.Delete(Index);
end;
</pre>
<br />
Вот собственно и все.<br />
Давайте посмотрим как это работает.<br />
<br />
<h3 style="text-align: left;">
13. Контрольный тест работы мультитач движка</h3>
<br />
Опять создаем новый проект и на главную форму кидаем TMemo, в который будут выводится результаты работы и кнопку.<br />
<br />
В обработчике кнопки будем пересоздавать главную форму, чтобы протестировать работу проксика:<br />
<br />
<pre class="brush:delphi">procedure TdlgGesturesText.Button1Click(Sender: TObject);
begin
RecreateWnd;
end;
</pre>
<br />
В конструкторе формы подключим ее к движку мультитача:<br />
<br />
<pre class="brush:delphi">procedure TdlgGesturesText.FormCreate(Sender: TObject);
var
Handlers: TTouchHandlers;
begin
ZeroMemory(@Handlers, SizeOf(TTouchHandlers));
Handlers.Gesture := OnGesture;
TouchManager.RegisterWindow(Self, Handlers);
end;
</pre>
после чего реализуем сам обработчик:<br />
<br />
<pre class="brush:delphi">procedure TdlgGesturesText.OnGesture(Sender: TObject; Control: TWinControl;
GestureType: TGestureType; Position: TPoint; Completed: Boolean);
begin
if not Completed then
if not (GestureType in [gt2Left..gt2Down]) then Exit;
Memo1.Lines.Add(Format('Control: "%s" gesture "%s" at %dx%d (completed: %s)',
[
Control.Name,
GetEnumName(TypeInfo(TGestureType), Integer(GestureType)),
Position.X,
Position.Y,
BoolToStr(Completed, True)
]));
end;
</pre>
<br />
Билдим, запускаем – вуаля.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='640' height='480' src='https://www.youtube.com/embed/EgabHZIzbLs?feature=player_embedded' frameborder='0'></iframe></div>
<br />
На видео наглядно видно распознавание всех 15 поддерживаемых жестов и также работа контролирующего зарегистрированное окно, проксика.<br />
<br />
Собственно, это и был тот самый цимус, о котором я говорил в конце 10 главы - буквально полтора десяток строчек кода и все работает из коробки :)<br />
<br />
Исходный код примера в папке "<a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">.\demos\gestures\</a>" в архиве с исходниками.<br />
<br />
<h3 style="text-align: left;">
14. Выводы</h3>
<br />
Жалко, конечно, что этот функционал отсутствует в ХЕ4.<br />
С другой стороны, если бы не этот момент, я бы так и не стал разбираться в том: "как оно там это все фунциклирует", так что есть плюсы :)<br />
<br />
Минусы данного подхода в том, что полностью выпилена работа с сообщениями WM_GESTURE + WM_POINTS и распознавание жестов передано на откуп коду в движке.<br />
Согласен, но это сделано преднамеренно.<br />
<br />
Если вы сами начнете копаться в этом направлении, вероятно вы в итоге согласитесь с моим подходом, хотя как знать. По крайней мере у вас останется поле для фантазии, как можно еще подойти к решению такой задачи.<br />
<br />
Исходный код класса Common.TouchManager, предоставленный в демопримерах к статье не является окончательным и будет периодически развиваться, правда не уверен что я буду его сопровождать на паблике. Впрочем ваши предложения и замечания только приветствуются.<br />
<br />
Как всегда, благодарю участников форума "Мастера Дельфи" за вычитку статьи.<br />
<br />
Исходный код демопримеров доступен по <span style="color: red;"><a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">данной ссылке</a></span>.<br />
<br />
Удачи!<br />
<br />
<b>Update:</b><br />
<br />
К сожалению, или к счастью, выяснилось что из-за некоторых особенностей ОС Windows 8 и выше, ловушка WH_GETMESSAGE не будет перехватывать сообщение WM_TOUCH, таким образом этот код работать не будет.<br />
<br />
Чтобы исправить такую неприятность, нужно убрать работу с ловушкой и передать обработку сообщения WM_TOUCH на проксик, переписав его следующим образом:<br />
<br />
<pre class="brush:delphi">type
// класс следящий за пересозданием зарегистрированного окна
TWinControlProxy = class(TWinControl)
private
FOldWndProc: TWndMethod;
procedure ParentWndProc(var Message: TMessage);
protected
procedure DestroyHandle; override;
procedure CreateWnd; override;
procedure CreateParams(var Params: TCreateParams); override;
public
destructor Destroy; override;
procedure InitParent(Value: TWinControl);
end;
{ TWinControlProxy }
//
// При создании выставляем стиль WS_EX_TRANSPARENT, мы будем не заметными.
// =============================================================================
procedure TWinControlProxy.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_TRANSPARENT;
end;
//
// При создании окна сразу включаем ему поддержку тача
// =============================================================================
procedure TWinControlProxy.CreateWnd;
begin
inherited CreateWnd;
if Parent.HandleAllocated then
RegisterTouchWindow(Parent.Handle, 0);
Visible := False;
end;
//
// При разрушении, возвращаем оконную процедуру на место
// =============================================================================
destructor TWinControlProxy.Destroy;
begin
if Parent <> nil then
Parent.WindowProc := FOldWndProc;
inherited;
end;
//
// При разрушении окна отключаем поддержку тача, дабы не мусорить
// =============================================================================
procedure TWinControlProxy.DestroyHandle;
begin
if Parent.HandleAllocated then
UnregisterTouchWindow(Parent.Handle);
Visible := True;
inherited DestroyHandle;
end;
//
// При инициализации парента, перекрываем его оконную процедуру
// =============================================================================
procedure TWinControlProxy.InitParent(Value: TWinControl);
begin
Parent := Value;
FOldWndProc := Value.WindowProc;
Value.WindowProc := ParentWndProc;
end;
//
// Перехватываем сообщение WM_TOUCH в оконной процедуре родителя
// =============================================================================
procedure TWinControlProxy.ParentWndProc(var Message: TMessage);
var
Msg: TMsg;
begin
if Message.Msg = WM_TOUCH then
begin
Msg.hwnd := Parent.Handle;
Msg.wParam := Message.WParam;
Msg.lParam := Message.LParam;
TouchManager.HandleMessage(@Msg);
end;
FOldWndProc(Message);
end;
</pre>
<br />
В <a href="http://rouse.drkb.ru/blog/multitouch.zip" target="_blank">архиве к статье</a>, данные изменения уже произведены.<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
© Александр (Rouse_) Багель</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Октябрь, 2014</div>
</div>
</div>
</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com4tag:blogger.com,1999:blog-2374465879949372415.post-76617957803456811932014-04-09T22:08:00.001+04:002014-04-10T10:22:03.573+04:00Анализ приложения защищенного виртуальной машиной<div dir="ltr" style="text-align: left;" trbidi="on">
В данной статье будет рассмотрено построение защиты приложения с использованием различных программных "трюков" таких как: сброс точки входа в ноль, шифрование тела файла и дешифровшик накрытый мусорным полиморфом, сокрытие логики исполнения алгоритма приложения в теле виртуальной машины.<br />
<br />
К сожалению, статья будет достаточно тяжелая для обычного прикладного программиста, не интересующегося тематикой защиты ПО, но тут уж ничего не поделать.<br />
<br />
Для более или менее адекватного восприятия статьи потребуется минимальные знания ассемблера (его будет много) а так-же навыков работы с отладчиком.<br />
<br />
Но и тем, кто надеется что здесь будут даны какие-то простые шаги по реализации такого типа защиты, придется разочароваться. В статье будет рассмотрен уже реализованный функционал, но... с точки зрения его взлома и полного реверса алгоритма.<br />
<br />
Основные цели, которые я ставил перед собой, это дать общее понятие как вообще работает такая защита ПО, но самое главное - как к этому будет подходить человек, который будет снимать вашу защиту, ибо есть старое правило - нельзя реализовать грамотный алгоритм ядра защиты, не представляя себе методы его анализа и взлома.<br />
<br />
В качестве реципиента, по совету одного достаточно компетентного товарища, я выбрал немножко старый (но не потерявший актуальности, в силу качества исполнения) keygenme от небезызвестного Ms-Rem.<br />
<br />
Вот первоначальная ссылка, где он появился: <a href="http://exelab.ru/f/index.php?action=vthread&forum=1&topic=4732" target="_blank">http://exelab.ru/f/index.php?action=vthread&forum=1&topic=4732</a><br />
А потом он попал вот сюда: <a href="http://www.crackmes.de/users/ms_rem/keygenme_by_ms_rem/" target="_blank">http://www.crackmes.de/users/ms_rem/keygenme_by_ms_rem/</a><br />
Где данному keygenme был выставлена сложность 8 из 10 (*VERY VERY* hard).<br />
Хотя, если честно, это слегка завышенная оценка - я бы поставил в районе 5-6 баллов.<br />
<br />
Пожалуй, начнем.<br />
<br />
<br />
<a name='more'></a><br />
<h3 style="text-align: left;">
0. Требования</h3>
<br />
По хорошему, для полноценной отладки данного keygenme, самым удобной площадкой будет Windows XP 32 бита, она вообще является самой оптимальной средой, поэтому постоянно развернута у меня на рабочей станции в виде виртуалки.<br />
<br />
Под Windows 7 - 32 бита (на которой собственно и производилась отладка в процессе написания данной статьи) будут небольшие затруднения, но они решаемые (об этом будет упоминание в четвертой главе статьи).<br />
<br />
На 64 битных OC начнутся серьезные трудности ввиду того, что используемый в качестве основного инструмент (OllyDebug) при отладке данного Keygenme будет выдавать ошибки еще на этапе работы загрузчика. Этими ошибками не будет сыпать OllyDebug версии 2, но здесь есть еще одно затруднение, для нее пока что нет необходимых плагинов (а может я плохо искал).<br />
<br />
Сам keygenme необходимо скачать по этой ссылке: <a href="http://exelab.ru/f/files/3635_03.05.2006_CRACKLAB.rU.tgz" target="_blank">http://exelab.ru/f/files/3635_03.05.2006_CRACKLAB.rU.tgz</a> (потребуется регистрация).<br />
<br />
Для полноценной работы с текстом статьи, если вы решите самостоятельно пройти все шаги, описанные в ней, вам потребуется небольшой набор инструментов.<br />
Я не буду здесь подробно расписывать про них, все что нужно сделать описано в файле "used_tools.txt", размещенном в корне архива с примерами к статье: <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">http://rouse.drkb.ru/blog/vm_analize.zip</a><br />
<br />
Еще необходимо запомнить правильную пару логина и серийного номера, предоставленную по самой первой ссылке, а именно "Ms-Rem" и "C38FB7A0CF38F73B1159". Эти данные очень сильно помогут в процессе разбора keygenme.<br />
<br />
Как только все будет установлено - можно начинать :)<br />
<br />
<h3 style="text-align: left;">
1. Первичный анализ</h3>
<br />
Для начала стоит определиться, с чем именно мы имеем дело.<br />
Запускаем PEiD и открываем в нем keygenme.exe.<br />
Нажимаем самую правую нижнюю кнопку и в меню выберем тип сканирования "Hardcore Scan".<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivu6JUhCw8CDvtihx_tUmU-V8su1yGan_gTjoQpy_mgCyiyV6xoIDogFHSJ5huZnJclb61dFpg2g7Z-6KEcKC-Ht28MwzE9dnTb85QC7Z2SE6yIhCNYD677f7DQTKMEfcCVwZ6SF3aZzc/s1600/0.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivu6JUhCw8CDvtihx_tUmU-V8su1yGan_gTjoQpy_mgCyiyV6xoIDogFHSJ5huZnJclb61dFpg2g7Z-6KEcKC-Ht28MwzE9dnTb85QC7Z2SE6yIhCNYD677f7DQTKMEfcCVwZ6SF3aZzc/s1600/0.png" /></a></div>
<br />
Сразу начинаются неприятности, во первых точка входа "Entrypoint" выставлена в ноль, что в нормальном исполняемом файле быть не может, во вторых сканирование показало что присутствует "UPolyX v0.5 *".<br />
Второе как раз не страшно - вот было бы там что-то наподобие "EXECryptor" или "Themida" - какой-то из коммерческих протекторов, тогда да, а тут просто видимо нашлась какая-то подходящая сигнатура.<br />
<br />
Нажимаем вторую справа нижнюю кнопку и в появившемся диалоге три кнопки справа.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSQmTZxW91LAbBph42rsIToTn9w1l_C9Z6YmNeA_QLtSCHlurt76oPNQpFdKHGH0niFELFHwLIz0wszYoV2KvMW5_D_GxEyXPdjIrEqe4L7HeABPjn-uks0vbI-RrmkuFKQH5UeO52sHk/s1600/0_1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSQmTZxW91LAbBph42rsIToTn9w1l_C9Z6YmNeA_QLtSCHlurt76oPNQpFdKHGH0niFELFHwLIz0wszYoV2KvMW5_D_GxEyXPdjIrEqe4L7HeABPjn-uks0vbI-RrmkuFKQH5UeO52sHk/s1600/0_1.png" /></a></div>
<br />
Говорит что файл упакован и энтропия аж 7.56.<br />
Ну допустим, хотя это еще ни о чем не говорит. Большая энтропия бывает не только у запакованных, но и у зашифрованных файлов.<br />
<br />
Закрываем диалог и щелкаем на кнопку справа от "Subsystem:"<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyRlGM35badM8TKU4qQ2Vo8_4wX0EvUMF3G4AsKFHfuTz1bFSvZ5I6YGwExzcVaulo6FaxZEvi3f-2wH2IlFOrCvUTts8ZlSL-cozFhPltymZGwZQNF8dMADthM0R0OoXFaulT98-9EYU/s1600/0_2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyRlGM35badM8TKU4qQ2Vo8_4wX0EvUMF3G4AsKFHfuTz1bFSvZ5I6YGwExzcVaulo6FaxZEvi3f-2wH2IlFOrCvUTts8ZlSL-cozFhPltymZGwZQNF8dMADthM0R0OoXFaulT98-9EYU/s1600/0_2.png" /></a></div>
<br />
Помимо точки входа убиты базы кода и данных, база загрузки стандартная 4000000.<br />
Ну что же, ладно - на руках у нас файл который немного поправили ручками.<br />
Попробуем пощупать все это в отладчике.<br />
<br />
<h3 style="text-align: left;">
2. Анализируем поведение приложения при Entrypoint = 0</h3>
<br />
Открываем OllyDebug, заходим в меню "Options", там выбираем "Debugging options" и на вкладке "Events" выставляем галку "Make first pause at: -> System breakpoint".<br />
Таким образом мы заставим отладчик прерваться при получении первого отладочного сообщения до передачи управления в тело отлаживаемого приложения.<br />
Это делается из-за скинутой в ноль точки входа.<br />
<br />
Открываем сам keygenme.exe и сразу прерываемся где-то внутри ntdll.dll<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQ8C4mDZUN5tHJanVJOgw31i6gYlVObf2qeYGjIFLVLexBn2LwGxtJ7LESwTzIUqkYzhuFzPNb4R9pxTy8R3e8G6HbYQHZbWZElFBuU4sI__H0DcKcepb2eB7J1XXoyG_d7de4KeU1ysk/s1600/0_3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQ8C4mDZUN5tHJanVJOgw31i6gYlVObf2qeYGjIFLVLexBn2LwGxtJ7LESwTzIUqkYzhuFzPNb4R9pxTy8R3e8G6HbYQHZbWZElFBuU4sI__H0DcKcepb2eB7J1XXoyG_d7de4KeU1ysk/s1600/0_3.png" height="289" width="400" /></a></div>
<br />
Что есть точка входа (для приложения) - это смещение от его базы загрузки (hInstance), на которое загрузчик передает управления сразу после инициализации процесса.<br />
База загрузки всегда содержит в себе PE заголовок, где самой первой идет структура _IMAGE_DOS_HEADER.<br />
<br />
Т.к. точка входа у keygenme равна нулю, значит управление будет передано непосредственно на его hInstance.<br />
<br />
Зная это, давайте посмотрим что там у нас находится.<br />
Нажимаем "Ctrl+G" и вбиваем адрес базы "400000", должно получится как-то так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkc0wfMmSK2LerngS-e6cdjqOWzjTi0y4YgP6my165nNOd24XGIg0xyeGSXjzrnk5JLFinMsU_ELqb2_KuWWou5F3cm27EyY-_88US6Xp0xfMeUjvpCG8Zqw6mx0xTIFN5A9bbYt59T7A/s1600/0_4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkc0wfMmSK2LerngS-e6cdjqOWzjTi0y4YgP6my165nNOd24XGIg0xyeGSXjzrnk5JLFinMsU_ELqb2_KuWWou5F3cm27EyY-_88US6Xp0xfMeUjvpCG8Zqw6mx0xTIFN5A9bbYt59T7A/s1600/0_4.png" /></a></div>
<br />
Вполне себе приличный код вместо стандартного заголовка, но заголовок должен быть на месте, иначе приложение не запустилось бы, значит были внесены правки непосредственно в _IMAGE_DOS_HEADER.<br />
<br />
Смотрим что именно поменялось:<br />
<br />
<pre class="brush:delphi"> _IMAGE_DOS_HEADER = record { DOS .EXE header }
e_magic: Word; { Magic number }
e_cblp: Word; { Bytes on last page of file }
e_cp: Word; { Pages in file }
e_crlc: Word; { Relocations }
e_cparhdr: Word; { Size of header in paragraphs }
e_minalloc: Word; { Minimum extra paragraphs needed }
</pre>
<br />
Поле e_magic - его трогать нельзя и оно всегда должно содержать инициалы Марка Збиковски 'MZ' (0x4D, 0x5A).<br />
Собственно оно и не тронуто, а оба этих символа трактуются как инструкции:<br />
<br />
<pre class="brush:asm"> DEC EBP // уменьшаем указатель стекового фрейма
POP EDX // читаем значение со стека в регистр EDX
</pre>
<br />
Значение второго поля e_cblp изменено на 0x45, 0x52, что в результате отменяет изменения сделанные первыми двумя инструкциями, восстанавливая правильное состояние стека.<br />
<br />
Остальные 4 поля используются для реализации команд MOV + JMP.<br />
Вот на этой картинке это показано более наглядно.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhieZvCjXWyMuUQlroOcl1jmHFi8-xoWk4zo7Ls7uUcefl2VA6JNQo0m252xtazWJaO4dsl5NYPOc7Eezr-pQDf8-I45qKe4QC1z2UWgpMpuSDPx8_HvIZYi7T3mKSj5yk174v05kaoz6g/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhieZvCjXWyMuUQlroOcl1jmHFi8-xoWk4zo7Ls7uUcefl2VA6JNQo0m252xtazWJaO4dsl5NYPOc7Eezr-pQDf8-I45qKe4QC1z2UWgpMpuSDPx8_HvIZYi7T3mKSj5yk174v05kaoz6g/s1600/1.png" height="262" width="640" /></a></div>
<br />
Весь смысл таких манипуляций с _IMAGE_DOS_HEADER и сброшенной точкой входа, это передача управления куда-то внутрь тела приложения по адресу 4053B6.<br />
<br />
Т.е. в принципе мы можем уже прямо сейчас открыть keygenme.exe и в соответствующем поле в качестве точки входа указать 53B6 (игнорируя правки в заголовке файла), но правильная ли это точка входа?<br />
<br />
<h3 style="text-align: left;">
3. Разбираем код декриптора тела приложения и распаковываем приложение</h3>
<br />
Идем по адресу перехода "Ctrl+G" 4056B6 и там видим вот такое:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKFhUqNytQXqxk9BFx1hFIqLnLndraPcl0vQh6HGydBK0VZ3cencLLIzGUZy06e73PwM_BjWVk1KAM88-MAeKV34DCjaFBECXtZVKp7I4SpgMMKo-0d27AMIznCWyj_MjSk-N0EELi5e4/s1600/0_5.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKFhUqNytQXqxk9BFx1hFIqLnLndraPcl0vQh6HGydBK0VZ3cencLLIzGUZy06e73PwM_BjWVk1KAM88-MAeKV34DCjaFBECXtZVKp7I4SpgMMKo-0d27AMIznCWyj_MjSk-N0EELi5e4/s1600/0_5.png" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Начало мусорного кода</td></tr>
</tbody></table>
Вообще сплошной мусор. Что ни строчка, то мусорная инструкция.<br />
К примеру все операторы условных переходов (JG/JPE/JCXZ/JE) являются полным мусором, т.к. не важно выполнится ли условие или нет, переход всегда будет осуществлен на следующую строчку (обратите внимание на адреса прыжков).<br />
Инструкции LEA, MOV, XCNG работают с одним и тем-же регистром не внося никаких изменений в его состояние - мусор.<br />
Инструкции работы с матсопроцессором (FCLEX/FFREE) сбрасывают исключения (которых нет, т.к. работа с матсопроцессором еще не проводилась) освобождают регистры (которые собственно и не заняты) - мусор.<br />
<br />
Пролистаем код до конца, чтобы посмотреть, где эта каша из мусора заканчивается.<br />
Просто скролим вниз, пока не доберемся до кода, состоящего из одних нулей:<br />
<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEie5pYillCuoIggu8Dd1Ijf5nFpTQWawlS4WJTxYz0cO0iBmrbjO7uQvnhVCbFm5fUfd0p5mNi1mScFkvFVYVlxFmSeYoLk3u79e_Mk7rjFkfJXwJAwUKhRgPM2wXFVHjNHeV0pOwA3Xj0/s1600/0_6.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEie5pYillCuoIggu8Dd1Ijf5nFpTQWawlS4WJTxYz0cO0iBmrbjO7uQvnhVCbFm5fUfd0p5mNi1mScFkvFVYVlxFmSeYoLk3u79e_Mk7rjFkfJXwJAwUKhRgPM2wXFVHjNHeV0pOwA3Xj0/s1600/0_6.png" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Конец мусорного кода, прыжок на OEP</td></tr>
</tbody></table>
Ага, а вот похоже и нужный нам адрес 401000, на который идет прыжок, который теоретически может являться оригинальной точкой входа.<br />
<br />
Давайте посмотрим что там:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDxshLTb2wSU6sfR4yYtkC2S_YmGzxybBFYQqTuvnI5SGZTUYgkEbyX4HDmR_M1-v9N0c6gq6Kh9G20A49g1SKnkYFSMoKT9G2Kr_7owN1maJC3Pc6UWl8MO40jdvlaSwmFn9u0sayXv0/s1600/7.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDxshLTb2wSU6sfR4yYtkC2S_YmGzxybBFYQqTuvnI5SGZTUYgkEbyX4HDmR_M1-v9N0c6gq6Kh9G20A49g1SKnkYFSMoKT9G2Kr_7owN1maJC3Pc6UWl8MO40jdvlaSwmFn9u0sayXv0/s1600/7.png" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Зашифрованная оригинальная точка входа</td></tr>
</tbody></table>
А там у нас код, которого явно не должно быть в Win32 приложении, о чем явно говорят инструкции IN и OUT, которые сгенерируют исключение при их выполнении.<br />
<br />
Значит получается, что код на оригинальной точке входа (OEP - Original Entry Point) зашифрован и код в процедуре 53B6 должен его расшифровать перед тем как выполнить финальный прыжок.<br />
<br />
Но!!!<br />
Но в процедуре 53B6, как было показано ранее, мусор.<br />
<br />
На самом деле там должен быть не только мусор. По всей видимости мы имеем дело с так называемым мусорным полиморфиком, причем в самой простейшей его реализации.<br />
<br />
Задача полиморфного движка преобразовать изначальный код заменой оригинальных инструкций на их аналоги (или группы аналогов). С целью затруднения анализа результирующего кода как правило добавляются мусорные блоки инструкций.<br />
Здесь же блоков не наблюдается, генерируются просто мусорные инструкции, плюс в итоге даже если и была замена инструкций на аналоги, то я заметил это только в одном случае. Вполне возможно что тут был применен просто генератор мусора, обильно напихавший его между полезными инструкциями, как знать...<br />
<br />
Впрочем, появилась задача - надо среди всего этого мусора от адреса 4053B6 по 406839 (5251 байт - однако) найти полезные инструкции, которые осуществляют декрипт тела приложения.<br />
<br />
Сделать это можно двумя способами.<br />
Первый - просмотреть весь код глазками и попытаться найти такие инструкции. Я даже ради интереса попробовал и потратил около 7 минут, в результате даже нашел две таких инструкции, не являющихся мусором. Правда, как оказалось в последствии, одну между ними пропустил, да и после второй найденной дальше искать как-то расхотелось - слишком уж утомительное занятие :)<br />
<br />
Поэтому пойдем вторым путем, и напишем небольшой скрипт, который поможет убрать весь мусор и оставит только полезную нагрузку.<br />
<br />
Сам скрипт расположен <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">в архиве, идущем со статьей</a> по следующему пути: ".\scripts\fill_trash_by_nop.txt".<br />
Для его запуска должен быть установлен плагин OllyScript.<br />
<br />
Запускается скрипт так: необходимо перезапустить keygenme в отладчике и дождаться срабатывания первого ВР внутри NTDLL, после чего в меню "Plugins" выбрать пункт "ODbgScript->Run Script...", в диалоге выбрать файл со скриптом (путь указан выше) и запустить его.<br />
<br />
Как только скрипт начнет свою работу можно сходить приготовить себе чай, минут пять свободного времени у вас будет.<br />
<br />
Логика работы скрипта проста:<br />
Т.к. мусорные инструкции не изменяют значений регистров (за исключением EIP), то детектирование мусорной инструкции происходит сверкой состояния регистров до и после ее выполнения, если регистры изменились - инструкция выполняет что-то полезное, в противном случае инструкция считается мусорной и вместо нее размещается NOP.<br />
<br />
Когда скрипт завершит свою работу и выведет сообщение, можно просмотреть результаты его работы (сам отладчик не останавливайте - он еще нужен).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizKxKBmPkF3gcYpJBx1TKmmZ0v3kVXTITzS8rc2yMEL7R1cnaQ4UYkbXqRr9qDNd2CYeRvYugVEvjn_z48S5BXCe33CVlmJ-2tSO_ZDloc76g1xRXk-u4ie3Tsp_4cweUeAfBO2HIuNhA/s1600/00.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizKxKBmPkF3gcYpJBx1TKmmZ0v3kVXTITzS8rc2yMEL7R1cnaQ4UYkbXqRr9qDNd2CYeRvYugVEvjn_z48S5BXCe33CVlmJ-2tSO_ZDloc76g1xRXk-u4ie3Tsp_4cweUeAfBO2HIuNhA/s1600/00.png" /></a></div>
<br />
<br />
Весь мусор будет заменен на NOP и на руках у нас останутся только следующие инструкции (нужно пробежаться от 4053B6 по 406839 и выписать в блокнотик все что не NOP):<br />
<br />
Первыми двумя строчками будет немного мусора (вызывается sleep с нулевой задержкой).<br />
<br />
<pre class="brush:asm">0040548B PUSH 0
0040548D CALL DWORD PTR DS:[<&kernel32.Sleep>] ; kernel32.Sleep
</pre>
<br />
Ну точнее как - это не совсем мусор, эта строчка заставляет загрузчик при старте процесса подгружать kernel32.dll в адресное пространство процесса, в пару к уже загруженной ntdll.dll, т.к. эта библиотека заявлена в таблице импорта keygenme (как раз в виде одной единственной функции sleep).<br />
<br />
Далее пойдет сам код декриптора:<br />
<br />
<pre class="brush:asm">004054B4 MOV ESI,keygenme.00401000 // в ESI помещаем указатель на зашифрованный буфер
0040559D MOV EDI,ESI // в EDI на результирующий
// они равны т.е. расшиврованные данные помещаются туда же
00405677 MOV ECX,1058 // устанавливаем количество итераций цикла.
// Всего расшифруется 16736 байт,
// т.к. зачитываем блоками по 4 байта ($1058 * 4)
004057FE LODS DWORD PTR DS:[ESI] // читаем 4 байта
00405904 NEG EAX // умножаем на -1
00405B69 NOT EAX // выполняем операцию NOT,
// в результате просто уменьшаем EAX на 1
// (NEG + NOT = DEC)
00405D3A BSWAP EAX // инвертируем байты
00405E90 SUB EAX,4FE62125 // отнимаем 0x4FE62125
00406121 XOR EAX,12345 // ксорим на 0x12345
00406256 STOS DWORD PTR ES:[EDI] // результат помещаем обратно
00406442 DEC ECX // уменьшаем значение счетчика итераций
004065C8 JNZ keygenme.004057D9 // переходим в начало цикла (на инструкцию 004057FE LODS)
</pre>
<div>
<br /></div>
и непосредственно переход на OEP на котором сейчас остановился отладчик.<br />
<pre class="brush:asm">00406839 JMP keygenme.00401000
</pre>
<br />
Грубо говоря, если посмотреть инструкции декриптора, тело keygenme расшифровывается вот таким простым алгоритмом:<br />
<br />
<pre class="brush:delphi">uses
Classes,
Winsock;
var
I, A: Integer;
M: TMemoryStream;
begin
M := TMemoryStream.Create;
try
M.LoadFromFile('keygenme.exe');
M.Position := 512; // после выравнивания позиция данных байтов будет $1000
for I := 0 to $1058 - 1 do
begin
M.ReadBuffer(A, 4); // LODS
Dec(A); // NEG + NOT
A := htonl(A); // BSWAP
Dec(A, $4FE62125); // SUB
A := A xor $12345; // XOR
M.Position := M.Position - 4;
M.WriteBuffer(A, 4); // STOS
end;
M.SaveToFile('keygenme.exe');
finally
M.Free;
end;
end.
</pre>
<br />
Теперь, чтобы каждый раз не ждать расшифровки файла, необходимо сдампить полученный результат (должен быть установлен плагин OllyDump).<br />
<br />
Для этого необходимо перейти на OEP ("Ctrl+G" 401000) и поставить там брякпойнт, после чего продолжить выполнение программы.<br />
Как только отладчик остановится на установленном ВР, идем в меню "Plugins", там выбираем "OllyDump->Dump debugged process", откроется вот такой диалог:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFMNLKIb7cGSvM0gP6O20rCk2TOEcW-qXOBGRms4QAHAhz3JmG0FfNBAAtlptbk6SZK1dQBKJNayM39zgOFWhyphenhyphenaqQIpXS6MDkM-y5BXF4f9lkZOB_TQW_KVeXLPDNUBq4EAlpK6d3uZ9g/s1600/8.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFMNLKIb7cGSvM0gP6O20rCk2TOEcW-qXOBGRms4QAHAhz3JmG0FfNBAAtlptbk6SZK1dQBKJNayM39zgOFWhyphenhyphenaqQIpXS6MDkM-y5BXF4f9lkZOB_TQW_KVeXLPDNUBq4EAlpK6d3uZ9g/s1600/8.png" /></a></div>
<br />
Ориентируясь на колонку "Virtual Offset" выставляем базу кода равную 1000, а базу данных равную 7000, снимаем галку "Rebuil import" и нажимаем кнопку "Dump".<br />
В появившемся диалоге указываем новое имя "keygen_unpacked.exe".<br />
<br />
Собственно все - вот мы и сняли первый конверт.<br />
<br />
<b>Небольшая хитрость:</b><br />
<b><br /></b>
Вообще сдампить можно было гораздо проще, без рассмотрения исходного кода декриптора и прочего, но раз уж я решил рассматривать все досконально, поэтому на нем тоже нужно было остановиться.<br />
<br />
Второй вариант распаковки выглядит следующим образом.<br />
1. Запускаем отладчик и ждем срабатывания первого ВР внутри NTDLL.<br />
2. Переходим на вкладку карты памяти "Alt+M" и на адресе 401000 ставим MBP на запись:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-TqO3_t6LtXO7y04SpzvkLx29v-n3nd0Mr8xP2CixN9K54umnQUMxsBW8OUI5tPqQqJC1zzW8hrHF0FNAWoYIAFtkWXcoMK9-Zo8cMl61xJuDQr0NlTLqbvOeABKDD6Pn_wAV3sCDsOE/s1600/9.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-TqO3_t6LtXO7y04SpzvkLx29v-n3nd0Mr8xP2CixN9K54umnQUMxsBW8OUI5tPqQqJC1zzW8hrHF0FNAWoYIAFtkWXcoMK9-Zo8cMl61xJuDQr0NlTLqbvOeABKDD6Pn_wAV3sCDsOE/s1600/9.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
3. Запускаем программу на выполнение, как только прервались на операции записи (это будет инструкция STOS DWORD), опять идем на карту памяти и снимаем MBP, после чего идем на OEP (401000) и там ставим обычный брякпойнт.<br />
4. Ну а как только прервемся на нем - нужно выполнить уже описанные шаги по дампу процесса.<br />
<br />
Кстати, если хотите, можете проверить получившийся файл под PEiD, энтропия волшебным образом стала 6.95 - а всего-то просто расшифровали блок данных.<br />
<br />
<h3 style="text-align: left;">
4. Первичный анализ распакованного файла и обход проблемы запуска под Vista и выше.</h3>
<br />
Теперь будем работать с уже распакованным файлом.<br />
Так как точка входа в нем выставлена правильная, то, чтобы не совершать лишних телодвижений, нужно настроить Olly сделать первую остановку уже не в NTDLL, а непосредственно на точке входа.<br />
Открываем OllyDebug, заходим в меню "Options", там выбираем "Debugging options" и на вкладке "Events" выставляем галку "Make first pause at: -> Entry point of main module".<br />
<br />
Открываем keygenme_unpacked.exe и смотрим во что превратился ранее зашифрованный код:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuEDQiZolnu21hX9ikvISnYpSu52ciPFrktifoQY4FoHNIpIwncG9N2l-9vfP9mrjhRheaJ5Nnc4GqQ-hUV4wrGETjHSZAsrOFiMyQi9YsH8yUPhqhdZ_xD7V4PoKA7ZmiynV_Lzhmz6M/s1600/10.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuEDQiZolnu21hX9ikvISnYpSu52ciPFrktifoQY4FoHNIpIwncG9N2l-9vfP9mrjhRheaJ5Nnc4GqQ-hUV4wrGETjHSZAsrOFiMyQi9YsH8yUPhqhdZ_xD7V4PoKA7ZmiynV_Lzhmz6M/s1600/10.png" /></a></div>
<br />
Сразу видим первую "неприятность", первый же вызов CALL идет внутрь самого себя (вызывается адрес 401004, в то время как следующая инструкция начинается только с 4010005).<br />
<br />
О таких прыжках я уже рассказывал ранее: <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-2.html" target="_blank">http://alexander-bagel.blogspot.ru/2012/11/debuger-2.html</a><br />
<br />
Суть такого трюка - запутать дизассемблер и заставить его отобразить не тот код, который будет выполнятся на самом деле. Ничего страшного в таком "трюке нет", просто нажимаем F7 выполняя этот CALL и сразу видим правильный код:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiavKlYwuMmTw5tV7oTucFxnY_I75u0xRodKuqWA8NeXqK5VhCUw9XxgJEdlhEz0ddcoBN4Tv29tfSqBgKTx_xqs40WET0aUrzRua5P5tb0XLtLlXFAFjzU2C3zy-VNUvT8_MmyxLQ2xd0/s1600/11.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiavKlYwuMmTw5tV7oTucFxnY_I75u0xRodKuqWA8NeXqK5VhCUw9XxgJEdlhEz0ddcoBN4Tv29tfSqBgKTx_xqs40WET0aUrzRua5P5tb0XLtLlXFAFjzU2C3zy-VNUvT8_MmyxLQ2xd0/s1600/11.png" /></a></div>
<br />
Можно прямо сейчас еще раз сдампить процесс + убрать инструкцию POP EBX для отключения такого "фокуса", но т.к. мешать он не будет - оставим как есть и начинаем анализировать.<br />
<br />
Сперва идет блок из пяти инструкций, зачитывающий некие данные из PEB (Process Environment Block), адрес которого всегда расположен в FS:[$30].<br />
Если открыть структуру PEB и посмотреть что означают показанные в коде оффсеты, то получим на руки примерно следующее:<br />
<br />
<pre class="brush:asm">0040100A MOV EAX,DWORD PTR FS:[30] // получаем указатель на PEB
00401010 MOV EAX,DWORD PTR DS:[EAX+C] // читаем значение на PEB->LoaderData
00401013 MOV EAX,DWORD PTR DS:[EAX+1C] // читаем LoaderData->InInitOrder
00401016 MOV EAX,DWORD PTR DS:[EAX] // переходим на структуру _LDR_DATA_TABLE_ENTRY
00401018 MOV EAX,DWORD PTR DS:[EAX+8] // читаем поле DllBase
</pre>
<br />
Таким образом эти пять инструкций ищут hInstance "kernel32.dll", которая будет располагаться по данному адресу, правда под Vista и выше по данному адресу будет расположен hInstance "kernelbase.dll" и с этим будет связана одна неприятная ошибочка.<br />
<br />
Инструкция LEA ESI, помещает в ESI указатель на небольшой массив из Ansi строк, размещенных по адресу 004012DE. Это три строки, разделенные нулями: "LoadLibraryA", "ExitProcess" и "VirtualAlloc".<br />
<br />
Кстати, совершенно забыл об этом упомянуть ранее, если вы посмотрите на таблицу импорта keygenme.exe то увидите что он импортирует одну единственную функцию kernel32.sleep, остальные отсутствуют. Значит адреса остальных, необходимых для работы, приложение должно найти самостоятельно.<br />
<br />
Следующая инструкция LEA EDI, помещает в EDI указатель на буфер, в который будет помещаться адреса найденных функций (это будет виртуальная таблица импорта для kernel32), после чего происходит вызов процедуры по адресу 401198.<br />
<br />
На самом деле оба вызова LEA EDI/ESI являются мусором, т.к. эти регистры перезатрутся при вызове процедуры по адресу 401198, но использоваться в ней они будут как раз таким образом, как я описал выше (EDI, точнее EBP+305, в итоге будет содержать адреса функций).<br />
<br />
Вкратце, задача процедуры 401198 подготовить регистр ESI, в котором помещается указатель на имя искомой функции, а также регистр EDI в котором размещается указатель на таблицу экспорта библиотеки, hInstance которой мы получили, считав данные из PEB,<br />
после чего вызвать функцию по адресу 4011E2, которая и будет производить поиск по имени.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_Xlg-GXDVdnKkC3NYrhQepIP5qpuCaCM1PSmLA3L-RO0BT1q5xlta4jXgqeaQhiJ-NyrTV_1STqt6CYSbDliUOg-9drtBe-CNPXKJhhCZ_bQxmnEzZQRn5i4cwc4lluBGnVFaVerhQIc/s1600/12.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_Xlg-GXDVdnKkC3NYrhQepIP5qpuCaCM1PSmLA3L-RO0BT1q5xlta4jXgqeaQhiJ-NyrTV_1STqt6CYSbDliUOg-9drtBe-CNPXKJhhCZ_bQxmnEzZQRn5i4cwc4lluBGnVFaVerhQIc/s1600/12.png" /></a></div>
<br />
И вот тут-то нас ждет вторая неприятность, гораздо более серьезная чем трюк с CALL в самом начале.<br />
<br />
Самой первой будет искаться "LoadLibraryA", которую "kernelbase.dll" не экспортирует.<br />
Это означает то, что под Windows Vista и выше данный keygenme работать не будет и будет падать при старте.<br />
<br />
Можете проверить, исключение поднимется вот тут:<br />
<br />
<pre class="brush:asm">004011EB CMPS BYTE PTR DS:[ESI],BYTE PTR ES:[EDI]
</pre>
<br />
Обойти это достаточно просто, достаточно после старта keygenme, поставить ВР на инструкцию:<br />
<br />
<pre class="brush:asm">0040101B LEA ESI,DWORD PTR SS:[EBP+2DE]
</pre>
<br />
т.е. сразу после получения адреса загрузки библиотеки, и подменить значение в регистре EAX на hInstance библиотеки "kernel32.dll" (правильный адрес можно подсмотреть в карте памяти процесса Alt+M).<br />
<br />
После таких манипуляций keygenme запустится штатным образом.<br />
<br />
Чтобы такого не делать каждый раз при старте приложения, достаточно будет запустить скрипт из папки ".\scripts\run_at_vista.txt", который будет в автоматическом режиме каждый раз при старте подменять значение EAX на правильное и запускать программу без ошибок.<br />
<br />
<h3 style="text-align: left;">
5. Чтение логина и серийного номера</h3>
<br />
Теперь пришло время посмотреть, каким образом производится чтение логина и серийного номера в память приложения и какие модификации над ними производятся.<br />
<br />
Обычно чтение данных из EDIT происходит посредством функции "GetWindowsText" или "GetDlgItemText", но т.к. вторая функция в итоге все равно вызывает первую, то ставить брякпойнт мы будем именно на "GetWindowsText".<br />
<br />
Для этого, после того как keygenme запустился (а также подгрузилась библиотека user32.dll) и появилось его диалоговое окно, переходим в отладчик и ищем все доступные функции во всех модулях:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjwzSx3T7qIJK8xCyl591J_iM54biliVY83kNJ12nAEUgwHWb3D-QwBenXHIUTFemkKUgSeUIhSYmL_NKfKAJwM318z9TAPGy4wUbDAE8fbXGwlwu03a2Ebl13LiGyK-1CR3ilMR9lbXE/s1600/14.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjwzSx3T7qIJK8xCyl591J_iM54biliVY83kNJ12nAEUgwHWb3D-QwBenXHIUTFemkKUgSeUIhSYmL_NKfKAJwM318z9TAPGy4wUbDAE8fbXGwlwu03a2Ebl13LiGyK-1CR3ilMR9lbXE/s1600/14.png" height="384" width="640" /></a></div>
<br />
В появившемся диалоге ищем имя экспортируемой функции "GetWindowsTextA", и в меню выбираем пункт "Follow in Disassembler":<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhr64TphP7WWBMYArP9IfsbaD8hOXTEzYQAptnfYwV_5XTSE-i1VxfWvDpFJ-BkevWMiUx_8w7cWuOSwxIAH9amfmdFsHFr1GWznXU9uZYw76z5e55jaShtvnDx-_lQvWJ3be3_GefwpSI/s1600/15.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhr64TphP7WWBMYArP9IfsbaD8hOXTEzYQAptnfYwV_5XTSE-i1VxfWvDpFJ-BkevWMiUx_8w7cWuOSwxIAH9amfmdFsHFr1GWznXU9uZYw76z5e55jaShtvnDx-_lQvWJ3be3_GefwpSI/s1600/15.png" /></a></div>
<br />
После этого ставим ВР, переходим в диалог с keygenme и в соответствующие поля вбиваем известные нам логин "Ms-Rem" и серийный номер "C38FB7A0CF38F73B1159".<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdIIK02ctougNPBlxj9ikLhGls7ylaLJgSVn7XfGV2UpGMv_Aot3G8D1s8Hg9EjY5fBjAMXLJBM6pkXt_GuQNnK8bGmbyCXPHgzDvjY76fgzfC_VsBQ5VJPF1NEGsxt8XY2Mir4ENsaEc/s1600/16.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdIIK02ctougNPBlxj9ikLhGls7ylaLJgSVn7XfGV2UpGMv_Aot3G8D1s8Hg9EjY5fBjAMXLJBM6pkXt_GuQNnK8bGmbyCXPHgzDvjY76fgzfC_VsBQ5VJPF1NEGsxt8XY2Mir4ENsaEc/s1600/16.png" /></a></div>
<br />
Нажимаем кнопку "Check" и... останавливаемся на бряке внутри user32 как раз на начале функции "GetWindowsTextA".<br />
<br />
Теперь необходимо вернуться к месту ее вызова.<br />
<br />
Нажимаем:<br />
1. Ctrl+Shift+F9 - прыгая в конец функции "GetWindowsTextA"<br />
2. F8 - выходим наверх в функцию "GetDlgItemText"<br />
3. Ctrl+Shift+F9 - прыгая в конец функции "GetDlgItemText"<br />
4. F8 - выходим наверх в место вызова<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRL1Qypqo4FJJXyrH0UBUQsJbuKfLG33gi6_PEMfhaZVU47fR5KldXFVJFMIVKmxtgTaSRY1StMBkFrloVRsxArrQf0Y9dSHYJX1ikoEdcJrZeX90HEHNgp1Jxs-KlUUEYpgs3jPfLHbE/s1600/17.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRL1Qypqo4FJJXyrH0UBUQsJbuKfLG33gi6_PEMfhaZVU47fR5KldXFVJFMIVKmxtgTaSRY1StMBkFrloVRsxArrQf0Y9dSHYJX1ikoEdcJrZeX90HEHNgp1Jxs-KlUUEYpgs3jPfLHbE/s1600/17.png" /></a></div>
<br />
В синей рамке содержится только что произведенный вызов "GetDlgItemText", со следующими параметрами:<br />
<br />
hDlg = ESI<br />
nIDDlgItem = 65<br />
lpString = EAX<br />
nMaxCount = $10<br />
<br />
Это мы прочитали значение логина в буфер, на который указывал регистр EAX размером в 16 байт, а в красной рамке выделен вызов чтения серийного номера в буфер, на который будет указывать EDI (EBP+414E) размером в 32 байта.<br />
<br />
Самое интересное начинается сразу после чтения серийного номера.<br />
За ним идет интересный цикл из 10 итераций, при этом регистр ESI указывает на буфер с только что считанным серийным номером, а EDI на буфер, куда помещается результат:<br />
<br />
<pre class="brush:asm">00401111 MOV ECX,0A // выставляем счетчик цикла
00401116 LODS WORD PTR DS:[ESI] // читаем два символа серийного номера
00401118 CALL keygenme.0040117B // вызываем функцию
0040111D STOS BYTE PTR ES:[EDI] // сохраняем 1 байт
0040111E LOOPD SHORT keygenme.00401116 // переход на следующую итерацию
</pre>
<br />
Т.е. над серийным номером в функции 40117B производятся какие то преобразования, результат которых помещается в EDI.<br />
<br />
Данная функция выглядит следующим образом:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDmCGwlOb7TsNcA0vvWTRxv6Byw35LJDDhla0ldAEpha4ETTPYeHcEBeCk9RcQFDiEi6grW-cQGpQJJ274ck6EiGYIRsokCSUleupoyu5FOJKFPndYkgy4Ac-WU_hvfBBLBBqqYZf9yUA/s1600/18.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDmCGwlOb7TsNcA0vvWTRxv6Byw35LJDDhla0ldAEpha4ETTPYeHcEBeCk9RcQFDiEi6grW-cQGpQJJ274ck6EiGYIRsokCSUleupoyu5FOJKFPndYkgy4Ac-WU_hvfBBLBBqqYZf9yUA/s1600/18.png" /></a></div>
<br />
Это что-то типа преобразования двух символов из строкового HEX представление в байт.<br />
Если грубо то это аналог Result := StrToInt('$' + Value);<br />
Возьмем к примеру изначальный известный серийный номер: "C38FB7A0CF38F73B1159"<br />
<br />
После 10 итераций он будет преобразован в массив с таким содержимым:<br />
<br />
<pre class="brush:delphi">var
sn: array [0..9] of Byte = ($C3, $8F, $B7, $A0, $CF, $38, $F7, $3B, $11, $59);
</pre>
<br />
Но т.к. в функции не производится проверки на границы, за которые не должны выходить HEX значения в строковом формате, то есть очень большой диапазон допустимых значений, которые после такого приведения дадут одно и то же число.<br />
<br />
К примеру что "C3" что "s1" в результате такого преобразования будут равны 195 (или $C3). Поэтому вот это тоже будет вполне себе валидным серийным номером: "<span style="color: blue;"><b>s1</b></span>8FB7A0CF38F73B1159".<br />
<br />
Таким образом делаем вывод: введенный логин зачитывается как есть, а серийный номер после чтения преобразовывается в байтовое представление, причем, т.к. итераций всего 10, то длина серийного номера не должна превышать 20 HEX символов (остальное не будет учитываться).<br />
<br />
В принципе эта вся информация, которая нам потребуется, теперь пришло время посмотреть на исходный код keygenme немного под другим углом :)<br />
<br />
<h3 style="text-align: left;">
6. Анализируем keygenme под IDA Pro, разбираем VM и получаем ее PiCode</h3>
<br />
Запускаем IDA Pro и открываем в нем keygenme_unpacked.exe, сразу после открытия идем на вкладку "Functions".<br />
<br />
Всего-то 8 штук:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwA5797IMM3TLtaIIsnwj3wTaiaabufgEH-Par-A3eE1PzfUI7Lzqp41Lqeo8CS33EkRabsWOtA4biz_7NC9Pgy21nIKjaFJ19AJrn25w52Nt2UqBGHVT8DGJ52Gbawd8v6PoqvW6dCMY/s1600/19.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwA5797IMM3TLtaIIsnwj3wTaiaabufgEH-Par-A3eE1PzfUI7Lzqp41Lqeo8CS33EkRabsWOtA4biz_7NC9Pgy21nIKjaFJ19AJrn25w52Nt2UqBGHVT8DGJ52Gbawd8v6PoqvW6dCMY/s1600/19.png" /></a></div>
<br />
Причем практически все нам известные:<br />
401000 - это OEP, не интересно<br />
<br />
401096 - а это похоже изначальная точка входа, которая была до того, как навесили всякое там шифрование и создание виртуальных IAT, но, впрочем, теперь она нам уже не нужна.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg80imU2psRJtQFhvgUNyxYdRkfiFHd7nuY9GL5bmdn1mqupilWBodicpJs3iGPx1D6rYhKh7W94E9HJoAcr1lMfhWVVpB9z78pkv8RSJ_pqeRLmZvqikdxX8_dkEji1F9ANDddjd9PN5Q/s1600/20.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg80imU2psRJtQFhvgUNyxYdRkfiFHd7nuY9GL5bmdn1mqupilWBodicpJs3iGPx1D6rYhKh7W94E9HJoAcr1lMfhWVVpB9z78pkv8RSJ_pqeRLmZvqikdxX8_dkEji1F9ANDddjd9PN5Q/s1600/20.png" height="320" width="190" /></a></div>
<br />
40117B - это HexToInt, видели...<br />
401198 - заполнение виртуальной IAT, видели...<br />
4011E2 - поиск адреса функции по имени, видели...<br />
<br />
sub_401204 - что-то интересное (судя по графу), с ней пожалуй и начнем, кстати она вызывает две оставшиеся функции sub_401257 и sub_401276.<br />
<br />
А вызовы функций чтения из EDIT-ов по адресам 004010F5 и 00401107, а также цикла по адресу 00401111, IDA Pro не распознала как процедуры из-за мусорных инструкций идущих перед ними (да и не столь важно).<br />
<br />
Итак, смотрим граф функции sub_401204:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEglaLtg0y9Rl9YA5BjjQk8xdni_QpX9B3lomGwTSYikVHIT_c3hA7TSmPotoKJyByNdxyDPS63BR6SWJkb_hrb3logHCrz5Ux4PJMXLo3faHm9CnRJb72KIxG6wRGJY1A6XA4lbBFqa6Gc/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEglaLtg0y9Rl9YA5BjjQk8xdni_QpX9B3lomGwTSYikVHIT_c3hA7TSmPotoKJyByNdxyDPS63BR6SWJkb_hrb3logHCrz5Ux4PJMXLo3faHm9CnRJb72KIxG6wRGJY1A6XA4lbBFqa6Gc/s1600/3.png" /></a></div>
<br />
Видели когда нибудь как выглядит граф VM в IDA?<br />
Если нет, то смотрите - это и есть тело виртуальной машины, причем достаточно простой.<br />
<br />
Что есть виртуальная машина?<br />
Грубо... обычный процессор выполняет набор известных ему инструкций (машкод, который можно дизассемблировать).<br />
Виртуальная машина - по сути это тоже процессор, только выполняет она свой набор инструкций, которые генерируются и размещаются где-то в доступной ей области памяти в виде так называемого PiCode.<br />
Совсем не обязательно что эти инструкции совпадут с теми, что может исполнить реальный процессор (точнее наоборот - в большинстве случаев они как раз не будут совпадать).<br />
<br />
В процессе защиты, коммерческие протекторы дизассемблируют защищаемые блоки кода, генерируют для каждого из них свою виртуальную машину с уникальным набором логики и набором инструкций, переводят дизассемблированный код в пикод, который может выполнить конкретная виртуальная машина и сохраняют результат в виде буфера где-то в теле приложения. Каждая VM, при получении управления в процессе выполнения программы, последовательно зачитывает инструкции пикода, предназначенные только для нее, и исполняет их.<br />
<br />
Таким образом от взломщика прячется логика защищаемого алгоритма, которую он мог бы разобрать под отладчиком. После таких модификаций придется сначала проанализировать каждую VM, и только после ее полного анализа вытащить алгоритм из исполняемого ей пикода, преобразовав его обратно в понятный обычному процессору машкод. Работенка та еще...<br />
<br />
На картинке выше мы видим как раз одну из реализаций VM, которая может выполнить всего 8 известных ей инструкций.<br />
<br />
Давайте рассмотрим поподробнее:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgz7zeMlPjdUUip8zxRGbNRCeC0pipsYiB3Bp9eBt1sv9okFthXmpJYzabYCTP10du0lLcQzFGNg26tNwt2Z56q1AmNacMI_9BlSCnXmvO-7F0HaR-PKGEwG_oyfZvl3Dtp202slLwUB88/s1600/21.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgz7zeMlPjdUUip8zxRGbNRCeC0pipsYiB3Bp9eBt1sv9okFthXmpJYzabYCTP10du0lLcQzFGNg26tNwt2Z56q1AmNacMI_9BlSCnXmvO-7F0HaR-PKGEwG_oyfZvl3Dtp202slLwUB88/s1600/21.png" /></a></div>
<br />
Все начинается с инициализации регистра EBX, который указывает на начало буфера с пикодом для виртуальной машины, а так-же регистра EAX, который является курсором (индексом исполняемой инструкции).<br />
<br />
Работа VM начинается с процедуры loc_40120D.<br />
<br />
Ее задача, сначала получить опкод выполняемой инструкции, вызовом функции sub_401276, код которой приведен в хинте.<br />
<br />
Судя по этому коду можно понять, что сам пикод тоже зашифрован и сразу после считывания каждого байта происходит его декрипт примерно вот таким алгоритмом:<br />
<br />
<pre class="brush:delphi">var
A, B: Byte;
...
A := PicodeBuff[I];
B := A;
B := B shr 4;
A := A xor B;
Result := A and 7;
</pre>
<br />
После получения опкода происходит его проверка, если он равен нулю, то передается управление вот сюда:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCytAD3P-BjCCKqkdHJ1EbnA3urFcRIucF-H3XMb_OoM4hf-IleRiaZWbPfwSdy74JotMDTyXa8j3h61dkqAz8vPuoNnMcn41xqjSCuGGxCoJzIRikmETRC6_Ld-nZcq9mtGr3J6F3TgU/s1600/22.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCytAD3P-BjCCKqkdHJ1EbnA3urFcRIucF-H3XMb_OoM4hf-IleRiaZWbPfwSdy74JotMDTyXa8j3h61dkqAz8vPuoNnMcn41xqjSCuGGxCoJzIRikmETRC6_Ld-nZcq9mtGr3J6F3TgU/s1600/22.png" /></a></div>
<br />
Где просто прибавляется единица какой-то переменной (назовем ее как есть arg_4)<br />
<br />
И в итоге дальше управление перелается на финализирующий блок, который запускает исполнение следующего опкода, инкрементируя регистр EAX.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgL4LQwpcEGHBCun5sm9f_DpJ8tEBAt3n_qymtWpc6y4Pi75V7YgJwteC1tCNQkO1lx0xZAFXVJ8xigAVHOI3DXimh5EEsKdgHBeTXsrFXuUSd2Tc6gzPQxBVQCnfwY2iZx5wWZKJNCYHs/s1600/23.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgL4LQwpcEGHBCun5sm9f_DpJ8tEBAt3n_qymtWpc6y4Pi75V7YgJwteC1tCNQkO1lx0xZAFXVJ8xigAVHOI3DXimh5EEsKdgHBeTXsrFXuUSd2Tc6gzPQxBVQCnfwY2iZx5wWZKJNCYHs/s1600/23.png" /></a></div>
<br />
Причем в нем же мы можем узнать общий размер пикода, он равен $3DA2 (15778 байт).<br />
<br />
Итак, разберем все инструкции по порядку:<br />
<br />
0. (loc_4012CA): увеличивает значение переменной "arg_4"<br />
1. (loc_4012C5): уменьшает значение переменной "arg_4"<br />
2. (loc_4012BE): увеличивает значение, на которое указывает "arg_4"<br />
3. (loc_4012B7): уменьшает значение, на которое указывает "arg_4"<br />
4. (loc_4012A8): помещает значение, на которое указывает "arg_4" в память на которую указывает "arg_8", после чего увеличивает значение переменной "arg_8"<br />
5 (loc_401299):. помещает значение, на которое указывает "arg_С" в память на которую указывает "arg_4", после чего увеличивает значение переменной "arg_С"<br />
6. (loc_401284): проверяет значение, на которое указывает "arg_4", и если оно равно нулю, запускает процедуру "sub_401257" передавая в EDX значение 1<br />
7. (0040123E): проверяет значение, на которое указывает "arg_4", и если оно не равно нулю, запускает процедуру "sub_401257" передавая в EDX значение -1<br />
<br />
А в процедуре "sub_401257" происходит следующее, EDX является направлением сканирования пикода, в случае положительного значения ищется опкод номер 7, соответствующий опкоду номер 6 с учетом вложенности (т.е. если идут 066770 то при вызове из первого опкода 6 курсор EAX установится на нуле, а при вызове из второго опкода 6, EAX будет указывать на вторую семерку).<br />
А в случае если EDX = -1, то сканирование идет в обратную сторону так-же с учетом вложенности.<br />
<br />
Вот собственно и вся VM.<br />
Ничего не напоминает?<br />
<br />
Угу - это собственно Brainfuck как он есть.<br />
<a href="http://ru.wikipedia.org/wiki/Brainfuck" target="_blank">http://ru.wikipedia.org/wiki/Brainfuck</a><br />
<br />
Т.е. получается что внутри keygenme расположен зашифрованный PiCode, который должен быть исполнен на интерпретаторе Brainfuck, который собственно и выступает в качестве виртуальной машины (ну а почему бы и нет?)<br />
Кстати, если вы обратите внимание на описание BF в википедии и реализацию обработчиков в данном варианте интерпретатора, то увидите что четвертый и пятый опкоды (чтение и запись) перепутаны местами.<br />
<br />
Ну а раз так, все что нам осталось, это вытащить из тела keygenme сам пикод и больше нам keygenme не нужен, дальше мы сами.<br />
<br />
Для того чтобы определить расположение буфера с пикодом, поставим ВР на начале VM и посмотрим на адрес, которым инициализируется EBX.<br />
<br />
Запускаем OllyDebug и в нем ставим ВР на адресе 401208.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUfjbJRUfTlHyDe75GLQvdzYZiyyrTTG4nyCh5bHYmgrd7Op7CtMr93kZcuWFA_cJN0C8pkhEfvFEsrFpy5jOX7anF6Lw6friSo60u-IOmKx8nlP6w0Io-CJW4MyazPKevAVNV-WVchfQ/s1600/24.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUfjbJRUfTlHyDe75GLQvdzYZiyyrTTG4nyCh5bHYmgrd7Op7CtMr93kZcuWFA_cJN0C8pkhEfvFEsrFpy5jOX7anF6Lw6friSo60u-IOmKx8nlP6w0Io-CJW4MyazPKevAVNV-WVchfQ/s1600/24.png" /></a></div>
<br />
После срабатывания BP смотрим на значение EBX, это адрес 401380.<br />
Переходим на него в окне дампа и смотрим на HEX значения, первые 8 байт равны "CE44 4E53101708DD".<br />
Теперь открываем keygenme_unpacked.exe в любом HEX редакторе и ищем эти 8 байт.<br />
Размер VM, как мы выяснили ранее 15778 байт.<br />
Копируем 15778 байт начиная с найденных в файл "vm.mem".<br />
<br />
Отлично, теперь у нас на руках есть PiCode виртуальной машины, с которым нам предстоит долгая и упорная работа, а сам keygenme вместе с OllyDebug и IDA Pro нам больше не нужны, они свое отработали.<br />
<br />
ЗЫ: уже скопированный файл "vm.mem" доступен <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">в архиве с примерами к статье</a> и расположен в папке ".\data\vm.mem".<br />
<br />
Кстати.<br />
В процессе вычитки статьи мне указали на такой момент: в реальном боевом приложении количество функций будет на несколько порядков больше, а как в этом случае определить тело виртуальной машины?<br />
В данной ситуации достаточно будет установить ВР на зачитку внешних данных (логина и серийного номера), от которого будет достаточно легко отследить переход на один из хэндлеров виртуальной машины, либо те же действия выполнить с выходным буфером (ведь что-то VM должна делать и иметь взаимодействие с внешней средой).<br />
Но все это, конечно, зависит от конкретной реализации VM и не всегда этот подход применим.<br />
<br />
<h3 style="text-align: left;">
7. Пишем собственный интерпретатор Brainfuck</h3>
<br />
Для начала декодируем полученный "vm.mem" в нормальное представление примерно вот таким кодом:<br />
<br />
<pre class="brush:delphi">const
BrainFuckOpcode: array [0..7] of AnsiChar = ('>', '<', '+', '-', ',', '.', '[', ']');
const
PicodeBuffSize = 15778;
var
PicodeBuff: array [0..PicodeBuffSize - 1] of Byte;
M: TMemoryStream;
I: Integer;
A, B: Byte;
begin
M := TMemoryStream.Create;
try
M.LoadFromFile('..\..\data\vm.mem');
M.ReadBuffer(PicodeBuff[0], PicodeBuffSize);
for I := 0 to PicodeBuffSize - 1 do
begin
A := PicodeBuff[I];
B := A;
B := B shr 4;
A := A xor B;
PicodeBuff[I] := Byte(BrainFuckOpcode[A and 7]);
end;
M.Clear;
M.WriteBuffer(PicodeBuff[0], PicodeBuffSize);
M.SaveToFile('..\..\data\vm.brainfuck');
finally
M.Free;
end;
</pre>
<br />
Получится файл "vm.brainfuck" содержащий код BF в том виде, в котором он обычно и записывается, причем тут уже учтено что инструкции "." и "," перепутаны местами.<br />
Этот файл в принципе уже можно скармливать любому интерпретатору BF и если подсунуть правильный буфер с логином и серийным номером - он даже выполнится :)<br />
<br />
Кстати про буфер с логином и серийным номером - совсем забыл про это упомянуть. Он идет на вход виртуальной машине в виде блока из 20 байт, где первые 10 байт заполнены символами логина (если логин меньше 10 байт, остальные байты равны нулю), а сразу за ними идут 10 байт серийного номера преобразованные из строкового HEX представления в байт.<br />
<br />
Т.е. для известных нам "Ms-Rem" и "C38FB7A0CF38F73B1159" буфер будет вот таким:<br />
<br />
('M', 's', '-', 'R', 'e', 'm', 0, 0, 0, 0, $C3, $8F, $B7, $A0, $CF, $38, $F7, $3B, $11, $59)<br />
<br />
Это можно было увидеть при старте виртуальной машины под отладчиком, подсмотрев данные, расположенные по адресу, на который указывала переменная "arg_С".<br />
<br />
Для работы интерпретатора BF требуется буфер в 300000 байт (по условиям, описанным в вики), но на самом деле данный вариант кода использует всего 221 байт из 300000.<br />
<br />
Собственно пишем код.<br />
<br />
Нам нужны 4 буфера, для пикода, для рабочего пространства VM, входной и выходной буфера.<br />
<br />
<pre class="brush:delphi">const
PicodeBuffSize = 15778;
var
// Буфер с пикодом
PicodeBuff: array [0..PicodeBuffSize - 1] of Byte;
PicodeIndex: Integer;
// Буфер для работы VM
WorkBuff: array [0..220] of Byte;
WorkBuffIndex: Integer;
// Выходной буфер
OutputBuff: array [0..39] of AnsiChar;
OutputBuffIndex: Integer;
// Буфер с логином и серийным номером
LoginAndPwd: array [0..29] of AnsiChar;
LoginAndPwdIndex: Integer;
</pre>
<br />
Нужно загрузить пикод и уметь инициализировать буфер с логином и серийным номером:<br />
<br />
<pre class="brush:delphi">procedure InitVM;
var
M: TMemoryStream;
begin
M := TMemoryStream.Create;
try
M.LoadFromFile('..\..\data\vm.brainfuck');
M.Read(PicodeBuff[0], PicodeBuffSize);
finally
M.Free;
end;
end;
procedure InitLoginAndPwd(const Login, Password: AnsiString);
var
I: Integer;
A, B: Byte;
begin
// Колируем логин
Move(Login[1], LoginAndPwd[0], Length(Login));
Move(Password[1], LoginAndPwd[10], Min(Length(Password), 20));
// подготавливаем буфер с серийным номером
for I := 0 to 9 do
begin
A := Byte(LoginAndPwd[10 + I * 2]);
B := Byte(LoginAndPwd[11 + I * 2]);
if A > $39 then
Dec(A, $37)
else
Dec(A, $30);
if B > $39 then
Dec(B, $37)
else
Dec(B, $30);
A := a shl 4;
A := A or B;
LoginAndPwd[10 + I] := AnsiChar(A);
end;
end;
</pre>
<br />
И собственно само тело интерпретатора:<br />
<br />
<pre class="brush:delphi">procedure RunVM;
var
I: Integer;
Count: Integer;
begin
repeat
case PicodeBuff[PicodeIndex] of
Byte('>'): Inc(WorkBuffIndex);
Byte('<'): Dec(WorkBuffIndex);
Byte('+'): Inc(WorkBuff[WorkBuffIndex]);
Byte('-'): Dec(WorkBuff[WorkBuffIndex]);
Byte('.'):
begin
OutputBuff[OutputBuffIndex] := AnsiChar(WorkBuff[WorkBuffIndex]);
Inc(OutputBuffIndex);
end;
Byte(','):
begin
WorkBuff[WorkBuffIndex] := Byte(LoginAndPwd[LoginAndPwdIndex]);
Inc(LoginAndPwdIndex);
end;
Byte('['):
begin
if WorkBuff[WorkBuffIndex] <> 0 then
begin
Inc(PicodeIndex);
Continue;
end;
Count := 1;
for I := PicodeIndex + 1 to PicodeBuffSize - 1 do
begin
if PicodeBuff[I] = Byte('[') then
begin
Inc(Count);
Continue;
end;
if PicodeBuff[I] = Byte(']') then
begin
Dec(Count);
if Count = 0 then
begin
PicodeIndex := I;
Break;
end;
end;
end;
end;
Byte(']'):
begin
if WorkBuff[WorkBuffIndex] = 0 then
begin
Inc(PicodeIndex);
Continue;
end;
Count := 1;
for I := PicodeIndex - 1 downto 0 do
begin
if PicodeBuff[I] = Byte(']') then
begin
Inc(Count);
Continue;
end;
if PicodeBuff[I] = Byte('[') then
begin
Dec(Count);
if Count = 0 then
begin
PicodeIndex := I;
Break;
end;
end;
end;
end;
end;
Inc(PicodeIndex);
until PicodeIndex = PicodeBuffSize;
end;
</pre>
<br />
Осталось только все это запустить:<br />
<br />
<pre class="brush:delphi"> InitVM;
InitLoginAndPwd('Ms-Rem', 'C38FB7A0CF38F73B1159');
RunVM;
Writeln(PAnsiChar(@OutputBuff[0]));
Readln;
</pre>
<br />
Запускаем и смотрим на результат:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZVRjcOfRybXMYqXnr4xGBrGncNGEpGMUt0FvCWtLf-kFcNWuYolq_XyeHGJCNOAZZco2C1PupcgAMxusphVfgeoroOfv4tpGTQ-moqv2h-A6OmxXS7XMY5CUz8mGNuEDQAzU2fTxf28E/s1600/25.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZVRjcOfRybXMYqXnr4xGBrGncNGEpGMUt0FvCWtLf-kFcNWuYolq_XyeHGJCNOAZZco2C1PupcgAMxusphVfgeoroOfv4tpGTQ-moqv2h-A6OmxXS7XMY5CUz8mGNuEDQAzU2fTxf28E/s1600/25.png" /></a></div>
<br />
Ну что ж, похоже все сделано правильно и работает так как надо.<br />
(Исходный код интерпретатора <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">в архиве c примерами</a> '.\tools\bf_execute\')<br />
<br />
Таким образом - второй конверт снят.<br />
<br />
Но что теперь нам со всем этим делать?<br />
Анализировать в лоб пикод не получится - нет инструментов, единственно что можно подсмотреть, это номера ячеек в которые заносится логин и пароль и из каких выводится результат.<br />
<br />
Ставим ВР на процедурах чтения и записи и смотрим что у нас выйдет...<br />
<br />
Да ничего хорошего, в тот момент когда читается данные логина и серийного номера, каждый байт читается всегда в ячейку номер шесть рабочего буфера.<br />
То же происходит и с выводом данных из VM, очередной символ забирается так же из ячейки номер шесть.<br />
<br />
Единственное, что может нам хоть как-то прояснить картинку, это дамп рабочего буфера в момент вывода данных, посмотрите на него:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiOWKAMlBEhqCTl_g2WBYFQrgu-b52jil1YzwnZb4zmDypcfqI1olpw4IUJhHe7Fw61VFSXcyLNYqSlq4-DeoK-AAvuhaOfSP1Y9uIpOPsDFAtjNnB35eVgDntbu89zD8dVbWUw-GLs4t8/s1600/26.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiOWKAMlBEhqCTl_g2WBYFQrgu-b52jil1YzwnZb4zmDypcfqI1olpw4IUJhHe7Fw61VFSXcyLNYqSlq4-DeoK-AAvuhaOfSP1Y9uIpOPsDFAtjNnB35eVgDntbu89zD8dVbWUw-GLs4t8/s1600/26.png" /></a></div>
<br />
Тут хотя бы видно, что в рабочем буфере VM находится логин и пароль, а так-же чуть ниже него две уже подготовленных строки с "хорошим сообщением" и "плохим".<br />
А в шестой ячейке рабочего буфера (вторая справа сверху) уже сидит подготовленный символ "С" из выводимого сообщения "Congratulations!!! It is valid serial!"<br />
<br />
Вот такая засада.<br />
<br />
<h3 style="text-align: left;">
8. Пишем декомпилятор Brainfuck</h3>
<br />
Что есть декомпилятор?<br />
По сути, это утилита, преобразующая набор машкодов для процессора, в понятный программисту набор ассемблерных инструкций. Для каждого процессора это будет свой ассемблер (32/64/ARM и т.д.). Ну а виртуальная машина (как говорилось ранее) тот же процессор со своим набором инструкций, выраженных в виде пикода.<br />
<br />
Сейчас стоит задача, написать декомпилятор пикода в 32-битный ассемблер, чтобы с результатом можно было хоть как-то работать, благо в этом случае инструмент для анализа у нас уже есть - это отладчик.<br />
Причем задача то по сути простая, не нужно учитывать префиксы, парсить ModRM/SIB - всего-то восемь инструкций, но для начала нужно разобраться как это будет выглядеть.<br />
<br />
Дизассемблировать в лоб будет не удачной затеей, нужно сворачивать блоки повторяющихся инструкций пикода BF<br />
<br />
К примеру у нас есть пикод BF такого вида: ">>>+++<<----"<br />
<br />
Здесь происходит переход к четвертой ячейке, увеличение ее значения на три, переход ко второй и уменьшение ее значения на четыре.<br />
<br />
Если дизассемблировать в лоб, то получится что-то невнятное:<br />
<br />
<pre class="brush:asm">inc eax
inc eax
inc eax
inc byte ptr [eax]
inc byte ptr [eax]
inc byte ptr [eax]
dec eax
dec eax
dec byte ptr [eax]
dec byte ptr [eax]
dec byte ptr [eax]
dec byte ptr [eax]
</pre>
<br />
гораздо проще свернуть повторяющиеся наборы инструкций:<br />
<br />
<pre class="brush:asm">add eax, 3
add byte ptr [eax], 3
sub eax, 2
sub byte ptr [eax], 4
</pre>
<br />
Ввод и вывод ассемблируются достаточно просто.<br />
Нам нужно только знать откуда читать и куда выводить. Причем нужно учитывать что каждый раз при чтении или выводе данных нужно сдвигать позицию, чтобы не прочитать то, что уже было прочитано или не записать поверх уже записанного.<br />
<br />
Остался только вариант со скобками.<br />
Скобки в BF выполняют аналог цикла while, т.е. пока ячейка не равна нулю происходит выполнение тела цикла.<br />
За вход в тело цикла отвечает открывающая скобка "[", за переход на следующую итерацию, закрывающая "]".<br />
<br />
Т.е. грубо в ассемблере нам нужно предусмотреть два условных перехода, один на вход в цикл, один на переход на следующую итерацию.<br />
До кучи нужен еще как минимум одна вещь, это адрес, куда нужно перейти если условие вхождения в цикл не выполнилось, или если не выполнилось условие перехода на следующую итерацию цикла.<br />
<br />
Следующим шагом нужно разобраться как осуществлять дизассемблирование циклов, с учетом описанных выше трех прыжков.<br />
<br />
К примеру есть вот такой пикод:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIfhA4srt4vBu01gH_DcNn-P822cdNoB6GTp9JL4GHTDCl2CC_LoVFm8keDqPiFG5tvHP4zctVXi6159lYFZDcFqJXzABL5OS3xn7IB5P-We5DgKeadhV5OzaDkIM8xtOG-Tn0xbBUGyE/s1600/27.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIfhA4srt4vBu01gH_DcNn-P822cdNoB6GTp9JL4GHTDCl2CC_LoVFm8keDqPiFG5tvHP4zctVXi6159lYFZDcFqJXzABL5OS3xn7IB5P-We5DgKeadhV5OzaDkIM8xtOG-Tn0xbBUGyE/s1600/27.png" /></a></div>
<br />
Более удобным для дизассемблирования (по крайней мере для меня) будет вынос тела цикла за пределы основного кода, примерно вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLrgr-9NGMNFCvJR2Ti0ecJYiD_HDlle-T0Me9_6tbF3cDgytzaIJQAE19m5qh2gMLLKYyKLxPRV8L3iZvF-5T5b7ckokSN95NaLv8Z5FMHF-5UhvF72722ugAJNVQVvaYHEBy4OIXhFk/s1600/28.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLrgr-9NGMNFCvJR2Ti0ecJYiD_HDlle-T0Me9_6tbF3cDgytzaIJQAE19m5qh2gMLLKYyKLxPRV8L3iZvF-5T5b7ckokSN95NaLv8Z5FMHF-5UhvF72722ugAJNVQVvaYHEBy4OIXhFk/s1600/28.png" /></a></div>
<br />
Если подходить таким образом, то от дизассемблера нужно только знать начало и конец каждой процедуры и в цикле декомпилировать тело каждой, вставляя в нужные места прыжки во внутренние циклы.<br />
<br />
ЗЫ: Правда, вот сейчас смотрю на эту картинку и понимаю что может наверное и не стоило выносить, ибо лишний прыжок появился, но... лень переписывать :)<br />
<br />
Ну да ладно, теперь по поводу рабочего буфера и входного/выходного буферов: в качестве указателя на рабочий буфер будет использоваться регистр ESI, он же будет являться и курсором, т.е. на операциях ">" или "<" будет увеличиваться или уменьшаться именно этот регистр.<br />
Указатель на буфер с логином и серийным номером будет хранить регистр EBX, а за выходной буфер будет отвечать регистр EDI.<br />
<br />
Пишем код.<br />
<br />
Декомпилятору потребуется буфер с пикодом и небольшой список в котором будет хранится начало и конец каждого While цикла.<br />
<br />
<pre class="brush:delphi">var
PicodeBuffSize: Integer;
PicodeBuff: array of Byte;
type
TWhileSubProc = record
StartAddr, EndAddr: Integer;
SubProcEndLabel, SubProcStartLabel: string;
end;
TWhileSubProcList = TList<twhilesubproc>;
</twhilesubproc></pre>
<br />
Далее нужны несколько утилитарных процедур, первая строит сам список, а вторая и третья, возвращает элемент списка ориентируясь на поля StartAddr и EndAddr.<br />
<br />
<pre class="brush:delphi">function GetWhileSubProcList: TWhileSubProcList;
var
I, A, Z: Integer;
Item: TWhileSubProc;
begin
Result := TWhileSubProcList.Create;
for I := 0 to PicodeBuffSize - 1 do
if PicodeBuff[I] = Byte('[') then
begin
Item.StartAddr := I;
Z := 1;
for A := I + 1 to PicodeBuffSize - 1 do
begin
if PicodeBuff[A] = Byte('[') then
begin
Inc(Z);
Continue;
end;
if PicodeBuff[A] = Byte(']') then
begin
Dec(Z);
if Z = 0 then
begin
Item.EndAddr := A;
Break;
end;
end;
end;
Result.Add(Item);
end;
end;
function IndexAtStart(List: TWhileSubProcList; Value: Integer): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to List.Count - 1 do
if List[I].StartAddr = Value then
Exit(I);
end;
function IndexAtEnd(List: TWhileSubProcList; Value: Integer): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to List.Count - 1 do
if List[I].EndAddr = Value then
Exit(I);
end;
</pre>
<br />
Теперь основная процедура - декомпилятор указанного извне While блока:<br />
<br />
<pre class="brush:delphi">procedure DecodeSubRoutine(List: TWhileSubProcList; Index: Integer);
function GetCharSimbol(Value: Integer): string;
begin
if Value < 32 then
Result := ' // #' + IntToHex(Abs(Value), 2)
else
Result := string(' // char "' + AnsiChar(Value) + '"');
end;
const
LabelPfx = '@vm_code_';
var
Count, I: Integer;
SubRoutineName: string;
Item: TWhileSubProc;
begin
if Index >= 0 then
begin
// если декомпилируем while цикл, генерируем ему имя по его адресу
PicodeIndex := List[Index].StartAddr;
SubRoutineName := LabelPfx + IntToHex(PicodeIndex, 4);
MakeAsmCode(SubRoutineName + ':');
Inc(PicodeIndex);
end
else
// в противном случае идет декомпиляция самой главной ветви пикода
SubRoutineName := '@root';
repeat
case PicodeBuff[PicodeIndex] of
// сворачиваем идущие подряд подвижки курсора
Byte('>'), Byte('<'):
begin
Count := 0;
while PicodeBuff[PicodeIndex] in [Byte('>'), Byte('<')] do
begin
if PicodeBuff[PicodeIndex] = Byte('>') then
Inc(Count)
else
Dec(Count);
Inc(PicodeIndex);
end;
if Count = 0 then Continue;
if Count < 0 then
begin
if Count = -1 then
MakeAsmCode(' dec esi')
else
MakeAsmCode(' sub esi, ' + IntToStr(Abs(Count)));
end;
if Count > 0 then
begin
if Count = 1 then
MakeAsmCode(' inc esi')
else
MakeAsmCode(' add esi, ' + IntToStr(Count));
end;
Continue;
end;
// сворачиваем идущие подряд изменения ячейки
Byte('+'), Byte('-'):
begin
Count := 0;
while PicodeBuff[PicodeIndex] in [Byte('+'), Byte('-')] do
begin
if PicodeBuff[PicodeIndex] = Byte('+') then
Inc(Count)
else
Dec(Count);
Inc(PicodeIndex);
end;
if Count = 0 then Continue;
if Count < 0 then
begin
if Count = -1 then
MakeAsmCode(' dec byte ptr [esi]')
else
MakeAsmCode(' sub byte ptr [esi], ' +
IntToStr(Abs(Count)) + GetCharSimbol(Count));
end;
if Count > 0 then
begin
if Count = 1 then
MakeAsmCode(' inc byte ptr [esi]')
else
MakeAsmCode(' add byte ptr [esi], ' +
IntToStr(Count) + GetCharSimbol(Count));
end;
Continue;
end;
// выводим значение ячейки
Byte('.'):
begin
MakeAsmCode(' mov al, byte ptr [esi]');
MakeAsmCode(' mov byte ptr [edi], al');
MakeAsmCode(' inc edi');
end;
// зачитываем значение из входного буфера
Byte(','):
begin
MakeAsmCode(' mov al, byte ptr [ebx]');
MakeAsmCode(' inc ebx');
MakeAsmCode(' mov byte ptr [esi], al');
end;
// вход в цикл While
Byte('['):
begin
I := IndexAtStart(List, PicodeIndex);
Item := List[I];
MakeAsmCode(' cmp byte ptr [esi], 0 // [');
Item.SubProcEndLabel :=
SubRoutineName + '_' + IntToHex(PicodeIndex + 1, 4);
Item.SubProcStartLabel := LabelPfx + IntToHex(PicodeIndex, 4);
// если не ноль, перейти на выполнение кода подпроцедуры
// сразу же за этой инструкцией будет код идущий за соответствующей закрывающей скобкой
MakeAsmCode(' jne ' + Item.SubProcStartLabel);
MakeAsmCode(Item.SubProcEndLabel + ':');
PicodeIndex := Item.EndAddr + 1;
List[I] := Item;
Continue;
end;
// переход на следующую итерацию
Byte(']'):
begin
I := IndexAtEnd(List, PicodeIndex);
MakeAsmCode(' cmp byte ptr [esi], 0 // ]');
// если не ноль, перейти на выполнение кода подпроцедуры
MakeAsmCode(' jne ' + List[I].SubProcStartLabel);
// в противном случае перейти на код следующий за завершением подпроцедуры
MakeAsmCode(' jmp ' + List[I].SubProcEndLabel);
// подпроцедура завершилась - идем на выход
Exit;
end;
end;
Inc(PicodeIndex);
until PicodeIndex = PicodeBuffSize;
// достигнут конец пикода, пишем финализацию
MakeAsmCode(' popa');
MakeAsmCode(' ret');
end;
</pre>
<br />
Осталось в цикле вызвать декомпиляцию каждого While блока:<br />
<br />
<pre class="brush:delphi">procedure DecodeVM;
var
I: Integer;
List: TWhileSubProcList;
begin
MakeAsmCode('procedure RunBrainfuck(pWorkBuff, pInBuf, pOutBuf: Pointer);');
MakeAsmCode('asm');
MakeAsmCode(' pusha');
MakeAsmCode(' mov esi, pWorkBuff');
MakeAsmCode(' mov ebx, pInBuf');
MakeAsmCode(' mov edi, pOutBuf');
List := GetWhileSubProcList;
try
DecodeSubRoutine(List, -1);
for I := 0 to List.Count - 1 do
DecodeSubRoutine(List, I);
finally
List.Free;
end;
MakeAsmCode('end;');
end;
</pre>
<br />
И написать код запуска всего этого:<br />
<br />
<pre class="brush:delphi">function InitPicode: Boolean;
var
M: TMemoryStream;
begin
Result := False;
if (ParamCount = 0) or not FileExists(ParamStr(1)) then
begin
Writeln('Brainfuck file not found.');
Exit;
end;
M := TMemoryStream.Create;
try
M.LoadFromFile(ParamStr(1));
PicodeBuffSize := M.Size;
SetLength(PicodeBuff, PicodeBuffSize);
M.ReadBuffer(PicodeBuff[0], PicodeBuffSize);
Result := True;
finally
M.Free;
end;
end;
begin
if InitPicode then
begin
AsmCode := TFileStream.Create(ChangeFileExt(ParamStr(1), '.inc'), fmCreate);
try
DecodeVM;
finally
AsmCode.Free;
end;
Writeln('Done.');
end;
Readln;
end.
</pre>
<br />
(Исходный код декомпилятора<a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank"> в архиве c примерами</a> '.\tools\bf_decompiler\')<br />
<br />
Теперь нужно проверить его работу на чем нибудь.<br />
Возьмем к примеру Hello World из вики с таким содержанием:<br />
<div>
<br /></div>
<blockquote class="tr_bq">
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++<br />
.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.<br />
------.--------.>+.>.</blockquote>
<br />
И посмотрим, во что это декомпилируется:<br />
<br />
<pre class="brush:delphi">procedure RunBrainfuck(pWorkBuff, pInBuf, pOutBuf: Pointer);
asm
pusha
mov esi, pWorkBuff
mov ebx, pInBuf
mov edi, pOutBuf
add byte ptr [esi], 10 // #0A
cmp byte ptr [esi], 0 // [
jne @vm_code_000B
@root_000C:
inc esi
add byte ptr [esi], 2 // #02
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
inc esi
inc byte ptr [esi]
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
add byte ptr [esi], 7 // #07
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
add byte ptr [esi], 3 // #03
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
inc esi
add byte ptr [esi], 2 // #02
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
sub esi, 2
add byte ptr [esi], 15 // #0F
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
inc esi
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
add byte ptr [esi], 3 // #03
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
sub byte ptr [esi], 6 // #06
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
sub byte ptr [esi], 8 // #08
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
inc esi
inc byte ptr [esi]
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
inc esi
mov al, byte ptr [esi]
mov byte ptr [edi], al
inc edi
popa
ret
@vm_code_000B:
inc esi
add byte ptr [esi], 7 // #07
inc esi
add byte ptr [esi], 10 // #0A
inc esi
add byte ptr [esi], 3 // #03
inc esi
inc byte ptr [esi]
sub esi, 4
dec byte ptr [esi]
cmp byte ptr [esi], 0 // ]
jne @vm_code_000B
jmp @root_000C
end;
</pre>
<br />
Выглядит похоже. Согласен, можно и подоптимизировать, но не та задача.<br />
Проверяем как будет работать и пишем тест:<br />
<br />
<pre class="brush:delphi">program hello_world_test;
{$APPTYPE CONSOLE}
{$R *.res}
{$I ..\..\data\helloworld.inc}
var
WorkBuff: array [0..300000] of Byte;
OutBuf: array [0..100] of Byte;
begin
RunBrainfuck(@WorkBuff[0], nil, @OutBuf[0]);
Writeln(PAnsiChar(@OutBuf[0]));
Readln;
end.
</pre>
<br />
Смотрим результат:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMNlDIx9dVcmgSfpyZKD2ZbGrHHGke5XnNl86kY3d47UBJjr-B549rmdMKwZ5HI31nWE18WqzjZ4Ui196pJ-wqJQBqVauqDxYKmDn3qJVZpHaUQ26Za_N34hgaNOzKwjmCHqxc07oyeIo/s1600/29.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMNlDIx9dVcmgSfpyZKD2ZbGrHHGke5XnNl86kY3d47UBJjr-B549rmdMKwZ5HI31nWE18WqzjZ4Ui196pJ-wqJQBqVauqDxYKmDn3qJVZpHaUQ26Za_N34hgaNOzKwjmCHqxc07oyeIo/s1600/29.png" /></a></div>
<br />
Ну что ж, не плохо.<br />
Получилось то что и задумывалось, теперь декомпилируем тело самой VM.<br />
<br />
На выходе получилось 8153 строчек чистейшего ассемблера :)<br />
(результат декомпиляции VM<a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank"> в архиве c примерами</a> '.\data\vm.inc')<br />
<br />
Бегло пробежав его глазками можно сразу заметить процедуру инициализации "хорошей" и "плохой" строчек внутри vm_code_001B_001D:<br />
<br />
<pre class="brush:asm">@vm_code_001B_001D:
add esi, 105
add byte ptr [esi], 67 // char "C"
add esi, 2
add byte ptr [esi], 111 // char "o"
add esi, 2
add byte ptr [esi], 110 // char "n"
add esi, 2
add byte ptr [esi], 103 // char "g"
add esi, 2
add byte ptr [esi], 114 // char "r"
add esi, 2
add byte ptr [esi], 97 // char "a"
add esi, 2
add byte ptr [esi], 116 // char "t"
add esi, 2
add byte ptr [esi], 117 // char "u"
add esi, 2
add byte ptr [esi], 108 // char "l"
add esi, 2
add byte ptr [esi], 97 // char "a"
add esi, 2
add byte ptr [esi], 116 // char "t"
add esi, 2
add byte ptr [esi], 105 // char "i"
add esi, 2
add byte ptr [esi], 111 // char "o"
add esi, 2
add byte ptr [esi], 110 // char "n"
add esi, 2
add byte ptr [esi], 115 // char "s"
add esi, 2
add byte ptr [esi], 33 // char "!"
add esi, 2
add byte ptr [esi], 33 // char "!"
add esi, 2
add byte ptr [esi], 33 // char "!"
add esi, 2
add byte ptr [esi], 32 // char " "
add esi, 2
add byte ptr [esi], 73 // char "I"
add esi, 2
add byte ptr [esi], 116 // char "t"
add esi, 2
add byte ptr [esi], 32 // char " "
add esi, 2
add byte ptr [esi], 105 // char "i"
add esi, 2
add byte ptr [esi], 115 // char "s"
add esi, 2
add byte ptr [esi], 32 // char " "
add esi, 2
add byte ptr [esi], 118 // char "v"
add esi, 2
add byte ptr [esi], 97 // char "a"
add esi, 2
add byte ptr [esi], 108 // char "l"
add esi, 2
add byte ptr [esi], 105 // char "i"
add esi, 2
add byte ptr [esi], 100 // char "d"
add esi, 2
add byte ptr [esi], 32 // char " "
add esi, 2
add byte ptr [esi], 115 // char "s"
add esi, 2
add byte ptr [esi], 101 // char "e"
add esi, 2
add byte ptr [esi], 114 // char "r"
add esi, 2
add byte ptr [esi], 105 // char "i"
add esi, 2
add byte ptr [esi], 97 // char "a"
add esi, 2
add byte ptr [esi], 108 // char "l"
add esi, 2
add byte ptr [esi], 33 // char "!"
add esi, 4
add byte ptr [esi], 83 // char "S"
add esi, 2
add byte ptr [esi], 101 // char "e"
add esi, 2
add byte ptr [esi], 114 // char "r"
add esi, 2
add byte ptr [esi], 105 // char "i"
add esi, 2
add byte ptr [esi], 97 // char "a"
add esi, 2
add byte ptr [esi], 108 // char "l"
add esi, 2
add byte ptr [esi], 32 // char " "
add esi, 2
add byte ptr [esi], 105 // char "i"
add esi, 2
add byte ptr [esi], 110 // char "n"
add esi, 2
add byte ptr [esi], 118 // char "v"
add esi, 2
add byte ptr [esi], 97 // char "a"
add esi, 2
add byte ptr [esi], 108 // char "l"
add esi, 2
add byte ptr [esi], 105 // char "i"
add esi, 2
add byte ptr [esi], 100 // char "d"
add esi, 2
add byte ptr [esi], 32 // char " "
add esi, 2
add byte ptr [esi], 58 // char ":"
add esi, 2
add byte ptr [esi], 40 // char "("
</pre>
<br />
Но... все таки больше 8 тысяч строк, с наскока их проанализировать не получится.<br />
Нужно писать дополнительный инструментарий.<br />
<br />
Но для этого нужно написать небольшое демоприложение, выполняющее декомпилированный код VM, с которым будем работать на протяжении всей оставшейся статьи.<br />
<br />
<pre class="brush:delphi">program decompiled_vm_test;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Classes,
Math;
var
// Буфер для работы VM
WorkBuff: array [0..220] of Byte;
WorkBuffIndex: Integer;
// Выходной буфер
OutputBuff: array [0..39] of AnsiChar;
OutputBuffIndex: Integer;
// Буфер с логином и серийным номером
LoginAndPwd: array [0..29] of AnsiChar;
LoginAndPwdIndex: Integer;
procedure InitLoginAndPwd(const Login, Password: AnsiString);
var
I: Integer;
A, B: Byte;
begin
// Колируем логин
Move(Login[1], LoginAndPwd[0], Length(Login));
Move(Password[1], LoginAndPwd[10], Min(Length(Password), 20));
// подготавливаем буфер с серийным номером
for I := 0 to 9 do
begin
A := Byte(LoginAndPwd[10 + I * 2]);
B := Byte(LoginAndPwd[11 + I * 2]);
if A > $39 then
Dec(A, $37)
else
Dec(A, $30);
if B > $39 then
Dec(B, $37)
else
Dec(B, $30);
A := a shl 4;
A := A or B;
LoginAndPwd[10 + I] := AnsiChar(A);
end;
end;
{$I ..\..\data\vm.inc}
begin
InitLoginAndPwd('Ms-Rem', 'C38FB7A0CF38F73B1159');
RunBrainfuck(@WorkBuff[0], @LoginAndPwd[0], @OutputBuff[0]);
Writeln(PAnsiChar(@OutputBuff[0]));
Readln;
end.
</pre>
<br />
В настройках демоприложения нужно включить генерацию МАР файла (в режиме Detailed), он нам пригодится в следующей главе.<br />
<br />
<h3 style="text-align: left;">
9. Пишем трейсер</h3>
<br />
Для того чтобы понять, что именно происходит в декомпилированном коде необходимо построить трассу исполнения. Причем будет достаточно информации о том, какие блоки выполняются и куда из них происходит переход.<br />
<br />
Суть трассы заключается в построении направленного графа, анализируя который можно купировать логические ветвления в векторные блоки (грубо, в подобие блок-схемы)<br />
<br />
Для этого потребуется отладчик: <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-2.html" target="_blank">http://alexander-bagel.blogspot.ru/2012/11/debuger-2.html</a><br />
В процессе построения трассы отладчик будет пошагово идти по коду реципиента, прерываясь на адресах, которые мы можем привести к номеру строчки в сгенерированном нами ранее исходнике.<br />
Для приведения адреса к номеру строчки в asm листинге потребуется небольшой модуль, который будет парсить МАР файл и возвращать эту информацию<br />
Также потребуется еще один модуль, который по номеру строчки в asm листинге будет возвращать имя подпроцедуры, на которой произошла остановка.<br />
<br />
Исходный код последних двух модулей приводить не буду - они очень простые, в любом случае код можно увидеть <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">в архиве с примерами к статье</a> по следующему пути: '.\tools\tracer\'.<br />
<br />
Так же потребуется класс, который будет хранить в себе данные о снятой трассе.<br />
Грубо вся его задача хранить массив записей вот такого вида:<br />
<br />
<pre class="brush:delphi">type
TTraceItem = record
SubName: string;
InList, OutList: TStringList;
CustomData: Pointer;
end;
</pre>
<br />
и предоставлять методы работы с ним.<br />
Также очень простой, посмотреть код можно <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">в архиве</a> по пути: '.\tools\common\trace_data.pas'<br />
<br />
Трасса выполнения будет сниматься в 4 прохода в 4 различных режимах.<br />
1. Полная трасса исполнения программы.<br />
2. Частичная трасса, из одной итерации чтения буфера с логином и серийным номером.<br />
3. Частичная трасса из одной итерации вывода результата.<br />
4. Детектирование процедур, в которых происходит запись ячеек с логином и паролем.<br />
<br />
Чуть попозже придется дописать трейсер, добавив в него пятый режим для полного снятия трассы, но снимаемой с демопримера, в котором серийный номер скинут в последовательность нулей (в исходном коде этот режим будет зваться ttWrongSN).<br />
Но об этом попозже, а сейчас...<br />
<br />
Реализация трейсера достаточно банальна: перекрываем OnCreateProcess отладчика и инициализируем его установкой соответствующих ВР:<br />
<br />
<pre class="brush:delphi">procedure TTracer.OnCreateProcess(Sender: TObject; ThreadIndex: Integer;
Data: TCreateProcessDebugInfo);
begin
Writeln('process start');
case FTraceType of
// трассируем от начала до конца
ttFull:
begin
FDebuger.SetHardwareBreakpoint(ThreadIndex,
Pointer(FMap.AddrAtLine(FSrc.StartLine)), hsByte, hmExecute, 0, 'vm_start');
FDebuger.SetHardwareBreakpoint(ThreadIndex,
Pointer(FMap.AddrAtLine(FSrc.EndLine)), hsByte, hmExecute, 1, 'vm_end');
end;
ttIn:
// включаем трассу от чтения логина
FDebuger.SetHardwareBreakpoint(ThreadIndex,
Pointer(FMap.AddrAtLine(FSrc.ReadPwdLine)), hsByte, hmExecute, 0, 'read_pwd_start');
ttOut:
// включаем трассу от вывода
FDebuger.SetHardwareBreakpoint(ThreadIndex,
Pointer(FMap.AddrAtLine(FSrc.OutputBufLine)), hsByte, hmExecute, 0, 'out_data_start');
ttCheckLoginBuff:
begin
// включаем трассу по первому обращению к буферу с логином и паролем
FDebuger.SetHardwareBreakpoint(ThreadIndex,
Pointer(FMap.AddrAtLine(FSrc.StartLine)), hsByte, hmExecute, 0, 'LoginAndSN_MBP_Present');
FDebuger.SetHardwareBreakpoint(ThreadIndex,
Pointer(FMap.AddrAtLine(FSrc.EndLine)), hsByte, hmExecute, 1, 'vm_end');
end;
end;
end;
</pre>
<br />
После этого в обработчике выставляем режимы работы:<br />
<br />
<pre class="brush:delphi">procedure TTracer.OnHardwareBreakpoint(Sender: TObject; ThreadIndex: Integer;
ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: THWBPIndex;
var ReleaseBreakpoint: Boolean);
var
CurrentName: string;
ThreadData: TThreadData;
I: Integer;
begin
Inc(FTotalStepCount);
ThreadData := FDebuger.GetThreadData(ThreadIndex);
Writeln(ThreadData.Breakpoint.Description[BreakPointIndex]);
case FTraceType of
ttFull:
begin
if BreakPointIndex = 0 then
begin
FDebuger.ResumeAction := raTraceInto;
ReleaseBreakpoint := True;
end
else
begin
ReleaseBreakpoint := True;
CurrentName :=
FSrc.GetProcedureNameAtLine(FMap.LineAtAddr(DWORD(ExceptionRecord.ExceptionAddress)));
FTrace.AddTace(FPreviousName, CurrentName);
FTrace.SaveToFile('..\..\data\full.trace');
Writeln('Trace done.');
Writeln('Total instructions traced: ', FTotalStepCount);
Writeln('Traced subroutine added: ', FTrace.Count);
Writeln('Time elapsed: ', GetTickCount - FStart, 'ms');
end;
end;
ttIn, ttOut:
begin
if FProcList.Count > 0 then
begin
ReleaseBreakpoint := True;
CurrentName :=
FSrc.GetProcedureNameAtLine(FMap.LineAtAddr(DWORD(ExceptionRecord.ExceptionAddress)));
FTrace.AddTace(FPreviousName, CurrentName);
if FTraceType = ttIn then
FProcList.SaveToFile('..\..\data\in.proclist')
else
FProcList.SaveToFile('..\..\data\out.proclist');
Writeln('Trace done.');
Writeln('Total instructions traced: ', FTotalStepCount);
Writeln('Traced subroutine added: ', FProcList.Count);
Writeln('Time elapsed: ', GetTickCount - FStart, 'ms');
FDebuger.ResumeAction := raRun;
end
else
FDebuger.ResumeAction := raTraceInto;
end;
ttCheckLoginBuff:
begin
ReleaseBreakpoint := True;
if BreakPointIndex = 0 then
begin
FWorkBuffAddr := PByte(FDebuger.GetContext(0).Eax);
for I := 0 to 9 do
FDebuger.SetMemoryBreakpoint(FWorkBuffAddr + 28 + (I * 2), 1, True, 'Login' + IntToStr(I));
for I := 0 to 9 do
FDebuger.SetMemoryBreakpoint(FWorkBuffAddr + 48 + (I * 2), 1, True, 'SN' + IntToStr(I));
end
else
begin
FProcList.SaveToFile('..\..\data\change_buff.proclist');
Writeln('Trace done.');
Writeln('Total instructions traced: ', FTotalStepCount);
Writeln('Traced subroutine added: ', FProcList.Count);
Writeln('Time elapsed: ', GetTickCount - FStart, 'ms');
end;
end;
end;
end;
</pre>
<br />
Если вкратце, то в режиме полного дампа (ttFull) второй HBP будет означать завершение процесса трассировки, режимы "ttIn, ttOut" будут останавливаться при втором срабатывании HBP (один проход ввода/вывода), ну а ttCheckLoginBuff будет выполнять трассировку с использованием MBP пока не будет выполнен весь цикл VM.<br />
<br />
Результаты трассировки будут собираться в этих двух обработчиках:<br />
<br />
<pre class="brush:delphi">procedure TTracer.OnSingleStep(Sender: TObject; ThreadIndex: Integer;
ExceptionRecord: Windows.TExceptionRecord);
var
CurrentName: string;
begin
Inc(FTotalStepCount);
CurrentName :=
FSrc.GetProcedureNameAtLine(FMap.LineAtAddr(DWORD(ExceptionRecord.ExceptionAddress)));
if FTraceType = ttFull then
begin
if FPreviousName = '' then FPreviousName := CurrentName;
if FPreviousName <> CurrentName then
begin
FTrace.AddTace(FPreviousName, CurrentName);
FPreviousName := CurrentName;
end;
end
else
FProcList.Add(CurrentName);
FDebuger.ResumeAction := raTraceInto;
end;
procedure TTracer.OnMemoryBreakpoint(Sender: TObject; ThreadIndex: Integer;
ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
var ReleaseBreakpoint: Boolean);
begin
Inc(FTotalStepCount);
FProcList.Add(
FSrc.GetProcedureNameAtLine(FMap.LineAtAddr(DWORD(ExceptionRecord.ExceptionAddress))));
end;
</pre>
<br />
Первый следит за ветвлением переходов, а второй просто собирает имена процедур, из которых происходило обращение к буферу, в которым расположен логин и серийный номер.<br />
<br />
В результате работы трейсера на руках у нас будет 4 файла, на основе которых мы сможем достаточно легко проанализировать всю работу VM, но...<br />
Но для этого необходимо каким либо образом отобразить полученные данные в том виде, в котором с ними можно работать...<br />
<br />
<h3 style="text-align: left;">
10. Отображаем трассу исполнения в виде графа</h3>
<br />
IDA Pro отличный инструмент, графы, выдаваемые им, очень сильно помогают в процессе анализа исходного кода, но... но он не универсален.<br />
В частности с графом декомпилированного кода VM, построенного IDA, работать в данном случае не совсем удобно.<br />
Именно поэтому я и пошел на собственноручное снятие трассы (описанное в прошлой главе) и написание инструмента, позволяющего её визуализировать<br />
<br />
Суть инструмента в следующем:<br />
1. свернуть трассу исполнения ПО в визуализируемый граф<br />
2. отобразить в графе логические блоки исполнения<br />
3. как результат, показать адреса блоков, интересные для анализа кода.<br />
<br />
Самый первый этап, это преобразование данных от трейсера в направленный граф.<br />
Он строится достаточно просто - берем текущий блок и от него рекурсивно строим исходящие переходы. Главное на забывать про while циклы, где переход в конце процедуры на её начало, будет направлен на уже добавленный элемент.<br />
Поэтому при построении графа необходимо каждому элементу добавить кастомное свойство, в котором будет хранится узел графа, на который должен быть выставлен линк при первом-же обращении.<br />
<br />
Анализируя код BF можно сделать следующие выводы:<br />
1. В каждый из while циклов может быть только 2 входа (из внешнего блока кода и переход из конца цикла).<br />
2. Конец каждого while цикла имеет только 2 выхода (если цикл завершен переходим на уровень выше по коду, в обратном случае возвращаемся в начало цикла)<br />
3. Из тела while блока может быть множественное кол-во выходов, при условии наличия ifthen блоков в его теле (но это не наш случай).<br />
<br />
Под сверткой кода подразумевается следующее:<br />
1 . Если процедура имеет один вход и один выход (по результатам трассы), то она добавляется к текущему блоку свертки.<br />
2. Началом каждого блока свертки будет являться любая процедура, имеющая два входа (цикл while).<br />
3. Концом блока свертки будет являться переход на уже добавленный код (конец while) либо любой другой блок кода, который по результатам трассы имеет два выхода (что подразумевает под собой тот же конец цикла, исходя из предыдущего пункта №3).<br />
<br />
За все эти этапы отвечает достаточно простая рекурсивная процедура ('.\tools\trace_viewer\trace_graph.pas'):<br />
<br />
<pre class="brush:delphi">procedure TTraceGraph.LoadItem(Index: Integer; AParent: TExecutionBlock);
var
Block: TExecutionBlock;
Item: TTraceItem;
begin
// начало блока исполнения - любая подпроцедура
// тело блока исполнения - подпроцедуры из одного входящего и одного исходящего вектора
// конец блока - ссылка на уже добавленную подпроцедуру или если два выходных вектора
// Если элемент был добавлен, делаем ссылку на него и на выход
Item := FTrace[Index];
if Item.CustomData <> nil then
begin
AddVector(AParent, Item.CustomData);
Exit;
end;
Block := TExecutionBlock(AddNode(NodesCount));
if AParent = nil then
Block.Level := 0
else
begin
AddVector(AParent, Block);
Block.Level := AParent.Level + 1;
FMaxLevel := Max(FMaxLevel, Block.Level);
end;
Block.ProcList.Add(Item.SubName);
FTrace.SetCustomData(Index, Block);
// если исходящих ссылок нет - дошли до конца трассы
if Item.OutList.Count = 0 then Exit;
// если у итема две исходящих ссылки - добавляем их через рекурсию
if (Item.OutList.Count = 2) then
begin
LoadItem(FTrace.GetItemIndexByName(Item.OutList[0]), Block);
LoadItem(FTrace.GetItemIndexByName(Item.OutList[1]), Block);
Exit;
end;
// в противном случае крутим цикл
Item := FTrace.ItemByName(Item.OutList[0]);
while Item.InList.Count = 1 do
begin
Block.ProcList.Add(Item.SubName);
FTrace.SetCustomData(FTrace.GetItemIndexByName(Item.SubName), Block);
case Item.OutList.Count of
0: Exit;
1: Item := FTrace.ItemByName(Item.OutList[0]);
2:
begin
LoadItem(FTrace.GetItemIndexByName(Item.OutList[0]), Block);
LoadItem(FTrace.GetItemIndexByName(Item.OutList[1]), Block);
Exit;
end;
end;
end;
if Item.InList.Count = 2 then
LoadItem(FTrace.GetItemIndexByName(Item.SubName), Block);
end;
</pre>
<br />
Результатом обработки трассы декомпилированного кода (в котором нет сложных случаев в виде множественных выходов из while), будет вот такая картинка:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizME-xbMth28_P5LXuAUEtQQJd0jRjFQL55IgtZkHMu0hsufwTZaYXe2mCRtMGj3HlRWpvh5Pxbux5ODdQb4gUkLJYq7NQmUqj5Al87Yy4GhGq-0jxjeZWIkrzXpf_ZqSFADmIDq2IC0o/s1600/30.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizME-xbMth28_P5LXuAUEtQQJd0jRjFQL55IgtZkHMu0hsufwTZaYXe2mCRtMGj3HlRWpvh5Pxbux5ODdQb4gUkLJYq7NQmUqj5Al87Yy4GhGq-0jxjeZWIkrzXpf_ZqSFADmIDq2IC0o/s1600/30.png" height="640" width="580" /></a></div>
<br />
На ней отображена трасса исполнения программы, со свернутыми блоками исполнения.<br />
<br />
Не обращайте внимания на то, что все так красиво расставлено вертикальными столбцами - я не писал алгоритм такой расстановки и все банально раскидал ручками (ибо это гораздо быстрее, чем писать такой же движок визуализации, как у IDA :)<br />
<br />
Исходный код данной утилиты я приводить не буду, его вы сможете посмотреть <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">в архиве</a> ".\tools\trace_viewer\".<br />
<br />
Основная ее задача, дать точку старта, от которой можно оттолкнуться при анализе кода VM.<br />
<br />
И вот две картинки, которые покажут часть логики работы.<br />
<br />
Вот это происходит при чтении логина и серийного номера:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjG9HPlVa92W9a5q8vEsgR2mVlEciDePS-yGV8jPFz9JiZ-lU1YNIeelD27x7eIG4XYLwYTKhT5uQzcnard0K83IDSHPe5VOlyUMxfixMV371RoRjei974OyytUtuMDFynMEAIvVAM8jss/s1600/31.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjG9HPlVa92W9a5q8vEsgR2mVlEciDePS-yGV8jPFz9JiZ-lU1YNIeelD27x7eIG4XYLwYTKhT5uQzcnard0K83IDSHPe5VOlyUMxfixMV371RoRjei974OyytUtuMDFynMEAIvVAM8jss/s1600/31.png" height="640" width="574" /></a></div>
<br />
А вот эти процедуры выполняются при выводе результата:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAOMFrMT9uQ2P2d8fdPJprs6mp2mdEkDNh4X2Nx76jX07vbquBK_A_GnmV5NCHMIvdz1QmdMwRALdsFjBDPYod6-xkaLFRs_HPWTQil0TPTtJi8cQKcgoRN0gOzPnCO-eRM4gKd87MNw8/s1600/32.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAOMFrMT9uQ2P2d8fdPJprs6mp2mdEkDNh4X2Nx76jX07vbquBK_A_GnmV5NCHMIvdz1QmdMwRALdsFjBDPYod6-xkaLFRs_HPWTQil0TPTtJi8cQKcgoRN0gOzPnCO-eRM4gKd87MNw8/s1600/32.png" height="640" width="574" /></a></div>
<br />
Обратите внимание на исполнение процедур по центру схемы, они являются матаппаратом и не участвуют в работе логики алгоритма, спрятанного в VM.<br />
<br />
Сразу оговорюсь - данный вывод был сделан уже в процессе исследования кода.<br />
Изначально, получив на руки такие две картинки я был немного озадачен и предположил что внутри может скрываться еще одна мини-VM (зеленый вертикальный блок снизу слева), которая раскидывает логику, опираясь на собственные инструкции, но... предположение оказалось неверным.<br />
<br />
А так, судя по картинкам мы имеем на руках:<br />
1. Верхний левый блок схемы - чтение данных.<br />
2. Нижний правый - вывод результата.<br />
3. Центральная ветка - какой-то матаппарат (его в итоге анализировать даже не придется).<br />
<br />
Все что не выделено зеленым и есть третий конверт (скрытая под VM логика).<br />
<br />
Те, кто будет изучать исходники данного вьювера, заранее приношу извинения - код инструмента очень сырой и писался буквально на коленке за два вечера, поэтому большим функционалом утилита не располагает. Умеет только отображать сам граф, предоставляет механизм его модификации (читай - можно передвигать блоки так как будет более удобно).<br />
Режим ZOOM-а не добавлен, для этого есть отдельная кнопка, генерирующая общее превью (с которого и снимались данные скриншоты).<br />
Но с ролью инструмента анализа графа справляется не плохо.<br />
<br />
<a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">В архиве со статьей</a> есть уже расставленный граф ('.\data\current.graph') который был получен из трассы, снятой в предыдущей главе ('.\data\full.trace').<br />
<br />
Впрочем... к нашим баранам.<br />
Инструмент для анализа декомпилированного кода VM готов, приступим непосредственно к его анализу...<br />
<br />
<h3 style="text-align: left;">
11. Анализ и детектирование алгоритма чтения переменных.</h3>
<br />
Приступив к анализу VM я уже примерно представлял с чем мне придется работать.<br />
Опираясь на карту памяти с которой работает VM (показанную в 7 главе), я знал оффсеты, по которым расположены "правильная" и "не правильная" строки с результатами, а так-же оффсет, по которому располагается буфер с логином и серийным номером.<br />
<br />
Зная как работает интерпретатор BF я даже прикинул примерный алгоритм работы с данными, с учетом того, что должны проводится некие матоперации над логином и SN.<br />
<br />
Например если требуется сложить два поля Z и X, то в Brainfuck нет операции сложения, для этого будет необходимо написать while цикл, в котором будет итерационно декрементироваться значение ячейки X и инкрементироваться значение ячейки Y.<br />
Таким образом ячейка Y будет содержать сумму обоих ячеек, а ячейка X будет обнилена.<br />
<br />
Но при выводе данных, как было показано выше, можно наблюдать что буфер с логином и серийным номером находится там, где он и должен быть и ни одно из его полей не изменено. Значит в VM применяется какой-то другой подход для чтения данных.<br />
<br />
Странный нюанс, на который я сразу обратил внимание - что логин, что серийный номер, что "правильная" и "не правильная" строки в карте памяти идут не в том виде, как есть, а каждый байт отделен от следующего нулем. Такое ощущение что мы работаем с юникодом, хотя в действительности работа идет с ANSI строками.<br />
<br />
Эти два момента мне были не понятны.<br />
<br />
Впрочем, начинать с чего-то все равно нужно, и я решил приступить к анализу алгоритма с его конца, предположив, что решение о выводе результата принимается вот с таком блоке процедур (там где выводится результат):<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhd4ORkWIZ5B21qIV-ftMDfBfi2CAa25p4_7c7g0cVaBn6uOGZ80t7OWQ3xlAHqabZVcM61DYych6CcDW0N0SEuOg-RWUF1-V3kbTlj2vjqbZS6yt8snngKv3k8otaVlaDpivJwth-iYu8/s1600/33.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhd4ORkWIZ5B21qIV-ftMDfBfi2CAa25p4_7c7g0cVaBn6uOGZ80t7OWQ3xlAHqabZVcM61DYych6CcDW0N0SEuOg-RWUF1-V3kbTlj2vjqbZS6yt8snngKv3k8otaVlaDpivJwth-iYu8/s1600/33.png" /></a></div>
<br />
Тем более что граф показывает данный блок в виде одной функции с одной точкой входа и одним выходом.<br />
<br />
(Кстати предположение о том, что в этом блоке принимается решение, в итоге оказалось не верным).<br />
<br />
Для отладки я использовал режим CPU-View непосредственно самой Delphi (этого оказалось достаточным за глаза).<br />
Отлаживаемым приложением было "decompiled_vm_text.exe" с которого и снималась трасса исполнения.<br />
<br />
В качестве вспомогательных подсказок в "Watch List" были добавлены три переменных:<br />
<br />
1. Текущее значение курсора рабочего буфера (регистра ESI), которое вычислялось разницей от адреса начала рабочего буфера (pWorkBuff) и текущим значением регистра. (esi-$4E5008)<br />
2. Значение текущей ячейки pWorkBuff (pbyte(esi)^)<br />
3. Оно-же только в HEX режиме.<br />
4. Окно дампа было настроено на начало рабочего буфера VM ($4E5008).<br />
<br />
ЗЫ: у вас адрес рабочего буфера может быть другой, поэтому число 4E5008 вы должны вычислить заранее при начале отладки, подсмотрев это значение в переменной pWorkBuff.<br />
<br />
Таким образом, настроенный отладчик, остановленный на начале процедуры "@vm_code_39D7", выглядел так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIvQcuq71UvlrC-n0DJaHQqye9qvgb5bA2tGYniL4U-nNzYg4UzjlG0aY8lDZwwDE_9YyI_kSQ2kzuPndybtBIN5IFw9aG-V5ELHM0-Vy4ZwCk6tBdH80zB0zsU8E2pxhBveU5Nkxh9-w/s1600/34.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIvQcuq71UvlrC-n0DJaHQqye9qvgb5bA2tGYniL4U-nNzYg4UzjlG0aY8lDZwwDE_9YyI_kSQ2kzuPndybtBIN5IFw9aG-V5ELHM0-Vy4ZwCk6tBdH80zB0zsU8E2pxhBveU5Nkxh9-w/s1600/34.png" /></a></div>
<br />
Собственно с этой процедуры и приступим.<br />
Если вы обратите внимание на картинку графа, это самая первая процедура в свернутом блоке выполнения, который вызывает сам себя (while цикл).<br />
<br />
На момент вызова данной процедуры, курсор рабочего буфера установлен на ячейку №27 (на картинке с картой памяти выделена красным), в которой содержится некое число (40 или $28). Эта ячейка находится как раз перед буфером с логином и серийным номером (расположенным по офсету 28, в котором хранится первый символ логина - "M").<br />
Запомните номер этой ячейки - она является одним из центральных элементов логики VM.<br />
<br />
Давайте посмотрим исходный код свернутого блока:<br />
<br />
<pre class="brush:asm">@vm_code_39D7:
cmp byte ptr [esi], 0 // равно ли значение ячейки №27 нулю?
jne @vm_code_39D8 // если нет, ищем правый ноль
@vm_code_39D8:
add esi, 2 // от текущего номера ячейки прыгаем на 2 ячейки вправо
cmp byte ptr [esi], 0 // ячейка равна нулю?
jne @vm_code_39D8 // если нет, переходим к следующей
@vm_code_39D7_39D9: // нашлась ячейка равная нулю?
inc byte ptr [esi] // увеличиваем ее значение на 1
cmp byte ptr [esi], 0
jne @vm_code_39DD // и переходим на поиск ячейки #27
@vm_code_39DD:
sub esi, 2 // ищем первый ноль слева (через 2 ячейки),
// т.к. справа все уже заполнено единицами, то первый ноль будет перед ячейкой #27
cmp byte ptr [esi], 0
jne @vm_code_39DD
@vm_code_39D7_39DE:
add esi, 2 // нашли ячейку #27
dec byte ptr [esi] // декрементируем ее значение
cmp byte ptr [esi], 0 // и если оно не равно нулю, пеереходим к началу цикла
jne @vm_code_39D7
jmp @vm_code_397A_39D8 // цикл завершен
</pre>
<br />
Здесь я выстроил блоки исполнения в порядке их исполнения (не в том виде, как они идут в исходном коде), чтобы было проще проанализировать логику из работы.<br />
<br />
Итак, что здесь происходит?<br />
Данный алгоритм инициализирует нули (которые разделяют каждый элемент логина/серийного номера, а так-же "плохую" и "хорошую" строки) единицами.<br />
<br />
Если прерваться на финализации цикла и посмотреть на результат, то можно увидеть вот такую картинку:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZSUd1d06QpMEp-8pfGmWVuQyguw3-hv1Vnd0DyOuBDgqBS0UW9nDrK-sjHBnrp0Y_mxSKEGS0CTujvhSD9MsbRrl9PhQbiXOWQruq0gUbLigy-uKa51ZIugio4F23l-Y5fjwfgA4HsG4/s1600/35.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZSUd1d06QpMEp-8pfGmWVuQyguw3-hv1Vnd0DyOuBDgqBS0UW9nDrK-sjHBnrp0Y_mxSKEGS0CTujvhSD9MsbRrl9PhQbiXOWQruq0gUbLigy-uKa51ZIugio4F23l-Y5fjwfgA4HsG4/s1600/35.png" /></a></div>
<br />
Ячейка №27 будет обнилена, но как результат можно увидеть что буфер будет проинициализирован единицами, заканчивающимися как раз перед первым выводимым VM символом "хорошей строки", а именно символом "С".<br />
<br />
Обратите внимание как интересно выполнены переходы в начало и конец изменяемых данных (процедуры @vm_code_39D8 и @vm_code_39DD). В качестве детектирования концевых блоков используется сверка значения текущей ячейки с нулем, самый левый ноль будет означать позицию ячейки №27 (со сдвигом), а самый правый ноль будет означать еще не инициализированную единицей ячейку.<br />
<br />
Таким образом суть данного блока сводится к постройке индекса (в виде единиц) к ячейке, с которой будет работать следующий while цикл, начинающийся с процедуры @vm_code_39EB.<br />
<br />
Он начнет свою работу с ячейки, на которую указывает последняя единица выстроенного ранее индекса, а именно с ячейки №108, где как раз и расположен символ "С" (выделен синим), который VM сейчас должна вывести во внешний буфер.<br />
<br />
Перед вызовом данной процедуры VM успела произвести некоторые подготовительные действия, одним из которых было инициализация ячейки №27 единицей, т.о. самая левая ячейка, на которую выйдет алгоритм поиска левого конца индекса с этого момента, будет ячейка №26.<br />
<br />
Итак:<br />
<br />
<pre class="brush:asm">@vm_code_39EB:
dec esi // сейчас находимся на ячейке №108, сдвигаемся и
cmp byte ptr [esi], 0 // проверяем, выставлен ли на нее индекс
jne @vm_code_39ED
@vm_code_39ED:
sub esi, 2 // если да, то
cmp byte ptr [esi], 0 // ищем левый ноль (перед ячейкой №26)
jne @vm_code_39ED
jmp @vm_code_39EB_39EE
@vm_code_39EB_39EE:
inc esi // сдвигаемся с индекса на ячейку №26
inc byte ptr [esi] // инкрементируем значение ячейки №26
sub esi, 20 // сдвигаемся на ячейку №6
inc byte ptr [esi] // инкрементируем и ее
add esi, 21 // прыгаем на начало индексного массива единиц
cmp byte ptr [esi], 0
jne @vm_code_3A1D
@vm_code_3A1D:
add esi, 2 // и ищем правый конец индекса (ячейку указывающую на 108)
cmp byte ptr [esi], 0
jne @vm_code_3A1D
jmp @vm_code_39EB_3A1E // сюда приходим тогда, когда закончился индекс
@vm_code_39EB_3A1E:
dec esi // сдвигаемся на ячейку №108
dec byte ptr [esi] // декремнтируем ее и
cmp byte ptr [esi], 0 // пока она не равна нулю, крутим цикл с самого начала...
jne @vm_code_39EB
jmp @vm_code_397A_39EC // выход
</pre>
<br />
Логика второго цикла следующая: он переносит значение ячейки №108 в ячейки за номером №26 и №6 (<b>обратите внимание</b> - вот оно, шестая ячейка из которой и забирается символ, на которую я указал еще в седьмой главе).<br />
<br />
И собственно уже начинается вырисовываться сама логика работы с данными (при чтении значения происходит дубляж значения по двум адресам).<br />
<br />
Карта памяти начинает выглядеть следующим образом:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrcfY9SZLEJ1M0qPbyoWiz-q8es6nSkzLWYa0vbZs6OwxEWC3SAHBXUzH2sBc07099HztQcF-kI0BZ0wB6f44uxMy4gaGhVKuCF8HEnLLrRTrR8Jy3393wII9ClGyLVY1kzHbt-OqQZys/s1600/36.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrcfY9SZLEJ1M0qPbyoWiz-q8es6nSkzLWYa0vbZs6OwxEWC3SAHBXUzH2sBc07099HztQcF-kI0BZ0wB6f44uxMy4gaGhVKuCF8HEnLLrRTrR8Jy3393wII9ClGyLVY1kzHbt-OqQZys/s1600/36.png" /></a></div>
<br />
Зелеными прямоугольниками выделены места, с которыми произошли изменения, класным выделена ячейка №108 из которой забиралось значение.<br />
<br />
Следующий while цикл в графе начинается с процедуры "@vm_code_3A2A".<br />
<br />
Он начнет сою работу с ячейки №26, в которой хранится копия числа, ранее размещенного в ячейке №108.<br />
<br />
Код выглядит следующим образом:<br />
<br />
<pre class="brush:asm">@vm_code_3A2A:
inc esi // прыгаем на начало индексного массива единиц
cmp byte ptr [esi], 0
jne @vm_code_3A2C
@vm_code_3A2C:
add esi, 2
cmp byte ptr [esi], 0 // ищем конец индекса, указывающий на ячейку №108
jne @vm_code_3A2C
jmp @vm_code_3A2A_3A2D
@vm_code_3A2A_3A2D:
dec esi // переходим с индекса на ячейку №108
inc byte ptr [esi] // и инкрементируем ее значние
dec esi // сдвинаемся обратно на индекс
cmp byte ptr [esi], 0
jne @vm_code_3A33
@vm_code_3A33:
sub esi, 2 // ищем начало индекса
cmp byte ptr [esi], 0
jne @vm_code_3A33
jmp @vm_code_3A2A_3A34 // сюда приходим тогда, когда закончился индекс
@vm_code_3A2A_3A34:
inc esi // сдвигаемся на ячейку №26
dec byte ptr [esi] // декрементируем ее
cmp byte ptr [esi], 0 // если она не равна нулю
jne @vm_code_3A2A // крутим цикл
jmp @vm_code_397A_3A2B // выход
</pre>
<br />
Задача данного кода переместить значение яз ячейки №26 обратно в ячейку №108, восстановив таким образом исходное значение ячейки, с которой алгоритм начал свою работу.<br />
<br />
Посмотрим на карту памяти после работы алгоритма:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3hABhfFo1jQb8nvM_CaXfq2NLD8wZoLhhRsa522yc_Kg1dB-YCXpJMbhyphenhyphenCm9R4WkU8ginEjcWMn6TnUWQ78PaPfMMeVOWnkYyNU3PHbNZFL4YJTqYjK98Nr98sUomlNyV35HLTrbh9O8/s1600/37.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3hABhfFo1jQb8nvM_CaXfq2NLD8wZoLhhRsa522yc_Kg1dB-YCXpJMbhyphenhyphenCm9R4WkU8ginEjcWMn6TnUWQ78PaPfMMeVOWnkYyNU3PHbNZFL4YJTqYjK98Nr98sUomlNyV35HLTrbh9O8/s1600/37.png" height="221" width="320" /></a></div>
<br />
<br />
И самым последним шагом в этой немного запутанной логике работы с ячейками будет финал, расположенный в процедуре "@vm_code_3A41"<br />
<br />
Она достаточно простая:<br />
<br />
<pre class="brush:asm">@vm_code_3A41:
dec byte ptr [esi]
sub esi, 2
cmp byte ptr [esi], 0
jne @vm_code_3A41
</pre>
<br />
Вся ее задача, начать работу с самой последней ячейки равной единице (посредством которой и выстроен индекс) и убрать все единицы индекса.<br />
<br />
Результатом ее работы будет вот такая карта:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg33m9n2cnAa8AKKsTjex0ueNmdOFud-IC7yp4kIj7_5PvzEAl0VlnBUFUQ6lKyuwSooSDtjNh_w4XOzRC9USjQvpe3K6JlthbmrnSMvU8a2SD5WTVLZ2HEf2_M5uRnjIlclu-HDBPjURI/s1600/38.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg33m9n2cnAa8AKKsTjex0ueNmdOFud-IC7yp4kIj7_5PvzEAl0VlnBUFUQ6lKyuwSooSDtjNh_w4XOzRC9USjQvpe3K6JlthbmrnSMvU8a2SD5WTVLZ2HEf2_M5uRnjIlclu-HDBPjURI/s1600/38.png" height="226" width="320" /></a></div>
<br />
Зеленым отмечены поля индекса, который убрала последняя процедура, а красным отмечен результат работы всех четырех свернутых блоков.<br />
<br />
Запутались?<br />
<br />
Ничего странного, но если внимательно взглянуть на самую последнюю картинку, то можно понять, что вся эта навороченная логика из четырех этапов по сути выполняет одну простую операцию.<br />
<br />
И операция эта - присвоение значения ячейки "А" ячейке "B".<br />
Причем алгоритм достаточно гибок и позволяет таким образом прочитать значение любого символа логина/серийного номера или "хорошей/плохой" строк, для этого достаточно проинициализировать ячейку №27 номером символа и выполнить все четыре вышеописанных этапа.<br />
<br />
Красивый подход - ничего не скажешь, достаточно солидно размазанная логика работы с ячейками, правда есть небольшой нюанс.<br />
Поняв как происходит получение значения ячейки и имея на руках граф исполнения, проанализовать всю логику работу с этого момента займет от силы полтора часа.<br />
<br />
Остальные процедуры с верхней картинки с графом рассматривать смысла не имеет, там сокрыт какой-то внутренний матаппарат, необходимый для работы VM, есть откровенно мусорные куски кода (например в цикле меняющие значение двух/трех ячеек местами и возвращающими в итоге все в исходное состояние).<br />
<br />
<h3 style="text-align: left;">
12. Разбор элементов логики на составляющие.</h3>
<br />
Таким образом выяснилось что получение значения ячейки происходит посредством выполнения трех циклов, которые можно наглядно увидеть на графе и заключительного блока финализации, убирающего массив индексов сразу после окончания работы третьего цикла.<br />
<br />
Вот на этой картинке я их сразу выделил красными прямоугольниками и отталкивался от данного изображения на протяжении всего разбора логики VM:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwjDYCt4PASbTU5j8qPRz67BmDH4w1UcEm46hEYxJs1s2urnCwZAT6iJnlET3De2DHwCGQEvQNk9ZvVzGFNmKBk1RVqaQwzglwA0RPypFdDZlcC1UvdheydED0EoqgmCEVv8PgUBm_Ar4/s1600/39.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwjDYCt4PASbTU5j8qPRz67BmDH4w1UcEm46hEYxJs1s2urnCwZAT6iJnlET3De2DHwCGQEvQNk9ZvVzGFNmKBk1RVqaQwzglwA0RPypFdDZlcC1UvdheydED0EoqgmCEVv8PgUBm_Ar4/s1600/39.png" height="508" width="640" /></a></div>
<br />
Двумя синими прямоугольниками выделены так-же циклы чтения значения ячеек, но т.к. в них зачитывается значения символов Login[0] и Login[1] (это я узнал проанализировав все циклы алгоритма), то индексный массив из единиц в этих двух случаях не строится, в первом цикле зачитывается значение в ячейки №26 и №6, а во втором значение из ячейки №26 возвращается обратно.<br />
<br />
На картинке не совсем хорошо видно (сказывается zoom), чтобы было более понятно, вот второй вариант, который показывает принцип, по которому я обводил блоки прямоугольниками:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9mOXctKZZZkFy0unQip9v2au5poMUg3qsaKsDdILhThIsbxWm61_3mjJI_JgihUdJsRrnCqAryEd5wNAl2_qi9-M39jy8U31hHXJeMO9c83SmCgSj3lrnqKIsa1jv2yKq8MrB3JR2te_O/s1600/42.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9mOXctKZZZkFy0unQip9v2au5poMUg3qsaKsDdILhThIsbxWm61_3mjJI_JgihUdJsRrnCqAryEd5wNAl2_qi9-M39jy8U31hHXJeMO9c83SmCgSj3lrnqKIsa1jv2yKq8MrB3JR2te_O/s1600/42.png" /></a></div>
<br />
<br />
Чтобы проверить мое предположение я проконтролировал самого себя и отобразил на графе блоки, в которых происходит изменение полей логина и серийного номера (кнопка "Показать доступ к буферу с логином и SN"). По данной кнопке вьювер подгрузит список таких процедур, созданный ранее трассировщиком в режиме ttCheckLoginBuff и покажет вот такую картинку:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBq_dpg6Q0gdv36sNfRjSE9XaqgKkvqU45ShTVhX6TJR-SGLnSPwBLX5oyGPFSVQhRk-uuskVoCkZXIilpTqobAAaOb4ksXBjrqdllKtSNdx8Io8nmVfMDUWX9yNNr8dU65sNDtFHUQX4/s1600/40.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBq_dpg6Q0gdv36sNfRjSE9XaqgKkvqU45ShTVhX6TJR-SGLnSPwBLX5oyGPFSVQhRk-uuskVoCkZXIilpTqobAAaOb4ksXBjrqdllKtSNdx8Io8nmVfMDUWX9yNNr8dU65sNDtFHUQX4/s1600/40.png" height="562" width="640" /></a></div>
<br />
Все как и предполагалось на предыдущем изображении, единственно из логики выбивается блок слева сверху (обведенный синим прямоугольником), это участок кода, который зачитывает значения логина и серийного номера из внешнего буфера, поэтому нет ничего удивительного, что в нем происходит изменения данных полей.<br />
<br />
Здесь я сделал вывод о том, что по всей видимости каждый из вертикальных блоков - это один из этапов проверки серийного номера (предположение так же оказалось верным).<br />
<br />
Следующим шагом мне стало интересно, а где вообще принимается решение о правильности серийного номера, для этого я пересобрал пример "decompiled_vm_text.exe" в котором изменил значение серийного номера на нули, и снова снял трассу в режиме ttWrongSN.<br />
После всех этих шагов я посмотрел различия трассы (кнопка "Показать различия трассы с неверным SN") в результате увидел следующее:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3ik_mYODomxPeYWCJqvNe-uIaFgS8E-OJ_zyDKBdJYRAjbSIsp7EDcdrQ1QTDFGPVeY8Xep72dBzNe9dmW4DGUXoOGLRNMIbEpaHZgVLOf6XqjpdVHZRstGpI0rf7nn9dWkmE6AIXd9E/s1600/41.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3ik_mYODomxPeYWCJqvNe-uIaFgS8E-OJ_zyDKBdJYRAjbSIsp7EDcdrQ1QTDFGPVeY8Xep72dBzNe9dmW4DGUXoOGLRNMIbEpaHZgVLOf6XqjpdVHZRstGpI0rf7nn9dWkmE6AIXd9E/s1600/41.png" height="640" width="560" /></a></div>
<br />
Можно сказать - бинго :)<br />
На первой же операции (правый вертикальный блок) произошел выход, причем выполнились не все процедуры (блок отмеченый красным). Значит именно в нем и происходит принятие решение по результатам первой же операции над буфером с логином и серийным номером.<br />
<br />
Точнее таких блоков в итоге будет несколько, для каждой операции свой, впрочем разбирать их логику работы не придется, т.к. смысл всех операций будет достаточно прозрачен.<br />
<br />
Следующим этапом я поставил бряки в отладчике в начале каждого вертикального блока (которые раньше разметил прямоугольниками), для того чтобы понять в каком порядке выполняются операции.<br />
Получился такой список:<br />
<br />
<ol style="text-align: left;">
<li>vm_code_17AB</li>
<li>vm_code_1A63</li>
<li>vm_code_1E74</li>
<li>vm_code_2333</li>
<li>vm_code_28DA - выполняется много раз в цикле</li>
<li>vm_code_2AF3</li>
<li>vm_code_2D27</li>
<li>vm_code_2F41</li>
</ol>
<br />
Осталось, ориентируясь на граф, просто пройтись по всем выделенным местам (пропуская уже известные элементы логики в которых забирается символ логина/SN) и проанализировать какие операции проводятся над этими символами.<br />
<br />
Я не буду давать диапазоны процедур, в которых происходит создание индекса, чтение значения, происходит возврат значения и убирание индекса. Для простоты я буду показывать только наименование процедуры первого цикла (в которой происходит построение индексного массива из единиц).<br />
<br />
Приступим к разбору логики:<br />
<br />
<b>1. vm_code_17AB</b><br />
1.1 vm_code_1815 - читаем значение SN[0] в ячейку №6<br />
1.2 vm_code_17AB_1880 прибавляем к ячейке №6 число 12<br />
1.3 vm_code_1903 - читаем значение SN[4] в ячейку №7<br />
<br />
В результате в обеих ячейках (6, 7) получится одно и то-же число, таким образом делаем вывод, что первая математическая операция производит следующую проверку: SN[4] = SN[0] + 12<br />
<br />
<b>2. vm_code_1A63</b><br />
2.1 vm_code_1AD4 - читаем значение SN[0] в ячейку №6<br />
2.2 vm_code_1B58 это значение переносится в ячейку №7<br />
2.3 vm_code_1BC8 - читаем значение SN[3] в ячейку №6<br />
2.4 vm_code_1C4C значение ячейки №6 суммируется с ячейкой №7 (результат в 7)<br />
2.5 vm_code_1A63_1C5A - ячейка №7 увеличивается на число 84<br />
2.6 vm_code_1D12 - читаем значение SN[2] в ячейку №6<br />
<br />
В результате в обеих ячейках (6, 7) получится одно и то-же число, таким образом делаем вывод, что вторая математическая операция производит следующую проверку: SN[2] = SN[0] + SN[3] + 84<br />
<br />
<b>3. vm_code_1E74</b><br />
3.1 vm_code_1EE8 - читаем значение Login[0] в ячейку №6<br />
3.2 vm_code_1F5B - переносим значение из 6 в 7 ячейку<br />
3.3 vm_code_1FDD - читаем значение Login[1] в ячейку №6<br />
3.4 vm_code_204D - значение ячейки №6 суммируется с ячейкой №7 (результат в 7)<br />
3.5 vm_code_20CB - читаем значение SN[4] в ячейку №6<br />
3.6 vm_code_214F - значение ячейки №6 суммируется с ячейкой №7 (результат в 7)<br />
3.7 vm_code_21C3 - читаем значение SN[1] в ячейку №8<br />
<br />
В результате в обеих ячейках (7, 8) получится одно и то-же число, таким образом делаем вывод, что третья математическая операция производит следующую проверку: SN[1] = Login[0] + Login[1] + SN[4]<br />
<br />
<b>4. vm_code_2333</b><br />
4.1 vm_code_239B - читаем значение Login[8] в ячейку №7<br />
4.2 vm_code_2493 - читаем значение Login[4] в ячейку №6<br />
4.3 vm_code_2517 - значение ячейки №6 суммируется с ячейкой №7 (результат в 7)<br />
4.4 vm_code_2586 - читаем значение Login[2] в ячейку №6<br />
4.5 vm_code_260A - значение ячейки №6 отнимается от ячейки №7 (результат в 7)<br />
4.6 vm_code_2689 - читаем значение SN[5] в ячейку №6<br />
<br />
В результате в обеих ячейках (6, 7) получится одно и то-же число, таким образом делаем вывод, что четвертая математическая операция производит следующую проверку: SN[5] = Login[8] + Login[4] - Login[2]<br />
<br />
<b>5. vm_code_28DA</b> - в цикле проходим по всем 10 символам логина и суммируем их с ячейкой №7<br />
<br />
Грубо в результате пятой математической операции выполняется вот такой код:<br />
<br />
<pre class="brush:delphi">Tmp := 0;
for I := 0 to 9 do
Inc(Tmp, Login[I]); // значение логина "Ms-Rem" в результате будет равно $11
</pre>
<br />
Сумма всех символов логина останется в ячейке №7<br />
<br />
<b>6. vm_code_2AF3</b><br />
6.1 vm_code_2bc5 - читаем значение SN[8] в ячейку №6<br />
<br />
В результате в обеих ячейках (6, 7) получится одно и то-же число, таким образом делаем вывод, что шестая математическая операция производит следующую проверку: SN[8] = Tmp (сумма всех чисел логина)<br />
<br />
<b>7. vm_code_2D27</b><br />
7.1 vm_code_2D27_2D29 - к ячейке №7 прибавляется число 72<br />
7.2 vm_code_2DD5 - читаем значение SN[9] в ячейку №8<br />
<br />
В результате в обеих ячейках (7, 8) получится одно и то-же число, таким образом делаем вывод, что седьмая математическая операция производит следующую проверку: SN[9] = Tmp (сумма всех чисел логина) + 72<br />
<br />
<b>8. vm_code_2F41</b> (на момент вызова этой процедуры сумма чисел логина будет лежать в ячейке №18)<br />
8.1 vm_code_2FA1 - читаем значение SN[6] в ячейку №6<br />
8.2 vm_code_2F41_300C - ячейка №6 увеличивается на число 51<br />
8.3 vm_code_3071_307D + vm_code_308B перекидываем Tmp в ячейку №7<br />
8.4 vm_code_30AA - значение ячейки №7 суммируется с ячейкой №6 (результат в 6)<br />
8.5 vm_code_311D - читаем значение SN[7] в ячейку №7<br />
<br />
В результате в обеих ячейках (6, 7) получится одно и то-же число, таким образом делаем вывод, что восьмая математическая операция производит следующую проверку: SN[7] = SN[6] + 51 + Tmp<br />
<br />
Вот собственно и весь алгоритм, сокрытый в виртуальной машине, как на ладони.<br />
Можно реализовать его в виде кода:<br />
<br />
<pre class="brush:delphi">program keygenme_source;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
Math;
function CheckSerial(const ALogin, ASerial: AnsiString): string;
const
ValidSN = 'Congratulations!!! It is valid serial!';
InvalidSN = 'Serial invalid :(';
var
Login: array [0..9] of Byte;
Serial: array [0..9] of Byte;
I, A, B, Tmp: Byte;
Checked: Boolean;
begin
ZeroMemory(@Login[0], 10);
Move(ALogin[1], Login[0], Min(10, Length(ALogin)));
// инициализируем серийник
for I := 0 to 9 do
begin
A := Byte(ASerial[1 + I * 2]);
B := Byte(ASerial[2 + I * 2]);
if A > $39 then
Dec(A, $37)
else
Dec(A, $30);
if B > $39 then
Dec(B, $37)
else
Dec(B, $30);
A := A shl 4;
Serial[I] := A or B;
end;
// проверка серийника
Checked := True;
// первый этап
if Serial[4] <> Byte(Serial[0] + 12) then
Checked := False;
// второй этап
if Serial[2] <> Byte(Serial[0] + Serial[3] + 84) then
Checked := False;
// третий этап
if Serial[1] <> Byte(Login[0] + Login[1] + Serial[4]) then
Checked := False;
// четвертый этап
if Serial[5] <> Byte(Login[8] + Login[4] - Login[2]) then
Checked := False;
// пятый этап
Tmp := 0;
for I := 0 to 9 do
Inc(Tmp, Login[I]);
// шестой этап
if Serial[8] <> Tmp then
Checked := False;
// седьмой этап
if Serial[9] <> Byte(Tmp + 72) then
Checked := False;
// восьмой этап
if Serial[7] <> Byte(Serial[6] + 51 + Tmp) then
Checked := False;
// вывод результата
if Checked then
Result := ValidSN
else
Result := InvalidSN;
end;
begin
Writeln(CheckSerial('Ms-Rem', 'C38FB7A0CF38F73B1159'));
Readln;
end.
</pre>
<br />
Ну что... вот практически и все, последний третий конверт снят.<br />
Осталось совсем чуть-чуть.<br />
<br />
<h3 style="text-align: left;">
13. Пишем генератор серийных номеров.</h3>
<br />
В задаче keygenme шло условие - необходимо его закейгенить, т.е. написать алгоритм, который будет генерировать серийный номер на основе введенного логина.<br />
<br />
Имея на руках алгоритм проверки серийного номера сделать это достаточно тривиально.<br />
Если посмотреть на поля серийного номера, то можно увидеть, что SN[0], SN[3] и SN[6] не проверяются, они только участвуют при проверке значений других полей. Стало быть эти три поля могут содержать абсолютно любые значения, а остальные поля будут рассчитываться уже на их основе.<br />
<br />
Таким образом код генератора будет выглядеть следующим образом:<br />
<br />
<pre class="brush:delphi">program serial_generator;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
Math,
SysUtils;
function GetSN(const ALogin: AnsiString): string;
var
Login: array [0..9] of Byte;
Serial: array [0..9] of Byte;
I, Tmp: Byte;
begin
ZeroMemory(@Login[0], 10);
Move(ALogin[1], Login[0], Min(10, Length(ALogin)));
Randomize;
// эти три поля не проверяются и могут содержать любые значения
Serial[0] := Random(255);
Serial[3] := Random(255);
Serial[6] := Random(255);
// генерируем число для проверки первого этапа
Serial[4] := Serial[0] + 12;
// генерируем число для проверки второго этапа
Serial[2] := Serial[0] + Serial[3] + 84;
// генерируем число для проверки третьего этапа
Serial[1] := Login[0] + Login[1] + Serial[4];
// генерируем число для проверки четвертого этапа
Serial[5] := Login[8] + Login[4] - Login[2];
// запоминаем сумму символов логина получаемом на пятом этапе
Tmp := 0;
for I := 0 to 9 do
Inc(Tmp, Login[I]);
// генерируем число для проверки шестого этапа
Serial[8] := Tmp;
// генерируем число для проверки седьмого этапа
Serial[9] := Tmp + 72;
// генерируем число для проверки восьмого этапа
Serial[7] := Serial[6] + 51 + Tmp;
// выводим результат в виде набора символов в HEX представлении
Result := '';
for I := 0 to 9 do
Result := Result + IntToHex(Serial[I], 2);
end;
begin
Writeln(GetSN('Rouse_'));
Readln;
end.
</pre>
<br />
В итоге вот небольшой список серийных номеров для логина "Rouse_":<br />
<br />
<ol style="text-align: left;">
<li>5E2BB2006AF04EEE6DB5</li>
<li>C5929D84D1F0F9996DB5</li>
<li>BC894434C8F0BA5A6DB5</li>
<li>14E19B3320F0B2526DB5</li>
</ol>
<br />
Keygenme решен.<br />
<br />
<h3 style="text-align: left;">
14. Выводы</h3>
<br />
Давайте заново пройдемся по этапам и вспомним, какие методы применялись для сокрытия алгоритма проверки серийного номера:<br />
<br />
1. Сброс точки входа в ноль - метод спорный, более того на исполняемые файлы, в которых применяется такой трюк, антивирусы будут смотреть с большим подозрением, ибо обычный компилятор никогда не сгенерирует такой исполняемый файл, стало быть кто-то его модифицировал что является сигналом для антивируса.<br />
<br />
2. Шифрование тела исполняемого файла - в принципе не наказуемо, но как было показано выше, снимается такое шифрование достаточно просто и не представляет из себя серьезного препятствия.<br />
<br />
3. Код декриптора обильно разбавленный мусором - спорное решение, достаточно просто снимается из-за того что в качестве мусора не использовались мусорные блоки. Ну например вместо инструкции ADD EAX, 2 можно написать вот такой мусорный блок (первое, что пришло в голову):<br />
<br />
<pre class="brush:delphi"> asm
inc eax // первая реальная инструкция
// начало мусорного блока
push eax
lea eax, @label
inc eax
xchg eax, [esp]
call @label
@label:
ret
// конец мусорного блока
inc eax // вторая реальная инструкция
end;
</pre>
<br />
Такой мусорный блок уже не снять на автомате скриптом, детектирующим отсутствие изменений в регистрах, т.к. каждая инструкция в реальности будет изменять их значения.<br />
Если над таким блоком хорошо подшаманить и раздуть до большого размера, то определить начало и конец мусора будет достаточно проблематично. Тем более всегда есть в наличии стек, сохранив значение регистров на нем можно, в качестве мусора, хоть теорему Ферма доказывать до пока не надоест, после чего просто восстановить значения регистров и продолжить выполнение программы :)<br />
<br />
4. Виртуальная машина - из-за незащищенных хэндлеров (обработчиков инструкций VM) написать ее аналог не составило больших времязатрат. По хорошему в боевом приложении хендлеры VM должны быть качественно обфусцированны, для затруднения понимания логики работы VM. Понятно, что для keygenme этот этап излишен, но не стоит забывать о такой тонкости.<br />
<br />
5. Логика работы PiCode - не смотря на наличие явно мусорных инструкций, основной алгоритм получения данных из буфера с логином/SN оказался единым на всех этапах, что дало возможность достаточно быстро разобрать всю логику на основе шаблона. Если бы было введено несколько вариантов получения данных (а лучше свой уникальный вариант для получения каждого символа) это бы сильно затруднило анализ.<br />
<br />
6. Анализ VM усложнился бы в разы, если бы внутри PiCode сидела еще одна виртуальная машина (допустим на базе того-же Brainfuck) которая бы и выполняла интерпретацию своего собственного PiCode.<br />
<br />
<b>Что в итоге:</b><br />
<br />
Очень качественно выполненный keygenme, великолепно показывающий работу с виртуальной машиной. Единственный нюанс, на который он не дает ответа - каким образом был получен PiCode для виртуальной машины, который она исполняет :)<br />
<br />
Этот этап индивидуален для каждого разработчика, каждый применяет свои методы, впрочем в планах у меня есть статья по реализации алгоритма генерации пикода, правда для немного другого типа VM.<br />
<br />
Со своей стороны я показал один из вариантов взлома таких VM, без этапа деобфускации асм листинга, используя в качестве инструментария граф исполнения VM.<br />
Я думаю это сможет помочь вам при анализе собственных реализаций VM, на предмет их стойкости к подобному варианту взлома.<br />
<br />
Ну а если вы еще не подошли к собственно реализации VM, по крайней мере теперь вы имеете минимальное представление о том, как она может работать :)<br />
<br />
Исходный код демопримеров к статье можно забрать по этой ссылке: <a href="http://rouse.drkb.ru/blog/vm_analize.zip" target="_blank">http://rouse.drkb.ru/blog/vm_analize.zip</a><br />
<br />
Делайте выводы и удачи :)<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
© Александр (Rouse_) Багель</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Апрель, 2014</div>
</div>
<br />
<br /></div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com15tag:blogger.com,1999:blog-2374465879949372415.post-6832047570436523162014-02-24T22:57:00.002+04:002014-02-27T11:23:50.918+04:00Ошибка загрузки в RichEdit большого блока данных<div dir="ltr" style="text-align: left;" trbidi="on">
Давеча пришлось дорабатывать одну из утилит сбора информации о системе и как-то неожиданно для меня от отдела тестирования пришел багрепорт плана:<br />
Утилита формирует и сохраняет данные правильно, но загружает их в искаженном виде.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSjxAs-rUScCL7hyuYpOWylv9ndRxctykNlTky-QX3yNs6CQs4JKH9rGa4bjJjOdxFenLqy5zqT1CuOTuTYo1vySO4pokZU0-F4kpPKzL5qP88ojC7cwfdIjdjyFCGQLiE1K7H_wydcnhE/s1600/Untitled.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSjxAs-rUScCL7hyuYpOWylv9ndRxctykNlTky-QX3yNs6CQs4JKH9rGa4bjJjOdxFenLqy5zqT1CuOTuTYo1vySO4pokZU0-F4kpPKzL5qP88ojC7cwfdIjdjyFCGQLiE1K7H_wydcnhE/s1600/Untitled.png" /></a></div>
<br />
Ну точнее не то, чтобы неожиданно...<br />
С чем-то подобным я уже ранее встречался, когда писал компаратор карт памяти процесса, <a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html" target="_blank">вот для этой утилиты</a>, но тогда, если честно, не сильно обратил на этот нюанс внимание, ибо тогда мне нужна была скорость загрузки информации в RichEdit.<br />
<br />
Но та работа была в виде хобби, а тут собственно рабочий проект.<br />
<br />
<a name='more'></a><br />
Если честно, причины такого поведения я не знаю, да и разбираться не сильно есть время, поэтому просто приведу решение данной проблемы.<br />
<br />
Такой код загружает файл большого объема в RichEdit с ошибками как на верхней картинке:<br />
<br />
<pre class="brush:delphi">RichEdit1.Lines.LoadFromFile('updates.rtf');
</pre>
<br />
А вот такой, грузит правильно:<br />
<br />
<pre class="brush:delphi">function RichEditStreamCallBack(Cookie: TMemoryStream; pbBuff: PByte;
cb: Longint; var pcb: Longint): Longint; stdcall;
begin
Result := 0;
try
pcb := Cookie.Read(pbBuff^, cb);
except
Result := 1;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
M: TMemoryStream;
Param: TEditStream;
begin
M := TMemoryStream.Create;
try
M.LoadFromFile('updates.rtf');
{$IFDEF WIN64}
Param.dwCookie := DWORD_PTR(M);
{$ELSE}
Param.dwCookie := LongInt(M); // LongInt для совместимости со старыми версиями Delphi
{$ENDIF}
Param.dwError := 0;
param.pfnCallback := @RichEditStreamCallBack;
SendMessage(RichEdit2.Handle, EM_STREAMIN, SF_RTF, LPARAM(@Param));
finally
M.Free;
end;
end;
</pre>
<br />
Сравнительный результат:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgd3-ixC-CzWZCMiK9SA5Ked_84Y1bbOeTwcwZ64Sl00hAvtyYcOQ-HL1fviD8UYHU-CL5kTTwo4uMCtNIGVtfRw9dv-Uf9Sddhg8oyhPBFb6fYz_N-671X8kIqlxOc-tAz0P_FLxlCmFEg/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgd3-ixC-CzWZCMiK9SA5Ked_84Y1bbOeTwcwZ64Sl00hAvtyYcOQ-HL1fviD8UYHU-CL5kTTwo4uMCtNIGVtfRw9dv-Uf9Sddhg8oyhPBFb6fYz_N-671X8kIqlxOc-tAz0P_FLxlCmFEg/s1600/1.png" /></a></div>
<br />
<br />
Сам код: <a href="http://rouse.drkb.ru/blog/rtf.zip" target="_blank">http://rouse.drkb.ru/blog/rtf.zip</a><br />
<br />
Судя по всему ошибка внутри метода загрузки в VCL обертке.</div>Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com15tag:blogger.com,1999:blog-2374465879949372415.post-1932256457309938512014-02-14T22:28:00.003+04:002014-02-17T10:22:00.612+04:00Есть ли жизнь после триала?<div dir="ltr" style="text-align: left;" trbidi="on">
Предпродажная демонстрация возможностей ПО обычно сводится к двум решениям:<br />
<ol>
<li>Ограничение функциональности ПО, так называемый демо-комплект.</li>
<li>Введение временного (тестового) периода, в течении которого ПО работает с полным функционалом, так называемая триальная версия (или попросту - триал).</li>
</ol>
<div>
Есть, конечно, варианты демонстрации возможностей в офисе разработчика/продавца, но это не относится к программированию и скорее всего называется менеджментом (или как там это умное слово называются?) :)</div>
<div>
<br /></div>
<div>
Как только на руки взломщику попадает именно триальная версия - он начинает довольно потирать руки, так как все что от него требуется, это заставить приложение думать, что его триальный период еще не истек.</div>
<div>
<br /></div>
<div>
А это, как правило, сделать довольно просто.</div>
<div>
Есть такая старая поговорка: если что-то запустилось и работает, можно запустить и заставить работать неограниченное количество раз :)</div>
<div>
<br /></div>
<div>
В данной статье я попробую рассказать почему в 99 процентах случаев сделать грамотную триальную защиту не возможно и постараюсь дать точки опоры, от которых можно будет оттолкнуться при реализации такого вида защиты.</div>
<div>
<br /></div>
<div>
<a name='more'></a><br /></div>
<h3>
0. Слоган: "Я самая лучшая программа для создания триала!!!"</h3>
<div>
<br /></div>
<div>
Видели такое (или вариации данного слогана)?<br />
Если да, то смело закрывайте такой сайт - вам предложат мусор.</div>
<div>
<br /></div>
<div>
На данный момент не существует протекторов, готовых предоставить более-менее серьезную триальную защиту. И хотя в большинстве навесных защит такая опция присутствует - полагаться на нее особого смысла нет.</div>
<div>
<br /></div>
<div>
Суть триала заключается в следующем:</div>
<div>
<ol>
<li>Необходимо где либо прописать время и дату установки ("хвост")</li>
<li>При старте приложения сверить дату из "хвоста" с текущей, и если истек триальный период - завершить работу приложения.</li>
</ol>
</div>
<div>
Очень просто и на самом деле очень сложно.</div>
<div>
<br /></div>
<div>
Суть проблемы заключается в следующем вопросе: а куда спрятать сам "хвост" так чтобы его не мог обнаружить и подменить взломщик?</div>
<div>
<br /></div>
<div>
Вариантов решения этого вопроса множество, впрочем утилит, снимающих все эти "псевдо триалы" в разы больше, впрочем...</div>
<div>
<br /></div>
<h3>
1. Как потерять свой "хвост"?</h3>
<div>
<br /></div>
<div>
Понятие "потеря хвоста" сводится к тому, что в качестве ожидаемых вами данных при его чтении, вы можете получить их в измененном виде, которые вам любезно предоставил взломщик.</div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div>
Вариантов, куда можно сохранить "хвост", в принципе, не много:</div>
<div>
<ol>
<li>В файл на диске</li>
<li>В реестр</li>
<li>Опять же на диск, но в неиспользуемые файловой системой кластеры (с их пометкой как испорченные).</li>
<li>На какой-то внешний сетевой ресурс.</li>
</ol>
</div>
<div>
Все четыре варианта являются уязвимыми по причине того, что есть набор замечательных утилит от Марка Руссиновича и Брюса Когсвела, отслеживающие операции как с файловой системой, с реестром, так и с сетью.</div>
<div>
<br /></div>
<div>
Впрочем большинство триальных приложений эти варианты устраивают.</div>
<div>
<br /></div>
<div>
Некоторые делают их модификации:</div>
<div>
<ol>
<li>К примеру пишут "хвост" не в произвольный файл, а в уже существующий (hosts или win.ini и т.п.)</li>
<li>Пишут в реестр и "скрывают" ключ реестра, добавляя в его наименование нули, или маскируют его под каким либо CLSID.</li>
<li>Если файл расположен на файловой системе NTFS - хранят "хвост" в NTFS потоке.</li>
<li>Часто "хвост" шифруется разработчиком, иногда даже на асиметричных алгоритмах, наивно полагая что отсутствие Private ключа в составе ПО, обеспечит гарантию от подмены.</li>
</ol>
</div>
<div>
Вариантов таких "велосипедов" много.</div>
<div>
<br /></div>
<div>
В свое время, году эдак в 1997-ом, мне была поставлена задача прикрутить триал на некий софт (связанный со складским учетом), причем триал не по времени, а на количество запусков. </div>
<div>
Я не стал излишне заморачиваться и взял в качестве хранилища "хвоста" библиотеку, отвечающую за отображение справки. </div>
<div>
Если честно, я уже не помню как она там называлась, но суть крылась в следующем:</div>
<div>
<ul>
<li>целостность данной библиотеки не контролировалась ОС</li>
<li>в составе ее ресурсов был битмап размерами (1х160) из черных и белых точек (кажется он использовался как картинка разделителя между параграфами)</li>
<li>логика хранения "хвоста" была примитивная - изымался данный битмап, бралось значение первого пикселя (по умолчанию он черный RGB(0, 0, 0)), производился инкремент пикселя на единицу и все писалось обратно. </li>
</ul>
</div>
<div>
Для человеческого глаза что RGB(0, 0, 0), что RGB(10, 0, 0) не заметны, однако для кода защиты ПО - это счетчик.</div>
<div>
<br /></div>
<div>
Очень грубое решение и легко детектируемое - но на тот момент (97 год) оно было достаточно эффективным :)</div>
<div>
<br /></div>
<div>
В современных реалиях ни один из данных способов не может рассматриваться серьезно.</div>
<div>
<br /></div>
<h3>
2. Как происходит подмена"хвоста"</h3>
<div>
<br /></div>
<div>
Это достаточно простой этап (и самый действенный).</div>
<div>
Если хвост не зашифрован - то взломщику нужно все лишь изменить хранящиеся в нем данные.</div>
<div>
<br /></div>
<div>
Вариантов два:</div>
<div>
<ol>
<li>Правим сами данные</li>
<li>Перехватываем функции, которые читают данные и подменяем на свои</li>
</ol>
</div>
<div>
Иногда не нужно производить даже этих действий.</div>
<div>
Если триал завязан на время и не контролирует изменение текущей даты - достаточно просто поменять время в настройках системы. </div>
<div>
Кстати - эта ошибка присутствует у большинства ПО с триальной защитой.<br />
<br />
Более сложный вариант выглядит в виде правки тела приложения и отключения проверки на валидность "хвоста" с ожидаемым.<br />
<br />
Шифрование тут не сильно спасет, ибо если взломщик решился на такой шаг, он скорее всего отреверсит и саму логику шифрации данных или вытащит сам ключ, если используется что-то стандартное.<br />
<br />
На шифрование "хвоста" асиметричным алгоритмом закладываться так же сильно не стоит.<br />
Во первых, кто-то должен поместить зашифрованый приватным ключом "хвост" на диск (как правило это будет делать инсталятор).<br />
Во вторых, достаточно произвести атаку на ключ, сгенерировав свою пару приват/паблик ключей, и изменив публичный ключ в теле приложения, мы сможем выдавать ему валидные (с точки зрения взломанного приложения) данные, зашифрованные нашим приватом.</div>
<div>
<br /></div>
<h3>
3. Как можно уйти от изменения времени</h3>
<div>
<br /></div>
<div>
Вариантов несколько - к примеру вы знаете время установки ОС и время установки своего софта.<br />
Самое логичное заключается в том, что текущая дата не может быть раньше этих двух составляющих, как собственно и время установки софта не может быть меньше чем дата установки ОС.</div>
<div>
Опорными точками могут выглядеть время компиляции системных библиотек (читается из PE заголовка) которое так же не может быть больше текущей даты.</div>
<div>
Одним из хороших решений является чтение системного журнала, по записям которого можно сверится с текущим временем.</div>
<div>
Реестр хранит в себе множество записей с пометками о текущем времени, на основании которых можно сделать вывод о том что время системы было изменено.</div>
<div>
Скорее всего, в случае изменения времени, выхода в интернет у вас не будет, но если он есть - не забывайте о серверах реального времени и сверяйтесь с ними.</div>
<div>
<br /></div>
<h3>
4. И куда деть "хвост"?</h3>
<div>
<br /></div>
<div>
В случае отсутствия внешнего хранилища, неподконтрольного взломщику - только в файл/реестр/внешний сервер.</div>
<div>
<br /></div>
<div>
Впрочем даже тут можно использовать несколько трюков:</div>
<div>
<br /></div>
<div>
К примеру время триала будет хранится в качестве составляющей GUID-а некоего COM сервера, без которого работа приложения не возможна. Нюансы с доступом к такому серверу оставляю на ваше усмотрение.</div>
<div>
<br /></div>
<div>
Вы работаете с базой?</div>
<div>
Если да - храните данные в ней, но не полем, а частью некоего BLOB-а изменяемого при каждом запросе.</div>
<div>
<br /></div>
<div>
Вы пишете драйверы режима ядра?</div>
<div>
Вся FS в вашем распоряжении - определитесь с местом хранения, застрахуйтесь от перехвата собственной правкой таблиц и пишите этот DWORD</div>
<div>
<br /></div>
<div>
Вы храните данные на внешнем сервере? </div>
<div>
Только шифрование - но это утопично.</div>
<div>
<br /></div>
<h3>
5. Так есть ли жизнь после триала?</h3>
<div>
<br /></div>
<div>
В 99 процентах случаев - конечно есть и ваше приложение во всех этих случаях будет гарантированно взломано.</div>
<div>
<br /></div>
<div>
Остается только один махонький процент, который обеспечивает действительный фактический триал разработчику и выглядит это следующим образом:</div>
<div>
<br /></div>
<div>
1. вынос логики работы на внешний сервер, где сервером принимается решение о завершении периода триала.</div>
<div>
2. вынос логики работы на электронный ключ защиты, где по истечению заданного разработчиком периода времени, ключ перестает работать.</div>
<div>
<br /></div>
<div>
Только в этих условиях можно говорить о такой "бутафории" как "триал".</div>
<div>
<br /></div>
<h3>
6. В заключение</h3>
<div>
<br /></div>
<div>
Триал - одна из самых примитивных и не надежных схем защиты ПО.</div>
<div>
Если разработчик пошел на этот шаг и не выбрал реализацию "демо" - видимо были определенные причины.</div>
<div>
<br /></div>
<div>
Впрочем, я хотел сделать только краткий обзор этой темы (без сильного разжевывания нюансов), ибо в последнее время почему-то участились вопросы на форумах о триале.<br />
<br />
Если появились вопросы или требуется уточнение определенных моментов - я всегда на связи.</div>
<div>
<br /></div>
<div>
<span style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.479999542236328px;">Удачи.</span><br />
<br style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.479999542236328px;" />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<span style="font-size: 13px;">© Александр (Rouse_) Багель</span></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Февраль, 2014</div>
</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com14tag:blogger.com,1999:blog-2374465879949372415.post-74975925589273348832014-01-20T15:44:00.000+04:002014-06-26T18:05:24.393+04:00Здравствуйте, я ошибка 217 и я вам ничего не скажу<div dir="ltr" style="text-align: left;" trbidi="on">
Вероятно многие встречались с таким вот "партизаном" при старте или завершении приложения:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBqDIYIBraomUllTpvPngLuJ1Mb6or6J5nMKHuqwdqD1oVRBna-IXH9ns4bkEr6ero-DsJNi3WXvFZ02leNh7uKFRQM3DsvCx6irO46XQp-Iz15EvbsOFgpSm3ZEjY-sGslSXlyPOWu8A/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBqDIYIBraomUllTpvPngLuJ1Mb6or6J5nMKHuqwdqD1oVRBna-IXH9ns4bkEr6ero-DsJNi3WXvFZ02leNh7uKFRQM3DsvCx6irO46XQp-Iz15EvbsOFgpSm3ZEjY-sGslSXlyPOWu8A/s1600/1.png" /></a></div>
<br />
Очень информативное сообщение, сразу понятна причина ошибки, место и способ ее решения :)<br />
Впрочем, если без шуток, что это вообще такое?<br />
Конечно-же это исключение, но ни тип исключения, ни его описание нам не доступны - просто "Runtime error 217" и адрес, а дальше сами...<br />
<br />
Если честно, раньше я как-то даже не задумывался по поводу данного исключения, т.к. в моих проектах оно явление редкое, пока однажды у целой череды пользователей не начала воспроизводится именно 217-я ошибка.<br />
Впрочем, даже тогда я не пошел по правильному пути и просто добавил дополнительный уровень логирования в проект, по результатам которого достаточно оперативно нашел причину и исправил ее.<br />
Но, по сути, я просто потратил свое время...<br />
<br />
И тратил бы его в дальнейшем, если бы на днях со мной не связался <a href="http://victor-vik.blogspot.ru/" rel="nofollow" target="_blank">Виктор Федоренков</a> и не рассказал о своих мыслях по поводу ошибки за номером 217.<br />
<br />
<br />
<a name='more'></a><br />
<h3 style="text-align: left;">
Теория и анализ проблемы</h3>
<br />
Без теории нам никуда, иначе можем уткнуться в пределы собственных знаний :)<br />
Поэтому начнем, конечно, с теоретической части.<br />
<br />
Для начала я немного освежил мои представления об ошибках в принципе, перечитав часть статьи "<a href="http://www.delphikingdom.ru/asp/viewitem.asp?catalogid=1392#SubSubHeader_1_2_2" target="_blank">Обработка ошибок - глава 1.2.2</a>" за авторством Александра Алексеева, откуда вынес информацию о том, что ошибка 217 будет отображена в том случае, если не инициализирован модуль SysUtils, причем это у Александра проиллюстрированно достаточно наглядно:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgD49Nd5VxJkFb1E12h0yuGq70X4G4vOlt8ovM4s4EIGE6_CVt2i0GxoRNCHKH7z60c_-gslCmQPzAzbeudRHsX0IB9tS_NJ1WKNlrKxx8UnzfeKs2a35Viw4w4Mic7kCYEBKc7e0nM0xM/s1600/AAErrors12.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgD49Nd5VxJkFb1E12h0yuGq70X4G4vOlt8ovM4s4EIGE6_CVt2i0GxoRNCHKH7z60c_-gslCmQPzAzbeudRHsX0IB9tS_NJ1WKNlrKxx8UnzfeKs2a35Viw4w4Mic7kCYEBKc7e0nM0xM/s1600/AAErrors12.png" height="640" width="451" /></a></div>
<div style="text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgD49Nd5VxJkFb1E12h0yuGq70X4G4vOlt8ovM4s4EIGE6_CVt2i0GxoRNCHKH7z60c_-gslCmQPzAzbeudRHsX0IB9tS_NJ1WKNlrKxx8UnzfeKs2a35Viw4w4Mic7kCYEBKc7e0nM0xM/s1600/AAErrors12.png" target="_Blank">Открыть картинку в полный размер...</a> </div>
<br />
На основании данной картинки можно сделать грубый вывод: пока SysUtils жив - все исключения должны отображаться в нормальном виде, о чем идет отдельное упоминание:<br />
<br />
<blockquote class="tr_bq">
<span style="background-color: white; font-family: Arial; font-size: 13px; text-align: justify; text-indent: 19.200000762939453px;">Например, если вы видите сообщение о runtime-ошибке, то, судя по приведённой схеме, маловероятно, чтобы ошибка возникла в обработчиках событий на форме. Зато гораздо вероятнее, что она возникает, скажем, в какой-то секции finalization (которая выполняется после секции finalization модуля SysUtils) или в назначенной процедуре ExitProcessProc. Но, разумеется, </span><em style="background-color: white; font-family: Arial; font-size: 13px; text-align: justify; text-indent: 19.200000762939453px;">причина</em><span style="background-color: white; font-family: Arial; font-size: 13px; text-align: justify; text-indent: 19.200000762939453px;"> ошибки может сидеть где угодно — в том числе и в упоминаемых обработчиках событий.</span></blockquote>
<br />
Ну что-ж давайте проверим, пишем код, в котором SysUtils должна быть финализирована позже модуля Unit1, в котором искусственно генерируем исключение:<br />
<br />
<pre class="brush:delphi">unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs;
type
TForm1 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
initialization
finalization
raise Exception.Create('finalization exception');
end.
</pre>
<br />
Билдим, запускаем, закрываем форму и... Runtime error 217.<br />
<br />
Утверждение о том, что 217 отображается после финализации SysUtils полностью верное, но давайте-ка посмотрим на сам код финализации:<br />
<br />
<pre class="brush:delphi">procedure FinalizeUnits;
...
begin
...
Count := InitContext.InitCount;
Table := InitContext.InitTable^.UnitInfo;
...
try
while Count > 0 do
begin
Dec(Count);
InitContext.InitCount := Count;
P := Table^[Count].FInit;
if Assigned(P) then
...
TProc(P)();
...
end;
end;
except
FinalizeUnits; { try to finalize the others }
raise;
end;
end;
</pre>
<br />
Смотрите что происходит: в процедуре FinalizeUnits вызываются все финализирующие процедуры, адреса которых расположены в массиве InitContext.InitTable^.UnitInfo в том порядке, в котором происходила их инициализация, т.е. самые первые расположены в начале массива (а финализация идет с конца).<br />
Где-то в самом низу расположен и SysUtils + System, ну а мы, с нашим модулем Unit1 где-то в самом верху.<br />
Но вдруг происходит исключение в нашем модуле и "бабах", порядок катарсиса нарушен.<br />
<br />
После "бабах" FinalizeUnits вызывается повторно, пропуская наш модуль, вызвавший исключение, вследствие чего разрушается SysUtils и разные, встречающиеся по пути, class destructor-ы, до кучи грохается System с менеджером памяти (сидящий одним из первых в начале списка), после чего идет контрольный выстрел в лоб - RAISE, вот тут-то мы и приплыли - здравствуй 217.<br />
<br />
А что если произойдет исключение в секции инициализации любого модуля?<br />
<br />
Да все тоже самое:<br />
<br />
<pre class="brush:delphi">procedure InitUnits;
...
begin
...
try
...
except
FinalizeUnits;
raise;
end;
end;
</pre>
<br />
Делаем вывод: любое <b>необработанное</b> исключение в секциях инициализации или финализации будет приводить к потере описания исключения и приводить к ошибке 217.<br />
<br />
На этом с теорией, думаю, закончим.<br />
Имея на руках понимание о причине возникновения Runtime error 217, попробуем получить на руки более привычный нам вариант сообщения об исключении.<br />
<br />
<h3 style="text-align: left;">
Отключаем финализацию модулей</h3>
<br />
В самом начале обсуждения <a href="http://victor-vik.blogspot.ru/" rel="nofollow" target="_blank">Виктором</a> был предложен достаточно эффективный способ обхода данной ошибки.<br />
<br />
Его анализ заключался в следующем: общая инициализация обработчика исключений производится в процедуре InitExceptions модуля SysUtils, а финализация вызовом DoneExceptions.<br />
<br />
Если каким либо образом отключить вызов DoneExceptions плюс не дать разрушиться менеджеру памяти, заблокировав вызов блока финализации System - на руки мы получим сообщение об исключении в приемлимом виде.<br />
<br />
Как вариант решения был предложен следующий код, который нужно подключить к файлу проекта самым первым модулем (будет работать начиная с D2005 и выше):<br />
<br />
<pre class="brush:delphi">unit suShowExceptionsInInitializeSections;
interface
uses
SysUtils;
implementation
uses
Windows;
//Получение структуры PackageInfo нашего приложения
//В System она находится в переменной InitTable, но не видна из других модулей
function GetInitTable: PackageInfo;
var
Lib: PLibModule;
TypeInfo: PPackageTypeInfo;
begin
Result := nil;
Lib := LibModuleList;
if not Assigned(Lib) then
Exit;
//Если загружено несколько модулей (BPL пакетов), то выходим,
//я не изучал как работает механизм загрузки/выгрузки BPL, поэтому на всякий
//случай выходим
if Assigned(Lib^.Next) then
Exit;
Typeinfo := Lib^.TypeInfo;
if Assigned(TypeInfo) then
begin
//Мы имеем TPackageTypeInfo
//Теперь по нему можно получить PackageInfo
//Воспользуемся особенностями компилятора.
//В IDA видно, что ссылка TypeInfo указывает на середину структуры
//PackageInfo программы
//Поэтому для того что бы вычислить PackageInfo нужно вычесть из адреса
//TypeInfo смещение этого поля
Result := PackageInfo(PByte(TypeInfo) - (LongWord(@PackageInfoTable(nil^).TypeInfo)));
end;
end;
//Отключить секцию финализации для всех модулей
procedure DisableAllFinalization;
var
Loop: Integer;
OldProtect: LongWord;
InitTable: PackageInfo;
Table: PUnitEntryTable;
begin
InitTable := GetInitTable;
if Assigned(InitTable) then
begin
Table := InitTable^.UnitInfo;
if Assigned(Table) then
//Разрешаем изменять структуру в которой хранятся ссылки на инициализаю/финализацию всех юнитов
if VirtualProtect(Table, SizeOf(PackageUnitEntry) * InitTable^.UnitCount,
PAGE_READWRITE, OldProtect) then
for Loop := 0 to InitTable^.UnitCount - 1 do
Table^[Loop].FInit := nil;
end;
end;
initialization
finalization
//Сейчас идет финализация всех модулей, модуль SysUtils создан раньше, поэтому
//он еще не финализирован. Наша задача здесь не дать ему финализироваться,
//Как и другим модулям которые он использует (интересует только System),
//это нужно для правильной отработки обработчиков исключений.
//Сюда мы можем попасть по двум причинам
//1. Произошел Exception во время инициализации каком-то модуля
//2. Нормальное завершение программы
//
//Мы не будем определять причину, так как процесс все равно завершается, а ОС
//сама освободит занятые ресурсы после смерти процесса.
//Но нужно иметь ввиду, данную технику использовать в DLL нельзя, что бы не
//допускать утечек памяти
if IsLibrary then
Exit;
//Мы не можем выборочно заблокировать финализацию юнитов по их имени
//так как нет соответствующих данных в RTTI. Тем не менее, мы можем отключить
//финализацию всех юнитов, которые идут в списке до этого
//модуля. Таким образом если данный модуль расположить первым в DPR файле,
//то мы минимизируем утечки.
//Вычислять адрес процедуры финализации данного юнита не обязательно,
//ведь к моменту выполнения данного кода уже финализированы все следующие юниты.
//Поэтому просто заблокируем финализцию всех оставшихся
DisableAllFinalization;
end.
</pre>
<br />
Если честно - аплодировал стоя.<br />
Вот он: <b>хак в самом грязном виде как он есть</b> - такие вещи могут делать только те, кто действительно понимает, чем это грозит :)<br />
И данный модуль вывел работу нашего IT отдела примерно на три часа - это была жесткая дискуссия :)<br />
<br />
Но, впрочем, давайте разберем логику работы данного кода:<br />
Суть его проста, необходимо выйти на данные о загруженных модулях (включая BPL) в том виде, в котором их понимает Delphi приложение. Это было сделано посредством доступа к началу однонаправленного списка структур TLibModule. Первым элементом списка будет структура, описывающая текущий образ, откуда нам нужно всего-то и получить данные о структуре UnitInfo, которая содержит в себе данные как о количестве инициализированных модулей, так и об адресах их процедур инициализации и финализации в виде записи PackageUnitEntry.<br />
<br />
Блокирование финализации модулей происходит посредством присвоения параметру FInit значения <b>nil</b> у каждой записи PackageUnitEntry.<br />
<br />
При обниливании данного параметра FinalizeUnits не сможет произвести вызов обработчика и в итоге тот самый <b>raise</b>, о котором я писал выше, сможет достаточно корректно произвести отображение возникшего исключения.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgfoKINx5kCAeneZoPjl8C6DTKlSBCYJrE9Tgv8TjKR39xR4MG2etvBFBsIVAaVS0Bo4jIUNDxdGBFpT1ZXwKQ60iWsLWESEVORTjOvYqiWEJz364OSJsR94s2u5HUBQ8s4KXwlLt0IVjs/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgfoKINx5kCAeneZoPjl8C6DTKlSBCYJrE9Tgv8TjKR39xR4MG2etvBFBsIVAaVS0Bo4jIUNDxdGBFpT1ZXwKQ60iWsLWESEVORTjOvYqiWEJz364OSJsR94s2u5HUBQ8s4KXwlLt0IVjs/s1600/2.png" /></a></div>
<br />
Но вот дальше все сложнее.<br />
<br />
<h3 style="text-align: left;">
Пытаемся причесать хорошую мысль</h3>
<br />
Идея здравая и причины понятны, но вот как-же так, ресурсы все-же не освобождены, FastMem перестанет нормально работать (она собирает утечки как раз при финализации), да и совместимости маловато, к примеру, как я и сказал выше, под Delphi 7 данный код вообще работать не сможет.<br />
<br />
После первого часа обсуждений в IT отделе мы даже умудрились прийти и к такому выводу: "да и хрен с ними с SysUtils и System - что-то критичного они за собой не несут".<br />
А потом, опять начали спорить - ну не устраивал нас этот подход, вроде все хорошо, но не аккуратненько как-то :)<br />
<br />
Рассматривались даже варианты прямого сплайсинга блоков финализации и до кучи деструктора Exception - но дополнительный хак, на уже существующий хак не устраивал вообще никого :)<br />
<br />
И тут, сидя в отладчике и прогоняя код по 70-му разу пришла мысля.<br />
Дык эта... а как вообще выводится сообщение о произошедшем исключении? :)<br />
<br />
А выводится оно посредством передачи управления на ExceptHandler, в коде которого нет ничего секретного.<br />
А что мы делаем убирая финализацию модулей?<br />
Правильно, заставляем вызваться его-же.<br />
<br />
Попробуем-ка проэмулировать вызов ExceptHandler.<br />
Пишем тестовый юнит и подключаем его к проекту самым первым:<br />
<br />
<pre class="brush:delphi">unit Test;
interface
uses
SysUtils;
var
E: Exception;
implementation
initialization
finalization
E := AcquireExceptionObject;
if E <> nil then
begin
ShowException(E, ExceptAddr);
E.Free;
Halt(1);
end;
end.
</pre>
<br />
Запускаем на выполнение и...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnU-Sgh2Xm86Cs8qiCpG-IRmh_RyzBbzKLRLZd8lZRhuZxB6eTp_-XsA_Wr4tDc4H6WEzClXHZ8DkMKm5bR0B3Mt6xWwXAgJgDmW9ho2Htli1HwG44oXaVrRSYz9BE_Jjvu8JB8rQMguY/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnU-Sgh2Xm86Cs8qiCpG-IRmh_RyzBbzKLRLZd8lZRhuZxB6eTp_-XsA_Wr4tDc4H6WEzClXHZ8DkMKm5bR0B3Mt6xWwXAgJgDmW9ho2Htli1HwG44oXaVrRSYz9BE_Jjvu8JB8rQMguY/s1600/3.png" /></a></div>
<br />
<br />
Получилось.<br />
<br />
Встроившись в цикл финализации, мы отобразили произошедшее исключение и продолжили финализацию дальше вызовом Halt(1).<br />
<br />
В итоге задача решена, грамотно и документировано, и совместимо с Delphi 7, но...<br />
<br />
<h3 style="text-align: left;">
А не развить ли идею?</h3>
<br />
Есть такое понятие, как "наведенные ошибки", т.е. ошибки произошедшие из-за того что перед ними тоже произошла ошибка.<br />
<br />
Ну к примеру, функция А, которая должна возвращать экземпляр некоего класса и функция Б, использующая этот экземпляр в работе. К примеру в функции А произошло необработанное исключение (например нет доступа к файлу) и она не создала класс, а потом где-то гораздо позже по коду приложения процедура Б выполняет обращение к этому экземпляру и в итоге происходит Access Violation.<br />
<br />
Тоже самое может произойти и в процедурах инициализации/финализации, причем исключение, произошедшее в финализации скроет от нас саму причину.<br />
<br />
Для демонстрации напишем вот такой код, в котором при инициализации приложения будет создаваться некий логер, в который будут писаться этапы работы приложения, а в финализации будет писаться что приложение завершило работу.<br />
Для генерации исключения заставим логер создаваться по несуществующему пути:<br />
<br />
<pre class="brush:delphi">uses
Classes;
var
Logger: TFileStream;
const
StartLog: AnsiString = 'Начало работы приложения' + sLineBreak;
EndLog: AnsiString = 'Работа приложения завершена' + sLineBreak;
implementation
initialization
Logger := TFileStream.Create('A:\MyLog,txt', fmCreate);
Logger.WriteBuffer(StartLog[1], Length(StartLog));
finalization
Logger.WriteBuffer(EndLog[1], Length(EndLog));
Logger.Free;
end.
</pre>
<br />
Мало у кого в системе присутствует диск "А" поэтому результатом этого кода будет либо "Runtime error 216" (именно 216, а не 217), либо, если подключим код из предыдущей главы:<br />
<blockquote class="tr_bq">
Exception EAccessViolation in module Project2.exe at 001B1593.<br />
Access violation at address 005B1593 in module 'Project2.exe'. Read of address 00000000.</blockquote>
А ведь причина то кроется в самом первом исключении, которое нами не отображается и с наскока разобраться в причине ошибки не получится.<br />
<br />
Для того чтобы исправить эту несправедливость, можно немного причесать код и довести его до вот такого состояния:<br />
<br />
<pre class="brush:delphi">unit ShowExceptSample;
interface
uses
SysUtils,
Classes;
implementation
type
PRaiseFrame = ^TRaiseFrame;
TRaiseFrame = packed record
NextRaise: PRaiseFrame;
ExceptAddr: Pointer;
ExceptObject: TObject;
ExceptionRecord: PExceptionRecord;
end;
var
// Указатель на вершину списка исключений
CurrentRaiseList: Pointer = nil;
// Функция возвращяет текущее исключение со стека
function GetNextException: Pointer;
begin
if CurrentRaiseList = nil then CurrentRaiseList := RaiseList;
if CurrentRaiseList <> nil then
begin
Result := PRaiseFrame(CurrentRaiseList)^.ExceptObject;
PRaiseFrame(CurrentRaiseList)^.ExceptObject := nil;
CurrentRaiseList := PRaiseFrame(CurrentRaiseList)^.NextRaise;
end
else
Result := nil;
end;
var
ExceptionStack: TList;
E: Exception;
initialization
finalization
// Смотрим, есть ли вообще исключения?
E := GetNextException;
if E <> nil then
begin
ExceptionStack := TList.Create;
try
// если есть, собираем о них информацию
while E <> nil do
begin
ExceptionStack.Add(E);
E := GetNextException;
end;
// и отображаем их в том порядке, в котором они произошли
while ExceptionStack.Count > 0 do
begin
E := ExceptionStack[ExceptionStack.Count - 1];
ExceptionStack.Delete(ExceptionStack.Count - 1);
ShowException(E, ExceptAddr);
E.Free;
end;
finally
ExceptionStack.Free;
end;
// финализируем все что осталось
Halt(1);
end;
end.
</pre>
<br />
Здесь идея проста, функция GetNextException по сути повторяет вызов AcquireExceptionObject, но после своего вызова не теряет ссылку на следующее в очереди исключение, а запоминает адрес следующего фрейма во внешней переменной.<br />
После чего все исключения заносятся в список (самое последнее будет первым в списке) и выводятся программисту с соблюдением очередности, в результате чего нам будет сразу понятно, что сначала произошло вот это:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEp3ZpyTFSBbFr85xmo6G-wUjyM9rnvg9T3QZwbH-nfIa_n_bPKlO07NWeu4WPhhQgUPpses_w5qwjo0pkwq3la8YA-COSNS5ZlZ_-VoT0_8fQB7x1G-48K6QYHEwBFtux75MEsX7Gltk/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEp3ZpyTFSBbFr85xmo6G-wUjyM9rnvg9T3QZwbH-nfIa_n_bPKlO07NWeu4WPhhQgUPpses_w5qwjo0pkwq3la8YA-COSNS5ZlZ_-VoT0_8fQB7x1G-48K6QYHEwBFtux75MEsX7Gltk/s1600/4.png" /></a></div>
<br />
И уже только после него пошли всякие там AV.<br />
<br />
Теперь по поводу остальных кодов ошибок.<br />
Почему я начал именно с "Runtime error 217"?<br />
Ну потому что она наиболее легко воспроизводима, а так технически, используя выше приведенный модуль, мы получим на руки вполне нормальное описание всех возможных Runtime ошибок, коих в наличии у нас вон сколько:<br />
<br />
<pre class="brush:delphi"> reMap: array [TRunTimeError] of Byte = (
0, { reNone }
203, { reOutOfMemory }
204, { reInvalidPtr }
200, { reDivByZero }
201, { reRangeError }
{ 210 Abstract error }
215, { reIntOverflow }
207, { reInvalidOp }
200, { reZeroDivide }
205, { reOverflow }
206, { reUnderflow }
219, { reInvalidCast }
216, { reAccessViolation }
218, { rePrivInstruction }
217, { reControlBreak }
202, { reStackOverflow }
220, { reVarTypeCast }
221, { reVarInvalidOp }
222, { reVarDispatch }
223, { reVarArrayCreate }
224, { reVarNotArray }
225, { reVarArrayBounds }
{ 226 Thread init failure }
227, { reAssertionFailed }
0, { reExternalException not used here; in SysUtils }
228, { reIntfCastError }
229, { reSafeCallError }
235, { reMonitorNotLocked }
236 { reNoMonitorSupport }
{$IFDEF PC_MAPPED_EXCEPTIONS}
{ 230 Reserved by the compiler for unhandled exceptions }
{$ENDIF PC_MAPPED_EXCEPTIONS}
{$IF defined(PC_MAPPED_EXCEPTIONS) or defined(STACK_BASED_EXCEPTIONS)}
{ 231 Too many nested exceptions }
{$ENDIF}
{$IF Defined(LINUX) or Defined(MACOS)}
{ 232 Fatal signal raised on a non-Delphi thread }
,
233 { reQuit }
{$ENDIF LINUX or MACOS}
{$IFDEF POSIX}
,
234 { reCodesetConversion }
{$ENDIF POSIX}
,
237, { rePlatformNotImplemented }
238 { reObjectDisposed }
);
</pre>
<br />
<h3 style="text-align: left;">
Итог</h3>
<br />
<span style="background-color: white; font-family: Verdana, sans-serif; font-size: 13px; line-height: 20.799999237060547px;">Вот таким небрежным кодом, мы можем получить то, о чем нам не хочет говорить ошибка под кодом 217.</span><br />
<br style="background-color: white; font-family: Verdana, sans-serif; font-size: 13px; line-height: 20.799999237060547px;" />
<span style="background-color: white; font-family: Verdana, sans-serif; font-size: 13px; line-height: 20.799999237060547px;">Впрочем, я не думаю что этот подход будет незнаком опытным программистам.</span><br />
<span style="background-color: white; font-family: Verdana, sans-serif; font-size: 13px; line-height: 20.799999237060547px;">Скорее всего это — здравствуй велосипед, ибо вероятнее всего данная проблема кем-то уже решалась ранее, но я просто не знал о данном решении :)</span><br />
<br style="background-color: white; font-family: Verdana, sans-serif; font-size: 13px; line-height: 20.799999237060547px;" />
<span style="background-color: white; font-family: Verdana, sans-serif; font-size: 13px; line-height: 20.799999237060547px;">А если нет — значит буду вторым :)</span><br />
<br />
Отдельный респект соавтору и вдохновителю данной статьи - <a href="http://alexander-bagel.blogspot.ru/2014/01/217.html" rel="nofollow" target="_blank">Виктору Федоренкову</a>.<br />
<br />
Удачи.<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
© Виктор Федоренков</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
© Александр (Rouse_) Багель</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Январь, 2014</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com6tag:blogger.com,1999:blog-2374465879949372415.post-18715825929747807202013-12-23T22:31:00.000+04:002013-12-26T10:50:38.996+04:00Ответ на задачку №2, часть первая<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" trbidi="on">
Попробуем разобраться.<br />
<br />
Изначально в <a href="http://alexander-bagel.blogspot.ru/2013/12/2.html" target="_blank">условии задачи уточнялось</a> - OnPaint не используем.<br />
Правильно это или нет - за условиями задачи, цель была прояснить, понимает ли собеседник поведение VCL в данном случае или нет.<br />
Для упорствующих, конечно, готов очередной подводный камень, к которому мы придем в процессе объяснения, но впрочем по порядку.<br />
<br />
Что было предложено в процессе обсуждения вопроса:<br />
1. Зависимость от версий Delphi<br />
2. Выравнивание текста или его многострочность.<br />
3. Смена/пересоздание DC при вызове Memo.Lines[0]<br />
4. Нарушение очереди сообщений из-за вызова обработчика OnClick и как следствие прочие неприятности...<br />
5. Неправильное место вызова, DC "не готов" для вывода (про готовность я не понял, честно, ибо если канва не готова - будет ошибка, все прочее для эстетов :)<br />
<br />
Начнем с первого пункта.<br />
<br />
<a name='more'></a><br />
Именно в изначальной постановке задачи, зависимости как таковой нет.<br />
В неправильных вариантах кода текст будет:<br />
<ol>
<li>либо не выведен;</li>
<li>либо будет выведен с искажениями, как на картинке ниже;</li>
</ol>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgAsKMnIRQCL4Ap12FM5V1ZZyN6L9yp851actHyysV3b2Vhkr8SGZzPGverTeZQvgQKuUT4LBU4bIXBlUnSKTNdeeNlXbkpR7D2OqasqF2A8z3LxX3rLiMypKHklw460ueUD8gi4JfNMQ8/s1600/Untitled.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgAsKMnIRQCL4Ap12FM5V1ZZyN6L9yp851actHyysV3b2Vhkr8SGZzPGverTeZQvgQKuUT4LBU4bIXBlUnSKTNdeeNlXbkpR7D2OqasqF2A8z3LxX3rLiMypKHklw460ueUD8gi4JfNMQ8/s1600/Untitled.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
Но.. по всей видимости есть некоторые изменения, ибо вывод текста с уплывшим Brush/Pen/Font гарантированно можно было воспроизвести под Delphi 2007 (и даже под 2010 - если мне не отказывает память) в случае использования самого первого варианта кода под ХР и ниже:<br />
<br />
<pre class="brush:delphi">var
ACanvas: THandle;
AText: string;
begin
ACanvas := Canvas.Handle;
AText := Memo1.Lines[0];
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
</pre>
<br />
Код под ХЕ4 такое поведение сегодня мне не показал, он просто не вывел текст. :)<br />
<br />
На этом я заострю ваше внимание чуть ниже, а пока что второй пункт...<br />
<br />
<b>Выравнивание текста или многострочность.</b><br />
<br />
Здесь по, всей видимости, рассматривалось то, что TextOut, в отличие от DrawText не может работать с (скажем так) навороченной строкой, ну, к примеру, выравнивание по правому краю (из-за форматирования) или то, что первая строка в Memo может представлять из себя просто sLineBreak.<br />
Это все мимо, хотя уже даже за такое я бы лично выставил ребятам пиво - люблю тех, кто мыслит не стандартно (без шуток) :)<br />
<br />
<b>Поэтому перейдем к сути (пункт три):</b><br />
<br />
Взяв в руки отладчик, мы сможем увидеть, что только один вариант кода (вышеозвученный из четырех) приводит к некорректному поведению.<br />
Происходит это из-за того, что ранее полученный нами DC, не то чтобы пересоздается - он просто разрушается, и при вызове TextOut на разрушенный DC, конечно может произойти все что угодно (вплоть до некорректного вывода текста).<br />
<br />
Происходит это по причине того, что вызов Memo1.Lines[0] осуществляется с участием вызова SendMessage, который приведет нас вот к этому участку кода:<br />
<br />
<pre class="brush:delphi">procedure TWinControl.MainWndProc(var Message: TMessage);
begin
try
try
WindowProc(Message);
finally
FreeDeviceContexts;
FreeMemoryContexts;
end;
except
Application.HandleException(Self);
end;
end;
</pre>
<br />
Обратите внимание на вызов FreeDeviceContexts - именно здесь и происходит разрушение ранее полученного DC.<br />
<br />
Таким образом, понимая что любой вызов SendMessage приводит к освобождению DC, мы можем реализовать вот такой код:<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button2Click(Sender: TObject);
var
ACanvas: THandle;
AText: string;
begin
AText := 'qweqweqwe';
ACanvas := Canvas.Handle;
SendMessage(Handle, WM_NULL, 0, 0); // << после данного вызова канвас не валиден
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
</pre>
<br />
А что с вызовом "Memo1.Text"?<br />
А здесь все очень просто, вызов Perform внутри данного метода обрабатывается самой VCL, поэтому и нет освобождения DC.<br />
<br />
<b>Впрочем... - пункт четыре.</b><br />
Я думаю что его стоит пропустить, ибо исходя из объяснений по пункту три и так все понятно - очередь событий, конечно, может повлиять на исполнение кода, но это нужно достаточно хитро все написать :)<br />
<br />
<b>Эпилог:</b><br />
Что я хотел услышать от человека, <a href="http://alexander-bagel.blogspot.ru/2013/12/2.html" target="_blank">который проходит данный тест</a> - это следующее:<br />
Первый вариант кода не верен, по причине работы с некорректным DC, после его освобождения в обработчике MainWndProc.<br />
По сути - это все :)<br />
<br />
А теперь перейдем к ответу на пять с плюсом и рассмотрим пятый пункт :)<br />
<br />
<b>Пункт пять - DC не готов, нужно рисовать в OnPaint.</b><br />
<br />
Хорошо, допустим он не готов, хотя такое понятие по сути никак не может быть использовано в VCL коде, оно присуще состоянию флагов непосредственно GDI хэндла, с которыми VCL не работает.<br />
<br />
Допустим и напишем вот такой код:<br />
<br />
<pre class="brush:delphi">procedure TForm1.FormPaint(Sender: TObject);
var
ACanvas: THandle;
AText: string;
begin
ACanvas := Canvas.Handle;
AText := Memo1.Lines[0];
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
</pre>
<br />
Код отработает правильно и нарисует содержимое первой строки TMemo.<br />
Возникает вопрос, почему вызов "Memo1.Lines[0]" не привел к пересозданию DC и текст вывелся правильно?<br />
Здесь все просто, суть кроется в той же процедуре FreeDeviceContexts.<br />
В обработчике OnPaint канвас создан, валиден и залочен, таким образом переменная ACanvas, даже после вызова "Memo1.Lines[0]" будет содержать правильный DC.<br />
<br />
Так работает VCL, но мы попробуем ее обойти и все-же вывести текст в обход залоченого канваса.<br />
<br />
Сделаем это следующим кодом:<br />
<br />
<pre class="brush:delphi">type
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure FormPaint(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
ACanvas: THandle;
AText: string;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
ACanvas := Canvas.Handle;
AText := Memo1.Lines[0];
Repaint;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin
if ACanvas <> 0 then
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
</pre>
<br />
Итак по порядку, как мы уже рассмотрели:<br />
1. В обработчике кнопки мы получаем валидный DC формы<br />
2. Вызовом "Memo1.Lines[0]" мы его разрушили<br />
3. Через Repaint вызвали отрисовку формы где..<br />
4. Через уже невалидный DC вывели текст на форму.<br />
<br />
Работает?<br />
Да еще как, но ведь не должен.<br />
А где ж засада, ведь по логике мы хэндл канваса уже разрушили (что и было ранее объяснено), причем под обработчик OnPaint внутри VCL было создано новое DC и залочено, но почему же текст выводится через старое DC?<br />
<br />
А давай-те ка поизучаем.<br />
<br />
Пишем:<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button3Click(Sender: TObject);
var
FakeDC: HDC;
AText: string;
begin
AText := 'Некий текст';
// Создаем DC
FakeDC := GetDC(Handle);
// Сразу его релизим
ReleaseDC(Handle, FakeDC);
// Проверочка
if WindowFromDC(FakeDC) <> 0 then
ShowMessage('FakeDC не разрушен');
TextOut(FakeDC, Button1.Left, 20, PChar(AText), Length(AText));
end;
</pre>
</div>
<br />
Здесь, выкинув лишнее и взяв суть из VCL кода демонстрируется что происходит с DC при выполнении всех вышеописанных строчек кода. Вдогонку мы проверяем - действительно ли DC не валиден вызовом WindowFromDC.<br />
<br />
А теперь.... Магия :))<br />
<br />
Пишем:<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button3Click(Sender: TObject);
var
FakeDC: HDC;
AText: string;
PS: TPaintStruct;
begin
AText := 'Некий текст';
// Создаем DC
FakeDC := GetDC(Handle);
// Сразу его релизим
ReleaseDC(Handle, FakeDC);
// Проверочка
if WindowFromDC(FakeDC) <> 0 then
ShowMessage('FakeDC не разрушен');
// Оживляем разрушенный DC
InvalidateRect(Handle, 0, True);
BeginPaint(Handle, PS);
// Проверка, привязан ли разрушенный DC к нашей форме?
if WindowFromDC(FakeDC) = Handle then
// оппа - привязан, а давай-ка выведем на нем текст...
TextOut(FakeDC, Button1.Left, 20, PChar(AText), Length(AText));
EndPaint(Handle, PS);
end;
</pre>
<br />
А вот это уже крайне интересно, работая с заведомо разрушенным DC, мы все же имеем возможность вывода текста на форму, именно поэтому и работает вариант с отложенной отрисовкой через OnPaint.<br />
<br />
Впрочем... здесь я пока что и остановлюсь.<br />
<br />
Это была только часть ответа на задачку, во второй части я объясню как такое может происходить, но...<br />
<br />
Но не раньше, чем будут предложены варианты, объясняющие такое поведение, или хотя бы предположения о таком поведении :)<br />
Думаю после НГ - всех с наступающим, кстати :)<br />
<br />
<b>UPD:</b><br />
Здесь я постараюсь дать развернутый ответ на вопросы - зачем давать такие примеры кода на собеседовании? :)<br />
<br />
Итак, начнем с того, что IT отдел у нас достаточно маленький (8 человек) а объем работы категорически большой/<br />
Поэтому при отборе сотрудников мы пользуемся старым шаблоном - нам не нужны ваши дипломы/пол/возраст/вес/социальный статус/связи и прочее, нам нужны только ваши знания и мы за них готовы платить ну очень хорошие деньги, даже для Москвы :)<br />
Но знания должны быть хорошие, ибо как правило мы берем разработчика один раз и навсегда, попрыгунчики нам не нужны, поэтому только квалификация и еще раз квалификация.<br />
Текучка очень маленькая - на коммерческом коде ушел всего один человек за последние 10 лет и то по семейным обстоятельствам, о чем кстати до сих пор жалеем, уж очень был хорош.<br />
<br />
Собеседование проходит в три этапа - сначала через онлайн с соискателем беседуют программисты на предмет - он вообще к нам подойдет? Этот этап простой, без вот таких вот мудрых вопросов (хотя, просто для понимания, в некоторых случаях могут задаваться и они). На этом этапе и происходит отсев соискателей, очень сложно найти человека той квалификации, которая требуется.<br />
<br />
Второй этап: IT отдел выдал вердикт - нам он нравится, после которого идет небольшое согласование у начальства и соискатель вызывается на допрос к техническому директору.<br />
Если он его проходит (а как правило проходит, ибо мы стараемся чтобы технический не тратил свое время на совсем уж студентов), то он приглашается на беседу с Генеральным, после чего происходит принятие на работу.<br />
<br />
Все - <b>после этого человек официально трудоустроен</b>, заключен договор и прочее.<br />
<br />
И вот тут уже доходит очередь и до меня, где я прогоняю человека через несколько тестов (их сейчас 19 - если не ошибаюсь) на основании которых я понимаю в каких областях человек наиболее силен и где его применение будет наиболее выгодно для компании.<br />
Грубо резюмы выглядят примерно так:<br />
отличные знания VCL с углубленкой - рекомендую двинуть на него объем работ по разработке компонент. Или - практическое абсолютное понимание работу служб и сетевого транспорта, предлагаю двинуть его на наши сервера. Очень редко бывает и такое - отличное знание ассемблера, системных документированных и не очень структур, его я забираю себе - он будет помогать мне писать ядро защиты.<br />
<br />
Конечно, понятное дело, что периодически программисты подменяют друг друга, но как правило все работают по своим профилям, к примеру я никогда не работаю с базами. Я конечно могу это сделать, но мой коллега сделает это гораздо быстрее и качественней чем я.</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com63tag:blogger.com,1999:blog-2374465879949372415.post-68117638805243341072013-12-20T22:51:00.000+04:002013-12-23T10:51:37.385+04:00Задачка на понимание №2<div dir="ltr" style="text-align: left;" trbidi="on">
Это уже достаточно старая задача, лет семь (если не отказала память) живет в моих тестах на профпригодность, выдаваемых кандидатам при собеседовании.<br />
В отличии от прошлой задачи, здесь не требуется знаний о памяти процесса включающую работу стека и прочее, в ней тестировалось знание VCL, как она реагирует при взаимодействии с прямыми API вызовами, где сидит засада в общем виде и углубленка по GDI.<br />
<br />
Итак, на скорую руку ваяем приложение в котором есть форма, кнопка и TMemo.<br />
Дано 4 варианта реализации кода обработчика кнопки:<br />
<br />
<pre class="brush:delphi">var
ACanvas: THandle;
AText: string;
begin
ACanvas := Canvas.Handle;
AText := Memo1.Lines[0];
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
...
begin
ACanvas := Canvas.Handle;
AText := Memo1.Text;
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
...
begin
AText := Memo1.Lines[0];
ACanvas := Canvas.Handle;
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
...
begin
AText := Memo1.Text;
ACanvas := Canvas.Handle;
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
</pre>
<br />
Суть данного кода проста - берем текст, который содержится в Memo и тупо выводим его на канву формы.<br />
Вариант того, что отрисовка должна быть в OnPaint, не рассматриваем, он тут лишний.<br />
<br />
В результате на форме будет либо выведен текст, либо он будет не выведен, либо уплывет бэкграунд (ХР и ниже), но текст все равно отобразится.<br />
<br />
Задача выглядит следующим образом:<br />
<br />
<a name='more'></a><br /><br />
1. Определить какой из вариантов кода правилен.<br />
2. Объяснить причину такого поведения.<br />
3. Если выполнили первые два пункта - объяснить причину отрисовки в неправильных вариантах (ХР и ниже).<br />
<br />
Дам пояснения:<br />
Тот кто даст ответ на первый вопрос - хорошист. Время решения - 10 минут (без использования компьютера).<br />
Тот кто разъяснит второй вопрос - отличник, как минимум. Время решения - час. (Отладчик в руки и вперед - пока не найдете).<br />
<br />
А вот с третьим вопросом сложнее, такое поведение можно воспроизвести только под ХР и ниже, да и до кучи ответ на данный вопрос, как минимум требует очень хорошего знания книжек Фень Юаня, откуда можно вынести понимание данного поведения (ну этот ответ конечно на 5 с плюсом, но таких подробностей на собеседованиях я никогда не требовал, ибо дай то бог решить второй пункт :)<br />
<br />
Удачи :)<br />
<br />
UPD:<br />
Для тех кто все-же против того что рисуется не в OnPaint, вот такой вариант кода:<br />
<br />
<pre class="brush:delphi">type
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure FormPaint(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
ACanvas: THandle;
AText: string;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
ACanvas := Canvas.Handle;
AText := Memo1.Lines[0];
Repaint;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin
if ACanvas <> 0 then
TextOut(ACanvas, Button1.Left, 20, PChar(AText), Length(AText));
end;
</pre>
<br />
<br /></div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com17tag:blogger.com,1999:blog-2374465879949372415.post-78938408303807444202013-12-19T23:21:00.002+04:002013-12-20T11:49:06.299+04:00Ответ на задачу №1<div dir="ltr" style="text-align: left;" trbidi="on">
Откуда вообще появляются такие вот непонятные куски кода в которых различные авторы предлагают искать ошибки? Вопрос по сути философский - народное творчество :)<br />
Благодаря народному творчеству и неким "неизвестным" товарищам, чьи произведения подхватываются и расползаются на лету по многочисленным форумам, мы можем наблюдать такие перлы как WParam в обработчике хука объявленный типом Word, либо реализацию метода Execute класса TThread обернутую в Synhronize да, в прочем, можно увидеть даже перекрытие штатной DLLEntryPoint с соглашением вызова stdcall (какая разница что это только обертка над DllMain - пусть и у нас будет так, как у "взрослых дядек" :)<br />
Особенно обидно становится тогда, когда это приобретает массовый характер.<br />
Очень сложно объяснить человеку на форуме, что тот код, который он прочитал в очередной книжке "для хакеров", по сути не верен чуть менее чем полностью, ведь кто я такой - по сути некий неизвестный аноним в интернет пространстве, а у автора вопроса на руках есть целая книга, выпущенная серьезной издательской конторой, к которой доверия будет явно больше чем к моим ответам :)<br />
<br />
Сейчас мы будем рассматривать один из образцов такого кода.<br />
<br />
Он достаточно популярен в интернете, к примеру:<br />
<a href="http://theroadtodelphi.wordpress.com/2009/10/26/detect-aero-glass-using-delphi/" target="_blank">http://theroadtodelphi.wordpress.com/2009/10/26/detect-aero-glass-using-delphi/</a><br />
или вот так:<br />
<a href="http://www.sql.ru/forum/900738/kak-uznat-vkluchen-li-aero-v-window-7-vista" target="_blank">http://www.sql.ru/forum/900738/kak-uznat-vkluchen-li-aero-v-window-7-vista</a><br />
Где посреди прочих разумных вариантов звучит и такой: "Этот код не приводит к крешу приложения, ищи причину в другом месте."<br />
<br />
Представьте, вы еще плаваете немного с различными указателями и прочим и тут вас огорошивают такой фразой: "этот код валиден - ошибка не здесь". Какие ваши действия? Конечно, вы будете перелопачивать сотни строк кода, пытаясь понять, где ж я промахнулся.<br />
Плюсы, конечно есть - вероятно вам удастся найти еще несколько ошибок и исправить их, но изначальную проблему решить не получится и придется снова и снова строчить вопросы на форумах плана: "программа падает, и похоже даже на 17-ой строке - поможите".<br />
<br />
Впрочем... к нашим баранам.<br />
<br />
<a name='more'></a><br />
Преамбула выглядела следующим образом:<br />
<br />
Дана функция, <a href="http://www.gunsmoker.ru/2010/05/90.html" target="_blank">реализованная в классическом стиле</a>.<br />
<br />
<pre class="brush:delphi">function IsAeroEnabledCheck: Boolean;
type
_DwmIsCompositionEnabledFunc = function(IsEnabled: PBool): HRESULT; stdcall;
var
DllHandle: THandle;
Flag: Boolean;
DwmIsCompositionEnabledFunc: _DwmIsCompositionEnabledFunc;
begin
DllHandle := LoadLibrary('dwmapi.dll');
if DllHandle > HINSTANCE_ERROR then
try
@DwmIsCompositionEnabledFunc := GetProcAddress(DllHandle, 'DwmIsCompositionEnabled');
if (@DwmIsCompositionEnabledFunc <> nil) then
begin
DwmIsCompositionEnabledFunc(@Flag);
Result := Flag;
end;
finally
FreeLibrary(DllHandle);
end;
end;
</pre>
<br />
Задача, описать что в данном коде не верно.<br />
<br />
<b>Основные предложения выглядели так:</b><br />
<br />
1. Результат выполнения функции может быть не определен.<br />
2. Отсутствует проверка результата выполнения DwmIsCompositionEnabledFunc.<br />
3. Условие "DllHandle > HINSTANCE_ERROR" некорректно.<br />
4. Может быть разрушен стек.<br />
5. SEH избыточен (try..finally)<br />
<br />
<b>Рассматриваем первый и второй пункт в совокупности.</b><br />
<b><br /></b>
Да, действительно - это самое первое, что должно бросится в глаза человеку, который будет анализировать код данной функции. В теории, после нахождения данных ошибок, как правило анализ кода останавливается и выносится вердикт, что ошибка найдена.<br />
Правда <a href="http://alexander-bagel.blogspot.ru/2013/12/1.html" target="_blank">изначально при постановке задачи</a> я допустил оговорку, что данная задача у меня осталась от собеседований соискателей на вакансию.<br />
<br />
Попробую пояснить.<br />
Вы наверное знаете тот старый анекдот, как создать проблему из "ничего"? Очень просто - достаточно взять "ничего" и женщину, проблема готова.<br />
<br />
Моя задача, перед проведением собеседования, заключается в подготовке материала, который поможет мне "расшатать" соискателя и определить его готовность решать нестандартные задачи. Поэтому, как правило, приходится брать код с явной ошибкой и как можно более качественно камуфлировать саму ошибку, вытаскивая наружу то, на что сможет среагировать соискатель первым делом (кто проходит сертификацию от MS, или хотя бы изучал публичные тесты от данной конторы, меня поймет :)<br />
<br />
Поэтому да, что первый, что второй пункт, были определены верно - но на самом деле ошибка не в них.<br />
<br />
<b>Пункт третий.</b><br />
<b><br /></b>
Ну... начать наверное нужно с того, что переменная DllHandle объявлена как THandle.<br />
В принципе, ничего страшного не произойдет (даже в 64 битном коде), кабы не одно НО.<br />
Никогда нельзя путать хэндл с инстансом.<br />
Хэндлы бывают разные, в частности хэндл USER объектов (окна/таймеры/хуки) представляют из себя определенную структуру, где старшие 16 бит представляют из себя уникальный идентификатор, по которому происходит проверка при обращению к массиву описателей, индекс которого хранится в младших 14 битах + самые нижние 2 бита используются под различные конфигурационные вещи (ну к примеру автоматом закрывать хэндл на директорию при создании процесса и т.п. - об этом я расскажу в последующих статьях).<br />
А вот функция LoadLibrary возвращает не хэндл, а указатель на инстанс загруженного модуля. Грубо адрес, прочитав два байта с которого, мы сможем наблюдать столь известные всему миру инициалы Марка Збиковски "MZ".<br />
<br />
Откуда вообще взялся этот HINSTANCE_ERROR?<br />
Тут все просто - это наследие от Win3.1 где все было организовано немного через не так как сейчас :)<br />
<br />
А теперь посмотрим как организована память приложения:<br />
Первые 64кб любого процесса (32/64) имеют атрибуты доступа NoAccess.<br />
Данный регион памяти используется для детектирования битых указателей.<br />
Поэтому, раз мы имеем на руках адрес загрузки библиотеки, которая явно не может быть загружена по адресу от нуля до 0x10000, то более корректно будет проверять данный адрес с числом 0x10000, а не с нулем или тем более с HINSTANCE_ERROR.<br />
Хотя лучше конечно придерживаться документации - которая говорит что ноль это плохо.<br />
<br />
С наследием от Win 3.1 разобрались, а что сейчас?<br />
А сейчас все гораздо проще, LoadLibrary действительно возвращает ноль, в случае ошибки загрузки библиотеки, но есть одно НО.<br />
Перед тем как принять решение о загрузке библиотеки данная функция пытается найти ее в структуре LDR_DATA_TABLE_ENTRY, которая доступна для модификации из пользовательского кода прямо в Ring3. Если она ее находит, то возвращает тот адрес, который записан в соответствующем поле данной структуры (одного из списков).<br />
<br />
Правильно пошаманив с данной структурой мы можем добиться как раз такого поведения, чтобы LoadLibrary выдала не ноль и не валидный инстанс образа, а к примеру число 0x777, к примеру для того чтобы вызвать отказ выполнения кода во взламываемом нами приложении, либо наоборот, мы можем правильно инициализировав кусок памяти представить его как загруженную библиотеку, причем таким образом что штатный вызов GetProcAddress найдет якобы экспортируемую этим модулем функцию (об этом будет отдельная обзорная статья).<br />
<br />
<b>Впрочем далее - четвертый пункт (Фабула).</b><br />
<br />
В четвертом пункте содержится вся соль данного кода - разрушение стека (как бы нам ни пытались доказать обратное на форумах).<br />
<br />
Суть проста, DwmIsCompositionEnabled хоть и принимает параметром указатель, но под разименованным указателем должен лежать буфер размером в 4 байта, в который она будет писать ровно 4 байта, но ни в коем разе не однобайтовый Boolean.<br />
<br />
Посмотрим картинку:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrAipOCnN-Sm07ioHbtant1Jffd51VOSVvWy1JvGEBpwPV-0-_zbVd9E0W4focKmPTTQRaEYFizDkPvASG7OQpfk_LMN5vuN3hKYLcd855UgWkz2jy79BDSS192AFUV8m-6etw-NKCPiY/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrAipOCnN-Sm07ioHbtant1Jffd51VOSVvWy1JvGEBpwPV-0-_zbVd9E0W4focKmPTTQRaEYFizDkPvASG7OQpfk_LMN5vuN3hKYLcd855UgWkz2jy79BDSS192AFUV8m-6etw-NKCPiY/s1600/1.png" /></a></div>
<br />
Видны отлично расставленные каменты в OllyDebug - респектую автору, кабы не одно но!!!<br />
Запись в буфер происходит внутри вызова DwmIsCompositionEnabled, а картинка показывает работу с уже "убитым" стеком и конкретнее c переменной Flag, где уже можно "сушить весла".<br />
<br />
Впрочем, когда я говорил о том, что пример был специально подготовлен таким образом, чтобы это не бросилось в глаза - я не слукавил.<br />
<br />
Данный код был составлен таким образом, чтоб в результате его выполнения и порчи стека поменяется всего лишь значение переменной DllHandle, что сможет заметить только очень крайне внимательный при отладке человек :)<br />
<br />
Демонстрирую:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizSSwqiz4JXuRmsOi0cL29dEOdCIwOr_VGdROGNKyPHlzYTKc1oSq3yd4F2A-rwbOeCp_wmiXrg_77z-0ap7qATRKVYAp3w9NvWbVa_So-C4EXB6CtG4mBFv_V0BD0Yd9gfTiCNw_VDas/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizSSwqiz4JXuRmsOi0cL29dEOdCIwOr_VGdROGNKyPHlzYTKc1oSq3yd4F2A-rwbOeCp_wmiXrg_77z-0ap7qATRKVYAp3w9NvWbVa_So-C4EXB6CtG4mBFv_V0BD0Yd9gfTiCNw_VDas/s1600/2.png" /></a></div>
<br />
Что в итоге приведет в отказе выгрузки библиотеки, ибо функция FreeLibrary не будет генерировать исключения при невалидном хэндле.<br />
Переписав код вот таким образом, мы сразу получим на руки "бадабум":<br />
<br />
<pre class="brush:delphi">function IsAeroEnabledCheck: Boolean;
type
_DwmIsCompositionEnabledFunc = function(IsEnabled: PBool): HRESULT; stdcall;
var
DwmIsCompositionEnabledFunc: _DwmIsCompositionEnabledFunc;
DllHandle: THandle;
Tmp: TObject;
Flag: Boolean;
begin
DllHandle := LoadLibrary('dwmapi.dll');
if DllHandle > HINSTANCE_ERROR then
try
@DwmIsCompositionEnabledFunc := GetProcAddress(DllHandle, 'DwmIsCompositionEnabled');
if (@DwmIsCompositionEnabledFunc <> nil) then
begin
Tmp := TObject.Create;
DwmIsCompositionEnabledFunc(@Flag);
Writeln(Tmp.ClassName);// << падаем тут
Result := Flag;
end;
finally
FreeLibrary(DllHandle);
end;
end;
</pre>
<br />
Ну а что произойдет, если вдруг побьется адрес, на который ориентируется финализирующий RET - я даже не берусь предсказать.<br />
<br />
Еще более интереснее становится при переключении флага оптимизации.<br />
Правильно подготовив параметры функции можно добиться того что из-за выравнивания в одном режиме память будет биться, а в другом нет.<br />
<br />
Собственно пункт номер пять я думаю рассматривать смысла не имеет, ибо исходя и предыдущего примера и так понятно, для чего тут нужен try..finally<br />
<br />
Вкратце как-то так, думаю данный материал был для вас полезен или по крайней мере заставил немного задуматься.<br />
<br />
Впрочем в завершение позволю себе вернуться к вопросу о перекрытии DLLEntryPoint.<br />
Если кто-то сможет внятно объяснить для чего нужно ее перекрытие вот таким образом:<br />
<br />
<pre class="brush:delphi">procedure DLLEntryPoint(dwReason: DWORD); //stdcall; <<< нет там stdcall и небыло никогда...
begin
...
end;
begin
DLLProc := @DLLEntryPoint;
DLLEntryPoint(DLL_PROCESS_ATTACH); // <<< зачем нужен именно этот вызов? ответ объяснить.
end.
</pre>
<br />
... тому лично от меня респект и уважуха и +1 в карму :)<br />
<br />
<b>UPD:</b><br />
<b><br /></b>
Правильный вариант кода выглядит следующим образом.<br />
<br />
<pre class="brush:delphi">function IsAeroEnabledCheck: Boolean;
type
_DwmIsCompositionEnabledFunc = function(IsEnabled: PBool): HRESULT; stdcall;
var
DllHandle: THandle;
Flag: BOOL;
DwmIsCompositionEnabledFunc: _DwmIsCompositionEnabledFunc;
begin
Result := False;
DllHandle := LoadLibrary('dwmapi.dll');
if DllHandle <> 0 then
try
@DwmIsCompositionEnabledFunc := GetProcAddress(DllHandle, 'DwmIsCompositionEnabled');
if Assigned(@DwmIsCompositionEnabledFunc) then
if Succeeded(DwmIsCompositionEnabledFunc(@Flag)) then
Result := Flag;
finally
FreeLibrary(DllHandle);
end;
end;
</pre>
<br />
---<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
© Александр (Rouse_) Багель</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Декабрь, 2013</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com17tag:blogger.com,1999:blog-2374465879949372415.post-18907800059876962172013-12-17T19:36:00.001+04:002013-12-20T15:17:48.814+04:00Задачка на понимание №1<div dir="ltr" style="text-align: left;" trbidi="on">
Основную идею задачек я подсмотрел у Александра Алексеева (более известного как GUNSMOKER), и подумал - а почему бы и мне не открыть такой подраздел, ибо таких у меня накопилось предостаточно от собеседований кандидатов на вакансию :)<br />
<br />
Итак, дана функция, <a href="http://www.gunsmoker.ru/2010/05/90.html" target="_blank">реализованная в классическом стиле</a> :<br />
<br />
<pre class="brush:delphi">function IsAeroEnabledCheck: Boolean;
type
_DwmIsCompositionEnabledFunc = function(IsEnabled: PBool): HRESULT; stdcall;
var
DllHandle: THandle;
Flag: Boolean;
DwmIsCompositionEnabledFunc: _DwmIsCompositionEnabledFunc;
begin
DllHandle := LoadLibrary('dwmapi.dll');
if DllHandle > HINSTANCE_ERROR then
try
@DwmIsCompositionEnabledFunc := GetProcAddress(DllHandle, 'DwmIsCompositionEnabled');
if (@DwmIsCompositionEnabledFunc <> nil) then
begin
DwmIsCompositionEnabledFunc(@Flag);
Result := Flag;
end;
finally
FreeLibrary(DllHandle);
end;
end;
</pre>
<br />
Задача, описать что в данном коде не верно.</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com14tag:blogger.com,1999:blog-2374465879949372415.post-32721360097430448862013-11-14T15:54:00.000+04:002013-11-14T19:15:40.426+04:00Карта памяти процесса<div dir="ltr" style="text-align: left;" trbidi="on">
Задумывались ли вы над тем, как именно используется память, доступная вашей программе, да и вообще, что именно размещается в этих двух-трех гигабайтах виртуальной памяти, с которыми работает ваше ПО?<br />
<br />
Спросите, зачем?<br />
Ну как же, для 32-битного приложения 2-3 гигабайта – это ваш лимит за пределы которого без использования AWE вы выбраться не сможете, а контролировать собственные ресурсы все же желательно. Но даже и без этого просто с целью разобраться...<br />
<br />
В прошлых статьях я описывал работу отладчика, где производились модификации памяти приложения, находящегося под отладкой. Эта статья является продолжением данного материала. И хотя к отладчику она не будет иметь отношения, но вот к процессу отладки – самое непосредственное...<br />
<br />
Давайте посмотрим, как именно программист работает с памятью при отладке (особенно при отладке стороннего приложения, проще говоря, при реверсе):<br />
<br />
1. Как правило, самой частой операцией будет поиск значения в памяти приложения и, к сожалению, данный функционал почему-то не предоставлен в отладчике Delphi (собственно, как и в MS VC++).<br />
2. Модификация системных структур (<abbr title="Process Environment Block">PEB</abbr>/<abbr title="Thread Environment Block">TEB</abbr>/SEHChain/Unwind/директорий PE-файлов etc...) будет происходить гораздо проще, когда поля структур размаплены на занимаемые ими адреса и представлены в читабельном виде.<br />
3. Отслеживание изменений в памяти процесса (практически никем не предоставляемый функционал, реализованный в виде плагинов к популярным отладчикам). Действительно, зачем трассировать до посинения, когда достаточно сравнить два снимка карты памяти, чтобы понять, тут ли происходит нужная нам модификация данных или нет?<br />
<br />
Да, собственно, вариантов использования много.<br />
<br />
Впрочем, если без лирики, утилит отображающих более-менее вменяемую информацию о карте памяти процесса, которую можно применить для отладки, очень мало.<br />
<br />
Самая удобная реализация от OllyDebug 2, но, к сожалению, она не отображает данные по 64 битам (все еще ждем).<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAjTs_DVek6ZfFxTXiN47HlFDEJjH-v7CTUKVMz2wvAxuOGu1D3KHgfvDRU72mdnfs468O3BC1MMDJRVAIC9MSEcTPQXSR38pwEvPRmV0lIzswu0UDS5t9cIkHBZ-0oVjqTG7UeDqZ1Uk/s1600/1.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="199" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAjTs_DVek6ZfFxTXiN47HlFDEJjH-v7CTUKVMz2wvAxuOGu1D3KHgfvDRU72mdnfs468O3BC1MMDJRVAIC9MSEcTPQXSR38pwEvPRmV0lIzswu0UDS5t9cIkHBZ-0oVjqTG7UeDqZ1Uk/s320/1.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">OllyDebug 2 - Process Environment Block Dump</td></tr>
</tbody></table>
<br />
VMMap от Марка Руссиновича выполняет чисто декоративные свойства, да красиво, да за подписью Microsoft, но практически применить выводимые ей данные тяжеловато.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjS_SMuFbaw4EM_uoEH_O4QKp_Xvj_x4J66hjY8kHR1Awd6bqiufzFbNa_HNQZ8i5bhxn5MKE9FPcsrQ9SS_ZcPXbe47dqNNdmYO3s-_CQQNDg8XEcCXduUvDDR0cn6hlvDwZRDoTG-pyk/s1600/2.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="260" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjS_SMuFbaw4EM_uoEH_O4QKp_Xvj_x4J66hjY8kHR1Awd6bqiufzFbNa_HNQZ8i5bhxn5MKE9FPcsrQ9SS_ZcPXbe47dqNNdmYO3s-_CQQNDg8XEcCXduUvDDR0cn6hlvDwZRDoTG-pyk/s320/2.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">VMMap отображает только самую базовую информацию не вникая в детали.</td></tr>
</tbody></table>
<br />
ProcessHacker – хороший инструмент, но его автор не ставил перед собой задач по работе с выводом данных о памяти, поэтому выводимая им информация можно сказать вообще самая простая.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0G7xmkf4yMUE0Z1YOfe5BJpJ9-48gVmpI2jcu2e3Cd0tzR6Akjfg4YztyDmmnhjRGEml_7EJ4WODJ8_ZEXsHrhydnOPUUiEP71OZUzQ-ItGBqQBUSmR-ldxoIBLhghI7DX1JfsUQFs1M/s1600/3.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="249" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0G7xmkf4yMUE0Z1YOfe5BJpJ9-48gVmpI2jcu2e3Cd0tzR6Akjfg4YztyDmmnhjRGEml_7EJ4WODJ8_ZEXsHrhydnOPUUiEP71OZUzQ-ItGBqQBUSmR-ldxoIBLhghI7DX1JfsUQFs1M/s320/3.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">Process Hacker - Memory Dump</td></tr>
</tbody></table>
<br />
Ну а к карте памяти от IDA Pro за столько лет работы с ней я так и не привык (мне не удобно) :)<br />
<br />
Впрочем, отладка это не все, где может пригодиться валидная карта памяти. В частности, по работе я использую карту памяти при анализах лога ошибок, присылаемых нам пользователями вместе с дампом критических участков, интегрировав информацию о ней в EurekaLog.<br />
<br />
В данной статье я попробую по шагам рассказать, как самостоятельно составить карту памяти процесса и разместить в ней информацию о нужных для отладки и анализа данных.<br />
<br />
<a name='more'></a><br />
<h3>
Содержание</h3>
<div>
<br /></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page1">1. Получаем список доступных регионов</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page2">2. Собираем данные о потоках</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page3">3. Собираем данные о кучах</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page4">4. Собираем данные о загруженных PE файлах</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page5">5. Блок окружения процесса (PEB) + KUSER_SHARED_DATA</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page6">6. TRegionData</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page7">7. TMemoryMap</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page8">8. TSymbols - работа с символами</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page9">9. ProcessMemoryMap</a></div>
<div>
<a href="http://alexander-bagel.blogspot.ru/2013/11/pmm2.html#page10">10. В качестве заключения</a></div>
<br />
<h3>
<br /></h3>
<h3>
1. Получаем список доступных регионов</h3>
<br />
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page1"></a>Вся виртуальная память процесса представлена в виде страниц.<br />
Страницы бывают маленькие (4096 байт) и большие. (Подробнее можно узнать в <a href="http://msdn.microsoft.com/en-us/library/aa366543(VS.85).aspx" target="_blank">MSDN</a>)<br />
В большинстве случаев идущие подряд страницы имеют одинаковые атрибуты.<br />
<br />
Что есть регион?<br />
Грубо (<a href="http://msdn.microsoft.com/en-us/library/aa366775(v=vs.85).aspx" target="_blank">если взять за основу MSDN</a>) – это набор всех страниц имеющих одинаковые атрибуты, которые начинающихся с переданного функции VirtualQuery адреса.<br />
<br />
В самом простейшем виде получить список регионов нашего процесса можно вот таким кодом:<br />
<br />
<pre class="brush:delphi">program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
SysUtils;
var
MBI: TMemoryBasicInformation;
dwLength: NativeUInt;
Address: PByte;
begin
Address := nil;
dwLength := SizeOf(TMemoryBasicInformation);
while VirtualQuery(Address, MBI, dwLength) <> 0 do
begin
Writeln(
'AllocationBase: ', IntToHex(NativeUInt(MBI.AllocationBase), 8),
', BaseAddress: ', IntToHex(NativeUInt(MBI.BaseAddress), 8),
', RegionSize: ', MBI.RegionSize);
Inc(Address, MBI.RegionSize);
end;
Readln;
end.
</pre>
<br />
К примеру, изначально мы передали первым параметром адрес <b>nil</b>. После вызова функции переменная MBI примет следующие значения:<br />
<br />
<ul>
<li>BaseAddress = nil</li>
<li>AllocationBase = nil</li>
<li>AllocationProtect = 0</li>
<li>RegionSize = $10000</li>
<li>State = $10000</li>
<li>Protect = 1</li>
<li>Type_9 = 0</li>
</ul>
<br />
Размер региона равен $10000 (64 кб), это соответствует 16 страницам, идущим подряд, начиная с адреса ноль, состояние которых (State) равно MEM_FREE ($10000) и выставлен атрибут защиты PAGE_NO_ACCESS (1) в параметре Protect.<br />
<br />
Если переписать код вот таким образом:<br />
<br />
<pre class="brush:delphi">function ExtractAccessString(const Value: DWORD): string;
const
PAGE_WRITECOMBINE = $400;
begin
Result := 'Unknown access';
if (Value and PAGE_EXECUTE) = PAGE_EXECUTE then Result := 'E';
if (Value and PAGE_EXECUTE_READ) = PAGE_EXECUTE_READ then Result := 'RE';
if (Value and PAGE_EXECUTE_READWRITE) = PAGE_EXECUTE_READWRITE then
Result := 'RWE';
if (Value and PAGE_EXECUTE_WRITECOPY) = PAGE_EXECUTE_WRITECOPY then
Result := 'RE, Write copy';
if (Value and PAGE_NOACCESS) = PAGE_NOACCESS then Result := 'No access';
if (Value and PAGE_READONLY) = PAGE_READONLY then Result := 'R';
if (Value and PAGE_READWRITE) = PAGE_READWRITE then Result := 'RW';
if (Value and PAGE_WRITECOPY) = PAGE_WRITECOPY then Result := 'Write copy';
if (Value and PAGE_GUARD) = PAGE_GUARD then
Result := Result + ', Guarded';
if (Value and PAGE_NOCACHE) = PAGE_NOCACHE then
Result := Result + ', No cache';
if (Value and PAGE_WRITECOMBINE) = PAGE_WRITECOMBINE then
Result := Result + ', Write Combine';
end;
function ExtractRegionTypeString(Value: TMemoryBasicInformation): string;
begin
Result := '';
case Value.State of
MEM_FREE: Result := 'Free';
MEM_RESERVE: Result := 'Reserved';
MEM_COMMIT:
case Value.Type_9 of
MEM_IMAGE: Result := 'Image';
MEM_MAPPED: Result := 'Mapped';
MEM_PRIVATE: Result := 'Private';
end;
end;
Result := Result + ', ' + ExtractAccessString(Value.Protect);
end;
var
MBI: TMemoryBasicInformation;
dwLength: NativeUInt;
Address: PByte;
begin
Address := nil;
dwLength := SizeOf(TMemoryBasicInformation);
while VirtualQuery(Address, MBI, dwLength) <> 0 do
begin
Writeln(
'AllocationBase: ', IntToHex(NativeUInt(MBI.AllocationBase), 8),
', BaseAddress: ', IntToHex(NativeUInt(MBI.BaseAddress), 8),
' - ', ExtractRegionTypeString(MBI));
Inc(Address, MBI.RegionSize);
end;
</pre>
<br />
... то можно наглядно увидеть принцип разбиения на регионы функцией VirtualAlloc:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmoa86_uMnf9-bunct6INepQ_yWKjtl18_Ckr-FpqKgEPkcBDoxRY6KUAXBNGC5syfpU-F7y_4dJW5qejzIscxDDfQrAbRVfj51QH7aSxlq89SNrTFqjQg0bk48tlQRFGGQ9-X7pfkYFk/s1600/4.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmoa86_uMnf9-bunct6INepQ_yWKjtl18_Ckr-FpqKgEPkcBDoxRY6KUAXBNGC5syfpU-F7y_4dJW5qejzIscxDDfQrAbRVfj51QH7aSxlq89SNrTFqjQg0bk48tlQRFGGQ9-X7pfkYFk/s1600/4.png" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">Страницы объединяются в регионы по совокупности свойств</td></tr>
</tbody></table>
<br />
К примеру, у второго и третьего региона атрибуты доступа одинаковые (чтение запись), но разная AllocationBase. AllocationBase назначается страницам при выделении памяти посредством VirtualAlloc, объединяя их таким образом в отдельный регион.<br />
<br />
<h3>
2. Собираем данные о потоках</h3>
<br />
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page2"></a>Пришла пора начать заполнять полученные нами регионы информацией о том, что они хранят, и начнем мы с потоков (нитей – кому как удобнее).<br />
<br />
Код получения списка потоков простой – через CreateToolhelp32Snapshot.<br />
<br />
<pre class="brush:delphi">const
THREAD_GET_CONTEXT = 8;
THREAD_SUSPEND_RESUME = 2;
THREAD_QUERY_INFORMATION = $40;
ThreadBasicInformation = 0;
ThreadQuerySetWin32StartAddress = 9;
STATUS_SUCCESS = 0;
var
hSnap, hThread: THandle;
ThreadEntry: TThreadEntry32;
TBI: TThreadBasicInformation;
TIB: NT_TIB;
lpNumberOfBytesRead: NativeUInt;
ThreadStartAddress: Pointer;
begin
// Делаем снимок нитей в системе
hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId);
if hSnap <> INVALID_HANDLE_VALUE then
try
ThreadEntry.dwSize := SizeOf(TThreadEntry32);
if Thread32First(hSnap, ThreadEntry) then
repeat
if ThreadEntry.th32OwnerProcessID <> GetCurrentProcessId then Continue;
Writeln('ThreadID: ', ThreadEntry.th32ThreadID);
// Открываем нить
hThread := OpenThread(THREAD_GET_CONTEXT or
THREAD_SUSPEND_RESUME or THREAD_QUERY_INFORMATION,
False, ThreadEntry.th32ThreadID);
if hThread <> 0 then
try
// Получаем адрес ThreadProc()
if NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress,
@ThreadStartAddress, SizeOf(ThreadStartAddress), nil) = STATUS_SUCCESS then
Writeln('ThreadProcAddr: ', IntToHex(NativeUInt(ThreadStartAddress), 1));
// Получаем информацию по нити
if NtQueryInformationThread(hThread, ThreadBasicInformation, @TBI,
SizeOf(TThreadBasicInformation), nil) = STATUS_SUCCESS then
begin
Writeln('Thread Environment Block (TEB) Addr: ',
IntToHex(NativeUInt(TBI.TebBaseAddress), 1));
// Читаем из удаленного адресного пространства
// TIB (Thread Information Block) открытой нити
if ReadProcessMemory(GetCurrentProcess,
TBI.TebBaseAddress, @TIB, SizeOf(NT_TIB),
lpNumberOfBytesRead) then
begin
Writeln('Thread StackBase Addr: ',
IntToHex(NativeUInt(TIB.StackBase), 1));
Writeln('Thread StackLimit Addr: ',
IntToHex(NativeUInt(TIB.StackLimit), 1));
end;
end;
finally
CloseHandle(hThread);
end;
until not Thread32Next(hSnap, ThreadEntry);
finally
CloseHandle(hSnap);
end;
Readln;
end.
</pre>
<br />
По шагам:<br />
<br />
<ol>
<li>При помощи CreateToolhelp32Snapshot/Thread32First/Thread32Next получаем список активных потоков у нашего приложения.</li>
<li>Для получения более подробной информации потребуется хендл потока, который получаем посредством вызова OpenThread.</li>
<li>При помощи NtQueryInformationThread получаем адрес процедуры потока, с которой он начал работу, и базовую информацию о потоке в виде структуры TThreadBasicInformation.</li>
<li>Из этой структуры нас интересует только одно поле – TebBaseAddress, которое содержит адрес блока окружения потока, т.н. <abbr title="Thread Environment Block">TEB </abbr>(Thread Environment Block).</li>
<li>Посредством вызова ReadProcessMemory (хотя для своего приложения это и избыточно) зачитываем данные по адресу <abbr title="Thread Environment Block">TEB</abbr>, а именно самый первый ее параметр, представляющий из себя структуру NT_TIB.</li>
</ol>
<br />
Декларация NT_TIB выглядит так:<br />
<br />
<pre class="brush:delphi"> PNT_TIB = ^_NT_TIB;
_NT_TIB = record
ExceptionList: Pointer;
StackBase,
StackLimit,
SubSystemTib: Pointer;
case Integer of
0: (
FiberData: Pointer
);
1: (
Version: ULONG;
ArbitraryUserPointer: Pointer;
Self: PNT_TIB;
)
end;
NT_TIB = _NT_TIB;
PPNT_TIB = ^PNT_TIB;
</pre>
<br />
Ну или вот так, если описывать чуть подробнее:<br />
<br />
<ul>
<li>ExceptionList – в 32-битном процессе указатель на адрес текущего <abbr title="Structured Exception Handling">SEH</abbr> фрейма (структуру EXCEPTION_REGISTRATION). Основываясь на данной информации, мы будем раскручивать всю цепочку <abbr title="Structured Exception Handling">SEH</abbr> фреймов.<br />Если же <abbr title="Thread Environment Block">TEB</abbr> принадлежит 64-битному потоку, работающего в 32-битном приложении, то данное поле будет указывать на поле ExceptionList своего 32-битного аналога.<br />В 64-битном процессе данное поле всегда обнилено, т.к. для 64 бит взамен механизма <abbr title="Structured Exception Handling">SEH</abbr> работает <a href="http://qxov.narod.ru/articles/seh/seh.html" target="_blank">немного другой механизм</a>.</li>
<li>StackBase – база стека. Адрес от которого стек начинает расти в направлении StackLimit.</li>
<li>StackLimit – текущая верхушка стека.</li>
<li>ArbitraryUserPointer – что-то наподобие свободного TLS слота. Грубо говоря переменная принадлежащая потоку, значение которой может произвольно изменятся самим программистом для собственных нужд.</li>
<li>Self - параметр, содержащий адрес TEB (т.е. самого себя)</li>
</ul>
<br />
Остальные поля не нужны.<br />
<br />
Ну, впрочем, как – не нужны?<br />
Нужны, конечно, но пока что они для нас избыточны.<br />
Кстати, вот ссылка, где вы сможете увидеть слегка устаревшее описание данной структуры: <a href="http://en.wikipedia.org/wiki/Win32_Thread_Information_Block" target="_blank">Thread Environment Block</a>.<br />
<br />
Данный код отобразит нам следующую картинку:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRjReqeNkBj9wWe1lYsRWjFp78S4zSNG2S-mXGPsXJQ8u6WlE0YTCFUGX8TpaV1yOEJ903xRfmC4qTdxcPvpvu0AYKkI7d2hXaFdfhhIdLTeGwSGAco67pxEGBcm4FPd1NGkiwZX1ByyM/s1600/5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRjReqeNkBj9wWe1lYsRWjFp78S4zSNG2S-mXGPsXJQ8u6WlE0YTCFUGX8TpaV1yOEJ903xRfmC4qTdxcPvpvu0AYKkI7d2hXaFdfhhIdLTeGwSGAco67pxEGBcm4FPd1NGkiwZX1ByyM/s1600/5.png" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<br />
А вот так это будет видно в VMMap.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="clear: right; margin-bottom: 1em; margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2wlDzFwaGqm7Fi9Bynb2CX0TlrbGZm_8wSL9YhEZLpIwhgTt1XaV9qwEmJb_j_1KTN9Dt7TPq-r8nGA_4FGYC6Rl0SiPV_7tCMVtStW9pOnG3R77SHUb46eOM_WD79G6pBHMeBIm3t3g/s1600/6.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2wlDzFwaGqm7Fi9Bynb2CX0TlrbGZm_8wSL9YhEZLpIwhgTt1XaV9qwEmJb_j_1KTN9Dt7TPq-r8nGA_4FGYC6Rl0SiPV_7tCMVtStW9pOnG3R77SHUb46eOM_WD79G6pBHMeBIm3t3g/s640/6.png" width="640" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">VMMap не отобразила информацию о <abbr title="Thread Environment Block">TEB</abbr></td></tr>
</tbody></table>
Кстати, часть функций и структур из приведенного выше кода не задекларированы в стандартных исходниках Delphi, их декларацию вы сможете увидеть в <a href="http://rouse.drkb.ru/blog/pmm2.zip" target="_blank">демо-примерах</a>, идущих в составе данной статьи. Но это не означает того, что они недокументированы в MSDN :)<br />
<br />
Если мы захотим работать с TEB своего потока, то код очень сильно упростится из-за того что не нужно использовать функции ToolHelp32.dll, а достаточно использовать сегментный регистр FS (или GS для х64).<br />
К примеру, очень часто встречается такая функция для получения адреса TEB:<br />
<br />
<pre class="brush:delphi">function GetCurrentTEB: Pointer;
asm
{$IFDEF WIN64}
// mov RAX, qword ptr GS:[30h]
// реализованно через машкоды, ввиду неверной генерации кода инструкции 64-битным компилятором
DB $65, $48, $8B, $04, $25
DD $00000030
{$ELSE}
mov EAX, FS:[18h]
{$ENDIF}
end;
</pre>
<div>
<br /></div>
В данном случае происходит доступ к параметру NtTIB.Self структуры TEB, который расположен по смещению 0x18 (или 0x30 в случае 64-битного TEB) от ее начала.<br />
<br />
Впрочем, продолжим...<br />
Часть данных получили, но это не вся информация доступная нам.<br />
<br />
На стеке каждого потока расположены <abbr title="Structured Exception Handling">SEH</abbr> фреймы, которые генерируются автоматом при входе в блок try..finally/except, а также стек вызовов процедур. Было бы хорошо иметь эти данные на руках и выводить их в более наглядном виде – с привязкой к региону.<br />
<br />
Раскруткой <abbr title="Structured Exception Handling">SEH</abbr> фреймов у нас будет заниматься вот такая простенькая процедура:<br />
<br />
<pre class="brush:delphi">procedure GetThreadSEHFrames(InitialAddr: Pointer);
type
EXCEPTION_REGISTRATION = record
prev, handler: Pointer;
end;
var
ER: EXCEPTION_REGISTRATION;
lpNumberOfBytesRead: NativeUInt;
begin
while ReadProcessMemory(GetCurrentProcess, InitialAddr, @ER,
SizeOf(EXCEPTION_REGISTRATION), lpNumberOfBytesRead) do
begin
Writeln('SEH Frame at Addr: ',
IntToHex(NativeUInt(InitialAddr), 1), ', handler at addr: ',
IntToHex(NativeUInt(ER.handler), 1));
InitialAddr := ER.prev;
if DWORD(InitialAddr) <= 0 then Break;
end;
end;
</pre>
<br />
Получив в качестве параметра значение TEB.TIB.ExceptionList, которое указывает на первую структуру EXCEPTION_REGISTRATION, она бежит по цепочке данных структур, ориентируясь на значение prev данной структуры, которое содержит адрес предыдущей структуры EXCEPTION_REGISTRATION. А параметр handler содержит адрес обработчика исключения, если оно вдруг произойдет.<br />
<br />
Выглядит все вот так:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhsESw1haRUMoyNexZUZBESxcJt1XysXXgKXElEH75L98pY6VVkuqY9sC6gbZEoUkw8Taeg6eVN5XGxrcViq9a6hOvRrLCUwrigC8VL1NJaMgEvKwgXVw-2i9WOpZtum-mi6i8n59K0i-M/s1600/7.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhsESw1haRUMoyNexZUZBESxcJt1XysXXgKXElEH75L98pY6VVkuqY9sC6gbZEoUkw8Taeg6eVN5XGxrcViq9a6hOvRrLCUwrigC8VL1NJaMgEvKwgXVw-2i9WOpZtum-mi6i8n59K0i-M/s1600/7.png" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">Список SEH фреймов</td></tr>
</tbody></table>
<br />
Ну а CallStack будет получать следующая процедура:<br />
<br />
<pre class="brush:delphi">procedure GetThreadCallStack(hThread: THandle);
var
StackFrame: TStackFrame;
ThreadContext: PContext;
MachineType: DWORD;
begin
// ThreadContext должен быть выровнен, поэтому используем VirtualAlloc
// которая автоматически выделит память выровненную по началу страницы
// в противном случае получим ERROR_NOACCESS (998)
ThreadContext := VirtualAlloc(nil, SizeOf(TContext), MEM_COMMIT, PAGE_READWRITE);
try
ThreadContext^.ContextFlags := CONTEXT_FULL;
if not GetThreadContext(hThread, ThreadContext^) then
Exit;
ZeroMemory(@StackFrame, SizeOf(TStackFrame));
StackFrame.AddrPC.Mode := AddrModeFlat;
StackFrame.AddrStack.Mode := AddrModeFlat;
StackFrame.AddrFrame.Mode := AddrModeFlat;
StackFrame.AddrPC.Offset := ThreadContext.Eip;
StackFrame.AddrStack.Offset := ThreadContext.Esp;
StackFrame.AddrFrame.Offset := ThreadContext.Ebp;
MachineType := IMAGE_FILE_MACHINE_I386;
while True do
begin
if not StackWalk(MachineType, GetCurrentProcess, hThread, @StackFrame,
ThreadContext, nil, nil, nil, nil) then
Break;
if StackFrame.AddrPC.Offset <= 0 then Break;
Writeln('CallStack Frame Addr: ',
IntToHex(NativeUInt(StackFrame.AddrFrame.Offset), 1));
Writeln('CallStack Handler: ',
IntToHex(NativeUInt(StackFrame.AddrPC.Offset), 1));
Writeln('CallStack Stack: ',
IntToHex(NativeUInt(StackFrame.AddrStack.Offset), 1));
Writeln('CallStack Return: ',
IntToHex(NativeUInt(StackFrame.AddrReturn.Offset), 1));
end;
finally
VirtualFree(ThreadContext, SizeOf(TContext), MEM_FREE);
end;
end;
</pre>
<br />
Правда, в отличие от отладчика Delphi, он будет выводить данные о процедурах, для которых сгенерирован стековый фрейм, остальные он пропустит.<br />
За перечисление информации о стековых фреймах отвечает функция StackWalk (или StackWalk64).<br />
<br />
Теперь нюанс: если мы применим данный код к самому себе, то он сможет оттрассировать только один стековый фрейм, после чего произойдет выход (можете проверить на <a href="http://rouse.drkb.ru/blog/pmm2.zip" target="_blank">демоприложении</a>).<br />
<br />
Произойдет это по следующей причине: для правильной трассировки функции StackWalk необходимо указать параметры текущего кадра стека (EBP и ESP/ RBP и RSP для х64) и, собственно, текущий адрес кода (регистр EIP или RIP для х64). Если мы будем брать эти данные с самого себя, то это произойдет в тот момент, когда бы вызвали функцию GetThreadContext, а раскручивать стек мы начнем уже после выхода из данной функции, где все три параметра станут, мягко говоря, не валидны. По этой причине сделать трассировку самого себя вызовом данной функции не получится.<br />
Этот момент желательно учитывать...<br />
<br />
На получении информации о потоках 32-битного процесса под 64-битной ОС включая 32 и 64-битные варианты я остановлюсь несколько позже, а сейчас...<br />
<br />
<h3>
3. Собираем данные о кучах</h3>
<br />
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page3"></a>Само по себе Delphi приложение, как правило, кучи не использует, это больше прерогатива С++ приложений, но все-таки кучи присутствуют и здесь. Обычно их создают и используют различные сторонние библиотеки для своих нужд.<br />
<br />
Нюанс при получении данных о кучах в том, что элементов HeapEntry, из которых состоит каждая куча, может быть несколько тысяч, а второй нюанс в том, что функция Heap32Next при каждом вызове заново перестраивает весь список, создавая при этом достаточно чувствительную задержку (вплоть до десятков секунд).<br />
<br />
Об этой неприятной особенности <a href="http://alexander-bagel.blogspot.ru/2012/12/api.html" target="_blank">я уже писал</a>.<br />
Правда, в той статье код был достаточно примерный, просто чтобы продемонстрировать сам принцип, и нам он не подойдет, но вполне устроит более причесанный вариант данного кода:<br />
<br />
<pre class="brush:delphi">const
RTL_HEAP_BUSY = 1;
RTL_HEAP_SEGMENT = 2;
RTL_HEAP_SETTABLE_VALUE = $10;
RTL_HEAP_SETTABLE_FLAG1 = $20;
RTL_HEAP_SETTABLE_FLAG2 = $40;
RTL_HEAP_SETTABLE_FLAG3 = $80;
RTL_HEAP_SETTABLE_FLAGS = $E0;
RTL_HEAP_UNCOMMITTED_RANGE = $100;
RTL_HEAP_PROTECTED_ENTRY = $200;
RTL_HEAP_FIXED = (RTL_HEAP_BUSY or RTL_HEAP_SETTABLE_VALUE or
RTL_HEAP_SETTABLE_FLAG2 or RTL_HEAP_SETTABLE_FLAG3 or
RTL_HEAP_SETTABLE_FLAGS or RTL_HEAP_PROTECTED_ENTRY);
STATUS_SUCCESS = 0;
function CheckSmallBuff(Value: DWORD): Boolean;
const
STATUS_NO_MEMORY = $C0000017;
STATUS_BUFFER_TOO_SMALL = $C0000023;
begin
Result := (Value = STATUS_NO_MEMORY) or (Value = STATUS_BUFFER_TOO_SMALL);
end;
function FlagToStr(Value: DWORD): string;
begin
case Value of
LF32_FIXED: Result := 'LF32_FIXED';
LF32_FREE: Result := 'LF32_FREE';
LF32_MOVEABLE: Result := 'LF32_MOVEABLE';
else
Result := '';
end;
end;
var
I, A: Integer;
pDbgBuffer: PRtlDebugInformation;
pHeapInformation: PRtlHeapInformation;
pHeapEntry: PRtrHeapEntry;
dwAddr, dwLastSize: ULONG_PTR;
hit_seg_count: Integer;
BuffSize: NativeUInt;
begin
// Т.к. связка Heap32ListFirst, Heap32ListNext, Heap32First, Heap32Next
// работает достаточно медленно, из-за постоянного вызова
// RtlQueryProcessDebugInformation на каждой итерации, мы заменим ее вызов
// аналогичным кодом без ненужного дубляжа
// Создаем отладочный буфер
BuffSize := $400000;
pDbgBuffer := RtlCreateQueryDebugBuffer(BuffSize, False);
// Запрашиваем информацию по списку куч процесса
while CheckSmallBuff(RtlQueryProcessDebugInformation(GetCurrentProcessId,
RTL_QUERY_PROCESS_HEAP_SUMMARY or RTL_QUERY_PROCESS_HEAP_ENTRIES,
pDbgBuffer)) do
begin
// если размера буфера не хватает, увеличиваем...
RtlDestroyQueryDebugBuffer(pDbgBuffer);
BuffSize := BuffSize shl 1;
pDbgBuffer := RtlCreateQueryDebugBuffer(BuffSize, False);
end;
if pDbgBuffer <> nil then
try
// Запрашиваем информацию по списку куч процесса
if RtlQueryProcessDebugInformation(GetCurrentProcessId,
RTL_QUERY_PROCESS_HEAP_SUMMARY or RTL_QUERY_PROCESS_HEAP_ENTRIES,
pDbgBuffer) = STATUS_SUCCESS then
begin
// Получаем указатель на кучу по умолчанию
pHeapInformation := @pDbgBuffer^.Heaps^.Heaps[0];
// перечисляем все ее блоки...
for I := 0 to pDbgBuffer^.Heaps^.NumberOfHeaps - 1 do
begin
// начиная с самого первого
pHeapEntry := pHeapInformation^.Entries;
dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) +
pHeapInformation^.EntryOverhead;
dwLastSize := 0;
A := 0;
while A < Integer(pHeapInformation^.NumberOfEntries) do
try
hit_seg_count := 0;
while (pHeapEntry^.Flags and RTL_HEAP_SEGMENT) = RTL_HEAP_SEGMENT do
begin
// Если блок отмечен флагом RTL_HEAP_SEGMENT,
// то рассчитываем новый адрес на основе EntryOverhead
dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) +
pHeapInformation^.EntryOverhead;
Inc(pHeapEntry);
Inc(A);
Inc(hit_seg_count);
// проверка выхода за границы блоков
if A + hit_seg_count >=
Integer(pHeapInformation^.NumberOfEntries - 1) then
Continue;
end;
// Если блок не самый первый в сегменте, то текущий адрес блока равен,
// адресу предыдущего блока + размер предыдущего блока
if hit_seg_count = 0 then
Inc(dwAddr, dwLastSize);
// Выставляем флаги
if pHeapEntry^.Flags and RTL_HEAP_FIXED <> 0 then
pHeapEntry^.Flags := LF32_FIXED
else
if pHeapEntry^.Flags and RTL_HEAP_SETTABLE_FLAG1 <> 0 then
pHeapEntry^.Flags := LF32_MOVEABLE
else
if pHeapEntry^.Flags and RTL_HEAP_UNCOMMITTED_RANGE <> 0 then
pHeapEntry^.Flags := LF32_FREE;
if pHeapEntry^.Flags = 0 then
pHeapEntry^.Flags := LF32_FIXED;
// Выводим данные
Writeln('HeapID: ', I, ', entry addr: ', IntToHex(dwAddr, 8),
', size: ', IntToHex(pHeapEntry^.Size, 8), ' ', FlagToStr(pHeapEntry^.Flags));
// Запоминаем адрес последнего блока
dwLastSize := pHeapEntry^.Size;
// Переходим к следующему блоку
Inc(pHeapEntry);
finally
Inc(A);
end;
// Переходим к следующей куче
Inc(pHeapInformation);
end;
end;
finally
RtlDestroyQueryDebugBuffer(pDbgBuffer);
end;
Readln;
end.
</pre>
<br />
Вкратце, при помощи вызова функций RtlQueryProcessDebugInformation, RtlCreateQueryDebugBuffer и RtlQueryProcessDebugInformation создается буфер, в котором содержится информация о текущих кучах процесса. После чего, зная структуру данных, хранящихся в нем, получаем эти данные в цикле.<br />
pDbgBuffer^.Heaps - хранит в себе списки куч (аналог THeapList32), а сами записи хранятся в pDbgBuffer^.Heaps^.Heaps[N].Entries (аналог THeapEntry32).<br />
<br />
Данный код выведет следующую информацию:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNuu1bWBaJ7Ycdvb4JxEXuKa0G5vIIKt2vvABuQ6DZe6qfV9bfYI9Kqp0aR4f0R0op-mrDwrz9bNAaAzlxzeHj7NSfRA34LWnMKfepC2vH9XbBBJAQjVujyjl5-ZZsbtC_nuUaFRW_-x8/s1600/8.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="210" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNuu1bWBaJ7Ycdvb4JxEXuKa0G5vIIKt2vvABuQ6DZe6qfV9bfYI9Kqp0aR4f0R0op-mrDwrz9bNAaAzlxzeHj7NSfRA34LWnMKfepC2vH9XbBBJAQjVujyjl5-ZZsbtC_nuUaFRW_-x8/s640/8.png" width="640" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">Сразу сверяемся с VMMap - выглядит похоже</td></tr>
</tbody></table>
<br />
В принципе, кучи я использую при отладке достаточно редко, но иногда и эта информация может пригодиться.<br />
<br />
<h3>
4. Собираем данные о загруженных PE файлах</h3>
<br />
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page4"></a>Теперь пришла пора получить информацию о загруженных в адресное пространство процесса исполняемым файлах и библиотеках. Есть несколько способов сделать это (например, проанализировав PEB.LoaderData), но поступим проще.<br />
<br />
Как правило, под PE файл выделяется отдельный регион (ну, по крайней мере, я еще не встречался с таким, чтобы PE образ был загружен без выравнивания по верхушке региона), поэтому, взяв за основу код из первой главы и проверив данные первой страницы региона на соответствие PE файлу, получим список всех загруженных библиотек и исполняемых файлов.<br />
<br />
Следующий код детектирует наличие валидного PE файла по указанному адресу:<br />
<br />
<pre class="brush:delphi">function CheckPEImage(hProcess: THandle;
ImageBase: Pointer; var IsPEImage64: Boolean): Boolean;
var
ReturnLength: NativeUInt;
IDH: TImageDosHeader;
NT: TImageNtHeaders;
begin
Result := False;
IsPEImage64 := False;
if not ReadProcessMemory(hProcess, ImageBase,
@IDH, SizeOf(TImageDosHeader), ReturnLength) then Exit;
if IDH.e_magic <> IMAGE_DOS_SIGNATURE then Exit;
ImageBase := Pointer(NativeInt(ImageBase) + IDH._lfanew);
if not ReadProcessMemory(hProcess, ImageBase,
@NT, SizeOf(TImageNtHeaders), ReturnLength) then Exit;
Result := NT.Signature = IMAGE_NT_SIGNATURE;
IsPEImage64 :=
(NT.FileHeader.Machine = IMAGE_FILE_MACHINE_IA64) or
(NT.FileHeader.Machine = IMAGE_FILE_MACHINE_ALPHA64) or
(NT.FileHeader.Machine = IMAGE_FILE_MACHINE_AMD64);
end;
</pre>
<br />
Ну точнее как, он просто проверяет наличие ImageDosHeader и ImageNTHeader, ориентируясь на их сигнатуры. В принципе для 99% случаев этого достаточно.<br />
<br />
Третий параметр просто информационный, он показывает является ли PE файл 64-битным.<br />
<br />
Получить путь к загруженному файлу можно вызовом функции GetMappedFileName:<br />
<br />
<pre class="brush:delphi">function GetFileAtAddr(hProcess: THandle; ImageBase: Pointer): string;
begin
SetLength(Result, MAX_PATH);
SetLength(Result,
GetMappedFileName(hProcess, ImageBase, @Result[1], MAX_PATH));
end;
</pre>
<br />
А теперь попробуем посмотреть, что у нас загружается в обычное консольное приложение:<br />
<br />
<pre class="brush:delphi">var
MBI: TMemoryBasicInformation;
dwLength: NativeUInt;
Address: PByte;
IsPEImage64: Boolean;
begin
Address := nil;
dwLength := SizeOf(TMemoryBasicInformation);
while VirtualQuery(Address, MBI, dwLength) <> 0 do
begin
if CheckPEImage(GetCurrentProcess, MBI.BaseAddress, IsPEImage64) then
begin
Write(IntToHex(NativeUInt(MBI.BaseAddress), 8), ': ',
GetFileAtAddr(GetCurrentProcess, MBI.BaseAddress));
if IsPEImage64 then
Writeln(' (x64)')
else
Writeln(' (x32)');
end;
Inc(Address, MBI.RegionSize);
end;
Readln;
end.
</pre>
<br />
Получится вот такая картинка:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiay_PHJdLli17JlJENLXJIfyaZYUGqhFHXNOZ4sNfZKv_FkdmEWUfgS8nstO5fMOAhisLZq8igmnouiMf14aJmDCbkmNMnLb3gIIkJu7U6cXa03VSp2uKtMJSlRO9043uIvXymjhlM2Gs/s1600/9.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiay_PHJdLli17JlJENLXJIfyaZYUGqhFHXNOZ4sNfZKv_FkdmEWUfgS8nstO5fMOAhisLZq8igmnouiMf14aJmDCbkmNMnLb3gIIkJu7U6cXa03VSp2uKtMJSlRO9043uIvXymjhlM2Gs/s1600/9.png" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">64-битная библиотека в 32-битном приложении? Да проще простого :)</td></tr>
</tbody></table>
<br />
Приложение у меня 32-битное, операционная система Windows 7 x64. Судя по тому что отображено на картинке, в нашем 32-битном процессе спокойно живут и работают четыре 64-битных библиотеки, впрочем, тут ничего не обычного - это так называемый Wow64 (<a href="http://modernlib.ru/books/russinovich_mark/1vnutrennee_ustroystvo_windows_gl_14/read_14/" target="_blank">эмуляция Win32 в 64-разрядной Windows</a>).<br />
<br />
Зато сразу становится понятно, откуда появляются 64-битные аналоги 32-битных потоков и куч.<br />
<br />
Теперь, по-хорошему, нужно получить адреса секций каждого PE файла, чтобы можно было их показать более наглядно. Все секции выравниваются по адресу начала страницы и не пересекаются друг с другом.<br />
<br />
Сделаем это вот таким кодом:<br />
<br />
<pre class="brush:delphi">procedure GetInfoFromImage(const FileName: string; ImageBase: Pointer);
var
ImageInfo: TLoadedImage;
ImageSectionHeader: PImageSectionHeader;
I: Integer;
begin
if MapAndLoad(PAnsiChar(AnsiString(FileName)), nil, @ImageInfo, True, True) then
try
ImageSectionHeader := ImageInfo.Sections;
for I := 0 to Integer(ImageInfo.NumberOfSections) - 1 do
begin
Write(
IntToHex((NativeUInt(ImageBase) + ImageSectionHeader^.VirtualAddress), 8), ': ',
string(PAnsiChar(@ImageSectionHeader^.Name[0])));
if IsExecute(ImageSectionHeader^.Characteristics) then
Write(' Execute');
if IsWrite(ImageSectionHeader^.Characteristics) then
Write(' Writable');
Writeln;
Inc(ImageSectionHeader);
end;
finally
UnMapAndLoad(@ImageInfo);
end;
Writeln;
end;
</pre>
<br />
Здесь используется вызов функции MapAndLoad, которая, помимо загрузки файла и проверки его заголовков, производит также выравнивание секций посредством вызова NtMapViewOfSection.<br />
<br />
Для своего собственного процесса, конечно, вызов данной функции избыточен, т.к. требуемый PE файл и так уже подгружен в адресное пространство процесса, но т.к. нам потребуется более универсальный код для работы и с другими процессами, то воспользуемся именно этим подходом.<br />
<br />
MapAndLoad хороша еще и тем, что позволяет 64-битным процессам подгружать 32-битные PE файлы (правда, это не работает для 32-битных процессов), и в дальнейшем эта возможность нам еще пригодится.<br />
<br />
Суть кода такова: после выполнения MapAndLoad у нас будет на руках заполненная структура TLoadedImage, параметр Sections которой указывает на массив из структур TImageSectionHeader. У каждой из этих структур есть поле VirtualAddress, которое является смещением от адреса загрузки библиотеки. Сложив значение этого поля с hInstance библиотеки, мы получим адрес секции.<br />
<br />
Функции IsExecute и IsWrite проверяют характеристики секции и возвращают True в том случае, если секция содержит исполняемый код (IsExecute) или данные, доступные для модификации (IsWrite). Выглядят они следующим образом:<br />
<br />
<pre class="brush:delphi">function IsExecute(const Value: DWORD): Boolean;
begin
Result := False;
if (Value and IMAGE_SCN_CNT_CODE) =
IMAGE_SCN_CNT_CODE then Result := True;
if (Value and IMAGE_SCN_MEM_EXECUTE) =
IMAGE_SCN_MEM_EXECUTE then Result := True;
end;
function IsWrite(const Value: DWORD): Boolean;
begin
Result := False;
if (Value and IMAGE_SCN_CNT_UNINITIALIZED_DATA) =
IMAGE_SCN_CNT_UNINITIALIZED_DATA then Result := True;
if (Value and IMAGE_SCN_MEM_WRITE) = IMAGE_SCN_MEM_WRITE then
Result := True;
end;
</pre>
<br />
В результате работы данного кода мы увидим следующее:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjyp0Gk3po1CfLR10KEWNp8_Bvy9ajM5xSHpk3v3hWq55zndP3w62iOo8Co0T2-dCik6ZePJ0VnIRJ3r1VDsl_d_As4E9FyPzxXAg5SMW0PS-03GrhXDP5IVTlD4bia7Bt2riHjCyQx07I/s1600/10.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjyp0Gk3po1CfLR10KEWNp8_Bvy9ajM5xSHpk3v3hWq55zndP3w62iOo8Co0T2-dCik6ZePJ0VnIRJ3r1VDsl_d_As4E9FyPzxXAg5SMW0PS-03GrhXDP5IVTlD4bia7Bt2riHjCyQx07I/s1600/10.png" /></a></td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px;">Вывод загруженных библиотек и адресов их секций</td></tr>
</tbody></table>
<br />
Правда, с этим кодом есть еще один небольшой нюанс.<br />
Как видно было на предыдущей картинке, функция GetMappedFileName возвращает путь к загруженному файлу в следующем виде: "\Device\HarddiskVolume2\Windows\System32\wow64cpu.dll", а функция MapAndLoad требует нормализированного пути вида "C:\Windows\System32\wow64cpu.dll".<br />
<br />
За приведение пути к привычному виду отвечает следующий код:<br />
<br />
<pre class="brush:delphi">function NormalizePath(const Value: string): string;
const
OBJ_CASE_INSENSITIVE = $00000040;
STATUS_SUCCESS = 0;
FILE_SYNCHRONOUS_IO_NONALERT = $00000020;
FILE_READ_DATA = 1;
ObjectNameInformation = 1;
DriveNameSize = 4;
VolumeCount = 26;
DriveTotalSize = DriveNameSize * VolumeCount;
var
US: UNICODE_STRING;
OA: OBJECT_ATTRIBUTES;
IO: IO_STATUS_BLOCK;
hFile: THandle;
NTSTAT, dwReturn: DWORD;
ObjectNameInfo: TOBJECT_NAME_INFORMATION;
Buff, Volume: string;
I, Count, dwQueryLength: Integer;
lpQuery: array [0..MAX_PATH - 1] of Char;
AnsiResult: AnsiString;
begin
Result := Value;
// Подготавливаем параметры для вызова ZwOpenFile
RtlInitUnicodeString(@US, StringToOleStr(Value));
// Аналог макроса InitializeObjectAttributes
FillChar(OA, SizeOf(OBJECT_ATTRIBUTES), #0);
OA.Length := SizeOf(OBJECT_ATTRIBUTES);
OA.ObjectName := @US;
OA.Attributes := OBJ_CASE_INSENSITIVE;
// Функция ZwOpenFile спокойно открывает файлы, путь к которым представлен
// с использованием символьных ссылок, например:
// \SystemRoot\System32\ntdll.dll
// \??\C:\Windows\System32\ntdll.dll
// \Device\HarddiskVolume1\WINDOWS\system32\ntdll.dll
// Поэтому будем использовать ее для получения хендла
NTSTAT := ZwOpenFile(@hFile, FILE_READ_DATA or SYNCHRONIZE, @OA, @IO,
FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
FILE_SYNCHRONOUS_IO_NONALERT);
if NTSTAT = STATUS_SUCCESS then
try
// Файл открыт, теперь смотрим его формализованный путь
NTSTAT := NtQueryObject(hFile, ObjectNameInformation,
@ObjectNameInfo, MAX_PATH * 2, @dwReturn);
if NTSTAT = STATUS_SUCCESS then
begin
SetLength(AnsiResult, MAX_PATH);
WideCharToMultiByte(CP_ACP, 0,
@ObjectNameInfo.Name.Buffer[ObjectNameInfo.Name.MaximumLength -
ObjectNameInfo.Name.Length {$IFDEF WIN64} + 4{$ENDIF}],
ObjectNameInfo.Name.Length, @AnsiResult[1],
MAX_PATH, nil, nil);
Result := string(PAnsiChar(AnsiResult));
// Путь на открытый через ZwOpenFile файл
// возвращается в виде \Device\HarddiskVolumeХ\бла-бла
// Осталось только его сопоставить с реальным диском
SetLength(Buff, DriveTotalSize);
Count := GetLogicalDriveStrings(DriveTotalSize, @Buff[1]) div DriveNameSize;
for I := 0 to Count - 1 do
begin
Volume := PChar(@Buff[(I * DriveNameSize) + 1]);
Volume[3] := #0;
// Преобразуем имя каждого диска в символьную ссылку и
// сравниваем с формализированным путем
QueryDosDevice(PChar(Volume), @lpQuery[0], MAX_PATH);
dwQueryLength := Length(string(lpQuery));
if Copy(Result, 1, dwQueryLength) = string(lpQuery) then
begin
Volume[3] := '\';
if lpQuery[dwQueryLength - 1] <> '\' then
Inc(dwQueryLength);
Delete(Result, 1, dwQueryLength);
Result := Volume + Result;
Break;
end;
end;
end;
finally
ZwClose(hFile);
end;
end;
</pre>
<br />
Это уже достаточно старый код, постоянно применяемый мной для приведения к нормальному пути. Суть его в том чтобы из путей следующих видов:<br />
<ul>
<li>\SystemRoot\System32\ntdll.dll</li>
<li>\??\C:\Windows\System32\ntdll.dll</li>
<li>\Device\HarddiskVolume1\WINDOWS\system32\ntdll.dll</li>
</ul>
<br />
<br />
... получить фиксированный "\Device\HarddiskVolume1\WINDOWS\system32\ntdll.dll".<br />
<div style="margin: 0px;">
Это делается посредством вызова ZwOpenFile + NtQueryObject, после чего просто перебираются все диски в системе и для каждого вызывается QueryDosDevice, который возвращает путь в таком же формате. После чего пути сравниваются и (при совпадении) к переданному пути подставляется соответствующая метка диска.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Но это лирика.</div>
<div style="margin: 0px;">
Чтобы быть полностью довольными собой, желательно вывести так же директории PE файла, чтобы было понятно сразу, где искать, к примеру, таблицу импорта, где сидит UNWIND и т.п.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Это делается довольно простым кодом:</div>
<br />
<pre class="brush:delphi">procedure EnumDirectoryes(ImageBase: Pointer; ImageInfo: TLoadedImage;
AddrStart, AddrEnd: NativeUInt);
const
DirectoryStr: array [0..14] of string =
('export', 'import', 'resource', 'exception',
'security', 'basereloc', 'debug', 'copyright',
'globalptr', 'tls', 'load_config', 'bound_import',
'iat', 'delay_import', 'com');
var
I: Integer;
dwDirSize: DWORD;
DirAddr: Pointer;
ReadlDirAddr: NativeUInt;
begin
for I := 0 to 14 do
begin
DirAddr := ImageDirectoryEntryToData(ImageInfo.MappedAddress,
True, I, dwDirSize);
if DirAddr = nil then Continue;
ReadlDirAddr := NativeUint(ImageBase) +
NativeUint(DirAddr) - NativeUint(ImageInfo.MappedAddress);
if (ReadlDirAddr >= AddrStart) and (ReadlDirAddr < AddrEnd) then
Writeln(
IntToHex(ReadlDirAddr, 8), ': directory "', DirectoryStr[I], '"');
end;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Имея на руках структуру TLoadedImage, мы можем достаточно просто вызовом функции ImageDirectoryEntryToData получить её адрес, правда, он будет привязан к адресу, по которому отображен PE файл. Чтобы перевести его в реальный, нужно из текущего адреса вычесть адрес, по которому отображен образ, получив таким образом смещение от начала файла, и уже его сложить с ImageBase библиотеки.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
В итоге получится вот такая картинка:</div>
<div style="margin: 0px;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvCcBiESmHpWcDQ5yrartEx7nx1nadhya4LSVvRDm1zV8V3TkIG5quIUWMzXjr4XXQGPhTWwS-mwRdCqQEtlvQP2aAo2RhJUNYN-U4EPs5EWbAGmJYGNH_M1Qa5wEzQb86ThEy8y18N98/s1600/11.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvCcBiESmHpWcDQ5yrartEx7nx1nadhya4LSVvRDm1zV8V3TkIG5quIUWMzXjr4XXQGPhTWwS-mwRdCqQEtlvQP2aAo2RhJUNYN-U4EPs5EWbAGmJYGNH_M1Qa5wEzQb86ThEy8y18N98/s1600/11.png" style="cursor: move;" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 12.727272033691406px; padding-top: 4px;"><div style="margin: 0px;">
Вывод директорий и секций PE файла</div>
</td></tr>
</tbody></table>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Сразу видно, что, к примеру, в секции ".text" библиотеки msctf.dll расположены директории импорта/экспорта/отложенного импорта и т.п.</div>
<div style="margin: 0px;">
Директория с ресурсами сидит в секции ".rsrc", да и релоки тоже там где положено, однако выпадает из схемы директория "bound_import".</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Да, действительно, данная директория не располагается непосредственно ни в одной из секций библиотеки, такова ее особенность. Она обычно идет сразу за PE заголовком (хотя иногда может встречаться и в промежутках между секциями). Данная директория служит для обеспечения механизма "привязанного импорта", который встречается в основном только у программ и библиотек, идущих в составе ОС.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Суть её в том, что все адреса импортируемых функций зашиваются в исполняемый файл еще на этапе компиляции, таким образом не нужно выполнять излишних телодвижений, бегая по таблице обычного импорта в поисках адреса функции.</div>
<div style="margin: 0px;">
Но и накладные расходы тоже соответствующие, ибо как только изменится любая из библиотек, заявленная в секции привязанного импорта, приложение должно быть перекомпилировано.</div>
<div style="margin: 0px;">
<br /></div>
<h3>
5. Блок окружения процесса (PEB) + KUSER_SHARED_DATA</h3>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page5"></a>Имея на руках данные о потоках, кучах и исполняемых файлах, уже прямо сейчас можно сделать небольшую утилиту, которая выведет информацию в удобочитаемом виде, но что можно еще добавить?</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Как минимум, крайне желательно получать и выводить информацию из блока окружения процесса.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Доступ к нему можно получить вызовом функции NtQueryInformationProcess с флагом ProcessBasicInformation (константа равная нулю). В этом случае на руках будет структура PROCESS_BASIC_INFORMATION, у которой поле PebBaseAddress и будет содержать адрес <abbr title="Process Environment Block">PEB</abbr>.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Но это будет актуально, только если битности процессов (запрашивающего и о котором запрашиваем информацию) совпадут. Если мы вызовем данную функцию из 64-битного приложения применительно к 32-битному, то получим адрес 64-битного <abbr title="Process Environment Block">PEB</abbr>, а не родного 32-битного.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Для того, чтобы из 64-битного приложения получить доступ к Wow64PEB (назовем его так), необходимо вызвать функцию NtQueryInformationProcess с параметром ProcessWow64Information (константа равная 26) и размером буфера равным SizeOf(ULONG_PTR). В этом случае вместо структуры PROCESS_BASIC_INFORMATION функция вернет указатель на 32-битный <abbr title="Process Environment Block">PEB</abbr>, из которого и будем зачитывать нужную нам информацию посредством ReadProcessMemory.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Что такое <abbr title="Process Environment Block">PEB</abbr>?</div>
<div style="margin: 0px;">
Грубо говоря, это не сильно документированная структура, в большинстве своем предназначенная для хранения данных, используемых непосредственно системой. Но это не означает, что она не интересна разработчику обычного прикладного приложения. В частности данная структура содержит ряд интересных полей, таких как: флаг BeingDebugged, указывающий подключен ли к процессу отладчик; указатель на PEB_LDR_DATA, в которой содержится информация о загруженных в процесс модулях; и много остальной достаточно полезной для программиста информации, особенно для того, кто знает как ее применить в своих целях :)</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Выглядит данная структура примерно вот так (декларация для Windows7 x86/64):</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;"> PPEB = ^TPEB;
TPEB = record
InheritedAddressSpace: BOOLEAN;
ReadImageFileExecOptions: BOOLEAN;
BeingDebugged: BOOLEAN;
BitField: BOOLEAN;
{
BOOLEAN ImageUsesLargePages : 1;
BOOLEAN IsProtectedProcess : 1;
BOOLEAN IsLegacyProcess : 1;
BOOLEAN IsImageDynamicallyRelocated : 1;
BOOLEAN SkipPatchingUser32Forwarders : 1;
BOOLEAN IsPackagedProcess : 1;
BOOLEAN IsAppContainer : 1;
BOOLEAN SpareBits : 1;
}
Mutant: HANDLE;
ImageBaseAddress: PVOID;
LoaderData: PVOID;
ProcessParameters: PRTL_USER_PROCESS_PARAMETERS;
SubSystemData: PVOID;
ProcessHeap: PVOID;
FastPebLock: PRTLCriticalSection;
AtlThunkSListPtr: PVOID;
IFEOKey: PVOID;
EnvironmentUpdateCount: ULONG;
UserSharedInfoPtr: PVOID;
SystemReserved: ULONG;
AtlThunkSListPtr32: ULONG;
ApiSetMap: PVOID;
TlsExpansionCounter: ULONG;
TlsBitmap: PVOID;
TlsBitmapBits: array[0..1] of ULONG;
ReadOnlySharedMemoryBase: PVOID;
HotpatchInformation: PVOID;
ReadOnlyStaticServerData: PPVOID;
AnsiCodePageData: PVOID;
OemCodePageData: PVOID;
UnicodeCaseTableData: PVOID;
KeNumberOfProcessors: ULONG;
NtGlobalFlag: ULONG;
CriticalSectionTimeout: LARGE_INTEGER;
HeapSegmentReserve: SIZE_T;
HeapSegmentCommit: SIZE_T;
HeapDeCommitTotalFreeThreshold: SIZE_T;
HeapDeCommitFreeBlockThreshold: SIZE_T;
NumberOfHeaps: ULONG;
MaximumNumberOfHeaps: ULONG;
ProcessHeaps: PPVOID;
GdiSharedHandleTable: PVOID;
ProcessStarterHelper: PVOID;
GdiDCAttributeList: ULONG;
LoaderLock: PRTLCriticalSection;
NtMajorVersion: ULONG;
NtMinorVersion: ULONG;
NtBuildNumber: USHORT;
NtCSDVersion: USHORT;
PlatformId: ULONG;
Subsystem: ULONG;
MajorSubsystemVersion: ULONG;
MinorSubsystemVersion: ULONG;
AffinityMask: ULONG_PTR;
{$IFDEF WIN32}
GdiHandleBuffer: array [0..33] of ULONG;
{$ELSE}
GdiHandleBuffer: array [0..59] of ULONG;
{$ENDIF}
PostProcessInitRoutine: PVOID;
TlsExpansionBitmap: PVOID;
TlsExpansionBitmapBits: array [0..31] of ULONG;
SessionId: ULONG;
AppCompatFlags: ULARGE_INTEGER;
AppCompatFlagsUser: ULARGE_INTEGER;
pShimData: PVOID;
AppCompatInfo: PVOID;
CSDVersion: UNICODE_STRING;
ActivationContextData: PVOID;
ProcessAssemblyStorageMap: PVOID;
SystemDefaultActivationContextData: PVOID;
SystemAssemblyStorageMap: PVOID;
MinimumStackCommit: SIZE_T;
FlsCallback: PPVOID;
FlsListHead: LIST_ENTRY;
FlsBitmap: PVOID;
FlsBitmapBits: array [1..FLS_MAXIMUM_AVAILABLE div SizeOf(ULONG) * 8] of ULONG;
FlsHighIndex: ULONG;
WerRegistrationData: PVOID;
WerShipAssertPtr: PVOID;
pContextData: PVOID;
pImageHeaderHash: PVOID;
TracingFlags: ULONG;
{
ULONG HeapTracingEnabled : 1;
ULONG CritSecTracingEnabled : 1;
ULONG LibLoaderTracingEnabled : 1;
ULONG SpareTracingBits : 29;
}
CsrServerReadOnlySharedMemoryBase: ULONGLONG;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Кстати, сравните эту структуру с той, <a href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx" target="_blank">что официально доступна в MSDN</a>.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Для Window 2000/XP/2003 будут небольшие изменения, но не сильно критичные.</div>
<div style="margin: 0px;">
Расписывать каждое поле я не буду, те кто работают с <abbr title="Process Environment Block">PEB</abbr> и так знают? что именно им нужно, но на некоторых полях я заострю ваше внимание.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Итак:</div>
<div style="margin: 0px;">
<br /></div>
<ul>
<li>Поле BeingDebugged - <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-3.html" target="_blank">в третьей части статьи об отладчике</a> я показывал один из вариантов обхода детектирования оного посредством патча памяти приложения. Суть подхода заключалась как раз в определении адреса <abbr title="Process Environment Block">PEB</abbr> и изменения значения параметра BeingDebugged на ноль, после чего функция IsDebuggerPresent, ориентирующаяся на данное поле, начинала возвращать False, говоря о том? что отладчика она не обнаружила.</li>
<li>Поле ImageBaseAddress - указывает на hInstance приложения (оно может не совпадать с полем ImageBase в PE заголовке).</li>
<li>LoaderData - указатель на данные о загруженных модулях, в нем хранится достаточно полезная информация для тех, кто строит защиту приложения самостоятельно, но, к сожалению, пока что это выходит за рамки данной статьи. На этом поле я остановлюсь чуть подробнее, когда увидит свет статья о детектировании инжекта в ваше приложение :)</li>
<li>ProcessParameters - откуда берут информацию ParamStr/GetCurrentDir и т.п. функции? Именно отсюда. Здесь же сидит адрес переменных окружения.</li>
<li>А еще мы можем узнать сервиспак системы, не дергая реестр, в этом нам поможет поле CSDVersion. Да, впрочем, поля NtMajorVersion/NtMinorVersion/NtBuildNumber говорят сами за себя.</li>
</ul>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Ну и так далее - продолжать можно долго.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Большинство данных полей занимают свои страницы в адресном пространстве процесса. К примеру, ProcessParameters обычно сидит в одной из куч, созданных загрузчиком, переменные окружения расположены тоже где-то в том районе.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Если мы хотим визуализировать все это (а к этому я и веду), эти данные мы должны иметь на руках, чтобы было что отобразить в финальном приложении.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Согласитесь, гораздо приятней иметь на руках вместо некоего блока бинарных данных что-то в виде такого:</div>
<div style="margin: 0px;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtd6Thz7uTBnn1GjL125q-EAVrAiZxv77jFRPL_IGxT9ig4uOhjkNDhaFIziS2FmcHUoL2tdPOipZOGpsw9vzFs85MAOUTmMkjmjhCLyLASjBzeYkQZCgOlTw67U-QhsGHCT49Ja6_Fuc/s1600/12.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="384" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtd6Thz7uTBnn1GjL125q-EAVrAiZxv77jFRPL_IGxT9ig4uOhjkNDhaFIziS2FmcHUoL2tdPOipZOGpsw9vzFs85MAOUTmMkjmjhCLyLASjBzeYkQZCgOlTw67U-QhsGHCT49Ja6_Fuc/s640/12.png" style="cursor: move;" width="640" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 13px; padding-top: 4px;"><div style="margin: 0px;">
Блок окружения 32-битного процесса</div>
</td></tr>
</tbody></table>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
А ведь есть еще и KUSER_SHARED_DATA.</div>
<div style="margin: 0px;">
Это тоже структура используемая системой, и вы постоянно встречаетесь с ней, вызывая тот же GetTickCount или IsProcessorFeaturePresent.</div>
<div style="margin: 0px;">
К примеру, NtSystemRoot сидит именно в ней, да и, опять же, зачем все перечислять, проще увидеть:</div>
<div style="margin: 0px;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy96XXQVAsAaA1Z5wAJpn0ZtySAAWlvNc9Pdr9ID5P9vfdRiAWFqlijCg9zOQbEZWHz2RKveDXAgigNw4aYqw7CwGaEhgjq8_L9luAWiZfdQAPUnr0BInxP2QMqrQfBqTuvIwXlmuPYWE/s1600/13.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="640" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy96XXQVAsAaA1Z5wAJpn0ZtySAAWlvNc9Pdr9ID5P9vfdRiAWFqlijCg9zOQbEZWHz2RKveDXAgigNw4aYqw7CwGaEhgjq8_L9luAWiZfdQAPUnr0BInxP2QMqrQfBqTuvIwXlmuPYWE/s640/13.png" style="cursor: move;" width="506" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 13px; padding-top: 4px;"><div style="margin: 0px;">
KUSER_SHARED_DATA как она и есть :)</div>
</td></tr>
</tbody></table>
<ul>
<li>Хотите узнать, что за процесс активен без вызова GetForegroundWindow – читайте ConsoleSessionForegroundProcessId.</li>
<li>Вам пытаются подсунуть левую версию Win, чтобы отключилась часть системы защиты, не рассчитанная на предыдущие OS? Читайте актуальные значения из полей NtMajorVersion/NtMinorVersion...</li>
</ul>
<ul></ul>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Впрочем, пожалуй, здесь мы пока и остановимся...</div>
<div style="margin: 0px;">
<br /></div>
<h3>
6. TRegionData</h3>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page6"></a>На этом теоретическая часть закончилась и пришла пора применить это все на практике.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Прежде всего нужно определиться с тем, каким образом хранить информацию о регионах. Готовясь к статье, я написал набор классов, выделенных в общее пространство имен "MemoryMap", их вы сможете найти в составе <a href="http://rouse.drkb.ru/blog/pmm2.zip" target="_blank">демо-примеров</a>.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<span style="color: red;"><b>ВАЖНО!!!</b></span></div>
<div style="margin: 0px;">
<span style="color: red;">Данный набор классов разработан с учетом нововведений, присутствующих в Delphi XE4, под более старыми версиями Delphi его работоспособность не проверялась и не гарантируется.</span></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Информацию по каждому региону будет хранить класс TRegionData, реализованный в модуле "MemoryMap.RegionData.pas".</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Выглядит он примерно следующим образом (в процессе развития проекта декларация класса может меняться).</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;"> TRegionData = class
private
FParent: TRegionData;
FRegionType: TRegionType;
FMBI: TMemoryBasicInformation;
FDetails: string;
FRegionVisible: Boolean;
FHiddenRegionCount: Integer;
FTotalRegionSize: NativeUInt;
FHeap: THeapData;
FThread: TThreadData;
FPEBData: TSystemData;
FSection: TSection;
FContains: TList;
FDirectories: TList;
FShared: Boolean;
FSharedCount: Integer;
FFiltered: Boolean;
protected
...
public
constructor Create;
destructor Destroy; override;
property RegionType: TRegionType read FRegionType;
property MBI: TMemoryBasicInformation read FMBI;
property Details: string read FDetails;
property RegionVisible: Boolean read FRegionVisible;
property HiddenRegionCount: Integer read FHiddenRegionCount;
property Parent: TRegionData read FParent;
property TotalRegionSize: NativeUInt read FTotalRegionSize;
property Heap: THeapData read FHeap;
property Thread: TThreadData read FThread;
property SystemData: TSystemData read FPEBData;
property Section: TSection read FSection;
property Directory: TList read FDirectories;
property Contains: TList read FContains;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
По порядку:</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Каждый регион, как правило, хранит в себе данные одного типа.</div>
<div style="margin: 0px;">
Т.е. для куч, стеков потоков, PE файлов, выделяется свой собственный регион страниц.</div>
<div style="margin: 0px;">
За хранение типа региона отвечает свойство RegionType. Это перечислимый тип, объявленный следующим образом:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;"> // Тип региона
TRegionType = (
rtDefault,
rtHeap, // регион содержит элементы кучи
rtThread, // регион содержит стек потока или TEB
rtSystem, // регион содержит системные данные (PEB/KUSER_SHARED_DATA и т.п.)
rtExecutableImage // регион содержит образ исполняемого PE файла
);
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Параметры региона, полученные при помощи вызова VirtualQueryEx хранятся в поле MBI.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Краткое описание региона хранится в Details. В нем можно хранить все что угодно, к примеру путь к отображенному PE файлу, если таковой присутствует, строковое описание ID потока и т.п..</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Следующие три параметра используются для организации древовидной структуры.</div>
<div style="margin: 0px;">
Один из регионов является корневым узлом (рутом), остальные дочерние.</div>
<div style="margin: 0px;">
Флаг RegionVisible указывает на то,является ли регион корневым узлом.</div>
<div style="margin: 0px;">
Свойство HiddenRegionCount содержит в себе количество подрегионов (AllocationBase которых равен BaseAddress рута).</div>
<div style="margin: 0px;">
Ну а параметр Parent хранит ссылку на рута.</div>
<div style="margin: 0px;">
Сделано не совсем оптимально, можно было бы организовать и классическое дерево, но на текущий момент банально нет времени переделывать, может быть, когда-нибудь потом :)</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
TotalRegionSize содержит в себе общий размер всех подрегионов, включая рутовый.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
В случае, если регион содержит кучу, данные о первом ее элементе помещаются в параметр Heap, представляющий из себя следующую структуру:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;"> THeapEntry = record
Address: ULONG_PTR;
Size: SIZE_T;
Flags: ULONG;
end;
THeapData = record
ID: DWORD;
Wow64: Boolean;
Entry: THeapEntry;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Остальные элементы кучи, расположенные в рамках региона, размещаются в поле Contains.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Вообще поле Contains может содержать данные многих типов.</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;"> TContainItemType = (itHeapBlock, itThreadData,
itStackFrame, itSEHFrame, itSystem);
TContainItem = record
ItemType: TContainItemType;
function Hash: string;
case Integer of
0: (Heap: THeapData);
1: (ThreadData: TThreadData);
2: (StackFrame: TThreadStackEntry);
3: (SEH: TSEHEntry);
4: (System: TSystemData);
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Далее идет поле Thread, в нем хранится информация о потоке, который использует регион для хранения собственных данных.</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">type
TThreadInfo = (tiNoData, tiExceptionList, tiStackBase,
tiStackLimit, tiTEB, tiThreadProc);
type
TThreadData = record
Flag: TThreadInfo;
ThreadID: Integer;
Address: Pointer;
Wow64: Boolean;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Если данных о потоке в пределах региона много (например список <abbr title="Structured Exception Handling">SEH</abbr> фреймов или CallStack потока), они также помещаются в поле Contains.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Данные из системных структур (поля структур <abbr title="Process Environment Block">PEB</abbr>/<abbr title="Thread Environment Block">TEB</abbr> и т.п.) помещаются в поле SystemData, представляющее из себя запись из адреса данных и их описания.</div>
<div style="margin: 0px;">
Также эти данные могут быть помещены в поле Contains.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Если регион принадлежит одной из секций PE файла, данные о секции размещаются в параметре Section. Ну а список директорий файла размещается в поле Directory.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Вот как-то так вкратце. Теперь для представления данных о карте памяти процесса нам необходимо получить список регионов, создать для каждого из них экземпляр класса TRegionData и инициализировать поля созданного объекта требуемой информацией.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
За это отвечает класс TMemoryMap...</div>
<div style="margin: 0px;">
<br /></div>
<h3>
7. TMemoryMap</h3>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page7"></a>Данный класс реализован в модуле "MemoryMap.Core.pas".</div>
<div style="margin: 0px;">
Задача его сводится буквально к трем основным этапам:</div>
<ol>
<li>Получению списка всех выделенных регионов в памяти указанного приложения, данных по нитям/кучам/загруженным образам и т.п..</li>
<li>Созданию списка TRegionData и заполнению его полей полученной информацией.</li>
<li>Сохранение/загрузка данных, фильтрация данных.</li>
</ol>
<div style="margin: 0px;">
На практике все выглядит несколько сложнее.</div>
<div style="margin: 0px;">
Основная процедура сбора информации выглядит вот так:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">function TMemoryMap.InitFromProcess(PID: Cardinal;
const ProcessName: string): Boolean;
var
ProcessLock: TProcessLockHandleList;
begin
Result := False;
FRegions.Clear;
FModules.Clear;
FFilter := fiNone;
ProcessLock := nil;
// Открываем процесс на чтение
FProcess := OpenProcess(
PROCESS_QUERY_INFORMATION or PROCESS_VM_READ,
False, PID);
if FProcess = 0 then
RaiseLastOSError;
try
FPID := PID;
FProcessName := ProcessName;
// определяем битность процесса
FProcess64 := False;
{$IFDEF WIN64}
if not IsWow64(FProcess) then
FProcess64 := True;
{$ELSE}
// если наше приложение 32 битное, а исследуемый процесс 64-битный
// кидаем исключение
if Is64OS and not IsWow64(FProcess) then
raise Exception.Create('Can''t scan process.');
{$ENDIF}
// проверяем необходимость суспенда процесса
if SuspendProcessBeforeScan then
ProcessLock := SuspendProcess(PID);
try
FSymbols := TSymbols.Create(FProcess);
try
FPEImage := TPEImage.Create;
try
FWorkset := TWorkset.Create(FProcess);;
try
// получаем данные по регионам и отмапленым файлам
GetAllRegions;
finally
FWorkset.Free;
end;
{$IFDEF WIN64}
// если есть возможность получаем данные о 32 битных кучах
AddWow64HeapsData;
{$ENDIF}
// добавляем данные о потоках
AddThreadsData;
// добавляем данные о кучах
AddHeapsData;
// добавляем данные о Process Environment Block
AddPEBData;
// добавляем данные о загруженых PE файлах
AddImagesData;
finally
FPEImage.Free;
end;
finally
FSymbols.Free;
end;
finally
if SuspendProcessBeforeScan then
ResumeProcess(ProcessLock);
end;
// сортируем
SortAllContainsBlocks;
// считаем общую информацию о регионах
CalcTotal;
// применяем текущий фильтр
UpdateRegionFilters;
finally
CloseHandle(FProcess);
end;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Примерный код процедур GetAllRegions/AddThreadsData/AddHeapsData и AddImagesData я приводил в первых четырех главах и на нем я заострять внимание не буду, а вот с остальным желательно разобраться.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Самым первым шагом после открытия процесса происходит определение битности процесса.</div>
<div style="margin: 0px;">
Это необходимо по той причине, что в случае, если битности процессов (текущего и по которому мы получаем информацию), не совпадают, то нужно предпринять некоторые дополнительные действия.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Общая схема такая:</div>
<div>
</div>
<ol>
<li>32-битный процесс может получить данные по 32-битному под 32-битной ОС в полном объеме.</li>
<li>64-битный процесс может получить данные по 64-битному в полном объеме.</li>
<li>32-битный процесс <b>НЕ МОЖЕТ</b> получить данные по 64-битному.</li>
<li>32-битный процесс может получить данные по 32-битному под 64-битной ОС, <b>но частично</b>.</li>
<li>64-битный процесс может получить данные по 32-битному, <b>но частично</b>.</li>
</ol>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Если с первыми двумя пунктами все ясно, то остальные три рассмотрим поподробнее.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Причина того что 32-битный процесс не сможет получить данные по 64-битному простая: не позволит размер указателя, плюс ReadProcessMemory периодически будет выдавать ошибку ERROR_PARTIAL_COPY.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
А вот с получением данных из 32-битного процесса в 64-битной ОС все гораздо хитрее.</div>
<div style="margin: 0px;">
Как я говорил ранее, в 32-битном приложении загружены четыре 64-битные библиотеки, которые создают свои кучи/потоки.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Если мы будем получать список куч и потоков из 32-битного приложения, то увидим данные только относящиеся к 32 битам, данные по 64-битным аналогам получить не удастся.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
То же будет и в случае запроса данных о 32-битном процессе из 64-битного, вернутся только данные, относящиеся к 64 битам. Хотя в этом случае есть вариант получить их частично.</div>
<div style="margin: 0px;">
В частности, доступ к 32-битному <abbr title="Process Environment Block">PEB</abbr> производится вызовом такой функции:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">const
ProcessWow64Information = 26;
...
NtQueryInformationProcess(FProcess, ProcessWow64Information,
@FPebWow64BaseAddress, SizeOf(ULONG_PTR), @ReturnLength)
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Доступ к 32-битному <abbr title="Thread Environment Block">TEB</abbr> можно получить, считав адрес из 64-битного <abbr title="Thread Environment Block">TEB</abbr>, который хранится в параметре NtTIB.ExceptionList.</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;"> // в 64 битном TEB поле TIB.ExceptionList указывает на начало Wow64TEB
if not ReadProcessMemory(hProcess,
TIB.ExceptionList, @WOW64_NT_TIB, SizeOf(TWOW64_NT_TIB),
lpNumberOfBytesRead) then Exit;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Получить контекст 32-битного потока для раскрутки CallStack можно вот таким кодом:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">const
ThreadWow64Context = 29;
...
ThreadContext^.ContextFlags := CONTEXT_FULL;
if NtQueryInformationThread(hThread, ThreadWow64Context, ThreadContext,
SizeOf(TWow64Context), nil) <> STATUS_SUCCESS then Exit;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Либо вызовом функции Wow64GetThreadContext.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
А вот как получить данные о 32-битных кучах из 64-битного процесса легальным способом, мне неизвестно. Единственный вариант, который я применяю сейчас, это передача команды 32-битному процессу, который собирает данные о 32-битных кучах и отдает их обратно в 64-битный (примерно этим и занимается обработчик в функции AddWow64HeapsData).</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Теперь, когда с определением битности процесса и для чего это нужно, разобрались, пойдем дальше, а именно к вызову функции SuspendProcess.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
По-хорошему, это нужно только для того, чтобы данные в удаленном процессе не изменились на неактуальные в момент их чтения. Правда, обычно я применяю этот набор классов в двух случаях, для своего собственного приложения или для приложения находящегося под отладчиком. В обоих случаях замораживать потоки не нужно, но если анализируется какое-то стороннее приложение, то почему бы и нет?</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
После заморозки удаленного процесса создаются три вспомогательных класса.</div>
<div style="margin: 0px;">
<br /></div>
<ol>
<li>TSymbols - о нем я расскажу в следующей главе.</li>
<li>TPEImage - этот класс содержит в себе методы, позволяющие получить информацию о PE файле, описанные в четвертой главе. Сделан исключительно для удобства.</li>
<li>TWorkset - еще один вспомогательный класс, задача которого получить информацию об общедоступной памяти.</li>
</ol>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
По сути, TWorkset хранит в себе список структур вида:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;"> TShareInfo = record
Shared: Boolean;
SharedCount: Byte;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Эти структуры хранятся в словаре и каждая ассоциируется с конкретным адресом страницы.</div>
<div style="margin: 0px;">
Параметры простые:</div>
<div style="margin: 0px;">
<br /></div>
<ul>
<li>Shared - является ли страница общедоступной</li>
<li>SharedCount - сколько ссылок есть на страницу</li>
</ul>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Получаются эти данные следующим способом, в котором все сводится к вызову функции QueryWorkingSet:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">procedure TWorkset.InitWorksetData(hProcess: THandle);
const
{$IFDEF WIN64}
AddrMask = $FFFFFFFFFFFFF000;
{$ELSE}
AddrMask = $FFFFF000;
{$ENDIF}
SharedBitMask = $100;
SharedCountMask = $E0;
function GetSharedCount(Value: ULONG_PTR): Byte; inline;
begin
Result := (Value and SharedCountMask) shr 5;
end;
var
WorksetBuff: array of ULONG_PTR;
I: Integer;
ShareInfo: TShareInfo;
begin
SetLength(WorksetBuff, $400000);
while not QueryWorkingSet(hProcess, @WorksetBuff[0],
Length(WorksetBuff) * SizeOf(ULONG_PTR)) do
SetLength(WorksetBuff, WorksetBuff[0] * 2);
for I := 0 to WorksetBuff[0] - 1 do
begin
ShareInfo.Shared := WorksetBuff[I] and SharedBitMask <> 0;
ShareInfo.SharedCount := GetSharedCount(WorksetBuff[I]);
try
FData.Add(Pointer(WorksetBuff[I] and AddrMask), ShareInfo);
except
on E: EListError do ;
else
raise;
end;
end;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Данная функция возвращает массив ULONG_PTR, каждый элемент которого хранит данные следующим образом: первые пять бит хранят в себе атрибуты защиты страницы; следующие три бита – количество процессов, которым доступна данная страница; еще один бит указывает общедоступность страницы; ну и далее идет адрес самой страницы.</div>
<div style="margin: 0px;">
Более подробно можно прочитать тут: <a href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms684902(v=vs.85).aspx" target="_blank">PSAPI_WORKING_SET_BLOCK</a>.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
По сути, это просто информационный класс, ни больше ни меньше.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Впрочем, вернемся к нашему коду.</div>
<div style="margin: 0px;">
Следующими шагами идут:</div>
<div style="margin: 0px;">
<br /></div>
<ol>
<li>GetAllRegions - аналог кода из первой главы.</li>
<li>AddThreadsData - аналог кода из второй главы.</li>
<li>AddHeapsData - аналог кода из третьей главы.</li>
<li>AddPEBData - вывод данных о структуре из пятой главы.</li>
<li>AddImagesData - аналог кода из четвертой главы.</li>
</ol>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Как видите, все интересное я уже рассказал (ну почти) :)</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Оставшиеся шаги не интересны, за исключением вызова UpdateRegionFilters.</div>
<div style="margin: 0px;">
Он выполняет утилитарную функцию, а именно исключает из списка ненужные на текущий момент регионы (ну, к примеру, убирает регионы с невыделенной памятью и т.п).</div>
<div style="margin: 0px;">
Данная процедура будет вызываться постоянно при изменении фильтра через свойство Filter.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Впрочем, все это вы сможете при желании увидеть из кода самого класса.</div>
<div style="margin: 0px;">
Работать с ним достаточно просто:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">var
AMemoryMap: TMemoryMap;
M: TMemoryStream;
I: Integer;
begin
try
M := TMemoryStream.Create;
try
// Создаем класс
AMemoryMap := TMemoryMap.Create;
try
// получаем текущую карту памяти
AMemoryMap.InitFromProcess(GetCurrentProcessId, '');
// сохраняем ее,
AMemoryMap.SaveToStream(M);
// тут можно прикрутить дампы регионов и все что душе угодно
finally
AMemoryMap.Free;
end;
// тут якобы передали данные куда-то, теперь загружаем их и работаем
M.Position := 0;
// Создаем класс
AMemoryMap := TMemoryMap.Create;
try
// загружаем данные
AMemoryMap.LoadFromStream(M);
// убираем вообще все фильтры
AMemoryMap.Filter := fiNone; // не обязательно
// говорим отображать регионы с невыделенной памятью
AMemoryMap.ShowEmpty := True;
// выводим список регионов
for I := 0 to AMemoryMap.Count - 1 do
Writeln(NativeUInt(AMemoryMap[I].MBI.BaseAddress));
finally
AMemoryMap.Free;
end;
finally
M.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Как говорится, писал сам для себя, поэтому и работать с этим классом проще простого :)</div>
<div style="margin: 0px;">
<br /></div>
<h3>
8. TSymbols - работа с символами</h3>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page8"></a>Суть данного класса заключается в получении более подробной информации об адресе в процессе. Ну, к примеру, во второй главе мы получали CallStack потока (или обработчики <abbr title="Structured Exception Handling">SEH</abbr> фреймов) и это были просто некие адреса. Но гораздо интереснее вместо сухих чисел видеть что-то наподобие этой картинки:</div>
<div style="margin: 0px;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9FBi4Hz5zyVT2ihgEVNX06oD8qDjz29KANGhzgKV3G4vb8avAiqVjFbCBVvIBIF5BMaYrium7KCoo_fRQZeHdBH3BANo1-hDUyfqm_ySc_X9J6nHQapVkigCwKvKhGIDT0CAzxT-sNpw/s1600/pmm_callstack.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9FBi4Hz5zyVT2ihgEVNX06oD8qDjz29KANGhzgKV3G4vb8avAiqVjFbCBVvIBIF5BMaYrium7KCoo_fRQZeHdBH3BANo1-hDUyfqm_ySc_X9J6nHQapVkigCwKvKhGIDT0CAzxT-sNpw/s1600/pmm_callstack.png" style="cursor: move;" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 13px; padding-top: 4px;"><div style="margin: 0px;">
CallStack потока.</div>
</td></tr>
</tbody></table>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Делается это очень просто - достаточно вызова функции SymGetSymFromAddr, но есть несколько нюансов.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Давайте сначала посмотрим на код:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">function TSymbols.GetDescriptionAtAddr(Address, BaseAddress: ULONG_PTR;
const ModuleName: string): string;
const
BuffSize = $7FF;
{$IFDEF WIN64}
SizeOfStruct = SizeOf(TImagehlpSymbol64);
MaxNameLength = BuffSize - SizeOfStruct;
var
Symbol: PImagehlpSymbol64;
Displacement: DWORD64;
{$ELSE}
SizeOfStruct = SizeOf(TImagehlpSymbol);
MaxNameLength = BuffSize - SizeOfStruct;
var
Symbol: PImagehlpSymbol;
Displacement: DWORD;
{$ENDIF}
begin
Result := '';
if not FInited then Exit;
GetMem(Symbol, BuffSize);
try
Symbol^.SizeOfStruct := SizeOfStruct;
Symbol^.MaxNameLength := MaxNameLength;
Symbol^.Size := 0;
SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)),
nil, BaseAddress, 0);
try
if SymGetSymFromAddr(FProcess, Address, @Displacement, Symbol) then
Result := string(PAnsiChar(@(Symbol^).Name[0])) + ' + 0x' + IntToHex(Displacement, 4)
else
begin
// с первой попытки может и не получиться
SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)), nil, BaseAddress, 0);
if SymGetSymFromAddr(FProcess, Address, @Displacement, Symbol) then
Result := string(PAnsiChar(@(Symbol^).Name[0])) + ' + 0x' + IntToHex(Displacement, 4);
end;
finally
SymUnloadModule(FProcess, BaseAddress);
end;
finally
FreeMem(Symbol);
end;
if Result = '' then
Result := ExtractFileName(ModuleName) + ' + 0x' + IntToHex(Address - BaseAddress, 1);
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Для правильного получения описания имени функции, которой принадлежит адрес, необходимо знать путь к библиотеке, которой принадлежит функция, либо адрес, по которому данная библиотека подгружена (в коде используются оба параметра). Эти параметры необходимы для функции SymLoadModule.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Второй нюанс заключается в том, что вызов функции SymGetSymFromAddr иногда может завершиться неуспешно. Причина мне не ясна, но в интернете периодически описывают данную ситуацию и как решение её - повторный вызов функции SymLoadModule без вызова SymUnloadModule. В таком странном поведении не разбирался – но действительно помогает.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Последний из нюансов заключается в том, что данная функция вернет валидное описание адреса только тогда, когда эта информация присутствует (загружены символы из внешнего файла или они есть в составе искомого модуля).</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Эта информация не является сильно важной при отладке, но немного ее упрощает.</div>
<div style="margin: 0px;">
Вот так, к примеру, выглядит стандартный стек потока браузера Chrome (CallStack + <abbr title="Structured Exception Handling">SEH</abbr> фреймы):</div>
<div style="margin: 0px;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5DqhLw7RDDeQ9JZ8qF5KDk5Ix1VB64yc72z3wu1CtH1U-PbOpUatSHSI_MI56uayQkhGHicsKKg0Za8jNijSEuncdQs2CBNsffG0EmV4TNBFQXlOSdbMQ2aOMRzEhI1sstK7dO7QPHUY/s1600/11.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="324" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5DqhLw7RDDeQ9JZ8qF5KDk5Ix1VB64yc72z3wu1CtH1U-PbOpUatSHSI_MI56uayQkhGHicsKKg0Za8jNijSEuncdQs2CBNsffG0EmV4TNBFQXlOSdbMQ2aOMRzEhI1sstK7dO7QPHUY/s640/11.png" style="cursor: move;" width="640" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 13px; padding-top: 4px;"><div style="margin: 0px;">
Стандартный стек потока.</div>
</td></tr>
</tbody></table>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Более полезная информация, которую могут предоставить символы, это список экспортируемых функций библиотеки и их текущие адреса.</div>
<div style="margin: 0px;">
В классе TSymbols эта информация получается вызовом процедуры GetExportFuncList и выглядит следующим образом:</div>
<div style="margin: 0px;">
<br /></div>
<pre class="brush:delphi" style="margin: 0px;">function SymEnumsymbolsCallback(SymbolName: LPSTR; SymbolAddress: ULONG_PTR;
SymbolSize: ULONG; UserContext: Pointer): Bool; stdcall;
var
List: TStringList;
begin
List := UserContext;
List.AddObject(string(SymbolName), Pointer(SymbolAddress));
Result := True;
end;
procedure TSymbols.GetExportFuncList(const ModuleName: string;
BaseAddress: ULONG_PTR; Value: TStringList);
begin
SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)),
nil, BaseAddress, 0);
try
if not SymEnumerateSymbols(FProcess, BaseAddress,
@SymEnumsymbolsCallback, Value) then
begin
SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)),
nil, BaseAddress, 0);
SymEnumerateSymbols(FProcess, BaseAddress,
@SymEnumsymbolsCallback, Value)
end;
finally
SymUnloadModule(FProcess, BaseAddress);
end;
end;
</pre>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Все сводится к вызову SymEnumerateSymbols, которой передается адрес функции обратного вызова.</div>
<div style="margin: 0px;">
При ее вызове параметр SymbolName будет содержать имя экспортируемой функции, а SymbolAddress ее адрес.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Этого вполне достаточно для того, чтобы отобразить пользователю вот такую табличку:</div>
<div style="margin: 0px;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgUd7aC3YGxS90SmmwGaqmnzO5R1dH3gWMAj5GOXfAMPnSWWUcf3n3BBWdLoGvuOR7X8cG_QNIJlFjyTOK_wgIDPZ740aHtoEFRwc1z3Nc3SC7QOLuuqzGoSMkErIb4Mpgvdy0o7YR_8iQ/s1600/14.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgUd7aC3YGxS90SmmwGaqmnzO5R1dH3gWMAj5GOXfAMPnSWWUcf3n3BBWdLoGvuOR7X8cG_QNIJlFjyTOK_wgIDPZ740aHtoEFRwc1z3Nc3SC7QOLuuqzGoSMkErIb4Mpgvdy0o7YR_8iQ/s1600/14.png" style="cursor: move;" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 13px; padding-top: 4px;"><div style="margin: 0px;">
Список экспортируемых функций подгруженными в процесс библиотеками.</div>
</td></tr>
</tbody></table>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Более подробно реализацию данного класса, включая опущенные вызовы SymSetOptions и SymInitialize, вы сможете увидеть в модуле "MemoryMap.Symbols.pas".</div>
<div style="margin: 0px;">
<br /></div>
<h3>
9. ProcessMemoryMap</h3>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page9"></a>Ну вот мы подошли и к заключительной части статьи.</div>
<div style="margin: 0px;">
Как я и говорил ранее, я использую набор классов MemoryMap в двух вариантах:</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
1. Интегрируя его в вывод EurekaLog посредством перекрытия ее обработчика OnAttachedFilesRequest, где добавляю текущую карту процесса актуальную на момент возникновения исключения, и дампы всех Private регионов (страниц, не ассоциированных с определенными данными, имеющих флаг MEM_PRIVATE) и стеков потоков, плюс часть информации из <abbr title="Process Environment Block">PEB</abbr>. Обычно этого достаточно для разбора причин возникновения ошибки.</div>
<div style="margin: 0px;">
2. Использую как альтернативный инструмент для анализа отлаживаемого приложения.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Для второго варианта была реализована отдельная утилита, которая работает непосредственно с классами MemoryMap, плюс добавляет некий дополнительный функционал.</div>
<div style="margin: 0px;">
<br /></div>
<div class="separator" style="clear: both; margin: 0px; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimiAlIKWOlsCMyIzpZRiCebfEuiZY41ozwttYxP17Un2HKhROnttlLyKefRMciFdxQczOqcMDpURV0lI-vs6OQIfqWdsdivCUXXZIpsV9e54SqN8ssrTlCCaw5jVJk-wkrPHBVLJ7xanw/s1600/15.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="404" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimiAlIKWOlsCMyIzpZRiCebfEuiZY41ozwttYxP17Un2HKhROnttlLyKefRMciFdxQczOqcMDpURV0lI-vs6OQIfqWdsdivCUXXZIpsV9e54SqN8ssrTlCCaw5jVJk-wkrPHBVLJ7xanw/s640/15.png" style="cursor: move;" width="640" /></a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Описывать ее исходный код я не буду, пройдусь только немного по функционалу.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
С интерфейсной части она практически один в один напоминает VMMap. Впрочем, так и планировалось изначально, ибо такой интерфейс наиболее удобен для анализа.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
В верхней части расположен список с общей информацией по регионам, сгруппированным по их типам, он же является фильтром.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
На текущий момент она представляет следующий функционал:</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
1. Просмотр содержимого памяти по указанному адресу (Ctrl+Q).</div>
<div style="margin: 0px;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXQBvXU9Ss888Azy09AXU0uOiX970TmFpxNQDwVnnxyDSFqyh1VrM7nLt6J2x1P2dwZtRA4ivu-G7WqwROflhIyTGy0k0ryNuFlr-EKF0iibzsbUq6Om6bgu7f2Y3NWifz0SM-bcDerDU/s1600/16.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="478" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXQBvXU9Ss888Azy09AXU0uOiX970TmFpxNQDwVnnxyDSFqyh1VrM7nLt6J2x1P2dwZtRA4ivu-G7WqwROflhIyTGy0k0ryNuFlr-EKF0iibzsbUq6Om6bgu7f2Y3NWifz0SM-bcDerDU/s640/16.png" style="cursor: move;" width="640" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 13px; padding-top: 4px;"><div style="margin: 0px;">
Свойства произвольного региона</div>
</td></tr>
</tbody></table>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Этот функционал, в принципе, присутствует в отладчике Delphi в окне CPU View, но возможностей у этого режима гораздо больше. К примеру, в случае просмотра поля <abbr title="Process Environment Block">PEB</abbr>, будут выводится данные в другом виде:</div>
<div style="margin: 0px;">
<br /></div>
<div class="separator" style="clear: both; margin: 0px; text-align: center;">
</div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-bottom: 0.5em; margin-left: auto; margin-right: auto; padding: 6px; text-align: center;"><tbody>
<tr><td><div style="margin: 0px;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTZ3OcWk5BtVuBNR5Ss_AwVDROGsrA5K1h8hYVpefRoBZZ0UbCmoR5JAqaYFsOjLqP4C7w6_0sAoemJbkZRQp9yRYHrn_edwxb7YSdqZC4Z6NNJmdCMXDhGebTACqMaes3eyirZato5cc/s1600/17.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="418" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTZ3OcWk5BtVuBNR5Ss_AwVDROGsrA5K1h8hYVpefRoBZZ0UbCmoR5JAqaYFsOjLqP4C7w6_0sAoemJbkZRQp9yRYHrn_edwxb7YSdqZC4Z6NNJmdCMXDhGebTACqMaes3eyirZato5cc/s640/17.png" style="cursor: move;" width="640" /></a></div>
</td></tr>
<tr><td class="tr-caption" style="font-size: 13px; padding-top: 4px;"><div style="margin: 0px;">
Process Environment Block 64</div>
</td></tr>
</tbody></table>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Вот так будет выглядеть блок параметров процесса:</div>
<div style="margin: 0px;">
<br /></div>
<div class="separator" style="clear: both; margin: 0px; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhT-rTbvjDu8M22kuDtL_bwA5muCJQDfJhHbq-V7AQnntCWQco6m2YY9xNaqvhvsiQX6N0SO58_00QvCUTE4G2SVU2pdVZg2vLiOXXYu_RFnlCAcr3TEGMsguUaDfCmssdlsuZsXIM1kqA/s1600/18.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="384" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhT-rTbvjDu8M22kuDtL_bwA5muCJQDfJhHbq-V7AQnntCWQco6m2YY9xNaqvhvsiQX6N0SO58_00QvCUTE4G2SVU2pdVZg2vLiOXXYu_RFnlCAcr3TEGMsguUaDfCmssdlsuZsXIM1kqA/s640/18.png" style="cursor: move;" width="640" /></a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Ну и так далее. Всего на данный момент утилита может выводить размапленные данные по следующим структурам:</div>
<ul>
<li>PEB - Process Environment Block (32/64)</li>
<li>TEB - Thread Environment Block (32/64)</li>
<li>KUSER_SHARED_DATA</li>
<li>PE Header (IMAGE_DOS_HEADER / IMAGE_NT_HEADER / IMAGE_FILE_HEADER / IMAGE_OPTIONAL_HEADER(32/64) / IMAGE_DATA_DIRECTORY / IMAGE_SECTION_HEADERS)</li>
<li>Process Parameters (32/64)</li>
</ul>
<div style="margin: 0px;">
Этот список не окончательный, периодически в него будут добавляться новые структуры.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
2. Поиск данных в памяти процесса (Ctrl+F):</div>
<div style="margin: 0px;">
<br /></div>
<div class="separator" style="clear: both; margin: 0px; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuVcNBwJBBYtTFNyY57bxTA3u24Ic4T2kwoSfKWB9OhtzJ0wZKBDPZfNK12ODghddYz_Vi7Rf4gULVhCJIK7-YvE0q44kqtbvqsfSWvNFN6XgUaxqtnwhB_385PqJl4aK4xwGULVwjZ-4/s1600/19.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuVcNBwJBBYtTFNyY57bxTA3u24Ic4T2kwoSfKWB9OhtzJ0wZKBDPZfNK12ODghddYz_Vi7Rf4gULVhCJIK7-YvE0q44kqtbvqsfSWvNFN6XgUaxqtnwhB_385PqJl4aK4xwGULVwjZ-4/s1600/19.png" style="cursor: move;" /></a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Этот функционал в отладчике Delphi, к сожалению, отсутствует.</div>
<div style="margin: 0px;">
Поиск можно производить как по Ansi, так и по Unicode строке, либо просто по абстрактному HEX буферу. При поиске можно указывать адрес начала поиска, а так же флаг, указывающий на необходимость поиска в страницах, доступ к которым возможен только на чтение.</div>
<div style="margin: 0px;">
Результат выводится в виде окна с дампом памяти, показанного выше.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
3. Компаратор двух карт памяти. Включается в настройках.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Позволяет найти отличия между двумя картами памяти и выводит их в виде текста.</div>
<div style="margin: 0px;">
<br /></div>
<div class="separator" style="clear: both; margin: 0px; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGNg8rIiZMNQ97llg2jkBr4HS6aRQ6_d0LeCN5-5dq_pPgdtVoqpgOaky4dz5CAHIci2hWECHJVwLWX9sGtbLqRQhAgbUrW2jBo77ZH5ZzRV-SEoFvinFrr1lbuAxO8Jx6GpRjbp76sp4/s1600/20.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="450" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGNg8rIiZMNQ97llg2jkBr4HS6aRQ6_d0LeCN5-5dq_pPgdtVoqpgOaky4dz5CAHIci2hWECHJVwLWX9sGtbLqRQhAgbUrW2jBo77ZH5ZzRV-SEoFvinFrr1lbuAxO8Jx6GpRjbp76sp4/s640/20.png" style="cursor: move;" width="640" /></a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Сравниваются только сами карты, а не данные. Т.е. если по какому-то адресу изменились 4 байта, это изменение не отобразится. Но вот в том случае, если изменился размер региона, удалилась куча, выгрузился/загрузился файл и т.п. - все это будет отображено в результатах сравнения.</div>
<div style="margin: 0px;">
Сравнивать можно как текущий снимок карты с сохраненным ранее, так и при обновлении снимка по горячей клавише F5.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
4. Дамп памяти.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Также отсутствующий в отладчике Delphi функционал. Позволяет сохранить на диск содержимое памяти указанного региона либо данные с указанного адреса.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
5. Вывод всех доступных экспортируемых функций из всех библиотек, подгруженных в анализируемый процесс (Ctrl+E).</div>
<div style="margin: 0px;">
<br /></div>
<div class="separator" style="clear: both; margin: 0px; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMfU8vgnOrxm6BId2Dmc9_86fOlvK2Skz6jyi1WqsTz5lpBDB6A1FwxzbRh_H34yFDwSwW_8h6Y-OGLbYINIWITcatGxgEa3MFnoHpWB1Dv281qp6dFuylAzNZTHSVMNk5p6et9_190No/s1600/21.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMfU8vgnOrxm6BId2Dmc9_86fOlvK2Skz6jyi1WqsTz5lpBDB6A1FwxzbRh_H34yFDwSwW_8h6Y-OGLbYINIWITcatGxgEa3MFnoHpWB1Dv281qp6dFuylAzNZTHSVMNk5p6et9_190No/s1600/21.png" style="cursor: move;" /></a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
А также быстрый поиск функции по ее наименованию или адресу.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Пока что текущего функционала для меня лично достаточно, и новый я не добавлял, но в перспективе данная утилита будет развиваться.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
ProcessMemoryMap является OpenSource проектом.</div>
<div style="margin: 0px;">
Ее последний стабильный релиз всегда доступен по ссылке: <a href="http://rouse.drkb.ru/winapi.php#pmm2" target="_blank">http://rouse.drkb.ru/winapi.php#pmm2</a></div>
<div style="margin: 0px;">
GitHub репозиторий с последними изменениями кода можно обнаружить здесь: <a href="https://github.com/AlexanderBagel/ProcessMemoryMap" target="_blank">https://github.com/AlexanderBagel/ProcessMemoryMap</a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Прямая ссылка на исходный код: <a href="https://github.com/AlexanderBagel/ProcessMemoryMap/archive/master.zip" target="_blank">https://github.com/AlexanderBagel/ProcessMemoryMap/archive/master.zip</a></div>
<div style="margin: 0px;">
Прямая ссылка на последнюю сборку: <a href="http://rouse.drkb.ru/files/processmm_bin.zip" target="_blank">http://rouse.drkb.ru/files/processmm_bin.zip</a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Для самостоятельной сборки потребуется установленный пакет компонентов Virtual TreeView версии 5 и выше: <a href="http://www.soft-gems.net/" target="_blank">http://www.soft-gems.net/</a>.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<span style="color: red;"><b>Сборка осуществляется с использованием Delphi XE4</b></span> и выше в режиме "Win32/Release", при этом автоматически будет собрана и подключена (в виде ресурса) 64-битная версия данной утилиты.</div>
<div style="margin: 0px;">
Под более старыми версиями Delphi работоспособность ProcessMemoryMap не проверялась.</div>
<div style="margin: 0px;">
<br /></div>
<h3>
10. В качестве заключения</h3>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
<a href="http://www.blogger.com/blogger.g?blogID=1087517875409073837" name="page10"></a>Ну что ж, надеюсь данный материал будет для вас полезен. Я, конечно, прошел только по самым вершкам, ибо если раскрывать весь материал более подробно, то объем статьи неимоверно увеличится.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Поэтому вот вам несколько ссылок, по которым вы сможете узнать немного больше информации.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Информацию о системных структурах TEB/PEB и т.п. можно найти здесь:</div>
<div style="margin: 0px;">
<a href="http://processhacker.sourceforge.net/" target="_blank">http://processhacker.sourceforge.net/</a></div>
<div style="margin: 0px;">
<a href="http://redplait.blogspot.ru/" target="_blank">http://redplait.blogspot.ru/</a></div>
<div style="margin: 0px;">
<a href="http://www.reactos.org/ru" target="_blank">http://www.reactos.org/ru</a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Информация о PE файлах:</div>
<div style="margin: 0px;">
<a href="http://msdn.microsoft.com/en-us/magazine/ms809762.aspx" target="_blank">http://msdn.microsoft.com/en-us/magazine/ms809762.aspx</a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Информация о SEH:</div>
<div style="margin: 0px;">
<a href="http://msdn.microsoft.com/en-us/library/ms680657(v=VS.85).aspx" target="_blank">http://msdn.microsoft.com/en-us/library/ms680657(v=VS.85).aspx</a></div>
<div style="margin: 0px;">
<a href="http://www.microsoft.com/msj/0197/exception/exception.aspx" target="_blank">http://www.microsoft.com/msj/0197/exception/exception.aspx</a></div>
<div style="margin: 0px;">
<a href="http://qxov.narod.ru/articles/seh/seh.html" target="_blank">http://qxov.narod.ru/articles/seh/seh.html</a></div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Исходный код всех демо-примеров можно забрать по <a href="http://rouse.drkb.ru/blog/pmm2.zip" target="_blank">данной ссылке</a>.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Огромное СПАСИБО форуму <a href="http://www.delphimaster.ru/" target="_blank">"Мастера Дельфи"</a> за неоднократную помощь в подготовке статьи.</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Персональное спасибо за вычитку материала Дмитрию aka "брат Птибурдукова", Андрею Васильеву aka Inovet, а также Сергею aka "Картман".</div>
<div style="margin: 0px;">
<br /></div>
<div style="margin: 0px;">
Удачи.</div>
<div style="margin: 0px;">
<br /></div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
---</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
<br /></div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
© Александр (Rouse_) Багель</div>
</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<div style="margin: 0px;">
Ноябрь, 2013</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com9tag:blogger.com,1999:blog-2374465879949372415.post-55148491178979147852013-06-01T13:51:00.003+04:002013-06-01T19:37:00.738+04:00Сортировка списка по аналогу "Проводника Windows"<div dir="ltr" style="text-align: left;" trbidi="on">
Когда проект практически завершен и вся бизнес логика находится в тестировании иногда возникает желание дополнить его "рюшечками и фишечками" и прочими "украшательствами", ну например перерисовать пару иконок на более красивые, или сделать выделение элементов через градиент с альфаканалом.<br />
Вариантов таких спонтанных хотелок (особенно при наличии времени) много и все из серии украшательств, не несущих по сути никакой смысловой нагрузки - но вот хочется и все :)<br />
<br />
В данной мини-статье я рассмотрю одну из таких "хотелок".<br />
<br />
Допустим у вас есть список элементов, отображаемый в TListView, вы пробуете его отсортировать и получаете вот такой результат.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXzoebjzTDOR-ElO7cN1HJiVjdpMJ9BHEFGbnbwKS5J0sj9OZaktfhTWDWW8VbX3tAWry6QID9kHwtvxmD0XufXjjvHqx2ROQkiVwa2Nr3MoXLQsjhGMsIJafzYifmtLEuTazA0FwpPNwX/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXzoebjzTDOR-ElO7cN1HJiVjdpMJ9BHEFGbnbwKS5J0sj9OZaktfhTWDWW8VbX3tAWry6QID9kHwtvxmD0XufXjjvHqx2ROQkiVwa2Nr3MoXLQsjhGMsIJafzYifmtLEuTazA0FwpPNwX/s1600/1.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
Не красиво, почему это второй элемент с именем "101" находится не на своем месте? Ведь это число, а стало быть место ему как минимум после элемента с именем "2". Да и "New Folder (101)" явно должна быть после "New Folder (2)". Ведь в проводнике все выглядит нормально.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5S8y3xUXrZ_CbC0iy4X0wXJt_7eW_ioKVRf1GSUF9icn3ZcinoDeU0s3xaAe_7c-17gSKKrUDIA-WfEXsebbpIHHDou2DFa-gUrvHEXTx87LoEfG_lkvHPGW4yke8BVpt56tE6QiqLd21/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5S8y3xUXrZ_CbC0iy4X0wXJt_7eW_ioKVRf1GSUF9icn3ZcinoDeU0s3xaAe_7c-17gSKKrUDIA-WfEXsebbpIHHDou2DFa-gUrvHEXTx87LoEfG_lkvHPGW4yke8BVpt56tE6QiqLd21/s1600/2.png" /></a></div>
<br />
Попробуем разобраться в причинах такого поведения и реализовать алгоритм более правильной, с точки зрения человека, сортировки.<br />
<br />
<br />
<a name='more'></a><br />
<br />
Для начала давайте разберемся в причинах неверной сортировки.<br />
По умолчанию в TListView для сравнения строк используется функция lstrcmp, которая сравнивает строки посимвольно.<br />
<br />
К примеру если взять две строки "1" и "2", то первая строка должна располагаться над второй, т.к. символ единицы идет перед двойкой. Однако если вместо первой строки взять "101", функция lstrcmp так-же скажет что данная строка должна идти первой, ибо в этом случае она принимает решение по результату сравнения самого первого символа обеих строк, не учитывая тот факт что обе строки являются строковым представлением чисел.<br />
<br />
Немножко усложним, возьмем строки "1a2" и "1a101" на которых lstrcmp опять выдаст неверный результат, сказав что вторая строка должна идти первой. Это решение она принимает на основе результата сравнения третьего символа обеих строк, не смотря на то что в данном случае они так-же являются строковыми представлениями чисел.<br />
<br />
С причинами разобрались, теперь думаем решение.<br />
Раз lstrcmp ошибается на сравнении, интерпретируя части чисел в виде символов, нужно реализовать аналогичный ей алгоритм сравнения, в котором числа будут сравниваться именно как числа, а не как символы.<br />
<br />
Алгоритмически это сделать достаточно просто.<br />
<br />
Возьмем опять-же "1a2" и "1a101". Разобьем их на отдельные составляющие, где символы будут отделены от чисел. Если представить первую строку в виде "1 + a + 2", а вторую в виде "1 + a + 101" то получится что нам нужно выполнить всего три сравнения.<br />
<br />
1. Число с числом<br />
2. Символ с символом<br />
3. Опять число с числом<br />
<br />
Итог такого сравнения будет верный и покажет что вторая строка действительно должна идти второй, а не первой, как нам об этом сообщала lstrcmp.<br />
<br />
Теперь продумаем ТЗ к реализации данного алгоритма.<br />
<br />
Очевидно что:<br />
1. Если одна из строк, переданная для сравнения пустая - она должна идти выше первой.<br />
2. Если обе строки пустые - они идентичны.<br />
3. Регистр строк при сравнении не учитывается.<br />
4. Для анализа строк используем курсор содержащий адрес текущего анализируемого символа каждой строки.<br />
5. Если курсор одной из строк содержит число, а курсор другой строки содержит символ - первая строка выше второй.<br />
6. Если курсоры строк указывают на символ - сравнение происходит по аналогу lstrcmp<br />
7. Если курсоры строк указывают на число - извлекаем оба числа и сравниваем их между собой.<br />
7.1 Если оба числа равны нулю (к примеру "00" и "0000") то вверх помещается число с меньшим количеством нулей.<br />
8. Если в процессе анализа курсор любой из строк обнаружил терминирующий ноль - эта строка идет выше.<br />
8.1 Если в этот-же момент курсор второй строки тоже находится на терминирующем нуле - строки идентичны.<br />
<br />
Для реализации алгоритма, данного ТЗ более чем достаточно.<br />
Собственно реализуем:<br />
<br />
<pre class="brush:delphi">//
// CompareStringOrdinal сравнивает две строки по аналогу проводника, т.е.
// "Новая папка (3)" < "Новая папка (103)"
//
// Возвращает следующие значения
// -1 - первая строка меньше второй
// 0 - строки эквивалентны
// 1 - первая строка больше второй
// =============================================================================
function CompareStringOrdinal(const S1, S2: string): Integer;
// Функция CharInSet появилась начиная с Delphi 2009,
// для более старых версий реализуем ее аналог
function CharInSet(AChar: Char; ASet: TSysCharSet): Boolean;
begin
Result := AChar in ASet;
end;
var
S1IsInt, S2IsInt: Boolean;
S1Cursor, S2Cursor: PChar;
S1Int, S2Int, Counter, S1IntCount, S2IntCount: Integer;
SingleByte: Byte;
begin
// Проверка на пустые строки
if S1 = '' then
if S2 = '' then
begin
Result := 0;
Exit;
end
else
begin
Result := -1;
Exit;
end;
if S2 = '' then
begin
Result := 1;
Exit;
end;
S1Cursor := @AnsiLowerCase(S1)[1];
S2Cursor := @AnsiLowerCase(S2)[1];
while True do
begin
// проверка на конец первой строки
if S1Cursor^ = #0 then
if S2Cursor^ = #0 then
begin
Result := 0;
Exit;
end
else
begin
Result := -1;
Exit;
end;
// проверка на конец второй строки
if S2Cursor^ = #0 then
begin
Result := 1;
Exit;
end;
// проверка на начало числа в обоих строках
S1IsInt := CharInSet(S1Cursor^, ['0'..'9']);
S2IsInt := CharInSet(S2Cursor^, ['0'..'9']);
if S1IsInt and not S2IsInt then
begin
Result := -1;
Exit;
end;
if not S1IsInt and S2IsInt then
begin
Result := 1;
Exit;
end;
// посимвольное сравнение
if not (S1IsInt and S2IsInt) then
begin
if S1Cursor^ = S2Cursor^ then
begin
Inc(S1Cursor);
Inc(S2Cursor);
Continue;
end;
if S1Cursor^ < S2Cursor^ then
begin
Result := -1;
Exit;
end
else
begin
Result := 1;
Exit;
end;
end;
// вытаскиваем числа из обоих строк и сравниваем
S1Int := 0;
Counter := 1;
S1IntCount := 0;
repeat
Inc(S1IntCount);
SingleByte := Byte(S1Cursor^) - Byte('0');
S1Int := S1Int * Counter + SingleByte;
Inc(S1Cursor);
Counter := 10;
until not CharInSet(S1Cursor^, ['0'..'9']);
S2Int := 0;
Counter := 1;
S2IntCount := 0;
repeat
SingleByte := Byte(S2Cursor^) - Byte('0');
Inc(S2IntCount);
S2Int := S2Int * Counter + SingleByte;
Inc(S2Cursor);
Counter := 10;
until not CharInSet(S2Cursor^, ['0'..'9']);
if S1Int = S2Int then
begin
if S1Int = 0 then
begin
if S1IntCount < S2IntCount then
begin
Result := -1;
Exit;
end;
if S1IntCount > S2IntCount then
begin
Result := 1;
Exit;
end;
end;
Continue;
end;
if S1Int < S2Int then
begin
Result := -1;
Exit;
end
else
begin
Result := 1;
Exit;
end;
end;
end;
</pre>
<br />
Смотрим результат работы данного алгоритма.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhktqEl_Ebsd6GdtNcnfgas1KMNT8aLmKFKG7v1KRXs5o9qBTUE2uVrvalnSvHZmZ10u134EHBXzRSkrmtnkyIb9_v5kfdMB2ZUlEot0up5O6tvX9Zz4wRQJqfV7jysC1JoBUcXmhumhmah/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhktqEl_Ebsd6GdtNcnfgas1KMNT8aLmKFKG7v1KRXs5o9qBTUE2uVrvalnSvHZmZ10u134EHBXzRSkrmtnkyIb9_v5kfdMB2ZUlEot0up5O6tvX9Zz4wRQJqfV7jysC1JoBUcXmhumhmah/s1600/3.png" /></a></div>
<br />
Собственно что и ожидалось.<br />
Очередная "украшалка" готова :)<br />
<br />
Можно конечно сказать что это велосипед и нужно использовать StrCmpLogicalW:<br />
<a href="http://msdn.microsoft.com/en-us/library/windows/desktop/bb759947" target="_blank">http://msdn.microsoft.com/en-us/library/windows/desktop/bb759947</a><br />
<br />
Чтож, попробуйте - третья кнопка отвечает за такую сортировку.<br />
Обратите внимание на первые пять элементов списка после сортировки.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgeDq2uXAgbRAFlMzCU5Etbp5FsG4v7PSK_1eelq7ey5L0ombgabn1x9HEQq_2BiCmYSOkQaU0Wuj5VeqwpwfWtWxyqOBZyzh7zlDVWEWgdh8BqFkUghKkbglagEIxNPDUykgV41-o-9zGx/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgeDq2uXAgbRAFlMzCU5Etbp5FsG4v7PSK_1eelq7ey5L0ombgabn1x9HEQq_2BiCmYSOkQaU0Wuj5VeqwpwfWtWxyqOBZyzh7zlDVWEWgdh8BqFkUghKkbglagEIxNPDUykgV41-o-9zGx/s1600/4.png" /></a></div>
<br />
Хоть они и похожи на то, что отобразит проводник, но не совсем верны. Ну не должен элемент с именем "0" располагаться под элементом "00" и прочими :)<br />
<br />
Исходный код демо-примера можно забрать по <a href="http://rouse.drkb.ru/blog/sort.zip" target="_blank">данной ссылке</a>.<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
---</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<br /></div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
© Александр (Rouse_) Багель</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
Июнь, 2013</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com26tag:blogger.com,1999:blog-2374465879949372415.post-22841789993201091872013-05-26T19:41:00.002+04:002013-05-27T18:47:38.615+04:00Правильное применение сплайсинга при перехвате функций подготовленных к HotPatch<div dir="ltr" style="text-align: left;" trbidi="on">
В <a href="http://alexander-bagel.blogspot.ru/2013/01/intercept.html" target="_blank">прошлой статье</a> я рассмотрел пять вариантов перехвата функций включая их вариации.<br />
<br />
Правда в ней я оставил не рассмотренными две неприятных ситуации:<br />
1. Вызов перехваченной функции в тот момент, когда ловушка снята.<br />
2. Одновременный вызов перехваченной функции из двух разных нитей.<br />
<br />
В первом случае программист, установивший перехватчик не будет видеть всю картину в целом, т.к. часть данных пройдет мимо него.<br />
Второй случай грозит более серьезными последствиями, вплоть до падения приложения, в котором установлен перехватчик.<br />
<br />
Обе этих ситуации могут быть только в случае применения сплайсинга. При перехвате через таблицы импорта/экспорта и т.п. модификации тела перехватываемой функции не происходит, поэтому данные варианты перехвата не требуют излишних телодвижений.<br />
<br />
В этой статье более подробно будет рассмотрен сплайсинг точки входа функции подготовленной к HopPatch, т.к. данные функции предоставляют нам способ ухода от вышеперечисленных ошибок.<br />
<br />
Перехват сплайсингом через JMP NEAR OFFSET или PUSH ADDR + RET (наиболее уязвимый к данным ошибкам) рассмотрен не будет, т.к. по хорошему, без реализации дизассемблера длин, заставить данный вариант перехвата работать как нужно не получится.<br />
<br />
<br />
<a name='more'></a><br />
<br />
<h3>
1. Реализуем приложение перехватывающее вызов CreateWindowExW</h3>
<br />
Для начала подготовим приложение, которое наглядно покажет нам потерю данных при перехвате API из-за того, что вызов перехваченной функции может происходить в тот момент, когда перехват с нее снят.<br />
<br />
Создайте новый проект и разместите на главной форме три элемента: TMemo, TOpenDialog и TButton.<br />
<br />
Суть приложения: при нажатии кнопки будет устанавливаться перехват на функцию CreateWindowExW и отображаться диалог. После закрытия диалога в TMemo будет выводится информация о всех созданных диалогом окнах.<br />
<br />
Для этого нам потребуется часть кода из <a href="http://alexander-bagel.blogspot.ru/2013/01/intercept.html" target="_blank">предыдущей статьи</a>, а именно:<br />
<br />
1. Декларация типов и констант для перехвата:<br />
<br />
<pre class="brush:delphi">const
LOCK_JMP_OPKODE: Word = $F9EB;
JMP_OPKODE: Word = $E9;
type
// структура для обычного сплайса через JMP NEAR OFFSET
TNearJmpSpliceRec = packed record
JmpOpcode: Byte;
Offset: DWORD;
end;
THotPachSpliceData = packed record
FuncAddr: FARPROC;
SpliceRec: TNearJmpSpliceRec;
LockJmp: Word;
end;
const
NearJmpSpliceRecSize = SizeOf(TNearJmpSpliceRec);
LockJmpOpcodeSize = SizeOf(Word);
</pre>
<br />
2. Процедуры записи NEAR JMP и атомарной записи SHORT JMP<br />
<br />
<pre class="brush:delphi">// процедура пищет новый блок данных по адресу функции
procedure SpliceNearJmp(FuncAddr: Pointer; NewData: TNearJmpSpliceRec);
var
OldProtect: DWORD;
begin
VirtualProtect(FuncAddr, NearJmpSpliceRecSize,
PAGE_EXECUTE_READWRITE, OldProtect);
try
Move(NewData, FuncAddr^, NearJmpSpliceRecSize);
finally
VirtualProtect(FuncAddr, NearJmpSpliceRecSize,
OldProtect, OldProtect);
end;
end;
// процедура атомарно изменяет два байта по переданному адресу
procedure SpliceLockJmp(FuncAddr: Pointer; NewData: Word);
var
OldProtect: DWORD;
begin
VirtualProtect(FuncAddr, LockJmpOpcodeSize, PAGE_EXECUTE_READWRITE, OldProtect);
try
asm
mov ax, NewData
mov ecx, FuncAddr
lock xchg word ptr [ecx], ax
end;
finally
VirtualProtect(FuncAddr, LockJmpOpcodeSize, OldProtect, OldProtect);
end;
end;
</pre>
<br />
3. Нeмного модифицированная процедура инициализация структуры THotPachSpliceData<br />
<br />
<pre class="brush:delphi">// процедура инициализирует структуру для установки перехвата
procedure InitHotPatchSpliceRec(const LibraryName, FunctionName: string;
InterceptHandler: Pointer; out HotPathSpliceRec: THotPachSpliceData);
begin
// запоминаем оригинальный адрес перехватываемой функции
HotPathSpliceRec.FuncAddr :=
GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName));
// читаем два байта с ее начала, их мы будем перезатирать
Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize);
// инициализируем опкод JMP NEAR
HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE;
// рассчитываем адрес прыжка (поправка на NearJmpSpliceRecSize не нужна,
// т.к. адрес находится уже со смещением)
HotPathSpliceRec.SpliceRec.Offset :=
PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr);
end;
</pre>
<br />
Весь этот код разместим в отдельном модуле SpliceHelper, он нам потребуется в следующих главах.<br />
<br />
Теперь перейдем к главной форме, нам потребуются две глобальных переменных:<br />
<br />
<pre class="brush:delphi">var
HotPathSpliceRec: THotPachSpliceData;
WindowList: TStringList;
</pre>
<br />
В переменной HotPathSpliceRec будет содержаться информация о перехватчике. Вторая будет содержать в себе список созданных окон.<br />
<br />
В конструкторе формы произведем инициализацию структуры THotPachSpliceData.<br />
<br />
<pre class="brush:delphi">procedure TForm1.FormCreate(Sender: TObject);
begin
// инициализируем структуру для перехватчика
InitHotPatchSpliceRec(user32, 'CreateWindowExW',
@InterceptedCreateWindowExW, HotPathSpliceRec);
// пишем прыжок в область NOP-ов
SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize,
HotPathSpliceRec.SpliceRec);
end;
</pre>
<br />
Создадим функцию перехватчик, вызываемую вместо оригинальной функции.<br />
<br />
<pre class="brush:delphi">function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar;
lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall;
var
S: string;
Index: Integer;
begin
// снимаем перехват
SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp);
try
// запоминаем информацию о созданном окне
Index := -1;
if not IsBadReadPtr(lpClassName, 1) then
begin
S := 'ClassName: ' + string(lpClassName);
S := IntToStr(WindowList.Count + 1) + ': ' + S;
Index := WindowList.Add(S);
end;
// вызываем оригинальную функцию
Result := CreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle,
X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam);
// добавляем информацию о вызове в список
if Index >= 0 then
begin
S := S + ', handle: ' + IntToStr(Result);
WindowList[Index] := S;
end;
finally
// восстанавливаем перехват
SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE);
end;
end;
</pre>
<br />
И осталось в завершение реализовать обработчик кнопки.<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
begin
// перехватываем CreateWindowExW
SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE);
try
// Создаем список в котором будет хранится информация о созданных окнах
WindowList := TStringList.Create;
try
// открываем диалог
OpenDialog1.Execute;
// по завершении отображаем полученный список
Memo1.Lines.Text := WindowList.Text;
finally
WindowList.Free;
end;
finally
// снимаем перехват
SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp);
end;
end;
</pre>
<br />
Все готово, можно запускать программу на исполнение.<br />
<br />
Подробно рассказывать о реализованном в данной главе коде я не буду, он более чем подробно описан в <a href="http://alexander-bagel.blogspot.ru/2013/01/intercept.html" target="_blank">предыдущей статье</a>, второй раз расписывать не имеет смысла.<br />
<br />
Запустите программу, нажмите кнопку и закройте диалог нажатием кнопки "Отмена", должно получиться так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4ymFZqWwQ730wLhFUYbM45BVd5zDvekRqFT-fHnFW_ONV4jGE8F_AmCQawKG1bTwuxIpu9B67ijlp5demGrtxcGTP65lSW-p1W5IMZ2oUGVOcPUDnpkZMc5SZoM2LrSMVARQoZV5M55E/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4ymFZqWwQ730wLhFUYbM45BVd5zDvekRqFT-fHnFW_ONV4jGE8F_AmCQawKG1bTwuxIpu9B67ijlp5demGrtxcGTP65lSW-p1W5IMZ2oUGVOcPUDnpkZMc5SZoM2LrSMVARQoZV5M55E/s1600/1.png" /></a></div>
<br />
Таким образом мы выяснили, что при открытии обычного TOpenDialog создается 14 окон различных классов.<br />
<br />
Теперь давайте выясним, на самом ли деле это так.<br />
<br />
<h3>
2. Создаем вспомогательную утилиту для просмотра дерева окон приложения.</h3>
<br />
Для проверки работы нашего перехватчика необходимо подстраховаться сторонней утилитой, которая может отобразить актуальный список окон у приложения, при помощи которой мы и выясним, всю ли информацию мы получили нашим перехватчиком или нет.<br />
<br />
Можно конечно воспользоваться сторонними программами, наподобие Spy++ но мы же программисты, что нам стоит реализовать ее самостоятельно, тем более и время на реализацию копеечное.<br />
<br />
Создайте новый проект и поместите на главной форме TTreeView после чего реализуйте следующий код:<br />
<br />
<pre class="brush:delphi">type
TdlgWindowTree = class(TForm)
WindowTreeView: TTreeView;
procedure FormCreate(Sender: TObject);
private
procedure Sys_Windows_Tree(Node: TTreeNode;
AHandle: HWND; ALevel: Integer);
end;
...
procedure TdlgWindowTree.FormCreate(Sender: TObject);
begin
Sys_Windows_Tree(nil, GetDesktopWindow, 0);
end;
procedure TdlgWindowTree.Sys_Windows_Tree(Node: TTreeNode;
AHandle: HWND; ALevel: Integer);
type
TRootNodeData = record
Node: TTreeNode;
PID: Cardinal;
end;
var
szClassName, szCaption, szLayoutName: array[0..MAXCHAR - 1] of Char;
szFileName : array[0..MAX_PATH - 1] of Char;
Result: String;
PID, TID: Cardinal;
I: Integer;
RootItems: array of TRootNodeData;
IsNew: Boolean;
begin
//Запускаем цикл пока не закончатся окна
while AHandle <> 0 do
begin
//Получаем имя класса окна
GetClassName(AHandle, szClassName, MAXCHAR);
//Получаем текст (Его Caption) окна
GetWindowText(AHandle, szCaption, MAXCHAR);
// Получаем имя модуля
if GetWindowModuleFilename(AHandle, szFileName, SizeOf(szFileName)) = 0 then
FillChar(szFileName, 256, #0);
TID := GetWindowThreadProcessId(AHandle, PID);
// Раскладка процесса
AttachThreadInput(GetCurrentThreadId, TID, True);
VerLanguageName(GetKeyboardLayout(TID) and $FFFF, szLayoutName, MAXCHAR);
AttachThreadInput(GetCurrentThreadId, TID, False);
// Результат
Result := Format('%s [%s] Caption = %s, Handle = %d, Layout = %s',
[String(szClassName), String(szFileName), String(szCaption),
AHandle, String(szLayoutName)]);
// Смотрим в какое место добавлять окно
if ALevel in [0..1] then
begin
IsNew := True;
for I := 0 to Length(RootItems) - 1 do
if RootItems[I].PID = PID then
begin
Node := RootItems[I].Node;
IsNew := False;
Break;
end;
if IsNew then
begin
SetLength(RootItems, Length(RootItems) + 1);
RootItems[Length(RootItems) - 1].PID := PID;
RootItems[Length(RootItems) - 1].Node :=
WindowTreeView.Items.AddChild(nil, 'PID: ' + IntToStr(PID));
Node := RootItems[Length(RootItems) - 1].Node;
end;
end;
// Пускаем рекурсию
Sys_Windows_Tree(WindowTreeView.Items.AddChild(Node, Result),
GetWindow(AHandle, GW_CHILD), ALevel + 1);
//Получаем хэндл следующего (не дочернего) окна
AHandle := GetNextWindow(AHandle, GW_HWNDNEXT);
end;
end;
</pre>
<br />
Собственно все, можно запускать на выполнение:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwFxmHmUQjb4NwBs25AA1vcOf_6dxklm-pjb4Yqqp5cLnzgLN53lTwEAKy56cAR0Vc5tUC8pLhN2LUXNxarQB7WlZ3UMNgtDabqvgVezowwrx1hAIAOCjIh2c5ZyAXRhdaKT2a4XrG3rg/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwFxmHmUQjb4NwBs25AA1vcOf_6dxklm-pjb4Yqqp5cLnzgLN53lTwEAKy56cAR0Vc5tUC8pLhN2LUXNxarQB7WlZ3UMNgtDabqvgVezowwrx1hAIAOCjIh2c5ZyAXRhdaKT2a4XrG3rg/s1600/2.png" /></a></div>
<br />
<br />
<h3>
3. Анализируем результаты</h3>
<br />
Теперь сравним результаты работы обеих программ. Сделаем это следующим образом.<br />
1. Запустите программу с перехватчиком и нажмите на кнопку, отображающую диалог.<br />
2. Запустите утилиту из второй главы<br />
3. Закройте диалог первой программы, для получения результата о перехваченных окнах.<br />
<br />
Теперь сравниваем:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgABitKQhGX3eCFbzHb0e1dKCQrJsKXdgyKkjnWM1wBC4RU9KEe5PCR355lQhlA_zru8RwLnv8JeEFbl7PvLSJFLgJKtv6LIMsS1UPdAFIJZc9RX5dneobQ9fCOVqMEAax2CudeCTn9y5g/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgABitKQhGX3eCFbzHb0e1dKCQrJsKXdgyKkjnWM1wBC4RU9KEe5PCR355lQhlA_zru8RwLnv8JeEFbl7PvLSJFLgJKtv6LIMsS1UPdAFIJZc9RX5dneobQ9fCOVqMEAax2CudeCTn9y5g/s1600/3.png" /></a></div>
<br />
Красным выделено окно с классом Auto-Suggest DropDown, давайте посмотрим что оно из себя представляет:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA84mBgIGv5bLck31-m333_td8rLjMvUn9p230-uAqiILrSDM0wRP47Xw9aB1CawJzkanl3GNEmd8OkAMvYOoBcux18Pgh4_S4ZlDxHmDjg0hJTOKB_Xbjs_Mq9NTzxCpdtCuK_TcmFEs/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA84mBgIGv5bLck31-m333_td8rLjMvUn9p230-uAqiILrSDM0wRP47Xw9aB1CawJzkanl3GNEmd8OkAMvYOoBcux18Pgh4_S4ZlDxHmDjg0hJTOKB_Xbjs_Mq9NTzxCpdtCuK_TcmFEs/s1600/4.png" /></a></div>
<br />
А оно оказывается содержит в себе еще 4 окна, два скролбара, ListView, который к тому-же чайлдом держит SysHeader32. А вот это уже интересно. Хэнлы окна в обоих приложениях совпадают, но ни ListView, ни SysHeader32, даже двух скролов в первом приложении мы не видим.<br />
<br />
Но, то что мы их не видим в первом списке еще ничего не означает. Создание этих окон происходило в тот момент, когда наш перехватчик был снят, а это могло произойти только по одной причине - по причине того, что вызов CreateWindowExW может привести к рекурсивному вызову самого себя.<br />
<br />
Значит нужно реализовать код перехватчика таким образом, чтобы не требовалось снятие и восстановление перехвата.<br />
<br />
<h3>
4. Вызов перехваченной функции без снятия кода перехвата.</h3>
<br />
Давайте посмотрим на вот такую картинку из <a href="http://alexander-bagel.blogspot.ru/2013/01/intercept.html" target="_blank">прошлой статьи</a>.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPAmFGiy0Gcp1OyNw5hxrQyMgsvtOHNXupym-cwpNEm2U47HLz7EBxKlKjP_Y5IhJ40JA-NQ60OHizkLRc5ynhY4_X0-3EaeX3dbdBIUbyS29C9onCGVzH8WXaOYeZHs6bbqYLkj0v6Xc/s1600/5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPAmFGiy0Gcp1OyNw5hxrQyMgsvtOHNXupym-cwpNEm2U47HLz7EBxKlKjP_Y5IhJ40JA-NQ60OHizkLRc5ynhY4_X0-3EaeX3dbdBIUbyS29C9onCGVzH8WXaOYeZHs6bbqYLkj0v6Xc/s1600/5.png" /></a></div>
<br />
Здесь показано начало функции MessageBoxW. Самой первой инструкцией идет ничего не делающая инструкция MOV EDI, EDI, предваряющаяся пятью инструкциями NOP.<br />
<br />
Именно так в большинстве своем и выглядят функции, подготовленные к перехвату посредством HotPatch, в том числе и перехваченная нами CreateWindowExW.<br />
<br />
В случае перехвата функции вместо выделенных семи байт, занятых ничем не делающими инструкциями будет расположен следующий код:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh3FUEbx6pqhJyBZ6dEce4S6RSShiIgTPhc4weVoMdeJU7WvTzl7ylQt5xofBKy8ngILIELAbpne27IWpLAEvCVGnyH6i6YMGXpEJCgfsHd_TlddotR9yV1k24ciS5EQGYf0-Uq-v9b4h4/s1600/6.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh3FUEbx6pqhJyBZ6dEce4S6RSShiIgTPhc4weVoMdeJU7WvTzl7ylQt5xofBKy8ngILIELAbpne27IWpLAEvCVGnyH6i6YMGXpEJCgfsHd_TlddotR9yV1k24ciS5EQGYf0-Uq-v9b4h4/s1600/6.png" /></a></div>
<br />
Собственно это и есть установленный нами перехватчик.<br />
Вместо инструкции MOV EDI, EDI размещен код JMP -7, передающий управление на предыдущую инструкцию.<br />
Вместо пяти инструкций NOP, расположен прыжок на начало функции перехватчика.<br />
<br />
Если мы начнем выполнение не с адреса начала функции CreateWindowExW, а с адреса ее первой полезной инструкции PUSH EBP, то мы не затронем установленный нами перехватчик, а раз так, то и нет смысла его снимать.<br />
<br />
В виде кода это выглядит таким образом:<br />
<br />
<pre class="brush:delphi">type
TCreateWindowExW = function(dwExStyle: DWORD; lpClassName: PWideChar;
lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
hWndParent: HWND; AMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall;
function InterceptedCreateWindowExW(dwExStyle: DWORD; lpClassName: PWideChar;
lpWindowName: PWideChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall;
var
S: string;
Index: Integer;
ACreateWindowExW: TCreateWindowExW;
begin
// запоминаем информацию о созданном окне
Index := -1;
if not IsBadReadPtr(lpClassName, 1) then
begin
S := 'ClassName: ' + string(lpClassName);
S := IntToStr(WindowList.Count + 1) + ': ' + S;
Index := WindowList.Add(S);
end;
// вызываем оригинальную функцию
@ACreateWindowExW := PAnsiChar(HotPathSpliceRec.FuncAddr) + LockJmpOpcodeSize;
Result := ACreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle,
X, Y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam);
// добавляем информацию о вызове в список
if Index >= 0 then
begin
S := S + ', handle: ' + IntToStr(Result);
WindowList[Index] := S;
end;
end;
</pre>
<br />
Рассчитав адрес первой полезной инструкции, равный смещению от начала функции на два байта, мы запоминаем его во временной переменной ACreateWindowExW, после чего вызываем функцию привычным нам образом.<br />
<br />
Давайте посмотрим что получится в этом случае, вот это мы ожидаем:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNY9_1bQcJTh-6MKwmkL6o_Bv7F3DwgmpHEdSo6y_bsFZUuqsA0Eb2O20iMa_TQhPkd5qfqX35JjAzQ11yzlpJFaF-KFVE87FaUhEzzYDm75Xa129cTjklptDSKWeRXUGNjEtYjlNQFQM/s1600/7.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNY9_1bQcJTh-6MKwmkL6o_Bv7F3DwgmpHEdSo6y_bsFZUuqsA0Eb2O20iMa_TQhPkd5qfqX35JjAzQ11yzlpJFaF-KFVE87FaUhEzzYDm75Xa129cTjklptDSKWeRXUGNjEtYjlNQFQM/s1600/7.png" /></a></div>
<br />
И именно это мы и находим в выдаваемом нам списке:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhh9vHtjnEEfPb8vqxoLap4Qtm5E2aUIaR_QL62bDlcSSVzm5UU2YKBimWrMZYUc5N76rplMgMafdyGCPok_gnnKyQ1u5srjgH7b_AKWLXL-suowPMd74GSvfTrcFxHj8xXrwB2qQ2H15E/s1600/8.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhh9vHtjnEEfPb8vqxoLap4Qtm5E2aUIaR_QL62bDlcSSVzm5UU2YKBimWrMZYUc5N76rplMgMafdyGCPok_gnnKyQ1u5srjgH7b_AKWLXL-suowPMd74GSvfTrcFxHj8xXrwB2qQ2H15E/s1600/8.png" /></a></div>
<br />
Ну вот мы и нашли наших "потеряшек", все таки 26 окон создается при вызове TOpenDialog, а не 14.<br />
<br />
Все дело было в пресловутом рекурсивном вызове, который можно увидеть в стеке вызова процедур, если установить брякпойнт в начале функции InterceptedCreateWindowExW.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg19j1dLJXOfVeflsRbX_1MSK-Cqtpa0TIfWbYESxtFUs-6wXC0NbSVoo1u8U0bzBIHIrFrX7hRFG76pcPm2ZLw_MRUeQhv76aS7BPTvyhrcW-KDwwTCBBZah8w95Ov2pO9QL659gwLmME/s1600/9.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg19j1dLJXOfVeflsRbX_1MSK-Cqtpa0TIfWbYESxtFUs-6wXC0NbSVoo1u8U0bzBIHIrFrX7hRFG76pcPm2ZLw_MRUeQhv76aS7BPTvyhrcW-KDwwTCBBZah8w95Ov2pO9QL659gwLmME/s1600/9.png" /></a></div>
<br />
<h3>
5. Ошибка при вызове перехватываемой функции из разных нитей.</h3>
<br />
С этой ошибкой то же все просто. Если постоянно снимать и восстанавливать перехватчик функции, то в какой-то момент нам будет выдана ошибка в функции SpliceLockJmp на инструкции "lock xchg word ptr [ecx], ax". Дело в том что в этот момент может завершиться операция возвращения атрибутов страницы по адресу перехватчика из другой нити и, не смотря на то, что мы в своей нити разрешили запись по данному адресу, реальные атрибуты страницы будут совершенно другими.<br />
<br />
Именно с таким поведением столкнулся автор этой ветки: <a href="http://forum.sources.ru/index.php?showtopic=374739" target="_blank">перехват recv</a>.<br />
<br />
Решать данную ошибку нужно таким-же способом как показано выше.<br />
Правда при этом нужно не забывать и об обработчике перехвата, он тоже должен быть ThreadSafe, но реализация обработчика остается на ваше усмотрение.<br />
<br />
<h3>
6. Всегда ли можно пропустить первые два байта перехватываемой функции?</h3>
<br />
Интересный вопрос и ответ на него - нет, не всегда.<br />
Когда функции подготавливаются к перехвату по методу HotPatch, Microsoft гарантирует только то, что перед ними всегда будет пять инструкций NOP и каждая такая функция будет начинаться с двухбайтовой инструкции. Больше нам ничего не гарантируется.<br />
<br />
Если рассмотреть код MessageBoxW или CreateWindowExW, то можно увидеть что первая их полезная инструкция PUSH EBP занимает один байт. Таким образом, раз она не удовлетворяет условиям, тело данной функции предваряется пустым вызовом MOV EDI, EDI. Тоже будет верно и для функций начинающихся с инструкций длиной три и более байт. Однако, если функция начинается с двухбайтовый инструкции, не имеет смысла раздувать ее тело пустой заглушкой, ведь все условия для HotPatch соблюдены (пять NOP и 2 байта).<br />
<br />
В этом случае, если мы применим способ описанный выше, ничего кроме ошибки нам увидеть не удастся.<br />
<br />
Пример такой функции - RtlCreateUnicodeString.<br />
Она начинается с полезной инструкции PUSH $0C.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSkjvjONpxmXMl9mzNeBaBk4_oz122VE51VO6KzckJe2uYyJvqUfMSyd4eVtkruxYhttSIicVsF2YLE0Jq-b_NjM9sTq1gP3IK2aCPSTys_ZepsiJv9Heh-g_rfPWmJl422NS3q35Ueuc/s1600/10.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSkjvjONpxmXMl9mzNeBaBk4_oz122VE51VO6KzckJe2uYyJvqUfMSyd4eVtkruxYhttSIicVsF2YLE0Jq-b_NjM9sTq1gP3IK2aCPSTys_ZepsiJv9Heh-g_rfPWmJl422NS3q35Ueuc/s1600/10.png" /></a></div>
<br />
Самым простым решением было бы восстановление оригинальной инструкции перед вызовом оригинальной функции, но как я уже говорил с самого начала, это грозит ошибками.<br />
<br />
Стало быть перед нами встала задача - обеспечить вызов затертой инструкции и обеспечить работоспособность функции даже с установленным кодом перехвата:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZGZusBP9mRf91k3ucRuheAKdZR1QgWUFgZtMnDXX3Aas7ZA3tgffnsV-WLzxYj2LgGDYgIKi3aROZOrrCkLbOxiSx_Wb7yTMsBobWTYh_egsCINuFzFCV-9dso-eycm4L7XcvntIu5zU/s1600/11.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZGZusBP9mRf91k3ucRuheAKdZR1QgWUFgZtMnDXX3Aas7ZA3tgffnsV-WLzxYj2LgGDYgIKi3aROZOrrCkLbOxiSx_Wb7yTMsBobWTYh_egsCINuFzFCV-9dso-eycm4L7XcvntIu5zU/s1600/11.png" /></a></div>
<br />
В принципе машинный код затертой инструкции у нас есть и хранится в структуре HotPathSpliceRec.LockJmp, но вызвать напрямую мы ее не можем по нескольким причинам.<br />
<br />
Ну во первых данная структура расположена в куче (ну точнее не в куче, а в выделенной памяти, т.к. Delphi не работает с механизмом Heap напрямую) у которой нет атрибутов исполнения, т.е. если мы каким-то образом выполним CALL по адресу HotPathSpliceRec.LockJmp то получим ошибку.<br />
<br />
Можно конечно выставить правильные атрибуты страницы, но это слишком топорно, все-же исполняемый код не должен перемешиваться с областью данных.<br />
<br />
Во вторых даже если мы и передадим выполнение на эту инструкцию, мы должны после нее заставить выполнится инструкцию JMP на правильный адрес (в данном случае это будет $77B062FB, см. предыдущую картинку) с учетом оффсета вызываемой инструкции.<br />
<br />
В третьих, помимо вызова, мы должны разместить на стеке в правильном порядке параметры, передаваемые вызываемой функции, что как минимум приведет нас к необходимости использования асм вставок.<br />
<br />
<b>Попробуем решить все по порядку.</b><br />
<br />
Чтобы не связываться с передачей параметров из асм вставки мы можем реализовать некую функцию-трамплин, возложив эту задачу на компилятор.<br />
<br />
Т.е. грубо пишем перехватчик таким образом:<br />
<br />
<pre class="brush:delphi">function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
SourceString: PWideChar): Integer; stdcall;
begin
asm
db $90, $90, $90, $90, $90, $90, $90
end;
end;
function InterceptedRtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
SourceString: PWideChar): Integer; stdcall;
begin
Result := TrampolineRtlCreateUnicodeString(DestinationString, SourceString);
ShowMessage(DestinationString^.Buffer);
end;
</pre>
<br />
В данном случае перехватчик будет заниматься вызовом трамплина и логированием.<br />
<br />
Внутри функции-трамплина зарезервировано 7 байт, что как раз хватит нам для записи двухбайтовой затертой инструкции и пятибайтовой NEAR JMP.<br />
Сама функция расположена в области кода, и с ее вызовом затруднений возникнуть не должно.<br />
<br />
А теперь важный нюанс.<br />
Если писать эти 7 байт на место зарезервированного блока, то мы столкнемся с одной неприятной особенностью Delphi. Дело в том что компилятор Delphi практически всегда генерирует для функций пролог и эпилог.<br />
<br />
К примеру допустим после патча код нашей функции стал выглядеть таким образом:<br />
<br />
<pre class="brush:delphi">function TrampolineRtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
SourceString: PWideChar): Integer; stdcall;
begin
asm
push $0C // выполняем затертый параметр
jmp $77B062FB // делаем прыжок на правильную инструкцию
end;
end;
</pre>
<br />
В действительности он превратится в следующее:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1gy4RMRzcwHeUdC7S1SoBE_0uzY_QsFy8aOBq9IVb0w6cHSMuPc4Wn5KkU-ECaNtZHLyPGhNEOaqtGWgCZE316QDzdZnonUEO04AoOP0AMdnkbWNz3Ihv4_CF2GPj-EeMLUR77fjg4ls/s1600/12.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1gy4RMRzcwHeUdC7S1SoBE_0uzY_QsFy8aOBq9IVb0w6cHSMuPc4Wn5KkU-ECaNtZHLyPGhNEOaqtGWgCZE316QDzdZnonUEO04AoOP0AMdnkbWNz3Ihv4_CF2GPj-EeMLUR77fjg4ls/s1600/12.png" /></a></div>
<br />
Т.е. на стеке, вместо двух параметров DestinationString и SourceString будут размещены значения регистров EBP и ECX, что приведет в результате к абсолютно не предсказуемым последствиям.<br />
<br />
Этого нам абсолютно не нужно, поэтому мы поступим проще, а именно код трамплина будет писаться прямо с начала данной функции, перезатирая инструкции пролога функции.<br />
<br />
Ну а ведь в действительности, данные инструкции нам абсолютно не нужны, т.к. после прыжка в тело перехваченной функции и ее выполнения, управление вернется не в искореженную нашими действиями функцию-трамплин, а непосредственно в то место, откуда производился ее вызов, т.е. в функцию - обработчик перехвата.<br />
<br />
Таким образом реализуем инициализацию перехватчика следующим способом:<br />
<br />
<pre class="brush:delphi">// процедура инициализирует структуру для установки перехвата и подготавливает трамплин для вызова
procedure InitHotPatchSpliceRecEx(const LibraryName, FunctionName: string;
InterceptHandler, Trampoline: Pointer; out HotPathSpliceRec: THotPachSpliceData);
var
OldProtect: DWORD;
TrampolineSplice: TNearJmpSpliceRec;
begin
// запоминаем оригинальный адрес перехватываемой функции
HotPathSpliceRec.FuncAddr :=
GetProcAddress(GetModuleHandle(PChar(LibraryName)), PChar(FunctionName));
// читаем два байта с ее начала, их мы будем перезатирать
Move(HotPathSpliceRec.FuncAddr^, HotPathSpliceRec.LockJmp, LockJmpOpcodeSize);
// Подготавливаем трамплин
VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize,
PAGE_EXECUTE_READWRITE, OldProtect);
try
Move(HotPathSpliceRec.LockJmp, Trampoline^, LockJmpOpcodeSize);
TrampolineSplice.JmpOpcode := JMP_OPKODE;
TrampolineSplice.Offset := PAnsiChar(HotPathSpliceRec.FuncAddr) -
PAnsiChar(Trampoline) - NearJmpSpliceRecSize;
Trampoline := PAnsiChar(Trampoline) + LockJmpOpcodeSize;
Move(TrampolineSplice, Trampoline^, SizeOf(TNearJmpSpliceRec));
finally
VirtualProtect(Trampoline, LockJmpOpcodeSize + NearJmpSpliceRecSize,
OldProtect, OldProtect);
end;
// инициализируем опкод JMP NEAR
HotPathSpliceRec.SpliceRec.JmpOpcode := JMP_OPKODE;
// рассчитываем адрес прыжка (поправка на NearJmpSpliceRecSize не нужна,
// т.к. адрес находится уже со смещением)
HotPathSpliceRec.SpliceRec.Offset :=
PAnsiChar(InterceptHandler) - PAnsiChar(HotPathSpliceRec.FuncAddr);
end;
</pre>
<br />
Сама инициализация и вызов перехваченной функции выглядит следующим образом:<br />
<br />
<pre class="brush:delphi">type
UNICODE_STRING = record
Length: WORD;
MaximumLength: WORD;
Buffer: PWideChar;
end;
PUNICODE_STRING = ^UNICODE_STRING;
function RtlCreateUnicodeString(DestinationString: PUNICODE_STRING;
SourceString: PWideChar): BOOLEAN; stdcall; external 'ntdll.dll';
...
procedure TForm2.FormCreate(Sender: TObject);
begin
// инициализируем структуру для перехватчика и трамплин
InitHotPatchSpliceRecEx('ntdll.dll', 'RtlCreateUnicodeString',
@InterceptedRtlCreateUnicodeString, @TrampolineRtlCreateUnicodeString,
HotPathSpliceRec);
// пишем прыжок в область NOP-ов
SpliceNearJmp(PAnsiChar(HotPathSpliceRec.FuncAddr) - NearJmpSpliceRecSize,
HotPathSpliceRec.SpliceRec);
end;
procedure TForm2.Button1Click(Sender: TObject);
var
US: UNICODE_STRING;
begin
// перехватываем RtlCreateUnicodeString
SpliceLockJmp(HotPathSpliceRec.FuncAddr, LOCK_JMP_OPKODE);
try
RtlCreateUnicodeString(@US, 'Test UNICODE String');
finally
// снимаем перехват
SpliceLockJmp(HotPathSpliceRec.FuncAddr, HotPathSpliceRec.LockJmp);
end;
end;
</pre>
<br />
Теперь можно нажать на кнопку и увидеть результат перехвата в виде сообщения.<br />
<br />
<h3>
В качестве заключения</h3>
<br />
В итоге вариант реализации сплайсинга, показанный в шестой главе, является наиболее универсальным в случае перехвата функций, подготовленных к HotPatch-у. Он будет работать корректно и в случае заглушки MOV EDI, EDI и в случае наличия полезной инструкции в начале перехватываемой функции. Он не подвержен ошибкам, описанным в самом начале статьи, но правда перехватить обычные функции при помощи данного алгоритма не получится, впрочем об этом я уже <a href="http://alexander-bagel.blogspot.ru/2013/01/intercept.html#splice" target="_blank">писал ранее</a>.<br />
<br />
Извиняюсь что приходится дробить информацию на куски и выдавать не все сразу, но как мне посоветовали еще год назад, лучше давать материал малыми порциями, чтобы было время для его переваривания :)<br />
<br />
С другой стороны если собрать весь материал в кучу, то во первых это займет достаточно продолжительное время, которого у меня нет в наличии, а во вторых приведет к его нечитабельности ввиду большого объема (прецеденты были).<br />
Поэтому лучше так.<br />
<br />
Исходный код к примерам можно забрать по <a href="http://rouse.drkb.ru/blog/intercept2.zip" target="_blank">данной ссылке</a>.<br />
<br />
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
---</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
<br /></div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
© Александр (Rouse_) Багель</div>
<div style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">
Май, 2013</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com8tag:blogger.com,1999:blog-2374465879949372415.post-62859042278502819942013-04-26T20:47:00.001+04:002013-04-28T15:31:27.966+04:00Атомарные операции<div dir="ltr" style="text-align: left;" trbidi="on">
Буквально на днях ко мне обратились с вопросом.<br />
<br />
А зачем нужен префикс LOCK, или его аналог InterlockedDecrement при вызове процедуры _LStrClr из модуля System. Данная процедура декрементирует счетчик ссылок строки и при его обнулении освобождает память, ранее занятую строкой.<br />
<br />
Суть вопроса была такова - практически невозможно представить себе ситуацию, когда строка потеряет рефы одновременно из двух нитей, а стало быть атомарная операция в данном случае избыточна.<br />
<br />
В принципе предпосылка интересная, но...<br />
<br />
<a name='more'></a><br />
Но ведь мы передаем строку в класс нити.<br />
Это как минимум приводит к увеличению refCnt, а стало быть мы можем "попасть" на MemLeak в том случае, если бы не использовались атомарные операции при декременте счетчика ссылок.<br />
<br />
Это демонстрирует нам код _LStrClr<br />
<br />
<pre class="brush:delphi">procedure _LStrClr(var S);
{$IFDEF PUREPASCAL}
var
P: PStrRec;
begin
if Pointer(S) <> nil then
begin
P := Pointer(Integer(S) - Sizeof(StrRec));
Pointer(S) := nil;
if P.refCnt > 0 then
if InterlockedDecrement(P.refCnt) = 0 then
FreeMem(P);
end;
end;
{$ELSE}
asm
{ -> EAX pointer to str }
MOV EDX,[EAX] { fetch str }
TEST EDX,EDX { if nil, nothing to do }
JE @@done
MOV dword ptr [EAX],0 { clear str }
MOV ECX,[EDX-skew].StrRec.refCnt { fetch refCnt }
DEC ECX { if < 0: literal str }
JL @@done
LOCK DEC [EDX-skew].StrRec.refCnt { threadsafe dec refCount }
JNE @@done
PUSH EAX
LEA EAX,[EDX-skew].StrRec.refCnt { if refCnt now zero, deallocate}
CALL _FreeMem
POP EAX
@@done:
end;
{$ENDIF}
</pre>
<br />
В случае использования неатомарного декремента инструкция JNE @@done имеет огромный шанс выполниться не верно. (И она действительно выполнится не верно, если убрать LOCK префикс).<br />
<br />
Я конечно пробовал объяснить данную ситуацию примерами из интеловского мануала, где объясняется работа, но в итоге решил реализовать следующий пример (которым и смог убедить автора вопроса):<br />
<br />
<pre class="brush:delphi">program interlocked;
{$APPTYPE CONSOLE}
uses
Windows;
const
Limit = 1000000;
DoubleLimit = Limit shl 1;
var
SameGlobalVariable: Integer;
function Test1(lpParam: Pointer): DWORD; stdcall;
var
I: Integer;
begin
for I := 0 to Limit - 1 do
asm
lea eax, SameGlobalVariable
inc [eax] // обычный инкремент
end;
end;
function Test2(lpParam: Pointer): DWORD; stdcall;
var
I: Integer;
begin
for I := 0 to Limit - 1 do
asm
lea eax, SameGlobalVariable
lock inc [eax] // атомарный инкремент
end;
end;
var
I: Integer;
hThread: THandle;
ThreadID: DWORD;
begin
// Неатомарное увеличение значения переменной SameGlobalVariable
SameGlobalVariable := 0;
hThread := CreateThread(nil, 0, @Test1, nil, 0, ThreadID);
for I := 0 to Limit - 1 do
asm
lea eax, SameGlobalVariable
inc [eax] // обычный инкремент
end;
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
if SameGlobalVariable <> DoubleLimit then
Writeln('Step one failed. Expected: ', DoubleLimit, ' but current: ', SameGlobalVariable);
// Атомарное увеличение значения переменной SameGlobalVariable
SameGlobalVariable := 0;
hThread := CreateThread(nil, 0, @Test2, nil, 0, ThreadID);
for I := 0 to Limit - 1 do
asm
lea eax, SameGlobalVariable
lock inc [eax] // атомарный инкремент
end;
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
if SameGlobalVariable <> DoubleLimit then
Writeln('Step two failed. Expected: ', DoubleLimit, ' but current: ', SameGlobalVariable);
Readln;
end.
</pre>
<br />
Суть примера - есть некая глобальная переменная SameGlobalVariable (она выступает в роли счетчика ссылок строки из изначальной постановки задачи) и выполняются изменения ее значения в обычном и атомарном режимах с использованием нити.<br />
<br />
Здесь наглядно видно различия между двумя режимами работы.<br />
В консоли вы увидите примерно следующее:<br />
<blockquote class="tr_bq">
Step one failed. Expected: 2000000 but current: 1018924</blockquote>
<div>
Ошибки по второму варианту реализации вы не увидите никогда :)</div>
<div>
<br /></div>
<div>
Кстати первый вариант может использоваться в качестве достаточно хорошего рандомизатора (о котором я говорил в предыдущих статьях).</div>
<div>
<br /></div>
<div>
<b>Резюмируя:</b><br />
<b><br /></b></div>
<div>
Анализ исходного кода системных модулей Delphi и VCL в частности, иногда может вам дать гораздо больше информации, чем предположения о том как оно работает на самом деле и это факт, но...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/IAVU5AjvCDk?feature=player_embedded' frameborder='0'></iframe></div>
<br /></div>
<div>
<br /></div>
<div>
Нет, это не факт, это больше чем факт — так оно и было на самом деле ©</div>
<div>
<br /></div>
<div>
---</div>
<div>
<br /></div>
<div>
© Александр (Rouse_) Багель</div>
<div>
Апрель, 2013</div>
<div>
<br /></div>
</div>Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com8tag:blogger.com,1999:blog-2374465879949372415.post-86731146518649901562013-04-22T22:02:00.000+04:002013-04-23T00:36:59.844+04:00Использование Lua скриптов в составе ПО<div dir="ltr" style="text-align: left;" trbidi="on">
Представьте себе ситуацию, ваше приложение реализует достаточно большой функционал для пользователя и он им пользуется достаточно успешно на протяжении достаточно большого периода времени. Но вот наступает момент, когда этого функционала пользователю становится недостаточно и он начинает просить немного расширить возможности ПО.<br />
<br />
Выливается это обычно в перекомпиляцию проекта под новые требования.<br />
Правда обычно проект в релизной стадии находится крайне редко, обычно он разобран. Нужно срочно собирать все хвосты и подчищать все нюансы, чтобы выкатить новый релиз заказчику. (Вариант с откатом на старый бранч - не рассматриваю :)<br />
А если проект находится под защитой, помимо этого потребуется перевыписать лицензии на него (т.к. контрольные суммы исполняемых файлов изменились) и прочее-прочее.<br />
Однако, если предусмотреть вариант расширения функционала ПО на базе уже имеющихся возможностей, вполне можно избежать лишних телодвижений.<br />
<br />
Такой функционал давно применяется разработчиками, ну к примеру возьмем системы генерации отчетов. В них давно не требуется пересобирать приложение для создания абсолютно нового формата отчета под изменившиеся требования пользователя - достаточно сгенерировать новый шаблон.<br />
<br />
Ну или в качестве другого варианта возьмем игры - в них разработчики пишут некий базовый фреймворк, а внутреннее наполнение производится скриптами, в которых и содержится большая часть игрового мира.<br />
Хотите новый уровень игрушки - пожалуйста, вот вам новый набор скриптов (утрирую, конечно) :)<br />
<br />
Да в принципе можно взять и более приземленные темы (по крайней мере для меня).<br />
К примеру возьмем те же отладчики. Практически в каждом из них есть собственный скриптовый язык, помогающий реверсеру выполнять большинство рутинных действий.<br />
Существуют даже хранилища подобных скриптов, доступные в онлайне, или распространяемые в виде больших паков. По сути это утилитарные подпрограммы.<br />
<br />
Вот что-то подобное мы и рассмотрим в данной статье.<br />
<br />
В качестве основы скрипта я возьму <a href="http://www.lua.org/" target="_blank">интерпретируемый язык программирования "Lua"</a>.<br />
Данный язык очень часто встречается в том инструментарии, который я использую в повседневной работе, поэтому его выбор для статьи был для меня весьма очевиден.<br />
<br />
<br />
<a name='more'></a><br />
<br />
<h3 style="text-align: left;">
1. Реализуем управляющий класс</h3>
<br />
Дабы не подготавливать очередной пример кода, который будет использоваться для повествования, я возьму уже известный моим читателям пример из <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-3.html" target="_blank">третьей части статьи: "Изучаем отладчик"</a><br />
Вкратце напомню суть данного кода (если кому лень читать прошлую статью целиком): задача была обойти защиту приложения crackme.exe. Решалась эта задача посредством использования отладчика перехватывающего обработчик кнопки и выставляющего правильный флаг, на который и реагировала команда перехода JE.<br />
<br />
Скачайте демопример к ней и посмотрите на реализацию DegugCoreTest.<br />
Достаточно много кода, а ведь по сути нам нужно было выполнить всего лишь несколько действий, которые можно изобразить в виде вот такого скрипта:<br />
<pre class="brush:delphi">-- закрывать процесс после завершения работы лоадера не будем
CloseDebugProcessOnFree = false;
-- инициализируем отладчик
InitProcess(true);
-- прячем отладчик
HideDebuger();
-- запускаем процесс
Run();
-- мы на точке входа, ставим бряк на обработчик кнопки
SetHardwareBreakpoint(0x467840, hsByte, hmExecute, 0);
-- запускаем процесс
Run();
-- мы на обработчике кнопки, говорим что все нормально
SetFlag(EFLAGS_ZF, true);
-- запускаем процесс и завершаем работу лоадера
StopScript();
</pre>
Если бы у нас была возможность выполнить такой скрипт мы бы выполнили все условия задачи из оригинальной статьи всего за семь команд!<br />
Более того, если перекомпилировать оригинальное приложение crackme.exe, изменится адрес обработчика кнопки, что приведет к неработоспособности лоадера.<br />
В случае самостоятельно реализации лоадера, показанного в статье, нам пришлось бы перекомпилировать и его с целью исправить данный адрес на валидный, а в скрипте нам потребуется просто изменить только одну строчку.<br />
<br />
Понравилась идея?<br />
Тогда попробуем это реализовать :)<br />
<br />
Для этого необходимо реализовать следующую архитектуру:<br />
1. Непосредственно сам отладчик, которым будем управлять.<br />
2. Класс, посредством которого происходит управление<br />
3. Модуль обеспечивающий связку управляющего класса со скриптом<br />
4. Сам скрипт<br />
5. Утилитарные модули<br />
<br />
Пункт первый у нас есть (это отладчик из прошлой статьи), а сам скрипт я показал чуть выше.<br />
<br />
Как будет реализован второй пункт:<br />
1. Данному классу нужно знать об отладчике и уметь с ним работать.<br />
2. Данный класс должен предоставлять необходимый перечень функций, которые мы можем вызывать из скрипта.<br />
<br />
Вкратце выглядеть он будет таким вот образом:<br />
<pre class="brush:delphi">type
TScriptProvider = class
private
// переменные, необходимые для работы класса
FDebuger: TFWDebugerCore;
FProcessPath: string;
FBreakOnEP: Boolean;
FCurrentThreadIndex: Integer;
FStopScript: Boolean;
protected
// обработчики отладчика
procedure OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
var ReleaseBreakpoint: Boolean);
procedure OnHardwareBreakPoint(Sender: TObject; ThreadIndex: Integer;
ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: THWBPIndex;
var ReleaseBreakpoint: Boolean);
procedure OnUnknownBreakPoint(Sender: TObject; ThreadIndex: Integer;
ExceptionRecord: Windows.TExceptionRecord);
procedure OnIDLE(Sender: TObject);
public
// основной метод, через который запускается скрипт
procedure ExecuteScript(Debuger: TFWDebugerCore; const ExePath, ScriptPath: string);
public
// функции и процедуры доступные скрипту
procedure InitProcess(BreakOnEP: Boolean);
function GetProcess: THandle;
function Run: Boolean;
procedure SetHardwareBreakpoint(Address: Pointer;
Size: THWBPSize; Mode: THWBPMode; HWIndex: THWBPIndex);
procedure SetFlag(Flag: DWORD; Value: Boolean);
procedure StopScript;
end;
</pre>
Работа с отладчиком реализуется через перекрытие его событий.<br />
Для описанного выше скрипта достаточно перекрыть всего три события:<br />
OnBreakPoint и OnHardwareBreakPoint - останавливают выполнение функции Run при достижении точки остановки.<br />
OnIDLE - для возможности работы функции StopScript, корректно завершающую работу отладчика.<br />
<br />
В реальности сам класс предоставляет для работы всего лишь один единственный метод: ExecuteScript, в который передается созданый экземпляр отладчика, путь к отлаживаемому приложению и путь к файлу со скриптом.<br />
<br />
Остальные публичные методы предназначены для вызова из самого скрипта.<br />
Более подробно методы работы с отладчиком, реализованным в виде класса TFWDebugerCore, были рассмотрены во <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-2.html" target="_blank">второй части статьи "Изучаем отладчик"</a>, поэтому заострять на них внимание я не буду.<br />
<br />
Рассмотрим доступные скрипту функции по порядку:<br />
<pre class="brush:delphi">procedure TScriptProvider.InitProcess(BreakOnEP: Boolean);
begin
FBreakOnEP := BreakOnEP;
if FDebuger.DebugProcessData.ProcessID = 0 then
if not FDebuger.DebugNewProcess(FProcessPath, True) then
RaiseLastOSError;
FDebuger.CloseDebugProcessOnFree := luaCloseDebugProcessOnFree;
end;
</pre>
Запускает отладку нового процесса вызовом DebugNewProcess и выставляет флаг, ориентируясь на который принимается решение, завершать отлаживаемый процесс при закрытии отладчика или нет.<br />
Параметр BreakOnEP указывает отладчику нужно ли останавливаться на точке входа в приложение или нет. Он будет использоваться в функции Run, поэтому пока мы его просто запомним в соответствующей переменной класса TScriptProvider.<br />
<pre class="brush:delphi">function TScriptProvider.Run: Boolean;
begin
FDebuger.RunMainLoop;
Result := FDebuger.DebugProcessData.AttachedFileHandle <> 0;
end;
</pre>
Запускает отладочный цикл.<br />
После окончания работы отладочного цикла проверяет, доступно ли еще приложение или нет, ориентируясь на поле DebugProcessData.AttachedFileHandle отладчика.<br />
<br />
Здесь есть нюанс. Как вы помните, процедура RunMainLoop в оригинальной статье крутила отладочный цикл до упора, пока мы не завершали отлаживаемое приложение вызовом StopDebug. В данном случае нам такое поведение не подойдет, так как после вызова функции Run из скрипта, должны производится другие действия над отлаживаемым приложением, поэтому в состав класса TFWDebugerCore был введен метод AbortMainLoop, прерывающий выполнение процедуры RunMainLoop без завершения процесса отладки.<br />
<br />
Логика функции Run проста, она выполняет отладочный цикл до тех пор, пока не произошла остановка на любой точке остановки, размещенной непосредственно нами.<br />
Для этого используются события:<br />
<pre class="brush:delphi">procedure TScriptProvider.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
var ReleaseBreakpoint: Boolean);
begin
FCurrentThreadIndex := ThreadIndex;
if not FBreakOnEP then
begin
ReleaseBreakpoint := True;
if ExceptionRecord.ExceptionAddress =
Pointer(FDebuger.DebugProcessData.EntryPoint) then Exit;
end;
FDebuger.AbortMainLoop;
end;
</pre>
и<br />
<pre class="brush:delphi">procedure TScriptProvider.OnHardwareBreakPoint(Sender: TObject;
ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord;
BreakPointIndex: THWBPIndex; var ReleaseBreakpoint: Boolean);
begin
FCurrentThreadIndex := ThreadIndex;
FDebuger.AbortMainLoop;
end;
</pre>
Логика этих двух обработчиков событий тривиальна, просто запоминается переменная ThreadIndex во внутреннем поле (т.к. наш скрипт ничего о ней не знает) и происходит остановка отладочного цикла вызовом AbortMainLoop.<br />
Ну и в обработчике OnBreakPoint происходит обработка ранее запомненного флага BreakOnEP. Если произошло прерывание на точке входа, а в скрипте сказано что на нем останавливаться не нужно, производиться выход из обработчика не вызывая AbortMainLoop.<br />
<br />
Второй нюанс функции Run связан с проверкой наличия приложения, проверкой параметра AttachedFileHandle. Это необходимо для того чтобы вовремя прекратить работу скрипта, если мы закрыли отлаживаемое приложение руками, а наш лоадер все еще работает.<br />
<pre class="brush:delphi">procedure TScriptProvider.SetHardwareBreakpoint(Address: Pointer;
Size: THWBPSize; Mode: THWBPMode; HWIndex: THWBPIndex);
begin
FDebuger.SetHardwareBreakpoint(FCurrentThreadIndex, Address, Size, Mode, HWIndex, '');
end;
</pre>
Производит установку HBP ориентируясь на переданные из скрипта параметры.<br />
<pre class="brush:delphi">procedure TScriptProvider.SetFlag(Flag: DWORD; Value: Boolean);
begin
FDebuger.SetFlag(FCurrentThreadIndex, Flag, Value);
end;
</pre>
Устанавливает указанный в скрипте флаг процессора в требуемое состояние.<br />
<pre class="brush:delphi">procedure TScriptProvider.StopScript;
begin
FStopScript := True;
Run;
end;
</pre>
Завершает работу отладчика, инициализируя флаг FStopScript, на который ориентируется обработчик OnIdle.<br />
<pre class="brush:delphi">procedure TScriptProvider.OnIDLE(Sender: TObject);
begin
if FStopScript then
FDebuger.StopDebug;
end;
</pre>
Ну вот собственно и все - класс для управления отладчиком готов.<br />
Он очень простой и очень легко расширяемый.<br />
Для удобства доступа к данному классу реализован синглтон:<br />
<pre class="brush:delphi">function ScriptProvider: TScriptProvider;
</pre>
<h3 style="text-align: left;">
2. Подключаем шлюз</h3>
<br />
Теперь нужно реализовать механизм, интерпретирующий команды скрипта и управляющий данным классом.<br />
<br />
Скриптовый движок, конечно, с нуля писать не нужно.<br />
Как я и сказал ранее, мы рассматриваем только вариант Lua.<br />
Использовать его можно как в нативном варианте (используя напрямую соответствующие API библиотеки lua.dll), так и посредством шлюзовых оберток.<br />
Ради интереса я даже пощупал нативный вариант..., но времени на изучение всех вкусностей не хватило :)<br />
<br />
Выбирая альтернативные варианты, я наткнулся на одну интересную разработку нашего программиста Дмитрия Мозулева aka DevilDevil, именуемую CrystalLUA.<br />
<a href="http://www.gamedev.ru/projects/forum/?id=140784">http://www.gamedev.ru/projects/forum/?id=140784</a><br />
Она обеспечивает удобный для Delphi программиста шлюз между привычным нам кодом (забирая на себя всю возню с нативным API Lua) и конечным скриптом.<br />
Ближайший аналог такого шлюза, который можно привести это VCL и WinAPI.<br />
<br />
У нее конечно есть недостатки (ну а у кого их нет?), но достоинств, если честно за глаза :)<br />
<br />
Впрочем будем последовательны:<br />
Сначала посмотрим как с ней работать, а потому уже будем искать её плюсы и минусы.<br />
<br />
Итак, для подключения данной библиотеки достаточно объявить в uses модуль CrystalLUA.<br />
Доступ к экземпляру TLua необходимо реализовать самостоятельно (как сделано в примере ), либо объявив директиву LUA_INITIALIZE.<br />
В статье рассматривается первый вариант через синглтон:<br />
<pre class="brush:delphi">function LuaScript: TLua;
</pre>
Таким образом программисту не нужно отвлекаться на создание и удаление данного объекта.<br />
<br />
Пишем код.<br />
Задача данного кода описать те функции которые может вызвать скрипт.<br />
Их всего шесть:<br />
<pre class="brush:delphi">procedure luaInitProcess(const Arg: TLuaArg);
begin
ScriptProvider.InitProcess(Arg.AsBoolean);
end;
procedure luaHideDebuger;
begin
HideDebuger(ScriptProvider.GetProcess);
end;
function luaRun(const Arg: TLuaArg): TLuaArg;
begin
Result.AsBoolean := ScriptProvider.Run;
end;
procedure luaSetHardwareBreakpoint(const Args: TLuaArgs);
begin
ScriptProvider.SetHardwareBreakpoint(
Pointer(Args[0].AsInteger),
THWBPSize(Args[1].AsInteger),
THWBPMode(Args[2].AsInteger),
THWBPIndex(Args[3].AsInteger));
end;
procedure luaSetFlag(const Args: TLuaArgs);
begin
ScriptProvider.SetFlag(
Args[0].AsInteger,
Args[1].AsBoolean);
end;
procedure luaStopScript;
begin
ScriptProvider.StopScript;
end;
</pre>
Код крайне прост, все функции выступают шлюзом между скриптом и вызовом класса, управляющего отладчиком, представленного в виде ScriptProvider.<br />
За исключением luaHideDebuger, она вызывает процедуру HideDebuger не реализованную в ScriptProvider, но в ней ничего особого нет, данную функцию я описывал ранее <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-3.html" target="_blank">в третьей части статьи "Изучаем отладчик"</a>.<br />
<br />
Комментировать что именно делают данные шесть функций я думаю не нужно, все и так наглядно видно из их кода. Правда обратите внимание на функцию luaRun, она умеет возвращать значение, которое может использовать скрипт.<br />
На этом я заострю внимание немного позже.<br />
<br />
Также объявим переменную, значение которой можно будет модифицировать из скрипта:<br />
<pre class="brush:delphi">var
luaCloseDebugProcessOnFree: Boolean;
</pre>
Она объявлена просто для демонстрации возможностей CristalLUA и используется для указания отладчику, что делать после завершения работы (оставлять отлаживаемый процесс работающим или закрыть его).<br />
<br />
С подготовительными процедурами закончили, теперь необходимо зарегистрировать данные шесть процедур и переменную непосредственно в движке Lua, дабы он знал о них и смог их вызвать, интерпретируя скрипт.<br />
<br />
Выполняется это следующим кодом:<br />
<pre class="brush:delphi">initialization
LuaScript.RegVariable('CloseDebugProcessOnFree', luaCloseDebugProcessOnFree, TypeInfo(Boolean));
LuaScript.RegProc('InitProcess', @luaInitProcess, 1);
LuaScript.RegProc('HideDebuger', @luaHideDebuger);
LuaScript.RegProc('Run', @luaRun);
LuaScript.RegEnum(TypeInfo(THWBPSize));
LuaScript.RegEnum(TypeInfo(THWBPMode));
LuaScript.RegProc('SetHardwareBreakpoint', @luaSetHardwareBreakpoint, 4);
LuaScript.RegConst('EFLAGS_ZF', EFLAGS_ZF);
LuaScript.RegProc('SetFlag', @luaSetFlag, 2);
LuaScript.RegProc('StopScript', @luaStopScript);
</pre>
По порядку.<br />
<br />
Lua.RegVariable - регистрирует переменную, значение которой можно изменять как из самой программы, так и из скрипта (и соответственно считывать ее значение).<br />
<br />
Lua.RegProc - регистрируем в движке процедуру или функцию, с указанием ее наименования, доступного в скрипте, ее адреса (это необходимо для движка CristalLUA) и указываем количество ее параметров, дабы движок CristalLUA правильно смог работать со стеком при вызове данных процедур.<br />
Здесь правда есть нюанс. В реальности количество параметров можно вообще не указывать, данный параметр необходим для автоматического отслеживания ошибок движком CristalLUA. В частности количество параметров можно не указывать в случае использования процедур и функций со всеми параметрами по умолчанию.<br />
Если их передаваемое и принимаемое количество не совпадут, получим ошибку от движка (ну или AV если все сделали неправильно :).<br />
<br />
Lua.RegConst - аналогично RegVariable, но есть нюанс, в языке Lua нет такого понятия как константа - все есть переменные. Но CristalLUA берет эту работу на себя отслеживая изменения переменных, объявленных как константа, и выдает соответствующую ошибку при попытке их модификации.<br />
<br />
А теперь к интересным моментам:<br />
Lua.RegEnum - регистрация перечислимых типов, объявленных в Delphi коде.<br />
<br />
Обратите внимание на эту строчку скрипта:<br />
SetHardwareBreakpoint(0x467840, hsByte, hmExecute, 0);<br />
<br />
Я могу свободно использовать параметры hsByte и hmExecute и их корректно опознает движок CristalLUA т.к. при регистрации данных перечислимых типов он пробежался по перечислению через RTTI и знает о всех возможных значениях.<br />
<br />
После всего этого мы имеем на руках готовый проект, который можно запустить выполнив следующий код:<br />
<pre class="brush:delphi">var
Debuger: TFWDebugerCore;
begin
try
Debuger := TFWDebugerCore.Create(100);
try
ScriptProvider.ExecuteScript(Debuger,
ExtractFilePath(ParamStr(0)) + 'crackme\crackme.exe',
ExtractFilePath(ParamStr(0)) + 'crackme.lua');
finally
Debuger.Free;
end;
except
on E:Exception do
Writeln(E.Classname, ': ', E.Message);
end;
end.
</pre>
В результате мы получим полный аналог работы кода из <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-3.html" target="_blank">третьей части статьи "Изучаем Отладчик"</a>.<br />
<br />
Теперь нюансик с функцией Run.<br />
Как я и говорил ранее, она возвращает булевое значение.<br />
В случае ели вы запустите пример и не будете нажимать на кнопку, а просто закроете отлаживаемый процесс, то будет произведен выход из данной функции в скрипте.<br />
Но так как скрипт линеен, он попробует выполнится дальше, т.е. будет выполнена строчка:<br />
<pre class="brush:delphi">SetFlag(EFLAGS_ZF, true);
</pre>
Дабы такого не произошло можно немного видозменить концовку скрипта следующим образом:<br />
<pre class="brush:delphi">-- запускаем процесс
if not Run() then
return;
end;
-- мы на обработчике кнопки, говорим что все нормально
SetFlag(EFLAGS_ZF, true);
-- запускаем процесс и завершаем работу лоадера
StopScript();
</pre>
<br />
<h3 style="text-align: left;">
3. Подключаем обработчики событий</h3>
<br />
А теперь рассмотрим пример, описываемый во <a href="http://alexander-bagel.blogspot.ru/2012/11/debuger-2.html" target="_blank">второй части статьи "Изучаем Отладчик"</a>, а именно:<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
begin
try
asm
int 3
end;
ShowMessage('Debuger detected.');
except
ShowMessage('Debuger not found.');
end;
end;
</pre>
Как вы помните, суть его сводилась к тому, что генерировалось отладочное исключение посредством вызова третьего прерывания (INT 3) и в случае присутствия отладчика, оно "проглатывалось" не давая коду перейти в обработчик except.<br />
<br />
Данная ситуация решалась вот таким кодом:<br />
<pre class="brush:delphi">procedure TdlgDebuger.OnUnknownBreakPoint(Sender: TObject;
ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord);
var
ApplicationBP: Boolean;
begin
ApplicationBP :=
(DWORD(ExceptionRecord.ExceptionAddress) > FCore.DebugProcessData.EntryPoint) and
(DWORD(ExceptionRecord.ExceptionAddress) < $500000);
Writeln;
if ApplicationBP then
begin
Writeln(Format('!!! --> Unknown application breakpoint at addr 0X%p',
[ExceptionRecord.ExceptionAddress]));
Writeln('!!! --> Exception not handled.');
FCore.ContinueStatus := DBG_EXCEPTION_NOT_HANDLED;
end
else
begin
Writeln(Format('!!! --> Unknown breakpoint at addr 0X%p',
[ExceptionRecord.ExceptionAddress]));
Writeln('!!! --> Exception handled.');
FCore.ContinueStatus := DBG_CONTINUE;
end;
Writeln;
end;
</pre>
Т.е. при получении неопознанного исключения, в случае, если оно располагалось в пределах секции памяти, в которой хранится образ отлаживаемого приложения, выставлялся флаг DBG_EXCEPTION_NOT_HANDLED.<br />
Таким образом данное исключение не обрабатывалось отладчиком и происходил переход в обработчик исключения непосредственно в отлаживаемом процессе.<br />
<br />
А теперь, как можно смоделировать данный код на базе LUA скрипта:<br />
Показанный выше скрипт был полностью линейный и он ничего не знал о событийной модели исполнения кода.<br />
Но мы можем это исправить попробовав реализовать вот такой скрипт:<br />
<pre class="brush:delphi">function OnUnknownBreakPoint(ExceptionRecord)
DEBUG = ExceptionRecord.ExceptionAddress;
if (ExceptionRecord.ExceptionAddress > 0x400000) and (ExceptionRecord.ExceptionAddress < 0x500000) then
ContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
else
ContinueStatus = DBG_CONTINUE
end;
end;
CloseDebugProcessOnFree = true;
-- инициализируем отладчик
InitProcess(false);
-- запускаем процесс
Run();
</pre>
Т.е. мы в скрипте объявим функцию, которая будет вызываться нашим лоадером при получении некоего события (в данном случае OnUnknownBreakPoint).<br />
<br />
Здесь есть нюанс:<br />
Функции и процедуры в скрипте всегда должны идти первыми, линейный код, исполняемый скриптом, в самом конце (таковы требования языка).<br />
<br />
Функции InitProcess и Run нами уже были реализованы в прошлой главе, осталось научится делать вызов функции OnUnknownBreakPoint из приложения, передавая управление скрипту.<br />
<br />
Для этого необходимо модифицировать код примерно таким образом:<br />
<pre class="brush:delphi">procedure TScriptProvider.ExecuteScript(Debuger: TFWDebugerCore;
const ExePath, ScriptPath: string);
begin
...
FDebuger.OnUnknownBreakPoint := OnUnknownBreakPoint;
...
end;
procedure TScriptProvider.OnUnknownBreakPoint(Sender: TObject;
ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord);
var
Args: TLuaArgs;
begin
if not Lua.ProcExists('OnUnknownBreakPoint') then Exit;
SetLength(Args, 1);
Args[0].AsRecord := LuaRecord(@ExceptionRecord, Lua.RecordInfo['TExceptionRecord']);
LuaScript.Call('OnUnknownBreakPoint', Args);
FDebuger.ContinueStatus := luaContinueStatus;
end;
</pre>
Т.е. грубо перекрываем обработчик OnUnknownBreakPoint у самого отладчика, а потом смотрим, есть ли такой в самом скрипте, если есть, вызываем его, передавая структуру ExceptionRecord в виде параметра.<br />
После вызова обработчика в скрипте, выставляем ContinueStatus измененный также в скрипте (извиняюсь за тафталогию :)<br />
<br />
Конечно передача параметров в обработчик может показаться не привычной практически всем, кто ранее не сталкивался с CristalLUA, но к этому можно быстро привыкнуть.<br />
(Тем более о такой непривычности я не раз упоминал, когда используешь сторонние фреймворки, взять тот же TcxBarEditItem, описанный в статье <a href="http://alexander-bagel.blogspot.ru/2013/02/ribbon-devexpress.html" target="_blank">"Нюансы использования Ribbon от DevExpress"</a>).<br />
<br />
Впрочем по поводу непривычности, сейчас покажу еще несколько моментов.<br />
В скрипте появились две новые константы и переменная, которые мы должны зарегистрировать.<br />
С ними все просто:<br />
<pre class="brush:delphi"> LuaScript.RegConst('DBG_EXCEPTION_NOT_HANDLED', DBG_EXCEPTION_NOT_HANDLED);
LuaScript.RegConst('DBG_CONTINUE', DBG_CONTINUE);
LuaScript.RegVariable('ContinueStatus', luaContinueStatus, TypeInfo(DWORD));
</pre>
Но помимо них в скрипт передается еще и структура ExceptionRecord с полями, одно из которых (ExceptionAddress) зачитывается, а вот с этим немного сложнее.<br />
<br />
Регистрация структур выглядит следующим образом:<br />
<pre class="brush:delphi"> Info := LuaScript.RegRecord('TExceptionRecord', Pointer(SizeOf(Windows.TExceptionRecord)));
with TExceptionRecord(nil^) do
begin
Info.RegField('ExceptionAddress', @ExceptionAddress, TypeInfo(DWORD));
end;
</pre>
Здесь производится регистрация только одного поля, которое нам необходимо в скрипте, а если нужны все, код конечно немного увеличится.<br />
Здесь сказывается то, что RTTI для структур не генерируется в ранних версиях дельфи, а CristalLUA пока что заточена на Delphi 2007 и ниже.<br />
Но автор сейчас ведет работу над доработкой библиотеки под старшие версии Delphi (где RTTI для записей присутствует) и я думаю вскорости появится перекрытый вариант RegRecord, не требующий дополнительной регистрации полей структуры через RegField.<br />
<br />
<h3 style="text-align: left;">
4. Итоги</h3>
<br />
Теперь остановимся на том, нужна ли вообще CristalLUA и что она дает разработчику?<br />
Ответ прост - нужна и я вам покажу почему.<br />
<br />
Вот вам регистрация ExceptionRecord без использования CristalLUA:<br />
<pre class="brush:delphi">function TExceptionRecord__index(L: Plua_State): Integer; cdecl;
var
Identifier: string;
userdata: pointer;
begin
Result := 1;
userdata := lua_touserdata(L, 1);
if (userdata = nil) then
begin
lua_pushstring(L, 'Wrong TExceptionRecord usage');
lua_error(L);
end;
userdata := Pointer(userdata^);
Identifier := lua_tostring(L, 2);
if (Identifier = 'ExceptionAddress') then
begin
lua_pushnumber(L, DWORD(Windows.PExceptionRecord(userdata).ExceptionAddress));
end else
begin
lua_pushstring(L, PChar(Format(
'Identifier "%s" not found in TExceptionRecord instance', [Identifier])));
lua_error(L);
end;
end;
function TExceptionRecord__newindex(L: Plua_State): Integer; cdecl;
var
Identifier: string;
userdata: pointer;
begin
Result := 0;
userdata := lua_touserdata(L, 1);
if (userdata = nil) then
begin
lua_pushstring(L, 'Wrong TExceptionRecord usage');
lua_error(L);
end;
userdata := Pointer(userdata^);
Identifier := lua_tostring(L, 2);
if (Identifier = 'ExceptionAddress') then
begin
DWORD(Windows.PExceptionRecord(userdata).ExceptionAddress) := Trunc(lua_tonumber(L, 3));
end else
begin
lua_pushstring(L, PChar(Format(
'Identifier "%s" not found in TExceptionRecord instance', [Identifier])));
lua_error(L);
end;
end;
...
lua_createtable(FScript, 0, 0);
lua_pushvalue(FScript, 1);
lua_pushstring(FScript, '__index');
lua_pushcfunction(FScript, @TExceptionRecord__index);
lua_rawset(FScript, -3);
lua_pushstring(FScript, '__newindex');
lua_pushcfunction(FScript, @TExceptionRecord__newindex);
lua_rawset(FScript, -3);
lua_setmetatable(FScript, 2);
lua_setfield(FScript, LUA_GLOBALSINDEX, 'TExceptionRecord');
</pre>
Я думаю разница очевидна :)<br />
<br />
Ну а если не впечатлились, то вот вам самый первый пример из справки по CristalLUA:<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
begin
Lua.RegClass(TForm1);
Lua.RegVariable('Form1', Form1, typeinfo(TForm1));
Lua.RunScript(
'Form1 {Caption="Text", Position=poScreenCenter, Color=0x007F7F7F}'); // сразу несколько свойств
end;
</pre>
Я думаю здесь сразу понятно, сколько работы производится в "скрытом от разработчика" режиме, по регистрации всего RTTI от формы, чтобы потом можно было вот так легко взять и поменять любой понравившийся параметр из скрипта :)<br />
<br />
<h3 style="text-align: left;">
5. Плюсы и минусы</h3>
<br />
Я описал только часть плюсов, затронув только малую часть верхушки айсберга.<br />
По функционалу библиотеки и ее возможностям на самом деле можно написать приличный такой букварь.<br />
Самый огромный плюс - это саппорт, например когда мне потребовался некий функционал из Lua 5.2 автор выкатил его мне буквально за сутки, хотя я думал что у него на это уйдет примерно неделя.<br />
<br />
Ну а теперь к минусам.<br />
<br />
Во первых CristalLUA пока что заточен исключительно под ANSI варианты дельфи (2007 и ниже). Это очень большой минус, но ведется работа...<br />
<br />
Во вторых, хоть Дмитрий и постарался описать достаточно подробно функционал данной библиотеки в виде справки - она не самодостаточна. Работая с CristalLUA я столкнулся с множеством недопониманий, обойти которые, без помощи автора, я увы не смог.<br />
Но это тоже решаемо, работа над переработкой справки будет вестись и я в ней буду принимать непосредственно участие.<br />
<br />
В третьих библиотека не совсем кроссплатформенная (в пределах битности) из-за обилия асмкода. Это конечно оправдано в некоторых вещах, да и по существу, проанализировав код, я не скажу что в данном случае это был неверный подход, хотя и есть перегибы.<br />
Но в принципе 90 процентов кода Delphi программистами пишутся пока что под 32 бита и под Win.<br />
<br />
Правда есть небольшой нюанс, для 32 битных ОС не на базе Windows добавить поддержку будет достаточно просто, достаточно убрать завязку на Win32 Lua API.<br />
<br />
<h3 style="text-align: left;">
6. Ну и в заключение.</h3>
<br />
Я надеюсь что по минимуму справился с поставленной задачей и заинтересовал вас в данной разработке. Такие вещи обычно пишутся командами из "много человек", взять тот-же паскаль-скрипт.<br />
<br />
В коде движка CristalLUA я вижу очень профессиональную работу, но, к сожалению, всего лишь одного энтузиаста.<br />
<br />
Правда, очень квалифицированного энтузиаста, но даже он не сможет объять необъятное и решить все насущные задачи.<br />
<br />
Если вы заинтересовались в данной разработке и имеете свободное время, и желание помочь в развитии данного проекта - то милости просим:<br />
<br />
Контакты автора библиотеки CristalLUA:<br />
<br />
ICQ: 250481638<br />
email: <a href="mailto:softforyou@inbox.ru">softforyou@inbox.ru</a><br />
<br />
Исходный код демопримеров можно забрать <a href="http://rouse.drkb.ru/blog/lua.zip" target="_blank">здесь</a>.<br />
<br />
Удачи.<br />
<br />
---<br />
<br />
Александр (Rouse_) Багель<br />
Апрель, 2013<br />
<br /></div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com0tag:blogger.com,1999:blog-2374465879949372415.post-57261850278890426082013-03-05T23:10:00.002+04:002013-03-06T00:13:45.012+04:00Рисуем поверх TWinControl<div dir="ltr" style="text-align: left;" trbidi="on">
Это будет шуточная статья, да и задача, рассматривая в ней тоже на практике редко встречается, впрочем в каждой шутке есть только доля шутки.<br />
<br />
Когда-то давным давно один уважаемый в Delphi сообществе человек разъяснял - почему есть проблемы с выводом графики поверх контролов.<br />
<br />
И объяснял он примерно таким образом:<br />
Вот представим себе стол, пусть он будет аналогом формы (TForm) и мы возьмем фломастер и начнем на нем рисовать. Поверхность стола - это его канва (TCanvas) и на ней у нас полный простор для фантазии. А теперь бросим на стол фотографии. Они представляют из себя TImage и собой они закрыли часть рисунка на столе. Они не убрали то изображение, которое было под ними, они просто находятся поверх него, а само изображение все еще присутствует, хоть его и не видно. Фотографий много, вы их можете перекладывать одну поверх другой, выбирая нравящиеся, тем самым вы неявно работаете со свойствами BringToFront конкретного TImage выводя его на передний план.<br />
Если мы опять захотим нарисовать прямо сейчас - мы возьмем фломастер и сделаем рисунок, и нам не помешают расположенные на столе фотографии, мы просто проведем линию поверх них.<br />
Но вот мы ставим на стол тарелку - она закрывает собой и стол, и фотографии. Это TWinControl. Возьмите фломастер и попробуйте нарисовать линию поверх стола так, чтобы она отобразилась еще и на тарелке, плавно продолжая рисунок с канвы формы - тогда вы сможете понять как сложно это сделать программно :)<br />
<br />
<br />
<a name='more'></a><br />
<br />
И как-то так зашло, что однажды даже пошел спор, а можно ли это сделать или нет? :)<br />
Ну к примеру у вас есть много элементов управления на форме и нужно через них рисовать линию.<br />
Предлагалось куча вариантов такой реализации, контролы с отсечкой по регионам, оверлей, впрочем решений было много, смелых и разных но ветка угасла.<br />
<br />
Сегодня совершенно случайно увидел в одном из сообществ вопрос именно такого плана - как нарисовать поверх и вспомнил про свой давний примерчик, написанный как раз для той старой ветки.<br />
<br />
Смысл примера очень прост - если переложить на изначальное объяснение концепции, то зачем рисовать на столе, если рисунок не виден на тарелке? Пусть тарелка рисует на самой себе.<br />
<br />
С точки зрения программной реализации суть сводится к перекрытию оконной процедуры, в которой вывод графики производится уже непосредственно на канве контрола.<br />
<br />
Выглядит следующим образом:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjidVc9SJZnZznRWFqHOfgVNwawPL2xn8sW5lrGAtAZ5n3iXqBR4TlVyvrZpt6On5o7euVs4LuT7PFARzlCQgHpknebWA3nSzd3wZ8axOAtsQ1c44fd63LTjcCDpQ2jA_SbO8ER0tnB-GEp/s1600/res.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjidVc9SJZnZznRWFqHOfgVNwawPL2xn8sW5lrGAtAZ5n3iXqBR4TlVyvrZpt6On5o7euVs4LuT7PFARzlCQgHpknebWA3nSzd3wZ8axOAtsQ1c44fd63LTjcCDpQ2jA_SbO8ER0tnB-GEp/s1600/res.png" /></a></div>
<br />
Ну и сам код:<br />
<br />
<pre class="brush:delphi">function ButtonSubclassProc(hWnd: HWND; Msg: Integer;
wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
OldWndProc, LeftOffset, TopOffset: Integer;
WndRect, ParentRect, ParentClientRect: TRect;
TmpCanvas: TCanvas;
X1, Y1, X2, Y2: Integer;
begin
OldWndProc := GetWindowLong(hWnd, GWL_USERDATA);
Result := CallWindowProc(Pointer(OldWndProc), hWnd, Msg, wParam, lParam);
if Msg = WM_PAINT then
begin
GetWindowRect(hWnd, WndRect);
GetWindowRect(GetParent(hWnd), ParentRect);
GetClientRect(GetParent(hWnd), ParentClientRect);
TopOffset := (ParentRect.Bottom - ParentRect.Top) -
(ParentClientRect.Bottom - ParentClientRect.Top);
LeftOffset := (ParentRect.Right - ParentRect.Left) -
(ParentClientRect.Right - ParentClientRect.Left);
X1 := ParentClientRect.Left + LeftOffset div 2 - (WndRect.Left - ParentRect.Left);
Y1 := ParentClientRect.Top + TopOffset -
(WndRect.Top - ParentRect.Top) - LeftOffset div 2;
X2 := X1 + (ParentClientRect.Right - ParentClientRect.Left);
Y2 := Y1 + (ParentClientRect.Bottom - ParentClientRect.Top);
TmpCanvas := TCanvas.Create;
try
TmpCanvas.Handle := GetDC(hWnd);
TmpCanvas.Pen.Color := clRed;
TmpCanvas.Pen.Width := 4;
TmpCanvas.MoveTo(X1, Y1);
TmpCanvas.LineTo(X2, Y2);
finally
ReleaseDC(hWnd, TmpCanvas.Handle);
TmpCanvas.Free;
end;
end;
end;
procedure TForm1.ButtonClick(Sender: TObject);
begin
ReleaseButtons;
GenerateButtons;
Invalidate;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
DoubleBuffered := True;
GenerateButtons;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
ReleaseButtons;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin
Canvas.Pen.Color := clRed;
Canvas.Pen.Width := 4;
Canvas.MoveTo(0, 0);
Canvas.LineTo(ClientWidth, ClientHeight);
end;
procedure TForm1.FormResize(Sender: TObject);
var
I: Integer;
begin
for I := 0 to 19 do
ButtonsData[I].Invalidate;
Invalidate;
end;
procedure TForm1.GenerateButtons;
var
I: Integer;
begin
Randomize;
for I := 0 to 19 do
begin
ButtonsData[I] := TButton.Create(Self);
ButtonsData[I].Parent := Self;
ButtonsData[I].Left := Random(ClientWidth - ButtonsData[I].Width);
ButtonsData[I].Top := Random(ClientHeight - ButtonsData[I].Height);
ButtonsData[I].Caption := 'Button' + IntToStr(I + 1);
ButtonsData[I].OnClick := ButtonClick;
SetWindowLong(ButtonsData[I].Handle,
GWL_USERDATA, GetWindowLong(ButtonsData[I].Handle, GWL_WNDPROC));
SetWindowLong(ButtonsData[I].Handle,
GWL_WNDPROC, Integer(@ButtonSubclassProc));
end;
end;
procedure TForm1.ReleaseButtons;
var
I: Integer;
begin
for I := 0 to 19 do
ButtonsData[I].Free;
end;
</pre>
<br />
В нем создаются 20 кнопок, у каждой из которых перекрывается оконная процедура посредством SetWindowLong + GWL_WNDPROC. В новой оконной процедуре просто рисуем на канве каждого конкретного элемента.<br />
<br />
Попробуйте изменить размеры формы и понаблюдать за поведением линии.<br />
<br />
Ну и на этом я заканчиваю.<br />
Моя задача в публикации данного шуточного кода закончена, а у вас возможно появился новый повод для размышлений :)<br />
<br />
Исходный код забирайте здесь: <a href="http://rouse.drkb.ru/blog/draw_over_controls.zip">http://rouse.drkb.ru/blog/draw_over_controls.zip</a><br />
<br />
Удачи.</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com18tag:blogger.com,1999:blog-2374465879949372415.post-89319004932099752112013-02-26T22:29:00.000+04:002013-03-01T20:48:58.203+04:00Нюансы использования Ribbon от DevExpress<div dir="ltr" style="text-align: left;" trbidi="on">
<span style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: xx-small; line-height: 14px; text-align: justify;">Версия 1.0.3 </span><br />
<span style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: xx-small; line-height: 14px; text-align: justify;"><br /></span>
Однако ребята из Редмонда не перестают удивлять нас своими наработками в рамках пользовательского интерфейса. То у них модными были тулбары и плоские кнопки, то тулбары в градиенте и разворачивающиеся меню, лента вот недавно появилась, а в последней версии офиса опять в моду входит концепция "плоских" элементов (я уж не говорю о METRO). Причем каждый раз с новым веянием Microsoft публикует большой манускрипт, описывающий почему именная текущая концепция самая правильная.<br />
<br />
А что делать разработчикам стремящимся поддерживать GUI в актуальном состоянии в соответствии с "модой" (как делают большинство коммерческих разработчиков)? По понятным причинам полностью самостоятельно переписывать GUI слишком накладно, нам бы логику программы отладить, какой уж там GUI и прочие финтифлюшки... :)<br />
<br />
Вот здесь-то и выходят на сцену компании, которые зарабатывают на этих новомодных тенденциях, предоставляя разработчикам собственный фреймворк для реализации необходимого.<br />
<br />
Их много.<br />
Хороших, плохих - разных. Но я остановлюсь только на одной из них - это <a href="http://www.devexpress.com/" target="_blank">DevExpress</a>, наши ребята с <a href="http://www.dxrussia.ru/" target="_blank">отделом разработки в Туле</a>.<br />
<br />
Сразу оговорюсь что из хороших альтернатив есть еще продукция от компании <a href="http://www.tmssoftware.com/" target="_blank">TMS Software</a>, но я с ней не работал, ограничившись только демками/триалом, когда выбирал что нам больше подходит и работа наших ребят мне понравилась гораздо больше в плане удобства использования.<br />
<br />
В статье я буду рассматривать "DevExpress VCL ExpressBars components" версии 12.1.4 и все описанное будет относится именно к данной версии. В более ранних версиях данного пакета некоторые элементы могут отсутствовать или работать не так как описано.<br />
<br />
Собственно для чего я вообще решил писать данную статью:<br />
Я не ставил себе целью подробно рассказать о данном наборе классов, для этого есть достаточно подробная справка и хороший набор демопримеров, идущие в составе с пакетом. Я просто хочу показать некоторые, скажем так, первые шаги и дать небольшой набор базовых знаний о данном пакете, которые будут некоей отправной точкой.<br />
Так же постараюсь показать некоторые нюансы, не описанные в справке и показать небольшой набор ошибок в самом пакете, с которыми вы можете столкнуться.<br />
На полноту изложения я не претендую, ибо буду рассказывать только о том, с чем сам сталкивался в повседневной работе, ну и конечно в завершение покажу некую практическую часть по расширению функционала контролов данного пакета.<br />
<br />
В статье будут присутствовать комментарии от разработчиков данного набора компонентов. В них описаны некоторые не упомянутые мной нюансы, поэтому обращайте внимание на следующий текст.<br />
<br />
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">Тело комментария</span></blockquote>
Ну что ж, приступим...<br />
<br />
<a name='more'></a><br />
<h3 style="text-align: left;">
1. Форма и dxRibbon</h3>
<br />
После установки данного пакета в меню New появится два новых пункта, это "DevExpress VCL v12.1 Ribbon 2007 Form" и "DevExpress VCL v12.1 Ribbon 2010 Form". Оба пункта создают новую форму с уже размещенным на ней компонентом dxRiboon в стиле MS Office 2007 и 2010 соответственно.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjj2C_qLIE-VpaoGE45VIFOu0H7fz7ZzZ_yAiOV1QrpuQzTDS0A0RA7dKB9RXUfHFjS_0Yp3R71ipuvpiNEz0AXC5LXhHR5NOEqL19UPYcIwIWJ0i_d47OjOvMTpfwTWkoKO-PTlmI4rkg/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjj2C_qLIE-VpaoGE45VIFOu0H7fz7ZzZ_yAiOV1QrpuQzTDS0A0RA7dKB9RXUfHFjS_0Yp3R71ipuvpiNEz0AXC5LXhHR5NOEqL19UPYcIwIWJ0i_d47OjOvMTpfwTWkoKO-PTlmI4rkg/s1600/1.png" /></a></div>
<br />
<br />
Данные пункты подойдут скорее только в случае создания проекта с нуля. Если же уже имеется на руках рабочий проект, который требуется перевести на стиль Ribbon, то лучше все сделать самостоятельно. За одно будет более понятно как правильно настроить сам dxRibbon.<br />
<br />
Тренироваться начнем с пустого проекта, чтобы сразу увидеть некоторые нюансы использования dxRibbon, называемым а просторечии - лентой.<br />
<br />
Итак, создайте новый проект и на главной форме проекта разместите компонент dxRibbon с вкладки ExpressBars. Должно получится вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijrR8Vs7jcIcnTyr8Qxay3Is-JFhh_tN8Dtj3jgLbc75RGy9DqijgXM7VnVz5WTpfdvhd9evi6zXtHIqohm1d-d2d2krxJkoF_QZjE52U9hynOJogeavxHVCcbH3cL72RyCnqo1xJsz-U/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="130" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijrR8Vs7jcIcnTyr8Qxay3Is-JFhh_tN8Dtj3jgLbc75RGy9DqijgXM7VnVz5WTpfdvhd9evi6zXtHIqohm1d-d2d2krxJkoF_QZjE52U9hynOJogeavxHVCcbH3cL72RyCnqo1xJsz-U/s320/2.png" width="320" /></a></div>
<br />
Это только заготовка будущей ленты. Чтобы заставить ленту заработать, к ней необходимо подключить основной компонент, через который будет происходить работа со всеми элементами DevExpress VCL ExpressBar, называется он dxBarManager и расположен на той же вкладке ExpressBars. Подключается через свойство BarManager компонента dxRibbon.<br />
<br />
После подключения лента примет следующий вид:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuGMnoM1BsvYqYcEYcF7pPnn0UC5znhkVeXliZHZ0mIn8jwuMTvmK6Fs3_0r6gore9CzNZYHtCzCaSnrTZXojNmODbQy6we904lRHmBcYIhGU4jRjPtm9Ac5jFWELOaO3WFvrtr8ubges/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="139" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuGMnoM1BsvYqYcEYcF7pPnn0UC5znhkVeXliZHZ0mIn8jwuMTvmK6Fs3_0r6gore9CzNZYHtCzCaSnrTZXojNmODbQy6we904lRHmBcYIhGU4jRjPtm9Ac5jFWELOaO3WFvrtr8ubges/s320/3.png" width="320" /></a></div>
<br />
Однако если мы запустим проект, мы увидим только пустую форму. Для того чтобы отобразить ленту в положенном ей месте необходимо выполнить следующие действия.<br />
<br />
1. В <b>uses </b>главной формы необходимо добавить юнит dxRibbonForm, а саму главную форму приложения унаследовать не от TForm, а от TdxRibbonForm, примерно вот таким образом:<br />
<br />
<pre class="brush:delphi">unit Unit1;
interface
uses
Windows, ..., dxRibbonForm;
type
TForm1 = class(TdxRibbonForm)
dxRibbon1Tab1: TdxRibbonTab;
dxRibbon1: TdxRibbon;
dxBarManager1: TdxBarManager;
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
end.
</pre>
<br />
2. В свойствах dxRibbon параметр SupportNonClientDrawing установить в True.<br />
<br />
Теперь можно запускать проект и посмотреть на первые результаты нашей работы:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidpgapxfgVqT16sMlTCXbowQ3q7cDwCJ5Dc0tyon8pa00qZYjFLS6jQHUy-9t6cfL05xOEbPT8kPd44Yc-6untcUdUxbKPr-3exQt5wE9PfeTkQyxuqhn7K7-5EfdkxeYk5LLpz_3mdGc/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="212" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidpgapxfgVqT16sMlTCXbowQ3q7cDwCJ5Dc0tyon8pa00qZYjFLS6jQHUy-9t6cfL05xOEbPT8kPd44Yc-6untcUdUxbKPr-3exQt5wE9PfeTkQyxuqhn7K7-5EfdkxeYk5LLpz_3mdGc/s320/4.png" width="320" /></a></div>
<br />
Кнопка слева вверху называется Application Button. При нажатии на нее должно появляться расширенное меню приложения, но сейчас оно не настроено, на этом я остановлюсь позже. Единственный нюанс заключается в том, что если мы прямо сейчас по ней щелкнем два раза, приложение закроется. Это связано с тем что при двойном клике на данную кнопку вызывается пункт по умолчанию у стандартного меню формы, вот у этого:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg35zRcwRhhL0BHldNJW1h3iAoA6q-pESSkRSYr5JP5WR7IAQpoi_F5weOa4MZkQciz8uYEq8JkpjhCPzjPDlEu0Nf9jKGQOdGYDT_j2MFkF1zcpbgkoy4o6JxkOGrFsQNDXNzxeWgKkI4/s1600/5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="167" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg35zRcwRhhL0BHldNJW1h3iAoA6q-pESSkRSYr5JP5WR7IAQpoi_F5weOa4MZkQciz8uYEq8JkpjhCPzjPDlEu0Nf9jKGQOdGYDT_j2MFkF1zcpbgkoy4o6JxkOGrFsQNDXNzxeWgKkI4/s320/5.png" width="320" /></a></div>
<br />
... который отвечает как раз за закрытие приложения.<br />
<br />
<h3>
2. Группы, кнопки и меню</h3>
<br />
Прежде чем разбираться с остальными параметрами dxRibbon необходимо научится работать с основными элементами управления, размещаемыми на ленте.<br />
<br />
Все элементы управления, используемые в DevExpress VCL ExpressBar разделяются на три подгруппы:<br />
1. Основные элементы управления: создаются и управляются посредством dxBarManager.<br />
2. Контейнеры для элементов управления: создаются отдельно на форме.<br />
3. Различные компоненты утилитарного назначения (для наведения красоты): наследники от TComponent, размещаются так же на форме.<br />
<br />
Элементами управления являются наследники от TdxBarItem.<br />
Их много разных: dxButton, dxBarEdit. dxBarCombo и т.д.<br />
Контейнерами для них являются наследники от TdxBarComponent и TcxControl.<br />
Их так же предостаточно: это и dxRibbon, и dxPopupMenu и прочее элементы.<br />
<br />
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">Кнопка в барах - это TdxBarButton.</span><br />
<span style="color: #444444;">TdxBarEdit, TdxBarCombo и иже с ними - это устаревшие версии баровских контролов.</span><br />
<span style="color: #444444;">Желательно использовать аналоги на базе TcxBarEditItem с соответствующим значением Properties: TcxComboBoxProperties, TcxTextEditProperties, etc.</span></blockquote>
<div>
Основное и главное отличие элементов управления DevExpress VCL ExpressBar от стандартных элементов управления (тех же кнопок TButton) заключается в том, что они являются виртуальными элементами. Т.е. они присутствуют только в рамках dxBarManager-а и нигде более, а за их визуализацию (отрисовку, реакцию на пользовательский ввод) отвечает класс TdxBarItemLink. По этой причине данные элементы управления нельзя разместить произвольно на форме, только в рамках контейнера. Но именно в их виртуализации и заключается большое удобство работы с данными элементами управления.<br />
<br />
Допустим раньше, если у нас была кнопка на тулбаре, которая совершает некое действие (выводит MessageBox, к примеру), то для того чтобы вызвать это же действие из меню необходимо было создавать новый пункт меню, где уже обработчиком цеплять вызов от первой кнопки. В случае с DevExpress VCL ExpressBar создание дополнительных элементов управления с общим обработчиком не потребуется. Из виртуального контрола мы спокойно можем управлять всеми линками на него, отвечающими за визуализацию, и управлять их состоянием (к примеру нажата кнопка или нет, указывать ее Enable/Visible состояния и прочее). Если немного огрубить, данная виртуализация чем-то похожа на механизм работы TAction, только реализованная немного другим способом.<br />
<br />
Впрочем рассмотрим это на практике.<br />
<br />
Откройте BarManager и на вкладке Commands создайте элемент TdxBarLargeButton.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOZZaDAzoWcr-LcDZa6p0wOqzzyK64-eTfqdQrCBovZKBRp5lWmVYrRURO5uSv2hBbkASBxUfOTm_KHuFl6J_eLBsE-bo4abkLTDzVJmWc1xWU19SnO_jJJJ7PQQiIOP_TipMd_bHIMk4/s1600/6.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOZZaDAzoWcr-LcDZa6p0wOqzzyK64-eTfqdQrCBovZKBRp5lWmVYrRURO5uSv2hBbkASBxUfOTm_KHuFl6J_eLBsE-bo4abkLTDzVJmWc1xWU19SnO_jJJJ7PQQiIOP_TipMd_bHIMk4/s320/6.png" width="320" /></a></div>
<br />
Далее откройте вкладку событий данного элемента и назначьте новый обработчик OnClick.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgoRg5FD_0cLDfW-yJNrU5ZaI7r7RIyhhC0NkoEDTQis7Xc96AnXSmCD0vhAK2hOx06yiFQ5DupVVe-lNG3_Wp_5hhbZCH-uQrx6GD2fRzwtxPWwjPsEbjNa2l-yFt08ON6eyzek9Y9dKQ/s1600/7.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="181" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgoRg5FD_0cLDfW-yJNrU5ZaI7r7RIyhhC0NkoEDTQis7Xc96AnXSmCD0vhAK2hOx06yiFQ5DupVVe-lNG3_Wp_5hhbZCH-uQrx6GD2fRzwtxPWwjPsEbjNa2l-yFt08ON6eyzek9Y9dKQ/s320/7.png" width="320" /></a></div>
<br />
В обработчике пропишите следующий код:<br />
<br />
<pre class="brush:delphi">procedure TForm1.dxBarLargeButton1Click(Sender: TObject);
begin
ShowMessage('Мой первый обработчик клика на кнопке TdxBarLargeButton');
end;
</pre>
<br />
Осталось разместить данный элемент в контейнере, но для начала его нужно создать, щелкните на закладке ленты с именем dxRibbonTab1 правой кнопкой и выберите пункт меню "Add Group With ToolBar"<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ9eJVR1drsVvsTHOIWaN5sJ2vbssf2QDvbzG47QzONc_UrzWG6CXeRiziOFLLREzUW0r4N_7wV97pKYUQ0rROyPoXt_C0U-j-XpYybmblcYgxcDASOwpxZRqBUQdPpW4lkZpxutTMveQ/s1600/8.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="160" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ9eJVR1drsVvsTHOIWaN5sJ2vbssf2QDvbzG47QzONc_UrzWG6CXeRiziOFLLREzUW0r4N_7wV97pKYUQ0rROyPoXt_C0U-j-XpYybmblcYgxcDASOwpxZRqBUQdPpW4lkZpxutTMveQ/s320/8.png" width="320" /></a></div>
<br />
При этом в ленте создастся пустая группа (о работе с группами чуть позже) а так же тулбар, на котором можно размещать дополнительные элементы управления.<br />
Данный тулбар так же появится в BarManager-е на вкладке "Toolbars".<br />
<br />
Для того чтобы разместить нашу кнопку в созданном тулбаре необходимо открыть BarManager и перетащить кнопку на нужное место в dxRibbon.<br />
<br />
Запустите приложение и проверьте работоспособность кнопки, должно получится вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEGoGHuvfz7Qk-BbPiWlioNhIF4BEoy8KRCTIYEUq1_BZzYr-GH1nc6wJvW9mCe8SYX-XSjo2IBkzaOCM9S8hCYJS2ey8p5o1l_w7_m_NSmOmnpYrzbpUP3zrpUZrt40bBvGIWY862Qbo/s1600/9.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="214" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEGoGHuvfz7Qk-BbPiWlioNhIF4BEoy8KRCTIYEUq1_BZzYr-GH1nc6wJvW9mCe8SYX-XSjo2IBkzaOCM9S8hCYJS2ey8p5o1l_w7_m_NSmOmnpYrzbpUP3zrpUZrt40bBvGIWY862Qbo/s320/9.png" width="320" /></a></div>
<br />
А теперь посмотрим что дает нам виртуализация. Откройте опять BarManager и снова перетащите кнопку на ленту так, чтобы она разместилась рядом с первой примерно таким образом:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj33xOHzwqW0YgyGnsqjo4AFlNO-CB6Ajk-F9JyaTziRnh8NJWTX9eSxH_NALu7yjRLdKyVDw2rMBh-YWze7mri8ID85ScYGNvXD_NGfwN7t_qKdqcZ6BN1HF-cZFxVReLIe97iKz5Czdg/s1600/10.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="231" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj33xOHzwqW0YgyGnsqjo4AFlNO-CB6Ajk-F9JyaTziRnh8NJWTX9eSxH_NALu7yjRLdKyVDw2rMBh-YWze7mri8ID85ScYGNvXD_NGfwN7t_qKdqcZ6BN1HF-cZFxVReLIe97iKz5Czdg/s320/10.png" width="320" /></a></div>
<br />
Я перенес на ленту данный элемент четыре раза и у меня получилось ровно четыре рабочих кнопки, каждая из которых является ссылкой на оригинальный элемент TdxBarLargeButton и каждая будет обрабатывать его обработчик OnClick.<br />
<br />
<b><span style="color: #cc0000;">А теперь важный нюанс!</span></b><br />
Для удаления элементов управления с ленты <b><span style="color: #cc0000;">не используйте кнопку Delete!!!</span></b><br />
Воспользуйтесь любым из следующих двух способов:<br />
1. Просто перетащите требуемый визуальный элемент за пределы контейнера.<br />
2. Щелкните на визуальный элемент правой кнопкой и в меню выберите пункт "Delete Link"<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMXDyXlXhl054-8xFet6AxFUs3ducn6qx1NUgoPDFvDLDt5352_T7J9RETdiZBHwN29UnILHCjAB4Ud8rK-maifjLgLV7GsHfBBhzpK_cm4o6Mfh-fo6e0K8JULAfoIL-5I_czd1QKbC4/s1600/11.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="253" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMXDyXlXhl054-8xFet6AxFUs3ducn6qx1NUgoPDFvDLDt5352_T7J9RETdiZBHwN29UnILHCjAB4Ud8rK-maifjLgLV7GsHfBBhzpK_cm4o6Mfh-fo6e0K8JULAfoIL-5I_czd1QKbC4/s320/11.png" width="320" /></a></div>
<br />
<br />
В случае если вы все же воспользуетесь кнопкой "Delete" исчезнут все 4 кнопки (можете поэкспериментировать). Произойдет это по той причине что уничтожились все ссылки TdxBarItemLink, являющиеся "визуальной проекцией" на виртуальный элемент TdxBarLargeButton. Причем разрушены будут ссылки во всех контейнерах, а не только в текущем.<br />
<br />
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">Есть небольшой нюанс. В том случае если открыта Customize форма, будет удален сам элемент TdxBarLargeButton, в результате чего все линки на данный Item также будут удалены. Если Customize форма не отображена, будут удалены сами линки.</span></blockquote>
<br />
<h3>
2.1 Меню</h3>
<br />
Размещать один и тот же элемент рядом четыре раза подряд конечно смысла не имеет, а вот разместить его допустим в том же меню, дублирующем функционал с закладки, достаточно хорошая идея.<br />
<br />
В качестве второго контейнера выступит всплывающее меню TdxRibbonPopupMenu из вкладки ExpressBars. После размещения данного элемента на форме откроется диалог его конфигурации. Выглядит следующим образом:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjto95Qw0IMYPGKJC8ofCY1oUpMFPU1ccWpbuSE6yg54VpixYiKIUmUssGC9YEaDf5SvvSzhWsUBk-sulj3kGG73G29_lJqHGN7MpcOsCE3ZhIzOOJecYJMR2lkMxfj0WspUfFmsFTNfAw/s1600/12.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="193" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjto95Qw0IMYPGKJC8ofCY1oUpMFPU1ccWpbuSE6yg54VpixYiKIUmUssGC9YEaDf5SvvSzhWsUBk-sulj3kGG73G29_lJqHGN7MpcOsCE3ZhIzOOJecYJMR2lkMxfj0WspUfFmsFTNfAw/s320/12.png" width="320" /></a></div>
<br />
Все что требуется сделать, это перетащить требуемые элементы на открывшуюся вкладку, по аналогии с перетаскиванием на ленту.<br />
<br />
Теперь необходимо подключить данное меню к форме. Проблема в том, что данное меню не является наследником от TPopupMenu и просто так его назначить форме не получится. Для этого обычно используется шлюз в виде обычного TPopupMenu в обработчике OnPopup которого реализуется следующий код.<br />
<br />
<pre class="brush:delphi">procedure TForm1.PopupMenuGatePopup(Sender: TObject);
begin
dxRibbonPopupMenu1.PopupFromCursorPos;
end;
</pre>
<br />
Ну а само меню, выступающее в качестве шлюза назначается требуемому контролу (в данном случае форме).<br />
<br />
Для подключения меню к обычным элементам управления (не форме), используется свойство PopupMenuLinks у BarManager.<br />
<br />
<h3>
3. Стили dxRibbon, цветовая гамма, панель быстрого доступа </h3>
<br />
С кнопками и меню разобрались, теперь настало время посмотреть на дополнительные параметры dxRibbon и различные стили самих кнопок.<br />
<br />
Скачайте <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">архив примеров</a> и откройте самый первый, скомпилируйте и запустите приложение.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjH0CC4-cNG5-V5rFJUgFdfqR_XRpmR1ICxQDopJl6xQTQQmoTQurn7NmBKqaOF78UYZ9aTKs6VU98EMPTUvkcW0kYQytt3XaZJt9YjuFiT7uxWnJUvtFcM3brtRVgBDCNbwoRFjest5Yg/s1600/13.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjH0CC4-cNG5-V5rFJUgFdfqR_XRpmR1ICxQDopJl6xQTQQmoTQurn7NmBKqaOF78UYZ9aTKs6VU98EMPTUvkcW0kYQytt3XaZJt9YjuFiT7uxWnJUvtFcM3brtRVgBDCNbwoRFjest5Yg/s320/13.png" width="320" /></a></div>
<br />
<br />
Начнем сверху вниз.<br />
<br />
В самом верху появилась панель быстрого доступа (QuickAccessToolbar).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1NgH0nI398yKB9oVTPtqnZ-8J57_H55f6KN64hPyJYeci12d4m1Rj1u9VJGhWAPVJ41QTI0zEe_BCbbriExinsp_XdV1uEBJsa8K0k6_hO_rTMFv9ECrXY5TGSvlcviO0NiEYEECbC6Y/s1600/14.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="130" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1NgH0nI398yKB9oVTPtqnZ-8J57_H55f6KN64hPyJYeci12d4m1Rj1u9VJGhWAPVJ41QTI0zEe_BCbbriExinsp_XdV1uEBJsa8K0k6_hO_rTMFv9ECrXY5TGSvlcviO0NiEYEECbC6Y/s320/14.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
Она уже содержит в себе несколько элементов, а так же предоставляет интерфейс собственной кастомизации в виде всплывающего меню.<br />
<br />
Создается данная панель достаточно тривиально, для этого необходимо открыть BarManager и на закладке Toolbars создать новую панель с произвольным описанием (допустим так и назовем: "Панель быстрого доступа"). Название данной панели будет примерно следующее: dxBarManager1Bar2<br />
После этого на форме появится сама панель снизу ленты:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEevu4jZibGONXoIjsginAU48xeUM4yRu0upnBahFGR97WMdYMEFtbIocYMZjYtpaALt3ZQboBTCRLi_qJBlUza8fotVCxYgCiVgdsvzNRpy0CSPredfJMzm7Fto-6rS3FEw3tt3nGQx4/s1600/15.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEevu4jZibGONXoIjsginAU48xeUM4yRu0upnBahFGR97WMdYMEFtbIocYMZjYtpaALt3ZQboBTCRLi_qJBlUza8fotVCxYgCiVgdsvzNRpy0CSPredfJMzm7Fto-6rS3FEw3tt3nGQx4/s1600/15.png" /></a></div>
<br />
Сейчас панель не слинкована ни на один из контейнеров, поэтому она размещена непосредственно на главной форме. Для подключения ее в виде панели быстрого доступа необходимо зайти в настройки dxRibbon и у параметра QuickAccessToolbar в качестве Tooolbar указать название только что созданной панели (dxBarManager1Bar2).<br />
После этого панель пропадет с формы, но появится сверху над лентой:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdcxcsMnPbrUlEBddEpPumYaVneVeNgQz1qSNGRMLBOXBKV-wJ8-xbcUbhZpEdmq4APC00TMKBNec77aV-BmtuQdxYVF3yrJCMdSaqA3cgeN8n2I-dDbhyphenhyphen7UuIS1YpJ5o2EbrsGfQ8NG8/s1600/16.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdcxcsMnPbrUlEBddEpPumYaVneVeNgQz1qSNGRMLBOXBKV-wJ8-xbcUbhZpEdmq4APC00TMKBNec77aV-BmtuQdxYVF3yrJCMdSaqA3cgeN8n2I-dDbhyphenhyphen7UuIS1YpJ5o2EbrsGfQ8NG8/s1600/16.png" /></a></div>
<br />
Добавлять в нее элементы можно так же как и при работе с обычной панелью - перетаскиванием из BarManager.<br />
У данной панели есть всего два свойства.<br />
<br />
<ol>
<li>Position - определяющее расположение панели над лентой или под ней.</li>
<li>Visible - отвечающее за видимость данной панели.</li>
</ol>
<br />
Так-же данная панель имеет меню кастомизации, за полноту которого отвечают флаги, хранящиеся в свойстве PopupMenuItems компонента dxRibbon.<br />
<br />
<ul>
<li>Флаг rpmiItems отвечает за отображение текущих элементов тулбара в меню, где можно управлять их видимостью.</li>
<li>Флаг rpmiMoreCommands отвечает за отображение пункта "More Commands...", при помощи которого можно изменять состав элементов всех тулбаров в рантайме.</li>
<li>Флаг rpmiQATPosition отвечает за отображение пункта меню "Show Quick Access Toolbar Below the Ribbon", при помощи которого в рантайм можно управлять свойством Position у QuickAccessToolbar.</li>
<li>Флаг rpmiQATAddRemoveItem отвечает за отображение пункта меню "Remove from Quick Access Toolbar" отображаемого при клике правой кнопкой непосредственно на элементе из данного тулбара.</li>
<li>Флаг rpmiMinimizeRibbon отвечает за отображение пункта меню "Minimize the Ribbon", позволяющему сворачивать ленту в рантайм.</li>
</ul>
<div>
Если все флаги отключены, глиф справа от тулбара отображаться не будет.</div>
<div>
<br /></div>
<div>
Кстати по поводу rpmiMinimizeRibbon. Манипуляции с данным флагом не повлияют на саму возможность минимизации ленты, данная возможность всегда будет доступна в рантайме и управляется двойным кликом на любой вкладке ленты + в стиле MS Office 2010 будет доступна дополнительная кнопка справа ленты, помогающая свернуть/развернуть ленту в один клик.<br />
Впрочем отключить такое поведение можно посредством параметра MinimizeOnTabDblClick у dxRibbon.</div>
<div>
<br /></div>
<h4>
3.1 Стиль ленты</h4>
<div>
<br />
Сама лента может быть представлена в виде двух различных визуализаций, в стиле MS Office 2007 и MS Office 2010. Отвечает за переключение между данными стилями параметр Style компонента dxRibbon, принимающий значения rs2007 и rs2010 соответственно.</div>
<div>
<br /></div>
<div>
Так же разработчику доступна возможность изменения цветовой схемы ленты через параметр dxRibbon.ColorSchemeName.</div>
<div>
<br /></div>
<div>
Самих цветовых схем достаточно много. Хоть по умолчанию доступны всего три - синяя, черная и серебряная, но можно подключить и расширенные через меню "Modify skin options" в настройках проекта.</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhi8dLqLm77zxjkrJSux9UTATAi-e0rx83A09iWlMorjzP7xnGZdLnMyRMqF5A48SwOT6Wq0mBQmHUYFHoXjFM-0d30vWSp15vMOWbk5Rczo1bKMpMjgexftbvTQIL0yiS-11uUwkvmY7w/s1600/17.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhi8dLqLm77zxjkrJSux9UTATAi-e0rx83A09iWlMorjzP7xnGZdLnMyRMqF5A48SwOT6Wq0mBQmHUYFHoXjFM-0d30vWSp15vMOWbk5Rczo1bKMpMjgexftbvTQIL0yiS-11uUwkvmY7w/s320/17.png" width="279" /></a></div>
<div>
<br /></div>
<div>
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">Дополнительные схемы - это часть ExpressSkins Library, которая является отдельным продуктом. Прежде чем начать работать с ними, нужно убедиться, что этот продукт установлен.</span></blockquote>
Со скинами связана одна небольшая неприятность. Каждый новый скин достаточно сильно утяжеляет проект и если размер пустого проекта с лентой находится в районе двух мегабайт, то при всех включенных скинах он вырастает аж до 17 мегабайт, что уже достаточно увесисто :)</div>
<div>
Вторая неприятность (и о ней даже специально написано в подсказке диалога настройки скинов) заключается в том, что при отключении скинов они не удаляются автоматом из секции <b>uses</b>, поэтому если вы отключили ненужные по вашему мнению скины - не забудьте почистить и секцию <b>uses</b>, сделав поиск по фразе dxSkin.<br />
<br />
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">Можно избежать утяжеления путем динамической загрузки скинов.</span><br />
<span style="color: #444444;">В составе инсталятора идут бинарные файлы скинов (...\DevExpress.VCL\ExpressSkins Library\Binary Skin Files\). Вы можете просто загрузить скины из этих бинарных файлов, используя технику, показанную вот в этой статье: <a href="http://www.devexpress.com/Support/Center/Issues/ViewIssue.aspx?issueid=K18293" target="_blank">How to load skins dynamically</a></span></blockquote>
</div>
<div>
В случае стиля MS Office 2010 для стандартных цветовых схем так же доступна возможность изменять цвет кнопки, отображающую BackStage панель (о ней чуть позже).</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdyROQtez0PE7GU5yTUea75NmABjjH34DBRRhebRNphCSp6z7naXu-kAhGRF7vbNMv3qcVsNygstumV5gtfvDhhrd5NGR0cTQ9dF6l8LzViax0E5ziAxOZIV89iwpGMvfPNO3F4P2pics/s1600/18.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="77" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdyROQtez0PE7GU5yTUea75NmABjjH34DBRRhebRNphCSp6z7naXu-kAhGRF7vbNMv3qcVsNygstumV5gtfvDhhrd5NGR0cTQ9dF6l8LzViax0E5ziAxOZIV89iwpGMvfPNO3F4P2pics/s320/18.png" width="320" /></a></div>
<div>
<br /></div>
<div>
Отвечает за это свойство dxRibbon.ColorSchemeAccent. Доступно правда всего пять цветов, но хоть и мелочь - а приятно :)</div>
<div>
<br /></div>
<h4>
3.2 TdxBarSubItem, TdxBarListItem</h4>
<div>
<br />
Для изменения настроек ленты в рантайм я выбрал два элемента управления.</div>
<div>
Это TdxBarSubitem, позволяющий отображать во всплывающем списке дополнительные элементы управления (грубо говоря он предоставляет контейнер в виде выпадающего меню). </div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRKmANoavQfvEanak9eory6gPiK2Wy_0LcV1d7ZeHLJRtPUltoRBnNT2x2LorOAxwlg4GvebOsgBN_LUisYt9Dwfx5njC4LUH33LQKMQy8Q_iMifcFZj6frPU0BFV9DkZzjGogLhLVIOE/s1600/19.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRKmANoavQfvEanak9eory6gPiK2Wy_0LcV1d7ZeHLJRtPUltoRBnNT2x2LorOAxwlg4GvebOsgBN_LUisYt9Dwfx5njC4LUH33LQKMQy8Q_iMifcFZj6frPU0BFV9DkZzjGogLhLVIOE/s1600/19.png" /></a></div>
<div>
<br /></div>
<div>
Очень простой элемент, по сути представляет для себя просто элемент управления отображающее меню, которое мы можем кастомизировать по собственному усмотрению.</div>
<div>
Как можно увидеть в примере, для наполнения данного меню я создал две кнопки TdxBarButton, назначил каждой из них иконку и перетащил на данное меню в дезайн тайме.</div>
<div>
<br /></div>
<div>
В обработчиках данных кнопок я поместил код, отвечающий за переключение стиля ленты:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">procedure TForm1.dxBarButton1Click(Sender: TObject);
begin
// включаем стиль MS Office 2007
dxRibbon.Style := rs2007;
// отключаем кнопку отвечающую за переключения цвета BackStage Button
btnColorSchemeAccent.Visible := ivNever;
end;
procedure TForm1.dxBarButton2Click(Sender: TObject);
begin
// включаем стиль MS Office 2010
dxRibbon.Style := rs2010;
// включаем кнопку отвечающую за переключения цвета BackStage Button
btnColorSchemeAccent.Visible := ivAlways;
end;
</pre>
<div>
<br /></div>
<div>
Пока не обращайте внимание на код отключения BackStage Button - до него еще успеем дойти.<br />
<br />
Для того чтобы данные две кнопки автоматически переключались и разработчику не нужно было ручками выставлять их состояние, свойство ButtonStyle обоих кнопок было установлено в bsChecked, а GroupIndex установлен в значение 10 (в принципе можно использовать любое другое, я обычно использую кратное десятке).<br />
Таким образом они стали работать в режиме RadioButton-ов.<br />
<br />
А для переключения цвета ленты я использовал еще более простой в использовании элемент TdxBarListItem.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDIgptaVBkMdX8MQfevT6hyphenhyphenJm2CKISruFjBwpAo5NG9XyNeJh9A-XVl30pFCnwscKf9e2WxDIEhxIQKcULVyAP5WMUZIzogpPSNFgTWYCcyiD18VdvfJE01fhGNu6N_-LKEIwGcNPOmek/s1600/20.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDIgptaVBkMdX8MQfevT6hyphenhyphenJm2CKISruFjBwpAo5NG9XyNeJh9A-XVl30pFCnwscKf9e2WxDIEhxIQKcULVyAP5WMUZIzogpPSNFgTWYCcyiD18VdvfJE01fhGNu6N_-LKEIwGcNPOmek/s1600/20.png" /></a></div>
<br />
Визуально он практически ничем не отличается от используемого ранее TdxBarSubitem, за исключением пары моментов.<br />
<ol>
<li>Он также отображает всплывающее меню, но оно не является контейнером для сторонних элементов управления и настраивается через свойство Items.</li>
<li>Так как меню не кастомизируемо, его элементам нельзя назначить иконку.</li>
<li>Для переключение элементов меню в режим RadioButton используется свойство ShowCheck</li>
</ol>
<div>
Обработчик данного элемента выглядит следующим образом:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">procedure TForm1.btnColorSchemeNameClick(Sender: TObject);
begin
// Устанавливаем цветовую схему ленты
case btnColorSchemeName.ItemIndex of
0: dxRibbon.ColorSchemeName := 'Blue';
1: dxRibbon.ColorSchemeName := 'Black';
2: dxRibbon.ColorSchemeName := 'Silver';
end;
end;
</pre>
<div>
<br /></div>
<div>
Наименование цветовой схемы является обычной строкой. Данная строка в виде констант присутствует в каждом стиле ленты, представляющего из себя наследник от TdxSkinLookAndFeelPainter и возвращается методом LookAndFeelName.<br />
<br />
Если вы не уверены в правильности данной константы, можете открыть юнит с соответствующим скином и посмотреть реализацию данного метода, откуда и взять строковую константу.<br />
<br />
<h4>
3.3. TcxImageList</h4>
<br />
Для назначения иконок элементам ленты используется внешние ImageList-ы, в принципе это могут быть и обычные TImageList, но желательно использовать TcxImageList ибо данный элемент обладает несколько большим функционалом, по сравнению со стандартным.<br />
<br />
Самым важным его свойством является то, что он позволяет работать с 32-битными иконками с альфа-каналом. Раньше приходилось возится с ImageList ручками переводя его в режим ILC_COLOR32 и руками добавлять в него иконки из ресурсов, дабы не было потери альфаканала при конвертации. С появлением TcxImageList данная проблема отпала.<br />
<br />
Второй интересной возможностью является параметр CompressData, позволяющий уменьшить размер информации об изображениях, размещаемой в DFM. В одном из моих проектов один из ImageList-ов содержит более 500 иконок (проект чем-то похож на MS Excel и такое количество изображений обусловлено значительным количеством функциональных элементов). Будь они помещены в обычный TImageList их объем занял-бы в районе 1.2 мегабайта. В случае использования сжатия размер уменьшается примерно до 700кб (мелочь, а приятно).<br />
<br />
Обычно используются два листа, подключаемые через свойство ImageOptions элемента BarManager. Первый ImageList содержит иконки размером 16х16 и подключается через параметр Images, второй 32х32 через параметр LargeImages. Есть еще несколько листов, таких как DisabledImages, DisabledLargeImages и HotImages, но я их обычно не использую ибо нет такой необходимости.<br />
<br />
После подключения листов к BarManager у каждого элемента можно назначить требуемую ему иконку, используя свойства ImageIndex/LargeImageIndex и т.п.<br />
<br />
Если вы работаете с "большими" элементами (не знаю как назвать правильно) такими как TdxBarLargeButton или TdxBarSubitem, которые могут отображать на ленте иконку большого размера, необходимо обязательно назначить соответствующую маленькую, ибо при изменении размеров ленты, размеры самих элементов будут подстраиваться под оставшийся размер свободной площади и будут переходить в отображение маленьких иконок, которые они не смогут отрисовать, по причине того что их индекс не назначен.<br />
Поэксперементируйте с демоприложением, запустите его и попробуйте плавно изменять его ширину и понаблюдайте за поведением элементов.<br />
<br />
Для примера посмотрите вот на эту форму:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim8xkgcWxgDnsrIIpGmKH8AKKLTBRMOX2SETd7FXtrLdFB_ACuU2W3ibGxuKjdsinuz1obSa2l5uq7Zbn181gv9feMCLncJvPpToib5A1-Hugt9ThceO2W9perp_50P6ctP_PigtehTjg/s1600/21.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim8xkgcWxgDnsrIIpGmKH8AKKLTBRMOX2SETd7FXtrLdFB_ACuU2W3ibGxuKjdsinuz1obSa2l5uq7Zbn181gv9feMCLncJvPpToib5A1-Hugt9ThceO2W9perp_50P6ctP_PigtehTjg/s1600/21.png" /></a></div>
<br />
Обратите внимание на кнопку "Цвет стиля", она сейчас отображает большую иконку, но вот маленькая ей не назначена и при уменьшении ширины формы произойдет следующее:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyueK4H4kG9TOd5UsD4t2qsnjIekTs15iTO-nn24whCg6aJuJEyXeYB02aRGSAvoCbQo3P1LQyHaQ6xU2gOndBZVV_llDjHQMyqUiwsXvE9MX0WJ0Zcx4jxHodwAx_DpbTpCCK5UfYQLU/s1600/22.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyueK4H4kG9TOd5UsD4t2qsnjIekTs15iTO-nn24whCg6aJuJEyXeYB02aRGSAvoCbQo3P1LQyHaQ6xU2gOndBZVV_llDjHQMyqUiwsXvE9MX0WJ0Zcx4jxHodwAx_DpbTpCCK5UfYQLU/s1600/22.png" /></a></div>
<br />
Иконка у данной кнопки пропадет.<br />
Конечно не критично, но как-то неаккуратненько :)<br />
<br />
Ну и кстати обратите внимание на зеленый шарик в панели быстрого доступа. Таким шариком отображаются элементы, у которых не назначен ImageIndex, а в данном случае этим элементом как раз и является кнопка "Цвет стиля".<br />
<br />
<b><span style="color: #cc0000;">Внимание ошибка!</span></b><br />
Ну а теперь к основной и главной проблеме использования двух ImageList.<br />
Дело в том что увы, но иногда значения параметров LargeImageIndex слетают, ну точнее не то чтобы слетают, но при загрузке DFM на позициях больших иконок отображаются совершенно не те изображения, которые были указаны изначально.<br />
В более ранних версиях данных контролов это было огромной головной болью, сейчас же стало немного полегче, особенно если вы используете параметр SyncImageIndex выставленный у каждого элемента в True, но все равно периодически бывают сбои.<br />
Спасает от данной ошибки SVN, используя который можно в любой момент откатится на правильный DFM формы, впрочем иногда достаточно обычного переоткрытия проекта не сохраняя при этом никаких изменений в диалогах закрытия, которые вывалит Delphi.<br />
Но самый правильный способ - это использование полностью синхронизированных картинок в обоих ImageList.<br />
Конечно количество маленьких иконок 16х16 всегда будет больше чем 32х32, поэтому я обычно пользуюсь таким решением:<br />
При разработке GUI приложения я стараюсь заложиться на максимальное количество элементов управления, которые будут представлены в виде больших икон, размещаю их изображения в обоих списках самыми первыми и синхронизированными (т.е. добиваюсь того чтобы свойства ImageIndex и LargeImageIndex имели одинаковые значения) после чего делаю буффер из 30 иконок в маленьком ImageList про запас, которые забиваю пустой иконкой. Если потребуется добавить еще какой нибудь элемент в большой ImageList, в маленьком я его буду добавлять не в конец, а на соответствующую позицию зарезервированного буфера.<br />
Обычно помогает. Размеры буфера, если вы будете использовать мой подход думайте сами, если планируется кардинальное изменение GUI в перспективе, лучше сделать его побольше. В противном случае придется перелопачивать все индексы иконок по всему проекту, ну либо иметь проблемы при рассинхронизации. Особенно больно бывает когда эти проблемы вылезают при релизной компиляции на билдсервере.<br />
<br />
<b>Важное уточнение:</b><br />
Такое поведение проявляется только в случаях если TcxImageList расположен не на главной форме (в моем случае он размещен на отдельном TDataModule). Вот что по этому поводу сообщают разработчики...<br />
<br />
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">Скорее всего, это известная проблема с TPersistent с референсами на компоненты, находящихся в DataModule. У нас достаточно много тикетов по этой теме. Например, посмотрите на объяснения в следующих тикетах:</span><br />
<span style="color: #444444;">1. <a href="http://www.devexpress.com/Support/Center/Issues/ViewIssue.aspx?issueid=Q264614" target="_blank">FakeComponentLink</a></span><br />
<span style="color: #444444;">2. <a href="http://www.devexpress.com/Support/Center/Issues/ViewIssue.aspx?issueid=B220048" target="_blank">cxGridDBTableView (and cxTreeList) lose properties referencing objects on another form</a>.</span><br />
<span style="color: #444444;">Во втором тикете предложен workaround пользователем Mike F, который может помочь в решении данной проблемы.</span></blockquote>
<br />
<h4>
3.4. параметры Application/BackStage button и QuickAccessBar</h4>
<br />
Перейдем к следующим трем кнопкам демопримера.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWmXKfPYHA-35VbiyFMapR9uSVFBhKhU1jARsQgwnpWMsgadVnSkl5M9HrBmoatV3ol8ihLAdb4AeRKkJZO5SKWQZ38V4cLkxz0ueL4XtsNtNTLyD2jlMsjCIO5AATTXaWKm7BNJPsxeg/s1600/23.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWmXKfPYHA-35VbiyFMapR9uSVFBhKhU1jARsQgwnpWMsgadVnSkl5M9HrBmoatV3ol8ihLAdb4AeRKkJZO5SKWQZ38V4cLkxz0ueL4XtsNtNTLyD2jlMsjCIO5AATTXaWKm7BNJPsxeg/s1600/23.png" /></a></div>
<br />
Они расположены на отдельном тулбаре, который я создал через меню "Add Group With ToolBar". Самая первая кнопка является обычной TdxBarLargeButton у которой выставлен стиль bsChecked, позволяющий ей находиться в двух режимах - вжатом и отжатом. За управление данным режимом отвечает свойство кнопки Down.<br />
Обработчик кнопки выглядит следующим образом:<br />
<br />
<pre class="brush:delphi">procedure TForm1.btnShowAppButtonClick(Sender: TObject);
begin
dxRibbon.ApplicationButton.Visible := btnShowAppButton.Down;
end;
</pre>
<br />
Грубо говоря в зависимости от состояния данной кнопки происходит переключение видимости ApplicationButton приложения.<br />
<br />
А вот с ней есть небольшой нюанс.<br />
В зависимости от стиля данная кнопка выглядит по разному.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinzc0HzkWReJ2VgUwmndKJGWei5kyY6nyMdlE9JfReOvcFlyzrrVoQZTsb0g587FF5Bxl9ODfzBWLzcmW74dktOlmKFJccMazKiDtiLY3jyKRsbQqwSVZp3b_gziyoVaU-ld3UV7cShVw/s1600/24.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="98" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinzc0HzkWReJ2VgUwmndKJGWei5kyY6nyMdlE9JfReOvcFlyzrrVoQZTsb0g587FF5Bxl9ODfzBWLzcmW74dktOlmKFJccMazKiDtiLY3jyKRsbQqwSVZp3b_gziyoVaU-ld3UV7cShVw/s320/24.png" width="320" /></a></div>
<br />
Слева вариант для стиля MS Office 2007, справа для 2010-го.<br />
Во втором случае у данной кнопки появляется возможность изменения цвета (как я и говорил выше в главе 3.1). Для изменения цвета данной кнопки я опять использую TdxBarListItem, содержащий список цветов в порядке, объявленном в типе TdxRibbonColorSchemeAccent и обработчик кнопки изменения цвета выглядит следующим образом:<br />
<br />
<pre class="brush:delphi">procedure TForm1.btnColorSchemeAccentClick(Sender: TObject);
begin
dxRibbon.ColorSchemeAccent :=
TdxRibbonColorSchemeAccent(btnColorSchemeAccent.ItemIndex);
end;
</pre>
<br />
Единственно, дабы кнопка изменения цвета была доступна только в случае включенного стиля rs2010 приходится управлять ее видимостью через параметр Visible, который не является булевым типом (как обычно) а представляет из себя перечисление TdxBarItemVisible и принимает следующие значения:<br />
<div>
</div>
<ul>
<li>ivNever - элемент управления не видим</li>
<li>ivInCustomizing - элемент управления виден только в режиме настройки ленты</li>
<li>ivAlways - элемент управления виден всегда</li>
</ul>
Третья кнопка "Отображать QuickAccessBar" отвечает за управление параметрами Position и Visible у компонента QuickAccessToolbar.<br />
<div>
Представляет она из себя составной элемент из TdxBarLargeButton со стилем bsCheckedDropDown, всплывающего меню, закрепленного через параметр DropDownMenu данной кнопки и двух кнопок TdxBarButton</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNbv9g_vnPbQNnGtZrnhwh2dr_jg_E0yIrSPPHa2_Xzs8wpmolxfc2m5cZFx6xR4hjPOqlI7BvtBX-z0p0KMzfKgZ8zXeWNDxViJ6otcszbjCV0SgbQ2hDIZ3q2Z9obxDZvH093xvRNsY/s1600/25.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNbv9g_vnPbQNnGtZrnhwh2dr_jg_E0yIrSPPHa2_Xzs8wpmolxfc2m5cZFx6xR4hjPOqlI7BvtBX-z0p0KMzfKgZ8zXeWNDxViJ6otcszbjCV0SgbQ2hDIZ3q2Z9obxDZvH093xvRNsY/s1600/25.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div>
Переключение кнопки в режим bsCheckedDropDown позволяет выполнить сразу несколько действий. Во первых верхняя часть кнопки (там где иконка с изображением "глаза") работает в режиме bsChecked, описаном чуть выше. Это позволяет выполнить следующий обработчик:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">procedure TForm1.btnShowQABClick(Sender: TObject);
begin
dxRibbon.QuickAccessToolbar.Visible := btnShowQAB.Down;
end;
</pre>
</div>
</div>
</div>
</div>
</div>
<br />
Во вторых нижняя часть кнопки работает в режиме bsDropDown, позволяющем отображать выпадающее меню закрепленное через параметр DropDownMenu.<br />
<br />
С оставшимися двумя кнопками, размещенными в выпадающем меню не связано никаких нюансов, за исключением одного момента. Если обратите внимания на их свойства, то увидите что у них не выставлены свойства ImageIndex и LargeImageIndex, однако изображения у обоих кнопок присутствуют. Эти изображения (представляющее собой стрелки вверх и вниз) добавлены посредством параметра Glyph, который принимает изображения только в виде BMP файлов.<br />
<br />
Последнее на чем остановлюсь в данном демопримере, это то, что самая правая кнопка отделена внутри своего тулбара от других вертикальным разделителем. Делается такой эффект достаточно просто, в DesignTime выделите требуемый элемент управления и перетащите его немного правее на пару пикселей, после чего слева от него автоматически появится разделитель группы, либо установите признак начала группы через меню элемента, включив флаг "Begin a Group":<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWvAopY0149jV8OtlFNU3MTfLZA04THsq9uqjpgLIuEBO8zV2wfHjW01T4FoMCkMJW2wBbj6kWbfYAQoaeXx8sBFol9gyC1Fe6i6ELNIB_kUv5uTXFZsvWCtw9UH0AYblkgfkpfqJebQk/s1600/26.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWvAopY0149jV8OtlFNU3MTfLZA04THsq9uqjpgLIuEBO8zV2wfHjW01T4FoMCkMJW2wBbj6kWbfYAQoaeXx8sBFol9gyC1Fe6i6ELNIB_kUv5uTXFZsvWCtw9UH0AYblkgfkpfqJebQk/s320/26.png" width="241" /></a></div>
<br />
<h3>
4. TdxBarApplicationMenu</h3>
<br />
Пришло время разобраться с настройкой Application Button в стиле MS Office 2007.<br />
Откройте <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">второй демопример</a> и посмотрите его исходный код. Как видите там объявлен всего один обработчик, все остальное полностью настроено в DesignTime и выглядит при запуске следующим образом:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmSQDqpwtZrsOB81D-tkJuMKRQlDRPRXe6GO5qEpDkyCWmyKwJQ0w8QU1QpvkNIi7KexaN2CTk8h0E0mPoHYdQaKtcSXqn5R170ZtKoPo7ZhLUV5zZtyURjXkvNwz_SThhfsZ5Ux5hECg/s1600/27.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmSQDqpwtZrsOB81D-tkJuMKRQlDRPRXe6GO5qEpDkyCWmyKwJQ0w8QU1QpvkNIi7KexaN2CTk8h0E0mPoHYdQaKtcSXqn5R170ZtKoPo7ZhLUV5zZtyURjXkvNwz_SThhfsZ5Ux5hECg/s1600/27.png" /></a></div>
<br />
Реализовано такое выпадающее меню при помощи компонента TdxBarApplicationMenu.<br />
Подключается данный компонент в dxRibbon через свойство ApplicationButton.Menu.<br />
<br />
Визуально оно разделено на три части.<br />
<br />
1. Меню слева, в котором представлены кнопки "Новый документ", "Открыть" и т.д.<br />
Настраивается как обычное меню перетаскиванием необходимых элементов из BarManager:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIZ0Q_D-sQdlB2TMFBPN8NB1-XiaUkO4RXyY-PaksR2N2wMa5l42-msdXWTumoJYDub0L-CMC6nN8Vv6n77u-Dz_3olseks6t2xGx5GLDckhQX0WXwSL8Rt3p-oVyucUCg2buQuub6rD4/s1600/28.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="175" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIZ0Q_D-sQdlB2TMFBPN8NB1-XiaUkO4RXyY-PaksR2N2wMa5l42-msdXWTumoJYDub0L-CMC6nN8Vv6n77u-Dz_3olseks6t2xGx5GLDckhQX0WXwSL8Rt3p-oVyucUCg2buQuub6rD4/s400/28.png" width="400" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both;">
Если не нравится что кнопки большие, то их можно уменьшить, указав в параметре TdxBarApplicationMenu.ItemOptions.Size значение misNormal.</div>
<div>
<br /></div>
<div>
2. Список открытых ранее документов. Настраивается через параметр ExtraPane, в котором параметр Header содержит текст, отображаемый в заголовке, AllowPin разрешает отрисовку маркеров закрепления справа от каждого элемента и самый главный параметр Items, который и содержит в себе список отображаемых элементов. Каждый элемент данного списка имеет следующие параметры:</div>
<div>
<ul>
<li>DisplayText - непосредственно отображаемый текст</li>
<li>ImageIndex - иконка документа</li>
<li>Pin - состояние маркера закрепления</li>
<li>Text - неотображаемый текст элемента</li>
</ul>
<div>
Первый и последний параметр можно комбинировать следующим образом, к примеру параметр Text содержит полный путь к файлу, а параметр DisplayText только наименование файла.</div>
</div>
<div>
<br /></div>
<div>
Остальные параметры не столь существенны.</div>
<div>
<br /></div>
<div>
Так же ExtraPane содержит свойство WidthRatio по умолчанию выставленное в 2.5</div>
<div>
Данное свойство отвечает за процентное увеличение ширины меню от его оригинального размера. В принципе просто как украшательство. Выставьте данному параметру значение 1.5 - этого будет за глаза.</div>
<div>
<br /></div>
<div>
3. Остались дополнительные кнопки снизу. В данном случае это кнопки "Настройки" и "Выход". Данные элементы являются двумя стандартными кнопками TdxBarButton подключенными через свойство TdxBarApplicationMenu.Buttons. Данное свойство так же является списком в котором у каждого элемента есть всего два параметра, это Item, в котором необходимо указать какой именно элемент управления будет отображен из присутствующих в BarManager-е и свойство Width, отвечающее за желаемую ширину данного элемента (если установлен ноль - используется ширина элемента по умолчанию).</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZUXmAjRaGhfKoV0TeR_-wCLYF0MnC24zgqcxTR9S52vrZmiuRg8dtQMt-B2UB-hvnQA1m41RiegYP7oFPTesD_OVyXKSCt18NC5diGFXGYRJIEIa8srITKaMYI5GjCw-BDAiF6ohLemM/s1600/29.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="150" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZUXmAjRaGhfKoV0TeR_-wCLYF0MnC24zgqcxTR9S52vrZmiuRg8dtQMt-B2UB-hvnQA1m41RiegYP7oFPTesD_OVyXKSCt18NC5diGFXGYRJIEIa8srITKaMYI5GjCw-BDAiF6ohLemM/s400/29.png" width="400" /></a></div>
<div>
<br /></div>
<div>
Работать с TdxBarApplicationMenu достаточно просто. Все что оно реально предоставляет, так это список открытых документов, остальное является сторонними элементами управления с собственными обработчиками, поэтому достаточно будет перекрыть событие ExtraPaneOnItemClick где прописать следующий обработчик:</div>
<div>
<br /></div>
<div>
<pre class="brush:delphi">procedure TForm2.dxBarApplicationMenu1ExtraPaneItemClick(Sender: TObject;
AIndex: Integer);
begin
ShowMessage('Требуется открыть файл: ' +
dxBarApplicationMenu1.ExtraPane.Items[AIndex].Text);
end;
</pre>
<br />
<h3>
5. TdxRibbonBackstageView</h3>
<br />
Перед тем как вы откроете <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">третий демопример</a> вам необходимо будет установить дополнительный компонент FWBackStageButton расположенный в папке components в составе примеров.<br />
Если лениво это делать - к данному примеру я приложил скомпилированный исполняемый файл, чтобы можно было посмотреть на те нюансы, о которых пойдет речь в данной главе.<br />
<br />
После этого откройте сам пример. Как вы сможете увидеть, компоненты TdxBarApplicationMenu нет, вместо нее на форме расположен компонент TdxRibbonBackstageView, в котором и происходит вся настройка.<br />
<br />
Подключается данный контрол так же в dxRibbon через свойство ApplicationButton.Menu.<br />
Он представляет из себя некий аналог TPageControl, т.е. для работы с ним нужно создать табы, размещаемые слева панели, а так-же туда могут помещаться дополнительные элементы управления из BarManager.<br />
<br />
Настройка его в принципе вообще минимальная. Он предоставляет всего одно настраиваемое свойство Buttons. Работает данное свойство по аналогии с TdxBarApplicationMenu.Buttons, т.е. это список в который подключаются элементы управления из BarManager, у каждого из которых есть всего одно дополнительное свойство Position, управляющее расположением элемента, над или под табами BackStage.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQxrUv9yX7N5904qsMimagmBmPtigI6mjDInMhRfj-8E5dVOfW1aXmXjYe-uLifjCBraFo91gzwK2DHmHYImX9z3JBLX5SIv8uvAleJj33OX4-kDllOR0GeFqc6qQyhgIhE09omqhtddY/s1600/30.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="283" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQxrUv9yX7N5904qsMimagmBmPtigI6mjDInMhRfj-8E5dVOfW1aXmXjYe-uLifjCBraFo91gzwK2DHmHYImX9z3JBLX5SIv8uvAleJj33OX4-kDllOR0GeFqc6qQyhgIhE09omqhtddY/s320/30.png" width="320" /></a></div>
<br />
Сами же табы являются скажем так неким аналогом панелей из TPageControl, т.е. все что мы можем, это создать необходимое количество табов и присвоить им название. Это практически все доступные настройки, влияющие на отображение. Добавляются табы через меню "Add Tab", удаляются там же.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJkt7EPwwnjAYd8wGk1MWbXvcm2HwZxdJZBlZ7Ij4XIKEts5hfLmvmu6x4hBC-5s-fMR5LZDD8ctx1qlDX92Pm_Id4QJdpMXaYQZ9CNbVf_CdRbCuk7yiAMo9Vplx4tbfiwXOdfKlfeDo/s1600/31.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJkt7EPwwnjAYd8wGk1MWbXvcm2HwZxdJZBlZ7Ij4XIKEts5hfLmvmu6x4hBC-5s-fMR5LZDD8ctx1qlDX92Pm_Id4QJdpMXaYQZ9CNbVf_CdRbCuk7yiAMo9Vplx4tbfiwXOdfKlfeDo/s320/31.png" width="140" /></a></div>
<br />
И вот здесь то и заключается самая большая проблема настройки панелей каждого таба. Дело в том, что стандартные элементы управления, размещенные на них, мягко говоря очень сильно выбиваются из общего стиля. Вот посмотрите:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgpQGmzDG4z6L2HkM5SNhrrsuf8AesF2rx1IZKDtT4eLckfEW-ngQFB3VamswrulWem8TNdKjAOe8H1Spe6V6yFKGzMuU7tluZ1Rbrmuifzwq1GwkaU_TFS1mryVnmKVHIp2QVeSsU5Uc/s1600/32.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="150" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgpQGmzDG4z6L2HkM5SNhrrsuf8AesF2rx1IZKDtT4eLckfEW-ngQFB3VamswrulWem8TNdKjAOe8H1Spe6V6yFKGzMuU7tluZ1Rbrmuifzwq1GwkaU_TFS1mryVnmKVHIp2QVeSsU5Uc/s320/32.png" width="320" /></a></div>
<br />
Вторая проблема в том, что как я и говорил выше, все элементы управления DevExpress размещаемые в BarManager-е и которые вроде как подходят по стилю, являются виртуальными и не могут быть размещены вне рамок контейнера. А панель каждого таба таким контейнером не является.<br />
<br />
Дабы обойти данное препятствие у себя в проектах я разработал компонент TFWBackStageButton, управляя параметрами которого можно добиться достаточно приемлемых результатов. Описывать параметры данного компонента я не буду, он идет с полным исходным кодом плюс, в рамках демопримера, я создал четыре закладки где показал пример работы с ним в различных режимах.<br />
<br />
Будете ли вы его использовать или нет - решать вам, вполне вероятно что у вас будет собственное решение или вы знаете о некоем альтернативном варианте - не важно.<br />
И так что данный компонент умеет:<br />
<br />
1. Умеет работать в качестве пина и многострочной кнопки с иконкой слева:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiK3fI7fOZxDQcvQK_GAhUA5vN1UDcqlm400vcE-SbA9vcu1LOMq1RhzjNHWYFft4CmeNgCudEcnSLQxnrm3c7HXCejidF2OEBgi5xW1I73P7FYuD-a07ndtOOwkfR1FBoJik0D0M-ciD0/s1600/33.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="220" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiK3fI7fOZxDQcvQK_GAhUA5vN1UDcqlm400vcE-SbA9vcu1LOMq1RhzjNHWYFft4CmeNgCudEcnSLQxnrm3c7HXCejidF2OEBgi5xW1I73P7FYuD-a07ndtOOwkfR1FBoJik0D0M-ciD0/s320/33.png" width="320" /></a></div>
<br />
2. Умеет работать как обычная кнопка с иконкой сверху и кнопка с отрисовкой угла для эмуляции подзакладок (автопереключение не реализовано, необходимо делать руками):<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLMojiQnsFwMpDJQC7MAb2_D2rkb0hQvY3QflI-oGl9wrQ0yYvgocXKIZSbr0VF2_6FIM07kYOCrnXIPlTCvoTFQMvbw7_lPZOk4ytcWkGiE46O0UAJEzX57qH9TGkC1PbLRbPmhilm7g/s1600/34.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="220" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLMojiQnsFwMpDJQC7MAb2_D2rkb0hQvY3QflI-oGl9wrQ0yYvgocXKIZSbr0VF2_6FIM07kYOCrnXIPlTCvoTFQMvbw7_lPZOk4ytcWkGiE46O0UAJEzX57qH9TGkC1PbLRbPmhilm7g/s320/34.png" width="320" /></a></div>
<br />
3. Умеет работать как DropDown кнопка (это только эмуляция, отображение меню необходимо делать самостоятельно)<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgw_t04DXTh6DSidn_RKolJpkFNlk98oiT5128EIspvtq1M923ieSM9ArghTvmRA1xRV2LL9bYkfBOnQKOwA3ia33G6VXKKEm3K_o1VIE-TZs0tOaMfxePsn1HIcJuj5-IgkXSalFkxSXI/s1600/35.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="220" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgw_t04DXTh6DSidn_RKolJpkFNlk98oiT5128EIspvtq1M923ieSM9ArghTvmRA1xRV2LL9bYkfBOnQKOwA3ia33G6VXKKEm3K_o1VIE-TZs0tOaMfxePsn1HIcJuj5-IgkXSalFkxSXI/s320/35.png" width="320" /></a></div>
<br />
4. Так же умеет работать в режиме Checked, поддерживает 3 ImageList и автоматом изменяет иконку при изменении размеров и прочее.<br />
<br />
В качестве отрисовки фона использует пайнтер текущего скина ленты, поэтому для каждой кнопки необходимо указывать Ribbon, дабы отрисовка была корректной.<br />
<br />
На первых порах этого в принципе достаточно.<br />
<br />
Да и, забыл нюанс, когда будете смотреть пример с закладкой принтеров, обратите внимание на компонент mnuPrinters. Он содержит в себе три TdxBarLargeButton, но отображаются они слегка не так как я показывал раньше. Это результат изменения настроек самого меню в параметре ItemOptions, где я включил флаг ShowDescriptions и Size указал misLarge.<br />
<br />
<h3>
6. TdxRibbonGalleryItem.</h3>
<br />
Галерея - один из самых интересных элементов управления DevExpress VCL ExpressBars Components.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDVfN1HDbamBWOGm144CjfYtHGqwFnirOE04bfosecCGysG1b3fN3YNC1sMcaB4S3OE8sUby_djoSpPvEqbv_cMBb4dZQ5xhVPKqyvrHR-T-clwCXl1WJzcEU52NTGQ43snPEKtJhTZ7U/s1600/36.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="292" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDVfN1HDbamBWOGm144CjfYtHGqwFnirOE04bfosecCGysG1b3fN3YNC1sMcaB4S3OE8sUby_djoSpPvEqbv_cMBb4dZQ5xhVPKqyvrHR-T-clwCXl1WJzcEU52NTGQ43snPEKtJhTZ7U/s640/36.png" width="640" /></a></div>
<br />
<strike>Правда имеет один минус - ее нельзя полностью настроить в DesignTime, поэтому работа с ней всегда требует написания кода.</strike><br />
<br />
<b>Update:</b> к сожалению здесь сказалась моя не внимательность. Невозможность настройки можно было наблюдать в ранних версиях пакетов. В текущей версии 12.1.4 полная настройка из DesignTime присутствует.<br />
<br />
Логически галерея разделена на несколько элементов.<br />
1. Группы - любой элемент в галерею добавляется только в группу. Если группа всего одна, ее заголовок можно не отображать.<br />
2. Элементы групп - непосредственное наполнение галереи.<br />
3. Фильтры - расположены в самом верху галереи. При помощи них можно сделать удобную фильтрацию, создав набор фильтров и к каждому из них подключить одну или несколько групп, за видимость которой он будет отвечать.<br />
4. Дополнительные элементы - располагаются внизу (как на скриншоте) и являются стандартными элементами управления перетаскиваемыми из BarManager.<br />
<br />
Откройте <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">четвертый демопример</a> и запустите его.<br />
Вы увидите две абсолютно одинаковых галереи, одна из которых отображается в свернутом режиме (левая), вторая в развернутом.<br />
Так как часть галереи можно настроить из DesignTime, а часть все равно придется прописывать в RunTime, я не стал данные галереи вообще настраивать в DesignTime, просто создал в BarManager-е и поместил на ленту, а код инициализации вынес в конструктор формы.<br />
Выглядит он следующим образом:<br />
<br />
<pre class="brush:delphi">procedure TForm1.InitGallery(Value: TdxRibbonGalleryItem; Collapsed: Boolean);
var
Group: TdxRibbonGalleryGroup;
GroupItem: TdxRibbonGalleryGroupItem;
FilterCategory: TdxRibbonGalleryFilterCategory;
begin
// Устанавливаем иконку по умолчанию
Value.ImageIndex := 0;
// Указываем как будет выглядеть галерея, в виде кнопки или в развернутом виде
Value.GalleryInRibbonOptions.Collapsed := Collapsed;
// Указываем что отображать элементы нужно с использованием заголовка и описания
Value.GalleryInMenuOptions.ItemTextKind := itkCaptionAndDescription;
// Указываем количество колонок в которых будут размещатся элементы
Value.GalleryOptions.ColumnCount := 2;
// Инициализируем фильтры
Value.GalleryFilter.Caption := 'Список фильтров';
Value.GalleryFilter.Visible := True;
// Добавляем первую группу
Group := Value.GalleryCategories.Add;
Group.Header.Caption := 'Страны';
Group.Header.Visible := True;
// Добавдяем ее в фильтры
FilterCategory := Value.GalleryFilter.Categories.Add;
FilterCategory.Caption := Group.Header.Caption;
FilterCategory.Groups.Add(Group); // добавлять можно неколько груп для каждого фильтра
// Добавляем элементы группы
GroupItem := Group.Items.Add;
GroupItem.Caption := 'Россия';
GroupItem.Description :=
'Россия - официально Российская Федерация или Россия,' +
' на практике используется также сокращение РФ';
GroupItem.ImageIndex := 1;
GroupItem := Group.Items.Add;
GroupItem.Caption := 'США';
GroupItem.Description := 'Соединённые Штаты Америки были' +
' образованы в 1776 году при объединении тринадцати ' +
'британских колоний, объявивших о своей независимости.';
GroupItem.ImageIndex := 2;
GroupItem := Group.Items.Add;
GroupItem.Caption := 'Норвегия';
GroupItem.Description := 'Королевство Норвегия, Норвегия — ' +
'государство в Северной Европе, располагающееся в западной' +
' части Скандинавского полуострова';
GroupItem.ImageIndex := 3;
// Добавляем вторую группу
Group := Value.GalleryCategories.Add;
Group.Header.Caption := 'Разное';
Group.Header.Visible := True;
// Добавдяем ее в фильтры
FilterCategory := Value.GalleryFilter.Categories.Add;
FilterCategory.Caption := Group.Header.Caption;
FilterCategory.Groups.Add(Group);
// Добавляем элементы группы
GroupItem := Group.Items.Add;
GroupItem.Caption := 'Почтовый конверт';
GroupItem.Description := 'Конверт — оболочка для вкладывания, ' +
'хранения и пересылки бумаг или плоских предметов';
GroupItem.ImageIndex := 4;
GroupItem := Group.Items.Add;
GroupItem.Caption := 'Патрон';
GroupItem.Description := 'Патрон (унитарный патрон) — боеприпас ' +
'стрелкового оружия и малокалиберных (до 76 мм) пушек, который ' +
'заряжается в один (лат. unitas — «единство») приём';
GroupItem.ImageIndex := 5;
GroupItem := Group.Items.Add;
GroupItem.Caption := 'Калькулятор';
GroupItem.Description := 'Калькулятор (лат. calculator «счётчик») — ' +
'электронное вычислительное устройство для выполнения операций ' +
'над числами или алгебраическими формулами';
GroupItem.ImageIndex := 6;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
InitGallery(dxRibbonGalleryItem1, True);
InitGallery(dxRibbonGalleryItem2, False);
end;
</pre>
<br />
Думаю по данному коду вы сможете разобраться какие части можно исключить и выполнить настройку параметров в DesignTime, а какую оставить на RunTime.<br />
<br />
Обработчик галерей так же достаточно тривиальный:<br />
<br />
<pre class="brush:delphi">procedure TForm1.GalleryGroupItemClick(Sender: TdxRibbonGalleryItem;
AItem: TdxRibbonGalleryGroupItem);
begin
// в качестве демо назначаем иконку выбранного элемента галерее
Sender.ImageIndex := AItem.ImageIndex;
// показываем что выбрали
ShowMessage(AItem.Caption</pre>
<br />
<h3>
7. TcxBarEditItem</h3>
<br />
Большинство элементов управления доступные через BarManager достаточно простые в настройке и затруднений с их использованием вызвать не должны. Единственно остановлюсь на достаточно нестандартном в использовании компоненте TcxBarEditItem. Он комбинирует в себе множество элементов управления, от гиперссылки до контейнера изображений.<br />
<br />
Обычно происходит недопонимание с тем, где именно менять значения данного контрола? Допустим когда он находится в режиме чекбокса, у него отсутствует значение Checked или хотя бы Down. Когда он отображает картинку, нет свойства Picture или Bitmap.<br />
<br />
На самом деле все достаточно просто, все значения задаются через одно единственное свойство - EditValue имеющее тип Variant.<br />
<br />
В <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">пятом демопримере</a> я создал единственный элемент TcxBarEditItem и разместил четыре кнопки, показывающих переключение данного контрола в один из расширенных стилей и примерную работу с ним.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEia47przEMlKqX9VWer_O7U2EK9S0nXnbT6y03q6OBztsLZW-DeCo2iFL6kDaWO3rSD7Sz2wqg1_BjJlcYOAIuepDQDrXAa-Kzviq9di_ki3ZcJwM0Q3ojdVlEfKqrqWJ740Zuj3Ex6bvg/s1600/37.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEia47przEMlKqX9VWer_O7U2EK9S0nXnbT6y03q6OBztsLZW-DeCo2iFL6kDaWO3rSD7Sz2wqg1_BjJlcYOAIuepDQDrXAa-Kzviq9di_ki3ZcJwM0Q3ojdVlEfKqrqWJ740Zuj3Ex6bvg/s1600/37.png" /></a></div>
<br />
<br />
Изначально TcxBarEditItem находится в режиме DateEdit.<br />
Обработчик первой кнопки переключает его в режим CheckBox-а<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button1Click(Sender: TObject);
// uses cxCheckBox
begin
// Устанавливаем заголовок
cxBarEditItem1.Caption := 'CheckBoxItem';
// переключаем режим на CheckBox
cxBarEditItem1.PropertiesClass := TcxCheckBoxProperties;
// Указываем контролу что в случае неопределенного значения оставаться в отключенном состоянии
(cxBarEditItem1.Properties as TcxCheckBoxProperties).NullStyle := nssUnchecked;
// Устанавливаем изначальное значние
cxBarEditItem1.EditValue := True;
// далее работаем непосредственно с линком
// так как линк у нас единственный, то сразу напрямую обращаемся к его свойствам
// Устанавливаем ширину (в данном случае ширину пространства на которой будет расположен сам чек)
cxBarEditItem1.Links[0].UserWidth := 0;
// указываем что Caption контрола должен быть всегда справа
cxBarEditItem1.Links[0].ViewLayout := ivlGlyphControlCaption;
end;
</pre>
<br />
Как видите достаточно нестандартный подход :)<br />
В зависимости от класса, указанного в PropertiesClass компонент переключается в тот или иной режим отображения. Для доступа получения к расширенным свойствам приходится кастовать Properties к требуемому классу и только тогда производить изменения. А чтобы настроить вид отображения приходится обращаться дополнительно к свойствам линка.<br />
Непривычно, но не критично :)<br />
<br />
Кстати вот картинка что будет если не производить дополнительную конфигурацию через линк.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGGswxf-LEPSmNqwpOUIfokVu50BsZSPQu-pSosbAkt2jbGJj_3lzgnUyu5rNHePNY1FZoHObFYLVackdZRuhpBhYn5tKno84dPzCpiEUPpTzOchl_VVIwX4eFefUpGX_q6Z8hILkbS10/s1600/38.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGGswxf-LEPSmNqwpOUIfokVu50BsZSPQu-pSosbAkt2jbGJj_3lzgnUyu5rNHePNY1FZoHObFYLVackdZRuhpBhYn5tKno84dPzCpiEUPpTzOchl_VVIwX4eFefUpGX_q6Z8hILkbS10/s1600/38.png" /></a></div>
<br />
Так получилось из-за того, что ранее данный линк отображал DateEdit и все настройки остались от него, вместо кучи пустого места где отображается чек - был EDIT, от него осталась ширина, ну а то что чек расположен справа - это тоже наследство от предыдущего состояния.<br />
<br />
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">На самом деле, это поведение некорректно. Спасибо, что указали на проблему. На данный момент ведется работа по ее исправлению.</span></blockquote>
Если хотите поиграться с настройками линка в режиме DesignTime щелкните правой кнопкой на требуемом элементе и выберите пункт "Select Link", после чего откроются параметры уже самого линка (по умолчанию отображаются параметры виртуального элемента).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhb_h84jylV8gJI5B8aFz3TTPfNFvNkeHRivxS5frJE3l3K0UNkQKPhT3ywao9iYC8rQ5tH6FTrp0yyNvtgmjI_T1tSxmzm07d0qzHwcw56tCZdwbdA5EnV1BAVhXXTaftWpbWaZz4o_2A/s1600/39.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="348" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhb_h84jylV8gJI5B8aFz3TTPfNFvNkeHRivxS5frJE3l3K0UNkQKPhT3ywao9iYC8rQ5tH6FTrp0yyNvtgmjI_T1tSxmzm07d0qzHwcw56tCZdwbdA5EnV1BAVhXXTaftWpbWaZz4o_2A/s640/39.png" width="640" /></a></div>
<br />
<br />
А вот так выглядит переключение в режим Image и загрузка изображения:<br />
<br />
<pre class="brush:delphi">procedure TForm1.Button2Click(Sender: TObject);
// uses cxImage
var
M: TMemoryStream;
S: AnsiString;
begin
cxBarEditItem1.Caption := 'ImageItem';
cxBarEditItem1.PropertiesClass := TcxImageProperties;
(cxBarEditItem1.Properties as TcxImageProperties).GraphicClass := TBitmap;
M := TMemoryStream.Create;
try
M.LoadFromFile(ExtractFilePath(ParamStr(0)) + 'tmp.bmp');
SetLength(S, M.Size);
M.ReadBuffer(S[1], M.Size);
cxBarEditItem1.EditValue := S;
finally
M.Free;
end;
cxBarEditItem1.Links[0].UserWidth := 100;
cxBarEditItem1.Links[0].ViewLayout := ivlGlyphCaptionControl;
end;
</pre>
<br />
Особенно обратите внимание на загрузку картинки в EditValue - очень не привычно :)<br />
<br />
<h3>
8. TdxBarItemLink</h3>
<br />
Как я говорил ранее, все элементы, расположенные в BarManager являются виртуальными. За их отображение отвечает класс TdxBarItemLink. Обычно параметры данного класса используются для более тонкой настройки отображения элементов.<br />
<br />
Например, если вы создадите шесть кнопок и разместите их на тулбаре, то выглядеть они как правило будут вот так:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC2ywpZxQL8FMnbupjl_ySoNdt91xuxDABSgO7apJ8AZ9fucn42B5Vtni_ZRlJJ5-WOvTIP_jceNypYJbVH5WOKWvYNktNiLfeWtY99bmd1-4BsrEd88ajHRL1w4J4Nuh-bvqh70h88BM/s1600/40.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC2ywpZxQL8FMnbupjl_ySoNdt91xuxDABSgO7apJ8AZ9fucn42B5Vtni_ZRlJJ5-WOvTIP_jceNypYJbVH5WOKWvYNktNiLfeWtY99bmd1-4BsrEd88ajHRL1w4J4Nuh-bvqh70h88BM/s1600/40.png" /></a></div>
<br />
Если у линка элемента 4 выставить параметр BeginGroup то элементы с четвертого по шестой будут отделены от первых трех:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHFC3rVNgKHU5jKDuPKiquw1-U2ONefZZ76co8tJltMMdSRW9-Ky-9mS4Q5zNHWGZlXvXF70mBfUw4GW9MoOfRW985AzMabfYkQcaoN_zO2UqWpidTuppEpsFSUDaZrgnx9i9CsYOQOa0/s1600/41.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHFC3rVNgKHU5jKDuPKiquw1-U2ONefZZ76co8tJltMMdSRW9-Ky-9mS4Q5zNHWGZlXvXF70mBfUw4GW9MoOfRW985AzMabfYkQcaoN_zO2UqWpidTuppEpsFSUDaZrgnx9i9CsYOQOa0/s1600/41.png" /></a></div>
<br />
Допустим мы хотим изменить расположение элементов, чтобы с первого по третий шли в линию друг за другом, а ниже них оставшиеся элементы 4-6, для этого свойство Position линков на элементы 2, 3, 5 и 6 должно быть установлено в ipContinuesRow, а у элементов 1 и 4 должно остаться в изначальном ipBeginsNewRow.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6lAMEssqnAM0dpPvMHfzu7orYIm_PXqY_uYBm9cRvvFtxf6UBV89afdF6dCd-MCvtDmojun5jCLnsTTTFsXG-oMxU9AKvl5oms3aFAobBf5U9SyavUKPm2Kx7libUhHjXfrYFvxczt_Y/s1600/42.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6lAMEssqnAM0dpPvMHfzu7orYIm_PXqY_uYBm9cRvvFtxf6UBV89afdF6dCd-MCvtDmojun5jCLnsTTTFsXG-oMxU9AKvl5oms3aFAobBf5U9SyavUKPm2Kx7libUhHjXfrYFvxczt_Y/s1600/42.png" /></a></div>
<br />
Параметр ViewLayout, как и говорилось выше отвечает за расположение элемента пользовательского ввода и заголовка элемента. С ним главное не переборщить, ибо можно нарваться на вот такие графические артефакты у компонентов не поддерживающих перестановку этих двух элементов местами:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqYJxuwc17yeSk46ORHTipi1XEu0HlkYZMxXthlik88HIFux-0v9DC7yi-ksaQnDSbVokQ2-t50hnjiaNiAgtuAnm59VoQWI8eORaL4MaNAPXAMDtW-gNZP8JBvTXjlwXUIHnciPfzDj0/s1600/43.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqYJxuwc17yeSk46ORHTipi1XEu0HlkYZMxXthlik88HIFux-0v9DC7yi-ksaQnDSbVokQ2-t50hnjiaNiAgtuAnm59VoQWI8eORaL4MaNAPXAMDtW-gNZP8JBvTXjlwXUIHnciPfzDj0/s1600/43.png" /></a></div>
<br />
Здесь изображены два TcxBarEditItem в режиме DateEdit, только у нижнего ViewLayout выставлен в ivlGlyphControlCaption, в результате чего получилась такая вот неприятность :)<br />
<br />
<h3>
9. Работа с TdxRibbonBarPainter</h3>
<br />
Теперь немного практической части. Если вы вдруг решите делать собственный компонент, который должен быть отрисован с использованием текущего скина ленты, единственный вариант достичь этого - использование TdxRibbonBarPainter. Как пример вышепоказанный компонент TFWBackStageButton.<br />
<br />
Работа с пайнером достаточно проста и выглядит примерно так:<br />
<br />
<pre class="brush:delphi">var
Painter: TdxRibbonBarPainter;
begin
Painter := TdxRibbonBarPainter.Create(TdxNativeUInt(dxRibbon1));
try
Painter.Skin.DrawBackground(Canvas.Handle, Rect, Part, State);
finally
Painter.Free;
end;
</pre>
<br />
В конструкторе пайнтера передается ссылка на текущий экземпляр класса TdxCustomRibbon, а далее производится вызов метода DrawBackground в котором двумя последними параметрами указывается какой именно элемент требуется отрисовать и его состояние.<br />
<br />
Так же у пайнтера есть дополнительные методы позволяющие узнать такие параметры как размеры элементов, например SubMenuControlGetScrollBandSize, есть утилитарные методы отрисовок, к примеру DrawMultilineCaption и прочее. Все это вы сможете увидеть в исходниках модуля dxBar.<br />
<br />
Константы элементов и их состояний объявлены в модуле dxBarSkinConsts.<br />
<br />
Для облегчения поиска подходящего бэкграунда я написал небольшой примерчик (<a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">шестой демопример</a>).<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVnWg3dqdj4b7RQeurBnXuPXqT_Cba3raiDlLFkgc2nhHBZQEXbAnaer87n5KfKXSok4Z-TadwR88K5PsKpM0am0yyyz_h6xsa5worAi3bngWunra4G9l1lOv5Iw4sq34MqFoSCk2zD6Y/s1600/44.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="344" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhVnWg3dqdj4b7RQeurBnXuPXqT_Cba3raiDlLFkgc2nhHBZQEXbAnaer87n5KfKXSok4Z-TadwR88K5PsKpM0am0yyyz_h6xsa5worAi3bngWunra4G9l1lOv5Iw4sq34MqFoSCk2zD6Y/s640/44.png" width="640" /></a></div>
<br />
При помощи галереи вы сможете быстро переключаться между скинами, а при помощи трекбара смотреть варианты отрисовок всех доступных элементов во всех их состояниях.<br />
<br />
<a href="http://www.blogger.com/blogger.g?blogID=2374465879949372415" name="errors"></a>
<br />
<h3>
10. Ошибки</h3>
<br />
К сожалению ошибки всегда присутствуют в коде, как бы разработчик ни старался. Правда здесь можно выразить большой респект разработчикам DevExpress - ошибки они правят очень оперативно и за последние шесть лет пользования их продуктом на моей памяти поменялось в этом плане очень много. Думаю я не ошибусь если поставлю их в один ряд по оперативности правки глюков с разработчиками Wine (линуксовый эмулятор Win) и ребят из компании EurekaLog, с которыми я постоянно работаю на уровне фидбэков, работающих на принципе - нет выпуску новой версии без правки известных ошибок (практически не утрирую :).<br />
<br />
Впрочем хватит петь дифирамбы, вернемся обратно к ошибкам:<br />
Об одной ошибке, с которой вы можете столкнуться, я уже рассказал чуть выше (рассинхронизация ImageIndex) а вот и вторая.<br />
<br />
В наличии есть некорректная работа с TAction, а именно с состоянием Checked (мы говорим только о версии 12.1.4 и более ранних).<br />
Воспроизводится данная ошибка достаточно банально, откройте <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">пример из папки Bug</a> и посмотрите на исходный код.<br />
<br />
Смысл данного примера заключается в следующем:<br />
Созданы три TAction у каждого из которых параметр "Tag" имеет некое значение (для простоты 1, 2 и 3) и имеется параметр "Foo", который содержит в себе номер элемента, который должен быть в состоянии Checked.<br />
<br />
Логика работы всех трех TAction простая, в Execute берем значение параметра "Tag" у активного акшена и назначаем его параметру "Foo", а в Update возвращаем состояние Checked, ориентируясь на значение параметра "Foo" и "Tag" акшена.<br />
<br />
Грубо говоря этим мы эмулируем поведение данных трех элементов в режиме RadioButton.<br />
<br />
А теперь запустите пример, вызовите меню и щелкните на первом элементе.<br />
После чего опять вызовите меню, первый элемент будет выделен, как и должно быть.<br />
Снова щелкните на первом элементе.<br />
<br />
По логике ничего изменится не должно т.к. в результате этих двух действий выполнилось присваивание единицы параметру "Foo" и при следующем отображении меню первый пункт должен быть нажат, однако вызовите снова меню и вы увидите что это не так.<br />
<br />
Хоть параметр "Foo" сейчас принял значение 1 и "Tag" акшена, на который слинкован первый пункт меню также равен единице и, даже более того, метод Action1Update тоже был вызван выставив правильное состояние Checked, первый пункт меню сейчас не выделен.<br />
<br />
А вот это уже ошибка.<br />
<br />
Ну либо второй вариант воспроизведения: запустите приложение и в меню выберите второй или третий пункт, после чего щелкните на кнопку внизу "Выбрать первый пункт" и снова вызовите меню. Хоть мы и попытались через TAction реализовать логику TRadioButton, этого у нас не получилось и вы увидите два (!!!) выделенных пункта меню.<br />
<br />
Почему это происходит и как это поправить:<br />
<br />
Все дело в том что в методе TdxBarButtonControl.ControlUnclick не учитывается возможность того, что к элементу может быть подключен TAction и всегда вызывается принудительная смена состояния параметра Down, который как раз и линкуется на параметр TAction.Checked.<br />
<br />
Поправить данную ошибку достаточно просто:<br />
<br />
<pre class="brush:delphi">procedure TdxBarButtonControl.ControlUnclick(ByMouse: Boolean);
begin
if not ByMouse or (HotPartIndex = bcpButton) then
begin
if (ButtonItem <> nil) and (bstChecked in ButtonItem.FInternalStates) then
// Исправление ошибки работы с TAction.Checked
if (ItemLink <> nil) and (ItemLink.Item <> nil) and (ItemLink.Item.Action <> nil) then
begin
// управление параметром Down отдаем на откуп Action-у
end
else
// если Action не слинкован - воспроизводим стандартное поведение
ButtonItem.Down := not ButtonItem.Down;
ControlInactivate(True);
inherited;
end;
end;
</pre>
<br />
Правда для этого придется править уже модуль dxBar.<br />
<br />
<b><span style="color: #cc0000;">Комментарий от DevExpress Team:</span></b><br />
<blockquote class="tr_bq">
<span style="color: #444444;">Первая ситуация известна давно и подробно описана в следующем тикете:</span><br />
<span style="color: #444444;"><a href="http://www.devexpress.com/Support/Center/Issues/ViewIssue.aspx?issueid=Q103874">How to deal with TdxBarButton ButtonStyle = bsChecked</a> По поводу второго варианта открыт новый тикет:</span><br />
<a href="http://www.devexpress.com/Support/Center/Issues/ViewIssue.aspx?issueid=B232418"><span style="color: #444444;">TdxBarButtons behave incorrectly on changing the TAction.Checked property if their style is set to bsChecked</span></a></blockquote>
</div>
<div>
Как я и говорил выше, реакция практически моментальная - респект парням :)<br />
<br />
<h3>
11. В качестве заключения</h3>
<div>
<br /></div>
<div>
Все время при написании данной статьи меня не покидало стойкое ощущение что я пишу какую-то банальщину, с которой может разобраться любой при должном желании :)</div>
<div>
В принципе по сути наверное так оно и есть, но с учетом что целью я ставил дать точку для быстрого старта, то, если честно, даже не знаю как можно подать материал по другому. </div>
<div>
<br /></div>
<div>
Но на всякий случай, дабы подсластить пилюлю, я решил в качестве бонуса добавить в папку с компонентами еще один юнит, в котором реализован класс TDragDropManager (пример работы показан в <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">седьмом демопримере</a>). Данный класс предназначен для работы с TcxPageControl.<br />
<br /></div>
<div>
TcxPageControl - это практически полный аналог стандартного TPageControl но у него есть несколько "вкусных" возможностей, в частности уже реализованный механизм перетаскивания табов обычным DragDrop. Правда перетаскивать можно только в рамках самого TcxPageControl, на другой перетащить не получится.</div>
<div>
<br /></div>
<div>
Чтобы исправить данную несправедливость и был разработан класс TDragDropManager который слегка помогает экземплярам TcxPageControl обмениваться табами между собой.</div>
<div>
<br /></div>
<div>
На сем откланиваюсь.<br />
<br />
Исходный код демопримеров можно <a href="http://rouse.drkb.ru/blog/dx.zip" target="_blank">забрать здесь</a>.<br />
<br />
Удачи.</div>
<br />
---<br />
<br />
Александр (Rouse_) Багель<br />
Февраль, 2013</div>
</div>
</div>
Александр (Rouse_) Багельhttp://www.blogger.com/profile/03072586754182036553noreply@blogger.com27