Пишу, по большей части, про историю, свою жизнь и немного про программирование.

Дизассемблируй это

Дум (71.16КиБ)
«ДУМ», скомпилированный с использованием одних только команд MOV

В моей жизни последовательно сменяли друг друга три ассемблера. Первый, для компьютера «Радио-86РК», я выучил по комментариям к ассемблерному коду в журнале «Радио» — другой литературы не было, а до моего знакомства с интернетом оставалось несколько лет. Потом были последовательно ассемблеры для «Спектрума» и интеловских процессоров.

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

В ассемблере есть такая команда — MOV (в некоторых ассемблерах — LD), записывает содержимое одного аргумента в другой. Сейчас набор комманд разросся, аргументом может быть почти что угодно — регистр, ячейка в памяти, сумма некоторого числа, одного регистра и другого, умноженного на число, но по сути это всегда присваивание.

И вот оказалось, что эта команда — полная по Тьюрингу. Звучит невероятно, но это так. Некие ребята заморочились и сделали компилятор, который компилирует любую программу на Си в последовательность команд MOV. Причём им даже ДУМ удалось скомпилировать, правда один кадр рисуется семь часов. Кстати, такая программа неуязвима для горюшка века — Мелтдауна и Спектра.

Есть небольшая (на 156 страниц и 90% воды) презентация, достаточно популярно объясняющая как этого удалось достичь, но для её чтения надо знать ассемблер, поэтому я позволю себе раскрыть детали трансляции двух инструкций, чтобы пояснить принцип для тех, кто ассемблера не знает или ленится причитать.

Например, сравнение двух чисел делается при помощи следующего псеводокода:

mov [X], 0
mov [Y], 1
mov R, [X]

У нас есть два числа в аргументах «X» и «Y», результат сравнения которых попадает в «R» — там будет ноль, если числа не равны и единица в противном случае. Как же это работает?

Первой командой ноль записывается в ячейку по адресу «X». Это ассемблер, у нас тут всё — число, остальное — человеческие интерператации, поэтому записанное в «X» мы используем как адрес. Второй командой единица записывается в ячейку по адресу «Y». Третьей командой мы читаем значение по адресу «X» и если значения в «X» и «Y» совпадают, то ноль перетрётся единицей (и она попадёт в R), если нет, то в ячейке по адресу «X» ноль останется (который попадёт в R).

Несложно. Но из кода выше непонятно как получаются другие необходимые инструкции. Увы, но какого-то единого принципа для всего нет, авторам для каждого набора приходилось придумывать что-то новое. Думаю интересно будет взглянуть на реализацию чего-нибудь ещё.

Возьмём, например, логическое «ИЛИ» («OR»), тут чуточку сложнее:

OR_ADDRS: dd OR_0, OR_1
OR_0: dd 0, 1
OR_1: dd 1, 1
; …
mov eax, X
mov edx, [OR_ADDRS + eax]
mov eax, Y
mov eax, [eax + edx]
mov R, eax

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

Что тут происходит? В регистр (переменную, с которыми работает процессор) «eax» записывается значение «X» (возможные входные значения у нас тут — ноль или единица, численное представление булевых значений).

Далее в регистр «edx» записывается число из адреса, который является суммой адреса массива OR_ADDRS и содержимого регистра eax. Таким образом в eax попадёт OR_0 или OR_1, в зависимости от того былы записаны в eax ноль или единица. Эти значения — тоже числа и являются адресами двух других массивов из двух элементов.

Далее в eax мы записываем аргумент Y, его значение складывается с адресом полученным на предыдущем шаге и из получившегося адреса мы читаем записанное там значение. В переводе на ПХП получается следующее:

function mov_or(int $X, int $Y): int
{
    define('OR_0', [0, 1]);
    define('OR_1', [1, 1]);

    define('OR_ADDRS', [OR_0, OR_1]);

    $R = OR_ADDRS[$X][$Y];

    return $R;
}

Кстати, интересно, что у знаменитого дисассемблера «ИДА» от полученной таким образом программы крепко уносит крышу — при попытке отладки диссасемблер не видит никаких ветвлений и падает на анализе кода. Получился бы неплохой метод защиты от анализа, если бы не производительность.

5 комментариев
Владимир Кузнецов 2018

Кстати, такая программа неуязвима для горюшка века — Мелтдауна и Спектра.

Вот тут не понял — эти уязвимости направлены на несанкционированное чтение памяти.
Любая программа хранящая что либо в памяти становится уязвима.

Евгений Степанищев 2018

Я где-то прочитал об этом, но не разбирался почему.

Евгений Степанищев 2019

Вспомнил про этот пост и разобрался. Уязвимости нет, потому что тут нет предсказаний переходов.

Владимир Кузнецов 2019

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

Евгений Степанищев 2019

Видимо уже позабыл матчасть.