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

в 10:13, , рубрики: Delphi, отладка, метки: ,

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

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

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

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

Простейшая 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

Автор: Rouse

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js