Статья TLS callback – рояль в кустах для отладчиков

Системы класса Win-NT имеют интересный тип памяти под названием "Thread Local Storage", или локальная память потока. После того как мир увидел многопоточные приложения оказалось, что родственные потоки одной программы ведут между собой нешуточную борьбу за системные ресурсы, т.к. все загружаются в единое пространство родителя. Это как дети, которые не могут поделить одну плитку шоколада. Здесь вполне очевидна ситуация, когда поток(А) записал что-то в память, а ничего не подозревающий поток(В) тут-же подмял эту область памяти под себя. В результате, программист получает клубок из змей и никакого профита.

Microsoft пыталась решить эту проблему по разному, например ввела механизмы синхронизации в виде семафоров, эвентов и прочее. Но в итоге решила выделить каждому потоку по своей/личной памяти внутри его блока окружения TEB – Thread Environment Block, и это стало наиболее разумным решением.

В данной статье TLS рассматривается совсем не с той стороны, для которой она предназначена – ну дерутся потоки, да и флаг им в руки.. пусть используют стек (благо он у каждого свой). Это скучно и чтобы в очередной раз не жевать одну и ту-же жвачку, мы будем использовать статическую TLS для противодействия отладчикам, которые морально не готовы к такому повороту событий. По неизвестной причине, этот приём редко встречается в защитных механизмах программ, хотя на какое-то время в серьёз сбивает с рельсов начинающих взломщиков.


Общие сведения о локальной памяти

Начнём с того, что при подготовке процесса к запуску, система сначала строит для него контекст – это ядерные структуры KPROCESS и EPROCESS, выделение фреймов физической памяти и т.д. после чего часть этой информации система прописывает в юзерское пространство для своих сервисов и служб. Делается это для того, чтобы находящиеся в пространстве пользователя системные механизмы не лезли в ядро по всяким пустякам, т.к. это отнимает много времени. Поэтому система создаёт для процесса структуру под названием PEB Process Environment Block, куда и сбрасывает эту инфу.

Но процесс это обобщённое понятие, а непосредственно исполняющей единицой является программный поток Thread, что на рашке звучит как "тред". У любого процесса имеется как-минимум один основной поток, для которого система создаёт его личное окружение TEB или Thread Environment Block размером в одну 4К-байтную страницу. Однако программист может создавать для своего приложения ещё и ещё дополнительных потоков, и тогда получаем многопоточное приложение, и у каждого потока свой ТЕВ.

Есть специальные утилиты, которые во-всех деталях отображают память процесса. Одной из таких утилит является , которая оставила далеко позади все утилиты в своём классе. От её чуткого взора не ускользнёт не один тушканчик, включая структуры PEB и TEB со-значениями каждого из их полей – двойной клик на строке выводит детальную информацию:

pmm.png


Так Microsoft удалось разрешить "семейный" конфликт, успокоив вздорные потоки личными ТЕВ'ами. Внутри этих тебов нашлось место и для локальной памяти TLS.

На радостях, мелкософт ввёла сразу два вида TLS – статическую (выделяется на этапе компиляции программы) и динамическую, с пропиской в ТЕВ. Динамическая – это небольшой карман с начальным значением в 256-байт, чего вполне хватает для хранения локальных переменных и прочего барахла. Если треду понадобится ещё, то система выделит из своего Expansion-резерва сразу 800 дополнительных байт, что в сумме составит ~1K. Поток не может записывать в динамическую TLS отдельные байты – эта память имеет ячеистую структуру, где каждая ячейка размером в 4-байта и называется "слот". Система адресует слоты по их индексам, для чего имеются соответствующие API:

TlsAlloc() – возвращает индекс очередного/свободного слота в TEB;
TlsSetValue(index,value) – записывает dword в этот слот;
TlsGetValue(index) – читает в EAX значение из указанного слота;
TlsFree(index) – очищает указанный слот в TEB.

Если дубль-кликнуть на строке ТЕВ в программе "Process Memory Map", то можно увидеть состояние динамической TLS для данного потока. Эта память начинается со-смещения ТЕВ.0е10 и у меня выглядит так.. Прокрутив список ниже, уткнёмся в резервную TlsExpansionSlots:

teb.png



Статическая TLS память

В отличии от динамической, статическая TLS представляет собой программную секцию, точно такую-же как, например, секция-кода, данных, импорта и прочие. Это официальная секция, которая освещается в спецификации на РЕ-файл, но очень причудливая – один шаг в сторону и мастдай стреляет без предупреждения. Мало того-что её нужно явно определить, ещё и оформить нужным образом.

Если заглянуть в доки на РЕ-файл, то секция .tls лежит девятой в каталоге-секций. Это обстоятельство играет важную роль, т.к. именно под номером девять мы будем должны её загнать в свой исходник. Всего в этом каталоге имеется место для 16-ти возможных секций, что представляет собой документированный предел для PE-файла. Каждая запись состоит из двух DWORD'ов, где первый – это указатель на секцию, а второй – её размер, который никто не проверяет. Как нельзя лучше демонстрирует каталог Hiew, последовательностью клавиш Enter->F8->F10:

hw_tls.png


Когда приходит время загрузить исполняемый файл в память, загрузчик образов в Ntdll.dll начиная с нуля сканирует поле RVA всех записей в этом списке и если оно не нуль, подгружает указанную секцию в память. Таким образом проверка начинается с секции экспорта(0), потом импорт(1), и.. программа фактически может исполнять уже любой код – ей выделена память и импортированы все функции из внешних библиотек.

Однако на этом этапе, загрузчик не сразу передаёт управление на EntryPoint в программу, а продолжает сканирование остальных записей в списке. Когда очередь доходит до секции TLS под номером(9), лоадер приступает к вызову цепочки TLS-callback'ов, которые лежат в данной секции. Каждый коллбэк содержит в себе произвольный код и может делать в это время, всё-что ему вздумается. Отстрелявшись, он передаёт ружьё следующему коллбэку, если таковой имеется в цепочке. Только после этого загрузчик передаёт управление на точку-входа в программу, с которой начинают анализ все отладчики, а значит мы в фоне его сможем поиметь.


Структура секции .TLS

Для реализации "аферы века", нам всего-то требуется оформить должным образом Tls-секцию, иначе загрузчик тупо проигнорирует наш план. Вот пример готовой секции TLS с описанием прототипа:

C-подобный:
data  9                     ;// Секция-данных под номером 9
          dd  @tls, @tls      ; указатель на начало/конец лок.памяти треда
          dd  @tls            ; указатель на индекс треда
          dd  chain_0         ; указатель на начало Callback цепочки

;//**************** Статическая память TLS *********************************
@tls      dd  0               ; будет индексом треда (заполняет загрузчик) *
chain_0   dd  callback_0      ; цепочка указателей на Callback-функции     *
chain_1   dd  callback_1      ;  ...                                       *
chain_N   dd  callback_N      ;    ...                                     *
          dd  0               ; маркер окончания цепочки!                  *
;//*************************************************************************
end  data

Немного проясним ситуацию..
Статическую TLS я выделил в блок – она является общей для всех потоков процесса и не может расширяться динамически. Например поток(А) может использовать нулевой коллбэк, а первый коллбэк мы можем повесить на поток(В) и т.д. Статическую память распределяет по-потокам сам программист, на этапе планирования алгоритма программы. Здесь уже нет кредитного портфеля на виртуальную память, как в случае с динамической TLS, и мы можем забить коллбэками хоть всю секцию.

Когда загрузчик обнаруживает тред со-статической памятью, он назначает для него уникальный индекс, и сохраняет его в первом dword'e выделенной памяти – в примере выше это переменная @tls. По этому индексу система отличает память одного треда, от памяти другого. В структуре ТЕВ потока по адресу fs:[2Ch] имеется поле TlsPointer, которое хранит указатель на TLS-массив, содержащий данные его локальной памяти.

Под занавес, рассмотрим что находится снаружи блока..
Там 4 двойных-слова, и все указатели. Первые два – это линки на голову и хвост локальной памяти. Как упоминалось выше, в каталоге-секций размер никто не проверяет, а только поле RVA. Поэтому туда можно пихать что-угодно – здесь я их сравнял. Эти указатели не используют переменную @tls, а линки нужны лишь для того, чтобы компилятор сгенерил адрес. Ну и последний chain – это указатель на начало цепочки. Лоадер будет пинать по-очереди каждую ссылку callback_0..N, пока не встретит терминальный нуль.


Практика – отладчик в топку!

На данный момент мы пришли к тому, что благодаря TLS-callback имеем возможность произвести какие-нибудь операции до точки-входа в программу. Возможность – это хорошо, только делать-то чё будем? Если решили по-бороться с отладчиком, то каким образом? Здесь есть несколько вариантов.. Козырь в рукаве в виде Tls у нас уже есть, поэтому можно выбрать самый ламерский метод обнаружения дебага – IsDebuggerPresent().

Эта бесполезная функция просто стреляет у всех сигареты, в том числе и у структуры РЕВ, а точнее у его поля РЕВ.02. Дело в том, что при обнаружении отладки система сама выставляет в этом поле флаг, а упомянутая функция просто его проверяет. Например вот скрин отладчика WinDbg, которому я вскормил приложение и запросил его-же структуру РЕВ:

dbg_88.png


Так-что не будем беспокоить эту функцию, а лучше проверим флаг BeingDebugged вручную. Указатель на РЕВ лежит во-всех структурах ТЕВ по смещению 0х30, а на сам ТЕВ всегда указывает сегментный регистр FS. Алгоритм эксперементальной прожки выстроим так:
  1. В секции TLS создать два коллбэка.
  2. Первый будет проверять агрессивную среду отладки.
  3. Если обнаружим, то выводим мессагу и висним до точки-входа в дебрях загрузчика.
  4. Второй коллбэк чисто для демо скажет, что нет отладчика (если нет), т.к. получит упр. после первого.
  5. В основном теле залезем в реестр, и вытащим от туда версию BIOS.
  6. На выход..
C-подобный:
include  'win32ax.inc'
.data
capt     db   'Читаем из реестра',0
string   db   'Версия BIOS:  '
buff     db   64 dup(0)                        ; буфер под строку реестра
bSize    dd   64                               ; размер буфера
subKey   db   'Hardware\Description\System',0  ; ветка реестра
key      db   'SystemBiosVersion',0            ; выбираем строку в ветке
hndl     dd   0                                ; дескриптор ветки
type     dd   0                                ; тип данных
clear    db   'В Багдаде всё спокойно!',0
fuck     db   'Внимание! Обнаружен отладчик!',13,10
         db   'Отладка этого приложения не возможна.',13,10
         db   'Найди меня, если сможешь..',0
;------
.code
start:
;//--- Читаем реестр -------------------
       invoke   RegOpenKeyEx,HKEY_LOCAL_MACHINE,subKey,0,KEY_ALL_ACCESS,hndl
       invoke   RegQueryValueEx,[hndl],key,0,type,buff,bSize
       invoke   MessageBox,0,string,capt,0
       invoke   ExitProcess, 0

;//--- Тут идёт цепочка колбэков -------
;//--- Первый пошёл..(ищет отладчик) ---
proc  killer
       mov      eax,[fs:30h]            ; указатель на РЕВ
       cmp      byte[eax+2],0           ; байт(3) в РЕВ - это "BeingDebugged"
       jz       @f                      ; если он зеро, отладчика нет
       invoke   MessageBox,0,fuck,0,0   ; иначе: мессага,
       jmp      $                       ;   ..и виснем внутри загрузчика.
@@:    ret                              ; горизонт чист, выходим из коллбэка.
endp
;//--- Второй колбэк в цепочке ---------
proc   demo                             ; сработает до метки 'START',
       invoke  MessageBox,0,clear,0,0   ;   ..поэтому под виндой увидим сначала вторую,
       ret                              ;      ..а потом первую мессагу из секции-данных.
endp
.end start                              ; конец секции кода!!!

;//~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.
;//--- Секция TLS со-всей подноготной
data  9
        dd  tls,tls,tls,first    ; указатели
tls     dd  0                    ; место под индекс
first   dd  killer, demo         ; ссылки на цепочку колбеков
        dd  0                    ; маркер окончания цепочки
end   data

olly.png



Заключение

Пример никак не претендует на чистоту и оригинальность, тем-более что он уже бородатый и "таблэтка" лежит на поверхности. Более того, у некоторых код может отдебажиться с первого раза. Однако TLS-коллбэкам можно найти и другие применения, например подготавливать ресурсы и прочее для своих программ задолго до их запуска. Да и просто ещё один таракано-клоп в программистскую копилку.

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

Aleks Binakril

Green Team
04.12.2019
38
5
BIT
0
впервые прочитал о том что можно TLS нафаршировать своими функциями на ВАСМЕ, но рассказанный здесь метод весьма оригинален
 
  • Нравится
Реакции: Marylin

Pavel Shuhray

Green Team
14.12.2016
19
5
BIT
65
После запуска Process Memory Map антивирус (Касперского) завыл и запустил полную проверку системы. Что-нибудь другое порекомендуете?
 

morgot

Green Team
25.07.2018
74
35
BIT
2
Что-нибудь другое порекомендуете?
Рекомендую удалить антивирус с машины, где занимаетесь низкоуровневой разработкой. Все эти вещи (дебаг, перехват апи, ассемблер в целом) вызывают негативную реакцию у аверов. Также аверы вставляют свои коллбеки куда не надо, подгружают ненужные длл и т.д. и т.п. Если у вас нет отдельной машины, тестируйте все на виртуалке, а если и ее нет - хотя бы отключайте авер полностью на момент обучения разработке.

Статья отличная.
 
  • Нравится
Реакции: Mikl___ и Marylin
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!