Любите ли вы отзывчивые программы так, как люблю их я? Любовь эта привела меня к Колибри ОС - невероятно шустрой операционной системе, которая запускает программу до того, как вы осознаете, что кликнули по ней. И недавно у неё нашли уязвимость: ping of death.
Так получилось, что моя первая работа была связана с симуляцией компьютерных систем – от серверов до мобильных устройств. И там мы использовали симулятор Simics. Этой системой пользуются крупные производители железа для опережающей разработки драйверов.
Если бы только можно было использовать Simics для отладки любительской ОС...

Часть 0: Проблема
Баг описал пользователь turbocat на форуме: достаточно большой ICMP echo-запрос может убить один из процессов операционной системы или полностью вывести её из строя. Для воспроизведения проблемы нужно пингануть машину с запущенной системой Колибри. А для этого потребуется два компьютера, подключенных к одной локальной сети.
Если бы только Simics мог симулировать компьютерные сети…
Часть 1: Воспроизведение
Симуляция в Simics настраивается с использованием simics-скриптов, которые создают и настраивают устройства. Для воспроизведения можно создать свой скрипт: «targets/linux-kolibri.simics».
Для начала нужна машина с Linux на борту. Это самая простая часть, такая машина включена в базовый дистрибутив Simics, и её можно создать, используя скрипт из пакета «simics-qsp-x86». Он даже сеть автоматически может настроить! Но здесь это не нужно:
$create_network = FALSE run-command-file "targets/qsp-x86/qsp-clear-linux.simics" machine_name = linux
Этот скрипт уже можно запустить командой simics linux-kolibri.simics и введя continue в командной строке Simics (изначально с��муляция находится в состоянии паузы). После этого видим графику и консоль Linux:

Ещё нужен ПК с установленной Колибри. Его можно создать тем же скриптом, нужно только указать свой образ жесткого диска. А для упрощения работы можно настроить виртуальный USB-планшет - он позволит с комфортом использовать мышь внутри Колибри:
$disk0_image = "kolibri.raw" $create_usb_tablet = TRUE run-command-file "targets/qsp-x86/qsp-clear-linux.simics" machine_name = kolibri
Теперь скрипт запускает две системы:

Но у Колибри проблема: она не распознаёт сетевую карточку. Не беда! В стандартную поставку Simics входит вагон устройств на любой вкус: от USB флешек до видеокарт. Есть и сетевые карточки. Нужно найти ту, которая поддерживается в Колибри ОС:

Среди устройств Simics есть i82543 – должен заработать. Нужно только загрузить одноименный модуль, создать устройство и подключить его к свободному PCIe слоту на северном мосту машины. Главное – не забыть инициализировать новые устройства командой «instantiate-components»:
load-module module = i82543 create-pci-i82543gc-comp mac_address = "AA:BB:CC:DD:EE:FF" name = kolibri.i82543gc kolibri.i82543gc.connect-to kolibri.mb.nb instantiate-components
Вуаля: новая сетевая карта определилась как «I8254x»:

Теперь нужно как-то соединить эти две машины в одну сеть. Для этого достаточно создать коммутатор и подключить к нему сетевые карты машин:
load-module eth-links $switch = (create-ethernet-switch) connect linux.mb.sb.eth_slot ($switch.get-free-connector) connect kolibri.i82543gc.eth ($switch.get-free-connector)
Чтобы не настраивать IP руками, можно поставить локальный DHCP сервер, который присвоит IP адреса автоматически. Для этого существует объект «Service node». Service node - это вообще универсальная штука: и DNS он тебе предоставит, и DHCP, и даже доступ к интернету. Нужно только его настроить: создать на нём шлюз по умолчанию, подключить его к коммутатору и указать пул выдаваемых адресов:
load-module service-node $service_node = (new-service-node-comp) $cnt = ($service_node.add-connector "192.168.1.1") connect $service_node. + $cnt ($switch.get-free-connector) $service_node.sn.dhcp-add-pool pool-size = 50 ip = "192.168.1.150"
Ну и, конечно же, снова инициализировать все компоненты:
instantiate-components
Если симулируемая машина ничем не нагружена, симуляция может идти очень быстро, что может приводить к таймаутам и разрывам соединений. Поэтому для ручного тестирования лучше включить режим реального времени:
enable-real-time-mode
Можно запустить этот скрипт и посмотреть IP адреса в Колибри:

И в Linux:

И пингануть Колибри из Линукса:

Система настроена! Теперь можно воспроизводить пр��блему. Но для начала лучше сохранить текущее состояние симуляции, чтобы не загружать обе системы заново, если что-то пойдет не так: остановить симуляцию командой stop, и выполнить команду:
write-configuration file = after_boot.ckpt
Текущая конфигурация сохранится в папку «after_boot.ckpt». Её можно будет запустить просто передав Simics название этой папки вместо скрипта: simics after_boot.ckpt.
У нашего Ping of Death два симптома: либо он убивает процесс сетевого драйвера, либо заставляет виснуть всю систему целиком. Возможно, если разобраться с первым случаем, будет исправлен и второй.
В Колибри есть доска отладки, в которую выводятся логи приложений и ядра. Если открыть её в режиме ядра, можно запечатлеть падение процесса драйвера. Но чтобы понять, на каком размере эхо-запроса начинаются проблемы, придется несколько раз запускать ping из консоли Линукса, и если зависание таки произойдёт - перезапускать симуляцию и пинговать заново.
Если бы только Simics позволял автоматизировать работу с ОС…
Часть 2: Автоматизация
Simics скрипты позволяют полностью автоматизировать взаимодействие с симулируемыми системами. Можно написать скрипт «ping-of-death.simics», который самостоятельно загрузит чекпойнт и пинганёт Колибри из Линукса. Скрипт с одним параметром – размер отправляемого echo-запроса. Это надуманный повод познакомить читателя с очередным понятием Это поможет быстрее найти значение, которое роняет драйвер.
Для загрузки чекпойнта в Simics скриптах есть отдельная команда:
read-configuration after_boot.ckpt
А дальше нужно как-то ввести в консоль линукса команду «ping», пр��чём сделать это нужно при запущенной симуляции. Но если написать в скрипте «continue», то следующие команды скрипта не выполнятся, пока пользователь сам не остановит симуляцию, как же быть?
На помощь приходят Script Branches. Они позволяют выполнять команды параллельно с запущенной симуляцией. Можно создать такой, который введёт команду в консоль линукса:
script-branch { linux.serconsole.con.input "ping 192.168.1.150 -s 1024\n" }
Осталось сделать так, чтобы можно было настроить размер пинга. У каждого скрипта могут быть собственные параметры, которые задаются при запуске. Обычно такие создаются в начале скрипта:
decl { param ping_size : int = 1024 }
Их можно использовать как обычные переменные:
linux.serconsole.con.input "ping 192.168.1.150 -s %d\n" % [ $ping_size ]
А чтобы не приходилось вводить «continue» в консоль Simics можно ввести его уже в скрипте, в самом его конце:
continue
С таким скриптом можно спокойно экспериментировать с размерами пингов:
simics targets/ping-of-death.simics ping_size=512
Экспериментальным путём было выявлено, что размер пинга, при котором он перестаёт возвращаться: 1469, но драйвер падает на другом значении: 1473. И, судя по доске отладки, падение вызвано исключением Page Fault:

Если бы только Simics мог показать, где возникло это исключение…
Часть 3: Взятие с поличным
В Simics предусмотрена тонна инструментов для отладки оборудования и драйверов. Например, точки останова: ставим такую на выполнение какого-то действия – и программа останавливается при её достижении. Можно поставить точку останова на вызов нужного исключения процессора:
kolibri.mb.cpu0.core[0][0].bp-break-exception Page_Fault_Exception
Тогда симуляция остановится на первом же возникшем Page Fault. Если сделать это в скрипте, то в консоли Simics это не будет никак отражено, он просто поставит симуляцию на паузу и разрешит ввод (simics>). Чтобы посмотреть, на какой инструкции возникло исключение, можно воспользоваться вторым инструментом: отладкой процессора. Начать отладку можно так:
kolibri.mb.cpu0.core[0][0].debug dbg
После чего появится контекст отладки «dbg», у которого есть такие полезные методы, как показ ассемблерного кода:
simics> dbg.list –d -> cs:0x00000000800312e7 p:0x0000312e7 inc dword ptr [edi-0x7ffa3974] ; Pending exception, vector 14 cs:0x00000000800312ed p:0x0000312ed call 0x80034eb4 cs:0x00000000800312f2 p:0x0000312f2 ret cs:0x00000000800312f3 p:0x0000312f3 pushfd cs:0x00000000800312f4 p:0x0000312f4 pushad
В simics можно загрузить информацию о символах ОС, но, к сожалению, ассемблер, на котором написана Колибри, не умеет генерировать информацию нужного формата. Но к счастью – это ассемблер! А значит, можно выявить строку кода с ошибкой по инструкциям, окружающим этот код. Вот она:
.dump: DEBUGF DEBUG_NETWORK_VERBOSE, "IPv4_input: dumping\n" --> inc [IPv4_packets_dumped + edi] call net_buff_free ret
Внезапно, ECHO-запрос вызывает блок кода «dump» (очевидно, имелось в виду «drop» – отбросить пакет). И если вывести содержимое регистра edi, становится понятно, что оно совсем не такое, какое ожидается:
simics> hex(kolibri.mb.cpu0.core[0][0]->edi) "0x80bc9800"
В edi должен был бы быть индекс сетевого девайса, а лежит какой-то адрес. Но почему? И что привело систему в этот блок?
Если бы только Simics умел выполнять код в обратном направлении…
Часть 4: Реконструкция
Прежде, чем воспользоваться функцией обратного выполнения в Simics, нужно включить её командой «enable-reverse-execution». Её можно выполнить после загрузки чекпойнта. Начиная с этого момента, можно будет выполнять код в обратном направлении. Для упрощения жизни можно тут же начать отладку:
kolibri.mb.cpu0.core[0][0].debug dbg enable-reverse-execution
Запускается тест и срабатывает точка останова. Теперь можно пройтись на одну инструкцию назад:
simics> reverse-step-instruction [kolibri.mb.cpu0.core[0][0]] cs:0x0000000080031594 p:0x000031594 jmp 0x800313f7 Now debugging the x86QSP1 kolibri.mb.cpu0.core[0][0] ??() jmp 0x800313f7
Если пройтись пару шагов назад, становится понятно, что пакет был сброшен при проверке размера собранного фрагментированного IP пакета.
Дело в том, что отправить такой большой запрос одним IP пакетом нельзя, поскольку размер стандартного ethernet фрейма ограничен полутора килобайтами. Поэтому ping использовал IP фрагментацию и разделил запрос на два пакета. Но при объединении этих пакетов в один Колибри не смогла определить размер конечного пакета.
Сумма размеров всех фрагментов равна одному:
simics> kolibri.mb.cpu0.core[0][0]->ax 60954
А если сложить размер последнего фрагмента и его смещение, получается другое:
simics> kolibri.mb.cpu0.core[0][0]->cx 1501
И это заставляет код дропать пакет. А если пройтись чуть дальше, можно заметить, что размер заголовка IP равен нулю, а размер пакета ненормально большой.
Что же содержится в таком странном IP пакете?
Для просмотра памяти можно использовать команду «x» процессора, который эту память видит:
simics> kolibri.mb.cpu0.core[0][0].x address = 0x80bc9800 size = 128 ds:0x80bc9800 ffff ffff ffff ffff 0010 93ad 0100 0000 ................ ds:0x80bc9810 ee05 0000 1800 0000 aabb ccdd eeff 0017 ................ ds:0x80bc9820 a000 0000 0800 4500 05dc 21dc 2000 4001 ......E...!. .@. ds:0x80bc9830 0000 c0a8 0197 c0a8 0196 0800 c307 010e ................ ds:0x80bc9840 0001 079c 6962 0000 0000 b3c7 0200 0000 ....ib.......... ds:0x80bc9850 0000 1011 1213 1415 1617 1819 1a1b 1c1d ................ ds:0x80bc9860 1e1f 2021 2223 2425 2627 2829 2a2b 2c2d .. !"#$%&'()*+,- ds:0x80bc9870 2e2f 3031 3233 3435 3637 3839 3a3b 3c3d ./0123456789:;<=
На первый взгляд, всё в порядке. И IP заголовок на месте, и размеры у него валидные. Но, почему-то, код берёт размеры не оттуда, где они находятся. К примеру, размер заголовка читается со смещения 0x0e, а в реальности он лежит по смещению 0x26.
Очевидно два варианта: либо этот код не оттуда читает, либо кто-то не туда записывает. Судя по коду, этот буфер должен состоять из двух частей: сначала идёт структура «IPv4_FRAGMENT_entry», а затем сам IP пакет. Но внимание на себя обращает подозрительный комментарий к скрытому полю этой структуры:
rb 2 ; to match ethernet header size ;;; FIXME ; Ip header begins here (we will need the IP header to re-construct the complete packet)
То есть подразумевалось, что эта структура лежит на месте ethernet-заголовка. Но что же тогда лежит в самом начале буфера?
Если бы только Simics мог показать, кто писал туда до н��шего кода…
Часть 5: Развязка
Для того, чтобы поставить точку останова на запись по виртуальному адресу можно воспользоваться командой «break», но перед этим нужно выбрать нужный процессор в качестве текущего:
simics> pselect "kolibri.mb.cpu0.core[0][0]" simics> break -w 0x80bc9800 Breakpoint 2 set on address 0x80bc9800 in 'kolibri.cell_context' with access mode 'w'
Теперь можно развернуть симуляцию вспять и подождать, когда сработает точка останова:
simics> reverse Breakpoint 2 on write to 0x80bc9800 in kolibri.cell_context. [kolibri.mb.cpu0.core[0][0]] cs:0x0000000080032623 p:0x000032623 mov dword ptr [eax+0x8],ebx Now debugging the x86QSP1 kolibri.mb.cpu0.core[0][0] ??() mov dword ptr [eax+0x8],ebx
Первым попался код инициализации полей структуры «IPv4_FRAGMENT_entry», наш старый знакомый. Реверсируем дальше:
simics> reverse Breakpoint 2 on write to 0x80bc9800 in kolibri.cell_context. [kolibri.mb.cpu0.core[0][0]] cs:0x0000000080031f0a p:0x000031f0a mov ebx,dword ptr [0x80039fea] mov ebx,dword ptr [0x80039fea]
Внезапно, теперь мы в ethernet.inc:
; Add frame to the end of the linked list mov [eax + NET_BUFF.NextPtr], ETH_frame_head --> mov ebx, [ETH_frame_tail] mov [eax + NET_BUFF.PrevPtr], ebx mov [ETH_frame_tail], eax mov [ebx + NET_BUFF.NextPtr], eax
По видимости, то, что мы видим в самом начале буфера, должно было быть структурой «NET_BUFF». А уже после него должна лежать наша «IPv4_FRAGMENT_entry». Дело не хитрое: мазок, другой - и в прод.
Ну, а дальше идёт исправление остаточных проблем при помощи всё тех же инструме��тов.
Сначала костылим восстановление значения edi перед обработкой ICMP запроса, чтобы драйвер не падал при проверке IP адреса. Главное не забыть потом привести это в нормальный вид.
Затем разбираемся с несовпадением чексуммы исправляя алгоритм сборки IP фрагментов.
Ну и, напоследок, исправляем стек внутри кода сборки пакета, поскольку этот код был написан ещё в те времена, когда обработчики сетевых протоколов принимали размер пакета как аргумент.
Результат:

Колибри живёт!
Правда, есть нюанс: хоть IP пакет и собирается без ошибок, обработать его Колибри пока не может. Но это - уже совсем другая история…
Вывод
Simics имеет достаточное количество инструментов для разработки и отладки операционных систем, а система скриптов позволяет автоматизировать тестирование и воспроизведение проблем.
Сильные стороны системы:
Возможность обратного выполнения кода.
Обширный инструментарий для отладки.
Богатый скриптовый язык с интегрированным Python.
Большое количество эмулируемых девайсов и возможность написания своих.
Слабые стороны:
Медлительность ввиду избыточного функционала.
Проприетарность.
Лично я предпочитаю использовать её при отладке багов в ядре и для тестирования драйверов. В прочих тестах использую Qemu.
