You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
14 KiB
258 lines
14 KiB
ПРАВИЛА РАЗРАБОТКИ МОДУЛЕЙ
|
|
|
|
Модуль представляет из себя DLL, которая будет работать в левом произвольном процессе.
|
|
На 64 битной системе будет 64-битный модуль, а на 32 битной системе 32-битный модуль.
|
|
Модуль – это не просто программа на С++. У модуля есть некоторые ограничения в связи с контекстом, в котором он работает:
|
|
1. Нужно минимизировать использование Visual C++ Runtime. Функции malloc, memcpy, str* - следует избегать. В WinAPI уже есть все необходимое – вместо malloc – HeapAlloc, вместо memcpy – CopyMemory, вместо strstr – StrStr (shlwapi.h/.lib).
|
|
1.1. STL в общем случае следует избегать. ТОЧНО не работает std::mutex. Остальное - на ваш страх и риск!
|
|
1.2. Некоторые фичи STL дают дополнительный пролог к коду. Для std::mutex он весит 200к. Его не видно в вашем коде, а он есть, и будет вести себя странно в контексте выполнения!
|
|
1.3. Несоблюдение пунктов 1 и 1.1 может быть причиной ОЧЕНЬ странного поведения.
|
|
2. Линковка рантайма и сторонних библиотек к модулю должна быть статической (флаг /MT).
|
|
3. Если используются сторонние библиотеки, они должны позволять заменять функции управления памятью malloc/realloc/free (вместо них вы туда подсунете свои обертки над VirtualAlloc). Также они и сами должны быть слинкованы статически со всем необходимым (чтобы не было попыток загрузить какую-то dll-ку, которой по умолчанию нету в Windows).
|
|
4. Модуль может работать как от имени текущего интерактивного пользователя, так и от имени локальной системы (nt authority\SYSTEM), как НЕИНТЕРАКТИВНЫЙ процесс (как служба). По поводу неинтерактивности, гуглите Session 0 isolation/Services isolation in session 0.
|
|
5. Код модуля не должен пытаться получить путь к самому себе, ибо в реальных условиях он будет запускаться в памяти без сохранения на диск.
|
|
Разработчик обязан это учитывать, и обеспечить работу модуля во всех режимах. Игнорирование или незнание этих пунктов обычно являются причиной того, что у вас локально модуль работает, а в production – нет.
|
|
Также, есть дополнительные требования, связанные с безопасностью:
|
|
1. Запрещено использование временных файлов. По умолчанию запрещена запись в любые файлы и реестр. Каждый случай такой записи должен оговариваться.
|
|
2. Модуль по возможности не должен обладать состоянием (т.е., чтобы ему не нужно было сохранять что-либо в реестр/файл при остановке, и потом читать оттуда при запуске).
|
|
3. Если модуль работает в нескольких процессах, наилучший способ коммуникации между ними (по убыванию – помним что временные файлы запрещены):
|
|
a. Прямое чтение/запись памяти (ReadProcessMemory/WriteProcessMemory)
|
|
b. Named pipes
|
|
|
|
|
|
ОФОРМЛЕНИЕ МОДУЛЯ
|
|
1. Должен иметься проект Microsoft Visual Studio версии не ниже 2015.
|
|
2. Проект Visual Studio должен быть настроен следующим образом:
|
|
* Для ВСЕХ профилей сборки:
|
|
- выходной каталог: $(SolutionDir)Bin\$(PlatformTarget)\$(Configuration)\
|
|
- Промежуточный каталог: $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(ProjectName)\
|
|
- Многопроцессорная компиляция: да
|
|
* Профиль Release:
|
|
- Формат отладочной информации (С/С++ создание кода): нет
|
|
- Создавать отладочную информацию (компоновщик/отладка): нет
|
|
3. Строки обфусцировать библиотекой Andrivet (приложена, см.макрос _STR())
|
|
4. Системные вызовы обфусцировать библиотекой GetApi.h. Быть внимательным, обфускация сисвызовов может давать падения.
|
|
5. Модуль должен иметь две версии - x32- и x64-разрядную.
|
|
6. В боевой сборке должны быть обфусцированы по максимуму строки, отключен всяческий отладочный вывод.
|
|
7. Модуль должен иметь отладочную версию. Отладочный вывод должен выводиться в c:/temp/modulename.log (путь к логу настраивается в макросе).
|
|
Каждая запись лога должна содержать временнУю метку с точностью до секунды.
|
|
8. В проекте должен быть файл настроек config.h (название неважно, важна суть - здесь все глобальные настройки - пути, макросы-переключатели условной компиляции итд).
|
|
9. Модуль должен работать на всех современных версиях Windows.
|
|
Минимальная поддерживаемая версия Windows - Windows XP (если невозможно - Windows Vista).
|
|
10. Дополнительно к компоновке должен добавляться файл notelemetry.obj (https://stackoverflow.com/questions/37761768/how-to-prevent-visual-studio-2015-update-2-to-add-telemetry-main-invoke-trigger)
|
|
|
|
|
|
НАИМЕНОВАНИЕ СБОРОК
|
|
|
|
В сборке должна быть структура каталогов:
|
|
|
|
modulename/Release_logged/x86
|
|
modulename/Release_logged/x64
|
|
modulename/Release_nologs/x86
|
|
modulename/Release_nologs/x64
|
|
|
|
В Release_logged идет версия с логом и без обфускации (но это все равно профиль сборки Release!
|
|
т.е. там должны быть включены все оптимизации, выключены отладочные символы итд).
|
|
В release_nologs - версия без лога, с полной обфускацией.
|
|
В имени каталога со сборкой должно быть имя модуля и дата (например cookies.22.04.2019).
|
|
Если на указанную дату есть несколько сборок одного модуля, должен добавляться уникальный суффикс (например cookies.22.04.2019.2)
|
|
|
|
Это нужно для нормальной эксплуатации модуля - чтобы тестировщики и администраторы отличали версии сборок,
|
|
чтобы был возможно откат или апгрейд, а также для выдачи нужной сборки по её дате.
|
|
|
|
|
|
ТЕСТИРОВАНИЕ МОДУЛЯ
|
|
|
|
Тестировать модуль следует в следующих режимах:
|
|
1. На ОС Windows 7, 8.1, 10 как 32-, так и 64-й разрядности. В ОС НЕ должны быть установлены программы, ставящие пакеты MSVC++ runtime library (т.е., ставим голую ОС и по минимуму что нужно для тестирования).
|
|
2. От имени интерактивного юзера (того, под кем вы вошли)
|
|
3. От имени НЕ администратора
|
|
4. От имени Системы (качаем пакет pstools, запускаем модуль так:
|
|
psexec –d –s runmodule.exe
|
|
это запуск модуля как неинтерактивный юзер SYSTEM
|
|
Только после прохождения внутренних тестов можно передавать модуль тестировщику.
|
|
|
|
|
|
ВЗАИМОДЕЙСТВИЕ С ВЫШЕСТОЯЩЕЙ ЛОГИКОЙ
|
|
|
|
Модуль получает необходимую информацию от вышестоящей логики. Идентификатор клиента ClientID, тэг группы group, внешний IP-адрес будут переданы через функцию Start в параметре ParentInfo
|
|
Конфиги модуль получает через функцию Control (если конфигов несколько, каждому из них соответствует свое значение параметра CtlArg).
|
|
Тело конфигов и их длина будут переданы через параметры CtlArg и CtlArgLen. остальные параметры не используются.
|
|
Для автономных тестов, следует запилить демо-запускатор, который будет грузить скомпиленный модуль через loadLIbrary. Псевдокод очень примерно такой:
|
|
main() {
|
|
LoadLibrary(“module.dll”);
|
|
Start = GetProcAddress(module, “Start”);
|
|
Handle = Start(…);
|
|
If(!handle) return 1;
|
|
Control(“config”, “config body”);
|
|
While(true) sleep(1000);
|
|
Release(handle);
|
|
}
|
|
Таким образом, следущая схема:
|
|
1. демо-запускатор грузит к себе модуль (DLL) и вызывает функции start и control чтобы передать в неё конфиги, внешний ip-адрес, clientid и group.
|
|
2. код внутри модуля, который делает полезную работу.
|
|
|
|
Поток выполнения следующий:
|
|
* Функция Start() инициализирует работу, запускает основной рабочий поток в фоне, и возвращает дескриптор модуля (обычно, адрес функции Start)
|
|
* Основной поток может дождаться, пока не прилетят необходимые вызовы Control() с конфигами, и только потом начать работу.
|
|
Это зависит от назначения самого модуля - может и не требоваться ожидание.
|
|
* Вызовы Control() обычно используются, чтобы передать в модуль конфиги, либо управляющие воздействия (смена режимов работы).
|
|
Эти вызовы должны обрабатываться как прерывания. Эти вызовы происходят в отдельном потоке, потому следует обеспечить потокобезопасность для всего,
|
|
что разделяется с основным потоком выполнения.
|
|
|
|
API МОДУЛЯ
|
|
Модуль экспортирует следующие функции: Start, Control, Release, FreeBuffer [*1]
|
|
Все функции имеют конвенцию вызова stdcall и экспортируются по именам.
|
|
Функция Start имеет следующий прототип:
|
|
PVOID Start(
|
|
LPCSTR ModuleName,
|
|
LPCBYTE Arg,
|
|
SIZE_T ArgLen,
|
|
LPSTR ResultInfo,
|
|
const ParentInfo* pParentData,
|
|
PVOID EventCallback,
|
|
PVOID EventCallbackContext,
|
|
PVOID Reserved1);
|
|
Функция вызывается при запуске модуля вышестоящей логикой.
|
|
ModuleName - имя модуля
|
|
Arg - аргумент команды start
|
|
ArgLen - размер параметр CtlArg в байтах
|
|
ResultInfo -буфер для результирующей строки ctl. Буфер имеет фиксированную длину 1024 байта
|
|
pParentData - информация о вышестоящей логике, управляется конфигом модуля
|
|
EventCallback – указатель на функцию логирования (см.ниже)
|
|
EventCallbackContext – контекст логирования (см.ниже)
|
|
Функция в случае удачи возвращает описатель, который необходимо использовать при вызове функций Control и Release (можно передавать адрес самой функции Start – он достаточно уникален в пределах ОС). В случае неудачи функция возвращает ноль.
|
|
|
|
typedef struct ParentInfo {
|
|
CHAR ParentID[256];
|
|
CHAR ParentGroup[64];
|
|
CHAR SelfIP[64];
|
|
LPCWSTR ParentFiles;
|
|
} ;
|
|
ParentID - полный ID клиента вышестоящей логики
|
|
ParentGroup - группирующий тег вышестоящей логики
|
|
SelfIP - внешний IP-адрес
|
|
ParentFiles - не используется
|
|
|
|
6.2 Функция Control имеет следующий прототип
|
|
BOOL Control (
|
|
PVOID ModuleHandle,
|
|
LPCSTR Ctl,
|
|
LPCBYTE CtlArg,
|
|
SIZE_T CtlArgLen,
|
|
LPSTR CtlResultInfo,
|
|
PVOID* ppOutData,
|
|
PDWORD pOutDataSize,
|
|
LPCSTR pOutDataTag,
|
|
PVOID Reserved1);
|
|
|
|
ModuleHandle - описатель модуля, которое вернула функция Start.
|
|
Ctl - строка содержащая описание управляющего сигнала к модулю
|
|
CtlArg - аргумент ctl к модулю (тело сигнала)
|
|
CtlArgLen - размер параметра CtlArg в байтах
|
|
ResultInfo -буфер для результирующей строки ctl. Буфер имеет фиксированную длину 1024 байта
|
|
ppOutData - указатель на переменную в которую будет сохранён указатель на буфер с выходными данными ctl (ctl_OutData)
|
|
pOutDataSize - указатель на переменную в которую будет сохранён размер данных в буфере с выходными данными ctl
|
|
pOutDataTag - буфер для вспомогательного тега, который будет отправлен на сервер. Буфер имеет фиксированную длину 128 байт
|
|
Функция в случае удачи возвращает TRUE, в противном случае функция возвращает FALSE. В случае успеха если значение *ppOutData после вызова не равно нулю, то этот буфер должен быть освобождён через функцию FreeBuffer.
|
|
ОБЫЧНОЕ назначение этой функции – передача конфига модулю.
|
|
|
|
Функция Release имеет следующий прототип
|
|
VOID Release (
|
|
PVOID ModuleHandle);
|
|
Функция реализует полное завершение работы модуля. В её задачи входит удаление всех ресурсов используемых в ходе работы модуля.
|
|
|
|
Функция FreeBuffer имеет следующий прототип
|
|
VOID FreeBuffer (
|
|
PVOID pMemory);
|
|
Функция освобождает буфер выделенный внутри функции Control (параметр ppOutData).
|
|
|
|
* Изменения именования функций в экспорте
|
|
1) старый вариант, продолжает работать, но антивирусы часто палят модуль по этим функциям
|
|
2) Модуль экспортирует любые минимум 4 функции по именам, имена функции сообщаются администратору в виде
|
|
CheckFuncStr=Start, GetLength=Control, SetHeigth=Release, Reload=FreeBuffer.
|
|
Функции можно изменить при чистке, но опять же нужно будет сообщить об этом администратору.
|
|
3) Модуль экспортирует функции по ординалу, или по имени, но обязательно в следующем порядке: 1.Start, 2.Control, 3.FreeBuffer 4.Release
|
|
Пример def файла для экспорта по ординалам
|
|
EXPORTS
|
|
@1 = Start_
|
|
@2 = Control_
|
|
@3 = FreeBuffer_
|
|
@4 = Release_
|
|
Пример def файла для экспорта по именам:
|
|
CheckFuncStr = Start_
|
|
GetLength = Control_
|
|
Reload = FreeBuffer_
|
|
SetHeigth = Release_
|
|
|
|
ПРАВИЛА ЛОГИРОВАНИЯ СОБЫТИЙ В АДМИНКУ
|
|
|
|
Два предпоследних параметра функции Start предназначены для логирования событий в админку.
|
|
EventCallback - это указатель на функцию, определенную как:
|
|
|
|
typedef VOID (__stdcall *pEventCallback)(
|
|
PVOID ModuleHandle,
|
|
LPCSTR EventName,
|
|
LPCSTR EventInfo,
|
|
PVOID pOutData,
|
|
DWORD OutDataSize,
|
|
LPCSTR pOutDataTag,
|
|
PVOID Context);
|
|
|
|
где:
|
|
ModuleHandle - дескриптор модуля (обычно это указатель на функцию Start)
|
|
EventName - тип события
|
|
EventInfo - содержимое события
|
|
pOutData - дополнительные данные события
|
|
OutDataSize - длина доп.данных
|
|
pOutDataTag - тэг события
|
|
Context - передаем сюда EventCallbackContext, полученный в Start
|
|
|
|
Определяем у себя в коде функцию наподобие
|
|
void SendEvent(char* name, char* value) {
|
|
pEventCallback callback = (pEventCallback)EvCallback;
|
|
if (callback) {
|
|
debug_printf("SendEvent(%s, %s)\r\n", name, value);
|
|
callback(Start, name, value, NULL, 0, tag, EvCallbackContext);
|
|
}
|
|
}
|
|
где
|
|
tag - тег события (нужен для фильтрации в логах; обычно это имя модуля. Ну например, "module1")
|
|
name - источник события (множество событий можно сгруппировать по их источнику - подсистеме в коде, где они сгенерированы. Ну например, "sql")
|
|
value - содержание события (произвольная строка. Ну например, "module1 build 01 Jan, 20xx 11:22:25 started ok")
|
|
|
|
Источник события разработчик определяет сам.
|
|
Источник должен состоять из 4 символов. К примеру, можно определить типы DEBG для отладки, VERS для передачи собственной версии, PING для передачи heartbeat'а в админку, итд.
|
|
Источник, идентифицирующий подсистему твоего модуля, из которого происходит событие. К примеру, в модуле есть подсистема работы с файлами, и подсистема сети - для них используются теги "file" и "net".
|
|
Раньше нужно было вешать мютекс на логирование, т.к. в вышестоящей логике не было защиты и проверки, и это было потоконебезопасным. Потом мютекс поставили с той стороны, и все вроде бы работает.
|
|
Но - если что - если будут проблемы - смотри в первую очередь в сторону многопоточности, во вторую очередь в конвенцию вызовов (_stdcall).
|
|
|
|
!!!!!!!!!!!
|
|
!!!ВАЖНО!!!
|
|
!!!!!!!!!!!
|
|
|
|
Событий от модуля должно быть мало и они должны быть важными. Недопустимо флудить отладкой.
|
|
т.е. ты можешь для отладки расставить в модуле логирование, но потом при выдаче сборки, ты обязан отладочные события убрать.
|
|
|
|
Отправка события является блокирующей операцией.
|
|
|
|
Следует понимать, что отправка события родителю не является мгновенной. Родитель получает события путем периодического опроса
|
|
через разделяемую память. Поэтому, хотя событие может быть отправлено, оно может быть еще не принято родителем.
|
|
Это в особенности касается событий, отправляемых перед завершением процесса - они могут быть потеряны по описанной причине.
|
|
|
|
Если модуль нерезидентный (единократно отрабатывает и далее отключается), по завершению работы он обязан отправить событие
|
|
EventName = WantRelease
|
|
EventInfo = NULL
|
|
По получению данного события носитель модуля выгрузит его из памяти.
|
|
|
|
|
|
ТИПИЧНЫЕ ОШИБКИ
|
|
|
|
1) отправление события (сообщения), чаще всего с версией и датой модуля из функции Start.
|
|
Этого делать НЕЛЬЗЯ.
|
|
Правильно так:
|
|
- Start провел начальные проверки, инициализации, запустил основной поток и завершился
|
|
- основной поток уже начинает спамить телеметрией в событиях
|
|
|
|
2) в функцию Start передаются botid, group и ip.
|
|
эти строковые значения нужно СКОПИРОВАТЬ в функции Start, потому что при выходе из функции память строк освобождается и строки становятся невалидными.
|