вторник, 11 сентября 2012 г.

Уход из под отладчика срывом стэка


Версия 1.0.1 

Работа программиста реализующего код защиты как правило является достаточно увлекательным занятием. Но правда сам интерес начинает проявляться только после того, как ваш продукт был в первый (или очередной) раз взломан :)
В этот момент у вас на самом деле появляется "работа", которая заключается в анализе результатов работы реверсера и построению алгоритма противодействия.
Это как война на баррикадах, сначала мы их, потом они нас и на новый круг :)

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

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

В момент анализа стороннего ПО мы работаем как правило со отладчиком. Одним из главных элементов отладки является наличие валидного стэка отлаживаемой нити. На основании стэка нити отладчик (а если ему не под силу, то дополнительные скрипты) достаточно подробно строят стэк вызовов процедур, на базе которого происходит часть анализа кода. На стэке лежат параметры вызова функций. На том-же стэке хранятся SEH фреймы. Ни один вызов функции не обходится без стэка. Собственно стэк наше все :) (Глава 6. Базовые сведения о потоках)

В случае если вы каким либо образом обнаружили отладчик, то не стоит сразу делать TerminateProcess(), его отловят и вернут как было. Если сломать "механизм стэка", то отладка вашего приложения в большинстве случаев станет не возможна. А отловить код поломки уже на порядок сложнее, чем перехватить пресловутый Halt или TerminateProcess.
Смысл стрыва стека сводится к генерации необрабатываемого исключения PAGE_FAULT, после чего процесс можно только закрыть. Дальнейшая его отладка бесперспективна.

Здесь я приведу четыре классических варианта срыва стэка:

1. Что есть стэк с точки зрения программиста в конкретной точке кода?
Это два регистра, EBP -  Base Pointer и ESP - Stack Pointer.
Изменив эти два значения на любые произвольные, мы разрушим стэк::
  asm
    mov ebp, 0
    mov esp, 0
  end;
2. Стек всегда растет вниз, в то время как сверху остаются некие данные в том числе и адреса возврата и SEH фреймы. Все эти данные доступны для чтения и модификации. Удалив их мы разрушим стэк.

procedure _FillChar(var Dest; count: Integer; Value: Byte);
asm
{     ->EAX     Pointer to destination  }
{       EDX     count   }
{       CL      value   }
 
        PUSH    EDI
 
        MOV     EDI,EAX { Point EDI to destination              }
 
        MOV     CH,CL   { Fill EAX with value repeated 4 times  }
        MOV     EAX,ECX
        SHL     EAX,16
        MOV     AX,CX
 
        MOV     ECX,EDX
        SAR     ECX,2
        JS      @@exit
 
        REP     STOSD   { Fill count DIV 4 dwords       }
 
        MOV     ECX,EDX
        AND     ECX,3
        REP     STOSB   { Fill count MOD 4 bytes        }
 
@@exit:
        POP     EDI
end;
 
procedure TForm1.btnKillUpStackClick(Sender: TObject);
var
  P: Pointer;
begin
  _FillChar(P, MaxInt, 0);
end; 

Здесь используется реализация  FillChar из Delphi 7. В более старших версиях данная продцедура выполнена несколько другим способом и не дает необходимого эффекта.

3. Границы стека всегда обрамлены страницами с флагом PAGE_GUARD. Это можно наглядно увидеть в данном примере. Механизм работы данного флага следующий, при обращении к участку памяти с данным флагом срабатывает исключение Access Violation и данный флаг снимается. После чего при повторном обращении к этому участку срабатывает PAGE_FAULT.
В данном коде используется локальный статический массив для ускорения переполнения стека.

procedure TForm1.btnKillStackOnGuardClick(Sender: TObject);
 
  procedure T;
  var
    HugeBuff: array [0..10000] of DWORD;
  begin
    if HugeBuff[0] <> HugeBuff[10000] then
      Inc(HugeBuff[0]);
    T;
  end;
 
begin
  try
    T;
  except
    T;
  end;
end;

4. Срыв стека на рекурсивном вызове SEH фрейма. Логика данных фреймов проста, после установки они обрабатывают все исключения до тех пор, пока не будут сняты. В Delphi они представлены в частично обрезанном виде в качестве оберток try..finally/except. Идея заключается в том, что после установки SEH фрейма мы не производим его удаления и в нем-же генерируем ошибку, заставляя рекурсивно вызывать самого себя. В результате мы имеем переполнение стека, плавно перерастающее в PAGE_FAULT.

procedure TForm1.btnKillStackOnSEHClick(Sender: TObject);
begin
  asm
    lea   eax, @KillStack
    push eax
    push dword ptr [fs:0]
    mov   [fs:0], esp
    xor eax, eax
    mov eax, [eax]
  @KillStack:
    mov eax, 0
    call eax
  end;
end;

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

Подсветка кода выполнена при помощи: http://highlight.hohli.com/

4 комментария:

  1. >Смысл стрыва стека сводится к генерации необрабатываемого исключения PAGE_FAULT, после чего процесс можно только закрыть. Дальнейшая его отладка бесперспективна.

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

    ОтветитьУдалить
    Ответы
    1. Любое разрушение стека приведет к ошибке, но иногда это можно побороть...

      Удалить
  2. Большое спасибо Администратору за предоставленную статью. Теперь у меня 333 проверки на каждой кнопке, 33333 рекурсии и куча дополнительного защитного кода. Крякерам и реверсерам придется постараться =)

    ОтветитьУдалить
    Ответы
    1. Непонятен смысл комментария. Разве в статье где-то говорится про 333 проверки? По всей видимости нет, в статье описывается способ ухода от отладчика - не более того, а уж как это применять на практике должен решать сам программист.
      Различных защитных трюков в данном блоге описано более чем предостаточно, и этот вариант является только одним из многих.

      Удалить