Simics: Забиваем гвозди сваебоем
Любите ли вы отзывчивые программы так, как люблю их я? Любовь эта привела меня к Колибри ОС — невероятно шустрой операционной системе, которая запускает программу до того, как вы осознаете, что кликнули по ней. И недавно у неё нашли уязвимость: 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 заголовок на месте, и размеры у него валидные. Но, почему-то, код берёт размеры не оттуда, где они находятся. К примеру, размер заголовка читается со смещения 0×0e, а в реальности он лежит по смещению 0×26.
Очевидно два варианта: либо этот код не оттуда читает, либо кто-то не туда записывает. Судя по коду, этот буфер должен состоять из двух частей: сначала идёт структура «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.