Изучаем отладчик, часть третья

от автора

Что такое отладчик, как им пользоваться и как он реализован, после прочтение первой и второй части статьи, вы знаете. В заключительной части статьи попробуем рассмотреть некоторые методы борьбы с отладчиком, на основе знаний о принципах его работы. Я не буду давать шаблонный набор антиотладочных приемов, благо при желании все это можно найти на просторах интернета, попробую это сделать немного другим способом, на основе некоего абстрактного приложения, у которого буду расширять код защиты от самой простейшей схемы до… пока не надоест 🙂

Сразу-же оговорюсь, в противостоянии приложение/отладчик, всегда победит последний 🙂
Но, только в том случае, если им будет пользоваться грамотный специалист, а с такими спецами бороться практически бесполезно (ну, если вы конечно не обладаете как минимум такой же квалификацией).

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

Вот что-то такое мы и рассмотрим, только в очень упрощенной форме.

Простейшая ShareWare:

Представим, у нас есть некое ПО, которое мы решили продавать. Для простоты пусть это будет обычное VCL приложение из пустой формы (ну хорошо, пусть не пустое, а с картинкой на всю морду) и мы хотим его продать. Первый же вопрос, которым нужно озаботится — как сделать так, чтобы наша картинка был видна только тем, кто за нее заплатил? Точнее — как разграничить триальных и легальных пользователей?

Самым очевидным решением является ключ. Триальный пользователь его не знает, а легальный, заплативший за него реальными деньгами, может активировать легальную копию приложения и насладиться картинкой.

Ключ так ключ.

Создаем новое VCL приложение, кидаем на форму TImage с картинкой, Visible ему выставляем в False. После чего размещаем на форме два TEdit, первый для имени пользователя и второй для кода активации. Ну и две кнопки — закрыть приложение и активировать.

Ну вот как-то так:

image

После чего пишем «совершенно секретный» код активации:

function TForm1.GenerateSerial(const AppUserName: string): string; const   MagicSerialMask: int64 = $C5315E6121543992; var   I: Integer;   SN: int64;   RawSN: string; begin   SN := 0;   Result := '';   for I := 1 to Length(AppUserName) do   begin     Inc(SN, Word(AppUserName[I]));     SN := SN * 123456;   end;   Sn := SN xor MagicSerialMask;   RawSN := IntToHex(SN, 16);   for I := 1 to 16 do     if ((I - 1) mod 4 = 0) and (I > 1) then       Result := Result + '-' + RawSN[I]     else       Result := Result + RawSN[I]; end;   procedure TForm1.btnCheckSerialClick(Sender: TObject); begin   if edSerial.Text <> GenerateSerial(edAppUserName.Text) then     Application.MessageBox('Неверный код активации',       PChar(Application.Title), MB_OK or MB_ICONERROR)   else   begin     Image1.Visible := True;     Label1.Visible := False;     Label2.Visible := False;     Label3.Visible := False;     edAppUserName.Visible := False;     edSerial.Visible := False;     btnCancel.Visible := False;     btnCheckSerial.Visible := False;   end; end; 

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

Допустим такая:

image

(ну… первое что нашел 🙂

После произведенных манипуляций, «это» публикуется на различных шароварных сайтах и даже иногда даются ссылки на программерские форумы в топиках вида: «потестируйте плз защиту».

А как это выглядит со стороны взломщика?

Он берет отладчик (для простоты возьмем тот-же Olly Debug) и видит вот такую картинку:

image

Кода приложения у него нет, но есть характерная ошибка начинающих «защитников» ПО — вывод диалога о неверном ключе.

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

image

На картинке точка принятия решения программой выделена восклицательными знаками.
Все что ему остается сделать, это исправить инструкцию JE на JMP, отключив, таким образом, проверку серийного кода и обеспечив валидный переход на область кода, который должен выполнятся только при активации приложения.

Как-то не понятно, да?

Ну тогда вот вам такая картинка, из отладчика Delphi:

image

Здесь код более понятен для отладки, и его чтение более удобно из-за размапливания адресов и приведения их в читабельный вид. Например теперь явно видно что перед выходом на адрес 0х475729, по которому происходит принятие решения, происходит получение текста из TEdit-ов и вызов процедуры GenerateSerial.

Взломщик такой информацией не обладает, и как видно на предыдущем изображении, ему придется проанализировать все вызовы, чтобы составить более-менее понятную для анализа картинку. Ну, правда, тут я немного утрирую, в действительности карта приложения строится достаточно просто, при наличии инструментария, но… Но некоторые иногда упорно стараются отлаживать системные модуля дельфи, таки честь им и хвала за упорство 🙂

Ну и нюанс, по адресу 0х475729 на скринах расположены две разные инструкции — JZ и JE, это нюансы интерпретации дизассемблеров, они идентичны.

Тут есть один интересный подход, который мне несколько раз озвучивали.
Вот чуть выше я озвучил что поставлю ВР на MessageBoxA, а мне говорят что вызовут MessageBoxW и вызов отловить не получится. Это заявление на твердую четверку с плюсом, ибо да, действительно, если приложение вызовет юникодную API, с бряком будет небольшой промах, но есть нюанс. А давайте-ка развернем весь стек вызова MessageBox.

Смотрите какая интересная схема получается:
MessageBoxA -> MessageBoxExA -> MessageBoxTimeOutA -> MessageBoxTimeOutW-> SoftModalMessageBox()

Таки да, мы можем поставить ВР на вызове любой из перечисленных функций (обычно достаточно MessageBoxTimeOutW) чтобы отловить необходимый нам вызов, ее кстати так же вызовет и функция MessageBoxW.

Есть правда небольшой нюансик, в Delphi есть и иные способы отображения окна.

Ну например ShowMessage(). Данный метод не вызывает API MessageBox.
Достаточно забавно слушать рассуждения, что данный метод целиком и полностью реализован в виде создания отдельной формы, в которой кнопки размещаются так как им надо и вообще это внутренности самого VCL из которых в отладчике вообще ничего не понятно.
Так-то оно так, кабы данный вызов не упирался в API ShowWindow, с которого по стеку мы так же выйдем на необходимый нам участок кода.

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

Поэтому, делайте первый вывод в свой блокнотик:
Вызов сообщения о неуспешной проверке кода, сразу после данной проверки — есть признак дурного тона.

Вводим контроль целостности приложения:

Ну чтож — вот нас и взломали, причем сделав всего лишь изменение в одном единственном байте приложения. Теперь наша веселая картинка доступна всем абсолютно бесплатно.

Печально, но не критично — будем бороться…

Взлом произошел посредством прямой правки тела приложения.
Значит выросла задача: обеспечить проверку целостности исходного кода.

Звучит грозно, но в действительности практически не выполнимо 🙂

Вот что мы можем применить для данной проверки?
Есть много умных слов: навесить цифровую подпись, сверить с образом файла на диске, проверить участок кода с контрольной суммой. Все пустое — в итоге все равно приходим к необходимости каким либо образом получить текущее значение кода приложения в памяти…

Ну хорошо: смотрим цифровую подпись. Она, во первых, платная. Во вторых проверка ее производится путем вызова API функции WinVerifyTrust, которая уязвима к перехвату. В третьих она легко удаляется штатными средствами через ImageRemoveCertificate.

Значит не вариант, что у нас по проверке образа файла на диске?
Тут тоже все печально. Смотрите, наш исполняемый файл пропатчили, мы хотим определить это сравнив с образом на диске и что мы делаем — получаем путь к текущему файлу через тот же ParamStr(0) (допустим) после чего открываем файл по данному пути и начинаем проверку, но…
Но на этапе вызова OpenFile/CreateFile взломщик подменяет путь в соответствующем параметре на путь к оригинальному, не измененному образу и все наши проверки идут лесом.

Есть еще один интересный момент. А ведь ваше приложение может храниться на диске и в не измененном виде. Есть такое понятие как лоадеры. Суть их заключается в том, что они запускают процесс и производят модификацию тела приложения непосредственно в памяти.

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

procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;   ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;   var ReleaseBreakpoint: Boolean); var   JmpOpcode: Byte; begin   if ExceptionRecord.ExceptionAddress =     Pointer(FCore.DebugProcessData.EntryPoint) then   begin     JmpOpcode := $EB;     FCore.WriteData(Pointer($475729), @JmpOpcode, 1); 

Приложение на диске останется неизменным, но вот вместо инструкции JE будет выполнен прямой переход из-за записанной инструкции JMP. Что уже гораздо печальней, т.к. в данном случае первые два варианта проверки целостности гарантированно не сработают.

Остается третий вариант, проверка участков кода непосредственно в теле приложения.
Это достаточно ресурсоемкий по реализации вариант и так же не всегда приводящий к успеху по следующим причинам.

Во первых константы контрольных сумм. Если они хранятся в теле приложения, взломщик их изменит на правильные. (второй вывод в ваш блокнотик — константы CRC блоков кода в приложении, есть дурной тон).
Во вторых, во второй части статьи я рассказывал о МВР — Memory Breakpoint. Это идеальный механизм детектирования проверок целостности кода (если не учитывать еще более грамотный HBP — Hardware BreakPoint).

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

Ну вот мы собственно и приплыли к патовой ситуации: абонент — не абонент 🙂

Впрочем…

image

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

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

Метки наше «всё».
На основе меток работает большинство навесных протекторов, стало быть зачем нам придумывать очередной велосипед. Что такое метка — в принципе это столь нелюбимый всеми label, используемый при goto(), о котором свое высококвалифицированное «ФИ» не высказал только самый ленивый.
Впрочем… что нам их мнение? Как я и сказал — метки наше все 🙂

Правда есть нюансик, label удобно использовать при контролировании небольшой части кода внутри процедуры (при перекрестном контроле — о нем позже), сейчас-же нас интересует несколько процедур в совокупности.

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

Ну и нужна до кучи сама процедура расчета целостности, а так же (что собственно было одним из озвученных выше нюансов) некая константа, с которой мы будем сверять CRC блока данных.

Ну впрочем хватит разглагольствовать, пишем:

const   CheckedCodeValidCheckSum: DWORD = 248268; // << тут мы будем хранить контрольную сумму   procedure CheckedCodeBegin; begin end;   function TForm1.CalcCheckSum(Addr: Pointer; Size: Integer): DWORD; var   pCursor: PByte;   I: Integer;   Dumee: DWORD; begin   Result := 0;   pCursor := Addr;   for I := 0 to Size - 1 do   begin     if pCursor^ <> 0 then       Inc(Result, pCursor^)     else       Dec(Result);     Inc(pCursor);   end; end;   procedure TForm1.CheckCodeProtect; var   CheckedCodeBeginAddr, CheckedCodeEndAddr: Pointer;   CurrentCheckSum: DWORD; begin   // получаем адрес начала защищенного кода   CheckedCodeBeginAddr := @CheckedCodeBegin;     // получаем адрес конца защищенного кода   CheckedCodeEndAddr := @CheckedCodeEnd;     // Считем контрольную сумму и сверяемся с оригиналом     CurrentCheckSum := CalcCheckSum(CheckedCodeBeginAddr,     Integer(CheckedCodeEndAddr) - Integer(CheckedCodeBeginAddr));     if CurrentCheckSum <> CheckedCodeValidCheckSum then   begin     MessageBox(Handle, 'Нарушение целостности исполняемого кода.',       PChar(Application.Title), MB_ICONERROR);     TerminateProcess(GetCurrentProcess, 0);   end; end;   function TForm1.GenerateSerial(const AppUserName: string): string; const   MagicSerialMask: int64 = $C5315E6121543992; var   I: Integer;   SN: int64;   RawSN: string; begin   SN := 0;   Result := '';   for I := 1 to Length(AppUserName) do   begin     Inc(SN, Word(AppUserName[I]));     SN := SN * 123456;   end;   Sn := SN xor MagicSerialMask;   RawSN := IntToHex(SN, 16);   for I := 1 to 16 do     if ((I - 1) mod 4 = 0) and (I > 1) then       Result := Result + '-' + RawSN[I]     else       Result := Result + RawSN[I]; end;   procedure TForm1.btnCheckSerialClick(Sender: TObject); begin   // Проверяем целостность кода   CheckCodeProtect;   if edSerial.Text <> GenerateSerial(edAppUserName.Text) then     ShowMessage('Неверный код активации')   else   begin     Image1.Visible := True;     Label1.Visible := False;     Label2.Visible := False;     Label3.Visible := False;     edAppUserName.Visible := False;     edSerial.Visible := False;     btnCancel.Visible := False;     btnCheckSerial.Visible := False;   end; end;   procedure CheckedCodeEnd; begin end; 

Что мы здесь имеем:
Две метки в виде пустых процедур CheckedCodeBegin и CheckedCodeEnd, расчет «контрольной суммы» данных между этими двумя метками, производимая процедурой CheckCodeProtect, ну и сама контрольная сумма, вынесенная за область проверяемого кода и представленная константой CheckedCodeValidCheckSum (на ее значение пока не обращайте внимание).

В принципе вообще ничего сложного, но давайте-ка проанализируем, а что нам это вообще дает?

В действительности много, так как:

  1. Этот код детектирует патч тела приложения на диске (ибо при запуске оно будет уже с измененными байтами).
  2. Этот код детектирует патч тела приложения лоадером (по вышеописанной схеме).
  3. И этот код детектирует… помните картинку с прошлой статьи?

image

Да-да, это самый что ни на есть Breakpoint, установленный отладчиком. И его данной код тоже идеально детектирует, ведь если помните, то механизм установки ВР заключается в модификации тела приложения.

Вот и третья заметочка в ваш блокнот — детект ВР производится проверкой тела кода.

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

Теперь к печальному, как я и говорил, данная проверка легко детектируется. Для примера вот скриншот из под отладчика, где он прерывается сразу в начале проверки:

image

Синим выделен асмкод процедуры расчета контрольной суммы, отладчик прервался на адресе 0х467069, как раз при первой же попытке чтения защищенной области.
Ну точнее тут я немного сжульничал, если-бы код проверки был вне рамок проверяемой области, то остановка произошла бы как раз на данной инструкции, а так я, естественно, остановился на самой первой «PUSH EBX».

Но это лирика, вопрос в другом, и что теперь делать?

Ну, во первых, все не так страшно. Здесь реализована всего лишь одна единственная проверка целостности кода приложения. Да, она легко детектируется. Да, она так-же легко снимается патчем, но что нам мешает сделать их несколько, перекрестно контролирующих друг друга? Снимут и их? Ну не вопрос, добавим еще, а что нам стоит?

Однажды мне прислали продукт на анализ защиты приложения непосредственно разработчики самой защиты (извините — без названий). Бегло просмотрев код инициализации ВМ я примерно сразу наметил путь её разбора, мне нужно было всего лишь вытащить алгоритм крипта маленьких блоков данных при вызове конкретной API функции. Проблема заключалась в том, что как только я пропатчил единственный байт приложения, сработал механизм проверки контрольной суммы. Естественно я его быстро занопил, но как оказалось занопленный код контролировали уже четыре различных алгоритма. Я начал патчить их и что вы думаете? На каждый патч понимались все новые и новые куски кода, контролирующие целостность кода лавинообразно. В итоге я просто утонул в объеме ручных патчей и пришлось писать автоматическую утилиту/отладчик, что заняло почти неделю работы с учетом всех нюансов. А в конце я уперся в следующий уровень ядра защиты.
Впрочем это уже не важно, важен смысл — при желании возможно реализовать достойную головную боль взломщику, даже на банальной проверке контрольных сумм.

Ну а теперь к реальности.
Для детектирования кода проверки целостности взломщик применил MBP.
А теперь вспоминаем как они работают — правильно через назначение странице атрибута PAGE_GUARD. Значит, зная принципы работы отладчика, мы можем этому воспрепятствовать, достаточно просто снять данный атрибут и отладчик перестанет реагировать на доступ к якобы контролируемой им памяти.
Правда есть нюансик, произвести мы это сможем при помощи вызова VirtualProtect, которая уязвима, ибо отладчик может ее перехватить и запретить её вызов. Но и на это у нас есть болт с обратной резьбой, например можно поступись так, как описано в данной статье: читаем.

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

Ну и с данного момента считаем, что код контроля целостности приложения написан так, что его не взломать (дабы упростить)…

Детектирование отладчика

Ну вот, теперь мы пришли к тому, что нашу форму с картинкой хотят, причем при помощи отладчика. Конечно же нужно научится его детектировать. Пока что остановимся на функции IsDebuggerPresent, для начала этого достаточно.

Пишем код:

function IsDebuggerPresent: BOOL; stdcall; external kernel32;   procedure TForm1.FormCreate(Sender: TObject); begin   if IsDebuggerPresent then   begin     MessageBox(Handle, 'Работа приложения под отладчиком запрещена.',       PChar(Application.Title), MB_ICONERROR);     TerminateProcess(GetCurrentProcess, 0);   end;   end; 

Все очень просто, если мы под отладчиком, данная функция вернет True.
Будем считать, что код проверки целостности приложения у нас настолько сложен, что пропатчить его нельзя и вызов данной функции у нас помещен в защищенный участок.
Что применит в данном случае взломщик?

Вариантов собственно всего три, с учетом того, что патчить тело приложения нельзя:

  1. поставить ВР на вызове данной функции, где подменить результат ее вызова.
  2. пропатчить код данной функции, чтобы она всегда возвращала False
  3. произвести изменение переменной Peb.BeingDebugged в адресном пространстве отлаживаемого процесса.

С третьим вариантом бороться сложно (можно, но не нужно), а вот первые два мы рассмотрим поподробнее, точнее будем рассматривать второй вариант, т.к. в первом так-же производится патч кода приложения, при установке ВР с опкодом 0хСС.

Для начала добавим вот такой код в отлаживаемом приложении в процедуру FormCreate:

procedure TForm1.FormCreate(Sender: TObject); var   P: PCardinal; begin   P := GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent');   ShowMessage(IntToHex(P^, 8)); 

Он покажет первые 4 байта функции IsDebuggerPresent.

Вот такой код писать нельзя:

function IsDebuggerPresent: BOOL; stdcall; external kernel32;   procedure TForm1.FormCreate(Sender: TObject); var   P: PCardinal; begin   P := @IsDebuggerPresent;   ShowMessage(IntToHex(P^, 8)); 

Ибо во втором варианте мы используем статическую функцию, и адрес будет указывать не на начало тела функции, а на таблицу импорта, где стоит переходник в виде JMP.

Выполним код и запомним значение.

Под каждой системой оно будет разное, в ХР например это будет тело оригинальной функции, в семерке будет переходник на аналог из kernelbase. У меня получилось значение 9090F3EB, что соответствует следующей картинке:

image

А теперь возьмем наш отладчик из второй части статьи, и в методе OnBreakPoint произведем патч тела данной функции вот таким кодом:

procedure TTestDebugger.HideDebugger; const   PachBuff: array [0..2] of Byte =     (       $31, $C0, // xor eax, eax       $C3       // ret     ); var   Addr: Pointer; begin   Addr := GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent');   FCore.WriteData(Addr, @PachBuff[0], 3); end;   procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;   ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;   var ReleaseBreakpoint: Boolean); var   JmpOpcode: Byte; begin   if ExceptionRecord.ExceptionAddress =     Pointer(FCore.DebugProcessData.EntryPoint) then   begin     HideDebugger; 

Здесь нюансик, адрес библиотеки kernel32.dll одинаков для всех приложений, поэтому адрес функции IsDebuggerPresent будет одинаков и в отладчике и в отлаживаемом приложении.

Смысл патча заключается в обниливании регистра EAX, через который возвращается результат функции и возврата к вызывающему данную функцию коду.

Запускаем отладчик, он запустит наше приложение и в результате вмешательства в память процесса, код в функции FormCreate отладчика не обнаружит. Правда теперь код, который считывает первые 4 байта данной функции вернет нам не число 9090F3EB, а число 90C3C031, которое соответствует опкодам патча.

Как мы можем определить что тело данной функции пропатчено? В принципе мы может считать первые 4 байта данной функции из файла kernel32.dll расположенного на диске, однако в этом случае, при открытии тела библиотеки нам могут подменить путь на такой-же патченый файл и проверка скажет что все нормально.

Но есть еще один способ, достаточно редко применяемый на практике (мне встречался, если не ошибаюсь, всего 1 раз) и заключается он в следующем.

Раз мы не можем считать правильное значение с диска, мы можем его получить, считав нужные нам 4 байта из памяти какого нибудь другого процесса. Есть конечно небольшой шанс, что данный процесс так-же находится под отладчиком и в нем таким-же образом перехвачена требуемая нам функция, но очень маленький.

В итоге пишем такой код:

function IsDebuggerPresent: BOOL; stdcall; external kernel32;   procedure TForm1.CheckIsDebugerPresent; var   Snapshot: THandle;   ProcessEntry: TProcessEntry32;   ProcessHandle: THandle;   pIsDebuggerPresent: PDWORD;   OriginalBytes: DWORD;   lpNumberOfBytesRead: DWORD; begin   pIsDebuggerPresent :=     GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent');   Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);   if Snapshot <> INVALID_HANDLE_VALUE then   try     ProcessEntry.dwSize := SizeOf(TProcessEntry32);     if Process32First(Snapshot, ProcessEntry) then     begin       repeat         if ProcessEntry.th32ProcessID = GetCurrentProcessId then Continue;         ProcessHandle := OpenProcess(PROCESS_ALL_ACCESS, False,           ProcessEntry.th32ProcessID);         if ProcessHandle <> 0 then         try           if ReadProcessMemory(ProcessHandle, pIsDebuggerPresent,             @OriginalBytes, 4, lpNumberOfBytesRead) then           begin             if OriginalBytes <> pIsDebuggerPresent^ then             begin               MessageBox(Handle, 'Функция IsDebuggerPresent перехвачена.',                 PChar(Application.Title), MB_ICONERROR);               TerminateProcess(GetCurrentProcess, 0);             end;             if IsDebuggerPresent then             begin               MessageBox(Handle, 'Работа приложения под отладчиком запрещена.',                 PChar(Application.Title), MB_ICONERROR);               TerminateProcess(GetCurrentProcess, 0);             end;           end;         finally           CloseHandle(ProcessHandle);         end;       until not Process32Next(Snapshot, ProcessEntry)     end;   finally     CloseHandle(Snapshot);   end; end;   procedure TForm1.FormCreate(Sender: TObject); begin   CheckIsDebugerPresent;   CheckCodeProtect; end; 

Здесь я не стал мудрить и воспользовался стандартными возможностями TlHelp32 для получения списка процессов, для примера достаточно.

Соответственно очередная заметка в ваш блокнотик — по возможности всегда проверяйте целостность критических API функций, любым известным вам способом, не обязательно делать так, как я показал.

Да, ну и здесь тоже есть очередной нюансик, под семеркой вызов IsDebuggerPresent из kernel32.dll приведет к вызову этой же функции из kernelbase.dll, где ее так же могут пропатчить, но тут уж думайте сами.

В итоге после всех модификаций данная функция у нас плотно опекается системой контроля целостностии и пропатчить ее в лоб уже достаточно проблематично. Как обойти данный кусок защиты, я покажу позже, а сейчас рассмотрим кое что другое.

Детектирование подключения отладчика к процессу.

Вот смотрите, до этого мы запускали приложение под отладчиком, а что нам стоит запустить его без отладчика, дождаться когда все проверки на наличие отладки пройдут и только после этого подключиться отладчиком к приложению?

Да, в таком варианте весь наш код не сработает, точнее сработает, но частично.

Как вариант, для детектирования такого безобразия, можно например поставить таймер и периодически вызывать процедуру CheckIsDebugerPresent, но де-факто, для детектирования подключения нам это не потребуется. Дело в том что при вызове в отладчике функции DebugActiveProcess, в отлаживаемом приложении всегда вызывается функция DbgUiRemoteBreakin. Зная это мы можем провернуть следующий трюк.

Мы пропатчим сами себя, точнее тело функции DbgUiRemoteBreakin, добавив в ее начало переход на адрес функции TerminateProcess, таким образом, как только произойдет подключение отладчика к процессу, процесс сразу же завершится.

Пишем очередной блок кода:

type   TDbgUiRemoteBreakinPath = packed record     push0: Word;     push: Byte;     CurrProc: DWORD;     moveax: byte;     TerminateProcAddr: DWORD;     calleax: Word;   end;   procedure TForm1.BlockDebugActiveProcess; var   pDbgUiRemoteBreakin: Pointer;   Path: TDbgUiRemoteBreakinPath;   OldProtect: DWORD; begin   pDbgUiRemoteBreakin :=     GetProcAddress(GetModuleHandle('ntdll.dll'), 'DbgUiRemoteBreakin');   if pDbgUiRemoteBreakin = nil then Exit;   Path.push0 := $006A;   Path.push := $68;   Path.CurrProc := $FFFFFFFF;   Path.moveax := $B8;   Path.TerminateProcAddr :=     DWORD(GetProcAddress(GetModuleHandle(kernel32), 'TerminateProcess'));   Path.calleax := $D0FF;   if VirtualProtect(pDbgUiRemoteBreakin, SizeOf(TDbgUiRemoteBreakinPath),     PAGE_READWRITE, OldProtect) then   try     Move(Path, pDbgUiRemoteBreakin^, SizeOf(TDbgUiRemoteBreakinPath));   finally     VirtualProtect(pDbgUiRemoteBreakin, SizeOf(TDbgUiRemoteBreakinPath),       OldProtect, OldProtect);   end; end;   procedure TForm1.FormCreate(Sender: TObject); begin   BlockDebugActiveProcess;   CheckIsDebugerPresent;   CheckCodeProtect; end; 

В результате такого патча в начале функции DbgUiRemoteBreakin будет размещен следующий код:

image

То есть грубо на стеке размещаются два параметра необходимые функции TerminateProcess (идут в обратном порядке), это параметр uExitCode равный нулю и параметр hProcess, вместо которого подставляется псевдохэндл DWORD(-1) означающий текущий процесс. После чего регистр EAX инициализируется адресом функции TerminateProcess и происходит ее вызов.

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

Для большинства отладчиков этого достаточно.
К сожалению, это не бронебойный вариант, ибо ничто не помешает повторно пропатчить тело нашего приложения, вернув оригинальный код обратно, перед вызовом DebugActiveProcess. (Правда я такого не встречал, но все же…)

Обход Memory Breakpoint

Как я уже и говорил, определить наличие МВР можно через проверку атрибута защиты страницы PAGE_GUARD. Делается это при помощи вызова функции VirtualQuery, а можно просто в лоб переназначить атрибуты вызовом VirtualProtect.

Но есть еще один хитрый способ и называется он ReadProcessMemory. Это та же функция, при помощи которой в отладчике мы читали данные из отлаживаемого процесса. Нюанс ее в следующем, если она попробует считать данные с страницы защищенной флагом PAGE_GUARD блок данных соответствующей странице будет заполнен нулями, причем цимус в том, что при этом не произойдет поднятие события EXCEPTION_GUARD_PAGE в отладчике. Такая вот «тихая проверка региона». Если мы будем ее использовать при проверки целостности кода приложения, в том случае если на него будет установлен МВР данные считаются не верно и в итоге контрольная сумма не сойдется с ожидаемой. Более того, если по адресу, откуда будет читать данная функция выставлен Hardware Breakpoint контролирующий запись, чтение/запись отладчик так же не получит уведомления о его срабатывании.

Поэтому перепишем функцию CalcCheckSum следующим образом:

function TForm1.CalcCheckSum(Addr: Pointer; Size: Integer): DWORD; var   pRealData, pCursor: PByte;   I: Integer;   Dumee: DWORD; begin   pRealData := GetMemory(Size);   try     ReadProcessMemory(GetCurrentProcess, Addr, pRealData, Size, Dumee);     Result := 0;     pCursor := pRealData;     for I := 0 to Size - 1 do     begin       if pCursor^ <> 0 then         Inc(Result, pCursor^)       else         Dec(Result);       Inc(pCursor);     end;   finally     FreeMemory(pRealData);   end; end; 

Таким образом одной единственной функцией мы защищаемся и от ВР, и от МВР, и даже от НВР.

Как это все обойти?

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

На некоторых нюансах я еще остановлюсь в конце статьи, а сейчас попробуем написать лоадер на базе отладчика, учитывая тот момент, что мы условились считать код проверки целостности непробиваемым и значит патчить тело проверки мы не будем.

Пишем каркас. Запуск и остановка у нас будет выглядеть так:

constructor TTestDebugger.Create(const Path: string); begin   FCore := TFWDebugerCore.Create;   if not FCore.DebugNewProcess(Path, True) then     RaiseLastOSError;   FCore.OnCreateProcess := OnCreateProcess;   FCore.OnLoadDll := OnLoadDll;   FCore.OnDebugString := OnDebugString;   FCore.OnBreakPoint := OnBreakPoint;   FCore.OnHardwareBreakpoint := OnHardwareBreakpoint;   FCore.OnUnknownBreakPoint := OnUnknownBreakPoint;   FCore.OnUnknownException := OnUnknownException; end;   destructor TTestDebugger.Destroy; begin   FCore.Free;   inherited; end; 

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

Первая наша задача каким то образом необходимо отключить детектирование отладчика приложением. Так как приложение проверяет целостность IsDebuggerPresent, а патчить проверку нельзя (по условию задачи) у нас остается только один вариант — изменить значение параметра Peb.BeingDebugged.

Сделаем это следующим кодом:

procedure TTestDebugger.HideDebugger(hProcess: THandle); var   pProcBasicInfo: PROCESS_BASIC_INFORMATION;   pPeb: PEB;   ReturnLength: DWORD; begin   if NtQueryInformationProcess(hProcess, 0,     @pProcBasicInfo, SizeOf(PROCESS_BASIC_INFORMATION),     @ReturnLength) <> STATUS_SUCCESS then     RaiseLastOSError;   if not ReadProcessMemory(hProcess, pProcBasicInfo.PebBaseAddress,     @pPeb, SizeOf(PEB), ReturnLength) then     RaiseLastOSError;   pPeb.BeingDebugged := False;   if not WriteProcessMemory(hProcess, pProcBasicInfo.PebBaseAddress,     @pPeb, SizeOf(PEB), ReturnLength) then     RaiseLastOSError; end; 

Здесь все просто, получаем адрес блока окружения процесса, изменяем параметр BeingDebugged и пишем все обратно. Таким образом функция IsDebuggerPresent перестает реагировать на отладчик. Декларацию используемых структур можно посмотреть в исходнике демопримера.

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

Поступим так:

Вы наверное не раз в отладчике меняли значения переменных (это описывалось в первой части статьи). Вот здесь мы сделаем что-то похожее. Как помните отвечает за отображение картинки инструкция JE, если огрубить то представьте что у нас есть булевая переменная и условие if value then..else, если мы прервемся на таком условии то мы сможем контролировать условия выполнение кода, т.е. указать изменением переменной value что именно должно выполнится: блок then или else.

Оператор JE принимает решение о переходе как раз на основе вот такой вот булевой переменной, правда она представлена в виде флага ZF. Если флаг включен происходит прыжок по новому адресу. Стало быть наша задача заставить приложение прерваться на инструкции JE чтобы мы смогли изменить значение данного флага на необходимое нам.

Сделаем это при помощи установки НВР на адрес инструкции JE, т.к. это единственное, что не умеет контролировать наше защищенное приложение. Как узнать данный адрес я пропущу. В примере в составе архива идет исполняемый файл crackme.exe, я его специально вложил в архив из-за того что при каждой перекомпиляции, да и в зависимости от версии дельфи и прочего, этот адрес будет разным. В скомпилированном экзешнике этот адрес уже вычислен и равен значению 0х467840.

Осталось написать код:

procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;   ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;   var ReleaseBreakpoint: Boolean); begin   if ExceptionRecord.ExceptionAddress =     Pointer(FCore.DebugProcessData.EntryPoint) then   begin     Writeln;     Writeln(Format('!!! --> Process Entry Point found. Address: %p',       [Pointer(FCore.DebugProcessData.EntryPoint)]));     Writeln;       HideDebugger(FCore.DebugProcessData.AttachedProcessHandle);       FCore.SetHardwareBreakpoint(ThreadIndex, Pointer($467840), hsByte,       hmExecute, 0, 'wait JE');   end   else   begin     Writeln;     Writeln(Format('!!! --> BreakPoint at addr 0X%p - "%s"',       [ExceptionRecord.ExceptionAddress,       FCore.BreakpointItem(BreakPointIndex).Description]));     Writeln;   end; end; 

После чего нужно обработать прерывание на НВР и выставить правильное значение флага:

procedure TTestDebugger.OnHardwareBreakPoint(Sender: TObject;   ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord;   BreakPointIndex: THWBPIndex; var ReleaseBreakpoint: Boolean); var   ThreadData: TThreadData; begin   Writeln;   ThreadData := FCore.GetThreadData(ThreadIndex);   Writeln(Format('!!! --> Hardware BreakPoint at addr 0X%p - "%s"',     [ExceptionRecord.ExceptionAddress,     ThreadData.Breakpoint.Description[BreakPointIndex]]));   FCore.SetFlag(ThreadIndex, EFLAGS_ZF, True);   Writeln; end; 

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

Результат будет примерно таким:

image

Так оно обычно и бывает, думаешь что написал бронебойную защиту, а потом раз и она обходится на коленке, ну не всегда конечно, но бывает 🙂

Детектируем Hardware BreakPoint:

Я намерено не остановился на детектировании НВР в рамках защищаемого приложения, по той причине, что будь там такая проверка, то пришлось бы писать достаточно сложный код обхода. А так вообще конечно желательно проверять и их наличие, закрывая таки образом отладчику возможность нормальной работы.

Детект наличия НВР достаточно прост, реализовать можно как через тот же GetThreadContext и проверкой регистра DR7 (если он не пуст — значит стоит НВР), либо, чтобы нас не перехватили на вызове API функции, мы может получить контекст нити при помощи генерации исключения.

Вот первый вариант

procedure TForm1.CheckHardwareBreakPoint; var   Context: TContext; begin   Context.ContextFlags := CONTEXT_DEBUG_REGISTERS;   GetThreadContext(GetCurrentThread, Context);   if Context.Dr7 <> 0 then   begin     MessageBox(Handle, 'Обнаружен HardwareBreaakPoint.',       PChar(Application.Title), MB_ICONERROR);     TerminateProcess(GetCurrentProcess, 0);   end; end; 

И второй вариант, в котором поднимается отладочное исключение и снимается информация о контексте нити в обработчике _except_handler.

type   // структура для восстановления   TSeh = packed record     Esp, Ebp, SafeEip: DWORD;   end;   var   seh: TSeh;   function _except_handler(ExceptionRecord: PExceptionRecord;   EstablisherFrame: Pointer; Context: PContext;   DispatcherContext: Pointer): DWORD; cdecl; const   ExceptionContinueExecution = 0; begin   if Context^.Dr7 <> 0 then   begin     MessageBox(0, 'Обнаружен HardwareBreaakPoint.',       PChar(Application.Title), MB_ICONERROR);     TerminateProcess(GetCurrentProcess, 0);   end;   // возвращаем регистры на место   Context^.Eip := seh.SafeEip;   Context^.Esp := seh.Esp;   Context^.Ebp := seh.Ebp;   // и говорим продолжить выполнение   Result := ExceptionContinueExecution; end;   procedure TForm1.CheckHardwareBreakPoint2; asm   // устанавливаем SEH фрейм   push offset _except_handler   xor   eax, eax   push  fs:[eax]   mov   fs:[eax], esp   // заполняем данные для восстановления   lea   eax, seh   mov   [eax], esp   add   eax, 4   mov   [eax], ebp   add   eax, 4   lea   ecx, @done   mov   [eax], ecx   // генерируем исключение   mov eax, [0]   @done:   // удаляем SEH фрейм   xor   eax, eax   pop   fs:[eax]   add   esp, 4 end; 

Кстати интересный момент. Обратите внимание на то, сколько информации приходит в обработчик исключения. Вся эта информация нам не доступна в обработчике except, именно поэтому я так часто называю try..finally..except куцей оберткой над SEH 🙂

Резюмируя

Теперь вы знаете несколько способов борьбы с отладчиком, правда теперь вы знаете и методы противодействия им, но на то она и статья, чтобы вы делали выводы.

Исходный код с примерами забрать можно по данной ссылке: http://rouse.drkb.ru/blog/dbg_part3.zip

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

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

© Александр (Rouse_) Багель
Москва, ноябрь 2012

ссылка на оригинал статьи http://habrahabr.ru/post/178183/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *