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

«Фундаментальные основы хакерства»

Пе­ред тобой уже во вто­рой раз обновлен­ная вер­сия цик­ла «Фун­дамен­таль­ные осно­вы хакерс­тва». В 2018 году Юрий Язев изме­нил текст Кри­са Кас­пер­ски для соот­ветс­твия новым вер­сиям Windows и Visual Studio, а теперь внес прав­ки с уче­том отладки прог­рамм для 64-раз­рядной архи­тек­туры.

Чи­тай так­же улуч­шенные вер­сии прош­лых ста­тей цик­ла:

  1. Учим­ся ана­лизи­ровать прог­раммы для x86-64 с нуля
  2. Ис­поль­зуем отладчик для ана­лиза 64-раз­рядных прог­рамм в Windows
  3. На­ходим реаль­ные адре­са инс­трук­ций в исполня­емых фай­лах x86-64

Все новые вер­сии ста­тей дос­тупны без плат­ной под­писки.

Цикл «Фун­дамен­таль­ные осно­вы хакерс­тва» со все­ми обновле­ниями опуб­ликован в виде кни­ги, ку­пить ее по выгод­ной цене ты можешь на сай­те изда­тель­ства «Солон‑пресс».

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

 

Способ 1. Прямой поиск введенного пароля в памяти

Па­роль, хра­нящий­ся в теле прог­раммы откры­тым тек­стом, — ско­рее из ряда вон выходя­щее исклю­чение, чем пра­вило. К чему услу­ги хакера, если пароль и без того виден нево­ору­жен­ным гла­зом? Поэто­му раз­работ­чики защиты вся­чес­ки пыта­ются скрыть его (о том, как имен­но они это дела­ют, мы погово­рим поз­же).

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

Да­вай подой­дем к решению проб­лемы от обратно­го — будем искать не исходный пароль, который нам неиз­вестен, а ту стро­ку, которую мы скор­мили прог­рамме в качес­тве пароля. А най­дя, уста­новим на нее бряк, и даль­ше всё точ­но так же, как и рань­ше. Бряк всплы­вает на обра­щение по срав­нению, мы выходим из срав­нива­ющей про­цеду­ры, кор­ректи­руем JMP и...

Взгля­нем еще раз на исходный текст лома­емо­го нами при­мера passCompare1.cpp:

for(;;)
{
printf("Enter password:");
fgets(&buff[0],PASSWORD_SIZE,stdin);
if (strcmp(&buff[0],PASSWORD))
printf("Wrong password\n");
else break;
if (++count>2) return -1;
}

Об­рати вни­мание — в buff чита­ется вве­ден­ный поль­зовате­лем пароль, срав­нива­ется с ори­гина­лом, затем (при неудач­ном срав­нении) зап­рашива­ется еще раз, но (!) при этом buff не очи­щает­ся! Отсю­да сле­дует, что, если пос­ле выдачи ругатель­ства «Wrong password» выз­вать отладчик и прой­тись по памяти кон­текс­тным поис­ком, мож­но обна­ружить тот завет­ный buff, а осталь­ное уже дело тех­ники!

Итак, прис­тупим (мы еще не зна­ем, во что мы ввя­зыва­емся, — но, увы, в жиз­ни все слож­нее, чем в теории). На этот раз запус­тим passCompare1.exe отдель­но от отладчи­ка. Затем под­клю­чим­ся к про­цес­су из отладчи­ка («Attach to process» в WinDbg). Обра­ти вни­мание: в окне выбора про­цес­са отоб­ража­ются все запущен­ные про­цес­сы и для каж­дого из них выводит­ся его раз­рядность в стол­бце Platform. Вво­дим любой при­шед­ший на ум пароль (нап­ример, KPNC Kaspersky++), про­пус­каем воз­мущен­ный вопль Wrong мимо ушей и в отладчи­ке нажима­ем Break (сочета­ние кла­виш Alt-Del).

Окно со списком процессов для выбора
Ок­но со спис­ком про­цес­сов для выбора

Поп­робу­ем отыс­кать в памяти вве­ден­ный пароль:

0:004> s -a 0x0 L? 0x7FFFFFFFFFF "KPNC Kaspersky"
 

Пояснения

Пер­вый параметр пос­ле коман­ды s — флаг -a — опре­деля­ет цель поис­ка как набор ASCII-сим­волов. Вто­рой параметр — сме­щение, по которо­му начать искать. Вооб­ще‑то начинать поиск с нулево­го сме­щения — идея глу­пая. Судя по кар­те памяти, здесь рас­положен слу­жеб­ный код и иско­мого пароля быть не может. Впро­чем, это ничему не вре­дит, и так гораз­до быс­трее, чем раз­бирать­ся, с какого адре­са заг­ружена прог­рамма и отку­да имен­но начинать поиск.

Тре­тий параметр — вер­хний пре­дел поис­ка, то есть докуда надо искать. Так как в 64-бит­ной Windows адресное прос­транс­тво про­цес­са огра­ниче­но 8 Тбайт, вер­хний лимит сос­тавля­ет 0x7FFFFFFFFFF. Пос­ледний параметр — собс­твен­но иско­мая стро­ка. Обра­ти вни­мание, что мы ищем не всю стро­ку, а толь­ко ее часть (KPNC Kaspersky++ про­тив KPNC Kaspersky). Это поз­воля­ет изба­вить­ся от лож­ных сра­баты­ваний, воз­ника­ющих из‑за ссы­лок на внут­ренние буфера.

 

Результат

0000002f10effe30 4b 50 4e 43 20 4b 61 73-70 65 72 73 6b 79 2b 2b KPNC Kaspersky++
000001dcd30f2580 4b 50 4e 43 20 4b 61 73-70 65 72 73 6b 79 2b 2b KPNC Kaspersky++

Це­лых два вхож­дения! Почему два? Пред­положим, что при чте­нии вво­да с кла­виату­ры сим­волы спер­ва попада­ют в сис­темный буфер, который и дает лож­ное сра­баты­вание. Тем не менее не ста­вить же, не разоб­равшись, сра­зу обе точ­ки оста­нова. В дан­ном слу­чае четырех отла­доч­ных регис­тров про­цес­сора хва­тит, а как быть, если бы мы наш­ли десяток вхож­дений? Да и в двух бря­ках немуд­рено заб­лудить­ся с неп­ривыч­ки! Как отфиль­тро­вать помехи?

 

Начинаем думать головой

На помощь при­ходит кар­та памяти — зная вла­дель­ца реги­она, которо­му при­над­лежит буфер, мож­но очень мно­гое ска­зать об этом буфере. Нас­коро набив уже зна­комую коман­ду !dh passCompare1, мы получим приб­лизитель­но сле­дующее (выб­раны све­дения толь­ко о сек­циях .data и .rdata):

SECTION HEADER #2
.rdata name
101C virtual size
2000 virtual address
1200 size of raw data
1400 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
(no align specified)
Read Only
SECTION HEADER #3
.data name
638 virtual size
4000 virtual address
200 size of raw data
2600 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
(no align specified)
Read Write

За­одно опре­делим базовый адрес модуля при­ложе­ния: lmf m passCompare1 (в моем кон­крет­ном слу­чае он равен 0x7ff7d78f0000, а у тебя зна­чение, ско­рее все­го, будет дру­гим). Узна­ем, куда в памяти заг­ружена сек­ция .rdata:

0x7ff7d78f0000 + 0x2000 == 0x7ff7d78f2000

И куда заг­ружена сек­ция .data:

0x7ff7d78f0000 + 0x4000 == 0x7ff7d78f4000

Это гораз­до выше най­ден­ных адре­сов рас­положе­ния буферов с вве­ден­ным паролем. Сле­дова­тель­но, най­ден­ные адре­са не ука­зыва­ют в области .data и .rdata.

Ду­маем даль­ше. Адрес 0x1dcd30f2580 выходит далеко за пре­делы лома­емо­го при­ложе­ния, и вооб­ще непонят­но, чему он при­над­лежит. Почесав затылок, мы вспом­ним о такой «вкус­ности» Windows, как куча (heap). С помощью коман­ды !heap пос­мотрим, где она начина­ется:

0:004> !heap
Heap Address NT/Segment Heap
1dcd30e0000 NT Heap
1dcd2fd0000 NT Heap

Из это­го зак­люча­ем, что адрес 0x1dcd30f2580 явно находит­ся в куче.

Раз­бира­емся даль­ше. Пос­коль­ку стек рас­тет свер­ху вниз (то есть от стар­ших адре­сов к млад­шим), адрес 0x2f10effe30 явно находит­ся в сте­ке. Уве­рен­ность подог­рева­ет тот факт, что боль­шинс­тво прог­раммис­тов раз­меща­ет буфера в локаль­ных перемен­ных, ну а локаль­ные перемен­ные, в свою оче­редь, раз­меща­ются ком­пилято­ром в сте­ке.

Ну что, поп­робу­ем пос­тавить бряк по пер­вому адре­су?

0:004> ba r4 0x2f10effe30
0:004> g

На вто­ром зап­росе пароля сно­ва вво­дим KPNC Kaspersky++. Жмем Enter и дожида­емся сиюми­нут­ной акти­вации отладчи­ка. Бряк про­изо­шел на вто­рой из этих строк:

00007ffb`5d3c4ffc 8806 mov byte ptr [rsi], al
00007ffb`5d3c4ffe 48ffc6 inc rsi
00007ffb`5d3c5001 4889742428 mov qword ptr [rsp+28h], rsi

Смот­рим, что находит­ся в регис­тре rsi:

dc rsi
0000002f`10effe30 434e504b 73614b20 73726570 2b2b796b KPNC Kaspersky++

Впро­чем, это­го и сле­дова­ло ожи­дать. Поп­робу­ем вый­ти из текущей фун­кции по Shift-F11. И мы сно­ва попадем на эту же стро­ку. Вновь пос­мотрим содер­жимое это­го регис­тра:

dc rsi
0000002f`10effe31 20434e50 7073614b 6b737265 0a2b2b79 PNC Kaspersky++

Ага, один сим­вол отку­сан. Сле­дова­тель­но, мы находим­ся в срав­нива­ющей про­цеду­ре. Вый­дем из нее нажати­ем на F5, так как при нажатии на Shift-F11 мы перей­дем на сле­дующую ите­рацию перебо­ра сим­волов.

00007ffb`5d37566e 77c8 ja ucrtbase!strcmp+0x8 (7ffb5d375638)
00007ffb`5d375670 488b01 mov rax, qword ptr [rcx]
00007ffb`5d375673 483b040a cmp rax, qword ptr [rdx+rcx]
00007ffb`5d375677 75bf jne ucrtbase!strcmp+0x8 (7ffb5d375638)

И вот мы в теле уже хорошо нам зна­комой (раз­вивай зри­тель­ную память!) про­цеду­ры срав­нения ори­гиналь­ного и вве­ден­ного поль­зовате­лем паролей. На вся­кий слу­чай для пущей убеж­деннос­ти выведем зна­чение ука­зате­лей [RDX+RCX] и RCX, что­бы узнать, что с чем срав­нива­ется:

0:000> dc [RDX+RCX]
00007ff7`d78f2280 4f47796d 6170444f 6f777373 000a6472 myGOODpassword..
0:000> dc RCX
0000002f`10effe30 434e504b 73614b20 73726570 2b2b796b KPNC Kaspersky++

Как раз то, что мы ищем!

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

 

Выводы

Итак, мы поз­накоми­лись с одним более или менее уни­вер­саль­ным спо­собом взло­ма защит, осно­ван­ных на срав­нении пароля (поз­же мы уви­дим, что он под­ходит и для защит, осно­ван­ных на регис­тра­цион­ных номерах). Его основное дос­тоинс­тво — прос­тота. А недос­татки... недос­татков у него мно­го:

  • ес­ли прог­раммист очис­тит буфера пос­ле срав­нения, поиск вве­ден­ного пароля ничего не даст, раз­ве что оста­нут­ся сис­темные буфера, которые так прос­то не зат­решь, но отсле­дить переме­щения пароля из сис­темных буферов в локаль­ные не так‑то лег­ко;
  • слу­жеб­ных буферов мно­го, и очень труд­но опре­делить, какой из них «нас­тоящий». Прог­раммист же может рас­полагать буфер и в сег­менте дан­ных (ста­тичес­кий буфер), и в сте­ке (локаль­ный буфер), и в куче, и даже выделять память низ­коуров­невыми вызова­ми типа VirtualAlloc или... да мало ли как разыг­рает­ся его фан­тазия. В резуль­тате под­час при­ходит­ся про­сеивать все най­ден­ные вхож­дения тупым перебо­ром.
 

Способ 2. Бряк на функции ввода пароля

 

Взлом приложения с GUI

Нас­тала пора раз­нооб­разить наш объ­ект взло­ма. Теперь поп­робу­ем заломить при­ложе­ние с гра­фичес­ким интерфей­сом. В качес­тве тре­ниров­ки раз­берем passCompare3. Это то же самое, что и passCompare1.exe, толь­ко с гра­фичес­ким интерфей­сом на осно­ве MFC Dialog Based App (ищи в ска­чива­емых матери­алах к статье).

Старый добрый MFC Application Wizard
Ста­рый доб­рый MFC Application Wizard

Так­же обра­ти вни­мание на то, что работа с тек­стом в этом при­мере орга­низо­вана по‑дру­гому. Если рань­ше мы работа­ли с базовым типом char, то здесь исполь­зует­ся обер­тка — класс CString, что, ско­рее все­го, при взло­ме про­фес­сиональ­ных при­ложе­ний будет встре­чать­ся нам чаще. Кро­ме двух кно­пок, иду­щих в заготов­ке по умол­чанию, добавь на фор­му эле­мент Edit Control. Свя­жи его с перемен­ной m_password и соз­дай событие обра­бот­ки нажатия на кноп­ке OK. Это и будет клю­чевая про­цеду­ра при­ложе­ния, про­веря­ющая вве­ден­ный пароль на равенс­тво эта­лон­ному:

const CString PASSWORD = _T("myGOODpassword");
void CpassCompare3Dlg::OnBnClickedOk()
{
CString str = NULL;
m_password.GetWindowText(str);
if (PASSWORD.Compare(str))
{
MessageBox(_T("Wrong password"));
m_password.SetSel(0, -1, 0);
return;
}
else
{
MessageBox(_T("Password OK"));
}
CDialogEx::OnOK();
}

Ка­жет­ся, никаких сюр­при­зов не пред­видит­ся.

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

На самом деле одно и то же дей­ствие может быть выпол­нено все­го лишь нес­коль­кими фун­кци­ями и их перебор не зай­мет мно­го вре­мени. В час­тнос­ти, содер­жимое окна редак­тирова­ния обыч­но добыва­ется при помощи либо фун­кции GetWindowTextW (чаще все­го), либо фун­кции GetDlgItemTextW (а это зна­читель­но реже). Все вер­сии Windows NT пред­почита­ют работать с юни­кодом, поэто­му на кон­це фун­кций работы с тек­стом W (wide), а не A (ASCII).

Раз уж речь заш­ла об окнах, запус­тим наш GUI «кряк­мис» и уста­новим точ­ку оста­нова на фун­кцию GetWindowTextW — bp User32!GetWindowTextW. Хотя эта фун­кция сис­темная, точ­ка оста­нова не будет гло­баль­ной и не зат­ронет все при­ложе­ния в сис­теме, а будет фун­кци­они­ровать толь­ко в кон­тек­сте дан­ного при­ложе­ния.

Вво­дим какой‑нибудь пароль (KPNC Kaspersky++, по обык­новению), нажима­ем кла­вишу Enter, и отладчик незамед­литель­но всплы­вает:

USER32!GetWindowTextW:
00007ffc`a2d7c2f0 48895c2408 mov qword ptr [rsp+8], rbx
00007ffc`a2d7c2f5 4889742418 mov qword ptr [rsp+18h], rsi
00007ffc`a2d7c2fa 57 push rdi
00007ffc`a2d7c2fb 4156 push r14
00007ffc`a2d7c2fd 4157 push r15
00007ffc`a2d7c2ff 4883ec60 sub rsp, 60h
00007ffc`a2d7c303 4d63f0 movsxd r14, r8d
00007ffc`a2d7c306 488bf2 mov rsi, rdx
00007ffc`a2d7c309 4c8bc9 mov r9, rcx
00007ffc`a2d7c30c 4885d2 test rdx, rdx

Как вид­но, мы попали в фун­кцию USER32!GetWindowTextW. Из нее надо вый­ти на более высокий уро­вень, нажав Shift-F11. Теперь мы попали в фун­кцию mfc140u!CWnd::GetWindowTextW:

00007ffc`3da173ca ff1550ca0300 call qword ptr [mfc140u!__imp_GetWindowTextW (7ffc3da53e20)]
00007ffc`3da173d0 488b0b mov rcx, qword ptr [rString{->m_pszData} (rbx)]
00007ffc`3da173d3 4885c9 test psz (rcx), psz (rcx)

Те­перь надо еще пот­расси­ровать эту фун­кцию нажати­ями Shift-F11. Наконец, мы попадем в фун­кцию, которая явля­ется обра­бот­чиком нажатия кноп­ки OK на фор­ме или Enter на кла­виату­ре:

passCompare3!CpassCompare3Dlg::OnBnClickedOk
00007ff6`daae1746 488d8b78010000 lea rcx, [this->m_password(??) (rbx+178h)]
00007ff6`daae174d 488d542420 lea rdx, [str{.m_pszData} (rsp+20h)]
00007ff6`daae1752 ff15581c0000 call qword ptr [passCompare3!__imp_?GetWindowTextW@CWnd@@QEBAXAEAV?$CStringT@_WV?$StrTraitMFC_DLL@_WV?$ChTraitsCRT@_W@ATL@@@@@ATL@@@Z (7ff6daae33b0)]
00007ff6`daae1758 488b542420 mov rdx, qword ptr [str{.m_pszData} (rsp+20h)]
00007ff6`daae175d 488d0d14630000 lea rcx, [passCompare3!PASSWORD{.m_pszData} (7ff6daae7a78)]
00007ff6`daae1764 ff154e1c0000 call qword ptr [passCompare3!__imp_?Compare@?$CStringT@_WV?$StrTraitMFC_DLL@_WV?$ChTraitsCRT@_W@ATL@@@@@ATL@@QEBAHPEB_W@Z (7ff6daae33b8)]

Сей­час мы можем узнать зна­чение в регис­тре RAX:

0:000> dc rax
000001a3`6e4405b8 0050004b 0043004e 004b0020 00730061 K.P.N.C. .K.a.s.
000001a3`6e4405c8 00650070 00730072 0079006b 002b002b p.e.r.s.k.y.+.+.

Хо­рошо, видим вве­ден­ный пароль. Есть кон­такт! Толь­ко почему пос­ле каж­дого сим­вола сто­ит точ­ка? Думаю, ты уже догадал­ся, что она озна­чает двух­бай­товую при­роду сим­вола перед ней. Отхлеб­нув пив­ка, ква­са или лимона­да (по желанию), вспо­мина­ем, что, хоть класс CString и может работать с типами char (одно­бай­товое пред­став­ление сим­волов) и wchar_t (мно­гобай­товое пред­став­ление до четырех байт, то есть юни­код в UTF-8, -16 или -32), это зависит от нас­тро­ек ком­пилято­ра. А имен­но от того, какой сим­вол вклю­чен: MBCS — char, UNICODE — wchar_t. Чаще все­го исполь­зует­ся вто­рой набор сим­волов, так как по умол­чанию вклю­чены имен­но широкие сим­волы.

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

mfc140u!ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t> > >::Compare:
00007ffc`3d78dcf0 4883ec28 sub rsp, 28h
00007ffc`3d78dcf4 4885d2 test psz (rdx), psz (rdx)
00007ffc`3d78dcf7 740e je mfc140u!ATL::CStringT<wchar_t, StrTraitMFC_DLL<wchar_t, ATL::ChTraitsCRT<wchar_t> > >::Compare+0x17 (7ffc3d78dd07)
00007ffc`3d78dcf9 488b09 mov this (rcx), qword ptr [this (rcx)]
00007ffc`3d78dcfc 4883c428 add rsp, 28h
00007ffc`3d78dd00 48ff25a16a2c00 jmp qword ptr [mfc140u!__imp_wcscmp (7ffc3da547a8)]
00007ffc`3d78dd07 b905400080 mov ecx, 80004005h
00007ffc`3d78dd0c e8bb56ffff call mfc140u!ATL::AtlThrowImpl (7ffc3d7833cc)
…………

Об­рати вни­мание вот на этот опе­ратор из лис­тинга:

mov this (rcx), qword ptr [this (rcx)]

Пос­ле его выпол­нения зна­чение в регис­тре RCX будет ука­зывать на буфер с эта­лон­ным паролем:

0:000> dc rcx
00000247`160d3ae8 0079006d 004f0047 0044004f 00610070 m.y.G.O.O.D.p.a.
00000247`160d3af8 00730073 006f0077 00640072 00610000 s.s.w.o.r.d...a.

И прав­да! Инту­иция нас не под­вела, эта­лон­ный пароль тут как тут.

Работа приложения
Ра­бота при­ложе­ния

Вве­ден­ная поль­зовате­лем стро­ка и эта­лон­ный пароль — как на блю­деч­ке с голубой каемоч­кой! Замеча­тель­но! Вот так, безо вся­ких лож­ных сра­баты­ваний, эле­ган­тно, быс­тро и кра­сиво, мы победи­ли защиту!

Этот спо­соб уни­вер­сален, и впос­ледс­твии мы еще не раз им вос­поль­зуем­ся. Вся соль — опре­делить клю­чевую фун­кцию защиты и пос­тавить на нее бряк. В Windows все попол­зно­вения (обра­щения к клю­чево­му фай­лу, реес­тру и про­чее) сво­дят­ся к вызову фун­кций API, перечень которых хоть и велик, но все же конечен и известен заранее.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии