Linux kernel: Пособие по эксплуатации переполнения кучи (по идее часть 1)
Предисловие⚙️
Короче говоря, друзья, сегодня мы разберём крайне увлекательный вопрос: как грамотно взломать кучу в ядре Linux? И не просто взломать, а как настоящий джедай ядра (да, да, именно ядра, а не тех, кто хвалится sudo apt install htop).
Статья построена на базе одной задачки, так что считайте, мы будем учиться на практике. Методов много — их не уместишь в один пост, поэтому готовься, материал будет выходить в нескольких частях. Это не Netflix, но будет трилогия!
Перед тем как ворваться с боевым пингвином в мир уязвимостей, полезно освежить в памяти, что такое куча и как она вообще устроена. Если ты вдруг новичок — открой Википедию, посмотри пару графиков и найди где-нибудь "введение в ядро кучи". Это как документация по C++, но читать чуть легче.
Обзор📜
Значит так, система управления памятью в Linux выглядит как бабушка, сортирующая картошку: память делится на три уровня:
- Страницы (page) — это базовый уровень.
- Зоны (zone) — чуть выше, распределяет страницы по нуждам.
- Узлы (node) — верхний слой, объединяющий все зоны для любителей многопроцессорных серверов и бубнов.
В главных ролях у нас сегодня два знаменитых менеджера памяти:
- 🪙 Buddy system — менеджер, отвечающий за раздачу памяти целыми страницами. Он, как строгий админ, просто кидает куски памяти налево и направо по принципу «на держи, только не разбазарь».
- 🎛️ Slub allocator — более утончённый менеджер, который сидит на buddy system-е и говорит: «Не-не-не, одну страницу на мелкие объекты порежем! Давай как в IKEA: каждому объекту своё отделение».
Slub работает с объектами и позволяет управлять памятью на микроуровне, чтобы каждому процессику хватило места и никто не жаловался на переполнения.
Buddy System: Где мой "бадди" для страниц?🛠️
Buddy system — это такой менеджер памяти, который работает с целыми страницами (page), и если бы у него был девиз, он бы звучал как: "Делим и властвуем, но всегда парными кусками!".
Каждая зона (zone) в ядре Linux содержит массив структур free_area, которые управляют страницами по принципу "order".
🧙 Order — это степень двойки, по которой определяется размер блока страниц. Например:order 0 = 1 страница, order 1 = 2 страницы, order 2 = 4 страницы и так далее...Как это всё работает?🔍
🧩 Аллокация (выделение памяти):
- Сначала buddy system округляет размер запроса до ближайшей степени двойки страниц. Если ты просишь 3 страницы — держи 4 (всё как у продакшн-разработчиков: «запросишь одно — получишь с запасом»).
- Затем идём в нужный "order" и пытаемся вытащить блок из соответствующего списка.
- Если в списке пусто (кто-то всё уже растащил), система делает следующее:Берёт блок из следующего order (более крупный).
- Разрезает его пополам ("cut here", как сказали бы в GNU).
- Один кусок отдаёт запрашивающему процессу, второй кладёт в текущий order, как добропорядочный менеджер склада.
- Если и в следующем order пусто, то процесс продолжается на более высоких уровнях. Buddy system лезет всё выше, пока не найдёт подходящий блок.
Короче говоря, это как раздача пиццы на вечеринке: если нет кусочка на одного, придётся резать большую пополам. 🍕
Освобождение памяти:🗑️
- Когда ты возвращаешь блок памяти (а ты же возвращаешь, правда? 😏), buddy system аккуратно кладёт его в список свободных блоков текущего order.
- Затем он начинает проверять:
- «А не завалялся ли тут свободный "бадди", чтобы можно было объединить блоки и вернуть их в более крупный order?»
- Если такой "бадди" найден (то есть страница рядом тоже свободна), блоки объединяются и переходят на следующий уровень (order выше).
- Этот процесс повторяется, пока не удастся объединить страницы по максимуму.
📦 Вывод для IT-шников:
Buddy system — это как Tetris для памяти:
- Выделение памяти: ищем блок, режем при необходимости, возвращаем результат.
- Освобождение памяти: кладём на место, проверяем соседей, сливаем куски.
И да, куча свободных блоков — это не повод радоваться. Если рядом не будет "бадди", память так и останется в фрагментированном состоянии. Как сказал бы старший сисадмин:
«Оптимизация — это искусство собирать всё в кучу, но не по частям».
🧵 SLUB Allocator: Разделяй и властвуй на микроуровне
SLUB allocator — это более продвинутая и стильная версия старого доброго slab allocator. Если buddy system был мясником, рубящим страницы на крупные куски, то SLUB — это ювелир памяти, который всё кромсает на объекты одинакового размера.
Затем аккуратно режет их на объекты и раздаёт эти кусочки для мелких операций.Важно: SLUB не разбазаривает память, как твой коллега на премию, а держит всё структурированно и по полочкам.
🚀 Аллокация (выделение объектов):
- Первым делом SLUB идёт в
kmem_cache_cpu— локальный кэш текущего CPU: - Если там есть свободный объект, его сразу возвращают. Всё быстро и просто, никакой суеты.
- Что делать, если кэш пуст?
- SLUB снимает текущий блок (slub) с
kmem_cache_cpuи говорит: «Хорошо, что у нас есть план Б!» - Пытается вытащить слегка заполненный блок из partial-цепочки (
partial list) вkmem_cache_node.Partial list — это такой склад, где живут блоки с частично свободными объектами. - Подключив новый SLUB-блок, SLUB вытаскивает из него свободный объект и возвращает его наверх.
- А если partial-цепочка тоже пустая?
- Тогда SLUB идёт к buddy system как к старшему брату: «Дай ещё страниц!».
- Получив страницу, SLUB режет её на объекты, загружает в
kmem_cache_cpu, и всё снова работает, как часы.
🗑️ Освобождение памяти (возврат объектов):
- Если освобождаемый объект находится в
kmem_cache_cpu: - Объект возвращается в локальный список свободных объектов (
freelist) методом «головой вперёд» — то есть ставится в начало списка. - Если освобождаемый объект из partial-цепочки в
kmem_cache_node: - Объект также добавляется в начало freelist в соответствующем SLUB-блоке.
- Если освобождаемый объект находится в full slub (полностью заполненном блоке):
- Этот объект становится головой freelist.
- Блок переводится из статуса full в partial и возвращается в partial-цепочку
kmem_cache_node.
📦 Подытожим, как это выглядит:
- SLUB умный: сначала берёт из локального CPU-кэша, если там пусто — ищет среди частично заполненных блоков.
- Если и там пусто — идёт к buddy system и просит новые страницы.
- Возвращение объектов — тоже строго по правилам: всегда в начало списка, чтобы всё было быстро и эффективно.
🎩 Мудрость для IT-шника:
SLUB allocator — это как менеджер на складе Амазона: всё упорядочено, оптимизировано и готово к мгновенной доставке. Система думает быстрее, чем ты успеешь сказать «malloc»! 🚀
heap_bof: Разбор полётов ✈️
Итак, у нас тут модуль heap_bof, который, судя по названию, обещает нам кучу боли и переполнение. Автор сего чуда, exp_ttt, видимо, большой любитель острых ощущений! 🌶️
Нам дали исходники, и там прямым текстом написано: UAF и heap overflow. Это как если бы на двери было написано: "Входите, грабьте, у нас всё равно всё плохо". Ну и вишенка на торте 🍒 — ядро старое, 4.4.27. Прямо музейный экспонат!
- ptr[40]: Массив из 40 указателей, которые могут указывать куда угодно. Это наша песочница для игр с памятью.
- struct param: Структура, через которую мы будем общаться с юзерспейсом. Поля len, buf и idx — это наши главные инструменты.
- bof_ioctl: Функция, которая обрабатывает команды из юзерспейса. Тут-то и кроется всё веселье!
- case 9: Копируем данные из ядра (из ptr[p_arg.idx]) в юзерспейс (p_arg.buf). Размер — p_arg.len. Если ptr[p_arg.idx] уже освободили, а мы пытаемся его прочитать, то это UAF. Ба-бах! 💥
- case 8: Копируем данные из юзерспейса (p_arg.buf) в ядро (ptr[p_arg.idx]). Размер — p_arg.len. Если записать больше, чем выделили, то это heap overflow. Ещё один ба-бах! 💥💥
- case 7: Освобождаем память по адресу ptr[p_arg.idx]. Но сам указатель не обнуляем! Привет, UAF!
- case 5: Выделяем память размером p_arg.len и сохраняем указатель в ptr[p_arg.idx].
Короче говоря, этот модуль — просто кладезь уязвимостей! 🎉 Можно и память попереполнять, и почитать уже освобождённую память. Автор явно перестарался, пытаясь написать что-то "интересное".
Ага, вот и скрипт запуска подъехал! 🚚 boot.sh — это наш пропуск в мир боли и страданий, который приготовил нам автор этого безобразия.
boot.sh: Коротко о главном
- qemu-system-x86_64: Запускаем QEMU, эмулятор, который будет нашим полигоном.
- -initrd rootfs.cpio: Это наша файловая система, которую QEMU подсунет ядру.
- -kernel bzImage: А это само ядро Linux, которое мы будем мучать.
- -m 512M: Выделяем 512 МБ оперативной памяти. Не густо, но нам хватит.
- -nographic: Запускаем без графического интерфейса. Мы же суровые консольные ниндзя! 🥷
- -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr': Параметры ядра:
- console=ttyS0: Вывод консоли на последовательный порт (чтобы мы видели, что там происходит).
- root=/dev/ram: Корневая файловая система в RAM.
- oops=panic: Если ядро словит Oops (ошибку), то сразу паникуем.
- panic=1: Перезагрузка через 1 секунду после паники. Чтобы не расслаблялись!
- quiet: Поменьше болтовни в консоли.
- kaslr: Включаем рандомизацию адресного пространства ядра. Это чтобы жизнь мёдом не казалась! 🍯
- -monitor /dev/null: Монитор QEMU нам не нужен, отправляем его в /dev/null.
- -smp cores=2,threads=2: Включаем многоядерность и многопоточность: 2 ядра, 2 потока на каждое. То есть всего 4 потока. Это важно! Нам же сказали, что задача многопоточная.
- -cpu kvm64,+smep,+smap: Включаем плюшки процессора:
- kvm64: Используем аппаратную виртуализацию KVM.
- +smep: Включаем SMEP (Supervisor Mode Execution Prevention). Это значит, что ядро не сможет выполнять код из юзерспейса. Облом для нас! 😥
- +smap: Включаем SMAP (Supervisor Mode Access Prevention). Теперь ядро не сможет ещё и читать данные из юзерспейса. Совсем грустно! 😭
Нам подкинули задачку с многоядерным ядром, включенным KASLR, SMEP и SMAP. Автор явно хочет, чтобы мы страдали! 😭 Но мы не сдаёмся! 💪 Будем искать обходные пути и ломать эту систему, как орешки! 🌰
Так, у нас тут нарисовался план по захвату мира с помощью UAF! 🌍 Давайте разберёмся, что тут к чему.
Kernel Use After Free: План действий 📝
Нам предлагают использовать уязвимость Use After Free (UAF) в ядре, чтобы поднять свои привилегии до рута. Звучит заманчиво! 😏
- cred структура: В ядре Linux есть структура cred, которая хранит информацию о привилегиях процесса. Размер этой структуры — 0xa8 байт.
- SLUB allocator: Когда мы выделяем память в ядре с помощью kmalloc, за кулисами работает SLUB аллокатор. Он выделяет память блоками определённых размеров. Если мы выделим блок размером 0xa8 (реальный размер с учётом метаданных будет 0xc0), то он попадёт в кеш kmalloc-192.
- Магия UAF: Если мы выделим блок размером 0xa8, а потом освободим его (kfree), он вернётся в кеш kmalloc-192. Но! Если в этот момент создать новый поток, то его структура cred может быть выделена как раз из этого освобождённого блока! 🤯
- Профит! Если мы сможем записать что-то в этот освобождённый блок (а у нас ведь есть уязвимость UAF!), то мы перезапишем данные в структуре cred нового потока. И если мы перезапишем UID на 0 (UID рута), то получим рутовые привилегии! 🎉
- Выделяет блок памяти в ядре размером 0xa8 байт.
- Освобождает этот блок.
- Создаёт новый поток.
- В дочернем потоке записывает нули в освобождённый блок, надеясь перезаписать cred и получить UID 0.
- Проверяет, получилось ли стать рутом.
Этот эксплоит работал раньше, но в новых версиях ядра (начиная с 4.5) его пофиксили. Теперь при создании кеша cred_jar (где хранятся структуры cred) устанавливается флаг SLAB_ACCOUNT. Из-за этого cred_jar больше не объединяется с kmalloc-192, и мы не можем выделить cred в том же месте, где был наш освобождённый блок.
План был хорош, но устарел. 🙁 В новых ядрах этот трюк не пройдёт. Придётся искать другие способы обхода защиты. Но сама идея использования UAF для перезаписи cred — это классика! 👍
А вот и второй раунд! 🥊 Теперь попробуем пробить защиту с помощью heap overflow.
Heap Overflow: Старая школа 🏫
Идея та же, что и с UAF: перезаписать структуру cred, чтобы получить рута. Но вместо UAF используем переполнение буфера в куче.
Как и в случае с UAF, этот способ тоже устарел для новых ядер из-за флага SLAB_ACCOUNT у cred_jar. Но, допустим, у нас старое ядро, где этот трюк ещё работает.
В многопоточной среде (а у нас именно такая, судя по boot.sh) блоки в куче могут выделяться и освобождаться в непредсказуемом порядке. Это значит, что после освобождения блока размером 0xa8 (наш кандидат на перезапись cred) следующий выделенный блок такого же размера может оказаться совсем не там, где мы ожидаем. 😩
Чтобы увеличить шансы на успех, нужно "выровнять" кучу. Для этого выделим много блоков размером 0xa8 перед тем, как делать что-то важное. Это заполнит "дыры" в kmalloc-192 и сделает расположение блоков более предсказуемым.
- Подготовка: Выделяем кучу блоков размером 0xa8, чтобы "очистить" kmalloc-192 от старых освобождённых блоков.
- Выделение: Выделяем ещё 10 блоков размером 0xa8.
- Освобождение: Освобождаем блок с индексом 5 (ptr[5]).
- Fork: Создаём новый поток. Надеемся, что его cred попадёт в освобождённый нами блок.
- Переполнение: Пишем в блок с индексом 4 (ptr[4]) данные размером больше, чем 0xa8. Если повезёт, мы затрём начало структуры cred следующего блока (который, как мы надеемся, принадлежит нашему новому потоку).
- Профит? Проверяем, стали ли мы рутом в дочернем потоке.
- Открывает устройство /dev/bof.
- Выделяет много (0x40) блоков по 0xa8 байт, чтобы "выровнять" кучу.
- Выделяет ещё 10 блоков по 0xa8 байт.
- Освобождает ptr[5].
- Создаёт новый поток.
- В родительском потоке переполняет ptr[4], записывая туда 0xc0 + 0x30 байт нулей.
- В дочернем потоке проверяет, стал ли он рутом.
Этот способ тоже основан на устаревшем трюке, который не работает в новых ядрах из-за SLAB_ACCOUNT. Но даже в старых ядрах он ненадёжен из-за непредсказуемости поведения аллокатора в многопоточной среде. Шансы на успех есть, но они невелики.
Опа, а вот и новый поворот! 🔀 Нам предлагают отвлечься от cred и поиграться с tty_struct. А ещё автор смилостивился и отключил SMEP в boot.sh. Прямо праздник какой-то! 🎉
tty_struct: Что это за зверь?
tty_struct — это структура в ядре Linux, которая представляет терминальное устройство (TTY). Она содержит кучу всего интересного, в том числе указатели на функции, которые вызываются при различных операциях с TTY (например, при чтении и записи).
Если мы сможем перезаписать указатели на функции в tty_struct, то сможем заставить ядро выполнить наш код! 😎
- -s: Добавили флаг -s. Это шорткат для -gdb tcp::1234. Теперь QEMU будет ждать подключения GDB на порту 1234. Удобно для отладки! 🐞
- -cpu kvm64: Убрали +smep и +smap. SMEP отключили (ура!), SMAP остался (ну и ладно, он нам не сильно мешает в данном случае).
- -smp cores=1,threads=1: Теперь у нас одно ядро и один поток. Это упрощает эксплуатацию, так как не нужно беспокоиться о гонках.
Вероятно, потому что в ядре мало гаджетов, которые можно было бы использовать для обхода SMEP. Без SMEP мы можем просто поместить шеллкод в юзерспейс и прыгнуть на него.
План действий (предварительный или нет):
- Найти способ выделить блок памяти, который потом будет использоваться как tty_struct.
- Найти способ перезаписать этот блок своими данными, включая поддельные указатели на функции.
- Вызвать одну из функций, на которую мы теперь указываем, чтобы выполнить свой код.
- Поднять привилегии до рута и запустить шелл.
Появилась новая зацепка — tty_struct. Отключение SMEP упрощает задачу, но расслабляться рано. Впереди ещё много работы! 💪
tty_struct: План взлома 💣
Вспоминаем, что tty_struct — это структура, представляющая терминальное устройство. В ней есть поле ops, которое указывает на таблицу функций tty_operations, ответственных за операции с TTY. Наша задача — подменить эту таблицу на свою.
- Выделяем блок памяти: Когда мы открываем устройство /dev/ptmx (псевдотерминал), ядро выделяет память под tty_struct.
- Находим Размер tty_struct — 0x2e0 байт. В начале структуры есть магическое число 0x5401. По нему можно определить, что мы нашли именно tty_struct.
- Подменяем Поле ops находится по смещению 0x18 от начала tty_struct. Записываем туда адрес нашей поддельной таблицы tty_operations.
- Триггерим вызов: Вызываем ioctl для псевдотерминала. Это приведёт к вызову функции из нашей поддельной таблицы.
- Выполняем код: В поддельной таблице tty_operations указываем адрес функции get_root, которая поднимет нам привилегии.
- Profit! 🎉
- get_shell: Просто запускает /bin/sh.
- save_status: Сохраняет регистры пользовательского режима, чтобы потом корректно вернуться из ядра.
- get_root:
- Определяет смещение KASLR, украв адрес из стека.
- Вычисляет реальные адреса commit_creds и init_cred.
- Вызывает commit_creds(init_cred), чтобы получить привилегии рута.
- Использует iretq для возврата в юзерспейс с изменённым контекстом (регистрами), чтобы запустить get_shell.
- Выделяет блок размером 0x2e0 через наше устройство.
- Заполняет его мусором (0xff) и освобождает.
- Открывает /dev/ptmx, что приводит к выделению tty_struct в том же месте.
- Читает данные из этого блока, чтобы найти tty_struct (по магическому числу 0x5401).
- Подменяет указатель ops на адрес fake_tty_ops.
- Вызывает ioctl для /dev/ptmx, что приводит к вызову get_root из нашей поддельной таблицы.
- get_root повышает привилегии и возвращается в юзерспейс, где запускается шелл от рута.
- KASLR обходится за счёт утечки адреса из стека.
- SMEP отключен, поэтому мы можем прыгнуть на код в юзерспейсе (get_root).
- SMAP не мешает, так как мы не читаем и не пишем данные в юзерспейсе из ядра.
Мы успешно использовали tty_struct для повышения привилегий! 🎉 Этот способ обходит защиту SLAB_ACCOUNT, которая мешала нам использовать cred в новых ядрах.
seq_operations: Что это и с чем его едят? 🍽️
seq_operations — это структура, которая используется в ядре Linux для работы с файлами в директории /proc. Она содержит указатели на функции, которые отвечают за чтение данных из этих файлов.
Если мы сможем подменить указатели в seq_operations, то при чтении определённого файла в /proc мы сможем выполнить свой код в контексте ядра! 🤯
- Найти Нужно найти способ выделить блок памяти, который затем будет использоваться как seq_operations для какого-нибудь файла в /proc.
- Подменить указатели: Записать в этот блок адреса своих функций.
- Триггернуть вызов: Прочитать файл, связанный с нашей поддельной seq_operations. Это приведёт к вызову одной из функций, на которые мы указали.
- Выполнить код: В одной из поддельных функций повысить привилегии и запустить шелл.
boot.sh (без изменений)
- seq_operations может использоваться для большего количества файлов, чем tty_struct (которая связана только с TTY). Это даёт больше возможностей для атаки.
- Сложнее найти подходящий файл в /proc, который использует seq_operations и при этом позволяет нам перезаписать его структуру.
seq_operations — это ещё один перспективный вектор атаки. 👍 Нужно провести разведку и выяснить, как его можно использовать в нашем случае.
seq_operations: План по шагам 👣
- seq_operations и /proc/self/stat: Структура seq_operations используется, в частности, для файла /proc/self/stat, который содержит информацию о текущем процессе. Когда мы открываем этот файл, ядро выделяет память под seq_operations из кеша kmalloc-32.
- Размер имеет значение: Размер seq_operations — 32 байта (0x20). Это как раз тот размер, который мы можем выделить с помощью нашего драйвера heap_bof.
- Переполнение: Если мы выделим много блоков по 32 байта, а затем откроем много файлов /proc/self/stat, то с большой вероятностью один из seq_operations окажется рядом с одним из наших блоков. И если мы сможем переполнить наш блок, то сможем перезаписать seq_operations!
- start — наше всё: При чтении файла /proc/self/stat вызывается функция start из seq_operations. Если мы подменим указатель на start, то сможем выполнить свой код.
- Обход защиты:
- SMEP отключен, поэтому мы можем прыгнуть на код в юзерспейсе.
- SMAP и KPTI тоже отключены, так что с этим проблем нет.
- KASLR обойдём, украв адрес из стека (как и в случае с tty_struct).
- Выделить много блоков по 32 байта с помощью heap_bof.
- Открыть много файлов /proc/self/stat.
- Найти блок, который переполнился, и перезаписать seq_operations, подменив указатель start на адрес нашей функции повышения привилегий в юзерспейсе.
- Прочитать /proc/self/stat, чтобы вызвать нашу функцию.
- В функции повышения привилегий:
- Обойти KASLR, найдя адрес в стеке.
- Вызвать commit_creds(init_cred), чтобы стать рутом.
- Вернуться в юзерспейс и запустить шелл.
- Не нужно угадывать, где окажется cred (как в старых методах с kmalloc-192).
- Достаточно надёжный способ, так как мы контролируем выделение памяти и можем увеличить шансы на успех, выделив много блоков.
- Всё ещё требуется переполнение буфера, что может быть не так просто реализовать.
- Нужно много раз открывать /proc/self/stat, что может выглядеть подозрительно.
План выглядит многообещающе! 🎉 Использование seq_operations — это более современный и надёжный способ эксплуатации по сравнению со старыми методами, основанными на cred.
Вот и сам эксплоит! 🔥 Давайте разберём его по косточкам.
- get_shell, save_status, get_root: Эти функции аналогичны тем, что были в эксплоите с tty_struct.
- main:
- Открывает устройство /dev/bof.
- Выделяет 0x40 блоков по 0x20 байт.
- Заполняет последний выделенный блок значением 0xff - вероятно, это нужно, чтобы сломать какие-то метаданные, обеспечив нужное место для записи в seq_operations.
- Открывает 0x200 (512) файлов /proc/self/stat. Это приводит к выделению seq_operations в kmalloc-32.
- Выделяет буфер p.buf размером DATA_SIZE (0x20 * 8 = 0x100 байт) и заполняет его адресом get_root.
- Переполняет ptr[0], записывая туда DATA_SIZE байт. Так как ptr[0] был выделен последним из 0x40 блоков, и с большой вероятностью где-то рядом расположены seq_operations, то начнётся перезапись данных за пределами ptr[0].
- Читает из каждого открытого файла /proc/self/stat, что приводит к вызову m->op->start(m, &pos). Если мы успешно перезаписали seq_operations, то вместо start вызовется get_root.
- get_root повышает привилегии и возвращается в юзерспейс, вызывая get_shell, которая, в свою очередь, запускает /bin/sh.
- Спрей Открытие большого количества файлов /proc/self/stat увеличивает шансы на то, что seq_operations окажется рядом с нашим переполняемым блоком.
- Переполнение: Запись DATA_SIZE байт вместо 0x20 приводит к переполнению и перезаписи соседних данных в куче.
- Обход KASLR: Функция get_root крадёт адрес из стека, чтобы вычислить смещение KASLR.
Эксплоит использует seq_operations для повышения привилегий, обходя защиту SLAB_ACCOUNT, которая мешала использовать cred в новых ядрах. 🎉 Это более современный и, вероятно, более надёжный метод по сравнению с tty_struct, так как не требует угадывать местоположение tty_struct в памяти.
ПРОДОЛЖЕНИЕ БУДЕТ, НИКТО НЕ ЗНАЕТ КОГДА, НО ОБЕЩАЮ ВЕРНУТСЯ...