Глава 1. Ускоренный курс по CISC/RISC и основам программирования
Прежде чем погрузиться в мир вредоносных программ, нам необходимо иметь полное представление об ядре машин, на которых мы анализируем вредоносное ПО. Для реверс инжиниринга имеет смысл сосредоточиться в основном на архитектуре и операционной системе, которую она поддерживает. Конечно, существует множество устройств и модулей, составляющих систему, но в основном именно эти два момента определяют набор инструментов и подходов, используемых при анализе. Физическим представлением любой архитектуры является процессор. Процессор похож на сердце любого смарт-устройства или компьютера, поскольку он поддерживает их работоспособность.
В этой главе мы рассмотрим основы наиболее широко используемых архитектур, от хорошо известных архитектур x86 и x64 Instruction Set Architecture (ISA) до решений, поддерживающих несколько мобильных устройств и устройств Интернета вещей (IoT), которые часто неправомерно используются семействами вредоносных программ, такие как Mirai и многие другие. Это задаст тон вашему путешествию по анализу вредоносных программ, поскольку статический анализ невозможен без понимания инструкций по ассемблеру. Хотя современные декомпиляторы действительно становятся все лучше и лучше, они существуют не для всех платформ, на которые нацелено вредоносное ПО. Кроме того, они, вероятно, никогда не смогут обрабатывать запутанный код. Не пугайтесь сложности ассемблера; просто нужно время, чтобы к нему привыкнуть, и через некоторое время его можно читать, как любой другой язык программирования. Хотя эта глава является отправной точкой, всегда имеет смысл повышать свои знания, практикуясь и исследуя дальше.
Эта глава разделена на следующие разделы для облегчения процесса обучения:
* Базовые концепции
* Языки ассемблера
* Знакомство с x86 (IA-32 и x64)
* Изучение ассемблера ARM
* Основы MIPS
* Ассемблер SuperH
* Работа со SPARC
* Переход от ассемблера к языкам программирования высокого уровня
Базовые концепции
Большинство людей на самом деле не понимают, что процессор в значительной степени является умным калькулятором. Если вы посмотрите на большинство его инструкций (независимо от языка ассемблера), вы обнаружите, что многие из них имеют дело с числами и выполняют некоторые вычисления. Однако есть несколько особенностей, которые на самом деле отличают процессоры от обычных калькуляторов, например:
* Процессоры имеют доступ к большему объему памяти по сравнению с традиционными калькуляторами. Это пространство памяти дает им возможность хранить миллиарды значений, что позволяет выполнять более сложные операции. Кроме того, они имеют несколько быстрых и небольших блоков памяти, встроенных в микросхему процессора, называемых регистрами.
* Процессоры поддерживают множество типов инструкций, кроме арифметических инструкций, например, изменение потока выполнения на основе определенных условий.
* Процессоры могут взаимодействовать с другими устройствами (например, динамиками, микрофонами, жесткими дисками, графическими картами и т.д.).
Обладая такими функциями в сочетании с большой гибкостью, процессоры стали незаменимыми интеллектуальными машинами для таких технологий, как искусственный интеллект, машинное обучение и другие. В следующих разделах мы рассмотрим эти функции, а позже углубимся в различные языки ассемблера и в то, как эти функции проявляются в наборах инструкций этих языков.
Регистры
Поскольку большинство процессоров имеют доступ к огромному пространству памяти, хранящему миллиарды значений, процессору требуется больше времени для доступа к данным (и это усложняется, как мы увидим позже). Таким образом, для ускорения операций процессора они содержат небольшие и быстрые блоки внутренней памяти, называемые регистрами.
Регистры встроены в микросхему процессора и способны хранить непосредственные значения, которые необходимы при выполнении вычислений и передаче данных из одного места в другое.
Регистры могут иметь разные имена, размеры и функции в зависимости от архитектуры. Вот некоторые из видов, которые широко используются:
* Регистры общего назначения: — это регистры, которые используются для сохранения значений или результатов различных арифметических и логических операций.
* Указатели стека и фрейма: это регистры, которые используются для указания на начало и конец стека.
* Указатель инструкций/счетчик программ: Указатель инструкций используется для указания на начало следующей инструкции, которая должна быть выполнена процессором.
Память
Память играет важную роль в развитии всех интеллектуальных устройств, которые мы видим в настоящее время. Возможность управлять большим количеством значений, текста, изображений и видео в быстрой и энергозависимой памяти позволяет процессорам обрабатывать больше информации и в конечном итоге выполнять более сложные операции, такие как отображение графических интерфейсов в 3D и виртуальной реальности.
Виртуальная память
В современных операционных системах, независимо от того, являются ли они 32-разрядными или 64-разрядными, операционная система выделяет изолированную виртуальную память (в которой ее страницы отображаются на страницы физической памяти) для каждого приложения, чтобы защитить операционную систему и другие приложения и данные.
Обычные приложения должны иметь возможность доступа только к своей виртуальной памяти. У них есть возможность читать, писать или выполнять инструкции на своих страницах виртуальной памяти. Каждой странице виртуальной памяти назначен набор разрешений, которые представляют тип операций, которые приложению разрешено выполнять на этой странице. Эти разрешения доступны для чтения, записи и выполнения. Кроме того, каждой странице памяти может быть назначено несколько разрешений.
Чтобы приложение могло получить доступ к любому значению, хранящемуся в памяти, ему нужен виртуальный адрес, который в основном представляет собой адрес места хранения этого значения в памяти.
Несмотря на знание виртуального адреса, доступ может быть затруднен другой проблемой, которая хранит этот виртуальный адрес. Размер виртуального адреса в 32-битных системах составляет 4 байта, а в 64-битных системах — 8 байтов. Это означает, что нам может понадобиться выделить еще одно место в памяти для хранения этого виртуального адреса. Для этого нового пространства в памяти, чтобы получить к нему прямой доступ только по его адресу, нам нужно будет сохранить его собственный адрес памяти в другом пространстве памяти, что приведет нас к бесконечному циклу, как показано на следующем рисунке:
Для решения этой проблемы в настоящее время используется несколько решений, и в следующем разделе мы рассмотрим одно из них — стек.
Стек
Стек буквально означает кучу объектов. В компьютерных науках стек — это, по сути, структура данных, которая помогает сохранять в памяти разные значения одинакового размера в виде стопки, используя принцип «последним пришел — первым вышел» (LIFO).
На вершину стека (куда будет помещен следующий элемент) указывает специальный указатель стека, который будет более подробно обсуждаться ниже.
Стек является общим для многих языков ассемблера и выполняет несколько функций. Например, это может помочь в решении математических уравнений, таких как X = 5 * 6 + 6 * 2 + 7 (4 + 6), путем сохранения каждого вычисленного значения и помещения каждого в стек, а затем извлечения (или извлечения) их обратно, чтобы вычислить сумму всех из них и сохранить их в переменной X.
Он также часто используется для передачи аргументов (особенно если их много) и хранения локальных переменных.
Стек также используется для сохранения адресов возврата непосредственно перед вызовом функции или подпрограммы. Таким образом, после завершения этой подпрограммы она извлекает адрес возврата из вершины стека и возвращает его туда, откуда она была вызвана, чтобы продолжить выполнение.
В то время как указатель стека обычно указывает на текущую вершину стека, указатель кадра сохраняет адрес вершины стека до вызова подпрограммы, поэтому его можно легко восстановить после возврата.
Ветвление, циклы и условия
Вторая особенность, которой обладают процессоры, — это возможность изменять поток выполнения программы в зависимости от заданного условия. В каждом языке ассемблера есть несколько инструкций сравнения и инструкций управления потоком. Инструкции по управлению потоком можно разделить на следующие категории:
* Безусловный переход: это тип инструкции, которая принудительно изменяет поток выполнения на другой адрес (без каких-либо условий).
* Условный переход: это похоже на логический вентиль, который переключается на другую ветвь на основе заданного условия (например, равно нулю, больше или меньше), как показано на следующем рисунке:
Вызов: Он изменяет выполнение на другую функцию и сохраняет адрес возврата, который будет восстановлен позже, если это необходимо.
Исключения, прерывания и обмен данными с другими устройствами
На языке ассемблера связь с различными аппаратными устройствами осуществляется посредством так называемых прерываний.
Прерывание — это сигнал процессору, отправляемый аппаратным или программным обеспечением, указывающий, что что-то происходит или есть сообщение, которое нужно доставить. Процессор приостанавливает свой текущий запущенный процесс, сохраняя свое состояние, и выполняет функцию, называемую обработчиком прерывания, для обработки этого прерывания. Прерывания имеют собственное обозначение и широко используются для связи с оборудованием для отправки запросов и обработки их ответов.
Существует два типа прерываний. Аппаратные прерывания обычно используются для обработки внешних событий при обмене данными с оборудованием. Программные прерывания вызываются программным обеспечением, обычно вызовом определенной инструкции. Разница между прерыванием и исключением заключается в том, что исключения происходят внутри процессора, а не извне. Примером операции, генерирующей исключение, может быть деление на ноль.
Языки ассемблера
Есть две большие группы архитектур, определяющих языки ассемблера, которые мы рассмотрим в этом разделе: компьютер со сложным набором команд (CISC) и компьютер с сокращенным набором команд (RISC).
CISC против RISC
Не вдаваясь в подробности, основное различие между CISC, такими как Intel IA-32 и x64, и языками RISC, связанными с такими архитектурами, как ARM, заключается в сложности их инструкций.
Языки ассемблера CISC имеют более сложные инструкции. Они сосредоточены на выполнении задач, используя как можно меньше строк инструкций по сборке. Для этого языки ассемблера CISC включают инструкции, которые могут выполнять несколько операций, например mul в ассемблере Intel, который выполняет операции доступа к данным, умножения и хранения данных.
В языке ассемблера RISC инструкции по ассемблеру просты и обычно выполняют только одну операцию каждая. Это может привести к увеличению количества строк кода для выполнения конкретной задачи. Однако это также может быть более эффективным, поскольку исключает выполнение любых ненужных операций.
Типы инструкций
В следующих разделах мы рассмотрим основную структуру каждого языка ассемблера, три основных типа ассемблерных инструкций и то, как они реализованы в каждом из них:
* Манипуляция данными:
- Арифметические манипуляции
- Логические и битовые манипуляции
- Сдвиги и вращения
* Передача данных:
- Передачи между памятью и регистрами
- Передачи между регистрами
* Выполнение потока исполнения:
- Переход или вызов
- Ветвление на основе условия
Знакомство с x86 (IA-32 и x64)
Intel x86 (IA-32 и x64) является наиболее распространенной архитектурой, используемой в ПК и на многих серверах, поэтому неудивительно, что большинство образцов вредоносных программ, которые у нас есть на данный момент, поддерживают ее. IA-32 также обычно называют i386 (замененный i686) или даже просто x86, тогда как x64 также известен как x86-64 или AMD64. x86 — это архитектура CISC, которая включает в себя несколько сложных инструкций в дополнение к простым. В этом разделе мы представим наиболее распространенные из них, а также то, как компиляторы используют их преимущества в своих соглашениях о вызовах.
Регистры
Вот таблица, показывающая взаимосвязь между регистрами в архитектурах IA-32 и x64:
От r8 до r15 доступны только в x64, но не в IA-32, а spl, bpl, sil и dil доступны только в x64.
Первые четыре регистра (rax, rbx, rcx и rdx) являются регистрами общего назначения (GPR), но некоторые из них имеют следующие специальные варианты использования для определенных инструкций:
* rax/eax: Используется для хранения информации и является специальным регистром для некоторых вычислений.
* rcx/ecx: используется как регистр счетчика в инструкциях цикла.
* rdx/edx: используется при делении при возврата модуля
В x64 регистры с r8 по r15 также являются GPR, которые были добавлены к доступным GPR.
Регистр rsp/esp используется как указатель стека, указывающий на вершину стека. Его значение уменьшается, когда значение помещается в стек, и увеличивается, когда значение извлекается из стека. Регистр rbp/ebp используется в качестве указателя кадра и полезен для доступа к локальным переменным и аргументам функции, как мы увидим позже в этом разделе. В дополнение к этому, rbp/ebp иногда используется в качестве GPR для хранения любых данных.
rsi/esi и rdi/edi в основном используются для определения адресов при копировании группы байтов в память. Регистр rsi/esi всегда играет роль источника, а регистр rdi/edi играет роль получателя. Оба регистра являются энергонезависимыми и также являются GPR.
Специальные регистры
В сборке Intel есть два специальных регистра и они следующие:
* rip/eip: Это указатель инструкции, указывающий на следующую выполняемую инструкцию. К нему нельзя получить доступ напрямую, но есть специальные инструкции для доступа к нему.
* rflags/eflags/flags: Этот регистр содержит текущее состояние процессора. На его флаги влияют арифметические и логические инструкции, включая инструкции сравнения, такие как cmp и test, а также он используется с условными переходами и другими инструкциями. Вот самые распространенные флаги:
- Флаг переноса (CF): это когда арифметическая операция выходит за границы; посмотрите на следующую операцию:
mov al, Ffh; al = 0xFF и CF = 0
add al, 1; al = 0 & CF = 1
- Флаг нуля (ZF): Этот флаг устанавливается, когда результат арифметической или логической операции равен нулю. Это также может быть установлено с помощью инструкций сравнения.
- Флаг знака (SF): Этот флаг указывает, что результат операции отрицательный.
- Флаг переполнения (OF): Этот флаг указывает, что в операции произошло переполнение, что привело к изменению знака (только для чисел со знаком), следующим образом:
mov cl, 7Fh; cl = 0x7F (127) & OF = 0
inc cl; cl = 0x80 (-128) & OF = 1
Существуют и другие регистры, такие как регистры MMX и FPU (и инструкции по работе с ними), но мы не будем их рассматривать в этой главе.
Структура инструкции
Для Intel x86 (IA-32 или x64) общей структурой инструкций является opcode, dest, src.
Давайте углубимся в них.
код операции
код операции — это имя инструкции. Некоторые инструкции имеют только код операции без назначения или источника, например:
Nop, pushad, popad, movsb
pushad и popad недоступны в x64.
dest
dest представляет место назначения или место, где будет сохранен результат вычислений, а также становится частью самих вычислений, например:
add eax, ecx; eax = (eax + ecx)
sub rdx, rcx; rdx = (rdx - rcx)
Кроме того, он может играть роль источника и пункта назначения с некоторыми инструкциями кода операции, которые принимают только пункт назначения без источника:
inc eax
dec ecx
Или это может быть только источник или место назначения, например, в случае этих инструкций, которые сохраняют значение в стеке, а затем возвращают его обратно:
push rdx
pop rcx
dest может выглядеть следующим образом:
* REG: регистр, такой как eax и edx.
* r/m: Место в памяти, например следующее:
DWORD PTR [00401000h]
BYTE PTR [EAX + 00401000h]
WORD PTR [EDX*4 + EAX+ 30]
* Значение в стеке (используемое для представления локальных переменных), например следующее:
DWORD PTR [ESP+4]
DWORD PTR [EBP-8]
Src
src представляет источник или другое значение в вычислениях, но впоследствии не сохраняет результаты. Это может выглядеть так:
* REG: например, добавить rcx, r8
* r/m: например, добавьте ecx, dword ptr [00401000h]
* imm: непосредственное значение, такое как mov eax, 00100000h
Набор инструкций
Здесь мы рассмотрим различные типы инструкций, которые мы перечислили в предыдущем разделе.
Инструкции по обработке данных
Вот некоторые из арифметических инструкций:
Кроме того, для логики и манипуляций с битами они выглядят так:
И, наконец, для сдвига и ротаций они такие:
Инструкции по передаче данных
Есть инструкция mov, которая копирует значение из src в dest. Эта инструкция имеет несколько форм, как мы видим в этой таблице:
Другие инструкции, связанные со стеком, выглядят так:
1
Для манипуляций со строками они такие:
Инструкции по управлению потоком
Вот некоторые из безусловных переходов:
Вот некоторые из условных переходов:
Аргументы, локальные переменные и соглашения о вызовах (в x86 и x64)
Существует несколько способов, которыми компиляторы представляют функции, вызовы, локальные переменные и многое другое. Мы не будем охватывать все из них, но мы рассмотрим некоторые из них. Мы рассмотрим стандартный вызов (stdcall), который используется только в x86, а затем рассмотрим различия между другими вызовами и stdcall.
Stdcall
Регистры стека, rsp/esp и rbp/ebp выполняют большую часть работы, когда речь идет об аргументах и локальных переменных. Инструкция call сохраняет адрес возврата в верхней части стека перед передачей выполнения новой функции, а инструкция ret в конце функции возвращает выполнение обратно в вызывающую функцию, используя адрес возврата, сохраненный в стеке.
Аргументы
Для stdcall аргументы также помещаются в стек от последнего аргумента к первому следующим образом:
Push Arg02
Push Arg01
Call Func01
В функции Func01 доступ к аргументам можно получить с помощью rsp/esp, но с учетом того, сколько значений было перемещено на вершину стека с течением времени, примерно так:
mov eax, [esp + 4] ;Arg01
push eax
mov ecx, [esp + 8] ; Arg01 keeping in mind the previous push
В этом случае передается значение, расположенное по адресу, указанному значением в квадратных скобках. К счастью, современные инструменты статического анализа, такие как IDA Pro, могут определить, к какому аргументу осуществляется доступ в каждой инструкции, как в этом случае.
Наиболее распространенный способ доступа к аргументам, а также к локальным переменным — использование rbp/ebp. Во-первых, вызываемая функция должна сохранить текущий rsp/esp в регистре rbp/ebp, а затем получить к ним доступ следующим образом:
push ebp
mov ebp, esp
...
mov ecx, [ebp + 8] ;Arg01
push eax
mov ecx, [ebp + 8] ;still Arg01 (no changes)
И в конце вызванной функции она возвращает исходное значение rbp/ebp и rsp/esp следующим образом:
mov esp,ebp
pop ebp
ret
Так как это общий эпилог функции, Intel создала для него специальную инструкцию, которая называется leave, поэтому она стала такой:
Leave
Ret
Локальные переменные
Для локальных переменных вызываемая функция выделяет для них место, уменьшая значение регистра rsp/esp. Чтобы выделить место для двух переменных по четыре байта каждая, код будет таким:
pushebp
mov ebp,esp
sub esp, 8
Кроме того, конец функции будет таким:
mov ebp,esp
pop ebp
ret
Кроме того, если есть аргументы, инструкция ret очищает стек, учитывая количество байтов, которые нужно извлечь из вершины стека, следующим образом:
ret 8 ;2 Arguments, 4 bytes each
cdecl
cdecl (объявление c) — еще одно соглашение о вызовах, которое использовалось многими компиляторами C в x86. Он очень похож на stdcall, с той лишь разницей, что вызывающая программа очищает стек после того, как вызываемая функция (вызванная функция) возвращается следующим образом:
Caller:
push Arg02
push Arg01
call Callee
add esp, 8 ;cleans the stack
fastcall
Соглашение о вызовах __fastcall также широко используется различными компиляторами, включая компилятор Microsoft C++ и GCC. Это соглашение о вызовах передает первые два аргумента в ecx и edx и помещает оставшиеся аргументы в стек. Он используется только в x86, поскольку в Windows существует только одно соглашение о вызовах для x64.
thiscall
Для объектно-ориентированного программирования и для нестатических функций-членов (таких как функции классов) компилятору C необходимо передать адрес объекта, к атрибуту которого будет осуществляться доступ или которым будут манипулировать, используя его в качестве аргумента.
В компиляторе GCC thiscall почти идентичен соглашению о вызовах cdecl и передает адрес объекта в качестве первого аргумента. Но в компиляторе Microsoft C++ он похож на stdcall и передает адрес объекта в ecx. Такие шаблоны часто встречаются в некоторых семействах объектно-ориентированных вредоносных программ.
Соглашение о вызовах x64
В x64 соглашение о вызовах больше зависит от регистров. В Windows вызывающая функция передает первые четыре аргумента в регистры в следующем порядке: rcx, rdx, r8, r9, а остальные помещаются обратно в стек. В то время как для других операционных систем первые шесть аргументов обычно передаются в регистры в таком порядке: rdi, rsi, rdx, rcx, r8, r9, а остальные в стек.
В обоих случаях вызываемая функция очищает стек после использования ret imm, и это единственный способ очистить стек для этих операционных систем в x64.
Изучение ассемблера ARM
Большинство читателей, вероятно, более знакомы с архитектурой x86, в которой реализован дизайн CISC, и могут задаться вопросом — а зачем нам вообще нужно что-то еще? Основное преимущество RISC-архитектур заключается в том, что для процессоров, которые их реализуют, обычно требуется меньше транзисторов, что в конечном итоге делает их более энергоэффективными и теплоэффективными и снижает связанные с этим производственные затраты, что делает их лучшим выбором для портативных устройств. Мы начинаем знакомство с архитектурой RISC с ARM по уважительной причине — на данный момент это наиболее широко используемая архитектура в мире.
Объяснение простое — процессоры, реализующие его, можно найти на множестве мобильных устройств и устройств, таких как телефоны, игровые приставки или цифровые камеры, число которых значительно превышает число ПК. По этой причине несколько семейств вредоносных программ IoT и мобильных вредоносных программ, нацеленных на платформы Android и iOS, имеют полезные нагрузки для архитектуры ARM; пример можно увидеть на следующем снимке экрана:
Таким образом, чтобы иметь возможность их анализировать, необходимо сначала понять, как работает ARM.
Первоначально ARM обозначало Acorn RISC Machine, а затем Advanced RISC Machine. Acorn была британской компанией, которую многие считали британской Apple, производившей одни из самых мощных ПК того времени. Позже он был разделен на несколько независимых организаций, при этом Arm Holdings (в настоящее время принадлежащая SoftBank Group) поддерживает и расширяет текущий стандарт.
Его поддерживают несколько операционных систем, включая Windows, Android, iOS, различные дистрибутивы Unix/Linux и многие другие менее известные встроенные ОС. Поддержка 64-битного адресного пространства была добавлена в 2011 году с выпуском стандарта ARMv8.
В целом доступны следующие профили архитектуры ARM:
* Профили приложений (суффикс A, например, семейство Cortex-A): реализует традиционную архитектуру ARM и поддерживает архитектуру системы виртуальной памяти на основе блока управления памятью (MMU). Эти профили поддерживают наборы инструкций ARM и Thumb (как будет описано ниже).
* Профили реального времени (суффикс R, например, семейство Cortex-R): они реализуют традиционную архитектуру ARM и поддерживают архитектуру системы защищенной памяти, основанную на блоке защиты памяти (MPU).
* Профили микроконтроллеров (суффикс M, например, семейство Cortex-M): реализуют программируемую модель и предназначены для интеграции в программируемые вентильные матрицы (FPGA).
Каждое семейство имеет свой собственный соответствующий набор связанных архитектур (например, 32-разрядное семейство Cortex-A включает в себя архитектуры ARMv7-A и ARMv8-A), которые, в свою очередь, включают в себя несколько ядер (например, архитектура ARMv7-R включает в себя Cortex- R4, Cortex-R5 и так далее).
Основы
Здесь мы рассмотрим как исходную 32-битную, так и более новую 64-битную архитектуру. Со временем было выпущено несколько версий, начиная с ARMv1. В этой книге мы сосредоточимся на последних их версиях.
ARM — это архитектура load-store; она делит все инструкции на следующие две категории:
* Доступ к памяти: перемещает данные между памятью и регистрами
* Операции арифметико-логического устройства (АЛУ): выполняет вычисления с использованием регистров.
ARM поддерживает арифметические операции сложения, вычитания и умножения, а некоторые новые версии, начиная с ARMv7, также поддерживают операции деления. Он поддерживает порядок с обратным порядком байтов и по умолчанию использует формат с прямым порядком байтов.
На 32-битном ARM в любое время доступны 16 регистров: R0-R15. Это число удобно, так как требуется всего 4 бита, чтобы определить, какой регистр будет использоваться. Из них 13 (иногда называемые 14, включая R14, или 15, также включая R13) являются регистрами общего назначения: каждый из R13 и R15 имеет специальную функцию, а R14 может выполнять ее время от времени. Давайте рассмотрим их подробнее:
-R0-R7: младшие регистры одинаковы во всех режимах ЦП.
- R8-R12: старшие регистры одинаковы во всех режимах ЦП, за исключением режима быстрого запроса прерывания (FIQ), недоступного для 16-битных инструкций.
- R13 (также известный как SP): указатель стека — указывает на вершину стека, и каждый режим ЦП имеет свою собственную версию. Не рекомендуется использовать его в качестве GPR.
- R14 (также известный как LR): регистр связи — в пользовательском режиме он содержит адрес возврата для текущей функции, в основном, когда выполняются инструкции BL (переход со связью) или BLX (переход с связью и обменом). Его также можно использовать как GPR, если адрес возврата хранится в стеке. Каждый режим ЦП имеет свою собственную версию.
- R15 (также известный как PC): Счетчик программ, указывает на текущую выполняемую команду. Это не GPR.
Всего на большинстве архитектур ARM имеется 30 32-битных регистров общего назначения, включая экземпляры с одинаковыми именами в разных режимах ЦП.
Помимо них, есть несколько других важных регистров, а именно:
* Регистр состояния текущей программы (CPSR): содержит биты, описывающие текущий режим процессора, состояние процессора и некоторые другие значения.
* Сохраненные регистры состояния программы (SPSR): в них сохраняется значение CPSR при возникновении исключения, поэтому его можно восстановить позже. Каждый режим ЦП имеет свою собственную версию, за исключением пользовательского и системного режимов, поскольку они не являются режимами обработки исключений.
* Регистр состояния прикладной программы (APSR): в нем хранятся копии флагов состояния ALU, также известных как флаги кода состояния, а в более поздних архитектурах он также содержит флаги Q (насыщение) и флаги больше или равно (GE).
Количество регистров с плавающей запятой (FPR) для 32-битной архитектуры может варьироваться, в зависимости от ядра, до 32.
ARMv8 (64-разрядная версия) имеет 31 универсальный X0-X30 (также можно найти нотацию R0-R30) и 32 FPR, доступных в любое время. Нижняя часть каждого регистра имеет префикс W и может быть доступна как W0-W30.
Есть несколько регистров, которые имеют определенную цель, а именно:
ARMv8 определяет четыре уровня исключений (EL0-EL3), и каждый из трех последних регистров получает свою собственную копию каждого из них; ELR и SPSR не имеют отдельной копии для EL0.
Нет регистра с именем X31 или W31; число 31 во многих инструкциях представляет нулевой регистр ZR (WZR/XZR). X29 можно использовать как указатель кадра (в котором хранится исходная позиция в стеке), а X30 — как регистр связи (в котором хранится возвращаемое значение из функций).
Что касается соглашения о вызовах, R0-R3 на 32-разрядном ARM и X0-X7 на 64-разрядном ARM используются для хранения значений аргументов, переданных функциям, а остальные аргументы, при необходимости, передаются через стек, R0-R1 и X0- X7 (и X8, также косвенно известный как XR) для хранения возвращаемых результатов. Если тип возвращаемого значения слишком велик для них, то необходимо выделить пространство и вернуть его в виде указателя. Кроме того, R12 (32-разрядный) и X16-X17 (64-разрядный) могут использоваться в качестве временных регистров внутри вызова процедуры (с помощью так называемых виниров и кода таблицы связывания процедур), R9 (32-разрядный) и X18 (64-разрядная версия) может использоваться в качестве регистров платформы (для конкретных целей ОС), если это необходимо, в противном случае они используются так же, как и другие временные регистры.
Как упоминалось ранее, в соответствии с официальной документацией реализовано несколько режимов ЦП, а именно:
Наборы инструкций
Для процессоров ARM доступно несколько наборов инструкций: ARM и Thumb. Говорят, что процессор, выполняющий инструкции ARM, работает в состоянии ARM, и наоборот. Процессоры ARM всегда запускаются в состоянии ARM, а затем программа может переключиться в состояние Thumb с помощью инструкции BX. Среда выполнения Thumb (ThumbEE) была представлена относительно недавно в ARMv7 и основана на Thumb с некоторыми изменениями и дополнениями для облегчения динамического генерирования кода.
Инструкции ARM имеют длину 32 бита (как для AArch32, так и для AArch64), в то время как инструкции Thumb и ThumbEE имеют длину 16 или 32 бита (первоначально почти все инструкции Thumb были 16-битными, а Thumb-2 представил сочетание 16- и 32-битных инструкций).
Согласно официальной документации все инструкции можно разделить на следующие категории:
Для взаимодействия с ОС к системным вызовам можно получить доступ с помощью инструкции Software Interrupt (SWI), которая позже была переименована в инструкцию Supervisor Call (SVC).
См. официальную документацию ARM, чтобы получить точный синтаксис любой инструкции. Вот пример того, как это может выглядеть:
SVC{условие} #imm
Код {условие} в этом случае будет кодом условия. ARM поддерживает несколько кодов состояния, а именно:
EQ: равно
NE: не равно
CS/HS: установить перенос
CC/LO: сбросить перенос
MI: отрицательный
PL: Положительный или нулевой
VS: переполнение
VC: нет переполнения
HI: выше без знака
LS: беззнаковый ниже или оба
GE: со знаком больше или равно
LT: со знаком менее
GT: со знаком больше
LE: со знаком меньше или равно
AL: Всегда (обычно опускается)
Значение imm означает непосредственное значение.
Основы MIPS
Микропроцессор без взаимосвязанных конвейерных стадий (MIPS) был разработан по технологиям MIPS . Подобно ARM, сначала это была 32-битная архитектура с добавленной позже 64-битной функциональностью. Используя преимущества RISC ISA, процессоры MIPS характеризуются низким энергопотреблением и энергопотреблением. Их часто можно найти в нескольких встроенных системах, таких как маршрутизаторы и шлюзы, а также в некоторых игровых консолях, таких как Sony PlayStation. К сожалению, из-за популярности этой архитектуры системы, они стали мишенью для нескольких семейств вредоносных программ IoT. Пример можно увидеть на следующем скриншоте:
По мере развития архитектуры появилось несколько ее версий, начиная с MIPS I и заканчивая V, а затем несколько выпусков более поздних MIPS32/MIPS64. MIPS64 остается обратно совместимым с MIPS32. Эти базовые архитектуры могут быть дополнительно дополнены необязательными архитектурными расширениями, называемыми Application Specific Extension (ASE), и модулями для повышения производительности для определенных задач, которые обычно мало используются вредоносным кодом. MicroMIPS32/64 — это надмножества архитектур MIPS32 и MIPS64 соответственно с почти таким же 32-битным набором инструкций и дополнительными 16-битными инструкциями для уменьшения размера кода. Они используются там, где требуется сжатие кода, и предназначены для микроконтроллеров и других небольших встраиваемых устройств.
Основы
MIPS поддерживает двунаправленность байтов. Доступны следующие регистры:
*32 GPR r0-r31, 32-битный размер для MIPS32 и 64-битный размер для MIPS64.
* Специальный регистр PC, на который некоторые инструкции могут влиять только косвенно.
* Два специальных регистра для хранения результатов целочисленного умножения и деления (HI и LO). Эти регистры и связанные с ними инструкции были удалены из базового набора инструкций в выпуске 6 и теперь существуют в модуле процессора цифровых сигналов (DSP).
Причина использования 32 GPR проста — MIPS использует 5 бит для указания регистра, поэтому таким образом мы можем иметь максимум 2^5 = 32 различных значения. Два GPR имеют определенную цель, а именно:
* Регистр r0 (иногда называемый $0 или $zero) является постоянным регистром, в нем всегда хранится ноль, и он обеспечивает доступ только для чтения. Его можно использовать как аналог /dev/null для отбрасывания вывода какой-либо операции или как быстрый источник нулевого значения.
* r31 (также известный как $ra) хранит адрес возврата во время инструкций перехода/перехода вызова процедуры и ссылки.
Другие регистры обычно используются для определенных целей, а именно:
* r1 (также известный как $at): временный ассемблер — используется при разрешении псевдоинструкций.
* r2-r3 (также известные как $v0 и $v1): значения — содержат значения возвращаемой функции.
* r4-r7 (также известные как $a0-$a3): Аргументы — используются для передачи аргументов функции.
* r8-r15 (также известные как $t0-$t7/$a4-$a7 и $t4-$t7): временные — первые четыре могут также использоваться для предоставления аргументов функций в соглашениях о вызовах N32 и N64 (еще один вызов O32). соглашение использует только регистры r4-r7; последующие аргументы передаются в стеке)
* r16-r23 (также известные как $s0-$s7): cохраненные временные — сохраняются при вызовах функций.
* r24-r25 (также известные как $t8-$t9): dременные
* r26-r27 (также известный как $k0-$k1): обычно зарезервирован для ядра ОС.
* r28 (также известный как $gp): глобальный указатель — указывает на глобальную область (сегмент данных).
* r29 (также известный как $sp): указатель стека
* r30 (также известный как $s8 или $fp): Сохраненный указатель значения/кадра — сохраняет исходный указатель стека (до вызова функции).
MIPS также имеет следующие доступные сопроцессоры:
* CP0: система управления
*CP1: FPU
* CP2: зависит от реализации
CP3: FPU (имеет специальные инструкции типа кода операции COP1X)
Набор инструкций
Большинство основных инструкций были введены в MIPS I и II. MIPS III представила 64-битные целые числа и адреса, а MIPS IV и V улучшили операции с плавающей запятой и добавили новый набор для повышения общей эффективности. Каждая инструкция там имеет одинаковую длину — 32 бита (4 байта), и любая инструкция начинается с кода операции, который занимает 6 бит. Поддерживаются три основных формата инструкций: R, I и J:
Для операций, связанных с FPU, существуют аналогичные типы FR и FI.
Помимо этого, существует несколько других менее распространенных форматов, в основном сопроцессоры и форматы, связанные с расширениями.
В документации регистры обычно имеют следующие суффиксы:
* Источник(s)
* Цель (t)
* Назначение (d)
Все инструкции можно разделить на следующие несколько групп в зависимости от типа функциональности:
* Поток управления — в основном состоит из условных и безусловных переходов и ответвлений:
- JR: перейти на регистр (формат J)
- BLTZ: переход если меньше нуля (формат I)
* Доступ к памяти — операции загрузки и сохранения:
- LB: загрузить байт (формат I)
- SW: сохранить слово (формат I)
* ALU — охватывает различные арифметические операции:
- ADDU: сложить без знака (формат R)
- XOR: исключающее или (формат R)
- SLL: логический сдвиг влево (формат R)
*Взаимодействие с ОС через исключения — взаимодействует с ядром ОС:
- SYSCALL: системный вызов (пользовательский формат)
- BREAK: точка останова (пользовательский формат)
В большинстве случаев инструкции с плавающей запятой будут иметь одинаковые имена для одних и тех же типов операций, например, ADD.S. Некоторые инструкции более уникальны, например Check for Equal (C.EQ.D).
Как мы увидим здесь и далее, одни и те же базовые группы можно применить практически к любой архитектуре, и единственная разница будет заключаться в реализации. Некоторые общие операции могут получить свои собственные инструкции, чтобы извлечь выгоду из оптимизации и, таким образом, уменьшить размер кода и повысить производительность.
Поскольку набор инструкций MIPS довольно минималистичный, также существуют макросы ассемблера, называемые псевдоинструкциями. Вот некоторые из наиболее часто используемых:
* ABS: абсолютное значение — преобразуется в комбинацию ADDU, BGEZ и SUB.
* BLT: переходи если меньше чем — переводится как комбинация SLT и BNE.
* BGT/BGE/BLE: аналогично BLT
* LI/LA: немедленная загрузка/адрес — преобразуется в комбинацию LUI и ORI или ADDIU для 16-битного LI.
* MOVE: перемещает содержимое одного регистра в другой — преобразует в ADD/ADDIU с нулевым значением.
* NOP: нет операции — преобразуется в SLL с нулевыми значениями.
* NOT: логическое NOT — преобразуется в NOR.
Погружение в PowerPC
PowerPC расшифровывается как «Оптимизация производительности с помощью Enhanced RISC — Performance Computing» и иногда пишется как PPC. Он был создан в начале 1990-х годов альянсом Apple, IBM и Motorola (обычно сокращенно AIM). Первоначально он предназначался для использования в ПК и до 2006 года использовался в продуктах Apple, включая PowerBook и iMac. Процессоры, реализующие его, также можно найти в игровых консолях, таких как Sony PlayStation 3, XBOX 360 и Wii, а также в серверах IBM и множестве встроенных устройств, таких как контроллеры автомобилей и самолетов, и даже в знаменитом роботе ASIMO. Позже административные обязанности были переданы органу открытых стандартов Power.org, членами которого остались некоторые из бывших создателей, такие как IBM и Freescale. Затем они отделились от Motorola и позже были приобретены NXP Semiconductors, а также многими другими компаниями. OpenPOWER Foundation — это новая инициатива IBM, Google, NVIDIA, Mellanox и Tyan, целью которой является содействие совместной разработке этой технологии.
PowerPC был в основном основан на IBM POWER ISA, а позже была выпущена унифицированная Power ISA, которая объединила POWER и PowerPC в единую ISA, которая теперь используется во многих продуктах под общим термином Power Architecture.
Существует множество семейств вредоносных программ для Интернета вещей, которые имеют полезную нагрузку для этой архитектуры.
Основы
Power ISA делится на несколько категорий; каждую категорию можно найти в определенной части спецификации или книги. ЦП реализуют набор этих категорий в зависимости от своего класса; только базовая категория является обязательной. Вот список основных категорий и их определений в последнем втором стандарте:
*База: описана в Книге I (Архитектура набора пользовательских инструкций Power ISA) и Книге II (Архитектура виртуальной среды Power ISA)
*Сервер: описан в Книге III-S (Архитектура операционной среды Power ISA — Серверная среда)
* Ембедед: Книга III-E (Архитектура операционной среды Power ISA — встроенная среда)
Существует множество более подробных категорий, охватывающих такие аспекты, как операции с плавающей запятой и кэширование определенных инструкций.
В другой книге, Book VLE (Архитектура операционной среды Power ISA — Архитектура инструкций кодирования переменной длины (VLE)), определяются альтернативные инструкции и определения, предназначенные для увеличения плотности кода за счет использования 16-битных инструкций в отличие от более распространенных 32-битных. те.
Power ISA версии 3 состоит из трех книг с теми же названиями, что и книги с I по III предыдущего стандарта, без различий между средами.
Процессор запускается в режиме с обратным порядком байтов, но может переключаться, изменяя бит в MSR (регистре состояния машины), так что поддерживается двунаправленный порядок байтов.
В Power ISA задокументировано множество наборов регистров, в основном сгруппированных либо по соответствующему объекту, либо по категории. Вот краткое изложение наиболее часто используемых:
* 32 GPR для операций с целыми числами, обычно используются только по их количеству (64-битные)
* 64 векторных скалярных регистра (VSR) для векторных операций и операций с плавающей запятой:
- 32 векторных регистра (VR) как часть VSR для векторных операций (128 бит)
- 32 FPR в составе VSR для операций с плавающей запятой (64-бит)
* Регистры средств фиксированной точки специального назначения, такие как следующие:
- Регистр исключений с фиксированной запятой (XER) — содержит несколько битов состояния (64 бита).
* Регистры переходов:
- Регистр условий (CR) — состоит из 8 4-битных полей, CR0-CR7, включая такие вещи, как поток управления и сравнение (32-битные)
- Link регистр (LR) — обеспечивает целевой адрес перехода (64-битный)
- Регистр счетчика (CTR) — содержит счетчик циклов (64-разрядный).
- Целевой регистр доступа (TAR) — указывает целевой адрес ветки (64-разрядный).
* Регистры объекта таймера:
- Time Base (TB) — периодически увеличивается с заданной частотой (64-бит)
* Другие регистры специального назначения из определенной категории, в том числе следующие:
- Аккумулятор (ACC) (64-разрядный) — категория механизма обработки сигналов (SPE).
Как правило, функции могут передавать все аргументы в регистры для нерекурсивных вызовов; дополнительные аргументы передаются в стек.
Набор инструкций
Большинство инструкций имеют 32-битный размер, только группа кодирования переменной длины (VLE) меньше, чтобы обеспечить более высокую плотность кода для встраиваемых приложений. Все инструкции разбиты на следующие три категории:
* Определено: все инструкции определены в книгах Power ISA.
* Недопустимо: Доступно для будущих расширений Power ISA. Попытка выполнить их вызовет обработчик ошибок недопустимых инструкций.
* Зарезервировано: предназначено для определенных целей, которые не входят в сферу действия Power ISA. Попытка выполнить их либо выполнит реализованное действие, либо вызовет обработчик ошибок недопустимой инструкции, если реализация недоступна.
Биты с 0 по 5 всегда определяют код операции, и многие инструкции также имеют расширенный код операции. Поддерживается большое количество форматов инструкций; Вот некоторые примеры:
* I-ФОРМА [OPCD+LI+AA+LK]
* B-ФОРМА [OPCD+BO+BI+BD+AA+LK]
Каждое поле инструкций имеет собственное сокращение и значение; имеет смысл обратиться к официальному документу Power ISA, чтобы получить их полный список и соответствующие им форматы. В случае ранее упомянутой I-FORM они таковы:
* OPCD: код операции
* LI: непосредственное поле, используемое для указания 24-битного целого числа в дополнении до двух со знаком.
* AA: Бит абсолютного адреса
* LK: Бит ссылки, влияющий на регистр ссылки
Инструкции также разделены на группы в соответствии с соответствующим средством и категорией, что делает их очень похожими на регистры:
* Инструкции ветвления:
- b/ba/bl/bla: ветвление
- bc/bca/bcl/bcla: условное ветвление
- sc: системный вызов
* Инструкции с фиксированной точкой:
- lbz: загрузить байт и ноль
- stb: сохранить байт
- addi: добавить немедленно
- ori: или немедленно
* Инструкции с плавающей запятой:
- fmr: Floating move register
- lfs: загрузить одно число с плавающей запятой
- stfd: сохранить двойное число с плавающей запятой
* Инструкции SP:
- brinc: приращение с обратным битом
Ассемблер SuperH
SuperH, часто сокращенно обозначаемый как SH, представляет собой RISC ISA, разработанный Hitachi. SuperH прошел несколько итераций, начиная с SH-1 и заканчивая SH-4. Более поздний SH-5 имеет два режима работы, один из которых идентичен инструкциям пользовательского режима SH-4, а другой, SHmedia, совершенно другой. Каждое семейство занимает свою рыночную нишу:
* SH-1: Бытовая техника
* SH-2: Автомобильные контроллеры и игровые приставки, такие как Sega Saturn.
* SH-3: Мобильные приложения, такие как автомобильные навигаторы
* SH-4: Автомобильные мультимедийные терминалы и игровые приставки, такие как Sega Dreamcast.
* SH-5: Мультимедийные приложения высокого класса.
Микроконтроллеры и процессоры, реализующие его, в настоящее время производятся Renesas Electronics, совместным предприятием групп Hitachi и Mitsubishi Semiconductor. Поскольку вредоносное ПО IoT в основном нацелено на системы на базе SH-4, мы сосредоточимся на этом семействе SuperH.
Основы
Что касается регистров, SH-4 предлагает следующее:
* 16 регистров общего назначения R0-R15 (32-разрядные)
* 7 регистров управления (32-разрядные):
- Глобальный базовый регистр (GBR)
- Регистр состояния (SR)
- Сохраненный регистр состояния (SSR)
- Сохраненный счетчик программ (SPC)
- Векторный базовый счетчик (VBR)
- Сохраненный общий регистр 15 (SGR)
- Debug Base Register (DBR) (только из привилегированного режима)
* 4 системных регистра (32-битные):
- MACH/MACL: регистры умножения и накопления
- PR: регистр процедур
- PC: счетчик программ
- FPSCR: Регистр состояния/управления с плавающей запятой
* 32 регистра FPU FR0-FR15 (также известные как DR0/2/4/... или FV0/4/...) и XF0-XF15 (также известные как XD0/2/4/... или XMTRX); два банка по 16 одинарной точности (32-разрядные) или восемь с двойной точностью (64-разрядные) FPR и FPUL (регистр связи с плавающей запятой) (32-разрядный)
Обычно R4-R7 используются для передачи аргументов функции, результат которой возвращается в R0. R8-R13 сохраняются для нескольких вызовов функций. R14 служит указателем кадра, а R15 — указателем стека.
Что касается форматов данных, в SH-4 слово занимает 16 бит, длинное слово — 32 бита, а учетверенное слово — 64 бита.
Поддерживаются два режима процессора: пользовательский режим и привилегированный режим. SH-4 обычно работает в пользовательском режиме и переключается в привилегированный режим в случае исключения или прерывания.
Набор инструкций
SH-4 имеет набор инструкций, который совместим снизу вверх с семействами SH-1, SH-2 и SH-3. Он использует 16-битные инструкции фиксированной длины, чтобы уменьшить размер программного кода. За исключением BF и BT, все инструкции ветвления и RTE (инструкция возврата из исключения) реализуют так называемые отложенные ветвления, когда инструкция, следующая за ветвью, выполняется до инструкции назначения ветвления.
Все инструкции разделены на следующие категории (с некоторыми примерами):
* Инструкции по передаче с фиксированной точкой:
- MOV: перемещение данных (или указанных определенных типов данных)
- SWAP: обмен местами половинки регистра
* Инструкции по арифметическим операциям:
- SUB: вычесть двоичные числа
- CMP/EQ: условное сравнение (в данном случае на равное)
* Инструкции по логической операции:
- AND: логический и
- XOR: исключающее ИЛИ
* Инструкции сдвига:
- ROTL: сдвиг влево
- SHLL: логический сдвиг влево
* инструкции ветвления:
- BF: перейти, если ложь
- JMP: перейти (безусловная ветвь)
* Инструкции по управлению системой:
- LDC: загрузить в управляющий регистр
- STS: сохранить в системный реестр
* Инструкции одинарной точности с плавающей запятой:
- FMOV: перемещение с плавающей запятой
* Инструкции двойной точности с плавающей запятой:
- FABS: абсолютное значение с плавающей запятой
* Инструкции управления с плавающей запятой:
- LDS: загрузить в системный регистр FPU
Работа со SPARC
Архитектура масштабируемого процессора (SPARC) — это RISC ISA, первоначально разработанная Sun Microsystems (теперь часть корпорации Oracle). Первая реализация использовалась в собственных рабочих станциях и серверных системах Sun. Позже он был лицензирован для нескольких других производителей, одним из которых была Fujitsu. Поскольку Oracle закрыла SPARC Design в 2017 году, все будущие разработки продолжались с Fujitsu в качестве основного поставщика серверов SPARC.
Существует несколько реализаций архитектуры SPARC с полностью открытым исходным кодом. В настоящее время его поддерживают несколько операционных систем, включая системы Oracle Solaris, Linux и BSD, а для нескольких семейств вредоносных программ IoT также есть специальные модули.
Основы
Согласно документации по архитектуре Oracle SPARC, конкретная реализация может содержать от 72 до 640 64-битных регистров R общего назначения. Однако одновременно видны только 31/32; 8 — это глобальные регистры, от R[0] до R[7] (также известные как g0-g7), причем первый регистр, g0, жестко привязан к 0; и 24 связаны со следующими окнами регистров:
* Восемь в регистрах in[0]-in[7] (R[24]-R[31]): для передачи аргументов и возврата результатов
* Восемь локальных регистров local[0]-local[7] (R[16]-R[23]): для хранения локальных переменных
* Восемь выходных регистров out[0]-out[7] (R[8]-R[15]): для передачи аргументов и возврата результатов
Инструкция CALL записывает свой адрес в регистр out[7] (R[15]).
Чтобы передать аргументы функции, они должны быть помещены в регистры out и, когда функция получит управление, она будет обращаться к ним в своих регистрах in. Дополнительные аргументы могут быть предоставлены через стек. Результат помещается в первый входной регистр, который затем становится первым выходным регистром, когда функция возвращается. Инструкции SAVE и RESTORE используются для выделения нового окна регистра и последующего восстановления предыдущего соответственно.
SPARC также имеет 32 FPR с одинарной точностью (32-битные), 32 FPR с двойной точностью (64-битные) и 16 FPR с четырехкратной точностью (128-битные), некоторые из которых перекрываются.
Помимо этого, существует множество других регистров, которые служат конкретным целям, в том числе следующие:
* FPRS: содержит информацию о режиме и состоянии FPU.
* Вспомогательные регистры состояния (ASR 0, ASR 2-6, ASR 19-22 и ASR 24-28 не зарезервированы): служат нескольким целям, включая следующие:
- ASR 2: Регистр кодов состояния (CCR)
- ASR 5: PC
- ASR 6: FPRS
- ASR 19: Регистр общего состояния (GSR)
* Регистры состояния PR окна регистров (PR 9-14): определяют состояние окон регистров, включая следующее:
- PR 9: Указатель текущего окна (CWP)
- PR 14: Состояние окна (WSTATE)
* Регистры состояния PR без регистрации окна (PR 0–3, PR 5–8 и PR 16): видны только программному обеспечению, работающему в привилегированном режиме.
32-разрядный SPARC использует обратный порядок байтов, а 64-разрядный SPARC использует инструкции с обратным порядком байтов, но может обращаться к данным в любом порядке. В SPARC также используется понятие ловушек, реализующих передачу управления привилегированному программному обеспечению с использованием специальной таблицы, которая может содержать первые 8 инструкций (32 для некоторых часто используемых ловушек) каждого обработчика ловушек. Базовый адрес таблицы устанавливается программным обеспечением в регистре базового адреса прерывания (TBA).
Набор инструкций
Команда из ячейки памяти, указанной ПК, извлекается и выполняется, а затем новые значения присваиваются ПК и счетчику следующей программы (NPC), который является псевдорегистром.
Подробные форматы инструкций можно найти в описаниях отдельных инструкций.
Вот основные категории поддерживаемых инструкций с примерами:
* Доступ к памяти:
- LDUB: загрузить беззнаковый байт
- ST: сохранить
* Арифметические/логические/сдвиг:
- ADD: добавить
- SLL: логический сдвиг влево
* Передача управления:
- BE: ветвление если равно
- JMPL: переход
- CALL: вязов
- RETURN: Возврат из функции
* Доступ к регистру статуса:
- WRCCR: запись CCR
* Операции с плавающей запятой:
- FOR: логическое ИЛИ для F-регистров
* Условнное перемещение:
- MOVcc: переместить, если условие истинно для выбранного кода условия (cc)
* Регистр управления окнами:
- SAVE: сохранить окно вызова
- FLUSHW: очистить окна регистров
Переход от ассемблера к языкам программирования высокого уровня
Разработчики в основном не пишут на ассемблере. Вместо этого они пишут на языках более высокого уровня, таких как C или C++, и компилятор преобразует этот высокоуровневый код в низкоуровневое представление на языке ассемблера. В этом разделе мы рассмотрим различные блоки кода, представленные в сборке.
Арифметические операторы
Теперь мы рассмотрим различные операторы C и то, как они представлены в ассемблере. В качестве примера мы возьмем Intel IA-32, и та же концепция применима и к другим языкам ассемблера:
* X = 50 (при условии, что 0x00010000 — это адрес переменной X в памяти):
mov eax, 50
mov dword ptr [00010000h],eax
X = Y+50 (при условии, что 0x00010000 представляет X, а 0x00020000 представляет Y):
mov eax, dword ptr [00020000h]
add eax, 50
mov dword ptr [00010000h],eax
X = Y + (50 * 2):
mov eax, dword ptr [00020000h]
push eax ;save Y for now
mov eax, 50 ;do the multiplication first
mov ebx,2
imul ebx ;the result is in edx:eax
mov ecx, eax
pop eax ;gets back Y value
add eax,ecx
mov dword ptr [00010000h],eax
X = Y + (50 / 2):
mov eax, dword ptr [00020000h]
push eax ;save Y for now
mov eax, 50
mov ebx,2
div ebx ;the result in eax, and the remainder is in edx
mov ecx, eax
pop eax
add eax,ecx
mov dword ptr [00010000h],eax
X = Y + (50 % 2)
mov eax, dword ptr [00020000h]
push eax ;save Y for now
mov eax, 50
mov ebx,2
div ebx ;the remainder is in edx
mov ecx, edx
pop eax
add eax,ecx
mov dword ptr [00010000h],eax
Будем надеяться, что это объясняет, как компилятор преобразует эти арифметические выражения в язык ассемблера.
Условия
Основные операторы If могут выглядеть следующим образом:
* if (X == 50) (при условии, что 0x0001000 представляет переменную X):
mov eax, 50
cmp dword ptr [00010000h],eax
* if (X | 00001000b) (| представляет логический элемент ИЛИ):
mov eax, 000001000b
test dword ptr [00010000h],eax
Чтобы понять ветвление и перенаправление потока, давайте взглянем на следующую диаграмму, чтобы увидеть, как это проявляется в псевдокоде:
Чтобы применить эту последовательность ветвления в ассемблере, компилятор использует сочетание условных и безусловных переходов, как показано ниже:
IF.. THEN.. ENDIF:
cmp dword ptr [00010000h],50
jnz 3rd_Block ; if not true
…
Some Code
…
3rd_Block:
Some code
IF.. THEN.. ELSE.. ENDIF:
cmp dword ptr [00010000h],50
jnz Else_Block ; if not true
...
Some code
...
jmp 4th_Block ;Jump after Else
Else_Block:
...
Some code
...
4th_Block:
...
Some code
Цикл while
Цикла while очень похожи на условия if с точки зрения того, как они представлены в ассемблере:
Краткое содержание
В этой главе мы рассмотрели основы компьютерного программирования и описали универсальные элементы, общие для нескольких архитектур CISC и RISC. Затем мы рассмотрели несколько языков ассемблера, в том числе языки, лежащие в основе Intel x86, ARM, MIPS и другие, и поняли области их применения, которые в конечном итоге сформировали дизайн и структуру. Мы также рассмотрели фундаментальные основы каждого из них, изучили наиболее важные понятия (такие как используемые регистры и поддерживаемые режимы процессора), получили представление о том, как выглядят наборы инструкций, узнали, какие форматы кодов операций поддерживаются, и изучили, какие вызовы используются соглашения. Наконец, мы перешли от языков ассемблера низкого уровня к их высокоуровневому представлению в C или других подобных языках и познакомились с набором примеров для универсальных блоков, таких как условия if и циклы.
После прочтения этой главы вы должны научиться читать дизассемблированный код различных языков ассемблера и понять, какой высокоуровневый код он может представлять. Не претендуя на полноту охвата, основная цель этой главы — предоставить прочную основу, а также направление, которому вы можете следовать, чтобы углубить свои знания, прежде чем приступать к анализу фактического вредоносного кода. Это должно стать вашей отправной точкой для изучения того, как выполнять статический анализ кода на разных платформах и устройствах.
В главе 2 «Базовый статический и динамический анализ для x86/x64» мы начнем анализ реального вредоносного ПО для конкретных платформ, а наборы инструкций, с которыми мы познакомились, будут использоваться в качестве языков, описывающих его функциональность.
Прежде чем погрузиться в мир вредоносных программ, нам необходимо иметь полное представление об ядре машин, на которых мы анализируем вредоносное ПО. Для реверс инжиниринга имеет смысл сосредоточиться в основном на архитектуре и операционной системе, которую она поддерживает. Конечно, существует множество устройств и модулей, составляющих систему, но в основном именно эти два момента определяют набор инструментов и подходов, используемых при анализе. Физическим представлением любой архитектуры является процессор. Процессор похож на сердце любого смарт-устройства или компьютера, поскольку он поддерживает их работоспособность.
В этой главе мы рассмотрим основы наиболее широко используемых архитектур, от хорошо известных архитектур x86 и x64 Instruction Set Architecture (ISA) до решений, поддерживающих несколько мобильных устройств и устройств Интернета вещей (IoT), которые часто неправомерно используются семействами вредоносных программ, такие как Mirai и многие другие. Это задаст тон вашему путешествию по анализу вредоносных программ, поскольку статический анализ невозможен без понимания инструкций по ассемблеру. Хотя современные декомпиляторы действительно становятся все лучше и лучше, они существуют не для всех платформ, на которые нацелено вредоносное ПО. Кроме того, они, вероятно, никогда не смогут обрабатывать запутанный код. Не пугайтесь сложности ассемблера; просто нужно время, чтобы к нему привыкнуть, и через некоторое время его можно читать, как любой другой язык программирования. Хотя эта глава является отправной точкой, всегда имеет смысл повышать свои знания, практикуясь и исследуя дальше.
Эта глава разделена на следующие разделы для облегчения процесса обучения:
* Базовые концепции
* Языки ассемблера
* Знакомство с x86 (IA-32 и x64)
* Изучение ассемблера ARM
* Основы MIPS
* Ассемблер SuperH
* Работа со SPARC
* Переход от ассемблера к языкам программирования высокого уровня
Базовые концепции
Большинство людей на самом деле не понимают, что процессор в значительной степени является умным калькулятором. Если вы посмотрите на большинство его инструкций (независимо от языка ассемблера), вы обнаружите, что многие из них имеют дело с числами и выполняют некоторые вычисления. Однако есть несколько особенностей, которые на самом деле отличают процессоры от обычных калькуляторов, например:
* Процессоры имеют доступ к большему объему памяти по сравнению с традиционными калькуляторами. Это пространство памяти дает им возможность хранить миллиарды значений, что позволяет выполнять более сложные операции. Кроме того, они имеют несколько быстрых и небольших блоков памяти, встроенных в микросхему процессора, называемых регистрами.
* Процессоры поддерживают множество типов инструкций, кроме арифметических инструкций, например, изменение потока выполнения на основе определенных условий.
* Процессоры могут взаимодействовать с другими устройствами (например, динамиками, микрофонами, жесткими дисками, графическими картами и т.д.).
Обладая такими функциями в сочетании с большой гибкостью, процессоры стали незаменимыми интеллектуальными машинами для таких технологий, как искусственный интеллект, машинное обучение и другие. В следующих разделах мы рассмотрим эти функции, а позже углубимся в различные языки ассемблера и в то, как эти функции проявляются в наборах инструкций этих языков.
Регистры
Поскольку большинство процессоров имеют доступ к огромному пространству памяти, хранящему миллиарды значений, процессору требуется больше времени для доступа к данным (и это усложняется, как мы увидим позже). Таким образом, для ускорения операций процессора они содержат небольшие и быстрые блоки внутренней памяти, называемые регистрами.
Регистры встроены в микросхему процессора и способны хранить непосредственные значения, которые необходимы при выполнении вычислений и передаче данных из одного места в другое.
Регистры могут иметь разные имена, размеры и функции в зависимости от архитектуры. Вот некоторые из видов, которые широко используются:
* Регистры общего назначения: — это регистры, которые используются для сохранения значений или результатов различных арифметических и логических операций.
* Указатели стека и фрейма: это регистры, которые используются для указания на начало и конец стека.
* Указатель инструкций/счетчик программ: Указатель инструкций используется для указания на начало следующей инструкции, которая должна быть выполнена процессором.
Память
Память играет важную роль в развитии всех интеллектуальных устройств, которые мы видим в настоящее время. Возможность управлять большим количеством значений, текста, изображений и видео в быстрой и энергозависимой памяти позволяет процессорам обрабатывать больше информации и в конечном итоге выполнять более сложные операции, такие как отображение графических интерфейсов в 3D и виртуальной реальности.
Виртуальная память
В современных операционных системах, независимо от того, являются ли они 32-разрядными или 64-разрядными, операционная система выделяет изолированную виртуальную память (в которой ее страницы отображаются на страницы физической памяти) для каждого приложения, чтобы защитить операционную систему и другие приложения и данные.
Обычные приложения должны иметь возможность доступа только к своей виртуальной памяти. У них есть возможность читать, писать или выполнять инструкции на своих страницах виртуальной памяти. Каждой странице виртуальной памяти назначен набор разрешений, которые представляют тип операций, которые приложению разрешено выполнять на этой странице. Эти разрешения доступны для чтения, записи и выполнения. Кроме того, каждой странице памяти может быть назначено несколько разрешений.
Чтобы приложение могло получить доступ к любому значению, хранящемуся в памяти, ему нужен виртуальный адрес, который в основном представляет собой адрес места хранения этого значения в памяти.
Несмотря на знание виртуального адреса, доступ может быть затруднен другой проблемой, которая хранит этот виртуальный адрес. Размер виртуального адреса в 32-битных системах составляет 4 байта, а в 64-битных системах — 8 байтов. Это означает, что нам может понадобиться выделить еще одно место в памяти для хранения этого виртуального адреса. Для этого нового пространства в памяти, чтобы получить к нему прямой доступ только по его адресу, нам нужно будет сохранить его собственный адрес памяти в другом пространстве памяти, что приведет нас к бесконечному циклу, как показано на следующем рисунке:
Для решения этой проблемы в настоящее время используется несколько решений, и в следующем разделе мы рассмотрим одно из них — стек.
Стек
Стек буквально означает кучу объектов. В компьютерных науках стек — это, по сути, структура данных, которая помогает сохранять в памяти разные значения одинакового размера в виде стопки, используя принцип «последним пришел — первым вышел» (LIFO).
На вершину стека (куда будет помещен следующий элемент) указывает специальный указатель стека, который будет более подробно обсуждаться ниже.
Стек является общим для многих языков ассемблера и выполняет несколько функций. Например, это может помочь в решении математических уравнений, таких как X = 5 * 6 + 6 * 2 + 7 (4 + 6), путем сохранения каждого вычисленного значения и помещения каждого в стек, а затем извлечения (или извлечения) их обратно, чтобы вычислить сумму всех из них и сохранить их в переменной X.
Он также часто используется для передачи аргументов (особенно если их много) и хранения локальных переменных.
Стек также используется для сохранения адресов возврата непосредственно перед вызовом функции или подпрограммы. Таким образом, после завершения этой подпрограммы она извлекает адрес возврата из вершины стека и возвращает его туда, откуда она была вызвана, чтобы продолжить выполнение.
В то время как указатель стека обычно указывает на текущую вершину стека, указатель кадра сохраняет адрес вершины стека до вызова подпрограммы, поэтому его можно легко восстановить после возврата.
Ветвление, циклы и условия
Вторая особенность, которой обладают процессоры, — это возможность изменять поток выполнения программы в зависимости от заданного условия. В каждом языке ассемблера есть несколько инструкций сравнения и инструкций управления потоком. Инструкции по управлению потоком можно разделить на следующие категории:
* Безусловный переход: это тип инструкции, которая принудительно изменяет поток выполнения на другой адрес (без каких-либо условий).
* Условный переход: это похоже на логический вентиль, который переключается на другую ветвь на основе заданного условия (например, равно нулю, больше или меньше), как показано на следующем рисунке:
Вызов: Он изменяет выполнение на другую функцию и сохраняет адрес возврата, который будет восстановлен позже, если это необходимо.
Исключения, прерывания и обмен данными с другими устройствами
На языке ассемблера связь с различными аппаратными устройствами осуществляется посредством так называемых прерываний.
Прерывание — это сигнал процессору, отправляемый аппаратным или программным обеспечением, указывающий, что что-то происходит или есть сообщение, которое нужно доставить. Процессор приостанавливает свой текущий запущенный процесс, сохраняя свое состояние, и выполняет функцию, называемую обработчиком прерывания, для обработки этого прерывания. Прерывания имеют собственное обозначение и широко используются для связи с оборудованием для отправки запросов и обработки их ответов.
Существует два типа прерываний. Аппаратные прерывания обычно используются для обработки внешних событий при обмене данными с оборудованием. Программные прерывания вызываются программным обеспечением, обычно вызовом определенной инструкции. Разница между прерыванием и исключением заключается в том, что исключения происходят внутри процессора, а не извне. Примером операции, генерирующей исключение, может быть деление на ноль.
Языки ассемблера
Есть две большие группы архитектур, определяющих языки ассемблера, которые мы рассмотрим в этом разделе: компьютер со сложным набором команд (CISC) и компьютер с сокращенным набором команд (RISC).
CISC против RISC
Не вдаваясь в подробности, основное различие между CISC, такими как Intel IA-32 и x64, и языками RISC, связанными с такими архитектурами, как ARM, заключается в сложности их инструкций.
Языки ассемблера CISC имеют более сложные инструкции. Они сосредоточены на выполнении задач, используя как можно меньше строк инструкций по сборке. Для этого языки ассемблера CISC включают инструкции, которые могут выполнять несколько операций, например mul в ассемблере Intel, который выполняет операции доступа к данным, умножения и хранения данных.
В языке ассемблера RISC инструкции по ассемблеру просты и обычно выполняют только одну операцию каждая. Это может привести к увеличению количества строк кода для выполнения конкретной задачи. Однако это также может быть более эффективным, поскольку исключает выполнение любых ненужных операций.
Типы инструкций
В следующих разделах мы рассмотрим основную структуру каждого языка ассемблера, три основных типа ассемблерных инструкций и то, как они реализованы в каждом из них:
* Манипуляция данными:
- Арифметические манипуляции
- Логические и битовые манипуляции
- Сдвиги и вращения
* Передача данных:
- Передачи между памятью и регистрами
- Передачи между регистрами
* Выполнение потока исполнения:
- Переход или вызов
- Ветвление на основе условия
Знакомство с x86 (IA-32 и x64)
Intel x86 (IA-32 и x64) является наиболее распространенной архитектурой, используемой в ПК и на многих серверах, поэтому неудивительно, что большинство образцов вредоносных программ, которые у нас есть на данный момент, поддерживают ее. IA-32 также обычно называют i386 (замененный i686) или даже просто x86, тогда как x64 также известен как x86-64 или AMD64. x86 — это архитектура CISC, которая включает в себя несколько сложных инструкций в дополнение к простым. В этом разделе мы представим наиболее распространенные из них, а также то, как компиляторы используют их преимущества в своих соглашениях о вызовах.
Регистры
Вот таблица, показывающая взаимосвязь между регистрами в архитектурах IA-32 и x64:
От r8 до r15 доступны только в x64, но не в IA-32, а spl, bpl, sil и dil доступны только в x64.
Первые четыре регистра (rax, rbx, rcx и rdx) являются регистрами общего назначения (GPR), но некоторые из них имеют следующие специальные варианты использования для определенных инструкций:
* rax/eax: Используется для хранения информации и является специальным регистром для некоторых вычислений.
* rcx/ecx: используется как регистр счетчика в инструкциях цикла.
* rdx/edx: используется при делении при возврата модуля
В x64 регистры с r8 по r15 также являются GPR, которые были добавлены к доступным GPR.
Регистр rsp/esp используется как указатель стека, указывающий на вершину стека. Его значение уменьшается, когда значение помещается в стек, и увеличивается, когда значение извлекается из стека. Регистр rbp/ebp используется в качестве указателя кадра и полезен для доступа к локальным переменным и аргументам функции, как мы увидим позже в этом разделе. В дополнение к этому, rbp/ebp иногда используется в качестве GPR для хранения любых данных.
rsi/esi и rdi/edi в основном используются для определения адресов при копировании группы байтов в память. Регистр rsi/esi всегда играет роль источника, а регистр rdi/edi играет роль получателя. Оба регистра являются энергонезависимыми и также являются GPR.
Специальные регистры
В сборке Intel есть два специальных регистра и они следующие:
* rip/eip: Это указатель инструкции, указывающий на следующую выполняемую инструкцию. К нему нельзя получить доступ напрямую, но есть специальные инструкции для доступа к нему.
* rflags/eflags/flags: Этот регистр содержит текущее состояние процессора. На его флаги влияют арифметические и логические инструкции, включая инструкции сравнения, такие как cmp и test, а также он используется с условными переходами и другими инструкциями. Вот самые распространенные флаги:
- Флаг переноса (CF): это когда арифметическая операция выходит за границы; посмотрите на следующую операцию:
mov al, Ffh; al = 0xFF и CF = 0
add al, 1; al = 0 & CF = 1
- Флаг нуля (ZF): Этот флаг устанавливается, когда результат арифметической или логической операции равен нулю. Это также может быть установлено с помощью инструкций сравнения.
- Флаг знака (SF): Этот флаг указывает, что результат операции отрицательный.
- Флаг переполнения (OF): Этот флаг указывает, что в операции произошло переполнение, что привело к изменению знака (только для чисел со знаком), следующим образом:
mov cl, 7Fh; cl = 0x7F (127) & OF = 0
inc cl; cl = 0x80 (-128) & OF = 1
Существуют и другие регистры, такие как регистры MMX и FPU (и инструкции по работе с ними), но мы не будем их рассматривать в этой главе.
Структура инструкции
Для Intel x86 (IA-32 или x64) общей структурой инструкций является opcode, dest, src.
Давайте углубимся в них.
код операции
код операции — это имя инструкции. Некоторые инструкции имеют только код операции без назначения или источника, например:
Nop, pushad, popad, movsb
pushad и popad недоступны в x64.
dest
dest представляет место назначения или место, где будет сохранен результат вычислений, а также становится частью самих вычислений, например:
add eax, ecx; eax = (eax + ecx)
sub rdx, rcx; rdx = (rdx - rcx)
Кроме того, он может играть роль источника и пункта назначения с некоторыми инструкциями кода операции, которые принимают только пункт назначения без источника:
inc eax
dec ecx
Или это может быть только источник или место назначения, например, в случае этих инструкций, которые сохраняют значение в стеке, а затем возвращают его обратно:
push rdx
pop rcx
dest может выглядеть следующим образом:
* REG: регистр, такой как eax и edx.
* r/m: Место в памяти, например следующее:
DWORD PTR [00401000h]
BYTE PTR [EAX + 00401000h]
WORD PTR [EDX*4 + EAX+ 30]
* Значение в стеке (используемое для представления локальных переменных), например следующее:
DWORD PTR [ESP+4]
DWORD PTR [EBP-8]
Src
src представляет источник или другое значение в вычислениях, но впоследствии не сохраняет результаты. Это может выглядеть так:
* REG: например, добавить rcx, r8
* r/m: например, добавьте ecx, dword ptr [00401000h]
* imm: непосредственное значение, такое как mov eax, 00100000h
Набор инструкций
Здесь мы рассмотрим различные типы инструкций, которые мы перечислили в предыдущем разделе.
Инструкции по обработке данных
Вот некоторые из арифметических инструкций:
Кроме того, для логики и манипуляций с битами они выглядят так:
И, наконец, для сдвига и ротаций они такие:
Инструкции по передаче данных
Есть инструкция mov, которая копирует значение из src в dest. Эта инструкция имеет несколько форм, как мы видим в этой таблице:
Другие инструкции, связанные со стеком, выглядят так:
Для манипуляций со строками они такие:
Инструкции по управлению потоком
Вот некоторые из безусловных переходов:
Вот некоторые из условных переходов:
Аргументы, локальные переменные и соглашения о вызовах (в x86 и x64)
Существует несколько способов, которыми компиляторы представляют функции, вызовы, локальные переменные и многое другое. Мы не будем охватывать все из них, но мы рассмотрим некоторые из них. Мы рассмотрим стандартный вызов (stdcall), который используется только в x86, а затем рассмотрим различия между другими вызовами и stdcall.
Stdcall
Регистры стека, rsp/esp и rbp/ebp выполняют большую часть работы, когда речь идет об аргументах и локальных переменных. Инструкция call сохраняет адрес возврата в верхней части стека перед передачей выполнения новой функции, а инструкция ret в конце функции возвращает выполнение обратно в вызывающую функцию, используя адрес возврата, сохраненный в стеке.
Аргументы
Для stdcall аргументы также помещаются в стек от последнего аргумента к первому следующим образом:
Push Arg02
Push Arg01
Call Func01
В функции Func01 доступ к аргументам можно получить с помощью rsp/esp, но с учетом того, сколько значений было перемещено на вершину стека с течением времени, примерно так:
mov eax, [esp + 4] ;Arg01
push eax
mov ecx, [esp + 8] ; Arg01 keeping in mind the previous push
В этом случае передается значение, расположенное по адресу, указанному значением в квадратных скобках. К счастью, современные инструменты статического анализа, такие как IDA Pro, могут определить, к какому аргументу осуществляется доступ в каждой инструкции, как в этом случае.
Наиболее распространенный способ доступа к аргументам, а также к локальным переменным — использование rbp/ebp. Во-первых, вызываемая функция должна сохранить текущий rsp/esp в регистре rbp/ebp, а затем получить к ним доступ следующим образом:
push ebp
mov ebp, esp
...
mov ecx, [ebp + 8] ;Arg01
push eax
mov ecx, [ebp + 8] ;still Arg01 (no changes)
И в конце вызванной функции она возвращает исходное значение rbp/ebp и rsp/esp следующим образом:
mov esp,ebp
pop ebp
ret
Так как это общий эпилог функции, Intel создала для него специальную инструкцию, которая называется leave, поэтому она стала такой:
Leave
Ret
Локальные переменные
Для локальных переменных вызываемая функция выделяет для них место, уменьшая значение регистра rsp/esp. Чтобы выделить место для двух переменных по четыре байта каждая, код будет таким:
pushebp
mov ebp,esp
sub esp, 8
Кроме того, конец функции будет таким:
mov ebp,esp
pop ebp
ret
Кроме того, если есть аргументы, инструкция ret очищает стек, учитывая количество байтов, которые нужно извлечь из вершины стека, следующим образом:
ret 8 ;2 Arguments, 4 bytes each
cdecl
cdecl (объявление c) — еще одно соглашение о вызовах, которое использовалось многими компиляторами C в x86. Он очень похож на stdcall, с той лишь разницей, что вызывающая программа очищает стек после того, как вызываемая функция (вызванная функция) возвращается следующим образом:
Caller:
push Arg02
push Arg01
call Callee
add esp, 8 ;cleans the stack
fastcall
Соглашение о вызовах __fastcall также широко используется различными компиляторами, включая компилятор Microsoft C++ и GCC. Это соглашение о вызовах передает первые два аргумента в ecx и edx и помещает оставшиеся аргументы в стек. Он используется только в x86, поскольку в Windows существует только одно соглашение о вызовах для x64.
thiscall
Для объектно-ориентированного программирования и для нестатических функций-членов (таких как функции классов) компилятору C необходимо передать адрес объекта, к атрибуту которого будет осуществляться доступ или которым будут манипулировать, используя его в качестве аргумента.
В компиляторе GCC thiscall почти идентичен соглашению о вызовах cdecl и передает адрес объекта в качестве первого аргумента. Но в компиляторе Microsoft C++ он похож на stdcall и передает адрес объекта в ecx. Такие шаблоны часто встречаются в некоторых семействах объектно-ориентированных вредоносных программ.
Соглашение о вызовах x64
В x64 соглашение о вызовах больше зависит от регистров. В Windows вызывающая функция передает первые четыре аргумента в регистры в следующем порядке: rcx, rdx, r8, r9, а остальные помещаются обратно в стек. В то время как для других операционных систем первые шесть аргументов обычно передаются в регистры в таком порядке: rdi, rsi, rdx, rcx, r8, r9, а остальные в стек.
В обоих случаях вызываемая функция очищает стек после использования ret imm, и это единственный способ очистить стек для этих операционных систем в x64.
Изучение ассемблера ARM
Большинство читателей, вероятно, более знакомы с архитектурой x86, в которой реализован дизайн CISC, и могут задаться вопросом — а зачем нам вообще нужно что-то еще? Основное преимущество RISC-архитектур заключается в том, что для процессоров, которые их реализуют, обычно требуется меньше транзисторов, что в конечном итоге делает их более энергоэффективными и теплоэффективными и снижает связанные с этим производственные затраты, что делает их лучшим выбором для портативных устройств. Мы начинаем знакомство с архитектурой RISC с ARM по уважительной причине — на данный момент это наиболее широко используемая архитектура в мире.
Объяснение простое — процессоры, реализующие его, можно найти на множестве мобильных устройств и устройств, таких как телефоны, игровые приставки или цифровые камеры, число которых значительно превышает число ПК. По этой причине несколько семейств вредоносных программ IoT и мобильных вредоносных программ, нацеленных на платформы Android и iOS, имеют полезные нагрузки для архитектуры ARM; пример можно увидеть на следующем снимке экрана:
Таким образом, чтобы иметь возможность их анализировать, необходимо сначала понять, как работает ARM.
Первоначально ARM обозначало Acorn RISC Machine, а затем Advanced RISC Machine. Acorn была британской компанией, которую многие считали британской Apple, производившей одни из самых мощных ПК того времени. Позже он был разделен на несколько независимых организаций, при этом Arm Holdings (в настоящее время принадлежащая SoftBank Group) поддерживает и расширяет текущий стандарт.
Его поддерживают несколько операционных систем, включая Windows, Android, iOS, различные дистрибутивы Unix/Linux и многие другие менее известные встроенные ОС. Поддержка 64-битного адресного пространства была добавлена в 2011 году с выпуском стандарта ARMv8.
В целом доступны следующие профили архитектуры ARM:
* Профили приложений (суффикс A, например, семейство Cortex-A): реализует традиционную архитектуру ARM и поддерживает архитектуру системы виртуальной памяти на основе блока управления памятью (MMU). Эти профили поддерживают наборы инструкций ARM и Thumb (как будет описано ниже).
* Профили реального времени (суффикс R, например, семейство Cortex-R): они реализуют традиционную архитектуру ARM и поддерживают архитектуру системы защищенной памяти, основанную на блоке защиты памяти (MPU).
* Профили микроконтроллеров (суффикс M, например, семейство Cortex-M): реализуют программируемую модель и предназначены для интеграции в программируемые вентильные матрицы (FPGA).
Каждое семейство имеет свой собственный соответствующий набор связанных архитектур (например, 32-разрядное семейство Cortex-A включает в себя архитектуры ARMv7-A и ARMv8-A), которые, в свою очередь, включают в себя несколько ядер (например, архитектура ARMv7-R включает в себя Cortex- R4, Cortex-R5 и так далее).
Основы
Здесь мы рассмотрим как исходную 32-битную, так и более новую 64-битную архитектуру. Со временем было выпущено несколько версий, начиная с ARMv1. В этой книге мы сосредоточимся на последних их версиях.
ARM — это архитектура load-store; она делит все инструкции на следующие две категории:
* Доступ к памяти: перемещает данные между памятью и регистрами
* Операции арифметико-логического устройства (АЛУ): выполняет вычисления с использованием регистров.
ARM поддерживает арифметические операции сложения, вычитания и умножения, а некоторые новые версии, начиная с ARMv7, также поддерживают операции деления. Он поддерживает порядок с обратным порядком байтов и по умолчанию использует формат с прямым порядком байтов.
На 32-битном ARM в любое время доступны 16 регистров: R0-R15. Это число удобно, так как требуется всего 4 бита, чтобы определить, какой регистр будет использоваться. Из них 13 (иногда называемые 14, включая R14, или 15, также включая R13) являются регистрами общего назначения: каждый из R13 и R15 имеет специальную функцию, а R14 может выполнять ее время от времени. Давайте рассмотрим их подробнее:
-R0-R7: младшие регистры одинаковы во всех режимах ЦП.
- R8-R12: старшие регистры одинаковы во всех режимах ЦП, за исключением режима быстрого запроса прерывания (FIQ), недоступного для 16-битных инструкций.
- R13 (также известный как SP): указатель стека — указывает на вершину стека, и каждый режим ЦП имеет свою собственную версию. Не рекомендуется использовать его в качестве GPR.
- R14 (также известный как LR): регистр связи — в пользовательском режиме он содержит адрес возврата для текущей функции, в основном, когда выполняются инструкции BL (переход со связью) или BLX (переход с связью и обменом). Его также можно использовать как GPR, если адрес возврата хранится в стеке. Каждый режим ЦП имеет свою собственную версию.
- R15 (также известный как PC): Счетчик программ, указывает на текущую выполняемую команду. Это не GPR.
Всего на большинстве архитектур ARM имеется 30 32-битных регистров общего назначения, включая экземпляры с одинаковыми именами в разных режимах ЦП.
Помимо них, есть несколько других важных регистров, а именно:
* Регистр состояния текущей программы (CPSR): содержит биты, описывающие текущий режим процессора, состояние процессора и некоторые другие значения.
* Сохраненные регистры состояния программы (SPSR): в них сохраняется значение CPSR при возникновении исключения, поэтому его можно восстановить позже. Каждый режим ЦП имеет свою собственную версию, за исключением пользовательского и системного режимов, поскольку они не являются режимами обработки исключений.
* Регистр состояния прикладной программы (APSR): в нем хранятся копии флагов состояния ALU, также известных как флаги кода состояния, а в более поздних архитектурах он также содержит флаги Q (насыщение) и флаги больше или равно (GE).
Количество регистров с плавающей запятой (FPR) для 32-битной архитектуры может варьироваться, в зависимости от ядра, до 32.
ARMv8 (64-разрядная версия) имеет 31 универсальный X0-X30 (также можно найти нотацию R0-R30) и 32 FPR, доступных в любое время. Нижняя часть каждого регистра имеет префикс W и может быть доступна как W0-W30.
Есть несколько регистров, которые имеют определенную цель, а именно:
ARMv8 определяет четыре уровня исключений (EL0-EL3), и каждый из трех последних регистров получает свою собственную копию каждого из них; ELR и SPSR не имеют отдельной копии для EL0.
Нет регистра с именем X31 или W31; число 31 во многих инструкциях представляет нулевой регистр ZR (WZR/XZR). X29 можно использовать как указатель кадра (в котором хранится исходная позиция в стеке), а X30 — как регистр связи (в котором хранится возвращаемое значение из функций).
Что касается соглашения о вызовах, R0-R3 на 32-разрядном ARM и X0-X7 на 64-разрядном ARM используются для хранения значений аргументов, переданных функциям, а остальные аргументы, при необходимости, передаются через стек, R0-R1 и X0- X7 (и X8, также косвенно известный как XR) для хранения возвращаемых результатов. Если тип возвращаемого значения слишком велик для них, то необходимо выделить пространство и вернуть его в виде указателя. Кроме того, R12 (32-разрядный) и X16-X17 (64-разрядный) могут использоваться в качестве временных регистров внутри вызова процедуры (с помощью так называемых виниров и кода таблицы связывания процедур), R9 (32-разрядный) и X18 (64-разрядная версия) может использоваться в качестве регистров платформы (для конкретных целей ОС), если это необходимо, в противном случае они используются так же, как и другие временные регистры.
Как упоминалось ранее, в соответствии с официальной документацией реализовано несколько режимов ЦП, а именно:
Наборы инструкций
Для процессоров ARM доступно несколько наборов инструкций: ARM и Thumb. Говорят, что процессор, выполняющий инструкции ARM, работает в состоянии ARM, и наоборот. Процессоры ARM всегда запускаются в состоянии ARM, а затем программа может переключиться в состояние Thumb с помощью инструкции BX. Среда выполнения Thumb (ThumbEE) была представлена относительно недавно в ARMv7 и основана на Thumb с некоторыми изменениями и дополнениями для облегчения динамического генерирования кода.
Инструкции ARM имеют длину 32 бита (как для AArch32, так и для AArch64), в то время как инструкции Thumb и ThumbEE имеют длину 16 или 32 бита (первоначально почти все инструкции Thumb были 16-битными, а Thumb-2 представил сочетание 16- и 32-битных инструкций).
Согласно официальной документации все инструкции можно разделить на следующие категории:
Для взаимодействия с ОС к системным вызовам можно получить доступ с помощью инструкции Software Interrupt (SWI), которая позже была переименована в инструкцию Supervisor Call (SVC).
См. официальную документацию ARM, чтобы получить точный синтаксис любой инструкции. Вот пример того, как это может выглядеть:
SVC{условие} #imm
Код {условие} в этом случае будет кодом условия. ARM поддерживает несколько кодов состояния, а именно:
EQ: равно
NE: не равно
CS/HS: установить перенос
CC/LO: сбросить перенос
MI: отрицательный
PL: Положительный или нулевой
VS: переполнение
VC: нет переполнения
HI: выше без знака
LS: беззнаковый ниже или оба
GE: со знаком больше или равно
LT: со знаком менее
GT: со знаком больше
LE: со знаком меньше или равно
AL: Всегда (обычно опускается)
Значение imm означает непосредственное значение.
Основы MIPS
Микропроцессор без взаимосвязанных конвейерных стадий (MIPS) был разработан по технологиям MIPS . Подобно ARM, сначала это была 32-битная архитектура с добавленной позже 64-битной функциональностью. Используя преимущества RISC ISA, процессоры MIPS характеризуются низким энергопотреблением и энергопотреблением. Их часто можно найти в нескольких встроенных системах, таких как маршрутизаторы и шлюзы, а также в некоторых игровых консолях, таких как Sony PlayStation. К сожалению, из-за популярности этой архитектуры системы, они стали мишенью для нескольких семейств вредоносных программ IoT. Пример можно увидеть на следующем скриншоте:
По мере развития архитектуры появилось несколько ее версий, начиная с MIPS I и заканчивая V, а затем несколько выпусков более поздних MIPS32/MIPS64. MIPS64 остается обратно совместимым с MIPS32. Эти базовые архитектуры могут быть дополнительно дополнены необязательными архитектурными расширениями, называемыми Application Specific Extension (ASE), и модулями для повышения производительности для определенных задач, которые обычно мало используются вредоносным кодом. MicroMIPS32/64 — это надмножества архитектур MIPS32 и MIPS64 соответственно с почти таким же 32-битным набором инструкций и дополнительными 16-битными инструкциями для уменьшения размера кода. Они используются там, где требуется сжатие кода, и предназначены для микроконтроллеров и других небольших встраиваемых устройств.
Основы
MIPS поддерживает двунаправленность байтов. Доступны следующие регистры:
*32 GPR r0-r31, 32-битный размер для MIPS32 и 64-битный размер для MIPS64.
* Специальный регистр PC, на который некоторые инструкции могут влиять только косвенно.
* Два специальных регистра для хранения результатов целочисленного умножения и деления (HI и LO). Эти регистры и связанные с ними инструкции были удалены из базового набора инструкций в выпуске 6 и теперь существуют в модуле процессора цифровых сигналов (DSP).
Причина использования 32 GPR проста — MIPS использует 5 бит для указания регистра, поэтому таким образом мы можем иметь максимум 2^5 = 32 различных значения. Два GPR имеют определенную цель, а именно:
* Регистр r0 (иногда называемый $0 или $zero) является постоянным регистром, в нем всегда хранится ноль, и он обеспечивает доступ только для чтения. Его можно использовать как аналог /dev/null для отбрасывания вывода какой-либо операции или как быстрый источник нулевого значения.
* r31 (также известный как $ra) хранит адрес возврата во время инструкций перехода/перехода вызова процедуры и ссылки.
Другие регистры обычно используются для определенных целей, а именно:
* r1 (также известный как $at): временный ассемблер — используется при разрешении псевдоинструкций.
* r2-r3 (также известные как $v0 и $v1): значения — содержат значения возвращаемой функции.
* r4-r7 (также известные как $a0-$a3): Аргументы — используются для передачи аргументов функции.
* r8-r15 (также известные как $t0-$t7/$a4-$a7 и $t4-$t7): временные — первые четыре могут также использоваться для предоставления аргументов функций в соглашениях о вызовах N32 и N64 (еще один вызов O32). соглашение использует только регистры r4-r7; последующие аргументы передаются в стеке)
* r16-r23 (также известные как $s0-$s7): cохраненные временные — сохраняются при вызовах функций.
* r24-r25 (также известные как $t8-$t9): dременные
* r26-r27 (также известный как $k0-$k1): обычно зарезервирован для ядра ОС.
* r28 (также известный как $gp): глобальный указатель — указывает на глобальную область (сегмент данных).
* r29 (также известный как $sp): указатель стека
* r30 (также известный как $s8 или $fp): Сохраненный указатель значения/кадра — сохраняет исходный указатель стека (до вызова функции).
MIPS также имеет следующие доступные сопроцессоры:
* CP0: система управления
*CP1: FPU
* CP2: зависит от реализации
CP3: FPU (имеет специальные инструкции типа кода операции COP1X)
Набор инструкций
Большинство основных инструкций были введены в MIPS I и II. MIPS III представила 64-битные целые числа и адреса, а MIPS IV и V улучшили операции с плавающей запятой и добавили новый набор для повышения общей эффективности. Каждая инструкция там имеет одинаковую длину — 32 бита (4 байта), и любая инструкция начинается с кода операции, который занимает 6 бит. Поддерживаются три основных формата инструкций: R, I и J:
Для операций, связанных с FPU, существуют аналогичные типы FR и FI.
Помимо этого, существует несколько других менее распространенных форматов, в основном сопроцессоры и форматы, связанные с расширениями.
В документации регистры обычно имеют следующие суффиксы:
* Источник(s)
* Цель (t)
* Назначение (d)
Все инструкции можно разделить на следующие несколько групп в зависимости от типа функциональности:
* Поток управления — в основном состоит из условных и безусловных переходов и ответвлений:
- JR: перейти на регистр (формат J)
- BLTZ: переход если меньше нуля (формат I)
* Доступ к памяти — операции загрузки и сохранения:
- LB: загрузить байт (формат I)
- SW: сохранить слово (формат I)
* ALU — охватывает различные арифметические операции:
- ADDU: сложить без знака (формат R)
- XOR: исключающее или (формат R)
- SLL: логический сдвиг влево (формат R)
*Взаимодействие с ОС через исключения — взаимодействует с ядром ОС:
- SYSCALL: системный вызов (пользовательский формат)
- BREAK: точка останова (пользовательский формат)
В большинстве случаев инструкции с плавающей запятой будут иметь одинаковые имена для одних и тех же типов операций, например, ADD.S. Некоторые инструкции более уникальны, например Check for Equal (C.EQ.D).
Как мы увидим здесь и далее, одни и те же базовые группы можно применить практически к любой архитектуре, и единственная разница будет заключаться в реализации. Некоторые общие операции могут получить свои собственные инструкции, чтобы извлечь выгоду из оптимизации и, таким образом, уменьшить размер кода и повысить производительность.
Поскольку набор инструкций MIPS довольно минималистичный, также существуют макросы ассемблера, называемые псевдоинструкциями. Вот некоторые из наиболее часто используемых:
* ABS: абсолютное значение — преобразуется в комбинацию ADDU, BGEZ и SUB.
* BLT: переходи если меньше чем — переводится как комбинация SLT и BNE.
* BGT/BGE/BLE: аналогично BLT
* LI/LA: немедленная загрузка/адрес — преобразуется в комбинацию LUI и ORI или ADDIU для 16-битного LI.
* MOVE: перемещает содержимое одного регистра в другой — преобразует в ADD/ADDIU с нулевым значением.
* NOP: нет операции — преобразуется в SLL с нулевыми значениями.
* NOT: логическое NOT — преобразуется в NOR.
Погружение в PowerPC
PowerPC расшифровывается как «Оптимизация производительности с помощью Enhanced RISC — Performance Computing» и иногда пишется как PPC. Он был создан в начале 1990-х годов альянсом Apple, IBM и Motorola (обычно сокращенно AIM). Первоначально он предназначался для использования в ПК и до 2006 года использовался в продуктах Apple, включая PowerBook и iMac. Процессоры, реализующие его, также можно найти в игровых консолях, таких как Sony PlayStation 3, XBOX 360 и Wii, а также в серверах IBM и множестве встроенных устройств, таких как контроллеры автомобилей и самолетов, и даже в знаменитом роботе ASIMO. Позже административные обязанности были переданы органу открытых стандартов Power.org, членами которого остались некоторые из бывших создателей, такие как IBM и Freescale. Затем они отделились от Motorola и позже были приобретены NXP Semiconductors, а также многими другими компаниями. OpenPOWER Foundation — это новая инициатива IBM, Google, NVIDIA, Mellanox и Tyan, целью которой является содействие совместной разработке этой технологии.
PowerPC был в основном основан на IBM POWER ISA, а позже была выпущена унифицированная Power ISA, которая объединила POWER и PowerPC в единую ISA, которая теперь используется во многих продуктах под общим термином Power Architecture.
Существует множество семейств вредоносных программ для Интернета вещей, которые имеют полезную нагрузку для этой архитектуры.
Основы
Power ISA делится на несколько категорий; каждую категорию можно найти в определенной части спецификации или книги. ЦП реализуют набор этих категорий в зависимости от своего класса; только базовая категория является обязательной. Вот список основных категорий и их определений в последнем втором стандарте:
*База: описана в Книге I (Архитектура набора пользовательских инструкций Power ISA) и Книге II (Архитектура виртуальной среды Power ISA)
*Сервер: описан в Книге III-S (Архитектура операционной среды Power ISA — Серверная среда)
* Ембедед: Книга III-E (Архитектура операционной среды Power ISA — встроенная среда)
Существует множество более подробных категорий, охватывающих такие аспекты, как операции с плавающей запятой и кэширование определенных инструкций.
В другой книге, Book VLE (Архитектура операционной среды Power ISA — Архитектура инструкций кодирования переменной длины (VLE)), определяются альтернативные инструкции и определения, предназначенные для увеличения плотности кода за счет использования 16-битных инструкций в отличие от более распространенных 32-битных. те.
Power ISA версии 3 состоит из трех книг с теми же названиями, что и книги с I по III предыдущего стандарта, без различий между средами.
Процессор запускается в режиме с обратным порядком байтов, но может переключаться, изменяя бит в MSR (регистре состояния машины), так что поддерживается двунаправленный порядок байтов.
В Power ISA задокументировано множество наборов регистров, в основном сгруппированных либо по соответствующему объекту, либо по категории. Вот краткое изложение наиболее часто используемых:
* 32 GPR для операций с целыми числами, обычно используются только по их количеству (64-битные)
* 64 векторных скалярных регистра (VSR) для векторных операций и операций с плавающей запятой:
- 32 векторных регистра (VR) как часть VSR для векторных операций (128 бит)
- 32 FPR в составе VSR для операций с плавающей запятой (64-бит)
* Регистры средств фиксированной точки специального назначения, такие как следующие:
- Регистр исключений с фиксированной запятой (XER) — содержит несколько битов состояния (64 бита).
* Регистры переходов:
- Регистр условий (CR) — состоит из 8 4-битных полей, CR0-CR7, включая такие вещи, как поток управления и сравнение (32-битные)
- Link регистр (LR) — обеспечивает целевой адрес перехода (64-битный)
- Регистр счетчика (CTR) — содержит счетчик циклов (64-разрядный).
- Целевой регистр доступа (TAR) — указывает целевой адрес ветки (64-разрядный).
* Регистры объекта таймера:
- Time Base (TB) — периодически увеличивается с заданной частотой (64-бит)
* Другие регистры специального назначения из определенной категории, в том числе следующие:
- Аккумулятор (ACC) (64-разрядный) — категория механизма обработки сигналов (SPE).
Как правило, функции могут передавать все аргументы в регистры для нерекурсивных вызовов; дополнительные аргументы передаются в стек.
Набор инструкций
Большинство инструкций имеют 32-битный размер, только группа кодирования переменной длины (VLE) меньше, чтобы обеспечить более высокую плотность кода для встраиваемых приложений. Все инструкции разбиты на следующие три категории:
* Определено: все инструкции определены в книгах Power ISA.
* Недопустимо: Доступно для будущих расширений Power ISA. Попытка выполнить их вызовет обработчик ошибок недопустимых инструкций.
* Зарезервировано: предназначено для определенных целей, которые не входят в сферу действия Power ISA. Попытка выполнить их либо выполнит реализованное действие, либо вызовет обработчик ошибок недопустимой инструкции, если реализация недоступна.
Биты с 0 по 5 всегда определяют код операции, и многие инструкции также имеют расширенный код операции. Поддерживается большое количество форматов инструкций; Вот некоторые примеры:
* I-ФОРМА [OPCD+LI+AA+LK]
* B-ФОРМА [OPCD+BO+BI+BD+AA+LK]
Каждое поле инструкций имеет собственное сокращение и значение; имеет смысл обратиться к официальному документу Power ISA, чтобы получить их полный список и соответствующие им форматы. В случае ранее упомянутой I-FORM они таковы:
* OPCD: код операции
* LI: непосредственное поле, используемое для указания 24-битного целого числа в дополнении до двух со знаком.
* AA: Бит абсолютного адреса
* LK: Бит ссылки, влияющий на регистр ссылки
Инструкции также разделены на группы в соответствии с соответствующим средством и категорией, что делает их очень похожими на регистры:
* Инструкции ветвления:
- b/ba/bl/bla: ветвление
- bc/bca/bcl/bcla: условное ветвление
- sc: системный вызов
* Инструкции с фиксированной точкой:
- lbz: загрузить байт и ноль
- stb: сохранить байт
- addi: добавить немедленно
- ori: или немедленно
* Инструкции с плавающей запятой:
- fmr: Floating move register
- lfs: загрузить одно число с плавающей запятой
- stfd: сохранить двойное число с плавающей запятой
* Инструкции SP:
- brinc: приращение с обратным битом
Ассемблер SuperH
SuperH, часто сокращенно обозначаемый как SH, представляет собой RISC ISA, разработанный Hitachi. SuperH прошел несколько итераций, начиная с SH-1 и заканчивая SH-4. Более поздний SH-5 имеет два режима работы, один из которых идентичен инструкциям пользовательского режима SH-4, а другой, SHmedia, совершенно другой. Каждое семейство занимает свою рыночную нишу:
* SH-1: Бытовая техника
* SH-2: Автомобильные контроллеры и игровые приставки, такие как Sega Saturn.
* SH-3: Мобильные приложения, такие как автомобильные навигаторы
* SH-4: Автомобильные мультимедийные терминалы и игровые приставки, такие как Sega Dreamcast.
* SH-5: Мультимедийные приложения высокого класса.
Микроконтроллеры и процессоры, реализующие его, в настоящее время производятся Renesas Electronics, совместным предприятием групп Hitachi и Mitsubishi Semiconductor. Поскольку вредоносное ПО IoT в основном нацелено на системы на базе SH-4, мы сосредоточимся на этом семействе SuperH.
Основы
Что касается регистров, SH-4 предлагает следующее:
* 16 регистров общего назначения R0-R15 (32-разрядные)
* 7 регистров управления (32-разрядные):
- Глобальный базовый регистр (GBR)
- Регистр состояния (SR)
- Сохраненный регистр состояния (SSR)
- Сохраненный счетчик программ (SPC)
- Векторный базовый счетчик (VBR)
- Сохраненный общий регистр 15 (SGR)
- Debug Base Register (DBR) (только из привилегированного режима)
* 4 системных регистра (32-битные):
- MACH/MACL: регистры умножения и накопления
- PR: регистр процедур
- PC: счетчик программ
- FPSCR: Регистр состояния/управления с плавающей запятой
* 32 регистра FPU FR0-FR15 (также известные как DR0/2/4/... или FV0/4/...) и XF0-XF15 (также известные как XD0/2/4/... или XMTRX); два банка по 16 одинарной точности (32-разрядные) или восемь с двойной точностью (64-разрядные) FPR и FPUL (регистр связи с плавающей запятой) (32-разрядный)
Обычно R4-R7 используются для передачи аргументов функции, результат которой возвращается в R0. R8-R13 сохраняются для нескольких вызовов функций. R14 служит указателем кадра, а R15 — указателем стека.
Что касается форматов данных, в SH-4 слово занимает 16 бит, длинное слово — 32 бита, а учетверенное слово — 64 бита.
Поддерживаются два режима процессора: пользовательский режим и привилегированный режим. SH-4 обычно работает в пользовательском режиме и переключается в привилегированный режим в случае исключения или прерывания.
Набор инструкций
SH-4 имеет набор инструкций, который совместим снизу вверх с семействами SH-1, SH-2 и SH-3. Он использует 16-битные инструкции фиксированной длины, чтобы уменьшить размер программного кода. За исключением BF и BT, все инструкции ветвления и RTE (инструкция возврата из исключения) реализуют так называемые отложенные ветвления, когда инструкция, следующая за ветвью, выполняется до инструкции назначения ветвления.
Все инструкции разделены на следующие категории (с некоторыми примерами):
* Инструкции по передаче с фиксированной точкой:
- MOV: перемещение данных (или указанных определенных типов данных)
- SWAP: обмен местами половинки регистра
* Инструкции по арифметическим операциям:
- SUB: вычесть двоичные числа
- CMP/EQ: условное сравнение (в данном случае на равное)
* Инструкции по логической операции:
- AND: логический и
- XOR: исключающее ИЛИ
* Инструкции сдвига:
- ROTL: сдвиг влево
- SHLL: логический сдвиг влево
* инструкции ветвления:
- BF: перейти, если ложь
- JMP: перейти (безусловная ветвь)
* Инструкции по управлению системой:
- LDC: загрузить в управляющий регистр
- STS: сохранить в системный реестр
* Инструкции одинарной точности с плавающей запятой:
- FMOV: перемещение с плавающей запятой
* Инструкции двойной точности с плавающей запятой:
- FABS: абсолютное значение с плавающей запятой
* Инструкции управления с плавающей запятой:
- LDS: загрузить в системный регистр FPU
Работа со SPARC
Архитектура масштабируемого процессора (SPARC) — это RISC ISA, первоначально разработанная Sun Microsystems (теперь часть корпорации Oracle). Первая реализация использовалась в собственных рабочих станциях и серверных системах Sun. Позже он был лицензирован для нескольких других производителей, одним из которых была Fujitsu. Поскольку Oracle закрыла SPARC Design в 2017 году, все будущие разработки продолжались с Fujitsu в качестве основного поставщика серверов SPARC.
Существует несколько реализаций архитектуры SPARC с полностью открытым исходным кодом. В настоящее время его поддерживают несколько операционных систем, включая системы Oracle Solaris, Linux и BSD, а для нескольких семейств вредоносных программ IoT также есть специальные модули.
Основы
Согласно документации по архитектуре Oracle SPARC, конкретная реализация может содержать от 72 до 640 64-битных регистров R общего назначения. Однако одновременно видны только 31/32; 8 — это глобальные регистры, от R[0] до R[7] (также известные как g0-g7), причем первый регистр, g0, жестко привязан к 0; и 24 связаны со следующими окнами регистров:
* Восемь в регистрах in[0]-in[7] (R[24]-R[31]): для передачи аргументов и возврата результатов
* Восемь локальных регистров local[0]-local[7] (R[16]-R[23]): для хранения локальных переменных
* Восемь выходных регистров out[0]-out[7] (R[8]-R[15]): для передачи аргументов и возврата результатов
Инструкция CALL записывает свой адрес в регистр out[7] (R[15]).
Чтобы передать аргументы функции, они должны быть помещены в регистры out и, когда функция получит управление, она будет обращаться к ним в своих регистрах in. Дополнительные аргументы могут быть предоставлены через стек. Результат помещается в первый входной регистр, который затем становится первым выходным регистром, когда функция возвращается. Инструкции SAVE и RESTORE используются для выделения нового окна регистра и последующего восстановления предыдущего соответственно.
SPARC также имеет 32 FPR с одинарной точностью (32-битные), 32 FPR с двойной точностью (64-битные) и 16 FPR с четырехкратной точностью (128-битные), некоторые из которых перекрываются.
Помимо этого, существует множество других регистров, которые служат конкретным целям, в том числе следующие:
* FPRS: содержит информацию о режиме и состоянии FPU.
* Вспомогательные регистры состояния (ASR 0, ASR 2-6, ASR 19-22 и ASR 24-28 не зарезервированы): служат нескольким целям, включая следующие:
- ASR 2: Регистр кодов состояния (CCR)
- ASR 5: PC
- ASR 6: FPRS
- ASR 19: Регистр общего состояния (GSR)
* Регистры состояния PR окна регистров (PR 9-14): определяют состояние окон регистров, включая следующее:
- PR 9: Указатель текущего окна (CWP)
- PR 14: Состояние окна (WSTATE)
* Регистры состояния PR без регистрации окна (PR 0–3, PR 5–8 и PR 16): видны только программному обеспечению, работающему в привилегированном режиме.
32-разрядный SPARC использует обратный порядок байтов, а 64-разрядный SPARC использует инструкции с обратным порядком байтов, но может обращаться к данным в любом порядке. В SPARC также используется понятие ловушек, реализующих передачу управления привилегированному программному обеспечению с использованием специальной таблицы, которая может содержать первые 8 инструкций (32 для некоторых часто используемых ловушек) каждого обработчика ловушек. Базовый адрес таблицы устанавливается программным обеспечением в регистре базового адреса прерывания (TBA).
Набор инструкций
Команда из ячейки памяти, указанной ПК, извлекается и выполняется, а затем новые значения присваиваются ПК и счетчику следующей программы (NPC), который является псевдорегистром.
Подробные форматы инструкций можно найти в описаниях отдельных инструкций.
Вот основные категории поддерживаемых инструкций с примерами:
* Доступ к памяти:
- LDUB: загрузить беззнаковый байт
- ST: сохранить
* Арифметические/логические/сдвиг:
- ADD: добавить
- SLL: логический сдвиг влево
* Передача управления:
- BE: ветвление если равно
- JMPL: переход
- CALL: вязов
- RETURN: Возврат из функции
* Доступ к регистру статуса:
- WRCCR: запись CCR
* Операции с плавающей запятой:
- FOR: логическое ИЛИ для F-регистров
* Условнное перемещение:
- MOVcc: переместить, если условие истинно для выбранного кода условия (cc)
* Регистр управления окнами:
- SAVE: сохранить окно вызова
- FLUSHW: очистить окна регистров
Переход от ассемблера к языкам программирования высокого уровня
Разработчики в основном не пишут на ассемблере. Вместо этого они пишут на языках более высокого уровня, таких как C или C++, и компилятор преобразует этот высокоуровневый код в низкоуровневое представление на языке ассемблера. В этом разделе мы рассмотрим различные блоки кода, представленные в сборке.
Арифметические операторы
Теперь мы рассмотрим различные операторы C и то, как они представлены в ассемблере. В качестве примера мы возьмем Intel IA-32, и та же концепция применима и к другим языкам ассемблера:
* X = 50 (при условии, что 0x00010000 — это адрес переменной X в памяти):
mov eax, 50
mov dword ptr [00010000h],eax
X = Y+50 (при условии, что 0x00010000 представляет X, а 0x00020000 представляет Y):
mov eax, dword ptr [00020000h]
add eax, 50
mov dword ptr [00010000h],eax
X = Y + (50 * 2):
mov eax, dword ptr [00020000h]
push eax ;save Y for now
mov eax, 50 ;do the multiplication first
mov ebx,2
imul ebx ;the result is in edx:eax
mov ecx, eax
pop eax ;gets back Y value
add eax,ecx
mov dword ptr [00010000h],eax
X = Y + (50 / 2):
mov eax, dword ptr [00020000h]
push eax ;save Y for now
mov eax, 50
mov ebx,2
div ebx ;the result in eax, and the remainder is in edx
mov ecx, eax
pop eax
add eax,ecx
mov dword ptr [00010000h],eax
X = Y + (50 % 2)
mov eax, dword ptr [00020000h]
push eax ;save Y for now
mov eax, 50
mov ebx,2
div ebx ;the remainder is in edx
mov ecx, edx
pop eax
add eax,ecx
mov dword ptr [00010000h],eax
Будем надеяться, что это объясняет, как компилятор преобразует эти арифметические выражения в язык ассемблера.
Условия
Основные операторы If могут выглядеть следующим образом:
* if (X == 50) (при условии, что 0x0001000 представляет переменную X):
mov eax, 50
cmp dword ptr [00010000h],eax
* if (X | 00001000b) (| представляет логический элемент ИЛИ):
mov eax, 000001000b
test dword ptr [00010000h],eax
Чтобы понять ветвление и перенаправление потока, давайте взглянем на следующую диаграмму, чтобы увидеть, как это проявляется в псевдокоде:
Чтобы применить эту последовательность ветвления в ассемблере, компилятор использует сочетание условных и безусловных переходов, как показано ниже:
IF.. THEN.. ENDIF:
cmp dword ptr [00010000h],50
jnz 3rd_Block ; if not true
…
Some Code
…
3rd_Block:
Some code
IF.. THEN.. ELSE.. ENDIF:
cmp dword ptr [00010000h],50
jnz Else_Block ; if not true
...
Some code
...
jmp 4th_Block ;Jump after Else
Else_Block:
...
Some code
...
4th_Block:
...
Some code
Цикл while
Цикла while очень похожи на условия if с точки зрения того, как они представлены в ассемблере:
Краткое содержание
В этой главе мы рассмотрели основы компьютерного программирования и описали универсальные элементы, общие для нескольких архитектур CISC и RISC. Затем мы рассмотрели несколько языков ассемблера, в том числе языки, лежащие в основе Intel x86, ARM, MIPS и другие, и поняли области их применения, которые в конечном итоге сформировали дизайн и структуру. Мы также рассмотрели фундаментальные основы каждого из них, изучили наиболее важные понятия (такие как используемые регистры и поддерживаемые режимы процессора), получили представление о том, как выглядят наборы инструкций, узнали, какие форматы кодов операций поддерживаются, и изучили, какие вызовы используются соглашения. Наконец, мы перешли от языков ассемблера низкого уровня к их высокоуровневому представлению в C или других подобных языках и познакомились с набором примеров для универсальных блоков, таких как условия if и циклы.
После прочтения этой главы вы должны научиться читать дизассемблированный код различных языков ассемблера и понять, какой высокоуровневый код он может представлять. Не претендуя на полноту охвата, основная цель этой главы — предоставить прочную основу, а также направление, которому вы можете следовать, чтобы углубить свои знания, прежде чем приступать к анализу фактического вредоносного кода. Это должно стать вашей отправной точкой для изучения того, как выполнять статический анализ кода на разных платформах и устройствах.
В главе 2 «Базовый статический и динамический анализ для x86/x64» мы начнем анализ реального вредоносного ПО для конкретных платформ, а наборы инструкций, с которыми мы познакомились, будут использоваться в качестве языков, описывающих его функциональность.