вторник, 1 марта 2016 г.

Раннее исполнение кода в Delphi приложениях

Как вы думаете, сколько кода выполняется до того момента, как приложение запустится, или отладчик передаст управление в ваши руки?

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

Нет, не вирусы, зачем? Вполне легальные продукты, контролирующие все и вся.

Думаете - вру и считаете что при нажатии кнопки F7 (Trace Into) вы полностью контролируете процесс отладки?
Тогда я вас разочарую, есть множество способов выполнить код, до того момента, как вы приступили к его дебагу.

О нескольких я попробую рассказать.



0. Теория


Начну с небольшой демонстрации.
Давайте создадим новое консольное приложение и запустим его.
Посмотрите на картинку:


Мы еще не успели ничего сделать, а в адресное пространство нашего процесса подгрузилось нормальное такое количество библиотек.

А почему?
Потому что они импортируются статически посредством таблицы импорта, которая есть в каждом приличном приложении.

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

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


Ух ты, а что это тут у нас?
Некая хромовская библиотека имеет помимо точки входа (Entry Point) еще аж 3 колбэка, которые будут выполнены до того как Chrome сможет с ней работать.

Вот они все трое:



А ведь у каждой библиотеки есть еще и обычная точка входа, которая выполнится после обработки колбэков.
И это все выполнится до того, как мы нажали F7 :)

1. Как работает загрузчик


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

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


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

И заметьте - пока что еще мы висим на F7 - код нашей программы не выполняется, но выполняется код из точек входа библиотек и их TLS Callback-ов

И вот когда все подгружено и время дошло до нас, только тогда загрузчик анализирует нашу TLS таблицу (передавая управление если нужно) и в финале отдает управление на точку входа, где мы и сидим в отладчике :)

2. Раннее исполнения кода в DLLMain


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

Пишем код библиотеки.

library init_lib;

uses
  Windows;

{$R *.res}

procedure Foo;
begin
end;

exports
  Foo;

begin
  MessageBox(0, 'Message from Lib', nil, 0);
end.

Процедура Foo по факту не нужна - она используется только для статической линковки с приложением (чтобы библиотека была заявлена в таблице импорта).

Ну и теперь сам код приложения:

program testlib;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Windows;

  procedure Foo; external 'init_lib.dll';

begin
  MessageBox(0, 'Message from Test APP', nil, 0);
  Foo;
end.


Вот так это выглядит на практике:




Если бы мы грузили библиотеку в динамике через LoadLibrary - код в DllMain библиотеки тоже бы выполнился, но!!!
Но уже после точки входа, а это нам не нужно, все таки рассматриваем раннее исполнение кода.

Зачем это вообще надо?

Хороший вопрос.
Как правило раннее исполнение кода применяется при реализации антиотладочных и антидамповых трюках.

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

А по поводу антидампа - тут тоже все достаточно просто, но на нем я остановлюсь чуть позже.

3. Раннее исполнение кода без библиотеки


Дочитали до  третьей главы?
Уважаю :)

Тогда займемся более серьезными вещами.

Итак, чуть выше я упоминал про TLS Callback - посмотрим что это за зверь.
Грубо говоря, при инициализации каждого потока, управление изначально передается не на TreadProc, а на цепочку его колбэков.

При запуске вашего приложения (если отбросить нюансы с инициализацией загрузчиком секций, релоков, PEB/TEB) начинает работать главный поток.
Но, перед передачей управления данному потоку, загрузчик первым образом анализирует образ вашего приложения на наличие TLS секции, ожидая там увидеть что-то похожее на вот эту структуру (для 32 бит к примеру - 64 бита не рассматриваю):

  _IMAGE_TLS_DIRECTORY32 = record
    StartAddressOfRawData: DWORD;
    EndAddressOfRawData: DWORD;
    AddressOfIndex: DWORD;         // PDWORD
    AddressOfCallBacks: DWORD;     // PIMAGE_TLS_CALLBACK *;
    SizeOfZeroFill: DWORD;
    Characteristics: DWORD;
  end;

Параметр AddressOfCallBacks указывает на VA адрес (если сильно упрощенно RVA + ImageBase образа) начала цепочки Callback-ов, которые должны быть выполнены, до передачи управления на точку входа в ThreadProc (для главного потока - это точка входа в приложение, т.н. EntryPoint).

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

Помните картинку выше с хромовской библиотекой?
Вот так это выглядит в карте памяти процесса.


Синим выделено окончание цепочки TLS колбэков.
Как бы и нам самим такое сделать?

К примеру в MSVC это выглядит вот так:
#include "windows.h"

// сам колбэк
VOID NTAPI tls_callback(HMODULE hModule,
 DWORD  ul_reason_for_call,LPVOID lpReserved)
{
 MessageBox(0, L"TLS Callback Message", L"", 0);
}

// указываем линкеру что его нужно подключить
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__xl_b")
#pragma section(".CRT$XLY",long,read)
extern "C" __declspec(allocate(".CRT$XLY"))
PIMAGE_TLS_CALLBACK _xl_b = (PIMAGE_TLS_CALLBACK)tls_callback;

int main()
{
 MessageBox(0, L"Entry Point Message", L"", 0);
 return 0;
}

Если собрать этот код в студии - то сначала отобразится окно:


И только потом мы увидим второе "Entry Point Message".
При билде данного исходного кода студия автоматом сформирует валидную TLS секцию и проинициализирует цепочку колбэков адресом процедуры tls_callback.

Это в Visual Studio, а вот в Дельфи такого сделать так сходу нельзя - не умеет она, но!!!

Но есть небольшой трюк, связанный с тем что разработчики компилера Дельфи, уж не знаю по каким причинам, немного нам помогли, создав неинициализированную TLS секцию в исполняемом файле. С какого времени это началось - я не знаю, но начиная с Delphi 2010 такая секция точно присутствует во всех исполняемых файлах.

А раз у нас на руках есть TLS секция, почему-бы нам ее не проинициализировать самостоятельно?

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

program test_app;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Windows;

// данный колбэк будет вызван, если файл будет корректно пропатчен
procedure tls_callback(hModule: HMODULE;
  ul_reason_for_call: DWORD; lpReserved: Pointer); stdcall;
begin
  if ul_reason_for_call = DLL_PROCESS_ATTACH then
    MessageBox(0, 'TLS Callback Message', nil, 0);
end;

const
  ptls_callback: Pointer = @tls_callback;

begin
  // дабы процедура tls_callback появилась в MAP файле
  // нужно чтобы на нее была ссылка, банально вот такая:
  if ptls_callback <> nil then
    MessageBox(0, 'Entry Point Message', nil, 0);
end.

Давайте его сразу соберем и запустим, на выходе у нас будет только сообщение "Entry Point Message".

Что собственно и логично, т.к. tls_callback сейчас является просто некоей функцией, о которой лоадер не знает и вызывать ее не собирается.

Но, нам нужно ее вызвать. А сделать это мы сможем только посредством патча уже собранного файла.

4. Инициализируем TLS секцию


Итак, у нас на руках есть исполняемый файл с объявленной функцией tls_callback.
Рядом с ним лежит MAP файл (вы же включили генерацию МАР файла в настройках линкера?)

Давайте посмотрим на него (покажу только интересующие нас куски мар файла):


Самая последняя строчка - это номер секции и смещение нашей процедуры tls_callback.
Вверху расположены сами секции и их адреса.

К примеру, мы видим что процедура с именем "tls_callback" расположена в первой секции (индекс секции равен 0001).
Оффсет колбэка относительно начала секции равен 00003208.
Смотрим VA адрес секции за индексом 0001 (вверху) - это секция ".text", содержащая в себе код (на что указывает подсказка о типе данных "CODE"), а ее адрес равен 00401000.
Банально суммируем оба значения (числа 16 битные, т.е. HEX).

00003208 + 00401000 = 00404208.

Проверяем.



Да, это наш код.

procedure tls_callback(DllHandle: Pointer;
  Reason: DWORD; Reserved: Pointer); stdcall;
begin
  if Reason = DLL_PROCESS_ATTACH then
    MessageBox(0, 'TLS Callback Message 1', nil, 0);
end;

Значит напишем небольшую функцию, которая будет получать из МАР файла реальный адрес колбэка по его имени (пусть он так и останется "tls_callback", хотя имя может быть любым).

Вот это нам пригодится.

type
  TSectionData = record
    Index: Integer;
    StartAddr: DWORD;
    SectionName: ShortString;
  end;
  TSectionDataList = TList<TSectionData>;

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

function GetSectionDataList(const FilePath: string; var Index: Integer): TSectionDataList;
var
  S: TStringList;
  Line: string;
  Section: TSectionData;
begin
  Result := TSectionDataList.Create;
  try
    S := TStringList.Create;
    try
      S.LoadFromFile(FilePath);
      Index := 0;
      Writeln('Ищу таблицу секций...');
      while Copy(Trim(S[Index]), 1, 5) <> 'Start' do
        Inc(Index);
      Inc(Index);
      while Trim(S[Index]) <> '' do
      begin
        Line := Trim(S[Index]);
        Section.Index := StrToInt(Copy(Line, 1, 4));
        Delete(Line, 1, 5);
        Section.StartAddr := StrToInt('$' + Copy(Line, 1, 8));
        Delete(Line, 1, 19);
        Section.SectionName := ShortString(Trim(Copy(Line, 1, 8)));
        Result.Add(Section);
        Inc(Index);
      end;
      Writeln('Всего секций найдно: ', Result.Count);
    finally
      S.Free;
    end;
  except
    // все исключения глушим. есть коды ошибок
    on E: Exception do
      Writeln('GetSectionDataList: ' + E.ClassName + ': ' + E.Message);
  end;
end;

А вот так найдем адрес нашей функции, ориентируясь на таблицу секций (такой-же парсинг):

function GetTlsCallbackAddr(const FilePath: string;
  SectionDataList: TSectionDataList; Index: Integer): DWORD;
var
  S: TStringList;
  Line: string;
  SectionIndex, TlsAddr: Integer;
begin
  Result := 0;
  try
    S := TStringList.Create;
    try
      S.LoadFromFile(FilePath);
      Writeln('Ищу tls_callback...');
      repeat
        Line := Trim(S[Index]);
        Inc(Index);
        if Index = S.Count then Break;
      until Pos('tls_callback', Line) <> 0;
      if Pos('tls_callback', Line) = 0 then
      begin
        Writeln('В МАР файле запись о tls_callback не обнаружена');
        Exit;
      end;
      SectionIndex := StrToInt(Copy(Line, 1, 4));
      Delete(Line, 1, 5);
      TlsAddr := StrToInt('$' + Copy(Line, 1, 8));
      Writeln('tls_callback найден, смещение: ', IntToHex(TlsAddr, 8), ', секция: ', SectionIndex);
      Writeln('Ищу запись о секции...');
      for Index := 0 to SectionDataList.Count - 1 do
        if SectionDataList[Index].Index = SectionIndex then
        begin
          Result := SectionDataList[Index].StartAddr + DWORD(TlsAddr);
          Writeln('TLS Callback, найден в секции "', SectionDataList[Index].SectionName,
            '", оффсет секции: ', IntToHex(SectionDataList[Index].StartAddr, 8),
            ', расcчитанный адресc: ', IntToHex(Result, 8));
          Break;
        end;
      if Result = 0 then
        Writeln('Секция содержащая tls_callback не найдена')
    finally
      S.Free;
    end;
  except
    // все исключения глушим. есть коды ошибок
    on E: Exception do    
      Writeln('GetTlsCallbackAddr: ' + E.ClassName + ': ' + E.Message);
  end;
end;

И вот на руках у нас есть экзешник и точный адрес функции, которая должна быть выполнена в качестве TLS колбэка - осталось только найти адрес IMAGE_TLS_DIRECTORY, которая кстати выглядит вот так:



И пропатчить в ней поле AddressOfCallBacks + добавить VA адреса самих колбэков (их может быть несколько, как вы помните).

Собственно:

//
// Это простой способ поиска TLS таблицы НО - только в проектах,
// собранных в Delphi 2007 и выше (версии ниже не проверял)
// Если экзешник собран другим компилятором - естественно работать не будет
// но статья не об этом :)
// итак:
// =============================================================================
function GetTlsTableAddr(const FilePath: string): DWORD;
var
  F: TFileStream;
  DOS: TImageDosHeader;
  NT: TImageNtHeaders;
  I: Integer;
  Section: TImageSectionHeader;
  TlsFound: Boolean;
begin
  Result := 0;
  // открываем файл на чтение
  F := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
  try
    // читаем DOS заголовок, чтобы выйти на NT
    F.ReadBuffer(DOS, SizeOf(TImageDosHeader));
    F.Position := DOS._lfanew;
    // Читаем NT заголовок, чтобы получить количество секций
    F.ReadBuffer(NT, SizeOf(TImageNtHeaders));
    // читаем секции и ищем TLS
    TlsFound := False;
    for I := 0 to NT.FileHeader.NumberOfSections - 1 do
    begin
      F.ReadBuffer(Section, SizeOf(TImageSectionHeader));
      if PAnsiChar(@Section.Name[0]) = '.tls'  then
      begin
        TlsFound := True;
        // нашли IMAGE_TLS_DIRECTORY, смотрим, заполнен ли адрес TLS?
        if Section.PointerToRawData <> 0 then
          // если заполнен, то сразу делаем поправку на поле AddressOfCallback
          Result := Section.PointerToRawData + HardcodeTLS32Offset;
        Break;
      end;
    end;
    // делаем проверку, если нашли TLS то ее адрес обнилен, значит он будет сидеть в следующей секции
    // такая вот особенность у Delphi, к примеру XE4 заполняет адрес, а XE8 нет
    if TlsFound and (Result = 0) then
    begin
      F.ReadBuffer(Section, SizeOf(TImageSectionHeader));
      Result := Section.PointerToRawData + HardcodeTLS32Offset;
    end;
  finally
    F.Free;
  end;
end;

Данная функция возвращает реальный адрес _IMAGE_TLS_DIRECTORY32 в исполняемом файле + сразу возвращает оффсет на поле AddressOfCallBacks.

Есть несколько оговорок:
Работать будет только в случае Delphi образов (не стал сильно накручивать все нюансы, особенно с небольшой странностью в образе Delphi скомпиленных файлов) и только для 32 битных приложений (за это отвечает константа HardcodeTLS32Offset дающая смещение на поле AddressOfCallBacks, в 64 битах она должна быть равна 24).
Если вдруг заинтересуетесь аналогичным патчем 64-битных образов - пишите, это легко делается, просто не охота раздувать объем статьи :)

Впрочем - теперь можем патчить:

// непосредственно патч файла
function Patch(const FilePath, MapPath: string; TlsTable, CallbackAddr: DWORD): Boolean;
var
  F: TFileStream;
  NewFilePath, BackUpFilePath: string;
  OldCallbackTableAddr: DWORD;
begin
  Result := False;
  try
    NewFilePath := ExtractFilePath(FilePath) + 'tls_aded_' +
      ExtractFileName(FilePath);
    Writeln('Создаю копию файла, путь: ', NewFilePath);
    CopyFile(PChar(FilePath), PChar(NewFilePath), False);
    F := TFileStream.Create(NewFilePath, fmOpenReadWrite);
    try
      Writeln('Файл открыт');
      F.Position := TlsTable;
      // читаем адрес куда ссылался предыдущий колбэк
      F.ReadBuffer(OldCallbackTableAddr, 4);
      // в delphi образе он ссылается на SizeOfZeroFill структуры IMAGE_TLS_DIRECTORY
      // в которой оба последних поля заполнены нулями (якобы нет цепочки колбэков)
      // Поэтому не будем портить рабочую структуру и заставим его ссылаться на адрес
      // сразу за пределами данной структуры (плюс 2 дворда что в 32 битном, что в 64 битном варианте)
      Inc(OldCallbackTableAddr, SizeOf(DWORD) * 2);
      F.Position := TlsTable;
      // пишем новый адрес на старое место
      F.WriteBuffer(OldCallbackTableAddr, 4);
      Writeln('Назначен новый адрес цепочки обработчиков, оффсет: ', IntToHex(TlsTable, 8),
        ', новое значение: ', IntToHex(OldCallbackTableAddr, 8));
      // теперь прыгаем на место в которое должен быть записан VA адрес обработчика (не RVA)
      // пропускаем SizeOfZeroFill и Characteristics и становимся сразу за ними
      F.Position := TlsTable + SizeOf(DWORD) * 3;
      // а теперь пишем адрес нашего колбэка
      F.WriteBuffer(CallbackAddr, 4);
      Writeln('Адрес колбэка выставлен, оффсет: ', IntToHex(TlsTable + SizeOf(DWORD) * 3, 8));
      // после чего пишем ноль, для обозначения конца цепочки колбэков
      CallbackAddr := 0;
      F.WriteBuffer(CallbackAddr, 4);
    finally
      F.Free;
    end;
    // если все нормально, то переименовываем обратно
    Writeln('Создаю бэкап');
    BackUpFilePath := FilePath + '.bak';
    DeleteFile(BackUpFilePath);
    RenameFile(FilePath, BackUpFilePath);
    Writeln('Сохраняю результат');
    RenameFile(NewFilePath, FilePath);
    Writeln('Все задачи завершены');
    Result := True;
  except
    // все исключения глушим. есть коды ошибок
    on E: Exception do
    begin
      // в случае ошибки подчищаем за собой - возращая все обратно
      DeleteFile(NewFilePath);
      RenameFile(BackUpFilePath, FilePath);
      Writeln('Patch: ' + E.ClassName + ': ' + E.Message);
    end;
  end;
end;

Полный код найдете в демопримерах, а теперь посмотрим как это работает.



Работает?
А куда ж оно денется :)

5. И что с этим делать?


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

Но тут нужно еще немножко теории, чтобы вы поняли про что я говорю, а именно - для чего вообще реверсер делает дамп процесса?

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

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

Утрирую конечно, но это чтобы просто на пальцах объяснить для самого простого случая.

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

Заметьте - он должен дождаться выхода на OEP, чуть раньше или чуть позже дампить процесс бессмысленно (все уплывет).

И вот тут можем включиться мы с таким вот простым трюком.

Зная что у нас есть TLS колбэк, который априори выполнится до выхода на точку входа в приложения (не говоря уже про OEP), мы заведем, скажем так - антидамповую метку.

var
  // наша антидамповая метка
  anti_dump_mark: DWORD = 0;

Вся ее суть заключается в следующем:
Изначально, как вы видите - она инициализируется нулем. Т.е. это штатный запуск приложения.

Следующим шагом в нашем приложении должен отработать tls_callback, где мы напишем вот такой код:

const
  // некая константа, с которой будем сверятся
  valid_anti_dump_mark_value = $DEADBEEF;

// данный колбэк будет вызван, если файл будет корректно пропатчен
procedure tls_callback(hModule: HMODULE;
  ul_reason_for_call: DWORD; lpReserved: Pointer); stdcall;
begin
  if ul_reason_for_call = DLL_PROCESS_ATTACH then
  begin
    // делаем проверку - при штатном запуске антидамповая метка должна быть равна нулю
    if anti_dump_mark = 0 then
      // если это так - инициализируем ее.
      // проверкой инициализации будем заниматся на точке входа
      anti_dump_mark := valid_anti_dump_mark_value
    else
      // если же антидамповая метка не равна нулю - значит нас сдампили
      // намеренно бьем ее значение, чтобы сработал код на точке входа
      anti_dump_mark := 1;
  end;
end;

Суть вырисовывается?
Ага, т.е. если при старте приложения наша метка равна нулю, то мы инициализируем ее "совершенно секретной константой" valid_anti_dump_mark_value :)
А если нет (в этом случае она скорее всего будет равна как раз константе valid_anti_dump_mark_value) - наоборот убиваем ее значение.

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

Всего-то и осталось что написать код проверки данной метки при старте процесса:

const
  ptls_callback: Pointer = @tls_callback;

begin
  // проверяем - что там у нас по антидамповой метке, все ли нормально?
  // если все нормально - она должна быть равна константе valid_anti_dump_mark_value
  if anti_dump_mark <> valid_anti_dump_mark_value then
  begin
    // а если не равна - показываем "страшное" сообщение
    // такое может произойти только в двух случаях:
    // 1. мы забыли проинициализировать TLS Callback (не пропатчили тело экзешника)
    // 2. ура - нас сдампипли :)
    MessageBox(0, 'Process dumped', nil, 0);
    // и уходим по английски, не оборачиваясь ;)
    TerminateProcess(GetCurrentProcess, 0);
  end;
  // дабы процедура tls_callback появилась в MAP файле
  // нужно чтобы на нее была ссылка, банально вот такая:
  if ptls_callback <> nil then
    MessageBox(0, 'All done', nil, 0);
end.

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

Давайте покажу как это работает в живую:



6. В завершение


Ну, в принципе - как-то так :)

Пробуйте - экспериментируйте, здесь на самом деле только верхушка айсберга рассказана.
Вариантов использования колбэков море.
Но, если кому-то пригодится, буду только рад.

Исходный код к статье забирайте вот тут: http://rouse.drkb.ru/blog/early_execution.zip
Весь код тестировался под следующими версиями Delphi: 2007, 2010, XE4, XE10 (остальных нет в наличии, но думаю что будет работать и там, вероятно даже начиная с Delphi 2005).

В архиве в папке "bin" лежит уже пропатченный экзешник, показывающий методику антидампа, поэтому Chrome может ругаться на то, что в архиве лежит исполняемый файл.
Не переживайте сильно и смело качайте - конечно же там вирус :)

Как всегда огромная благодарность форуму "Мастера Дельфи", и особенное спасибо "NoUser", за некоторые моменты :)

---

© Александр (Rouse_) Багель

Март, 2016

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

  1. Спасибо за статью, очень познавательно для общего развития :)

    ОтветитьУдалить
  2. Отличная статья, спасибо! На хабр надо для популяризации Delphi )

    ОтветитьУдалить
  3. > Если вдруг заинтересуетесь аналогичным патчем 64-битных образов - пишите, это легко делается, просто не охота раздувать объем статьи :)

    Пишем, очень интересует. Заранее спасибо!

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

      Удалить
    2. очевидно ждать не стоит?..

      Удалить
    3. Хм, я вообще про это забыл, сделаю :)

      Удалить
    4. http://rouse.drkb.ru/blog/add_tls.zip
      Собирать в 64 битном режиме, изменения в функциях GetTlsTableAddr и Patch

      Удалить
    5. Спасибо!

      А есть ли в планах на ближайшее будущее опубликовать еще что нибудь?

      Удалить
    6. Ну были планы по написании статьи о реализации простейшей виртуальной машины на базе стрешки Пирса/штриха Шеффера, даже исходный код подготовил, да все как-то руки не дойдут.
      Так что пока что в планах ничего нет, если будет настроение и желание - может еще что-то напишу.
      Спускаться в нулевое кольце не охота (это для сишников уже материал будет), а описывать что-то высокоуровневое не охота, для этого есть форумы.

      Удалить
    7. Было бы интересно почитать… Надеюсь хорошее настроение посетит вас… Пишите еще, а мы подождем!

      Удалить
    8. Ну, наработки я выложил: http://rouse.drkb.ru/other.php#vm_core
      А вот писать статью заново пока не охота (она из-за сбоя практически перед релизом стерлась - дурацкая ситуация, а копии в виде вордовского документа я никогда не делал)

      Удалить
  4. По поводу того как узнать что мы отладчиком, может пригодиться кому

    library init_lib;

    uses Windows, System.SysUtils;

    {$R *.res}


    var
    ntdllLibrary: HMODULE = INVALID_HANDLE_VALUE;

    type
    NTSTATUS = System.LongInt;

    const
    STATUS_SUCCESS = NTSTATUS($00000000);

    type
    PROCESS_BASIC_INFORMATION = record
    ExitStatus: NTSTATUS;
    PebBaseAddress: PVOID;
    AffinityMask: ULONG_PTR;
    BasePriority: Integer;
    UniqueProcessId: THandle;
    InheritedFromUniqueProcessId: THandle;
    end;

    TProcessBasicInformation = PROCESS_BASIC_INFORMATION;
    PPROCESS_BASIC_INFORMATION = ^TProcessBasicInformation;


    type
    PPEB = ^TPEB;

    TPEB = packed record
    InheritedAddressSpace: System.Boolean;
    ReadImageFileExecOptions: System.Boolean;
    BeingDebugged: System.Boolean;
    //...
    end;

    type
    TNtQueryInformationProcess = function(
    ProcessHandle: THandle;
    ProcessInformationClass: ULONG;
    ProcessInformation: PVOID;
    ProcessInformationLength: ULONG;
    ReturnLength: PULONG): NTSTATUS; stdcall;

    TNtReadVirtualMemory = function(
    ProcessHandle: THandle;
    BaseAddress: PVOID;
    Buffer: PVOID;
    BufferLength: SIZE_T;
    ReturnLength: PSIZE_T): NTSTATUS; stdcall;

    var
    NtQueryInformationProcess: TNtQueryInformationProcess = nil;
    NtReadVirtualMemory: TNtReadVirtualMemory = nil;

    function GetProcessBeingDebugged(ProcessHandle: THandle; var BeingDebugged: Boolean): NTSTATUS;
    var
    ProcessBasicInformation: PROCESS_BASIC_INFORMATION;
    PEB: TPEB;
    ReturnLength: SIZE_T;
    begin
    BeingDebugged := False;

    if (@NtQueryInformationProcess = nil) then
    begin
    Result := ERROR_NOT_SUPPORTED;
    Exit;
    end;

    Result := NtQueryInformationProcess(ProcessHandle, 0, @ProcessBasicInformation, SizeOf(ProcessBasicInformation), nil);

    if Result <> STATUS_SUCCESS then
    Exit;

    if (@NtReadVirtualMemory = nil) then
    begin
    Result := ERROR_NOT_SUPPORTED;
    Exit;
    end;

    Result := NtReadVirtualMemory(ProcessHandle, ProcessBasicInformation.PebBaseAddress, @PEB, SizeOf(TPEB), @ReturnLength);

    if Result <> STATUS_SUCCESS then
    Exit;

    BeingDebugged := PEB.BeingDebugged;
    end;

    procedure Initialize;
    var
    BeingDebugged: Boolean;
    begin
    ntdllLibrary := LoadLibrary('ntdll.dll');

    @NtQueryInformationProcess := GetProcAddress(ntdllLibrary, 'NtQueryInformationProcess');
    @NtReadVirtualMemory := GetProcAddress(ntdllLibrary, 'NtReadVirtualMemory');

    GetProcessBeingDebugged(GetCurrentProcess, BeingDebugged);

    if BeingDebugged then
    TerminateProcess(GetCurrentProcess, ERROR_SUCCESS);
    end;

    exports
    Initialize;

    begin
    Initialize;
    end.

    ОтветитьУдалить
  5. Александр, Вы окончательно забросили свой блог?

    ОтветитьУдалить
  6. Ссылка на архив исходников была удалена с rouse.drkb.ru (мол там вирус - на самом деле нет)
    Вот новая ссылка: https://www.dropbox.com/s/ewjvyrf18m850bg/early_execution.zip

    ОтветитьУдалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить