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

Обычно наиболее целесообразна методика работы «сверху вниз», предполагающая сначала рассмотрение общих случаев, а затем, по мере необходимости, их детализацию. В большинстве случаев это означает первоначальное применение средств мониторинга операционной системы для определения файлов и ресурсов, к которым обращается исследуемая программа. (Исключение из этого правила составляют сетевые программы. При исследовании сетевых программ чаще всего требуется перейти к анализу передаваемых по сети пакетов.)

В составе операционных систем Windows подобный инструментарий не поставляется, поэтому используют программные средства других производителей. До настоящего времени главным их источником для Windows был сайт Syslntemals по адресу www.sysintemals.com в Интернете. Особенно интересны программы FileMon, RegMon, а в случае NT - HandleEx. Подробнее о них будет рассказано в главе 5. Все, что вам необходимо знать об этих средствах сейчас, - это то, что они позволяют контролировать выполняющуюся программу (или программы), узнать, к каким файлам обращается контролируемая исследователем программа, читает ли она их или пишет в них информацию и куда именно, какие другие файлы она ищет. Перечисленные функции реализованы в программе FileMon. Программа RegMon позволяет исследовать то же самое применительно к реестру Windows NT: к каким ключам реестра программа обращается, какие ключи читает, обновляет, ищет и т. д. HandleEx реализует те же самые функции, но несколько другим способом. Результаты ее работы упорядочены по процессам, дескрипторам файлов и тем объектам, на которые указывают дескрипторы файлов.

В качестве дополнительной услуги доступны свободно распространяемые версии почти всех инструментальных средств Syslntemals, причем большинство из них с исходными текстами программ. (Помимо сайта Syslnternals, на родственном сайте Winternals.com продаются аналогичные коммерческие инструментальные средства с несколько большими возможностями.) Для пользователя UNIX в этом нет ничего необычного, но для пользователя Windows это приятная неожиданность.

В состав большинства дистрибутивов UNIX включены программные средства, которые могут облегчить анализ VB-программ. В списке Rosetta Stone перечислено много программ трассировки. Список можно найти по адресу http://bhami.com/rosetta.html в Интернете. Любая программа трассировки реализует низкоуровневые функции, поэтому она может работать только под управлением ограниченного количества операционных систем. Примерами программ трассировки могут служить программы trace, strace, ktrace и truss, а также утилита strace, выполняемая в операционной системе Red Hat Linux, версия 6.2. Утилита (как и большинство ранее упомянутых программ трассировки) позволяет исследовать системные обращения (обращения к ядру операционной системы) и их параметры, что позволяет многое узнать о работе программы.

Приоткрывая завесу

Декомпиляторы VB

Изрядное количество программ во всем мире написано на Visual Basic (VB). Сюда относятся как зловредные программы, так и программы законопослушных программистов. VB бросает вызов всякому, кто отважится декомпилировать программу, написанную на этом языке. Последний свободно доступный декомпилятор мог работать только с программами VB3. Начиная с VB5, результат компиляции программы может быть представлен как во внутреннем коде исполняемого модуля («native code»), реализующем стандартное обращение к Windows, так и в p-code. Р-код - это псевдокод, который похож на байт-код (или машинно-независимый код), генерируемый Java-компилятором. P-код распознает интерпретатор реального времени Visual Basic VBRUN300.DLL, VBRUN500.DLL или VBRUN600.DLL. Сложность заключается в том, что очень мало доступной документации, в которой описано соответствие конструкций исходного текста программы функциям VB в откомпилированной программе. Конечно, всегда можно декомпилировать интерпретатор VB DLL и восстановить соответствие, но это очень трудоемкое занятие.

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

Для удобства восприятия протокол трассировки дополнен комментариями:

[elliptic@ellipse]$ echo hello > test [elliptic@ellipse]$ strace cat test execve(“/bin/cat”, [“cat”, “test”], [/* 21 vars */]) = 0

Утилита strace не начинает вывод до тех пор, пока не будет вызвана команда cat. Поэтому нельзя отследить процесс ее поиска командным процессором shell. К моменту начала работы утилиты strace команда cat находилась в директории /bin. Из примера трассировки видно, что входными параметрами программы execve являются местонахождение cat, ее имя, аргумент (test) и список из 21 переменной окружения. brk(O) = 0x804bl60 old_mmap(NULL, 4096, PROTREADPROT_WRITE,

М APPRIV ATE IM AP_AN ON YMOU S, - 1,0) = 0x40014000 open(“/etc/ld.so.preload”, 0_RD0NLY)= -1 ENOENT (No such file or directory)

После вызова execve начинается обычный процесс загрузки: распределение памяти и т. д. Отметим свидетельствующий об ошибке код возврата, равный -1, при попытке открыть /etc/ld.so.preload. Ошибка поясняется диагностическим сообщением об отсутствии файла или директории. Действительно, такого файла нет. Из примера ясно, что если вместо файла указать параметры так, как это указано в примере open, execve самостоятельно найдет файл. Это было бы полезно для последующих действий привилегированного пользователя. Но для этого нужно поместить новый файл в директорию /etc, в которой запрещено что-либо делать до тех пор, пока кто-нибудь не откорректирует файл системных разрешений. В большинстве Unix-систем право записи в директорию /etc имеет только пользователь, получивший привилегированные права «root» тем или иным способом. Это еще одна причина, по которой обычные пользователи не могут писать в /etc. Если задаться целью спрятать Троянского коня, то лучшего места не найти (после того как будут получены права привилегированного пользователя root).

open(“/etc/ld.so.cache”, ORDONLY) = 4 fstat(4, {st_mode=S_IFREG0644, st_size= 12431, ...}) = 0
old_mmap(NULL, 12431, PROT READ, MAP PRIVATE, 4, 0) =
0x40015000 close(4) = 0
open(“/lib/libc.so.6”, O RDONLY) = 4
fstat(4, {st_mode=S_IFREG0755, st_size=4101324, ...}) = 0
read(4,
“\17 7ELF\ 1 \ 1 \ 1 \0\0\0\0\0\0\0\0\0\3 \0\3 \0\ 1 \0\0\0\210\212”...,
4096) = 4096

Прочитаны первые 4 Кб библиотеки libc. Libc - это стандартная библиотека коллективного доступа, в которой расположены функции, вызываемые из программ на языке С (такие какprintf, scanf и т. д.).

old_mmap(NULL, 1001564, PROT_READPROT_EXEC, MAP PRIVATE, 4, 0) = 0x40019000
mprotect(0x40106000, 30812, PROT NONE) = 0 old_mmap(0x40106000, 16384, PROTREADPROT_WRITE, MAP_PRIVATEMAP_FIXED, 4, OxecOOO) = 0x40106000 old_mmap(0x401 OaOOO, 14428, PROTREAD PROT_WRITE,
M APPRI V ATE  M AP_F IXED  M AP_AN ON YMOU S, -1, 0)
= 0x401OaOOO close(4) = 0
mprotect(0x40019000, 970752, PROT_READPROT_WRITE) = 0
mprotect(0x40019000, 970752, PROT_READPROT_EXEC) = 0
munmap(0x40015000, 12431) = 0
personality(PERLINUX) = 0
getpid() = 9271
brk(0) = 0x804bl60

Ьгк(0х804Ь198) = 0x804bl98 brk(0x804c000)= 0x804c000

open(“/usr/share/locale/locale.alias”, 0_RD0NLY)= 4 fstat64(0x4, 0xbfffb79c) = -1 ENOSYS (Function not implemented)
fstat(4, {st_mode=S_IFREG0644, st_size=2265, ...}) = 0 old_mmap(NULL, 4096, PROTREAD PROT_WRITE,
M APPRIV ATE  M AP_AN ON YMOU S, - 1,0) = 0x40015000 read(4, “# Locale name alias data baseAn#”..., 4096) = 2265 read(4, 4096) = 0 close(4) = 0
munmap(0x40015000, 4096) = 0

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

open(“/usr/share/il8n/locale.alias”, 0_RD0NLY) = -1 ENOENT (No such file or directory) open(‘7usr/share/locale/en_US/LC_MESSAGES”, 0_RD0NLY) = 4 fstat(4, {st_mode=S_IFDIR0755, st_size=4096, ...}) = 0 close(4) = 0
open(“/usr/share/locale/en_US/LC_MESSAGES/SYS_LC_MESSAGES”,
ORDONLY) = 4
fstat(4, {st_mode=S_IFREG0644, st_size=44, ...}) = 0 old_mmap(NULL, 44, PROT READ, MAPPRIVATE, 4, 0)
= 0x40015000 close(4) = 0
open(“/usr/share/locale/en_US/LC_MONETARY”, 0_RD0NLY) = 4 fstat(4, {st_mode=S_IFREG0644, st_size=93, ...}) = 0 old_mmap(NULL, 93, PROT READ, MAP PRIVATE, 4, 0)
= 0x40016000 close(4) = 0
open(“/usr/share/locale/en_US/LC_COLLATE”, 0_RD0NLY)= 4 fstat(4, {st_mode=S_IFREG0644, st_size=29970, ...}) = 0 old_mmap(NULL, 29970, PROT READ, MAP PRIVATE, 4, 0)
= 0x4010e000 close(4) = 0
brk(0x804d000) = 0x804d000
open(“/usr/share/locale/en_US/LC_TIME”, O RDONLY) = 4 fstat(4, {st_mode=S_IFREG0644, st_size=508, ...}) = 0 old_mmap(NULL, 508, PROT READ, MAP PRIVATE, 4, 0)
= 0x40017000 close(4) = 0
open(“/usr/share/locale/en_US/LC_NUMERIC”, 0_RD0NLY)= 4 fstat(4, {st_mode=S_IFREG0644, st_size=27, ...}) = 0 old_mmap(NULL, 27, PROT READ, MAP PRIVATE, 4, 0)
= 0x40018000

close(4) = О

open(“/usr/share/locale/en_US/LC_CTYPE”, 0_RD0NLY) = 4 fstat(4, {st_mode=S_IFREG0644, st_size=87756, ...}) = 0 old_mmap(NULL, 87756, PROT READ, MAP PRIVATE, 4, 0)
= 0x40116000 close(4) = 0
fstat(l, {st_mode=S_IFCHR0620, st_rdev=makedev(136, 4),
...}) = 0
open(“test”, ORDONL Y 0_L ARGEFILE) = 4 fstat(4, {st_mode=S_IFREG0664, st_size=6, ...}) = 0

Наконец, команда cat открывает наш файл «test». Конечно, имя файла - это входные данные команды, но они безопасны для работоспособности команды из-за ее логики работы. В других случаях может потребоваться учет содержимого входного файла. read(4, “hello\n”, 512) = 6 write(l, “hello\n”, 6) = 6 read(4, 512) = 0 close(4) = 0 close(l) = 0 _exit(0) = ?

В заключение cat пытается прочитать 512 байтов из файла (читает 6) и выводит их на экран (который описан STDOUT с дескриптором файла 1). При повторной попытке прочитать очередные 512 байтов файла читается 0 байт, что свидетельствует о достижении конца файла. В результате файл закрывается, дескриптор файла освобождается и выполняется нормальный выход (признаком нормального выхода является нулевой код завершения). Для демонстрации читателю представляем очень простой пример. Логика работы команды cat очень проста и легко восстанавливается. На псевдокоде команду cat можно записать следующим образом: int count, handle string contents handle = open (argv[l]) while (count = read (handle, contents, 512)) write (STDOUT, contents, count) exit (0)

Для сравнения приведем результат выполнения утилиты truss для той же самой команды, выполненной в системе Solaris 7 на машине (х86):

execve(“/usr/bin/cat”, 0х08047Е50, 0х08047Е5С) arge = 2 open(“/dev/zero”, 0_RD0NLY)

= 3
mmap(0x00000000, 4096, PROT READPROT WRITEPROT EXEC,
MAP PRIVATE, 3, 0) = 0xDFBE1000 xstat(2, “/usr/bin/cat”, 0x08047BCC) = 0 sysconfig(CONFIGPAGESIZE) = 4096 open(‘7usr/lib/libc.so.l”, 0_RD0NLY)= 4 fxstat(2, 4, 0x08047A0C) = 0
mmap(0x00000000, 4096, PROT_READPROT_EXEC, MAP_PRIVATE,4,
0) = OxDFBDFOOO
mmap(0x00000000, 598016, PROT_READPROT_EXEC, MAP PRIVATE,
4, 0) = 0xDFB4C000
mmap(0xDFBD6000, 24392, PROT READPROT WRITEPROT EXEC, MAP_PRIVATEMAP_FIXED, 4, 561152) = 0xDFBD6000 mmap(0xDFBDC000, 6356, PROT READPROT WRITEPROT EXEC, MAP_PRIVATEMAP_FIXED,3, 0) = OxDFBDCOOO close(4) = 0
open(‘7usr/lib/libdl.so.l”, O RDONLY) = 4 fxstat(2, 4, 0x08047A0C) = 0

mmap(OxDFBDFООО, 4096, PROT_READPROT_EXEC,

MAP_PRIVATEMAP_FIXED, 4, 0) = OxDFBDFOOO close(4) = 0 close(3) = 0
sysi86(SI86FPHW, OxDFBDD8CO, 0x08047E0C, OxDFBFCEAO)
= 0x00000000 fstat64(l, 0x08047D80) = 0 open64(“test”, O RDONLY) = 3 fstat64(3, 0x08047CF0) = 0 llseek(3, 0, SEEKCUR) = 0
mmap64(0x00000000, 6, PROT READ, MAP SHARED, 3, 0)
= 0xDFB4A000 read(3, “ h”, 1)= 1
memcntl(0xDFB4A000, 6, MC_ADVISE, 0x0002, 0, 0) = 0
write(l, “hell o\n”, 6) = 6
llseek(3, 6, SEEK SET) = 6
munmap(0xDFB4A000, 6) = 0
llseek(3, 0, SEEK CUR) = 6
close(3) = 0
close(l) = 0
llseek(0, 0, SEEK CUR) = 296569 _exit(0)

Проанализировав конец протокола, можно заметить, что в Solaris команда cat выполняется несколько по-другому. Различие проявляется в том, что в Solaris ош использует проецируемый в память файл для передачи диапазона адресов непосредственно вызову функции write. Эксперимент с большим файлом (результаты которого здесь не приведены) выявил цикл запросов между вызовами функций memorymap/write, причем за один раз обрабатывается 256 Кб. Приведенная трассировка не раскрывает правил использования инструментария трассировки (хотя с этим и стоило бы познакомиться, но для этого потребовалось написать бы несколько глав). Скорее всего, приведенный пример демонстрирует некоторые факты, с помощью которых можно выяснить логику работы операционных систем в этой ситуации.

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

Дизассемблеры, декомпиляторы и отладчики

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

Декомпилятор (или дизассемблер) - программа, которая преобразует двоичный код программ в исходный текст, написанный на одном из языков программирования, чаще всего

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

Проблема несколько упрощается, если исследователь в состоянии разобраться с ассемблерным кодом, генерируемым декомпилятором. В этом случае декомпилятор особенно полезен. Рассмотрим пример результатов работы декомпилятора.

Среди коммерческих декомпиляторов для Windows хорошая репутация у IDA Pro компании DataRescue (пример работы декомпилятора показан на рис. 4.1). ША Pro может декомпилировать программный код многих процессоров, включая виртуальную машину Java.

Пример работы IDA Pro На рисунке показан пример применения декомпилятора ШA Pro для дизассемблирования программы pbrush

Рис. 4.1. Пример работы IDA Pro На рисунке показан пример применения декомпилятора ШA Pro для дизассемблирования программы pbrush.exe (Paintbrush). ША Pro нашел секцию внешних функций, используемых программой pbrush.exe. Если программа выполняется под управлением операционной системы, которая поддерживает разделяемые библиотеки (например, под управлением операционных систем Windows или UNIX), то она содержит список необходимых ей библиотек. Обычно этот список представлен в удобочитаемом виде, который легко обнаружить при экспертизе выполняемого кода. Для выполнения программ операционной системе также требуется этот список, поэтому она загружает его в память. В большинстве случаев это позволяет декомпилятору вставить список в двоичный код программы, сделав его более понятным. Чаще всего таблица соответствия имен pbrush.exe недоступна, поэтому в большей части сгенерированного декомпилятором ассемблерного кода отсутствуют имена.

Оценочную версию IDA Pro, пригодную для первоначального знакомства с программой, можно загрузить с www.datarescue.com/idabase/ida.htm. SoftlCE компании Numega - другой популярный отладчик. Дополнительные сведения о нем можно найти по адресу www.compuware.com/products/numega/drivercentral/.

Для сравнения была написана небольшая программа на языке С (классическая небольшая программа, выводящая строку «Hello World»). Для отладки использовался отладчик GNU (GDB). Код программы представлен ниже:

#include <stdio.h> int main ()
{
printf (“Hello World\n”); return (0);
}

Программа была скомпилирована с включением отладочной информации (был включен переключатель -g):

[elliptic@]$ gcc -g hello.с -о hello [elliptic@ellipse]$ ./hello

Hello World

Пример протокола отладки под управлением GDB показан ниже: [elliptic@ellipse]$ gdb hello GNU gdb 19991004 Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” for details.
This GDB was configured as “i386-redhat4inux”...
(gdb) break main

Была установлена точка прерывания при входе в функцию main. При ее достижении выполнение программы было приостановлено для выполнения программистом действий по отладке программы. Контрольная точка была установлена до выдачи команды run. Breakpoint 1 at 0x80483d3: file hello.с, line 5. (gdb) run Команда run начинает выполнение программы под управлением отладчика. Starting program: /home/ryan/hello Breakpoint 1, main () at hello.c:5

5 printf (“Hello World\n”);
(gdb) disassemble

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

Dump of assembler code for function main: 0x80483d0 <main>: push %ebp 0x80483dl <main+l>: mov %esp,%ebp 0x80483d3 <main+3>: push $0x8048440 0x80483d8 <main+8>: call 0x8048308 <printf>
0x80483dd <main+13>: add $0x4,%esp 0x80483eO <main+16>: xor %eax,%eax 0x80483e2 <main+18>: jmp 0x80483e4 <main+20>
0x80483e4 <main+20>: leave
0x80483e5 <main+21>: ret End of assembler dump.

Протокол отображает ассемблерный код программы «hello world», соответствующий ассемблеру х86 Linux. Исследование собственных программ в отладчике - хороший способ изучения листингов ассемблерного кода.

(gdb) s printf (format=0x8048440 “Hello World\n”) at printf.c:30 printf.c: No such file or directory.

После задания командой s («step») режима пошагового выполнения программы в отладчике выполняется переход к вызову функции printf. GDB сообщает о невозможности дальнейшей детализации функции printf из-за отсутствия в распоряжении отладчика исходного кода функции printf.

(gdb) s 31 in printf.c (gdb)s Hello World 35 in printf.c (gdb)с Continuing.

Далее выполняется несколько шагов реализации программы внутри функции printf и программа завершается. По команде с («continue») программа будет выполнена до следующей точки прерывания или будет завершена.

Program exited normally, (gdb)

В число аналогичных программ входят программы пт и objdump пакета GNU. Программа objdump позволяет управлять объектными файлами. Она применяется для отображения символов объектного файла, его заголовка и даже дизассемблирования. Nm выполняет аналогичные действия, что и objdump, позволяя просматривать символьные ссылки на объектные файлы.

Инструментарий и ловушки

Инструментарий никогда не заменит знаний

Некоторые из средств дизассемблирования и отладки предлагают фантастические возможности. Но они, как и любой другой инструментарий, несовершенны. Особенно это проявляется при анализе злонамеренного кода (вирусов, червей, Троянских коней) или специально разработанных программ. Обычно авторы подобных поделок делают все возможное для затруднения их анализа и снижения эффекта от применения отладчиков, дизассемблеров и других подобных инструментальных средств. Например, вирус RST для Linux в случае обнаружения, что он работает под управлением отладчика, завершает свою работу. Этот же вирус при инфицировании исполняемых и компонуемых файлов ELF (Executable and Linkable Format - формат исполняемых и компонуемых модулей) модифицирует их заголовки таким образом, что некоторые дизассемблеры не могут дизассемблировать двоичный код вируса (обычно это достигается путем размещения кода вируса в необъявленном программном сегменте). Часто злоумышленный код шифруется или сжимается, что защищает его от экспертизы. Черви Code Red распространялись половинками своих программ, маскируясь под строки переполнения, и, таким образом, формат их двоичных данных не подходил ни под один из стандартных заголовков файла.

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

Необходимо знать ассемблер не только в объеме, достаточном для чтения ассемблерных программ, но и для написания программ расшифровки или восстановления данных. Писать на ассемблере намного сложнее, чем читать программы, написанные на этом языке.

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

Значение экспертизы исходного текста программы | Защита от хакеров корпоративных сетей | Тестирование методом «черного ящика»


Защита от хакеров корпоративных сетей



Новости за месяц

  • Июнь
    2019
  • Пн
  • Вт
  • Ср
  • Чт
  • Пт
  • Сб
  • Вс