Как вы думаете, сколько кода выполняется до того момента, как приложение запустится, или отладчик передаст управление в ваши руки?
Много - поверьте мне.
Вы даже чихнуть не успеете, как окажется, что кто-то уже успел поработать в теле вашего процесса и отдал его вам на руки в момент его запуска - работайте и удивляйтесь.
Нет, не вирусы, зачем? Вполне легальные продукты, контролирующие все и вся.
Думаете - вру и считаете что при нажатии кнопки F7 (Trace Into) вы полностью контролируете процесс отладки?
Тогда я вас разочарую, есть множество способов выполнить код, до того момента, как вы приступили к его дебагу.
О нескольких я попробую рассказать.
Начну с небольшой демонстрации.
Давайте создадим новое консольное приложение и запустим его.
Посмотрите на картинку:
Мы еще не успели ничего сделать, а в адресное пространство нашего процесса подгрузилось нормальное такое количество библиотек.
А почему?
Потому что они импортируются статически посредством таблицы импорта, которая есть в каждом приличном приложении.
Кто их загружает?
Правильно, загрузчик. Каждая загруженная библиотека - это в принципе самостоятельное приложение, где есть точка входа, те же таблицы импорта/эспорта и функционал.
В большинстве своем они, конечно, являются именно - библиотеками.
Но, давайте посмотрим на это через карту памяти процесса (рассмотрим браузер Chrome):
Ух ты, а что это тут у нас?
Некая хромовская библиотека имеет помимо точки входа (Entry Point) еще аж 3 колбэка, которые будут выполнены до того как Chrome сможет с ней работать.
Вот они все трое:
А ведь у каждой библиотеки есть еще и обычная точка входа, которая выполнится после обработки колбэков.
И это все выполнится до того, как мы нажали F7 :)
При старте процесса он читает его PE образ и потихонечку начинает подгружать модули один за одним, ориентируясь на таблицу импорта каждого подгруженного модуля.
К примеру, наше приложение через импорт тянет kernel32.dll (так бывает).
Вот он загрузил наш образ и, не передавая управления на точку входа, грузит kernel32, подгрузив который он анализирует его таблицу импорта и понимает, что нужно в довесок подтянуть еще вот такенный список библиотек
Грузит каждую из них, и когда убедился что все что нужно подгружено - начинается магия.
Он смотрит TLS таблицу библиотеки, если она не пуста, передает управление каждому колбэку из таблицы и в завершении всего передает управление на точку входа каждой подгруженной им библиотеки.
И заметьте - пока что еще мы висим на F7 - код нашей программы не выполняется, но выполняется код из точек входа библиотек и их TLS Callback-ов
И вот когда все подгружено и время дошло до нас, только тогда загрузчик анализирует нашу TLS таблицу (передавая управление если нужно) и в финале отдает управление на точку входа, где мы и сидим в отладчике :)
Зная, что управление сначала передается на код в библиотеках (слинкованных статически) мы можем сделать вот такой простой трюк для исполнения кода до выхода на точку входа в приложение.
Пишем код библиотеки.
Процедура Foo по факту не нужна - она используется только для статической линковки с приложением (чтобы библиотека была заявлена в таблице импорта).
Ну и теперь сам код приложения:
Вот так это выглядит на практике:
Если бы мы грузили библиотеку в динамике через LoadLibrary - код в DllMain библиотеки тоже бы выполнился, но!!!
Но уже после точки входа, а это нам не нужно, все таки рассматриваем раннее исполнение кода.
Хороший вопрос.
Как правило раннее исполнение кода применяется при реализации антиотладочных и антидамповых трюках.
С антиотладкой все очень просто, как я показал выше, ваш код выполнится еще до того, как на него получит управление отладчик.
Немного утрирую, конечно - отладчик, естественно, тоже получит уведомление о загрузке библиотеки, но, как правило - это никто не анализирует.
Но находясь на точке входа в библиотеку, вы можете абсолютно легально проверить - а не находимся ли мы под отладкой?
Как это сделать - это вам решать, способов масса.
А по поводу антидампа - тут тоже все достаточно просто, но на нем я остановлюсь чуть позже.
Дочитали до третьей главы?
Уважаю :)
Тогда займемся более серьезными вещами.
Итак, чуть выше я упоминал про TLS Callback - посмотрим что это за зверь.
Грубо говоря, при инициализации каждого потока, управление изначально передается не на TreadProc, а на цепочку его колбэков.
При запуске вашего приложения (если отбросить нюансы с инициализацией загрузчиком секций, релоков, PEB/TEB) начинает работать главный поток.
Но, перед передачей управления данному потоку, загрузчик первым образом анализирует образ вашего приложения на наличие TLS секции, ожидая там увидеть что-то похожее на вот эту структуру (для 32 бит к примеру - 64 бита не рассматриваю):
Параметр AddressOfCallBacks указывает на VA адрес (если сильно упрощенно RVA + ImageBase образа) начала цепочки Callback-ов, которые должны быть выполнены, до передачи управления на точку входа в ThreadProc (для главного потока - это точка входа в приложение, т.н. EntryPoint).
Цепочка - это банально массив адресов каждого колбэка в памяти процесса, завершающийся нулем.
Помните картинку выше с хромовской библиотекой?
Вот так это выглядит в карте памяти процесса.
Синим выделено окончание цепочки TLS колбэков.
Как бы и нам самим такое сделать?
К примеру в MSVC это выглядит вот так:
Если собрать этот код в студии - то сначала отобразится окно:
И только потом мы увидим второе "Entry Point Message".
При билде данного исходного кода студия автоматом сформирует валидную TLS секцию и проинициализирует цепочку колбэков адресом процедуры tls_callback.
Это в Visual Studio, а вот в Дельфи такого сделать так сходу нельзя - не умеет она, но!!!
Но есть небольшой трюк, связанный с тем что разработчики компилера Дельфи, уж не знаю по каким причинам, немного нам помогли, создав неинициализированную TLS секцию в исполняемом файле. С какого времени это началось - я не знаю, но начиная с Delphi 2010 такая секция точно присутствует во всех исполняемых файлах.
А раз у нас на руках есть TLS секция, почему-бы нам ее не проинициализировать самостоятельно?
Давайте посмотрим что она из себя представляет и накидаем небольшое приложение.
Давайте его сразу соберем и запустим, на выходе у нас будет только сообщение "Entry Point Message".
Что собственно и логично, т.к. tls_callback сейчас является просто некоей функцией, о которой лоадер не знает и вызывать ее не собирается.
Но, нам нужно ее вызвать. А сделать это мы сможем только посредством патча уже собранного файла.
Итак, у нас на руках есть исполняемый файл с объявленной функцией tls_callback.
Рядом с ним лежит MAP файл (вы же включили генерацию МАР файла в настройках линкера?)
Давайте посмотрим на него (покажу только интересующие нас куски мар файла):
Самая последняя строчка - это номер секции и смещение нашей процедуры tls_callback.
Вверху расположены сами секции и их адреса.
К примеру, мы видим что процедура с именем "tls_callback" расположена в первой секции (индекс секции равен 0001).
Оффсет колбэка относительно начала секции равен 00003208.
Смотрим VA адрес секции за индексом 0001 (вверху) - это секция ".text", содержащая в себе код (на что указывает подсказка о типе данных "CODE"), а ее адрес равен 00401000.
Банально суммируем оба значения (числа 16 битные, т.е. HEX).
00003208 + 00401000 = 00404208.
Проверяем.
Да, это наш код.
Значит напишем небольшую функцию, которая будет получать из МАР файла реальный адрес колбэка по его имени (пусть он так и останется "tls_callback", хотя имя может быть любым).
Вот это нам пригодится.
Вот так мы зачитаем всю таблицу секций (банальный парсинг текстового файла):
А вот так найдем адрес нашей функции, ориентируясь на таблицу секций (такой-же парсинг):
И вот на руках у нас есть экзешник и точный адрес функции, которая должна быть выполнена в качестве TLS колбэка - осталось только найти адрес IMAGE_TLS_DIRECTORY, которая кстати выглядит вот так:
И пропатчить в ней поле AddressOfCallBacks + добавить VA адреса самих колбэков (их может быть несколько, как вы помните).
Собственно:
Данная функция возвращает реальный адрес _IMAGE_TLS_DIRECTORY32 в исполняемом файле + сразу возвращает оффсет на поле AddressOfCallBacks.
Есть несколько оговорок:
Работать будет только в случае Delphi образов (не стал сильно накручивать все нюансы, особенно с небольшой странностью в образе Delphi скомпиленных файлов) и только для 32 битных приложений (за это отвечает константа HardcodeTLS32Offset дающая смещение на поле AddressOfCallBacks, в 64 битах она должна быть равна 24).
Если вдруг заинтересуетесь аналогичным патчем 64-битных образов - пишите, это легко делается, просто не охота раздувать объем статьи :)
Впрочем - теперь можем патчить:
Полный код найдете в демопримерах, а теперь посмотрим как это работает.
Работает?
А куда ж оно денется :)
С этим нужно работать, к примеру сейчас я продемонстрирую небольшой антидамповый трюк, который можно реализовать при помощи использования TLS колбэка.
Но тут нужно еще немножко теории, чтобы вы поняли про что я говорю, а именно - для чего вообще реверсер делает дамп процесса?
Представьте что ваше приложение на диске расположено немного не в том виде, в каком оно будет выглядеть в запущенном виде, ну к примеру оно запаковано, или зашифровано, или у него сбита точка входа каким нибудь вашим хитрым трюком (да в принципе какая по сути разница).
Суть в что, что реверсер, такой-же ленивый как и мы все, и ему лениво каждый раз при старте дожидаться распаковки/расшифровки - он знает, что в тот момент, когда приложение выйдет на свою оригинальную точку входа (OEP - не путать с EntryPoint), с него будет уже снята вся распаковка/расшифровка и прочее.
Утрирую конечно, но это чтобы просто на пальцах объяснить для самого простого случая.
И вот дождавшись выхода на OEP - он дампит процесс, правит таблицу импорта, и потом продолжает работать уже с этим образом, не утруждая себя работой со всем тем навесняком - который присутствовал изначально в вашем приложении.
Заметьте - он должен дождаться выхода на OEP, чуть раньше или чуть позже дампить процесс бессмысленно (все уплывет).
И вот тут можем включиться мы с таким вот простым трюком.
Зная что у нас есть TLS колбэк, который априори выполнится до выхода на точку входа в приложения (не говоря уже про OEP), мы заведем, скажем так - антидамповую метку.
Вся ее суть заключается в следующем:
Изначально, как вы видите - она инициализируется нулем. Т.е. это штатный запуск приложения.
Следующим шагом в нашем приложении должен отработать tls_callback, где мы напишем вот такой код:
Суть вырисовывается?
Ага, т.е. если при старте приложения наша метка равна нулю, то мы инициализируем ее "совершенно секретной константой" valid_anti_dump_mark_value :)
А если нет (в этом случае она скорее всего будет равна как раз константе valid_anti_dump_mark_value) - наоборот убиваем ее значение.
Поясню - anti_dump_mark сидит в области данных нашего процесса, и реверсер, когда будет дампить процесс, после того как tls_callback инициализировал эту метку, захватит ее значение в новый сдампленный образ.
Соотвественно, при старте - метка anti_dump_mark уже не будет проинициализированна нулем, т.к. в том месте образа, где расположено её изначальное значение уже будет не ноль, а значение константы valid_anti_dump_mark_value.
Всего-то и осталось что написать код проверки данной метки при старте процесса:
Ну, конечно-же не стоит забывать что исполняемый файл должен быть пропатчен, чтобы выполнялась процедура tls_callback, в противном случае антидамповая метка при старте не будет проинициализирована и будет ложное срабатывание - мол нас сдампили :)
Давайте покажу как это работает в живую:
Ну, в принципе - как-то так :)
Пробуйте - экспериментируйте, здесь на самом деле только верхушка айсберга рассказана.
Вариантов использования колбэков море.
Но, если кому-то пригодится, буду только рад.
Исходный код к статье забирайте вот тут: http://rouse.drkb.ru/blog/early_execution.zip
Весь код тестировался под следующими версиями Delphi: 2007, 2010, XE4, XE10 (остальных нет в наличии, но думаю что будет работать и там, вероятно даже начиная с Delphi 2005).
В архиве в папке "bin" лежит уже пропатченный экзешник, показывающий методику антидампа, поэтому Chrome может ругаться на то, что в архиве лежит исполняемый файл.
Не переживайте сильно и смело качайте - конечно же там вирус :)
Как всегда огромная благодарность форуму "Мастера Дельфи", и особенное спасибо "NoUser", за некоторые моменты :)
---
Много - поверьте мне.
Вы даже чихнуть не успеете, как окажется, что кто-то уже успел поработать в теле вашего процесса и отдал его вам на руки в момент его запуска - работайте и удивляйтесь.
Нет, не вирусы, зачем? Вполне легальные продукты, контролирующие все и вся.
Думаете - вру и считаете что при нажатии кнопки 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
Спасибо за статью, очень познавательно для общего развития :)
ОтветитьУдалитьЭто всегда - пожалуйста :)
УдалитьОтличная статья, спасибо! На хабр надо для популяризации Delphi )
ОтветитьУдалитьЛениво :)
ОтветитьУдалить> Если вдруг заинтересуетесь аналогичным патчем 64-битных образов - пишите, это легко делается, просто не охота раздувать объем статьи :)
ОтветитьУдалитьПишем, очень интересует. Заранее спасибо!
Угу, чуть попозже исходники обновлю и отпишусь.
Удалитьочевидно ждать не стоит?..
УдалитьХм, я вообще про это забыл, сделаю :)
Удалитьhttp://rouse.drkb.ru/blog/add_tls.zip
УдалитьСобирать в 64 битном режиме, изменения в функциях GetTlsTableAddr и Patch
Спасибо!
УдалитьА есть ли в планах на ближайшее будущее опубликовать еще что нибудь?
Ну были планы по написании статьи о реализации простейшей виртуальной машины на базе стрешки Пирса/штриха Шеффера, даже исходный код подготовил, да все как-то руки не дойдут.
УдалитьТак что пока что в планах ничего нет, если будет настроение и желание - может еще что-то напишу.
Спускаться в нулевое кольце не охота (это для сишников уже материал будет), а описывать что-то высокоуровневое не охота, для этого есть форумы.
Было бы интересно почитать… Надеюсь хорошее настроение посетит вас… Пишите еще, а мы подождем!
УдалитьНу, наработки я выложил: http://rouse.drkb.ru/other.php#vm_core
УдалитьА вот писать статью заново пока не охота (она из-за сбоя практически перед релизом стерлась - дурацкая ситуация, а копии в виде вордовского документа я никогда не делал)
По поводу того как узнать что мы отладчиком, может пригодиться кому
ОтветитьУдалить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.
Александр, Вы окончательно забросили свой блог?
ОтветитьУдалитьПока что нет времени
ОтветитьУдалитьСсылка на архив исходников была удалена с rouse.drkb.ru (мол там вирус - на самом деле нет)
ОтветитьУдалитьВот новая ссылка: https://www.dropbox.com/s/ewjvyrf18m850bg/early_execution.zip
Этот комментарий был удален автором.
ОтветитьУдалить