четверг, 19 декабря 2013 г.

Ответ на задачу №1

Откуда вообще появляются такие вот непонятные куски кода в которых различные авторы предлагают искать ошибки? Вопрос по сути философский - народное творчество :)
Благодаря народному творчеству и неким "неизвестным" товарищам, чьи произведения подхватываются и расползаются на лету по многочисленным форумам, мы можем наблюдать такие перлы как 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

17 комментариев:

  1. Здравствуйте. Если честно понял только половину из того что написано :) Я понимаю что надо больше читать и т.п. Но... может наглость или нет, но все таки... Можно подбить готовый код, как функция будет в итоге выглядеть правильно без глюков? ;)

    ОтветитьУдалить
    Ответы
    1. Ну например вот так:

      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;

      Удалить
    2. Хм, похоже в ответах не поддерживается форматирование...

      Удалить
    3. Хех, а ларчик легко открывался :) При таком подходе значит не будет краша? Я единственное не знаю что такое "Succeeded", ни разу не встречал такой функции. Полез в интернет искать ответ. Спасибо в любом случае за интересный материал.

      Удалить
  2. Добрый день. Большое спасибо за материал. .....очень круто.

    ОтветитьУдалить
  3. По поводу DLLEntryPoint(DLL_PROCESS_ATTACH).
    Необходимость вызова упоминается в документации DLLProc.

    Если в двух словах, то секция begin-end в .dpr файле для DLL и есть DllMain, но с одной особенностью: в самом начале вставлен вызов _InitLib. Сделано так, по моему мнению, для унификации с EXE, в которых begin-end есть WinMain, а в начале вызов _InitExe.

    И вот DLLProc вызывается из _InitLib, а присваивание DLLProc происходит позже.

    ОтветитьУдалить
    Ответы
    1. >> И вот DLLProc вызывается из _InitLib, а присваивание DLLProc происходит позже.
      Абсолютно верно :)

      Удалить
    2. Я раньше думал, что компилятор как-то более интеллектуально генерирует код 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).

      Удалить
    3. Ну да, вопросик интересный, правда боюсь что это уже более для гиков подходит, ибо обычному прикладнику такая информация врятли когда в жизни пригодится :)

      Удалить
  4. Да, порча DllHandle - это косяк. Просто удивительно, как ошибки сами себя покрывают, пока не заглянешь в окно CPU, или не пронаблюдаешь за другими переменными, ну или наконец, не *прочитаешь*, а не посмотришь по-диагонали, документацию :)
    Спасибо, что показали именно такой пример.
    Про LoadLibrary <> 0 и LoadLibrary >= $1000 хитро завернуто. Тянет на Secure Delphi Programming, очень бы хотелось почитать про это.

    ОтветитьУдалить
    Ответы
    1. >> Про LoadLibrary <> 0 и LoadLibrary >= $1000 хитро завернуто. Тянет на Secure Delphi Programming, очень бы хотелось почитать про это.
      Я уже готовлю статью, но это явно не в январе.
      Необходимо полностью подготовить весь код для полноценной демонстрации...
      Боюсь может затянуться на несколько месяцев, как это произошло со статьей о Карте памяти процесса.

      Удалить
    2. Тогда с карты памяти чтение можно и начать. Я много раз оставлял ту статью в браузере, но так и не хватило духу прочитать. Для таких как я, наверное, было бы удобнее разбить ее на Часть1, Часть2,.., что можно сделать самостоятельно.

      Удалить
    3. Сильно дробить материал на куски не получится, иначе изложение выходит рваным и немного оторванным от контекста, поэтому приходится постоянно ваять многостраничные тексты, дабы охватить всю проблему целиком :)

      Удалить
  5. А ведь уже было http://stackoverflow.com/questions/7198646/accessviolationexception-in-delphi-impossible-check-it-unbelievable

    ОтветитьУдалить
    Ответы
    1. Дык об чем и речь шла :)
      Сколько лет прошло, а люди все еще спотыкаются об неверную декларацию "IsEnabled: PBoolean"

      Удалить
  6. "Моя задача, перед проведением собеседования, заключается в подготовке материала, который поможет мне "расшатать" соискателя"
    Простите, но это больше похоже на конторы в которых не хотят платить деньги и потому задают задротские вопросы, чтобы тем самым показать соискателю что он что-то да не знает. Ну а раз не знает, значит платить будем мало. То, что чего-то человек не знает - это нормально и естественно. Докапаться то можно даже до столба. Нормальный человек должен узнать, то что соискатель знает, а не то чего он не знает.

    ОтветитьУдалить
    Ответы
    1. Нет, это больше похоже на тестирование с целью понять в каких вопросах человек плавает, чтобы давать ему те задачи с которыми он справится без проблем :)

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

      Удалить