Откуда вообще появляются такие вот непонятные куски кода в которых различные авторы предлагают искать ошибки? Вопрос по сути философский - народное творчество :)
Благодаря народному творчеству и неким "неизвестным" товарищам, чьи произведения подхватываются и расползаются на лету по многочисленным форумам, мы можем наблюдать такие перлы как WParam в обработчике хука объявленный типом Word, либо реализацию метода Execute класса TThread обернутую в Synhronize да, в прочем, можно увидеть даже перекрытие штатной DLLEntryPoint с соглашением вызова stdcall (какая разница что это только обертка над DllMain - пусть и у нас будет так, как у "взрослых дядек" :)
Особенно обидно становится тогда, когда это приобретает массовый характер.
Очень сложно объяснить человеку на форуме, что тот код, который он прочитал в очередной книжке "для хакеров", по сути не верен чуть менее чем полностью, ведь кто я такой - по сути некий неизвестный аноним в интернет пространстве, а у автора вопроса на руках есть целая книга, выпущенная серьезной издательской конторой, к которой доверия будет явно больше чем к моим ответам :)
Сейчас мы будем рассматривать один из образцов такого кода.
Он достаточно популярен в интернете, к примеру:
http://theroadtodelphi.wordpress.com/2009/10/26/detect-aero-glass-using-delphi/
или вот так:
http://www.sql.ru/forum/900738/kak-uznat-vkluchen-li-aero-v-window-7-vista
Где посреди прочих разумных вариантов звучит и такой: "Этот код не приводит к крешу приложения, ищи причину в другом месте."
Представьте, вы еще плаваете немного с различными указателями и прочим и тут вас огорошивают такой фразой: "этот код валиден - ошибка не здесь". Какие ваши действия? Конечно, вы будете перелопачивать сотни строк кода, пытаясь понять, где ж я промахнулся.
Плюсы, конечно есть - вероятно вам удастся найти еще несколько ошибок и исправить их, но изначальную проблему решить не получится и придется снова и снова строчить вопросы на форумах плана: "программа падает, и похоже даже на 17-ой строке - поможите".
Впрочем... к нашим баранам.
Преамбула выглядела следующим образом:
Дана функция, реализованная в классическом стиле.
Задача, описать что в данном коде не верно.
Основные предложения выглядели так:
1. Результат выполнения функции может быть не определен.
2. Отсутствует проверка результата выполнения DwmIsCompositionEnabledFunc.
3. Условие "DllHandle > HINSTANCE_ERROR" некорректно.
4. Может быть разрушен стек.
5. SEH избыточен (try..finally)
Рассматриваем первый и второй пункт в совокупности.
Да, действительно - это самое первое, что должно бросится в глаза человеку, который будет анализировать код данной функции. В теории, после нахождения данных ошибок, как правило анализ кода останавливается и выносится вердикт, что ошибка найдена.
Правда изначально при постановке задачи я допустил оговорку, что данная задача у меня осталась от собеседований соискателей на вакансию.
Попробую пояснить.
Вы наверное знаете тот старый анекдот, как создать проблему из "ничего"? Очень просто - достаточно взять "ничего" и женщину, проблема готова.
Моя задача, перед проведением собеседования, заключается в подготовке материала, который поможет мне "расшатать" соискателя и определить его готовность решать нестандартные задачи. Поэтому, как правило, приходится брать код с явной ошибкой и как можно более качественно камуфлировать саму ошибку, вытаскивая наружу то, на что сможет среагировать соискатель первым делом (кто проходит сертификацию от MS, или хотя бы изучал публичные тесты от данной конторы, меня поймет :)
Поэтому да, что первый, что второй пункт, были определены верно - но на самом деле ошибка не в них.
Пункт третий.
Ну... начать наверное нужно с того, что переменная DllHandle объявлена как THandle.
В принципе, ничего страшного не произойдет (даже в 64 битном коде), кабы не одно НО.
Никогда нельзя путать хэндл с инстансом.
Хэндлы бывают разные, в частности хэндл USER объектов (окна/таймеры/хуки) представляют из себя определенную структуру, где старшие 16 бит представляют из себя уникальный идентификатор, по которому происходит проверка при обращению к массиву описателей, индекс которого хранится в младших 14 битах + самые нижние 2 бита используются под различные конфигурационные вещи (ну к примеру автоматом закрывать хэндл на директорию при создании процесса и т.п. - об этом я расскажу в последующих статьях).
А вот функция LoadLibrary возвращает не хэндл, а указатель на инстанс загруженного модуля. Грубо адрес, прочитав два байта с которого, мы сможем наблюдать столь известные всему миру инициалы Марка Збиковски "MZ".
Откуда вообще взялся этот HINSTANCE_ERROR?
Тут все просто - это наследие от Win3.1 где все было организовано немного через не так как сейчас :)
А теперь посмотрим как организована память приложения:
Первые 64кб любого процесса (32/64) имеют атрибуты доступа NoAccess.
Данный регион памяти используется для детектирования битых указателей.
Поэтому, раз мы имеем на руках адрес загрузки библиотеки, которая явно не может быть загружена по адресу от нуля до 0x10000, то более корректно будет проверять данный адрес с числом 0x10000, а не с нулем или тем более с HINSTANCE_ERROR.
Хотя лучше конечно придерживаться документации - которая говорит что ноль это плохо.
С наследием от Win 3.1 разобрались, а что сейчас?
А сейчас все гораздо проще, LoadLibrary действительно возвращает ноль, в случае ошибки загрузки библиотеки, но есть одно НО.
Перед тем как принять решение о загрузке библиотеки данная функция пытается найти ее в структуре LDR_DATA_TABLE_ENTRY, которая доступна для модификации из пользовательского кода прямо в Ring3. Если она ее находит, то возвращает тот адрес, который записан в соответствующем поле данной структуры (одного из списков).
Правильно пошаманив с данной структурой мы можем добиться как раз такого поведения, чтобы LoadLibrary выдала не ноль и не валидный инстанс образа, а к примеру число 0x777, к примеру для того чтобы вызвать отказ выполнения кода во взламываемом нами приложении, либо наоборот, мы можем правильно инициализировав кусок памяти представить его как загруженную библиотеку, причем таким образом что штатный вызов GetProcAddress найдет якобы экспортируемую этим модулем функцию (об этом будет отдельная обзорная статья).
Впрочем далее - четвертый пункт (Фабула).
В четвертом пункте содержится вся соль данного кода - разрушение стека (как бы нам ни пытались доказать обратное на форумах).
Суть проста, DwmIsCompositionEnabled хоть и принимает параметром указатель, но под разименованным указателем должен лежать буфер размером в 4 байта, в который она будет писать ровно 4 байта, но ни в коем разе не однобайтовый Boolean.
Посмотрим картинку:
Видны отлично расставленные каменты в OllyDebug - респектую автору, кабы не одно но!!!
Запись в буфер происходит внутри вызова DwmIsCompositionEnabled, а картинка показывает работу с уже "убитым" стеком и конкретнее c переменной Flag, где уже можно "сушить весла".
Впрочем, когда я говорил о том, что пример был специально подготовлен таким образом, чтобы это не бросилось в глаза - я не слукавил.
Данный код был составлен таким образом, чтоб в результате его выполнения и порчи стека поменяется всего лишь значение переменной DllHandle, что сможет заметить только очень крайне внимательный при отладке человек :)
Демонстрирую:
Что в итоге приведет в отказе выгрузки библиотеки, ибо функция FreeLibrary не будет генерировать исключения при невалидном хэндле.
Переписав код вот таким образом, мы сразу получим на руки "бадабум":
Ну а что произойдет, если вдруг побьется адрес, на который ориентируется финализирующий RET - я даже не берусь предсказать.
Еще более интереснее становится при переключении флага оптимизации.
Правильно подготовив параметры функции можно добиться того что из-за выравнивания в одном режиме память будет биться, а в другом нет.
Собственно пункт номер пять я думаю рассматривать смысла не имеет, ибо исходя и предыдущего примера и так понятно, для чего тут нужен try..finally
Вкратце как-то так, думаю данный материал был для вас полезен или по крайней мере заставил немного задуматься.
Впрочем в завершение позволю себе вернуться к вопросу о перекрытии DLLEntryPoint.
Если кто-то сможет внятно объяснить для чего нужно ее перекрытие вот таким образом:
... тому лично от меня респект и уважуха и +1 в карму :)
UPD:
Правильный вариант кода выглядит следующим образом.
---
Благодаря народному творчеству и неким "неизвестным" товарищам, чьи произведения подхватываются и расползаются на лету по многочисленным форумам, мы можем наблюдать такие перлы как WParam в обработчике хука объявленный типом Word, либо реализацию метода Execute класса TThread обернутую в Synhronize да, в прочем, можно увидеть даже перекрытие штатной DLLEntryPoint с соглашением вызова stdcall (какая разница что это только обертка над DllMain - пусть и у нас будет так, как у "взрослых дядек" :)
Особенно обидно становится тогда, когда это приобретает массовый характер.
Очень сложно объяснить человеку на форуме, что тот код, который он прочитал в очередной книжке "для хакеров", по сути не верен чуть менее чем полностью, ведь кто я такой - по сути некий неизвестный аноним в интернет пространстве, а у автора вопроса на руках есть целая книга, выпущенная серьезной издательской конторой, к которой доверия будет явно больше чем к моим ответам :)
Сейчас мы будем рассматривать один из образцов такого кода.
Он достаточно популярен в интернете, к примеру:
http://theroadtodelphi.wordpress.com/2009/10/26/detect-aero-glass-using-delphi/
или вот так:
http://www.sql.ru/forum/900738/kak-uznat-vkluchen-li-aero-v-window-7-vista
Где посреди прочих разумных вариантов звучит и такой: "Этот код не приводит к крешу приложения, ищи причину в другом месте."
Представьте, вы еще плаваете немного с различными указателями и прочим и тут вас огорошивают такой фразой: "этот код валиден - ошибка не здесь". Какие ваши действия? Конечно, вы будете перелопачивать сотни строк кода, пытаясь понять, где ж я промахнулся.
Плюсы, конечно есть - вероятно вам удастся найти еще несколько ошибок и исправить их, но изначальную проблему решить не получится и придется снова и снова строчить вопросы на форумах плана: "программа падает, и похоже даже на 17-ой строке - поможите".
Впрочем... к нашим баранам.
Преамбула выглядела следующим образом:
Дана функция, реализованная в классическом стиле.
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;
Задача, описать что в данном коде не верно.
Основные предложения выглядели так:
1. Результат выполнения функции может быть не определен.
2. Отсутствует проверка результата выполнения DwmIsCompositionEnabledFunc.
3. Условие "DllHandle > HINSTANCE_ERROR" некорректно.
4. Может быть разрушен стек.
5. SEH избыточен (try..finally)
Рассматриваем первый и второй пункт в совокупности.
Да, действительно - это самое первое, что должно бросится в глаза человеку, который будет анализировать код данной функции. В теории, после нахождения данных ошибок, как правило анализ кода останавливается и выносится вердикт, что ошибка найдена.
Правда изначально при постановке задачи я допустил оговорку, что данная задача у меня осталась от собеседований соискателей на вакансию.
Попробую пояснить.
Вы наверное знаете тот старый анекдот, как создать проблему из "ничего"? Очень просто - достаточно взять "ничего" и женщину, проблема готова.
Моя задача, перед проведением собеседования, заключается в подготовке материала, который поможет мне "расшатать" соискателя и определить его готовность решать нестандартные задачи. Поэтому, как правило, приходится брать код с явной ошибкой и как можно более качественно камуфлировать саму ошибку, вытаскивая наружу то, на что сможет среагировать соискатель первым делом (кто проходит сертификацию от MS, или хотя бы изучал публичные тесты от данной конторы, меня поймет :)
Поэтому да, что первый, что второй пункт, были определены верно - но на самом деле ошибка не в них.
Пункт третий.
Ну... начать наверное нужно с того, что переменная DllHandle объявлена как THandle.
В принципе, ничего страшного не произойдет (даже в 64 битном коде), кабы не одно НО.
Никогда нельзя путать хэндл с инстансом.
Хэндлы бывают разные, в частности хэндл USER объектов (окна/таймеры/хуки) представляют из себя определенную структуру, где старшие 16 бит представляют из себя уникальный идентификатор, по которому происходит проверка при обращению к массиву описателей, индекс которого хранится в младших 14 битах + самые нижние 2 бита используются под различные конфигурационные вещи (ну к примеру автоматом закрывать хэндл на директорию при создании процесса и т.п. - об этом я расскажу в последующих статьях).
А вот функция LoadLibrary возвращает не хэндл, а указатель на инстанс загруженного модуля. Грубо адрес, прочитав два байта с которого, мы сможем наблюдать столь известные всему миру инициалы Марка Збиковски "MZ".
Откуда вообще взялся этот HINSTANCE_ERROR?
Тут все просто - это наследие от Win3.1 где все было организовано немного через не так как сейчас :)
А теперь посмотрим как организована память приложения:
Первые 64кб любого процесса (32/64) имеют атрибуты доступа NoAccess.
Данный регион памяти используется для детектирования битых указателей.
Поэтому, раз мы имеем на руках адрес загрузки библиотеки, которая явно не может быть загружена по адресу от нуля до 0x10000, то более корректно будет проверять данный адрес с числом 0x10000, а не с нулем или тем более с HINSTANCE_ERROR.
Хотя лучше конечно придерживаться документации - которая говорит что ноль это плохо.
С наследием от Win 3.1 разобрались, а что сейчас?
А сейчас все гораздо проще, LoadLibrary действительно возвращает ноль, в случае ошибки загрузки библиотеки, но есть одно НО.
Перед тем как принять решение о загрузке библиотеки данная функция пытается найти ее в структуре LDR_DATA_TABLE_ENTRY, которая доступна для модификации из пользовательского кода прямо в Ring3. Если она ее находит, то возвращает тот адрес, который записан в соответствующем поле данной структуры (одного из списков).
Правильно пошаманив с данной структурой мы можем добиться как раз такого поведения, чтобы LoadLibrary выдала не ноль и не валидный инстанс образа, а к примеру число 0x777, к примеру для того чтобы вызвать отказ выполнения кода во взламываемом нами приложении, либо наоборот, мы можем правильно инициализировав кусок памяти представить его как загруженную библиотеку, причем таким образом что штатный вызов GetProcAddress найдет якобы экспортируемую этим модулем функцию (об этом будет отдельная обзорная статья).
Впрочем далее - четвертый пункт (Фабула).
В четвертом пункте содержится вся соль данного кода - разрушение стека (как бы нам ни пытались доказать обратное на форумах).
Суть проста, DwmIsCompositionEnabled хоть и принимает параметром указатель, но под разименованным указателем должен лежать буфер размером в 4 байта, в который она будет писать ровно 4 байта, но ни в коем разе не однобайтовый Boolean.
Посмотрим картинку:
Видны отлично расставленные каменты в OllyDebug - респектую автору, кабы не одно но!!!
Запись в буфер происходит внутри вызова DwmIsCompositionEnabled, а картинка показывает работу с уже "убитым" стеком и конкретнее c переменной Flag, где уже можно "сушить весла".
Впрочем, когда я говорил о том, что пример был специально подготовлен таким образом, чтобы это не бросилось в глаза - я не слукавил.
Данный код был составлен таким образом, чтоб в результате его выполнения и порчи стека поменяется всего лишь значение переменной DllHandle, что сможет заметить только очень крайне внимательный при отладке человек :)
Демонстрирую:
Что в итоге приведет в отказе выгрузки библиотеки, ибо функция FreeLibrary не будет генерировать исключения при невалидном хэндле.
Переписав код вот таким образом, мы сразу получим на руки "бадабум":
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;
Ну а что произойдет, если вдруг побьется адрес, на который ориентируется финализирующий RET - я даже не берусь предсказать.
Еще более интереснее становится при переключении флага оптимизации.
Правильно подготовив параметры функции можно добиться того что из-за выравнивания в одном режиме память будет биться, а в другом нет.
Собственно пункт номер пять я думаю рассматривать смысла не имеет, ибо исходя и предыдущего примера и так понятно, для чего тут нужен try..finally
Вкратце как-то так, думаю данный материал был для вас полезен или по крайней мере заставил немного задуматься.
Впрочем в завершение позволю себе вернуться к вопросу о перекрытии DLLEntryPoint.
Если кто-то сможет внятно объяснить для чего нужно ее перекрытие вот таким образом:
procedure DLLEntryPoint(dwReason: DWORD); //stdcall; <<< нет там stdcall и небыло никогда... begin ... end; begin DLLProc := @DLLEntryPoint; DLLEntryPoint(DLL_PROCESS_ATTACH); // <<< зачем нужен именно этот вызов? ответ объяснить. end.
... тому лично от меня респект и уважуха и +1 в карму :)
UPD:
Правильный вариант кода выглядит следующим образом.
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;
---
© Александр (Rouse_) Багель
Декабрь, 2013
Здравствуйте. Если честно понял только половину из того что написано :) Я понимаю что надо больше читать и т.п. Но... может наглость или нет, но все таки... Можно подбить готовый код, как функция будет в итоге выглядеть правильно без глюков? ;)
ОтветитьУдалитьНу например вот так:
Удалить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;
Хм, похоже в ответах не поддерживается форматирование...
УдалитьХех, а ларчик легко открывался :) При таком подходе значит не будет краша? Я единственное не знаю что такое "Succeeded", ни разу не встречал такой функции. Полез в интернет искать ответ. Спасибо в любом случае за интересный материал.
УдалитьДобрый день. Большое спасибо за материал. .....очень круто.
ОтветитьУдалитьПо поводу DLLEntryPoint(DLL_PROCESS_ATTACH).
ОтветитьУдалитьНеобходимость вызова упоминается в документации DLLProc.
Если в двух словах, то секция begin-end в .dpr файле для DLL и есть DllMain, но с одной особенностью: в самом начале вставлен вызов _InitLib. Сделано так, по моему мнению, для унификации с EXE, в которых begin-end есть WinMain, а в начале вызов _InitExe.
И вот DLLProc вызывается из _InitLib, а присваивание DLLProc происходит позже.
>> И вот DLLProc вызывается из _InitLib, а присваивание DLLProc происходит позже.
УдалитьАбсолютно верно :)
Я раньше думал, что компилятор как-то более интеллектуально генерирует код DllMain. А он делает все то же, что и для EXE, только заменяет вызов _InitExe на _InitLib.
УдалитьТ.е. получается такой код:
function DllMain(hinst: HINSTANCE; Reason: DWORD; Reserved: Pointer): LongBool; stdcall;
begin
_InitLib;// вместо _InitExe
// Код который написан между begin-end в .dpr файле.
// Для примера выше код будет таким:
// begin
DLLProc := @DLLEntryPoint;
DLLEntryPoint(DLL_PROCESS_ATTACH);
// end.
_Halt0;
end;
Чтобы такая функция работала как настоящая DllMain, в недрах RTL происходит весьма забавная магия. Есть идея для следующей задачи: как происходит так, что код между begin-end выполняется только один раз при DLL_PROCESS_ATTACH, хотя DllMain вызывается многократно (DLL_THREAD_ATTACH/DLL_THREAD_DETACH).
Ну да, вопросик интересный, правда боюсь что это уже более для гиков подходит, ибо обычному прикладнику такая информация врятли когда в жизни пригодится :)
УдалитьДа, порча DllHandle - это косяк. Просто удивительно, как ошибки сами себя покрывают, пока не заглянешь в окно CPU, или не пронаблюдаешь за другими переменными, ну или наконец, не *прочитаешь*, а не посмотришь по-диагонали, документацию :)
ОтветитьУдалитьСпасибо, что показали именно такой пример.
Про LoadLibrary <> 0 и LoadLibrary >= $1000 хитро завернуто. Тянет на Secure Delphi Programming, очень бы хотелось почитать про это.
>> Про LoadLibrary <> 0 и LoadLibrary >= $1000 хитро завернуто. Тянет на Secure Delphi Programming, очень бы хотелось почитать про это.
УдалитьЯ уже готовлю статью, но это явно не в январе.
Необходимо полностью подготовить весь код для полноценной демонстрации...
Боюсь может затянуться на несколько месяцев, как это произошло со статьей о Карте памяти процесса.
Тогда с карты памяти чтение можно и начать. Я много раз оставлял ту статью в браузере, но так и не хватило духу прочитать. Для таких как я, наверное, было бы удобнее разбить ее на Часть1, Часть2,.., что можно сделать самостоятельно.
УдалитьСильно дробить материал на куски не получится, иначе изложение выходит рваным и немного оторванным от контекста, поэтому приходится постоянно ваять многостраничные тексты, дабы охватить всю проблему целиком :)
УдалитьА ведь уже было http://stackoverflow.com/questions/7198646/accessviolationexception-in-delphi-impossible-check-it-unbelievable
ОтветитьУдалитьДык об чем и речь шла :)
УдалитьСколько лет прошло, а люди все еще спотыкаются об неверную декларацию "IsEnabled: PBoolean"
"Моя задача, перед проведением собеседования, заключается в подготовке материала, который поможет мне "расшатать" соискателя"
ОтветитьУдалитьПростите, но это больше похоже на конторы в которых не хотят платить деньги и потому задают задротские вопросы, чтобы тем самым показать соискателю что он что-то да не знает. Ну а раз не знает, значит платить будем мало. То, что чего-то человек не знает - это нормально и естественно. Докапаться то можно даже до столба. Нормальный человек должен узнать, то что соискатель знает, а не то чего он не знает.
Нет, это больше похоже на тестирование с целью понять в каких вопросах человек плавает, чтобы давать ему те задачи с которыми он справится без проблем :)
УдалитьВсе знать не возможно, к примеру я достаточно сильно плаваю в вопросах связанных с базами данных и ничего страшного, занимаюсь другими вещами, не имеющими отношение к базам :)