Любите ли вы отзывчивые программы так, как люблю их я? Любовь эта привела меня к Колибри ОС - невероятно шустрой операционной системе, которая запускает программу до того, как вы осознаете, что кликнули по ней. И недавно у неё нашли уязвимость: 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.