Форум программистов, компьютерный форум, киберфорум
FASM
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.94/167: Рейтинг темы: голосов - 167, средняя оценка - 4.94
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4

Пишем DLL на FASM'e

25.10.2018, 00:16. Показов 36788. Ответов 17
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Всё описанное здесь - это личное видение,
если не оговорено обратное.

Если вас мучает вопрос: "Как создать DLL-библиотеку?", то вы пришли по адресу.
Небольшой ликбез для новичков позволит разобраться с такими вопросами как:
  1. Что такое DLL и зачем они нужны.
  2. Системная поддержка и проецирование в память.
  3. Точка входа в DLL-библиотеку.
  4. Пример простейшей DDL на FASM.
  5. Подводные камни при их создании.
  6. Заключение.
--------------------------------------------------

Dynamic Link Library в простонародье DLL - часть исполняемого файла в виде внешнего модуля. Модуль представляет из-себя библиотеку уникальных процедур и функций, которых нет в составе системных WinAPI, но необходимых нашей программе. Разделение исполняемого файла на подгружаемые модули позволяет эффективно использовать память, т.к. услугами одной библиотеки могут пользоваться сразу несколько приложений.

По своей природе, DLL не отличается от EXE.. те-же секции кода/данных/импорта, только добавляется ещё одна - секция экспорта, благодаря которой приложения могут импортировать из библиотеки имеющиеся в ней функции. Есть и ещё одно/важное отличие *dll от *exe - они не способны сами исполнять код, поскольку каждый участок обёрнут в отдельную процедуру. Эти процедуры просто ждут своего часа, пока exe-модуль (или другая dll) не запросит какую-нибудь из них.


Системная поддержка и проецирование в память

Когда система запускает юзерский процесс, ядро проделывает огромную работу. Помимо прочего, это выделение памяти для юзерской кучи TLS - Thread Local Storage, и создание в этой куче служебных структур типа PEB - Process Environment Block (блок окружения процесса), и дочернюю к РЕВ структуру TEB - Thread Environment Block (блок окружения основного потока). Это т.н. "процесс инициализации приложения", подготавливающий юзерский контекст и окружение. Занимается этим "загрузчик образа", который находится в Ntdll.dll.

Поскольку процедура инициализации стартует до нашего приложения, алгоритм её действий остаётся для нас прозрачным. Однако после загрузки exe, в стеке кое-что остаётся и если прокрутить его в отладчике назад (в сторону меньших адресов), то можно найти там много интересного. Инструкции POP и RET не затирают данные в стеке, а просто смещают указатель ESP, в результате чего всё тайны всплывают наружу.

Проделав глобальную работу, загрузчик приступает к разбору таблицы импорта нашего EXE-модуля, чтобы подтянуть в юзерскую память внешние библиотеки, динамически скомпонованные во время компиляции EXE. Каждая импортируемая DLL может иметь свою/собственную таблицу импорта, поэтому эта операция будет продолжаться в рекурсивном режиме до тех пор, пока не будет найден весь импорт. По мере загрузки каждой DLL, загрузчик будет сохранять её информацию в виде базы, в структурах PEB и TEB.

В поисках библиотек, загрузчик сначала открывает в реестре ветку:
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
где прописаны системные либы пользовательского режима. Если в таблице импорта нашего exe имеются и сторонние (по-факту самописные) dll'ки, то загрузчик ищет их в следующих директориях:

1. Текущий каталог, от куда запускается приложение;
2. C:\Windows\system32;
3. C:\Windows\system (для 16-бит приложений, но находит и 32-бит).

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

Добавлено через 3 минуты
База данных загруженных модулей

Загрузчик ведёт список всех модулей, которые были загружены нашим процессом. Инфа хранится в структуре PEB, ..а именно в её подструктуре PEB_LDR_DATA. В ней загрузчик ведёт три списка 'list', в которых содержится одинаковые, но по разному выстроенные листы: по порядку загрузки(Load), по размещению в памяти(Memory) и в порядке инициализации(Init):
Code
1
2
3
4
5
6
7
8
typedef struct _PEB_LDR_DATA {
  ULONG               Length;
  BOOLEAN             Initialized;
  PVOID               SsHandle;
  LIST_ENTRY          InLoadOrderModuleList;
  LIST_ENTRY          InMemoryOrderModuleList;
  LIST_ENTRY          InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
Эти списки представляют из-себя дочерние структуры LDR_MODULE, в которых и хранится инфа о каждом/подгружаемом модуле (для всех импортируемых DLL создаётся свой лист). Посмотрим на эти поля и дадим им короткие пояснения:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _LDR_MODULE {
  LIST_ENTRY          LoadOrderList     ; см. PEB_LDR_DATA
  LIST_ENTRY          MemoryOrderList   ;  ...
  LIST_ENTRY          InitOrderList     ;    ...
  PVOID               BaseAddress       ; Базовый адрес загрузки в память
  PVOID               EntryPoint        ; Указатель на точку-входа в библиотеку
  ULONG               SizeOfImage       ; Её размер в памяти
  UNICODE_STRING      FullDllName       ; Полный путь к либе
  UNICODE_STRING      BaseDllName       ; Имя без пути
  ULONG               Flags             ; Атрибуты доступа
  SHORT               LoadCount         ; Важно! кол-во обращений к DLL
  SHORT               TlsIndex          ; Слот либы в юзерской куче
  LIST_ENTRY          HashTableEntry    ; Хеш для быстрого запуска/остановки
  ULONG               TimeDateStamp     ;
} LDR_MODULE, *PLDR_MODULE;
Нужно сказать, что это детали загрузчика пользовательского режима в системной Ntdll.dll. Ядро использует свой/собственный загрузчик для нативных драйверов и зависимых к ним DLL, с похожей структурой записей. Важно понять, что РЕВ создаётся загрузчиком для каждой задачи (процесса) в отдельности: сколько процессов - столько и структур РЕВ.

Посмотрим, чем занимается загрузчик на финальном этапе, после настройки окружения:
  1. Загружает каждую DLL, на которую есть линк в таблице импорта EXE-модуля.
  2. Проверяет в базе LDR_MODULE, не была-ли DLL уже загружена (поле LoadCount). Если нет, загрузчик создает структуру для этой DLL и вставляет её в базу данных РЕВ.
  3. При поиске, загрузчик сначала проверяет DDL на соответсвие Known (в реестре). Попадание означает, что DLL уже в памяти и была загружена системой при запуске. Если в реестре прописки нет, то ищется рядом с EXE или в папках System.
  4. После того как DLL найдена, загрузчик передаёт управление ядерной функции LdrLoadDLL() для её загрузки в память. Эта fn. возвращает управление опять загрузчику, который проверяет, не загрузило-ли ядро нашу DLL'ку в какое-нибудь другое место - механизм перемещения, или Relocation.
  5. Если загрузчик обнаружит перемещение проекции файла от базового значения, он проводит анализ секции ".reloc" в DLL-модуле и выполняет поправки в структурах РЕВ. Если инфа о перемещении в либе отсутствует, встречаем сбой загрузки DLL.
  6. Теперь загрузчик проводит анализ IAT (таблицы импорта) для поиска конкретных функций по их именам или ординалам (порядковым номерам).
  7. На этом загрузчик завершает свою работу и передаёт управление на точку входа в EXE-приложение, которая находится в стеке. Процесс запускается.. но загрузчик не уходит в тень, а всегда ожидает нас на низком старте. К его услугам мы обращаемся всякий раз, когда динамически вызываем функции DLL-библиотек, например через LoadLibrary(). Загрузчику приходится повторять все/перечисленные пункты сначала, что сказывается на времени выполнения программы.

Добавлено через 1 минуту
Точка входа в DLL-библиотеку

Когда EXE-модуль вызывает какую-нибудь функцию из DLL, системный загрузчик прибегает к услугам API LdrLoadDLL() из либы ntdll. Эта-же API принимает управление от функций динамического вызова процедур LoadLibrary(), GetModuleFileName() etc, ожидающих получить дескрипторы модулей. Вот её прототип:
Code
1
2
3
4
5
LdrLoadDll(
   IN PWCHAR               PathToFile OPTIONAL,
   IN ULONG                Flags OPTIONAL,
   IN PUNICODE_STRING      ModuleFileName,
  OUT PHANDLE              ModuleHandle );
Запрос от LdrLoadDLL() принимает на грудь необязательная функция DllEntryPoint() (аналог DllMain), которая по-факту является точкой-входа в любую библиотеку. Необязательная она тем, что мы можем игнорировать её аргументы и это не приведёт к краху. Однако чего нельзя ингорить, так это возвращаемое ею значение TRUE в регистре EAX, поскольку LdrLoadDLL() рассчитывает его получить. Ldr передаёт в DllEntryPoint() три аргумента в таком порядке, а обрабатывать их или нет - дело хозяйское, ..главное вернуть EAX=1:
Code
1
2
3
4
DllEntryPoint (
***IN HINSTANCE            hinstDLL,
***IN DWORD                fdwReason,
***IN LPVOID               lpvReserved *);
  1. hinstDLL - в теории дескриптор DLL, а в реале - её VA-база в памяти.
  2. fdwReason - одна из четырёх причин вызова точки входа:
    • DLL_PROCESS_ATTACH - модуль подключается к пространству текущего процесса при его запуске.
    • DLL_PROCESS_DETACH - модуль удаляется из адресного пространства процесса.
    • DLL_THREAD_ATTACH - процесс создаёт новый поток, причем на основной поток процесса это не распространяется.
    • DLL_THREAD_DETACH - созданный поток отработал своё и завершается.
  3. lpvReserved - резерв для системных нужд.
Здесь видно, что интересным является лишь параметр fdwReason, который передаёт нам LdrLoadDLL(). Благодаря ему мы можем провести какие-нибудь подготовительные операции init, перед вызовом конкретной функции из нашей тушки. Но повторюсь, что никто нас к этому не принуждает и мы можем просто вернуть единицу и всё. Нужно сказать и о том, что Ldr ожидает TRUE только при обращение к DDL'ке с флагом 'DLL_PROCESS_ATTACH', а в остальных случаях возврат не критичен.

Кроме этого нужно учитывать, что на входе либа ещё не определена, и фактически мы находимся внутри обработчика LdrLoadDLL(). Такая картина накладывает определённые ограничения на вызов некоторых/вложенных Win-API внутри DllEntryPoint(), пока не выйдем из точки-входа по RET. Не сбрасывайте это со-счетов!

С деталями можно ознакомиться в статье КК:
"Динамические библиотеки для гурманов" - http://citforum.ru/book/cook/dll1.shtml
3
Programming
Эксперт
39485 / 9562 / 3019
Регистрация: 12.04.2006
Сообщений: 41,671
Блог
25.10.2018, 00:16
Ответы с готовыми решениями:

Обсуждение статьи "Пишем DLL на FASM'e"
R71MT, на сам знаешь каком сайте в разделе ARTICLES моя статья "Import x64", посмотри, может найдешь что-нибудь новое :)

Пишем DLL для работы с регистром.
Добрый день. Пишу DLL для работы с регистром и столкнулся со следующей проблемой при записи значения в регистр все отрабатывает...

Ошибка об отсутствии MSVCP140D.dll, ucrtbased.dll, CONCRT140D.dll, VCRUNTIME140D.dll
Добрый день, товарищи. Возникла проблема. Делаю программу с использованием openCV 3.1 на Visual Studio 2013. При компиляции выдает ошибку...

17
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
25.10.2018, 00:20  [ТС]
Лучший ответ Сообщение было отмечено Mikl___ как решение

Решение

Пример простейшей DLL на FASM

Думаю для теории достаточно и предлагаю подкрепить её практикой.
Напишем небольшую тестовую DLL, чтобы проверить вышесказанное в отладчике.
Из инструментов понадобятся: собственно FASM, отладчик 'OllyDbg', редактор 'HIEW' и наконец 'TotalComander', в котором используем только его плагин Lister для сбора инфы о модулях exe/dll (Ctrl+Q). Скелет типичной библиотеки DLL представлен ниже:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
format   PE gui dll                 ; собираем DLL-модуль
include 'win32ax.inc'               ; макросы для укороченных директив
include 'encoding/win1251.inc'      ; подключим кирилицу
;---------
.data
mes0    db  'Библиотека Dll удачно поключена!',0      ; мессага-флаг, что DLL робит
wName   db  'Тест внешнего модуля',0                  ;  ..обзовём окно.
 
;---------
.code
start:                    ; точка входа!
       mov     eax,1      ; возвращаем TRUE
       ret                ;  ..в родитель LdrLoadDLL()
 
; Теперь наша либа зарегана, и мы можем вставлять всякие процедуры.
; Пока только одна.., которая просто покажет окно.
proc   Hello
       invoke  MessageBox,0,mes0,wName,0   ; даём о себе знать!
       ret                                 ; на выход.
endp                                       ; конец процедуры 'Hello'
 
.end start                ; макрос автоматом вставит секцию импорта.
 
; А вот секцию экспорта нужно вбить на клаве,
; и перечислить в ней весь экспорт, чтобы EXE смог их импортировать
;---------
section '.edata' export data readable      ;
export  'myDll.dll',\                      ; имя нашей библиотеки
         Hello, 'Hello'                    ; тут перечисляем имена всех процедур 
;---------
section '.reloc' fixups data discardable   ; разрешаем загрузчику перемещать базу DLL в памяти
Здесь я привёл пример без стандартной функции входа 'DllEntryPoint', и сразу вернул TRUE загрузчику образа. Поскольку в секции импорта указано имя библиотеки как 'myDLL.dll', выходной файл тоже должен быть с таким-же именем, иначе экзешник его не подхватит.

После компиляции исходника по F9, откроем полученную либу в Оле и посмотрим на состояние стека, где последовательно выстроились аргументы для 'DllEntryPoint'. Значит можно вообще не придерживаться правил и читать флаги fdwReason прямо из стека, что позволит проводить ручную инициализацию даже за пределами точки-входа:



Намотав это на ус, теперь напишем EXE-модуль, который будет вызывать процедуру 'Hello' из нашей DLL. Если принять во-внимание толмут выше, проблем возникнуть не должно - компилим обычное гуй-приложение, которое затребует сначала внешнюю процедуру, а потом и сама покажет форточку:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
format  PE gui                    ; собираем EXE
include 'win32ax.inc'
include 'encoding/win1251.inc'
entry   start
;---------
.data
mess0    db  'А это уже EXE приложение..',0
wName    db  'Проверка EXE!',0
 
;---------
.code
start:
        invoke  Hello             ; зовём внешнюю процедуру из нашей DLL
        nop
        invoke  MessageBox,0,mess0,wName,0     ; следом - сразу системную.
        invoke  ExitProcess,0                  ; на выход!
 
;---------
; Тут макрос '.end start' уже не поможет, т.к. в импорте есть самопальные библиотеки dll.
; Перечисляем их имена и импортируемые из них функции
;---------
section '.idata' import data readable
library  myLib,  'myDLL.dll',\                 ; прописываем нашу либу,
         kernel, 'kernel32.dll',\              ;   ..а так-же системные.
         user,   'user32.dll'                  ;     ^^^^
import   myLib,  Hello, 'Hello'                ; таблица IAT со-списком функций.
import   kernel, ExitProcess, 'ExitProcess'
import   user,   MessageBox, 'MessageBoxA'
Осталось положить EXE рядом с DLL, и попробовать запустить EXE.
Если всё сделали правильно, то функциональность либы должно подтвердиться окном 'Библиотека Dll удачно поключена!', иначе проблема в имени DLL.

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

Позже рассмотрим способы подключения библиотек, которых всего-то два - статический (как в примере выше) и динамический (требует некоторых пояснений). Кроме того, нужно уделить внимание и проблеме авто/перемещения загрузчиком базовых адресов загружаемых модулей (релоки) с технологией ALSR. Актуальными являются и способы обмена данными между DLL и EXE, аргументов и глобальных переменных. Было-бы желание, настрой и время..
5
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
25.10.2018, 16:16  [ТС]
Динамический вызов процедур и функций

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

Решить эту проблему позволяет вызов функций по-требованию, который назвали "динамическим". Здесь рулит минимализм, когда в память грузятся только те библиотеки, которые на самом деле нужны. После того-как функция отработает, библиотеку можно опять выгрузить из памяти. Ясно, что программировать динамику сложнее чем статику, однако в этом есть и свои плюсы. Поскольку ответственность за вызов тут лежит полностью на программисте, он сам проверяет передаваемые и возвращаемые значения. Например, если вызов вернёт ошибку, то статика сразу рухнет. А вот динамическое подключение позволяет обработать ошибку и продолжить исполнение, что немаловажно в случае миграции приложения на другую платформу.

Динамический вызов держится на трёх API-китах: LoadLibrary, GetProcAddress и FreeLibrary.
  1. LoadLibrary() ищет либу по имени. Поиск ведётся в тех-же дирах, что и при статическом подключении. Если библиотека найдена, функция загружает и возвращает в EAX её базу в памяти. EAX=0 свидетельствует об ошибке.
  2. GetProcAddress() берёт базу библиотеки у LoadLibrary(), имя искомой функции, и возвращает в EAX её базу+смещение. В результате, получаем прямой VA-указатель на функцию в памяти. Эррор определяется по EAX=0.
  3. FreeLibrary() получив в EAX базу, выгружает DLL из памяти.
Посмотрим, как можно вызвать функцию по такому алгоритму..
У нас есть 'myDll.dll' с функций 'Hello' - оставим её без изменений, а вот EXE-модуль придётся модифицировать, причём с полной кастрацией нашей библиотеки из таблицы импорта экзешника:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
format  PE gui
include 'win32ax.inc'
include 'encoding/win1251.inc'
;-------
.data
mess0    db    'EXE-модуль',0       ; мессаша-флаг, что вызов из EXE
libName  db    'myDll.dll',0        ; имя внешней библиотеки
fName    db    'Hello',0            ;     ..имя функции в ней.
;-------
.code
start:
        invoke  LoadLibraryA,libName    ;<=== загружаем DLL в память!
        push    eax                         ; ++ получили её базу - запомнить
        invoke  GetProcAddress,eax,fName    ; ищем смещение функции в загруженой DLL
        push    @next                       ; пихаем в стек адрес возврата в нашу программу
        push    eax                         ; следом - указатель на функцию 'Hello'
        ret                       ;<<<------; вызываем 'Hello' по указателю!
@next:  pop     eax                         ; -- снимаем базу DLL со-стека
        invoke  FreeLibrary,eax         ;===> выгружаем её к чертям из памяти!
 
        invoke  MessageBox,0,mess0,0,0      ;
        invoke  ExitProcess,0               ;
;-------
.end start
; напомню, что макрос '.end метка' автоматом импортирует только системные либы,
; и самописной 'myDll.dll' в таблице импорта сейчас нет!
После запуска этого EXE мы должны увидеть окно приветствия, которое построит функция 'Hello' из внешней библиотеки 'myDll.dll'. Для пущщей уверенности, что нашей либы нет в таблице-импорта исполняемого файла, можно открыть EXE в редакторе 'HIEW' и переключившись по Enter в окно дизассемблера, вызвать просмотр таблицы-импорта IAT по комбинации клавиш: F8-->F9

4
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
26.10.2018, 10:01  [ТС]
DllEntryPoint, как обработчик событий EXE

Выше упоминалось, что при каждом обращении к DLL, управление принимает необязательная функция 'DllEntryPoint', возвращающая EAX=1. Более того, библиотеки реагируют на некоторые события внутри исполняемого файла, поскольку являются его логическим продолжением в адресном пространстве - их связывает невидимая нить, которая работает в фоне.

Событий всего шесть, но их комбинацией можно значительно расширить поле деятельности. Замечу, что имя процедуры не обязательно должно быть 'DllEntryPoint' и может быть произвольным.. главное - передать на неё управление. Вот определение этих событий, которые отправляет приложение на точку-входа в библиотеку:
Code
1
2
3
4
5
6
7
8
9
// DWORD  fdwReason
   #define  DLL_PROCESS_DETACH    0
   #define  DLL_PROCESS_ATTACH    1
   #define  DLL_THREAD_ATTACH     2
   #define  DLL_THREAD_DETACH     3
 
// LPVOID lpReserved
   #define  Dynamic_Call          0
   #define  Static_Call           1
Аргумент lpvReserved [esp+12] указывает на способ подключения DLL - если он равен нулю, библиотека загружена динамически чз LoadLibrary и, соответственно если аргумент =1, наоборот - статически.

Теперь про fdwReason по адресу [esp+8]..

1. DLL_PROCESS_DETACH информирует о том, что DLL выгружается из пространства процесса, потому что счётчик обращений 'LoadCount' в структуре 'PEB_LDR_MODULE' достиг нуля, или обнаружена FreeLibrary().

2. DLL_PROCESS_ATTACH - это флаг загрузки DLL в пространство EXE. Он выставляется в трёх случаях: запуска процесса, первого обращения к функции, или в результате вызова LoadLibrary(). Если происходит повторный запуск какой-нибудь функции из DLL, этот флаг не выставляется, т.к. загрузчик берёт инфу уже из базы РЕВ. В этот момент 'lpReserved' указывает на способ загрузки - статический или динамический.

3. DLL_THREAD_ATTACH - в текущем процессе создаётся новый поток, при этом основной поток процесса этот флаг не выставляет. То-есть LdrLoadDLL() выставляет его только для потоков, созданных после загрузки DLL.

4. DLL_THREAD_DETACH - соответственно, завершение потока.

Пример ниже демонстрирует обработку флагов в самописной библиотеки.
Обратите внимание, что макрос .if определён только для аргумента 'fdwReason', а способ загрузки 'lpvReserved' нужно обрабатывать в ручную, через стек (по крайней мере у меня макрос не срабатывает). Видимо на это влияет то, что этот флаг определяется в манах как зарезервированный:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
format   PE gui dll               ; библиотека
include 'win32ax.inc'
include 'encoding/win1251.inc'
entry    r71mt                    ; имя 'DLLEntryPoint' может быть любым
;---------
.data
capt    db  'Причина вызова DLL ',0
att0    db  'Динамический PROCESS_ATTACH!',0
att1    db  'Статический PROCESS_ATTACH! ',0
detach  db  'Процесс выгрузил DLL - PROCESS_DETACH!',0
;---------
.code
proc   r71mt                             ; Точка-входа!
.if    DLL_PROCESS_ATTACH                ; если это процесс,
       mov     eax,[esp+8]               ; ..и если ксор флага 'fdwReason=1'
       mov     ebx,[esp+12]              ;   ..с флагом 'lpvReserved=???'
       xor     eax,ebx                   ;      ..не взводят ZF (1 xor 0).
       jz      @01
       invoke  MessageBox,0,att0,capt,0  ; значит EBX=0, и это динамика!
       jmp     @00                       ; .endif
@01:   invoke  MessageBox,0,att1,capt,0  ; иначе: EBX=1, т.е. статический вызов.
@00:  .endif
 
.if    DLL_PROCESS_DETACH                ; DETACH не выставляет 'lpvReserved'
       invoke  MessageBox,0,detach,capt,0
      .endif
 
       mov     eax,1      ; возвращаем TRUE ядру.
       ret                ;
endp
;-- Тестовая функция ------
proc   Hello
       nop
       ret
endp
;---------
section '.idata' import data readable
library   user,'user32.dll'
import    user,MessageBox,'MessageBoxA'
;---------
section '.edata' export data readable
export   'myDll.dll', Hello,'Hello'
;---------
section '.reloc' fixups data discardable
4
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
26.10.2018, 15:07  [ТС]
Проблемы загрузки DLL в адресное пространство EXE

Address Space Layout Randomization или коротко 'ASLR' - системный механизм рандомизации адресного пространства. Присутствует по-всех ОС и призван случайным образом подменять расположение в памяти критических данных: EXE/DLL-образов, хипа и т.д. Зачем это нужно и можно-ли обойтись без него?

Проблема заключается в том, что по-умолчанию компиляторы зашивают в образы один и тот-же базовый адрес как для EXE, так и для DLL. Если не принять каких-либо мер, то при старте EXE, он попытается подгрузить библиотеку в аккурат поверх самого-себя, т.к. база в образах указана одинаковая - в дефолте 00400000h. Ясно, что ни к чему/хорошему это не приведёт, и приложение вылетит с ошибкой. Вот детали моих файлов в просмотрщике Тотала по Сtrl+Q:
Code
1
2
3
4
5
FILE TECHNICAL INFO:     myDLL.dll    myEXE.exe
                         ---------    ---------
  Image base             00400000h    00400000h
  Entry Point (RVA)      00002000h    00002000h
  Loader flags           00000000h    00000000h
Здесь и вступает в игру ASLR, который сравнивает базы, и при совпадение подменяет их на-лету. В результате конфликтов нет и всё остаются довольны. ASLR берёт информацию из секции-релоков с таким определением:
section '.reloc' data readable fixups discardable

Если в программе эта секция не определена, то EXE/DLL грузиться по фиксированному адресу 00400000h и операция перемещения образа не производится.

Есть и другой способ решить проблему перемещаемых адресов - в ручную указать базовый адрес загрузки образа. Тогда секция релоков становится не обязательной, и уходит на скамейку запасных. Чтобы сменить дефолтную базу в FASM'e, достаточно в заголовке указать предпочтительный адрес, предварив его оператором at. Только учтите, что 'беззнаковые адреса' выше 80000000h принадлежат системе, и соваться туда не следует.

На скрине - два варианта сборки образа..
На первом рисунке я скомпилировал DLL с настройками по-умолчанию и секцией-релоков. Здесь видно, что ASLR автоматически подменил базу DLL на адрес 00380000h вместо дефолтной 00400000h, хотя для EXE она осталась прежней. На нижнем-же рисунке, я зафиксил базу DLL на 10000000h, и отправил в топку секцию-релоков (закоментировал её) - приложение с такими настройками запускается без проблем. А теперь попробуйте запустить сборку DLL с настройками по-умолчанию и без секции-релоков. Увидите отборный мат системы, поскольку ASLR забьёт на наши проблемы - крах приложения обеспечен!

4
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
30.10.2018, 08:35  [ТС]
Что такое релоки и фиксапы?

В описании РЕ-формата упоминается таблица перемещения адресов Relocation Table. Но по сравнению с описанием таблиц импорта/экспорта и прочих, релокация не избалована вниманием, и остаётся как-то в стороне. Рассмотрим осново/полагающую этой фишки..

По своей природе, код PE-файлов позиционно-зависимый - он компилируется для базового адреса 00400000h. Чтобы получить виртуальные адреса процедур (переменных, меток и переходов), система складывает Base + Offset. Вроде всё ясно, но есть одна проблема - занятая ImageBase! EXE-модули редко сталкиваются с такой проблемой, т.к. они грузятся в свободное вирт.пространство памяти, до загрузки библиотек. Поэтому сказанное ниже относится только к образам DLL, которые подгружаются уже в адресное пространство EXE.

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

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

Посмотрим на дизассемблерный листинг моей DLL, которая проверяет тип запроса на DLL_PROCESS_ATTACH. Здесь видно, что условный переход JE 00402022 FASM скомпилировал в опкоды 74 16, т.е. вставил относительный адрес [+16h]. Однако во-втором случае, в качестве аргумента для MessageBox используется уже абсолютный адрес 00401000, о чём свидетельствует его опкод. Если система и поменяет ImageBase на лету, то опкод она уже не сможет изменить, пока мы не ткнём её носом в эту инструкцию.



Таким образом, таблица-релоков решает проблему перемещаемых адресов, а конкретные адреса в коде назвали 'FixUp': сколько подменяемых адресов - столько и фиксапов в RelocationTable. Вот как выглядит эта таблица в программе PE-Explorer (у меня всего 9-фиксапов в секции-кода), и результат её работы в отладчике OllyDbg (Оля подчёркивает абсолютные адреса). Как видим, после загрузки DLL в память, опкоды аргументов с абсолютными адресами подверглись изменению (база стала 36 вместо 40). По-сути, механизм релокации основан на само/модификации (сама Win легализовала её), чем поспешили воспользоваться хакеры:



Рассмотрим формат таблицы-релоков и каждого из её фиксапов..
Для начала нужно найти таблицу всех секций файла ObjectTable, которая начинается по-смещению РЕ+F8h (указатель на РЕ-заголовок лежит в поле 3Ch от начала файла). В этой таблице плечом-к-плечу выстроены 28h-байтные описатели всех секций. Находим среди них нужный нам описатель .reloc, где по-смещению 14h от начала описателя будет прописан указатель на саму секцию - в данном случае 0х00000С00:
Code
1
2
3
4
5
6
7
8
9
10
  Offs| Size |    Description
  ----|------|----------------------------------
  00h |  8   | Имя секции (остаток забит нулями) 
  08h |  4   | Размер секции в памяти
  0Ch |  4   | RVA-адрес секции в памяти (относительно базы)
  10h |  4   | Размер секции в файле
  14h |  4   | Адрес секции в файле
  18h |  12  | Резерв для OBJ-файла
  24h |  4   | Битовые флаги секции 
  ----+------+----------------------------------


На рисунке выше, 9 моих фиксапов имеют значения: 300F, 3014, 301C, ..etc
Каждый фиксап размером 16-бит и представляет из себя 12-битное смещение от начала блока памяти, и 4-битный тип самого фиксапа. Например первый фикс(300F) кодируется как: тип=3, смещение=00F. Поскольку для смещения веделяется всего 12-бит, то макс.адрес до которого дотягивается фиксап получается 212=4Kb - т.е. одна страница виртуальной памяти. В терминологии релоков, эту страницу назвали "Перемещаемым блоком". Формат таблицы-релоков и его фиксапов представлен ниже:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  Offs| Size |        Name                | Мои значения
  ----|------|----------------------------|--------------
  00h |  4   | RVA-база блока для правки  | 00002000
  04h |  4   | Размер перемещаемого блока | 0000001A
  08h |  2   | FixUp (0)                  | 300F
  0Ah |  2   | FixUp (1)                  | 3014
  --- |  --  | --------                   | ----
  xxh |  2   | FixUp (N)                  | 3048
  ----+------+----------------------------+--------------
 
  FixUp = 300Fh = 0011.000000001111
                  ----|------------
                  Type| Offset (внутри блока)
   
  4-битное поле 'Type' имеет следующие значения:
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  0 (0000) - изменений не производить;
  3 (0011) - прибавить 32-бит Дельту к 32-бит адресу.
4
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
31.10.2018, 10:15  [ТС]
Раздаём атрибуты секциям программы

Рассмотрим определения, благодаря которым секции получают битовые флаги доступа и различные атрибуты. В глобальном масштабе, секции делятся только на код и данные, однако секция-кода (как-правило) присутствует в единственном экземпляре, зато секций-данных в природе - целый зоопарк. Идентификаторы типов могут принимать следующие значения, ..именно по ним выставляются 32-битные флаги атрибутов:
Code
1
2
3
4
5
• code     - секция кода;
• data     - секция данных;
• export   - секция экспорта;
• import   - секция импорта;
• resource - секция ресурсов.
Далее нужно задать имя секции..
Нужно сказать, что FASM имеет зарезервированные имена, которые привязаны к встроенным макросам. Но можно создавать и свои, с произвольными именами макс.8-символов (вместе с точкой). Распространённый список имён выглядит так:
Code
1
2
3
4
5
6
7
8
9
10
• section '.code'  code (атрибуты) - исполняемый код;
• section '.data'  data (атрибуты) - инициал.данные (строки и переменные);
• section '.bss'   data (атрибуты) - неинициал.данные ('0' или '?');
• section '.idata' data (атрибуты) - импорт функций;
• section '.edata' data (атрибуты) - экспорт функций;
• section '.reloc' data (атрибуты) - правка ImageBase;
• section '.rsrc'  data (атрибуты) - ресурсы (иконки, курсоры и пр.);
• section '.tls'   data (атрибуты) - локальная память потока (Thread Local Storage);
• section '.rdata' data (атрибуты) - отладочная информация;
• section '.r71mt' data (атрибуты) - своя секция под барахло.
Обратите внимание, что исполняемой является только секция-кода, которой нужно выставить флаг "executable". Политика безопасности Win рекомендует запрещать в неё запись, чтобы предотвратить модификацию кода 'чёрными силами'. Кроме исполняемой, секциям можно задавать следующие атрибуты, комбинируя их нужным образом:
Code
1
2
3
4
5
6
7
• readable    - секция доступна на чтение;
• writable    - секция доступна на запись;
• executable  - исполняемая секция;
• shareable   - расшаренная секция (вреда от которой больше, чем пользы);
• discardable - при ограниченной физ.памяти, это первый кандидат на выгрузку в своп; 
• notpageable - из физ.памяти секцию сбрасывать нельзя!
• fixups      - авто/коррекция адресов и ошибок.
Под занавес, приведём несколько примеров установки флагов секциям:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; Секция кода с атрибутами R/X (бит(x) от слова eXe).
section '.code' code readable executable
 
; Секция данных с атрибутами R/W.
section '.data' data readable writable
 
; Секция импорта с атрибутом только-чтение.
section '.idata' data import readable
 
; Секция релоков, которая авто-исправляет ошибки,
; и которую можно выгрузить из памяти после старта.
section '.reloc' data fixups discardable
 
; Собственная, открытая на R/W и шару секция,
; которую нельзя выгружать из памяти.
section '.r71mt' data readable writable shareable notpageable
Отмечу, что это не полное собрание атрибутов, а только распространённые..
Полный список можно найти в файле определений "winnt.h" - вот вырезка из него:

Section flags and characteristics
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//
//      IMAGE_SCN_TYPE_REG                   0x00000000  // Reserved.
//      IMAGE_SCN_TYPE_DSECT                 0x00000001  // Reserved.
//      IMAGE_SCN_TYPE_NOLOAD                0x00000002  // Reserved.
//      IMAGE_SCN_TYPE_GROUP                 0x00000004  // Reserved.
#define IMAGE_SCN_TYPE_NO_PAD                0x00000008  // Reserved.
//      IMAGE_SCN_TYPE_COPY                  0x00000010  // Reserved.
 
#define IMAGE_SCN_CNT_CODE                   0x00000020  // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  // Section contains uninitialized data.
 
#define IMAGE_SCN_LNK_OTHER                  0x00000100  // Reserved.
#define IMAGE_SCN_LNK_INFO                   0x00000200  // Section contains comments or some other type of information.
//      IMAGE_SCN_TYPE_OVER                  0x00000400  // Reserved.
#define IMAGE_SCN_LNK_REMOVE                 0x00000800  // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT                 0x00001000  // Section contents comdat.
//                                           0x00002000  // Reserved.
//      IMAGE_SCN_MEM_PROTECTED - Obsolete   0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC          0x00004000  // Reset speculative exceptions handling bits in the TLB entries for this section.
#define IMAGE_SCN_GPREL                      0x00008000  // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA                0x00008000
//      IMAGE_SCN_MEM_SYSHEAP  - Obsolete    0x00010000
#define IMAGE_SCN_MEM_PURGEABLE              0x00020000
#define IMAGE_SCN_MEM_16BIT                  0x00020000
#define IMAGE_SCN_MEM_LOCKED                 0x00040000
#define IMAGE_SCN_MEM_PRELOAD                0x00080000
 
#define IMAGE_SCN_ALIGN_1BYTES               0x00100000  //
#define IMAGE_SCN_ALIGN_2BYTES               0x00200000  //
#define IMAGE_SCN_ALIGN_4BYTES               0x00300000  //
#define IMAGE_SCN_ALIGN_8BYTES               0x00400000  //
#define IMAGE_SCN_ALIGN_16BYTES              0x00500000  // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES              0x00600000  //
#define IMAGE_SCN_ALIGN_64BYTES              0x00700000  //
#define IMAGE_SCN_ALIGN_128BYTES             0x00800000  //
#define IMAGE_SCN_ALIGN_256BYTES             0x00900000  //
#define IMAGE_SCN_ALIGN_512BYTES             0x00A00000  //
#define IMAGE_SCN_ALIGN_1024BYTES            0x00B00000  //
#define IMAGE_SCN_ALIGN_2048BYTES            0x00C00000  //
#define IMAGE_SCN_ALIGN_4096BYTES            0x00D00000  //
#define IMAGE_SCN_ALIGN_8192BYTES            0x00E00000  //
// Unused                                    0x00F00000
#define IMAGE_SCN_ALIGN_MASK                 0x00F00000
 
#define IMAGE_SCN_LNK_NRELOC_OVFL            0x01000000  // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE            0x02000000  // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED             0x04000000  // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED              0x08000000  // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED                 0x10000000  // Section is shareable.
#define IMAGE_SCN_MEM_EXECUTE                0x20000000  // Section is executable.
#define IMAGE_SCN_MEM_READ                   0x40000000  // Section is readable.
#define IMAGE_SCN_MEM_WRITE                  0x80000000  // Section is writeable.


Комбинацию этих флагов показывают дизассемберы, в разделе информации.
Например так они отображаются в "Hacker's Disassembler" - в самом быстром и удобном дизасме, с которым мне приходилось иметь дело:
Code
1
2
3
4
5
6
7
8
9
10
11
File: myDll.DLL
Type: PE
Sections:
 
   Name   | VirtAddr | VirtSize |  Offset  | PhysSize |  Flags   | Code
----------+----------+----------+----------+----------+----------+------
 .data    | 00401000 | 00000075 | 00000400 | 00000200 | C0000040 |  
 .text    | 00402000 | 00000054 | 00000600 | 00000200 | 60000020 |  Yes
 .idata   | 00403000 | 00000052 | 00000800 | 00000200 | 40000040 |  
 .edata   | 00404000 | 00000042 | 00000A00 | 00000200 | 40000040 |  
 .reloc   | 00405000 | 0000001A | 00000C00 | 00000200 | 02000040 |
3
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
31.10.2018, 19:22  [ТС]
Лучший ответ Сообщение было отмечено Mikl___ как решение

Решение

Обмен данными между DLL и EXE

Если библиотека не вещь-в-себе, то должна обмениваться данными с EXE. Судя по гуглу - проблема стоит остро, только на мой взгляд высосана она из пальца. FASM поддерживает транспорт данных в обе стороны, причём без каких-либо ухищрений и танцов с бубнами. Основное условие - чтобы атрибуты секций соответствовали требованиям, т.е. были открыты на R/W.

Гонять инфу между модулями можно разными способами, и все они работают. Например: через регистры, через стек, через указатели, и наконец через MMF - MemoryMappedFile. Прямая передача через регистры - наиболее простой способ, только имеет ограничения в 32/64-бита. Когда нужно отправить какие-нибудь строки или блоки данных, лучше воспользоваться указателями на эти блоки и передавать их удобным программисту способом. В качестве примера рассмотрим такой код..

Имеем библиотеку, которая по запросу должна отправить строку из своей секции-данных, в секцию-данных приложения. Проблема в том, что на этапе компиляции, приложение ещё не знает адрес строки mes0, т.к. эта строка прописана в коде DLL. То-есть приложение не может обратиться к строке напрямую. Поэтому EXE должен запросить указатель на строку у самой DLL. Вот реализация такого алгоритма:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
format   PE gui dll                    ; код DLL'ки..
include 'win32ax.inc'
;---------
.data
mes0     db  'Строка передана из DLL!',0     ; данные, которые хотим отправить в EXE
len      =   $ - mes0                        ; размер этих данных
;---------
.code
start: mov    eax,1                    ; точка-входа в DLL
       ret
;-- Функция обмена данными ------
proc   GetStr arg1                     ; функция с одним аргументом
       mov    edi,[ebp+8]              ; читаем аргумент - это приёмник в секции-данных EXE
       mov    esi,mes0                 ; источник внутри DLL
       mov    ecx,len                  ; размер копируемых данных
       rep    movsb            ;<------; скопировать ECX-байт из ESI в EDI
       ret
endp
;---------
section  '.edata' data export readable
export   'myDll.dll', GetStr,'GetStr'
;---------
section  '.reloc' data fixups discardable
Здесь функция "GetStr" томится в надежде, что EXE отправит ей (в стеке) указатель на приёмник, куда она сможет переслать запрошенные данные. Для всех функций, FASM на входе автоматом выстраивает пролог по типу push ebp --> mov ebp,esp, поэтому аргумент/указатель оказывается по-смещению [ebp+8], оставив за бортом адрес-возврата и сам ebp. Эпилог на выходе восстанавливает стек через leave. Это то, что касается кода библиотеки..

Теперь приложению всего-то нужно вызвать функцию "GetStr",
как данные окажутся у него в кармане, и их можно вывести на экран:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; Код EXE, который принимает строку из DLL
;-----------------------------------------
include 'win32ax.inc'
entry start
;--------------
.data
capt     db   'EXE-модуль',0
buff     db   64 dup(0)                    ; общий буфер, для приёма строки из либы
;--------------
.code
start:   invoke  GetStr, buff               ; передаём указатель в DLL, и принимаем строку
         invoke  MessageBox,0,buff,capt,0   ; выводим строку на экран!
         invoke  ExitProcess,0
;--------------
section '.idata' data import readable
library  user,'user32.dll', kernel,'kernel32.dll', myDll,'myDll.dll'
import   user,MessageBox,'MessageBoxA'
import   kernel,ExitProcess,'ExitProcess'
import   myDll,GetStr,'GetStr'


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

Здесь-же, приложение получило от системы базу 00400000h, а библиотека - базу 00180000h (по крайней мере у меня). При этом обмен происходит удачно, поскольку DLL расценивается как часть EXE и значит адресное пространство у них одно на двоих.
4
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
01.11.2018, 18:52  [ТС]
Memory Mapping File - обмен через файл подкачки

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

Для таких случаев Microsoft предусмотрела вполне легальный способ - это именованные файлы, которые создаются в пространстве файла-подкачки (pagefile.sys), что делает их общими для всех процессов. Поскольку подкачка есть ничто-иное как продолжение PhysicalMemory (физ.памяти), большой Билли назвал такие файлы "Memory Mapping File" - файлы, отображённые в память. Процессы могут получить доступ к MMF тремя способами: по имени, по дескриптору, и по его дубликату. Схема ниже демонстрирует работу этого механизма:



Рассмотрим в общих чертах, откуда берутся и куда исчезают такие файлы..
Основа Win - объекты ядра, т.е. она построена на объектной модели. Диспетчер объектов позволяет создавать и оперировать несколькими типами объектов, в том числе и File-Mapping object. На самом деле каждый объект - это просто блок памяти, доступный только самому ядру. По известным причинам, юзеру не светит прямой доступ к ядерным объектам, для него выделена только директория "Global\", в которой он может создавать расшаренные (или нет) файлы. К каждому файлу привязывается счётчик обращений, и если этот счётчик сбрасывается в нуль, то файл автоматом удаляется. Для просмотра системных объектов можно воспользоваться утилитой WinObj от Sysinternal.

Теперь разработаем план действий..
Поскольку файла ещё нет, значит его нужно создать функцией CreateFileMapping(). Не важно кто будет этим заниматься, главное что-бы он опубликовал всем имя этого файла, ..ну или его дескриптор. Суть в том, что "один" создаёт - а "общество" пользуется этим файлом. В общем случае, при публикации последовательность функций такая:
  1. CreateFileMapping() - создать файл с дескриптором (-1) =Invalid;
  2. MapViewOfFile() - отобразить его в памяти;
  3. CopyMemory() - записать в него общую информацию;
  4. UnmapViewOfFile() - освободить от него память;
  5. CloseHandle() - закрыть дескриптор.

С этого момента любой процесс может прочитать этот файл по имени, или наоборот записать в него информацию, если при создании это было предусмотрено флагами защиты "CreateFileMapping()". В свою очередь тот-кто хочет прочитать этот файл, должен проделать эти-же операции, только в первом пункте не создать, а открыть его чз OpenFileMapping().
3
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
05.12.2018, 21:54  [ТС]
Лучший ответ Сообщение было отмечено Mikl___ как решение

Решение

TLS-CallBack, как обратный вызов загрузчика образов

Win имеет довольно интересный тип памяти с аббревиатурой TLS, которая плохо освящена в доках. В дословном переводе, это Thread Local Storage - локальная память отдельно взятого потока. Кстати в узких кругах, потоки называют "тредами" (thread), чтобы исключить путаницы с понятием "процесс".

Процессы 64-битных систем могут порождать огромное кол-во тредов, в то время как в х32 их число не превышает 2000 единиц - потолок диктует размер юзерской кучи (heap). На системах х32 куча застыла на отметке 2Gb (ст.половину из 4-х система забирает себе). Если известно, что дефолтный стек процесса =1Mb, а всего памяти 2Gb, то тредов на процесс получаем ~2000 штук, и все/они выполняются в контексте родительского процесса. Напомню, что это только один процесс, а в реал-тайме - их десятки.

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

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

Мало того, TLS поддерживает фишку под названием Tls-Callback - это то, на чём нужно заострить своё внимание в первую очередь! Системный загрузчик образов вызывает функции обратного вызова (!)до точки входа в нашу программу, в результате чего отладчики с дефолтными опциями пропускают колбэки между ног - они дебажат именно с EntryPoint. Значит внутри callback-функции мы можем обнаружить платформу, и принять соответствующие меры. Нужно сказать, что Win сама предлагает нам это, а значит все/наши действия вполне легальны.


Системная поддержка памяти TLS

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

Обзор начнём с динамической TLS, которая физически расположена в структуре TEB треда. ТЕВ - это конспиративная квартира потока и представляет из себя его инфо-базу "Thread Environment Block". У каждого треда свой ТЕВ, а значит и своя TLS-память. 32-битная система хранит указатель на TEB в сегментном регистре(FS), а системы х64 - в регистре(GS). Посмотрим на поля этой структуры (в первом столбце указано смещение от начала):
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
=== TEB (фрагмент структуры) =============================
==========================================================
    02C   00 00 00 00                   TlsPointer             = 0
    030   00 C0 FD 7F                   PEB                    = 7FFDC000
    E10                                 TlsSlots
     |
     +-- 00 00 00 00 00 00 00 00 | 00 00 00 00 30 3C 89 00
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
         00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     = Всего: 256-байт.
 
   F10   00 00 00 00                    TlsLinks.FLink         = 0
   F14   00 00 00 00                    TlsLinks.BLink         = 0
   F94   00 00 00 00                    TlsExpansionSlots      = NULL
Здесь, по смещению fs:[0e10h] видим поле "TlsSlots" - это и есть локальная память треда, в которой он может хранить свои переменные и указатели размером dword. Так-же, по смещению fs:[0f94h] имеется поле "TlsExpansionSlots" - физ.расширение основной TLS, которое в данном случае находится под флагом null, т.е. не используется и находится в резерве.

Динамическая TLS устроена таким образом, что оперировать с ней можно только 4-байтными словами. Каждое/такое слово представляет из себя TLS-ячейку, а место хранения ячеек назвали "слотами". Для адресации слотов применяют индексы, и если основной памяти в байтах всего 256, то получаем общее кол-во слотов: 256/4=64, и соответственно столько-же индексов (отсчёт с нуля).

Когда тред забьёт все/64 слота основной памяти, система на автопилоте расширит память, и следующий индекс отправится уже в массив "TlsExpansionSlots". Согласно документации, в этом массиве имеется резерв ещё для 1024-слотов. От сюда следует, что каждому из тредов система выделяет: 64+1024=1088 слотов TLS, что чуть больше одной (4К-байтной) страницы виртуальной памяти. Используете-ли вы эту память в своих программах? - большой вопрос.

Ладно.. будем считать, что с локальной памятью треда разобрались..
Но как на счёт её контроля со-стороны системы и родительского процесса?
Ясно, что если треды принадлежат процессу, то и управлять их памятью должен тоже процесс, для чего в его структуре PEB имеются соответствующие поля. Точно так-же как и ТЕВ, PEB является структурой, только не треда, а родительского процесса - Process Environment Block. Это довольно содержательная база, из которой можно почерпнуть не мало интересного.

Как ни странно, но указатель на РЕВ находится в ТЕВ, а точнее в его поле fs:[30h] под ником "РЕВ" (см.фрагмент teb выше). Здесь рулит оригинальное решение с определением "BitMap" - битовая карта TLS-индексов.

Посмотрим на структуру PEB ниже..
Суть в том, что к каждому слоту в структуре TEB, привязан соответствующий бит в битовой карте РЕВ. Состояние слотов основной памяти TLS отражает 8-байтное поле TlsBitmapBits по смещению [fs:30h]+44h. 8-байт это в аккурат 64-бита, чего хватает для определения флагов состояния 64 слотов: (0)=чисто, (1)=используется.
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
=== PEB (фрагмент структуры)===============================
===========================================================
    03C   00 00 00 00                   TlsExpansionCounter    = 0
    040   E0 05 98 7C                   TlsBitmap              = 7C9805E0
    044   00 00 00 00 00 00 00 00       TlsBitmapBits          = 0
    150   D8 05 98 7C                   TlsExpansionBitmap     = 7C9805D8
    154                                 TlsExpansionBitmapBits 
     |
     +--- 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
          00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
          00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
          00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
          00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
          00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
          00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
          00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00    = 1024 bits clear
Для расширенных слотов свыше 64, используется карта "TlsExpansionBitmapBits", дамп которого показан выше. Если в одном параграфе 128-бит (16-байт), то весь массив получается 1024. Опять-же получаем контроль над: 64+1024=1088 слотами памяти TLS.

Для динамического выделения локальной памяти, система предоставляет тредам всего 4 функции. Поскольку они оперируют только индексами, тред не может работать с отдельными байтами TLS - мин.порцией является DWORD'ный слот. Вот эти функции:
Code
1
2
3
4
  TlsAlloc() - возвращает индекс очередного/свободного слота в TEB;
  TlsSetValue(index,value) - записывает dword в этот слот;
  TlsGetValue(index) - читает в EAX значение из указанного слота;
  TlsFree(index) - очищает указанный слот в TEB (и сбрасывает бит в РЕВ).
Вот собственно и всё, что я хотел сказать о динамической TLS.
Кого заинтересует эта фишка, советую почитать Д.Рихтера "Win для профессионалов", где этой теме посвящена целая глава.

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

Добавлено через 3 минуты
Полное описание структуры РЕВ процесса
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
AllocationBase:  0x7FFDA000
Type:            Private
Shared:          False
Access:          RW
Shared count:    0
RegionSize:      0x1000 (4096 byte)
Block size:      0x020C (524  byte)
 
-------------------- Process Environment Block (x32) -----------------------------
----- PEB ------------------------------------------------------------------------
7FFDA_000  00                               InheritedAddressSpace = 0
7FFDA_001  00                               ReadImageFileExecOptions = 0  ; запуск файла
7FFDA_002  00                               BeingDebugged = 0
7FFDA_003  00                               BitField [] = 0
7FFDA_004  FF FF FF FF                      Mutant = FFFFFFFF
7FFDA_008  00 00 40 00                      ImageBaseAddress = 400000
7FFDA_00C  90 1E 24 00                      LoaderData = 241E90
7FFDA_010  00 00 02 00                      ProcessParameters = 20000
7FFDA_014  00 00 00 00                      SubSystemData = 0
7FFDA_018  00 00 14 00                      ProcessHeap = 140000
7FFDA_01C  20 06 98 7C                      FastPebLock = 7C980620
7FFDA_020  00 10 90 7C                      FastPebLockRoutine = 7C901000
7FFDA_024  E0 10 90 7C                      FastPebUnlockRoutine = 7C9010E0
7FFDA_028  02 00 00 00                      EnvironmentUpdateCount = 2
7FFDA_02C  70 29 36 7E                      KernelCallbackTable = 7E362970
7FFDA_030  00 00 00 00                      SystemReserved = 0
7FFDA_034  00 00 00 00                      AtlThunkSListPtr32 = 0
7FFDA_038  00 00 00 00                      ApiSetMap = 0
7FFDA_03C  00 00 00 00                   TlsExpansionCounter = 0
7FFDA_040  E0 05 98 7C                   TlsBitmap = 7C9805E0
7FFDA_044  FF FF 00 00 00 00 00 00       TlsBitmapBits = 000000000000FFFF
7FFDA_04C  00 00 6F 7F                      ReadOnlySharedMemoryBase = 7F6F0000
7FFDA_050  00 00 6F 7F                      ReadOnlySharedMemoryHeap = 7F6F0000
7FFDA_054  88 06 6F 7F                      ReadOnlyStaticServerData = [7F6F0688] "NULL"
7FFDA_058  00 00 FB 7F                      AnsiCodePageData = 7FFB0000
7FFDA_05C  00 10 FC 7F                      OemCodePageData = 7FFC1000
7FFDA_060  00 20 FD 7F                      UnicodeCaseTableData = 7FFD2000
7FFDA_064  01 00 00 00                      KeNumberOfProcessors = 1
7FFDA_068  00 00 00 00                      NtGlobalFlag = 0
7FFDA_06C  00 00 00 00                      Spare
7FFDA_070  00 80 9B 07 6D E8 FF FF          CriticalSectionTimeout = FFFFE86D079B8000
7FFDA_078  00 00 10 00                      HeapSegmentReserve = 100000
7FFDA_07C  00 20 00 00                      HeapSegmentCommit = 2000
7FFDA_080  00 00 01 00                      HeapDeCommitTotalFreeThreshold = 10000
7FFDA_084  00 10 00 00                      HeapDeCommitFreeBlockThreshold = 1000
7FFDA_088  08 00 00 00                      NumberOfHeaps = 8
7FFDA_08C  10 00 00 00                      MaximumNumberOfHeaps = 10
7FFDA_090  E0 FF 97 7C                      ProcessHeaps = [7C97FFE0] "140000"
7FFDA_094  00 00 46 00                      GdiSharedHandleTable = 460000
7FFDA_098  00 00 00 00                      ProcessStarterHelper = 0
7FFDA_09C  14 00 00 00                      GdiDCAttributeList = 14
7FFDA_0A0  74 E1 97 7C                      LoaderLock = 7C97E174
7FFDA_0A4  05 00 00 00                      NtMajorVersion = 5
7FFDA_0A8  01 00 00 00                      NtMinorVersion = 1
7FFDA_0AC  28 0A                            NtBuildNumber = A28
7FFDA_0AE  00 03                            NtCSDVersion = 300
7FFDA_0B0  02 00 00 00                      PlatformId = 2
7FFDA_0B4  02 00 00 00                      Subsystem = 2
7FFDA_0B8  04 00 00 00                      MajorSubsystemVersion = 4
7FFDA_0BC  00 00 00 00                      MinorSubsystemVersion = 0
7FFDA_0C0  00 00 00 00                      AffinityMask = 0
----------------------------------------------------------------------------------
7FFDA_0C4  00 00 00 00 02 00 00 00 | 00 00 00 00 01 00 00 00     GdiHandleBuffer
7FFDA_0D4  00 00 00 00 E2 03 10 07 | E0 03 10 07 00 00 00 00 
7FFDA_0E4  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_0F4  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_104  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_114  00 00 00 00 00 00 00 00 | 00 00 00 00 C2 03 04 8C 
7FFDA_124  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_134  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_144  00 00 00 00 00 00 00 00 
----------------------------------------------------------------------------------
7FFDA_14C  00 00 00 00                      PostProcessInitRoutine = 0
7FFDA_150  D8 05 98 7C                      TlsExpansionBitmap = 7C9805D8
----------------------------------------------------------------------------------
7FFDA_154  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     TlsExpansionBitmapBits
7FFDA_164  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_174  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_184  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_194  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_1A4  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_1B4  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDA_1C4  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
----------------------------------------------------------------------------------
7FFDA_1D4  00 00 00 00                      SessionId = 0
7FFDA_1D8  00 00 00 00 00 00 00 00          AppCompatFlags = 0
7FFDA_1E0  00 00 00 00 00 00 00 00          AppCompatFlagsUser = 0
7FFDA_1E8  00 00 00 00                      pShimData = 0
7FFDA_1EC  00 00 00 00                      AppCompatInfo = 0
7FFDA_1F0  1C 00 1E 00 C2 06 6F 7F          CSDVersion = [7F6F06C2] "Service Pack 3"
7FFDA_1F8  00 00 00 00                      ActivationContextData = 0
7FFDA_1FC  00 00 00 00                      ProcessAssemblyStorageMap = 0
7FFDA_200  00 00 13 00                      SystemDefaultActivationContextData = 130000
7FFDA_204  00 00 00 00                      SystemAssemblyStorageMap = 0
7FFDA_208  00 00 00 00                      MinimumStackCommit = 0


Полное описание структуры ТЕВ отдельного треда
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
AllocationBase: 7FFDD000
RegionSize:     4 K
Type:           Private
Access:         RW
Shared:         False
Shared count:   0
=========================== NT_TIB32 ===========================
===== TEB ======================================================
7FFDD_000  DC FF F0 00                      ExceptionList = F0FFDC
7FFDD_004  00 00 F1 00                      StackBase = F10000
7FFDD_008  00 F0 F0 00                      StackLimit = F0F000
7FFDD_00C  00 00 00 00                      SubSystemTib = 0
7FFDD_010  00 1E 00 00                      FiberData = 1E00
7FFDD_014  00 00 00 00                      ArbitraryUserPointer = 0
7FFDD_018  00 D0 FD 7F                      Self = 7FFDD_000
--------------------------- TEB32 ------------------------------
7FFDD_01C  00 00 00 00                      EnvironmentPointer = 0
7FFDD_020  04 07 00 00                      ClientId.UniqueProcess = 704
7FFDD_024  B4 0C 00 00                      ClientId.UniqueThread = CB4
7FFDD_028  00 00 00 00                      ActiveRpcHandle = 0
7FFDD_02C  00 00 00 00                      TlsPointer = 0
7FFDD_030  00 C0 FD 7F                      ProcessEnvironmentBlock = 7FFDC000
7FFDD_034  00 00 00 00                      LastErrorValue = 0
7FFDD_038  00 00 00 00                      CountOfOwnedCriticalSections = 0
7FFDD_03C  00 00 00 00                      CsrClientThread = 0
7FFDD_040  00 00 00 00                      Win32ThreadInfo = 0
7FFDD_044  ------128  dup(00)               User32Reserved
7FFDD_0C0  00 00 00 00                      WOW32Reserved = 0
7FFDD_0C4  19 04 00 00                      CurrentLocale = 419
7FFDD_0C8  00 00 00 00                      FpSoftwareStatusRegister = 0
7FFDD_0CC  ------216  dup(00)               SystemReserved1
7FFDD_1A4  00 00 00 00                      ExceptionCode = 0
7FFDD_1A8  00 00 00 00                      ActivationContextStack.Flags = 0
7FFDD_1AC  01 00 00 00                      ActivationContextStack.NextCookieSequenceNumber = 1
7FFDD_1B0  00 00 00 00                      ActivationContextStack.ActiveFrame = 0
7FFDD_1B4  B4 D1 FD 7F                      ActivationContextStack.FrameListCache.FLink = 7FFDD_1B4
7FFDD_1B8  B4 D1 FD 7F                      ActivationContextStack.FrameListCache.BLink = 7FFDD_1B4
7FFDD_1BC  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     SpareBytes
7FFDD_1CC  00 00 00 00 00 00 00 00 
7FFDD_1D4  00 00 00 00                      GdiTebBatch.Offset = 0
7FFDD_1D8  00 00 00 00                      GdiTebBatch.HDC = 0
7FFDD_1DC  ------1240 dup(00)               GdiTebBatch.Buffer
7FFDD_6B4  04 07 00 00                      RealClientId.UniqueProcess = 704
7FFDD_6B8  B4 0C 00 00                      RealClientId.UniqueThread = CB4
7FFDD_6BC  00 00 00 00                      GdiCachedProcessHandle = 0
7FFDD_6C0  00 00 00 00                      GdiClientPID = 0
7FFDD_6C4  00 00 00 00                      GdiClientTID = 0
7FFDD_6C8  00 00 00 00                      GdiThreadLocalInfo = 0
7FFDD_6CC  ------248  dup(00)               Win32ClientInfo
7FFDD_7C4  ------932  dup(00)               glDispatchTable
7FFDD_B68  ------116  dup(00)               glReserved1
7FFDD_BDC  00 00 00 00                      glReserved2 = 0
7FFDD_BE0  00 00 00 00                      glSectionInfo = 0
7FFDD_BE4  00 00 00 00                      glSection = 0
7FFDD_BE8  00 00 00 00                      glTable = 0
7FFDD_BEC  00 00 00 00                      glCurrentRC = 0
7FFDD_BF0  00 00 00 00                      glContext = 0
7FFDD_BF4  00 00 00 00                      LastStatusValue = 0
7FFDD_BF8  00 00 0A 02 00 DC FD 7F          StaticUnicodeString = [7FFDD_C00] ""
7FFDD_C00  ------524  dup(00)               StaticUnicodeBuffer
7FFDD_E0C  00 00 E1 00                      DeallocationStack = E10000
7FFDD_E10  ------256  dup(00)               TlsSlots
7FFDD_F10  00 00 00 00                      TlsLinks.FLink = 0
7FFDD_F14  00 00 00 00                      TlsLinks.BLink = 0
7FFDD_F18  00 00 00 00                      Vdm = 0
7FFDD_F1C  A0 97 15 00                      ReservedForNtRpc = 1597A0
7FFDD_F20  00 00 00 00 00 00 00 00          DbgSsReserved = 0
7FFDD_F28  00 00 00 00                      HardErrorsAreDisabled = 0
 
7FFDD_F2C  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     Instrumentation
7FFDD_F3C  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDD_F4C  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
7FFDD_F5C  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00 
 
7FFDD_F6C  00 00 00 00                      WinSockData = 0
7FFDD_F70  00 00 00 00                      GdiBatchCount = 0
7FFDD_F74  00                               InDbgPrint = 0
7FFDD_F75  00                               FreeStackOnTermination = 0
7FFDD_F76  00                               HasFiberData = 0
7FFDD_F77  00                               IdealProcessor = 0
7FFDD_F78  00 00 00 00                      Spare3 = 0
7FFDD_F7C  00 00 00 00                      ReservedForPerf = 0
7FFDD_F80  00 00 00 00                      ReservedForOle = 0
7FFDD_F84  00 00 00 00                      WaitingOnLoaderLock = 0
7FFDD_F88  00 00 00 00                      Wx86Thread.CallBx86Eip = 0
7FFDD_F8C  00 00 00 00                      Wx86Thread.DeallocationCpu = 0
7FFDD_F90  00                               Wx86Thread.UseKnownWx86Dll = 0
7FFDD_F91  00                               Wx86Thread.OleStubInvoked = 0
7FFDD_F92  00 00                            Spare
7FFDD_F94  00 00 00 00                      TlsExpansionSlots = [NULL] "NULL"
7FFDD_F98  00 00 00 00                      ImpersonationLocale = 0
7FFDD_F9C  00 00 00 00                      IsImpersonating = 0
7FFDD_FA0  00 00 00 00                      NlsCache = 0
7FFDD_FA4  00 00 00 00                      pShimData = 0
7FFDD_FA8  00 00 00 00                      HeapVirtualAffinity = 0
7FFDD_FAC  00 00 00 00                      CurrentTransactionHandle = 0
7FFDD_FB0  00 00 00 00                      ActiveFrame = 0
7FFDD_FB4  00                               SafeThunkCall = 0
7FFDD_FB5  00 00 00                         BooleanSpare
---------------------------- Memory dump --------------------------
7FFDD_FB8  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     ................
7FFDD_FC8  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     ................
7FFDD_FD8  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     ................
7FFDD_FE8  00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00     ................
7FFDD_FF8  00 00 00 00 00 00 00 00                               ........
2
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
06.12.2018, 20:43  [ТС]
Статическая TLS - вид сверху

Для разбора статический памяти тредов нужно сменить тактику и переключиться на другую волну. Ключ к пониманию её принципов лежит в спецификации на РЕ-формат, а точнее лишь в описании её раздела Image_Data_Directory. Этот дир смещён на 78h от начала РЕ-заголовка и хранит RVA имеющихся в файле секций, с указанием размеров каждой из них. Размер - игнорируемый всеми пережиток прошлого.

Для наглядности, возьмём обычный "Hello World" и открыв его в HIEW посмотрим на эту "DataDirectory" (меню F8->F10). Я чуть модифицировал скрин, добавив смещения записей от начала PE-Header (слева), и их порядковые номера (справа). Структура до неприличия проста: первый dword - это RVA на таблицу соответствующей секции в памяти, второй - её размер:


Загрузчик, начиная с нуля по-очереди сканирует поле RVA всех записей и если оно не нуль, подгружает указанную секцию в виртуальную память VA (вычисляется по формуле: VA=ImageBase+RVA). Таким образом проверка начинается с секции экспорта(0), потом импорт(1), и.. программа фактически может исполнять уже любой код - ей выделена память и импортированы все функции из внешних модулей DLL.

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

Здесь, загрузчик приступает к вызову цепочки Tls-Callback'ов, которые (судя по названию) должны что-то возвращать загрузчику (back), но на самом-деле ему глубоко безразлично, чем будет заниматься вызванная им функция - он даже не проверяет возвращаемые ею параметры, и вызывает следующий колбэк в цепочке (если таковой имеется). Только после этого управление передаётся на ЕР в программу, с которой начинают анализ все отладчики, а значит мы его в фоне сможем обнаружить.

От сюда следует, что пока мы не заполним поле под номером(9) в "ImageDataDirectory", не видать нам долгожданных Callback'ов как своих ушей. Сделать это просто - достаточно явно указать номер секции в коде, и компилятор сам позаботится об остальном. Кроме того нужно и секцию оформить должным образом (она имеет свой прототип), иначе загрузчик тупо проигнорирует наш план. Вот пример готовой секции TLS с описанием прототипа:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
data  9
      dd  @tls, @tls          ; указатель на начало/конец лок.памяти треда
      dd  @tls                ; указатель на индекс треда
      dd  cb0                 ; указатель на начало Callback цепочки
;**************** Статическая память треда **********************************
@tls  dd  0                   ; будет индексом треда (заполняет загрузчик)  *
cb0   dd  callback_0          ; цепочка указателей на Callback-функции      *
cb1   dd  callback_1          ;  ...                                        *
cbN   dd  callback_N          ;    ...                                      *
      dd  0                   ; маркер окончания цепочки!                   *
;****************************************************************************
end   data
Приведу небольшие пояснения..
Всё-что находится внутри блока - это и есть статическая TLS тредов. Она является общей для всех потоков и не может расширяться динамически, как это было с полем "TlsExpansionSlots" в структуре ТЕВ. Этому есть простое объяснение - статическая TLS выделяется явно, на этапе компиляции программы.

Данный блок можно переместить в любую секцию где есть доступ на запись, например в секцию-данных или специально предназначенную для этого - секцию '.tls'. Если ваш код многопоточный и создаёт ещё тредов (при этом вы хотите использовать для них статическую TLS), то должны зарезервировать TLS отдельно для каждого треда, внутри общей памяти.

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

Осталось рассмотреть, что находится снаружи блока..
Там 4 двойных-слова, и все/они указатели. Первые два - это линки на голову и хвост локальной памяти. Хвост никто не проверяет, поэтому туда можно пихать что-угодно (в данном случае я их сравнял). Эти указатели не используют переменную(@tls), а линки нужны лишь для того, чтобы компилятор сгенерил адрес. Кто использует эту переменную, так это загрузчик, записывая туда TLS-индекс обнаруженного треда. Ну и четвёртый dword - это указатель на колбэк-цепочку. Загрузчик будет вызывать по-очереди каждую ссылку из цепочки, пока не встретит нуль-терминальный dword.
2
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
08.12.2018, 11:47  [ТС]
Обратные вызовы TLS-Callbacks

Колбэки – это цепочка функций, которые загрузчик вызывает до точки-входа в программу. Девятая запись на скрине выше, указывает на набор этих функций. Загрузчик вызывает второй колбэк в цепочке только когда получит ret от первого. В сети встречаются предположения, что цепочку можно расширять бесконечно, но в доках имеется ограничение на 8-штук. Код колбэков выполняются внутри кода загрузчика, а лоадер подменяет SEH заглушкой, поэтому любые исключения на этом этапе игнорируются системой.

Функция "TlsCallback" имеет свой прототип, который ни к чему нас не обязывает. Это точная ксерокопия функции DllEntryPoint(), которая проверяет флаг "reason" на ATTACH/DETACH процессов и тредов - её мы уже подвергли трепанации, поэтому повторяться не будем: Tls_Callback (handle, reason, reserved).

Пришла пора по-практиковаться..
Модифицируем обычный "HelloWorld", подключив к нему колбеки..
По-сути это приём антиотладки. Второй колбек - чисто для демонстрации вызова цепочки. Для обозначения своего присутствия, он тупо боксит мессагу и всё. Замечу, что первый не возвращает EAX=1 загрузчику, на чём так настаивает мелкософт. При инициализации DLL этот выхлоп может-быть и нужен, но в случае с EXE - им можно пренебречь.

На функцию первого колбека возложена задача по разоблачению присутствия отладчика. Обнаружить Олю и её братию можно разными способами. Нам даже не нужно напрягаться, т.к. Win вычисляет отладку сама, выставляя флаги в структуре PEB - просто читаем их и делаем вывод. Первое поле: PEB.02.BeingDebugged выставляется системой в 1, а второе PEB.68h.NtGlobalFlag - это OR трёх/глобальных флагов системы, которые под отладчиком будут иметь значение 70h. Я выбрал первый вариант:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
format   pe gui
include  'win32ax.inc'
.data
mesWin0  db  'Проверь код в отладчике!',0
mesWin1  db  'Сработал второй Callback в цепочке!',0
mesDbg   db  'Внимание! Обнаружен отладчик!',13,10
         db  'Отладка данного приложения невозможна.',0 
;------
.code
start:   invoke  MessageBox,0,mesWin0,0,0       ; увидим, если процесс не отлаживается
         invoke  ExitProcess,0                  ; конец основной программы!
 
;------ Тут идёт цепочка колбеков -----------
; первый ---------------------
proc  FindDbg
         mov     eax,[fs:30h]                ; указатель на начало РЕВ
         cmp     byte[eax+2],0               ; третий байт в РЕВ - это флаг "BeingDebugged"
         jz      @f                          ; если он нуль, то нет отладчика
         invoke  MessageBox,0,mesDbg,0,0     ; иначе: мессага,
         jmp     $                           ;    ..и виснем внутри загрузчика.
@@:      ret                                 ; если отладчика нет, то тихо выходим из колбека.
endp
; второй колбек в цепочке ----
proc  callback2                              ; он сработает до метки 'START',
         invoke  MessageBox,0,mesWin1,0,0    ;   ..поэтому под виндой увидим сначала вторую,
         ret                                 ;      ..а потом первую мессагу из секции-данных.
endp
.end start                                   ; конец секции кода
;--------------------------------------------
; TLS таблица (и в ней-же статическая память)
data  9
         dd  tls, tls, tls, cbLink           ; указатели 
tls      dd  0                               ; место под индекс
cbLink   dd  FindDbg, callback2              ; ссылки на цепочку колбеков
         dd  0                               ; маркер окончания цепочки
end   data
Попробуйте собрать и вскормить этот пример отладчику..
Если запустить его в обычном режиме, то увидим две мессаги. Но если приложение распознает агрессивную среду отладки, то выдаст мессагу и тупо встанет как-вкопанная, не отобразив в окне дебагера ни одного тушканчика. "Достучаться до небес" конечно-же можно, но начинающих исследователей это сбивает с толку не по-детски.

Добавлено через 40 минут
Кстати вот так можно вынести TLS-память за пределы таблицы, в спец-секцию .tls
Assembler
1
2
3
4
5
6
7
8
9
10
11
;-----------------------------
; TLS таблица ----------------
data 9
         dd  tls, tls, tls, cbLink
tls      dd  0                    ; место под индекс треда
end  data
;-----------------------------
; статическая память треда ---
section '.tls' data readable writable
cbLink   dd  FindDbg, callback2   ; TLS-цепочка
         dd  0
Только в этом случае секция будет выдавать себя с потрахами в дизассемблерах.
Вот логи дизасма для обоих случаев:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Disassembly of File: BUG.EXE
Code Offset = 00000200, Code Size = 00000200
Data Offset = 00000200, Data Size = 00000200
 
Number of Objects = 0003 (dec), Imagebase = 00400000h
; для первого примера -------------------------------
   Object01: .data    RVA: 00001000 Offset: 00000200 Size: 00000200 Flags: C0000040
   Object02: .text    RVA: 00002000 Offset: 00000400 Size: 00000200 Flags: 60000020
   Object03: .idata   RVA: 00003000 Offset: 00000600 Size: 00000200 Flags: C0000040
 
; для второго варианта ------------------------------
   Object01: .data    RVA: 00001000 Offset: 00000400 Size: 00000200 Flags: C0000040
   Object02: .text    RVA: 00002000 Offset: 00000600 Size: 00000200 Flags: 60000020
   Object03: .idata   RVA: 00003000 Offset: 00000800 Size: 00000200 Flags: C0000040
   Object04: .tls     RVA: 00004000 Offset: 00000A00 Size: 00000200 Flags: C0000040
Чтобы карта секций сильно не бросалась в глаза, можно вообще запихать все секции в одну/безымянную, тогда FASM определяет её как '.flat', но это уже на любителя, и придётся открывать код на запись, т.к. загрузчик записывает в TLS уникальный индекс стартуемого треда.
2
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
23.01.2019, 15:27  [ТС]
Hook виндовых API

Рассмотрим тему, за которую недавно сожгли-бы на костре, а сегодня - это легальный способ получить управление при вызовах API, который, кстати, в красках расписывается на RSDN например здесь: http://rsdn.ru/article/qna/baseserv/hookapi.xml. Речь пойдёт о перехвате API-функций, а всю/черновую работу можно возложить на TLS-callback, который проделает это в фоне отладчика.

Вспомним алгоритм перехвата прерываний в среде MS-DOS..
Имеется таблица векторов прерываний(IVT) Interrupt Vector Table, эдакая штаб-квартира адресов в обработчики всех сервисов DOS. Если подменить вектор на адрес своей процедуры, то получаем управление до оригинального входа, что и представляет из-себя собственно перехват.

С приходом Win алгоритм не изменился, зато изменился способ его реализации. Тут уже нет глобальной таблицы векторов, а обработчики в виде API-функции хранятся внутри системных библиотек, например kernel32.dll и прочие. Если в двух словах, то процесс отображения функций в пространство юзера происходит примерно так..

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

• name = User32.dll;
• offs = 0x77665544;
• func = MessageBox().

Однако компилятор этого не делает, и оставляет (в исполняемом файле) поле с адресом пустым. В спецификации на РЕ-формат это поле называют IAT - Import Address Table. Это вынужденная мера, чтобы файл можно было запускать на любой ОС, т.к. библиотеки у них загружаются по-разным адресам ядерного пространства, и если сделать адрес статичным, то можно промахнуться мимо функции.



Поэтому IAT экзешника заполняется загрузчиком РЕ-файлов уже после того, как он загрузит файл в память. Загрузчик сканирует таблицу-импорта файла, и для каждой из прописанных в ней функций вызывает двойку: LoadLibrary() и GetProcAddress(), на основании выхлопа которых и заполняет IAT. Первая fn.запрашивает у системы адрес библиотеки, а вторая - смещение нужной API-функции внутри этой "матки".

Если-же на момент вызова LoadLibrary() нужной библиотеки в системной памяти не окажется, то либа динамически подгружается в память только вызывающего процесса, а не системы в целом. Но большинство из библиотек сидят в автозагрузке и уже болтаются в пространстве ядра. Если память библиотеки не расшарена флагом "shareable", то система копирует в пространство юзера код импортируемой API из своей области. Для этих целей выделена верхняя часть адресов юзера, ниже 0x7FFFFFFF (регион 32-битной памяти выше 0х80000000 принадлежит ОС).

В противном случае и при расшаренной секции-экспорта библиотеки DLL, система уже не копирует код-функции юзеру, а выдаёт ему прямую ссылку на свою/ядерную память, что является огромной брешью в системе безопасности. В многопроцессорных системах планировщик если и ставит все потоки в кольцевую очередь, то только на одном из процессоров - второе его ядро абсолютно самостоятельно со-своим планировщиком, и работает параллельно первому. То-есть многопоточность не эмулируется, и в любой момент оба ядра могут затребовать одну API-функцию. Значит нужны аппаратные механизмы синхронизации, поддерживать которые дороже, чем раздавать каждому процессу по своей копии API.
2
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
23.01.2019, 15:31  [ТС]
Таким образом, алгоритмы перехвата API в среде Windows можно разделить на два/основных типа: первый - это подмена адреса в таблице IAT исполняемого файла, а второй - это вставка перехода непосредственно в начало функции-обработчика, что известно как "сплайсинг".

К недостатку первого метода можно отнести тот факт, что если API вызывается из кода динамически чз LoadLibrary(), то адрес функции запрашивается напрямую у системы, и его подмена в таблице IAT нашего контекста профита не принесёт. Тут нужно перехватывать уже (стоящую ниже уровнем) недокументированную функцию LdrLoadDll(), что сделать не так-уж просто, т.к. память ядра не доступна на запись из юзера.

Эту проблему решает сплайсинг - тупая врезка перехода в начало обработчика. Чтобы закодировать переход на функцию-двойник, достаточно всего 5-ти байт опкодов: 1-байт для инструкции(jmp), и 4-байта на сам адрес перехода (оригинальные 5-байт нужно запомнить в своём теле, и восстанавливать их после перехвата).

Остановимся на первом варианте с IAT, и попробуем перезаписать адрес именно в контексте нашего процесса. Отлавливать будем MessageBox(), до передачи управления которой расшифруем выводимую строку (предварительно зашифровав её). Это позволит нам скрыть в секции-данных все строки от жадных взоров кодокопателей. Возможный план действий описан ниже:

1. Получить через GetProcAddress(), оригинальный адрес функции MessageBox();
2. Сохранить его в своей переменной для возврата управления;
3. Найти IAT в памяти, и подменить в нём адрес MessageBox() на свой адрес;
4. Как-только получим управление, снять со-стека аргумент "адрес-строки" и расшифровать её до терминального нуля.

Первые/два пункта не представляют из-себя проблемы, а вот третий - нужно рассмотреть подробней, для чего откроем спеку на РЕ-формат, и перейдём в её раздел ".idata". Дела с таблицей-импорта в РЕ-заголовке обстоят так..

• Поскольку мы подряжаемся иметь дело с образом файла в памяти (а не на диске), для начала нам нужно найти РЕ-заголовок в своём пространстве. Авансом скажу, что загрузчик копирует его с диска в область, начиная с "ImageBase" (в дефолте, это адрес 0х00400000). Тогда указатель на РЕ-Header можно будет найти по смещению Base.3Ch.



• Теперь ищем секцию-импорта в памяти.. Как мы помним, её RVA хранится в первой записи Image_Directory_Table, а указывает на эту запись - поле РЕ.80h (см.скрин HIEW'a двумя постами выше). Получив содержимое данного поля прибавляем его к базе, и в лапках у нас оказывается прямой линк на начало секции-импорта в памяти - в моём случае это адрес 0x00403000:
Code
1
2
3
4
5
6
7
8
9
10
11
File: R71MT.EXE
Size: 2*048
Type: PE
Image Base : 00400000
Entry Point: 00402000
Sections   :
    Name   | VirtAddr | VirtSize |  Offset  | PhysSize |  Flags   | Code
 ----------+----------+----------+----------+----------+----------+------
  .data    | 00401000 | 00000030 | 00000200 | 00000200 | C0000040 |  
  .text    | 00402000 | 0000002B | 00000400 | 00000200 | 60000020 |  Yes
  .idata   | 00403000 | 000000AE | 00000600 | 00000200 | C0000040 |
Оказавшись в секции-импорта мы (в первую очередь) натыкаемся на N-нное кол-во структур типа IMAGE_IMPORT_DESCRIPTOR. Каждая библиотека импорта имеет свой дескриптор размером 14h байт - сколько библиотек, столько и дескрипторов. Например, если мы импортируем функции только из двух библиотек kernel32.dll и user32.dll, то дескрипторов в секции будет тоже два. Ниже представлен дамп моей секции-импорта и формат её дескрипторов:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
          00 01 02 03  04 05 06 07  08 09 0A 0B  0C 0D 0E 0F
          --------------------------------------------------
00403000  56 30 00 00  00 00 00 00  00 00 00 00  3C 30 00 00  V0..........<0..
00403010  62 30 00 00  8E 30 00 00  00 00 00 00  00 00 00 00  b0..Ћ0..........
00403020  4A 30 00 00  96 30 00 00  00 00 00 00  00 00 00 00  J0..–0..........
00403030  00 00 00 00  00 00 00 00  00 00 00 00  4B 45 52 4E  ............KERN
00403040  45 4C 33 32  2E 44 4C 4C  00 00 55 53  45 52 33 32  EL32.DLL..USER32
00403050  2E 44 4C 4C  00 00 6E 30  00 00 7C 30  00 00 00 00  .DLL..n0..|0....
00403060  00 00 A2 BF  81 7C D4 1A  80 7C 00 00  00 00 00 00  ..ўїЃ|ФЂ|......
00403070  45 78 69 74  50 72 6F 63  65 73 73 00  00 00 56 69  ExitProcess...Vi
00403080  72 74 75 61  6C 50 72 6F  74 65 63 74  00 00 9E 30  rtualProtect..ћ0
00403090  00 00 00 00  00 00 EA 07  3A 7E 00 00  00 00 00 00  ......к:~......
004030A0  4D 65 73 73  61 67 65 42  6F 78 41 00  00 00 00 00  MessageBoxA.....
004030B0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
 
Адрес  | Размер | Значение | Описание
-------|--------|----------|-----------------------------------------------
 00h      2       3056       "IMAGE_THUNK_DATA" - RVA на указатель имени функции.
 02h      2       0000       Старший бит определяет присутствие "ординала" функции.
 04h      4       00000000   TimeDateStamp - обычно нуль или -1.
 08h      4       00000000   ForwarderChain - если fn.входит в цепочку вызовов из др.либы.
 0Сh      4       0000303С   RVA на строку с именем текущей DLL.
 10h      4       00003062   FirstThunk - таблица адресов IAT для данной библиотеки.
 --------------------------
 Всего:   14h     .... 
 14h      ..      ....       Начало следующего дескриптора...
Здесь видно, что кол-во дескрипторов в массиве нигде не учитывается. Вместо этого, последняя структура определяется нулевым дескриптором. Это относится и к адресам в таблице IAT - если он нуль, значит это последний адрес в цепочке. Неопределённость возникает только с полем "IMAGE_THUNK_DATA", рассмотрим его подноготную..

Если перейти по-смещению 3056h, то попадаем в список указателей на имена импортируемых функций 0000306E и 0000307С (нулевой указатель является последним в списке). В данном случае, по-смещению 306Eh лежит word со-значением нуль. Если-бы по смещению(02) старший бит был-бы взведён, то данный word расценивался-бы как ординал/номер функции в библиотеке, за которым сразу следует ASCIIZ-имя этой функции. Таким образом, просканировав список указателей в "IMAGE_THUNK_DATA" можно получить полный лист функций из текущей библиотеки, вместе с их ординалами.

Вот как выглядит лог просмотрщика РЕ-файлов в "Тотале" по Ctrl+Q..
Напомню, что это образ файла на диске, с незаполненным IAT:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IMPORTS TABLE:
   KERNEL32.DLL
     Import Lookup Table RVA :  00003056h  (Unbound IAT)
     TimeDateStamp           :  00000000h
     ForwarderChain          :  00000000h
     DLL Name RVA            :  0000303Ch
     Import Address Table RVA:  00003062h
     First thunk RVA         :  00003062h  <--- заполнит загрузчик
     Ordn   Name
     -----   -----
      0     ExitProcess
      0     VirtualProtect
 
   USER32.DLL 
     Import Lookup Table RVA :  0000308Eh  (Unbound IAT)
     TimeDateStamp           :  00000000h   
     ForwarderChain          :  00000000h
     DLL Name RVA            :  0000304Ah
     Import Address Table RVA:  00003096h
     First thunk RVA         :  00003096h  <--- заполнит загрузчик
     Ordn   Name
     -----   -----
      0     MessageBoxA
На мой взгляд, таблица-импорта в РЕ-файлах организована крайне неудачно - это очевидно всем, кроме парней из Microsoft. Поля в ней выстроены по-типу данных, а не их принадлежности к конкретному импорту. Например, чтобы собрать информацию об одной из функций, приходится рысачить по всей таблице, т.к. поля в ней абсолютно не последовательны, и размазаны по всей секции.
2
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
23.01.2019, 15:31  [ТС]
Думаю для теории - более чем достаточно..
Теперь попробуем реализовать что-то подобное на практике.
Код ниже перехватывает функцию MessageBox(), и декодирует для неё зашифрованную ключом(DCh) строку. После этого, код передаёт управление на оригинальную точку-входа, чтобы функция сбоксила уже готовую мессагу:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
format   pe gui
include  'win32ax.inc'
.data
mes0     db  'H' xor 0dch,'e' xor 0dch,'l' xor 0dch,'l' xor 0dch,'o' xor 0dch,' ' xor 0dch
         db  'W' xor 0dch,'o' xor 0dch,'r' xor 0dch,'l' xor 0dch,'d' xor 0dch, 0
msBox    dd  0                             ; под оригинальный адрес MessageBox()
;------
.code
start:   stdcall HookAPI                   ; уходим на перехват
 
         invoke  MessageBox,0,mes0,0,0     ; функция для проверки
         invoke  ExitProcess,0             ;
 
;------ Тут начинается подпрограмма перехвата API --------
proc  HookAPI
         jmp     @next
 
libName  db  'user32.dll',0                  ; имя внешней DLL,
fName    db  'MessageBoxA',0                 ;  ..и функции в ней для перехвата.
 
@next:   mov     edi,msBox                   ; для stosd
         invoke  LoadLibraryA,libName        ; получить адрес либы в нашей памяти
         push    eax                         ;
         invoke  GetProcAddress,eax,fName    ; получить ориг.адрес функции
         stosd                               ;  ..(запомнить его в 'msBox').
         pop     eax                         ;
         invoke  FreeLibrary,eax             ; освобождаем либу user32.dll.
 
         and     di,0                  ; получаем из EDI базу 00400000h
         mov     esi,[edi+3Ch]         ; RVA на РЕ-заголовок
         add     esi,edi               ;   ..(делаем из него VA).
         mov     esi,[esi+80h]         ; RVA на секцию-импорта
         add     esi,edi               ;   ..(делаем из него VA).
;---------------------------------------
;-- Мы в начале секции-импорта!
;-- Начинаем поиск перехватываемого адреса в таблицах IAT библиотек
;---------------------------------------
         mov     ebx,esi               ; указатель на начало секции
         mov     ecx,2                 ; всего дескрипторов (внешний цикл)
         mov     esi,[esi+10h]         ; по-смещению 10h лежит RVA на первый IAT
@IAT:    add     esi,edi               ;    ..делаем из него VA
@find:   lodsd                         ; перебираем адреса..
         or      eax,eax               ; это последний адрес в данной либе?
         je      @newIAT               ; да - к IAT следущей библиотеки
         cmp     [msBox],eax           ;    ..иначе: проверка на валидный адрес
         je      @stop                 ; если нашли!
         jmp     @find                 ;    ..иначе: продолжить
 
@newIAT: mov     esi,ebx               ; сбросить указатель в начало секции-импорта
         mov     esi,[esi+24h]         ; 14h один дескриптор, +10h = второй "FirstThunk"
         loop    @IAT                  ; продолжить поиск в cл.DLL
 
         jmp     @err                  ; что-то пошло не так!
 
;--- Нашли нужный адрес!!! Подставляем туда свой оффсет.
@stop:   sub     esi,4                 ; вернём указатель у lodsd
         mov     dword[esi],myMsg      ; перехват адреса API-функции!
@err:    ret                           ;
endp                                   ;
;---------------------------------------
;--- А это новый обработчик MessageBox()
;--- Дешифрует строку ключом DCh до терминального нуля
;---------------------------------------
myMsg:   mov     esi,[esp+8]           ; снимаем со-стека адрес строки
         mov     edi,esi               ; EDI для перезаписи.
@crypt:  lodsb                         ; AL = очередной байт из ESI
         or      al,al                 ;   ...
         je      @ok                   ;     ...выйти, если нуль
         xor     al,0dch               ; остальное ксорим..
         stosb                         ;
         jmp     @crypt                ;
@ok:     mov     eax,[msBox]           ; оригинальный адрес MessageBox()
         jmp     eax                   ; отдать ему управление!!!
 
.end start                             ;
Такой-вот получился незатейливый код..
В идеале, нужно расшифровывать строку перед выводом её на экран, и криптовать обратно - после. Организовать это просто. Достаточно вместо jmp eax использовать call eax, и получив управление от оригинальной функции, повторить ксор строки.

Кстати, аверки типа KAV и NOD относятся к нему вполне лояльно. Можно online-вскормить его Касперскому на: https://virusdesk.kaspersky.ru/ и убедится в этом. Это доказывает, что в перехвате API под виндой нет ничего криминального, и системный протекторат никак не препятствует этому, а только поощряет.
2
Эксперт Hardware
Эксперт Hardware
 Аватар для R71MT
6211 / 2445 / 403
Регистрация: 29.07.2014
Сообщений: 3,175
Записей в блоге: 4
23.03.2019, 23:54  [ТС]
SEH - Structured Exception Handling

В Win-системах, решать проблемы внештатных ситуаций выпало на долю механизма SEH - "Структурной обработки исключений". Структурной её назвали из-за цепочки 8-байтных структур "Exception_Registration", которая помещается исключительно в стек. Первый dword в этой структуре служит указателем на предыдущую структуру в цепочке, а второй - это указатель на обработчик данного исключения.
Code
1
2
3
4
EXCEPTION_REGISTRATION {
   Exception_Registration*  prev;   // линк на предыдущий фрейм
   pException_Handler    handler;   // линк на привязанный к данной структуре обработчик
}
Каждую из структур назвали SEH-фреймом, а указатель на первый фрейм в цепочке "Exception_List" прописывается в ТЕВ по смещению FS:[0]. Нам дозволено помещать в стек неограниченное число таких фреймов, что позволит перехватывать различные типы необрабатываемых (unhandled) системой исключений - главное чтобы в "TEB.Exception_List" находился валидный указатель на первый SEH-фрейм. Последний фрейм имеет маркер 0xffffffff и принадлежит самой системе - именно его обработчик выводит посмертное окно с посланием типа: "Обнаружена недопустимая операция. Программа будет закрыта!"

ОС не утруждает себя обработкой всех/юзерских ошибок, поскольку физически не может все/их предугадать. Поэтому мелкософт предоставил программистам возможность самим решать свои проблемы. Прописав начало цепочки в ТЕВ, нам остаётся только залечь в окопы и ждать исключение, после чего новоиспечённая ловушка украдёт сцену и мы можем играть соло на свой вкус и цвет - например, указать на схему пожарного выхода (безопасное место "safe place", заранее предусмотрев его в коде), сбалансировать стек, или тупо пропустить ошибку, если она не критична. В стерильных программах, в стеке всегда присутствует только один/дефолтный SEH-фрейм системы, с маркером -1. Скрин ниже демонстрирует устройство этого механизма:



На этом рисунке, красно-жёлтые блоки выступают в роли структур "Exception_Registration" или SEH-фреймов. Их можно "размазывать" по всему стеку, т.е. фреймы необязательно должны следовать в строгом порядке один-за-другим, и это радует. SEH нас вдохновляет тем, что даёт возможность обрабатывать исключения каждого из программных потоков в отдельности, т.к. у каждого потока свой ТЕВ, стек и свои регистры.

Этим не может похвастаться функция SetUnhandledExceptionFilter() (установить фильтр необрабатываемых исключений). У неё только один аргумент - указатель на обработчик, который будет глобальным внутри всего программного кода. Обработчик этой функции называют ещё "Финальным обработчиком" и из них нельзя выстраивать цепочку - она может быть только в единственном экземпляре (повторный вызов игнорируется). Обработчик такого типа не привязывается к конкретным потокам и будет всплывать при каждом чихе программы.

Таким образом, в нашем распоряжении оказываются два типа обработчиков: финальный и внутрипоточный. Здесь и далее, мы будем рассматривать только внутрипоточный SEH (структуры и прочее), т.к. он представляет более гибкие возможности по сравнению с финальным. Пример установки до неприличия прост:



Поскольку механизм SEH основан на процедурах обратного вызова (CallBack_Routine), обработчики должны возвращать диспетчеру один из двух идентификаторов в регистре EAX - от этого выхлопа зависят дальнейшие действия диспетчера:

• Exception_Execute_Handler(0) - перезагрузить контекст и продолжить выполнение программы.
• Exception_Continue_Search(1) - передать управление следующему SEH-фрейму в цепочке.

Если ни один из наших фильтров в цепочке не вернул(0), то диспетчер зовёт на помощь функцию UnhandledExceptionFilter() (фильтр необрабатываемых исключений) - дефолтный фрейм системы с маркером 0хffffffff, который присутствует всегда и регистрируется загрузчиком образов при подготовке треда к запуску.

Добавлено через 14 минут
SEH - системный диспетчер исключений

Весь функционал диспетчера пользовательского режима находится в kernel-API KiUserExceptionDispatcher(). Эта неэкспортируемая из модуля ntdll.dll функция занимается тем, что ищет подходящий обработчик и если такового в списке нет, просто прихлопывает процесс по Exit, не дав ему возможности даже попрощаться. По большому счёту, диспетчер - это бокс для нескольких API, работой которых он и управляет. Краткий перечень находящихся в подчинении диспетчера функций, приведён ниже:

• RtlDispatchException() - ядро логики диспетчера прикладного уровня;
• UnhandledExceptionFilter() - системная процедура финального обработчика исключений;
• SetUnhandledExceptionFilter() - установить пользовательский финальный обработчик;
• NtRaiseException() - сгенерить программное исключение (код = Е0000100h);
• RtlUnwind() - обратная раскрутка стека после обработки исключения;
• RtlRestoreContext() - восстанавливает регистры в прежнее состояние (x64);
• NtContinue() - восстанавливает регистры в прежнее состояние (x32);

При возникновении исключения, первым принимает его на грудь притаившийся в засаде диспетчер. Сначала в стек породившего исключение потока, диспетчер дампит текущее состояние всех регистров CPU - это важная для нас структура, которую назвали "Context". Среди членов этой структуры есть доступный для модификации EIP, что позволит нашему коду вернуться в прошлое (или будущее), если мы возвратим диспетчеру EAX=0 перезагрузить контекст!

На сл.этапе, диспетчер отправляет в стек ещё одну структуру "Exception_Record" - технические детали подвергшегося трепанации исключения. На финишной прямой, диспетчер кладёт в стек указатели на оба/указанных инфо-блока, и возвращает управление опять нашей программе, чтобы она попыталась сама обработать исключение посредством своего SEH.

Вышеописанный алгоритм приводит к тому, что когда наша/токсичная программа принимает управление от диспетчера, на макушке стека оказывается фрейм "Exception_Pointers" из четырёх указателей. Через эти указатели мы можем подобраться к структурам диспетчера и получить полную голограмму критической ошибки.
Assembler
1
2
3
4
5
EXCEPTION_POINTERS:
   pExceptRecord  dd ?     ; линк на информацию об исключении
   pExceptFrame   dd ?     ; линк на свой SEH-фрейм.
   pContext       dd ?     ; дамп на контекст процессора
   pParam         dd ?     ; резерв..
Рассмотрим перечисленные структуры более подробно..
В блоке "Context" нет ничего особенного - это просто упорядоченный список состояния всех регистров процессора на момент исключения, последовательность которых приводится в инклуде "Except.inc" (оформим позже). Пристального внимания требует вторая структура "Exception_Record":
Assembler
1
2
3
4
5
6
7
EXCEPTION_RECORD:
  ExceptionCode      dd  ?      ; код ошибки - см.сишный хидер "ntstatus.h";
  ExceptionFlags     dd  ?      ; флаг управления данным обработчиком;
  NestedRecord       dd  ?      ; указатель на предыдущий SEH-фрейма;
  ExceptionAddress   dd  ?      ; адрес инструкции вызвавшей исключение;
  NumberParameters   dd  ?      ; число доп.параметров в сл.поле "ExceptionInfo";
  ExceptionInfo      rd  512    ; дополнительные параметры.
• ExceptionFlag - передаёт диспетчер и используется для управления работой самого обработчика. Характеризует то, какие значения может вернуть процедура обработки: EAX=0 или 1. Возможны следующие значения:

0 – обрабатываемое исключение, допускающее перезагрузки контекста CPU (EAX=0);
1 – необрабатываемое исключение (обработчик должен вернуть EAX=1);
2 – процедура обратной раскрутки стека (вообще нельзя обрабатывать).

• NestedRecord - если обработчик вызван для обработки исключения, которое произошло во время обработки другого исключения, то указывает на "Exception_Record", которая относится к тому исключению, обработка которого была аварийно прервана;

• ExceptionInfo – доп/информация. Здесь могут быть параметры функции RaiseException(), или (если код исключения = C0000005h = AccessViolation), здесь будет находиться сл.информация:

1st dword - 0= нарушение прав при чтении, 1= при попытке записи;
2nd dword – адрес, при попытке доступа к которому, произошло исключение.

--------------------------------------------------
По-умолчанию, в своей тушке fasm'a не имеет инклуд с описанием структур SEH, поэтому есть резон оформить его самим. Это позволит обращаться к членам структур по их именам, а не использовать безликие смещения относительно начала инфо-блока.

EXCEPT.INC - инклуд fasm'a,
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
; EXCEPT.INC - инклуд fasm'a,
; для пользовательской SEH-обработки внутрипоточных исключений 
; ------------------------------------------------------------
EXCEPTION_MAXIMUM_PARAMETERS = 15
MAXIMUM_SUPPORTED_EXTENSION  = 512
SIZE_OF_80387_REGISTERS      = 80
 
struct FLOATING_SAVE_AREA
  ControlWord          dd ?
  StatusWord           dd ?
  TagWord              dd ?
  ErrorOffset          dd ?
  ErrorSelector        dd ?
  DataOffset           dd ?
  DataSelector         dd ?
  RegisterArea         rb SIZE_OF_80387_REGISTERS
  Cr0NpxState          dd ?
ends
 
struct CONTEXT
  ContextFlags         dd ?
  iDr0                 dd ?
  iDr1                 dd ?
  iDr2                 dd ?
  iDr3                 dd ?
  iDr6                 dd ?
  iDr7                 dd ?
  FloatSave            FLOATING_SAVE_AREA
  regGs                dd ?
  regFs                dd ?
  regEs                dd ?
  regDs                dd ?
  regEdi               dd ?
  regEsi               dd ?
  regEbx               dd ?
  regEdx               dd ?
  regEcx               dd ?
  regEax               dd ?
  regEbp               dd ?
  regEip               dd ?
  regCs                dd ?
  regFlag              dd ?
  regEsp               dd ?
  regSs                dd ?
  ExtendedRegisters    rb MAXIMUM_SUPPORTED_EXTENSION
ends
 
struct EXCEPTION_RECORD
  ExceptionCode        dd ?
  ExceptionFlags       dd ?
  pExceptionRecord     dd ?
  ExceptionAddress     dd ?
  NumberParameters     dd ?
  ExceptionInformation rd EXCEPTION_MAXIMUM_PARAMETERS
ends
 
struct EXCEPTION_POINTERS
   pExceptionRecord    dd ?
   pExceptionFrame     dd ?
   pExceptionContext   dd ?
   pParam              dd ?
ends


Добавлено через 20 минут
SEH - программная реализация

В качестве демонстрации, напишем простой код для обработки исключений и вскормим этот код отладчику. Смысл в том, чтобы перехватить исключение и вытащить приложение "с того света", куда ему светит дорога при отсутствии SEH. Внутри своего обработчика я вывожу в форточку код-ошибки, и подменяю значение регистра EIP в структуре "Context". Дальше, прошу диспетчер перезагрузить контекст, в результате чего приложение не терпит крах, а благополучно отправляется на выход по "ExitProcess()":
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
format    pe  gui
include  'win32ax.inc'
include  'except.inc'
.data
;--------
mess0    db  'Стандартный выход из приложения после исключения!',0
mess1    db  'Ошибка!!! Недопустимая операция!',13,10
         db  'Код исключения: '
erCode   db   9 dup(0)
frm      db  '%08X',0
;--------
.code
start:   push   mySEH
         push   dword[fs:0]
         mov    [fs:0],esp
 
         xor    eax,eax
         mov    eax,[eax]
;         div    eax
 
next:    pop    dword[fs:0]
         add    esp,4
 
         invoke  MessageBox,0,mess0,0,0
         invoke  ExitProcess,0
;
;--- Внутрипоточный обработчик исключений -------------
;------------------------------------------------------
proc  mySEH  pRecord, pFrame, pContext, pParam   ; параметры в виде "ExceptionPointers"
         mov     esi,[pRecord]                   ; ESI = Exception_Record
         mov     eax,[esi+EXCEPTION_RECORD.ExceptionCode]   ; берём код-исключения из структуры
        cinvoke  wsprintf,erCode,frm,eax         ; переводим его в строку (Hex2Asc)
         invoke  MessageBox,0,mess1,0,10h        ; боксим мессагу об исключении
 
         mov     edi,[pContext]                  ; EDI = адрес структуры "Context"
         mov     eax,next                        ; EAX = адрес безопасного места
         mov     [edi+CONTEXT.regEip],eax        ; подмена указателя EIP
 
         xor     eax,eax                         ; EAX=0 перезагрузить контекст!
;         inc     eax                            ;   ..(можно не перезагружать)
         ret                                     ; CallBack диспетчеру-исключений..
endp
.end start


На этом скрине видно, что структура "Exception_Record" заполнена следующим образом, и мы можем собрать из этих данных достаточно информации:

С0000005 = код ошибки "Access Violation" (нет прав на RW);
00000000 = флаг "Обрабатываемое исключение";
00000000 = исключение не вложенное (не от другого SEH-фрейма в цепочке);
00402015 = адрес глючной инструкции;
00000002 = два дополнительных параметра;
00000000 = параметр(1) =0 (нарушение прав при чтении);
00000000 = параметр(2) =0 (адрес попытки доступа).
3
1 / 1 / 0
Регистрация: 21.09.2017
Сообщений: 31
11.02.2021, 11:29
Отличная статья, еще не все осилил, тем не менее, спасибо автору.
Возможно у кого то не будет работать 1 пример. При вызове функции приложение будет вылетать с исключением (0xc0000005).
Чтобы устранить, нужно секцию '.reloc' сделать доступной для чтения:
Code
1
section '.reloc' fixups data discardable readable
1
Модератор
Эксперт по электронике
 Аватар для ФедосеевПавел
8659 / 4494 / 1669
Регистрация: 01.02.2015
Сообщений: 13,905
Записей в блоге: 12
12.02.2021, 12:51
R71MT, если у вас нет возможности править эту тему - могу внести исправление, предложенноеboolc, а его сообщение переместить в тему, для обсуждения данной статьи.

Исправить и перенести?
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
inter-admin
Эксперт
29715 / 6470 / 2152
Регистрация: 06.03.2009
Сообщений: 28,500
Блог
12.02.2021, 12:51
Помогаю со студенческими работами здесь

Кто может поделиться файлами ogg.dll, vorbis.dll и vorbisfile.dll - 32-х и 64-битными версиями?
Движок перевожу на платформу Win64 и нужно, чтобы разрядность ЕХЕ и DLL совпадали, а в интернете искал 64-битные версии ogg.dll, vorbis.dll...

Затупил, скажите где взять libmysqld.dll, libmysqld50.dll, libmysqld51.dll
Вечер добрый, ставлю ZEOS на Delphi 7, и для нормальной работы нужны файлы libmysqld.dll, libmysqld50.dll, libmysqld51.dll. Но я нигде...

Сборка Qt: отучение Qt от mingw10.dll, libgcc_s_dw2-1.dll и других Qt***.dll
В связи с тем, что часто возникают одни и те же вопросы, а в нете копаться никто не любит привожу перевод буржуйской вики по отлучению...

Пишем патч на C++
Обясните как мне написать патч на C++, в asm естественно. Данные: 1) есть файл с именем Prog.exe 2) Адрес файла который надо поменять...

Пишем брут
Кто знает возможно ли написать брут другвокруг


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
18
Ответ Создать тему
Новые блоги и статьи
SDL3 для Web (WebAssembly): Реализация движения на Box2D v3 - трение и коллизии с повёрнутыми стенами
8Observer8 20.02.2026
Содержание блога Box2D позволяет легко создать главного героя, который не проходит сквозь стены и перемещается с заданным трением о препятствия, которые можно располагать под углом, как верхнее. . .
Конвертировать закладки radiotray-ng в m3u-плейлист
damix 19.02.2026
Это можно сделать скриптом для PowerShell. Использование . \СonvertRadiotrayToM3U. ps1 <path_to_bookmarks. json> Рядом с файлом bookmarks. json появится файл bookmarks. m3u с результатом. # Check if. . .
Семь CDC на одном интерфейсе: 5 U[S]ARTов, 1 CAN и 1 SSI
Eddy_Em 18.02.2026
Постепенно допиливаю свою "многоинтерфейсную плату". Выглядит вот так: https:/ / www. cyberforum. ru/ blog_attachment. php?attachmentid=11617&stc=1&d=1771445347 Основана на STM32F303RBT6. На борту пять. . .
Камера Toupcam IUA500KMA
Eddy_Em 12.02.2026
Т. к. у всяких "хикроботов" слишком уж мелкий пиксель, для подсмотра в ESPriF они вообще плохо годятся: уже 14 величину можно рассмотреть еле-еле лишь на экспозициях под 3 секунды (а то и больше),. . .
И ясному Солнцу
zbw 12.02.2026
И ясному Солнцу, и светлой Луне. В мире покоя нет и люди не могут жить в тишине. А жить им немного лет.
«Знание-Сила»
zbw 12.02.2026
«Знание-Сила» «Время-Деньги» «Деньги -Пуля»
SDL3 для Web (WebAssembly): Подключение Box2D v3, физика и отрисовка коллайдеров
8Observer8 12.02.2026
Содержание блога Box2D - это библиотека для 2D физики для анимаций и игр. С её помощью можно определять были ли коллизии между конкретными объектами и вызывать обработчики событий столкновения. . . .
SDL3 для Web (WebAssembly): Загрузка PNG с прозрачным фоном с помощью SDL_LoadPNG (без SDL3_image)
8Observer8 11.02.2026
Содержание блога Библиотека SDL3 содержит встроенные инструменты для базовой работы с изображениями - без использования библиотеки SDL3_image. Пошагово создадим проект для загрузки изображения. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru