30 декабря 2014

Инфобез. Введение в Реверс

Ребята, серьезно, может лучше не будем?..
(вступление от автора)


Виды архитектур

CISC (англ. Complex instruction set computing, или англ. complex instruction set computer — компьютер с полным набором команд) — концепция проектирования процессоров. Имеет следующие свойства:
  1. Нефиксированные по длине команды
  2. Каждая команда является "обычным числом", то есть просто hex-значение в памяти. В состав команды обычно входят её параметры, которые также имеют определенный размер. В результате размер команды сложится собственно из того набора байт (от 1 до n), которое обозначает инструкцию процессора и все сопутствующие параметры. В архитектуре CISC одна команда может быть длиной 1 байт, другая 3 байта и так далее.
  3. Каждый регистр имеет свою функцию
  4. Это значит, что некоторые команды не используют какие-то особые параметры, а по умолчанию используют определенный регистр процессора. В таком случае явно такой регистр при вызове команды не указывается, а в него просто заранее кладется (из него достается) некоторое нужное значение.
  5. Арифметические операции требуют 1 команду
  6. Противопоставляется первым процессорам RISC, в которых не было операций умножения и деления в угоду скорости исполнения. Таким образом, исполнение инструкции умножения на CISC архитектуре проще выполняется, но не распараллеливается во конвеерам процессора, в то время как на RISC процессорах та же инструкция представляется совокупностью других инструкций, которые соответственно можно выполнить параллельно (подробнее гугли про устройство процессора).

 
RISC (англ. restricted (reduced) instruction set computer — компьютер с сокращённым набором команд) — архитектура процессора, в котором быстродействие увеличивается за счёт упрощения инструкций чтобы их декодирование было более простым, а время выполнения — меньшим.
  1. Команды имеют одинаковую длину и структуру
  2. Во-первых, так проще запоминать эти инструкции, и как они работают. Во-вторых, не надо учитывать особенности вызова той или иной команды с разными параметрами - все унифицировано. В-третьих, эти команды работают быстрее, так как занимают одинаковое количество байт и их проще запустить параллельно
  3. Регистров значительно больше, и они используются значительно свободнее
  4. Дело в том, что в компьютере самой медленной является ПЗУ на жестком диске, а оперативная память считается быстрой. Но если посмотреть глубже, то для процессора оперативная память тоже является достаточно медленной, поэтому он использует КЭШ, встроенный на кристалле, а самой быстрой является память, организованная в самом процессоре на ячейках памяти, называемых СОЗУ (сверх быстрая ОЗУ).
  5. Специализированные команды для работы с памятью
  6. Как уже говорилось выше, доступ к памяти штука медленная (в категориях процессорного времени), поэтому надо минимализировать использование инструкций, работающих с памятью. Если у нас есть универсальная инструкция для работы с памятью и с регистрами, то каждый раз, когда мы будет писать/читать регистр у нас будет уходить столько же времени, сколько и на чтение ячейки памяти. Поэтому команды для работы с памятью вынесены отдельно.
Дальнейшую классификацию архитектурных решений не привожу, в силу их узкой специализированности. Кому интересно - гугл в помощь.

РОН и РСН

Как было сказано выше, у процессора есть внутренняя память. Для удобства обращения она разделена на регистры (можете воспринимать их как "переменные" или просто именованные участки памяти). Каждый регистр имеет определенный размер (зависит от архитектуры системы) и указывает на определенный участок памяти. Эти параметры являются постоянными и вы их не измените. Когда процессор работает с регистрами, это занимает у него намного меньше времени, чем когда он работает с памятью.
Но программист не имеет доступа ко всем регистрам, а только к некоторым. Из множества доступных можно выделить две большие группы: регистры общего назначения (РОН) и регистры специального назначения (СР или РСН). Первые нужны для выполнения любых "пользовательских" операций, другими словами, в них можно свободно писать данные и читать из них, а также выполнять над ними любые доступные операции. Вторая группа регистров используется для "системных" целей: хранение текущего состояния процесса, номер следующей выполняемой инструкции, указатель на стек и так далее.
На изображении выше указаны самые важные регистры, с которыми вам предстоит столкнуться в жизни. Следует отметить что 4 регистра имеют особенный способ адресации: EAX, EBX, ECX, EDX. В отличие от остальных 32-битных регистров, здесь можно получить доступ не только к младшей 16 битной части целиком (AX, BX, CX, DX), но и к старшему и младшему байту в этой части (AH, AL; BH, BL; CH, CL; DH, DL). На системах с 64 битностью используются регистры с приставкой R- (RAX, RBX, RCX, RDX). Структура таких регистров представлена на рисунке ниже.
Про остальные регистры предоставляю читателю ознакомиться собственноручно (если интересна сама тема).

Виды инструкций


Процессор легко представить как обычный "вычислитель" (для неопытных). А вычислителю чтобы что-то делать, нужно рассказать на его "языке" последовательность действий. Как было сказано в первом разделе, команды представляют собой некоторые последовательности байт (вместе с параметрами от 1 до n байт). Записываются в памяти эти команды по-разному (в зависимости от архитектуры системы). А теперь попробуйте запомнить хотя бы 256 различных команд в виде циферок - свихнетесь. Чтобы разгрузить мозги придумали давать каждой команде короткое английское сокращение от того действия, которое она делает. В результате большинство команд не превышает 5 символьной записи, а сам язык для написания этих команд в "удобном виде" (посмотрите в ассемблерный листинг и скажите удобно ли вам? Думаю не очень. Но всяко лучше, чем просто "втыкать в непонятные циферки") назвали языком ассемблера. Это язык низкого уровня и его особенность в том, что он различен для разных архитектур.
У каждого процессора есть "стандартный набор команд" (он примерно одинаков для подавляющего большинства архитектур): арифметические команды, булева алгебра, вызовы инструкций и условные переходы и т. д. Но кроме этих, есть дополнительные списки команд, относящиеся к конкретному процессору или семейству процессоров. Эти инструкции решают строго определенный круг задач и предназначены для оптимизации сложных действий и ускорения работы. К таким командам можно отнести: MMX, SSE, SSE2 и т. д.





Стек

Регистры - участки памяти для хранения данных в процессе их обработки. Это понятно. Но куда девать параметры функции при её вызове, "старые" значения регистров при их изменении, прочие временные данные? Для этих целей служит стек: участок из выделенной просцессу памяти, используемый программой для вышеперечисленных, и не только, целей. "Как работает стек? Почему это выделенная отдельно область памяти?" - спросите вы. Правильно спросите. Стек работает по принципу LIFO: что последнее в него положил, то первое можешь достать. Например, представьте женскую сумочку. На её дно мы положим ключи от дома, сверху одноразовые салфетки, и сверху ключи от машины. Теперь чтобы достать ключи от дома надо сначала достать ключи от машины, потом достать одноразовые салфетки, и только потом можно будет достать ключи от дома. Подробнее принцип работы представлен на картинке.
Зачем его так сделали: затем что так работают вызовы функций. Если мы вызвали функцию a, то её адрес возврата (указатель на следующую инструкцию) попадает на стек, потом мы из функции a вызвали функцию b, а из неё, в свою очередь, функцию c. После выполнения функции c работа программы возвратится к тому месту в функции b, где вызвали c. При этом со стека достается этот самый адрес возврата, который и оказывается сверху стека. После завершения функции b её адрес аналогичным образом снимается со стека и там остается только адрес функции a. Знаю, что не совсем понятно, но это надо увидеть в живую (почитать еще немножко литературы, посмотреть видюшки тематические, попытаться посмотреть в отладчик или дизассемблер).
Кроме того, в некоторых системах принято передавать аргументы функций (то что вы на C++ пишете в скобочках после имени функции) через стек. Ещё некоторые функции меняют значения регистров, и  если не сохранить их состояние предварительно, то можно нарушить всю работу программы. При осуществлении всех вышеперечисленных способов использования стека, возникает неоднозначный момент: основная "ветка" выполнения может иметь указатель стека не в том месте, где закончилось выполнение вызванной функции. В таких случаях стек "выравнивают", то есть устанавливают указатель на то значение, которое было до вызова функции, иначе следующий элемент стека может просто потеряться и программа перестанет работать.

Вызов функции

"Ага. И как понять, нужно ли мне выравнивать стек и как с ним вообще работать?"
Для этого необходимо знать, какое соглашение вызовов используется на вашей архитектуре. Соглашение о вызове подпрограмм это такая спецификация (договоренность), которая описывает как раз все неясные моменты: как передавать аргументы, в каком порядке, куда попадет возвращаемое значение, кто следит за стеком, как использовать регистры и так далее. Когда вы пишете на языках высокого уровня, они сами в курсе когда и какое соглашение использовать, но если вы пишете на ассемблере, то тут вся "кухня" реализовывается вручную. В целом реверсеру знание соглашений позволяет быстрее понять код и соответственно найти нужную уязвимость.

Что почитать/посмотреть/повтыкать?

Для начала почитайте общие статьи про устройство какой-нибудь одной ОС, например DOS или Windows. Потом стоит близко познакомиться с языком ассемблера под эту ОС, скачать нужный ассемблер и попробовать написать на нем разные программки от Hello world до простенького шифратора. После этого собрать их и исследовать в дизассемблере, пройтись отладчиком. Далее попробуйте написать минимальные программы с тем же функционалом например на C и сравните в дизассемблере, что общего, а что различается. Как только вы проделаете хотя бы половину из вышеизложенного, вы начнете понимать с чем имеете дело, и соответственно, что нужно для саморазвития дальше.