Что такое отладчик, как им пользоваться и как он реализован, после прочтение первых двух частей, вы знаете. В заключительной части статьи попробуем рассмотреть некоторые методы борьбы с отладчиком, на основе знаний о принципах его работы. Я не буду давать шаблонный набор антиотладочных приемов, благо при желании все это можно найти на просторах интернета, попробую это сделать немного другим способом, на основе некоего абстрактного приложения, у которого буду расширять код защиты от самой простейшей схемы до... пока не надоест :)
Сразу-же оговорюсь, в противостоянии приложение/отладчик, всегда победит последний :)
Но, только в том случае, если им будет пользоваться грамотный специалист, а с такими спецами бороться практически бесполезно (ну, если вы конечно не обладаете как минимум такой же квалификацией).
Правда как показывает практика, грамотные спецы не занимаются неинтересными им задачами, оставляя их на откуп начинающим реверсерам, которые еще не прогрызли свой гранит науки и могут спотыкаться на некоторых неочевидных трюках.
Вот что-то такое мы и рассмотрим, только в очень упрощенной форме.
Простейшая ShareWare:
Представим, у нас есть некое ПО, которое мы решили продавать. Для простоты пусть это будет обычное VCL приложение из пустой формы (ну хорошо, пусть не пустое, а с картинкой на всю морду) и мы хотим его продать. Первый же вопрос, которым нужно озаботится - как сделать так, чтобы наша картинка был видна только тем, кто за нее заплатил? Точнее - как разграничить триальных и легальных пользователей?
Самым очевидным решением является ключ. Триальный пользователь его не знает, а легальный, заплативший за него реальными деньгами, может активировать легальную копию приложения и насладиться картинкой.
Ключ так ключ.
Создаем новое VCL приложение, кидаем на форму TImage с картинкой, Visible ему выставляем в False. После чего размещаем на форме два TEdit, первый для имени пользователя и второй для кода активации. Ну и две кнопки - закрыть приложение и активировать.
Ну вот как-то так:
После чего пишем совершенно секретный код активации:
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;
Суть кода в следующем:
на основании имени пользователя приложение генерирует некий серийник и сравнивает его с введенным пользователем. Если все нормально, то убираются все элементы управления, отвечающие за активацию и отображается картинка, которую и жаждал узреть пользователь.
Оть такая:
(ну... первое что нашел :)
После произведенных манипуляций, "это" публикуется на различных шароварных сайтах и даже иногда даются ссылки на программерские форумы в топиках вида: "потестируйте плз защиту".
А как это выглядит со стороны взломщика?
Он берет отладчик (для простоты возьмем тот-же Olly Debug) и видит вот такую картинку:
Кода приложения у него нет, но есть характерная ошибка начинающих "защитников" ПО - вывод диалога о неверном ключе.
Что это дает взломщику?
Он ставит ВР на вызов MessageBoxA и запустив приложение ловит вызов данного сообщения, после чего, нажав на кнопку "ОК" он может вернуться к коду, в котором происходит вызов данной ошибки, где, посмотрев немного выше, сможет определить наличие условного перехода, на основании которого и происходит данный вызов:
На картинке точка принятия решения программой выделена восклицательными знаками.
Все что ему остается сделать, это исправить инструкцию JE на JMP, отключив, таким образом, проверку серийного кода и обеспечив валидный переход на область кода, который должен выполнятся только при активации приложения.
Как-то не понятно, да?
Ну тогда вот вам такая картинка, из отладчика Delphi:
Здесь код более понятен для отладки, и его чтение более удобно из-за размапливания адресов и приведения их в читабельный вид. Например теперь явно видно что перед выходом на адрес 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).
Работает просто - если есть подозрение на то, что текущий участок кода контролируется механизмом защиты, на него навешивается МВР или НВР с целью определить, где именно расположена сама проверка целостности кода.
Если таковая проверка детектируется - она так же отключается патчем.
Ну вот мы собственно и приплыли к патовой ситуации: абонент - не абонент :)
Впрочем...
Выкрутиться, конечно можно, но...
Но для начала посмотрим, как вообще реализовать проверку целостности кода приложения.
Если конкретнее, то мы хотим защитить от изменений код который был в самом начале. Для этого нам нужно каким-то образом выяснить его расположение в памяти во время работы приложения.
Метки наше "всё".
На основе меток работает большинство навесных протекторов, стало быть зачем нам придумывать очередной велосипед. Что такое метка - в принципе это столь нелюбимый всеми 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. И этот код детектирует... помните картинку с прошлой статьи?
Да-да, это самый что ни на есть Breakpoint, установленный отладчиком. И его данной код тоже идеально детектирует, ведь если помните, то механизм установки ВР заключается в модификации тела приложения.
Вот и третья заметочка в ваш блокнот - детект ВР производится проверкой тела кода.
Правда, к сожалению тут не все так просто, в некоторых случаях данный код не сработает, но не будем торопиться, дойдем и до этого.
Теперь к печальному, как я и говорил, данная проверка легко детектируется. Для примера вот скриншот из под отладчика, где он прерывается сразу в начале проверки:
Синим выделен асмкод процедуры расчета контрольной суммы, отладчик прервался на адресе 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, что соответствует следующей картинке:
А теперь возьмем наш отладчик из второй части статьи, и в методе 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 будет размещен следующий код:
То есть грубо на стеке размещаются два параметра необходимые функции 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;
Ну вот и все, можно запускать на выполнение, вводить любое левое значение и наслаждаться картинкой.
Результат будет примерно таким:
Так оно обычно и бывает, думаешь что написал бронебойную защиту, а потом раз и она обходится на коленке, ну не всегда конечно, но бывает :)
Детектируем 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
Спасибо большое за серию публикаций, с большим интересом прочитал.
ОтветитьУдалитьПравда лично я никогда не занимался защитой приложений (понимая, на сколько я далёк от темы и как легко заменяются простые cmp/jmp), и в будущем маловероятно буду заниматься, т.к. мы пишем приложения, ориентированные на работу с БД, где и реализуется вся бизнес логика (приложения нет смысла защищать, они доступны для свободного скачивания, всё равно без БД работать не будут).
А вот пока читал про методы детектирования работы из-под отладчика и методы обхода этой темы, подумалось о том, что кроме Microsoft Windows, существуют такие свободно распространяемые вещи как Wine и ReactOS (и вроде бы даже доступны их исходники).
Как Вы думаете, может ли взломщик воспользоваться этими ... средами, как-то модифицировать их под себя, и уже спокойно запускать взламываемое приложение из-под отладчика?
Вопрос достаточно интересный.
УдалитьПопробую начать со стороны.
В свое время я очень плотно поработал с ребятами "ядершщикаи" из Wine.
Проблема была в том, что мое ядро защиты использовало некие особенности процессора, из-за чего в их виртуалке определенные регистры на выходе были невалидно проинициализированы, соответственно "что-то где-то" падало.
Слава богу что меня не свели с "манагерами" (спасибо нашим манагерам), а дали прямые контакты квалифицированного спеца.
Пообщавшись со специалистом (без кавычек - действительно, грамотный парень) я вынес для себя следующее: отладчик не изменился, даже в их виртуализированной среде :)
Но...
Ну допустим что взломщик целиком контролирует свою систему, даже, допустим, он запускает приложение на исполнение с полным логированием каждого чиха. Тут есть момент - каждый чих, это трассировка, а что такое трассировка, я уже объяснял.
Теперь ситуация: наше приложение защищено. То что взломщик контролирует систему ему ничего не даст, ибо VM защиты все равно находится внутри приложения. Даже если он его выполнит на своей системе, это не прибавит ему знаний о том как обойти защиту, ибо это можно понять только полным разбором VM.
Ну и в принципе как-то так, сори что сумбурно. Спрашивайте - отвечу...
Спасибо за статью. Очень понравилось, что показаны способы изучения проблемы как со стороны Delphi, так и со стороны крекерского инструментария. Я сам в этих делах не достиг каких-то результатов, но в свое время перечитал некоторое количество статей и книг (тем, кому интересна тема рекомендую книги под псевдонимом Крис Касперский), но там как правило рассматривается все со стороны низкого уровня, и очень трудно понять как из ассемблерного кода построить высокоуровневую модель приложения. Собственно в этом у меня был основной затык. Статьи вроде этой отчасти способны решить эту проблему, но для полного понимания предмета, как мне кажется, не хватает ссылок на какие-то другие источники, чтобы интересующиеся могли при желании глубже изучить низкоуровневые вопросы.
ОтветитьУдалитьНадеюсь, что по этой теме будут еще публикации в такой же форме.
По поводу ссылок, с сожалению тут трудно что-то подобрать в таком-же стиле, в основном это будет как раз ссылки на статьи с низкоуровневым кодом, увы...
УдалитьА продолжать в этом же направлении конечно же буду, сейчас на вскидку могу сказать что скорее всего будут опубликованы статьи о использовании TLS-Callback в рамках Delphi приложений/библиотек, о технологии DebugBlock и наномитах (а-ля Armadilo) и вероятно сделаю обзорную статью о карте памяти процесса (вполне возможно что с использованием технологии AWE).
Ну а что дальше - пока не выбрал темы, на которой можно остановится, ибо и так есть фронт для работы, сначала нужно с ним закончить :)